@carrierllc/mcp 0.12.0 → 0.13.3

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-J52VP7E7.js";
42
+ } from "./chunk-2TVU2GRC.js";
43
43
  import {
44
44
  copyTree,
45
45
  exists,
@@ -272,6 +272,320 @@ 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());
288
+ }
289
+ function base64url(input) {
290
+ return input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
291
+ }
292
+ function pkcePair() {
293
+ const verifier = base64url(randomBytes(64));
294
+ return { verifier, challenge: base64url(createHash("sha256").update(verifier).digest()) };
295
+ }
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();
306
+ }
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);
317
+ }
318
+ 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) };
337
+ }
338
+ }
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;
343
+ }
344
+ async function registerOauthClient(fetchImpl) {
345
+ const registeredRedirect = `http://${REDIRECT_HOST}${REDIRECT_PATH}`;
346
+ const res = await resendFetch(fetchImpl, "/oauth/register", {
347
+ method: "POST",
348
+ json: {
349
+ client_name: CLIENT_NAME,
350
+ redirect_uris: [registeredRedirect],
351
+ grant_types: ["authorization_code", "refresh_token"],
352
+ response_types: ["code"],
353
+ token_endpoint_auth_method: "none",
354
+ scope: OAUTH_SCOPE
355
+ }
356
+ });
357
+ if (!res.ok) {
358
+ return { ok: false, reason: `Resend rejected client registration: ${res.error ?? res.status}` };
359
+ }
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 };
379
+ }
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);
389
+ }
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;
398
+ }
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 } : {}
408
+ }
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();
429
+ return;
430
+ }
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);
445
+ });
446
+ const port = server.address().port;
447
+ const redirectUri = `http://${REDIRECT_HOST}:${port}${REDIRECT_PATH}`;
448
+ 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
+ };
459
+ const timer = setTimeout(
460
+ () => done({ ok: false, reason: "Timed out waiting for the browser." }),
461
+ timeoutMs
462
+ );
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);
469
+ };
470
+ });
471
+ }
472
+ };
473
+ }
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
+ }
516
+ }
517
+ return { ok: false, tier: "manual", reason: notes.join(" "), guidance: MANUAL_STEPS };
518
+ }
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
+ }
531
+ try {
532
+ const registered = await registerOauthClient(deps.fetch);
533
+ if (!registered.ok) return { ok: false, tier: "oauth", reason: registered.reason };
534
+ 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
562
+ });
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
+ };
584
+ } finally {
585
+ server.close();
586
+ }
587
+ }
588
+
275
589
  // src/cli/lib/dash.ts
276
590
  var DASH_DOMAINS = [
277
591
  "fleet",
@@ -417,6 +731,54 @@ function buildDashScreen(domain, payload) {
417
731
  // src/cli/lib/status.ts
418
732
  import { homedir as homedir2 } from "os";
419
733
  import { join as join5 } from "path";
734
+
735
+ // src/cli/lib/mcp-url.ts
736
+ var PRODUCTION_MCP_URL = "https://mcp.carrier.llc/mcp";
737
+ var PRODUCTION_HOST = "mcp.carrier.llc";
738
+ var URL_TOKEN = /https?:\/\/[^\s"'<>)]+/g;
739
+ function pathWithoutTrailingSlash(pathname) {
740
+ let end = pathname.length;
741
+ while (end > 1 && pathname.charCodeAt(end - 1) === 47) end -= 1;
742
+ return pathname.slice(0, end);
743
+ }
744
+ function parseHttpUrl(value) {
745
+ try {
746
+ const parsed = new URL(value);
747
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return null;
748
+ return parsed;
749
+ } catch {
750
+ return null;
751
+ }
752
+ }
753
+ function isProductionMcpUrl(value) {
754
+ if (value === PRODUCTION_MCP_URL) return true;
755
+ const parsed = parseHttpUrl(value);
756
+ if (!parsed || parsed.protocol !== "https:" || parsed.hostname !== PRODUCTION_HOST) return false;
757
+ return pathWithoutTrailingSlash(parsed.pathname) === "/mcp";
758
+ }
759
+ function isCarrierMcpHost(value) {
760
+ const parsed = parseHttpUrl(value);
761
+ return parsed?.protocol === "https:" && parsed.hostname === PRODUCTION_HOST;
762
+ }
763
+ function trimTrailingPunctuation(token) {
764
+ let end = token.length;
765
+ while (end > 0) {
766
+ const code = token.charCodeAt(end - 1);
767
+ if (code !== 46 && code !== 44 && code !== 59 && code !== 58) break;
768
+ end -= 1;
769
+ }
770
+ return token.slice(0, end);
771
+ }
772
+ function lineMentionsProductionMcp(line) {
773
+ const tokens = line.match(URL_TOKEN);
774
+ if (!tokens) return false;
775
+ for (const token of tokens) {
776
+ if (isProductionMcpUrl(trimTrailingPunctuation(token))) return true;
777
+ }
778
+ return false;
779
+ }
780
+
781
+ // src/cli/lib/status.ts
420
782
  function detectEnvToken() {
421
783
  const api = process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || "";
422
784
  if (api.startsWith("ak_") || api.length > 0) {
@@ -432,8 +794,8 @@ function rankHit(name, url) {
432
794
  else if (/^carrier[-_]?/i.test(name) && !/test|staging|stg|dev|local/i.test(name)) rank += 40;
433
795
  else if (/carrier/i.test(name) && !/test|staging|stg|dev|local/i.test(name)) rank += 20;
434
796
  else if (/carrier/i.test(name)) rank += 5;
435
- if (url === MCP_URL || url.includes("https://mcp.carrier.llc/mcp")) rank += 50;
436
- else if (/mcp\.carrier\.llc/i.test(url)) rank += 25;
797
+ if (isProductionMcpUrl(url)) rank += 50;
798
+ else if (isCarrierMcpHost(url)) rank += 25;
437
799
  return rank;
438
800
  }
439
801
  function scanMcpServers(servers) {
@@ -445,10 +807,10 @@ function scanMcpServers(servers) {
445
807
  }
446
808
  const url = typeof entry?.url === "string" ? entry.url : "";
447
809
  const rank = rankHit(name, url);
448
- if (rank < 20 && !url.includes("mcp.carrier.llc")) {
810
+ if (rank < 20 && !isCarrierMcpHost(url)) {
449
811
  continue;
450
812
  }
451
- const urlMatches = url === MCP_URL || url.includes("https://mcp.carrier.llc/mcp");
813
+ const urlMatches = isProductionMcpUrl(url);
452
814
  const hit = {
453
815
  registered: true,
454
816
  urlMatches,
@@ -526,7 +888,7 @@ ${r.stderr}`;
526
888
  if (!carrierLine) {
527
889
  return { registered: false, urlMatches: false, detail: "carrier not in claude mcp list" };
528
890
  }
529
- const urlMatches = carrierLine.includes(MCP_URL) || /mcp\.carrier\.llc\/mcp/i.test(carrierLine);
891
+ const urlMatches = lineMentionsProductionMcp(carrierLine);
530
892
  return { registered: true, urlMatches, detail: carrierLine.slice(0, 200) };
531
893
  }
532
894
  function buildNextSteps(s) {
@@ -649,8 +1011,8 @@ import * as p from "@clack/prompts";
649
1011
  import pc2 from "picocolors";
650
1012
 
651
1013
  // src/cli/lib/auth.ts
652
- import { createHash, randomBytes, timingSafeEqual } from "crypto";
653
- import { createServer } from "http";
1014
+ import { createHash as createHash2, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
1015
+ import { createServer as createServer2 } from "http";
654
1016
 
655
1017
  // src/cli/lib/credentials.ts
656
1018
  import { homedir as homedir3 } from "os";
@@ -694,7 +1056,7 @@ function redact(text3) {
694
1056
 
695
1057
  // src/cli/lib/auth.ts
696
1058
  var DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
697
- var CLIENT_NAME = "Carrier CLI";
1059
+ var CLIENT_NAME2 = "Carrier CLI";
698
1060
  var DEFAULT_SCOPE = "read write";
699
1061
  var CALLBACK_PORTS = [3118, 3119, 8976, 8977, 0];
700
1062
  async function discover(base = MCP_HOME) {
@@ -708,12 +1070,12 @@ async function discover(base = MCP_HOME) {
708
1070
  }
709
1071
  return meta;
710
1072
  }
711
- function base64url(buf) {
1073
+ function base64url2(buf) {
712
1074
  return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
713
1075
  }
714
- function pkcePair() {
715
- const verifier = base64url(randomBytes(32));
716
- const challenge = base64url(createHash("sha256").update(verifier).digest());
1076
+ function pkcePair2() {
1077
+ const verifier = base64url2(randomBytes2(32));
1078
+ const challenge = base64url2(createHash2("sha256").update(verifier).digest());
717
1079
  return { verifier, challenge };
718
1080
  }
719
1081
  function sameState(a, b) {
@@ -730,7 +1092,7 @@ async function registerClient(meta, redirectUri) {
730
1092
  method: "POST",
731
1093
  headers: { "Content-Type": "application/json", Accept: "application/json" },
732
1094
  body: JSON.stringify({
733
- client_name: CLIENT_NAME,
1095
+ client_name: CLIENT_NAME2,
734
1096
  redirect_uris: [redirectUri],
735
1097
  grant_types: ["authorization_code", "refresh_token"],
736
1098
  response_types: ["code"],
@@ -761,7 +1123,7 @@ async function startCallbackServer() {
761
1123
  let resolveCode;
762
1124
  let rejectCode;
763
1125
  let expected = "";
764
- const server = createServer((req, res) => {
1126
+ const server = createServer2((req, res) => {
765
1127
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
766
1128
  if (url.pathname !== "/callback") {
767
1129
  res.writeHead(404).end();
@@ -868,8 +1230,8 @@ async function login(opts) {
868
1230
  try {
869
1231
  const existing = await readCredentials();
870
1232
  const clientId = existing?.clientId && existing.issuer === meta.issuer ? existing.clientId : await registerClient(meta, server.redirectUri);
871
- const { verifier, challenge } = pkcePair();
872
- const state = base64url(randomBytes(16));
1233
+ const { verifier, challenge } = pkcePair2();
1234
+ const state = base64url2(randomBytes2(16));
873
1235
  const authUrl = new URL(meta.authorization_endpoint);
874
1236
  authUrl.searchParams.set("response_type", "code");
875
1237
  authUrl.searchParams.set("client_id", clientId);
@@ -1671,12 +2033,12 @@ var CLI_DOMAINS = [
1671
2033
  {
1672
2034
  name: "sim-status set",
1673
2035
  tool: "change_sim_status",
1674
- summary: "Change the SIM/eSIM status at the SIM provider level. DELETED is irreversible.",
2036
+ summary: "Change the SIM/eSIM status at the SIM provider level. ENABLED and DISABLED map to OCS Affected and Free.",
1675
2037
  options: [
1676
2038
  ICCID,
1677
2039
  {
1678
2040
  flags: "--status <status>",
1679
- description: "New SIM status: ENABLED, DISABLED or DELETED (irreversible)",
2041
+ description: "New SIM status: ENABLED or DISABLED",
1680
2042
  arg: "simStatus",
1681
2043
  type: "string",
1682
2044
  required: true
@@ -1776,12 +2138,12 @@ var CLI_DOMAINS = [
1776
2138
  {
1777
2139
  name: "bitrate set",
1778
2140
  tool: "hlr_set_bitrate",
1779
- summary: "Set an HLR-level bandwidth cap. Pass a numeric bps value or an OCS level.",
2141
+ summary: "Set an HLR-level bandwidth cap. Bits per second are mapped onto the OCS KB_ enum.",
1780
2142
  options: [
1781
2143
  ICCID,
1782
2144
  {
1783
2145
  flags: "--bitrate <bps>",
1784
- description: "Max bitrate in bits per second (0 removes the limit)",
2146
+ description: "Max bitrate in bits per second. 256000 is KB_256. 0 is UNLIMITED.",
1785
2147
  arg: "bitrate",
1786
2148
  type: "number"
1787
2149
  },
@@ -2052,7 +2414,7 @@ var CLI_DOMAINS = [
2052
2414
  {
2053
2415
  name: "set-core",
2054
2416
  tool: "modify_template_core",
2055
- summary: "Change name, limits, price, validity or zone on a template.",
2417
+ summary: "Change name, limits, cost, validity or zone on a template.",
2056
2418
  options: [
2057
2419
  TEMPLATE_ID,
2058
2420
  {
@@ -2073,7 +2435,7 @@ var CLI_DOMAINS = [
2073
2435
  TEMPLATE_ID,
2074
2436
  {
2075
2437
  flags: "--changes <json>",
2076
- description: `Recurring fields as JSON, e.g. '{"periodicity":"monthly"}'`,
2438
+ description: `Recurring fields as JSON, e.g. '{"recurringPeriodicityType":2,"nbOccurrence":12}'`,
2077
2439
  arg: "changes",
2078
2440
  type: "string",
2079
2441
  required: true
@@ -2138,7 +2500,7 @@ var CLI_DOMAINS = [
2138
2500
  options: [
2139
2501
  {
2140
2502
  flags: "--zone <json>",
2141
- description: `Zone config as JSON, e.g. '{"name":"Europe","countries":["NL","DE"]}'`,
2503
+ description: `Zone config as JSON, e.g. '{"locationZoneName":"Europe","tadigList":["AZER1","QWER1"],"networkProfileId":79}'`,
2142
2504
  arg: "zone",
2143
2505
  type: "string",
2144
2506
  required: true
@@ -5177,6 +5539,65 @@ site.command("clerk [dir]").description("Create or discover a Clerk instance and
5177
5539
  p3.outro("");
5178
5540
  }
5179
5541
  );
5542
+ site.command("email [dir]").description("Connect Resend in the browser so the storefront can email QR codes.").option("--no-browser", "Never open a browser; print the manual steps instead").option("--yes", "Non-interactive: skip rather than wait for a human").option("--timeout <seconds>", "How long to wait for the browser approval", "180").action(
5543
+ async (dir, o) => {
5544
+ header();
5545
+ const storefront = resolve2(dir ?? "./storefront");
5546
+ const interactive = process.stdin.isTTY === true && o.yes !== true;
5547
+ try {
5548
+ const brand = await loadStorefrontBrand(storefront);
5549
+ const result = await provisionResend({
5550
+ name: brand.name,
5551
+ domain: brand.domain,
5552
+ env: process.env,
5553
+ noBrowser: o.browser === false || !interactive,
5554
+ timeoutMs: Math.max(10, Number(o.timeout) || 180) * 1e3,
5555
+ deps: {
5556
+ fetch,
5557
+ openBrowser: (url) => openUrl(url),
5558
+ startLoopback: () => startLoopbackServer(),
5559
+ notify: (message) => info(pc4.dim(message)),
5560
+ promptForKey: interactive ? async (url) => {
5561
+ const value = await p3.text({
5562
+ message: `Paste the Resend API key from ${url}`,
5563
+ placeholder: "re_\u2026"
5564
+ });
5565
+ return p3.isCancel(value) ? null : value;
5566
+ } : void 0
5567
+ }
5568
+ });
5569
+ if (!result.ok || !result.credentials) {
5570
+ if (result.reason) info(pc4.yellow(result.reason));
5571
+ p3.note((result.guidance ?? []).join("\n"), "Set up email delivery");
5572
+ if (interactive) process.exitCode = 1;
5573
+ p3.outro("");
5574
+ return;
5575
+ }
5576
+ const written = await mergeEnvLocal(storefront, {
5577
+ RESEND_API_KEY: result.credentials.apiKey
5578
+ });
5579
+ info(
5580
+ result.tier === "oauth" ? pc4.green(
5581
+ `Created a send-only Resend key on your account${result.credentials.domain ? `, restricted to ${result.credentials.domain}` : ""}`
5582
+ ) : result.tier === "discovered" ? pc4.green("Reused the Resend key already on this machine") : pc4.green("Saved the Resend key you pasted")
5583
+ );
5584
+ if (written.length) info(pc4.dim("Wrote RESEND_API_KEY to .env.local"));
5585
+ else info(pc4.yellow("RESEND_API_KEY already has a value in .env.local \u2014 left it alone."));
5586
+ if (!result.credentials.domain) {
5587
+ info(
5588
+ pc4.dim(
5589
+ `Mail will be sent from ${brand.emailFromMailbox}@${brand.domain}. Verify that domain at ${RESEND_DOMAINS_URL} or it will not be delivered.`
5590
+ )
5591
+ );
5592
+ }
5593
+ } catch (e) {
5594
+ fail(String(e instanceof Error ? e.message : e), ["Scaffold first: carrier site create"]);
5595
+ process.exitCode = 1;
5596
+ return;
5597
+ }
5598
+ p3.outro("");
5599
+ }
5600
+ );
5180
5601
  site.command("logo [dir]").description("Generate a storefront logo (SVG always; PNG if an image key is on this box or Carrier).").action(async (dir) => {
5181
5602
  header();
5182
5603
  const target = resolve2(dir ?? "./storefront");