@trackunit/iris-app-playwright 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +230 -0
  2. package/generators.json +12 -0
  3. package/package.json +43 -0
  4. package/src/fixtures/baseTest.d.ts +9 -0
  5. package/src/fixtures/baseTest.js +12 -0
  6. package/src/fixtures/describe.d.ts +11 -0
  7. package/src/fixtures/describe.js +23 -0
  8. package/src/fixtures/featureFlags.fixture.d.ts +19 -0
  9. package/src/fixtures/featureFlags.fixture.js +76 -0
  10. package/src/fixtures/harRecording.fixture.d.ts +12 -0
  11. package/src/fixtures/harRecording.fixture.js +35 -0
  12. package/src/fixtures/hydrateStorageState.d.ts +36 -0
  13. package/src/fixtures/hydrateStorageState.js +43 -0
  14. package/src/fixtures/irisApp.fixture.d.ts +15 -0
  15. package/src/fixtures/irisApp.fixture.js +13 -0
  16. package/src/fixtures/localDevMode.fixture.d.ts +5 -0
  17. package/src/fixtures/localDevMode.fixture.js +17 -0
  18. package/src/fixtures/login.fixture.d.ts +24 -0
  19. package/src/fixtures/login.fixture.js +95 -0
  20. package/src/fixtures/loginCacheKey.d.ts +25 -0
  21. package/src/fixtures/loginCacheKey.js +29 -0
  22. package/src/fixtures/resolveCredentials.d.ts +14 -0
  23. package/src/fixtures/resolveCredentials.js +49 -0
  24. package/src/generators/playwright-configuration/files/root/playwright/fixtures/auth.json__tmpl__ +4 -0
  25. package/src/generators/playwright-configuration/files/root/playwright/support/fixtures.ts__tmpl__ +1 -0
  26. package/src/generators/playwright-configuration/files/root/playwright/tests/app.spec.ts__tmpl__ +16 -0
  27. package/src/generators/playwright-configuration/files/root/playwright/tsconfig.json__tmpl__ +15 -0
  28. package/src/generators/playwright-configuration/files/root/playwright.config.ts__tmpl__ +13 -0
  29. package/src/generators/playwright-configuration/generator.d.ts +18 -0
  30. package/src/generators/playwright-configuration/generator.js +77 -0
  31. package/src/generators/playwright-configuration/schema.d.ts +3 -0
  32. package/src/generators/playwright-configuration/schema.json +18 -0
  33. package/src/index.d.ts +15 -0
  34. package/src/index.js +14 -0
  35. package/src/plugins/createLogFile.d.ts +24 -0
  36. package/src/plugins/createLogFile.js +51 -0
  37. package/src/plugins/defaultPlaywrightConfig.d.ts +68 -0
  38. package/src/plugins/defaultPlaywrightConfig.js +150 -0
  39. package/src/plugins/logsReporter.d.ts +55 -0
  40. package/src/plugins/logsReporter.js +119 -0
  41. package/src/plugins/nxPreset.d.ts +9 -0
  42. package/src/plugins/nxPreset.js +17 -0
  43. package/src/plugins/redactHar.d.ts +17 -0
  44. package/src/plugins/redactHar.js +138 -0
  45. package/src/plugins/redactSensitive.d.ts +31 -0
  46. package/src/plugins/redactSensitive.js +72 -0
  47. package/src/plugins/redactTrace.d.ts +12 -0
  48. package/src/plugins/redactTrace.js +48 -0
  49. package/src/plugins/setupPlugins.d.ts +11 -0
  50. package/src/plugins/setupPlugins.js +68 -0
  51. package/src/plugins/writeFileWithPrettier.d.ts +17 -0
  52. package/src/plugins/writeFileWithPrettier.js +37 -0
  53. package/src/support.d.ts +2 -0
  54. package/src/support.js +6 -0
  55. package/src/utils/Codeowner.d.ts +26 -0
  56. package/src/utils/Codeowner.js +89 -0
  57. package/src/utils/fileNameBuilder.d.ts +18 -0
  58. package/src/utils/fileNameBuilder.js +32 -0
  59. package/src/utils/fileUpdater.d.ts +16 -0
  60. package/src/utils/fileUpdater.js +38 -0
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultPlaywrightConfig = void 0;
4
+ exports.findWorkspaceRoot = findWorkspaceRoot;
5
+ const tslib_1 = require("tslib");
6
+ const crypto_1 = tslib_1.__importDefault(require("crypto"));
7
+ const fs_1 = require("fs");
8
+ const path_1 = tslib_1.__importDefault(require("path"));
9
+ const Codeowner_1 = require("../utils/Codeowner");
10
+ /**
11
+ * Finds the NX workspace root by walking up the directory tree looking for nx.json or workspace.json.
12
+ *
13
+ * @param startDir - Starting directory for the search (defaults to current working directory)
14
+ * @returns {string} Absolute path to the workspace root
15
+ * @throws Error if workspace root cannot be found
16
+ */
17
+ function findWorkspaceRoot(startDir = process.cwd()) {
18
+ let currentDir = startDir;
19
+ while (currentDir !== path_1.default.dirname(currentDir)) {
20
+ if ((0, fs_1.existsSync)(path_1.default.join(currentDir, "nx.json")) || (0, fs_1.existsSync)(path_1.default.join(currentDir, "workspace.json"))) {
21
+ return currentDir;
22
+ }
23
+ currentDir = path_1.default.dirname(currentDir);
24
+ }
25
+ throw new Error("Could not find NX workspace root (nx.json or workspace.json not found)");
26
+ }
27
+ // Memoized at module scope so multiple `defaultPlaywrightConfig()` calls within
28
+ // a single process (Playwright loads the config twice — once for discovery, once
29
+ // for the run) share the same ids. Otherwise the junit reporter would write to
30
+ // two different filenames and `metadata.runId` would diverge between the two
31
+ // loads.
32
+ const runId = crypto_1.default.randomUUID();
33
+ const junitFileSuffix = crypto_1.default.randomUUID();
34
+ /**
35
+ * Default cap when running against a shared, rate-limited environment (dev,
36
+ * stage, prod, feature branches). Picked deliberately: it stays under Okta's
37
+ * `/api/v1/authn` burst threshold (typically ~5/s) for the initial parallel
38
+ * login fan-out, while still letting most suites finish in roughly half the
39
+ * wall-clock of a fully serial run.
40
+ */
41
+ const SHARED_ENV_WORKER_CAP = 2;
42
+ /**
43
+ * Recognises whether a `BASE_URL` value points at a developer's own machine
44
+ * (Vite dev server, in-cluster preview, etc.) vs a shared, rate-limited
45
+ * environment. We don't try to enumerate every dev pattern — just the few
46
+ * that genuinely don't share an Okta tenant with other workers running
47
+ * elsewhere. Anything else (including unset) is treated conservatively.
48
+ */
49
+ const isSharedEnvironmentBaseURL = (baseURL) => {
50
+ if (baseURL === undefined || baseURL === "") {
51
+ return false;
52
+ }
53
+ try {
54
+ // Node's URL parser wraps IPv6 hostnames in brackets (e.g. `"[::1]"`).
55
+ // Strip them so the IPv6 loopback check below works regardless.
56
+ const hostname = new URL(baseURL).hostname.toLowerCase().replace(/^\[|\]$/g, "");
57
+ if (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "0.0.0.0" || hostname === "::1") {
58
+ return false;
59
+ }
60
+ if (hostname.endsWith(".local") || hostname.endsWith(".localhost")) {
61
+ return false;
62
+ }
63
+ return true;
64
+ }
65
+ catch {
66
+ // Malformed URLs slip through as "not shared" — Playwright will fail
67
+ // loudly on its own URL parsing later, and we'd rather not cap workers
68
+ // based on a value we don't understand.
69
+ return false;
70
+ }
71
+ };
72
+ /**
73
+ * Creates default Playwright configuration for E2E testing.
74
+ * Returns a partial `PlaywrightTestConfig` that projects can spread and extend.
75
+ *
76
+ * @param options - Optional configuration overrides
77
+ * @returns {Partial<PlaywrightTestConfig>} Playwright test configuration
78
+ */
79
+ const defaultPlaywrightConfig = (options = {}) => {
80
+ const { configDir, nxRoot: providedNxRoot, outputDirOverride, projectConfig = {}, behaviorConfig = {}, pluginConfig = {}, } = options;
81
+ const callerDir = configDir ?? process.cwd();
82
+ const nxRoot = providedNxRoot ?? findWorkspaceRoot(callerDir);
83
+ const relativePath = path_1.default.relative(nxRoot, callerDir);
84
+ const dotsToNxRoot = relativePath
85
+ .split(path_1.default.sep)
86
+ .map(_ => "..")
87
+ .join("/");
88
+ // Playwright owns its own output-dir env var, decoupled from the
89
+ // Cypress-owned NX_E2E_OUTPUT_DIR. On CI this points at
90
+ // `./dist/playwright/executor-N` so Playwright artifacts land in a
91
+ // first-class `dist/playwright` tree (never inside `dist/cypress`).
92
+ const envOutputDirOverride = process.env.NX_PLAYWRIGHT_OUTPUT_DIR || outputDirOverride;
93
+ const buildOutputPath = (subPath) => {
94
+ if (envOutputDirOverride) {
95
+ return path_1.default.join(dotsToNxRoot, envOutputDirOverride, subPath);
96
+ }
97
+ return `${dotsToNxRoot}/dist/playwright/${relativePath}/${subPath}`;
98
+ };
99
+ const codeowner = (0, Codeowner_1.toShortTeamName)((0, Codeowner_1.getCodeowner)(callerDir, nxRoot));
100
+ // Only set workers / fullyParallel when we have a concrete value. Spreading
101
+ // `undefined` would still create the key on the returned config, which
102
+ // Playwright treats as "the user said `undefined`" — surprising semantics
103
+ // we'd rather avoid by leaving the key absent altogether.
104
+ //
105
+ // `workers` resolution order:
106
+ // 1. Explicit `behaviorConfig.workers` always wins.
107
+ // 2. Otherwise, when running against a shared environment (BASE_URL set
108
+ // and not pointing at localhost) we default to `SHARED_ENV_WORKER_CAP`.
109
+ // The `login` fixture's per-worker cache collapses subsequent Okta
110
+ // calls within a worker, but the first call on each worker still races
111
+ // at suite startup — and Okta's `/api/v1/authn` returns `E0000047`
112
+ // ("rate limit") once you fan out past a handful of parallel logins.
113
+ // Two workers stays comfortably under that threshold while preserving
114
+ // ~2x wall-clock speedup over fully serial execution.
115
+ // 3. Local/feature-branch runs (`BASE_URL` unset or localhost) keep
116
+ // Playwright's default — those environments aren't rate-limited.
117
+ const baseURL = process.env.NX_FEATURE_BRANCH_BASE_URL ?? process.env.BASE_URL;
118
+ const optional = {};
119
+ if (behaviorConfig.workers !== undefined) {
120
+ optional.workers = behaviorConfig.workers;
121
+ }
122
+ else if (isSharedEnvironmentBaseURL(baseURL)) {
123
+ optional.workers = SHARED_ENV_WORKER_CAP;
124
+ }
125
+ if (behaviorConfig.fullyParallel !== undefined) {
126
+ optional.fullyParallel = behaviorConfig.fullyParallel;
127
+ }
128
+ return {
129
+ timeout: behaviorConfig.timeout ?? 30000,
130
+ retries: behaviorConfig.retries ?? 3,
131
+ use: {
132
+ baseURL: process.env.NX_FEATURE_BRANCH_BASE_URL ?? process.env.BASE_URL ?? undefined,
133
+ navigationTimeout: behaviorConfig.navigationTimeout ?? 35000,
134
+ actionTimeout: behaviorConfig.actionTimeout ?? 20000,
135
+ screenshot: "only-on-failure",
136
+ video: "retain-on-failure",
137
+ trace: "retain-on-failure",
138
+ },
139
+ outputDir: pluginConfig.outputPath ?? buildOutputPath("test-results"),
140
+ reporter: [["junit", { outputFile: buildOutputPath(`results-${junitFileSuffix}.xml`) }]],
141
+ testDir: projectConfig.testDir ?? "./src/e2e",
142
+ metadata: {
143
+ codeowner,
144
+ runId,
145
+ },
146
+ ...optional,
147
+ };
148
+ };
149
+ exports.defaultPlaywrightConfig = defaultPlaywrightConfig;
150
+ //# sourceMappingURL=defaultPlaywrightConfig.js.map
@@ -0,0 +1,55 @@
1
+ import type { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from "@playwright/test/reporter";
2
+ import type { Formatter } from "./writeFileWithPrettier";
3
+ export interface LogsReporterOptions {
4
+ /** Directory where log files are written on test failure */
5
+ logsFolder: string;
6
+ /** Directory where HAR files are moved on test failure (temp HAR deleted on pass) */
7
+ harsFolder?: string;
8
+ /** Workspace root used by createLogFile to locate .prettierrc */
9
+ nxRoot: string;
10
+ /** Prettier formatter instance */
11
+ formatter: Formatter;
12
+ }
13
+ /**
14
+ * Custom Playwright reporter that writes redacted log files on test failure
15
+ * and manages HAR file cleanup (retain on failure, delete on pass).
16
+ */
17
+ export declare class LogsReporter implements Reporter {
18
+ private readonly logsFolder;
19
+ private readonly harsFolder;
20
+ private readonly nxRoot;
21
+ private readonly formatter;
22
+ /**
23
+ * Promises of in-flight log file writes from `onTestEnd`. `createLogFile`
24
+ * formats with Prettier (async) and writes via `writeFileSync`; Playwright
25
+ * may finish the run before those formats settle, so we track them here and
26
+ * await them in `onEnd` to surface formatter errors and ensure files exist
27
+ * by the time the process exits.
28
+ */
29
+ private readonly pendingWrites;
30
+ /** @param options - Reporter configuration options */
31
+ constructor(options: LogsReporterOptions);
32
+ /** Creates the logs folder before the run begins. */
33
+ onBegin(_config: FullConfig, _suite: Suite): void;
34
+ /** Writes a redacted log file on test failure and manages HAR file retention. */
35
+ onTestEnd(test: TestCase, result: TestResult): void;
36
+ /**
37
+ * Awaits in-flight log writes started in `onTestEnd` before the reporter
38
+ * signals completion. Without this, Playwright may exit before Prettier
39
+ * settles, leaving partial or missing log files and swallowing formatter
40
+ * errors.
41
+ */
42
+ onEnd(_result: FullResult): Promise<void>;
43
+ /** Returns false so Playwright adds its own terminal reporter alongside this one. */
44
+ printsToStdio(): boolean;
45
+ /**
46
+ * Returns the HAR attachment path for *this specific result*. Looking at
47
+ * `test.results.flatMap(r => r.attachments)` would mix attachments across
48
+ * retries — on a retry, the first attempt's HAR could be retained instead
49
+ * of (or alongside) the current attempt's HAR. Per-result scoping keeps
50
+ * each attempt's artifacts paired with its own log file.
51
+ */
52
+ private getResultHarPath;
53
+ private getResultTracePath;
54
+ }
55
+ export default LogsReporter;
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LogsReporter = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs_1 = require("fs");
6
+ const path = tslib_1.__importStar(require("path"));
7
+ const fileNameBuilder_1 = require("../utils/fileNameBuilder");
8
+ const createLogFile_1 = require("./createLogFile");
9
+ const redactHar_1 = require("./redactHar");
10
+ const redactSensitive_1 = require("./redactSensitive");
11
+ const redactTrace_1 = require("./redactTrace");
12
+ /**
13
+ * Custom Playwright reporter that writes redacted log files on test failure
14
+ * and manages HAR file cleanup (retain on failure, delete on pass).
15
+ */
16
+ class LogsReporter {
17
+ /** @param options - Reporter configuration options */
18
+ constructor(options) {
19
+ /**
20
+ * Promises of in-flight log file writes from `onTestEnd`. `createLogFile`
21
+ * formats with Prettier (async) and writes via `writeFileSync`; Playwright
22
+ * may finish the run before those formats settle, so we track them here and
23
+ * await them in `onEnd` to surface formatter errors and ensure files exist
24
+ * by the time the process exits.
25
+ */
26
+ this.pendingWrites = [];
27
+ this.logsFolder = options.logsFolder;
28
+ this.harsFolder = options.harsFolder;
29
+ this.nxRoot = options.nxRoot;
30
+ this.formatter = options.formatter;
31
+ }
32
+ /** Creates the logs folder before the run begins. */
33
+ onBegin(_config, _suite) {
34
+ if (!(0, fs_1.existsSync)(this.logsFolder)) {
35
+ (0, fs_1.mkdirSync)(this.logsFolder, { recursive: true });
36
+ }
37
+ }
38
+ /** Writes a redacted log file on test failure and manages HAR file retention. */
39
+ onTestEnd(test, result) {
40
+ const titlePath = test.titlePath().filter(t => t.length > 0);
41
+ const describeTitle = titlePath.slice(0, -1).join(" > ");
42
+ const testTitle = test.title;
43
+ const fullTitle = describeTitle ? `${describeTitle} - ${testTitle}` : testTitle;
44
+ if (result.status === "failed" || result.status === "timedOut") {
45
+ const fileName = (0, fileNameBuilder_1.fileNameBuilder)(fullTitle, "failed", result.retry);
46
+ const logs = result.stdout.concat(result.stderr).map(entry => {
47
+ const message = typeof entry === "string" ? entry : entry.toString();
48
+ return {
49
+ type: "stdout",
50
+ message: (0, redactSensitive_1.redactSensitive)(message),
51
+ severity: result.status === "failed" ? "error" : "warning",
52
+ };
53
+ });
54
+ this.pendingWrites.push((0, createLogFile_1.createLogFile)(this.nxRoot, this.logsFolder, fileName, logs, this.formatter));
55
+ if (this.harsFolder !== undefined) {
56
+ const tempHarPath = this.getResultHarPath(result);
57
+ if (tempHarPath !== undefined && (0, fs_1.existsSync)(tempHarPath)) {
58
+ if (!(0, fs_1.existsSync)(this.harsFolder)) {
59
+ (0, fs_1.mkdirSync)(this.harsFolder, { recursive: true });
60
+ }
61
+ const harFileName = `${(0, fileNameBuilder_1.fileNameBuilder)(fullTitle, "failed", result.retry)}.har`;
62
+ // HARs include Authorization, Cookie, and request/response bodies for
63
+ // authenticated traffic. Redact before moving to the shared artifacts folder.
64
+ (0, redactHar_1.redactHarFile)(tempHarPath, path.join(this.harsFolder, harFileName));
65
+ (0, fs_1.rmSync)(tempHarPath);
66
+ }
67
+ }
68
+ // Defense in depth for trace artifacts: `login()`'s slow path does
69
+ // `page.goto("/auth/manager-classic#session_token=...")`, and Playwright
70
+ // records the full `goto` URL as the action argument. With
71
+ // `trace: "retain-on-failure"` from `defaultPlaywrightConfig`, a live
72
+ // Okta session token would otherwise persist in the retained trace zip
73
+ // alongside the test artifacts. The fragment is client-side only — it
74
+ // never reaches the HTTP server and HAR/log redaction already covers
75
+ // everything that does — so this pass scrubs the remaining surface.
76
+ const tracePath = this.getResultTracePath(result);
77
+ if (tracePath !== undefined && (0, fs_1.existsSync)(tracePath)) {
78
+ this.pendingWrites.push((0, redactTrace_1.redactTraceFile)(tracePath));
79
+ }
80
+ }
81
+ else if (this.harsFolder !== undefined) {
82
+ const tempHarPath = this.getResultHarPath(result);
83
+ if (tempHarPath !== undefined && (0, fs_1.existsSync)(tempHarPath)) {
84
+ (0, fs_1.rmSync)(tempHarPath);
85
+ }
86
+ }
87
+ }
88
+ /**
89
+ * Awaits in-flight log writes started in `onTestEnd` before the reporter
90
+ * signals completion. Without this, Playwright may exit before Prettier
91
+ * settles, leaving partial or missing log files and swallowing formatter
92
+ * errors.
93
+ */
94
+ async onEnd(_result) {
95
+ await Promise.all(this.pendingWrites);
96
+ }
97
+ /** Returns false so Playwright adds its own terminal reporter alongside this one. */
98
+ printsToStdio() {
99
+ return false;
100
+ }
101
+ /**
102
+ * Returns the HAR attachment path for *this specific result*. Looking at
103
+ * `test.results.flatMap(r => r.attachments)` would mix attachments across
104
+ * retries — on a retry, the first attempt's HAR could be retained instead
105
+ * of (or alongside) the current attempt's HAR. Per-result scoping keeps
106
+ * each attempt's artifacts paired with its own log file.
107
+ */
108
+ getResultHarPath(result) {
109
+ const attachment = result.attachments.find(a => a.name === "har" && a.path !== undefined);
110
+ return attachment?.path;
111
+ }
112
+ getResultTracePath(result) {
113
+ const attachment = result.attachments.find(a => a.name === "trace" && a.path !== undefined);
114
+ return attachment?.path;
115
+ }
116
+ }
117
+ exports.LogsReporter = LogsReporter;
118
+ exports.default = LogsReporter;
119
+ //# sourceMappingURL=logsReporter.js.map
@@ -0,0 +1,9 @@
1
+ import { nxE2EPreset } from "@nx/playwright/preset";
2
+ /**
3
+ * Creates the standard NX E2E preset configuration for Playwright.
4
+ * Wraps the `@nx/playwright` preset with Trackunit defaults.
5
+ *
6
+ * @param filename - Pass `__filename` from the calling `playwright.config.ts`
7
+ * @returns {ReturnType<typeof nxE2EPreset>} NX Playwright preset configuration
8
+ */
9
+ export declare function createNxPreset(filename: string): ReturnType<typeof nxE2EPreset>;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createNxPreset = createNxPreset;
4
+ const preset_1 = require("@nx/playwright/preset");
5
+ /**
6
+ * Creates the standard NX E2E preset configuration for Playwright.
7
+ * Wraps the `@nx/playwright` preset with Trackunit defaults.
8
+ *
9
+ * @param filename - Pass `__filename` from the calling `playwright.config.ts`
10
+ * @returns {ReturnType<typeof nxE2EPreset>} NX Playwright preset configuration
11
+ */
12
+ function createNxPreset(filename) {
13
+ return (0, preset_1.nxE2EPreset)(filename, {
14
+ testDir: "./src",
15
+ });
16
+ }
17
+ //# sourceMappingURL=nxPreset.js.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Strips sensitive headers, cookies, query parameters, and bodies from a HAR document.
3
+ * Returns a new object — the input is not mutated.
4
+ *
5
+ * Redacts:
6
+ * - Headers matching SENSITIVE_HEADER_NAMES (Authorization, Cookie, Set-Cookie, etc.)
7
+ * - All cookie values (request & response)
8
+ * - Query-string parameters with sensitive names
9
+ * - Form params with sensitive names
10
+ * - JSON-like text in request/response bodies via redactSensitive
11
+ */
12
+ export declare const redactHar: (har: unknown) => unknown;
13
+ /**
14
+ * Reads a HAR file, redacts sensitive content, and writes it to a new path.
15
+ * The source file is unchanged. Returns the bytes written.
16
+ */
17
+ export declare const redactHarFile: (sourcePath: string, destPath: string) => void;
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.redactHarFile = exports.redactHar = void 0;
4
+ const fs_1 = require("fs");
5
+ const redactSensitive_1 = require("./redactSensitive");
6
+ const SENSITIVE_HEADER_NAMES = new Set([...redactSensitive_1.SENSITIVE_KEYS, "authorization", "cookie", "set-cookie", "x-api-key", "x-auth-token", "proxy-authorization"].map(n => n.toLowerCase()));
7
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
8
+ const redactHeaderArray = (headers) => {
9
+ if (!Array.isArray(headers))
10
+ return headers;
11
+ return headers.map((h) => {
12
+ if (!isRecord(h))
13
+ return h;
14
+ const name = typeof h.name === "string" ? h.name : "";
15
+ if (SENSITIVE_HEADER_NAMES.has(name.toLowerCase())) {
16
+ return { ...h, value: "***" };
17
+ }
18
+ return h;
19
+ });
20
+ };
21
+ const redactCookieArray = (cookies) => {
22
+ if (!Array.isArray(cookies))
23
+ return cookies;
24
+ return cookies.map((c) => (isRecord(c) ? { ...c, value: "***" } : c));
25
+ };
26
+ const redactQueryString = (qs) => {
27
+ if (!Array.isArray(qs))
28
+ return qs;
29
+ return qs.map(entry => {
30
+ if (!isRecord(entry))
31
+ return entry;
32
+ const name = typeof entry.name === "string" ? entry.name : "";
33
+ if (SENSITIVE_HEADER_NAMES.has(name.toLowerCase())) {
34
+ return { ...entry, value: "***" };
35
+ }
36
+ return entry;
37
+ });
38
+ };
39
+ const redactStringField = (value) => (typeof value === "string" ? (0, redactSensitive_1.redactSensitive)(value) : value);
40
+ const redactPostData = (postData) => {
41
+ if (!isRecord(postData))
42
+ return postData;
43
+ const next = { ...postData };
44
+ if (typeof postData.text === "string") {
45
+ next.text = (0, redactSensitive_1.redactSensitive)(postData.text);
46
+ }
47
+ if (Array.isArray(postData.params)) {
48
+ next.params = postData.params.map(p => {
49
+ if (!isRecord(p))
50
+ return p;
51
+ const name = typeof p.name === "string" ? p.name : "";
52
+ if (SENSITIVE_HEADER_NAMES.has(name.toLowerCase())) {
53
+ return { ...p, value: "***" };
54
+ }
55
+ return p;
56
+ });
57
+ }
58
+ return next;
59
+ };
60
+ const redactContent = (content) => {
61
+ if (!isRecord(content))
62
+ return content;
63
+ if (typeof content.text === "string") {
64
+ return { ...content, text: (0, redactSensitive_1.redactSensitive)(content.text) };
65
+ }
66
+ return content;
67
+ };
68
+ const redactRequest = (request) => {
69
+ if (!isRecord(request))
70
+ return request;
71
+ const next = { ...request };
72
+ next.url = redactStringField(request.url);
73
+ next.headers = redactHeaderArray(request.headers);
74
+ next.cookies = redactCookieArray(request.cookies);
75
+ next.queryString = redactQueryString(request.queryString);
76
+ next.postData = redactPostData(request.postData);
77
+ return next;
78
+ };
79
+ const redactResponse = (response) => {
80
+ if (!isRecord(response))
81
+ return response;
82
+ const next = { ...response };
83
+ next.url = redactStringField(response.url);
84
+ next.headers = redactHeaderArray(response.headers);
85
+ next.cookies = redactCookieArray(response.cookies);
86
+ next.content = redactContent(response.content);
87
+ return next;
88
+ };
89
+ /**
90
+ * Strips sensitive headers, cookies, query parameters, and bodies from a HAR document.
91
+ * Returns a new object — the input is not mutated.
92
+ *
93
+ * Redacts:
94
+ * - Headers matching SENSITIVE_HEADER_NAMES (Authorization, Cookie, Set-Cookie, etc.)
95
+ * - All cookie values (request & response)
96
+ * - Query-string parameters with sensitive names
97
+ * - Form params with sensitive names
98
+ * - JSON-like text in request/response bodies via redactSensitive
99
+ */
100
+ const redactHar = (har) => {
101
+ if (!isRecord(har))
102
+ return har;
103
+ const log = har.log;
104
+ if (!isRecord(log))
105
+ return har;
106
+ const entries = log.entries;
107
+ if (!Array.isArray(entries))
108
+ return har;
109
+ const redactedEntries = entries.map(entry => {
110
+ if (!isRecord(entry))
111
+ return entry;
112
+ return {
113
+ ...entry,
114
+ request: redactRequest(entry.request),
115
+ response: redactResponse(entry.response),
116
+ };
117
+ });
118
+ return {
119
+ ...har,
120
+ log: {
121
+ ...log,
122
+ entries: redactedEntries,
123
+ },
124
+ };
125
+ };
126
+ exports.redactHar = redactHar;
127
+ /**
128
+ * Reads a HAR file, redacts sensitive content, and writes it to a new path.
129
+ * The source file is unchanged. Returns the bytes written.
130
+ */
131
+ const redactHarFile = (sourcePath, destPath) => {
132
+ const raw = (0, fs_1.readFileSync)(sourcePath, "utf-8");
133
+ const parsed = JSON.parse(raw);
134
+ const redacted = (0, exports.redactHar)(parsed);
135
+ (0, fs_1.writeFileSync)(destPath, JSON.stringify(redacted));
136
+ };
137
+ exports.redactHarFile = redactHarFile;
138
+ //# sourceMappingURL=redactHar.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Keys whose values must be redacted when they appear as JSON properties
3
+ * (`"key": "value"`) or URL-encoded params (`key=value`). Both camelCase and
4
+ * snake_case variants are listed because Okta uses `session_token` in its
5
+ * redirect URL while the rest of the codebase uses `sessionToken`.
6
+ */
7
+ export declare const SENSITIVE_KEYS: string[];
8
+ /**
9
+ * HTTP header names whose values must be redacted when they appear in
10
+ * `Header-Name: value` form (e.g. log lines printed by HTTP clients, request
11
+ * dumps in test output). Header redaction is line-scoped because header
12
+ * values can contain `=` and `:` characters that would otherwise confuse the
13
+ * URL/JSON patterns.
14
+ */
15
+ export declare const SENSITIVE_HEADERS: string[];
16
+ /**
17
+ * Redacts sensitive key-value pairs from a string in JSON, escaped-JSON,
18
+ * URL-encoded, and `Header: value` formats.
19
+ *
20
+ * Replaces:
21
+ * - `"key": "<anything>"` → `"key": "***"` (case-insensitive)
22
+ * - `\"key\": \"<anything>\"` → `\"key\": \"***\"` (case-insensitive — JSON
23
+ * string embedded inside another JSON string, e.g. a request body field in
24
+ * a Playwright trace entry)
25
+ * - `key=<anything>` → `key=***` (URL-encoded, value terminates at `&`/whitespace/`"`)
26
+ * - `Header-Name: <rest-of-line>` → `Header-Name: ***` (case-insensitive, line-scoped)
27
+ *
28
+ * @param message - The string to redact sensitive values from
29
+ * @returns {string} The string with sensitive values replaced by `***`
30
+ */
31
+ export declare const redactSensitive: (message: string) => string;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.redactSensitive = exports.SENSITIVE_HEADERS = exports.SENSITIVE_KEYS = void 0;
4
+ /**
5
+ * Keys whose values must be redacted when they appear as JSON properties
6
+ * (`"key": "value"`) or URL-encoded params (`key=value`). Both camelCase and
7
+ * snake_case variants are listed because Okta uses `session_token` in its
8
+ * redirect URL while the rest of the codebase uses `sessionToken`.
9
+ */
10
+ exports.SENSITIVE_KEYS = [
11
+ "password",
12
+ "sessionToken",
13
+ "session_token",
14
+ "accessToken",
15
+ "access_token",
16
+ "refreshToken",
17
+ "refresh_token",
18
+ "idToken",
19
+ "id_token",
20
+ "authorization",
21
+ "apiKey",
22
+ "api_key",
23
+ ];
24
+ /**
25
+ * HTTP header names whose values must be redacted when they appear in
26
+ * `Header-Name: value` form (e.g. log lines printed by HTTP clients, request
27
+ * dumps in test output). Header redaction is line-scoped because header
28
+ * values can contain `=` and `:` characters that would otherwise confuse the
29
+ * URL/JSON patterns.
30
+ */
31
+ exports.SENSITIVE_HEADERS = [
32
+ "authorization",
33
+ "cookie",
34
+ "set-cookie",
35
+ "x-api-key",
36
+ "x-auth-token",
37
+ "proxy-authorization",
38
+ ];
39
+ /**
40
+ * Redacts sensitive key-value pairs from a string in JSON, escaped-JSON,
41
+ * URL-encoded, and `Header: value` formats.
42
+ *
43
+ * Replaces:
44
+ * - `"key": "<anything>"` → `"key": "***"` (case-insensitive)
45
+ * - `\"key\": \"<anything>\"` → `\"key\": \"***\"` (case-insensitive — JSON
46
+ * string embedded inside another JSON string, e.g. a request body field in
47
+ * a Playwright trace entry)
48
+ * - `key=<anything>` → `key=***` (URL-encoded, value terminates at `&`/whitespace/`"`)
49
+ * - `Header-Name: <rest-of-line>` → `Header-Name: ***` (case-insensitive, line-scoped)
50
+ *
51
+ * @param message - The string to redact sensitive values from
52
+ * @returns {string} The string with sensitive values replaced by `***`
53
+ */
54
+ const redactSensitive = (message) => {
55
+ let redacted = message;
56
+ for (const key of exports.SENSITIVE_KEYS) {
57
+ // Direct JSON: "key": "value"
58
+ redacted = redacted.replace(new RegExp(`("${key}"\\s*:\\s*)"[^"]*"`, "gi"), `$1"***"`);
59
+ // Escaped JSON (one level): \"key\": \"value\" — appears when a JSON
60
+ // object is serialized as a string field of another JSON object, which
61
+ // is exactly what Playwright traces store for request/response bodies.
62
+ redacted = redacted.replace(new RegExp(`(\\\\"${key}\\\\"\\s*:\\s*)\\\\"[^"\\\\]*\\\\"`, "gi"), `$1\\"***\\"`);
63
+ // URL-encoded: key=value (value terminates at &, whitespace, or quote)
64
+ redacted = redacted.replace(new RegExp(`(${key}=)[^&\\s"]+`, "gi"), "$1***");
65
+ }
66
+ for (const header of exports.SENSITIVE_HEADERS) {
67
+ redacted = redacted.replace(new RegExp(`(^|[\\r\\n])(${header}\\s*:\\s*)[^\\r\\n]*`, "gim"), "$1$2***");
68
+ }
69
+ return redacted;
70
+ };
71
+ exports.redactSensitive = redactSensitive;
72
+ //# sourceMappingURL=redactSensitive.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Rewrites a Playwright trace.zip in place with sensitive values stripped from
3
+ * the action/network metadata. The `login()` fixture issues a `page.goto`
4
+ * against `/auth/manager-classic#session_token=...`; Playwright records the
5
+ * full URL as the action argument, so without this pass a retained trace zip
6
+ * would carry a live Okta session token alongside the test artifacts.
7
+ *
8
+ * The fragment is client-side only — it never reaches the HTTP server, and
9
+ * HAR/log redaction already covers everything that does — so this pass is the
10
+ * defense-in-depth layer that scrubs the remaining surface.
11
+ */
12
+ export declare const redactTraceFile: (filePath: string) => Promise<void>;