@launchsecure/launch-kit 0.0.35 → 0.0.37

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.
Files changed (83) hide show
  1. package/dist/chart-client/assets/index-DJrjyXbN.css +1 -0
  2. package/dist/chart-client/index.html +2 -2
  3. package/dist/client/assets/index-8eSXr3Ez.css +32 -0
  4. package/dist/client/index.html +2 -2
  5. package/dist/council-client/assets/index-4K0t2WrZ.css +1 -0
  6. package/dist/council-client/index.html +2 -2
  7. package/dist/deck-client/assets/{_baseUniq-BiVx0WO_.js → _baseUniq-Cn5TyL9s.js} +1 -1
  8. package/dist/deck-client/assets/{arc-DGMkiEzS.js → arc-D61amKYu.js} +1 -1
  9. package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-Y2WRmHtk.js → architectureDiagram-Q4EWVU46-CpKrvC2W.js} +1 -1
  10. package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-_Lbfu5BQ.js → blockDiagram-DXYQGD6D-Yj5OjxvG.js} +1 -1
  11. package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CTqpYTBX.js → c4Diagram-AHTNJAMY-BIR810Tv.js} +1 -1
  12. package/dist/deck-client/assets/channel-DrJz2x-n.js +1 -0
  13. package/dist/deck-client/assets/{chunk-4BX2VUAB-liEIbPHs.js → chunk-4BX2VUAB-BeSHwGvx.js} +1 -1
  14. package/dist/deck-client/assets/{chunk-4TB4RGXK-CCc6lYvL.js → chunk-4TB4RGXK-CCqzsLpg.js} +1 -1
  15. package/dist/deck-client/assets/{chunk-55IACEB6-D02jJUR2.js → chunk-55IACEB6-CuW_aq4-.js} +1 -1
  16. package/dist/deck-client/assets/{chunk-EDXVE4YY-BFmGMbLD.js → chunk-EDXVE4YY-Dl35ixYh.js} +1 -1
  17. package/dist/deck-client/assets/{chunk-FMBD7UC4-6wFLOVcJ.js → chunk-FMBD7UC4-TwreZQTv.js} +1 -1
  18. package/dist/deck-client/assets/{chunk-OYMX7WX6-Bnr8RiBf.js → chunk-OYMX7WX6-Ahfw8EUo.js} +1 -1
  19. package/dist/deck-client/assets/{chunk-QZHKN3VN-Ct82MksJ.js → chunk-QZHKN3VN-DlE_zlU-.js} +1 -1
  20. package/dist/deck-client/assets/{chunk-YZCP3GAM-BXmN1diQ.js → chunk-YZCP3GAM-Dj6QWzSg.js} +1 -1
  21. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-a3tg9w7z.js +1 -0
  22. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-a3tg9w7z.js +1 -0
  23. package/dist/deck-client/assets/clone-Dd7JBCL5.js +1 -0
  24. package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-CmQCT-mH.js → cose-bilkent-S5V4N54A-BO1z5aOM.js} +1 -1
  25. package/dist/deck-client/assets/{dagre-KV5264BT-DDdSa9EX.js → dagre-KV5264BT-DVsw17fE.js} +1 -1
  26. package/dist/deck-client/assets/{diagram-5BDNPKRD-Bccks2xJ.js → diagram-5BDNPKRD-6jYs7oZk.js} +1 -1
  27. package/dist/deck-client/assets/{diagram-G4DWMVQ6-CPPNgxmQ.js → diagram-G4DWMVQ6-6DbggeGE.js} +1 -1
  28. package/dist/deck-client/assets/{diagram-MMDJMWI5-KrD300pS.js → diagram-MMDJMWI5-CQtk1cSU.js} +1 -1
  29. package/dist/deck-client/assets/{diagram-TYMM5635-DefnLuQf.js → diagram-TYMM5635-BR-gt75b.js} +1 -1
  30. package/dist/deck-client/assets/{erDiagram-SMLLAGMA-DI9FfnFP.js → erDiagram-SMLLAGMA-C9qMtjdY.js} +1 -1
  31. package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-twKyd3Fx.js → flowDiagram-DWJPFMVM-CdaPhPYb.js} +1 -1
  32. package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-Wau3jhBr.js → ganttDiagram-T4ZO3ILL-BRsZWUy4.js} +1 -1
  33. package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-D9GgYXwb.js → gitGraphDiagram-UUTBAWPF-B8Z90jCj.js} +1 -1
  34. package/dist/deck-client/assets/{graph-BhNLzyXS.js → graph-my2Zphm4.js} +1 -1
  35. package/dist/deck-client/assets/index-ByqxPEgU.css +1 -0
  36. package/dist/deck-client/assets/{index-BtQBaQ7s.js → index-DqAoYZwV.js} +43 -42
  37. package/dist/deck-client/assets/{infoDiagram-42DDH7IO-TylGlSG-.js → infoDiagram-42DDH7IO-Csr9loin.js} +1 -1
  38. package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DAT8icpg.js → ishikawaDiagram-UXIWVN3A-HWdvUNFi.js} +1 -1
  39. package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-D3v_XL72.js → journeyDiagram-VCZTEJTY-CjYHG6EM.js} +1 -1
  40. package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-DNUOBiNr.js → kanban-definition-6JOO6SKY-CX3JdUu7.js} +1 -1
  41. package/dist/deck-client/assets/{layout-COfodgwF.js → layout-Bcucv5Gi.js} +1 -1
  42. package/dist/deck-client/assets/{linear-DmTsuIvK.js → linear-CUGM5FJZ.js} +1 -1
  43. package/dist/deck-client/assets/{min-BW1F7i1D.js → min-Dw4g5w9z.js} +1 -1
  44. package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-CErFzKWl.js → mindmap-definition-QFDTVHPH-C8oo61fg.js} +1 -1
  45. package/dist/deck-client/assets/{pieDiagram-DEJITSTG-DW5F757o.js → pieDiagram-DEJITSTG-D2WYGkq8.js} +1 -1
  46. package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-B1S2-TfI.js → quadrantDiagram-34T5L4WZ-Vh00GISt.js} +1 -1
  47. package/dist/deck-client/assets/{requirementDiagram-MS252O5E-BY5BAR-5.js → requirementDiagram-MS252O5E-DxI-DFrN.js} +1 -1
  48. package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-CE1Cp9HS.js → sankeyDiagram-XADWPNL6-QgwyjasI.js} +1 -1
  49. package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-IaHnbKye.js → sequenceDiagram-FGHM5R23-DmOmD5Ni.js} +1 -1
  50. package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-CwPJm9hU.js → stateDiagram-FHFEXIEX-CRwglGg_.js} +1 -1
  51. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-BvZLEWAA.js +1 -0
  52. package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-DVFGGSgN.js → timeline-definition-GMOUNBTQ-Dj9YGKOh.js} +1 -1
  53. package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-C1194MJi.js → vennDiagram-DHZGUBPP-xzIaOzEU.js} +1 -1
  54. package/dist/deck-client/assets/wardley-RL74JXVD-CEAay09T.js +162 -0
  55. package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-hpwdFfGj.js → wardleyDiagram-NUSXRM2D-BIYYh-JZ.js} +1 -1
  56. package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-DYkotwy8.js → xychartDiagram-5P7HB3ND-Cy9EoJCh.js} +1 -1
  57. package/dist/deck-client/index.html +2 -2
  58. package/dist/server/cli.js +261 -26
  59. package/dist/server/council-entry.js +86 -2
  60. package/dist/server/council-serve.js +81 -2
  61. package/dist/server/deck-mcp-entry.js +449 -68
  62. package/dist/server/deck-serve.js +411 -42
  63. package/dist/server/init-entry.js +732 -237
  64. package/dist/server/orbit-entry.js +880 -144
  65. package/dist/server/radar-docker-init-entry.js +371 -37
  66. package/dist/server/rover-entry.js +108 -20
  67. package/package.json +1 -1
  68. package/scaffolds/ls-marketplace/plugins/kit/skills/deploy-check/SKILL.md +5 -0
  69. package/scaffolds/ls-marketplace/plugins/kit/skills/kickoff/SKILL.md +20 -4
  70. package/scaffolds/ls-marketplace/plugins/kit/skills/orbit/SKILL.md +27 -7
  71. package/dist/chart-client/assets/index-DpKO9p0s.css +0 -1
  72. package/dist/client/assets/index-Dv6dD2zY.css +0 -32
  73. package/dist/council-client/assets/index-AqQ9Sei6.css +0 -1
  74. package/dist/deck-client/assets/channel-DB6LxW_l.js +0 -1
  75. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-g944ZyG8.js +0 -1
  76. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-g944ZyG8.js +0 -1
  77. package/dist/deck-client/assets/clone-DiIRH1pI.js +0 -1
  78. package/dist/deck-client/assets/index-B-YQq5b5.css +0 -1
  79. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-DQYa2M1q.js +0 -1
  80. package/dist/deck-client/assets/wardley-RL74JXVD-CHZiUbBa.js +0 -162
  81. /package/dist/chart-client/assets/{index-DFu2xIrM.js → index-BgUxHxwE.js} +0 -0
  82. /package/dist/client/assets/{index-Cbw6bVdx.js → index-CUivaQnN.js} +0 -0
  83. /package/dist/council-client/assets/{index-CAsmGTzg.js → index-DN8HN_5K.js} +0 -0
@@ -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,
@@ -283,6 +286,17 @@ async function cf(opts) {
283
286
  function isNotFound(env) {
284
287
  return !env.success && (env.errors ?? []).some((e) => e.code === 7003 || e.code === 1001 || e.code === 81044);
285
288
  }
289
+ async function findTunnelByName(input) {
290
+ const q = new URLSearchParams({ name: input.tunnelName, is_deleted: "false" }).toString();
291
+ const res = await cf({
292
+ apiToken: input.apiToken,
293
+ method: "GET",
294
+ path: `/accounts/${input.accountId}/cfd_tunnel?${q}`
295
+ });
296
+ if (!res.success || !Array.isArray(res.result)) return null;
297
+ const live = res.result.find((t) => t.name === input.tunnelName && !t.deleted_at);
298
+ return live?.id ?? null;
299
+ }
286
300
  function loadState(path) {
287
301
  if (!(0, import_node_fs2.existsSync)(path)) return null;
288
302
  try {
@@ -314,16 +328,26 @@ async function ensureTunnel(input, knownTunnelId) {
314
328
  throw new Error(`[cf] tunnel GET failed: ${JSON.stringify(got.errors)}`);
315
329
  }
316
330
  }
331
+ const existing = await findTunnelByName(input);
332
+ if (existing) {
333
+ console.log(`[cf] adopted existing tunnel "${input.tunnelName}" (${existing}) \u2014 local state was missing`);
334
+ return existing;
335
+ }
317
336
  const created = await cf({
318
337
  apiToken: input.apiToken,
319
338
  method: "POST",
320
339
  path: `/accounts/${input.accountId}/cfd_tunnel`,
321
340
  body: { name: input.tunnelName, config_src: "cloudflare" }
322
341
  });
323
- if (!created.success || !created.result) {
324
- throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
342
+ if (created.success && created.result) return created.result.id;
343
+ if ((created.errors ?? []).some((e) => e.code === 1013)) {
344
+ const adopted = await findTunnelByName(input);
345
+ if (adopted) {
346
+ console.log(`[cf] tunnel "${input.tunnelName}" already existed (1013) \u2014 adopted ${adopted}`);
347
+ return adopted;
348
+ }
325
349
  }
326
- return created.result.id;
350
+ throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
327
351
  }
328
352
  async function fetchConnectorToken(input, tunnelId) {
329
353
  const res = await cf({
@@ -338,7 +362,7 @@ async function fetchConnectorToken(input, tunnelId) {
338
362
  }
339
363
  async function setIngressConfig(input, tunnelId) {
340
364
  const ingress = input.services.map((s) => ({
341
- hostname: `${s.name}.${input.zone.name}`,
365
+ hostname: `${serviceLabel(s)}.${input.zone.name}`,
342
366
  service: `http://localhost:${s.port}`
343
367
  }));
344
368
  ingress.push({ service: "http_status:404" });
@@ -353,7 +377,7 @@ async function setIngressConfig(input, tunnelId) {
353
377
  }
354
378
  }
355
379
  async function ensureDnsRecord(input, tunnelId, service) {
356
- const fqdn = `${service.name}.${input.zone.name}`;
380
+ const fqdn = `${serviceLabel(service)}.${input.zone.name}`;
357
381
  const target = `${tunnelId}.cfargotunnel.com`;
358
382
  const existing = await cf({
359
383
  apiToken: input.apiToken,
@@ -397,10 +421,181 @@ async function provisionIngress(input) {
397
421
  await setIngressConfig(input, tunnelId);
398
422
  await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
399
423
  const hostnames = {};
400
- for (const s of input.services) hostnames[s.name] = `${s.name}.${input.zone.name}`;
424
+ for (const s of input.services) hostnames[s.name] = `${serviceLabel(s)}.${input.zone.name}`;
401
425
  return { tunnelId, connectorToken, hostnames };
402
426
  }
403
427
 
428
+ // src/server/cf-access.ts
429
+ var import_node_fs3 = require("node:fs");
430
+ var import_node_path3 = require("node:path");
431
+ var CF_API_BASE2 = "https://api.cloudflare.com/client/v4";
432
+ var IDP_NAME = "launch-kit-oidc";
433
+ async function cf2(opts) {
434
+ const res = await fetch(`${CF_API_BASE2}${opts.path}`, {
435
+ method: opts.method,
436
+ headers: {
437
+ Authorization: `Bearer ${opts.apiToken}`,
438
+ "Content-Type": "application/json",
439
+ Accept: "application/json",
440
+ "User-Agent": "launch-kit/cf-access"
441
+ },
442
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
443
+ signal: AbortSignal.timeout(15e3)
444
+ });
445
+ const text = await res.text();
446
+ try {
447
+ return text ? JSON.parse(text) : { success: false };
448
+ } catch {
449
+ throw new Error(`[cf-access] ${opts.method} ${opts.path} \u2192 ${res.status}, non-JSON: ${text.slice(0, 200)}`);
450
+ }
451
+ }
452
+ function fail(env, what) {
453
+ throw new Error(`[cf-access] ${what} failed: ${JSON.stringify(env.errors)}`);
454
+ }
455
+ function loadState2(path) {
456
+ if (!(0, import_node_fs3.existsSync)(path)) return null;
457
+ try {
458
+ const parsed = JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
459
+ return typeof parsed?.accountId === "string" ? parsed : null;
460
+ } catch {
461
+ return null;
462
+ }
463
+ }
464
+ function saveState2(path, state) {
465
+ const dir = (0, import_node_path3.dirname)(path);
466
+ if (!(0, import_node_fs3.existsSync)(dir)) (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
467
+ (0, import_node_fs3.writeFileSync)(path, JSON.stringify(state, null, 2));
468
+ }
469
+ async function getAccessAuthDomain(apiToken, accountId) {
470
+ const res = await cf2({
471
+ apiToken,
472
+ method: "GET",
473
+ path: `/accounts/${accountId}/access/organizations`
474
+ });
475
+ if (!res.success || !res.result?.auth_domain) {
476
+ fail(res, "GET access/organizations (is Zero Trust enabled on this account?)");
477
+ }
478
+ return res.result.auth_domain;
479
+ }
480
+ async function ensureAccessIdp(input) {
481
+ const config = {
482
+ client_id: input.clientId,
483
+ client_secret: input.clientSecret,
484
+ auth_url: `${input.issuer}/api/oidc/authorize`,
485
+ token_url: `${input.issuer}/api/oidc/token`,
486
+ certs_url: `${input.issuer}/.well-known/jwks.json`,
487
+ scopes: ["openid", "email", "profile"],
488
+ claims: ["org", "project_access", "roles", "email"],
489
+ email_claim_name: "email",
490
+ pkce_enabled: true
491
+ };
492
+ const body = { name: IDP_NAME, type: "oidc", config };
493
+ let idpId = input.knownIdpId;
494
+ if (!idpId) {
495
+ const list = await cf2({
496
+ apiToken: input.apiToken,
497
+ method: "GET",
498
+ path: `/accounts/${input.accountId}/access/identity_providers`
499
+ });
500
+ if (!list.success) fail(list, "list identity_providers");
501
+ idpId = (list.result ?? []).find((p) => p.name === IDP_NAME)?.id ?? null;
502
+ }
503
+ if (idpId) {
504
+ const upd = await cf2({
505
+ apiToken: input.apiToken,
506
+ method: "PUT",
507
+ path: `/accounts/${input.accountId}/access/identity_providers/${idpId}`,
508
+ body
509
+ });
510
+ if (!upd.success || !upd.result) fail(upd, "update identity_provider");
511
+ return upd.result.id;
512
+ }
513
+ const created = await cf2({
514
+ apiToken: input.apiToken,
515
+ method: "POST",
516
+ path: `/accounts/${input.accountId}/access/identity_providers`,
517
+ body
518
+ });
519
+ if (!created.success || !created.result) fail(created, "create identity_provider");
520
+ return created.result.id;
521
+ }
522
+ async function ensureAccessApp(input) {
523
+ const policy = {
524
+ name: "launch-kit-org-allow",
525
+ decision: "allow",
526
+ include: [
527
+ {
528
+ oidc: {
529
+ identity_provider_id: input.idpId,
530
+ claim_name: "org",
531
+ claim_value: input.organizationId
532
+ }
533
+ }
534
+ ]
535
+ };
536
+ const body = {
537
+ name: `launch-kit ${input.service.hostname}`,
538
+ domain: input.service.hostname,
539
+ type: "self_hosted",
540
+ // Bot terminal = RCE surface → short session. Read portals = a workday.
541
+ session_duration: input.service.strict ? "30m" : "24h",
542
+ allowed_idps: [input.idpId],
543
+ auto_redirect_to_identity: true,
544
+ policies: [policy]
545
+ };
546
+ const list = await cf2({
547
+ apiToken: input.apiToken,
548
+ method: "GET",
549
+ path: `/accounts/${input.accountId}/access/apps`
550
+ });
551
+ if (!list.success) fail(list, "list access apps");
552
+ const existing = (list.result ?? []).find((a) => a.domain === input.service.hostname);
553
+ if (existing) {
554
+ const upd = await cf2({
555
+ apiToken: input.apiToken,
556
+ method: "PUT",
557
+ path: `/accounts/${input.accountId}/access/apps/${existing.id}`,
558
+ body
559
+ });
560
+ if (!upd.success || !upd.result) fail(upd, `update access app ${input.service.hostname}`);
561
+ return upd.result.id;
562
+ }
563
+ const created = await cf2({
564
+ apiToken: input.apiToken,
565
+ method: "POST",
566
+ path: `/accounts/${input.accountId}/access/apps`,
567
+ body
568
+ });
569
+ if (!created.success || !created.result) fail(created, `create access app ${input.service.hostname}`);
570
+ return created.result.id;
571
+ }
572
+ async function provisionAccess(input) {
573
+ const authDomain = await getAccessAuthDomain(input.apiToken, input.accountId);
574
+ const callbackUrl = `https://${authDomain}/cdn-cgi/access/callback`;
575
+ const { clientId, clientSecret, organizationId } = await input.registerClient([callbackUrl]);
576
+ const prior = loadState2(input.stateFile);
577
+ const idpId = await ensureAccessIdp({
578
+ apiToken: input.apiToken,
579
+ accountId: input.accountId,
580
+ issuer: input.issuer,
581
+ clientId,
582
+ clientSecret,
583
+ knownIdpId: prior?.idpId ?? null
584
+ });
585
+ saveState2(input.stateFile, { idpId, accountId: input.accountId });
586
+ const appIds = {};
587
+ for (const service of input.services) {
588
+ appIds[service.hostname] = await ensureAccessApp({
589
+ apiToken: input.apiToken,
590
+ accountId: input.accountId,
591
+ idpId,
592
+ organizationId,
593
+ service
594
+ });
595
+ }
596
+ return { idpId, authDomain, appIds };
597
+ }
598
+
404
599
  // src/server/radar-docker-init-entry.ts
405
600
  var REQUIRED_ENV = [
406
601
  "CLAUDE_CREDENTIALS_B64",
@@ -408,19 +603,77 @@ var REQUIRED_ENV = [
408
603
  "LS_ORG_SLUG",
409
604
  "LS_PROJECT_SLUG"
410
605
  ];
411
- function fail(message) {
606
+ function fail2(message) {
412
607
  console.error(message);
413
608
  process.exit(1);
414
609
  }
415
610
  function requireEnv(name) {
416
611
  const v = process.env[name];
417
- if (!v) fail(`ERROR: ${name} is required but not set`);
612
+ if (!v) fail2(`ERROR: ${name} is required but not set`);
418
613
  return v;
419
614
  }
420
615
  function run(cmd, args, stdio = "inherit") {
421
616
  const r = (0, import_node_child_process.spawnSync)(cmd, args, { stdio });
422
617
  return r.status ?? 1;
423
618
  }
619
+ var LAUNCHPOD_DIR = "/workspace/.launchpod";
620
+ var CRASH_STATE_FILE = (0, import_node_path4.join)(LAUNCHPOD_DIR, ".boot-crash.json");
621
+ var MAX_BOOT_CRASHES = 5;
622
+ var STABLE_AFTER_MS = 3e4;
623
+ function readCrashState() {
624
+ try {
625
+ const s = JSON.parse((0, import_node_fs4.readFileSync)(CRASH_STATE_FILE, "utf8"));
626
+ return typeof s?.count === "number" && s.count >= 0 ? s : null;
627
+ } catch {
628
+ return null;
629
+ }
630
+ }
631
+ function bumpCrashCount() {
632
+ const prev = readCrashState();
633
+ const now = (/* @__PURE__ */ new Date()).toISOString();
634
+ const next = {
635
+ count: (prev?.count ?? 0) + 1,
636
+ firstAt: prev?.firstAt ?? now,
637
+ lastAt: now
638
+ };
639
+ try {
640
+ (0, import_node_fs4.mkdirSync)(LAUNCHPOD_DIR, { recursive: true });
641
+ (0, import_node_fs4.writeFileSync)(CRASH_STATE_FILE, JSON.stringify(next, null, 2));
642
+ } catch (err) {
643
+ console.warn(`[entrypoint] could not persist boot-crash counter (continuing unprotected): ${err instanceof Error ? err.message : String(err)}`);
644
+ }
645
+ return next.count;
646
+ }
647
+ function clearCrashCount() {
648
+ try {
649
+ if ((0, import_node_fs4.existsSync)(CRASH_STATE_FILE)) (0, import_node_fs4.writeFileSync)(CRASH_STATE_FILE, JSON.stringify({ count: 0, firstAt: "", lastAt: "" }));
650
+ } catch {
651
+ }
652
+ }
653
+ async function parkAfterCrashLoop(count) {
654
+ const lines = [
655
+ "==================================================================",
656
+ `[entrypoint] CRASH-LOOP HALT \u2014 ${count} consecutive failed boots (cap ${MAX_BOOT_CRASHES}).`,
657
+ "[entrypoint] Refusing to restart again. Container is now PARKED (idle, not",
658
+ "[entrypoint] exiting) so it stops thrashing CF APIs and logs. Fix the root",
659
+ "[entrypoint] cause, clear the counter, then restart the container:",
660
+ `[entrypoint] rm ${CRASH_STATE_FILE} && docker restart <container>`,
661
+ "=================================================================="
662
+ ];
663
+ for (const l of lines) console.error(l);
664
+ for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
665
+ process.on(sig, () => {
666
+ console.log(`[entrypoint] received ${sig} while parked \u2014 exiting`);
667
+ process.exit(0);
668
+ });
669
+ }
670
+ setInterval(() => {
671
+ console.error(`[entrypoint] still parked after crash-loop halt \u2014 clear ${CRASH_STATE_FILE} and restart to retry`);
672
+ }, 15 * 6e4);
673
+ await new Promise(() => {
674
+ });
675
+ throw new Error("unreachable");
676
+ }
424
677
  async function setupFromCloud() {
425
678
  const pat = requireEnv("LS_PAT");
426
679
  const orgSlug = requireEnv("LS_ORG_SLUG");
@@ -431,13 +684,16 @@ async function setupFromCloud() {
431
684
  try {
432
685
  bundle = await mcp.call("radar_bootstrap_get", {});
433
686
  } 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.`);
687
+ 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
688
  }
436
689
  if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
437
690
  if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
438
691
  if (!process.env.GH_TOKEN && bundle.githubToken) process.env.GH_TOKEN = bundle.githubToken;
692
+ if (!process.env.RADAR_RULES && Array.isArray(bundle.radarRules) && bundle.radarRules.length > 0) {
693
+ process.env.RADAR_RULES = JSON.stringify(bundle.radarRules);
694
+ }
439
695
  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.`);
696
+ 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
697
  }
442
698
  const cfNote = bundle.cloudflareToken ? "cloudflare=connected" : "cloudflare=none";
443
699
  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 +701,17 @@ async function setupFromCloud() {
445
701
  }
446
702
  function setupClaudeCredentials() {
447
703
  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 });
704
+ const claudeDir = (0, import_node_path4.join)(home, ".claude");
705
+ (0, import_node_fs4.mkdirSync)(claudeDir, { recursive: true });
450
706
  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");
707
+ const credsPath = (0, import_node_path4.join)(claudeDir, ".credentials.json");
708
+ (0, import_node_fs4.writeFileSync)(credsPath, decoded);
709
+ (0, import_node_fs4.chmodSync)(credsPath, 384);
710
+ const configPath = (0, import_node_path4.join)(home, ".claude.json");
455
711
  let cfg = {};
456
- if ((0, import_node_fs3.existsSync)(configPath)) {
712
+ if ((0, import_node_fs4.existsSync)(configPath)) {
457
713
  try {
458
- cfg = JSON.parse((0, import_node_fs3.readFileSync)(configPath, "utf8"));
714
+ cfg = JSON.parse((0, import_node_fs4.readFileSync)(configPath, "utf8"));
459
715
  } catch {
460
716
  cfg = {};
461
717
  }
@@ -481,21 +737,21 @@ function setupClaudeCredentials() {
481
737
  wsProject.enabledMcpjsonServers = mergedEnabled;
482
738
  projects[wsKey] = wsProject;
483
739
  cfg.projects = projects;
484
- (0, import_node_fs3.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
485
- (0, import_node_fs3.chmodSync)(configPath, 384);
740
+ (0, import_node_fs4.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
741
+ (0, import_node_fs4.chmodSync)(configPath, 384);
486
742
  }
487
743
  function setupGitAndGh() {
488
744
  const name = process.env.GIT_USER_NAME ?? "Radar Bot";
489
745
  const email = process.env.GIT_USER_EMAIL ?? "radar@launchpod.local";
490
746
  const status = run("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
491
- if (status !== 0) fail(`[entrypoint] launch-kit setup-git failed (status ${status})`);
747
+ if (status !== 0) fail2(`[entrypoint] launch-kit setup-git failed (status ${status})`);
492
748
  }
493
749
  function detectAndSetPreviewPort() {
494
750
  if (process.env.PREVIEW_PORT) return;
495
751
  try {
496
752
  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"));
753
+ if (!(0, import_node_fs4.existsSync)(pkgPath)) return;
754
+ const pkg = JSON.parse((0, import_node_fs4.readFileSync)(pkgPath, "utf-8"));
499
755
  const scripts = pkg.scripts ?? {};
500
756
  const portRe = /(?:--port[= ]|-p\s+|\bPORT=)(\d{2,5})\b/;
501
757
  for (const name of ["dev", "start", "serve"]) {
@@ -513,7 +769,7 @@ function detectAndSetPreviewPort() {
513
769
  }
514
770
  function initWorkspaceIfEmpty() {
515
771
  process.chdir("/workspace");
516
- if ((0, import_node_fs3.existsSync)(".git")) {
772
+ if ((0, import_node_fs4.existsSync)(".git")) {
517
773
  console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
518
774
  return;
519
775
  }
@@ -526,7 +782,7 @@ function initWorkspaceIfEmpty() {
526
782
  `--url=${process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app"}`,
527
783
  `--dir=/workspace`
528
784
  ]);
529
- if (status !== 0) fail(`[entrypoint] launch-kit init failed (status ${status})`);
785
+ if (status !== 0) fail2(`[entrypoint] launch-kit init failed (status ${status})`);
530
786
  }
531
787
  async function maybeProvisionIngress(bundle, services, projectSlug) {
532
788
  const token = bundle.cloudflareToken ?? null;
@@ -534,28 +790,36 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
534
790
  const zones = bundle.cloudflareZones ?? [];
535
791
  if (!token && !accountId && zones.length === 0) return null;
536
792
  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.`);
793
+ fail2(`[entrypoint] cloudflare integration is partial \u2014 token=${token ? "set" : "missing"} accountId=${accountId ? "set" : "missing"}. Re-connect the Cloudflare provider in LS.`);
538
794
  }
539
795
  const baseDomain = process.env.LAUNCHKIT_CF_BASE_DOMAIN?.trim();
540
796
  let chosen = null;
541
797
  if (baseDomain) {
542
798
  chosen = zones.find((z) => z.name === baseDomain) ?? null;
543
799
  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.`);
800
+ 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
801
  }
546
802
  } else if (zones.length === 1) {
547
803
  chosen = { id: zones[0].id, name: zones[0].name };
548
804
  } 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.`);
805
+ fail2(`[entrypoint] cloudflare token covers ${zones.length} zones (${zones.map((z) => z.name).join(", ")}) \u2014 set LAUNCHKIT_CF_BASE_DOMAIN to pick one.`);
550
806
  }
551
807
  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(",")}`);
808
+ const slugLabel = projectSlug.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
809
+ const DNS_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
810
+ for (const s of services) {
811
+ const label = `${slugLabel}-${s.name}`;
812
+ if (!DNS_LABEL_RE.test(label)) {
813
+ 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}").`);
814
+ }
815
+ }
816
+ console.log(`[entrypoint] provisioning CF named tunnel \u2014 name=launch-kit-${projectSlug} zone=${chosen.name} services=${services.map((s) => `${slugLabel}-${s.name}`).join(",")}`);
553
817
  const result = await provisionIngress({
554
818
  apiToken: token,
555
819
  accountId,
556
820
  zone: chosen,
557
821
  tunnelName: `launch-kit-${projectSlug}`,
558
- services: services.map((s) => ({ name: s.name, port: s.port })),
822
+ services: services.map((s) => ({ name: s.name, label: `${slugLabel}-${s.name}`, port: s.port })),
559
823
  stateFile
560
824
  });
561
825
  for (const [name, fqdn] of Object.entries(result.hostnames)) {
@@ -563,6 +827,61 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
563
827
  }
564
828
  return result;
565
829
  }
830
+ var GATED_SERVICES = {
831
+ // Claude web terminal — live drivable shell ⇒ RCE surface ⇒ short session.
832
+ bot: { strict: true },
833
+ // The user's own dev/preview server — a workday-length session is fine.
834
+ preview: { strict: false }
835
+ };
836
+ async function registerOidcClient(serverUrl, pat, redirectUris) {
837
+ const res = await fetch(new URL("/api/rover/oidc-client", serverUrl), {
838
+ method: "POST",
839
+ headers: {
840
+ Authorization: `Bearer ${pat}`,
841
+ "Content-Type": "application/json",
842
+ Accept: "application/json"
843
+ },
844
+ body: JSON.stringify({ redirectUris }),
845
+ signal: AbortSignal.timeout(15e3)
846
+ });
847
+ const body = await res.json().catch(() => null);
848
+ if (!res.ok || !body?.success || !body.data) {
849
+ fail2(`[entrypoint] OIDC client provisioning failed (HTTP ${res.status}): ${body?.error ?? "unexpected response"}`);
850
+ }
851
+ return body.data;
852
+ }
853
+ async function maybeProvisionAccess(bundle, ingress) {
854
+ const token = bundle.cloudflareToken ?? null;
855
+ const accountId = bundle.cloudflareAccountId ?? null;
856
+ if (!token || !accountId) return;
857
+ const services = [];
858
+ const skipped = [];
859
+ for (const [name, hostname] of Object.entries(ingress.hostnames)) {
860
+ const cfg = GATED_SERVICES[name];
861
+ if (cfg) services.push({ hostname, strict: cfg.strict });
862
+ else skipped.push(name);
863
+ }
864
+ if (skipped.length > 0) {
865
+ console.log(`[entrypoint] CF Access: leaving machine surface(s) ungated: ${skipped.join(", ")}`);
866
+ }
867
+ if (services.length === 0) {
868
+ console.log("[entrypoint] CF Access: no human-facing service to gate (bot/preview not provisioned)");
869
+ return;
870
+ }
871
+ const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
872
+ const pat = requireEnv("LS_PAT");
873
+ const stateFile = "/workspace/.launchpod/launch-kit-access.json";
874
+ console.log(`[entrypoint] gating ${services.map((s) => s.hostname).join(", ")} behind CF Access (IdP: ${serverUrl})`);
875
+ const result = await provisionAccess({
876
+ apiToken: token,
877
+ accountId,
878
+ issuer: serverUrl,
879
+ services,
880
+ stateFile,
881
+ registerClient: (redirectUris) => registerOidcClient(serverUrl, pat, redirectUris)
882
+ });
883
+ console.log(`[entrypoint] CF Access gate live \u2014 IdP ${result.idpId}, auth domain ${result.authDomain}`);
884
+ }
566
885
  function spawnServiceGroup(services) {
567
886
  const children = [];
568
887
  let shuttingDown = false;
@@ -645,6 +964,12 @@ function spawnServiceGroup(services) {
645
964
  }).finally(removeSignals);
646
965
  }
647
966
  async function main() {
967
+ const priorCrashes = readCrashState()?.count ?? 0;
968
+ if (priorCrashes >= MAX_BOOT_CRASHES) await parkAfterCrashLoop(priorCrashes);
969
+ const bootAttempt = bumpCrashCount();
970
+ if (bootAttempt > 1) {
971
+ console.warn(`[entrypoint] boot attempt ${bootAttempt}/${MAX_BOOT_CRASHES} \u2014 prior boot(s) crashed before becoming stable`);
972
+ }
648
973
  for (const k of REQUIRED_ENV) requireEnv(k);
649
974
  const bundle = await setupFromCloud();
650
975
  setupClaudeCredentials();
@@ -655,7 +980,7 @@ async function main() {
655
980
  try {
656
981
  services = resolveServices();
657
982
  } catch (err) {
658
- fail(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
983
+ fail2(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
659
984
  }
660
985
  console.log(`[entrypoint] services: ${services.map((s) => `${s.name}@${s.port}`).join(", ")}`);
661
986
  const ingress = await maybeProvisionIngress(bundle, services, requireEnv("LS_PROJECT_SLUG"));
@@ -664,8 +989,9 @@ async function main() {
664
989
  const radarFqdn = ingress.hostnames.radar;
665
990
  if (radarFqdn) process.env.RADAR_CF_TUNNEL_HOSTNAME = radarFqdn;
666
991
  else if (services.some((s) => s.name === "radar")) {
667
- fail(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
992
+ fail2(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
668
993
  }
994
+ await maybeProvisionAccess(bundle, ingress);
669
995
  } else if (services.length > 1) {
670
996
  const first = services[0];
671
997
  console.warn(
@@ -675,10 +1001,17 @@ async function main() {
675
1001
  console.warn(`[entrypoint] \u26A0 first service is "${first.name}", not "radar" \u2014 quick tunneling is owned by the radar agent today, so NO external URL will be available.`);
676
1002
  }
677
1003
  }
1004
+ const stableTimer = setTimeout(() => {
1005
+ clearCrashCount();
1006
+ console.log(`[entrypoint] services stable for ${Math.round(STABLE_AFTER_MS / 1e3)}s \u2014 boot-crash counter cleared`);
1007
+ }, STABLE_AFTER_MS);
1008
+ stableTimer.unref?.();
678
1009
  try {
679
1010
  await spawnServiceGroup(services);
1011
+ clearTimeout(stableTimer);
680
1012
  process.exit(0);
681
1013
  } catch (err) {
1014
+ clearTimeout(stableTimer);
682
1015
  console.error(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
683
1016
  process.exit(1);
684
1017
  }
@@ -691,6 +1024,7 @@ if (!process.env.VITEST) {
691
1024
  }
692
1025
  // Annotate the CommonJS export names for ESM import in node:
693
1026
  0 && (module.exports = {
1027
+ maybeProvisionAccess,
694
1028
  maybeProvisionIngress,
695
1029
  spawnServiceGroup
696
1030
  });