test-proxy-recorder 0.1.9 → 0.1.11
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 +2 -2
- package/dist/{index-CG-XcFDa.d.cts → index-Cx_Kflfl.d.cts} +1 -1
- package/dist/{index-CG-XcFDa.d.ts → index-Cx_Kflfl.d.ts} +1 -1
- package/dist/index.cjs +173 -70
- package/dist/index.d.cts +15 -3
- package/dist/index.d.ts +15 -3
- package/dist/index.mjs +171 -68
- package/dist/playwright/index.d.cts +1 -1
- package/dist/playwright/index.d.ts +1 -1
- package/dist/proxy.js +172 -69
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -3,9 +3,9 @@ import http from 'http';
|
|
|
3
3
|
import https from 'https';
|
|
4
4
|
import httpProxy from 'http-proxy';
|
|
5
5
|
import { WebSocket, WebSocketServer } from 'ws';
|
|
6
|
-
import path from 'path';
|
|
7
6
|
import crypto from 'crypto';
|
|
8
|
-
import
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import filenamify2 from 'filenamify';
|
|
9
9
|
|
|
10
10
|
// src/ProxyServer.ts
|
|
11
11
|
|
|
@@ -24,23 +24,53 @@ var Modes = {
|
|
|
24
24
|
replay: "replay"
|
|
25
25
|
};
|
|
26
26
|
var JSON_INDENT_SPACES = 2;
|
|
27
|
+
var EXTENSION = ".mock.json";
|
|
28
|
+
var MAX_FILENAME_LENGTH = 255 - EXTENSION.length;
|
|
29
|
+
var HASH_LENGTH = 8;
|
|
30
|
+
function generateHash(str) {
|
|
31
|
+
return crypto.createHash("shake256", { outputLength: HASH_LENGTH / 2 }).update(str).digest("hex");
|
|
32
|
+
}
|
|
27
33
|
function getRecordingPath(recordingsDir, id) {
|
|
28
|
-
|
|
34
|
+
let processedId = id.replaceAll("/", "__");
|
|
35
|
+
if (processedId.length > MAX_FILENAME_LENGTH) {
|
|
36
|
+
const hash = generateHash(id);
|
|
37
|
+
const maxBaseLength = MAX_FILENAME_LENGTH - HASH_LENGTH - 1;
|
|
38
|
+
processedId = `${processedId.slice(0, maxBaseLength)}_${hash}`;
|
|
39
|
+
}
|
|
40
|
+
const sanitizedId = filenamify2(processedId, {
|
|
41
|
+
replacement: "_",
|
|
42
|
+
maxLength: 255
|
|
43
|
+
// Set explicit max to prevent filenamify's default truncation
|
|
44
|
+
});
|
|
45
|
+
return path.join(recordingsDir, `${sanitizedId}${EXTENSION}`);
|
|
29
46
|
}
|
|
30
47
|
async function loadRecordingSession(filePath) {
|
|
31
48
|
const fileContent = await fs.readFile(filePath, "utf8");
|
|
32
49
|
return JSON.parse(fileContent);
|
|
33
50
|
}
|
|
51
|
+
function processRecordings(recordings) {
|
|
52
|
+
const keySequenceMap = /* @__PURE__ */ new Map();
|
|
53
|
+
return recordings.map((recording) => {
|
|
54
|
+
const key = recording.key;
|
|
55
|
+
const currentSeq = keySequenceMap.get(key) || 0;
|
|
56
|
+
keySequenceMap.set(key, currentSeq + 1);
|
|
57
|
+
return { ...recording, sequence: currentSeq };
|
|
58
|
+
});
|
|
59
|
+
}
|
|
34
60
|
async function saveRecordingSession(recordingsDir, session) {
|
|
35
61
|
const filePath = getRecordingPath(recordingsDir, session.id);
|
|
36
|
-
|
|
37
|
-
|
|
62
|
+
await fs.mkdir(recordingsDir, { recursive: true });
|
|
63
|
+
const processedRecordings = processRecordings(session.recordings);
|
|
64
|
+
const processedSession = {
|
|
65
|
+
...session,
|
|
66
|
+
recordings: processedRecordings
|
|
67
|
+
};
|
|
38
68
|
await fs.writeFile(
|
|
39
69
|
filePath,
|
|
40
|
-
JSON.stringify(
|
|
70
|
+
JSON.stringify(processedSession, null, JSON_INDENT_SPACES)
|
|
41
71
|
);
|
|
42
72
|
console.log(
|
|
43
|
-
`Saved ${
|
|
73
|
+
`Saved ${processedRecordings.length} HTTP recordings and ${session.websocketRecordings?.length || 0} WebSocket recordings to ${filePath}`
|
|
44
74
|
);
|
|
45
75
|
}
|
|
46
76
|
function getReqID(req) {
|
|
@@ -48,10 +78,10 @@ function getReqID(req) {
|
|
|
48
78
|
const pathname = urlParts[0];
|
|
49
79
|
const query = urlParts[1] || "";
|
|
50
80
|
const pathPart = pathname === "/" ? "root" : pathname.slice(1);
|
|
51
|
-
const normalizedPath =
|
|
81
|
+
const normalizedPath = filenamify2(pathPart, { replacement: "_" });
|
|
52
82
|
const queryHash = generateQueryHash(query);
|
|
53
83
|
const filename = `${req.method}_${normalizedPath}${queryHash}.json`;
|
|
54
|
-
return
|
|
84
|
+
return filenamify2(filename, { replacement: "_" });
|
|
55
85
|
}
|
|
56
86
|
function generateQueryHash(query) {
|
|
57
87
|
if (!query) {
|
|
@@ -86,12 +116,10 @@ var ProxyServer = class {
|
|
|
86
116
|
proxy;
|
|
87
117
|
currentSession;
|
|
88
118
|
recordingsDir;
|
|
89
|
-
requestSequenceMap;
|
|
90
|
-
// Track sequence per request key
|
|
91
|
-
replaySequenceMap;
|
|
92
|
-
// Track replay position per request key
|
|
93
119
|
recordingIdCounter;
|
|
94
120
|
// Unique ID for each recording entry
|
|
121
|
+
replaySessions;
|
|
122
|
+
// Track multiple concurrent replay sessions by recording ID
|
|
95
123
|
constructor(targets, recordingsDir) {
|
|
96
124
|
this.targets = targets;
|
|
97
125
|
this.currentTargetIndex = 0;
|
|
@@ -102,11 +130,11 @@ var ProxyServer = class {
|
|
|
102
130
|
this.modeTimeout = null;
|
|
103
131
|
this.currentSession = null;
|
|
104
132
|
this.recordingsDir = recordingsDir;
|
|
105
|
-
this.
|
|
106
|
-
this.replaySequenceMap = /* @__PURE__ */ new Map();
|
|
133
|
+
this.replaySessions = /* @__PURE__ */ new Map();
|
|
107
134
|
this.proxy = httpProxy.createProxyServer({
|
|
108
135
|
secure: false,
|
|
109
|
-
changeOrigin: true
|
|
136
|
+
changeOrigin: true,
|
|
137
|
+
ws: true
|
|
110
138
|
});
|
|
111
139
|
this.setupProxyEventHandlers();
|
|
112
140
|
}
|
|
@@ -174,6 +202,43 @@ var ProxyServer = class {
|
|
|
174
202
|
this.currentTargetIndex = (this.currentTargetIndex + 1) % this.targets.length;
|
|
175
203
|
return target;
|
|
176
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* Extract recording ID from request cookie
|
|
207
|
+
* Used for concurrent replay session routing
|
|
208
|
+
* @param req The incoming HTTP request
|
|
209
|
+
* @returns The recording ID from cookie, or null if not found
|
|
210
|
+
*/
|
|
211
|
+
getRecordingIdFromCookie(req) {
|
|
212
|
+
const cookies = req.headers.cookie;
|
|
213
|
+
if (!cookies) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const match = cookies.match(/proxy-recording-id=([^;]+)/);
|
|
217
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Get or create a replay session state for a given recording ID
|
|
221
|
+
* @param recordingId The recording ID to get/create session for
|
|
222
|
+
* @returns The replay session state
|
|
223
|
+
*/
|
|
224
|
+
getOrCreateReplaySession(recordingId) {
|
|
225
|
+
let session = this.replaySessions.get(recordingId);
|
|
226
|
+
if (session) {
|
|
227
|
+
session.lastAccessTime = Date.now();
|
|
228
|
+
} else {
|
|
229
|
+
session = {
|
|
230
|
+
recordingId,
|
|
231
|
+
servedRecordingIdsByKey: /* @__PURE__ */ new Map(),
|
|
232
|
+
loadedSession: null,
|
|
233
|
+
lastAccessTime: Date.now()
|
|
234
|
+
};
|
|
235
|
+
this.replaySessions.set(recordingId, session);
|
|
236
|
+
console.log(
|
|
237
|
+
`[CONCURRENT REPLAY] Created new session for recording: ${recordingId}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
return session;
|
|
241
|
+
}
|
|
177
242
|
parseGetParams(req) {
|
|
178
243
|
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
|
179
244
|
const mode = url.searchParams.get("mode");
|
|
@@ -190,16 +255,25 @@ var ProxyServer = class {
|
|
|
190
255
|
let data;
|
|
191
256
|
if (req.method === "GET") {
|
|
192
257
|
data = this.parseGetParams(req);
|
|
193
|
-
} else {
|
|
258
|
+
} else if (req.method === "POST") {
|
|
194
259
|
const body = await readRequestBody(req);
|
|
195
|
-
console.log(
|
|
260
|
+
console.log(`MODE CHANGE (${req.method})`, body);
|
|
196
261
|
data = JSON.parse(body);
|
|
262
|
+
} else {
|
|
263
|
+
return;
|
|
197
264
|
}
|
|
198
265
|
const { mode, id, timeout: requestTimeout } = data;
|
|
199
266
|
const timeout = requestTimeout ?? DEFAULT_TIMEOUT_MS;
|
|
200
267
|
this.clearModeTimeout();
|
|
201
268
|
await this.switchMode(mode, id);
|
|
202
269
|
this.setupModeTimeout(timeout);
|
|
270
|
+
if (mode === Modes.replay && id) {
|
|
271
|
+
res.setHeader(
|
|
272
|
+
"Set-Cookie",
|
|
273
|
+
`proxy-recording-id=${encodeURIComponent(id)}; HttpOnly; Path=/; SameSite=Lax`
|
|
274
|
+
);
|
|
275
|
+
console.log(`[CONCURRENT REPLAY] Set cookie for recording: ${id}`);
|
|
276
|
+
}
|
|
203
277
|
sendJsonResponse(res, HTTP_STATUS_OK, {
|
|
204
278
|
success: true,
|
|
205
279
|
mode: this.mode,
|
|
@@ -214,14 +288,12 @@ var ProxyServer = class {
|
|
|
214
288
|
}
|
|
215
289
|
}
|
|
216
290
|
clearModeTimeout() {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
this.modeTimeout = null;
|
|
220
|
-
}
|
|
291
|
+
clearTimeout(this.modeTimeout || 0);
|
|
292
|
+
this.modeTimeout = null;
|
|
221
293
|
}
|
|
222
294
|
async switchMode(mode, id) {
|
|
223
|
-
|
|
224
|
-
|
|
295
|
+
console.log(`Switching to ${mode.toUpperCase()} mode`);
|
|
296
|
+
if (this.currentSession && this.mode === Modes.record) {
|
|
225
297
|
await this.saveCurrentSession(true);
|
|
226
298
|
console.log("Session saved, continuing with mode switch");
|
|
227
299
|
}
|
|
@@ -231,11 +303,17 @@ var ProxyServer = class {
|
|
|
231
303
|
break;
|
|
232
304
|
}
|
|
233
305
|
case Modes.record: {
|
|
306
|
+
if (!id) {
|
|
307
|
+
throw new Error("Record ID is required");
|
|
308
|
+
}
|
|
234
309
|
this.switchToRecordMode(id);
|
|
235
310
|
break;
|
|
236
311
|
}
|
|
237
312
|
case Modes.replay: {
|
|
238
|
-
|
|
313
|
+
if (!id) {
|
|
314
|
+
throw new Error("Replay ID is required");
|
|
315
|
+
}
|
|
316
|
+
await this.switchToReplayMode(id);
|
|
239
317
|
break;
|
|
240
318
|
}
|
|
241
319
|
default: {
|
|
@@ -252,36 +330,33 @@ var ProxyServer = class {
|
|
|
252
330
|
console.log("Switched to transparent mode");
|
|
253
331
|
}
|
|
254
332
|
switchToRecordMode(id) {
|
|
255
|
-
if (!id) {
|
|
256
|
-
throw new Error("Record ID is required");
|
|
257
|
-
}
|
|
258
333
|
this.mode = Modes.record;
|
|
259
334
|
this.recordingId = id;
|
|
260
335
|
this.replayId = null;
|
|
261
336
|
this.currentSession = { id, recordings: [], websocketRecordings: [] };
|
|
262
|
-
this.requestSequenceMap.clear();
|
|
263
337
|
console.log(`Switched to record mode with ID: ${id}`);
|
|
264
338
|
}
|
|
265
|
-
switchToReplayMode(id) {
|
|
266
|
-
if (!id) {
|
|
267
|
-
throw new Error("Replay ID is required");
|
|
268
|
-
}
|
|
339
|
+
async switchToReplayMode(id) {
|
|
269
340
|
this.mode = Modes.replay;
|
|
270
341
|
this.replayId = id;
|
|
271
342
|
this.recordingId = null;
|
|
272
343
|
this.currentSession = null;
|
|
273
|
-
this.
|
|
344
|
+
const session = this.replaySessions.get(id);
|
|
345
|
+
if (session) {
|
|
346
|
+
session.servedRecordingIdsByKey.clear();
|
|
347
|
+
console.log(`Reset served recordings tracker for session: ${id}`);
|
|
348
|
+
} else {
|
|
349
|
+
this.getOrCreateReplaySession(id);
|
|
350
|
+
}
|
|
274
351
|
console.log(`Switched to replay mode with ID: ${id}`);
|
|
275
352
|
}
|
|
276
353
|
setupModeTimeout(timeout) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}, timeout);
|
|
284
|
-
}
|
|
354
|
+
this.modeTimeout = setTimeout(async () => {
|
|
355
|
+
console.log("Timeout reached, switching back to transparent mode");
|
|
356
|
+
await this.saveCurrentSession(true);
|
|
357
|
+
this.switchToTransparentMode();
|
|
358
|
+
this.modeTimeout = null;
|
|
359
|
+
}, timeout);
|
|
285
360
|
}
|
|
286
361
|
async saveCurrentSession(filterIncomplete = false) {
|
|
287
362
|
if (!this.currentSession) {
|
|
@@ -307,9 +382,6 @@ var ProxyServer = class {
|
|
|
307
382
|
return;
|
|
308
383
|
}
|
|
309
384
|
const key = getReqID(req);
|
|
310
|
-
const currentSequence = this.requestSequenceMap.get(key) || 0;
|
|
311
|
-
const sequence = currentSequence;
|
|
312
|
-
this.requestSequenceMap.set(key, currentSequence + 1);
|
|
313
385
|
const recordingId = this.recordingIdCounter++;
|
|
314
386
|
req.__recordingId = recordingId;
|
|
315
387
|
const record = {
|
|
@@ -321,13 +393,12 @@ var ProxyServer = class {
|
|
|
321
393
|
},
|
|
322
394
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
323
395
|
key,
|
|
324
|
-
sequence,
|
|
325
396
|
recordingId
|
|
326
397
|
};
|
|
327
398
|
this.currentSession.recordings.push(record);
|
|
328
399
|
console.log(
|
|
329
400
|
// eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
330
|
-
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key},
|
|
401
|
+
`saveRequestRecordSync: Saved ${req.method} ${req.url} (key: ${key}, recordingId: ${recordingId}, body: ${body ? `${body.length} chars` : "null"}, total: ${this.currentSession.recordings.length}, sessionId: ${this.currentSession.id})`
|
|
331
402
|
);
|
|
332
403
|
}
|
|
333
404
|
updateRequestBodySync(req, body) {
|
|
@@ -387,7 +458,7 @@ var ProxyServer = class {
|
|
|
387
458
|
body: body || null
|
|
388
459
|
};
|
|
389
460
|
console.log(
|
|
390
|
-
`Recorded: ${req.method} ${req.url} (
|
|
461
|
+
`Recorded: ${req.method} ${req.url} (recordingId: ${recordingId})`
|
|
391
462
|
);
|
|
392
463
|
});
|
|
393
464
|
}
|
|
@@ -417,46 +488,77 @@ var ProxyServer = class {
|
|
|
417
488
|
body: body || null
|
|
418
489
|
};
|
|
419
490
|
console.log(
|
|
420
|
-
`recordResponseData: Recorded response for ${req.method} ${req.url} (
|
|
491
|
+
`recordResponseData: Recorded response for ${req.method} ${req.url} (recordingId: ${recordingId})`
|
|
421
492
|
);
|
|
422
493
|
return true;
|
|
423
494
|
}
|
|
424
495
|
async handleReplayRequest(req, res) {
|
|
496
|
+
const recordingId = this.getRecordingIdFromCookie(req) || this.replayId;
|
|
497
|
+
if (!recordingId) {
|
|
498
|
+
const corsHeaders = this.getCorsHeaders(req);
|
|
499
|
+
res.writeHead(HTTP_STATUS_BAD_REQUEST, {
|
|
500
|
+
"Content-Type": "application/json",
|
|
501
|
+
...corsHeaders
|
|
502
|
+
});
|
|
503
|
+
res.end(JSON.stringify({ error: "No replay session active" }));
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
425
506
|
const key = getReqID(req);
|
|
426
|
-
const filePath = getRecordingPath(this.recordingsDir,
|
|
507
|
+
const filePath = getRecordingPath(this.recordingsDir, recordingId);
|
|
427
508
|
try {
|
|
428
|
-
const
|
|
509
|
+
const sessionState = this.getOrCreateReplaySession(recordingId);
|
|
510
|
+
if (!sessionState.loadedSession) {
|
|
511
|
+
sessionState.loadedSession = await loadRecordingSession(filePath);
|
|
512
|
+
console.log(`[REPLAY] Loaded recording session: ${recordingId}`);
|
|
513
|
+
}
|
|
514
|
+
const session = sessionState.loadedSession;
|
|
515
|
+
if (!sessionState.servedRecordingIdsByKey.has(key)) {
|
|
516
|
+
sessionState.servedRecordingIdsByKey.set(key, /* @__PURE__ */ new Set());
|
|
517
|
+
}
|
|
518
|
+
const servedForThisKey = sessionState.servedRecordingIdsByKey.get(key);
|
|
429
519
|
const host = req.headers.host || "unknown";
|
|
430
|
-
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.
|
|
520
|
+
const recordsWithKey = session.recordings.filter((r) => r.key === key && r.response).toSorted((a, b) => a.recordingId - b.recordingId);
|
|
431
521
|
if (recordsWithKey.length === 0) {
|
|
432
|
-
|
|
433
|
-
|
|
522
|
+
const errorMsg = `No recording found for ${key} at ${req.method} ${host}${req.url}`;
|
|
523
|
+
console.error(`[REPLAY ERROR] ${errorMsg} (session: ${recordingId})`);
|
|
524
|
+
console.error(
|
|
525
|
+
`[REPLAY ERROR] This request was not made during recording - possible test non-determinism`
|
|
434
526
|
);
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
}
|
|
527
|
+
const errorResponse = {
|
|
528
|
+
error: "No recording found",
|
|
529
|
+
message: errorMsg,
|
|
530
|
+
key,
|
|
531
|
+
sessionId: recordingId
|
|
532
|
+
};
|
|
441
533
|
const corsHeaders = this.getCorsHeaders(req);
|
|
442
|
-
res.writeHead(
|
|
534
|
+
res.writeHead(HTTP_STATUS_NOT_FOUND, {
|
|
443
535
|
"Content-Type": "application/json",
|
|
444
536
|
...corsHeaders
|
|
445
537
|
});
|
|
446
|
-
res.end(JSON.stringify(
|
|
538
|
+
res.end(JSON.stringify(errorResponse));
|
|
447
539
|
return;
|
|
448
540
|
}
|
|
449
|
-
const
|
|
541
|
+
const requestCount = servedForThisKey.size + 1;
|
|
542
|
+
console.log(
|
|
543
|
+
`[REPLAY REQUEST #${requestCount}] ${req.method} ${req.url} (session: ${recordingId}, total: ${recordsWithKey.length}, served: ${servedForThisKey.size})`
|
|
544
|
+
);
|
|
450
545
|
let record;
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
546
|
+
for (const rec of recordsWithKey) {
|
|
547
|
+
if (!servedForThisKey.has(rec.recordingId)) {
|
|
548
|
+
record = rec;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (!record) {
|
|
553
|
+
console.log(
|
|
554
|
+
`[REPLAY WARNING] All ${recordsWithKey.length} recordings already served for ${key} (session: ${recordingId}), reusing last one`
|
|
555
|
+
);
|
|
454
556
|
record = recordsWithKey[recordsWithKey.length - 1];
|
|
455
557
|
}
|
|
558
|
+
servedForThisKey.add(record.recordingId);
|
|
456
559
|
console.log(
|
|
457
|
-
`
|
|
560
|
+
`[REPLAY SERVING] recordingId: ${record.recordingId}, session: ${recordingId}, body_len: ${record.response?.body?.length || 0}`
|
|
458
561
|
);
|
|
459
|
-
this.replaySequenceMap.set(key, usageCount + 1);
|
|
460
562
|
if (!record.response) {
|
|
461
563
|
throw new Error(
|
|
462
564
|
`No response recorded for this request: ${req.method} ${host}${req.url}`
|
|
@@ -522,6 +624,7 @@ var ProxyServer = class {
|
|
|
522
624
|
this.proxy.web(req, res, { target });
|
|
523
625
|
}
|
|
524
626
|
}
|
|
627
|
+
// TODO: check if can handle streaming requests
|
|
525
628
|
async bufferAndProxyRequest(req, res, target) {
|
|
526
629
|
const chunks = [];
|
|
527
630
|
req.on("data", (chunk) => {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@playwright/test';
|
|
2
|
-
export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-
|
|
2
|
+
export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-Cx_Kflfl.cjs';
|
|
3
3
|
import 'node:http';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@playwright/test';
|
|
2
|
-
export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-
|
|
2
|
+
export { P as PlaywrightTestInfo, g as generateSessionId, p as playwrightProxy, s as setProxyMode, b as startRecording, c as startReplay, d as stopProxy } from '../index-Cx_Kflfl.js';
|
|
3
3
|
import 'node:http';
|