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,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
|
+
|
package/scripts/setup/login.mjs
CHANGED
|
@@ -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") {
|