@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/README.md +137 -1
- package/dist/{chunk-LJPYVILK.js → chunk-ZJY7H2QW.js} +12 -3
- package/dist/chunk-ZJY7H2QW.js.map +1 -0
- package/dist/cli.js +937 -330
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/native/claude-keychain.js +57 -0
- package/package.json +10 -2
- package/plugin/.claude-plugin/marketplace.json +2 -2
- package/plugin/carrier/.claude-plugin/plugin.json +1 -1
- package/shell/carrier.bash +56 -0
- package/shell/carrier.zsh +35 -0
- package/templates/storefront/open-next.config.ts +8 -1
- package/templates/storefront/wrangler.jsonc +1 -1
- package/dist/chunk-LJPYVILK.js.map +0 -1
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-
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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(
|
|
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
|
-
|
|
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",
|
|
404
|
-
rejectCode?.(new Error(
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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
|
|
576
|
-
|
|
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
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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
|
|
828
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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(
|
|
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((
|
|
2223
|
+
await new Promise((resolve5, reject) => {
|
|
1904
2224
|
server.once("error", reject);
|
|
1905
|
-
server.listen(0, REDIRECT_HOST,
|
|
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((
|
|
2233
|
+
return new Promise((resolve5) => {
|
|
1914
2234
|
const done = (r) => {
|
|
1915
2235
|
clearTimeout(timer);
|
|
1916
2236
|
resolveCode = null;
|
|
1917
2237
|
server.close();
|
|
1918
|
-
|
|
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 = !!
|
|
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 (
|
|
2259
|
-
hint: hasToken ? "Uses
|
|
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 (!
|
|
2298
|
-
p.log.warn("No
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
6939
|
+
const target = resolve4(dir ?? "./storefront");
|
|
6333
6940
|
try {
|
|
6334
6941
|
const brand = await loadStorefrontBrand(target);
|
|
6335
6942
|
const logo = await writeStorefrontLogo(target, brand);
|