test-proxy-recorder 0.3.8 → 0.4.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/README.md CHANGED
@@ -37,6 +37,8 @@ Both can be used together or independently.
37
37
  - [Full-stack (SSR + browser) Quick Start](#full-stack-ssr--browser-quick-start)
38
38
  - [Browser-only / SPA / Extension Quick Start](#browser-only--spa--extension-quick-start)
39
39
  - [CLI](#cli)
40
+ - [Config file](#config-file)
41
+ - [Secret redaction](#secret-redaction)
40
42
  - [Example Apps](#example-apps)
41
43
  - [Playwright Integration](#playwright-integration)
42
44
  - [Next.js Integration](#nextjs-integration)
@@ -221,11 +223,15 @@ CI now runs without any network access.
221
223
  test-proxy-recorder <target-url> [options]
222
224
  ```
223
225
 
224
- | Option | Default | Description |
225
- | -------------- | -------------- | ----------------------------- |
226
- | `<target-url>` | *(required)* | Backend URL to proxy |
227
- | `--port, -p` | `8080` | Proxy listen port |
228
- | `--dir, -d` | `./recordings` | Directory for recording files |
226
+ | Option | Default | Description |
227
+ | ---------------- | -------------- | ----------------------------------- |
228
+ | `<target-url>` | *(required)* | Backend URL to proxy |
229
+ | `--port, -p` | `8000` | Proxy listen port |
230
+ | `--dir, -d` | `./recordings` | Directory for recording files |
231
+ | `--timeout, -t` | `120000` | Session auto-reset timeout (ms) |
232
+ | `--config, -c` | *(auto)* | Path to a config file (see below) |
233
+
234
+ Secrets are redacted from recordings by default — see [Secret redaction](#secret-redaction) for the `--no-redact`, `--redact-headers`, and `--redact-body` flags.
229
235
 
230
236
  ```bash
231
237
  # Examples
@@ -233,6 +239,141 @@ test-proxy-recorder http://localhost:8000
233
239
  test-proxy-recorder http://localhost:8000 --port 8100 --dir ./mocks
234
240
  ```
235
241
 
242
+ ### Scaffold the setup (`init`)
243
+
244
+ One command wires test-proxy-recorder into a project:
245
+
246
+ ```bash
247
+ npx test-proxy-recorder init http://localhost:3002 --port 8100 --dir ./e2e/recordings
248
+ ```
249
+
250
+ It generates / edits, **non-destructively**:
251
+
252
+ - `test-proxy-recorder.config.ts` — the proxy config (auto-discovered, so
253
+ `npx test-proxy-recorder` then needs no flags).
254
+ - `playwright.config.ts` — adds a `webServer` pointing at the proxy's
255
+ `/__control` endpoint plus a `globalTeardown`. If you already have a Playwright
256
+ config it's **edited in place**; if you don't have Playwright at all, `init`
257
+ runs the Playwright CLI to set it up first (pass `--no-install` to skip).
258
+ - `e2e/fixtures.ts` and `e2e/global-teardown.ts` — the per-test proxy fixture and
259
+ teardown.
260
+ - `package.json` — adds `proxy`, `test:e2e`, and `test:e2e:record` scripts. If
261
+ you have a `dev` script it's wrapped: the original moves to `dev:app` and `dev`
262
+ becomes a `concurrently` command that runs the proxy alongside your app (so
263
+ `npm run dev` records while you develop). `concurrently` is added to
264
+ `devDependencies`.
265
+
266
+ All arguments are optional and fall back to sensible defaults
267
+ (`http://localhost:3000`, port `8100`, `./e2e/recordings`). Existing files and
268
+ scripts are never overwritten unless you pass `--force`; a Playwright config that
269
+ already defines a `webServer` is left untouched (with a note on what to add).
270
+
271
+ The **one step it can't do for you** is routing your app's backend calls through
272
+ the proxy — which env var holds your API base URL, and how you scope it to dev,
273
+ is app-specific. `init` prints concrete instructions for this when it finishes:
274
+ point that env var at `http://localhost:8100` **in dev/test only, never in
275
+ production** (e.g. prefix the `dev:app` script, using `cross-env` on Windows).
276
+ The proxy then forwards to your real backend while recording and serves
277
+ recordings on replay.
278
+
279
+ ### Config file
280
+
281
+ For anything beyond a couple of flags — especially body-redaction regexes — put the
282
+ options in a config file instead. The proxy auto-discovers
283
+ `test-proxy-recorder.config.{ts,js,mjs,cjs}` in the current directory, or pass
284
+ `--config <path>` to point at one explicitly. `.ts` files work out of the box.
285
+
286
+ ```ts
287
+ // test-proxy-recorder.config.ts
288
+ import { defineConfig } from 'test-proxy-recorder';
289
+
290
+ export default defineConfig({
291
+ target: 'http://localhost:3002',
292
+ port: 8100,
293
+ recordingsDir: './e2e/recordings',
294
+ timeout: 120_000,
295
+ redaction: {
296
+ headers: ['x-api-key'], // extra headers, merged with the defaults
297
+ bodyPatterns: [/sk_live_\w+/g], // real RegExp literals — no CLI escaping
298
+ allowCookies: ['theme'], // keep these cookies unredacted
299
+ },
300
+ });
301
+ ```
302
+
303
+ ```bash
304
+ test-proxy-recorder # all options from the config file
305
+ test-proxy-recorder --port 9000 # config file, but CLI port wins
306
+ ```
307
+
308
+ **Precedence:** every option resolves as **CLI flag → config file → built-in default**.
309
+ A flag you pass on the command line always overrides the config file; anything you
310
+ omit falls back to the config, then the default. (List flags like `--redact-headers`
311
+ *replace* the config's list rather than merging — pass it only when you want to
312
+ override.) `target` may be given as the CLI argument or as `target` in the config;
313
+ the argument wins when both are present.
314
+
315
+ ---
316
+
317
+ ## Secret redaction
318
+
319
+ <details>
320
+ <summary>Secrets are stripped automatically by default — show details</summary>
321
+
322
+ Recordings are meant to be committed to git, so secrets are stripped **automatically** before anything is written to disk. By default the proxy replaces the values of these request/response headers with `[REDACTED]`:
323
+
324
+ - `Authorization`
325
+ - `Cookie`
326
+ - `Set-Cookie`
327
+
328
+ This is safe: replay matching ignores these headers, so redaction never breaks playback. It applies to both `.mock.json` recordings and WebSocket recordings.
329
+
330
+ When only *some* cookies are sensitive, allow-list the harmless ones by name (e.g. a `theme` or A/B-test cookie). Allow-listed cookies keep their values inside `Cookie`/`Set-Cookie`; every other cookie is still redacted.
331
+
332
+ > **Note:** `.har` files (browser-side requests recorded via Playwright's `routeFromHAR`) are written by Playwright, not the proxy, so this redaction does not cover them. Keep tokens out of HAR by recording with short-lived test credentials and reviewing HARs before committing — see the recommended setup-auth pattern below.
333
+
334
+ ### Recommended auth pattern
335
+
336
+ To keep the login flow and credentials out of recordings entirely, run authentication in a Playwright **setup project** with the proxy in `transparent` mode, persist `storageState` to a **gitignored** `auth-state.json`, and reuse it in your tests. Recorded requests then carry only the (redacted) session headers, never the login.
337
+
338
+ ### Tweaking what gets redacted
339
+
340
+ The defaults always apply while redaction is enabled; you can add to them or turn it off.
341
+
342
+ **CLI flags:**
343
+
344
+ - `--redact-headers <names>` — comma-separated extra header names to redact (merged with the defaults).
345
+ - `--redact-body <patterns>` — comma-separated regex patterns to redact from request/response bodies.
346
+ - `--allow-headers <names>` — comma-separated header names to exempt from redaction (e.g. `set-cookie`).
347
+ - `--allow-cookies <names>` — comma-separated cookie names to keep unredacted inside `Cookie`/`Set-Cookie`.
348
+ - `--no-redact` — disable redaction and commit raw secrets (not recommended).
349
+
350
+ ```bash
351
+ # Redact an API-key header and "sk_live_..." tokens, but keep the theme cookie
352
+ test-proxy-recorder http://localhost:8000 \
353
+ --redact-headers x-api-key \
354
+ --redact-body "sk_live_[a-zA-Z0-9]+" \
355
+ --allow-cookies theme,locale
356
+ ```
357
+
358
+ **Programmatic** (when constructing `ProxyServer` directly):
359
+
360
+ ```typescript
361
+ import { ProxyServer } from 'test-proxy-recorder';
362
+
363
+ const proxy = new ProxyServer('http://localhost:3000', './recordings', undefined, {
364
+ enabled: true, // default; set false to disable
365
+ headers: ['x-api-key', 'x-auth'], // extra headers, merged with the defaults
366
+ bodyPatterns: [/sk_live_[a-z0-9]+/i], // regexes replaced in request/response bodies
367
+ allowHeaders: ['set-cookie'], // never redact these headers
368
+ allowCookies: ['theme', 'locale'], // keep these cookies inside Cookie/Set-Cookie
369
+ placeholder: '[REDACTED]', // default
370
+ });
371
+ ```
372
+
373
+ `redactSession(session, config)` is also exported if you want to redact existing recordings yourself.
374
+
375
+ </details>
376
+
236
377
  ---
237
378
 
238
379
  ## Example Apps
@@ -407,6 +548,23 @@ function setProxyMode(
407
548
  ): Promise<void>;
408
549
  ```
409
550
 
551
+ ### `defineConfig`
552
+
553
+ Type-checked identity helper for a `test-proxy-recorder.config.{ts,js,mjs}` file
554
+ (see [Config file](#config-file)).
555
+
556
+ ```typescript
557
+ function defineConfig(config: Config): Config;
558
+
559
+ interface Config {
560
+ target?: string;
561
+ port?: number;
562
+ recordingsDir?: string;
563
+ timeout?: number;
564
+ redaction?: RedactionConfig;
565
+ }
566
+ ```
567
+
410
568
  ### Next.js helpers (`test-proxy-recorder/nextjs`)
411
569
 
412
570
  ```typescript
@@ -122,4 +122,4 @@ declare const playwrightProxy: {
122
122
  teardown(): Promise<void>;
123
123
  };
124
124
 
125
- export { type ControlRequest as C, type Mode as M, type PlaywrightTestInfo as P, type Recording as R, type WebSocketRecording as W, type RecordingSession as a, startRecording as b, startReplay as c, stopProxy as d, cleanupSession as e, type ClientSideRecordingOptions as f, generateSessionId as g, playwrightProxy as p, setProxyMode as s };
125
+ export { type ControlRequest as C, type Mode as M, type PlaywrightTestInfo as P, type RecordingSession as R, type WebSocketRecording as W, type Recording as a, startRecording as b, startReplay as c, stopProxy as d, cleanupSession as e, type ClientSideRecordingOptions as f, generateSessionId as g, playwrightProxy as p, setProxyMode as s };
@@ -122,4 +122,4 @@ declare const playwrightProxy: {
122
122
  teardown(): Promise<void>;
123
123
  };
124
124
 
125
- export { type ControlRequest as C, type Mode as M, type PlaywrightTestInfo as P, type Recording as R, type WebSocketRecording as W, type RecordingSession as a, startRecording as b, startReplay as c, stopProxy as d, cleanupSession as e, type ClientSideRecordingOptions as f, generateSessionId as g, playwrightProxy as p, setProxyMode as s };
125
+ export { type ControlRequest as C, type Mode as M, type PlaywrightTestInfo as P, type RecordingSession as R, type WebSocketRecording as W, type Recording as a, startRecording as b, startReplay as c, stopProxy as d, cleanupSession as e, type ClientSideRecordingOptions as f, generateSessionId as g, playwrightProxy as p, setProxyMode as s };
package/dist/index.cjs CHANGED
@@ -19,6 +19,11 @@ var crypto__default = /*#__PURE__*/_interopDefault(crypto);
19
19
  var path2__default = /*#__PURE__*/_interopDefault(path2);
20
20
  var filenamify2__default = /*#__PURE__*/_interopDefault(filenamify2);
21
21
 
22
+ // src/config.ts
23
+ function defineConfig(config) {
24
+ return config;
25
+ }
26
+
22
27
  // src/constants.ts
23
28
  var DEFAULT_TIMEOUT_MS = 120 * 1e3;
24
29
  var HTTP_STATUS_BAD_GATEWAY = 502;
@@ -272,6 +277,139 @@ var Modes = {
272
277
  record: "record",
273
278
  replay: "replay"
274
279
  };
280
+
281
+ // src/utils/redact.ts
282
+ var DEFAULT_REDACTED_HEADERS = [
283
+ "authorization",
284
+ "cookie",
285
+ "set-cookie"
286
+ ];
287
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
288
+ var COOKIE_HEADERS = /* @__PURE__ */ new Set(["cookie", "set-cookie"]);
289
+ function resolveRedaction(config) {
290
+ const extra = (config?.headers ?? []).map((name) => name.toLowerCase());
291
+ const headerSet = /* @__PURE__ */ new Set([...DEFAULT_REDACTED_HEADERS, ...extra]);
292
+ for (const name of config?.allowHeaders ?? []) {
293
+ headerSet.delete(name.toLowerCase());
294
+ }
295
+ return {
296
+ headerSet,
297
+ allowCookies: new Set(
298
+ (config?.allowCookies ?? []).map((name) => name.toLowerCase())
299
+ ),
300
+ regexes: toGlobalRegexes(config?.bodyPatterns),
301
+ placeholder: config?.placeholder ?? REDACTED_PLACEHOLDER
302
+ };
303
+ }
304
+ function toGlobalRegexes(patterns) {
305
+ if (!patterns || patterns.length === 0) {
306
+ return [];
307
+ }
308
+ return patterns.map((pattern) => {
309
+ if (typeof pattern === "string") {
310
+ return new RegExp(pattern, "g");
311
+ }
312
+ return pattern.flags.includes("g") ? pattern : new RegExp(pattern.source, `${pattern.flags}g`);
313
+ });
314
+ }
315
+ function redactCookieHeader(value, allowCookies, placeholder) {
316
+ return value.split(";").map((part) => part.trim()).filter(Boolean).map((pair) => {
317
+ const eq = pair.indexOf("=");
318
+ if (eq === -1) {
319
+ return pair;
320
+ }
321
+ const name = pair.slice(0, eq);
322
+ return allowCookies.has(name.toLowerCase()) ? pair : `${name}=${placeholder}`;
323
+ }).join("; ");
324
+ }
325
+ function redactSetCookieValue(value, allowCookies, placeholder) {
326
+ const semicolon = value.indexOf(";");
327
+ const firstPair = semicolon === -1 ? value : value.slice(0, semicolon);
328
+ const attributes = semicolon === -1 ? "" : value.slice(semicolon);
329
+ const eq = firstPair.indexOf("=");
330
+ if (eq === -1) {
331
+ return value;
332
+ }
333
+ const name = firstPair.slice(0, eq).trim();
334
+ if (allowCookies.has(name.toLowerCase())) {
335
+ return value;
336
+ }
337
+ return `${name}=${placeholder}${attributes}`;
338
+ }
339
+ function redactCookieAware(lower, value, resolved) {
340
+ const { allowCookies, placeholder } = resolved;
341
+ const redactOne = (cookie) => lower === "cookie" ? redactCookieHeader(cookie, allowCookies, placeholder) : redactSetCookieValue(cookie, allowCookies, placeholder);
342
+ return Array.isArray(value) ? value.map((v) => redactOne(v)) : redactOne(String(value));
343
+ }
344
+ function redactHeaderValue(name, value, resolved) {
345
+ const lower = name.toLowerCase();
346
+ if (resolved.allowCookies.size > 0 && COOKIE_HEADERS.has(lower)) {
347
+ return redactCookieAware(lower, value, resolved);
348
+ }
349
+ return Array.isArray(value) ? value.map(() => resolved.placeholder) : resolved.placeholder;
350
+ }
351
+ function redactHeaders(headers, resolved) {
352
+ const result = {};
353
+ for (const [name, value] of Object.entries(headers)) {
354
+ result[name] = resolved.headerSet.has(name.toLowerCase()) ? redactHeaderValue(name, value, resolved) : value;
355
+ }
356
+ return result;
357
+ }
358
+ function redactBody(body, regexes, placeholder) {
359
+ if (!body || regexes.length === 0) {
360
+ return body ?? null;
361
+ }
362
+ let result = body;
363
+ for (const regex of regexes) {
364
+ regex.lastIndex = 0;
365
+ result = result.replace(regex, placeholder);
366
+ }
367
+ return result;
368
+ }
369
+ function redactRecording(recording, resolved) {
370
+ const { regexes, placeholder } = resolved;
371
+ return {
372
+ ...recording,
373
+ request: {
374
+ ...recording.request,
375
+ headers: redactHeaders(recording.request.headers, resolved),
376
+ body: redactBody(recording.request.body, regexes, placeholder)
377
+ },
378
+ response: recording.response && {
379
+ ...recording.response,
380
+ headers: redactHeaders(recording.response.headers, resolved),
381
+ body: redactBody(recording.response.body, regexes, placeholder)
382
+ }
383
+ };
384
+ }
385
+ function redactWebSocketRecording(recording, resolved) {
386
+ const { regexes, placeholder } = resolved;
387
+ return {
388
+ ...recording,
389
+ headers: recording.headers ? redactHeaders(recording.headers, resolved) : recording.headers,
390
+ messages: recording.messages.map((message) => ({
391
+ ...message,
392
+ data: redactBody(message.data, regexes, placeholder) ?? message.data
393
+ }))
394
+ };
395
+ }
396
+ function redactSession(session, config) {
397
+ if (config?.enabled === false) {
398
+ return session;
399
+ }
400
+ const resolved = resolveRedaction(config);
401
+ return {
402
+ ...session,
403
+ recordings: session.recordings.map(
404
+ (recording) => redactRecording(recording, resolved)
405
+ ),
406
+ websocketRecordings: (session.websocketRecordings ?? []).map(
407
+ (recording) => redactWebSocketRecording(recording, resolved)
408
+ )
409
+ };
410
+ }
411
+
412
+ // src/utils/fileUtils.ts
275
413
  var JSON_INDENT_SPACES = 2;
276
414
  var EXTENSION = ".mock.json";
277
415
  var MAX_FILENAME_LENGTH = 255 - EXTENSION.length;
@@ -316,14 +454,17 @@ function processRecordings(recordings) {
316
454
  processedRecordings.sort((a, b) => a.recordingId - b.recordingId);
317
455
  return processedRecordings;
318
456
  }
319
- async function saveRecordingSession(recordingsDir, session) {
457
+ async function saveRecordingSession(recordingsDir, session, redaction) {
320
458
  const filePath = getRecordingPath(recordingsDir, session.id);
321
459
  await fs__default.default.mkdir(recordingsDir, { recursive: true });
322
460
  const processedRecordings = processRecordings(session.recordings);
323
- const processedSession = {
324
- ...session,
325
- recordings: processedRecordings
326
- };
461
+ const processedSession = redactSession(
462
+ {
463
+ ...session,
464
+ recordings: processedRecordings
465
+ },
466
+ redaction
467
+ );
327
468
  await fs__default.default.writeFile(
328
469
  filePath,
329
470
  JSON.stringify(processedSession, null, JSON_INDENT_SPACES)
@@ -552,19 +693,22 @@ var ProxyServer = class {
552
693
  currentSession;
553
694
  recordingsDir;
554
695
  timeoutMs;
555
- recordingIdCounter;
556
696
  // Unique ID for each recording entry
557
- sequenceCounterByKey;
697
+ recordingIdCounter;
558
698
  // Sequence counter per key (endpoint)
699
+ sequenceCounterByKey;
559
700
  replaySessions;
560
701
  // Track multiple concurrent replay sessions by recording ID
561
702
  recordingPromises;
562
703
  // Stack of promises that resolve to completed recordings
563
704
  flushPromise;
564
705
  // Promise for in-progress flush operation
565
- constructor(target, recordingsDir, timeoutMs) {
706
+ redaction;
707
+ // Secret-redaction config applied before saving
708
+ constructor(target, recordingsDir, timeoutMs, redaction) {
566
709
  this.target = target;
567
710
  this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
711
+ this.redaction = redaction;
568
712
  this.mode = Modes.transparent;
569
713
  this.recordingId = null;
570
714
  this.recordingIdCounter = 0;
@@ -814,7 +958,11 @@ var ProxyServer = class {
814
958
  console.log(
815
959
  `Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
816
960
  );
817
- await saveRecordingSession(this.recordingsDir, this.currentSession);
961
+ await saveRecordingSession(
962
+ this.recordingsDir,
963
+ this.currentSession,
964
+ this.redaction
965
+ );
818
966
  }
819
967
  getRecordingIdOrError(req, res) {
820
968
  const recordingIdFromRequest = getRecordingIdFromRequest(req);
@@ -1318,12 +1466,16 @@ function createHeadersWithRecordingId(requestHeaders, additionalHeaders = {}) {
1318
1466
  };
1319
1467
  }
1320
1468
 
1469
+ exports.DEFAULT_REDACTED_HEADERS = DEFAULT_REDACTED_HEADERS;
1321
1470
  exports.ProxyServer = ProxyServer;
1322
1471
  exports.RECORDING_ID_HEADER = RECORDING_ID_HEADER;
1472
+ exports.REDACTED_PLACEHOLDER = REDACTED_PLACEHOLDER;
1323
1473
  exports.createHeadersWithRecordingId = createHeadersWithRecordingId;
1474
+ exports.defineConfig = defineConfig;
1324
1475
  exports.generateSessionId = generateSessionId;
1325
1476
  exports.getRecordingId = getRecordingId;
1326
1477
  exports.playwrightProxy = playwrightProxy;
1478
+ exports.redactSession = redactSession;
1327
1479
  exports.setNextProxyHeaders = setNextProxyHeaders;
1328
1480
  exports.setProxyMode = setProxyMode;
1329
1481
  exports.startRecording = startRecording;
package/dist/index.d.cts CHANGED
@@ -1,8 +1,92 @@
1
+ import { R as RecordingSession } from './index-BnkejxM_.cjs';
2
+ export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, a as Recording, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-BnkejxM_.cjs';
1
3
  export { RECORDING_ID_HEADER, createHeadersWithRecordingId, getRecordingId, setNextProxyHeaders } from './nextjs/index.cjs';
2
4
  import http from 'node:http';
3
- export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-BloXCw69.cjs';
4
5
  import '@playwright/test';
5
6
 
7
+ /**
8
+ * Header names (lower-cased) whose values are stripped from recordings by
9
+ * default. These commonly carry credentials and are safe to remove: replay
10
+ * matching ignores request/response headers, so redaction never breaks
11
+ * playback.
12
+ */
13
+ declare const DEFAULT_REDACTED_HEADERS: string[];
14
+ /** Value written in place of a redacted secret. */
15
+ declare const REDACTED_PLACEHOLDER = "[REDACTED]";
16
+ interface RedactionConfig {
17
+ /**
18
+ * Master switch. Redaction is on by default; set to `false` to commit raw
19
+ * headers and bodies (not recommended).
20
+ */
21
+ enabled?: boolean;
22
+ /**
23
+ * Additional header names (case-insensitive) to redact. Merged with
24
+ * {@link DEFAULT_REDACTED_HEADERS} — the defaults always apply while
25
+ * enabled.
26
+ */
27
+ headers?: string[];
28
+ /**
29
+ * Header names (case-insensitive) to leave untouched even if they would
30
+ * otherwise be redacted. Use to exempt a header from the defaults, e.g.
31
+ * `['set-cookie']` when no session cookie is set on responses.
32
+ */
33
+ allowHeaders?: string[];
34
+ /**
35
+ * Cookie names (case-insensitive) to keep unredacted inside the `Cookie`
36
+ * and `Set-Cookie` headers. Every other cookie in those headers still has
37
+ * its value replaced. Use this when only some cookies are sensitive — e.g.
38
+ * keep a `theme` or A/B-test cookie while redacting the session cookie.
39
+ */
40
+ allowCookies?: string[];
41
+ /**
42
+ * Patterns matched against request/response bodies (and WebSocket message
43
+ * payloads). Every match is replaced with the placeholder. Strings are
44
+ * treated as global regular expressions. Use this for API keys or tokens
45
+ * embedded in payloads.
46
+ */
47
+ bodyPatterns?: (RegExp | string)[];
48
+ /** Replacement string. Defaults to {@link REDACTED_PLACEHOLDER}. */
49
+ placeholder?: string;
50
+ }
51
+ /**
52
+ * Return a redacted copy of a recording session. Sensitive headers are
53
+ * stripped and any configured body patterns replaced. The input is not
54
+ * mutated. When `config.enabled === false` the session is returned unchanged.
55
+ */
56
+ declare function redactSession(session: RecordingSession, config?: RedactionConfig): RecordingSession;
57
+
58
+ /**
59
+ * Shape of a `test-proxy-recorder.config.{ts,js,mjs}` file. Every field is
60
+ * optional; any value passed as a CLI flag overrides the matching config value.
61
+ */
62
+ interface Config {
63
+ /** Target API service URL, e.g. `http://localhost:3000`. */
64
+ target?: string;
65
+ /** Port for the proxy server. */
66
+ port?: number;
67
+ /** Directory to store recordings, resolved relative to CWD. */
68
+ recordingsDir?: string;
69
+ /** Session timeout in milliseconds. */
70
+ timeout?: number;
71
+ /** Secret redaction settings. See {@link RedactionConfig}. */
72
+ redaction?: RedactionConfig;
73
+ }
74
+ /**
75
+ * Identity helper for config files. Wrapping the object gives type-checking and
76
+ * editor autocomplete without changing its value:
77
+ *
78
+ * ```ts
79
+ * // test-proxy-recorder.config.ts
80
+ * import { defineConfig } from 'test-proxy-recorder';
81
+ *
82
+ * export default defineConfig({
83
+ * target: 'http://localhost:3000',
84
+ * redaction: { bodyPatterns: [/sk_live_\w+/g] },
85
+ * });
86
+ * ```
87
+ */
88
+ declare function defineConfig(config: Config): Config;
89
+
6
90
  declare class ProxyServer {
7
91
  private target;
8
92
  private mode;
@@ -18,7 +102,8 @@ declare class ProxyServer {
18
102
  private replaySessions;
19
103
  private recordingPromises;
20
104
  private flushPromise;
21
- constructor(target: string, recordingsDir: string, timeoutMs?: number);
105
+ private redaction?;
106
+ constructor(target: string, recordingsDir: string, timeoutMs?: number, redaction?: RedactionConfig);
22
107
  init(): Promise<void>;
23
108
  listen(port: number): http.Server;
24
109
  private setupProxyEventHandlers;
@@ -60,4 +145,4 @@ declare class ProxyServer {
60
145
  private logServerStartup;
61
146
  }
62
147
 
63
- export { ProxyServer };
148
+ export { type Config, DEFAULT_REDACTED_HEADERS, ProxyServer, REDACTED_PLACEHOLDER, RecordingSession, type RedactionConfig, defineConfig, redactSession };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,92 @@
1
+ import { R as RecordingSession } from './index-BnkejxM_.js';
2
+ export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, a as Recording, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-BnkejxM_.js';
1
3
  export { RECORDING_ID_HEADER, createHeadersWithRecordingId, getRecordingId, setNextProxyHeaders } from './nextjs/index.js';
2
4
  import http from 'node:http';
3
- export { C as ControlRequest, M as Mode, P as PlaywrightTestInfo, R as Recording, a as RecordingSession, W as WebSocketRecording, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from './index-BloXCw69.js';
4
5
  import '@playwright/test';
5
6
 
7
+ /**
8
+ * Header names (lower-cased) whose values are stripped from recordings by
9
+ * default. These commonly carry credentials and are safe to remove: replay
10
+ * matching ignores request/response headers, so redaction never breaks
11
+ * playback.
12
+ */
13
+ declare const DEFAULT_REDACTED_HEADERS: string[];
14
+ /** Value written in place of a redacted secret. */
15
+ declare const REDACTED_PLACEHOLDER = "[REDACTED]";
16
+ interface RedactionConfig {
17
+ /**
18
+ * Master switch. Redaction is on by default; set to `false` to commit raw
19
+ * headers and bodies (not recommended).
20
+ */
21
+ enabled?: boolean;
22
+ /**
23
+ * Additional header names (case-insensitive) to redact. Merged with
24
+ * {@link DEFAULT_REDACTED_HEADERS} — the defaults always apply while
25
+ * enabled.
26
+ */
27
+ headers?: string[];
28
+ /**
29
+ * Header names (case-insensitive) to leave untouched even if they would
30
+ * otherwise be redacted. Use to exempt a header from the defaults, e.g.
31
+ * `['set-cookie']` when no session cookie is set on responses.
32
+ */
33
+ allowHeaders?: string[];
34
+ /**
35
+ * Cookie names (case-insensitive) to keep unredacted inside the `Cookie`
36
+ * and `Set-Cookie` headers. Every other cookie in those headers still has
37
+ * its value replaced. Use this when only some cookies are sensitive — e.g.
38
+ * keep a `theme` or A/B-test cookie while redacting the session cookie.
39
+ */
40
+ allowCookies?: string[];
41
+ /**
42
+ * Patterns matched against request/response bodies (and WebSocket message
43
+ * payloads). Every match is replaced with the placeholder. Strings are
44
+ * treated as global regular expressions. Use this for API keys or tokens
45
+ * embedded in payloads.
46
+ */
47
+ bodyPatterns?: (RegExp | string)[];
48
+ /** Replacement string. Defaults to {@link REDACTED_PLACEHOLDER}. */
49
+ placeholder?: string;
50
+ }
51
+ /**
52
+ * Return a redacted copy of a recording session. Sensitive headers are
53
+ * stripped and any configured body patterns replaced. The input is not
54
+ * mutated. When `config.enabled === false` the session is returned unchanged.
55
+ */
56
+ declare function redactSession(session: RecordingSession, config?: RedactionConfig): RecordingSession;
57
+
58
+ /**
59
+ * Shape of a `test-proxy-recorder.config.{ts,js,mjs}` file. Every field is
60
+ * optional; any value passed as a CLI flag overrides the matching config value.
61
+ */
62
+ interface Config {
63
+ /** Target API service URL, e.g. `http://localhost:3000`. */
64
+ target?: string;
65
+ /** Port for the proxy server. */
66
+ port?: number;
67
+ /** Directory to store recordings, resolved relative to CWD. */
68
+ recordingsDir?: string;
69
+ /** Session timeout in milliseconds. */
70
+ timeout?: number;
71
+ /** Secret redaction settings. See {@link RedactionConfig}. */
72
+ redaction?: RedactionConfig;
73
+ }
74
+ /**
75
+ * Identity helper for config files. Wrapping the object gives type-checking and
76
+ * editor autocomplete without changing its value:
77
+ *
78
+ * ```ts
79
+ * // test-proxy-recorder.config.ts
80
+ * import { defineConfig } from 'test-proxy-recorder';
81
+ *
82
+ * export default defineConfig({
83
+ * target: 'http://localhost:3000',
84
+ * redaction: { bodyPatterns: [/sk_live_\w+/g] },
85
+ * });
86
+ * ```
87
+ */
88
+ declare function defineConfig(config: Config): Config;
89
+
6
90
  declare class ProxyServer {
7
91
  private target;
8
92
  private mode;
@@ -18,7 +102,8 @@ declare class ProxyServer {
18
102
  private replaySessions;
19
103
  private recordingPromises;
20
104
  private flushPromise;
21
- constructor(target: string, recordingsDir: string, timeoutMs?: number);
105
+ private redaction?;
106
+ constructor(target: string, recordingsDir: string, timeoutMs?: number, redaction?: RedactionConfig);
22
107
  init(): Promise<void>;
23
108
  listen(port: number): http.Server;
24
109
  private setupProxyEventHandlers;
@@ -60,4 +145,4 @@ declare class ProxyServer {
60
145
  private logServerStartup;
61
146
  }
62
147
 
63
- export { ProxyServer };
148
+ export { type Config, DEFAULT_REDACTED_HEADERS, ProxyServer, REDACTED_PLACEHOLDER, RecordingSession, type RedactionConfig, defineConfig, redactSession };