@tonyclaw/agent-inspector 3.1.32 → 4.0.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/.output/backend/nitro.json +1 -1
- package/.output/cli.js +241 -120
- package/.output/server/_ssr/index.mjs +1 -1
- package/.output/server/_ssr/{router-oXUk3MBR.mjs → router-BBY_zWRK.mjs} +91 -3
- package/.output/server/index.mjs +1 -1
- package/.output/ui/assets/{CompareDrawer-BtfPSpkV.js → CompareDrawer-rtNElN-B.js} +1 -1
- package/.output/ui/assets/{InspectorPet-D9DPPQ3n.js → InspectorPet-QaAiuF3d.js} +1 -1
- package/.output/ui/assets/{ProxyViewerContainer-Buu8vkCK.js → ProxyViewerContainer-6twNZHwl.js} +4 -4
- package/.output/ui/assets/{ReplayDialog-BER-QzpD.js → ReplayDialog-B7KlziO2.js} +1 -1
- package/.output/ui/assets/{RequestAnatomy-C8Zwxrr7.js → RequestAnatomy-2XVFBrW4.js} +1 -1
- package/.output/ui/assets/{ResponseView-DDpKdYk6.js → ResponseView-BULgzLVB.js} +1 -1
- package/.output/ui/assets/{StreamingChunkSequence-sMPGG7Sw.js → StreamingChunkSequence-DSkvkbns.js} +1 -1
- package/.output/ui/assets/{_sessionId-e4IheHnP.js → _sessionId-D2dmJi3G.js} +1 -1
- package/.output/ui/assets/{_sessionId-C5Uv72Fx.js → _sessionId-DcS0s1Mr.js} +1 -1
- package/.output/ui/assets/{index-B__qT0Yd.js → index-B0TN1KKO.js} +2 -2
- package/.output/ui/assets/{index-Be3bTCzi.js → index-Bva1zHEs.js} +1 -1
- package/.output/ui/assets/{index-C9q__p8J.js → index-BzBExOxu.js} +1 -1
- package/.output/ui/assets/{index-CtB0deSZ.js → index-k6sRQz55.js} +1 -1
- package/.output/ui/assets/{json-viewer-Db0AFp-7.js → json-viewer-CWM4tY-j.js} +1 -1
- package/.output/ui/assets/{jszip.min-DkY0Ym_q.js → jszip.min-BkWvRFlo.js} +1 -1
- package/.output/ui/index.html +1 -1
- package/package.json +8 -13
- package/src/assets/IDE/claudecode-color.svg +0 -1
- package/src/assets/IDE/codex-color.svg +0 -1
- package/src/assets/IDE/opencode.svg +0 -1
- package/src/assets/IDE/xiaomimimo.svg +0 -1
- package/src/assets/favicon.svg +0 -21
- package/src/assets/logos/alibaba.svg +0 -1
- package/src/assets/logos/anthropic.svg +0 -1
- package/src/assets/logos/claude-code.svg +0 -4
- package/src/assets/logos/deepseek.svg +0 -1
- package/src/assets/logos/minimax.jpeg +0 -0
- package/src/assets/logos/openai.svg +0 -1
- package/src/assets/logos/opencode.svg +0 -4
- package/src/assets/logos/qwen.webp +0 -0
- package/src/assets/logos/zhipuai.svg +0 -219
- package/src/backend/router.tsx +0 -36
- package/src/backend/routes/$.ts +0 -26
- package/src/backend/routes/__root.ts +0 -4
- package/src/backend/routes/api/-instances.ts +0 -7
- package/src/backend/routes/api/alerts.summary.ts +0 -28
- package/src/backend/routes/api/alerts.ts +0 -70
- package/src/backend/routes/api/config.paths.ts +0 -14
- package/src/backend/routes/api/config.ts +0 -73
- package/src/backend/routes/api/ecosystem.packages.$packageId.help.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.install.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.runner-presets.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.$packageId.upgrade.ts +0 -11
- package/src/backend/routes/api/ecosystem.packages.ts +0 -21
- package/src/backend/routes/api/ecosystem.recipes.$recipeId.run.ts +0 -11
- package/src/backend/routes/api/ecosystem.tasks.$taskId.ts +0 -16
- package/src/backend/routes/api/ecosystem.tasks.ts +0 -12
- package/src/backend/routes/api/groups.$groupId.evidence.ts +0 -55
- package/src/backend/routes/api/groups.$groupId.sessions.ts +0 -50
- package/src/backend/routes/api/groups.$groupId.ts +0 -93
- package/src/backend/routes/api/groups.ts +0 -79
- package/src/backend/routes/api/health.ts +0 -22
- package/src/backend/routes/api/instances.$name.connection.ts +0 -12
- package/src/backend/routes/api/instances.$name.restart.ts +0 -23
- package/src/backend/routes/api/instances.$name.start.ts +0 -18
- package/src/backend/routes/api/instances.$name.stop.ts +0 -23
- package/src/backend/routes/api/instances.$name.ts +0 -12
- package/src/backend/routes/api/instances.ts +0 -16
- package/src/backend/routes/api/knowledge.candidates.$candidateId.promote.ts +0 -32
- package/src/backend/routes/api/knowledge.candidates.$candidateId.ts +0 -50
- package/src/backend/routes/api/knowledge.candidates.ts +0 -10
- package/src/backend/routes/api/knowledge.project-context.ts +0 -29
- package/src/backend/routes/api/knowledge.search.ts +0 -62
- package/src/backend/routes/api/knowledge.sessions.$sessionId.candidates.ts +0 -26
- package/src/backend/routes/api/logs.$id.body.ts +0 -58
- package/src/backend/routes/api/logs.$id.chunks.ts +0 -37
- package/src/backend/routes/api/logs.$id.replay.ts +0 -297
- package/src/backend/routes/api/logs.$id.ts +0 -23
- package/src/backend/routes/api/logs.import.ts +0 -39
- package/src/backend/routes/api/logs.stream.ts +0 -174
- package/src/backend/routes/api/logs.ts +0 -214
- package/src/backend/routes/api/mcp.ts +0 -25
- package/src/backend/routes/api/models.ts +0 -10
- package/src/backend/routes/api/pi-agent.ts +0 -29
- package/src/backend/routes/api/providers.$providerId.model-metadata.ts +0 -173
- package/src/backend/routes/api/providers.$providerId.test.log.ts +0 -342
- package/src/backend/routes/api/providers.$providerId.ts +0 -111
- package/src/backend/routes/api/providers.export.ts +0 -44
- package/src/backend/routes/api/providers.import.ts +0 -81
- package/src/backend/routes/api/providers.scan.ts +0 -28
- package/src/backend/routes/api/providers.ts +0 -102
- package/src/backend/routes/api/runs.$runId.evidence.ts +0 -55
- package/src/backend/routes/api/runs.$runId.ts +0 -50
- package/src/backend/routes/api/runs.ts +0 -62
- package/src/backend/routes/api/runtime.ts +0 -46
- package/src/backend/routes/api/sessions.ts +0 -51
- package/src/backend/routes/api/storage.ts +0 -56
- package/src/backend/routes/livez.ts +0 -13
- package/src/backend/routes/metrics.ts +0 -14
- package/src/backend/routes/proxy/$.ts +0 -15
- package/src/backend/routes/readyz.ts +0 -18
- package/src/backend/start.ts +0 -10
- package/src/cli/alias.ts +0 -431
- package/src/cli/detect-tools.ts +0 -123
- package/src/cli/doctor.ts +0 -836
- package/src/cli/instance.ts +0 -1071
- package/src/cli/instanceApi.ts +0 -213
- package/src/cli/instanceArgs.ts +0 -301
- package/src/cli/instanceControl.ts +0 -191
- package/src/cli/instanceModel.ts +0 -276
- package/src/cli/instanceRegistry.ts +0 -731
- package/src/cli/networkHints.ts +0 -170
- package/src/cli/onboard.ts +0 -1668
- package/src/cli/onboardTarget.ts +0 -228
- package/src/cli/rustBackendPackage.ts +0 -458
- package/src/cli/startupOutput.ts +0 -16
- package/src/cli/templates/codex-skill-onboard.ts +0 -320
- package/src/cli/templates/command-onboard.ts +0 -25
- package/src/cli/templates/skill-onboard.ts +0 -625
- package/src/cli.ts +0 -1397
- package/src/components/OnboardingBanner.tsx +0 -115
- package/src/components/ProxyViewer.tsx +0 -2259
- package/src/components/ProxyViewerContainer.tsx +0 -1076
- package/src/components/alerts/AlertsDialog.tsx +0 -615
- package/src/components/clients/ClientLogo.tsx +0 -175
- package/src/components/ecosystem/AgentLabDialog.tsx +0 -947
- package/src/components/errors/SafeErrorBoundary.tsx +0 -201
- package/src/components/groups/GroupsDialog.tsx +0 -1304
- package/src/components/inspector-pet/InspectorPet.tsx +0 -398
- package/src/components/inspector-pet/InspectorPetCrab3D.tsx +0 -455
- package/src/components/inspector-pet/inspectorPetLogic.ts +0 -272
- package/src/components/pi-agent/PiAgentPanel.tsx +0 -446
- package/src/components/pi-agent/piAgentChatLogic.ts +0 -273
- package/src/components/providers/ImportWizardDialog.tsx +0 -281
- package/src/components/providers/ProviderCard.tsx +0 -347
- package/src/components/providers/ProviderForm.tsx +0 -1183
- package/src/components/providers/ProviderLogo.tsx +0 -122
- package/src/components/providers/ProviderTestResultsView.tsx +0 -734
- package/src/components/providers/ProvidersPanel.tsx +0 -714
- package/src/components/providers/SettingsDialog.tsx +0 -1042
- package/src/components/providers/providerFormCapabilities.ts +0 -36
- package/src/components/proxy-viewer/AgentTraceSummary.tsx +0 -902
- package/src/components/proxy-viewer/AnswerMarkdown.tsx +0 -51
- package/src/components/proxy-viewer/ApplicationBar.tsx +0 -123
- package/src/components/proxy-viewer/CompareDrawer.tsx +0 -984
- package/src/components/proxy-viewer/ConversationGroup.tsx +0 -208
- package/src/components/proxy-viewer/ConversationGroupList.tsx +0 -652
- package/src/components/proxy-viewer/ConversationHeader.tsx +0 -429
- package/src/components/proxy-viewer/LiveConnectionStatus.tsx +0 -186
- package/src/components/proxy-viewer/LogEntry.tsx +0 -1419
- package/src/components/proxy-viewer/LogEntryHeader.tsx +0 -596
- package/src/components/proxy-viewer/ProviderLogoStack.tsx +0 -57
- package/src/components/proxy-viewer/RawExportWarning.tsx +0 -77
- package/src/components/proxy-viewer/ReplayDialog.tsx +0 -636
- package/src/components/proxy-viewer/RequestToolsPanel.tsx +0 -299
- package/src/components/proxy-viewer/ResponseView.tsx +0 -202
- package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +0 -61
- package/src/components/proxy-viewer/StreamingChunkSequence.tsx +0 -265
- package/src/components/proxy-viewer/ThreadConnector.tsx +0 -165
- package/src/components/proxy-viewer/ToolTraceEvents.tsx +0 -64
- package/src/components/proxy-viewer/TurnGroup.tsx +0 -383
- package/src/components/proxy-viewer/TurnGroupList.tsx +0 -156
- package/src/components/proxy-viewer/accessibility.ts +0 -8
- package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +0 -887
- package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +0 -222
- package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +0 -737
- package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +0 -268
- package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +0 -53
- package/src/components/proxy-viewer/anatomy/types.ts +0 -41
- package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +0 -114
- package/src/components/proxy-viewer/bodyHydration.ts +0 -123
- package/src/components/proxy-viewer/cacheTrend.ts +0 -50
- package/src/components/proxy-viewer/diff/DiffView.tsx +0 -321
- package/src/components/proxy-viewer/diff/computeDiff.ts +0 -178
- package/src/components/proxy-viewer/diff/index.ts +0 -3
- package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +0 -194
- package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +0 -81
- package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +0 -21
- package/src/components/proxy-viewer/formats/index.tsx +0 -47
- package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +0 -442
- package/src/components/proxy-viewer/index.ts +0 -9
- package/src/components/proxy-viewer/lazy.ts +0 -41
- package/src/components/proxy-viewer/liveConnectionState.ts +0 -158
- package/src/components/proxy-viewer/log-formats/anthropic.ts +0 -191
- package/src/components/proxy-viewer/log-formats/index.ts +0 -23
- package/src/components/proxy-viewer/log-formats/openai.ts +0 -256
- package/src/components/proxy-viewer/log-formats/types.ts +0 -40
- package/src/components/proxy-viewer/log-formats/unknown.ts +0 -18
- package/src/components/proxy-viewer/logEntryVisibility.ts +0 -73
- package/src/components/proxy-viewer/logFocus.ts +0 -54
- package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +0 -124
- package/src/components/proxy-viewer/proxyViewerLogic.ts +0 -488
- package/src/components/proxy-viewer/replayComparison.ts +0 -131
- package/src/components/proxy-viewer/requestDiff.ts +0 -277
- package/src/components/proxy-viewer/requestTools.ts +0 -220
- package/src/components/proxy-viewer/useCopyFeedback.ts +0 -38
- package/src/components/proxy-viewer/useKeyboardNavigation.ts +0 -232
- package/src/components/proxy-viewer/viewerState.ts +0 -176
- package/src/components/ui/badge.tsx +0 -47
- package/src/components/ui/button.tsx +0 -47
- package/src/components/ui/collapsible.tsx +0 -21
- package/src/components/ui/confirm-dialog.tsx +0 -51
- package/src/components/ui/crab-logo.tsx +0 -45
- package/src/components/ui/crab-variants.tsx +0 -467
- package/src/components/ui/dialog.tsx +0 -130
- package/src/components/ui/icon-trigger.ts +0 -2
- package/src/components/ui/json-viewer-bulk.ts +0 -132
- package/src/components/ui/json-viewer.tsx +0 -508
- package/src/components/ui/scroll-area.tsx +0 -54
- package/src/components/ui/select.tsx +0 -178
- package/src/components/ui/separator.tsx +0 -28
- package/src/components/ui/tabs.tsx +0 -88
- package/src/components/ui/tooltip.tsx +0 -51
- package/src/components/ui/transient-toast.tsx +0 -105
- package/src/contracts/anthropic.ts +0 -230
- package/src/contracts/index.ts +0 -59
- package/src/contracts/json.ts +0 -37
- package/src/contracts/log.ts +0 -139
- package/src/contracts/openai.ts +0 -267
- package/src/index.css +0 -11
- package/src/knowledge/candidateStore.ts +0 -94
- package/src/knowledge/distiller.ts +0 -98
- package/src/knowledge/openclawClient.ts +0 -161
- package/src/knowledge/openclawGatewayClient.ts +0 -249
- package/src/knowledge/openclawMarkdown.ts +0 -146
- package/src/knowledge/redactor.ts +0 -80
- package/src/knowledge/types.ts +0 -83
- package/src/lib/alertContract.ts +0 -76
- package/src/lib/apiClient.ts +0 -166
- package/src/lib/apiFormat.ts +0 -29
- package/src/lib/backendImplementation.ts +0 -16
- package/src/lib/basePath.ts +0 -107
- package/src/lib/browserRuntimeContract.ts +0 -124
- package/src/lib/browserRuntimeResolver.ts +0 -333
- package/src/lib/chunkLoadRecovery.ts +0 -83
- package/src/lib/claudeCodeStrip.ts +0 -98
- package/src/lib/clipboard.ts +0 -67
- package/src/lib/codingAgentEndpoints.ts +0 -54
- package/src/lib/ecosystemContract.ts +0 -130
- package/src/lib/export-logs.ts +0 -287
- package/src/lib/groupContract.ts +0 -166
- package/src/lib/instanceContract.ts +0 -239
- package/src/lib/jsonFile.ts +0 -46
- package/src/lib/logImportContract.ts +0 -12
- package/src/lib/managedInstance.ts +0 -112
- package/src/lib/mask.ts +0 -4
- package/src/lib/objectUtils.ts +0 -22
- package/src/lib/piAgentContract.ts +0 -102
- package/src/lib/providerContract.ts +0 -125
- package/src/lib/providerImportContract.ts +0 -27
- package/src/lib/providerModelMetadata.ts +0 -458
- package/src/lib/providerTestContract.ts +0 -170
- package/src/lib/providerTestPrompt.ts +0 -100
- package/src/lib/publicBasePathContext.ts +0 -137
- package/src/lib/resourceLimits.ts +0 -345
- package/src/lib/runContract.ts +0 -164
- package/src/lib/runtimeConfig.ts +0 -43
- package/src/lib/safeDiagnostic.ts +0 -38
- package/src/lib/safeFetch.ts +0 -508
- package/src/lib/serverPort.ts +0 -47
- package/src/lib/sessionInfoContract.ts +0 -78
- package/src/lib/sessionUrl.ts +0 -46
- package/src/lib/ssrfGuard.ts +0 -133
- package/src/lib/stopReason.ts +0 -130
- package/src/lib/themeMode.ts +0 -41
- package/src/lib/timeDisplay.ts +0 -22
- package/src/lib/toolTrace.ts +0 -290
- package/src/lib/uiScale.ts +0 -117
- package/src/lib/unknown.ts +0 -27
- package/src/lib/upstreamUrl.ts +0 -150
- package/src/lib/useAlerts.ts +0 -82
- package/src/lib/useGroupEvidence.ts +0 -37
- package/src/lib/useGroups.ts +0 -32
- package/src/lib/useOnboarding.ts +0 -93
- package/src/lib/useProviders.ts +0 -30
- package/src/lib/useStripConfig.ts +0 -154
- package/src/lib/useThemeMode.ts +0 -40
- package/src/lib/useUiScale.ts +0 -52
- package/src/lib/utils.ts +0 -78
- package/src/mcp/currentContext.ts +0 -65
- package/src/mcp/instanceHandlers.ts +0 -151
- package/src/mcp/loopback.ts +0 -88
- package/src/mcp/mode.ts +0 -75
- package/src/mcp/previewExtractor.ts +0 -233
- package/src/mcp/server.ts +0 -2274
- package/src/mcp/toolHandlers.ts +0 -1533
- package/src/proxy/alerts.ts +0 -664
- package/src/proxy/chunkStorage.ts +0 -123
- package/src/proxy/config.ts +0 -218
- package/src/proxy/constants.ts +0 -66
- package/src/proxy/dataDir.ts +0 -147
- package/src/proxy/ecosystemExecutionConfirmation.ts +0 -114
- package/src/proxy/ecosystemExecutionRoute.ts +0 -116
- package/src/proxy/ecosystemRegistry.ts +0 -296
- package/src/proxy/ecosystemTasks.ts +0 -415
- package/src/proxy/evidenceAnalysis.ts +0 -526
- package/src/proxy/evidenceExporter.ts +0 -235
- package/src/proxy/formats/anthropic/anthropicProvider.ts +0 -75
- package/src/proxy/formats/anthropic/handler.ts +0 -75
- package/src/proxy/formats/anthropic/index.ts +0 -13
- package/src/proxy/formats/anthropic/register.ts +0 -4
- package/src/proxy/formats/anthropic/schemas.ts +0 -6
- package/src/proxy/formats/anthropic/stream.ts +0 -205
- package/src/proxy/formats/handler.ts +0 -46
- package/src/proxy/formats/index.ts +0 -13
- package/src/proxy/formats/jsonSchema.ts +0 -2
- package/src/proxy/formats/openai/alibabaProvider.ts +0 -34
- package/src/proxy/formats/openai/codexMetadata.ts +0 -61
- package/src/proxy/formats/openai/handler.ts +0 -133
- package/src/proxy/formats/openai/index.ts +0 -33
- package/src/proxy/formats/openai/provider.ts +0 -50
- package/src/proxy/formats/openai/register.ts +0 -4
- package/src/proxy/formats/openai/schemas.ts +0 -25
- package/src/proxy/formats/openai/stream.ts +0 -600
- package/src/proxy/formats/openai/zhipuProvider.ts +0 -31
- package/src/proxy/formats/providerAdapter.ts +0 -50
- package/src/proxy/formats/providerRegistry.ts +0 -51
- package/src/proxy/formats/providers/index.ts +0 -3
- package/src/proxy/formats/registry.ts +0 -65
- package/src/proxy/groupEvidenceExporter.ts +0 -371
- package/src/proxy/groupStore.ts +0 -336
- package/src/proxy/handler.ts +0 -729
- package/src/proxy/identityProxy.ts +0 -1842
- package/src/proxy/jsonlRecovery.ts +0 -554
- package/src/proxy/logBodyChunks.ts +0 -91
- package/src/proxy/logFinalizer.ts +0 -777
- package/src/proxy/logFinalizer.worker.ts +0 -24
- package/src/proxy/logImportUpload.ts +0 -128
- package/src/proxy/logImporter.ts +0 -813
- package/src/proxy/logIndex.ts +0 -663
- package/src/proxy/logSearch.ts +0 -122
- package/src/proxy/logger.ts +0 -537
- package/src/proxy/openaiOrphanToolStrip.ts +0 -142
- package/src/proxy/platformCommands.ts +0 -253
- package/src/proxy/privateDataPath.ts +0 -419
- package/src/proxy/providerImporters.ts +0 -578
- package/src/proxy/providerScanStore.ts +0 -87
- package/src/proxy/providerSecretStore.ts +0 -216
- package/src/proxy/providers.ts +0 -998
- package/src/proxy/rawStreamCapture.ts +0 -342
- package/src/proxy/runFailures.ts +0 -94
- package/src/proxy/runStore.ts +0 -214
- package/src/proxy/runtimeAdmission.ts +0 -55
- package/src/proxy/runtimeHealth.ts +0 -221
- package/src/proxy/runtimeMetrics.ts +0 -149
- package/src/proxy/runtimeShutdown.ts +0 -157
- package/src/proxy/schemas.ts +0 -112
- package/src/proxy/sessionArchive.ts +0 -479
- package/src/proxy/sessionInfo.ts +0 -266
- package/src/proxy/sessionProcess.ts +0 -194
- package/src/proxy/sessionRuntime.ts +0 -121
- package/src/proxy/sessionSupervisor.ts +0 -305
- package/src/proxy/sessionWorkerEntry.ts +0 -26
- package/src/proxy/shutdownCoordinator.ts +0 -90
- package/src/proxy/socketTracker.ts +0 -639
- package/src/proxy/sqliteLogIndex.ts +0 -733
- package/src/proxy/storageLifecycle.ts +0 -432
- package/src/proxy/store.ts +0 -1773
- package/src/proxy/streamTiming.ts +0 -31
- package/src/proxy/toolSchemaWarnings.ts +0 -529
- package/src/proxy/uiAssetServer.ts +0 -106
- package/src/proxy/upstream.ts +0 -127
- package/src/services/alerts.ts +0 -12
- package/src/services/groups.ts +0 -119
- package/src/services/piAgent.ts +0 -421
- package/src/services/piAgentRetrieval.ts +0 -1180
- package/src/services/result.ts +0 -17
- package/src/services/runs.ts +0 -76
- package/src/ui/bootstrap.ts +0 -167
- package/src/ui/index.html +0 -38
- package/src/ui/logsStreamClient.ts +0 -20
- package/src/ui/main.tsx +0 -13
- package/src/ui/public/runtime-config.js +0 -2
- package/src/ui/router.tsx +0 -13
- package/src/ui/routes/__root.tsx +0 -69
- package/src/ui/routes/index.tsx +0 -35
- package/src/ui/routes/session/$sessionId.tsx +0 -50
- package/src/ui/runtimeDiscovery.ts +0 -50
|
@@ -1,554 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { Buffer } from "node:buffer";
|
|
3
|
-
import { constants, existsSync, lstatSync } from "node:fs";
|
|
4
|
-
import { link, lstat, open, readFile, readdir, stat, unlink } from "node:fs/promises";
|
|
5
|
-
import { basename, join, resolve } from "node:path";
|
|
6
|
-
import { CapturedLogSchema } from "./schemas";
|
|
7
|
-
import {
|
|
8
|
-
ensurePrivateDirectorySync,
|
|
9
|
-
PRIVATE_FILE_MODE,
|
|
10
|
-
securePrivateFileSync,
|
|
11
|
-
} from "./privateDataPath";
|
|
12
|
-
|
|
13
|
-
const READ_CHUNK_BYTES = 64 * 1024;
|
|
14
|
-
const QUARANTINE_DIRECTORY = "quarantine";
|
|
15
|
-
|
|
16
|
-
export type CapturedLogRecoveryLevel = "ok" | "degraded" | "unavailable";
|
|
17
|
-
|
|
18
|
-
export type CapturedLogRecoveryHealth = {
|
|
19
|
-
level: CapturedLogRecoveryLevel;
|
|
20
|
-
diagnostic: string | null;
|
|
21
|
-
repairedFiles: number;
|
|
22
|
-
delimiterRepairs: number;
|
|
23
|
-
quarantinedTails: number;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
type FileSignature = {
|
|
27
|
-
size: number;
|
|
28
|
-
modifiedAt: number;
|
|
29
|
-
changedAt: number;
|
|
30
|
-
device: number;
|
|
31
|
-
inode: number;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type RecoveryContext = {
|
|
35
|
-
root: string;
|
|
36
|
-
health: CapturedLogRecoveryHealth;
|
|
37
|
-
startup: Promise<CapturedLogRecoveryHealth> | null;
|
|
38
|
-
startupComplete: boolean;
|
|
39
|
-
safeFiles: Map<string, FileSignature>;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
type RecordValidation =
|
|
43
|
-
| { valid: true }
|
|
44
|
-
| { valid: false; reason: "invalid-json" | "invalid-log-schema" };
|
|
45
|
-
|
|
46
|
-
type InvalidRecordReason = "invalid-json" | "invalid-log-schema";
|
|
47
|
-
|
|
48
|
-
type FileRecoveryResult =
|
|
49
|
-
| { status: "clean"; signature: FileSignature }
|
|
50
|
-
| { status: "delimiter-appended"; signature: FileSignature }
|
|
51
|
-
| { status: "tail-quarantined"; signature: FileSignature }
|
|
52
|
-
| {
|
|
53
|
-
status: "interior-corruption";
|
|
54
|
-
line: number;
|
|
55
|
-
byteOffset: number;
|
|
56
|
-
recordHash: string;
|
|
57
|
-
reason: InvalidRecordReason;
|
|
58
|
-
}
|
|
59
|
-
| { status: "failed"; reason: string };
|
|
60
|
-
|
|
61
|
-
type ScanResult =
|
|
62
|
-
| {
|
|
63
|
-
status: "complete";
|
|
64
|
-
pending: Buffer;
|
|
65
|
-
pendingOffset: number;
|
|
66
|
-
nextLine: number;
|
|
67
|
-
}
|
|
68
|
-
| {
|
|
69
|
-
status: "interior-corruption";
|
|
70
|
-
line: number;
|
|
71
|
-
byteOffset: number;
|
|
72
|
-
recordHash: string;
|
|
73
|
-
reason: InvalidRecordReason;
|
|
74
|
-
}
|
|
75
|
-
| { status: "failed"; reason: string };
|
|
76
|
-
|
|
77
|
-
type QuarantineRecord = {
|
|
78
|
-
schemaVersion: 1;
|
|
79
|
-
reason: "invalid-final-jsonl-record";
|
|
80
|
-
sourceFile: string;
|
|
81
|
-
sourcePathHash: string;
|
|
82
|
-
recoveredAt: string;
|
|
83
|
-
tailOffset: number;
|
|
84
|
-
tailBytes: number;
|
|
85
|
-
tailSha256: string;
|
|
86
|
-
tailBase64: string;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
type ExistingQuarantineState = "missing" | "matching" | "conflicting";
|
|
90
|
-
|
|
91
|
-
let activeContext: RecoveryContext | null = null;
|
|
92
|
-
|
|
93
|
-
function emptyHealth(): CapturedLogRecoveryHealth {
|
|
94
|
-
return {
|
|
95
|
-
level: "ok",
|
|
96
|
-
diagnostic: null,
|
|
97
|
-
repairedFiles: 0,
|
|
98
|
-
delimiterRepairs: 0,
|
|
99
|
-
quarantinedTails: 0,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function contextFor(logDir: string): RecoveryContext {
|
|
104
|
-
const root = resolve(logDir);
|
|
105
|
-
if (activeContext === null || activeContext.root !== root) {
|
|
106
|
-
activeContext = {
|
|
107
|
-
root,
|
|
108
|
-
health: emptyHealth(),
|
|
109
|
-
startup: null,
|
|
110
|
-
startupComplete: false,
|
|
111
|
-
safeFiles: new Map(),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return activeContext;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function copyHealth(health: CapturedLogRecoveryHealth): CapturedLogRecoveryHealth {
|
|
118
|
-
return { ...health };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function hashBytes(value: Uint8Array): string {
|
|
122
|
-
return createHash("sha256").update(value).digest("hex");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function signatureFromStats(stats: {
|
|
126
|
-
size: number;
|
|
127
|
-
mtimeMs: number;
|
|
128
|
-
ctimeMs: number;
|
|
129
|
-
dev: number;
|
|
130
|
-
ino: number;
|
|
131
|
-
}): FileSignature {
|
|
132
|
-
return {
|
|
133
|
-
size: stats.size,
|
|
134
|
-
modifiedAt: stats.mtimeMs,
|
|
135
|
-
changedAt: stats.ctimeMs,
|
|
136
|
-
device: stats.dev,
|
|
137
|
-
inode: stats.ino,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function signaturesMatch(left: FileSignature, right: FileSignature): boolean {
|
|
142
|
-
return (
|
|
143
|
-
left.size === right.size &&
|
|
144
|
-
left.modifiedAt === right.modifiedAt &&
|
|
145
|
-
left.changedAt === right.changedAt &&
|
|
146
|
-
left.device === right.device &&
|
|
147
|
-
left.inode === right.inode
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function validateRecord(bytes: Buffer): RecordValidation {
|
|
152
|
-
const withoutCarriageReturn =
|
|
153
|
-
bytes.length > 0 && bytes[bytes.length - 1] === 0x0d ? bytes.subarray(0, -1) : bytes;
|
|
154
|
-
const text = withoutCarriageReturn.toString("utf-8");
|
|
155
|
-
if (text.trim() === "") return { valid: true };
|
|
156
|
-
|
|
157
|
-
let parsed: unknown;
|
|
158
|
-
try {
|
|
159
|
-
parsed = JSON.parse(text);
|
|
160
|
-
} catch {
|
|
161
|
-
return { valid: false, reason: "invalid-json" };
|
|
162
|
-
}
|
|
163
|
-
return CapturedLogSchema.safeParse(parsed).success
|
|
164
|
-
? { valid: true }
|
|
165
|
-
: { valid: false, reason: "invalid-log-schema" };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async function scanFile(
|
|
169
|
-
handle: Awaited<ReturnType<typeof open>>,
|
|
170
|
-
size: number,
|
|
171
|
-
): Promise<ScanResult> {
|
|
172
|
-
const readBuffer = Buffer.alloc(READ_CHUNK_BYTES);
|
|
173
|
-
let position = 0;
|
|
174
|
-
let pending = Buffer.alloc(0);
|
|
175
|
-
let pendingOffset = 0;
|
|
176
|
-
let line = 1;
|
|
177
|
-
|
|
178
|
-
while (position < size) {
|
|
179
|
-
const length = Math.min(READ_CHUNK_BYTES, size - position);
|
|
180
|
-
let bytesRead: number;
|
|
181
|
-
try {
|
|
182
|
-
const result = await handle.read(readBuffer, 0, length, position);
|
|
183
|
-
bytesRead = result.bytesRead;
|
|
184
|
-
} catch (error) {
|
|
185
|
-
return { status: "failed", reason: `read failed: ${String(error)}` };
|
|
186
|
-
}
|
|
187
|
-
if (bytesRead === 0) {
|
|
188
|
-
return { status: "failed", reason: "captured-log file changed while being scanned" };
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
const chunk = readBuffer.subarray(0, bytesRead);
|
|
192
|
-
const combined = pending.length === 0 ? Buffer.from(chunk) : Buffer.concat([pending, chunk]);
|
|
193
|
-
let consumed = 0;
|
|
194
|
-
let newline = combined.indexOf(0x0a, consumed);
|
|
195
|
-
while (newline !== -1) {
|
|
196
|
-
const record = combined.subarray(consumed, newline);
|
|
197
|
-
const validation = validateRecord(record);
|
|
198
|
-
if (!validation.valid) {
|
|
199
|
-
return {
|
|
200
|
-
status: "interior-corruption",
|
|
201
|
-
line,
|
|
202
|
-
byteOffset: pendingOffset + consumed,
|
|
203
|
-
recordHash: hashBytes(record),
|
|
204
|
-
reason: validation.reason,
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
consumed = newline + 1;
|
|
208
|
-
line += 1;
|
|
209
|
-
newline = combined.indexOf(0x0a, consumed);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
pending = Buffer.from(combined.subarray(consumed));
|
|
213
|
-
pendingOffset += consumed;
|
|
214
|
-
position += bytesRead;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return { status: "complete", pending, pendingOffset, nextLine: line };
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
async function syncDirectory(directory: string): Promise<void> {
|
|
221
|
-
let handle: Awaited<ReturnType<typeof open>> | null = null;
|
|
222
|
-
try {
|
|
223
|
-
handle = await open(directory, "r");
|
|
224
|
-
await handle.sync();
|
|
225
|
-
} catch {
|
|
226
|
-
// Directory fsync is unavailable on some supported Windows filesystems.
|
|
227
|
-
} finally {
|
|
228
|
-
await handle?.close().catch(() => undefined);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
async function removeTemporaryFile(filePath: string): Promise<void> {
|
|
233
|
-
await unlink(filePath).catch(() => undefined);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function readOwnProperty(value: unknown, name: string): unknown {
|
|
237
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return undefined;
|
|
238
|
-
return Object.getOwnPropertyDescriptor(value, name)?.value;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function inspectExistingQuarantine(
|
|
242
|
-
filePath: string,
|
|
243
|
-
expected: QuarantineRecord,
|
|
244
|
-
): Promise<ExistingQuarantineState> {
|
|
245
|
-
if (!existsSync(filePath)) return "missing";
|
|
246
|
-
try {
|
|
247
|
-
const parsed: unknown = JSON.parse(await readFile(filePath, "utf-8"));
|
|
248
|
-
return readOwnProperty(parsed, "schemaVersion") === expected.schemaVersion &&
|
|
249
|
-
readOwnProperty(parsed, "reason") === expected.reason &&
|
|
250
|
-
readOwnProperty(parsed, "sourceFile") === expected.sourceFile &&
|
|
251
|
-
readOwnProperty(parsed, "sourcePathHash") === expected.sourcePathHash &&
|
|
252
|
-
readOwnProperty(parsed, "tailOffset") === expected.tailOffset &&
|
|
253
|
-
readOwnProperty(parsed, "tailBytes") === expected.tailBytes &&
|
|
254
|
-
readOwnProperty(parsed, "tailSha256") === expected.tailSha256 &&
|
|
255
|
-
readOwnProperty(parsed, "tailBase64") === expected.tailBase64
|
|
256
|
-
? "matching"
|
|
257
|
-
: "conflicting";
|
|
258
|
-
} catch {
|
|
259
|
-
return "conflicting";
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function quarantineTail(
|
|
264
|
-
context: RecoveryContext,
|
|
265
|
-
sourceFile: string,
|
|
266
|
-
tail: Buffer,
|
|
267
|
-
tailOffset: number,
|
|
268
|
-
): Promise<string | null> {
|
|
269
|
-
const quarantineDirectory = join(context.root, QUARANTINE_DIRECTORY);
|
|
270
|
-
if (!ensurePrivateDirectorySync(quarantineDirectory)) return null;
|
|
271
|
-
|
|
272
|
-
const tailSha256 = hashBytes(tail);
|
|
273
|
-
const sourceFileName = basename(sourceFile);
|
|
274
|
-
const destination = join(quarantineDirectory, `${sourceFileName}.tail-${tailSha256}.json`);
|
|
275
|
-
const temporary = join(
|
|
276
|
-
quarantineDirectory,
|
|
277
|
-
`.${sourceFileName}.${String(process.pid)}.${randomUUID()}.tmp`,
|
|
278
|
-
);
|
|
279
|
-
const record: QuarantineRecord = {
|
|
280
|
-
schemaVersion: 1,
|
|
281
|
-
reason: "invalid-final-jsonl-record",
|
|
282
|
-
sourceFile: sourceFileName,
|
|
283
|
-
sourcePathHash: hashBytes(Buffer.from(resolve(sourceFile), "utf-8")),
|
|
284
|
-
recoveredAt: new Date().toISOString(),
|
|
285
|
-
tailOffset,
|
|
286
|
-
tailBytes: tail.length,
|
|
287
|
-
tailSha256,
|
|
288
|
-
tailBase64: tail.toString("base64"),
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
const existing = await inspectExistingQuarantine(destination, record);
|
|
292
|
-
if (existing === "matching") {
|
|
293
|
-
return securePrivateFileSync(destination) ? destination : null;
|
|
294
|
-
}
|
|
295
|
-
if (existing === "conflicting") return null;
|
|
296
|
-
|
|
297
|
-
let handle: Awaited<ReturnType<typeof open>> | null = null;
|
|
298
|
-
try {
|
|
299
|
-
handle = await open(temporary, "wx", PRIVATE_FILE_MODE);
|
|
300
|
-
await handle.writeFile(`${JSON.stringify(record)}\n`, "utf-8");
|
|
301
|
-
await handle.sync();
|
|
302
|
-
await handle.close();
|
|
303
|
-
handle = null;
|
|
304
|
-
if (!securePrivateFileSync(temporary)) {
|
|
305
|
-
await removeTemporaryFile(temporary);
|
|
306
|
-
return null;
|
|
307
|
-
}
|
|
308
|
-
// A hard-link install has create-if-absent semantics on every supported platform. Unlike
|
|
309
|
-
// rename(), it cannot overwrite a conflicting quarantine artifact that appears after the
|
|
310
|
-
// preflight check on POSIX.
|
|
311
|
-
await link(temporary, destination);
|
|
312
|
-
await unlink(temporary);
|
|
313
|
-
if (!securePrivateFileSync(destination)) return null;
|
|
314
|
-
await syncDirectory(quarantineDirectory);
|
|
315
|
-
return destination;
|
|
316
|
-
} catch {
|
|
317
|
-
await handle?.close().catch(() => undefined);
|
|
318
|
-
await removeTemporaryFile(temporary);
|
|
319
|
-
const racedExisting = await inspectExistingQuarantine(destination, record);
|
|
320
|
-
return racedExisting === "matching" && securePrivateFileSync(destination) ? destination : null;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
async function recoverFile(
|
|
325
|
-
context: RecoveryContext,
|
|
326
|
-
filePath: string,
|
|
327
|
-
): Promise<FileRecoveryResult> {
|
|
328
|
-
if (!existsSync(filePath)) {
|
|
329
|
-
context.safeFiles.delete(filePath);
|
|
330
|
-
return {
|
|
331
|
-
status: "clean",
|
|
332
|
-
signature: { size: 0, modifiedAt: 0, changedAt: 0, device: 0, inode: 0 },
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
let fileStats: Awaited<ReturnType<typeof lstat>>;
|
|
337
|
-
try {
|
|
338
|
-
fileStats = await lstat(filePath);
|
|
339
|
-
} catch (error) {
|
|
340
|
-
return { status: "failed", reason: `stat failed: ${String(error)}` };
|
|
341
|
-
}
|
|
342
|
-
if (fileStats.isSymbolicLink() || !fileStats.isFile()) {
|
|
343
|
-
return { status: "failed", reason: "captured-log path is not a regular private file" };
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const initialSignature = signatureFromStats(fileStats);
|
|
347
|
-
const safeSignature = context.safeFiles.get(filePath);
|
|
348
|
-
if (safeSignature !== undefined && signaturesMatch(safeSignature, initialSignature)) {
|
|
349
|
-
return { status: "clean", signature: initialSignature };
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
let handle: Awaited<ReturnType<typeof open>> | null = null;
|
|
353
|
-
try {
|
|
354
|
-
handle = await open(filePath, constants.O_RDWR | constants.O_NOFOLLOW);
|
|
355
|
-
const openedStats = await handle.stat();
|
|
356
|
-
if (!openedStats.isFile()) {
|
|
357
|
-
return { status: "failed", reason: "captured-log handle is not a regular file" };
|
|
358
|
-
}
|
|
359
|
-
const openedSignature = signatureFromStats(openedStats);
|
|
360
|
-
if (!signaturesMatch(initialSignature, openedSignature)) {
|
|
361
|
-
return {
|
|
362
|
-
status: "failed",
|
|
363
|
-
reason: "captured-log path changed between metadata validation and open",
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
const scan = await scanFile(handle, openedStats.size);
|
|
367
|
-
const scannedSignature = signatureFromStats(await handle.stat());
|
|
368
|
-
if (!signaturesMatch(openedSignature, scannedSignature)) {
|
|
369
|
-
return {
|
|
370
|
-
status: "failed",
|
|
371
|
-
reason: "captured-log file changed while recovery was scanning it",
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
switch (scan.status) {
|
|
375
|
-
case "failed":
|
|
376
|
-
return scan;
|
|
377
|
-
case "interior-corruption":
|
|
378
|
-
return scan;
|
|
379
|
-
case "complete": {
|
|
380
|
-
if (scan.pending.length === 0) {
|
|
381
|
-
return { status: "clean", signature: signatureFromStats(await handle.stat()) };
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const validation = validateRecord(scan.pending);
|
|
385
|
-
if (validation.valid) {
|
|
386
|
-
const write = await handle.write(Buffer.from("\n", "utf-8"), 0, 1, openedStats.size);
|
|
387
|
-
if (write.bytesWritten !== 1) {
|
|
388
|
-
return { status: "failed", reason: "captured-log delimiter append was incomplete" };
|
|
389
|
-
}
|
|
390
|
-
await handle.sync();
|
|
391
|
-
return {
|
|
392
|
-
status: "delimiter-appended",
|
|
393
|
-
signature: signatureFromStats(await handle.stat()),
|
|
394
|
-
};
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const quarantinePath = await quarantineTail(
|
|
398
|
-
context,
|
|
399
|
-
filePath,
|
|
400
|
-
scan.pending,
|
|
401
|
-
scan.pendingOffset,
|
|
402
|
-
);
|
|
403
|
-
if (quarantinePath === null) {
|
|
404
|
-
return { status: "failed", reason: "invalid tail could not be privately quarantined" };
|
|
405
|
-
}
|
|
406
|
-
const beforeTruncate = signatureFromStats(await handle.stat());
|
|
407
|
-
if (!signaturesMatch(openedSignature, beforeTruncate)) {
|
|
408
|
-
return {
|
|
409
|
-
status: "failed",
|
|
410
|
-
reason: "captured-log file changed before invalid-tail truncation",
|
|
411
|
-
};
|
|
412
|
-
}
|
|
413
|
-
await handle.truncate(scan.pendingOffset);
|
|
414
|
-
await handle.sync();
|
|
415
|
-
return {
|
|
416
|
-
status: "tail-quarantined",
|
|
417
|
-
signature: signatureFromStats(await handle.stat()),
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
return { status: "failed", reason: "unsupported captured-log scan state" };
|
|
422
|
-
} catch (error) {
|
|
423
|
-
return { status: "failed", reason: String(error) };
|
|
424
|
-
} finally {
|
|
425
|
-
await handle?.close().catch(() => undefined);
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function applyFileResult(
|
|
430
|
-
context: RecoveryContext,
|
|
431
|
-
filePath: string,
|
|
432
|
-
result: FileRecoveryResult,
|
|
433
|
-
): void {
|
|
434
|
-
const file = basename(filePath);
|
|
435
|
-
switch (result.status) {
|
|
436
|
-
case "clean":
|
|
437
|
-
context.safeFiles.set(filePath, result.signature);
|
|
438
|
-
return;
|
|
439
|
-
case "delimiter-appended":
|
|
440
|
-
context.safeFiles.set(filePath, result.signature);
|
|
441
|
-
context.health.delimiterRepairs += 1;
|
|
442
|
-
context.health.repairedFiles += 1;
|
|
443
|
-
return;
|
|
444
|
-
case "tail-quarantined":
|
|
445
|
-
context.safeFiles.set(filePath, result.signature);
|
|
446
|
-
context.health.quarantinedTails += 1;
|
|
447
|
-
context.health.repairedFiles += 1;
|
|
448
|
-
if (context.health.level !== "unavailable") {
|
|
449
|
-
context.health.level = "degraded";
|
|
450
|
-
context.health.diagnostic = `Recovered and quarantined an invalid JSONL tail in ${file}`;
|
|
451
|
-
}
|
|
452
|
-
return;
|
|
453
|
-
case "interior-corruption":
|
|
454
|
-
context.safeFiles.delete(filePath);
|
|
455
|
-
context.health.level = "unavailable";
|
|
456
|
-
context.health.diagnostic =
|
|
457
|
-
`JSONL corruption in ${file} at line ${String(result.line)} ` +
|
|
458
|
-
`(offset ${String(result.byteOffset)}, ${result.reason}, sha256 ${result.recordHash})`;
|
|
459
|
-
return;
|
|
460
|
-
case "failed":
|
|
461
|
-
context.safeFiles.delete(filePath);
|
|
462
|
-
context.health.level = "unavailable";
|
|
463
|
-
context.health.diagnostic = `JSONL recovery failed for ${file}: ${result.reason}`;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
async function runStartupRecovery(context: RecoveryContext): Promise<CapturedLogRecoveryHealth> {
|
|
468
|
-
if (!ensurePrivateDirectorySync(context.root)) {
|
|
469
|
-
context.health.level = "unavailable";
|
|
470
|
-
context.health.diagnostic = "Captured-log directory could not be made private";
|
|
471
|
-
return copyHealth(context.health);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
let entries: string[];
|
|
475
|
-
try {
|
|
476
|
-
entries = await readdir(context.root);
|
|
477
|
-
} catch (error) {
|
|
478
|
-
context.health.level = "unavailable";
|
|
479
|
-
context.health.diagnostic = `Captured-log directory could not be listed: ${String(error)}`;
|
|
480
|
-
return copyHealth(context.health);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const files = entries.filter((entry) => entry.endsWith(".jsonl")).sort();
|
|
484
|
-
for (const file of files) {
|
|
485
|
-
const filePath = join(context.root, file);
|
|
486
|
-
const result = await recoverFile(context, filePath);
|
|
487
|
-
applyFileResult(context, filePath, result);
|
|
488
|
-
if (context.health.level === "unavailable") break;
|
|
489
|
-
}
|
|
490
|
-
return copyHealth(context.health);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/** Recover all captured-log ledgers once before startup reads, indexing, readiness, or appends. */
|
|
494
|
-
export async function recoverCapturedLogStorage(
|
|
495
|
-
logDir: string,
|
|
496
|
-
): Promise<CapturedLogRecoveryHealth> {
|
|
497
|
-
const context = contextFor(logDir);
|
|
498
|
-
if (context.startup === null) {
|
|
499
|
-
context.startup = runStartupRecovery(context).then((health) => {
|
|
500
|
-
context.startupComplete = true;
|
|
501
|
-
return health;
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
return await context.startup;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/** Re-check a ledger immediately before appending; unchanged files use a stat-only fast path. */
|
|
508
|
-
export async function recoverCapturedLogFileBeforeAppend(
|
|
509
|
-
logDir: string,
|
|
510
|
-
filePath: string,
|
|
511
|
-
): Promise<CapturedLogRecoveryHealth> {
|
|
512
|
-
const context = contextFor(logDir);
|
|
513
|
-
await recoverCapturedLogStorage(logDir);
|
|
514
|
-
if (context.health.level === "unavailable") return copyHealth(context.health);
|
|
515
|
-
|
|
516
|
-
const result = await recoverFile(context, filePath);
|
|
517
|
-
applyFileResult(context, filePath, result);
|
|
518
|
-
return copyHealth(context.health);
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
/** Refresh the trusted signature after this process successfully appends a complete JSONL record. */
|
|
522
|
-
export async function markCapturedLogFileAppended(logDir: string, filePath: string): Promise<void> {
|
|
523
|
-
const context = contextFor(logDir);
|
|
524
|
-
try {
|
|
525
|
-
const fileStats = await stat(filePath);
|
|
526
|
-
context.safeFiles.set(filePath, signatureFromStats(fileStats));
|
|
527
|
-
} catch {
|
|
528
|
-
context.safeFiles.delete(filePath);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
export function getCapturedLogRecoveryHealth(logDir: string): CapturedLogRecoveryHealth {
|
|
533
|
-
return copyHealth(contextFor(logDir).health);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
/** Fail-closed guard for the process-exit writer, where asynchronous recovery is unavailable. */
|
|
537
|
-
export function canAppendCapturedLogFileOnExit(logDir: string, filePath: string): boolean {
|
|
538
|
-
const context = contextFor(logDir);
|
|
539
|
-
if (!context.startupComplete || context.health.level === "unavailable") return false;
|
|
540
|
-
const trusted = context.safeFiles.get(filePath);
|
|
541
|
-
if (trusted === undefined) return false;
|
|
542
|
-
|
|
543
|
-
try {
|
|
544
|
-
const observed = lstatSync(filePath);
|
|
545
|
-
if (observed === undefined || observed.isSymbolicLink() || !observed.isFile()) return false;
|
|
546
|
-
return signaturesMatch(trusted, signatureFromStats(observed));
|
|
547
|
-
} catch {
|
|
548
|
-
return false;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
export function _resetCapturedLogRecoveryForTests(): void {
|
|
553
|
-
activeContext = null;
|
|
554
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
|
-
import type { CapturedLog, LogBodyChunk, LogBodyPart } from "../contracts";
|
|
3
|
-
|
|
4
|
-
export const DEFAULT_LOG_BODY_CHUNK_BYTES = 256 * 1024;
|
|
5
|
-
export const MAX_LOG_BODY_CHUNK_BYTES = 1024 * 1024;
|
|
6
|
-
|
|
7
|
-
export function normalizeLogBodyChunkOffset(offset: number): number {
|
|
8
|
-
if (!Number.isInteger(offset) || offset < 0) return 0;
|
|
9
|
-
return offset;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function normalizeLogBodyChunkLimit(limit: number): number {
|
|
13
|
-
if (!Number.isInteger(limit) || limit < 1) return DEFAULT_LOG_BODY_CHUNK_BYTES;
|
|
14
|
-
return Math.min(limit, MAX_LOG_BODY_CHUNK_BYTES);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function bodyTextForPart(log: CapturedLog, part: LogBodyPart): string | null {
|
|
18
|
-
switch (part) {
|
|
19
|
-
case "request":
|
|
20
|
-
return log.rawRequestBody;
|
|
21
|
-
case "response":
|
|
22
|
-
return log.responseText;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function isUtf8ContinuationByte(byte: number | undefined): boolean {
|
|
27
|
-
return byte !== undefined && (byte & 0b1100_0000) === 0b1000_0000;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function expandToUtf8Boundary(buffer: Buffer, end: number): number {
|
|
31
|
-
if (end >= buffer.byteLength) return buffer.byteLength;
|
|
32
|
-
let safeEnd = end;
|
|
33
|
-
while (safeEnd < buffer.byteLength && isUtf8ContinuationByte(buffer[safeEnd])) {
|
|
34
|
-
safeEnd++;
|
|
35
|
-
}
|
|
36
|
-
return safeEnd;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function createLogBodyChunk({
|
|
40
|
-
log,
|
|
41
|
-
part,
|
|
42
|
-
offset,
|
|
43
|
-
limit,
|
|
44
|
-
}: {
|
|
45
|
-
log: CapturedLog;
|
|
46
|
-
part: LogBodyPart;
|
|
47
|
-
offset: number;
|
|
48
|
-
limit: number;
|
|
49
|
-
}): LogBodyChunk {
|
|
50
|
-
const text = bodyTextForPart(log, part);
|
|
51
|
-
const normalizedLimit = normalizeLogBodyChunkLimit(limit);
|
|
52
|
-
if (text === null || text === "") {
|
|
53
|
-
return {
|
|
54
|
-
logId: log.id,
|
|
55
|
-
part,
|
|
56
|
-
text: "",
|
|
57
|
-
offset: 0,
|
|
58
|
-
limit: normalizedLimit,
|
|
59
|
-
totalBytes: 0,
|
|
60
|
-
textBytes: 0,
|
|
61
|
-
nextOffset: null,
|
|
62
|
-
hasMore: false,
|
|
63
|
-
contentMode: "empty",
|
|
64
|
-
};
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const bodyBytes = Buffer.from(text, "utf8");
|
|
68
|
-
const totalBytes = bodyBytes.byteLength;
|
|
69
|
-
const normalizedOffset = Math.min(normalizeLogBodyChunkOffset(offset), totalBytes);
|
|
70
|
-
const end = expandToUtf8Boundary(
|
|
71
|
-
bodyBytes,
|
|
72
|
-
Math.min(normalizedOffset + normalizedLimit, totalBytes),
|
|
73
|
-
);
|
|
74
|
-
const chunkText = bodyBytes.toString("utf8", normalizedOffset, end);
|
|
75
|
-
const textBytes = end - normalizedOffset;
|
|
76
|
-
const hasMore = end < totalBytes;
|
|
77
|
-
const contentMode = hasMore || normalizedOffset > 0 ? "partial" : "full";
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
logId: log.id,
|
|
81
|
-
part,
|
|
82
|
-
text: chunkText,
|
|
83
|
-
offset: normalizedOffset,
|
|
84
|
-
limit: normalizedLimit,
|
|
85
|
-
totalBytes,
|
|
86
|
-
textBytes,
|
|
87
|
-
nextOffset: hasMore ? end : null,
|
|
88
|
-
hasMore,
|
|
89
|
-
contentMode,
|
|
90
|
-
};
|
|
91
|
-
}
|