@tonyclaw/agent-inspector 3.0.29 → 3.0.30
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/.output/cli.js +791 -191
- package/.output/nitro.json +1 -1
- package/.output/public/assets/CompareDrawer-B6cXJohL.js +1 -0
- package/.output/public/assets/InspectorPet-mAzeGmEE.js +4108 -0
- package/.output/public/assets/ProxyViewerContainer-CkFWv_Nm.js +126 -0
- package/.output/public/assets/{ReplayDialog-CctnITbt.js → ReplayDialog-DodiTL6E.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-BrN0RBbL.js → RequestAnatomy-B3IauToI.js} +1 -1
- package/.output/public/assets/ResponseView-Hn-5ordK.js +3 -0
- package/.output/public/assets/StreamingChunkSequence-vLDOVQM_.js +1 -0
- package/.output/public/assets/_sessionId-BFH4TOaI.js +1 -0
- package/.output/public/assets/{_sessionId-DKM91udC.js → _sessionId-Cj_HIFZx.js} +1 -1
- package/.output/public/assets/{index-Bq-nqolG.js → index-Bmj2lcWE.js} +1 -1
- package/.output/public/assets/index-CgJj-HBC.css +1 -0
- package/.output/public/assets/index-Ch7uqnCT.js +1 -0
- package/.output/public/assets/index-DkDdKvLl.js +76 -0
- package/.output/public/assets/{index-DPiESBo2.js → index-ZlhqCrSg.js} +6 -6
- package/.output/public/assets/{json-viewer-CfaDJwx8.js → json-viewer-BdrnVQu-.js} +1 -1
- package/.output/public/assets/jszip.min-CS_nt_o1.js +2 -0
- package/.output/public/assets/qwen-mMn3f5ul.webp +0 -0
- package/.output/server/_libs/jszip.mjs +35 -9
- package/.output/server/_libs/lucide-react.mjs +86 -86
- package/.output/server/_libs/radix-ui__react-dialog.mjs +3 -3
- package/.output/server/_libs/tanstack__react-router.mjs +1 -1
- package/.output/server/{_sessionId-CwGXPD4C.mjs → _sessionId-C5S3pGZd.mjs} +15 -15
- package/.output/server/_sessionId-g6D69lKq.mjs +81 -0
- package/.output/server/_ssr/{CompareDrawer-R_P1-VNr.mjs → CompareDrawer-CqUmGHal.mjs} +138 -103
- package/.output/server/_ssr/InspectorPet-DI0UJd01.mjs +925 -0
- package/.output/server/_ssr/{ProxyViewerContainer-DUgxRNbj.mjs → ProxyViewerContainer-CvsUmbJ3.mjs} +1157 -1613
- package/.output/server/_ssr/{ReplayDialog-De9wKKzS.mjs → ReplayDialog-DS48dpFG.mjs} +18 -18
- package/.output/server/_ssr/{RequestAnatomy-Db4MxUYo.mjs → RequestAnatomy-DxYVQU1C.mjs} +58 -58
- package/.output/server/_ssr/{ResponseView-8rDXOiNB.mjs → ResponseView-BLXW73eq.mjs} +60 -60
- package/.output/server/_ssr/{StreamingChunkSequence-N9teZHkA.mjs → StreamingChunkSequence-DD0iFuy0.mjs} +16 -16
- package/.output/server/_ssr/{index-D4DvByuW.mjs → index-B10LIaK0.mjs} +15 -15
- package/.output/server/_ssr/index-RHVJnU2p.mjs +81 -0
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-BjoHRbjS.mjs → json-viewer-LDu2XBaK.mjs} +58 -58
- package/.output/server/_ssr/{router-BYlePDj8.mjs → router-GKvrwifJ.mjs} +9005 -6488
- package/.output/server/_tanstack-start-manifest_v-Df2n4mbT.mjs +4 -0
- package/.output/server/index.mjs +103 -75
- package/.output/workers/logFinalizer.worker.js +16908 -0
- package/.output/workers/sessionWorkerEntry.js +16904 -0
- package/README.md +21 -5
- package/package.json +6 -2
- package/src/assets/logos/qwen.webp +0 -0
- package/src/cli.ts +85 -10
- package/src/components/OnboardingBanner.tsx +41 -21
- package/src/components/ProxyViewer.tsx +198 -269
- package/src/components/ProxyViewerContainer.tsx +128 -17
- package/src/components/ecosystem/AgentLabDialog.tsx +166 -19
- package/src/components/errors/SafeErrorBoundary.tsx +201 -0
- package/src/components/pi-agent/PiAgentPanel.tsx +4 -1
- package/src/components/providers/ImportWizardDialog.tsx +18 -42
- package/src/components/providers/ProviderLogo.tsx +1 -1
- package/src/components/providers/ProvidersPanel.tsx +8 -5
- package/src/components/providers/SettingsDialog.tsx +34 -11
- package/src/components/proxy-viewer/ApplicationBar.tsx +63 -0
- package/src/components/proxy-viewer/CompareDrawer.tsx +92 -49
- package/src/components/proxy-viewer/LiveConnectionStatus.tsx +186 -0
- package/src/components/proxy-viewer/LogEntry.tsx +283 -275
- package/src/components/proxy-viewer/RawExportWarning.tsx +77 -0
- package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +61 -0
- package/src/components/proxy-viewer/ThreadConnector.tsx +16 -1
- package/src/components/proxy-viewer/TurnGroup.tsx +13 -2
- package/src/components/proxy-viewer/accessibility.ts +8 -0
- package/src/components/proxy-viewer/lazy.ts +4 -0
- package/src/components/proxy-viewer/liveConnectionState.ts +158 -0
- package/src/components/proxy-viewer/proxyViewerLogic.ts +10 -0
- package/src/components/proxy-viewer/viewerState.ts +2 -6
- package/src/components/ui/select.tsx +1 -1
- package/src/components/ui/tabs.tsx +3 -3
- package/src/components/ui/transient-toast.tsx +1 -1
- package/src/lib/apiClient.ts +17 -2
- package/src/lib/ecosystemContract.ts +34 -0
- package/src/lib/export-logs.ts +1 -1
- package/src/lib/providerContract.ts +70 -4
- package/src/lib/providerImportContract.ts +27 -0
- package/src/lib/providerModelMetadata.ts +16 -7
- package/src/lib/resourceLimits.ts +152 -0
- package/src/lib/safeDiagnostic.ts +38 -0
- package/src/lib/useProviders.ts +4 -4
- package/src/mcp/server.ts +39 -6
- package/src/mcp/toolHandlers.ts +131 -4
- package/src/proxy/chunkStorage.ts +20 -6
- package/src/proxy/config.ts +20 -6
- package/src/proxy/dataDir.ts +3 -0
- package/src/proxy/ecosystemExecutionConfirmation.ts +114 -0
- package/src/proxy/ecosystemExecutionRoute.ts +116 -0
- package/src/proxy/ecosystemTasks.ts +48 -0
- package/src/proxy/evidenceExporter.ts +23 -9
- package/src/proxy/groupEvidenceExporter.ts +26 -5
- package/src/proxy/groupStore.ts +13 -3
- package/src/proxy/handler.ts +103 -20
- package/src/proxy/identityProxy.ts +333 -14
- package/src/proxy/logFinalizer.ts +73 -6
- package/src/proxy/logImportUpload.ts +128 -0
- package/src/proxy/logImporter.ts +321 -70
- package/src/proxy/logIndex.ts +16 -6
- package/src/proxy/logger.ts +294 -41
- package/src/proxy/privateDataPath.ts +183 -0
- package/src/proxy/providerScanStore.ts +87 -0
- package/src/proxy/providerSecretStore.ts +58 -19
- package/src/proxy/providers.ts +175 -58
- package/src/proxy/rawStreamCapture.ts +66 -5
- package/src/proxy/runStore.ts +13 -3
- package/src/proxy/runtimeAdmission.ts +52 -0
- package/src/proxy/runtimeHealth.ts +206 -0
- package/src/proxy/runtimeShutdown.ts +75 -0
- package/src/proxy/sessionArchive.ts +15 -2
- package/src/proxy/sessionProcess.ts +19 -0
- package/src/proxy/sessionRuntime.ts +7 -0
- package/src/proxy/shutdownCoordinator.ts +90 -0
- package/src/proxy/sqliteLogIndex.ts +18 -2
- package/src/proxy/store.ts +18 -2
- package/src/routes/__root.tsx +18 -1
- package/src/routes/api/alerts.summary.ts +12 -8
- package/src/routes/api/alerts.ts +27 -9
- package/src/routes/api/ecosystem.packages.$packageId.help.ts +3 -8
- package/src/routes/api/ecosystem.packages.$packageId.install.ts +3 -8
- package/src/routes/api/ecosystem.packages.$packageId.runner-presets.ts +3 -11
- package/src/routes/api/ecosystem.packages.$packageId.upgrade.ts +3 -8
- package/src/routes/api/ecosystem.recipes.$recipeId.run.ts +3 -8
- package/src/routes/api/health.ts +9 -2
- package/src/routes/api/knowledge.project-context.ts +11 -0
- package/src/routes/api/knowledge.search.ts +33 -2
- package/src/routes/api/logs.$id.body.ts +16 -2
- package/src/routes/api/logs.import.ts +7 -18
- package/src/routes/api/logs.stream.ts +147 -69
- package/src/routes/api/logs.ts +55 -14
- package/src/routes/api/providers.$providerId.model-metadata.ts +22 -4
- package/src/routes/api/providers.$providerId.ts +37 -6
- package/src/routes/api/providers.export.ts +34 -16
- package/src/routes/api/providers.import.ts +42 -8
- package/src/routes/api/providers.scan.ts +13 -8
- package/src/routes/api/providers.ts +36 -5
- package/src/routes/api/runs.ts +12 -12
- package/src/routes/api/sessions.ts +15 -8
- package/src/routes/index.tsx +6 -0
- package/src/routes/livez.ts +13 -0
- package/src/routes/readyz.ts +18 -0
- package/src/routes/session/$sessionId.tsx +6 -0
- package/styles/globals.css +19 -3
- package/.output/public/assets/CompareDrawer-Cb5ccPeX.js +0 -1
- package/.output/public/assets/ProxyViewerContainer-COBe5GTT.js +0 -4234
- package/.output/public/assets/ResponseView-Cqq87lX_.js +0 -3
- package/.output/public/assets/StreamingChunkSequence-CnTa9mCr.js +0 -1
- package/.output/public/assets/index-DTLNVcgc.js +0 -76
- package/.output/public/assets/index-DliqmwUw.css +0 -1
- package/.output/public/assets/qwen-CONDcHqt.png +0 -0
- package/.output/server/_tanstack-start-manifest_v-DMCA8KK3.mjs +0 -4
- package/src/assets/logos/mcp.png +0 -0
- package/src/assets/logos/qwen.png +0 -0
- package/src/components/ui/mcp-logo.tsx +0 -20
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { DEFAULT_LOG_IMPORT_LIMITS } from "./logImporter";
|
|
2
|
+
|
|
3
|
+
const MAX_MULTIPART_OVERHEAD_BYTES = 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
export const MAX_LOG_IMPORT_REQUEST_BYTES =
|
|
6
|
+
DEFAULT_LOG_IMPORT_LIMITS.maxCompressedBytes + MAX_MULTIPART_OVERHEAD_BYTES;
|
|
7
|
+
|
|
8
|
+
export type BoundedLogImportUpload =
|
|
9
|
+
| {
|
|
10
|
+
ok: true;
|
|
11
|
+
fileName: string;
|
|
12
|
+
data: Uint8Array;
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
ok: false;
|
|
16
|
+
status: 400 | 413;
|
|
17
|
+
message: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type BoundedBody =
|
|
21
|
+
| { ok: true; data: ArrayBuffer }
|
|
22
|
+
| { ok: false; status: 400 | 413; message: string };
|
|
23
|
+
|
|
24
|
+
function declaredContentLength(value: string | null): number | null | "invalid" {
|
|
25
|
+
if (value === null) return null;
|
|
26
|
+
if (!/^[0-9]+$/.test(value)) return "invalid";
|
|
27
|
+
const parsed = Number.parseInt(value, 10);
|
|
28
|
+
return Number.isSafeInteger(parsed) ? parsed : "invalid";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function readBoundedBody(request: Request, maxBytes: number): Promise<BoundedBody> {
|
|
32
|
+
const declaredBytes = declaredContentLength(request.headers.get("content-length"));
|
|
33
|
+
if (declaredBytes === "invalid") {
|
|
34
|
+
return { ok: false, status: 400, message: "Invalid Content-Length header" };
|
|
35
|
+
}
|
|
36
|
+
if (declaredBytes !== null && declaredBytes > maxBytes) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
status: 413,
|
|
40
|
+
message: `Import request exceeds maximum size of ${String(maxBytes)} bytes.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const body = request.body;
|
|
45
|
+
if (body === null) {
|
|
46
|
+
return { ok: false, status: 400, message: "Expected multipart form data" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const reader = body.getReader();
|
|
50
|
+
const chunks: Uint8Array[] = [];
|
|
51
|
+
let totalBytes = 0;
|
|
52
|
+
while (true) {
|
|
53
|
+
try {
|
|
54
|
+
const result = await reader.read();
|
|
55
|
+
if (result.done) break;
|
|
56
|
+
|
|
57
|
+
totalBytes += result.value.byteLength;
|
|
58
|
+
if (totalBytes > maxBytes) {
|
|
59
|
+
await reader.cancel();
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
status: 413,
|
|
63
|
+
message: `Import request exceeds maximum size of ${String(maxBytes)} bytes.`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
chunks.push(result.value);
|
|
67
|
+
} catch {
|
|
68
|
+
return { ok: false, status: 400, message: "Failed to read import request" };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const data = new Uint8Array(totalBytes);
|
|
73
|
+
let offset = 0;
|
|
74
|
+
for (const chunk of chunks) {
|
|
75
|
+
data.set(chunk, offset);
|
|
76
|
+
offset += chunk.byteLength;
|
|
77
|
+
}
|
|
78
|
+
return { ok: true, data: data.buffer };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isUploadedFile(value: FormDataEntryValue | null): value is File {
|
|
82
|
+
return value instanceof File;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function readBoundedLogImportUpload(
|
|
86
|
+
request: Request,
|
|
87
|
+
options: {
|
|
88
|
+
maxFileBytes?: number;
|
|
89
|
+
maxRequestBytes?: number;
|
|
90
|
+
} = {},
|
|
91
|
+
): Promise<BoundedLogImportUpload> {
|
|
92
|
+
const contentType = request.headers.get("content-type");
|
|
93
|
+
if (contentType === null || !contentType.toLowerCase().startsWith("multipart/form-data")) {
|
|
94
|
+
return { ok: false, status: 400, message: "Expected multipart form data" };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const maxFileBytes = options.maxFileBytes ?? DEFAULT_LOG_IMPORT_LIMITS.maxCompressedBytes;
|
|
98
|
+
const maxRequestBytes = options.maxRequestBytes ?? MAX_LOG_IMPORT_REQUEST_BYTES;
|
|
99
|
+
const body = await readBoundedBody(request, maxRequestBytes);
|
|
100
|
+
if (!body.ok) return body;
|
|
101
|
+
|
|
102
|
+
let formData: FormData;
|
|
103
|
+
try {
|
|
104
|
+
formData = await new Response(body.data, {
|
|
105
|
+
headers: { "content-type": contentType },
|
|
106
|
+
}).formData();
|
|
107
|
+
} catch {
|
|
108
|
+
return { ok: false, status: 400, message: "Expected multipart form data" };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const upload = formData.get("file");
|
|
112
|
+
if (!isUploadedFile(upload)) {
|
|
113
|
+
return { ok: false, status: 400, message: "Missing import file" };
|
|
114
|
+
}
|
|
115
|
+
if (upload.size > maxFileBytes) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
status: 413,
|
|
119
|
+
message: `Import file exceeds maximum size of ${String(maxFileBytes)} bytes.`,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
ok: true,
|
|
125
|
+
fileName: upload.name,
|
|
126
|
+
data: new Uint8Array(await upload.arrayBuffer()),
|
|
127
|
+
};
|
|
128
|
+
}
|
package/src/proxy/logImporter.ts
CHANGED
|
@@ -15,16 +15,29 @@ import { analyzeToolSchemaWarnings } from "./toolSchemaWarnings";
|
|
|
15
15
|
|
|
16
16
|
export type ImportedLogDraft = Omit<CapturedLog, "id">;
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
18
|
+
export type LogImportLimits = {
|
|
19
|
+
maxCompressedBytes: number;
|
|
20
|
+
maxEntries: number;
|
|
21
|
+
maxEntryUncompressedBytes: number;
|
|
22
|
+
maxTotalUncompressedBytes: number;
|
|
23
|
+
maxExpansionRatio: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_LOG_IMPORT_LIMITS: Readonly<LogImportLimits> = {
|
|
27
|
+
maxCompressedBytes: 200 * 1024 * 1024,
|
|
28
|
+
maxEntries: 10_000,
|
|
29
|
+
maxEntryUncompressedBytes: 128 * 1024 * 1024,
|
|
30
|
+
maxTotalUncompressedBytes: 500 * 1024 * 1024,
|
|
31
|
+
maxExpansionRatio: 100,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE = 0x06054b50;
|
|
35
|
+
const ZIP_CENTRAL_DIRECTORY_SIGNATURE = 0x02014b50;
|
|
36
|
+
const ZIP_END_OF_CENTRAL_DIRECTORY_BYTES = 22;
|
|
37
|
+
const ZIP_CENTRAL_DIRECTORY_HEADER_BYTES = 46;
|
|
38
|
+
const ZIP_MAX_COMMENT_BYTES = 0xffff;
|
|
39
|
+
const ZIP_16_BIT_SENTINEL = 0xffff;
|
|
40
|
+
const ZIP_32_BIT_SENTINEL = 0xffffffff;
|
|
28
41
|
|
|
29
42
|
export type ParsedLogImport =
|
|
30
43
|
| {
|
|
@@ -58,6 +71,17 @@ const StreamingChunksImportSchema = z.object({
|
|
|
58
71
|
|
|
59
72
|
type ParsedManifest = z.infer<typeof ManifestSchema>;
|
|
60
73
|
|
|
74
|
+
type ZipBudget = {
|
|
75
|
+
remainingBytes: number;
|
|
76
|
+
decompressedBytes: number;
|
|
77
|
+
compressedBytes: number;
|
|
78
|
+
limits: Readonly<LogImportLimits>;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type ZipTextResult = { ok: true; text: string | null } | { ok: false; message: string };
|
|
82
|
+
|
|
83
|
+
type ZipDirectoryInspection = { ok: true } | { ok: false; message: string };
|
|
84
|
+
|
|
61
85
|
const SEMVER_MAJOR_PATTERN = /^([0-9]+)\.[0-9]+\.[0-9]+(?:[-+].*)?$/;
|
|
62
86
|
|
|
63
87
|
function readRecordProperty(value: unknown, name: string): unknown {
|
|
@@ -300,33 +324,249 @@ function legacyDraftFromParts(input: {
|
|
|
300
324
|
});
|
|
301
325
|
}
|
|
302
326
|
|
|
327
|
+
function zipEntryLimitMessage(path: string, maxBytes: number): string {
|
|
328
|
+
return `Archive entry "${path}" exceeds maximum uncompressed size of ${String(maxBytes)} bytes.`;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function zipTotalLimitMessage(maxBytes: number): string {
|
|
332
|
+
return `Archive exceeds maximum total uncompressed size of ${String(maxBytes)} bytes.`;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function zipExpansionLimitMessage(maxRatio: number): string {
|
|
336
|
+
return `Archive exceeds maximum expansion ratio of ${String(maxRatio)}:1.`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function exceedsExpansionRatio(
|
|
340
|
+
uncompressedBytes: number,
|
|
341
|
+
compressedBytes: number,
|
|
342
|
+
maxRatio: number,
|
|
343
|
+
): boolean {
|
|
344
|
+
if (uncompressedBytes === 0) return false;
|
|
345
|
+
if (compressedBytes === 0) return true;
|
|
346
|
+
return uncompressedBytes > compressedBytes * maxRatio;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function findEndOfCentralDirectory(data: Uint8Array, view: DataView): number | null {
|
|
350
|
+
const firstCandidate = Math.max(
|
|
351
|
+
0,
|
|
352
|
+
data.byteLength - ZIP_END_OF_CENTRAL_DIRECTORY_BYTES - ZIP_MAX_COMMENT_BYTES,
|
|
353
|
+
);
|
|
354
|
+
for (
|
|
355
|
+
let offset = data.byteLength - ZIP_END_OF_CENTRAL_DIRECTORY_BYTES;
|
|
356
|
+
offset >= firstCandidate;
|
|
357
|
+
offset -= 1
|
|
358
|
+
) {
|
|
359
|
+
if (view.getUint32(offset, true) !== ZIP_END_OF_CENTRAL_DIRECTORY_SIGNATURE) continue;
|
|
360
|
+
const commentBytes = view.getUint16(offset + 20, true);
|
|
361
|
+
if (offset + ZIP_END_OF_CENTRAL_DIRECTORY_BYTES + commentBytes === data.byteLength) {
|
|
362
|
+
return offset;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
303
368
|
/**
|
|
304
|
-
*
|
|
305
|
-
*
|
|
306
|
-
* detect that the cumulative decompressed size has blown the budget (a
|
|
307
|
-
* zip-bomb signal). We do NOT throw on budget exceeded because the project's
|
|
308
|
-
* lint rules forbid throwing inside non-CLI modules — using a mutable
|
|
309
|
-
* counter keeps the budget check declarative at every call site.
|
|
369
|
+
* JSZip does not expose declared entry sizes. Inspect the central directory so
|
|
370
|
+
* hostile archives are rejected before an inflater or JSON parser runs.
|
|
310
371
|
*/
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
372
|
+
function inspectZipDirectory(
|
|
373
|
+
data: Uint8Array,
|
|
374
|
+
limits: Readonly<LogImportLimits>,
|
|
375
|
+
): ZipDirectoryInspection {
|
|
376
|
+
if (data.byteLength < ZIP_END_OF_CENTRAL_DIRECTORY_BYTES) {
|
|
377
|
+
return { ok: false, message: "Invalid ZIP archive: central directory was not found." };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
381
|
+
const endOffset = findEndOfCentralDirectory(data, view);
|
|
382
|
+
if (endOffset === null) {
|
|
383
|
+
return { ok: false, message: "Invalid ZIP archive: central directory was not found." };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const diskNumber = view.getUint16(endOffset + 4, true);
|
|
387
|
+
const centralDirectoryDisk = view.getUint16(endOffset + 6, true);
|
|
388
|
+
const entriesOnDisk = view.getUint16(endOffset + 8, true);
|
|
389
|
+
const entryCount = view.getUint16(endOffset + 10, true);
|
|
390
|
+
const directoryBytes = view.getUint32(endOffset + 12, true);
|
|
391
|
+
const directoryOffset = view.getUint32(endOffset + 16, true);
|
|
392
|
+
|
|
393
|
+
if (
|
|
394
|
+
diskNumber !== 0 ||
|
|
395
|
+
centralDirectoryDisk !== 0 ||
|
|
396
|
+
entriesOnDisk !== entryCount ||
|
|
397
|
+
entryCount === ZIP_16_BIT_SENTINEL ||
|
|
398
|
+
directoryBytes === ZIP_32_BIT_SENTINEL ||
|
|
399
|
+
directoryOffset === ZIP_32_BIT_SENTINEL
|
|
400
|
+
) {
|
|
401
|
+
return { ok: false, message: "ZIP64 and split archives are not supported for log import." };
|
|
402
|
+
}
|
|
403
|
+
if (entryCount > limits.maxEntries) {
|
|
404
|
+
return {
|
|
405
|
+
ok: false,
|
|
406
|
+
message: `Archive contains more than ${String(limits.maxEntries)} entries.`,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const directoryEnd = directoryOffset + directoryBytes;
|
|
411
|
+
if (directoryEnd > endOffset || directoryEnd > data.byteLength) {
|
|
412
|
+
return { ok: false, message: "Invalid ZIP archive: central directory is out of bounds." };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
let offset = directoryOffset;
|
|
416
|
+
let totalCompressedBytes = 0;
|
|
417
|
+
let totalUncompressedBytes = 0;
|
|
418
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
419
|
+
if (
|
|
420
|
+
offset + ZIP_CENTRAL_DIRECTORY_HEADER_BYTES > directoryEnd ||
|
|
421
|
+
view.getUint32(offset, true) !== ZIP_CENTRAL_DIRECTORY_SIGNATURE
|
|
422
|
+
) {
|
|
423
|
+
return { ok: false, message: "Invalid ZIP archive: malformed central directory entry." };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const flags = view.getUint16(offset + 8, true);
|
|
427
|
+
const compressedBytes = view.getUint32(offset + 20, true);
|
|
428
|
+
const uncompressedBytes = view.getUint32(offset + 24, true);
|
|
429
|
+
const fileNameBytes = view.getUint16(offset + 28, true);
|
|
430
|
+
const extraBytes = view.getUint16(offset + 30, true);
|
|
431
|
+
const commentBytes = view.getUint16(offset + 32, true);
|
|
432
|
+
const localHeaderOffset = view.getUint32(offset + 42, true);
|
|
433
|
+
const entryEnd =
|
|
434
|
+
offset + ZIP_CENTRAL_DIRECTORY_HEADER_BYTES + fileNameBytes + extraBytes + commentBytes;
|
|
435
|
+
|
|
436
|
+
if (
|
|
437
|
+
compressedBytes === ZIP_32_BIT_SENTINEL ||
|
|
438
|
+
uncompressedBytes === ZIP_32_BIT_SENTINEL ||
|
|
439
|
+
localHeaderOffset === ZIP_32_BIT_SENTINEL
|
|
440
|
+
) {
|
|
441
|
+
return { ok: false, message: "ZIP64 archives are not supported for log import." };
|
|
442
|
+
}
|
|
443
|
+
if ((flags & 0x1) !== 0) {
|
|
444
|
+
return { ok: false, message: "Encrypted ZIP entries are not supported for log import." };
|
|
445
|
+
}
|
|
446
|
+
if (entryEnd > directoryEnd || localHeaderOffset >= directoryOffset) {
|
|
447
|
+
return { ok: false, message: "Invalid ZIP archive: entry metadata is out of bounds." };
|
|
448
|
+
}
|
|
449
|
+
if (uncompressedBytes > limits.maxEntryUncompressedBytes) {
|
|
450
|
+
return {
|
|
451
|
+
ok: false,
|
|
452
|
+
message: zipEntryLimitMessage(
|
|
453
|
+
`entry ${String(index + 1)}`,
|
|
454
|
+
limits.maxEntryUncompressedBytes,
|
|
455
|
+
),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
if (exceedsExpansionRatio(uncompressedBytes, compressedBytes, limits.maxExpansionRatio)) {
|
|
459
|
+
return { ok: false, message: zipExpansionLimitMessage(limits.maxExpansionRatio) };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
totalCompressedBytes += compressedBytes;
|
|
463
|
+
totalUncompressedBytes += uncompressedBytes;
|
|
464
|
+
if (totalUncompressedBytes > limits.maxTotalUncompressedBytes) {
|
|
465
|
+
return { ok: false, message: zipTotalLimitMessage(limits.maxTotalUncompressedBytes) };
|
|
466
|
+
}
|
|
467
|
+
if (
|
|
468
|
+
exceedsExpansionRatio(totalUncompressedBytes, totalCompressedBytes, limits.maxExpansionRatio)
|
|
469
|
+
) {
|
|
470
|
+
return { ok: false, message: zipExpansionLimitMessage(limits.maxExpansionRatio) };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
offset = entryEnd;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (offset !== directoryEnd) {
|
|
477
|
+
return { ok: false, message: "Invalid ZIP archive: central directory size is inconsistent." };
|
|
478
|
+
}
|
|
479
|
+
return { ok: true };
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function bufferFromZipChunk(chunk: unknown): Buffer | null {
|
|
483
|
+
if (Buffer.isBuffer(chunk)) return chunk;
|
|
484
|
+
if (chunk instanceof Uint8Array) return Buffer.from(chunk);
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function readZipText(zip: JSZip, path: string, budget: ZipBudget): Promise<ZipTextResult> {
|
|
316
489
|
const file = zip.file(path);
|
|
317
|
-
if (file === null) return null;
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
490
|
+
if (file === null) return { ok: true, text: null };
|
|
491
|
+
|
|
492
|
+
return await new Promise((resolve) => {
|
|
493
|
+
const chunks: Buffer[] = [];
|
|
494
|
+
const stream = file.nodeStream("nodebuffer");
|
|
495
|
+
let entryBytes = 0;
|
|
496
|
+
let settled = false;
|
|
497
|
+
|
|
498
|
+
const settle = (result: ZipTextResult): void => {
|
|
499
|
+
if (settled) return;
|
|
500
|
+
settled = true;
|
|
501
|
+
resolve(result);
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
stream.on("data", (chunk: unknown) => {
|
|
505
|
+
if (settled) return;
|
|
506
|
+
const buffer = bufferFromZipChunk(chunk);
|
|
507
|
+
if (buffer === null) {
|
|
508
|
+
stream.pause();
|
|
509
|
+
settle({ ok: false, message: `Archive entry "${path}" produced invalid data.` });
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
entryBytes += buffer.byteLength;
|
|
514
|
+
if (entryBytes > budget.limits.maxEntryUncompressedBytes) {
|
|
515
|
+
stream.pause();
|
|
516
|
+
settle({
|
|
517
|
+
ok: false,
|
|
518
|
+
message: zipEntryLimitMessage(path, budget.limits.maxEntryUncompressedBytes),
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (buffer.byteLength > budget.remainingBytes) {
|
|
523
|
+
stream.pause();
|
|
524
|
+
settle({
|
|
525
|
+
ok: false,
|
|
526
|
+
message: zipTotalLimitMessage(budget.limits.maxTotalUncompressedBytes),
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const decompressedBytes = budget.decompressedBytes + buffer.byteLength;
|
|
531
|
+
if (
|
|
532
|
+
exceedsExpansionRatio(
|
|
533
|
+
decompressedBytes,
|
|
534
|
+
budget.compressedBytes,
|
|
535
|
+
budget.limits.maxExpansionRatio,
|
|
536
|
+
)
|
|
537
|
+
) {
|
|
538
|
+
stream.pause();
|
|
539
|
+
settle({
|
|
540
|
+
ok: false,
|
|
541
|
+
message: zipExpansionLimitMessage(budget.limits.maxExpansionRatio),
|
|
542
|
+
});
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
budget.remainingBytes -= buffer.byteLength;
|
|
547
|
+
budget.decompressedBytes = decompressedBytes;
|
|
548
|
+
chunks.push(buffer);
|
|
549
|
+
});
|
|
550
|
+
stream.on("error", (error: unknown) => {
|
|
551
|
+
settle({
|
|
552
|
+
ok: false,
|
|
553
|
+
message: error instanceof Error ? error.message : `Failed to decompress "${path}".`,
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
stream.on("end", () => {
|
|
557
|
+
settle({ ok: true, text: Buffer.concat(chunks, entryBytes).toString("utf8") });
|
|
558
|
+
});
|
|
559
|
+
});
|
|
321
560
|
}
|
|
322
561
|
|
|
323
562
|
async function readManifest(
|
|
324
563
|
zip: JSZip,
|
|
325
|
-
|
|
326
|
-
): Promise<ParsedManifest | null> {
|
|
327
|
-
const
|
|
328
|
-
|
|
329
|
-
|
|
564
|
+
budget: ZipBudget,
|
|
565
|
+
): Promise<{ ok: true; manifest: ParsedManifest | null } | { ok: false; message: string }> {
|
|
566
|
+
const result = await readZipText(zip, "manifest.json", budget);
|
|
567
|
+
if (!result.ok) return result;
|
|
568
|
+
const parsed = ManifestSchema.safeParse(parseJson(result.text));
|
|
569
|
+
return { ok: true, manifest: parsed.success ? parsed.data : null };
|
|
330
570
|
}
|
|
331
571
|
|
|
332
572
|
function versionMajor(version: string): number | null {
|
|
@@ -387,24 +627,25 @@ function validateManifestVersion(manifest: ParsedManifest | null):
|
|
|
387
627
|
return { ok: true, warning: null };
|
|
388
628
|
}
|
|
389
629
|
|
|
390
|
-
function
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
warnings,
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
async function parseZipLogImport(data: Uint8Array): Promise<ParsedLogImport> {
|
|
630
|
+
async function parseZipLogImport(
|
|
631
|
+
data: Uint8Array,
|
|
632
|
+
limits: Readonly<LogImportLimits>,
|
|
633
|
+
): Promise<ParsedLogImport> {
|
|
399
634
|
const warnings: string[] = [];
|
|
400
|
-
const
|
|
635
|
+
const inspection = inspectZipDirectory(data, limits);
|
|
636
|
+
if (!inspection.ok) return { ...inspection, warnings };
|
|
401
637
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
638
|
+
const zip = await JSZip.loadAsync(data);
|
|
639
|
+
const budget: ZipBudget = {
|
|
640
|
+
remainingBytes: limits.maxTotalUncompressedBytes,
|
|
641
|
+
decompressedBytes: 0,
|
|
642
|
+
compressedBytes: data.byteLength,
|
|
643
|
+
limits,
|
|
644
|
+
};
|
|
405
645
|
|
|
406
|
-
const
|
|
407
|
-
if (
|
|
646
|
+
const manifestResult = await readManifest(zip, budget);
|
|
647
|
+
if (!manifestResult.ok) return { ...manifestResult, warnings };
|
|
648
|
+
const manifest = manifestResult.manifest;
|
|
408
649
|
const versionCheck = validateManifestVersion(manifest);
|
|
409
650
|
if (!versionCheck.ok) {
|
|
410
651
|
return {
|
|
@@ -422,28 +663,30 @@ async function parseZipLogImport(data: Uint8Array): Promise<ParsedLogImport> {
|
|
|
422
663
|
warnings.push("Imported redacted export. Secrets and some replay details may be unavailable.");
|
|
423
664
|
}
|
|
424
665
|
|
|
425
|
-
const
|
|
426
|
-
if (
|
|
427
|
-
const metadataLogs =
|
|
666
|
+
const metadataResult = await readZipText(zip, "logs.json", budget);
|
|
667
|
+
if (!metadataResult.ok) return { ...metadataResult, warnings };
|
|
668
|
+
const metadataLogs = metadataResult.text === null ? null : parseJsonLogs(metadataResult.text);
|
|
428
669
|
const logs: ImportedLogDraft[] = [];
|
|
429
670
|
let skipped = 0;
|
|
430
671
|
|
|
431
672
|
if (metadataLogs !== null) {
|
|
432
673
|
for (const log of metadataLogs) {
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
const
|
|
674
|
+
const requestResult = await readZipText(zip, `#${String(log.id)}.Request.json`, budget);
|
|
675
|
+
if (!requestResult.ok) return { ...requestResult, warnings };
|
|
676
|
+
const responseResult = await readZipText(zip, `#${String(log.id)}.Response.json`, budget);
|
|
677
|
+
if (!responseResult.ok) return { ...responseResult, warnings };
|
|
678
|
+
const streamingResult = await readZipText(
|
|
436
679
|
zip,
|
|
437
680
|
`#${String(log.id)}.SSE.Response.json`,
|
|
438
|
-
|
|
681
|
+
budget,
|
|
439
682
|
);
|
|
440
|
-
if (
|
|
683
|
+
if (!streamingResult.ok) return { ...streamingResult, warnings };
|
|
441
684
|
logs.push(
|
|
442
685
|
draftFromCapturedLog(
|
|
443
686
|
log,
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
streamingChunksFromText(
|
|
687
|
+
requestResult.text,
|
|
688
|
+
responseResult.text,
|
|
689
|
+
streamingChunksFromText(streamingResult.text),
|
|
447
690
|
),
|
|
448
691
|
);
|
|
449
692
|
}
|
|
@@ -451,24 +694,30 @@ async function parseZipLogImport(data: Uint8Array): Promise<ParsedLogImport> {
|
|
|
451
694
|
}
|
|
452
695
|
|
|
453
696
|
for (const originalId of manifest?.logIds ?? []) {
|
|
454
|
-
const
|
|
455
|
-
|
|
456
|
-
const
|
|
697
|
+
const requestResult = await readZipText(zip, `#${String(originalId)}.Request.json`, budget);
|
|
698
|
+
if (!requestResult.ok) return { ...requestResult, warnings };
|
|
699
|
+
const responseResult = await readZipText(zip, `#${String(originalId)}.Response.json`, budget);
|
|
700
|
+
if (!responseResult.ok) return { ...responseResult, warnings };
|
|
701
|
+
const streamingResult = await readZipText(
|
|
457
702
|
zip,
|
|
458
703
|
`#${String(originalId)}.SSE.Response.json`,
|
|
459
|
-
|
|
704
|
+
budget,
|
|
460
705
|
);
|
|
461
|
-
if (
|
|
462
|
-
if (
|
|
706
|
+
if (!streamingResult.ok) return { ...streamingResult, warnings };
|
|
707
|
+
if (
|
|
708
|
+
requestResult.text === null &&
|
|
709
|
+
responseResult.text === null &&
|
|
710
|
+
streamingResult.text === null
|
|
711
|
+
) {
|
|
463
712
|
skipped += 1;
|
|
464
713
|
continue;
|
|
465
714
|
}
|
|
466
715
|
logs.push(
|
|
467
716
|
legacyDraftFromParts({
|
|
468
717
|
exportedAt,
|
|
469
|
-
requestText,
|
|
470
|
-
responseText,
|
|
471
|
-
streamingChunks: streamingChunksFromText(
|
|
718
|
+
requestText: requestResult.text,
|
|
719
|
+
responseText: responseResult.text,
|
|
720
|
+
streamingChunks: streamingChunksFromText(streamingResult.text),
|
|
472
721
|
}),
|
|
473
722
|
);
|
|
474
723
|
}
|
|
@@ -521,23 +770,25 @@ function parseTextLogImport(text: string): ParsedLogImport {
|
|
|
521
770
|
export async function parseImportedLogFile(input: {
|
|
522
771
|
fileName: string;
|
|
523
772
|
data: Uint8Array;
|
|
773
|
+
limits?: Readonly<LogImportLimits>;
|
|
524
774
|
}): Promise<ParsedLogImport> {
|
|
775
|
+
const limits = input.limits ?? DEFAULT_LOG_IMPORT_LIMITS;
|
|
525
776
|
try {
|
|
526
777
|
if (looksLikeZip(input.data, input.fileName)) {
|
|
527
|
-
if (input.data.byteLength >
|
|
778
|
+
if (input.data.byteLength > limits.maxCompressedBytes) {
|
|
528
779
|
return {
|
|
529
780
|
ok: false,
|
|
530
|
-
message: `Archive exceeds maximum size of ${
|
|
781
|
+
message: `Archive exceeds maximum compressed size of ${String(limits.maxCompressedBytes)} bytes.`,
|
|
531
782
|
warnings: [],
|
|
532
783
|
};
|
|
533
784
|
}
|
|
534
|
-
return await parseZipLogImport(input.data);
|
|
785
|
+
return await parseZipLogImport(input.data, limits);
|
|
535
786
|
}
|
|
536
787
|
|
|
537
|
-
if (input.data.byteLength >
|
|
788
|
+
if (input.data.byteLength > limits.maxCompressedBytes) {
|
|
538
789
|
return {
|
|
539
790
|
ok: false,
|
|
540
|
-
message: `Import file exceeds maximum size of ${
|
|
791
|
+
message: `Import file exceeds maximum size of ${String(limits.maxCompressedBytes)} bytes.`,
|
|
541
792
|
warnings: [],
|
|
542
793
|
};
|
|
543
794
|
}
|
package/src/proxy/logIndex.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFile, writeFile, stat, readdir
|
|
1
|
+
import { readFile, writeFile, stat, readdir } from "node:fs/promises";
|
|
2
2
|
import { createReadStream, existsSync } from "node:fs";
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
@@ -14,6 +14,11 @@ import {
|
|
|
14
14
|
syncSqliteLogIndexEntries,
|
|
15
15
|
upsertSqliteLogIndexEntry,
|
|
16
16
|
} from "./sqliteLogIndex";
|
|
17
|
+
import {
|
|
18
|
+
ensurePrivateDirectorySync,
|
|
19
|
+
PRIVATE_FILE_MODE,
|
|
20
|
+
securePrivateFileSync,
|
|
21
|
+
} from "./privateDataPath";
|
|
17
22
|
|
|
18
23
|
export type LogIndexSummary = {
|
|
19
24
|
id: number;
|
|
@@ -176,14 +181,19 @@ export async function saveIndex(index: LogIndex): Promise<void> {
|
|
|
176
181
|
const indexPath = getIndexPath();
|
|
177
182
|
const dir = dirname(indexPath);
|
|
178
183
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// Ignore
|
|
184
|
+
if (!ensurePrivateDirectorySync(dir)) {
|
|
185
|
+
logger.error("[logIndex] Failed to secure index directory:", dir);
|
|
186
|
+
return;
|
|
183
187
|
}
|
|
184
188
|
|
|
185
189
|
try {
|
|
186
|
-
await writeFile(indexPath, JSON.stringify(index),
|
|
190
|
+
await writeFile(indexPath, JSON.stringify(index), {
|
|
191
|
+
encoding: "utf-8",
|
|
192
|
+
mode: PRIVATE_FILE_MODE,
|
|
193
|
+
});
|
|
194
|
+
if (!securePrivateFileSync(indexPath)) {
|
|
195
|
+
logger.error("[logIndex] Failed to secure index file:", indexPath);
|
|
196
|
+
}
|
|
187
197
|
} catch (err) {
|
|
188
198
|
logger.error("[logIndex] Failed to save index:", String(err));
|
|
189
199
|
}
|