diff --git a/apps/extension/src/content/__tests__/record-frame-agent.test.ts b/apps/extension/src/content/__tests__/record-frame-agent.test.ts index d81479f..0c0a651 100644 --- a/apps/extension/src/content/__tests__/record-frame-agent.test.ts +++ b/apps/extension/src/content/__tests__/record-frame-agent.test.ts @@ -1,7 +1,7 @@ import { afterEach, describe, expect, it, vi } from "vitest"; -import { RECORD_DOCUMENT_ATTRIBUTE } from "@/lib/recording/document-marker"; import type { RecordFramePortMessage } from "@/lib/recording/frame-bridge"; import { RECORD_FRAME_START } from "@/lib/recording/frame-bridge"; +import { RECORD_DOCUMENT_ATTRIBUTE } from "@/shared/recording-document-identity"; import { RecordFrameAgent } from "../recording/frame-agent"; class PortListeners unknown> { diff --git a/apps/extension/src/lib/__tests__/recording-document-marker.test.ts b/apps/extension/src/lib/__tests__/recording-document-marker.test.ts index cc91fb9..53fb2af 100644 --- a/apps/extension/src/lib/__tests__/recording-document-marker.test.ts +++ b/apps/extension/src/lib/__tests__/recording-document-marker.test.ts @@ -1,14 +1,26 @@ import { describe, expect, it } from "vitest"; -import { RECORD_DOCUMENT_ATTRIBUTE } from "@/shared/recording-document-identity"; +import { + RECORD_DOCUMENT_ATTRIBUTE, + recordingDocumentMarkerValue, +} from "@/shared/recording-document-identity"; import { markRecordingDocument } from "../recording/document-marker"; import { ObservationNodeIndex } from "../recording/observation-capture"; describe("recording document marker", () => { + it("rejects a non-random Document identity", () => { + expect(() => markRecordingDocument("user@example.com")).toThrow( + "recording Document identity must be a random UUID", + ); + }); + it("restores the Document attribute exactly", () => { document.documentElement.setAttribute(RECORD_DOCUMENT_ATTRIBUTE, "page-value"); - const marker = markRecordingDocument("producer-1"); + const producerId = "123e4567-e89b-42d3-a456-426614174000"; + const marker = markRecordingDocument(producerId); - expect(document.documentElement.getAttribute(RECORD_DOCUMENT_ATTRIBUTE)).toBe("producer-1"); + expect(document.documentElement.getAttribute(RECORD_DOCUMENT_ATTRIBUTE)).toBe( + recordingDocumentMarkerValue(producerId), + ); marker.restore(); expect(document.documentElement.getAttribute(RECORD_DOCUMENT_ATTRIBUTE)).toBe("page-value"); document.documentElement.removeAttribute(RECORD_DOCUMENT_ATTRIBUTE); diff --git a/apps/extension/src/lib/recording/document-marker.ts b/apps/extension/src/lib/recording/document-marker.ts index b1b3226..f8f383f 100644 --- a/apps/extension/src/lib/recording/document-marker.ts +++ b/apps/extension/src/lib/recording/document-marker.ts @@ -1,6 +1,7 @@ -import { RECORD_DOCUMENT_ATTRIBUTE } from "@/shared/recording-document-identity"; - -export { RECORD_DOCUMENT_ATTRIBUTE } from "@/shared/recording-document-identity"; +import { + RECORD_DOCUMENT_ATTRIBUTE, + recordingDocumentMarkerValue, +} from "@/shared/recording-document-identity"; export interface RecordingDocumentMarker { restore(): void; @@ -24,12 +25,13 @@ export function markRecordingDocument( root: HTMLElement = document.documentElement, ): RecordingDocumentMarker { const previous = root.getAttribute(RECORD_DOCUMENT_ATTRIBUTE); - const ensure = () => root.setAttribute(RECORD_DOCUMENT_ATTRIBUTE, producerId); + const markerValue = recordingDocumentMarkerValue(producerId); + const ensure = () => root.setAttribute(RECORD_DOCUMENT_ATTRIBUTE, markerValue); ensure(); return { ensure, restore() { - if (root.getAttribute(RECORD_DOCUMENT_ATTRIBUTE) !== producerId) return; + if (root.getAttribute(RECORD_DOCUMENT_ATTRIBUTE) !== markerValue) return; if (previous === null) root.removeAttribute(RECORD_DOCUMENT_ATTRIBUTE); else root.setAttribute(RECORD_DOCUMENT_ATTRIBUTE, previous); }, diff --git a/apps/extension/src/lib/recording/observation-capture.ts b/apps/extension/src/lib/recording/observation-capture.ts index efafad4..8423f43 100644 --- a/apps/extension/src/lib/recording/observation-capture.ts +++ b/apps/extension/src/lib/recording/observation-capture.ts @@ -1,4 +1,5 @@ import type { RenderedRef } from "@browser-skill/vom"; +import type { CdpTarget } from "@/browser-driver/frame-graph"; import { type CaptureVomMatchNode, type CaptureVomObservationResult, diff --git a/apps/extension/src/shared/recording-document-identity.ts b/apps/extension/src/shared/recording-document-identity.ts index a044a5d..6cd3bc2 100644 --- a/apps/extension/src/shared/recording-document-identity.ts +++ b/apps/extension/src/shared/recording-document-identity.ts @@ -1,9 +1,22 @@ export const RECORD_DOCUMENT_ATTRIBUTE = "data-bsk-record-document"; +const RECORDING_DOCUMENT_ID_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; +const RECORDING_DOCUMENT_MARKER_PREFIX = "bsk:"; + +export function recordingDocumentMarkerValue(identity: string): string { + if (!RECORDING_DOCUMENT_ID_PATTERN.test(identity)) { + throw new TypeError("recording Document identity must be a random UUID"); + } + return `${RECORDING_DOCUMENT_MARKER_PREFIX}${identity}`; +} + /** Read BrowserSkill's opaque per-Document recording identity from captured attributes. */ export function readRecordingDocumentIdentity( attrs: Readonly>, ): string | undefined { - const identity = attrs[RECORD_DOCUMENT_ATTRIBUTE]; - return identity ? identity : undefined; + const marker = attrs[RECORD_DOCUMENT_ATTRIBUTE]; + if (!marker?.startsWith(RECORDING_DOCUMENT_MARKER_PREFIX)) return undefined; + const identity = marker.slice(RECORDING_DOCUMENT_MARKER_PREFIX.length); + return identity && RECORDING_DOCUMENT_ID_PATTERN.test(identity) ? identity : undefined; } diff --git a/apps/extension/src/tools/vom/__tests__/record-safe-observation.test.ts b/apps/extension/src/tools/vom/__tests__/record-safe-observation.test.ts index 2889d9e..4401792 100644 --- a/apps/extension/src/tools/vom/__tests__/record-safe-observation.test.ts +++ b/apps/extension/src/tools/vom/__tests__/record-safe-observation.test.ts @@ -1,9 +1,13 @@ import { describe, expect, it } from "vitest"; -import { RECORD_DOCUMENT_ATTRIBUTE } from "@/shared/recording-document-identity"; +import { + RECORD_DOCUMENT_ATTRIBUTE, + recordingDocumentMarkerValue, +} from "@/shared/recording-document-identity"; import { projectRecordSafeObservation } from "../record-safe-observation"; describe("projectRecordSafeObservation", () => { it("projects the opaque recording identity without exposing captured attributes", () => { + const recordingDocumentId = "123e4567-e89b-42d3-a456-426614174000"; const result = projectRecordSafeObservation({ rootFrameId: "child", frameDocuments: [ @@ -18,7 +22,7 @@ describe("projectRecordSafeObservation", () => { parentBackendNodeId: null, tag: "html", attrs: { - [RECORD_DOCUMENT_ATTRIBUTE]: "producer-1", + [RECORD_DOCUMENT_ATTRIBUTE]: recordingDocumentMarkerValue(recordingDocumentId), value: "user@example.com", }, rect: null, @@ -36,7 +40,7 @@ describe("projectRecordSafeObservation", () => { { frameId: "child", target: { tabId: 4, sessionId: "oopif-session" }, - recordingDocumentId: "producer-1", + recordingDocumentId, }, ]); const dumped = JSON.stringify(result); @@ -45,4 +49,34 @@ describe("projectRecordSafeObservation", () => { expect(dumped).not.toContain("domNodes"); expect(dumped).not.toContain("axNodes"); }); + + it("does not project a page-controlled marker value as recording identity", () => { + const result = projectRecordSafeObservation({ + rootFrameId: "root", + frameDocuments: [ + { + frameId: "root", + target: { tabId: 4 }, + contextScopeId: "scope-1", + axNodes: [], + domNodes: [ + { + backendNodeId: 1, + parentBackendNodeId: null, + tag: "html", + attrs: { [RECORD_DOCUMENT_ATTRIBUTE]: "bsk:user@example.com" }, + rect: null, + paintOrder: 0, + position: "static", + pointerEvents: "auto", + }, + ], + }, + ], + rendered: { text: '@vom 1\nRootWebArea "Example"', refs: [], truncated: false }, + }); + + expect(result.frames).toEqual([{ frameId: "root", target: { tabId: 4 } }]); + expect(JSON.stringify(result)).not.toContain("user@example.com"); + }); }); diff --git a/apps/extension/src/tools/vom/record-safe-observation.ts b/apps/extension/src/tools/vom/record-safe-observation.ts index b4fd093..d58e757 100644 --- a/apps/extension/src/tools/vom/record-safe-observation.ts +++ b/apps/extension/src/tools/vom/record-safe-observation.ts @@ -41,9 +41,11 @@ export interface CaptureVomObservationResult { function projectFrames(documents: CapturedFrameDocument[]): CaptureVomFrame[] { return documents.map((document) => { - const recordingDocumentId = document.domNodes - .map((node) => readRecordingDocumentIdentity(node.attrs)) - .find((identity) => identity !== undefined); + let recordingDocumentId: string | undefined; + for (const node of document.domNodes) { + recordingDocumentId = readRecordingDocumentIdentity(node.attrs); + if (recordingDocumentId) break; + } return { frameId: document.frameId, target: document.target,