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

559 lines
16 KiB
JavaScript

import pkg from '@jlinc/core';
const { JlincAgreement, JlincAudit } = pkg;
import { getPool } from "../../../db/index.js";
import { entity } from "./entity.js";
import { putQueue } from "../../../common/queue.js";
import { cacheAgreementMarkdown } from "../../../util/cacheAgreement.js";
async function getAgreement(client, userId, id, key) {
const whereClause = id ? `a.agreement_id_uuid = $1` : `a.agreement_id_uuid = (SELECT agreement_uuid FROM agreement_content WHERE lower(key) = lower($1))`;
const whereValue = id ? id : key;
const sql = `
SELECT
JSON_BUILD_OBJECT(
'version', a.version,
'@context', a.context,
'parent', a.parent,
'references', (
SELECT JSON_AGG(r.uri ORDER BY r.uri)
FROM reference r
INNER JOIN agreement_reference ar
ON r.id = ar.reference_id
AND ar.agreement_id = a.id
AND (
(r.user_id = a.user_id AND ar.user_id = a.user_id)
OR
(r.user_id IS NULL AND ar.user_id IS NULL AND a.user_id IS NULL)
)
),
'agreementId', a.agreement_id_uuid,
'created', a.created,
'ids', (
SELECT JSON_AGG(ari.required_id)
FROM agreement_required_id ari
WHERE ari.agreement_id = a.id
),
'purposes', (
SELECT JSON_AGG(p.value ORDER BY p.value)
FROM purpose p
INNER JOIN agreement_purpose ap
ON p.id = ap.purpose_id
AND ap.agreement_id = a.id
AND (
(p.user_id = a.user_id AND ap.user_id = a.user_id)
OR
(p.user_id IS NULL AND ap.user_id IS NULL AND a.user_id IS NULL)
)
),
'prohibitions', (
SELECT JSON_AGG(c.value ORDER BY c.value)
FROM prohibition c
INNER JOIN agreement_prohibition ac
ON c.id = ac.prohibition_id
AND ac.agreement_id = a.id
AND (
(c.user_id = ac.user_id AND ac.user_id = a.user_id)
OR
(c.user_id IS NULL AND ac.user_id IS NULL AND a.user_id IS NULL)
)
),
'validRoles', (
SELECT JSON_AGG(r.value ORDER BY r.value)
FROM role r
INNER JOIN agreement_role ar
ON r.id = ar.role_id
AND ar.agreement_id = a.id
AND (
(r.user_id = ar.user_id AND ar.user_id = a.user_id)
OR
(r.user_id IS NULL AND ar.user_id IS NULL AND a.user_id IS NULL)
)
)
) AS record
FROM agreement a
WHERE ${whereClause}
AND (
(
a.user_id = $2
OR a.user_id IS NULL
)
OR a.public IS TRUE
)
`
const res = await client.query(sql, [
whereValue,
userId,
]);
if (res.rows.length > 0 && res.rows[0].record) {
const ret = res.rows[0].record;
if (!ret.ids) ret.ids = [];
if (!ret.purposes) ret.purposes = [];
if (!ret.prohibitions) ret.prohibitions = [];
if (!ret.validRoles) ret.validRoles = [];
if (ret.version == 1) {
delete ret.references;
} else {
delete ret["@context"];
delete ret.parent;
ret.permitted = ret.purposes;
delete ret.purposes;
ret.prohibited = ret.prohibitions;
delete ret.prohibitions;
}
return ret;
}
return null;
}
async function getSignatures(client, userId, id) {
const sql = `
SELECT
JSON_AGG(
JSON_BUILD_OBJECT(
'version', s.version,
'id', s.signer_id,
'signedOn', s.signed_on,
'type', s.type,
'jws', s.jws,
'role', (
SELECT r.value
FROM role r
WHERE r.id = s.role_id
AND r.user_id = $2
)
)
) AS records
FROM signature s
INNER JOIN agreement a ON s.agreement_id = a.id
WHERE a.agreement_id_uuid = $1
AND a.user_id = $2;
`
const res = await client.query(sql, [
id,
userId,
]);
if (res.rows.length > 0 && res.rows[0].records) {
return res.rows[0].records;
}
return null;
}
async function get(input, userId) {
let response = {
success: false,
error: 'Unknown error',
};
const client = await getPool();
try {
const existingAgreement = await getAgreement(client, userId, input.agreementId, input.key)
if (!existingAgreement) {
response.error = 'agreement not found'
} else {
const data = {
agreement: existingAgreement
}
if (input.includeSignatures) {
data.signatures = await getSignatures(client, userId, input.agreementId)
for (let x = 0; x < data.signatures.length; x++) {
if (data.signatures[x].role === null) {
delete data.signatures[x].role;
}
}
}
response = {
message: `retrieved: ${input.agreementId}`,
data,
}
}
} catch(e) {
console.error(e)
} finally {
await client.release();
}
return response;
}
async function save(client, userId, agreement, publicAgreement) {
await client.query(`BEGIN`);
const isPublic = publicAgreement ? true : false;
const res = await client.query(`
INSERT INTO agreement (
user_id,
version,
parent,
agreement_id_uuid,
context,
public,
created,
created_as_ts
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
$7,
$8
) RETURNING id;
`, [
userId,
agreement.version,
agreement.parent,
agreement.agreementId,
agreement["@context"],
isPublic,
agreement.created,
new Date(agreement.created).toISOString(),
]);
for (const id of agreement.ids) {
await client.query(`
INSERT INTO agreement_required_id (
user_id,
agreement_id,
required_id
) VALUES (
$1,
$2,
$3
);
`, [
userId,
res.rows[0].id,
id,
]);
}
if (agreement.references) {
for (const reference of agreement.references) {
await client.query(`
INSERT INTO reference (
user_id,
uri
) VALUES (
$1,
$2
) ON CONFLICT DO NOTHING;
`, [
userId,
reference,
]);
await client.query(`
INSERT INTO agreement_reference (
user_id,
agreement_id,
reference_id
) VALUES (
$1,
$2,
(
SELECT id
FROM reference
WHERE uri = $3
AND user_id ${userId ? `= $1` : `IS NULL`}
)
);
`, [
userId,
res.rows[0].id,
reference,
]);
}
}
const purposes = agreement.version == 1 ? agreement.purposes : agreement.permitted;
for (const purpose of purposes) {
await client.query(`
INSERT INTO purpose (
user_id,
value
) VALUES (
$1,
$2
) ON CONFLICT DO NOTHING;
`, [
userId,
purpose,
]);
await client.query(`
INSERT INTO agreement_purpose (
user_id,
agreement_id,
purpose_id
) VALUES (
$1,
$2,
(
SELECT id
FROM purpose
WHERE value = $3
AND user_id ${userId ? `= $1` : `IS NULL`}
)
);
`, [
userId,
res.rows[0].id,
purpose,
]);
}
const prohibitions = agreement.version == 1 ? agreement.prohibitions : agreement.prohibited;
for (const prohibition of prohibitions) {
await client.query(`
INSERT INTO prohibition (
user_id,
value
) VALUES (
$1,
$2
) ON CONFLICT DO NOTHING;
`, [
userId,
prohibition,
]);
await client.query(`
INSERT INTO agreement_prohibition (
user_id,
agreement_id,
prohibition_id
) VALUES (
$1,
$2,
(
SELECT id
FROM prohibition
WHERE value = $3
AND user_id ${userId ? `= $1` : `IS NULL`}
)
);
`, [
userId,
res.rows[0].id,
prohibition,
]);
}
for (const role of agreement.validRoles) {
await client.query(`
INSERT INTO role (
user_id,
value
) VALUES (
$1,
$2
) ON CONFLICT DO NOTHING;
`, [
userId,
role,
]);
await client.query(`
INSERT INTO agreement_role (
user_id,
agreement_id,
role_id
) VALUES (
$1,
$2,
(
SELECT id
FROM role
WHERE value = $3
AND user_id ${userId ? `= $1` : `IS NULL`}
)
);
`, [
userId,
res.rows[0].id,
role,
]);
}
await client.query('COMMIT');
}
async function saveSignatures(client, userId, agreementId, signatures) {
await client.query(`BEGIN`);
for (const signature of signatures) {
await client.query(`
INSERT INTO signature (
user_id,
version,
signer_id,
signed_on,
type,
jws,
role_id,
agreement_id,
event_id,
signed_on_as_ts
) VALUES (
$1,
$2,
$3,
$4,
$5,
$6,
(
SELECT id
FROM role
WHERE value = $7
AND user_id = $1
),
(
SELECT id
FROM agreement
WHERE agreement_id_uuid = $8
AND user_id = $1
),
$9,
$10
) ON CONFLICT DO NOTHING;
`, [
userId,
signature.version,
signature.id,
signature.signedOn,
signature.type,
signature.jws,
signature.role,
agreementId,
null,
new Date(signature.signedOn).toISOString(),
]);
}
await client.query('COMMIT');
}
async function create(input, userId, _client, uuid) {
let response = {
success: false,
error: 'Unknown error',
};
const client = _client || await getPool();
try {
input.didDocs = [];
for (const shortName of input.shortNames) {
input.didDocs.push((await entity.getEntity(client, userId, shortName)).didDoc)
}
delete input.shortNames;
if (input.caveats) {
input.prohibitions = input.caveats;
delete input.caveats;
}
const agreement = await JlincAgreement.create(input);
if (uuid) {
agreement.agreementId = uuid;
}
await save(client, userId, agreement, input.public);
for (const uri of (agreement.references || [])) {
await cacheAgreementMarkdown({ userId, agreementUuid: agreement.agreementId, uri }, client);
}
response = {
message: `created and saved: ${agreement.agreementId}`,
data: agreement,
}
} catch(e) {
console.error(e)
} finally {
if (!_client)
await client.release();
}
return response;
}
async function process(input, userId, _client, _agreement) {
let response = {
success: false,
error: 'Unknown error',
};
const client = _client || await getPool();
try {
const existingAgreement = _agreement || await getAgreement(client, userId, input.agreementId)
if (!existingAgreement) {
throw new Error('agreement does not exist')
}
const inputEntity = input.shortName ? await entity.getEntity(client, userId, input.shortName) : null;
const didDoc = inputEntity ? inputEntity.didDoc : input.didDoc;
const signingKey = inputEntity ? inputEntity.controlPrivateKeyB64U : input.signingKey;
const signingPublicKey = inputEntity ? inputEntity.didDoc.verificationMethod[0].key : input.signingPublicKey;
const signingInput = {
agreement: existingAgreement,
didDoc,
signingKey,
signingPublicKey,
role: input.role,
}
const agreementData = await JlincAgreement.sign(signingInput);
await saveSignatures(client, userId, existingAgreement.agreementId, agreementData.signatures);
const audit = await JlincAudit.create(agreementData);
const auditInput = {
audit,
didDoc,
signingKey,
signingPublicKey,
}
const auditData = await JlincAudit.sign(auditInput);
response = {
message: `signed and saved: ${agreementData?.agreement?.agreementId}`,
data: {
auditData,
},
}
if (input.archive) {
await putQueue(
client,
'audit',
`${input.archive.url}/api/v1/audit/put`,
{
'Authorization': `Bearer ${input.archive.key}`,
},
response.data.auditData,
)
}
} catch(e) {
console.error(e)
} finally {
if (!_client)
await client.release();
}
return response;
}
async function produce(input, userId) {
let response = {
success: false,
error: 'Unknown error',
};
const client = await getPool();
try {
const created = (await create(input.data, userId, client)).data;
const processed = (await process(
{
agreementId: created.agreementId,
shortName: input.shortName,
role: input.role,
archive: input.archive,
},
userId,
client,
created,
)).data;
response = {
message: `created and processed: ${created.agreementId}`,
data: {
created,
processed,
},
}
} catch(e) {
console.error(e)
} finally {
await client.release();
}
return response;
}
export const agreement = {
getAgreement,
getSignatures,
get,
create,
process,
produce,
}