import { getPool } from "../../db/index.js"; // Lazy import so this module does not pull in @jlinc/core. const defaultVerify = async (input, userId) => (await import("../core/data/index.js")).data.audit.verify(input, userId); // A target is ONE ROW: { eventUuid } covers all of an event's audit records, // { auditId } covers exactly one. Audit rows share an eventUuid, so keying them // by event reports one record's verdict on all its siblings. const MAX_BATCH = 200; // Joined through `event` on user_id: an audit id is a lookup key, never an // authorization decision. async function fetchAuditRecords(client, userId, { eventUuid, auditId }) { const byAuditId = auditId != null; const rows = ( await client.query( `SELECT a.audit_id, a.version, a.event_id, a.hash_type, a.digest, a.created, e.sender_id, e.recipient_id, COALESCE( JSON_AGG( JSON_BUILD_OBJECT('version', s.version, 'id', s.id, 'signedon', s.signedon, 'type', s.type, 'jws', s.jws) ) FILTER (WHERE s.audit_id IS NOT NULL), '[]' ) AS signatures FROM audit a JOIN event e ON e.event_id_uuid = a.event_id AND e.user_id = $1 LEFT JOIN audit_signature s ON s.audit_id = a.audit_id WHERE ${byAuditId ? "a.audit_id = $2" : "a.event_id = $2"} GROUP BY a.audit_id, a.version, a.event_id, a.hash_type, a.digest, a.created, e.sender_id, e.recipient_id ORDER BY a.audit_id LIMIT $3`, [userId, byAuditId ? auditId : eventUuid, byAuditId ? 1 : MAX_BATCH], ) ).rows; const dids = new Set(); const records = rows.map((row) => { if (row.sender_id) dids.add(row.sender_id); if (row.recipient_id) dids.add(row.recipient_id); return { auditId: Number(row.audit_id), audit: { version: row.version, hashType: row.hash_type, digest: row.digest, created: Number(row.created), eventId: row.event_id, }, signatures: (row.signatures || []).map((r) => ({ version: r.version, id: r.id, signedOn: Number(r.signedon), type: r.type, jws: r.jws, })), }; }); return { records, dids: [...dids] }; } // Only the DIDs the batch actually references, and only did_doc — getEntity // would return the control and recovery private keys, which verification never // needs. Resolved once per batch and memoised across targets. async function resolveDidDocs(client, userId, dids, cache) { const missing = dids.filter((did) => !cache.has(did)); if (missing.length > 0) { const rows = ( await client.query( `SELECT did_id, did_doc FROM entity WHERE user_id = $1 AND did_id = ANY($2)`, [userId, missing], ) ).rows; for (const did of missing) cache.set(did, null); for (const row of rows) cache.set(row.did_id, row.did_doc); } return dids.map((did) => cache.get(did)).filter(Boolean); } export function summarizeVerification(target, records, verifierResult) { const { eventUuid = null, auditId = null } = target; if (records.length === 0) { return { eventUuid, auditId, status: "no-audit", verified: false, signatureCount: 0, auditCount: 0, checks: null, reason: "no audit record", }; } const valid = verifierResult?.data?.valid || []; const invalid = verifierResult?.data?.invalid || []; // Matched by audit id: the verifier splits input across two buckets, so // position is meaningless once a target has several records. const verdicts = records.map((r) => ({ verified: valid.some((e) => e.audit?.auditId === r.auditId), checks: [...valid, ...invalid].find((e) => e.audit?.auditId === r.auditId)?.results || null, })); const failed = verdicts.find((v) => !v.verified); return { eventUuid, auditId, status: failed ? "invalid" : "verified", verified: !failed, signatureCount: records.reduce((n, r) => n + r.signatures.length, 0), auditCount: records.length, checks: (failed || verdicts[0]).checks, }; } async function verifyTarget(acquire, verify, userId, target, cache) { const client = await acquire(); let records, didDocs; try { const fetched = await fetchAuditRecords(client, userId, target); records = fetched.records; didDocs = records.length === 0 ? [] : await resolveDidDocs(client, userId, fetched.dids, cache); } finally { await client.release(); } if (records.length === 0) return summarizeVerification(target, records, null); const result = await verify({ didDocs, audits: records }, userId); return summarizeVerification(target, records, result); } export async function verifyMany(userId, { eventUuids = [], auditIds = [] } = {}, { acquire = getPool, verify = defaultVerify } = {}) { const targets = [ ...eventUuids.map((eventUuid) => ({ eventUuid, auditId: null })), ...auditIds.map((auditId) => ({ eventUuid: null, auditId })), ].slice(0, MAX_BATCH); if (targets.length === 0) return []; const cache = new Map(); const results = []; for (const target of targets) { results.push(await verifyTarget(acquire, verify, userId, target, cache)); } return results; }