2026-10-06 11:04:39 -04:00
// Served-dashboard qualification. Signing responses are isolated fixtures; no real keys or payments.
const fs = require ( 'fs' );
const { chromium , expect } = require ( process . env . PLAYWRIGHT_MODULE || '@playwright/test' );
( async ()=>{
const node = process . env . QUALIFICATION_LABEL || 'qualification-node' ;
const origin = process . env . QUALIFICATION_ORIGIN , cookieFile = process . env . QUALIFICATION_COOKIES ;
if ( ! origin || ! cookieFile ) throw new Error ( 'Set QUALIFICATION_ORIGIN and QUALIFICATION_COOKIES for an authorized private node' );
const url = new URL ( origin ), octets = url . hostname . split ( '.' ). map ( Number );
const privateHost = url . hostname === 'localhost' || ( octets . length === 4 && octets . every ( n => Number . isInteger ( n ) && n >= 0 && n <= 255 )
&& ( octets [ 0 ] === 127 || octets [ 0 ] === 10 || ( octets [ 0 ] === 192 && octets [ 1 ] === 168 )
|| ( octets [ 0 ] === 172 && octets [ 1 ] >= 16 && octets [ 1 ] <= 31 ) || ( octets [ 0 ] === 100 && octets [ 1 ] >= 64 && octets [ 1 ] <= 127 )));
if ( ! privateHost || ! [ 'http:' , 'https:' ]. includes ( url . protocol ) || url . username || url . password ) throw new Error ( 'Refusing to send qualification cookies outside a private node' );
const cookies = JSON . parse ( fs . readFileSync ( cookieFile , 'utf8' ));
const browser = await chromium . connectOverCDP ( process . env . BROWSER_CDP || 'http://127.0.0.1:32911' );
2026-10-06 11:43:33 -04:00
for ( const mode of [ 'session' , 'overlay' ]) for ( const width of [ 390 , 1440 ]){
const fixturePort = mode === 'overlay' ? 19999 : 7778 ;
const fixtureUrl = new URL ( origin ); fixtureUrl . port = String ( fixturePort ); fixtureUrl . pathname = '/' ;
const overlayUrl = fixtureUrl . href ;
2026-10-06 11:04:39 -04:00
const context = await browser . newContext ({ viewport : { width , height : 900 }, serviceWorkers : 'block' });
try {
await context . addCookies ( Object . entries ( cookies ). map (([ name , value ])=>({ name , value , url : origin , httpOnly : name !== 'csrf_token' })));
2026-10-06 11:43:33 -04:00
await context . addInitScript (({ overlayUrl })=>{
2026-10-06 11:04:39 -04:00
localStorage . setItem ( 'neode-auth' , 'true' );
localStorage . removeItem ( 'archipelago_nostr_consent_v2' );
2026-10-06 11:43:33 -04:00
const identity = { id : 'qualification-only' , name : 'Qualification identity' , did : 'did:key:qualification-only' , pubkey : 'a' . repeat ( 64 ), nostr_pubkey : 'b' . repeat ( 64 )};
localStorage . setItem ( 'archipelago_app_identity_indeedhub' , JSON . stringify ( identity ));
localStorage . setItem ( 'archipelago_app_identity_' + overlayUrl . replace ( /[^a-z0-9]/gi , '_' ), JSON . stringify ( identity ));
},{ overlayUrl });
2026-10-06 11:04:39 -04:00
let signed = 0 , frameLoads = 0 ; const signedContents = [];
await context . route ( '**/rpc/v1' , async route =>{
let request ; try { request = route . request (). postDataJSON ()} catch { return route . continue ()}
if ( request ? . method === 'identity.sign' && request . params ? . id === 'qualification-only' ){
return route . fulfill ({ contentType : 'application/json' , body : JSON . stringify ({ jsonrpc : '2.0' , id : request . id , result : { signature : 'qualification-fixture' }})});
}
if ( request ? . method === 'identity.nostr-sign' ){
if ( request . params ? . id !== 'qualification-only' ||! [ 'qualification-first' , 'qualification-second' ]. includes ( request . params ? . event ? . content )) throw new Error ( 'Unexpected signer request in isolated fixture' );
signed ++ ; signedContents . push ( request . params . event . content );
return route . fulfill ({ contentType : 'application/json' , body : JSON . stringify ({ jsonrpc : '2.0' , id : request . id , result : { id : 'a' . repeat ( 64 ), content : request . params . event . content }})});
}
return route . continue ();
});
2026-10-06 11:43:33 -04:00
await context . route ( '**:' + fixturePort + '/**' , async route =>{
2026-10-06 11:04:39 -04:00
if ( route . request (). resourceType () !== 'document' ) return route . abort ();
frameLoads ++ ;
return route . fulfill ({ contentType : 'text/html' , body : `<!doctype html><html><body><p id="result">waiting</p><script>
const responses=[];
addEventListener('message',e=>{if(e.data?.type==='nostr-response'){responses.push({id:e.data.id,ok:!!e.data.result,error:e.data.error});document.querySelector('#result').textContent=JSON.stringify(responses)}});
setTimeout(()=>{for(const id of ['first','second'])parent.postMessage({type:'nostr-request',id,method:'signEvent',params:{event:{kind:27235,content:'qualification-'+id}}}, ${ JSON . stringify ( origin ) } )},500);
</script></body></html>` });
});
const page = await context . newPage ();
2026-10-06 11:43:33 -04:00
await page . goto ( origin + ( mode === 'overlay' ? '/dashboard/apps' : '/dashboard/app-session/indeedhub' ),{ waitUntil : 'domcontentloaded' });
if ( mode === 'overlay' ){
await page . waitForFunction (()=> document . querySelector ( '#app' ) ? . __vue_app__ ? . config . globalProperties . $pinia ? . _s . has ( 'appLauncher' ));
await page . evaluate ( url => document . querySelector ( '#app' ). __vue_app__ . config . globalProperties . $pinia . _s . get ( 'appLauncher' ). open ({ url , title : 'Qualification custom app' }), overlayUrl );
}
2026-10-06 11:04:39 -04:00
const approve = page . getByRole ( 'button' ,{ name : 'Approve' , exact : true });
try { await expect ( approve ). toBeVisible ({ timeout : 30000 }); } catch ( error ) {
2026-10-06 11:43:33 -04:00
console . log ( JSON . stringify ({ node , mode , width , path : new URL ( page . url ()). pathname , headings : await page . locator ( 'h1' ). allTextContents (), frames : await page . locator ( 'iframe' ). evaluateAll ( items => items . map ( item =>{ const u = new URL ( item . src ); return { origin : u . origin , path : u . pathname }}))}));
2026-10-06 11:04:39 -04:00
throw error ;
}
await approve . click ();
await expect . poll (()=> signed ,{ timeout : 10000 }). toBe ( 1 );
await expect ( approve ). toBeVisible ({ timeout : 10000 });
await approve . click ();
await expect . poll (()=> signed ,{ timeout : 10000 }). toBe ( 2 );
2026-10-06 11:43:33 -04:00
const frame = page . frameLocator ( 'iframe[src*="' + fixturePort + '"]' );
2026-10-06 11:04:39 -04:00
await expect ( frame . locator ( '#result' )). toContainText ( '"id":"first","ok":true' );
2026-10-06 11:43:33 -04:00
try { await expect ( frame . locator ( '#result' )). toContainText ( '"id":"second","ok":true' ); } catch ( error ) { console . log ( JSON . stringify ({ node , mode , width , frameLoads , signedContents })); throw error ; }
2026-10-06 11:04:39 -04:00
await page . waitForTimeout ( 800 ); // allow the required 675ms completion presentation to finish
await expect ( approve ). toHaveCount ( 0 );
expect ( frameLoads ). toBe ( 1 );
expect ( signedContents ). toEqual ([ 'qualification-first' , 'qualification-second' ]);
2026-10-06 11:43:33 -04:00
console . log ( JSON . stringify ({ node , mode , width , result : 'PASS' , scope : 'served dashboard, concurrent iframe requests and consent responses; signing RPC isolated with fixtures; no real signing/payment' }));
2026-10-06 11:04:39 -04:00
} finally { await context . close ()}
}
process . exit ( 0 );
})(). catch ( e =>{ console . error ( String ( e . message ). split ( 'Call log:' )[ 0 ]); process . exit ( 1 )});