@monster-orange/den 0.2.7 → 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 +1655 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +619 -3
- 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/agent-actor.ts +38 -3
- 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 +63 -0
- package/src/shared/constants.ts +3 -24
- package/src/shared/den-config.ts +185 -7
- 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/catalog.ts +52 -1
- package/src/shared/environment-manager/default-workspace.ts +26 -3
- package/src/shared/environment-manager/git-remote.ts +49 -0
- 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/environment-manager/workspace-manager.ts +67 -25
- package/src/shared/environment-manager/workspace-org.ts +26 -11
- package/src/shared/health/rollup.ts +48 -0
- package/src/shared/manifest/service-links.ts +20 -8
- package/src/shared/open-url.ts +21 -2
- package/src/shared/org-sources.ts +328 -0
- 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 +24 -5
- package/src/ui/cli/clone-plan.ts +154 -23
- 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 +62 -3
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +649 -72
- package/src/ui/cli/notice.ts +20 -0
- package/src/ui/cli/org-command.ts +12 -2
- 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/up-args.ts +15 -0
- package/src/ui/cli/web-command.ts +47 -23
- package/src/ui/mcp/api.ts +28 -2
- package/src/ui/mcp/scope.ts +298 -0
- package/src/ui/mcp/server.ts +70 -8
- package/src/ui/mcp/tools.ts +151 -24
- package/src/ui/server/appearance-prefs.ts +95 -0
- package/src/ui/server/audit.ts +90 -3
- 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 +29 -45
- 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 +590 -45
- 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 +15 -3
- 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 +24 -4
- package/src/ui/server/release-service.ts +25 -3
- 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 +55 -9
- package/themes/den/den-theme.json +72 -0
- package/themes/den/favicon.ico +0 -0
- package/themes/den/mark.svg +6 -0
|
@@ -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
|
/**
|
|
@@ -14,7 +14,13 @@ import * as path from "path";
|
|
|
14
14
|
import * as yaml from "yaml";
|
|
15
15
|
import type { ServiceCatalog, ServiceCatalogEntry } from "../types/index.js";
|
|
16
16
|
import { getDistRoot } from "../paths.js";
|
|
17
|
-
import { requireGithubOrg } from "../den-config.js";
|
|
17
|
+
import { readDenConfig, requireGithubOrg } from "../den-config.js";
|
|
18
|
+
import {
|
|
19
|
+
parseRemote,
|
|
20
|
+
repoSource,
|
|
21
|
+
type WorkOrgsConfig,
|
|
22
|
+
} from "../org-sources.js";
|
|
23
|
+
import { checkedRemote, isGitRemote } from "./git-remote.js";
|
|
18
24
|
|
|
19
25
|
/** Fallback GitHub org when `config/den.yml` doesn't set `github_org`. */
|
|
20
26
|
|
|
@@ -77,6 +83,51 @@ export function resolveRepoGhSlug(
|
|
|
77
83
|
return `${owner()}/${repoName}`;
|
|
78
84
|
}
|
|
79
85
|
|
|
86
|
+
/**
|
|
87
|
+
* What `repoName` clones from (ORG-1d): a GitHub slug or a git URL (see
|
|
88
|
+
* git-remote.ts). The repo's source decides: a GitHub source gives its slug
|
|
89
|
+
* (`gh`, else `<owner>/<repo>`). A repo with no source and a `remote:` gives
|
|
90
|
+
* that remote, through plain git. Anything else is the legacy GitHub slug
|
|
91
|
+
* (resolveRepoGhSlug), so a catalog without `remote:` clones as before.
|
|
92
|
+
*/
|
|
93
|
+
export function resolveRepoCloneSource(
|
|
94
|
+
repoName: string,
|
|
95
|
+
catalog: ServiceCatalog = readCatalog(),
|
|
96
|
+
owner: () => string = () => requireGithubOrg(),
|
|
97
|
+
orgs: WorkOrgsConfig | undefined = readDenConfig().orgs,
|
|
98
|
+
): string {
|
|
99
|
+
const entry = catalog[repoName];
|
|
100
|
+
const gh = entry?.gh?.trim();
|
|
101
|
+
const remote = entry?.remote?.trim();
|
|
102
|
+
const src = repoSource(repoName, catalog, orgs ?? {});
|
|
103
|
+
if (src?.host === "github") {
|
|
104
|
+
if (gh) return gh;
|
|
105
|
+
const fromRemote = remote ? parseRemote(remote) : null;
|
|
106
|
+
return fromRemote?.host === "github"
|
|
107
|
+
? `${fromRemote.owner}/${repoName}`
|
|
108
|
+
: `${src.owner ?? ""}/${repoName}`;
|
|
109
|
+
}
|
|
110
|
+
if (remote) return checkedRemote(repoName, remote);
|
|
111
|
+
if (src)
|
|
112
|
+
throw new Error(
|
|
113
|
+
`${repoName}: its org's source is on ${src.host}, which Den clones only ` +
|
|
114
|
+
`from a remote: — set ${repoName}.remote in the service catalog`,
|
|
115
|
+
);
|
|
116
|
+
return resolveRepoGhSlug(repoName, catalog, owner);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Whether a clone source may be written back as the catalog's `gh` (the
|
|
121
|
+
* empty-slug repair): only a GitHub slug, and only for an entry with no
|
|
122
|
+
* `gh` and no `remote` of its own.
|
|
123
|
+
*/
|
|
124
|
+
export function needsGhRepair(
|
|
125
|
+
entry: ServiceCatalogEntry | undefined,
|
|
126
|
+
source: string,
|
|
127
|
+
): boolean {
|
|
128
|
+
return !isGitRemote(source) && !entry?.gh?.trim() && !entry?.remote?.trim();
|
|
129
|
+
}
|
|
130
|
+
|
|
80
131
|
export function removeCatalogEntry(repoName: string): void {
|
|
81
132
|
const catalog = readCatalog();
|
|
82
133
|
delete catalog[repoName];
|
|
@@ -22,8 +22,10 @@ import {
|
|
|
22
22
|
import {
|
|
23
23
|
githubOrgFor,
|
|
24
24
|
readOrgContext,
|
|
25
|
+
resolveWorkspaceOrg,
|
|
25
26
|
type OrgContext,
|
|
26
27
|
} from "./workspace-org.js";
|
|
28
|
+
import { orgHasGithub } from "../org-sources.js";
|
|
27
29
|
import type {
|
|
28
30
|
ServiceCatalog,
|
|
29
31
|
WorkspaceConfig,
|
|
@@ -71,6 +73,16 @@ function blueprintServices(
|
|
|
71
73
|
return services;
|
|
72
74
|
}
|
|
73
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
|
+
|
|
74
86
|
/**
|
|
75
87
|
* The clone slug seeding would give each repo the catalog has no `gh` for:
|
|
76
88
|
* `<org>/<repo>` with its workspace's resolved org. A repo in several
|
|
@@ -83,11 +95,17 @@ export function catalogFill(
|
|
|
83
95
|
ctx: OrgContext = readOrgContext(),
|
|
84
96
|
): Map<string, string> {
|
|
85
97
|
const fill = new Map<string, string>();
|
|
86
|
-
for (const ws of workspaces)
|
|
98
|
+
for (const ws of workspaces) {
|
|
99
|
+
// ORG-1d: an org with no GitHub source has no slug to give; its repos
|
|
100
|
+
// clone from their catalog `remote:`.
|
|
101
|
+
const org = resolveWorkspaceOrg(ws.name, ctx).org;
|
|
102
|
+
if (org && !orgHasGithub(org, ctx.config.orgs)) continue;
|
|
87
103
|
for (const repo of ws.repos) {
|
|
88
|
-
if (catalog[repo]?.gh || fill.has(repo))
|
|
104
|
+
if (catalog[repo]?.gh || catalog[repo]?.remote || fill.has(repo))
|
|
105
|
+
continue;
|
|
89
106
|
fill.set(repo, `${githubOrgFor(ws.name, ctx)}/${repo}`);
|
|
90
107
|
}
|
|
108
|
+
}
|
|
91
109
|
return fill;
|
|
92
110
|
}
|
|
93
111
|
|
|
@@ -166,7 +184,8 @@ function sameEntry(a: unknown, b: unknown): boolean {
|
|
|
166
184
|
/**
|
|
167
185
|
* Whether seeding now would change nothing: every workspace the blueprint
|
|
168
186
|
* seeds holds exactly what seeding writes (same repos, all managed on the
|
|
169
|
-
* blueprint's branch, same autostart defaults
|
|
187
|
+
* blueprint's branch, same autostart defaults, its stack_namespace),
|
|
188
|
+
* records the blueprint as
|
|
170
189
|
* applied, and (multi blueprint) is active. A workspace the user changed
|
|
171
190
|
* since (a repo switched to a local checkout, one added or removed) is not
|
|
172
191
|
* "already seeded": seeding would reset it.
|
|
@@ -188,6 +207,8 @@ export function blueprintAlreadySeeded(
|
|
|
188
207
|
bp.repos.some((r, i) => applied[i] !== r)
|
|
189
208
|
)
|
|
190
209
|
return false;
|
|
210
|
+
if (bp.stackNamespace && ws.stack_namespace !== bp.stackNamespace)
|
|
211
|
+
return false;
|
|
191
212
|
return sameEntry(ws.services ?? {}, blueprintServices(bp));
|
|
192
213
|
});
|
|
193
214
|
}
|
|
@@ -214,6 +235,7 @@ export function seedDefaultWorkspace(): string[] {
|
|
|
214
235
|
...(file.workspaces.default ?? {}),
|
|
215
236
|
services: blueprintServices(bp),
|
|
216
237
|
seeded_repos: [...repos],
|
|
238
|
+
...blueprintStackNamespace(bp),
|
|
217
239
|
};
|
|
218
240
|
if (!file.active?.length) {
|
|
219
241
|
file.active = ["default"];
|
|
@@ -240,6 +262,7 @@ export function seedBlueprintWorkspaces(): { name: string; repos: string[] }[] {
|
|
|
240
262
|
...(prev ?? {}),
|
|
241
263
|
services: blueprintServices(bp),
|
|
242
264
|
seeded_repos: [...bp.repos],
|
|
265
|
+
...blueprintStackNamespace(bp),
|
|
243
266
|
};
|
|
244
267
|
}
|
|
245
268
|
const names = bps.map((b) => b.name);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a managed clone is made from (ORG-1d): a GitHub slug (`owner/repo`,
|
|
3
|
+
* cloned from github.com with the user's gh as credential helper) or a git
|
|
4
|
+
* URL from the catalog's `remote:` (cloned through plain git and the user's
|
|
5
|
+
* own credential helper, never Den's or gh's: SEC-0).
|
|
6
|
+
*
|
|
7
|
+
* Clone commands run through a shell, so a remote must pass REMOTE_RE: a URL
|
|
8
|
+
* with a known transport, scp-style `user@host:path`, or an absolute path,
|
|
9
|
+
* and nothing a shell or git would read as a command (`ext::`, quotes, `$`,
|
|
10
|
+
* spaces, a leading `-`).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** A GitHub slug, as the clone guards accept it. */
|
|
14
|
+
export const SAFE_SLUG_RE = /^[a-zA-Z0-9._-]+\/[a-zA-Z0-9._-]+$/;
|
|
15
|
+
|
|
16
|
+
const REMOTE_RE =
|
|
17
|
+
/^(?:(?:https?|ssh|git|file):\/\/[A-Za-z0-9._~@:%+/-]+|[A-Za-z0-9._-]+@[A-Za-z0-9.-]+:[A-Za-z0-9._~+/-]+|\/[A-Za-z0-9._~@+/-]+)$/;
|
|
18
|
+
|
|
19
|
+
/** True when `spec` is a git URL rather than a GitHub slug. */
|
|
20
|
+
export function isGitRemote(spec: string): boolean {
|
|
21
|
+
return !SAFE_SLUG_RE.test(spec);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** True when `remote` is a git URL Den will hand to git. */
|
|
25
|
+
export function isSafeRemote(remote: string): boolean {
|
|
26
|
+
return REMOTE_RE.test(remote) && !remote.includes("..");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* `remote` for display: any user info (`https://user:token@host/…`) masked.
|
|
31
|
+
* Remotes can carry a token, so one is never printed as written.
|
|
32
|
+
*/
|
|
33
|
+
export function redactRemote(remote: string): string {
|
|
34
|
+
return remote.replace(/^([a-z+]+:\/\/)[^@/]+@/i, "$1***@");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The checked remote for `repo`, or a throw naming its catalog key (and not
|
|
39
|
+
* its value, which may hold a token).
|
|
40
|
+
*/
|
|
41
|
+
export function checkedRemote(repo: string, remote: string): string {
|
|
42
|
+
const r = remote.trim();
|
|
43
|
+
if (!isSafeRemote(r))
|
|
44
|
+
throw new Error(
|
|
45
|
+
`service catalog: ${repo}.remote is not a git URL Den can clone ` +
|
|
46
|
+
`(https://, ssh://, git://, file://, user@host:path or an absolute path)`,
|
|
47
|
+
);
|
|
48
|
+
return r;
|
|
49
|
+
}
|