settld 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/CONFIG.md +12 -0
- package/docs/README.md +3 -0
- package/docs/ops/HOSTED_BASELINE_R2.md +4 -2
- package/docs/ops/MINIMUM_PRODUCTION_TOPOLOGY.md +19 -7
- package/docs/ops/PRODUCTION_DEPLOYMENT_CHECKLIST.md +8 -3
- package/package.json +3 -1
- package/scripts/ci/run-public-onboarding-gate.mjs +136 -0
- package/scripts/setup/login.mjs +111 -17
- package/scripts/setup/onboard.mjs +176 -40
- package/scripts/setup/onboarding-failure-taxonomy.mjs +96 -0
- package/scripts/setup/onboarding-state-machine.mjs +102 -0
- package/services/magic-link/README.md +343 -0
- package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_criteria.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/acceptance/acceptance_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/evidence/evidence_index.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/metering/metering_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/payload/invoice_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_definition.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/sla/sla_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-bad/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_criteria.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/acceptance/acceptance_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/evidence/evidence_index.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/invoice/invoice_claim.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/metering/metering_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/attestation/bundle_head_attestation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/events.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/events/payload_material.jsonl +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/global/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/policy.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/revocations.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/events.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/events/payload_material.jsonl +0 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/governance/tenant/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/job/snapshot.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/keys/public_keys.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/manifest.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/payload/job_proof_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/pricing/pricing_matrix_signatures.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/payload/invoice_bundle/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/settld.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/sla/sla_definition.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/sla/sla_evaluation.json +1 -0
- package/services/magic-link/assets/samples/closepack/known-good/verify/verification_report.json +1 -0
- package/services/magic-link/assets/samples/trust.json +11 -0
- package/services/magic-link/src/audit-log.js +24 -0
- package/services/magic-link/src/buyer-auth.js +220 -0
- package/services/magic-link/src/buyer-notifications.js +402 -0
- package/services/magic-link/src/buyer-users.js +129 -0
- package/services/magic-link/src/decision-otp.js +156 -0
- package/services/magic-link/src/decisions.js +92 -0
- package/services/magic-link/src/ingest-keys.js +137 -0
- package/services/magic-link/src/maintenance.js +70 -0
- package/services/magic-link/src/onboarding-email-sequence.js +331 -0
- package/services/magic-link/src/payment-triggers.js +733 -0
- package/services/magic-link/src/pdf.js +149 -0
- package/services/magic-link/src/policy.js +69 -0
- package/services/magic-link/src/redaction.js +6 -0
- package/services/magic-link/src/render-model.js +70 -0
- package/services/magic-link/src/retention-gc.js +158 -0
- package/services/magic-link/src/run-records.js +496 -0
- package/services/magic-link/src/s3.js +171 -0
- package/services/magic-link/src/server.js +15788 -0
- package/services/magic-link/src/settlement-decisions.js +84 -0
- package/services/magic-link/src/smtp.js +202 -0
- package/services/magic-link/src/storage-cli.js +88 -0
- package/services/magic-link/src/storage-format.js +59 -0
- package/services/magic-link/src/tenant-billing.js +115 -0
- package/services/magic-link/src/tenant-onboarding.js +467 -0
- package/services/magic-link/src/tenant-settings.js +1140 -0
- package/services/magic-link/src/usage.js +80 -0
- package/services/magic-link/src/verify-queue.js +179 -0
- package/services/magic-link/src/verify-worker.js +157 -0
- package/services/magic-link/src/webhook-retries.js +542 -0
- package/services/magic-link/src/webhooks.js +218 -0
- package/src/api/app.js +129 -0
|
@@ -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,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:
|
|
1444
|
+
{ defaultValue: defaultKeyMode, color }
|
|
1362
1445
|
);
|
|
1363
1446
|
if (keyMode === "login") {
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
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
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
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
|
-
|
|
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
|
+
}
|