settld 0.2.3 → 0.2.5
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/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 +3 -1
- package/scripts/ci/run-public-onboarding-gate.mjs +136 -0
- package/scripts/setup/login.mjs +111 -17
- package/scripts/setup/onboard.mjs +176 -40
- package/scripts/setup/onboarding-failure-taxonomy.mjs +96 -0
- package/scripts/setup/onboarding-state-machine.mjs +102 -0
- package/services/magic-link/README.md +343 -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 +220 -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 +156 -0
- package/services/magic-link/src/decisions.js +92 -0
- package/services/magic-link/src/ingest-keys.js +137 -0
- package/services/magic-link/src/maintenance.js +70 -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 +15788 -0
- package/services/magic-link/src/settlement-decisions.js +84 -0
- package/services/magic-link/src/smtp.js +202 -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 +129 -0
|
@@ -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 {
|
|
@@ -21977,6 +21983,126 @@ export function createApi({
|
|
|
21977
21983
|
return "info";
|
|
21978
21984
|
}
|
|
21979
21985
|
|
|
21986
|
+
const ONBOARDING_PROXY_ROUTES = Object.freeze([
|
|
21987
|
+
{ method: "GET", re: /^\/v1\/public\/auth-mode$/ },
|
|
21988
|
+
{ method: "POST", re: /^\/v1\/public\/signup$/ },
|
|
21989
|
+
{ method: "GET", re: /^\/v1\/buyer\/me$/ },
|
|
21990
|
+
{ method: "POST", re: /^\/v1\/buyer\/logout$/ },
|
|
21991
|
+
{ method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/buyer\/login\/otp$/ },
|
|
21992
|
+
{ method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/buyer\/login$/ },
|
|
21993
|
+
{ method: "GET", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding$/ },
|
|
21994
|
+
{ method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/events$/ },
|
|
21995
|
+
{ method: "GET", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding-metrics$/ },
|
|
21996
|
+
{ method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/wallet-bootstrap$/ },
|
|
21997
|
+
{ method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/wallet-funding$/ },
|
|
21998
|
+
{ method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/runtime-bootstrap$/ },
|
|
21999
|
+
{ method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/runtime-bootstrap\/smoke-test$/ },
|
|
22000
|
+
{ method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/first-paid-call$/ },
|
|
22001
|
+
{ method: "GET", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/first-paid-call\/history$/ },
|
|
22002
|
+
{ method: "POST", re: /^\/v1\/tenants\/[a-zA-Z0-9_-]{1,64}\/onboarding\/conformance-matrix$/ }
|
|
22003
|
+
]);
|
|
22004
|
+
const ONBOARDING_PROXY_BODY_METHODS = new Set(["POST", "PUT", "PATCH"]);
|
|
22005
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
22006
|
+
"connection",
|
|
22007
|
+
"keep-alive",
|
|
22008
|
+
"proxy-authenticate",
|
|
22009
|
+
"proxy-authorization",
|
|
22010
|
+
"te",
|
|
22011
|
+
"trailer",
|
|
22012
|
+
"transfer-encoding",
|
|
22013
|
+
"upgrade",
|
|
22014
|
+
"host",
|
|
22015
|
+
"content-length"
|
|
22016
|
+
]);
|
|
22017
|
+
|
|
22018
|
+
function isOnboardingProxyRoute({ method, path }) {
|
|
22019
|
+
const m = String(method ?? "").toUpperCase();
|
|
22020
|
+
const p = String(path ?? "");
|
|
22021
|
+
for (const route of ONBOARDING_PROXY_ROUTES) {
|
|
22022
|
+
if (route.method !== m) continue;
|
|
22023
|
+
if (route.re.test(p)) return true;
|
|
22024
|
+
}
|
|
22025
|
+
return false;
|
|
22026
|
+
}
|
|
22027
|
+
|
|
22028
|
+
async function proxyOnboardingRequest(req, res, { path, search, requestId }) {
|
|
22029
|
+
if (!onboardingProxyBaseUrl) {
|
|
22030
|
+
return sendError(
|
|
22031
|
+
res,
|
|
22032
|
+
503,
|
|
22033
|
+
"onboarding proxy is not configured on this API host",
|
|
22034
|
+
{ env: "PROXY_ONBOARDING_BASE_URL" },
|
|
22035
|
+
{ code: "ONBOARDING_PROXY_NOT_CONFIGURED" }
|
|
22036
|
+
);
|
|
22037
|
+
}
|
|
22038
|
+
const upstreamFetch = typeof fetchFn === "function" ? fetchFn : globalThis.fetch;
|
|
22039
|
+
if (typeof upstreamFetch !== "function") {
|
|
22040
|
+
return sendError(res, 500, "onboarding proxy fetch is unavailable", null, { code: "ONBOARDING_PROXY_FETCH_UNAVAILABLE" });
|
|
22041
|
+
}
|
|
22042
|
+
|
|
22043
|
+
const targetUrl = new URL(`${path}${search || ""}`, `${onboardingProxyBaseUrl}/`).toString();
|
|
22044
|
+
const upstreamHeaders = new Headers();
|
|
22045
|
+
for (const [nameRaw, valueRaw] of Object.entries(req.headers ?? {})) {
|
|
22046
|
+
const name = String(nameRaw ?? "").toLowerCase();
|
|
22047
|
+
if (!name || HOP_BY_HOP_HEADERS.has(name)) continue;
|
|
22048
|
+
if (Array.isArray(valueRaw)) {
|
|
22049
|
+
if (!valueRaw.length) continue;
|
|
22050
|
+
upstreamHeaders.set(name, valueRaw.join(", "));
|
|
22051
|
+
continue;
|
|
22052
|
+
}
|
|
22053
|
+
if (valueRaw === undefined || valueRaw === null) continue;
|
|
22054
|
+
upstreamHeaders.set(name, String(valueRaw));
|
|
22055
|
+
}
|
|
22056
|
+
if (!upstreamHeaders.has("x-request-id") && requestId) {
|
|
22057
|
+
upstreamHeaders.set("x-request-id", String(requestId));
|
|
22058
|
+
}
|
|
22059
|
+
|
|
22060
|
+
let body = undefined;
|
|
22061
|
+
const method = String(req.method ?? "").toUpperCase();
|
|
22062
|
+
if (ONBOARDING_PROXY_BODY_METHODS.has(method)) {
|
|
22063
|
+
const raw = await readRawBody(req);
|
|
22064
|
+
if (raw && raw.length > 0) body = raw;
|
|
22065
|
+
}
|
|
22066
|
+
|
|
22067
|
+
let upstreamRes;
|
|
22068
|
+
try {
|
|
22069
|
+
upstreamRes = await upstreamFetch(targetUrl, {
|
|
22070
|
+
method,
|
|
22071
|
+
headers: upstreamHeaders,
|
|
22072
|
+
body
|
|
22073
|
+
});
|
|
22074
|
+
} catch (err) {
|
|
22075
|
+
return sendError(
|
|
22076
|
+
res,
|
|
22077
|
+
502,
|
|
22078
|
+
"onboarding proxy upstream unreachable",
|
|
22079
|
+
{ message: err?.message ?? String(err) },
|
|
22080
|
+
{ code: "ONBOARDING_PROXY_UNREACHABLE" }
|
|
22081
|
+
);
|
|
22082
|
+
}
|
|
22083
|
+
|
|
22084
|
+
const bytes = Buffer.from(await upstreamRes.arrayBuffer());
|
|
22085
|
+
res.statusCode = upstreamRes.status;
|
|
22086
|
+
for (const [name, value] of upstreamRes.headers.entries()) {
|
|
22087
|
+
const n = String(name ?? "").toLowerCase();
|
|
22088
|
+
if (!n || HOP_BY_HOP_HEADERS.has(n)) continue;
|
|
22089
|
+
try {
|
|
22090
|
+
res.setHeader(n, value);
|
|
22091
|
+
} catch {
|
|
22092
|
+
// ignore invalid header copy
|
|
22093
|
+
}
|
|
22094
|
+
}
|
|
22095
|
+
try {
|
|
22096
|
+
const setCookies = typeof upstreamRes.headers.getSetCookie === "function" ? upstreamRes.headers.getSetCookie() : [];
|
|
22097
|
+
if (Array.isArray(setCookies) && setCookies.length > 0) {
|
|
22098
|
+
res.setHeader("set-cookie", setCookies);
|
|
22099
|
+
}
|
|
22100
|
+
} catch {
|
|
22101
|
+
// ignore
|
|
22102
|
+
}
|
|
22103
|
+
return res.end(bytes);
|
|
22104
|
+
}
|
|
22105
|
+
|
|
21980
22106
|
async function handle(req, res) {
|
|
21981
22107
|
const url = new URL(req.url ?? "/", "http://localhost");
|
|
21982
22108
|
const path = url.pathname;
|
|
@@ -21990,6 +22116,9 @@ export function createApi({
|
|
|
21990
22116
|
setProtocolResponseHeaders(res);
|
|
21991
22117
|
|
|
21992
22118
|
return withLogContext({ requestId, route, method: req.method, path }, async () => {
|
|
22119
|
+
if (isOnboardingProxyRoute({ method: req.method, path })) {
|
|
22120
|
+
return proxyOnboardingRequest(req, res, { path, search: url.search, requestId });
|
|
22121
|
+
}
|
|
21993
22122
|
const startedMs = Date.now();
|
|
21994
22123
|
let tenantId = "tenant_default";
|
|
21995
22124
|
let principalId = "anon";
|