fix(browser): unify worker target admission across capture workflows

This commit is contained in:
morluto committed 2026-10-26 23:44:08 +08:00
1 parent cadd5b75b3
commit a5ad164283
6 files changed
+253 -107

No files matched your search

+16 -33
View File
@@ -5,13 +5,16 @@ import type {
} from "../domain/javascript/electronObservation.js"; } from "../domain/javascript/electronObservation.js";
import type { CdpConnection } from "./CdpConnection.js"; import type { CdpConnection } from "./CdpConnection.js";
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js"; import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
import { recordValue, cdpStringValue } from "./CdpCaptureValues.js"; import { recordValue } from "./CdpCaptureValues.js";
import type { CdpEndpointTarget } from "./CdpEndpoint.js"; import type { CdpEndpointTarget } from "./CdpEndpoint.js";
import { optionalCdpCommand } from "./CdpOptionalCommand.js"; import { optionalCdpCommand } from "./CdpOptionalCommand.js";
import { authorizedElectronFile } from "./ElectronFileScope.js"; import { authorizedElectronFile } from "./ElectronFileScope.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js"; import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import { workerTargetOwnership } from "./CdpWorkerTargetOwnership.js"; import {
parseCdpWorkerTarget,
workerTargetOwnership,
} from "./CdpWorkerTarget.js";
/** Inventory local worker targets without attaching to them. */ /** Inventory local worker targets without attaching to them. */
export const captureElectronWorkers = async (input: { export const captureElectronWorkers = async (input: {
@@ -47,33 +50,27 @@ export const captureElectronWorkers = async (input: {
} }
for (const entry of targetInfos) { for (const entry of targetInfos) {
throwIfElectronWorkerCaptureCancelled(input.signal); throwIfElectronWorkerCaptureCancelled(input.signal);
const target = recordValue(entry); const parsed = parseCdpWorkerTarget(entry);
if (target === undefined) { if (!parsed.ok) {
input.completeness.exclude("workers", "invalid_protocol_value"); input.completeness.exclude("workers", "invalid_protocol_value");
continue; continue;
} }
const type = cdpStringValue(target.type) ?? ""; const target = parsed.value;
if (!type.includes("worker")) continue; if (target.kind === "non_worker") continue;
const ownership = workerTargetOwnership( const ownership = workerTargetOwnership(
target, target,
input.target.id, input.target.id,
input.frameIds, input.frameIds,
); );
if (!ownership.ok) { if (ownership !== "selected") {
input.completeness.exclude("workers", "invalid_protocol_value");
continue;
}
if (ownership.value.scope !== "selected") {
input.completeness.exclude( input.completeness.exclude(
"workers", "workers",
ownership.value.scope === "unknown" ownership === "unknown" ? "unattributed_target" : "out_of_target_scope",
? "unattributed_target"
: "out_of_target_scope",
); );
continue; continue;
} }
const path = await authorizedElectronFile( const path = await authorizedElectronFile(
cdpStringValue(target.url) ?? "", target.url,
input.signal, input.signal,
"inspect_electron_page", "inspect_electron_page",
input.asarHeaderAdmission, input.asarHeaderAdmission,
@@ -82,22 +79,13 @@ export const captureElectronWorkers = async (input: {
input.completeness.exclude("workers", "out_of_target_scope"); input.completeness.exclude("workers", "out_of_target_scope");
continue; continue;
} }
const targetId = boundedTargetField(target.targetId);
if (targetId === null) {
input.completeness.exclude("workers", "invalid_protocol_value");
continue;
}
if (typeof target.attached !== "boolean") {
input.completeness.exclude("workers", "invalid_protocol_value");
continue;
}
workers.push({ workers.push({
target_id: targetId, target_id: target.targetId,
type, type: target.type,
file_path: path, file_path: path,
attached: target.attached, attached: target.attached,
opener_target_id: ownership.value.openerTargetId, opener_target_id: target.openerTargetId,
parent_frame_id: ownership.value.parentFrameId, parent_frame_id: target.parentFrameId,
}); });
} }
return workers.sort((left, right) => return workers.sort((left, right) =>
@@ -111,8 +99,3 @@ const throwIfElectronWorkerCaptureCancelled = (signal?: AbortSignal): void => {
cause: signal.reason, cause: signal.reason,
}); });
}; };
const boundedTargetField = (value: unknown): string | null => {
const field = cdpStringValue(value);
return field === undefined || field === "" ? null : field;
};
+73 -2
View File
@@ -9,7 +9,7 @@ import type { CaptureContext } from "./CdpPageCapture.js";
const origin = "http://127.0.0.1:43127"; const origin = "http://127.0.0.1:43127";
const capture = async (inventory: unknown) => { const capture = async (inventory: unknown, frameIds = new Set<string>()) => {
const input = inspectWebPageInputSchema.parse({ const input = inspectWebPageInputSchema.parse({
cdp_endpoint: origin, cdp_endpoint: origin,
allowed_origins: [origin], allowed_origins: [origin],
@@ -38,7 +38,7 @@ const capture = async (inventory: unknown) => {
limitations: [], limitations: [],
events, events,
}, },
new Set(), frameIds,
); );
return { workers, completeness: events.completeness.snapshot() }; return { workers, completeness: events.completeness.snapshot() };
}; };
@@ -199,3 +199,74 @@ it.each([{ openerId: "" }, { parentFrameId: null }])(
}); });
}, },
); );
it.each([
{ type: undefined },
{ type: 7 },
{ type: "" },
{ targetId: "" },
{ url: undefined },
{ attached: null },
{ openerId: null },
{ parentFrameId: "" },
])(
"reports malformed worker facts before ownership and URL policy: %j",
async (override) => {
const result = await capture({
targetInfos: [
{
targetId: "worker",
type: "worker",
url: "https://outside.example/worker.js",
attached: false,
openerId: "foreign-page",
...override,
},
],
});
expect(result.workers).toEqual([]);
expect(result.completeness.excluded).toEqual([
{ section: "workers", reason: "invalid_protocol_value", count: 1 },
]);
},
);
it("uses parsed frame relationships without treating valid non-workers as malformed", async () => {
const result = await capture(
{
targetInfos: [
{ type: "page" },
{
targetId: "worker",
type: "shared_worker",
url: `${origin}/worker.js`,
attached: false,
openerId: "foreign-page",
parentFrameId: "selected-frame",
},
{
targetId: "foreign",
type: "worker",
url: `${origin}/foreign.js`,
attached: true,
parentFrameId: "foreign-frame",
},
],
},
new Set(["selected-frame"]),
);
expect(result.workers).toEqual([
{
target_id: "worker",
type: "shared_worker",
url: `${origin}/worker.js`,
origin,
attached: false,
opener_target_id: "foreign-page",
parent_frame_id: "selected-frame",
},
]);
expect(result.completeness.excluded).toEqual([
{ section: "workers", reason: "out_of_target_scope", count: 1 },
]);
});
+15 -34
View File
@@ -4,11 +4,13 @@ import {
admitRecords, admitRecords,
allowedSanitizedUrl, allowedSanitizedUrl,
recordValue, recordValue,
cdpStringValue,
} from "./CdpCaptureValues.js"; } from "./CdpCaptureValues.js";
import type { CaptureContext } from "./CdpPageCapture.js"; import type { CaptureContext } from "./CdpPageCapture.js";
import { optionalCdpCommand } from "./CdpOptionalCommand.js"; import { optionalCdpCommand } from "./CdpOptionalCommand.js";
import { workerTargetOwnership } from "./CdpWorkerTargetOwnership.js"; import {
parseCdpWorkerTarget,
workerTargetOwnership,
} from "./CdpWorkerTarget.js";
import { exclusionReasonForUrl } from "./CdpCaptureEventHelpers.js"; import { exclusionReasonForUrl } from "./CdpCaptureEventHelpers.js";
interface CaptureWorkersInput { interface CaptureWorkersInput {
@@ -42,59 +44,38 @@ export const captureWorkers = async (
} }
let malformedEntries = 0; let malformedEntries = 0;
for (const entry of targets ?? []) { for (const entry of targets ?? []) {
const target = recordValue(entry); const parsed = parseCdpWorkerTarget(entry);
if (target === undefined) { if (!parsed.ok) {
malformedEntries += 1;
continue;
}
const type = cdpStringValue(target.type);
if (type === undefined || type.length === 0) {
malformedEntries += 1;
continue;
}
if (!type.includes("worker")) continue;
const targetId = cdpStringValue(target.targetId);
if (
targetId === undefined ||
targetId.length === 0 ||
typeof target.attached !== "boolean" ||
typeof target.url !== "string"
) {
malformedEntries += 1; malformedEntries += 1;
continue; continue;
} }
const target = parsed.value;
if (target.kind === "non_worker") continue;
const ownership = workerTargetOwnership( const ownership = workerTargetOwnership(
target, target,
context.target.id, context.target.id,
frameIds, frameIds,
); );
if (!ownership.ok) { if (ownership !== "selected") {
malformedEntries += 1;
continue;
}
if (ownership.value.scope !== "selected") {
completeness.exclude( completeness.exclude(
"workers", "workers",
ownership.value.scope === "unknown" ownership === "unknown" ? "unattributed_target" : "out_of_target_scope",
? "unattributed_target"
: "out_of_target_scope",
); );
continue; continue;
} }
const url = allowedSanitizedUrl(target.url, allowedOrigins); const url = allowedSanitizedUrl(target.url, allowedOrigins);
if (url === undefined) { if (url === undefined) {
const rawUrl = cdpStringValue(target.url); completeness.exclude("workers", exclusionReasonForUrl(target.url));
completeness.exclude("workers", exclusionReasonForUrl(rawUrl));
continue; continue;
} }
items.push({ items.push({
target_id: targetId, target_id: target.targetId,
type, type: target.type,
url: url.url, url: url.url,
origin: url.origin, origin: url.origin,
attached: target.attached, attached: target.attached,
opener_target_id: ownership.value.openerTargetId, opener_target_id: target.openerTargetId,
parent_frame_id: ownership.value.parentFrameId, parent_frame_id: target.parentFrameId,
}); });
} }
if (malformedEntries > 0) if (malformedEntries > 0)
+70
View File
@@ -0,0 +1,70 @@
import { err, ok, type Result } from "../domain/result.js";
import { cdpStringValue, recordValue } from "./CdpCaptureValues.js";
export interface CdpWorkerRelationships {
readonly openerTargetId: string | null;
readonly parentFrameId: string | null;
}
export interface CdpWorkerTarget extends CdpWorkerRelationships {
readonly kind: "worker";
readonly targetId: string;
readonly type: string;
readonly url: string;
readonly attached: boolean;
}
/** Admit protocol facts before either consumer applies target or URL policy. */
export const parseCdpWorkerTarget = (
input: unknown,
): Result<
CdpWorkerTarget | { readonly kind: "non_worker" },
{ readonly _tag: "InvalidWorkerTarget" }
> => {
const target = recordValue(input);
const type = cdpStringValue(target?.type);
if (target === undefined || type === undefined || type === "")
return err({ _tag: "InvalidWorkerTarget" });
if (!type.includes("worker")) return ok({ kind: "non_worker" });
const targetId = cdpStringValue(target.targetId);
const url = cdpStringValue(target.url);
const openerTargetId = cdpStringValue(target.openerId);
const parentFrameId = cdpStringValue(target.parentFrameId);
if (
targetId === undefined ||
targetId === "" ||
url === undefined ||
typeof target.attached !== "boolean" ||
(target.openerId !== undefined &&
(openerTargetId === undefined || openerTargetId === "")) ||
(target.parentFrameId !== undefined &&
(parentFrameId === undefined || parentFrameId === ""))
)
return err({ _tag: "InvalidWorkerTarget" });
return ok({
kind: "worker",
targetId,
type,
url,
attached: target.attached,
openerTargetId: openerTargetId ?? null,
parentFrameId: parentFrameId ?? null,
});
};
/** Optional CDP relationships establish ownership only when supplied. */
export const workerTargetOwnership = (
relationships: CdpWorkerRelationships,
selectedTargetId: string,
frameIds: ReadonlySet<string>,
): "selected" | "foreign" | "unknown" => {
const { openerTargetId, parentFrameId } = relationships;
if (
openerTargetId === selectedTargetId ||
(parentFrameId !== null && frameIds.has(parentFrameId))
)
return "selected";
return openerTargetId === null && parentFrameId === null
? "unknown"
: "foreign";
};
-38
View File
@@ -1,38 +0,0 @@
import { err, ok, type Result } from "../domain/result.js";
import { cdpStringValue } from "./CdpCaptureValues.js";
/** Optional CDP relationships establish ownership only when supplied. */
export const workerTargetOwnership = (
target: Readonly<Record<string, unknown>>,
selectedTargetId: string,
frameIds: ReadonlySet<string>,
): Result<
{
readonly scope: "selected" | "foreign" | "unknown";
readonly openerTargetId: string | null;
readonly parentFrameId: string | null;
},
{ readonly _tag: "InvalidWorkerRelationship" }
> => {
const openerTargetId = cdpStringValue(target.openerId);
const parentFrameId = cdpStringValue(target.parentFrameId);
if (
(target.openerId !== undefined &&
(openerTargetId === undefined || openerTargetId === "")) ||
(target.parentFrameId !== undefined &&
(parentFrameId === undefined || parentFrameId === ""))
)
return err({ _tag: "InvalidWorkerRelationship" });
const selected =
openerTargetId === selectedTargetId ||
(parentFrameId !== undefined && frameIds.has(parentFrameId));
return ok({
scope: selected
? "selected"
: openerTargetId === undefined && parentFrameId === undefined
? "unknown"
: "foreign",
openerTargetId: openerTargetId ?? null,
parentFrameId: parentFrameId ?? null,
});
};
@@ -849,6 +849,85 @@ describe("CdpElectronProvider malformed inventory replies", () => {
); );
}); });
it.each([
["missing type", { type: undefined }],
["non-string type", { type: 7 }],
["empty type", { type: "" }],
["missing target identity", { targetId: undefined }],
["non-string target identity", { targetId: 7 }],
["empty target identity", { targetId: "" }],
["missing URL", { url: undefined }],
["non-string URL", { url: 7 }],
["missing attached state", { attached: undefined }],
["non-boolean attached state", { attached: "false" }],
["non-string opener relationship", { openerId: 7 }],
["empty opener relationship", { openerId: "" }],
["non-string frame relationship", { parentFrameId: 7 }],
["empty frame relationship", { parentFrameId: "" }],
] as const)(
"retains valid workers while reporting %s before target policy",
async (_kind, malformed) => {
const inspected = await inspectWith("Target.getTargets", (fileUrl) => ({
targetInfos: [
{
targetId: "selected-worker",
type: "worker",
url: fileUrl,
attached: false,
openerId: "electron-page",
},
{
targetId: "malformed-worker",
type: "worker",
url: fileUrl,
attached: false,
openerId: "foreign-page",
...malformed,
},
{
targetId: "foreign-worker",
type: "shared_worker",
url: fileUrl,
attached: true,
openerId: "foreign-page",
},
{
targetId: "unattributed-worker",
type: "service_worker",
url: fileUrl,
attached: false,
},
// Non-worker rows do not need worker identity or relationship fields.
{ type: "page", targetId: 7, openerId: null },
],
}));
expect(inspected.workers).toEqual([
expect.objectContaining({
target_id: "selected-worker",
type: "worker",
attached: false,
opener_target_id: "electron-page",
parent_frame_id: null,
}),
]);
const workerExclusions = inspected.completeness.excluded.filter(
({ section }) => section === "workers",
);
expect(workerExclusions).toHaveLength(3);
expect(workerExclusions).toEqual(
expect.arrayContaining([
{ section: "workers", reason: "invalid_protocol_value", count: 1 },
{ section: "workers", reason: "out_of_target_scope", count: 1 },
{ section: "workers", reason: "unattributed_target", count: 1 },
]),
);
expect(inspected.completeness.unavailable_sections).toContain("workers");
expect(inspected.completeness.policy_filtered_sections).toContain(
"workers",
);
},
);
it.each([ it.each([
["dom", "DOMSnapshot.captureSnapshot", () => ({ strings: [] })], ["dom", "DOMSnapshot.captureSnapshot", () => ({ strings: [] })],
[ [