test-proxy-recorder 0.1.6 → 0.1.7

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/dist/index.cjs CHANGED
@@ -144,11 +144,10 @@ var ProxyServer = class {
144
144
  return;
145
145
  }
146
146
  if (!res.headersSent) {
147
- const origin = req.headers.origin;
147
+ const corsHeaders = this.getCorsHeaders(req);
148
148
  res.writeHead(HTTP_STATUS_BAD_GATEWAY, {
149
149
  "Content-Type": "application/json",
150
- "Access-Control-Allow-Origin": origin || "*",
151
- "Access-Control-Allow-Credentials": "true"
150
+ ...corsHeaders
152
151
  });
153
152
  }
154
153
  res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
@@ -159,24 +158,51 @@ var ProxyServer = class {
159
158
  this.recordResponse(req, proxyRes);
160
159
  }
161
160
  }
162
- addCorsHeaders(proxyRes, req) {
161
+ /**
162
+ * Get CORS headers for a given request
163
+ * @param req The incoming HTTP request
164
+ * @returns An object containing CORS headers
165
+ */
166
+ getCorsHeaders(req) {
163
167
  const origin = req.headers.origin;
164
- proxyRes.headers["access-control-allow-origin"] = origin || "*";
165
- proxyRes.headers["access-control-allow-credentials"] = "true";
166
- proxyRes.headers["access-control-allow-headers"] = req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization";
167
- proxyRes.headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
168
- proxyRes.headers["access-control-expose-headers"] = "*";
168
+ return {
169
+ "access-control-allow-origin": origin || "*",
170
+ "access-control-allow-credentials": "true",
171
+ "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
172
+ "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
173
+ "access-control-expose-headers": "*"
174
+ };
175
+ }
176
+ addCorsHeaders(proxyRes, req) {
177
+ const corsHeaders = this.getCorsHeaders(req);
178
+ Object.assign(proxyRes.headers, corsHeaders);
169
179
  }
170
180
  getTarget() {
171
181
  const target = this.targets[this.currentTargetIndex];
172
182
  this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
173
183
  return target;
174
184
  }
185
+ parseGetParams(req) {
186
+ const url = new URL(req.url || "", `http://${req.headers.host}`);
187
+ const mode = url.searchParams.get("mode");
188
+ const id = url.searchParams.get("id") || void 0;
189
+ const timeoutParam = url.searchParams.get("timeout");
190
+ const timeout = timeoutParam ? Number.parseInt(timeoutParam, 10) : void 0;
191
+ if (!mode) {
192
+ throw new Error("Mode parameter is required");
193
+ }
194
+ return { mode, id, timeout };
195
+ }
175
196
  async handleControlRequest(req, res) {
176
197
  try {
177
- const body = await readRequestBody(req);
178
- console.log("MODE CHANGE", body);
179
- const data = JSON.parse(body);
198
+ let data;
199
+ if (req.method === "GET") {
200
+ data = this.parseGetParams(req);
201
+ } else {
202
+ const body = await readRequestBody(req);
203
+ console.log("MODE CHANGE (POST)", body);
204
+ data = JSON.parse(body);
205
+ }
180
206
  const { mode, id, timeout: requestTimeout } = data;
181
207
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
182
208
  this.clearModeTimeout();
@@ -270,10 +296,6 @@ var ProxyServer = class {
270
296
  console.log("No current session to save");
271
297
  return;
272
298
  }
273
- if (this.currentSession.recordings.length === 0 && this.currentSession.websocketRecordings.length === 0) {
274
- console.log("Session has no recordings, skipping save");
275
- return;
276
- }
277
299
  if (filterIncomplete) {
278
300
  const incompleteCount = this.currentSession.recordings.filter(
279
301
  (r) => !r.response
@@ -298,8 +320,6 @@ var ProxyServer = class {
298
320
  return;
299
321
  }
300
322
  const key = getReqID(req);
301
- const currentSequence = this.requestSequenceMap.get(key) || 0;
302
- this.requestSequenceMap.set(key, currentSequence + 1);
303
323
  const record = {
304
324
  request: {
305
325
  method: req.method,
@@ -309,12 +329,13 @@ var ProxyServer = class {
309
329
  },
310
330
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
311
331
  key,
312
- sequence: currentSequence
332
+ sequence: -1
333
+ // Temporary, will be set when response arrives
313
334
  };
314
335
  this.currentSession.recordings.push(record);
315
336
  console.log(
316
337
  // eslint-disable-next-line sonarjs/no-nested-template-literals
317
- `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, seq: ${currentSequence}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
338
+ `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
318
339
  );
319
340
  }
320
341
  updateRequestBodySync(req, body) {
@@ -360,7 +381,6 @@ var ProxyServer = class {
360
381
  headers: proxyRes.headers,
361
382
  body: body || null
362
383
  };
363
- await this.saveCurrentSession();
364
384
  console.log(`Recorded: ${req.method} ${req.url}`);
365
385
  });
366
386
  }
@@ -398,9 +418,12 @@ var ProxyServer = class {
398
418
  headers: proxyRes.headers,
399
419
  body: body || null
400
420
  };
401
- await this.saveCurrentSession();
421
+ record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
422
+ const currentSequence = this.requestSequenceMap.get(key) || 0;
423
+ record.sequence = currentSequence;
424
+ this.requestSequenceMap.set(key, currentSequence + 1);
402
425
  console.log(
403
- `recordResponseData: Recorded response for ${req.method} ${req.url}`
426
+ `recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
404
427
  );
405
428
  return true;
406
429
  }
@@ -410,19 +433,22 @@ var ProxyServer = class {
410
433
  try {
411
434
  const session = await loadRecordingSession(filePath);
412
435
  const host = req.headers.host || "unknown";
413
- const recordsWithKey = session.recordings.filter(
414
- (r) => r.key === key && r.response
415
- );
436
+ const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
416
437
  if (recordsWithKey.length === 0) {
417
438
  throw new Error(
418
439
  `No recording found for ${key} at ${req.method} ${host}${req.url}`
419
440
  );
420
441
  }
421
442
  const usageCount = this.replaySequenceMap.get(key) || 0;
422
- const recordIndex = usageCount % recordsWithKey.length;
423
- const record = recordsWithKey[recordIndex];
443
+ let record;
444
+ if (recordsWithKey.length > 1) {
445
+ record = recordsWithKey[recordsWithKey.length - 1];
446
+ } else {
447
+ const recordIndex = usageCount % recordsWithKey.length;
448
+ record = recordsWithKey[recordIndex];
449
+ }
424
450
  console.log(
425
- `Replaying ${req.method} ${req.url} (usage: ${usageCount}, using recording ${recordIndex}/${recordsWithKey.length})`
451
+ `Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
426
452
  );
427
453
  this.replaySequenceMap.set(key, usageCount + 1);
428
454
  if (!record.response) {
@@ -431,14 +457,9 @@ var ProxyServer = class {
431
457
  );
432
458
  }
433
459
  const { statusCode, headers, body } = record.response;
434
- const origin = req.headers.origin;
435
460
  const responseHeaders = {
436
461
  ...headers,
437
- "access-control-allow-origin": origin || "*",
438
- "access-control-allow-credentials": "true",
439
- "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
440
- "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
441
- "access-control-expose-headers": "*"
462
+ ...this.getCorsHeaders(req)
442
463
  };
443
464
  res.writeHead(statusCode, responseHeaders);
444
465
  res.end(body);
@@ -449,11 +470,10 @@ var ProxyServer = class {
449
470
  handleReplayError(req, res, err, key, filePath) {
450
471
  const isFileNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
451
472
  console.error("Replay error:", err);
452
- const origin = req.headers.origin;
473
+ const corsHeaders = this.getCorsHeaders(req);
453
474
  res.writeHead(HTTP_STATUS_NOT_FOUND, {
454
475
  "Content-Type": "application/json",
455
- "Access-Control-Allow-Origin": origin || "*",
456
- "Access-Control-Allow-Credentials": "true"
476
+ ...corsHeaders
457
477
  });
458
478
  res.end(
459
479
  JSON.stringify({
@@ -468,7 +488,8 @@ var ProxyServer = class {
468
488
  if (req.method === "OPTIONS") {
469
489
  return this.handleCorsPreflightRequest(req, res);
470
490
  }
471
- if (req.url === CONTROL_ENDPOINT) {
491
+ const urlPath = req.url?.split("?")[0] || "";
492
+ if (urlPath === CONTROL_ENDPOINT) {
472
493
  return this.handleControlRequest(req, res);
473
494
  }
474
495
  if (this.mode === Modes.replay) {
@@ -477,12 +498,9 @@ var ProxyServer = class {
477
498
  await this.handleProxyRequest(req, res);
478
499
  }
479
500
  handleCorsPreflightRequest(req, res) {
480
- const origin = req.headers.origin;
501
+ const corsHeaders = this.getCorsHeaders(req);
481
502
  res.writeHead(HTTP_STATUS_OK, {
482
- "Access-Control-Allow-Origin": origin || "*",
483
- "Access-Control-Allow-Credentials": "true",
484
- "Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
485
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
503
+ ...corsHeaders,
486
504
  "Access-Control-Max-Age": "86400"
487
505
  // 24 hours
488
506
  });
@@ -542,14 +560,9 @@ var ProxyServer = class {
542
560
  proxyRes,
543
561
  responseBody.toString("utf8")
544
562
  );
545
- const origin = req.headers.origin;
546
563
  const responseHeaders = {
547
564
  ...proxyRes.headers,
548
- "access-control-allow-origin": origin || "*",
549
- "access-control-allow-credentials": "true",
550
- "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
551
- "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
552
- "access-control-expose-headers": "*"
565
+ ...this.getCorsHeaders(req)
553
566
  };
554
567
  res.writeHead(proxyRes.statusCode || 200, responseHeaders);
555
568
  res.end(responseBody);
@@ -614,9 +627,6 @@ var ProxyServer = class {
614
627
  if (backendWs.readyState === ws.WebSocket.OPEN) {
615
628
  backendWs.send(message);
616
629
  }
617
- this.saveCurrentSession().catch((error) => {
618
- console.error("Failed to save WebSocket recording:", error);
619
- });
620
630
  });
621
631
  backendWs.on("message", (data) => {
622
632
  const message = data.toString();
@@ -628,9 +638,6 @@ var ProxyServer = class {
628
638
  if (clientWs.readyState === ws.WebSocket.OPEN) {
629
639
  clientWs.send(message);
630
640
  }
631
- this.saveCurrentSession().catch((error) => {
632
- console.error("Failed to save WebSocket recording:", error);
633
- });
634
641
  });
635
642
  clientWs.on("error", (err) => {
636
643
  console.error("Client WebSocket error:", err);
package/dist/index.d.cts CHANGED
@@ -20,8 +20,15 @@ declare class ProxyServer {
20
20
  private setupProxyEventHandlers;
21
21
  private handleProxyError;
22
22
  private handleProxyResponse;
23
+ /**
24
+ * Get CORS headers for a given request
25
+ * @param req The incoming HTTP request
26
+ * @returns An object containing CORS headers
27
+ */
28
+ private getCorsHeaders;
23
29
  private addCorsHeaders;
24
30
  private getTarget;
31
+ private parseGetParams;
25
32
  private handleControlRequest;
26
33
  private clearModeTimeout;
27
34
  private switchMode;
package/dist/index.d.ts CHANGED
@@ -20,8 +20,15 @@ declare class ProxyServer {
20
20
  private setupProxyEventHandlers;
21
21
  private handleProxyError;
22
22
  private handleProxyResponse;
23
+ /**
24
+ * Get CORS headers for a given request
25
+ * @param req The incoming HTTP request
26
+ * @returns An object containing CORS headers
27
+ */
28
+ private getCorsHeaders;
23
29
  private addCorsHeaders;
24
30
  private getTarget;
31
+ private parseGetParams;
25
32
  private handleControlRequest;
26
33
  private clearModeTimeout;
27
34
  private switchMode;
package/dist/index.mjs CHANGED
@@ -133,11 +133,10 @@ var ProxyServer = class {
133
133
  return;
134
134
  }
135
135
  if (!res.headersSent) {
136
- const origin = req.headers.origin;
136
+ const corsHeaders = this.getCorsHeaders(req);
137
137
  res.writeHead(HTTP_STATUS_BAD_GATEWAY, {
138
138
  "Content-Type": "application/json",
139
- "Access-Control-Allow-Origin": origin || "*",
140
- "Access-Control-Allow-Credentials": "true"
139
+ ...corsHeaders
141
140
  });
142
141
  }
143
142
  res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
@@ -148,24 +147,51 @@ var ProxyServer = class {
148
147
  this.recordResponse(req, proxyRes);
149
148
  }
150
149
  }
151
- addCorsHeaders(proxyRes, req) {
150
+ /**
151
+ * Get CORS headers for a given request
152
+ * @param req The incoming HTTP request
153
+ * @returns An object containing CORS headers
154
+ */
155
+ getCorsHeaders(req) {
152
156
  const origin = req.headers.origin;
153
- proxyRes.headers["access-control-allow-origin"] = origin || "*";
154
- proxyRes.headers["access-control-allow-credentials"] = "true";
155
- proxyRes.headers["access-control-allow-headers"] = req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization";
156
- proxyRes.headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
157
- proxyRes.headers["access-control-expose-headers"] = "*";
157
+ return {
158
+ "access-control-allow-origin": origin || "*",
159
+ "access-control-allow-credentials": "true",
160
+ "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
161
+ "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
162
+ "access-control-expose-headers": "*"
163
+ };
164
+ }
165
+ addCorsHeaders(proxyRes, req) {
166
+ const corsHeaders = this.getCorsHeaders(req);
167
+ Object.assign(proxyRes.headers, corsHeaders);
158
168
  }
159
169
  getTarget() {
160
170
  const target = this.targets[this.currentTargetIndex];
161
171
  this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
162
172
  return target;
163
173
  }
174
+ parseGetParams(req) {
175
+ const url = new URL(req.url || "", `http://${req.headers.host}`);
176
+ const mode = url.searchParams.get("mode");
177
+ const id = url.searchParams.get("id") || void 0;
178
+ const timeoutParam = url.searchParams.get("timeout");
179
+ const timeout = timeoutParam ? Number.parseInt(timeoutParam, 10) : void 0;
180
+ if (!mode) {
181
+ throw new Error("Mode parameter is required");
182
+ }
183
+ return { mode, id, timeout };
184
+ }
164
185
  async handleControlRequest(req, res) {
165
186
  try {
166
- const body = await readRequestBody(req);
167
- console.log("MODE CHANGE", body);
168
- const data = JSON.parse(body);
187
+ let data;
188
+ if (req.method === "GET") {
189
+ data = this.parseGetParams(req);
190
+ } else {
191
+ const body = await readRequestBody(req);
192
+ console.log("MODE CHANGE (POST)", body);
193
+ data = JSON.parse(body);
194
+ }
169
195
  const { mode, id, timeout: requestTimeout } = data;
170
196
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
171
197
  this.clearModeTimeout();
@@ -259,10 +285,6 @@ var ProxyServer = class {
259
285
  console.log("No current session to save");
260
286
  return;
261
287
  }
262
- if (this.currentSession.recordings.length === 0 && this.currentSession.websocketRecordings.length === 0) {
263
- console.log("Session has no recordings, skipping save");
264
- return;
265
- }
266
288
  if (filterIncomplete) {
267
289
  const incompleteCount = this.currentSession.recordings.filter(
268
290
  (r) => !r.response
@@ -287,8 +309,6 @@ var ProxyServer = class {
287
309
  return;
288
310
  }
289
311
  const key = getReqID(req);
290
- const currentSequence = this.requestSequenceMap.get(key) || 0;
291
- this.requestSequenceMap.set(key, currentSequence + 1);
292
312
  const record = {
293
313
  request: {
294
314
  method: req.method,
@@ -298,12 +318,13 @@ var ProxyServer = class {
298
318
  },
299
319
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
300
320
  key,
301
- sequence: currentSequence
321
+ sequence: -1
322
+ // Temporary, will be set when response arrives
302
323
  };
303
324
  this.currentSession.recordings.push(record);
304
325
  console.log(
305
326
  // eslint-disable-next-line sonarjs/no-nested-template-literals
306
- `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, seq: ${currentSequence}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
327
+ `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
307
328
  );
308
329
  }
309
330
  updateRequestBodySync(req, body) {
@@ -349,7 +370,6 @@ var ProxyServer = class {
349
370
  headers: proxyRes.headers,
350
371
  body: body || null
351
372
  };
352
- await this.saveCurrentSession();
353
373
  console.log(`Recorded: ${req.method} ${req.url}`);
354
374
  });
355
375
  }
@@ -387,9 +407,12 @@ var ProxyServer = class {
387
407
  headers: proxyRes.headers,
388
408
  body: body || null
389
409
  };
390
- await this.saveCurrentSession();
410
+ record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
411
+ const currentSequence = this.requestSequenceMap.get(key) || 0;
412
+ record.sequence = currentSequence;
413
+ this.requestSequenceMap.set(key, currentSequence + 1);
391
414
  console.log(
392
- `recordResponseData: Recorded response for ${req.method} ${req.url}`
415
+ `recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
393
416
  );
394
417
  return true;
395
418
  }
@@ -399,19 +422,22 @@ var ProxyServer = class {
399
422
  try {
400
423
  const session = await loadRecordingSession(filePath);
401
424
  const host = req.headers.host || "unknown";
402
- const recordsWithKey = session.recordings.filter(
403
- (r) => r.key === key && r.response
404
- );
425
+ const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
405
426
  if (recordsWithKey.length === 0) {
406
427
  throw new Error(
407
428
  `No recording found for ${key} at ${req.method} ${host}${req.url}`
408
429
  );
409
430
  }
410
431
  const usageCount = this.replaySequenceMap.get(key) || 0;
411
- const recordIndex = usageCount % recordsWithKey.length;
412
- const record = recordsWithKey[recordIndex];
432
+ let record;
433
+ if (recordsWithKey.length > 1) {
434
+ record = recordsWithKey[recordsWithKey.length - 1];
435
+ } else {
436
+ const recordIndex = usageCount % recordsWithKey.length;
437
+ record = recordsWithKey[recordIndex];
438
+ }
413
439
  console.log(
414
- `Replaying ${req.method} ${req.url} (usage: ${usageCount}, using recording ${recordIndex}/${recordsWithKey.length})`
440
+ `Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
415
441
  );
416
442
  this.replaySequenceMap.set(key, usageCount + 1);
417
443
  if (!record.response) {
@@ -420,14 +446,9 @@ var ProxyServer = class {
420
446
  );
421
447
  }
422
448
  const { statusCode, headers, body } = record.response;
423
- const origin = req.headers.origin;
424
449
  const responseHeaders = {
425
450
  ...headers,
426
- "access-control-allow-origin": origin || "*",
427
- "access-control-allow-credentials": "true",
428
- "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
429
- "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
430
- "access-control-expose-headers": "*"
451
+ ...this.getCorsHeaders(req)
431
452
  };
432
453
  res.writeHead(statusCode, responseHeaders);
433
454
  res.end(body);
@@ -438,11 +459,10 @@ var ProxyServer = class {
438
459
  handleReplayError(req, res, err, key, filePath) {
439
460
  const isFileNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
440
461
  console.error("Replay error:", err);
441
- const origin = req.headers.origin;
462
+ const corsHeaders = this.getCorsHeaders(req);
442
463
  res.writeHead(HTTP_STATUS_NOT_FOUND, {
443
464
  "Content-Type": "application/json",
444
- "Access-Control-Allow-Origin": origin || "*",
445
- "Access-Control-Allow-Credentials": "true"
465
+ ...corsHeaders
446
466
  });
447
467
  res.end(
448
468
  JSON.stringify({
@@ -457,7 +477,8 @@ var ProxyServer = class {
457
477
  if (req.method === "OPTIONS") {
458
478
  return this.handleCorsPreflightRequest(req, res);
459
479
  }
460
- if (req.url === CONTROL_ENDPOINT) {
480
+ const urlPath = req.url?.split("?")[0] || "";
481
+ if (urlPath === CONTROL_ENDPOINT) {
461
482
  return this.handleControlRequest(req, res);
462
483
  }
463
484
  if (this.mode === Modes.replay) {
@@ -466,12 +487,9 @@ var ProxyServer = class {
466
487
  await this.handleProxyRequest(req, res);
467
488
  }
468
489
  handleCorsPreflightRequest(req, res) {
469
- const origin = req.headers.origin;
490
+ const corsHeaders = this.getCorsHeaders(req);
470
491
  res.writeHead(HTTP_STATUS_OK, {
471
- "Access-Control-Allow-Origin": origin || "*",
472
- "Access-Control-Allow-Credentials": "true",
473
- "Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
474
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
492
+ ...corsHeaders,
475
493
  "Access-Control-Max-Age": "86400"
476
494
  // 24 hours
477
495
  });
@@ -531,14 +549,9 @@ var ProxyServer = class {
531
549
  proxyRes,
532
550
  responseBody.toString("utf8")
533
551
  );
534
- const origin = req.headers.origin;
535
552
  const responseHeaders = {
536
553
  ...proxyRes.headers,
537
- "access-control-allow-origin": origin || "*",
538
- "access-control-allow-credentials": "true",
539
- "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
540
- "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
541
- "access-control-expose-headers": "*"
554
+ ...this.getCorsHeaders(req)
542
555
  };
543
556
  res.writeHead(proxyRes.statusCode || 200, responseHeaders);
544
557
  res.end(responseBody);
@@ -603,9 +616,6 @@ var ProxyServer = class {
603
616
  if (backendWs.readyState === WebSocket.OPEN) {
604
617
  backendWs.send(message);
605
618
  }
606
- this.saveCurrentSession().catch((error) => {
607
- console.error("Failed to save WebSocket recording:", error);
608
- });
609
619
  });
610
620
  backendWs.on("message", (data) => {
611
621
  const message = data.toString();
@@ -617,9 +627,6 @@ var ProxyServer = class {
617
627
  if (clientWs.readyState === WebSocket.OPEN) {
618
628
  clientWs.send(message);
619
629
  }
620
- this.saveCurrentSession().catch((error) => {
621
- console.error("Failed to save WebSocket recording:", error);
622
- });
623
630
  });
624
631
  clientWs.on("error", (err) => {
625
632
  console.error("Client WebSocket error:", err);
package/dist/proxy.js CHANGED
@@ -167,11 +167,10 @@ var ProxyServer = class {
167
167
  return;
168
168
  }
169
169
  if (!res.headersSent) {
170
- const origin = req.headers.origin;
170
+ const corsHeaders = this.getCorsHeaders(req);
171
171
  res.writeHead(HTTP_STATUS_BAD_GATEWAY, {
172
172
  "Content-Type": "application/json",
173
- "Access-Control-Allow-Origin": origin || "*",
174
- "Access-Control-Allow-Credentials": "true"
173
+ ...corsHeaders
175
174
  });
176
175
  }
177
176
  res.end(JSON.stringify({ error: "Proxy error", message: err.message }));
@@ -182,24 +181,51 @@ var ProxyServer = class {
182
181
  this.recordResponse(req, proxyRes);
183
182
  }
184
183
  }
185
- addCorsHeaders(proxyRes, req) {
184
+ /**
185
+ * Get CORS headers for a given request
186
+ * @param req The incoming HTTP request
187
+ * @returns An object containing CORS headers
188
+ */
189
+ getCorsHeaders(req) {
186
190
  const origin = req.headers.origin;
187
- proxyRes.headers["access-control-allow-origin"] = origin || "*";
188
- proxyRes.headers["access-control-allow-credentials"] = "true";
189
- proxyRes.headers["access-control-allow-headers"] = req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization";
190
- proxyRes.headers["access-control-allow-methods"] = "GET, POST, PUT, DELETE, PATCH, OPTIONS";
191
- proxyRes.headers["access-control-expose-headers"] = "*";
191
+ return {
192
+ "access-control-allow-origin": origin || "*",
193
+ "access-control-allow-credentials": "true",
194
+ "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
195
+ "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
196
+ "access-control-expose-headers": "*"
197
+ };
198
+ }
199
+ addCorsHeaders(proxyRes, req) {
200
+ const corsHeaders = this.getCorsHeaders(req);
201
+ Object.assign(proxyRes.headers, corsHeaders);
192
202
  }
193
203
  getTarget() {
194
204
  const target = this.targets[this.currentTargetIndex];
195
205
  this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
196
206
  return target;
197
207
  }
208
+ parseGetParams(req) {
209
+ const url = new URL(req.url || "", `http://${req.headers.host}`);
210
+ const mode = url.searchParams.get("mode");
211
+ const id = url.searchParams.get("id") || void 0;
212
+ const timeoutParam = url.searchParams.get("timeout");
213
+ const timeout = timeoutParam ? Number.parseInt(timeoutParam, 10) : void 0;
214
+ if (!mode) {
215
+ throw new Error("Mode parameter is required");
216
+ }
217
+ return { mode, id, timeout };
218
+ }
198
219
  async handleControlRequest(req, res) {
199
220
  try {
200
- const body = await readRequestBody(req);
201
- console.log("MODE CHANGE", body);
202
- const data = JSON.parse(body);
221
+ let data;
222
+ if (req.method === "GET") {
223
+ data = this.parseGetParams(req);
224
+ } else {
225
+ const body = await readRequestBody(req);
226
+ console.log("MODE CHANGE (POST)", body);
227
+ data = JSON.parse(body);
228
+ }
203
229
  const { mode, id, timeout: requestTimeout } = data;
204
230
  const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
205
231
  this.clearModeTimeout();
@@ -293,10 +319,6 @@ var ProxyServer = class {
293
319
  console.log("No current session to save");
294
320
  return;
295
321
  }
296
- if (this.currentSession.recordings.length === 0 && this.currentSession.websocketRecordings.length === 0) {
297
- console.log("Session has no recordings, skipping save");
298
- return;
299
- }
300
322
  if (filterIncomplete) {
301
323
  const incompleteCount = this.currentSession.recordings.filter(
302
324
  (r) => !r.response
@@ -321,8 +343,6 @@ var ProxyServer = class {
321
343
  return;
322
344
  }
323
345
  const key = getReqID(req);
324
- const currentSequence = this.requestSequenceMap.get(key) || 0;
325
- this.requestSequenceMap.set(key, currentSequence + 1);
326
346
  const record = {
327
347
  request: {
328
348
  method: req.method,
@@ -332,12 +352,13 @@ var ProxyServer = class {
332
352
  },
333
353
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
334
354
  key,
335
- sequence: currentSequence
355
+ sequence: -1
356
+ // Temporary, will be set when response arrives
336
357
  };
337
358
  this.currentSession.recordings.push(record);
338
359
  console.log(
339
360
  // eslint-disable-next-line sonarjs/no-nested-template-literals
340
- `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, seq: ${currentSequence}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
361
+ `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
341
362
  );
342
363
  }
343
364
  updateRequestBodySync(req, body) {
@@ -383,7 +404,6 @@ var ProxyServer = class {
383
404
  headers: proxyRes.headers,
384
405
  body: body || null
385
406
  };
386
- await this.saveCurrentSession();
387
407
  console.log(`Recorded: ${req.method} ${req.url}`);
388
408
  });
389
409
  }
@@ -421,9 +441,12 @@ var ProxyServer = class {
421
441
  headers: proxyRes.headers,
422
442
  body: body || null
423
443
  };
424
- await this.saveCurrentSession();
444
+ record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
445
+ const currentSequence = this.requestSequenceMap.get(key) || 0;
446
+ record.sequence = currentSequence;
447
+ this.requestSequenceMap.set(key, currentSequence + 1);
425
448
  console.log(
426
- `recordResponseData: Recorded response for ${req.method} ${req.url}`
449
+ `recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
427
450
  );
428
451
  return true;
429
452
  }
@@ -433,19 +456,22 @@ var ProxyServer = class {
433
456
  try {
434
457
  const session = await loadRecordingSession(filePath);
435
458
  const host = req.headers.host || "unknown";
436
- const recordsWithKey = session.recordings.filter(
437
- (r) => r.key === key && r.response
438
- );
459
+ const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
439
460
  if (recordsWithKey.length === 0) {
440
461
  throw new Error(
441
462
  `No recording found for ${key} at ${req.method} ${host}${req.url}`
442
463
  );
443
464
  }
444
465
  const usageCount = this.replaySequenceMap.get(key) || 0;
445
- const recordIndex = usageCount % recordsWithKey.length;
446
- const record = recordsWithKey[recordIndex];
466
+ let record;
467
+ if (recordsWithKey.length > 1) {
468
+ record = recordsWithKey[recordsWithKey.length - 1];
469
+ } else {
470
+ const recordIndex = usageCount % recordsWithKey.length;
471
+ record = recordsWithKey[recordIndex];
472
+ }
447
473
  console.log(
448
- `Replaying ${req.method} ${req.url} (usage: ${usageCount}, using recording ${recordIndex}/${recordsWithKey.length})`
474
+ `Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
449
475
  );
450
476
  this.replaySequenceMap.set(key, usageCount + 1);
451
477
  if (!record.response) {
@@ -454,14 +480,9 @@ var ProxyServer = class {
454
480
  );
455
481
  }
456
482
  const { statusCode, headers, body } = record.response;
457
- const origin = req.headers.origin;
458
483
  const responseHeaders = {
459
484
  ...headers,
460
- "access-control-allow-origin": origin || "*",
461
- "access-control-allow-credentials": "true",
462
- "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
463
- "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
464
- "access-control-expose-headers": "*"
485
+ ...this.getCorsHeaders(req)
465
486
  };
466
487
  res.writeHead(statusCode, responseHeaders);
467
488
  res.end(body);
@@ -472,11 +493,10 @@ var ProxyServer = class {
472
493
  handleReplayError(req, res, err, key, filePath) {
473
494
  const isFileNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
474
495
  console.error("Replay error:", err);
475
- const origin = req.headers.origin;
496
+ const corsHeaders = this.getCorsHeaders(req);
476
497
  res.writeHead(HTTP_STATUS_NOT_FOUND, {
477
498
  "Content-Type": "application/json",
478
- "Access-Control-Allow-Origin": origin || "*",
479
- "Access-Control-Allow-Credentials": "true"
499
+ ...corsHeaders
480
500
  });
481
501
  res.end(
482
502
  JSON.stringify({
@@ -491,7 +511,8 @@ var ProxyServer = class {
491
511
  if (req.method === "OPTIONS") {
492
512
  return this.handleCorsPreflightRequest(req, res);
493
513
  }
494
- if (req.url === CONTROL_ENDPOINT) {
514
+ const urlPath = req.url?.split("?")[0] || "";
515
+ if (urlPath === CONTROL_ENDPOINT) {
495
516
  return this.handleControlRequest(req, res);
496
517
  }
497
518
  if (this.mode === Modes.replay) {
@@ -500,12 +521,9 @@ var ProxyServer = class {
500
521
  await this.handleProxyRequest(req, res);
501
522
  }
502
523
  handleCorsPreflightRequest(req, res) {
503
- const origin = req.headers.origin;
524
+ const corsHeaders = this.getCorsHeaders(req);
504
525
  res.writeHead(HTTP_STATUS_OK, {
505
- "Access-Control-Allow-Origin": origin || "*",
506
- "Access-Control-Allow-Credentials": "true",
507
- "Access-Control-Allow-Headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
508
- "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
526
+ ...corsHeaders,
509
527
  "Access-Control-Max-Age": "86400"
510
528
  // 24 hours
511
529
  });
@@ -565,14 +583,9 @@ var ProxyServer = class {
565
583
  proxyRes,
566
584
  responseBody.toString("utf8")
567
585
  );
568
- const origin = req.headers.origin;
569
586
  const responseHeaders = {
570
587
  ...proxyRes.headers,
571
- "access-control-allow-origin": origin || "*",
572
- "access-control-allow-credentials": "true",
573
- "access-control-allow-headers": req.headers["access-control-request-headers"] || "Origin, X-Requested-With, Content-Type, Accept, Authorization",
574
- "access-control-allow-methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
575
- "access-control-expose-headers": "*"
588
+ ...this.getCorsHeaders(req)
576
589
  };
577
590
  res.writeHead(proxyRes.statusCode || 200, responseHeaders);
578
591
  res.end(responseBody);
@@ -637,9 +650,6 @@ var ProxyServer = class {
637
650
  if (backendWs.readyState === WebSocket.OPEN) {
638
651
  backendWs.send(message);
639
652
  }
640
- this.saveCurrentSession().catch((error) => {
641
- console.error("Failed to save WebSocket recording:", error);
642
- });
643
653
  });
644
654
  backendWs.on("message", (data) => {
645
655
  const message = data.toString();
@@ -651,9 +661,6 @@ var ProxyServer = class {
651
661
  if (clientWs.readyState === WebSocket.OPEN) {
652
662
  clientWs.send(message);
653
663
  }
654
- this.saveCurrentSession().catch((error) => {
655
- console.error("Failed to save WebSocket recording:", error);
656
- });
657
664
  });
658
665
  clientWs.on("error", (err) => {
659
666
  console.error("Client WebSocket error:", err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "test-proxy-recorder",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
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",