@carrierllc/mcp 0.13.7 → 0.13.9
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-U2UVUM3D.js → chunk-DF2PGR5H.js} +8 -2
- package/dist/chunk-DF2PGR5H.js.map +1 -0
- package/dist/cli.js +1565 -1095
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +2 -1
- package/plugin/carrier/skills/publish-storefront/SKILL.md +114 -0
- package/templates/storefront/src/vendor/carrier/client.ts +9 -1
- package/dist/chunk-U2UVUM3D.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-DF2PGR5H.js";
|
|
43
43
|
import {
|
|
44
44
|
copyTree,
|
|
45
45
|
exists,
|
|
@@ -272,465 +272,615 @@ async function writeStorefrontLogo(target, brand) {
|
|
|
272
272
|
return { path: svgPath, source: logo.source };
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
-
// src/cli/lib/
|
|
276
|
-
import {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
var
|
|
280
|
-
var
|
|
281
|
-
var
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
275
|
+
// src/cli/lib/hosted-publish.ts
|
|
276
|
+
import { readFileSync } from "fs";
|
|
277
|
+
|
|
278
|
+
// ../../packages/sdk/src/storefront-labels.ts
|
|
279
|
+
var regionNames = new Intl.DisplayNames(["en"], { type: "region" });
|
|
280
|
+
var PREFERRED = /* @__PURE__ */ new Set(["CW", "BF", "BJ", "MM", "CD", "RS", "DE", "FR", "GB", "VU", "ZW", "RU", "TL", "VN", "YE"]);
|
|
281
|
+
var NAME_TO_CODE = /* @__PURE__ */ new Map();
|
|
282
|
+
for (let first = 65; first <= 90; first += 1) {
|
|
283
|
+
for (let second = 65; second <= 90; second += 1) {
|
|
284
|
+
const code = String.fromCharCode(first, second);
|
|
285
|
+
const label = regionLabel(code);
|
|
286
|
+
if (!label) continue;
|
|
287
|
+
const key = label.toLowerCase();
|
|
288
|
+
const current = NAME_TO_CODE.get(key);
|
|
289
|
+
if (!current || PREFERRED.has(code)) NAME_TO_CODE.set(key, code);
|
|
290
|
+
}
|
|
288
291
|
}
|
|
289
|
-
|
|
290
|
-
|
|
292
|
+
NAME_TO_CODE.set("uk", "GB");
|
|
293
|
+
var CODE_ALIASES = { UK: "GB" };
|
|
294
|
+
function parseDataGb(raw) {
|
|
295
|
+
if (raw.trim().toLowerCase() === "unlimited") return { ok: true, value: 0 };
|
|
296
|
+
return parseAmount(raw, /gigabytes?|gbs?|gb/gi, "Enter data like 1, 1 GB, or Unlimited.", 1e4, false);
|
|
291
297
|
}
|
|
292
|
-
function
|
|
293
|
-
|
|
294
|
-
return { verifier, challenge: base64url(createHash("sha256").update(verifier).digest()) };
|
|
298
|
+
function parseValidityDays(raw) {
|
|
299
|
+
return parseAmount(raw, /days?/gi, "Enter validity like 7 or 7 days.", 3650, true);
|
|
295
300
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
301
|
+
var ZERO_DECIMAL = /* @__PURE__ */ new Set([
|
|
302
|
+
"BIF",
|
|
303
|
+
"CLP",
|
|
304
|
+
"DJF",
|
|
305
|
+
"GNF",
|
|
306
|
+
"JPY",
|
|
307
|
+
"KMF",
|
|
308
|
+
"KRW",
|
|
309
|
+
"MGA",
|
|
310
|
+
"PYG",
|
|
311
|
+
"RWF",
|
|
312
|
+
"UGX",
|
|
313
|
+
"VND",
|
|
314
|
+
"VUV",
|
|
315
|
+
"XAF",
|
|
316
|
+
"XOF",
|
|
317
|
+
"XPF"
|
|
318
|
+
]);
|
|
319
|
+
function majorToCents(input, currency = "EUR") {
|
|
320
|
+
const text3 = input.trim();
|
|
321
|
+
if (ZERO_DECIMAL.has(currency.trim().toUpperCase())) {
|
|
322
|
+
if (!/^\d+$/.test(text3)) return { ok: false, message: "Enter a whole number, such as 1000." };
|
|
323
|
+
const cents2 = Number(text3);
|
|
324
|
+
if (!Number.isSafeInteger(cents2) || cents2 <= 0) return { ok: false, message: "Enter a price above zero." };
|
|
325
|
+
return { ok: true, cents: cents2 };
|
|
326
|
+
}
|
|
327
|
+
const match = /^(\d+)(?:\.(\d{1,2}))?$/.exec(text3);
|
|
328
|
+
if (!match) return { ok: false, message: "Enter a price like 9.00." };
|
|
329
|
+
const major = Number(match[1]);
|
|
330
|
+
const fraction = (match[2] ?? "").padEnd(2, "0");
|
|
331
|
+
const cents = major * 100 + Number(fraction);
|
|
332
|
+
if (!Number.isSafeInteger(cents) || cents <= 0) return { ok: false, message: "Enter a price above zero." };
|
|
333
|
+
return { ok: true, cents };
|
|
306
334
|
}
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
335
|
+
function parseCountryInput(raw) {
|
|
336
|
+
const regions = [];
|
|
337
|
+
const parts = raw.split(",").map((part) => part.trim()).filter((part) => part !== "");
|
|
338
|
+
for (const part of parts) {
|
|
339
|
+
const added = codesFor(part);
|
|
340
|
+
if (!added) return { ok: false, message: "Countries are names, such as France, or codes such as FR." };
|
|
341
|
+
for (const code of added) {
|
|
342
|
+
if (regions.length >= 40) break;
|
|
343
|
+
if (!regions.includes(code)) regions.push(code);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return { ok: true, regions };
|
|
347
|
+
}
|
|
348
|
+
function parseAmount(raw, unit, message, max, whole) {
|
|
349
|
+
const text3 = raw.trim().replace(unit, "").trim();
|
|
350
|
+
if (text3 === "" || !/^\d+(\.\d+)?$/.test(text3)) return { ok: false, message };
|
|
351
|
+
const value = Number(text3);
|
|
352
|
+
if (!Number.isFinite(value) || value < 0 || value > max) return { ok: false, message };
|
|
353
|
+
if (whole && !Number.isInteger(value)) return { ok: false, message };
|
|
354
|
+
return { ok: true, value };
|
|
355
|
+
}
|
|
356
|
+
function codesFor(part) {
|
|
357
|
+
const tokens = part.split(/\s+/).filter((token) => token !== "");
|
|
358
|
+
if (tokens.length > 1 && tokens.every((token) => /^[A-Za-z]{2}$/.test(token))) {
|
|
359
|
+
return tokens.map((token) => canonical(token));
|
|
360
|
+
}
|
|
361
|
+
if (/^[A-Za-z]{2}$/.test(part)) return [canonical(part)];
|
|
362
|
+
const code = NAME_TO_CODE.get(part.toLowerCase());
|
|
363
|
+
return code ? [code] : null;
|
|
364
|
+
}
|
|
365
|
+
function canonical(code) {
|
|
366
|
+
const upper = code.toUpperCase();
|
|
367
|
+
return CODE_ALIASES[upper] ?? upper;
|
|
368
|
+
}
|
|
369
|
+
var LOGO_DATA = /^data:image\/(png|jpeg|webp|gif);base64,[A-Za-z0-9+/]+={0,2}$/;
|
|
370
|
+
var LOGO_MAX_CHARS = 1e5;
|
|
371
|
+
var LOGO_MAX_BYTES = 70 * 1024;
|
|
372
|
+
function logoFileDataUrl(bytes) {
|
|
373
|
+
const mime = logoMime(bytes);
|
|
374
|
+
if (!mime || bytes.byteLength < 1 || bytes.byteLength > LOGO_MAX_BYTES) {
|
|
375
|
+
return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
|
|
376
|
+
}
|
|
377
|
+
const url = `data:${mime};base64,${toBase64(bytes)}`;
|
|
378
|
+
if (url.length > LOGO_MAX_CHARS || !LOGO_DATA.test(url)) {
|
|
379
|
+
return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
|
|
380
|
+
}
|
|
381
|
+
return { ok: true, url };
|
|
382
|
+
}
|
|
383
|
+
function logoReference(value) {
|
|
384
|
+
const trimmed = value.trim();
|
|
385
|
+
if (trimmed.startsWith("data:")) {
|
|
386
|
+
if (trimmed.length <= LOGO_MAX_CHARS && LOGO_DATA.test(trimmed)) return { ok: true, url: trimmed };
|
|
387
|
+
return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
|
|
317
388
|
}
|
|
318
389
|
try {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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) };
|
|
390
|
+
const url = new URL(trimmed);
|
|
391
|
+
if (url.protocol === "https:") return { ok: true, url: url.toString() };
|
|
392
|
+
} catch {
|
|
393
|
+
return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
|
|
337
394
|
}
|
|
395
|
+
return { ok: false, message: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
|
|
338
396
|
}
|
|
339
|
-
function
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
return
|
|
397
|
+
function toBase64(bytes) {
|
|
398
|
+
let binary = "";
|
|
399
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
400
|
+
return btoa(binary);
|
|
343
401
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
402
|
+
function logoMime(bytes) {
|
|
403
|
+
if (bytes.length >= 8 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71) {
|
|
404
|
+
return "image/png";
|
|
405
|
+
}
|
|
406
|
+
if (bytes.length >= 3 && bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
|
|
407
|
+
if (bytes.length >= 6 && bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70) return "image/gif";
|
|
408
|
+
if (bytes.length >= 12 && bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) {
|
|
409
|
+
return "image/webp";
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
function regionLabel(code) {
|
|
414
|
+
if (!/^[A-Z]{2}$/.test(code)) return void 0;
|
|
415
|
+
try {
|
|
416
|
+
const label = regionNames.of(code);
|
|
417
|
+
if (!label || label.toUpperCase() === code) return void 0;
|
|
418
|
+
return label;
|
|
419
|
+
} catch {
|
|
420
|
+
return void 0;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// ../../packages/sdk/src/storefront-write.ts
|
|
425
|
+
var THEME_FIELDS = [
|
|
426
|
+
["accent", "accent"],
|
|
427
|
+
["accentDark", "accentDark"],
|
|
428
|
+
["bg", "bg"],
|
|
429
|
+
["text", "text"],
|
|
430
|
+
["logoUrl", "logoUrl"]
|
|
431
|
+
];
|
|
432
|
+
var COPY_FIELDS = [
|
|
433
|
+
["hero", "hero"],
|
|
434
|
+
["tagline", "tagline"],
|
|
435
|
+
["supportEmail", "supportEmail"],
|
|
436
|
+
["legalName", "legalName"]
|
|
437
|
+
];
|
|
438
|
+
function storefrontBrandPatch(input, existing) {
|
|
439
|
+
const theme = overlay(existing?.theme, input, THEME_FIELDS);
|
|
440
|
+
const copy = overlay(existing?.copy, input, COPY_FIELDS);
|
|
441
|
+
return {
|
|
442
|
+
...theme ? { theme } : {},
|
|
443
|
+
...copy ? { copy } : {}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function completePlanInput(input, plans) {
|
|
447
|
+
if ((input.productId?.trim() ?? "") !== "") return input;
|
|
448
|
+
const touched = [
|
|
449
|
+
input.slug,
|
|
450
|
+
input.name,
|
|
451
|
+
input.priceCents,
|
|
452
|
+
input.currency,
|
|
453
|
+
input.templateId,
|
|
454
|
+
input.dataGb,
|
|
455
|
+
input.validityDays,
|
|
456
|
+
input.regions,
|
|
457
|
+
input.description,
|
|
458
|
+
input.active
|
|
459
|
+
].some((value) => value !== void 0);
|
|
460
|
+
if (!touched) return input;
|
|
461
|
+
const slug2 = input.slug?.trim();
|
|
462
|
+
const match = slug2 ? plans.find((plan) => plan.slug === slug2) : plans.length === 1 ? plans[0] : void 0;
|
|
463
|
+
if (!match) {
|
|
464
|
+
const currency = sharedCurrency(plans);
|
|
465
|
+
if (!input.currency && currency) return { ...input, currency };
|
|
466
|
+
return input;
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
...input,
|
|
470
|
+
slug: input.slug ?? match.slug,
|
|
471
|
+
name: input.name ?? match.name,
|
|
472
|
+
currency: input.currency ?? match.currency,
|
|
473
|
+
templateId: input.templateId ?? match.ocs_package_template_id
|
|
474
|
+
};
|
|
475
|
+
}
|
|
476
|
+
function sharedCurrency(plans) {
|
|
477
|
+
const codes = new Set(plans.map((plan) => plan.currency.trim().toUpperCase()).filter((code) => code !== ""));
|
|
478
|
+
if (codes.size !== 1) return void 0;
|
|
479
|
+
return [...codes][0];
|
|
480
|
+
}
|
|
481
|
+
function storefrontPlanBody(input) {
|
|
482
|
+
const productId = input.productId?.trim() ?? "";
|
|
483
|
+
if (productId !== "") {
|
|
484
|
+
const body2 = { product_id: productId };
|
|
485
|
+
if (input.priceCents !== void 0) body2.price_cents = input.priceCents;
|
|
486
|
+
if (input.active !== void 0) body2.active = input.active;
|
|
487
|
+
return body2;
|
|
488
|
+
}
|
|
489
|
+
const touched = [
|
|
490
|
+
input.slug,
|
|
491
|
+
input.name,
|
|
492
|
+
input.priceCents,
|
|
493
|
+
input.currency,
|
|
494
|
+
input.templateId,
|
|
495
|
+
input.dataGb,
|
|
496
|
+
input.validityDays,
|
|
497
|
+
input.regions,
|
|
498
|
+
input.description,
|
|
499
|
+
input.active
|
|
500
|
+
].some((value) => value !== void 0);
|
|
501
|
+
if (!touched) return null;
|
|
502
|
+
if (!input.slug || !input.name || input.priceCents === void 0 || !input.currency || input.templateId === void 0) {
|
|
503
|
+
return { error: "A plan needs a slug, name, price in minor units, currency, and OCS template id." };
|
|
504
|
+
}
|
|
505
|
+
const body = {
|
|
506
|
+
slug: input.slug,
|
|
507
|
+
name: input.name,
|
|
508
|
+
price_cents: input.priceCents,
|
|
509
|
+
currency: input.currency,
|
|
510
|
+
ocs_package_template_id: input.templateId
|
|
511
|
+
};
|
|
512
|
+
if (input.dataGb !== void 0) body.data_gb = input.dataGb;
|
|
513
|
+
if (input.validityDays !== void 0) body.validity_days = input.validityDays;
|
|
514
|
+
if (input.regions !== void 0) body.regions = input.regions;
|
|
515
|
+
if (input.description !== void 0) body.description = input.description;
|
|
516
|
+
if (input.active !== void 0) body.active = input.active;
|
|
517
|
+
return body;
|
|
518
|
+
}
|
|
519
|
+
function isStorefrontPlanError(value) {
|
|
520
|
+
return value !== null && "error" in value;
|
|
521
|
+
}
|
|
522
|
+
function parseWholeNumber(value, label) {
|
|
523
|
+
if (value === void 0) return { ok: true, value: void 0 };
|
|
524
|
+
if (!/^\d+$/.test(value)) return { ok: false, message: `${label} must be a whole number.` };
|
|
525
|
+
return { ok: true, value: Number(value) };
|
|
526
|
+
}
|
|
527
|
+
function overlay(base, input, fields) {
|
|
528
|
+
const patch = {};
|
|
529
|
+
let touched = false;
|
|
530
|
+
for (const [field, jsonKey] of fields) {
|
|
531
|
+
const value = input[field];
|
|
532
|
+
if (typeof value !== "string" || value.trim() === "") continue;
|
|
533
|
+
patch[jsonKey] = value.trim();
|
|
534
|
+
touched = true;
|
|
535
|
+
}
|
|
536
|
+
if (!touched) return void 0;
|
|
537
|
+
return { ...base ?? {}, ...patch };
|
|
538
|
+
}
|
|
539
|
+
var RESERVED_SLUGS = /* @__PURE__ */ new Set(["www", "app", "api", "stores", "checkout", "dashboard", "admin", "mail"]);
|
|
540
|
+
function hostedStorefrontUrl(slug2) {
|
|
541
|
+
return `https://${slug2}.stores.carrier.llc`;
|
|
542
|
+
}
|
|
543
|
+
function storefrontSlugFrom(raw) {
|
|
544
|
+
const folded = raw.trim().toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
545
|
+
const clipped = folded.slice(0, 40).replace(/-+$/g, "");
|
|
546
|
+
if (RESERVED_SLUGS.has(clipped)) return "";
|
|
547
|
+
return /^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/.test(clipped) ? clipped : "";
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/cli/lib/auth.ts
|
|
551
|
+
import { createHash, randomBytes, timingSafeEqual } from "crypto";
|
|
552
|
+
import { createServer } from "http";
|
|
553
|
+
|
|
554
|
+
// src/cli/lib/credentials.ts
|
|
555
|
+
import { homedir as homedir2 } from "os";
|
|
556
|
+
import { join as join5 } from "path";
|
|
557
|
+
import { chmod, mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile3 } from "fs/promises";
|
|
558
|
+
function carrierHome() {
|
|
559
|
+
return process.env.CARRIER_HOME?.trim() || join5(homedir2(), ".carrier");
|
|
560
|
+
}
|
|
561
|
+
function credentialsPath() {
|
|
562
|
+
return join5(carrierHome(), "credentials.json");
|
|
563
|
+
}
|
|
564
|
+
async function readCredentials() {
|
|
565
|
+
try {
|
|
566
|
+
const raw = await readFile2(credentialsPath(), "utf8");
|
|
567
|
+
const parsed = JSON.parse(raw);
|
|
568
|
+
if (!parsed?.accessToken || typeof parsed.accessToken !== "string") return null;
|
|
569
|
+
return parsed;
|
|
570
|
+
} catch {
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
async function writeCredentials(creds) {
|
|
575
|
+
const dir = carrierHome();
|
|
576
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
577
|
+
await chmod(dir, 448).catch(() => void 0);
|
|
578
|
+
const file = credentialsPath();
|
|
579
|
+
await writeFile3(file, `${JSON.stringify(creds, null, 2)}
|
|
580
|
+
`, { mode: 384 });
|
|
581
|
+
await chmod(file, 384).catch(() => void 0);
|
|
582
|
+
}
|
|
583
|
+
async function clearCredentials() {
|
|
584
|
+
await rm(credentialsPath(), { force: true });
|
|
585
|
+
}
|
|
586
|
+
function isExpired(creds, skewMs = 6e4) {
|
|
587
|
+
if (!creds.expiresAt) return false;
|
|
588
|
+
return Date.now() >= creds.expiresAt - skewMs;
|
|
589
|
+
}
|
|
590
|
+
function redact(text3) {
|
|
591
|
+
return text3.replace(/\b(ak_|sk_|rt_)[A-Za-z0-9._-]{6,}/g, "$1***").replace(/\bBearer\s+[A-Za-z0-9._-]{8,}/gi, "Bearer ***");
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// src/cli/lib/auth.ts
|
|
595
|
+
var DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
|
|
596
|
+
var CLIENT_NAME = "Carrier CLI";
|
|
597
|
+
var DEFAULT_SCOPE = "read write";
|
|
598
|
+
var CALLBACK_PORTS = [3118, 3119, 8976, 8977, 0];
|
|
599
|
+
async function discover(base = MCP_HOME) {
|
|
600
|
+
const res = await fetch(`${base}${DISCOVERY_PATH}`, {
|
|
601
|
+
headers: { Accept: "application/json" }
|
|
602
|
+
});
|
|
603
|
+
if (!res.ok) throw new Error(`OAuth discovery failed (${res.status}) at ${base}${DISCOVERY_PATH}`);
|
|
604
|
+
const meta = await res.json();
|
|
605
|
+
if (!meta.authorization_endpoint || !meta.token_endpoint) {
|
|
606
|
+
throw new Error("OAuth discovery returned no authorization or token endpoint");
|
|
607
|
+
}
|
|
608
|
+
return meta;
|
|
609
|
+
}
|
|
610
|
+
function base64url(buf) {
|
|
611
|
+
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
612
|
+
}
|
|
613
|
+
function pkcePair() {
|
|
614
|
+
const verifier = base64url(randomBytes(32));
|
|
615
|
+
const challenge = base64url(createHash("sha256").update(verifier).digest());
|
|
616
|
+
return { verifier, challenge };
|
|
617
|
+
}
|
|
618
|
+
function sameState(a, b) {
|
|
619
|
+
const left = Buffer.from(a);
|
|
620
|
+
const right = Buffer.from(b);
|
|
621
|
+
if (left.length !== right.length) return false;
|
|
622
|
+
return timingSafeEqual(left, right);
|
|
623
|
+
}
|
|
624
|
+
async function registerClient(meta, redirectUri) {
|
|
625
|
+
if (!meta.registration_endpoint) {
|
|
626
|
+
throw new Error("This server does not support dynamic client registration");
|
|
627
|
+
}
|
|
628
|
+
const res = await fetch(meta.registration_endpoint, {
|
|
347
629
|
method: "POST",
|
|
348
|
-
json:
|
|
630
|
+
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
631
|
+
body: JSON.stringify({
|
|
349
632
|
client_name: CLIENT_NAME,
|
|
350
|
-
redirect_uris: [
|
|
633
|
+
redirect_uris: [redirectUri],
|
|
351
634
|
grant_types: ["authorization_code", "refresh_token"],
|
|
352
635
|
response_types: ["code"],
|
|
636
|
+
// Installed apps cannot keep a secret; register as a public client.
|
|
353
637
|
token_endpoint_auth_method: "none",
|
|
354
|
-
|
|
355
|
-
}
|
|
638
|
+
application_type: "native"
|
|
639
|
+
})
|
|
356
640
|
});
|
|
357
641
|
if (!res.ok) {
|
|
358
|
-
|
|
642
|
+
const body = (await res.text().catch(() => "")).slice(0, 200);
|
|
643
|
+
throw new Error(`Client registration failed (${res.status}). ${body}`);
|
|
359
644
|
}
|
|
360
|
-
const
|
|
361
|
-
if (!
|
|
362
|
-
return
|
|
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 };
|
|
645
|
+
const json = await res.json();
|
|
646
|
+
if (!json.client_id) throw new Error("Client registration returned no client_id");
|
|
647
|
+
return json.client_id;
|
|
379
648
|
}
|
|
380
|
-
|
|
381
|
-
|
|
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);
|
|
649
|
+
function escapeHtml(text3) {
|
|
650
|
+
return text3.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
389
651
|
}
|
|
390
|
-
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
const match = list?.find((d) => d.name?.toLowerCase() === wanted && d.status === "verified");
|
|
397
|
-
return match?.id;
|
|
652
|
+
function page(title, detail) {
|
|
653
|
+
const safeTitle = escapeHtml(title);
|
|
654
|
+
const safeDetail = escapeHtml(detail);
|
|
655
|
+
return `<!doctype html><meta charset="utf-8"><title>${safeTitle}</title>
|
|
656
|
+
<body style="font-family:system-ui,sans-serif;max-width:32rem;margin:20vh auto;text-align:center">
|
|
657
|
+
<h1 style="font-size:1.25rem">${safeTitle}</h1><p style="color:#555">${safeDetail}</p></body>`;
|
|
398
658
|
}
|
|
399
|
-
async function
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
659
|
+
async function startCallbackServer() {
|
|
660
|
+
let resolveCode;
|
|
661
|
+
let rejectCode;
|
|
662
|
+
let expected = "";
|
|
663
|
+
const server = createServer((req, res) => {
|
|
664
|
+
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
665
|
+
if (url.pathname !== "/callback") {
|
|
666
|
+
res.writeHead(404).end();
|
|
667
|
+
return;
|
|
408
668
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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();
|
|
669
|
+
const err = url.searchParams.get("error");
|
|
670
|
+
const code = url.searchParams.get("code");
|
|
671
|
+
const state = url.searchParams.get("state") ?? "";
|
|
672
|
+
if (err) {
|
|
673
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
674
|
+
res.end(page("Sign-in failed", err));
|
|
675
|
+
rejectCode?.(new Error(`Authorization server returned "${err}"`));
|
|
429
676
|
return;
|
|
430
677
|
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
);
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
);
|
|
441
|
-
});
|
|
442
|
-
await new Promise((resolve3, reject) => {
|
|
443
|
-
server.once("error", reject);
|
|
444
|
-
server.listen(0, REDIRECT_HOST, resolve3);
|
|
678
|
+
if (!code || !expected || !sameState(state, expected)) {
|
|
679
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
680
|
+
res.end(page("Sign-in failed", "The redirect did not match this login attempt."));
|
|
681
|
+
rejectCode?.(new Error("State mismatch \u2014 ignoring the redirect"));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
685
|
+
res.end(page("You're signed in", "Close this tab and return to your terminal."));
|
|
686
|
+
resolveCode?.(code);
|
|
445
687
|
});
|
|
446
|
-
|
|
447
|
-
|
|
688
|
+
let bound = false;
|
|
689
|
+
let lastErr;
|
|
690
|
+
for (const port2 of CALLBACK_PORTS) {
|
|
691
|
+
try {
|
|
692
|
+
await new Promise((resolve3, reject) => {
|
|
693
|
+
server.once("error", reject);
|
|
694
|
+
server.listen(port2, "127.0.0.1", () => {
|
|
695
|
+
server.removeListener("error", reject);
|
|
696
|
+
resolve3();
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
bound = true;
|
|
700
|
+
break;
|
|
701
|
+
} catch (e) {
|
|
702
|
+
lastErr = e;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
if (!bound) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
`Could not bind a local callback port (tried ${CALLBACK_PORTS.filter(Boolean).join(", ")}): ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
const { port } = server.address();
|
|
448
711
|
return {
|
|
449
|
-
redirectUri
|
|
450
|
-
close
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
resolveCode = null;
|
|
456
|
-
server.close();
|
|
457
|
-
resolve3(r);
|
|
458
|
-
};
|
|
712
|
+
redirectUri: `http://localhost:${port}/callback`,
|
|
713
|
+
// `close()` alone only stops new connections; a keep-alive socket from the
|
|
714
|
+
// browser would hold the event loop open and the CLI would never exit.
|
|
715
|
+
waitForCode: (expectedState, timeoutMs) => {
|
|
716
|
+
expected = expectedState;
|
|
717
|
+
return new Promise((resolve3, reject) => {
|
|
459
718
|
const timer = setTimeout(
|
|
460
|
-
() =>
|
|
719
|
+
() => reject(new Error(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for the browser`)),
|
|
461
720
|
timeoutMs
|
|
462
721
|
);
|
|
463
|
-
resolveCode = (
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
722
|
+
resolveCode = (code) => {
|
|
723
|
+
clearTimeout(timer);
|
|
724
|
+
resolve3(code);
|
|
725
|
+
};
|
|
726
|
+
rejectCode = (err) => {
|
|
727
|
+
clearTimeout(timer);
|
|
728
|
+
reject(err);
|
|
469
729
|
};
|
|
470
730
|
});
|
|
731
|
+
},
|
|
732
|
+
close: () => {
|
|
733
|
+
server.closeAllConnections();
|
|
734
|
+
server.close();
|
|
471
735
|
}
|
|
472
736
|
};
|
|
473
737
|
}
|
|
474
|
-
function
|
|
475
|
-
const
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
}
|
|
738
|
+
async function postToken(meta, body) {
|
|
739
|
+
const res = await fetch(meta.token_endpoint, {
|
|
740
|
+
method: "POST",
|
|
741
|
+
headers: {
|
|
742
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
743
|
+
Accept: "application/json"
|
|
744
|
+
},
|
|
745
|
+
body: new URLSearchParams(body).toString()
|
|
746
|
+
});
|
|
747
|
+
const json = await res.json().catch(() => ({}));
|
|
748
|
+
if (!res.ok || json.error) {
|
|
749
|
+
throw new Error(json.error_description || json.error || `Token endpoint returned ${res.status}`);
|
|
516
750
|
}
|
|
517
|
-
|
|
751
|
+
if (!json.access_token) throw new Error("Token endpoint returned no access_token");
|
|
752
|
+
return json;
|
|
518
753
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
754
|
+
function store(meta, token, clientId) {
|
|
755
|
+
return {
|
|
756
|
+
issuer: meta.issuer,
|
|
757
|
+
accessToken: token.access_token,
|
|
758
|
+
refreshToken: token.refresh_token,
|
|
759
|
+
expiresAt: Date.now() + (token.expires_in ?? 900) * 1e3,
|
|
760
|
+
scope: token.scope,
|
|
761
|
+
clientId
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
async function login(opts) {
|
|
765
|
+
const meta = await discover();
|
|
766
|
+
const server = await startCallbackServer();
|
|
531
767
|
try {
|
|
532
|
-
const
|
|
533
|
-
|
|
768
|
+
const existing = await readCredentials();
|
|
769
|
+
const clientId = existing?.clientId && existing.issuer === meta.issuer ? existing.clientId : await registerClient(meta, server.redirectUri);
|
|
534
770
|
const { verifier, challenge } = pkcePair();
|
|
535
|
-
const state = base64url(randomBytes(
|
|
536
|
-
const
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
);
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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
|
|
771
|
+
const state = base64url(randomBytes(16));
|
|
772
|
+
const authUrl = new URL(meta.authorization_endpoint);
|
|
773
|
+
authUrl.searchParams.set("response_type", "code");
|
|
774
|
+
authUrl.searchParams.set("client_id", clientId);
|
|
775
|
+
authUrl.searchParams.set("redirect_uri", server.redirectUri);
|
|
776
|
+
authUrl.searchParams.set("scope", pickScope(meta));
|
|
777
|
+
authUrl.searchParams.set("state", state);
|
|
778
|
+
authUrl.searchParams.set("code_challenge", challenge);
|
|
779
|
+
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
780
|
+
const pendingCode = server.waitForCode(state, opts.timeoutMs ?? 3e5);
|
|
781
|
+
pendingCode.catch(() => void 0);
|
|
782
|
+
opts.onUrl?.(authUrl.toString());
|
|
783
|
+
await opts.openBrowser(authUrl.toString());
|
|
784
|
+
const code = await pendingCode;
|
|
785
|
+
const token = await postToken(meta, {
|
|
786
|
+
grant_type: "authorization_code",
|
|
787
|
+
code,
|
|
788
|
+
redirect_uri: server.redirectUri,
|
|
789
|
+
client_id: clientId,
|
|
790
|
+
code_verifier: verifier
|
|
562
791
|
});
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
};
|
|
792
|
+
await writeCredentials(store(meta, token, clientId));
|
|
793
|
+
return { ok: true, message: "Signed in.", scope: token.scope };
|
|
584
794
|
} finally {
|
|
585
795
|
server.close();
|
|
586
796
|
}
|
|
587
797
|
}
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
"
|
|
592
|
-
"subscribers",
|
|
593
|
-
"usage",
|
|
594
|
-
"packages",
|
|
595
|
-
"billing",
|
|
596
|
-
"wallet",
|
|
597
|
-
"greenzone",
|
|
598
|
-
"storefront"
|
|
599
|
-
];
|
|
600
|
-
function isDashDomain(value) {
|
|
601
|
-
return DASH_DOMAINS.includes(value);
|
|
798
|
+
function pickScope(meta) {
|
|
799
|
+
const supported = meta.scopes_supported ?? [];
|
|
800
|
+
if (!supported.length) return DEFAULT_SCOPE;
|
|
801
|
+
return DEFAULT_SCOPE.split(" ").filter((s) => supported.includes(s)).join(" ") || supported[0];
|
|
602
802
|
}
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
803
|
+
async function getAccessToken() {
|
|
804
|
+
const creds = await readCredentials();
|
|
805
|
+
if (!creds) return null;
|
|
806
|
+
if (!isExpired(creds)) return creds.accessToken;
|
|
807
|
+
if (!creds.refreshToken || !creds.clientId) return null;
|
|
808
|
+
try {
|
|
809
|
+
const meta = await discover();
|
|
810
|
+
const token = await postToken(meta, {
|
|
811
|
+
grant_type: "refresh_token",
|
|
812
|
+
refresh_token: creds.refreshToken,
|
|
813
|
+
client_id: creds.clientId
|
|
814
|
+
});
|
|
815
|
+
const next = store(meta, token, creds.clientId);
|
|
816
|
+
if (!next.refreshToken) next.refreshToken = creds.refreshToken;
|
|
817
|
+
await writeCredentials(next);
|
|
818
|
+
return next.accessToken;
|
|
819
|
+
} catch {
|
|
820
|
+
return null;
|
|
618
821
|
}
|
|
619
|
-
return void 0;
|
|
620
822
|
}
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return Number.isFinite(n) ? n : fallback;
|
|
624
|
-
};
|
|
625
|
-
var str = (value, fallback = "") => value === void 0 || value === null ? fallback : String(value);
|
|
626
|
-
function rows(payload, keys) {
|
|
627
|
-
if (Array.isArray(payload)) return payload;
|
|
628
|
-
const rec = payload ?? {};
|
|
629
|
-
for (const key of keys) {
|
|
630
|
-
const candidate = rec[key];
|
|
631
|
-
if (Array.isArray(candidate)) return candidate;
|
|
632
|
-
}
|
|
633
|
-
return [];
|
|
823
|
+
async function logout() {
|
|
824
|
+
await clearCredentials();
|
|
634
825
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
case "billing":
|
|
689
|
-
return billingScreen({
|
|
690
|
-
balance: payload?.balance === void 0 ? void 0 : num(payload.balance),
|
|
691
|
-
currency: str(payload?.currency, ""),
|
|
692
|
-
events: rows(payload, ["events", "data"]).map((e) => ({
|
|
693
|
-
date: str(e.date ?? e.timestamp, "?"),
|
|
694
|
-
description: str(e.description ?? e.type, "\u2014"),
|
|
695
|
-
amount: num(e.amount)
|
|
696
|
-
}))
|
|
697
|
-
});
|
|
698
|
-
case "wallet": {
|
|
699
|
-
const p4 = payload ?? {};
|
|
700
|
-
const eur = (value) => value === void 0 || value === null ? void 0 : num(value) / 100;
|
|
701
|
-
return walletScreen({
|
|
702
|
-
balance: eur(p4.balance_eur_cents ?? p4.balance) ?? 0,
|
|
703
|
-
currency: str(p4.currency, "\u20AC"),
|
|
704
|
-
autoTopupEnabled: Boolean(p4.autoTopupEnabled ?? p4.auto_topup_enabled),
|
|
705
|
-
threshold: eur(p4.auto_topup_threshold_cents ?? p4.threshold),
|
|
706
|
-
credits: p4.credits === void 0 || p4.credits === null ? void 0 : num(p4.credits)
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
case "greenzone":
|
|
710
|
-
return greenzoneScreen(
|
|
711
|
-
rows(payload, ["entries", "whitelist", "data"]).map((e) => ({
|
|
712
|
-
value: str(e.value ?? e.prefix ?? e.destination, "?"),
|
|
713
|
-
note: e.note ? str(e.note) : void 0
|
|
714
|
-
}))
|
|
715
|
-
);
|
|
716
|
-
case "storefront": {
|
|
717
|
-
const p4 = payload ?? {};
|
|
718
|
-
return storefrontScreen({
|
|
719
|
-
brand: str(p4.brand ?? p4.project, "storefront"),
|
|
720
|
-
url: p4.url ? str(p4.url) : void 0,
|
|
721
|
-
target: p4.target ? str(p4.target) : void 0,
|
|
722
|
-
verified: p4.verified === void 0 ? null : Boolean(p4.verified),
|
|
723
|
-
diagnosis: p4.diagnosis ? str(p4.diagnosis) : void 0,
|
|
724
|
-
secretsStaged: Array.isArray(p4.secrets_staged) ? p4.secrets_staged : void 0,
|
|
725
|
-
plans: p4.plans === void 0 ? void 0 : num(p4.plans)
|
|
726
|
-
});
|
|
727
|
-
}
|
|
728
|
-
}
|
|
826
|
+
|
|
827
|
+
// src/cli/lib/guidance.ts
|
|
828
|
+
import pc from "picocolors";
|
|
829
|
+
var FLEET_NL_EXAMPLES = [
|
|
830
|
+
{ label: "Fleet health", prompt: "Show my fleet health \u2014 accounts, eSIMs, low-balance alerts" },
|
|
831
|
+
{ label: "Subscriber lookup", prompt: "Look up subscriber ICCID 8944\u2026 and diagnose connectivity" },
|
|
832
|
+
{ label: "Usage anomalies", prompt: "Detect usage anomalies and burn-rate risks this week" },
|
|
833
|
+
{ label: "Issue from inventory", prompt: "Issue 3 free eSIMs from inventory on my account and assign the starter package" },
|
|
834
|
+
{ label: "Billing check", prompt: "What's my platform credit balance and Stripe Connect payout status?" },
|
|
835
|
+
{ label: "Wallet balance", prompt: "What's my managed prepaid wallet balance and auto-top-up status?" },
|
|
836
|
+
{ label: "Churn risk", prompt: "List high churn-risk subscribers with retention ideas" }
|
|
837
|
+
];
|
|
838
|
+
function oauthFirstUseNote() {
|
|
839
|
+
return [
|
|
840
|
+
"Entry A \u2014 zero credentials up front",
|
|
841
|
+
` MCP URL: ${MCP_URL}`,
|
|
842
|
+
" Auth: OAuth on first use (Clerk \u2014 Google, GitHub, or email)",
|
|
843
|
+
" Token: none required in Claude / Cursor / Windsurf",
|
|
844
|
+
"",
|
|
845
|
+
"When you first talk to Carrier in your MCP client, the browser opens",
|
|
846
|
+
"sign-in/sign-up. After authorize, tools work. Headless? Use an org API key",
|
|
847
|
+
`(ak_\u2026) from ${CONSOLE_URL} \u2192 Settings \u2192 API Keys.`
|
|
848
|
+
].join("\n");
|
|
849
|
+
}
|
|
850
|
+
function formatStatusBlock(st) {
|
|
851
|
+
const yn = (ok2, yes = "yes", no = "no") => ok2 ? pc.green(yes) : pc.yellow(no);
|
|
852
|
+
const authLabel = st.authMode === "oauth-first-use" ? pc.cyan("OAuth on first use (no token needed)") : st.authMode === "api-key" ? pc.green("org API key in env (CARRIER_API_KEY)") : st.authMode === "ocs-token" ? pc.green("OCS token in env") : pc.yellow("not ready \u2014 install MCP or open sign-up");
|
|
853
|
+
return [
|
|
854
|
+
`Claude Code: ${yn(st.claudeFound, "found", "not on PATH")}`,
|
|
855
|
+
`Plugin: ${st.pluginInstalled ? pc.green(`installed v${st.pluginVersion ?? "?"}`) : pc.yellow("not installed")}`,
|
|
856
|
+
`MCP registered:${st.mcpRegistered ? pc.green(" yes") : pc.yellow(" no")}${st.mcpDetail ? pc.dim(` (${st.mcpDetail})`) : ""}`,
|
|
857
|
+
`MCP URL: ${st.mcpUrlMatches ? pc.green(MCP_URL) : pc.yellow("not production / missing")}`,
|
|
858
|
+
`Auth path: ${authLabel}`,
|
|
859
|
+
`Headless token:${st.tokenPresent ? pc.green(` ${st.tokenKind}`) : pc.dim(" none (optional)")}`
|
|
860
|
+
].join("\n");
|
|
861
|
+
}
|
|
862
|
+
function formatNextSteps(st) {
|
|
863
|
+
return st.nextSteps.map((s, i) => ` ${i + 1}. ${s}`).join("\n");
|
|
864
|
+
}
|
|
865
|
+
function formatNlExamples() {
|
|
866
|
+
return FLEET_NL_EXAMPLES.map((e) => ` \u2022 ${pc.bold(e.label)}: "${e.prompt}"`).join("\n");
|
|
867
|
+
}
|
|
868
|
+
function accountLinksNote() {
|
|
869
|
+
return [
|
|
870
|
+
`Sign up: ${SIGN_UP_URL}`,
|
|
871
|
+
`Sign in: ${SIGN_IN_URL}`,
|
|
872
|
+
`Console: ${CONSOLE_URL}`,
|
|
873
|
+
`Onboarding: ${ONBOARDING_URL}`,
|
|
874
|
+
`MCP home: https://mcp.carrier.llc`
|
|
875
|
+
].join("\n");
|
|
876
|
+
}
|
|
877
|
+
function withNextStep(errorMsg, next) {
|
|
878
|
+
return [errorMsg, "", "What to do next:", ...next.map((s) => ` \u2192 ${s}`)].join("\n");
|
|
729
879
|
}
|
|
730
880
|
|
|
731
881
|
// src/cli/lib/status.ts
|
|
732
|
-
import { homedir as
|
|
733
|
-
import { join as
|
|
882
|
+
import { homedir as homedir3 } from "os";
|
|
883
|
+
import { join as join6 } from "path";
|
|
734
884
|
|
|
735
885
|
// src/cli/lib/mcp-url.ts
|
|
736
886
|
var PRODUCTION_MCP_URL = "https://mcp.carrier.llc/mcp";
|
|
@@ -823,10 +973,10 @@ function scanMcpServers(servers) {
|
|
|
823
973
|
}
|
|
824
974
|
async function probeMcpFromConfig() {
|
|
825
975
|
const candidates = [
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
976
|
+
join6(homedir3(), ".claude.json"),
|
|
977
|
+
join6(claudeHome(), "settings.json"),
|
|
978
|
+
join6(claudeHome(), ".mcp.json"),
|
|
979
|
+
join6(process.cwd(), ".mcp.json")
|
|
830
980
|
];
|
|
831
981
|
let best = null;
|
|
832
982
|
let configSeen = false;
|
|
@@ -952,569 +1102,1038 @@ function resolveCliToken() {
|
|
|
952
1102
|
return ocs || null;
|
|
953
1103
|
}
|
|
954
1104
|
|
|
955
|
-
// src/cli/lib/
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
{ label: "Subscriber lookup", prompt: "Look up subscriber ICCID 8944\u2026 and diagnose connectivity" },
|
|
960
|
-
{ label: "Usage anomalies", prompt: "Detect usage anomalies and burn-rate risks this week" },
|
|
961
|
-
{ label: "Issue from inventory", prompt: "Issue 3 free eSIMs from inventory on my account and assign the starter package" },
|
|
962
|
-
{ label: "Billing check", prompt: "What's my platform credit balance and Stripe Connect payout status?" },
|
|
963
|
-
{ label: "Wallet balance", prompt: "What's my managed prepaid wallet balance and auto-top-up status?" },
|
|
964
|
-
{ label: "Churn risk", prompt: "List high churn-risk subscribers with retention ideas" }
|
|
1105
|
+
// src/cli/lib/ask.ts
|
|
1106
|
+
var DEFAULT_ERROR_HINTS = [
|
|
1107
|
+
"Re-check the arguments: carrier <domain> <command> --help",
|
|
1108
|
+
"Or open Claude and ask there with full tool routing"
|
|
965
1109
|
];
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
" Auth: OAuth on first use (Clerk \u2014 Google, GitHub, or email)",
|
|
971
|
-
" Token: none required in Claude / Cursor / Windsurf",
|
|
972
|
-
"",
|
|
973
|
-
"When you first talk to Carrier in your MCP client, the browser opens",
|
|
974
|
-
"sign-in/sign-up. After authorize, tools work. Headless? Use an org API key",
|
|
975
|
-
`(ak_\u2026) from ${CONSOLE_URL} \u2192 Settings \u2192 API Keys.`
|
|
976
|
-
].join("\n");
|
|
977
|
-
}
|
|
978
|
-
function formatStatusBlock(st) {
|
|
979
|
-
const yn = (ok2, yes = "yes", no = "no") => ok2 ? pc.green(yes) : pc.yellow(no);
|
|
980
|
-
const authLabel = st.authMode === "oauth-first-use" ? pc.cyan("OAuth on first use (no token needed)") : st.authMode === "api-key" ? pc.green("org API key in env (CARRIER_API_KEY)") : st.authMode === "ocs-token" ? pc.green("OCS token in env") : pc.yellow("not ready \u2014 install MCP or open sign-up");
|
|
981
|
-
return [
|
|
982
|
-
`Claude Code: ${yn(st.claudeFound, "found", "not on PATH")}`,
|
|
983
|
-
`Plugin: ${st.pluginInstalled ? pc.green(`installed v${st.pluginVersion ?? "?"}`) : pc.yellow("not installed")}`,
|
|
984
|
-
`MCP registered:${st.mcpRegistered ? pc.green(" yes") : pc.yellow(" no")}${st.mcpDetail ? pc.dim(` (${st.mcpDetail})`) : ""}`,
|
|
985
|
-
`MCP URL: ${st.mcpUrlMatches ? pc.green(MCP_URL) : pc.yellow("not production / missing")}`,
|
|
986
|
-
`Auth path: ${authLabel}`,
|
|
987
|
-
`Headless token:${st.tokenPresent ? pc.green(` ${st.tokenKind}`) : pc.dim(" none (optional)")}`
|
|
988
|
-
].join("\n");
|
|
1110
|
+
var DEFAULT_MCP_TIMEOUT_MS = 45e3;
|
|
1111
|
+
function mcpTimeoutMs() {
|
|
1112
|
+
const parsed = Number(process.env.CARRIER_MCP_TIMEOUT_MS);
|
|
1113
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MCP_TIMEOUT_MS;
|
|
989
1114
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
`Console: ${CONSOLE_URL}`,
|
|
1001
|
-
`Onboarding: ${ONBOARDING_URL}`,
|
|
1002
|
-
`MCP home: https://mcp.carrier.llc`
|
|
1003
|
-
].join("\n");
|
|
1004
|
-
}
|
|
1005
|
-
function withNextStep(errorMsg, next) {
|
|
1006
|
-
return [errorMsg, "", "What to do next:", ...next.map((s) => ` \u2192 ${s}`)].join("\n");
|
|
1115
|
+
var McpTimeoutError = class extends Error {
|
|
1116
|
+
constructor(timeoutMs) {
|
|
1117
|
+
super(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
|
|
1118
|
+
this.timeoutMs = timeoutMs;
|
|
1119
|
+
this.name = "McpTimeoutError";
|
|
1120
|
+
}
|
|
1121
|
+
timeoutMs;
|
|
1122
|
+
};
|
|
1123
|
+
function isTimeoutish(err) {
|
|
1124
|
+
return err instanceof McpTimeoutError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
1007
1125
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
// src/cli/lib/credentials.ts
|
|
1018
|
-
import { homedir as homedir3 } from "os";
|
|
1019
|
-
import { join as join6 } from "path";
|
|
1020
|
-
import { chmod, mkdir as mkdir2, readFile as readFile2, rm, writeFile as writeFile3 } from "fs/promises";
|
|
1021
|
-
function carrierHome() {
|
|
1022
|
-
return process.env.CARRIER_HOME?.trim() || join6(homedir3(), ".carrier");
|
|
1126
|
+
function parseRpcBody(raw) {
|
|
1127
|
+
const trimmed = raw.trim();
|
|
1128
|
+
if (!trimmed) return {};
|
|
1129
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
1130
|
+
return JSON.parse(trimmed);
|
|
1131
|
+
}
|
|
1132
|
+
const dataLine = trimmed.split("\n").map((l) => l.trim()).find((l) => l.startsWith("data:"));
|
|
1133
|
+
if (!dataLine) return {};
|
|
1134
|
+
return JSON.parse(dataLine.slice(5).trim());
|
|
1023
1135
|
}
|
|
1024
|
-
function
|
|
1025
|
-
|
|
1136
|
+
async function callMcpTool(tool, args, opts = {}) {
|
|
1137
|
+
const token = resolveCliToken() ?? await getAccessToken();
|
|
1138
|
+
if (!token) {
|
|
1139
|
+
return {
|
|
1140
|
+
ok: false,
|
|
1141
|
+
text: withNextStep("Not signed in.", [
|
|
1142
|
+
"Sign in from this terminal: carrier login",
|
|
1143
|
+
"No account yet? carrier open signup",
|
|
1144
|
+
"Headless/CI instead: export CARRIER_API_KEY=ak_\u2026 from Console \u2192 Settings \u2192 API Keys"
|
|
1145
|
+
])
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
const headers = {
|
|
1149
|
+
Authorization: `Bearer ${token}`,
|
|
1150
|
+
"Content-Type": "application/json",
|
|
1151
|
+
Accept: "application/json, text/event-stream"
|
|
1152
|
+
};
|
|
1153
|
+
const timeoutMs = mcpTimeoutMs();
|
|
1154
|
+
const controller = new AbortController();
|
|
1155
|
+
let deadlineTimer;
|
|
1156
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
1157
|
+
deadlineTimer = setTimeout(() => {
|
|
1158
|
+
controller.abort();
|
|
1159
|
+
reject(new McpTimeoutError(timeoutMs));
|
|
1160
|
+
}, timeoutMs);
|
|
1161
|
+
});
|
|
1162
|
+
const signal = controller.signal;
|
|
1163
|
+
try {
|
|
1164
|
+
return await Promise.race([
|
|
1165
|
+
(async () => {
|
|
1166
|
+
const initRes = await fetch(MCP_URL, {
|
|
1167
|
+
method: "POST",
|
|
1168
|
+
headers,
|
|
1169
|
+
signal,
|
|
1170
|
+
body: JSON.stringify({
|
|
1171
|
+
jsonrpc: "2.0",
|
|
1172
|
+
id: 1,
|
|
1173
|
+
method: "initialize",
|
|
1174
|
+
params: {
|
|
1175
|
+
protocolVersion: "2024-11-05",
|
|
1176
|
+
capabilities: {},
|
|
1177
|
+
clientInfo: { name: "carrier-cli", version: CARRIER_VERSION }
|
|
1178
|
+
}
|
|
1179
|
+
})
|
|
1180
|
+
});
|
|
1181
|
+
if (initRes.status === 401 || initRes.status === 403) {
|
|
1182
|
+
return {
|
|
1183
|
+
ok: false,
|
|
1184
|
+
text: withNextStep(`MCP returned ${initRes.status} (auth rejected).`, [
|
|
1185
|
+
"Confirm CARRIER_API_KEY is a valid org key (ak_\u2026) from app.carrier.llc",
|
|
1186
|
+
"Or complete OCS onboarding: https://app.carrier.llc/onboarding",
|
|
1187
|
+
"Interactive: use Claude + OAuth instead of a headless key"
|
|
1188
|
+
])
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
const sessionId = initRes.headers.get("mcp-session-id") ?? initRes.headers.get("Mcp-Session-Id");
|
|
1192
|
+
await initRes.arrayBuffer().catch(() => void 0);
|
|
1193
|
+
if (!sessionId) {
|
|
1194
|
+
return {
|
|
1195
|
+
ok: false,
|
|
1196
|
+
text: withNextStep("MCP initialize did not return a session id.", [
|
|
1197
|
+
"Check network access to https://mcp.carrier.llc/mcp",
|
|
1198
|
+
"Retry later, or use Claude with the registered MCP (OAuth path)"
|
|
1199
|
+
])
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
const sessionHeaders = { ...headers, "Mcp-Session-Id": sessionId };
|
|
1203
|
+
await fetch(MCP_URL, {
|
|
1204
|
+
method: "POST",
|
|
1205
|
+
headers: sessionHeaders,
|
|
1206
|
+
signal,
|
|
1207
|
+
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
|
|
1208
|
+
}).catch(() => void 0);
|
|
1209
|
+
const res = await fetch(MCP_URL, {
|
|
1210
|
+
method: "POST",
|
|
1211
|
+
headers: sessionHeaders,
|
|
1212
|
+
signal,
|
|
1213
|
+
body: JSON.stringify({
|
|
1214
|
+
jsonrpc: "2.0",
|
|
1215
|
+
id: 2,
|
|
1216
|
+
method: "tools/call",
|
|
1217
|
+
params: { name: tool, arguments: args }
|
|
1218
|
+
})
|
|
1219
|
+
});
|
|
1220
|
+
if (!res.ok) {
|
|
1221
|
+
const snippet = (await res.text().catch(() => "")).slice(0, 240);
|
|
1222
|
+
return {
|
|
1223
|
+
ok: false,
|
|
1224
|
+
text: withNextStep(`MCP call failed (${res.status}). ${snippet}`, [
|
|
1225
|
+
"Check network access to https://mcp.carrier.llc/mcp",
|
|
1226
|
+
"Retry later, or use Claude with the registered MCP (OAuth path)"
|
|
1227
|
+
])
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
const json = parseRpcBody(await res.text());
|
|
1231
|
+
if (json.error?.message) {
|
|
1232
|
+
return {
|
|
1233
|
+
ok: false,
|
|
1234
|
+
text: withNextStep(json.error.message, opts.errorHints ?? DEFAULT_ERROR_HINTS)
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
const parts = json.result?.content ?? [];
|
|
1238
|
+
const text3 = parts.map((p4) => p4.text).filter(Boolean).join("\n").trim() || JSON.stringify(json.result ?? json, null, 2);
|
|
1239
|
+
return {
|
|
1240
|
+
ok: !json.result?.isError,
|
|
1241
|
+
text: text3,
|
|
1242
|
+
structured: json.result?.structuredContent
|
|
1243
|
+
};
|
|
1244
|
+
})(),
|
|
1245
|
+
deadline
|
|
1246
|
+
]);
|
|
1247
|
+
} catch (e) {
|
|
1248
|
+
if (isTimeoutish(e)) {
|
|
1249
|
+
const secs = Math.round(timeoutMs / 1e3);
|
|
1250
|
+
return {
|
|
1251
|
+
ok: false,
|
|
1252
|
+
text: withNextStep(
|
|
1253
|
+
`MCP call to ${tool} timed out after ${secs}s (no response).`,
|
|
1254
|
+
[
|
|
1255
|
+
"The tool is likely returning more data than the MCP transport can deliver \u2014 see issue #663",
|
|
1256
|
+
"Narrow the request (a specific --account-id / --iccid returns far less than a whole-fleet call)",
|
|
1257
|
+
`Raise the deadline for one slow call: CARRIER_MCP_TIMEOUT_MS=${timeoutMs * 2} carrier \u2026`,
|
|
1258
|
+
"Confirm the service is up: check network access to https://mcp.carrier.llc/mcp",
|
|
1259
|
+
"Or open Claude and ask there with full tool routing"
|
|
1260
|
+
]
|
|
1261
|
+
)
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1265
|
+
return {
|
|
1266
|
+
ok: false,
|
|
1267
|
+
text: withNextStep(`Could not reach MCP: ${msg}`, [
|
|
1268
|
+
"Verify outbound HTTPS to mcp.carrier.llc",
|
|
1269
|
+
"Use the interactive Claude path while offline from this machine"
|
|
1270
|
+
])
|
|
1271
|
+
};
|
|
1272
|
+
} finally {
|
|
1273
|
+
if (deadlineTimer !== void 0) clearTimeout(deadlineTimer);
|
|
1274
|
+
}
|
|
1026
1275
|
}
|
|
1027
|
-
|
|
1276
|
+
var ASK_ERROR_HINTS = [
|
|
1277
|
+
"Rephrase the intent more specifically (include ICCID / MSISDN if relevant)",
|
|
1278
|
+
"Or use a discrete command: carrier --help lists every domain"
|
|
1279
|
+
];
|
|
1280
|
+
function parseRouting(text3) {
|
|
1281
|
+
const trimmed = text3.trim();
|
|
1282
|
+
if (!trimmed.startsWith("{")) return void 0;
|
|
1028
1283
|
try {
|
|
1029
|
-
const
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1284
|
+
const parsed = JSON.parse(trimmed);
|
|
1285
|
+
if (!parsed || typeof parsed !== "object") return void 0;
|
|
1286
|
+
const routes = typeof parsed.resolved_tool === "string" && parsed.resolved_tool;
|
|
1287
|
+
const asksBack = typeof parsed.clarifying_question === "string" || Array.isArray(parsed.candidates);
|
|
1288
|
+
return routes || asksBack ? parsed : void 0;
|
|
1033
1289
|
} catch {
|
|
1034
|
-
return
|
|
1290
|
+
return void 0;
|
|
1035
1291
|
}
|
|
1036
1292
|
}
|
|
1037
|
-
async function
|
|
1038
|
-
const
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
return Date.now() >= creds.expiresAt - skewMs;
|
|
1052
|
-
}
|
|
1053
|
-
function redact(text3) {
|
|
1054
|
-
return text3.replace(/\b(ak_|sk_|rt_)[A-Za-z0-9._-]{6,}/g, "$1***").replace(/\bBearer\s+[A-Za-z0-9._-]{8,}/gi, "Bearer ***");
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
// src/cli/lib/auth.ts
|
|
1058
|
-
var DISCOVERY_PATH = "/.well-known/oauth-authorization-server";
|
|
1059
|
-
var CLIENT_NAME2 = "Carrier CLI";
|
|
1060
|
-
var DEFAULT_SCOPE = "read write";
|
|
1061
|
-
var CALLBACK_PORTS = [3118, 3119, 8976, 8977, 0];
|
|
1062
|
-
async function discover(base = MCP_HOME) {
|
|
1063
|
-
const res = await fetch(`${base}${DISCOVERY_PATH}`, {
|
|
1064
|
-
headers: { Accept: "application/json" }
|
|
1065
|
-
});
|
|
1066
|
-
if (!res.ok) throw new Error(`OAuth discovery failed (${res.status}) at ${base}${DISCOVERY_PATH}`);
|
|
1067
|
-
const meta = await res.json();
|
|
1068
|
-
if (!meta.authorization_endpoint || !meta.token_endpoint) {
|
|
1069
|
-
throw new Error("OAuth discovery returned no authorization or token endpoint");
|
|
1293
|
+
async function carrierAsk(intent) {
|
|
1294
|
+
const routed = await callMcpTool("carrier_ask", { intent }, { errorHints: ASK_ERROR_HINTS });
|
|
1295
|
+
if (!routed.ok) return routed;
|
|
1296
|
+
const routing = parseRouting(routed.text);
|
|
1297
|
+
if (!routing) return routed;
|
|
1298
|
+
if (!routing.resolved_tool) {
|
|
1299
|
+
const candidates = (routing.candidates ?? []).map((c) => c.tool ? `Candidate: ${c.tool}${c.reason ? ` \u2014 ${c.reason}` : ""}` : void 0).filter((s) => Boolean(s));
|
|
1300
|
+
return {
|
|
1301
|
+
ok: false,
|
|
1302
|
+
text: withNextStep(routing.clarifying_question?.trim() || "Carrier could not tell which tool you meant.", [
|
|
1303
|
+
...candidates,
|
|
1304
|
+
...ASK_ERROR_HINTS
|
|
1305
|
+
])
|
|
1306
|
+
};
|
|
1070
1307
|
}
|
|
1071
|
-
|
|
1072
|
-
}
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
}
|
|
1081
|
-
function sameState(a, b) {
|
|
1082
|
-
const left = Buffer.from(a);
|
|
1083
|
-
const right = Buffer.from(b);
|
|
1084
|
-
if (left.length !== right.length) return false;
|
|
1085
|
-
return timingSafeEqual(left, right);
|
|
1086
|
-
}
|
|
1087
|
-
async function registerClient(meta, redirectUri) {
|
|
1088
|
-
if (!meta.registration_endpoint) {
|
|
1089
|
-
throw new Error("This server does not support dynamic client registration");
|
|
1308
|
+
const tool = routing.resolved_tool;
|
|
1309
|
+
const args = routing.resolved_params ?? {};
|
|
1310
|
+
if (routing.match && routing.match !== "confirmed") {
|
|
1311
|
+
return {
|
|
1312
|
+
ok: false,
|
|
1313
|
+
text: withNextStep(`Carrier could not confirm what you meant (match: ${routing.match}).`, [
|
|
1314
|
+
`Closest tool: ${tool}`,
|
|
1315
|
+
...ASK_ERROR_HINTS
|
|
1316
|
+
])
|
|
1317
|
+
};
|
|
1090
1318
|
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
token_endpoint_auth_method: "none",
|
|
1101
|
-
application_type: "native"
|
|
1102
|
-
})
|
|
1103
|
-
});
|
|
1104
|
-
if (!res.ok) {
|
|
1105
|
-
const body = (await res.text().catch(() => "")).slice(0, 200);
|
|
1106
|
-
throw new Error(`Client registration failed (${res.status}). ${body}`);
|
|
1319
|
+
if (routing.confirm_required) {
|
|
1320
|
+
return {
|
|
1321
|
+
ok: false,
|
|
1322
|
+
text: withNextStep(`"${intent}" resolves to ${tool}, which changes live service or billing.`, [
|
|
1323
|
+
`Params: ${JSON.stringify(args)}`,
|
|
1324
|
+
"Run the matching discrete command so every flag is explicit and reviewable",
|
|
1325
|
+
"carrier --help lists every domain"
|
|
1326
|
+
])
|
|
1327
|
+
};
|
|
1107
1328
|
}
|
|
1108
|
-
|
|
1109
|
-
if (!json.client_id) throw new Error("Client registration returned no client_id");
|
|
1110
|
-
return json.client_id;
|
|
1329
|
+
return callMcpTool(tool, args, { errorHints: ASK_ERROR_HINTS });
|
|
1111
1330
|
}
|
|
1112
|
-
|
|
1113
|
-
|
|
1331
|
+
|
|
1332
|
+
// src/cli/lib/rest.ts
|
|
1333
|
+
var DEFAULT_ERROR_HINTS2 = [
|
|
1334
|
+
"Re-check the arguments: carrier <domain> <command> --help",
|
|
1335
|
+
"Or open Claude and ask there with full tool routing"
|
|
1336
|
+
];
|
|
1337
|
+
function resolveApiKey() {
|
|
1338
|
+
return process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || null;
|
|
1114
1339
|
}
|
|
1115
|
-
function
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
<body style="font-family:system-ui,sans-serif;max-width:32rem;margin:20vh auto;text-align:center">
|
|
1120
|
-
<h1 style="font-size:1.25rem">${safeTitle}</h1><p style="color:#555">${safeDetail}</p></body>`;
|
|
1340
|
+
function ocsTokenOnly() {
|
|
1341
|
+
return Boolean(
|
|
1342
|
+
process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim()
|
|
1343
|
+
);
|
|
1121
1344
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
return;
|
|
1131
|
-
}
|
|
1132
|
-
const err = url.searchParams.get("error");
|
|
1133
|
-
const code = url.searchParams.get("code");
|
|
1134
|
-
const state = url.searchParams.get("state") ?? "";
|
|
1135
|
-
if (err) {
|
|
1136
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1137
|
-
res.end(page("Sign-in failed", err));
|
|
1138
|
-
rejectCode?.(new Error(`Authorization server returned "${err}"`));
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
if (!code || !expected || !sameState(state, expected)) {
|
|
1142
|
-
res.writeHead(400, { "Content-Type": "text/html" });
|
|
1143
|
-
res.end(page("Sign-in failed", "The redirect did not match this login attempt."));
|
|
1144
|
-
rejectCode?.(new Error("State mismatch \u2014 ignoring the redirect"));
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1148
|
-
res.end(page("You're signed in", "Close this tab and return to your terminal."));
|
|
1149
|
-
resolveCode?.(code);
|
|
1150
|
-
});
|
|
1151
|
-
let bound = false;
|
|
1152
|
-
let lastErr;
|
|
1153
|
-
for (const port2 of CALLBACK_PORTS) {
|
|
1154
|
-
try {
|
|
1155
|
-
await new Promise((resolve3, reject) => {
|
|
1156
|
-
server.once("error", reject);
|
|
1157
|
-
server.listen(port2, "127.0.0.1", () => {
|
|
1158
|
-
server.removeListener("error", reject);
|
|
1159
|
-
resolve3();
|
|
1160
|
-
});
|
|
1161
|
-
});
|
|
1162
|
-
bound = true;
|
|
1163
|
-
break;
|
|
1164
|
-
} catch (e) {
|
|
1165
|
-
lastErr = e;
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
if (!bound) {
|
|
1169
|
-
throw new Error(
|
|
1170
|
-
`Could not bind a local callback port (tried ${CALLBACK_PORTS.filter(Boolean).join(", ")}): ${lastErr instanceof Error ? lastErr.message : String(lastErr)}`
|
|
1171
|
-
);
|
|
1172
|
-
}
|
|
1173
|
-
const { port } = server.address();
|
|
1174
|
-
return {
|
|
1175
|
-
redirectUri: `http://localhost:${port}/callback`,
|
|
1176
|
-
// `close()` alone only stops new connections; a keep-alive socket from the
|
|
1177
|
-
// browser would hold the event loop open and the CLI would never exit.
|
|
1178
|
-
waitForCode: (expectedState, timeoutMs) => {
|
|
1179
|
-
expected = expectedState;
|
|
1180
|
-
return new Promise((resolve3, reject) => {
|
|
1181
|
-
const timer = setTimeout(
|
|
1182
|
-
() => reject(new Error(`Timed out after ${Math.round(timeoutMs / 1e3)}s waiting for the browser`)),
|
|
1183
|
-
timeoutMs
|
|
1184
|
-
);
|
|
1185
|
-
resolveCode = (code) => {
|
|
1186
|
-
clearTimeout(timer);
|
|
1187
|
-
resolve3(code);
|
|
1188
|
-
};
|
|
1189
|
-
rejectCode = (err) => {
|
|
1190
|
-
clearTimeout(timer);
|
|
1191
|
-
reject(err);
|
|
1192
|
-
};
|
|
1193
|
-
});
|
|
1194
|
-
},
|
|
1195
|
-
close: () => {
|
|
1196
|
-
server.closeAllConnections();
|
|
1197
|
-
server.close();
|
|
1345
|
+
function applyPath(path, args) {
|
|
1346
|
+
const rest = { ...args };
|
|
1347
|
+
const missing = [];
|
|
1348
|
+
const filled = path.replace(/\{(\w+)\}/g, (_match, name) => {
|
|
1349
|
+
const value = rest[name];
|
|
1350
|
+
if (value === void 0 || value === null || value === "") {
|
|
1351
|
+
missing.push(name);
|
|
1352
|
+
return `{${name}}`;
|
|
1198
1353
|
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
async function postToken(meta, body) {
|
|
1202
|
-
const res = await fetch(meta.token_endpoint, {
|
|
1203
|
-
method: "POST",
|
|
1204
|
-
headers: {
|
|
1205
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
1206
|
-
Accept: "application/json"
|
|
1207
|
-
},
|
|
1208
|
-
body: new URLSearchParams(body).toString()
|
|
1354
|
+
delete rest[name];
|
|
1355
|
+
return encodeURIComponent(String(value));
|
|
1209
1356
|
});
|
|
1210
|
-
|
|
1211
|
-
if (!res.ok || json.error) {
|
|
1212
|
-
throw new Error(json.error_description || json.error || `Token endpoint returned ${res.status}`);
|
|
1213
|
-
}
|
|
1214
|
-
if (!json.access_token) throw new Error("Token endpoint returned no access_token");
|
|
1215
|
-
return json;
|
|
1216
|
-
}
|
|
1217
|
-
function store(meta, token, clientId) {
|
|
1218
|
-
return {
|
|
1219
|
-
issuer: meta.issuer,
|
|
1220
|
-
accessToken: token.access_token,
|
|
1221
|
-
refreshToken: token.refresh_token,
|
|
1222
|
-
expiresAt: Date.now() + (token.expires_in ?? 900) * 1e3,
|
|
1223
|
-
scope: token.scope,
|
|
1224
|
-
clientId
|
|
1225
|
-
};
|
|
1357
|
+
return { path: filled, rest, missing };
|
|
1226
1358
|
}
|
|
1227
|
-
|
|
1228
|
-
const
|
|
1229
|
-
const
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
const clientId = existing?.clientId && existing.issuer === meta.issuer ? existing.clientId : await registerClient(meta, server.redirectUri);
|
|
1233
|
-
const { verifier, challenge } = pkcePair2();
|
|
1234
|
-
const state = base64url2(randomBytes2(16));
|
|
1235
|
-
const authUrl = new URL(meta.authorization_endpoint);
|
|
1236
|
-
authUrl.searchParams.set("response_type", "code");
|
|
1237
|
-
authUrl.searchParams.set("client_id", clientId);
|
|
1238
|
-
authUrl.searchParams.set("redirect_uri", server.redirectUri);
|
|
1239
|
-
authUrl.searchParams.set("scope", pickScope(meta));
|
|
1240
|
-
authUrl.searchParams.set("state", state);
|
|
1241
|
-
authUrl.searchParams.set("code_challenge", challenge);
|
|
1242
|
-
authUrl.searchParams.set("code_challenge_method", "S256");
|
|
1243
|
-
const pendingCode = server.waitForCode(state, opts.timeoutMs ?? 3e5);
|
|
1244
|
-
pendingCode.catch(() => void 0);
|
|
1245
|
-
opts.onUrl?.(authUrl.toString());
|
|
1246
|
-
await opts.openBrowser(authUrl.toString());
|
|
1247
|
-
const code = await pendingCode;
|
|
1248
|
-
const token = await postToken(meta, {
|
|
1249
|
-
grant_type: "authorization_code",
|
|
1250
|
-
code,
|
|
1251
|
-
redirect_uri: server.redirectUri,
|
|
1252
|
-
client_id: clientId,
|
|
1253
|
-
code_verifier: verifier
|
|
1254
|
-
});
|
|
1255
|
-
await writeCredentials(store(meta, token, clientId));
|
|
1256
|
-
return { ok: true, message: "Signed in.", scope: token.scope };
|
|
1257
|
-
} finally {
|
|
1258
|
-
server.close();
|
|
1359
|
+
function queryString(args) {
|
|
1360
|
+
const params = new URLSearchParams();
|
|
1361
|
+
for (const [key, value] of Object.entries(args)) {
|
|
1362
|
+
if (value === void 0 || value === null) continue;
|
|
1363
|
+
params.set(key, String(value));
|
|
1259
1364
|
}
|
|
1365
|
+
const qs = params.toString();
|
|
1366
|
+
return qs ? `?${qs}` : "";
|
|
1260
1367
|
}
|
|
1261
|
-
function
|
|
1262
|
-
const
|
|
1263
|
-
if (!
|
|
1264
|
-
return DEFAULT_SCOPE.split(" ").filter((s) => supported.includes(s)).join(" ") || supported[0];
|
|
1265
|
-
}
|
|
1266
|
-
async function getAccessToken() {
|
|
1267
|
-
const creds = await readCredentials();
|
|
1268
|
-
if (!creds) return null;
|
|
1269
|
-
if (!isExpired(creds)) return creds.accessToken;
|
|
1270
|
-
if (!creds.refreshToken || !creds.clientId) return null;
|
|
1368
|
+
function bodyText(raw) {
|
|
1369
|
+
const trimmed = raw.trim();
|
|
1370
|
+
if (!trimmed) return "";
|
|
1271
1371
|
try {
|
|
1272
|
-
const
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
client_id: creds.clientId
|
|
1277
|
-
});
|
|
1278
|
-
const next = store(meta, token, creds.clientId);
|
|
1279
|
-
if (!next.refreshToken) next.refreshToken = creds.refreshToken;
|
|
1280
|
-
await writeCredentials(next);
|
|
1281
|
-
return next.accessToken;
|
|
1372
|
+
const parsed = JSON.parse(trimmed);
|
|
1373
|
+
if (typeof parsed.error === "string") return parsed.error;
|
|
1374
|
+
if (typeof parsed.message === "string") return parsed.message;
|
|
1375
|
+
return trimmed;
|
|
1282
1376
|
} catch {
|
|
1283
|
-
return
|
|
1377
|
+
return trimmed;
|
|
1284
1378
|
}
|
|
1285
1379
|
}
|
|
1286
|
-
async function
|
|
1287
|
-
await
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
// src/cli/lib/ask.ts
|
|
1291
|
-
var DEFAULT_ERROR_HINTS = [
|
|
1292
|
-
"Re-check the arguments: carrier <domain> <command> --help",
|
|
1293
|
-
"Or open Claude and ask there with full tool routing"
|
|
1294
|
-
];
|
|
1295
|
-
var DEFAULT_MCP_TIMEOUT_MS = 45e3;
|
|
1296
|
-
function mcpTimeoutMs() {
|
|
1297
|
-
const parsed = Number(process.env.CARRIER_MCP_TIMEOUT_MS);
|
|
1298
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_MCP_TIMEOUT_MS;
|
|
1299
|
-
}
|
|
1300
|
-
var McpTimeoutError = class extends Error {
|
|
1301
|
-
constructor(timeoutMs) {
|
|
1302
|
-
super(`timed out after ${Math.round(timeoutMs / 1e3)}s`);
|
|
1303
|
-
this.timeoutMs = timeoutMs;
|
|
1304
|
-
this.name = "McpTimeoutError";
|
|
1305
|
-
}
|
|
1306
|
-
timeoutMs;
|
|
1307
|
-
};
|
|
1308
|
-
function isTimeoutish(err) {
|
|
1309
|
-
return err instanceof McpTimeoutError || err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
|
|
1310
|
-
}
|
|
1311
|
-
function parseRpcBody(raw) {
|
|
1312
|
-
const trimmed = raw.trim();
|
|
1313
|
-
if (!trimmed) return {};
|
|
1314
|
-
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
1315
|
-
return JSON.parse(trimmed);
|
|
1316
|
-
}
|
|
1317
|
-
const dataLine = trimmed.split("\n").map((l) => l.trim()).find((l) => l.startsWith("data:"));
|
|
1318
|
-
if (!dataLine) return {};
|
|
1319
|
-
return JSON.parse(dataLine.slice(5).trim());
|
|
1320
|
-
}
|
|
1321
|
-
async function callMcpTool(tool, args, opts = {}) {
|
|
1322
|
-
const token = resolveCliToken() ?? await getAccessToken();
|
|
1380
|
+
async function callRestEndpoint(method, path, args, opts = {}) {
|
|
1381
|
+
const token = resolveApiKey() ?? await getAccessToken();
|
|
1323
1382
|
if (!token) {
|
|
1324
1383
|
return {
|
|
1325
1384
|
ok: false,
|
|
1326
|
-
text: withNextStep(
|
|
1327
|
-
"
|
|
1328
|
-
|
|
1329
|
-
|
|
1385
|
+
text: withNextStep(
|
|
1386
|
+
ocsTokenOnly() ? "Not signed in. An eSIMVault/OCS token is set, but the Carrier API only accepts Carrier credentials." : "Not signed in.",
|
|
1387
|
+
[
|
|
1388
|
+
"Sign in from this terminal: carrier login",
|
|
1389
|
+
"No account yet? carrier open signup",
|
|
1390
|
+
"Headless/CI instead: export CARRIER_API_KEY=ak_\u2026 from Console \u2192 Settings \u2192 API Keys"
|
|
1391
|
+
]
|
|
1392
|
+
)
|
|
1393
|
+
};
|
|
1394
|
+
}
|
|
1395
|
+
const { path: resolvedPath, rest, missing } = applyPath(path, args);
|
|
1396
|
+
if (missing.length) {
|
|
1397
|
+
return {
|
|
1398
|
+
ok: false,
|
|
1399
|
+
text: withNextStep(`Cannot build ${method} ${path}.`, [
|
|
1400
|
+
`Missing path value(s): ${missing.join(", ")}`,
|
|
1401
|
+
"See every flag: carrier <domain> <command> --help"
|
|
1330
1402
|
])
|
|
1331
1403
|
};
|
|
1332
1404
|
}
|
|
1405
|
+
const sendsBody = method === "POST" || method === "PUT" || method === "PATCH";
|
|
1406
|
+
const url = `${API_URL}${resolvedPath}${sendsBody ? "" : queryString(rest)}`;
|
|
1333
1407
|
const headers = {
|
|
1334
1408
|
Authorization: `Bearer ${token}`,
|
|
1335
|
-
|
|
1336
|
-
|
|
1409
|
+
Accept: "application/json",
|
|
1410
|
+
"User-Agent": `carrier-cli/${CARRIER_VERSION}`
|
|
1337
1411
|
};
|
|
1412
|
+
if (sendsBody) headers["Content-Type"] = "application/json";
|
|
1413
|
+
if (opts.confirmToken) headers["X-Confirm-Token"] = opts.confirmToken;
|
|
1414
|
+
if (opts.idempotencyKey) headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
1338
1415
|
const timeoutMs = mcpTimeoutMs();
|
|
1339
1416
|
const controller = new AbortController();
|
|
1340
|
-
|
|
1341
|
-
const deadline = new Promise((_resolve, reject) => {
|
|
1342
|
-
deadlineTimer = setTimeout(() => {
|
|
1343
|
-
controller.abort();
|
|
1344
|
-
reject(new McpTimeoutError(timeoutMs));
|
|
1345
|
-
}, timeoutMs);
|
|
1346
|
-
});
|
|
1347
|
-
const signal = controller.signal;
|
|
1417
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
1348
1418
|
try {
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
return {
|
|
1368
|
-
ok: false,
|
|
1369
|
-
text: withNextStep(`MCP returned ${initRes.status} (auth rejected).`, [
|
|
1370
|
-
"Confirm CARRIER_API_KEY is a valid org key (ak_\u2026) from app.carrier.llc",
|
|
1371
|
-
"Or complete OCS onboarding: https://app.carrier.llc/onboarding",
|
|
1372
|
-
"Interactive: use Claude + OAuth instead of a headless key"
|
|
1373
|
-
])
|
|
1374
|
-
};
|
|
1375
|
-
}
|
|
1376
|
-
const sessionId = initRes.headers.get("mcp-session-id") ?? initRes.headers.get("Mcp-Session-Id");
|
|
1377
|
-
await initRes.arrayBuffer().catch(() => void 0);
|
|
1378
|
-
if (!sessionId) {
|
|
1379
|
-
return {
|
|
1380
|
-
ok: false,
|
|
1381
|
-
text: withNextStep("MCP initialize did not return a session id.", [
|
|
1382
|
-
"Check network access to https://mcp.carrier.llc/mcp",
|
|
1383
|
-
"Retry later, or use Claude with the registered MCP (OAuth path)"
|
|
1384
|
-
])
|
|
1385
|
-
};
|
|
1386
|
-
}
|
|
1387
|
-
const sessionHeaders = { ...headers, "Mcp-Session-Id": sessionId };
|
|
1388
|
-
await fetch(MCP_URL, {
|
|
1389
|
-
method: "POST",
|
|
1390
|
-
headers: sessionHeaders,
|
|
1391
|
-
signal,
|
|
1392
|
-
body: JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" })
|
|
1393
|
-
}).catch(() => void 0);
|
|
1394
|
-
const res = await fetch(MCP_URL, {
|
|
1395
|
-
method: "POST",
|
|
1396
|
-
headers: sessionHeaders,
|
|
1397
|
-
signal,
|
|
1398
|
-
body: JSON.stringify({
|
|
1399
|
-
jsonrpc: "2.0",
|
|
1400
|
-
id: 2,
|
|
1401
|
-
method: "tools/call",
|
|
1402
|
-
params: { name: tool, arguments: args }
|
|
1403
|
-
})
|
|
1404
|
-
});
|
|
1405
|
-
if (!res.ok) {
|
|
1406
|
-
const snippet = (await res.text().catch(() => "")).slice(0, 240);
|
|
1407
|
-
return {
|
|
1408
|
-
ok: false,
|
|
1409
|
-
text: withNextStep(`MCP call failed (${res.status}). ${snippet}`, [
|
|
1410
|
-
"Check network access to https://mcp.carrier.llc/mcp",
|
|
1411
|
-
"Retry later, or use Claude with the registered MCP (OAuth path)"
|
|
1412
|
-
])
|
|
1413
|
-
};
|
|
1414
|
-
}
|
|
1415
|
-
const json = parseRpcBody(await res.text());
|
|
1416
|
-
if (json.error?.message) {
|
|
1417
|
-
return {
|
|
1418
|
-
ok: false,
|
|
1419
|
-
text: withNextStep(json.error.message, opts.errorHints ?? DEFAULT_ERROR_HINTS)
|
|
1420
|
-
};
|
|
1421
|
-
}
|
|
1422
|
-
const parts = json.result?.content ?? [];
|
|
1423
|
-
const text3 = parts.map((p4) => p4.text).filter(Boolean).join("\n").trim() || JSON.stringify(json.result ?? json, null, 2);
|
|
1424
|
-
return {
|
|
1425
|
-
ok: !json.result?.isError,
|
|
1426
|
-
text: text3,
|
|
1427
|
-
structured: json.result?.structuredContent
|
|
1428
|
-
};
|
|
1429
|
-
})(),
|
|
1430
|
-
deadline
|
|
1431
|
-
]);
|
|
1432
|
-
} catch (e) {
|
|
1433
|
-
if (isTimeoutish(e)) {
|
|
1434
|
-
const secs = Math.round(timeoutMs / 1e3);
|
|
1419
|
+
const res = await fetch(url, {
|
|
1420
|
+
method,
|
|
1421
|
+
headers,
|
|
1422
|
+
signal: controller.signal,
|
|
1423
|
+
body: sendsBody ? JSON.stringify(rest) : void 0
|
|
1424
|
+
});
|
|
1425
|
+
const raw = await res.text();
|
|
1426
|
+
if (res.status === 202) {
|
|
1427
|
+
let confirmToken;
|
|
1428
|
+
let expires;
|
|
1429
|
+
let message = "";
|
|
1430
|
+
try {
|
|
1431
|
+
const parsed = JSON.parse(raw);
|
|
1432
|
+
confirmToken = parsed.confirm_token;
|
|
1433
|
+
expires = parsed.expires_in_seconds;
|
|
1434
|
+
message = parsed.message ?? "";
|
|
1435
|
+
} catch {
|
|
1436
|
+
}
|
|
1435
1437
|
return {
|
|
1436
1438
|
ok: false,
|
|
1437
1439
|
text: withNextStep(
|
|
1438
|
-
|
|
1440
|
+
message || `${method} ${path} needs an explicit confirmation.`,
|
|
1441
|
+
confirmToken ? [
|
|
1442
|
+
`Re-run the same command with: --confirm-token ${confirmToken}`,
|
|
1443
|
+
`The token is single-use and expires in ${expires ?? 120}s.`
|
|
1444
|
+
] : ["The server asked for confirmation but returned no token \u2014 retry the command."]
|
|
1445
|
+
)
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
if (res.status === 401 || res.status === 403) {
|
|
1449
|
+
return {
|
|
1450
|
+
ok: false,
|
|
1451
|
+
text: withNextStep(
|
|
1452
|
+
`Carrier API returned ${res.status} (auth rejected).`,
|
|
1439
1453
|
[
|
|
1440
|
-
"
|
|
1441
|
-
"
|
|
1442
|
-
|
|
1443
|
-
"Confirm the service is up: check network access to https://mcp.carrier.llc/mcp",
|
|
1444
|
-
"Or open Claude and ask there with full tool routing"
|
|
1454
|
+
"Confirm CARRIER_API_KEY is a valid org key (ak_\u2026) from app.carrier.llc",
|
|
1455
|
+
"Or sign in from this terminal: carrier login",
|
|
1456
|
+
...res.status === 403 ? ["A 403 here usually means the key lacks write scope."] : []
|
|
1445
1457
|
]
|
|
1446
1458
|
)
|
|
1447
1459
|
};
|
|
1448
1460
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
const
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1461
|
+
if (!res.ok) {
|
|
1462
|
+
return {
|
|
1463
|
+
ok: false,
|
|
1464
|
+
text: withNextStep(
|
|
1465
|
+
redact(`${method} ${path} failed (${res.status}). ${bodyText(raw).slice(0, 500)}`),
|
|
1466
|
+
opts.errorHints ?? DEFAULT_ERROR_HINTS2
|
|
1467
|
+
)
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
return { ok: true, text: raw.trim() || JSON.stringify({ status: res.status }) };
|
|
1471
|
+
} catch (e) {
|
|
1472
|
+
const aborted = e instanceof Error && (e.name === "AbortError" || e.name === "TimeoutError");
|
|
1473
|
+
if (aborted) {
|
|
1474
|
+
const secs = Math.round(timeoutMs / 1e3);
|
|
1475
|
+
return {
|
|
1476
|
+
ok: false,
|
|
1477
|
+
text: withNextStep(`${method} ${path} timed out after ${secs}s (no response).`, [
|
|
1478
|
+
`Raise the deadline for one slow call: CARRIER_MCP_TIMEOUT_MS=${timeoutMs * 2} carrier \u2026`,
|
|
1479
|
+
"Confirm the service is up: check network access to https://api.carrier.llc"
|
|
1480
|
+
])
|
|
1481
|
+
};
|
|
1482
|
+
}
|
|
1483
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1484
|
+
return {
|
|
1485
|
+
ok: false,
|
|
1486
|
+
text: withNextStep(redact(`Could not reach the Carrier API: ${msg}`), [
|
|
1487
|
+
"Verify outbound HTTPS to api.carrier.llc",
|
|
1488
|
+
"Use the interactive Claude path while offline from this machine"
|
|
1489
|
+
])
|
|
1490
|
+
};
|
|
1491
|
+
} finally {
|
|
1492
|
+
clearTimeout(timer);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// src/cli/lib/hosted-publish.ts
|
|
1497
|
+
async function publishHostedStorefront(input) {
|
|
1498
|
+
const slug2 = storefrontSlugFrom(input.slug);
|
|
1499
|
+
if (slug2 === "") return { ok: false, text: "Use a short name, such as acme." };
|
|
1500
|
+
input = { ...input, slug: slug2 };
|
|
1501
|
+
const listed = await callRestEndpoint("GET", "/v1/storefronts", {});
|
|
1502
|
+
if (!listed.ok) return listed;
|
|
1503
|
+
const existing = findStore(listed.text, input.slug);
|
|
1504
|
+
if (!existing && input.status !== "draft") {
|
|
1505
|
+
const missing = firstPublishCopy(input);
|
|
1506
|
+
if (missing) return { ok: false, text: missing };
|
|
1507
|
+
}
|
|
1508
|
+
const filled = completePlanInput(planInput(input), existing?.plans ?? []);
|
|
1509
|
+
const plan = storefrontPlanBody(filled);
|
|
1510
|
+
if (isStorefrontPlanError(plan)) return { ok: false, text: plan.error };
|
|
1511
|
+
const brand = storefrontBrandPatch(input, existing ?? void 0);
|
|
1512
|
+
const record2 = { slug: input.slug, name: input.name, status: input.status, ...brand };
|
|
1513
|
+
const saved = existing ? await callRestEndpoint("PATCH", "/v1/storefronts/{id}", { id: existing.id, ...record2 }) : await callRestEndpoint("POST", "/v1/storefronts", record2, { idempotencyKey: `site-publish:${input.slug}` });
|
|
1514
|
+
if (!saved.ok) return saved;
|
|
1515
|
+
if (!plan) return withShopAddress(saved, input.slug);
|
|
1516
|
+
const id = existing?.id ?? readId(saved.text);
|
|
1517
|
+
if (!id) return { ok: false, text: "The store was saved but its id was missing, so the plan was not written." };
|
|
1518
|
+
const planned = await callRestEndpoint("POST", "/v1/storefronts/{id}/plans", { id, ...plan });
|
|
1519
|
+
return planned.ok ? withShopAddress(planned, input.slug) : planned;
|
|
1520
|
+
}
|
|
1521
|
+
function firstPublishCopy(input) {
|
|
1522
|
+
const hero = input.hero?.trim() ?? "";
|
|
1523
|
+
const tagline = input.tagline?.trim() ?? "";
|
|
1524
|
+
const email = input.supportEmail?.trim() ?? "";
|
|
1525
|
+
const legalName = input.legalName?.trim() ?? "";
|
|
1526
|
+
if (hero === "" || hero === "Data that works the day you land.") return "Enter a hero.";
|
|
1527
|
+
if (tagline === "" || tagline === "Programmable connectivity, on demand.") return "Enter a tagline.";
|
|
1528
|
+
if (email === "" || email === "support@carrier.llc") return "Enter a support email.";
|
|
1529
|
+
if (!/^[^\s@]+@[^\s@]+$/.test(email)) return "Support email must be an email address.";
|
|
1530
|
+
if (legalName === "" || legalName === "Lifecycle Innovations Limited") return "Enter a legal name.";
|
|
1531
|
+
return null;
|
|
1532
|
+
}
|
|
1533
|
+
function withShopAddress(result, slug2) {
|
|
1534
|
+
const address = hostedStorefrontUrl(slug2);
|
|
1535
|
+
if (result.text.includes(address)) return result;
|
|
1536
|
+
return { ...result, text: `${result.text}
|
|
1537
|
+
${address}` };
|
|
1538
|
+
}
|
|
1539
|
+
function hostedPublishInputFromFlags(flags) {
|
|
1540
|
+
const status = flags.status === "draft" || flags.status === "published" ? flags.status : null;
|
|
1541
|
+
if (!status) return { ok: false, text: "Status must be draft or published." };
|
|
1542
|
+
const price = readPrice(flags);
|
|
1543
|
+
if (!price.ok) return { ok: false, text: price.text };
|
|
1544
|
+
const template = parseWholeNumber(flags.templateId, "Template id");
|
|
1545
|
+
if (!template.ok) return { ok: false, text: template.message };
|
|
1546
|
+
const data = readLabel(flags.dataGb, parseDataGb);
|
|
1547
|
+
if (!data.ok) return { ok: false, text: data.text };
|
|
1548
|
+
const validity = readLabel(flags.validityDays, parseValidityDays);
|
|
1549
|
+
if (!validity.ok) return { ok: false, text: validity.text };
|
|
1550
|
+
const countries = readCountries(flags.countries);
|
|
1551
|
+
if (!countries.ok) return { ok: false, text: countries.text };
|
|
1552
|
+
const logo = readLogo(flags.logo);
|
|
1553
|
+
if (!logo.ok) return { ok: false, text: logo.text };
|
|
1554
|
+
return {
|
|
1555
|
+
ok: true,
|
|
1556
|
+
input: {
|
|
1557
|
+
slug: flags.slug,
|
|
1558
|
+
name: flags.name,
|
|
1559
|
+
status,
|
|
1560
|
+
accent: flags.accent,
|
|
1561
|
+
accentDark: flags.accentDark,
|
|
1562
|
+
bg: flags.bg,
|
|
1563
|
+
text: flags.text,
|
|
1564
|
+
logoUrl: logo.value,
|
|
1565
|
+
hero: flags.hero,
|
|
1566
|
+
tagline: flags.tagline,
|
|
1567
|
+
supportEmail: flags.supportEmail,
|
|
1568
|
+
legalName: flags.legalName,
|
|
1569
|
+
productId: flags.product,
|
|
1570
|
+
planSlug: flags.plan,
|
|
1571
|
+
planName: flags.planName,
|
|
1572
|
+
priceCents: price.value,
|
|
1573
|
+
currency: flags.currency,
|
|
1574
|
+
templateId: template.value,
|
|
1575
|
+
dataGb: data.value,
|
|
1576
|
+
validityDays: validity.value,
|
|
1577
|
+
regions: countries.value,
|
|
1578
|
+
description: flags.planText,
|
|
1579
|
+
planActive: flags.inactive ? false : void 0
|
|
1580
|
+
}
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
function readLogo(value) {
|
|
1584
|
+
if (value === void 0 || value.trim() === "") return { ok: true, value: void 0 };
|
|
1585
|
+
const trimmed = value.trim();
|
|
1586
|
+
if (trimmed.startsWith("https://") || trimmed.startsWith("data:")) {
|
|
1587
|
+
const parsed = logoReference(trimmed);
|
|
1588
|
+
return parsed.ok ? { ok: true, value: parsed.url } : { ok: false, text: parsed.message };
|
|
1589
|
+
}
|
|
1590
|
+
try {
|
|
1591
|
+
const bytes = readFileSync(trimmed);
|
|
1592
|
+
const parsed = logoFileDataUrl(bytes);
|
|
1593
|
+
return parsed.ok ? { ok: true, value: parsed.url } : { ok: false, text: parsed.message };
|
|
1594
|
+
} catch {
|
|
1595
|
+
return { ok: false, text: "Use a PNG, JPEG, WebP, or GIF under 70 KB, or an https address." };
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
function readPrice(flags) {
|
|
1599
|
+
if (flags.price !== void 0 && flags.priceCents !== void 0) {
|
|
1600
|
+
return { ok: false, text: "Use either --price 9.00 or --price-cents 900." };
|
|
1601
|
+
}
|
|
1602
|
+
if (flags.price !== void 0) {
|
|
1603
|
+
const parsed2 = majorToCents(flags.price, flags.currency);
|
|
1604
|
+
return parsed2.ok ? { ok: true, value: parsed2.cents } : { ok: false, text: parsed2.message };
|
|
1605
|
+
}
|
|
1606
|
+
const parsed = parseWholeNumber(flags.priceCents, "Price");
|
|
1607
|
+
return parsed.ok ? { ok: true, value: parsed.value } : { ok: false, text: parsed.message };
|
|
1608
|
+
}
|
|
1609
|
+
function readLabel(value, parse) {
|
|
1610
|
+
if (value === void 0) return { ok: true, value: void 0 };
|
|
1611
|
+
const parsed = parse(value);
|
|
1612
|
+
return parsed.ok ? { ok: true, value: parsed.value } : { ok: false, text: parsed.message };
|
|
1613
|
+
}
|
|
1614
|
+
function readCountries(value) {
|
|
1615
|
+
if (value === void 0) return { ok: true, value: void 0 };
|
|
1616
|
+
const parsed = parseCountryInput(value);
|
|
1617
|
+
return parsed.ok ? { ok: true, value: parsed.regions } : { ok: false, text: parsed.message };
|
|
1618
|
+
}
|
|
1619
|
+
function planInput(input) {
|
|
1620
|
+
return {
|
|
1621
|
+
productId: input.productId,
|
|
1622
|
+
slug: input.planSlug,
|
|
1623
|
+
name: input.planName,
|
|
1624
|
+
priceCents: input.priceCents,
|
|
1625
|
+
currency: input.currency,
|
|
1626
|
+
templateId: input.templateId,
|
|
1627
|
+
dataGb: input.dataGb,
|
|
1628
|
+
validityDays: input.validityDays,
|
|
1629
|
+
regions: input.regions,
|
|
1630
|
+
description: input.description,
|
|
1631
|
+
active: input.planActive
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
function findStore(body, slug2) {
|
|
1635
|
+
try {
|
|
1636
|
+
const parsed = JSON.parse(body);
|
|
1637
|
+
const match = parsed.data?.find((row) => row["slug"] === slug2);
|
|
1638
|
+
if (!match || typeof match["id"] !== "string") return null;
|
|
1639
|
+
return {
|
|
1640
|
+
id: match["id"],
|
|
1641
|
+
theme: record(match["theme"]),
|
|
1642
|
+
copy: record(match["copy"]),
|
|
1643
|
+
plans: storedPlans(match["plans"])
|
|
1644
|
+
};
|
|
1645
|
+
} catch {
|
|
1646
|
+
return null;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
function storedPlans(value) {
|
|
1650
|
+
if (!Array.isArray(value)) return [];
|
|
1651
|
+
const plans = [];
|
|
1652
|
+
for (const item of value) {
|
|
1653
|
+
const row = record(item);
|
|
1654
|
+
if (!row) continue;
|
|
1655
|
+
if (typeof row["slug"] !== "string" || typeof row["name"] !== "string" || typeof row["currency"] !== "string") continue;
|
|
1656
|
+
if (typeof row["ocs_package_template_id"] !== "number") continue;
|
|
1657
|
+
plans.push({
|
|
1658
|
+
slug: row["slug"],
|
|
1659
|
+
name: row["name"],
|
|
1660
|
+
currency: row["currency"],
|
|
1661
|
+
ocs_package_template_id: row["ocs_package_template_id"]
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
return plans;
|
|
1665
|
+
}
|
|
1666
|
+
function readId(body) {
|
|
1667
|
+
try {
|
|
1668
|
+
const parsed = JSON.parse(body);
|
|
1669
|
+
return typeof parsed.id === "string" ? parsed.id : null;
|
|
1670
|
+
} catch {
|
|
1671
|
+
return null;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
function record(value) {
|
|
1675
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// src/cli/lib/resend.ts
|
|
1679
|
+
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
1680
|
+
import { createServer as createServer2 } from "http";
|
|
1681
|
+
var RESEND_API = "https://api.resend.com";
|
|
1682
|
+
var RESEND_API_KEYS_URL = "https://resend.com/api-keys";
|
|
1683
|
+
var RESEND_DOMAINS_URL = "https://resend.com/domains";
|
|
1684
|
+
var USER_AGENT = "carrier-cli";
|
|
1685
|
+
var CLIENT_NAME2 = "Carrier storefront setup";
|
|
1686
|
+
var OAUTH_SCOPE = "full_access";
|
|
1687
|
+
var REDIRECT_PATH = "/oauth/callback";
|
|
1688
|
+
var REDIRECT_HOST = "127.0.0.1";
|
|
1689
|
+
function looksResendKey(key) {
|
|
1690
|
+
return /^re_[A-Za-z0-9_-]{8,}$/.test(key.trim());
|
|
1691
|
+
}
|
|
1692
|
+
function base64url2(input) {
|
|
1693
|
+
return input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1694
|
+
}
|
|
1695
|
+
function pkcePair2() {
|
|
1696
|
+
const verifier = base64url2(randomBytes2(64));
|
|
1697
|
+
return { verifier, challenge: base64url2(createHash2("sha256").update(verifier).digest()) };
|
|
1698
|
+
}
|
|
1699
|
+
function buildAuthorizeUrl(opts) {
|
|
1700
|
+
const url = new URL(`${RESEND_API}/oauth/authorize`);
|
|
1701
|
+
url.searchParams.set("client_id", opts.clientId);
|
|
1702
|
+
url.searchParams.set("response_type", "code");
|
|
1703
|
+
url.searchParams.set("redirect_uri", opts.redirectUri);
|
|
1704
|
+
url.searchParams.set("scope", opts.scope ?? OAUTH_SCOPE);
|
|
1705
|
+
url.searchParams.set("state", opts.state);
|
|
1706
|
+
url.searchParams.set("code_challenge", opts.challenge);
|
|
1707
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
1708
|
+
return url.toString();
|
|
1709
|
+
}
|
|
1710
|
+
async function resendFetch(fetchImpl, path, init = {}) {
|
|
1711
|
+
const headers = { "User-Agent": USER_AGENT, Accept: "application/json" };
|
|
1712
|
+
if (init.token) headers.Authorization = `Bearer ${init.token}`;
|
|
1713
|
+
let body;
|
|
1714
|
+
if (init.form) {
|
|
1715
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
1716
|
+
body = new URLSearchParams(init.form).toString();
|
|
1717
|
+
} else if (init.json !== void 0) {
|
|
1718
|
+
headers["Content-Type"] = "application/json";
|
|
1719
|
+
body = JSON.stringify(init.json);
|
|
1720
|
+
}
|
|
1721
|
+
try {
|
|
1722
|
+
const res = await fetchImpl(`${RESEND_API}${path}`, {
|
|
1723
|
+
method: init.method ?? "GET",
|
|
1724
|
+
headers,
|
|
1725
|
+
body
|
|
1726
|
+
});
|
|
1727
|
+
const text3 = await res.text();
|
|
1728
|
+
let json;
|
|
1729
|
+
try {
|
|
1730
|
+
json = text3 ? JSON.parse(text3) : void 0;
|
|
1731
|
+
} catch {
|
|
1732
|
+
json = void 0;
|
|
1733
|
+
}
|
|
1734
|
+
if (!res.ok) {
|
|
1735
|
+
return { ok: false, status: res.status, json, error: resendError(json) ?? text3.slice(0, 300) };
|
|
1736
|
+
}
|
|
1737
|
+
return { ok: true, status: res.status, json };
|
|
1738
|
+
} catch (e) {
|
|
1739
|
+
return { ok: false, status: 0, error: e instanceof Error ? e.message : String(e) };
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
function resendError(json) {
|
|
1743
|
+
if (typeof json !== "object" || json === null) return void 0;
|
|
1744
|
+
const body = json;
|
|
1745
|
+
return body.message ?? body.error_description ?? body.error;
|
|
1746
|
+
}
|
|
1747
|
+
async function registerOauthClient(fetchImpl) {
|
|
1748
|
+
const registeredRedirect = `http://${REDIRECT_HOST}${REDIRECT_PATH}`;
|
|
1749
|
+
const res = await resendFetch(fetchImpl, "/oauth/register", {
|
|
1750
|
+
method: "POST",
|
|
1751
|
+
json: {
|
|
1752
|
+
client_name: CLIENT_NAME2,
|
|
1753
|
+
redirect_uris: [registeredRedirect],
|
|
1754
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
1755
|
+
response_types: ["code"],
|
|
1756
|
+
token_endpoint_auth_method: "none",
|
|
1757
|
+
scope: OAUTH_SCOPE
|
|
1758
|
+
}
|
|
1759
|
+
});
|
|
1760
|
+
if (!res.ok) {
|
|
1761
|
+
return { ok: false, reason: `Resend rejected client registration: ${res.error ?? res.status}` };
|
|
1762
|
+
}
|
|
1763
|
+
const clientId = res.json?.client_id;
|
|
1764
|
+
if (!clientId) return { ok: false, reason: "Resend registered no client_id." };
|
|
1765
|
+
return { ok: true, clientId };
|
|
1766
|
+
}
|
|
1767
|
+
async function exchangeCode(fetchImpl, opts) {
|
|
1768
|
+
const res = await resendFetch(fetchImpl, "/oauth/token", {
|
|
1769
|
+
method: "POST",
|
|
1770
|
+
form: {
|
|
1771
|
+
grant_type: "authorization_code",
|
|
1772
|
+
client_id: opts.clientId,
|
|
1773
|
+
code: opts.code,
|
|
1774
|
+
redirect_uri: opts.redirectUri,
|
|
1775
|
+
code_verifier: opts.verifier
|
|
1776
|
+
}
|
|
1777
|
+
});
|
|
1778
|
+
if (!res.ok) return { ok: false, reason: `Token exchange failed: ${res.error ?? res.status}` };
|
|
1779
|
+
const body = res.json;
|
|
1780
|
+
if (!body?.access_token) return { ok: false, reason: "Resend returned no access token." };
|
|
1781
|
+
return { ok: true, accessToken: body.access_token, refreshToken: body.refresh_token };
|
|
1782
|
+
}
|
|
1783
|
+
async function revokeGrant(fetchImpl, opts) {
|
|
1784
|
+
await resendFetch(fetchImpl, "/oauth/revoke", {
|
|
1785
|
+
method: "POST",
|
|
1786
|
+
form: {
|
|
1787
|
+
client_id: opts.clientId,
|
|
1788
|
+
token: opts.refreshToken,
|
|
1789
|
+
token_type_hint: "refresh_token"
|
|
1790
|
+
}
|
|
1791
|
+
}).catch(() => void 0);
|
|
1792
|
+
}
|
|
1793
|
+
async function findVerifiedDomainId(fetchImpl, accessToken, domain) {
|
|
1794
|
+
const wanted = domain.trim().replace(/^https?:\/\//, "").replace(/\/+$/, "").toLowerCase();
|
|
1795
|
+
if (!wanted) return void 0;
|
|
1796
|
+
const res = await resendFetch(fetchImpl, "/domains", { token: accessToken });
|
|
1797
|
+
if (!res.ok) return void 0;
|
|
1798
|
+
const list = res.json?.data;
|
|
1799
|
+
const match = list?.find((d) => d.name?.toLowerCase() === wanted && d.status === "verified");
|
|
1800
|
+
return match?.id;
|
|
1801
|
+
}
|
|
1802
|
+
async function createSendingKey(fetchImpl, accessToken, opts) {
|
|
1803
|
+
const res = await resendFetch(fetchImpl, "/api-keys", {
|
|
1804
|
+
method: "POST",
|
|
1805
|
+
token: accessToken,
|
|
1806
|
+
json: {
|
|
1807
|
+
// Resend caps the name at 50 characters.
|
|
1808
|
+
name: opts.name.slice(0, 50),
|
|
1809
|
+
permission: "sending_access",
|
|
1810
|
+
...opts.domainId ? { domain_id: opts.domainId } : {}
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
if (!res.ok) return { ok: false, reason: `Could not create an API key: ${res.error ?? res.status}` };
|
|
1814
|
+
const body = res.json;
|
|
1815
|
+
if (!body?.token) return { ok: false, reason: "Resend created a key but returned no token." };
|
|
1816
|
+
return { ok: true, token: body.token, id: body.id };
|
|
1817
|
+
}
|
|
1818
|
+
async function validateResendKey(fetchImpl, key) {
|
|
1819
|
+
if (!looksResendKey(key)) return { ok: false, reason: "That does not look like a Resend key (re_\u2026)." };
|
|
1820
|
+
const res = await resendFetch(fetchImpl, "/emails", { method: "POST", token: key, json: {} });
|
|
1821
|
+
if (res.status === 401) return { ok: false, reason: "Resend says that key is missing or malformed." };
|
|
1822
|
+
if (res.status === 403) return { ok: false, reason: "Resend rejected that key as invalid." };
|
|
1823
|
+
if (res.status === 0) return { ok: false, reason: `Could not reach Resend: ${res.error ?? "network error"}` };
|
|
1824
|
+
return { ok: true };
|
|
1825
|
+
}
|
|
1826
|
+
async function startLoopbackServer() {
|
|
1827
|
+
let resolveCode = null;
|
|
1828
|
+
const server = createServer2((req, res) => {
|
|
1829
|
+
const url = new URL(req.url ?? "/", `http://${REDIRECT_HOST}`);
|
|
1830
|
+
if (url.pathname !== REDIRECT_PATH) {
|
|
1831
|
+
res.writeHead(404).end();
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
const error = url.searchParams.get("error");
|
|
1835
|
+
const code = url.searchParams.get("code");
|
|
1836
|
+
const state = url.searchParams.get("state");
|
|
1837
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1838
|
+
res.end(
|
|
1839
|
+
`<!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>`
|
|
1840
|
+
);
|
|
1841
|
+
resolveCode?.(
|
|
1842
|
+
error ? { ok: false, reason: `Resend returned "${error}".` } : code ? { ok: true, code, reason: state ?? void 0 } : { ok: false, reason: "Resend sent no authorization code." }
|
|
1843
|
+
);
|
|
1844
|
+
});
|
|
1845
|
+
await new Promise((resolve3, reject) => {
|
|
1846
|
+
server.once("error", reject);
|
|
1847
|
+
server.listen(0, REDIRECT_HOST, resolve3);
|
|
1848
|
+
});
|
|
1849
|
+
const port = server.address().port;
|
|
1850
|
+
const redirectUri = `http://${REDIRECT_HOST}:${port}${REDIRECT_PATH}`;
|
|
1851
|
+
return {
|
|
1852
|
+
redirectUri,
|
|
1853
|
+
close: () => server.close(),
|
|
1854
|
+
waitForCode(state, timeoutMs) {
|
|
1855
|
+
return new Promise((resolve3) => {
|
|
1856
|
+
const done = (r) => {
|
|
1857
|
+
clearTimeout(timer);
|
|
1858
|
+
resolveCode = null;
|
|
1859
|
+
server.close();
|
|
1860
|
+
resolve3(r);
|
|
1861
|
+
};
|
|
1862
|
+
const timer = setTimeout(
|
|
1863
|
+
() => done({ ok: false, reason: "Timed out waiting for the browser." }),
|
|
1864
|
+
timeoutMs
|
|
1865
|
+
);
|
|
1866
|
+
resolveCode = (r) => {
|
|
1867
|
+
if (r.ok && r.reason !== state) {
|
|
1868
|
+
done({ ok: false, reason: "The browser callback did not match this request." });
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
done(r.ok ? { ok: true, code: r.code } : r);
|
|
1872
|
+
};
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
};
|
|
1876
|
+
}
|
|
1877
|
+
function discoverResendKey(env) {
|
|
1878
|
+
const key = env.RESEND_API_KEY?.trim();
|
|
1879
|
+
if (!key || !looksResendKey(key)) return void 0;
|
|
1880
|
+
return { apiKey: key, tier: "discovered" };
|
|
1881
|
+
}
|
|
1882
|
+
var MANUAL_STEPS = [
|
|
1883
|
+
`Open ${RESEND_API_KEYS_URL} and create an API key.`,
|
|
1884
|
+
`Give it Sending access, and restrict it to your sending domain if that domain is verified at ${RESEND_DOMAINS_URL}.`,
|
|
1885
|
+
"Put the key in the storefront's .env.local as RESEND_API_KEY.",
|
|
1886
|
+
"Re-run `carrier site deploy` \u2014 the key is pushed to the deploy target automatically."
|
|
1887
|
+
];
|
|
1888
|
+
async function provisionResend(opts) {
|
|
1889
|
+
const { deps } = opts;
|
|
1890
|
+
const notes = [];
|
|
1891
|
+
const discovered = discoverResendKey(opts.env);
|
|
1892
|
+
if (discovered) {
|
|
1893
|
+
const check = await validateResendKey(deps.fetch, discovered.apiKey);
|
|
1894
|
+
if (check.ok) return { ok: true, tier: "discovered", credentials: discovered };
|
|
1895
|
+
notes.push(`A RESEND_API_KEY was already set but did not work: ${check.reason}`);
|
|
1896
|
+
}
|
|
1897
|
+
if (!opts.noBrowser) {
|
|
1898
|
+
const viaBrowser = await oauthTier(opts);
|
|
1899
|
+
if (viaBrowser.ok) {
|
|
1900
|
+
return { ...viaBrowser, reason: notes.length ? notes.join(" ") : void 0 };
|
|
1901
|
+
}
|
|
1902
|
+
notes.push(viaBrowser.reason ?? "The browser authorization did not complete.");
|
|
1903
|
+
}
|
|
1904
|
+
if (deps.promptForKey) {
|
|
1905
|
+
await deps.openBrowser(RESEND_API_KEYS_URL);
|
|
1906
|
+
const pasted = (await deps.promptForKey(RESEND_API_KEYS_URL))?.trim();
|
|
1907
|
+
if (pasted) {
|
|
1908
|
+
const check = await validateResendKey(deps.fetch, pasted);
|
|
1909
|
+
if (check.ok) {
|
|
1910
|
+
return {
|
|
1911
|
+
ok: true,
|
|
1912
|
+
tier: "paste",
|
|
1913
|
+
credentials: { apiKey: pasted, tier: "paste" },
|
|
1914
|
+
reason: notes.length ? notes.join(" ") : void 0
|
|
1915
|
+
};
|
|
1916
|
+
}
|
|
1917
|
+
notes.push(check.reason ?? "The pasted key did not work.");
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
return { ok: false, tier: "manual", reason: notes.join(" "), guidance: MANUAL_STEPS };
|
|
1921
|
+
}
|
|
1922
|
+
async function oauthTier(opts) {
|
|
1923
|
+
const { deps } = opts;
|
|
1924
|
+
let server;
|
|
1925
|
+
try {
|
|
1926
|
+
server = await deps.startLoopback();
|
|
1927
|
+
} catch (e) {
|
|
1928
|
+
return {
|
|
1929
|
+
ok: false,
|
|
1930
|
+
tier: "oauth",
|
|
1931
|
+
reason: `Could not open a local callback listener: ${e instanceof Error ? e.message : String(e)}`
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
try {
|
|
1935
|
+
const registered = await registerOauthClient(deps.fetch);
|
|
1936
|
+
if (!registered.ok) return { ok: false, tier: "oauth", reason: registered.reason };
|
|
1937
|
+
const { verifier, challenge } = pkcePair2();
|
|
1938
|
+
const state = base64url2(randomBytes2(24));
|
|
1939
|
+
const authorizeUrl = buildAuthorizeUrl({
|
|
1940
|
+
clientId: registered.clientId,
|
|
1941
|
+
redirectUri: server.redirectUri,
|
|
1942
|
+
state,
|
|
1943
|
+
challenge
|
|
1944
|
+
});
|
|
1945
|
+
const opened = await deps.openBrowser(authorizeUrl);
|
|
1946
|
+
deps.notify(
|
|
1947
|
+
opened.ok ? "Approve the connection in your browser \u2014 this waits for you." : `Open this to approve the connection:
|
|
1948
|
+
${authorizeUrl}`
|
|
1949
|
+
);
|
|
1950
|
+
const callback = await server.waitForCode(state, opts.timeoutMs ?? 18e4);
|
|
1951
|
+
if (!callback.ok || !callback.code) {
|
|
1952
|
+
return { ok: false, tier: "oauth", reason: callback.reason ?? "No authorization code." };
|
|
1953
|
+
}
|
|
1954
|
+
const token = await exchangeCode(deps.fetch, {
|
|
1955
|
+
clientId: registered.clientId,
|
|
1956
|
+
code: callback.code,
|
|
1957
|
+
redirectUri: server.redirectUri,
|
|
1958
|
+
verifier
|
|
1959
|
+
});
|
|
1960
|
+
if (!token.ok) return { ok: false, tier: "oauth", reason: token.reason };
|
|
1961
|
+
const domainId = opts.domain ? await findVerifiedDomainId(deps.fetch, token.accessToken, opts.domain) : void 0;
|
|
1962
|
+
const created = await createSendingKey(deps.fetch, token.accessToken, {
|
|
1963
|
+
name: `${opts.name} storefront`,
|
|
1964
|
+
domainId
|
|
1965
|
+
});
|
|
1966
|
+
if (token.refreshToken) {
|
|
1967
|
+
await revokeGrant(deps.fetch, {
|
|
1968
|
+
clientId: registered.clientId,
|
|
1969
|
+
refreshToken: token.refreshToken
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
if (!created.ok) return { ok: false, tier: "oauth", reason: created.reason };
|
|
1973
|
+
const check = await validateResendKey(deps.fetch, created.token);
|
|
1974
|
+
if (!check.ok) {
|
|
1975
|
+
return { ok: false, tier: "oauth", reason: `Resend minted a key that it then rejected: ${check.reason}` };
|
|
1976
|
+
}
|
|
1977
|
+
return {
|
|
1978
|
+
ok: true,
|
|
1979
|
+
tier: "oauth",
|
|
1980
|
+
credentials: {
|
|
1981
|
+
apiKey: created.token,
|
|
1982
|
+
tier: "oauth",
|
|
1983
|
+
keyId: created.id,
|
|
1984
|
+
domain: domainId ? opts.domain : void 0
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
} finally {
|
|
1988
|
+
server.close();
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
// src/cli/lib/dash.ts
|
|
1993
|
+
var DASH_DOMAINS = [
|
|
1994
|
+
"fleet",
|
|
1995
|
+
"subscribers",
|
|
1996
|
+
"usage",
|
|
1997
|
+
"packages",
|
|
1998
|
+
"billing",
|
|
1999
|
+
"wallet",
|
|
2000
|
+
"greenzone",
|
|
2001
|
+
"storefront"
|
|
2002
|
+
];
|
|
2003
|
+
function isDashDomain(value) {
|
|
2004
|
+
return DASH_DOMAINS.includes(value);
|
|
2005
|
+
}
|
|
2006
|
+
var DASH_TOOL = {
|
|
2007
|
+
fleet: "fleet_health_app",
|
|
2008
|
+
subscribers: "list_subscribers",
|
|
2009
|
+
usage: "subscriber_usage",
|
|
2010
|
+
packages: "list_package_templates",
|
|
2011
|
+
billing: "billing_events",
|
|
2012
|
+
wallet: "wallet_balance",
|
|
2013
|
+
greenzone: "greenzone_whitelist_list",
|
|
2014
|
+
storefront: ""
|
|
2015
|
+
};
|
|
2016
|
+
function screenFromStructured(structured) {
|
|
2017
|
+
const rec = structured;
|
|
2018
|
+
const screen = rec?.screen;
|
|
2019
|
+
if (screen && typeof screen === "object" && Array.isArray(screen.sections) && screen.title) {
|
|
2020
|
+
return screen;
|
|
2021
|
+
}
|
|
2022
|
+
return void 0;
|
|
2023
|
+
}
|
|
2024
|
+
var num = (value, fallback = 0) => {
|
|
2025
|
+
const n = Number(value);
|
|
2026
|
+
return Number.isFinite(n) ? n : fallback;
|
|
2027
|
+
};
|
|
2028
|
+
var str = (value, fallback = "") => value === void 0 || value === null ? fallback : String(value);
|
|
2029
|
+
function rows(payload, keys) {
|
|
2030
|
+
if (Array.isArray(payload)) return payload;
|
|
2031
|
+
const rec = payload ?? {};
|
|
2032
|
+
for (const key of keys) {
|
|
2033
|
+
const candidate = rec[key];
|
|
2034
|
+
if (Array.isArray(candidate)) return candidate;
|
|
2035
|
+
}
|
|
2036
|
+
return [];
|
|
2037
|
+
}
|
|
2038
|
+
function buildDashScreen(domain, payload) {
|
|
2039
|
+
switch (domain) {
|
|
2040
|
+
case "fleet": {
|
|
2041
|
+
const accounts = rows(payload, ["accounts", "account"]);
|
|
2042
|
+
return fleetScreen({
|
|
2043
|
+
active: num(payload?.active),
|
|
2044
|
+
suspended: num(payload?.suspended),
|
|
2045
|
+
inventory: num(payload?.inventory),
|
|
2046
|
+
other: num(payload?.other),
|
|
2047
|
+
accounts: accounts.map((a) => ({
|
|
2048
|
+
name: str(a.name ?? a.accountId, "?"),
|
|
2049
|
+
balance: num(a.balance),
|
|
2050
|
+
active: num(a.active),
|
|
2051
|
+
suspended: num(a.suspended),
|
|
2052
|
+
inventory: num(a.inventory),
|
|
2053
|
+
other: num(a.other),
|
|
2054
|
+
packageOnly: Boolean(a.packageOnly)
|
|
2055
|
+
}))
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
case "subscribers":
|
|
2059
|
+
return subscribersScreen(
|
|
2060
|
+
rows(payload, ["subscribers", "subscriber", "data"]).map((s) => ({
|
|
2061
|
+
iccid: str(s.iccid ?? s.ICCID, "?"),
|
|
2062
|
+
msisdn: s.msisdn ? str(s.msisdn) : void 0,
|
|
2063
|
+
status: str(s.status ?? s.statusStr, "unknown"),
|
|
2064
|
+
account: s.accountName ? str(s.accountName) : void 0,
|
|
2065
|
+
dataUsedBytes: s.dataUsed === void 0 ? void 0 : num(s.dataUsed)
|
|
2066
|
+
}))
|
|
2067
|
+
);
|
|
2068
|
+
case "usage":
|
|
2069
|
+
return usageScreen({
|
|
2070
|
+
subject: str(payload?.subject, "Subscriber"),
|
|
2071
|
+
timeline: rows(payload, ["timeline", "days", "usage"]).map((p4) => ({
|
|
2072
|
+
date: str(p4.date ?? p4.day, "?"),
|
|
2073
|
+
bytes: num(p4.bytes ?? p4.dataUsed)
|
|
2074
|
+
})),
|
|
2075
|
+
countries: rows(payload, ["countries"]).map((c) => ({
|
|
2076
|
+
country: str(c.country ?? c.countryName, "?"),
|
|
2077
|
+
bytes: num(c.bytes ?? c.dataUsed)
|
|
2078
|
+
}))
|
|
2079
|
+
});
|
|
2080
|
+
case "packages":
|
|
2081
|
+
return packagesScreen(
|
|
2082
|
+
rows(payload, ["template", "templates", "data"]).map((t) => ({
|
|
2083
|
+
name: str(t.prepaidpackagetemplatename ?? t.name, "?"),
|
|
2084
|
+
id: str(t.prepaidpackagetemplateid ?? t.id, "?"),
|
|
2085
|
+
dataLimitBytes: t.databyte === void 0 ? void 0 : num(t.databyte),
|
|
2086
|
+
validityDays: t.perioddays === void 0 ? void 0 : num(t.perioddays),
|
|
2087
|
+
price: t.cost === void 0 ? void 0 : num(t.cost),
|
|
2088
|
+
recurring: Boolean(t.recurring)
|
|
2089
|
+
}))
|
|
2090
|
+
);
|
|
2091
|
+
case "billing":
|
|
2092
|
+
return billingScreen({
|
|
2093
|
+
balance: payload?.balance === void 0 ? void 0 : num(payload.balance),
|
|
2094
|
+
currency: str(payload?.currency, ""),
|
|
2095
|
+
events: rows(payload, ["events", "data"]).map((e) => ({
|
|
2096
|
+
date: str(e.date ?? e.timestamp, "?"),
|
|
2097
|
+
description: str(e.description ?? e.type, "\u2014"),
|
|
2098
|
+
amount: num(e.amount)
|
|
2099
|
+
}))
|
|
2100
|
+
});
|
|
2101
|
+
case "wallet": {
|
|
2102
|
+
const p4 = payload ?? {};
|
|
2103
|
+
const eur = (value) => value === void 0 || value === null ? void 0 : num(value) / 100;
|
|
2104
|
+
return walletScreen({
|
|
2105
|
+
balance: eur(p4.balance_eur_cents ?? p4.balance) ?? 0,
|
|
2106
|
+
currency: str(p4.currency, "\u20AC"),
|
|
2107
|
+
autoTopupEnabled: Boolean(p4.autoTopupEnabled ?? p4.auto_topup_enabled),
|
|
2108
|
+
threshold: eur(p4.auto_topup_threshold_cents ?? p4.threshold),
|
|
2109
|
+
credits: p4.credits === void 0 || p4.credits === null ? void 0 : num(p4.credits)
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
case "greenzone":
|
|
2113
|
+
return greenzoneScreen(
|
|
2114
|
+
rows(payload, ["entries", "whitelist", "data"]).map((e) => ({
|
|
2115
|
+
value: str(e.value ?? e.prefix ?? e.destination, "?"),
|
|
2116
|
+
note: e.note ? str(e.note) : void 0
|
|
2117
|
+
}))
|
|
2118
|
+
);
|
|
2119
|
+
case "storefront": {
|
|
2120
|
+
const p4 = payload ?? {};
|
|
2121
|
+
return storefrontScreen({
|
|
2122
|
+
brand: str(p4.brand ?? p4.project, "storefront"),
|
|
2123
|
+
url: p4.url ? str(p4.url) : void 0,
|
|
2124
|
+
target: p4.target ? str(p4.target) : void 0,
|
|
2125
|
+
verified: p4.verified === void 0 ? null : Boolean(p4.verified),
|
|
2126
|
+
diagnosis: p4.diagnosis ? str(p4.diagnosis) : void 0,
|
|
2127
|
+
secretsStaged: Array.isArray(p4.secrets_staged) ? p4.secrets_staged : void 0,
|
|
2128
|
+
plans: p4.plans === void 0 ? void 0 : num(p4.plans)
|
|
2129
|
+
});
|
|
2130
|
+
}
|
|
1513
2131
|
}
|
|
1514
|
-
return callMcpTool(tool, args, { errorHints: ASK_ERROR_HINTS });
|
|
1515
2132
|
}
|
|
1516
2133
|
|
|
1517
2134
|
// src/cli/lib/home.ts
|
|
2135
|
+
import * as p from "@clack/prompts";
|
|
2136
|
+
import pc2 from "picocolors";
|
|
1518
2137
|
function mark(ok2) {
|
|
1519
2138
|
return ok2 ? pc2.green("\u25CF") : pc2.yellow("\u25CB");
|
|
1520
2139
|
}
|
|
@@ -4254,171 +4873,6 @@ function capabilityTarget(cmd) {
|
|
|
4254
4873
|
// src/cli/lib/capability.ts
|
|
4255
4874
|
import * as p2 from "@clack/prompts";
|
|
4256
4875
|
import pc3 from "picocolors";
|
|
4257
|
-
|
|
4258
|
-
// src/cli/lib/rest.ts
|
|
4259
|
-
var DEFAULT_ERROR_HINTS2 = [
|
|
4260
|
-
"Re-check the arguments: carrier <domain> <command> --help",
|
|
4261
|
-
"Or open Claude and ask there with full tool routing"
|
|
4262
|
-
];
|
|
4263
|
-
function resolveApiKey() {
|
|
4264
|
-
return process.env.CARRIER_API_KEY?.trim() || process.env.CARRIER_ORG_API_KEY?.trim() || null;
|
|
4265
|
-
}
|
|
4266
|
-
function ocsTokenOnly() {
|
|
4267
|
-
return Boolean(
|
|
4268
|
-
process.env.ESIMVAULT_API_TOKEN?.trim() || process.env.CARRIER_OCS_API_TOKEN?.trim()
|
|
4269
|
-
);
|
|
4270
|
-
}
|
|
4271
|
-
function applyPath(path, args) {
|
|
4272
|
-
const rest = { ...args };
|
|
4273
|
-
const missing = [];
|
|
4274
|
-
const filled = path.replace(/\{(\w+)\}/g, (_match, name) => {
|
|
4275
|
-
const value = rest[name];
|
|
4276
|
-
if (value === void 0 || value === null || value === "") {
|
|
4277
|
-
missing.push(name);
|
|
4278
|
-
return `{${name}}`;
|
|
4279
|
-
}
|
|
4280
|
-
delete rest[name];
|
|
4281
|
-
return encodeURIComponent(String(value));
|
|
4282
|
-
});
|
|
4283
|
-
return { path: filled, rest, missing };
|
|
4284
|
-
}
|
|
4285
|
-
function queryString(args) {
|
|
4286
|
-
const params = new URLSearchParams();
|
|
4287
|
-
for (const [key, value] of Object.entries(args)) {
|
|
4288
|
-
if (value === void 0 || value === null) continue;
|
|
4289
|
-
params.set(key, String(value));
|
|
4290
|
-
}
|
|
4291
|
-
const qs = params.toString();
|
|
4292
|
-
return qs ? `?${qs}` : "";
|
|
4293
|
-
}
|
|
4294
|
-
function bodyText(raw) {
|
|
4295
|
-
const trimmed = raw.trim();
|
|
4296
|
-
if (!trimmed) return "";
|
|
4297
|
-
try {
|
|
4298
|
-
const parsed = JSON.parse(trimmed);
|
|
4299
|
-
if (typeof parsed.error === "string") return parsed.error;
|
|
4300
|
-
if (typeof parsed.message === "string") return parsed.message;
|
|
4301
|
-
return trimmed;
|
|
4302
|
-
} catch {
|
|
4303
|
-
return trimmed;
|
|
4304
|
-
}
|
|
4305
|
-
}
|
|
4306
|
-
async function callRestEndpoint(method, path, args, opts = {}) {
|
|
4307
|
-
const token = resolveApiKey() ?? await getAccessToken();
|
|
4308
|
-
if (!token) {
|
|
4309
|
-
return {
|
|
4310
|
-
ok: false,
|
|
4311
|
-
text: withNextStep(
|
|
4312
|
-
ocsTokenOnly() ? "Not signed in. An eSIMVault/OCS token is set, but the Carrier API only accepts Carrier credentials." : "Not signed in.",
|
|
4313
|
-
[
|
|
4314
|
-
"Sign in from this terminal: carrier login",
|
|
4315
|
-
"No account yet? carrier open signup",
|
|
4316
|
-
"Headless/CI instead: export CARRIER_API_KEY=ak_\u2026 from Console \u2192 Settings \u2192 API Keys"
|
|
4317
|
-
]
|
|
4318
|
-
)
|
|
4319
|
-
};
|
|
4320
|
-
}
|
|
4321
|
-
const { path: resolvedPath, rest, missing } = applyPath(path, args);
|
|
4322
|
-
if (missing.length) {
|
|
4323
|
-
return {
|
|
4324
|
-
ok: false,
|
|
4325
|
-
text: withNextStep(`Cannot build ${method} ${path}.`, [
|
|
4326
|
-
`Missing path value(s): ${missing.join(", ")}`,
|
|
4327
|
-
"See every flag: carrier <domain> <command> --help"
|
|
4328
|
-
])
|
|
4329
|
-
};
|
|
4330
|
-
}
|
|
4331
|
-
const sendsBody = method === "POST" || method === "PUT" || method === "PATCH";
|
|
4332
|
-
const url = `${API_URL}${resolvedPath}${sendsBody ? "" : queryString(rest)}`;
|
|
4333
|
-
const headers = {
|
|
4334
|
-
Authorization: `Bearer ${token}`,
|
|
4335
|
-
Accept: "application/json",
|
|
4336
|
-
"User-Agent": `carrier-cli/${CARRIER_VERSION}`
|
|
4337
|
-
};
|
|
4338
|
-
if (sendsBody) headers["Content-Type"] = "application/json";
|
|
4339
|
-
if (opts.confirmToken) headers["X-Confirm-Token"] = opts.confirmToken;
|
|
4340
|
-
const timeoutMs = mcpTimeoutMs();
|
|
4341
|
-
const controller = new AbortController();
|
|
4342
|
-
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
4343
|
-
try {
|
|
4344
|
-
const res = await fetch(url, {
|
|
4345
|
-
method,
|
|
4346
|
-
headers,
|
|
4347
|
-
signal: controller.signal,
|
|
4348
|
-
body: sendsBody ? JSON.stringify(rest) : void 0
|
|
4349
|
-
});
|
|
4350
|
-
const raw = await res.text();
|
|
4351
|
-
if (res.status === 202) {
|
|
4352
|
-
let confirmToken;
|
|
4353
|
-
let expires;
|
|
4354
|
-
let message = "";
|
|
4355
|
-
try {
|
|
4356
|
-
const parsed = JSON.parse(raw);
|
|
4357
|
-
confirmToken = parsed.confirm_token;
|
|
4358
|
-
expires = parsed.expires_in_seconds;
|
|
4359
|
-
message = parsed.message ?? "";
|
|
4360
|
-
} catch {
|
|
4361
|
-
}
|
|
4362
|
-
return {
|
|
4363
|
-
ok: false,
|
|
4364
|
-
text: withNextStep(
|
|
4365
|
-
message || `${method} ${path} needs an explicit confirmation.`,
|
|
4366
|
-
confirmToken ? [
|
|
4367
|
-
`Re-run the same command with: --confirm-token ${confirmToken}`,
|
|
4368
|
-
`The token is single-use and expires in ${expires ?? 120}s.`
|
|
4369
|
-
] : ["The server asked for confirmation but returned no token \u2014 retry the command."]
|
|
4370
|
-
)
|
|
4371
|
-
};
|
|
4372
|
-
}
|
|
4373
|
-
if (res.status === 401 || res.status === 403) {
|
|
4374
|
-
return {
|
|
4375
|
-
ok: false,
|
|
4376
|
-
text: withNextStep(
|
|
4377
|
-
`Carrier API returned ${res.status} (auth rejected).`,
|
|
4378
|
-
[
|
|
4379
|
-
"Confirm CARRIER_API_KEY is a valid org key (ak_\u2026) from app.carrier.llc",
|
|
4380
|
-
"Or sign in from this terminal: carrier login",
|
|
4381
|
-
...res.status === 403 ? ["A 403 here usually means the key lacks write scope."] : []
|
|
4382
|
-
]
|
|
4383
|
-
)
|
|
4384
|
-
};
|
|
4385
|
-
}
|
|
4386
|
-
if (!res.ok) {
|
|
4387
|
-
return {
|
|
4388
|
-
ok: false,
|
|
4389
|
-
text: withNextStep(
|
|
4390
|
-
redact(`${method} ${path} failed (${res.status}). ${bodyText(raw).slice(0, 500)}`),
|
|
4391
|
-
opts.errorHints ?? DEFAULT_ERROR_HINTS2
|
|
4392
|
-
)
|
|
4393
|
-
};
|
|
4394
|
-
}
|
|
4395
|
-
return { ok: true, text: raw.trim() || JSON.stringify({ status: res.status }) };
|
|
4396
|
-
} catch (e) {
|
|
4397
|
-
const aborted = e instanceof Error && (e.name === "AbortError" || e.name === "TimeoutError");
|
|
4398
|
-
if (aborted) {
|
|
4399
|
-
const secs = Math.round(timeoutMs / 1e3);
|
|
4400
|
-
return {
|
|
4401
|
-
ok: false,
|
|
4402
|
-
text: withNextStep(`${method} ${path} timed out after ${secs}s (no response).`, [
|
|
4403
|
-
`Raise the deadline for one slow call: CARRIER_MCP_TIMEOUT_MS=${timeoutMs * 2} carrier \u2026`,
|
|
4404
|
-
"Confirm the service is up: check network access to https://api.carrier.llc"
|
|
4405
|
-
])
|
|
4406
|
-
};
|
|
4407
|
-
}
|
|
4408
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
4409
|
-
return {
|
|
4410
|
-
ok: false,
|
|
4411
|
-
text: withNextStep(redact(`Could not reach the Carrier API: ${msg}`), [
|
|
4412
|
-
"Verify outbound HTTPS to api.carrier.llc",
|
|
4413
|
-
"Use the interactive Claude path while offline from this machine"
|
|
4414
|
-
])
|
|
4415
|
-
};
|
|
4416
|
-
} finally {
|
|
4417
|
-
clearTimeout(timer);
|
|
4418
|
-
}
|
|
4419
|
-
}
|
|
4420
|
-
|
|
4421
|
-
// src/cli/lib/capability.ts
|
|
4422
4876
|
var MAX_PRETTY_CHARS = 8e3;
|
|
4423
4877
|
function optionKey(flags) {
|
|
4424
4878
|
const long = flags.split(/[ ,|]+/).find((t) => t.startsWith("--"));
|
|
@@ -5530,6 +5984,22 @@ site.command("deploy [dir]").description("Build + deploy a storefront to whichev
|
|
|
5530
5984
|
p3.outro("");
|
|
5531
5985
|
}
|
|
5532
5986
|
);
|
|
5987
|
+
site.command("publish").description("Publish a Carrier-hosted storefront. Does not deploy a self-hosted copy.").requiredOption("--slug <slug>", "Shop address, such as acme or Acme Mobile").requiredOption("--name <name>", "Brand name").option("--status <status>", "draft or published", "published").option("--accent <hex>", "Accent color, such as #112233").option("--accent-dark <hex>", "Darker accent").option("--bg <hex>", "Page background").option("--text <hex>", "Body text color").option("--logo <url-or-file>", "https logo address, or a PNG, JPEG, WebP, or GIF under 70 KB").option("--hero <text>", "Shop headline").option("--tagline <text>", "Line under the headline").option("--support-email <email>", "Support address").option("--legal-name <name>", "Legal name in the footer").option("--product <id>", "Connectivity plan id. Copies its template, price, and allowance.").option("--plan <slug>", "Plan slug, such as eu-1gb").option("--plan-name <name>", "Plan name on the shop").option("--price <amount>", "Retail price, such as 9.00").option("--price-cents <cents>", "Retail price in minor units").option("--currency <code>", "Three-letter currency code").option("--template-id <id>", "OCS package template id").option("--data-gb <gb>", "Data allowance, such as 1 or 1 GB").option("--validity-days <days>", "Validity, such as 7 or 7 days").option("--countries <names>", "Country names or codes, such as France, DE").option("--plan-text <text>", "Buyer-facing sentence. Omit to leave the saved text unchanged.").option("--inactive", "Keep the plan off the shop").action(async (o) => {
|
|
5988
|
+
header();
|
|
5989
|
+
const built = hostedPublishInputFromFlags(o);
|
|
5990
|
+
if (!built.ok) {
|
|
5991
|
+
fail(built.text, ["Re-run with --status published"]);
|
|
5992
|
+
process.exitCode = 1;
|
|
5993
|
+
return;
|
|
5994
|
+
}
|
|
5995
|
+
const result = await publishHostedStorefront(built.input);
|
|
5996
|
+
if (!result.ok) {
|
|
5997
|
+
fail(result.text, ["Sign in: carrier login", "Or export CARRIER_API_KEY from the console"]);
|
|
5998
|
+
process.exitCode = 1;
|
|
5999
|
+
return;
|
|
6000
|
+
}
|
|
6001
|
+
ok(result.text);
|
|
6002
|
+
});
|
|
5533
6003
|
site.command("targets [dir]").description("Show which deploy hosts are installed, logged in, and ready.").action(async (dir) => {
|
|
5534
6004
|
header();
|
|
5535
6005
|
const storefront = resolve2(dir ?? "./storefront");
|