fix(inspector): retain captures through authorization cleanup failures

This commit is contained in:
morluto committed 2026-10-24 03:16:30 +08:00
1 parent fc404f008d
commit 09b188bb0d
3 files changed
+62 -28

No files matched your search

+4
View File
@@ -147,6 +147,10 @@ Inspector does not authenticate the operating-system PID or distinguish an
Electron role, so Evidence records the role authority as
`caller-declared-unverified`.
If final script-location authorization fails, collected script and context facts
remain available in the typed failure with explicitly unverified locations.
Descriptor cleanup status and its retry owner remain independent of that capture.
Electron main, preload, and renderer behavior can therefore be observed only
as separate Inspector targets. The provider does not infer that two
targets belong to the same Electron application.
@@ -73,6 +73,10 @@ vi.mock("node:fs/promises", async (importOriginal) => {
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { CdpElectronProvider } from "./CdpElectronProvider.js";
import { V8InspectorProvider } from "../inspector/V8InspectorProvider.js";
import { observeJavaScriptRuntime } from "../application/javascript/JavaScriptRuntimeObservationService.js";
import { startFakeV8Inspector } from "../../tests/fixtures/inspector/fakeV8Inspector.js";
import { prependAsarJsonWhitespace } from "../../tests/fixtures/asarHeader.js";
import {
authorizedElectronFile,
mapElectronFileAuthorizationFailure,
@@ -120,6 +124,50 @@ const asarPage = async (): Promise<string> => {
return pathToFileURL(join(archive, "index.html")).href;
};
it("retains Inspector scripts and contexts when final ASAR authorization cannot close its descriptor", async () => {
const scriptUrl = await asarPage();
const archive = handleState.archive;
if (archive === undefined) throw new Error("Missing fixture ASAR");
const inspector = await startFakeV8Inspector({
targetUrl: "https://example.test/entry.js",
scriptUrls: [scriptUrl],
});
const provider = new V8InspectorProvider();
const input = {
inspector_endpoint: inspector.endpoint,
target_id: inspector.targetId,
observation_ms: 0,
};
try {
const control = await observeJavaScriptRuntime(provider, input);
expect(control.ok).toBe(true);
if (!control.ok) throw control.error;
expect(control.value.normalized_result).toMatchObject({
scripts: { observed_total: 1 },
execution_contexts: [{ name: "main" }],
});
await prependAsarJsonWhitespace(archive, " ");
handleState.failCloseCount = 1;
const result = await observeJavaScriptRuntime(provider, input);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("Expected descriptor close failure");
expect(result.error.cleanupIncomplete).toBe(true);
expect(result.error.partialObservation).toMatchObject({
scripts: {
observed_total: 1,
items: [{ location: { kind: "unresolved", reported_url: scriptUrl } }],
},
execution_contexts: [{ name: "main" }],
});
await provider.close();
expect(result.error.cleanupIncomplete).toBe(false);
} finally {
handleState.failCloseCount = 0;
await provider.close();
await inspector.close();
}
});
it("retains a failed ASAR close through denial and retries before reopening", async () => {
const location = await asarPage();
const missing = pathToFileURL(
+10 -28
View File
@@ -410,13 +410,11 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort {
});
outcome = ok(javascriptRuntimeObservationSchema.parse(result));
} catch (cause: unknown) {
let error = providerError(cause, "observe_javascript_runtime");
const error = providerError(cause, "observe_javascript_runtime");
if (
state !== undefined &&
discovery !== undefined &&
target !== undefined &&
(cause instanceof BrowserObservationError ||
cause instanceof AnalysisCancelledError)
target !== undefined
) {
try {
const partialObservation = await finalizeInspectorCapture({
@@ -427,35 +425,19 @@ export class V8InspectorProvider implements JavaScriptRuntimeObservationPort {
state,
locationMode: "reported",
});
if (cause instanceof AnalysisCancelledError) {
cause.retainPartialObservation(partialObservation);
error = cause;
} else {
error = new BrowserObservationError(
"observe_javascript_runtime",
cause.reason,
{
cause: error,
...(error.userMessage === undefined
? {}
: { detail: error.userMessage }),
partialObservation,
},
);
}
error.retainPartialObservation(partialObservation);
} catch (projectionFailure: unknown) {
if (!(cause instanceof AnalysisCancelledError))
error = new ProviderAdapterError(
error.retainSecondaryFailure(
new ProviderAdapterError(
V8_INSPECTOR_PROVIDER_IDENTITY.id,
"observe_javascript_runtime",
{
cause: new AggregateError(
[error, projectionFailure],
"Inspector failure and partial-observation projection both failed",
),
userMessage: `${error.message} Partial observations could not be projected.`,
cause: projectionFailure,
userMessage:
"Inspector partial observations could not be projected.",
},
);
),
);
}
}
outcome = err(error);