@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/README.md +137 -1
- package/dist/{chunk-RF3NNGZX.js → chunk-ZJY7H2QW.js} +12 -3
- package/dist/chunk-ZJY7H2QW.js.map +1 -0
- package/dist/cli.js +966 -343
- 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-RF3NNGZX.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
|
|
@@ -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
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
`
|
|
586
|
-
`
|
|
587
|
-
`
|
|
588
|
-
`
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
return
|
|
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 =
|
|
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()
|
|
@@ -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 =
|
|
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
|
|
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(
|
|
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((
|
|
2223
|
+
await new Promise((resolve5, reject) => {
|
|
1888
2224
|
server.once("error", reject);
|
|
1889
|
-
server.listen(0, REDIRECT_HOST,
|
|
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((
|
|
2233
|
+
return new Promise((resolve5) => {
|
|
1898
2234
|
const done = (r) => {
|
|
1899
2235
|
clearTimeout(timer);
|
|
1900
2236
|
resolveCode = null;
|
|
1901
2237
|
server.close();
|
|
1902
|
-
|
|
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 = !!
|
|
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 (
|
|
2243
|
-
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"
|
|
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 (!
|
|
2282
|
-
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.");
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
6939
|
+
const target = resolve4(dir ?? "./storefront");
|
|
6317
6940
|
try {
|
|
6318
6941
|
const brand = await loadStorefrontBrand(target);
|
|
6319
6942
|
const logo = await writeStorefrontLogo(target, brand);
|