settld 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/Dockerfile +2 -2
  2. package/docs/CONFIG.md +12 -0
  3. package/docs/README.md +3 -0
  4. package/docs/ops/HOSTED_BASELINE_R2.md +4 -2
  5. package/docs/ops/MINIMUM_PRODUCTION_TOPOLOGY.md +19 -7
  6. package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +8 -3
  7. package/package.json +4 -1
  8. package/packages/api-sdk/README.md +71 -0
  9. package/packages/api-sdk/src/client.js +1021 -0
  10. package/packages/api-sdk/src/express-middleware.js +163 -0
  11. package/packages/api-sdk/src/index.d.ts +1662 -0
  12. package/packages/api-sdk/src/index.js +10 -0
  13. package/packages/api-sdk/src/webhook-signature.js +182 -0
  14. package/packages/api-sdk/src/x402-autopay.js +210 -0
  15. package/scripts/ci/cli-pack-smoke.mjs +2 -0
  16. package/scripts/ci/run-public-onboarding-gate.mjs +136 -0
  17. package/scripts/setup/login.mjs +73 -2
  18. package/scripts/setup/onboard.mjs +173 -28
  19. package/scripts/setup/onboarding-failure-taxonomy.mjs +107 -0
  20. package/scripts/setup/onboarding-state-machine.mjs +102 -0
  21. package/services/magic-link/README.md +352 -0
  22. package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_criteria.json +1 -0
  23. package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_evaluation.json +1 -0
  24. package/services/magic-link/assets/samples/closepack/known-bad/attestation/bundle_head_attestation.json +1 -0
  25. package/services/magic-link/assets/samples/closepack/known-bad/evidence/evidence_index.json +1 -0
  26. package/services/magic-link/assets/samples/closepack/known-bad/governance/policy.json +1 -0
  27. package/services/magic-link/assets/samples/closepack/known-bad/governance/revocations.json +1 -0
  28. package/services/magic-link/assets/samples/closepack/known-bad/manifest.json +1 -0
  29. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
  30. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/policy.json +1 -0
  31. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/revocations.json +1 -0
  32. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
  33. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/manifest.json +1 -0
  34. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/metering/metering_report.json +1 -0
  35. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
  36. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
  37. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
  38. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
  39. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
  40. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
  41. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
  42. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
  43. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
  44. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
  45. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
  46. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
  47. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
  48. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
  49. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
  50. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
  51. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
  52. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
  53. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/settld.json +1 -0
  54. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/verify/verification_report.json +1 -0
  55. package/services/magic-link/assets/samples/closepack/known-bad/settld.json +1 -0
  56. package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_definition.json +1 -0
  57. package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_evaluation.json +1 -0
  58. package/services/magic-link/assets/samples/closepack/known-bad/verify/verification_report.json +1 -0
  59. package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_criteria.json +1 -0
  60. package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_evaluation.json +1 -0
  61. package/services/magic-link/assets/samples/closepack/known-good/attestation/bundle_head_attestation.json +1 -0
  62. package/services/magic-link/assets/samples/closepack/known-good/evidence/evidence_index.json +1 -0
  63. package/services/magic-link/assets/samples/closepack/known-good/governance/policy.json +1 -0
  64. package/services/magic-link/assets/samples/closepack/known-good/governance/revocations.json +1 -0
  65. package/services/magic-link/assets/samples/closepack/known-good/manifest.json +1 -0
  66. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
  67. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/policy.json +1 -0
  68. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/revocations.json +1 -0
  69. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
  70. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/manifest.json +1 -0
  71. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/metering/metering_report.json +1 -0
  72. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
  73. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
  74. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
  75. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
  76. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
  77. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
  78. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
  79. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
  80. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
  81. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
  82. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
  83. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
  84. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
  85. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
  86. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
  87. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
  88. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
  89. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
  90. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/settld.json +1 -0
  91. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/verify/verification_report.json +1 -0
  92. package/services/magic-link/assets/samples/closepack/known-good/settld.json +1 -0
  93. package/services/magic-link/assets/samples/closepack/known-good/sla/sla_definition.json +1 -0
  94. package/services/magic-link/assets/samples/closepack/known-good/sla/sla_evaluation.json +1 -0
  95. package/services/magic-link/assets/samples/closepack/known-good/verify/verification_report.json +1 -0
  96. package/services/magic-link/assets/samples/trust.json +11 -0
  97. package/services/magic-link/src/audit-log.js +24 -0
  98. package/services/magic-link/src/buyer-auth.js +251 -0
  99. package/services/magic-link/src/buyer-notifications.js +402 -0
  100. package/services/magic-link/src/buyer-users.js +129 -0
  101. package/services/magic-link/src/decision-otp.js +187 -0
  102. package/services/magic-link/src/decisions.js +92 -0
  103. package/services/magic-link/src/email-resend.js +89 -0
  104. package/services/magic-link/src/ingest-keys.js +137 -0
  105. package/services/magic-link/src/maintenance.js +95 -0
  106. package/services/magic-link/src/onboarding-email-sequence.js +331 -0
  107. package/services/magic-link/src/payment-triggers.js +733 -0
  108. package/services/magic-link/src/pdf.js +149 -0
  109. package/services/magic-link/src/policy.js +69 -0
  110. package/services/magic-link/src/redaction.js +6 -0
  111. package/services/magic-link/src/render-model.js +70 -0
  112. package/services/magic-link/src/retention-gc.js +158 -0
  113. package/services/magic-link/src/run-records.js +496 -0
  114. package/services/magic-link/src/s3.js +171 -0
  115. package/services/magic-link/src/server.js +15849 -0
  116. package/services/magic-link/src/settlement-decisions.js +84 -0
  117. package/services/magic-link/src/smtp.js +217 -0
  118. package/services/magic-link/src/storage-cli.js +88 -0
  119. package/services/magic-link/src/storage-format.js +59 -0
  120. package/services/magic-link/src/tenant-billing.js +115 -0
  121. package/services/magic-link/src/tenant-onboarding.js +467 -0
  122. package/services/magic-link/src/tenant-settings.js +1140 -0
  123. package/services/magic-link/src/usage.js +80 -0
  124. package/services/magic-link/src/verify-queue.js +179 -0
  125. package/services/magic-link/src/verify-worker.js +157 -0
  126. package/services/magic-link/src/webhook-retries.js +542 -0
  127. package/services/magic-link/src/webhooks.js +218 -0
  128. package/src/api/app.js +135 -1
@@ -13,8 +13,10 @@ import { bootstrapWalletProvider } from "../../src/core/wallet-provider-bootstra
13
13
  import { extractBootstrapMcpEnv, loadHostConfigHelper, runWizard } from "./wizard.mjs";
14
14
  import { SUPPORTED_HOSTS } from "./host-config.mjs";
15
15
  import { defaultSessionPath, readSavedSession } from "./session-store.mjs";
16
- import { runLogin } from "./login.mjs";
16
+ import { detectDeploymentAuthMode, runLogin } from "./login.mjs";
17
17
  import { runWalletCli } from "../wallet/cli.mjs";
18
+ import { classifyOnboardingFailure } from "./onboarding-failure-taxonomy.mjs";
19
+ import { ONBOARDING_EVENTS, ONBOARDING_STATES, transitionOnboardingState } from "./onboarding-state-machine.mjs";
18
20
 
19
21
  const WALLET_MODES = new Set(["managed", "byo", "none"]);
20
22
  const WALLET_PROVIDERS = new Set(["circle"]);
@@ -42,6 +44,7 @@ const REPO_ROOT = path.resolve(SCRIPT_DIR, "..", "..");
42
44
  const SETTLD_BIN = path.join(REPO_ROOT, "bin", "settld.js");
43
45
  const PROFILE_FINGERPRINT_REGEX = /^[0-9a-f]{64}$/;
44
46
  const DEFAULT_PUBLIC_BASE_URL = "https://api.settld.work";
47
+ const MAX_INTERACTIVE_API_KEY_MODE_ATTEMPTS = 8;
45
48
  const ANSI_RESET = "\u001b[0m";
46
49
  const ANSI_BOLD = "\u001b[1m";
47
50
  const ANSI_DIM = "\u001b[2m";
@@ -1123,10 +1126,24 @@ async function requestRuntimeBootstrapMcpEnv({
1123
1126
  json = null;
1124
1127
  }
1125
1128
  if (!res.ok) {
1129
+ const responseCode =
1130
+ json && typeof json === "object" && (typeof json?.code === "string" || typeof json?.error === "string")
1131
+ ? String(json?.code ?? json?.error ?? "").trim().toUpperCase()
1132
+ : "";
1126
1133
  const message =
1127
1134
  json && typeof json === "object"
1128
1135
  ? json?.message ?? json?.error ?? `HTTP ${res.status}`
1129
1136
  : text || `HTTP ${res.status}`;
1137
+ if (res.status === 403 && cookie && !apiKey) {
1138
+ throw new Error(
1139
+ `runtime bootstrap request failed (403): ${String(message)}. Saved login session was rejected for this tenant; rerun \`settld login\` without --tenant-id to create a fresh tenant, or choose \`Generate during setup\`.`
1140
+ );
1141
+ }
1142
+ if (res.status === 400 && responseCode === "BUYER_AUTH_DISABLED") {
1143
+ throw new Error(
1144
+ "runtime bootstrap request failed (400): buyer OTP login is not enabled for this tenant. Rerun `settld login` without --tenant-id to create a fresh tenant, or choose `Generate during setup`."
1145
+ );
1146
+ }
1130
1147
  throw new Error(`runtime bootstrap request failed (${res.status}): ${String(message)}`);
1131
1148
  }
1132
1149
  return extractBootstrapMcpEnv(json);
@@ -1136,6 +1153,8 @@ async function requestRemoteWalletBootstrap({
1136
1153
  baseUrl,
1137
1154
  tenantId,
1138
1155
  settldApiKey,
1156
+ bootstrapApiKey,
1157
+ sessionCookie,
1139
1158
  walletProvider,
1140
1159
  circleMode,
1141
1160
  circleBaseUrl,
@@ -1157,30 +1176,69 @@ async function requestRemoteWalletBootstrap({
1157
1176
  }
1158
1177
 
1159
1178
  const url = new URL(`/v1/tenants/${encodeURIComponent(String(tenantId ?? ""))}/onboarding/wallet-bootstrap`, normalizedBaseUrl);
1160
- const res = await fetchImpl(url.toString(), {
1161
- method: "POST",
1162
- headers: {
1163
- "content-type": "application/json",
1164
- "x-api-key": String(settldApiKey ?? "")
1165
- },
1166
- body: JSON.stringify(body)
1167
- });
1179
+ const candidates = [];
1180
+ const runtimeKey = String(settldApiKey ?? "").trim();
1181
+ const bootstrapKey = String(bootstrapApiKey ?? "").trim();
1182
+ const cookie = String(sessionCookie ?? "").trim();
1183
+ if (runtimeKey) candidates.push({ kind: "runtime_key", apiKey: runtimeKey });
1184
+ if (bootstrapKey && bootstrapKey !== runtimeKey) candidates.push({ kind: "bootstrap_key", apiKey: bootstrapKey });
1185
+ if (cookie) candidates.push({ kind: "session_cookie", cookie });
1186
+ if (!candidates.length) {
1187
+ throw new Error("remote wallet bootstrap requires SETTLD_API_KEY, bootstrap API key, or saved login session");
1188
+ }
1168
1189
 
1169
- const text = await res.text();
1190
+ let lastError = null;
1191
+ let succeeded = false;
1170
1192
  let json = null;
1171
- try {
1172
- json = text ? JSON.parse(text) : null;
1173
- } catch {
1193
+ for (const candidate of candidates) {
1194
+ const headers = {
1195
+ "content-type": "application/json"
1196
+ };
1197
+ if (candidate.apiKey) headers["x-api-key"] = candidate.apiKey;
1198
+ if (candidate.cookie) headers.cookie = candidate.cookie;
1199
+
1200
+ const res = await fetchImpl(url.toString(), {
1201
+ method: "POST",
1202
+ headers,
1203
+ body: JSON.stringify(body)
1204
+ });
1205
+
1206
+ const text = await res.text();
1174
1207
  json = null;
1175
- }
1176
- if (!res.ok) {
1208
+ try {
1209
+ json = text ? JSON.parse(text) : null;
1210
+ } catch {
1211
+ json = null;
1212
+ }
1213
+ if (res.ok) {
1214
+ succeeded = true;
1215
+ break;
1216
+ }
1177
1217
  const message =
1178
1218
  json && typeof json === "object"
1179
1219
  ? json?.message ?? json?.error ?? `HTTP ${res.status}`
1180
1220
  : text || `HTTP ${res.status}`;
1181
- throw new Error(`remote wallet bootstrap failed (${res.status}): ${String(message)}`);
1221
+ lastError = { status: res.status, message: String(message), kind: candidate.kind };
1222
+ if (res.status !== 403) {
1223
+ throw new Error(`remote wallet bootstrap failed (${res.status}): ${String(message)}`);
1224
+ }
1182
1225
  }
1183
- const bootstrap = json?.walletBootstrap;
1226
+ if (!succeeded) {
1227
+ const detail = lastError ? ` (${lastError.kind})` : "";
1228
+ if (lastError && lastError.status === 403) {
1229
+ throw new Error(
1230
+ `remote wallet bootstrap failed (403): forbidden${detail}. Try --wallet-mode none (finish trust wiring) or --wallet-bootstrap local with --circle-api-key.`
1231
+ );
1232
+ }
1233
+ if (lastError) {
1234
+ throw new Error(`remote wallet bootstrap failed (${lastError.status}): ${lastError.message}${detail}`);
1235
+ }
1236
+ throw new Error("remote wallet bootstrap failed: unknown error");
1237
+ }
1238
+ if (!json || typeof json !== "object" || Array.isArray(json)) {
1239
+ throw new Error("remote wallet bootstrap response missing payload");
1240
+ }
1241
+ const bootstrap = json.walletBootstrap;
1184
1242
  if (!bootstrap || typeof bootstrap !== "object" || Array.isArray(bootstrap)) {
1185
1243
  throw new Error("remote wallet bootstrap response missing walletBootstrap object");
1186
1244
  }
@@ -1196,6 +1254,7 @@ async function resolveRuntimeConfig({
1196
1254
  stdin = process.stdin,
1197
1255
  stdout = process.stdout,
1198
1256
  detectInstalledHostsImpl = detectInstalledHosts,
1257
+ detectDeploymentAuthModeImpl = detectDeploymentAuthMode,
1199
1258
  fetchImpl = fetch,
1200
1259
  runLoginImpl = runLogin,
1201
1260
  readSavedSessionImpl = readSavedSession
@@ -1234,6 +1293,7 @@ async function resolveRuntimeConfig({
1234
1293
  preflight: Boolean(args.preflight),
1235
1294
  smoke: Boolean(args.smoke),
1236
1295
  dryRun: Boolean(args.dryRun),
1296
+ authMode: "unknown",
1237
1297
  installedHosts
1238
1298
  };
1239
1299
  if (savedSession) {
@@ -1327,21 +1387,48 @@ async function resolveRuntimeConfig({
1327
1387
  );
1328
1388
 
1329
1389
  if (!out.baseUrl) out.baseUrl = DEFAULT_PUBLIC_BASE_URL;
1390
+ if (out.baseUrl) {
1391
+ try {
1392
+ const authDiscovery = await detectDeploymentAuthModeImpl({ baseUrl: out.baseUrl, fetchImpl });
1393
+ out.authMode = String(authDiscovery?.mode ?? "unknown");
1394
+ if (out.authMode !== "unknown") {
1395
+ stdout.write(`Detected auth mode: ${out.authMode}\n`);
1396
+ }
1397
+ } catch {
1398
+ out.authMode = "unknown";
1399
+ }
1400
+ }
1401
+ if (out.authMode === "enterprise_preprovisioned" && !out.tenantId && !savedSession?.tenantId) {
1402
+ out.tenantId = await promptLine(rl, "Tenant ID (required for this deployment mode)", { required: true });
1403
+ }
1330
1404
  if (!out.settldApiKey) {
1405
+ let loginUnavailableForRun = false;
1406
+ let preferredKeyMode = null;
1407
+ let keyModeAttempts = 0;
1331
1408
  while (!out.settldApiKey) {
1409
+ keyModeAttempts += 1;
1410
+ if (keyModeAttempts > MAX_INTERACTIVE_API_KEY_MODE_ATTEMPTS) {
1411
+ const error = new Error(
1412
+ `setup API key flow exceeded ${MAX_INTERACTIVE_API_KEY_MODE_ATTEMPTS} attempts; choose \`Generate during setup\` or \`Paste existing key\``
1413
+ );
1414
+ error.code = "ONBOARDING_KEY_MODE_LOOP_GUARD";
1415
+ throw error;
1416
+ }
1332
1417
  const canUseSavedSession =
1333
1418
  Boolean(out.sessionCookie) &&
1334
1419
  (!savedSession ||
1335
1420
  (normalizeHttpUrl(out.baseUrl) === normalizeHttpUrl(savedSession?.baseUrl) &&
1336
1421
  (!out.tenantId || String(out.tenantId ?? "").trim() === String(savedSession?.tenantId ?? "").trim())));
1337
1422
  const keyOptions = [];
1423
+ const loginAllowedForMode =
1424
+ out.authMode !== "enterprise_preprovisioned" || Boolean(String(out.tenantId ?? "").trim());
1338
1425
  if (canUseSavedSession) {
1339
1426
  keyOptions.push({
1340
1427
  value: "session",
1341
1428
  label: "Use saved login session",
1342
1429
  hint: `Reuse ${out.sessionFile} to mint runtime key`
1343
1430
  });
1344
- } else {
1431
+ } else if (!loginUnavailableForRun && loginAllowedForMode) {
1345
1432
  keyOptions.push({
1346
1433
  value: "login",
1347
1434
  label: "Login / create tenant (recommended)",
@@ -1352,18 +1439,35 @@ async function resolveRuntimeConfig({
1352
1439
  { value: "bootstrap", label: "Generate during setup", hint: "Use onboarding bootstrap API key" },
1353
1440
  { value: "manual", label: "Paste existing key", hint: "Use an existing tenant API key" }
1354
1441
  );
1442
+ const defaultKeyMode =
1443
+ preferredKeyMode && keyOptions.some((option) => option.value === preferredKeyMode)
1444
+ ? preferredKeyMode
1445
+ : canUseSavedSession
1446
+ ? "session"
1447
+ : loginUnavailableForRun
1448
+ ? "bootstrap"
1449
+ : loginAllowedForMode
1450
+ ? "login"
1451
+ : "bootstrap";
1355
1452
  const keyMode = await promptSelect(
1356
1453
  rl,
1357
1454
  stdin,
1358
1455
  stdout,
1359
1456
  "How should setup get your Settld API key?",
1360
1457
  keyOptions,
1361
- { defaultValue: canUseSavedSession ? "session" : "login", color }
1458
+ { defaultValue: defaultKeyMode, color }
1362
1459
  );
1363
1460
  if (keyMode === "login") {
1461
+ if (!loginAllowedForMode) {
1462
+ throw new Error(
1463
+ "login flow requires --tenant-id in enterprise_preprovisioned mode. Provide tenant ID or choose bootstrap/manual API key mode."
1464
+ );
1465
+ }
1364
1466
  try {
1467
+ const loginArgv = ["--base-url", out.baseUrl, "--session-file", out.sessionFile];
1468
+ if (out.tenantId) loginArgv.push("--tenant-id", out.tenantId);
1365
1469
  await runLoginImpl({
1366
- argv: ["--base-url", out.baseUrl, "--session-file", out.sessionFile],
1470
+ argv: loginArgv,
1367
1471
  stdin,
1368
1472
  stdout,
1369
1473
  fetchImpl
@@ -1378,13 +1482,21 @@ async function resolveRuntimeConfig({
1378
1482
  savedSession.tenantId = refreshedSession.tenantId;
1379
1483
  savedSession.cookie = refreshedSession.cookie;
1380
1484
  }
1485
+ preferredKeyMode = "session";
1381
1486
  } catch (err) {
1382
- stdout.write(`Login failed: ${err?.message ?? "unknown error"}\n`);
1383
- stdout.write("Choose `Generate during setup` if your deployment does not expose public signup/login.\n");
1487
+ const failure = classifyOnboardingFailure(err);
1488
+ stdout.write(`[${failure.code}] ${failure.message}\n`);
1489
+ if (failure.remediation) stdout.write(`Remediation: ${failure.remediation}\n`);
1490
+ if (failure.code === "ONBOARDING_AUTH_PUBLIC_SIGNUP_UNAVAILABLE" || failure.code === "ONBOARDING_AUTH_LOGIN_UNAVAILABLE") {
1491
+ loginUnavailableForRun = true;
1492
+ stdout.write("Login/signup has been disabled for this setup run. Continuing with API key modes.\n");
1493
+ }
1494
+ preferredKeyMode = "bootstrap";
1384
1495
  }
1385
1496
  continue;
1386
1497
  }
1387
1498
  if (keyMode === "bootstrap") {
1499
+ preferredKeyMode = "bootstrap";
1388
1500
  if (!out.bootstrapApiKey) {
1389
1501
  out.bootstrapApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Onboarding bootstrap API key");
1390
1502
  }
@@ -1397,6 +1509,7 @@ async function resolveRuntimeConfig({
1397
1509
  break;
1398
1510
  }
1399
1511
  if (keyMode === "manual") {
1512
+ preferredKeyMode = "manual";
1400
1513
  out.settldApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Settld API key");
1401
1514
  break;
1402
1515
  }
@@ -1530,6 +1643,7 @@ export async function runOnboard({
1530
1643
  requestRemoteWalletBootstrapImpl = requestRemoteWalletBootstrap,
1531
1644
  runPreflightChecksImpl = runPreflightChecks,
1532
1645
  detectInstalledHostsImpl = detectInstalledHosts,
1646
+ detectDeploymentAuthModeImpl = detectDeploymentAuthMode,
1533
1647
  runLoginImpl = runLogin,
1534
1648
  readSavedSessionImpl = readSavedSession,
1535
1649
  runWalletCliImpl = runWalletCli
@@ -1543,6 +1657,11 @@ export async function runOnboard({
1543
1657
  const showSteps = args.format !== "json";
1544
1658
  const totalSteps = args.preflightOnly ? 4 : 6;
1545
1659
  let step = 1;
1660
+ let onboardingState = ONBOARDING_STATES.INIT;
1661
+ const advanceOnboardingState = (event) => {
1662
+ onboardingState = transitionOnboardingState({ state: onboardingState, event });
1663
+ return onboardingState;
1664
+ };
1546
1665
 
1547
1666
  if (showSteps) printStep(stdout, step, totalSteps, "Resolve setup configuration");
1548
1667
  const config = await resolveRuntimeConfig({
@@ -1551,10 +1670,12 @@ export async function runOnboard({
1551
1670
  stdin,
1552
1671
  stdout,
1553
1672
  detectInstalledHostsImpl,
1673
+ detectDeploymentAuthModeImpl,
1554
1674
  fetchImpl,
1555
1675
  runLoginImpl,
1556
1676
  readSavedSessionImpl
1557
1677
  });
1678
+ advanceOnboardingState(ONBOARDING_EVENTS.RESOLVE_CONFIG_OK);
1558
1679
  step += 1;
1559
1680
  const normalizedBaseUrl = normalizeHttpUrl(mustString(config.baseUrl, "SETTLD_BASE_URL / --base-url"));
1560
1681
  if (!normalizedBaseUrl) throw new Error(`invalid Settld base URL: ${config.baseUrl}`);
@@ -1574,6 +1695,7 @@ export async function runOnboard({
1574
1695
  });
1575
1696
  settldApiKey = mustString(runtimeBootstrapEnv?.SETTLD_API_KEY ?? "", "runtime bootstrap SETTLD_API_KEY");
1576
1697
  }
1698
+ advanceOnboardingState(ONBOARDING_EVENTS.RUNTIME_KEY_OK);
1577
1699
  const runtimeBootstrapOptionalEnv = {};
1578
1700
  if (runtimeBootstrapEnv?.SETTLD_PAID_TOOLS_BASE_URL) {
1579
1701
  runtimeBootstrapOptionalEnv.SETTLD_PAID_TOOLS_BASE_URL = String(runtimeBootstrapEnv.SETTLD_PAID_TOOLS_BASE_URL);
@@ -1607,6 +1729,8 @@ export async function runOnboard({
1607
1729
  baseUrl: normalizedBaseUrl,
1608
1730
  tenantId,
1609
1731
  settldApiKey,
1732
+ bootstrapApiKey: config.bootstrapApiKey,
1733
+ sessionCookie: config.sessionCookie,
1610
1734
  walletProvider: config.walletProvider,
1611
1735
  circleMode: config.circleMode,
1612
1736
  circleBaseUrl: config.circleBaseUrl || null,
@@ -1623,6 +1747,7 @@ export async function runOnboard({
1623
1747
  runtimeEnv
1624
1748
  });
1625
1749
  }
1750
+ advanceOnboardingState(ONBOARDING_EVENTS.WALLET_OK);
1626
1751
  step += 1;
1627
1752
 
1628
1753
  let preflight = { ok: false, skipped: true, checks: [] };
@@ -1643,9 +1768,11 @@ export async function runOnboard({
1643
1768
  } else {
1644
1769
  if (showSteps) printStep(stdout, step, totalSteps, "Skip preflight checks");
1645
1770
  }
1771
+ advanceOnboardingState(ONBOARDING_EVENTS.PREFLIGHT_OK);
1646
1772
  step += 1;
1647
1773
 
1648
1774
  if (args.preflightOnly) {
1775
+ advanceOnboardingState(ONBOARDING_EVENTS.COMPLETE);
1649
1776
  if (showSteps) printStep(stdout, step, totalSteps, "Finalize preflight-only output");
1650
1777
  const payload = {
1651
1778
  ok: true,
@@ -1658,14 +1785,19 @@ export async function runOnboard({
1658
1785
  details: wallet && typeof wallet === "object" ? wallet : null
1659
1786
  },
1660
1787
  settld: {
1661
- baseUrl: normalizedBaseUrl,
1662
- tenantId,
1663
- preflight: Boolean(config.preflight),
1664
- smoke: false,
1665
- profileApplied: false,
1788
+ baseUrl: normalizedBaseUrl,
1789
+ tenantId,
1790
+ authMode: config.authMode ?? "unknown",
1791
+ preflight: Boolean(config.preflight),
1792
+ smoke: false,
1793
+ profileApplied: false,
1666
1794
  profileId: null
1667
1795
  },
1668
1796
  preflight,
1797
+ onboarding: {
1798
+ schemaVersion: "SettldOnboardingState.v1",
1799
+ state: onboardingState
1800
+ },
1669
1801
  hostInstallDetected: Array.isArray(config.installedHosts) && config.installedHosts.includes(config.host),
1670
1802
  installedHosts: config.installedHosts,
1671
1803
  env: {
@@ -1738,6 +1870,7 @@ export async function runOnboard({
1738
1870
  ...walletEnv
1739
1871
  }
1740
1872
  });
1873
+ advanceOnboardingState(ONBOARDING_EVENTS.HOST_CONFIG_OK);
1741
1874
  step += 1;
1742
1875
 
1743
1876
  const mergedEnv = {
@@ -1765,9 +1898,11 @@ export async function runOnboard({
1765
1898
  stdout,
1766
1899
  runWalletCliImpl
1767
1900
  });
1901
+ advanceOnboardingState(ONBOARDING_EVENTS.GUIDED_OK);
1768
1902
  step += 1;
1769
1903
 
1770
1904
  if (showSteps) printStep(stdout, step, totalSteps, "Finalize output");
1905
+ advanceOnboardingState(ONBOARDING_EVENTS.COMPLETE);
1771
1906
  const payload = {
1772
1907
  ok: true,
1773
1908
  setupMode: config.setupMode,
@@ -1782,12 +1917,17 @@ export async function runOnboard({
1782
1917
  settld: {
1783
1918
  baseUrl: normalizedBaseUrl,
1784
1919
  tenantId,
1920
+ authMode: config.authMode ?? "unknown",
1785
1921
  preflight: Boolean(config.preflight),
1786
1922
  smoke: Boolean(config.smoke),
1787
1923
  profileApplied: !config.skipProfileApply,
1788
1924
  profileId: config.skipProfileApply ? null : (config.profileId || "engineering-spend")
1789
1925
  },
1790
1926
  preflight,
1927
+ onboarding: {
1928
+ schemaVersion: "SettldOnboardingState.v1",
1929
+ state: onboardingState
1930
+ },
1791
1931
  hostInstallDetected: Array.isArray(config.installedHosts) && config.installedHosts.includes(config.host),
1792
1932
  installedHosts: config.installedHosts,
1793
1933
  env: mergedEnv,
@@ -1803,6 +1943,7 @@ export async function runOnboard({
1803
1943
  lines.push("Settld onboard complete.");
1804
1944
  lines.push(`Host: ${config.host}`);
1805
1945
  lines.push(`Settld: ${normalizedBaseUrl} (tenant=${tenantId})`);
1946
+ lines.push(`Auth mode: ${config.authMode ?? "unknown"}`);
1806
1947
  lines.push(`Setup mode: ${config.setupMode}`);
1807
1948
  lines.push(`Preflight: ${config.preflight ? "passed" : "skipped"}`);
1808
1949
  lines.push(`Wallet mode: ${config.walletMode}`);
@@ -1852,7 +1993,11 @@ async function main(argv = process.argv.slice(2)) {
1852
1993
  try {
1853
1994
  await runOnboard({ argv });
1854
1995
  } catch (err) {
1855
- process.stderr.write(`${err?.message ?? String(err)}\n`);
1996
+ const failure = classifyOnboardingFailure(err);
1997
+ process.stderr.write(`[${failure.code}] ${failure.message}\n`);
1998
+ if (failure.remediation) {
1999
+ process.stderr.write(`Remediation: ${failure.remediation}\n`);
2000
+ }
1856
2001
  process.exit(1);
1857
2002
  }
1858
2003
  }
@@ -0,0 +1,107 @@
1
+ const FAILURE_CLASSES = Object.freeze([
2
+ {
3
+ code: "ONBOARDING_AUTH_PUBLIC_SIGNUP_UNAVAILABLE",
4
+ phase: "auth",
5
+ patterns: [/Public signup is unavailable/i, /Public signup is disabled/i],
6
+ remediation:
7
+ "Use `Generate during setup` with an onboarding bootstrap API key, or rerun with `--tenant-id <existing_tenant>`."
8
+ },
9
+ {
10
+ code: "ONBOARDING_AUTH_LOGIN_UNAVAILABLE",
11
+ phase: "auth",
12
+ patterns: [/OTP login is unavailable on this base URL/i, /otp request failed \(403\)/i, /login failed \(403\): forbidden/i],
13
+ remediation:
14
+ "This base URL does not expose public OTP login. Use `Generate during setup` with an onboarding bootstrap API key, or use `Paste existing key`."
15
+ },
16
+ {
17
+ code: "ONBOARDING_AUTH_OTP_INVALID",
18
+ phase: "auth",
19
+ patterns: [/OTP_(INVALID|EXPIRED|CONSUMED|MISSING)/i, /otp code is required/i, /invalid otp/i],
20
+ remediation: "Request a fresh OTP and retry `settld login`."
21
+ },
22
+ {
23
+ code: "ONBOARDING_AUTH_TENANT_DISABLED",
24
+ phase: "auth",
25
+ patterns: [
26
+ /buyer OTP login is not enabled for this tenant/i,
27
+ /BUYER_AUTH_DISABLED/i,
28
+ /Saved login session was rejected for this tenant/i
29
+ ],
30
+ remediation:
31
+ "Rerun `settld login` without `--tenant-id` to create a fresh tenant, or choose `Generate during setup` / `Paste existing key`."
32
+ },
33
+ {
34
+ code: "ONBOARDING_BOOTSTRAP_FORBIDDEN",
35
+ phase: "bootstrap",
36
+ patterns: [/runtime bootstrap request failed \(403\)/i],
37
+ remediation: "Check onboarding bootstrap API key scopes and tenant binding, then rerun setup."
38
+ },
39
+ {
40
+ code: "ONBOARDING_BOOTSTRAP_UNAUTHORIZED",
41
+ phase: "bootstrap",
42
+ patterns: [/runtime bootstrap request failed \(401\)/i, /unauthorized/i],
43
+ remediation: "Verify API key validity and retry with a fresh key/session."
44
+ },
45
+ {
46
+ code: "ONBOARDING_WALLET_BOOTSTRAP_FAILED",
47
+ phase: "wallet",
48
+ patterns: [/remote wallet bootstrap failed/i, /wallet bootstrap/i],
49
+ remediation: "Switch wallet mode to `none` to finish trust wiring, then run `settld wallet status` and retry funding."
50
+ },
51
+ {
52
+ code: "ONBOARDING_BYO_ENV_MISSING",
53
+ phase: "wallet",
54
+ patterns: [/BYO wallet mode missing required env keys/i],
55
+ remediation: "Provide the missing `--wallet-env KEY=VALUE` entries or export required Circle env vars."
56
+ },
57
+ {
58
+ code: "ONBOARDING_HOST_WRITE_FAILED",
59
+ phase: "host",
60
+ patterns: [/host config/i, /path not writable/i],
61
+ remediation: "Use `--dry-run` to inspect target path, then rerun with a writable host config location."
62
+ },
63
+ {
64
+ code: "ONBOARDING_PREFLIGHT_FAILED",
65
+ phase: "preflight",
66
+ patterns: [/preflight failed/i],
67
+ remediation: "Run with `--preflight` and fix the reported failing check before rerunning setup."
68
+ }
69
+ ]);
70
+
71
+ export function classifyOnboardingFailure(error) {
72
+ const message = String(error?.message ?? error ?? "").trim();
73
+ if (!message) {
74
+ return {
75
+ code: "ONBOARDING_UNKNOWN_FAILURE",
76
+ phase: "unknown",
77
+ message: "unknown onboarding failure",
78
+ remediation: "Retry setup with `--format json` and inspect the report output."
79
+ };
80
+ }
81
+
82
+ for (const failureClass of FAILURE_CLASSES) {
83
+ if (failureClass.patterns.some((pattern) => pattern.test(message))) {
84
+ return {
85
+ code: failureClass.code,
86
+ phase: failureClass.phase,
87
+ message,
88
+ remediation: failureClass.remediation
89
+ };
90
+ }
91
+ }
92
+
93
+ return {
94
+ code: "ONBOARDING_UNKNOWN_FAILURE",
95
+ phase: "unknown",
96
+ message,
97
+ remediation: "Retry setup with `--format json` and inspect the report output."
98
+ };
99
+ }
100
+
101
+ export function listOnboardingFailureClasses() {
102
+ return FAILURE_CLASSES.map((item) => ({
103
+ code: item.code,
104
+ phase: item.phase,
105
+ remediation: item.remediation
106
+ }));
107
+ }
@@ -0,0 +1,102 @@
1
+ export const ONBOARDING_STATES = Object.freeze({
2
+ INIT: "init",
3
+ CONFIG_RESOLVED: "config_resolved",
4
+ RUNTIME_KEY_READY: "runtime_key_ready",
5
+ WALLET_RESOLVED: "wallet_resolved",
6
+ PREFLIGHT_DONE: "preflight_done",
7
+ HOST_CONFIGURED: "host_configured",
8
+ GUIDED_NEXT_DONE: "guided_next_done",
9
+ COMPLETED: "completed",
10
+ FAILED: "failed"
11
+ });
12
+
13
+ export const ONBOARDING_EVENTS = Object.freeze({
14
+ RESOLVE_CONFIG_OK: "resolve_config_ok",
15
+ RESOLVE_CONFIG_FAILED: "resolve_config_failed",
16
+ RUNTIME_KEY_OK: "runtime_key_ok",
17
+ RUNTIME_KEY_FAILED: "runtime_key_failed",
18
+ WALLET_OK: "wallet_ok",
19
+ WALLET_FAILED: "wallet_failed",
20
+ PREFLIGHT_OK: "preflight_ok",
21
+ PREFLIGHT_FAILED: "preflight_failed",
22
+ HOST_CONFIG_OK: "host_config_ok",
23
+ HOST_CONFIG_FAILED: "host_config_failed",
24
+ GUIDED_OK: "guided_ok",
25
+ GUIDED_FAILED: "guided_failed",
26
+ COMPLETE: "complete",
27
+ FATAL: "fatal"
28
+ });
29
+
30
+ const TRANSITIONS = Object.freeze({
31
+ [ONBOARDING_STATES.INIT]: Object.freeze({
32
+ [ONBOARDING_EVENTS.RESOLVE_CONFIG_OK]: ONBOARDING_STATES.CONFIG_RESOLVED,
33
+ [ONBOARDING_EVENTS.RESOLVE_CONFIG_FAILED]: ONBOARDING_STATES.FAILED
34
+ }),
35
+ [ONBOARDING_STATES.CONFIG_RESOLVED]: Object.freeze({
36
+ [ONBOARDING_EVENTS.RUNTIME_KEY_OK]: ONBOARDING_STATES.RUNTIME_KEY_READY,
37
+ [ONBOARDING_EVENTS.RUNTIME_KEY_FAILED]: ONBOARDING_STATES.FAILED
38
+ }),
39
+ [ONBOARDING_STATES.RUNTIME_KEY_READY]: Object.freeze({
40
+ [ONBOARDING_EVENTS.WALLET_OK]: ONBOARDING_STATES.WALLET_RESOLVED,
41
+ [ONBOARDING_EVENTS.WALLET_FAILED]: ONBOARDING_STATES.FAILED
42
+ }),
43
+ [ONBOARDING_STATES.WALLET_RESOLVED]: Object.freeze({
44
+ [ONBOARDING_EVENTS.PREFLIGHT_OK]: ONBOARDING_STATES.PREFLIGHT_DONE,
45
+ [ONBOARDING_EVENTS.PREFLIGHT_FAILED]: ONBOARDING_STATES.FAILED
46
+ }),
47
+ [ONBOARDING_STATES.PREFLIGHT_DONE]: Object.freeze({
48
+ [ONBOARDING_EVENTS.COMPLETE]: ONBOARDING_STATES.COMPLETED,
49
+ [ONBOARDING_EVENTS.HOST_CONFIG_OK]: ONBOARDING_STATES.HOST_CONFIGURED,
50
+ [ONBOARDING_EVENTS.HOST_CONFIG_FAILED]: ONBOARDING_STATES.FAILED
51
+ }),
52
+ [ONBOARDING_STATES.HOST_CONFIGURED]: Object.freeze({
53
+ [ONBOARDING_EVENTS.GUIDED_OK]: ONBOARDING_STATES.GUIDED_NEXT_DONE,
54
+ [ONBOARDING_EVENTS.GUIDED_FAILED]: ONBOARDING_STATES.FAILED
55
+ }),
56
+ [ONBOARDING_STATES.GUIDED_NEXT_DONE]: Object.freeze({
57
+ [ONBOARDING_EVENTS.COMPLETE]: ONBOARDING_STATES.COMPLETED,
58
+ [ONBOARDING_EVENTS.FATAL]: ONBOARDING_STATES.FAILED
59
+ }),
60
+ [ONBOARDING_STATES.COMPLETED]: Object.freeze({}),
61
+ [ONBOARDING_STATES.FAILED]: Object.freeze({})
62
+ });
63
+
64
+ function knownStatesList() {
65
+ return Object.values(ONBOARDING_STATES).join(", ");
66
+ }
67
+
68
+ function knownEventsList() {
69
+ return Object.values(ONBOARDING_EVENTS).join(", ");
70
+ }
71
+
72
+ export function transitionOnboardingState({ state, event }) {
73
+ const current = String(state ?? "").trim();
74
+ const nextEvent = String(event ?? "").trim();
75
+ const stateTransitions = TRANSITIONS[current];
76
+ if (!stateTransitions) {
77
+ const err = new Error(`unknown onboarding state: ${current}. expected one of: ${knownStatesList()}`);
78
+ err.code = "ONBOARDING_UNKNOWN_STATE";
79
+ throw err;
80
+ }
81
+ if (!nextEvent || !Object.prototype.hasOwnProperty.call(stateTransitions, nextEvent)) {
82
+ const err = new Error(
83
+ `invalid onboarding transition: state=${current} event=${nextEvent || "<empty>"}. allowed events: ${Object.keys(stateTransitions).join(", ") || "<none>"}`
84
+ );
85
+ err.code = "ONBOARDING_INVALID_TRANSITION";
86
+ throw err;
87
+ }
88
+ return stateTransitions[nextEvent];
89
+ }
90
+
91
+ export function assertOnboardingTransitionSequence(events = []) {
92
+ if (!Array.isArray(events)) {
93
+ const err = new Error(`onboarding events must be an array. expected event names: ${knownEventsList()}`);
94
+ err.code = "ONBOARDING_INVALID_SEQUENCE";
95
+ throw err;
96
+ }
97
+ let state = ONBOARDING_STATES.INIT;
98
+ for (const event of events) {
99
+ state = transitionOnboardingState({ state, event });
100
+ }
101
+ return state;
102
+ }