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.
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 +111 -17
  9. package/scripts/setup/onboard.mjs +176 -40
  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
@@ -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";
@@ -1136,6 +1139,8 @@ async function requestRemoteWalletBootstrap({
1136
1139
  baseUrl,
1137
1140
  tenantId,
1138
1141
  settldApiKey,
1142
+ bootstrapApiKey,
1143
+ sessionCookie,
1139
1144
  walletProvider,
1140
1145
  circleMode,
1141
1146
  circleBaseUrl,
@@ -1157,30 +1162,69 @@ async function requestRemoteWalletBootstrap({
1157
1162
  }
1158
1163
 
1159
1164
  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
- });
1165
+ const candidates = [];
1166
+ const runtimeKey = String(settldApiKey ?? "").trim();
1167
+ const bootstrapKey = String(bootstrapApiKey ?? "").trim();
1168
+ const cookie = String(sessionCookie ?? "").trim();
1169
+ if (runtimeKey) candidates.push({ kind: "runtime_key", apiKey: runtimeKey });
1170
+ if (bootstrapKey && bootstrapKey !== runtimeKey) candidates.push({ kind: "bootstrap_key", apiKey: bootstrapKey });
1171
+ if (cookie) candidates.push({ kind: "session_cookie", cookie });
1172
+ if (!candidates.length) {
1173
+ throw new Error("remote wallet bootstrap requires SETTLD_API_KEY, bootstrap API key, or saved login session");
1174
+ }
1168
1175
 
1169
- const text = await res.text();
1176
+ let lastError = null;
1177
+ let succeeded = false;
1170
1178
  let json = null;
1171
- try {
1172
- json = text ? JSON.parse(text) : null;
1173
- } catch {
1179
+ for (const candidate of candidates) {
1180
+ const headers = {
1181
+ "content-type": "application/json"
1182
+ };
1183
+ if (candidate.apiKey) headers["x-api-key"] = candidate.apiKey;
1184
+ if (candidate.cookie) headers.cookie = candidate.cookie;
1185
+
1186
+ const res = await fetchImpl(url.toString(), {
1187
+ method: "POST",
1188
+ headers,
1189
+ body: JSON.stringify(body)
1190
+ });
1191
+
1192
+ const text = await res.text();
1174
1193
  json = null;
1175
- }
1176
- if (!res.ok) {
1194
+ try {
1195
+ json = text ? JSON.parse(text) : null;
1196
+ } catch {
1197
+ json = null;
1198
+ }
1199
+ if (res.ok) {
1200
+ succeeded = true;
1201
+ break;
1202
+ }
1177
1203
  const message =
1178
1204
  json && typeof json === "object"
1179
1205
  ? json?.message ?? json?.error ?? `HTTP ${res.status}`
1180
1206
  : text || `HTTP ${res.status}`;
1181
- throw new Error(`remote wallet bootstrap failed (${res.status}): ${String(message)}`);
1207
+ lastError = { status: res.status, message: String(message), kind: candidate.kind };
1208
+ if (res.status !== 403) {
1209
+ throw new Error(`remote wallet bootstrap failed (${res.status}): ${String(message)}`);
1210
+ }
1182
1211
  }
1183
- const bootstrap = json?.walletBootstrap;
1212
+ if (!succeeded) {
1213
+ const detail = lastError ? ` (${lastError.kind})` : "";
1214
+ if (lastError && lastError.status === 403) {
1215
+ throw new Error(
1216
+ `remote wallet bootstrap failed (403): forbidden${detail}. Try --wallet-mode none (finish trust wiring) or --wallet-bootstrap local with --circle-api-key.`
1217
+ );
1218
+ }
1219
+ if (lastError) {
1220
+ throw new Error(`remote wallet bootstrap failed (${lastError.status}): ${lastError.message}${detail}`);
1221
+ }
1222
+ throw new Error("remote wallet bootstrap failed: unknown error");
1223
+ }
1224
+ if (!json || typeof json !== "object" || Array.isArray(json)) {
1225
+ throw new Error("remote wallet bootstrap response missing payload");
1226
+ }
1227
+ const bootstrap = json.walletBootstrap;
1184
1228
  if (!bootstrap || typeof bootstrap !== "object" || Array.isArray(bootstrap)) {
1185
1229
  throw new Error("remote wallet bootstrap response missing walletBootstrap object");
1186
1230
  }
@@ -1196,6 +1240,7 @@ async function resolveRuntimeConfig({
1196
1240
  stdin = process.stdin,
1197
1241
  stdout = process.stdout,
1198
1242
  detectInstalledHostsImpl = detectInstalledHosts,
1243
+ detectDeploymentAuthModeImpl = detectDeploymentAuthMode,
1199
1244
  fetchImpl = fetch,
1200
1245
  runLoginImpl = runLogin,
1201
1246
  readSavedSessionImpl = readSavedSession
@@ -1234,6 +1279,7 @@ async function resolveRuntimeConfig({
1234
1279
  preflight: Boolean(args.preflight),
1235
1280
  smoke: Boolean(args.smoke),
1236
1281
  dryRun: Boolean(args.dryRun),
1282
+ authMode: "unknown",
1237
1283
  installedHosts
1238
1284
  };
1239
1285
  if (savedSession) {
@@ -1327,21 +1373,48 @@ async function resolveRuntimeConfig({
1327
1373
  );
1328
1374
 
1329
1375
  if (!out.baseUrl) out.baseUrl = DEFAULT_PUBLIC_BASE_URL;
1376
+ if (out.baseUrl) {
1377
+ try {
1378
+ const authDiscovery = await detectDeploymentAuthModeImpl({ baseUrl: out.baseUrl, fetchImpl });
1379
+ out.authMode = String(authDiscovery?.mode ?? "unknown");
1380
+ if (out.authMode !== "unknown") {
1381
+ stdout.write(`Detected auth mode: ${out.authMode}\n`);
1382
+ }
1383
+ } catch {
1384
+ out.authMode = "unknown";
1385
+ }
1386
+ }
1387
+ if (out.authMode === "enterprise_preprovisioned" && !out.tenantId && !savedSession?.tenantId) {
1388
+ out.tenantId = await promptLine(rl, "Tenant ID (required for this deployment mode)", { required: true });
1389
+ }
1330
1390
  if (!out.settldApiKey) {
1391
+ let loginUnavailableForRun = false;
1392
+ let preferredKeyMode = null;
1393
+ let keyModeAttempts = 0;
1331
1394
  while (!out.settldApiKey) {
1395
+ keyModeAttempts += 1;
1396
+ if (keyModeAttempts > MAX_INTERACTIVE_API_KEY_MODE_ATTEMPTS) {
1397
+ const error = new Error(
1398
+ `setup API key flow exceeded ${MAX_INTERACTIVE_API_KEY_MODE_ATTEMPTS} attempts; choose \`Generate during setup\` or \`Paste existing key\``
1399
+ );
1400
+ error.code = "ONBOARDING_KEY_MODE_LOOP_GUARD";
1401
+ throw error;
1402
+ }
1332
1403
  const canUseSavedSession =
1333
1404
  Boolean(out.sessionCookie) &&
1334
1405
  (!savedSession ||
1335
1406
  (normalizeHttpUrl(out.baseUrl) === normalizeHttpUrl(savedSession?.baseUrl) &&
1336
1407
  (!out.tenantId || String(out.tenantId ?? "").trim() === String(savedSession?.tenantId ?? "").trim())));
1337
1408
  const keyOptions = [];
1409
+ const loginAllowedForMode =
1410
+ out.authMode !== "enterprise_preprovisioned" || Boolean(String(out.tenantId ?? "").trim());
1338
1411
  if (canUseSavedSession) {
1339
1412
  keyOptions.push({
1340
1413
  value: "session",
1341
1414
  label: "Use saved login session",
1342
1415
  hint: `Reuse ${out.sessionFile} to mint runtime key`
1343
1416
  });
1344
- } else {
1417
+ } else if (!loginUnavailableForRun && loginAllowedForMode) {
1345
1418
  keyOptions.push({
1346
1419
  value: "login",
1347
1420
  label: "Login / create tenant (recommended)",
@@ -1352,34 +1425,64 @@ async function resolveRuntimeConfig({
1352
1425
  { value: "bootstrap", label: "Generate during setup", hint: "Use onboarding bootstrap API key" },
1353
1426
  { value: "manual", label: "Paste existing key", hint: "Use an existing tenant API key" }
1354
1427
  );
1428
+ const defaultKeyMode =
1429
+ preferredKeyMode && keyOptions.some((option) => option.value === preferredKeyMode)
1430
+ ? preferredKeyMode
1431
+ : canUseSavedSession
1432
+ ? "session"
1433
+ : loginUnavailableForRun
1434
+ ? "bootstrap"
1435
+ : loginAllowedForMode
1436
+ ? "login"
1437
+ : "bootstrap";
1355
1438
  const keyMode = await promptSelect(
1356
1439
  rl,
1357
1440
  stdin,
1358
1441
  stdout,
1359
1442
  "How should setup get your Settld API key?",
1360
1443
  keyOptions,
1361
- { defaultValue: canUseSavedSession ? "session" : "login", color }
1444
+ { defaultValue: defaultKeyMode, color }
1362
1445
  );
1363
1446
  if (keyMode === "login") {
1364
- await runLoginImpl({
1365
- argv: ["--base-url", out.baseUrl, "--session-file", out.sessionFile],
1366
- stdin,
1367
- stdout,
1368
- fetchImpl
1369
- });
1370
- const refreshedSession = await readSavedSessionImpl({ sessionPath: out.sessionFile });
1371
- if (!refreshedSession) throw new Error("login did not produce a saved session");
1372
- out.baseUrl = String(refreshedSession.baseUrl ?? out.baseUrl).trim() || out.baseUrl;
1373
- out.tenantId = String(refreshedSession.tenantId ?? out.tenantId).trim();
1374
- out.sessionCookie = String(refreshedSession.cookie ?? out.sessionCookie).trim();
1375
- if (savedSession) {
1376
- savedSession.baseUrl = refreshedSession.baseUrl;
1377
- savedSession.tenantId = refreshedSession.tenantId;
1378
- savedSession.cookie = refreshedSession.cookie;
1447
+ if (!loginAllowedForMode) {
1448
+ throw new Error(
1449
+ "login flow requires --tenant-id in enterprise_preprovisioned mode. Provide tenant ID or choose bootstrap/manual API key mode."
1450
+ );
1451
+ }
1452
+ try {
1453
+ const loginArgv = ["--base-url", out.baseUrl, "--session-file", out.sessionFile];
1454
+ if (out.tenantId) loginArgv.push("--tenant-id", out.tenantId);
1455
+ await runLoginImpl({
1456
+ argv: loginArgv,
1457
+ stdin,
1458
+ stdout,
1459
+ fetchImpl
1460
+ });
1461
+ const refreshedSession = await readSavedSessionImpl({ sessionPath: out.sessionFile });
1462
+ if (!refreshedSession) throw new Error("login did not produce a saved session");
1463
+ out.baseUrl = String(refreshedSession.baseUrl ?? out.baseUrl).trim() || out.baseUrl;
1464
+ out.tenantId = String(refreshedSession.tenantId ?? out.tenantId).trim();
1465
+ out.sessionCookie = String(refreshedSession.cookie ?? out.sessionCookie).trim();
1466
+ if (savedSession) {
1467
+ savedSession.baseUrl = refreshedSession.baseUrl;
1468
+ savedSession.tenantId = refreshedSession.tenantId;
1469
+ savedSession.cookie = refreshedSession.cookie;
1470
+ }
1471
+ preferredKeyMode = "session";
1472
+ } catch (err) {
1473
+ const failure = classifyOnboardingFailure(err);
1474
+ stdout.write(`[${failure.code}] ${failure.message}\n`);
1475
+ if (failure.remediation) stdout.write(`Remediation: ${failure.remediation}\n`);
1476
+ if (failure.code === "ONBOARDING_AUTH_PUBLIC_SIGNUP_UNAVAILABLE" || failure.code === "ONBOARDING_AUTH_LOGIN_UNAVAILABLE") {
1477
+ loginUnavailableForRun = true;
1478
+ stdout.write("Login/signup has been disabled for this setup run. Continuing with API key modes.\n");
1479
+ }
1480
+ preferredKeyMode = "bootstrap";
1379
1481
  }
1380
1482
  continue;
1381
1483
  }
1382
1484
  if (keyMode === "bootstrap") {
1485
+ preferredKeyMode = "bootstrap";
1383
1486
  if (!out.bootstrapApiKey) {
1384
1487
  out.bootstrapApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Onboarding bootstrap API key");
1385
1488
  }
@@ -1392,6 +1495,7 @@ async function resolveRuntimeConfig({
1392
1495
  break;
1393
1496
  }
1394
1497
  if (keyMode === "manual") {
1498
+ preferredKeyMode = "manual";
1395
1499
  out.settldApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Settld API key");
1396
1500
  break;
1397
1501
  }
@@ -1525,6 +1629,7 @@ export async function runOnboard({
1525
1629
  requestRemoteWalletBootstrapImpl = requestRemoteWalletBootstrap,
1526
1630
  runPreflightChecksImpl = runPreflightChecks,
1527
1631
  detectInstalledHostsImpl = detectInstalledHosts,
1632
+ detectDeploymentAuthModeImpl = detectDeploymentAuthMode,
1528
1633
  runLoginImpl = runLogin,
1529
1634
  readSavedSessionImpl = readSavedSession,
1530
1635
  runWalletCliImpl = runWalletCli
@@ -1538,6 +1643,11 @@ export async function runOnboard({
1538
1643
  const showSteps = args.format !== "json";
1539
1644
  const totalSteps = args.preflightOnly ? 4 : 6;
1540
1645
  let step = 1;
1646
+ let onboardingState = ONBOARDING_STATES.INIT;
1647
+ const advanceOnboardingState = (event) => {
1648
+ onboardingState = transitionOnboardingState({ state: onboardingState, event });
1649
+ return onboardingState;
1650
+ };
1541
1651
 
1542
1652
  if (showSteps) printStep(stdout, step, totalSteps, "Resolve setup configuration");
1543
1653
  const config = await resolveRuntimeConfig({
@@ -1546,10 +1656,12 @@ export async function runOnboard({
1546
1656
  stdin,
1547
1657
  stdout,
1548
1658
  detectInstalledHostsImpl,
1659
+ detectDeploymentAuthModeImpl,
1549
1660
  fetchImpl,
1550
1661
  runLoginImpl,
1551
1662
  readSavedSessionImpl
1552
1663
  });
1664
+ advanceOnboardingState(ONBOARDING_EVENTS.RESOLVE_CONFIG_OK);
1553
1665
  step += 1;
1554
1666
  const normalizedBaseUrl = normalizeHttpUrl(mustString(config.baseUrl, "SETTLD_BASE_URL / --base-url"));
1555
1667
  if (!normalizedBaseUrl) throw new Error(`invalid Settld base URL: ${config.baseUrl}`);
@@ -1569,6 +1681,7 @@ export async function runOnboard({
1569
1681
  });
1570
1682
  settldApiKey = mustString(runtimeBootstrapEnv?.SETTLD_API_KEY ?? "", "runtime bootstrap SETTLD_API_KEY");
1571
1683
  }
1684
+ advanceOnboardingState(ONBOARDING_EVENTS.RUNTIME_KEY_OK);
1572
1685
  const runtimeBootstrapOptionalEnv = {};
1573
1686
  if (runtimeBootstrapEnv?.SETTLD_PAID_TOOLS_BASE_URL) {
1574
1687
  runtimeBootstrapOptionalEnv.SETTLD_PAID_TOOLS_BASE_URL = String(runtimeBootstrapEnv.SETTLD_PAID_TOOLS_BASE_URL);
@@ -1602,6 +1715,8 @@ export async function runOnboard({
1602
1715
  baseUrl: normalizedBaseUrl,
1603
1716
  tenantId,
1604
1717
  settldApiKey,
1718
+ bootstrapApiKey: config.bootstrapApiKey,
1719
+ sessionCookie: config.sessionCookie,
1605
1720
  walletProvider: config.walletProvider,
1606
1721
  circleMode: config.circleMode,
1607
1722
  circleBaseUrl: config.circleBaseUrl || null,
@@ -1618,6 +1733,7 @@ export async function runOnboard({
1618
1733
  runtimeEnv
1619
1734
  });
1620
1735
  }
1736
+ advanceOnboardingState(ONBOARDING_EVENTS.WALLET_OK);
1621
1737
  step += 1;
1622
1738
 
1623
1739
  let preflight = { ok: false, skipped: true, checks: [] };
@@ -1638,9 +1754,11 @@ export async function runOnboard({
1638
1754
  } else {
1639
1755
  if (showSteps) printStep(stdout, step, totalSteps, "Skip preflight checks");
1640
1756
  }
1757
+ advanceOnboardingState(ONBOARDING_EVENTS.PREFLIGHT_OK);
1641
1758
  step += 1;
1642
1759
 
1643
1760
  if (args.preflightOnly) {
1761
+ advanceOnboardingState(ONBOARDING_EVENTS.COMPLETE);
1644
1762
  if (showSteps) printStep(stdout, step, totalSteps, "Finalize preflight-only output");
1645
1763
  const payload = {
1646
1764
  ok: true,
@@ -1653,14 +1771,19 @@ export async function runOnboard({
1653
1771
  details: wallet && typeof wallet === "object" ? wallet : null
1654
1772
  },
1655
1773
  settld: {
1656
- baseUrl: normalizedBaseUrl,
1657
- tenantId,
1658
- preflight: Boolean(config.preflight),
1659
- smoke: false,
1660
- profileApplied: false,
1774
+ baseUrl: normalizedBaseUrl,
1775
+ tenantId,
1776
+ authMode: config.authMode ?? "unknown",
1777
+ preflight: Boolean(config.preflight),
1778
+ smoke: false,
1779
+ profileApplied: false,
1661
1780
  profileId: null
1662
1781
  },
1663
1782
  preflight,
1783
+ onboarding: {
1784
+ schemaVersion: "SettldOnboardingState.v1",
1785
+ state: onboardingState
1786
+ },
1664
1787
  hostInstallDetected: Array.isArray(config.installedHosts) && config.installedHosts.includes(config.host),
1665
1788
  installedHosts: config.installedHosts,
1666
1789
  env: {
@@ -1733,6 +1856,7 @@ export async function runOnboard({
1733
1856
  ...walletEnv
1734
1857
  }
1735
1858
  });
1859
+ advanceOnboardingState(ONBOARDING_EVENTS.HOST_CONFIG_OK);
1736
1860
  step += 1;
1737
1861
 
1738
1862
  const mergedEnv = {
@@ -1760,9 +1884,11 @@ export async function runOnboard({
1760
1884
  stdout,
1761
1885
  runWalletCliImpl
1762
1886
  });
1887
+ advanceOnboardingState(ONBOARDING_EVENTS.GUIDED_OK);
1763
1888
  step += 1;
1764
1889
 
1765
1890
  if (showSteps) printStep(stdout, step, totalSteps, "Finalize output");
1891
+ advanceOnboardingState(ONBOARDING_EVENTS.COMPLETE);
1766
1892
  const payload = {
1767
1893
  ok: true,
1768
1894
  setupMode: config.setupMode,
@@ -1777,12 +1903,17 @@ export async function runOnboard({
1777
1903
  settld: {
1778
1904
  baseUrl: normalizedBaseUrl,
1779
1905
  tenantId,
1906
+ authMode: config.authMode ?? "unknown",
1780
1907
  preflight: Boolean(config.preflight),
1781
1908
  smoke: Boolean(config.smoke),
1782
1909
  profileApplied: !config.skipProfileApply,
1783
1910
  profileId: config.skipProfileApply ? null : (config.profileId || "engineering-spend")
1784
1911
  },
1785
1912
  preflight,
1913
+ onboarding: {
1914
+ schemaVersion: "SettldOnboardingState.v1",
1915
+ state: onboardingState
1916
+ },
1786
1917
  hostInstallDetected: Array.isArray(config.installedHosts) && config.installedHosts.includes(config.host),
1787
1918
  installedHosts: config.installedHosts,
1788
1919
  env: mergedEnv,
@@ -1798,6 +1929,7 @@ export async function runOnboard({
1798
1929
  lines.push("Settld onboard complete.");
1799
1930
  lines.push(`Host: ${config.host}`);
1800
1931
  lines.push(`Settld: ${normalizedBaseUrl} (tenant=${tenantId})`);
1932
+ lines.push(`Auth mode: ${config.authMode ?? "unknown"}`);
1801
1933
  lines.push(`Setup mode: ${config.setupMode}`);
1802
1934
  lines.push(`Preflight: ${config.preflight ? "passed" : "skipped"}`);
1803
1935
  lines.push(`Wallet mode: ${config.walletMode}`);
@@ -1847,7 +1979,11 @@ async function main(argv = process.argv.slice(2)) {
1847
1979
  try {
1848
1980
  await runOnboard({ argv });
1849
1981
  } catch (err) {
1850
- process.stderr.write(`${err?.message ?? String(err)}\n`);
1982
+ const failure = classifyOnboardingFailure(err);
1983
+ process.stderr.write(`[${failure.code}] ${failure.message}\n`);
1984
+ if (failure.remediation) {
1985
+ process.stderr.write(`Remediation: ${failure.remediation}\n`);
1986
+ }
1851
1987
  process.exit(1);
1852
1988
  }
1853
1989
  }
@@ -0,0 +1,96 @@
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_BOOTSTRAP_FORBIDDEN",
24
+ phase: "bootstrap",
25
+ patterns: [/runtime bootstrap request failed \(403\)/i],
26
+ remediation: "Check onboarding bootstrap API key scopes and tenant binding, then rerun setup."
27
+ },
28
+ {
29
+ code: "ONBOARDING_BOOTSTRAP_UNAUTHORIZED",
30
+ phase: "bootstrap",
31
+ patterns: [/runtime bootstrap request failed \(401\)/i, /unauthorized/i],
32
+ remediation: "Verify API key validity and retry with a fresh key/session."
33
+ },
34
+ {
35
+ code: "ONBOARDING_WALLET_BOOTSTRAP_FAILED",
36
+ phase: "wallet",
37
+ patterns: [/remote wallet bootstrap failed/i, /wallet bootstrap/i],
38
+ remediation: "Switch wallet mode to `none` to finish trust wiring, then run `settld wallet status` and retry funding."
39
+ },
40
+ {
41
+ code: "ONBOARDING_BYO_ENV_MISSING",
42
+ phase: "wallet",
43
+ patterns: [/BYO wallet mode missing required env keys/i],
44
+ remediation: "Provide the missing `--wallet-env KEY=VALUE` entries or export required Circle env vars."
45
+ },
46
+ {
47
+ code: "ONBOARDING_HOST_WRITE_FAILED",
48
+ phase: "host",
49
+ patterns: [/host config/i, /path not writable/i],
50
+ remediation: "Use `--dry-run` to inspect target path, then rerun with a writable host config location."
51
+ },
52
+ {
53
+ code: "ONBOARDING_PREFLIGHT_FAILED",
54
+ phase: "preflight",
55
+ patterns: [/preflight failed/i],
56
+ remediation: "Run with `--preflight` and fix the reported failing check before rerunning setup."
57
+ }
58
+ ]);
59
+
60
+ export function classifyOnboardingFailure(error) {
61
+ const message = String(error?.message ?? error ?? "").trim();
62
+ if (!message) {
63
+ return {
64
+ code: "ONBOARDING_UNKNOWN_FAILURE",
65
+ phase: "unknown",
66
+ message: "unknown onboarding failure",
67
+ remediation: "Retry setup with `--format json` and inspect the report output."
68
+ };
69
+ }
70
+
71
+ for (const failureClass of FAILURE_CLASSES) {
72
+ if (failureClass.patterns.some((pattern) => pattern.test(message))) {
73
+ return {
74
+ code: failureClass.code,
75
+ phase: failureClass.phase,
76
+ message,
77
+ remediation: failureClass.remediation
78
+ };
79
+ }
80
+ }
81
+
82
+ return {
83
+ code: "ONBOARDING_UNKNOWN_FAILURE",
84
+ phase: "unknown",
85
+ message,
86
+ remediation: "Retry setup with `--format json` and inspect the report output."
87
+ };
88
+ }
89
+
90
+ export function listOnboardingFailureClasses() {
91
+ return FAILURE_CLASSES.map((item) => ({
92
+ code: item.code,
93
+ phase: item.phase,
94
+ remediation: item.remediation
95
+ }));
96
+ }
@@ -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
+ }