@monster-orange/den 0.0.0 → 0.2.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 +204 -0
- package/LICENSE +21 -0
- package/README.md +165 -1
- package/bin/den +68 -0
- package/docs/distributions.md +499 -0
- package/make/demo.mk +82 -0
- package/make/den.mk +80 -0
- package/make/journeys.mk +57 -0
- package/make/local_dev.mk +154 -0
- package/make/security.mk +59 -0
- package/make/test.mk +405 -0
- package/package.json +101 -4
- package/scripts/auth-configure.ts +415 -0
- package/scripts/heal-config.ts +92 -0
- package/scripts/jsonl-playwright-reporter.js +70 -0
- package/scripts/manifest-read.cjs +91 -0
- package/scripts/setup-1password.ts +570 -0
- package/scripts/setup-secrets.ts +149 -0
- package/scripts/view-results.js +156 -0
- package/service/install.sh +112 -0
- package/src/pillars/api/fixtures/index.ts +128 -0
- package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
- package/src/pillars/api/global-setup.ts +103 -0
- package/src/pillars/api/playwright.config.ts +105 -0
- package/src/pillars/api/schemas/example-api.json +110 -0
- package/src/pillars/api/tests/health/health.spec.ts +19 -0
- package/src/pillars/api/tests/health/manifest.yml +6 -0
- package/src/pillars/browser/fixtures/index.ts +232 -0
- package/src/pillars/browser/fixtures/package.json +10 -0
- package/src/pillars/browser/global-setup.ts +140 -0
- package/src/pillars/browser/global-teardown.ts +48 -0
- package/src/pillars/browser/playwright.config.ts +212 -0
- package/src/pillars/browser/setup.ts +219 -0
- package/src/pillars/journeys/app-config.ts +237 -0
- package/src/pillars/journeys/discover.ts +160 -0
- package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
- package/src/pillars/journeys/observability/events.ts +288 -0
- package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
- package/src/pillars/journeys/report/friction.ts +304 -0
- package/src/pillars/journeys/report/junit.ts +105 -0
- package/src/pillars/journeys/report/render.ts +273 -0
- package/src/pillars/journeys/report/results.ts +323 -0
- package/src/pillars/journeys/run.ts +673 -0
- package/src/pillars/journeys/runner.ts +1025 -0
- package/src/pillars/journeys/sandbox.ts +403 -0
- package/src/pillars/journeys/spec/loader.ts +152 -0
- package/src/pillars/journeys/spec/schema.ts +208 -0
- package/src/pillars/journeys/teardown.ts +98 -0
- package/src/pillars/journeys/tools/browser-session.ts +219 -0
- package/src/pillars/journeys/tools/browser-tool.ts +914 -0
- package/src/pillars/journeys/tools/http-tool.ts +564 -0
- package/src/pillars/journeys/tools/ledger.ts +278 -0
- package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
- package/src/pillars/journeys/verify/assertions.ts +502 -0
- package/src/pillars/journeys/verify/verdict.ts +71 -0
- package/src/pillars/perf/build-report.ts +36 -0
- package/src/pillars/perf/k6.config.js +112 -0
- package/src/pillars/perf/run.ts +345 -0
- package/src/pillars/perf/scenarios/load-profile.js +97 -0
- package/src/pillars/perf/staging.ts +73 -0
- package/src/pillars/security/discover.ts +124 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +757 -0
- package/src/pillars/smoke/admin-probe.ts +68 -0
- package/src/pillars/smoke/base-url.ts +56 -0
- package/src/pillars/smoke/cleanup.ts +8 -0
- package/src/pillars/smoke/discover.ts +237 -0
- package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
- package/src/pillars/smoke/fixture-manager.ts +217 -0
- package/src/pillars/smoke/fixtures.ts +468 -0
- package/src/pillars/smoke/global-setup-all.ts +262 -0
- package/src/pillars/smoke/global-setup-global.ts +262 -0
- package/src/pillars/smoke/global-setup.ts +273 -0
- package/src/pillars/smoke/global-teardown-all.ts +8 -0
- package/src/pillars/smoke/global-teardown-global.ts +8 -0
- package/src/pillars/smoke/global-teardown.ts +139 -0
- package/src/pillars/smoke/playwright-all.config.ts +147 -0
- package/src/pillars/smoke/playwright-global.config.ts +141 -0
- package/src/pillars/smoke/playwright.config.ts +162 -0
- package/src/pillars/smoke/setup-env.ts +132 -0
- package/src/pillars/smoke/setup-global.ts +126 -0
- package/src/pillars/smoke/setup.ts +115 -0
- package/src/pillars/smoke/smoke-repo.ts +16 -0
- package/src/pillars/smoke/staging.ts +49 -0
- package/src/pillars/suites/run.ts +147 -0
- package/src/scripts/configure-aws-profiles.ts +114 -0
- package/src/scripts/register-repos.ts +307 -0
- package/src/scripts/setup-default-workspace.ts +203 -0
- package/src/shared/agent-actor.ts +77 -0
- package/src/shared/applications.ts +138 -0
- package/src/shared/auth/api-token.ts +416 -0
- package/src/shared/auth/auth0-helper.ts +284 -0
- package/src/shared/auth/headless-login.ts +265 -0
- package/src/shared/auth/jwt-helper.ts +186 -0
- package/src/shared/auth/sudo-vault.ts +69 -0
- package/src/shared/auth/test-accounts.ts +98 -0
- package/src/shared/auth/token-injection.ts +184 -0
- package/src/shared/auth/token-providers/auth0.ts +80 -0
- package/src/shared/auth/token-providers/index.ts +182 -0
- package/src/shared/auth/token-refresh.ts +273 -0
- package/src/shared/banner.ts +15 -0
- package/src/shared/brand.ts +79 -0
- package/src/shared/cli-tabs.ts +131 -0
- package/src/shared/constants.ts +128 -0
- package/src/shared/den-config.ts +688 -0
- package/src/shared/dev-metrics/emit.ts +354 -0
- package/src/shared/edge/config.ts +229 -0
- package/src/shared/edge/render.ts +221 -0
- package/src/shared/edge/runtime.ts +268 -0
- package/src/shared/edge/status.ts +74 -0
- package/src/shared/edge/types.ts +62 -0
- package/src/shared/engine.ts +67 -0
- package/src/shared/environment/aws-profiles.ts +49 -0
- package/src/shared/environment/env-selector.ts +161 -0
- package/src/shared/environment/playwright-env.ts +82 -0
- package/src/shared/environment-manager/catalog.ts +77 -0
- package/src/shared/environment-manager/clone-guard.ts +163 -0
- package/src/shared/environment-manager/dag.ts +286 -0
- package/src/shared/environment-manager/default-workspace.ts +182 -0
- package/src/shared/environment-manager/derivation/alias.ts +90 -0
- package/src/shared/environment-manager/derivation/cache.ts +97 -0
- package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
- package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
- package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
- package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
- package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
- package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
- package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
- package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
- package/src/shared/environment-manager/derivation/scan.ts +160 -0
- package/src/shared/environment-manager/derivation/text.ts +40 -0
- package/src/shared/environment-manager/derivation/types.ts +69 -0
- package/src/shared/environment-manager/health.ts +635 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +108 -0
- package/src/shared/environment-manager/log-tail.ts +213 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +335 -0
- package/src/shared/environment-manager/readiness.ts +93 -0
- package/src/shared/environment-manager/register-local.ts +72 -0
- package/src/shared/environment-manager/secrets-check.ts +260 -0
- package/src/shared/environment-manager/service-container.ts +47 -0
- package/src/shared/environment-manager/service-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1603 -0
- package/src/shared/environment-manager/sync-targets.ts +66 -0
- package/src/shared/environment-manager/up-plan.ts +326 -0
- package/src/shared/environment-manager/workspace-manager.ts +905 -0
- package/src/shared/environment-manager/workspaces.ts +860 -0
- package/src/shared/manifest/deps.ts +27 -0
- package/src/shared/manifest/merge-override.ts +75 -0
- package/src/shared/manifest/migrate-manifest.ts +101 -0
- package/src/shared/manifest/placement.ts +321 -0
- package/src/shared/manifest/read-manifest.ts +278 -0
- package/src/shared/manifest/schema.ts +313 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-layers.ts +229 -0
- package/src/shared/manifest/validate-manifest.ts +200 -0
- package/src/shared/manifest/workspace-manifests.ts +92 -0
- package/src/shared/open-url.ts +122 -0
- package/src/shared/org.ts +44 -0
- package/src/shared/paths.ts +190 -0
- package/src/shared/perf/optional-fields.ts +105 -0
- package/src/shared/perf/report-normalizer.ts +404 -0
- package/src/shared/perf/report-paths.ts +19 -0
- package/src/shared/perf/report-store.ts +110 -0
- package/src/shared/perf/report-types.ts +70 -0
- package/src/shared/perf/test-paths.ts +25 -0
- package/src/shared/process/log-rotation.ts +147 -0
- package/src/shared/process/spawn-streaming.ts +229 -0
- package/src/shared/product.ts +92 -0
- package/src/shared/profile.ts +141 -0
- package/src/shared/redact.ts +107 -0
- package/src/shared/repo-kind.ts +32 -0
- package/src/shared/reporting/jsonl-reporter.ts +131 -0
- package/src/shared/reporting/slack-reporter.ts +340 -0
- package/src/shared/results/result-reader.ts +266 -0
- package/src/shared/results/result-writer.ts +168 -0
- package/src/shared/secrets/doppler.ts +126 -0
- package/src/shared/secrets/env.ts +48 -0
- package/src/shared/secrets/index.ts +197 -0
- package/src/shared/secrets/keys.ts +33 -0
- package/src/shared/secrets/onepassword.ts +75 -0
- package/src/shared/secrets/types.ts +52 -0
- package/src/shared/service-url.ts +119 -0
- package/src/shared/smoke/branch-resolver.ts +141 -0
- package/src/shared/smoke/git-remote.ts +70 -0
- package/src/shared/smoke/repo-cloner.ts +151 -0
- package/src/shared/stacks/config.ts +137 -0
- package/src/shared/stacks/manifest-commands.ts +118 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -0
- package/src/shared/stacks/status.ts +99 -0
- package/src/shared/stacks/types.ts +53 -0
- package/src/shared/terminal/tabs.ts +212 -0
- package/src/shared/test-discovery/module-scanner.ts +569 -0
- package/src/shared/types/index.ts +701 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +148 -0
- package/src/ui/cli/config-command.ts +68 -0
- package/src/ui/cli/create-main.ts +9 -0
- package/src/ui/cli/create.ts +360 -0
- package/src/ui/cli/edge-command.ts +81 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +570 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3742 -0
- package/src/ui/cli/init.ts +855 -0
- package/src/ui/cli/invocation-target.ts +175 -0
- package/src/ui/cli/logs-command.ts +104 -0
- package/src/ui/cli/mcp-main.ts +13 -0
- package/src/ui/cli/mcp-stdout-guard.ts +10 -0
- package/src/ui/cli/module-selection.ts +31 -0
- package/src/ui/cli/prompts.ts +767 -0
- package/src/ui/cli/results-viewer.ts +436 -0
- package/src/ui/cli/secrets-preflight.ts +91 -0
- package/src/ui/cli/stack-command.ts +117 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1319 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/mcp/api.ts +284 -0
- package/src/ui/mcp/guards.ts +141 -0
- package/src/ui/mcp/server.ts +147 -0
- package/src/ui/mcp/tools.ts +565 -0
- package/src/ui/server/audit.ts +71 -0
- package/src/ui/server/changelog-parser.ts +105 -0
- package/src/ui/server/daemon-reaper.ts +129 -0
- package/src/ui/server/editor-manager.ts +301 -0
- package/src/ui/server/github-service.ts +223 -0
- package/src/ui/server/graph-prefs.ts +89 -0
- package/src/ui/server/health-row.ts +15 -0
- package/src/ui/server/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3350 -0
- package/src/ui/server/log-history.ts +453 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +496 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/graph-model.js +416 -0
- package/src/ui/server/public/graph.css +976 -0
- package/src/ui/server/public/graph.js +986 -0
- package/src/ui/server/public/header.js +1373 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +5344 -0
- package/src/ui/server/public/logs.html +1496 -0
- package/src/ui/server/public/panel.js +787 -0
- package/src/ui/server/public/perf-report.js +359 -0
- package/src/ui/server/public/release.html +2111 -0
- package/src/ui/server/public/services.html +1103 -0
- package/src/ui/server/public/workspace-picker.js +69 -0
- package/src/ui/server/readonly-routes.ts +200 -0
- package/src/ui/server/release-deploy-session.ts +246 -0
- package/src/ui/server/release-service.ts +1125 -0
- package/src/ui/server/sudo-store.ts +111 -0
- package/src/ui/server/ui-assets.ts +73 -0
- package/src/ui/server/workspace-routes.ts +2185 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result Writer
|
|
3
|
+
* Writes test results to JSONL (JSON Lines) format
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
appendFile,
|
|
8
|
+
writeFile,
|
|
9
|
+
readFile,
|
|
10
|
+
mkdir,
|
|
11
|
+
rename,
|
|
12
|
+
unlink,
|
|
13
|
+
} from "fs/promises";
|
|
14
|
+
import { resolve, join } from "path";
|
|
15
|
+
import type { TestResult, Pillar } from "../types/index.js";
|
|
16
|
+
|
|
17
|
+
export class ResultWriter {
|
|
18
|
+
private resultsDir: string;
|
|
19
|
+
|
|
20
|
+
constructor(resultsDir: string = "./results") {
|
|
21
|
+
this.resultsDir = resolve(resultsDir);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Write a single test result to JSONL file
|
|
26
|
+
*/
|
|
27
|
+
async writeResult(result: TestResult): Promise<void> {
|
|
28
|
+
const filename = this.getFilename(result.pillar, result.environment);
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// Ensure results directory exists
|
|
32
|
+
await mkdir(this.resultsDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
// Append result as JSON line
|
|
35
|
+
const line = JSON.stringify(result) + "\n";
|
|
36
|
+
await appendFile(filename, line);
|
|
37
|
+
|
|
38
|
+
// Update index
|
|
39
|
+
await this.updateIndex(result.pillar, result.environment);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
throw new Error(`Failed to write result: ${(error as Error).message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Write multiple test results in batch
|
|
47
|
+
*/
|
|
48
|
+
async writeResults(results: TestResult[]): Promise<void> {
|
|
49
|
+
if (results.length === 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Group results by pillar and environment
|
|
54
|
+
const grouped = this.groupResults(results);
|
|
55
|
+
|
|
56
|
+
for (const [key, groupResults] of Object.entries(grouped)) {
|
|
57
|
+
const [pillar, environment] = key.split(":");
|
|
58
|
+
const filename = this.getFilename(pillar as Pillar, environment);
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
// Ensure results directory exists
|
|
62
|
+
await mkdir(this.resultsDir, { recursive: true });
|
|
63
|
+
|
|
64
|
+
// Write all results as JSON lines
|
|
65
|
+
const lines =
|
|
66
|
+
groupResults.map((r) => JSON.stringify(r)).join("\n") + "\n";
|
|
67
|
+
await appendFile(filename, lines);
|
|
68
|
+
|
|
69
|
+
// Update index
|
|
70
|
+
await this.updateIndex(pillar as Pillar, environment);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`Failed to write results for ${pillar}/${environment}: ${(error as Error).message}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Update results index for quick lookups
|
|
81
|
+
*/
|
|
82
|
+
private async updateIndex(
|
|
83
|
+
pillar: Pillar,
|
|
84
|
+
environment: string,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
const indexPath = join(this.resultsDir, "index.json");
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
let index: Record<string, unknown> = {};
|
|
90
|
+
|
|
91
|
+
// Load existing index. Distinguish "no index yet" (ENOENT → start fresh)
|
|
92
|
+
// from a corrupt/unreadable existing file — clobbering a corrupt index
|
|
93
|
+
// with {} would silently wipe every other pillar/env entry.
|
|
94
|
+
let content: string | undefined;
|
|
95
|
+
try {
|
|
96
|
+
content = await readFile(indexPath, "utf-8");
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
|
|
99
|
+
}
|
|
100
|
+
if (content !== undefined) {
|
|
101
|
+
const parsed = JSON.parse(content) as unknown;
|
|
102
|
+
if (
|
|
103
|
+
parsed === null ||
|
|
104
|
+
typeof parsed !== "object" ||
|
|
105
|
+
Array.isArray(parsed)
|
|
106
|
+
) {
|
|
107
|
+
throw new Error("existing index.json is not an object");
|
|
108
|
+
}
|
|
109
|
+
index = parsed as Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const key = `${pillar}:${environment}`;
|
|
113
|
+
const date = new Date().toISOString().split("T")[0];
|
|
114
|
+
|
|
115
|
+
// Update index entry
|
|
116
|
+
index[key] = {
|
|
117
|
+
pillar,
|
|
118
|
+
environment,
|
|
119
|
+
lastUpdated: new Date().toISOString(),
|
|
120
|
+
latestFile: this.getFilename(pillar, environment),
|
|
121
|
+
date,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Write atomically (temp + rename) so a crash/concurrent worker can't
|
|
125
|
+
// leave a half-written index.json.
|
|
126
|
+
const tmp = `${indexPath}.${process.pid}.tmp`;
|
|
127
|
+
try {
|
|
128
|
+
await writeFile(tmp, JSON.stringify(index, null, 2));
|
|
129
|
+
await rename(tmp, indexPath);
|
|
130
|
+
} catch (err) {
|
|
131
|
+
// Don't leave an orphaned temp file behind when the write/rename fails.
|
|
132
|
+
await unlink(tmp).catch(() => {});
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
} catch (error) {
|
|
136
|
+
// Index update is non-critical, just log error
|
|
137
|
+
console.warn(`Failed to update index: ${(error as Error).message}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get filename for results
|
|
143
|
+
*/
|
|
144
|
+
private getFilename(pillar: string, environment: string): string {
|
|
145
|
+
const date = new Date().toISOString().split("T")[0];
|
|
146
|
+
return join(this.resultsDir, `${pillar}-${environment}-${date}.jsonl`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Group results by pillar and environment
|
|
151
|
+
*/
|
|
152
|
+
private groupResults(results: TestResult[]): Record<string, TestResult[]> {
|
|
153
|
+
return results.reduce(
|
|
154
|
+
(acc, result) => {
|
|
155
|
+
const key = `${result.pillar}:${result.environment}`;
|
|
156
|
+
if (!acc[key]) {
|
|
157
|
+
acc[key] = [];
|
|
158
|
+
}
|
|
159
|
+
acc[key].push(result);
|
|
160
|
+
return acc;
|
|
161
|
+
},
|
|
162
|
+
{} as Record<string, TestResult[]>,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Export singleton instance
|
|
168
|
+
export const resultWriter = new ResultWriter();
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doppler strategy — reads/writes flat keys with the `doppler` CLI.
|
|
3
|
+
* Selected with `secrets.provider: doppler` (the default).
|
|
4
|
+
*
|
|
5
|
+
* Project/config come from `secrets.doppler.{project,config}` in
|
|
6
|
+
* config/den.yml, else from the directory's `doppler setup` scope,
|
|
7
|
+
* else from DOPPLER_PROJECT / DOPPLER_CONFIG / DOPPLER_TOKEN.
|
|
8
|
+
*/
|
|
9
|
+
import { execFile, spawn } from "child_process";
|
|
10
|
+
import { promisify } from "util";
|
|
11
|
+
import { flatKeyFor } from "./keys.js";
|
|
12
|
+
import type { PreflightResult, SecretRef, SecretsProvider } from "./types.js";
|
|
13
|
+
|
|
14
|
+
import { product } from "../product.js";
|
|
15
|
+
const execFileAsync = promisify(execFile);
|
|
16
|
+
|
|
17
|
+
export interface DopplerOptions {
|
|
18
|
+
project?: string;
|
|
19
|
+
config?: string;
|
|
20
|
+
keys?: Record<string, string>;
|
|
21
|
+
timeoutMs?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class DopplerProvider implements SecretsProvider {
|
|
25
|
+
readonly name = "doppler" as const;
|
|
26
|
+
private readonly timeout: number;
|
|
27
|
+
|
|
28
|
+
constructor(private readonly opts: DopplerOptions = {}) {
|
|
29
|
+
this.timeout = opts.timeoutMs ?? 30_000;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `--project/--config` flags when configured explicitly. */
|
|
33
|
+
scopeArgs(): string[] {
|
|
34
|
+
const a: string[] = [];
|
|
35
|
+
if (this.opts.project) a.push("--project", this.opts.project);
|
|
36
|
+
if (this.opts.config) a.push("--config", this.opts.config);
|
|
37
|
+
return a;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
keyFor(ref: SecretRef): string {
|
|
41
|
+
return flatKeyFor(ref, this.opts.keys);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async isReady(): Promise<boolean> {
|
|
45
|
+
try {
|
|
46
|
+
await execFileAsync("doppler", ["me", "--json"], {
|
|
47
|
+
timeout: this.timeout,
|
|
48
|
+
});
|
|
49
|
+
return true;
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async get(ref: SecretRef): Promise<string> {
|
|
56
|
+
const key = this.keyFor(ref);
|
|
57
|
+
const { stdout } = await execFileAsync(
|
|
58
|
+
"doppler",
|
|
59
|
+
["secrets", "get", key, "--plain", ...this.scopeArgs()],
|
|
60
|
+
{ timeout: this.timeout },
|
|
61
|
+
);
|
|
62
|
+
return String(stdout).trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async tryGet(ref: SecretRef): Promise<string | null> {
|
|
66
|
+
try {
|
|
67
|
+
const v = await this.get(ref);
|
|
68
|
+
return v.length > 0 ? v : null;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Value goes over stdin so it never appears in the process table. */
|
|
75
|
+
async set(ref: SecretRef, value: string): Promise<void> {
|
|
76
|
+
const key = this.keyFor(ref);
|
|
77
|
+
await new Promise<void>((resolve, reject) => {
|
|
78
|
+
const child = spawn(
|
|
79
|
+
"doppler",
|
|
80
|
+
["secrets", "set", key, "--silent", ...this.scopeArgs()],
|
|
81
|
+
{ stdio: ["pipe", "ignore", "pipe"] },
|
|
82
|
+
);
|
|
83
|
+
let stderr = "";
|
|
84
|
+
child.stderr?.on("data", (d) => (stderr += String(d)));
|
|
85
|
+
child.on("error", reject);
|
|
86
|
+
child.on("close", (code) =>
|
|
87
|
+
code === 0
|
|
88
|
+
? resolve()
|
|
89
|
+
: reject(
|
|
90
|
+
new Error(`doppler secrets set ${key} failed: ${stderr.trim()}`),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
child.stdin?.end(value);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async preflight(): Promise<PreflightResult> {
|
|
98
|
+
if (!(await this.isReady())) {
|
|
99
|
+
return {
|
|
100
|
+
ok: false,
|
|
101
|
+
message:
|
|
102
|
+
"Doppler CLI is not installed or not logged in.\n" +
|
|
103
|
+
"Install: brew install dopplerhq/cli/doppler then: doppler login",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
await execFileAsync(
|
|
108
|
+
"doppler",
|
|
109
|
+
["secrets", "--only-names", "--json", ...this.scopeArgs()],
|
|
110
|
+
{ timeout: this.timeout },
|
|
111
|
+
);
|
|
112
|
+
return { ok: true };
|
|
113
|
+
} catch {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
message:
|
|
117
|
+
"Doppler is logged in but no project/config is selected.\n" +
|
|
118
|
+
`Run \`doppler setup\` in the repo, or set secrets.doppler.project/config in ${product().configFile}.`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
runPrefix(): string[] {
|
|
124
|
+
return ["doppler", "run", ...this.scopeArgs(), "--"];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-variable strategy — reads flat keys from process.env.
|
|
3
|
+
* Selected with `secrets.provider: env`. Intended for CI and for engineers who
|
|
4
|
+
* inject secrets themselves (direnv, `doppler run -- den …`, CI secrets).
|
|
5
|
+
*/
|
|
6
|
+
import { flatKeyFor } from "./keys.js";
|
|
7
|
+
import type { PreflightResult, SecretRef, SecretsProvider } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export class EnvProvider implements SecretsProvider {
|
|
10
|
+
readonly name = "env" as const;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private readonly keys: Record<string, string> = {},
|
|
14
|
+
private readonly env: NodeJS.ProcessEnv = process.env,
|
|
15
|
+
) {}
|
|
16
|
+
|
|
17
|
+
keyFor(ref: SecretRef): string {
|
|
18
|
+
return flatKeyFor(ref, this.keys);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async isReady(): Promise<boolean> {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async get(ref: SecretRef): Promise<string> {
|
|
26
|
+
const key = this.keyFor(ref);
|
|
27
|
+
const v = this.env[key];
|
|
28
|
+
if (!v) throw new Error(`Secret ${key} is not set in the environment`);
|
|
29
|
+
return v;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async tryGet(ref: SecretRef): Promise<string | null> {
|
|
33
|
+
return this.env[this.keyFor(ref)] || null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async set(ref: SecretRef, value: string): Promise<void> {
|
|
37
|
+
// Process-local only: the env backend has no durable store.
|
|
38
|
+
this.env[this.keyFor(ref)] = value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async preflight(): Promise<PreflightResult> {
|
|
42
|
+
return { ok: true };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
runPrefix(): string[] {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets strategy selection.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order for the backend:
|
|
5
|
+
* 1. DEN_SECRETS_PROVIDER env var (doppler | 1password | env)
|
|
6
|
+
* 2. `secrets.provider` in config/local/user.yml (per-engineer)
|
|
7
|
+
* 3. `secrets.provider` in config/den.yml (team default)
|
|
8
|
+
* 4. "doppler"
|
|
9
|
+
*
|
|
10
|
+
* Adding a backend = implement SecretsProvider and register it in FACTORIES.
|
|
11
|
+
*/
|
|
12
|
+
import { OP_ACCOUNT_DEFAULT, OP_CLI_TIMEOUT_MS } from "../constants.js";
|
|
13
|
+
import { readDenConfig, readUserConfig } from "../den-config.js";
|
|
14
|
+
import { DopplerProvider } from "./doppler.js";
|
|
15
|
+
import { EnvProvider } from "./env.js";
|
|
16
|
+
import { OnePasswordProvider } from "./onepassword.js";
|
|
17
|
+
import type {
|
|
18
|
+
SecretRef,
|
|
19
|
+
SecretsProvider,
|
|
20
|
+
SecretsProviderName,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
|
|
23
|
+
import { envKey } from "../product.js";
|
|
24
|
+
export * from "./types.js";
|
|
25
|
+
export { flatKeyFor, toEnvName, refPath } from "./keys.js";
|
|
26
|
+
export { DopplerProvider } from "./doppler.js";
|
|
27
|
+
export { EnvProvider } from "./env.js";
|
|
28
|
+
export { OnePasswordProvider } from "./onepassword.js";
|
|
29
|
+
|
|
30
|
+
export interface SecretsConfig {
|
|
31
|
+
provider?: SecretsProviderName;
|
|
32
|
+
/**
|
|
33
|
+
* When true, `den up` / `make web` refuse to start until the backend is
|
|
34
|
+
* ready. Default false: a missing backend is a warning, because each repo's
|
|
35
|
+
* own `make up` manages its own secrets and den only needs this backend
|
|
36
|
+
* for test logins and the saved sudo password.
|
|
37
|
+
*/
|
|
38
|
+
required?: boolean;
|
|
39
|
+
doppler?: { project?: string; config?: string };
|
|
40
|
+
onepassword?: { account?: string };
|
|
41
|
+
/** `"vault/item/field": FLAT_KEY` overrides for doppler/env. */
|
|
42
|
+
keys?: Record<string, string>;
|
|
43
|
+
/**
|
|
44
|
+
* Where the engine's own named secrets live, overriding the built-in
|
|
45
|
+
* defaults field by field (see {@link namedSecretRef}). Names in use:
|
|
46
|
+
* `sudo` (the saved local-dev sudo password) and `journeys_agent_key`
|
|
47
|
+
* (the journeys pillar's agent API key).
|
|
48
|
+
*
|
|
49
|
+
* refs:
|
|
50
|
+
* journeys_agent_key: { vault: Shared, item: CI Keys }
|
|
51
|
+
*/
|
|
52
|
+
refs?: Record<string, Partial<SecretRef>>;
|
|
53
|
+
/**
|
|
54
|
+
* The per-engineer developer keys `make den-setup-1password` collects
|
|
55
|
+
* into the developer-keys item (see scripts/setup-1password.ts). Default:
|
|
56
|
+
* only the keys the engine itself uses.
|
|
57
|
+
*/
|
|
58
|
+
developer_keys?: DeveloperKeysConfig;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One developer key the setup script prompts for. */
|
|
62
|
+
export interface DeveloperKeyDef {
|
|
63
|
+
/** Field label in the item (= the env-var name). */
|
|
64
|
+
field: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
/** Where to create one. */
|
|
67
|
+
url?: string;
|
|
68
|
+
required?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface DeveloperKeysConfig {
|
|
72
|
+
/**
|
|
73
|
+
* A dotenv file (relative to the den root, e.g. an app checkout's
|
|
74
|
+
* `.env`) whose values are offered as defaults.
|
|
75
|
+
*/
|
|
76
|
+
env_file?: string;
|
|
77
|
+
fields?: DeveloperKeyDef[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const SECRETS_PROVIDERS: readonly SecretsProviderName[] = [
|
|
81
|
+
"doppler",
|
|
82
|
+
"1password",
|
|
83
|
+
"env",
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const FACTORIES: Record<
|
|
87
|
+
SecretsProviderName,
|
|
88
|
+
(c: SecretsConfig) => SecretsProvider
|
|
89
|
+
> = {
|
|
90
|
+
doppler: (c) => new DopplerProvider({ ...c.doppler, keys: c.keys }),
|
|
91
|
+
"1password": (c) =>
|
|
92
|
+
new OnePasswordProvider({
|
|
93
|
+
account: c.onepassword?.account ?? OP_ACCOUNT_DEFAULT,
|
|
94
|
+
timeoutMs: OP_CLI_TIMEOUT_MS,
|
|
95
|
+
}),
|
|
96
|
+
env: (c) => new EnvProvider(c.keys),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
function isProviderName(v: unknown): v is SecretsProviderName {
|
|
100
|
+
return typeof v === "string" && (SECRETS_PROVIDERS as string[]).includes(v);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Effective secrets config (team file, then per-engineer override). */
|
|
104
|
+
export function readSecretsConfig(): SecretsConfig {
|
|
105
|
+
let team: SecretsConfig = {};
|
|
106
|
+
let user: SecretsConfig = {};
|
|
107
|
+
try {
|
|
108
|
+
team = readDenConfig().secrets ?? {};
|
|
109
|
+
} catch {
|
|
110
|
+
/* unreadable config → defaults */
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
user = readUserConfig().secrets ?? {};
|
|
114
|
+
} catch {
|
|
115
|
+
/* no user config */
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
...team,
|
|
119
|
+
...user,
|
|
120
|
+
doppler: { ...team.doppler, ...user.doppler },
|
|
121
|
+
onepassword: { ...team.onepassword, ...user.onepassword },
|
|
122
|
+
keys: { ...team.keys, ...user.keys },
|
|
123
|
+
refs: { ...team.refs, ...user.refs },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The engine-named secret `name`: `defaults` with any configured
|
|
129
|
+
* `secrets.refs.<name>` fields laid over them. The engine never hard-wires
|
|
130
|
+
* an org's vault or item names; the defaults are neutral placeholders.
|
|
131
|
+
*/
|
|
132
|
+
export function namedSecretRef(
|
|
133
|
+
name: string,
|
|
134
|
+
defaults: SecretRef,
|
|
135
|
+
cfg: SecretsConfig = readSecretsConfig(),
|
|
136
|
+
): SecretRef {
|
|
137
|
+
const override = cfg.refs?.[name] ?? {};
|
|
138
|
+
const pick = (v: unknown, d: string): string =>
|
|
139
|
+
typeof v === "string" && v.trim() ? v.trim() : d;
|
|
140
|
+
const ref: SecretRef = {
|
|
141
|
+
vault: pick(override.vault, defaults.vault),
|
|
142
|
+
item: pick(override.item, defaults.item),
|
|
143
|
+
field: pick(override.field, defaults.field),
|
|
144
|
+
};
|
|
145
|
+
const key = override.key ?? defaults.key;
|
|
146
|
+
if (key) ref.key = key;
|
|
147
|
+
return ref;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function resolveProviderName(
|
|
151
|
+
cfg: SecretsConfig,
|
|
152
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
153
|
+
): SecretsProviderName {
|
|
154
|
+
const fromEnv = env.DEN_SECRETS_PROVIDER;
|
|
155
|
+
if (fromEnv) {
|
|
156
|
+
if (!isProviderName(fromEnv)) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`${envKey("SECRETS_PROVIDER")}="${fromEnv}" is not one of ${SECRETS_PROVIDERS.join(", ")}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
return fromEnv;
|
|
162
|
+
}
|
|
163
|
+
if (cfg.provider !== undefined && !isProviderName(cfg.provider)) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`secrets.provider "${String(cfg.provider)}" is not one of ${SECRETS_PROVIDERS.join(", ")}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
return cfg.provider ?? "doppler";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function createSecretsProvider(
|
|
172
|
+
name: SecretsProviderName,
|
|
173
|
+
cfg: SecretsConfig = {},
|
|
174
|
+
): SecretsProvider {
|
|
175
|
+
return FACTORIES[name](cfg);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let cached: SecretsProvider | null = null;
|
|
179
|
+
let cachedKey = "";
|
|
180
|
+
|
|
181
|
+
/** The configured backend. Re-resolves when DEN_SECRETS_PROVIDER changes. */
|
|
182
|
+
export function getSecretsProvider(): SecretsProvider {
|
|
183
|
+
const cfg = readSecretsConfig();
|
|
184
|
+
const name = resolveProviderName(cfg);
|
|
185
|
+
const key = `${name}|${JSON.stringify(cfg)}`;
|
|
186
|
+
if (!cached || key !== cachedKey) {
|
|
187
|
+
cached = createSecretsProvider(name, cfg);
|
|
188
|
+
cachedKey = key;
|
|
189
|
+
}
|
|
190
|
+
return cached;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Test hook. */
|
|
194
|
+
export function resetSecretsProviderCache(): void {
|
|
195
|
+
cached = null;
|
|
196
|
+
cachedKey = "";
|
|
197
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SecretRef } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Upper-snake a free-form label: "Test User (Auth0)" → "TEST_USER_AUTH0". */
|
|
4
|
+
export function toEnvName(label: string): string {
|
|
5
|
+
return label
|
|
6
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
7
|
+
.replace(/[^A-Za-z0-9]+/g, "_")
|
|
8
|
+
.replace(/^_+|_+$/g, "")
|
|
9
|
+
.toUpperCase();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Canonical `vault/item/field` string used as the key of `secrets.keys`. */
|
|
13
|
+
export function refPath(ref: SecretRef): string {
|
|
14
|
+
return `${ref.vault}/${ref.item}/${ref.field}`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the flat key a key/value backend should use for `ref`:
|
|
19
|
+
* 1. `ref.key` when the caller pinned one;
|
|
20
|
+
* 2. the `secrets.keys` override map (`"vault/item/field": KEY`);
|
|
21
|
+
* 3. the field itself when it already looks like an env var (API keys);
|
|
22
|
+
* 4. otherwise `ITEM_FIELD` upper-snaked.
|
|
23
|
+
*/
|
|
24
|
+
export function flatKeyFor(
|
|
25
|
+
ref: SecretRef,
|
|
26
|
+
overrides: Record<string, string> = {},
|
|
27
|
+
): string {
|
|
28
|
+
if (ref.key) return ref.key;
|
|
29
|
+
const mapped = overrides[refPath(ref)];
|
|
30
|
+
if (mapped) return mapped;
|
|
31
|
+
if (/^[A-Z][A-Z0-9_]*$/.test(ref.field)) return ref.field;
|
|
32
|
+
return toEnvName(`${ref.item} ${ref.field}`);
|
|
33
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 1Password strategy — reads/writes item fields with the `op` CLI.
|
|
3
|
+
* Selected with `secrets.provider: 1password`.
|
|
4
|
+
*/
|
|
5
|
+
import { execFile } from "child_process";
|
|
6
|
+
import { promisify } from "util";
|
|
7
|
+
import type { PreflightResult, SecretRef, SecretsProvider } from "./types.js";
|
|
8
|
+
|
|
9
|
+
import { product } from "../product.js";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
|
|
12
|
+
export interface OnePasswordOptions {
|
|
13
|
+
account?: string;
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class OnePasswordProvider implements SecretsProvider {
|
|
18
|
+
readonly name = "1password" as const;
|
|
19
|
+
private readonly timeout: number;
|
|
20
|
+
|
|
21
|
+
constructor(opts: OnePasswordOptions = {}) {
|
|
22
|
+
this.timeout = opts.timeoutMs ?? 60_000;
|
|
23
|
+
if (opts.account) process.env.OP_ACCOUNT ||= opts.account;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async isReady(): Promise<boolean> {
|
|
27
|
+
try {
|
|
28
|
+
await execFileAsync("op", ["whoami"], { timeout: this.timeout });
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async get(ref: SecretRef): Promise<string> {
|
|
36
|
+
// Direct CLI args rather than an op:// URI — URIs break on "(" "," etc.
|
|
37
|
+
const args = ["item", "get", ref.item];
|
|
38
|
+
if (ref.vault) args.push("--vault", ref.vault);
|
|
39
|
+
args.push("--fields", `label=${ref.field}`, "--reveal");
|
|
40
|
+
const { stdout } = await execFileAsync("op", args, {
|
|
41
|
+
timeout: this.timeout,
|
|
42
|
+
});
|
|
43
|
+
return String(stdout).trim();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async tryGet(ref: SecretRef): Promise<string | null> {
|
|
47
|
+
try {
|
|
48
|
+
const v = await this.get(ref);
|
|
49
|
+
return v.length > 0 ? v : null;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async set(ref: SecretRef, value: string): Promise<void> {
|
|
56
|
+
const args = ["item", "edit", ref.item];
|
|
57
|
+
if (ref.vault) args.push("--vault", ref.vault);
|
|
58
|
+
args.push(`${ref.field}[password]=${value}`);
|
|
59
|
+
await execFileAsync("op", args, { timeout: this.timeout });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async preflight(): Promise<PreflightResult> {
|
|
63
|
+
if (await this.isReady()) return { ok: true };
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
message:
|
|
67
|
+
"1Password CLI is not signed in. Run: eval $(op signin)\n" +
|
|
68
|
+
`Or switch backends with secrets.provider in ${product().configFile}.`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
runPrefix(): string[] {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Secrets strategy — the contract every secrets backend implements.
|
|
3
|
+
*
|
|
4
|
+
* Den never talks to a vault directly. Callers describe WHAT they need as
|
|
5
|
+
* a {@link SecretRef} (a logical vault / item / field triple) and the active
|
|
6
|
+
* {@link SecretsProvider} decides HOW to fetch it: 1Password item fields,
|
|
7
|
+
* Doppler flat keys, or plain environment variables. The backend is selected
|
|
8
|
+
* by configuration (see `resolveSecretsProvider`), never hard-wired.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** A logical pointer to one secret value. */
|
|
12
|
+
export interface SecretRef {
|
|
13
|
+
/** Logical vault/namespace (1Password vault; ignored by flat-key backends). */
|
|
14
|
+
vault: string;
|
|
15
|
+
/** Logical item within the vault (1Password item name or ID). */
|
|
16
|
+
item: string;
|
|
17
|
+
/** Field within the item (e.g. "password", "username", "OPENAI_API_KEY"). */
|
|
18
|
+
field: string;
|
|
19
|
+
/**
|
|
20
|
+
* Flat key for key/value backends (Doppler, env). When omitted it is taken
|
|
21
|
+
* from the `secrets.keys` map in config/den.yml, else derived — see
|
|
22
|
+
* `flatKeyFor`.
|
|
23
|
+
*/
|
|
24
|
+
key?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SecretsProviderName = "doppler" | "1password" | "env";
|
|
28
|
+
|
|
29
|
+
export interface PreflightResult {
|
|
30
|
+
ok: boolean;
|
|
31
|
+
/** Human-readable remediation when !ok. */
|
|
32
|
+
message?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SecretsProvider {
|
|
36
|
+
readonly name: SecretsProviderName;
|
|
37
|
+
/** True when the backend CLI is installed and authenticated. Never throws. */
|
|
38
|
+
isReady(): Promise<boolean>;
|
|
39
|
+
/** Read a secret. Throws when it cannot be resolved. */
|
|
40
|
+
get(ref: SecretRef): Promise<string>;
|
|
41
|
+
/** Read a secret, returning null instead of throwing. */
|
|
42
|
+
tryGet(ref: SecretRef): Promise<string | null>;
|
|
43
|
+
/** Write (create or overwrite) a secret. Throws on failure. */
|
|
44
|
+
set(ref: SecretRef, value: string): Promise<void>;
|
|
45
|
+
/** Check the backend is usable before `den up`/`restart`. */
|
|
46
|
+
preflight(): Promise<PreflightResult>;
|
|
47
|
+
/**
|
|
48
|
+
* argv prefix that runs a child command with secrets injected as env vars,
|
|
49
|
+
* e.g. `["doppler","run","--"]`. Empty when the backend injects nothing.
|
|
50
|
+
*/
|
|
51
|
+
runPrefix(): string[];
|
|
52
|
+
}
|