@carrierllc/mcp 0.11.17 → 0.13.0

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.
Files changed (39) hide show
  1. package/dist/{chunk-EHGZRVSO.js → chunk-ZUUC3J4J.js} +26 -3
  2. package/dist/chunk-ZUUC3J4J.js.map +1 -0
  3. package/dist/cli.js +397 -16
  4. package/dist/cli.js.map +1 -1
  5. package/dist/index.js +1 -1
  6. package/package.json +1 -1
  7. package/plugin/carrier/agents/carrier-storefront-builder.md +3 -3
  8. package/plugin/carrier/skills/storefront/SKILL.md +2 -2
  9. package/templates/storefront/src/app/activate/[orderId]/ActivateClient.tsx +205 -17
  10. package/templates/storefront/src/app/activate/[orderId]/page.tsx +1 -1
  11. package/templates/storefront/src/app/api/checkout/guest/route.ts +72 -9
  12. package/templates/storefront/src/app/api/orders/[orderId]/cancel/route.ts +33 -0
  13. package/templates/storefront/src/app/api/orders/[orderId]/email/route.ts +73 -0
  14. package/templates/storefront/src/app/api/orders/[orderId]/top-up-options/route.ts +28 -0
  15. package/templates/storefront/src/app/api/orders/[orderId]/usage/route.ts +25 -0
  16. package/templates/storefront/src/app/api/profile/phone/route.ts +1 -1
  17. package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +142 -39
  18. package/templates/storefront/src/app/checkout/success/CheckoutSuccessClient.tsx +1 -1
  19. package/templates/storefront/src/app/checkout/success/page.tsx +1 -1
  20. package/templates/storefront/src/app/dashboard/EsimControls.tsx +293 -0
  21. package/templates/storefront/src/app/dashboard/page.tsx +12 -1
  22. package/templates/storefront/src/app/shop/ShopClient.tsx +126 -5
  23. package/templates/storefront/src/app/sign-up/[[...sign-up]]/StorefrontSignUpClient.tsx +1 -1
  24. package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +1 -1
  25. package/templates/storefront/src/brand.config.ts +19 -0
  26. package/templates/storefront/src/components/checkout/EmbeddedStripeCheckout.tsx +191 -0
  27. package/templates/storefront/src/components/landing/PlanCard.tsx +12 -4
  28. package/templates/storefront/src/lib/checkout-order-claim.ts +113 -8
  29. package/templates/storefront/src/lib/esim-install.ts +74 -0
  30. package/templates/storefront/src/lib/order-route-guard.ts +85 -0
  31. package/templates/storefront/src/lib/qr-delivery-email.ts +106 -0
  32. package/templates/storefront/src/middleware.ts +1 -1
  33. package/templates/storefront/src/vendor/carrier/client.ts +86 -1
  34. package/templates/storefront/src/vendor/carrier/types.ts +34 -0
  35. package/templates/storefront/src/vendor/email/qr-delivery.ts +254 -0
  36. package/templates/storefront/src/vendor/email/resend.ts +140 -0
  37. package/templates/storefront/src/vendor/geo/index.ts +45 -0
  38. package/templates/storefront/src/vendor/ui/brand.ts +8 -1
  39. package/dist/chunk-EHGZRVSO.js.map +0 -1
package/dist/cli.js CHANGED
@@ -39,7 +39,7 @@ import {
39
39
  verifyStorefront,
40
40
  walletScreen,
41
41
  which
42
- } from "./chunk-EHGZRVSO.js";
42
+ } from "./chunk-ZUUC3J4J.js";
43
43
  import {
44
44
  copyTree,
45
45
  exists,
@@ -210,10 +210,8 @@ async function scaffoldStorefront(target, brand) {
210
210
  await copyTree(template, target);
211
211
  await pruneTree(template, target);
212
212
  const subs = [
213
- ["Mango", brand.name],
214
- ["\u{1F96D}", ""]
213
+ ...deriveAccentPaletteSubs(brand.colors.accent, brand.colors.accentDark)
215
214
  ];
216
- subs.push(...deriveAccentPaletteSubs(brand.colors.accent, brand.colors.accentDark));
217
215
  if (brand.colors.bg.toUpperCase() !== "#080C16") subs.push(["#080C16", brand.colors.bg]);
218
216
  if (brand.colors.text.toUpperCase() !== "#F5F1EA") subs.push(["#F5F1EA", brand.colors.text]);
219
217
  await replaceInTree(target, subs);
@@ -236,6 +234,10 @@ async function patchBrandConfig(path, brand) {
236
234
  setField("supportUrl", brand.supportUrl);
237
235
  setField("supportEmail", brand.supportEmail);
238
236
  setField("supportWhatsapp", brand.supportWhatsapp);
237
+ setField("description", brand.description);
238
+ setField("emailFromName", brand.emailFromName);
239
+ setField("emailFromMailbox", brand.emailFromMailbox);
240
+ setField("appUrl", brand.appUrl);
239
241
  setField("accent", brand.colors.accent);
240
242
  setField("accentDark", brand.colors.accentDark);
241
243
  setField("accentLight", deriveAccentLight(brand.colors.accent));
@@ -270,6 +272,320 @@ async function writeStorefrontLogo(target, brand) {
270
272
  return { path: svgPath, source: logo.source };
271
273
  }
272
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
+
273
589
  // src/cli/lib/dash.ts
274
590
  var DASH_DOMAINS = [
275
591
  "fleet",
@@ -647,8 +963,8 @@ import * as p from "@clack/prompts";
647
963
  import pc2 from "picocolors";
648
964
 
649
965
  // src/cli/lib/auth.ts
650
- import { createHash, randomBytes, timingSafeEqual } from "crypto";
651
- import { createServer } from "http";
966
+ import { createHash as createHash2, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
967
+ import { createServer as createServer2 } from "http";
652
968
 
653
969
  // src/cli/lib/credentials.ts
654
970
  import { homedir as homedir3 } from "os";
@@ -692,7 +1008,7 @@ function redact(text3) {
692
1008
 
693
1009
  // src/cli/lib/auth.ts
694
1010
  var DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
695
- var CLIENT_NAME = "Carrier CLI";
1011
+ var CLIENT_NAME2 = "Carrier CLI";
696
1012
  var DEFAULT_SCOPE = "read write";
697
1013
  var CALLBACK_PORTS = [3118, 3119, 8976, 8977, 0];
698
1014
  async function discover(base = MCP_HOME) {
@@ -706,12 +1022,12 @@ async function discover(base = MCP_HOME) {
706
1022
  }
707
1023
  return meta;
708
1024
  }
709
- function base64url(buf) {
1025
+ function base64url2(buf) {
710
1026
  return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
711
1027
  }
712
- function pkcePair() {
713
- const verifier = base64url(randomBytes(32));
714
- const challenge = base64url(createHash("sha256").update(verifier).digest());
1028
+ function pkcePair2() {
1029
+ const verifier = base64url2(randomBytes2(32));
1030
+ const challenge = base64url2(createHash2("sha256").update(verifier).digest());
715
1031
  return { verifier, challenge };
716
1032
  }
717
1033
  function sameState(a, b) {
@@ -728,7 +1044,7 @@ async function registerClient(meta, redirectUri) {
728
1044
  method: "POST",
729
1045
  headers: { "Content-Type": "application/json", Accept: "application/json" },
730
1046
  body: JSON.stringify({
731
- client_name: CLIENT_NAME,
1047
+ client_name: CLIENT_NAME2,
732
1048
  redirect_uris: [redirectUri],
733
1049
  grant_types: ["authorization_code", "refresh_token"],
734
1050
  response_types: ["code"],
@@ -759,7 +1075,7 @@ async function startCallbackServer() {
759
1075
  let resolveCode;
760
1076
  let rejectCode;
761
1077
  let expected = "";
762
- const server = createServer((req, res) => {
1078
+ const server = createServer2((req, res) => {
763
1079
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
764
1080
  if (url.pathname !== "/callback") {
765
1081
  res.writeHead(404).end();
@@ -866,8 +1182,8 @@ async function login(opts) {
866
1182
  try {
867
1183
  const existing = await readCredentials();
868
1184
  const clientId = existing?.clientId && existing.issuer === meta.issuer ? existing.clientId : await registerClient(meta, server.redirectUri);
869
- const { verifier, challenge } = pkcePair();
870
- const state = base64url(randomBytes(16));
1185
+ const { verifier, challenge } = pkcePair2();
1186
+ const state = base64url2(randomBytes2(16));
871
1187
  const authUrl = new URL(meta.authorization_endpoint);
872
1188
  authUrl.searchParams.set("response_type", "code");
873
1189
  authUrl.searchParams.set("client_id", clientId);
@@ -4266,10 +4582,13 @@ async function promptBrand(seed) {
4266
4582
  name,
4267
4583
  legalName: name,
4268
4584
  tagline: isCarrier ? seed.tagline : `Mobile connectivity by ${name}.`,
4585
+ description: isCarrier ? seed.description : `Mobile data from ${name}. Instant QR activation, no contract.`,
4269
4586
  domain,
4270
4587
  supportEmail,
4271
4588
  supportUrl: `https://${domain}/help`,
4272
4589
  supportWhatsapp: isCarrier ? seed.supportWhatsapp : "",
4590
+ emailFromName: name,
4591
+ appUrl: `https://${domain}`,
4273
4592
  social: isCarrier ? seed.social : {},
4274
4593
  colors: { ...seed.colors, accent, accentDark }
4275
4594
  };
@@ -5032,7 +5351,7 @@ program.command("dash [domain]").description(`Render a status screen in the term
5032
5351
  );
5033
5352
  });
5034
5353
  var site = program.command("site").description("Scaffold and deploy a white-labeled storefront.");
5035
- site.command("create [dir]").description("Scaffold a white-labeled storefront from the Mango template.").option("--name <name>", "Brand name").option("--domain <domain>", "Domain").option("--accent <hex>", "Accent color").option("--yes", "Carrier defaults, no prompts").action(async (dir, o) => {
5354
+ site.command("create [dir]").description("Scaffold a white-labeled storefront from the bundled template.").option("--name <name>", "Brand name").option("--domain <domain>", "Domain").option("--accent <hex>", "Accent color").option("--yes", "Carrier defaults, no prompts").action(async (dir, o) => {
5036
5355
  header();
5037
5356
  let brand = { ...CARRIER_BRAND };
5038
5357
  if (o.name || o.domain || o.accent) {
@@ -5045,9 +5364,12 @@ site.command("create [dir]").description("Scaffold a white-labeled storefront fr
5045
5364
  domain,
5046
5365
  legalName: o.name ?? brand.legalName,
5047
5366
  tagline: o.name ? `Mobile connectivity by ${o.name}.` : brand.tagline,
5367
+ description: o.name ? `Mobile data from ${o.name}. Instant QR activation, no contract.` : brand.description,
5048
5368
  supportEmail: `support@${domain}`,
5049
5369
  supportUrl: `https://${domain}/help`,
5050
5370
  supportWhatsapp: isCarrier ? brand.supportWhatsapp : "",
5371
+ emailFromName: name,
5372
+ appUrl: `https://${domain}`,
5051
5373
  social: isCarrier ? brand.social : {},
5052
5374
  colors: {
5053
5375
  ...brand.colors,
@@ -5169,6 +5491,65 @@ site.command("clerk [dir]").description("Create or discover a Clerk instance and
5169
5491
  p3.outro("");
5170
5492
  }
5171
5493
  );
5494
+ 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(
5495
+ async (dir, o) => {
5496
+ header();
5497
+ const storefront = resolve2(dir ?? "./storefront");
5498
+ const interactive = process.stdin.isTTY === true && o.yes !== true;
5499
+ try {
5500
+ const brand = await loadStorefrontBrand(storefront);
5501
+ const result = await provisionResend({
5502
+ name: brand.name,
5503
+ domain: brand.domain,
5504
+ env: process.env,
5505
+ noBrowser: o.browser === false || !interactive,
5506
+ timeoutMs: Math.max(10, Number(o.timeout) || 180) * 1e3,
5507
+ deps: {
5508
+ fetch,
5509
+ openBrowser: (url) => openUrl(url),
5510
+ startLoopback: () => startLoopbackServer(),
5511
+ notify: (message) => info(pc4.dim(message)),
5512
+ promptForKey: interactive ? async (url) => {
5513
+ const value = await p3.text({
5514
+ message: `Paste the Resend API key from ${url}`,
5515
+ placeholder: "re_\u2026"
5516
+ });
5517
+ return p3.isCancel(value) ? null : value;
5518
+ } : void 0
5519
+ }
5520
+ });
5521
+ if (!result.ok || !result.credentials) {
5522
+ if (result.reason) info(pc4.yellow(result.reason));
5523
+ p3.note((result.guidance ?? []).join("\n"), "Set up email delivery");
5524
+ if (interactive) process.exitCode = 1;
5525
+ p3.outro("");
5526
+ return;
5527
+ }
5528
+ const written = await mergeEnvLocal(storefront, {
5529
+ RESEND_API_KEY: result.credentials.apiKey
5530
+ });
5531
+ info(
5532
+ result.tier === "oauth" ? pc4.green(
5533
+ `Created a send-only Resend key on your account${result.credentials.domain ? `, restricted to ${result.credentials.domain}` : ""}`
5534
+ ) : result.tier === "discovered" ? pc4.green("Reused the Resend key already on this machine") : pc4.green("Saved the Resend key you pasted")
5535
+ );
5536
+ if (written.length) info(pc4.dim("Wrote RESEND_API_KEY to .env.local"));
5537
+ else info(pc4.yellow("RESEND_API_KEY already has a value in .env.local \u2014 left it alone."));
5538
+ if (!result.credentials.domain) {
5539
+ info(
5540
+ pc4.dim(
5541
+ `Mail will be sent from ${brand.emailFromMailbox}@${brand.domain}. Verify that domain at ${RESEND_DOMAINS_URL} or it will not be delivered.`
5542
+ )
5543
+ );
5544
+ }
5545
+ } catch (e) {
5546
+ fail(String(e instanceof Error ? e.message : e), ["Scaffold first: carrier site create"]);
5547
+ process.exitCode = 1;
5548
+ return;
5549
+ }
5550
+ p3.outro("");
5551
+ }
5552
+ );
5172
5553
  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) => {
5173
5554
  header();
5174
5555
  const target = resolve2(dir ?? "./storefront");