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.
Files changed (128) hide show
  1. package/Dockerfile +2 -2
  2. package/docs/CONFIG.md +12 -0
  3. package/docs/README.md +3 -0
  4. package/docs/ops/HOSTED_BASELINE_R2.md +4 -2
  5. package/docs/ops/MINIMUM_PRODUCTION_TOPOLOGY.md +19 -7
  6. package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +8 -3
  7. package/package.json +4 -1
  8. package/packages/api-sdk/README.md +71 -0
  9. package/packages/api-sdk/src/client.js +1021 -0
  10. package/packages/api-sdk/src/express-middleware.js +163 -0
  11. package/packages/api-sdk/src/index.d.ts +1662 -0
  12. package/packages/api-sdk/src/index.js +10 -0
  13. package/packages/api-sdk/src/webhook-signature.js +182 -0
  14. package/packages/api-sdk/src/x402-autopay.js +210 -0
  15. package/scripts/ci/cli-pack-smoke.mjs +2 -0
  16. package/scripts/ci/run-public-onboarding-gate.mjs +136 -0
  17. package/scripts/setup/login.mjs +73 -2
  18. package/scripts/setup/onboard.mjs +173 -28
  19. package/scripts/setup/onboarding-failure-taxonomy.mjs +107 -0
  20. package/scripts/setup/onboarding-state-machine.mjs +102 -0
  21. package/services/magic-link/README.md +352 -0
  22. package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_criteria.json +1 -0
  23. package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_evaluation.json +1 -0
  24. package/services/magic-link/assets/samples/closepack/known-bad/attestation/bundle_head_attestation.json +1 -0
  25. package/services/magic-link/assets/samples/closepack/known-bad/evidence/evidence_index.json +1 -0
  26. package/services/magic-link/assets/samples/closepack/known-bad/governance/policy.json +1 -0
  27. package/services/magic-link/assets/samples/closepack/known-bad/governance/revocations.json +1 -0
  28. package/services/magic-link/assets/samples/closepack/known-bad/manifest.json +1 -0
  29. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
  30. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/policy.json +1 -0
  31. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/revocations.json +1 -0
  32. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
  33. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/manifest.json +1 -0
  34. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/metering/metering_report.json +1 -0
  35. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
  36. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
  37. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
  38. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
  39. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
  40. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
  41. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
  42. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
  43. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
  44. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
  45. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
  46. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
  47. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
  48. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
  49. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
  50. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
  51. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
  52. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
  53. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/settld.json +1 -0
  54. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/verify/verification_report.json +1 -0
  55. package/services/magic-link/assets/samples/closepack/known-bad/settld.json +1 -0
  56. package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_definition.json +1 -0
  57. package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_evaluation.json +1 -0
  58. package/services/magic-link/assets/samples/closepack/known-bad/verify/verification_report.json +1 -0
  59. package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_criteria.json +1 -0
  60. package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_evaluation.json +1 -0
  61. package/services/magic-link/assets/samples/closepack/known-good/attestation/bundle_head_attestation.json +1 -0
  62. package/services/magic-link/assets/samples/closepack/known-good/evidence/evidence_index.json +1 -0
  63. package/services/magic-link/assets/samples/closepack/known-good/governance/policy.json +1 -0
  64. package/services/magic-link/assets/samples/closepack/known-good/governance/revocations.json +1 -0
  65. package/services/magic-link/assets/samples/closepack/known-good/manifest.json +1 -0
  66. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
  67. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/policy.json +1 -0
  68. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/revocations.json +1 -0
  69. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
  70. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/manifest.json +1 -0
  71. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/metering/metering_report.json +1 -0
  72. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
  73. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
  74. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
  75. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
  76. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
  77. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
  78. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
  79. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
  80. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
  81. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
  82. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
  83. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
  84. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
  85. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
  86. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
  87. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
  88. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
  89. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
  90. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/settld.json +1 -0
  91. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/verify/verification_report.json +1 -0
  92. package/services/magic-link/assets/samples/closepack/known-good/settld.json +1 -0
  93. package/services/magic-link/assets/samples/closepack/known-good/sla/sla_definition.json +1 -0
  94. package/services/magic-link/assets/samples/closepack/known-good/sla/sla_evaluation.json +1 -0
  95. package/services/magic-link/assets/samples/closepack/known-good/verify/verification_report.json +1 -0
  96. package/services/magic-link/assets/samples/trust.json +11 -0
  97. package/services/magic-link/src/audit-log.js +24 -0
  98. package/services/magic-link/src/buyer-auth.js +251 -0
  99. package/services/magic-link/src/buyer-notifications.js +402 -0
  100. package/services/magic-link/src/buyer-users.js +129 -0
  101. package/services/magic-link/src/decision-otp.js +187 -0
  102. package/services/magic-link/src/decisions.js +92 -0
  103. package/services/magic-link/src/email-resend.js +89 -0
  104. package/services/magic-link/src/ingest-keys.js +137 -0
  105. package/services/magic-link/src/maintenance.js +95 -0
  106. package/services/magic-link/src/onboarding-email-sequence.js +331 -0
  107. package/services/magic-link/src/payment-triggers.js +733 -0
  108. package/services/magic-link/src/pdf.js +149 -0
  109. package/services/magic-link/src/policy.js +69 -0
  110. package/services/magic-link/src/redaction.js +6 -0
  111. package/services/magic-link/src/render-model.js +70 -0
  112. package/services/magic-link/src/retention-gc.js +158 -0
  113. package/services/magic-link/src/run-records.js +496 -0
  114. package/services/magic-link/src/s3.js +171 -0
  115. package/services/magic-link/src/server.js +15849 -0
  116. package/services/magic-link/src/settlement-decisions.js +84 -0
  117. package/services/magic-link/src/smtp.js +217 -0
  118. package/services/magic-link/src/storage-cli.js +88 -0
  119. package/services/magic-link/src/storage-format.js +59 -0
  120. package/services/magic-link/src/tenant-billing.js +115 -0
  121. package/services/magic-link/src/tenant-onboarding.js +467 -0
  122. package/services/magic-link/src/tenant-settings.js +1140 -0
  123. package/services/magic-link/src/usage.js +80 -0
  124. package/services/magic-link/src/verify-queue.js +179 -0
  125. package/services/magic-link/src/verify-worker.js +157 -0
  126. package/services/magic-link/src/webhook-retries.js +542 -0
  127. package/services/magic-link/src/webhooks.js +218 -0
  128. package/src/api/app.js +135 -1
@@ -0,0 +1,80 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ function isPlainObject(v) {
5
+ return Boolean(v && typeof v === "object" && !Array.isArray(v) && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null));
6
+ }
7
+
8
+ export function monthKeyUtcNow(d = new Date()) {
9
+ const year = d.getUTCFullYear();
10
+ const month = String(d.getUTCMonth() + 1).padStart(2, "0");
11
+ return `${year}-${month}`;
12
+ }
13
+
14
+ function usageJsonlPath({ dataDir, tenantId, monthKey }) {
15
+ return path.join(dataDir, "usage", tenantId, `${monthKey}.jsonl`);
16
+ }
17
+
18
+ function usageSummaryPath({ dataDir, tenantId, monthKey }) {
19
+ return path.join(dataDir, "usage", tenantId, `${monthKey}.summary.json`);
20
+ }
21
+
22
+ export async function appendUsageRecord({ dataDir, tenantId, monthKey, record }) {
23
+ const jsonl = usageJsonlPath({ dataDir, tenantId, monthKey });
24
+ const summaryFp = usageSummaryPath({ dataDir, tenantId, monthKey });
25
+ await fs.mkdir(path.dirname(jsonl), { recursive: true });
26
+
27
+ const line = JSON.stringify(record ?? {}) + "\n";
28
+ await fs.appendFile(jsonl, line, "utf8");
29
+
30
+ let summary = null;
31
+ try {
32
+ summary = JSON.parse(await fs.readFile(summaryFp, "utf8"));
33
+ } catch {
34
+ summary = null;
35
+ }
36
+ const cur = isPlainObject(summary) ? summary : { schemaVersion: "MagicLinkUsageSummary.v1", month: monthKey, verificationRuns: 0, uploadedBytes: 0, totalDurationMs: 0 };
37
+
38
+ cur.month = monthKey;
39
+ cur.verificationRuns = Number(cur.verificationRuns ?? 0) + 1;
40
+ cur.uploadedBytes = Number(cur.uploadedBytes ?? 0) + Number(record?.zipBytes ?? 0);
41
+ cur.totalDurationMs = Number(cur.totalDurationMs ?? 0) + Number(record?.durationMs ?? 0);
42
+ cur.lastUpdatedAt = new Date().toISOString();
43
+
44
+ await fs.writeFile(summaryFp, JSON.stringify(cur, null, 2) + "\n", "utf8");
45
+ return cur;
46
+ }
47
+
48
+ export async function loadUsageSummary({ dataDir, tenantId, monthKey }) {
49
+ const fp = usageSummaryPath({ dataDir, tenantId, monthKey });
50
+ try {
51
+ const raw = await fs.readFile(fp, "utf8");
52
+ const j = JSON.parse(raw);
53
+ if (!isPlainObject(j)) return { schemaVersion: "MagicLinkUsageSummary.v1", month: monthKey, verificationRuns: 0, uploadedBytes: 0, totalDurationMs: 0 };
54
+ return { schemaVersion: "MagicLinkUsageSummary.v1", month: monthKey, verificationRuns: 0, uploadedBytes: 0, totalDurationMs: 0, ...j };
55
+ } catch {
56
+ return { schemaVersion: "MagicLinkUsageSummary.v1", month: monthKey, verificationRuns: 0, uploadedBytes: 0, totalDurationMs: 0 };
57
+ }
58
+ }
59
+
60
+ export async function loadUsageRecords({ dataDir, tenantId, monthKey, limit = 10_000 } = {}) {
61
+ const jsonl = usageJsonlPath({ dataDir, tenantId, monthKey });
62
+ try {
63
+ const raw = await fs.readFile(jsonl, "utf8");
64
+ const lines = raw.split("\n").filter((l) => l.trim());
65
+ const out = [];
66
+ for (const line of lines.slice(-limit)) {
67
+ try {
68
+ const j = JSON.parse(line);
69
+ if (isPlainObject(j)) out.push(j);
70
+ } catch {
71
+ // ignore
72
+ }
73
+ }
74
+ out.sort((a, b) => String(a.startedAt ?? "").localeCompare(String(b.startedAt ?? "")) || String(a.token ?? "").localeCompare(String(b.token ?? "")));
75
+ return out;
76
+ } catch {
77
+ return [];
78
+ }
79
+ }
80
+
@@ -0,0 +1,179 @@
1
+ import crypto from "node:crypto";
2
+
3
+ function sleep(ms) {
4
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0)));
5
+ }
6
+
7
+ class InMemoryQueue {
8
+ constructor() {
9
+ this.items = [];
10
+ this.waiters = [];
11
+ }
12
+
13
+ size() {
14
+ return this.items.length;
15
+ }
16
+
17
+ enqueue(item) {
18
+ if (this.waiters.length) {
19
+ const waiter = this.waiters.shift();
20
+ waiter(item);
21
+ return;
22
+ }
23
+ this.items.push(item);
24
+ }
25
+
26
+ async dequeue() {
27
+ if (this.items.length) return this.items.shift();
28
+ return await new Promise((resolve) => {
29
+ this.waiters.push(resolve);
30
+ });
31
+ }
32
+ }
33
+
34
+ function jobId() {
35
+ return `vq_${crypto.randomBytes(16).toString("hex")}`;
36
+ }
37
+
38
+ export function createVerifyQueue({
39
+ workerCount = 1,
40
+ maxAttempts = 3,
41
+ retryBackoffMs = 250,
42
+ handler,
43
+ onRetry = null,
44
+ onDeadLetter = null,
45
+ onDepthChange = null
46
+ } = {}) {
47
+ if (typeof handler !== "function") throw new TypeError("handler is required");
48
+ const workers = Math.max(1, Number.parseInt(String(workerCount ?? "1"), 10) || 1);
49
+ const queue = new InMemoryQueue();
50
+ const deadLetters = [];
51
+ let closed = false;
52
+
53
+ function emitDepth() {
54
+ if (typeof onDepthChange === "function") {
55
+ try {
56
+ onDepthChange(queue.size());
57
+ } catch {
58
+ // ignore
59
+ }
60
+ }
61
+ }
62
+
63
+ function enqueue(entry) {
64
+ queue.enqueue(entry);
65
+ emitDepth();
66
+ }
67
+
68
+ function requeue(entry) {
69
+ const backoff = Math.max(0, Math.trunc(retryBackoffMs * Math.pow(2, Math.max(0, entry.attempt - 1))));
70
+ setTimeout(() => {
71
+ if (closed) return;
72
+ enqueue(entry);
73
+ }, backoff);
74
+ }
75
+
76
+ async function workerLoop() {
77
+ while (!closed) {
78
+ const entry = await queue.dequeue();
79
+ emitDepth();
80
+ if (!entry || closed) continue;
81
+ const startedAtMs = Date.now();
82
+ let out = null;
83
+ try {
84
+ out = await handler(entry.payload);
85
+ } catch (err) {
86
+ out = { ok: false, error: "VERIFY_QUEUE_HANDLER_ERROR", detail: { message: err?.message ?? String(err ?? "") } };
87
+ }
88
+
89
+ const ok = Boolean(out?.ok);
90
+ if (ok) {
91
+ entry.resolve({
92
+ ...out,
93
+ queued: {
94
+ id: entry.id,
95
+ attempt: entry.attempt,
96
+ startedAt: new Date(startedAtMs).toISOString(),
97
+ finishedAt: new Date().toISOString(),
98
+ latencyMs: Math.max(0, Date.now() - entry.enqueuedAtMs)
99
+ }
100
+ });
101
+ continue;
102
+ }
103
+
104
+ if (entry.attempt < maxAttempts) {
105
+ entry.attempt += 1;
106
+ if (typeof onRetry === "function") {
107
+ try {
108
+ onRetry({ id: entry.id, attempt: entry.attempt, error: out?.error ?? "VERIFY_FAILED" });
109
+ } catch {
110
+ // ignore
111
+ }
112
+ }
113
+ requeue(entry);
114
+ continue;
115
+ }
116
+
117
+ const dead = {
118
+ id: entry.id,
119
+ attempt: entry.attempt,
120
+ enqueuedAt: new Date(entry.enqueuedAtMs).toISOString(),
121
+ failedAt: new Date().toISOString(),
122
+ payload: entry.payload,
123
+ result: out
124
+ };
125
+ deadLetters.push(dead);
126
+ if (typeof onDeadLetter === "function") {
127
+ try {
128
+ onDeadLetter(dead);
129
+ } catch {
130
+ // ignore
131
+ }
132
+ }
133
+ entry.resolve({
134
+ ok: false,
135
+ error: out?.error ?? "VERIFY_QUEUE_DEAD_LETTER",
136
+ detail: out?.detail ?? null,
137
+ deadLetter: dead
138
+ });
139
+ }
140
+ }
141
+
142
+ for (let i = 0; i < workers; i += 1) {
143
+ workerLoop().catch(() => {
144
+ // ignore worker loop failures; per-job failures are reported through queue outputs.
145
+ });
146
+ }
147
+
148
+ return {
149
+ async submit(payload) {
150
+ if (closed) return { ok: false, error: "VERIFY_QUEUE_CLOSED" };
151
+ return await new Promise((resolve) => {
152
+ enqueue({ id: jobId(), payload, attempt: 1, enqueuedAtMs: Date.now(), resolve });
153
+ });
154
+ },
155
+ stats() {
156
+ return { queued: queue.size(), workers, deadLetters: deadLetters.length };
157
+ },
158
+ deadLetters() {
159
+ return [...deadLetters];
160
+ },
161
+ async drain({ timeoutMs = 30_000 } = {}) {
162
+ const started = Date.now();
163
+ while (queue.size() > 0) {
164
+ if (Date.now() - started > timeoutMs) return { ok: false, error: "VERIFY_QUEUE_DRAIN_TIMEOUT" };
165
+ // eslint-disable-next-line no-await-in-loop
166
+ await sleep(10);
167
+ }
168
+ return { ok: true };
169
+ },
170
+ close() {
171
+ closed = true;
172
+ while (queue.waiters.length) {
173
+ const waiter = queue.waiters.shift();
174
+ waiter(null);
175
+ }
176
+ }
177
+ };
178
+ }
179
+
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ import fsSync from "node:fs";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+
6
+ import { verifyClosePackBundleDir, verifyInvoiceBundleDir } from "../../../packages/artifact-verify/src/index.js";
7
+
8
+ function usage() {
9
+ // eslint-disable-next-line no-console
10
+ console.error("usage: verify-worker --dir <bundleDir> [--strict|--nonstrict] [--hash-concurrency <n>]");
11
+ process.exit(2);
12
+ }
13
+
14
+ function readJsonSync(fp) {
15
+ const raw = fsSync.readFileSync(fp, "utf8");
16
+ return JSON.parse(raw);
17
+ }
18
+
19
+ function readJsonIfExistsSync(fp) {
20
+ try {
21
+ return readJsonSync(fp);
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+
27
+ function readBundleType(dir) {
28
+ const fp = path.join(dir, "settld.json");
29
+ try {
30
+ const raw = fsSync.readFileSync(fp, "utf8");
31
+ const j = JSON.parse(raw);
32
+ const t = typeof j?.type === "string" ? j.type : null;
33
+ return t;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ function parseArgs(argv) {
40
+ const out = { dir: null, strict: false, hashConcurrency: 16 };
41
+ for (let i = 0; i < argv.length; i += 1) {
42
+ const a = argv[i];
43
+ if (a === "--dir") {
44
+ out.dir = String(argv[i + 1] ?? "");
45
+ if (!out.dir) usage();
46
+ i += 1;
47
+ continue;
48
+ }
49
+ if (a === "--strict") {
50
+ out.strict = true;
51
+ continue;
52
+ }
53
+ if (a === "--nonstrict") {
54
+ out.strict = false;
55
+ continue;
56
+ }
57
+ if (a === "--hash-concurrency") {
58
+ const v = argv[i + 1] ?? null;
59
+ const n = Number.parseInt(String(v ?? ""), 10);
60
+ if (!Number.isInteger(n) || n < 1) usage();
61
+ out.hashConcurrency = n;
62
+ i += 1;
63
+ continue;
64
+ }
65
+ usage();
66
+ }
67
+ if (!out.dir) usage();
68
+ return out;
69
+ }
70
+
71
+ function deriveEvidenceTypeKeyFromIndexItem(it) {
72
+ if (!it || typeof it !== "object" || Array.isArray(it)) return null;
73
+ const kind = typeof it.kind === "string" ? it.kind.toLowerCase() : "";
74
+ const contentType = typeof it.contentType === "string" ? it.contentType.toLowerCase() : "";
75
+ const key = typeof it.key === "string" ? it.key.toLowerCase() : "";
76
+ const pathValue = typeof it.path === "string" ? it.path.toLowerCase() : "";
77
+
78
+ if (kind.includes("checkpoint") || key.includes("checkpoint") || pathValue.includes("checkpoint")) return "checkpoint";
79
+ if (kind.includes("gps") || key.includes("gps") || pathValue.includes("gps") || kind.includes("track") || key.includes("track")) return "gps";
80
+ if (contentType.startsWith("video/") || kind.includes("video") || key.includes("video") || pathValue.includes("video")) return "video";
81
+ return null;
82
+ }
83
+
84
+ function computeEvalPassFail({ overallStatus, results, failStatus = "fail" } = {}) {
85
+ const res = Array.isArray(results) ? results : [];
86
+ const failing = res.filter((r) => r && typeof r === "object" && !Array.isArray(r) && String(r.status ?? "") === failStatus).length;
87
+ const pass = String(overallStatus ?? "") === "ok" && failing === 0;
88
+ return { pass, failingCount: failing };
89
+ }
90
+
91
+ function computeClosePackSummaryV1({ dir } = {}) {
92
+ const evidenceIndex = readJsonIfExistsSync(path.join(dir, "evidence", "evidence_index.json"));
93
+ const evidenceItems = Array.isArray(evidenceIndex?.items) ? evidenceIndex.items : [];
94
+ const byType = { gps: 0, video: 0, checkpoint: 0 };
95
+ for (const it of evidenceItems) {
96
+ const k = deriveEvidenceTypeKeyFromIndexItem(it);
97
+ if (k && Object.hasOwn(byType, k)) byType[k] += 1;
98
+ }
99
+
100
+ const slaDef = readJsonIfExistsSync(path.join(dir, "sla", "sla_definition.json"));
101
+ const slaEval = readJsonIfExistsSync(path.join(dir, "sla", "sla_evaluation.json"));
102
+ const slaPresent = Boolean(slaDef && slaEval);
103
+ const slaComputed = slaPresent ? computeEvalPassFail({ overallStatus: slaEval?.overallStatus ?? null, results: slaEval?.results ?? null }) : null;
104
+
105
+ const accCrit = readJsonIfExistsSync(path.join(dir, "acceptance", "acceptance_criteria.json"));
106
+ const accEval = readJsonIfExistsSync(path.join(dir, "acceptance", "acceptance_evaluation.json"));
107
+ const accPresent = Boolean(accCrit && accEval);
108
+ const accComputed = accPresent ? computeEvalPassFail({ overallStatus: accEval?.overallStatus ?? null, results: accEval?.results ?? null }) : null;
109
+
110
+ return {
111
+ hasClosePack: true,
112
+ sla: {
113
+ present: slaPresent,
114
+ pass: slaPresent ? Boolean(slaComputed?.pass) : false,
115
+ failingClausesCount: slaPresent ? Number(slaComputed?.failingCount ?? 0) : 0
116
+ },
117
+ acceptance: {
118
+ present: accPresent,
119
+ pass: accPresent ? Boolean(accComputed?.pass) : false,
120
+ failingCriteriaCount: accPresent ? Number(accComputed?.failingCount ?? 0) : 0
121
+ },
122
+ evidenceIndex: {
123
+ present: Boolean(evidenceIndex),
124
+ itemCount: evidenceItems.length,
125
+ byType
126
+ }
127
+ };
128
+ }
129
+
130
+ async function main() {
131
+ const args = parseArgs(process.argv.slice(2));
132
+ const bundleType = readBundleType(args.dir);
133
+ const result =
134
+ bundleType === "ClosePack.v1"
135
+ ? await verifyClosePackBundleDir({ dir: args.dir, strict: args.strict, hashConcurrency: args.hashConcurrency })
136
+ : bundleType === "InvoiceBundle.v1"
137
+ ? await verifyInvoiceBundleDir({ dir: args.dir, strict: args.strict, hashConcurrency: args.hashConcurrency })
138
+ : { ok: false, error: "unsupported bundle type", type: bundleType, warnings: [] };
139
+
140
+ if (bundleType === "ClosePack.v1" && result && typeof result === "object" && !Array.isArray(result)) {
141
+ // Only attach the hosted summary when verification succeeded, so the UI doesn't summarize untrusted/broken surfaces.
142
+ if (result.ok === true) {
143
+ try {
144
+ result.closepackSummaryV1 = computeClosePackSummaryV1({ dir: args.dir });
145
+ } catch {
146
+ // ignore
147
+ }
148
+ }
149
+ }
150
+ // Use sync FD writes so callers reading from pipes don't race on "exit" vs "stdout data" delivery.
151
+ fsSync.writeFileSync(1, JSON.stringify(result));
152
+ }
153
+
154
+ main().catch((err) => {
155
+ process.stderr.write(String(err?.stack ?? err?.message ?? err ?? "unknown error") + "\n");
156
+ process.exitCode = 1;
157
+ });