@monster-orange/den 0.0.0-stage → 0.1.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 +67 -0
- package/LICENSE +21 -0
- package/README.md +166 -2
- package/bin/den +61 -0
- package/docs/distributions.md +256 -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 +104 -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 +406 -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 +108 -0
- package/src/pillars/security/findings.ts +151 -0
- package/src/pillars/security/plan.ts +355 -0
- package/src/pillars/security/run.ts +729 -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/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 +566 -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/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 +589 -0
- package/src/shared/environment-manager/index.ts +6 -0
- package/src/shared/environment-manager/load-applications.ts +78 -0
- package/src/shared/environment-manager/log-tail.ts +212 -0
- package/src/shared/environment-manager/make-targets.ts +78 -0
- package/src/shared/environment-manager/opctl.ts +334 -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-env.ts +110 -0
- package/src/shared/environment-manager/service-runner.ts +1588 -0
- package/src/shared/environment-manager/up-plan.ts +302 -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/migrate-manifest.ts +101 -0
- package/src/shared/manifest/read-manifest.ts +160 -0
- package/src/shared/manifest/schema.ts +299 -0
- package/src/shared/manifest/service-links.ts +93 -0
- package/src/shared/manifest/validate-manifest.ts +140 -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 +178 -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/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 +88 -0
- package/src/shared/stacks/render.ts +253 -0
- package/src/shared/stacks/runtime.ts +334 -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 +678 -0
- package/src/shared/ui-marker.ts +19 -0
- package/src/ui/cli/applications-command.ts +116 -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 +91 -0
- package/src/ui/cli/flag-parser.ts +246 -0
- package/src/ui/cli/graph-command.ts +524 -0
- package/src/ui/cli/health-display.ts +63 -0
- package/src/ui/cli/index.ts +3656 -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/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 +144 -0
- package/src/ui/cli/stalled-setup-recovery.ts +129 -0
- package/src/ui/cli/test-runner.ts +1312 -0
- package/src/ui/cli/web-command.ts +1073 -0
- package/src/ui/server/changelog-parser.ts +94 -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/http-helpers.ts +98 -0
- package/src/ui/server/index.ts +3236 -0
- package/src/ui/server/log-history.ts +452 -0
- package/src/ui/server/perf-report.ts +42 -0
- package/src/ui/server/public/environments.html +494 -0
- package/src/ui/server/public/favicon.ico +0 -0
- package/src/ui/server/public/header.js +1371 -0
- package/src/ui/server/public/index.html +2595 -0
- package/src/ui/server/public/local-dev.html +7405 -0
- package/src/ui/server/public/logs.html +1489 -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 +1098 -0
- package/src/ui/server/public/workspace-picker.js +69 -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/workspace-routes.ts +2154 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token Refresh Utility
|
|
3
|
+
* Check a saved browser session before running tests, and refresh it by
|
|
4
|
+
* letting the app's own auth SDK renew it when it has gone stale.
|
|
5
|
+
*
|
|
6
|
+
* "Signed in" means: opening `signedInPath` (environment config
|
|
7
|
+
* `auth.browser.signedInPath`, default "/") ends on the app origin under
|
|
8
|
+
* that path, rather than at the IdP or a login page.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from "fs/promises";
|
|
12
|
+
import { chromium } from "@playwright/test";
|
|
13
|
+
import type { BrowserLoginConfig } from "../types/index.js";
|
|
14
|
+
|
|
15
|
+
interface StorageState {
|
|
16
|
+
cookies: Array<{
|
|
17
|
+
name: string;
|
|
18
|
+
value: string;
|
|
19
|
+
domain: string;
|
|
20
|
+
path: string;
|
|
21
|
+
expires: number;
|
|
22
|
+
httpOnly: boolean;
|
|
23
|
+
secure: boolean;
|
|
24
|
+
// Playwright's storageState cookie type requires the literal union.
|
|
25
|
+
sameSite: "Strict" | "Lax" | "None";
|
|
26
|
+
}>;
|
|
27
|
+
origins: Array<{
|
|
28
|
+
origin: string;
|
|
29
|
+
localStorage: Array<{
|
|
30
|
+
name: string;
|
|
31
|
+
value: string;
|
|
32
|
+
}>;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if auth tokens are expired and refresh if needed
|
|
38
|
+
*/
|
|
39
|
+
export async function refreshAuthIfNeeded(
|
|
40
|
+
authFilePath: string,
|
|
41
|
+
baseUrl: string,
|
|
42
|
+
login: BrowserLoginConfig = {},
|
|
43
|
+
): Promise<boolean> {
|
|
44
|
+
const signedInPath = normalisePath(login.signedInPath ?? "/");
|
|
45
|
+
try {
|
|
46
|
+
// Check if auth file exists
|
|
47
|
+
const fileExists = await fs
|
|
48
|
+
.access(authFilePath)
|
|
49
|
+
.then(() => true)
|
|
50
|
+
.catch(() => false);
|
|
51
|
+
if (!fileExists) {
|
|
52
|
+
console.log(`⚠️ No auth session found at: ${authFilePath}`);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Read storage state
|
|
57
|
+
const storageStateContent = await fs.readFile(authFilePath, "utf-8");
|
|
58
|
+
const storageState: StorageState = JSON.parse(storageStateContent);
|
|
59
|
+
|
|
60
|
+
// Check if tokens are still valid
|
|
61
|
+
const isValid = await checkTokenValidity(
|
|
62
|
+
storageState,
|
|
63
|
+
baseUrl,
|
|
64
|
+
signedInPath,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
if (isValid) {
|
|
68
|
+
console.log("✓ Auth session is still valid");
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log("⚠️ Auth session expired, attempting to refresh...");
|
|
73
|
+
|
|
74
|
+
// Try to refresh using the saved session
|
|
75
|
+
const refreshed = await attemptRefresh(
|
|
76
|
+
authFilePath,
|
|
77
|
+
baseUrl,
|
|
78
|
+
storageState,
|
|
79
|
+
signedInPath,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (refreshed) {
|
|
83
|
+
console.log("✓ Auth session refreshed successfully");
|
|
84
|
+
return true;
|
|
85
|
+
} else {
|
|
86
|
+
console.log(
|
|
87
|
+
"❌ Could not refresh auth session - manual re-authentication required",
|
|
88
|
+
);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.warn(`Token refresh check failed: ${(error as Error).message}`);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalisePath(p: string): string {
|
|
98
|
+
return p.startsWith("/") ? p : `/${p}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** True when `href` is on the app origin under `signedInPath`. */
|
|
102
|
+
export function isSignedInUrl(
|
|
103
|
+
href: string,
|
|
104
|
+
baseUrl: string,
|
|
105
|
+
signedInPath: string,
|
|
106
|
+
): boolean {
|
|
107
|
+
try {
|
|
108
|
+
const url = new URL(href);
|
|
109
|
+
if (url.origin !== new URL(baseUrl).origin) return false;
|
|
110
|
+
const prefix = signedInPath.replace(/\/+$/, "");
|
|
111
|
+
return (
|
|
112
|
+
prefix === "" ||
|
|
113
|
+
url.pathname === prefix ||
|
|
114
|
+
url.pathname.startsWith(`${prefix}/`)
|
|
115
|
+
);
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Wait (briefly) for any IdP redirect to land back on the app origin. */
|
|
122
|
+
async function settleOnApp(
|
|
123
|
+
page: { waitForURL(fn: (url: URL) => boolean, o: object): Promise<void> },
|
|
124
|
+
baseUrl: string,
|
|
125
|
+
): Promise<void> {
|
|
126
|
+
const appOrigin = new URL(baseUrl).origin;
|
|
127
|
+
try {
|
|
128
|
+
await page.waitForURL((url) => url.origin === appOrigin, {
|
|
129
|
+
timeout: 5000,
|
|
130
|
+
});
|
|
131
|
+
} catch {
|
|
132
|
+
// Timeout is OK - the check below decides
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if the current session is still valid
|
|
138
|
+
*/
|
|
139
|
+
async function checkTokenValidity(
|
|
140
|
+
storageState: StorageState,
|
|
141
|
+
baseUrl: string,
|
|
142
|
+
signedInPath: string,
|
|
143
|
+
): Promise<boolean> {
|
|
144
|
+
let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined;
|
|
145
|
+
try {
|
|
146
|
+
// Launch headless browser with saved state
|
|
147
|
+
browser = await chromium.launch({ headless: true });
|
|
148
|
+
const context = await browser.newContext({ storageState });
|
|
149
|
+
const page = await context.newPage();
|
|
150
|
+
|
|
151
|
+
// Try to navigate to a protected route
|
|
152
|
+
await page.goto(baseUrl.replace(/\/$/, "") + signedInPath, {
|
|
153
|
+
waitUntil: "domcontentloaded",
|
|
154
|
+
timeout: 10000,
|
|
155
|
+
});
|
|
156
|
+
await settleOnApp(page, baseUrl);
|
|
157
|
+
|
|
158
|
+
// Still on the protected route (not redirected to a login) = signed in
|
|
159
|
+
return isSignedInUrl(page.url(), baseUrl, signedInPath);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const errorMsg = (error as Error).message;
|
|
162
|
+
// If the app isn't running, we can't validate - assume tokens might be valid
|
|
163
|
+
if (
|
|
164
|
+
errorMsg.includes("ERR_NAME_NOT_RESOLVED") ||
|
|
165
|
+
errorMsg.includes("ERR_CONNECTION_REFUSED")
|
|
166
|
+
) {
|
|
167
|
+
console.log(" ℹ️ Cannot reach app - skipping validation");
|
|
168
|
+
return true; // Assume valid, let tests fail naturally if they're not
|
|
169
|
+
}
|
|
170
|
+
console.warn(` Token validity check failed: ${errorMsg}`);
|
|
171
|
+
return false;
|
|
172
|
+
} finally {
|
|
173
|
+
// Close on every path — a thrown goto previously leaked the Chromium process.
|
|
174
|
+
await browser?.close();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Attempt to refresh the session
|
|
180
|
+
*/
|
|
181
|
+
async function attemptRefresh(
|
|
182
|
+
authFilePath: string,
|
|
183
|
+
baseUrl: string,
|
|
184
|
+
oldStorageState: StorageState,
|
|
185
|
+
signedInPath: string,
|
|
186
|
+
): Promise<boolean> {
|
|
187
|
+
let browser: Awaited<ReturnType<typeof chromium.launch>> | undefined;
|
|
188
|
+
try {
|
|
189
|
+
// Refresh tokens usually live in HTTP-only cookies we can't touch
|
|
190
|
+
// directly. The best approach is to:
|
|
191
|
+
// 1. Load the page with the old session
|
|
192
|
+
// 2. Let the app's auth SDK handle the refresh automatically
|
|
193
|
+
// 3. Save the new session
|
|
194
|
+
|
|
195
|
+
browser = await chromium.launch({ headless: true });
|
|
196
|
+
const context = await browser.newContext({ storageState: oldStorageState });
|
|
197
|
+
const page = await context.newPage();
|
|
198
|
+
|
|
199
|
+
// Navigate to the app - its auth SDK might auto-refresh
|
|
200
|
+
await page.goto(baseUrl, {
|
|
201
|
+
waitUntil: "networkidle",
|
|
202
|
+
timeout: 30000,
|
|
203
|
+
});
|
|
204
|
+
await settleOnApp(page, baseUrl);
|
|
205
|
+
|
|
206
|
+
// Try to access a protected route
|
|
207
|
+
await page.goto(baseUrl.replace(/\/$/, "") + signedInPath, {
|
|
208
|
+
waitUntil: "domcontentloaded",
|
|
209
|
+
timeout: 15000,
|
|
210
|
+
});
|
|
211
|
+
await settleOnApp(page, baseUrl);
|
|
212
|
+
|
|
213
|
+
// Check if we made it to the protected route
|
|
214
|
+
const isAuthenticated = isSignedInUrl(page.url(), baseUrl, signedInPath);
|
|
215
|
+
|
|
216
|
+
if (isAuthenticated) {
|
|
217
|
+
// Save the refreshed session
|
|
218
|
+
await context.storageState({ path: authFilePath });
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return false;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
const errorMsg = (error as Error).message;
|
|
225
|
+
// If the app isn't running, we can't refresh - but this is OK
|
|
226
|
+
if (
|
|
227
|
+
errorMsg.includes("ERR_NAME_NOT_RESOLVED") ||
|
|
228
|
+
errorMsg.includes("ERR_CONNECTION_REFUSED")
|
|
229
|
+
) {
|
|
230
|
+
console.log(" ℹ️ Cannot reach app - skipping refresh");
|
|
231
|
+
return true; // Assume OK, let tests handle the connection error
|
|
232
|
+
}
|
|
233
|
+
console.warn(` Token refresh attempt failed: ${errorMsg}`);
|
|
234
|
+
return false;
|
|
235
|
+
} finally {
|
|
236
|
+
// Close on every path — a thrown goto previously leaked the Chromium process.
|
|
237
|
+
await browser?.close();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Get token expiry time in seconds (for manual token inspection)
|
|
243
|
+
*/
|
|
244
|
+
function getTokenExpiry(token: string): number | null {
|
|
245
|
+
try {
|
|
246
|
+
// JWT tokens are base64 encoded
|
|
247
|
+
const parts = token.split(".");
|
|
248
|
+
if (parts.length !== 3) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Decode payload
|
|
253
|
+
const payload = JSON.parse(Buffer.from(parts[1], "base64").toString());
|
|
254
|
+
return payload.exp || null;
|
|
255
|
+
} catch {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Check if token is expired
|
|
262
|
+
*/
|
|
263
|
+
export function isTokenExpired(token: string): boolean {
|
|
264
|
+
const expiry = getTokenExpiry(token);
|
|
265
|
+
if (!expiry) {
|
|
266
|
+
return true; // Assume expired if we can't parse
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const now = Math.floor(Date.now() / 1000);
|
|
270
|
+
const bufferSeconds = 300; // 5 minute buffer
|
|
271
|
+
|
|
272
|
+
return now >= expiry - bufferSeconds;
|
|
273
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The startup banner for the CLI and web server: the org's `brand.banner`
|
|
3
|
+
* lines when configured, else the brand name spelled out. No product's art
|
|
4
|
+
* is built in.
|
|
5
|
+
*/
|
|
6
|
+
import { readBrand } from "./brand.js";
|
|
7
|
+
|
|
8
|
+
export const DEN_BANNER_LINES: string[] = (() => {
|
|
9
|
+
const b = readBrand();
|
|
10
|
+
if (b.banner?.length) return b.banner;
|
|
11
|
+
return [` ${b.name.toUpperCase().split("").join(" ")}`];
|
|
12
|
+
})();
|
|
13
|
+
|
|
14
|
+
export const DEN_SUBTITLE = ` ${readBrand().tagline}`;
|
|
15
|
+
export const DEN_DIVIDER = " ──────────────────────────────────";
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product identity shown to people: the banner, web page titles and the help
|
|
3
|
+
* header. Read from the org config's `brand:` block, defaulting to the
|
|
4
|
+
* profile's name (den.yml). Runtime names (env vars, files, directories) come
|
|
5
|
+
* from the profile, never from the brand.
|
|
6
|
+
*
|
|
7
|
+
* brand:
|
|
8
|
+
* name: Acme Dev # shown in titles and the banner
|
|
9
|
+
* tagline: Acme local dev and E2E harness
|
|
10
|
+
* banner: [ "...", "..." ] # optional ASCII art lines
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import * as yaml from "yaml";
|
|
15
|
+
import { getDistRoot } from "./paths.js";
|
|
16
|
+
import { product } from "./product.js";
|
|
17
|
+
|
|
18
|
+
export interface BrandConfig {
|
|
19
|
+
name: string;
|
|
20
|
+
tagline: string;
|
|
21
|
+
banner?: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function defaultBrand(): BrandConfig {
|
|
25
|
+
return { name: product().title, tagline: "Local dev and E2E harness" };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let cached: BrandConfig | null = null;
|
|
29
|
+
|
|
30
|
+
export function readBrand(root: string = getDistRoot()): BrandConfig {
|
|
31
|
+
if (cached) return cached;
|
|
32
|
+
const fallback = defaultBrand();
|
|
33
|
+
let raw: Partial<BrandConfig> = {};
|
|
34
|
+
try {
|
|
35
|
+
const parsed = yaml.parse(
|
|
36
|
+
fs.readFileSync(path.join(root, product().configFile), "utf-8"),
|
|
37
|
+
) as { brand?: Partial<BrandConfig> } | null;
|
|
38
|
+
raw = parsed?.brand ?? {};
|
|
39
|
+
} catch {
|
|
40
|
+
/* no config — defaults */
|
|
41
|
+
}
|
|
42
|
+
cached = {
|
|
43
|
+
name: raw.name?.trim() || fallback.name,
|
|
44
|
+
tagline: raw.tagline?.trim() || fallback.tagline,
|
|
45
|
+
banner: Array.isArray(raw.banner) ? raw.banner.map(String) : undefined,
|
|
46
|
+
};
|
|
47
|
+
return cached;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The brand name as a slug: "Acme Dev" → "acme-dev". Names the web route. */
|
|
51
|
+
export function brandSlug(brand: BrandConfig = readBrand()): string {
|
|
52
|
+
const s = brand.name
|
|
53
|
+
.trim()
|
|
54
|
+
.toLowerCase()
|
|
55
|
+
.replace(/[^a-z0-9-]+/g, "-")
|
|
56
|
+
.replace(/^-+|-+$/g, "");
|
|
57
|
+
return s || product().name;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function resetBrandCache(): void {
|
|
61
|
+
cached = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Put the brand name in an HTML page's <title> (pages carry the product title). */
|
|
65
|
+
export function brandHtmlTitle(html: string, brand: BrandConfig): string {
|
|
66
|
+
const title = product().title;
|
|
67
|
+
if (brand.name === title) return html;
|
|
68
|
+
return html.replace(
|
|
69
|
+
/<title>([^<]*)<\/title>/i,
|
|
70
|
+
(_m, t: string) =>
|
|
71
|
+
`<title>${t.split(title).join(escapeHtml(brand.name))}</title>`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function escapeHtml(s: string): string {
|
|
76
|
+
return s.replace(/[&<>"]/g, (c) =>
|
|
77
|
+
c === "&" ? "&" : c === "<" ? "<" : c === ">" ? ">" : """,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { tabExists, type TerminalApp } from "./terminal/tabs.js";
|
|
4
|
+
import { getStateDir } from "./paths.js";
|
|
5
|
+
|
|
6
|
+
export interface CliTabRecord {
|
|
7
|
+
address: string;
|
|
8
|
+
terminalApp: TerminalApp;
|
|
9
|
+
tabId: string;
|
|
10
|
+
startedAt: Date;
|
|
11
|
+
cwd?: string;
|
|
12
|
+
command?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type CliTabRecordJSON = Omit<CliTabRecord, "startedAt"> & {
|
|
16
|
+
startedAt: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const _tabRegistry = new Map<string, CliTabRecord>();
|
|
20
|
+
let loadedFromPath: string | null = null;
|
|
21
|
+
|
|
22
|
+
function getTabsFile(): string {
|
|
23
|
+
return (
|
|
24
|
+
process.env.DEN_CLI_TABS_FILE ?? path.join(getStateDir(), "cli-tabs.json")
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toRecord(json: CliTabRecordJSON): CliTabRecord {
|
|
29
|
+
return {
|
|
30
|
+
...json,
|
|
31
|
+
startedAt: new Date(json.startedAt),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function readTabsFile(filePath: string): Map<string, CliTabRecord> {
|
|
36
|
+
const map = new Map<string, CliTabRecord>();
|
|
37
|
+
try {
|
|
38
|
+
if (!fs.existsSync(filePath)) return map;
|
|
39
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8")) as
|
|
40
|
+
| Record<string, CliTabRecordJSON>
|
|
41
|
+
| CliTabRecordJSON[];
|
|
42
|
+
const records = Array.isArray(parsed) ? parsed : Object.values(parsed);
|
|
43
|
+
for (const item of records) {
|
|
44
|
+
if (!item.address || !item.tabId || !item.terminalApp) continue;
|
|
45
|
+
map.set(item.address, toRecord(item));
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// Corrupt sidecar: leave the in-memory map empty.
|
|
49
|
+
}
|
|
50
|
+
return map;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function writeTabsFile(filePath: string, map: Map<string, CliTabRecord>): void {
|
|
54
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
55
|
+
const json: Record<string, CliTabRecordJSON> = {};
|
|
56
|
+
for (const [address, record] of map.entries()) {
|
|
57
|
+
json[address] = {
|
|
58
|
+
...record,
|
|
59
|
+
startedAt: record.startedAt.toISOString(),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const tmp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
64
|
+
fs.writeFileSync(tmp, JSON.stringify(json, null, 2) + "\n", "utf-8");
|
|
65
|
+
fs.renameSync(tmp, filePath);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function loadTabs(force = false): void {
|
|
69
|
+
const filePath = getTabsFile();
|
|
70
|
+
if (!force && loadedFromPath === filePath) return;
|
|
71
|
+
_tabRegistry.clear();
|
|
72
|
+
for (const [address, record] of readTabsFile(filePath).entries()) {
|
|
73
|
+
_tabRegistry.set(address, record);
|
|
74
|
+
}
|
|
75
|
+
loadedFromPath = filePath;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function reloadForMutation(): string {
|
|
79
|
+
const filePath = getTabsFile();
|
|
80
|
+
loadTabs(true);
|
|
81
|
+
return filePath;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function recordTab(
|
|
85
|
+
address: string,
|
|
86
|
+
info: Omit<CliTabRecord, "address" | "startedAt"> &
|
|
87
|
+
Partial<Pick<CliTabRecord, "startedAt">>,
|
|
88
|
+
): CliTabRecord {
|
|
89
|
+
const filePath = reloadForMutation();
|
|
90
|
+
const record: CliTabRecord = {
|
|
91
|
+
address,
|
|
92
|
+
...info,
|
|
93
|
+
startedAt: info.startedAt ?? new Date(),
|
|
94
|
+
};
|
|
95
|
+
_tabRegistry.set(address, record);
|
|
96
|
+
writeTabsFile(filePath, _tabRegistry);
|
|
97
|
+
return record;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function lookupTab(address: string): CliTabRecord | null {
|
|
101
|
+
loadTabs();
|
|
102
|
+
return _tabRegistry.get(address) ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function forgetTab(address: string): void {
|
|
106
|
+
const filePath = reloadForMutation();
|
|
107
|
+
_tabRegistry.delete(address);
|
|
108
|
+
writeTabsFile(filePath, _tabRegistry);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function listTabs(): CliTabRecord[] {
|
|
112
|
+
loadTabs();
|
|
113
|
+
return Array.from(_tabRegistry.values());
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function cleanupStaleTabs(): void {
|
|
117
|
+
const filePath = reloadForMutation();
|
|
118
|
+
let changed = false;
|
|
119
|
+
for (const [address, record] of _tabRegistry.entries()) {
|
|
120
|
+
try {
|
|
121
|
+
if (!tabExists(record.tabId, record.terminalApp)) {
|
|
122
|
+
_tabRegistry.delete(address);
|
|
123
|
+
changed = true;
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
// Automation may be denied while a tab still exists. Keep the record so a
|
|
127
|
+
// later `den down` can retry closing it.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (changed) writeTabsFile(filePath, _tabRegistry);
|
|
131
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants — single source of truth for values used across modules.
|
|
3
|
+
*
|
|
4
|
+
* If a value appears in 2+ files, it belongs here.
|
|
5
|
+
* If it's specific to one module (e.g. a Playwright config timeout), keep it local.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { envKey } from "./product.js";
|
|
9
|
+
// ── 1Password strategy (only used when secrets.provider: 1password) ──────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Default 1Password account domain used by every `op` invocation across den.
|
|
13
|
+
* Kept in sync with the `OP_ACCOUNT ?= …` default in the Makefile. Scripts that
|
|
14
|
+
* may run outside `make` (setup-1password, auth-configure)
|
|
15
|
+
* set `process.env.OP_ACCOUNT ||= OP_ACCOUNT_DEFAULT` at entry so op picks up
|
|
16
|
+
* the right account even when invoked directly via `npx tsx`.
|
|
17
|
+
*/
|
|
18
|
+
export const OP_ACCOUNT_DEFAULT = "example.1password.com";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default vault for shared test accounts. Deprecated: the vault is org data —
|
|
22
|
+
* read it with testAccountsVault() (src/shared/auth/test-accounts.ts), which
|
|
23
|
+
* honours `test_accounts.vault` in the org config and falls back to this.
|
|
24
|
+
*/
|
|
25
|
+
export const OP_TEST_VAULT = "test-accounts";
|
|
26
|
+
|
|
27
|
+
/** Vault containing per-engineer API keys (1Password's built-in personal vault). */
|
|
28
|
+
export const OP_API_KEYS_VAULT = "Employee";
|
|
29
|
+
|
|
30
|
+
/** Item within OP_API_KEYS_VAULT holding the developer's API keys. */
|
|
31
|
+
export const OP_DEV_KEYS_ITEM = "Developer API Keys";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Field within OP_DEV_KEYS_ITEM where the user's local admin (sudo) password
|
|
35
|
+
* is optionally stored. Populated when the user checks "Save to 1Password" on
|
|
36
|
+
* the den local-dev unlock banner; read on server startup so subsequent
|
|
37
|
+
* sessions unlock without prompting.
|
|
38
|
+
*/
|
|
39
|
+
export const OP_DEN_SUDO_FIELD = `${envKey("LOCAL_ADMIN_PWD")}`;
|
|
40
|
+
|
|
41
|
+
// ── Timeouts ─────────────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/** Health check HTTP/shell ping timeout. */
|
|
44
|
+
export const HEALTH_CHECK_TIMEOUT_MS = 3_000;
|
|
45
|
+
/** Hosted environments (beta, prod) may be scaled to zero; a cold start takes seconds. */
|
|
46
|
+
export const HOSTED_HEALTH_CHECK_TIMEOUT_MS = 10_000;
|
|
47
|
+
|
|
48
|
+
/** Timeout for spawning short-lived CLI subprocesses (open, sudo, git). */
|
|
49
|
+
export const CLI_SPAWN_TIMEOUT_MS = 5_000;
|
|
50
|
+
|
|
51
|
+
/** Timeout for `gh api` calls (branch listing, repo search). */
|
|
52
|
+
export const GH_API_TIMEOUT_MS = 15_000;
|
|
53
|
+
|
|
54
|
+
/** Timeout for 1Password `op` CLI commands.
|
|
55
|
+
* Generous so macOS Touch ID biometric prompts have time to land
|
|
56
|
+
* (≤10s killed the loadtest seed mid-mint when the prompt took >10s). */
|
|
57
|
+
export const OP_CLI_TIMEOUT_MS = 60_000;
|
|
58
|
+
|
|
59
|
+
/** Git remote HEAD check timeout (fast operation, low timeout). */
|
|
60
|
+
export const GIT_REMOTE_TIMEOUT_MS = 2_000;
|
|
61
|
+
|
|
62
|
+
/** Time to wait for a daemon to produce initial output before retrying. */
|
|
63
|
+
export const DAEMON_QUICK_EXIT_MS = 3_000;
|
|
64
|
+
|
|
65
|
+
/** Delay between daemon log stream retries. */
|
|
66
|
+
export const DAEMON_RETRY_DELAY_MS = 5_000;
|
|
67
|
+
|
|
68
|
+
/** Lines of immediate context a live log stream starts with (deeper history
|
|
69
|
+
* is paged on demand via /api/service/history). */
|
|
70
|
+
export const LIVE_TAIL_LINES = 500;
|
|
71
|
+
|
|
72
|
+
/** Window of time to show detached process output before closing the stream. */
|
|
73
|
+
export const DETACH_WINDOW_MS = 5_000;
|
|
74
|
+
|
|
75
|
+
/** Max time to wait for a `depends_on: { wait: true }` dependency to become healthy before starting the dependent. */
|
|
76
|
+
export const READINESS_TIMEOUT_MS = 60_000;
|
|
77
|
+
|
|
78
|
+
/** Poll interval while waiting for a `wait: true` dependency to become healthy. */
|
|
79
|
+
export const READINESS_INTERVAL_MS = 1_000;
|
|
80
|
+
|
|
81
|
+
/** Daemon graceful shutdown timeout. */
|
|
82
|
+
export const DAEMON_SHUTDOWN_GRACE_MS = 5_000;
|
|
83
|
+
|
|
84
|
+
// ── Cache TTLs ───────────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/** TTL for cached GitHub branch listings. */
|
|
87
|
+
export const BRANCH_CACHE_TTL_MS = 60_000;
|
|
88
|
+
|
|
89
|
+
/** TTL for cached GitHub repo/org listings. */
|
|
90
|
+
export const REPO_CACHE_TTL_MS = 5 * 60_000;
|
|
91
|
+
|
|
92
|
+
// ── Log rotation ─────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Size at which a service log is rotated. Service logs are written by DETACHED
|
|
96
|
+
* children holding an inherited O_APPEND fd, so a chatty one grows without
|
|
97
|
+
* bound — a localstack tail was measured at 8.6 GB.
|
|
98
|
+
*/
|
|
99
|
+
export const MAX_LOG_BYTES = 128 * 1024 * 1024;
|
|
100
|
+
|
|
101
|
+
/** How much of the tail is preserved into `<name>.log.1` when rotating. */
|
|
102
|
+
export const ROTATED_KEEP_BYTES = 32 * 1024 * 1024;
|
|
103
|
+
|
|
104
|
+
/** How often the server sweeps the log tree for oversized files. */
|
|
105
|
+
export const LOG_SWEEP_INTERVAL_MS = 15 * 60_000;
|
|
106
|
+
|
|
107
|
+
// ── Test file patterns ───────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
/** File extension patterns per test pillar. */
|
|
110
|
+
export const TEST_FILE_EXTENSIONS: Record<string, string[]> = {
|
|
111
|
+
smoke: [".smoke.ts"],
|
|
112
|
+
synthetic: [".spec.ts", ".test.ts"],
|
|
113
|
+
integration: [".spec.ts", ".test.ts"],
|
|
114
|
+
performance: [".js"],
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/** Directories to skip when scanning for test modules. */
|
|
118
|
+
export const SKIP_TEST_DIRS = [
|
|
119
|
+
"user-flows",
|
|
120
|
+
"helpers",
|
|
121
|
+
"fixtures",
|
|
122
|
+
"utils",
|
|
123
|
+
] as const;
|
|
124
|
+
|
|
125
|
+
// ── Server ───────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
/** Default den web server port. */
|
|
128
|
+
export const DEFAULT_PORT = 7777;
|