@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,265 @@
1
+ /**
2
+ * Headless browser login
3
+ *
4
+ * Two exported functions:
5
+ * getCredentials() — resolves email+password from ENV vars or the secrets backend
6
+ * headlessLogin() — performs a full browser-based login and saves the session
7
+ *
8
+ * Credential priority:
9
+ * 1. ENV vars — E2E_ADMIN_EMAIL / E2E_ADMIN_PASSWORD (CI only)
10
+ * E2E_USER_EMAIL / E2E_USER_PASSWORD
11
+ * E2E_BETA_EMAIL / E2E_BETA_PASSWORD
12
+ * E2E_ROOT_ADMIN_EMAIL / E2E_ROOT_ADMIN_PASSWORD
13
+ * 2. Shared account — `test_accounts.roles.<role>` in the org config
14
+ * (email + secrets-backend item; see ./test-accounts.ts)
15
+ * 3. admin / root_admin only — the per-engineer account written by
16
+ * `make auth-configure [env=<env>]` (email + secret ref)
17
+ */
18
+
19
+ import { chromium } from "@playwright/test";
20
+ import * as fs from "fs";
21
+ import * as path from "path";
22
+ import type { BrowserLoginConfig, Environment } from "../types/index.js";
23
+ import { getDistRoot } from "../paths.js";
24
+ import { getSecretsProvider } from "../secrets/index.js";
25
+ import { sharedTestAccount } from "./test-accounts.js";
26
+
27
+ import { product } from "../product.js";
28
+ // Anchored to the den checkout, NEVER process.cwd(): AUTH_DIR below is
29
+ // WRITTEN (saved browser sessions are secrets), and the "scripts always run
30
+ // from the repo root" assumption is false — bin/den never cds and
31
+ // test-runner-spawned playwright children inherit the user's cwd.
32
+ const PROJECT_ROOT = getDistRoot();
33
+
34
+ const AUTH_DIR = path.resolve(PROJECT_ROOT, "playwright/.auth");
35
+
36
+ // Per-engineer admin config — stores email + secret reference (no password)
37
+ // Created by: make auth-configure [env=<env>]
38
+ // Lookup order: config/local/admin-auth-{env}.json → config/local/admin-auth.json
39
+ const LOCAL_CONFIG_DIR = path.resolve(PROJECT_ROOT, "config/local");
40
+
41
+ interface AdminAuthConfig {
42
+ email: string;
43
+ opRef: string;
44
+ // Saved by auth-configure for robust lookup by ID (avoids op:// URI parsing
45
+ // issues with special characters in vault/item names)
46
+ itemId?: string;
47
+ vaultId?: string;
48
+ fieldLabel?: string;
49
+ }
50
+
51
+ function loadAdminConfig(environment: Environment): AdminAuthConfig | null {
52
+ const candidates = [
53
+ path.join(LOCAL_CONFIG_DIR, `admin-auth-${environment}.json`),
54
+ path.join(LOCAL_CONFIG_DIR, "admin-auth.json"),
55
+ ];
56
+ for (const configPath of candidates) {
57
+ try {
58
+ if (!fs.existsSync(configPath)) continue;
59
+ return JSON.parse(
60
+ fs.readFileSync(configPath, "utf-8"),
61
+ ) as AdminAuthConfig;
62
+ } catch {
63
+ continue;
64
+ }
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * Read a secret through the configured secrets strategy (Doppler, 1Password,
71
+ * or env — see src/shared/secrets). Name kept for call-site stability.
72
+ */
73
+ async function opGetField(opts: {
74
+ item: string; // item ID or name
75
+ vault?: string; // vault ID or name
76
+ field: string; // field label (e.g. "password")
77
+ }): Promise<string> {
78
+ return getSecretsProvider().get({
79
+ vault: opts.vault ?? "",
80
+ item: opts.item,
81
+ field: opts.field,
82
+ });
83
+ }
84
+
85
+ const ENV_PREFIX: Record<"admin" | "user" | "beta" | "root_admin", string> = {
86
+ admin: "E2E_ADMIN",
87
+ user: "E2E_USER",
88
+ beta: "E2E_BETA",
89
+ root_admin: "E2E_ROOT_ADMIN",
90
+ };
91
+
92
+ /**
93
+ * Retrieve credentials for a role.
94
+ *
95
+ * Priority:
96
+ * 1. ENV vars — CI-friendly, never used on local dev
97
+ * 2. The shared account from `test_accounts` in the org config
98
+ * 3. admin / root_admin: config/local/admin-auth[-<env>].json
99
+ * (email + secret ref, set via make auth-configure)
100
+ *
101
+ * Returns null if no credentials are available.
102
+ */
103
+ export async function getCredentials(
104
+ role: "admin" | "user" | "beta" | "root_admin",
105
+ environment: Environment,
106
+ ): Promise<{ email: string; password: string } | null> {
107
+ // 1. ENV vars — CI only
108
+ const prefix = ENV_PREFIX[role];
109
+ const envEmail = process.env[`${prefix}_EMAIL`];
110
+ const envPassword = process.env[`${prefix}_PASSWORD`];
111
+ if (envEmail && envPassword) {
112
+ console.log(` Using ENV var credentials for ${role}`);
113
+ return { email: envEmail, password: envPassword };
114
+ }
115
+
116
+ // 2/3. Secrets backend — local dev (dynamic, nothing written to disk)
117
+ try {
118
+ const shared = sharedTestAccount(role, environment);
119
+ if (shared) {
120
+ const password = await opGetField({
121
+ item: shared.ref.item,
122
+ vault: shared.ref.vault,
123
+ field: shared.ref.field,
124
+ });
125
+ console.log(
126
+ ` Using secrets-backend credentials for ${role} (${shared.email})`,
127
+ );
128
+ return { email: shared.email, password };
129
+ }
130
+
131
+ if (role === "admin" || role === "root_admin") {
132
+ // No shared account here: use the per-engineer personal account
133
+ // (personal accounts serve both admin and root_admin).
134
+ const adminConfig = loadAdminConfig(environment);
135
+ if (adminConfig?.email && adminConfig?.opRef) {
136
+ const password = await opGetField({
137
+ item: adminConfig.itemId ?? adminConfig.opRef,
138
+ vault: adminConfig.vaultId,
139
+ field: adminConfig.fieldLabel ?? "password",
140
+ });
141
+ console.log(
142
+ ` Using secrets-backend credentials for ${role} (${adminConfig.email})`,
143
+ );
144
+ return { email: adminConfig.email, password };
145
+ }
146
+ console.log(
147
+ ` No admin config for ${environment} — run: make auth-configure env=${environment}`,
148
+ );
149
+ } else {
150
+ console.log(
151
+ ` No shared ${role} account for ${environment} — set test_accounts.roles.${role} in the org config`,
152
+ );
153
+ }
154
+ } catch (err) {
155
+ const msg = (err as Error).message ?? String(err);
156
+ if (
157
+ msg.includes("not signed in") ||
158
+ msg.includes("sign in") ||
159
+ msg.includes("ENOENT") ||
160
+ msg.includes("not found") ||
161
+ msg.includes("command not found")
162
+ ) {
163
+ console.log(
164
+ ` Secrets backend unavailable — sign in (make ${product().cli}-setup-secrets)`,
165
+ );
166
+ } else {
167
+ console.log(` Secrets backend error: ${msg.split("\n")[0]}`);
168
+ }
169
+ }
170
+
171
+ return null;
172
+ }
173
+
174
+ /**
175
+ * Perform a fully headless browser login using email+password credentials.
176
+ *
177
+ * Navigates to the app's login path → follows the redirect to the IdP →
178
+ * fills email/password → waits for the redirect back to the app origin →
179
+ * saves context.storageState() to authFile.
180
+ *
181
+ * `login.loginPath` (environment config `auth.browser.loginPath`, default
182
+ * "/") is where the app starts its login redirect. Success is "back on the
183
+ * app origin", which holds for any IdP host (hosted domain or vanity CNAME).
184
+ *
185
+ * Sessions are ephemeral — written fresh on each test run by global-setup.
186
+ *
187
+ * @istanbul ignore next - Browser automation tested via E2E tests
188
+ */
189
+ /* istanbul ignore next */
190
+ export async function headlessLogin(
191
+ role: "admin" | "user" | "beta",
192
+ baseUrl: string,
193
+ email: string,
194
+ password: string,
195
+ outputPath?: string,
196
+ login: BrowserLoginConfig = {},
197
+ ): Promise<void> {
198
+ const env = process.env.E2E_ENVIRONMENT || "local";
199
+ const authFile = outputPath ?? path.join(AUTH_DIR, `${role}-${env}.json`);
200
+
201
+ console.log(` Logging in: ${role} (${email})`);
202
+
203
+ const browser = await chromium.launch({ headless: true });
204
+ const context = await browser.newContext();
205
+ const page = await context.newPage();
206
+
207
+ try {
208
+ const loginPath = login.loginPath ?? "/";
209
+ const loginUrl = `${baseUrl.replace(/\/$/, "")}${loginPath.startsWith("/") ? "" : "/"}${loginPath}`;
210
+ const appOrigin = new URL(baseUrl).origin;
211
+ await page.goto(loginUrl, {
212
+ waitUntil: "domcontentloaded",
213
+ timeout: 30000,
214
+ });
215
+
216
+ // Wait for the IdP redirect: "navigated away from the app origin",
217
+ // never a hard-coded IdP host, so hosted and vanity domains both work.
218
+ await page.waitForURL((url) => url.origin !== appOrigin, {
219
+ timeout: 15000,
220
+ });
221
+
222
+ // Fill email
223
+ await page.fill('input[name="username"], input[type="email"]', email, {
224
+ timeout: 10000,
225
+ });
226
+
227
+ // Some IdPs show a "Continue" step before the password
228
+ const continueBtn = page.locator(
229
+ 'button:has-text("Continue"), button[name="action"][value="default"]',
230
+ );
231
+ if ((await continueBtn.count()) > 0) {
232
+ await continueBtn.first().click();
233
+ await page.waitForTimeout(500);
234
+ }
235
+
236
+ // Fill password
237
+ await page.fill(
238
+ 'input[name="password"], input[type="password"]',
239
+ password,
240
+ { timeout: 10000 },
241
+ );
242
+
243
+ // Submit
244
+ await page.click('button[type="submit"], button[name="action"]', {
245
+ timeout: 10000,
246
+ });
247
+
248
+ // Wait for the redirect back to the app: "back on the app origin" is
249
+ // the IdP-agnostic signal.
250
+ await page.waitForURL((url) => url.origin === appOrigin, {
251
+ timeout: 30000,
252
+ });
253
+
254
+ console.log(` ✓ ${role} authenticated → ${page.url()}`);
255
+
256
+ if (!fs.existsSync(AUTH_DIR)) {
257
+ fs.mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
258
+ }
259
+ await context.storageState({ path: authFile });
260
+ // The saved session is a secret — Playwright writes it 0644, so restrict it.
261
+ fs.chmodSync(authFile, 0o600);
262
+ } finally {
263
+ await browser.close();
264
+ }
265
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * JWT Helper
3
+ * Handles JWT token operations
4
+ */
5
+
6
+ import jwt from "jsonwebtoken";
7
+
8
+ export interface JWTPayload {
9
+ sub?: string;
10
+ email?: string;
11
+ name?: string;
12
+ roles?: string[];
13
+ permissions?: string[];
14
+ exp?: number;
15
+ iat?: number;
16
+ [key: string]: unknown;
17
+ }
18
+
19
+ export class JWTHelper {
20
+ /**
21
+ * Decode JWT token without verification
22
+ */
23
+ decode(token: string): JWTPayload {
24
+ try {
25
+ const decoded = jwt.decode(token);
26
+ if (!decoded || typeof decoded === "string") {
27
+ throw new Error("Invalid token format");
28
+ }
29
+ return decoded as JWTPayload;
30
+ } catch (error) {
31
+ throw new Error(`Failed to decode JWT: ${(error as Error).message}`);
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Check if token is expired
37
+ */
38
+ isExpired(token: string): boolean {
39
+ try {
40
+ const decoded = this.decode(token);
41
+
42
+ if (!decoded.exp) {
43
+ // If no expiration, consider it as never expires
44
+ return false;
45
+ }
46
+
47
+ const now = Math.floor(Date.now() / 1000);
48
+ return decoded.exp < now;
49
+ } catch {
50
+ // If we can't decode, consider it expired
51
+ return true;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Get remaining time in seconds
57
+ */
58
+ getRemainingTime(token: string): number {
59
+ try {
60
+ const decoded = this.decode(token);
61
+
62
+ if (!decoded.exp) {
63
+ return Infinity;
64
+ }
65
+
66
+ const now = Math.floor(Date.now() / 1000);
67
+ const remaining = decoded.exp - now;
68
+
69
+ return remaining > 0 ? remaining : 0;
70
+ } catch {
71
+ return 0;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Extract claims from token
77
+ */
78
+ extractClaims(token: string): Record<string, unknown> {
79
+ return this.decode(token);
80
+ }
81
+
82
+ /**
83
+ * Extract roles from token
84
+ */
85
+ extractRoles(token: string): string[] {
86
+ const decoded = this.decode(token);
87
+
88
+ // Try common role claim names
89
+ if (decoded.roles && Array.isArray(decoded.roles)) {
90
+ return decoded.roles;
91
+ }
92
+
93
+ if (
94
+ decoded["https://example.app/roles"] &&
95
+ Array.isArray(decoded["https://example.app/roles"])
96
+ ) {
97
+ return decoded["https://example.app/roles"] as string[];
98
+ }
99
+
100
+ if (decoded.role && Array.isArray(decoded.role)) {
101
+ return decoded.role as string[];
102
+ }
103
+
104
+ if (typeof decoded.role === "string") {
105
+ return [decoded.role];
106
+ }
107
+
108
+ return [];
109
+ }
110
+
111
+ /**
112
+ * Extract permissions from token
113
+ */
114
+ extractPermissions(token: string): string[] {
115
+ const decoded = this.decode(token);
116
+
117
+ // Try common permission claim names
118
+ if (decoded.permissions && Array.isArray(decoded.permissions)) {
119
+ return decoded.permissions;
120
+ }
121
+
122
+ if (
123
+ decoded["https://example.app/permissions"] &&
124
+ Array.isArray(decoded["https://example.app/permissions"])
125
+ ) {
126
+ return decoded["https://example.app/permissions"] as string[];
127
+ }
128
+
129
+ if (decoded.scope) {
130
+ // Handle space-separated scopes (common in OAuth)
131
+ if (typeof decoded.scope === "string") {
132
+ return decoded.scope.split(" ");
133
+ }
134
+ if (Array.isArray(decoded.scope)) {
135
+ return decoded.scope as string[];
136
+ }
137
+ }
138
+
139
+ return [];
140
+ }
141
+
142
+ /**
143
+ * Check if token has specific role
144
+ */
145
+ hasRole(token: string, role: string): boolean {
146
+ const roles = this.extractRoles(token);
147
+ return roles.includes(role);
148
+ }
149
+
150
+ /**
151
+ * Check if token has specific permission
152
+ */
153
+ hasPermission(token: string, permission: string): boolean {
154
+ const permissions = this.extractPermissions(token);
155
+ return permissions.includes(permission);
156
+ }
157
+
158
+ /**
159
+ * Get token expiration date
160
+ */
161
+ getExpirationDate(token: string): Date | null {
162
+ const decoded = this.decode(token);
163
+
164
+ if (!decoded.exp) {
165
+ return null;
166
+ }
167
+
168
+ return new Date(decoded.exp * 1000);
169
+ }
170
+
171
+ /**
172
+ * Get token issue date
173
+ */
174
+ getIssueDate(token: string): Date | null {
175
+ const decoded = this.decode(token);
176
+
177
+ if (!decoded.iat) {
178
+ return null;
179
+ }
180
+
181
+ return new Date(decoded.iat * 1000);
182
+ }
183
+ }
184
+
185
+ // Export singleton instance
186
+ export const jwtHelper = new JWTHelper();
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Secrets-backed persistence for the local-dev sudo password.
3
+ *
4
+ * Goes through the configured secrets strategy (Doppler, 1Password or env —
5
+ * see src/shared/secrets), addressing the named ref `sudo` (default
6
+ * OP_API_KEYS_VAULT / OP_DEV_KEYS_ITEM / OP_DEN_SUDO_FIELD; an org
7
+ * moves it with `secrets.refs.sudo` in its config) — flat key
8
+ * DEN_LOCAL_ADMIN_PWD for key/value backends.
9
+ *
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 null/false so the server can
12
+ * fall back to the manual unlock banner without surfacing CLI noise.
13
+ */
14
+
15
+ import {
16
+ OP_API_KEYS_VAULT,
17
+ OP_DEV_KEYS_ITEM,
18
+ OP_DEN_SUDO_FIELD,
19
+ } from "../constants.js";
20
+ import {
21
+ getSecretsProvider,
22
+ namedSecretRef,
23
+ type SecretRef,
24
+ } from "../secrets/index.js";
25
+
26
+ /** The built-in default for the `sudo` named ref. */
27
+ export const SUDO_SECRET_REF: SecretRef = {
28
+ vault: OP_API_KEYS_VAULT,
29
+ item: OP_DEV_KEYS_ITEM,
30
+ field: OP_DEN_SUDO_FIELD,
31
+ };
32
+
33
+ /** The effective sudo ref (`secrets.refs.sudo` over the default). */
34
+ function sudoRef(): SecretRef {
35
+ return namedSecretRef("sudo", SUDO_SECRET_REF);
36
+ }
37
+
38
+ /** True when the secrets backend is installed AND authenticated. */
39
+ export async function isSecretsBackendReady(): Promise<boolean> {
40
+ return getSecretsProvider().isReady();
41
+ }
42
+
43
+ /** @deprecated use isSecretsBackendReady */
44
+ export const isOpSignedIn = isSecretsBackendReady;
45
+
46
+ /** Read the stored sudo password. Null when unavailable or empty. */
47
+ export async function readSudoFromVault(): Promise<string | null> {
48
+ return getSecretsProvider().tryGet(sudoRef());
49
+ }
50
+
51
+ /** Whether a non-empty sudo password is stored (without returning it). */
52
+ export async function hasSudoInVault(): Promise<boolean> {
53
+ return (await readSudoFromVault()) !== null;
54
+ }
55
+
56
+ /** Save (or overwrite) the sudo password. Throws on backend errors. */
57
+ export async function saveSudoToVault(password: string): Promise<void> {
58
+ await getSecretsProvider().set(sudoRef(), password);
59
+ }
60
+
61
+ /** Name of the active backend, for UI copy ("Save to Doppler"). */
62
+ export function secretsBackendLabel(): string {
63
+ const n = getSecretsProvider().name;
64
+ return n === "1password"
65
+ ? "1Password"
66
+ : n === "doppler"
67
+ ? "Doppler"
68
+ : "environment";
69
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Shared test accounts, from the org config.
3
+ *
4
+ * Which accounts a team logs in as, and where their passwords live in the
5
+ * secrets backend, are org data, so the engine reads them from
6
+ * `test_accounts` in the org config instead of hard-wiring any:
7
+ *
8
+ * test_accounts:
9
+ * vault: QA # secrets vault holding the items (default "test-accounts")
10
+ * roles:
11
+ * user: { email: user@example.com, item: Web Test User }
12
+ * beta: { email: beta@example.com, item: Web Test Beta }
13
+ * admin: # shared only in these environments; elsewhere
14
+ * email: admin@example.com # each engineer runs `make auth-configure`
15
+ * item: Web Test Admin
16
+ * environments: [local, dev]
17
+ *
18
+ * A role with no entry (or outside its `environments`) has no shared
19
+ * account; admin and root_admin then fall back to the per-engineer config
20
+ * written by `make auth-configure`.
21
+ */
22
+
23
+ import { readDenConfig } from "../den-config.js";
24
+ import type { SecretRef } from "../secrets/index.js";
25
+
26
+ export type TestRole = "admin" | "user" | "beta" | "root_admin";
27
+
28
+ export interface TestAccountConfig {
29
+ /** Login email (or username) of the shared account. */
30
+ email: string;
31
+ /** Secrets-backend item holding the password. */
32
+ item: string;
33
+ /** Field within the item (default "password"). */
34
+ field?: string;
35
+ /** Vault override for this account (default: `test_accounts.vault`). */
36
+ vault?: string;
37
+ /** Environments the account is shared in; omitted = every environment. */
38
+ environments?: string[];
39
+ }
40
+
41
+ export interface TestAccountsConfig {
42
+ /** Default vault for every account (default "test-accounts"). */
43
+ vault?: string;
44
+ roles?: Partial<Record<TestRole, TestAccountConfig>>;
45
+ }
46
+
47
+ /** Vault used when `test_accounts.vault` is not set. */
48
+ export const DEFAULT_TEST_ACCOUNTS_VAULT = "test-accounts";
49
+
50
+ /** The configured `test_accounts` block ({} when absent or unreadable). */
51
+ export function readTestAccountsConfig(): TestAccountsConfig {
52
+ try {
53
+ return readDenConfig().test_accounts ?? {};
54
+ } catch {
55
+ return {};
56
+ }
57
+ }
58
+
59
+ /** The vault test-account items live in. */
60
+ export function testAccountsVault(
61
+ cfg: TestAccountsConfig = readTestAccountsConfig(),
62
+ ): string {
63
+ return cfg.vault?.trim() || DEFAULT_TEST_ACCOUNTS_VAULT;
64
+ }
65
+
66
+ /** A shared account resolved for one role and environment. */
67
+ export interface SharedTestAccount {
68
+ email: string;
69
+ ref: SecretRef;
70
+ }
71
+
72
+ /**
73
+ * The shared account for `role` in `environment`, or undefined when the
74
+ * org config declares none there (no entry, incomplete entry, or an
75
+ * `environments` list that excludes it).
76
+ */
77
+ export function sharedTestAccount(
78
+ role: TestRole,
79
+ environment: string,
80
+ cfg: TestAccountsConfig = readTestAccountsConfig(),
81
+ ): SharedTestAccount | undefined {
82
+ const account = cfg.roles?.[role];
83
+ if (!account?.email?.trim() || !account.item?.trim()) return undefined;
84
+ if (
85
+ Array.isArray(account.environments) &&
86
+ !account.environments.includes(environment)
87
+ ) {
88
+ return undefined;
89
+ }
90
+ return {
91
+ email: account.email.trim(),
92
+ ref: {
93
+ vault: account.vault?.trim() || testAccountsVault(cfg),
94
+ item: account.item.trim(),
95
+ field: account.field?.trim() || "password",
96
+ },
97
+ };
98
+ }