@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.
- package/dist/{chunk-EHGZRVSO.js → chunk-ZUUC3J4J.js} +26 -3
- package/dist/chunk-ZUUC3J4J.js.map +1 -0
- package/dist/cli.js +397 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/plugin/carrier/agents/carrier-storefront-builder.md +3 -3
- package/plugin/carrier/skills/storefront/SKILL.md +2 -2
- package/templates/storefront/src/app/activate/[orderId]/ActivateClient.tsx +205 -17
- package/templates/storefront/src/app/activate/[orderId]/page.tsx +1 -1
- package/templates/storefront/src/app/api/checkout/guest/route.ts +72 -9
- package/templates/storefront/src/app/api/orders/[orderId]/cancel/route.ts +33 -0
- package/templates/storefront/src/app/api/orders/[orderId]/email/route.ts +73 -0
- package/templates/storefront/src/app/api/orders/[orderId]/top-up-options/route.ts +28 -0
- package/templates/storefront/src/app/api/orders/[orderId]/usage/route.ts +25 -0
- package/templates/storefront/src/app/api/profile/phone/route.ts +1 -1
- package/templates/storefront/src/app/checkout/[templateId]/CheckoutClient.tsx +142 -39
- package/templates/storefront/src/app/checkout/success/CheckoutSuccessClient.tsx +1 -1
- package/templates/storefront/src/app/checkout/success/page.tsx +1 -1
- package/templates/storefront/src/app/dashboard/EsimControls.tsx +293 -0
- package/templates/storefront/src/app/dashboard/page.tsx +12 -1
- package/templates/storefront/src/app/shop/ShopClient.tsx +126 -5
- package/templates/storefront/src/app/sign-up/[[...sign-up]]/StorefrontSignUpClient.tsx +1 -1
- package/templates/storefront/src/app/sign-up/[[...sign-up]]/page.tsx +1 -1
- package/templates/storefront/src/brand.config.ts +19 -0
- package/templates/storefront/src/components/checkout/EmbeddedStripeCheckout.tsx +191 -0
- package/templates/storefront/src/components/landing/PlanCard.tsx +12 -4
- package/templates/storefront/src/lib/checkout-order-claim.ts +113 -8
- package/templates/storefront/src/lib/esim-install.ts +74 -0
- package/templates/storefront/src/lib/order-route-guard.ts +85 -0
- package/templates/storefront/src/lib/qr-delivery-email.ts +106 -0
- package/templates/storefront/src/middleware.ts +1 -1
- package/templates/storefront/src/vendor/carrier/client.ts +86 -1
- package/templates/storefront/src/vendor/carrier/types.ts +34 -0
- package/templates/storefront/src/vendor/email/qr-delivery.ts +254 -0
- package/templates/storefront/src/vendor/email/resend.ts +140 -0
- package/templates/storefront/src/vendor/geo/index.ts +45 -0
- package/templates/storefront/src/vendor/ui/brand.ts +8 -1
- 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-
|
|
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
|
-
|
|
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
|
|
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
|
|
1025
|
+
function base64url2(buf) {
|
|
710
1026
|
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
711
1027
|
}
|
|
712
|
-
function
|
|
713
|
-
const verifier =
|
|
714
|
-
const challenge =
|
|
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:
|
|
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 =
|
|
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 } =
|
|
870
|
-
const state =
|
|
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
|
|
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");
|