test-proxy-recorder 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -172
- package/dist/index.cjs +597 -0
- package/dist/index.d.cts +88 -0
- package/dist/index.d.ts +88 -5
- package/dist/index.mjs +582 -0
- package/dist/playwright/index.cjs +72 -0
- package/dist/playwright/index.d.cts +50 -0
- package/dist/playwright/index.d.ts +12 -10
- package/dist/playwright/index.mjs +65 -0
- package/dist/proxy.js +555 -4
- package/package.json +12 -5
- package/dist/ProxyServer.d.ts +0 -39
- package/dist/ProxyServer.d.ts.map +0 -1
- package/dist/ProxyServer.js +0 -464
- package/dist/cli.d.ts +0 -8
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -32
- package/dist/constants.d.ts +0 -7
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -3
- package/dist/playwright/index.d.ts.map +0 -1
- package/dist/playwright/index.js +0 -92
- package/dist/proxy.d.ts +0 -2
- package/dist/proxy.d.ts.map +0 -1
- package/dist/types.d.ts +0 -46
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -6
- package/dist/utils/fileUtils.d.ts +0 -5
- package/dist/utils/fileUtils.d.ts.map +0 -1
- package/dist/utils/fileUtils.js +0 -16
- package/dist/utils/httpHelpers.d.ts +0 -4
- package/dist/utils/httpHelpers.d.ts.map +0 -1
- package/dist/utils/httpHelpers.js +0 -13
- package/dist/utils/index.d.ts +0 -4
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -4
- package/dist/utils/requestKeyGenerator.d.ts +0 -3
- package/dist/utils/requestKeyGenerator.d.ts.map +0 -1
- package/dist/utils/requestKeyGenerator.js +0 -24
package/dist/ProxyServer.js
DELETED
|
@@ -1,464 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import http from 'node:http';
|
|
3
|
-
import httpProxy from 'http-proxy';
|
|
4
|
-
import { WebSocket, WebSocketServer } from 'ws';
|
|
5
|
-
import { CONTROL_ENDPOINT, DEFAULT_TIMEOUT_MS, HTTP_STATUS_BAD_GATEWAY, HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK, } from './constants.js';
|
|
6
|
-
import { Modes, } from './types.js';
|
|
7
|
-
import { getRecordingPath, loadRecordingSession, saveRecordingSession, } from './utils/fileUtils.js';
|
|
8
|
-
import { readRequestBody, sendJsonResponse } from './utils/httpHelpers.js';
|
|
9
|
-
import { generateRequestKey } from './utils/requestKeyGenerator.js';
|
|
10
|
-
export class ProxyServer {
|
|
11
|
-
targets;
|
|
12
|
-
currentTargetIndex;
|
|
13
|
-
mode;
|
|
14
|
-
recordingId;
|
|
15
|
-
replayId;
|
|
16
|
-
modeTimeout;
|
|
17
|
-
proxy;
|
|
18
|
-
currentSession;
|
|
19
|
-
recordingsDir;
|
|
20
|
-
constructor(targets, recordingsDir) {
|
|
21
|
-
this.targets = targets;
|
|
22
|
-
this.currentTargetIndex = 0;
|
|
23
|
-
this.mode = Modes.transparent;
|
|
24
|
-
this.recordingId = null;
|
|
25
|
-
this.replayId = null;
|
|
26
|
-
this.modeTimeout = null;
|
|
27
|
-
this.currentSession = null;
|
|
28
|
-
this.recordingsDir = recordingsDir;
|
|
29
|
-
this.proxy = httpProxy.createProxyServer({
|
|
30
|
-
secure: false,
|
|
31
|
-
changeOrigin: true,
|
|
32
|
-
});
|
|
33
|
-
this.setupProxyEventHandlers();
|
|
34
|
-
}
|
|
35
|
-
async init() {
|
|
36
|
-
await fs.mkdir(this.recordingsDir, { recursive: true });
|
|
37
|
-
}
|
|
38
|
-
listen(port) {
|
|
39
|
-
const server = http.createServer((req, res) => {
|
|
40
|
-
this.handleRequest(req, res);
|
|
41
|
-
});
|
|
42
|
-
// Handle WebSocket upgrade requests
|
|
43
|
-
server.on('upgrade', (req, socket, head) => {
|
|
44
|
-
this.handleUpgrade(req, socket, head);
|
|
45
|
-
});
|
|
46
|
-
server.listen(port, () => {
|
|
47
|
-
this.logServerStartup(port);
|
|
48
|
-
});
|
|
49
|
-
return server;
|
|
50
|
-
}
|
|
51
|
-
setupProxyEventHandlers() {
|
|
52
|
-
this.proxy.on('error', this.handleProxyError.bind(this));
|
|
53
|
-
this.proxy.on('proxyRes', this.handleProxyResponse.bind(this));
|
|
54
|
-
}
|
|
55
|
-
handleProxyError(err, _req, res) {
|
|
56
|
-
console.error('Proxy error:', err);
|
|
57
|
-
if (!(res instanceof http.ServerResponse)) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
if (!res.headersSent) {
|
|
61
|
-
res.writeHead(HTTP_STATUS_BAD_GATEWAY, {
|
|
62
|
-
'Content-Type': 'application/json',
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
res.end(JSON.stringify({ error: 'Proxy error', message: err.message }));
|
|
66
|
-
}
|
|
67
|
-
handleProxyResponse(proxyRes, req) {
|
|
68
|
-
if (this.mode === Modes.record && this.recordingId) {
|
|
69
|
-
this.recordResponse(req, proxyRes);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
getTarget() {
|
|
73
|
-
const target = this.targets[this.currentTargetIndex];
|
|
74
|
-
this.currentTargetIndex =
|
|
75
|
-
(this.currentTargetIndex + 1) % this.targets.length;
|
|
76
|
-
return target;
|
|
77
|
-
}
|
|
78
|
-
async handleControlRequest(req, res) {
|
|
79
|
-
try {
|
|
80
|
-
const body = await readRequestBody(req);
|
|
81
|
-
console.log('MODE CHANGE', body);
|
|
82
|
-
const data = JSON.parse(body);
|
|
83
|
-
const { mode, id, timeout: requestTimeout } = data;
|
|
84
|
-
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
85
|
-
this.clearModeTimeout();
|
|
86
|
-
await this.switchMode(mode, id);
|
|
87
|
-
this.setupModeTimeout(timeout);
|
|
88
|
-
sendJsonResponse(res, HTTP_STATUS_OK, {
|
|
89
|
-
success: true,
|
|
90
|
-
mode: this.mode,
|
|
91
|
-
id: this.recordingId || this.replayId,
|
|
92
|
-
timeout,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
catch (error) {
|
|
96
|
-
console.error('Control request error:', error);
|
|
97
|
-
sendJsonResponse(res, HTTP_STATUS_BAD_REQUEST, {
|
|
98
|
-
error: error instanceof Error ? error.message : 'Unknown error',
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
clearModeTimeout() {
|
|
103
|
-
if (this.modeTimeout) {
|
|
104
|
-
clearTimeout(this.modeTimeout);
|
|
105
|
-
this.modeTimeout = null;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
async switchMode(mode, id) {
|
|
109
|
-
// Save current session before switching
|
|
110
|
-
if (this.currentSession) {
|
|
111
|
-
console.log('Switching mode, saving current session first');
|
|
112
|
-
await this.saveCurrentSession();
|
|
113
|
-
console.log('Session saved, continuing with mode switch');
|
|
114
|
-
}
|
|
115
|
-
switch (mode) {
|
|
116
|
-
case Modes.transparent: {
|
|
117
|
-
this.switchToTransparentMode();
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
case Modes.record: {
|
|
121
|
-
this.switchToRecordMode(id);
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
case Modes.replay: {
|
|
125
|
-
this.switchToReplayMode(id);
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
default: {
|
|
129
|
-
throw new Error('Invalid mode. Use: transparent, record, or replay');
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
switchToTransparentMode() {
|
|
134
|
-
this.mode = Modes.transparent;
|
|
135
|
-
this.recordingId = null;
|
|
136
|
-
this.replayId = null;
|
|
137
|
-
this.currentSession = null;
|
|
138
|
-
console.log('Switched to transparent mode');
|
|
139
|
-
}
|
|
140
|
-
switchToRecordMode(id) {
|
|
141
|
-
if (!id) {
|
|
142
|
-
throw new Error('Record ID is required');
|
|
143
|
-
}
|
|
144
|
-
this.mode = Modes.record;
|
|
145
|
-
this.recordingId = id;
|
|
146
|
-
this.replayId = null;
|
|
147
|
-
this.currentSession = { id, recordings: [], websocketRecordings: [] };
|
|
148
|
-
console.log(`Switched to record mode with ID: ${id}`);
|
|
149
|
-
}
|
|
150
|
-
switchToReplayMode(id) {
|
|
151
|
-
if (!id) {
|
|
152
|
-
throw new Error('Replay ID is required');
|
|
153
|
-
}
|
|
154
|
-
this.mode = Modes.replay;
|
|
155
|
-
this.replayId = id;
|
|
156
|
-
this.recordingId = null;
|
|
157
|
-
this.currentSession = null;
|
|
158
|
-
console.log(`Switched to replay mode with ID: ${id}`);
|
|
159
|
-
}
|
|
160
|
-
setupModeTimeout(timeout) {
|
|
161
|
-
if (timeout && timeout > 0) {
|
|
162
|
-
this.modeTimeout = setTimeout(async () => {
|
|
163
|
-
console.log('Timeout reached, switching back to transparent mode');
|
|
164
|
-
await this.saveCurrentSession();
|
|
165
|
-
this.switchToTransparentMode();
|
|
166
|
-
this.modeTimeout = null;
|
|
167
|
-
}, timeout);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
async saveCurrentSession() {
|
|
171
|
-
if (!this.currentSession) {
|
|
172
|
-
console.log('No current session to save');
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
if (this.currentSession.recordings.length === 0 &&
|
|
176
|
-
this.currentSession.websocketRecordings.length === 0) {
|
|
177
|
-
console.log('Session has no recordings, skipping save');
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
console.log(`Saving session with ${this.currentSession.recordings.length} HTTP and ${this.currentSession.websocketRecordings.length} WebSocket recordings`);
|
|
181
|
-
await saveRecordingSession(this.recordingsDir, this.currentSession);
|
|
182
|
-
}
|
|
183
|
-
async saveRequestRecord(req, body) {
|
|
184
|
-
if (!this.currentSession) {
|
|
185
|
-
return;
|
|
186
|
-
}
|
|
187
|
-
const key = generateRequestKey(req);
|
|
188
|
-
const record = {
|
|
189
|
-
request: {
|
|
190
|
-
method: req.method,
|
|
191
|
-
url: req.url,
|
|
192
|
-
headers: req.headers,
|
|
193
|
-
body: body || null,
|
|
194
|
-
},
|
|
195
|
-
timestamp: new Date().toISOString(),
|
|
196
|
-
key,
|
|
197
|
-
};
|
|
198
|
-
this.currentSession.recordings.push(record);
|
|
199
|
-
}
|
|
200
|
-
async recordResponse(req, proxyRes) {
|
|
201
|
-
if (!this.currentSession) {
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
const key = generateRequestKey(req);
|
|
205
|
-
const record = this.currentSession.recordings.find((r) => r.key === key);
|
|
206
|
-
if (!record) {
|
|
207
|
-
console.error('Request record not found for response:', key);
|
|
208
|
-
return;
|
|
209
|
-
}
|
|
210
|
-
const chunks = [];
|
|
211
|
-
proxyRes.on('data', (chunk) => {
|
|
212
|
-
chunks.push(chunk);
|
|
213
|
-
});
|
|
214
|
-
proxyRes.on('end', async () => {
|
|
215
|
-
const body = Buffer.concat(chunks).toString('utf8');
|
|
216
|
-
record.response = {
|
|
217
|
-
statusCode: proxyRes.statusCode,
|
|
218
|
-
headers: proxyRes.headers,
|
|
219
|
-
body: body || null,
|
|
220
|
-
};
|
|
221
|
-
await this.saveCurrentSession();
|
|
222
|
-
console.log(`Recorded: ${req.method} ${req.url}`);
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
async handleReplayRequest(req, res) {
|
|
226
|
-
const key = generateRequestKey(req);
|
|
227
|
-
const filePath = getRecordingPath(this.recordingsDir, this.replayId);
|
|
228
|
-
try {
|
|
229
|
-
const session = await loadRecordingSession(filePath);
|
|
230
|
-
const record = session.recordings.find((r) => r.key === key);
|
|
231
|
-
if (!record) {
|
|
232
|
-
throw new Error(`No recording found for ${key}`);
|
|
233
|
-
}
|
|
234
|
-
if (!record.response) {
|
|
235
|
-
throw new Error('No response recorded for this request');
|
|
236
|
-
}
|
|
237
|
-
const { statusCode, headers, body } = record.response;
|
|
238
|
-
res.writeHead(statusCode, headers);
|
|
239
|
-
res.end(body);
|
|
240
|
-
console.log(`Replayed: ${req.method} ${req.url}`);
|
|
241
|
-
}
|
|
242
|
-
catch (error) {
|
|
243
|
-
this.handleReplayError(res, error, key, filePath);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
handleReplayError(res, err, key, filePath) {
|
|
247
|
-
const isFileNotFound = err instanceof Error && 'code' in err && err.code === 'ENOENT';
|
|
248
|
-
console.error('Replay error:', err);
|
|
249
|
-
sendJsonResponse(res, HTTP_STATUS_NOT_FOUND, {
|
|
250
|
-
error: isFileNotFound
|
|
251
|
-
? 'Recording file not found'
|
|
252
|
-
: 'Recording not found',
|
|
253
|
-
message: err instanceof Error ? err.message : 'Unknown error',
|
|
254
|
-
key,
|
|
255
|
-
filePath,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
async handleRequest(req, res) {
|
|
259
|
-
if (req.url === CONTROL_ENDPOINT) {
|
|
260
|
-
return this.handleControlRequest(req, res);
|
|
261
|
-
}
|
|
262
|
-
if (this.mode === Modes.replay) {
|
|
263
|
-
return this.handleReplayRequest(req, res);
|
|
264
|
-
}
|
|
265
|
-
await this.handleProxyRequest(req, res);
|
|
266
|
-
}
|
|
267
|
-
async handleProxyRequest(req, res) {
|
|
268
|
-
const target = this.getTarget();
|
|
269
|
-
console.log(`[${this.mode}] ${req.method} ${req.url} -> ${target}`);
|
|
270
|
-
if (this.mode === Modes.record) {
|
|
271
|
-
await this.bufferRequestForRecord(req);
|
|
272
|
-
}
|
|
273
|
-
this.proxy.web(req, res, { target });
|
|
274
|
-
}
|
|
275
|
-
async bufferRequestForRecord(req) {
|
|
276
|
-
const chunks = [];
|
|
277
|
-
req.on('data', (chunk) => {
|
|
278
|
-
chunks.push(chunk);
|
|
279
|
-
});
|
|
280
|
-
req.on('end', async () => {
|
|
281
|
-
const body = Buffer.concat(chunks).toString('utf8');
|
|
282
|
-
await this.saveRequestRecord(req, body);
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
handleUpgrade(req, socket, head) {
|
|
286
|
-
if (this.mode === Modes.replay) {
|
|
287
|
-
this.handleReplayWebSocket(req, socket);
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
const target = this.getTarget();
|
|
291
|
-
console.log(`[${this.mode}] WebSocket upgrade ${req.url} -> ${target}`);
|
|
292
|
-
if (this.mode === Modes.record) {
|
|
293
|
-
this.handleRecordWebSocket(req, socket, head, target);
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
// Transparent mode - just proxy through
|
|
297
|
-
this.proxy.ws(req, socket, head, { target });
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
handleRecordWebSocket(req, clientSocket, head, target) {
|
|
301
|
-
const url = req.url || '/';
|
|
302
|
-
const key = `WS_${url.replaceAll('/', '_')}`;
|
|
303
|
-
const wsRecording = {
|
|
304
|
-
url,
|
|
305
|
-
messages: [],
|
|
306
|
-
timestamp: new Date().toISOString(),
|
|
307
|
-
key,
|
|
308
|
-
};
|
|
309
|
-
if (this.currentSession) {
|
|
310
|
-
this.currentSession.websocketRecordings.push(wsRecording);
|
|
311
|
-
}
|
|
312
|
-
// Create WebSocket connection to backend
|
|
313
|
-
const backendWsUrl = `${target.replace('http', 'ws')}${url}`;
|
|
314
|
-
const backendWs = new WebSocket(backendWsUrl);
|
|
315
|
-
// Create WebSocket server for client
|
|
316
|
-
const wss = new WebSocketServer({ noServer: true });
|
|
317
|
-
// Wait for backend connection before accepting client
|
|
318
|
-
backendWs.on('open', () => {
|
|
319
|
-
console.log(`WebSocket recording: connected to backend ${backendWsUrl}`);
|
|
320
|
-
wss.handleUpgrade(req, clientSocket, head, (clientWs) => {
|
|
321
|
-
// Forward messages from client to backend
|
|
322
|
-
clientWs.on('message', (data) => {
|
|
323
|
-
const message = data.toString();
|
|
324
|
-
// Record client message
|
|
325
|
-
wsRecording.messages.push({
|
|
326
|
-
direction: 'client-to-server',
|
|
327
|
-
data: message,
|
|
328
|
-
timestamp: new Date().toISOString(),
|
|
329
|
-
});
|
|
330
|
-
// Forward to backend if connected
|
|
331
|
-
if (backendWs.readyState === WebSocket.OPEN) {
|
|
332
|
-
backendWs.send(message);
|
|
333
|
-
}
|
|
334
|
-
this.saveCurrentSession().catch((error) => {
|
|
335
|
-
console.error('Failed to save WebSocket recording:', error);
|
|
336
|
-
});
|
|
337
|
-
});
|
|
338
|
-
// Forward messages from backend to client
|
|
339
|
-
backendWs.on('message', (data) => {
|
|
340
|
-
const message = data.toString();
|
|
341
|
-
// Record server message
|
|
342
|
-
wsRecording.messages.push({
|
|
343
|
-
direction: 'server-to-client',
|
|
344
|
-
data: message,
|
|
345
|
-
timestamp: new Date().toISOString(),
|
|
346
|
-
});
|
|
347
|
-
// Forward to client
|
|
348
|
-
if (clientWs.readyState === WebSocket.OPEN) {
|
|
349
|
-
clientWs.send(message);
|
|
350
|
-
}
|
|
351
|
-
this.saveCurrentSession().catch((error) => {
|
|
352
|
-
console.error('Failed to save WebSocket recording:', error);
|
|
353
|
-
});
|
|
354
|
-
});
|
|
355
|
-
// Handle errors
|
|
356
|
-
clientWs.on('error', (err) => {
|
|
357
|
-
console.error('Client WebSocket error:', err);
|
|
358
|
-
});
|
|
359
|
-
backendWs.on('error', (err) => {
|
|
360
|
-
console.error('Backend WebSocket error:', err);
|
|
361
|
-
});
|
|
362
|
-
// Handle close
|
|
363
|
-
clientWs.on('close', () => {
|
|
364
|
-
backendWs.close();
|
|
365
|
-
console.log('Client WebSocket closed');
|
|
366
|
-
});
|
|
367
|
-
backendWs.on('close', () => {
|
|
368
|
-
clientWs.close();
|
|
369
|
-
console.log('Backend WebSocket closed');
|
|
370
|
-
});
|
|
371
|
-
});
|
|
372
|
-
});
|
|
373
|
-
backendWs.on('error', (err) => {
|
|
374
|
-
console.error('Backend WebSocket connection error:', err);
|
|
375
|
-
clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
|
|
376
|
-
clientSocket.destroy();
|
|
377
|
-
});
|
|
378
|
-
wss.on('error', (err) => {
|
|
379
|
-
console.error('WebSocket server error:', err);
|
|
380
|
-
});
|
|
381
|
-
}
|
|
382
|
-
handleReplayWebSocket(req, socket) {
|
|
383
|
-
const url = req.url || '/';
|
|
384
|
-
const key = `WS_${url.replaceAll('/', '_')}`;
|
|
385
|
-
const filePath = getRecordingPath(this.recordingsDir, this.replayId);
|
|
386
|
-
loadRecordingSession(filePath)
|
|
387
|
-
.then((session) => {
|
|
388
|
-
const wsRecording = session.websocketRecordings.find((r) => r.key === key);
|
|
389
|
-
if (!wsRecording) {
|
|
390
|
-
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
|
391
|
-
socket.destroy();
|
|
392
|
-
console.log(`No WebSocket recording found for ${key}`);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
// Create WebSocket server for replay
|
|
396
|
-
const wss = new WebSocketServer({ noServer: true });
|
|
397
|
-
// Fake upgrade request with proper headers
|
|
398
|
-
const fakeReq = Object.assign(req, {
|
|
399
|
-
headers: {
|
|
400
|
-
...req.headers,
|
|
401
|
-
'sec-websocket-key': req.headers['sec-websocket-key'] || 'replay-key',
|
|
402
|
-
'sec-websocket-version': '13',
|
|
403
|
-
},
|
|
404
|
-
});
|
|
405
|
-
wss.handleUpgrade(fakeReq, socket, Buffer.alloc(0), (ws) => {
|
|
406
|
-
console.log(`Replaying WebSocket: ${url}`);
|
|
407
|
-
// Replay server-to-client messages
|
|
408
|
-
const serverMessages = wsRecording.messages.filter((m) => m.direction === 'server-to-client');
|
|
409
|
-
let messageIndex = 0;
|
|
410
|
-
// Handle client messages and send corresponding server responses
|
|
411
|
-
ws.on('message', (data) => {
|
|
412
|
-
const clientMessage = data.toString();
|
|
413
|
-
console.log(`Replay: Client sent: ${clientMessage}`);
|
|
414
|
-
// Send next server message if available
|
|
415
|
-
if (messageIndex < serverMessages.length) {
|
|
416
|
-
setTimeout(() => {
|
|
417
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
418
|
-
ws.send(serverMessages[messageIndex].data);
|
|
419
|
-
console.log(`Replay: Sent server message ${messageIndex}`);
|
|
420
|
-
messageIndex++;
|
|
421
|
-
}
|
|
422
|
-
}, 10);
|
|
423
|
-
}
|
|
424
|
-
});
|
|
425
|
-
// Send initial server messages (those sent before any client message)
|
|
426
|
-
let initialMessagesSent = 0;
|
|
427
|
-
for (let i = 0; i < wsRecording.messages.length; i++) {
|
|
428
|
-
const msg = wsRecording.messages[i];
|
|
429
|
-
if (msg.direction === 'client-to-server') {
|
|
430
|
-
break;
|
|
431
|
-
}
|
|
432
|
-
if (msg.direction === 'server-to-client') {
|
|
433
|
-
setTimeout(() => {
|
|
434
|
-
if (ws.readyState === WebSocket.OPEN) {
|
|
435
|
-
ws.send(msg.data);
|
|
436
|
-
console.log(`Replay: Sent initial server message: ${msg.data}`);
|
|
437
|
-
messageIndex++;
|
|
438
|
-
initialMessagesSent++;
|
|
439
|
-
}
|
|
440
|
-
}, 10 * (initialMessagesSent + 1));
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
ws.on('error', (err) => {
|
|
444
|
-
console.error('Replay WebSocket error:', err);
|
|
445
|
-
});
|
|
446
|
-
ws.on('close', () => {
|
|
447
|
-
console.log('Replay WebSocket closed');
|
|
448
|
-
});
|
|
449
|
-
});
|
|
450
|
-
})
|
|
451
|
-
.catch((error) => {
|
|
452
|
-
console.error('Replay error:', error);
|
|
453
|
-
socket.write('HTTP/1.1 404 Not Found\r\n\r\n');
|
|
454
|
-
socket.destroy();
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
logServerStartup(port) {
|
|
458
|
-
console.log(`Proxy server running on http://localhost:${port}`);
|
|
459
|
-
console.log(`Mode: ${this.mode}`);
|
|
460
|
-
console.log(`Targets: ${this.targets.join(', ')}`);
|
|
461
|
-
console.log(`Control endpoint: http://localhost:${port}${CONTROL_ENDPOINT}`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
|
-
//# sourceMappingURL=ProxyServer.js.map
|
package/dist/cli.d.ts
DELETED
package/dist/cli.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AASA,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wBAAgB,YAAY,IAAI,UAAU,CA6CzC"}
|
package/dist/cli.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
const DEFAULT_PORT = 8000;
|
|
5
|
-
const DEFAULT_RECORDINGS_DIR = './recordings';
|
|
6
|
-
export function parseCliArgs() {
|
|
7
|
-
const program = new Command();
|
|
8
|
-
program
|
|
9
|
-
.name('dev-proxy')
|
|
10
|
-
.description('Development proxy server with recording and replay capabilities')
|
|
11
|
-
.argument('<targets...>', 'Target API service URLs (e.g., http://localhost:3000)')
|
|
12
|
-
.option('-p, --port <number>', 'Port number for the proxy server', String(DEFAULT_PORT))
|
|
13
|
-
.option('-r, --recordings-dir <path>', 'Directory to store recordings (relative to CWD)', DEFAULT_RECORDINGS_DIR)
|
|
14
|
-
.action(() => {
|
|
15
|
-
// Action handled after parse
|
|
16
|
-
});
|
|
17
|
-
program.parse();
|
|
18
|
-
const targets = program.args;
|
|
19
|
-
const options = program.opts();
|
|
20
|
-
const port = Number.parseInt(options.port, 10);
|
|
21
|
-
if (Number.isNaN(port) || port < 1025 || port > 65_535) {
|
|
22
|
-
console.error('Error: Invalid port number. Must be between 1 and 65535');
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
if (targets.length === 0) {
|
|
26
|
-
program.help();
|
|
27
|
-
}
|
|
28
|
-
// Resolve recordings directory relative to the current working directory (where the command is run)
|
|
29
|
-
const recordingsDir = path.resolve(process.cwd(), options.recordingsDir);
|
|
30
|
-
return { targets, port, recordingsDir };
|
|
31
|
-
}
|
|
32
|
-
//# sourceMappingURL=cli.js.map
|
package/dist/constants.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export declare const DEFAULT_TIMEOUT_MS: number;
|
|
2
|
-
export declare const HTTP_STATUS_BAD_GATEWAY = 502;
|
|
3
|
-
export declare const HTTP_STATUS_OK = 200;
|
|
4
|
-
export declare const HTTP_STATUS_BAD_REQUEST = 400;
|
|
5
|
-
export declare const HTTP_STATUS_NOT_FOUND = 404;
|
|
6
|
-
export declare const CONTROL_ENDPOINT = "/__control";
|
|
7
|
-
//# sourceMappingURL=constants.d.ts.map
|
package/dist/constants.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,kBAAkB,QAAa,CAAC;AAC7C,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C,eAAO,MAAM,cAAc,MAAM,CAAC;AAClC,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAC3C,eAAO,MAAM,qBAAqB,MAAM,CAAC;AACzC,eAAO,MAAM,gBAAgB,eAAe,CAAC"}
|
package/dist/constants.js
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_TIMEOUT_MS = 120 * 1000;
|
|
2
|
-
export const HTTP_STATUS_BAD_GATEWAY = 502;
|
|
3
|
-
export const HTTP_STATUS_OK = 200;
|
|
4
|
-
export const HTTP_STATUS_BAD_REQUEST = 400;
|
|
5
|
-
export const HTTP_STATUS_NOT_FOUND = 404;
|
|
6
|
-
export const CONTROL_ENDPOINT = '/__control';
|
|
7
|
-
//# sourceMappingURL=constants.js.map
|
package/dist/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EACV,cAAc,EACd,IAAI,EACJ,SAAS,EACT,gBAAgB,EAChB,kBAAkB,GACnB,MAAM,SAAS,CAAC;AAGjB,YAAY,EAAE,kBAAkB,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,YAAY,EACZ,cAAc,EACd,WAAW,EACX,SAAS,GACV,MAAM,cAAc,CAAC"}
|
package/dist/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/playwright/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAKjD,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,aAAa,CAAC;AAE/D,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;AAQzD;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,SAAS,EACf,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,MAAM,CAEtE;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,QAAQ,EAAE,kBAAkB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAGf;AAED;;;GAGG;AACH,wBAAsB,WAAW,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAG7E;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAG3E;AAED;;;GAGG;AACH,eAAO,MAAM,eAAe;IAC1B;;;;OAIG;qBACoB,kBAAkB,QAAQ,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAM1E;;;OAGG;oBACmB,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;CAIzD,CAAC"}
|
package/dist/playwright/index.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
const INTERNAL_API_URL = process.env.INTERNAL_API_URL || 'http://localhost:8100';
|
|
2
|
-
/**
|
|
3
|
-
* Set the proxy mode for a given session
|
|
4
|
-
* @param mode - The proxy mode to set (recording, replay, transparent)
|
|
5
|
-
* @param sessionId - Unique identifier for the session
|
|
6
|
-
* @param timeout - Optional timeout in milliseconds
|
|
7
|
-
*/
|
|
8
|
-
export async function setProxyMode(mode, sessionId, timeout) {
|
|
9
|
-
if (!INTERNAL_API_URL) {
|
|
10
|
-
console.warn('INTERNAL_API_URL not set, proxy mode not changed');
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
try {
|
|
14
|
-
const body = {
|
|
15
|
-
mode,
|
|
16
|
-
id: sessionId,
|
|
17
|
-
...(timeout && { timeout }),
|
|
18
|
-
};
|
|
19
|
-
const response = await fetch(`${INTERNAL_API_URL}/__control`, {
|
|
20
|
-
method: 'POST',
|
|
21
|
-
headers: { 'Content-Type': 'application/json' },
|
|
22
|
-
body: JSON.stringify(body),
|
|
23
|
-
});
|
|
24
|
-
if (!response.ok) {
|
|
25
|
-
const text = await response.text();
|
|
26
|
-
console.error(`Failed to set proxy mode to ${mode}:`, text);
|
|
27
|
-
throw new Error(`Failed to set proxy mode: ${text}`);
|
|
28
|
-
}
|
|
29
|
-
await response.json();
|
|
30
|
-
console.log(`Proxy mode set to: ${mode} (session: ${sessionId})`);
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
console.error(`Error setting proxy mode:`, error);
|
|
34
|
-
throw error;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Generate a session ID from test info
|
|
39
|
-
* @param testInfo - Playwright test info object
|
|
40
|
-
*/
|
|
41
|
-
export function generateSessionId(testInfo) {
|
|
42
|
-
return testInfo.title.toLowerCase().replaceAll(/\s+/g, '-');
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Start recording for a test
|
|
46
|
-
* @param testInfo - Playwright test info object
|
|
47
|
-
*/
|
|
48
|
-
export async function startRecording(testInfo) {
|
|
49
|
-
const sessionId = generateSessionId(testInfo);
|
|
50
|
-
await setProxyMode('recording', sessionId);
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Start replay for a test
|
|
54
|
-
* @param testInfo - Playwright test info object
|
|
55
|
-
*/
|
|
56
|
-
export async function startReplay(testInfo) {
|
|
57
|
-
const sessionId = generateSessionId(testInfo);
|
|
58
|
-
await setProxyMode('replay', sessionId);
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Stop recording/replay and return to transparent mode
|
|
62
|
-
* @param testInfo - Playwright test info object
|
|
63
|
-
*/
|
|
64
|
-
export async function stopProxy(testInfo) {
|
|
65
|
-
const sessionId = generateSessionId(testInfo);
|
|
66
|
-
await setProxyMode('transparent', sessionId);
|
|
67
|
-
}
|
|
68
|
-
/**
|
|
69
|
-
* Playwright test fixture helper for managing proxy mode
|
|
70
|
-
* Use this in beforeEach/afterEach hooks
|
|
71
|
-
*/
|
|
72
|
-
export const playwrightProxy = {
|
|
73
|
-
/**
|
|
74
|
-
* Setup before test - sets the proxy mode
|
|
75
|
-
* @param testInfo - Playwright test info object
|
|
76
|
-
* @param mode - The proxy mode to use for this test
|
|
77
|
-
*/
|
|
78
|
-
async before(testInfo, mode) {
|
|
79
|
-
const sessionId = generateSessionId(testInfo);
|
|
80
|
-
console.log('Proxy setup:', { mode, sessionId });
|
|
81
|
-
await setProxyMode(mode, sessionId);
|
|
82
|
-
},
|
|
83
|
-
/**
|
|
84
|
-
* Cleanup after test - returns to transparent mode
|
|
85
|
-
* @param testInfo - Playwright test info object
|
|
86
|
-
*/
|
|
87
|
-
async after(testInfo) {
|
|
88
|
-
const sessionId = generateSessionId(testInfo);
|
|
89
|
-
await setProxyMode('transparent', sessionId);
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
//# sourceMappingURL=index.js.map
|
package/dist/proxy.d.ts
DELETED
package/dist/proxy.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":""}
|