@m-kopa/launchpad-cli 0.26.1 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -19,21 +19,33 @@ var __toESM = (mod, isNodeMode, target) => {
19
19
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
20
20
 
21
21
  // src/version.ts
22
- var CLI_VERSION = "0.26.1";
22
+ var CLI_VERSION = "0.27.1";
23
23
 
24
24
  // src/config.ts
25
25
  import * as os from "node:os";
26
26
  import * as path from "node:path";
27
27
  var DEFAULT_BOT_URL = "https://launchpad-portal-bot.mkopa-launchpad.workers.dev";
28
+ var DEFAULT_AUTH_GATEWAY_URL = "https://auth.launchpad.m-kopa.us";
28
29
  function loadConfig(env = process.env) {
29
30
  const rawBotUrl = env.LAUNCHPAD_BOT_URL ?? DEFAULT_BOT_URL;
30
31
  const botUrl = rawBotUrl.replace(/\/+$/, "");
32
+ const rawGatewayUrl = env.LAUNCHPAD_AUTH_GATEWAY_URL ?? DEFAULT_AUTH_GATEWAY_URL;
33
+ const authGatewayUrl = rawGatewayUrl.replace(/\/+$/, "");
34
+ const authLegacy = env.LAUNCHPAD_AUTH_LEGACY === "1";
31
35
  const sessionPath = env.LAUNCHPAD_SESSION_PATH ?? path.join(os.homedir(), ".launchpad", "session.json");
32
36
  const cacheDir = env.LAUNCHPAD_CACHE_DIR ?? path.join(os.homedir(), ".launchpad", "cache");
33
37
  const stateDir = env.LAUNCHPAD_STATE_DIR ?? path.join(os.homedir(), ".launchpad", "state");
34
38
  const rawPlatformRepo = env.LAUNCHPAD_PLATFORM_REPO;
35
39
  const platformRepoPath = rawPlatformRepo !== undefined && rawPlatformRepo.length > 0 ? path.resolve(rawPlatformRepo) : null;
36
- return { botUrl, sessionPath, cacheDir, stateDir, platformRepoPath };
40
+ return {
41
+ botUrl,
42
+ authGatewayUrl,
43
+ authLegacy,
44
+ sessionPath,
45
+ cacheDir,
46
+ stateDir,
47
+ platformRepoPath
48
+ };
37
49
  }
38
50
 
39
51
  // src/http/errors.ts
@@ -64,7 +76,7 @@ class TransportError extends Error {
64
76
  }
65
77
 
66
78
  // src/auth/flow.ts
67
- import { randomBytes as randomBytes3 } from "node:crypto";
79
+ import { randomBytes as randomBytes4 } from "node:crypto";
68
80
 
69
81
  // src/auth/callback-server.ts
70
82
  import { createServer } from "node:http";
@@ -371,6 +383,10 @@ import * as fs from "node:fs/promises";
371
383
  import * as path2 from "node:path";
372
384
  import { randomBytes as randomBytes2 } from "node:crypto";
373
385
  var SESSION_VERSION = 1;
386
+ var GATEWAY_SESSION_VERSION = 2;
387
+ function isGatewaySession(s) {
388
+ return s.version === GATEWAY_SESSION_VERSION;
389
+ }
374
390
 
375
391
  class SessionParseError extends Error {
376
392
  code = "session_parse_error";
@@ -394,8 +410,11 @@ async function readSession(sessionPath) {
394
410
  throw new SessionParseError(`session file at ${sessionPath} is not an object`);
395
411
  }
396
412
  const obj = parsed;
413
+ if (obj.version === GATEWAY_SESSION_VERSION) {
414
+ return parseGatewaySession(obj, sessionPath);
415
+ }
397
416
  if (obj.version !== SESSION_VERSION) {
398
- throw new SessionParseError(`unsupported session version ${String(obj.version)} at ${sessionPath}; expected ${SESSION_VERSION}`);
417
+ throw new SessionParseError(`unsupported session version ${String(obj.version)} at ${sessionPath}; expected ${SESSION_VERSION} or ${GATEWAY_SESSION_VERSION}`);
399
418
  }
400
419
  for (const k of [
401
420
  "accessToken",
@@ -416,6 +435,20 @@ async function readSession(sessionPath) {
416
435
  }
417
436
  return obj;
418
437
  }
438
+ function parseGatewaySession(obj, sessionPath) {
439
+ if (obj.kind !== "gateway") {
440
+ throw new SessionParseError(`session file at ${sessionPath}: version 2 with unknown kind ${String(obj.kind)}`);
441
+ }
442
+ for (const k of ["accessToken", "refreshToken", "gatewayUrl", "issuedAt"]) {
443
+ if (typeof obj[k] !== "string") {
444
+ throw new SessionParseError(`session file at ${sessionPath}: missing or non-string ${k}`);
445
+ }
446
+ }
447
+ if (typeof obj.accessTokenExpiresAt !== "number") {
448
+ throw new SessionParseError(`session file at ${sessionPath}: missing or non-number accessTokenExpiresAt`);
449
+ }
450
+ return obj;
451
+ }
419
452
  async function writeSession(sessionPath, session) {
420
453
  const dir = path2.dirname(sessionPath);
421
454
  await fs.mkdir(dir, { recursive: true, mode: 448 });
@@ -453,6 +486,9 @@ function describe4(e) {
453
486
  return e instanceof Error ? e.message : String(e);
454
487
  }
455
488
 
489
+ // src/auth/gateway-flow.ts
490
+ import { randomBytes as randomBytes3 } from "node:crypto";
491
+
456
492
  // src/auth/browser.ts
457
493
  import { spawn } from "node:child_process";
458
494
 
@@ -487,16 +523,185 @@ function chooseOpener(platform, url) {
487
523
  }
488
524
  }
489
525
 
526
+ // src/auth/gateway-flow.ts
527
+ var CLI_SESSION_AUTH_PATH = "/__cli_session_auth";
528
+ var CLI_SESSION_TOKEN_PATH = "/__cli_session_token";
529
+ var CLI_LOGOUT_PATH = "/__cli_logout";
530
+
531
+ class GatewayUnavailableError extends Error {
532
+ code = "gateway_unavailable";
533
+ }
534
+
535
+ class GatewayTokenError extends Error {
536
+ code = "gateway_token_error";
537
+ httpStatus;
538
+ constructor(message, httpStatus) {
539
+ super(message);
540
+ this.name = "GatewayTokenError";
541
+ if (httpStatus !== undefined)
542
+ this.httpStatus = httpStatus;
543
+ }
544
+ }
545
+
546
+ class GatewayRateLimitError extends Error {
547
+ code = "gateway_rate_limited";
548
+ retryAfterSec;
549
+ constructor(retryAfterSec) {
550
+ super(`gateway rate-limited the request — retry in ${retryAfterSec}s (your session is intact)`);
551
+ this.name = "GatewayRateLimitError";
552
+ this.retryAfterSec = retryAfterSec;
553
+ }
554
+ }
555
+ async function gatewayLogin(opts) {
556
+ const fetcher = opts.fetcher ?? fetch;
557
+ const opener = opts.browserOpener ?? ((url) => openBrowser(url));
558
+ await probeGateway(opts.gatewayUrl, fetcher);
559
+ const state = randomBytes3(16).toString("hex");
560
+ const server = await bindCallbackServer(state);
561
+ try {
562
+ const redirectUri = `http://127.0.0.1:${server.port}/callback`;
563
+ const pkce = generatePkcePair();
564
+ const u = new URL(`${opts.gatewayUrl}${CLI_SESSION_AUTH_PATH}`);
565
+ u.searchParams.set("cb", redirectUri);
566
+ u.searchParams.set("code_challenge", pkce.challenge);
567
+ u.searchParams.set("state", state);
568
+ const authUrl = u.toString();
569
+ opts.onAuthUrl?.(authUrl);
570
+ try {
571
+ await opener(authUrl);
572
+ } catch (e) {
573
+ opts.onAuthUrl?.(`(could not auto-open browser: ${describe5(e)} — copy the URL above into a browser instead)`);
574
+ }
575
+ const callback = await server.result;
576
+ const pair = await postSessionTokenForm(opts.gatewayUrl, new URLSearchParams({
577
+ grant_type: "authorization_code",
578
+ code: callback.code,
579
+ code_verifier: pkce.verifier
580
+ }), fetcher);
581
+ const session = {
582
+ version: GATEWAY_SESSION_VERSION,
583
+ kind: "gateway",
584
+ accessToken: pair.accessToken,
585
+ refreshToken: pair.refreshToken,
586
+ accessTokenExpiresAt: Date.now() + pair.expiresInSec * 1000,
587
+ gatewayUrl: opts.gatewayUrl,
588
+ issuedAt: new Date().toISOString()
589
+ };
590
+ await writeSession(opts.sessionPath, session);
591
+ return session;
592
+ } finally {
593
+ await server.close();
594
+ }
595
+ }
596
+ async function refreshGatewayTokens(params, fetcher = fetch) {
597
+ return postSessionTokenForm(params.gatewayUrl, new URLSearchParams({
598
+ grant_type: "refresh_token",
599
+ refresh_token: params.refreshToken
600
+ }), fetcher);
601
+ }
602
+ async function revokeGatewaySession(params, fetcher = fetch) {
603
+ const url = `${params.gatewayUrl}${CLI_LOGOUT_PATH}`;
604
+ let res;
605
+ try {
606
+ res = await fetcher(url, {
607
+ method: "POST",
608
+ headers: { "content-type": "application/x-www-form-urlencoded" },
609
+ body: new URLSearchParams({ refresh_token: params.refreshToken }).toString()
610
+ });
611
+ } catch (e) {
612
+ throw new GatewayTokenError(`logout endpoint: network error: ${describe5(e)}`);
613
+ }
614
+ if (res.status === 429) {
615
+ throw new GatewayRateLimitError(readRetryAfter(res));
616
+ }
617
+ if (res.status !== 204 && !res.ok) {
618
+ const detail = await res.text().catch(() => "");
619
+ throw new GatewayTokenError(`logout endpoint returned HTTP ${res.status}: ${detail.slice(0, 200)}`, res.status);
620
+ }
621
+ }
622
+ async function probeGateway(gatewayUrl, fetcher) {
623
+ const url = `${gatewayUrl}${CLI_SESSION_AUTH_PATH}`;
624
+ let res;
625
+ try {
626
+ res = await fetcher(url, { method: "GET" });
627
+ } catch (e) {
628
+ throw new GatewayUnavailableError(`auth gateway unreachable at ${gatewayUrl}: ${describe5(e)}`);
629
+ }
630
+ if (res.status === 429) {
631
+ throw new GatewayRateLimitError(readRetryAfter(res));
632
+ }
633
+ if (res.status !== 400) {
634
+ throw new GatewayUnavailableError(`auth gateway at ${gatewayUrl} is not serving the cli-session grant (HTTP ${res.status} from ${CLI_SESSION_AUTH_PATH})`);
635
+ }
636
+ }
637
+ async function postSessionTokenForm(gatewayUrl, form, fetcher) {
638
+ const url = `${gatewayUrl}${CLI_SESSION_TOKEN_PATH}`;
639
+ let res;
640
+ try {
641
+ res = await fetcher(url, {
642
+ method: "POST",
643
+ headers: {
644
+ "content-type": "application/x-www-form-urlencoded",
645
+ accept: "application/json"
646
+ },
647
+ body: form.toString()
648
+ });
649
+ } catch (e) {
650
+ throw new GatewayTokenError(`session token endpoint: network error: ${describe5(e)}`);
651
+ }
652
+ if (res.status === 429) {
653
+ throw new GatewayRateLimitError(readRetryAfter(res));
654
+ }
655
+ if (!res.ok) {
656
+ const detail = await res.text().catch(() => "");
657
+ throw new GatewayTokenError(`session token endpoint returned HTTP ${res.status}: ${detail.slice(0, 200)}`, res.status);
658
+ }
659
+ let parsed;
660
+ try {
661
+ parsed = await res.json();
662
+ } catch (e) {
663
+ throw new GatewayTokenError(`session token endpoint: non-JSON response: ${describe5(e)}`);
664
+ }
665
+ if (parsed === null || typeof parsed !== "object") {
666
+ throw new GatewayTokenError(`session token endpoint: response is not an object`);
667
+ }
668
+ const obj = parsed;
669
+ if (typeof obj.access_token !== "string") {
670
+ throw new GatewayTokenError(`session token endpoint: response missing access_token`);
671
+ }
672
+ if (typeof obj.refresh_token !== "string") {
673
+ throw new GatewayTokenError(`session token endpoint: response missing refresh_token`);
674
+ }
675
+ const expiresIn = obj.expires_in;
676
+ if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
677
+ throw new GatewayTokenError(`session token endpoint: response missing positive finite numeric expires_in (got ${String(expiresIn)})`);
678
+ }
679
+ return {
680
+ accessToken: obj.access_token,
681
+ refreshToken: obj.refresh_token,
682
+ expiresInSec: expiresIn
683
+ };
684
+ }
685
+ function readRetryAfter(res) {
686
+ const raw = res.headers.get("retry-after");
687
+ const n = raw === null ? NaN : Number.parseInt(raw, 10);
688
+ return Number.isFinite(n) && n > 0 ? n : 60;
689
+ }
690
+ function describe5(e) {
691
+ return e instanceof Error ? e.message : String(e);
692
+ }
693
+
490
694
  // src/auth/flow.ts
491
695
  class LoginRequiredError extends Error {
492
696
  code = "login_required";
493
697
  }
494
698
  var REFRESH_SKEW_MS = 30000;
699
+ var MAX_ABSORBED_RETRY_AFTER_SEC = 10;
495
700
  async function login(opts) {
496
701
  const fetcher = opts.fetcher ?? fetch;
497
702
  const opener = opts.browserOpener ?? ((url) => openBrowser(url));
498
703
  const endpoints = await discoverOauthEndpoints(opts.botUrl, fetcher);
499
- const state = randomBytes3(16).toString("hex");
704
+ const state = randomBytes4(16).toString("hex");
500
705
  const server = await bindCallbackServer(state);
501
706
  try {
502
707
  const redirectUri = `http://127.0.0.1:${server.port}/callback`;
@@ -514,7 +719,7 @@ async function login(opts) {
514
719
  try {
515
720
  await opener(authUrl);
516
721
  } catch (e) {
517
- opts.onAuthUrl?.(`(could not auto-open browser: ${describe5(e)} — copy the URL above into a browser instead)`);
722
+ opts.onAuthUrl?.(`(could not auto-open browser: ${describe6(e)} — copy the URL above into a browser instead)`);
518
723
  }
519
724
  const callback = await server.result;
520
725
  const tokens = await exchangeCodeForTokens({
@@ -541,7 +746,7 @@ async function login(opts) {
541
746
  await server.close();
542
747
  }
543
748
  }
544
- async function getValidAccessToken(sessionPath, fetcher = fetch, now = Date.now) {
749
+ async function getValidAccessToken(sessionPath, fetcher = fetch, now = Date.now, sleep = (ms) => new Promise((r) => setTimeout(r, ms))) {
545
750
  const session = await readSession(sessionPath);
546
751
  if (session === null) {
547
752
  throw new LoginRequiredError("no session — run `launchpad login`");
@@ -549,6 +754,9 @@ async function getValidAccessToken(sessionPath, fetcher = fetch, now = Date.now)
549
754
  if (session.accessTokenExpiresAt - REFRESH_SKEW_MS > now()) {
550
755
  return { accessToken: session.accessToken, session };
551
756
  }
757
+ if (isGatewaySession(session)) {
758
+ return refreshGatewaySession(session, sessionPath, fetcher, now, sleep);
759
+ }
552
760
  if (session.resource === undefined) {
553
761
  throw new LoginRequiredError("session was written by a pre-0.7.1 CLI and is missing the resource indicator — run `launchpad login`");
554
762
  }
@@ -576,7 +784,39 @@ async function getValidAccessToken(sessionPath, fetcher = fetch, now = Date.now)
576
784
  await writeSession(sessionPath, refreshed);
577
785
  return { accessToken: refreshed.accessToken, session: refreshed };
578
786
  }
579
- function describe5(e) {
787
+ async function refreshGatewaySession(session, sessionPath, fetcher, now, sleep) {
788
+ let pair;
789
+ try {
790
+ try {
791
+ pair = await refreshGatewayTokens({ gatewayUrl: session.gatewayUrl, refreshToken: session.refreshToken }, fetcher);
792
+ } catch (e) {
793
+ if (e instanceof GatewayRateLimitError && e.retryAfterSec <= MAX_ABSORBED_RETRY_AFTER_SEC) {
794
+ await sleep(e.retryAfterSec * 1000);
795
+ pair = await refreshGatewayTokens({ gatewayUrl: session.gatewayUrl, refreshToken: session.refreshToken }, fetcher);
796
+ } else {
797
+ throw e;
798
+ }
799
+ }
800
+ } catch (e) {
801
+ if (e instanceof GatewayTokenError && (e.httpStatus === 400 || e.httpStatus === 401)) {
802
+ await clearSession(sessionPath).catch(() => {
803
+ return;
804
+ });
805
+ throw new LoginRequiredError(`session revoked or expired — run \`launchpad login\` (gateway said: ${e.message})`);
806
+ }
807
+ throw e;
808
+ }
809
+ const refreshed = {
810
+ ...session,
811
+ accessToken: pair.accessToken,
812
+ refreshToken: pair.refreshToken,
813
+ accessTokenExpiresAt: now() + pair.expiresInSec * 1000,
814
+ issuedAt: new Date(now()).toISOString()
815
+ };
816
+ await writeSession(sessionPath, refreshed);
817
+ return { accessToken: refreshed.accessToken, session: refreshed };
818
+ }
819
+ function describe6(e) {
580
820
  return e instanceof Error ? e.message : String(e);
581
821
  }
582
822
  function buildAuthorizationUrl(params) {
@@ -598,7 +838,7 @@ async function apiJson(cfg, opts, fetcher = fetch) {
598
838
  try {
599
839
  parsed = await res.json();
600
840
  } catch (e) {
601
- throw new TransportError(`bot returned non-JSON body for ${opts.path}: ${describe6(e)}`);
841
+ throw new TransportError(`bot returned non-JSON body for ${opts.path}: ${describe7(e)}`);
602
842
  }
603
843
  return parsed;
604
844
  }
@@ -640,7 +880,7 @@ async function apiRaw(cfg, opts, fetcher = fetch) {
640
880
  try {
641
881
  res = await fetcher(url, init);
642
882
  } catch (e) {
643
- throw new TransportError(`network error calling ${url}: ${describe6(e)}`);
883
+ throw new TransportError(`network error calling ${url}: ${describe7(e)}`);
644
884
  }
645
885
  if (res.status === 401) {
646
886
  const detail = await peek(res);
@@ -671,7 +911,7 @@ async function peek(res) {
671
911
  return "";
672
912
  }
673
913
  }
674
- function describe6(e) {
914
+ function describe7(e) {
675
915
  return e instanceof Error ? e.message : String(e);
676
916
  }
677
917
 
@@ -701,7 +941,7 @@ async function runApps(_args, io) {
701
941
  io.err(e.message);
702
942
  return 1;
703
943
  }
704
- io.err(`launchpad apps failed: ${describe7(e)}`);
944
+ io.err(`launchpad apps failed: ${describe8(e)}`);
705
945
  return 1;
706
946
  }
707
947
  }
@@ -749,7 +989,7 @@ function formatRelative(iso) {
749
989
  return `${hr}h ago`;
750
990
  return `${Math.floor(hr / 24)}d ago`;
751
991
  }
752
- function describe7(e) {
992
+ function describe8(e) {
753
993
  return e instanceof Error ? e.message : String(e);
754
994
  }
755
995
 
@@ -1046,7 +1286,7 @@ async function runClone(args, io) {
1046
1286
  io.err(`launchpad clone: ${e.message}`);
1047
1287
  return 1;
1048
1288
  }
1049
- io.err(`launchpad clone failed: ${describe8(e)}`);
1289
+ io.err(`launchpad clone failed: ${describe9(e)}`);
1050
1290
  return 1;
1051
1291
  }
1052
1292
  }
@@ -1063,7 +1303,7 @@ function formatBytes(n) {
1063
1303
  return `${(n / 1024).toFixed(1)}KB`;
1064
1304
  return `${(n / (1024 * 1024)).toFixed(1)}MB`;
1065
1305
  }
1066
- function describe8(e) {
1306
+ function describe9(e) {
1067
1307
  return e instanceof Error ? e.message : String(e);
1068
1308
  }
1069
1309
 
@@ -1129,7 +1369,7 @@ async function runCreate(args, io) {
1129
1369
  io.err(`launchpad create: ${e.message}`);
1130
1370
  return 1;
1131
1371
  }
1132
- io.err(`launchpad create failed: ${describe9(e)}`);
1372
+ io.err(`launchpad create failed: ${describe10(e)}`);
1133
1373
  return 1;
1134
1374
  }
1135
1375
  }
@@ -1214,7 +1454,7 @@ function printUsage(io) {
1214
1454
  ].join(`
1215
1455
  `));
1216
1456
  }
1217
- function describe9(e) {
1457
+ function describe10(e) {
1218
1458
  return e instanceof Error ? e.message : String(e);
1219
1459
  }
1220
1460
 
@@ -3882,7 +4122,7 @@ async function pollUntilApplied(args) {
3882
4122
  path: `/apps/${args.slug}/manifest/state`
3883
4123
  }, args.fetcher);
3884
4124
  } catch (e) {
3885
- args.io.err(`! state fetch failed (will retry): ${describe10(e)}`);
4125
+ args.io.err(`! state fetch failed (will retry): ${describe11(e)}`);
3886
4126
  await sleep(args.pollIntervalSec * 1000);
3887
4127
  continue;
3888
4128
  }
@@ -3942,7 +4182,7 @@ function deletePinIfPresent(cfg, slug, io) {
3942
4182
  rmSync(pinPath, { force: true });
3943
4183
  io.out(` (cleaned up obsolete pin file ${pinPath})`);
3944
4184
  } catch (e) {
3945
- io.err(`! warning: failed to delete obsolete pin file ${pinPath}: ${describe10(e)}`);
4185
+ io.err(`! warning: failed to delete obsolete pin file ${pinPath}: ${describe11(e)}`);
3946
4186
  }
3947
4187
  }
3948
4188
  async function loadSlugForResume(opts, io) {
@@ -3966,7 +4206,7 @@ async function defaultPrompt(question) {
3966
4206
  rl.close();
3967
4207
  }
3968
4208
  }
3969
- function describe10(e) {
4209
+ function describe11(e) {
3970
4210
  return e instanceof Error ? e.message : String(e);
3971
4211
  }
3972
4212
  function mapHttpError(e, slug, io) {
@@ -3990,7 +4230,7 @@ function mapHttpError(e, slug, io) {
3990
4230
  io.err(`launchpad deploy --apply: ${e.message}`);
3991
4231
  return 1;
3992
4232
  }
3993
- io.err(`launchpad deploy --apply failed: ${describe10(e)}`);
4233
+ io.err(`launchpad deploy --apply failed: ${describe11(e)}`);
3994
4234
  return 2;
3995
4235
  }
3996
4236
  function renderManifestError(loaded, io) {
@@ -4032,7 +4272,7 @@ async function runDeployDryRun(opts, io, deps = {}) {
4032
4272
  try {
4033
4273
  manifestSha = (deps.gitHeadSha ?? defaultGitHeadSha)(process.cwd());
4034
4274
  } catch (e) {
4035
- const msg = `failed to resolve git HEAD in ${process.cwd()}: ${describe11(e)}`;
4275
+ const msg = `failed to resolve git HEAD in ${process.cwd()}: ${describe12(e)}`;
4036
4276
  if (opts.json) {
4037
4277
  io.out(JSON.stringify({ ok: false, kind: "git-error", message: msg }));
4038
4278
  } else {
@@ -4081,7 +4321,7 @@ function defaultGitHeadSha(cwd) {
4081
4321
  });
4082
4322
  return out.trim();
4083
4323
  }
4084
- function describe11(e) {
4324
+ function describe12(e) {
4085
4325
  return e instanceof Error ? e.message : String(e);
4086
4326
  }
4087
4327
  function mapHttpError2(e, slug, json, io) {
@@ -4570,7 +4810,7 @@ async function runDeploy(args, io) {
4570
4810
  io.err(`launchpad deploy: ${e.message}`);
4571
4811
  return 1;
4572
4812
  }
4573
- io.err(`launchpad deploy failed: ${describe12(e)}`);
4813
+ io.err(`launchpad deploy failed: ${describe13(e)}`);
4574
4814
  return 1;
4575
4815
  }
4576
4816
  }
@@ -4642,7 +4882,7 @@ function formatBytes2(n) {
4642
4882
  return `${(n / 1024).toFixed(1)}KB`;
4643
4883
  return `${(n / (1024 * 1024)).toFixed(1)}MB`;
4644
4884
  }
4645
- function describe12(e) {
4885
+ function describe13(e) {
4646
4886
  return e instanceof Error ? e.message : String(e);
4647
4887
  }
4648
4888
  function surfaceDeployExtras(body, io, slug) {
@@ -4678,7 +4918,7 @@ async function runModelADeploy(args) {
4678
4918
  }
4679
4919
  slug = metaSlug;
4680
4920
  } catch (e) {
4681
- io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe12(e)}`);
4921
+ io.err(`launchpad deploy: failed to read ${manifestPath}: ${describe13(e)}`);
4682
4922
  return 1;
4683
4923
  }
4684
4924
  if (!SLUG_RE4.test(slug)) {
@@ -4690,7 +4930,7 @@ async function runModelADeploy(args) {
4690
4930
  try {
4691
4931
  cfg = loadConfig();
4692
4932
  } catch (e) {
4693
- io.err(`launchpad deploy: ${describe12(e)}`);
4933
+ io.err(`launchpad deploy: ${describe13(e)}`);
4694
4934
  return 1;
4695
4935
  }
4696
4936
  let result;
@@ -4702,7 +4942,7 @@ async function runModelADeploy(args) {
4702
4942
  io.err(" run `launchpad login` to refresh your session.");
4703
4943
  return 1;
4704
4944
  }
4705
- io.err(`launchpad deploy: unexpected error: ${describe12(e)}`);
4945
+ io.err(`launchpad deploy: unexpected error: ${describe13(e)}`);
4706
4946
  return 1;
4707
4947
  }
4708
4948
  switch (result.kind) {
@@ -4777,9 +5017,12 @@ async function runModelADeploy(args) {
4777
5017
  io.out("");
4778
5018
  io.out(message);
4779
5019
  io.out("");
5020
+ io.out(`Your bundle ships with this provisioning run — when lifecycle reaches "live",`);
5021
+ io.out("this deploy's content is what's serving. No second deploy needed.");
5022
+ io.out("");
4780
5023
  io.out("Next steps:");
4781
5024
  io.out(` launchpad status ${slug} # watch lifecycle (provisioning → live)`);
4782
- io.out(` launchpad deploy # re-run once lifecycle is "live"`);
5025
+ io.out(` launchpad deploy # only if the app comes up live WITHOUT your content (rare)`);
4783
5026
  return 0;
4784
5027
  }
4785
5028
  if (typeof success.commit_sha !== "string" || typeof success.repo !== "string") {
@@ -4896,7 +5139,7 @@ async function runEnvvars(args, io) {
4896
5139
  io.err(`launchpad envvars: ${e.message}`);
4897
5140
  return 1;
4898
5141
  }
4899
- io.err(`launchpad envvars failed: ${describe13(e)}`);
5142
+ io.err(`launchpad envvars failed: ${describe14(e)}`);
4900
5143
  return 1;
4901
5144
  }
4902
5145
  }
@@ -4993,7 +5236,7 @@ function renderList(envVars, io) {
4993
5236
  io.out(fmt(row));
4994
5237
  }
4995
5238
  }
4996
- function describe13(e) {
5239
+ function describe14(e) {
4997
5240
  return e instanceof Error ? e.message : String(e);
4998
5241
  }
4999
5242
 
@@ -5325,7 +5568,7 @@ async function fetchGroups(cfg, opts = {}) {
5325
5568
  if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
5326
5569
  throw e;
5327
5570
  }
5328
- return { kind: "error", message: describe14(e) };
5571
+ return { kind: "error", message: describe15(e) };
5329
5572
  }
5330
5573
  if (typeof response !== "object" || response === null || !Array.isArray(response.groups) || !response.groups.every(isEntraGroup)) {
5331
5574
  return {
@@ -5375,7 +5618,7 @@ function writeCache(path9, envelope) {
5375
5618
  writeFileSync2(path9, JSON.stringify(envelope), "utf8");
5376
5619
  } catch {}
5377
5620
  }
5378
- function describe14(e) {
5621
+ function describe15(e) {
5379
5622
  return e instanceof Error ? e.message : String(e);
5380
5623
  }
5381
5624
  var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
@@ -5413,13 +5656,13 @@ function decodeJwtPayload(token) {
5413
5656
  try {
5414
5657
  json = Buffer.from(b64UrlToB64(payload), "base64").toString("utf8");
5415
5658
  } catch (e) {
5416
- throw new JwtParseError(`could not base64-decode JWT payload: ${describe15(e)}`);
5659
+ throw new JwtParseError(`could not base64-decode JWT payload: ${describe16(e)}`);
5417
5660
  }
5418
5661
  let parsed;
5419
5662
  try {
5420
5663
  parsed = JSON.parse(json);
5421
5664
  } catch (e) {
5422
- throw new JwtParseError(`JWT payload is not JSON: ${describe15(e)}`);
5665
+ throw new JwtParseError(`JWT payload is not JSON: ${describe16(e)}`);
5423
5666
  }
5424
5667
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
5425
5668
  throw new JwtParseError(`JWT payload is not an object`);
@@ -5430,7 +5673,7 @@ function b64UrlToB64(s) {
5430
5673
  const padded = s.padEnd(s.length + (4 - s.length % 4) % 4, "=");
5431
5674
  return padded.replace(/-/g, "+").replace(/_/g, "/");
5432
5675
  }
5433
- function describe15(e) {
5676
+ function describe16(e) {
5434
5677
  return e instanceof Error ? e.message : String(e);
5435
5678
  }
5436
5679
 
@@ -5457,7 +5700,7 @@ async function runGroupsWhoami(args, io) {
5457
5700
  try {
5458
5701
  payload = decodeJwtPayload(session.accessToken);
5459
5702
  } catch (e) {
5460
- const message = e instanceof JwtParseError ? e.message : describe16(e);
5703
+ const message = e instanceof JwtParseError ? e.message : describe17(e);
5461
5704
  if (json) {
5462
5705
  io.out(JSON.stringify({ ok: false, kind: "jwt-parse-error", message }));
5463
5706
  } else {
@@ -5538,7 +5781,7 @@ function parseFlags2(args) {
5538
5781
  }
5539
5782
  return { kind: "ok", json };
5540
5783
  }
5541
- function describe16(e) {
5784
+ function describe17(e) {
5542
5785
  return e instanceof Error ? e.message : String(e);
5543
5786
  }
5544
5787
 
@@ -5641,7 +5884,7 @@ async function loadGroups(io, refresh, json) {
5641
5884
  }
5642
5885
  return { kind: "error", code: 3 };
5643
5886
  }
5644
- renderFetchError(io, describe17(e), json);
5887
+ renderFetchError(io, describe18(e), json);
5645
5888
  return { kind: "error", code: 2 };
5646
5889
  }
5647
5890
  }
@@ -5836,7 +6079,7 @@ function renderTable2(io, groups) {
5836
6079
  for (const r of rows)
5837
6080
  io.out(fmt(r));
5838
6081
  }
5839
- function describe17(e) {
6082
+ function describe18(e) {
5840
6083
  return e instanceof Error ? e.message : String(e);
5841
6084
  }
5842
6085
 
@@ -6472,56 +6715,111 @@ async function defaultPrompt2(question, fallback) {
6472
6715
  }
6473
6716
 
6474
6717
  // src/commands/login.ts
6475
- var loginCommand = {
6476
- name: "login",
6477
- summary: "authenticate (browser-based PKCE) and store a session",
6478
- run: runLogin
6479
- };
6480
- async function runLogin(_args, io) {
6718
+ var REAL_DEPS = { gatewayLogin, legacyLogin: login };
6719
+ var loginCommand = makeLoginCommand();
6720
+ function makeLoginCommand(deps = REAL_DEPS) {
6721
+ return {
6722
+ name: "login",
6723
+ summary: "authenticate (browser-based PKCE) and store a session",
6724
+ run: (args, io) => runLogin(args, io, deps)
6725
+ };
6726
+ }
6727
+ async function runLogin(_args, io, deps) {
6481
6728
  try {
6482
6729
  const cfg = loadConfig();
6483
- io.out("Opening browser to authenticate with Cloudflare Access…");
6730
+ if (cfg.authLegacy) {
6731
+ io.out("LAUNCHPAD_AUTH_LEGACY=1 — using the legacy Cloudflare Access flow (deprecated; this path will be removed when the dual-auth window closes).");
6732
+ return await runLegacyLogin(io, cfg.botUrl, cfg.sessionPath, deps);
6733
+ }
6734
+ io.out("Opening browser to sign in via the Launchpad auth gateway…");
6484
6735
  io.out("(if it doesn't open automatically, copy the URL below)");
6485
6736
  io.out("");
6486
- const session = await login({
6487
- botUrl: cfg.botUrl,
6488
- sessionPath: cfg.sessionPath,
6489
- onAuthUrl: (url) => {
6490
- io.out(url);
6491
- io.out("");
6492
- }
6493
- });
6494
- const expiresIn = Math.max(0, Math.round((session.accessTokenExpiresAt - Date.now()) / 1000));
6495
- io.out(`Logged in. Session stored at ${cfg.sessionPath}`);
6496
- io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
6497
- return 0;
6737
+ try {
6738
+ const session = await deps.gatewayLogin({
6739
+ gatewayUrl: cfg.authGatewayUrl,
6740
+ sessionPath: cfg.sessionPath,
6741
+ onAuthUrl: (url) => {
6742
+ io.out(url);
6743
+ io.out("");
6744
+ }
6745
+ });
6746
+ const expiresIn = Math.max(0, Math.round((session.accessTokenExpiresAt - Date.now()) / 1000));
6747
+ io.out(`Logged in. Session stored at ${cfg.sessionPath}`);
6748
+ io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
6749
+ return 0;
6750
+ } catch (e) {
6751
+ io.err(`Gateway login failed: ${describe19(e)}`);
6752
+ io.err("Falling back to the legacy Cloudflare Access flow (DEPRECATED — this fallback will be removed when the dual-auth window closes).");
6753
+ io.err("");
6754
+ return await runLegacyLogin(io, cfg.botUrl, cfg.sessionPath, deps);
6755
+ }
6498
6756
  } catch (e) {
6499
- io.err(`launchpad login failed: ${describe18(e)}`);
6757
+ io.err(`launchpad login failed: ${describe19(e)}`);
6500
6758
  return 1;
6501
6759
  }
6502
6760
  }
6503
- function describe18(e) {
6761
+ async function runLegacyLogin(io, botUrl, sessionPath, deps) {
6762
+ io.out("Opening browser to authenticate with Cloudflare Access…");
6763
+ io.out("(if it doesn't open automatically, copy the URL below)");
6764
+ io.out("");
6765
+ const session = await deps.legacyLogin({
6766
+ botUrl,
6767
+ sessionPath,
6768
+ onAuthUrl: (url) => {
6769
+ io.out(url);
6770
+ io.out("");
6771
+ }
6772
+ });
6773
+ const expiresIn = Math.max(0, Math.round((session.accessTokenExpiresAt - Date.now()) / 1000));
6774
+ io.out(`Logged in. Session stored at ${sessionPath}`);
6775
+ io.out(`Access token expires in ~${expiresIn}s; refreshes silently.`);
6776
+ return 0;
6777
+ }
6778
+ function describe19(e) {
6504
6779
  return e instanceof Error ? e.message : String(e);
6505
6780
  }
6506
6781
 
6507
6782
  // src/commands/logout.ts
6508
- var logoutCommand = {
6509
- name: "logout",
6510
- summary: "clear the persisted session",
6511
- run: runLogout
6512
- };
6513
- async function runLogout(_args, io) {
6783
+ var REAL_DEPS2 = { revoke: revokeGatewaySession };
6784
+ var logoutCommand = makeLogoutCommand();
6785
+ function makeLogoutCommand(deps = REAL_DEPS2) {
6786
+ return {
6787
+ name: "logout",
6788
+ summary: "revoke the session server-side and clear it locally",
6789
+ run: (args, io) => runLogout(args, io, deps)
6790
+ };
6791
+ }
6792
+ async function runLogout(_args, io, deps) {
6514
6793
  try {
6515
6794
  const cfg = loadConfig();
6795
+ let session = null;
6796
+ try {
6797
+ session = await readSession(cfg.sessionPath);
6798
+ } catch (e) {
6799
+ if (!(e instanceof SessionParseError))
6800
+ throw e;
6801
+ io.err(`warning: session file is corrupt (${e.message}) — clearing it without a server-side revoke.`);
6802
+ }
6803
+ if (session !== null && isGatewaySession(session)) {
6804
+ try {
6805
+ await deps.revoke({
6806
+ gatewayUrl: session.gatewayUrl,
6807
+ refreshToken: session.refreshToken
6808
+ });
6809
+ io.out("Server-side session revoked.");
6810
+ } catch (e) {
6811
+ io.err(`warning: could not revoke the session server-side (${describe20(e)}) — clearing the local session anyway. Any in-flight access token expires within 15 minutes.`);
6812
+ }
6813
+ }
6516
6814
  const had = await clearSession(cfg.sessionPath);
6517
6815
  io.out(had ? `Logged out. Session cleared at ${cfg.sessionPath}` : "Already logged out.");
6518
6816
  return 0;
6519
6817
  } catch (e) {
6520
- io.err(`launchpad logout failed: ${describe19(e)}`);
6818
+ io.err(`launchpad logout failed: ${describe20(e)}`);
6521
6819
  return 1;
6522
6820
  }
6523
6821
  }
6524
- function describe19(e) {
6822
+ function describe20(e) {
6525
6823
  return e instanceof Error ? e.message : String(e);
6526
6824
  }
6527
6825
 
@@ -6591,7 +6889,7 @@ async function runLogs(args, io) {
6591
6889
  io.err(`launchpad logs: ${e.message}`);
6592
6890
  return 1;
6593
6891
  }
6594
- io.err(`launchpad logs failed: ${describe20(e)}`);
6892
+ io.err(`launchpad logs failed: ${describe21(e)}`);
6595
6893
  return 1;
6596
6894
  }
6597
6895
  }
@@ -6671,7 +6969,7 @@ function formatRelative2(iso) {
6671
6969
  return `${hr}h ago`;
6672
6970
  return `${Math.floor(hr / 24)}d ago`;
6673
6971
  }
6674
- function describe20(e) {
6972
+ function describe21(e) {
6675
6973
  return e instanceof Error ? e.message : String(e);
6676
6974
  }
6677
6975
 
@@ -6720,7 +7018,7 @@ async function runMerge(args, io) {
6720
7018
  try {
6721
7019
  body = await res.json();
6722
7020
  } catch (e) {
6723
- io.err(`launchpad merge: bot returned 2xx with malformed body: ${describe21(e)}`);
7021
+ io.err(`launchpad merge: bot returned 2xx with malformed body: ${describe22(e)}`);
6724
7022
  return 1;
6725
7023
  }
6726
7024
  io.out("");
@@ -6760,7 +7058,7 @@ async function runMerge(args, io) {
6760
7058
  io.err(`launchpad merge: ${e.message}`);
6761
7059
  return 1;
6762
7060
  }
6763
- io.err(`launchpad merge failed: ${describe21(e)}`);
7061
+ io.err(`launchpad merge failed: ${describe22(e)}`);
6764
7062
  return 1;
6765
7063
  }
6766
7064
  }
@@ -6844,7 +7142,7 @@ function renderBotError(status, env, io, prNumber = null) {
6844
7142
  io.err(`launchpad merge: bot returned ${status} ${code}${detail !== undefined ? `: ${detail}` : ""}`);
6845
7143
  }
6846
7144
  }
6847
- function describe21(e) {
7145
+ function describe22(e) {
6848
7146
  return e instanceof Error ? e.message : String(e);
6849
7147
  }
6850
7148
 
@@ -7096,7 +7394,7 @@ async function runDestroy(args, io, prompt, isTty) {
7096
7394
  io.err(`launchpad destroy: ${e.message}`);
7097
7395
  return 1;
7098
7396
  }
7099
- io.err(`launchpad destroy failed: ${describe22(e)}`);
7397
+ io.err(`launchpad destroy failed: ${describe23(e)}`);
7100
7398
  return 1;
7101
7399
  }
7102
7400
  }
@@ -7297,7 +7595,7 @@ function printUsage3(io) {
7297
7595
  ].join(`
7298
7596
  `));
7299
7597
  }
7300
- function describe22(e) {
7598
+ function describe23(e) {
7301
7599
  return e instanceof Error ? e.message : String(e);
7302
7600
  }
7303
7601
 
@@ -7441,7 +7739,7 @@ async function runPull(args, io) {
7441
7739
  io.err(`launchpad pull: ${e.message}`);
7442
7740
  return 1;
7443
7741
  }
7444
- io.err(`launchpad pull failed: ${describe23(e)}`);
7742
+ io.err(`launchpad pull failed: ${describe24(e)}`);
7445
7743
  return 1;
7446
7744
  }
7447
7745
  }
@@ -7522,7 +7820,7 @@ function printUsage4(io) {
7522
7820
  ].join(`
7523
7821
  `));
7524
7822
  }
7525
- function describe23(e) {
7823
+ function describe24(e) {
7526
7824
  return e instanceof Error ? e.message : String(e);
7527
7825
  }
7528
7826
 
@@ -7731,7 +8029,7 @@ function mapBotError(e, slug, io) {
7731
8029
  io.err(`launchpad status: ${e.message}`);
7732
8030
  return 2;
7733
8031
  }
7734
- io.err(`launchpad status failed: ${describe24(e)}`);
8032
+ io.err(`launchpad status failed: ${describe25(e)}`);
7735
8033
  return 2;
7736
8034
  }
7737
8035
  async function runStatus(args, io) {
@@ -7757,7 +8055,7 @@ async function runStatus(args, io) {
7757
8055
  localYaml = readFileSync12(parsed.file, "utf8");
7758
8056
  } catch (e) {
7759
8057
  if (!isEnoent(e)) {
7760
- io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe24(e)}`);
8058
+ io.err(`launchpad status: cannot read local manifest at ${parsed.file}: ${describe25(e)}`);
7761
8059
  return 2;
7762
8060
  }
7763
8061
  }
@@ -7768,7 +8066,7 @@ async function runStatus(args, io) {
7768
8066
  const { parse: parseYaml6 } = await import("yaml");
7769
8067
  localObj = parseYaml6(localYaml);
7770
8068
  } catch (e) {
7771
- io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe24(e)}`);
8069
+ io.err(`launchpad status: ${parsed.file} is not valid YAML: ${describe25(e)}`);
7772
8070
  return 2;
7773
8071
  }
7774
8072
  const localParse = parseManifest(localObj);
@@ -7797,7 +8095,7 @@ async function runStatus(args, io) {
7797
8095
  if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
7798
8096
  return mapBotError(e, parsed.slug, io);
7799
8097
  }
7800
- io.err(`launchpad status: live deployment state unavailable (${describe24(e)}) — ` + `the report below is from the platform manifest view only.`);
8098
+ io.err(`launchpad status: live deployment state unavailable (${describe25(e)}) — ` + `the report below is from the platform manifest view only.`);
7801
8099
  }
7802
8100
  let standingExceptions = null;
7803
8101
  try {
@@ -7806,7 +8104,7 @@ async function runStatus(args, io) {
7806
8104
  if (e instanceof UnauthenticatedError || e instanceof ForbiddenError) {
7807
8105
  return mapBotError(e, parsed.slug, io);
7808
8106
  }
7809
- io.err(`launchpad status: standing-exception inventory unavailable (${describe24(e)}).`);
8107
+ io.err(`launchpad status: standing-exception inventory unavailable (${describe25(e)}).`);
7810
8108
  }
7811
8109
  if (state.manifestYaml === null || state.manifestYaml === undefined) {
7812
8110
  const live = deployment?.liveDeployment ?? null;
@@ -7857,7 +8155,7 @@ async function runStatus(args, io) {
7857
8155
  const { parse: parseYaml6 } = await import("yaml");
7858
8156
  deployedObj = parseYaml6(state.manifestYaml);
7859
8157
  } catch (e) {
7860
- io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${describe24(e)}`);
8158
+ io.err(`launchpad status: deployed manifest at ${state.lastAppliedManifestSha} is not valid YAML: ${describe25(e)}`);
7861
8159
  return 2;
7862
8160
  }
7863
8161
  const deployedParse = parseManifest(deployedObj);
@@ -8196,7 +8494,7 @@ function printUsage6(io) {
8196
8494
  ].join(`
8197
8495
  `));
8198
8496
  }
8199
- function describe24(e) {
8497
+ function describe25(e) {
8200
8498
  return e instanceof Error ? e.message : String(e);
8201
8499
  }
8202
8500
  function isEnoent(e) {
@@ -8274,7 +8572,7 @@ async function runReview(args, io) {
8274
8572
  io.err(`launchpad review: ${e.message}`);
8275
8573
  return 1;
8276
8574
  }
8277
- io.err(`launchpad review failed: ${describe25(e)}`);
8575
+ io.err(`launchpad review failed: ${describe26(e)}`);
8278
8576
  return 1;
8279
8577
  }
8280
8578
  }
@@ -8352,7 +8650,7 @@ function severityRank(s) {
8352
8650
  return 3;
8353
8651
  }
8354
8652
  }
8355
- function describe25(e) {
8653
+ function describe26(e) {
8356
8654
  return e instanceof Error ? e.message : String(e);
8357
8655
  }
8358
8656
 
@@ -8412,7 +8710,7 @@ async function runRollback(opts, io, deps = {}) {
8412
8710
  cwd: process.cwd()
8413
8711
  });
8414
8712
  } catch (e) {
8415
- io.err(`launchpad rollback: git rev-parse failed to start: ${describe26(e)}`);
8713
+ io.err(`launchpad rollback: git rev-parse failed to start: ${describe27(e)}`);
8416
8714
  return 2;
8417
8715
  }
8418
8716
  if (revParse.exitCode !== 0) {
@@ -8427,7 +8725,7 @@ async function runRollback(opts, io, deps = {}) {
8427
8725
  try {
8428
8726
  show = await runner.run("git", ["show", `${verifiedSha}:${manifestRelpath}`], { cwd: process.cwd() });
8429
8727
  } catch (e) {
8430
- io.err(`launchpad rollback: git show failed to start: ${describe26(e)}`);
8728
+ io.err(`launchpad rollback: git show failed to start: ${describe27(e)}`);
8431
8729
  return 2;
8432
8730
  }
8433
8731
  if (show.exitCode !== 0) {
@@ -8461,7 +8759,7 @@ async function runRollback(opts, io, deps = {}) {
8461
8759
  try {
8462
8760
  writeFileSync5(manifestPath, historicalYaml, "utf8");
8463
8761
  } catch (e) {
8464
- io.err(`launchpad rollback: failed to write ${manifestPath}: ${describe26(e)}`);
8762
+ io.err(`launchpad rollback: failed to write ${manifestPath}: ${describe27(e)}`);
8465
8763
  return 2;
8466
8764
  }
8467
8765
  io.out(`Restored ${manifestPath} from ${verifiedSha.slice(0, 12)}.`);
@@ -8543,7 +8841,7 @@ async function defaultPrompt4(question) {
8543
8841
  rl.close();
8544
8842
  }
8545
8843
  }
8546
- function describe26(e) {
8844
+ function describe27(e) {
8547
8845
  return e instanceof Error ? e.message : String(e);
8548
8846
  }
8549
8847
  function renderManifestError3(result, io) {
@@ -8706,7 +9004,7 @@ async function runSecretsPush(opts, io, deps = {}) {
8706
9004
  try {
8707
9005
  envText = readFileSync14(envPath, "utf8");
8708
9006
  } catch (e) {
8709
- io.err(`launchpad secrets push: failed to read ${envPath}: ${describe27(e)}`);
9007
+ io.err(`launchpad secrets push: failed to read ${envPath}: ${describe28(e)}`);
8710
9008
  return 2;
8711
9009
  }
8712
9010
  const parsed = parseEnv(envText);
@@ -8837,10 +9135,10 @@ function mapPushError(e, slug, name, io) {
8837
9135
  io.err(`✗ secrets push ${name}: ${e.message}`);
8838
9136
  return 1;
8839
9137
  }
8840
- io.err(`✗ secrets push ${name} failed: ${describe27(e)}`);
9138
+ io.err(`✗ secrets push ${name} failed: ${describe28(e)}`);
8841
9139
  return 2;
8842
9140
  }
8843
- function describe27(e) {
9141
+ function describe28(e) {
8844
9142
  return e instanceof Error ? e.message : String(e);
8845
9143
  }
8846
9144
  function renderManifestError4(result, io) {
@@ -9584,7 +9882,7 @@ async function runSkills(args, io) {
9584
9882
  return 64;
9585
9883
  }
9586
9884
  } catch (e) {
9587
- io.err(`launchpad skills ${action}: ${describe28(e)}`);
9885
+ io.err(`launchpad skills ${action}: ${describe29(e)}`);
9588
9886
  return 1;
9589
9887
  }
9590
9888
  }
@@ -9714,7 +10012,7 @@ async function isDir(path12) {
9714
10012
  return false;
9715
10013
  }
9716
10014
  }
9717
- function describe28(e) {
10015
+ function describe29(e) {
9718
10016
  return e instanceof Error ? e.message : String(e);
9719
10017
  }
9720
10018
 
@@ -9728,7 +10026,7 @@ import { readFileSync as readFileSync16, mkdtempSync, writeFileSync as writeFile
9728
10026
 
9729
10027
  // src/commands/channel-auth.ts
9730
10028
  import { createServer as createServer2 } from "node:http";
9731
- import { createHash as createHash2, randomBytes as randomBytes4 } from "node:crypto";
10029
+ import { createHash as createHash2, randomBytes as randomBytes5 } from "node:crypto";
9732
10030
  var CHANNEL_BASE = "https://get.launchpad.m-kopa.us";
9733
10031
  var CLI_AUTH_URL = `${CHANNEL_BASE}/__cli_auth`;
9734
10032
  var CLI_TOKEN_URL = `${CHANNEL_BASE}/__cli_token`;
@@ -9738,7 +10036,7 @@ function base64url(b) {
9738
10036
  return b.toString("base64url");
9739
10037
  }
9740
10038
  function pkcePair() {
9741
- const verifier = base64url(randomBytes4(32));
10039
+ const verifier = base64url(randomBytes5(32));
9742
10040
  const challenge = base64url(createHash2("sha256").update(verifier).digest());
9743
10041
  return { verifier, challenge };
9744
10042
  }
@@ -9784,7 +10082,7 @@ async function startLoopback(state, timeoutMs) {
9784
10082
  async function runChannelLoopbackUpdate(deps) {
9785
10083
  const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
9786
10084
  const { verifier, challenge } = pkcePair();
9787
- const state = base64url(randomBytes4(16));
10085
+ const state = base64url(randomBytes5(16));
9788
10086
  const loopback = await startLoopback(state, timeoutMs);
9789
10087
  if (!loopback)
9790
10088
  return { ok: false, reason: "no-browser", detail: "could not bind a loopback port" };
@@ -10235,7 +10533,7 @@ async function checkGroup(allowedGroup, cfg) {
10235
10533
  if (e instanceof ForbiddenError) {
10236
10534
  return { kind: "forbidden", message: e.message };
10237
10535
  }
10238
- return { kind: "network", message: describe29(e) };
10536
+ return { kind: "network", message: describe30(e) };
10239
10537
  }
10240
10538
  }
10241
10539
  async function checkGroups(groups) {
@@ -10448,7 +10746,7 @@ function parseArgs12(args) {
10448
10746
  }
10449
10747
  return { kind: "ok", file, json, strictGroups };
10450
10748
  }
10451
- function describe29(e) {
10749
+ function describe30(e) {
10452
10750
  return e instanceof Error ? e.message : String(e);
10453
10751
  }
10454
10752
 
@@ -10497,11 +10795,11 @@ async function runWhoami(_args, io) {
10497
10795
  io.err(e.message);
10498
10796
  return 1;
10499
10797
  }
10500
- io.err(`launchpad whoami failed: ${describe30(e)}`);
10798
+ io.err(`launchpad whoami failed: ${describe31(e)}`);
10501
10799
  return 1;
10502
10800
  }
10503
10801
  }
10504
- function describe30(e) {
10802
+ function describe31(e) {
10505
10803
  return e instanceof Error ? e.message : String(e);
10506
10804
  }
10507
10805