@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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AWS Profile loader
|
|
3
|
+
*
|
|
4
|
+
* Reads config/local/aws-profiles.json — a gitignored per-user file that maps
|
|
5
|
+
* den environments to AWS CLI profile names (set via `make aws-configure`).
|
|
6
|
+
*
|
|
7
|
+
* Returns empty object if the file doesn't exist (local-only usage).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from "fs";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import type { Environment } from "../types/index.js";
|
|
13
|
+
import { getDistRoot } from "../paths.js";
|
|
14
|
+
|
|
15
|
+
// The profiles file lives under the den repo root, not the caller's cwd —
|
|
16
|
+
// den is frequently invoked from inside other repos.
|
|
17
|
+
const PROFILES_FILE = path.resolve(
|
|
18
|
+
getDistRoot(),
|
|
19
|
+
"config",
|
|
20
|
+
"local",
|
|
21
|
+
"aws-profiles.json",
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
let cache: Partial<Record<Environment, string>> | null = null;
|
|
25
|
+
|
|
26
|
+
export function loadAwsProfiles(): Partial<Record<Environment, string>> {
|
|
27
|
+
if (cache) return cache;
|
|
28
|
+
if (!fs.existsSync(PROFILES_FILE)) return (cache = {});
|
|
29
|
+
try {
|
|
30
|
+
cache = JSON.parse(fs.readFileSync(PROFILES_FILE, "utf-8"));
|
|
31
|
+
return cache!;
|
|
32
|
+
} catch {
|
|
33
|
+
return (cache = {});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Returns { AWS_PROFILE: "..." } for the given env if a profile is configured,
|
|
39
|
+
* otherwise returns {}. Spread into process.env when spawning AWS CLI commands.
|
|
40
|
+
*/
|
|
41
|
+
export function awsProfileEnv(env: Environment): Record<string, string> {
|
|
42
|
+
const profile = loadAwsProfiles()[env];
|
|
43
|
+
return profile ? { AWS_PROFILE: profile } : {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Invalidate cache (used in tests or after aws-configure runs). */
|
|
47
|
+
export function clearAwsProfileCache(): void {
|
|
48
|
+
cache = null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Selector
|
|
3
|
+
* Loads and validates environment configurations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFile } from "fs/promises";
|
|
7
|
+
import { resolve, join } from "path";
|
|
8
|
+
import type { Environment, EnvironmentConfig } from "../types/index.js";
|
|
9
|
+
|
|
10
|
+
export class EnvironmentSelector {
|
|
11
|
+
private configDir: string;
|
|
12
|
+
private cache: Map<string, EnvironmentConfig> = new Map();
|
|
13
|
+
|
|
14
|
+
constructor(configDir: string = "./config/environments") {
|
|
15
|
+
this.configDir = resolve(configDir);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load environment configuration
|
|
20
|
+
*/
|
|
21
|
+
async loadEnvironment(env: Environment): Promise<EnvironmentConfig> {
|
|
22
|
+
// Check cache first
|
|
23
|
+
if (this.cache.has(env)) {
|
|
24
|
+
return this.cache.get(env)!;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const configPath = join(this.configDir, `${env}.json`);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const content = await readFile(configPath, "utf-8");
|
|
31
|
+
const config: EnvironmentConfig = JSON.parse(content);
|
|
32
|
+
|
|
33
|
+
// Validate config
|
|
34
|
+
this.validateConfig(config, env);
|
|
35
|
+
|
|
36
|
+
// Cache and return
|
|
37
|
+
this.cache.set(env, config);
|
|
38
|
+
return config;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Environment configuration not found: ${env}\n` +
|
|
43
|
+
`Expected file: ${configPath}\n` +
|
|
44
|
+
`Available environments: ${(await this.getAvailableEnvironments()).join(", ")}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Failed to load environment config for "${env}": ${(error as Error).message}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get list of available environments
|
|
55
|
+
*/
|
|
56
|
+
async getAvailableEnvironments(): Promise<Environment[]> {
|
|
57
|
+
const { readdir } = await import("fs/promises");
|
|
58
|
+
const preferredOrder: Environment[] = [
|
|
59
|
+
"local",
|
|
60
|
+
"dev",
|
|
61
|
+
"sandbox",
|
|
62
|
+
"staging",
|
|
63
|
+
"beta",
|
|
64
|
+
"prod",
|
|
65
|
+
"production",
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const files = await readdir(this.configDir);
|
|
70
|
+
const available = files
|
|
71
|
+
.filter((f) => f.endsWith(".json"))
|
|
72
|
+
.map((f) => f.replace(".json", "") as Environment)
|
|
73
|
+
.filter((e) => preferredOrder.includes(e));
|
|
74
|
+
|
|
75
|
+
// Sort by preferred order
|
|
76
|
+
return available.sort(
|
|
77
|
+
(a, b) => preferredOrder.indexOf(a) - preferredOrder.indexOf(b),
|
|
78
|
+
);
|
|
79
|
+
} catch {
|
|
80
|
+
return preferredOrder;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if environment exists
|
|
86
|
+
*/
|
|
87
|
+
async environmentExists(env: Environment): Promise<boolean> {
|
|
88
|
+
const { access } = await import("fs/promises");
|
|
89
|
+
const configPath = join(this.configDir, `${env}.json`);
|
|
90
|
+
try {
|
|
91
|
+
await access(configPath);
|
|
92
|
+
return true;
|
|
93
|
+
} catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Validate environment configuration
|
|
100
|
+
*/
|
|
101
|
+
private validateConfig(config: EnvironmentConfig, env: Environment): void {
|
|
102
|
+
if (!config.name) {
|
|
103
|
+
throw new Error(`Invalid config for ${env}: missing "name" field`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (config.name !== env) {
|
|
107
|
+
throw new Error(
|
|
108
|
+
`Invalid config for ${env}: "name" field is "${config.name}" but should be "${env}"`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!config.baseUrls) {
|
|
113
|
+
throw new Error(`Invalid config for ${env}: missing "baseUrls" field`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!config.baseUrls.web || !config.baseUrls.api) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Invalid config for ${env}: "baseUrls" must contain "web" and "api"`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const provider = config.auth?.provider ?? (config.auth0 ? "auth0" : "none");
|
|
123
|
+
if (
|
|
124
|
+
provider === "auth0" &&
|
|
125
|
+
(!config.auth0 || !config.auth0.domain || !config.auth0.clientId)
|
|
126
|
+
) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
`Invalid config for ${env}: missing or incomplete "auth0" field`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (
|
|
132
|
+
provider === "cognito" &&
|
|
133
|
+
(!config.auth?.cognito?.region || !config.auth.cognito.clientId)
|
|
134
|
+
) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Invalid config for ${env}: auth.provider "cognito" needs auth.cognito.region and auth.cognito.clientId`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
if (provider === "local-jwt" && !config.auth?.localJwt?.secretKey) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`Invalid config for ${env}: auth.provider "local-jwt" needs auth.localJwt.secretKey`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!config.timeouts || typeof config.timeouts.api !== "number") {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Invalid config for ${env}: missing or invalid "timeouts" field`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clear cache
|
|
154
|
+
*/
|
|
155
|
+
clearCache(): void {
|
|
156
|
+
this.cache.clear();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Export singleton instance
|
|
161
|
+
export const envSelector = new EnvironmentSelector();
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Playwright Config Helpers — environment resolution for sync `defineConfig` callers.
|
|
3
|
+
*
|
|
4
|
+
* The async `envSelector` (see env-selector.ts) is the canonical loader at
|
|
5
|
+
* runtime, but Playwright's `defineConfig` is synchronous and runs before
|
|
6
|
+
* any test fixtures, so it cannot await. These helpers centralize the small
|
|
7
|
+
* amount of env-resolution logic Playwright configs need at config-load
|
|
8
|
+
* time:
|
|
9
|
+
*
|
|
10
|
+
* - resolveEnv → which `Environment` is this run targeting?
|
|
11
|
+
* - resolveAuthPath → where is the saved browser storageState for {role,env}?
|
|
12
|
+
* - resolveBrowserBaseUrl → canonical web URL from config/environments/<env>.json
|
|
13
|
+
*
|
|
14
|
+
* `resolveBrowserBaseUrl` reads the env JSON synchronously (readFileSync)
|
|
15
|
+
* because it runs at module-eval time inside the Playwright config. This is
|
|
16
|
+
* the same source of truth `envSelector.loadEnvironment` consumes — just the
|
|
17
|
+
* sync flavour.
|
|
18
|
+
*/
|
|
19
|
+
import { readFileSync } from "fs";
|
|
20
|
+
import { resolve } from "path";
|
|
21
|
+
import type { Environment, EnvironmentConfig } from "../types/index.js";
|
|
22
|
+
import { getDistRoot } from "../paths.js";
|
|
23
|
+
|
|
24
|
+
export type Role = "admin" | "user" | "beta";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Resolve the target environment from `E2E_ENVIRONMENT`, falling back to
|
|
28
|
+
* `defaultEnv` (typically "local").
|
|
29
|
+
*/
|
|
30
|
+
export function resolveEnv(defaultEnv: Environment = "local"): Environment {
|
|
31
|
+
const fromEnv = process.env.E2E_ENVIRONMENT;
|
|
32
|
+
return (fromEnv && fromEnv.length > 0 ? fromEnv : defaultEnv) as Environment;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Absolute path to the saved browser storageState file for a given role+env.
|
|
37
|
+
*
|
|
38
|
+
* Convention: `<repoRoot>/playwright/.auth/<role>-<env>.json`. Mirrors the
|
|
39
|
+
* pattern used in `pillars/browser/global-setup.ts` when sessions are minted.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveAuthPath(
|
|
42
|
+
role: Role,
|
|
43
|
+
env: Environment,
|
|
44
|
+
repoRoot: string,
|
|
45
|
+
): string {
|
|
46
|
+
return resolve(repoRoot, `playwright/.auth/${role}-${env}.json`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Canonical browser-facing base URL for `env`, read synchronously from
|
|
51
|
+
* `config/environments/<env>.json`. Same source of truth as the async
|
|
52
|
+
* `envSelector.loadEnvironment(env).baseUrls.web`.
|
|
53
|
+
*
|
|
54
|
+
* Throws if the env config is missing/malformed — fail fast at config-load
|
|
55
|
+
* rather than letting Playwright proceed with a bogus baseURL.
|
|
56
|
+
*/
|
|
57
|
+
export function resolveBrowserBaseUrl(env: Environment): string {
|
|
58
|
+
// read-only fail-fast lookup, den-root anchored for determinism
|
|
59
|
+
const configPath = resolve(getDistRoot(), `config/environments/${env}.json`);
|
|
60
|
+
let raw: string;
|
|
61
|
+
try {
|
|
62
|
+
raw = readFileSync(configPath, "utf-8");
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`[playwright-env] Cannot read environment config for "${env}" at ${configPath}: ${(error as Error).message}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
let cfg: EnvironmentConfig;
|
|
69
|
+
try {
|
|
70
|
+
cfg = JSON.parse(raw) as EnvironmentConfig;
|
|
71
|
+
} catch (error) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
`[playwright-env] Invalid JSON in ${configPath}: ${(error as Error).message}`,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
if (!cfg.baseUrls || !cfg.baseUrls.web) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`[playwright-env] Env config ${configPath} is missing baseUrls.web`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return cfg.baseUrls.web;
|
|
82
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Catalog
|
|
3
|
+
*
|
|
4
|
+
* Manages config/local/service-catalog.yml — the per-engineer registry of
|
|
5
|
+
* repos known to den, whether linked (developer's own checkout) or managed
|
|
6
|
+
* (den clones and updates them).
|
|
7
|
+
*
|
|
8
|
+
* Gracefully returns an empty catalog when the file does not exist so that
|
|
9
|
+
* all callers work on a fresh machine without requiring setup first.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import * as yaml from "yaml";
|
|
15
|
+
import type { ServiceCatalog, ServiceCatalogEntry } from "../types/index.js";
|
|
16
|
+
import { getDistRoot } from "../paths.js";
|
|
17
|
+
import { requireGithubOrg } from "../den-config.js";
|
|
18
|
+
|
|
19
|
+
/** Fallback GitHub org when `config/den.yml` doesn't set `github_org`. */
|
|
20
|
+
|
|
21
|
+
const ROOT = getDistRoot();
|
|
22
|
+
const CATALOG_PATH = path.join(ROOT, "config", "local", "service-catalog.yml");
|
|
23
|
+
|
|
24
|
+
export function readCatalog(): ServiceCatalog {
|
|
25
|
+
try {
|
|
26
|
+
const raw = fs.readFileSync(CATALOG_PATH, "utf-8");
|
|
27
|
+
const parsed = yaml.parse(raw) as unknown;
|
|
28
|
+
if (parsed == null) return {}; // empty file
|
|
29
|
+
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`Malformed service catalog at ${CATALOG_PATH}: expected a mapping`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return parsed as ServiceCatalog;
|
|
35
|
+
} catch (err: unknown) {
|
|
36
|
+
if ((err as NodeJS.ErrnoException).code === "ENOENT") return {};
|
|
37
|
+
throw err;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function writeCatalog(catalog: ServiceCatalog): void {
|
|
42
|
+
fs.mkdirSync(path.dirname(CATALOG_PATH), { recursive: true });
|
|
43
|
+
fs.writeFileSync(CATALOG_PATH, yaml.stringify(catalog), "utf-8");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function upsertCatalogEntry(
|
|
47
|
+
repoName: string,
|
|
48
|
+
entry: ServiceCatalogEntry,
|
|
49
|
+
): void {
|
|
50
|
+
const catalog = readCatalog();
|
|
51
|
+
catalog[repoName] = entry;
|
|
52
|
+
writeCatalog(catalog);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* GitHub slug to clone `repoName` from.
|
|
57
|
+
*
|
|
58
|
+
* The catalog's `gh` when set, else the conventional `<github_org>/<repo>` —
|
|
59
|
+
* the same rule `seedDefaultWorkspace` uses. Entries written by the
|
|
60
|
+
* dev-managed add flow carry `gh: ""`, so a repo can be IN the catalog and
|
|
61
|
+
* still have no slug; deriving it (rather than refusing to clone) is what lets
|
|
62
|
+
* a repo switched to managed actually be cloned.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveRepoGhSlug(
|
|
65
|
+
repoName: string,
|
|
66
|
+
catalog: ServiceCatalog = readCatalog(),
|
|
67
|
+
): string {
|
|
68
|
+
const gh = catalog[repoName]?.gh?.trim();
|
|
69
|
+
if (gh) return gh;
|
|
70
|
+
return `${requireGithubOrg()}/${repoName}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function removeCatalogEntry(repoName: string): void {
|
|
74
|
+
const catalog = readCatalog();
|
|
75
|
+
delete catalog[repoName];
|
|
76
|
+
writeCatalog(catalog);
|
|
77
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clone-destination preflight (defense in depth for the Jun 29 incident).
|
|
3
|
+
*
|
|
4
|
+
* Path anchoring (getDistRoot) is the first line of defense; this guard is
|
|
5
|
+
* the second: even if some entry point regresses and computes a destination
|
|
6
|
+
* from the wrong anchor, refuse to clone anywhere that could damage another
|
|
7
|
+
* repo. Refusal conditions:
|
|
8
|
+
*
|
|
9
|
+
* 1. OUTSIDE DEN-OWNED LOCATIONS — destinations must live under the
|
|
10
|
+
* repos root, den-workspaces, .den-services, or .smoke-clones.
|
|
11
|
+
*
|
|
12
|
+
* 2. SELF-NESTING — the destination is inside a checkout of the same repo
|
|
13
|
+
* being cloned (e.g. ~/projects/infra-stack/infra-stack).
|
|
14
|
+
*
|
|
15
|
+
* 3. FOREIGN WORKING TREE — the destination sits inside an existing git
|
|
16
|
+
* working tree that den does not own. (Den's own tree is expected:
|
|
17
|
+
* .den-services/ and .smoke-clones/ live inside the den checkout.
|
|
18
|
+
* A tree that already enclosed the allowed root itself — e.g. a
|
|
19
|
+
* home-directory dotfiles repo above ~/projects — is the user's chosen
|
|
20
|
+
* layout and is allowed.)
|
|
21
|
+
*
|
|
22
|
+
* Refusals throw with the computed destination, cwd, and resolved den root
|
|
23
|
+
* so a misconfiguration is diagnosable in one glance.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { execFileSync } from "child_process";
|
|
27
|
+
import * as fs from "fs";
|
|
28
|
+
import * as path from "path";
|
|
29
|
+
import { getDistRoot } from "../paths.js";
|
|
30
|
+
import { getReposRoot } from "../den-config.js";
|
|
31
|
+
import { DEN_SERVICES_DIR, DEN_WORKSPACES_DIR } from "./workspaces.js";
|
|
32
|
+
|
|
33
|
+
import { envKey, product } from "../product.js";
|
|
34
|
+
export class CloneDestinationError extends Error {}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Split `p` into the realpath of its nearest EXISTING ancestor plus the
|
|
38
|
+
* not-yet-existing suffix. Never collapses the suffix — a collapsed path
|
|
39
|
+
* compared against the allowlist would over- or under-permit.
|
|
40
|
+
*/
|
|
41
|
+
function splitExisting(p: string): { real: string; suffix: string } {
|
|
42
|
+
let probe = path.resolve(p);
|
|
43
|
+
let suffix = "";
|
|
44
|
+
for (;;) {
|
|
45
|
+
if (fs.existsSync(probe)) {
|
|
46
|
+
let real = probe;
|
|
47
|
+
try {
|
|
48
|
+
real = fs.realpathSync(probe);
|
|
49
|
+
} catch {
|
|
50
|
+
/* keep unresolved */
|
|
51
|
+
}
|
|
52
|
+
return { real, suffix };
|
|
53
|
+
}
|
|
54
|
+
suffix = suffix
|
|
55
|
+
? path.join(path.basename(probe), suffix)
|
|
56
|
+
: path.basename(probe);
|
|
57
|
+
const parent = path.dirname(probe);
|
|
58
|
+
if (parent === probe) return { real: probe, suffix };
|
|
59
|
+
probe = parent;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `p` with symlinks resolved on its existing prefix (macOS /tmp → /private/tmp). */
|
|
64
|
+
function realFull(p: string): string {
|
|
65
|
+
const { real, suffix } = splitExisting(p);
|
|
66
|
+
return suffix ? path.join(real, suffix) : real;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Toplevel of the git working tree containing existing dir `dir`, or null. */
|
|
70
|
+
function gitToplevelOf(dir: string): string | null {
|
|
71
|
+
try {
|
|
72
|
+
const out = execFileSync(
|
|
73
|
+
"git",
|
|
74
|
+
["-C", dir, "rev-parse", "--show-toplevel"],
|
|
75
|
+
{
|
|
76
|
+
encoding: "utf-8",
|
|
77
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
78
|
+
},
|
|
79
|
+
).trim();
|
|
80
|
+
return out ? realFull(out) : null;
|
|
81
|
+
} catch {
|
|
82
|
+
return null; // not inside a git working tree (or git unavailable)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isWithin(child: string, parent: string): boolean {
|
|
87
|
+
const rel = path.relative(parent, child);
|
|
88
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function refuse(
|
|
92
|
+
reason: string,
|
|
93
|
+
dest: string,
|
|
94
|
+
extra: Record<string, string> = {},
|
|
95
|
+
): never {
|
|
96
|
+
const lines = [
|
|
97
|
+
`${product().cli}: REFUSING to clone — ${reason}`,
|
|
98
|
+
` destination: ${dest}`,
|
|
99
|
+
` process cwd: ${process.cwd()}`,
|
|
100
|
+
` ${product().cli} root: ${getDistRoot()}`,
|
|
101
|
+
];
|
|
102
|
+
for (const [k, v] of Object.entries(extra)) lines.push(` ${k}: ${v}`);
|
|
103
|
+
lines.push(
|
|
104
|
+
` If the destination is wrong, fix the anchor (${envKey("ROOT")} / ` +
|
|
105
|
+
`repos_root in ${product().configFile}) rather than cloning by hand.`,
|
|
106
|
+
);
|
|
107
|
+
throw new CloneDestinationError(lines.join("\n"));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Throw (CloneDestinationError) unless `dest` is a safe place to create a
|
|
112
|
+
* fresh clone of `repoName`. Call BEFORE any mkdir/clone side effect.
|
|
113
|
+
*/
|
|
114
|
+
export function assertSafeCloneDestination(
|
|
115
|
+
dest: string,
|
|
116
|
+
repoName: string,
|
|
117
|
+
): void {
|
|
118
|
+
const absDest = path.resolve(dest);
|
|
119
|
+
const distRoot = realFull(getDistRoot());
|
|
120
|
+
|
|
121
|
+
const allowedRoots = [
|
|
122
|
+
realFull(getReposRoot()),
|
|
123
|
+
realFull(DEN_WORKSPACES_DIR),
|
|
124
|
+
realFull(DEN_SERVICES_DIR),
|
|
125
|
+
realFull(path.join(distRoot, ".smoke-clones")),
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
const destReal = realFull(absDest);
|
|
129
|
+
const containingRoot = allowedRoots.find((r) => isWithin(destReal, r));
|
|
130
|
+
if (!containingRoot) {
|
|
131
|
+
refuse(
|
|
132
|
+
`destination is outside every ${product().cli}-owned location`,
|
|
133
|
+
absDest,
|
|
134
|
+
{
|
|
135
|
+
"allowed under": allowedRoots.join(" | "),
|
|
136
|
+
},
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const enclosingTree = gitToplevelOf(
|
|
141
|
+
splitExisting(path.dirname(absDest)).real,
|
|
142
|
+
);
|
|
143
|
+
if (enclosingTree) {
|
|
144
|
+
if (path.basename(enclosingTree) === repoName) {
|
|
145
|
+
refuse(
|
|
146
|
+
`destination would nest ${repoName} inside a checkout of itself`,
|
|
147
|
+
absDest,
|
|
148
|
+
{ "enclosing git working tree": enclosingTree },
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
const isDenTree = enclosingTree === distRoot;
|
|
152
|
+
// A tree that encloses the allowed root itself (repos root under a
|
|
153
|
+
// dotfiles-home repo, etc.) predates den's write — user's layout.
|
|
154
|
+
const enclosesAllowedRoot = isWithin(containingRoot, enclosingTree);
|
|
155
|
+
if (!isDenTree && !enclosesAllowedRoot) {
|
|
156
|
+
refuse(
|
|
157
|
+
`destination is inside a git working tree ${product().cli} does not own`,
|
|
158
|
+
absDest,
|
|
159
|
+
{ "enclosing git working tree": enclosingTree },
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|