@launchsecure/launch-kit 0.0.43 → 0.0.45

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 (62) hide show
  1. package/dist/deck-client/assets/{_baseUniq-C6w7kg8x.js → _baseUniq-9540Lrb7.js} +1 -1
  2. package/dist/deck-client/assets/{arc-Cx9pT3Nn.js → arc-2FFU5_0l.js} +1 -1
  3. package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-BITSj3vA.js → architectureDiagram-Q4EWVU46-CV2e3n5Z.js} +1 -1
  4. package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-BehOFuwh.js → blockDiagram-DXYQGD6D-B9JFwjAL.js} +1 -1
  5. package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-BZTYM4na.js → c4Diagram-AHTNJAMY-C0Gwco04.js} +1 -1
  6. package/dist/deck-client/assets/channel-DUo1BfyB.js +1 -0
  7. package/dist/deck-client/assets/{chunk-4BX2VUAB-CCUx5CTd.js → chunk-4BX2VUAB-pnGex62D.js} +1 -1
  8. package/dist/deck-client/assets/{chunk-4TB4RGXK-UDZXXga6.js → chunk-4TB4RGXK-BRfs5enT.js} +1 -1
  9. package/dist/deck-client/assets/{chunk-55IACEB6-CfcU6PIW.js → chunk-55IACEB6-DV2sc7BN.js} +1 -1
  10. package/dist/deck-client/assets/{chunk-EDXVE4YY-BK6F5Fof.js → chunk-EDXVE4YY-BH8QD8Jn.js} +1 -1
  11. package/dist/deck-client/assets/{chunk-FMBD7UC4-C-2idlFB.js → chunk-FMBD7UC4-BVuRSGoP.js} +1 -1
  12. package/dist/deck-client/assets/{chunk-OYMX7WX6-D6hBkYLP.js → chunk-OYMX7WX6-DBBVRR48.js} +1 -1
  13. package/dist/deck-client/assets/{chunk-QZHKN3VN-DixNpysA.js → chunk-QZHKN3VN-DNLfqlpV.js} +1 -1
  14. package/dist/deck-client/assets/{chunk-YZCP3GAM-Cd3pNBtQ.js → chunk-YZCP3GAM-w7-OIPaD.js} +1 -1
  15. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-sHUWMvyj.js +1 -0
  16. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-sHUWMvyj.js +1 -0
  17. package/dist/deck-client/assets/clone-pfbkP49m.js +1 -0
  18. package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-OF3JWdEt.js → cose-bilkent-S5V4N54A-D8kmXu30.js} +1 -1
  19. package/dist/deck-client/assets/{dagre-KV5264BT-Bqu-qcv4.js → dagre-KV5264BT--b1FD3_Y.js} +1 -1
  20. package/dist/deck-client/assets/{diagram-5BDNPKRD--0eHmUBS.js → diagram-5BDNPKRD-DKPVzUtl.js} +1 -1
  21. package/dist/deck-client/assets/{diagram-G4DWMVQ6-nss6oL20.js → diagram-G4DWMVQ6-DYSdoCus.js} +1 -1
  22. package/dist/deck-client/assets/{diagram-MMDJMWI5-D_gSGnLR.js → diagram-MMDJMWI5-DmqAI88z.js} +1 -1
  23. package/dist/deck-client/assets/{diagram-TYMM5635-BIt-P6Pk.js → diagram-TYMM5635-Dbt6BCnF.js} +1 -1
  24. package/dist/deck-client/assets/{erDiagram-SMLLAGMA-Bi-E4KQm.js → erDiagram-SMLLAGMA-C7Kfi12r.js} +1 -1
  25. package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-DMJCvLMA.js → flowDiagram-DWJPFMVM-h1nKPzIv.js} +1 -1
  26. package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-C3xgEoPD.js → ganttDiagram-T4ZO3ILL-40rX1Tln.js} +1 -1
  27. package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-CD0BEGAW.js → gitGraphDiagram-UUTBAWPF-CtvJWtdg.js} +1 -1
  28. package/dist/deck-client/assets/{graph-Dtsd9Jwe.js → graph-ByiwozwM.js} +1 -1
  29. package/dist/deck-client/assets/{index-TFX8vtTG.js → index-CGG2xGJc.js} +1 -1
  30. package/dist/deck-client/assets/{infoDiagram-42DDH7IO-7IcQYqe_.js → infoDiagram-42DDH7IO-CrGXAAvG.js} +1 -1
  31. package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DsCEbx3u.js → ishikawaDiagram-UXIWVN3A-csBJinUG.js} +1 -1
  32. package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-1mP2JwCk.js → journeyDiagram-VCZTEJTY-B2dZRDOR.js} +1 -1
  33. package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-vT0Xrqh9.js → kanban-definition-6JOO6SKY-Dyznt2QN.js} +1 -1
  34. package/dist/deck-client/assets/{layout-Cw4rS2pn.js → layout-tvNTD61-.js} +1 -1
  35. package/dist/deck-client/assets/{linear-CzOjL-Ih.js → linear-BTcLgDqZ.js} +1 -1
  36. package/dist/deck-client/assets/{mermaid.core-DYi3A-qK.js → mermaid.core-CZoq2C4e.js} +4 -4
  37. package/dist/deck-client/assets/{min-DstloRoL.js → min-J7Zsly_t.js} +1 -1
  38. package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-D-cCX2d2.js → mindmap-definition-QFDTVHPH-Ba-GyprJ.js} +1 -1
  39. package/dist/deck-client/assets/{pieDiagram-DEJITSTG-BqW2NTmy.js → pieDiagram-DEJITSTG-BMLzzK64.js} +1 -1
  40. package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-DbJoWA8f.js → quadrantDiagram-34T5L4WZ-Dh_Ut0Yp.js} +1 -1
  41. package/dist/deck-client/assets/{requirementDiagram-MS252O5E-DQrUiz_d.js → requirementDiagram-MS252O5E-Dkva9qg0.js} +1 -1
  42. package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-kB7PZc3g.js → sankeyDiagram-XADWPNL6-3lcCBbua.js} +1 -1
  43. package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-CpyVu1TN.js → sequenceDiagram-FGHM5R23-MH0H8qIE.js} +1 -1
  44. package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-CjqQcnty.js → stateDiagram-FHFEXIEX-ByLZbwGe.js} +1 -1
  45. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-C87YsgaN.js +1 -0
  46. package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-B2PAO9bk.js → timeline-definition-GMOUNBTQ-COT8qN-f.js} +1 -1
  47. package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-C0G3ItCr.js → vennDiagram-DHZGUBPP-YuKnKVX_.js} +1 -1
  48. package/dist/deck-client/assets/wardley-RL74JXVD-C4YfwXTm.js +162 -0
  49. package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-B-qtbNZe.js → wardleyDiagram-NUSXRM2D-BuLtRVnC.js} +1 -1
  50. package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-41kcBoBE.js → xychartDiagram-5P7HB3ND-CcIc1c6H.js} +1 -1
  51. package/dist/deck-client/index.html +1 -1
  52. package/dist/server/init-entry.js +43 -9
  53. package/dist/server/radar-docker-init-entry.js +37 -9
  54. package/dist/server/rover-entry.js +525 -131
  55. package/package.json +1 -1
  56. package/scaffolds/ls-marketplace/plugins/kit/skills/ship/SKILL.md +24 -18
  57. package/dist/deck-client/assets/channel-Cw2WDt9a.js +0 -1
  58. package/dist/deck-client/assets/classDiagram-6PBFFD2Q-JLUXVCUr.js +0 -1
  59. package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-JLUXVCUr.js +0 -1
  60. package/dist/deck-client/assets/clone-H0XCnSb6.js +0 -1
  61. package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-tfMSn8xx.js +0 -1
  62. package/dist/deck-client/assets/wardley-RL74JXVD-B0TVaOmp.js +0 -162
@@ -33,12 +33,201 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
33
33
  mod
34
34
  ));
35
35
 
36
+ // src/server/cf-access.ts
37
+ async function cf(opts) {
38
+ const res = await fetch(`${CF_API_BASE}${opts.path}`, {
39
+ method: opts.method,
40
+ headers: {
41
+ Authorization: `Bearer ${opts.apiToken}`,
42
+ "Content-Type": "application/json",
43
+ Accept: "application/json",
44
+ "User-Agent": "launch-kit/cf-access"
45
+ },
46
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
47
+ signal: AbortSignal.timeout(15e3)
48
+ });
49
+ const text = await res.text();
50
+ try {
51
+ return text ? JSON.parse(text) : { success: false };
52
+ } catch {
53
+ throw new Error(`[cf-access] ${opts.method} ${opts.path} \u2192 ${res.status}, non-JSON: ${text.slice(0, 200)}`);
54
+ }
55
+ }
56
+ function fail(env, what) {
57
+ throw new Error(`[cf-access] ${what} failed: ${JSON.stringify(env.errors)}`);
58
+ }
59
+ function loadState(path) {
60
+ if (!(0, import_node_fs.existsSync)(path)) return null;
61
+ try {
62
+ const parsed = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
63
+ return typeof parsed?.accountId === "string" ? parsed : null;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+ function saveState(path, state) {
69
+ const dir = (0, import_node_path.dirname)(path);
70
+ if (!(0, import_node_fs.existsSync)(dir)) (0, import_node_fs.mkdirSync)(dir, { recursive: true });
71
+ (0, import_node_fs.writeFileSync)(path, JSON.stringify(state, null, 2));
72
+ }
73
+ async function getAccessAuthDomain(apiToken, accountId) {
74
+ const res = await cf({
75
+ apiToken,
76
+ method: "GET",
77
+ path: `/accounts/${accountId}/access/organizations`
78
+ });
79
+ if (!res.success || !res.result?.auth_domain) {
80
+ fail(res, "GET access/organizations (is Zero Trust enabled on this account?)");
81
+ }
82
+ return res.result.auth_domain;
83
+ }
84
+ async function ensureAccessIdp(input) {
85
+ const config = {
86
+ client_id: input.clientId,
87
+ client_secret: input.clientSecret,
88
+ auth_url: `${input.issuer}/api/oidc/authorize`,
89
+ token_url: `${input.issuer}/api/oidc/token`,
90
+ certs_url: `${input.issuer}/.well-known/jwks.json`,
91
+ scopes: ["openid", "email", "profile"],
92
+ claims: ["org", "project_access", "roles", "email"],
93
+ email_claim_name: "email",
94
+ pkce_enabled: true
95
+ };
96
+ const body = { name: IDP_NAME, type: "oidc", config };
97
+ let idpId = input.knownIdpId;
98
+ if (!idpId) {
99
+ const list3 = await cf({
100
+ apiToken: input.apiToken,
101
+ method: "GET",
102
+ path: `/accounts/${input.accountId}/access/identity_providers`
103
+ });
104
+ if (!list3.success) fail(list3, "list identity_providers");
105
+ idpId = (list3.result ?? []).find((p) => p.name === IDP_NAME)?.id ?? null;
106
+ }
107
+ if (idpId) {
108
+ const upd = await cf({
109
+ apiToken: input.apiToken,
110
+ method: "PUT",
111
+ path: `/accounts/${input.accountId}/access/identity_providers/${idpId}`,
112
+ body
113
+ });
114
+ if (!upd.success || !upd.result) fail(upd, "update identity_provider");
115
+ return upd.result.id;
116
+ }
117
+ const created = await cf({
118
+ apiToken: input.apiToken,
119
+ method: "POST",
120
+ path: `/accounts/${input.accountId}/access/identity_providers`,
121
+ body
122
+ });
123
+ if (!created.success || !created.result) fail(created, "create identity_provider");
124
+ return created.result.id;
125
+ }
126
+ async function ensureAccessApp(input) {
127
+ const { service } = input;
128
+ const appDomain = service.path ? `${service.hostname}${service.path}` : service.hostname;
129
+ const policy = service.bypass ? {
130
+ name: "launch-kit-public-bypass",
131
+ decision: "bypass",
132
+ include: [{ everyone: {} }]
133
+ } : {
134
+ name: "launch-kit-org-allow",
135
+ decision: "allow",
136
+ include: [
137
+ {
138
+ oidc: {
139
+ identity_provider_id: input.idpId,
140
+ claim_name: "org",
141
+ claim_value: input.organizationId
142
+ }
143
+ }
144
+ ]
145
+ };
146
+ const body = service.bypass ? {
147
+ name: `launch-kit ${appDomain} (public)`,
148
+ domain: appDomain,
149
+ type: "self_hosted",
150
+ policies: [policy]
151
+ } : {
152
+ name: `launch-kit ${appDomain}`,
153
+ domain: appDomain,
154
+ type: "self_hosted",
155
+ // Bot terminal = RCE surface → short session. Read portals = a workday.
156
+ session_duration: service.strict ? "30m" : "24h",
157
+ allowed_idps: [input.idpId],
158
+ auto_redirect_to_identity: true,
159
+ policies: [policy]
160
+ };
161
+ const list3 = await cf({
162
+ apiToken: input.apiToken,
163
+ method: "GET",
164
+ path: `/accounts/${input.accountId}/access/apps`
165
+ });
166
+ if (!list3.success) fail(list3, "list access apps");
167
+ const existing = (list3.result ?? []).find((a) => a.domain === appDomain);
168
+ if (existing) {
169
+ const upd = await cf({
170
+ apiToken: input.apiToken,
171
+ method: "PUT",
172
+ path: `/accounts/${input.accountId}/access/apps/${existing.id}`,
173
+ body
174
+ });
175
+ if (!upd.success || !upd.result) fail(upd, `update access app ${appDomain}`);
176
+ return upd.result.id;
177
+ }
178
+ const created = await cf({
179
+ apiToken: input.apiToken,
180
+ method: "POST",
181
+ path: `/accounts/${input.accountId}/access/apps`,
182
+ body
183
+ });
184
+ if (!created.success || !created.result) fail(created, `create access app ${appDomain}`);
185
+ return created.result.id;
186
+ }
187
+ async function provisionAccess(input) {
188
+ const authDomain = await getAccessAuthDomain(input.apiToken, input.accountId);
189
+ const callbackUrl = `https://${authDomain}/cdn-cgi/access/callback`;
190
+ const { clientId, clientSecret, organizationId } = await input.registerClient([callbackUrl]);
191
+ const prior = loadState(input.stateFile);
192
+ const idpId = await ensureAccessIdp({
193
+ apiToken: input.apiToken,
194
+ accountId: input.accountId,
195
+ issuer: input.issuer,
196
+ clientId,
197
+ clientSecret,
198
+ knownIdpId: prior?.idpId ?? null
199
+ });
200
+ saveState(input.stateFile, { idpId, accountId: input.accountId });
201
+ const appIds = {};
202
+ for (const service of input.services) {
203
+ const appDomain = service.path ? `${service.hostname}${service.path}` : service.hostname;
204
+ appIds[appDomain] = await ensureAccessApp({
205
+ apiToken: input.apiToken,
206
+ accountId: input.accountId,
207
+ idpId,
208
+ organizationId,
209
+ service
210
+ });
211
+ }
212
+ return { idpId, authDomain, appIds };
213
+ }
214
+ var import_node_fs, import_node_path, CF_API_BASE, IDP_NAME;
215
+ var init_cf_access = __esm({
216
+ "src/server/cf-access.ts"() {
217
+ "use strict";
218
+ import_node_fs = require("node:fs");
219
+ import_node_path = require("node:path");
220
+ CF_API_BASE = "https://api.cloudflare.com/client/v4";
221
+ IDP_NAME = "launch-kit-oidc";
222
+ }
223
+ });
224
+
36
225
  // src/server/cf-ingress.ts
37
226
  function serviceLabel(s) {
38
227
  return s.label ?? s.name;
39
228
  }
40
- async function cf(opts) {
41
- const res = await fetch(`${CF_API_BASE}${opts.path}`, {
229
+ async function cf2(opts) {
230
+ const res = await fetch(`${CF_API_BASE2}${opts.path}`, {
42
231
  method: opts.method,
43
232
  headers: {
44
233
  Authorization: `Bearer ${opts.apiToken}`,
@@ -63,7 +252,7 @@ function isNotFound(env) {
63
252
  }
64
253
  async function findTunnelByName(input) {
65
254
  const q = new URLSearchParams({ name: input.tunnelName, is_deleted: "false" }).toString();
66
- const res = await cf({
255
+ const res = await cf2({
67
256
  apiToken: input.apiToken,
68
257
  method: "GET",
69
258
  path: `/accounts/${input.accountId}/cfd_tunnel?${q}`
@@ -72,10 +261,10 @@ async function findTunnelByName(input) {
72
261
  const live = res.result.find((t) => t.name === input.tunnelName && !t.deleted_at);
73
262
  return live?.id ?? null;
74
263
  }
75
- function loadState(path) {
76
- if (!(0, import_node_fs.existsSync)(path)) return null;
264
+ function loadState2(path) {
265
+ if (!(0, import_node_fs2.existsSync)(path)) return null;
77
266
  try {
78
- const parsed = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
267
+ const parsed = JSON.parse((0, import_node_fs2.readFileSync)(path, "utf8"));
79
268
  if (typeof parsed?.tunnelId === "string" && typeof parsed?.accountId === "string") {
80
269
  return parsed;
81
270
  }
@@ -84,14 +273,14 @@ function loadState(path) {
84
273
  return null;
85
274
  }
86
275
  }
87
- function saveState(path, state) {
88
- const dir = (0, import_node_path.dirname)(path);
89
- if (!(0, import_node_fs.existsSync)(dir)) (0, import_node_fs.mkdirSync)(dir, { recursive: true });
90
- (0, import_node_fs.writeFileSync)(path, JSON.stringify(state, null, 2));
276
+ function saveState2(path, state) {
277
+ const dir = (0, import_node_path2.dirname)(path);
278
+ if (!(0, import_node_fs2.existsSync)(dir)) (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
279
+ (0, import_node_fs2.writeFileSync)(path, JSON.stringify(state, null, 2));
91
280
  }
92
281
  async function ensureTunnel(input, knownTunnelId) {
93
282
  if (knownTunnelId) {
94
- const got = await cf({
283
+ const got = await cf2({
95
284
  apiToken: input.apiToken,
96
285
  method: "GET",
97
286
  path: `/accounts/${input.accountId}/cfd_tunnel/${knownTunnelId}`
@@ -108,7 +297,7 @@ async function ensureTunnel(input, knownTunnelId) {
108
297
  console.log(`[cf] adopted existing tunnel "${input.tunnelName}" (${existing}) \u2014 local state was missing`);
109
298
  return existing;
110
299
  }
111
- const created = await cf({
300
+ const created = await cf2({
112
301
  apiToken: input.apiToken,
113
302
  method: "POST",
114
303
  path: `/accounts/${input.accountId}/cfd_tunnel`,
@@ -125,7 +314,7 @@ async function ensureTunnel(input, knownTunnelId) {
125
314
  throw new Error(`[cf] tunnel create failed: ${JSON.stringify(created.errors)}`);
126
315
  }
127
316
  async function fetchConnectorToken(input, tunnelId) {
128
- const res = await cf({
317
+ const res = await cf2({
129
318
  apiToken: input.apiToken,
130
319
  method: "GET",
131
320
  path: `/accounts/${input.accountId}/cfd_tunnel/${tunnelId}/token`
@@ -138,10 +327,10 @@ async function fetchConnectorToken(input, tunnelId) {
138
327
  async function setIngressConfig(input, tunnelId) {
139
328
  const ingress = input.services.map((s) => ({
140
329
  hostname: `${serviceLabel(s)}.${input.zone.name}`,
141
- service: `http://localhost:${s.port}`
330
+ service: `http://127.0.0.1:${s.port}`
142
331
  }));
143
332
  ingress.push({ service: "http_status:404" });
144
- const res = await cf({
333
+ const res = await cf2({
145
334
  apiToken: input.apiToken,
146
335
  method: "PUT",
147
336
  path: `/accounts/${input.accountId}/cfd_tunnel/${tunnelId}/configurations`,
@@ -154,7 +343,7 @@ async function setIngressConfig(input, tunnelId) {
154
343
  async function ensureDnsRecord(input, tunnelId, service) {
155
344
  const fqdn = `${serviceLabel(service)}.${input.zone.name}`;
156
345
  const target = `${tunnelId}.cfargotunnel.com`;
157
- const existing = await cf({
346
+ const existing = await cf2({
158
347
  apiToken: input.apiToken,
159
348
  method: "GET",
160
349
  path: `/zones/${input.zone.id}/dns_records?name=${encodeURIComponent(fqdn)}&type=CNAME`
@@ -162,7 +351,7 @@ async function ensureDnsRecord(input, tunnelId, service) {
162
351
  if (existing.success && Array.isArray(existing.result) && existing.result.length > 0) {
163
352
  const rec = existing.result[0];
164
353
  if (rec.content === target) return;
165
- const upd = await cf({
354
+ const upd = await cf2({
166
355
  apiToken: input.apiToken,
167
356
  method: "PUT",
168
357
  path: `/zones/${input.zone.id}/dns_records/${rec.id}`,
@@ -173,7 +362,7 @@ async function ensureDnsRecord(input, tunnelId, service) {
173
362
  }
174
363
  return;
175
364
  }
176
- const created = await cf({
365
+ const created = await cf2({
177
366
  apiToken: input.apiToken,
178
367
  method: "POST",
179
368
  path: `/zones/${input.zone.id}/dns_records`,
@@ -184,9 +373,9 @@ async function ensureDnsRecord(input, tunnelId, service) {
184
373
  throw new Error(`[cf] DNS record create for ${fqdn} failed: ${JSON.stringify(created.errors)}`);
185
374
  }
186
375
  async function provisionIngress(input) {
187
- const prior = loadState(input.stateFile);
376
+ const prior = loadState2(input.stateFile);
188
377
  const tunnelId = await ensureTunnel(input, prior?.tunnelId ?? null);
189
- saveState(input.stateFile, {
378
+ saveState2(input.stateFile, {
190
379
  tunnelId,
191
380
  accountId: input.accountId,
192
381
  tunnelName: input.tunnelName,
@@ -199,13 +388,13 @@ async function provisionIngress(input) {
199
388
  for (const s of input.services) hostnames[s.name] = `${serviceLabel(s)}.${input.zone.name}`;
200
389
  return { tunnelId, connectorToken, hostnames };
201
390
  }
202
- var import_node_fs, import_node_path, CF_API_BASE, CF_ERR_DNS_RECORD_EXISTS;
391
+ var import_node_fs2, import_node_path2, CF_API_BASE2, CF_ERR_DNS_RECORD_EXISTS;
203
392
  var init_cf_ingress = __esm({
204
393
  "src/server/cf-ingress.ts"() {
205
394
  "use strict";
206
- import_node_fs = require("node:fs");
207
- import_node_path = require("node:path");
208
- CF_API_BASE = "https://api.cloudflare.com/client/v4";
395
+ import_node_fs2 = require("node:fs");
396
+ import_node_path2 = require("node:path");
397
+ CF_API_BASE2 = "https://api.cloudflare.com/client/v4";
209
398
  CF_ERR_DNS_RECORD_EXISTS = 81053;
210
399
  }
211
400
  });
@@ -516,16 +705,16 @@ __export(state_exports, {
516
705
  saveRoverState: () => saveRoverState
517
706
  });
518
707
  function stateDir() {
519
- return (0, import_node_path2.join)((0, import_node_os.homedir)(), LAUNCHSECURE_DIR, "rover");
708
+ return (0, import_node_path3.join)((0, import_node_os.homedir)(), LAUNCHSECURE_DIR, "rover");
520
709
  }
521
710
  function statePath(roverId) {
522
- return (0, import_node_path2.join)(stateDir(), `${roverId}.json`);
711
+ return (0, import_node_path3.join)(stateDir(), `${roverId}.json`);
523
712
  }
524
713
  function loadRoverState(roverId) {
525
714
  const p = statePath(roverId);
526
- if (!(0, import_node_fs2.existsSync)(p)) return null;
715
+ if (!(0, import_node_fs3.existsSync)(p)) return null;
527
716
  try {
528
- const data = JSON.parse((0, import_node_fs2.readFileSync)(p, "utf-8"));
717
+ const data = JSON.parse((0, import_node_fs3.readFileSync)(p, "utf-8"));
529
718
  if (!data.roverId || !data.secret || !data.tunnelUrl) return null;
530
719
  return data;
531
720
  } catch {
@@ -533,21 +722,21 @@ function loadRoverState(roverId) {
533
722
  }
534
723
  }
535
724
  function saveRoverState(state) {
536
- (0, import_node_fs2.mkdirSync)(stateDir(), { recursive: true });
725
+ (0, import_node_fs3.mkdirSync)(stateDir(), { recursive: true });
537
726
  const final = statePath(state.roverId);
538
727
  const tmp = `${final}.tmp`;
539
- (0, import_node_fs2.writeFileSync)(tmp, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
540
- (0, import_node_fs2.renameSync)(tmp, final);
728
+ (0, import_node_fs3.writeFileSync)(tmp, JSON.stringify(state, null, 2) + "\n", { mode: 384 });
729
+ (0, import_node_fs3.renameSync)(tmp, final);
541
730
  }
542
731
  function clearRoverState(roverId) {
543
732
  try {
544
- (0, import_node_fs2.unlinkSync)(statePath(roverId));
733
+ (0, import_node_fs3.unlinkSync)(statePath(roverId));
545
734
  } catch {
546
735
  }
547
736
  }
548
737
  function listRoverIds() {
549
738
  const dir = stateDir();
550
- if (!(0, import_node_fs2.existsSync)(dir)) return [];
739
+ if (!(0, import_node_fs3.existsSync)(dir)) return [];
551
740
  try {
552
741
  const fs = require("node:fs");
553
742
  return fs.readdirSync(dir).filter((f) => f.endsWith(".json") && !f.includes(".tunnel.")).map((f) => f.slice(0, -5));
@@ -555,13 +744,13 @@ function listRoverIds() {
555
744
  return [];
556
745
  }
557
746
  }
558
- var import_node_fs2, import_node_os, import_node_path2;
747
+ var import_node_fs3, import_node_os, import_node_path3;
559
748
  var init_state = __esm({
560
749
  "src/server/rover/state.ts"() {
561
750
  "use strict";
562
- import_node_fs2 = require("node:fs");
751
+ import_node_fs3 = require("node:fs");
563
752
  import_node_os = require("node:os");
564
- import_node_path2 = require("node:path");
753
+ import_node_path3 = require("node:path");
565
754
  init_launch_kit_paths();
566
755
  }
567
756
  });
@@ -610,7 +799,7 @@ var require_package = __commonJS({
610
799
  "package.json"(exports2, module2) {
611
800
  module2.exports = {
612
801
  name: "@launchsecure/launch-kit",
613
- version: "0.0.43",
802
+ version: "0.0.45",
614
803
  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.",
615
804
  license: "MIT",
616
805
  author: "LaunchSecure - AutomateWithUs",
@@ -751,7 +940,11 @@ function cog(transform) {
751
940
  function dish(transform) {
752
941
  return `<g transform="${transform}" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 10a7.31 7.31 0 0 0 10 10Z" fill="currentColor"/><path d="m9 15 3-3" fill="none"/><path d="M17 13a6 6 0 0 0-6-6" fill="none"/><path d="M21 13A10 10 0 0 0 11 3" fill="none"/></g>`;
753
942
  }
754
- function roverMark(size, bg) {
943
+ function roverMark(size, bg, variant = "mini") {
944
+ if (variant === "full") {
945
+ const h2 = Math.round(size * 300 / 420);
946
+ return `<svg width="${size}" height="${h2}" viewBox="-60 0 420 300" fill="none" xmlns="http://www.w3.org/2000/svg" style="overflow:visible">` + armClaw(6, "translate(0,10)") + armClaw(6, "translate(320,10) scale(-1,1)") + `<g fill="none" stroke="currentColor" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"><path d="M140 127 H180"/><path d="M160 127 V160"/></g><g fill="currentColor"><circle cx="140" cy="127" r="8"/><circle cx="160" cy="127" r="8"/><circle cx="180" cy="127" r="8"/></g><g fill="${bg}"><circle cx="140" cy="127" r="3"/><circle cx="160" cy="127" r="3"/><circle cx="180" cy="127" r="3"/></g><g transform="translate(40.6,70) scale(3.7)" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12 7 2"/><path d="m7 12 5-10"/><path d="m12 12 5-10"/><path d="m17 12 5-10"/><path d="M7.3 1.3h15"/><path d="M4.5 7h15"/><path d="M1.7 12.7h15"/><path d="M12 16v6"/></g><rect x="230" y="146" width="6" height="21" fill="currentColor"/>` + dish("translate(200,66) scale(4)") + `<path fill="currentColor" d="M61.95 148.09 L258.05 165.91 Q270 167 270 179 L270 228 Q270 240 258 240 L62 240 Q50 240 50 228 L50 159 Q50 147 61.95 148.09 Z"/><path fill="none" stroke="${bg}" stroke-width="6" stroke-linecap="round" d="M60 157.95 L260 176.13"/><g fill="${bg}"><circle cx="50" cy="240" r="44"/><circle cx="160" cy="240" r="44"/><circle cx="270" cy="240" r="44"/></g><g fill="currentColor"><circle cx="50" cy="240" r="38"/><circle cx="160" cy="240" r="38"/><circle cx="270" cy="240" r="38"/></g><g fill="none" stroke="${bg}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${cog("translate(32,222) scale(1.5)")}${cog("translate(142,222) scale(1.5)")}${cog("translate(252,222) scale(1.5)")}</g></svg>`;
947
+ }
755
948
  const h = Math.round(size * 195 / 190);
756
949
  return `<svg width="${size}" height="${h}" viewBox="25 0 190 195" fill="none" xmlns="http://www.w3.org/2000/svg" style="overflow:visible">` + armClaw(7, "translate(46.5,-6.6) scale(0.55)") + armClaw(7, "translate(193.5,-6.6) scale(-0.55,0.55)") + `<rect x="112" y="46" width="5" height="22" fill="currentColor"/>` + dish("translate(92.4,4.4) scale(2.3)") + `<path fill="currentColor" d="M84.15 65 L155.85 71.9 Q166.8 72.95 166.8 83.95 L166.8 119.55 Q166.8 130.55 155.8 130.55 L84.2 130.55 Q73.2 130.55 73.2 119.55 L73.2 74.95 Q73.2 63.95 84.15 65 Z"/><path fill="none" stroke="${bg}" stroke-width="3" stroke-linecap="round" d="M85 73 L155 80"/><g fill="${bg}"><circle cx="73" cy="131" r="29"/><circle cx="167" cy="131" r="29"/></g><g fill="currentColor"><circle cx="73" cy="131" r="24"/><circle cx="167" cy="131" r="24"/></g><g fill="none" stroke="${bg}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${cog("translate(59.8,117.8) scale(1.1)")}${cog("translate(153.8,117.8) scale(1.1)")}</g></svg>`;
757
950
  }
@@ -793,7 +986,7 @@ function renderDashboard(state, pods) {
793
986
  const runtime = state.defaultRuntime ?? "docker";
794
987
  const roverName = state.name ?? shortId(state.roverId);
795
988
  const body = pods.length === 0 ? `<div class="empty">
796
- <div class="emptyicon" style="color:${C.primary}">${roverMark(56, C.card)}</div>
989
+ <div class="emptyicon" style="color:${C.primary}">${roverMark(104, C.card, "full")}</div>
797
990
  <div class="big">No pods running</div>
798
991
  <p class="muted">This rover is up and connected, but isn't handling any pods yet.</p>
799
992
  <a class="btn" href="${esc(ls)}" target="_blank" rel="noopener">Launch a project in LaunchSecure \u2192</a>
@@ -877,7 +1070,7 @@ function renderDashboard(state, pods) {
877
1070
  </head><body>
878
1071
  <div class="topbar">
879
1072
  <div class="brand">
880
- <span class="logobox">${roverMark(20, "hsl(259 28% 12%)")}</span>
1073
+ <span class="logobox">${roverMark(28, "hsl(259 28% 12%)", "full")}</span>
881
1074
  <span class="wordmark">LaunchRover</span>
882
1075
  </div>
883
1076
  <div class="context">
@@ -1001,12 +1194,14 @@ function coerceEntry(raw, index) {
1001
1194
  if (r.args !== void 0 && (!Array.isArray(r.args) || r.args.some((a) => typeof a !== "string"))) {
1002
1195
  throw new Error(`[launch-kit-services] entry #${index}: args must be a string[]`);
1003
1196
  }
1004
- return {
1197
+ const spec = {
1005
1198
  name: r.name,
1006
1199
  port: r.port,
1007
1200
  bin: r.bin,
1008
1201
  args: r.args ?? []
1009
1202
  };
1203
+ if (r.skipSpawn === true || r.bin === "") spec.skipSpawn = true;
1204
+ return spec;
1010
1205
  }
1011
1206
  function validate(services) {
1012
1207
  if (services.length === 0) {
@@ -1029,6 +1224,9 @@ function validate(services) {
1029
1224
  throw new Error(`[launch-kit-services] duplicate port ${s.port} (services must each listen on a unique port)`);
1030
1225
  }
1031
1226
  seenPorts.add(s.port);
1227
+ if (!s.skipSpawn && s.bin === "") {
1228
+ throw new Error(`[launch-kit-services] service "${s.name}" has an empty bin but is not ingress-only (skipSpawn) \u2014 nothing to spawn`);
1229
+ }
1032
1230
  }
1033
1231
  return services;
1034
1232
  }
@@ -1048,11 +1246,11 @@ function resolveServices(opts = {}) {
1048
1246
  }
1049
1247
  return validate(parsed.map(coerceEntry));
1050
1248
  }
1051
- const filePath = (0, import_node_path3.join)(cwd, ".launchpod", "services.json");
1052
- if ((0, import_node_fs3.existsSync)(filePath)) {
1249
+ const filePath = (0, import_node_path4.join)(cwd, ".launchpod", "services.json");
1250
+ if ((0, import_node_fs4.existsSync)(filePath)) {
1053
1251
  let parsed;
1054
1252
  try {
1055
- parsed = JSON.parse((0, import_node_fs3.readFileSync)(filePath, "utf8"));
1253
+ parsed = JSON.parse((0, import_node_fs4.readFileSync)(filePath, "utf8"));
1056
1254
  } catch (err) {
1057
1255
  throw new Error(`[launch-kit-services] ${filePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
1058
1256
  }
@@ -1063,12 +1261,12 @@ function resolveServices(opts = {}) {
1063
1261
  }
1064
1262
  return validate(defaultServices());
1065
1263
  }
1066
- var import_node_fs3, import_node_path3, SHORTHANDS, DNS_NAME_RE, SHORTHAND_NAMES;
1264
+ var import_node_fs4, import_node_path4, SHORTHANDS, DNS_NAME_RE, SHORTHAND_NAMES;
1067
1265
  var init_launch_kit_services = __esm({
1068
1266
  "src/server/launch-kit-services.ts"() {
1069
1267
  "use strict";
1070
- import_node_fs3 = require("node:fs");
1071
- import_node_path3 = require("node:path");
1268
+ import_node_fs4 = require("node:fs");
1269
+ import_node_path4 = require("node:path");
1072
1270
  SHORTHANDS = {
1073
1271
  radar: { port: 3517, bin: "launch-radar", args: [] },
1074
1272
  sequencer: { port: 3517, bin: "launch-sequencer", args: [] },
@@ -1088,12 +1286,23 @@ var init_launch_kit_services = __esm({
1088
1286
  function slugLabel(projectSlug) {
1089
1287
  return projectSlug.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1090
1288
  }
1091
- function serviceHostname(projectSlug, serviceName, zone) {
1092
- return `${slugLabel(projectSlug)}-${serviceName}.${zone}`;
1289
+ function roverToken(roverId) {
1290
+ let h = 2166136261;
1291
+ for (let i = 0; i < roverId.length; i++) {
1292
+ h ^= roverId.charCodeAt(i);
1293
+ h = Math.imul(h, 16777619);
1294
+ }
1295
+ return ((h >>> 0) % 2176782336).toString(36).padStart(6, "0");
1296
+ }
1297
+ function serviceLabel2(projectSlug, serviceName, roverId) {
1298
+ return `${serviceName}-${slugLabel(projectSlug)}-${roverToken(roverId)}`;
1299
+ }
1300
+ function serviceHostname(projectSlug, serviceName, roverId, zone) {
1301
+ return `${serviceLabel2(projectSlug, serviceName, roverId)}.${zone}`;
1093
1302
  }
1094
- function serviceUrl(projectSlug, serviceName, zone) {
1095
- if (!zone) return null;
1096
- return `https://${serviceHostname(projectSlug, serviceName, zone)}`;
1303
+ function serviceUrl(projectSlug, serviceName, roverId, zone) {
1304
+ if (!zone || !roverId) return null;
1305
+ return `https://${serviceHostname(projectSlug, serviceName, roverId, zone)}`;
1097
1306
  }
1098
1307
  var init_hostnames = __esm({
1099
1308
  "src/server/rover/runtime/hostnames.ts"() {
@@ -1115,8 +1324,12 @@ function validateUpRequest(state, body) {
1115
1324
  if (typeof body.projectSlug !== "string" || !DNS_SLUG_RE.test(body.projectSlug)) {
1116
1325
  throw new PodError("projectSlug must be a DNS-safe string (a-z, 0-9, -)");
1117
1326
  }
1118
- if (typeof body.claudeCredentialsB64 !== "string" || body.claudeCredentialsB64.length < 16) {
1119
- throw new PodError("claudeCredentialsB64 is required (base64-encoded Claude credentials JSON)");
1327
+ const hasBlob = typeof body.claudeCredentialsB64 === "string" && body.claudeCredentialsB64.length >= 16;
1328
+ const hasToken = typeof body.claudeOauthToken === "string" && body.claudeOauthToken.length >= 16;
1329
+ if (!hasBlob && !hasToken) {
1330
+ throw new PodError(
1331
+ "one of claudeCredentialsB64 (base64-encoded Claude credentials JSON) or claudeOauthToken (CLAUDE_CODE_OAUTH_TOKEN from `claude setup-token`) is required"
1332
+ );
1120
1333
  }
1121
1334
  }
1122
1335
  var DNS_SLUG_RE, PodError;
@@ -1174,12 +1387,17 @@ async function create(state, body) {
1174
1387
  if (body.podId) runArgs.push("--label", `launch-rover.podId=${body.podId}`);
1175
1388
  if (body.name) runArgs.push("--label", `launch-rover.name=${body.name}`);
1176
1389
  const envVars = {
1177
- CLAUDE_CREDENTIALS_B64: body.claudeCredentialsB64,
1178
1390
  LS_PAT: state.installPat,
1179
1391
  LS_ORG_SLUG: state.orgSlug,
1180
1392
  LS_PROJECT_SLUG: body.projectSlug,
1393
+ // Folded into each service's DNS label by the pod entry so two rovers
1394
+ // running the same project on the same zone don't collide — see
1395
+ // runtime/hostnames.ts. Also read back in servicesForContainer below.
1396
+ LS_ROVER_ID: state.roverId,
1181
1397
  LS_SERVER_URL: rewriteLocalhostForContainer(state.serverUrl)
1182
1398
  };
1399
+ if (body.claudeCredentialsB64) envVars.CLAUDE_CREDENTIALS_B64 = body.claudeCredentialsB64;
1400
+ if (body.claudeOauthToken) envVars.CLAUDE_CODE_OAUTH_TOKEN = body.claudeOauthToken;
1183
1401
  if (body.services && Array.isArray(body.services)) {
1184
1402
  envVars.LAUNCHKIT_SERVICES = JSON.stringify(body.services);
1185
1403
  }
@@ -1196,7 +1414,7 @@ async function create(state, body) {
1196
1414
  const containerId = (await dockerRun(runArgs)).stdout.trim();
1197
1415
  return { podId: body.podId ?? null, projectSlug: body.projectSlug, containerId, image, createdAt, runtime: "docker" };
1198
1416
  } finally {
1199
- if (dockerConfigDir) (0, import_node_fs4.rmSync)(dockerConfigDir, { recursive: true, force: true });
1417
+ if (dockerConfigDir) (0, import_node_fs5.rmSync)(dockerConfigDir, { recursive: true, force: true });
1200
1418
  }
1201
1419
  }
1202
1420
  async function stop(_state, body) {
@@ -1278,6 +1496,7 @@ async function servicesForContainer(ref, projectSlug) {
1278
1496
  if (eq > 0) env[kv.slice(0, eq)] = kv.slice(eq + 1);
1279
1497
  }
1280
1498
  const zone = env.LAUNCHKIT_CF_BASE_DOMAIN || null;
1499
+ const roverId = env.LS_ROVER_ID || null;
1281
1500
  let specs;
1282
1501
  try {
1283
1502
  specs = resolveServices({
@@ -1286,7 +1505,7 @@ async function servicesForContainer(ref, projectSlug) {
1286
1505
  } catch {
1287
1506
  return void 0;
1288
1507
  }
1289
- return specs.filter((s) => !s.skipSpawn).map((s) => ({ name: s.name, port: s.port, url: serviceUrl(projectSlug, s.name, zone) }));
1508
+ return specs.filter((s) => !s.skipSpawn).map((s) => ({ name: s.name, port: s.port, url: serviceUrl(projectSlug, s.name, roverId, zone) }));
1290
1509
  }
1291
1510
  async function purgeAll(state) {
1292
1511
  const { pods } = await list(state);
@@ -1376,10 +1595,10 @@ function setupDockerConfig() {
1376
1595
  );
1377
1596
  return null;
1378
1597
  }
1379
- const dir = (0, import_node_fs4.mkdtempSync)((0, import_node_path4.join)((0, import_node_os2.tmpdir)(), "launch-rover-docker-"));
1598
+ const dir = (0, import_node_fs5.mkdtempSync)((0, import_node_path5.join)((0, import_node_os2.tmpdir)(), "launch-rover-docker-"));
1380
1599
  const auth = Buffer.from(`${GHCR_USERNAME}:${token}`, "utf-8").toString("base64");
1381
1600
  const config = { auths: { [GHCR_REGISTRY]: { auth } } };
1382
- (0, import_node_fs4.writeFileSync)((0, import_node_path4.join)(dir, "config.json"), JSON.stringify(config), { mode: 384 });
1601
+ (0, import_node_fs5.writeFileSync)((0, import_node_path5.join)(dir, "config.json"), JSON.stringify(config), { mode: 384 });
1383
1602
  return dir;
1384
1603
  }
1385
1604
  function parsePsRow(row) {
@@ -1410,14 +1629,14 @@ function parseLabels(raw) {
1410
1629
  }
1411
1630
  return out;
1412
1631
  }
1413
- var import_node_child_process2, import_node_fs4, import_node_os2, import_node_path4, import_node_util, execFileAsync, DEFAULT_IMAGE, GHCR_REGISTRY, GHCR_USERNAME, MANAGED_LABEL, POD_NAME_PREFIX, VOLUME_PREFIX, dockerRuntime;
1632
+ var import_node_child_process2, import_node_fs5, import_node_os2, import_node_path5, import_node_util, execFileAsync, DEFAULT_IMAGE, GHCR_REGISTRY, GHCR_USERNAME, MANAGED_LABEL, POD_NAME_PREFIX, VOLUME_PREFIX, dockerRuntime;
1414
1633
  var init_docker = __esm({
1415
1634
  "src/server/rover/runtime/docker.ts"() {
1416
1635
  "use strict";
1417
1636
  import_node_child_process2 = require("node:child_process");
1418
- import_node_fs4 = require("node:fs");
1637
+ import_node_fs5 = require("node:fs");
1419
1638
  import_node_os2 = require("node:os");
1420
- import_node_path4 = require("node:path");
1639
+ import_node_path5 = require("node:path");
1421
1640
  import_node_util = require("node:util");
1422
1641
  init_launch_kit_services();
1423
1642
  init_hostnames();
@@ -1465,16 +1684,16 @@ var init_ports = __esm({
1465
1684
 
1466
1685
  // src/server/rover/runtime/registry.ts
1467
1686
  function podsRoot() {
1468
- return (0, import_node_path5.join)((0, import_node_os3.homedir)(), LAUNCHSECURE_DIR, "pods");
1687
+ return (0, import_node_path6.join)((0, import_node_os3.homedir)(), LAUNCHSECURE_DIR, "pods");
1469
1688
  }
1470
1689
  function registryPath() {
1471
- return (0, import_node_path5.join)(podsRoot(), "registry.json");
1690
+ return (0, import_node_path6.join)(podsRoot(), "registry.json");
1472
1691
  }
1473
1692
  function load() {
1474
1693
  const p = registryPath();
1475
- if (!(0, import_node_fs5.existsSync)(p)) return { pods: [] };
1694
+ if (!(0, import_node_fs6.existsSync)(p)) return { pods: [] };
1476
1695
  try {
1477
- const doc = JSON.parse((0, import_node_fs5.readFileSync)(p, "utf-8"));
1696
+ const doc = JSON.parse((0, import_node_fs6.readFileSync)(p, "utf-8"));
1478
1697
  if (!doc || !Array.isArray(doc.pods)) return { pods: [] };
1479
1698
  return doc;
1480
1699
  } catch {
@@ -1483,11 +1702,11 @@ function load() {
1483
1702
  }
1484
1703
  function save(doc) {
1485
1704
  const dir = podsRoot();
1486
- (0, import_node_fs5.mkdirSync)(dir, { recursive: true });
1705
+ (0, import_node_fs6.mkdirSync)(dir, { recursive: true });
1487
1706
  const final = registryPath();
1488
1707
  const tmp = `${final}.tmp`;
1489
- (0, import_node_fs5.writeFileSync)(tmp, JSON.stringify(doc, null, 2) + "\n", { mode: 384 });
1490
- (0, import_node_fs5.renameSync)(tmp, final);
1708
+ (0, import_node_fs6.writeFileSync)(tmp, JSON.stringify(doc, null, 2) + "\n", { mode: 384 });
1709
+ (0, import_node_fs6.renameSync)(tmp, final);
1491
1710
  }
1492
1711
  function recordIsAlive(rec) {
1493
1712
  try {
@@ -1529,13 +1748,13 @@ function removeBySlug(projectSlug) {
1529
1748
  const next = doc.pods.filter((p) => p.projectSlug !== projectSlug);
1530
1749
  if (next.length !== doc.pods.length) save({ pods: next });
1531
1750
  }
1532
- var import_node_fs5, import_node_os3, import_node_path5;
1751
+ var import_node_fs6, import_node_os3, import_node_path6;
1533
1752
  var init_registry = __esm({
1534
1753
  "src/server/rover/runtime/registry.ts"() {
1535
1754
  "use strict";
1536
- import_node_fs5 = require("node:fs");
1755
+ import_node_fs6 = require("node:fs");
1537
1756
  import_node_os3 = require("node:os");
1538
- import_node_path5 = require("node:path");
1757
+ import_node_path6 = require("node:path");
1539
1758
  init_launch_kit_paths();
1540
1759
  init_ports();
1541
1760
  }
@@ -1546,27 +1765,29 @@ function isDeniedEnvKey(k) {
1546
1765
  return DENIED_ENV.has(k) || /^(LD_|DYLD_)/.test(k);
1547
1766
  }
1548
1767
  function buildPodEnv(state, spec, root, services) {
1549
- const home = (0, import_node_path6.join)(root, "home");
1550
- const workspace = (0, import_node_path6.join)(root, "workspace");
1768
+ const home = (0, import_node_path7.join)(root, "home");
1769
+ const workspace = (0, import_node_path7.join)(root, "workspace");
1551
1770
  const env = {};
1552
1771
  for (const k of BASE_ENV_PASSTHROUGH) {
1553
1772
  const v = process.env[k];
1554
1773
  if (typeof v === "string") env[k] = v;
1555
1774
  }
1556
1775
  if (spec.claudeCredentialsB64) env.CLAUDE_CREDENTIALS_B64 = spec.claudeCredentialsB64;
1776
+ if (spec.claudeOauthToken) env.CLAUDE_CODE_OAUTH_TOKEN = spec.claudeOauthToken;
1557
1777
  env.LS_PAT = state.installPat;
1558
1778
  env.LS_ORG_SLUG = state.orgSlug;
1559
1779
  env.LS_PROJECT_SLUG = spec.projectSlug;
1780
+ env.LS_ROVER_ID = state.roverId;
1560
1781
  env.LS_SERVER_URL = state.serverUrl;
1561
1782
  env.LAUNCHKIT_SERVICES = JSON.stringify(servicesToEntries(services));
1562
1783
  if (spec.cfBaseDomain) env.LAUNCHKIT_CF_BASE_DOMAIN = spec.cfBaseDomain;
1563
1784
  env.HOME = home;
1564
1785
  env.LAUNCHPOD_WORKSPACE = workspace;
1565
- env.XDG_CONFIG_HOME = (0, import_node_path6.join)(home, ".config");
1566
- env.XDG_DATA_HOME = (0, import_node_path6.join)(home, ".local", "share");
1567
- env.XDG_CACHE_HOME = (0, import_node_path6.join)(home, ".cache");
1568
- env.GIT_CONFIG_GLOBAL = (0, import_node_path6.join)(home, ".gitconfig");
1569
- env.GH_CONFIG_DIR = (0, import_node_path6.join)(home, ".config", "gh");
1786
+ env.XDG_CONFIG_HOME = (0, import_node_path7.join)(home, ".config");
1787
+ env.XDG_DATA_HOME = (0, import_node_path7.join)(home, ".local", "share");
1788
+ env.XDG_CACHE_HOME = (0, import_node_path7.join)(home, ".cache");
1789
+ env.GIT_CONFIG_GLOBAL = (0, import_node_path7.join)(home, ".gitconfig");
1790
+ env.GH_CONFIG_DIR = (0, import_node_path7.join)(home, ".config", "gh");
1570
1791
  env.LAUNCHPOD_RUNTIME = "process";
1571
1792
  env.LAUNCHPOD_NO_PARK = "1";
1572
1793
  if (spec.env && typeof spec.env === "object") {
@@ -1582,21 +1803,21 @@ function buildPodEnv(state, spec, root, services) {
1582
1803
  return env;
1583
1804
  }
1584
1805
  function servicesToEntries(services) {
1585
- return services.map((s) => ({ name: s.name, port: s.port, bin: s.bin, args: s.args }));
1806
+ return services.map((s) => ({ name: s.name, port: s.port, bin: s.bin, args: s.args, skipSpawn: s.skipSpawn }));
1586
1807
  }
1587
1808
  function ensurePodDirs(root) {
1588
- const workspace = (0, import_node_path6.join)(root, "workspace");
1589
- const home = (0, import_node_path6.join)(root, "home");
1590
- (0, import_node_fs6.mkdirSync)(root, { recursive: true, mode: 448 });
1591
- (0, import_node_fs6.mkdirSync)(workspace, { recursive: true, mode: 448 });
1592
- (0, import_node_fs6.mkdirSync)((0, import_node_path6.join)(home, ".claude"), { recursive: true, mode: 448 });
1593
- (0, import_node_fs6.mkdirSync)((0, import_node_path6.join)(home, ".config", "gh"), { recursive: true, mode: 448 });
1594
- (0, import_node_fs6.mkdirSync)((0, import_node_path6.join)(home, ".local", "share"), { recursive: true, mode: 448 });
1595
- (0, import_node_fs6.mkdirSync)((0, import_node_path6.join)(home, ".cache"), { recursive: true, mode: 448 });
1809
+ const workspace = (0, import_node_path7.join)(root, "workspace");
1810
+ const home = (0, import_node_path7.join)(root, "home");
1811
+ (0, import_node_fs7.mkdirSync)(root, { recursive: true, mode: 448 });
1812
+ (0, import_node_fs7.mkdirSync)(workspace, { recursive: true, mode: 448 });
1813
+ (0, import_node_fs7.mkdirSync)((0, import_node_path7.join)(home, ".claude"), { recursive: true, mode: 448 });
1814
+ (0, import_node_fs7.mkdirSync)((0, import_node_path7.join)(home, ".config", "gh"), { recursive: true, mode: 448 });
1815
+ (0, import_node_fs7.mkdirSync)((0, import_node_path7.join)(home, ".local", "share"), { recursive: true, mode: 448 });
1816
+ (0, import_node_fs7.mkdirSync)((0, import_node_path7.join)(home, ".cache"), { recursive: true, mode: 448 });
1596
1817
  return { workspace, home };
1597
1818
  }
1598
1819
  function spawnPod(workspace, env, logFile, projectSlug) {
1599
- const logFd = (0, import_node_fs6.openSync)(logFile, "a", 384);
1820
+ const logFd = (0, import_node_fs7.openSync)(logFile, "a", 384);
1600
1821
  let child;
1601
1822
  try {
1602
1823
  child = (0, import_node_child_process3.spawn)(POD_ENTRY_BIN, [POD_ENTRY_ARG], {
@@ -1607,7 +1828,7 @@ function spawnPod(workspace, env, logFile, projectSlug) {
1607
1828
  stdio: ["ignore", logFd, logFd]
1608
1829
  });
1609
1830
  } finally {
1610
- (0, import_node_fs6.closeSync)(logFd);
1831
+ (0, import_node_fs7.closeSync)(logFd);
1611
1832
  }
1612
1833
  if (typeof child.pid !== "number") {
1613
1834
  throw new PodError(`failed to spawn pod process for "${projectSlug}"`, 500);
@@ -1615,12 +1836,12 @@ function spawnPod(workspace, env, logFile, projectSlug) {
1615
1836
  child.unref();
1616
1837
  return child.pid;
1617
1838
  }
1618
- function reconstructUrls(projectSlug, services, cfBaseDomain, localOnly) {
1839
+ function reconstructUrls(projectSlug, services, roverId, cfBaseDomain, localOnly) {
1619
1840
  const urls = {};
1620
1841
  if (localOnly || !cfBaseDomain) return urls;
1621
1842
  for (const s of services) {
1622
1843
  if (s.skipSpawn) continue;
1623
- const u = serviceUrl(projectSlug, s.name, cfBaseDomain);
1844
+ const u = serviceUrl(projectSlug, s.name, roverId, cfBaseDomain);
1624
1845
  if (u) urls[s.name] = u;
1625
1846
  }
1626
1847
  return urls;
@@ -1632,7 +1853,7 @@ async function create2(state, body) {
1632
1853
  await signalGroupDown(prior, "SIGTERM");
1633
1854
  }
1634
1855
  const slot = assignSlot(body.projectSlug);
1635
- const root = (0, import_node_path6.join)(podsRoot(), body.projectSlug);
1856
+ const root = (0, import_node_path7.join)(podsRoot(), body.projectSlug);
1636
1857
  const { workspace } = ensurePodDirs(root);
1637
1858
  let baseServices;
1638
1859
  try {
@@ -1645,12 +1866,12 @@ async function create2(state, body) {
1645
1866
  const services = assignPortsForSlot(baseServices, slot);
1646
1867
  const env = buildPodEnv(state, body, root, services);
1647
1868
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1648
- const logFile = (0, import_node_path6.join)(root, "pod.log");
1869
+ const logFile = (0, import_node_path7.join)(root, "pod.log");
1649
1870
  const pid = spawnPod(workspace, env, logFile, body.projectSlug);
1650
1871
  const ports = {};
1651
1872
  for (const s of services) if (!s.skipSpawn) ports[s.name] = s.port;
1652
1873
  const localOnly = body.env?.LAUNCHKIT_LOCAL_ONLY === "1";
1653
- const urls = reconstructUrls(body.projectSlug, services, body.cfBaseDomain, localOnly);
1874
+ const urls = reconstructUrls(body.projectSlug, services, state.roverId, body.cfBaseDomain, localOnly);
1654
1875
  const record = {
1655
1876
  projectSlug: body.projectSlug,
1656
1877
  podId: body.podId ?? null,
@@ -1661,9 +1882,13 @@ async function create2(state, body) {
1661
1882
  ports,
1662
1883
  urls,
1663
1884
  // Kept so `start` can respawn the same service set + ingress without the
1664
- // original up request (creds come from the pod's own on-disk store).
1885
+ // original up request (blob creds come from the pod's own on-disk store).
1665
1886
  services: baseServices.map((s) => s.name),
1666
1887
  cfBaseDomain: body.cfBaseDomain,
1888
+ // Token mode writes nothing to disk, so the long-lived token is kept here to
1889
+ // re-inject on respawn. Undefined (omitted) in blob mode. registry.json is
1890
+ // 0o600 — same protection blob mode gives the on-disk credentials file.
1891
+ claudeOauthToken: body.claudeOauthToken,
1667
1892
  stopped: false,
1668
1893
  root,
1669
1894
  logFile,
@@ -1700,7 +1925,7 @@ async function tear2(_state, body) {
1700
1925
  let volumesRemoved = false;
1701
1926
  if (body.purge) {
1702
1927
  try {
1703
- (0, import_node_fs6.rmSync)(rec.root, { recursive: true, force: true });
1928
+ (0, import_node_fs7.rmSync)(rec.root, { recursive: true, force: true });
1704
1929
  volumesRemoved = true;
1705
1930
  } catch (err) {
1706
1931
  console.warn(`[rover] purge of process pod folder "${rec.root}" failed: ${err instanceof Error ? err.message : String(err)}`);
@@ -1726,7 +1951,12 @@ async function start2(state, body) {
1726
1951
  }
1727
1952
  const services = assignPortsForSlot(baseServices, rec.slot);
1728
1953
  const { workspace } = ensurePodDirs(rec.root);
1729
- const env = buildPodEnv(state, { projectSlug: rec.projectSlug, cfBaseDomain: rec.cfBaseDomain }, rec.root, services);
1954
+ const env = buildPodEnv(
1955
+ state,
1956
+ { projectSlug: rec.projectSlug, cfBaseDomain: rec.cfBaseDomain, claudeOauthToken: rec.claudeOauthToken },
1957
+ rec.root,
1958
+ services
1959
+ );
1730
1960
  const pid = spawnPod(workspace, env, rec.logFile, rec.projectSlug);
1731
1961
  upsert({ ...rec, pid, pgid: pid, stopped: false });
1732
1962
  console.log(`[rover] process pod "${body.projectSlug}" resumed \u2014 pid ${pid}, slot ${rec.slot}`);
@@ -1792,13 +2022,13 @@ async function signalGroupDown(rec, signal) {
1792
2022
  function sleep2(ms) {
1793
2023
  return new Promise((resolve) => setTimeout(resolve, ms));
1794
2024
  }
1795
- var import_node_child_process3, import_node_fs6, import_node_path6, POD_ENTRY_BIN, POD_ENTRY_ARG, DENIED_ENV, BASE_ENV_PASSTHROUGH, processRuntime;
2025
+ var import_node_child_process3, import_node_fs7, import_node_path7, POD_ENTRY_BIN, POD_ENTRY_ARG, DENIED_ENV, BASE_ENV_PASSTHROUGH, processRuntime;
1796
2026
  var init_process = __esm({
1797
2027
  "src/server/rover/runtime/process.ts"() {
1798
2028
  "use strict";
1799
2029
  import_node_child_process3 = require("node:child_process");
1800
- import_node_fs6 = require("node:fs");
1801
- import_node_path6 = require("node:path");
2030
+ import_node_fs7 = require("node:fs");
2031
+ import_node_path7 = require("node:path");
1802
2032
  init_launch_kit_services();
1803
2033
  init_hostnames();
1804
2034
  init_ports();
@@ -1893,25 +2123,25 @@ __export(daemon_exports, {
1893
2123
  startDaemon: () => startDaemon
1894
2124
  });
1895
2125
  function lockPath() {
1896
- return (0, import_node_path7.join)((0, import_node_os4.homedir)(), LAUNCHSECURE_DIR, ROVER_LOCK_FILENAME);
2126
+ return (0, import_node_path8.join)((0, import_node_os4.homedir)(), LAUNCHSECURE_DIR, ROVER_LOCK_FILENAME);
1897
2127
  }
1898
2128
  function readRoverLock() {
1899
2129
  const p = lockPath();
1900
- if (!(0, import_node_fs7.existsSync)(p)) return null;
2130
+ if (!(0, import_node_fs8.existsSync)(p)) return null;
1901
2131
  try {
1902
- return JSON.parse((0, import_node_fs7.readFileSync)(p, "utf-8"));
2132
+ return JSON.parse((0, import_node_fs8.readFileSync)(p, "utf-8"));
1903
2133
  } catch {
1904
2134
  return null;
1905
2135
  }
1906
2136
  }
1907
2137
  function writeLock(lock) {
1908
- const dir = (0, import_node_path7.join)((0, import_node_os4.homedir)(), LAUNCHSECURE_DIR);
1909
- (0, import_node_fs7.mkdirSync)(dir, { recursive: true });
1910
- (0, import_node_fs7.writeFileSync)(lockPath(), JSON.stringify(lock, null, 2) + "\n", "utf-8");
2138
+ const dir = (0, import_node_path8.join)((0, import_node_os4.homedir)(), LAUNCHSECURE_DIR);
2139
+ (0, import_node_fs8.mkdirSync)(dir, { recursive: true });
2140
+ (0, import_node_fs8.writeFileSync)(lockPath(), JSON.stringify(lock, null, 2) + "\n", "utf-8");
1911
2141
  }
1912
2142
  function clearLock() {
1913
2143
  try {
1914
- (0, import_node_fs7.unlinkSync)(lockPath());
2144
+ (0, import_node_fs8.unlinkSync)(lockPath());
1915
2145
  } catch {
1916
2146
  }
1917
2147
  }
@@ -1981,6 +2211,9 @@ async function handleRequest(req, res, state) {
1981
2211
  if (method === "GET" && path === "/health") {
1982
2212
  return json(res, 200, { ok: true, roverId: state.roverId, orgSlug: state.orgSlug });
1983
2213
  }
2214
+ if (method === "GET" && path === "/self/status") {
2215
+ return json(res, 200, { roverId: state.roverId, kitVersion: KIT_VERSION, podImage: POD_IMAGE });
2216
+ }
1984
2217
  if (method === "GET" && (path === "/" || path === "/pods" || path === "/ui")) {
1985
2218
  try {
1986
2219
  const { pods } = await podsList(state);
@@ -2031,6 +2264,47 @@ async function handleRequest(req, res, state) {
2031
2264
  return json(res, 500, { error: err instanceof Error ? err.message : "internal_error" });
2032
2265
  }
2033
2266
  }
2267
+ if (method === "POST" && path === "/self/update") {
2268
+ const rawBody = await readBody(req);
2269
+ const sigHeader = req.headers["x-ls-signature"];
2270
+ const sigStr = Array.isArray(sigHeader) ? sigHeader[0] : sigHeader;
2271
+ const failure = verifySignature(state.secret, rawBody, sigStr);
2272
+ if (failure) {
2273
+ console.warn(`[rover] POST /self/update \u2192 401 (${failure})`);
2274
+ return json(res, 401, { error: "invalid_signature" });
2275
+ }
2276
+ let targetVersion;
2277
+ if (rawBody.length > 0) {
2278
+ try {
2279
+ targetVersion = JSON.parse(rawBody).targetVersion;
2280
+ } catch {
2281
+ return json(res, 400, { error: "malformed_json" });
2282
+ }
2283
+ }
2284
+ if (!targetVersion || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(targetVersion)) {
2285
+ return json(res, 400, { error: "invalid_target_version", detail: "expected a concrete semver, e.g. 0.0.45" });
2286
+ }
2287
+ if (targetVersion === KIT_VERSION) {
2288
+ return json(res, 200, { ok: true, updated: false, reason: "already_on_target", kitVersion: KIT_VERSION });
2289
+ }
2290
+ json(res, 200, { ok: true, updating: true, from: KIT_VERSION, to: targetVersion });
2291
+ const argv0 = process.argv[1] ?? "launch-rover";
2292
+ const child = (0, import_node_child_process4.spawn)(
2293
+ process.execPath,
2294
+ [argv0, "rover-update", `--rover=${state.roverId}`, `--version=${targetVersion}`, `--pod-image=${POD_IMAGE}`],
2295
+ { detached: true, stdio: "ignore", env: process.env }
2296
+ );
2297
+ child.unref();
2298
+ console.log(`[rover] /self/update \u2014 spawned updater (pid ${child.pid}) ${KIT_VERSION} \u2192 ${targetVersion}; exiting for re-exec`);
2299
+ setTimeout(() => {
2300
+ try {
2301
+ process.kill(process.pid, "SIGTERM");
2302
+ } catch {
2303
+ process.exit(0);
2304
+ }
2305
+ }, 250).unref();
2306
+ return;
2307
+ }
2034
2308
  if (method === "POST" && path === "/shutdown") {
2035
2309
  const rawBody = await readBody(req);
2036
2310
  const sigHeader = req.headers["x-ls-signature"];
@@ -2078,7 +2352,7 @@ function readBody(req) {
2078
2352
  });
2079
2353
  }
2080
2354
  async function sendHeartbeat(state) {
2081
- const payload = JSON.stringify({ roverId: state.roverId });
2355
+ const payload = JSON.stringify({ roverId: state.roverId, kitVersion: KIT_VERSION, podImage: POD_IMAGE });
2082
2356
  const { header } = buildSignatureHeader(state.secret, payload);
2083
2357
  const url = new URL("/api/rover/heartbeat", state.serverUrl);
2084
2358
  await postJson(url, payload, { "X-LS-Signature": header });
@@ -2117,21 +2391,24 @@ function postJson(url, body, extra) {
2117
2391
  req.end();
2118
2392
  });
2119
2393
  }
2120
- var import_node_fs7, import_node_http2, import_node_http3, import_node_https2, import_node_os4, import_node_path7, ROVER_LOCK_FILENAME;
2394
+ var import_node_child_process4, import_node_fs8, import_node_http2, import_node_http3, import_node_https2, import_node_os4, import_node_path8, KIT_VERSION, POD_IMAGE, ROVER_LOCK_FILENAME;
2121
2395
  var init_daemon = __esm({
2122
2396
  "src/server/rover/daemon.ts"() {
2123
2397
  "use strict";
2124
- import_node_fs7 = require("node:fs");
2398
+ import_node_child_process4 = require("node:child_process");
2399
+ import_node_fs8 = require("node:fs");
2125
2400
  import_node_http2 = require("node:http");
2126
2401
  import_node_http3 = require("node:http");
2127
2402
  import_node_https2 = require("node:https");
2128
2403
  import_node_os4 = require("node:os");
2129
- import_node_path7 = require("node:path");
2404
+ import_node_path8 = require("node:path");
2130
2405
  init_launch_kit_paths();
2131
2406
  init_dashboard();
2132
2407
  init_hmac();
2133
2408
  init_pods();
2134
2409
  init_state();
2410
+ KIT_VERSION = require_package().version;
2411
+ POD_IMAGE = process.env.LAUNCHKIT_POD_IMAGE ?? "ghcr.io/launchsecure/launch-pod:latest";
2135
2412
  ROVER_LOCK_FILENAME = "launch-rover.lock";
2136
2413
  }
2137
2414
  });
@@ -20894,11 +21171,11 @@ function createTunnel(opts) {
20894
21171
  const exhaustive = opts.provider;
20895
21172
  throw new Error(`[tunnel] unknown provider: ${String(exhaustive)}`);
20896
21173
  }
20897
- var import_node_fs8, import_node_events, import_promises3, import_undici, import_cloudflared, dnsResolver, dnsCache, dnsResilientDispatcher, BACKOFF_MIN_MS, BACKOFF_MAX_MS, SELFTEST_INTERVAL_MS, SELFTEST_TIMEOUT_MS, NAMED_FATAL_PATTERNS, CloudflaredTunnel;
21174
+ var import_node_fs9, import_node_events, import_promises3, import_undici, import_cloudflared, dnsResolver, dnsCache, dnsResilientDispatcher, BACKOFF_MIN_MS, BACKOFF_MAX_MS, SELFTEST_INTERVAL_MS, SELFTEST_TIMEOUT_MS, NAMED_FATAL_PATTERNS, CloudflaredTunnel;
20898
21175
  var init_tunnel = __esm({
20899
21176
  "src/server/tunnel/index.ts"() {
20900
21177
  "use strict";
20901
- import_node_fs8 = require("node:fs");
21178
+ import_node_fs9 = require("node:fs");
20902
21179
  import_node_events = require("node:events");
20903
21180
  import_promises3 = require("node:dns/promises");
20904
21181
  init_source();
@@ -20946,7 +21223,7 @@ var init_tunnel = __esm({
20946
21223
  }
20947
21224
  async start() {
20948
21225
  if (this.tunnel) return;
20949
- if (!(0, import_node_fs8.existsSync)(import_cloudflared.bin)) {
21226
+ if (!(0, import_node_fs9.existsSync)(import_cloudflared.bin)) {
20950
21227
  console.log("[tunnel] downloading cloudflared binary (one-time setup)\u2026");
20951
21228
  try {
20952
21229
  await (0, import_cloudflared.install)(import_cloudflared.bin);
@@ -21213,9 +21490,9 @@ async function runSetup(argv) {
21213
21490
  }
21214
21491
  const zone = await resolveZone(lsConfig.cfApiToken, lsConfig.cfDomainName);
21215
21492
  console.log(`[rover] resolved CF zone: ${zone.name} (account ${zone.accountId})`);
21216
- const stateDir2 = (0, import_node_path8.join)((0, import_node_os6.homedir)(), LAUNCHSECURE_DIR, "rover");
21217
- (0, import_node_fs9.mkdirSync)(stateDir2, { recursive: true });
21218
- const tunnelStateFile = (0, import_node_path8.join)(stateDir2, `${roverId}.tunnel.json`);
21493
+ const stateDir2 = (0, import_node_path9.join)((0, import_node_os6.homedir)(), LAUNCHSECURE_DIR, "rover");
21494
+ (0, import_node_fs10.mkdirSync)(stateDir2, { recursive: true });
21495
+ const tunnelStateFile = (0, import_node_path9.join)(stateDir2, `${roverId}.tunnel.json`);
21219
21496
  const idSuffix = roverId.replace(/[^a-z0-9]/gi, "").toLowerCase().slice(-8);
21220
21497
  const subdomain = `rover-${args.orgSlug.replace(/[^a-z0-9-]/gi, "-").toLowerCase().slice(0, 30)}-${idSuffix}`;
21221
21498
  const tunnelName = `launch-rover-${roverId}`;
@@ -21248,7 +21525,7 @@ async function runSetup(argv) {
21248
21525
  platform: (0, import_node_os6.platform)(),
21249
21526
  hostname: args.name ?? (0, import_node_os6.hostname)(),
21250
21527
  dockerVersion,
21251
- kitVersion: KIT_VERSION,
21528
+ kitVersion: KIT_VERSION2,
21252
21529
  // The actual pod backend this host runs — lets LS show the reported
21253
21530
  // runtime and flag drift from the configured `defaultRuntime`.
21254
21531
  runtime: args.runtime
@@ -21258,6 +21535,14 @@ async function runSetup(argv) {
21258
21535
  console.log(
21259
21536
  `[rover] LS state: ${outcome.approvalStatus}${outcome.reused ? " (reused existing secret)" : " (new secret minted)"}`
21260
21537
  );
21538
+ await gateDashboard({
21539
+ apiToken: lsConfig.cfApiToken,
21540
+ accountId: zone.accountId,
21541
+ issuer: args.serverUrl,
21542
+ hostname,
21543
+ pat: args.pat,
21544
+ stateFile: (0, import_node_path9.join)(stateDir2, `${roverId}.access.json`)
21545
+ });
21261
21546
  await launchDaemon(args, ingress, outcome.state);
21262
21547
  console.log("");
21263
21548
  if (outcome.approvalStatus === "pending_approval") {
@@ -21313,7 +21598,7 @@ async function launchDaemon(args, ingress, state) {
21313
21598
  return;
21314
21599
  }
21315
21600
  const argv0 = process.argv[1] ?? "launch-rover";
21316
- const child = (0, import_node_child_process4.spawn)(process.execPath, [argv0, "serve", `--rover=${state.roverId}`], {
21601
+ const child = (0, import_node_child_process5.spawn)(process.execPath, [argv0, "serve", `--rover=${state.roverId}`], {
21317
21602
  detached: true,
21318
21603
  stdio: "ignore",
21319
21604
  env: {
@@ -21325,6 +21610,52 @@ async function launchDaemon(args, ingress, state) {
21325
21610
  child.unref();
21326
21611
  console.log(`[rover] daemon spawned (pid ${child.pid}, detached)`);
21327
21612
  }
21613
+ async function gateDashboard(input) {
21614
+ const services = [
21615
+ // Host-level gate — the read-only dashboard. 24h session (not RCE-bearing,
21616
+ // unlike the bot terminal which the pod entrypoint marks strict).
21617
+ { hostname: input.hostname, strict: false },
21618
+ // Public carve-outs for the machine routes.
21619
+ ...ACCESS_BYPASS_PATHS.map((path) => ({ hostname: input.hostname, path, bypass: true }))
21620
+ ];
21621
+ console.log(`[rover] gating ${input.hostname} behind CF Access (IdP: ${input.issuer})`);
21622
+ try {
21623
+ const result = await provisionAccess({
21624
+ apiToken: input.apiToken,
21625
+ accountId: input.accountId,
21626
+ issuer: input.issuer,
21627
+ services,
21628
+ stateFile: input.stateFile,
21629
+ registerClient: (redirectUris) => registerOidcClient(input.issuer, input.pat, redirectUris)
21630
+ });
21631
+ console.log(`[rover] CF Access gate live \u2014 IdP ${result.idpId}, auth domain ${result.authDomain}`);
21632
+ } catch (err) {
21633
+ const msg = err instanceof Error ? err.message : String(err);
21634
+ throw new Error(
21635
+ `CF Access gating failed: ${msg}
21636
+ The dashboard would be publicly reachable without it, so setup is aborting before the tunnel goes live.
21637
+ Most common cause: the rover's Cloudflare token lacks Access scopes (Access: Apps and Policies + Account: Access Organizations, both Edit).
21638
+ Re-mint the token from LS \u2192 Organization \u2192 Integrations \u2192 Cloudflare (the "Create token" link pre-selects them), update the rover, and re-run setup.`
21639
+ );
21640
+ }
21641
+ }
21642
+ async function registerOidcClient(serverUrl, pat, redirectUris) {
21643
+ const res = await fetch(new URL("/api/rover/oidc-client", serverUrl), {
21644
+ method: "POST",
21645
+ headers: {
21646
+ Authorization: `Bearer ${pat}`,
21647
+ "Content-Type": "application/json",
21648
+ Accept: "application/json"
21649
+ },
21650
+ body: JSON.stringify({ redirectUris }),
21651
+ signal: AbortSignal.timeout(15e3)
21652
+ });
21653
+ const body = await res.json().catch(() => null);
21654
+ if (!res.ok || !body?.success || !body.data) {
21655
+ throw new Error(`OIDC client provisioning failed (HTTP ${res.status}): ${body?.error ?? "unexpected response"}`);
21656
+ }
21657
+ return body.data;
21658
+ }
21328
21659
  function runSetupReset(roverId) {
21329
21660
  clearRoverState(roverId);
21330
21661
  }
@@ -21336,24 +21667,26 @@ function isPidAlive(pid) {
21336
21667
  return false;
21337
21668
  }
21338
21669
  }
21339
- var import_node_os6, import_node_child_process4, import_node_path8, import_node_fs9, KIT_VERSION, DEFAULT_SERVER, DEFAULT_DAEMON_PORT, MIN_NODE_MAJOR;
21670
+ var import_node_os6, import_node_child_process5, import_node_path9, import_node_fs10, KIT_VERSION2, DEFAULT_SERVER, DEFAULT_DAEMON_PORT, MIN_NODE_MAJOR, ACCESS_BYPASS_PATHS;
21340
21671
  var init_setup = __esm({
21341
21672
  "src/server/rover/setup.ts"() {
21342
21673
  "use strict";
21343
21674
  import_node_os6 = require("node:os");
21344
- import_node_child_process4 = require("node:child_process");
21345
- import_node_path8 = require("node:path");
21346
- import_node_fs9 = require("node:fs");
21675
+ import_node_child_process5 = require("node:child_process");
21676
+ import_node_path9 = require("node:path");
21677
+ import_node_fs10 = require("node:fs");
21678
+ init_cf_access();
21347
21679
  init_cf_ingress();
21348
21680
  init_launch_kit_paths();
21349
21681
  init_docker_install();
21350
21682
  init_mcp_client();
21351
21683
  init_registration();
21352
21684
  init_state();
21353
- KIT_VERSION = require_package().version;
21685
+ KIT_VERSION2 = require_package().version;
21354
21686
  DEFAULT_SERVER = "https://launchsecure-v2.vercel.app";
21355
21687
  DEFAULT_DAEMON_PORT = 52749;
21356
21688
  MIN_NODE_MAJOR = 18;
21689
+ ACCESS_BYPASS_PATHS = ["/health", "/pods/up", "/pods/down", "/pods/start", "/pods/list", "/shutdown"];
21357
21690
  }
21358
21691
  });
21359
21692
 
@@ -21418,15 +21751,15 @@ async function runTeardown(argv) {
21418
21751
  }
21419
21752
  }
21420
21753
  clearRoverState(parsed.roverId);
21421
- const tunnelStateFile = (0, import_node_path9.join)(
21754
+ const tunnelStateFile = (0, import_node_path10.join)(
21422
21755
  (0, import_node_os7.homedir)(),
21423
21756
  LAUNCHSECURE_DIR,
21424
21757
  "rover",
21425
21758
  `${parsed.roverId}.tunnel.json`
21426
21759
  );
21427
- if ((0, import_node_fs10.existsSync)(tunnelStateFile)) {
21760
+ if ((0, import_node_fs11.existsSync)(tunnelStateFile)) {
21428
21761
  try {
21429
- (0, import_node_fs10.unlinkSync)(tunnelStateFile);
21762
+ (0, import_node_fs11.unlinkSync)(tunnelStateFile);
21430
21763
  } catch {
21431
21764
  }
21432
21765
  }
@@ -21477,19 +21810,75 @@ function isPidAlive2(pid) {
21477
21810
  function sleep3(ms) {
21478
21811
  return new Promise((resolve) => setTimeout(resolve, ms));
21479
21812
  }
21480
- var import_node_fs10, import_node_os7, import_node_path9;
21813
+ var import_node_fs11, import_node_os7, import_node_path10;
21481
21814
  var init_teardown = __esm({
21482
21815
  "src/server/rover/teardown.ts"() {
21483
21816
  "use strict";
21484
- import_node_fs10 = require("node:fs");
21817
+ import_node_fs11 = require("node:fs");
21485
21818
  import_node_os7 = require("node:os");
21486
- import_node_path9 = require("node:path");
21819
+ import_node_path10 = require("node:path");
21487
21820
  init_launch_kit_paths();
21488
21821
  init_daemon();
21489
21822
  init_state();
21490
21823
  }
21491
21824
  });
21492
21825
 
21826
+ // src/server/rover/update.ts
21827
+ var update_exports = {};
21828
+ __export(update_exports, {
21829
+ runUpdate: () => runUpdate
21830
+ });
21831
+ function parseArgs3(argv) {
21832
+ const get = (k) => argv.find((a) => a.startsWith(`--${k}=`))?.slice(`--${k}=`.length);
21833
+ const roverId = get("rover");
21834
+ const version = get("version");
21835
+ if (!roverId) throw new Error("rover-update requires --rover=<id>");
21836
+ if (!version || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
21837
+ throw new Error(`rover-update requires --version=<concrete semver> (got "${version ?? ""}")`);
21838
+ }
21839
+ return { roverId, version, podImage: get("pod-image") };
21840
+ }
21841
+ function log(msg) {
21842
+ process.stdout.write(`[rover-update] ${msg}
21843
+ `);
21844
+ }
21845
+ async function runUpdate(argv) {
21846
+ const args = parseArgs3(argv);
21847
+ log(`updating ${KIT_PACKAGE} \u2192 ${args.version} for rover ${args.roverId}`);
21848
+ const install2 = (0, import_node_child_process6.spawnSync)("npm", ["install", "-g", `${KIT_PACKAGE}@${args.version}`], {
21849
+ stdio: "inherit",
21850
+ env: process.env
21851
+ });
21852
+ if (install2.status !== 0) {
21853
+ log(`\u2717 npm install failed (status ${install2.status ?? "unknown"}); re-launching the existing version`);
21854
+ } else {
21855
+ log(`\u2713 installed ${KIT_PACKAGE}@${args.version}`);
21856
+ }
21857
+ if (args.podImage) {
21858
+ const pull = (0, import_node_child_process6.spawnSync)("docker", ["pull", args.podImage], { stdio: "inherit", env: process.env });
21859
+ if (pull.status === 0) {
21860
+ log(`\u2713 pulled ${args.podImage}`);
21861
+ } else {
21862
+ log(`\u26A0 docker pull skipped/failed (status ${pull.status ?? "unknown"}) \u2014 pod will pull on next up`);
21863
+ }
21864
+ }
21865
+ const child = (0, import_node_child_process6.spawn)("launch-rover", ["serve", `--rover=${args.roverId}`], {
21866
+ detached: true,
21867
+ stdio: "ignore",
21868
+ env: process.env
21869
+ });
21870
+ child.unref();
21871
+ log(`re-launched daemon (pid ${child.pid}); updater exiting`);
21872
+ }
21873
+ var import_node_child_process6, KIT_PACKAGE;
21874
+ var init_update = __esm({
21875
+ "src/server/rover/update.ts"() {
21876
+ "use strict";
21877
+ import_node_child_process6 = require("node:child_process");
21878
+ KIT_PACKAGE = "@launchsecure/launch-kit";
21879
+ }
21880
+ });
21881
+
21493
21882
  // src/server/rover-entry.ts
21494
21883
  function logStderr(msg) {
21495
21884
  process.stderr.write(`[launch-rover] ${msg}
@@ -21552,6 +21941,11 @@ async function main() {
21552
21941
  runStatus2(argv.slice(1));
21553
21942
  return;
21554
21943
  }
21944
+ case "rover-update": {
21945
+ const { runUpdate: runUpdate2 } = await Promise.resolve().then(() => (init_update(), update_exports));
21946
+ await runUpdate2(argv.slice(1));
21947
+ return;
21948
+ }
21555
21949
  case "teardown": {
21556
21950
  const { runTeardown: runTeardown2 } = await Promise.resolve().then(() => (init_teardown(), teardown_exports));
21557
21951
  await runTeardown2(argv.slice(1));