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 +163 -5
- package/dist/{index-BloXCw69.d.cts → index-BnkejxM_.d.cts} +1 -1
- package/dist/{index-BloXCw69.d.ts → index-BnkejxM_.d.ts} +1 -1
- package/dist/index.cjs +161 -9
- package/dist/index.d.cts +88 -3
- package/dist/index.d.ts +88 -3
- package/dist/index.mjs +158 -10
- package/dist/playwright/index.d.cts +1 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/proxy.js +695 -40
- package/package.json +3 -2
- package/skills/proxy-setup/SKILL.md +80 -3
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
|
|
225
|
-
|
|
|
226
|
-
| `<target-url>`
|
|
227
|
-
| `--port, -p`
|
|
228
|
-
| `--dir, -d`
|
|
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
|
|
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
|
|
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
|
-
|
|
325
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 };
|