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,218 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import http from "node:http";
5
+ import https from "node:https";
6
+
7
+ import { decryptWebhookSecret } from "./tenant-settings.js";
8
+
9
+ function isPlainObject(v) {
10
+ return Boolean(v && typeof v === "object" && !Array.isArray(v) && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null));
11
+ }
12
+
13
+ function hmacSha256Hex(secret, message) {
14
+ return crypto.createHmac("sha256", String(secret ?? "")).update(String(message ?? ""), "utf8").digest("hex");
15
+ }
16
+
17
+ function isHttpSuccessStatus(statusCode) {
18
+ const code = Number(statusCode);
19
+ return Number.isFinite(code) && code >= 200 && code < 300;
20
+ }
21
+
22
+ async function waitMs(ms) {
23
+ const n = Number(ms);
24
+ if (!Number.isFinite(n) || n <= 0) return;
25
+ await new Promise((resolve) => setTimeout(resolve, n));
26
+ }
27
+
28
+ function buildSignatureHeader({ secret, timestamp, body }) {
29
+ // Simple, stable scheme: v1 = HMAC_SHA256(secret, `${timestamp}.${body}`)
30
+ const ts = String(timestamp ?? "");
31
+ const msg = `${ts}.${String(body ?? "")}`;
32
+ const sig = hmacSha256Hex(secret, msg);
33
+ return { timestamp: ts, signature: `v1=${sig}` };
34
+ }
35
+
36
+ async function request({ url, method, headers, body, timeoutMs }) {
37
+ const u = new URL(url);
38
+ const lib = u.protocol === "https:" ? https : http;
39
+ return await new Promise((resolve) => {
40
+ const req = lib.request(
41
+ {
42
+ protocol: u.protocol,
43
+ hostname: u.hostname,
44
+ port: u.port ? Number(u.port) : u.protocol === "https:" ? 443 : 80,
45
+ path: u.pathname + u.search,
46
+ method,
47
+ headers,
48
+ timeout: timeoutMs
49
+ },
50
+ (res) => {
51
+ const chunks = [];
52
+ res.on("data", (d) => chunks.push(d));
53
+ res.on("end", () => {
54
+ resolve({ ok: true, statusCode: res.statusCode ?? 0, body: Buffer.concat(chunks).toString("utf8") });
55
+ });
56
+ }
57
+ );
58
+ req.on("timeout", () => {
59
+ try { req.destroy(new Error("timeout")); } catch { /* ignore */ }
60
+ });
61
+ req.on("error", (err) => resolve({ ok: false, error: err?.message ?? String(err ?? "error") }));
62
+ req.end(body);
63
+ });
64
+ }
65
+
66
+ export function buildWebhookPayload({ event, tenantId, token, zipSha256, zipBytes, modeResolved, modeRequested, cliOut, publicBaseUrl, decisionReport = null, publicSummary = null, closePackZipUrl = null }) {
67
+ const base = publicBaseUrl ? String(publicBaseUrl).replace(/\/+$/, "") : "";
68
+ const rel = `/r/${token}`;
69
+ const url = base ? `${base}${rel}` : rel;
70
+
71
+ const errorCodes = Array.isArray(cliOut?.errors) ? cliOut.errors.map((e) => String(e?.code ?? "")).filter(Boolean) : [];
72
+ const warningCodes = Array.isArray(cliOut?.warnings) ? cliOut.warnings.map((w) => String(w?.code ?? "")).filter(Boolean) : [];
73
+
74
+ const payload = {
75
+ schemaVersion: "MagicLinkWebhookPayload.v1",
76
+ event: String(event ?? ""),
77
+ sentAt: new Date().toISOString(),
78
+ tenantId,
79
+ token,
80
+ magicLinkUrl: url,
81
+ zipSha256,
82
+ zipBytes,
83
+ modeRequested,
84
+ modeResolved,
85
+ verification: {
86
+ ok: Boolean(cliOut?.ok),
87
+ verificationOk: Boolean(cliOut?.verificationOk),
88
+ errorCodes,
89
+ warningCodes
90
+ },
91
+ artifacts: {
92
+ verifyJsonUrl: base ? `${base}${rel}/verify.json` : `${rel}/verify.json`,
93
+ bundleZipUrl: base ? `${base}${rel}/bundle.zip` : `${rel}/bundle.zip`,
94
+ receiptJsonUrl: base ? `${base}${rel}/receipt.json` : `${rel}/receipt.json`,
95
+ auditPacketUrl: base ? `${base}${rel}/audit-packet.zip` : `${rel}/audit-packet.zip`
96
+ }
97
+ };
98
+ if (closePackZipUrl) payload.artifacts.closePackZipUrl = base && String(closePackZipUrl).startsWith("/") ? `${base}${closePackZipUrl}` : closePackZipUrl;
99
+ if (decisionReport && typeof decisionReport === "object" && !Array.isArray(decisionReport)) {
100
+ payload.decision = {
101
+ decision: typeof decisionReport.decision === "string" ? decisionReport.decision : null,
102
+ decidedAt: typeof decisionReport.decidedAt === "string" ? decisionReport.decidedAt : null,
103
+ signerKeyId: typeof decisionReport.signerKeyId === "string" ? decisionReport.signerKeyId : null,
104
+ actorEmail: typeof decisionReport?.actor?.email === "string" ? decisionReport.actor.email : null
105
+ };
106
+ payload.artifacts.decisionReportUrl = base ? `${base}${rel}/settlement_decision_report.json` : `${rel}/settlement_decision_report.json`;
107
+ }
108
+ if (publicSummary && typeof publicSummary === "object" && !Array.isArray(publicSummary)) {
109
+ payload.invoice = publicSummary.invoiceClaim && typeof publicSummary.invoiceClaim === "object" && !Array.isArray(publicSummary.invoiceClaim)
110
+ ? {
111
+ invoiceId: typeof publicSummary.invoiceClaim.invoiceId === "string" ? publicSummary.invoiceClaim.invoiceId : null,
112
+ currency: typeof publicSummary.invoiceClaim.currency === "string" ? publicSummary.invoiceClaim.currency : null,
113
+ totalCents: typeof publicSummary.invoiceClaim.totalCents === "string" ? publicSummary.invoiceClaim.totalCents : null
114
+ }
115
+ : null;
116
+ if (publicSummary.closePackSummaryV1 && typeof publicSummary.closePackSummaryV1 === "object" && !Array.isArray(publicSummary.closePackSummaryV1)) {
117
+ payload.closePack = publicSummary.closePackSummaryV1;
118
+ }
119
+ }
120
+ return payload;
121
+ }
122
+
123
+ export async function deliverTenantWebhooks({
124
+ dataDir,
125
+ tenantId,
126
+ token,
127
+ event,
128
+ payload,
129
+ webhooks,
130
+ settingsKey,
131
+ deliveryMode = "http",
132
+ timeoutMs = 5_000,
133
+ maxAttempts = 1,
134
+ retryBackoffMs = 0
135
+ }) {
136
+ const list = Array.isArray(webhooks) ? webhooks : [];
137
+ const body = JSON.stringify(payload ?? {});
138
+ const maxAttemptsSafe = Number.isInteger(maxAttempts) && maxAttempts > 0 ? maxAttempts : 1;
139
+ const retryBackoffSafe = Number.isInteger(retryBackoffMs) && retryBackoffMs >= 0 ? retryBackoffMs : 0;
140
+
141
+ const results = [];
142
+ for (let i = 0; i < list.length; i += 1) {
143
+ const w = list[i];
144
+ if (!isPlainObject(w)) continue;
145
+ if (!w.enabled) continue;
146
+ const events = Array.isArray(w.events) ? w.events.map(String) : [];
147
+ if (!events.includes(event)) continue;
148
+ const url = typeof w.url === "string" ? w.url.trim() : "";
149
+ if (!url) continue;
150
+
151
+ const secret = decryptWebhookSecret({ settingsKey, storedSecret: w.secret });
152
+ if (!secret) {
153
+ results.push({ ok: false, url, error: "WEBHOOK_SECRET_MISSING" });
154
+ continue;
155
+ }
156
+
157
+ const ts = new Date().toISOString();
158
+ const sig = buildSignatureHeader({ secret, timestamp: ts, body });
159
+ const headers = {
160
+ "content-type": "application/json; charset=utf-8",
161
+ "content-length": String(Buffer.byteLength(body, "utf8")),
162
+ "user-agent": "settld-magic-link/0",
163
+ "x-settld-event": String(event),
164
+ "x-settld-timestamp": sig.timestamp,
165
+ "x-settld-signature": sig.signature
166
+ };
167
+
168
+ const attempt = {
169
+ schemaVersion: "MagicLinkWebhookAttempt.v1",
170
+ tenantId,
171
+ token,
172
+ event,
173
+ url,
174
+ headers,
175
+ bodySha256: crypto.createHash("sha256").update(body, "utf8").digest("hex"),
176
+ sentAt: ts,
177
+ deliveryMode
178
+ };
179
+
180
+ const outDir = path.join(dataDir, "webhooks", deliveryMode === "record" ? "record" : "attempts");
181
+ await fs.mkdir(outDir, { recursive: true });
182
+ if (deliveryMode === "record") {
183
+ const id = `${token}_${Date.now()}_${i}`;
184
+ const fp = path.join(outDir, `${id}.json`);
185
+ await fs.writeFile(fp, JSON.stringify({ ...attempt, body, attempt: 1, maxAttempts: 1 }, null, 2) + "\n", "utf8");
186
+ results.push({ ok: true, url, recorded: true, attempts: 1 });
187
+ continue;
188
+ }
189
+
190
+ let finalResult = { ok: false, error: "request failed", statusCode: null };
191
+ let attemptsUsed = 0;
192
+ for (let attemptIndex = 1; attemptIndex <= maxAttemptsSafe; attemptIndex += 1) {
193
+ const id = `${token}_${Date.now()}_${i}_${attemptIndex}`;
194
+ const fp = path.join(outDir, `${id}.json`);
195
+
196
+ const res = await request({ url, method: "POST", headers, body, timeoutMs });
197
+ const delivered = Boolean(res.ok) && isHttpSuccessStatus(res.statusCode);
198
+ finalResult = delivered
199
+ ? { ok: true, statusCode: res.statusCode ?? 200, error: null }
200
+ : {
201
+ ok: false,
202
+ statusCode: Number.isFinite(Number(res.statusCode)) ? Number(res.statusCode) : null,
203
+ error: res.ok ? `HTTP_${res.statusCode ?? "UNKNOWN"}` : res.error ?? "request failed"
204
+ };
205
+ attemptsUsed = attemptIndex;
206
+ await fs.writeFile(fp, JSON.stringify({ ...attempt, attempt: attemptIndex, maxAttempts: maxAttemptsSafe, result: finalResult }, null, 2) + "\n", "utf8");
207
+
208
+ if (finalResult.ok) break;
209
+ if (attemptIndex < maxAttemptsSafe) {
210
+ const waitFor = retryBackoffSafe * (2 ** (attemptIndex - 1));
211
+ // Exponential backoff for transient webhook failures.
212
+ await waitMs(waitFor);
213
+ }
214
+ }
215
+ results.push({ url, ...finalResult, attempts: attemptsUsed });
216
+ }
217
+ return results;
218
+ }
package/src/api/app.js CHANGED
@@ -498,6 +498,12 @@ export function createApi({
498
498
  }
499
499
  const rateBucketsByApiKey = new Map(); // `${tenantId}\n${apiKeyId}` -> { tokens, lastMs }
500
500
  const ratePerKeyRefillPerMs = rateLimitPerKeyRpmValue ? rateLimitPerKeyRpmValue / 60_000 : 0;
501
+ const onboardingProxyBaseUrlRaw =
502
+ typeof process !== "undefined" ? process.env.PROXY_ONBOARDING_BASE_URL ?? process.env.PROXY_MAGIC_LINK_BASE_URL ?? null : null;
503
+ const onboardingProxyBaseUrl =
504
+ onboardingProxyBaseUrlRaw && String(onboardingProxyBaseUrlRaw).trim() !== ""
505
+ ? normalizeOptionalAbsoluteUrl(String(onboardingProxyBaseUrlRaw).trim(), { fieldName: "PROXY_ONBOARDING_BASE_URL" })?.replace(/\/+$/, "")
506
+ : null;
501
507
 
502
508
  function setProtocolResponseHeaders(res) {
503
509
  try {
@@ -7582,7 +7588,12 @@ export function createApi({
7582
7588
  const out = [];
7583
7589
  const seen = new Set();
7584
7590
  for (const entry of parsed) {
7585
- const id = normalizeOptionalX402RefInput(entry, fieldPath, { allowNull: true, max });
7591
+ if (entry === null || entry === undefined || String(entry).trim() === "") continue;
7592
+ if (typeof entry !== "string") throw new TypeError(`${fieldPath} must contain only strings`);
7593
+ const id = String(entry).trim();
7594
+ if (!id) continue;
7595
+ if (id.length > max) throw new TypeError(`${fieldPath} must be <= ${max} chars`);
7596
+ if (!/^[A-Za-z0-9:_.-]+$/.test(id)) throw new TypeError(`${fieldPath} must match ^[A-Za-z0-9:_.-]+$`);
7586
7597
  if (!id) continue;
7587
7598
  if (seen.has(id)) continue;
7588
7599
  seen.add(id);
@@ -21977,6 +21988,126 @@ export function createApi({
21977
21988
  return "info";
21978
21989
  }
21979
21990
 
21991
+ const ONBOARDING_PROXY_ROUTES = Object.freeze([
21992
+ { method: "GET", re: /^\/v1\/public\/auth-mode$/ },
21993
+ { method: "POST", re: /^\/v1\/public\/signup$/ },
21994
+ { method: "GET", re: /^\/v1\/buyer\/me$/ },
21995
+ { method: "POST", re: /^\/v1\/buyer\/logout$/ },
21996
+ { method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/buyer\/login\/otp$/ },
21997
+ { method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/buyer\/login$/ },
21998
+ { method: "GET", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding$/ },
21999
+ { method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/events$/ },
22000
+ { method: "GET", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding-metrics$/ },
22001
+ { method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/wallet-bootstrap$/ },
22002
+ { method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/wallet-funding$/ },
22003
+ { method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/runtime-bootstrap$/ },
22004
+ { method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/runtime-bootstrap\/smoke-test$/ },
22005
+ { method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/first-paid-call$/ },
22006
+ { method: "GET", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/first-paid-call\/history$/ },
22007
+ { method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/conformance-matrix$/ }
22008
+ ]);
22009
+ const ONBOARDING_PROXY_BODY_METHODS = new Set(["POST", "PUT", "PATCH"]);
22010
+ const HOP_BY_HOP_HEADERS = new Set([
22011
+ "connection",
22012
+ "keep-alive",
22013
+ "proxy-authenticate",
22014
+ "proxy-authorization",
22015
+ "te",
22016
+ "trailer",
22017
+ "transfer-encoding",
22018
+ "upgrade",
22019
+ "host",
22020
+ "content-length"
22021
+ ]);
22022
+
22023
+ function isOnboardingProxyRoute({ method, path }) {
22024
+ const m = String(method ?? "").toUpperCase();
22025
+ const p = String(path ?? "");
22026
+ for (const route of ONBOARDING_PROXY_ROUTES) {
22027
+ if (route.method !== m) continue;
22028
+ if (route.re.test(p)) return true;
22029
+ }
22030
+ return false;
22031
+ }
22032
+
22033
+ async function proxyOnboardingRequest(req, res, { path, search, requestId }) {
22034
+ if (!onboardingProxyBaseUrl) {
22035
+ return sendError(
22036
+ res,
22037
+ 503,
22038
+ "onboarding proxy is not configured on this API host",
22039
+ { env: "PROXY_ONBOARDING_BASE_URL" },
22040
+ { code: "ONBOARDING_PROXY_NOT_CONFIGURED" }
22041
+ );
22042
+ }
22043
+ const upstreamFetch = typeof fetchFn === "function" ? fetchFn : globalThis.fetch;
22044
+ if (typeof upstreamFetch !== "function") {
22045
+ return sendError(res, 500, "onboarding proxy fetch is unavailable", null, { code: "ONBOARDING_PROXY_FETCH_UNAVAILABLE" });
22046
+ }
22047
+
22048
+ const targetUrl = new URL(`${path}${search || ""}`, `${onboardingProxyBaseUrl}/`).toString();
22049
+ const upstreamHeaders = new Headers();
22050
+ for (const [nameRaw, valueRaw] of Object.entries(req.headers ?? {})) {
22051
+ const name = String(nameRaw ?? "").toLowerCase();
22052
+ if (!name || HOP_BY_HOP_HEADERS.has(name)) continue;
22053
+ if (Array.isArray(valueRaw)) {
22054
+ if (!valueRaw.length) continue;
22055
+ upstreamHeaders.set(name, valueRaw.join(", "));
22056
+ continue;
22057
+ }
22058
+ if (valueRaw === undefined || valueRaw === null) continue;
22059
+ upstreamHeaders.set(name, String(valueRaw));
22060
+ }
22061
+ if (!upstreamHeaders.has("x-request-id") && requestId) {
22062
+ upstreamHeaders.set("x-request-id", String(requestId));
22063
+ }
22064
+
22065
+ let body = undefined;
22066
+ const method = String(req.method ?? "").toUpperCase();
22067
+ if (ONBOARDING_PROXY_BODY_METHODS.has(method)) {
22068
+ const raw = await readRawBody(req);
22069
+ if (raw && raw.length > 0) body = raw;
22070
+ }
22071
+
22072
+ let upstreamRes;
22073
+ try {
22074
+ upstreamRes = await upstreamFetch(targetUrl, {
22075
+ method,
22076
+ headers: upstreamHeaders,
22077
+ body
22078
+ });
22079
+ } catch (err) {
22080
+ return sendError(
22081
+ res,
22082
+ 502,
22083
+ "onboarding proxy upstream unreachable",
22084
+ { message: err?.message ?? String(err) },
22085
+ { code: "ONBOARDING_PROXY_UNREACHABLE" }
22086
+ );
22087
+ }
22088
+
22089
+ const bytes = Buffer.from(await upstreamRes.arrayBuffer());
22090
+ res.statusCode = upstreamRes.status;
22091
+ for (const [name, value] of upstreamRes.headers.entries()) {
22092
+ const n = String(name ?? "").toLowerCase();
22093
+ if (!n || HOP_BY_HOP_HEADERS.has(n)) continue;
22094
+ try {
22095
+ res.setHeader(n, value);
22096
+ } catch {
22097
+ // ignore invalid header copy
22098
+ }
22099
+ }
22100
+ try {
22101
+ const setCookies = typeof upstreamRes.headers.getSetCookie === "function" ? upstreamRes.headers.getSetCookie() : [];
22102
+ if (Array.isArray(setCookies) && setCookies.length > 0) {
22103
+ res.setHeader("set-cookie", setCookies);
22104
+ }
22105
+ } catch {
22106
+ // ignore
22107
+ }
22108
+ return res.end(bytes);
22109
+ }
22110
+
21980
22111
  async function handle(req, res) {
21981
22112
  const url = new URL(req.url ?? "/", "http://localhost");
21982
22113
  const path = url.pathname;
@@ -21990,6 +22121,9 @@ export function createApi({
21990
22121
  setProtocolResponseHeaders(res);
21991
22122
 
21992
22123
  return withLogContext({ requestId, route, method: req.method, path }, async () => {
22124
+ if (isOnboardingProxyRoute({ method: req.method, path })) {
22125
+ return proxyOnboardingRequest(req, res, { path, search: url.search, requestId });
22126
+ }
21993
22127
  const startedMs = Date.now();
21994
22128
  let tenantId = "tenant_default";
21995
22129
  let principalId = "anon";