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,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
|
+
});
|