settld 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/docs/CONFIG.md +12 -0
  2. package/docs/README.md +3 -0
  3. package/docs/ops/HOSTED_BASELINE_R2.md +4 -2
  4. package/docs/ops/MINIMUM_PRODUCTION_TOPOLOGY.md +19 -7
  5. package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +8 -3
  6. package/package.json +3 -1
  7. package/scripts/ci/run-public-onboarding-gate.mjs +136 -0
  8. package/scripts/setup/login.mjs +67 -1
  9. package/scripts/setup/onboard.mjs +159 -28
  10. package/scripts/setup/onboarding-failure-taxonomy.mjs +96 -0
  11. package/scripts/setup/onboarding-state-machine.mjs +102 -0
  12. package/services/magic-link/README.md +343 -0
  13. package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_criteria.json +1 -0
  14. package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_evaluation.json +1 -0
  15. package/services/magic-link/assets/samples/closepack/known-bad/attestation/bundle_head_attestation.json +1 -0
  16. package/services/magic-link/assets/samples/closepack/known-bad/evidence/evidence_index.json +1 -0
  17. package/services/magic-link/assets/samples/closepack/known-bad/governance/policy.json +1 -0
  18. package/services/magic-link/assets/samples/closepack/known-bad/governance/revocations.json +1 -0
  19. package/services/magic-link/assets/samples/closepack/known-bad/manifest.json +1 -0
  20. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
  21. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/policy.json +1 -0
  22. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/revocations.json +1 -0
  23. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
  24. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/manifest.json +1 -0
  25. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/metering/metering_report.json +1 -0
  26. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
  27. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
  28. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
  29. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
  30. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
  31. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
  32. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
  33. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
  34. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
  35. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
  36. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
  37. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
  38. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
  39. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
  40. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
  41. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
  42. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
  43. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
  44. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/settld.json +1 -0
  45. package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/verify/verification_report.json +1 -0
  46. package/services/magic-link/assets/samples/closepack/known-bad/settld.json +1 -0
  47. package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_definition.json +1 -0
  48. package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_evaluation.json +1 -0
  49. package/services/magic-link/assets/samples/closepack/known-bad/verify/verification_report.json +1 -0
  50. package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_criteria.json +1 -0
  51. package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_evaluation.json +1 -0
  52. package/services/magic-link/assets/samples/closepack/known-good/attestation/bundle_head_attestation.json +1 -0
  53. package/services/magic-link/assets/samples/closepack/known-good/evidence/evidence_index.json +1 -0
  54. package/services/magic-link/assets/samples/closepack/known-good/governance/policy.json +1 -0
  55. package/services/magic-link/assets/samples/closepack/known-good/governance/revocations.json +1 -0
  56. package/services/magic-link/assets/samples/closepack/known-good/manifest.json +1 -0
  57. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
  58. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/policy.json +1 -0
  59. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/revocations.json +1 -0
  60. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
  61. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/manifest.json +1 -0
  62. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/metering/metering_report.json +1 -0
  63. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
  64. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
  65. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
  66. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
  67. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
  68. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
  69. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
  70. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
  71. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
  72. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
  73. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
  74. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
  75. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
  76. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
  77. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
  78. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
  79. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
  80. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
  81. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/settld.json +1 -0
  82. package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/verify/verification_report.json +1 -0
  83. package/services/magic-link/assets/samples/closepack/known-good/settld.json +1 -0
  84. package/services/magic-link/assets/samples/closepack/known-good/sla/sla_definition.json +1 -0
  85. package/services/magic-link/assets/samples/closepack/known-good/sla/sla_evaluation.json +1 -0
  86. package/services/magic-link/assets/samples/closepack/known-good/verify/verification_report.json +1 -0
  87. package/services/magic-link/assets/samples/trust.json +11 -0
  88. package/services/magic-link/src/audit-log.js +24 -0
  89. package/services/magic-link/src/buyer-auth.js +220 -0
  90. package/services/magic-link/src/buyer-notifications.js +402 -0
  91. package/services/magic-link/src/buyer-users.js +129 -0
  92. package/services/magic-link/src/decision-otp.js +156 -0
  93. package/services/magic-link/src/decisions.js +92 -0
  94. package/services/magic-link/src/ingest-keys.js +137 -0
  95. package/services/magic-link/src/maintenance.js +70 -0
  96. package/services/magic-link/src/onboarding-email-sequence.js +331 -0
  97. package/services/magic-link/src/payment-triggers.js +733 -0
  98. package/services/magic-link/src/pdf.js +149 -0
  99. package/services/magic-link/src/policy.js +69 -0
  100. package/services/magic-link/src/redaction.js +6 -0
  101. package/services/magic-link/src/render-model.js +70 -0
  102. package/services/magic-link/src/retention-gc.js +158 -0
  103. package/services/magic-link/src/run-records.js +496 -0
  104. package/services/magic-link/src/s3.js +171 -0
  105. package/services/magic-link/src/server.js +15788 -0
  106. package/services/magic-link/src/settlement-decisions.js +84 -0
  107. package/services/magic-link/src/smtp.js +202 -0
  108. package/services/magic-link/src/storage-cli.js +88 -0
  109. package/services/magic-link/src/storage-format.js +59 -0
  110. package/services/magic-link/src/tenant-billing.js +115 -0
  111. package/services/magic-link/src/tenant-onboarding.js +467 -0
  112. package/services/magic-link/src/tenant-settings.js +1140 -0
  113. package/services/magic-link/src/usage.js +80 -0
  114. package/services/magic-link/src/verify-queue.js +179 -0
  115. package/services/magic-link/src/verify-worker.js +157 -0
  116. package/services/magic-link/src/webhook-retries.js +542 -0
  117. package/services/magic-link/src/webhooks.js +218 -0
  118. package/src/api/app.js +129 -0
@@ -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,18 +1425,35 @@ 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") {
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
+ }
1364
1452
  try {
1453
+ const loginArgv = ["--base-url", out.baseUrl, "--session-file", out.sessionFile];
1454
+ if (out.tenantId) loginArgv.push("--tenant-id", out.tenantId);
1365
1455
  await runLoginImpl({
1366
- argv: ["--base-url", out.baseUrl, "--session-file", out.sessionFile],
1456
+ argv: loginArgv,
1367
1457
  stdin,
1368
1458
  stdout,
1369
1459
  fetchImpl
@@ -1378,13 +1468,21 @@ async function resolveRuntimeConfig({
1378
1468
  savedSession.tenantId = refreshedSession.tenantId;
1379
1469
  savedSession.cookie = refreshedSession.cookie;
1380
1470
  }
1471
+ preferredKeyMode = "session";
1381
1472
  } 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");
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";
1384
1481
  }
1385
1482
  continue;
1386
1483
  }
1387
1484
  if (keyMode === "bootstrap") {
1485
+ preferredKeyMode = "bootstrap";
1388
1486
  if (!out.bootstrapApiKey) {
1389
1487
  out.bootstrapApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Onboarding bootstrap API key");
1390
1488
  }
@@ -1397,6 +1495,7 @@ async function resolveRuntimeConfig({
1397
1495
  break;
1398
1496
  }
1399
1497
  if (keyMode === "manual") {
1498
+ preferredKeyMode = "manual";
1400
1499
  out.settldApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Settld API key");
1401
1500
  break;
1402
1501
  }
@@ -1530,6 +1629,7 @@ export async function runOnboard({
1530
1629
  requestRemoteWalletBootstrapImpl = requestRemoteWalletBootstrap,
1531
1630
  runPreflightChecksImpl = runPreflightChecks,
1532
1631
  detectInstalledHostsImpl = detectInstalledHosts,
1632
+ detectDeploymentAuthModeImpl = detectDeploymentAuthMode,
1533
1633
  runLoginImpl = runLogin,
1534
1634
  readSavedSessionImpl = readSavedSession,
1535
1635
  runWalletCliImpl = runWalletCli
@@ -1543,6 +1643,11 @@ export async function runOnboard({
1543
1643
  const showSteps = args.format !== "json";
1544
1644
  const totalSteps = args.preflightOnly ? 4 : 6;
1545
1645
  let step = 1;
1646
+ let onboardingState = ONBOARDING_STATES.INIT;
1647
+ const advanceOnboardingState = (event) => {
1648
+ onboardingState = transitionOnboardingState({ state: onboardingState, event });
1649
+ return onboardingState;
1650
+ };
1546
1651
 
1547
1652
  if (showSteps) printStep(stdout, step, totalSteps, "Resolve setup configuration");
1548
1653
  const config = await resolveRuntimeConfig({
@@ -1551,10 +1656,12 @@ export async function runOnboard({
1551
1656
  stdin,
1552
1657
  stdout,
1553
1658
  detectInstalledHostsImpl,
1659
+ detectDeploymentAuthModeImpl,
1554
1660
  fetchImpl,
1555
1661
  runLoginImpl,
1556
1662
  readSavedSessionImpl
1557
1663
  });
1664
+ advanceOnboardingState(ONBOARDING_EVENTS.RESOLVE_CONFIG_OK);
1558
1665
  step += 1;
1559
1666
  const normalizedBaseUrl = normalizeHttpUrl(mustString(config.baseUrl, "SETTLD_BASE_URL / --base-url"));
1560
1667
  if (!normalizedBaseUrl) throw new Error(`invalid Settld base URL: ${config.baseUrl}`);
@@ -1574,6 +1681,7 @@ export async function runOnboard({
1574
1681
  });
1575
1682
  settldApiKey = mustString(runtimeBootstrapEnv?.SETTLD_API_KEY ?? "", "runtime bootstrap SETTLD_API_KEY");
1576
1683
  }
1684
+ advanceOnboardingState(ONBOARDING_EVENTS.RUNTIME_KEY_OK);
1577
1685
  const runtimeBootstrapOptionalEnv = {};
1578
1686
  if (runtimeBootstrapEnv?.SETTLD_PAID_TOOLS_BASE_URL) {
1579
1687
  runtimeBootstrapOptionalEnv.SETTLD_PAID_TOOLS_BASE_URL = String(runtimeBootstrapEnv.SETTLD_PAID_TOOLS_BASE_URL);
@@ -1607,6 +1715,8 @@ export async function runOnboard({
1607
1715
  baseUrl: normalizedBaseUrl,
1608
1716
  tenantId,
1609
1717
  settldApiKey,
1718
+ bootstrapApiKey: config.bootstrapApiKey,
1719
+ sessionCookie: config.sessionCookie,
1610
1720
  walletProvider: config.walletProvider,
1611
1721
  circleMode: config.circleMode,
1612
1722
  circleBaseUrl: config.circleBaseUrl || null,
@@ -1623,6 +1733,7 @@ export async function runOnboard({
1623
1733
  runtimeEnv
1624
1734
  });
1625
1735
  }
1736
+ advanceOnboardingState(ONBOARDING_EVENTS.WALLET_OK);
1626
1737
  step += 1;
1627
1738
 
1628
1739
  let preflight = { ok: false, skipped: true, checks: [] };
@@ -1643,9 +1754,11 @@ export async function runOnboard({
1643
1754
  } else {
1644
1755
  if (showSteps) printStep(stdout, step, totalSteps, "Skip preflight checks");
1645
1756
  }
1757
+ advanceOnboardingState(ONBOARDING_EVENTS.PREFLIGHT_OK);
1646
1758
  step += 1;
1647
1759
 
1648
1760
  if (args.preflightOnly) {
1761
+ advanceOnboardingState(ONBOARDING_EVENTS.COMPLETE);
1649
1762
  if (showSteps) printStep(stdout, step, totalSteps, "Finalize preflight-only output");
1650
1763
  const payload = {
1651
1764
  ok: true,
@@ -1658,14 +1771,19 @@ export async function runOnboard({
1658
1771
  details: wallet && typeof wallet === "object" ? wallet : null
1659
1772
  },
1660
1773
  settld: {
1661
- baseUrl: normalizedBaseUrl,
1662
- tenantId,
1663
- preflight: Boolean(config.preflight),
1664
- smoke: false,
1665
- profileApplied: false,
1774
+ baseUrl: normalizedBaseUrl,
1775
+ tenantId,
1776
+ authMode: config.authMode ?? "unknown",
1777
+ preflight: Boolean(config.preflight),
1778
+ smoke: false,
1779
+ profileApplied: false,
1666
1780
  profileId: null
1667
1781
  },
1668
1782
  preflight,
1783
+ onboarding: {
1784
+ schemaVersion: "SettldOnboardingState.v1",
1785
+ state: onboardingState
1786
+ },
1669
1787
  hostInstallDetected: Array.isArray(config.installedHosts) && config.installedHosts.includes(config.host),
1670
1788
  installedHosts: config.installedHosts,
1671
1789
  env: {
@@ -1738,6 +1856,7 @@ export async function runOnboard({
1738
1856
  ...walletEnv
1739
1857
  }
1740
1858
  });
1859
+ advanceOnboardingState(ONBOARDING_EVENTS.HOST_CONFIG_OK);
1741
1860
  step += 1;
1742
1861
 
1743
1862
  const mergedEnv = {
@@ -1765,9 +1884,11 @@ export async function runOnboard({
1765
1884
  stdout,
1766
1885
  runWalletCliImpl
1767
1886
  });
1887
+ advanceOnboardingState(ONBOARDING_EVENTS.GUIDED_OK);
1768
1888
  step += 1;
1769
1889
 
1770
1890
  if (showSteps) printStep(stdout, step, totalSteps, "Finalize output");
1891
+ advanceOnboardingState(ONBOARDING_EVENTS.COMPLETE);
1771
1892
  const payload = {
1772
1893
  ok: true,
1773
1894
  setupMode: config.setupMode,
@@ -1782,12 +1903,17 @@ export async function runOnboard({
1782
1903
  settld: {
1783
1904
  baseUrl: normalizedBaseUrl,
1784
1905
  tenantId,
1906
+ authMode: config.authMode ?? "unknown",
1785
1907
  preflight: Boolean(config.preflight),
1786
1908
  smoke: Boolean(config.smoke),
1787
1909
  profileApplied: !config.skipProfileApply,
1788
1910
  profileId: config.skipProfileApply ? null : (config.profileId || "engineering-spend")
1789
1911
  },
1790
1912
  preflight,
1913
+ onboarding: {
1914
+ schemaVersion: "SettldOnboardingState.v1",
1915
+ state: onboardingState
1916
+ },
1791
1917
  hostInstallDetected: Array.isArray(config.installedHosts) && config.installedHosts.includes(config.host),
1792
1918
  installedHosts: config.installedHosts,
1793
1919
  env: mergedEnv,
@@ -1803,6 +1929,7 @@ export async function runOnboard({
1803
1929
  lines.push("Settld onboard complete.");
1804
1930
  lines.push(`Host: ${config.host}`);
1805
1931
  lines.push(`Settld: ${normalizedBaseUrl} (tenant=${tenantId})`);
1932
+ lines.push(`Auth mode: ${config.authMode ?? "unknown"}`);
1806
1933
  lines.push(`Setup mode: ${config.setupMode}`);
1807
1934
  lines.push(`Preflight: ${config.preflight ? "passed" : "skipped"}`);
1808
1935
  lines.push(`Wallet mode: ${config.walletMode}`);
@@ -1852,7 +1979,11 @@ async function main(argv = process.argv.slice(2)) {
1852
1979
  try {
1853
1980
  await runOnboard({ argv });
1854
1981
  } catch (err) {
1855
- 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
+ }
1856
1987
  process.exit(1);
1857
1988
  }
1858
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
+ }