addition of new dashboard
This commit is contained in:
146
backend/modules/dashboard/verify.js
Normal file
146
backend/modules/dashboard/verify.js
Normal file
@@ -0,0 +1,146 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user