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,10 @@
1
+ export { SettldClient } from "./client.js";
2
+ export { fetchWithSettldAutopay } from "./x402-autopay.js";
3
+ export {
4
+ verifySettldWebhookSignature,
5
+ SettldWebhookSignatureError,
6
+ SettldWebhookSignatureHeaderError,
7
+ SettldWebhookTimestampToleranceError,
8
+ SettldWebhookNoMatchingSignatureError
9
+ } from "./webhook-signature.js";
10
+ export { verifySettldWebhook } from "./express-middleware.js";
@@ -0,0 +1,182 @@
1
+ import crypto from "node:crypto";
2
+
3
+ function isNonEmptyString(value) {
4
+ return typeof value === "string" && value.trim() !== "";
5
+ }
6
+
7
+ function toBodyBuffer(rawBody) {
8
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(rawBody)) return rawBody;
9
+ if (typeof rawBody === "string") return Buffer.from(rawBody, "utf8");
10
+ if (rawBody instanceof ArrayBuffer) return Buffer.from(rawBody);
11
+ if (rawBody instanceof Uint8Array) return Buffer.from(rawBody.buffer, rawBody.byteOffset, rawBody.byteLength);
12
+ throw new TypeError("rawBody must be a string, Buffer, Uint8Array, or ArrayBuffer");
13
+ }
14
+
15
+ function parseTimestampToMs(timestamp) {
16
+ const raw = String(timestamp ?? "").trim();
17
+ if (!raw) return Number.NaN;
18
+ if (/^\d+$/.test(raw)) {
19
+ const asSeconds = Number(raw);
20
+ if (!Number.isSafeInteger(asSeconds) || asSeconds <= 0) return Number.NaN;
21
+ return asSeconds * 1000;
22
+ }
23
+ const asMs = Date.parse(raw);
24
+ if (!Number.isFinite(asMs)) return Number.NaN;
25
+ return asMs;
26
+ }
27
+
28
+ function normalizeHex(value) {
29
+ const candidate = String(value ?? "").trim().toLowerCase();
30
+ if (!/^[0-9a-f]{64}$/.test(candidate)) return null;
31
+ return candidate;
32
+ }
33
+
34
+ function timingSafeEqualHex(leftHex, rightHex) {
35
+ const left = normalizeHex(leftHex);
36
+ const right = normalizeHex(rightHex);
37
+ if (!left || !right) return false;
38
+ const leftBuf = Buffer.from(left, "hex");
39
+ const rightBuf = Buffer.from(right, "hex");
40
+ if (leftBuf.length !== rightBuf.length) return false;
41
+ return crypto.timingSafeEqual(leftBuf, rightBuf);
42
+ }
43
+
44
+ function parseSignatureHeader(signatureHeader) {
45
+ if (!isNonEmptyString(signatureHeader)) {
46
+ throw new SettldWebhookSignatureHeaderError("x-settld-signature header is required");
47
+ }
48
+ const parts = signatureHeader
49
+ .split(",")
50
+ .map((value) => value.trim())
51
+ .filter(Boolean);
52
+ if (parts.length === 0) {
53
+ throw new SettldWebhookSignatureHeaderError("x-settld-signature header is empty");
54
+ }
55
+
56
+ let timestamp = null;
57
+ const signatures = [];
58
+ for (const part of parts) {
59
+ const separatorIndex = part.indexOf("=");
60
+ if (separatorIndex <= 0) {
61
+ signatures.push(part);
62
+ continue;
63
+ }
64
+ const key = part.slice(0, separatorIndex).trim().toLowerCase();
65
+ const value = part.slice(separatorIndex + 1).trim();
66
+ if (!value) continue;
67
+ if (key === "t") {
68
+ timestamp = value;
69
+ continue;
70
+ }
71
+ if (key === "v1") {
72
+ signatures.push(value);
73
+ }
74
+ }
75
+
76
+ if (signatures.length === 0) {
77
+ throw new SettldWebhookSignatureHeaderError("x-settld-signature header did not include any signatures");
78
+ }
79
+ return { timestamp, signatures };
80
+ }
81
+
82
+ function parseVerifyOptions(optionsOrTolerance) {
83
+ if (optionsOrTolerance === null || optionsOrTolerance === undefined) {
84
+ return { toleranceSeconds: 300, timestamp: null, nowMs: Date.now() };
85
+ }
86
+ if (typeof optionsOrTolerance === "number") {
87
+ return { toleranceSeconds: optionsOrTolerance, timestamp: null, nowMs: Date.now() };
88
+ }
89
+ if (!optionsOrTolerance || typeof optionsOrTolerance !== "object" || Array.isArray(optionsOrTolerance)) {
90
+ throw new TypeError("options must be a number or plain object");
91
+ }
92
+ return {
93
+ toleranceSeconds:
94
+ optionsOrTolerance.toleranceSeconds === undefined || optionsOrTolerance.toleranceSeconds === null
95
+ ? 300
96
+ : Number(optionsOrTolerance.toleranceSeconds),
97
+ timestamp: optionsOrTolerance.timestamp ?? null,
98
+ nowMs:
99
+ optionsOrTolerance.nowMs === undefined || optionsOrTolerance.nowMs === null
100
+ ? Date.now()
101
+ : Number(optionsOrTolerance.nowMs)
102
+ };
103
+ }
104
+
105
+ export class SettldWebhookSignatureError extends Error {
106
+ constructor(message, { code = "SETTLD_WEBHOOK_SIGNATURE_ERROR" } = {}) {
107
+ super(message);
108
+ this.name = "SettldWebhookSignatureError";
109
+ this.code = code;
110
+ }
111
+ }
112
+
113
+ export class SettldWebhookSignatureHeaderError extends SettldWebhookSignatureError {
114
+ constructor(message) {
115
+ super(message, { code: "SETTLD_WEBHOOK_SIGNATURE_HEADER_INVALID" });
116
+ this.name = "SettldWebhookSignatureHeaderError";
117
+ }
118
+ }
119
+
120
+ export class SettldWebhookTimestampToleranceError extends SettldWebhookSignatureError {
121
+ constructor(message, { timestamp = null, toleranceSeconds = null, nowMs = null } = {}) {
122
+ super(message, { code: "SETTLD_WEBHOOK_TIMESTAMP_OUTSIDE_TOLERANCE" });
123
+ this.name = "SettldWebhookTimestampToleranceError";
124
+ this.timestamp = timestamp;
125
+ this.toleranceSeconds = toleranceSeconds;
126
+ this.nowMs = nowMs;
127
+ }
128
+ }
129
+
130
+ export class SettldWebhookNoMatchingSignatureError extends SettldWebhookSignatureError {
131
+ constructor(message) {
132
+ super(message, { code: "SETTLD_WEBHOOK_SIGNATURE_NO_MATCH" });
133
+ this.name = "SettldWebhookNoMatchingSignatureError";
134
+ }
135
+ }
136
+
137
+ export function verifySettldWebhookSignature(rawBody, signatureHeader, secret, optionsOrTolerance = 300) {
138
+ if (!isNonEmptyString(secret)) throw new TypeError("secret is required");
139
+ const bodyBuffer = toBodyBuffer(rawBody);
140
+ const parsed = parseSignatureHeader(signatureHeader);
141
+ const options = parseVerifyOptions(optionsOrTolerance);
142
+
143
+ if (!Number.isFinite(options.toleranceSeconds) || options.toleranceSeconds <= 0) {
144
+ throw new TypeError("toleranceSeconds must be a positive number");
145
+ }
146
+ if (!Number.isFinite(options.nowMs)) {
147
+ throw new TypeError("nowMs must be a finite number");
148
+ }
149
+
150
+ const timestamp = isNonEmptyString(parsed.timestamp)
151
+ ? parsed.timestamp.trim()
152
+ : isNonEmptyString(options.timestamp)
153
+ ? String(options.timestamp).trim()
154
+ : null;
155
+ if (!timestamp) {
156
+ throw new SettldWebhookSignatureHeaderError("timestamp is required (use t=... in signature header or options.timestamp)");
157
+ }
158
+
159
+ const timestampMs = parseTimestampToMs(timestamp);
160
+ if (!Number.isFinite(timestampMs)) {
161
+ throw new SettldWebhookSignatureHeaderError("timestamp is invalid");
162
+ }
163
+ const ageSeconds = Math.abs(options.nowMs - timestampMs) / 1000;
164
+ if (ageSeconds > options.toleranceSeconds) {
165
+ throw new SettldWebhookTimestampToleranceError("signature timestamp is outside tolerance", {
166
+ timestamp,
167
+ toleranceSeconds: options.toleranceSeconds,
168
+ nowMs: options.nowMs
169
+ });
170
+ }
171
+
172
+ const hmac = crypto.createHmac("sha256", String(secret));
173
+ hmac.update(`${timestamp}.`, "utf8");
174
+ hmac.update(bodyBuffer);
175
+ const expected = hmac.digest("hex");
176
+ for (const provided of parsed.signatures) {
177
+ if (timingSafeEqualHex(provided, expected)) {
178
+ return true;
179
+ }
180
+ }
181
+ throw new SettldWebhookNoMatchingSignatureError("no matching signature in x-settld-signature header");
182
+ }
@@ -0,0 +1,210 @@
1
+ function assertFetchFunction(fetchImpl) {
2
+ if (typeof fetchImpl !== "function") throw new TypeError("fetch must be a function");
3
+ return fetchImpl;
4
+ }
5
+
6
+ function normalizeHeaderName(name, fallback) {
7
+ const raw = typeof name === "string" && name.trim() !== "" ? name.trim() : fallback;
8
+ return raw.toLowerCase();
9
+ }
10
+
11
+ function parseBooleanLike(value) {
12
+ const raw = typeof value === "string" ? value.trim().toLowerCase() : "";
13
+ if (raw === "1" || raw === "true" || raw === "yes" || raw === "on") return true;
14
+ if (raw === "0" || raw === "false" || raw === "no" || raw === "off") return false;
15
+ return null;
16
+ }
17
+
18
+ function normalizeChallengeRef(value) {
19
+ const raw = typeof value === "string" ? value.trim() : "";
20
+ return raw === "" ? null : raw;
21
+ }
22
+
23
+ function normalizeChallengeHash(value) {
24
+ const raw = typeof value === "string" ? value.trim().toLowerCase() : "";
25
+ return /^[0-9a-f]{64}$/.test(raw) ? raw : null;
26
+ }
27
+
28
+ function parseChallengeFields(text) {
29
+ const raw = typeof text === "string" ? text.trim() : "";
30
+ if (!raw) return null;
31
+ if (raw.startsWith("{") && raw.endsWith("}")) {
32
+ try {
33
+ const parsed = JSON.parse(raw);
34
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
35
+ return parsed;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+ const out = {};
41
+ for (const part of raw.split(";")) {
42
+ const chunk = part.trim();
43
+ if (!chunk) continue;
44
+ const idx = chunk.indexOf("=");
45
+ if (idx <= 0) continue;
46
+ const key = chunk.slice(0, idx).trim();
47
+ const value = chunk.slice(idx + 1).trim();
48
+ if (!key) continue;
49
+ out[key] = value;
50
+ }
51
+ return Object.keys(out).length > 0 ? out : null;
52
+ }
53
+
54
+ function parseBase64UrlJson(value) {
55
+ const raw = typeof value === "string" ? value.trim() : "";
56
+ if (!raw) return null;
57
+ if (typeof Buffer === "undefined") return null;
58
+ try {
59
+ const text = Buffer.from(raw, "base64url").toString("utf8");
60
+ const parsed = JSON.parse(text);
61
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
62
+ return parsed;
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function sortedJsonClone(value) {
69
+ if (Array.isArray(value)) return value.map((entry) => sortedJsonClone(entry));
70
+ if (!value || typeof value !== "object") return value;
71
+ const out = {};
72
+ for (const key of Object.keys(value).sort((left, right) => left.localeCompare(right))) {
73
+ out[key] = sortedJsonClone(value[key]);
74
+ }
75
+ return out;
76
+ }
77
+
78
+ function normalizeAgentPassportHeaderValue(agentPassport) {
79
+ if (agentPassport === null || agentPassport === undefined) return null;
80
+ if (!agentPassport || typeof agentPassport !== "object" || Array.isArray(agentPassport)) {
81
+ throw new TypeError("agentPassport must be an object when provided");
82
+ }
83
+ if (typeof Buffer === "undefined") {
84
+ throw new TypeError("agentPassport header encoding requires Buffer support");
85
+ }
86
+ const canonical = JSON.stringify(sortedJsonClone(agentPassport));
87
+ return Buffer.from(canonical, "utf8").toString("base64url");
88
+ }
89
+
90
+ function buildX402ChallengeMetadata(res, { gateHeaderName }) {
91
+ const gateIdRaw = res.headers.get(gateHeaderName);
92
+ const gateId = typeof gateIdRaw === "string" && gateIdRaw.trim() !== "" ? gateIdRaw.trim() : null;
93
+ const paymentRequiredRaw = res.headers.get("x-payment-required") ?? res.headers.get("payment-required");
94
+ const paymentRequired = typeof paymentRequiredRaw === "string" && paymentRequiredRaw.trim() !== "" ? paymentRequiredRaw.trim() : null;
95
+ const fields = paymentRequired ? parseChallengeFields(paymentRequired) : null;
96
+ const quote = parseBase64UrlJson(res.headers.get("x-settld-provider-quote"));
97
+ const quoteSignature = parseBase64UrlJson(res.headers.get("x-settld-provider-quote-signature"));
98
+ const quoteRequired = parseBooleanLike(fields?.quoteRequired);
99
+ return {
100
+ gateId,
101
+ paymentRequired,
102
+ fields,
103
+ policyChallenge: {
104
+ spendAuthorizationMode: normalizeChallengeRef(fields?.spendAuthorizationMode),
105
+ requestBindingMode: normalizeChallengeRef(fields?.requestBindingMode),
106
+ requestBindingSha256: normalizeChallengeHash(fields?.requestBindingSha256),
107
+ quoteRequired,
108
+ quoteId: normalizeChallengeRef(fields?.quoteId),
109
+ providerId: normalizeChallengeRef(fields?.providerId),
110
+ toolId: normalizeChallengeRef(fields?.toolId),
111
+ policyRef: normalizeChallengeRef(fields?.policyRef),
112
+ policyVersion: normalizeChallengeRef(fields?.policyVersion),
113
+ policyHash: normalizeChallengeHash(fields?.policyHash),
114
+ policyFingerprint: normalizeChallengeHash(fields?.policyFingerprint),
115
+ sponsorRef: normalizeChallengeRef(fields?.sponsorRef),
116
+ sponsorWalletRef: normalizeChallengeRef(fields?.sponsorWalletRef)
117
+ },
118
+ providerQuote: quote,
119
+ providerQuoteSignature: quoteSignature
120
+ };
121
+ }
122
+
123
+ function cloneBodyForRetry(body) {
124
+ if (body === null || body === undefined) return { ok: true, value: undefined };
125
+ if (typeof body === "string") return { ok: true, value: body };
126
+ if (body instanceof URLSearchParams) return { ok: true, value: new URLSearchParams(body) };
127
+ if (body instanceof ArrayBuffer) return { ok: true, value: body.slice(0) };
128
+ if (ArrayBuffer.isView(body)) {
129
+ const bytes = new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
130
+ return { ok: true, value: Uint8Array.from(bytes) };
131
+ }
132
+ if (typeof Buffer !== "undefined" && Buffer.isBuffer(body)) return { ok: true, value: Buffer.from(body) };
133
+ return { ok: false, reason: "body_not_replayable" };
134
+ }
135
+
136
+ function normalizeInitHeaders(initHeaders) {
137
+ const out = new Headers();
138
+ if (!initHeaders) return out;
139
+ const input = new Headers(initHeaders);
140
+ for (const [k, v] of input.entries()) out.set(k, v);
141
+ return out;
142
+ }
143
+
144
+ function buildInitialInit(init, { agentPassportHeaderName, agentPassportHeaderValue }) {
145
+ const safeInit = init && typeof init === "object" ? init : {};
146
+ const headers = normalizeInitHeaders(safeInit.headers);
147
+ if (agentPassportHeaderValue) headers.set(agentPassportHeaderName, agentPassportHeaderValue);
148
+ return {
149
+ ...safeInit,
150
+ headers
151
+ };
152
+ }
153
+
154
+ function buildRetryInit(init, { gateHeaderName, gateId, agentPassportHeaderName, agentPassportHeaderValue }) {
155
+ const safeInit = init && typeof init === "object" ? init : {};
156
+ const bodyResult = cloneBodyForRetry(safeInit.body);
157
+ if (!bodyResult.ok) {
158
+ const err = new Error("x402 autopay cannot replay this request body");
159
+ err.code = "SETTLD_AUTOPAY_BODY_NOT_REPLAYABLE";
160
+ throw err;
161
+ }
162
+
163
+ const headers = normalizeInitHeaders(safeInit.headers);
164
+ headers.set(gateHeaderName, gateId);
165
+ if (agentPassportHeaderValue) headers.set(agentPassportHeaderName, agentPassportHeaderValue);
166
+
167
+ return {
168
+ ...safeInit,
169
+ headers,
170
+ body: bodyResult.value
171
+ };
172
+ }
173
+
174
+ export async function fetchWithSettldAutopay(url, init = {}, opts = {}) {
175
+ const fetchImpl = assertFetchFunction(opts?.fetch ?? globalThis.fetch);
176
+ const gateHeaderName = normalizeHeaderName(opts?.gateHeaderName, "x-settld-gate-id");
177
+ const agentPassportHeaderName = normalizeHeaderName(opts?.agentPassportHeaderName, "x-settld-agent-passport");
178
+ const agentPassportHeaderValue = normalizeAgentPassportHeaderValue(opts?.agentPassport ?? null);
179
+ const onChallenge = typeof opts?.onChallenge === "function" ? opts.onChallenge : null;
180
+ const maxAttemptsRaw = Number(opts?.maxAttempts ?? 2);
181
+ const maxAttempts = Number.isSafeInteger(maxAttemptsRaw) && maxAttemptsRaw >= 1 ? maxAttemptsRaw : 2;
182
+
183
+ let attempt = 0;
184
+ let currentInit = buildInitialInit(init, { agentPassportHeaderName, agentPassportHeaderValue });
185
+ let lastResponse = null;
186
+ while (attempt < maxAttempts) {
187
+ attempt += 1;
188
+ const res = await fetchImpl(url, currentInit);
189
+ lastResponse = res;
190
+ if (res.status !== 402) return res;
191
+
192
+ if (onChallenge) {
193
+ try {
194
+ onChallenge(buildX402ChallengeMetadata(res, { gateHeaderName }));
195
+ } catch {
196
+ // Ignore callback failures to keep autopay deterministic.
197
+ }
198
+ }
199
+ if (attempt >= maxAttempts) return res;
200
+
201
+ const gateIdRaw = res.headers.get(gateHeaderName);
202
+ const gateId = typeof gateIdRaw === "string" ? gateIdRaw.trim() : "";
203
+ if (!gateId) return res;
204
+
205
+ const nextInit = buildRetryInit(currentInit, { gateHeaderName, gateId, agentPassportHeaderName, agentPassportHeaderValue });
206
+ currentInit = nextInit;
207
+ }
208
+
209
+ return lastResponse;
210
+ }
@@ -42,6 +42,8 @@ async function main() {
42
42
  sh("tar", ["-xzf", tarballPath, "-C", unpackDir], { env: npmEnv });
43
43
  const packageRoot = path.join(unpackDir, "package");
44
44
  const cliPath = path.join(packageRoot, "bin", "settld.js");
45
+ await fs.access(path.join(packageRoot, "scripts", "mcp", "settld-mcp-server.mjs"));
46
+ await fs.access(path.join(packageRoot, "packages", "api-sdk", "src", "x402-autopay.js"));
45
47
 
46
48
  const runTarballCli = (args) => {
47
49
  const cmd = ["npx", "--yes", "--package", tarballPath, "--", "settld", ...args].map(shellQuote).join(" ");
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ function parseArgs(argv) {
6
+ const out = {
7
+ baseUrl: process.env.SETTLD_BASE_URL ?? "https://api.settld.work",
8
+ tenantId: process.env.SETTLD_TENANT_ID ?? "tenant_default",
9
+ email: process.env.SETTLD_ONBOARDING_PROBE_EMAIL ?? "probe@settld.work",
10
+ out: "artifacts/gates/public-onboarding-gate.json"
11
+ };
12
+ for (let i = 0; i < argv.length; i += 1) {
13
+ const arg = String(argv[i] ?? "");
14
+ const next = () => {
15
+ i += 1;
16
+ if (i >= argv.length) throw new Error(`missing value for ${arg}`);
17
+ return String(argv[i] ?? "");
18
+ };
19
+ if (arg === "--base-url") out.baseUrl = next();
20
+ else if (arg.startsWith("--base-url=")) out.baseUrl = arg.slice("--base-url=".length);
21
+ else if (arg === "--tenant-id") out.tenantId = next();
22
+ else if (arg.startsWith("--tenant-id=")) out.tenantId = arg.slice("--tenant-id=".length);
23
+ else if (arg === "--email") out.email = next();
24
+ else if (arg.startsWith("--email=")) out.email = arg.slice("--email=".length);
25
+ else if (arg === "--out") out.out = next();
26
+ else if (arg.startsWith("--out=")) out.out = arg.slice("--out=".length);
27
+ }
28
+ out.baseUrl = String(out.baseUrl ?? "").trim().replace(/\/+$/, "");
29
+ out.tenantId = String(out.tenantId ?? "").trim();
30
+ out.email = String(out.email ?? "").trim().toLowerCase();
31
+ out.out = String(out.out ?? "").trim();
32
+ if (!out.baseUrl) throw new Error("--base-url is required");
33
+ if (!out.tenantId) throw new Error("--tenant-id is required");
34
+ if (!out.email) throw new Error("--email is required");
35
+ if (!out.out) throw new Error("--out is required");
36
+ return out;
37
+ }
38
+
39
+ async function requestJson(url, { method = "GET", body = null, headers = {} } = {}) {
40
+ const res = await fetch(url, {
41
+ method,
42
+ headers: {
43
+ ...(body === null ? {} : { "content-type": "application/json" }),
44
+ ...headers
45
+ },
46
+ body: body === null ? undefined : JSON.stringify(body)
47
+ });
48
+ const text = await res.text();
49
+ let json = null;
50
+ try {
51
+ json = text ? JSON.parse(text) : null;
52
+ } catch {
53
+ json = null;
54
+ }
55
+ return {
56
+ ok: res.ok,
57
+ statusCode: res.status,
58
+ url,
59
+ text,
60
+ json
61
+ };
62
+ }
63
+
64
+ function summarizeBody(outcome) {
65
+ if (outcome?.json && typeof outcome.json === "object") {
66
+ return {
67
+ code: outcome.json.code ?? null,
68
+ error: outcome.json.error ?? null,
69
+ message: outcome.json.message ?? null,
70
+ authMode: outcome.json.authMode ?? null
71
+ };
72
+ }
73
+ return { raw: String(outcome?.text ?? "").slice(0, 500) };
74
+ }
75
+
76
+ async function main() {
77
+ const args = parseArgs(process.argv.slice(2));
78
+ const startedAt = new Date().toISOString();
79
+ const steps = [];
80
+ const errors = [];
81
+
82
+ const authMode = await requestJson(`${args.baseUrl}/v1/public/auth-mode`);
83
+ steps.push({
84
+ step: "public_auth_mode",
85
+ statusCode: authMode.statusCode,
86
+ body: summarizeBody(authMode)
87
+ });
88
+ if (authMode.statusCode !== 200 || typeof authMode.json?.authMode !== "string") {
89
+ errors.push({
90
+ code: "PUBLIC_AUTH_MODE_UNAVAILABLE",
91
+ message: `expected GET /v1/public/auth-mode to return 200 with authMode; got ${authMode.statusCode}`
92
+ });
93
+ }
94
+
95
+ const otpProbe = await requestJson(
96
+ `${args.baseUrl}/v1/tenants/${encodeURIComponent(args.tenantId)}/buyer/login/otp`,
97
+ {
98
+ method: "POST",
99
+ body: { email: args.email }
100
+ }
101
+ );
102
+ steps.push({
103
+ step: "buyer_login_otp_probe",
104
+ statusCode: otpProbe.statusCode,
105
+ body: summarizeBody(otpProbe)
106
+ });
107
+ if ([403, 404, 405, 503].includes(otpProbe.statusCode)) {
108
+ errors.push({
109
+ code: "BUYER_LOGIN_OTP_UNAVAILABLE",
110
+ message: `expected buyer OTP endpoint to be reachable (non-403/404/405/503); got ${otpProbe.statusCode}`
111
+ });
112
+ }
113
+
114
+ const report = {
115
+ schemaVersion: "PublicOnboardingGate.v1",
116
+ ok: errors.length === 0,
117
+ startedAt,
118
+ completedAt: new Date().toISOString(),
119
+ baseUrl: args.baseUrl,
120
+ tenantId: args.tenantId,
121
+ steps,
122
+ errors
123
+ };
124
+
125
+ await fs.mkdir(path.dirname(args.out), { recursive: true });
126
+ await fs.writeFile(args.out, `${JSON.stringify(report, null, 2)}\n`, "utf8");
127
+ process.stdout.write(`wrote public onboarding gate report: ${path.resolve(args.out)}\n`);
128
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
129
+ if (!report.ok) process.exit(1);
130
+ }
131
+
132
+ main().catch((err) => {
133
+ process.stderr.write(`${err?.stack ?? err?.message ?? String(err)}\n`);
134
+ process.exit(1);
135
+ });
136
+
@@ -8,6 +8,10 @@ import { fileURLToPath } from "node:url";
8
8
  import { cookieHeaderFromSetCookie, defaultSessionPath, writeSavedSession } from "./session-store.mjs";
9
9
 
10
10
  const FORMAT_OPTIONS = new Set(["text", "json"]);
11
+ const AUTH_MODE_PUBLIC_SIGNUP = "public_signup";
12
+ const AUTH_MODE_ENTERPRISE_PREPROVISIONED = "enterprise_preprovisioned";
13
+ const AUTH_MODE_HYBRID = "hybrid";
14
+ const KNOWN_AUTH_MODES = new Set([AUTH_MODE_PUBLIC_SIGNUP, AUTH_MODE_ENTERPRISE_PREPROVISIONED, AUTH_MODE_HYBRID]);
11
15
 
12
16
  function usage() {
13
17
  const text = [
@@ -151,6 +155,52 @@ async function requestJson(url, { method, body, headers = {}, fetchImpl = fetch
151
155
  return { res, text, json };
152
156
  }
153
157
 
158
+ function normalizeAuthMode(value) {
159
+ const mode = String(value ?? "").trim().toLowerCase();
160
+ return KNOWN_AUTH_MODES.has(mode) ? mode : "unknown";
161
+ }
162
+
163
+ export async function detectDeploymentAuthMode({ baseUrl, fetchImpl = fetch } = {}) {
164
+ const normalizedBaseUrl = mustHttpUrl(baseUrl, "base URL");
165
+ let response;
166
+ try {
167
+ response = await requestJson(`${normalizedBaseUrl}/v1/public/auth-mode`, {
168
+ method: "GET",
169
+ fetchImpl
170
+ });
171
+ } catch {
172
+ return {
173
+ schemaVersion: "SettldAuthModeDiscovery.v1",
174
+ mode: "unknown",
175
+ source: "network_error",
176
+ enterpriseProvisionedTenantsOnly: null
177
+ };
178
+ }
179
+ if (!response.res.ok) {
180
+ return {
181
+ schemaVersion: "SettldAuthModeDiscovery.v1",
182
+ mode: "unknown",
183
+ source: `http_${response.res.status}`,
184
+ enterpriseProvisionedTenantsOnly: null
185
+ };
186
+ }
187
+ const mode = normalizeAuthMode(response.json?.authMode);
188
+ const enterpriseOnly =
189
+ typeof response.json?.enterpriseProvisionedTenantsOnly === "boolean"
190
+ ? response.json.enterpriseProvisionedTenantsOnly
191
+ : mode === AUTH_MODE_ENTERPRISE_PREPROVISIONED
192
+ ? true
193
+ : mode === "unknown"
194
+ ? null
195
+ : false;
196
+ return {
197
+ schemaVersion: "SettldAuthModeDiscovery.v1",
198
+ mode,
199
+ source: "endpoint",
200
+ enterpriseProvisionedTenantsOnly: enterpriseOnly
201
+ };
202
+ }
203
+
154
204
  function responseCode({ json }) {
155
205
  const direct = typeof json?.code === "string" ? json.code : "";
156
206
  if (direct) return direct;
@@ -220,6 +270,15 @@ export async function runLogin({
220
270
  }
221
271
 
222
272
  const baseUrl = mustHttpUrl(state.baseUrl, "base URL");
273
+ const authMode = await detectDeploymentAuthMode({ baseUrl, fetchImpl });
274
+ if (interactive && authMode.mode !== "unknown") {
275
+ stdout.write(`Detected auth mode: ${authMode.mode}\n`);
276
+ }
277
+ if (!state.tenantId && authMode.mode === AUTH_MODE_ENTERPRISE_PREPROVISIONED) {
278
+ throw new Error(
279
+ "This deployment uses enterprise_preprovisioned mode. Pass --tenant-id <existing_tenant> and login via OTP, or use bootstrap/manual API key flow."
280
+ );
281
+ }
223
282
  if (!state.email) throw new Error("email is required");
224
283
  if (!state.tenantId && !state.company) throw new Error("company is required when tenant ID is omitted");
225
284
 
@@ -230,6 +289,17 @@ export async function runLogin({
230
289
  fetchImpl
231
290
  });
232
291
  if (!otpRequest.res.ok) {
292
+ const code = responseCode(otpRequest);
293
+ if (otpRequest.res.status === 400 && code === "BUYER_AUTH_DISABLED") {
294
+ throw new Error(
295
+ "buyer OTP login is not enabled for this tenant. This tenant may be stale on the onboarding service; rerun `settld login` without --tenant-id to create a fresh tenant, or use bootstrap/manual API key mode."
296
+ );
297
+ }
298
+ if (otpRequest.res.status === 403 && code === "FORBIDDEN") {
299
+ throw new Error(
300
+ "OTP login is unavailable on this base URL. Use `Generate during setup` with an onboarding bootstrap API key, or use an existing tenant API key."
301
+ );
302
+ }
233
303
  const message = responseMessage(otpRequest);
234
304
  throw new Error(`otp request failed (${otpRequest.res.status}): ${message}`);
235
305
  }
@@ -261,7 +331,7 @@ export async function runLogin({
261
331
  otpAlreadyIssued = Boolean(signup.json?.otpIssued);
262
332
  if (interactive) stdout.write(`Created tenant: ${tenantId}\n`);
263
333
  }
264
- if (!otpAlreadyIssued) await requestTenantOtp(tenantId);
334
+ if (!otpAlreadyIssued && !state.otp) await requestTenantOtp(tenantId);
265
335
 
266
336
  if (!state.otp && interactive) {
267
337
  state.otp = await promptLine(rl, "OTP code", { required: true });
@@ -299,7 +369,8 @@ export async function runLogin({
299
369
  tenantId: session.tenantId,
300
370
  email: session.email,
301
371
  sessionFile: state.sessionFile,
302
- expiresAt: session.expiresAt ?? null
372
+ expiresAt: session.expiresAt ?? null,
373
+ authMode: authMode.mode
303
374
  };
304
375
 
305
376
  if (state.format === "json") {