Files
2026-08-20 15:08:32 +00:00

434 lines
12 KiB
JavaScript

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 = `<meta key/value>`;
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,
}