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, }