@launchsecure/launch-kit 0.0.35 → 0.0.36

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.
@@ -21,13 +21,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  // src/server/radar-docker-init-entry.ts
22
22
  var radar_docker_init_entry_exports = {};
23
23
  __export(radar_docker_init_entry_exports, {
24
+ maybeProvisionAccess: () => maybeProvisionAccess,
24
25
  maybeProvisionIngress: () => maybeProvisionIngress,
25
26
  spawnServiceGroup: () => spawnServiceGroup
26
27
  });
27
28
  module.exports = __toCommonJS(radar_docker_init_entry_exports);
28
29
  var import_node_child_process = require("node:child_process");
29
- var import_node_fs3 = require("node:fs");
30
- var import_node_path3 = require("node:path");
30
+ var import_node_fs4 = require("node:fs");
31
+ var import_node_path4 = require("node:path");
31
32
 
32
33
  // src/server/radar/mcp.ts
33
34
  var import_node_https = require("node:https");
@@ -152,10 +153,9 @@ var SHORTHANDS = {
152
153
  sequencer: { port: 3517, bin: "launch-sequencer", args: [] },
153
154
  chart: { port: 52819, bin: "launch-chart", args: ["serve"] },
154
155
  deck: { port: 52829, bin: "launch-deck", args: ["serve"] },
155
- council: { port: 52839, bin: "launch-council", args: ["serve"] },
156
156
  // Claude web terminal — exposes a viewable/drivable `claude` session at
157
- // `bot.<baseDomain>`. NOTE: no auth gate yet (tracked as a separate
158
- // high-priority rover-security work item); ships behind a plain link first.
157
+ // `bot.<baseDomain>`. Gated at the edge by CF Access our OIDC IdP (#183);
158
+ // see GATED_SERVICES in radar-docker-init-entry.ts (bot = strict session).
159
159
  bot: { port: 52849, bin: "launch-bot", args: ["serve"] }
160
160
  };
161
161
  var DNS_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
@@ -259,6 +259,9 @@ var import_node_fs2 = require("node:fs");
259
259
  var import_node_path2 = require("node:path");
260
260
  var CF_API_BASE = "https://api.cloudflare.com/client/v4";
261
261
  var CF_ERR_DNS_RECORD_EXISTS = 81053;
262
+ function serviceLabel(s) {
263
+ return s.label ?? s.name;
264
+ }
262
265
  async function cf(opts) {
263
266
  const res = await fetch(`${CF_API_BASE}${opts.path}`, {
264
267
  method: opts.method,
@@ -338,7 +341,7 @@ async function fetchConnectorToken(input, tunnelId) {
338
341
  }
339
342
  async function setIngressConfig(input, tunnelId) {
340
343
  const ingress = input.services.map((s) => ({
341
- hostname: `${s.name}.${input.zone.name}`,
344
+ hostname: `${serviceLabel(s)}.${input.zone.name}`,
342
345
  service: `http://localhost:${s.port}`
343
346
  }));
344
347
  ingress.push({ service: "http_status:404" });
@@ -353,7 +356,7 @@ async function setIngressConfig(input, tunnelId) {
353
356
  }
354
357
  }
355
358
  async function ensureDnsRecord(input, tunnelId, service) {
356
- const fqdn = `${service.name}.${input.zone.name}`;
359
+ const fqdn = `${serviceLabel(service)}.${input.zone.name}`;
357
360
  const target = `${tunnelId}.cfargotunnel.com`;
358
361
  const existing = await cf({
359
362
  apiToken: input.apiToken,
@@ -397,10 +400,181 @@ async function provisionIngress(input) {
397
400
  await setIngressConfig(input, tunnelId);
398
401
  await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
399
402
  const hostnames = {};
400
- for (const s of input.services) hostnames[s.name] = `${s.name}.${input.zone.name}`;
403
+ for (const s of input.services) hostnames[s.name] = `${serviceLabel(s)}.${input.zone.name}`;
401
404
  return { tunnelId, connectorToken, hostnames };
402
405
  }
403
406
 
407
+ // src/server/cf-access.ts
408
+ var import_node_fs3 = require("node:fs");
409
+ var import_node_path3 = require("node:path");
410
+ var CF_API_BASE2 = "https://api.cloudflare.com/client/v4";
411
+ var IDP_NAME = "launch-kit-oidc";
412
+ async function cf2(opts) {
413
+ const res = await fetch(`${CF_API_BASE2}${opts.path}`, {
414
+ method: opts.method,
415
+ headers: {
416
+ Authorization: `Bearer ${opts.apiToken}`,
417
+ "Content-Type": "application/json",
418
+ Accept: "application/json",
419
+ "User-Agent": "launch-kit/cf-access"
420
+ },
421
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
422
+ signal: AbortSignal.timeout(15e3)
423
+ });
424
+ const text = await res.text();
425
+ try {
426
+ return text ? JSON.parse(text) : { success: false };
427
+ } catch {
428
+ throw new Error(`[cf-access] ${opts.method} ${opts.path} \u2192 ${res.status}, non-JSON: ${text.slice(0, 200)}`);
429
+ }
430
+ }
431
+ function fail(env, what) {
432
+ throw new Error(`[cf-access] ${what} failed: ${JSON.stringify(env.errors)}`);
433
+ }
434
+ function loadState2(path) {
435
+ if (!(0, import_node_fs3.existsSync)(path)) return null;
436
+ try {
437
+ const parsed = JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
438
+ return typeof parsed?.accountId === "string" ? parsed : null;
439
+ } catch {
440
+ return null;
441
+ }
442
+ }
443
+ function saveState2(path, state) {
444
+ const dir = (0, import_node_path3.dirname)(path);
445
+ if (!(0, import_node_fs3.existsSync)(dir)) (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
446
+ (0, import_node_fs3.writeFileSync)(path, JSON.stringify(state, null, 2));
447
+ }
448
+ async function getAccessAuthDomain(apiToken, accountId) {
449
+ const res = await cf2({
450
+ apiToken,
451
+ method: "GET",
452
+ path: `/accounts/${accountId}/access/organizations`
453
+ });
454
+ if (!res.success || !res.result?.auth_domain) {
455
+ fail(res, "GET access/organizations (is Zero Trust enabled on this account?)");
456
+ }
457
+ return res.result.auth_domain;
458
+ }
459
+ async function ensureAccessIdp(input) {
460
+ const config = {
461
+ client_id: input.clientId,
462
+ client_secret: input.clientSecret,
463
+ auth_url: `${input.issuer}/api/oidc/authorize`,
464
+ token_url: `${input.issuer}/api/oidc/token`,
465
+ certs_url: `${input.issuer}/.well-known/jwks.json`,
466
+ scopes: ["openid", "email", "profile"],
467
+ claims: ["org", "project_access", "roles", "email"],
468
+ email_claim_name: "email",
469
+ pkce_enabled: true
470
+ };
471
+ const body = { name: IDP_NAME, type: "oidc", config };
472
+ let idpId = input.knownIdpId;
473
+ if (!idpId) {
474
+ const list = await cf2({
475
+ apiToken: input.apiToken,
476
+ method: "GET",
477
+ path: `/accounts/${input.accountId}/access/identity_providers`
478
+ });
479
+ if (!list.success) fail(list, "list identity_providers");
480
+ idpId = (list.result ?? []).find((p) => p.name === IDP_NAME)?.id ?? null;
481
+ }
482
+ if (idpId) {
483
+ const upd = await cf2({
484
+ apiToken: input.apiToken,
485
+ method: "PUT",
486
+ path: `/accounts/${input.accountId}/access/identity_providers/${idpId}`,
487
+ body
488
+ });
489
+ if (!upd.success || !upd.result) fail(upd, "update identity_provider");
490
+ return upd.result.id;
491
+ }
492
+ const created = await cf2({
493
+ apiToken: input.apiToken,
494
+ method: "POST",
495
+ path: `/accounts/${input.accountId}/access/identity_providers`,
496
+ body
497
+ });
498
+ if (!created.success || !created.result) fail(created, "create identity_provider");
499
+ return created.result.id;
500
+ }
501
+ async function ensureAccessApp(input) {
502
+ const policy = {
503
+ name: "launch-kit-org-allow",
504
+ decision: "allow",
505
+ include: [
506
+ {
507
+ oidc: {
508
+ identity_provider_id: input.idpId,
509
+ claim_name: "org",
510
+ claim_value: input.organizationId
511
+ }
512
+ }
513
+ ]
514
+ };
515
+ const body = {
516
+ name: `launch-kit ${input.service.hostname}`,
517
+ domain: input.service.hostname,
518
+ type: "self_hosted",
519
+ // Bot terminal = RCE surface → short session. Read portals = a workday.
520
+ session_duration: input.service.strict ? "30m" : "24h",
521
+ allowed_idps: [input.idpId],
522
+ auto_redirect_to_identity: true,
523
+ policies: [policy]
524
+ };
525
+ const list = await cf2({
526
+ apiToken: input.apiToken,
527
+ method: "GET",
528
+ path: `/accounts/${input.accountId}/access/apps`
529
+ });
530
+ if (!list.success) fail(list, "list access apps");
531
+ const existing = (list.result ?? []).find((a) => a.domain === input.service.hostname);
532
+ if (existing) {
533
+ const upd = await cf2({
534
+ apiToken: input.apiToken,
535
+ method: "PUT",
536
+ path: `/accounts/${input.accountId}/access/apps/${existing.id}`,
537
+ body
538
+ });
539
+ if (!upd.success || !upd.result) fail(upd, `update access app ${input.service.hostname}`);
540
+ return upd.result.id;
541
+ }
542
+ const created = await cf2({
543
+ apiToken: input.apiToken,
544
+ method: "POST",
545
+ path: `/accounts/${input.accountId}/access/apps`,
546
+ body
547
+ });
548
+ if (!created.success || !created.result) fail(created, `create access app ${input.service.hostname}`);
549
+ return created.result.id;
550
+ }
551
+ async function provisionAccess(input) {
552
+ const authDomain = await getAccessAuthDomain(input.apiToken, input.accountId);
553
+ const callbackUrl = `https://${authDomain}/cdn-cgi/access/callback`;
554
+ const { clientId, clientSecret, organizationId } = await input.registerClient([callbackUrl]);
555
+ const prior = loadState2(input.stateFile);
556
+ const idpId = await ensureAccessIdp({
557
+ apiToken: input.apiToken,
558
+ accountId: input.accountId,
559
+ issuer: input.issuer,
560
+ clientId,
561
+ clientSecret,
562
+ knownIdpId: prior?.idpId ?? null
563
+ });
564
+ saveState2(input.stateFile, { idpId, accountId: input.accountId });
565
+ const appIds = {};
566
+ for (const service of input.services) {
567
+ appIds[service.hostname] = await ensureAccessApp({
568
+ apiToken: input.apiToken,
569
+ accountId: input.accountId,
570
+ idpId,
571
+ organizationId,
572
+ service
573
+ });
574
+ }
575
+ return { idpId, authDomain, appIds };
576
+ }
577
+
404
578
  // src/server/radar-docker-init-entry.ts
405
579
  var REQUIRED_ENV = [
406
580
  "CLAUDE_CREDENTIALS_B64",
@@ -408,13 +582,13 @@ var REQUIRED_ENV = [
408
582
  "LS_ORG_SLUG",
409
583
  "LS_PROJECT_SLUG"
410
584
  ];
411
- function fail(message) {
585
+ function fail2(message) {
412
586
  console.error(message);
413
587
  process.exit(1);
414
588
  }
415
589
  function requireEnv(name) {
416
590
  const v = process.env[name];
417
- if (!v) fail(`ERROR: ${name} is required but not set`);
591
+ if (!v) fail2(`ERROR: ${name} is required but not set`);
418
592
  return v;
419
593
  }
420
594
  function run(cmd, args, stdio = "inherit") {
@@ -431,13 +605,16 @@ async function setupFromCloud() {
431
605
  try {
432
606
  bundle = await mcp.call("radar_bootstrap_get", {});
433
607
  } catch (err) {
434
- fail(`[entrypoint] radar_bootstrap_get failed (${err instanceof Error ? err.message : String(err)}) \u2014 check LS_PAT has mcp:radar:bootstrap scope and LS_ORG_SLUG/LS_PROJECT_SLUG point at a project the user has access to.`);
608
+ fail2(`[entrypoint] radar_bootstrap_get failed (${err instanceof Error ? err.message : String(err)}) \u2014 check LS_PAT has mcp:radar:bootstrap scope and LS_ORG_SLUG/LS_PROJECT_SLUG point at a project the user has access to.`);
435
609
  }
436
610
  if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
437
611
  if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
438
612
  if (!process.env.GH_TOKEN && bundle.githubToken) process.env.GH_TOKEN = bundle.githubToken;
613
+ if (!process.env.RADAR_RULES && Array.isArray(bundle.radarRules) && bundle.radarRules.length > 0) {
614
+ process.env.RADAR_RULES = JSON.stringify(bundle.radarRules);
615
+ }
439
616
  if (!process.env.GH_TOKEN) {
440
- fail(`[entrypoint] no GH_TOKEN available \u2014 user has not connected GitHub (githubTokenStatus=${bundle.githubTokenStatus}). Connect GitHub in LS or pre-set GH_TOKEN in the container env.`);
617
+ fail2(`[entrypoint] no GH_TOKEN available \u2014 user has not connected GitHub (githubTokenStatus=${bundle.githubTokenStatus}). Connect GitHub in LS or pre-set GH_TOKEN in the container env.`);
441
618
  }
442
619
  const cfNote = bundle.cloudflareToken ? "cloudflare=connected" : "cloudflare=none";
443
620
  console.log(`[entrypoint] bundle from cloud: org=${orgSlug} project=${projectSlug} git=${process.env.GIT_USER_NAME} <${process.env.GIT_USER_EMAIL}> github=${bundle.githubTokenStatus.toLowerCase()} ${cfNote}`);
@@ -445,17 +622,17 @@ async function setupFromCloud() {
445
622
  }
446
623
  function setupClaudeCredentials() {
447
624
  const home = process.env.HOME ?? "/home/launchpod";
448
- const claudeDir = (0, import_node_path3.join)(home, ".claude");
449
- (0, import_node_fs3.mkdirSync)(claudeDir, { recursive: true });
625
+ const claudeDir = (0, import_node_path4.join)(home, ".claude");
626
+ (0, import_node_fs4.mkdirSync)(claudeDir, { recursive: true });
450
627
  const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
451
- const credsPath = (0, import_node_path3.join)(claudeDir, ".credentials.json");
452
- (0, import_node_fs3.writeFileSync)(credsPath, decoded);
453
- (0, import_node_fs3.chmodSync)(credsPath, 384);
454
- const configPath = (0, import_node_path3.join)(home, ".claude.json");
628
+ const credsPath = (0, import_node_path4.join)(claudeDir, ".credentials.json");
629
+ (0, import_node_fs4.writeFileSync)(credsPath, decoded);
630
+ (0, import_node_fs4.chmodSync)(credsPath, 384);
631
+ const configPath = (0, import_node_path4.join)(home, ".claude.json");
455
632
  let cfg = {};
456
- if ((0, import_node_fs3.existsSync)(configPath)) {
633
+ if ((0, import_node_fs4.existsSync)(configPath)) {
457
634
  try {
458
- cfg = JSON.parse((0, import_node_fs3.readFileSync)(configPath, "utf8"));
635
+ cfg = JSON.parse((0, import_node_fs4.readFileSync)(configPath, "utf8"));
459
636
  } catch {
460
637
  cfg = {};
461
638
  }
@@ -481,21 +658,21 @@ function setupClaudeCredentials() {
481
658
  wsProject.enabledMcpjsonServers = mergedEnabled;
482
659
  projects[wsKey] = wsProject;
483
660
  cfg.projects = projects;
484
- (0, import_node_fs3.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
485
- (0, import_node_fs3.chmodSync)(configPath, 384);
661
+ (0, import_node_fs4.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
662
+ (0, import_node_fs4.chmodSync)(configPath, 384);
486
663
  }
487
664
  function setupGitAndGh() {
488
665
  const name = process.env.GIT_USER_NAME ?? "Radar Bot";
489
666
  const email = process.env.GIT_USER_EMAIL ?? "radar@launchpod.local";
490
667
  const status = run("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
491
- if (status !== 0) fail(`[entrypoint] launch-kit setup-git failed (status ${status})`);
668
+ if (status !== 0) fail2(`[entrypoint] launch-kit setup-git failed (status ${status})`);
492
669
  }
493
670
  function detectAndSetPreviewPort() {
494
671
  if (process.env.PREVIEW_PORT) return;
495
672
  try {
496
673
  const pkgPath = "/workspace/package.json";
497
- if (!(0, import_node_fs3.existsSync)(pkgPath)) return;
498
- const pkg = JSON.parse((0, import_node_fs3.readFileSync)(pkgPath, "utf-8"));
674
+ if (!(0, import_node_fs4.existsSync)(pkgPath)) return;
675
+ const pkg = JSON.parse((0, import_node_fs4.readFileSync)(pkgPath, "utf-8"));
499
676
  const scripts = pkg.scripts ?? {};
500
677
  const portRe = /(?:--port[= ]|-p\s+|\bPORT=)(\d{2,5})\b/;
501
678
  for (const name of ["dev", "start", "serve"]) {
@@ -513,7 +690,7 @@ function detectAndSetPreviewPort() {
513
690
  }
514
691
  function initWorkspaceIfEmpty() {
515
692
  process.chdir("/workspace");
516
- if ((0, import_node_fs3.existsSync)(".git")) {
693
+ if ((0, import_node_fs4.existsSync)(".git")) {
517
694
  console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
518
695
  return;
519
696
  }
@@ -526,7 +703,7 @@ function initWorkspaceIfEmpty() {
526
703
  `--url=${process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app"}`,
527
704
  `--dir=/workspace`
528
705
  ]);
529
- if (status !== 0) fail(`[entrypoint] launch-kit init failed (status ${status})`);
706
+ if (status !== 0) fail2(`[entrypoint] launch-kit init failed (status ${status})`);
530
707
  }
531
708
  async function maybeProvisionIngress(bundle, services, projectSlug) {
532
709
  const token = bundle.cloudflareToken ?? null;
@@ -534,28 +711,36 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
534
711
  const zones = bundle.cloudflareZones ?? [];
535
712
  if (!token && !accountId && zones.length === 0) return null;
536
713
  if (!token || !accountId) {
537
- fail(`[entrypoint] cloudflare integration is partial \u2014 token=${token ? "set" : "missing"} accountId=${accountId ? "set" : "missing"}. Re-connect the Cloudflare provider in LS.`);
714
+ fail2(`[entrypoint] cloudflare integration is partial \u2014 token=${token ? "set" : "missing"} accountId=${accountId ? "set" : "missing"}. Re-connect the Cloudflare provider in LS.`);
538
715
  }
539
716
  const baseDomain = process.env.LAUNCHKIT_CF_BASE_DOMAIN?.trim();
540
717
  let chosen = null;
541
718
  if (baseDomain) {
542
719
  chosen = zones.find((z) => z.name === baseDomain) ?? null;
543
720
  if (!chosen) {
544
- fail(`[entrypoint] LAUNCHKIT_CF_BASE_DOMAIN="${baseDomain}" is not among the connected CF token's zones (${zones.map((z) => z.name).join(", ") || "none"}). Either change the env or grant Zone:Read on that zone in the CF token.`);
721
+ fail2(`[entrypoint] LAUNCHKIT_CF_BASE_DOMAIN="${baseDomain}" is not among the connected CF token's zones (${zones.map((z) => z.name).join(", ") || "none"}). Either change the env or grant Zone:Read on that zone in the CF token.`);
545
722
  }
546
723
  } else if (zones.length === 1) {
547
724
  chosen = { id: zones[0].id, name: zones[0].name };
548
725
  } else {
549
- fail(`[entrypoint] cloudflare token covers ${zones.length} zones (${zones.map((z) => z.name).join(", ")}) \u2014 set LAUNCHKIT_CF_BASE_DOMAIN to pick one.`);
726
+ fail2(`[entrypoint] cloudflare token covers ${zones.length} zones (${zones.map((z) => z.name).join(", ")}) \u2014 set LAUNCHKIT_CF_BASE_DOMAIN to pick one.`);
550
727
  }
551
728
  const stateFile = "/workspace/.launchpod/launch-kit-tunnel.json";
552
- console.log(`[entrypoint] provisioning CF named tunnel \u2014 name=launch-kit-${projectSlug} zone=${chosen.name} services=${services.map((s) => s.name).join(",")}`);
729
+ const slugLabel = projectSlug.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
730
+ const DNS_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
731
+ for (const s of services) {
732
+ const label = `${slugLabel}-${s.name}`;
733
+ if (!DNS_LABEL_RE.test(label)) {
734
+ fail2(`[entrypoint] hostname label "${label}" (${label.length} chars) is not a valid DNS label \u2014 must be 1\u201363 chars of [a-z0-9-] with no leading/trailing hyphen. Shorten the project slug ("${projectSlug}") or the service name ("${s.name}").`);
735
+ }
736
+ }
737
+ console.log(`[entrypoint] provisioning CF named tunnel \u2014 name=launch-kit-${projectSlug} zone=${chosen.name} services=${services.map((s) => `${slugLabel}-${s.name}`).join(",")}`);
553
738
  const result = await provisionIngress({
554
739
  apiToken: token,
555
740
  accountId,
556
741
  zone: chosen,
557
742
  tunnelName: `launch-kit-${projectSlug}`,
558
- services: services.map((s) => ({ name: s.name, port: s.port })),
743
+ services: services.map((s) => ({ name: s.name, label: `${slugLabel}-${s.name}`, port: s.port })),
559
744
  stateFile
560
745
  });
561
746
  for (const [name, fqdn] of Object.entries(result.hostnames)) {
@@ -563,6 +748,61 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
563
748
  }
564
749
  return result;
565
750
  }
751
+ var GATED_SERVICES = {
752
+ // Claude web terminal — live drivable shell ⇒ RCE surface ⇒ short session.
753
+ bot: { strict: true },
754
+ // The user's own dev/preview server — a workday-length session is fine.
755
+ preview: { strict: false }
756
+ };
757
+ async function registerOidcClient(serverUrl, pat, redirectUris) {
758
+ const res = await fetch(new URL("/api/rover/oidc-client", serverUrl), {
759
+ method: "POST",
760
+ headers: {
761
+ Authorization: `Bearer ${pat}`,
762
+ "Content-Type": "application/json",
763
+ Accept: "application/json"
764
+ },
765
+ body: JSON.stringify({ redirectUris }),
766
+ signal: AbortSignal.timeout(15e3)
767
+ });
768
+ const body = await res.json().catch(() => null);
769
+ if (!res.ok || !body?.success || !body.data) {
770
+ fail2(`[entrypoint] OIDC client provisioning failed (HTTP ${res.status}): ${body?.error ?? "unexpected response"}`);
771
+ }
772
+ return body.data;
773
+ }
774
+ async function maybeProvisionAccess(bundle, ingress) {
775
+ const token = bundle.cloudflareToken ?? null;
776
+ const accountId = bundle.cloudflareAccountId ?? null;
777
+ if (!token || !accountId) return;
778
+ const services = [];
779
+ const skipped = [];
780
+ for (const [name, hostname] of Object.entries(ingress.hostnames)) {
781
+ const cfg = GATED_SERVICES[name];
782
+ if (cfg) services.push({ hostname, strict: cfg.strict });
783
+ else skipped.push(name);
784
+ }
785
+ if (skipped.length > 0) {
786
+ console.log(`[entrypoint] CF Access: leaving machine surface(s) ungated: ${skipped.join(", ")}`);
787
+ }
788
+ if (services.length === 0) {
789
+ console.log("[entrypoint] CF Access: no human-facing service to gate (bot/preview not provisioned)");
790
+ return;
791
+ }
792
+ const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
793
+ const pat = requireEnv("LS_PAT");
794
+ const stateFile = "/workspace/.launchpod/launch-kit-access.json";
795
+ console.log(`[entrypoint] gating ${services.map((s) => s.hostname).join(", ")} behind CF Access (IdP: ${serverUrl})`);
796
+ const result = await provisionAccess({
797
+ apiToken: token,
798
+ accountId,
799
+ issuer: serverUrl,
800
+ services,
801
+ stateFile,
802
+ registerClient: (redirectUris) => registerOidcClient(serverUrl, pat, redirectUris)
803
+ });
804
+ console.log(`[entrypoint] CF Access gate live \u2014 IdP ${result.idpId}, auth domain ${result.authDomain}`);
805
+ }
566
806
  function spawnServiceGroup(services) {
567
807
  const children = [];
568
808
  let shuttingDown = false;
@@ -655,7 +895,7 @@ async function main() {
655
895
  try {
656
896
  services = resolveServices();
657
897
  } catch (err) {
658
- fail(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
898
+ fail2(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
659
899
  }
660
900
  console.log(`[entrypoint] services: ${services.map((s) => `${s.name}@${s.port}`).join(", ")}`);
661
901
  const ingress = await maybeProvisionIngress(bundle, services, requireEnv("LS_PROJECT_SLUG"));
@@ -664,8 +904,9 @@ async function main() {
664
904
  const radarFqdn = ingress.hostnames.radar;
665
905
  if (radarFqdn) process.env.RADAR_CF_TUNNEL_HOSTNAME = radarFqdn;
666
906
  else if (services.some((s) => s.name === "radar")) {
667
- fail(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
907
+ fail2(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
668
908
  }
909
+ await maybeProvisionAccess(bundle, ingress);
669
910
  } else if (services.length > 1) {
670
911
  const first = services[0];
671
912
  console.warn(
@@ -691,6 +932,7 @@ if (!process.env.VITEST) {
691
932
  }
692
933
  // Annotate the CommonJS export names for ESM import in node:
693
934
  0 && (module.exports = {
935
+ maybeProvisionAccess,
694
936
  maybeProvisionIngress,
695
937
  spawnServiceGroup
696
938
  });
File without changes
File without changes