// Native page behavior. HTTP authorization is covered by proxy-page.test.mjs; // this stand-in serves the real page/script and records only route names/counts. import test from 'node:test'; import assert from 'node:assert/strict'; import http from 'node:http'; import { readFileSync } from 'node:fs'; import { browser } from './browser.mjs'; async function setup(t, readonly = false) { const state = { routes: [], loginStatus: 'wait', receivedKey: false, release: null, priority: 0 }; const server = http.createServer(async (req, res) => { const send = value => { res.setHeader('content-type', 'application/json'); res.end(JSON.stringify(value)); }; if (req.url === '/api/config') return send({ send: !readonly }); if (req.url.startsWith('/api/proxy/')) { state.routes.push(req.url); const chunks = []; for await (const c of req) chunks.push(c); const args = chunks.length ? JSON.parse(Buffer.concat(chunks)) : {}; if (req.url.endsWith('/accounts')) return send({ accounts: [{ accountId: 'a'.repeat(12), provider: 'claude', displayName: '', status: 'ready', priority: state.priority }] }); if (req.url.endsWith('/keys')) return send({ keys: [{ keyId: 'b'.repeat(12), management: 'sentinel', agent: null, issuedAt: null, state: null }] }); if (req.url.endsWith('/apikey/add')) { state.receivedKey = args.key === 'p4-browser-key'; await new Promise(r => { state.release = r; }); return send({ added: true }); } if (req.url.endsWith('/login/start')) return send({ url: 'https://claude.ai/oauth/authorize?state=p4-state', state: 'p4-state' }); if (req.url.endsWith('/login/status')) return send({ status: state.loginStatus }); if (req.url.endsWith('/login/submit')) return send({ status: 'wait' }); if (req.url.endsWith('/login/cancel')) return send({ status: 'cancelled' }); if (req.url.endsWith('/priority/set')) { state.priority = args.priority; return send({ priority: args.priority }); } } if (req.url === '/proxy') { res.setHeader('content-type', 'text/html'); return res.end(readFileSync(new URL('../src/public/proxy.html', import.meta.url))); } if (req.url === '/proxy.js') { res.setHeader('content-type', 'text/javascript'); return res.end(readFileSync(new URL('../src/public/proxy.js', import.meta.url))); } res.statusCode = 404; res.end(); }); await new Promise(r => server.listen(0, '127.0.0.1', r)); t.after(() => { state.release?.(); return new Promise(r => server.close(r)); }); const b = await browser({ machine: false }); t.after(() => b.close()); await b.call('Page.addScriptToEvaluateOnNewDocument', { source: "window.openFeatures=null;window.open=(url,target,features)=>{window.openFeatures=features;return null;};" }); await b.navigate(`http://127.0.0.1:${server.address().port}/proxy`); const wait = expression => b.evaluate(`(async()=>{for(let n=0;n<200;n++){if(${expression})return true;await new Promise(r=>setTimeout(r,25));}throw Error('proxy UI condition timed out');})()`); await wait('document.querySelector("#proxy-message").textContent.includes("loaded")'); return { b, state, wait }; } test('proxy browser clears write-only key immediately, sends once and renders data as text', { timeout: 60000 }, async t => { const { b, state, wait } = await setup(t); assert.equal(await b.evaluate('window.injected===true'), false); assert.equal(await b.evaluate('document.querySelectorAll("#proxy-accounts img").length'), 0); assert.equal(await b.evaluate('document.querySelector("#proxy-apikey input").type'), 'password'); assert.equal(await b.evaluate('document.querySelector("#proxy-apikey input").autocomplete'), 'off'); await b.evaluate('document.querySelector("#proxy-apikey input").value="p4-browser-key";document.querySelector("#proxy-apikey").requestSubmit();true'); await wait('document.querySelector("#proxy-apikey input").value===""'); for (let n = 0; n < 100 && !state.release; n++) await new Promise(r => setTimeout(r, 20)); assert.equal(state.receivedKey, true); state.release(); await wait('document.querySelector("#proxy-message").textContent.includes("added")'); assert.equal(state.routes.filter(r => r.endsWith('/apikey/add')).length, 1); assert.equal(await b.evaluate('localStorage.length+sessionStorage.length'), 0); assert.equal(await b.evaluate('document.body.textContent.includes("p4-browser-key")'), false); }); test('proxy browser OAuth opens safely, callback wait stays pending, status ok completes', { timeout: 60000 }, async t => { const { b, state, wait } = await setup(t); await b.evaluate('document.querySelector("#proxy-login-start").requestSubmit();true'); await wait('!document.querySelector("#proxy-authorize").hidden'); assert.equal(await b.evaluate('window.openFeatures'), 'noopener,noreferrer'); assert.equal(await b.evaluate('document.querySelector("#proxy-authorize").rel'), 'noopener noreferrer'); await b.evaluate('document.querySelector("#proxy-login-submit input").value="http://localhost/callback?state=p4-state&code=p4-code";document.querySelector("#proxy-login-submit").requestSubmit();true'); await wait('document.querySelector("#proxy-login-message").textContent.includes("Waiting")'); assert.equal(await b.evaluate('document.querySelector("#proxy-login-message").textContent.includes("signed in")'), false); assert.equal(await b.evaluate('document.querySelector("#proxy-login-submit input").value===""'), true); state.loginStatus = 'ok'; await wait('document.querySelector("#proxy-login-message").textContent==="Account signed in."'); assert.equal(await b.evaluate('document.querySelector("#proxy-authorize").hasAttribute("href")'), false); assert.equal(await b.evaluate('localStorage.length+sessionStorage.length'), 0); }); test('proxy browser read-only disables account changes', { timeout: 60000 }, async t => { const { b, state } = await setup(t, true); assert.equal(await b.evaluate('[...document.querySelectorAll("[data-write]")].every(e=>e.disabled)'), true); assert.equal(await b.evaluate('document.querySelector("#proxy-accounts input").disabled'), true); assert.equal(state.routes.some(r => r.endsWith('/apikey/add') || r.endsWith('/login/start')), false); });