settld 0.2.0 → 0.2.2

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.
@@ -10,8 +10,9 @@ import { spawnSync } from "node:child_process";
10
10
  import { fileURLToPath } from "node:url";
11
11
 
12
12
  import { bootstrapWalletProvider } from "../../src/core/wallet-provider-bootstrap.js";
13
- import { loadHostConfigHelper, runWizard } from "./wizard.mjs";
13
+ import { extractBootstrapMcpEnv, loadHostConfigHelper, runWizard } from "./wizard.mjs";
14
14
  import { SUPPORTED_HOSTS } from "./host-config.mjs";
15
+ import { defaultSessionPath, readSavedSession } from "./session-store.mjs";
15
16
 
16
17
  const WALLET_MODES = new Set(["managed", "byo", "none"]);
17
18
  const WALLET_PROVIDERS = new Set(["circle"]);
@@ -37,6 +38,12 @@ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
37
38
  const REPO_ROOT = path.resolve(SCRIPT_DIR, "..", "..");
38
39
  const SETTLD_BIN = path.join(REPO_ROOT, "bin", "settld.js");
39
40
  const PROFILE_FINGERPRINT_REGEX = /^[0-9a-f]{64}$/;
41
+ const ANSI_RESET = "\u001b[0m";
42
+ const ANSI_BOLD = "\u001b[1m";
43
+ const ANSI_DIM = "\u001b[2m";
44
+ const ANSI_CYAN = "\u001b[36m";
45
+ const ANSI_GREEN = "\u001b[32m";
46
+ const ANSI_MAGENTA = "\u001b[35m";
40
47
 
41
48
  function usage() {
42
49
  const text = [
@@ -51,6 +58,11 @@ function usage() {
51
58
  " --base-url <url> Settld API base URL (or SETTLD_BASE_URL)",
52
59
  " --tenant-id <id> Settld tenant ID (or SETTLD_TENANT_ID)",
53
60
  " --settld-api-key <key> Settld tenant API key (or SETTLD_API_KEY)",
61
+ " --bootstrap-api-key <key> Onboarding bootstrap API key used to mint tenant API key",
62
+ " --magic-link-api-key <key> Alias for --bootstrap-api-key",
63
+ " --session-file <path> Saved login session path (default: ~/.settld/session.json)",
64
+ " --bootstrap-key-id <id> Optional API key ID hint for runtime bootstrap",
65
+ " --bootstrap-scopes <csv> Optional scopes for generated tenant API key",
54
66
  " --wallet-mode <managed|byo|none> Wallet setup mode (default: managed)",
55
67
  " --wallet-provider <name> Wallet provider (circle; default: circle)",
56
68
  " --wallet-bootstrap <auto|local|remote> Managed wallet setup path (default: auto)",
@@ -101,6 +113,10 @@ function parseArgs(argv) {
101
113
  baseUrl: null,
102
114
  tenantId: null,
103
115
  settldApiKey: null,
116
+ bootstrapApiKey: null,
117
+ sessionFile: defaultSessionPath(),
118
+ bootstrapKeyId: null,
119
+ bootstrapScopes: null,
104
120
  walletMode: "managed",
105
121
  walletProvider: "circle",
106
122
  walletBootstrap: "auto",
@@ -186,6 +202,35 @@ function parseArgs(argv) {
186
202
  i = parsed.nextIndex;
187
203
  continue;
188
204
  }
205
+ if (
206
+ arg === "--bootstrap-api-key" ||
207
+ arg === "--magic-link-api-key" ||
208
+ arg.startsWith("--bootstrap-api-key=") ||
209
+ arg.startsWith("--magic-link-api-key=")
210
+ ) {
211
+ const parsed = readArgValue(argv, i, arg);
212
+ out.bootstrapApiKey = parsed.value;
213
+ i = parsed.nextIndex;
214
+ continue;
215
+ }
216
+ if (arg === "--session-file" || arg.startsWith("--session-file=")) {
217
+ const parsed = readArgValue(argv, i, arg);
218
+ out.sessionFile = parsed.value;
219
+ i = parsed.nextIndex;
220
+ continue;
221
+ }
222
+ if (arg === "--bootstrap-key-id" || arg.startsWith("--bootstrap-key-id=")) {
223
+ const parsed = readArgValue(argv, i, arg);
224
+ out.bootstrapKeyId = parsed.value;
225
+ i = parsed.nextIndex;
226
+ continue;
227
+ }
228
+ if (arg === "--bootstrap-scopes" || arg.startsWith("--bootstrap-scopes=")) {
229
+ const parsed = readArgValue(argv, i, arg);
230
+ out.bootstrapScopes = parsed.value;
231
+ i = parsed.nextIndex;
232
+ continue;
233
+ }
189
234
  if (arg === "--wallet-mode" || arg.startsWith("--wallet-mode=")) {
190
235
  const parsed = readArgValue(argv, i, arg);
191
236
  out.walletMode = String(parsed.value ?? "").trim().toLowerCase();
@@ -272,6 +317,7 @@ function parseArgs(argv) {
272
317
  if (out.preflightOnly && out.preflight === false) {
273
318
  throw new Error("--preflight-only cannot be combined with --no-preflight");
274
319
  }
320
+ out.sessionFile = path.resolve(process.cwd(), String(out.sessionFile ?? "").trim() || defaultSessionPath());
275
321
  if (out.outEnv) out.outEnv = path.resolve(process.cwd(), out.outEnv);
276
322
  if (out.reportPath) out.reportPath = path.resolve(process.cwd(), out.reportPath);
277
323
  return out;
@@ -290,6 +336,19 @@ function normalizeHttpUrl(value) {
290
336
  return parsed.toString().replace(/\/+$/, "");
291
337
  }
292
338
 
339
+ function supportsColor(output = process.stdout, env = process.env) {
340
+ if (!output?.isTTY) return false;
341
+ if (String(env.NO_COLOR ?? "").trim()) return false;
342
+ if (String(env.FORCE_COLOR ?? "").trim() === "0") return false;
343
+ return true;
344
+ }
345
+
346
+ function tint(enabled, code, value) {
347
+ const text = String(value ?? "");
348
+ if (!enabled) return text;
349
+ return `${code}${text}${ANSI_RESET}`;
350
+ }
351
+
293
352
  function commandExists(command, { platform = process.platform } = {}) {
294
353
  const lookupCmd = platform === "win32" ? "where" : "which";
295
354
  const probe = spawnSync(lookupCmd, [command], { stdio: "ignore" });
@@ -582,7 +641,7 @@ async function promptSelect(
582
641
  stdout,
583
642
  label,
584
643
  options,
585
- { defaultValue = null, hint = null } = {}
644
+ { defaultValue = null, hint = null, color = false } = {}
586
645
  ) {
587
646
  if (!Array.isArray(options) || options.length === 0) {
588
647
  throw new Error(`${label} requires at least one option`);
@@ -614,14 +673,14 @@ async function promptSelect(
614
673
 
615
674
  const render = () => {
616
675
  const lines = [];
617
- lines.push(`${label} (arrow keys + Enter)`);
676
+ lines.push(tint(color, ANSI_CYAN, `${label} (arrow keys + Enter)`));
618
677
  for (let i = 0; i < normalizedOptions.length; i += 1) {
619
678
  const option = normalizedOptions[i];
620
- const prefix = i === index ? ">" : " ";
679
+ const prefix = i === index ? tint(color, ANSI_GREEN, "") : tint(color, ANSI_DIM, "○");
621
680
  const detail = option.hint ? ` - ${option.hint}` : "";
622
- lines.push(` ${prefix} ${option.label}${detail}`);
681
+ lines.push(` ${prefix} ${i === index ? tint(color, ANSI_BOLD, option.label) : option.label}${tint(color, ANSI_DIM, detail)}`);
623
682
  }
624
- if (hint) lines.push(` ${hint}`);
683
+ if (hint) lines.push(` ${tint(color, ANSI_DIM, hint)}`);
625
684
  if (renderedLines > 0) {
626
685
  stdout.write(`\u001b[${renderedLines}A`);
627
686
  }
@@ -648,7 +707,7 @@ async function promptSelect(
648
707
  const resolveWithSelection = () => {
649
708
  const selected = normalizedOptions[index];
650
709
  cleanup();
651
- stdout.write(`\u001b[2K\r${label}: ${selected.label}\n`);
710
+ stdout.write(`\u001b[2K\r${tint(color, ANSI_CYAN, label)}: ${tint(color, ANSI_GREEN, selected.label)}\n`);
652
711
  resolve(selected.value);
653
712
  };
654
713
 
@@ -679,7 +738,14 @@ async function promptSelect(
679
738
  });
680
739
  }
681
740
 
682
- async function promptBooleanChoice(rl, stdin, stdout, label, defaultValue, { trueLabel = "Yes", falseLabel = "No", hint = null } = {}) {
741
+ async function promptBooleanChoice(
742
+ rl,
743
+ stdin,
744
+ stdout,
745
+ label,
746
+ defaultValue,
747
+ { trueLabel = "Yes", falseLabel = "No", hint = null, color = false } = {}
748
+ ) {
683
749
  const selected = await promptSelect(
684
750
  rl,
685
751
  stdin,
@@ -689,7 +755,7 @@ async function promptBooleanChoice(rl, stdin, stdout, label, defaultValue, { tru
689
755
  { value: "yes", label: trueLabel },
690
756
  { value: "no", label: falseLabel }
691
757
  ],
692
- { defaultValue: defaultValue ? "yes" : "no", hint }
758
+ { defaultValue: defaultValue ? "yes" : "no", hint, color }
693
759
  );
694
760
  return selected === "yes";
695
761
  }
@@ -824,6 +890,81 @@ function resolveByoWalletEnv({ walletProvider, walletEnvRows, runtimeEnv }) {
824
890
  return env;
825
891
  }
826
892
 
893
+ function parseScopes(raw) {
894
+ if (!raw || !String(raw).trim()) return [];
895
+ const seen = new Set();
896
+ const out = [];
897
+ for (const part of String(raw).split(",")) {
898
+ const scope = String(part ?? "").trim();
899
+ if (!scope || seen.has(scope)) continue;
900
+ seen.add(scope);
901
+ out.push(scope);
902
+ }
903
+ return out;
904
+ }
905
+
906
+ async function requestRuntimeBootstrapMcpEnv({
907
+ baseUrl,
908
+ tenantId,
909
+ bootstrapApiKey,
910
+ sessionCookie,
911
+ bootstrapKeyId = null,
912
+ bootstrapScopes = [],
913
+ idempotencyKey = null,
914
+ fetchImpl = fetch
915
+ } = {}) {
916
+ const normalizedBaseUrl = normalizeHttpUrl(baseUrl);
917
+ if (!normalizedBaseUrl) throw new Error(`invalid runtime bootstrap base URL: ${baseUrl}`);
918
+ const apiKey = String(bootstrapApiKey ?? "").trim();
919
+ const cookie = String(sessionCookie ?? "").trim();
920
+ if (!apiKey && !cookie) {
921
+ throw new Error("runtime bootstrap requires bootstrap API key or saved login session");
922
+ }
923
+
924
+ const headers = {
925
+ "content-type": "application/json"
926
+ };
927
+ if (apiKey) headers["x-api-key"] = apiKey;
928
+ if (cookie) headers.cookie = cookie;
929
+ if (idempotencyKey) headers["x-idempotency-key"] = String(idempotencyKey);
930
+
931
+ const body = {
932
+ apiKey: {
933
+ create: true,
934
+ description: "settld setup runtime bootstrap"
935
+ }
936
+ };
937
+ if (bootstrapKeyId) body.apiKey.keyId = String(bootstrapKeyId);
938
+ if (Array.isArray(bootstrapScopes) && bootstrapScopes.length > 0) {
939
+ body.apiKey.scopes = bootstrapScopes;
940
+ }
941
+
942
+ const url = new URL(
943
+ `/v1/tenants/${encodeURIComponent(String(tenantId ?? ""))}/onboarding/runtime-bootstrap`,
944
+ normalizedBaseUrl
945
+ );
946
+ const res = await fetchImpl(url.toString(), {
947
+ method: "POST",
948
+ headers,
949
+ body: JSON.stringify(body)
950
+ });
951
+ const text = await res.text();
952
+ let json = null;
953
+ try {
954
+ json = text ? JSON.parse(text) : null;
955
+ } catch {
956
+ json = null;
957
+ }
958
+ if (!res.ok) {
959
+ const message =
960
+ json && typeof json === "object"
961
+ ? json?.message ?? json?.error ?? `HTTP ${res.status}`
962
+ : text || `HTTP ${res.status}`;
963
+ throw new Error(`runtime bootstrap request failed (${res.status}): ${String(message)}`);
964
+ }
965
+ return extractBootstrapMcpEnv(json);
966
+ }
967
+
827
968
  async function requestRemoteWalletBootstrap({
828
969
  baseUrl,
829
970
  tenantId,
@@ -889,6 +1030,8 @@ async function resolveRuntimeConfig({
889
1030
  stdout = process.stdout,
890
1031
  detectInstalledHostsImpl = detectInstalledHosts
891
1032
  }) {
1033
+ const sessionFile = String(args.sessionFile ?? runtimeEnv.SETTLD_SESSION_FILE ?? defaultSessionPath()).trim();
1034
+ const savedSession = await readSavedSession({ sessionPath: sessionFile });
892
1035
  const installedHosts = detectInstalledHostsImpl();
893
1036
  const defaultHost = selectDefaultHost({
894
1037
  explicitHost: args.host ? String(args.host).toLowerCase() : "",
@@ -900,6 +1043,13 @@ async function resolveRuntimeConfig({
900
1043
  baseUrl: String(args.baseUrl ?? runtimeEnv.SETTLD_BASE_URL ?? "").trim(),
901
1044
  tenantId: String(args.tenantId ?? runtimeEnv.SETTLD_TENANT_ID ?? "").trim(),
902
1045
  settldApiKey: String(args.settldApiKey ?? runtimeEnv.SETTLD_API_KEY ?? "").trim(),
1046
+ bootstrapApiKey: String(
1047
+ args.bootstrapApiKey ?? runtimeEnv.SETTLD_BOOTSTRAP_API_KEY ?? runtimeEnv.MAGIC_LINK_API_KEY ?? ""
1048
+ ).trim(),
1049
+ sessionFile,
1050
+ sessionCookie: String(runtimeEnv.SETTLD_SESSION_COOKIE ?? "").trim(),
1051
+ bootstrapKeyId: String(args.bootstrapKeyId ?? runtimeEnv.SETTLD_BOOTSTRAP_KEY_ID ?? "").trim(),
1052
+ bootstrapScopes: String(args.bootstrapScopes ?? runtimeEnv.SETTLD_BOOTSTRAP_SCOPES ?? "").trim(),
903
1053
  walletProvider: args.walletProvider,
904
1054
  walletBootstrap: args.walletBootstrap,
905
1055
  circleApiKey: String(args.circleApiKey ?? runtimeEnv.CIRCLE_API_KEY ?? "").trim(),
@@ -914,12 +1064,19 @@ async function resolveRuntimeConfig({
914
1064
  dryRun: Boolean(args.dryRun),
915
1065
  installedHosts
916
1066
  };
1067
+ if (savedSession) {
1068
+ if (!out.baseUrl) out.baseUrl = String(savedSession.baseUrl ?? "").trim();
1069
+ if (!out.tenantId) out.tenantId = String(savedSession.tenantId ?? "").trim();
1070
+ if (!out.sessionCookie) out.sessionCookie = String(savedSession.cookie ?? "").trim();
1071
+ }
917
1072
 
918
1073
  if (args.nonInteractive) {
919
1074
  if (!SUPPORTED_HOSTS.includes(out.host)) throw new Error(`--host must be one of: ${SUPPORTED_HOSTS.join(", ")}`);
920
1075
  if (!out.baseUrl) throw new Error("--base-url is required");
921
1076
  if (!out.tenantId) throw new Error("--tenant-id is required");
922
- if (!out.settldApiKey) throw new Error("--settld-api-key is required");
1077
+ if (!out.settldApiKey && !out.bootstrapApiKey && !out.sessionCookie) {
1078
+ throw new Error("--settld-api-key, --bootstrap-api-key, or saved login session is required");
1079
+ }
923
1080
  if (out.walletMode === "managed" && out.walletBootstrap === "local" && !out.circleApiKey) {
924
1081
  throw new Error("--circle-api-key is required for --wallet-mode managed --wallet-bootstrap local");
925
1082
  }
@@ -929,15 +1086,22 @@ async function resolveRuntimeConfig({
929
1086
  if (!stdin.isTTY || !stdout.isTTY) {
930
1087
  throw new Error("interactive mode requires a TTY. Re-run with --non-interactive and explicit flags.");
931
1088
  }
1089
+ const color = supportsColor(stdout, runtimeEnv);
932
1090
  const mutableOutput = createMutableOutput(stdout);
933
1091
  const rl = createInterface({ input: stdin, output: mutableOutput });
934
1092
  try {
935
- stdout.write("Settld guided setup\n");
936
- stdout.write("===================\n");
1093
+ const title = tint(color, ANSI_BOLD, "Settld guided setup");
1094
+ const subtitle = tint(color, ANSI_DIM, "Deterministic onboarding for trusted agent spend");
1095
+ stdout.write(`${title}\n`);
1096
+ stdout.write(`${tint(color, ANSI_MAGENTA, "===================")}\n`);
1097
+ stdout.write(`${subtitle}\n`);
937
1098
  if (installedHosts.length > 0) {
938
- stdout.write(`Detected hosts: ${installedHosts.join(", ")}\n`);
1099
+ stdout.write(`${tint(color, ANSI_CYAN, "Detected hosts")}: ${installedHosts.join(", ")}\n`);
939
1100
  } else {
940
- stdout.write("Detected hosts: none (will still write config files)\n");
1101
+ stdout.write(`${tint(color, ANSI_CYAN, "Detected hosts")}: none (will still write config files)\n`);
1102
+ }
1103
+ if (savedSession?.tenantId) {
1104
+ stdout.write(`${tint(color, ANSI_GREEN, "Saved login session")}: tenant ${savedSession.tenantId}\n`);
941
1105
  }
942
1106
  stdout.write("\n");
943
1107
 
@@ -952,7 +1116,7 @@ async function resolveRuntimeConfig({
952
1116
  stdout,
953
1117
  "Select host",
954
1118
  hostOptions,
955
- { defaultValue: hostPromptDefault, hint: "Up/Down arrows change selection" }
1119
+ { defaultValue: hostPromptDefault, hint: "Up/Down arrows change selection", color }
956
1120
  );
957
1121
 
958
1122
  if (!out.walletMode) out.walletMode = "managed";
@@ -966,7 +1130,7 @@ async function resolveRuntimeConfig({
966
1130
  { value: "byo", label: "byo", hint: "Use your existing wallet IDs and secrets" },
967
1131
  { value: "none", label: "none", hint: "No payment rail wiring during setup" }
968
1132
  ],
969
- { defaultValue: out.walletMode }
1133
+ { defaultValue: out.walletMode, color }
970
1134
  );
971
1135
 
972
1136
  if (!out.baseUrl) {
@@ -975,7 +1139,48 @@ async function resolveRuntimeConfig({
975
1139
  if (!out.tenantId) {
976
1140
  out.tenantId = await promptLine(rl, "Tenant ID", { defaultValue: "tenant_default" });
977
1141
  }
978
- if (!out.settldApiKey) out.settldApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Settld API key");
1142
+ if (!out.settldApiKey) {
1143
+ const canUseSavedSession =
1144
+ Boolean(out.sessionCookie) &&
1145
+ (!savedSession ||
1146
+ (normalizeHttpUrl(out.baseUrl) === normalizeHttpUrl(savedSession?.baseUrl) &&
1147
+ String(out.tenantId ?? "").trim() === String(savedSession?.tenantId ?? "").trim()));
1148
+ const keyOptions = [];
1149
+ if (canUseSavedSession) {
1150
+ keyOptions.push({
1151
+ value: "session",
1152
+ label: "Use saved login session",
1153
+ hint: `Reuse ${out.sessionFile} to mint runtime key`
1154
+ });
1155
+ }
1156
+ keyOptions.push(
1157
+ { value: "bootstrap", label: "Generate during setup", hint: "Use onboarding bootstrap API key" },
1158
+ { value: "manual", label: "Paste existing key", hint: "Use an existing tenant API key" }
1159
+ );
1160
+ const keyMode = await promptSelect(
1161
+ rl,
1162
+ stdin,
1163
+ stdout,
1164
+ "How should setup get your Settld API key?",
1165
+ keyOptions,
1166
+ { defaultValue: canUseSavedSession ? "session" : "bootstrap", color }
1167
+ );
1168
+ if (keyMode === "bootstrap") {
1169
+ if (!out.bootstrapApiKey) {
1170
+ out.bootstrapApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Onboarding bootstrap API key");
1171
+ }
1172
+ if (!out.bootstrapKeyId) {
1173
+ out.bootstrapKeyId = await promptLine(rl, "Generated key ID (optional)", { required: false });
1174
+ }
1175
+ if (!out.bootstrapScopes) {
1176
+ out.bootstrapScopes = await promptLine(rl, "Generated key scopes CSV (optional)", { required: false });
1177
+ }
1178
+ } else if (keyMode === "manual") {
1179
+ out.settldApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Settld API key");
1180
+ } else {
1181
+ out.bootstrapApiKey = "";
1182
+ }
1183
+ }
979
1184
 
980
1185
  if (out.walletMode === "managed") {
981
1186
  out.walletBootstrap = await promptSelect(
@@ -988,7 +1193,7 @@ async function resolveRuntimeConfig({
988
1193
  { value: "local", label: "local", hint: "Always use local Circle API key flow" },
989
1194
  { value: "remote", label: "remote", hint: "Always use tenant onboarding endpoint" }
990
1195
  ],
991
- { defaultValue: out.walletBootstrap || "auto" }
1196
+ { defaultValue: out.walletBootstrap || "auto", color }
992
1197
  );
993
1198
  if (out.walletBootstrap === "local" && !out.circleApiKey) {
994
1199
  out.circleApiKey = await promptSecretLine(rl, mutableOutput, stdout, "Circle API key");
@@ -1024,7 +1229,8 @@ async function resolveRuntimeConfig({
1024
1229
  out.preflight,
1025
1230
  {
1026
1231
  trueLabel: "Yes - validate API/auth/paths",
1027
- falseLabel: "No - skip preflight"
1232
+ falseLabel: "No - skip preflight",
1233
+ color
1028
1234
  }
1029
1235
  );
1030
1236
  out.smoke = await promptBooleanChoice(
@@ -1035,7 +1241,8 @@ async function resolveRuntimeConfig({
1035
1241
  out.smoke,
1036
1242
  {
1037
1243
  trueLabel: "Yes - run settld.about probe",
1038
- falseLabel: "No - skip smoke"
1244
+ falseLabel: "No - skip smoke",
1245
+ color
1039
1246
  }
1040
1247
  );
1041
1248
 
@@ -1047,7 +1254,8 @@ async function resolveRuntimeConfig({
1047
1254
  !out.skipProfileApply,
1048
1255
  {
1049
1256
  trueLabel: "Yes - apply profile now",
1050
- falseLabel: "No - skip profile apply"
1257
+ falseLabel: "No - skip profile apply",
1258
+ color
1051
1259
  }
1052
1260
  );
1053
1261
  out.skipProfileApply = !applyProfile;
@@ -1065,7 +1273,8 @@ async function resolveRuntimeConfig({
1065
1273
  out.dryRun,
1066
1274
  {
1067
1275
  trueLabel: "Yes - preview only",
1068
- falseLabel: "No - write config"
1276
+ falseLabel: "No - write config",
1277
+ color
1069
1278
  }
1070
1279
  );
1071
1280
  }
@@ -1084,6 +1293,7 @@ export async function runOnboard({
1084
1293
  runWizardImpl = runWizard,
1085
1294
  loadHostConfigHelperImpl = loadHostConfigHelper,
1086
1295
  bootstrapWalletProviderImpl = bootstrapWalletProvider,
1296
+ requestRuntimeBootstrapMcpEnvImpl = requestRuntimeBootstrapMcpEnv,
1087
1297
  requestRemoteWalletBootstrapImpl = requestRemoteWalletBootstrap,
1088
1298
  runPreflightChecksImpl = runPreflightChecks,
1089
1299
  detectInstalledHostsImpl = detectInstalledHosts
@@ -1110,7 +1320,28 @@ export async function runOnboard({
1110
1320
  const normalizedBaseUrl = normalizeHttpUrl(mustString(config.baseUrl, "SETTLD_BASE_URL / --base-url"));
1111
1321
  if (!normalizedBaseUrl) throw new Error(`invalid Settld base URL: ${config.baseUrl}`);
1112
1322
  const tenantId = mustString(config.tenantId, "SETTLD_TENANT_ID / --tenant-id");
1113
- const settldApiKey = mustString(config.settldApiKey, "SETTLD_API_KEY / --settld-api-key");
1323
+ let settldApiKey = String(config.settldApiKey ?? "").trim();
1324
+ let runtimeBootstrapEnv = null;
1325
+ if (!settldApiKey) {
1326
+ if (showSteps) stdout.write("Generating tenant runtime API key via onboarding bootstrap/session...\n");
1327
+ runtimeBootstrapEnv = await requestRuntimeBootstrapMcpEnvImpl({
1328
+ baseUrl: normalizedBaseUrl,
1329
+ tenantId,
1330
+ bootstrapApiKey: config.bootstrapApiKey,
1331
+ sessionCookie: config.sessionCookie,
1332
+ bootstrapKeyId: config.bootstrapKeyId || null,
1333
+ bootstrapScopes: parseScopes(config.bootstrapScopes),
1334
+ fetchImpl
1335
+ });
1336
+ settldApiKey = mustString(runtimeBootstrapEnv?.SETTLD_API_KEY ?? "", "runtime bootstrap SETTLD_API_KEY");
1337
+ }
1338
+ const runtimeBootstrapOptionalEnv = {};
1339
+ if (runtimeBootstrapEnv?.SETTLD_PAID_TOOLS_BASE_URL) {
1340
+ runtimeBootstrapOptionalEnv.SETTLD_PAID_TOOLS_BASE_URL = String(runtimeBootstrapEnv.SETTLD_PAID_TOOLS_BASE_URL);
1341
+ }
1342
+ if (runtimeBootstrapEnv?.SETTLD_PAID_TOOLS_AGENT_PASSPORT) {
1343
+ runtimeBootstrapOptionalEnv.SETTLD_PAID_TOOLS_AGENT_PASSPORT = String(runtimeBootstrapEnv.SETTLD_PAID_TOOLS_AGENT_PASSPORT);
1344
+ }
1114
1345
 
1115
1346
  if (showSteps) printStep(stdout, step, totalSteps, "Resolve wallet configuration");
1116
1347
  let walletBootstrapMode = "none";
@@ -1198,13 +1429,19 @@ export async function runOnboard({
1198
1429
  preflight,
1199
1430
  hostInstallDetected: Array.isArray(config.installedHosts) && config.installedHosts.includes(config.host),
1200
1431
  installedHosts: config.installedHosts,
1201
- env: walletEnv,
1432
+ env: {
1433
+ SETTLD_BASE_URL: normalizedBaseUrl,
1434
+ SETTLD_TENANT_ID: tenantId,
1435
+ SETTLD_API_KEY: settldApiKey,
1436
+ ...runtimeBootstrapOptionalEnv,
1437
+ ...walletEnv
1438
+ },
1202
1439
  outEnv: args.outEnv ?? null,
1203
1440
  reportPath: args.reportPath ?? null
1204
1441
  };
1205
1442
  if (args.outEnv) {
1206
1443
  await fs.mkdir(path.dirname(args.outEnv), { recursive: true });
1207
- await fs.writeFile(args.outEnv, toEnvFileText(walletEnv), "utf8");
1444
+ await fs.writeFile(args.outEnv, toEnvFileText(payload.env), "utf8");
1208
1445
  }
1209
1446
  await writeJsonReport(args.reportPath, payload);
1210
1447
  if (args.format === "json") {
@@ -1216,6 +1453,12 @@ export async function runOnboard({
1216
1453
  lines.push(`Settld: ${normalizedBaseUrl} (tenant=${tenantId})`);
1217
1454
  lines.push(`Wallet mode: ${config.walletMode}`);
1218
1455
  lines.push(`Wallet bootstrap mode: ${walletBootstrapMode}`);
1456
+ if (config.walletMode !== "none") {
1457
+ lines.push("Wallet next steps:");
1458
+ lines.push("- settld wallet status");
1459
+ lines.push("- settld wallet fund --method transfer");
1460
+ lines.push("- settld wallet balance --watch --min-usdc 1");
1461
+ }
1219
1462
  if (args.outEnv) lines.push(`Wrote env file: ${args.outEnv}`);
1220
1463
  if (args.reportPath) lines.push(`Wrote report: ${args.reportPath}`);
1221
1464
  lines.push("");
@@ -1251,11 +1494,15 @@ export async function runOnboard({
1251
1494
  argv: wizardArgv,
1252
1495
  fetchImpl,
1253
1496
  stdout,
1254
- extraEnv: walletEnv
1497
+ extraEnv: {
1498
+ ...runtimeBootstrapOptionalEnv,
1499
+ ...walletEnv
1500
+ }
1255
1501
  });
1256
1502
  step += 1;
1257
1503
 
1258
1504
  const mergedEnv = {
1505
+ ...runtimeBootstrapOptionalEnv,
1259
1506
  ...(walletEnv ?? {}),
1260
1507
  ...(wizardResult?.env && typeof wizardResult.env === "object" ? wizardResult.env : {})
1261
1508
  };
@@ -1316,6 +1563,14 @@ export async function runOnboard({
1316
1563
  lines.push(`${step}. ${row}`);
1317
1564
  step += 1;
1318
1565
  }
1566
+ if (config.walletMode !== "none") {
1567
+ lines.push(`${step}. Run \`settld wallet status\` to confirm wallet wiring.`);
1568
+ step += 1;
1569
+ lines.push(`${step}. Fund with \`settld wallet fund --method transfer\` (or \`--method card --open\` if hosted links are configured).`);
1570
+ step += 1;
1571
+ lines.push(`${step}. Confirm funds: \`settld wallet balance --watch --min-usdc 1\`.`);
1572
+ step += 1;
1573
+ }
1319
1574
  lines.push(`${step}. Run \`npm run mcp:probe\` for an immediate health check.`);
1320
1575
  stdout.write(`${lines.join("\n")}\n`);
1321
1576
  }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+
7
+ const SESSION_SCHEMA_VERSION = "SettldCliSession.v1";
8
+
9
+ function normalizeCookieHeader(value) {
10
+ const raw = String(value ?? "").trim();
11
+ if (!raw) return null;
12
+ const firstSegment = raw.split(";")[0]?.trim() ?? "";
13
+ if (!firstSegment) return null;
14
+ const eq = firstSegment.indexOf("=");
15
+ if (eq <= 0) return null;
16
+ const name = firstSegment.slice(0, eq).trim();
17
+ const token = firstSegment.slice(eq + 1).trim();
18
+ if (!name || !token) return null;
19
+ return `${name}=${token}`;
20
+ }
21
+
22
+ export function defaultSessionPath({ homeDir = os.homedir() } = {}) {
23
+ return path.join(homeDir, ".settld", "session.json");
24
+ }
25
+
26
+ export function normalizeSession(input) {
27
+ const row = input && typeof input === "object" && !Array.isArray(input) ? input : {};
28
+ const baseUrl = typeof row.baseUrl === "string" ? row.baseUrl.trim().replace(/\/+$/, "") : "";
29
+ const tenantId = typeof row.tenantId === "string" ? row.tenantId.trim() : "";
30
+ const email = typeof row.email === "string" ? row.email.trim().toLowerCase() : "";
31
+ const cookie = normalizeCookieHeader(row.cookie);
32
+ if (!baseUrl || !tenantId || !cookie) return null;
33
+ const out = {
34
+ schemaVersion: SESSION_SCHEMA_VERSION,
35
+ savedAt: typeof row.savedAt === "string" && row.savedAt.trim() ? row.savedAt.trim() : new Date().toISOString(),
36
+ baseUrl,
37
+ tenantId,
38
+ cookie,
39
+ email: email || null
40
+ };
41
+ if (typeof row.expiresAt === "string" && row.expiresAt.trim()) out.expiresAt = row.expiresAt.trim();
42
+ return out;
43
+ }
44
+
45
+ export async function readSavedSession({ sessionPath = defaultSessionPath() } = {}) {
46
+ try {
47
+ const raw = await fs.readFile(sessionPath, "utf8");
48
+ const parsed = JSON.parse(raw);
49
+ return normalizeSession(parsed);
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ export async function writeSavedSession({ session, sessionPath = defaultSessionPath() } = {}) {
56
+ const normalized = normalizeSession(session);
57
+ if (!normalized) throw new Error("invalid session payload");
58
+ await fs.mkdir(path.dirname(sessionPath), { recursive: true });
59
+ await fs.writeFile(sessionPath, `${JSON.stringify(normalized, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
60
+ return normalized;
61
+ }
62
+
63
+ export function cookieHeaderFromSetCookie(value) {
64
+ return normalizeCookieHeader(value);
65
+ }
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
- if [ ! -x ".vercel-venv/bin/mkdocs" ]; then
5
- echo "MkDocs venv not found; running install step first..."
4
+ if ! python3 -m mkdocs --version >/dev/null 2>&1; then
5
+ echo "MkDocs not found; running install step first..."
6
6
  bash scripts/vercel/install-mkdocs.sh
7
7
  fi
8
8
 
9
- .vercel-venv/bin/mkdocs build --strict --config-file mkdocs.yml
9
+ python3 -m mkdocs build --strict --config-file mkdocs.yml
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Vercel "ignoreCommand" contract:
5
+ # - exit 0 => skip deployment
6
+ # - exit 1 => continue with deployment
7
+
8
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
9
+ cd "$REPO_ROOT"
10
+
11
+ if ! git rev-parse --verify HEAD^ >/dev/null 2>&1; then
12
+ # No parent commit context available; build to stay safe.
13
+ exit 1
14
+ fi
15
+
16
+ if git diff --quiet HEAD^ HEAD -- \
17
+ dashboard/ \
18
+ scripts/vercel/ignore-dashboard.sh \
19
+ .github/workflows/release.yml \
20
+ .github/workflows/tests.yml; then
21
+ # No website changes; skip dashboard deployment.
22
+ exit 0
23
+ fi
24
+
25
+ # Relevant website/deploy files changed; run deployment.
26
+ exit 1
@@ -11,6 +11,8 @@ if ! git rev-parse --verify HEAD^ >/dev/null 2>&1; then
11
11
  fi
12
12
 
13
13
  if git diff --quiet HEAD^ HEAD -- \
14
+ mkdocs/docs/ \
15
+ mkdocs/ \
14
16
  docs/ \
15
17
  mkdocs.yml \
16
18
  scripts/vercel/ \
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env bash
2
2
  set -euo pipefail
3
3
 
4
- python3 -m venv .vercel-venv
5
- .vercel-venv/bin/python -m pip install --upgrade pip setuptools wheel
6
- .vercel-venv/bin/pip install mkdocs mkdocs-material
4
+ python3 -m pip install --break-system-packages --upgrade pip setuptools wheel
5
+ python3 -m pip install --break-system-packages mkdocs mkdocs-material