@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,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
|
+
}
|