Files
reverse-engineering-anything/src/browser/CdpPageCaptureScripts.ts
T
morluto b5e98915c6 fix(tools)!: simplify direct invocation and harden boundary contracts (#555)
Remove REA permission grants, scope ceilings, approval confirmations, and inert settings. Preserve selected local evidence, accept caller-chosen commands and paths, and report actual runtime failures.

Harden provider results, schema serialization, snapshot evidence binding, archive paths, source maps, native screenshots, managed EH parsing, and cancellation cleanup. Regenerate tool metadata and document direct invocation.

BREAKING CHANGE: Removed permission configuration and policy commands, approval fields, filesystem scope inputs, and fixed confirmation options. Process capture inherits the host environment and accepts command names; callers must refresh tool schemas and migrate removed fields.
2026-10-05 20:16:44 +08:00

167 lines
5.0 KiB
TypeScript

import type { WebPageInspection } from "../domain/browserObservation.js";
import {
reconcileCapturedWebScript,
stableWebScriptKey,
} from "../domain/webInventory.js";
import type { CapturedResource } from "./CdpCaptureDocuments.js";
import type { CdpCaptureEvents } from "./CdpCaptureEvents.js";
import type { CapturedScript } from "./CdpCaptureEventTypes.js";
import {
requiredRecord,
sourceExcluded,
sourceResult,
stringValue,
} from "./CdpCaptureValues.js";
import type { CaptureContext } from "./CdpPageCapture.js";
import type { WebSourceMapRequest } from "./WebSourceMapFetcher.js";
export interface CapturedScripts {
readonly inventory: WebPageInspection["scripts"];
readonly sourceMapRequests: readonly WebSourceMapRequest[];
}
export interface CaptureScriptsOptions {
readonly context: CaptureContext;
readonly events: CdpCaptureEvents;
readonly rawResources: readonly CapturedResource[];
readonly resources: WebPageInspection["resources"];
readonly frameIds: ReadonlySet<string>;
}
interface ScriptDraftState {
readonly context: CaptureContext;
readonly events: CdpCaptureEvents;
readonly frameIds: ReadonlySet<string>;
readonly rawResources: readonly CapturedResource[];
readonly resources: WebPageInspection["resources"];
}
type ScriptDraftItem = Omit<
WebPageInspection["scripts"]["items"][number],
"script_key"
>;
export const captureScripts = async (
options: CaptureScriptsOptions,
): Promise<CapturedScripts> => {
const { context, events, rawResources, resources, frameIds } = options;
const state: ScriptDraftState = {
context,
events,
frameIds,
rawResources,
resources,
};
const drafts: {
readonly item: ScriptDraftItem;
readonly sourceMapRawUrl: string | null;
}[] = [];
for (const script of events.scripts.values())
drafts.push(await buildScriptDraft(script, state));
if (!context.input.include_script_sources)
events.completeness.exclude(
"script_sources",
"not_approved",
events.scripts.size,
);
return keyAndBuildInventory(drafts, events.scripts.size);
};
const buildScriptDraft = async (
script: CapturedScript,
state: ScriptDraftState,
): Promise<{
readonly item: ScriptDraftItem;
readonly sourceMapRawUrl: string | null;
}> => {
let source: WebPageInspection["scripts"]["items"][number]["source"] = state
.context.input.include_script_sources
? await captureScriptSource(script, state)
: sourceExcluded("source capture was not selected");
const inventoryScript = {
frame_id: state.events.frameForScript(script, state.frameIds),
url: script.url,
cdp_hash: script.hash,
length: script.length,
is_module: script.isModule,
language: script.language,
source_map_url: script.sourceMapUrl,
};
return {
item: {
...inventoryScript,
origin: script.origin,
resource_reconciliation: reconcileCapturedWebScript(
{ ...inventoryScript, rawUrl: script.rawUrl },
state.rawResources,
state.resources,
),
source,
},
sourceMapRawUrl: script.sourceMapRawUrl,
};
};
const captureScriptSource = async (
script: CapturedScript,
state: ScriptDraftState,
): Promise<WebPageInspection["scripts"]["items"][number]["source"]> => {
const { context } = state;
const result = requiredRecord(
await context.connection.send(
"Debugger.getScriptSource",
{ scriptId: script.scriptId },
context.sessionId,
context.signal,
),
);
const content = stringValue(result.scriptSource) ?? "";
return sourceResult(content);
};
const keyAndBuildInventory = (
drafts: readonly {
readonly item: ScriptDraftItem;
readonly sourceMapRawUrl: string | null;
}[],
scriptCount: number,
): CapturedScripts => {
const keyed = drafts.map((draft) => ({
...draft,
base: stableWebScriptKey(draft.item),
}));
keyed.sort(
(left, right) =>
left.base.localeCompare(right.base) ||
sourceDigest(left.item.source).localeCompare(
sourceDigest(right.item.source),
) ||
(left.item.frame_id ?? "").localeCompare(right.item.frame_id ?? ""),
);
const totals = new Map<string, number>();
for (const { base } of keyed) totals.set(base, (totals.get(base) ?? 0) + 1);
const seen = new Map<string, number>();
const sourceMapRequests: WebSourceMapRequest[] = [];
const items = keyed.map(({ base, item, sourceMapRawUrl }) => {
const occurrence = (seen.get(base) ?? 0) + 1;
seen.set(base, occurrence);
const scriptKey =
totals.get(base) === 1 ? base : `${base}_${String(occurrence)}`;
if (item.source_map_url !== null && sourceMapRawUrl !== null)
sourceMapRequests.push({
scriptKey,
declaredUrl: item.source_map_url,
fetchUrl: sourceMapRawUrl,
});
return { script_key: scriptKey, ...item };
});
return {
inventory: { total: scriptCount, items },
sourceMapRequests,
};
};
const sourceDigest = (
source: WebPageInspection["scripts"]["items"][number]["source"],
): string => (source.included ? source.artifact.sha256 : source.reason);