@carrierllc/mcp 0.13.29 → 0.13.30

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/dist/cli.js CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  lastNDaysPeriod,
23
23
  loadStorefrontBrand,
24
24
  mergeEnvLocal,
25
+ package_default,
25
26
  packagesScreen,
26
27
  probeAll,
27
28
  provisionClerk,
@@ -40,7 +41,7 @@ import {
40
41
  verifyStorefront,
41
42
  walletScreen,
42
43
  which
43
- } from "./chunk-LJPYVILK.js";
44
+ } from "./chunk-ZJY7H2QW.js";
44
45
  import {
45
46
  copyTree,
46
47
  exists,
@@ -54,7 +55,7 @@ import {
54
55
  import { Command } from "commander";
55
56
  import * as p3 from "@clack/prompts";
56
57
  import pc4 from "picocolors";
57
- import { resolve as resolve2 } from "path";
58
+ import { resolve as resolve4 } from "path";
58
59
 
59
60
  // src/cli/lib/plugin.ts
60
61
  import { join as join2 } from "path";
@@ -66,7 +67,7 @@ import { existsSync } from "fs";
66
67
  import { homedir } from "os";
67
68
  function packageRoot() {
68
69
  const here = dirname(fileURLToPath(import.meta.url));
69
- for (const candidate of [resolve(here, ".."), resolve(here, "..", "..")]) {
70
+ for (const candidate of [resolve(here, ".."), resolve(here, "..", ".."), resolve(here, "..", "..", "..")]) {
70
71
  if (existsSync(join(candidate, "plugin")) && existsSync(join(candidate, "templates"))) {
71
72
  return candidate;
72
73
  }
@@ -219,7 +220,7 @@ async function scaffoldStorefront(target, brand) {
219
220
  await patchBrandConfig(join3(target, "src", "brand.config.ts"), brand);
220
221
  const envBody = existingEnv ? preserveClerkFromEnv(existingEnv, renderEnv(brand)) : renderEnv(brand);
221
222
  await writeFile(envPath, envBody);
222
- await writeFile(join3(target, ".npmrc"), "ignore-workspace-root-check=true\n");
223
+ await writeFile(join3(target, ".npmrc"), "# Standalone template installs use pnpm --ignore-workspace explicitly.\n");
223
224
  }
224
225
  async function patchBrandConfig(path, brand) {
225
226
  if (!await exists(path)) return;
@@ -277,13 +278,24 @@ async function writeStorefrontLogo(target, brand) {
277
278
  }
278
279
 
279
280
  // src/cli/lib/auth.ts
280
- import { createHash, randomBytes, timingSafeEqual } from "crypto";
281
+ import { createHash as createHash2, randomBytes, timingSafeEqual } from "crypto";
281
282
  import { createServer } from "http";
282
283
 
284
+ // src/cli/lib/mcp-oauth.ts
285
+ import { createHash } from "crypto";
286
+ import { open as open2, lstat, realpath } from "fs/promises";
287
+ import { constants as constants2 } from "fs";
288
+ import { join as join6, resolve as resolve2, dirname as dirname2 } from "path";
289
+ import { homedir as homedir3, userInfo } from "os";
290
+ import { spawn as spawn2, execFile } from "child_process";
291
+ import { promisify } from "util";
292
+
283
293
  // src/cli/lib/credentials.ts
284
294
  import { homedir as homedir2 } from "os";
285
295
  import { join as join5 } from "path";
286
- import { chmod, mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile3 } from "fs/promises";
296
+ import { chmod, mkdir as mkdir2, rm, rename, open } from "fs/promises";
297
+ import { constants, rmSync } from "fs";
298
+ import { randomUUID } from "crypto";
287
299
  function carrierHome() {
288
300
  return process.env.CARRIER_HOME?.trim() || join5(homedir2(), ".carrier");
289
301
  }
@@ -292,7 +304,15 @@ function credentialsPath() {
292
304
  }
293
305
  async function readCredentials() {
294
306
  try {
295
- const raw = await readFile2(credentialsPath(), "utf8");
307
+ const handle = await open(credentialsPath(), constants.O_RDONLY | constants.O_NOFOLLOW);
308
+ let raw;
309
+ try {
310
+ const stat = await handle.stat();
311
+ if (!stat.isFile() || stat.size > 1024 * 1024 || process.platform !== "win32" && stat.mode & 63 || process.getuid && stat.uid !== process.getuid()) return null;
312
+ raw = await handle.readFile("utf8");
313
+ } finally {
314
+ await handle.close();
315
+ }
296
316
  const parsed = JSON.parse(raw);
297
317
  if (!parsed?.accessToken || typeof parsed.accessToken !== "string") return null;
298
318
  return parsed;
@@ -305,9 +325,56 @@ async function writeCredentials(creds) {
305
325
  await mkdir2(dir, { recursive: true, mode: 448 });
306
326
  await chmod(dir, 448).catch(() => void 0);
307
327
  const file = credentialsPath();
308
- await writeFile3(file, `${JSON.stringify(creds, null, 2)}
309
- `, { mode: 384 });
310
- await chmod(file, 384).catch(() => void 0);
328
+ const temporary = join5(dir, `.credentials-${randomUUID()}.json`);
329
+ try {
330
+ const handle = await open(temporary, "wx", 384);
331
+ try {
332
+ await handle.writeFile(`${JSON.stringify({ ...creds, generation: randomUUID() }, null, 2)}
333
+ `);
334
+ await handle.sync();
335
+ } finally {
336
+ await handle.close();
337
+ }
338
+ await rename(temporary, file);
339
+ } finally {
340
+ await rm(temporary, { force: true });
341
+ }
342
+ }
343
+ async function withCredentialLock(action) {
344
+ const dir = carrierHome();
345
+ await mkdir2(dir, { recursive: true, mode: 448 });
346
+ await chmod(dir, 448);
347
+ const lock = join5(dir, "credentials.lock");
348
+ const deadline = Date.now() + 31e4;
349
+ while (true) {
350
+ try {
351
+ await mkdir2(lock, { mode: 448 });
352
+ break;
353
+ } catch (error) {
354
+ if (error.code !== "EEXIST") throw error;
355
+ if (Date.now() >= deadline) throw new Error("Carrier credential owner is busy; retry or recover its abandoned lock.");
356
+ await new Promise((resolve5) => setTimeout(resolve5, 25));
357
+ }
358
+ }
359
+ let released = false;
360
+ const release = () => {
361
+ if (released) return;
362
+ released = true;
363
+ rmSync(lock, { recursive: true, force: true });
364
+ };
365
+ const onSignal = () => {
366
+ release();
367
+ process.exit(1);
368
+ };
369
+ process.once("SIGINT", onSignal);
370
+ process.once("SIGTERM", onSignal);
371
+ try {
372
+ return await action();
373
+ } finally {
374
+ process.removeListener("SIGINT", onSignal);
375
+ process.removeListener("SIGTERM", onSignal);
376
+ if (!released) await rm(lock, { recursive: true });
377
+ }
311
378
  }
312
379
  async function clearCredentials() {
313
380
  await rm(credentialsPath(), { force: true });
@@ -316,11 +383,403 @@ function isExpired(creds, skewMs = 6e4) {
316
383
  if (!creds.expiresAt) return false;
317
384
  return Date.now() >= creds.expiresAt - skewMs;
318
385
  }
386
+ function redactCredential(value, token) {
387
+ if (typeof value === "string") return value.split(token).join("***");
388
+ if (Array.isArray(value)) return value.map((item) => redactCredential(item, token));
389
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key.split(token).join("***"), redactCredential(item, token)]));
390
+ return value;
391
+ }
319
392
  function redact(text3) {
320
- return text3.replace(/\b(ak_|sk_|rt_)[A-Za-z0-9._-]{6,}/g, "$1***").replace(/\bBearer\s+[A-Za-z0-9._-]{8,}/gi, "Bearer ***");
393
+ return text3.replace(/\bclerk_[^\s:"<>]+:[^\s:"<>]+:[^\s"<>]+/g, "clerk_***").replace(/\b(ak_|sk_|rt_)[A-Za-z0-9._-]{6,}/g, "$1***").replace(/\bBearer\s+[A-Za-z0-9._-]{8,}/gi, "Bearer ***");
394
+ }
395
+
396
+ // src/cli/lib/mcp-oauth.ts
397
+ var CLAUDE_OWNER_VERSIONS = package_default.carrierCompatibility.claudeOAuthOwnerVersions;
398
+ function usableOAuth(creds) {
399
+ if (creds.issuer !== MCP_HOME || !Number.isFinite(creds.expiresAt) || creds.expiresAt <= 0) return false;
400
+ if (Object.hasOwn(creds, "resource") && creds.resource !== MCP_URL) return false;
401
+ if (typeof creds.scope !== "string" || !creds.scope.split(/\s+/).includes("read")) return false;
402
+ if (typeof creds.accessToken !== "string" || !creds.accessToken || /[\s\0]/.test(creds.accessToken)) return false;
403
+ const account = process.env.CARRIER_OAUTH_ACCOUNT?.trim();
404
+ if (account && creds.accessToken.split(":")[0] !== `clerk_${account}`) return false;
405
+ return true;
406
+ }
407
+ async function privateText(file, privateMode = true) {
408
+ try {
409
+ const handle = await open2(file, constants2.O_RDONLY | constants2.O_NOFOLLOW);
410
+ try {
411
+ const stat = await handle.stat();
412
+ if (!stat.isFile() || stat.size > 1024 * 1024 || privateMode && process.platform !== "win32" && stat.mode & 63 || process.getuid && stat.uid !== process.getuid()) return null;
413
+ return await handle.readFile("utf8");
414
+ } finally {
415
+ await handle.close();
416
+ }
417
+ } catch {
418
+ return null;
419
+ }
420
+ }
421
+ async function privateJson(file) {
422
+ try {
423
+ const text3 = await privateText(file);
424
+ return text3 ? JSON.parse(text3) : null;
425
+ } catch {
426
+ return null;
427
+ }
428
+ }
429
+ function oauthBinding(creds, selection) {
430
+ const parts = creds.accessToken.split(":");
431
+ const subject = /^clerk_[^:\s]+:[^:\s]+:[^:\s]+$/.test(creds.accessToken) ? parts.slice(0, 2) : [createHash("sha256").update(creds.accessToken).digest("hex")];
432
+ return createHash("sha256").update(JSON.stringify([selection, creds.issuer, creds.resource, subject])).digest("hex");
433
+ }
434
+ function validImport(creds) {
435
+ if (!/^clerk_[^:\s]+:[^:\s]+:[^:\s]+$/.test(creds.accessToken ?? "")) return null;
436
+ return usableOAuth(creds) && !isExpired(creds) ? creds : null;
437
+ }
438
+ function hermesServerConfig(text3, server) {
439
+ try {
440
+ return JSON.parse(text3)?.mcp_servers?.[server] ?? null;
441
+ } catch {
442
+ }
443
+ let inside = false, selected = false;
444
+ let sectionIndent = -1, serverIndent = -1;
445
+ const entry = {};
446
+ for (const line of text3.split(/\r?\n/)) {
447
+ if (!line.trim() || line.trimStart().startsWith("#")) continue;
448
+ if (/\t/.test(line)) return null;
449
+ const match = /^( *)([^:]+):\s*(.*?)\s*$/.exec(line);
450
+ if (!match) {
451
+ if (inside) return null;
452
+ continue;
453
+ }
454
+ const indent = match[1].length, key = match[2], value = match[3];
455
+ if (!inside) {
456
+ if (key === "mcp_servers" && value === "") {
457
+ inside = true;
458
+ sectionIndent = indent;
459
+ }
460
+ continue;
461
+ }
462
+ if (indent <= sectionIndent) break;
463
+ if (serverIndent < 0) serverIndent = indent;
464
+ if (indent === serverIndent) {
465
+ selected = key === server || key === JSON.stringify(server) || key === `'${server}'`;
466
+ if (selected && value) return null;
467
+ continue;
468
+ }
469
+ if (!selected) continue;
470
+ if (!value || /^[&*!>|\[{]/.test(value)) return null;
471
+ if (Object.hasOwn(entry, key)) return null;
472
+ let scalar = value;
473
+ if (value.startsWith('"')) {
474
+ try {
475
+ scalar = JSON.parse(value);
476
+ } catch {
477
+ return null;
478
+ }
479
+ } else if (value.startsWith("'")) {
480
+ if (!value.endsWith("'")) return null;
481
+ scalar = value.slice(1, -1).replace(/''/g, "'");
482
+ } else scalar = value.replace(/\s+#.*$/, "");
483
+ entry[key] = scalar;
484
+ }
485
+ return Object.keys(entry).length ? entry : null;
486
+ }
487
+ async function readHermesOAuth(server) {
488
+ if (!/^[\w-]{1,128}$/.test(server)) return null;
489
+ const home = process.env.HERMES_HOME?.trim() || join6(homedir3(), ".hermes");
490
+ const text3 = await privateText(join6(home, "config.yaml"), false);
491
+ const config = text3 && hermesServerConfig(text3, server);
492
+ if (!config || config.url !== MCP_URL || config.command || config.headers || config.type && config.type !== "http" && config.type !== "streamable-http") return null;
493
+ const dir = join6(home, "mcp-tokens");
494
+ const [token, client, meta] = await Promise.all([privateJson(join6(dir, `${server}.json`)), privateJson(join6(dir, `${server}.client.json`)), privateJson(join6(dir, `${server}.meta.json`))]);
495
+ if (!token || !client || !meta || token.hermes_issuer !== MCP_HOME || meta.issuer !== MCP_HOME) return null;
496
+ if (typeof client.client_id !== "string" || !client.client_id || token.client_id && token.client_id !== client.client_id) return null;
497
+ if (typeof token.token_type !== "string" || token.token_type.toLowerCase() !== "bearer") return null;
498
+ for (const field of ["authorization_endpoint", "token_endpoint"]) {
499
+ try {
500
+ if (new URL(String(meta[field])).origin !== MCP_HOME) return null;
501
+ } catch {
502
+ return null;
503
+ }
504
+ }
505
+ const hasResource = Object.hasOwn(token, "resource");
506
+ if (hasResource && token.resource !== MCP_URL) return null;
507
+ if (typeof token.expires_at !== "number") return null;
508
+ const creds = validImport({ issuer: MCP_HOME, ...hasResource ? { resource: token.resource } : {}, origin: "hermes-mcp", accessToken: token.access_token, expiresAt: token.expires_at * 1e3, scope: token.scope });
509
+ if (creds) creds.binding = oauthBinding(creds, [
510
+ "hermes",
511
+ resolve2(home),
512
+ server,
513
+ config,
514
+ client,
515
+ meta,
516
+ token.organization_id,
517
+ token.org_id,
518
+ token.account_id,
519
+ token.user_id
520
+ ]);
521
+ return creds;
522
+ }
523
+ function configuredClaudeDir() {
524
+ const value = process.env.CLAUDE_CONFIG_DIR;
525
+ return value && value.trim() ? value : void 0;
526
+ }
527
+ function claudeKeychainSelection() {
528
+ const custom = process.env.CLAUDE_CODE_CUSTOM_OAUTH_URL;
529
+ if (custom && !["https://beacon.claude-ai.staging.ant.dev", "https://claude.fedstart.com", "https://claude-staging.fedstart.com"].includes(custom.replace(/\/$/, ""))) return null;
530
+ const configured = (configuredClaudeDir() ?? join6(homedir3(), ".claude")).normalize("NFC");
531
+ const secure = process.env.CLAUDE_SECURESTORAGE_CONFIG_DIR;
532
+ const home = (secure !== void 0 ? secure || join6(homedir3(), ".claude") : configured).normalize("NFC");
533
+ const isDefault = secure !== void 0 ? !secure : !configuredClaudeDir();
534
+ const suffix = isDefault ? "" : `-${createHash("sha256").update(secure !== void 0 ? secure.normalize("NFC") : configured).digest("hex").slice(0, 8)}`;
535
+ let account;
536
+ try {
537
+ account = process.env.USER || userInfo().username;
538
+ } catch {
539
+ account = "claude-code-user";
540
+ }
541
+ if (!/^[a-zA-Z0-9._-]+$/.test(account)) account = "claude-code-user";
542
+ return { service: `Claude Code${custom ? "-custom-oauth" : ""}-credentials${suffix}`, account, home };
543
+ }
544
+ async function claudeResolvedConfig(server) {
545
+ const configDir = (configuredClaudeDir() ?? join6(homedir3(), ".claude")).normalize("NFC");
546
+ let file = join6(configuredClaudeDir() || homedir3(), `.claude${process.env.CLAUDE_CODE_CUSTOM_OAUTH_URL ? "-custom-oauth" : ""}.json`);
547
+ try {
548
+ await lstat(join6(configDir, ".config.json"));
549
+ file = join6(configDir, ".config.json");
550
+ } catch (e) {
551
+ if (e.code !== "ENOENT") return null;
552
+ }
553
+ const json = await privateJson(file);
554
+ if (!json) return null;
555
+ const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
556
+ const readOptional = async (path) => {
557
+ try {
558
+ await lstat(path);
559
+ const text3 = await privateText(path, false);
560
+ if (text3 === null) return null;
561
+ const value = JSON.parse(text3);
562
+ return object(value) ? value : null;
563
+ } catch (e) {
564
+ return e.code === "ENOENT" ? void 0 : null;
565
+ }
566
+ };
567
+ let cwd;
568
+ try {
569
+ cwd = await realpath(process.cwd());
570
+ } catch {
571
+ return null;
572
+ }
573
+ let workspace = cwd;
574
+ for (let dir = cwd; ; ) {
575
+ try {
576
+ await lstat(join6(dir, ".git"));
577
+ const { stdout } = await promisify(execFile)("git", ["-C", cwd, "rev-parse", "--show-toplevel"], { timeout: 3e3, maxBuffer: 8192 });
578
+ workspace = await realpath(stdout.trim());
579
+ break;
580
+ } catch (e) {
581
+ if (e.code !== "ENOENT") return null;
582
+ }
583
+ const parent = dirname2(dir);
584
+ if (parent === dir) break;
585
+ dir = parent;
586
+ }
587
+ const managedDir = process.platform === "darwin" ? "/Library/Application Support/ClaudeCode" : "/etc/claude-code";
588
+ for (const path of [join6(managedDir, "managed-mcp.json"), join6(managedDir, "managed-settings.json"), join6(configDir, "managed-mcp.json"), join6(configDir, "managed-settings.json")]) {
589
+ try {
590
+ await lstat(path);
591
+ return null;
592
+ } catch (e) {
593
+ if (e.code !== "ENOENT") return null;
594
+ }
595
+ }
596
+ const policyFields = ["managedMcpServers", "allowedMcpServers", "deniedMcpServers", "mcpServers", "env", "enabledSettingsSources", "allowManagedMcpServersOnly"];
597
+ if (["managedMcpServers", "allowedMcpServers", "deniedMcpServers"].some((k) => Object.hasOwn(json, k))) return null;
598
+ for (const path of [join6(configDir, "settings.json"), join6(workspace, ".claude/settings.json"), join6(workspace, ".claude/settings.local.json")]) {
599
+ const settings = await readOptional(path);
600
+ if (settings === null || settings && policyFields.some((k) => Object.hasOwn(settings, k))) return null;
601
+ }
602
+ const projects = json.projects;
603
+ if (projects !== void 0 && !object(projects)) return null;
604
+ const local = object(projects) ? projects[workspace] : void 0;
605
+ if (local !== void 0 && !object(local)) return null;
606
+ const userServers = json.mcpServers;
607
+ const localServers = object(local) ? local.mcpServers : void 0;
608
+ if (userServers !== void 0 && !object(userServers) || localServers !== void 0 && !object(localServers)) return null;
609
+ let config = object(userServers) ? userServers[server] : void 0;
610
+ let scope = "user", selectedFile = file;
611
+ const hasLocal = object(localServers) && Object.hasOwn(localServers, server);
612
+ let project, projectFile;
613
+ const dirs = [];
614
+ for (let dir = cwd; ; ) {
615
+ const parent = dirname2(dir);
616
+ if (parent === dir) break;
617
+ dirs.push(dir);
618
+ dir = parent;
619
+ }
620
+ for (const dir of dirs.reverse()) {
621
+ const path = join6(dir, ".mcp.json"), value = await readOptional(path);
622
+ if (value === null) return null;
623
+ if (!value) continue;
624
+ if (!object(value.mcpServers)) return null;
625
+ if (Object.hasOwn(value.mcpServers, server)) {
626
+ project = value.mcpServers[server];
627
+ projectFile = path;
628
+ }
629
+ }
630
+ if (projectFile && !hasLocal) {
631
+ const settings = await readOptional(join6(configDir, "settings.json"));
632
+ if (settings === null || !object(local) || local.hasTrustDialogAccepted !== true) return null;
633
+ const disabled = settings?.disabledMcpjsonServers;
634
+ const enabled = settings?.enabledMcpjsonServers;
635
+ if (disabled !== void 0 && (!Array.isArray(disabled) || disabled.some((v) => typeof v !== "string"))) return null;
636
+ if (enabled !== void 0 && (!Array.isArray(enabled) || enabled.some((v) => typeof v !== "string"))) return null;
637
+ if (Array.isArray(disabled) && disabled.includes(server)) return null;
638
+ if (settings?.enableAllProjectMcpServers !== true && !(Array.isArray(enabled) && enabled.includes(server))) return null;
639
+ for (const path of [join6(workspace, ".claude/settings.json"), join6(workspace, ".claude/settings.local.json")]) {
640
+ const extra = await readOptional(path);
641
+ if (extra === null || extra && ["env", "enabledMcpjsonServers", "disabledMcpjsonServers", "enableAllProjectMcpServers"].some((k) => Object.hasOwn(extra, k))) return null;
642
+ }
643
+ config = project;
644
+ scope = "project";
645
+ selectedFile = projectFile;
646
+ }
647
+ if (hasLocal) {
648
+ config = localServers[server];
649
+ scope = "local";
650
+ }
651
+ if (!object(config) || config.url !== MCP_URL || config.command || config.type !== void 0 && config.type !== "http" && config.type !== "sse") return null;
652
+ if (config.headers !== void 0 && !object(config.headers)) return null;
653
+ for (const [key, value] of Object.entries(config.headers ?? {})) {
654
+ if (typeof value !== "string" || /authorization|api[-_]?key|token|cookie/i.test(key) || /\$\{|[\r\n\0]/.test(value)) return null;
655
+ }
656
+ return { config, file: selectedFile, scope, cwd: workspace };
657
+ }
658
+ async function readClaudeKeychain(request) {
659
+ return new Promise((resolveResult) => {
660
+ const child = spawn2(
661
+ "/usr/bin/osascript",
662
+ ["-l", "JavaScript", join6(packageRoot(), "native/claude-keychain.js")],
663
+ { stdio: ["pipe", "ignore", "ignore", "pipe"], env: { PATH: "/usr/bin:/bin" } }
664
+ );
665
+ let text3 = "", failed = false;
666
+ const pipe = child.stdio[3];
667
+ if (!pipe || !("on" in pipe)) {
668
+ child.kill();
669
+ resolveResult({ status: "unavailable" });
670
+ return;
671
+ }
672
+ const timer = setTimeout(() => {
673
+ failed = true;
674
+ child.kill();
675
+ }, 5e3);
676
+ pipe.on("data", (data) => {
677
+ if (text3.length + data.length > 65536) {
678
+ failed = true;
679
+ child.kill();
680
+ } else text3 += data.toString("utf8");
681
+ });
682
+ child.once("error", () => {
683
+ failed = true;
684
+ });
685
+ child.stdin?.on("error", () => {
686
+ failed = true;
687
+ });
688
+ child.once("close", (code) => {
689
+ clearTimeout(timer);
690
+ if (failed || code !== 0) {
691
+ resolveResult({ status: "unavailable" });
692
+ return;
693
+ }
694
+ try {
695
+ const result = JSON.parse(text3);
696
+ if (!["ok", "absent", "absent-entry", "locked", "denied", "malformed", "unavailable"].includes(result.status)) throw new Error();
697
+ resolveResult(result);
698
+ } catch {
699
+ resolveResult({ status: "malformed" });
700
+ }
701
+ });
702
+ child.stdin?.end(JSON.stringify(request));
703
+ });
704
+ }
705
+ async function claudeVersion() {
706
+ try {
707
+ const { stdout } = await promisify(execFile)("claude", ["--version"], { timeout: 3e3, maxBuffer: 1024 });
708
+ const version = stdout.trim().replace(/ \(Claude Code\)$/, "");
709
+ return CLAUDE_OWNER_VERSIONS.includes(version) ? version : null;
710
+ } catch {
711
+ return null;
712
+ }
713
+ }
714
+ async function readClaudeOwner(server, options = {}) {
715
+ if (!/^[a-zA-Z0-9_-]{1,128}$/.test(server)) return { status: "unsupported-config" };
716
+ const selection = claudeKeychainSelection(), resolved = await claudeResolvedConfig(server);
717
+ if (!selection || !resolved) return { status: "unsupported-config" };
718
+ const hash = createHash("sha256").update(JSON.stringify({ type: resolved.config.type, url: resolved.config.url, headers: resolved.config.headers || {} })).digest("hex").slice(0, 16);
719
+ const key = `${server}|${hash}`, explicitFile = process.env.CARRIER_MCP_OAUTH_FILE;
720
+ let entry;
721
+ let source;
722
+ if ((options.platform ?? process.platform) === "darwin" && !explicitFile) {
723
+ if (!CLAUDE_OWNER_VERSIONS.includes(await (options.version ?? claudeVersion)() ?? "")) return { status: "unsupported-version" };
724
+ const result = await (options.native ?? readClaudeKeychain)({ ...selection, key });
725
+ if (result.status === "ok") {
726
+ if (!result.entry) return { status: "malformed" };
727
+ entry = result.entry;
728
+ source = ["keychain", selection.service, selection.account, selection.home];
729
+ } else if (result.status !== "absent") return { status: result.status };
730
+ }
731
+ if (!entry) {
732
+ const file = explicitFile || join6(selection.home, ".credentials.json");
733
+ const json = await privateJson(file);
734
+ entry = json?.mcpOAuth?.[key];
735
+ source = ["file", resolve2(file)];
736
+ }
737
+ if (!entry || entry.serverName !== server || entry.serverUrl !== MCP_URL) return { status: "absent-entry" };
738
+ const discovery = entry.discoveryState;
739
+ const issuer = entry.issuer ?? discovery?.authorizationServerUrl;
740
+ if (entry.issuer !== void 0 && discovery?.authorizationServerUrl !== void 0 && entry.issuer !== discovery.authorizationServerUrl || entry.resource !== void 0 && entry.resource !== MCP_URL || discovery?.resourceMetadataUrl !== void 0 && discovery.resourceMetadataUrl !== `${MCP_HOME}/.well-known/oauth-protected-resource/mcp`) return { status: "malformed" };
741
+ const creds = validImport({ issuer, resource: MCP_URL, origin: "claude-mcp", accessToken: entry.accessToken, expiresAt: entry.expiresAt, scope: entry.scope });
742
+ if (!creds) return { status: "malformed" };
743
+ creds.binding = oauthBinding(creds, [
744
+ "claude",
745
+ source,
746
+ resolve2(resolved.file),
747
+ resolved.scope,
748
+ resolved.cwd,
749
+ server,
750
+ hash,
751
+ entry.clientId,
752
+ entry.organizationId,
753
+ entry.organization_id,
754
+ entry.orgId,
755
+ entry.accountId,
756
+ discovery?.authorizationServerUrl,
757
+ discovery?.resourceMetadataUrl
758
+ ]);
759
+ return { status: "ok", credentials: creds };
760
+ }
761
+ async function readClaudeOAuth(server) {
762
+ return (await readClaudeOwner(server)).credentials ?? null;
763
+ }
764
+ async function readMcpOAuth(selectedOwner) {
765
+ const server = process.env.CARRIER_MCP_OAUTH_SERVER?.trim() || "carrier";
766
+ const owner = process.env.CARRIER_MCP_OAUTH_OWNER?.trim();
767
+ if (owner && owner !== "hermes" && owner !== "claude") return null;
768
+ if (selectedOwner) {
769
+ if (owner && owner !== selectedOwner || selectedOwner === "hermes" && process.env.CARRIER_MCP_OAUTH_FILE) return null;
770
+ return selectedOwner === "hermes" ? readHermesOAuth(server) : readClaudeOAuth(server);
771
+ }
772
+ if (owner === "hermes") return readHermesOAuth(server);
773
+ if (owner === "claude" || process.env.CARRIER_MCP_OAUTH_FILE) return readClaudeOAuth(server);
774
+ const claude = await readClaudeOwner(server);
775
+ if (["locked", "denied", "malformed", "unavailable", "unsupported-version"].includes(claude.status)) return null;
776
+ const owners = [await readHermesOAuth(server), claude.credentials ?? null].filter((c) => c !== null);
777
+ return owners.length === 1 ? owners[0] : null;
321
778
  }
322
779
 
323
780
  // src/cli/lib/auth.ts
781
+ import { access, writeFile as writeFile3, rm as rm2 } from "fs/promises";
782
+ import { join as join7, resolve as resolve3 } from "path";
324
783
  var DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
325
784
  var CLIENT_NAME = "Carrier CLI";
326
785
  var DEFAULT_SCOPE = "read write";
@@ -334,6 +793,11 @@ async function discover(base = MCP_HOME) {
334
793
  if (!meta.authorization_endpoint || !meta.token_endpoint) {
335
794
  throw new Error("OAuth discovery returned no authorization or token endpoint");
336
795
  }
796
+ if (meta.issuer !== MCP_HOME) throw new Error("OAuth issuer does not match Carrier");
797
+ for (const endpoint of [meta.authorization_endpoint, meta.token_endpoint, meta.registration_endpoint].filter(Boolean)) {
798
+ const url = new URL(endpoint);
799
+ if (url.origin !== MCP_HOME || url.username || url.password) throw new Error("OAuth endpoint does not match Carrier");
800
+ }
337
801
  return meta;
338
802
  }
339
803
  function base64url(buf) {
@@ -341,7 +805,7 @@ function base64url(buf) {
341
805
  }
342
806
  function pkcePair() {
343
807
  const verifier = base64url(randomBytes(32));
344
- const challenge = base64url(createHash("sha256").update(verifier).digest());
808
+ const challenge = base64url(createHash2("sha256").update(verifier).digest());
345
809
  return { verifier, challenge };
346
810
  }
347
811
  function sameState(a, b) {
@@ -368,8 +832,7 @@ async function registerClient(meta, redirectUri) {
368
832
  })
369
833
  });
370
834
  if (!res.ok) {
371
- const body = (await res.text().catch(() => "")).slice(0, 200);
372
- throw new Error(`Client registration failed (${res.status}). ${body}`);
835
+ throw new Error(`Client registration failed (${res.status}).`);
373
836
  }
374
837
  const json = await res.json();
375
838
  if (!json.client_id) throw new Error("Client registration returned no client_id");
@@ -400,8 +863,8 @@ async function startCallbackServer() {
400
863
  const state = url.searchParams.get("state") ?? "";
401
864
  if (err) {
402
865
  res.writeHead(400, { "Content-Type": "text/html" });
403
- res.end(page("Sign-in failed", err));
404
- rejectCode?.(new Error(`Authorization server returned "${err}"`));
866
+ res.end(page("Sign-in failed", "Authorization was rejected."));
867
+ rejectCode?.(new Error("Authorization server rejected sign-in."));
405
868
  return;
406
869
  }
407
870
  if (!code || !expected || !sameState(state, expected)) {
@@ -418,11 +881,11 @@ async function startCallbackServer() {
418
881
  let lastErr;
419
882
  for (const port2 of CALLBACK_PORTS) {
420
883
  try {
421
- await new Promise((resolve3, reject) => {
884
+ await new Promise((resolve5, reject) => {
422
885
  server.once("error", reject);
423
886
  server.listen(port2, "127.0.0.1", () => {
424
887
  server.removeListener("error", reject);
425
- resolve3();
888
+ resolve5();
426
889
  });
427
890
  });
428
891
  bound = true;
@@ -443,14 +906,14 @@ async function startCallbackServer() {
443
906
  // browser would hold the event loop open and the CLI would never exit.
444
907
  waitForCode: (expectedState, timeoutMs) => {
445
908
  expected = expectedState;
446
- return new Promise((resolve3, reject) => {
909
+ return new Promise((resolve5, reject) => {
447
910
  const timer = setTimeout(
448
911
  () => reject(new Error(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for the browser`)),
449
912
  timeoutMs
450
913
  );
451
914
  resolveCode = (code) => {
452
915
  clearTimeout(timer);
453
- resolve3(code);
916
+ resolve5(code);
454
917
  };
455
918
  rejectCode = (err) => {
456
919
  clearTimeout(timer);
@@ -475,14 +938,20 @@ async function postToken(meta, body) {
475
938
  });
476
939
  const json = await res.json().catch(() => ({}));
477
940
  if (!res.ok || json.error) {
478
- throw new Error(json.error_description || json.error || `Token endpoint returned ${res.status}`);
941
+ throw new Error(`OAuth token exchange rejected (${res.status}).`);
479
942
  }
480
943
  if (!json.access_token) throw new Error("Token endpoint returned no access_token");
944
+ if (Object.hasOwn(json, "resource")) {
945
+ const resources = Array.isArray(json.resource) ? json.resource : [json.resource];
946
+ if (resources.length !== 1 || resources[0] !== MCP_URL) throw new Error("OAuth token resource does not match Carrier.");
947
+ json.resource = MCP_URL;
948
+ }
481
949
  return json;
482
950
  }
483
951
  function store(meta, token, clientId) {
484
952
  return {
485
953
  issuer: meta.issuer,
954
+ ...Object.hasOwn(token, "resource") ? { resource: token.resource } : {},
486
955
  accessToken: token.access_token,
487
956
  refreshToken: token.refresh_token,
488
957
  expiresAt: Date.now() + (token.expires_in ?? 900) * 1e3,
@@ -492,65 +961,140 @@ function store(meta, token, clientId) {
492
961
  }
493
962
  async function login(opts) {
494
963
  const meta = await discover();
495
- const server = await startCallbackServer();
496
- try {
497
- const existing = await readCredentials();
498
- const clientId = existing?.clientId && existing.issuer === meta.issuer ? existing.clientId : await registerClient(meta, server.redirectUri);
499
- const { verifier, challenge } = pkcePair();
500
- const state = base64url(randomBytes(16));
501
- const authUrl = new URL(meta.authorization_endpoint);
502
- authUrl.searchParams.set("response_type", "code");
503
- authUrl.searchParams.set("client_id", clientId);
504
- authUrl.searchParams.set("redirect_uri", server.redirectUri);
505
- authUrl.searchParams.set("scope", pickScope(meta));
506
- authUrl.searchParams.set("state", state);
507
- authUrl.searchParams.set("code_challenge", challenge);
508
- authUrl.searchParams.set("code_challenge_method", "S256");
509
- const pendingCode = server.waitForCode(state, opts.timeoutMs ?? 3e5);
510
- pendingCode.catch(() => void 0);
511
- opts.onUrl?.(authUrl.toString());
512
- await opts.openBrowser(authUrl.toString());
513
- const code = await pendingCode;
514
- const token = await postToken(meta, {
515
- grant_type: "authorization_code",
516
- code,
517
- redirect_uri: server.redirectUri,
518
- client_id: clientId,
519
- code_verifier: verifier
520
- });
521
- await writeCredentials(store(meta, token, clientId));
522
- return { ok: true, message: "Signed in.", scope: token.scope };
523
- } finally {
524
- server.close();
525
- }
964
+ return withCredentialLock(async () => {
965
+ const server = await startCallbackServer();
966
+ try {
967
+ const existing = await readCredentials();
968
+ const clientId = existing?.clientId && existing.issuer === meta.issuer && existing.redirectUri === server.redirectUri ? existing.clientId : await registerClient(meta, server.redirectUri);
969
+ const { verifier, challenge } = pkcePair();
970
+ const state = base64url(randomBytes(16));
971
+ const authUrl = new URL(meta.authorization_endpoint);
972
+ authUrl.searchParams.set("response_type", "code");
973
+ authUrl.searchParams.set("resource", MCP_URL);
974
+ authUrl.searchParams.set("client_id", clientId);
975
+ authUrl.searchParams.set("redirect_uri", server.redirectUri);
976
+ authUrl.searchParams.set("scope", pickScope(meta));
977
+ authUrl.searchParams.set("state", state);
978
+ authUrl.searchParams.set("code_challenge", challenge);
979
+ authUrl.searchParams.set("code_challenge_method", "S256");
980
+ const pendingCode = server.waitForCode(state, opts.timeoutMs ?? 3e5);
981
+ pendingCode.catch(() => void 0);
982
+ opts.onUrl?.(authUrl.toString());
983
+ await opts.openBrowser(authUrl.toString());
984
+ const code = await pendingCode;
985
+ const token = await postToken(meta, {
986
+ grant_type: "authorization_code",
987
+ code,
988
+ redirect_uri: server.redirectUri,
989
+ client_id: clientId,
990
+ code_verifier: verifier,
991
+ resource: MCP_URL
992
+ });
993
+ const next = { ...store(meta, token, clientId), redirectUri: server.redirectUri };
994
+ if (!usableOAuth(next)) throw new Error("OAuth endpoint returned an unusable Carrier session.");
995
+ await writeCredentials(next);
996
+ await rm2(join7(carrierHome(), "mcp-oauth-disabled"), { force: true });
997
+ return { ok: true, message: "Signed in.", scope: token.scope };
998
+ } finally {
999
+ server.close();
1000
+ }
1001
+ });
526
1002
  }
527
1003
  function pickScope(meta) {
528
1004
  const supported = meta.scopes_supported ?? [];
529
1005
  if (!supported.length) return DEFAULT_SCOPE;
530
1006
  return DEFAULT_SCOPE.split(" ").filter((s) => supported.includes(s)).join(" ") || supported[0];
531
1007
  }
532
- async function getAccessToken() {
1008
+ function inheritedOAuth() {
1009
+ const api = process.env.CARRIER_API_KEY?.trim();
1010
+ if (!api || process.env.CARRIER_OAUTH_HANDOFF !== createHash2("sha256").update(api).digest("hex")) return null;
1011
+ const origin = process.env.CARRIER_OAUTH_ORIGIN, binding = process.env.CARRIER_OAUTH_BINDING;
1012
+ if (!origin || !["native-cli", "hermes-mcp", "claude-mcp"].includes(origin) || !/^[a-f0-9]{64}$/.test(binding ?? "")) return false;
1013
+ return { origin, binding };
1014
+ }
1015
+ function nativeBinding(creds) {
1016
+ return oauthBinding(creds, ["native-cli", resolve3(carrierHome()), creds.clientId]);
1017
+ }
1018
+ async function selectedMcpOAuth(handoff) {
1019
+ if (handoff === false || handoff && handoff.origin === "native-cli") return null;
1020
+ const owner = await readMcpOAuth(handoff ? handoff.origin === "hermes-mcp" ? "hermes" : "claude" : void 0);
1021
+ return owner && (!handoff || owner.binding === handoff.binding) ? owner : null;
1022
+ }
1023
+ async function getAccessTokenLocked() {
1024
+ const handoff = inheritedOAuth();
1025
+ if (handoff === false) return null;
533
1026
  const creds = await readCredentials();
534
- if (!creds) return null;
1027
+ if (creds && handoff && (handoff.origin !== "native-cli" || nativeBinding(creds) !== handoff.binding)) return null;
1028
+ if (!creds) {
1029
+ try {
1030
+ await access(join7(carrierHome(), "credentials.json"));
1031
+ return null;
1032
+ } catch (error) {
1033
+ if (error.code !== "ENOENT") return null;
1034
+ }
1035
+ try {
1036
+ await access(join7(carrierHome(), "mcp-oauth-disabled"));
1037
+ return null;
1038
+ } catch (error) {
1039
+ if (error.code !== "ENOENT") return null;
1040
+ }
1041
+ return (await selectedMcpOAuth(handoff))?.accessToken ?? null;
1042
+ }
1043
+ if (!usableOAuth(creds)) return null;
535
1044
  if (!isExpired(creds)) return creds.accessToken;
536
1045
  if (!creds.refreshToken || !creds.clientId) return null;
537
1046
  try {
538
1047
  const meta = await discover();
1048
+ if (meta.issuer !== creds.issuer) return null;
539
1049
  const token = await postToken(meta, {
540
1050
  grant_type: "refresh_token",
1051
+ resource: MCP_URL,
541
1052
  refresh_token: creds.refreshToken,
542
1053
  client_id: creds.clientId
543
1054
  });
544
1055
  const next = store(meta, token, creds.clientId);
545
1056
  if (!next.refreshToken) next.refreshToken = creds.refreshToken;
1057
+ if (!next.scope) next.scope = creds.scope;
1058
+ if (!Object.hasOwn(next, "resource") && Object.hasOwn(creds, "resource")) next.resource = creds.resource;
1059
+ if (!usableOAuth(next) || handoff && nativeBinding(next) !== handoff.binding) return null;
1060
+ next.redirectUri = creds.redirectUri;
1061
+ const latest = await readCredentials();
1062
+ if (latest?.generation !== creds.generation || latest?.accessToken !== creds.accessToken) return null;
546
1063
  await writeCredentials(next);
547
1064
  return next.accessToken;
548
1065
  } catch {
549
1066
  return null;
550
1067
  }
551
1068
  }
1069
+ function resolveExplicitCredential(opts = {}) {
1070
+ const api = process.env.CARRIER_API_KEY?.trim();
1071
+ const handedOff = api && process.env.CARRIER_OAUTH_HANDOFF === createHash2("sha256").update(api).digest("hex");
1072
+ if (api && !handedOff) return { token: api, kind: "api-key" };
1073
+ const org = process.env.CARRIER_ORG_API_KEY?.trim();
1074
+ if (org) return { token: org, kind: "api-key" };
1075
+ if (opts.allowOcs) {
1076
+ const ocs = process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim();
1077
+ if (ocs) return { token: ocs, kind: "ocs-token" };
1078
+ }
1079
+ return null;
1080
+ }
1081
+ async function resolveCredential(opts = {}) {
1082
+ const explicit = resolveExplicitCredential(opts);
1083
+ if (explicit) return explicit;
1084
+ return withCredentialLock(async () => {
1085
+ const token = await getAccessTokenLocked();
1086
+ if (!token) return null;
1087
+ const native = await readCredentials();
1088
+ const owner = native ?? await selectedMcpOAuth(inheritedOAuth());
1089
+ if (!owner || owner.accessToken !== token) return null;
1090
+ return { token, kind: "oauth", origin: native ? "native-cli" : owner.origin, expiresAt: owner.expiresAt, binding: native ? nativeBinding(native) : owner.binding };
1091
+ });
1092
+ }
552
1093
  async function logout() {
553
- await clearCredentials();
1094
+ await withCredentialLock(async () => {
1095
+ await writeFile3(join7(carrierHome(), "mcp-oauth-disabled"), "disabled\n", { mode: 384 });
1096
+ await clearCredentials();
1097
+ });
554
1098
  }
555
1099
 
556
1100
  // src/cli/lib/guidance.ts
@@ -572,13 +1116,15 @@ function oauthFirstUseNote() {
572
1116
  " Token: none required in Claude / Cursor / Windsurf",
573
1117
  "",
574
1118
  "When you first talk to Carrier in your MCP client, the browser opens",
575
- "sign-in/sign-up. After authorize, tools work. Headless? Use an org API key",
576
- `(ak_\u2026) from ${CONSOLE_URL} \u2192 Settings \u2192 API Keys.`
1119
+ "sign-in/sign-up. After authorize, tools work. Headless: carrier login,",
1120
+ "or reuse compatible Claude Code Carrier OAuth. No manual key export needed.",
1121
+ "To export CARRIER_API_KEY into a child terminal: carrier exec -- bash.",
1122
+ `For unattended CI use an org key from ${CONSOLE_URL} \u2192 Settings \u2192 API Keys.`
577
1123
  ].join("\n");
578
1124
  }
579
1125
  function formatStatusBlock(st) {
580
1126
  const yn = (ok2, yes = "yes", no = "no") => ok2 ? pc.green(yes) : pc.yellow(no);
581
- const authLabel = st.authMode === "oauth-first-use" ? pc.cyan("OAuth on first use (no token needed)") : st.authMode === "api-key" ? pc.green("org API key in env (CARRIER_API_KEY)") : st.authMode === "ocs-token" ? pc.green("OCS token in env") : pc.yellow("not ready \u2014 install MCP or open sign-up");
1127
+ const authLabel = st.authMode === "oauth" ? pc.green("OAuth session ready for CLI / Ask the fleet") : st.authMode === "oauth-first-use" ? pc.cyan("OAuth on first use (no token needed)") : st.authMode === "api-key" ? pc.green("org API key in env (CARRIER_API_KEY)") : st.authMode === "ocs-token" ? pc.green("OCS token in env") : pc.yellow("not ready \u2014 install MCP or open sign-up");
582
1128
  return [
583
1129
  `Claude Code: ${yn(st.claudeFound, "found", "not on PATH")}`,
584
1130
  `Plugin: ${st.pluginInstalled ? pc.green(`installed v${st.pluginVersion ?? "?"}`) : pc.yellow("not installed")}`,
@@ -586,249 +1132,25 @@ function formatStatusBlock(st) {
586
1132
  `MCP URL: ${st.mcpUrlMatches ? pc.green(MCP_URL) : pc.yellow("not production / missing")}`,
587
1133
  `Auth path: ${authLabel}`,
588
1134
  `Headless token:${st.tokenPresent ? pc.green(` ${st.tokenKind}`) : pc.dim(" none (optional)")}`
589
- ].join("\n");
590
- }
591
- function formatNextSteps(st) {
592
- return st.nextSteps.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
593
- }
594
- function formatNlExamples() {
595
- return FLEET_NL_EXAMPLES.map((e) => ` \u2022 ${pc.bold(e.label)}: "${e.prompt}"`).join("\n");
596
- }
597
- function accountLinksNote() {
598
- return [
599
- `Sign up: ${SIGN_UP_URL}`,
600
- `Sign in: ${SIGN_IN_URL}`,
601
- `Console: ${CONSOLE_URL}`,
602
- `Onboarding: ${ONBOARDING_URL}`,
603
- `MCP home: https://mcp.carrier.llc`
604
- ].join("\n");
605
- }
606
- function withNextStep(errorMsg, next) {
607
- return [errorMsg, "", "What to do next:", ...next.map((s) => ` \u2192 ${s}`)].join("\n");
608
- }
609
-
610
- // src/cli/lib/status.ts
611
- import { homedir as homedir3 } from "os";
612
- import { join as join6 } from "path";
613
-
614
- // src/cli/lib/mcp-url.ts
615
- var PRODUCTION_MCP_URL = "https://mcp.carrier.llc/mcp";
616
- var PRODUCTION_HOST = "mcp.carrier.llc";
617
- var URL_TOKEN = /https?:\/\/[^\s"'<>)]+/g;
618
- function pathWithoutTrailingSlash(pathname) {
619
- let end = pathname.length;
620
- while (end > 1 && pathname.charCodeAt(end - 1) === 47) end -= 1;
621
- return pathname.slice(0, end);
622
- }
623
- function parseHttpUrl(value) {
624
- try {
625
- const parsed = new URL(value);
626
- if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
627
- return parsed;
628
- } catch {
629
- return null;
630
- }
631
- }
632
- function isProductionMcpUrl(value) {
633
- if (value === PRODUCTION_MCP_URL) return true;
634
- const parsed = parseHttpUrl(value);
635
- if (!parsed || parsed.protocol !== "https:" || parsed.hostname !== PRODUCTION_HOST) return false;
636
- return pathWithoutTrailingSlash(parsed.pathname) === "/mcp";
637
- }
638
- function isCarrierMcpHost(value) {
639
- const parsed = parseHttpUrl(value);
640
- return parsed?.protocol === "https:" && parsed.hostname === PRODUCTION_HOST;
641
- }
642
- function trimTrailingPunctuation(token) {
643
- let end = token.length;
644
- while (end > 0) {
645
- const code = token.charCodeAt(end - 1);
646
- if (code !== 46 && code !== 44 && code !== 59 && code !== 58) break;
647
- end -= 1;
648
- }
649
- return token.slice(0, end);
650
- }
651
- function lineMentionsProductionMcp(line) {
652
- const tokens = line.match(URL_TOKEN);
653
- if (!tokens) return false;
654
- for (const token of tokens) {
655
- if (isProductionMcpUrl(trimTrailingPunctuation(token))) return true;
656
- }
657
- return false;
658
- }
659
-
660
- // src/cli/lib/status.ts
661
- function detectEnvToken() {
662
- const api = process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || "";
663
- if (api.startsWith("ak_") || api.length > 0) {
664
- return { present: true, kind: "api-key" };
665
- }
666
- const ocs = process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim() || "";
667
- if (ocs) return { present: true, kind: "ocs-token" };
668
- return { present: false, kind: "none" };
669
- }
670
- function rankHit(name, url) {
671
- let rank = 0;
672
- if (name.toLowerCase() === MCP_NAME) rank += 100;
673
- else if (/^carrier[-_]?/i.test(name) && !/test|staging|stg|dev|local/i.test(name)) rank += 40;
674
- else if (/carrier/i.test(name) && !/test|staging|stg|dev|local/i.test(name)) rank += 20;
675
- else if (/carrier/i.test(name)) rank += 5;
676
- if (isProductionMcpUrl(url)) rank += 50;
677
- else if (isCarrierMcpHost(url)) rank += 25;
678
- return rank;
679
- }
680
- function scanMcpServers(servers) {
681
- if (!servers || typeof servers !== "object") return null;
682
- let best = null;
683
- for (const [name, entry] of Object.entries(servers)) {
684
- if (!/carrier/i.test(name) && !(typeof entry?.url === "string" && /carrier\.llc/i.test(entry.url))) {
685
- continue;
686
- }
687
- const url = typeof entry?.url === "string" ? entry.url : "";
688
- const rank = rankHit(name, url);
689
- if (rank < 20 && !isCarrierMcpHost(url)) {
690
- continue;
691
- }
692
- const urlMatches = isProductionMcpUrl(url);
693
- const hit = {
694
- registered: true,
695
- urlMatches,
696
- detail: url ? `${name}: ${url}` : `${name}: ${entry?.command ?? "configured"}`,
697
- rank
698
- };
699
- if (!best || hit.rank > best.rank) best = hit;
700
- }
701
- return best;
702
- }
703
- async function probeMcpFromConfig() {
704
- const candidates = [
705
- join6(homedir3(), ".claude.json"),
706
- join6(claudeHome(), "settings.json"),
707
- join6(claudeHome(), ".mcp.json"),
708
- join6(process.cwd(), ".mcp.json")
709
- ];
710
- let best = null;
711
- let configSeen = false;
712
- for (const file of candidates) {
713
- if (!await exists(file)) continue;
714
- try {
715
- const json = JSON.parse(await readFile(file, "utf8"));
716
- configSeen = true;
717
- const direct = scanMcpServers(json.mcpServers);
718
- if (direct && (!best || direct.rank > best.rank)) best = direct;
719
- if (json.projects) {
720
- for (const proj of Object.values(json.projects)) {
721
- const hit = scanMcpServers(proj?.mcpServers);
722
- if (hit && (!best || hit.rank > best.rank)) best = hit;
723
- }
724
- }
725
- } catch {
726
- }
727
- }
728
- if (!best) return { hit: null, configSeen };
729
- return {
730
- hit: { registered: best.registered, urlMatches: best.urlMatches, detail: best.detail },
731
- configSeen
732
- };
733
- }
734
- async function probeMcpRegistration() {
735
- const { hit, configSeen } = await probeMcpFromConfig();
736
- if (hit) return hit;
737
- if (configSeen) {
738
- return {
739
- registered: false,
740
- urlMatches: false,
741
- detail: "carrier not registered (no production MCP URL in Claude config)"
742
- };
743
- }
744
- const claudeFound = await which("claude");
745
- if (!claudeFound) {
746
- return {
747
- registered: false,
748
- urlMatches: false,
749
- detail: "claude CLI not on PATH"
750
- };
751
- }
752
- const r = await run("claude", ["mcp", "list"], { timeoutMs: 5e3 });
753
- const out = `${r.stdout}
754
- ${r.stderr}`;
755
- if (!r.ok && !out.trim()) {
756
- return { registered: false, urlMatches: false, detail: "claude mcp list failed or timed out" };
757
- }
758
- if (/timeout after/i.test(r.stderr) && !/carrier/i.test(out)) {
759
- return {
760
- registered: false,
761
- urlMatches: false,
762
- detail: "claude mcp list timed out \u2014 run `claude mcp list` manually"
763
- };
764
- }
765
- const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
766
- const carrierLine = lines.find((l) => new RegExp(`^${MCP_NAME}\\s*:`, "i").test(l)) ?? lines.find((l) => /carrier/i.test(l) && /mcp\.carrier\.llc/i.test(l));
767
- if (!carrierLine) {
768
- return { registered: false, urlMatches: false, detail: "carrier not in claude mcp list" };
769
- }
770
- const urlMatches = lineMentionsProductionMcp(carrierLine);
771
- return { registered: true, urlMatches, detail: carrierLine.slice(0, 200) };
772
- }
773
- function buildNextSteps(s) {
774
- const steps = [];
775
- if (!s.claudeFound) {
776
- steps.push("Install Claude Code (https://claude.ai/code), then re-run: carrier plugin install");
777
- } else if (!s.pluginInstalled) {
778
- steps.push("Install the Carrier plugin: carrier plugin install");
779
- } else if (!s.mcpRegistered) {
780
- steps.push(`Register the zero-cred MCP URL: claude mcp add --transport http ${MCP_NAME} ${MCP_URL}`);
781
- } else if (!s.mcpUrlMatches) {
782
- steps.push(`Update MCP to production URL: claude mcp add --transport http ${MCP_NAME} ${MCP_URL}`);
783
- }
784
- if (s.mcpRegistered || s.pluginInstalled) {
785
- steps.push(
786
- "Talk to your fleet in Claude \u2014 first tool call opens Clerk OAuth (Google / GitHub / email). No API token needed."
787
- );
788
- } else {
789
- steps.push("Create a free account (browser): carrier open signup");
790
- steps.push("Or sign in: carrier open signin");
791
- }
792
- if (!s.tokenPresent) {
793
- steps.push(
794
- "Optional headless key: Console \u2192 Settings \u2192 API Keys (ak_\u2026) then export CARRIER_API_KEY=ak_\u2026 for `carrier ask`."
795
- );
796
- } else {
797
- steps.push('Try a natural-language fleet query: carrier ask "show fleet health"');
798
- }
799
- steps.push("Scaffold a white-label storefront anytime: carrier site create");
800
- return steps;
1135
+ ].join("\n");
801
1136
  }
802
- async function gatherStatus() {
803
- const [plugin2, mcp, claudeFound] = await Promise.all([
804
- pluginStatus(),
805
- probeMcpRegistration(),
806
- which("claude")
807
- ]);
808
- const token = detectEnvToken();
809
- let authMode = "none";
810
- if (token.kind === "api-key") authMode = "api-key";
811
- else if (token.kind === "ocs-token") authMode = "ocs-token";
812
- else if (mcp.registered || plugin2.installed) authMode = "oauth-first-use";
813
- const base = {
814
- pluginInstalled: plugin2.installed,
815
- pluginVersion: plugin2.version,
816
- pluginDir: plugin2.dir,
817
- claudeFound,
818
- mcpRegistered: mcp.registered,
819
- mcpUrlMatches: mcp.urlMatches,
820
- mcpDetail: mcp.detail,
821
- tokenPresent: token.present,
822
- tokenKind: token.kind,
823
- authMode
824
- };
825
- return { ...base, nextSteps: buildNextSteps(base) };
1137
+ function formatNextSteps(st) {
1138
+ return st.nextSteps.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
1139
+ }
1140
+ function formatNlExamples() {
1141
+ return FLEET_NL_EXAMPLES.map((e) => ` \u2022 ${pc.bold(e.label)}: "${e.prompt}"`).join("\n");
1142
+ }
1143
+ function accountLinksNote() {
1144
+ return [
1145
+ `Sign up: ${SIGN_UP_URL}`,
1146
+ `Sign in: ${SIGN_IN_URL}`,
1147
+ `Console: ${CONSOLE_URL}`,
1148
+ `Onboarding: ${ONBOARDING_URL}`,
1149
+ `MCP home: https://mcp.carrier.llc`
1150
+ ].join("\n");
826
1151
  }
827
- function resolveCliToken() {
828
- const api = process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || "";
829
- if (api) return api;
830
- const ocs = process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim() || "";
831
- return ocs || null;
1152
+ function withNextStep(errorMsg, next) {
1153
+ return [errorMsg, "", "What to do next:", ...next.map((s) => ` \u2192 ${s}`)].join("\n");
832
1154
  }
833
1155
 
834
1156
  // src/cli/lib/ask.ts
@@ -863,7 +1185,7 @@ function parseRpcBody(raw) {
863
1185
  return JSON.parse(dataLine.slice(5).trim());
864
1186
  }
865
1187
  async function callMcpTool(tool, args, opts = {}) {
866
- const token = resolveCliToken() ?? await getAccessToken();
1188
+ const token = (resolveExplicitCredential({ allowOcs: true }) ?? await resolveCredential({ allowOcs: true }))?.token;
867
1189
  if (!token) {
868
1190
  return {
869
1191
  ok: false,
@@ -890,7 +1212,7 @@ async function callMcpTool(tool, args, opts = {}) {
890
1212
  });
891
1213
  const signal = controller.signal;
892
1214
  try {
893
- return await Promise.race([
1215
+ const result = await Promise.race([
894
1216
  (async () => {
895
1217
  const initRes = await fetch(MCP_URL, {
896
1218
  method: "POST",
@@ -973,6 +1295,7 @@ async function callMcpTool(tool, args, opts = {}) {
973
1295
  })(),
974
1296
  deadline
975
1297
  ]);
1298
+ return redactCredential(result, token);
976
1299
  } catch (e) {
977
1300
  if (isTimeoutish(e)) {
978
1301
  const secs = Math.round(timeoutMs / 1e3);
@@ -990,7 +1313,7 @@ async function callMcpTool(tool, args, opts = {}) {
990
1313
  )
991
1314
  };
992
1315
  }
993
- const msg = e instanceof Error ? e.message : String(e);
1316
+ const msg = redact((e instanceof Error ? e.message : String(e)).split(token).join("***"));
994
1317
  return {
995
1318
  ok: false,
996
1319
  text: withNextStep(`Could not reach MCP: ${msg}`, [
@@ -1063,9 +1386,6 @@ var DEFAULT_ERROR_HINTS2 = [
1063
1386
  "Re-check the arguments: carrier <domain> <command> --help",
1064
1387
  "Or open Claude and ask there with full tool routing"
1065
1388
  ];
1066
- function resolveApiKey() {
1067
- return process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || null;
1068
- }
1069
1389
  function ocsTokenOnly() {
1070
1390
  return Boolean(
1071
1391
  process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim()
@@ -1114,7 +1434,7 @@ function orgNotConnected(raw) {
1114
1434
  }
1115
1435
  }
1116
1436
  async function callRestEndpoint(method, path, args, opts = {}) {
1117
- const token = resolveApiKey() ?? await getAccessToken();
1437
+ const token = (await resolveCredential())?.token;
1118
1438
  if (!token) {
1119
1439
  return {
1120
1440
  ok: false,
@@ -1158,7 +1478,7 @@ async function callRestEndpoint(method, path, args, opts = {}) {
1158
1478
  signal: controller.signal,
1159
1479
  body: sendsBody ? JSON.stringify(rest) : void 0
1160
1480
  });
1161
- const raw = await res.text();
1481
+ const raw = (await res.text()).split(token).join("***");
1162
1482
  if (res.status === 202) {
1163
1483
  let confirmToken;
1164
1484
  let expires;
@@ -1225,7 +1545,7 @@ async function callRestEndpoint(method, path, args, opts = {}) {
1225
1545
  ])
1226
1546
  };
1227
1547
  }
1228
- const msg = e instanceof Error ? e.message : String(e);
1548
+ const msg = (e instanceof Error ? e.message : String(e)).split(token).join("***");
1229
1549
  return {
1230
1550
  ok: false,
1231
1551
  text: withNextStep(redact(`Could not reach the Carrier API: ${msg}`), [
@@ -1734,7 +2054,7 @@ function record(value) {
1734
2054
  }
1735
2055
 
1736
2056
  // src/cli/lib/resend.ts
1737
- import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
2057
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
1738
2058
  import { createServer as createServer2 } from "http";
1739
2059
  var RESEND_API = "https://api.resend.com";
1740
2060
  var RESEND_API_KEYS_URL = "https://resend.com/api-keys";
@@ -1752,7 +2072,7 @@ function base64url2(input) {
1752
2072
  }
1753
2073
  function pkcePair2() {
1754
2074
  const verifier = base64url2(randomBytes2(64));
1755
- return { verifier, challenge: base64url2(createHash2("sha256").update(verifier).digest()) };
2075
+ return { verifier, challenge: base64url2(createHash3("sha256").update(verifier).digest()) };
1756
2076
  }
1757
2077
  function buildAuthorizeUrl(opts) {
1758
2078
  const url = new URL(`${RESEND_API}/oauth/authorize`);
@@ -1900,9 +2220,9 @@ async function startLoopbackServer() {
1900
2220
  error ? { ok: false, reason: `Resend returned "${error}".` } : code ? { ok: true, code, reason: state ?? void 0 } : { ok: false, reason: "Resend sent no authorization code." }
1901
2221
  );
1902
2222
  });
1903
- await new Promise((resolve3, reject) => {
2223
+ await new Promise((resolve5, reject) => {
1904
2224
  server.once("error", reject);
1905
- server.listen(0, REDIRECT_HOST, resolve3);
2225
+ server.listen(0, REDIRECT_HOST, resolve5);
1906
2226
  });
1907
2227
  const port = server.address().port;
1908
2228
  const redirectUri = `http://${REDIRECT_HOST}:${port}${REDIRECT_PATH}`;
@@ -1910,12 +2230,12 @@ async function startLoopbackServer() {
1910
2230
  redirectUri,
1911
2231
  close: () => server.close(),
1912
2232
  waitForCode(state, timeoutMs) {
1913
- return new Promise((resolve3) => {
2233
+ return new Promise((resolve5) => {
1914
2234
  const done = (r) => {
1915
2235
  clearTimeout(timer);
1916
2236
  resolveCode = null;
1917
2237
  server.close();
1918
- resolve3(r);
2238
+ resolve5(r);
1919
2239
  };
1920
2240
  const timer = setTimeout(
1921
2241
  () => done({ ok: false, reason: "Timed out waiting for the browser." }),
@@ -2189,6 +2509,217 @@ function buildDashScreen(domain, payload) {
2189
2509
  }
2190
2510
  }
2191
2511
 
2512
+ // src/cli/lib/status.ts
2513
+ import { homedir as homedir4 } from "os";
2514
+ import { join as join8 } from "path";
2515
+
2516
+ // src/cli/lib/mcp-url.ts
2517
+ var PRODUCTION_MCP_URL = "https://mcp.carrier.llc/mcp";
2518
+ var PRODUCTION_HOST = "mcp.carrier.llc";
2519
+ var URL_TOKEN = /https?:\/\/[^\s"'<>)]+/g;
2520
+ function pathWithoutTrailingSlash(pathname) {
2521
+ let end = pathname.length;
2522
+ while (end > 1 && pathname.charCodeAt(end - 1) === 47) end -= 1;
2523
+ return pathname.slice(0, end);
2524
+ }
2525
+ function parseHttpUrl(value) {
2526
+ try {
2527
+ const parsed = new URL(value);
2528
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
2529
+ return parsed;
2530
+ } catch {
2531
+ return null;
2532
+ }
2533
+ }
2534
+ function isProductionMcpUrl(value) {
2535
+ if (value === PRODUCTION_MCP_URL) return true;
2536
+ const parsed = parseHttpUrl(value);
2537
+ if (!parsed || parsed.protocol !== "https:" || parsed.hostname !== PRODUCTION_HOST) return false;
2538
+ return pathWithoutTrailingSlash(parsed.pathname) === "/mcp";
2539
+ }
2540
+ function isCarrierMcpHost(value) {
2541
+ const parsed = parseHttpUrl(value);
2542
+ return parsed?.protocol === "https:" && parsed.hostname === PRODUCTION_HOST;
2543
+ }
2544
+ function trimTrailingPunctuation(token) {
2545
+ let end = token.length;
2546
+ while (end > 0) {
2547
+ const code = token.charCodeAt(end - 1);
2548
+ if (code !== 46 && code !== 44 && code !== 59 && code !== 58) break;
2549
+ end -= 1;
2550
+ }
2551
+ return token.slice(0, end);
2552
+ }
2553
+ function lineMentionsProductionMcp(line) {
2554
+ const tokens = line.match(URL_TOKEN);
2555
+ if (!tokens) return false;
2556
+ for (const token of tokens) {
2557
+ if (isProductionMcpUrl(trimTrailingPunctuation(token))) return true;
2558
+ }
2559
+ return false;
2560
+ }
2561
+
2562
+ // src/cli/lib/status.ts
2563
+ function rankHit(name, url) {
2564
+ let rank = 0;
2565
+ if (name.toLowerCase() === MCP_NAME) rank += 100;
2566
+ else if (/^carrier[-_]?/i.test(name) && !/test|staging|stg|dev|local/i.test(name)) rank += 40;
2567
+ else if (/carrier/i.test(name) && !/test|staging|stg|dev|local/i.test(name)) rank += 20;
2568
+ else if (/carrier/i.test(name)) rank += 5;
2569
+ if (isProductionMcpUrl(url)) rank += 50;
2570
+ else if (isCarrierMcpHost(url)) rank += 25;
2571
+ return rank;
2572
+ }
2573
+ function scanMcpServers(servers) {
2574
+ if (!servers || typeof servers !== "object") return null;
2575
+ let best = null;
2576
+ for (const [name, entry] of Object.entries(servers)) {
2577
+ if (!/carrier/i.test(name) && !(typeof entry?.url === "string" && /carrier\.llc/i.test(entry.url))) {
2578
+ continue;
2579
+ }
2580
+ const url = typeof entry?.url === "string" ? entry.url : "";
2581
+ const rank = rankHit(name, url);
2582
+ if (rank < 20 && !isCarrierMcpHost(url)) {
2583
+ continue;
2584
+ }
2585
+ const urlMatches = isProductionMcpUrl(url);
2586
+ const hit = {
2587
+ registered: true,
2588
+ urlMatches,
2589
+ detail: url ? `${name}: ${url}` : `${name}: ${entry?.command ?? "configured"}`,
2590
+ rank
2591
+ };
2592
+ if (!best || hit.rank > best.rank) best = hit;
2593
+ }
2594
+ return best;
2595
+ }
2596
+ async function probeMcpFromConfig() {
2597
+ const candidates = [
2598
+ join8(homedir4(), ".claude.json"),
2599
+ join8(claudeHome(), "settings.json"),
2600
+ join8(claudeHome(), ".mcp.json"),
2601
+ join8(process.cwd(), ".mcp.json")
2602
+ ];
2603
+ let best = null;
2604
+ let configSeen = false;
2605
+ for (const file of candidates) {
2606
+ if (!await exists(file)) continue;
2607
+ try {
2608
+ const json = JSON.parse(await readFile(file, "utf8"));
2609
+ configSeen = true;
2610
+ const direct = scanMcpServers(json.mcpServers);
2611
+ if (direct && (!best || direct.rank > best.rank)) best = direct;
2612
+ if (json.projects) {
2613
+ for (const proj of Object.values(json.projects)) {
2614
+ const hit = scanMcpServers(proj?.mcpServers);
2615
+ if (hit && (!best || hit.rank > best.rank)) best = hit;
2616
+ }
2617
+ }
2618
+ } catch {
2619
+ }
2620
+ }
2621
+ if (!best) return { hit: null, configSeen };
2622
+ return {
2623
+ hit: { registered: best.registered, urlMatches: best.urlMatches, detail: best.detail },
2624
+ configSeen
2625
+ };
2626
+ }
2627
+ async function probeMcpRegistration() {
2628
+ const { hit, configSeen } = await probeMcpFromConfig();
2629
+ if (hit) return hit;
2630
+ if (configSeen) {
2631
+ return {
2632
+ registered: false,
2633
+ urlMatches: false,
2634
+ detail: "carrier not registered (no production MCP URL in Claude config)"
2635
+ };
2636
+ }
2637
+ const claudeFound = await which("claude");
2638
+ if (!claudeFound) {
2639
+ return {
2640
+ registered: false,
2641
+ urlMatches: false,
2642
+ detail: "claude CLI not on PATH"
2643
+ };
2644
+ }
2645
+ const r = await run("claude", ["mcp", "list"], { timeoutMs: 5e3 });
2646
+ const out = `${r.stdout}
2647
+ ${r.stderr}`;
2648
+ if (!r.ok && !out.trim()) {
2649
+ return { registered: false, urlMatches: false, detail: "claude mcp list failed or timed out" };
2650
+ }
2651
+ if (/timeout after/i.test(r.stderr) && !/carrier/i.test(out)) {
2652
+ return {
2653
+ registered: false,
2654
+ urlMatches: false,
2655
+ detail: "claude mcp list timed out \u2014 run `claude mcp list` manually"
2656
+ };
2657
+ }
2658
+ const lines = out.split("\n").map((l) => l.trim()).filter(Boolean);
2659
+ const carrierLine = lines.find((l) => new RegExp(`^${MCP_NAME}\\s*:`, "i").test(l)) ?? lines.find((l) => /carrier/i.test(l) && /mcp\.carrier\.llc/i.test(l));
2660
+ if (!carrierLine) {
2661
+ return { registered: false, urlMatches: false, detail: "carrier not in claude mcp list" };
2662
+ }
2663
+ const urlMatches = lineMentionsProductionMcp(carrierLine);
2664
+ return { registered: true, urlMatches, detail: carrierLine.slice(0, 200) };
2665
+ }
2666
+ function buildNextSteps(s) {
2667
+ const steps = [];
2668
+ if (!s.claudeFound) {
2669
+ steps.push("Install Claude Code (https://claude.ai/code), then re-run: carrier plugin install");
2670
+ } else if (!s.pluginInstalled) {
2671
+ steps.push("Install the Carrier plugin: carrier plugin install");
2672
+ } else if (!s.mcpRegistered) {
2673
+ steps.push(`Register the zero-cred MCP URL: claude mcp add --transport http ${MCP_NAME} ${MCP_URL}`);
2674
+ } else if (!s.mcpUrlMatches) {
2675
+ steps.push(`Update MCP to production URL: claude mcp add --transport http ${MCP_NAME} ${MCP_URL}`);
2676
+ }
2677
+ if (s.mcpRegistered || s.pluginInstalled) {
2678
+ steps.push(
2679
+ "Talk to your fleet in Claude \u2014 first tool call opens Clerk OAuth (Google / GitHub / email). No API token needed."
2680
+ );
2681
+ } else {
2682
+ steps.push("Create a free account (browser): carrier open signup");
2683
+ steps.push("Or sign in: carrier open signin");
2684
+ }
2685
+ if (!s.tokenPresent) {
2686
+ steps.push(
2687
+ "Headless: carrier login, or refresh compatible Carrier OAuth in Claude. Export to a child terminal: carrier exec -- bash."
2688
+ );
2689
+ } else {
2690
+ steps.push('Try a natural-language fleet query: carrier ask "show fleet health"');
2691
+ }
2692
+ steps.push("Scaffold a white-label storefront anytime: carrier site create");
2693
+ return steps;
2694
+ }
2695
+ async function gatherStatus() {
2696
+ const [plugin2, mcp, claudeFound] = await Promise.all([
2697
+ pluginStatus(),
2698
+ probeMcpRegistration(),
2699
+ which("claude")
2700
+ ]);
2701
+ const credential = await resolveCredential({ allowOcs: true });
2702
+ const token = { present: !!credential, kind: credential?.kind ?? "none" };
2703
+ let authMode = "none";
2704
+ if (token.kind === "oauth") authMode = "oauth";
2705
+ else if (token.kind === "api-key") authMode = "api-key";
2706
+ else if (token.kind === "ocs-token") authMode = "ocs-token";
2707
+ else if (mcp.registered || plugin2.installed) authMode = "oauth-first-use";
2708
+ const base = {
2709
+ pluginInstalled: plugin2.installed,
2710
+ pluginVersion: plugin2.version,
2711
+ pluginDir: plugin2.dir,
2712
+ claudeFound,
2713
+ mcpRegistered: mcp.registered,
2714
+ mcpUrlMatches: mcp.urlMatches,
2715
+ mcpDetail: mcp.detail,
2716
+ tokenPresent: token.present,
2717
+ tokenKind: token.kind,
2718
+ authMode
2719
+ };
2720
+ return { ...base, nextSteps: buildNextSteps(base) };
2721
+ }
2722
+
2192
2723
  // src/cli/lib/home.ts
2193
2724
  import * as p from "@clack/prompts";
2194
2725
  import pc2 from "picocolors";
@@ -2219,7 +2750,7 @@ async function runHome(opts) {
2219
2750
  let st = await showHomeStatus();
2220
2751
  p.note(oauthFirstUseNote(), "How auth works");
2221
2752
  for (; ; ) {
2222
- const hasToken = !!resolveCliToken();
2753
+ const hasToken = !!await resolveCredential({ allowOcs: true });
2223
2754
  const choice = await p.select({
2224
2755
  message: "What do you want to do?",
2225
2756
  options: [
@@ -2255,8 +2786,8 @@ async function runHome(opts) {
2255
2786
  },
2256
2787
  {
2257
2788
  value: "ask",
2258
- label: hasToken ? "Ask the fleet (carrier ask)" : "Ask the fleet (needs CARRIER_API_KEY)",
2259
- hint: hasToken ? "Uses env token" : "Optional headless path"
2789
+ label: hasToken ? "Ask the fleet (carrier ask)" : "Ask the fleet (sign-in needed)",
2790
+ hint: hasToken ? "Uses Carrier credential" : "Run carrier login"
2260
2791
  },
2261
2792
  {
2262
2793
  value: "site",
@@ -2294,8 +2825,8 @@ async function runHome(opts) {
2294
2825
  p.log.info("Link OCS credentials (BYO) or finish managed setup, then use Claude MCP.");
2295
2826
  break;
2296
2827
  case "ask": {
2297
- if (!resolveCliToken()) {
2298
- p.log.warn("No CARRIER_API_KEY / OCS token in env.");
2828
+ if (!await resolveCredential({ allowOcs: true })) {
2829
+ p.log.warn("No usable Carrier credential. Run carrier login or refresh Carrier OAuth in Claude.");
2299
2830
  p.note(
2300
2831
  [
2301
2832
  "Interactive (recommended): install MCP, open Claude, ask in plain English.",
@@ -2330,6 +2861,68 @@ async function runHome(opts) {
2330
2861
  }
2331
2862
  }
2332
2863
 
2864
+ // src/cli/lib/credential-exec.ts
2865
+ import { spawn as spawn3 } from "child_process";
2866
+ import { createHash as createHash4 } from "crypto";
2867
+ import { constants as constants3 } from "os";
2868
+ import { writeSync, fstatSync } from "fs";
2869
+ async function credentialHandoff() {
2870
+ try {
2871
+ const stat = fstatSync(3);
2872
+ if (!stat.isFIFO() && !stat.isSocket()) throw new Error("unsafe fd");
2873
+ const credential = await resolveCredential();
2874
+ if (!credential) throw new Error("no credential");
2875
+ const fields = [credential.token, credential.origin ?? "explicit", String(credential.expiresAt ?? 0), credential.kind === "oauth" ? createHash4("sha256").update(credential.token).digest("hex") : "-", credential.binding ?? "-"];
2876
+ if (fields.some((field) => /[\r\n\0]/.test(field))) throw new Error("unsafe field");
2877
+ const data = Buffer.from(`${fields.join("\n")}
2878
+ `);
2879
+ let offset = 0;
2880
+ while (offset < data.length) offset += writeSync(3, data, offset, data.length - offset);
2881
+ return 0;
2882
+ } catch {
2883
+ console.error("Carrier credential handoff unavailable; use the static shell integration with a usable owner session.");
2884
+ return 1;
2885
+ }
2886
+ }
2887
+ async function credentialExec(command, args) {
2888
+ const credential = await resolveCredential();
2889
+ if (!credential) {
2890
+ console.error("No usable Carrier credential. Run carrier login, or refresh Carrier OAuth in Claude. See carrier exec --help.");
2891
+ return 1;
2892
+ }
2893
+ const env = { ...process.env, CARRIER_API_KEY: credential.token };
2894
+ if (credential.kind === "oauth") {
2895
+ env.CARRIER_OAUTH_HANDOFF = createHash4("sha256").update(credential.token).digest("hex");
2896
+ env.CARRIER_OAUTH_ORIGIN = credential.origin;
2897
+ env.CARRIER_OAUTH_BINDING = credential.binding;
2898
+ env.CARRIER_OAUTH_EXPIRES_AT = String(credential.expiresAt);
2899
+ } else {
2900
+ delete env.CARRIER_OAUTH_HANDOFF;
2901
+ delete env.CARRIER_OAUTH_BINDING;
2902
+ delete env.CARRIER_OAUTH_ORIGIN;
2903
+ delete env.CARRIER_OAUTH_EXPIRES_AT;
2904
+ }
2905
+ return new Promise((resolve5) => {
2906
+ const child = spawn3(command, args, { env, stdio: "inherit", shell: false });
2907
+ const forward = (signal) => {
2908
+ child.kill(signal);
2909
+ };
2910
+ const signals = ["SIGINT", "SIGTERM", "SIGHUP"];
2911
+ const handlers = signals.map((signal) => () => forward(signal));
2912
+ signals.forEach((signal, i) => process.on(signal, handlers[i]));
2913
+ const cleanup = () => signals.forEach((signal, i) => process.removeListener(signal, handlers[i]));
2914
+ child.once("error", () => {
2915
+ cleanup();
2916
+ console.error("Could not start the requested command.");
2917
+ resolve5(1);
2918
+ });
2919
+ child.once("close", (code, signal) => {
2920
+ cleanup();
2921
+ resolve5(code ?? (signal ? 128 + (constants3.signals[signal] ?? 1) : 1));
2922
+ });
2923
+ });
2924
+ }
2925
+
2333
2926
  // src/cli/lib/signup.ts
2334
2927
  var DEFAULT_MAX_CODE_ATTEMPTS = 3;
2335
2928
  function parseEnvironment(raw) {
@@ -5593,7 +6186,7 @@ async function interactiveSiteCreate() {
5593
6186
  defaultValue: "./storefront"
5594
6187
  });
5595
6188
  if (p3.isCancel(dir)) return;
5596
- const target = resolve2(dir);
6189
+ const target = resolve4(dir);
5597
6190
  if (await exists(target)) {
5598
6191
  const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
5599
6192
  if (p3.isCancel(go) || !go) {
@@ -5605,6 +6198,20 @@ async function interactiveSiteCreate() {
5605
6198
  p3.outro(pc4.green(`Scaffolded. cd ${dir} && pnpm install && pnpm dev`));
5606
6199
  }
5607
6200
  var program = new Command();
6201
+ program.command("credential-handoff").description("Private FD protocol for the explicitly sourced static shell integration; never stdout.").action(async () => {
6202
+ process.exitCode = await credentialHandoff();
6203
+ });
6204
+ program.command("shell-path").description("Print the static integration file path, without credentials or startup edits.").argument("<shell>", "bash or zsh").action((shell) => {
6205
+ if (shell !== "bash" && shell !== "zsh") {
6206
+ console.error("Supported shells: bash, zsh.");
6207
+ process.exitCode = 1;
6208
+ return;
6209
+ }
6210
+ console.log(resolve4(packageRoot(), "shell", `carrier.${shell}`));
6211
+ });
6212
+ program.command("exec").description("Run a command/child shell with CARRIER_API_KEY securely in its environment.").argument("<command>", "Executable to start (use -- before the command)").argument("[args...]", "Arguments passed without shell interpolation").addHelpText("after", "\nExamples: carrier exec -- bash\n carrier exec -- your-headless-job\nOAuth snapshots expire; Carrier commands refresh their owner per call. Exit the child shell after logout. This never changes the existing parent shell or prints a token.").action(async (command, args) => {
6213
+ process.exitCode = await credentialExec(command, args);
6214
+ });
5608
6215
  program.name("carrier").description(
5609
6216
  [
5610
6217
  "Carrier CLI \u2014 status, OAuth-ready MCP install, storefront scaffold, and fleet control.",
@@ -5624,7 +6231,7 @@ program.command("init").description("Interactive home: status, install plugin+MC
5624
6231
  if (o.yes) {
5625
6232
  await doPluginInstall();
5626
6233
  const brand = CARRIER_BRAND;
5627
- const target = resolve2(o.dir);
6234
+ const target = resolve4(o.dir);
5628
6235
  if (await exists(target)) {
5629
6236
  info(pc4.yellow(`${target} exists \u2014 writing into it (--yes).`));
5630
6237
  }
@@ -5649,7 +6256,7 @@ program.command("init").description("Interactive home: status, install plugin+MC
5649
6256
  printStatus(st);
5650
6257
  await doPluginInstall();
5651
6258
  const brand = await promptBrand(CARRIER_BRAND);
5652
- const target = resolve2(o.dir);
6259
+ const target = resolve4(o.dir);
5653
6260
  if (await exists(target)) {
5654
6261
  const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
5655
6262
  if (p3.isCancel(go) || !go) {
@@ -5690,7 +6297,7 @@ program.command("init").description("Interactive home: status, install plugin+MC
5690
6297
  onInstall: doPluginInstall,
5691
6298
  onSiteCreate: async () => {
5692
6299
  const brand = await promptBrand(CARRIER_BRAND);
5693
- const target = resolve2(o.dir);
6300
+ const target = resolve4(o.dir);
5694
6301
  if (await exists(target)) {
5695
6302
  const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
5696
6303
  if (p3.isCancel(go) || !go) {
@@ -5765,7 +6372,7 @@ program.command("login").description("Sign in from this terminal. Opens a browse
5765
6372
  return;
5766
6373
  }
5767
6374
  p3.note(
5768
- [`Scope: ${r.scope ?? "read write"}`, "", "Try it: carrier intelligence fleet-health"].join("\n"),
6375
+ [`Scope: ${r.scope ?? "read write"}`, "", "Try it: carrier intelligence fleet-health", 'Existing shell: source "$(carrier shell-path bash)"; carrier_auth_export', "Child terminal: carrier exec -- bash"].join("\n"),
5769
6376
  "Signed in"
5770
6377
  );
5771
6378
  } catch (e) {
@@ -6133,7 +6740,7 @@ site.command("create [dir]").description("Scaffold a white-labeled storefront fr
6133
6740
  } else if (!o.yes) {
6134
6741
  brand = await promptBrand(CARRIER_BRAND);
6135
6742
  }
6136
- const target = resolve2(dir ?? "./storefront");
6743
+ const target = resolve4(dir ?? "./storefront");
6137
6744
  if (await exists(target)) {
6138
6745
  const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
6139
6746
  if (p3.isCancel(go) || !go) {
@@ -6158,7 +6765,7 @@ site.command("create [dir]").description("Scaffold a white-labeled storefront fr
6158
6765
  site.command("deploy [dir]").description("Build + deploy a storefront to whichever host is available on this box.").option("--name <name>", "Project/worker name (defaults from brand)").option("--force", "Deploy under the scaffold default name anyway (acknowledges the overwrite risk)").option(`--target <target>`, `Force a host: ${TARGET_IDS.join(" | ")}`).option("--no-secrets", "Skip staging runtime secrets on the host").option("--clerk", "Provision and configure Clerk before building").option("--no-verify", "Skip the post-deploy check that the site actually serves").option("--autofix-agent", "On an unrecognised failure, print an agent-ready diagnosis bundle").option("--domain", "Route the brand domain to the deployment (Cloudflare provisions DNS + cert)").action(
6159
6766
  async (dir, o) => {
6160
6767
  header();
6161
- const target = resolve2(dir ?? "./storefront");
6768
+ const target = resolve4(dir ?? "./storefront");
6162
6769
  if (o.target && !isTargetId(o.target)) {
6163
6770
  fail(`Unknown target "${o.target}".`, [`Supported: ${TARGET_IDS.join(", ")}`]);
6164
6771
  process.exitCode = 1;
@@ -6208,7 +6815,7 @@ site.command("publish").description("Publish a Carrier-hosted storefront. Does n
6208
6815
  });
6209
6816
  site.command("targets [dir]").description("Show which deploy hosts are installed, logged in, and ready.").action(async (dir) => {
6210
6817
  header();
6211
- const storefront = resolve2(dir ?? "./storefront");
6818
+ const storefront = resolve4(dir ?? "./storefront");
6212
6819
  const statuses = await probeAll(storefront);
6213
6820
  const ranked = rankTargets(statuses);
6214
6821
  const rows2 = statuses.map((s) => {
@@ -6225,7 +6832,7 @@ site.command("targets [dir]").description("Show which deploy hosts are installed
6225
6832
  site.command("clerk [dir]").description("Create or discover a Clerk instance and configure it for the storefront.").option("--name <name>", "Application name (defaults from brand)").option("--production", "Also create a production instance (Platform API only)").option("--no-create", "Never create a new application; only reuse existing keys").option("--url <url>", "Deployed URL to whitelist as an origin/redirect").action(
6226
6833
  async (dir, o) => {
6227
6834
  header();
6228
- const storefront = resolve2(dir ?? "./storefront");
6835
+ const storefront = resolve4(dir ?? "./storefront");
6229
6836
  try {
6230
6837
  const brand = await loadStorefrontBrand(storefront, o.name ? { name: o.name } : void 0);
6231
6838
  const result = await provisionClerk({
@@ -6271,7 +6878,7 @@ site.command("clerk [dir]").description("Create or discover a Clerk instance and
6271
6878
  site.command("email [dir]").description("Connect Resend in the browser so the storefront can email QR codes.").option("--no-browser", "Never open a browser; print the manual steps instead").option("--yes", "Non-interactive: skip rather than wait for a human").option("--timeout <seconds>", "How long to wait for the browser approval", "180").action(
6272
6879
  async (dir, o) => {
6273
6880
  header();
6274
- const storefront = resolve2(dir ?? "./storefront");
6881
+ const storefront = resolve4(dir ?? "./storefront");
6275
6882
  const interactive = process.stdin.isTTY === true && o.yes !== true;
6276
6883
  try {
6277
6884
  const brand = await loadStorefrontBrand(storefront);
@@ -6329,7 +6936,7 @@ site.command("email [dir]").description("Connect Resend in the browser so the st
6329
6936
  );
6330
6937
  site.command("logo [dir]").description("Generate a storefront logo (SVG always; PNG if an image key is on this box or Carrier).").action(async (dir) => {
6331
6938
  header();
6332
- const target = resolve2(dir ?? "./storefront");
6939
+ const target = resolve4(dir ?? "./storefront");
6333
6940
  try {
6334
6941
  const brand = await loadStorefrontBrand(target);
6335
6942
  const logo = await writeStorefrontLogo(target, brand);