// PLAYWRIGHT_MODULE=/absolute/path/to/playwright node scripts/validation/marketplace-browser.cjs const fs = require('node:fs'); const os = require('node:os'); const path = require('node:path'); const http = require('node:http'); const assert = require('node:assert/strict'); const { DatabaseSync } = require('node:sqlite'); const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright'); const { startMarketplaceServer } = process.env.CLOUDFLARE_REGISTRY === '1' ? require('./cloudflare-registry.cjs') : require('../../dist/plugins/marketplace'); const { startExtensionBridge } = require('../../dist/plugins/bridge'); process.env.CODEGRAPH_TELEMETRY = '0'; process.env.CODEGRAPH_PARSE_WORKERS = '2'; const out = path.resolve(process.env.BROWSER_OUTPUT || '.qa/recovery/browser'); fs.mkdirSync(out, { recursive: true }); const checks = []; const safeError = error => String(error.stack || error).replace(/token=[a-f0-9]+/g, 'token=[redacted]'); function artifact(version, broken = false) { return Buffer.from(JSON.stringify({ format: 'codegraph-extension-1', package: { name: '@browser/example', version, main: 'index.cjs', codegraph: { id: 'hosted-browser-20260922c', apiVersion: 1, capabilities: ['frameworks'] }, }, files: { 'index.cjs': `module.exports=()=>({frameworks:[{name:'route',languages:['typescript'],detect:()=>true,resolve:()=>null, extract(file){${broken ? "throw Error('Rejected browser update');" : ''} return {nodes:[{id:'plugin:hosted-browser-20260922c:'+file,kind:'route',name:'/browser-${version}',qualifiedName:file+'::browser',filePath:file,language:'typescript',startLine:1,endLine:1,startColumn:0,endColumn:0,updatedAt:0}],references:[]}}}]});` } })); } function routes(root) { const file = path.join(root, '.codegraph/codegraph.db'); if (!fs.existsSync(file)) return []; const db = new DatabaseSync(file, { readOnly: true }); try { return db.prepare("SELECT name FROM nodes WHERE kind='route' ORDER BY name").all().map(n => n.name); } finally { db.close(); } } (async () => { const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-browser-recovery-')); const roots = ['one', 'two'].map(n => path.join(temp, n)); roots.forEach(r => { fs.mkdirSync(r); fs.writeFileSync(path.join(r, 'app.ts'), 'export function main() {}'); }); let server, bridge, browser, attacker, debugPage, debugSnapshot, https; try { let origin = process.env.HOSTED_REGISTRY; assert.match(origin || '', /^https:\/\/[a-z0-9.-]+\.workers\.dev$/); assert.equal((await(await fetch(origin+'/api/extensions/hosted-browser-20260922c')).json()).length,0,'Fixture already exists; use a fresh isolated target or rename this owned fixture'); if (process.env.REGISTRY_TEST_HTTPS === '1') { https = await require('./https-marketplace.cjs').httpsMarketplace(server.port, out); origin = https.origin; } bridge = await startExtensionBridge(roots, origin); const token = new URLSearchParams(new URL(bridge.connectionUrl).hash.slice(1)).get('token'); const snapshot = async () => (await fetch(bridge.url + '/status', { headers: { Authorization: 'Bearer ' + token } })).json(); debugSnapshot = snapshot; browser = await chromium.launch({ executablePath: process.env.CHROME_PATH || '/opt/google/chrome/chrome', headless: true, args: ['--no-sandbox'] }); const context = await browser.newContext({ viewport: { width: 1440, height: 1000 } }); context.setDefaultTimeout(30000); let lastPublication=0; async function publicationSlot(route){await new Promise(r=>setTimeout(r,Math.max(0,lastPublication+7500-Date.now())));lastPublication=Date.now();await route.continue();} await context.route('**/api/publish',publicationSlot); context.setDefaultNavigationTimeout(20000); const page = await context.newPage(); debugPage = page; const pageErrors = []; page.on('pageerror', e => pageErrors.push(e.message)); let releaseCatalog, firstCatalog = true; await page.route('**/api/extensions', async route => { if (firstCatalog) { firstCatalog = false; await new Promise(resolve => { releaseCatalog = resolve; }); } await route.continue(); }); await page.goto(bridge.connectionUrl, { waitUntil: 'domcontentloaded' }); if (origin.startsWith('https:')) assert.equal(await page.evaluate(() => isSecureContext), true); // Reproduce slow native file reads: metadata must never erase typed fields. await page.evaluate(() => { const original=File.prototype.text; File.prototype.text=async function(){await new Promise(resolve=>setTimeout(resolve,500));return original.call(this);}; }); console.log('Opened marketplace'); await page.getByRole('link', { name: 'Publish an extension' }).click(); async function publish(version, broken = false) { console.log('Publishing', version); if (new URL(page.url()).pathname !== '/publish') await page.getByRole('link', { name: 'Publish an extension' }).click(); await page.locator('#artifact').setInputFiles({ name: 'example.cgext', mimeType: 'application/json', buffer: artifact(version, broken) }); await page.locator('#name').fill('Browser example'); await page.locator('#publisher').fill('Recovery publisher'); await page.locator('#description').fill('A second framework used for end-to-end validation.'); await page.locator('#source').fill('https://example.com/recovery-source'); await page.locator('#readme').fill('Validation-only extension. Adds one explicit route to each TypeScript file.'); await page.locator('#package-summary').filter({ hasText: 'v'+version+' ยท' }).waitFor(); if (releaseCatalog) { const response = page.waitForResponse(r=>r.url().endsWith('/api/extensions')); releaseCatalog(); releaseCatalog = undefined; await response; await page.waitForTimeout(200); assert.equal(await page.locator('#artifact').evaluate(input=>input.files[0]?.name),'example.cgext'); checks.push('late catalog response preserves the entered publisher form and package'); } assert.equal(await page.locator('#description').inputValue(), 'A second framework used for end-to-end validation.'); assert.equal(await page.locator('#source').inputValue(), 'https://example.com/recovery-source'); await page.getByRole('button', { name: 'Publish release' }).click(); console.log('Submitted', version); await page.waitForURL('**/extensions/hosted-browser-20260922c'); await page.getByText(version, { exact: true }).waitFor(); } await publish('1.0.0'); checks.push('real browser signed publisher upload'); checks.push('slow package metadata preserves typed publisher fields'); const first = (await (await fetch(origin + '/api/extensions')).json()).find(x=>x.id==='hosted-browser-20260922c'); assert.equal(first.official, false); // A separate browser has a separate signing identity and cannot claim this id. const otherContext = await browser.newContext(); otherContext.setDefaultTimeout(30000); await otherContext.route('**/api/publish',publicationSlot); const other = await otherContext.newPage(); await other.goto(origin + '/publish'); await other.locator('#artifact').setInputFiles({ name: 'example.cgext', mimeType: 'application/json', buffer: artifact('9.0.0') }); for (const [id, value] of Object.entries({ name: 'Attempted takeover', publisher: 'Other publisher', description: 'Test', source: 'https://example.com/other', readme: 'Test' })) await other.locator('#' + id).fill(value); await other.getByRole('button', { name: 'Publish release' }).click(); await other.getByText('This extension id belongs to another publisher', { exact: true }).waitFor(); checks.push('browser publisher ownership rejection'); // Send the real form submission to the real API with a corrupted signature. await other.route('**/api/publish', async route => { const body = route.request().postDataJSON(); body.signature = 'invalid'; await route.continue({ postData: JSON.stringify(body) }); }); await other.getByRole('button', { name: 'Publish release' }).click(); await other.locator('#publish-result').filter({ hasText: /signature/i }).waitFor(); await other.unroute('**/api/publish'); checks.push('browser invalid-signature failure state'); const malformed = JSON.parse(artifact('9.0.0')); malformed.format = 'invalid-format'; await other.locator('#artifact').setInputFiles({ name: 'invalid.cgext', mimeType: 'application/json', buffer: Buffer.from(JSON.stringify(malformed)) }); await other.locator('#description').fill('Invalid package validation'); await other.locator('#source').fill('https://example.com/invalid'); await other.getByRole('button', { name: 'Publish release' }).click(); await other.locator('#publish-result').filter({ hasText: /package/i }).waitFor(); assert.equal((await (await fetch(origin + '/api/extensions/hosted-browser-20260922c')).json()).length, 1); await otherContext.close(); checks.push('browser malformed-package rejection without publication'); await page.getByRole('link', { name: 'Marketplace', exact: true }).first().click(); const popupPromise = page.waitForEvent('popup'); await page.locator('#connect').click(); const popup = await popupPromise; await popup.getByRole('button', { name: 'Allow connection' }).click(); await page.getByText('CodeGraph connected', { exact: true }).waitFor(); await page.locator('#project').selectOption('1'); await page.locator('a[href="/extensions/hosted-browser-20260922c"]').first().click(); let completedJob = 0; await page.getByRole('button', { name: 'Install extension' }).click(); async function settle(expected) { await page.waitForFunction(previous => state.snapshot && !state.snapshot.busy && state.snapshot.jobId > previous, completedJob); const current = await snapshot(); completedJob = current.jobId; console.log('Companion result', JSON.stringify({ progress: current.progress, error: current.error, jobId: current.jobId })); assert.ok(!current.error, current.error); await page.getByText(expected, { exact: true }).first().waitFor({ timeout: 60000 }); } await settle('Extension activated and graph refreshed'); assert.deepEqual(routes(roots[1]), ['/browser-1.0.0']); assert.deepEqual(routes(roots[0]), []); checks.push('one-click selected-project install verified in SQLite'); await page.screenshot({ path: path.join(out, 'desktop-installed.png'), fullPage: true }); // Real same-origin message from an iframe: right origin, wrong source window. const before = (await snapshot()).jobId; await page.evaluate(() => { window.__recoveryPopup = state.popup; const iframe = document.createElement('iframe'); iframe.name = 'wrong-source'; document.body.append(iframe); }); await page.frame({ name: 'wrong-source' }).evaluate(() => parent.__recoveryPopup.postMessage({ channel: 'codegraph-extensions-v1', type: 'command', id: 'wrong-window', command: { action: 'remove', id: 'hosted-browser-20260922c', project: '1' } }, '*')); await page.waitForTimeout(300); assert.equal((await snapshot()).jobId, before); await page.locator('iframe[name="wrong-source"]').evaluate(node => node.remove()); checks.push('real postMessage wrong-window command ignored'); await publish('2.0.0', true); await page.getByRole('button', { name: 'Update to 2.0.0' }).click(); await page.getByText(/Extension activation failed/).first().waitFor({ timeout: 60000 }); completedJob = (await snapshot()).jobId; assert.deepEqual(routes(roots[1]), ['/browser-1.0.0']); assert.equal((await snapshot()).projects[1].extensions[0].version, '1.0.0'); checks.push('failed browser update preserves working version and graph'); await publish('3.0.0'); await page.getByRole('button', { name: 'Update to 3.0.0' }).click(); await settle('Extension activated and graph refreshed'); assert.deepEqual(routes(roots[1]), ['/browser-3.0.0']); checks.push('browser update replaces stale graph contributions'); await page.getByRole('button', { name: 'Disable', exact: true }).click(); await page.getByRole('button', { name: 'Enable', exact: true }).waitFor({ timeout: 60000 }); assert.deepEqual(routes(roots[1]), []); await page.getByRole('button', { name: 'Enable', exact: true }).click(); await page.getByRole('button', { name: 'Disable', exact: true }).waitFor({ timeout: 60000 }); assert.deepEqual(routes(roots[1]), ['/browser-3.0.0']); await page.getByRole('button', { name: 'Remove', exact: true }).click(); await page.getByRole('button', { name: 'Install extension' }).waitFor({ timeout: 60000 }); await page.getByText('Extension removed and graph refreshed', { exact: true }).first().waitFor(); assert.deepEqual(routes(roots[1]), []); assert.deepEqual(routes(roots[0]), []); checks.push('browser disable/enable/remove verified in SQLite'); await page.setViewportSize({ width: 390, height: 844 }); await page.getByText('CodeGraph compatibility', { exact: true }).waitFor(); await page.getByText('3.0.0', { exact: true }).waitFor(); await page.screenshot({ path: path.join(out, 'mobile-detail.png'), fullPage: true }); assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)); checks.push('mobile detail fits viewport'); // Bootstrap a companion from a hostile origin, even with a valid test token: // its real message origin must still be refused after the local allow click. attacker = http.createServer((req, res) => { res.setHeader('Content-Type', 'text/html'); res.end(''); }); await new Promise(resolve => attacker.listen(0, '127.0.0.1', resolve)); const evil = await context.newPage(); await evil.goto(`http://127.0.0.1:${attacker.address().port}`); await evil.evaluate(({ url }) => { document.querySelector('#open').onclick = () => { window.target = window.open(url, '_blank'); }; }, { url: bridge.url + '/#token=' + token }); const badPopupPromise = evil.waitForEvent('popup'); await evil.locator('#open').click(); const badPopup = await badPopupPromise; await badPopup.getByRole('button', { name: 'Allow connection' }).click(); const beforeOrigin = (await snapshot()).jobId; await evil.evaluate(() => window.target.postMessage({ channel: 'codegraph-extensions-v1', type: 'command', id: 'wrong-origin', command: { action: 'enable', id: 'hosted-browser-20260922c', project: '1' } }, '*')); await evil.waitForTimeout(300); assert.equal((await snapshot()).jobId, beforeOrigin); checks.push('real postMessage wrong-origin command ignored'); await popup.close(); await page.getByText('Reconnect CodeGraph', { exact: true }).waitFor(); checks.push('closed local companion expires the connection visibly'); await page.setViewportSize({ width: 1440, height: 1000 }); await page.getByRole('link', { name: 'Publish an extension' }).click(); await page.locator('#artifact').setInputFiles({ name: 'example.cgext', mimeType: 'application/json', buffer: artifact('4.0.0') }); for (const [id, value] of Object.entries({ name: 'Browser example', publisher: 'Recovery publisher', description: 'Outage check', source: 'https://example.com/source', readme: 'Outage check' })) await page.locator('#'+id).fill(value); await page.locator('#package-summary').filter({ hasText: 'v4.0.0' }).waitFor(); await page.route('**/api/publish', route=>route.abort('connectionfailed')); await page.getByRole('button', { name: 'Publish release' }).click(); await page.locator('#publish-result').filter({ hasText: /marketplace registry/i }).waitFor(); assert.equal(await page.getByRole('button', { name: 'Publish release' }).isEnabled(), true); assert.deepEqual(routes(roots[1]), []); checks.push('injected client transport outage shows actionable publisher failure (provider remains running) without changing local graph'); assert.deepEqual(pageErrors, []); fs.writeFileSync(path.join(out, 'result.json'), JSON.stringify({ origin, browser: browser.version(), platform: process.platform, checks, pageErrors }, null, 2)); console.log(JSON.stringify({ checks, browser: browser.version(), platform: process.platform })); } catch (error) { await debugPage?.screenshot({ path: path.join(out, 'failure.png'), fullPage: true }).catch(() => {}); fs.writeFileSync(path.join(out, 'failure.json'), JSON.stringify({ checks, error: safeError(error), snapshot: await debugSnapshot?.() }, null, 2)); throw Error(safeError(error)); } finally { await browser?.close(); await https?.close(); await bridge?.close(); await server?.close(); if (attacker) await new Promise(resolve => attacker.close(resolve)); fs.rmSync(temp, { recursive: true, force: true }); } })().catch(error => { console.error(safeError(error)); if(error.cause) console.error(error.cause); process.exitCode = 1; });