@carrierllc/mcp 0.13.28 → 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-RF3NNGZX.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
@@ -571,264 +1115,42 @@ function oauthFirstUseNote() {
571
1115
  " Auth: OAuth on first use (Clerk \u2014 Google, GitHub, or email)",
572
1116
  " Token: none required in Claude / Cursor / Windsurf",
573
1117
  "",
574
- "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.`
577
- ].join("\n");
578
- }
579
- function formatStatusBlock(st) {
580
- 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");
582
- return [
583
- `Claude Code: ${yn(st.claudeFound, "found", "not on PATH")}`,
584
- `Plugin: ${st.pluginInstalled ? pc.green(`installed v${st.pluginVersion ?? "?"}`) : pc.yellow("not installed")}`,
585
- `MCP registered:${st.mcpRegistered ? pc.green(" yes") : pc.yellow(" no")}${st.mcpDetail ? pc.dim(` (${st.mcpDetail})`) : ""}`,
586
- `MCP URL: ${st.mcpUrlMatches ? pc.green(MCP_URL) : pc.yellow("not production / missing")}`,
587
- `Auth path: ${authLabel}`,
588
- `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;
801
- }
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) };
1118
+ "When you first talk to Carrier in your MCP client, the browser opens",
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.`
1123
+ ].join("\n");
1124
+ }
1125
+ function formatStatusBlock(st) {
1126
+ const yn = (ok2, yes = "yes", no = "no") => ok2 ? pc.green(yes) : pc.yellow(no);
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");
1128
+ return [
1129
+ `Claude Code: ${yn(st.claudeFound, "found", "not on PATH")}`,
1130
+ `Plugin: ${st.pluginInstalled ? pc.green(`installed v${st.pluginVersion ?? "?"}`) : pc.yellow("not installed")}`,
1131
+ `MCP registered:${st.mcpRegistered ? pc.green(" yes") : pc.yellow(" no")}${st.mcpDetail ? pc.dim(` (${st.mcpDetail})`) : ""}`,
1132
+ `MCP URL: ${st.mcpUrlMatches ? pc.green(MCP_URL) : pc.yellow("not production / missing")}`,
1133
+ `Auth path: ${authLabel}`,
1134
+ `Headless token:${st.tokenPresent ? pc.green(` ${st.tokenKind}`) : pc.dim(" none (optional)")}`
1135
+ ].join("\n");
1136
+ }
1137
+ function formatNextSteps(st) {
1138
+ return st.nextSteps.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
826
1139
  }
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;
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");
1151
+ }
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()
@@ -1106,8 +1426,15 @@ function bodyText(raw) {
1106
1426
  return trimmed;
1107
1427
  }
1108
1428
  }
1429
+ function orgNotConnected(raw) {
1430
+ try {
1431
+ return JSON.parse(raw).error_code === "org_not_connected";
1432
+ } catch {
1433
+ return false;
1434
+ }
1435
+ }
1109
1436
  async function callRestEndpoint(method, path, args, opts = {}) {
1110
- const token = resolveApiKey() ?? await getAccessToken();
1437
+ const token = (await resolveCredential())?.token;
1111
1438
  if (!token) {
1112
1439
  return {
1113
1440
  ok: false,
@@ -1151,7 +1478,7 @@ async function callRestEndpoint(method, path, args, opts = {}) {
1151
1478
  signal: controller.signal,
1152
1479
  body: sendsBody ? JSON.stringify(rest) : void 0
1153
1480
  });
1154
- const raw = await res.text();
1481
+ const raw = (await res.text()).split(token).join("***");
1155
1482
  if (res.status === 202) {
1156
1483
  let confirmToken;
1157
1484
  let expires;
@@ -1187,6 +1514,15 @@ async function callRestEndpoint(method, path, args, opts = {}) {
1187
1514
  )
1188
1515
  };
1189
1516
  }
1517
+ if (res.status === 409 && orgNotConnected(raw)) {
1518
+ return {
1519
+ ok: false,
1520
+ text: withNextStep(redact(bodyText(raw).slice(0, 500)), [
1521
+ "Switch back: sign in again with `carrier login` and pick your connected organization",
1522
+ "Or use an org API key (ak_\u2026) from that organization: export CARRIER_API_KEY=\u2026"
1523
+ ])
1524
+ };
1525
+ }
1190
1526
  if (!res.ok) {
1191
1527
  return {
1192
1528
  ok: false,
@@ -1209,7 +1545,7 @@ async function callRestEndpoint(method, path, args, opts = {}) {
1209
1545
  ])
1210
1546
  };
1211
1547
  }
1212
- const msg = e instanceof Error ? e.message : String(e);
1548
+ const msg = (e instanceof Error ? e.message : String(e)).split(token).join("***");
1213
1549
  return {
1214
1550
  ok: false,
1215
1551
  text: withNextStep(redact(`Could not reach the Carrier API: ${msg}`), [
@@ -1718,7 +2054,7 @@ function record(value) {
1718
2054
  }
1719
2055
 
1720
2056
  // src/cli/lib/resend.ts
1721
- import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
2057
+ import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
1722
2058
  import { createServer as createServer2 } from "http";
1723
2059
  var RESEND_API = "https://api.resend.com";
1724
2060
  var RESEND_API_KEYS_URL = "https://resend.com/api-keys";
@@ -1736,7 +2072,7 @@ function base64url2(input) {
1736
2072
  }
1737
2073
  function pkcePair2() {
1738
2074
  const verifier = base64url2(randomBytes2(64));
1739
- return { verifier, challenge: base64url2(createHash2("sha256").update(verifier).digest()) };
2075
+ return { verifier, challenge: base64url2(createHash3("sha256").update(verifier).digest()) };
1740
2076
  }
1741
2077
  function buildAuthorizeUrl(opts) {
1742
2078
  const url = new URL(`${RESEND_API}/oauth/authorize`);
@@ -1884,9 +2220,9 @@ async function startLoopbackServer() {
1884
2220
  error ? { ok: false, reason: `Resend returned "${error}".` } : code ? { ok: true, code, reason: state ?? void 0 } : { ok: false, reason: "Resend sent no authorization code." }
1885
2221
  );
1886
2222
  });
1887
- await new Promise((resolve3, reject) => {
2223
+ await new Promise((resolve5, reject) => {
1888
2224
  server.once("error", reject);
1889
- server.listen(0, REDIRECT_HOST, resolve3);
2225
+ server.listen(0, REDIRECT_HOST, resolve5);
1890
2226
  });
1891
2227
  const port = server.address().port;
1892
2228
  const redirectUri = `http://${REDIRECT_HOST}:${port}${REDIRECT_PATH}`;
@@ -1894,12 +2230,12 @@ async function startLoopbackServer() {
1894
2230
  redirectUri,
1895
2231
  close: () => server.close(),
1896
2232
  waitForCode(state, timeoutMs) {
1897
- return new Promise((resolve3) => {
2233
+ return new Promise((resolve5) => {
1898
2234
  const done = (r) => {
1899
2235
  clearTimeout(timer);
1900
2236
  resolveCode = null;
1901
2237
  server.close();
1902
- resolve3(r);
2238
+ resolve5(r);
1903
2239
  };
1904
2240
  const timer = setTimeout(
1905
2241
  () => done({ ok: false, reason: "Timed out waiting for the browser." }),
@@ -2173,6 +2509,217 @@ function buildDashScreen(domain, payload) {
2173
2509
  }
2174
2510
  }
2175
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
+
2176
2723
  // src/cli/lib/home.ts
2177
2724
  import * as p from "@clack/prompts";
2178
2725
  import pc2 from "picocolors";
@@ -2203,7 +2750,7 @@ async function runHome(opts) {
2203
2750
  let st = await showHomeStatus();
2204
2751
  p.note(oauthFirstUseNote(), "How auth works");
2205
2752
  for (; ; ) {
2206
- const hasToken = !!resolveCliToken();
2753
+ const hasToken = !!await resolveCredential({ allowOcs: true });
2207
2754
  const choice = await p.select({
2208
2755
  message: "What do you want to do?",
2209
2756
  options: [
@@ -2239,8 +2786,8 @@ async function runHome(opts) {
2239
2786
  },
2240
2787
  {
2241
2788
  value: "ask",
2242
- label: hasToken ? "Ask the fleet (carrier ask)" : "Ask the fleet (needs CARRIER_API_KEY)",
2243
- 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"
2244
2791
  },
2245
2792
  {
2246
2793
  value: "site",
@@ -2278,8 +2825,8 @@ async function runHome(opts) {
2278
2825
  p.log.info("Link OCS credentials (BYO) or finish managed setup, then use Claude MCP.");
2279
2826
  break;
2280
2827
  case "ask": {
2281
- if (!resolveCliToken()) {
2282
- 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.");
2283
2830
  p.note(
2284
2831
  [
2285
2832
  "Interactive (recommended): install MCP, open Claude, ask in plain English.",
@@ -2314,6 +2861,68 @@ async function runHome(opts) {
2314
2861
  }
2315
2862
  }
2316
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
+
2317
2926
  // src/cli/lib/signup.ts
2318
2927
  var DEFAULT_MAX_CODE_ATTEMPTS = 3;
2319
2928
  function parseEnvironment(raw) {
@@ -5577,7 +6186,7 @@ async function interactiveSiteCreate() {
5577
6186
  defaultValue: "./storefront"
5578
6187
  });
5579
6188
  if (p3.isCancel(dir)) return;
5580
- const target = resolve2(dir);
6189
+ const target = resolve4(dir);
5581
6190
  if (await exists(target)) {
5582
6191
  const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
5583
6192
  if (p3.isCancel(go) || !go) {
@@ -5589,6 +6198,20 @@ async function interactiveSiteCreate() {
5589
6198
  p3.outro(pc4.green(`Scaffolded. cd ${dir} && pnpm install && pnpm dev`));
5590
6199
  }
5591
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
+ });
5592
6215
  program.name("carrier").description(
5593
6216
  [
5594
6217
  "Carrier CLI \u2014 status, OAuth-ready MCP install, storefront scaffold, and fleet control.",
@@ -5608,7 +6231,7 @@ program.command("init").description("Interactive home: status, install plugin+MC
5608
6231
  if (o.yes) {
5609
6232
  await doPluginInstall();
5610
6233
  const brand = CARRIER_BRAND;
5611
- const target = resolve2(o.dir);
6234
+ const target = resolve4(o.dir);
5612
6235
  if (await exists(target)) {
5613
6236
  info(pc4.yellow(`${target} exists \u2014 writing into it (--yes).`));
5614
6237
  }
@@ -5633,7 +6256,7 @@ program.command("init").description("Interactive home: status, install plugin+MC
5633
6256
  printStatus(st);
5634
6257
  await doPluginInstall();
5635
6258
  const brand = await promptBrand(CARRIER_BRAND);
5636
- const target = resolve2(o.dir);
6259
+ const target = resolve4(o.dir);
5637
6260
  if (await exists(target)) {
5638
6261
  const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
5639
6262
  if (p3.isCancel(go) || !go) {
@@ -5674,7 +6297,7 @@ program.command("init").description("Interactive home: status, install plugin+MC
5674
6297
  onInstall: doPluginInstall,
5675
6298
  onSiteCreate: async () => {
5676
6299
  const brand = await promptBrand(CARRIER_BRAND);
5677
- const target = resolve2(o.dir);
6300
+ const target = resolve4(o.dir);
5678
6301
  if (await exists(target)) {
5679
6302
  const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
5680
6303
  if (p3.isCancel(go) || !go) {
@@ -5749,7 +6372,7 @@ program.command("login").description("Sign in from this terminal. Opens a browse
5749
6372
  return;
5750
6373
  }
5751
6374
  p3.note(
5752
- [`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"),
5753
6376
  "Signed in"
5754
6377
  );
5755
6378
  } catch (e) {
@@ -6117,7 +6740,7 @@ site.command("create [dir]").description("Scaffold a white-labeled storefront fr
6117
6740
  } else if (!o.yes) {
6118
6741
  brand = await promptBrand(CARRIER_BRAND);
6119
6742
  }
6120
- const target = resolve2(dir ?? "./storefront");
6743
+ const target = resolve4(dir ?? "./storefront");
6121
6744
  if (await exists(target)) {
6122
6745
  const go = await p3.confirm({ message: `${target} exists \u2014 write into it anyway?`, initialValue: false });
6123
6746
  if (p3.isCancel(go) || !go) {
@@ -6142,7 +6765,7 @@ site.command("create [dir]").description("Scaffold a white-labeled storefront fr
6142
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(
6143
6766
  async (dir, o) => {
6144
6767
  header();
6145
- const target = resolve2(dir ?? "./storefront");
6768
+ const target = resolve4(dir ?? "./storefront");
6146
6769
  if (o.target && !isTargetId(o.target)) {
6147
6770
  fail(`Unknown target "${o.target}".`, [`Supported: ${TARGET_IDS.join(", ")}`]);
6148
6771
  process.exitCode = 1;
@@ -6192,7 +6815,7 @@ site.command("publish").description("Publish a Carrier-hosted storefront. Does n
6192
6815
  });
6193
6816
  site.command("targets [dir]").description("Show which deploy hosts are installed, logged in, and ready.").action(async (dir) => {
6194
6817
  header();
6195
- const storefront = resolve2(dir ?? "./storefront");
6818
+ const storefront = resolve4(dir ?? "./storefront");
6196
6819
  const statuses = await probeAll(storefront);
6197
6820
  const ranked = rankTargets(statuses);
6198
6821
  const rows2 = statuses.map((s) => {
@@ -6209,7 +6832,7 @@ site.command("targets [dir]").description("Show which deploy hosts are installed
6209
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(
6210
6833
  async (dir, o) => {
6211
6834
  header();
6212
- const storefront = resolve2(dir ?? "./storefront");
6835
+ const storefront = resolve4(dir ?? "./storefront");
6213
6836
  try {
6214
6837
  const brand = await loadStorefrontBrand(storefront, o.name ? { name: o.name } : void 0);
6215
6838
  const result = await provisionClerk({
@@ -6255,7 +6878,7 @@ site.command("clerk [dir]").description("Create or discover a Clerk instance and
6255
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(
6256
6879
  async (dir, o) => {
6257
6880
  header();
6258
- const storefront = resolve2(dir ?? "./storefront");
6881
+ const storefront = resolve4(dir ?? "./storefront");
6259
6882
  const interactive = process.stdin.isTTY === true && o.yes !== true;
6260
6883
  try {
6261
6884
  const brand = await loadStorefrontBrand(storefront);
@@ -6313,7 +6936,7 @@ site.command("email [dir]").description("Connect Resend in the browser so the st
6313
6936
  );
6314
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) => {
6315
6938
  header();
6316
- const target = resolve2(dir ?? "./storefront");
6939
+ const target = resolve4(dir ?? "./storefront");
6317
6940
  try {
6318
6941
  const brand = await loadStorefrontBrand(target);
6319
6942
  const logo = await writeStorefrontLogo(target, brand);