settld 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/CONFIG.md +12 -0
- package/docs/README.md +3 -0
- package/docs/ops/HOSTED_BASELINE_R2.md +4 -2
- package/docs/ops/MINIMUM_PRODUCTION_TOPOLOGY.md +19 -7
- package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +8 -3
- package/package.json +3 -1
- package/scripts/ci/run-public-onboarding-gate.mjs +136 -0
- package/scripts/setup/login.mjs +111 -17
- package/scripts/setup/onboard.mjs +176 -40
- package/scripts/setup/onboarding-failure-taxonomy.mjs +96 -0
- package/scripts/setup/onboarding-state-machine.mjs +102 -0
- package/services/magic-link/README.md +343 -0
- package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_criteria.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/evidence/evidence_index.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/metering/metering_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_definition.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_criteria.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/evidence/evidence_index.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/metering/metering_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/sla/sla_definition.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/sla/sla_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/trust.json +11 -0
- package/services/magic-link/src/audit-log.js +24 -0
- package/services/magic-link/src/buyer-auth.js +220 -0
- package/services/magic-link/src/buyer-notifications.js +402 -0
- package/services/magic-link/src/buyer-users.js +129 -0
- package/services/magic-link/src/decision-otp.js +156 -0
- package/services/magic-link/src/decisions.js +92 -0
- package/services/magic-link/src/ingest-keys.js +137 -0
- package/services/magic-link/src/maintenance.js +70 -0
- package/services/magic-link/src/onboarding-email-sequence.js +331 -0
- package/services/magic-link/src/payment-triggers.js +733 -0
- package/services/magic-link/src/pdf.js +149 -0
- package/services/magic-link/src/policy.js +69 -0
- package/services/magic-link/src/redaction.js +6 -0
- package/services/magic-link/src/render-model.js +70 -0
- package/services/magic-link/src/retention-gc.js +158 -0
- package/services/magic-link/src/run-records.js +496 -0
- package/services/magic-link/src/s3.js +171 -0
- package/services/magic-link/src/server.js +15788 -0
- package/services/magic-link/src/settlement-decisions.js +84 -0
- package/services/magic-link/src/smtp.js +202 -0
- package/services/magic-link/src/storage-cli.js +88 -0
- package/services/magic-link/src/storage-format.js +59 -0
- package/services/magic-link/src/tenant-billing.js +115 -0
- package/services/magic-link/src/tenant-onboarding.js +467 -0
- package/services/magic-link/src/tenant-settings.js +1140 -0
- package/services/magic-link/src/usage.js +80 -0
- package/services/magic-link/src/verify-queue.js +179 -0
- package/services/magic-link/src/verify-worker.js +157 -0
- package/services/magic-link/src/webhook-retries.js +542 -0
- package/services/magic-link/src/webhooks.js +218 -0
- package/src/api/app.js +129 -0
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
|
|
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
|
-
|
|
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
|
|
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-
|
|
71
|
-
3.
|
|
72
|
-
4.
|
|
73
|
-
5.
|
|
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.
|
|
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-
|
|
37
|
-
3. Deploy `
|
|
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.
|
|
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
|
+
|
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 = [
|
|
@@ -39,6 +43,7 @@ function readArgValue(argv, index, rawArg) {
|
|
|
39
43
|
function parseArgs(argv) {
|
|
40
44
|
const out = {
|
|
41
45
|
baseUrl: "https://api.settld.work",
|
|
46
|
+
baseUrlProvided: false,
|
|
42
47
|
tenantId: "",
|
|
43
48
|
email: "",
|
|
44
49
|
company: "",
|
|
@@ -64,6 +69,7 @@ function parseArgs(argv) {
|
|
|
64
69
|
if (arg === "--base-url" || arg.startsWith("--base-url=")) {
|
|
65
70
|
const parsed = readArgValue(argv, i, arg);
|
|
66
71
|
out.baseUrl = parsed.value;
|
|
72
|
+
out.baseUrlProvided = true;
|
|
67
73
|
i = parsed.nextIndex;
|
|
68
74
|
continue;
|
|
69
75
|
}
|
|
@@ -149,6 +155,66 @@ async function requestJson(url, { method, body, headers = {}, fetchImpl = fetch
|
|
|
149
155
|
return { res, text, json };
|
|
150
156
|
}
|
|
151
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
|
+
|
|
204
|
+
function responseCode({ json }) {
|
|
205
|
+
const direct = typeof json?.code === "string" ? json.code : "";
|
|
206
|
+
if (direct) return direct;
|
|
207
|
+
const error = typeof json?.error === "string" ? json.error : "";
|
|
208
|
+
return error ? error.toUpperCase() : "";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function responseMessage({ json, text }, fallback = "unknown error") {
|
|
212
|
+
if (typeof json?.message === "string" && json.message.trim()) return json.message.trim();
|
|
213
|
+
if (typeof json?.error === "string" && json.error.trim()) return json.error.trim();
|
|
214
|
+
const raw = String(text ?? "").trim();
|
|
215
|
+
return raw || fallback;
|
|
216
|
+
}
|
|
217
|
+
|
|
152
218
|
async function promptLine(rl, label, { defaultValue = "", required = true } = {}) {
|
|
153
219
|
const suffix = defaultValue ? ` [${defaultValue}]` : "";
|
|
154
220
|
const value = String(await rl.question(`${label}${suffix}: `) ?? "").trim() || String(defaultValue ?? "").trim();
|
|
@@ -190,7 +256,9 @@ export async function runLogin({
|
|
|
190
256
|
const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
|
|
191
257
|
try {
|
|
192
258
|
if (interactive) {
|
|
193
|
-
|
|
259
|
+
if (!args.baseUrlProvided) {
|
|
260
|
+
state.baseUrl = await promptLine(rl, "Settld base URL", { defaultValue: state.baseUrl || "https://api.settld.work" });
|
|
261
|
+
}
|
|
194
262
|
state.tenantId = await promptLine(rl, "Tenant ID (optional, leave blank to create new)", {
|
|
195
263
|
defaultValue: state.tenantId,
|
|
196
264
|
required: false
|
|
@@ -202,10 +270,38 @@ export async function runLogin({
|
|
|
202
270
|
}
|
|
203
271
|
|
|
204
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
|
+
}
|
|
205
282
|
if (!state.email) throw new Error("email is required");
|
|
206
283
|
if (!state.tenantId && !state.company) throw new Error("company is required when tenant ID is omitted");
|
|
207
284
|
|
|
285
|
+
const requestTenantOtp = async (tenantId) => {
|
|
286
|
+
const otpRequest = await requestJson(`${baseUrl}/v1/tenants/${encodeURIComponent(String(tenantId ?? ""))}/buyer/login/otp`, {
|
|
287
|
+
method: "POST",
|
|
288
|
+
body: { email: state.email },
|
|
289
|
+
fetchImpl
|
|
290
|
+
});
|
|
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
|
+
}
|
|
298
|
+
const message = responseMessage(otpRequest);
|
|
299
|
+
throw new Error(`otp request failed (${otpRequest.res.status}): ${message}`);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
208
303
|
let tenantId = state.tenantId;
|
|
304
|
+
let otpAlreadyIssued = false;
|
|
209
305
|
if (!tenantId) {
|
|
210
306
|
const signup = await requestJson(`${baseUrl}/v1/public/signup`, {
|
|
211
307
|
method: "POST",
|
|
@@ -213,27 +309,24 @@ export async function runLogin({
|
|
|
213
309
|
fetchImpl
|
|
214
310
|
});
|
|
215
311
|
if (!signup.res.ok) {
|
|
216
|
-
const code =
|
|
217
|
-
const message =
|
|
312
|
+
const code = responseCode(signup);
|
|
313
|
+
const message = responseMessage(signup);
|
|
218
314
|
if (code === "SIGNUP_DISABLED") {
|
|
219
315
|
throw new Error("Public signup is disabled for this environment. Use an existing tenant ID or bootstrap key flow.");
|
|
220
316
|
}
|
|
221
|
-
|
|
317
|
+
if (signup.res.status === 403 && code === "FORBIDDEN") {
|
|
318
|
+
throw new Error(
|
|
319
|
+
"Public signup is unavailable on this base URL. Retry with --tenant-id <existing_tenant>, or in `settld setup` choose `Generate during setup` and provide an onboarding bootstrap API key."
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
throw new Error(`public signup failed (${signup.res.status}): ${message}`);
|
|
222
323
|
}
|
|
223
324
|
tenantId = String(signup.json?.tenantId ?? "").trim();
|
|
224
325
|
if (!tenantId) throw new Error("public signup response missing tenantId");
|
|
326
|
+
otpAlreadyIssued = Boolean(signup.json?.otpIssued);
|
|
225
327
|
if (interactive) stdout.write(`Created tenant: ${tenantId}\n`);
|
|
226
|
-
} else {
|
|
227
|
-
const otpRequest = await requestJson(`${baseUrl}/v1/tenants/${encodeURIComponent(tenantId)}/buyer/login/otp`, {
|
|
228
|
-
method: "POST",
|
|
229
|
-
body: { email: state.email },
|
|
230
|
-
fetchImpl
|
|
231
|
-
});
|
|
232
|
-
if (!otpRequest.res.ok) {
|
|
233
|
-
const message = typeof otpRequest.json?.message === "string" ? otpRequest.json.message : otpRequest.text;
|
|
234
|
-
throw new Error(`otp request failed (${otpRequest.res.status}): ${message || "unknown error"}`);
|
|
235
|
-
}
|
|
236
328
|
}
|
|
329
|
+
if (!otpAlreadyIssued) await requestTenantOtp(tenantId);
|
|
237
330
|
|
|
238
331
|
if (!state.otp && interactive) {
|
|
239
332
|
state.otp = await promptLine(rl, "OTP code", { required: true });
|
|
@@ -246,8 +339,8 @@ export async function runLogin({
|
|
|
246
339
|
fetchImpl
|
|
247
340
|
});
|
|
248
341
|
if (!login.res.ok) {
|
|
249
|
-
const message =
|
|
250
|
-
throw new Error(`login failed (${login.res.status}): ${message
|
|
342
|
+
const message = responseMessage(login);
|
|
343
|
+
throw new Error(`login failed (${login.res.status}): ${message}`);
|
|
251
344
|
}
|
|
252
345
|
const setCookie = login.res.headers.get("set-cookie") ?? "";
|
|
253
346
|
const cookie = cookieHeaderFromSetCookie(setCookie);
|
|
@@ -271,7 +364,8 @@ export async function runLogin({
|
|
|
271
364
|
tenantId: session.tenantId,
|
|
272
365
|
email: session.email,
|
|
273
366
|
sessionFile: state.sessionFile,
|
|
274
|
-
expiresAt: session.expiresAt ?? null
|
|
367
|
+
expiresAt: session.expiresAt ?? null,
|
|
368
|
+
authMode: authMode.mode
|
|
275
369
|
};
|
|
276
370
|
|
|
277
371
|
if (state.format === "json") {
|