settld 0.2.4 → 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.
Files changed (118) hide show
  1. package/docs/CONFIG.md +12 -0
  2. package/docs/README.md +3 -0
  3. package/docs/ops/HOSTED_BASELINE_R2.md +4 -2
  4. package/docs/ops/MINIMUM_PRODUCTION_TOPOLOGY.md +19 -7
  5. package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +8 -3
  6. package/package.json +3 -1
  7. package/scripts/ci/run-public-onboarding-gate.mjs +136 -0
  8. package/scripts/setup/login.mjs +67 -1
  9. package/scripts/setup/onboard.mjs +159 -28
  10. package/scripts/setup/onboarding-failure-taxonomy.mjs +96 -0
  11. package/scripts/setup/onboarding-state-machine.mjs +102 -0
  12. package/services/magic-link/README.md +343 -0
  13. package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_criteria.json +1 -0
  14. package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_evaluation.json +1 -0
  15. package/services/magic-link/assets/samples/closepack/known-bad/attestation/bundle_head_attestation.json +1 -0
  16. package/services/magic-link/assets/samples/closepack/known-bad/evidence/evidence_index.json +1 -0
  17. package/services/magic-link/assets/samples/closepack/known-bad/governance/policy.json +1 -0
  18. package/services/magic-link/assets/samples/closepack/known-bad/governance/revocations.json +1 -0
  19. package/services/magic-link/assets/samples/closepack/known-bad/manifest.json +1 -0
  20. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
  21. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/policy.json +1 -0
  22. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/revocations.json +1 -0
  23. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
  24. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/manifest.json +1 -0
  25. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/metering/metering_report.json +1 -0
  26. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
  27. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
  28. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
  29. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
  30. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
  31. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
  32. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
  33. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
  34. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
  35. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
  36. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
  37. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
  38. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
  39. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
  40. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
  41. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
  42. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
  43. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
  44. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/settld.json +1 -0
  45. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/verify/verification_report.json +1 -0
  46. package/services/magic-link/assets/samples/closepack/known-bad/settld.json +1 -0
  47. package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_definition.json +1 -0
  48. package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_evaluation.json +1 -0
  49. package/services/magic-link/assets/samples/closepack/known-bad/verify/verification_report.json +1 -0
  50. package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_criteria.json +1 -0
  51. package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_evaluation.json +1 -0
  52. package/services/magic-link/assets/samples/closepack/known-good/attestation/bundle_head_attestation.json +1 -0
  53. package/services/magic-link/assets/samples/closepack/known-good/evidence/evidence_index.json +1 -0
  54. package/services/magic-link/assets/samples/closepack/known-good/governance/policy.json +1 -0
  55. package/services/magic-link/assets/samples/closepack/known-good/governance/revocations.json +1 -0
  56. package/services/magic-link/assets/samples/closepack/known-good/manifest.json +1 -0
  57. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
  58. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/policy.json +1 -0
  59. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/revocations.json +1 -0
  60. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
  61. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/manifest.json +1 -0
  62. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/metering/metering_report.json +1 -0
  63. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
  64. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
  65. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
  66. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
  67. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
  68. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
  69. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
  70. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
  71. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
  72. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
  73. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
  74. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
  75. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
  76. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
  77. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
  78. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
  79. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
  80. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
  81. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/settld.json +1 -0
  82. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/verify/verification_report.json +1 -0
  83. package/services/magic-link/assets/samples/closepack/known-good/settld.json +1 -0
  84. package/services/magic-link/assets/samples/closepack/known-good/sla/sla_definition.json +1 -0
  85. package/services/magic-link/assets/samples/closepack/known-good/sla/sla_evaluation.json +1 -0
  86. package/services/magic-link/assets/samples/closepack/known-good/verify/verification_report.json +1 -0
  87. package/services/magic-link/assets/samples/trust.json +11 -0
  88. package/services/magic-link/src/audit-log.js +24 -0
  89. package/services/magic-link/src/buyer-auth.js +220 -0
  90. package/services/magic-link/src/buyer-notifications.js +402 -0
  91. package/services/magic-link/src/buyer-users.js +129 -0
  92. package/services/magic-link/src/decision-otp.js +156 -0
  93. package/services/magic-link/src/decisions.js +92 -0
  94. package/services/magic-link/src/ingest-keys.js +137 -0
  95. package/services/magic-link/src/maintenance.js +70 -0
  96. package/services/magic-link/src/onboarding-email-sequence.js +331 -0
  97. package/services/magic-link/src/payment-triggers.js +733 -0
  98. package/services/magic-link/src/pdf.js +149 -0
  99. package/services/magic-link/src/policy.js +69 -0
  100. package/services/magic-link/src/redaction.js +6 -0
  101. package/services/magic-link/src/render-model.js +70 -0
  102. package/services/magic-link/src/retention-gc.js +158 -0
  103. package/services/magic-link/src/run-records.js +496 -0
  104. package/services/magic-link/src/s3.js +171 -0
  105. package/services/magic-link/src/server.js +15788 -0
  106. package/services/magic-link/src/settlement-decisions.js +84 -0
  107. package/services/magic-link/src/smtp.js +202 -0
  108. package/services/magic-link/src/storage-cli.js +88 -0
  109. package/services/magic-link/src/storage-format.js +59 -0
  110. package/services/magic-link/src/tenant-billing.js +115 -0
  111. package/services/magic-link/src/tenant-onboarding.js +467 -0
  112. package/services/magic-link/src/tenant-settings.js +1140 -0
  113. package/services/magic-link/src/usage.js +80 -0
  114. package/services/magic-link/src/verify-queue.js +179 -0
  115. package/services/magic-link/src/verify-worker.js +157 -0
  116. package/services/magic-link/src/webhook-retries.js +542 -0
  117. package/services/magic-link/src/webhooks.js +218 -0
  118. package/src/api/app.js +129 -0
package/docs/CONFIG.md CHANGED
@@ -82,6 +82,18 @@ Delivery/worker tuning:
82
82
  - `PROXY_AUTH_KEY_TOUCH_MIN_SECONDS` (default: `60`)
83
83
  Throttle how often `last_used_at` is updated for API keys (reduces DB write amplification).
84
84
 
85
+ ## Public onboarding routing
86
+
87
+ - `PROXY_ONBOARDING_BASE_URL` (optional but required for public onboarding on `settld-api`)
88
+ Absolute `http(s)` URL for the onboarding service (`services/magic-link`). When set, `settld-api` reverse-proxies public onboarding routes:
89
+ - `/v1/public/auth-mode`
90
+ - `/v1/public/signup`
91
+ - `/v1/tenants/:tenantId/buyer/login/otp`
92
+ - `/v1/tenants/:tenantId/buyer/login`
93
+ - `/v1/tenants/:tenantId/onboarding/*`
94
+
95
+ If missing, these routes fail closed with `503` and code `ONBOARDING_PROXY_NOT_CONFIGURED`.
96
+
85
97
  ## Ingest auth
86
98
 
87
99
  - `PROXY_INGEST_TOKEN` (optional)
package/docs/README.md CHANGED
@@ -32,3 +32,6 @@ Reference docs:
32
32
 
33
33
  - `docs/QUICKSTART_MCP_HOSTS.md`
34
34
  - `docs/QUICKSTART_MCP.md`
35
+ - `planning/trust-os-v1/state-of-the-art-launch-readiness-scorecard.md`
36
+ - `planning/sprints/state-of-the-art-v1-6-week-plan.md`
37
+ - `planning/jira/state-of-the-art-v1-backlog.json`
@@ -13,14 +13,16 @@ This is the minimum hosted setup for a real product surface.
13
13
 
14
14
  ## 2) Railway service split
15
15
 
16
- Create two Railway services from this repo per environment:
16
+ Create three Railway services from this repo per environment:
17
17
 
18
18
  - `settld-api`:
19
19
  - start command: `npm run start:prod`
20
+ - `settld-magic-link`:
21
+ - start command: `npm run start:magic-link`
20
22
  - `settld-worker`:
21
23
  - start command: `npm run start:maintenance`
22
24
 
23
- Both services must point at the same environment DB and secret set for that environment.
25
+ All services must point at the same environment DB and secret set for that environment.
24
26
 
25
27
  ## 3) Required runtime controls
26
28
 
@@ -7,12 +7,13 @@ This is the smallest topology that supports real paid agent tool calls with audi
7
7
  | Component | Purpose | Start command |
8
8
  |---|---|---|
9
9
  | `settld-api` | control plane + kernel APIs + receipts + ops endpoints | `npm run start:prod` |
10
+ | `settld-magic-link` | public onboarding/auth/wallet bootstrap service | `npm run start:magic-link` |
10
11
  | `settld-maintenance` | reconciliation/cleanup/maintenance ticks | `npm run start:maintenance` |
11
12
  | `postgres` | system of record for tenants, gates, receipts, ops state | managed Postgres |
12
13
  | `x402-gateway` | payment challenge/authorize/verify wrapper for paid tool calls | `npm run start:x402-gateway` |
13
14
  | paid upstream tool API(s) | actual provider tools (`/exa`, `/weather`, etc.) | provider-specific |
14
15
 
15
- Without all five, the end-to-end paid tool path is incomplete.
16
+ Without all six, public onboarding + paid tool path is incomplete.
16
17
 
17
18
  ## 2) Recommended production shape
18
19
 
@@ -35,9 +36,19 @@ Reference baseline: `docs/ops/HOSTED_BASELINE_R2.md`.
35
36
  - `PROXY_OPS_TOKENS` (scoped ops tokens)
36
37
  - `PROXY_FINANCE_RECONCILE_ENABLED=1`
37
38
  - `PROXY_MONEY_RAIL_RECONCILE_ENABLED=1`
39
+ - `PROXY_ONBOARDING_BASE_URL=https://<magic-link-host>`
38
40
 
39
41
  Primary config source: `docs/CONFIG.md`.
40
42
 
43
+ ### `settld-magic-link`
44
+
45
+ - `NODE_ENV=production`
46
+ - `MAGIC_LINK_API_KEY` (admin key)
47
+ - `MAGIC_LINK_PUBLIC_SIGNUP_ENABLED=1` (for self-serve public onboarding)
48
+ - `MAGIC_LINK_BUYER_OTP_DELIVERY_MODE=smtp` + SMTP env
49
+ - `MAGIC_LINK_SETTLD_API_BASE_URL=https://<settld-api-host>`
50
+ - `MAGIC_LINK_SETTLD_OPS_TOKEN=<scoped ops token>`
51
+
41
52
  ### `settld-maintenance`
42
53
 
43
54
  - Same DB/env set as `settld-api`
@@ -67,22 +78,23 @@ Reference flow: `docs/QUICKSTART_X402_GATEWAY.md`.
67
78
  Must host for real customer traffic:
68
79
 
69
80
  1. `settld-api`
70
- 2. `settld-maintenance`
71
- 3. Postgres
72
- 4. `x402-gateway`
73
- 5. At least one paid upstream provider API
81
+ 2. `settld-magic-link`
82
+ 3. `settld-maintenance`
83
+ 4. Postgres
84
+ 5. `x402-gateway`
85
+ 6. At least one paid upstream provider API
74
86
 
75
87
  Optional at first:
76
88
 
77
89
  1. Receiver service (`npm run start:receiver`)
78
90
  2. Finance sink (`npm run start:finance-sink`)
79
- 3. Magic-link UI service
91
+ 3. Additional onboarding UI shells
80
92
 
81
93
  ## 6) Definition of "usable in production"
82
94
 
83
95
  A deployment is considered usable when all are true:
84
96
 
85
- 1. `GET /healthz` is green on API and gateway.
97
+ 1. `GET /healthz` is green on API and gateway; `GET /v1/public/auth-mode` is reachable on API host.
86
98
  2. Hosted baseline evidence command passes for the environment.
87
99
  3. One paid MCP tool call succeeds end-to-end with artifact output.
88
100
  4. Receipt verification succeeds and is replay-auditable.
@@ -14,7 +14,7 @@ Use this checklist to launch and verify a real hosted Settld environment.
14
14
  2. Confirm release workflow is blocked unless NOO-50 and the kernel/cutover gates are green for the release commit.
15
15
  3. Confirm release workflow runs NOO-65 promotion guard and blocks publish lanes if `release-promotion-guard.json` verdict is not pass/override-pass.
16
16
  4. Confirm staging and production have separate domains, databases, secrets, and signer keys.
17
- 5. Confirm required services are deployable: `npm run start:prod`, `npm run start:maintenance`, `npm run start:x402-gateway`.
17
+ 5. Confirm required services are deployable: `npm run start:prod`, `npm run start:magic-link`, `npm run start:maintenance`, `npm run start:x402-gateway`.
18
18
  6. Configure GitHub Environment `production_cutover_gate` with:
19
19
  - `PROD_BASE_URL`
20
20
  - `PROD_TENANT_ID`
@@ -29,17 +29,22 @@ Use this checklist to launch and verify a real hosted Settld environment.
29
29
  3. Set scoped `PROXY_OPS_TOKENS`.
30
30
  4. Configure rate limits and quotas from `docs/CONFIG.md`.
31
31
  5. Configure gateway secrets: `SETTLD_API_URL`, `SETTLD_API_KEY`, `UPSTREAM_URL`.
32
+ 6. Configure onboarding routing:
33
+ - `PROXY_ONBOARDING_BASE_URL=https://<magic-link-host>` on `settld-api`
34
+ - `MAGIC_LINK_PUBLIC_SIGNUP_ENABLED=1` and OTP delivery (`smtp`) on `settld-magic-link`
32
35
 
33
36
  ## Phase 2: Deploy services
34
37
 
35
38
  1. Deploy `settld-api`.
36
- 2. Deploy `settld-maintenance`.
37
- 3. Deploy `x402-gateway`.
39
+ 2. Deploy `settld-magic-link`.
40
+ 3. Deploy `settld-maintenance`.
41
+ 4. Deploy `x402-gateway`.
38
42
  4. Verify service health:
39
43
 
40
44
  ```bash
41
45
  curl -fsS https://api.settld.work/healthz
42
46
  curl -fsS https://gateway.settld.work/healthz
47
+ npm run test:ops:public-onboarding-gate -- --base-url https://api.settld.work --tenant-id tenant_default
43
48
  ```
44
49
 
45
50
  ## Phase 3: Baseline ops verification
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "settld",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "Settld kernel CLI and local control-plane tooling",
5
5
  "private": false,
6
6
  "type": "module",
@@ -25,6 +25,7 @@
25
25
  "scripts",
26
26
  "docs",
27
27
  "src",
28
+ "services/magic-link",
28
29
  "services/finance-sink",
29
30
  "services/x402-gateway",
30
31
  "services/receiver"
@@ -110,6 +111,7 @@
110
111
  "test:ops:onboarding-host-success-gate": "node scripts/ci/run-onboarding-host-success-gate.mjs",
111
112
  "test:ops:self-serve-benchmark": "node scripts/ci/build-self-serve-benchmark-report.mjs",
112
113
  "test:ops:launch-cutover-packet": "node scripts/ci/build-launch-cutover-packet.mjs",
114
+ "test:ops:public-onboarding-gate": "node scripts/ci/run-public-onboarding-gate.mjs",
113
115
  "test:ops:production-cutover-gate": "node scripts/ci/run-production-cutover-gate.mjs",
114
116
  "test:ops:release-promotion-guard": "node scripts/ci/run-release-promotion-guard.mjs",
115
117
  "test:ci:mcp-host-smoke": "node scripts/ci/run-mcp-host-smoke.mjs",
@@ -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,12 @@ 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 === 403 && code === "FORBIDDEN") {
294
+ throw new Error(
295
+ "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."
296
+ );
297
+ }
233
298
  const message = responseMessage(otpRequest);
234
299
  throw new Error(`otp request failed (${otpRequest.res.status}): ${message}`);
235
300
  }
@@ -299,7 +364,8 @@ export async function runLogin({
299
364
  tenantId: session.tenantId,
300
365
  email: session.email,
301
366
  sessionFile: state.sessionFile,
302
- expiresAt: session.expiresAt ?? null
367
+ expiresAt: session.expiresAt ?? null,
368
+ authMode: authMode.mode
303
369
  };
304
370
 
305
371
  if (state.format === "json") {