@carrierllc/mcp 0.12.0 → 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.
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-ZUUC3J4J.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",
@@ -649,8 +963,8 @@ import * as p from "@clack/prompts";
649
963
  import pc2 from "picocolors";
650
964
 
651
965
  // src/cli/lib/auth.ts
652
- import { createHash, randomBytes, timingSafeEqual } from "crypto";
653
- import { createServer } from "http";
966
+ import { createHash as createHash2, randomBytes as randomBytes2, timingSafeEqual } from "crypto";
967
+ import { createServer as createServer2 } from "http";
654
968
 
655
969
  // src/cli/lib/credentials.ts
656
970
  import { homedir as homedir3 } from "os";
@@ -694,7 +1008,7 @@ function redact(text3) {
694
1008
 
695
1009
  // src/cli/lib/auth.ts
696
1010
  var DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
697
- var CLIENT_NAME = "Carrier CLI";
1011
+ var CLIENT_NAME2 = "Carrier CLI";
698
1012
  var DEFAULT_SCOPE = "read write";
699
1013
  var CALLBACK_PORTS = [3118, 3119, 8976, 8977, 0];
700
1014
  async function discover(base = MCP_HOME) {
@@ -708,12 +1022,12 @@ async function discover(base = MCP_HOME) {
708
1022
  }
709
1023
  return meta;
710
1024
  }
711
- function base64url(buf) {
1025
+ function base64url2(buf) {
712
1026
  return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
713
1027
  }
714
- function pkcePair() {
715
- const verifier = base64url(randomBytes(32));
716
- 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());
717
1031
  return { verifier, challenge };
718
1032
  }
719
1033
  function sameState(a, b) {
@@ -730,7 +1044,7 @@ async function registerClient(meta, redirectUri) {
730
1044
  method: "POST",
731
1045
  headers: { "Content-Type": "application/json", Accept: "application/json" },
732
1046
  body: JSON.stringify({
733
- client_name: CLIENT_NAME,
1047
+ client_name: CLIENT_NAME2,
734
1048
  redirect_uris: [redirectUri],
735
1049
  grant_types: ["authorization_code", "refresh_token"],
736
1050
  response_types: ["code"],
@@ -761,7 +1075,7 @@ async function startCallbackServer() {
761
1075
  let resolveCode;
762
1076
  let rejectCode;
763
1077
  let expected = "";
764
- const server = createServer((req, res) => {
1078
+ const server = createServer2((req, res) => {
765
1079
  const url = new URL(req.url ?? "/", "http://127.0.0.1");
766
1080
  if (url.pathname !== "/callback") {
767
1081
  res.writeHead(404).end();
@@ -868,8 +1182,8 @@ async function login(opts) {
868
1182
  try {
869
1183
  const existing = await readCredentials();
870
1184
  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));
1185
+ const { verifier, challenge } = pkcePair2();
1186
+ const state = base64url2(randomBytes2(16));
873
1187
  const authUrl = new URL(meta.authorization_endpoint);
874
1188
  authUrl.searchParams.set("response_type", "code");
875
1189
  authUrl.searchParams.set("client_id", clientId);
@@ -5177,6 +5491,65 @@ site.command("clerk [dir]").description("Create or discover a Clerk instance and
5177
5491
  p3.outro("");
5178
5492
  }
5179
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
+ );
5180
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) => {
5181
5554
  header();
5182
5555
  const target = resolve2(dir ?? "./storefront");