@launchsecure/launch-kit 0.0.34 → 0.0.36
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/server/cli.js +277 -33
- package/dist/server/init-entry.js +741 -230
- package/dist/server/launch-bot-entry.js +4078 -0
- package/dist/server/orbit-entry.js +969 -136
- package/dist/server/radar-docker-init-entry.js +326 -32
- package/dist/server/rover-entry.js +624 -124
- package/package.json +4 -3
- package/scaffolds/ls-marketplace/plugins/kit/skills/brief/SKILL.md +53 -22
- package/scaffolds/ls-marketplace/plugins/kit/skills/brief/briefs.mjs +152 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/kickoff/SKILL.md +167 -0
- package/scaffolds/ls-marketplace/plugins/kit/skills/orbit/SKILL.md +41 -9
|
@@ -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,17 +153,25 @@ 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
|
-
|
|
156
|
+
// Claude web terminal — exposes a viewable/drivable `claude` session at
|
|
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
|
+
bot: { port: 52849, bin: "launch-bot", args: ["serve"] }
|
|
156
160
|
};
|
|
157
161
|
var DNS_NAME_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
158
162
|
function defaultServices() {
|
|
159
163
|
return [expandShorthand("radar")];
|
|
160
164
|
}
|
|
161
165
|
function expandShorthand(name) {
|
|
166
|
+
if (name === "preview") {
|
|
167
|
+
const raw = process.env.PREVIEW_PORT;
|
|
168
|
+
const port = raw && Number.isFinite(Number.parseInt(raw, 10)) ? Number.parseInt(raw, 10) : 3e3;
|
|
169
|
+
return { name: "preview", port, bin: "", args: [], skipSpawn: true };
|
|
170
|
+
}
|
|
162
171
|
const def = SHORTHANDS[name];
|
|
163
172
|
if (!def) {
|
|
164
173
|
throw new Error(
|
|
165
|
-
`[launch-kit-services] unknown shorthand "${name}" \u2014 known: ${Object.keys(SHORTHANDS).join(", ")}. Use an object entry { name, port, bin, args } for custom services.`
|
|
174
|
+
`[launch-kit-services] unknown shorthand "${name}" \u2014 known: ${[...Object.keys(SHORTHANDS), "preview"].join(", ")}. Use an object entry { name, port, bin, args } for custom services.`
|
|
166
175
|
);
|
|
167
176
|
}
|
|
168
177
|
return { name, port: def.port, bin: def.bin, args: [...def.args] };
|
|
@@ -243,13 +252,16 @@ function resolveServices(opts = {}) {
|
|
|
243
252
|
}
|
|
244
253
|
return validate(defaultServices());
|
|
245
254
|
}
|
|
246
|
-
var SHORTHAND_NAMES = Object.keys(SHORTHANDS);
|
|
255
|
+
var SHORTHAND_NAMES = [...Object.keys(SHORTHANDS), "preview"];
|
|
247
256
|
|
|
248
257
|
// src/server/cf-ingress.ts
|
|
249
258
|
var import_node_fs2 = require("node:fs");
|
|
250
259
|
var import_node_path2 = require("node:path");
|
|
251
260
|
var CF_API_BASE = "https://api.cloudflare.com/client/v4";
|
|
252
261
|
var CF_ERR_DNS_RECORD_EXISTS = 81053;
|
|
262
|
+
function serviceLabel(s) {
|
|
263
|
+
return s.label ?? s.name;
|
|
264
|
+
}
|
|
253
265
|
async function cf(opts) {
|
|
254
266
|
const res = await fetch(`${CF_API_BASE}${opts.path}`, {
|
|
255
267
|
method: opts.method,
|
|
@@ -329,7 +341,7 @@ async function fetchConnectorToken(input, tunnelId) {
|
|
|
329
341
|
}
|
|
330
342
|
async function setIngressConfig(input, tunnelId) {
|
|
331
343
|
const ingress = input.services.map((s) => ({
|
|
332
|
-
hostname: `${s
|
|
344
|
+
hostname: `${serviceLabel(s)}.${input.zone.name}`,
|
|
333
345
|
service: `http://localhost:${s.port}`
|
|
334
346
|
}));
|
|
335
347
|
ingress.push({ service: "http_status:404" });
|
|
@@ -344,7 +356,7 @@ async function setIngressConfig(input, tunnelId) {
|
|
|
344
356
|
}
|
|
345
357
|
}
|
|
346
358
|
async function ensureDnsRecord(input, tunnelId, service) {
|
|
347
|
-
const fqdn = `${service
|
|
359
|
+
const fqdn = `${serviceLabel(service)}.${input.zone.name}`;
|
|
348
360
|
const target = `${tunnelId}.cfargotunnel.com`;
|
|
349
361
|
const existing = await cf({
|
|
350
362
|
apiToken: input.apiToken,
|
|
@@ -388,10 +400,181 @@ async function provisionIngress(input) {
|
|
|
388
400
|
await setIngressConfig(input, tunnelId);
|
|
389
401
|
await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
|
|
390
402
|
const hostnames = {};
|
|
391
|
-
for (const s of input.services) hostnames[s.name] = `${s
|
|
403
|
+
for (const s of input.services) hostnames[s.name] = `${serviceLabel(s)}.${input.zone.name}`;
|
|
392
404
|
return { tunnelId, connectorToken, hostnames };
|
|
393
405
|
}
|
|
394
406
|
|
|
407
|
+
// src/server/cf-access.ts
|
|
408
|
+
var import_node_fs3 = require("node:fs");
|
|
409
|
+
var import_node_path3 = require("node:path");
|
|
410
|
+
var CF_API_BASE2 = "https://api.cloudflare.com/client/v4";
|
|
411
|
+
var IDP_NAME = "launch-kit-oidc";
|
|
412
|
+
async function cf2(opts) {
|
|
413
|
+
const res = await fetch(`${CF_API_BASE2}${opts.path}`, {
|
|
414
|
+
method: opts.method,
|
|
415
|
+
headers: {
|
|
416
|
+
Authorization: `Bearer ${opts.apiToken}`,
|
|
417
|
+
"Content-Type": "application/json",
|
|
418
|
+
Accept: "application/json",
|
|
419
|
+
"User-Agent": "launch-kit/cf-access"
|
|
420
|
+
},
|
|
421
|
+
body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0,
|
|
422
|
+
signal: AbortSignal.timeout(15e3)
|
|
423
|
+
});
|
|
424
|
+
const text = await res.text();
|
|
425
|
+
try {
|
|
426
|
+
return text ? JSON.parse(text) : { success: false };
|
|
427
|
+
} catch {
|
|
428
|
+
throw new Error(`[cf-access] ${opts.method} ${opts.path} \u2192 ${res.status}, non-JSON: ${text.slice(0, 200)}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function fail(env, what) {
|
|
432
|
+
throw new Error(`[cf-access] ${what} failed: ${JSON.stringify(env.errors)}`);
|
|
433
|
+
}
|
|
434
|
+
function loadState2(path) {
|
|
435
|
+
if (!(0, import_node_fs3.existsSync)(path)) return null;
|
|
436
|
+
try {
|
|
437
|
+
const parsed = JSON.parse((0, import_node_fs3.readFileSync)(path, "utf8"));
|
|
438
|
+
return typeof parsed?.accountId === "string" ? parsed : null;
|
|
439
|
+
} catch {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function saveState2(path, state) {
|
|
444
|
+
const dir = (0, import_node_path3.dirname)(path);
|
|
445
|
+
if (!(0, import_node_fs3.existsSync)(dir)) (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
|
|
446
|
+
(0, import_node_fs3.writeFileSync)(path, JSON.stringify(state, null, 2));
|
|
447
|
+
}
|
|
448
|
+
async function getAccessAuthDomain(apiToken, accountId) {
|
|
449
|
+
const res = await cf2({
|
|
450
|
+
apiToken,
|
|
451
|
+
method: "GET",
|
|
452
|
+
path: `/accounts/${accountId}/access/organizations`
|
|
453
|
+
});
|
|
454
|
+
if (!res.success || !res.result?.auth_domain) {
|
|
455
|
+
fail(res, "GET access/organizations (is Zero Trust enabled on this account?)");
|
|
456
|
+
}
|
|
457
|
+
return res.result.auth_domain;
|
|
458
|
+
}
|
|
459
|
+
async function ensureAccessIdp(input) {
|
|
460
|
+
const config = {
|
|
461
|
+
client_id: input.clientId,
|
|
462
|
+
client_secret: input.clientSecret,
|
|
463
|
+
auth_url: `${input.issuer}/api/oidc/authorize`,
|
|
464
|
+
token_url: `${input.issuer}/api/oidc/token`,
|
|
465
|
+
certs_url: `${input.issuer}/.well-known/jwks.json`,
|
|
466
|
+
scopes: ["openid", "email", "profile"],
|
|
467
|
+
claims: ["org", "project_access", "roles", "email"],
|
|
468
|
+
email_claim_name: "email",
|
|
469
|
+
pkce_enabled: true
|
|
470
|
+
};
|
|
471
|
+
const body = { name: IDP_NAME, type: "oidc", config };
|
|
472
|
+
let idpId = input.knownIdpId;
|
|
473
|
+
if (!idpId) {
|
|
474
|
+
const list = await cf2({
|
|
475
|
+
apiToken: input.apiToken,
|
|
476
|
+
method: "GET",
|
|
477
|
+
path: `/accounts/${input.accountId}/access/identity_providers`
|
|
478
|
+
});
|
|
479
|
+
if (!list.success) fail(list, "list identity_providers");
|
|
480
|
+
idpId = (list.result ?? []).find((p) => p.name === IDP_NAME)?.id ?? null;
|
|
481
|
+
}
|
|
482
|
+
if (idpId) {
|
|
483
|
+
const upd = await cf2({
|
|
484
|
+
apiToken: input.apiToken,
|
|
485
|
+
method: "PUT",
|
|
486
|
+
path: `/accounts/${input.accountId}/access/identity_providers/${idpId}`,
|
|
487
|
+
body
|
|
488
|
+
});
|
|
489
|
+
if (!upd.success || !upd.result) fail(upd, "update identity_provider");
|
|
490
|
+
return upd.result.id;
|
|
491
|
+
}
|
|
492
|
+
const created = await cf2({
|
|
493
|
+
apiToken: input.apiToken,
|
|
494
|
+
method: "POST",
|
|
495
|
+
path: `/accounts/${input.accountId}/access/identity_providers`,
|
|
496
|
+
body
|
|
497
|
+
});
|
|
498
|
+
if (!created.success || !created.result) fail(created, "create identity_provider");
|
|
499
|
+
return created.result.id;
|
|
500
|
+
}
|
|
501
|
+
async function ensureAccessApp(input) {
|
|
502
|
+
const policy = {
|
|
503
|
+
name: "launch-kit-org-allow",
|
|
504
|
+
decision: "allow",
|
|
505
|
+
include: [
|
|
506
|
+
{
|
|
507
|
+
oidc: {
|
|
508
|
+
identity_provider_id: input.idpId,
|
|
509
|
+
claim_name: "org",
|
|
510
|
+
claim_value: input.organizationId
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
]
|
|
514
|
+
};
|
|
515
|
+
const body = {
|
|
516
|
+
name: `launch-kit ${input.service.hostname}`,
|
|
517
|
+
domain: input.service.hostname,
|
|
518
|
+
type: "self_hosted",
|
|
519
|
+
// Bot terminal = RCE surface → short session. Read portals = a workday.
|
|
520
|
+
session_duration: input.service.strict ? "30m" : "24h",
|
|
521
|
+
allowed_idps: [input.idpId],
|
|
522
|
+
auto_redirect_to_identity: true,
|
|
523
|
+
policies: [policy]
|
|
524
|
+
};
|
|
525
|
+
const list = await cf2({
|
|
526
|
+
apiToken: input.apiToken,
|
|
527
|
+
method: "GET",
|
|
528
|
+
path: `/accounts/${input.accountId}/access/apps`
|
|
529
|
+
});
|
|
530
|
+
if (!list.success) fail(list, "list access apps");
|
|
531
|
+
const existing = (list.result ?? []).find((a) => a.domain === input.service.hostname);
|
|
532
|
+
if (existing) {
|
|
533
|
+
const upd = await cf2({
|
|
534
|
+
apiToken: input.apiToken,
|
|
535
|
+
method: "PUT",
|
|
536
|
+
path: `/accounts/${input.accountId}/access/apps/${existing.id}`,
|
|
537
|
+
body
|
|
538
|
+
});
|
|
539
|
+
if (!upd.success || !upd.result) fail(upd, `update access app ${input.service.hostname}`);
|
|
540
|
+
return upd.result.id;
|
|
541
|
+
}
|
|
542
|
+
const created = await cf2({
|
|
543
|
+
apiToken: input.apiToken,
|
|
544
|
+
method: "POST",
|
|
545
|
+
path: `/accounts/${input.accountId}/access/apps`,
|
|
546
|
+
body
|
|
547
|
+
});
|
|
548
|
+
if (!created.success || !created.result) fail(created, `create access app ${input.service.hostname}`);
|
|
549
|
+
return created.result.id;
|
|
550
|
+
}
|
|
551
|
+
async function provisionAccess(input) {
|
|
552
|
+
const authDomain = await getAccessAuthDomain(input.apiToken, input.accountId);
|
|
553
|
+
const callbackUrl = `https://${authDomain}/cdn-cgi/access/callback`;
|
|
554
|
+
const { clientId, clientSecret, organizationId } = await input.registerClient([callbackUrl]);
|
|
555
|
+
const prior = loadState2(input.stateFile);
|
|
556
|
+
const idpId = await ensureAccessIdp({
|
|
557
|
+
apiToken: input.apiToken,
|
|
558
|
+
accountId: input.accountId,
|
|
559
|
+
issuer: input.issuer,
|
|
560
|
+
clientId,
|
|
561
|
+
clientSecret,
|
|
562
|
+
knownIdpId: prior?.idpId ?? null
|
|
563
|
+
});
|
|
564
|
+
saveState2(input.stateFile, { idpId, accountId: input.accountId });
|
|
565
|
+
const appIds = {};
|
|
566
|
+
for (const service of input.services) {
|
|
567
|
+
appIds[service.hostname] = await ensureAccessApp({
|
|
568
|
+
apiToken: input.apiToken,
|
|
569
|
+
accountId: input.accountId,
|
|
570
|
+
idpId,
|
|
571
|
+
organizationId,
|
|
572
|
+
service
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
return { idpId, authDomain, appIds };
|
|
576
|
+
}
|
|
577
|
+
|
|
395
578
|
// src/server/radar-docker-init-entry.ts
|
|
396
579
|
var REQUIRED_ENV = [
|
|
397
580
|
"CLAUDE_CREDENTIALS_B64",
|
|
@@ -399,13 +582,13 @@ var REQUIRED_ENV = [
|
|
|
399
582
|
"LS_ORG_SLUG",
|
|
400
583
|
"LS_PROJECT_SLUG"
|
|
401
584
|
];
|
|
402
|
-
function
|
|
585
|
+
function fail2(message) {
|
|
403
586
|
console.error(message);
|
|
404
587
|
process.exit(1);
|
|
405
588
|
}
|
|
406
589
|
function requireEnv(name) {
|
|
407
590
|
const v = process.env[name];
|
|
408
|
-
if (!v)
|
|
591
|
+
if (!v) fail2(`ERROR: ${name} is required but not set`);
|
|
409
592
|
return v;
|
|
410
593
|
}
|
|
411
594
|
function run(cmd, args, stdio = "inherit") {
|
|
@@ -422,13 +605,16 @@ async function setupFromCloud() {
|
|
|
422
605
|
try {
|
|
423
606
|
bundle = await mcp.call("radar_bootstrap_get", {});
|
|
424
607
|
} catch (err) {
|
|
425
|
-
|
|
608
|
+
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.`);
|
|
426
609
|
}
|
|
427
610
|
if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
|
|
428
611
|
if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
|
|
429
612
|
if (!process.env.GH_TOKEN && bundle.githubToken) process.env.GH_TOKEN = bundle.githubToken;
|
|
613
|
+
if (!process.env.RADAR_RULES && Array.isArray(bundle.radarRules) && bundle.radarRules.length > 0) {
|
|
614
|
+
process.env.RADAR_RULES = JSON.stringify(bundle.radarRules);
|
|
615
|
+
}
|
|
430
616
|
if (!process.env.GH_TOKEN) {
|
|
431
|
-
|
|
617
|
+
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.`);
|
|
432
618
|
}
|
|
433
619
|
const cfNote = bundle.cloudflareToken ? "cloudflare=connected" : "cloudflare=none";
|
|
434
620
|
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}`);
|
|
@@ -436,17 +622,17 @@ async function setupFromCloud() {
|
|
|
436
622
|
}
|
|
437
623
|
function setupClaudeCredentials() {
|
|
438
624
|
const home = process.env.HOME ?? "/home/launchpod";
|
|
439
|
-
const claudeDir = (0,
|
|
440
|
-
(0,
|
|
625
|
+
const claudeDir = (0, import_node_path4.join)(home, ".claude");
|
|
626
|
+
(0, import_node_fs4.mkdirSync)(claudeDir, { recursive: true });
|
|
441
627
|
const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
|
|
442
|
-
const credsPath = (0,
|
|
443
|
-
(0,
|
|
444
|
-
(0,
|
|
445
|
-
const configPath = (0,
|
|
628
|
+
const credsPath = (0, import_node_path4.join)(claudeDir, ".credentials.json");
|
|
629
|
+
(0, import_node_fs4.writeFileSync)(credsPath, decoded);
|
|
630
|
+
(0, import_node_fs4.chmodSync)(credsPath, 384);
|
|
631
|
+
const configPath = (0, import_node_path4.join)(home, ".claude.json");
|
|
446
632
|
let cfg = {};
|
|
447
|
-
if ((0,
|
|
633
|
+
if ((0, import_node_fs4.existsSync)(configPath)) {
|
|
448
634
|
try {
|
|
449
|
-
cfg = JSON.parse((0,
|
|
635
|
+
cfg = JSON.parse((0, import_node_fs4.readFileSync)(configPath, "utf8"));
|
|
450
636
|
} catch {
|
|
451
637
|
cfg = {};
|
|
452
638
|
}
|
|
@@ -455,18 +641,56 @@ function setupClaudeCredentials() {
|
|
|
455
641
|
cfg.lastOnboardingVersion = cfg.lastOnboardingVersion ?? "2.1.159";
|
|
456
642
|
cfg.numStartups = (cfg.numStartups ?? 0) + 1;
|
|
457
643
|
cfg.installMethod = cfg.installMethod ?? "global";
|
|
458
|
-
|
|
459
|
-
|
|
644
|
+
const PREAPPROVED_MCPS = [
|
|
645
|
+
"launch-secure",
|
|
646
|
+
"launch-chart",
|
|
647
|
+
"launch-deck",
|
|
648
|
+
"launch-orbit",
|
|
649
|
+
"launch-recall",
|
|
650
|
+
"launch-beacon",
|
|
651
|
+
"launch-sequencer"
|
|
652
|
+
];
|
|
653
|
+
const projects = cfg.projects ?? {};
|
|
654
|
+
const wsKey = "/workspace";
|
|
655
|
+
const wsProject = projects[wsKey] ?? {};
|
|
656
|
+
const existingEnabled = Array.isArray(wsProject.enabledMcpjsonServers) ? wsProject.enabledMcpjsonServers : [];
|
|
657
|
+
const mergedEnabled = Array.from(/* @__PURE__ */ new Set([...existingEnabled, ...PREAPPROVED_MCPS]));
|
|
658
|
+
wsProject.enabledMcpjsonServers = mergedEnabled;
|
|
659
|
+
projects[wsKey] = wsProject;
|
|
660
|
+
cfg.projects = projects;
|
|
661
|
+
(0, import_node_fs4.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
|
|
662
|
+
(0, import_node_fs4.chmodSync)(configPath, 384);
|
|
460
663
|
}
|
|
461
664
|
function setupGitAndGh() {
|
|
462
665
|
const name = process.env.GIT_USER_NAME ?? "Radar Bot";
|
|
463
666
|
const email = process.env.GIT_USER_EMAIL ?? "radar@launchpod.local";
|
|
464
667
|
const status = run("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
|
|
465
|
-
if (status !== 0)
|
|
668
|
+
if (status !== 0) fail2(`[entrypoint] launch-kit setup-git failed (status ${status})`);
|
|
669
|
+
}
|
|
670
|
+
function detectAndSetPreviewPort() {
|
|
671
|
+
if (process.env.PREVIEW_PORT) return;
|
|
672
|
+
try {
|
|
673
|
+
const pkgPath = "/workspace/package.json";
|
|
674
|
+
if (!(0, import_node_fs4.existsSync)(pkgPath)) return;
|
|
675
|
+
const pkg = JSON.parse((0, import_node_fs4.readFileSync)(pkgPath, "utf-8"));
|
|
676
|
+
const scripts = pkg.scripts ?? {};
|
|
677
|
+
const portRe = /(?:--port[= ]|-p\s+|\bPORT=)(\d{2,5})\b/;
|
|
678
|
+
for (const name of ["dev", "start", "serve"]) {
|
|
679
|
+
const script = scripts[name];
|
|
680
|
+
if (typeof script !== "string") continue;
|
|
681
|
+
const m = script.match(portRe);
|
|
682
|
+
if (m) {
|
|
683
|
+
process.env.PREVIEW_PORT = m[1];
|
|
684
|
+
console.log(`[entrypoint] preview port detected from package.json scripts.${name}: ${m[1]}`);
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
} catch {
|
|
689
|
+
}
|
|
466
690
|
}
|
|
467
691
|
function initWorkspaceIfEmpty() {
|
|
468
692
|
process.chdir("/workspace");
|
|
469
|
-
if ((0,
|
|
693
|
+
if ((0, import_node_fs4.existsSync)(".git")) {
|
|
470
694
|
console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
|
|
471
695
|
return;
|
|
472
696
|
}
|
|
@@ -479,7 +703,7 @@ function initWorkspaceIfEmpty() {
|
|
|
479
703
|
`--url=${process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app"}`,
|
|
480
704
|
`--dir=/workspace`
|
|
481
705
|
]);
|
|
482
|
-
if (status !== 0)
|
|
706
|
+
if (status !== 0) fail2(`[entrypoint] launch-kit init failed (status ${status})`);
|
|
483
707
|
}
|
|
484
708
|
async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
485
709
|
const token = bundle.cloudflareToken ?? null;
|
|
@@ -487,28 +711,36 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
|
487
711
|
const zones = bundle.cloudflareZones ?? [];
|
|
488
712
|
if (!token && !accountId && zones.length === 0) return null;
|
|
489
713
|
if (!token || !accountId) {
|
|
490
|
-
|
|
714
|
+
fail2(`[entrypoint] cloudflare integration is partial \u2014 token=${token ? "set" : "missing"} accountId=${accountId ? "set" : "missing"}. Re-connect the Cloudflare provider in LS.`);
|
|
491
715
|
}
|
|
492
716
|
const baseDomain = process.env.LAUNCHKIT_CF_BASE_DOMAIN?.trim();
|
|
493
717
|
let chosen = null;
|
|
494
718
|
if (baseDomain) {
|
|
495
719
|
chosen = zones.find((z) => z.name === baseDomain) ?? null;
|
|
496
720
|
if (!chosen) {
|
|
497
|
-
|
|
721
|
+
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.`);
|
|
498
722
|
}
|
|
499
723
|
} else if (zones.length === 1) {
|
|
500
724
|
chosen = { id: zones[0].id, name: zones[0].name };
|
|
501
725
|
} else {
|
|
502
|
-
|
|
726
|
+
fail2(`[entrypoint] cloudflare token covers ${zones.length} zones (${zones.map((z) => z.name).join(", ")}) \u2014 set LAUNCHKIT_CF_BASE_DOMAIN to pick one.`);
|
|
503
727
|
}
|
|
504
728
|
const stateFile = "/workspace/.launchpod/launch-kit-tunnel.json";
|
|
505
|
-
|
|
729
|
+
const slugLabel = projectSlug.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
730
|
+
const DNS_LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
731
|
+
for (const s of services) {
|
|
732
|
+
const label = `${slugLabel}-${s.name}`;
|
|
733
|
+
if (!DNS_LABEL_RE.test(label)) {
|
|
734
|
+
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}").`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
console.log(`[entrypoint] provisioning CF named tunnel \u2014 name=launch-kit-${projectSlug} zone=${chosen.name} services=${services.map((s) => `${slugLabel}-${s.name}`).join(",")}`);
|
|
506
738
|
const result = await provisionIngress({
|
|
507
739
|
apiToken: token,
|
|
508
740
|
accountId,
|
|
509
741
|
zone: chosen,
|
|
510
742
|
tunnelName: `launch-kit-${projectSlug}`,
|
|
511
|
-
services: services.map((s) => ({ name: s.name, port: s.port })),
|
|
743
|
+
services: services.map((s) => ({ name: s.name, label: `${slugLabel}-${s.name}`, port: s.port })),
|
|
512
744
|
stateFile
|
|
513
745
|
});
|
|
514
746
|
for (const [name, fqdn] of Object.entries(result.hostnames)) {
|
|
@@ -516,6 +748,61 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
|
516
748
|
}
|
|
517
749
|
return result;
|
|
518
750
|
}
|
|
751
|
+
var GATED_SERVICES = {
|
|
752
|
+
// Claude web terminal — live drivable shell ⇒ RCE surface ⇒ short session.
|
|
753
|
+
bot: { strict: true },
|
|
754
|
+
// The user's own dev/preview server — a workday-length session is fine.
|
|
755
|
+
preview: { strict: false }
|
|
756
|
+
};
|
|
757
|
+
async function registerOidcClient(serverUrl, pat, redirectUris) {
|
|
758
|
+
const res = await fetch(new URL("/api/rover/oidc-client", serverUrl), {
|
|
759
|
+
method: "POST",
|
|
760
|
+
headers: {
|
|
761
|
+
Authorization: `Bearer ${pat}`,
|
|
762
|
+
"Content-Type": "application/json",
|
|
763
|
+
Accept: "application/json"
|
|
764
|
+
},
|
|
765
|
+
body: JSON.stringify({ redirectUris }),
|
|
766
|
+
signal: AbortSignal.timeout(15e3)
|
|
767
|
+
});
|
|
768
|
+
const body = await res.json().catch(() => null);
|
|
769
|
+
if (!res.ok || !body?.success || !body.data) {
|
|
770
|
+
fail2(`[entrypoint] OIDC client provisioning failed (HTTP ${res.status}): ${body?.error ?? "unexpected response"}`);
|
|
771
|
+
}
|
|
772
|
+
return body.data;
|
|
773
|
+
}
|
|
774
|
+
async function maybeProvisionAccess(bundle, ingress) {
|
|
775
|
+
const token = bundle.cloudflareToken ?? null;
|
|
776
|
+
const accountId = bundle.cloudflareAccountId ?? null;
|
|
777
|
+
if (!token || !accountId) return;
|
|
778
|
+
const services = [];
|
|
779
|
+
const skipped = [];
|
|
780
|
+
for (const [name, hostname] of Object.entries(ingress.hostnames)) {
|
|
781
|
+
const cfg = GATED_SERVICES[name];
|
|
782
|
+
if (cfg) services.push({ hostname, strict: cfg.strict });
|
|
783
|
+
else skipped.push(name);
|
|
784
|
+
}
|
|
785
|
+
if (skipped.length > 0) {
|
|
786
|
+
console.log(`[entrypoint] CF Access: leaving machine surface(s) ungated: ${skipped.join(", ")}`);
|
|
787
|
+
}
|
|
788
|
+
if (services.length === 0) {
|
|
789
|
+
console.log("[entrypoint] CF Access: no human-facing service to gate (bot/preview not provisioned)");
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
const serverUrl = process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app";
|
|
793
|
+
const pat = requireEnv("LS_PAT");
|
|
794
|
+
const stateFile = "/workspace/.launchpod/launch-kit-access.json";
|
|
795
|
+
console.log(`[entrypoint] gating ${services.map((s) => s.hostname).join(", ")} behind CF Access (IdP: ${serverUrl})`);
|
|
796
|
+
const result = await provisionAccess({
|
|
797
|
+
apiToken: token,
|
|
798
|
+
accountId,
|
|
799
|
+
issuer: serverUrl,
|
|
800
|
+
services,
|
|
801
|
+
stateFile,
|
|
802
|
+
registerClient: (redirectUris) => registerOidcClient(serverUrl, pat, redirectUris)
|
|
803
|
+
});
|
|
804
|
+
console.log(`[entrypoint] CF Access gate live \u2014 IdP ${result.idpId}, auth domain ${result.authDomain}`);
|
|
805
|
+
}
|
|
519
806
|
function spawnServiceGroup(services) {
|
|
520
807
|
const children = [];
|
|
521
808
|
let shuttingDown = false;
|
|
@@ -563,6 +850,10 @@ function spawnServiceGroup(services) {
|
|
|
563
850
|
let exitedCount = 0;
|
|
564
851
|
let firstFailure = null;
|
|
565
852
|
for (const spec of services) {
|
|
853
|
+
if (spec.skipSpawn) {
|
|
854
|
+
console.log(`[entrypoint] ${spec.name} \u2192 ingress-only on port ${spec.port} (no spawn; user starts dev server here)`);
|
|
855
|
+
continue;
|
|
856
|
+
}
|
|
566
857
|
const args = [...spec.args, "--port", String(spec.port)];
|
|
567
858
|
console.log(`[entrypoint] starting ${spec.name}: ${spec.bin} ${args.join(" ")}`);
|
|
568
859
|
const proc = (0, import_node_child_process.spawn)(spec.bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -599,11 +890,12 @@ async function main() {
|
|
|
599
890
|
setupClaudeCredentials();
|
|
600
891
|
setupGitAndGh();
|
|
601
892
|
initWorkspaceIfEmpty();
|
|
893
|
+
detectAndSetPreviewPort();
|
|
602
894
|
let services;
|
|
603
895
|
try {
|
|
604
896
|
services = resolveServices();
|
|
605
897
|
} catch (err) {
|
|
606
|
-
|
|
898
|
+
fail2(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
|
|
607
899
|
}
|
|
608
900
|
console.log(`[entrypoint] services: ${services.map((s) => `${s.name}@${s.port}`).join(", ")}`);
|
|
609
901
|
const ingress = await maybeProvisionIngress(bundle, services, requireEnv("LS_PROJECT_SLUG"));
|
|
@@ -612,8 +904,9 @@ async function main() {
|
|
|
612
904
|
const radarFqdn = ingress.hostnames.radar;
|
|
613
905
|
if (radarFqdn) process.env.RADAR_CF_TUNNEL_HOSTNAME = radarFqdn;
|
|
614
906
|
else if (services.some((s) => s.name === "radar")) {
|
|
615
|
-
|
|
907
|
+
fail2(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
|
|
616
908
|
}
|
|
909
|
+
await maybeProvisionAccess(bundle, ingress);
|
|
617
910
|
} else if (services.length > 1) {
|
|
618
911
|
const first = services[0];
|
|
619
912
|
console.warn(
|
|
@@ -639,6 +932,7 @@ if (!process.env.VITEST) {
|
|
|
639
932
|
}
|
|
640
933
|
// Annotate the CommonJS export names for ESM import in node:
|
|
641
934
|
0 && (module.exports = {
|
|
935
|
+
maybeProvisionAccess,
|
|
642
936
|
maybeProvisionIngress,
|
|
643
937
|
spawnServiceGroup
|
|
644
938
|
});
|