import { digestCanonicalValue } from "../domain/canonicalDigest.js"; import { createWebTextArtifact } from "../domain/webContentArtifact.js"; import { authorizedElectronFile } from "./ElectronFileScope.js"; import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js"; const MAX_SHEETS = 1024; const MAX_SHEET_BYTES = 8 * 1024 * 1024; const MAX_TOTAL_BYTES = 64 * 1024 * 1024; /** Retain only stylesheet lifecycle metadata from the selected target session. */ export const ingestElectronStyleEvent = (event, stylesMap) => { if (event?.method === "CSS.styleSheetRemoved") { const styleSheetId = stringValue(recordValue(event.params)?.styleSheetId); if (styleSheetId !== undefined) stylesMap.delete(styleSheetId); return; } if (event?.method !== "CSS.styleSheetAdded") return; const header = recordValue(recordValue(event.params)?.header); const styleSheetId = stringValue(header?.styleSheetId); if (styleSheetId === undefined || styleSheetId === "") return; stylesMap.set(styleSheetId, { styleSheetId, frameId: stringValue(header?.frameId) ?? null, sourceURL: stringValue(header?.sourceURL) ?? null, origin: stringValue(header?.origin) ?? null, isInline: header?.isInline === true, isConstructed: header?.isConstructed === true, }); }; /** Capture CSS text only for authorized file frames and explicit source opt-in. */ export const captureElectronStyles = async (input) => { const styles = [...input.styles.values()]; const items = []; let excluded = 0; let sourceFailures = 0; let truncated = false; let includedBytes = 0; let sourceRequests = 0; const limitationNotes = new Set(); void input.completeness; limitationNotes.add("Stylesheet inventory includes only CSS.styleSheetAdded events still present at the capture snapshot."); for (const style of styles) { const scoped = await authorizedStyle(style, input.frameIds); if (scoped === undefined) { excluded += 1; continue; } if (input.request?.include_style_sources !== true) continue; const identity = { style_sheet_id: style.styleSheetId, frame_id: scoped.frameId, file_path: scoped.filePath, origin: style.origin, is_inline: style.isInline, is_constructed: style.isConstructed, }; const item = { style_sheet_key: `electron_stylesheet_${digestCanonicalValue(identity, "CDP capture")}`, style_sheet_id: style.styleSheetId, frame_id: scoped.frameId, file_path: scoped.filePath, origin: style.origin, is_inline: style.isInline, is_constructed: style.isConstructed, source: { included: false, reason: "CSS source was not requested" }, }; if (sourceRequests >= MAX_SHEETS) { sourceFailures += 1; truncated = true; item.source = { included: false, reason: "stylesheet source count limit reached" }; limitationNotes.add(`CSS source capture reached the ${String(MAX_SHEETS)} stylesheet limit.`); items.push(item); continue; } sourceRequests += 1; let sourceText; try { const result = await input.connection.send( "CSS.getStyleSheetText", { styleSheetId: style.styleSheetId }, input.sessionId, input.signal ); sourceText = stringValue(recordValue(result)?.text); } catch (cause) { if (input.signal?.aborted === true) throw cause; sourceFailures += 1; item.source = { included: false, reason: "CSS.getStyleSheetText failed" }; limitationNotes.add("CSS text could not be retrieved for one or more authorized stylesheets."); items.push(item); continue; } if (sourceText === undefined) { sourceFailures += 1; item.source = { included: false, reason: "CSS.getStyleSheetText returned invalid text" }; limitationNotes.add("CSS text was invalid or absent for one or more authorized stylesheets."); items.push(item); continue; } const bytes = Buffer.byteLength(sourceText, "utf8"); if (bytes > MAX_SHEET_BYTES) { sourceFailures += 1; truncated = true; item.source = { included: false, reason: "stylesheet source exceeds the per-stylesheet byte limit" }; limitationNotes.add(`CSS source capture omitted text larger than ${String(MAX_SHEET_BYTES)} bytes per stylesheet.`); items.push(item); continue; } if (includedBytes + bytes > MAX_TOTAL_BYTES) { sourceFailures += 1; truncated = true; item.source = { included: false, reason: "aggregate stylesheet source byte limit reached" }; limitationNotes.add(`CSS source capture omitted text after reaching the ${String(MAX_TOTAL_BYTES)} byte aggregate limit.`); items.push(item); continue; } includedBytes += bytes; item.source = { included: true, artifact: createWebTextArtifact(sourceText, "text/css") }; items.push(item); } if (input.request?.include_style_sources !== true) limitationNotes.add("Stylesheet source capture was not selected; no CSS.getStyleSheetText requests were sent."); if (excluded > 0) limitationNotes.add(`${String(excluded)} stylesheet(s) were excluded because their origin or frame was outside the authorized local scope.`); if (sourceFailures > 0) limitationNotes.add(`${String(sourceFailures)} stylesheet source(s) failed retrieval or exceeded a source budget.`); input.limitations.push(...limitationNotes); return { total: styles.length, items: items.sort((left, right) => compareUnicodeCodePoints(left.style_sheet_key, right.style_sheet_key)), excluded, source_failures: sourceFailures, truncated, limits: { max_sheets: MAX_SHEETS, max_sheet_bytes: MAX_SHEET_BYTES, max_total_bytes: MAX_TOTAL_BYTES, }, }; }; const authorizedStyle = async (style, frameIds) => { if (style.frameId === null || !frameIds.has(style.frameId) || style.origin !== "regular") return undefined; const sourceURL = style.sourceURL ?? ""; if (sourceURL === "") { if (!style.isInline && !style.isConstructed) return undefined; return { frameId: style.frameId, filePath: null }; } const filePath = await authorizedElectronFile(sourceURL); return filePath === undefined ? undefined : { frameId: style.frameId, filePath }; }; const recordValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : undefined; const stringValue = (value) => typeof value === "string" ? value : undefined;