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