settld 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dockerfile +2 -2
- package/docs/CONFIG.md +12 -0
- package/docs/README.md +3 -0
- package/docs/ops/HOSTED_BASELINE_R2.md +4 -2
- package/docs/ops/MINIMUM_PRODUCTION_TOPOLOGY.md +19 -7
- package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +8 -3
- package/package.json +4 -1
- package/packages/api-sdk/README.md +71 -0
- package/packages/api-sdk/src/client.js +1021 -0
- package/packages/api-sdk/src/express-middleware.js +163 -0
- package/packages/api-sdk/src/index.d.ts +1662 -0
- package/packages/api-sdk/src/index.js +10 -0
- package/packages/api-sdk/src/webhook-signature.js +182 -0
- package/packages/api-sdk/src/x402-autopay.js +210 -0
- package/scripts/ci/cli-pack-smoke.mjs +2 -0
- package/scripts/ci/run-public-onboarding-gate.mjs +136 -0
- package/scripts/setup/login.mjs +73 -2
- package/scripts/setup/onboard.mjs +173 -28
- package/scripts/setup/onboarding-failure-taxonomy.mjs +107 -0
- package/scripts/setup/onboarding-state-machine.mjs +102 -0
- package/services/magic-link/README.md +352 -0
- package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_criteria.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/evidence/evidence_index.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/metering/metering_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_definition.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_criteria.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/evidence/evidence_index.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/metering/metering_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/sla/sla_definition.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/sla/sla_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/trust.json +11 -0
- package/services/magic-link/src/audit-log.js +24 -0
- package/services/magic-link/src/buyer-auth.js +251 -0
- package/services/magic-link/src/buyer-notifications.js +402 -0
- package/services/magic-link/src/buyer-users.js +129 -0
- package/services/magic-link/src/decision-otp.js +187 -0
- package/services/magic-link/src/decisions.js +92 -0
- package/services/magic-link/src/email-resend.js +89 -0
- package/services/magic-link/src/ingest-keys.js +137 -0
- package/services/magic-link/src/maintenance.js +95 -0
- package/services/magic-link/src/onboarding-email-sequence.js +331 -0
- package/services/magic-link/src/payment-triggers.js +733 -0
- package/services/magic-link/src/pdf.js +149 -0
- package/services/magic-link/src/policy.js +69 -0
- package/services/magic-link/src/redaction.js +6 -0
- package/services/magic-link/src/render-model.js +70 -0
- package/services/magic-link/src/retention-gc.js +158 -0
- package/services/magic-link/src/run-records.js +496 -0
- package/services/magic-link/src/s3.js +171 -0
- package/services/magic-link/src/server.js +15849 -0
- package/services/magic-link/src/settlement-decisions.js +84 -0
- package/services/magic-link/src/smtp.js +217 -0
- package/services/magic-link/src/storage-cli.js +88 -0
- package/services/magic-link/src/storage-format.js +59 -0
- package/services/magic-link/src/tenant-billing.js +115 -0
- package/services/magic-link/src/tenant-onboarding.js +467 -0
- package/services/magic-link/src/tenant-settings.js +1140 -0
- package/services/magic-link/src/usage.js +80 -0
- package/services/magic-link/src/verify-queue.js +179 -0
- package/services/magic-link/src/verify-worker.js +157 -0
- package/services/magic-link/src/webhook-retries.js +542 -0
- package/services/magic-link/src/webhooks.js +218 -0
- package/src/api/app.js +135 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
function pdfEscapeText(s) {
|
|
2
|
+
return String(s ?? "")
|
|
3
|
+
.replaceAll("\\", "\\\\")
|
|
4
|
+
.replaceAll("(", "\\(")
|
|
5
|
+
.replaceAll(")", "\\)");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function concatBuffers(buffers) {
|
|
9
|
+
const total = buffers.reduce((sum, b) => sum + b.length, 0);
|
|
10
|
+
const out = Buffer.allocUnsafe(total);
|
|
11
|
+
let off = 0;
|
|
12
|
+
for (const b of buffers) {
|
|
13
|
+
b.copy(out, off);
|
|
14
|
+
off += b.length;
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function buildPdfObjects(objs) {
|
|
20
|
+
const header = Buffer.from("%PDF-1.4\n", "ascii");
|
|
21
|
+
const parts = [header];
|
|
22
|
+
const offsets = [0];
|
|
23
|
+
let offset = header.length;
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < objs.length; i += 1) {
|
|
26
|
+
offsets.push(offset);
|
|
27
|
+
const n = i + 1;
|
|
28
|
+
const body = Buffer.isBuffer(objs[i]) ? objs[i] : Buffer.from(String(objs[i]), "utf8");
|
|
29
|
+
const prefix = Buffer.from(`${n} 0 obj\n`, "ascii");
|
|
30
|
+
const suffix = Buffer.from("\nendobj\n", "ascii");
|
|
31
|
+
const chunk = concatBuffers([prefix, body, suffix]);
|
|
32
|
+
parts.push(chunk);
|
|
33
|
+
offset += chunk.length;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const xrefStart = offset;
|
|
37
|
+
const xrefLines = [];
|
|
38
|
+
xrefLines.push("xref\n");
|
|
39
|
+
xrefLines.push(`0 ${objs.length + 1}\n`);
|
|
40
|
+
xrefLines.push("0000000000 65535 f \n");
|
|
41
|
+
for (let i = 1; i < offsets.length; i += 1) {
|
|
42
|
+
const off = String(offsets[i]).padStart(10, "0");
|
|
43
|
+
xrefLines.push(`${off} 00000 n \n`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const xref = Buffer.from(xrefLines.join(""), "ascii");
|
|
47
|
+
parts.push(xref);
|
|
48
|
+
offset += xref.length;
|
|
49
|
+
|
|
50
|
+
const trailer = Buffer.from(
|
|
51
|
+
`trailer\n<< /Size ${objs.length + 1} /Root 1 0 R >>\nstartxref\n${xrefStart}\n%%EOF\n`,
|
|
52
|
+
"ascii"
|
|
53
|
+
);
|
|
54
|
+
parts.push(trailer);
|
|
55
|
+
return concatBuffers(parts);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatMoneyFromCentsString({ currency, cents }) {
|
|
59
|
+
const cur = String(currency ?? "").trim() || "UNK";
|
|
60
|
+
const raw = String(cents ?? "").trim();
|
|
61
|
+
if (!/^[0-9]+$/.test(raw)) return `${cur} ${raw}`;
|
|
62
|
+
if (cur === "USD") {
|
|
63
|
+
const padded = raw.padStart(3, "0");
|
|
64
|
+
const dollars = padded.slice(0, -2);
|
|
65
|
+
const centsPart = padded.slice(-2);
|
|
66
|
+
return `$${dollars}.${centsPart}`;
|
|
67
|
+
}
|
|
68
|
+
return `${cur} ${raw} cents`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function buildInvoiceSummaryPdf({ title, lines }) {
|
|
72
|
+
const safeTitle = String(title ?? "Invoice Summary (non-normative)");
|
|
73
|
+
const safeLines = Array.isArray(lines) ? lines.map((l) => String(l ?? "")) : [];
|
|
74
|
+
|
|
75
|
+
const pageWidth = 612;
|
|
76
|
+
const pageHeight = 792;
|
|
77
|
+
const fontSize = 11;
|
|
78
|
+
const leading = 14;
|
|
79
|
+
const startX = 72;
|
|
80
|
+
const startY = 720;
|
|
81
|
+
|
|
82
|
+
const content = [];
|
|
83
|
+
content.push("BT");
|
|
84
|
+
content.push(`/F1 ${fontSize} Tf`);
|
|
85
|
+
content.push(`${leading} TL`);
|
|
86
|
+
content.push(`${startX} ${startY} Td`);
|
|
87
|
+
content.push(`(${pdfEscapeText(safeTitle)}) Tj`);
|
|
88
|
+
content.push("T*");
|
|
89
|
+
for (const line of safeLines) {
|
|
90
|
+
content.push(`(${pdfEscapeText(line)}) Tj`);
|
|
91
|
+
content.push("T*");
|
|
92
|
+
}
|
|
93
|
+
content.push("ET");
|
|
94
|
+
const contentStream = Buffer.from(content.join("\n") + "\n", "ascii");
|
|
95
|
+
|
|
96
|
+
const obj1 = "<< /Type /Catalog /Pages 2 0 R >>";
|
|
97
|
+
const obj2 = "<< /Type /Pages /Kids [3 0 R] /Count 1 >>";
|
|
98
|
+
const obj3 = `<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${pageWidth} ${pageHeight}] /Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>`;
|
|
99
|
+
const obj4 = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>";
|
|
100
|
+
const obj5 = concatBuffers([Buffer.from(`<< /Length ${contentStream.length} >>\nstream\n`, "ascii"), contentStream, Buffer.from("endstream", "ascii")]);
|
|
101
|
+
|
|
102
|
+
return buildPdfObjects([obj1, obj2, obj3, obj4, obj5]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function buildInvoiceSummaryPdfFromClaim({ claim, verification, trust }) {
|
|
106
|
+
const invoiceId = typeof claim?.invoiceId === "string" ? claim.invoiceId : "";
|
|
107
|
+
const tenantId = typeof claim?.tenantId === "string" ? claim.tenantId : "";
|
|
108
|
+
const currency = typeof claim?.currency === "string" ? claim.currency : "UNK";
|
|
109
|
+
const totalCents = typeof claim?.totalCents === "string" ? claim.totalCents : "";
|
|
110
|
+
const createdAt = typeof claim?.createdAt === "string" ? claim.createdAt : "";
|
|
111
|
+
|
|
112
|
+
const status = verification?.status ?? "unknown";
|
|
113
|
+
const bundleSha256 = verification?.zipSha256 ?? "";
|
|
114
|
+
const manifestHash = verification?.manifestHash ?? "";
|
|
115
|
+
const mode = verification?.mode ?? "";
|
|
116
|
+
|
|
117
|
+
const trustLine = trust?.configured
|
|
118
|
+
? `Trust roots: ${Array.isArray(trust.keyIds) ? trust.keyIds.join(", ") : ""}`
|
|
119
|
+
: "Trust roots: none configured";
|
|
120
|
+
|
|
121
|
+
const lines = [];
|
|
122
|
+
if (invoiceId) lines.push(`Invoice: ${invoiceId}`);
|
|
123
|
+
if (tenantId) lines.push(`Tenant: ${tenantId}`);
|
|
124
|
+
if (createdAt) lines.push(`Created: ${createdAt}`);
|
|
125
|
+
if (totalCents) lines.push(`Total: ${formatMoneyFromCentsString({ currency, cents: totalCents })}`);
|
|
126
|
+
if (mode) lines.push(`Mode: ${mode}`);
|
|
127
|
+
lines.push(`Status: ${status}`);
|
|
128
|
+
if (bundleSha256) lines.push(`Bundle SHA-256: ${bundleSha256}`);
|
|
129
|
+
if (manifestHash) lines.push(`Manifest hash: ${manifestHash}`);
|
|
130
|
+
lines.push(trustLine);
|
|
131
|
+
|
|
132
|
+
const items = Array.isArray(claim?.lineItems) ? claim.lineItems : [];
|
|
133
|
+
if (items.length) {
|
|
134
|
+
lines.push("");
|
|
135
|
+
lines.push("Line items:");
|
|
136
|
+
for (const it of items.slice(0, 200)) {
|
|
137
|
+
const code = typeof it?.code === "string" ? it.code : "";
|
|
138
|
+
const qty = typeof it?.quantity === "string" ? it.quantity : "";
|
|
139
|
+
const unit = typeof it?.unitPriceCents === "string" ? it.unitPriceCents : "";
|
|
140
|
+
const amt = typeof it?.amountCents === "string" ? it.amountCents : "";
|
|
141
|
+
const row = [code && `code=${code}`, qty && `qty=${qty}`, unit && `unitCents=${unit}`, amt && `amountCents=${amt}`].filter(Boolean).join(" ");
|
|
142
|
+
if (row) lines.push(`- ${row}`);
|
|
143
|
+
}
|
|
144
|
+
if (items.length > 200) lines.push(`… (${items.length - 200} more)`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return buildInvoiceSummaryPdf({ title: "Invoice Summary (non-normative)", lines });
|
|
148
|
+
}
|
|
149
|
+
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
|
|
3
|
+
function isPlainObject(v) {
|
|
4
|
+
return Boolean(v && typeof v === "object" && !Array.isArray(v) && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function sha256Hex(text) {
|
|
8
|
+
return crypto.createHash("sha256").update(String(text ?? ""), "utf8").digest("hex");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function resolvePolicyForRun({ tenantSettings, vendorId, contractId }) {
|
|
12
|
+
const contractPolicies = isPlainObject(tenantSettings?.contractPolicies) ? tenantSettings.contractPolicies : null;
|
|
13
|
+
if (contractId && contractPolicies && isPlainObject(contractPolicies[contractId])) {
|
|
14
|
+
return { policy: contractPolicies[contractId], source: { kind: "contract", id: contractId } };
|
|
15
|
+
}
|
|
16
|
+
const vendorPolicies = isPlainObject(tenantSettings?.vendorPolicies) ? tenantSettings.vendorPolicies : null;
|
|
17
|
+
if (vendorId && vendorPolicies && isPlainObject(vendorPolicies[vendorId])) {
|
|
18
|
+
return { policy: vendorPolicies[vendorId], source: { kind: "vendor", id: vendorId } };
|
|
19
|
+
}
|
|
20
|
+
return { policy: null, source: null };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizePolicyProfileForEnforcement(profile) {
|
|
24
|
+
const p = isPlainObject(profile) ? profile : {};
|
|
25
|
+
const requiredModeRaw = p.requiredMode === undefined ? null : String(p.requiredMode ?? "").trim().toLowerCase();
|
|
26
|
+
const requiredMode = requiredModeRaw === "auto" || requiredModeRaw === "strict" || requiredModeRaw === "compat" ? requiredModeRaw : null;
|
|
27
|
+
const failOnWarnings = Boolean(p.failOnWarnings);
|
|
28
|
+
const allowAmberApprovals = p.allowAmberApprovals === undefined ? true : Boolean(p.allowAmberApprovals);
|
|
29
|
+
const requireProducerReceiptPresent = Boolean(p.requireProducerReceiptPresent);
|
|
30
|
+
const retentionDays =
|
|
31
|
+
p.retentionDays === null || p.retentionDays === undefined
|
|
32
|
+
? null
|
|
33
|
+
: Number.isInteger(p.retentionDays)
|
|
34
|
+
? p.retentionDays
|
|
35
|
+
: Number.parseInt(String(p.retentionDays ?? ""), 10);
|
|
36
|
+
const requiredSignerKeyIdsRaw = p.requiredPricingMatrixSignerKeyIds;
|
|
37
|
+
const requiredSignerKeyIds = Array.isArray(requiredSignerKeyIdsRaw)
|
|
38
|
+
? [...new Set(requiredSignerKeyIdsRaw.map((x) => String(x ?? "").trim()).filter(Boolean))].sort()
|
|
39
|
+
: null;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
requiredMode,
|
|
43
|
+
failOnWarnings,
|
|
44
|
+
allowAmberApprovals,
|
|
45
|
+
requireProducerReceiptPresent,
|
|
46
|
+
requiredSignerKeyIds,
|
|
47
|
+
retentionDays: Number.isInteger(retentionDays) && retentionDays > 0 ? retentionDays : null
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function policyHashHex(policyEffective) {
|
|
52
|
+
const obj = {
|
|
53
|
+
requiredMode: policyEffective?.requiredMode ?? null,
|
|
54
|
+
failOnWarnings: Boolean(policyEffective?.failOnWarnings),
|
|
55
|
+
allowAmberApprovals: policyEffective?.allowAmberApprovals === undefined ? true : Boolean(policyEffective?.allowAmberApprovals),
|
|
56
|
+
requireProducerReceiptPresent: Boolean(policyEffective?.requireProducerReceiptPresent),
|
|
57
|
+
requiredSignerKeyIds: Array.isArray(policyEffective?.requiredSignerKeyIds) ? policyEffective.requiredSignerKeyIds : null,
|
|
58
|
+
retentionDays: Number.isInteger(policyEffective?.retentionDays) ? policyEffective.retentionDays : null
|
|
59
|
+
};
|
|
60
|
+
return sha256Hex(JSON.stringify(obj));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function effectiveRetentionDaysForRun({ tenantSettings, vendorId, contractId }) {
|
|
64
|
+
const base = Number.isInteger(tenantSettings?.retentionDays) ? tenantSettings.retentionDays : 30;
|
|
65
|
+
const { policy } = resolvePolicyForRun({ tenantSettings, vendorId, contractId });
|
|
66
|
+
const eff = normalizePolicyProfileForEnforcement(policy);
|
|
67
|
+
return Number.isInteger(eff.retentionDays) ? eff.retentionDays : base;
|
|
68
|
+
}
|
|
69
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { safeTruncate } from "./redaction.js";
|
|
2
|
+
|
|
3
|
+
export const MAGIC_LINK_RENDER_MODEL_ALLOWLIST_V1 = {
|
|
4
|
+
schemaVersion: "MagicLinkRenderModelAllowlist.v1",
|
|
5
|
+
version: 1,
|
|
6
|
+
// Source of truth for fields that may appear in hosted outputs (HTML/PDF/CSV/support exports).
|
|
7
|
+
invoiceClaim: {
|
|
8
|
+
schemaVersion: { maxChars: 128 },
|
|
9
|
+
tenantId: { maxChars: 500 },
|
|
10
|
+
invoiceId: { maxChars: 500 },
|
|
11
|
+
createdAt: { maxChars: 500 },
|
|
12
|
+
currency: { maxChars: 16 },
|
|
13
|
+
subtotalCents: { maxChars: 64 },
|
|
14
|
+
totalCents: { maxChars: 64 },
|
|
15
|
+
lineItems: {
|
|
16
|
+
maxItems: 200,
|
|
17
|
+
fields: {
|
|
18
|
+
code: { maxChars: 200 },
|
|
19
|
+
quantity: { maxChars: 64 },
|
|
20
|
+
unitPriceCents: { maxChars: 64 },
|
|
21
|
+
amountCents: { maxChars: 64 }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
metering: { itemsCount: true, evidenceRefsCount: true },
|
|
26
|
+
decision: {
|
|
27
|
+
decision: { maxChars: 64 },
|
|
28
|
+
decidedAt: { maxChars: 500 },
|
|
29
|
+
decidedByEmail: { pii: true, maxChars: 320 }
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
function isPlainObject(v) {
|
|
34
|
+
return Boolean(v && typeof v === "object" && !Array.isArray(v) && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function buildPublicInvoiceClaimFromClaimJson(claimJson) {
|
|
38
|
+
if (!isPlainObject(claimJson)) return null;
|
|
39
|
+
return {
|
|
40
|
+
schemaVersion: typeof claimJson.schemaVersion === "string" ? safeTruncate(claimJson.schemaVersion, { max: 128 }) : claimJson.schemaVersion ?? null,
|
|
41
|
+
tenantId: typeof claimJson.tenantId === "string" ? safeTruncate(claimJson.tenantId, { max: 500 }) : null,
|
|
42
|
+
invoiceId: typeof claimJson.invoiceId === "string" ? safeTruncate(claimJson.invoiceId, { max: 500 }) : null,
|
|
43
|
+
createdAt: typeof claimJson.createdAt === "string" ? safeTruncate(claimJson.createdAt, { max: 500 }) : null,
|
|
44
|
+
currency: typeof claimJson.currency === "string" ? safeTruncate(claimJson.currency, { max: 16 }) : null,
|
|
45
|
+
subtotalCents: typeof claimJson.subtotalCents === "string" ? safeTruncate(claimJson.subtotalCents, { max: 64 }) : claimJson.subtotalCents ?? null,
|
|
46
|
+
totalCents: typeof claimJson.totalCents === "string" ? safeTruncate(claimJson.totalCents, { max: 64 }) : null,
|
|
47
|
+
lineItems: Array.isArray(claimJson.lineItems)
|
|
48
|
+
? claimJson.lineItems.slice(0, 200).map((it) => ({
|
|
49
|
+
code: typeof it?.code === "string" ? safeTruncate(it.code, { max: 200 }) : null,
|
|
50
|
+
quantity: typeof it?.quantity === "string" ? safeTruncate(it.quantity, { max: 64 }) : null,
|
|
51
|
+
unitPriceCents: typeof it?.unitPriceCents === "string" ? safeTruncate(it.unitPriceCents, { max: 64 }) : null,
|
|
52
|
+
amountCents: typeof it?.amountCents === "string" ? safeTruncate(it.amountCents, { max: 64 }) : null
|
|
53
|
+
}))
|
|
54
|
+
: []
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function sampleRenderModelInvoiceClaimV1() {
|
|
59
|
+
return {
|
|
60
|
+
schemaVersion: "InvoiceClaim.v1",
|
|
61
|
+
tenantId: "tenant_demo",
|
|
62
|
+
invoiceId: "invoice_demo_1",
|
|
63
|
+
createdAt: "2026-02-05T00:00:00.000Z",
|
|
64
|
+
currency: "USD",
|
|
65
|
+
subtotalCents: "10000",
|
|
66
|
+
totalCents: "10000",
|
|
67
|
+
lineItems: [{ code: "WORK_MINUTES", quantity: "10", unitPriceCents: "100", amountCents: "1000" }]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { effectiveRetentionDaysForRun } from "./policy.js";
|
|
5
|
+
import { deleteRunRecordBestEffort } from "./run-records.js";
|
|
6
|
+
|
|
7
|
+
function isPastRetention(createdAtIso, retentionDays) {
|
|
8
|
+
const createdMs = Date.parse(String(createdAtIso ?? ""));
|
|
9
|
+
if (!Number.isFinite(createdMs)) return true;
|
|
10
|
+
const days = Number.isInteger(retentionDays) ? retentionDays : 30;
|
|
11
|
+
return Date.now() > createdMs + days * 24 * 3600 * 1000;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function loadMeta({ dataDir, token }) {
|
|
15
|
+
const fp = path.join(dataDir, "meta", `${token}.json`);
|
|
16
|
+
const raw = await fs.readFile(fp, "utf8");
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function deleteTokenFilesBestEffort({ dataDir, token, meta }) {
|
|
21
|
+
const paths = [];
|
|
22
|
+
const zipPath = typeof meta?.zipPath === "string" ? meta.zipPath : path.join(dataDir, "zips", `${token}.zip`);
|
|
23
|
+
const verifyJsonPath = typeof meta?.verifyJsonPath === "string" ? meta.verifyJsonPath : path.join(dataDir, "verify", `${token}.json`);
|
|
24
|
+
const metaPath = path.join(dataDir, "meta", `${token}.json`);
|
|
25
|
+
const publicJsonPath = typeof meta?.publicJsonPath === "string" ? meta.publicJsonPath : path.join(dataDir, "public", `${token}.json`);
|
|
26
|
+
const receiptJsonPath = typeof meta?.receiptJsonPath === "string" ? meta.receiptJsonPath : path.join(dataDir, "receipt", `${token}.json`);
|
|
27
|
+
const summaryPdfPath = typeof meta?.summaryPdfPath === "string" ? meta.summaryPdfPath : path.join(dataDir, "pdf", `${token}.pdf`);
|
|
28
|
+
const decisionPath = path.join(dataDir, "decisions", `${token}.json`);
|
|
29
|
+
const settlementDecisionsDir = path.join(dataDir, "settlement_decisions", token);
|
|
30
|
+
const closePackDir = typeof meta?.closePackDir === "string" ? meta.closePackDir : path.join(dataDir, "closepack", token);
|
|
31
|
+
const approvalClosePackZipPath =
|
|
32
|
+
typeof meta?.approvalClosePackZipPath === "string" ? meta.approvalClosePackZipPath : path.join(dataDir, "closepack_exports", `${token}.zip`);
|
|
33
|
+
|
|
34
|
+
// Delete blobs first, then metadata/index surfaces.
|
|
35
|
+
paths.push(zipPath, closePackDir, approvalClosePackZipPath, verifyJsonPath, summaryPdfPath, receiptJsonPath, decisionPath, settlementDecisionsDir, publicJsonPath, metaPath);
|
|
36
|
+
|
|
37
|
+
// Webhook delivery records are stored outside per-run meta paths; remove attempts/records for this token.
|
|
38
|
+
for (const sub of ["attempts", "record"]) {
|
|
39
|
+
const dir = path.join(dataDir, "webhooks", sub);
|
|
40
|
+
let names = [];
|
|
41
|
+
try {
|
|
42
|
+
// eslint-disable-next-line no-await-in-loop
|
|
43
|
+
names = (await fs.readdir(dir)).filter((n) => n.endsWith(".json") && n.startsWith(`${token}_`));
|
|
44
|
+
} catch {
|
|
45
|
+
names = [];
|
|
46
|
+
}
|
|
47
|
+
for (const name of names) {
|
|
48
|
+
const fp = path.join(dir, name);
|
|
49
|
+
try {
|
|
50
|
+
// eslint-disable-next-line no-await-in-loop
|
|
51
|
+
await fs.rm(fp, { force: true });
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Persistent webhook retry queue entries can outlive retention windows;
|
|
59
|
+
// remove jobs that target this token from pending/dead-letter buckets.
|
|
60
|
+
for (const sub of ["pending", "dead-letter"]) {
|
|
61
|
+
const dir = path.join(dataDir, "webhook_retry", sub);
|
|
62
|
+
let names = [];
|
|
63
|
+
try {
|
|
64
|
+
// eslint-disable-next-line no-await-in-loop
|
|
65
|
+
names = (await fs.readdir(dir)).filter((n) => n.endsWith(".json") && n.includes(`_${token}_`));
|
|
66
|
+
} catch {
|
|
67
|
+
names = [];
|
|
68
|
+
}
|
|
69
|
+
for (const name of names) {
|
|
70
|
+
const fp = path.join(dir, name);
|
|
71
|
+
try {
|
|
72
|
+
// eslint-disable-next-line no-await-in-loop
|
|
73
|
+
await fs.rm(fp, { force: true });
|
|
74
|
+
} catch {
|
|
75
|
+
// ignore
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const p of paths) {
|
|
81
|
+
try {
|
|
82
|
+
// eslint-disable-next-line no-await-in-loop
|
|
83
|
+
await fs.rm(p, { recursive: true, force: true });
|
|
84
|
+
} catch {
|
|
85
|
+
// ignore
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function listTenantIdsWithIndex({ dataDir, max = 10_000 } = {}) {
|
|
91
|
+
const idxRoot = path.join(dataDir, "index");
|
|
92
|
+
let entries = [];
|
|
93
|
+
try {
|
|
94
|
+
entries = await fs.readdir(idxRoot, { withFileTypes: true });
|
|
95
|
+
} catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const tenants = entries.filter((e) => e.isDirectory()).map((e) => e.name).slice(0, max);
|
|
99
|
+
tenants.sort();
|
|
100
|
+
return tenants;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function garbageCollectTenantByRetention({ dataDir, tenantId, tenantSettings }) {
|
|
104
|
+
const idxDir = path.join(dataDir, "index", tenantId);
|
|
105
|
+
let names = [];
|
|
106
|
+
try {
|
|
107
|
+
names = (await fs.readdir(idxDir)).filter((n) => n.endsWith(".json"));
|
|
108
|
+
} catch {
|
|
109
|
+
return { ok: true, deleted: 0, kept: 0 };
|
|
110
|
+
}
|
|
111
|
+
let deleted = 0;
|
|
112
|
+
let kept = 0;
|
|
113
|
+
for (const name of names) {
|
|
114
|
+
const fp = path.join(idxDir, name);
|
|
115
|
+
let idx = null;
|
|
116
|
+
try {
|
|
117
|
+
// eslint-disable-next-line no-await-in-loop
|
|
118
|
+
idx = JSON.parse(await fs.readFile(fp, "utf8"));
|
|
119
|
+
} catch {
|
|
120
|
+
idx = null;
|
|
121
|
+
}
|
|
122
|
+
const token = typeof idx?.token === "string" ? idx.token : null;
|
|
123
|
+
if (!token || !/^ml_[0-9a-f]{48}$/.test(token)) {
|
|
124
|
+
// eslint-disable-next-line no-await-in-loop
|
|
125
|
+
await fs.rm(fp, { force: true });
|
|
126
|
+
deleted += 1;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
let meta = null;
|
|
130
|
+
try {
|
|
131
|
+
// eslint-disable-next-line no-await-in-loop
|
|
132
|
+
meta = await loadMeta({ dataDir, token });
|
|
133
|
+
} catch {
|
|
134
|
+
meta = null;
|
|
135
|
+
}
|
|
136
|
+
const retentionDays = meta
|
|
137
|
+
? effectiveRetentionDaysForRun({
|
|
138
|
+
tenantSettings,
|
|
139
|
+
vendorId: typeof meta.vendorId === "string" ? meta.vendorId : null,
|
|
140
|
+
contractId: typeof meta.contractId === "string" ? meta.contractId : null
|
|
141
|
+
})
|
|
142
|
+
: Number.isInteger(tenantSettings?.retentionDays)
|
|
143
|
+
? tenantSettings.retentionDays
|
|
144
|
+
: 30;
|
|
145
|
+
if (!meta || isPastRetention(meta.createdAt, retentionDays)) {
|
|
146
|
+
// eslint-disable-next-line no-await-in-loop
|
|
147
|
+
await deleteTokenFilesBestEffort({ dataDir, token, meta });
|
|
148
|
+
// eslint-disable-next-line no-await-in-loop
|
|
149
|
+
await deleteRunRecordBestEffort({ dataDir, tenantId, token });
|
|
150
|
+
// eslint-disable-next-line no-await-in-loop
|
|
151
|
+
await fs.rm(fp, { force: true });
|
|
152
|
+
deleted += 1;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
kept += 1;
|
|
156
|
+
}
|
|
157
|
+
return { ok: true, deleted, kept };
|
|
158
|
+
}
|