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.
- package/docs/CONFIG.md +12 -0
- package/docs/README.md +3 -0
- package/docs/ops/HOSTED_BASELINE_R2.md +4 -2
- package/docs/ops/MINIMUM_PRODUCTION_TOPOLOGY.md +19 -7
- package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +8 -3
- package/package.json +3 -1
- package/scripts/ci/run-public-onboarding-gate.mjs +136 -0
- package/scripts/setup/login.mjs +67 -1
- package/scripts/setup/onboard.mjs +159 -28
- package/scripts/setup/onboarding-failure-taxonomy.mjs +96 -0
- package/scripts/setup/onboarding-state-machine.mjs +102 -0
- package/services/magic-link/README.md +343 -0
- package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_criteria.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/evidence/evidence_index.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/metering/metering_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_definition.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_criteria.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/evidence/evidence_index.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/metering/metering_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/sla/sla_definition.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/sla/sla_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/trust.json +11 -0
- package/services/magic-link/src/audit-log.js +24 -0
- package/services/magic-link/src/buyer-auth.js +220 -0
- package/services/magic-link/src/buyer-notifications.js +402 -0
- package/services/magic-link/src/buyer-users.js +129 -0
- package/services/magic-link/src/decision-otp.js +156 -0
- package/services/magic-link/src/decisions.js +92 -0
- package/services/magic-link/src/ingest-keys.js +137 -0
- package/services/magic-link/src/maintenance.js +70 -0
- package/services/magic-link/src/onboarding-email-sequence.js +331 -0
- package/services/magic-link/src/payment-triggers.js +733 -0
- package/services/magic-link/src/pdf.js +149 -0
- package/services/magic-link/src/policy.js +69 -0
- package/services/magic-link/src/redaction.js +6 -0
- package/services/magic-link/src/render-model.js +70 -0
- package/services/magic-link/src/retention-gc.js +158 -0
- package/services/magic-link/src/run-records.js +496 -0
- package/services/magic-link/src/s3.js +171 -0
- package/services/magic-link/src/server.js +15788 -0
- package/services/magic-link/src/settlement-decisions.js +84 -0
- package/services/magic-link/src/smtp.js +202 -0
- package/services/magic-link/src/storage-cli.js +88 -0
- package/services/magic-link/src/storage-format.js +59 -0
- package/services/magic-link/src/tenant-billing.js +115 -0
- package/services/magic-link/src/tenant-onboarding.js +467 -0
- package/services/magic-link/src/tenant-settings.js +1140 -0
- package/services/magic-link/src/usage.js +80 -0
- package/services/magic-link/src/verify-queue.js +179 -0
- package/services/magic-link/src/verify-worker.js +157 -0
- package/services/magic-link/src/webhook-retries.js +542 -0
- package/services/magic-link/src/webhooks.js +218 -0
- package/src/api/app.js +129 -0
|
@@ -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
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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
|
-
|
|
1176
|
+
let lastError = null;
|
|
1177
|
+
let succeeded = false;
|
|
1170
1178
|
let json = null;
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1383
|
-
stdout.write(
|
|
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
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
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
|
-
|
|
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
|
+
}
|