import { getPool } from "../../db/index.js"; function isUuid(uuid) { if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(uuid)) return true; return false; } function getAuditErrors(audit) { if (typeof audit !== 'object') { return 'invalid audit object format'; } if (audit.version != 1) { return 'audit version invalid'; } if (audit.hashType != 'SHA256') { return 'audit hash type invalid'; } if (!audit.digest) { return 'audit digest should exist'; } if (isNaN(audit.created)) { return 'audit created type invalid'; } if (!audit.eventId && !audit.agreementId) { return 'eventId or agreementId should exist'; } if (audit.eventId && !isUuid(audit.eventId)) { return 'eventId invalid'; } if (audit.agreementId && !isUuid(audit.agreementId)) { return 'agreementId invalid'; } return null; } async function processAudit(data) { const client = await getPool(); try { let type = data.audit.eventId ? 'event' : 'agreement'; const digest = data.audit.digest; let id = data.audit.eventId ? data.audit.eventId : data.audit.agreementId; await client.query(`BEGIN`); let res = await client.query(` SELECT 1 FROM audit a WHERE a.${type}_id = $1 AND digest = $2 LIMIT 1; `, [ id, digest, ]); if (res.rows.length > 0) { throw new Error('audit record with id and digest exists'); } res = await client.query(` INSERT INTO audit ( version, event_id, agreement_id, hash_type, digest, created ) VALUES ( $1, $2, $3, $4, $5, $6 ) RETURNING audit_id; `, [ data.audit.version, data.audit.eventId, data.audit.agreementId, data.audit.hashType, data.audit.digest, data.audit.created, ]); for (const signature of data.signatures) { await client.query(` INSERT INTO audit_signature ( audit_id, version, id, signedOn, type, jws ) VALUES ( $1, $2, $3, $4, $5, $6 ); `, [ res.rows[0].audit_id, signature.version, signature.id, signature.signedOn, signature.type, signature.jws, ]); } if (data.meta) { for await (const [key, value] of Object.entries(data.meta)) { await client.query(` INSERT INTO audit_meta ( audit_id, key, value ) VALUES ( $1, $2, $3 ); `, [ res.rows[0].audit_id, key, value, ]); } } await client.query('COMMIT'); } catch(e) { console.error(e) } finally { await client.release(); } } async function getAuditsByAgreementId(client, id, offset, limit) { let ret = await client.query(` WITH origAudits AS ( SELECT audit_id, version, agreement_id, hash_type, digest, created FROM audit WHERE agreement_id = $1 ORDER BY created DESC OFFSET $2 LIMIT $3 ), audits AS ( SELECT a.*, JSON_AGG(json_build_object( 'version', s.version, 'id', s.id, 'signedOn', s.signedOn, 'type', s.type, 'jws', s.jws )) AS signatures FROM origAudits a, audit_signature s WHERE s.audit_id = a.audit_id GROUP BY a.audit_id, a.version, a.agreement_id, a.hash_type, a.digest, a.created ) SELECT JSON_AGG(json_build_object( 'audit', json_build_object( 'version', a.version, 'agreementId', a.agreement_id, 'hashType', a.hash_type, 'digest', a.digest, 'created', a.created ), 'signatures', a.signatures )) AS records FROM audits a `, [ id, offset, limit, ]); return ret; } async function getAuditsByEventId(client, id, offset, limit) { let ret = await client.query(` WITH origAudits AS ( SELECT audit_id, version, event_id, hash_type, digest, created FROM audit WHERE event_id = $1 ORDER BY created DESC OFFSET $2 LIMIT $3 ), audits AS ( SELECT a.*, JSON_AGG(json_build_object( 'version', s.version, 'id', s.id, 'signedOn', s.signedOn, 'type', s.type, 'jws', s.jws )) AS signatures FROM origAudits a, audit_signature s WHERE s.audit_id = a.audit_id GROUP BY a.audit_id, a.version, a.event_id, a.hash_type, a.digest, a.created ) SELECT JSON_AGG(json_build_object( 'audit', json_build_object( 'version', a.version, 'eventId', a.event_id, 'hashType', a.hash_type, 'digest', a.digest, 'created', a.created ), 'signatures', a.signatures )) AS records FROM audits a `, [ id, offset, limit, ]); return ret; } async function getAuditsByMeta(client, meta, offset, limit) { const fields = [offset, limit]; let count = fields.length + 1; let whereInVals = `` for await (const [key, value] of Object.entries(meta)) { if (whereInVals != ``) whereInVals = ` AND ` whereInVals += `(key = $${count++} AND value = $${count++})`; fields.push(key); fields.push(value); } let ret = await client.query(` WITH origAudits AS ( SELECT audit_id, version, event_id, hash_type, digest, created FROM audit WHERE audit_id IN ( SELECT audit_id FROM audit_meta WHERE ${whereInVals} ) ORDER BY created DESC OFFSET $1 LIMIT $2 ), audits AS ( SELECT a.*, JSON_AGG(json_build_object( 'version', s.version, 'id', s.id, 'signedOn', s.signedOn, 'type', s.type, 'jws', s.jws )) AS signatures FROM origAudits a, audit_signature s WHERE s.audit_id = a.audit_id GROUP BY a.audit_id, a.version, a.event_id, a.hash_type, a.digest, a.created ) SELECT JSON_AGG(json_build_object( 'audit', json_build_object( 'version', a.version, 'eventId', a.event_id, 'hashType', a.hash_type, 'digest', a.digest, 'created', a.created ), 'signatures', a.signatures )) AS records FROM audits a `, fields); return ret; } async function getAudits(eventId, agreementId, meta, offset) { let ret; let res; const limit = 100; const client = await getPool(); try { if (eventId) { res = await getAuditsByEventId(client, eventId, offset, limit); } else if (agreementId) { res = await getAuditsByAgreementId(client, agreementId, offset, limit); } else { res = await getAuditsByMeta(client, meta, offset, limit); } if (res.rows.length > 0 && res.rows[0].records) { ret = { auditRecords: res.rows[0].records }; if (ret.auditRecords.length === limit) { ret.nextPageToken = Buffer.from(`${offset + limit}`).toString("base64"); } } } catch(e) { console.error(e); } finally { await client.release(); } return ret; } async function put(input) { let res = { success: false, error: 'Unknown error', }; try { const { audit, meta } = input; const error = getAuditErrors(audit); if (error) { res.error = error; console.log(`${prefix} - Bad Audit: ${error}`); } else { const id = audit.eventId ? audit.eventId : audit.agreementId; if (audit.eventId) { res.message = `event: ${audit.eventId}`; } else { res.message = `agreement: ${audit.agreementId}`; } await processAudit(input); res.success = true; delete res.error; } } catch (e) { if (e.message != 'audit record with id and digest exists') console.error(e); res.error = e.message; } return res; } async function get(input) { let res = { success: false, error: 'Unknown error', }; try { let error; let id; let type; if (input.eventId) { id = input.eventId; type = 'event'; error = isUuid(input.eventId) ? null : 'eventId invalid'; } else if (input.agreementId) { id = input.agreementId; type = 'agreement'; error = isUuid(input.agreementId) ? null : 'agreementId invalid'; } else if (input.meta) { id = ``; type = 'meta'; } else { error = 'eventId, agreementId, or meta required'; } if (error) { res.error = error; res.message = `bad request: ${error}`; } else { res.message = `requested ${type}: ${id}`; const offset = input.pageToken ? parseInt(Buffer.from(`${input.pageToken}`, "base64").toString('ascii')) : 0; let ret = await getAudits(input.eventId, input.agreementId, input.meta, offset); if (ret) { res.data = ret; res.success = true; delete res.error; } else { res.error = "not found" } } } catch (e) { if (e.message != 'audit record with id and digest exists') console.error(e); res.error = e.message; } return res; } export const archive = { put, get, }