@monster-orange/den 0.2.8 → 0.4.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 +1731 -0
- package/README.md +4 -2
- package/bin/den +2 -1
- package/docs/distributions.md +730 -6
- 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 +12 -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/agents/default-pack/be-dev.md +20 -0
- package/src/shared/agents/default-pack/devops.md +14 -0
- package/src/shared/agents/default-pack/fe-dev.md +20 -0
- package/src/shared/agents/default-pack/orchestrator.md +16 -0
- package/src/shared/agents/default-pack/planner.md +17 -0
- package/src/shared/agents/default-pack/reviewer.md +14 -0
- package/src/shared/agents/default-pack/ux-designer.md +15 -0
- package/src/shared/agents/den-claude.ts +173 -0
- package/src/shared/agents/generic.ts +52 -0
- package/src/shared/agents/personas.ts +218 -0
- package/src/shared/agents/records.ts +118 -0
- package/src/shared/agents/runtime.ts +192 -0
- package/src/shared/agents/runtimes.ts +47 -0
- 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 +257 -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/engine.ts +23 -0
- 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/environment-manager/workspaces.ts +19 -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/secret-env.ts +70 -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/sessions/client.ts +277 -0
- package/src/shared/sessions/den-pty.ts +97 -0
- package/src/shared/sessions/env.ts +79 -0
- package/src/shared/sessions/host.ts +667 -0
- package/src/shared/sessions/names.ts +85 -0
- package/src/shared/sessions/paths.ts +27 -0
- package/src/shared/sessions/protocol.ts +72 -0
- package/src/shared/sessions/pty.ts +141 -0
- package/src/shared/sessions/ring-buffer.ts +160 -0
- package/src/shared/sessions/types.ts +81 -0
- 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/agents-command.ts +812 -0
- package/src/ui/cli/create.ts +24 -7
- package/src/ui/cli/doctor-command.ts +260 -0
- package/src/ui/cli/edge-command.ts +56 -10
- package/src/ui/cli/env-probe.ts +266 -0
- package/src/ui/cli/flag-parser.ts +18 -5
- package/src/ui/cli/health-display.ts +25 -0
- package/src/ui/cli/index.ts +580 -60
- package/src/ui/cli/notice.ts +20 -0
- package/src/ui/cli/plugin-command.ts +849 -0
- package/src/ui/cli/sessions-command.ts +718 -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 +145 -20
- package/src/ui/mcp/server.ts +27 -2
- package/src/ui/mcp/tools.ts +94 -12
- package/src/ui/server/appearance-prefs.ts +95 -0
- package/src/ui/server/audit.ts +35 -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 +539 -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 +37 -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 +48 -18
- package/themes/den/den-theme.json +72 -0
- package/themes/den/favicon.ico +0 -0
- package/themes/den/mark.svg +6 -0
|
@@ -2,30 +2,40 @@
|
|
|
2
2
|
* Secrets strategy selection.
|
|
3
3
|
*
|
|
4
4
|
* Resolution order for the backend:
|
|
5
|
-
* 1. DEN_SECRETS_PROVIDER env var (doppler | 1password | env)
|
|
5
|
+
* 1. DEN_SECRETS_PROVIDER env var (none | doppler | 1password | env)
|
|
6
6
|
* 2. `secrets.provider` in config/local/user.yml (per-engineer)
|
|
7
|
-
* 3. `secrets.provider` in config
|
|
8
|
-
* 4. "doppler"
|
|
7
|
+
* 3. `secrets.provider` in the org config (team default)
|
|
8
|
+
* 4. "doppler" — implicit and deprecated: a warning says to set it.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* Every backend is a strategy registered in the strategy registry
|
|
11
|
+
* (./strategies.ts); a new one registers there, like a plugin.
|
|
12
|
+
*
|
|
13
|
+
* Den assumes nothing about where a distribution keeps its secrets: no
|
|
14
|
+
* vault, item, field or account is built in. The engine's own named secrets
|
|
15
|
+
* (`sudo`, `journeys_agent_key`) exist only when `secrets.refs.<name>` says
|
|
16
|
+
* where they are; otherwise the feature reports "not configured".
|
|
11
17
|
*/
|
|
12
|
-
import { OP_ACCOUNT_DEFAULT, OP_CLI_TIMEOUT_MS } from "../constants.js";
|
|
13
18
|
import { readDenConfig, readUserConfig } from "../den-config.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
19
|
+
import type { DetectResult } from "../strategies/registry.js";
|
|
20
|
+
import { notConfiguredMessage, SecretsNotConfiguredError } from "./none.js";
|
|
21
|
+
import { secretsStrategy, type SecretsStrategy } from "./strategies.js";
|
|
17
22
|
import type {
|
|
23
|
+
PreflightResult,
|
|
18
24
|
SecretRef,
|
|
19
25
|
SecretsProvider,
|
|
20
26
|
SecretsProviderName,
|
|
21
27
|
} from "./types.js";
|
|
22
28
|
|
|
23
|
-
import { envKey } from "../product.js";
|
|
29
|
+
import { envKey, product } from "../product.js";
|
|
24
30
|
export * from "./types.js";
|
|
25
31
|
export { flatKeyFor, toEnvName, refPath } from "./keys.js";
|
|
26
32
|
export { DopplerProvider } from "./doppler.js";
|
|
27
33
|
export { EnvProvider } from "./env.js";
|
|
34
|
+
export { NoneProvider } from "./none.js";
|
|
28
35
|
export { OnePasswordProvider } from "./onepassword.js";
|
|
36
|
+
export { notConfiguredMessage, SecretsNotConfiguredError } from "./none.js";
|
|
37
|
+
export { secretsStrategy, secretsStrategyIds } from "./strategies.js";
|
|
38
|
+
export type { SecretsStrategy, SecretsSetupContext } from "./strategies.js";
|
|
29
39
|
export * from "./secret-env.js";
|
|
30
40
|
|
|
31
41
|
export interface SecretsConfig {
|
|
@@ -42,17 +52,19 @@ export interface SecretsConfig {
|
|
|
42
52
|
/** `"vault/item/field": FLAT_KEY` overrides for doppler/env. */
|
|
43
53
|
keys?: Record<string, string>;
|
|
44
54
|
/**
|
|
45
|
-
* Where the engine's own named secrets live
|
|
46
|
-
* defaults
|
|
47
|
-
*
|
|
48
|
-
*
|
|
55
|
+
* Where the engine's own named secrets live (see {@link namedSecretRef}).
|
|
56
|
+
* There are no built-in defaults: a name with no entry here is "not
|
|
57
|
+
* configured" and its feature stays off. Names in use: `sudo` (the saved
|
|
58
|
+
* local-dev sudo password) and `journeys_agent_key` (the journeys
|
|
59
|
+
* pillar's agent API key).
|
|
49
60
|
*
|
|
50
61
|
* refs:
|
|
51
|
-
*
|
|
62
|
+
* sudo: { vault: Private, item: Dev Keys, field: ACME_LOCAL_ADMIN_PWD }
|
|
63
|
+
* journeys_agent_key: { key: CURSOR_API_KEY } # doppler / env
|
|
52
64
|
*/
|
|
53
65
|
refs?: Record<string, Partial<SecretRef>>;
|
|
54
66
|
/**
|
|
55
|
-
* The per-engineer developer keys `make
|
|
67
|
+
* The per-engineer developer keys `make <cli>-setup-1password` collects
|
|
56
68
|
* into the developer-keys item (see scripts/setup-1password.ts). Default:
|
|
57
69
|
* only the keys the engine itself uses.
|
|
58
70
|
*/
|
|
@@ -70,6 +82,10 @@ export interface DeveloperKeyDef {
|
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
export interface DeveloperKeysConfig {
|
|
85
|
+
/** The 1Password vault holding the developer-keys item (no default). */
|
|
86
|
+
vault?: string;
|
|
87
|
+
/** The developer-keys item (no default). */
|
|
88
|
+
item?: string;
|
|
73
89
|
/**
|
|
74
90
|
* A dotenv file (relative to the den root, e.g. an app checkout's
|
|
75
91
|
* `.env`) whose values are offered as defaults.
|
|
@@ -79,26 +95,21 @@ export interface DeveloperKeysConfig {
|
|
|
79
95
|
}
|
|
80
96
|
|
|
81
97
|
export const SECRETS_PROVIDERS: readonly SecretsProviderName[] = [
|
|
98
|
+
"none",
|
|
82
99
|
"doppler",
|
|
83
100
|
"1password",
|
|
84
101
|
"env",
|
|
85
102
|
];
|
|
86
103
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
(c: SecretsConfig) => SecretsProvider
|
|
90
|
-
> = {
|
|
91
|
-
doppler: (c) => new DopplerProvider({ ...c.doppler, keys: c.keys }),
|
|
92
|
-
"1password": (c) =>
|
|
93
|
-
new OnePasswordProvider({
|
|
94
|
-
account: c.onepassword?.account ?? OP_ACCOUNT_DEFAULT,
|
|
95
|
-
timeoutMs: OP_CLI_TIMEOUT_MS,
|
|
96
|
-
}),
|
|
97
|
-
env: (c) => new EnvProvider(c.keys),
|
|
98
|
-
};
|
|
104
|
+
/** The backend an unset `secrets.provider` falls back to (deprecated). */
|
|
105
|
+
export const IMPLICIT_SECRETS_PROVIDER: SecretsProviderName = "doppler";
|
|
99
106
|
|
|
100
107
|
function isProviderName(v: unknown): v is SecretsProviderName {
|
|
101
|
-
return
|
|
108
|
+
return (
|
|
109
|
+
typeof v === "string" &&
|
|
110
|
+
(SECRETS_PROVIDERS as string[]).includes(v) &&
|
|
111
|
+
secretsStrategy(v) !== undefined
|
|
112
|
+
);
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
/** Effective secrets config (team file, then per-engineer override). */
|
|
@@ -125,33 +136,48 @@ export function readSecretsConfig(): SecretsConfig {
|
|
|
125
136
|
};
|
|
126
137
|
}
|
|
127
138
|
|
|
139
|
+
/** Whether `ref` gives backend `name` enough to address a secret. */
|
|
140
|
+
export function refIsComplete(
|
|
141
|
+
name: SecretsProviderName,
|
|
142
|
+
ref: Partial<SecretRef> | undefined,
|
|
143
|
+
): boolean {
|
|
144
|
+
if (!ref || typeof ref !== "object") return false;
|
|
145
|
+
return secretsStrategy(name)?.capabilities.refComplete(ref) ?? false;
|
|
146
|
+
}
|
|
147
|
+
|
|
128
148
|
/**
|
|
129
|
-
* The engine-named secret `name
|
|
130
|
-
*
|
|
131
|
-
*
|
|
149
|
+
* The engine-named secret `name`, exactly as `secrets.refs.<name>` gives it,
|
|
150
|
+
* or null when the distribution has not configured it (or configured too
|
|
151
|
+
* little for the selected backend). There are no built-in defaults: Den
|
|
152
|
+
* never assumes a vault, item, field or key of its own.
|
|
132
153
|
*/
|
|
133
154
|
export function namedSecretRef(
|
|
134
155
|
name: string,
|
|
135
|
-
defaults: SecretRef,
|
|
136
156
|
cfg: SecretsConfig = readSecretsConfig(),
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
157
|
+
provider: SecretsProviderName = resolveProviderName(cfg),
|
|
158
|
+
): SecretRef | null {
|
|
159
|
+
const raw = cfg.refs?.[name];
|
|
160
|
+
if (!refIsComplete(provider, raw)) return null;
|
|
161
|
+
const str = (v: unknown): string => (typeof v === "string" ? v.trim() : "");
|
|
141
162
|
const ref: SecretRef = {
|
|
142
|
-
vault:
|
|
143
|
-
item:
|
|
144
|
-
field:
|
|
163
|
+
vault: str(raw!.vault),
|
|
164
|
+
item: str(raw!.item),
|
|
165
|
+
field: str(raw!.field) || str(raw!.key),
|
|
145
166
|
};
|
|
146
|
-
|
|
147
|
-
if (key) ref.key = key;
|
|
167
|
+
if (str(raw!.key)) ref.key = str(raw!.key);
|
|
148
168
|
return ref;
|
|
149
169
|
}
|
|
150
170
|
|
|
151
|
-
export
|
|
171
|
+
export interface ResolvedProvider {
|
|
172
|
+
name: SecretsProviderName;
|
|
173
|
+
/** True when nothing chose it: the deprecated implicit doppler default. */
|
|
174
|
+
implicit: boolean;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function resolveProvider(
|
|
152
178
|
cfg: SecretsConfig,
|
|
153
179
|
env: NodeJS.ProcessEnv = process.env,
|
|
154
|
-
):
|
|
180
|
+
): ResolvedProvider {
|
|
155
181
|
const fromEnv = env.DEN_SECRETS_PROVIDER;
|
|
156
182
|
if (fromEnv) {
|
|
157
183
|
if (!isProviderName(fromEnv)) {
|
|
@@ -159,40 +185,243 @@ export function resolveProviderName(
|
|
|
159
185
|
`${envKey("SECRETS_PROVIDER")}="${fromEnv}" is not one of ${SECRETS_PROVIDERS.join(", ")}`,
|
|
160
186
|
);
|
|
161
187
|
}
|
|
162
|
-
return fromEnv;
|
|
188
|
+
return { name: fromEnv, implicit: false };
|
|
163
189
|
}
|
|
164
190
|
if (cfg.provider !== undefined && !isProviderName(cfg.provider)) {
|
|
165
191
|
throw new Error(
|
|
166
192
|
`secrets.provider "${String(cfg.provider)}" is not one of ${SECRETS_PROVIDERS.join(", ")}`,
|
|
167
193
|
);
|
|
168
194
|
}
|
|
169
|
-
return cfg.provider
|
|
195
|
+
return cfg.provider
|
|
196
|
+
? { name: cfg.provider, implicit: false }
|
|
197
|
+
: { name: IMPLICIT_SECRETS_PROVIDER, implicit: true };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function resolveProviderName(
|
|
201
|
+
cfg: SecretsConfig,
|
|
202
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
203
|
+
): SecretsProviderName {
|
|
204
|
+
return resolveProvider(cfg, env).name;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The registered strategy for `name` (always registered for a valid name). */
|
|
208
|
+
export function strategyFor(name: SecretsProviderName): SecretsStrategy {
|
|
209
|
+
const s = secretsStrategy(name);
|
|
210
|
+
if (!s) throw new Error(`secrets strategy "${name}" is not registered`);
|
|
211
|
+
return s;
|
|
170
212
|
}
|
|
171
213
|
|
|
172
214
|
export function createSecretsProvider(
|
|
173
215
|
name: SecretsProviderName,
|
|
174
216
|
cfg: SecretsConfig = {},
|
|
175
217
|
): SecretsProvider {
|
|
176
|
-
return
|
|
218
|
+
return strategyFor(name).capabilities.create(cfg);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The provider behind detect(): while the strategy reports its backend
|
|
223
|
+
* unavailable (its CLI is not on PATH), nothing is spawned. Reads report
|
|
224
|
+
* null / not ready, and get, set and preflight give detect()'s reason
|
|
225
|
+
* instead of a spawn error.
|
|
226
|
+
*/
|
|
227
|
+
class DetectedProvider implements SecretsProvider {
|
|
228
|
+
constructor(
|
|
229
|
+
private readonly inner: SecretsProvider,
|
|
230
|
+
private readonly strategy: SecretsStrategy,
|
|
231
|
+
private readonly implicit = false,
|
|
232
|
+
) {}
|
|
233
|
+
get name(): SecretsProviderName {
|
|
234
|
+
return this.inner.name;
|
|
235
|
+
}
|
|
236
|
+
private unavailable(): string | null {
|
|
237
|
+
// The implicit default is resolved lazily: only a secret actually being
|
|
238
|
+
// read or written gets here, and only then does it warn (once).
|
|
239
|
+
if (this.implicit) warnImplicitOnce();
|
|
240
|
+
const d = this.strategy.detect();
|
|
241
|
+
return d.available ? null : `not available: ${d.reason ?? "unavailable"}`;
|
|
242
|
+
}
|
|
243
|
+
async isReady(): Promise<boolean> {
|
|
244
|
+
return this.unavailable() ? false : this.inner.isReady();
|
|
245
|
+
}
|
|
246
|
+
async get(ref: SecretRef): Promise<string> {
|
|
247
|
+
const why = this.unavailable();
|
|
248
|
+
if (why) throw new Error(`${this.name} secrets backend ${why}`);
|
|
249
|
+
return this.inner.get(ref);
|
|
250
|
+
}
|
|
251
|
+
async tryGet(ref: SecretRef): Promise<string | null> {
|
|
252
|
+
return this.unavailable() ? null : this.inner.tryGet(ref);
|
|
253
|
+
}
|
|
254
|
+
async set(ref: SecretRef, value: string): Promise<void> {
|
|
255
|
+
const why = this.unavailable();
|
|
256
|
+
if (why) throw new Error(`${this.name} secrets backend ${why}`);
|
|
257
|
+
return this.inner.set(ref, value);
|
|
258
|
+
}
|
|
259
|
+
async preflight(): Promise<PreflightResult> {
|
|
260
|
+
// Nothing chose a backend, so nothing requires one to be ready.
|
|
261
|
+
if (this.implicit) return { ok: true };
|
|
262
|
+
const why = this.unavailable();
|
|
263
|
+
if (why)
|
|
264
|
+
return {
|
|
265
|
+
ok: false,
|
|
266
|
+
message:
|
|
267
|
+
`${this.strategy.capabilities.label} ${why}.\n` +
|
|
268
|
+
`Install it, or set secrets.provider in ${product().configFile} (none | doppler | 1password | env).`,
|
|
269
|
+
};
|
|
270
|
+
return this.inner.preflight();
|
|
271
|
+
}
|
|
272
|
+
runPrefix(): string[] {
|
|
273
|
+
// Never wrap a command in an implicit backend.
|
|
274
|
+
if (this.implicit) return [];
|
|
275
|
+
return this.unavailable() ? [] : this.inner.runPrefix();
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let warnedImplicit = false;
|
|
280
|
+
function warnImplicitOnce(): void {
|
|
281
|
+
if (warnedImplicit) return;
|
|
282
|
+
warnedImplicit = true;
|
|
283
|
+
console.warn(`⚠️ ${IMPLICIT_PROVIDER_WARNING}`);
|
|
177
284
|
}
|
|
178
285
|
|
|
179
286
|
let cached: SecretsProvider | null = null;
|
|
180
287
|
let cachedKey = "";
|
|
181
288
|
|
|
182
|
-
/**
|
|
289
|
+
/**
|
|
290
|
+
* The configured backend, behind its strategy's detect(). Re-resolves when
|
|
291
|
+
* DEN_SECRETS_PROVIDER or the config changes.
|
|
292
|
+
*/
|
|
183
293
|
export function getSecretsProvider(): SecretsProvider {
|
|
184
294
|
const cfg = readSecretsConfig();
|
|
185
|
-
const name =
|
|
186
|
-
const key = `${name}|${JSON.stringify(cfg)}`;
|
|
295
|
+
const { name, implicit } = resolveProvider(cfg);
|
|
296
|
+
const key = `${name}|${implicit}|${JSON.stringify(cfg)}`;
|
|
187
297
|
if (!cached || key !== cachedKey) {
|
|
188
|
-
cached =
|
|
298
|
+
cached = new DetectedProvider(
|
|
299
|
+
createSecretsProvider(name, cfg),
|
|
300
|
+
strategyFor(name),
|
|
301
|
+
implicit,
|
|
302
|
+
);
|
|
189
303
|
cachedKey = key;
|
|
190
304
|
}
|
|
191
305
|
return cached;
|
|
192
306
|
}
|
|
193
307
|
|
|
308
|
+
export interface SecretsStatus {
|
|
309
|
+
/** The selected strategy id. */
|
|
310
|
+
backend: SecretsProviderName;
|
|
311
|
+
/** Name people see ("1Password"). */
|
|
312
|
+
label: string;
|
|
313
|
+
/** True when secrets.provider is unset (the deprecated doppler default). */
|
|
314
|
+
implicit: boolean;
|
|
315
|
+
/** detect() of the selected strategy. */
|
|
316
|
+
detect: DetectResult;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** The selected strategy and its detect() result. Never spawns, never throws. */
|
|
320
|
+
export function secretsStatus(
|
|
321
|
+
cfg: SecretsConfig = readSecretsConfig(),
|
|
322
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
323
|
+
opts: { detect?: boolean } = {},
|
|
324
|
+
): SecretsStatus {
|
|
325
|
+
let r: ResolvedProvider;
|
|
326
|
+
try {
|
|
327
|
+
r = resolveProvider(cfg, env);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
return {
|
|
330
|
+
backend: "none",
|
|
331
|
+
label: "none",
|
|
332
|
+
implicit: false,
|
|
333
|
+
detect: { available: false, reason: (err as Error).message },
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const s = strategyFor(r.name);
|
|
337
|
+
return {
|
|
338
|
+
backend: r.name,
|
|
339
|
+
label: s.capabilities.label,
|
|
340
|
+
implicit: r.implicit,
|
|
341
|
+
// detect:false: report the choice without probing (nothing needs it).
|
|
342
|
+
detect:
|
|
343
|
+
opts.detect === false
|
|
344
|
+
? { available: false, reason: "not checked: nothing needs it" }
|
|
345
|
+
: s.detect(env),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Env vars the selected strategy gives the processes Den starts (the web
|
|
351
|
+
* server). Only 1password gives any (OP_ACCOUNT, from the distribution's
|
|
352
|
+
* config or the engineer's shell); every other backend gives none.
|
|
353
|
+
*/
|
|
354
|
+
export function secretsProcessEnv(
|
|
355
|
+
cfg: SecretsConfig = readSecretsConfig(),
|
|
356
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
357
|
+
): Record<string, string> {
|
|
358
|
+
let name: SecretsProviderName;
|
|
359
|
+
try {
|
|
360
|
+
name = resolveProviderName(cfg, env);
|
|
361
|
+
} catch {
|
|
362
|
+
return {};
|
|
363
|
+
}
|
|
364
|
+
return strategyFor(name).capabilities.env?.(cfg, env) ?? {};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** The engine's own named refs, for the legacy warning. */
|
|
368
|
+
export const ENGINE_SECRET_REFS = ["sudo", "journeys_agent_key"] as const;
|
|
369
|
+
|
|
370
|
+
export const IMPLICIT_PROVIDER_WARNING =
|
|
371
|
+
"secrets.provider is not set; the implicit doppler default is deprecated and will be removed in a later minor. " +
|
|
372
|
+
"Set it explicitly: doppler, 1password, env or none.";
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Config warnings about secrets, for the server's startup log and
|
|
376
|
+
* `validate`: a 1password distribution with no refs (the old built-in
|
|
377
|
+
* vault/item/field are gone). The implicit doppler default warns lazily,
|
|
378
|
+
* the first time it resolves a secret (see getSecretsProvider), so a
|
|
379
|
+
* distribution that needs no secrets never hears about it.
|
|
380
|
+
*/
|
|
381
|
+
export function secretsConfigWarnings(
|
|
382
|
+
cfg: SecretsConfig = readSecretsConfig(),
|
|
383
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
384
|
+
): string[] {
|
|
385
|
+
let r: ResolvedProvider;
|
|
386
|
+
try {
|
|
387
|
+
r = resolveProvider(cfg, env);
|
|
388
|
+
} catch (err) {
|
|
389
|
+
return [(err as Error).message];
|
|
390
|
+
}
|
|
391
|
+
const out: string[] = [];
|
|
392
|
+
if (
|
|
393
|
+
r.name === "1password" &&
|
|
394
|
+
!ENGINE_SECRET_REFS.some((n) => refIsComplete(r.name, cfg.refs?.[n]))
|
|
395
|
+
) {
|
|
396
|
+
out.push(
|
|
397
|
+
"secrets.provider is 1password but secrets.refs is empty: Den no longer assumes a default vault, item or field. " +
|
|
398
|
+
`Add the refs to ${product().configFile}, e.g.\n` +
|
|
399
|
+
" secrets:\n" +
|
|
400
|
+
" refs:\n" +
|
|
401
|
+
" sudo: { vault: <vault>, item: <item>, field: <field> }\n" +
|
|
402
|
+
" journeys_agent_key: { vault: <vault>, item: <item>, field: CURSOR_API_KEY }\n" +
|
|
403
|
+
"The vault-backed features (saved sudo password, journeys agent key) stay off until then.",
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Throw "not configured" for engine ref `name`, or return it. For features
|
|
411
|
+
* that cannot run without it.
|
|
412
|
+
*/
|
|
413
|
+
export function requireNamedSecretRef(
|
|
414
|
+
name: string,
|
|
415
|
+
cfg: SecretsConfig = readSecretsConfig(),
|
|
416
|
+
): SecretRef {
|
|
417
|
+
const ref = namedSecretRef(name, cfg);
|
|
418
|
+
if (!ref) throw new SecretsNotConfiguredError(notConfiguredMessage(name));
|
|
419
|
+
return ref;
|
|
420
|
+
}
|
|
421
|
+
|
|
194
422
|
/** Test hook. */
|
|
195
423
|
export function resetSecretsProviderCache(): void {
|
|
196
424
|
cached = null;
|
|
197
425
|
cachedKey = "";
|
|
426
|
+
warnedImplicit = false;
|
|
198
427
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `none` strategy: no secrets backend. What a distribution gets when it
|
|
3
|
+
* sets no `secrets.provider`. It resolves nothing, never spawns a CLI, and
|
|
4
|
+
* every secret-backed feature reports "not configured" instead of failing.
|
|
5
|
+
*/
|
|
6
|
+
import { product } from "../product.js";
|
|
7
|
+
import type { PreflightResult, SecretRef, SecretsProvider } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* "not configured: set secrets.provider and secrets.refs.<name> in
|
|
11
|
+
* config/<cli>.yml" — the one message every surface shows for a secret Den
|
|
12
|
+
* has no configured place for.
|
|
13
|
+
*/
|
|
14
|
+
export function notConfiguredMessage(refName = "<name>"): string {
|
|
15
|
+
return `not configured: set secrets.provider and secrets.refs.${refName} in ${product().configFile}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Thrown when a secret is asked of a backend or ref that is not configured. */
|
|
19
|
+
export class SecretsNotConfiguredError extends Error {
|
|
20
|
+
readonly code = "not-configured";
|
|
21
|
+
constructor(message = notConfiguredMessage()) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "SecretsNotConfiguredError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class NoneProvider implements SecretsProvider {
|
|
28
|
+
readonly name = "none" as const;
|
|
29
|
+
|
|
30
|
+
async isReady(): Promise<boolean> {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async get(ref: SecretRef): Promise<string> {
|
|
35
|
+
throw new SecretsNotConfiguredError(
|
|
36
|
+
`${ref.field || "secret"}: no secrets backend is configured (${notConfiguredMessage()})`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async tryGet(): Promise<string | null> {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async set(): Promise<void> {
|
|
45
|
+
throw new SecretsNotConfiguredError();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async preflight(): Promise<PreflightResult> {
|
|
49
|
+
// Nothing to sign in to: `up` and `web` run as they are.
|
|
50
|
+
return { ok: true, message: "no secrets backend configured" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
runPrefix(): string[] {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The built-in secrets strategies, registered through the generic strategy
|
|
3
|
+
* registry (src/shared/strategies/registry.ts) exactly as a plugin would
|
|
4
|
+
* register one. Each entry is `{ id, kind: "secrets", detect(), capabilities }`.
|
|
5
|
+
*
|
|
6
|
+
* Capabilities:
|
|
7
|
+
* - `create(cfg)`: the SecretsProvider for this backend;
|
|
8
|
+
* - `label`: the name people see ("Save to 1Password");
|
|
9
|
+
* - `cli`: the CLI it needs, for `make setup`'s install step;
|
|
10
|
+
* - `refComplete(ref)`: whether a configured `secrets.refs.<name>` gives
|
|
11
|
+
* this backend enough to address a secret;
|
|
12
|
+
* - `env(cfg, env)`: optional vars this backend gives the processes Den
|
|
13
|
+
* starts (only 1password: OP_ACCOUNT, from the distribution's config);
|
|
14
|
+
* - `setup(ctx)`: optional interactive setup (`make <cli>-setup-secrets`).
|
|
15
|
+
*/
|
|
16
|
+
import * as path from "path";
|
|
17
|
+
import { OP_CLI_TIMEOUT_MS } from "../constants.js";
|
|
18
|
+
import {
|
|
19
|
+
getStrategy,
|
|
20
|
+
onPath,
|
|
21
|
+
registerStrategy,
|
|
22
|
+
strategyIds,
|
|
23
|
+
type DetectResult,
|
|
24
|
+
type StrategyEntry,
|
|
25
|
+
} from "../strategies/registry.js";
|
|
26
|
+
import { DopplerProvider } from "./doppler.js";
|
|
27
|
+
import { EnvProvider } from "./env.js";
|
|
28
|
+
import { NoneProvider } from "./none.js";
|
|
29
|
+
import { OnePasswordProvider } from "./onepassword.js";
|
|
30
|
+
import type { SecretsConfig } from "./index.js";
|
|
31
|
+
import type { SecretRef, SecretsProvider } from "./types.js";
|
|
32
|
+
|
|
33
|
+
export const SECRETS_KIND = "secrets";
|
|
34
|
+
|
|
35
|
+
export interface SecretsSetupContext {
|
|
36
|
+
cfg: SecretsConfig;
|
|
37
|
+
provider: SecretsProvider;
|
|
38
|
+
/** Den's package root (its scripts live there). */
|
|
39
|
+
engineRoot: string;
|
|
40
|
+
/** The distribution's CLI name, for remediation lines. */
|
|
41
|
+
cli: string;
|
|
42
|
+
/** Run a command with inherited stdio; its exit code. */
|
|
43
|
+
run(cmd: string, args: string[]): number;
|
|
44
|
+
log(line: string): void;
|
|
45
|
+
error(line: string): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SecretsCapabilities {
|
|
49
|
+
label: string;
|
|
50
|
+
create(cfg: SecretsConfig): SecretsProvider;
|
|
51
|
+
cli?: { bin: string; formula: string };
|
|
52
|
+
refComplete(ref: Partial<SecretRef>): boolean;
|
|
53
|
+
env?(cfg: SecretsConfig, env: NodeJS.ProcessEnv): Record<string, string>;
|
|
54
|
+
setup?(ctx: SecretsSetupContext): Promise<number>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type SecretsStrategy = StrategyEntry<
|
|
58
|
+
typeof SECRETS_KIND,
|
|
59
|
+
SecretsCapabilities
|
|
60
|
+
>;
|
|
61
|
+
|
|
62
|
+
const nonEmpty = (v: unknown): boolean =>
|
|
63
|
+
typeof v === "string" && v.trim().length > 0;
|
|
64
|
+
|
|
65
|
+
/** Flat-key backends address a secret by its key, or an env-shaped field. */
|
|
66
|
+
const flatRefComplete = (ref: Partial<SecretRef>): boolean =>
|
|
67
|
+
nonEmpty(ref.key) || nonEmpty(ref.field);
|
|
68
|
+
|
|
69
|
+
function cliDetect(bin: string, label: string) {
|
|
70
|
+
return (env: NodeJS.ProcessEnv = process.env): DetectResult =>
|
|
71
|
+
onPath(bin, env)
|
|
72
|
+
? { available: true }
|
|
73
|
+
: { available: false, reason: `${label} CLI (${bin}) is not on PATH` };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function otherBackends(): string {
|
|
77
|
+
return (
|
|
78
|
+
"Or pick another backend in the org config (secrets.provider: env | doppler | 1password),\n" +
|
|
79
|
+
" or for just you in config/local/user.yml."
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const none: SecretsStrategy = {
|
|
84
|
+
id: "none",
|
|
85
|
+
kind: SECRETS_KIND,
|
|
86
|
+
detect: () => ({
|
|
87
|
+
available: true,
|
|
88
|
+
reason: "no secrets backend configured",
|
|
89
|
+
}),
|
|
90
|
+
capabilities: {
|
|
91
|
+
label: "none",
|
|
92
|
+
create: () => new NoneProvider(),
|
|
93
|
+
refComplete: () => false,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const env: SecretsStrategy = {
|
|
98
|
+
id: "env",
|
|
99
|
+
kind: SECRETS_KIND,
|
|
100
|
+
detect: () => ({ available: true }),
|
|
101
|
+
capabilities: {
|
|
102
|
+
label: "environment",
|
|
103
|
+
create: (c) => new EnvProvider(c.keys),
|
|
104
|
+
refComplete: flatRefComplete,
|
|
105
|
+
async setup(ctx) {
|
|
106
|
+
ctx.log(
|
|
107
|
+
"The env backend reads secrets from the process environment.\n" +
|
|
108
|
+
`Export them yourself (direnv, CI secrets, \`doppler run -- ${ctx.cli} up\`).`,
|
|
109
|
+
);
|
|
110
|
+
return 0;
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const doppler: SecretsStrategy = {
|
|
116
|
+
id: "doppler",
|
|
117
|
+
kind: SECRETS_KIND,
|
|
118
|
+
detect: cliDetect("doppler", "The Doppler"),
|
|
119
|
+
capabilities: {
|
|
120
|
+
label: "Doppler",
|
|
121
|
+
create: (c) => new DopplerProvider({ ...c.doppler, keys: c.keys }),
|
|
122
|
+
cli: { bin: "doppler", formula: "dopplerhq/cli/doppler" },
|
|
123
|
+
refComplete: flatRefComplete,
|
|
124
|
+
async setup(ctx) {
|
|
125
|
+
if (!doppler.detect().available) {
|
|
126
|
+
ctx.error(
|
|
127
|
+
"✗ The Doppler CLI is not installed.\n" +
|
|
128
|
+
` Install it: brew install dopplerhq/cli/doppler\n then re-run: make ${ctx.cli}-setup-secrets\n\n ` +
|
|
129
|
+
otherBackends(),
|
|
130
|
+
);
|
|
131
|
+
return 1;
|
|
132
|
+
}
|
|
133
|
+
if (!(await ctx.provider.isReady())) {
|
|
134
|
+
ctx.log("→ doppler login");
|
|
135
|
+
if (ctx.run("doppler", ["login"]) !== 0) return 1;
|
|
136
|
+
}
|
|
137
|
+
const scope: string[] = [];
|
|
138
|
+
if (ctx.cfg.doppler?.project)
|
|
139
|
+
scope.push("--project", ctx.cfg.doppler.project);
|
|
140
|
+
if (ctx.cfg.doppler?.config)
|
|
141
|
+
scope.push("--config", ctx.cfg.doppler.config);
|
|
142
|
+
ctx.log(`→ doppler setup ${scope.join(" ")}`.trim());
|
|
143
|
+
if (
|
|
144
|
+
ctx.run("doppler", ["setup", "--no-interactive", ...scope]) !== 0 &&
|
|
145
|
+
scope.length === 0
|
|
146
|
+
) {
|
|
147
|
+
ctx.run("doppler", ["setup"]);
|
|
148
|
+
}
|
|
149
|
+
return 0;
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const onepassword: SecretsStrategy = {
|
|
155
|
+
id: "1password",
|
|
156
|
+
kind: SECRETS_KIND,
|
|
157
|
+
detect: cliDetect("op", "The 1Password"),
|
|
158
|
+
capabilities: {
|
|
159
|
+
label: "1Password",
|
|
160
|
+
create: (c) =>
|
|
161
|
+
new OnePasswordProvider({
|
|
162
|
+
// From the distribution's config or the engineer's shell; never a
|
|
163
|
+
// built-in account.
|
|
164
|
+
account: c.onepassword?.account,
|
|
165
|
+
timeoutMs: OP_CLI_TIMEOUT_MS,
|
|
166
|
+
}),
|
|
167
|
+
cli: { bin: "op", formula: "1password-cli" },
|
|
168
|
+
refComplete: (ref) => nonEmpty(ref.item) && nonEmpty(ref.field),
|
|
169
|
+
env(c, e): Record<string, string> {
|
|
170
|
+
const account = c.onepassword?.account?.trim() || e.OP_ACCOUNT?.trim();
|
|
171
|
+
return account ? { OP_ACCOUNT: account } : {};
|
|
172
|
+
},
|
|
173
|
+
async setup(ctx) {
|
|
174
|
+
if (!onepassword.detect().available) {
|
|
175
|
+
ctx.error(
|
|
176
|
+
"✗ The 1Password CLI (op) is not installed.\n" +
|
|
177
|
+
` Install it: brew install 1password-cli\n\n ` +
|
|
178
|
+
otherBackends(),
|
|
179
|
+
);
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
return ctx.run(path.join(ctx.engineRoot, "node_modules", ".bin", "tsx"), [
|
|
183
|
+
path.join(ctx.engineRoot, "scripts", "setup-1password.ts"),
|
|
184
|
+
]);
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
for (const s of [none, env, doppler, onepassword]) registerStrategy(s);
|
|
190
|
+
|
|
191
|
+
/** The registered secrets strategy `id`, or undefined. */
|
|
192
|
+
export function secretsStrategy(id: string): SecretsStrategy | undefined {
|
|
193
|
+
return getStrategy<typeof SECRETS_KIND, SecretsCapabilities>(
|
|
194
|
+
SECRETS_KIND,
|
|
195
|
+
id,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Every registered secrets strategy id. */
|
|
200
|
+
export function secretsStrategyIds(): string[] {
|
|
201
|
+
return strategyIds(SECRETS_KIND);
|
|
202
|
+
}
|