test-proxy-recorder 0.1.3 → 0.1.4

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.
@@ -28,6 +28,7 @@ interface Recording {
28
28
  response?: RecordedResponse;
29
29
  timestamp: string;
30
30
  key: string;
31
+ sequence: number;
31
32
  }
32
33
  interface WebSocketMessage {
33
34
  direction: 'client-to-server' | 'server-to-client';
@@ -53,7 +54,7 @@ type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
53
54
  * @param sessionId - Unique identifier for the session
54
55
  * @param timeout - Optional timeout in milliseconds
55
56
  */
56
- declare function setProxyMode(mode: Mode, sessionId: string, timeout?: number): Promise<void>;
57
+ declare function setProxyMode(mode: Mode, sessionId?: string, timeout?: number): Promise<void>;
57
58
  /**
58
59
  * Generate a session ID from test info
59
60
  * @param testInfo - Playwright test info object
@@ -28,6 +28,7 @@ interface Recording {
28
28
  response?: RecordedResponse;
29
29
  timestamp: string;
30
30
  key: string;
31
+ sequence: number;
31
32
  }
32
33
  interface WebSocketMessage {
33
34
  direction: 'client-to-server' | 'server-to-client';
@@ -53,7 +54,7 @@ type PlaywrightTestInfo = Pick<TestInfo, 'title'>;
53
54
  * @param sessionId - Unique identifier for the session
54
55
  * @param timeout - Optional timeout in milliseconds
55
56
  */
56
- declare function setProxyMode(mode: Mode, sessionId: string, timeout?: number): Promise<void>;
57
+ declare function setProxyMode(mode: Mode, sessionId?: string, timeout?: number): Promise<void>;
57
58
  /**
58
59
  * Generate a session ID from test info
59
60
  * @param testInfo - Playwright test info object
package/dist/index.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var fs = require('fs/promises');
4
4
  var http = require('http');
5
+ var https = require('https');
5
6
  var httpProxy = require('http-proxy');
6
7
  var ws = require('ws');
7
8
  var path = require('path');
@@ -11,6 +12,7 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
12
 
12
13
  var fs__default = /*#__PURE__*/_interopDefault(fs);
13
14
  var http__default = /*#__PURE__*/_interopDefault(http);
15
+ var https__default = /*#__PURE__*/_interopDefault(https);
14
16
  var httpProxy__default = /*#__PURE__*/_interopDefault(httpProxy);
15
17
  var path__default = /*#__PURE__*/_interopDefault(path);
16
18
  var filenamify__default = /*#__PURE__*/_interopDefault(filenamify);
@@ -93,6 +95,10 @@ var ProxyServer = class {
93
95
  proxy;
94
96
  currentSession;
95
97
  recordingsDir;
98
+ requestSequenceMap;
99
+ // Track sequence per request key
100
+ replaySequenceMap;
101
+ // Track replay position per request key
96
102
  constructor(targets, recordingsDir) {
97
103
  this.targets = targets;
98
104
  this.currentTargetIndex = 0;
@@ -102,6 +108,8 @@ var ProxyServer = class {
102
108
  this.modeTimeout = null;
103
109
  this.currentSession = null;
104
110
  this.recordingsDir = recordingsDir;
111
+ this.requestSequenceMap = /* @__PURE__ */ new Map();
112
+ this.replaySequenceMap = /* @__PURE__ */ new Map();
105
113
  this.proxy = httpProxy__default.default.createProxyServer({
106
114
  secure: false,
107
115
  changeOrigin: true
@@ -140,10 +148,19 @@ var ProxyServer = class {
140
148
  res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
141
149
  }
142
150
  handleProxyResponse(proxyRes, req) {
151
+ this.addCorsHeaders(proxyRes, req);
143
152
  if (this.mode === Modes.record && this.recordingId) {
144
153
  this.recordResponse(req, proxyRes);
145
154
  }
146
155
  }
156
+ addCorsHeaders(proxyRes, req) {
157
+ const origin = req.headers.origin;
158
+ proxyRes.headers["access-control-allow-origin"] = origin || "*";
159
+ proxyRes.headers["access-control-allow-credentials"] = "true";
160
+ proxyRes.headers["access-control-allow-headers"] = req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization";
161
+ proxyRes.headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
162
+ proxyRes.headers["access-control-expose-headers"] = "*";
163
+ }
147
164
  getTarget() {
148
165
  const target = this.targets[this.currentTargetIndex];
149
166
  this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
@@ -218,6 +235,7 @@ var ProxyServer = class {
218
235
  this.recordingId = id;
219
236
  this.replayId = null;
220
237
  this.currentSession = { id, recordings: [], websocketRecordings: [] };
238
+ this.requestSequenceMap.clear();
221
239
  console.log(`Switched to record mode with ID: ${id}`);
222
240
  }
223
241
  switchToReplayMode(id) {
@@ -228,6 +246,7 @@ var ProxyServer = class {
228
246
  this.replayId = id;
229
247
  this.recordingId = null;
230
248
  this.currentSession = null;
249
+ this.replaySequenceMap.clear();
231
250
  console.log(`Switched to replay mode with ID: ${id}`);
232
251
  }
233
252
  setupModeTimeout(timeout) {
@@ -259,6 +278,8 @@ var ProxyServer = class {
259
278
  return;
260
279
  }
261
280
  const key = getReqID(req);
281
+ const currentSequence = this.requestSequenceMap.get(key) || 0;
282
+ this.requestSequenceMap.set(key, currentSequence + 1);
262
283
  const record = {
263
284
  request: {
264
285
  method: req.method,
@@ -267,7 +288,8 @@ var ProxyServer = class {
267
288
  body: body || null
268
289
  },
269
290
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
270
- key
291
+ key,
292
+ sequence: currentSequence
271
293
  };
272
294
  this.currentSession.recordings.push(record);
273
295
  }
@@ -276,7 +298,9 @@ var ProxyServer = class {
276
298
  return;
277
299
  }
278
300
  const key = getReqID(req);
279
- const record = this.currentSession.recordings.find((r) => r.key === key);
301
+ const record = this.currentSession.recordings.findLast(
302
+ (r) => r.key === key && !r.response
303
+ );
280
304
  if (!record) {
281
305
  console.error("Request record not found for response:", key);
282
306
  return;
@@ -301,17 +325,31 @@ var ProxyServer = class {
301
325
  const filePath = getRecordingPath(this.recordingsDir, this.replayId);
302
326
  try {
303
327
  const session = await loadRecordingSession(filePath);
304
- const record = session.recordings.find((r) => r.key === key);
328
+ const currentSequence = this.replaySequenceMap.get(key) || 0;
329
+ const record = session.recordings.find(
330
+ (r) => r.key === key && r.sequence === currentSequence
331
+ );
305
332
  if (!record) {
306
- throw new Error(`No recording found for ${key}`);
333
+ throw new Error(
334
+ `No recording found for ${key} with sequence ${currentSequence}`
335
+ );
307
336
  }
308
337
  if (!record.response) {
309
338
  throw new Error("No response recorded for this request");
310
339
  }
340
+ this.replaySequenceMap.set(key, currentSequence + 1);
311
341
  const { statusCode, headers, body } = record.response;
312
- res.writeHead(statusCode, headers);
342
+ const origin = req.headers.origin;
343
+ const responseHeaders = {
344
+ ...headers,
345
+ "access-control-allow-origin": origin || "*",
346
+ "access-control-allow-credentials": "true"
347
+ };
348
+ res.writeHead(statusCode, responseHeaders);
313
349
  res.end(body);
314
- console.log(`Replayed: ${req.method} ${req.url}`);
350
+ console.log(
351
+ `Replayed: ${req.method} ${req.url} (sequence: ${currentSequence})`
352
+ );
315
353
  } catch (error) {
316
354
  this.handleReplayError(res, error, key, filePath);
317
355
  }
@@ -327,6 +365,9 @@ var ProxyServer = class {
327
365
  });
328
366
  }
329
367
  async handleRequest(req, res) {
368
+ if (req.method === "OPTIONS") {
369
+ return this.handleCorsPreflightRequest(req, res);
370
+ }
330
371
  if (req.url === CONTROL_ENDPOINT) {
331
372
  return this.handleControlRequest(req, res);
332
373
  }
@@ -335,23 +376,63 @@ var ProxyServer = class {
335
376
  }
336
377
  await this.handleProxyRequest(req, res);
337
378
  }
379
+ handleCorsPreflightRequest(req, res) {
380
+ const origin = req.headers.origin;
381
+ res.writeHead(HTTP_STATUS_OK, {
382
+ "Access-Control-Allow-Origin": origin || "*",
383
+ "Access-Control-Allow-Credentials": "true",
384
+ "Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
385
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
386
+ "Access-Control-Max-Age": "86400"
387
+ // 24 hours
388
+ });
389
+ res.end();
390
+ }
338
391
  async handleProxyRequest(req, res) {
339
392
  const target = this.getTarget();
340
393
  console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
341
394
  if (this.mode === Modes.record) {
342
- await this.bufferRequestForRecord(req);
395
+ await this.bufferAndProxyRequest(req, res, target);
396
+ } else {
397
+ this.proxy.web(req, res, { target });
343
398
  }
344
- this.proxy.web(req, res, { target });
345
399
  }
346
- async bufferRequestForRecord(req) {
400
+ async bufferAndProxyRequest(req, res, target) {
347
401
  const chunks = [];
348
402
  req.on("data", (chunk) => {
349
403
  chunks.push(chunk);
350
404
  });
351
- req.on("end", async () => {
352
- const body = Buffer.concat(chunks).toString("utf8");
353
- await this.saveRequestRecord(req, body);
405
+ await new Promise((resolve) => {
406
+ req.on("end", () => resolve());
354
407
  });
408
+ const body = Buffer.concat(chunks).toString("utf8");
409
+ await this.saveRequestRecord(req, body);
410
+ const targetUrl = new URL(target);
411
+ const isHttps = targetUrl.protocol === "https:";
412
+ const requestModule = isHttps ? https__default.default : http__default.default;
413
+ const defaultPort = isHttps ? 443 : 80;
414
+ const proxyReq = requestModule.request(
415
+ {
416
+ hostname: targetUrl.hostname,
417
+ port: targetUrl.port || defaultPort,
418
+ path: req.url,
419
+ method: req.method,
420
+ headers: req.headers
421
+ },
422
+ (proxyRes) => {
423
+ this.addCorsHeaders(proxyRes, req);
424
+ this.recordResponse(req, proxyRes);
425
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
426
+ proxyRes.pipe(res);
427
+ }
428
+ );
429
+ proxyReq.on("error", (err) => {
430
+ this.handleProxyError(err, req, res);
431
+ });
432
+ if (chunks.length > 0) {
433
+ proxyReq.write(Buffer.concat(chunks));
434
+ }
435
+ proxyReq.end();
355
436
  }
356
437
  handleUpgrade(req, socket, head) {
357
438
  if (this.mode === Modes.replay) {
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import http from 'node:http';
2
- 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-DfFpm8mB.cjs';
2
+ 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-De4mgziH.cjs';
3
3
  import '@playwright/test';
4
4
 
5
5
  declare class ProxyServer {
@@ -12,12 +12,15 @@ declare class ProxyServer {
12
12
  private proxy;
13
13
  private currentSession;
14
14
  private recordingsDir;
15
+ private requestSequenceMap;
16
+ private replaySequenceMap;
15
17
  constructor(targets: string[], recordingsDir: string);
16
18
  init(): Promise<void>;
17
19
  listen(port: number): http.Server;
18
20
  private setupProxyEventHandlers;
19
21
  private handleProxyError;
20
22
  private handleProxyResponse;
23
+ private addCorsHeaders;
21
24
  private getTarget;
22
25
  private handleControlRequest;
23
26
  private clearModeTimeout;
@@ -32,8 +35,9 @@ declare class ProxyServer {
32
35
  private handleReplayRequest;
33
36
  private handleReplayError;
34
37
  private handleRequest;
38
+ private handleCorsPreflightRequest;
35
39
  private handleProxyRequest;
36
- private bufferRequestForRecord;
40
+ private bufferAndProxyRequest;
37
41
  private handleUpgrade;
38
42
  private handleRecordWebSocket;
39
43
  private handleReplayWebSocket;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import http from 'node:http';
2
- 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-DfFpm8mB.js';
2
+ 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-De4mgziH.js';
3
3
  import '@playwright/test';
4
4
 
5
5
  declare class ProxyServer {
@@ -12,12 +12,15 @@ declare class ProxyServer {
12
12
  private proxy;
13
13
  private currentSession;
14
14
  private recordingsDir;
15
+ private requestSequenceMap;
16
+ private replaySequenceMap;
15
17
  constructor(targets: string[], recordingsDir: string);
16
18
  init(): Promise<void>;
17
19
  listen(port: number): http.Server;
18
20
  private setupProxyEventHandlers;
19
21
  private handleProxyError;
20
22
  private handleProxyResponse;
23
+ private addCorsHeaders;
21
24
  private getTarget;
22
25
  private handleControlRequest;
23
26
  private clearModeTimeout;
@@ -32,8 +35,9 @@ declare class ProxyServer {
32
35
  private handleReplayRequest;
33
36
  private handleReplayError;
34
37
  private handleRequest;
38
+ private handleCorsPreflightRequest;
35
39
  private handleProxyRequest;
36
- private bufferRequestForRecord;
40
+ private bufferAndProxyRequest;
37
41
  private handleUpgrade;
38
42
  private handleRecordWebSocket;
39
43
  private handleReplayWebSocket;
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs/promises';
2
2
  import http from 'http';
3
+ import https from 'https';
3
4
  import httpProxy from 'http-proxy';
4
5
  import { WebSocket, WebSocketServer } from 'ws';
5
6
  import path from 'path';
@@ -83,6 +84,10 @@ var ProxyServer = class {
83
84
  proxy;
84
85
  currentSession;
85
86
  recordingsDir;
87
+ requestSequenceMap;
88
+ // Track sequence per request key
89
+ replaySequenceMap;
90
+ // Track replay position per request key
86
91
  constructor(targets, recordingsDir) {
87
92
  this.targets = targets;
88
93
  this.currentTargetIndex = 0;
@@ -92,6 +97,8 @@ var ProxyServer = class {
92
97
  this.modeTimeout = null;
93
98
  this.currentSession = null;
94
99
  this.recordingsDir = recordingsDir;
100
+ this.requestSequenceMap = /* @__PURE__ */ new Map();
101
+ this.replaySequenceMap = /* @__PURE__ */ new Map();
95
102
  this.proxy = httpProxy.createProxyServer({
96
103
  secure: false,
97
104
  changeOrigin: true
@@ -130,10 +137,19 @@ var ProxyServer = class {
130
137
  res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
131
138
  }
132
139
  handleProxyResponse(proxyRes, req) {
140
+ this.addCorsHeaders(proxyRes, req);
133
141
  if (this.mode === Modes.record && this.recordingId) {
134
142
  this.recordResponse(req, proxyRes);
135
143
  }
136
144
  }
145
+ addCorsHeaders(proxyRes, req) {
146
+ const origin = req.headers.origin;
147
+ proxyRes.headers["access-control-allow-origin"] = origin || "*";
148
+ proxyRes.headers["access-control-allow-credentials"] = "true";
149
+ proxyRes.headers["access-control-allow-headers"] = req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization";
150
+ proxyRes.headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
151
+ proxyRes.headers["access-control-expose-headers"] = "*";
152
+ }
137
153
  getTarget() {
138
154
  const target = this.targets[this.currentTargetIndex];
139
155
  this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
@@ -208,6 +224,7 @@ var ProxyServer = class {
208
224
  this.recordingId = id;
209
225
  this.replayId = null;
210
226
  this.currentSession = { id, recordings: [], websocketRecordings: [] };
227
+ this.requestSequenceMap.clear();
211
228
  console.log(`Switched to record mode with ID: ${id}`);
212
229
  }
213
230
  switchToReplayMode(id) {
@@ -218,6 +235,7 @@ var ProxyServer = class {
218
235
  this.replayId = id;
219
236
  this.recordingId = null;
220
237
  this.currentSession = null;
238
+ this.replaySequenceMap.clear();
221
239
  console.log(`Switched to replay mode with ID: ${id}`);
222
240
  }
223
241
  setupModeTimeout(timeout) {
@@ -249,6 +267,8 @@ var ProxyServer = class {
249
267
  return;
250
268
  }
251
269
  const key = getReqID(req);
270
+ const currentSequence = this.requestSequenceMap.get(key) || 0;
271
+ this.requestSequenceMap.set(key, currentSequence + 1);
252
272
  const record = {
253
273
  request: {
254
274
  method: req.method,
@@ -257,7 +277,8 @@ var ProxyServer = class {
257
277
  body: body || null
258
278
  },
259
279
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
260
- key
280
+ key,
281
+ sequence: currentSequence
261
282
  };
262
283
  this.currentSession.recordings.push(record);
263
284
  }
@@ -266,7 +287,9 @@ var ProxyServer = class {
266
287
  return;
267
288
  }
268
289
  const key = getReqID(req);
269
- const record = this.currentSession.recordings.find((r) => r.key === key);
290
+ const record = this.currentSession.recordings.findLast(
291
+ (r) => r.key === key && !r.response
292
+ );
270
293
  if (!record) {
271
294
  console.error("Request record not found for response:", key);
272
295
  return;
@@ -291,17 +314,31 @@ var ProxyServer = class {
291
314
  const filePath = getRecordingPath(this.recordingsDir, this.replayId);
292
315
  try {
293
316
  const session = await loadRecordingSession(filePath);
294
- const record = session.recordings.find((r) => r.key === key);
317
+ const currentSequence = this.replaySequenceMap.get(key) || 0;
318
+ const record = session.recordings.find(
319
+ (r) => r.key === key && r.sequence === currentSequence
320
+ );
295
321
  if (!record) {
296
- throw new Error(`No recording found for ${key}`);
322
+ throw new Error(
323
+ `No recording found for ${key} with sequence ${currentSequence}`
324
+ );
297
325
  }
298
326
  if (!record.response) {
299
327
  throw new Error("No response recorded for this request");
300
328
  }
329
+ this.replaySequenceMap.set(key, currentSequence + 1);
301
330
  const { statusCode, headers, body } = record.response;
302
- res.writeHead(statusCode, headers);
331
+ const origin = req.headers.origin;
332
+ const responseHeaders = {
333
+ ...headers,
334
+ "access-control-allow-origin": origin || "*",
335
+ "access-control-allow-credentials": "true"
336
+ };
337
+ res.writeHead(statusCode, responseHeaders);
303
338
  res.end(body);
304
- console.log(`Replayed: ${req.method} ${req.url}`);
339
+ console.log(
340
+ `Replayed: ${req.method} ${req.url} (sequence: ${currentSequence})`
341
+ );
305
342
  } catch (error) {
306
343
  this.handleReplayError(res, error, key, filePath);
307
344
  }
@@ -317,6 +354,9 @@ var ProxyServer = class {
317
354
  });
318
355
  }
319
356
  async handleRequest(req, res) {
357
+ if (req.method === "OPTIONS") {
358
+ return this.handleCorsPreflightRequest(req, res);
359
+ }
320
360
  if (req.url === CONTROL_ENDPOINT) {
321
361
  return this.handleControlRequest(req, res);
322
362
  }
@@ -325,23 +365,63 @@ var ProxyServer = class {
325
365
  }
326
366
  await this.handleProxyRequest(req, res);
327
367
  }
368
+ handleCorsPreflightRequest(req, res) {
369
+ const origin = req.headers.origin;
370
+ res.writeHead(HTTP_STATUS_OK, {
371
+ "Access-Control-Allow-Origin": origin || "*",
372
+ "Access-Control-Allow-Credentials": "true",
373
+ "Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
374
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
375
+ "Access-Control-Max-Age": "86400"
376
+ // 24 hours
377
+ });
378
+ res.end();
379
+ }
328
380
  async handleProxyRequest(req, res) {
329
381
  const target = this.getTarget();
330
382
  console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
331
383
  if (this.mode === Modes.record) {
332
- await this.bufferRequestForRecord(req);
384
+ await this.bufferAndProxyRequest(req, res, target);
385
+ } else {
386
+ this.proxy.web(req, res, { target });
333
387
  }
334
- this.proxy.web(req, res, { target });
335
388
  }
336
- async bufferRequestForRecord(req) {
389
+ async bufferAndProxyRequest(req, res, target) {
337
390
  const chunks = [];
338
391
  req.on("data", (chunk) => {
339
392
  chunks.push(chunk);
340
393
  });
341
- req.on("end", async () => {
342
- const body = Buffer.concat(chunks).toString("utf8");
343
- await this.saveRequestRecord(req, body);
394
+ await new Promise((resolve) => {
395
+ req.on("end", () => resolve());
344
396
  });
397
+ const body = Buffer.concat(chunks).toString("utf8");
398
+ await this.saveRequestRecord(req, body);
399
+ const targetUrl = new URL(target);
400
+ const isHttps = targetUrl.protocol === "https:";
401
+ const requestModule = isHttps ? https : http;
402
+ const defaultPort = isHttps ? 443 : 80;
403
+ const proxyReq = requestModule.request(
404
+ {
405
+ hostname: targetUrl.hostname,
406
+ port: targetUrl.port || defaultPort,
407
+ path: req.url,
408
+ method: req.method,
409
+ headers: req.headers
410
+ },
411
+ (proxyRes) => {
412
+ this.addCorsHeaders(proxyRes, req);
413
+ this.recordResponse(req, proxyRes);
414
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
415
+ proxyRes.pipe(res);
416
+ }
417
+ );
418
+ proxyReq.on("error", (err) => {
419
+ this.handleProxyError(err, req, res);
420
+ });
421
+ if (chunks.length > 0) {
422
+ proxyReq.write(Buffer.concat(chunks));
423
+ }
424
+ proxyReq.end();
345
425
  }
346
426
  handleUpgrade(req, socket, head) {
347
427
  if (this.mode === Modes.replay) {
@@ -1,3 +1,3 @@
1
1
  import '@playwright/test';
2
- export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-DfFpm8mB.cjs';
2
+ export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-De4mgziH.cjs';
3
3
  import 'node:http';
@@ -1,3 +1,3 @@
1
1
  import '@playwright/test';
2
- export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-DfFpm8mB.js';
2
+ export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-De4mgziH.js';
3
3
  import 'node:http';
package/dist/proxy.js CHANGED
@@ -2,6 +2,7 @@ import path from 'path';
2
2
  import { Command } from 'commander';
3
3
  import fs from 'fs/promises';
4
4
  import http from 'http';
5
+ import https from 'https';
5
6
  import httpProxy from 'http-proxy';
6
7
  import { WebSocket, WebSocketServer } from 'ws';
7
8
  import filenamify from 'filenamify';
@@ -117,6 +118,10 @@ var ProxyServer = class {
117
118
  proxy;
118
119
  currentSession;
119
120
  recordingsDir;
121
+ requestSequenceMap;
122
+ // Track sequence per request key
123
+ replaySequenceMap;
124
+ // Track replay position per request key
120
125
  constructor(targets2, recordingsDir2) {
121
126
  this.targets = targets2;
122
127
  this.currentTargetIndex = 0;
@@ -126,6 +131,8 @@ var ProxyServer = class {
126
131
  this.modeTimeout = null;
127
132
  this.currentSession = null;
128
133
  this.recordingsDir = recordingsDir2;
134
+ this.requestSequenceMap = /* @__PURE__ */ new Map();
135
+ this.replaySequenceMap = /* @__PURE__ */ new Map();
129
136
  this.proxy = httpProxy.createProxyServer({
130
137
  secure: false,
131
138
  changeOrigin: true
@@ -164,10 +171,19 @@ var ProxyServer = class {
164
171
  res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
165
172
  }
166
173
  handleProxyResponse(proxyRes, req) {
174
+ this.addCorsHeaders(proxyRes, req);
167
175
  if (this.mode === Modes.record && this.recordingId) {
168
176
  this.recordResponse(req, proxyRes);
169
177
  }
170
178
  }
179
+ addCorsHeaders(proxyRes, req) {
180
+ const origin = req.headers.origin;
181
+ proxyRes.headers["access-control-allow-origin"] = origin || "*";
182
+ proxyRes.headers["access-control-allow-credentials"] = "true";
183
+ proxyRes.headers["access-control-allow-headers"] = req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization";
184
+ proxyRes.headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
185
+ proxyRes.headers["access-control-expose-headers"] = "*";
186
+ }
171
187
  getTarget() {
172
188
  const target = this.targets[this.currentTargetIndex];
173
189
  this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
@@ -242,6 +258,7 @@ var ProxyServer = class {
242
258
  this.recordingId = id;
243
259
  this.replayId = null;
244
260
  this.currentSession = { id, recordings: [], websocketRecordings: [] };
261
+ this.requestSequenceMap.clear();
245
262
  console.log(`Switched to record mode with ID: ${id}`);
246
263
  }
247
264
  switchToReplayMode(id) {
@@ -252,6 +269,7 @@ var ProxyServer = class {
252
269
  this.replayId = id;
253
270
  this.recordingId = null;
254
271
  this.currentSession = null;
272
+ this.replaySequenceMap.clear();
255
273
  console.log(`Switched to replay mode with ID: ${id}`);
256
274
  }
257
275
  setupModeTimeout(timeout) {
@@ -283,6 +301,8 @@ var ProxyServer = class {
283
301
  return;
284
302
  }
285
303
  const key = getReqID(req);
304
+ const currentSequence = this.requestSequenceMap.get(key) || 0;
305
+ this.requestSequenceMap.set(key, currentSequence + 1);
286
306
  const record = {
287
307
  request: {
288
308
  method: req.method,
@@ -291,7 +311,8 @@ var ProxyServer = class {
291
311
  body: body || null
292
312
  },
293
313
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
294
- key
314
+ key,
315
+ sequence: currentSequence
295
316
  };
296
317
  this.currentSession.recordings.push(record);
297
318
  }
@@ -300,7 +321,9 @@ var ProxyServer = class {
300
321
  return;
301
322
  }
302
323
  const key = getReqID(req);
303
- const record = this.currentSession.recordings.find((r) => r.key === key);
324
+ const record = this.currentSession.recordings.findLast(
325
+ (r) => r.key === key && !r.response
326
+ );
304
327
  if (!record) {
305
328
  console.error("Request record not found for response:", key);
306
329
  return;
@@ -325,17 +348,31 @@ var ProxyServer = class {
325
348
  const filePath = getRecordingPath(this.recordingsDir, this.replayId);
326
349
  try {
327
350
  const session = await loadRecordingSession(filePath);
328
- const record = session.recordings.find((r) => r.key === key);
351
+ const currentSequence = this.replaySequenceMap.get(key) || 0;
352
+ const record = session.recordings.find(
353
+ (r) => r.key === key && r.sequence === currentSequence
354
+ );
329
355
  if (!record) {
330
- throw new Error(`No recording found for ${key}`);
356
+ throw new Error(
357
+ `No recording found for ${key} with sequence ${currentSequence}`
358
+ );
331
359
  }
332
360
  if (!record.response) {
333
361
  throw new Error("No response recorded for this request");
334
362
  }
363
+ this.replaySequenceMap.set(key, currentSequence + 1);
335
364
  const { statusCode, headers, body } = record.response;
336
- res.writeHead(statusCode, headers);
365
+ const origin = req.headers.origin;
366
+ const responseHeaders = {
367
+ ...headers,
368
+ "access-control-allow-origin": origin || "*",
369
+ "access-control-allow-credentials": "true"
370
+ };
371
+ res.writeHead(statusCode, responseHeaders);
337
372
  res.end(body);
338
- console.log(`Replayed: ${req.method} ${req.url}`);
373
+ console.log(
374
+ `Replayed: ${req.method} ${req.url} (sequence: ${currentSequence})`
375
+ );
339
376
  } catch (error) {
340
377
  this.handleReplayError(res, error, key, filePath);
341
378
  }
@@ -351,6 +388,9 @@ var ProxyServer = class {
351
388
  });
352
389
  }
353
390
  async handleRequest(req, res) {
391
+ if (req.method === "OPTIONS") {
392
+ return this.handleCorsPreflightRequest(req, res);
393
+ }
354
394
  if (req.url === CONTROL_ENDPOINT) {
355
395
  return this.handleControlRequest(req, res);
356
396
  }
@@ -359,23 +399,63 @@ var ProxyServer = class {
359
399
  }
360
400
  await this.handleProxyRequest(req, res);
361
401
  }
402
+ handleCorsPreflightRequest(req, res) {
403
+ const origin = req.headers.origin;
404
+ res.writeHead(HTTP_STATUS_OK, {
405
+ "Access-Control-Allow-Origin": origin || "*",
406
+ "Access-Control-Allow-Credentials": "true",
407
+ "Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
408
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
409
+ "Access-Control-Max-Age": "86400"
410
+ // 24 hours
411
+ });
412
+ res.end();
413
+ }
362
414
  async handleProxyRequest(req, res) {
363
415
  const target = this.getTarget();
364
416
  console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
365
417
  if (this.mode === Modes.record) {
366
- await this.bufferRequestForRecord(req);
418
+ await this.bufferAndProxyRequest(req, res, target);
419
+ } else {
420
+ this.proxy.web(req, res, { target });
367
421
  }
368
- this.proxy.web(req, res, { target });
369
422
  }
370
- async bufferRequestForRecord(req) {
423
+ async bufferAndProxyRequest(req, res, target) {
371
424
  const chunks = [];
372
425
  req.on("data", (chunk) => {
373
426
  chunks.push(chunk);
374
427
  });
375
- req.on("end", async () => {
376
- const body = Buffer.concat(chunks).toString("utf8");
377
- await this.saveRequestRecord(req, body);
428
+ await new Promise((resolve) => {
429
+ req.on("end", () => resolve());
378
430
  });
431
+ const body = Buffer.concat(chunks).toString("utf8");
432
+ await this.saveRequestRecord(req, body);
433
+ const targetUrl = new URL(target);
434
+ const isHttps = targetUrl.protocol === "https:";
435
+ const requestModule = isHttps ? https : http;
436
+ const defaultPort = isHttps ? 443 : 80;
437
+ const proxyReq = requestModule.request(
438
+ {
439
+ hostname: targetUrl.hostname,
440
+ port: targetUrl.port || defaultPort,
441
+ path: req.url,
442
+ method: req.method,
443
+ headers: req.headers
444
+ },
445
+ (proxyRes) => {
446
+ this.addCorsHeaders(proxyRes, req);
447
+ this.recordResponse(req, proxyRes);
448
+ res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
449
+ proxyRes.pipe(res);
450
+ }
451
+ );
452
+ proxyReq.on("error", (err) => {
453
+ this.handleProxyError(err, req, res);
454
+ });
455
+ if (chunks.length > 0) {
456
+ proxyReq.write(Buffer.concat(chunks));
457
+ }
458
+ proxyReq.end();
379
459
  }
380
460
  handleUpgrade(req, socket, head) {
381
461
  if (this.mode === Modes.replay) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "test-proxy-recorder",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright testing framework.",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",