@carrierllc/mcp 0.13.6 → 0.13.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  verifyStorefront,
40
40
  walletScreen,
41
41
  which
42
- } from "./chunk-BIZFEVMT.js";
42
+ } from "./chunk-DF2PGR5H.js";
43
43
  import {
44
44
  copyTree,
45
45
  exists,
@@ -272,465 +272,615 @@ async function writeStorefrontLogo(target, brand) {
272
272
  return { path: svgPath, source: logo.source };
273
273
  }
274
274
 
275
- // src/cli/lib/resend.ts
276
- import { createHash, randomBytes } from "crypto";
277
- import { createServer } from "http";
278
- var RESEND_API = "https://api.resend.com";
279
- var RESEND_API_KEYS_URL = "https://resend.com/api-keys";
280
- var RESEND_DOMAINS_URL = "https://resend.com/domains";
281
- var USER_AGENT = "carrier-cli";
282
- var CLIENT_NAME = "Carrier storefront setup";
283
- var OAUTH_SCOPE = "full_access";
284
- var REDIRECT_PATH = "/oauth/callback";
285
- var REDIRECT_HOST = "127.0.0.1";
286
- function looksResendKey(key) {
287
- return /^re_[A-Za-z0-9_-]{8,}$/.test(key.trim());
275
+ // src/cli/lib/hosted-publish.ts
276
+ import { readFileSync } from "fs";
277
+
278
+ // ../../packages/sdk/src/storefront-labels.ts
279
+ var regionNames = new Intl.DisplayNames(["en"], { type: "region" });
280
+ var PREFERRED = /* @__PURE__ */ new Set(["CW", "BF", "BJ", "MM", "CD", "RS", "DE", "FR", "GB", "VU", "ZW", "RU", "TL", "VN", "YE"]);
281
+ var NAME_TO_CODE = /* @__PURE__ */ new Map();
282
+ for (let first = 65; first <= 90; first += 1) {
283
+ for (let second = 65; second <= 90; second += 1) {
284
+ const code = String.fromCharCode(first, second);
285
+ const label = regionLabel(code);
286
+ if (!label) continue;
287
+ const key = label.toLowerCase();
288
+ const current = NAME_TO_CODE.get(key);
289
+ if (!current || PREFERRED.has(code)) NAME_TO_CODE.set(key, code);
290
+ }
288
291
  }
289
- function base64url(input) {
290
- return input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
292
+ NAME_TO_CODE.set("uk", "GB");
293
+ var CODE_ALIASES = { UK: "GB" };
294
+ function parseDataGb(raw) {
295
+ if (raw.trim().toLowerCase() === "unlimited") return { ok: true, value: 0 };
296
+ return parseAmount(raw, /gigabytes?|gbs?|gb/gi, "Enter data like 1, 1 GB, or Unlimited.", 1e4, false);
291
297
  }
292
- function pkcePair() {
293
- const verifier = base64url(randomBytes(64));
294
- return { verifier, challenge: base64url(createHash("sha256").update(verifier).digest()) };
298
+ function parseValidityDays(raw) {
299
+ return parseAmount(raw, /days?/gi, "Enter validity like 7 or 7 days.", 3650, true);
295
300
  }
296
- function buildAuthorizeUrl(opts) {
297
- const url = new URL(`${RESEND_API}/oauth/authorize`);
298
- url.searchParams.set("client_id", opts.clientId);
299
- url.searchParams.set("response_type", "code");
300
- url.searchParams.set("redirect_uri", opts.redirectUri);
301
- url.searchParams.set("scope", opts.scope ?? OAUTH_SCOPE);
302
- url.searchParams.set("state", opts.state);
303
- url.searchParams.set("code_challenge", opts.challenge);
304
- url.searchParams.set("code_challenge_method", "S256");
305
- return url.toString();
301
+ var ZERO_DECIMAL = /* @__PURE__ */ new Set([
302
+ "BIF",
303
+ "CLP",
304
+ "DJF",
305
+ "GNF",
306
+ "JPY",
307
+ "KMF",
308
+ "KRW",
309
+ "MGA",
310
+ "PYG",
311
+ "RWF",
312
+ "UGX",
313
+ "VND",
314
+ "VUV",
315
+ "XAF",
316
+ "XOF",
317
+ "XPF"
318
+ ]);
319
+ function majorToCents(input, currency = "EUR") {
320
+ const text3 = input.trim();
321
+ if (ZERO_DECIMAL.has(currency.trim().toUpperCase())) {
322
+ if (!/^\d+$/.test(text3)) return { ok: false, message: "Enter a whole number, such as 1000." };
323
+ const cents2 = Number(text3);
324
+ if (!Number.isSafeInteger(cents2) || cents2 <= 0) return { ok: false, message: "Enter a price above zero." };
325
+ return { ok: true, cents: cents2 };
326
+ }
327
+ const match = /^(\d+)(?:\.(\d{1,2}))?$/.exec(text3);
328
+ if (!match) return { ok: false, message: "Enter a price like 9.00." };
329
+ const major = Number(match[1]);
330
+ const fraction = (match[2] ?? "").padEnd(2, "0");
331
+ const cents = major * 100 + Number(fraction);
332
+ if (!Number.isSafeInteger(cents) || cents <= 0) return { ok: false, message: "Enter a price above zero." };
333
+ return { ok: true, cents };
306
334
  }
307
- async function resendFetch(fetchImpl, path, init = {}) {
308
- const headers = { "User-Agent": USER_AGENT, Accept: "application/json" };
309
- if (init.token) headers.Authorization = `Bearer ${init.token}`;
310
- let body;
311
- if (init.form) {
312
- headers["Content-Type"] = "application/x-www-form-urlencoded";
313
- body = new URLSearchParams(init.form).toString();
314
- } else if (init.json !== void 0) {
315
- headers["Content-Type"] = "application/json";
316
- body = JSON.stringify(init.json);
335
+ function parseCountryInput(raw) {
336
+ const regions = [];
337
+ const parts = raw.split(",").map((part) => part.trim()).filter((part) => part !== "");
338
+ for (const part of parts) {
339
+ const added = codesFor(part);
340
+ if (!added) return { ok: false, message: "Countries are names, such as France, or codes such as FR." };
341
+ for (const code of added) {
342
+ if (regions.length >= 40) break;
343
+ if (!regions.includes(code)) regions.push(code);
344
+ }
345
+ }
346
+ return { ok: true, regions };
347
+ }
348
+ function parseAmount(raw, unit, message, max, whole) {
349
+ const text3 = raw.trim().replace(unit, "").trim();
350
+ if (text3 === "" || !/^\d+(\.\d+)?$/.test(text3)) return { ok: false, message };
351
+ const value = Number(text3);
352
+ if (!Number.isFinite(value) || value < 0 || value > max) return { ok: false, message };
353
+ if (whole && !Number.isInteger(value)) return { ok: false, message };
354
+ return { ok: true, value };
355
+ }
356
+ function codesFor(part) {
357
+ const tokens = part.split(/\s+/).filter((token) => token !== "");
358
+ if (tokens.length > 1 && tokens.every((token) => /^[A-Za-z]{2}$/.test(token))) {
359
+ return tokens.map((token) => canonical(token));
360
+ }
361
+ if (/^[A-Za-z]{2}$/.test(part)) return [canonical(part)];
362
+ const code = NAME_TO_CODE.get(part.toLowerCase());
363
+ return code ? [code] : null;
364
+ }
365
+ function canonical(code) {
366
+ const upper = code.toUpperCase();
367
+ return CODE_ALIASES[upper] ?? upper;
368
+ }
369
+ var LOGO_DATA = /^data:image\/(png|jpeg|webp|gif);base64,[A-Za-z0-9+/]+={0,2}$/;
370
+ var LOGO_MAX_CHARS = 1e5;
371
+ var LOGO_MAX_BYTES = 70 * 1024;
372
+ function logoFileDataUrl(bytes) {
373
+ const mime = logoMime(bytes);
374
+ if (!mime || bytes.byteLength < 1 || bytes.byteLength > LOGO_MAX_BYTES) {
375
+ return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
376
+ }
377
+ const url = `data:${mime};base64,${toBase64(bytes)}`;
378
+ if (url.length > LOGO_MAX_CHARS || !LOGO_DATA.test(url)) {
379
+ return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
380
+ }
381
+ return { ok: true, url };
382
+ }
383
+ function logoReference(value) {
384
+ const trimmed = value.trim();
385
+ if (trimmed.startsWith("data:")) {
386
+ if (trimmed.length <= LOGO_MAX_CHARS && LOGO_DATA.test(trimmed)) return { ok: true, url: trimmed };
387
+ return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
317
388
  }
318
389
  try {
319
- const res = await fetchImpl(`${RESEND_API}${path}`, {
320
- method: init.method ?? "GET",
321
- headers,
322
- body
323
- });
324
- const text3 = await res.text();
325
- let json;
326
- try {
327
- json = text3 ? JSON.parse(text3) : void 0;
328
- } catch {
329
- json = void 0;
330
- }
331
- if (!res.ok) {
332
- return { ok: false, status: res.status, json, error: resendError(json) ?? text3.slice(0, 300) };
333
- }
334
- return { ok: true, status: res.status, json };
335
- } catch (e) {
336
- return { ok: false, status: 0, error: e instanceof Error ? e.message : String(e) };
390
+ const url = new URL(trimmed);
391
+ if (url.protocol === "https:") return { ok: true, url: url.toString() };
392
+ } catch {
393
+ return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
337
394
  }
395
+ return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
338
396
  }
339
- function resendError(json) {
340
- if (typeof json !== "object" || json === null) return void 0;
341
- const body = json;
342
- return body.message ?? body.error_description ?? body.error;
397
+ function toBase64(bytes) {
398
+ let binary = "";
399
+ for (const byte of bytes) binary += String.fromCharCode(byte);
400
+ return btoa(binary);
343
401
  }
344
- async function registerOauthClient(fetchImpl) {
345
- const registeredRedirect = `http://${REDIRECT_HOST}${REDIRECT_PATH}`;
346
- const res = await resendFetch(fetchImpl, "/oauth/register", {
402
+ function logoMime(bytes) {
403
+ if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
404
+ return "image/png";
405
+ }
406
+ if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
407
+ if (bytes.length >= 6 && bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) return "image/gif";
408
+ if (bytes.length >= 12 && bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
409
+ return "image/webp";
410
+ }
411
+ return null;
412
+ }
413
+ function regionLabel(code) {
414
+ if (!/^[A-Z]{2}$/.test(code)) return void 0;
415
+ try {
416
+ const label = regionNames.of(code);
417
+ if (!label || label.toUpperCase() === code) return void 0;
418
+ return label;
419
+ } catch {
420
+ return void 0;
421
+ }
422
+ }
423
+
424
+ // ../../packages/sdk/src/storefront-write.ts
425
+ var THEME_FIELDS = [
426
+ ["accent", "accent"],
427
+ ["accentDark", "accentDark"],
428
+ ["bg", "bg"],
429
+ ["text", "text"],
430
+ ["logoUrl", "logoUrl"]
431
+ ];
432
+ var COPY_FIELDS = [
433
+ ["hero", "hero"],
434
+ ["tagline", "tagline"],
435
+ ["supportEmail", "supportEmail"],
436
+ ["legalName", "legalName"]
437
+ ];
438
+ function storefrontBrandPatch(input, existing) {
439
+ const theme = overlay(existing?.theme, input, THEME_FIELDS);
440
+ const copy = overlay(existing?.copy, input, COPY_FIELDS);
441
+ return {
442
+ ...theme ? { theme } : {},
443
+ ...copy ? { copy } : {}
444
+ };
445
+ }
446
+ function completePlanInput(input, plans) {
447
+ if ((input.productId?.trim() ?? "") !== "") return input;
448
+ const touched = [
449
+ input.slug,
450
+ input.name,
451
+ input.priceCents,
452
+ input.currency,
453
+ input.templateId,
454
+ input.dataGb,
455
+ input.validityDays,
456
+ input.regions,
457
+ input.description,
458
+ input.active
459
+ ].some((value) => value !== void 0);
460
+ if (!touched) return input;
461
+ const slug2 = input.slug?.trim();
462
+ const match = slug2 ? plans.find((plan) => plan.slug === slug2) : plans.length === 1 ? plans[0] : void 0;
463
+ if (!match) {
464
+ const currency = sharedCurrency(plans);
465
+ if (!input.currency && currency) return { ...input, currency };
466
+ return input;
467
+ }
468
+ return {
469
+ ...input,
470
+ slug: input.slug ?? match.slug,
471
+ name: input.name ?? match.name,
472
+ currency: input.currency ?? match.currency,
473
+ templateId: input.templateId ?? match.ocs_package_template_id
474
+ };
475
+ }
476
+ function sharedCurrency(plans) {
477
+ const codes = new Set(plans.map((plan) => plan.currency.trim().toUpperCase()).filter((code) => code !== ""));
478
+ if (codes.size !== 1) return void 0;
479
+ return [...codes][0];
480
+ }
481
+ function storefrontPlanBody(input) {
482
+ const productId = input.productId?.trim() ?? "";
483
+ if (productId !== "") {
484
+ const body2 = { product_id: productId };
485
+ if (input.priceCents !== void 0) body2.price_cents = input.priceCents;
486
+ if (input.active !== void 0) body2.active = input.active;
487
+ return body2;
488
+ }
489
+ const touched = [
490
+ input.slug,
491
+ input.name,
492
+ input.priceCents,
493
+ input.currency,
494
+ input.templateId,
495
+ input.dataGb,
496
+ input.validityDays,
497
+ input.regions,
498
+ input.description,
499
+ input.active
500
+ ].some((value) => value !== void 0);
501
+ if (!touched) return null;
502
+ if (!input.slug || !input.name || input.priceCents === void 0 || !input.currency || input.templateId === void 0) {
503
+ return { error: "A plan needs a slug, name, price in minor units, currency, and OCS template id." };
504
+ }
505
+ const body = {
506
+ slug: input.slug,
507
+ name: input.name,
508
+ price_cents: input.priceCents,
509
+ currency: input.currency,
510
+ ocs_package_template_id: input.templateId
511
+ };
512
+ if (input.dataGb !== void 0) body.data_gb = input.dataGb;
513
+ if (input.validityDays !== void 0) body.validity_days = input.validityDays;
514
+ if (input.regions !== void 0) body.regions = input.regions;
515
+ if (input.description !== void 0) body.description = input.description;
516
+ if (input.active !== void 0) body.active = input.active;
517
+ return body;
518
+ }
519
+ function isStorefrontPlanError(value) {
520
+ return value !== null && "error" in value;
521
+ }
522
+ function parseWholeNumber(value, label) {
523
+ if (value === void 0) return { ok: true, value: void 0 };
524
+ if (!/^\d+$/.test(value)) return { ok: false, message: `${label} must be a whole number.` };
525
+ return { ok: true, value: Number(value) };
526
+ }
527
+ function overlay(base, input, fields) {
528
+ const patch = {};
529
+ let touched = false;
530
+ for (const [field, jsonKey] of fields) {
531
+ const value = input[field];
532
+ if (typeof value !== "string" || value.trim() === "") continue;
533
+ patch[jsonKey] = value.trim();
534
+ touched = true;
535
+ }
536
+ if (!touched) return void 0;
537
+ return { ...base ?? {}, ...patch };
538
+ }
539
+ var RESERVED_SLUGS = /* @__PURE__ */ new Set(["www", "app", "api", "stores", "checkout", "dashboard", "admin", "mail"]);
540
+ function hostedStorefrontUrl(slug2) {
541
+ return `https://${slug2}.stores.carrier.llc`;
542
+ }
543
+ function storefrontSlugFrom(raw) {
544
+ const folded = raw.trim().toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
545
+ const clipped = folded.slice(0, 40).replace(/-+$/g, "");
546
+ if (RESERVED_SLUGS.has(clipped)) return "";
547
+ return /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/.test(clipped) ? clipped : "";
548
+ }
549
+
550
+ // src/cli/lib/auth.ts
551
+ import { createHash, randomBytes, timingSafeEqual } from "crypto";
552
+ import { createServer } from "http";
553
+
554
+ // src/cli/lib/credentials.ts
555
+ import { homedir as homedir2 } from "os";
556
+ import { join as join5 } from "path";
557
+ import { chmod, mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile3 } from "fs/promises";
558
+ function carrierHome() {
559
+ return process.env.CARRIER_HOME?.trim() || join5(homedir2(), ".carrier");
560
+ }
561
+ function credentialsPath() {
562
+ return join5(carrierHome(), "credentials.json");
563
+ }
564
+ async function readCredentials() {
565
+ try {
566
+ const raw = await readFile2(credentialsPath(), "utf8");
567
+ const parsed = JSON.parse(raw);
568
+ if (!parsed?.accessToken || typeof parsed.accessToken !== "string") return null;
569
+ return parsed;
570
+ } catch {
571
+ return null;
572
+ }
573
+ }
574
+ async function writeCredentials(creds) {
575
+ const dir = carrierHome();
576
+ await mkdir2(dir, { recursive: true, mode: 448 });
577
+ await chmod(dir, 448).catch(() => void 0);
578
+ const file = credentialsPath();
579
+ await writeFile3(file, `${JSON.stringify(creds, null, 2)}
580
+ `, { mode: 384 });
581
+ await chmod(file, 384).catch(() => void 0);
582
+ }
583
+ async function clearCredentials() {
584
+ await rm(credentialsPath(), { force: true });
585
+ }
586
+ function isExpired(creds, skewMs = 6e4) {
587
+ if (!creds.expiresAt) return false;
588
+ return Date.now() >= creds.expiresAt - skewMs;
589
+ }
590
+ function redact(text3) {
591
+ return text3.replace(/\b(ak_|sk_|rt_)[A-Za-z0-9._-]{6,}/g, "$1***").replace(/\bBearer\s+[A-Za-z0-9._-]{8,}/gi, "Bearer ***");
592
+ }
593
+
594
+ // src/cli/lib/auth.ts
595
+ var DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
596
+ var CLIENT_NAME = "Carrier CLI";
597
+ var DEFAULT_SCOPE = "read write";
598
+ var CALLBACK_PORTS = [3118, 3119, 8976, 8977, 0];
599
+ async function discover(base = MCP_HOME) {
600
+ const res = await fetch(`${base}${DISCOVERY_PATH}`, {
601
+ headers: { Accept: "application/json" }
602
+ });
603
+ if (!res.ok) throw new Error(`OAuth discovery failed (${res.status}) at ${base}${DISCOVERY_PATH}`);
604
+ const meta = await res.json();
605
+ if (!meta.authorization_endpoint || !meta.token_endpoint) {
606
+ throw new Error("OAuth discovery returned no authorization or token endpoint");
607
+ }
608
+ return meta;
609
+ }
610
+ function base64url(buf) {
611
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
612
+ }
613
+ function pkcePair() {
614
+ const verifier = base64url(randomBytes(32));
615
+ const challenge = base64url(createHash("sha256").update(verifier).digest());
616
+ return { verifier, challenge };
617
+ }
618
+ function sameState(a, b) {
619
+ const left = Buffer.from(a);
620
+ const right = Buffer.from(b);
621
+ if (left.length !== right.length) return false;
622
+ return timingSafeEqual(left, right);
623
+ }
624
+ async function registerClient(meta, redirectUri) {
625
+ if (!meta.registration_endpoint) {
626
+ throw new Error("This server does not support dynamic client registration");
627
+ }
628
+ const res = await fetch(meta.registration_endpoint, {
347
629
  method: "POST",
348
- json: {
630
+ headers: { "Content-Type": "application/json", Accept: "application/json" },
631
+ body: JSON.stringify({
349
632
  client_name: CLIENT_NAME,
350
- redirect_uris: [registeredRedirect],
633
+ redirect_uris: [redirectUri],
351
634
  grant_types: ["authorization_code", "refresh_token"],
352
635
  response_types: ["code"],
636
+ // Installed apps cannot keep a secret; register as a public client.
353
637
  token_endpoint_auth_method: "none",
354
- scope: OAUTH_SCOPE
355
- }
638
+ application_type: "native"
639
+ })
356
640
  });
357
641
  if (!res.ok) {
358
- return { ok: false, reason: `Resend rejected client registration: ${res.error ?? res.status}` };
642
+ const body = (await res.text().catch(() => "")).slice(0, 200);
643
+ throw new Error(`Client registration failed (${res.status}). ${body}`);
359
644
  }
360
- const clientId = res.json?.client_id;
361
- if (!clientId) return { ok: false, reason: "Resend registered no client_id." };
362
- return { ok: true, clientId };
363
- }
364
- async function exchangeCode(fetchImpl, opts) {
365
- const res = await resendFetch(fetchImpl, "/oauth/token", {
366
- method: "POST",
367
- form: {
368
- grant_type: "authorization_code",
369
- client_id: opts.clientId,
370
- code: opts.code,
371
- redirect_uri: opts.redirectUri,
372
- code_verifier: opts.verifier
373
- }
374
- });
375
- if (!res.ok) return { ok: false, reason: `Token exchange failed: ${res.error ?? res.status}` };
376
- const body = res.json;
377
- if (!body?.access_token) return { ok: false, reason: "Resend returned no access token." };
378
- return { ok: true, accessToken: body.access_token, refreshToken: body.refresh_token };
645
+ const json = await res.json();
646
+ if (!json.client_id) throw new Error("Client registration returned no client_id");
647
+ return json.client_id;
379
648
  }
380
- async function revokeGrant(fetchImpl, opts) {
381
- await resendFetch(fetchImpl, "/oauth/revoke", {
382
- method: "POST",
383
- form: {
384
- client_id: opts.clientId,
385
- token: opts.refreshToken,
386
- token_type_hint: "refresh_token"
387
- }
388
- }).catch(() => void 0);
649
+ function escapeHtml(text3) {
650
+ return text3.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
389
651
  }
390
- async function findVerifiedDomainId(fetchImpl, accessToken, domain) {
391
- const wanted = domain.trim().replace(/^https?:\/\//, "").replace(/\/+$/, "").toLowerCase();
392
- if (!wanted) return void 0;
393
- const res = await resendFetch(fetchImpl, "/domains", { token: accessToken });
394
- if (!res.ok) return void 0;
395
- const list = res.json?.data;
396
- const match = list?.find((d) => d.name?.toLowerCase() === wanted && d.status === "verified");
397
- return match?.id;
652
+ function page(title, detail) {
653
+ const safeTitle = escapeHtml(title);
654
+ const safeDetail = escapeHtml(detail);
655
+ return `<!doctype html><meta charset="utf-8"><title>${safeTitle}</title>
656
+ <body style="font-family:system-ui,sans-serif;max-width:32rem;margin:20vh auto;text-align:center">
657
+ <h1 style="font-size:1.25rem">${safeTitle}</h1><p style="color:#555">${safeDetail}</p></body>`;
398
658
  }
399
- async function createSendingKey(fetchImpl, accessToken, opts) {
400
- const res = await resendFetch(fetchImpl, "/api-keys", {
401
- method: "POST",
402
- token: accessToken,
403
- json: {
404
- // Resend caps the name at 50 characters.
405
- name: opts.name.slice(0, 50),
406
- permission: "sending_access",
407
- ...opts.domainId ? { domain_id: opts.domainId } : {}
659
+ async function startCallbackServer() {
660
+ let resolveCode;
661
+ let rejectCode;
662
+ let expected = "";
663
+ const server = createServer((req, res) => {
664
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
665
+ if (url.pathname !== "/callback") {
666
+ res.writeHead(404).end();
667
+ return;
408
668
  }
409
- });
410
- if (!res.ok) return { ok: false, reason: `Could not create an API key: ${res.error ?? res.status}` };
411
- const body = res.json;
412
- if (!body?.token) return { ok: false, reason: "Resend created a key but returned no token." };
413
- return { ok: true, token: body.token, id: body.id };
414
- }
415
- async function validateResendKey(fetchImpl, key) {
416
- if (!looksResendKey(key)) return { ok: false, reason: "That does not look like a Resend key (re_\u2026)." };
417
- const res = await resendFetch(fetchImpl, "/emails", { method: "POST", token: key, json: {} });
418
- if (res.status === 401) return { ok: false, reason: "Resend says that key is missing or malformed." };
419
- if (res.status === 403) return { ok: false, reason: "Resend rejected that key as invalid." };
420
- if (res.status === 0) return { ok: false, reason: `Could not reach Resend: ${res.error ?? "network error"}` };
421
- return { ok: true };
422
- }
423
- async function startLoopbackServer() {
424
- let resolveCode = null;
425
- const server = createServer((req, res) => {
426
- const url = new URL(req.url ?? "/", `http://${REDIRECT_HOST}`);
427
- if (url.pathname !== REDIRECT_PATH) {
428
- res.writeHead(404).end();
669
+ const err = url.searchParams.get("error");
670
+ const code = url.searchParams.get("code");
671
+ const state = url.searchParams.get("state") ?? "";
672
+ if (err) {
673
+ res.writeHead(400, { "Content-Type": "text/html" });
674
+ res.end(page("Sign-in failed", err));
675
+ rejectCode?.(new Error(`Authorization server returned "${err}"`));
429
676
  return;
430
677
  }
431
- const error = url.searchParams.get("error");
432
- const code = url.searchParams.get("code");
433
- const state = url.searchParams.get("state");
434
- res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
435
- res.end(
436
- `<!doctype html><meta charset="utf-8"><title>Carrier</title><body style="font-family:system-ui;padding:3rem;text-align:center"><p>${error ? "Authorization was refused." : "Connected. You can close this tab."}</p>`
437
- );
438
- resolveCode?.(
439
- error ? { ok: false, reason: `Resend returned "${error}".` } : code ? { ok: true, code, reason: state ?? void 0 } : { ok: false, reason: "Resend sent no authorization code." }
440
- );
441
- });
442
- await new Promise((resolve3, reject) => {
443
- server.once("error", reject);
444
- server.listen(0, REDIRECT_HOST, resolve3);
678
+ if (!code || !expected || !sameState(state, expected)) {
679
+ res.writeHead(400, { "Content-Type": "text/html" });
680
+ res.end(page("Sign-in failed", "The redirect did not match this login attempt."));
681
+ rejectCode?.(new Error("State mismatch \u2014 ignoring the redirect"));
682
+ return;
683
+ }
684
+ res.writeHead(200, { "Content-Type": "text/html" });
685
+ res.end(page("You're signed in", "Close this tab and return to your terminal."));
686
+ resolveCode?.(code);
445
687
  });
446
- const port = server.address().port;
447
- const redirectUri = `http://${REDIRECT_HOST}:${port}${REDIRECT_PATH}`;
688
+ let bound = false;
689
+ let lastErr;
690
+ for (const port2 of CALLBACK_PORTS) {
691
+ try {
692
+ await new Promise((resolve3, reject) => {
693
+ server.once("error", reject);
694
+ server.listen(port2, "127.0.0.1", () => {
695
+ server.removeListener("error", reject);
696
+ resolve3();
697
+ });
698
+ });
699
+ bound = true;
700
+ break;
701
+ } catch (e) {
702
+ lastErr = e;
703
+ }
704
+ }
705
+ if (!bound) {
706
+ throw new Error(
707
+ `Could not bind a local callback port (tried ${CALLBACK_PORTS.filter(Boolean).join(", ")}): ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
708
+ );
709
+ }
710
+ const { port } = server.address();
448
711
  return {
449
- redirectUri,
450
- close: () => server.close(),
451
- waitForCode(state, timeoutMs) {
452
- return new Promise((resolve3) => {
453
- const done = (r) => {
454
- clearTimeout(timer);
455
- resolveCode = null;
456
- server.close();
457
- resolve3(r);
458
- };
712
+ redirectUri: `http://localhost:${port}/callback`,
713
+ // `close()` alone only stops new connections; a keep-alive socket from the
714
+ // browser would hold the event loop open and the CLI would never exit.
715
+ waitForCode: (expectedState, timeoutMs) => {
716
+ expected = expectedState;
717
+ return new Promise((resolve3, reject) => {
459
718
  const timer = setTimeout(
460
- () => done({ ok: false, reason: "Timed out waiting for the browser." }),
719
+ () => reject(new Error(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for the browser`)),
461
720
  timeoutMs
462
721
  );
463
- resolveCode = (r) => {
464
- if (r.ok && r.reason !== state) {
465
- done({ ok: false, reason: "The browser callback did not match this request." });
466
- return;
467
- }
468
- done(r.ok ? { ok: true, code: r.code } : r);
722
+ resolveCode = (code) => {
723
+ clearTimeout(timer);
724
+ resolve3(code);
725
+ };
726
+ rejectCode = (err) => {
727
+ clearTimeout(timer);
728
+ reject(err);
469
729
  };
470
730
  });
731
+ },
732
+ close: () => {
733
+ server.closeAllConnections();
734
+ server.close();
471
735
  }
472
736
  };
473
737
  }
474
- function discoverResendKey(env) {
475
- const key = env.RESEND_API_KEY?.trim();
476
- if (!key || !looksResendKey(key)) return void 0;
477
- return { apiKey: key, tier: "discovered" };
478
- }
479
- var MANUAL_STEPS = [
480
- `Open ${RESEND_API_KEYS_URL} and create an API key.`,
481
- `Give it Sending access, and restrict it to your sending domain if that domain is verified at ${RESEND_DOMAINS_URL}.`,
482
- "Put the key in the storefront's .env.local as RESEND_API_KEY.",
483
- "Re-run `carrier site deploy` \u2014 the key is pushed to the deploy target automatically."
484
- ];
485
- async function provisionResend(opts) {
486
- const { deps } = opts;
487
- const notes = [];
488
- const discovered = discoverResendKey(opts.env);
489
- if (discovered) {
490
- const check = await validateResendKey(deps.fetch, discovered.apiKey);
491
- if (check.ok) return { ok: true, tier: "discovered", credentials: discovered };
492
- notes.push(`A RESEND_API_KEY was already set but did not work: ${check.reason}`);
493
- }
494
- if (!opts.noBrowser) {
495
- const viaBrowser = await oauthTier(opts);
496
- if (viaBrowser.ok) {
497
- return { ...viaBrowser, reason: notes.length ? notes.join(" ") : void 0 };
498
- }
499
- notes.push(viaBrowser.reason ?? "The browser authorization did not complete.");
500
- }
501
- if (deps.promptForKey) {
502
- await deps.openBrowser(RESEND_API_KEYS_URL);
503
- const pasted = (await deps.promptForKey(RESEND_API_KEYS_URL))?.trim();
504
- if (pasted) {
505
- const check = await validateResendKey(deps.fetch, pasted);
506
- if (check.ok) {
507
- return {
508
- ok: true,
509
- tier: "paste",
510
- credentials: { apiKey: pasted, tier: "paste" },
511
- reason: notes.length ? notes.join(" ") : void 0
512
- };
513
- }
514
- notes.push(check.reason ?? "The pasted key did not work.");
515
- }
738
+ async function postToken(meta, body) {
739
+ const res = await fetch(meta.token_endpoint, {
740
+ method: "POST",
741
+ headers: {
742
+ "Content-Type": "application/x-www-form-urlencoded",
743
+ Accept: "application/json"
744
+ },
745
+ body: new URLSearchParams(body).toString()
746
+ });
747
+ const json = await res.json().catch(() => ({}));
748
+ if (!res.ok || json.error) {
749
+ throw new Error(json.error_description || json.error || `Token endpoint returned ${res.status}`);
516
750
  }
517
- return { ok: false, tier: "manual", reason: notes.join(" "), guidance: MANUAL_STEPS };
751
+ if (!json.access_token) throw new Error("Token endpoint returned no access_token");
752
+ return json;
518
753
  }
519
- async function oauthTier(opts) {
520
- const { deps } = opts;
521
- let server;
522
- try {
523
- server = await deps.startLoopback();
524
- } catch (e) {
525
- return {
526
- ok: false,
527
- tier: "oauth",
528
- reason: `Could not open a local callback listener: ${e instanceof Error ? e.message : String(e)}`
529
- };
530
- }
754
+ function store(meta, token, clientId) {
755
+ return {
756
+ issuer: meta.issuer,
757
+ accessToken: token.access_token,
758
+ refreshToken: token.refresh_token,
759
+ expiresAt: Date.now() + (token.expires_in ?? 900) * 1e3,
760
+ scope: token.scope,
761
+ clientId
762
+ };
763
+ }
764
+ async function login(opts) {
765
+ const meta = await discover();
766
+ const server = await startCallbackServer();
531
767
  try {
532
- const registered = await registerOauthClient(deps.fetch);
533
- if (!registered.ok) return { ok: false, tier: "oauth", reason: registered.reason };
768
+ const existing = await readCredentials();
769
+ const clientId = existing?.clientId && existing.issuer === meta.issuer ? existing.clientId : await registerClient(meta, server.redirectUri);
534
770
  const { verifier, challenge } = pkcePair();
535
- const state = base64url(randomBytes(24));
536
- const authorizeUrl = buildAuthorizeUrl({
537
- clientId: registered.clientId,
538
- redirectUri: server.redirectUri,
539
- state,
540
- challenge
541
- });
542
- const opened = await deps.openBrowser(authorizeUrl);
543
- deps.notify(
544
- opened.ok ? "Approve the connection in your browser \u2014 this waits for you." : `Open this to approve the connection:
545
- ${authorizeUrl}`
546
- );
547
- const callback = await server.waitForCode(state, opts.timeoutMs ?? 18e4);
548
- if (!callback.ok || !callback.code) {
549
- return { ok: false, tier: "oauth", reason: callback.reason ?? "No authorization code." };
550
- }
551
- const token = await exchangeCode(deps.fetch, {
552
- clientId: registered.clientId,
553
- code: callback.code,
554
- redirectUri: server.redirectUri,
555
- verifier
556
- });
557
- if (!token.ok) return { ok: false, tier: "oauth", reason: token.reason };
558
- const domainId = opts.domain ? await findVerifiedDomainId(deps.fetch, token.accessToken, opts.domain) : void 0;
559
- const created = await createSendingKey(deps.fetch, token.accessToken, {
560
- name: `${opts.name} storefront`,
561
- domainId
771
+ const state = base64url(randomBytes(16));
772
+ const authUrl = new URL(meta.authorization_endpoint);
773
+ authUrl.searchParams.set("response_type", "code");
774
+ authUrl.searchParams.set("client_id", clientId);
775
+ authUrl.searchParams.set("redirect_uri", server.redirectUri);
776
+ authUrl.searchParams.set("scope", pickScope(meta));
777
+ authUrl.searchParams.set("state", state);
778
+ authUrl.searchParams.set("code_challenge", challenge);
779
+ authUrl.searchParams.set("code_challenge_method", "S256");
780
+ const pendingCode = server.waitForCode(state, opts.timeoutMs ?? 3e5);
781
+ pendingCode.catch(() => void 0);
782
+ opts.onUrl?.(authUrl.toString());
783
+ await opts.openBrowser(authUrl.toString());
784
+ const code = await pendingCode;
785
+ const token = await postToken(meta, {
786
+ grant_type: "authorization_code",
787
+ code,
788
+ redirect_uri: server.redirectUri,
789
+ client_id: clientId,
790
+ code_verifier: verifier
562
791
  });
563
- if (token.refreshToken) {
564
- await revokeGrant(deps.fetch, {
565
- clientId: registered.clientId,
566
- refreshToken: token.refreshToken
567
- });
568
- }
569
- if (!created.ok) return { ok: false, tier: "oauth", reason: created.reason };
570
- const check = await validateResendKey(deps.fetch, created.token);
571
- if (!check.ok) {
572
- return { ok: false, tier: "oauth", reason: `Resend minted a key that it then rejected: ${check.reason}` };
573
- }
574
- return {
575
- ok: true,
576
- tier: "oauth",
577
- credentials: {
578
- apiKey: created.token,
579
- tier: "oauth",
580
- keyId: created.id,
581
- domain: domainId ? opts.domain : void 0
582
- }
583
- };
792
+ await writeCredentials(store(meta, token, clientId));
793
+ return { ok: true, message: "Signed in.", scope: token.scope };
584
794
  } finally {
585
795
  server.close();
586
796
  }
587
797
  }
588
-
589
- // src/cli/lib/dash.ts
590
- var DASH_DOMAINS = [
591
- "fleet",
592
- "subscribers",
593
- "usage",
594
- "packages",
595
- "billing",
596
- "wallet",
597
- "greenzone",
598
- "storefront"
599
- ];
600
- function isDashDomain(value) {
601
- return DASH_DOMAINS.includes(value);
798
+ function pickScope(meta) {
799
+ const supported = meta.scopes_supported ?? [];
800
+ if (!supported.length) return DEFAULT_SCOPE;
801
+ return DEFAULT_SCOPE.split(" ").filter((s) => supported.includes(s)).join(" ") || supported[0];
602
802
  }
603
- var DASH_TOOL = {
604
- fleet: "fleet_health_app",
605
- subscribers: "list_subscribers",
606
- usage: "subscriber_usage",
607
- packages: "list_package_templates",
608
- billing: "billing_events",
609
- wallet: "wallet_balance",
610
- greenzone: "greenzone_whitelist_list",
611
- storefront: ""
612
- };
613
- function screenFromStructured(structured) {
614
- const rec = structured;
615
- const screen = rec?.screen;
616
- if (screen && typeof screen === "object" && Array.isArray(screen.sections) && screen.title) {
617
- return screen;
803
+ async function getAccessToken() {
804
+ const creds = await readCredentials();
805
+ if (!creds) return null;
806
+ if (!isExpired(creds)) return creds.accessToken;
807
+ if (!creds.refreshToken || !creds.clientId) return null;
808
+ try {
809
+ const meta = await discover();
810
+ const token = await postToken(meta, {
811
+ grant_type: "refresh_token",
812
+ refresh_token: creds.refreshToken,
813
+ client_id: creds.clientId
814
+ });
815
+ const next = store(meta, token, creds.clientId);
816
+ if (!next.refreshToken) next.refreshToken = creds.refreshToken;
817
+ await writeCredentials(next);
818
+ return next.accessToken;
819
+ } catch {
820
+ return null;
618
821
  }
619
- return void 0;
620
822
  }
621
- var num = (value, fallback = 0) => {
622
- const n = Number(value);
623
- return Number.isFinite(n) ? n : fallback;
624
- };
625
- var str = (value, fallback = "") => value === void 0 || value === null ? fallback : String(value);
626
- function rows(payload, keys) {
627
- if (Array.isArray(payload)) return payload;
628
- const rec = payload ?? {};
629
- for (const key of keys) {
630
- const candidate = rec[key];
631
- if (Array.isArray(candidate)) return candidate;
632
- }
633
- return [];
823
+ async function logout() {
824
+ await clearCredentials();
634
825
  }
635
- function buildDashScreen(domain, payload) {
636
- switch (domain) {
637
- case "fleet": {
638
- const accounts = rows(payload, ["accounts", "account"]);
639
- return fleetScreen({
640
- active: num(payload?.active),
641
- suspended: num(payload?.suspended),
642
- inventory: num(payload?.inventory),
643
- other: num(payload?.other),
644
- accounts: accounts.map((a) => ({
645
- name: str(a.name ?? a.accountId, "?"),
646
- balance: num(a.balance),
647
- active: num(a.active),
648
- suspended: num(a.suspended),
649
- inventory: num(a.inventory),
650
- other: num(a.other),
651
- packageOnly: Boolean(a.packageOnly)
652
- }))
653
- });
654
- }
655
- case "subscribers":
656
- return subscribersScreen(
657
- rows(payload, ["subscribers", "subscriber", "data"]).map((s) => ({
658
- iccid: str(s.iccid ?? s.ICCID, "?"),
659
- msisdn: s.msisdn ? str(s.msisdn) : void 0,
660
- status: str(s.status ?? s.statusStr, "unknown"),
661
- account: s.accountName ? str(s.accountName) : void 0,
662
- dataUsedBytes: s.dataUsed === void 0 ? void 0 : num(s.dataUsed)
663
- }))
664
- );
665
- case "usage":
666
- return usageScreen({
667
- subject: str(payload?.subject, "Subscriber"),
668
- timeline: rows(payload, ["timeline", "days", "usage"]).map((p4) => ({
669
- date: str(p4.date ?? p4.day, "?"),
670
- bytes: num(p4.bytes ?? p4.dataUsed)
671
- })),
672
- countries: rows(payload, ["countries"]).map((c) => ({
673
- country: str(c.country ?? c.countryName, "?"),
674
- bytes: num(c.bytes ?? c.dataUsed)
675
- }))
676
- });
677
- case "packages":
678
- return packagesScreen(
679
- rows(payload, ["template", "templates", "data"]).map((t) => ({
680
- name: str(t.prepaidpackagetemplatename ?? t.name, "?"),
681
- id: str(t.prepaidpackagetemplateid ?? t.id, "?"),
682
- dataLimitBytes: t.databyte === void 0 ? void 0 : num(t.databyte),
683
- validityDays: t.perioddays === void 0 ? void 0 : num(t.perioddays),
684
- price: t.cost === void 0 ? void 0 : num(t.cost),
685
- recurring: Boolean(t.recurring)
686
- }))
687
- );
688
- case "billing":
689
- return billingScreen({
690
- balance: payload?.balance === void 0 ? void 0 : num(payload.balance),
691
- currency: str(payload?.currency, ""),
692
- events: rows(payload, ["events", "data"]).map((e) => ({
693
- date: str(e.date ?? e.timestamp, "?"),
694
- description: str(e.description ?? e.type, "\u2014"),
695
- amount: num(e.amount)
696
- }))
697
- });
698
- case "wallet": {
699
- const p4 = payload ?? {};
700
- const eur = (value) => value === void 0 || value === null ? void 0 : num(value) / 100;
701
- return walletScreen({
702
- balance: eur(p4.balance_eur_cents ?? p4.balance) ?? 0,
703
- currency: str(p4.currency, "\u20AC"),
704
- autoTopupEnabled: Boolean(p4.autoTopupEnabled ?? p4.auto_topup_enabled),
705
- threshold: eur(p4.auto_topup_threshold_cents ?? p4.threshold),
706
- credits: p4.credits === void 0 || p4.credits === null ? void 0 : num(p4.credits)
707
- });
708
- }
709
- case "greenzone":
710
- return greenzoneScreen(
711
- rows(payload, ["entries", "whitelist", "data"]).map((e) => ({
712
- value: str(e.value ?? e.prefix ?? e.destination, "?"),
713
- note: e.note ? str(e.note) : void 0
714
- }))
715
- );
716
- case "storefront": {
717
- const p4 = payload ?? {};
718
- return storefrontScreen({
719
- brand: str(p4.brand ?? p4.project, "storefront"),
720
- url: p4.url ? str(p4.url) : void 0,
721
- target: p4.target ? str(p4.target) : void 0,
722
- verified: p4.verified === void 0 ? null : Boolean(p4.verified),
723
- diagnosis: p4.diagnosis ? str(p4.diagnosis) : void 0,
724
- secretsStaged: Array.isArray(p4.secrets_staged) ? p4.secrets_staged : void 0,
725
- plans: p4.plans === void 0 ? void 0 : num(p4.plans)
726
- });
727
- }
728
- }
826
+
827
+ // src/cli/lib/guidance.ts
828
+ import pc from "picocolors";
829
+ var FLEET_NL_EXAMPLES = [
830
+ { label: "Fleet health", prompt: "Show my fleet health \u2014 accounts, eSIMs, low-balance alerts" },
831
+ { label: "Subscriber lookup", prompt: "Look up subscriber ICCID 8944\u2026 and diagnose connectivity" },
832
+ { label: "Usage anomalies", prompt: "Detect usage anomalies and burn-rate risks this week" },
833
+ { label: "Issue from inventory", prompt: "Issue 3 free eSIMs from inventory on my account and assign the starter package" },
834
+ { label: "Billing check", prompt: "What's my platform credit balance and Stripe Connect payout status?" },
835
+ { label: "Wallet balance", prompt: "What's my managed prepaid wallet balance and auto-top-up status?" },
836
+ { label: "Churn risk", prompt: "List high churn-risk subscribers with retention ideas" }
837
+ ];
838
+ function oauthFirstUseNote() {
839
+ return [
840
+ "Entry A \u2014 zero credentials up front",
841
+ ` MCP URL: ${MCP_URL}`,
842
+ " Auth: OAuth on first use (Clerk \u2014 Google, GitHub, or email)",
843
+ " Token: none required in Claude / Cursor / Windsurf",
844
+ "",
845
+ "When you first talk to Carrier in your MCP client, the browser opens",
846
+ "sign-in/sign-up. After authorize, tools work. Headless? Use an org API key",
847
+ `(ak_\u2026) from ${CONSOLE_URL} \u2192 Settings \u2192 API Keys.`
848
+ ].join("\n");
849
+ }
850
+ function formatStatusBlock(st) {
851
+ const yn = (ok2, yes = "yes", no = "no") => ok2 ? pc.green(yes) : pc.yellow(no);
852
+ 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");
853
+ return [
854
+ `Claude Code: ${yn(st.claudeFound, "found", "not on PATH")}`,
855
+ `Plugin: ${st.pluginInstalled ? pc.green(`installed v${st.pluginVersion ?? "?"}`) : pc.yellow("not installed")}`,
856
+ `MCP registered:${st.mcpRegistered ? pc.green(" yes") : pc.yellow(" no")}${st.mcpDetail ? pc.dim(` (${st.mcpDetail})`) : ""}`,
857
+ `MCP URL: ${st.mcpUrlMatches ? pc.green(MCP_URL) : pc.yellow("not production / missing")}`,
858
+ `Auth path: ${authLabel}`,
859
+ `Headless token:${st.tokenPresent ? pc.green(` ${st.tokenKind}`) : pc.dim(" none (optional)")}`
860
+ ].join("\n");
861
+ }
862
+ function formatNextSteps(st) {
863
+ return st.nextSteps.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
864
+ }
865
+ function formatNlExamples() {
866
+ return FLEET_NL_EXAMPLES.map((e) => ` \u2022 ${pc.bold(e.label)}: "${e.prompt}"`).join("\n");
867
+ }
868
+ function accountLinksNote() {
869
+ return [
870
+ `Sign up: ${SIGN_UP_URL}`,
871
+ `Sign in: ${SIGN_IN_URL}`,
872
+ `Console: ${CONSOLE_URL}`,
873
+ `Onboarding: ${ONBOARDING_URL}`,
874
+ `MCP home: https://mcp.carrier.llc`
875
+ ].join("\n");
876
+ }
877
+ function withNextStep(errorMsg, next) {
878
+ return [errorMsg, "", "What to do next:", ...next.map((s) => ` \u2192 ${s}`)].join("\n");
729
879
  }
730
880
 
731
881
  // src/cli/lib/status.ts
732
- import { homedir as homedir2 } from "os";
733
- import { join as join5 } from "path";
882
+ import { homedir as homedir3 } from "os";
883
+ import { join as join6 } from "path";
734
884
 
735
885
  // src/cli/lib/mcp-url.ts
736
886
  var PRODUCTION_MCP_URL = "https://mcp.carrier.llc/mcp";
@@ -823,10 +973,10 @@ function scanMcpServers(servers) {
823
973
  }
824
974
  async function probeMcpFromConfig() {
825
975
  const candidates = [
826
- join5(homedir2(), ".claude.json"),
827
- join5(claudeHome(), "settings.json"),
828
- join5(claudeHome(), ".mcp.json"),
829
- join5(process.cwd(), ".mcp.json")
976
+ join6(homedir3(), ".claude.json"),
977
+ join6(claudeHome(), "settings.json"),
978
+ join6(claudeHome(), ".mcp.json"),
979
+ join6(process.cwd(), ".mcp.json")
830
980
  ];
831
981
  let best = null;
832
982
  let configSeen = false;
@@ -952,569 +1102,1038 @@ function resolveCliToken() {
952
1102
  return ocs || null;
953
1103
  }
954
1104
 
955
- // src/cli/lib/guidance.ts
956
- import pc from "picocolors";
957
- var FLEET_NL_EXAMPLES = [
958
- { label: "Fleet health", prompt: "Show my fleet health \u2014 accounts, eSIMs, low-balance alerts" },
959
- { label: "Subscriber lookup", prompt: "Look up subscriber ICCID 8944\u2026 and diagnose connectivity" },
960
- { label: "Usage anomalies", prompt: "Detect usage anomalies and burn-rate risks this week" },
961
- { label: "Issue from inventory", prompt: "Issue 3 free eSIMs from inventory on my account and assign the starter package" },
962
- { label: "Billing check", prompt: "What's my platform credit balance and Stripe Connect payout status?" },
963
- { label: "Wallet balance", prompt: "What's my managed prepaid wallet balance and auto-top-up status?" },
964
- { label: "Churn risk", prompt: "List high churn-risk subscribers with retention ideas" }
1105
+ // src/cli/lib/ask.ts
1106
+ var DEFAULT_ERROR_HINTS = [
1107
+ "Re-check the arguments: carrier <domain> <command> --help",
1108
+ "Or open Claude and ask there with full tool routing"
965
1109
  ];
966
- function oauthFirstUseNote() {
967
- return [
968
- "Entry A \u2014 zero credentials up front",
969
- ` MCP URL: ${MCP_URL}`,
970
- " Auth: OAuth on first use (Clerk \u2014 Google, GitHub, or email)",
971
- " Token: none required in Claude / Cursor / Windsurf",
972
- "",
973
- "When you first talk to Carrier in your MCP client, the browser opens",
974
- "sign-in/sign-up. After authorize, tools work. Headless? Use an org API key",
975
- `(ak_\u2026) from ${CONSOLE_URL} \u2192 Settings \u2192 API Keys.`
976
- ].join("\n");
977
- }
978
- function formatStatusBlock(st) {
979
- const yn = (ok2, yes = "yes", no = "no") => ok2 ? pc.green(yes) : pc.yellow(no);
980
- 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");
981
- return [
982
- `Claude Code: ${yn(st.claudeFound, "found", "not on PATH")}`,
983
- `Plugin: ${st.pluginInstalled ? pc.green(`installed v${st.pluginVersion ?? "?"}`) : pc.yellow("not installed")}`,
984
- `MCP registered:${st.mcpRegistered ? pc.green(" yes") : pc.yellow(" no")}${st.mcpDetail ? pc.dim(` (${st.mcpDetail})`) : ""}`,
985
- `MCP URL: ${st.mcpUrlMatches ? pc.green(MCP_URL) : pc.yellow("not production / missing")}`,
986
- `Auth path: ${authLabel}`,
987
- `Headless token:${st.tokenPresent ? pc.green(` ${st.tokenKind}`) : pc.dim(" none (optional)")}`
988
- ].join("\n");
1110
+ var DEFAULT_MCP_TIMEOUT_MS = 45e3;
1111
+ function mcpTimeoutMs() {
1112
+ const parsed = Number(process.env.CARRIER_MCP_TIMEOUT_MS);
1113
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MCP_TIMEOUT_MS;
989
1114
  }
990
- function formatNextSteps(st) {
991
- return st.nextSteps.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
1115
+ var McpTimeoutError = class extends Error {
1116
+ constructor(timeoutMs) {
1117
+ super(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
1118
+ this.timeoutMs = timeoutMs;
1119
+ this.name = "McpTimeoutError";
1120
+ }
1121
+ timeoutMs;
1122
+ };
1123
+ function isTimeoutish(err) {
1124
+ return err instanceof McpTimeoutError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
992
1125
  }
993
- function formatNlExamples() {
994
- return FLEET_NL_EXAMPLES.map((e) => ` \u2022 ${pc.bold(e.label)}: "${e.prompt}"`).join("\n");
1126
+ function parseRpcBody(raw) {
1127
+ const trimmed = raw.trim();
1128
+ if (!trimmed) return {};
1129
+ if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
1130
+ return JSON.parse(trimmed);
1131
+ }
1132
+ const dataLine = trimmed.split("\n").map((l) => l.trim()).find((l) => l.startsWith("data:"));
1133
+ if (!dataLine) return {};
1134
+ return JSON.parse(dataLine.slice(5).trim());
995
1135
  }
996
- function accountLinksNote() {
997
- return [
998
- `Sign up: ${SIGN_UP_URL}`,
999
- `Sign in: ${SIGN_IN_URL}`,
1000
- `Console: ${CONSOLE_URL}`,
1001
- `Onboarding: ${ONBOARDING_URL}`,
1002
- `MCP home: https://mcp.carrier.llc`
1003
- ].join("\n");
1136
+ async function callMcpTool(tool, args, opts = {}) {
1137
+ const token = resolveCliToken() ?? await getAccessToken();
1138
+ if (!token) {
1139
+ return {
1140
+ ok: false,
1141
+ text: withNextStep("Not signed in.", [
1142
+ "Sign in from this terminal: carrier login",
1143
+ "No account yet? carrier open signup",
1144
+ "Headless/CI instead: export CARRIER_API_KEY=ak_\u2026 from Console \u2192 Settings \u2192 API Keys"
1145
+ ])
1146
+ };
1147
+ }
1148
+ const headers = {
1149
+ Authorization: `Bearer ${token}`,
1150
+ "Content-Type": "application/json",
1151
+ Accept: "application/json, text/event-stream"
1152
+ };
1153
+ const timeoutMs = mcpTimeoutMs();
1154
+ const controller = new AbortController();
1155
+ let deadlineTimer;
1156
+ const deadline = new Promise((_resolve, reject) => {
1157
+ deadlineTimer = setTimeout(() => {
1158
+ controller.abort();
1159
+ reject(new McpTimeoutError(timeoutMs));
1160
+ }, timeoutMs);
1161
+ });
1162
+ const signal = controller.signal;
1163
+ try {
1164
+ return await Promise.race([
1165
+ (async () => {
1166
+ const initRes = await fetch(MCP_URL, {
1167
+ method: "POST",
1168
+ headers,
1169
+ signal,
1170
+ body: JSON.stringify({
1171
+ jsonrpc: "2.0",
1172
+ id: 1,
1173
+ method: "initialize",
1174
+ params: {
1175
+ protocolVersion: "2024-11-05",
1176
+ capabilities: {},
1177
+ clientInfo: { name: "carrier-cli", version: CARRIER_VERSION }
1178
+ }
1179
+ })
1180
+ });
1181
+ if (initRes.status === 401 || initRes.status === 403) {
1182
+ return {
1183
+ ok: false,
1184
+ text: withNextStep(`MCP returned ${initRes.status} (auth rejected).`, [
1185
+ "Confirm CARRIER_API_KEY is a valid org key (ak_\u2026) from app.carrier.llc",
1186
+ "Or complete OCS onboarding: https://app.carrier.llc/onboarding",
1187
+ "Interactive: use Claude + OAuth instead of a headless key"
1188
+ ])
1189
+ };
1190
+ }
1191
+ const sessionId = initRes.headers.get("mcp-session-id") ?? initRes.headers.get("Mcp-Session-Id");
1192
+ await initRes.arrayBuffer().catch(() => void 0);
1193
+ if (!sessionId) {
1194
+ return {
1195
+ ok: false,
1196
+ text: withNextStep("MCP initialize did not return a session id.", [
1197
+ "Check network access to https://mcp.carrier.llc/mcp",
1198
+ "Retry later, or use Claude with the registered MCP (OAuth path)"
1199
+ ])
1200
+ };
1201
+ }
1202
+ const sessionHeaders = { ...headers, "Mcp-Session-Id": sessionId };
1203
+ await fetch(MCP_URL, {
1204
+ method: "POST",
1205
+ headers: sessionHeaders,
1206
+ signal,
1207
+ body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
1208
+ }).catch(() => void 0);
1209
+ const res = await fetch(MCP_URL, {
1210
+ method: "POST",
1211
+ headers: sessionHeaders,
1212
+ signal,
1213
+ body: JSON.stringify({
1214
+ jsonrpc: "2.0",
1215
+ id: 2,
1216
+ method: "tools/call",
1217
+ params: { name: tool, arguments: args }
1218
+ })
1219
+ });
1220
+ if (!res.ok) {
1221
+ const snippet = (await res.text().catch(() => "")).slice(0, 240);
1222
+ return {
1223
+ ok: false,
1224
+ text: withNextStep(`MCP call failed (${res.status}). ${snippet}`, [
1225
+ "Check network access to https://mcp.carrier.llc/mcp",
1226
+ "Retry later, or use Claude with the registered MCP (OAuth path)"
1227
+ ])
1228
+ };
1229
+ }
1230
+ const json = parseRpcBody(await res.text());
1231
+ if (json.error?.message) {
1232
+ return {
1233
+ ok: false,
1234
+ text: withNextStep(json.error.message, opts.errorHints ?? DEFAULT_ERROR_HINTS)
1235
+ };
1236
+ }
1237
+ const parts = json.result?.content ?? [];
1238
+ const text3 = parts.map((p4) => p4.text).filter(Boolean).join("\n").trim() || JSON.stringify(json.result ?? json, null, 2);
1239
+ return {
1240
+ ok: !json.result?.isError,
1241
+ text: text3,
1242
+ structured: json.result?.structuredContent
1243
+ };
1244
+ })(),
1245
+ deadline
1246
+ ]);
1247
+ } catch (e) {
1248
+ if (isTimeoutish(e)) {
1249
+ const secs = Math.round(timeoutMs / 1e3);
1250
+ return {
1251
+ ok: false,
1252
+ text: withNextStep(
1253
+ `MCP call to ${tool} timed out after ${secs}s (no response).`,
1254
+ [
1255
+ "The tool is likely returning more data than the MCP transport can deliver \u2014 see issue #663",
1256
+ "Narrow the request (a specific --account-id / --iccid returns far less than a whole-fleet call)",
1257
+ `Raise the deadline for one slow call: CARRIER_MCP_TIMEOUT_MS=${timeoutMs * 2} carrier \u2026`,
1258
+ "Confirm the service is up: check network access to https://mcp.carrier.llc/mcp",
1259
+ "Or open Claude and ask there with full tool routing"
1260
+ ]
1261
+ )
1262
+ };
1263
+ }
1264
+ const msg = e instanceof Error ? e.message : String(e);
1265
+ return {
1266
+ ok: false,
1267
+ text: withNextStep(`Could not reach MCP: ${msg}`, [
1268
+ "Verify outbound HTTPS to mcp.carrier.llc",
1269
+ "Use the interactive Claude path while offline from this machine"
1270
+ ])
1271
+ };
1272
+ } finally {
1273
+ if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
1274
+ }
1004
1275
  }
1005
- function withNextStep(errorMsg, next) {
1006
- return [errorMsg, "", "What to do next:", ...next.map((s) => ` \u2192 ${s}`)].join("\n");
1276
+ var ASK_ERROR_HINTS = [
1277
+ "Rephrase the intent more specifically (include ICCID / MSISDN if relevant)",
1278
+ "Or use a discrete command: carrier --help lists every domain"
1279
+ ];
1280
+ function parseRouting(text3) {
1281
+ const trimmed = text3.trim();
1282
+ if (!trimmed.startsWith("{")) return void 0;
1283
+ try {
1284
+ const parsed = JSON.parse(trimmed);
1285
+ if (!parsed || typeof parsed !== "object") return void 0;
1286
+ const routes = typeof parsed.resolved_tool === "string" && parsed.resolved_tool;
1287
+ const asksBack = typeof parsed.clarifying_question === "string" || Array.isArray(parsed.candidates);
1288
+ return routes || asksBack ? parsed : void 0;
1289
+ } catch {
1290
+ return void 0;
1291
+ }
1292
+ }
1293
+ async function carrierAsk(intent) {
1294
+ const routed = await callMcpTool("carrier_ask", { intent }, { errorHints: ASK_ERROR_HINTS });
1295
+ if (!routed.ok) return routed;
1296
+ const routing = parseRouting(routed.text);
1297
+ if (!routing) return routed;
1298
+ if (!routing.resolved_tool) {
1299
+ const candidates = (routing.candidates ?? []).map((c) => c.tool ? `Candidate: ${c.tool}${c.reason ? ` \u2014 ${c.reason}` : ""}` : void 0).filter((s) => Boolean(s));
1300
+ return {
1301
+ ok: false,
1302
+ text: withNextStep(routing.clarifying_question?.trim() || "Carrier could not tell which tool you meant.", [
1303
+ ...candidates,
1304
+ ...ASK_ERROR_HINTS
1305
+ ])
1306
+ };
1307
+ }
1308
+ const tool = routing.resolved_tool;
1309
+ const args = routing.resolved_params ?? {};
1310
+ if (routing.match && routing.match !== "confirmed") {
1311
+ return {
1312
+ ok: false,
1313
+ text: withNextStep(`Carrier could not confirm what you meant (match: ${routing.match}).`, [
1314
+ `Closest tool: ${tool}`,
1315
+ ...ASK_ERROR_HINTS
1316
+ ])
1317
+ };
1318
+ }
1319
+ if (routing.confirm_required) {
1320
+ return {
1321
+ ok: false,
1322
+ text: withNextStep(`"${intent}" resolves to ${tool}, which changes live service or billing.`, [
1323
+ `Params: ${JSON.stringify(args)}`,
1324
+ "Run the matching discrete command so every flag is explicit and reviewable",
1325
+ "carrier --help lists every domain"
1326
+ ])
1327
+ };
1328
+ }
1329
+ return callMcpTool(tool, args, { errorHints: ASK_ERROR_HINTS });
1007
1330
  }
1008
1331
 
1009
- // src/cli/lib/home.ts
1010
- import * as p from "@clack/prompts";
1011
- import pc2 from "picocolors";
1012
-
1013
- // src/cli/lib/auth.ts
1014
- import { createHash as createHash2, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
1015
- import { createServer as createServer2 } from "http";
1332
+ // src/cli/lib/rest.ts
1333
+ var DEFAULT_ERROR_HINTS2 = [
1334
+ "Re-check the arguments: carrier <domain> <command> --help",
1335
+ "Or open Claude and ask there with full tool routing"
1336
+ ];
1337
+ function resolveApiKey() {
1338
+ return process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || null;
1339
+ }
1340
+ function ocsTokenOnly() {
1341
+ return Boolean(
1342
+ process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim()
1343
+ );
1344
+ }
1345
+ function applyPath(path, args) {
1346
+ const rest = { ...args };
1347
+ const missing = [];
1348
+ const filled = path.replace(/\{(\w+)\}/g, (_match, name) => {
1349
+ const value = rest[name];
1350
+ if (value === void 0 || value === null || value === "") {
1351
+ missing.push(name);
1352
+ return `{${name}}`;
1353
+ }
1354
+ delete rest[name];
1355
+ return encodeURIComponent(String(value));
1356
+ });
1357
+ return { path: filled, rest, missing };
1358
+ }
1359
+ function queryString(args) {
1360
+ const params = new URLSearchParams();
1361
+ for (const [key, value] of Object.entries(args)) {
1362
+ if (value === void 0 || value === null) continue;
1363
+ params.set(key, String(value));
1364
+ }
1365
+ const qs = params.toString();
1366
+ return qs ? `?${qs}` : "";
1367
+ }
1368
+ function bodyText(raw) {
1369
+ const trimmed = raw.trim();
1370
+ if (!trimmed) return "";
1371
+ try {
1372
+ const parsed = JSON.parse(trimmed);
1373
+ if (typeof parsed.error === "string") return parsed.error;
1374
+ if (typeof parsed.message === "string") return parsed.message;
1375
+ return trimmed;
1376
+ } catch {
1377
+ return trimmed;
1378
+ }
1379
+ }
1380
+ async function callRestEndpoint(method, path, args, opts = {}) {
1381
+ const token = resolveApiKey() ?? await getAccessToken();
1382
+ if (!token) {
1383
+ return {
1384
+ ok: false,
1385
+ text: withNextStep(
1386
+ ocsTokenOnly() ? "Not signed in. An eSIMVault/OCS token is set, but the Carrier API only accepts Carrier credentials." : "Not signed in.",
1387
+ [
1388
+ "Sign in from this terminal: carrier login",
1389
+ "No account yet? carrier open signup",
1390
+ "Headless/CI instead: export CARRIER_API_KEY=ak_\u2026 from Console \u2192 Settings \u2192 API Keys"
1391
+ ]
1392
+ )
1393
+ };
1394
+ }
1395
+ const { path: resolvedPath, rest, missing } = applyPath(path, args);
1396
+ if (missing.length) {
1397
+ return {
1398
+ ok: false,
1399
+ text: withNextStep(`Cannot build ${method} ${path}.`, [
1400
+ `Missing path value(s): ${missing.join(", ")}`,
1401
+ "See every flag: carrier <domain> <command> --help"
1402
+ ])
1403
+ };
1404
+ }
1405
+ const sendsBody = method === "POST" || method === "PUT" || method === "PATCH";
1406
+ const url = `${API_URL}${resolvedPath}${sendsBody ? "" : queryString(rest)}`;
1407
+ const headers = {
1408
+ Authorization: `Bearer ${token}`,
1409
+ Accept: "application/json",
1410
+ "User-Agent": `carrier-cli/${CARRIER_VERSION}`
1411
+ };
1412
+ if (sendsBody) headers["Content-Type"] = "application/json";
1413
+ if (opts.confirmToken) headers["X-Confirm-Token"] = opts.confirmToken;
1414
+ if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
1415
+ const timeoutMs = mcpTimeoutMs();
1416
+ const controller = new AbortController();
1417
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
1418
+ try {
1419
+ const res = await fetch(url, {
1420
+ method,
1421
+ headers,
1422
+ signal: controller.signal,
1423
+ body: sendsBody ? JSON.stringify(rest) : void 0
1424
+ });
1425
+ const raw = await res.text();
1426
+ if (res.status === 202) {
1427
+ let confirmToken;
1428
+ let expires;
1429
+ let message = "";
1430
+ try {
1431
+ const parsed = JSON.parse(raw);
1432
+ confirmToken = parsed.confirm_token;
1433
+ expires = parsed.expires_in_seconds;
1434
+ message = parsed.message ?? "";
1435
+ } catch {
1436
+ }
1437
+ return {
1438
+ ok: false,
1439
+ text: withNextStep(
1440
+ message || `${method} ${path} needs an explicit confirmation.`,
1441
+ confirmToken ? [
1442
+ `Re-run the same command with: --confirm-token ${confirmToken}`,
1443
+ `The token is single-use and expires in ${expires ?? 120}s.`
1444
+ ] : ["The server asked for confirmation but returned no token \u2014 retry the command."]
1445
+ )
1446
+ };
1447
+ }
1448
+ if (res.status === 401 || res.status === 403) {
1449
+ return {
1450
+ ok: false,
1451
+ text: withNextStep(
1452
+ `Carrier API returned ${res.status} (auth rejected).`,
1453
+ [
1454
+ "Confirm CARRIER_API_KEY is a valid org key (ak_\u2026) from app.carrier.llc",
1455
+ "Or sign in from this terminal: carrier login",
1456
+ ...res.status === 403 ? ["A 403 here usually means the key lacks write scope."] : []
1457
+ ]
1458
+ )
1459
+ };
1460
+ }
1461
+ if (!res.ok) {
1462
+ return {
1463
+ ok: false,
1464
+ text: withNextStep(
1465
+ redact(`${method} ${path} failed (${res.status}). ${bodyText(raw).slice(0, 500)}`),
1466
+ opts.errorHints ?? DEFAULT_ERROR_HINTS2
1467
+ )
1468
+ };
1469
+ }
1470
+ return { ok: true, text: raw.trim() || JSON.stringify({ status: res.status }) };
1471
+ } catch (e) {
1472
+ const aborted = e instanceof Error && (e.name === "AbortError" || e.name === "TimeoutError");
1473
+ if (aborted) {
1474
+ const secs = Math.round(timeoutMs / 1e3);
1475
+ return {
1476
+ ok: false,
1477
+ text: withNextStep(`${method} ${path} timed out after ${secs}s (no response).`, [
1478
+ `Raise the deadline for one slow call: CARRIER_MCP_TIMEOUT_MS=${timeoutMs * 2} carrier \u2026`,
1479
+ "Confirm the service is up: check network access to https://api.carrier.llc"
1480
+ ])
1481
+ };
1482
+ }
1483
+ const msg = e instanceof Error ? e.message : String(e);
1484
+ return {
1485
+ ok: false,
1486
+ text: withNextStep(redact(`Could not reach the Carrier API: ${msg}`), [
1487
+ "Verify outbound HTTPS to api.carrier.llc",
1488
+ "Use the interactive Claude path while offline from this machine"
1489
+ ])
1490
+ };
1491
+ } finally {
1492
+ clearTimeout(timer);
1493
+ }
1494
+ }
1016
1495
 
1017
- // src/cli/lib/credentials.ts
1018
- import { homedir as homedir3 } from "os";
1019
- import { join as join6 } from "path";
1020
- import { chmod, mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile3 } from "fs/promises";
1021
- function carrierHome() {
1022
- return process.env.CARRIER_HOME?.trim() || join6(homedir3(), ".carrier");
1496
+ // src/cli/lib/hosted-publish.ts
1497
+ async function publishHostedStorefront(input) {
1498
+ const slug2 = storefrontSlugFrom(input.slug);
1499
+ if (slug2 === "") return { ok: false, text: "Use a short name, such as acme." };
1500
+ input = { ...input, slug: slug2 };
1501
+ const listed = await callRestEndpoint("GET", "/v1/storefronts", {});
1502
+ if (!listed.ok) return listed;
1503
+ const existing = findStore(listed.text, input.slug);
1504
+ if (!existing && input.status !== "draft") {
1505
+ const missing = firstPublishCopy(input);
1506
+ if (missing) return { ok: false, text: missing };
1507
+ }
1508
+ const filled = completePlanInput(planInput(input), existing?.plans ?? []);
1509
+ const plan = storefrontPlanBody(filled);
1510
+ if (isStorefrontPlanError(plan)) return { ok: false, text: plan.error };
1511
+ const brand = storefrontBrandPatch(input, existing ?? void 0);
1512
+ const record2 = { slug: input.slug, name: input.name, status: input.status, ...brand };
1513
+ const saved = existing ? await callRestEndpoint("PATCH", "/v1/storefronts/{id}", { id: existing.id, ...record2 }) : await callRestEndpoint("POST", "/v1/storefronts", record2, { idempotencyKey: `site-publish:${input.slug}` });
1514
+ if (!saved.ok) return saved;
1515
+ if (!plan) return withShopAddress(saved, input.slug);
1516
+ const id = existing?.id ?? readId(saved.text);
1517
+ if (!id) return { ok: false, text: "The store was saved but its id was missing, so the plan was not written." };
1518
+ const planned = await callRestEndpoint("POST", "/v1/storefronts/{id}/plans", { id, ...plan });
1519
+ return planned.ok ? withShopAddress(planned, input.slug) : planned;
1023
1520
  }
1024
- function credentialsPath() {
1025
- return join6(carrierHome(), "credentials.json");
1521
+ function firstPublishCopy(input) {
1522
+ const hero = input.hero?.trim() ?? "";
1523
+ const tagline = input.tagline?.trim() ?? "";
1524
+ const email = input.supportEmail?.trim() ?? "";
1525
+ const legalName = input.legalName?.trim() ?? "";
1526
+ if (hero === "" || hero === "Data that works the day you land.") return "Enter a hero.";
1527
+ if (tagline === "" || tagline === "Programmable connectivity, on demand.") return "Enter a tagline.";
1528
+ if (email === "" || email === "support@carrier.llc") return "Enter a support email.";
1529
+ if (!/^[^\s@]+@[^\s@]+$/.test(email)) return "Support email must be an email address.";
1530
+ if (legalName === "" || legalName === "Lifecycle Innovations Limited") return "Enter a legal name.";
1531
+ return null;
1026
1532
  }
1027
- async function readCredentials() {
1533
+ function withShopAddress(result, slug2) {
1534
+ const address = hostedStorefrontUrl(slug2);
1535
+ if (result.text.includes(address)) return result;
1536
+ return { ...result, text: `${result.text}
1537
+ ${address}` };
1538
+ }
1539
+ function hostedPublishInputFromFlags(flags) {
1540
+ const status = flags.status === "draft" || flags.status === "published" ? flags.status : null;
1541
+ if (!status) return { ok: false, text: "Status must be draft or published." };
1542
+ const price = readPrice(flags);
1543
+ if (!price.ok) return { ok: false, text: price.text };
1544
+ const template = parseWholeNumber(flags.templateId, "Template id");
1545
+ if (!template.ok) return { ok: false, text: template.message };
1546
+ const data = readLabel(flags.dataGb, parseDataGb);
1547
+ if (!data.ok) return { ok: false, text: data.text };
1548
+ const validity = readLabel(flags.validityDays, parseValidityDays);
1549
+ if (!validity.ok) return { ok: false, text: validity.text };
1550
+ const countries = readCountries(flags.countries);
1551
+ if (!countries.ok) return { ok: false, text: countries.text };
1552
+ const logo = readLogo(flags.logo);
1553
+ if (!logo.ok) return { ok: false, text: logo.text };
1554
+ return {
1555
+ ok: true,
1556
+ input: {
1557
+ slug: flags.slug,
1558
+ name: flags.name,
1559
+ status,
1560
+ accent: flags.accent,
1561
+ accentDark: flags.accentDark,
1562
+ bg: flags.bg,
1563
+ text: flags.text,
1564
+ logoUrl: logo.value,
1565
+ hero: flags.hero,
1566
+ tagline: flags.tagline,
1567
+ supportEmail: flags.supportEmail,
1568
+ legalName: flags.legalName,
1569
+ productId: flags.product,
1570
+ planSlug: flags.plan,
1571
+ planName: flags.planName,
1572
+ priceCents: price.value,
1573
+ currency: flags.currency,
1574
+ templateId: template.value,
1575
+ dataGb: data.value,
1576
+ validityDays: validity.value,
1577
+ regions: countries.value,
1578
+ description: flags.planText,
1579
+ planActive: flags.inactive ? false : void 0
1580
+ }
1581
+ };
1582
+ }
1583
+ function readLogo(value) {
1584
+ if (value === void 0 || value.trim() === "") return { ok: true, value: void 0 };
1585
+ const trimmed = value.trim();
1586
+ if (trimmed.startsWith("https://") || trimmed.startsWith("data:")) {
1587
+ const parsed = logoReference(trimmed);
1588
+ return parsed.ok ? { ok: true, value: parsed.url } : { ok: false, text: parsed.message };
1589
+ }
1028
1590
  try {
1029
- const raw = await readFile2(credentialsPath(), "utf8");
1030
- const parsed = JSON.parse(raw);
1031
- if (!parsed?.accessToken || typeof parsed.accessToken !== "string") return null;
1032
- return parsed;
1591
+ const bytes = readFileSync(trimmed);
1592
+ const parsed = logoFileDataUrl(bytes);
1593
+ return parsed.ok ? { ok: true, value: parsed.url } : { ok: false, text: parsed.message };
1033
1594
  } catch {
1034
- return null;
1595
+ return { ok: false, text: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
1035
1596
  }
1036
1597
  }
1037
- async function writeCredentials(creds) {
1038
- const dir = carrierHome();
1039
- await mkdir2(dir, { recursive: true, mode: 448 });
1040
- await chmod(dir, 448).catch(() => void 0);
1041
- const file = credentialsPath();
1042
- await writeFile3(file, `${JSON.stringify(creds, null, 2)}
1043
- `, { mode: 384 });
1044
- await chmod(file, 384).catch(() => void 0);
1598
+ function readPrice(flags) {
1599
+ if (flags.price !== void 0 && flags.priceCents !== void 0) {
1600
+ return { ok: false, text: "Use either --price 9.00 or --price-cents 900." };
1601
+ }
1602
+ if (flags.price !== void 0) {
1603
+ const parsed2 = majorToCents(flags.price, flags.currency);
1604
+ return parsed2.ok ? { ok: true, value: parsed2.cents } : { ok: false, text: parsed2.message };
1605
+ }
1606
+ const parsed = parseWholeNumber(flags.priceCents, "Price");
1607
+ return parsed.ok ? { ok: true, value: parsed.value } : { ok: false, text: parsed.message };
1045
1608
  }
1046
- async function clearCredentials() {
1047
- await rm(credentialsPath(), { force: true });
1609
+ function readLabel(value, parse) {
1610
+ if (value === void 0) return { ok: true, value: void 0 };
1611
+ const parsed = parse(value);
1612
+ return parsed.ok ? { ok: true, value: parsed.value } : { ok: false, text: parsed.message };
1048
1613
  }
1049
- function isExpired(creds, skewMs = 6e4) {
1050
- if (!creds.expiresAt) return false;
1051
- return Date.now() >= creds.expiresAt - skewMs;
1614
+ function readCountries(value) {
1615
+ if (value === void 0) return { ok: true, value: void 0 };
1616
+ const parsed = parseCountryInput(value);
1617
+ return parsed.ok ? { ok: true, value: parsed.regions } : { ok: false, text: parsed.message };
1052
1618
  }
1053
- function redact(text3) {
1054
- return text3.replace(/\b(ak_|sk_|rt_)[A-Za-z0-9._-]{6,}/g, "$1***").replace(/\bBearer\s+[A-Za-z0-9._-]{8,}/gi, "Bearer ***");
1619
+ function planInput(input) {
1620
+ return {
1621
+ productId: input.productId,
1622
+ slug: input.planSlug,
1623
+ name: input.planName,
1624
+ priceCents: input.priceCents,
1625
+ currency: input.currency,
1626
+ templateId: input.templateId,
1627
+ dataGb: input.dataGb,
1628
+ validityDays: input.validityDays,
1629
+ regions: input.regions,
1630
+ description: input.description,
1631
+ active: input.planActive
1632
+ };
1055
1633
  }
1056
-
1057
- // src/cli/lib/auth.ts
1058
- var DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
1059
- var CLIENT_NAME2 = "Carrier CLI";
1060
- var DEFAULT_SCOPE = "read write";
1061
- var CALLBACK_PORTS = [3118, 3119, 8976, 8977, 0];
1062
- async function discover(base = MCP_HOME) {
1063
- const res = await fetch(`${base}${DISCOVERY_PATH}`, {
1064
- headers: { Accept: "application/json" }
1065
- });
1066
- if (!res.ok) throw new Error(`OAuth discovery failed (${res.status}) at ${base}${DISCOVERY_PATH}`);
1067
- const meta = await res.json();
1068
- if (!meta.authorization_endpoint || !meta.token_endpoint) {
1069
- throw new Error("OAuth discovery returned no authorization or token endpoint");
1634
+ function findStore(body, slug2) {
1635
+ try {
1636
+ const parsed = JSON.parse(body);
1637
+ const match = parsed.data?.find((row) => row["slug"] === slug2);
1638
+ if (!match || typeof match["id"] !== "string") return null;
1639
+ return {
1640
+ id: match["id"],
1641
+ theme: record(match["theme"]),
1642
+ copy: record(match["copy"]),
1643
+ plans: storedPlans(match["plans"])
1644
+ };
1645
+ } catch {
1646
+ return null;
1647
+ }
1648
+ }
1649
+ function storedPlans(value) {
1650
+ if (!Array.isArray(value)) return [];
1651
+ const plans = [];
1652
+ for (const item of value) {
1653
+ const row = record(item);
1654
+ if (!row) continue;
1655
+ if (typeof row["slug"] !== "string" || typeof row["name"] !== "string" || typeof row["currency"] !== "string") continue;
1656
+ if (typeof row["ocs_package_template_id"] !== "number") continue;
1657
+ plans.push({
1658
+ slug: row["slug"],
1659
+ name: row["name"],
1660
+ currency: row["currency"],
1661
+ ocs_package_template_id: row["ocs_package_template_id"]
1662
+ });
1663
+ }
1664
+ return plans;
1665
+ }
1666
+ function readId(body) {
1667
+ try {
1668
+ const parsed = JSON.parse(body);
1669
+ return typeof parsed.id === "string" ? parsed.id : null;
1670
+ } catch {
1671
+ return null;
1070
1672
  }
1071
- return meta;
1072
1673
  }
1073
- function base64url2(buf) {
1074
- return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1674
+ function record(value) {
1675
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
1676
+ }
1677
+
1678
+ // src/cli/lib/resend.ts
1679
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
1680
+ import { createServer as createServer2 } from "http";
1681
+ var RESEND_API = "https://api.resend.com";
1682
+ var RESEND_API_KEYS_URL = "https://resend.com/api-keys";
1683
+ var RESEND_DOMAINS_URL = "https://resend.com/domains";
1684
+ var USER_AGENT = "carrier-cli";
1685
+ var CLIENT_NAME2 = "Carrier storefront setup";
1686
+ var OAUTH_SCOPE = "full_access";
1687
+ var REDIRECT_PATH = "/oauth/callback";
1688
+ var REDIRECT_HOST = "127.0.0.1";
1689
+ function looksResendKey(key) {
1690
+ return /^re_[A-Za-z0-9_-]{8,}$/.test(key.trim());
1691
+ }
1692
+ function base64url2(input) {
1693
+ return input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
1075
1694
  }
1076
1695
  function pkcePair2() {
1077
- const verifier = base64url2(randomBytes2(32));
1078
- const challenge = base64url2(createHash2("sha256").update(verifier).digest());
1079
- return { verifier, challenge };
1696
+ const verifier = base64url2(randomBytes2(64));
1697
+ return { verifier, challenge: base64url2(createHash2("sha256").update(verifier).digest()) };
1080
1698
  }
1081
- function sameState(a, b) {
1082
- const left = Buffer.from(a);
1083
- const right = Buffer.from(b);
1084
- if (left.length !== right.length) return false;
1085
- return timingSafeEqual(left, right);
1699
+ function buildAuthorizeUrl(opts) {
1700
+ const url = new URL(`${RESEND_API}/oauth/authorize`);
1701
+ url.searchParams.set("client_id", opts.clientId);
1702
+ url.searchParams.set("response_type", "code");
1703
+ url.searchParams.set("redirect_uri", opts.redirectUri);
1704
+ url.searchParams.set("scope", opts.scope ?? OAUTH_SCOPE);
1705
+ url.searchParams.set("state", opts.state);
1706
+ url.searchParams.set("code_challenge", opts.challenge);
1707
+ url.searchParams.set("code_challenge_method", "S256");
1708
+ return url.toString();
1086
1709
  }
1087
- async function registerClient(meta, redirectUri) {
1088
- if (!meta.registration_endpoint) {
1089
- throw new Error("This server does not support dynamic client registration");
1710
+ async function resendFetch(fetchImpl, path, init = {}) {
1711
+ const headers = { "User-Agent": USER_AGENT, Accept: "application/json" };
1712
+ if (init.token) headers.Authorization = `Bearer ${init.token}`;
1713
+ let body;
1714
+ if (init.form) {
1715
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
1716
+ body = new URLSearchParams(init.form).toString();
1717
+ } else if (init.json !== void 0) {
1718
+ headers["Content-Type"] = "application/json";
1719
+ body = JSON.stringify(init.json);
1090
1720
  }
1091
- const res = await fetch(meta.registration_endpoint, {
1721
+ try {
1722
+ const res = await fetchImpl(`${RESEND_API}${path}`, {
1723
+ method: init.method ?? "GET",
1724
+ headers,
1725
+ body
1726
+ });
1727
+ const text3 = await res.text();
1728
+ let json;
1729
+ try {
1730
+ json = text3 ? JSON.parse(text3) : void 0;
1731
+ } catch {
1732
+ json = void 0;
1733
+ }
1734
+ if (!res.ok) {
1735
+ return { ok: false, status: res.status, json, error: resendError(json) ?? text3.slice(0, 300) };
1736
+ }
1737
+ return { ok: true, status: res.status, json };
1738
+ } catch (e) {
1739
+ return { ok: false, status: 0, error: e instanceof Error ? e.message : String(e) };
1740
+ }
1741
+ }
1742
+ function resendError(json) {
1743
+ if (typeof json !== "object" || json === null) return void 0;
1744
+ const body = json;
1745
+ return body.message ?? body.error_description ?? body.error;
1746
+ }
1747
+ async function registerOauthClient(fetchImpl) {
1748
+ const registeredRedirect = `http://${REDIRECT_HOST}${REDIRECT_PATH}`;
1749
+ const res = await resendFetch(fetchImpl, "/oauth/register", {
1092
1750
  method: "POST",
1093
- headers: { "Content-Type": "application/json", Accept: "application/json" },
1094
- body: JSON.stringify({
1751
+ json: {
1095
1752
  client_name: CLIENT_NAME2,
1096
- redirect_uris: [redirectUri],
1753
+ redirect_uris: [registeredRedirect],
1097
1754
  grant_types: ["authorization_code", "refresh_token"],
1098
1755
  response_types: ["code"],
1099
- // Installed apps cannot keep a secret; register as a public client.
1100
1756
  token_endpoint_auth_method: "none",
1101
- application_type: "native"
1102
- })
1757
+ scope: OAUTH_SCOPE
1758
+ }
1103
1759
  });
1104
1760
  if (!res.ok) {
1105
- const body = (await res.text().catch(() => "")).slice(0, 200);
1106
- throw new Error(`Client registration failed (${res.status}). ${body}`);
1761
+ return { ok: false, reason: `Resend rejected client registration: ${res.error ?? res.status}` };
1107
1762
  }
1108
- const json = await res.json();
1109
- if (!json.client_id) throw new Error("Client registration returned no client_id");
1110
- return json.client_id;
1763
+ const clientId = res.json?.client_id;
1764
+ if (!clientId) return { ok: false, reason: "Resend registered no client_id." };
1765
+ return { ok: true, clientId };
1111
1766
  }
1112
- function escapeHtml(text3) {
1113
- return text3.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1767
+ async function exchangeCode(fetchImpl, opts) {
1768
+ const res = await resendFetch(fetchImpl, "/oauth/token", {
1769
+ method: "POST",
1770
+ form: {
1771
+ grant_type: "authorization_code",
1772
+ client_id: opts.clientId,
1773
+ code: opts.code,
1774
+ redirect_uri: opts.redirectUri,
1775
+ code_verifier: opts.verifier
1776
+ }
1777
+ });
1778
+ if (!res.ok) return { ok: false, reason: `Token exchange failed: ${res.error ?? res.status}` };
1779
+ const body = res.json;
1780
+ if (!body?.access_token) return { ok: false, reason: "Resend returned no access token." };
1781
+ return { ok: true, accessToken: body.access_token, refreshToken: body.refresh_token };
1114
1782
  }
1115
- function page(title, detail) {
1116
- const safeTitle = escapeHtml(title);
1117
- const safeDetail = escapeHtml(detail);
1118
- return `<!doctype html><meta charset="utf-8"><title>${safeTitle}</title>
1119
- <body style="font-family:system-ui,sans-serif;max-width:32rem;margin:20vh auto;text-align:center">
1120
- <h1 style="font-size:1.25rem">${safeTitle}</h1><p style="color:#555">${safeDetail}</p></body>`;
1783
+ async function revokeGrant(fetchImpl, opts) {
1784
+ await resendFetch(fetchImpl, "/oauth/revoke", {
1785
+ method: "POST",
1786
+ form: {
1787
+ client_id: opts.clientId,
1788
+ token: opts.refreshToken,
1789
+ token_type_hint: "refresh_token"
1790
+ }
1791
+ }).catch(() => void 0);
1121
1792
  }
1122
- async function startCallbackServer() {
1123
- let resolveCode;
1124
- let rejectCode;
1125
- let expected = "";
1793
+ async function findVerifiedDomainId(fetchImpl, accessToken, domain) {
1794
+ const wanted = domain.trim().replace(/^https?:\/\//, "").replace(/\/+$/, "").toLowerCase();
1795
+ if (!wanted) return void 0;
1796
+ const res = await resendFetch(fetchImpl, "/domains", { token: accessToken });
1797
+ if (!res.ok) return void 0;
1798
+ const list = res.json?.data;
1799
+ const match = list?.find((d) => d.name?.toLowerCase() === wanted && d.status === "verified");
1800
+ return match?.id;
1801
+ }
1802
+ async function createSendingKey(fetchImpl, accessToken, opts) {
1803
+ const res = await resendFetch(fetchImpl, "/api-keys", {
1804
+ method: "POST",
1805
+ token: accessToken,
1806
+ json: {
1807
+ // Resend caps the name at 50 characters.
1808
+ name: opts.name.slice(0, 50),
1809
+ permission: "sending_access",
1810
+ ...opts.domainId ? { domain_id: opts.domainId } : {}
1811
+ }
1812
+ });
1813
+ if (!res.ok) return { ok: false, reason: `Could not create an API key: ${res.error ?? res.status}` };
1814
+ const body = res.json;
1815
+ if (!body?.token) return { ok: false, reason: "Resend created a key but returned no token." };
1816
+ return { ok: true, token: body.token, id: body.id };
1817
+ }
1818
+ async function validateResendKey(fetchImpl, key) {
1819
+ if (!looksResendKey(key)) return { ok: false, reason: "That does not look like a Resend key (re_\u2026)." };
1820
+ const res = await resendFetch(fetchImpl, "/emails", { method: "POST", token: key, json: {} });
1821
+ if (res.status === 401) return { ok: false, reason: "Resend says that key is missing or malformed." };
1822
+ if (res.status === 403) return { ok: false, reason: "Resend rejected that key as invalid." };
1823
+ if (res.status === 0) return { ok: false, reason: `Could not reach Resend: ${res.error ?? "network error"}` };
1824
+ return { ok: true };
1825
+ }
1826
+ async function startLoopbackServer() {
1827
+ let resolveCode = null;
1126
1828
  const server = createServer2((req, res) => {
1127
- const url = new URL(req.url ?? "/", "http://127.0.0.1");
1128
- if (url.pathname !== "/callback") {
1829
+ const url = new URL(req.url ?? "/", `http://${REDIRECT_HOST}`);
1830
+ if (url.pathname !== REDIRECT_PATH) {
1129
1831
  res.writeHead(404).end();
1130
1832
  return;
1131
1833
  }
1132
- const err = url.searchParams.get("error");
1834
+ const error = url.searchParams.get("error");
1133
1835
  const code = url.searchParams.get("code");
1134
- const state = url.searchParams.get("state") ?? "";
1135
- if (err) {
1136
- res.writeHead(400, { "Content-Type": "text/html" });
1137
- res.end(page("Sign-in failed", err));
1138
- rejectCode?.(new Error(`Authorization server returned "${err}"`));
1139
- return;
1140
- }
1141
- if (!code || !expected || !sameState(state, expected)) {
1142
- res.writeHead(400, { "Content-Type": "text/html" });
1143
- res.end(page("Sign-in failed", "The redirect did not match this login attempt."));
1144
- rejectCode?.(new Error("State mismatch \u2014 ignoring the redirect"));
1145
- return;
1146
- }
1147
- res.writeHead(200, { "Content-Type": "text/html" });
1148
- res.end(page("You're signed in", "Close this tab and return to your terminal."));
1149
- resolveCode?.(code);
1150
- });
1151
- let bound = false;
1152
- let lastErr;
1153
- for (const port2 of CALLBACK_PORTS) {
1154
- try {
1155
- await new Promise((resolve3, reject) => {
1156
- server.once("error", reject);
1157
- server.listen(port2, "127.0.0.1", () => {
1158
- server.removeListener("error", reject);
1159
- resolve3();
1160
- });
1161
- });
1162
- bound = true;
1163
- break;
1164
- } catch (e) {
1165
- lastErr = e;
1166
- }
1167
- }
1168
- if (!bound) {
1169
- throw new Error(
1170
- `Could not bind a local callback port (tried ${CALLBACK_PORTS.filter(Boolean).join(", ")}): ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
1836
+ const state = url.searchParams.get("state");
1837
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1838
+ res.end(
1839
+ `<!doctype html><meta charset="utf-8"><title>Carrier</title><body style="font-family:system-ui;padding:3rem;text-align:center"><p>${error ? "Authorization was refused." : "Connected. You can close this tab."}</p>`
1171
1840
  );
1172
- }
1173
- const { port } = server.address();
1841
+ resolveCode?.(
1842
+ error ? { ok: false, reason: `Resend returned "${error}".` } : code ? { ok: true, code, reason: state ?? void 0 } : { ok: false, reason: "Resend sent no authorization code." }
1843
+ );
1844
+ });
1845
+ await new Promise((resolve3, reject) => {
1846
+ server.once("error", reject);
1847
+ server.listen(0, REDIRECT_HOST, resolve3);
1848
+ });
1849
+ const port = server.address().port;
1850
+ const redirectUri = `http://${REDIRECT_HOST}:${port}${REDIRECT_PATH}`;
1174
1851
  return {
1175
- redirectUri: `http://localhost:${port}/callback`,
1176
- // `close()` alone only stops new connections; a keep-alive socket from the
1177
- // browser would hold the event loop open and the CLI would never exit.
1178
- waitForCode: (expectedState, timeoutMs) => {
1179
- expected = expectedState;
1180
- return new Promise((resolve3, reject) => {
1852
+ redirectUri,
1853
+ close: () => server.close(),
1854
+ waitForCode(state, timeoutMs) {
1855
+ return new Promise((resolve3) => {
1856
+ const done = (r) => {
1857
+ clearTimeout(timer);
1858
+ resolveCode = null;
1859
+ server.close();
1860
+ resolve3(r);
1861
+ };
1181
1862
  const timer = setTimeout(
1182
- () => reject(new Error(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for the browser`)),
1863
+ () => done({ ok: false, reason: "Timed out waiting for the browser." }),
1183
1864
  timeoutMs
1184
1865
  );
1185
- resolveCode = (code) => {
1186
- clearTimeout(timer);
1187
- resolve3(code);
1188
- };
1189
- rejectCode = (err) => {
1190
- clearTimeout(timer);
1191
- reject(err);
1866
+ resolveCode = (r) => {
1867
+ if (r.ok && r.reason !== state) {
1868
+ done({ ok: false, reason: "The browser callback did not match this request." });
1869
+ return;
1870
+ }
1871
+ done(r.ok ? { ok: true, code: r.code } : r);
1192
1872
  };
1193
1873
  });
1194
- },
1195
- close: () => {
1196
- server.closeAllConnections();
1197
- server.close();
1198
1874
  }
1199
1875
  };
1200
1876
  }
1201
- async function postToken(meta, body) {
1202
- const res = await fetch(meta.token_endpoint, {
1203
- method: "POST",
1204
- headers: {
1205
- "Content-Type": "application/x-www-form-urlencoded",
1206
- Accept: "application/json"
1207
- },
1208
- body: new URLSearchParams(body).toString()
1209
- });
1210
- const json = await res.json().catch(() => ({}));
1211
- if (!res.ok || json.error) {
1212
- throw new Error(json.error_description || json.error || `Token endpoint returned ${res.status}`);
1213
- }
1214
- if (!json.access_token) throw new Error("Token endpoint returned no access_token");
1215
- return json;
1216
- }
1217
- function store(meta, token, clientId) {
1218
- return {
1219
- issuer: meta.issuer,
1220
- accessToken: token.access_token,
1221
- refreshToken: token.refresh_token,
1222
- expiresAt: Date.now() + (token.expires_in ?? 900) * 1e3,
1223
- scope: token.scope,
1224
- clientId
1225
- };
1226
- }
1227
- async function login(opts) {
1228
- const meta = await discover();
1229
- const server = await startCallbackServer();
1230
- try {
1231
- const existing = await readCredentials();
1232
- const clientId = existing?.clientId && existing.issuer === meta.issuer ? existing.clientId : await registerClient(meta, server.redirectUri);
1233
- const { verifier, challenge } = pkcePair2();
1234
- const state = base64url2(randomBytes2(16));
1235
- const authUrl = new URL(meta.authorization_endpoint);
1236
- authUrl.searchParams.set("response_type", "code");
1237
- authUrl.searchParams.set("client_id", clientId);
1238
- authUrl.searchParams.set("redirect_uri", server.redirectUri);
1239
- authUrl.searchParams.set("scope", pickScope(meta));
1240
- authUrl.searchParams.set("state", state);
1241
- authUrl.searchParams.set("code_challenge", challenge);
1242
- authUrl.searchParams.set("code_challenge_method", "S256");
1243
- const pendingCode = server.waitForCode(state, opts.timeoutMs ?? 3e5);
1244
- pendingCode.catch(() => void 0);
1245
- opts.onUrl?.(authUrl.toString());
1246
- await opts.openBrowser(authUrl.toString());
1247
- const code = await pendingCode;
1248
- const token = await postToken(meta, {
1249
- grant_type: "authorization_code",
1250
- code,
1251
- redirect_uri: server.redirectUri,
1252
- client_id: clientId,
1253
- code_verifier: verifier
1254
- });
1255
- await writeCredentials(store(meta, token, clientId));
1256
- return { ok: true, message: "Signed in.", scope: token.scope };
1257
- } finally {
1258
- server.close();
1259
- }
1260
- }
1261
- function pickScope(meta) {
1262
- const supported = meta.scopes_supported ?? [];
1263
- if (!supported.length) return DEFAULT_SCOPE;
1264
- return DEFAULT_SCOPE.split(" ").filter((s) => supported.includes(s)).join(" ") || supported[0];
1265
- }
1266
- async function getAccessToken() {
1267
- const creds = await readCredentials();
1268
- if (!creds) return null;
1269
- if (!isExpired(creds)) return creds.accessToken;
1270
- if (!creds.refreshToken || !creds.clientId) return null;
1271
- try {
1272
- const meta = await discover();
1273
- const token = await postToken(meta, {
1274
- grant_type: "refresh_token",
1275
- refresh_token: creds.refreshToken,
1276
- client_id: creds.clientId
1277
- });
1278
- const next = store(meta, token, creds.clientId);
1279
- if (!next.refreshToken) next.refreshToken = creds.refreshToken;
1280
- await writeCredentials(next);
1281
- return next.accessToken;
1282
- } catch {
1283
- return null;
1284
- }
1285
- }
1286
- async function logout() {
1287
- await clearCredentials();
1877
+ function discoverResendKey(env) {
1878
+ const key = env.RESEND_API_KEY?.trim();
1879
+ if (!key || !looksResendKey(key)) return void 0;
1880
+ return { apiKey: key, tier: "discovered" };
1288
1881
  }
1289
-
1290
- // src/cli/lib/ask.ts
1291
- var DEFAULT_ERROR_HINTS = [
1292
- "Re-check the arguments: carrier <domain> <command> --help",
1293
- "Or open Claude and ask there with full tool routing"
1882
+ var MANUAL_STEPS = [
1883
+ `Open ${RESEND_API_KEYS_URL} and create an API key.`,
1884
+ `Give it Sending access, and restrict it to your sending domain if that domain is verified at ${RESEND_DOMAINS_URL}.`,
1885
+ "Put the key in the storefront's .env.local as RESEND_API_KEY.",
1886
+ "Re-run `carrier site deploy` \u2014 the key is pushed to the deploy target automatically."
1294
1887
  ];
1295
- var DEFAULT_MCP_TIMEOUT_MS = 45e3;
1296
- function mcpTimeoutMs() {
1297
- const parsed = Number(process.env.CARRIER_MCP_TIMEOUT_MS);
1298
- return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MCP_TIMEOUT_MS;
1299
- }
1300
- var McpTimeoutError = class extends Error {
1301
- constructor(timeoutMs) {
1302
- super(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
1303
- this.timeoutMs = timeoutMs;
1304
- this.name = "McpTimeoutError";
1305
- }
1306
- timeoutMs;
1307
- };
1308
- function isTimeoutish(err) {
1309
- return err instanceof McpTimeoutError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
1310
- }
1311
- function parseRpcBody(raw) {
1312
- const trimmed = raw.trim();
1313
- if (!trimmed) return {};
1314
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
1315
- return JSON.parse(trimmed);
1888
+ async function provisionResend(opts) {
1889
+ const { deps } = opts;
1890
+ const notes = [];
1891
+ const discovered = discoverResendKey(opts.env);
1892
+ if (discovered) {
1893
+ const check = await validateResendKey(deps.fetch, discovered.apiKey);
1894
+ if (check.ok) return { ok: true, tier: "discovered", credentials: discovered };
1895
+ notes.push(`A RESEND_API_KEY was already set but did not work: ${check.reason}`);
1316
1896
  }
1317
- const dataLine = trimmed.split("\n").map((l) => l.trim()).find((l) => l.startsWith("data:"));
1318
- if (!dataLine) return {};
1319
- return JSON.parse(dataLine.slice(5).trim());
1320
- }
1321
- async function callMcpTool(tool, args, opts = {}) {
1322
- const token = resolveCliToken() ?? await getAccessToken();
1323
- if (!token) {
1324
- return {
1325
- ok: false,
1326
- text: withNextStep("Not signed in.", [
1327
- "Sign in from this terminal: carrier login",
1328
- "No account yet? carrier open signup",
1329
- "Headless/CI instead: export CARRIER_API_KEY=ak_\u2026 from Console \u2192 Settings \u2192 API Keys"
1330
- ])
1331
- };
1897
+ if (!opts.noBrowser) {
1898
+ const viaBrowser = await oauthTier(opts);
1899
+ if (viaBrowser.ok) {
1900
+ return { ...viaBrowser, reason: notes.length ? notes.join(" ") : void 0 };
1901
+ }
1902
+ notes.push(viaBrowser.reason ?? "The browser authorization did not complete.");
1332
1903
  }
1333
- const headers = {
1334
- Authorization: `Bearer ${token}`,
1335
- "Content-Type": "application/json",
1336
- Accept: "application/json, text/event-stream"
1337
- };
1338
- const timeoutMs = mcpTimeoutMs();
1339
- const controller = new AbortController();
1340
- let deadlineTimer;
1341
- const deadline = new Promise((_resolve, reject) => {
1342
- deadlineTimer = setTimeout(() => {
1343
- controller.abort();
1344
- reject(new McpTimeoutError(timeoutMs));
1345
- }, timeoutMs);
1346
- });
1347
- const signal = controller.signal;
1348
- try {
1349
- return await Promise.race([
1350
- (async () => {
1351
- const initRes = await fetch(MCP_URL, {
1352
- method: "POST",
1353
- headers,
1354
- signal,
1355
- body: JSON.stringify({
1356
- jsonrpc: "2.0",
1357
- id: 1,
1358
- method: "initialize",
1359
- params: {
1360
- protocolVersion: "2024-11-05",
1361
- capabilities: {},
1362
- clientInfo: { name: "carrier-cli", version: CARRIER_VERSION }
1363
- }
1364
- })
1365
- });
1366
- if (initRes.status === 401 || initRes.status === 403) {
1367
- return {
1368
- ok: false,
1369
- text: withNextStep(`MCP returned ${initRes.status} (auth rejected).`, [
1370
- "Confirm CARRIER_API_KEY is a valid org key (ak_\u2026) from app.carrier.llc",
1371
- "Or complete OCS onboarding: https://app.carrier.llc/onboarding",
1372
- "Interactive: use Claude + OAuth instead of a headless key"
1373
- ])
1374
- };
1375
- }
1376
- const sessionId = initRes.headers.get("mcp-session-id") ?? initRes.headers.get("Mcp-Session-Id");
1377
- await initRes.arrayBuffer().catch(() => void 0);
1378
- if (!sessionId) {
1379
- return {
1380
- ok: false,
1381
- text: withNextStep("MCP initialize did not return a session id.", [
1382
- "Check network access to https://mcp.carrier.llc/mcp",
1383
- "Retry later, or use Claude with the registered MCP (OAuth path)"
1384
- ])
1385
- };
1386
- }
1387
- const sessionHeaders = { ...headers, "Mcp-Session-Id": sessionId };
1388
- await fetch(MCP_URL, {
1389
- method: "POST",
1390
- headers: sessionHeaders,
1391
- signal,
1392
- body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
1393
- }).catch(() => void 0);
1394
- const res = await fetch(MCP_URL, {
1395
- method: "POST",
1396
- headers: sessionHeaders,
1397
- signal,
1398
- body: JSON.stringify({
1399
- jsonrpc: "2.0",
1400
- id: 2,
1401
- method: "tools/call",
1402
- params: { name: tool, arguments: args }
1403
- })
1404
- });
1405
- if (!res.ok) {
1406
- const snippet = (await res.text().catch(() => "")).slice(0, 240);
1407
- return {
1408
- ok: false,
1409
- text: withNextStep(`MCP call failed (${res.status}). ${snippet}`, [
1410
- "Check network access to https://mcp.carrier.llc/mcp",
1411
- "Retry later, or use Claude with the registered MCP (OAuth path)"
1412
- ])
1413
- };
1414
- }
1415
- const json = parseRpcBody(await res.text());
1416
- if (json.error?.message) {
1417
- return {
1418
- ok: false,
1419
- text: withNextStep(json.error.message, opts.errorHints ?? DEFAULT_ERROR_HINTS)
1420
- };
1421
- }
1422
- const parts = json.result?.content ?? [];
1423
- const text3 = parts.map((p4) => p4.text).filter(Boolean).join("\n").trim() || JSON.stringify(json.result ?? json, null, 2);
1424
- return {
1425
- ok: !json.result?.isError,
1426
- text: text3,
1427
- structured: json.result?.structuredContent
1904
+ if (deps.promptForKey) {
1905
+ await deps.openBrowser(RESEND_API_KEYS_URL);
1906
+ const pasted = (await deps.promptForKey(RESEND_API_KEYS_URL))?.trim();
1907
+ if (pasted) {
1908
+ const check = await validateResendKey(deps.fetch, pasted);
1909
+ if (check.ok) {
1910
+ return {
1911
+ ok: true,
1912
+ tier: "paste",
1913
+ credentials: { apiKey: pasted, tier: "paste" },
1914
+ reason: notes.length ? notes.join(" ") : void 0
1428
1915
  };
1429
- })(),
1430
- deadline
1431
- ]);
1432
- } catch (e) {
1433
- if (isTimeoutish(e)) {
1434
- const secs = Math.round(timeoutMs / 1e3);
1435
- return {
1436
- ok: false,
1437
- text: withNextStep(
1438
- `MCP call to ${tool} timed out after ${secs}s (no response).`,
1439
- [
1440
- "The tool is likely returning more data than the MCP transport can deliver \u2014 see issue #663",
1441
- "Narrow the request (a specific --account-id / --iccid returns far less than a whole-fleet call)",
1442
- `Raise the deadline for one slow call: CARRIER_MCP_TIMEOUT_MS=${timeoutMs * 2} carrier \u2026`,
1443
- "Confirm the service is up: check network access to https://mcp.carrier.llc/mcp",
1444
- "Or open Claude and ask there with full tool routing"
1445
- ]
1446
- )
1447
- };
1916
+ }
1917
+ notes.push(check.reason ?? "The pasted key did not work.");
1448
1918
  }
1449
- const msg = e instanceof Error ? e.message : String(e);
1450
- return {
1451
- ok: false,
1452
- text: withNextStep(`Could not reach MCP: ${msg}`, [
1453
- "Verify outbound HTTPS to mcp.carrier.llc",
1454
- "Use the interactive Claude path while offline from this machine"
1455
- ])
1456
- };
1457
- } finally {
1458
- if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
1459
1919
  }
1920
+ return { ok: false, tier: "manual", reason: notes.join(" "), guidance: MANUAL_STEPS };
1460
1921
  }
1461
- var ASK_ERROR_HINTS = [
1462
- "Rephrase the intent more specifically (include ICCID / MSISDN if relevant)",
1463
- "Or use a discrete command: carrier --help lists every domain"
1464
- ];
1465
- function parseRouting(text3) {
1466
- const trimmed = text3.trim();
1467
- if (!trimmed.startsWith("{")) return void 0;
1922
+ async function oauthTier(opts) {
1923
+ const { deps } = opts;
1924
+ let server;
1468
1925
  try {
1469
- const parsed = JSON.parse(trimmed);
1470
- if (!parsed || typeof parsed !== "object") return void 0;
1471
- const routes = typeof parsed.resolved_tool === "string" && parsed.resolved_tool;
1472
- const asksBack = typeof parsed.clarifying_question === "string" || Array.isArray(parsed.candidates);
1473
- return routes || asksBack ? parsed : void 0;
1474
- } catch {
1475
- return void 0;
1476
- }
1477
- }
1478
- async function carrierAsk(intent) {
1479
- const routed = await callMcpTool("carrier_ask", { intent }, { errorHints: ASK_ERROR_HINTS });
1480
- if (!routed.ok) return routed;
1481
- const routing = parseRouting(routed.text);
1482
- if (!routing) return routed;
1483
- if (!routing.resolved_tool) {
1484
- const candidates = (routing.candidates ?? []).map((c) => c.tool ? `Candidate: ${c.tool}${c.reason ? ` \u2014 ${c.reason}` : ""}` : void 0).filter((s) => Boolean(s));
1926
+ server = await deps.startLoopback();
1927
+ } catch (e) {
1485
1928
  return {
1486
1929
  ok: false,
1487
- text: withNextStep(routing.clarifying_question?.trim() || "Carrier could not tell which tool you meant.", [
1488
- ...candidates,
1489
- ...ASK_ERROR_HINTS
1490
- ])
1930
+ tier: "oauth",
1931
+ reason: `Could not open a local callback listener: ${e instanceof Error ? e.message : String(e)}`
1491
1932
  };
1492
1933
  }
1493
- const tool = routing.resolved_tool;
1494
- const args = routing.resolved_params ?? {};
1495
- if (routing.match && routing.match !== "confirmed") {
1934
+ try {
1935
+ const registered = await registerOauthClient(deps.fetch);
1936
+ if (!registered.ok) return { ok: false, tier: "oauth", reason: registered.reason };
1937
+ const { verifier, challenge } = pkcePair2();
1938
+ const state = base64url2(randomBytes2(24));
1939
+ const authorizeUrl = buildAuthorizeUrl({
1940
+ clientId: registered.clientId,
1941
+ redirectUri: server.redirectUri,
1942
+ state,
1943
+ challenge
1944
+ });
1945
+ const opened = await deps.openBrowser(authorizeUrl);
1946
+ deps.notify(
1947
+ opened.ok ? "Approve the connection in your browser \u2014 this waits for you." : `Open this to approve the connection:
1948
+ ${authorizeUrl}`
1949
+ );
1950
+ const callback = await server.waitForCode(state, opts.timeoutMs ?? 18e4);
1951
+ if (!callback.ok || !callback.code) {
1952
+ return { ok: false, tier: "oauth", reason: callback.reason ?? "No authorization code." };
1953
+ }
1954
+ const token = await exchangeCode(deps.fetch, {
1955
+ clientId: registered.clientId,
1956
+ code: callback.code,
1957
+ redirectUri: server.redirectUri,
1958
+ verifier
1959
+ });
1960
+ if (!token.ok) return { ok: false, tier: "oauth", reason: token.reason };
1961
+ const domainId = opts.domain ? await findVerifiedDomainId(deps.fetch, token.accessToken, opts.domain) : void 0;
1962
+ const created = await createSendingKey(deps.fetch, token.accessToken, {
1963
+ name: `${opts.name} storefront`,
1964
+ domainId
1965
+ });
1966
+ if (token.refreshToken) {
1967
+ await revokeGrant(deps.fetch, {
1968
+ clientId: registered.clientId,
1969
+ refreshToken: token.refreshToken
1970
+ });
1971
+ }
1972
+ if (!created.ok) return { ok: false, tier: "oauth", reason: created.reason };
1973
+ const check = await validateResendKey(deps.fetch, created.token);
1974
+ if (!check.ok) {
1975
+ return { ok: false, tier: "oauth", reason: `Resend minted a key that it then rejected: ${check.reason}` };
1976
+ }
1496
1977
  return {
1497
- ok: false,
1498
- text: withNextStep(`Carrier could not confirm what you meant (match: ${routing.match}).`, [
1499
- `Closest tool: ${tool}`,
1500
- ...ASK_ERROR_HINTS
1501
- ])
1978
+ ok: true,
1979
+ tier: "oauth",
1980
+ credentials: {
1981
+ apiKey: created.token,
1982
+ tier: "oauth",
1983
+ keyId: created.id,
1984
+ domain: domainId ? opts.domain : void 0
1985
+ }
1502
1986
  };
1987
+ } finally {
1988
+ server.close();
1503
1989
  }
1504
- if (routing.confirm_required) {
1505
- return {
1506
- ok: false,
1507
- text: withNextStep(`"${intent}" resolves to ${tool}, which changes live service or billing.`, [
1508
- `Params: ${JSON.stringify(args)}`,
1509
- "Run the matching discrete command so every flag is explicit and reviewable",
1510
- "carrier --help lists every domain"
1511
- ])
1512
- };
1990
+ }
1991
+
1992
+ // src/cli/lib/dash.ts
1993
+ var DASH_DOMAINS = [
1994
+ "fleet",
1995
+ "subscribers",
1996
+ "usage",
1997
+ "packages",
1998
+ "billing",
1999
+ "wallet",
2000
+ "greenzone",
2001
+ "storefront"
2002
+ ];
2003
+ function isDashDomain(value) {
2004
+ return DASH_DOMAINS.includes(value);
2005
+ }
2006
+ var DASH_TOOL = {
2007
+ fleet: "fleet_health_app",
2008
+ subscribers: "list_subscribers",
2009
+ usage: "subscriber_usage",
2010
+ packages: "list_package_templates",
2011
+ billing: "billing_events",
2012
+ wallet: "wallet_balance",
2013
+ greenzone: "greenzone_whitelist_list",
2014
+ storefront: ""
2015
+ };
2016
+ function screenFromStructured(structured) {
2017
+ const rec = structured;
2018
+ const screen = rec?.screen;
2019
+ if (screen && typeof screen === "object" && Array.isArray(screen.sections) && screen.title) {
2020
+ return screen;
2021
+ }
2022
+ return void 0;
2023
+ }
2024
+ var num = (value, fallback = 0) => {
2025
+ const n = Number(value);
2026
+ return Number.isFinite(n) ? n : fallback;
2027
+ };
2028
+ var str = (value, fallback = "") => value === void 0 || value === null ? fallback : String(value);
2029
+ function rows(payload, keys) {
2030
+ if (Array.isArray(payload)) return payload;
2031
+ const rec = payload ?? {};
2032
+ for (const key of keys) {
2033
+ const candidate = rec[key];
2034
+ if (Array.isArray(candidate)) return candidate;
2035
+ }
2036
+ return [];
2037
+ }
2038
+ function buildDashScreen(domain, payload) {
2039
+ switch (domain) {
2040
+ case "fleet": {
2041
+ const accounts = rows(payload, ["accounts", "account"]);
2042
+ return fleetScreen({
2043
+ active: num(payload?.active),
2044
+ suspended: num(payload?.suspended),
2045
+ inventory: num(payload?.inventory),
2046
+ other: num(payload?.other),
2047
+ accounts: accounts.map((a) => ({
2048
+ name: str(a.name ?? a.accountId, "?"),
2049
+ balance: num(a.balance),
2050
+ active: num(a.active),
2051
+ suspended: num(a.suspended),
2052
+ inventory: num(a.inventory),
2053
+ other: num(a.other),
2054
+ packageOnly: Boolean(a.packageOnly)
2055
+ }))
2056
+ });
2057
+ }
2058
+ case "subscribers":
2059
+ return subscribersScreen(
2060
+ rows(payload, ["subscribers", "subscriber", "data"]).map((s) => ({
2061
+ iccid: str(s.iccid ?? s.ICCID, "?"),
2062
+ msisdn: s.msisdn ? str(s.msisdn) : void 0,
2063
+ status: str(s.status ?? s.statusStr, "unknown"),
2064
+ account: s.accountName ? str(s.accountName) : void 0,
2065
+ dataUsedBytes: s.dataUsed === void 0 ? void 0 : num(s.dataUsed)
2066
+ }))
2067
+ );
2068
+ case "usage":
2069
+ return usageScreen({
2070
+ subject: str(payload?.subject, "Subscriber"),
2071
+ timeline: rows(payload, ["timeline", "days", "usage"]).map((p4) => ({
2072
+ date: str(p4.date ?? p4.day, "?"),
2073
+ bytes: num(p4.bytes ?? p4.dataUsed)
2074
+ })),
2075
+ countries: rows(payload, ["countries"]).map((c) => ({
2076
+ country: str(c.country ?? c.countryName, "?"),
2077
+ bytes: num(c.bytes ?? c.dataUsed)
2078
+ }))
2079
+ });
2080
+ case "packages":
2081
+ return packagesScreen(
2082
+ rows(payload, ["template", "templates", "data"]).map((t) => ({
2083
+ name: str(t.prepaidpackagetemplatename ?? t.name, "?"),
2084
+ id: str(t.prepaidpackagetemplateid ?? t.id, "?"),
2085
+ dataLimitBytes: t.databyte === void 0 ? void 0 : num(t.databyte),
2086
+ validityDays: t.perioddays === void 0 ? void 0 : num(t.perioddays),
2087
+ price: t.cost === void 0 ? void 0 : num(t.cost),
2088
+ recurring: Boolean(t.recurring)
2089
+ }))
2090
+ );
2091
+ case "billing":
2092
+ return billingScreen({
2093
+ balance: payload?.balance === void 0 ? void 0 : num(payload.balance),
2094
+ currency: str(payload?.currency, ""),
2095
+ events: rows(payload, ["events", "data"]).map((e) => ({
2096
+ date: str(e.date ?? e.timestamp, "?"),
2097
+ description: str(e.description ?? e.type, "\u2014"),
2098
+ amount: num(e.amount)
2099
+ }))
2100
+ });
2101
+ case "wallet": {
2102
+ const p4 = payload ?? {};
2103
+ const eur = (value) => value === void 0 || value === null ? void 0 : num(value) / 100;
2104
+ return walletScreen({
2105
+ balance: eur(p4.balance_eur_cents ?? p4.balance) ?? 0,
2106
+ currency: str(p4.currency, "\u20AC"),
2107
+ autoTopupEnabled: Boolean(p4.autoTopupEnabled ?? p4.auto_topup_enabled),
2108
+ threshold: eur(p4.auto_topup_threshold_cents ?? p4.threshold),
2109
+ credits: p4.credits === void 0 || p4.credits === null ? void 0 : num(p4.credits)
2110
+ });
2111
+ }
2112
+ case "greenzone":
2113
+ return greenzoneScreen(
2114
+ rows(payload, ["entries", "whitelist", "data"]).map((e) => ({
2115
+ value: str(e.value ?? e.prefix ?? e.destination, "?"),
2116
+ note: e.note ? str(e.note) : void 0
2117
+ }))
2118
+ );
2119
+ case "storefront": {
2120
+ const p4 = payload ?? {};
2121
+ return storefrontScreen({
2122
+ brand: str(p4.brand ?? p4.project, "storefront"),
2123
+ url: p4.url ? str(p4.url) : void 0,
2124
+ target: p4.target ? str(p4.target) : void 0,
2125
+ verified: p4.verified === void 0 ? null : Boolean(p4.verified),
2126
+ diagnosis: p4.diagnosis ? str(p4.diagnosis) : void 0,
2127
+ secretsStaged: Array.isArray(p4.secrets_staged) ? p4.secrets_staged : void 0,
2128
+ plans: p4.plans === void 0 ? void 0 : num(p4.plans)
2129
+ });
2130
+ }
1513
2131
  }
1514
- return callMcpTool(tool, args, { errorHints: ASK_ERROR_HINTS });
1515
2132
  }
1516
2133
 
1517
2134
  // src/cli/lib/home.ts
2135
+ import * as p from "@clack/prompts";
2136
+ import pc2 from "picocolors";
1518
2137
  function mark(ok2) {
1519
2138
  return ok2 ? pc2.green("\u25CF") : pc2.yellow("\u25CB");
1520
2139
  }
@@ -3441,6 +4060,7 @@ var CLI_DOMAINS = [
3441
4060
  tool: "greenzone_whitelist_remove",
3442
4061
  summary: "Remove a host or IP from the Greenzone whitelist. Needs Kapture running in the operator's own Chrome \u2014 without it the dry run still returns the portal steps to follow by hand.",
3443
4062
  write: true,
4063
+ requireConfirm: true,
3444
4064
  options: [
3445
4065
  {
3446
4066
  flags: "--host <host>",
@@ -3454,8 +4074,7 @@ var CLI_DOMAINS = [
3454
4074
  arg: "ip",
3455
4075
  type: "string"
3456
4076
  }
3457
- ],
3458
- requireConfirm: true
4077
+ ]
3459
4078
  }
3460
4079
  ]
3461
4080
  },
@@ -3622,6 +4241,7 @@ var CLI_DOMAINS = [
3622
4241
  tool: "ui_create_account",
3623
4242
  summary: "Creates a new sub-account under the reseller via the OCS web dashboard.",
3624
4243
  write: true,
4244
+ requireConfirm: true,
3625
4245
  options: [
3626
4246
  {
3627
4247
  flags: "--name <name>",
@@ -3642,8 +4262,7 @@ var CLI_DOMAINS = [
3642
4262
  arg: "initial_balance",
3643
4263
  type: "number"
3644
4264
  }
3645
- ],
3646
- requireConfirm: true
4265
+ ]
3647
4266
  },
3648
4267
  {
3649
4268
  name: "steering build",
@@ -3823,6 +4442,7 @@ var CLI_DOMAINS = [
3823
4442
  tool: "ui_delete_destination_list",
3824
4443
  summary: "Delete a destination list \u2014 a named set of phone-number prefixes \u2014 by driving the OCS web dashboard.",
3825
4444
  write: true,
4445
+ requireConfirm: true,
3826
4446
  options: [
3827
4447
  {
3828
4448
  flags: "--destination-list-id <n>",
@@ -3831,8 +4451,7 @@ var CLI_DOMAINS = [
3831
4451
  type: "number",
3832
4452
  required: true
3833
4453
  }
3834
- ],
3835
- requireConfirm: true
4454
+ ]
3836
4455
  },
3837
4456
  {
3838
4457
  name: "zones edit",
@@ -3872,6 +4491,7 @@ var CLI_DOMAINS = [
3872
4491
  tool: "ui_delete_location_zone",
3873
4492
  summary: "Delete a location zone by driving the OCS web dashboard, because the OCS REST API can create a zone (`create_location_zone`) but not remove one.",
3874
4493
  write: true,
4494
+ requireConfirm: true,
3875
4495
  options: [
3876
4496
  {
3877
4497
  flags: "--zone-id <n>",
@@ -3880,14 +4500,14 @@ var CLI_DOMAINS = [
3880
4500
  type: "number",
3881
4501
  required: true
3882
4502
  }
3883
- ],
3884
- requireConfirm: true
4503
+ ]
3885
4504
  },
3886
4505
  {
3887
4506
  name: "templates delete",
3888
4507
  tool: "ui_delete_package_template",
3889
4508
  summary: "Remove a package template from the reseller's product catalogue for good, by driving the OCS web dashboard \u2014 the OCS REST API cannot delete a template.",
3890
4509
  write: true,
4510
+ requireConfirm: true,
3891
4511
  options: [
3892
4512
  {
3893
4513
  flags: "--template-id <n>",
@@ -3896,8 +4516,7 @@ var CLI_DOMAINS = [
3896
4516
  type: "number",
3897
4517
  required: true
3898
4518
  }
3899
- ],
3900
- requireConfirm: true
4519
+ ]
3901
4520
  }
3902
4521
  ]
3903
4522
  },
@@ -4191,6 +4810,59 @@ var CLI_DOMAINS = [
4191
4810
  ]
4192
4811
  }
4193
4812
  ]
4813
+ },
4814
+ {
4815
+ name: "atlas",
4816
+ summary: "Carrier Atlas coverage catalog. Configured networks, not live radio telemetry.",
4817
+ commands: [
4818
+ {
4819
+ name: "coverage",
4820
+ tool: "get_atlas_coverage",
4821
+ summary: "List the mobile networks Carrier Atlas has configured for one country.",
4822
+ options: [
4823
+ {
4824
+ flags: "--country <country>",
4825
+ description: "ISO 3166-1 alpha-2 country code, for example JP or NL.",
4826
+ arg: "country",
4827
+ type: "string",
4828
+ required: true
4829
+ },
4830
+ {
4831
+ flags: "--status <status>",
4832
+ description: "Keep only networks whose latest probe has this status. One of: up, degraded, down, unknown.",
4833
+ arg: "status",
4834
+ type: "string"
4835
+ },
4836
+ {
4837
+ flags: "--technology <technology>",
4838
+ description: "Keep only networks of this radio generation. One of: 2G, 3G, 4G, 5G.",
4839
+ arg: "technology",
4840
+ type: "string"
4841
+ }
4842
+ ]
4843
+ },
4844
+ {
4845
+ name: "network",
4846
+ tool: "get_atlas_network_status",
4847
+ summary: "Read one mobile network from Carrier Atlas by MCC and MNC, including the latest status sample and the last 24 hours of probes.",
4848
+ options: [
4849
+ {
4850
+ flags: "--mcc <mcc>",
4851
+ description: "Mobile Country Code.",
4852
+ arg: "mcc",
4853
+ type: "string",
4854
+ required: true
4855
+ },
4856
+ {
4857
+ flags: "--mnc <mnc>",
4858
+ description: "Mobile Network Code.",
4859
+ arg: "mnc",
4860
+ type: "string",
4861
+ required: true
4862
+ }
4863
+ ]
4864
+ }
4865
+ ]
4194
4866
  }
4195
4867
  // </generated:catalog-domains>
4196
4868
  ];
@@ -4201,171 +4873,6 @@ function capabilityTarget(cmd) {
4201
4873
  // src/cli/lib/capability.ts
4202
4874
  import * as p2 from "@clack/prompts";
4203
4875
  import pc3 from "picocolors";
4204
-
4205
- // src/cli/lib/rest.ts
4206
- var DEFAULT_ERROR_HINTS2 = [
4207
- "Re-check the arguments: carrier <domain> <command> --help",
4208
- "Or open Claude and ask there with full tool routing"
4209
- ];
4210
- function resolveApiKey() {
4211
- return process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || null;
4212
- }
4213
- function ocsTokenOnly() {
4214
- return Boolean(
4215
- process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim()
4216
- );
4217
- }
4218
- function applyPath(path, args) {
4219
- const rest = { ...args };
4220
- const missing = [];
4221
- const filled = path.replace(/\{(\w+)\}/g, (_match, name) => {
4222
- const value = rest[name];
4223
- if (value === void 0 || value === null || value === "") {
4224
- missing.push(name);
4225
- return `{${name}}`;
4226
- }
4227
- delete rest[name];
4228
- return encodeURIComponent(String(value));
4229
- });
4230
- return { path: filled, rest, missing };
4231
- }
4232
- function queryString(args) {
4233
- const params = new URLSearchParams();
4234
- for (const [key, value] of Object.entries(args)) {
4235
- if (value === void 0 || value === null) continue;
4236
- params.set(key, String(value));
4237
- }
4238
- const qs = params.toString();
4239
- return qs ? `?${qs}` : "";
4240
- }
4241
- function bodyText(raw) {
4242
- const trimmed = raw.trim();
4243
- if (!trimmed) return "";
4244
- try {
4245
- const parsed = JSON.parse(trimmed);
4246
- if (typeof parsed.error === "string") return parsed.error;
4247
- if (typeof parsed.message === "string") return parsed.message;
4248
- return trimmed;
4249
- } catch {
4250
- return trimmed;
4251
- }
4252
- }
4253
- async function callRestEndpoint(method, path, args, opts = {}) {
4254
- const token = resolveApiKey() ?? await getAccessToken();
4255
- if (!token) {
4256
- return {
4257
- ok: false,
4258
- text: withNextStep(
4259
- ocsTokenOnly() ? "Not signed in. An eSIMVault/OCS token is set, but the Carrier API only accepts Carrier credentials." : "Not signed in.",
4260
- [
4261
- "Sign in from this terminal: carrier login",
4262
- "No account yet? carrier open signup",
4263
- "Headless/CI instead: export CARRIER_API_KEY=ak_\u2026 from Console \u2192 Settings \u2192 API Keys"
4264
- ]
4265
- )
4266
- };
4267
- }
4268
- const { path: resolvedPath, rest, missing } = applyPath(path, args);
4269
- if (missing.length) {
4270
- return {
4271
- ok: false,
4272
- text: withNextStep(`Cannot build ${method} ${path}.`, [
4273
- `Missing path value(s): ${missing.join(", ")}`,
4274
- "See every flag: carrier <domain> <command> --help"
4275
- ])
4276
- };
4277
- }
4278
- const sendsBody = method === "POST" || method === "PUT" || method === "PATCH";
4279
- const url = `${API_URL}${resolvedPath}${sendsBody ? "" : queryString(rest)}`;
4280
- const headers = {
4281
- Authorization: `Bearer ${token}`,
4282
- Accept: "application/json",
4283
- "User-Agent": `carrier-cli/${CARRIER_VERSION}`
4284
- };
4285
- if (sendsBody) headers["Content-Type"] = "application/json";
4286
- if (opts.confirmToken) headers["X-Confirm-Token"] = opts.confirmToken;
4287
- const timeoutMs = mcpTimeoutMs();
4288
- const controller = new AbortController();
4289
- const timer = setTimeout(() => controller.abort(), timeoutMs);
4290
- try {
4291
- const res = await fetch(url, {
4292
- method,
4293
- headers,
4294
- signal: controller.signal,
4295
- body: sendsBody ? JSON.stringify(rest) : void 0
4296
- });
4297
- const raw = await res.text();
4298
- if (res.status === 202) {
4299
- let confirmToken;
4300
- let expires;
4301
- let message = "";
4302
- try {
4303
- const parsed = JSON.parse(raw);
4304
- confirmToken = parsed.confirm_token;
4305
- expires = parsed.expires_in_seconds;
4306
- message = parsed.message ?? "";
4307
- } catch {
4308
- }
4309
- return {
4310
- ok: false,
4311
- text: withNextStep(
4312
- message || `${method} ${path} needs an explicit confirmation.`,
4313
- confirmToken ? [
4314
- `Re-run the same command with: --confirm-token ${confirmToken}`,
4315
- `The token is single-use and expires in ${expires ?? 120}s.`
4316
- ] : ["The server asked for confirmation but returned no token \u2014 retry the command."]
4317
- )
4318
- };
4319
- }
4320
- if (res.status === 401 || res.status === 403) {
4321
- return {
4322
- ok: false,
4323
- text: withNextStep(
4324
- `Carrier API returned ${res.status} (auth rejected).`,
4325
- [
4326
- "Confirm CARRIER_API_KEY is a valid org key (ak_\u2026) from app.carrier.llc",
4327
- "Or sign in from this terminal: carrier login",
4328
- ...res.status === 403 ? ["A 403 here usually means the key lacks write scope."] : []
4329
- ]
4330
- )
4331
- };
4332
- }
4333
- if (!res.ok) {
4334
- return {
4335
- ok: false,
4336
- text: withNextStep(
4337
- redact(`${method} ${path} failed (${res.status}). ${bodyText(raw).slice(0, 500)}`),
4338
- opts.errorHints ?? DEFAULT_ERROR_HINTS2
4339
- )
4340
- };
4341
- }
4342
- return { ok: true, text: raw.trim() || JSON.stringify({ status: res.status }) };
4343
- } catch (e) {
4344
- const aborted = e instanceof Error && (e.name === "AbortError" || e.name === "TimeoutError");
4345
- if (aborted) {
4346
- const secs = Math.round(timeoutMs / 1e3);
4347
- return {
4348
- ok: false,
4349
- text: withNextStep(`${method} ${path} timed out after ${secs}s (no response).`, [
4350
- `Raise the deadline for one slow call: CARRIER_MCP_TIMEOUT_MS=${timeoutMs * 2} carrier \u2026`,
4351
- "Confirm the service is up: check network access to https://api.carrier.llc"
4352
- ])
4353
- };
4354
- }
4355
- const msg = e instanceof Error ? e.message : String(e);
4356
- return {
4357
- ok: false,
4358
- text: withNextStep(redact(`Could not reach the Carrier API: ${msg}`), [
4359
- "Verify outbound HTTPS to api.carrier.llc",
4360
- "Use the interactive Claude path while offline from this machine"
4361
- ])
4362
- };
4363
- } finally {
4364
- clearTimeout(timer);
4365
- }
4366
- }
4367
-
4368
- // src/cli/lib/capability.ts
4369
4876
  var MAX_PRETTY_CHARS = 8e3;
4370
4877
  function optionKey(flags) {
4371
4878
  const long = flags.split(/[ ,|]+/).find((t) => t.startsWith("--"));
@@ -5477,6 +5984,22 @@ site.command("deploy [dir]").description("Build + deploy a storefront to whichev
5477
5984
  p3.outro("");
5478
5985
  }
5479
5986
  );
5987
+ site.command("publish").description("Publish a Carrier-hosted storefront. Does not deploy a self-hosted copy.").requiredOption("--slug <slug>", "Shop address, such as acme or Acme Mobile").requiredOption("--name <name>", "Brand name").option("--status <status>", "draft or published", "published").option("--accent <hex>", "Accent color, such as #112233").option("--accent-dark <hex>", "Darker accent").option("--bg <hex>", "Page background").option("--text <hex>", "Body text color").option("--logo <url-or-file>", "https logo address, or a PNG, JPEG, WebP, or GIF under 70 KB").option("--hero <text>", "Shop headline").option("--tagline <text>", "Line under the headline").option("--support-email <email>", "Support address").option("--legal-name <name>", "Legal name in the footer").option("--product <id>", "Connectivity plan id. Copies its template, price, and allowance.").option("--plan <slug>", "Plan slug, such as eu-1gb").option("--plan-name <name>", "Plan name on the shop").option("--price <amount>", "Retail price, such as 9.00").option("--price-cents <cents>", "Retail price in minor units").option("--currency <code>", "Three-letter currency code").option("--template-id <id>", "OCS package template id").option("--data-gb <gb>", "Data allowance, such as 1 or 1 GB").option("--validity-days <days>", "Validity, such as 7 or 7 days").option("--countries <names>", "Country names or codes, such as France, DE").option("--plan-text <text>", "Buyer-facing sentence. Omit to leave the saved text unchanged.").option("--inactive", "Keep the plan off the shop").action(async (o) => {
5988
+ header();
5989
+ const built = hostedPublishInputFromFlags(o);
5990
+ if (!built.ok) {
5991
+ fail(built.text, ["Re-run with --status published"]);
5992
+ process.exitCode = 1;
5993
+ return;
5994
+ }
5995
+ const result = await publishHostedStorefront(built.input);
5996
+ if (!result.ok) {
5997
+ fail(result.text, ["Sign in: carrier login", "Or export CARRIER_API_KEY from the console"]);
5998
+ process.exitCode = 1;
5999
+ return;
6000
+ }
6001
+ ok(result.text);
6002
+ });
5480
6003
  site.command("targets [dir]").description("Show which deploy hosts are installed, logged in, and ready.").action(async (dir) => {
5481
6004
  header();
5482
6005
  const storefront = resolve2(dir ?? "./storefront");