import test from 'node:test'; import assert from 'node:assert/strict'; import {EventEmitter} from 'node:events'; import {createRequire} from 'node:module'; import vm from 'node:vm'; const require=createRequire(import.meta.url); const {attachPageClipboardGuard,guardedScreenshot,disposePageClipboardGuard,createNativeTransportChannel}=require('../apps/browser/page-clipboard-guard.cjs'); const {capturePNG}=require('../apps/browser/visual-capture.cjs'); function fixture(valid=true){ const api=new EventEmitter(),commands=[];let attached=false,detaches=0,failures=0; api.isAttached=()=>attached;api.attach=version=>{assert.equal(version,'1.3');attached=true;}; api.detach=()=>{attached=false;detaches++;api.emit('detach');}; const png=Buffer.from('Public bounded screenshot fixture'); api.sendCommand=async(method,parameters)=>{commands.push({method,parameters});if(method==='Page.addScriptToEvaluateOnNewDocument')return valid?{identifier:'fixed-guard'}:{};if(method==='Page.captureScreenshot')return {data:png.toString('base64')};return {};}; const contents={debugger:api,isDestroyed:()=>false}; return {api,contents,commands,png,detachCount:()=>detaches,failureCount:()=>failures,initialize:()=>attachPageClipboardGuard(contents,()=>failures++)}; } test('private transport lease exposes only fixed commands and releases waiting targets without detaching clipboard protection',async()=>{ const f=fixture();await f.initialize();const channel=createNativeTransportChannel(f.contents),messages=[]; assert.equal(channel.attach,undefined);assert.equal(channel.detach,undefined);assert.equal(channel.sendCommand,undefined); assert.throws(()=>createNativeTransportChannel(f.contents));channel.subscribeMessage((_event,method)=>messages.push(method)); f.api.emit('message',{},'Network.requestWillBeSent',{request:{url:'https://private.invalid/?token=secret',postData:'secret'}}); f.api.emit('message',{},'Target.attachedToTarget',{sessionId:'worker-1',targetInfo:{type:'worker'}}); assert.deepEqual(messages,['Target.attachedToTarget']);await channel.enableNetwork('worker-1');await channel.autoAttachTargets('worker-1');await channel.resumeTarget('worker-1'); assert.deepEqual(f.commands.slice(2).map(c=>c.method),['Network.enable','Target.setAutoAttach','Runtime.runIfWaitingForDebugger']); const before=f.commands.length;await assert.rejects(channel.resumeTarget('private;command'));assert.equal(f.commands.length,before); channel.release();await new Promise(resolve=>setImmediate(resolve));assert.equal(channel.isHealthy(),false);assert.equal(f.detachCount(),0);assert.equal(f.api.isAttached(),true); assert.equal(f.commands.at(-1).parameters.autoAttach,false);assert.ok(f.commands.slice(before).some(c=>c.method==='Runtime.runIfWaitingForDebugger')); assert.deepEqual(await capturePNG(f.contents),f.png);await assert.rejects(channel.enableNetwork());disposePageClipboardGuard(f.contents);assert.equal(f.failureCount(),0); }); test('transport cannot borrow an unguarded or revoked native debugger',async()=>{ const f=fixture();assert.throws(()=>createNativeTransportChannel(f.contents));await f.initialize();const channel=createNativeTransportChannel(f.contents);let detach=0;channel.subscribeDetach(()=>detach++);f.api.detach();assert.equal(detach,1);assert.equal(channel.isHealthy(),false);await assert.rejects(channel.enableNetwork());channel.release();assert.equal(f.failureCount(),1); }); test('static page guard registers only fixed initialization and shares screenshot without detaching',async()=>{ const f=fixture();await f.initialize();assert.deepEqual(f.commands.map(c=>c.method),['Page.enable','Page.addScriptToEvaluateOnNewDocument']); assert.equal(/require\(|ipcRenderer|process\./.test(f.commands[1].parameters.source),false); assert.deepEqual(await capturePNG(f.contents),f.png);assert.equal(f.commands.at(-1).method,'Page.captureScreenshot');assert.equal(f.detachCount(),0); disposePageClipboardGuard(f.contents);assert.equal(f.detachCount(),1);assert.equal(f.failureCount(),0);assert.equal(guardedScreenshot(f.contents,{}),null); }); test('unknown attachment and invalid initialization cannot gain trusted screenshot ownership',async()=>{ const occupied=fixture();occupied.api.attach('1.3');await assert.rejects(occupied.initialize());assert.equal(occupied.commands.length,0); const invalid=fixture(false);await assert.rejects(invalid.initialize());assert.equal(invalid.detachCount(),1);assert.equal(guardedScreenshot(invalid.contents,{}),null); }); test('unexpected disconnect revokes ownership and reports failure; deliberate disposal is idempotent',async()=>{ const f=fixture();await f.initialize();f.api.detach();assert.equal(f.failureCount(),1);assert.equal(guardedScreenshot(f.contents,{}),null);disposePageClipboardGuard(f.contents);assert.equal(f.detachCount(),1); }); test('new-frame source freezes clipboard and replacement primitives against page tampering',async()=>{ const f=fixture();await f.initialize();const native=[]; class Document{execCommand(command){native.push(command);return true;}open(){return 'opened';}write(){}writeln(){}} const listeners=[],realm={Document,DOMException,window:{addEventListener:(name,handler,capture)=>listeners.push({name,handler,capture})}}; vm.createContext(realm);vm.runInContext(f.commands[1].parameters.source,realm); const doc=new Document();for(const name of ['copy','cut','paste','PasteAndMatchStyle'])assert.equal(doc.execCommand(name),false); assert.equal(doc.execCommand('insertText'),true);assert.deepEqual(native,['insertText']); for(const name of ['open','write','writeln'])assert.throws(()=>doc[name](),error=>error.name==='SecurityError'); vm.runInContext('Reflect.apply=()=>true;RegExp.prototype.test=()=>false;',realm);assert.equal(doc.execCommand('copy'),false); assert.throws(()=>Object.defineProperty(Document.prototype,'execCommand',{value:()=>true})); for(const name of ['copy','cut','paste','dragstart','drop']){let prevented=false,stopped=false;const listener=listeners.find(item=>item.name===name);assert.equal(listener.capture,true);listener.handler({preventDefault:()=>prevented=true,stopImmediatePropagation:()=>stopped=true});assert.equal(prevented&&stopped,true);} disposePageClipboardGuard(f.contents); });