test-proxy-recorder 0.1.6 → 0.1.8

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();
@@ -267,11 +293,6 @@ var ProxyServer = class {
267
293
  }
268
294
  async saveCurrentSession(filterIncomplete = false) {
269
295
  if (!this.currentSession) {
270
- console.log("No current session to save");
271
- return;
272
- }
273
- if (this.currentSession.recordings.length === 0 && this.currentSession.websocketRecordings.length === 0) {
274
- console.log("Session has no recordings, skipping save");
275
296
  return;
276
297
  }
277
298
  if (filterIncomplete) {
@@ -279,9 +300,6 @@ var ProxyServer = class {
279
300
  (r) => !r.response
280
301
  ).length;
281
302
  if (incompleteCount > 0) {
282
- console.log(
283
- `Removing ${incompleteCount} incomplete recording(s) without responses`
284
- );
285
303
  this.currentSession.recordings = this.currentSession.recordings.filter(
286
304
  (r) => r.response
287
305
  );
@@ -294,12 +312,9 @@ var ProxyServer = class {
294
312
  }
295
313
  saveRequestRecordSync(req, body) {
296
314
  if (!this.currentSession) {
297
- console.log("saveRequestRecordSync: No current session");
298
315
  return;
299
316
  }
300
317
  const key = getReqID(req);
301
- const currentSequence = this.requestSequenceMap.get(key) || 0;
302
- this.requestSequenceMap.set(key, currentSequence + 1);
303
318
  const record = {
304
319
  request: {
305
320
  method: req.method,
@@ -309,17 +324,17 @@ var ProxyServer = class {
309
324
  },
310
325
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
311
326
  key,
312
- sequence: currentSequence
327
+ sequence: -1
328
+ // Temporary, will be set when response arrives
313
329
  };
314
330
  this.currentSession.recordings.push(record);
315
331
  console.log(
316
332
  // 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})`
333
+ `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
318
334
  );
319
335
  }
320
336
  updateRequestBodySync(req, body) {
321
337
  if (!this.currentSession) {
322
- console.log("updateRequestBodySync: No current session");
323
338
  return;
324
339
  }
325
340
  const key = getReqID(req);
@@ -360,13 +375,11 @@ var ProxyServer = class {
360
375
  headers: proxyRes.headers,
361
376
  body: body || null
362
377
  };
363
- await this.saveCurrentSession();
364
378
  console.log(`Recorded: ${req.method} ${req.url}`);
365
379
  });
366
380
  }
367
381
  async recordResponseData(req, proxyRes, body) {
368
382
  if (!this.currentSession) {
369
- console.log("recordResponseData: No current session");
370
383
  return false;
371
384
  }
372
385
  const key = getReqID(req);
@@ -398,9 +411,12 @@ var ProxyServer = class {
398
411
  headers: proxyRes.headers,
399
412
  body: body || null
400
413
  };
401
- await this.saveCurrentSession();
414
+ record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
415
+ const currentSequence = this.requestSequenceMap.get(key) || 0;
416
+ record.sequence = currentSequence;
417
+ this.requestSequenceMap.set(key, currentSequence + 1);
402
418
  console.log(
403
- `recordResponseData: Recorded response for ${req.method} ${req.url}`
419
+ `recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
404
420
  );
405
421
  return true;
406
422
  }
@@ -410,9 +426,7 @@ var ProxyServer = class {
410
426
  try {
411
427
  const session = await loadRecordingSession(filePath);
412
428
  const host = req.headers.host || "unknown";
413
- const recordsWithKey = session.recordings.filter(
414
- (r) => r.key === key && r.response
415
- );
429
+ const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
416
430
  if (recordsWithKey.length === 0) {
417
431
  throw new Error(
418
432
  `No recording found for ${key} at ${req.method} ${host}${req.url}`
@@ -422,7 +436,7 @@ var ProxyServer = class {
422
436
  const recordIndex = usageCount % recordsWithKey.length;
423
437
  const record = recordsWithKey[recordIndex];
424
438
  console.log(
425
- `Replaying ${req.method} ${req.url} (usage: ${usageCount}, using recording ${recordIndex}/${recordsWithKey.length})`
439
+ `Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
426
440
  );
427
441
  this.replaySequenceMap.set(key, usageCount + 1);
428
442
  if (!record.response) {
@@ -431,14 +445,9 @@ var ProxyServer = class {
431
445
  );
432
446
  }
433
447
  const { statusCode, headers, body } = record.response;
434
- const origin = req.headers.origin;
435
448
  const responseHeaders = {
436
449
  ...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": "*"
450
+ ...this.getCorsHeaders(req)
442
451
  };
443
452
  res.writeHead(statusCode, responseHeaders);
444
453
  res.end(body);
@@ -449,11 +458,10 @@ var ProxyServer = class {
449
458
  handleReplayError(req, res, err, key, filePath) {
450
459
  const isFileNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
451
460
  console.error("Replay error:", err);
452
- const origin = req.headers.origin;
461
+ const corsHeaders = this.getCorsHeaders(req);
453
462
  res.writeHead(HTTP_STATUS_NOT_FOUND, {
454
463
  "Content-Type": "application/json",
455
- "Access-Control-Allow-Origin": origin || "*",
456
- "Access-Control-Allow-Credentials": "true"
464
+ ...corsHeaders
457
465
  });
458
466
  res.end(
459
467
  JSON.stringify({
@@ -468,7 +476,8 @@ var ProxyServer = class {
468
476
  if (req.method === "OPTIONS") {
469
477
  return this.handleCorsPreflightRequest(req, res);
470
478
  }
471
- if (req.url === CONTROL_ENDPOINT) {
479
+ const urlPath = req.url?.split("?")[0] || "";
480
+ if (urlPath === CONTROL_ENDPOINT) {
472
481
  return this.handleControlRequest(req, res);
473
482
  }
474
483
  if (this.mode === Modes.replay) {
@@ -477,12 +486,9 @@ var ProxyServer = class {
477
486
  await this.handleProxyRequest(req, res);
478
487
  }
479
488
  handleCorsPreflightRequest(req, res) {
480
- const origin = req.headers.origin;
489
+ const corsHeaders = this.getCorsHeaders(req);
481
490
  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",
491
+ ...corsHeaders,
486
492
  "Access-Control-Max-Age": "86400"
487
493
  // 24 hours
488
494
  });
@@ -542,14 +548,9 @@ var ProxyServer = class {
542
548
  proxyRes,
543
549
  responseBody.toString("utf8")
544
550
  );
545
- const origin = req.headers.origin;
546
551
  const responseHeaders = {
547
552
  ...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": "*"
553
+ ...this.getCorsHeaders(req)
553
554
  };
554
555
  res.writeHead(proxyRes.statusCode || 200, responseHeaders);
555
556
  res.end(responseBody);
@@ -614,9 +615,6 @@ var ProxyServer = class {
614
615
  if (backendWs.readyState === ws.WebSocket.OPEN) {
615
616
  backendWs.send(message);
616
617
  }
617
- this.saveCurrentSession().catch((error) => {
618
- console.error("Failed to save WebSocket recording:", error);
619
- });
620
618
  });
621
619
  backendWs.on("message", (data) => {
622
620
  const message = data.toString();
@@ -628,9 +626,6 @@ var ProxyServer = class {
628
626
  if (clientWs.readyState === ws.WebSocket.OPEN) {
629
627
  clientWs.send(message);
630
628
  }
631
- this.saveCurrentSession().catch((error) => {
632
- console.error("Failed to save WebSocket recording:", error);
633
- });
634
629
  });
635
630
  clientWs.on("error", (err) => {
636
631
  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();
@@ -256,11 +282,6 @@ var ProxyServer = class {
256
282
  }
257
283
  async saveCurrentSession(filterIncomplete = false) {
258
284
  if (!this.currentSession) {
259
- console.log("No current session to save");
260
- return;
261
- }
262
- if (this.currentSession.recordings.length === 0 && this.currentSession.websocketRecordings.length === 0) {
263
- console.log("Session has no recordings, skipping save");
264
285
  return;
265
286
  }
266
287
  if (filterIncomplete) {
@@ -268,9 +289,6 @@ var ProxyServer = class {
268
289
  (r) => !r.response
269
290
  ).length;
270
291
  if (incompleteCount > 0) {
271
- console.log(
272
- `Removing ${incompleteCount} incomplete recording(s) without responses`
273
- );
274
292
  this.currentSession.recordings = this.currentSession.recordings.filter(
275
293
  (r) => r.response
276
294
  );
@@ -283,12 +301,9 @@ var ProxyServer = class {
283
301
  }
284
302
  saveRequestRecordSync(req, body) {
285
303
  if (!this.currentSession) {
286
- console.log("saveRequestRecordSync: No current session");
287
304
  return;
288
305
  }
289
306
  const key = getReqID(req);
290
- const currentSequence = this.requestSequenceMap.get(key) || 0;
291
- this.requestSequenceMap.set(key, currentSequence + 1);
292
307
  const record = {
293
308
  request: {
294
309
  method: req.method,
@@ -298,17 +313,17 @@ var ProxyServer = class {
298
313
  },
299
314
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
300
315
  key,
301
- sequence: currentSequence
316
+ sequence: -1
317
+ // Temporary, will be set when response arrives
302
318
  };
303
319
  this.currentSession.recordings.push(record);
304
320
  console.log(
305
321
  // 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})`
322
+ `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
307
323
  );
308
324
  }
309
325
  updateRequestBodySync(req, body) {
310
326
  if (!this.currentSession) {
311
- console.log("updateRequestBodySync: No current session");
312
327
  return;
313
328
  }
314
329
  const key = getReqID(req);
@@ -349,13 +364,11 @@ var ProxyServer = class {
349
364
  headers: proxyRes.headers,
350
365
  body: body || null
351
366
  };
352
- await this.saveCurrentSession();
353
367
  console.log(`Recorded: ${req.method} ${req.url}`);
354
368
  });
355
369
  }
356
370
  async recordResponseData(req, proxyRes, body) {
357
371
  if (!this.currentSession) {
358
- console.log("recordResponseData: No current session");
359
372
  return false;
360
373
  }
361
374
  const key = getReqID(req);
@@ -387,9 +400,12 @@ var ProxyServer = class {
387
400
  headers: proxyRes.headers,
388
401
  body: body || null
389
402
  };
390
- await this.saveCurrentSession();
403
+ record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
404
+ const currentSequence = this.requestSequenceMap.get(key) || 0;
405
+ record.sequence = currentSequence;
406
+ this.requestSequenceMap.set(key, currentSequence + 1);
391
407
  console.log(
392
- `recordResponseData: Recorded response for ${req.method} ${req.url}`
408
+ `recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
393
409
  );
394
410
  return true;
395
411
  }
@@ -399,9 +415,7 @@ var ProxyServer = class {
399
415
  try {
400
416
  const session = await loadRecordingSession(filePath);
401
417
  const host = req.headers.host || "unknown";
402
- const recordsWithKey = session.recordings.filter(
403
- (r) => r.key === key && r.response
404
- );
418
+ const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
405
419
  if (recordsWithKey.length === 0) {
406
420
  throw new Error(
407
421
  `No recording found for ${key} at ${req.method} ${host}${req.url}`
@@ -411,7 +425,7 @@ var ProxyServer = class {
411
425
  const recordIndex = usageCount % recordsWithKey.length;
412
426
  const record = recordsWithKey[recordIndex];
413
427
  console.log(
414
- `Replaying ${req.method} ${req.url} (usage: ${usageCount}, using recording ${recordIndex}/${recordsWithKey.length})`
428
+ `Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
415
429
  );
416
430
  this.replaySequenceMap.set(key, usageCount + 1);
417
431
  if (!record.response) {
@@ -420,14 +434,9 @@ var ProxyServer = class {
420
434
  );
421
435
  }
422
436
  const { statusCode, headers, body } = record.response;
423
- const origin = req.headers.origin;
424
437
  const responseHeaders = {
425
438
  ...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": "*"
439
+ ...this.getCorsHeaders(req)
431
440
  };
432
441
  res.writeHead(statusCode, responseHeaders);
433
442
  res.end(body);
@@ -438,11 +447,10 @@ var ProxyServer = class {
438
447
  handleReplayError(req, res, err, key, filePath) {
439
448
  const isFileNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
440
449
  console.error("Replay error:", err);
441
- const origin = req.headers.origin;
450
+ const corsHeaders = this.getCorsHeaders(req);
442
451
  res.writeHead(HTTP_STATUS_NOT_FOUND, {
443
452
  "Content-Type": "application/json",
444
- "Access-Control-Allow-Origin": origin || "*",
445
- "Access-Control-Allow-Credentials": "true"
453
+ ...corsHeaders
446
454
  });
447
455
  res.end(
448
456
  JSON.stringify({
@@ -457,7 +465,8 @@ var ProxyServer = class {
457
465
  if (req.method === "OPTIONS") {
458
466
  return this.handleCorsPreflightRequest(req, res);
459
467
  }
460
- if (req.url === CONTROL_ENDPOINT) {
468
+ const urlPath = req.url?.split("?")[0] || "";
469
+ if (urlPath === CONTROL_ENDPOINT) {
461
470
  return this.handleControlRequest(req, res);
462
471
  }
463
472
  if (this.mode === Modes.replay) {
@@ -466,12 +475,9 @@ var ProxyServer = class {
466
475
  await this.handleProxyRequest(req, res);
467
476
  }
468
477
  handleCorsPreflightRequest(req, res) {
469
- const origin = req.headers.origin;
478
+ const corsHeaders = this.getCorsHeaders(req);
470
479
  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",
480
+ ...corsHeaders,
475
481
  "Access-Control-Max-Age": "86400"
476
482
  // 24 hours
477
483
  });
@@ -531,14 +537,9 @@ var ProxyServer = class {
531
537
  proxyRes,
532
538
  responseBody.toString("utf8")
533
539
  );
534
- const origin = req.headers.origin;
535
540
  const responseHeaders = {
536
541
  ...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": "*"
542
+ ...this.getCorsHeaders(req)
542
543
  };
543
544
  res.writeHead(proxyRes.statusCode || 200, responseHeaders);
544
545
  res.end(responseBody);
@@ -603,9 +604,6 @@ var ProxyServer = class {
603
604
  if (backendWs.readyState === WebSocket.OPEN) {
604
605
  backendWs.send(message);
605
606
  }
606
- this.saveCurrentSession().catch((error) => {
607
- console.error("Failed to save WebSocket recording:", error);
608
- });
609
607
  });
610
608
  backendWs.on("message", (data) => {
611
609
  const message = data.toString();
@@ -617,9 +615,6 @@ var ProxyServer = class {
617
615
  if (clientWs.readyState === WebSocket.OPEN) {
618
616
  clientWs.send(message);
619
617
  }
620
- this.saveCurrentSession().catch((error) => {
621
- console.error("Failed to save WebSocket recording:", error);
622
- });
623
618
  });
624
619
  clientWs.on("error", (err) => {
625
620
  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();
@@ -290,11 +316,6 @@ var ProxyServer = class {
290
316
  }
291
317
  async saveCurrentSession(filterIncomplete = false) {
292
318
  if (!this.currentSession) {
293
- console.log("No current session to save");
294
- return;
295
- }
296
- if (this.currentSession.recordings.length === 0 && this.currentSession.websocketRecordings.length === 0) {
297
- console.log("Session has no recordings, skipping save");
298
319
  return;
299
320
  }
300
321
  if (filterIncomplete) {
@@ -302,9 +323,6 @@ var ProxyServer = class {
302
323
  (r) => !r.response
303
324
  ).length;
304
325
  if (incompleteCount > 0) {
305
- console.log(
306
- `Removing ${incompleteCount} incomplete recording(s) without responses`
307
- );
308
326
  this.currentSession.recordings = this.currentSession.recordings.filter(
309
327
  (r) => r.response
310
328
  );
@@ -317,12 +335,9 @@ var ProxyServer = class {
317
335
  }
318
336
  saveRequestRecordSync(req, body) {
319
337
  if (!this.currentSession) {
320
- console.log("saveRequestRecordSync: No current session");
321
338
  return;
322
339
  }
323
340
  const key = getReqID(req);
324
- const currentSequence = this.requestSequenceMap.get(key) || 0;
325
- this.requestSequenceMap.set(key, currentSequence + 1);
326
341
  const record = {
327
342
  request: {
328
343
  method: req.method,
@@ -332,17 +347,17 @@ var ProxyServer = class {
332
347
  },
333
348
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
334
349
  key,
335
- sequence: currentSequence
350
+ sequence: -1
351
+ // Temporary, will be set when response arrives
336
352
  };
337
353
  this.currentSession.recordings.push(record);
338
354
  console.log(
339
355
  // 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})`
356
+ `saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
341
357
  );
342
358
  }
343
359
  updateRequestBodySync(req, body) {
344
360
  if (!this.currentSession) {
345
- console.log("updateRequestBodySync: No current session");
346
361
  return;
347
362
  }
348
363
  const key = getReqID(req);
@@ -383,13 +398,11 @@ var ProxyServer = class {
383
398
  headers: proxyRes.headers,
384
399
  body: body || null
385
400
  };
386
- await this.saveCurrentSession();
387
401
  console.log(`Recorded: ${req.method} ${req.url}`);
388
402
  });
389
403
  }
390
404
  async recordResponseData(req, proxyRes, body) {
391
405
  if (!this.currentSession) {
392
- console.log("recordResponseData: No current session");
393
406
  return false;
394
407
  }
395
408
  const key = getReqID(req);
@@ -421,9 +434,12 @@ var ProxyServer = class {
421
434
  headers: proxyRes.headers,
422
435
  body: body || null
423
436
  };
424
- await this.saveCurrentSession();
437
+ record.timestamp = (/* @__PURE__ */ new Date()).toISOString();
438
+ const currentSequence = this.requestSequenceMap.get(key) || 0;
439
+ record.sequence = currentSequence;
440
+ this.requestSequenceMap.set(key, currentSequence + 1);
425
441
  console.log(
426
- `recordResponseData: Recorded response for ${req.method} ${req.url}`
442
+ `recordResponseData: Recorded response for ${req.method} ${req.url} (seq: ${record.sequence})`
427
443
  );
428
444
  return true;
429
445
  }
@@ -433,9 +449,7 @@ var ProxyServer = class {
433
449
  try {
434
450
  const session = await loadRecordingSession(filePath);
435
451
  const host = req.headers.host || "unknown";
436
- const recordsWithKey = session.recordings.filter(
437
- (r) => r.key === key && r.response
438
- );
452
+ const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.sequence - b.sequence);
439
453
  if (recordsWithKey.length === 0) {
440
454
  throw new Error(
441
455
  `No recording found for ${key} at ${req.method} ${host}${req.url}`
@@ -445,7 +459,7 @@ var ProxyServer = class {
445
459
  const recordIndex = usageCount % recordsWithKey.length;
446
460
  const record = recordsWithKey[recordIndex];
447
461
  console.log(
448
- `Replaying ${req.method} ${req.url} (usage: ${usageCount}, using recording ${recordIndex}/${recordsWithKey.length})`
462
+ `Replaying ${req.method} ${req.url} (usage: ${usageCount}, sequence: ${record.sequence}, body_len: ${record.response?.body?.length || 0})`
449
463
  );
450
464
  this.replaySequenceMap.set(key, usageCount + 1);
451
465
  if (!record.response) {
@@ -454,14 +468,9 @@ var ProxyServer = class {
454
468
  );
455
469
  }
456
470
  const { statusCode, headers, body } = record.response;
457
- const origin = req.headers.origin;
458
471
  const responseHeaders = {
459
472
  ...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": "*"
473
+ ...this.getCorsHeaders(req)
465
474
  };
466
475
  res.writeHead(statusCode, responseHeaders);
467
476
  res.end(body);
@@ -472,11 +481,10 @@ var ProxyServer = class {
472
481
  handleReplayError(req, res, err, key, filePath) {
473
482
  const isFileNotFound = err instanceof Error && "code" in err && err.code === "ENOENT";
474
483
  console.error("Replay error:", err);
475
- const origin = req.headers.origin;
484
+ const corsHeaders = this.getCorsHeaders(req);
476
485
  res.writeHead(HTTP_STATUS_NOT_FOUND, {
477
486
  "Content-Type": "application/json",
478
- "Access-Control-Allow-Origin": origin || "*",
479
- "Access-Control-Allow-Credentials": "true"
487
+ ...corsHeaders
480
488
  });
481
489
  res.end(
482
490
  JSON.stringify({
@@ -491,7 +499,8 @@ var ProxyServer = class {
491
499
  if (req.method === "OPTIONS") {
492
500
  return this.handleCorsPreflightRequest(req, res);
493
501
  }
494
- if (req.url === CONTROL_ENDPOINT) {
502
+ const urlPath = req.url?.split("?")[0] || "";
503
+ if (urlPath === CONTROL_ENDPOINT) {
495
504
  return this.handleControlRequest(req, res);
496
505
  }
497
506
  if (this.mode === Modes.replay) {
@@ -500,12 +509,9 @@ var ProxyServer = class {
500
509
  await this.handleProxyRequest(req, res);
501
510
  }
502
511
  handleCorsPreflightRequest(req, res) {
503
- const origin = req.headers.origin;
512
+ const corsHeaders = this.getCorsHeaders(req);
504
513
  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",
514
+ ...corsHeaders,
509
515
  "Access-Control-Max-Age": "86400"
510
516
  // 24 hours
511
517
  });
@@ -565,14 +571,9 @@ var ProxyServer = class {
565
571
  proxyRes,
566
572
  responseBody.toString("utf8")
567
573
  );
568
- const origin = req.headers.origin;
569
574
  const responseHeaders = {
570
575
  ...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": "*"
576
+ ...this.getCorsHeaders(req)
576
577
  };
577
578
  res.writeHead(proxyRes.statusCode || 200, responseHeaders);
578
579
  res.end(responseBody);
@@ -637,9 +638,6 @@ var ProxyServer = class {
637
638
  if (backendWs.readyState === WebSocket.OPEN) {
638
639
  backendWs.send(message);
639
640
  }
640
- this.saveCurrentSession().catch((error) => {
641
- console.error("Failed to save WebSocket recording:", error);
642
- });
643
641
  });
644
642
  backendWs.on("message", (data) => {
645
643
  const message = data.toString();
@@ -651,9 +649,6 @@ var ProxyServer = class {
651
649
  if (clientWs.readyState === WebSocket.OPEN) {
652
650
  clientWs.send(message);
653
651
  }
654
- this.saveCurrentSession().catch((error) => {
655
- console.error("Failed to save WebSocket recording:", error);
656
- });
657
652
  });
658
653
  clientWs.on("error", (err) => {
659
654
  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.8",
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",