@monster-orange/den 0.0.0 → 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 +165 -1
- 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 +99 -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,416 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API JWT Token Manager
|
|
3
|
+
*
|
|
4
|
+
* Mints access tokens for direct API calls through the environment's token
|
|
5
|
+
* provider (auth0 password grant, cognito, local-jwt — see
|
|
6
|
+
* ./token-providers). Tokens are cached to disk and reused until 5 minutes
|
|
7
|
+
* before expiry.
|
|
8
|
+
*
|
|
9
|
+
* Prerequisites when the provider is auth0 (one-time Auth0 Dashboard setup):
|
|
10
|
+
* 1. Applications → [App] → Settings → Advanced Settings → Grant Types
|
|
11
|
+
* → enable "Password"
|
|
12
|
+
* 2. Auth0 Dashboard → Settings → API Authorization Settings
|
|
13
|
+
* → Default Directory = "Username-Password-Authentication"
|
|
14
|
+
*
|
|
15
|
+
* Credential sources (same priority as headless browser login):
|
|
16
|
+
* 1. ENV vars: E2E_ADMIN_EMAIL / E2E_ADMIN_PASSWORD (CI only)
|
|
17
|
+
* 2. Secrets backend (Doppler / 1Password / env — src/shared/secrets),
|
|
18
|
+
* at the accounts named by `test_accounts` in the org config
|
|
19
|
+
*
|
|
20
|
+
* Note: social-login (e.g. Google) accounts cannot be used — the password
|
|
21
|
+
* grant requires email+password.
|
|
22
|
+
*
|
|
23
|
+
* Cache location: playwright/.auth/api-token-{role}-{env}.json (gitignored)
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import * as fs from "fs";
|
|
27
|
+
import * as path from "path";
|
|
28
|
+
import { getCredentials } from "./headless-login.js";
|
|
29
|
+
import { mintAccessToken } from "./token-providers/index.js";
|
|
30
|
+
import type { Environment } from "../types/index.js";
|
|
31
|
+
import { getDistRoot } from "../paths.js";
|
|
32
|
+
import { envSelector } from "../environment/env-selector.js";
|
|
33
|
+
|
|
34
|
+
import { product } from "../product.js";
|
|
35
|
+
// Anchored to the den root, NOT process.cwd(): token caches are WRITTEN
|
|
36
|
+
// here, and `den test ...` can be invoked from any directory (bin/den
|
|
37
|
+
// never cds; spawned playwright children inherit the user's cwd).
|
|
38
|
+
const AUTH_DIR = path.resolve(getDistRoot(), "playwright/.auth");
|
|
39
|
+
const EXPIRY_BUFFER_MS = 5 * 60 * 1000; // refresh 5 min before actual expiry
|
|
40
|
+
|
|
41
|
+
// Per-environment Auth0 client secrets (gitignored).
|
|
42
|
+
// Keys are environment names; values are client_secret strings.
|
|
43
|
+
// Created by: echo '{"staging":"<secret>"}' > config/local/auth0-client-secrets.json
|
|
44
|
+
const AUTH0_SECRETS_FILE = path.resolve(
|
|
45
|
+
getDistRoot(),
|
|
46
|
+
"config/local/auth0-client-secrets.json",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
function loadAuth0ClientSecret(environment: string): string | undefined {
|
|
50
|
+
try {
|
|
51
|
+
if (!fs.existsSync(AUTH0_SECRETS_FILE)) return undefined;
|
|
52
|
+
const secrets = JSON.parse(
|
|
53
|
+
fs.readFileSync(AUTH0_SECRETS_FILE, "utf-8"),
|
|
54
|
+
) as Record<string, string>;
|
|
55
|
+
return secrets[environment] ?? undefined;
|
|
56
|
+
} catch {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse a .env / .env.local file and return key→value pairs.
|
|
63
|
+
* Handles quoted values, inline comments, and blank/comment lines.
|
|
64
|
+
*/
|
|
65
|
+
function parseDotEnv(filePath: string): Record<string, string> {
|
|
66
|
+
try {
|
|
67
|
+
if (!fs.existsSync(filePath)) return {};
|
|
68
|
+
const result: Record<string, string> = {};
|
|
69
|
+
for (const line of fs.readFileSync(filePath, "utf-8").split("\n")) {
|
|
70
|
+
const trimmed = line.trim();
|
|
71
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
72
|
+
const eqIdx = trimmed.indexOf("=");
|
|
73
|
+
if (eqIdx === -1) continue;
|
|
74
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
75
|
+
const value = trimmed
|
|
76
|
+
.slice(eqIdx + 1)
|
|
77
|
+
.trim()
|
|
78
|
+
.replace(/^["']|["']$/g, ""); // strip surrounding quotes
|
|
79
|
+
result[key] = value;
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
} catch {
|
|
83
|
+
return {};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface CachedToken {
|
|
88
|
+
accessToken: string;
|
|
89
|
+
tokenType: string;
|
|
90
|
+
expiresAt: number; // epoch ms
|
|
91
|
+
role: string;
|
|
92
|
+
email: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function cachePath(
|
|
96
|
+
role: "admin" | "user" | "beta" | "root_admin",
|
|
97
|
+
environment: string,
|
|
98
|
+
): string {
|
|
99
|
+
return path.join(AUTH_DIR, `api-token-${role}-${environment}.json`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function loadCache(filePath: string): CachedToken | null {
|
|
103
|
+
try {
|
|
104
|
+
if (!fs.existsSync(filePath)) return null;
|
|
105
|
+
const data = JSON.parse(fs.readFileSync(filePath, "utf-8")) as CachedToken;
|
|
106
|
+
if (Date.now() >= data.expiresAt - EXPIRY_BUFFER_MS) return null; // expired / near-expiry
|
|
107
|
+
return data;
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function saveCache(filePath: string, token: CachedToken): void {
|
|
114
|
+
// Cached JWTs are secrets — keep the dir and file owner-only (0700/0600).
|
|
115
|
+
if (!fs.existsSync(AUTH_DIR))
|
|
116
|
+
fs.mkdirSync(AUTH_DIR, { recursive: true, mode: 0o700 });
|
|
117
|
+
fs.writeFileSync(filePath, JSON.stringify(token, null, 2), { mode: 0o600 });
|
|
118
|
+
fs.chmodSync(filePath, 0o600); // mode arg is ignored if the file already exists
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The dotenv file named by the environment config's
|
|
123
|
+
* `auth0.clientSecretEnvFile` (relative to the den root, e.g. an app
|
|
124
|
+
* checkout's `.env.local`), or undefined when none is configured.
|
|
125
|
+
*/
|
|
126
|
+
async function clientSecretEnvFile(
|
|
127
|
+
environment: Environment,
|
|
128
|
+
): Promise<string | undefined> {
|
|
129
|
+
try {
|
|
130
|
+
const cfg = await envSelector.loadEnvironment(environment);
|
|
131
|
+
const file = cfg.auth0?.clientSecretEnvFile?.trim();
|
|
132
|
+
return file ? path.resolve(getDistRoot(), file) : undefined;
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Resolve the Auth0 client secret from the file-based local-dev fallbacks
|
|
140
|
+
* (config/local/auth0-client-secrets.json keyed by environment, else
|
|
141
|
+
* AUTH0_CLIENT_SECRET in the configured `auth0.clientSecretEnvFile`), with
|
|
142
|
+
* a label naming where it came from. Pure read — never mutates process.env.
|
|
143
|
+
*/
|
|
144
|
+
async function resolveFileClientSecret(
|
|
145
|
+
environment: Environment,
|
|
146
|
+
): Promise<{ secret: string; source: string } | undefined> {
|
|
147
|
+
const fromJson = loadAuth0ClientSecret(environment);
|
|
148
|
+
if (fromJson) {
|
|
149
|
+
return {
|
|
150
|
+
secret: fromJson,
|
|
151
|
+
source: `config/local/auth0-client-secrets.json (${environment})`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const envFile = await clientSecretEnvFile(environment);
|
|
155
|
+
const fromEnvFile = envFile
|
|
156
|
+
? parseDotEnv(envFile)["AUTH0_CLIENT_SECRET"]
|
|
157
|
+
: undefined;
|
|
158
|
+
return fromEnvFile && envFile
|
|
159
|
+
? { secret: fromEnvFile, source: envFile }
|
|
160
|
+
: undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Wrap `mintAuth0JWT` with operator-friendly error hints for the well-known
|
|
165
|
+
* Auth0 misconfigurations hit when first enabling the password grant.
|
|
166
|
+
* Keeps the elaborate setup guidance attached to the shared test-user flow,
|
|
167
|
+
* while the loadtest per-user flow uses the bare `mintAuth0JWT` (its operators
|
|
168
|
+
* are already familiar with the setup).
|
|
169
|
+
*/
|
|
170
|
+
async function fetchRopcToken(
|
|
171
|
+
environment: Environment,
|
|
172
|
+
email: string,
|
|
173
|
+
password: string,
|
|
174
|
+
options: { clientSecret?: string } = {},
|
|
175
|
+
): Promise<{ access_token: string; token_type: string; expires_in: number }> {
|
|
176
|
+
try {
|
|
177
|
+
const minted = await mintAccessToken(environment, email, password, options);
|
|
178
|
+
return {
|
|
179
|
+
access_token: minted.accessToken,
|
|
180
|
+
token_type: minted.tokenType,
|
|
181
|
+
expires_in: minted.expiresIn,
|
|
182
|
+
};
|
|
183
|
+
} catch (err) {
|
|
184
|
+
const msg = (err as Error).message ?? String(err);
|
|
185
|
+
const isGrantError =
|
|
186
|
+
msg.includes("grant type") ||
|
|
187
|
+
msg.includes("Password grant") ||
|
|
188
|
+
msg.includes("unauthorized_client");
|
|
189
|
+
|
|
190
|
+
if (isGrantError) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`Auth0 ROPC not enabled: ${msg}\n\n` +
|
|
193
|
+
` To enable the Password grant:\n` +
|
|
194
|
+
` 1. Auth0 Dashboard → Applications → [your app]\n` +
|
|
195
|
+
` 2. Settings → Advanced Settings → Grant Types → check "Password"\n` +
|
|
196
|
+
` 3. Auth0 Dashboard → Settings → API Authorization Settings\n` +
|
|
197
|
+
` → Default Directory = "Username-Password-Authentication"\n` +
|
|
198
|
+
` 4. Save and retry.\n`,
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const isDefaultDirectoryMissing =
|
|
203
|
+
msg.includes("access_denied") && msg.includes("Unauthorized");
|
|
204
|
+
|
|
205
|
+
if (isDefaultDirectoryMissing) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Auth0 ROPC failed: Password grant is enabled but "Default Directory" is not set.\n\n` +
|
|
208
|
+
` Fix (one-time Auth0 Dashboard change):\n` +
|
|
209
|
+
` Auth0 Dashboard → Settings → API Authorization Settings\n` +
|
|
210
|
+
` → Default Directory = "Username-Password-Authentication"\n` +
|
|
211
|
+
` → Save Changes\n\n` +
|
|
212
|
+
` Note: This is separate from the Grant Types setting on the Application.`,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const isCredentialError =
|
|
217
|
+
msg.includes("invalid_grant") ||
|
|
218
|
+
msg.includes("Wrong email") ||
|
|
219
|
+
msg.includes("Wrong password") ||
|
|
220
|
+
msg.includes("user not found");
|
|
221
|
+
|
|
222
|
+
if (isCredentialError) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Auth0 login failed for "${email}": ${msg}\n` +
|
|
225
|
+
` Check the test-account secret in your secrets backend (make ${product().cli}-setup-secrets)`,
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get a JWT access token for the given role, suitable for
|
|
235
|
+
* Authorization: Bearer <token> headers when calling the app's API directly.
|
|
236
|
+
*
|
|
237
|
+
* Checks disk cache first. Fetches a fresh token via ROPC only when the
|
|
238
|
+
* cache is missing or within 5 minutes of expiry.
|
|
239
|
+
*
|
|
240
|
+
* @throws if credentials are unavailable or Auth0 ROPC fails
|
|
241
|
+
*/
|
|
242
|
+
export async function getApiToken(
|
|
243
|
+
role: "admin" | "user" | "beta" | "root_admin",
|
|
244
|
+
environment: Environment,
|
|
245
|
+
): Promise<string> {
|
|
246
|
+
const cache = cachePath(role, environment);
|
|
247
|
+
|
|
248
|
+
// Return cached token if still valid
|
|
249
|
+
const cached = loadCache(cache);
|
|
250
|
+
if (cached) {
|
|
251
|
+
const minsLeft = Math.round((cached.expiresAt - Date.now()) / 60000);
|
|
252
|
+
console.log(
|
|
253
|
+
` ✓ Using cached API token for ${role} (${minsLeft}m remaining)`,
|
|
254
|
+
);
|
|
255
|
+
return cached.accessToken;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
console.log(` Fetching API token for ${role}...`);
|
|
259
|
+
|
|
260
|
+
// Load credentials (ENV vars or credentials file)
|
|
261
|
+
const creds = await getCredentials(role, environment);
|
|
262
|
+
if (!creds) {
|
|
263
|
+
const hint =
|
|
264
|
+
role === "admin" || role === "root_admin"
|
|
265
|
+
? ` • Local ${role}: set test_accounts.roles.${role} in the org config, or run make auth-configure for a personal account\n`
|
|
266
|
+
: ` • Local ${role}: set test_accounts.roles.${role} in the org config and store its password in your secrets backend\n`;
|
|
267
|
+
throw new Error(
|
|
268
|
+
`No credentials available for ${role} API token.\n` +
|
|
269
|
+
` Options:\n` +
|
|
270
|
+
` • CI: set E2E_${role.toUpperCase()}_EMAIL / E2E_${role.toUpperCase()}_PASSWORD\n` +
|
|
271
|
+
` • Local: ensure the secrets backend is signed in (make ${product().cli}-setup-secrets)\n` +
|
|
272
|
+
hint +
|
|
273
|
+
` Note: social-login accounts cannot be used for a password grant — email+password only.`,
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Client secret priority (surfaced via E2E_AUTH0_CLIENT_SECRET env var):
|
|
278
|
+
// 1. E2E_AUTH0_CLIENT_SECRET env var (CI) — passed through unchanged
|
|
279
|
+
// 2. config/local/auth0-client-secrets.json keyed by environment (gitignored)
|
|
280
|
+
// 3. AUTH0_CLIENT_SECRET in the environment's auth0.clientSecretEnvFile
|
|
281
|
+
//
|
|
282
|
+
// mintAuth0JWT reads E2E_AUTH0_CLIENT_SECRET, so we set it here from the
|
|
283
|
+
// file-based fallbacks if not already in the env.
|
|
284
|
+
if (!process.env.E2E_AUTH0_CLIENT_SECRET) {
|
|
285
|
+
const fromFile = await resolveFileClientSecret(environment);
|
|
286
|
+
if (fromFile) {
|
|
287
|
+
console.log(` Using client_secret from ${fromFile.source}`);
|
|
288
|
+
process.env.E2E_AUTH0_CLIENT_SECRET = fromFile.secret;
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
console.log(` Using client_secret from E2E_AUTH0_CLIENT_SECRET`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const data = await fetchRopcToken(environment, creds.email, creds.password);
|
|
295
|
+
|
|
296
|
+
const expiresAt = Date.now() + data.expires_in * 1000;
|
|
297
|
+
const token: CachedToken = {
|
|
298
|
+
accessToken: data.access_token,
|
|
299
|
+
tokenType: data.token_type,
|
|
300
|
+
expiresAt,
|
|
301
|
+
role,
|
|
302
|
+
email: creds.email,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
saveCache(cache, token);
|
|
306
|
+
|
|
307
|
+
const minsValid = Math.round(data.expires_in / 60);
|
|
308
|
+
console.log(
|
|
309
|
+
` ✓ API token fetched for ${role} (${creds.email}), valid for ${minsValid}m`,
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
return data.access_token;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Injectable collaborators for {@link mintFreshApiToken} (unit tests). */
|
|
316
|
+
export interface MintFreshApiTokenDeps {
|
|
317
|
+
getCredentials?: typeof getCredentials;
|
|
318
|
+
mintJwt?: typeof mintAccessToken;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Mint a fresh JWT for the given role, MEMORY-ONLY: unlike
|
|
323
|
+
* {@link getApiToken}, this path never reads or writes the disk cache
|
|
324
|
+
* under playwright/.auth and never mutates process.env — the Auth0 client
|
|
325
|
+
* secret (env var first, else the file-based local-dev fallbacks) is
|
|
326
|
+
* passed explicitly into the mint. Built for the journeys runner, whose
|
|
327
|
+
* captured secrets must not outlive the run in any persistent store.
|
|
328
|
+
*
|
|
329
|
+
* @throws if credentials are unavailable or Auth0 ROPC fails
|
|
330
|
+
*/
|
|
331
|
+
export async function mintFreshApiToken(
|
|
332
|
+
role: "admin" | "user" | "beta" | "root_admin",
|
|
333
|
+
environment: Environment,
|
|
334
|
+
deps: MintFreshApiTokenDeps = {},
|
|
335
|
+
): Promise<string> {
|
|
336
|
+
const resolveCredentials = deps.getCredentials ?? getCredentials;
|
|
337
|
+
const creds = await resolveCredentials(role, environment);
|
|
338
|
+
if (!creds) {
|
|
339
|
+
throw new Error(
|
|
340
|
+
`No credentials available for ${role} API token.\n` +
|
|
341
|
+
` Options:\n` +
|
|
342
|
+
` • CI: set E2E_${role.toUpperCase()}_EMAIL / E2E_${role.toUpperCase()}_PASSWORD\n` +
|
|
343
|
+
` • Local: ensure the secrets backend is signed in (make ${product().cli}-setup-secrets)\n` +
|
|
344
|
+
` Note: social-login accounts cannot be used for a password grant — email+password only.`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const clientSecret =
|
|
349
|
+
process.env.E2E_AUTH0_CLIENT_SECRET ??
|
|
350
|
+
(await resolveFileClientSecret(environment))?.secret;
|
|
351
|
+
|
|
352
|
+
if (deps.mintJwt) {
|
|
353
|
+
const minted = await deps.mintJwt(
|
|
354
|
+
environment,
|
|
355
|
+
creds.email,
|
|
356
|
+
creds.password,
|
|
357
|
+
{
|
|
358
|
+
...(clientSecret !== undefined ? { clientSecret } : {}),
|
|
359
|
+
},
|
|
360
|
+
);
|
|
361
|
+
return minted.accessToken;
|
|
362
|
+
}
|
|
363
|
+
const data = await fetchRopcToken(
|
|
364
|
+
environment,
|
|
365
|
+
creds.email,
|
|
366
|
+
creds.password,
|
|
367
|
+
clientSecret !== undefined ? { clientSecret } : {},
|
|
368
|
+
);
|
|
369
|
+
return data.access_token;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Delete the cached API token for a role/environment, forcing a fresh
|
|
374
|
+
* ROPC fetch on the next call to getApiToken().
|
|
375
|
+
*/
|
|
376
|
+
export function clearApiTokenCache(
|
|
377
|
+
role: "admin" | "user" | "beta" | "root_admin",
|
|
378
|
+
environment: Environment,
|
|
379
|
+
): void {
|
|
380
|
+
const file = cachePath(role, environment);
|
|
381
|
+
if (fs.existsSync(file)) {
|
|
382
|
+
fs.unlinkSync(file);
|
|
383
|
+
console.log(` ✓ API token cache cleared: ${role}/${environment}`);
|
|
384
|
+
} else {
|
|
385
|
+
console.log(` (no cached token for ${role}/${environment})`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Return status info about the cached token for a role/environment.
|
|
391
|
+
* Used by make int-jwt-status.
|
|
392
|
+
*/
|
|
393
|
+
export function getApiTokenCacheStatus(
|
|
394
|
+
role: "admin" | "user" | "beta" | "root_admin",
|
|
395
|
+
environment: string,
|
|
396
|
+
): {
|
|
397
|
+
found: boolean;
|
|
398
|
+
email?: string;
|
|
399
|
+
expiresAt?: number;
|
|
400
|
+
minsRemaining?: number;
|
|
401
|
+
} {
|
|
402
|
+
const file = cachePath(role, environment as Environment);
|
|
403
|
+
try {
|
|
404
|
+
if (!fs.existsSync(file)) return { found: false };
|
|
405
|
+
const data = JSON.parse(fs.readFileSync(file, "utf-8")) as CachedToken;
|
|
406
|
+
const minsRemaining = Math.round((data.expiresAt - Date.now()) / 60000);
|
|
407
|
+
return {
|
|
408
|
+
found: true,
|
|
409
|
+
email: data.email,
|
|
410
|
+
expiresAt: data.expiresAt,
|
|
411
|
+
minsRemaining,
|
|
412
|
+
};
|
|
413
|
+
} catch {
|
|
414
|
+
return { found: false };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth0 Helper
|
|
3
|
+
* Auth0-specific authentication for Next.js frontend testing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { AuthenticationClient } from "auth0";
|
|
7
|
+
import type { Page } from "@playwright/test";
|
|
8
|
+
import type { TokenSet } from "../types/index.js";
|
|
9
|
+
|
|
10
|
+
export interface Auth0Config {
|
|
11
|
+
domain: string;
|
|
12
|
+
clientId: string;
|
|
13
|
+
clientSecret?: string;
|
|
14
|
+
audience?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class Auth0Helper {
|
|
18
|
+
private auth0Client: AuthenticationClient;
|
|
19
|
+
private config: Auth0Config;
|
|
20
|
+
|
|
21
|
+
constructor(config: Auth0Config) {
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.auth0Client = new AuthenticationClient({
|
|
24
|
+
domain: config.domain,
|
|
25
|
+
clientId: config.clientId,
|
|
26
|
+
clientSecret: config.clientSecret,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Login with username and password (Resource Owner Password Grant)
|
|
32
|
+
* For API testing and automated flows
|
|
33
|
+
*/
|
|
34
|
+
async loginWithPassword(
|
|
35
|
+
username: string,
|
|
36
|
+
password: string,
|
|
37
|
+
): Promise<TokenSet> {
|
|
38
|
+
try {
|
|
39
|
+
const result = await this.auth0Client.oauth.passwordGrant({
|
|
40
|
+
username,
|
|
41
|
+
password,
|
|
42
|
+
realm: "Username-Password-Authentication",
|
|
43
|
+
scope: "openid profile email",
|
|
44
|
+
audience: this.config.audience,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const data = result.data as unknown as Record<string, unknown>;
|
|
48
|
+
return {
|
|
49
|
+
access_token: data.access_token as string,
|
|
50
|
+
refresh_token: data.refresh_token as string | undefined,
|
|
51
|
+
id_token: data.id_token as string | undefined,
|
|
52
|
+
expires_in: data.expires_in as number | undefined,
|
|
53
|
+
token_type: data.token_type as string | undefined,
|
|
54
|
+
scope: data.scope as string | undefined,
|
|
55
|
+
};
|
|
56
|
+
} catch (error: unknown) {
|
|
57
|
+
const msg =
|
|
58
|
+
error instanceof Error ? error.message : JSON.stringify(error);
|
|
59
|
+
throw new Error(`Auth0 password login failed: ${msg}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Complete Auth0 Universal Login flow in browser (for Playwright tests)
|
|
65
|
+
* Handles the Auth0 login page specifically
|
|
66
|
+
* @istanbul ignore next - Browser automation tested via E2E tests
|
|
67
|
+
*/
|
|
68
|
+
async completeAuth0LoginInBrowser(
|
|
69
|
+
page: Page,
|
|
70
|
+
username: string,
|
|
71
|
+
password: string,
|
|
72
|
+
options: {
|
|
73
|
+
waitForUrl?: string | RegExp;
|
|
74
|
+
timeout?: number;
|
|
75
|
+
} = {},
|
|
76
|
+
): Promise<void> {
|
|
77
|
+
const { waitForUrl = /.*\/dashboard.*/, timeout = 15000 } = options;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
// Wait for Auth0 Universal Login page to load
|
|
81
|
+
// Auth0 typically shows email/username first
|
|
82
|
+
await page.waitForSelector(
|
|
83
|
+
'input[name="username"], input[type="email"]',
|
|
84
|
+
{
|
|
85
|
+
timeout,
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
// Fill in email/username
|
|
90
|
+
const usernameInput = page
|
|
91
|
+
.locator('input[name="username"], input[type="email"]')
|
|
92
|
+
.first();
|
|
93
|
+
await usernameInput.fill(username);
|
|
94
|
+
|
|
95
|
+
// Auth0 Universal Login may have a continue button before password
|
|
96
|
+
const continueButton = page
|
|
97
|
+
.locator('button[type="submit"], button[name="action"]')
|
|
98
|
+
.first();
|
|
99
|
+
if (
|
|
100
|
+
await continueButton.isVisible({ timeout: 1000 }).catch(() => false)
|
|
101
|
+
) {
|
|
102
|
+
await continueButton.click();
|
|
103
|
+
|
|
104
|
+
// Wait for password field to appear
|
|
105
|
+
await page.waitForSelector(
|
|
106
|
+
'input[name="password"], input[type="password"]',
|
|
107
|
+
{
|
|
108
|
+
timeout: 5000,
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Fill in password
|
|
114
|
+
const passwordInput = page
|
|
115
|
+
.locator('input[name="password"], input[type="password"]')
|
|
116
|
+
.first();
|
|
117
|
+
await passwordInput.fill(password);
|
|
118
|
+
|
|
119
|
+
// Submit the form
|
|
120
|
+
const submitButton = page
|
|
121
|
+
.locator('button[type="submit"], button[name="action"]')
|
|
122
|
+
.first();
|
|
123
|
+
await submitButton.click();
|
|
124
|
+
|
|
125
|
+
// Wait for redirect back to application
|
|
126
|
+
await page.waitForURL(waitForUrl, { timeout });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
// Take screenshot for debugging
|
|
129
|
+
const screenshot = await page
|
|
130
|
+
.screenshot({ fullPage: true })
|
|
131
|
+
.catch(() => null);
|
|
132
|
+
if (screenshot) {
|
|
133
|
+
console.error("Login failed, screenshot saved");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
throw new Error(
|
|
137
|
+
`Auth0 browser login failed: ${(error as Error).message}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Refresh access token using refresh token
|
|
144
|
+
*/
|
|
145
|
+
async refreshToken(refreshToken: string): Promise<TokenSet> {
|
|
146
|
+
try {
|
|
147
|
+
const result = await this.auth0Client.oauth.refreshTokenGrant({
|
|
148
|
+
refresh_token: refreshToken,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const data = result.data as unknown as Record<string, unknown>;
|
|
152
|
+
return {
|
|
153
|
+
access_token: data.access_token as string,
|
|
154
|
+
refresh_token: data.refresh_token as string | undefined,
|
|
155
|
+
id_token: data.id_token as string | undefined,
|
|
156
|
+
expires_in: data.expires_in as number | undefined,
|
|
157
|
+
token_type: data.token_type as string | undefined,
|
|
158
|
+
scope: data.scope as string | undefined,
|
|
159
|
+
};
|
|
160
|
+
} catch (error: unknown) {
|
|
161
|
+
const msg =
|
|
162
|
+
error instanceof Error ? error.message : JSON.stringify(error);
|
|
163
|
+
throw new Error(`Auth0 token refresh failed: ${msg}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Get user info from Auth0
|
|
169
|
+
* Note: Uses the /userinfo endpoint directly
|
|
170
|
+
*/
|
|
171
|
+
async getUserInfo(accessToken: string): Promise<Record<string, unknown>> {
|
|
172
|
+
try {
|
|
173
|
+
const response = await fetch(`https://${this.config.domain}/userinfo`, {
|
|
174
|
+
headers: {
|
|
175
|
+
Authorization: `Bearer ${accessToken}`,
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!response.ok) {
|
|
180
|
+
throw new Error(`Failed to get user info: ${response.status}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (await response.json()) as Record<string, unknown>;
|
|
184
|
+
} catch (error: unknown) {
|
|
185
|
+
const msg =
|
|
186
|
+
error instanceof Error ? error.message : JSON.stringify(error);
|
|
187
|
+
throw new Error(`Failed to get user info: ${msg}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Client credentials grant (for machine-to-machine authentication)
|
|
193
|
+
*/
|
|
194
|
+
async clientCredentialsGrant(audience?: string): Promise<TokenSet> {
|
|
195
|
+
if (!this.config.clientSecret) {
|
|
196
|
+
throw new Error("Client secret is required for client credentials grant");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const result = await this.auth0Client.oauth.clientCredentialsGrant({
|
|
201
|
+
audience:
|
|
202
|
+
audience ||
|
|
203
|
+
this.config.audience ||
|
|
204
|
+
`https://${this.config.domain}/api/v2/`,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
const data = result.data as unknown as Record<string, unknown>;
|
|
208
|
+
return {
|
|
209
|
+
access_token: data.access_token as string,
|
|
210
|
+
expires_in: data.expires_in as number | undefined,
|
|
211
|
+
token_type: data.token_type as string | undefined,
|
|
212
|
+
scope: data.scope as string | undefined,
|
|
213
|
+
};
|
|
214
|
+
} catch (error: unknown) {
|
|
215
|
+
const msg =
|
|
216
|
+
error instanceof Error ? error.message : JSON.stringify(error);
|
|
217
|
+
throw new Error(`Auth0 client credentials grant failed: ${msg}`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Wait for Auth0 session in browser
|
|
223
|
+
* Useful for checking if user is already logged in
|
|
224
|
+
*/
|
|
225
|
+
async waitForAuth0Session(page: Page): Promise<boolean> {
|
|
226
|
+
try {
|
|
227
|
+
// Check for Auth0 session cookie or localStorage
|
|
228
|
+
const hasSession = await page.evaluate(() => {
|
|
229
|
+
// @ts-expect-error - window is available in browser context
|
|
230
|
+
// Check localStorage for Auth0 session (Next.js with @auth0/nextjs-auth0)
|
|
231
|
+
const auth0Keys = Object.keys(window.localStorage).filter(
|
|
232
|
+
(key: string) =>
|
|
233
|
+
key.includes("auth0") || key.includes("@@auth0spajs@@"),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
if (auth0Keys.length > 0) {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// @ts-expect-error - window is available in browser context
|
|
241
|
+
// Check for session cookie
|
|
242
|
+
const cookies = window.document.cookie.split(";");
|
|
243
|
+
return cookies.some((cookie: string) =>
|
|
244
|
+
cookie.trim().startsWith("appSession="),
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return hasSession;
|
|
249
|
+
} catch {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Clear Auth0 session from browser
|
|
256
|
+
* Useful for test cleanup
|
|
257
|
+
*/
|
|
258
|
+
async clearAuth0Session(page: Page): Promise<void> {
|
|
259
|
+
try {
|
|
260
|
+
await page.evaluate(() => {
|
|
261
|
+
// @ts-expect-error - window is available in browser context
|
|
262
|
+
// Clear localStorage
|
|
263
|
+
const auth0Keys = Object.keys(window.localStorage).filter(
|
|
264
|
+
(key: string) =>
|
|
265
|
+
key.includes("auth0") || key.includes("@@auth0spajs@@"),
|
|
266
|
+
);
|
|
267
|
+
// @ts-expect-error - window is available in browser context
|
|
268
|
+
auth0Keys.forEach((key: string) => window.localStorage.removeItem(key));
|
|
269
|
+
|
|
270
|
+
// @ts-expect-error - window is available in browser context
|
|
271
|
+
// Clear cookies
|
|
272
|
+
window.document.cookie.split(";").forEach((cookie: string) => {
|
|
273
|
+
const [name] = cookie.split("=");
|
|
274
|
+
// @ts-expect-error - window is available in browser context
|
|
275
|
+
window.document.cookie = `${name.trim()}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.warn(
|
|
280
|
+
`Failed to clear Auth0 session: ${(error as Error).message}`,
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|