@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/README.md +4 -3
- package/dist/{chunk-J52VP7E7.js → chunk-2TVU2GRC.js} +1541 -1131
- package/dist/chunk-2TVU2GRC.js.map +1 -0
- package/dist/cli.js +445 -24
- package/dist/cli.js.map +1 -1
- package/dist/index.js +95 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/plugin/carrier/README.md +2 -1
- package/plugin/carrier/skills/issue-esim/SKILL.md +92 -0
- package/plugin/carrier/skills/provision/SKILL.md +2 -0
- package/templates/storefront/src/app/activate/[orderId]/ActivateClient.tsx +79 -22
- package/templates/storefront/src/app/api/orders/[orderId]/email/route.ts +73 -0
- package/templates/storefront/src/lib/checkout-order-claim.ts +45 -4
- package/templates/storefront/src/lib/esim-install.ts +74 -0
- package/templates/storefront/src/lib/qr-delivery-email.ts +106 -0
- package/templates/storefront/src/vendor/email/qr-delivery.ts +254 -0
- package/templates/storefront/src/vendor/email/resend.ts +140 -0
- package/dist/chunk-J52VP7E7.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-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
|
|
436
|
-
else if (
|
|
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
|
|
810
|
+
if (rank < 20 && !isCarrierMcpHost(url)) {
|
|
449
811
|
continue;
|
|
450
812
|
}
|
|
451
|
-
const urlMatches = url
|
|
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 =
|
|
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
|
|
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
|
|
1073
|
+
function base64url2(buf) {
|
|
712
1074
|
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
713
1075
|
}
|
|
714
|
-
function
|
|
715
|
-
const verifier =
|
|
716
|
-
const challenge =
|
|
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:
|
|
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 =
|
|
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 } =
|
|
872
|
-
const state =
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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,
|
|
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. '{"
|
|
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. '{"
|
|
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");
|