// Generated, do not edit. sha256:e3489a6085d47798380dafd4c4992473c5fdddb26fbfb6bf3b1a4ff191ab8437 import { AUTH_KINDS, CONFIG } from '../../shared/config.js'; import { AppError, fail } from '../../shared/app-error.js'; import { toLocalIso } from '../../shared/time.js'; const TIMEOUT_MS = 12000; const CAPABILITIES = ['spot', 'daily_history', 'history_point']; function tokenize(path, externalId) { return path.split('.').map(seg => { if (seg === '{external_id}') { if (externalId == null) fail('integrations.price.externalIdMissing', null, 'The output path uses {external_id} but there is no external id.'); return externalId; } return /^\d+$/.test(seg) ? Number(seg) : seg; }); } export function resolveScalar(obj, path, externalId) { let cur = obj; for (const key of tokenize(path, externalId)) { if (cur == null) return undefined; cur = cur[key]; } return cur; } export function resolveColumn(obj, path, externalId) { const o = path.indexOf('{}'); if (o >= 0 && (path.indexOf('[]') < 0 || o < path.indexOf('[]'))) { const before = path.slice(0, o); const after = path.slice(o + 2).replace(/^\./, ''); const map = before ? resolveScalar(obj, before, externalId) : obj; if (map == null || typeof map !== 'object' || Array.isArray(map)) { fail('integrations.price.expectedObject', { pad: before || '(root)' }, `Expected an object at '${before || '(root)'}'.`); } return Object.entries(map).map(([key, value]) => (after === '@key' ? key : after ? resolveScalar(value, after, externalId) : value)); } const i = path.indexOf('[]'); if (i < 0) fail('integrations.price.listPathWithoutBrackets', { pad: path }, `List path without []: '${path}'.`); const before = path.slice(0, i); const after = path.slice(i + 2).replace(/^\./, ''); const arr = before ? resolveScalar(obj, before, externalId) : obj; if (!Array.isArray(arr)) fail('integrations.price.expectedArray', { pad: before || '(root)' }, `Expected an array at '${before || '(root)'}'.`); return arr.map(el => (after ? resolveScalar(el, after, externalId) : el)); } function applyScalarTransform(v, transform) { switch (transform) { case 'none': return v; case 'to_number': { const n = Number(v); return Number.isFinite(n) ? n : null; } case 'ms_to_s': { const n = Number(v); return Number.isFinite(n) ? Math.floor(n / 1000) : null; } case 'last_per_day': return v; case 'troy_ounce_to_gram': { const n = Number(v); const f = CONFIG.METALS.GRAMS_PER_TROY_OUNCE; return (Number.isFinite(n) && f) ? n / f : null; } case 'iso_date_to_s': { if (typeof v !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(v)) return null; const ms = Date.parse(`${v}T00:00:00Z`); return Number.isFinite(ms) ? Math.floor(ms / 1000) : null; } case 'ms_to_s_local_close': { const n = Number(v); if (!Number.isFinite(n)) return null; const utc = new Date(n); if (n % 86400000 === 0 && utc.getUTCDate() === 1) return null; const lokaal = toLocalIso(utc.toISOString()); if (/T00:00:00(\.000)?$/.test(lokaal)) return Date.parse(`${lokaal.slice(0, 10)}T00:00:00Z`) / 1000; return Math.floor(n / 1000); } default: fail('integrations.price.unknownTransform', { transform }, `Unknown transform: '${transform}'.`); } } function outputsFor(provider, capability) { const rows = provider.outputs.filter(o => o.capability === capability); if (!rows.length) fail('integrations.price.noOutputMapping', { provider: provider.key, capability }, `Provider ${provider.key}: no output mapping for '${capability}'.`); return rows; } function assertPriced(value, provider) { if (Number.isFinite(value)) return; fail('integrations.price.noValidPrice', { provider: provider.key }, `Provider ${provider.key}: no valid price in the response.`); } export function mapSpot(parsed, provider, externalId) { const out = {}; for (const o of outputsFor(provider, 'spot')) out[o.internalField] = applyScalarTransform(resolveScalar(parsed, o.jsonPath, externalId), o.transform); assertPriced(out.price, provider); return { price: out.price }; } export function mapHistoryPoint(parsed, provider, externalId, dateIso) { const out = {}; for (const o of outputsFor(provider, 'history_point')) out[o.internalField] = applyScalarTransform(resolveScalar(parsed, o.jsonPath, externalId), o.transform); const time = Number.isFinite(out.time) ? out.time : Math.floor(Date.parse(`${dateIso}T00:00:00Z`) / 1000); if (!Number.isFinite(time)) fail('integrations.price.invalidDate', { provider: provider.key, datum: dateIso }, `Provider ${provider.key}: invalid date '${dateIso}'.`); assertPriced(out.price, provider); return { time, price: out.price }; } export function mapDailyHistory(parsed, provider, externalId) { const cols = {}; let len = null; let lastPerDay = false; for (const o of outputsFor(provider, 'daily_history')) { if (o.transform === 'last_per_day') lastPerDay = true; const vals = resolveColumn(parsed, o.jsonPath, externalId) .map(v => applyScalarTransform(v, o.transform === 'last_per_day' ? 'none' : o.transform)); cols[o.internalField] = vals; if (len == null) len = vals.length; else if (vals.length !== len) fail('integrations.price.columnsUneven', { provider: provider.key }, `Provider ${provider.key}: the output columns have unequal length.`); } let rows = []; for (let i = 0; i < (len || 0); i++) { const time = cols.time ? cols.time[i] : null; const price = cols.price?.[i]; if (Number.isFinite(time) && Number.isFinite(price)) rows.push({ time, price }); } rows.sort((a, b) => a.time - b.time); if (lastPerDay) { const byDay = new Map(); for (const r of rows) byDay.set(new Date(r.time * 1000).toISOString().slice(0, 10), r); rows = [...byDay.values()].sort((a, b) => a.time - b.time); } return rows; } function mapResult(parsed, provider, capability, externalId, dateIso) { if (capability === 'spot') return mapSpot(parsed, provider, externalId); if (capability === 'history_point') return mapHistoryPoint(parsed, provider, externalId, dateIso); return mapDailyHistory(parsed, provider, externalId); } function endpointFor(provider, capability) { const e = provider.endpoints.find(ep => ep.capability === capability); if (!e) fail('integrations.price.capabilityNotConfigured', { provider: provider.key, capability }, `Provider ${provider.key}: capability '${capability}' is not configured.`); return e; } function mapRowFor(provider, symbol) { const rows = provider.symbols || []; return rows.find(s => !s.isCatchAll && s.symbol === symbol) || rows.find(s => s.isCatchAll) || null; } function externalIdFor(provider, symbol) { const row = mapRowFor(provider, symbol); return row ? row.externalId : (symbol != null ? symbol : null); } function subst(tpl, externalId, symbol, dateIso, providerKey) { if (tpl == null) return tpl; let s = tpl; if (s.includes('{external_id}')) { if (externalId == null) fail('integrations.price.symbolMissing', { provider: providerKey }, `Provider ${providerKey}: {external_id} is used but no symbol was given.`); s = s.replaceAll('{external_id}', encodeURIComponent(externalId)); } if (s.includes('{symbol}')) s = s.replaceAll('{symbol}', encodeURIComponent(symbol ?? '')); if (s.includes('{date}')) { const ymd = String(dateIso || '').slice(0, 10).replace(/-/g, ''); if (ymd.length !== 8) fail('integrations.price.templateDateInvalid', { provider: providerKey, datum: dateIso }, `Provider ${providerKey}: the template uses {date} but date '${dateIso}' is invalid.`); s = s.replaceAll('{date}', ymd); } return s; } const toHex = buf => [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, '0')).join(''); export async function hmacHeaders(hmac, method, fullPath, body, creds) { const ts = Date.now(); const tokens = { timestamp: String(ts), method, full_path: fullPath, body: body || '' }; const message = hmac.messageParts.split(',').map(tok => { if (!(tok in tokens)) fail('integrations.price.unknownHmacToken', { token: tok }, `Unknown hmac message token: '${tok}'.`); return tokens[tok]; }).join(''); const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(creds.api_secret), { name: 'HMAC', hash: hmac.hashAlgorithm }, false, ['sign']); const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(message)); const signature = hmac.encoding === 'hex' ? toHex(sig) : Buffer.from(sig).toString('base64'); const headers = { [hmac.keyHeader]: creds.api_key, [hmac.signatureHeader]: signature, [hmac.timestampHeader]: String(ts) }; if (hmac.windowHeader && hmac.windowMs != null) headers[hmac.windowHeader] = String(hmac.windowMs); return headers; } export async function buildRequest(provider, capability, symbol, dateIso, creds) { if (!CAPABILITIES.includes(capability)) fail('integrations.price.unknownCapability', { capability }, `Unknown capability: '${capability}'.`); const e = endpointFor(provider, capability); if (e.method !== 'GET') fail('integrations.price.onlyGet', { provider: provider.key }, `Provider ${provider.key}: only GET is supported.`); const externalId = externalIdFor(provider, symbol); const kind = provider.authenticationKind; if (kind !== 'none' && !AUTH_KINDS[kind]) fail('integrations.price.unknownAuthKind', { soort: kind }, `Unknown authenticationKind: '${kind}'.`); const fields = kind === 'none' ? [] : AUTH_KINDS[kind].secret_fields; const hasCreds = kind !== 'none' && fields.every(f => creds && creds[f]); if (provider.authenticationRequired && !hasCreds) fail('integrations.price.keyMissing', { provider: provider.key }, `Provider ${provider.key}: a key is required but missing or incomplete.`); const path = subst(e.pathTemplate, externalId, symbol, dateIso, provider.key); let query = subst(e.queryTemplate, externalId, symbol, dateIso, provider.key) || ''; if (kind === 'query_key' && hasCreds) { if (!provider.authenticationParameterName) fail('integrations.price.parameterNameMissing', { provider: provider.key }, `Provider ${provider.key}: authenticationParameterName is missing for query_key.`); const qp = `${encodeURIComponent(provider.authenticationParameterName)}=${encodeURIComponent(creds.api_key)}`; query = query ? `${query}&${qp}` : qp; } const fullPath = path + (query ? `?${query}` : ''); const headers = {}; if (hasCreds) { if (kind === 'header_key') { if (!provider.authenticationHeaderName) fail('integrations.price.headerNameMissing', { provider: provider.key }, `Provider ${provider.key}: authenticationHeaderName is missing for header_key.`); headers[provider.authenticationHeaderName] = creds.api_key; } else if (kind === 'bearer') { headers['Authorization'] = `Bearer ${creds.api_key}`; } else if (kind === 'basic') { headers['Authorization'] = `Basic ${Buffer.from(`${creds.api_key}:${creds.api_secret}`).toString('base64')}`; } else if (kind === 'hmac') { if (!provider.hmac) fail('integrations.price.hmacConfigMissing', { provider: provider.key }, `Provider ${provider.key}: the hmac config is missing.`); Object.assign(headers, await hmacHeaders(provider.hmac, e.method, fullPath, '', creds)); } } return { url: provider.baseUrl + fullPath, method: e.method, headers, externalId }; } function describeBody(res, text) { const type = String(res.headers?.get?.('content-type') || 'no type').split(';')[0].trim(); return `no JSON (${type}, ${Buffer.byteLength(text || '', 'utf8')} bytes)`; } function tagged(e, extra) { return Object.assign(e, extra); } export async function priceCall(provider, capability, symbol, dateIso, creds, { withRaw = false } = {}) { let built; try { built = await buildRequest(provider, capability, symbol, dateIso, creds); } catch (e) { throw tagged(e, { notSent: true }); } const { url, method, headers, externalId } = built; let res; try { res = await fetch(url, { method, headers, signal: AbortSignal.timeout(TIMEOUT_MS) }); } catch (err) { fail('integrations.price.networkError', { provider: provider.key, capability, reden: err.name === 'TimeoutError' ? 'timeout' : err.message }, `${provider.key} ${capability}: network error (${err.name === 'TimeoutError' ? 'timeout' : err.message}).`); } const text = await res.text().catch(() => ''); let parsed; let notJson = null; try { parsed = JSON.parse(text); } catch { notJson = describeBody(res, text); } const httpStatus = res.status; if (!res.ok) { const m = parsed && (parsed.error || parsed.message); const detail = typeof m === 'string' && m ? ` ${m}` : notJson ? ` ${notJson}` : ''; throw tagged(new AppError('integrations.price.httpFailed', { provider: provider.key, capability, status: res.status, detail }, `${provider.key} ${capability} failed (${res.status})${detail}.`), { httpStatus }); } if (notJson) { throw tagged(new AppError('integrations.price.notJson', { provider: provider.key, capability, detail: notJson }, `${provider.key} ${capability}: the answer is ${notJson}.`), { httpStatus }); } let result; try { result = inStorageUnit(mapResult(parsed, provider, capability, externalId, dateIso), provider, symbol); } catch (e) { throw tagged(e, { httpStatus }); } return withRaw ? { result, raw: parsed } : result; } export function failureEntry(provider, capability, symbol, e, latencyMs) { const outcome = e?.notSent ? 'refused' : e?.key === 'integrations.price.noValidPrice' ? 'empty' : 'fail'; return { target: 'provider', providerKey: provider?.key, operation: capability, subject: typeof symbol === 'string' && symbol ? symbol : null, outcome, httpStatus: e?.httpStatus ?? null, errorKey: e?.key ?? null, detail: e?.message ?? String(e), latencyMs: e?.notSent ? null : latencyMs, }; } function inStorageUnit(result, provider, symbol) { const row = mapRowFor(provider, symbol); if (!row || row.quoteUnit !== 'ozt') return result; const f = CONFIG.METALS.GRAMS_PER_TROY_OUNCE; const perGram = r => (Number.isFinite(r.price) ? { ...r, price: r.price / f } : r); return Array.isArray(result) ? result.map(perGram) : perGram(result); }