// Generated, do not edit. sha256:3c9603902fe01cc9593bf69315c4734fdbba824688512bed37af01059c22d688 import * as db from '../db.js'; import { recordApiCall } from './api-metrics.js'; import { CONFIG, ENUMS } from '../../shared/config.js'; const LOG = CONFIG.INTEGRATION_LOG; const MIN_SECRET_LENGTH = 4; const AUTH_VALUE = /\b(Bearer|Basic)\s+\S+/gi; const SECRET_PARAM = /\b((?:api[_-]?key|apikey|access[_-]?token|token|key|secret|signature|password)=)[^&\s"']+/gi; const URL_TEXT = /\b[a-z][a-z0-9+.-]*:\/\/\S+/gi; function cleanDetail(text, secrets) { if (text == null) return null; let s = String(text); for (const v of secrets) { if (typeof v !== 'string' || v.length < MIN_SECRET_LENGTH) continue; for (const form of new Set([v, encodeURIComponent(v)])) s = s.split(form).join('***'); } s = s.replace(URL_TEXT, '').replace(AUTH_VALUE, '$1 ***').replace(SECRET_PARAM, '$1***'); s = s.replace(/\s+/g, ' ').trim(); if (!s) return null; return s.length > LOG.DETAIL_MAX_CHARS ? `${s.slice(0, LOG.DETAIL_MAX_CHARS - 1)}…` : s; } const KEY_PART = /^[A-Za-z][\w.-]{0,79}$/; const cleanKey = (key) => { const parts = String(key ?? '').split(/\s+/).filter(p => KEY_PART.test(p)); return parts.length ? parts.join(' ') : null; }; const REQUEST_ID = /^[A-Za-z0-9-]{8,64}$/; function timeOf(at) { if (at == null) return new Date().toISOString(); const d = new Date(at); if (Number.isNaN(d.getTime())) throw new Error(`Invalid occurredAt: '${at}'.`); return d.toISOString(); } function prune() { const grens = new Date(Date.now() - LOG.RETENTION_DAYS * 86_400_000).toISOString(); db.run(`DELETE FROM IntegrationCall WHERE occurredAt < ?`, [grens]); db.run(`DELETE FROM IntegrationCall WHERE id <= (SELECT id FROM IntegrationCall ORDER BY id DESC LIMIT 1 OFFSET ?)`, [LOG.MAX_ROWS]); } export async function recordIntegrationCall(entry, { secrets = [] } = {}) { const e = entry || {}; if (!ENUMS.INTEGRATION_TARGET.includes(e.target)) throw new Error(`Unknown integration target: '${e.target}'.`); if (!ENUMS.INTEGRATION_OUTCOME.includes(e.outcome)) throw new Error(`Unknown integration outcome: '${e.outcome}'.`); const providerKey = e.target === 'provider' ? String(e.providerKey || '') : null; if (e.target === 'provider' && !providerKey) throw new Error('A provider row needs a providerKey.'); const operation = String(e.operation || ''); if (!operation || /[?#]|:\/\//.test(operation)) throw new Error(`An operation is a path or a capability: '${operation}'.`); const subject = e.subject == null ? null : String(e.subject).slice(0, 100); const status = Number.isInteger(e.httpStatus) && e.httpStatus >= 100 && e.httpStatus <= 599 ? e.httpStatus : null; const latency = Number.isFinite(e.latencyMs) && e.latencyMs >= 0 ? Math.round(e.latencyMs) : null; const requestId = e.target === 'service' && REQUEST_ID.test(String(e.requestId ?? '')) ? e.requestId : null; const clean = cleanDetail(e.detail, Array.isArray(secrets) ? secrets : []); db.run(`INSERT INTO IntegrationCall (target, providerKey, operation, subject, outcome, httpStatus, errorKey, detail, latencyMs, requestId, occurredAt) VALUES (?,?,?,?,?,?,?,?,?,?,?)`, [e.target, providerKey, operation, subject, e.outcome, status, cleanKey(e.errorKey), clean, latency, requestId, timeOf(e.occurredAt)]); prune(); await db.persist(); } export async function countServiceCall({ capability, ok, latencyMs }) { const p = db.one(`SELECT id FROM Provider WHERE key = ?`, [CONFIG.MARKET_SERVICE.PROVIDER_KEY]); if (!p) return; await recordApiCall({ providerId: p.id, capability, ok: !!ok, latency_ms: latencyMs }); } export function listIntegrationCalls({ days = 7, target = null, providerKey = null, limit = 200 } = {}) { const sinds = new Date(Date.now() - Math.max(0, Number(days) || 0) * 86_400_000).toISOString(); const n = Math.min(Math.max(1, Math.trunc(Number(limit)) || 200), LOG.MAX_ROWS); const t = target || null; const p = providerKey || null; return db.all(`SELECT * FROM IntegrationCall WHERE occurredAt >= ? AND (? IS NULL OR target = ?) AND (? IS NULL OR providerKey = ?) ORDER BY id DESC LIMIT ?`, [sinds, t, t, p, p, n]); } export function serviceState() { const lastFail = db.one(`SELECT * FROM IntegrationCall WHERE target = 'service' AND outcome = 'fail' ORDER BY id DESC LIMIT 1`) || null; const row = db.one(`SELECT MAX(bucketMinute) AS at FROM ApiCallMetric JOIN Provider ON Provider.id = ApiCallMetric.providerId WHERE Provider.key = ? AND okCount > 0`, [CONFIG.MARKET_SERVICE.PROVIDER_KEY]); const lastOkAt = row?.at ?? null; if (lastFail && (!lastOkAt || lastFail.occurredAt.slice(0, 16) >= lastOkAt)) { return { ok: false, at: lastFail.occurredAt, lastFail, lastOkAt }; } return { ok: lastOkAt ? true : null, at: lastOkAt ? `${lastOkAt}:00.000Z` : null, lastFail, lastOkAt }; } export function lastIntegrationFailures() { return db.all(`SELECT * FROM IntegrationCall WHERE id IN (SELECT MAX(id) FROM IntegrationCall GROUP BY target, providerKey) ORDER BY id DESC`); }