@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
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 1Password Developer API Keys setup.
|
|
3
3
|
*
|
|
4
|
-
* Creates and populates the developer-keys item
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Creates and populates the developer-keys item inside a vault, which
|
|
5
|
+
* services can use to inject secrets at `make up` time via
|
|
6
|
+
* `op run --env-file=.env.op`. Where it lives is the distribution's choice:
|
|
7
|
+
* `secrets.developer_keys.{vault,item}` in the org config, else the vault
|
|
8
|
+
* and item of `secrets.refs.sudo`. Den has no default; with neither set the
|
|
9
|
+
* script says "not configured" and exits 0 (1 with --check).
|
|
7
10
|
*
|
|
8
11
|
* Which keys to collect is org data: `secrets.developer_keys.fields` in the
|
|
9
12
|
* org config (default: only the keys the engine itself uses), and
|
|
@@ -11,7 +14,8 @@
|
|
|
11
14
|
* offered as defaults.
|
|
12
15
|
*
|
|
13
16
|
* Usage:
|
|
14
|
-
* make
|
|
17
|
+
* make <cli>-setup-1password — full interactive setup (only when
|
|
18
|
+
* secrets.provider is 1password)
|
|
15
19
|
* npx tsx scripts/setup-1password.ts --check — non-interactive vault check
|
|
16
20
|
* exits 0 if ready, 1 if not
|
|
17
21
|
*/
|
|
@@ -20,22 +24,23 @@ import { execFile, spawnSync } from "child_process";
|
|
|
20
24
|
import { promisify } from "util";
|
|
21
25
|
import * as fs from "fs";
|
|
22
26
|
import * as path from "path";
|
|
23
|
-
import { fileURLToPath } from "url";
|
|
24
27
|
import chalk from "chalk";
|
|
25
28
|
import {
|
|
26
|
-
|
|
27
|
-
OP_API_KEYS_VAULT,
|
|
28
|
-
OP_DEV_KEYS_ITEM,
|
|
29
|
-
} from "../src/shared/constants.js";
|
|
30
|
-
import {
|
|
29
|
+
notConfiguredMessage,
|
|
31
30
|
readSecretsConfig,
|
|
31
|
+
secretsStrategy,
|
|
32
32
|
type DeveloperKeyDef,
|
|
33
33
|
} from "../src/shared/secrets/index.js";
|
|
34
34
|
|
|
35
35
|
import { product } from "../src/shared/product.js";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
import { getDistRoot } from "../src/shared/paths.js";
|
|
37
|
+
const SECRETS_CFG = readSecretsConfig();
|
|
38
|
+
// op uses the distribution's account (secrets.onepassword.account) or the
|
|
39
|
+
// engineer's own OP_ACCOUNT; Den has none of its own.
|
|
40
|
+
Object.assign(
|
|
41
|
+
process.env,
|
|
42
|
+
secretsStrategy("1password")?.capabilities.env?.(SECRETS_CFG, process.env),
|
|
43
|
+
);
|
|
39
44
|
|
|
40
45
|
const execFileAsync = promisify(execFile);
|
|
41
46
|
|
|
@@ -46,11 +51,19 @@ async function opJson<T>(args: string[]): Promise<T> {
|
|
|
46
51
|
|
|
47
52
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
const DEN_ROOT =
|
|
54
|
+
// dotenv paths are relative to the distribution root, never the engine.
|
|
55
|
+
const DEN_ROOT = getDistRoot();
|
|
51
56
|
|
|
52
|
-
const
|
|
53
|
-
|
|
57
|
+
const pick = (...v: Array<string | undefined>): string =>
|
|
58
|
+
v.map((x) => (typeof x === "string" ? x.trim() : "")).find(Boolean) ?? "";
|
|
59
|
+
const VAULT_NAME = pick(
|
|
60
|
+
SECRETS_CFG.developer_keys?.vault,
|
|
61
|
+
SECRETS_CFG.refs?.sudo?.vault,
|
|
62
|
+
);
|
|
63
|
+
const ITEM_NAME = pick(
|
|
64
|
+
SECRETS_CFG.developer_keys?.item,
|
|
65
|
+
SECRETS_CFG.refs?.sudo?.item,
|
|
66
|
+
);
|
|
54
67
|
|
|
55
68
|
// ─── .env reader ─────────────────────────────────────────────────────────────
|
|
56
69
|
|
|
@@ -89,7 +102,7 @@ function parseEnvFile(filePath: string): Record<string, string> {
|
|
|
89
102
|
return result;
|
|
90
103
|
}
|
|
91
104
|
|
|
92
|
-
const DEVELOPER_KEYS =
|
|
105
|
+
const DEVELOPER_KEYS = SECRETS_CFG.developer_keys ?? {};
|
|
93
106
|
|
|
94
107
|
/**
|
|
95
108
|
* The dotenv file `secrets.developer_keys.env_file` names (relative to the
|
|
@@ -560,7 +573,16 @@ async function setupMode(): Promise<void> {
|
|
|
560
573
|
|
|
561
574
|
const isCheck = process.argv.includes("--check");
|
|
562
575
|
|
|
563
|
-
if (
|
|
576
|
+
if (!VAULT_NAME || !ITEM_NAME) {
|
|
577
|
+
// No place for the developer keys: nothing to set up, and no op call.
|
|
578
|
+
console.log(
|
|
579
|
+
`Developer keys item ${notConfiguredMessage("sudo").replace(
|
|
580
|
+
/secrets\.refs\.sudo/,
|
|
581
|
+
"secrets.developer_keys.vault + .item (or secrets.refs.sudo)",
|
|
582
|
+
)}`,
|
|
583
|
+
);
|
|
584
|
+
process.exit(isCheck ? 1 : 0);
|
|
585
|
+
} else if (isCheck) {
|
|
564
586
|
checkMode().catch(() => process.exit(1));
|
|
565
587
|
} else {
|
|
566
588
|
setupMode().catch((err: Error) => {
|
package/scripts/setup-secrets.ts
CHANGED
|
@@ -1,17 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Backend-agnostic developer-secrets setup.
|
|
3
3
|
*
|
|
4
|
-
* Dispatches to the strategy selected by
|
|
5
|
-
*
|
|
6
|
-
* config/local/user.yml). See src/shared/secrets.
|
|
4
|
+
* Dispatches to the `setup` capability of the strategy selected by
|
|
5
|
+
* `secrets.provider` in the org config (override: DEN_SECRETS_PROVIDER or
|
|
6
|
+
* config/local/user.yml). See src/shared/secrets/strategies.ts.
|
|
7
7
|
*
|
|
8
8
|
* Usage:
|
|
9
9
|
* make den-setup-secrets — interactive setup
|
|
10
10
|
* npx tsx scripts/setup-secrets.ts --check — non-interactive; exit 0 when ready
|
|
11
11
|
* npx tsx scripts/setup-secrets.ts --cli — print "<provider> <binary> <formula>"
|
|
12
|
-
* npx tsx scripts/setup-secrets.ts --
|
|
13
|
-
*
|
|
14
|
-
* secrets.required
|
|
12
|
+
* npx tsx scripts/setup-secrets.ts --provider — print the selected strategy id
|
|
13
|
+
* npx tsx scripts/setup-secrets.ts --gate — the up / web gate: passes
|
|
14
|
+
* silently unless secrets.required
|
|
15
|
+
* is true, then like --check
|
|
16
|
+
*
|
|
17
|
+
* With `secrets.provider: none` there is nothing to set up: it says so and
|
|
18
|
+
* exits 0.
|
|
15
19
|
*/
|
|
16
20
|
import { spawnSync } from "child_process";
|
|
17
21
|
import * as path from "path";
|
|
@@ -20,6 +24,8 @@ import chalk from "chalk";
|
|
|
20
24
|
import {
|
|
21
25
|
getSecretsProvider,
|
|
22
26
|
readSecretsConfig,
|
|
27
|
+
resolveProvider,
|
|
28
|
+
strategyFor,
|
|
23
29
|
} from "../src/shared/secrets/index.js";
|
|
24
30
|
|
|
25
31
|
import { envKey, product } from "../src/shared/product.js";
|
|
@@ -31,126 +37,86 @@ function run(cmd: string, args: string[]): number {
|
|
|
31
37
|
return spawnSync(cmd, args, { stdio: "inherit", cwd: ROOT }).status ?? 1;
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const INSTALL: Record<string, string> = {
|
|
39
|
-
doppler: "brew install dopplerhq/cli/doppler",
|
|
40
|
-
op: "brew install 1password-cli",
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
function otherBackends(current: string): string {
|
|
44
|
-
return (
|
|
45
|
-
"Or pick another backend for just you (config/local/user.yml):\n" +
|
|
46
|
-
" secrets:\n" +
|
|
47
|
-
` provider: env # or 1password / doppler (team default: ${current})\n` +
|
|
48
|
-
` or for one shell: ${envKey("SECRETS_PROVIDER")}=env make web`
|
|
49
|
-
);
|
|
50
|
-
}
|
|
40
|
+
const NONE_MESSAGE =
|
|
41
|
+
"No secrets backend is configured (secrets.provider: none), so there is nothing to set up.\n" +
|
|
42
|
+
`To use one, set secrets.provider (doppler | 1password | env) and secrets.refs.<name> in ${product().configFile}.`;
|
|
51
43
|
|
|
52
44
|
async function main(): Promise<number> {
|
|
53
|
-
const provider = getSecretsProvider();
|
|
54
45
|
const cfg = readSecretsConfig();
|
|
55
|
-
const
|
|
46
|
+
const resolved = resolveProvider(cfg);
|
|
47
|
+
const strategy = strategyFor(resolved.name);
|
|
48
|
+
const label = `secrets backend: ${resolved.name}`;
|
|
49
|
+
|
|
50
|
+
// `--provider`: print the selected strategy id (make reads it to offer
|
|
51
|
+
// backend-specific targets). Never probes the backend.
|
|
52
|
+
if (process.argv.includes("--provider")) {
|
|
53
|
+
console.log(resolved.name);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
56
|
|
|
57
57
|
// `--cli`: print "<provider> <binary> <brew formula>" for make setup's
|
|
58
|
-
// install step ("
|
|
58
|
+
// install step ("<provider> - -" when the backend needs no CLI). An unset
|
|
59
|
+
// provider needs nothing installed until something asks for a secret.
|
|
59
60
|
if (process.argv.includes("--cli")) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
? "doppler"
|
|
63
|
-
: provider.name === "1password"
|
|
64
|
-
? "op"
|
|
65
|
-
: "";
|
|
66
|
-
const formula = bin ? INSTALL[bin].replace(/^brew install /, "") : "";
|
|
67
|
-
console.log(`${provider.name} ${bin || "-"} ${formula || "-"}`);
|
|
61
|
+
const cli = resolved.implicit ? undefined : strategy.capabilities.cli;
|
|
62
|
+
console.log(`${resolved.name} ${cli?.bin ?? "-"} ${cli?.formula ?? "-"}`);
|
|
68
63
|
return 0;
|
|
69
64
|
}
|
|
70
65
|
|
|
66
|
+
// The up / web gate: only a distribution that requires its backend
|
|
67
|
+
// (secrets.required) is checked. Nothing required: pass, silently, whatever
|
|
68
|
+
// the provider; a feature that needs a secret reports it when it asks.
|
|
69
|
+
if (gate && !cfg.required) return 0;
|
|
70
|
+
|
|
71
71
|
if (check || gate) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// Signed in but no default scope: secret_env refs name their own
|
|
76
|
-
// (<project>/<config>/<key>), so only the engine's own secrets lack one.
|
|
77
|
-
if (await provider.isReady()) {
|
|
78
|
-
console.error(
|
|
79
|
-
chalk.gray(
|
|
80
|
-
` ${label}: signed in, no default scope set (secret_env refs still resolve;` +
|
|
81
|
-
` test logins and the saved sudo password need one: \`make ${product().cli}-setup-secrets\`).`,
|
|
82
|
-
),
|
|
83
|
-
);
|
|
84
|
-
return 0;
|
|
85
|
-
}
|
|
86
|
-
console.error(
|
|
87
|
-
chalk.yellow(
|
|
88
|
-
`⚠️ ${label} is not ready — continuing.\n` +
|
|
89
|
-
" Test logins and the saved sudo password need it; run " +
|
|
90
|
-
`\`make ${product().cli}-setup-secrets\` when you want them.`,
|
|
91
|
-
),
|
|
92
|
-
);
|
|
72
|
+
if (resolved.name === "none" || (resolved.implicit && !cfg.required)) {
|
|
73
|
+
if (resolved.name === "none")
|
|
74
|
+
console.log(` ✓ ${NONE_MESSAGE.split("\n")[0]}`);
|
|
93
75
|
return 0;
|
|
94
76
|
}
|
|
77
|
+
const r = await getSecretsProvider().preflight();
|
|
78
|
+
if (r.ok) return 0;
|
|
95
79
|
console.error(chalk.red(`✗ ${label}\n${r.message ?? ""}`));
|
|
96
80
|
return 1;
|
|
97
81
|
}
|
|
98
82
|
|
|
83
|
+
const setup = strategy.capabilities.setup;
|
|
84
|
+
if (resolved.name === "none" || !setup) {
|
|
85
|
+
console.log(NONE_MESSAGE);
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
if (resolved.implicit && !cfg.required) {
|
|
89
|
+
// Nothing chose a backend and nothing requires one: no store to set up.
|
|
90
|
+
console.log(
|
|
91
|
+
"secrets.provider is not set, so there is nothing to set up.\n" +
|
|
92
|
+
`To use a backend, set secrets.provider (doppler | 1password | env | none) in ${product().configFile}.`,
|
|
93
|
+
);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
99
96
|
console.log(
|
|
100
97
|
chalk.bold(`\n${product().title} developer secrets (${label})\n`),
|
|
101
98
|
);
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
console.log("→ doppler login");
|
|
114
|
-
if (run("doppler", ["login"]) !== 0) return 1;
|
|
115
|
-
}
|
|
116
|
-
const scope: string[] = [];
|
|
117
|
-
if (cfg.doppler?.project) scope.push("--project", cfg.doppler.project);
|
|
118
|
-
if (cfg.doppler?.config) scope.push("--config", cfg.doppler.config);
|
|
119
|
-
console.log(`→ doppler setup ${scope.join(" ")}`.trim());
|
|
120
|
-
if (
|
|
121
|
-
run("doppler", ["setup", "--no-interactive", ...scope]) !== 0 &&
|
|
122
|
-
scope.length === 0
|
|
123
|
-
) {
|
|
124
|
-
run("doppler", ["setup"]);
|
|
125
|
-
}
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
case "1password":
|
|
129
|
-
if (!installed("op")) {
|
|
130
|
-
console.error(
|
|
131
|
-
chalk.red("✗ The 1Password CLI (op) is not installed.\n") +
|
|
132
|
-
` Install it: ${INSTALL.op}\n\n ` +
|
|
133
|
-
otherBackends(provider.name),
|
|
134
|
-
);
|
|
135
|
-
return 1;
|
|
136
|
-
}
|
|
137
|
-
return run(path.join(ROOT, "node_modules", ".bin", "tsx"), [
|
|
138
|
-
path.join(ROOT, "scripts", "setup-1password.ts"),
|
|
139
|
-
]);
|
|
140
|
-
case "env":
|
|
141
|
-
console.log(
|
|
142
|
-
"The env backend reads secrets from the process environment.\n" +
|
|
143
|
-
`Export them yourself (direnv, CI secrets, \`doppler run -- ${product().cli} up\`).`,
|
|
144
|
-
);
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
99
|
+
const provider = getSecretsProvider();
|
|
100
|
+
const code = await setup({
|
|
101
|
+
cfg,
|
|
102
|
+
provider,
|
|
103
|
+
engineRoot: ROOT,
|
|
104
|
+
cli: product().cli,
|
|
105
|
+
run,
|
|
106
|
+
log: (l) => console.log(l),
|
|
107
|
+
error: (l) => console.error(chalk.red(l)),
|
|
108
|
+
});
|
|
109
|
+
if (code !== 0) return code;
|
|
147
110
|
const r = await provider.preflight();
|
|
148
111
|
console.log(
|
|
149
112
|
r.ok ? chalk.green(`✓ ${label} ready`) : chalk.red(`✗ ${r.message}`),
|
|
150
113
|
);
|
|
114
|
+
if (!r.ok)
|
|
115
|
+
console.log(
|
|
116
|
+
` Or for one shell: ${envKey("SECRETS_PROVIDER")}=none make web`,
|
|
117
|
+
);
|
|
151
118
|
return r.ok ? 0 : 1;
|
|
152
119
|
}
|
|
153
|
-
|
|
154
120
|
main().then(
|
|
155
121
|
(code) => process.exit(code),
|
|
156
122
|
(err: unknown) => {
|
|
@@ -86,7 +86,7 @@ async function globalSetup(_config: FullConfig) {
|
|
|
86
86
|
` ⚠ No credentials for ${role} — authenticated ${role} tests will fail`,
|
|
87
87
|
);
|
|
88
88
|
console.warn(
|
|
89
|
-
`
|
|
89
|
+
` set E2E_${role.toUpperCase()}_EMAIL / _PASSWORD, or set up the secrets backend (secrets.provider)`,
|
|
90
90
|
);
|
|
91
91
|
} else {
|
|
92
92
|
// Token mint failure, IdP misconfiguration, etc. — fail fast
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
|
|
37
37
|
import * as fs from "fs";
|
|
38
38
|
import * as path from "path";
|
|
39
|
-
import { OP_API_KEYS_VAULT, OP_DEV_KEYS_ITEM } from "../../shared/constants.js";
|
|
40
39
|
import {
|
|
41
40
|
getSecretsProvider,
|
|
42
41
|
namedSecretRef,
|
|
42
|
+
notConfiguredMessage,
|
|
43
43
|
type SecretRef,
|
|
44
44
|
} from "../../shared/secrets/index.js";
|
|
45
45
|
import type { Environment } from "../../shared/types/index.js";
|
|
@@ -51,22 +51,12 @@ import { getDistRoot } from "../../shared/paths.js";
|
|
|
51
51
|
import { containsValue } from "../../shared/redact.js";
|
|
52
52
|
// ── Layer 1: captureSecrets ──────────────────────────────────────────────────
|
|
53
53
|
|
|
54
|
-
/**
|
|
54
|
+
/** The Cursor API key's env var name (and the usual field label). */
|
|
55
55
|
export const CURSOR_API_KEY_FIELD = "CURSOR_API_KEY";
|
|
56
56
|
|
|
57
57
|
/** Name of the agent-key ref under `secrets.refs` in the org config. */
|
|
58
58
|
export const JOURNEYS_AGENT_KEY_REF = "journeys_agent_key";
|
|
59
59
|
|
|
60
|
-
/**
|
|
61
|
-
* Where the agent key lives when `secrets.refs.journeys_agent_key` is not
|
|
62
|
-
* configured: the engineer's own developer-keys item (neutral default).
|
|
63
|
-
*/
|
|
64
|
-
export const JOURNEYS_AGENT_KEY_DEFAULT: SecretRef = {
|
|
65
|
-
vault: OP_API_KEYS_VAULT,
|
|
66
|
-
item: OP_DEV_KEYS_ITEM,
|
|
67
|
-
field: CURSOR_API_KEY_FIELD,
|
|
68
|
-
};
|
|
69
|
-
|
|
70
60
|
/**
|
|
71
61
|
* Secrets captured into memory before environment sanitization.
|
|
72
62
|
*
|
|
@@ -84,6 +74,7 @@ export interface OpFieldRequest {
|
|
|
84
74
|
item: string;
|
|
85
75
|
vault: string;
|
|
86
76
|
field: string;
|
|
77
|
+
key?: string;
|
|
87
78
|
}
|
|
88
79
|
|
|
89
80
|
export interface CaptureSecretsDeps {
|
|
@@ -91,8 +82,11 @@ export interface CaptureSecretsDeps {
|
|
|
91
82
|
opReadField?: (request: OpFieldRequest) => Promise<string>;
|
|
92
83
|
/** Env-var source; defaults to process.env. */
|
|
93
84
|
env?: NodeJS.ProcessEnv;
|
|
94
|
-
/**
|
|
95
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Where the key lives; defaults to the configured `journeys_agent_key`
|
|
87
|
+
* ref. null: not configured (there is no built-in location).
|
|
88
|
+
*/
|
|
89
|
+
agentKeyRef?: SecretRef | null;
|
|
96
90
|
}
|
|
97
91
|
|
|
98
92
|
/** Default reader: the configured secrets strategy. */
|
|
@@ -118,14 +112,25 @@ export async function captureSecrets(
|
|
|
118
112
|
if (cursorApiKey.length === 0) {
|
|
119
113
|
let opError = "";
|
|
120
114
|
const ref =
|
|
121
|
-
deps.agentKeyRef
|
|
122
|
-
|
|
115
|
+
deps.agentKeyRef !== undefined
|
|
116
|
+
? deps.agentKeyRef
|
|
117
|
+
: namedSecretRef(JOURNEYS_AGENT_KEY_REF);
|
|
118
|
+
if (!ref) {
|
|
119
|
+
// No configured place for the key: say so, without asking a backend.
|
|
120
|
+
throw new Error(
|
|
121
|
+
`No Cursor API key available for journey agents.\n` +
|
|
122
|
+
` Options:\n` +
|
|
123
|
+
` • set the ${CURSOR_API_KEY_FIELD} env var\n` +
|
|
124
|
+
` • secrets backend: ${notConfiguredMessage(JOURNEYS_AGENT_KEY_REF)}\n`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
123
127
|
try {
|
|
124
128
|
cursorApiKey = (
|
|
125
129
|
await opReadField({
|
|
126
130
|
item: ref.item,
|
|
127
131
|
vault: ref.vault,
|
|
128
132
|
field: ref.field,
|
|
133
|
+
...(ref.key ? { key: ref.key } : {}),
|
|
129
134
|
})
|
|
130
135
|
).trim();
|
|
131
136
|
} catch (error) {
|
|
@@ -137,8 +142,8 @@ export async function captureSecrets(
|
|
|
137
142
|
` Options:\n` +
|
|
138
143
|
` • CI: set the ${CURSOR_API_KEY_FIELD} env var\n` +
|
|
139
144
|
` • Local: sign in to the secrets backend (make ${product().cli}-setup-secrets) and\n` +
|
|
140
|
-
` make sure
|
|
141
|
-
` ${ref.field}
|
|
145
|
+
` make sure secrets.refs.${JOURNEYS_AGENT_KEY_REF} (item "${ref.item}", vault "${ref.vault}",\n` +
|
|
146
|
+
` field ${ref.field}) holds the key\n` +
|
|
142
147
|
(opError ? ` Secrets backend error: ${opError}\n` : ""),
|
|
143
148
|
);
|
|
144
149
|
}
|
|
@@ -32,7 +32,7 @@ import { product } from "../../../shared/product.js";
|
|
|
32
32
|
* Mint a fresh authenticated browser session for `role` against `env`'s
|
|
33
33
|
* web surface and return the storageState auth-file path (and nothing
|
|
34
34
|
* else). Throws a loud, actionable error when credentials cannot be
|
|
35
|
-
* sourced — naming both the
|
|
35
|
+
* sourced — naming both the secrets-backend path (local) and the E2E_* env-var
|
|
36
36
|
* path (CI), mirroring browser/global-setup.ts.
|
|
37
37
|
*/
|
|
38
38
|
export async function provisionBrowserSession(
|
|
@@ -47,7 +47,7 @@ export async function provisionBrowserSession(
|
|
|
47
47
|
if (!credentials) {
|
|
48
48
|
const localHint =
|
|
49
49
|
role === "admin"
|
|
50
|
-
? ` Local: make auth-configure env=${env}
|
|
50
|
+
? ` Local: with secrets.provider: 1password, make auth-configure env=${env}\n`
|
|
51
51
|
: ` Local: sign in to the secrets backend (make ${product().cli}-setup-secrets); the ${role} account comes from test_accounts in the org config\n`;
|
|
52
52
|
throw new Error(
|
|
53
53
|
`No credentials for ${role}.\n` +
|
|
@@ -145,7 +145,7 @@ async function main() {
|
|
|
145
145
|
' This builds the "default" workspace — every standard system repo,\n' +
|
|
146
146
|
` all managed: ${product().cli} clones and keeps each one up to date for\n` +
|
|
147
147
|
" you, no checkouts or decisions required. For anything you actively\n" +
|
|
148
|
-
" develop, flip that repo to dev-managed in the web UX (
|
|
148
|
+
" develop, flip that repo to dev-managed in the web UX (Workspaces)\n" +
|
|
149
149
|
" and point it at your own checkout.\n",
|
|
150
150
|
);
|
|
151
151
|
|
|
@@ -183,7 +183,7 @@ async function main() {
|
|
|
183
183
|
`\n ✓ Default workspace configured with ${seed.length} service(s), all managed.`,
|
|
184
184
|
);
|
|
185
185
|
console.log(
|
|
186
|
-
" Change any repo to dev-managed in the web UX: make web →
|
|
186
|
+
" Change any repo to dev-managed in the web UX: make web → Workspaces.",
|
|
187
187
|
);
|
|
188
188
|
console.log(` Run 'make ${product().cli}-up' to start your services.\n`);
|
|
189
189
|
}
|
|
@@ -34,7 +34,7 @@ const PROJECT_ROOT = getDistRoot();
|
|
|
34
34
|
const AUTH_DIR = path.resolve(PROJECT_ROOT, "playwright/.auth");
|
|
35
35
|
|
|
36
36
|
// Per-engineer admin config — stores email + secret reference (no password)
|
|
37
|
-
// Created by: make auth-configure [env=<env>]
|
|
37
|
+
// Created by: make auth-configure [env=<env>] (secrets.provider: 1password)
|
|
38
38
|
// Lookup order: config/local/admin-auth-{env}.json → config/local/admin-auth.json
|
|
39
39
|
const LOCAL_CONFIG_DIR = path.resolve(PROJECT_ROOT, "config/local");
|
|
40
40
|
|
|
@@ -1,51 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Secrets-backed persistence for the local-dev sudo password.
|
|
3
3
|
*
|
|
4
|
-
* Goes through the configured secrets strategy (
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Goes through the configured secrets strategy (see src/shared/secrets),
|
|
5
|
+
* addressing the named ref `sudo`. There is no built-in location: a
|
|
6
|
+
* distribution that wants the saved password sets `secrets.refs.sudo` in its
|
|
7
|
+
* config. Without it, the vault path is "not configured" and only the
|
|
8
|
+
* interactive unlock is offered; nothing is spawned.
|
|
9
9
|
*
|
|
10
10
|
* All read helpers fail *soft*: if the backend isn't installed, the user isn't
|
|
11
|
-
* signed in, or the value is missing, they return
|
|
12
|
-
* fall back to the manual unlock
|
|
11
|
+
* signed in, the ref is not configured or the value is missing, they return
|
|
12
|
+
* null/false so the server can fall back to the manual unlock without
|
|
13
|
+
* surfacing CLI noise.
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
|
-
import {
|
|
16
|
-
OP_API_KEYS_VAULT,
|
|
17
|
-
OP_DEV_KEYS_ITEM,
|
|
18
|
-
OP_DEN_SUDO_FIELD,
|
|
19
|
-
} from "../constants.js";
|
|
20
16
|
import {
|
|
21
17
|
getSecretsProvider,
|
|
22
18
|
namedSecretRef,
|
|
19
|
+
notConfiguredMessage,
|
|
20
|
+
readSecretsConfig,
|
|
21
|
+
secretsStatus,
|
|
22
|
+
SecretsNotConfiguredError,
|
|
23
23
|
type SecretRef,
|
|
24
|
+
type SecretsProviderName,
|
|
24
25
|
} from "../secrets/index.js";
|
|
26
|
+
import { product } from "../product.js";
|
|
25
27
|
|
|
26
|
-
/**
|
|
27
|
-
export const
|
|
28
|
-
vault: OP_API_KEYS_VAULT,
|
|
29
|
-
item: OP_DEV_KEYS_ITEM,
|
|
30
|
-
field: OP_DEN_SUDO_FIELD,
|
|
31
|
-
};
|
|
28
|
+
/** Name of the sudo ref under `secrets.refs`. */
|
|
29
|
+
export const SUDO_REF_NAME = "sudo";
|
|
32
30
|
|
|
33
|
-
/** The
|
|
34
|
-
function sudoRef(): SecretRef {
|
|
35
|
-
|
|
31
|
+
/** The configured sudo ref, or null when `secrets.refs.sudo` is not set. */
|
|
32
|
+
export function sudoRef(): SecretRef | null {
|
|
33
|
+
try {
|
|
34
|
+
return namedSecretRef(SUDO_REF_NAME, readSecretsConfig());
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
36
38
|
}
|
|
37
39
|
|
|
38
|
-
/** True when the
|
|
40
|
+
/** True when the sudo ref is configured AND its backend is ready. */
|
|
39
41
|
export async function isSecretsBackendReady(): Promise<boolean> {
|
|
42
|
+
if (!sudoRef()) return false;
|
|
40
43
|
return getSecretsProvider().isReady();
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
/** @deprecated use isSecretsBackendReady */
|
|
44
47
|
export const isOpSignedIn = isSecretsBackendReady;
|
|
45
48
|
|
|
46
|
-
/** Read the stored sudo password. Null when unavailable or empty. */
|
|
49
|
+
/** Read the stored sudo password. Null when unavailable, unconfigured or empty. */
|
|
47
50
|
export async function readSudoFromVault(): Promise<string | null> {
|
|
48
|
-
|
|
51
|
+
const ref = sudoRef();
|
|
52
|
+
if (!ref) return null;
|
|
53
|
+
return getSecretsProvider().tryGet(ref);
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
/** Whether a non-empty sudo password is stored (without returning it). */
|
|
@@ -55,15 +60,72 @@ export async function hasSudoInVault(): Promise<boolean> {
|
|
|
55
60
|
|
|
56
61
|
/** Save (or overwrite) the sudo password. Throws on backend errors. */
|
|
57
62
|
export async function saveSudoToVault(password: string): Promise<void> {
|
|
58
|
-
|
|
63
|
+
const ref = sudoRef();
|
|
64
|
+
if (!ref)
|
|
65
|
+
throw new SecretsNotConfiguredError(notConfiguredMessage(SUDO_REF_NAME));
|
|
66
|
+
await getSecretsProvider().set(ref, password);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** What the UI's unlock form may offer: `/api/sudo-password/vault-status`. */
|
|
70
|
+
export interface SudoVaultStatus {
|
|
71
|
+
/** The selected strategy id. */
|
|
72
|
+
backend: SecretsProviderName;
|
|
73
|
+
/** Name people see ("1Password"), for "Save to <label>". */
|
|
74
|
+
label: string;
|
|
75
|
+
/** `secrets.refs.sudo` is set for this backend. */
|
|
76
|
+
configured: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* The backend can work here, whether or not a sudo ref is configured
|
|
79
|
+
* (`configured` says that). With no ref: detect() only (a PATH check,
|
|
80
|
+
* nothing spawned), and only for an explicitly chosen provider; an unset
|
|
81
|
+
* provider is never probed (it is not needed). With a ref: detect() and
|
|
82
|
+
* signed in.
|
|
83
|
+
*/
|
|
84
|
+
backendAvailable: boolean;
|
|
85
|
+
/** Why it is not available (not configured, CLI not on PATH, signed out). */
|
|
86
|
+
reason?: string;
|
|
87
|
+
/** The distribution's org config file (e.g. config/acme.yml), for UI copy. */
|
|
88
|
+
configFile: string;
|
|
89
|
+
hasStored: boolean;
|
|
90
|
+
/** @deprecated alias of backendAvailable; removed in a later minor. */
|
|
91
|
+
opAvailable: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function sudoVaultStatus(): Promise<SudoVaultStatus> {
|
|
95
|
+
const cfg = readSecretsConfig();
|
|
96
|
+
const ref = sudoRef();
|
|
97
|
+
const pre = secretsStatus(cfg, process.env, { detect: false });
|
|
98
|
+
// Probe only what is needed or was chosen: a ref, or an explicit backend.
|
|
99
|
+
const probe = !!ref || (!pre.implicit && pre.backend !== "none");
|
|
100
|
+
const st = probe ? secretsStatus(cfg, process.env) : pre;
|
|
101
|
+
const out = (
|
|
102
|
+
backendAvailable: boolean,
|
|
103
|
+
hasStored: boolean,
|
|
104
|
+
reason?: string,
|
|
105
|
+
): SudoVaultStatus => ({
|
|
106
|
+
backend: st.backend,
|
|
107
|
+
label: st.label,
|
|
108
|
+
configured: !!ref,
|
|
109
|
+
backendAvailable,
|
|
110
|
+
...(reason ? { reason } : {}),
|
|
111
|
+
configFile: product().configFile,
|
|
112
|
+
hasStored,
|
|
113
|
+
opAvailable: backendAvailable,
|
|
114
|
+
});
|
|
115
|
+
if (!ref)
|
|
116
|
+
return out(
|
|
117
|
+
probe && st.detect.available,
|
|
118
|
+
false,
|
|
119
|
+
notConfiguredMessage(SUDO_REF_NAME),
|
|
120
|
+
);
|
|
121
|
+
if (!st.detect.available)
|
|
122
|
+
return out(false, false, `not available: ${st.detect.reason ?? ""}`.trim());
|
|
123
|
+
if (!(await getSecretsProvider().isReady()))
|
|
124
|
+
return out(false, false, `${st.label} is not signed in`);
|
|
125
|
+
return out(true, await hasSudoInVault());
|
|
59
126
|
}
|
|
60
127
|
|
|
61
128
|
/** Name of the active backend, for UI copy ("Save to Doppler"). */
|
|
62
129
|
export function secretsBackendLabel(): string {
|
|
63
|
-
|
|
64
|
-
return n === "1password"
|
|
65
|
-
? "1Password"
|
|
66
|
-
: n === "doppler"
|
|
67
|
-
? "Doppler"
|
|
68
|
-
: "environment";
|
|
130
|
+
return secretsStatus().label;
|
|
69
131
|
}
|
|
@@ -115,9 +115,13 @@ const localJwtProvider: TokenProvider = {
|
|
|
115
115
|
const l = cfg.auth?.localJwt;
|
|
116
116
|
if (!l) throw new Error(`auth.localJwt is not set for ${cfg.name}`);
|
|
117
117
|
const env = o.env ?? process.env;
|
|
118
|
+
// An explicit key the environment file names: not a default ref, so it
|
|
119
|
+
// resolves as it always has (env first, then the selected backend).
|
|
120
|
+
const fromEnv = env[l.secretKey];
|
|
121
|
+
const backend = fromEnv ? null : getSecretsProvider();
|
|
118
122
|
const secret =
|
|
119
|
-
|
|
120
|
-
(await
|
|
123
|
+
fromEnv ??
|
|
124
|
+
(await backend!.tryGet({
|
|
121
125
|
vault: "",
|
|
122
126
|
item: "",
|
|
123
127
|
field: l.secretKey,
|
|
@@ -125,7 +129,7 @@ const localJwtProvider: TokenProvider = {
|
|
|
125
129
|
}));
|
|
126
130
|
if (!secret) {
|
|
127
131
|
throw new Error(
|
|
128
|
-
`local-jwt signing secret ${l.secretKey} not found in env or the secrets backend`,
|
|
132
|
+
`local-jwt signing secret ${l.secretKey} not found in env or the ${backend?.name ?? "configured"} secrets backend (secrets.provider)`,
|
|
129
133
|
);
|
|
130
134
|
}
|
|
131
135
|
const ttl = l.ttlSeconds ?? 3600;
|