mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
fix(record): saftey policy and rebase bug fix
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
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 type { RecordFramePortMessage } from "@/lib/recording/frame-bridge";
|
||||||
import { RECORD_FRAME_START } 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";
|
import { RecordFrameAgent } from "../recording/frame-agent";
|
||||||
|
|
||||||
class PortListeners<T extends (...args: never[]) => unknown> {
|
class PortListeners<T extends (...args: never[]) => unknown> {
|
||||||
|
|||||||
@@ -1,14 +1,26 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
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 { markRecordingDocument } from "../recording/document-marker";
|
||||||
import { ObservationNodeIndex } from "../recording/observation-capture";
|
import { ObservationNodeIndex } from "../recording/observation-capture";
|
||||||
|
|
||||||
describe("recording document marker", () => {
|
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", () => {
|
it("restores the Document attribute exactly", () => {
|
||||||
document.documentElement.setAttribute(RECORD_DOCUMENT_ATTRIBUTE, "page-value");
|
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();
|
marker.restore();
|
||||||
expect(document.documentElement.getAttribute(RECORD_DOCUMENT_ATTRIBUTE)).toBe("page-value");
|
expect(document.documentElement.getAttribute(RECORD_DOCUMENT_ATTRIBUTE)).toBe("page-value");
|
||||||
document.documentElement.removeAttribute(RECORD_DOCUMENT_ATTRIBUTE);
|
document.documentElement.removeAttribute(RECORD_DOCUMENT_ATTRIBUTE);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { RECORD_DOCUMENT_ATTRIBUTE } from "@/shared/recording-document-identity";
|
import {
|
||||||
|
RECORD_DOCUMENT_ATTRIBUTE,
|
||||||
export { RECORD_DOCUMENT_ATTRIBUTE } from "@/shared/recording-document-identity";
|
recordingDocumentMarkerValue,
|
||||||
|
} from "@/shared/recording-document-identity";
|
||||||
|
|
||||||
export interface RecordingDocumentMarker {
|
export interface RecordingDocumentMarker {
|
||||||
restore(): void;
|
restore(): void;
|
||||||
@@ -24,12 +25,13 @@ export function markRecordingDocument(
|
|||||||
root: HTMLElement = document.documentElement,
|
root: HTMLElement = document.documentElement,
|
||||||
): RecordingDocumentMarker {
|
): RecordingDocumentMarker {
|
||||||
const previous = root.getAttribute(RECORD_DOCUMENT_ATTRIBUTE);
|
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();
|
ensure();
|
||||||
return {
|
return {
|
||||||
ensure,
|
ensure,
|
||||||
restore() {
|
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);
|
if (previous === null) root.removeAttribute(RECORD_DOCUMENT_ATTRIBUTE);
|
||||||
else root.setAttribute(RECORD_DOCUMENT_ATTRIBUTE, previous);
|
else root.setAttribute(RECORD_DOCUMENT_ATTRIBUTE, previous);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { RenderedRef } from "@browser-skill/vom";
|
import type { RenderedRef } from "@browser-skill/vom";
|
||||||
|
import type { CdpTarget } from "@/browser-driver/frame-graph";
|
||||||
import {
|
import {
|
||||||
type CaptureVomMatchNode,
|
type CaptureVomMatchNode,
|
||||||
type CaptureVomObservationResult,
|
type CaptureVomObservationResult,
|
||||||
|
|||||||
@@ -1,9 +1,22 @@
|
|||||||
export const RECORD_DOCUMENT_ATTRIBUTE = "data-bsk-record-document";
|
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. */
|
/** Read BrowserSkill's opaque per-Document recording identity from captured attributes. */
|
||||||
export function readRecordingDocumentIdentity(
|
export function readRecordingDocumentIdentity(
|
||||||
attrs: Readonly<Record<string, string>>,
|
attrs: Readonly<Record<string, string>>,
|
||||||
): string | undefined {
|
): string | undefined {
|
||||||
const identity = attrs[RECORD_DOCUMENT_ATTRIBUTE];
|
const marker = attrs[RECORD_DOCUMENT_ATTRIBUTE];
|
||||||
return identity ? identity : undefined;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
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";
|
import { projectRecordSafeObservation } from "../record-safe-observation";
|
||||||
|
|
||||||
describe("projectRecordSafeObservation", () => {
|
describe("projectRecordSafeObservation", () => {
|
||||||
it("projects the opaque recording identity without exposing captured attributes", () => {
|
it("projects the opaque recording identity without exposing captured attributes", () => {
|
||||||
|
const recordingDocumentId = "123e4567-e89b-42d3-a456-426614174000";
|
||||||
const result = projectRecordSafeObservation({
|
const result = projectRecordSafeObservation({
|
||||||
rootFrameId: "child",
|
rootFrameId: "child",
|
||||||
frameDocuments: [
|
frameDocuments: [
|
||||||
@@ -18,7 +22,7 @@ describe("projectRecordSafeObservation", () => {
|
|||||||
parentBackendNodeId: null,
|
parentBackendNodeId: null,
|
||||||
tag: "html",
|
tag: "html",
|
||||||
attrs: {
|
attrs: {
|
||||||
[RECORD_DOCUMENT_ATTRIBUTE]: "producer-1",
|
[RECORD_DOCUMENT_ATTRIBUTE]: recordingDocumentMarkerValue(recordingDocumentId),
|
||||||
value: "user@example.com",
|
value: "user@example.com",
|
||||||
},
|
},
|
||||||
rect: null,
|
rect: null,
|
||||||
@@ -36,7 +40,7 @@ describe("projectRecordSafeObservation", () => {
|
|||||||
{
|
{
|
||||||
frameId: "child",
|
frameId: "child",
|
||||||
target: { tabId: 4, sessionId: "oopif-session" },
|
target: { tabId: 4, sessionId: "oopif-session" },
|
||||||
recordingDocumentId: "producer-1",
|
recordingDocumentId,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
const dumped = JSON.stringify(result);
|
const dumped = JSON.stringify(result);
|
||||||
@@ -45,4 +49,34 @@ describe("projectRecordSafeObservation", () => {
|
|||||||
expect(dumped).not.toContain("domNodes");
|
expect(dumped).not.toContain("domNodes");
|
||||||
expect(dumped).not.toContain("axNodes");
|
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");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -41,9 +41,11 @@ export interface CaptureVomObservationResult {
|
|||||||
|
|
||||||
function projectFrames(documents: CapturedFrameDocument<FrameAxNode>[]): CaptureVomFrame[] {
|
function projectFrames(documents: CapturedFrameDocument<FrameAxNode>[]): CaptureVomFrame[] {
|
||||||
return documents.map((document) => {
|
return documents.map((document) => {
|
||||||
const recordingDocumentId = document.domNodes
|
let recordingDocumentId: string | undefined;
|
||||||
.map((node) => readRecordingDocumentIdentity(node.attrs))
|
for (const node of document.domNodes) {
|
||||||
.find((identity) => identity !== undefined);
|
recordingDocumentId = readRecordingDocumentIdentity(node.attrs);
|
||||||
|
if (recordingDocumentId) break;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
frameId: document.frameId,
|
frameId: document.frameId,
|
||||||
target: document.target,
|
target: document.target,
|
||||||
|
|||||||
Reference in New Issue
Block a user