test-proxy-recorder 0.3.9 → 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 +160 -8
- package/dist/index.d.cts +88 -3
- package/dist/index.d.ts +88 -3
- package/dist/index.mjs +157 -9
- package/dist/playwright/index.d.cts +1 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/proxy.js +694 -39
- package/package.json +3 -2
- package/skills/proxy-setup/SKILL.md +80 -3
package/dist/index.mjs
CHANGED
|
@@ -7,6 +7,11 @@ import path2 from 'path';
|
|
|
7
7
|
import filenamify2 from 'filenamify';
|
|
8
8
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
9
9
|
|
|
10
|
+
// src/config.ts
|
|
11
|
+
function defineConfig(config) {
|
|
12
|
+
return config;
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
// src/constants.ts
|
|
11
16
|
var DEFAULT_TIMEOUT_MS = 120 * 1e3;
|
|
12
17
|
var HTTP_STATUS_BAD_GATEWAY = 502;
|
|
@@ -260,6 +265,139 @@ var Modes = {
|
|
|
260
265
|
record: "record",
|
|
261
266
|
replay: "replay"
|
|
262
267
|
};
|
|
268
|
+
|
|
269
|
+
// src/utils/redact.ts
|
|
270
|
+
var DEFAULT_REDACTED_HEADERS = [
|
|
271
|
+
"authorization",
|
|
272
|
+
"cookie",
|
|
273
|
+
"set-cookie"
|
|
274
|
+
];
|
|
275
|
+
var REDACTED_PLACEHOLDER = "[REDACTED]";
|
|
276
|
+
var COOKIE_HEADERS = /* @__PURE__ */ new Set(["cookie", "set-cookie"]);
|
|
277
|
+
function resolveRedaction(config) {
|
|
278
|
+
const extra = (config?.headers ?? []).map((name) => name.toLowerCase());
|
|
279
|
+
const headerSet = /* @__PURE__ */ new Set([...DEFAULT_REDACTED_HEADERS, ...extra]);
|
|
280
|
+
for (const name of config?.allowHeaders ?? []) {
|
|
281
|
+
headerSet.delete(name.toLowerCase());
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
headerSet,
|
|
285
|
+
allowCookies: new Set(
|
|
286
|
+
(config?.allowCookies ?? []).map((name) => name.toLowerCase())
|
|
287
|
+
),
|
|
288
|
+
regexes: toGlobalRegexes(config?.bodyPatterns),
|
|
289
|
+
placeholder: config?.placeholder ?? REDACTED_PLACEHOLDER
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
function toGlobalRegexes(patterns) {
|
|
293
|
+
if (!patterns || patterns.length === 0) {
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
return patterns.map((pattern) => {
|
|
297
|
+
if (typeof pattern === "string") {
|
|
298
|
+
return new RegExp(pattern, "g");
|
|
299
|
+
}
|
|
300
|
+
return pattern.flags.includes("g") ? pattern : new RegExp(pattern.source, `${pattern.flags}g`);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
function redactCookieHeader(value, allowCookies, placeholder) {
|
|
304
|
+
return value.split(";").map((part) => part.trim()).filter(Boolean).map((pair) => {
|
|
305
|
+
const eq = pair.indexOf("=");
|
|
306
|
+
if (eq === -1) {
|
|
307
|
+
return pair;
|
|
308
|
+
}
|
|
309
|
+
const name = pair.slice(0, eq);
|
|
310
|
+
return allowCookies.has(name.toLowerCase()) ? pair : `${name}=${placeholder}`;
|
|
311
|
+
}).join("; ");
|
|
312
|
+
}
|
|
313
|
+
function redactSetCookieValue(value, allowCookies, placeholder) {
|
|
314
|
+
const semicolon = value.indexOf(";");
|
|
315
|
+
const firstPair = semicolon === -1 ? value : value.slice(0, semicolon);
|
|
316
|
+
const attributes = semicolon === -1 ? "" : value.slice(semicolon);
|
|
317
|
+
const eq = firstPair.indexOf("=");
|
|
318
|
+
if (eq === -1) {
|
|
319
|
+
return value;
|
|
320
|
+
}
|
|
321
|
+
const name = firstPair.slice(0, eq).trim();
|
|
322
|
+
if (allowCookies.has(name.toLowerCase())) {
|
|
323
|
+
return value;
|
|
324
|
+
}
|
|
325
|
+
return `${name}=${placeholder}${attributes}`;
|
|
326
|
+
}
|
|
327
|
+
function redactCookieAware(lower, value, resolved) {
|
|
328
|
+
const { allowCookies, placeholder } = resolved;
|
|
329
|
+
const redactOne = (cookie) => lower === "cookie" ? redactCookieHeader(cookie, allowCookies, placeholder) : redactSetCookieValue(cookie, allowCookies, placeholder);
|
|
330
|
+
return Array.isArray(value) ? value.map((v) => redactOne(v)) : redactOne(String(value));
|
|
331
|
+
}
|
|
332
|
+
function redactHeaderValue(name, value, resolved) {
|
|
333
|
+
const lower = name.toLowerCase();
|
|
334
|
+
if (resolved.allowCookies.size > 0 && COOKIE_HEADERS.has(lower)) {
|
|
335
|
+
return redactCookieAware(lower, value, resolved);
|
|
336
|
+
}
|
|
337
|
+
return Array.isArray(value) ? value.map(() => resolved.placeholder) : resolved.placeholder;
|
|
338
|
+
}
|
|
339
|
+
function redactHeaders(headers, resolved) {
|
|
340
|
+
const result = {};
|
|
341
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
342
|
+
result[name] = resolved.headerSet.has(name.toLowerCase()) ? redactHeaderValue(name, value, resolved) : value;
|
|
343
|
+
}
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
function redactBody(body, regexes, placeholder) {
|
|
347
|
+
if (!body || regexes.length === 0) {
|
|
348
|
+
return body ?? null;
|
|
349
|
+
}
|
|
350
|
+
let result = body;
|
|
351
|
+
for (const regex of regexes) {
|
|
352
|
+
regex.lastIndex = 0;
|
|
353
|
+
result = result.replace(regex, placeholder);
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
}
|
|
357
|
+
function redactRecording(recording, resolved) {
|
|
358
|
+
const { regexes, placeholder } = resolved;
|
|
359
|
+
return {
|
|
360
|
+
...recording,
|
|
361
|
+
request: {
|
|
362
|
+
...recording.request,
|
|
363
|
+
headers: redactHeaders(recording.request.headers, resolved),
|
|
364
|
+
body: redactBody(recording.request.body, regexes, placeholder)
|
|
365
|
+
},
|
|
366
|
+
response: recording.response && {
|
|
367
|
+
...recording.response,
|
|
368
|
+
headers: redactHeaders(recording.response.headers, resolved),
|
|
369
|
+
body: redactBody(recording.response.body, regexes, placeholder)
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
function redactWebSocketRecording(recording, resolved) {
|
|
374
|
+
const { regexes, placeholder } = resolved;
|
|
375
|
+
return {
|
|
376
|
+
...recording,
|
|
377
|
+
headers: recording.headers ? redactHeaders(recording.headers, resolved) : recording.headers,
|
|
378
|
+
messages: recording.messages.map((message) => ({
|
|
379
|
+
...message,
|
|
380
|
+
data: redactBody(message.data, regexes, placeholder) ?? message.data
|
|
381
|
+
}))
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function redactSession(session, config) {
|
|
385
|
+
if (config?.enabled === false) {
|
|
386
|
+
return session;
|
|
387
|
+
}
|
|
388
|
+
const resolved = resolveRedaction(config);
|
|
389
|
+
return {
|
|
390
|
+
...session,
|
|
391
|
+
recordings: session.recordings.map(
|
|
392
|
+
(recording) => redactRecording(recording, resolved)
|
|
393
|
+
),
|
|
394
|
+
websocketRecordings: (session.websocketRecordings ?? []).map(
|
|
395
|
+
(recording) => redactWebSocketRecording(recording, resolved)
|
|
396
|
+
)
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/utils/fileUtils.ts
|
|
263
401
|
var JSON_INDENT_SPACES = 2;
|
|
264
402
|
var EXTENSION = ".mock.json";
|
|
265
403
|
var MAX_FILENAME_LENGTH = 255 - EXTENSION.length;
|
|
@@ -304,14 +442,17 @@ function processRecordings(recordings) {
|
|
|
304
442
|
processedRecordings.sort((a, b) => a.recordingId - b.recordingId);
|
|
305
443
|
return processedRecordings;
|
|
306
444
|
}
|
|
307
|
-
async function saveRecordingSession(recordingsDir, session) {
|
|
445
|
+
async function saveRecordingSession(recordingsDir, session, redaction) {
|
|
308
446
|
const filePath = getRecordingPath(recordingsDir, session.id);
|
|
309
447
|
await fs.mkdir(recordingsDir, { recursive: true });
|
|
310
448
|
const processedRecordings = processRecordings(session.recordings);
|
|
311
|
-
const processedSession =
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
449
|
+
const processedSession = redactSession(
|
|
450
|
+
{
|
|
451
|
+
...session,
|
|
452
|
+
recordings: processedRecordings
|
|
453
|
+
},
|
|
454
|
+
redaction
|
|
455
|
+
);
|
|
315
456
|
await fs.writeFile(
|
|
316
457
|
filePath,
|
|
317
458
|
JSON.stringify(processedSession, null, JSON_INDENT_SPACES)
|
|
@@ -542,17 +683,20 @@ var ProxyServer = class {
|
|
|
542
683
|
timeoutMs;
|
|
543
684
|
// Unique ID for each recording entry
|
|
544
685
|
recordingIdCounter;
|
|
545
|
-
sequenceCounterByKey;
|
|
546
686
|
// Sequence counter per key (endpoint)
|
|
687
|
+
sequenceCounterByKey;
|
|
547
688
|
replaySessions;
|
|
548
689
|
// Track multiple concurrent replay sessions by recording ID
|
|
549
690
|
recordingPromises;
|
|
550
691
|
// Stack of promises that resolve to completed recordings
|
|
551
692
|
flushPromise;
|
|
552
693
|
// Promise for in-progress flush operation
|
|
553
|
-
|
|
694
|
+
redaction;
|
|
695
|
+
// Secret-redaction config applied before saving
|
|
696
|
+
constructor(target, recordingsDir, timeoutMs, redaction) {
|
|
554
697
|
this.target = target;
|
|
555
698
|
this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
699
|
+
this.redaction = redaction;
|
|
556
700
|
this.mode = Modes.transparent;
|
|
557
701
|
this.recordingId = null;
|
|
558
702
|
this.recordingIdCounter = 0;
|
|
@@ -802,7 +946,11 @@ var ProxyServer = class {
|
|
|
802
946
|
console.log(
|
|
803
947
|
`Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`
|
|
804
948
|
);
|
|
805
|
-
await saveRecordingSession(
|
|
949
|
+
await saveRecordingSession(
|
|
950
|
+
this.recordingsDir,
|
|
951
|
+
this.currentSession,
|
|
952
|
+
this.redaction
|
|
953
|
+
);
|
|
806
954
|
}
|
|
807
955
|
getRecordingIdOrError(req, res) {
|
|
808
956
|
const recordingIdFromRequest = getRecordingIdFromRequest(req);
|
|
@@ -1306,6 +1454,6 @@ function createHeadersWithRecordingId(requestHeaders, additionalHeaders = {}) {
|
|
|
1306
1454
|
};
|
|
1307
1455
|
}
|
|
1308
1456
|
|
|
1309
|
-
export { ProxyServer, RECORDING_ID_HEADER, createHeadersWithRecordingId, generateSessionId, getRecordingId, playwrightProxy, setNextProxyHeaders, setProxyMode, startRecording, startReplay, stopProxy };
|
|
1457
|
+
export { DEFAULT_REDACTED_HEADERS, ProxyServer, RECORDING_ID_HEADER, REDACTED_PLACEHOLDER, createHeadersWithRecordingId, defineConfig, generateSessionId, getRecordingId, playwrightProxy, redactSession, setNextProxyHeaders, setProxyMode, startRecording, startReplay, stopProxy };
|
|
1310
1458
|
//# sourceMappingURL=index.mjs.map
|
|
1311
1459
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@playwright/test';
|
|
2
|
-
export { f as ClientSideRecordingOptions, P as PlaywrightTestInfo, e as cleanupSession, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-
|
|
2
|
+
export { f as ClientSideRecordingOptions, P as PlaywrightTestInfo, e as cleanupSession, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-BnkejxM_.cjs';
|
|
3
3
|
import 'node:http';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@playwright/test';
|
|
2
|
-
export { f as ClientSideRecordingOptions, P as PlaywrightTestInfo, e as cleanupSession, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-
|
|
2
|
+
export { f as ClientSideRecordingOptions, P as PlaywrightTestInfo, e as cleanupSession, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-BnkejxM_.js';
|
|
3
3
|
import 'node:http';
|