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,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
|
-
|
|
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";
|