@launchsecure/launch-kit 0.0.34 → 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,17 +153,25 @@ 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
+ // Claude web terminal exposes a viewable/drivable `claude` session at
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
+ bot: { port: 52849, bin: "launch-bot", args: ["serve"] }
156
160
  };
157
161
  var DNS_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
158
162
  function defaultServices() {
159
163
  return [expandShorthand("radar")];
160
164
  }
161
165
  function expandShorthand(name) {
166
+ if (name === "preview") {
167
+ const raw = process.env.PREVIEW_PORT;
168
+ const port = raw && Number.isFinite(Number.parseInt(raw, 10)) ? Number.parseInt(raw, 10) : 3e3;
169
+ return { name: "preview", port, bin: "", args: [], skipSpawn: true };
170
+ }
162
171
  const def = SHORTHANDS[name];
163
172
  if (!def) {
164
173
  throw new Error(
165
- `[launch-kit-services] unknown shorthand "${name}" \u2014 known: ${Object.keys(SHORTHANDS).join(", ")}. Use an object entry { name, port, bin, args } for custom services.`
174
+ `[launch-kit-services] unknown shorthand "${name}" \u2014 known: ${[...Object.keys(SHORTHANDS), "preview"].join(", ")}. Use an object entry { name, port, bin, args } for custom services.`
166
175
  );
167
176
  }
168
177
  return { name, port: def.port, bin: def.bin, args: [...def.args] };
@@ -243,13 +252,16 @@ function resolveServices(opts = {}) {
243
252
  }
244
253
  return validate(defaultServices());
245
254
  }
246
- var SHORTHAND_NAMES = Object.keys(SHORTHANDS);
255
+ var SHORTHAND_NAMES = [...Object.keys(SHORTHANDS), "preview"];
247
256
 
248
257
  // src/server/cf-ingress.ts
249
258
  var import_node_fs2 = require("node:fs");
250
259
  var import_node_path2 = require("node:path");
251
260
  var CF_API_BASE = "https://api.cloudflare.com/client/v4";
252
261
  var CF_ERR_DNS_RECORD_EXISTS = 81053;
262
+ function serviceLabel(s) {
263
+ return s.label ?? s.name;
264
+ }
253
265
  async function cf(opts) {
254
266
  const res = await fetch(`${CF_API_BASE}${opts.path}`, {
255
267
  method: opts.method,
@@ -329,7 +341,7 @@ async function fetchConnectorToken(input, tunnelId) {
329
341
  }
330
342
  async function setIngressConfig(input, tunnelId) {
331
343
  const ingress = input.services.map((s) => ({
332
- hostname: `${s.name}.${input.zone.name}`,
344
+ hostname: `${serviceLabel(s)}.${input.zone.name}`,
333
345
  service: `http://localhost:${s.port}`
334
346
  }));
335
347
  ingress.push({ service: "http_status:404" });
@@ -344,7 +356,7 @@ async function setIngressConfig(input, tunnelId) {
344
356
  }
345
357
  }
346
358
  async function ensureDnsRecord(input, tunnelId, service) {
347
- const fqdn = `${service.name}.${input.zone.name}`;
359
+ const fqdn = `${serviceLabel(service)}.${input.zone.name}`;
348
360
  const target = `${tunnelId}.cfargotunnel.com`;
349
361
  const existing = await cf({
350
362
  apiToken: input.apiToken,
@@ -388,10 +400,181 @@ async function provisionIngress(input) {
388
400
  await setIngressConfig(input, tunnelId);
389
401
  await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
390
402
  const hostnames = {};
391
- 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}`;
392
404
  return { tunnelId, connectorToken, hostnames };
393
405
  }
394
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
+
395
578
  // src/server/radar-docker-init-entry.ts
396
579
  var REQUIRED_ENV = [
397
580
  "CLAUDE_CREDENTIALS_B64",
@@ -399,13 +582,13 @@ var REQUIRED_ENV = [
399
582
  "LS_ORG_SLUG",
400
583
  "LS_PROJECT_SLUG"
401
584
  ];
402
- function fail(message) {
585
+ function fail2(message) {
403
586
  console.error(message);
404
587
  process.exit(1);
405
588
  }
406
589
  function requireEnv(name) {
407
590
  const v = process.env[name];
408
- if (!v) fail(`ERROR: ${name} is required but not set`);
591
+ if (!v) fail2(`ERROR: ${name} is required but not set`);
409
592
  return v;
410
593
  }
411
594
  function run(cmd, args, stdio = "inherit") {
@@ -422,13 +605,16 @@ async function setupFromCloud() {
422
605
  try {
423
606
  bundle = await mcp.call("radar_bootstrap_get", {});
424
607
  } catch (err) {
425
- 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.`);
426
609
  }
427
610
  if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
428
611
  if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
429
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
+ }
430
616
  if (!process.env.GH_TOKEN) {
431
- 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.`);
432
618
  }
433
619
  const cfNote = bundle.cloudflareToken ? "cloudflare=connected" : "cloudflare=none";
434
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}`);
@@ -436,17 +622,17 @@ async function setupFromCloud() {
436
622
  }
437
623
  function setupClaudeCredentials() {
438
624
  const home = process.env.HOME ?? "/home/launchpod";
439
- const claudeDir = (0, import_node_path3.join)(home, ".claude");
440
- (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 });
441
627
  const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
442
- const credsPath = (0, import_node_path3.join)(claudeDir, ".credentials.json");
443
- (0, import_node_fs3.writeFileSync)(credsPath, decoded);
444
- (0, import_node_fs3.chmodSync)(credsPath, 384);
445
- 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");
446
632
  let cfg = {};
447
- if ((0, import_node_fs3.existsSync)(configPath)) {
633
+ if ((0, import_node_fs4.existsSync)(configPath)) {
448
634
  try {
449
- cfg = JSON.parse((0, import_node_fs3.readFileSync)(configPath, "utf8"));
635
+ cfg = JSON.parse((0, import_node_fs4.readFileSync)(configPath, "utf8"));
450
636
  } catch {
451
637
  cfg = {};
452
638
  }
@@ -455,18 +641,56 @@ function setupClaudeCredentials() {
455
641
  cfg.lastOnboardingVersion = cfg.lastOnboardingVersion ?? "2.1.159";
456
642
  cfg.numStartups = (cfg.numStartups ?? 0) + 1;
457
643
  cfg.installMethod = cfg.installMethod ?? "global";
458
- (0, import_node_fs3.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
459
- (0, import_node_fs3.chmodSync)(configPath, 384);
644
+ const PREAPPROVED_MCPS = [
645
+ "launch-secure",
646
+ "launch-chart",
647
+ "launch-deck",
648
+ "launch-orbit",
649
+ "launch-recall",
650
+ "launch-beacon",
651
+ "launch-sequencer"
652
+ ];
653
+ const projects = cfg.projects ?? {};
654
+ const wsKey = "/workspace";
655
+ const wsProject = projects[wsKey] ?? {};
656
+ const existingEnabled = Array.isArray(wsProject.enabledMcpjsonServers) ? wsProject.enabledMcpjsonServers : [];
657
+ const mergedEnabled = Array.from(/* @__PURE__ */ new Set([...existingEnabled, ...PREAPPROVED_MCPS]));
658
+ wsProject.enabledMcpjsonServers = mergedEnabled;
659
+ projects[wsKey] = wsProject;
660
+ cfg.projects = projects;
661
+ (0, import_node_fs4.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
662
+ (0, import_node_fs4.chmodSync)(configPath, 384);
460
663
  }
461
664
  function setupGitAndGh() {
462
665
  const name = process.env.GIT_USER_NAME ?? "Radar Bot";
463
666
  const email = process.env.GIT_USER_EMAIL ?? "radar@launchpod.local";
464
667
  const status = run("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
465
- 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})`);
669
+ }
670
+ function detectAndSetPreviewPort() {
671
+ if (process.env.PREVIEW_PORT) return;
672
+ try {
673
+ const pkgPath = "/workspace/package.json";
674
+ if (!(0, import_node_fs4.existsSync)(pkgPath)) return;
675
+ const pkg = JSON.parse((0, import_node_fs4.readFileSync)(pkgPath, "utf-8"));
676
+ const scripts = pkg.scripts ?? {};
677
+ const portRe = /(?:--port[= ]|-p\s+|\bPORT=)(\d{2,5})\b/;
678
+ for (const name of ["dev", "start", "serve"]) {
679
+ const script = scripts[name];
680
+ if (typeof script !== "string") continue;
681
+ const m = script.match(portRe);
682
+ if (m) {
683
+ process.env.PREVIEW_PORT = m[1];
684
+ console.log(`[entrypoint] preview port detected from package.json scripts.${name}: ${m[1]}`);
685
+ return;
686
+ }
687
+ }
688
+ } catch {
689
+ }
466
690
  }
467
691
  function initWorkspaceIfEmpty() {
468
692
  process.chdir("/workspace");
469
- if ((0, import_node_fs3.existsSync)(".git")) {
693
+ if ((0, import_node_fs4.existsSync)(".git")) {
470
694
  console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
471
695
  return;
472
696
  }
@@ -479,7 +703,7 @@ function initWorkspaceIfEmpty() {
479
703
  `--url=${process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app"}`,
480
704
  `--dir=/workspace`
481
705
  ]);
482
- if (status !== 0) fail(`[entrypoint] launch-kit init failed (status ${status})`);
706
+ if (status !== 0) fail2(`[entrypoint] launch-kit init failed (status ${status})`);
483
707
  }
484
708
  async function maybeProvisionIngress(bundle, services, projectSlug) {
485
709
  const token = bundle.cloudflareToken ?? null;
@@ -487,28 +711,36 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
487
711
  const zones = bundle.cloudflareZones ?? [];
488
712
  if (!token && !accountId && zones.length === 0) return null;
489
713
  if (!token || !accountId) {
490
- 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.`);
491
715
  }
492
716
  const baseDomain = process.env.LAUNCHKIT_CF_BASE_DOMAIN?.trim();
493
717
  let chosen = null;
494
718
  if (baseDomain) {
495
719
  chosen = zones.find((z) => z.name === baseDomain) ?? null;
496
720
  if (!chosen) {
497
- 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.`);
498
722
  }
499
723
  } else if (zones.length === 1) {
500
724
  chosen = { id: zones[0].id, name: zones[0].name };
501
725
  } else {
502
- 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.`);
503
727
  }
504
728
  const stateFile = "/workspace/.launchpod/launch-kit-tunnel.json";
505
- 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(",")}`);
506
738
  const result = await provisionIngress({
507
739
  apiToken: token,
508
740
  accountId,
509
741
  zone: chosen,
510
742
  tunnelName: `launch-kit-${projectSlug}`,
511
- 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 })),
512
744
  stateFile
513
745
  });
514
746
  for (const [name, fqdn] of Object.entries(result.hostnames)) {
@@ -516,6 +748,61 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
516
748
  }
517
749
  return result;
518
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
+ }
519
806
  function spawnServiceGroup(services) {
520
807
  const children = [];
521
808
  let shuttingDown = false;
@@ -563,6 +850,10 @@ function spawnServiceGroup(services) {
563
850
  let exitedCount = 0;
564
851
  let firstFailure = null;
565
852
  for (const spec of services) {
853
+ if (spec.skipSpawn) {
854
+ console.log(`[entrypoint] ${spec.name} \u2192 ingress-only on port ${spec.port} (no spawn; user starts dev server here)`);
855
+ continue;
856
+ }
566
857
  const args = [...spec.args, "--port", String(spec.port)];
567
858
  console.log(`[entrypoint] starting ${spec.name}: ${spec.bin} ${args.join(" ")}`);
568
859
  const proc = (0, import_node_child_process.spawn)(spec.bin, args, { stdio: ["ignore", "pipe", "pipe"] });
@@ -599,11 +890,12 @@ async function main() {
599
890
  setupClaudeCredentials();
600
891
  setupGitAndGh();
601
892
  initWorkspaceIfEmpty();
893
+ detectAndSetPreviewPort();
602
894
  let services;
603
895
  try {
604
896
  services = resolveServices();
605
897
  } catch (err) {
606
- fail(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
898
+ fail2(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
607
899
  }
608
900
  console.log(`[entrypoint] services: ${services.map((s) => `${s.name}@${s.port}`).join(", ")}`);
609
901
  const ingress = await maybeProvisionIngress(bundle, services, requireEnv("LS_PROJECT_SLUG"));
@@ -612,8 +904,9 @@ async function main() {
612
904
  const radarFqdn = ingress.hostnames.radar;
613
905
  if (radarFqdn) process.env.RADAR_CF_TUNNEL_HOSTNAME = radarFqdn;
614
906
  else if (services.some((s) => s.name === "radar")) {
615
- fail(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
907
+ fail2(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
616
908
  }
909
+ await maybeProvisionAccess(bundle, ingress);
617
910
  } else if (services.length > 1) {
618
911
  const first = services[0];
619
912
  console.warn(
@@ -639,6 +932,7 @@ if (!process.env.VITEST) {
639
932
  }
640
933
  // Annotate the CommonJS export names for ESM import in node:
641
934
  0 && (module.exports = {
935
+ maybeProvisionAccess,
642
936
  maybeProvisionIngress,
643
937
  spawnServiceGroup
644
938
  });