// Generated, do not edit. sha256:79db5d8cc5706ad987b207d98e1c67ca389284a24121f65b199acbe48bce76ec const GRAMS_PER_TROY_OUNCE = 31.1034768; export const CONFIG = Object.freeze({ PRICE_SOURCES: Object.freeze({ MEMPOOL_HISTORICAL: '/api/v1/historical-price', MEMPOOL_PRICES: '/api/v1/prices', MEMPOOL_PUBLIC_BASE: 'https://mempool.space', BITVAVO_BASE: 'https://api.bitvavo.com', BITVAVO_CANDLES: '/v2/{market}/candles', BITVAVO_TICKER: '/v2/ticker/price', BITVAVO_ACCOUNT: '/v2/account', COINGECKO_BASE: 'https://api.coingecko.com/api/v3', COINGECKO_PRICE: '/simple/price', COINGECKO_CHART: '/coins/{id}/market_chart', COINGECKO_PING: '/ping', GOLDAPI_BASE: 'https://www.goldapi.io', GOLDAPI_SPOT: '/api/{metal}/{currency}', GOLDAPI_HISTORICAL: '/api/{metal}/{currency}/{date}', EULERPOOL_BASE: 'https://api.eulerpool.com', EULERPOOL_QUOTES: '/api/1/{external_id}/quotes/{symbol}', METALSDEV_BASE: 'https://api.metals.dev', METALSDEV_SPOT: '/v1/metal/spot', METALSDEV_TIMESERIES: '/v1/timeseries', FRANKFURTER_BASE: 'https://api.frankfurter.dev', FRANKFURTER_LATEST: '/v1/latest', FRANKFURTER_SERIES: '/v1/1999-01-04..', }), CHART_LIBS: Object.freeze({ CHARTJS: 'vendor/chartjs/chart.umd.js', HAMMER: 'vendor/hammerjs/hammer.min.js', ZOOM: 'vendor/chartjs-plugin-zoom/chartjs-plugin-zoom.min.js', }), SCHEMA_VERSION: 128, MAX_ASSET_DECIMALS: 9, LOCALE_TAGS: Object.freeze({ nl: 'nl-NL', en: 'en-US' }), INSTALLATION: Object.freeze({ SETTINGS_FILE_NAME: 'installation.json', DEFAULT_LOCALE: 'en' }), METALS: Object.freeze({ BASE_UNIT: 'g', BASE_DECIMALS: 7, GRAMS_PER_TROY_OUNCE, DISPLAY_DECIMALS: 4, UNITS: Object.freeze({ g: 1, kg: 1000, ozt: GRAMS_PER_TROY_OUNCE }), SPOT_DEVIATION_WARN: 0.10, }), DB_TABLES: Object.freeze([ 'Asset', 'Account', 'Event', 'Valuation', 'Label', 'WalletSource', 'WalletAddress', 'WalletTransaction', 'ScanLog', 'ReconcileProposal', 'ImportBatch', 'PriceHistory', 'WalletTransactionEventLink', 'InterestSchedule', 'InterestPeriod', 'InterestCredit', 'Notification', 'MetalProduct', 'Provider', 'ProviderAuthenticationHmac', 'ProviderEndpoint', 'ProviderFixedRate', 'ProviderCallLimit', 'ProviderInputMap', 'ProviderOutputMap', 'ProviderCoverage', 'ApiCallMetric', 'PriceSource', 'Connection', 'DashboardProfile', 'DashboardWidget', 'AiToolCall', 'AiToolCallMessage', 'BusSignal', 'Command', 'AiProposal', 'Preference', 'Schema', 'ChangeLog', 'HygieneAcceptance', 'IgnoredPriceGap', 'AssetNote', 'Portfolio', 'PriceAbsence', 'IntegrationCall', ]), DB_DELETABLE_TABLES: Object.freeze([ 'ImportBatch', 'PriceHistory', 'ReconcileProposal', 'WalletTransactionEventLink', 'Valuation', 'WalletAddress', 'InterestCredit', 'Notification', 'PriceSource', 'ProviderCoverage', 'Preference', 'BusSignal', 'PriceAbsence', 'IntegrationCall', ]), DB_HISTORY: Object.freeze({ TRACKED: Object.freeze([ 'Account', 'Asset', 'Event', 'Valuation', 'Label', 'MetalProduct', 'InterestSchedule', 'InterestPeriod', 'InterestCredit', 'WalletSource', 'Connection', ]), UNTRACKED: Object.freeze({ AiToolCall: 'logboek', AiToolCallMessage: 'logboek', BusSignal: 'logboek', Command: 'logboek', ScanLog: 'logboek', ApiCallMetric: 'logboek', Notification: 'logboek', WalletTransaction: 'scanketen', WalletAddress: 'scanketen', WalletTransactionEventLink: 'scanketen', PriceHistory: 'volume', PriceAbsence: 'volume', AiProposal: 'herkomst', ReconcileProposal: 'herkomst', ImportBatch: 'herkomst', Provider: 'configuratie', ProviderAuthenticationHmac: 'configuratie', ProviderEndpoint: 'configuratie', ProviderFixedRate: 'configuratie', ProviderCallLimit: 'configuratie', ProviderInputMap: 'configuratie', ProviderOutputMap: 'configuratie', ProviderCoverage: 'configuratie', PriceSource: 'configuratie', DashboardProfile: 'weergave', DashboardWidget: 'weergave', Preference: 'weergave', Schema: 'systeem', Portfolio: 'systeem', ChangeLog: 'logboek', IntegrationCall: 'logboek', HygieneAcceptance: 'weergave', IgnoredPriceGap: 'weergave', AssetNote: 'weergave', }), }), PEILDATUM: Object.freeze({ MONTH: 1, DAY: 1, HOUR: 0, MINUTE: 0, TIMEZONE: 'Europe/Amsterdam', }), START_YEAR: Object.freeze({ AUTO: 0, MIN: 1900, MAX: 2200, }), WEALTH_RETURN: Object.freeze({ DUST_SHARE: 0.005, REOPEN_MULTIPLE: 10, ROLLING_DAYS: Object.freeze([1, 7, 30, 365]) }), AUTO_BLUR: Object.freeze({ PRESETS_MIN: Object.freeze([5, 15, 30]), NEVER_MIN: 525600, }), GLOW_TEMPO: Object.freeze({ brisk: Object.freeze({ MOVE_S: 11, PAUSE_MIN_S: 18, PAUSE_MAX_S: 40, ALWAYS_MULT: 1 }), calm: Object.freeze({ MOVE_S: 24, PAUSE_MIN_S: 45, PAUSE_MAX_S: 90, ALWAYS_MULT: 2 }), slow: Object.freeze({ MOVE_S: 36, PAUSE_MIN_S: 80, PAUSE_MAX_S: 140, ALWAYS_MULT: 3 }), }), GLOW_CORE_CHANCE: 0.35, ATTENTION_RING: Object.freeze({ ROUND_MS: 3500, ROUNDS: 3 }), TIMERS: Object.freeze({ OFF_SECONDS: 31536000, SPOT_REFRESH: Object.freeze({ MIN_SECONDS: 60, MAX_SECONDS: 3600, PRESETS_SECONDS: Object.freeze([60, 300, 900]), }), BUS_POLL: Object.freeze({ MIN_SECONDS: 5, MAX_SECONDS: 300, PRESETS_SECONDS: Object.freeze([5, 15, 60]), }), }), PRICE_HISTORY: Object.freeze({ NEAREST_MAX_SECONDS: 60, CARRY_FORWARD_DAYS: Object.freeze({ crypto: 7, metal: 14, fiat: null }), CLOSED_MARKET_DAYS: Object.freeze({ crypto: 0, metal: 4, fiat: 4 }), WEEKEND_PRICE: Object.freeze({ crypto: true, metal: false, fiat: false }), RECENT_CLOSE_DAYS: 14, FX_MAX_AGE_DAYS: 7, }), API_USAGE: Object.freeze({ WARN_SHARE: 0.8 }), HYGIENE: Object.freeze({ PRICE_GAP_DAYS: 7, MAX_ROWS_PER_FINDING: 200, OUTLIER_FACTOR: 3, TROY_OUNCE_TOLERANCE: 0.2, SOURCES_DISAGREE_SHARE: 0.08, BOOKING_PRICE_DEVIATION: 0.2, }), BUS: Object.freeze({ RETENTION_HOURS: 24, }), INTEGRATION_LOG: Object.freeze({ RETENTION_DAYS: 90, MAX_ROWS: 10_000, DETAIL_MAX_CHARS: 300, WINDOW_DAYS: 7, AI_MAX_ROWS: 20, }), COMMANDS: Object.freeze({ CLAIM_SECONDS: 20, COUNTDOWN_SECONDS: 5, NOTICE_SECONDS: 3, RETENTION_HOURS: 24, }), BACKUP: Object.freeze({ REMIND_AFTER_DAYS: 14, }), HEALTH_CHECK: Object.freeze({ REMIND_AFTER_DAYS: 30, }), AUTH: Object.freeze({ SESSION_TTL_MS: 12 * 60 * 60 * 1000, COOKIE_NAME: 'assetapp_session', LOGIN_PATH: '/login', LOGIN_ATTEMPTS: Object.freeze({ FREE: 3, BASE_LOCK_MS: 2000, MAX_LOCK_MS: 5 * 60 * 1000, }), }), ATTENTION: Object.freeze({ FADE_AFTER_DAYS: 1, HIDE_AFTER_DAYS: 2, }), FOCUS: Object.freeze({ INTEREST_ACCOUNT: 'interest.focusAccount', ROW: 'focus.row', ROW_TTL_MS: 30_000, }), LICENSE: Object.freeze({ FILE_NAME: 'license.json', FREE_FILE_NAME: 'free-license.json', BUYER_FILE_NAME: 'buyer.json', SUPPORT: Object.freeze({ ATTACHMENT_TYPES: Object.freeze(['image/png', 'image/jpeg', 'image/webp']), MAX_ATTACHMENT_BYTES: 2 * 1024 * 1024, RECIPIENT: '72304ea4d273d6c685aaa76342975e7a001190c9f15369ed3d3b303b79c8c74c', RECIPIENT_NAME: 'Atreides Assets', RELAYS: Object.freeze(['wss://nos.lol']), MAX_TEXT: 4000, MAX_EVENT_BYTES: 64 * 1024, PUBLISH_TIMEOUT_MS: 10_000, }), INFO_LINKS: Object.freeze([ Object.freeze({ id: 'nostrHow', url: 'https://nostr.how/' }), Object.freeze({ id: 'nostrOrg', url: 'https://nostr.org/' }), Object.freeze({ id: 'alby', url: 'https://getalby.com/alby-extension' }), Object.freeze({ id: 'nos2x', url: 'https://github.com/fiatjaf/nos2x' }), Object.freeze({ id: 'lightning', url: 'https://lightning.network/' }), ]), PAY_APPS: Object.freeze([ Object.freeze({ id: 'phoenix', name: 'Phoenix', group: 'easy', url: 'https://phoenix.acinq.co/' }), Object.freeze({ id: 'pocket', name: 'Pocket', group: 'easy', url: 'https://pocketbitcoin.com/lightning' }), Object.freeze({ id: 'strike', name: 'Strike', group: 'other', url: 'https://strike.me/' }), Object.freeze({ id: 'zeus', name: 'Zeus', group: 'other', url: 'https://zeusln.com/' }), Object.freeze({ id: 'bringin', name: 'Bringin', group: 'other', url: 'https://bringin.app/' }), ]), FEATURE_IMAGES: 'img/features', FEATURE_GROUPS: Object.freeze(['overview', 'records', 'prices', 'app', 'bitcoin']), FEATURES: Object.freeze([ { id: 'dashboard', group: 'overview', free: null }, { id: 'wealth', group: 'overview', free: null }, { id: 'returns', group: 'overview', free: null }, { id: 'taxes', group: 'overview', has: 'tax' }, { id: 'balances', group: 'records', free: null }, { id: 'interest', group: 'records', free: null }, { id: 'import', group: 'records', has: 'import' }, { id: 'data', group: 'records', has: 'data' }, { id: 'health', group: 'records', has: 'health' }, { id: 'metals', group: 'records', free: false }, { id: 'priceHistory', group: 'prices', free: null }, { id: 'sourceChoice', group: 'prices', has: 'sourceMatrix' }, { id: 'providerKeys', group: 'prices', has: 'providerKeys' }, { id: 'portfolios', group: 'app', has: 'portfolios' }, { id: 'ai', group: 'app', has: 'ai' }, { id: 'kiosk', group: 'app', has: 'kiosk' }, { id: 'tracking', group: 'bitcoin', free: null }, { id: 'lightning', group: 'bitcoin', has: 'lightning' }, { id: 'ownMempool', group: 'bitcoin', has: 'ownMempool' }, { id: 'signals', group: 'bitcoin', has: 'signals' }, { id: 'netshare', group: 'bitcoin', has: 'netshare' }, ].map(f => Object.freeze(f))), SELLER_KEY: 'cfc6e67daaf080abbdcbacdbca587d54a3c18b119f2f05d7b087b859caa96a2f', FORMAT: 1, FORMATS: Object.freeze([1, 2]), MESSAGE_PREFIX: 'assetapp-license-v1', ISSUER_PREFIX: 'assetapp-issuer-v1', UNLIMITED: 'unlimited', LEVELS: Object.freeze(['full', 'free']), FREE: Object.freeze({ MAX: Object.freeze({ assets: 1, fiatAccounts: 1, portfolios: 1, valuations: 1, }), ASSETS: Object.freeze(['BTC']), HAS: Object.freeze({ ai: true, sourceMatrix: false, lightning: false, tax: false, providerKeys: false, ownMempool: false, signals: false, netshare: false, portfolios: false, import: false, data: false, health: false, kiosk: false, }), }), }), EDITION: Object.freeze({ CLIENT: Object.freeze({ entry: 'client/edition.js', starts: Object.freeze(['client/app.js', 'client/boot-gate.js', 'client/login.js']), }), SERVER: Object.freeze({ entry: 'server/edition.mjs', starts: Object.freeze(['server/index.mjs', 'server/supervisor.mjs', 'server/mcp/stdio.mjs']), }), }), PORTFOLIO: Object.freeze({ RESTART_EXIT_CODE: 75, DEFAULT_BASE_CURRENCY: 'EUR', BASE_CURRENCIES: Object.freeze([ 'AUD', 'BRL', 'CAD', 'CHF', 'CNY', 'CZK', 'DKK', 'EUR', 'GBP', 'HKD', 'HUF', 'IDR', 'ILS', 'INR', 'ISK', 'JPY', 'KRW', 'MXN', 'MYR', 'NOK', 'NZD', 'PHP', 'PLN', 'RON', 'SEK', 'SGD', 'THB', 'TRY', 'USD', 'ZAR', ]), ACTIVE_KEY: 'portfolio_active', SWITCH_KEY: 'portfolio_switching', }), SPLASH: Object.freeze({ MIN_MS: 3200, FADE_MS: 900, QUOTE_START_MS: 1800, QUOTE_CHAR_MS: 40, QUOTE_WORD_PAUSE_MS: 110, }), CONNECTIONS: Object.freeze({ TEST_TIMEOUT_MS: 8000, RESULT_HOLD_MS: 6000, RESULT_FADE_MS: 600, }), NWC: Object.freeze({ REQUEST_TIMEOUT_MS: 15000, CLIENT_TIMEOUT_MS: 20000, }), LND: Object.freeze({ REQUEST_TIMEOUT_MS: 15000, CLIENT_TIMEOUT_MS: 20000, CHANNEL_LABEL: 'Lightning-kanaal', }), MARKET_SERVICE: Object.freeze({ SETTINGS_FILE_NAME: 'market-service.json', URL: 'https://atreides.kamenier-hamer.nl', APP_NAME: 'Atreides Services', REQUEST_TIMEOUT_MS: 10000, CLIENT_TIMEOUT_MS: 15000, REGISTER_TTL_MS: 10 * 60_000, MAX_SERIES_PAGES: 10, INTRADAY_MAX_DAYS: 7, MOMENT_WINDOW_MS: 10 * 60_000, HISTORY_MAX_DAYS: 3650, PROVIDER_KEY: 'atreides_services', SPOT_TTL_MS: 30_000, RETRY_PAUSE_MS: 5 * 60_000, MAX_INSTRUMENTS_PER_SPOT: 100, }), AI: Object.freeze({ SERVER_NAME: 'assetapp', SERVER_VERSION: '3', PROTOCOL_VERSION: '2025-06-18', AUDIT_PAGE_SIZE: 100, PROPOSAL_PAGE_SIZE: 50, DESTRUCTIVE_PROPOSAL_KINDS: Object.freeze(['account_delete']), FIELD_CLASSES: Object.freeze(['plain', 'rate', 'quantityFiat', 'quantity', 'handle', 'chain', 'drop']), BUCKET_LADDERS: Object.freeze({ COARSE: Object.freeze([1, 1.5, 2, 3, 5, 7]), FINE: Object.freeze([1, 1.25, 1.6, 2, 2.5, 3.15, 4, 5, 6.3, 8]), }), BUCKET_MIN_VALUES: Object.freeze({ COARSE: 1000, FINE: 0 }), SHARE_BUCKETS: Object.freeze([2, 4, 6, 8, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, 100]), SHARED_PREF_SCOPES: Object.freeze(['app', 'timers']), PROPOSABLE_PREF_SCOPES: Object.freeze(['app', 'timers']), LIMITS: Object.freeze({ MAX_ROWS_PER_LIST: 50, MAX_TEXT_CHARS: 10000, MAX_LOG_CHARS: 10000, METRICS_WINDOW_HOURS: 24, }), }), ROW_GUARD: false, }); export const AUTH_KINDS = Object.freeze({ none: Object.freeze({ secret_fields: Object.freeze([]) }), header_key: Object.freeze({ secret_fields: Object.freeze(['api_key']) }), query_key: Object.freeze({ secret_fields: Object.freeze(['api_key']) }), bearer: Object.freeze({ secret_fields: Object.freeze(['api_key']) }), basic: Object.freeze({ secret_fields: Object.freeze(['api_key', 'api_secret']) }), hmac: Object.freeze({ secret_fields: Object.freeze(['api_key', 'api_secret']) }), }); export const ENUMS = Object.freeze({ ASSET_KIND: Object.freeze(['crypto', 'metal', 'fiat']), ASSET_DISPLAY_UNIT: Object.freeze(['unit', 'subunit']), METAL_FORM: Object.freeze(['coin', 'bar', 'other']), ACCOUNT_KIND: Object.freeze(['exchange', 'wallet', 'lightning', 'card', 'bank', 'vault']), TAX_CATEGORY: Object.freeze(['bank', 'overig']), TAX_SCOPE: Object.freeze(['all', 'category', 'none']), WEALTH_SCOPE: Object.freeze(['managed', 'none']), EVENT_TYPE: Object.freeze(['buy', 'sell', 'transfer', 'interest', 'reservation']), LEDGER_EVENT_TYPE: Object.freeze(['buy', 'sell', 'transfer', 'interest']), EVENT_SOURCE: Object.freeze(['manual', 'import', 'reconcile', 'lightning']), EVENT_PRICE_ORIGIN: Object.freeze(['paid', 'market_day', 'market_moment', 'none']), VALUATION_SOURCE: Object.freeze(['onchain', 'manual', 'statement']), CHANGE_OPERATION: Object.freeze(['update', 'delete']), CHANGE_ORIGIN: Object.freeze(['manual', 'import', 'reconcile', 'interest', 'lightning', 'ai_proposal', 'merge', 'system']), INTEREST_CADENCE: Object.freeze(['term_end', 'annual', 'quarterly', 'monthly', 'weekly']), INTEREST_RATE_KIND: Object.freeze(['fixed', 'variable']), INTEREST_CREDIT_STATUS: Object.freeze(['due', 'booked', 'dismissed']), WALLET_SOURCE_KIND: Object.freeze(['xpub', 'address', 'nwc', 'lnd']), ONCHAIN_SOURCE_KIND: Object.freeze(['xpub', 'address']), SCRIPT_TYPE: Object.freeze(['p2tr', 'p2wpkh', 'p2sh-p2wpkh', 'p2pkh']), SCAN_STATUS: Object.freeze(['success', 'partial', 'failed']), RECONCILE_DIRECTION: Object.freeze(['in', 'out']), WALLET_TX_LAYER: Object.freeze(['onchain', 'lightning']), WALLET_TX_KIND: Object.freeze(['channel', 'self']), RECONCILE_SUGGESTED: Object.freeze(['trade', 'transfer']), RECONCILE_STATUS: Object.freeze(['pending', 'accepted_trade', 'accepted_transfer', 'accepted_interest', 'ignored', 'auto_linked']), LINK_METHOD: Object.freeze(['exact', 'soft', 'manual']), NOTIFICATION_KIND: Object.freeze(['reconcile', 'interest', 'valuation', 'schedule_ended', 'source_proposal', 'ai_proposal', 'reservation']), CONNECTION_KIND: Object.freeze(['btc_explorer', 'lnd', 'nwc']), THEME: Object.freeze(['dark', 'light']), ACCENT: Object.freeze(['orange', 'magenta', 'lila', 'indigo', 'blue', 'cyan', 'teal', 'slate', 'sand']), DEPTH: Object.freeze(['flat', 'deep']), PROFILE: Object.freeze(['business', 'glass']), GLOW: Object.freeze(['off', 'on']), GLOW_SHAPE: Object.freeze(['blobs', 'bands']), MOTION: Object.freeze(['off', 'step', 'always']), GLOW_TEMPO: Object.freeze(['brisk', 'calm', 'slow']), ROW_SELECTION: Object.freeze(['light', 'tint', 'pill']), GLASS_RENDER: Object.freeze(['real', 'mimic']), MWR_METHOD: Object.freeze(['dietz', 'irr']), PERSONA: Object.freeze(['bitcoin', 'broad']), MARKET_SPOT: Object.freeze(['off', 'fallback', 'first']), REPEAT_ENTRY_KEY: Object.freeze(['ctrl_enter', 'enter']), RESULT_SIGNS: Object.freeze(['none', 'negative', 'both']), PRICE_RESOLUTION: Object.freeze(['day', 'timestamp']), CONNECTION_STATUS: Object.freeze(['ok', 'fail']), PRICE_SOURCE_STATUS: Object.freeze(['proposed', 'confirmed', 'manual']), HMAC_HASH_ALGORITHM: Object.freeze(['SHA-256']), HMAC_ENCODING: Object.freeze(['hex', 'base64']), AI_CALL_OUTCOME: Object.freeze(['ok', 'error', 'refused']), INTEGRATION_TARGET: Object.freeze(['service', 'provider']), INTEGRATION_OUTCOME: Object.freeze(['fail', 'empty', 'refused']), AI_MESSAGE_KIND: Object.freeze(['request', 'response']), AI_SCOPE: Object.freeze(['none', 'read', 'write', 'control']), COMMAND_KIND: Object.freeze(['navigate', 'locate']), AI_PROPOSAL_KIND: Object.freeze(['price_point', 'account', 'event', 'account_delete', 'valuation', 'portfolio', 'preference', 'source_choice']), SOURCE_CHOICE_ACTION: Object.freeze(['first', 'remove']), QUOTE_UNIT: Object.freeze(['ozt']), AI_PROPOSAL_STATUS: Object.freeze(['pending', 'accepted', 'rejected']), COMMAND_STATE: Object.freeze(['open', 'claimed', 'done', 'rejected', 'expired']), }); export const SYSTEM_PAGE = Object.freeze({ ROUTE: 'system', TABS: Object.freeze({ PORTFOLIOS: 'portfolios', SETTINGS: 'settings', ASSETS: 'assets', ACCOUNTS: 'accounts', INTEGRATIONS: 'integrations', IMPORT: 'import', DATA: 'data', HEALTH: 'health', AI: 'ai', LICENSE: 'license', }), }); export const EVENTS_PAGE = Object.freeze({ ROUTE: 'events', TABS: Object.freeze({ ACCOUNTS: 'accounts', VALUATIONS: 'valuations', TRADES: 'trades', TRANSFERS: 'transfers', INTEREST: 'interest', RESERVATIONS: 'reservations', }), }); export const CHARTS_PAGE = Object.freeze({ ROUTE: 'charts', TABS: Object.freeze({ PRICE: 'price', POWERLAW: 'pl', WMA: 'wma', MAYER: 'mayer', }), }); export const ROUTE_KEYS = Object.freeze({ HOME: 'home', ACTIONS: 'actions', EVENTS: EVENTS_PAGE.ROUTE, WEALTH: 'wealth', TAXES: 'taxes', VALIDATION: 'validation', TRACKING: 'tracking', CONVERSION_BITCOIN: 'conversion/bitcoin', PRICE_HISTORY: 'price-history', CHARTS: CHARTS_PAGE.ROUTE, SYSTEM: SYSTEM_PAGE.ROUTE, GUIDE: 'guide', }); const doel = (route, tab, sub) => Object.freeze({ route, tab: tab ?? null, sub: sub ?? null, element: null }); export const TARGETS = Object.freeze({ HOME: doel(ROUTE_KEYS.HOME), ACTIONS: doel(ROUTE_KEYS.ACTIONS), CHARTS: doel(ROUTE_KEYS.CHARTS), TAXES: doel(ROUTE_KEYS.TAXES), WEALTH: doel(ROUTE_KEYS.WEALTH), TRACKING: doel(ROUTE_KEYS.TRACKING), PRICE_HISTORY: doel(ROUTE_KEYS.PRICE_HISTORY), CONVERSION: doel(ROUTE_KEYS.CONVERSION_BITCOIN), GUIDE: doel(ROUTE_KEYS.GUIDE), PORTFOLIOS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.PORTFOLIOS), ASSETS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.ASSETS), ACCOUNTS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.ACCOUNTS), INTEGRATIONS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.INTEGRATIONS), AI_ACTIVITY: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.AI), SETTINGS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.SETTINGS), LICENSE: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.LICENSE), IMPORT: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.IMPORT), DATA: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.DATA), HEALTH: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.HEALTH), PROVIDER_KEYS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.INTEGRATIONS, 'providers'), SOURCE_PROPOSALS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.INTEGRATIONS, 'sources'), CONNECTIONS: doel(ROUTE_KEYS.SYSTEM, SYSTEM_PAGE.TABS.INTEGRATIONS, 'connections'), CHART_PRICE: doel(ROUTE_KEYS.CHARTS, CHARTS_PAGE.TABS.PRICE), CHART_POWERLAW: doel(ROUTE_KEYS.CHARTS, CHARTS_PAGE.TABS.POWERLAW), CHART_WMA: doel(ROUTE_KEYS.CHARTS, CHARTS_PAGE.TABS.WMA), CHART_MAYER: doel(ROUTE_KEYS.CHARTS, CHARTS_PAGE.TABS.MAYER), EVENT_ACCOUNTS: doel(ROUTE_KEYS.EVENTS, EVENTS_PAGE.TABS.ACCOUNTS), VALUATIONS: doel(ROUTE_KEYS.EVENTS, EVENTS_PAGE.TABS.VALUATIONS), TRADES: doel(ROUTE_KEYS.EVENTS, EVENTS_PAGE.TABS.TRADES), TRANSFERS: doel(ROUTE_KEYS.EVENTS, EVENTS_PAGE.TABS.TRANSFERS), INTEREST: doel(ROUTE_KEYS.EVENTS, EVENTS_PAGE.TABS.INTEREST), RESERVATIONS: doel(ROUTE_KEYS.EVENTS, EVENTS_PAGE.TABS.RESERVATIONS), }); export const NOTIFICATION_TARGETS = Object.freeze({ reconcile: TARGETS.ACTIONS, interest: TARGETS.ACTIONS, valuation: TARGETS.ACTIONS, schedule_ended: TARGETS.ACCOUNTS, source_proposal: TARGETS.SOURCE_PROPOSALS, ai_proposal: TARGETS.ACTIONS, reservation: TARGETS.ACTIONS, }); export const NOTIFICATION_PRIORITY = Object.freeze([ 'reconcile', 'valuation', 'reservation', 'interest', 'ai_proposal', 'source_proposal', 'schedule_ended', ]); export const PLACE_LABELS = Object.freeze({ providers: 'integrations.tabProviders', sources: 'integrations.tabSources', connections: 'integrations.tabConnections', [CHARTS_PAGE.TABS.PRICE]: 'signals.tabPrice', [CHARTS_PAGE.TABS.POWERLAW]: 'signals.tabPl', [CHARTS_PAGE.TABS.WMA]: 'signals.tabWma', [CHARTS_PAGE.TABS.MAYER]: 'signals.tabMayer', }); export function targetHash(target) { if (!target || !target.route) throw new Error('A target without a route has no address.'); return `#/${[target.route, target.tab, target.sub].filter(Boolean).join('/')}`; } export function hashFromLocation(hash) { const pad = String(hash || '').replace(/^#\/?/, ''); return pad ? `#/${pad}` : targetHash(TARGETS.HOME); } export function commandTooLate(expiresAt, nu = Date.now()) { if (!expiresAt) return false; const einde = Date.parse(expiresAt); if (Number.isNaN(einde)) return false; return einde - nu < CONFIG.COMMANDS.COUNTDOWN_SECONDS * 1000; } export function targetTab(target) { if (!target || !target.tab) return null; return [target.tab, target.sub].filter(Boolean).join('/'); } export const CAPABILITIES = Object.freeze(['spot', 'daily_history', 'history_point']); export const SOURCE_CAPABILITIES = Object.freeze(['spot', 'history']); export const SOURCE_ENDPOINTS = Object.freeze({ spot: Object.freeze(['spot']), history: Object.freeze(['daily_history', 'history_point']), }); export function sourceCapabilityOf(capability) { return SOURCE_CAPABILITIES.find(c => SOURCE_ENDPOINTS[c].includes(capability)) ?? null; } export const OUTPUT_FIELDS = Object.freeze(['time', 'price']); export const TRANSFORMS = Object.freeze(['none', 'ms_to_s', 'to_number', 'last_per_day', 'troy_ounce_to_gram', 'iso_date_to_s', 'ms_to_s_local_close']); export const HTTP_METHODS = Object.freeze(['GET', 'POST']); export const ACCOUNT_SOURCE_KINDS = Object.freeze({ exchange: Object.freeze([]), wallet: Object.freeze(['xpub', 'address']), lightning: Object.freeze(['xpub', 'address', 'nwc', 'lnd']), card: Object.freeze([]), bank: Object.freeze([]), vault: Object.freeze([]), }); export const ACCOUNT_INTEREST_KINDS = Object.freeze(['exchange', 'card', 'bank']); export const ACCOUNT_ASSET_KINDS = Object.freeze({ exchange: Object.freeze(['crypto', 'fiat']), wallet: Object.freeze(['crypto']), lightning: Object.freeze(['crypto']), card: Object.freeze(['fiat']), bank: Object.freeze(['fiat']), vault: Object.freeze(['metal']), }); export const TAX_CATEGORY_BY_ASSET_KIND = Object.freeze({ crypto: 'overig', metal: 'overig', fiat: 'bank' }); export const BALANCE_CAPTURE_KINDS = Object.freeze(['fiat']); export function balanceCaptureOn(asset) { if (!asset) return false; if (asset.balanceCapture === 0 || asset.balanceCapture === 1) return asset.balanceCapture === 1; return BALANCE_CAPTURE_KINDS.includes(asset.kind); } export const CONNECTION_PROVIDER = Object.freeze({ btc_explorer: 'mempool' }); export const ASSET_DEFINITIONS = Object.freeze({ BTC: Object.freeze({ symbol: 'BTC', name: 'Bitcoin', kind: 'crypto', unit: 'BTC', subunit: 'sat', decimals: 8, displayUnit: 'subunit' }), ETH: Object.freeze({ symbol: 'ETH', name: 'Ethereum', kind: 'crypto', unit: 'ETH', subunit: null, decimals: 8, displayUnit: 'unit' }), XAU: Object.freeze({ symbol: 'XAU', name: 'Goud', kind: 'metal', unit: 'ozt', subunit: 'g', decimals: 7, displayUnit: 'unit' }), XAG: Object.freeze({ symbol: 'XAG', name: 'Zilver', kind: 'metal', unit: 'ozt', subunit: 'g', decimals: 7, displayUnit: 'unit' }), }); export const PREFERENCES = Object.freeze({ app: Object.freeze({ theme: Object.freeze({ type: 'enum', allowed: ENUMS.THEME, initial: 'dark' }), accent: Object.freeze({ type: 'enum', allowed: ENUMS.ACCENT, initial: 'blue' }), depth: Object.freeze({ type: 'enum', allowed: ENUMS.DEPTH, initial: 'deep' }), profile: Object.freeze({ type: 'enum', allowed: ENUMS.PROFILE, initial: 'business' }), glow: Object.freeze({ type: 'enum', allowed: ENUMS.GLOW, initial: 'on' }), glowshape: Object.freeze({ type: 'enum', allowed: ENUMS.GLOW_SHAPE, initial: 'blobs' }), motion: Object.freeze({ type: 'enum', allowed: ENUMS.MOTION, initial: 'step' }), rowsel: Object.freeze({ type: 'enum', allowed: ENUMS.ROW_SELECTION, initial: 'light' }), glow_tempo: Object.freeze({ type: 'enum', allowed: ENUMS.GLOW_TEMPO, initial: 'calm' }), glass_transparency: Object.freeze({ type: 'int', allowed: null, initial: 65, min: 0, max: 100 }), glass_frost: Object.freeze({ type: 'int', allowed: null, initial: 3, min: 0, max: 50 }), glass_blur: Object.freeze({ type: 'int', allowed: null, initial: 30, min: 0, max: 100 }), glass_render: Object.freeze({ type: 'enum', allowed: ENUMS.GLASS_RENDER, initial: 'mimic' }), blur: Object.freeze({ type: 'bool', allowed: null, initial: 0 }), auto_blur_min: Object.freeze({ type: 'int', allowed: null, initial: 5, min: 1, max: CONFIG.AUTO_BLUR.NEVER_MIN }), asset_show_name: Object.freeze({ type: 'bool', allowed: null, initial: 1 }), asset_show_symbol: Object.freeze({ type: 'bool', allowed: null, initial: 1 }), show_page_title: Object.freeze({ type: 'bool', allowed: null, initial: 1 }), show_page_hint: Object.freeze({ type: 'bool', allowed: null, initial: 1 }), kiosk: Object.freeze({ type: 'bool', allowed: null, initial: 0 }), last_backup_at: Object.freeze({ type: 'string', allowed: null, initial: '' }), last_health_check_at: Object.freeze({ type: 'string', allowed: null, initial: '' }), start_year: Object.freeze({ type: 'int', allowed: null, initial: CONFIG.START_YEAR.AUTO, min: CONFIG.START_YEAR.AUTO, max: CONFIG.START_YEAR.MAX }), mwr_method: Object.freeze({ type: 'enum', allowed: ENUMS.MWR_METHOD, initial: 'dietz' }), repeat_entry_key: Object.freeze({ type: 'enum', allowed: ENUMS.REPEAT_ENTRY_KEY, initial: 'ctrl_enter' }), persona: Object.freeze({ type: 'enum', allowed: ENUMS.PERSONA, initial: 'bitcoin' }), setup_done: Object.freeze({ type: 'bool', allowed: null, initial: 0 }), market_spot: Object.freeze({ type: 'enum', allowed: ENUMS.MARKET_SPOT, initial: 'off' }), result_signs: Object.freeze({ type: 'enum', allowed: ENUMS.RESULT_SIGNS, initial: 'negative' }), }), timers: Object.freeze({ spot_refresh_seconds: Object.freeze({ type: 'int', allowed: null, initial: CONFIG.TIMERS.SPOT_REFRESH.MIN_SECONDS, min: CONFIG.TIMERS.SPOT_REFRESH.MIN_SECONDS, max: CONFIG.TIMERS.OFF_SECONDS }), bus_poll_seconds: Object.freeze({ type: 'int', allowed: null, initial: CONFIG.TIMERS.BUS_POLL.MIN_SECONDS, min: CONFIG.TIMERS.BUS_POLL.MIN_SECONDS, max: CONFIG.TIMERS.OFF_SECONDS }), }), ai: Object.freeze({ enabled: Object.freeze({ type: 'bool', allowed: null, initial: 1 }), allow_read: Object.freeze({ type: 'bool', allowed: null, initial: 1 }), allow_raw: Object.freeze({ type: 'bool', allowed: null, initial: 0 }), allow_write: Object.freeze({ type: 'bool', allowed: null, initial: 0 }), seen_call_id: Object.freeze({ type: 'int', allowed: null, initial: 0, min: 0 }), allow_control: Object.freeze({ type: 'bool', allowed: null, initial: 0 }), bucket_shares: Object.freeze({ type: 'bool', allowed: null, initial: 1 }), bucket_totals: Object.freeze({ type: 'bool', allowed: null, initial: 1 }), bucket_amounts: Object.freeze({ type: 'bool', allowed: null, initial: 1 }), bucket_quantities: Object.freeze({ type: 'bool', allowed: null, initial: 1 }), }), }); export const NETWORK = Object.freeze({ MAX_SUPPLY_BTC: 21_000_000 }); export const METRICS = Object.freeze({ WINDOW_MINUTES: 60, FAIL_WARN: 0.05, FAIL_BAD: 0.20, }); export const CLOCKS = Object.freeze({ LIST_MAX: 4, OPENING_WINDOW_MINUTES: 15, CITIES: Object.freeze([ Object.freeze({ key: 'amsterdam', name: 'Amsterdam', tz: 'Europe/Amsterdam' }), Object.freeze({ key: 'london', name: 'Londen', tz: 'Europe/London' }), Object.freeze({ key: 'frankfurt', name: 'Frankfurt', tz: 'Europe/Berlin' }), Object.freeze({ key: 'newyork', name: 'New York', tz: 'America/New_York' }), Object.freeze({ key: 'losangeles', name: 'Los Angeles', tz: 'America/Los_Angeles' }), Object.freeze({ key: 'saopaulo', name: 'São Paulo', tz: 'America/Sao_Paulo' }), Object.freeze({ key: 'tokyo', name: 'Tokyo', tz: 'Asia/Tokyo' }), Object.freeze({ key: 'hongkong', name: 'Hongkong', tz: 'Asia/Hong_Kong' }), Object.freeze({ key: 'shanghai', name: 'Shanghai', tz: 'Asia/Shanghai' }), Object.freeze({ key: 'singapore', name: 'Singapore', tz: 'Asia/Singapore' }), Object.freeze({ key: 'dubai', name: 'Dubai', tz: 'Asia/Dubai' }), Object.freeze({ key: 'mumbai', name: 'Mumbai', tz: 'Asia/Kolkata' }), Object.freeze({ key: 'sydney', name: 'Sydney', tz: 'Australia/Sydney' }), ]), EXCHANGES: Object.freeze([ Object.freeze({ key: 'tse', name: 'Tokyo (TSE)', tz: 'Asia/Tokyo', open: '09:00', close: '15:00', days: [1, 2, 3, 4, 5] }), Object.freeze({ key: 'hkex', name: 'Hongkong (HKEX)', tz: 'Asia/Hong_Kong', open: '09:30', close: '16:00', days: [1, 2, 3, 4, 5] }), Object.freeze({ key: 'sse', name: 'Shanghai (SSE)', tz: 'Asia/Shanghai', open: '09:30', close: '15:00', days: [1, 2, 3, 4, 5] }), Object.freeze({ key: 'lse', name: 'Londen (LSE)', tz: 'Europe/London', open: '08:00', close: '16:30', days: [1, 2, 3, 4, 5] }), Object.freeze({ key: 'euronext', name: 'Euronext (AMS)', tz: 'Europe/Amsterdam', open: '09:00', close: '17:30', days: [1, 2, 3, 4, 5] }), Object.freeze({ key: 'xetra', name: 'Xetra (FRA)', tz: 'Europe/Berlin', open: '09:00', close: '17:30', days: [1, 2, 3, 4, 5] }), Object.freeze({ key: 'nyse', name: 'New York (NYSE)', tz: 'America/New_York', open: '09:30', close: '16:00', days: [1, 2, 3, 4, 5] }), Object.freeze({ key: 'nasdaq', name: 'Nasdaq', tz: 'America/New_York', open: '09:30', close: '16:00', days: [1, 2, 3, 4, 5] }), ]), }); export const DASHBOARD = Object.freeze({ GRID_COLUMNS: 12, MIN_SPAN: 1, MAX_SPAN: 12, MIN_H: 1, MAX_H: 16, HEIGHT_STEP_REM: 3, SETTLE_MS: 250, LONG_PRESS_MS: 500, LONG_PRESS_SLOP_PX: 6, HEIGHT_MODEL: 6, LAYOUT_MODEL: 1, SKELETON_STEPS: 2, REVEAL_MAX_MS: 4000, SIZE_SPAN: Object.freeze({ s: 3, m: 4, l: 6, xl: 12 }), WIDGETS: Object.freeze({ signal: Object.freeze({ defaultSpan: 4, instanceable: false, persona: 'bitcoin', label_key: 'dashboard.widget.signal', defaultH: 4 }), notifications: Object.freeze({ defaultSpan: 4, instanceable: false, persona: null, label_key: 'dashboard.widget.notifications', defaultH: 3 }), standing: Object.freeze({ defaultSpan: 4, instanceable: false, persona: null, label_key: 'dashboard.widget.standing', defaultH: 3 }), metrics: Object.freeze({ defaultSpan: 3, instanceable: false, persona: null, label_key: 'dashboard.widget.metrics', defaultH: 1 }), netshare: Object.freeze({ defaultSpan: 3, instanceable: false, persona: 'bitcoin', label_key: 'dashboard.widget.netshare', defaultH: 2 }), spot: Object.freeze({ defaultSpan: 2, instanceable: true, persona: null, label_key: 'dashboard.widget.spot', defaultH: 2 }), clocks: Object.freeze({ defaultSpan: 4, instanceable: false, persona: null, label_key: 'dashboard.widget.clocks', defaultH: 5 }), return: Object.freeze({ defaultSpan: 4, instanceable: true, persona: null, label_key: 'dashboard.widget.return', defaultH: 4 }), }), DEFAULT_LAYOUT: Object.freeze([ Object.freeze({ instId: 'notifications', type: 'notifications', x: 8 }), ]), }); export function assertConfig() { for (const cap of CAPABILITIES) { if (SOURCE_CAPABILITIES.filter(c => SOURCE_ENDPOINTS[c].includes(cap)).length !== 1) { throw new Error(`Capability '${cap}' must fall under exactly one source choice. Startup aborted.`); } } for (const kind of ENUMS.ONCHAIN_SOURCE_KIND) { if (!ENUMS.WALLET_SOURCE_KIND.includes(kind)) { throw new Error(`ONCHAIN_SOURCE_KIND '${kind}' is not in WALLET_SOURCE_KIND. Startup aborted.`); } } for (const kind of ACCOUNT_INTEREST_KINDS) { if (!ENUMS.ACCOUNT_KIND.includes(kind)) { throw new Error(`ACCOUNT_INTEREST_KINDS '${kind}' is not an account kind. Startup aborted.`); } } for (const kind of Object.keys(ACCOUNT_SOURCE_KINDS)) { if (!ENUMS.ACCOUNT_KIND.includes(kind)) { throw new Error(`ACCOUNT_SOURCE_KINDS '${kind}' is not an account kind. Startup aborted.`); } } for (const kind of ENUMS.ACCOUNT_KIND) { const soorten = ACCOUNT_ASSET_KINDS[kind]; if (!soorten || !soorten.length || soorten.some(s => !ENUMS.ASSET_KIND.includes(s))) { throw new Error(`ACCOUNT_ASSET_KINDS '${kind}' is missing or names an unknown asset kind. Startup aborted.`); } } if (!CONFIG.PORTFOLIO.BASE_CURRENCIES.includes(CONFIG.PORTFOLIO.DEFAULT_BASE_CURRENCY)) { throw new Error(`PORTFOLIO.DEFAULT_BASE_CURRENCY '${CONFIG.PORTFOLIO.DEFAULT_BASE_CURRENCY}' is not in BASE_CURRENCIES. Startup aborted.`); } const essential = [ ['PRICE_SOURCES.MEMPOOL_PUBLIC_BASE', CONFIG.PRICE_SOURCES.MEMPOOL_PUBLIC_BASE], ['PRICE_SOURCES.BITVAVO_BASE', CONFIG.PRICE_SOURCES.BITVAVO_BASE], ['PRICE_SOURCES.COINGECKO_BASE', CONFIG.PRICE_SOURCES.COINGECKO_BASE], ['PRICE_SOURCES.GOLDAPI_BASE', CONFIG.PRICE_SOURCES.GOLDAPI_BASE], ['PRICE_SOURCES.EULERPOOL_BASE', CONFIG.PRICE_SOURCES.EULERPOOL_BASE], ['PRICE_SOURCES.METALSDEV_BASE', CONFIG.PRICE_SOURCES.METALSDEV_BASE], ['PRICE_SOURCES.FRANKFURTER_BASE', CONFIG.PRICE_SOURCES.FRANKFURTER_BASE], ['METALS.GRAMS_PER_TROY_OUNCE', CONFIG.METALS.GRAMS_PER_TROY_OUNCE], ['METALS.SPOT_DEVIATION_WARN', CONFIG.METALS.SPOT_DEVIATION_WARN], ['CHART_LIBS.CHARTJS', CONFIG.CHART_LIBS.CHARTJS], ['CHART_LIBS.HAMMER', CONFIG.CHART_LIBS.HAMMER], ['CHART_LIBS.ZOOM', CONFIG.CHART_LIBS.ZOOM], ['SCHEMA_VERSION', CONFIG.SCHEMA_VERSION], ['MAX_ASSET_DECIMALS', CONFIG.MAX_ASSET_DECIMALS], ['PRICE_HISTORY.NEAREST_MAX_SECONDS', CONFIG.PRICE_HISTORY.NEAREST_MAX_SECONDS], ['PRICE_HISTORY.CARRY_FORWARD_DAYS.crypto', CONFIG.PRICE_HISTORY.CARRY_FORWARD_DAYS.crypto], ['PRICE_HISTORY.RECENT_CLOSE_DAYS', CONFIG.PRICE_HISTORY.RECENT_CLOSE_DAYS], ['PRICE_HISTORY.FX_MAX_AGE_DAYS', CONFIG.PRICE_HISTORY.FX_MAX_AGE_DAYS], ['API_USAGE.WARN_SHARE', CONFIG.API_USAGE.WARN_SHARE], ['START_YEAR.MIN', CONFIG.START_YEAR.MIN], ['START_YEAR.MAX', CONFIG.START_YEAR.MAX], ['TIMERS.SPOT_REFRESH.MIN_SECONDS', CONFIG.TIMERS.SPOT_REFRESH.MIN_SECONDS], ['TIMERS.BUS_POLL.MIN_SECONDS', CONFIG.TIMERS.BUS_POLL.MIN_SECONDS], ['BUS.RETENTION_HOURS', CONFIG.BUS.RETENTION_HOURS], ['COMMANDS.CLAIM_SECONDS', CONFIG.COMMANDS.CLAIM_SECONDS], ['COMMANDS.COUNTDOWN_SECONDS', CONFIG.COMMANDS.COUNTDOWN_SECONDS], ['TIMERS.OFF_SECONDS', CONFIG.TIMERS.OFF_SECONDS], ['LND.REQUEST_TIMEOUT_MS', CONFIG.LND.REQUEST_TIMEOUT_MS], ['LND.CLIENT_TIMEOUT_MS', CONFIG.LND.CLIENT_TIMEOUT_MS], ['NWC.REQUEST_TIMEOUT_MS', CONFIG.NWC.REQUEST_TIMEOUT_MS], ['NWC.CLIENT_TIMEOUT_MS', CONFIG.NWC.CLIENT_TIMEOUT_MS], ['MARKET_SERVICE.SETTINGS_FILE_NAME', CONFIG.MARKET_SERVICE.SETTINGS_FILE_NAME], ['MARKET_SERVICE.URL', CONFIG.MARKET_SERVICE.URL], ['LICENSE.FREE_FILE_NAME', CONFIG.LICENSE.FREE_FILE_NAME], ['MARKET_SERVICE.APP_NAME', CONFIG.MARKET_SERVICE.APP_NAME], ['MARKET_SERVICE.REQUEST_TIMEOUT_MS', CONFIG.MARKET_SERVICE.REQUEST_TIMEOUT_MS], ['MARKET_SERVICE.CLIENT_TIMEOUT_MS', CONFIG.MARKET_SERVICE.CLIENT_TIMEOUT_MS], ['MARKET_SERVICE.REGISTER_TTL_MS', CONFIG.MARKET_SERVICE.REGISTER_TTL_MS], ['MARKET_SERVICE.MAX_SERIES_PAGES', CONFIG.MARKET_SERVICE.MAX_SERIES_PAGES], ['MARKET_SERVICE.INTRADAY_MAX_DAYS', CONFIG.MARKET_SERVICE.INTRADAY_MAX_DAYS], ['MARKET_SERVICE.HISTORY_MAX_DAYS', CONFIG.MARKET_SERVICE.HISTORY_MAX_DAYS], ['MARKET_SERVICE.PROVIDER_KEY', CONFIG.MARKET_SERVICE.PROVIDER_KEY], ['MARKET_SERVICE.SPOT_TTL_MS', CONFIG.MARKET_SERVICE.SPOT_TTL_MS], ['MARKET_SERVICE.MAX_INSTRUMENTS_PER_SPOT', CONFIG.MARKET_SERVICE.MAX_INSTRUMENTS_PER_SPOT], ['AI.SERVER_NAME', CONFIG.AI.SERVER_NAME], ['AI.SERVER_VERSION', CONFIG.AI.SERVER_VERSION], ['AI.PROTOCOL_VERSION', CONFIG.AI.PROTOCOL_VERSION], ['AI.AUDIT_PAGE_SIZE', CONFIG.AI.AUDIT_PAGE_SIZE], ['AI.PROPOSAL_PAGE_SIZE', CONFIG.AI.PROPOSAL_PAGE_SIZE], ['AI.LIMITS.MAX_ROWS_PER_LIST', CONFIG.AI.LIMITS.MAX_ROWS_PER_LIST], ['AI.LIMITS.MAX_TEXT_CHARS', CONFIG.AI.LIMITS.MAX_TEXT_CHARS], ['AI.LIMITS.MAX_LOG_CHARS', CONFIG.AI.LIMITS.MAX_LOG_CHARS], ['AI.LIMITS.METRICS_WINDOW_HOURS', CONFIG.AI.LIMITS.METRICS_WINDOW_HOURS], ['AUTH.SESSION_TTL_MS', CONFIG.AUTH.SESSION_TTL_MS], ['AUTH.COOKIE_NAME', CONFIG.AUTH.COOKIE_NAME], ['AUTH.LOGIN_PATH', CONFIG.AUTH.LOGIN_PATH], ['AUTH.LOGIN_ATTEMPTS.FREE', CONFIG.AUTH.LOGIN_ATTEMPTS.FREE], ['AUTH.LOGIN_ATTEMPTS.BASE_LOCK_MS', CONFIG.AUTH.LOGIN_ATTEMPTS.BASE_LOCK_MS], ['AUTH.LOGIN_ATTEMPTS.MAX_LOCK_MS', CONFIG.AUTH.LOGIN_ATTEMPTS.MAX_LOCK_MS], ]; for (const [name, val] of essential) { if (val === undefined || val === null || val === '') { throw new Error(`Essential config missing: ${name}. Startup aborted.`); } } }