doru 1.0.0
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 +248 -0
- package/dist/cli.js +940 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.mts +100 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.js +631 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +605 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ui/app.js +29334 -0
- package/dist/ui/app.js.map +1 -0
- package/dist/ui/favicon-paused.svg +20 -0
- package/dist/ui/favicon.svg +30 -0
- package/dist/ui/index.html +14 -0
- package/dist/ui/styles.css +1293 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var zod = require('zod');
|
|
4
|
+
var interceptors = require('@mswjs/interceptors');
|
|
5
|
+
var ClientRequest = require('@mswjs/interceptors/ClientRequest');
|
|
6
|
+
var fetch = require('@mswjs/interceptors/fetch');
|
|
7
|
+
var zlib = require('zlib');
|
|
8
|
+
var fs = require('fs');
|
|
9
|
+
var path = require('path');
|
|
10
|
+
|
|
11
|
+
function _interopNamespace(e) {
|
|
12
|
+
if (e && e.__esModule) return e;
|
|
13
|
+
var n = Object.create(null);
|
|
14
|
+
if (e) {
|
|
15
|
+
Object.keys(e).forEach(function (k) {
|
|
16
|
+
if (k !== 'default') {
|
|
17
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
18
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
get: function () { return e[k]; }
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
n.default = e;
|
|
26
|
+
return Object.freeze(n);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
var zlib__namespace = /*#__PURE__*/_interopNamespace(zlib);
|
|
30
|
+
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
31
|
+
var path__namespace = /*#__PURE__*/_interopNamespace(path);
|
|
32
|
+
|
|
33
|
+
// src/core/config.ts
|
|
34
|
+
var statusCodesSchema = zod.z.object({
|
|
35
|
+
min: zod.z.number().int().min(100).max(599).nullable().default(null),
|
|
36
|
+
max: zod.z.number().int().min(100).max(599).nullable().default(null)
|
|
37
|
+
}).refine((s) => s.min === null || s.max === null || s.min <= s.max, {
|
|
38
|
+
message: "filters.statusCodes.min must be <= filters.statusCodes.max"
|
|
39
|
+
});
|
|
40
|
+
var filtersSchema = zod.z.object({
|
|
41
|
+
includeHosts: zod.z.array(zod.z.string()).default([]),
|
|
42
|
+
excludeHosts: zod.z.array(zod.z.string()).default([]),
|
|
43
|
+
includePaths: zod.z.array(zod.z.string()).default([]),
|
|
44
|
+
excludePaths: zod.z.array(zod.z.string()).default([]),
|
|
45
|
+
methods: zod.z.array(zod.z.string()).default([]),
|
|
46
|
+
statusCodes: statusCodesSchema.default({ min: null, max: null }),
|
|
47
|
+
minSize: zod.z.number().int().nullable().default(null),
|
|
48
|
+
maxSize: zod.z.number().int().nullable().default(null)
|
|
49
|
+
});
|
|
50
|
+
var performanceSchema = zod.z.object({
|
|
51
|
+
bufferSize: zod.z.number().int().positive().default(1024),
|
|
52
|
+
flushInterval: zod.z.number().int().positive().default(1e3),
|
|
53
|
+
compression: zod.z.boolean().default(false)
|
|
54
|
+
// minimal core: compression disabled by default
|
|
55
|
+
});
|
|
56
|
+
var doruConfigSchema = zod.z.object({
|
|
57
|
+
enabled: zod.z.boolean().default(true),
|
|
58
|
+
outputFile: zod.z.string().default("./network-capture.jsonl"),
|
|
59
|
+
format: zod.z.enum(["json", "jsonl"]).default("jsonl"),
|
|
60
|
+
maxFileSize: zod.z.number().int().positive().default(10 * 1024 * 1024),
|
|
61
|
+
// 10MB
|
|
62
|
+
filters: filtersSchema.default({
|
|
63
|
+
includeHosts: [],
|
|
64
|
+
excludeHosts: [],
|
|
65
|
+
includePaths: [],
|
|
66
|
+
excludePaths: [],
|
|
67
|
+
methods: [],
|
|
68
|
+
statusCodes: { min: null, max: null },
|
|
69
|
+
minSize: null,
|
|
70
|
+
maxSize: null
|
|
71
|
+
}),
|
|
72
|
+
performance: performanceSchema.default({
|
|
73
|
+
bufferSize: 1024,
|
|
74
|
+
flushInterval: 1e3,
|
|
75
|
+
compression: false
|
|
76
|
+
}),
|
|
77
|
+
debug: zod.z.boolean().default(false)
|
|
78
|
+
});
|
|
79
|
+
var deepMerge = (a, b) => {
|
|
80
|
+
const out = { ...a };
|
|
81
|
+
for (const [k, v] of Object.entries(b || {})) {
|
|
82
|
+
if (v === void 0) continue;
|
|
83
|
+
const key = k;
|
|
84
|
+
const av = a[key];
|
|
85
|
+
if (Array.isArray(v)) {
|
|
86
|
+
out[key] = v.slice();
|
|
87
|
+
} else if (v && typeof v === "object" && av && typeof av === "object" && !Array.isArray(av)) {
|
|
88
|
+
out[key] = deepMerge(
|
|
89
|
+
av,
|
|
90
|
+
v
|
|
91
|
+
);
|
|
92
|
+
} else out[key] = v;
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
};
|
|
96
|
+
var envDebugDefault = () => {
|
|
97
|
+
const v = process.env.DORU_DEBUG;
|
|
98
|
+
if (v === "true") return true;
|
|
99
|
+
if (v === "false") return false;
|
|
100
|
+
const legacy = process.env.NETPEEK_DEBUG;
|
|
101
|
+
if (legacy === "true") return true;
|
|
102
|
+
if (legacy === "false") return false;
|
|
103
|
+
return false;
|
|
104
|
+
};
|
|
105
|
+
var normalizeConfig = (c) => {
|
|
106
|
+
return {
|
|
107
|
+
...c,
|
|
108
|
+
filters: {
|
|
109
|
+
...c.filters,
|
|
110
|
+
methods: c.filters.methods.map((m) => m.toUpperCase())
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
var createDefaultConfig = (user = {}) => {
|
|
115
|
+
const defaults = doruConfigSchema.parse({});
|
|
116
|
+
const merged = deepMerge(defaults, { ...user, debug: user.debug ?? envDebugDefault() });
|
|
117
|
+
const parsed = doruConfigSchema.parse(merged);
|
|
118
|
+
return normalizeConfig(parsed);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// src/core/filters.ts
|
|
122
|
+
var FilterEngine = class _FilterEngine {
|
|
123
|
+
constructor(cfg) {
|
|
124
|
+
this.cfg = cfg;
|
|
125
|
+
}
|
|
126
|
+
updateConfig(next) {
|
|
127
|
+
this.cfg = next;
|
|
128
|
+
}
|
|
129
|
+
shouldCaptureRequest(req) {
|
|
130
|
+
if (!this.checkHost(req.hostname)) return false;
|
|
131
|
+
if (!this.checkPath(req.path)) return false;
|
|
132
|
+
if (!this.checkMethod(req.method)) return false;
|
|
133
|
+
if (!this.checkSize(req.body)) return false;
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
shouldCaptureResponse(res) {
|
|
137
|
+
if (!this.checkStatus(res.statusCode)) return false;
|
|
138
|
+
if (!this.checkSize(res.body)) return false;
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
checkHost(host) {
|
|
142
|
+
const { includeHosts, excludeHosts } = this.cfg;
|
|
143
|
+
if (includeHosts.length > 0) {
|
|
144
|
+
const ok = includeHosts.some((p) => _FilterEngine.matchStringOrRegex(host, p));
|
|
145
|
+
if (!ok) return false;
|
|
146
|
+
}
|
|
147
|
+
if (excludeHosts.length > 0) {
|
|
148
|
+
const hit = excludeHosts.some((p) => _FilterEngine.matchStringOrRegex(host, p));
|
|
149
|
+
if (hit) return false;
|
|
150
|
+
}
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
checkPath(path2) {
|
|
154
|
+
const { includePaths, excludePaths } = this.cfg;
|
|
155
|
+
if (includePaths.length > 0) {
|
|
156
|
+
const ok = includePaths.some((p) => _FilterEngine.matchRegexSafe(path2, p));
|
|
157
|
+
if (!ok) return false;
|
|
158
|
+
}
|
|
159
|
+
if (excludePaths.length > 0) {
|
|
160
|
+
const hit = excludePaths.some((p) => _FilterEngine.matchRegexSafe(path2, p));
|
|
161
|
+
if (hit) return false;
|
|
162
|
+
}
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
checkMethod(method) {
|
|
166
|
+
const { methods } = this.cfg;
|
|
167
|
+
if (methods.length === 0) return true;
|
|
168
|
+
return methods.includes(method.toUpperCase());
|
|
169
|
+
}
|
|
170
|
+
checkStatus(code) {
|
|
171
|
+
const { min, max } = this.cfg.statusCodes;
|
|
172
|
+
if (min !== null && code < min) return false;
|
|
173
|
+
if (max !== null && code > max) return false;
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
checkSize(body) {
|
|
177
|
+
if (!body) return true;
|
|
178
|
+
const size = Buffer.isBuffer(body) ? body.length : Buffer.byteLength(body);
|
|
179
|
+
const { minSize, maxSize } = this.cfg;
|
|
180
|
+
if (minSize !== null && size < minSize) return false;
|
|
181
|
+
if (maxSize !== null && size > maxSize) return false;
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
// accept user-provided regex in /.../ form, otherwise do substring match
|
|
185
|
+
static matchStringOrRegex(value, pattern) {
|
|
186
|
+
if (pattern.startsWith("/") && pattern.endsWith("/")) {
|
|
187
|
+
try {
|
|
188
|
+
return new RegExp(pattern.slice(1, -1)).test(value);
|
|
189
|
+
} catch {
|
|
190
|
+
return value.includes(pattern);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return value.includes(pattern);
|
|
194
|
+
}
|
|
195
|
+
static matchRegexSafe(value, pattern) {
|
|
196
|
+
try {
|
|
197
|
+
return new RegExp(pattern).test(value);
|
|
198
|
+
} catch {
|
|
199
|
+
return value.includes(pattern);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// src/core/logging.ts
|
|
205
|
+
var DebugLogger = class {
|
|
206
|
+
constructor(cfg) {
|
|
207
|
+
this.cfg = cfg;
|
|
208
|
+
}
|
|
209
|
+
info(msg, data) {
|
|
210
|
+
if (!this.cfg.debug) return;
|
|
211
|
+
if (data === void 0) console.log(msg);
|
|
212
|
+
else console.log(msg, data);
|
|
213
|
+
}
|
|
214
|
+
error(msg, err) {
|
|
215
|
+
if (!this.cfg.debug) return;
|
|
216
|
+
if (err === void 0) console.error(msg);
|
|
217
|
+
else console.error(msg, err);
|
|
218
|
+
}
|
|
219
|
+
tag(tag, msg, data) {
|
|
220
|
+
this.info(`${tag} ${msg}`, data);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
var createDebugLogger = (cfg) => new DebugLogger(cfg);
|
|
224
|
+
|
|
225
|
+
// src/core/interceptor.ts
|
|
226
|
+
var isErrorPayload = (p) => {
|
|
227
|
+
return p !== null && typeof p === "object" && "requestId" in p && typeof p.requestId === "string";
|
|
228
|
+
};
|
|
229
|
+
var NetworkInterceptor = class _NetworkInterceptor {
|
|
230
|
+
constructor(cfg, storage) {
|
|
231
|
+
this.started = false;
|
|
232
|
+
this.counter = 0;
|
|
233
|
+
this.pendings = /* @__PURE__ */ new Map();
|
|
234
|
+
this.storage = storage;
|
|
235
|
+
this.filter = new FilterEngine(cfg.filters);
|
|
236
|
+
this.log = createDebugLogger(cfg);
|
|
237
|
+
this.interceptor = new interceptors.BatchInterceptor({
|
|
238
|
+
name: "doru",
|
|
239
|
+
interceptors: [new ClientRequest.ClientRequestInterceptor(), new fetch.FetchInterceptor()]
|
|
240
|
+
});
|
|
241
|
+
this.wire();
|
|
242
|
+
}
|
|
243
|
+
wire() {
|
|
244
|
+
this.interceptor.on(
|
|
245
|
+
"request",
|
|
246
|
+
async ({ request, requestId }) => {
|
|
247
|
+
try {
|
|
248
|
+
if (request.url.startsWith("data:") || request.url.startsWith("blob:")) return;
|
|
249
|
+
const captureId = _NetworkInterceptor.newCaptureId();
|
|
250
|
+
const start = Date.now();
|
|
251
|
+
const req = await _NetworkInterceptor.getRequestData(request);
|
|
252
|
+
if (!this.filter.shouldCaptureRequest(req)) {
|
|
253
|
+
this.log.tag("\u{1F4E4}", "Request filtered", req.hostname);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
this.pendings.set(requestId, {
|
|
257
|
+
captureId,
|
|
258
|
+
start,
|
|
259
|
+
hostname: req.hostname,
|
|
260
|
+
method: req.method
|
|
261
|
+
});
|
|
262
|
+
this.captureRequest(captureId, req);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
this.log.error("Request intercept error", error);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
this.interceptor.on(
|
|
269
|
+
"response",
|
|
270
|
+
async ({ response, requestId }) => {
|
|
271
|
+
try {
|
|
272
|
+
const pending = this.pendings.get(requestId);
|
|
273
|
+
if (!pending) return;
|
|
274
|
+
this.pendings.delete(requestId);
|
|
275
|
+
const res = await _NetworkInterceptor.getResponseData(response);
|
|
276
|
+
if (!this.filter.shouldCaptureResponse(res)) return;
|
|
277
|
+
this.captureResponse(pending.captureId, res, pending.start);
|
|
278
|
+
} catch (error) {
|
|
279
|
+
this.log.error("Response intercept error", error);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
const handleErrorEvent = (event) => (payload) => {
|
|
284
|
+
if (!isErrorPayload(payload)) return;
|
|
285
|
+
const { error, requestId } = payload;
|
|
286
|
+
try {
|
|
287
|
+
const pending = this.pendings.get(requestId);
|
|
288
|
+
if (!pending) return;
|
|
289
|
+
this.pendings.delete(requestId);
|
|
290
|
+
this.captureError(pending.captureId, error, pending.start);
|
|
291
|
+
} catch (error_) {
|
|
292
|
+
this.log.error(`${event} handler error`, error_);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
this.interceptor.on("unhandledException", handleErrorEvent("unhandledException"));
|
|
296
|
+
}
|
|
297
|
+
start() {
|
|
298
|
+
if (this.started) return;
|
|
299
|
+
this.started = true;
|
|
300
|
+
this.interceptor.apply();
|
|
301
|
+
this.log.tag("\u{1F527}", "Network interception started");
|
|
302
|
+
}
|
|
303
|
+
stop() {
|
|
304
|
+
if (!this.started) return;
|
|
305
|
+
this.started = false;
|
|
306
|
+
this.interceptor.dispose();
|
|
307
|
+
this.pendings.clear();
|
|
308
|
+
this.log.tag("\u{1F527}", "Network interception stopped");
|
|
309
|
+
}
|
|
310
|
+
updateConfig(next) {
|
|
311
|
+
this.filter.updateConfig(next.filters);
|
|
312
|
+
}
|
|
313
|
+
static async getRequestData(request) {
|
|
314
|
+
const url = new URL(request.url);
|
|
315
|
+
const headers = {};
|
|
316
|
+
for (const [k, v] of request.headers.entries()) {
|
|
317
|
+
headers[k.toLowerCase()] = v;
|
|
318
|
+
}
|
|
319
|
+
let body = null;
|
|
320
|
+
if (request.body && ["PATCH", "POST", "PUT"].includes(request.method.toUpperCase())) {
|
|
321
|
+
try {
|
|
322
|
+
const cloned = request.clone();
|
|
323
|
+
const ab = await cloned.arrayBuffer();
|
|
324
|
+
body = ab.byteLength > 0 ? Buffer.from(ab) : null;
|
|
325
|
+
} catch {
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
method: request.method,
|
|
330
|
+
hostname: url.hostname,
|
|
331
|
+
port: url.port ? Number.parseInt(url.port) : void 0,
|
|
332
|
+
path: url.pathname + url.search,
|
|
333
|
+
protocol: url.protocol.replace(/:$/, ""),
|
|
334
|
+
headers,
|
|
335
|
+
body
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
static async getResponseData(response) {
|
|
339
|
+
const headers = {};
|
|
340
|
+
for (const [k, v] of response.headers.entries()) {
|
|
341
|
+
headers[k.toLowerCase()] = v;
|
|
342
|
+
}
|
|
343
|
+
let body = null;
|
|
344
|
+
try {
|
|
345
|
+
const cloned = response.clone();
|
|
346
|
+
const ab = await cloned.arrayBuffer();
|
|
347
|
+
if (ab.byteLength > 0) {
|
|
348
|
+
body = Buffer.from(ab);
|
|
349
|
+
const enc = headers["content-encoding"];
|
|
350
|
+
if (enc && enc.includes("gzip") && body) {
|
|
351
|
+
try {
|
|
352
|
+
const head = body.toString("utf8", 0, Math.min(body.length, 100));
|
|
353
|
+
const looksText = /^[\s\u0020-\u007E]*$/.test(head) && /^\s*[<[{]/.test(head.trim());
|
|
354
|
+
if (!looksText) body = zlib__namespace.gunzipSync(body);
|
|
355
|
+
} catch {
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
} catch {
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
statusCode: response.status,
|
|
363
|
+
statusMessage: response.statusText,
|
|
364
|
+
headers,
|
|
365
|
+
body
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
captureRequest(captureId, req) {
|
|
369
|
+
const rec = {
|
|
370
|
+
id: this.newRequestId(),
|
|
371
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
372
|
+
type: "request",
|
|
373
|
+
method: req.method,
|
|
374
|
+
hostname: req.hostname,
|
|
375
|
+
port: req.port,
|
|
376
|
+
path: req.path,
|
|
377
|
+
url: _NetworkInterceptor.buildUrl(req),
|
|
378
|
+
headers: req.headers,
|
|
379
|
+
body: req.body ?? null,
|
|
380
|
+
captureId
|
|
381
|
+
};
|
|
382
|
+
this.storage.write(rec);
|
|
383
|
+
}
|
|
384
|
+
captureResponse(captureId, res, start) {
|
|
385
|
+
const rec = {
|
|
386
|
+
id: this.newRequestId(),
|
|
387
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
388
|
+
type: "response",
|
|
389
|
+
statusCode: res.statusCode,
|
|
390
|
+
statusMessage: res.statusMessage,
|
|
391
|
+
headers: res.headers,
|
|
392
|
+
body: res.body ?? null,
|
|
393
|
+
duration: Date.now() - start,
|
|
394
|
+
captureId
|
|
395
|
+
};
|
|
396
|
+
this.storage.write(rec);
|
|
397
|
+
}
|
|
398
|
+
captureError(captureId, error, start) {
|
|
399
|
+
const rec = {
|
|
400
|
+
id: this.newRequestId(),
|
|
401
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
402
|
+
type: "error",
|
|
403
|
+
error: error?.message ?? String(error),
|
|
404
|
+
duration: Date.now() - start,
|
|
405
|
+
captureId
|
|
406
|
+
};
|
|
407
|
+
this.storage.write(rec);
|
|
408
|
+
}
|
|
409
|
+
static buildUrl(req) {
|
|
410
|
+
const proto = req.protocol;
|
|
411
|
+
const port = req.port && (proto === "http" && req.port !== 80 || proto === "https" && req.port !== 443) ? `:${req.port}` : "";
|
|
412
|
+
return `${proto}://${req.hostname}${port}${req.path}`;
|
|
413
|
+
}
|
|
414
|
+
static newCaptureId() {
|
|
415
|
+
return `cap-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
416
|
+
}
|
|
417
|
+
newRequestId() {
|
|
418
|
+
return `req-${++this.counter}-${Date.now()}`;
|
|
419
|
+
}
|
|
420
|
+
updateStorage(storage) {
|
|
421
|
+
this.storage = storage;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
// src/core/storage/data-serializer.ts
|
|
426
|
+
var DataSerializer = {
|
|
427
|
+
serialize(data, compact) {
|
|
428
|
+
const { body, ...rest } = data;
|
|
429
|
+
let outBody = null;
|
|
430
|
+
if (typeof body === "string") {
|
|
431
|
+
outBody = body;
|
|
432
|
+
} else if (body && Buffer.isBuffer(body)) {
|
|
433
|
+
if (DataSerializer.isTextContent(body, data.headers)) {
|
|
434
|
+
outBody = body.toString("utf8");
|
|
435
|
+
} else {
|
|
436
|
+
outBody = {
|
|
437
|
+
type: "buffer",
|
|
438
|
+
data: body.toString("base64"),
|
|
439
|
+
encoding: "base64"
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
const out = { ...rest, body: outBody };
|
|
444
|
+
return JSON.stringify(out, null, compact ? 0 : 2);
|
|
445
|
+
},
|
|
446
|
+
isTextContent(buffer, headers) {
|
|
447
|
+
if (buffer.length === 0) return true;
|
|
448
|
+
const getHeader = (name) => {
|
|
449
|
+
if (!headers) return void 0;
|
|
450
|
+
const v = headers[name] ?? headers[name.toLowerCase()] ?? headers[name.toUpperCase()];
|
|
451
|
+
return Array.isArray(v) ? v[0] : v;
|
|
452
|
+
};
|
|
453
|
+
const ct = getHeader("content-type");
|
|
454
|
+
if (ct) {
|
|
455
|
+
if (/^(text\/|application\/(json|xml|javascript|x-www-form-urlencoded))/.test(ct)) return true;
|
|
456
|
+
if (/^(image\/|video\/|audio\/|application\/(pdf|zip|octet-stream))/.test(ct)) return false;
|
|
457
|
+
}
|
|
458
|
+
const sampleSize = Math.min(buffer.length, 1024);
|
|
459
|
+
const sample = buffer.subarray(0, sampleSize);
|
|
460
|
+
let textish = 0;
|
|
461
|
+
for (const byte of sample) {
|
|
462
|
+
if (byte >= 32 && byte <= 126 || byte === 9 || byte === 10 || byte === 13 || byte >= 128) textish++;
|
|
463
|
+
}
|
|
464
|
+
return textish / sample.length > 0.9;
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
var FileManager = class _FileManager {
|
|
468
|
+
constructor(cfg) {
|
|
469
|
+
this.cfg = cfg;
|
|
470
|
+
}
|
|
471
|
+
getExt() {
|
|
472
|
+
return this.cfg.format === "jsonl" ? ".jsonl" : ".json";
|
|
473
|
+
}
|
|
474
|
+
genPath(counter) {
|
|
475
|
+
const ext = this.getExt();
|
|
476
|
+
const base = path__namespace.basename(this.cfg.outputFile, path__namespace.extname(this.cfg.outputFile));
|
|
477
|
+
const dir = path__namespace.dirname(this.cfg.outputFile);
|
|
478
|
+
const file = counter === 0 ? `${base}${ext}` : `${base}-${counter}${ext}`;
|
|
479
|
+
return path__namespace.join(dir, file);
|
|
480
|
+
}
|
|
481
|
+
static ensureDir(filePath) {
|
|
482
|
+
const dir = path__namespace.dirname(filePath);
|
|
483
|
+
if (!fs__namespace.existsSync(dir)) fs__namespace.mkdirSync(dir, { recursive: true });
|
|
484
|
+
}
|
|
485
|
+
shouldRotate(currentSize) {
|
|
486
|
+
return currentSize > this.cfg.maxFileSize;
|
|
487
|
+
}
|
|
488
|
+
static createWriteStream(filePath) {
|
|
489
|
+
_FileManager.ensureDir(filePath);
|
|
490
|
+
return fs__namespace.createWriteStream(filePath, { flags: "w", encoding: "utf8" });
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
// src/core/storage/formats.ts
|
|
495
|
+
var JsonFormatter = {
|
|
496
|
+
writeHeader(stream) {
|
|
497
|
+
stream.write("[\n");
|
|
498
|
+
},
|
|
499
|
+
formatEntry(json, isFirst) {
|
|
500
|
+
return isFirst ? ` ${json}` : `,
|
|
501
|
+
${json}`;
|
|
502
|
+
},
|
|
503
|
+
getHeaderSize() {
|
|
504
|
+
return 2;
|
|
505
|
+
},
|
|
506
|
+
close(stream, isFirst) {
|
|
507
|
+
if (isFirst) stream.write("]");
|
|
508
|
+
else stream.write("\n]");
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
var StreamFormatter = {
|
|
512
|
+
writeHeader() {
|
|
513
|
+
},
|
|
514
|
+
formatEntry(json) {
|
|
515
|
+
return `${json}
|
|
516
|
+
`;
|
|
517
|
+
},
|
|
518
|
+
getHeaderSize() {
|
|
519
|
+
return 0;
|
|
520
|
+
},
|
|
521
|
+
close() {
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
var createFormatHandler = (fmt) => fmt === "json" ? JsonFormatter : StreamFormatter;
|
|
525
|
+
|
|
526
|
+
// src/core/storage/storage.ts
|
|
527
|
+
var StorageManager = class {
|
|
528
|
+
constructor(config) {
|
|
529
|
+
this.stream = null;
|
|
530
|
+
this.size = 0;
|
|
531
|
+
this.counter = 0;
|
|
532
|
+
this.isFirst = true;
|
|
533
|
+
this.closed = false;
|
|
534
|
+
this.cfg = config;
|
|
535
|
+
this.fm = new FileManager(config);
|
|
536
|
+
this.fmt = createFormatHandler(config.format);
|
|
537
|
+
this.log = createDebugLogger(config);
|
|
538
|
+
this.initFile();
|
|
539
|
+
}
|
|
540
|
+
write(data) {
|
|
541
|
+
if (this.closed || !this.stream) return;
|
|
542
|
+
try {
|
|
543
|
+
if (this.fm.shouldRotate(this.size)) this.rotate();
|
|
544
|
+
const compact = this.cfg.format === "jsonl";
|
|
545
|
+
const json = DataSerializer.serialize(data, compact);
|
|
546
|
+
const chunk = this.fmt.formatEntry(json, this.isFirst);
|
|
547
|
+
this.stream.write(chunk);
|
|
548
|
+
this.size += Buffer.byteLength(chunk);
|
|
549
|
+
if (this.isFirst) this.isFirst = false;
|
|
550
|
+
this.log.tag("\u{1F4BE}", `Wrote ${Buffer.byteLength(chunk)} bytes (${data.type})`);
|
|
551
|
+
} catch (error) {
|
|
552
|
+
this.log.error("Storage write error:", error);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
close() {
|
|
556
|
+
if (this.closed || !this.stream) return;
|
|
557
|
+
try {
|
|
558
|
+
this.fmt.close(this.stream, this.isFirst);
|
|
559
|
+
this.stream.end();
|
|
560
|
+
this.stream = null;
|
|
561
|
+
this.closed = true;
|
|
562
|
+
this.log.tag("\u{1F4BE}", "Storage closed");
|
|
563
|
+
} catch {
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
initFile() {
|
|
567
|
+
try {
|
|
568
|
+
const filePath = this.fm.genPath(this.counter);
|
|
569
|
+
this.stream = FileManager.createWriteStream(filePath);
|
|
570
|
+
this.size = this.fmt.getHeaderSize();
|
|
571
|
+
this.isFirst = true;
|
|
572
|
+
this.fmt.writeHeader(this.stream);
|
|
573
|
+
if (!this.stream.writableNeedDrain) {
|
|
574
|
+
this.stream.cork();
|
|
575
|
+
this.stream.uncork();
|
|
576
|
+
}
|
|
577
|
+
this.log.tag("\u{1F4C1}", `Writing to ${filePath}`);
|
|
578
|
+
} catch (error) {
|
|
579
|
+
this.log.error("Storage init error:", error);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
rotate() {
|
|
583
|
+
if (this.stream) {
|
|
584
|
+
this.fmt.close(this.stream, this.isFirst);
|
|
585
|
+
this.stream.end();
|
|
586
|
+
}
|
|
587
|
+
this.counter++;
|
|
588
|
+
this.initFile();
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// src/index.ts
|
|
593
|
+
var createInterceptor = (userConfig = {}) => {
|
|
594
|
+
let config = createDefaultConfig(userConfig);
|
|
595
|
+
let storage = new StorageManager(config);
|
|
596
|
+
const interceptor = new NetworkInterceptor(config, storage);
|
|
597
|
+
const api = {
|
|
598
|
+
start() {
|
|
599
|
+
interceptor.start();
|
|
600
|
+
},
|
|
601
|
+
stop() {
|
|
602
|
+
try {
|
|
603
|
+
interceptor.stop();
|
|
604
|
+
} finally {
|
|
605
|
+
storage.close();
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
updateConfig(next) {
|
|
609
|
+
const updated = createDefaultConfig({ ...config, ...next });
|
|
610
|
+
const old = config;
|
|
611
|
+
config = updated;
|
|
612
|
+
interceptor.updateConfig(updated);
|
|
613
|
+
const changed = updated.outputFile !== old.outputFile || updated.format !== old.format || updated.maxFileSize !== old.maxFileSize || updated.performance.compression !== old.performance.compression;
|
|
614
|
+
if (changed) {
|
|
615
|
+
storage.close();
|
|
616
|
+
storage = new StorageManager(updated);
|
|
617
|
+
interceptor.updateStorage(storage);
|
|
618
|
+
}
|
|
619
|
+
},
|
|
620
|
+
getConfig() {
|
|
621
|
+
return config;
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
return api;
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
exports.NetworkInterceptor = NetworkInterceptor;
|
|
628
|
+
exports.StorageManager = StorageManager;
|
|
629
|
+
exports.createInterceptor = createInterceptor;
|
|
630
|
+
//# sourceMappingURL=index.js.map
|
|
631
|
+
//# sourceMappingURL=index.js.map
|