@launchsecure/launch-kit 0.0.36 → 0.0.38

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 (85) hide show
  1. package/dist/chart-client/assets/index-B6rR0CWx.css +1 -0
  2. package/dist/chart-client/index.html +2 -2
  3. package/dist/client/assets/index-D6uX1lQe.css +32 -0
  4. package/dist/client/index.html +2 -2
  5. package/dist/council-client/assets/index-CleYLarJ.css +1 -0
  6. package/dist/council-client/index.html +2 -2
  7. package/dist/deck-client/assets/{_baseUniq-BiVx0WO_.js → _baseUniq-CgW32Gdk.js} +1 -1
  8. package/dist/deck-client/assets/{arc-DGMkiEzS.js → arc-D-Mg9gvM.js} +1 -1
  9. package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-Y2WRmHtk.js → architectureDiagram-Q4EWVU46-CdTsXsgl.js} +1 -1
  10. package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-_Lbfu5BQ.js → blockDiagram-DXYQGD6D-mwTneYyB.js} +1 -1
  11. package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-CTqpYTBX.js → c4Diagram-AHTNJAMY-C4R8IbjO.js} +1 -1
  12. package/dist/deck-client/assets/channel-CuSee7GO.js +1 -0
  13. package/dist/deck-client/assets/{chunk-4BX2VUAB-liEIbPHs.js → chunk-4BX2VUAB-ZWuRIUwb.js} +1 -1
  14. package/dist/deck-client/assets/{chunk-4TB4RGXK-CCc6lYvL.js → chunk-4TB4RGXK-PNHX10sF.js} +1 -1
  15. package/dist/deck-client/assets/{chunk-55IACEB6-D02jJUR2.js → chunk-55IACEB6-CD9MUgPr.js} +1 -1
  16. package/dist/deck-client/assets/{chunk-EDXVE4YY-BFmGMbLD.js → chunk-EDXVE4YY-C_CpORb3.js} +1 -1
  17. package/dist/deck-client/assets/{chunk-FMBD7UC4-6wFLOVcJ.js → chunk-FMBD7UC4-Bg5RoVC-.js} +1 -1
  18. package/dist/deck-client/assets/{chunk-OYMX7WX6-Bnr8RiBf.js → chunk-OYMX7WX6-DhTwgQwd.js} +1 -1
  19. package/dist/deck-client/assets/{chunk-QZHKN3VN-Ct82MksJ.js → chunk-QZHKN3VN-C5VLMaFa.js} +1 -1
  20. package/dist/deck-client/assets/{chunk-YZCP3GAM-BXmN1diQ.js → chunk-YZCP3GAM-NAGHy4Sr.js} +1 -1
  21. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-_kTisqzs.js +1 -0
  22. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-_kTisqzs.js +1 -0
  23. package/dist/deck-client/assets/clone-kb3zkY60.js +1 -0
  24. package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-CmQCT-mH.js → cose-bilkent-S5V4N54A-CpUczjZk.js} +1 -1
  25. package/dist/deck-client/assets/{dagre-KV5264BT-DDdSa9EX.js → dagre-KV5264BT-BOvb07MG.js} +1 -1
  26. package/dist/deck-client/assets/{diagram-5BDNPKRD-Bccks2xJ.js → diagram-5BDNPKRD-BPxwTiC-.js} +1 -1
  27. package/dist/deck-client/assets/{diagram-G4DWMVQ6-CPPNgxmQ.js → diagram-G4DWMVQ6-Dz_gsHgx.js} +1 -1
  28. package/dist/deck-client/assets/{diagram-MMDJMWI5-KrD300pS.js → diagram-MMDJMWI5-B7z-oVTW.js} +1 -1
  29. package/dist/deck-client/assets/{diagram-TYMM5635-DefnLuQf.js → diagram-TYMM5635-CAIAglLQ.js} +1 -1
  30. package/dist/deck-client/assets/{erDiagram-SMLLAGMA-DI9FfnFP.js → erDiagram-SMLLAGMA-BiViTWF3.js} +1 -1
  31. package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-twKyd3Fx.js → flowDiagram-DWJPFMVM-DYVemp0H.js} +1 -1
  32. package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-Wau3jhBr.js → ganttDiagram-T4ZO3ILL-Chc1Iyu1.js} +1 -1
  33. package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-D9GgYXwb.js → gitGraphDiagram-UUTBAWPF-B7eFgaj5.js} +1 -1
  34. package/dist/deck-client/assets/{graph-BhNLzyXS.js → graph-CKaIoNwb.js} +1 -1
  35. package/dist/deck-client/assets/index-55P73aS_.css +1 -0
  36. package/dist/deck-client/assets/{index-BtQBaQ7s.js → index-BRawc7RA.js} +49 -48
  37. package/dist/deck-client/assets/{infoDiagram-42DDH7IO-TylGlSG-.js → infoDiagram-42DDH7IO-BxsVq7vO.js} +1 -1
  38. package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DAT8icpg.js → ishikawaDiagram-UXIWVN3A-DAM7vPwa.js} +1 -1
  39. package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-D3v_XL72.js → journeyDiagram-VCZTEJTY-Xe20Nf7R.js} +1 -1
  40. package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-DNUOBiNr.js → kanban-definition-6JOO6SKY-DS8YguzB.js} +1 -1
  41. package/dist/deck-client/assets/{layout-COfodgwF.js → layout-DKMBpzR-.js} +1 -1
  42. package/dist/deck-client/assets/{linear-DmTsuIvK.js → linear-DTNtBg5h.js} +1 -1
  43. package/dist/deck-client/assets/{min-BW1F7i1D.js → min-C4DrxCcA.js} +1 -1
  44. package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-CErFzKWl.js → mindmap-definition-QFDTVHPH-B4nEtsw5.js} +1 -1
  45. package/dist/deck-client/assets/{pieDiagram-DEJITSTG-DW5F757o.js → pieDiagram-DEJITSTG-BzHdGNu5.js} +1 -1
  46. package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-B1S2-TfI.js → quadrantDiagram-34T5L4WZ-CaX0SD4-.js} +1 -1
  47. package/dist/deck-client/assets/{requirementDiagram-MS252O5E-BY5BAR-5.js → requirementDiagram-MS252O5E-QeG4p2ni.js} +1 -1
  48. package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-CE1Cp9HS.js → sankeyDiagram-XADWPNL6-BoAwgAj-.js} +1 -1
  49. package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-IaHnbKye.js → sequenceDiagram-FGHM5R23-Dn4pYYgu.js} +1 -1
  50. package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-CwPJm9hU.js → stateDiagram-FHFEXIEX-Is6KRmQV.js} +1 -1
  51. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-Cy45Ttqq.js +1 -0
  52. package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-DVFGGSgN.js → timeline-definition-GMOUNBTQ-v64IZGuY.js} +1 -1
  53. package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-C1194MJi.js → vennDiagram-DHZGUBPP-noh9eouF.js} +1 -1
  54. package/dist/deck-client/assets/wardley-RL74JXVD-cJ_1is2S.js +162 -0
  55. package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-hpwdFfGj.js → wardleyDiagram-NUSXRM2D-DxR4j737.js} +1 -1
  56. package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-DYkotwy8.js → xychartDiagram-5P7HB3ND-B26vodaL.js} +1 -1
  57. package/dist/deck-client/index.html +2 -2
  58. package/dist/server/cli.js +25 -2
  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/fb-wizard.js +0 -0
  64. package/dist/server/init-entry.js +147 -18
  65. package/dist/server/radar-docker-init-entry.js +139 -17
  66. package/dist/server/radar-entrypoint-entry.js +0 -0
  67. package/dist/server/radar-teardown-entry.js +0 -0
  68. package/dist/server/rover-entry.js +25 -4
  69. package/package.json +22 -23
  70. package/scaffolds/ls-marketplace/plugins/kit/skills/deploy-check/SKILL.md +5 -0
  71. package/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +0 -0
  72. package/scaffolds/recall-hook/scripts/ensure-recall.sh +0 -0
  73. package/dist/chart-client/assets/index-DpKO9p0s.css +0 -1
  74. package/dist/client/assets/index-Dv6dD2zY.css +0 -32
  75. package/dist/council-client/assets/index-AqQ9Sei6.css +0 -1
  76. package/dist/deck-client/assets/channel-DB6LxW_l.js +0 -1
  77. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-g944ZyG8.js +0 -1
  78. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-g944ZyG8.js +0 -1
  79. package/dist/deck-client/assets/clone-DiIRH1pI.js +0 -1
  80. package/dist/deck-client/assets/index-B-YQq5b5.css +0 -1
  81. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-DQYa2M1q.js +0 -1
  82. package/dist/deck-client/assets/wardley-RL74JXVD-CHZiUbBa.js +0 -162
  83. /package/dist/chart-client/assets/{index-DFu2xIrM.js → index-C_xCi3gW.js} +0 -0
  84. /package/dist/client/assets/{index-Cbw6bVdx.js → index-CRecYFUA.js} +0 -0
  85. /package/dist/council-client/assets/{index-CAsmGTzg.js → index-DO-Vn15O.js} +0 -0
@@ -617,6 +617,17 @@ async function cf(opts) {
617
617
  function isNotFound(env) {
618
618
  return !env.success && (env.errors ?? []).some((e) => e.code === 7003 || e.code === 1001 || e.code === 81044);
619
619
  }
620
+ async function findTunnelByName(input) {
621
+ const q = new URLSearchParams({ name: input.tunnelName, is_deleted: "false" }).toString();
622
+ const res = await cf({
623
+ apiToken: input.apiToken,
624
+ method: "GET",
625
+ path: `/accounts/${input.accountId}/cfd_tunnel?${q}`
626
+ });
627
+ if (!res.success || !Array.isArray(res.result)) return null;
628
+ const live = res.result.find((t) => t.name === input.tunnelName && !t.deleted_at);
629
+ return live?.id ?? null;
630
+ }
620
631
  function loadState(path6) {
621
632
  if (!(0, import_node_fs2.existsSync)(path6)) return null;
622
633
  try {
@@ -648,16 +659,26 @@ async function ensureTunnel(input, knownTunnelId) {
648
659
  throw new Error(`[cf] tunnel GET failed: ${JSON.stringify(got.errors)}`);
649
660
  }
650
661
  }
662
+ const existing = await findTunnelByName(input);
663
+ if (existing) {
664
+ console.log(`[cf] adopted existing tunnel "${input.tunnelName}" (${existing}) \u2014 local state was missing`);
665
+ return existing;
666
+ }
651
667
  const created = await cf({
652
668
  apiToken: input.apiToken,
653
669
  method: "POST",
654
670
  path: `/accounts/${input.accountId}/cfd_tunnel`,
655
671
  body: { name: input.tunnelName, config_src: "cloudflare" }
656
672
  });
657
- if (!created.success || !created.result) {
658
- throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
673
+ if (created.success && created.result) return created.result.id;
674
+ if ((created.errors ?? []).some((e) => e.code === 1013)) {
675
+ const adopted = await findTunnelByName(input);
676
+ if (adopted) {
677
+ console.log(`[cf] tunnel "${input.tunnelName}" already existed (1013) \u2014 adopted ${adopted}`);
678
+ return adopted;
679
+ }
659
680
  }
660
- return created.result.id;
681
+ throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
661
682
  }
662
683
  async function fetchConnectorToken(input, tunnelId) {
663
684
  const res = await cf({
@@ -836,7 +857,13 @@ async function ensureAccessIdp(input) {
836
857
  return created.result.id;
837
858
  }
838
859
  async function ensureAccessApp(input) {
839
- const policy = {
860
+ const { service } = input;
861
+ const appDomain = service.path ? `${service.hostname}${service.path}` : service.hostname;
862
+ const policy = service.bypass ? {
863
+ name: "launch-kit-public-bypass",
864
+ decision: "bypass",
865
+ include: [{ everyone: {} }]
866
+ } : {
840
867
  name: "launch-kit-org-allow",
841
868
  decision: "allow",
842
869
  include: [
@@ -849,12 +876,17 @@ async function ensureAccessApp(input) {
849
876
  }
850
877
  ]
851
878
  };
852
- const body = {
853
- name: `launch-kit ${input.service.hostname}`,
854
- domain: input.service.hostname,
879
+ const body = service.bypass ? {
880
+ name: `launch-kit ${appDomain} (public)`,
881
+ domain: appDomain,
882
+ type: "self_hosted",
883
+ policies: [policy]
884
+ } : {
885
+ name: `launch-kit ${appDomain}`,
886
+ domain: appDomain,
855
887
  type: "self_hosted",
856
888
  // Bot terminal = RCE surface → short session. Read portals = a workday.
857
- session_duration: input.service.strict ? "30m" : "24h",
889
+ session_duration: service.strict ? "30m" : "24h",
858
890
  allowed_idps: [input.idpId],
859
891
  auto_redirect_to_identity: true,
860
892
  policies: [policy]
@@ -865,7 +897,7 @@ async function ensureAccessApp(input) {
865
897
  path: `/accounts/${input.accountId}/access/apps`
866
898
  });
867
899
  if (!list.success) fail(list, "list access apps");
868
- const existing = (list.result ?? []).find((a) => a.domain === input.service.hostname);
900
+ const existing = (list.result ?? []).find((a) => a.domain === appDomain);
869
901
  if (existing) {
870
902
  const upd = await cf2({
871
903
  apiToken: input.apiToken,
@@ -873,7 +905,7 @@ async function ensureAccessApp(input) {
873
905
  path: `/accounts/${input.accountId}/access/apps/${existing.id}`,
874
906
  body
875
907
  });
876
- if (!upd.success || !upd.result) fail(upd, `update access app ${input.service.hostname}`);
908
+ if (!upd.success || !upd.result) fail(upd, `update access app ${appDomain}`);
877
909
  return upd.result.id;
878
910
  }
879
911
  const created = await cf2({
@@ -882,7 +914,7 @@ async function ensureAccessApp(input) {
882
914
  path: `/accounts/${input.accountId}/access/apps`,
883
915
  body
884
916
  });
885
- if (!created.success || !created.result) fail(created, `create access app ${input.service.hostname}`);
917
+ if (!created.success || !created.result) fail(created, `create access app ${appDomain}`);
886
918
  return created.result.id;
887
919
  }
888
920
  async function provisionAccess(input) {
@@ -901,7 +933,8 @@ async function provisionAccess(input) {
901
933
  saveState2(input.stateFile, { idpId, accountId: input.accountId });
902
934
  const appIds = {};
903
935
  for (const service of input.services) {
904
- appIds[service.hostname] = await ensureAccessApp({
936
+ const appDomain = service.path ? `${service.hostname}${service.path}` : service.hostname;
937
+ appIds[appDomain] = await ensureAccessApp({
905
938
  apiToken: input.apiToken,
906
939
  accountId: input.accountId,
907
940
  idpId,
@@ -922,6 +955,15 @@ var init_cf_access = __esm({
922
955
  }
923
956
  });
924
957
 
958
+ // src/server/radar/registration.ts
959
+ var RECEIVER_PATH;
960
+ var init_registration = __esm({
961
+ "src/server/radar/registration.ts"() {
962
+ "use strict";
963
+ RECEIVER_PATH = "/api/radar/ingest";
964
+ }
965
+ });
966
+
925
967
  // src/server/radar-docker-init-entry.ts
926
968
  var radar_docker_init_entry_exports = {};
927
969
  __export(radar_docker_init_entry_exports, {
@@ -942,6 +984,60 @@ function run2(cmd, args, stdio = "inherit") {
942
984
  const r = (0, import_node_child_process2.spawnSync)(cmd, args, { stdio });
943
985
  return r.status ?? 1;
944
986
  }
987
+ function readCrashState() {
988
+ try {
989
+ const s = JSON.parse((0, import_node_fs4.readFileSync)(CRASH_STATE_FILE, "utf8"));
990
+ return typeof s?.count === "number" && s.count >= 0 ? s : null;
991
+ } catch {
992
+ return null;
993
+ }
994
+ }
995
+ function bumpCrashCount() {
996
+ const prev = readCrashState();
997
+ const now = (/* @__PURE__ */ new Date()).toISOString();
998
+ const next = {
999
+ count: (prev?.count ?? 0) + 1,
1000
+ firstAt: prev?.firstAt ?? now,
1001
+ lastAt: now
1002
+ };
1003
+ try {
1004
+ (0, import_node_fs4.mkdirSync)(LAUNCHPOD_DIR, { recursive: true });
1005
+ (0, import_node_fs4.writeFileSync)(CRASH_STATE_FILE, JSON.stringify(next, null, 2));
1006
+ } catch (err) {
1007
+ console.warn(`[entrypoint] could not persist boot-crash counter (continuing unprotected): ${err instanceof Error ? err.message : String(err)}`);
1008
+ }
1009
+ return next.count;
1010
+ }
1011
+ function clearCrashCount() {
1012
+ try {
1013
+ if ((0, import_node_fs4.existsSync)(CRASH_STATE_FILE)) (0, import_node_fs4.writeFileSync)(CRASH_STATE_FILE, JSON.stringify({ count: 0, firstAt: "", lastAt: "" }));
1014
+ } catch {
1015
+ }
1016
+ }
1017
+ async function parkAfterCrashLoop(count) {
1018
+ const lines = [
1019
+ "==================================================================",
1020
+ `[entrypoint] CRASH-LOOP HALT \u2014 ${count} consecutive failed boots (cap ${MAX_BOOT_CRASHES}).`,
1021
+ "[entrypoint] Refusing to restart again. Container is now PARKED (idle, not",
1022
+ "[entrypoint] exiting) so it stops thrashing CF APIs and logs. Fix the root",
1023
+ "[entrypoint] cause, clear the counter, then restart the container:",
1024
+ `[entrypoint] rm ${CRASH_STATE_FILE} && docker restart <container>`,
1025
+ "=================================================================="
1026
+ ];
1027
+ for (const l of lines) console.error(l);
1028
+ for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
1029
+ process.on(sig, () => {
1030
+ console.log(`[entrypoint] received ${sig} while parked \u2014 exiting`);
1031
+ process.exit(0);
1032
+ });
1033
+ }
1034
+ setInterval(() => {
1035
+ console.error(`[entrypoint] still parked after crash-loop halt \u2014 clear ${CRASH_STATE_FILE} and restart to retry`);
1036
+ }, 15 * 6e4);
1037
+ await new Promise(() => {
1038
+ });
1039
+ throw new Error("unreachable");
1040
+ }
945
1041
  async function setupFromCloud() {
946
1042
  const pat = requireEnv("LS_PAT");
947
1043
  const orgSlug = requireEnv("LS_ORG_SLUG");
@@ -1120,20 +1216,31 @@ async function maybeProvisionAccess(bundle, ingress) {
1120
1216
  const skipped = [];
1121
1217
  for (const [name, hostname] of Object.entries(ingress.hostnames)) {
1122
1218
  const cfg = GATED_SERVICES[name];
1123
- if (cfg) services.push({ hostname, strict: cfg.strict });
1124
- else skipped.push(name);
1219
+ if (!cfg) {
1220
+ skipped.push(name);
1221
+ continue;
1222
+ }
1223
+ services.push({ hostname, strict: cfg.strict });
1224
+ for (const path6 of cfg.publicPaths ?? []) {
1225
+ services.push({ hostname, path: path6, bypass: true });
1226
+ }
1125
1227
  }
1126
1228
  if (skipped.length > 0) {
1127
1229
  console.log(`[entrypoint] CF Access: leaving machine surface(s) ungated: ${skipped.join(", ")}`);
1128
1230
  }
1129
1231
  if (services.length === 0) {
1130
- console.log("[entrypoint] CF Access: no human-facing service to gate (bot/preview not provisioned)");
1232
+ console.log("[entrypoint] CF Access: no human-facing service to gate (bot/preview/radar/deck not provisioned)");
1131
1233
  return;
1132
1234
  }
1133
1235
  const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
1134
1236
  const pat = requireEnv("LS_PAT");
1135
1237
  const stateFile = "/workspace/.launchpod/launch-kit-access.json";
1136
- console.log(`[entrypoint] gating ${services.map((s) => s.hostname).join(", ")} behind CF Access (IdP: ${serverUrl})`);
1238
+ const gatedHosts = services.filter((s) => !s.bypass).map((s) => s.hostname);
1239
+ const bypassed = services.filter((s) => s.bypass).map((s) => `${s.hostname}${s.path ?? ""}`);
1240
+ console.log(`[entrypoint] gating ${gatedHosts.join(", ")} behind CF Access (IdP: ${serverUrl})`);
1241
+ if (bypassed.length > 0) {
1242
+ console.log(`[entrypoint] CF Access: public bypass for ${bypassed.join(", ")}`);
1243
+ }
1137
1244
  const result = await provisionAccess({
1138
1245
  apiToken: token,
1139
1246
  accountId,
@@ -1226,6 +1333,12 @@ function spawnServiceGroup(services) {
1226
1333
  }).finally(removeSignals);
1227
1334
  }
1228
1335
  async function main() {
1336
+ const priorCrashes = readCrashState()?.count ?? 0;
1337
+ if (priorCrashes >= MAX_BOOT_CRASHES) await parkAfterCrashLoop(priorCrashes);
1338
+ const bootAttempt = bumpCrashCount();
1339
+ if (bootAttempt > 1) {
1340
+ console.warn(`[entrypoint] boot attempt ${bootAttempt}/${MAX_BOOT_CRASHES} \u2014 prior boot(s) crashed before becoming stable`);
1341
+ }
1229
1342
  for (const k of REQUIRED_ENV) requireEnv(k);
1230
1343
  const bundle = await setupFromCloud();
1231
1344
  setupClaudeCredentials();
@@ -1257,15 +1370,22 @@ async function main() {
1257
1370
  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.`);
1258
1371
  }
1259
1372
  }
1373
+ const stableTimer = setTimeout(() => {
1374
+ clearCrashCount();
1375
+ console.log(`[entrypoint] services stable for ${Math.round(STABLE_AFTER_MS / 1e3)}s \u2014 boot-crash counter cleared`);
1376
+ }, STABLE_AFTER_MS);
1377
+ stableTimer.unref?.();
1260
1378
  try {
1261
1379
  await spawnServiceGroup(services);
1380
+ clearTimeout(stableTimer);
1262
1381
  process.exit(0);
1263
1382
  } catch (err) {
1383
+ clearTimeout(stableTimer);
1264
1384
  console.error(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
1265
1385
  process.exit(1);
1266
1386
  }
1267
1387
  }
1268
- var import_node_child_process2, import_node_fs4, import_node_path4, REQUIRED_ENV, GATED_SERVICES;
1388
+ var import_node_child_process2, import_node_fs4, import_node_path4, REQUIRED_ENV, LAUNCHPOD_DIR, CRASH_STATE_FILE, MAX_BOOT_CRASHES, STABLE_AFTER_MS, GATED_SERVICES;
1269
1389
  var init_radar_docker_init_entry = __esm({
1270
1390
  "src/server/radar-docker-init-entry.ts"() {
1271
1391
  "use strict";
@@ -1276,17 +1396,26 @@ var init_radar_docker_init_entry = __esm({
1276
1396
  init_launch_kit_services();
1277
1397
  init_cf_ingress();
1278
1398
  init_cf_access();
1399
+ init_registration();
1279
1400
  REQUIRED_ENV = [
1280
1401
  "CLAUDE_CREDENTIALS_B64",
1281
1402
  "LS_PAT",
1282
1403
  "LS_ORG_SLUG",
1283
1404
  "LS_PROJECT_SLUG"
1284
1405
  ];
1406
+ LAUNCHPOD_DIR = "/workspace/.launchpod";
1407
+ CRASH_STATE_FILE = (0, import_node_path4.join)(LAUNCHPOD_DIR, ".boot-crash.json");
1408
+ MAX_BOOT_CRASHES = 5;
1409
+ STABLE_AFTER_MS = 3e4;
1285
1410
  GATED_SERVICES = {
1286
1411
  // Claude web terminal — live drivable shell ⇒ RCE surface ⇒ short session.
1287
1412
  bot: { strict: true },
1288
1413
  // The user's own dev/preview server — a workday-length session is fine.
1289
- preview: { strict: false }
1414
+ preview: { strict: false },
1415
+ // Radar: gate the UI, bypass the (HMAC-verified) webhook receiver path.
1416
+ radar: { strict: false, publicPaths: [RECEIVER_PATH] },
1417
+ // Deck: gate the whole host — its push is localhost-only, never hits the edge.
1418
+ deck: { strict: false }
1290
1419
  };
1291
1420
  if (!process.env.VITEST) {
1292
1421
  main().catch((err) => {
@@ -286,6 +286,17 @@ async function cf(opts) {
286
286
  function isNotFound(env) {
287
287
  return !env.success && (env.errors ?? []).some((e) => e.code === 7003 || e.code === 1001 || e.code === 81044);
288
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
+ }
289
300
  function loadState(path) {
290
301
  if (!(0, import_node_fs2.existsSync)(path)) return null;
291
302
  try {
@@ -317,16 +328,26 @@ async function ensureTunnel(input, knownTunnelId) {
317
328
  throw new Error(`[cf] tunnel GET failed: ${JSON.stringify(got.errors)}`);
318
329
  }
319
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
+ }
320
336
  const created = await cf({
321
337
  apiToken: input.apiToken,
322
338
  method: "POST",
323
339
  path: `/accounts/${input.accountId}/cfd_tunnel`,
324
340
  body: { name: input.tunnelName, config_src: "cloudflare" }
325
341
  });
326
- if (!created.success || !created.result) {
327
- 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
+ }
328
349
  }
329
- return created.result.id;
350
+ throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
330
351
  }
331
352
  async function fetchConnectorToken(input, tunnelId) {
332
353
  const res = await cf({
@@ -499,7 +520,13 @@ async function ensureAccessIdp(input) {
499
520
  return created.result.id;
500
521
  }
501
522
  async function ensureAccessApp(input) {
502
- const policy = {
523
+ const { service } = input;
524
+ const appDomain = service.path ? `${service.hostname}${service.path}` : service.hostname;
525
+ const policy = service.bypass ? {
526
+ name: "launch-kit-public-bypass",
527
+ decision: "bypass",
528
+ include: [{ everyone: {} }]
529
+ } : {
503
530
  name: "launch-kit-org-allow",
504
531
  decision: "allow",
505
532
  include: [
@@ -512,12 +539,17 @@ async function ensureAccessApp(input) {
512
539
  }
513
540
  ]
514
541
  };
515
- const body = {
516
- name: `launch-kit ${input.service.hostname}`,
517
- domain: input.service.hostname,
542
+ const body = service.bypass ? {
543
+ name: `launch-kit ${appDomain} (public)`,
544
+ domain: appDomain,
545
+ type: "self_hosted",
546
+ policies: [policy]
547
+ } : {
548
+ name: `launch-kit ${appDomain}`,
549
+ domain: appDomain,
518
550
  type: "self_hosted",
519
551
  // Bot terminal = RCE surface → short session. Read portals = a workday.
520
- session_duration: input.service.strict ? "30m" : "24h",
552
+ session_duration: service.strict ? "30m" : "24h",
521
553
  allowed_idps: [input.idpId],
522
554
  auto_redirect_to_identity: true,
523
555
  policies: [policy]
@@ -528,7 +560,7 @@ async function ensureAccessApp(input) {
528
560
  path: `/accounts/${input.accountId}/access/apps`
529
561
  });
530
562
  if (!list.success) fail(list, "list access apps");
531
- const existing = (list.result ?? []).find((a) => a.domain === input.service.hostname);
563
+ const existing = (list.result ?? []).find((a) => a.domain === appDomain);
532
564
  if (existing) {
533
565
  const upd = await cf2({
534
566
  apiToken: input.apiToken,
@@ -536,7 +568,7 @@ async function ensureAccessApp(input) {
536
568
  path: `/accounts/${input.accountId}/access/apps/${existing.id}`,
537
569
  body
538
570
  });
539
- if (!upd.success || !upd.result) fail(upd, `update access app ${input.service.hostname}`);
571
+ if (!upd.success || !upd.result) fail(upd, `update access app ${appDomain}`);
540
572
  return upd.result.id;
541
573
  }
542
574
  const created = await cf2({
@@ -545,7 +577,7 @@ async function ensureAccessApp(input) {
545
577
  path: `/accounts/${input.accountId}/access/apps`,
546
578
  body
547
579
  });
548
- if (!created.success || !created.result) fail(created, `create access app ${input.service.hostname}`);
580
+ if (!created.success || !created.result) fail(created, `create access app ${appDomain}`);
549
581
  return created.result.id;
550
582
  }
551
583
  async function provisionAccess(input) {
@@ -564,7 +596,8 @@ async function provisionAccess(input) {
564
596
  saveState2(input.stateFile, { idpId, accountId: input.accountId });
565
597
  const appIds = {};
566
598
  for (const service of input.services) {
567
- appIds[service.hostname] = await ensureAccessApp({
599
+ const appDomain = service.path ? `${service.hostname}${service.path}` : service.hostname;
600
+ appIds[appDomain] = await ensureAccessApp({
568
601
  apiToken: input.apiToken,
569
602
  accountId: input.accountId,
570
603
  idpId,
@@ -575,6 +608,9 @@ async function provisionAccess(input) {
575
608
  return { idpId, authDomain, appIds };
576
609
  }
577
610
 
611
+ // src/server/radar/registration.ts
612
+ var RECEIVER_PATH = "/api/radar/ingest";
613
+
578
614
  // src/server/radar-docker-init-entry.ts
579
615
  var REQUIRED_ENV = [
580
616
  "CLAUDE_CREDENTIALS_B64",
@@ -595,6 +631,64 @@ function run(cmd, args, stdio = "inherit") {
595
631
  const r = (0, import_node_child_process.spawnSync)(cmd, args, { stdio });
596
632
  return r.status ?? 1;
597
633
  }
634
+ var LAUNCHPOD_DIR = "/workspace/.launchpod";
635
+ var CRASH_STATE_FILE = (0, import_node_path4.join)(LAUNCHPOD_DIR, ".boot-crash.json");
636
+ var MAX_BOOT_CRASHES = 5;
637
+ var STABLE_AFTER_MS = 3e4;
638
+ function readCrashState() {
639
+ try {
640
+ const s = JSON.parse((0, import_node_fs4.readFileSync)(CRASH_STATE_FILE, "utf8"));
641
+ return typeof s?.count === "number" && s.count >= 0 ? s : null;
642
+ } catch {
643
+ return null;
644
+ }
645
+ }
646
+ function bumpCrashCount() {
647
+ const prev = readCrashState();
648
+ const now = (/* @__PURE__ */ new Date()).toISOString();
649
+ const next = {
650
+ count: (prev?.count ?? 0) + 1,
651
+ firstAt: prev?.firstAt ?? now,
652
+ lastAt: now
653
+ };
654
+ try {
655
+ (0, import_node_fs4.mkdirSync)(LAUNCHPOD_DIR, { recursive: true });
656
+ (0, import_node_fs4.writeFileSync)(CRASH_STATE_FILE, JSON.stringify(next, null, 2));
657
+ } catch (err) {
658
+ console.warn(`[entrypoint] could not persist boot-crash counter (continuing unprotected): ${err instanceof Error ? err.message : String(err)}`);
659
+ }
660
+ return next.count;
661
+ }
662
+ function clearCrashCount() {
663
+ try {
664
+ if ((0, import_node_fs4.existsSync)(CRASH_STATE_FILE)) (0, import_node_fs4.writeFileSync)(CRASH_STATE_FILE, JSON.stringify({ count: 0, firstAt: "", lastAt: "" }));
665
+ } catch {
666
+ }
667
+ }
668
+ async function parkAfterCrashLoop(count) {
669
+ const lines = [
670
+ "==================================================================",
671
+ `[entrypoint] CRASH-LOOP HALT \u2014 ${count} consecutive failed boots (cap ${MAX_BOOT_CRASHES}).`,
672
+ "[entrypoint] Refusing to restart again. Container is now PARKED (idle, not",
673
+ "[entrypoint] exiting) so it stops thrashing CF APIs and logs. Fix the root",
674
+ "[entrypoint] cause, clear the counter, then restart the container:",
675
+ `[entrypoint] rm ${CRASH_STATE_FILE} && docker restart <container>`,
676
+ "=================================================================="
677
+ ];
678
+ for (const l of lines) console.error(l);
679
+ for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) {
680
+ process.on(sig, () => {
681
+ console.log(`[entrypoint] received ${sig} while parked \u2014 exiting`);
682
+ process.exit(0);
683
+ });
684
+ }
685
+ setInterval(() => {
686
+ console.error(`[entrypoint] still parked after crash-loop halt \u2014 clear ${CRASH_STATE_FILE} and restart to retry`);
687
+ }, 15 * 6e4);
688
+ await new Promise(() => {
689
+ });
690
+ throw new Error("unreachable");
691
+ }
598
692
  async function setupFromCloud() {
599
693
  const pat = requireEnv("LS_PAT");
600
694
  const orgSlug = requireEnv("LS_ORG_SLUG");
@@ -752,7 +846,11 @@ var GATED_SERVICES = {
752
846
  // Claude web terminal — live drivable shell ⇒ RCE surface ⇒ short session.
753
847
  bot: { strict: true },
754
848
  // The user's own dev/preview server — a workday-length session is fine.
755
- preview: { strict: false }
849
+ preview: { strict: false },
850
+ // Radar: gate the UI, bypass the (HMAC-verified) webhook receiver path.
851
+ radar: { strict: false, publicPaths: [RECEIVER_PATH] },
852
+ // Deck: gate the whole host — its push is localhost-only, never hits the edge.
853
+ deck: { strict: false }
756
854
  };
757
855
  async function registerOidcClient(serverUrl, pat, redirectUris) {
758
856
  const res = await fetch(new URL("/api/rover/oidc-client", serverUrl), {
@@ -779,20 +877,31 @@ async function maybeProvisionAccess(bundle, ingress) {
779
877
  const skipped = [];
780
878
  for (const [name, hostname] of Object.entries(ingress.hostnames)) {
781
879
  const cfg = GATED_SERVICES[name];
782
- if (cfg) services.push({ hostname, strict: cfg.strict });
783
- else skipped.push(name);
880
+ if (!cfg) {
881
+ skipped.push(name);
882
+ continue;
883
+ }
884
+ services.push({ hostname, strict: cfg.strict });
885
+ for (const path of cfg.publicPaths ?? []) {
886
+ services.push({ hostname, path, bypass: true });
887
+ }
784
888
  }
785
889
  if (skipped.length > 0) {
786
890
  console.log(`[entrypoint] CF Access: leaving machine surface(s) ungated: ${skipped.join(", ")}`);
787
891
  }
788
892
  if (services.length === 0) {
789
- console.log("[entrypoint] CF Access: no human-facing service to gate (bot/preview not provisioned)");
893
+ console.log("[entrypoint] CF Access: no human-facing service to gate (bot/preview/radar/deck not provisioned)");
790
894
  return;
791
895
  }
792
896
  const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
793
897
  const pat = requireEnv("LS_PAT");
794
898
  const stateFile = "/workspace/.launchpod/launch-kit-access.json";
795
- console.log(`[entrypoint] gating ${services.map((s) => s.hostname).join(", ")} behind CF Access (IdP: ${serverUrl})`);
899
+ const gatedHosts = services.filter((s) => !s.bypass).map((s) => s.hostname);
900
+ const bypassed = services.filter((s) => s.bypass).map((s) => `${s.hostname}${s.path ?? ""}`);
901
+ console.log(`[entrypoint] gating ${gatedHosts.join(", ")} behind CF Access (IdP: ${serverUrl})`);
902
+ if (bypassed.length > 0) {
903
+ console.log(`[entrypoint] CF Access: public bypass for ${bypassed.join(", ")}`);
904
+ }
796
905
  const result = await provisionAccess({
797
906
  apiToken: token,
798
907
  accountId,
@@ -885,6 +994,12 @@ function spawnServiceGroup(services) {
885
994
  }).finally(removeSignals);
886
995
  }
887
996
  async function main() {
997
+ const priorCrashes = readCrashState()?.count ?? 0;
998
+ if (priorCrashes >= MAX_BOOT_CRASHES) await parkAfterCrashLoop(priorCrashes);
999
+ const bootAttempt = bumpCrashCount();
1000
+ if (bootAttempt > 1) {
1001
+ console.warn(`[entrypoint] boot attempt ${bootAttempt}/${MAX_BOOT_CRASHES} \u2014 prior boot(s) crashed before becoming stable`);
1002
+ }
888
1003
  for (const k of REQUIRED_ENV) requireEnv(k);
889
1004
  const bundle = await setupFromCloud();
890
1005
  setupClaudeCredentials();
@@ -916,10 +1031,17 @@ async function main() {
916
1031
  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.`);
917
1032
  }
918
1033
  }
1034
+ const stableTimer = setTimeout(() => {
1035
+ clearCrashCount();
1036
+ console.log(`[entrypoint] services stable for ${Math.round(STABLE_AFTER_MS / 1e3)}s \u2014 boot-crash counter cleared`);
1037
+ }, STABLE_AFTER_MS);
1038
+ stableTimer.unref?.();
919
1039
  try {
920
1040
  await spawnServiceGroup(services);
1041
+ clearTimeout(stableTimer);
921
1042
  process.exit(0);
922
1043
  } catch (err) {
1044
+ clearTimeout(stableTimer);
923
1045
  console.error(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
924
1046
  process.exit(1);
925
1047
  }
File without changes
File without changes
@@ -61,6 +61,17 @@ async function cf(opts) {
61
61
  function isNotFound(env) {
62
62
  return !env.success && (env.errors ?? []).some((e) => e.code === 7003 || e.code === 1001 || e.code === 81044);
63
63
  }
64
+ async function findTunnelByName(input) {
65
+ const q = new URLSearchParams({ name: input.tunnelName, is_deleted: "false" }).toString();
66
+ const res = await cf({
67
+ apiToken: input.apiToken,
68
+ method: "GET",
69
+ path: `/accounts/${input.accountId}/cfd_tunnel?${q}`
70
+ });
71
+ if (!res.success || !Array.isArray(res.result)) return null;
72
+ const live = res.result.find((t) => t.name === input.tunnelName && !t.deleted_at);
73
+ return live?.id ?? null;
74
+ }
64
75
  function loadState(path) {
65
76
  if (!(0, import_node_fs.existsSync)(path)) return null;
66
77
  try {
@@ -92,16 +103,26 @@ async function ensureTunnel(input, knownTunnelId) {
92
103
  throw new Error(`[cf] tunnel GET failed: ${JSON.stringify(got.errors)}`);
93
104
  }
94
105
  }
106
+ const existing = await findTunnelByName(input);
107
+ if (existing) {
108
+ console.log(`[cf] adopted existing tunnel "${input.tunnelName}" (${existing}) \u2014 local state was missing`);
109
+ return existing;
110
+ }
95
111
  const created = await cf({
96
112
  apiToken: input.apiToken,
97
113
  method: "POST",
98
114
  path: `/accounts/${input.accountId}/cfd_tunnel`,
99
115
  body: { name: input.tunnelName, config_src: "cloudflare" }
100
116
  });
101
- if (!created.success || !created.result) {
102
- throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
117
+ if (created.success && created.result) return created.result.id;
118
+ if ((created.errors ?? []).some((e) => e.code === 1013)) {
119
+ const adopted = await findTunnelByName(input);
120
+ if (adopted) {
121
+ console.log(`[cf] tunnel "${input.tunnelName}" already existed (1013) \u2014 adopted ${adopted}`);
122
+ return adopted;
123
+ }
103
124
  }
104
- return created.result.id;
125
+ throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
105
126
  }
106
127
  async function fetchConnectorToken(input, tunnelId) {
107
128
  const res = await cf({
@@ -587,7 +608,7 @@ var require_package = __commonJS({
587
608
  "package.json"(exports2, module2) {
588
609
  module2.exports = {
589
610
  name: "@launchsecure/launch-kit",
590
- version: "0.0.36",
611
+ version: "0.0.38",
591
612
  description: "LaunchSecure toolkit \u2014 launch-sequencer (pipeline runner + terminal bridge), launch-radar (feedback webhook receiver), launch-chart (project graph MCP), launch-deck (visual playground MCP), launch-kit-beacon (feedback Web Component), launch-recall (file-watcher backup). launch-pod is the container image these run inside.",
592
613
  license: "MIT",
593
614
  author: "LaunchSecure - AutomateWithUs",