@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.
- package/dist/deck-client/assets/{_baseUniq-C6w7kg8x.js → _baseUniq-9540Lrb7.js} +1 -1
- package/dist/deck-client/assets/{arc-Cx9pT3Nn.js → arc-2FFU5_0l.js} +1 -1
- package/dist/deck-client/assets/{architectureDiagram-Q4EWVU46-BITSj3vA.js → architectureDiagram-Q4EWVU46-CV2e3n5Z.js} +1 -1
- package/dist/deck-client/assets/{blockDiagram-DXYQGD6D-BehOFuwh.js → blockDiagram-DXYQGD6D-B9JFwjAL.js} +1 -1
- package/dist/deck-client/assets/{c4Diagram-AHTNJAMY-BZTYM4na.js → c4Diagram-AHTNJAMY-C0Gwco04.js} +1 -1
- package/dist/deck-client/assets/channel-DUo1BfyB.js +1 -0
- package/dist/deck-client/assets/{chunk-4BX2VUAB-CCUx5CTd.js → chunk-4BX2VUAB-pnGex62D.js} +1 -1
- package/dist/deck-client/assets/{chunk-4TB4RGXK-UDZXXga6.js → chunk-4TB4RGXK-BRfs5enT.js} +1 -1
- package/dist/deck-client/assets/{chunk-55IACEB6-CfcU6PIW.js → chunk-55IACEB6-DV2sc7BN.js} +1 -1
- package/dist/deck-client/assets/{chunk-EDXVE4YY-BK6F5Fof.js → chunk-EDXVE4YY-BH8QD8Jn.js} +1 -1
- package/dist/deck-client/assets/{chunk-FMBD7UC4-C-2idlFB.js → chunk-FMBD7UC4-BVuRSGoP.js} +1 -1
- package/dist/deck-client/assets/{chunk-OYMX7WX6-D6hBkYLP.js → chunk-OYMX7WX6-DBBVRR48.js} +1 -1
- package/dist/deck-client/assets/{chunk-QZHKN3VN-DixNpysA.js → chunk-QZHKN3VN-DNLfqlpV.js} +1 -1
- package/dist/deck-client/assets/{chunk-YZCP3GAM-Cd3pNBtQ.js → chunk-YZCP3GAM-w7-OIPaD.js} +1 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-sHUWMvyj.js +1 -0
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-sHUWMvyj.js +1 -0
- package/dist/deck-client/assets/clone-pfbkP49m.js +1 -0
- package/dist/deck-client/assets/{cose-bilkent-S5V4N54A-OF3JWdEt.js → cose-bilkent-S5V4N54A-D8kmXu30.js} +1 -1
- package/dist/deck-client/assets/{dagre-KV5264BT-Bqu-qcv4.js → dagre-KV5264BT--b1FD3_Y.js} +1 -1
- package/dist/deck-client/assets/{diagram-5BDNPKRD--0eHmUBS.js → diagram-5BDNPKRD-DKPVzUtl.js} +1 -1
- package/dist/deck-client/assets/{diagram-G4DWMVQ6-nss6oL20.js → diagram-G4DWMVQ6-DYSdoCus.js} +1 -1
- package/dist/deck-client/assets/{diagram-MMDJMWI5-D_gSGnLR.js → diagram-MMDJMWI5-DmqAI88z.js} +1 -1
- package/dist/deck-client/assets/{diagram-TYMM5635-BIt-P6Pk.js → diagram-TYMM5635-Dbt6BCnF.js} +1 -1
- package/dist/deck-client/assets/{erDiagram-SMLLAGMA-Bi-E4KQm.js → erDiagram-SMLLAGMA-C7Kfi12r.js} +1 -1
- package/dist/deck-client/assets/{flowDiagram-DWJPFMVM-DMJCvLMA.js → flowDiagram-DWJPFMVM-h1nKPzIv.js} +1 -1
- package/dist/deck-client/assets/{ganttDiagram-T4ZO3ILL-C3xgEoPD.js → ganttDiagram-T4ZO3ILL-40rX1Tln.js} +1 -1
- package/dist/deck-client/assets/{gitGraphDiagram-UUTBAWPF-CD0BEGAW.js → gitGraphDiagram-UUTBAWPF-CtvJWtdg.js} +1 -1
- package/dist/deck-client/assets/{graph-Dtsd9Jwe.js → graph-ByiwozwM.js} +1 -1
- package/dist/deck-client/assets/{index-TFX8vtTG.js → index-CGG2xGJc.js} +1 -1
- package/dist/deck-client/assets/{infoDiagram-42DDH7IO-7IcQYqe_.js → infoDiagram-42DDH7IO-CrGXAAvG.js} +1 -1
- package/dist/deck-client/assets/{ishikawaDiagram-UXIWVN3A-DsCEbx3u.js → ishikawaDiagram-UXIWVN3A-csBJinUG.js} +1 -1
- package/dist/deck-client/assets/{journeyDiagram-VCZTEJTY-1mP2JwCk.js → journeyDiagram-VCZTEJTY-B2dZRDOR.js} +1 -1
- package/dist/deck-client/assets/{kanban-definition-6JOO6SKY-vT0Xrqh9.js → kanban-definition-6JOO6SKY-Dyznt2QN.js} +1 -1
- package/dist/deck-client/assets/{layout-Cw4rS2pn.js → layout-tvNTD61-.js} +1 -1
- package/dist/deck-client/assets/{linear-CzOjL-Ih.js → linear-BTcLgDqZ.js} +1 -1
- package/dist/deck-client/assets/{mermaid.core-DYi3A-qK.js → mermaid.core-CZoq2C4e.js} +4 -4
- package/dist/deck-client/assets/{min-DstloRoL.js → min-J7Zsly_t.js} +1 -1
- package/dist/deck-client/assets/{mindmap-definition-QFDTVHPH-D-cCX2d2.js → mindmap-definition-QFDTVHPH-Ba-GyprJ.js} +1 -1
- package/dist/deck-client/assets/{pieDiagram-DEJITSTG-BqW2NTmy.js → pieDiagram-DEJITSTG-BMLzzK64.js} +1 -1
- package/dist/deck-client/assets/{quadrantDiagram-34T5L4WZ-DbJoWA8f.js → quadrantDiagram-34T5L4WZ-Dh_Ut0Yp.js} +1 -1
- package/dist/deck-client/assets/{requirementDiagram-MS252O5E-DQrUiz_d.js → requirementDiagram-MS252O5E-Dkva9qg0.js} +1 -1
- package/dist/deck-client/assets/{sankeyDiagram-XADWPNL6-kB7PZc3g.js → sankeyDiagram-XADWPNL6-3lcCBbua.js} +1 -1
- package/dist/deck-client/assets/{sequenceDiagram-FGHM5R23-CpyVu1TN.js → sequenceDiagram-FGHM5R23-MH0H8qIE.js} +1 -1
- package/dist/deck-client/assets/{stateDiagram-FHFEXIEX-CjqQcnty.js → stateDiagram-FHFEXIEX-ByLZbwGe.js} +1 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-C87YsgaN.js +1 -0
- package/dist/deck-client/assets/{timeline-definition-GMOUNBTQ-B2PAO9bk.js → timeline-definition-GMOUNBTQ-COT8qN-f.js} +1 -1
- package/dist/deck-client/assets/{vennDiagram-DHZGUBPP-C0G3ItCr.js → vennDiagram-DHZGUBPP-YuKnKVX_.js} +1 -1
- package/dist/deck-client/assets/wardley-RL74JXVD-C4YfwXTm.js +162 -0
- package/dist/deck-client/assets/{wardleyDiagram-NUSXRM2D-B-qtbNZe.js → wardleyDiagram-NUSXRM2D-BuLtRVnC.js} +1 -1
- package/dist/deck-client/assets/{xychartDiagram-5P7HB3ND-41kcBoBE.js → xychartDiagram-5P7HB3ND-CcIc1c6H.js} +1 -1
- package/dist/deck-client/index.html +1 -1
- package/dist/server/init-entry.js +43 -9
- package/dist/server/radar-docker-init-entry.js +37 -9
- package/dist/server/rover-entry.js +525 -131
- package/package.json +1 -1
- package/scaffolds/ls-marketplace/plugins/kit/skills/ship/SKILL.md +24 -18
- package/dist/deck-client/assets/channel-Cw2WDt9a.js +0 -1
- package/dist/deck-client/assets/classDiagram-6PBFFD2Q-JLUXVCUr.js +0 -1
- package/dist/deck-client/assets/classDiagram-v2-HSJHXN6E-JLUXVCUr.js +0 -1
- package/dist/deck-client/assets/clone-H0XCnSb6.js +0 -1
- package/dist/deck-client/assets/stateDiagram-v2-QKLJ7IA2-tfMSn8xx.js +0 -1
- 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
|
|
41
|
-
const res = await fetch(`${
|
|
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
|
|
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
|
|
76
|
-
if (!(0,
|
|
264
|
+
function loadState2(path) {
|
|
265
|
+
if (!(0, import_node_fs2.existsSync)(path)) return null;
|
|
77
266
|
try {
|
|
78
|
-
const parsed = JSON.parse((0,
|
|
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
|
|
88
|
-
const dir = (0,
|
|
89
|
-
if (!(0,
|
|
90
|
-
(0,
|
|
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
|
|
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
|
|
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
|
|
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://
|
|
330
|
+
service: `http://127.0.0.1:${s.port}`
|
|
142
331
|
}));
|
|
143
332
|
ingress.push({ service: "http_status:404" });
|
|
144
|
-
const res = await
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
376
|
+
const prior = loadState2(input.stateFile);
|
|
188
377
|
const tunnelId = await ensureTunnel(input, prior?.tunnelId ?? null);
|
|
189
|
-
|
|
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
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
715
|
+
if (!(0, import_node_fs3.existsSync)(p)) return null;
|
|
527
716
|
try {
|
|
528
|
-
const data = JSON.parse((0,
|
|
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,
|
|
725
|
+
(0, import_node_fs3.mkdirSync)(stateDir(), { recursive: true });
|
|
537
726
|
const final = statePath(state.roverId);
|
|
538
727
|
const tmp = `${final}.tmp`;
|
|
539
|
-
(0,
|
|
540
|
-
(0,
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
751
|
+
import_node_fs3 = require("node:fs");
|
|
563
752
|
import_node_os = require("node:os");
|
|
564
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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,
|
|
1052
|
-
if ((0,
|
|
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,
|
|
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
|
|
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
|
-
|
|
1071
|
-
|
|
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
|
|
1092
|
-
|
|
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
|
-
|
|
1119
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
-
|
|
1637
|
+
import_node_fs5 = require("node:fs");
|
|
1419
1638
|
import_node_os2 = require("node:os");
|
|
1420
|
-
|
|
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,
|
|
1687
|
+
return (0, import_node_path6.join)((0, import_node_os3.homedir)(), LAUNCHSECURE_DIR, "pods");
|
|
1469
1688
|
}
|
|
1470
1689
|
function registryPath() {
|
|
1471
|
-
return (0,
|
|
1690
|
+
return (0, import_node_path6.join)(podsRoot(), "registry.json");
|
|
1472
1691
|
}
|
|
1473
1692
|
function load() {
|
|
1474
1693
|
const p = registryPath();
|
|
1475
|
-
if (!(0,
|
|
1694
|
+
if (!(0, import_node_fs6.existsSync)(p)) return { pods: [] };
|
|
1476
1695
|
try {
|
|
1477
|
-
const doc = JSON.parse((0,
|
|
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,
|
|
1705
|
+
(0, import_node_fs6.mkdirSync)(dir, { recursive: true });
|
|
1487
1706
|
const final = registryPath();
|
|
1488
1707
|
const tmp = `${final}.tmp`;
|
|
1489
|
-
(0,
|
|
1490
|
-
(0,
|
|
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
|
|
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
|
-
|
|
1755
|
+
import_node_fs6 = require("node:fs");
|
|
1537
1756
|
import_node_os3 = require("node:os");
|
|
1538
|
-
|
|
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,
|
|
1550
|
-
const workspace = (0,
|
|
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,
|
|
1566
|
-
env.XDG_DATA_HOME = (0,
|
|
1567
|
-
env.XDG_CACHE_HOME = (0,
|
|
1568
|
-
env.GIT_CONFIG_GLOBAL = (0,
|
|
1569
|
-
env.GH_CONFIG_DIR = (0,
|
|
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,
|
|
1589
|
-
const home = (0,
|
|
1590
|
-
(0,
|
|
1591
|
-
(0,
|
|
1592
|
-
(0,
|
|
1593
|
-
(0,
|
|
1594
|
-
(0,
|
|
1595
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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(
|
|
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,
|
|
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
|
-
|
|
1801
|
-
|
|
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,
|
|
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,
|
|
2130
|
+
if (!(0, import_node_fs8.existsSync)(p)) return null;
|
|
1901
2131
|
try {
|
|
1902
|
-
return JSON.parse((0,
|
|
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,
|
|
1909
|
-
(0,
|
|
1910
|
-
(0,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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,
|
|
21217
|
-
(0,
|
|
21218
|
-
const tunnelStateFile = (0,
|
|
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:
|
|
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,
|
|
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,
|
|
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
|
-
|
|
21345
|
-
|
|
21346
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
21760
|
+
if ((0, import_node_fs11.existsSync)(tunnelStateFile)) {
|
|
21428
21761
|
try {
|
|
21429
|
-
(0,
|
|
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
|
|
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
|
-
|
|
21817
|
+
import_node_fs11 = require("node:fs");
|
|
21485
21818
|
import_node_os7 = require("node:os");
|
|
21486
|
-
|
|
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));
|