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