feat(agent-browser): add opt-in preview data injection
This commit is contained in:
@@ -15,6 +15,14 @@ import type {
|
||||
AgentBrowserViewPort,
|
||||
PortListener,
|
||||
} from './adapter';
|
||||
import type {
|
||||
PreviewDataInjectionValue,
|
||||
PreviewDataSessionInvalidationListener,
|
||||
PreviewDataSessionInvalidationReason,
|
||||
PreviewDataSessionManager,
|
||||
PreviewDataSessionOpenInput,
|
||||
PreviewDataSessionSnapshot,
|
||||
} from '../services/preview-data-session';
|
||||
import { AgentBrowserCdpGuard } from './cdp-guard';
|
||||
import { AgentBrowserEventBuffer } from './event-buffer';
|
||||
import { AgentBrowserFault } from './fault';
|
||||
@@ -111,14 +119,34 @@ interface BrowserRecord {
|
||||
listener: PortListener;
|
||||
}>;
|
||||
interruptWaiters: Set<(fault: AgentBrowserFault) => void>;
|
||||
previewDataRequested: boolean;
|
||||
previewData?: PreviewDataBinding;
|
||||
}
|
||||
|
||||
type PreviewDataSession = Pick<
|
||||
PreviewDataSessionManager,
|
||||
'open' | 'getInjectionValue' | 'invalidate' | 'subscribeInvalidation'
|
||||
>;
|
||||
|
||||
type PreviewDataScript = Readonly<{
|
||||
identifier: string;
|
||||
sessionRef?: string;
|
||||
}>;
|
||||
|
||||
type PreviewDataBinding = {
|
||||
session: PreviewDataSessionSnapshot;
|
||||
scripts: PreviewDataScript[];
|
||||
invalidated: boolean;
|
||||
cleanup?: Promise<void>;
|
||||
};
|
||||
|
||||
export interface AgentBrowserOpenInput {
|
||||
projectId: string;
|
||||
projectPath: string;
|
||||
url: string;
|
||||
bounds?: AgentBrowserBounds;
|
||||
visible?: boolean;
|
||||
injectProjectData?: boolean;
|
||||
}
|
||||
|
||||
export interface AgentBrowserPresentInput {
|
||||
@@ -160,6 +188,7 @@ export interface AgentBrowserModuleOptions {
|
||||
payloadStore?: AgentBrowserPayloadStore;
|
||||
cdpGuard?: AgentBrowserCdpGuard;
|
||||
onLifecycle?(event: AgentBrowserLifecycleEvent): void;
|
||||
previewDataSession?: PreviewDataSession;
|
||||
}
|
||||
|
||||
export type AgentBrowserLifecycleEvent = Readonly<{
|
||||
@@ -187,6 +216,8 @@ export class AgentBrowserModule {
|
||||
private readonly commandCancellers = new Set<(fault: AgentBrowserFault) => void>();
|
||||
private readonly lifecycleListeners = new Set<AgentBrowserLifecycleListener>();
|
||||
private record: BrowserRecord | null = null;
|
||||
private previewDataSession: PreviewDataSession | undefined;
|
||||
private previewDataSessionUnsubscribe: (() => void) | undefined;
|
||||
private commandTail: Promise<void> = Promise.resolve();
|
||||
private lifecycleBarrier: Promise<void> = Promise.resolve();
|
||||
private queueEpoch = 0;
|
||||
@@ -200,6 +231,21 @@ export class AgentBrowserModule {
|
||||
this.payloadStore = options.payloadStore ?? new AgentBrowserPayloadStore();
|
||||
this.cdpGuard = options.cdpGuard ?? new AgentBrowserCdpGuard();
|
||||
if (options.onLifecycle) this.lifecycleListeners.add(options.onLifecycle);
|
||||
if (options.previewDataSession) this.configurePreviewDataSession(options.previewDataSession);
|
||||
}
|
||||
|
||||
configurePreviewDataSession(session?: PreviewDataSession): void {
|
||||
this.previewDataSessionUnsubscribe?.();
|
||||
this.previewDataSessionUnsubscribe = undefined;
|
||||
this.previewDataSession = session;
|
||||
if (!session) return;
|
||||
const onInvalidated: PreviewDataSessionInvalidationListener = (_reason) => {
|
||||
const record = this.record;
|
||||
if (!record?.previewData || record.previewData.invalidated) return;
|
||||
record.previewData.invalidated = true;
|
||||
void this.removePreviewDataScripts(record, record.previewData).catch(() => undefined);
|
||||
};
|
||||
this.previewDataSessionUnsubscribe = session.subscribeInvalidation(onInvalidated);
|
||||
}
|
||||
|
||||
subscribeLifecycle(listener: AgentBrowserLifecycleListener): () => void {
|
||||
@@ -263,6 +309,8 @@ export class AgentBrowserModule {
|
||||
const projectPath = normalizeRequiredPath(input.projectPath);
|
||||
const targetUrl = normalizeUrl(input.url);
|
||||
const bounds = input.bounds ? normalizeBounds(input.bounds) : null;
|
||||
const injectProjectData = input.injectProjectData === true;
|
||||
if (injectProjectData) assertPreviewDataTarget(targetUrl);
|
||||
|
||||
if (this.record && !samePath(this.record.projectPath, projectPath)) {
|
||||
await this.closeInternal();
|
||||
@@ -281,6 +329,17 @@ export class AgentBrowserModule {
|
||||
await this.closeInternal();
|
||||
}
|
||||
}
|
||||
if (this.record) {
|
||||
const record = this.record;
|
||||
const shouldReplacePreview = injectProjectData
|
||||
? !record.previewData
|
||||
|| record.previewData.invalidated
|
||||
|| !sameOrigin(record.previewData.session.origin, targetUrl)
|
||||
: Boolean(record.previewData);
|
||||
if (shouldReplacePreview) {
|
||||
await this.closeInternal(record);
|
||||
}
|
||||
}
|
||||
if (this.record) {
|
||||
const record = this.record;
|
||||
if (bounds) {
|
||||
@@ -312,6 +371,8 @@ export class AgentBrowserModule {
|
||||
webContentsListeners: [],
|
||||
debuggerListeners: [],
|
||||
interruptWaiters: new Set(),
|
||||
previewDataRequested: injectProjectData,
|
||||
previewData: undefined,
|
||||
};
|
||||
this.record = record;
|
||||
|
||||
@@ -332,6 +393,7 @@ export class AgentBrowserModule {
|
||||
this.attachDebugger(record, false),
|
||||
OPEN_TIMEOUT_MS,
|
||||
);
|
||||
if (injectProjectData) await this.installPreviewData(record);
|
||||
} catch (error) {
|
||||
await this.closeInternal(record);
|
||||
throw toFault(
|
||||
@@ -541,6 +603,9 @@ export class AgentBrowserModule {
|
||||
this.disposed = true;
|
||||
this.preemptCommands('开发浏览器模块已关闭。');
|
||||
await this.closeInternal();
|
||||
this.previewDataSessionUnsubscribe?.();
|
||||
this.previewDataSessionUnsubscribe = undefined;
|
||||
this.previewDataSession = undefined;
|
||||
this.payloadStore.clear();
|
||||
}
|
||||
|
||||
@@ -694,6 +759,7 @@ export class AgentBrowserModule {
|
||||
record.generation = ++this.generation;
|
||||
record.childSessions.clear();
|
||||
record.ioHandles.clear();
|
||||
await this.invalidatePreviewData(record, 'browser_generation_replaced');
|
||||
this.notifyLifecycle({
|
||||
type: 'generation-replaced',
|
||||
projectId: record.projectId,
|
||||
@@ -708,7 +774,7 @@ export class AgentBrowserModule {
|
||||
|
||||
await port.sendCommand('Target.setAutoAttach', {
|
||||
autoAttach: true,
|
||||
waitForDebuggerOnStart: false,
|
||||
waitForDebuggerOnStart: record.previewDataRequested,
|
||||
flatten: true,
|
||||
});
|
||||
if (record.diagnosticsEnabled) {
|
||||
@@ -729,6 +795,195 @@ export class AgentBrowserModule {
|
||||
record.error = undefined;
|
||||
}
|
||||
|
||||
private async installPreviewData(
|
||||
record: BrowserRecord,
|
||||
sessionRef?: string,
|
||||
): Promise<void> {
|
||||
if (record !== this.record || record.view.webContents.isDestroyed()) {
|
||||
throw new AgentBrowserFault(
|
||||
'TARGET_GONE',
|
||||
'开发浏览器页面已关闭。',
|
||||
true,
|
||||
record.generation,
|
||||
);
|
||||
}
|
||||
const previewDataSession = this.previewDataSession;
|
||||
if (!previewDataSession) {
|
||||
throw new Error('Preview data session is not configured');
|
||||
}
|
||||
|
||||
let binding = record.previewData;
|
||||
if (!sessionRef) {
|
||||
const openInput: PreviewDataSessionOpenInput = {
|
||||
projectPath: record.projectPath,
|
||||
origin: new URL(record.url).origin,
|
||||
browserGeneration: record.generation,
|
||||
};
|
||||
const session = await previewDataSession.open(openInput);
|
||||
if (
|
||||
record !== this.record
|
||||
|| record.state === 'closing'
|
||||
|| record.view.webContents.isDestroyed()
|
||||
) {
|
||||
previewDataSession.invalidate('preview_closed');
|
||||
throw new AgentBrowserFault(
|
||||
'CLOSED',
|
||||
'开发浏览器页面在创建数据运行时期间已关闭或切换。',
|
||||
true,
|
||||
record.generation,
|
||||
'unknown',
|
||||
);
|
||||
}
|
||||
const value = previewDataSession.getInjectionValue();
|
||||
if (!value) {
|
||||
previewDataSession.invalidate('manual');
|
||||
throw new Error('Preview data injection value is unavailable');
|
||||
}
|
||||
binding = {
|
||||
session,
|
||||
scripts: [],
|
||||
invalidated: false,
|
||||
};
|
||||
record.previewData = binding;
|
||||
try {
|
||||
await this.addPreviewDataScript(record, binding, value);
|
||||
} catch (error) {
|
||||
await this.invalidatePreviewData(record, 'manual');
|
||||
throw error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!binding || binding.invalidated) return;
|
||||
const value = previewDataSession.getInjectionValue();
|
||||
if (!value) return;
|
||||
await this.addPreviewDataScript(record, binding, value, sessionRef);
|
||||
}
|
||||
|
||||
private async addPreviewDataScript(
|
||||
record: BrowserRecord,
|
||||
binding: PreviewDataBinding,
|
||||
value: PreviewDataInjectionValue,
|
||||
sessionRef?: string,
|
||||
): Promise<void> {
|
||||
const response = await record.view.webContents.debugger.sendCommand(
|
||||
'Page.addScriptToEvaluateOnNewDocument',
|
||||
{ source: previewDataInjectionScript(binding.session.origin, value) },
|
||||
sessionRef,
|
||||
);
|
||||
const identifier = isRecord(response) && typeof response.identifier === 'string'
|
||||
? response.identifier
|
||||
: undefined;
|
||||
if (!identifier) throw new Error('Preview data injection script was not registered');
|
||||
const script = {
|
||||
identifier,
|
||||
...(sessionRef ? { sessionRef } : {}),
|
||||
};
|
||||
if (
|
||||
record !== this.record
|
||||
|| record.view.webContents.isDestroyed()
|
||||
|| binding.invalidated
|
||||
|| record.previewData !== binding
|
||||
) {
|
||||
await record.view.webContents.debugger.sendCommand(
|
||||
'Page.removeScriptToEvaluateOnNewDocument',
|
||||
{ identifier },
|
||||
sessionRef,
|
||||
).catch(() => undefined);
|
||||
throw new AgentBrowserFault(
|
||||
'CLOSED',
|
||||
'开发浏览器页面在安装数据运行时期间已关闭或切换。',
|
||||
true,
|
||||
record.generation,
|
||||
'unknown',
|
||||
);
|
||||
}
|
||||
binding.scripts.push(script);
|
||||
}
|
||||
|
||||
private invalidatePreviewData(
|
||||
record: BrowserRecord,
|
||||
reason: PreviewDataSessionInvalidationReason,
|
||||
): Promise<void> {
|
||||
const binding = record.previewData;
|
||||
if (!binding) return Promise.resolve();
|
||||
if (!binding.invalidated) {
|
||||
binding.invalidated = true;
|
||||
this.previewDataSession?.invalidate(reason);
|
||||
}
|
||||
return this.removePreviewDataScripts(record, binding);
|
||||
}
|
||||
|
||||
private removePreviewDataScripts(
|
||||
record: BrowserRecord,
|
||||
binding: PreviewDataBinding,
|
||||
): Promise<void> {
|
||||
if (binding.cleanup) return binding.cleanup;
|
||||
binding.cleanup = (async () => {
|
||||
record.previewDataRequested = false;
|
||||
const scripts = binding.scripts.splice(0);
|
||||
await Promise.all(scripts.map(async ({ identifier, sessionRef }) => {
|
||||
try {
|
||||
await record.view.webContents.debugger.sendCommand(
|
||||
'Page.removeScriptToEvaluateOnNewDocument',
|
||||
{ identifier },
|
||||
sessionRef,
|
||||
);
|
||||
} catch {
|
||||
// The debugger or child target may already be detached.
|
||||
}
|
||||
if (sessionRef) {
|
||||
await record.view.webContents.debugger.sendCommand(
|
||||
'Runtime.runIfWaitingForDebugger',
|
||||
undefined,
|
||||
sessionRef,
|
||||
).catch(() => undefined);
|
||||
}
|
||||
}));
|
||||
if (
|
||||
record === this.record
|
||||
&& !record.view.webContents.isDestroyed()
|
||||
&& record.view.webContents.debugger.isAttached()
|
||||
) {
|
||||
await record.view.webContents.debugger.sendCommand('Target.setAutoAttach', {
|
||||
autoAttach: true,
|
||||
waitForDebuggerOnStart: false,
|
||||
flatten: true,
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
if (record.previewData === binding) record.previewData = undefined;
|
||||
})();
|
||||
return binding.cleanup;
|
||||
}
|
||||
|
||||
private isPreviewDataCrossOrigin(record: BrowserRecord, nextUrl: string): boolean {
|
||||
const origin = record.previewData?.session.origin;
|
||||
if (!origin) return false;
|
||||
return !sameOrigin(origin, nextUrl);
|
||||
}
|
||||
|
||||
private handleCrossOriginNavigationRequest(
|
||||
record: BrowserRecord,
|
||||
nextUrl: string,
|
||||
eventValue: unknown,
|
||||
): void {
|
||||
if (!this.isPreviewDataCrossOrigin(record, nextUrl)) return;
|
||||
const binding = record.previewData;
|
||||
if (!binding || binding.invalidated) return;
|
||||
const event = preventableEvent(eventValue);
|
||||
event?.preventDefault();
|
||||
const cleanup = this.invalidatePreviewData(record, 'cross_origin_navigation');
|
||||
if (!event) return;
|
||||
void cleanup.then(() => {
|
||||
if (
|
||||
record !== this.record
|
||||
|| record.view.webContents.isDestroyed()
|
||||
|| record.state === 'closing'
|
||||
) return;
|
||||
void this.navigateTo(record, nextUrl).catch(() => undefined);
|
||||
});
|
||||
}
|
||||
|
||||
private registerListeners(record: BrowserRecord): void {
|
||||
const contents = record.view.webContents;
|
||||
const onDebuggerMessage: PortListener = (
|
||||
@@ -747,6 +1002,7 @@ export class AgentBrowserModule {
|
||||
if (record !== this.record || record.state === 'closing' || record.state === 'crashed') {
|
||||
return;
|
||||
}
|
||||
this.invalidatePreviewData(record, 'browser_detached');
|
||||
record.eventBuffer.markGap('debugger-detached');
|
||||
record.childSessions.clear();
|
||||
record.ioHandles.clear();
|
||||
@@ -770,10 +1026,22 @@ export class AgentBrowserModule {
|
||||
this.addDebuggerListener(record, 'message', onDebuggerMessage);
|
||||
this.addDebuggerListener(record, 'detach', onDebuggerDetach);
|
||||
|
||||
this.addWebContentsListener(record, 'will-navigate', (eventValue, urlValue, _isInPlace, isMainFrameValue) => {
|
||||
if (isMainFrameValue === false || typeof urlValue !== 'string') return;
|
||||
this.handleCrossOriginNavigationRequest(record, urlValue, eventValue);
|
||||
});
|
||||
this.addWebContentsListener(record, 'will-redirect', (eventValue, urlValue, _isInPlace, isMainFrameValue) => {
|
||||
if (isMainFrameValue === false || typeof urlValue !== 'string') return;
|
||||
this.handleCrossOriginNavigationRequest(record, urlValue, eventValue);
|
||||
});
|
||||
|
||||
this.addWebContentsListener(record, 'did-navigate', (_event, urlValue) => {
|
||||
const previousUrl = record.url;
|
||||
if (typeof urlValue === 'string') {
|
||||
record.url = urlValue;
|
||||
if (this.isPreviewDataCrossOrigin(record, urlValue)) {
|
||||
this.invalidatePreviewData(record, 'cross_origin_navigation');
|
||||
}
|
||||
if (crossOriginNavigation(previousUrl, urlValue)) {
|
||||
this.notifyLifecycle({
|
||||
type: 'cross-origin-navigation',
|
||||
@@ -792,6 +1060,9 @@ export class AgentBrowserModule {
|
||||
const previousUrl = record.url;
|
||||
if (typeof urlValue === 'string' && isMainFrameValue !== false) {
|
||||
record.url = urlValue;
|
||||
if (this.isPreviewDataCrossOrigin(record, urlValue)) {
|
||||
this.invalidatePreviewData(record, 'cross_origin_navigation');
|
||||
}
|
||||
if (crossOriginNavigation(previousUrl, urlValue)) {
|
||||
this.notifyLifecycle({
|
||||
type: 'cross-origin-navigation',
|
||||
@@ -845,6 +1116,7 @@ export class AgentBrowserModule {
|
||||
});
|
||||
this.addWebContentsListener(record, 'render-process-gone', () => {
|
||||
if (record !== this.record || record.state === 'closing') return;
|
||||
this.invalidatePreviewData(record, 'browser_crashed');
|
||||
record.state = 'crashed';
|
||||
record.error = {
|
||||
code: 'RENDERER_CRASHED',
|
||||
@@ -864,6 +1136,7 @@ export class AgentBrowserModule {
|
||||
});
|
||||
this.addWebContentsListener(record, 'destroyed', () => {
|
||||
if (record !== this.record || record.state === 'closing') return;
|
||||
this.invalidatePreviewData(record, 'browser_crashed');
|
||||
record.state = 'crashed';
|
||||
record.error = {
|
||||
code: 'TARGET_GONE',
|
||||
@@ -949,11 +1222,63 @@ export class AgentBrowserModule {
|
||||
if (!isRecord(params)) return;
|
||||
if (method === 'Target.attachedToTarget' && typeof params.sessionId === 'string') {
|
||||
record.childSessions.add(params.sessionId);
|
||||
const previewBinding = record.previewData;
|
||||
if (previewBinding && !previewBinding.invalidated && isDocumentTarget(params)) {
|
||||
void this.installPreviewData(record, params.sessionId)
|
||||
.then(async () => {
|
||||
if (params.waitingForDebugger !== true
|
||||
|| record !== this.record
|
||||
|| record.view.webContents.isDestroyed()) return;
|
||||
// If invalidation won the race with script registration, wait for
|
||||
// removal before releasing the paused child target. It must never
|
||||
// execute with a token after the session has been invalidated.
|
||||
await previewBinding.cleanup;
|
||||
if (record !== this.record || record.view.webContents.isDestroyed()) return;
|
||||
if (record.previewData?.invalidated) return;
|
||||
await record.view.webContents.debugger.sendCommand(
|
||||
'Runtime.runIfWaitingForDebugger',
|
||||
undefined,
|
||||
params.sessionId as string,
|
||||
);
|
||||
})
|
||||
.catch(async () => {
|
||||
await this.invalidatePreviewData(record, 'manual');
|
||||
if (
|
||||
params.waitingForDebugger === true
|
||||
&& record === this.record
|
||||
&& !record.view.webContents.isDestroyed()
|
||||
) {
|
||||
await record.view.webContents.debugger.sendCommand(
|
||||
'Runtime.runIfWaitingForDebugger',
|
||||
undefined,
|
||||
params.sessionId as string,
|
||||
).catch(() => undefined);
|
||||
}
|
||||
});
|
||||
} else if (params.waitingForDebugger === true) {
|
||||
void record.view.webContents.debugger.sendCommand(
|
||||
'Runtime.runIfWaitingForDebugger',
|
||||
undefined,
|
||||
params.sessionId,
|
||||
).catch(() => undefined);
|
||||
}
|
||||
} else if (
|
||||
method === 'Target.detachedFromTarget'
|
||||
&& typeof params.sessionId === 'string'
|
||||
) {
|
||||
record.childSessions.delete(params.sessionId);
|
||||
const binding = record.previewData;
|
||||
if (binding) {
|
||||
const detachedScripts = binding.scripts.filter((script) => script.sessionRef === params.sessionId);
|
||||
binding.scripts = binding.scripts.filter((script) => script.sessionRef !== params.sessionId);
|
||||
for (const script of detachedScripts) {
|
||||
void record.view.webContents.debugger.sendCommand(
|
||||
'Page.removeScriptToEvaluateOnNewDocument',
|
||||
{ identifier: script.identifier },
|
||||
script.sessionRef,
|
||||
).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -992,6 +1317,9 @@ export class AgentBrowserModule {
|
||||
}
|
||||
|
||||
private async navigateTo(record: BrowserRecord, targetUrl: string): Promise<void> {
|
||||
if (this.isPreviewDataCrossOrigin(record, targetUrl)) {
|
||||
await this.invalidatePreviewData(record, 'cross_origin_navigation');
|
||||
}
|
||||
try {
|
||||
await this.loadPageUntilReady(record, targetUrl);
|
||||
record.url = targetUrl;
|
||||
@@ -1216,6 +1544,7 @@ export class AgentBrowserModule {
|
||||
}
|
||||
if (expected && this.record !== expected) return;
|
||||
record.state = 'closing';
|
||||
await this.invalidatePreviewData(record, 'preview_closed');
|
||||
this.notifyLifecycle({
|
||||
type: 'closed',
|
||||
projectId: record.projectId,
|
||||
@@ -1566,6 +1895,66 @@ function normalizeUrl(value: string): string {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function assertPreviewDataTarget(value: string): void {
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(value);
|
||||
} catch {
|
||||
throw new AgentBrowserFault('TARGET_DENIED', '数据运行时只允许打开本机预览地址。', false);
|
||||
}
|
||||
const hostname = url.hostname.toLowerCase();
|
||||
const loopback = hostname === 'localhost'
|
||||
|| hostname === '127.0.0.1'
|
||||
|| hostname === '[::1]';
|
||||
if (
|
||||
!loopback
|
||||
|| (url.protocol !== 'http:' && url.protocol !== 'https:')
|
||||
|| url.username
|
||||
|| url.password
|
||||
) {
|
||||
throw new AgentBrowserFault('TARGET_DENIED', '数据运行时只允许打开本机预览地址。', false);
|
||||
}
|
||||
}
|
||||
|
||||
function sameOrigin(left: string, right: string): boolean {
|
||||
try {
|
||||
return new URL(left).origin === new URL(right).origin;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function preventableEvent(value: unknown): { preventDefault(): void } | undefined {
|
||||
return isRecord(value) && typeof value.preventDefault === 'function'
|
||||
? value as { preventDefault(): void }
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function isDocumentTarget(params: Record<string, unknown>): boolean {
|
||||
const targetInfo = isRecord(params.targetInfo) ? params.targetInfo : undefined;
|
||||
const type = targetInfo?.type;
|
||||
return type === 'page' || type === 'iframe' || type === 'webview';
|
||||
}
|
||||
|
||||
function previewDataInjectionScript(
|
||||
origin: string,
|
||||
value: PreviewDataInjectionValue,
|
||||
): string {
|
||||
const serializedOrigin = JSON.stringify(origin);
|
||||
const serializedValue = JSON.stringify(value);
|
||||
if (!serializedOrigin || !serializedValue) {
|
||||
throw new Error('Preview data injection value is not serializable');
|
||||
}
|
||||
return `if (globalThis.location?.origin === ${serializedOrigin}) {
|
||||
Object.defineProperty(globalThis, "__MAKELORE_DATA__", {
|
||||
value: ${serializedValue},
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
configurable: false,
|
||||
});
|
||||
}`;
|
||||
}
|
||||
|
||||
function crossOriginNavigation(previousUrl: string, nextUrl: string): boolean {
|
||||
if (previousUrl === 'about:blank' || nextUrl === 'about:blank') return false;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user