@getmonoceros/workbench 1.37.14 → 1.38.1
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/bundled-components/services/keycloak/component.yml +8 -1
- package/dist/bin.js +359 -96
- package/dist/bin.js.map +1 -1
- package/package.json +3 -1
|
@@ -9,7 +9,14 @@ service:
|
|
|
9
9
|
# Dev mode + realm import. The Keycloak image has no auto-start default,
|
|
10
10
|
# so a command is mandatory. `--import-realm` reads every *.json from
|
|
11
11
|
# /opt/keycloak/data/import at startup (a no-op if the dir is empty).
|
|
12
|
-
|
|
12
|
+
# `--proxy-headers=xforwarded` makes Keycloak derive the issuer host and
|
|
13
|
+
# scheme from X-Forwarded-Host / X-Forwarded-Proto, so the realm stamps a
|
|
14
|
+
# matching issuer whether it is reached over http via the Traefik proxy
|
|
15
|
+
# (`plantlove.localhost`) or over https via `monoceros share` on a phone.
|
|
16
|
+
# Without it Keycloak uses the raw connection scheme (always http behind a
|
|
17
|
+
# TLS-terminating proxy) and the OIDC token exchange fails on the https
|
|
18
|
+
# client. See ADR 0033.
|
|
19
|
+
command: start-dev --import-realm --proxy-headers=xforwarded
|
|
13
20
|
# Starts in a host-side second wave AFTER the repo clone (ADR 0025), so a
|
|
14
21
|
# mounted realm.json / theme from the project is present at boot. No
|
|
15
22
|
# dataMount: the dev H2 database is ephemeral and re-seeds from the
|
package/dist/bin.js
CHANGED
|
@@ -4107,12 +4107,12 @@ async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
|
|
|
4107
4107
|
const devcontainerDir = path11.join(targetDir, ".devcontainer");
|
|
4108
4108
|
const monocerosDir = path11.join(targetDir, ".monoceros");
|
|
4109
4109
|
const projectsDir = path11.join(targetDir, "projects");
|
|
4110
|
-
const
|
|
4110
|
+
const homeDir2 = path11.join(targetDir, "home");
|
|
4111
4111
|
const dataDir = path11.join(targetDir, "data");
|
|
4112
4112
|
await fs9.mkdir(devcontainerDir, { recursive: true });
|
|
4113
4113
|
await fs9.mkdir(monocerosDir, { recursive: true });
|
|
4114
4114
|
await fs9.mkdir(projectsDir, { recursive: true });
|
|
4115
|
-
await fs9.mkdir(
|
|
4115
|
+
await fs9.mkdir(homeDir2, { recursive: true });
|
|
4116
4116
|
if (needsCompose(opts)) {
|
|
4117
4117
|
await fs9.mkdir(dataDir, { recursive: true });
|
|
4118
4118
|
for (const svc of opts.services) {
|
|
@@ -4176,10 +4176,10 @@ async function writeScaffold(opts, targetDir, scaffoldOpts = {}) {
|
|
|
4176
4176
|
}
|
|
4177
4177
|
for (const f of resolvedFeatures) {
|
|
4178
4178
|
for (const sub of f.persistentHomePaths) {
|
|
4179
|
-
await fs9.mkdir(path11.join(
|
|
4179
|
+
await fs9.mkdir(path11.join(homeDir2, sub), { recursive: true });
|
|
4180
4180
|
}
|
|
4181
4181
|
for (const entry2 of f.persistentHomeFiles) {
|
|
4182
|
-
const filePath = path11.join(
|
|
4182
|
+
const filePath = path11.join(homeDir2, entry2.path);
|
|
4183
4183
|
await fs9.mkdir(path11.dirname(filePath), { recursive: true });
|
|
4184
4184
|
if (!existsSync8(filePath)) {
|
|
4185
4185
|
await fs9.writeFile(filePath, entry2.initialContent);
|
|
@@ -4593,75 +4593,87 @@ function generateAgentsMd(input) {
|
|
|
4593
4593
|
" DataGrip) to one of the services."
|
|
4594
4594
|
);
|
|
4595
4595
|
lines.push("");
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
"Declare it in the app's own launch config at",
|
|
4608
|
-
"`projects/<app>/.monoceros/launch.json`, then start it with",
|
|
4609
|
-
"`monoceros-ctl`. Add or update an entry whenever you set up a",
|
|
4610
|
-
"long-running server. The file travels with the app, so the human can",
|
|
4611
|
-
"restart it later without knowing your start command:"
|
|
4612
|
-
);
|
|
4613
|
-
lines.push("");
|
|
4614
|
-
lines.push("```json");
|
|
4615
|
-
lines.push("{");
|
|
4616
|
-
lines.push(' "configurations": [');
|
|
4617
|
-
lines.push(
|
|
4618
|
-
` { "name": "web", "command": "<the project's start command>", "port": ${input.ports[0]}, "default": true }`
|
|
4619
|
-
);
|
|
4620
|
-
lines.push(" ]");
|
|
4621
|
-
lines.push("}");
|
|
4622
|
-
lines.push("```");
|
|
4623
|
-
lines.push("");
|
|
4596
|
+
const examplePort = input.ports.length > 0 ? String(input.ports[0]) : "<port>";
|
|
4597
|
+
lines.push("## Running a long-running server");
|
|
4598
|
+
lines.push("");
|
|
4599
|
+
lines.push(
|
|
4600
|
+
"When you build something that serves on a port (a web app, an API),",
|
|
4601
|
+
"it must keep running after this session ends. A plain `npm start` (or",
|
|
4602
|
+
"any foreground start) dies the moment the user exits you or closes the",
|
|
4603
|
+
input.ports.length > 0 ? `terminal, and then \`${input.containerName}.localhost${portSuffix}\` returns 502 Bad Gateway.` : "terminal, and the app stops responding."
|
|
4604
|
+
);
|
|
4605
|
+
lines.push("");
|
|
4606
|
+
if (input.ports.length === 0) {
|
|
4624
4607
|
lines.push(
|
|
4625
|
-
"
|
|
4626
|
-
"
|
|
4627
|
-
"
|
|
4628
|
-
"`projects/`; `port` must be a port already exposed on the container."
|
|
4608
|
+
"This container exposes **no ports yet**, so a server has nothing to be",
|
|
4609
|
+
"reached on. Before serving one, ask the user to add a port on the host",
|
|
4610
|
+
"and re-apply - you cannot do this from inside:"
|
|
4629
4611
|
);
|
|
4630
4612
|
lines.push("");
|
|
4631
|
-
lines.push("Start it, stop it, tail its log:");
|
|
4632
|
-
lines.push("");
|
|
4633
4613
|
lines.push("```");
|
|
4634
|
-
lines.push(
|
|
4635
|
-
lines.push(
|
|
4636
|
-
lines.push("monoceros-ctl logs <app>");
|
|
4614
|
+
lines.push(`monoceros add-port ${input.containerName} <port>`);
|
|
4615
|
+
lines.push(`monoceros apply ${input.containerName}`);
|
|
4637
4616
|
lines.push("```");
|
|
4638
4617
|
lines.push("");
|
|
4639
|
-
lines.push(
|
|
4640
|
-
"`start` launches it detached (it survives your session) and, when a",
|
|
4641
|
-
"`port` is set, waits until it actually listens before returning. The",
|
|
4642
|
-
"human can do the same from the host with",
|
|
4643
|
-
`\`monoceros start ${input.containerName} <app>\` / \`monoceros stop ${input.containerName} <app>\`,`,
|
|
4644
|
-
`and follow output with \`monoceros logs ${input.containerName} <app>\`.`
|
|
4645
|
-
);
|
|
4646
|
-
lines.push("");
|
|
4647
|
-
lines.push(
|
|
4648
|
-
"An app can declare several servers (e.g. an API and a web frontend).",
|
|
4649
|
-
'Mark every server that should come up together with `"default": true`;',
|
|
4650
|
-
"`monoceros-ctl start <app>` (no `--target`) then starts the whole default",
|
|
4651
|
-
"set in the order the entries appear in the file, waiting for each",
|
|
4652
|
-
"server's `port` to listen before starting the next - so order an entry",
|
|
4653
|
-
"before anything that depends on it. If one fails to come up, the rest are",
|
|
4654
|
-
"not started. Pass `--target <name>` to start or stop a single one."
|
|
4655
|
-
);
|
|
4656
|
-
lines.push("");
|
|
4657
|
-
lines.push(
|
|
4658
|
-
"The server must listen on `0.0.0.0` (not `127.0.0.1`) on the exposed",
|
|
4659
|
-
"port, or Traefik cannot reach it. You only have the ports already",
|
|
4660
|
-
"declared on the container; if you need another, ask the human to add it",
|
|
4661
|
-
`on the host (\`monoceros add-port ${input.containerName} <port>\`) and re-apply.`
|
|
4662
|
-
);
|
|
4663
|
-
lines.push("");
|
|
4664
4618
|
}
|
|
4619
|
+
lines.push(
|
|
4620
|
+
"Declare the server in the app's own launch config at",
|
|
4621
|
+
"`projects/<app>/.monoceros/launch.json`, then start it with",
|
|
4622
|
+
"`monoceros-ctl`. Add or update an entry whenever you set up a",
|
|
4623
|
+
"long-running server. The file travels with the app, so the human can",
|
|
4624
|
+
"restart it later without knowing your start command:"
|
|
4625
|
+
);
|
|
4626
|
+
lines.push("");
|
|
4627
|
+
lines.push("```json");
|
|
4628
|
+
lines.push("{");
|
|
4629
|
+
lines.push(' "configurations": [');
|
|
4630
|
+
lines.push(
|
|
4631
|
+
` { "name": "web", "command": "<the project's start command>", "port": ${examplePort}, "default": true }`
|
|
4632
|
+
);
|
|
4633
|
+
lines.push(" ]");
|
|
4634
|
+
lines.push("}");
|
|
4635
|
+
lines.push("```");
|
|
4636
|
+
lines.push("");
|
|
4637
|
+
lines.push(
|
|
4638
|
+
"Use whatever start command the project actually uses (`npm run dev`,",
|
|
4639
|
+
"`./mvnw spring-boot:run`, `python manage.py runserver`, `go run .`, \u2026).",
|
|
4640
|
+
"Do not force a language-specific one. `<app>` is the path under",
|
|
4641
|
+
"`projects/`; `port` must be a port exposed on the container."
|
|
4642
|
+
);
|
|
4643
|
+
lines.push("");
|
|
4644
|
+
lines.push("Start it, stop it, tail its log:");
|
|
4645
|
+
lines.push("");
|
|
4646
|
+
lines.push("```");
|
|
4647
|
+
lines.push("monoceros-ctl start <app>");
|
|
4648
|
+
lines.push("monoceros-ctl stop <app>");
|
|
4649
|
+
lines.push("monoceros-ctl logs <app>");
|
|
4650
|
+
lines.push("```");
|
|
4651
|
+
lines.push("");
|
|
4652
|
+
lines.push(
|
|
4653
|
+
"`start` launches it detached (it survives your session) and, when a",
|
|
4654
|
+
"`port` is set, waits until it actually listens before returning. The",
|
|
4655
|
+
"human can do the same from the host with",
|
|
4656
|
+
`\`monoceros start ${input.containerName} <app>\` / \`monoceros stop ${input.containerName} <app>\`,`,
|
|
4657
|
+
`and follow output with \`monoceros logs ${input.containerName} <app>\`.`
|
|
4658
|
+
);
|
|
4659
|
+
lines.push("");
|
|
4660
|
+
lines.push(
|
|
4661
|
+
"An app can declare several servers (e.g. an API and a web frontend).",
|
|
4662
|
+
'Mark every server that should come up together with `"default": true`;',
|
|
4663
|
+
"`monoceros-ctl start <app>` (no `--target`) then starts the whole default",
|
|
4664
|
+
"set in the order the entries appear in the file, waiting for each",
|
|
4665
|
+
"server's `port` to listen before starting the next - so order an entry",
|
|
4666
|
+
"before anything that depends on it. If one fails to come up, the rest are",
|
|
4667
|
+
"not started. Pass `--target <name>` to start or stop a single one."
|
|
4668
|
+
);
|
|
4669
|
+
lines.push("");
|
|
4670
|
+
lines.push(
|
|
4671
|
+
"The server must listen on `0.0.0.0` (not `127.0.0.1`) on the exposed",
|
|
4672
|
+
"port, or Traefik cannot reach it. You only have the ports already",
|
|
4673
|
+
"declared on the container; if you need another, ask the human to add it",
|
|
4674
|
+
`on the host (\`monoceros add-port ${input.containerName} <port>\`) and re-apply.`
|
|
4675
|
+
);
|
|
4676
|
+
lines.push("");
|
|
4665
4677
|
lines.push("## Command reference");
|
|
4666
4678
|
lines.push("");
|
|
4667
4679
|
lines.push(
|
|
@@ -5481,8 +5493,8 @@ function removeRepoFromDoc(doc, urlOrPath) {
|
|
|
5481
5493
|
if (!isMap2(item)) return false;
|
|
5482
5494
|
const url = item.get("url");
|
|
5483
5495
|
if (url === urlOrPath) return true;
|
|
5484
|
-
const
|
|
5485
|
-
const effectivePath = typeof
|
|
5496
|
+
const path35 = item.get("path");
|
|
5497
|
+
const effectivePath = typeof path35 === "string" ? path35 : typeof url === "string" ? deriveRepoName(url) : void 0;
|
|
5486
5498
|
return effectivePath === urlOrPath;
|
|
5487
5499
|
});
|
|
5488
5500
|
if (idx < 0) return false;
|
|
@@ -5625,7 +5637,7 @@ async function runAddRepo(input) {
|
|
|
5625
5637
|
"Missing repo URL. Usage: monoceros add-repo <containername> <url>."
|
|
5626
5638
|
);
|
|
5627
5639
|
}
|
|
5628
|
-
const
|
|
5640
|
+
const path35 = (input.path ?? deriveRepoName(url)).trim();
|
|
5629
5641
|
const hasName = typeof input.gitName === "string" && input.gitName.trim().length > 0;
|
|
5630
5642
|
const hasEmail = typeof input.gitEmail === "string" && input.gitEmail.trim().length > 0;
|
|
5631
5643
|
if (hasName !== hasEmail) {
|
|
@@ -5662,7 +5674,7 @@ async function runAddRepo(input) {
|
|
|
5662
5674
|
const providerToWrite = !canonical && explicitProvider ? explicitProvider : void 0;
|
|
5663
5675
|
const entry2 = {
|
|
5664
5676
|
url,
|
|
5665
|
-
path:
|
|
5677
|
+
path: path35,
|
|
5666
5678
|
...hasName && hasEmail ? {
|
|
5667
5679
|
gitUser: {
|
|
5668
5680
|
name: input.gitName.trim(),
|
|
@@ -8974,7 +8986,7 @@ var CLI_VERSION;
|
|
|
8974
8986
|
var init_version = __esm({
|
|
8975
8987
|
"src/version.ts"() {
|
|
8976
8988
|
"use strict";
|
|
8977
|
-
CLI_VERSION = true ? "1.
|
|
8989
|
+
CLI_VERSION = true ? "1.38.1" : "dev";
|
|
8978
8990
|
}
|
|
8979
8991
|
});
|
|
8980
8992
|
|
|
@@ -12370,12 +12382,220 @@ var init_run3 = __esm({
|
|
|
12370
12382
|
}
|
|
12371
12383
|
});
|
|
12372
12384
|
|
|
12373
|
-
// src/
|
|
12385
|
+
// src/tls/ca.ts
|
|
12386
|
+
import { promises as fs21 } from "fs";
|
|
12374
12387
|
import os3 from "os";
|
|
12388
|
+
import path33 from "path";
|
|
12389
|
+
import forge from "node-forge";
|
|
12390
|
+
function homeDir(monocerosHome2) {
|
|
12391
|
+
return monocerosHome2 ?? monocerosHome();
|
|
12392
|
+
}
|
|
12393
|
+
function randomSerial() {
|
|
12394
|
+
return "00" + forge.util.bytesToHex(forge.random.getBytesSync(16));
|
|
12395
|
+
}
|
|
12396
|
+
function caCommonName() {
|
|
12397
|
+
const host = os3.hostname().replace(/\.local$/, "").trim();
|
|
12398
|
+
return host ? `Monoceros Local CA (${host})` : "Monoceros Local CA";
|
|
12399
|
+
}
|
|
12400
|
+
async function fileExists(p) {
|
|
12401
|
+
try {
|
|
12402
|
+
await fs21.access(p);
|
|
12403
|
+
return true;
|
|
12404
|
+
} catch {
|
|
12405
|
+
return false;
|
|
12406
|
+
}
|
|
12407
|
+
}
|
|
12408
|
+
async function loadOrCreateCa(monocerosHome2) {
|
|
12409
|
+
const dir = path33.join(homeDir(monocerosHome2), CA_DIR);
|
|
12410
|
+
const certPath = path33.join(dir, CA_CERT_FILE);
|
|
12411
|
+
const keyPath = path33.join(dir, CA_KEY_FILE);
|
|
12412
|
+
if (await fileExists(certPath) && await fileExists(keyPath)) {
|
|
12413
|
+
const cert2 = forge.pki.certificateFromPem(
|
|
12414
|
+
await fs21.readFile(certPath, "utf8")
|
|
12415
|
+
);
|
|
12416
|
+
const key = forge.pki.privateKeyFromPem(
|
|
12417
|
+
await fs21.readFile(keyPath, "utf8")
|
|
12418
|
+
);
|
|
12419
|
+
return { cert: cert2, key, certPath, keyPath };
|
|
12420
|
+
}
|
|
12421
|
+
const keys = forge.pki.rsa.generateKeyPair(2048);
|
|
12422
|
+
const cert = forge.pki.createCertificate();
|
|
12423
|
+
cert.publicKey = keys.publicKey;
|
|
12424
|
+
cert.serialNumber = randomSerial();
|
|
12425
|
+
cert.validity.notBefore = /* @__PURE__ */ new Date();
|
|
12426
|
+
cert.validity.notAfter = new Date(
|
|
12427
|
+
cert.validity.notBefore.getTime() + CA_DAYS * 24 * 60 * 60 * 1e3
|
|
12428
|
+
);
|
|
12429
|
+
const attrs = [{ name: "commonName", value: caCommonName() }];
|
|
12430
|
+
cert.setSubject(attrs);
|
|
12431
|
+
cert.setIssuer(attrs);
|
|
12432
|
+
cert.setExtensions([
|
|
12433
|
+
{ name: "basicConstraints", cA: true, critical: true },
|
|
12434
|
+
{ name: "keyUsage", keyCertSign: true, cRLSign: true, critical: true },
|
|
12435
|
+
{ name: "subjectKeyIdentifier" }
|
|
12436
|
+
]);
|
|
12437
|
+
cert.sign(keys.privateKey, forge.md.sha256.create());
|
|
12438
|
+
await fs21.mkdir(dir, { recursive: true });
|
|
12439
|
+
await fs21.writeFile(certPath, forge.pki.certificateToPem(cert), {
|
|
12440
|
+
mode: 420
|
|
12441
|
+
});
|
|
12442
|
+
await fs21.writeFile(keyPath, forge.pki.privateKeyToPem(keys.privateKey), {
|
|
12443
|
+
mode: 384
|
|
12444
|
+
});
|
|
12445
|
+
return { cert, key: keys.privateKey, certPath, keyPath };
|
|
12446
|
+
}
|
|
12447
|
+
function normalizeSans(sans) {
|
|
12448
|
+
return [...new Set(sans.filter((s) => s.length > 0))].sort();
|
|
12449
|
+
}
|
|
12450
|
+
async function readLeafMeta(metaPath) {
|
|
12451
|
+
try {
|
|
12452
|
+
return JSON.parse(await fs21.readFile(metaPath, "utf8"));
|
|
12453
|
+
} catch {
|
|
12454
|
+
return null;
|
|
12455
|
+
}
|
|
12456
|
+
}
|
|
12457
|
+
function leafStillGood(meta, sans) {
|
|
12458
|
+
if (!meta) return false;
|
|
12459
|
+
if (meta.sans.join(",") !== sans.join(",")) return false;
|
|
12460
|
+
const notAfter = new Date(meta.notAfter).getTime();
|
|
12461
|
+
if (Number.isNaN(notAfter)) return false;
|
|
12462
|
+
return notAfter - Date.now() > LEAF_RENEW_BEFORE_MS;
|
|
12463
|
+
}
|
|
12464
|
+
async function ensureLeafCert(opts) {
|
|
12465
|
+
const sans = normalizeSans(opts.sans);
|
|
12466
|
+
const dir = path33.join(homeDir(opts.monocerosHome), CERTS_DIR);
|
|
12467
|
+
const certPath = path33.join(dir, LEAF_CERT_FILE);
|
|
12468
|
+
const keyPath = path33.join(dir, LEAF_KEY_FILE);
|
|
12469
|
+
const metaPath = path33.join(dir, LEAF_META_FILE);
|
|
12470
|
+
const ca = await loadOrCreateCa(opts.monocerosHome);
|
|
12471
|
+
const haveFiles = await fileExists(certPath) && await fileExists(keyPath);
|
|
12472
|
+
if (haveFiles && leafStillGood(await readLeafMeta(metaPath), sans)) {
|
|
12473
|
+
return {
|
|
12474
|
+
caCertPath: ca.certPath,
|
|
12475
|
+
certDir: dir,
|
|
12476
|
+
certFile: LEAF_CERT_FILE,
|
|
12477
|
+
keyFile: LEAF_KEY_FILE
|
|
12478
|
+
};
|
|
12479
|
+
}
|
|
12480
|
+
const keys = forge.pki.rsa.generateKeyPair(2048);
|
|
12481
|
+
const cert = forge.pki.createCertificate();
|
|
12482
|
+
cert.publicKey = keys.publicKey;
|
|
12483
|
+
cert.serialNumber = randomSerial();
|
|
12484
|
+
cert.validity.notBefore = /* @__PURE__ */ new Date();
|
|
12485
|
+
cert.validity.notAfter = new Date(
|
|
12486
|
+
cert.validity.notBefore.getTime() + LEAF_DAYS * 24 * 60 * 60 * 1e3
|
|
12487
|
+
);
|
|
12488
|
+
cert.setSubject([{ name: "commonName", value: "monoceros share" }]);
|
|
12489
|
+
cert.setIssuer(ca.cert.subject.attributes);
|
|
12490
|
+
cert.setExtensions([
|
|
12491
|
+
{ name: "basicConstraints", cA: false },
|
|
12492
|
+
{
|
|
12493
|
+
name: "keyUsage",
|
|
12494
|
+
digitalSignature: true,
|
|
12495
|
+
keyEncipherment: true,
|
|
12496
|
+
critical: true
|
|
12497
|
+
},
|
|
12498
|
+
{ name: "extKeyUsage", serverAuth: true },
|
|
12499
|
+
{
|
|
12500
|
+
name: "subjectAltName",
|
|
12501
|
+
altNames: sans.map(
|
|
12502
|
+
(s) => IPV4_RE2.test(s) ? { type: 7, ip: s } : { type: 2, value: s }
|
|
12503
|
+
)
|
|
12504
|
+
}
|
|
12505
|
+
]);
|
|
12506
|
+
cert.sign(ca.key, forge.md.sha256.create());
|
|
12507
|
+
await fs21.mkdir(dir, { recursive: true });
|
|
12508
|
+
await fs21.writeFile(certPath, forge.pki.certificateToPem(cert), {
|
|
12509
|
+
mode: 420
|
|
12510
|
+
});
|
|
12511
|
+
await fs21.writeFile(keyPath, forge.pki.privateKeyToPem(keys.privateKey), {
|
|
12512
|
+
mode: 384
|
|
12513
|
+
});
|
|
12514
|
+
const meta = {
|
|
12515
|
+
sans,
|
|
12516
|
+
notAfter: cert.validity.notAfter.toISOString()
|
|
12517
|
+
};
|
|
12518
|
+
await fs21.writeFile(metaPath, JSON.stringify(meta), { mode: 420 });
|
|
12519
|
+
return {
|
|
12520
|
+
caCertPath: ca.certPath,
|
|
12521
|
+
certDir: dir,
|
|
12522
|
+
certFile: LEAF_CERT_FILE,
|
|
12523
|
+
keyFile: LEAF_KEY_FILE
|
|
12524
|
+
};
|
|
12525
|
+
}
|
|
12526
|
+
async function provisionShareTls(opts) {
|
|
12527
|
+
return ensureLeafCert(opts);
|
|
12528
|
+
}
|
|
12529
|
+
var CA_DIR, CA_CERT_FILE, CA_KEY_FILE, CERTS_DIR, LEAF_CERT_FILE, LEAF_KEY_FILE, LEAF_META_FILE, CA_DAYS, LEAF_DAYS, LEAF_RENEW_BEFORE_MS, IPV4_RE2;
|
|
12530
|
+
var init_ca = __esm({
|
|
12531
|
+
"src/tls/ca.ts"() {
|
|
12532
|
+
"use strict";
|
|
12533
|
+
init_paths();
|
|
12534
|
+
CA_DIR = "ca";
|
|
12535
|
+
CA_CERT_FILE = "rootCA.pem";
|
|
12536
|
+
CA_KEY_FILE = "rootCA-key.pem";
|
|
12537
|
+
CERTS_DIR = "certs";
|
|
12538
|
+
LEAF_CERT_FILE = "leaf.pem";
|
|
12539
|
+
LEAF_KEY_FILE = "leaf-key.pem";
|
|
12540
|
+
LEAF_META_FILE = "leaf.json";
|
|
12541
|
+
CA_DAYS = 3650;
|
|
12542
|
+
LEAF_DAYS = 397;
|
|
12543
|
+
LEAF_RENEW_BEFORE_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
12544
|
+
IPV4_RE2 = /^(\d{1,3}\.){3}\d{1,3}$/;
|
|
12545
|
+
}
|
|
12546
|
+
});
|
|
12547
|
+
|
|
12548
|
+
// src/share/caddy.ts
|
|
12549
|
+
function renderCaddyfile(sites, certFile, keyFile) {
|
|
12550
|
+
const blocks = sites.map(
|
|
12551
|
+
(s) => `:${s.port} {
|
|
12552
|
+
tls /certs/${certFile} /certs/${keyFile}
|
|
12553
|
+
reverse_proxy http://${s.targetHost}:${s.port}
|
|
12554
|
+
}`
|
|
12555
|
+
);
|
|
12556
|
+
return [
|
|
12557
|
+
"{",
|
|
12558
|
+
" auto_https off",
|
|
12559
|
+
" admin off",
|
|
12560
|
+
" servers {",
|
|
12561
|
+
" protocols h1 h2",
|
|
12562
|
+
" }",
|
|
12563
|
+
"}",
|
|
12564
|
+
...blocks,
|
|
12565
|
+
""
|
|
12566
|
+
].join("\n");
|
|
12567
|
+
}
|
|
12568
|
+
function buildCaddyDockerArgs(input) {
|
|
12569
|
+
const args = ["run", "--rm", "-i", `--network=${input.network}`];
|
|
12570
|
+
for (const port of input.ports) {
|
|
12571
|
+
args.push("-p", `${input.localAddress}:${port}:${port}`);
|
|
12572
|
+
}
|
|
12573
|
+
args.push(
|
|
12574
|
+
"-v",
|
|
12575
|
+
`${input.certDir}:/certs:ro`,
|
|
12576
|
+
"-v",
|
|
12577
|
+
`${input.caddyfilePath}:/etc/caddy/Caddyfile:ro`,
|
|
12578
|
+
CADDY_IMAGE
|
|
12579
|
+
);
|
|
12580
|
+
return args;
|
|
12581
|
+
}
|
|
12582
|
+
var CADDY_IMAGE;
|
|
12583
|
+
var init_caddy = __esm({
|
|
12584
|
+
"src/share/caddy.ts"() {
|
|
12585
|
+
"use strict";
|
|
12586
|
+
CADDY_IMAGE = "caddy:2.11.4";
|
|
12587
|
+
}
|
|
12588
|
+
});
|
|
12589
|
+
|
|
12590
|
+
// src/share/run.ts
|
|
12591
|
+
import os4 from "os";
|
|
12592
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
12375
12593
|
import { consola as consola35 } from "consola";
|
|
12594
|
+
import { promises as fs22 } from "fs";
|
|
12595
|
+
import path34 from "path";
|
|
12376
12596
|
function realHostAddresses() {
|
|
12377
12597
|
let ip;
|
|
12378
|
-
for (const list of Object.values(
|
|
12598
|
+
for (const list of Object.values(os4.networkInterfaces())) {
|
|
12379
12599
|
for (const addr of list ?? []) {
|
|
12380
12600
|
if (addr.family === "IPv4" && !addr.internal) {
|
|
12381
12601
|
ip = addr.address;
|
|
@@ -12384,9 +12604,18 @@ function realHostAddresses() {
|
|
|
12384
12604
|
}
|
|
12385
12605
|
if (ip) break;
|
|
12386
12606
|
}
|
|
12387
|
-
|
|
12388
|
-
|
|
12389
|
-
|
|
12607
|
+
return { ...ip ? { ip } : {}, mdnsName: mdnsHostName() };
|
|
12608
|
+
}
|
|
12609
|
+
function mdnsHostName() {
|
|
12610
|
+
if (process.platform === "darwin") {
|
|
12611
|
+
const res = spawnSync2("scutil", ["--get", "LocalHostName"], {
|
|
12612
|
+
encoding: "utf8"
|
|
12613
|
+
});
|
|
12614
|
+
const name = res.status === 0 ? res.stdout.trim() : "";
|
|
12615
|
+
if (name) return `${name}.local`;
|
|
12616
|
+
}
|
|
12617
|
+
const hn = os4.hostname();
|
|
12618
|
+
return hn.endsWith(".local") ? hn : `${hn}.local`;
|
|
12390
12619
|
}
|
|
12391
12620
|
async function runShare(opts) {
|
|
12392
12621
|
const log = opts.logger ?? {
|
|
@@ -12418,31 +12647,62 @@ async function runShare(opts) {
|
|
|
12418
12647
|
for (const port of ports) {
|
|
12419
12648
|
await preflight({ port, address: SHARE_ADDRESS });
|
|
12420
12649
|
}
|
|
12650
|
+
const { ip, mdnsName } = (opts.hostAddresses ?? realHostAddresses)();
|
|
12651
|
+
const sans = [mdnsName, ip, "localhost", "127.0.0.1"].filter(
|
|
12652
|
+
(s) => typeof s === "string" && s.length > 0
|
|
12653
|
+
);
|
|
12654
|
+
const provisionTls = opts.provisionTls ?? provisionShareTls;
|
|
12655
|
+
const tls = await provisionTls({
|
|
12656
|
+
sans,
|
|
12657
|
+
...opts.monocerosHome !== void 0 ? { monocerosHome: opts.monocerosHome } : {}
|
|
12658
|
+
});
|
|
12659
|
+
const sites = ports.map((port) => ({
|
|
12660
|
+
port,
|
|
12661
|
+
targetHost: base.targetHost
|
|
12662
|
+
}));
|
|
12663
|
+
const home = opts.monocerosHome ?? monocerosHome();
|
|
12664
|
+
const shareDir = path34.join(home, "share");
|
|
12665
|
+
await fs22.mkdir(shareDir, { recursive: true });
|
|
12666
|
+
const caddyfilePath = path34.join(
|
|
12667
|
+
shareDir,
|
|
12668
|
+
`${opts.name}__${opts.app}.Caddyfile`
|
|
12669
|
+
);
|
|
12670
|
+
await fs22.rm(caddyfilePath, { force: true });
|
|
12671
|
+
await fs22.writeFile(
|
|
12672
|
+
caddyfilePath,
|
|
12673
|
+
renderCaddyfile(sites, tls.certFile, tls.keyFile)
|
|
12674
|
+
);
|
|
12421
12675
|
const dockerSpawn = opts.dockerSpawn ?? defaultDockerSpawn;
|
|
12422
|
-
const handles =
|
|
12423
|
-
|
|
12424
|
-
|
|
12676
|
+
const handles = [
|
|
12677
|
+
dockerSpawn(
|
|
12678
|
+
buildCaddyDockerArgs({
|
|
12425
12679
|
localAddress: SHARE_ADDRESS,
|
|
12426
|
-
|
|
12427
|
-
internalPort: port,
|
|
12680
|
+
ports,
|
|
12428
12681
|
network: base.network,
|
|
12429
|
-
|
|
12682
|
+
certDir: tls.certDir,
|
|
12683
|
+
caddyfilePath
|
|
12430
12684
|
})
|
|
12431
12685
|
)
|
|
12432
|
-
|
|
12433
|
-
const
|
|
12434
|
-
const host = ip ?? mdnsName ?? "<host-ip>";
|
|
12686
|
+
];
|
|
12687
|
+
const host = mdnsName ?? ip ?? "<host-ip>";
|
|
12435
12688
|
log.info(
|
|
12436
12689
|
`Sharing ${opts.name}/${opts.app} on the local network (Ctrl+C to stop):`
|
|
12437
12690
|
);
|
|
12438
12691
|
for (const t of ported) {
|
|
12439
|
-
log.info(` ${cyan2(t.name)}
|
|
12692
|
+
log.info(` ${cyan2(t.name)} https://${host}:${t.port}`);
|
|
12440
12693
|
}
|
|
12441
12694
|
if (mdnsName && ip) {
|
|
12442
12695
|
log.info(
|
|
12443
|
-
dim(
|
|
12696
|
+
dim(
|
|
12697
|
+
` also reachable as https://${ip}:<port> if .local does not resolve`
|
|
12698
|
+
)
|
|
12444
12699
|
);
|
|
12445
12700
|
}
|
|
12701
|
+
log.info(
|
|
12702
|
+
dim(
|
|
12703
|
+
` First device? Trust the local CA once so HTTPS is warning-free: ${tls.caCertPath}`
|
|
12704
|
+
)
|
|
12705
|
+
);
|
|
12446
12706
|
const installSignalHandler = opts.installSignalHandler ?? installSigintDefault;
|
|
12447
12707
|
let stopping = false;
|
|
12448
12708
|
const uninstall = installSignalHandler(() => {
|
|
@@ -12466,6 +12726,9 @@ var init_run4 = __esm({
|
|
|
12466
12726
|
init_resolve2();
|
|
12467
12727
|
init_port_check2();
|
|
12468
12728
|
init_run3();
|
|
12729
|
+
init_ca();
|
|
12730
|
+
init_caddy();
|
|
12731
|
+
init_paths();
|
|
12469
12732
|
init_format();
|
|
12470
12733
|
SHARE_ADDRESS = "0.0.0.0";
|
|
12471
12734
|
}
|
|
@@ -12483,7 +12746,7 @@ var init_share = __esm({
|
|
|
12483
12746
|
meta: {
|
|
12484
12747
|
name: "share",
|
|
12485
12748
|
group: "discovery",
|
|
12486
|
-
description: "Expose an app's ports to the local network (phone, tablet, other devices) so any device can open it - reached via the host's LAN IP (or `.local` name). Every target in the app's launch.json that declares a port is shared. Foreground: Ctrl+C stops sharing. See ADR 0030."
|
|
12749
|
+
description: "Expose an app's ports to the local network (phone, tablet, other devices) over HTTPS so any device can open it - reached via the host's LAN IP (or `.local` name). TLS is served with a machine-local CA; trust the printed rootCA.pem once per device for warning-free HTTPS (and a working PWA secure context). Every target in the app's launch.json that declares a port is shared. Foreground: Ctrl+C stops sharing. See ADR 0030 and 0033."
|
|
12487
12750
|
},
|
|
12488
12751
|
args: {
|
|
12489
12752
|
name: {
|
|
@@ -13267,7 +13530,7 @@ var init_prune = __esm({
|
|
|
13267
13530
|
});
|
|
13268
13531
|
|
|
13269
13532
|
// src/upgrade/index.ts
|
|
13270
|
-
import { existsSync as existsSync21, promises as
|
|
13533
|
+
import { existsSync as existsSync21, promises as fs23 } from "fs";
|
|
13271
13534
|
import { consola as consola41 } from "consola";
|
|
13272
13535
|
async function fetchRuntimeVersions() {
|
|
13273
13536
|
const tokenUrl = `https://ghcr.io/token?service=ghcr.io&scope=repository:${RUNTIME_REPO}:pull`;
|
|
@@ -13384,10 +13647,10 @@ async function runUpgrade(opts) {
|
|
|
13384
13647
|
for (const name of targets) {
|
|
13385
13648
|
const ymlPath = containerConfigPath(name, home);
|
|
13386
13649
|
if (!existsSync21(ymlPath)) continue;
|
|
13387
|
-
const raw = await
|
|
13650
|
+
const raw = await fs23.readFile(ymlPath, "utf8");
|
|
13388
13651
|
const updated = setRuntimeVersion(raw, pinVersion);
|
|
13389
13652
|
if (updated !== raw) {
|
|
13390
|
-
await
|
|
13653
|
+
await fs23.writeFile(ymlPath, updated);
|
|
13391
13654
|
bumped += 1;
|
|
13392
13655
|
logger.info(`Pinned '${name}' to runtime ${pinVersion}.`);
|
|
13393
13656
|
}
|
|
@@ -13429,7 +13692,7 @@ function formatPruneLine(prune) {
|
|
|
13429
13692
|
}
|
|
13430
13693
|
async function listContainerNames2(home) {
|
|
13431
13694
|
try {
|
|
13432
|
-
const entries = await
|
|
13695
|
+
const entries = await fs23.readdir(containerConfigsDir(home));
|
|
13433
13696
|
return entries.filter((e) => e.endsWith(".yml")).map((e) => e.slice(0, -".yml".length));
|
|
13434
13697
|
} catch {
|
|
13435
13698
|
return [];
|
|
@@ -13844,25 +14107,25 @@ function detectHelpRequest(argv, main2) {
|
|
|
13844
14107
|
const separatorIdx = argv.indexOf("--");
|
|
13845
14108
|
if (helpIdx === -1) return null;
|
|
13846
14109
|
if (separatorIdx !== -1 && separatorIdx < helpIdx) return null;
|
|
13847
|
-
const
|
|
14110
|
+
const path35 = [];
|
|
13848
14111
|
const tokens = argv.slice(
|
|
13849
14112
|
0,
|
|
13850
14113
|
separatorIdx === -1 ? argv.length : separatorIdx
|
|
13851
14114
|
);
|
|
13852
14115
|
let cursor = main2;
|
|
13853
14116
|
const mainName = (main2.meta ?? {}).name ?? "monoceros";
|
|
13854
|
-
|
|
14117
|
+
path35.push(mainName);
|
|
13855
14118
|
for (const tok of tokens) {
|
|
13856
14119
|
if (tok.startsWith("-")) continue;
|
|
13857
14120
|
const subs = cursor.subCommands ?? {};
|
|
13858
14121
|
if (tok in subs) {
|
|
13859
14122
|
cursor = subs[tok];
|
|
13860
|
-
|
|
14123
|
+
path35.push(tok);
|
|
13861
14124
|
continue;
|
|
13862
14125
|
}
|
|
13863
14126
|
break;
|
|
13864
14127
|
}
|
|
13865
|
-
return { path:
|
|
14128
|
+
return { path: path35, cmd: cursor };
|
|
13866
14129
|
}
|
|
13867
14130
|
async function maybeRenderHelp(argv, main2) {
|
|
13868
14131
|
const hit = detectHelpRequest(argv, main2);
|