@monster-orange/den 0.2.8 → 0.3.0
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/CHANGELOG.md +1547 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +507 -4
- package/make/den.mk +19 -4
- package/make/journeys.mk +2 -1
- package/make/local_dev.mk +6 -13
- package/make/secrets-1password.mk +18 -0
- package/make/test.mk +1 -1
- package/package.json +9 -2
- package/schemas/den.plugin.v1.json +156 -0
- package/schemas/den.theme.v1.json +269 -0
- package/scripts/auth-configure.ts +16 -8
- package/scripts/heal-config.ts +0 -5
- package/scripts/setup-1password.ts +41 -19
- package/scripts/setup-secrets.ts +67 -101
- package/src/pillars/api/global-setup.ts +1 -1
- package/src/pillars/journeys/sandbox.ts +23 -18
- package/src/pillars/journeys/tools/browser-session.ts +2 -2
- package/src/scripts/setup-default-workspace.ts +2 -2
- package/src/shared/auth/headless-login.ts +1 -1
- package/src/shared/auth/sudo-vault.ts +93 -31
- package/src/shared/auth/token-providers/index.ts +7 -3
- package/src/shared/brand.ts +7 -0
- package/src/shared/config-error.ts +33 -3
- package/src/shared/constants.ts +3 -24
- package/src/shared/den-config.ts +141 -5
- package/src/shared/edge/config.ts +147 -21
- package/src/shared/edge/exec.ts +31 -0
- package/src/shared/edge/included.ts +169 -0
- package/src/shared/edge/render.ts +69 -14
- package/src/shared/edge/runtime.ts +128 -121
- package/src/shared/edge/status.ts +68 -8
- package/src/shared/edge/tls.ts +328 -0
- package/src/shared/edge/types.ts +25 -9
- package/src/shared/environment-manager/default-workspace.ts +16 -1
- package/src/shared/environment-manager/health.ts +73 -38
- package/src/shared/environment-manager/scope.ts +168 -0
- package/src/shared/environment-manager/service-runner.ts +789 -43
- package/src/shared/environment-manager/up-targets.ts +132 -0
- package/src/shared/health/rollup.ts +48 -0
- package/src/shared/open-url.ts +21 -2
- package/src/shared/org-sources.ts +78 -59
- package/src/shared/org.ts +53 -5
- package/src/shared/plugins/manifest.ts +406 -0
- package/src/shared/plugins/store.ts +358 -0
- package/src/shared/process/legacy-adopt.ts +285 -0
- package/src/shared/process/process-group.ts +202 -0
- package/src/shared/process/spawn-streaming.ts +7 -0
- package/src/shared/secrets/doppler.ts +1 -1
- package/src/shared/secrets/index.ts +279 -50
- package/src/shared/secrets/none.ts +56 -0
- package/src/shared/secrets/strategies.ts +202 -0
- package/src/shared/secrets/types.ts +1 -1
- package/src/shared/smoke/git-remote.ts +40 -0
- package/src/shared/stacks/manifest-commands.ts +1 -1
- package/src/shared/stacks/namespace.ts +178 -0
- package/src/shared/stacks/owners.ts +303 -0
- package/src/shared/stacks/render.ts +8 -3
- package/src/shared/stacks/runtime.ts +100 -16
- package/src/shared/stacks/status.ts +63 -12
- package/src/shared/strategies/registry.ts +89 -0
- package/src/shared/theme/contract.ts +538 -0
- package/src/shared/types/index.ts +12 -3
- package/src/ui/cli/create.ts +24 -7
- package/src/ui/cli/doctor-command.ts +131 -0
- package/src/ui/cli/edge-command.ts +56 -10
- package/src/ui/cli/flag-parser.ts +7 -2
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +548 -60
- package/src/ui/cli/notice.ts +20 -0
- package/src/ui/cli/plugin-command.ts +849 -0
- package/src/ui/cli/stack-command.ts +63 -2
- package/src/ui/cli/test-runner.ts +11 -9
- package/src/ui/cli/theme-command.ts +136 -0
- package/src/ui/cli/web-command.ts +47 -23
- package/src/ui/mcp/api.ts +28 -2
- package/src/ui/mcp/scope.ts +22 -1
- package/src/ui/mcp/tools.ts +50 -7
- package/src/ui/server/appearance-prefs.ts +95 -0
- package/src/ui/server/daemon-reaper.ts +29 -1
- package/src/ui/server/engine-stamp.ts +83 -0
- package/src/ui/server/github-service.ts +23 -44
- package/src/ui/server/graph-page.ts +20 -0
- package/src/ui/server/graph-prefs.ts +44 -1
- package/src/ui/server/http-errors.ts +293 -0
- package/src/ui/server/http-helpers.ts +22 -3
- package/src/ui/server/index.ts +532 -51
- package/src/ui/server/public/appearance.js +531 -0
- package/src/ui/server/public/edge-page.js +248 -0
- package/src/ui/server/public/edge.html +251 -0
- package/src/ui/server/public/environments.html +65 -83
- package/src/ui/server/public/error-page.js +247 -0
- package/src/ui/server/public/error.html +260 -0
- package/src/ui/server/public/graph-model.js +332 -14
- package/src/ui/server/public/graph.css +141 -47
- package/src/ui/server/public/graph.js +223 -44
- package/src/ui/server/public/header.js +396 -600
- package/src/ui/server/public/health-fit.js +248 -0
- package/src/ui/server/public/health-rollup.js +128 -0
- package/src/ui/server/public/i18n/es.js +287 -0
- package/src/ui/server/public/i18n/ja.js +276 -0
- package/src/ui/server/public/index.html +139 -438
- package/src/ui/server/public/local-dev.html +3468 -4044
- package/src/ui/server/public/logs.html +71 -208
- package/src/ui/server/public/panel.js +14 -2
- package/src/ui/server/public/release.html +175 -389
- package/src/ui/server/public/services.html +596 -770
- package/src/ui/server/public/settings-page.js +759 -0
- package/src/ui/server/public/settings-registry.js +521 -0
- package/src/ui/server/public/settings.html +590 -0
- package/src/ui/server/public/shell.js +3869 -0
- package/src/ui/server/public/topbar.js +1413 -0
- package/src/ui/server/public/version-label.js +12 -0
- package/src/ui/server/public/workspace-picker.js +74 -34
- package/src/ui/server/readonly-routes.ts +12 -1
- package/src/ui/server/services-list.ts +94 -17
- package/src/ui/server/settings-distribution.ts +74 -0
- package/src/ui/server/shell-prefs.ts +192 -0
- package/src/ui/server/theme-routes.ts +292 -0
- package/src/ui/server/theme-runtime.ts +407 -0
- package/src/ui/server/ui-assets.ts +16 -1
- package/src/ui/server/ui-language.ts +50 -0
- package/src/ui/server/workspace-routes.ts +45 -3
- package/themes/den/den-theme.json +72 -0
- package/themes/den/favicon.ico +0 -0
- package/themes/den/mark.svg +6 -0
|
@@ -6,7 +6,13 @@
|
|
|
6
6
|
import * as fs from "fs";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import { certNames, edgeDomains, edgeNames, routeUrl } from "./render.js";
|
|
9
|
+
import {
|
|
10
|
+
includedRouters,
|
|
11
|
+
stackRouters,
|
|
12
|
+
type IncludedRouters,
|
|
13
|
+
} from "./included.js";
|
|
9
14
|
import { caTrusted, containerState } from "./runtime.js";
|
|
15
|
+
import { caDir } from "./tls.js";
|
|
10
16
|
import type { EdgeConfig, EdgePorts } from "./types.js";
|
|
11
17
|
|
|
12
18
|
export interface EdgeStatus {
|
|
@@ -23,8 +29,34 @@ export interface EdgeStatus {
|
|
|
23
29
|
/** Names on the issued cert, or the planned ones when none is issued yet. */
|
|
24
30
|
certNames: string[];
|
|
25
31
|
certIssued: boolean;
|
|
26
|
-
/** Whether the CA is trusted on this machine (null: can't tell). */
|
|
32
|
+
/** Whether the CA is trusted on this machine (null: can't tell, or none). */
|
|
27
33
|
caTrusted: boolean | null;
|
|
34
|
+
/**
|
|
35
|
+
* The edge.tls strategy; whether nothing chose it (implicit mkcert);
|
|
36
|
+
* where it came from (edge.tls, the legacy caroot/issue_cert keys, or
|
|
37
|
+
* neither); and the CA directory (mkcert: the one it issues from; files:
|
|
38
|
+
* `ca` when set; none: null).
|
|
39
|
+
*/
|
|
40
|
+
tls: {
|
|
41
|
+
strategy: EdgeConfig["tls"]["strategy"];
|
|
42
|
+
implicit: boolean;
|
|
43
|
+
from: EdgeConfig["tlsFrom"];
|
|
44
|
+
ca: string | null;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Routers in the included dynamic files (`edge include`), each with its
|
|
48
|
+
* source file: Traefik serves them, Den doesn't generate them.
|
|
49
|
+
*/
|
|
50
|
+
included: IncludedRouters["routers"];
|
|
51
|
+
/** Included files that could not be parsed (listed, never fatal). */
|
|
52
|
+
includedErrors: IncludedRouters["errors"];
|
|
53
|
+
/**
|
|
54
|
+
* Routers in the stacks' route files (`stack-*.yml`, written by `stack
|
|
55
|
+
* up` into the edge's dynamic directory), each with its file (0.3.0-dev.32).
|
|
56
|
+
*/
|
|
57
|
+
stacks: IncludedRouters["routers"];
|
|
58
|
+
/** Stack route files that could not be parsed (listed, never fatal). */
|
|
59
|
+
stackErrors: IncludedRouters["errors"];
|
|
28
60
|
}
|
|
29
61
|
|
|
30
62
|
export interface EdgeStatusDeps {
|
|
@@ -32,6 +64,10 @@ export interface EdgeStatusDeps {
|
|
|
32
64
|
caTrusted: (cfg: EdgeConfig) => boolean | null;
|
|
33
65
|
/** Names on the issued cert (its stamp file), or null when none is issued. */
|
|
34
66
|
issuedNames: (cfg: EdgeConfig) => string[] | null;
|
|
67
|
+
/** The included files' routers (read from disk by default). */
|
|
68
|
+
included?: (cfg: EdgeConfig) => IncludedRouters;
|
|
69
|
+
/** The stacks' route files' routers (read from disk by default). */
|
|
70
|
+
stacks?: (cfg: EdgeConfig) => IncludedRouters;
|
|
35
71
|
}
|
|
36
72
|
|
|
37
73
|
/** The names stamp `ensureCerts` writes beside the cert. */
|
|
@@ -45,8 +81,19 @@ const defaultDeps: EdgeStatusDeps = {
|
|
|
45
81
|
containerState,
|
|
46
82
|
caTrusted,
|
|
47
83
|
issuedNames: issuedCertNames,
|
|
84
|
+
included: includedRouters,
|
|
85
|
+
stacks: stackRouters,
|
|
48
86
|
};
|
|
49
87
|
|
|
88
|
+
/** Den's own routes: `<name>.<domain>` → upstream (no Docker asked). */
|
|
89
|
+
export function edgeRoutes(cfg: EdgeConfig): EdgeStatus["routes"] {
|
|
90
|
+
return Object.entries(cfg.routes).map(([name, upstream]) => ({
|
|
91
|
+
name,
|
|
92
|
+
url: routeUrl(name, cfg),
|
|
93
|
+
upstream,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
50
97
|
export function edgeStatus(
|
|
51
98
|
cfg: EdgeConfig,
|
|
52
99
|
deps: EdgeStatusDeps = defaultDeps,
|
|
@@ -54,6 +101,8 @@ export function edgeStatus(
|
|
|
54
101
|
const n = edgeNames(cfg.org);
|
|
55
102
|
const state = deps.containerState(n.container);
|
|
56
103
|
const issued = deps.issuedNames(cfg);
|
|
104
|
+
const inc = (deps.included ?? includedRouters)(cfg);
|
|
105
|
+
const stk = (deps.stacks ?? stackRouters)(cfg);
|
|
57
106
|
return {
|
|
58
107
|
org: cfg.org,
|
|
59
108
|
container: n.container,
|
|
@@ -62,13 +111,24 @@ export function edgeStatus(
|
|
|
62
111
|
ports: cfg.ports,
|
|
63
112
|
network: n.network,
|
|
64
113
|
domains: edgeDomains(cfg),
|
|
65
|
-
routes:
|
|
66
|
-
|
|
67
|
-
url: routeUrl(name, cfg),
|
|
68
|
-
upstream,
|
|
69
|
-
})),
|
|
70
|
-
certNames: issued ?? certNames(cfg),
|
|
114
|
+
routes: edgeRoutes(cfg),
|
|
115
|
+
certNames: issued ?? (cfg.tls.strategy === "none" ? [] : certNames(cfg)),
|
|
71
116
|
certIssued: issued !== null,
|
|
72
|
-
caTrusted: deps.caTrusted(cfg),
|
|
117
|
+
caTrusted: cfg.tls.strategy === "none" ? null : deps.caTrusted(cfg),
|
|
118
|
+
tls: {
|
|
119
|
+
strategy: cfg.tls.strategy,
|
|
120
|
+
implicit: cfg.tlsFrom === "implicit",
|
|
121
|
+
from: cfg.tlsFrom,
|
|
122
|
+
ca:
|
|
123
|
+
cfg.tls.strategy === "mkcert"
|
|
124
|
+
? caDir(cfg)
|
|
125
|
+
: cfg.tls.strategy === "files"
|
|
126
|
+
? cfg.tls.ca
|
|
127
|
+
: null,
|
|
128
|
+
},
|
|
129
|
+
included: inc.routers,
|
|
130
|
+
includedErrors: inc.errors,
|
|
131
|
+
stacks: stk.routers,
|
|
132
|
+
stackErrors: stk.errors,
|
|
73
133
|
};
|
|
74
134
|
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The edge's TLS strategies (TLS-1a), registered through the generic
|
|
3
|
+
* strategy registry (src/shared/strategies/registry.ts) as kind "tls",
|
|
4
|
+
* exactly as a plugin would register one: `{ id, kind, detect(),
|
|
5
|
+
* capabilities }`.
|
|
6
|
+
*
|
|
7
|
+
* - `none`: http only. No https entrypoint, no cert work, no mkcert.
|
|
8
|
+
* - `mkcert`: the edge issues its own wildcard cert from a local CA, its
|
|
9
|
+
* own under the state dir (minted on first use) or `ca`, an existing one
|
|
10
|
+
* it never mints or uninstalls. The only strategy with `trust`/`untrust`.
|
|
11
|
+
* - `files`: the distribution's included dynamic config and mounts hold the
|
|
12
|
+
* certs; Den issues none. `ca` names the CA that signed them, so health
|
|
13
|
+
* checks trust it.
|
|
14
|
+
*
|
|
15
|
+
* `secret` (cert and key from the selected secrets strategy) and `acme` are
|
|
16
|
+
* later plugins; they register here the same way.
|
|
17
|
+
*
|
|
18
|
+
* Capabilities:
|
|
19
|
+
* - `https`: the edge has an https entrypoint;
|
|
20
|
+
* - `issuesCert`: Den issues the edge's own cert (the TLS store it renders);
|
|
21
|
+
* - `ensure(cfg)`: make sure the cert exists for certNames(cfg);
|
|
22
|
+
* - `trust(cfg)` / `untrust(cfg)`: mkcert only;
|
|
23
|
+
* - `status(cfg)`: the names on the cert and whether its CA is trusted.
|
|
24
|
+
*/
|
|
25
|
+
import * as fs from "fs";
|
|
26
|
+
import * as path from "path";
|
|
27
|
+
import {
|
|
28
|
+
getStrategy,
|
|
29
|
+
onPath,
|
|
30
|
+
registerStrategy,
|
|
31
|
+
strategyIds,
|
|
32
|
+
type DetectResult,
|
|
33
|
+
type StrategyEntry,
|
|
34
|
+
} from "../strategies/registry.js";
|
|
35
|
+
import { must, sh } from "./exec.js";
|
|
36
|
+
import { certNames } from "./render.js";
|
|
37
|
+
import type { EdgeConfig, EdgeTls } from "./types.js";
|
|
38
|
+
|
|
39
|
+
export const TLS_KIND = "tls";
|
|
40
|
+
|
|
41
|
+
export type TlsEnsureResult =
|
|
42
|
+
| {
|
|
43
|
+
kind: "cert";
|
|
44
|
+
certFile: string;
|
|
45
|
+
keyFile: string;
|
|
46
|
+
issued: boolean;
|
|
47
|
+
names: string[];
|
|
48
|
+
}
|
|
49
|
+
| { kind: "dynamic" | "none"; issued: false; names: [] };
|
|
50
|
+
|
|
51
|
+
export interface TlsCapabilities {
|
|
52
|
+
label: string;
|
|
53
|
+
https: boolean;
|
|
54
|
+
issuesCert: boolean;
|
|
55
|
+
ensure(cfg: EdgeConfig): TlsEnsureResult;
|
|
56
|
+
trust?(cfg: EdgeConfig): number;
|
|
57
|
+
untrust?(cfg: EdgeConfig): number;
|
|
58
|
+
status(cfg: EdgeConfig): { names: string[]; trusted: boolean | null };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type TlsStrategy = StrategyEntry<typeof TLS_KIND, TlsCapabilities>;
|
|
62
|
+
|
|
63
|
+
// ── The CA ───────────────────────────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
/** The configured CA (`edge.tls.ca`, formerly `edge.caroot`), or null. */
|
|
66
|
+
export function tlsCa(cfg: EdgeConfig): string | null {
|
|
67
|
+
return cfg.tls.strategy === "none" ? null : cfg.tls.ca;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function caDir(cfg: EdgeConfig): string {
|
|
71
|
+
return tlsCa(cfg) ?? path.join(cfg.stateDir, "ca");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** True when the edge uses an existing CA (`ca`) rather than its own. */
|
|
75
|
+
export function sharedCa(cfg: EdgeConfig): boolean {
|
|
76
|
+
return tlsCa(cfg) !== null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* A shared CA must already exist with its key: mkcert silently creates a new
|
|
81
|
+
* CA in an empty CAROOT, and devices that trust the shared one would not
|
|
82
|
+
* trust that. The org's own CA may be created on first use.
|
|
83
|
+
*/
|
|
84
|
+
export function assertCaUsable(cfg: EdgeConfig): void {
|
|
85
|
+
if (!sharedCa(cfg)) return;
|
|
86
|
+
const dir = caDir(cfg);
|
|
87
|
+
if (!fs.existsSync(dir))
|
|
88
|
+
throw new Error(
|
|
89
|
+
`edge.tls.ca ${dir} does not exist. The edge never creates a CA there; ` +
|
|
90
|
+
`point it at the existing CA (\`mkcert -CAROOT\`).`,
|
|
91
|
+
);
|
|
92
|
+
for (const f of ["rootCA.pem", "rootCA-key.pem"])
|
|
93
|
+
if (!fs.existsSync(path.join(dir, f)))
|
|
94
|
+
throw new Error(
|
|
95
|
+
`edge.tls.ca ${dir} has no ${f}. Refusing to run: mkcert would mint a ` +
|
|
96
|
+
`new CA that other devices don't trust.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Whether the CA is in the macOS system keychain (null: can't tell). */
|
|
101
|
+
export function caTrusted(cfg: EdgeConfig): boolean | null {
|
|
102
|
+
if (cfg.tls.strategy === "none") return null;
|
|
103
|
+
const pem = path.join(caDir(cfg), "rootCA.pem");
|
|
104
|
+
if (!fs.existsSync(pem)) return false;
|
|
105
|
+
if (process.platform !== "darwin") return null;
|
|
106
|
+
return sh("security", ["verify-cert", "-c", pem]).code === 0;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── mkcert ───────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
export const MKCERT_MISSING = "mkcert is not installed";
|
|
112
|
+
|
|
113
|
+
/** detect() first: a missing mkcert is a reason, never a spawn error. */
|
|
114
|
+
function requireMkcert(): void {
|
|
115
|
+
const d = mkcert.detect();
|
|
116
|
+
if (!d.available)
|
|
117
|
+
throw new Error(
|
|
118
|
+
`${d.reason}. Install it (https://github.com/FiloSottile/mkcert, e.g. ` +
|
|
119
|
+
"`brew install mkcert`), or set edge.tls to none or files.",
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function mkcertEnv(cfg: EdgeConfig): NodeJS.ProcessEnv {
|
|
124
|
+
return { CAROOT: caDir(cfg) };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const NO_CERT = { issued: false, names: [] } as const;
|
|
128
|
+
|
|
129
|
+
const none: TlsStrategy = {
|
|
130
|
+
id: "none",
|
|
131
|
+
kind: TLS_KIND,
|
|
132
|
+
detect: () => ({ available: true, reason: "http only" }),
|
|
133
|
+
capabilities: {
|
|
134
|
+
label: "none (http only)",
|
|
135
|
+
https: false,
|
|
136
|
+
issuesCert: false,
|
|
137
|
+
ensure: () => ({ kind: "none", ...NO_CERT, names: [] }),
|
|
138
|
+
status: () => ({ names: [], trusted: null }),
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const mkcert: TlsStrategy = {
|
|
143
|
+
id: "mkcert",
|
|
144
|
+
kind: TLS_KIND,
|
|
145
|
+
detect: (env: NodeJS.ProcessEnv = process.env): DetectResult =>
|
|
146
|
+
onPath("mkcert", env)
|
|
147
|
+
? { available: true }
|
|
148
|
+
: { available: false, reason: MKCERT_MISSING },
|
|
149
|
+
capabilities: {
|
|
150
|
+
label: "mkcert",
|
|
151
|
+
https: true,
|
|
152
|
+
issuesCert: true,
|
|
153
|
+
/** Issue (or re-issue when names changed) the edge cert from the CA. */
|
|
154
|
+
ensure(cfg) {
|
|
155
|
+
const names = certNames(cfg);
|
|
156
|
+
assertCaUsable(cfg);
|
|
157
|
+
const dir = path.join(cfg.stateDir, "certs");
|
|
158
|
+
const certFile = path.join(dir, "edge.pem");
|
|
159
|
+
const keyFile = path.join(dir, "edge-key.pem");
|
|
160
|
+
const stamp = path.join(dir, "names.txt");
|
|
161
|
+
const want = names.join("\n");
|
|
162
|
+
if (
|
|
163
|
+
fs.existsSync(certFile) &&
|
|
164
|
+
fs.existsSync(stamp) &&
|
|
165
|
+
fs.readFileSync(stamp, "utf-8") === want
|
|
166
|
+
)
|
|
167
|
+
return { kind: "cert", certFile, keyFile, issued: false, names };
|
|
168
|
+
requireMkcert();
|
|
169
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
170
|
+
if (!sharedCa(cfg))
|
|
171
|
+
fs.mkdirSync(caDir(cfg), { recursive: true, mode: 0o700 });
|
|
172
|
+
must("mkcert", ["-cert-file", certFile, "-key-file", keyFile, ...names], {
|
|
173
|
+
env: mkcertEnv(cfg),
|
|
174
|
+
});
|
|
175
|
+
fs.mkdirSync(path.dirname(stamp), { recursive: true });
|
|
176
|
+
fs.writeFileSync(stamp, want, "utf-8");
|
|
177
|
+
return { kind: "cert", certFile, keyFile, issued: true, names };
|
|
178
|
+
},
|
|
179
|
+
/** Install the CA into the system + browser trust stores (prompts). */
|
|
180
|
+
trust(cfg) {
|
|
181
|
+
assertCaUsable(cfg);
|
|
182
|
+
requireMkcert();
|
|
183
|
+
if (!sharedCa(cfg))
|
|
184
|
+
fs.mkdirSync(caDir(cfg), { recursive: true, mode: 0o700 });
|
|
185
|
+
return sh("mkcert", ["-install"], { env: mkcertEnv(cfg), inherit: true })
|
|
186
|
+
.code;
|
|
187
|
+
},
|
|
188
|
+
untrust(cfg) {
|
|
189
|
+
if (sharedCa(cfg))
|
|
190
|
+
throw new Error(
|
|
191
|
+
`edge.tls.ca is a shared CA (${caDir(cfg)}); other tools and devices ` +
|
|
192
|
+
`rely on it being trusted, so the edge never uninstalls it.`,
|
|
193
|
+
);
|
|
194
|
+
requireMkcert();
|
|
195
|
+
return sh("mkcert", ["-uninstall"], {
|
|
196
|
+
env: mkcertEnv(cfg),
|
|
197
|
+
inherit: true,
|
|
198
|
+
}).code;
|
|
199
|
+
},
|
|
200
|
+
status: (cfg) => ({ names: certNames(cfg), trusted: caTrusted(cfg) }),
|
|
201
|
+
},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const files: TlsStrategy = {
|
|
205
|
+
id: "files",
|
|
206
|
+
kind: TLS_KIND,
|
|
207
|
+
detect: () => ({ available: true }),
|
|
208
|
+
capabilities: {
|
|
209
|
+
label: "files (included config)",
|
|
210
|
+
https: true,
|
|
211
|
+
issuesCert: false,
|
|
212
|
+
ensure: () => ({ kind: "dynamic", ...NO_CERT, names: [] }),
|
|
213
|
+
status: (cfg) => ({ names: [], trusted: caTrusted(cfg) }),
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
for (const s of [none, mkcert, files]) registerStrategy(s);
|
|
218
|
+
|
|
219
|
+
/** Every registered TLS strategy id (built-ins first). */
|
|
220
|
+
export function tlsStrategyIds(): string[] {
|
|
221
|
+
return strategyIds(TLS_KIND);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** The registered TLS strategy `id`, or undefined. */
|
|
225
|
+
export function tlsStrategy(id: string): TlsStrategy | undefined {
|
|
226
|
+
return getStrategy<typeof TLS_KIND, TlsCapabilities>(TLS_KIND, id);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** The strategy the edge config selects (always registered once parsed). */
|
|
230
|
+
export function tlsStrategyFor(cfg: EdgeConfig): TlsStrategy {
|
|
231
|
+
const s = tlsStrategy(cfg.tls.strategy);
|
|
232
|
+
if (!s)
|
|
233
|
+
throw new Error(`TLS strategy "${cfg.tls.strategy}" is not registered`);
|
|
234
|
+
return s;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** True when the edge serves https (every strategy but none). */
|
|
238
|
+
export function servesHttps(cfg: EdgeConfig): boolean {
|
|
239
|
+
return cfg.tls.strategy !== "none";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** True when Den issues the edge's own cert (mkcert). */
|
|
243
|
+
export function issuesCert(cfg: EdgeConfig): boolean {
|
|
244
|
+
return cfg.tls.strategy === "mkcert";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const TRUST_NEEDS_MKCERT =
|
|
248
|
+
"edge trust needs edge.tls.strategy: mkcert (or files with a ca)";
|
|
249
|
+
|
|
250
|
+
function trustRefusal(cfg: EdgeConfig, verb: "trust" | "untrust"): Error {
|
|
251
|
+
return new Error(
|
|
252
|
+
`${TRUST_NEEDS_MKCERT.replace("trust", verb)} (this edge's is ${cfg.tls.strategy}: ` +
|
|
253
|
+
(cfg.tls.strategy === "none"
|
|
254
|
+
? "it serves http only, there is no CA to trust)."
|
|
255
|
+
: "its certs come from your included config; trust their CA yourself)."),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Trust capabilities for the edge: mkcert's own, or, for `files` with an
|
|
261
|
+
* existing `ca` (the legacy caroot + issue_cert: false shape), mkcert's
|
|
262
|
+
* against that CA: `edge trust` installs it, as caroot always did, and
|
|
263
|
+
* untrust refuses it as a shared CA.
|
|
264
|
+
*/
|
|
265
|
+
function trustCapabilities(cfg: EdgeConfig): TlsCapabilities | null {
|
|
266
|
+
const c = tlsStrategyFor(cfg).capabilities;
|
|
267
|
+
if (c.trust) return c;
|
|
268
|
+
if (cfg.tls.strategy === "files" && sharedCa(cfg)) return mkcert.capabilities;
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** `edge trust`: mkcert, or files with an existing ca. */
|
|
273
|
+
export function edgeTrust(cfg: EdgeConfig): number {
|
|
274
|
+
const t = trustCapabilities(cfg)?.trust;
|
|
275
|
+
if (!t) throw trustRefusal(cfg, "trust");
|
|
276
|
+
return t(cfg);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** `edge untrust`: mkcert only, and never for a shared CA. */
|
|
280
|
+
export function edgeUntrust(cfg: EdgeConfig): number {
|
|
281
|
+
const u = trustCapabilities(cfg)?.untrust;
|
|
282
|
+
if (!u) throw trustRefusal(cfg, "untrust");
|
|
283
|
+
return u(cfg);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ── Config warnings ──────────────────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
export const IMPLICIT_TLS_WARNING =
|
|
289
|
+
"edge.tls is not set; defaulting to mkcert with a local CA. Set edge.tls explicitly.";
|
|
290
|
+
|
|
291
|
+
/** The explicit `edge.tls` line that says what `tls` says. */
|
|
292
|
+
export function tlsYaml(tls: EdgeTls): string {
|
|
293
|
+
if (tls.strategy === "none") return "tls: none";
|
|
294
|
+
const ca = tls.ca ? `, ca: ${tls.ca}` : "";
|
|
295
|
+
if (tls.strategy === "mkcert") return `tls: { strategy: mkcert${ca} }`;
|
|
296
|
+
return `tls: { strategy: files${ca}, include: <edge.include>, mounts: <edge.mounts> }`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Config warnings about the edge's TLS, for `validate` and `doctor`. */
|
|
300
|
+
export function edgeTlsWarnings(cfg: EdgeConfig): string[] {
|
|
301
|
+
if (cfg.tlsFrom === "implicit") return [IMPLICIT_TLS_WARNING];
|
|
302
|
+
if (cfg.tlsFrom === "legacy")
|
|
303
|
+
return [
|
|
304
|
+
"edge.caroot and edge.issue_cert are deprecated; they still work. " +
|
|
305
|
+
`Say the same with edge.${tlsYaml(cfg.tls)}`,
|
|
306
|
+
];
|
|
307
|
+
return [];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
let warnedImplicit = false;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The implicit-mkcert warning, at most once per run: from `validate` and
|
|
314
|
+
* from the paths that would mint a cert (`edge up`, `stack up`).
|
|
315
|
+
*/
|
|
316
|
+
export function warnImplicitTlsOnce(
|
|
317
|
+
cfg: EdgeConfig,
|
|
318
|
+
warn: (line: string) => void = (l) => console.warn(`⚠️ ${l}`),
|
|
319
|
+
): void {
|
|
320
|
+
if (cfg.tlsFrom !== "implicit" || warnedImplicit) return;
|
|
321
|
+
warnedImplicit = true;
|
|
322
|
+
warn(IMPLICIT_TLS_WARNING);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/** Test seam: forget that the warning was given. */
|
|
326
|
+
export function __resetTlsWarningForTests(): void {
|
|
327
|
+
warnedImplicit = false;
|
|
328
|
+
}
|
package/src/shared/edge/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The org's
|
|
3
|
-
*
|
|
2
|
+
* The org's edge: one Traefik that terminates TLS as `edge.tls` says (none,
|
|
3
|
+
* mkcert or files) and routes `<name>.<domain>` to local services.
|
|
4
4
|
* File-provider routes, plus Docker labels when `docker_labels` opts in
|
|
5
5
|
* (through a read-only socket proxy; the edge never mounts the socket).
|
|
6
6
|
*
|
|
@@ -15,6 +15,11 @@ export interface EdgePorts {
|
|
|
15
15
|
https: number;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
export type EdgeTls =
|
|
19
|
+
| { strategy: "none" }
|
|
20
|
+
| { strategy: "mkcert"; ca: string | null }
|
|
21
|
+
| { strategy: "files"; ca: string | null };
|
|
22
|
+
|
|
18
23
|
export interface EdgeConfig {
|
|
19
24
|
/** Org slug — names the edge container and its Docker network. */
|
|
20
25
|
org: string;
|
|
@@ -35,14 +40,25 @@ export interface EdgeConfig {
|
|
|
35
40
|
/** Where the org's CA, certs and generated config live. */
|
|
36
41
|
stateDir: string;
|
|
37
42
|
/**
|
|
38
|
-
*
|
|
39
|
-
* (
|
|
40
|
-
|
|
41
|
-
|
|
43
|
+
* The state dir the config names, when DEN_EDGE_DIR overrides it with
|
|
44
|
+
* another (0.3.0-dev.40): an unlabelled edge is adopted only from that one.
|
|
45
|
+
*/
|
|
46
|
+
defaultStateDir?: string;
|
|
47
|
+
/**
|
|
48
|
+
* How the edge gets its certificates: the `edge.tls` strategy (see
|
|
49
|
+
* tls.ts). `none`: http only, no https entrypoint. `mkcert`: the edge
|
|
50
|
+
* issues its own wildcard cert from a local CA (its own under `stateDir`,
|
|
51
|
+
* or `ca`, an existing one it never mints or uninstalls). `files`: the
|
|
52
|
+
* included dynamic config and mounts hold the certs; `ca` is the CA that
|
|
53
|
+
* signed them, if Den should trust it for health checks.
|
|
54
|
+
*/
|
|
55
|
+
tls: EdgeTls;
|
|
56
|
+
/**
|
|
57
|
+
* Where `tls` came from: `explicit` (edge.tls), `legacy` (edge.caroot /
|
|
58
|
+
* edge.issue_cert, mapped), or `implicit` (neither: mkcert with a local CA,
|
|
59
|
+
* deprecated, warned once per run).
|
|
42
60
|
*/
|
|
43
|
-
|
|
44
|
-
/** Issue the edge's own wildcard cert. Off when included config holds the certs. */
|
|
45
|
-
issueCert: boolean;
|
|
61
|
+
tlsFrom: "explicit" | "legacy" | "implicit";
|
|
46
62
|
/** Catch-all HTTP→HTTPS redirect. Off when included config decides per name. */
|
|
47
63
|
httpsRedirect: boolean;
|
|
48
64
|
/**
|
|
@@ -73,6 +73,16 @@ function blueprintServices(
|
|
|
73
73
|
return services;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
/**
|
|
77
|
+
* The blueprint's `stack_namespace`, carried into the seeded workspace so
|
|
78
|
+
* its stacks keep that namespace on this machine ({} when it sets none).
|
|
79
|
+
*/
|
|
80
|
+
function blueprintStackNamespace(
|
|
81
|
+
bp: ResolvedWorkspaceBlueprint,
|
|
82
|
+
): Pick<WorkspaceConfig, "stack_namespace"> {
|
|
83
|
+
return bp.stackNamespace ? { stack_namespace: bp.stackNamespace } : {};
|
|
84
|
+
}
|
|
85
|
+
|
|
76
86
|
/**
|
|
77
87
|
* The clone slug seeding would give each repo the catalog has no `gh` for:
|
|
78
88
|
* `<org>/<repo>` with its workspace's resolved org. A repo in several
|
|
@@ -174,7 +184,8 @@ function sameEntry(a: unknown, b: unknown): boolean {
|
|
|
174
184
|
/**
|
|
175
185
|
* Whether seeding now would change nothing: every workspace the blueprint
|
|
176
186
|
* seeds holds exactly what seeding writes (same repos, all managed on the
|
|
177
|
-
* blueprint's branch, same autostart defaults
|
|
187
|
+
* blueprint's branch, same autostart defaults, its stack_namespace),
|
|
188
|
+
* records the blueprint as
|
|
178
189
|
* applied, and (multi blueprint) is active. A workspace the user changed
|
|
179
190
|
* since (a repo switched to a local checkout, one added or removed) is not
|
|
180
191
|
* "already seeded": seeding would reset it.
|
|
@@ -196,6 +207,8 @@ export function blueprintAlreadySeeded(
|
|
|
196
207
|
bp.repos.some((r, i) => applied[i] !== r)
|
|
197
208
|
)
|
|
198
209
|
return false;
|
|
210
|
+
if (bp.stackNamespace && ws.stack_namespace !== bp.stackNamespace)
|
|
211
|
+
return false;
|
|
199
212
|
return sameEntry(ws.services ?? {}, blueprintServices(bp));
|
|
200
213
|
});
|
|
201
214
|
}
|
|
@@ -222,6 +235,7 @@ export function seedDefaultWorkspace(): string[] {
|
|
|
222
235
|
...(file.workspaces.default ?? {}),
|
|
223
236
|
services: blueprintServices(bp),
|
|
224
237
|
seeded_repos: [...repos],
|
|
238
|
+
...blueprintStackNamespace(bp),
|
|
225
239
|
};
|
|
226
240
|
if (!file.active?.length) {
|
|
227
241
|
file.active = ["default"];
|
|
@@ -248,6 +262,7 @@ export function seedBlueprintWorkspaces(): { name: string; repos: string[] }[] {
|
|
|
248
262
|
...(prev ?? {}),
|
|
249
263
|
services: blueprintServices(bp),
|
|
250
264
|
seeded_repos: [...bp.repos],
|
|
265
|
+
...blueprintStackNamespace(bp),
|
|
251
266
|
};
|
|
252
267
|
}
|
|
253
268
|
const names = bps.map((b) => b.name);
|