@launchsecure/launch-kit 0.0.35 → 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 +240 -24
- package/dist/server/council-entry.js +0 -0
- package/dist/server/fb-wizard.js +0 -0
- package/dist/server/init-entry.js +637 -234
- package/dist/server/orbit-entry.js +880 -144
- package/dist/server/radar-docker-init-entry.js +276 -34
- package/dist/server/radar-entrypoint-entry.js +0 -0
- package/dist/server/radar-teardown-entry.js +0 -0
- package/dist/server/rover-entry.js +84 -17
- package/package.json +23 -22
- 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/scaffolds/migrate-safety/scripts/migrate-with-backup.sh +0 -0
- package/scaffolds/recall-hook/scripts/ensure-recall.sh +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,
|
|
@@ -338,7 +341,7 @@ async function fetchConnectorToken(input, tunnelId) {
|
|
|
338
341
|
}
|
|
339
342
|
async function setIngressConfig(input, tunnelId) {
|
|
340
343
|
const ingress = input.services.map((s) => ({
|
|
341
|
-
hostname: `${s
|
|
344
|
+
hostname: `${serviceLabel(s)}.${input.zone.name}`,
|
|
342
345
|
service: `http://localhost:${s.port}`
|
|
343
346
|
}));
|
|
344
347
|
ingress.push({ service: "http_status:404" });
|
|
@@ -353,7 +356,7 @@ async function setIngressConfig(input, tunnelId) {
|
|
|
353
356
|
}
|
|
354
357
|
}
|
|
355
358
|
async function ensureDnsRecord(input, tunnelId, service) {
|
|
356
|
-
const fqdn = `${service
|
|
359
|
+
const fqdn = `${serviceLabel(service)}.${input.zone.name}`;
|
|
357
360
|
const target = `${tunnelId}.cfargotunnel.com`;
|
|
358
361
|
const existing = await cf({
|
|
359
362
|
apiToken: input.apiToken,
|
|
@@ -397,10 +400,181 @@ async function provisionIngress(input) {
|
|
|
397
400
|
await setIngressConfig(input, tunnelId);
|
|
398
401
|
await Promise.all(input.services.map((s) => ensureDnsRecord(input, tunnelId, s)));
|
|
399
402
|
const hostnames = {};
|
|
400
|
-
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}`;
|
|
401
404
|
return { tunnelId, connectorToken, hostnames };
|
|
402
405
|
}
|
|
403
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
|
+
|
|
404
578
|
// src/server/radar-docker-init-entry.ts
|
|
405
579
|
var REQUIRED_ENV = [
|
|
406
580
|
"CLAUDE_CREDENTIALS_B64",
|
|
@@ -408,13 +582,13 @@ var REQUIRED_ENV = [
|
|
|
408
582
|
"LS_ORG_SLUG",
|
|
409
583
|
"LS_PROJECT_SLUG"
|
|
410
584
|
];
|
|
411
|
-
function
|
|
585
|
+
function fail2(message) {
|
|
412
586
|
console.error(message);
|
|
413
587
|
process.exit(1);
|
|
414
588
|
}
|
|
415
589
|
function requireEnv(name) {
|
|
416
590
|
const v = process.env[name];
|
|
417
|
-
if (!v)
|
|
591
|
+
if (!v) fail2(`ERROR: ${name} is required but not set`);
|
|
418
592
|
return v;
|
|
419
593
|
}
|
|
420
594
|
function run(cmd, args, stdio = "inherit") {
|
|
@@ -431,13 +605,16 @@ async function setupFromCloud() {
|
|
|
431
605
|
try {
|
|
432
606
|
bundle = await mcp.call("radar_bootstrap_get", {});
|
|
433
607
|
} catch (err) {
|
|
434
|
-
|
|
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.`);
|
|
435
609
|
}
|
|
436
610
|
if (!process.env.GIT_USER_NAME) process.env.GIT_USER_NAME = bundle.gitName;
|
|
437
611
|
if (!process.env.GIT_USER_EMAIL) process.env.GIT_USER_EMAIL = bundle.gitEmail;
|
|
438
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
|
+
}
|
|
439
616
|
if (!process.env.GH_TOKEN) {
|
|
440
|
-
|
|
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.`);
|
|
441
618
|
}
|
|
442
619
|
const cfNote = bundle.cloudflareToken ? "cloudflare=connected" : "cloudflare=none";
|
|
443
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}`);
|
|
@@ -445,17 +622,17 @@ async function setupFromCloud() {
|
|
|
445
622
|
}
|
|
446
623
|
function setupClaudeCredentials() {
|
|
447
624
|
const home = process.env.HOME ?? "/home/launchpod";
|
|
448
|
-
const claudeDir = (0,
|
|
449
|
-
(0,
|
|
625
|
+
const claudeDir = (0, import_node_path4.join)(home, ".claude");
|
|
626
|
+
(0, import_node_fs4.mkdirSync)(claudeDir, { recursive: true });
|
|
450
627
|
const decoded = Buffer.from(requireEnv("CLAUDE_CREDENTIALS_B64"), "base64").toString("utf8");
|
|
451
|
-
const credsPath = (0,
|
|
452
|
-
(0,
|
|
453
|
-
(0,
|
|
454
|
-
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");
|
|
455
632
|
let cfg = {};
|
|
456
|
-
if ((0,
|
|
633
|
+
if ((0, import_node_fs4.existsSync)(configPath)) {
|
|
457
634
|
try {
|
|
458
|
-
cfg = JSON.parse((0,
|
|
635
|
+
cfg = JSON.parse((0, import_node_fs4.readFileSync)(configPath, "utf8"));
|
|
459
636
|
} catch {
|
|
460
637
|
cfg = {};
|
|
461
638
|
}
|
|
@@ -481,21 +658,21 @@ function setupClaudeCredentials() {
|
|
|
481
658
|
wsProject.enabledMcpjsonServers = mergedEnabled;
|
|
482
659
|
projects[wsKey] = wsProject;
|
|
483
660
|
cfg.projects = projects;
|
|
484
|
-
(0,
|
|
485
|
-
(0,
|
|
661
|
+
(0, import_node_fs4.writeFileSync)(configPath, JSON.stringify(cfg, null, 2));
|
|
662
|
+
(0, import_node_fs4.chmodSync)(configPath, 384);
|
|
486
663
|
}
|
|
487
664
|
function setupGitAndGh() {
|
|
488
665
|
const name = process.env.GIT_USER_NAME ?? "Radar Bot";
|
|
489
666
|
const email = process.env.GIT_USER_EMAIL ?? "radar@launchpod.local";
|
|
490
667
|
const status = run("launch-kit", ["setup-git", `--identity=${name} <${email}>`]);
|
|
491
|
-
if (status !== 0)
|
|
668
|
+
if (status !== 0) fail2(`[entrypoint] launch-kit setup-git failed (status ${status})`);
|
|
492
669
|
}
|
|
493
670
|
function detectAndSetPreviewPort() {
|
|
494
671
|
if (process.env.PREVIEW_PORT) return;
|
|
495
672
|
try {
|
|
496
673
|
const pkgPath = "/workspace/package.json";
|
|
497
|
-
if (!(0,
|
|
498
|
-
const pkg = JSON.parse((0,
|
|
674
|
+
if (!(0, import_node_fs4.existsSync)(pkgPath)) return;
|
|
675
|
+
const pkg = JSON.parse((0, import_node_fs4.readFileSync)(pkgPath, "utf-8"));
|
|
499
676
|
const scripts = pkg.scripts ?? {};
|
|
500
677
|
const portRe = /(?:--port[= ]|-p\s+|\bPORT=)(\d{2,5})\b/;
|
|
501
678
|
for (const name of ["dev", "start", "serve"]) {
|
|
@@ -513,7 +690,7 @@ function detectAndSetPreviewPort() {
|
|
|
513
690
|
}
|
|
514
691
|
function initWorkspaceIfEmpty() {
|
|
515
692
|
process.chdir("/workspace");
|
|
516
|
-
if ((0,
|
|
693
|
+
if ((0, import_node_fs4.existsSync)(".git")) {
|
|
517
694
|
console.log("[entrypoint] /workspace already initialized \u2014 skipping init");
|
|
518
695
|
return;
|
|
519
696
|
}
|
|
@@ -526,7 +703,7 @@ function initWorkspaceIfEmpty() {
|
|
|
526
703
|
`--url=${process.env.LS_SERVER_URL ?? "https://launchsecure-v2.vercel.app"}`,
|
|
527
704
|
`--dir=/workspace`
|
|
528
705
|
]);
|
|
529
|
-
if (status !== 0)
|
|
706
|
+
if (status !== 0) fail2(`[entrypoint] launch-kit init failed (status ${status})`);
|
|
530
707
|
}
|
|
531
708
|
async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
532
709
|
const token = bundle.cloudflareToken ?? null;
|
|
@@ -534,28 +711,36 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
|
534
711
|
const zones = bundle.cloudflareZones ?? [];
|
|
535
712
|
if (!token && !accountId && zones.length === 0) return null;
|
|
536
713
|
if (!token || !accountId) {
|
|
537
|
-
|
|
714
|
+
fail2(`[entrypoint] cloudflare integration is partial \u2014 token=${token ? "set" : "missing"} accountId=${accountId ? "set" : "missing"}. Re-connect the Cloudflare provider in LS.`);
|
|
538
715
|
}
|
|
539
716
|
const baseDomain = process.env.LAUNCHKIT_CF_BASE_DOMAIN?.trim();
|
|
540
717
|
let chosen = null;
|
|
541
718
|
if (baseDomain) {
|
|
542
719
|
chosen = zones.find((z) => z.name === baseDomain) ?? null;
|
|
543
720
|
if (!chosen) {
|
|
544
|
-
|
|
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.`);
|
|
545
722
|
}
|
|
546
723
|
} else if (zones.length === 1) {
|
|
547
724
|
chosen = { id: zones[0].id, name: zones[0].name };
|
|
548
725
|
} else {
|
|
549
|
-
|
|
726
|
+
fail2(`[entrypoint] cloudflare token covers ${zones.length} zones (${zones.map((z) => z.name).join(", ")}) \u2014 set LAUNCHKIT_CF_BASE_DOMAIN to pick one.`);
|
|
550
727
|
}
|
|
551
728
|
const stateFile = "/workspace/.launchpod/launch-kit-tunnel.json";
|
|
552
|
-
|
|
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(",")}`);
|
|
553
738
|
const result = await provisionIngress({
|
|
554
739
|
apiToken: token,
|
|
555
740
|
accountId,
|
|
556
741
|
zone: chosen,
|
|
557
742
|
tunnelName: `launch-kit-${projectSlug}`,
|
|
558
|
-
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 })),
|
|
559
744
|
stateFile
|
|
560
745
|
});
|
|
561
746
|
for (const [name, fqdn] of Object.entries(result.hostnames)) {
|
|
@@ -563,6 +748,61 @@ async function maybeProvisionIngress(bundle, services, projectSlug) {
|
|
|
563
748
|
}
|
|
564
749
|
return result;
|
|
565
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
|
+
}
|
|
566
806
|
function spawnServiceGroup(services) {
|
|
567
807
|
const children = [];
|
|
568
808
|
let shuttingDown = false;
|
|
@@ -655,7 +895,7 @@ async function main() {
|
|
|
655
895
|
try {
|
|
656
896
|
services = resolveServices();
|
|
657
897
|
} catch (err) {
|
|
658
|
-
|
|
898
|
+
fail2(`[entrypoint] ${err instanceof Error ? err.message : String(err)}`);
|
|
659
899
|
}
|
|
660
900
|
console.log(`[entrypoint] services: ${services.map((s) => `${s.name}@${s.port}`).join(", ")}`);
|
|
661
901
|
const ingress = await maybeProvisionIngress(bundle, services, requireEnv("LS_PROJECT_SLUG"));
|
|
@@ -664,8 +904,9 @@ async function main() {
|
|
|
664
904
|
const radarFqdn = ingress.hostnames.radar;
|
|
665
905
|
if (radarFqdn) process.env.RADAR_CF_TUNNEL_HOSTNAME = radarFqdn;
|
|
666
906
|
else if (services.some((s) => s.name === "radar")) {
|
|
667
|
-
|
|
907
|
+
fail2(`[entrypoint] internal: ingress provisioned but no hostname for radar`);
|
|
668
908
|
}
|
|
909
|
+
await maybeProvisionAccess(bundle, ingress);
|
|
669
910
|
} else if (services.length > 1) {
|
|
670
911
|
const first = services[0];
|
|
671
912
|
console.warn(
|
|
@@ -691,6 +932,7 @@ if (!process.env.VITEST) {
|
|
|
691
932
|
}
|
|
692
933
|
// Annotate the CommonJS export names for ESM import in node:
|
|
693
934
|
0 && (module.exports = {
|
|
935
|
+
maybeProvisionAccess,
|
|
694
936
|
maybeProvisionIngress,
|
|
695
937
|
spawnServiceGroup
|
|
696
938
|
});
|
|
File without changes
|
|
File without changes
|