mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
refactor(domain): extract source-map envelope walk and disambiguate stringValue (#828)
Move the indexed sections walk shared by the browser fetcher and the artifact reader into domain/sourceMapEnvelope with the fetchers stricter offset validation as an option; both validators keep their leaf strictness. Rename the browser CDP stringValue to cdpStringValue across its 18 files, fold the fingerprint duplicate into the static helper, and rename the version-keys shadow to nonEmptyString.
This commit is contained in:
@@ -4,6 +4,7 @@ import { analyzeParsedJavaScriptStaticSource } from "../domain/javascriptStaticA
|
||||
import { analyzeParsedJavaScriptSemantics } from "../domain/javascriptSemanticAnalysis.js";
|
||||
import { parseJavaScriptSource } from "../domain/javascriptSourceParser.js";
|
||||
import { hasValidSourceMapContents } from "../domain/sourceMapContents.js";
|
||||
import { flattenSourceMapLeaves } from "../domain/sourceMapEnvelope.js";
|
||||
import type {
|
||||
JavaScriptSourceRange,
|
||||
JavaScriptSourcePoint,
|
||||
@@ -300,20 +301,8 @@ const unavailableSourceMap = (
|
||||
const flattenSourceMaps = (
|
||||
root: unknown,
|
||||
): Readonly<Record<string, unknown>>[] | undefined => {
|
||||
if (!isRecord(root) || root.version !== 3) return undefined;
|
||||
const maps: Readonly<Record<string, unknown>>[] = [];
|
||||
const pending = [root];
|
||||
while (pending.length > 0) {
|
||||
const map = pending.pop();
|
||||
if (map === undefined || map.version !== 3) return undefined;
|
||||
if (Array.isArray(map.sections)) {
|
||||
for (const section of map.sections) {
|
||||
if (!isRecord(section) || !isRecord(section.map)) return undefined;
|
||||
pending.push(section.map);
|
||||
}
|
||||
} else maps.push(map);
|
||||
}
|
||||
return maps;
|
||||
const leaves = flattenSourceMapLeaves(root);
|
||||
return leaves === undefined ? undefined : [...leaves];
|
||||
};
|
||||
|
||||
const invalidSourceMap = (
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
recordValue,
|
||||
recordsValue,
|
||||
requiredRecord,
|
||||
stringValue,
|
||||
cdpStringValue,
|
||||
type UnknownRecord,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import { exclusionReasonForUrl } from "./CdpCaptureEventHelpers.js";
|
||||
@@ -38,7 +38,7 @@ export const captureFrames = (
|
||||
completeness?.exclude("frames", "invalid_protocol_value"),
|
||||
)) {
|
||||
const frame = recordValue(tree.frame);
|
||||
const frameId = stringValue(frame?.id);
|
||||
const frameId = cdpStringValue(frame?.id);
|
||||
const sanitized = allowedSanitizedUrl(frame?.url, allowedOrigins);
|
||||
if (frame === undefined || frameId === undefined) {
|
||||
completeness?.exclude("frames", "invalid_protocol_value");
|
||||
@@ -47,14 +47,14 @@ export const captureFrames = (
|
||||
if (sanitized === undefined || sanitized.origin === null) {
|
||||
completeness?.exclude(
|
||||
"frames",
|
||||
exclusionReasonForUrl(stringValue(frame.url)),
|
||||
exclusionReasonForUrl(cdpStringValue(frame.url)),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (maximum !== undefined && items.length >= maximum) continue;
|
||||
items.push({
|
||||
frame_id: frameId,
|
||||
parent_frame_id: stringValue(frame.parentId) ?? null,
|
||||
parent_frame_id: cdpStringValue(frame.parentId) ?? null,
|
||||
url: sanitized.url,
|
||||
origin: sanitized.origin,
|
||||
});
|
||||
@@ -64,7 +64,7 @@ export const captureFrames = (
|
||||
|
||||
/** Read the current main-frame URL from an untrusted Page.getFrameTree result. */
|
||||
export const mainFrameUrl = (result: unknown): string | undefined =>
|
||||
stringValue(
|
||||
cdpStringValue(
|
||||
recordValue(recordValue(requiredRecord(result).frameTree)?.frame)?.url,
|
||||
);
|
||||
|
||||
@@ -90,17 +90,17 @@ export const captureResources = (
|
||||
if (url === undefined || url.origin === null) {
|
||||
completeness?.exclude(
|
||||
"resources",
|
||||
exclusionReasonForUrl(stringValue(resource.url)),
|
||||
exclusionReasonForUrl(cdpStringValue(resource.url)),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const contentSize = numberValue(resource.contentSize);
|
||||
items.push({
|
||||
rawUrl: stringValue(resource.url) ?? "",
|
||||
rawUrl: cdpStringValue(resource.url) ?? "",
|
||||
url: url.url,
|
||||
origin: url.origin,
|
||||
type: stringValue(resource.type) ?? "Other",
|
||||
mime_type: stringValue(resource.mimeType) ?? "",
|
||||
type: cdpStringValue(resource.type) ?? "Other",
|
||||
mime_type: cdpStringValue(resource.mimeType) ?? "",
|
||||
content_size:
|
||||
contentSize === undefined ? null : Math.max(0, contentSize),
|
||||
});
|
||||
@@ -149,7 +149,7 @@ export const captureDom = (
|
||||
} => {
|
||||
const root = requiredRecord(result);
|
||||
const strings = Array.isArray(root.strings)
|
||||
? root.strings.map((value) => stringValue(value) ?? "")
|
||||
? root.strings.map((value) => cdpStringValue(value) ?? "")
|
||||
: [];
|
||||
const nodes: WebPageInspection["dom"]["nodes"] = [];
|
||||
const urls: WebPageInspection["metadata"]["dom_urls"] = [];
|
||||
@@ -240,14 +240,14 @@ export const captureAccessibility = (
|
||||
);
|
||||
const nodeIds = new Set(
|
||||
all.flatMap((node) => {
|
||||
const nodeId = stringValue(node.nodeId);
|
||||
const nodeId = cdpStringValue(node.nodeId);
|
||||
return nodeId === undefined ? [] : [nodeId];
|
||||
}),
|
||||
);
|
||||
let excludedFields = 0;
|
||||
const nodes = all.map((node) => {
|
||||
const captureText = (value: unknown): string | null => {
|
||||
const raw = stringValue(recordValue(value)?.value);
|
||||
const raw = cdpStringValue(recordValue(value)?.value);
|
||||
if (raw === undefined) return null;
|
||||
if (!options.includeText) {
|
||||
excludedFields += 1;
|
||||
@@ -256,8 +256,8 @@ export const captureAccessibility = (
|
||||
return raw;
|
||||
};
|
||||
return {
|
||||
node_id: stringValue(node.nodeId) ?? "",
|
||||
parent_id: stringValue(node.parentId) ?? null,
|
||||
node_id: cdpStringValue(node.nodeId) ?? "",
|
||||
parent_id: cdpStringValue(node.parentId) ?? null,
|
||||
role: axText(node.role),
|
||||
name: captureText(node.name),
|
||||
description: captureText(node.description),
|
||||
@@ -272,7 +272,7 @@ export const captureAccessibility = (
|
||||
(node) =>
|
||||
Array.isArray(node.childIds) &&
|
||||
node.childIds.some((childId) => {
|
||||
const id = stringValue(childId);
|
||||
const id = cdpStringValue(childId);
|
||||
return id !== undefined && !nodeIds.has(id);
|
||||
}),
|
||||
),
|
||||
@@ -326,7 +326,7 @@ const accessibilityStates = (
|
||||
): WebPageInspection["accessibility"]["nodes"][number]["states"] => {
|
||||
const states = new Map<string, boolean | number | string>();
|
||||
for (const property of recordsValue(value)) {
|
||||
const name = stringValue(property.name);
|
||||
const name = cdpStringValue(property.name);
|
||||
if (name === undefined || !ACCESSIBILITY_STATE_NAMES.has(name)) continue;
|
||||
const raw = recordValue(property.value)?.value;
|
||||
const state =
|
||||
@@ -345,7 +345,7 @@ const accessibilityStates = (
|
||||
};
|
||||
|
||||
const axText = (value: unknown): string | null =>
|
||||
stringValue(recordValue(value)?.value) ?? null;
|
||||
cdpStringValue(recordValue(value)?.value) ?? null;
|
||||
|
||||
const indexedString = (strings: readonly string[], index: unknown): string => {
|
||||
const integer = numberValue(index);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { inferJsonShape, type JsonShape } from "../domain/jsonShape.js";
|
||||
import {
|
||||
recordValue,
|
||||
stringValue,
|
||||
cdpStringValue,
|
||||
type UnknownRecord,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import { decodeBase64, isJsonContentType } from "./CdpCaptureEventHelpers.js";
|
||||
@@ -14,7 +14,7 @@ export const ingestResponseBodyShape = (
|
||||
value: unknown,
|
||||
): void => {
|
||||
const result = recordValue(value);
|
||||
const body = stringValue(result?.body);
|
||||
const body = cdpStringValue(result?.body);
|
||||
if (body === undefined) {
|
||||
invalidResponseBodyShape(state, requestId);
|
||||
return;
|
||||
@@ -37,7 +37,7 @@ export const requestBodyShape = (
|
||||
): NetworkState["body_shapes"] => {
|
||||
if (!state.input.include_json_body_shapes)
|
||||
return { status: "not_approved", request: null, response: null };
|
||||
const body = stringValue(request.postData);
|
||||
const body = cdpStringValue(request.postData);
|
||||
if (!isJsonContentType(recordValue(request.headers)) || body === undefined)
|
||||
return { status: "unavailable", request: null, response: null };
|
||||
const inferred = inferBodyShape(body);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
numberValue,
|
||||
recordValue,
|
||||
recordsValue,
|
||||
stringValue,
|
||||
cdpStringValue,
|
||||
type UnknownRecord,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import { requestBodyShape } from "./CdpCaptureEventBodyShapes.js";
|
||||
@@ -30,7 +30,7 @@ export const handleExecutionContextCreated = (
|
||||
): void => {
|
||||
const context = recordValue(params.context);
|
||||
const identifier = numberValue(context?.id);
|
||||
const frameId = stringValue(recordValue(context?.auxData)?.frameId);
|
||||
const frameId = cdpStringValue(recordValue(context?.auxData)?.frameId);
|
||||
if (
|
||||
identifier === undefined ||
|
||||
!Number.isSafeInteger(identifier) ||
|
||||
@@ -53,8 +53,8 @@ export const handleScriptParsed = (
|
||||
state: CdpCaptureEventsState,
|
||||
params: UnknownRecord,
|
||||
): void => {
|
||||
const scriptId = stringValue(params.scriptId);
|
||||
const rawUrl = stringValue(params.url) ?? "";
|
||||
const scriptId = cdpStringValue(params.scriptId);
|
||||
const rawUrl = cdpStringValue(params.url) ?? "";
|
||||
const sanitized = allowedSanitizedUrl(rawUrl, state.allowedOrigins);
|
||||
if (scriptId === undefined) {
|
||||
state.completeness.exclude("scripts", "invalid_protocol_value");
|
||||
@@ -70,7 +70,7 @@ export const handleScriptParsed = (
|
||||
rawUrl,
|
||||
url: sanitized.url,
|
||||
origin: sanitized.origin,
|
||||
hash: stringValue(params.hash) ?? "",
|
||||
hash: cdpStringValue(params.hash) ?? "",
|
||||
length: Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
@@ -79,7 +79,7 @@ export const handleScriptParsed = (
|
||||
),
|
||||
),
|
||||
isModule: params.isModule === true,
|
||||
language: stringValue(params.scriptLanguage) ?? null,
|
||||
language: cdpStringValue(params.scriptLanguage) ?? null,
|
||||
sourceMapUrl: sourceMap.sanitized,
|
||||
sourceMapRawUrl: sourceMap.raw,
|
||||
executionContextKey: executionContextKey(params.executionContextId),
|
||||
@@ -92,7 +92,7 @@ const sourceMapForScript = (
|
||||
scriptUrl: string,
|
||||
state: CdpCaptureEventsState,
|
||||
): { readonly sanitized: string | null; readonly raw: string | null } => {
|
||||
const declaredUrl = stringValue(value);
|
||||
const declaredUrl = cdpStringValue(value);
|
||||
if (declaredUrl === undefined || declaredUrl === "")
|
||||
return { sanitized: null, raw: null };
|
||||
let rawUrl: string;
|
||||
@@ -114,7 +114,7 @@ export const handleRequestWillBeSent = (
|
||||
state: CdpCaptureEventsState,
|
||||
params: UnknownRecord,
|
||||
): void => {
|
||||
const requestId = stringValue(params.requestId);
|
||||
const requestId = cdpStringValue(params.requestId);
|
||||
const request = recordValue(params.request);
|
||||
if (requestId === undefined) {
|
||||
state.completeness.exclude("network_requests", "invalid_protocol_value");
|
||||
@@ -129,14 +129,14 @@ export const handleRequestWillBeSent = (
|
||||
if (sanitized === undefined) {
|
||||
state.completeness.exclude(
|
||||
"network_requests",
|
||||
exclusionReasonForUrl(stringValue(request.url)),
|
||||
exclusionReasonForUrl(cdpStringValue(request.url)),
|
||||
);
|
||||
state.network.delete(requestId);
|
||||
return;
|
||||
}
|
||||
const initiator = recordValue(params.initiator);
|
||||
const initiatorFrame = initiatorLocation(initiator);
|
||||
const rawInitiatorUrl = stringValue(initiatorFrame?.url);
|
||||
const rawInitiatorUrl = cdpStringValue(initiatorFrame?.url);
|
||||
const initiatorUrl = allowedSanitizedUrl(
|
||||
rawInitiatorUrl,
|
||||
state.allowedOrigins,
|
||||
@@ -150,13 +150,13 @@ export const handleRequestWillBeSent = (
|
||||
request_id: requestId,
|
||||
url: sanitized.url,
|
||||
origin: sanitized.origin ?? "",
|
||||
method: stringValue(request.method) ?? "GET",
|
||||
resource_type: stringValue(params.type) ?? null,
|
||||
method: cdpStringValue(request.method) ?? "GET",
|
||||
resource_type: cdpStringValue(params.type) ?? null,
|
||||
status: null,
|
||||
mime_type: null,
|
||||
encoded_data_length: null,
|
||||
initiator: {
|
||||
type: stringValue(initiator?.type) ?? "other",
|
||||
type: cdpStringValue(initiator?.type) ?? "other",
|
||||
url: initiatorUrl?.url ?? null,
|
||||
line: integerOrNull(initiatorFrame?.lineNumber),
|
||||
column: integerOrNull(initiatorFrame?.columnNumber),
|
||||
@@ -169,7 +169,7 @@ export const handleResponseReceived = (
|
||||
state: CdpCaptureEventsState,
|
||||
params: UnknownRecord,
|
||||
): void => {
|
||||
const requestId = stringValue(params.requestId);
|
||||
const requestId = cdpStringValue(params.requestId);
|
||||
if (requestId === undefined) {
|
||||
state.completeness.exclude("network_requests", "invalid_protocol_value");
|
||||
return;
|
||||
@@ -183,7 +183,7 @@ export const handleResponseReceived = (
|
||||
"network_requests",
|
||||
response === undefined
|
||||
? "invalid_protocol_value"
|
||||
: exclusionReasonForUrl(stringValue(response.url)),
|
||||
: exclusionReasonForUrl(cdpStringValue(response.url)),
|
||||
);
|
||||
state.network.delete(requestId);
|
||||
return;
|
||||
@@ -191,7 +191,7 @@ export const handleResponseReceived = (
|
||||
state.network.set(requestId, {
|
||||
...current,
|
||||
status: numberValue(response.status) ?? null,
|
||||
mime_type: stringValue(response.mimeType) ?? null,
|
||||
mime_type: cdpStringValue(response.mimeType) ?? null,
|
||||
});
|
||||
const metadata = safeResponseMetadata(
|
||||
requestId,
|
||||
@@ -207,7 +207,7 @@ export const handleLoadingFinished = (
|
||||
state: CdpCaptureEventsState,
|
||||
params: UnknownRecord,
|
||||
): void => {
|
||||
const requestId = stringValue(params.requestId);
|
||||
const requestId = cdpStringValue(params.requestId);
|
||||
if (requestId === undefined) return;
|
||||
const current = state.network.get(requestId);
|
||||
if (current === undefined) return;
|
||||
@@ -233,16 +233,16 @@ export const handleConsoleAPICalled = (
|
||||
if (source === undefined) {
|
||||
state.completeness.exclude(
|
||||
"console_events",
|
||||
exclusionReasonForUrl(stringValue(frame.url)),
|
||||
exclusionReasonForUrl(cdpStringValue(frame.url)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const arguments_ = recordsValue(params.args);
|
||||
state.console.push({
|
||||
type: stringValue(params.type) ?? "unknown",
|
||||
type: cdpStringValue(params.type) ?? "unknown",
|
||||
timestamp: numberValue(params.timestamp) ?? 0,
|
||||
argument_types: arguments_.map(
|
||||
(argument) => stringValue(argument.type) ?? "unknown",
|
||||
(argument) => cdpStringValue(argument.type) ?? "unknown",
|
||||
),
|
||||
url: source.url,
|
||||
line: integerOrNull(frame.lineNumber),
|
||||
@@ -287,14 +287,14 @@ export const handleWebSocketFrame = (
|
||||
params: UnknownRecord,
|
||||
direction: "sent" | "received",
|
||||
): void => {
|
||||
const requestId = stringValue(params.requestId);
|
||||
const requestId = cdpStringValue(params.requestId);
|
||||
if (
|
||||
requestId === undefined ||
|
||||
(!state.network.has(requestId) && !state.allowedWebSockets.has(requestId))
|
||||
)
|
||||
return;
|
||||
const response = recordValue(params.response);
|
||||
const payload = stringValue(response?.payloadData) ?? "";
|
||||
const payload = cdpStringValue(response?.payloadData) ?? "";
|
||||
const opcode = Math.max(0, Math.trunc(numberValue(response?.opcode) ?? 0));
|
||||
const decoded = opcode === 1 ? undefined : decodeBase64(payload);
|
||||
if (opcode !== 1 && decoded === undefined)
|
||||
@@ -327,8 +327,8 @@ export const handleWebSocketCreated = (
|
||||
state: CdpCaptureEventsState,
|
||||
params: UnknownRecord,
|
||||
): void => {
|
||||
const requestId = stringValue(params.requestId);
|
||||
const rawUrl = stringValue(params.url);
|
||||
const requestId = cdpStringValue(params.requestId);
|
||||
const rawUrl = cdpStringValue(params.url);
|
||||
if (requestId === undefined) {
|
||||
state.completeness.exclude(
|
||||
"websocket_connections",
|
||||
@@ -378,8 +378,8 @@ export const handleFrameNavigated = (
|
||||
)
|
||||
)
|
||||
return;
|
||||
const rawUrl = stringValue(frame?.url);
|
||||
const loaderId = stringValue(frame?.loaderId);
|
||||
const rawUrl = cdpStringValue(frame?.url);
|
||||
const loaderId = cdpStringValue(frame?.loaderId);
|
||||
// `frameNavigated` re-fires for a document already reported (same URL and
|
||||
// loader). Treating that as a navigation aborted a valid capture with
|
||||
// `target_changed`, which is a false claim that the target moved.
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
numberValue,
|
||||
recordValue,
|
||||
recordsValue,
|
||||
stringValue,
|
||||
cdpStringValue,
|
||||
type UnknownRecord,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import type { CdpEvent } from "./CdpConnection.js";
|
||||
@@ -17,10 +17,10 @@ export const isMainFrameNavigation = (
|
||||
if (params === undefined) return false;
|
||||
const frameId =
|
||||
event.method === "Page.frameNavigated"
|
||||
? stringValue(recordValue(params.frame)?.id)
|
||||
? cdpStringValue(recordValue(params.frame)?.id)
|
||||
: event.method === "Page.frameRequestedNavigation" ||
|
||||
event.method === "Page.navigatedWithinDocument"
|
||||
? stringValue(params.frameId)
|
||||
? cdpStringValue(params.frameId)
|
||||
: undefined;
|
||||
return frameId === mainFrameId;
|
||||
};
|
||||
@@ -38,7 +38,7 @@ export const initiatorLocation = (
|
||||
if (frame !== undefined) return frame;
|
||||
stack = recordValue(stack.parent);
|
||||
}
|
||||
return stringValue(initiator?.url) === undefined ? undefined : initiator;
|
||||
return cdpStringValue(initiator?.url) === undefined ? undefined : initiator;
|
||||
};
|
||||
|
||||
export const exclusionReasonForUrl = (
|
||||
@@ -69,7 +69,7 @@ export const isJsonContentType = (
|
||||
for (const [name, value] of Object.entries(headers))
|
||||
if (
|
||||
name.toLowerCase() === "content-type" &&
|
||||
isJsonMediaType(stringValue(value))
|
||||
isJsonMediaType(cdpStringValue(value))
|
||||
)
|
||||
return true;
|
||||
return false;
|
||||
@@ -85,7 +85,7 @@ export const isJsonMediaType = (value: string | null | undefined): boolean => {
|
||||
export const consolePrimitive = (
|
||||
value: UnknownRecord,
|
||||
): { readonly type: string; readonly text: string } | undefined => {
|
||||
const type = stringValue(value.type);
|
||||
const type = cdpStringValue(value.type);
|
||||
switch (type) {
|
||||
case "string":
|
||||
return typeof value.value === "string"
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
numberValue,
|
||||
recordValue,
|
||||
recordsValue,
|
||||
stringValue,
|
||||
cdpStringValue,
|
||||
} from "./CdpCaptureValues.js";
|
||||
|
||||
/** Capture redacted storage metadata for an authorized page and its origin. */
|
||||
@@ -108,8 +108,8 @@ const capturedCaches = (
|
||||
values: readonly Record<string, unknown>[],
|
||||
): CapturedCache[] =>
|
||||
values.flatMap((cache) => {
|
||||
const name = stringValue(cache.cacheName);
|
||||
const id = stringValue(cache.cacheId);
|
||||
const name = cdpStringValue(cache.cacheName);
|
||||
const id = cdpStringValue(cache.cacheId);
|
||||
return name === undefined || id === undefined ? [] : [{ name, id }];
|
||||
});
|
||||
|
||||
@@ -130,8 +130,8 @@ const storageItems = async (
|
||||
return { items: [], complete: false };
|
||||
const items = entries.flatMap((entry) => {
|
||||
if (!Array.isArray(entry)) return [];
|
||||
const key = stringValue(entry[0]);
|
||||
const value = stringValue(entry[1]);
|
||||
const key = cdpStringValue(entry[0]);
|
||||
const value = cdpStringValue(entry[1]);
|
||||
return key === undefined || value === undefined ? [] : [{ key, value }];
|
||||
});
|
||||
return { items, complete: items.length === entries.length };
|
||||
@@ -140,7 +140,7 @@ const storageItems = async (
|
||||
const stringArray = (value: unknown): readonly string[] =>
|
||||
Array.isArray(value)
|
||||
? value.flatMap((item) => {
|
||||
const text = stringValue(item);
|
||||
const text = cdpStringValue(item);
|
||||
return text === undefined ? [] : [text];
|
||||
})
|
||||
: [];
|
||||
|
||||
@@ -9,7 +9,11 @@ import type {
|
||||
import { compareCodePoints } from "../domain/canonicalOrdering.js";
|
||||
import { CdpConnection } from "./CdpConnection.js";
|
||||
import { optionalCdpCommand } from "./CdpOptionalCommand.js";
|
||||
import { recordValue, recordsValue, stringValue } from "./CdpCaptureValues.js";
|
||||
import {
|
||||
recordValue,
|
||||
recordsValue,
|
||||
cdpStringValue,
|
||||
} from "./CdpCaptureValues.js";
|
||||
|
||||
type StorageFingerprint =
|
||||
WebPageInspection["storage"]["content_fingerprints"][number];
|
||||
@@ -132,10 +136,10 @@ const addCookies = async (
|
||||
const cookies = recordsValue(cookieValues);
|
||||
if (cookies.length !== cookieValues.length) run.state.complete = false;
|
||||
for (const cookie of cookies) {
|
||||
const name = stringValue(cookie.name);
|
||||
const value = stringValue(cookie.value);
|
||||
const domain = stringValue(cookie.domain);
|
||||
const path = stringValue(cookie.path);
|
||||
const name = cdpStringValue(cookie.name);
|
||||
const value = cdpStringValue(cookie.value);
|
||||
const domain = cdpStringValue(cookie.domain);
|
||||
const path = cdpStringValue(cookie.path);
|
||||
if (
|
||||
name === undefined ||
|
||||
value === undefined ||
|
||||
@@ -176,7 +180,7 @@ const addIndexedDb = async (
|
||||
run.state.complete = false;
|
||||
continue;
|
||||
}
|
||||
if (stringValue(database.name) !== databaseName) {
|
||||
if (cdpStringValue(database.name) !== databaseName) {
|
||||
run.state.complete = false;
|
||||
continue;
|
||||
}
|
||||
@@ -194,7 +198,7 @@ const addIndexedDb = async (
|
||||
complete: indexedDbSchemaComplete(stores),
|
||||
});
|
||||
for (const store of stores) {
|
||||
const objectStoreName = stringValue(store.name);
|
||||
const objectStoreName = cdpStringValue(store.name);
|
||||
if (objectStoreName === undefined) {
|
||||
run.state.complete = false;
|
||||
continue;
|
||||
@@ -209,11 +213,11 @@ const addIndexedDb = async (
|
||||
};
|
||||
|
||||
const indexedDbStoreSchema = (store: Readonly<Record<string, unknown>>) => ({
|
||||
name: stringValue(store.name) ?? null,
|
||||
name: cdpStringValue(store.name) ?? null,
|
||||
key_path: stableValue(store.keyPath),
|
||||
auto_increment: store.autoIncrement === true,
|
||||
indexes: recordsValue(store.indexes).map((index) => ({
|
||||
name: stringValue(index.name) ?? null,
|
||||
name: cdpStringValue(index.name) ?? null,
|
||||
key_path: stableValue(index.keyPath),
|
||||
unique: index.unique === true,
|
||||
multi_entry: index.multiEntry === true,
|
||||
@@ -225,14 +229,14 @@ const indexedDbSchemaComplete = (
|
||||
): boolean =>
|
||||
stores.every(
|
||||
(store) =>
|
||||
stringValue(store.name) !== undefined &&
|
||||
cdpStringValue(store.name) !== undefined &&
|
||||
stableValueComplete(store.keyPath) &&
|
||||
typeof store.autoIncrement === "boolean" &&
|
||||
Array.isArray(store.indexes) &&
|
||||
recordsValue(store.indexes).length === store.indexes.length &&
|
||||
recordsValue(store.indexes).every(
|
||||
(index) =>
|
||||
stringValue(index.name) !== undefined &&
|
||||
cdpStringValue(index.name) !== undefined &&
|
||||
stableValueComplete(index.keyPath) &&
|
||||
typeof index.unique === "boolean" &&
|
||||
typeof index.multiEntry === "boolean",
|
||||
@@ -358,8 +362,8 @@ const addCacheEntry = async (
|
||||
cache: CapturedCache,
|
||||
entry: Readonly<Record<string, unknown>>,
|
||||
): Promise<void> => {
|
||||
const requestURL = stringValue(entry.requestURL);
|
||||
const requestMethod = stringValue(entry.requestMethod);
|
||||
const requestURL = cdpStringValue(entry.requestURL);
|
||||
const requestMethod = cdpStringValue(entry.requestMethod);
|
||||
if (requestURL === undefined || requestMethod === undefined) {
|
||||
run.state.complete = false;
|
||||
return;
|
||||
@@ -370,8 +374,8 @@ const addCacheEntry = async (
|
||||
}
|
||||
const headerRecords = recordsValue(entry.requestHeaders);
|
||||
const requestHeaders = headerRecords.flatMap((header) => {
|
||||
const name = stringValue(header.name);
|
||||
const value = stringValue(header.value);
|
||||
const name = cdpStringValue(header.name);
|
||||
const value = cdpStringValue(header.value);
|
||||
return name === undefined || value === undefined ? [] : [{ name, value }];
|
||||
});
|
||||
if (
|
||||
@@ -387,7 +391,7 @@ const addCacheEntry = async (
|
||||
{ cacheId: cache.id, requestURL, requestHeaders },
|
||||
run.limitations,
|
||||
);
|
||||
const body = stringValue(recordValue(recordValue(raw)?.response)?.body);
|
||||
const body = cdpStringValue(recordValue(recordValue(raw)?.response)?.body);
|
||||
const decoded = body === undefined ? null : decodeBase64(body);
|
||||
const complete = decoded !== null;
|
||||
if (!complete) run.state.complete = false;
|
||||
|
||||
@@ -20,7 +20,7 @@ export const recordsValue = (value: unknown): readonly UnknownRecord[] =>
|
||||
})
|
||||
: [];
|
||||
|
||||
export const stringValue = (value: unknown): string | undefined =>
|
||||
export const cdpStringValue = (value: unknown): string | undefined =>
|
||||
typeof value === "string" ? value : undefined;
|
||||
|
||||
export const numberValue = (value: unknown): number | undefined =>
|
||||
@@ -49,7 +49,7 @@ export const allowedSanitizedUrl = (
|
||||
value: unknown,
|
||||
allowedOrigins: ReadonlySet<string>,
|
||||
): ReturnType<typeof sanitizeBrowserUrl> | undefined => {
|
||||
const text = stringValue(value);
|
||||
const text = cdpStringValue(value);
|
||||
if (text === undefined) return undefined;
|
||||
const sanitized = sanitizeBrowserUrl(text);
|
||||
return sanitized.origin !== null && allowedOrigins.has(sanitized.origin)
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
recordValue,
|
||||
recordsValue,
|
||||
requiredRecord,
|
||||
stringValue,
|
||||
cdpStringValue,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import { walkFrameTrees } from "./CdpCaptureDocuments.js";
|
||||
import {
|
||||
@@ -220,9 +220,9 @@ const navigatedFrameId = (event: CdpEvent): string | undefined => {
|
||||
const params = recordValue(event.params);
|
||||
if (params === undefined) return undefined;
|
||||
if (event.method === "Page.frameNavigated")
|
||||
return stringValue(recordValue(params.frame)?.id);
|
||||
return cdpStringValue(recordValue(params.frame)?.id);
|
||||
if (event.method === "Page.navigatedWithinDocument")
|
||||
return stringValue(params.frameId);
|
||||
return cdpStringValue(params.frameId);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -238,15 +238,15 @@ const captureFrames = async (
|
||||
completeness.exclude("frames", "invalid_protocol_value"),
|
||||
)) {
|
||||
const frame = recordValue(tree.frame);
|
||||
const frameId = stringValue(frame?.id);
|
||||
const path = await authorizedElectronFile(stringValue(frame?.url) ?? "");
|
||||
const frameId = cdpStringValue(frame?.id);
|
||||
const path = await authorizedElectronFile(cdpStringValue(frame?.url) ?? "");
|
||||
if (frameId === undefined || path === undefined) {
|
||||
completeness.exclude("frames", "out_of_target_scope");
|
||||
continue;
|
||||
}
|
||||
frames.push({
|
||||
frame_id: frameId,
|
||||
parent_frame_id: stringValue(frame?.parentId) ?? null,
|
||||
parent_frame_id: cdpStringValue(frame?.parentId) ?? null,
|
||||
file_path: path,
|
||||
});
|
||||
}
|
||||
@@ -274,7 +274,7 @@ const captureResources = async (
|
||||
if (tree === undefined) break;
|
||||
for (const resource of recordsValue(tree.resources)) {
|
||||
const path = await authorizedElectronFile(
|
||||
stringValue(resource.url) ?? "",
|
||||
cdpStringValue(resource.url) ?? "",
|
||||
);
|
||||
if (path === undefined) {
|
||||
completeness.exclude("resources", "out_of_target_scope");
|
||||
@@ -282,8 +282,8 @@ const captureResources = async (
|
||||
}
|
||||
const item = {
|
||||
file_path: path,
|
||||
type: stringValue(resource.type) ?? "Other",
|
||||
mime_type: stringValue(resource.mimeType) ?? "",
|
||||
type: cdpStringValue(resource.type) ?? "Other",
|
||||
mime_type: cdpStringValue(resource.mimeType) ?? "",
|
||||
content_size:
|
||||
numberValue(resource.contentSize) === undefined
|
||||
? null
|
||||
@@ -358,7 +358,7 @@ const captureDom = async (
|
||||
const mainFilePath = async (result: unknown): Promise<string | undefined> => {
|
||||
const frameTree = recordValue(requiredRecord(result).frameTree);
|
||||
const frame = recordValue(frameTree?.frame);
|
||||
return await authorizedElectronFile(stringValue(frame?.url) ?? "");
|
||||
return await authorizedElectronFile(cdpStringValue(frame?.url) ?? "");
|
||||
};
|
||||
|
||||
const arrayValue = (value: unknown): readonly unknown[] =>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { CdpEvent } from "./CdpConnection.js";
|
||||
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
|
||||
import { numberValue, recordValue, stringValue } from "./CdpCaptureValues.js";
|
||||
import {
|
||||
numberValue,
|
||||
recordValue,
|
||||
cdpStringValue,
|
||||
} from "./CdpCaptureValues.js";
|
||||
|
||||
export interface ElectronScriptDraft {
|
||||
readonly scriptId: string;
|
||||
@@ -35,8 +39,8 @@ export const ingestElectronScriptEvent = (input: {
|
||||
}
|
||||
if (input.event.method !== "Debugger.scriptParsed") return;
|
||||
const value = recordValue(input.event.params);
|
||||
const scriptId = stringValue(value?.scriptId);
|
||||
const rawUrl = stringValue(value?.url);
|
||||
const scriptId = cdpStringValue(value?.scriptId);
|
||||
const rawUrl = cdpStringValue(value?.url);
|
||||
if (scriptId === undefined) {
|
||||
input.completeness.exclude("scripts", "invalid_protocol_value");
|
||||
return;
|
||||
@@ -48,10 +52,10 @@ export const ingestElectronScriptEvent = (input: {
|
||||
input.scripts.push({
|
||||
scriptId,
|
||||
rawUrl,
|
||||
hash: stringValue(value?.hash) ?? "",
|
||||
hash: cdpStringValue(value?.hash) ?? "",
|
||||
length: nonnegativeInteger(value?.length),
|
||||
isModule: value?.isModule === true,
|
||||
language: stringValue(value?.scriptLanguage) ?? null,
|
||||
language: cdpStringValue(value?.scriptLanguage) ?? null,
|
||||
executionContextKey: executionContextKey(value?.executionContextId),
|
||||
});
|
||||
};
|
||||
@@ -63,7 +67,7 @@ const retainExecutionContext = (
|
||||
const parameters = recordValue(event.params);
|
||||
const runtimeContext = recordValue(parameters?.context);
|
||||
const contextKey = executionContextKey(runtimeContext?.id);
|
||||
const frameId = stringValue(recordValue(runtimeContext?.auxData)?.frameId);
|
||||
const frameId = cdpStringValue(recordValue(runtimeContext?.auxData)?.frameId);
|
||||
if (contextKey === null || frameId === undefined) return;
|
||||
frames.set(contextKey, frameId);
|
||||
};
|
||||
|
||||
@@ -6,7 +6,7 @@ import { canonicalDigest } from "../domain/comparisonSemantics.js";
|
||||
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
|
||||
import type { CdpConnection } from "./CdpConnection.js";
|
||||
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
|
||||
import { requiredRecord, stringValue } from "./CdpCaptureValues.js";
|
||||
import { requiredRecord, cdpStringValue } from "./CdpCaptureValues.js";
|
||||
import type { ElectronScriptDraft } from "./CdpElectronScriptEvents.js";
|
||||
import { authorizedElectronFile } from "./ElectronFileScope.js";
|
||||
|
||||
@@ -94,7 +94,7 @@ const captureScriptSource = async (
|
||||
input.signal,
|
||||
),
|
||||
);
|
||||
const text = stringValue(result.scriptSource) ?? "";
|
||||
const text = cdpStringValue(result.scriptSource) ?? "";
|
||||
return {
|
||||
source: {
|
||||
included: true,
|
||||
|
||||
@@ -4,7 +4,11 @@ import type {
|
||||
} from "../domain/electronObservation.js";
|
||||
import type { CdpConnection } from "./CdpConnection.js";
|
||||
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
|
||||
import { recordValue, recordsValue, stringValue } from "./CdpCaptureValues.js";
|
||||
import {
|
||||
recordValue,
|
||||
recordsValue,
|
||||
cdpStringValue,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import type { CdpEndpointTarget } from "./CdpEndpoint.js";
|
||||
import { optionalCdpCommand } from "./CdpOptionalCommand.js";
|
||||
import { authorizedElectronFile } from "./ElectronFileScope.js";
|
||||
@@ -35,7 +39,7 @@ export const captureElectronWorkers = async (input: {
|
||||
}
|
||||
const workers: ElectronPageInspection["workers"] = [];
|
||||
for (const target of recordsValue(recordValue(result)?.targetInfos)) {
|
||||
const type = stringValue(target.type) ?? "";
|
||||
const type = cdpStringValue(target.type) ?? "";
|
||||
if (!type.includes("worker")) continue;
|
||||
const openerTargetId = boundedTargetField(target.openerId);
|
||||
const parentFrameId = boundedTargetField(target.parentFrameId);
|
||||
@@ -46,7 +50,7 @@ export const captureElectronWorkers = async (input: {
|
||||
input.completeness.exclude("workers", "out_of_target_scope");
|
||||
continue;
|
||||
}
|
||||
const path = await authorizedElectronFile(stringValue(target.url) ?? "");
|
||||
const path = await authorizedElectronFile(cdpStringValue(target.url) ?? "");
|
||||
if (path === undefined) {
|
||||
input.completeness.exclude("workers", "out_of_target_scope");
|
||||
continue;
|
||||
@@ -71,6 +75,6 @@ export const captureElectronWorkers = async (input: {
|
||||
};
|
||||
|
||||
const boundedTargetField = (value: unknown): string | null => {
|
||||
const field = stringValue(value);
|
||||
const field = cdpStringValue(value);
|
||||
return field === undefined || field === "" ? null : field;
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
isHttpUrl,
|
||||
numberValue,
|
||||
recordValue,
|
||||
stringValue,
|
||||
cdpStringValue,
|
||||
type UnknownRecord,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import { mainFrameUrl } from "./CdpCaptureDocuments.js";
|
||||
@@ -218,7 +218,7 @@ class TimelineCapture {
|
||||
this.#add({
|
||||
type: "navigation_requested",
|
||||
params,
|
||||
rawUrl: stringValue(params.url) ?? null,
|
||||
rawUrl: cdpStringValue(params.url) ?? null,
|
||||
requestId: null,
|
||||
detail: reason,
|
||||
});
|
||||
@@ -227,7 +227,7 @@ class TimelineCapture {
|
||||
#navigationCommitted(event: CdpEvent, params: UnknownRecord): void {
|
||||
const frame = recordValue(params.frame);
|
||||
if (!isMainFrameNavigation(event, this.mainFrameId)) return;
|
||||
const rawUrl = stringValue(frame?.url);
|
||||
const rawUrl = cdpStringValue(frame?.url);
|
||||
const destination = scopedUrl(rawUrl, this.allowedOrigins);
|
||||
this.#add({
|
||||
type: this.#pendingReload ? "same_origin_reload" : "navigation_committed",
|
||||
@@ -247,7 +247,7 @@ class TimelineCapture {
|
||||
|
||||
#sameDocument(event: CdpEvent, params: UnknownRecord): void {
|
||||
if (!isMainFrameNavigation(event, this.mainFrameId)) return;
|
||||
const rawUrl = stringValue(params.url);
|
||||
const rawUrl = cdpStringValue(params.url);
|
||||
const destination = scopedUrl(rawUrl, this.allowedOrigins);
|
||||
this.#add({
|
||||
type: "same_document_navigation",
|
||||
@@ -265,21 +265,21 @@ class TimelineCapture {
|
||||
}
|
||||
|
||||
#redirect(params: UnknownRecord): void {
|
||||
const resourceType = stringValue(params.type);
|
||||
const resourceType = cdpStringValue(params.type);
|
||||
if (resourceType !== undefined && resourceType !== "Document") return;
|
||||
if (
|
||||
stringValue(params.frameId) !== this.mainFrameId ||
|
||||
cdpStringValue(params.frameId) !== this.mainFrameId ||
|
||||
recordValue(params.redirectResponse) === undefined
|
||||
)
|
||||
return;
|
||||
const request = recordValue(params.request);
|
||||
const rawUrl = stringValue(request?.url);
|
||||
const rawUrl = cdpStringValue(request?.url);
|
||||
const destination = scopedUrl(rawUrl, this.allowedOrigins);
|
||||
this.#add({
|
||||
type: "redirect",
|
||||
params,
|
||||
rawUrl: rawUrl ?? null,
|
||||
requestId: stringValue(params.requestId) ?? null,
|
||||
requestId: cdpStringValue(params.requestId) ?? null,
|
||||
detail: null,
|
||||
});
|
||||
if (destination.scope !== "approved") {
|
||||
@@ -289,19 +289,19 @@ class TimelineCapture {
|
||||
}
|
||||
|
||||
#loadingFailed(params: UnknownRecord): void {
|
||||
const frame = stringValue(params.frameId);
|
||||
const frame = cdpStringValue(params.frameId);
|
||||
if (frame !== undefined && frame !== this.mainFrameId) return;
|
||||
this.#add({
|
||||
type: "load_failed",
|
||||
params,
|
||||
rawUrl: null,
|
||||
requestId: stringValue(params.requestId) ?? null,
|
||||
requestId: cdpStringValue(params.requestId) ?? null,
|
||||
detail: safeNetworkError(params.errorText),
|
||||
});
|
||||
}
|
||||
|
||||
#lifecycle(params: UnknownRecord): void {
|
||||
if (stringValue(params.frameId) !== this.mainFrameId) return;
|
||||
if (cdpStringValue(params.frameId) !== this.mainFrameId) return;
|
||||
this.#add({
|
||||
type: "lifecycle",
|
||||
params,
|
||||
@@ -344,7 +344,7 @@ const observationEventMatches = (
|
||||
return event.sessionId === sessionId;
|
||||
return (
|
||||
sessionId !== undefined &&
|
||||
stringValue(recordValue(event.params)?.sessionId) === sessionId
|
||||
cdpStringValue(recordValue(event.params)?.sessionId) === sessionId
|
||||
);
|
||||
};
|
||||
|
||||
@@ -405,7 +405,7 @@ const captureFinalUrl = async (
|
||||
};
|
||||
|
||||
const frameId = (result: unknown): string | undefined =>
|
||||
stringValue(
|
||||
cdpStringValue(
|
||||
recordValue(recordValue(recordValue(result)?.frameTree)?.frame)?.id,
|
||||
);
|
||||
|
||||
@@ -423,17 +423,18 @@ const scopedUrl = (
|
||||
: { url: null, scope: "unsupported" };
|
||||
};
|
||||
|
||||
const boundedId = (value: unknown): string | null => stringValue(value) ?? null;
|
||||
const boundedId = (value: unknown): string | null =>
|
||||
cdpStringValue(value) ?? null;
|
||||
|
||||
const safeDetail = (value: unknown): string | null => {
|
||||
const detail = stringValue(value)?.toLowerCase();
|
||||
const detail = cdpStringValue(value)?.toLowerCase();
|
||||
return detail !== undefined && /^[a-z0-9_.:-]{1,100}$/u.test(detail)
|
||||
? detail
|
||||
: null;
|
||||
};
|
||||
|
||||
const safeNetworkError = (value: unknown): string | null => {
|
||||
const error = stringValue(value);
|
||||
const error = cdpStringValue(value);
|
||||
return error !== undefined && /^net::ERR_[A-Z0-9_]{1,100}$/u.test(error)
|
||||
? error
|
||||
: null;
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
requiredRecord,
|
||||
sourceExcluded,
|
||||
sourceResult,
|
||||
stringValue,
|
||||
cdpStringValue,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import type { CaptureContext } from "./CdpPageCapture.js";
|
||||
import type { WebSourceMapRequest } from "./WebSourceMapFetcher.js";
|
||||
@@ -115,7 +115,7 @@ const captureScriptSource = async (
|
||||
context.signal,
|
||||
),
|
||||
);
|
||||
const content = stringValue(result.scriptSource) ?? "";
|
||||
const content = cdpStringValue(result.scriptSource) ?? "";
|
||||
return sourceResult(content);
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
isHttpUrl,
|
||||
recordValue,
|
||||
recordsValue,
|
||||
stringValue,
|
||||
cdpStringValue,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import type { CaptureContext } from "./CdpPageCapture.js";
|
||||
import { optionalCdpCommand } from "./CdpOptionalCommand.js";
|
||||
@@ -32,19 +32,19 @@ export const captureWorkers = async (
|
||||
const items: WebPageInspection["workers"] = [];
|
||||
if (result === undefined) completeness.unavailable("workers");
|
||||
for (const target of recordsValue(recordValue(result)?.targetInfos)) {
|
||||
const type = stringValue(target.type) ?? "";
|
||||
const type = cdpStringValue(target.type) ?? "";
|
||||
const url = allowedSanitizedUrl(target.url, allowedOrigins);
|
||||
const relatedToPage =
|
||||
stringValue(target.openerId) === context.target.id ||
|
||||
(stringValue(target.parentFrameId) !== undefined &&
|
||||
frameIds.has(stringValue(target.parentFrameId) ?? ""));
|
||||
cdpStringValue(target.openerId) === context.target.id ||
|
||||
(cdpStringValue(target.parentFrameId) !== undefined &&
|
||||
frameIds.has(cdpStringValue(target.parentFrameId) ?? ""));
|
||||
if (!type.includes("worker")) continue;
|
||||
if (!relatedToPage) {
|
||||
completeness.exclude("workers", "out_of_target_scope");
|
||||
continue;
|
||||
}
|
||||
if (url === undefined) {
|
||||
const rawUrl = stringValue(target.url);
|
||||
const rawUrl = cdpStringValue(target.url);
|
||||
completeness.exclude(
|
||||
"workers",
|
||||
rawUrl === undefined || rawUrl === ""
|
||||
@@ -56,13 +56,13 @@ export const captureWorkers = async (
|
||||
continue;
|
||||
}
|
||||
items.push({
|
||||
target_id: stringValue(target.targetId) ?? "",
|
||||
target_id: cdpStringValue(target.targetId) ?? "",
|
||||
type,
|
||||
url: url.url,
|
||||
origin: url.origin,
|
||||
attached: target.attached === true,
|
||||
opener_target_id: stringValue(target.openerId) ?? null,
|
||||
parent_frame_id: stringValue(target.parentFrameId) ?? null,
|
||||
opener_target_id: cdpStringValue(target.openerId) ?? null,
|
||||
parent_frame_id: cdpStringValue(target.parentFrameId) ?? null,
|
||||
});
|
||||
}
|
||||
return items;
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { WebPageInspection } from "../domain/browserObservation.js";
|
||||
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
|
||||
import {
|
||||
numberValue,
|
||||
stringValue,
|
||||
cdpStringValue,
|
||||
type UnknownRecord,
|
||||
} from "./CdpCaptureValues.js";
|
||||
|
||||
@@ -42,7 +42,7 @@ export const safeResponseMetadata = (
|
||||
response: {
|
||||
request_id: requestId,
|
||||
url,
|
||||
mime_type: boundedHeader(stringValue(response.mimeType)),
|
||||
mime_type: boundedHeader(cdpStringValue(response.mimeType)),
|
||||
content_length: nonnegativeInteger(headers.get("content-length")),
|
||||
content_encoding: boundedHeader(headers.get("content-encoding")),
|
||||
csp,
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { CdpConnection, CdpEvent } from "./CdpConnection.js";
|
||||
import {
|
||||
allowedSanitizedUrl,
|
||||
recordValue,
|
||||
stringValue,
|
||||
cdpStringValue,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import { mainFrameUrl } from "./CdpCaptureDocuments.js";
|
||||
import { captureFrames } from "./CdpCaptureDocuments.js";
|
||||
@@ -63,7 +63,7 @@ export const captureCdpScreenshot = async (
|
||||
changed: false,
|
||||
leftScope: false,
|
||||
url: beforeUrl,
|
||||
loaderId: stringValue(initialFrame?.loaderId),
|
||||
loaderId: cdpStringValue(initialFrame?.loaderId),
|
||||
};
|
||||
const removeListener = context.connection.onEvent((event) => {
|
||||
if (event.sessionId !== context.sessionId) return;
|
||||
@@ -89,7 +89,7 @@ export const captureCdpScreenshot = async (
|
||||
} finally {
|
||||
removeListener();
|
||||
}
|
||||
const encoded = stringValue(result?.data);
|
||||
const encoded = cdpStringValue(result?.data);
|
||||
const bytes =
|
||||
encoded === undefined ? undefined : decodeCanonicalBase64(encoded);
|
||||
if (bytes === undefined)
|
||||
@@ -149,8 +149,8 @@ const observeScreenshotNavigation = (
|
||||
event.method === "Page.frameNavigated" &&
|
||||
state.loaderId !== undefined &&
|
||||
state.loaderId !== "" &&
|
||||
stringValue(frame?.loaderId) === state.loaderId &&
|
||||
stringValue(frame?.url) === state.url
|
||||
cdpStringValue(frame?.loaderId) === state.loaderId &&
|
||||
cdpStringValue(frame?.url) === state.url
|
||||
)
|
||||
return;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
numberValue,
|
||||
recordValue,
|
||||
recordsValue,
|
||||
stringValue,
|
||||
cdpStringValue,
|
||||
type UnknownRecord,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import {
|
||||
@@ -173,10 +173,13 @@ const initialFrameScope = (
|
||||
completeness.exclude("webmcp_tools", "invalid_protocol_value"),
|
||||
)) {
|
||||
const frame = recordValue(tree.frame);
|
||||
const frameId = stringValue(frame?.id);
|
||||
const frameId = cdpStringValue(frame?.id);
|
||||
const prior = frameId === undefined ? undefined : scoped.get(frameId);
|
||||
if (frameId !== undefined && prior !== undefined)
|
||||
scoped.set(frameId, { ...prior, loaderId: stringValue(frame?.loaderId) });
|
||||
scoped.set(frameId, {
|
||||
...prior,
|
||||
loaderId: cdpStringValue(frame?.loaderId),
|
||||
});
|
||||
}
|
||||
return scoped;
|
||||
};
|
||||
@@ -203,7 +206,7 @@ const handleFrameDetached = (
|
||||
params: UnknownRecord,
|
||||
state: FrameScopeState,
|
||||
): boolean => {
|
||||
const frameId = stringValue(params.frameId);
|
||||
const frameId = cdpStringValue(params.frameId);
|
||||
if (frameId === undefined) {
|
||||
state.completeness.exclude("webmcp_tools", "invalid_protocol_value");
|
||||
return true;
|
||||
@@ -223,12 +226,12 @@ const handleFrameUpdate = (
|
||||
state.completeness.exclude("webmcp_tools", "invalid_protocol_value");
|
||||
return true;
|
||||
}
|
||||
const frameId = stringValue(frame?.id) ?? stringValue(frame?.frameId);
|
||||
const frameId = cdpStringValue(frame?.id) ?? cdpStringValue(frame?.frameId);
|
||||
if (frameId === undefined) {
|
||||
state.completeness.exclude("webmcp_tools", "invalid_protocol_value");
|
||||
return true;
|
||||
}
|
||||
const rawUrl = stringValue(frame?.url);
|
||||
const rawUrl = cdpStringValue(frame?.url);
|
||||
// Chromium can briefly commit an inherited, empty document while replacing
|
||||
// a subframe. That intermediate URL says nothing about the frame's eventual
|
||||
// scope or its registered tools; wait for the next committed URL (or the
|
||||
@@ -259,7 +262,7 @@ const updateFrameScope = (
|
||||
return;
|
||||
}
|
||||
const previous = state.frames.get(frameId);
|
||||
const loaderId = stringValue(frame?.loaderId);
|
||||
const loaderId = cdpStringValue(frame?.loaderId);
|
||||
if (hasNewDocument(documentCommitted, previous, url.url, loaderId))
|
||||
removeFrameTools(frameId, state.tools);
|
||||
if (documentCommitted && state.transientFrames.has(frameId))
|
||||
@@ -269,7 +272,7 @@ const updateFrameScope = (
|
||||
origin: url.origin,
|
||||
...(loaderId === undefined ? {} : { loaderId }),
|
||||
parentFrameId:
|
||||
(stringValue(frame?.parentId) ??
|
||||
(cdpStringValue(frame?.parentId) ??
|
||||
state.frames.get(frameId)?.parentFrameId ??
|
||||
"") ||
|
||||
null,
|
||||
@@ -364,8 +367,8 @@ const ingestWebMcpEvent = (options: WebMcpIngestOptions): void => {
|
||||
if (params === undefined) return;
|
||||
if (event.method === "WebMCP.toolsRemoved") {
|
||||
for (const removed of recordsValue(params.tools)) {
|
||||
const name = stringValue(removed.name);
|
||||
const frameId = stringValue(removed.frameId);
|
||||
const name = cdpStringValue(removed.name);
|
||||
const frameId = cdpStringValue(removed.frameId);
|
||||
if (name === undefined || frameId === undefined) continue;
|
||||
for (const [key, tool] of tools)
|
||||
if (tool.name === name && tool.frame_id === frameId) tools.delete(key);
|
||||
@@ -374,7 +377,7 @@ const ingestWebMcpEvent = (options: WebMcpIngestOptions): void => {
|
||||
}
|
||||
if (event.method !== "WebMCP.toolsAdded") return;
|
||||
for (const declared of recordsValue(params.tools)) {
|
||||
const frameId = stringValue(declared.frameId);
|
||||
const frameId = cdpStringValue(declared.frameId);
|
||||
if (frameId !== undefined && transientFrames.has(frameId)) {
|
||||
if (!resolvedTransientFrames.has(frameId)) continue;
|
||||
transientFrames.delete(frameId);
|
||||
@@ -395,8 +398,8 @@ const normalizeTool = (
|
||||
>,
|
||||
completeness: CdpCaptureCompleteness,
|
||||
): WebMcpDiscovery["tools"]["items"][number] | undefined => {
|
||||
const frameId = stringValue(value.frameId);
|
||||
const name = stringValue(value.name);
|
||||
const frameId = cdpStringValue(value.frameId);
|
||||
const name = cdpStringValue(value.name);
|
||||
const frame = frameId === undefined ? undefined : frames.get(frameId);
|
||||
if (
|
||||
frameId === undefined ||
|
||||
@@ -407,7 +410,7 @@ const normalizeTool = (
|
||||
completeness.exclude("webmcp_tools", "out_of_target_scope");
|
||||
return undefined;
|
||||
}
|
||||
const description = stringValue(value.description) ?? "";
|
||||
const description = cdpStringValue(value.description) ?? "";
|
||||
const annotations = recordValue(value.annotations);
|
||||
return {
|
||||
tool_key: toolKey(frame.url, frameId, name),
|
||||
|
||||
@@ -19,7 +19,7 @@ import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
numberValue,
|
||||
recordValue,
|
||||
stringValue,
|
||||
cdpStringValue,
|
||||
delayWithCancellation,
|
||||
} from "./CdpCaptureValues.js";
|
||||
import { CdpConnection, type CdpEvent } from "./CdpConnection.js";
|
||||
@@ -331,7 +331,7 @@ const ingestScript = (
|
||||
): void => {
|
||||
state.scriptsObserved += 1;
|
||||
const value = recordValue(event.params);
|
||||
const rawUrl = stringValue(value?.url);
|
||||
const rawUrl = cdpStringValue(value?.url);
|
||||
if (rawUrl === undefined || rawUrl === "") {
|
||||
state.invalidScripts += 1;
|
||||
retainEvent(state, 0);
|
||||
@@ -340,7 +340,7 @@ const ingestScript = (
|
||||
const draft: ScriptDraft = {
|
||||
rawUrl,
|
||||
executionContextKey: contextKey(value?.executionContextId),
|
||||
cdpHash: stringValue(value?.hash) ?? null,
|
||||
cdpHash: cdpStringValue(value?.hash) ?? null,
|
||||
length: nonnegativeInteger(value?.length),
|
||||
isModule: value?.isModule === true,
|
||||
};
|
||||
@@ -375,7 +375,7 @@ const ingestContext = (
|
||||
}
|
||||
const origin =
|
||||
event.method === "Runtime.executionContextCreated"
|
||||
? (stringValue(runtimeContext?.origin) ?? null)
|
||||
? (cdpStringValue(runtimeContext?.origin) ?? null)
|
||||
: null;
|
||||
const draft: ContextDraft = {
|
||||
contextKey: key,
|
||||
|
||||
@@ -14,6 +14,10 @@ import type {
|
||||
import { webSourceMapsSchema } from "../domain/webBundleAnalysis.js";
|
||||
import { createWebTextArtifact } from "../domain/webContentArtifact.js";
|
||||
import { safeParseJson } from "../domain/safeJson.js";
|
||||
import {
|
||||
flattenSourceMapLeaves,
|
||||
isVersion3Map,
|
||||
} from "../domain/sourceMapEnvelope.js";
|
||||
|
||||
export interface WebSourceMapRequest {
|
||||
readonly scriptKey: string;
|
||||
@@ -278,32 +282,13 @@ const validSourceMapEnvelope = (text: string): boolean => {
|
||||
const parsedResult = safeParseJson(text);
|
||||
if (!parsedResult.ok) return false;
|
||||
const parsed: unknown = parsedResult.value;
|
||||
if (!isRecord(parsed) || parsed.version !== 3) return false;
|
||||
if (typeof parsed.mappings === "string") return validSourceMapLeaf(parsed);
|
||||
if (!Array.isArray(parsed.sections)) return false;
|
||||
const pending: unknown[] = [...parsed.sections];
|
||||
while (pending.length > 0) {
|
||||
const section = pending.pop();
|
||||
if (
|
||||
!isRecord(section) ||
|
||||
!isRecord(section.offset) ||
|
||||
!validSourceMapOffset(section.offset.line) ||
|
||||
!validSourceMapOffset(section.offset.column) ||
|
||||
!("map" in section)
|
||||
)
|
||||
return false;
|
||||
const map = section.map;
|
||||
if (!isRecord(map) || map.version !== 3) return false;
|
||||
if (Array.isArray(map.sections))
|
||||
for (const child of map.sections) pending.push(child);
|
||||
else if (!validSourceMapLeaf(map)) return false;
|
||||
}
|
||||
return true;
|
||||
if (isVersion3Map(parsed) && typeof parsed.mappings === "string")
|
||||
return validSourceMapLeaf(parsed);
|
||||
const leaves = flattenSourceMapLeaves(parsed, { validateOffsets: true });
|
||||
if (leaves === undefined) return false;
|
||||
return leaves.every(validSourceMapLeaf);
|
||||
};
|
||||
|
||||
const validSourceMapOffset = (value: unknown): boolean =>
|
||||
typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
||||
|
||||
const validSourceMapLeaf = (map: Readonly<Record<string, unknown>>): boolean =>
|
||||
typeof map.mappings === "string" &&
|
||||
Array.isArray(map.sources) &&
|
||||
@@ -380,9 +365,6 @@ const sourceMediaType = (source: string | null): string =>
|
||||
? "text/typescript"
|
||||
: "text/javascript";
|
||||
|
||||
const isRecord = (value: unknown): value is Readonly<Record<string, unknown>> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
const originalDependency = (
|
||||
node: t.Node,
|
||||
): {
|
||||
|
||||
@@ -239,9 +239,9 @@ const semanticValue = (
|
||||
if (node.kind === "ipc-channel")
|
||||
return properties.resolution === "dynamic"
|
||||
? null
|
||||
: (stringValue(properties.channel) ?? firstLabel(node));
|
||||
: (nonEmptyString(properties.channel) ?? firstLabel(node));
|
||||
if (node.kind === "context-bridge-api")
|
||||
return firstLabel(node) ?? stringValue(properties.api_name);
|
||||
return firstLabel(node) ?? nonEmptyString(properties.api_name);
|
||||
if (node.kind === "native-export")
|
||||
return (
|
||||
joined(
|
||||
@@ -258,8 +258,8 @@ const semanticValue = (
|
||||
].includes(node.kind)
|
||||
)
|
||||
return joined(properties.declared_path, properties.resolved_path);
|
||||
if (node.kind === "package") return stringValue(properties.name);
|
||||
return canonicalPath(node) ?? stringValue(properties.path);
|
||||
if (node.kind === "package") return nonEmptyString(properties.name);
|
||||
return canonicalPath(node) ?? nonEmptyString(properties.path);
|
||||
};
|
||||
|
||||
const canonicalPath = (node: ApplicationNode): string | null =>
|
||||
@@ -286,11 +286,11 @@ const uniqueStringProperty = (
|
||||
};
|
||||
|
||||
const joined = (...values: readonly unknown[]): string | null => {
|
||||
const strings = values.map(stringValue);
|
||||
const strings = values.map(nonEmptyString);
|
||||
return strings.some((value) => value === null) ? null : strings.join("\0");
|
||||
};
|
||||
|
||||
const stringValue = (value: unknown): string | null =>
|
||||
const nonEmptyString = (value: unknown): string | null =>
|
||||
typeof value === "string" && value !== "" ? value : null;
|
||||
|
||||
const stringList = (value: unknown): string[] =>
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as t from "@babel/types";
|
||||
|
||||
import { compareCodePoints } from "./canonicalOrdering.js";
|
||||
import { propertyName } from "./javascriptAstValues.js";
|
||||
import { stringValue } from "./javascriptStaticAnalysisHelpers.js";
|
||||
|
||||
/** Static CommonJS export names. */
|
||||
export interface StaticExports {
|
||||
@@ -123,6 +124,3 @@ const memberPath = (node: t.Node | null | undefined): string => {
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
const stringValue = (node: t.Node | null | undefined): string | undefined =>
|
||||
t.isStringLiteral(node) ? node.value : undefined;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { flattenSourceMapLeaves, isVersion3Map } from "./sourceMapEnvelope.js";
|
||||
|
||||
const leaf = {
|
||||
version: 3,
|
||||
mappings: "AAAA",
|
||||
sources: ["app.ts"],
|
||||
names: [],
|
||||
};
|
||||
|
||||
describe("source-map envelope walk", () => {
|
||||
it("accepts version-3 maps and rejects other shapes", () => {
|
||||
expect(isVersion3Map(leaf)).toBe(true);
|
||||
expect(isVersion3Map({ version: 2, mappings: "AAAA" })).toBe(false);
|
||||
expect(isVersion3Map(null)).toBe(false);
|
||||
expect(isVersion3Map([{ version: 3 }])).toBe(false);
|
||||
});
|
||||
|
||||
it("collects indexed section leaves without validating offsets by default", () => {
|
||||
const root = {
|
||||
version: 3,
|
||||
sections: [
|
||||
{ offset: { line: 0, column: 0 }, map: leaf },
|
||||
{ offset: { line: 10, column: 0 }, map: { ...leaf, sources: [] } },
|
||||
],
|
||||
};
|
||||
expect(flattenSourceMapLeaves(root)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("rejects non-map sections and enforces offsets when requested", () => {
|
||||
const missing = {
|
||||
version: 3,
|
||||
sections: [{ offset: { line: 0, column: 0 } }],
|
||||
};
|
||||
expect(flattenSourceMapLeaves(missing)).toBeUndefined();
|
||||
const badOffset = {
|
||||
version: 3,
|
||||
sections: [{ offset: { line: -1, column: 0 }, map: leaf }],
|
||||
};
|
||||
expect(
|
||||
flattenSourceMapLeaves(badOffset, { validateOffsets: true }),
|
||||
).toBeUndefined();
|
||||
expect(flattenSourceMapLeaves(badOffset)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("rejects non-version-3 roots and nested maps", () => {
|
||||
expect(flattenSourceMapLeaves({ version: 3 })).toHaveLength(1);
|
||||
expect(flattenSourceMapLeaves({ mappings: "AAAA" })).toBeUndefined();
|
||||
expect(
|
||||
flattenSourceMapLeaves({
|
||||
version: 3,
|
||||
sections: [{ offset: { line: 0, column: 0 }, map: { version: 2 } }],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Structural walk over indexed source-map envelopes. Both the browser
|
||||
* source-map fetcher and the artifact source-map reader accept version-3
|
||||
* maps with recursively indexed `sections`; this module owns that walk so
|
||||
* the two validators cannot drift. Leaf content checks (mappings, sources,
|
||||
* names, contents) stay with the callers, which enforce different
|
||||
* strictness.
|
||||
*/
|
||||
type SourceMapRecord = Readonly<Record<string, unknown>>;
|
||||
|
||||
const isRecord = (value: unknown): value is SourceMapRecord =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
/** Whether a parsed value has the version-3 map shape. */
|
||||
export const isVersion3Map = (value: unknown): value is SourceMapRecord =>
|
||||
isRecord(value) && value.version === 3;
|
||||
|
||||
const isValidOffset = (value: unknown): boolean =>
|
||||
typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
||||
|
||||
export interface FlattenSourceMapOptions {
|
||||
/** Require each section offset to carry safe-integer line/column. */
|
||||
readonly validateOffsets?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect leaf maps from an indexed source-map envelope, or undefined when
|
||||
* the envelope is not a version-3 map. A map carrying neither `mappings`
|
||||
* nor `sections` yields itself as the single leaf; callers decide whether
|
||||
* that leaf is valid content.
|
||||
*/
|
||||
export const flattenSourceMapLeaves = (
|
||||
root: unknown,
|
||||
options?: FlattenSourceMapOptions,
|
||||
): readonly SourceMapRecord[] | undefined => {
|
||||
if (!isVersion3Map(root)) return undefined;
|
||||
const leaves: SourceMapRecord[] = [];
|
||||
const pending: unknown[] = [root];
|
||||
while (pending.length > 0) {
|
||||
const map = pending.pop();
|
||||
if (!isVersion3Map(map)) return undefined;
|
||||
if (Array.isArray(map.sections)) {
|
||||
for (const section of map.sections) {
|
||||
if (!isRecord(section) || !isRecord(section.map)) return undefined;
|
||||
if (options?.validateOffsets === true) {
|
||||
if (!isRecord(section.offset)) return undefined;
|
||||
if (
|
||||
!isValidOffset(section.offset.line) ||
|
||||
!isValidOffset(section.offset.column)
|
||||
)
|
||||
return undefined;
|
||||
}
|
||||
pending.push(section.map);
|
||||
}
|
||||
} else leaves.push(map);
|
||||
}
|
||||
return leaves;
|
||||
};
|
||||
Reference in New Issue
Block a user