@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.
Files changed (255) hide show
  1. package/CHANGELOG.md +204 -0
  2. package/LICENSE +21 -0
  3. package/README.md +165 -1
  4. package/bin/den +68 -0
  5. package/docs/distributions.md +499 -0
  6. package/make/demo.mk +82 -0
  7. package/make/den.mk +80 -0
  8. package/make/journeys.mk +57 -0
  9. package/make/local_dev.mk +154 -0
  10. package/make/security.mk +59 -0
  11. package/make/test.mk +405 -0
  12. package/package.json +101 -4
  13. package/scripts/auth-configure.ts +415 -0
  14. package/scripts/heal-config.ts +92 -0
  15. package/scripts/jsonl-playwright-reporter.js +70 -0
  16. package/scripts/manifest-read.cjs +91 -0
  17. package/scripts/setup-1password.ts +570 -0
  18. package/scripts/setup-secrets.ts +149 -0
  19. package/scripts/view-results.js +156 -0
  20. package/service/install.sh +112 -0
  21. package/src/pillars/api/fixtures/index.ts +128 -0
  22. package/src/pillars/api/fixtures/openapi-validator.ts +111 -0
  23. package/src/pillars/api/global-setup.ts +103 -0
  24. package/src/pillars/api/playwright.config.ts +105 -0
  25. package/src/pillars/api/schemas/example-api.json +110 -0
  26. package/src/pillars/api/tests/health/health.spec.ts +19 -0
  27. package/src/pillars/api/tests/health/manifest.yml +6 -0
  28. package/src/pillars/browser/fixtures/index.ts +232 -0
  29. package/src/pillars/browser/fixtures/package.json +10 -0
  30. package/src/pillars/browser/global-setup.ts +140 -0
  31. package/src/pillars/browser/global-teardown.ts +48 -0
  32. package/src/pillars/browser/playwright.config.ts +212 -0
  33. package/src/pillars/browser/setup.ts +219 -0
  34. package/src/pillars/journeys/app-config.ts +237 -0
  35. package/src/pillars/journeys/discover.ts +160 -0
  36. package/src/pillars/journeys/hooks/deny-shell.cjs +44 -0
  37. package/src/pillars/journeys/observability/events.ts +288 -0
  38. package/src/pillars/journeys/observability/sdk-mapper.ts +306 -0
  39. package/src/pillars/journeys/report/friction.ts +304 -0
  40. package/src/pillars/journeys/report/junit.ts +105 -0
  41. package/src/pillars/journeys/report/render.ts +273 -0
  42. package/src/pillars/journeys/report/results.ts +323 -0
  43. package/src/pillars/journeys/run.ts +673 -0
  44. package/src/pillars/journeys/runner.ts +1025 -0
  45. package/src/pillars/journeys/sandbox.ts +403 -0
  46. package/src/pillars/journeys/spec/loader.ts +152 -0
  47. package/src/pillars/journeys/spec/schema.ts +208 -0
  48. package/src/pillars/journeys/teardown.ts +98 -0
  49. package/src/pillars/journeys/tools/browser-session.ts +219 -0
  50. package/src/pillars/journeys/tools/browser-tool.ts +914 -0
  51. package/src/pillars/journeys/tools/http-tool.ts +564 -0
  52. package/src/pillars/journeys/tools/ledger.ts +278 -0
  53. package/src/pillars/journeys/tools/snapshot-store.ts +117 -0
  54. package/src/pillars/journeys/verify/assertions.ts +502 -0
  55. package/src/pillars/journeys/verify/verdict.ts +71 -0
  56. package/src/pillars/perf/build-report.ts +36 -0
  57. package/src/pillars/perf/k6.config.js +112 -0
  58. package/src/pillars/perf/run.ts +345 -0
  59. package/src/pillars/perf/scenarios/load-profile.js +97 -0
  60. package/src/pillars/perf/staging.ts +73 -0
  61. package/src/pillars/security/discover.ts +124 -0
  62. package/src/pillars/security/findings.ts +151 -0
  63. package/src/pillars/security/plan.ts +355 -0
  64. package/src/pillars/security/run.ts +757 -0
  65. package/src/pillars/smoke/admin-probe.ts +68 -0
  66. package/src/pillars/smoke/base-url.ts +56 -0
  67. package/src/pillars/smoke/cleanup.ts +8 -0
  68. package/src/pillars/smoke/discover.ts +237 -0
  69. package/src/pillars/smoke/ensure-smoke-teardown.ts +40 -0
  70. package/src/pillars/smoke/fixture-manager.ts +217 -0
  71. package/src/pillars/smoke/fixtures.ts +468 -0
  72. package/src/pillars/smoke/global-setup-all.ts +262 -0
  73. package/src/pillars/smoke/global-setup-global.ts +262 -0
  74. package/src/pillars/smoke/global-setup.ts +273 -0
  75. package/src/pillars/smoke/global-teardown-all.ts +8 -0
  76. package/src/pillars/smoke/global-teardown-global.ts +8 -0
  77. package/src/pillars/smoke/global-teardown.ts +139 -0
  78. package/src/pillars/smoke/playwright-all.config.ts +147 -0
  79. package/src/pillars/smoke/playwright-global.config.ts +141 -0
  80. package/src/pillars/smoke/playwright.config.ts +162 -0
  81. package/src/pillars/smoke/setup-env.ts +132 -0
  82. package/src/pillars/smoke/setup-global.ts +126 -0
  83. package/src/pillars/smoke/setup.ts +115 -0
  84. package/src/pillars/smoke/smoke-repo.ts +16 -0
  85. package/src/pillars/smoke/staging.ts +49 -0
  86. package/src/pillars/suites/run.ts +147 -0
  87. package/src/scripts/configure-aws-profiles.ts +114 -0
  88. package/src/scripts/register-repos.ts +307 -0
  89. package/src/scripts/setup-default-workspace.ts +203 -0
  90. package/src/shared/agent-actor.ts +77 -0
  91. package/src/shared/applications.ts +138 -0
  92. package/src/shared/auth/api-token.ts +416 -0
  93. package/src/shared/auth/auth0-helper.ts +284 -0
  94. package/src/shared/auth/headless-login.ts +265 -0
  95. package/src/shared/auth/jwt-helper.ts +186 -0
  96. package/src/shared/auth/sudo-vault.ts +69 -0
  97. package/src/shared/auth/test-accounts.ts +98 -0
  98. package/src/shared/auth/token-injection.ts +184 -0
  99. package/src/shared/auth/token-providers/auth0.ts +80 -0
  100. package/src/shared/auth/token-providers/index.ts +182 -0
  101. package/src/shared/auth/token-refresh.ts +273 -0
  102. package/src/shared/banner.ts +15 -0
  103. package/src/shared/brand.ts +79 -0
  104. package/src/shared/cli-tabs.ts +131 -0
  105. package/src/shared/constants.ts +128 -0
  106. package/src/shared/den-config.ts +688 -0
  107. package/src/shared/dev-metrics/emit.ts +354 -0
  108. package/src/shared/edge/config.ts +229 -0
  109. package/src/shared/edge/render.ts +221 -0
  110. package/src/shared/edge/runtime.ts +268 -0
  111. package/src/shared/edge/status.ts +74 -0
  112. package/src/shared/edge/types.ts +62 -0
  113. package/src/shared/engine.ts +67 -0
  114. package/src/shared/environment/aws-profiles.ts +49 -0
  115. package/src/shared/environment/env-selector.ts +161 -0
  116. package/src/shared/environment/playwright-env.ts +82 -0
  117. package/src/shared/environment-manager/catalog.ts +77 -0
  118. package/src/shared/environment-manager/clone-guard.ts +163 -0
  119. package/src/shared/environment-manager/dag.ts +286 -0
  120. package/src/shared/environment-manager/default-workspace.ts +182 -0
  121. package/src/shared/environment-manager/derivation/alias.ts +90 -0
  122. package/src/shared/environment-manager/derivation/cache.ts +97 -0
  123. package/src/shared/environment-manager/derivation/derive-graph.ts +421 -0
  124. package/src/shared/environment-manager/derivation/derived-up.ts +354 -0
  125. package/src/shared/environment-manager/derivation/load-alias.ts +11 -0
  126. package/src/shared/environment-manager/derivation/rules-compose.ts +42 -0
  127. package/src/shared/environment-manager/derivation/rules-events.ts +50 -0
  128. package/src/shared/environment-manager/derivation/rules-http.ts +247 -0
  129. package/src/shared/environment-manager/derivation/rules-infra.ts +64 -0
  130. package/src/shared/environment-manager/derivation/rules-readiness.ts +96 -0
  131. package/src/shared/environment-manager/derivation/scan.ts +160 -0
  132. package/src/shared/environment-manager/derivation/text.ts +40 -0
  133. package/src/shared/environment-manager/derivation/types.ts +69 -0
  134. package/src/shared/environment-manager/health.ts +635 -0
  135. package/src/shared/environment-manager/index.ts +6 -0
  136. package/src/shared/environment-manager/load-applications.ts +108 -0
  137. package/src/shared/environment-manager/log-tail.ts +213 -0
  138. package/src/shared/environment-manager/make-targets.ts +78 -0
  139. package/src/shared/environment-manager/opctl.ts +335 -0
  140. package/src/shared/environment-manager/readiness.ts +93 -0
  141. package/src/shared/environment-manager/register-local.ts +72 -0
  142. package/src/shared/environment-manager/secrets-check.ts +260 -0
  143. package/src/shared/environment-manager/service-container.ts +47 -0
  144. package/src/shared/environment-manager/service-env.ts +110 -0
  145. package/src/shared/environment-manager/service-runner.ts +1603 -0
  146. package/src/shared/environment-manager/sync-targets.ts +66 -0
  147. package/src/shared/environment-manager/up-plan.ts +326 -0
  148. package/src/shared/environment-manager/workspace-manager.ts +905 -0
  149. package/src/shared/environment-manager/workspaces.ts +860 -0
  150. package/src/shared/manifest/deps.ts +27 -0
  151. package/src/shared/manifest/merge-override.ts +75 -0
  152. package/src/shared/manifest/migrate-manifest.ts +101 -0
  153. package/src/shared/manifest/placement.ts +321 -0
  154. package/src/shared/manifest/read-manifest.ts +278 -0
  155. package/src/shared/manifest/schema.ts +313 -0
  156. package/src/shared/manifest/service-links.ts +93 -0
  157. package/src/shared/manifest/validate-layers.ts +229 -0
  158. package/src/shared/manifest/validate-manifest.ts +200 -0
  159. package/src/shared/manifest/workspace-manifests.ts +92 -0
  160. package/src/shared/open-url.ts +122 -0
  161. package/src/shared/org.ts +44 -0
  162. package/src/shared/paths.ts +190 -0
  163. package/src/shared/perf/optional-fields.ts +105 -0
  164. package/src/shared/perf/report-normalizer.ts +404 -0
  165. package/src/shared/perf/report-paths.ts +19 -0
  166. package/src/shared/perf/report-store.ts +110 -0
  167. package/src/shared/perf/report-types.ts +70 -0
  168. package/src/shared/perf/test-paths.ts +25 -0
  169. package/src/shared/process/log-rotation.ts +147 -0
  170. package/src/shared/process/spawn-streaming.ts +229 -0
  171. package/src/shared/product.ts +92 -0
  172. package/src/shared/profile.ts +141 -0
  173. package/src/shared/redact.ts +107 -0
  174. package/src/shared/repo-kind.ts +32 -0
  175. package/src/shared/reporting/jsonl-reporter.ts +131 -0
  176. package/src/shared/reporting/slack-reporter.ts +340 -0
  177. package/src/shared/results/result-reader.ts +266 -0
  178. package/src/shared/results/result-writer.ts +168 -0
  179. package/src/shared/secrets/doppler.ts +126 -0
  180. package/src/shared/secrets/env.ts +48 -0
  181. package/src/shared/secrets/index.ts +197 -0
  182. package/src/shared/secrets/keys.ts +33 -0
  183. package/src/shared/secrets/onepassword.ts +75 -0
  184. package/src/shared/secrets/types.ts +52 -0
  185. package/src/shared/service-url.ts +119 -0
  186. package/src/shared/smoke/branch-resolver.ts +141 -0
  187. package/src/shared/smoke/git-remote.ts +70 -0
  188. package/src/shared/smoke/repo-cloner.ts +151 -0
  189. package/src/shared/stacks/config.ts +137 -0
  190. package/src/shared/stacks/manifest-commands.ts +118 -0
  191. package/src/shared/stacks/render.ts +253 -0
  192. package/src/shared/stacks/runtime.ts +334 -0
  193. package/src/shared/stacks/status.ts +99 -0
  194. package/src/shared/stacks/types.ts +53 -0
  195. package/src/shared/terminal/tabs.ts +212 -0
  196. package/src/shared/test-discovery/module-scanner.ts +569 -0
  197. package/src/shared/types/index.ts +701 -0
  198. package/src/shared/ui-marker.ts +19 -0
  199. package/src/ui/cli/applications-command.ts +148 -0
  200. package/src/ui/cli/config-command.ts +68 -0
  201. package/src/ui/cli/create-main.ts +9 -0
  202. package/src/ui/cli/create.ts +360 -0
  203. package/src/ui/cli/edge-command.ts +81 -0
  204. package/src/ui/cli/flag-parser.ts +246 -0
  205. package/src/ui/cli/graph-command.ts +570 -0
  206. package/src/ui/cli/health-display.ts +63 -0
  207. package/src/ui/cli/index.ts +3742 -0
  208. package/src/ui/cli/init.ts +855 -0
  209. package/src/ui/cli/invocation-target.ts +175 -0
  210. package/src/ui/cli/logs-command.ts +104 -0
  211. package/src/ui/cli/mcp-main.ts +13 -0
  212. package/src/ui/cli/mcp-stdout-guard.ts +10 -0
  213. package/src/ui/cli/module-selection.ts +31 -0
  214. package/src/ui/cli/prompts.ts +767 -0
  215. package/src/ui/cli/results-viewer.ts +436 -0
  216. package/src/ui/cli/secrets-preflight.ts +91 -0
  217. package/src/ui/cli/stack-command.ts +117 -0
  218. package/src/ui/cli/stalled-setup-recovery.ts +129 -0
  219. package/src/ui/cli/test-runner.ts +1319 -0
  220. package/src/ui/cli/web-command.ts +1073 -0
  221. package/src/ui/mcp/api.ts +284 -0
  222. package/src/ui/mcp/guards.ts +141 -0
  223. package/src/ui/mcp/server.ts +147 -0
  224. package/src/ui/mcp/tools.ts +565 -0
  225. package/src/ui/server/audit.ts +71 -0
  226. package/src/ui/server/changelog-parser.ts +105 -0
  227. package/src/ui/server/daemon-reaper.ts +129 -0
  228. package/src/ui/server/editor-manager.ts +301 -0
  229. package/src/ui/server/github-service.ts +223 -0
  230. package/src/ui/server/graph-prefs.ts +89 -0
  231. package/src/ui/server/health-row.ts +15 -0
  232. package/src/ui/server/http-helpers.ts +98 -0
  233. package/src/ui/server/index.ts +3350 -0
  234. package/src/ui/server/log-history.ts +453 -0
  235. package/src/ui/server/perf-report.ts +42 -0
  236. package/src/ui/server/public/environments.html +496 -0
  237. package/src/ui/server/public/favicon.ico +0 -0
  238. package/src/ui/server/public/graph-model.js +416 -0
  239. package/src/ui/server/public/graph.css +976 -0
  240. package/src/ui/server/public/graph.js +986 -0
  241. package/src/ui/server/public/header.js +1373 -0
  242. package/src/ui/server/public/index.html +2595 -0
  243. package/src/ui/server/public/local-dev.html +5344 -0
  244. package/src/ui/server/public/logs.html +1496 -0
  245. package/src/ui/server/public/panel.js +787 -0
  246. package/src/ui/server/public/perf-report.js +359 -0
  247. package/src/ui/server/public/release.html +2111 -0
  248. package/src/ui/server/public/services.html +1103 -0
  249. package/src/ui/server/public/workspace-picker.js +69 -0
  250. package/src/ui/server/readonly-routes.ts +200 -0
  251. package/src/ui/server/release-deploy-session.ts +246 -0
  252. package/src/ui/server/release-service.ts +1125 -0
  253. package/src/ui/server/sudo-store.ts +111 -0
  254. package/src/ui/server/ui-assets.ts +73 -0
  255. 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
+ }