mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 05:41:18 +08:00
feat: bound hosted publication windows and add HTTPS acceptance [skip ci]
This commit is contained in:
@@ -3,10 +3,10 @@ import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
const root=path.dirname(fileURLToPath(import.meta.url));
|
||||
export async function startLocal({state,testing=false,boundary,port=0,publishing=true,initialize=true}={}) {
|
||||
export async function startLocal({state,testing=false,boundary,port=0,publishing=true,publishingUntil,initialize=true}={}) {
|
||||
const mf=new Miniflare({...convertV4MiniflareOptions({host:'127.0.0.1',port,workers:[{name:'registry',modules:true,scriptPath:path.join(root,'.build',testing?'test-worker.js':'worker.js'),compatibilityDate:'2026-09-21',
|
||||
d1Databases:{DB:'registry'},r2Buckets:{PACKAGES:'packages'},
|
||||
bindings:{PUBLISHING_ENABLED:String(publishing)},serviceBindings:{
|
||||
bindings:{PUBLISHING_ENABLED:String(publishing),...(publishingUntil===undefined?{}:{PUBLISHING_UNTIL:publishingUntil})},serviceBindings:{
|
||||
ASSETS:async req=>{const name=path.basename(new URL(req.url).pathname);if(!['index.html','app.js','style.css'].includes(name))return new Response('Not found',{status:404});return new Response(fs.readFileSync(path.join(root,'../public',name)),{headers:{'Content-Type':name.endsWith('.html')?'text/html':name.endsWith('.js')?'text/javascript':'text/css'}});},
|
||||
...(boundary?{TEST_BOUNDARY:boundary}:{})}}]}),resourcePersistencePath:state});
|
||||
await mf.ready;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import {startLocal} from './local.mjs';
|
||||
const checks=[];
|
||||
for(const [label,deadline,enabled] of [['future',new Date(Date.now()+60000).toISOString(),true],['expired',new Date(0).toISOString(),false],['invalid','invalid',false]]){
|
||||
const s=await startLocal({publishingUntil:deadline});
|
||||
try{const base='http://127.0.0.1:'+s.port;assert.equal((await(await fetch(base+'/api/health')).json()).publishing,enabled);const r=await fetch(base+'/api/publish',{method:'POST',body:'{}'});assert.equal(r.status,enabled?400:503);checks.push(label+' deadline health and POST agree');}finally{await s.close();}
|
||||
}
|
||||
const s=await startLocal({publishingUntil:new Date(Date.now()+4000).toISOString()});
|
||||
try{const base='http://127.0.0.1:'+s.port;assert.equal((await(await fetch(base+'/api/health')).json()).publishing,true);await new Promise(r=>setTimeout(r,4100));assert.equal((await(await fetch(base+'/api/health')).json()).publishing,false);assert.equal((await fetch(base+'/api/publish',{method:'POST',body:'{}'})).status,503);checks.push('live window expires without deployment or process restart');}finally{await s.close();}
|
||||
console.log(JSON.stringify({checks,success:true}));
|
||||
@@ -15,6 +15,12 @@ function decodeBase64(value:string) {
|
||||
}
|
||||
function json(status:number,data:unknown) { return Response.json(data,{status,headers:{...headers,'Cache-Control':'no-store'}}); }
|
||||
class InputError extends Error {}
|
||||
// Optional operator deadline makes a controlled acceptance window fail closed after interruption.
|
||||
// Existing local fixtures may omit it; hosted acceptance always supplies an absolute UTC deadline.
|
||||
function publishingEnabled(env:any) {
|
||||
return env.PUBLISHING_ENABLED === 'true' &&
|
||||
(env.PUBLISHING_UNTIL === undefined || Date.now() < Date.parse(env.PUBLISHING_UNTIL));
|
||||
}
|
||||
async function readBody(request:Request) {
|
||||
const limit=MAX_PACKAGE_BYTES*1.5;
|
||||
if(Number(request.headers.get('content-length'))>limit) throw new InputError('Submission exceeds 12 MiB envelope limit');
|
||||
@@ -58,7 +64,7 @@ export function createHandler(hook: (phase:string)=>Promise<void> = async()=>{})
|
||||
// All reads use the primary D1 endpoint. No eventually-consistent cache or replicas.
|
||||
const schema=await env.DB.prepare("SELECT value FROM registry_meta WHERE key='schema'").first('value');
|
||||
if(schema!=='1')return json(503,{error:'Registry is not initialized or schema is unsupported; operator must apply migrations'});
|
||||
if(request.method==='GET'&&url.pathname==='/api/health')return json(200,{ok:true,publishing:env.PUBLISHING_ENABLED==='true',protocol:1,storage:'d1-r2',schema:1});
|
||||
if(request.method==='GET'&&url.pathname==='/api/health')return json(200,{ok:true,publishing:publishingEnabled(env),protocol:1,storage:'d1-r2',schema:1});
|
||||
const releases=/^\/api\/extensions\/([a-z0-9-]+)$/.exec(url.pathname);
|
||||
if(request.method==='GET'&&(url.pathname==='/api/extensions'||releases)) {
|
||||
const query=releases?env.DB.prepare('SELECT listing FROM releases WHERE id=?').bind(releases[1]):env.DB.prepare('SELECT listing FROM releases');
|
||||
@@ -78,7 +84,7 @@ export function createHandler(hook: (phase:string)=>Promise<void> = async()=>{})
|
||||
return new Response(bytes,{headers:{...headers,'Content-Type':'application/vnd.codegraph.extension+json','Content-Length':String(row.size),'Cache-Control':'public, max-age=31536000, immutable','Content-Disposition':`attachment; filename="${download[1]}-${encodeURIComponent(version)}.cgext"`}});
|
||||
}
|
||||
if(request.method==='POST'&&url.pathname==='/api/publish') {
|
||||
if(env.PUBLISHING_ENABLED!=='true')return json(503,{error:'Publishing is disabled; operator must verify storage and usage limits before enabling it'});
|
||||
if(!publishingEnabled(env))return json(503,{error:'Publishing is disabled; operator must verify storage and usage limits before enabling it'});
|
||||
const origin=request.headers.get('origin');if(origin&&origin!==url.origin)return json(403,{error:'Publisher origin is not allowed'});
|
||||
const minute=Math.floor(Date.now()/60000),ip=request.headers.get('CF-Connecting-IP')??'local';
|
||||
const rateKey=await digest(utf8.encode(ip+':'+minute));
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
// 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-20260922', 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-20260922:'+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-20260922')).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-20260922');
|
||||
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-20260922');
|
||||
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-20260922')).json()).length, 1);
|
||||
await otherContext.close(); checks.push('browser malformed-package rejection without publication');
|
||||
await page.getByRole('link', { name: 'Marketplace', exact: true }).first().click();
|
||||
await page.getByRole('link', {name:'Browser example',exact:true}).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');
|
||||
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-20260922', 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('<button id="open">Open</button>'); });
|
||||
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-20260922', 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; });
|
||||
@@ -0,0 +1,193 @@
|
||||
// Build first; PLAYWRIGHT_MODULE=/absolute/path/to/playwright node scripts/validation/extensions-compatibility.cjs
|
||||
const fs = require('node:fs'), os = require('node:os'), path = require('node:path');
|
||||
const assert = require('node:assert/strict');
|
||||
const { spawn } = require('node:child_process');
|
||||
const { generateKeyPairSync, sign, randomUUID, createHash } = require('node:crypto');
|
||||
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
||||
const { createExtensionProject, CodeGraph } = require('../../dist');
|
||||
const { packExtension } = require('../../dist/plugins/package');
|
||||
const { startMarketplaceServer } = process.env.CLOUDFLARE_REGISTRY === '1' ? require('./cloudflare-registry.cjs') : require('../../dist/plugins/marketplace');
|
||||
const { startExtensionBridge } = require('../../dist/plugins/bridge');
|
||||
const repo = path.resolve(__dirname, '../..'), out = path.resolve(process.env.COMPAT_OUTPUT || path.join(repo, '.qa/recovery/compatibility'));
|
||||
fs.mkdirSync(out, { recursive: true });
|
||||
const lab = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-compatible-'));
|
||||
const author = path.join(lab, 'author'), checks = [], commands = [], releases = [];
|
||||
const env = { ...process.env, CODEGRAPH_TELEMETRY: '0', CODEGRAPH_PARSE_WORKERS: '2', CODEGRAPH_RESOLVE_WORKERS: '2', CODEGRAPH_PARALLEL_RESOLVE_MIN: '0' };
|
||||
Object.assign(process.env, env);
|
||||
const roots = ['first-python-project', 'selected-python-project', 'headless-python-project'].map(n => path.join(lab, n));
|
||||
for (const root of roots) {
|
||||
fs.mkdirSync(root); fs.writeFileSync(path.join(root, 'checkout.events.yaml'), 'order.created: send_receipt\n');
|
||||
fs.writeFileSync(path.join(root, 'handlers.py'), 'def send_receipt():\n return "sent"\n');
|
||||
}
|
||||
createExtensionProject(author, 'hosted-compatible-20260922');
|
||||
const original = fs.readFileSync(path.join(author, 'index.cjs'), 'utf8');
|
||||
const keys = generateKeyPairSync('ec', { namedCurve: 'prime256v1' });
|
||||
let server, bridge, browser, page, registry, https;
|
||||
let lastPublication=0;
|
||||
async function publicationSlot(){await new Promise(r=>setTimeout(r,Math.max(0,lastPublication+7500-Date.now())));lastPublication=Date.now();}
|
||||
async function publish(version, engines = '>=1.6.0 <2', id = 'hosted-compatible-20260922', broken = false) {
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(author, 'package.json'), 'utf8'));
|
||||
pkg.version = version; pkg.codegraph.id = id; pkg.codegraph.engines = engines;
|
||||
fs.writeFileSync(path.join(author, 'package.json'), JSON.stringify(pkg));
|
||||
fs.writeFileSync(path.join(author, 'index.cjs'), original.replace('Python event dispatch', 'Python event dispatch ' + version).replace('extract(file, source) {', broken ? "extract(file, source) { throw Error('Rejected compatible update');" : 'extract(file, source) {'));
|
||||
const bytes = packExtension(author);
|
||||
const payload = JSON.stringify({ name: id, description: 'Compatible generated Python event extension', publisher: 'Compatibility validation',
|
||||
source: 'https://example.com/extension-source', readme: 'Generated author template; local release selection acceptance.', artifact: bytes.toString('base64'), timestamp: Date.now(), nonce: randomUUID() });
|
||||
const body = { payload, publicKey: keys.publicKey.export({ format: 'jwk' }), signature: sign('sha256', Buffer.from(payload), { key: keys.privateKey, dsaEncoding: 'ieee-p1363' }).toString('base64') };
|
||||
await publicationSlot();
|
||||
const res = await fetch(registry + '/api/publish', { method: 'POST', body: JSON.stringify(body) });
|
||||
assert.equal(res.status, 201, await res.clone().text());
|
||||
releases.push({ id, version, engines, integrity: createHash('sha256').update(bytes).digest('hex') });
|
||||
return { bytes, body };
|
||||
}
|
||||
async function graph(root) {
|
||||
if (!CodeGraph.isInitialized(root)) return [];
|
||||
const g = await CodeGraph.open(root);
|
||||
try { return g.getNodesByKind('route').flatMap(n => g.getOutgoingEdges(n.id).filter(e => e.metadata?.synthesizedBy === 'hosted-compatible-20260922').map(e => ({ source: n.name, target: g.getNode(e.target)?.name, label: e.metadata.label }))); }
|
||||
finally { g.close(); }
|
||||
}
|
||||
async function expectGraph(root, version) {
|
||||
assert.deepEqual(await graph(root), [{ source: 'event:order.created', target: 'send_receipt', label: 'Python event dispatch ' + version }]);
|
||||
}
|
||||
async function cli(args, expected = 0) {
|
||||
const command = [path.join(repo, 'dist/bin/codegraph.js'), 'extensions', ...args];
|
||||
const start = Date.now();
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const child = spawn(process.execPath, command, { env, cwd: lab, timeout: 120000 });
|
||||
let stdout = '', stderr = ''; child.stdout.on('data', b => stdout += b); child.stderr.on('data', b => stderr += b);
|
||||
child.on('error', reject); child.on('close', exit => resolve({ exit, stdout, stderr }));
|
||||
});
|
||||
commands.push({ command: [process.execPath, ...command], exit: result.exit, seconds: (Date.now() - start) / 1000 });
|
||||
fs.writeFileSync(path.join(out, 'commands.json'), JSON.stringify(commands, null, 2));
|
||||
fs.writeFileSync(path.join(out, `command-${commands.length}.log`), result.stdout + result.stderr);
|
||||
assert.equal(result.exit, expected, result.stderr); return result;
|
||||
}
|
||||
(async () => {
|
||||
try {
|
||||
registry = process.env.HOSTED_REGISTRY;
|
||||
assert.match(registry || '', /^https:\/\/[a-z0-9.-]+\.workers\.dev$/);
|
||||
assert.equal((await (await fetch(registry+'/api/extensions/hosted-compatible-20260922')).json()).length,0,'Use a fresh isolated target or explicitly rename this owned fixture before another run');
|
||||
await cli(['test', author]);
|
||||
await cli(['pack', author, '--out', path.join(lab,'starter.cgext')]);
|
||||
if (process.env.REGISTRY_TEST_HTTPS === '1') { https = await require('./https-marketplace.cjs').httpsMarketplace(server.port, out); registry = https.origin; }
|
||||
// Publication order intentionally differs from semantic order; previews and
|
||||
// a newer incompatible release must never displace the compatible stable one.
|
||||
const pinned = await publish('1.10.0'); await publish('1.9.0'); await publish('0.5.0');
|
||||
await publish('20.0.0-beta.1'); await publish('9.0.0', '>=9'); await publish('1.0.0', '>=9', 'hosted-unavailable-20260922');
|
||||
await publicationSlot();
|
||||
const duplicate = await fetch(registry + '/api/publish', { method: 'POST', body: JSON.stringify(pinned.body) });
|
||||
assert.equal(duplicate.status, 400);
|
||||
const all = await (await fetch(registry + '/api/extensions/hosted-compatible-20260922')).json();
|
||||
assert.deepEqual(all.map(r => r.version), ['9.0.0', '1.10.0', '1.9.0', '0.5.0', '20.0.0-beta.1']);
|
||||
assert.equal(all[0].apiVersion, 1); checks.push('signed immutable versions; semantic catalog order independent of publication order');
|
||||
bridge = await startExtensionBridge(roots.slice(0, 2), registry);
|
||||
browser = await chromium.launch({ executablePath: process.env.CHROME_PATH || '/opt/google/chrome/chrome', headless: true, args: ['--no-sandbox'] });
|
||||
page = await browser.newPage({ viewport: { width: 1440, height: 1000 } });
|
||||
page.setDefaultTimeout(60000);
|
||||
const errors = []; page.on('pageerror', e => errors.push(e.message));
|
||||
await page.goto(bridge.connectionUrl);
|
||||
if (registry.startsWith('https:')) assert.equal(await page.evaluate(() => isSecureContext), true);
|
||||
const popupWait = page.waitForEvent('popup'); await page.locator('#connect').click(); const popup = await popupWait;
|
||||
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('.compatibility').filter({ hasText: 'Selected 1.10.0' }).waitFor();
|
||||
await page.getByRole('link', { name: 'hosted-compatible-20260922', exact: true }).click();
|
||||
await page.locator('.compatibility').filter({ hasText: 'selected-python-project' }).waitFor();
|
||||
await page.screenshot({ path: path.join(out, 'desktop-selected.png'), fullPage: true });
|
||||
// Exactly one Install after connection; wait for the real companion job.
|
||||
await page.getByRole('button', { name: 'Install extension' }).click();
|
||||
await page.waitForFunction(() => state.snapshot?.jobId === 1 && !state.snapshot.busy);
|
||||
assert.equal(await page.evaluate(() => state.snapshot.error), undefined);
|
||||
await expectGraph(roots[1], '1.10.0'); assert.deepEqual(await graph(roots[0]), []);
|
||||
checks.push('one browser Install selects 1.10.0 over incompatible 9.0.0 and preview; actual Python graph refreshed only in chosen destination');
|
||||
// Hold actual bridge resolution to inspect the transient destination state.
|
||||
await page.evaluate(() => {
|
||||
const originalRpc = rpc;
|
||||
const gate = new Promise(resolve => window.releaseSelection = resolve);
|
||||
rpc = (type, value) => type === 'command' && value?.action === 'resolve' ? gate.then(() => originalRpc(type, value)) : originalRpc(type, value);
|
||||
window.pendingSelection = refreshSelections().finally(() => { rpc = originalRpc; });
|
||||
});
|
||||
await page.screenshot({ path: path.join(out, 'metadata-pending.png'), fullPage: true });
|
||||
assert.equal(await page.locator('[data-installed-version]').innerText(), '1.10.0');
|
||||
assert.equal(await page.locator('[data-selected-version]').innerText(), 'Checking compatibility…');
|
||||
assert.equal(await page.locator('a[href^="/api/download/"]').count(), 0);
|
||||
await page.evaluate(async () => { releaseSelection(); await pendingSelection; });
|
||||
assert.equal(await page.locator('[data-selected-version]').innerText(), '1.10.0');
|
||||
assert.equal(await page.locator('[data-installed-version]').innerText(), '1.10.0');
|
||||
checks.push('pending compatibility refresh never shows incompatible catalog version as selected; installed and selected versions remain distinct');
|
||||
await page.screenshot({ path: path.join(out, 'desktop-installed.png'), fullPage: true });
|
||||
const config = fs.readFileSync(path.join(roots[1], 'codegraph.json'), 'utf8');
|
||||
const noChoice = await page.evaluate(async () => {
|
||||
try { await rpc('command', { action: 'resolve', id: 'hosted-unavailable-20260922', project: '1' }); return ''; } catch (e) { return e.message; }
|
||||
});
|
||||
assert.match(noChoice, /No compatible stable release/);
|
||||
await page.evaluate(() => rpc('command', { action: 'install', id: 'hosted-unavailable-20260922', project: '1' }));
|
||||
await page.waitForFunction(() => state.snapshot?.jobId === 2 && !state.snapshot.busy);
|
||||
assert.match(await page.evaluate(() => state.snapshot.error), /No compatible stable release/);
|
||||
assert.equal(fs.readFileSync(path.join(roots[1], 'codegraph.json'), 'utf8'), config); await expectGraph(roots[1], '1.10.0');
|
||||
await page.evaluate(() => navigate('/extensions/hosted-unavailable-20260922'));
|
||||
await page.locator('.compatibility').filter({ hasText: 'No compatible stable release' }).waitFor();
|
||||
assert.equal(await page.getByRole('button', { name: 'Install extension' }).isDisabled(), true);
|
||||
checks.push('no-compatible browser selection/command fails usefully and preserves existing config/graph');
|
||||
// Independent CLI destination resolves exactly the same release.
|
||||
await cli(['install', 'hosted-compatible-20260922', '--registry', registry, '--path', roots[2]]); await expectGraph(roots[2], '1.10.0');
|
||||
const headlessConfig = fs.readFileSync(path.join(roots[2], 'codegraph.json'), 'utf8');
|
||||
const exactFailure = await cli(['install', 'hosted-compatible-20260922', '--registry', registry, '--version', '9.0.0', '--path', roots[2]], 1);
|
||||
assert.match(exactFailure.stderr, /No compatible exact release 9.0.0/);
|
||||
await cli(['update', 'hosted-unavailable-20260922', '--registry', registry, '--path', roots[2]], 1);
|
||||
const none = await cli(['install', 'hosted-unavailable-20260922', '--registry', registry, '--path', roots[2]], 1);
|
||||
assert.match(none.stderr, /No compatible stable release/);
|
||||
assert.equal(fs.readFileSync(path.join(roots[2], 'codegraph.json'), 'utf8'), headlessConfig); await expectGraph(roots[2], '1.10.0');
|
||||
checks.push('headless automatic selection agrees; exact incompatible pin/no-compatible/missing update fail without mutation');
|
||||
await cli(['install', 'hosted-compatible-20260922', '--registry', registry, '--version', '20.0.0-beta.1', '--path', roots[2]]);
|
||||
await expectGraph(roots[2], '20.0.0-beta.1');
|
||||
const previewConfig = fs.readFileSync(path.join(roots[2], 'codegraph.json'), 'utf8');
|
||||
const downgrade = await cli(['update', 'hosted-compatible-20260922', '--registry', registry, '--path', roots[2]], 1);
|
||||
assert.match(downgrade.stderr, /would downgrade/);
|
||||
assert.equal(fs.readFileSync(path.join(roots[2], 'codegraph.json'), 'utf8'), previewConfig); await expectGraph(roots[2], '20.0.0-beta.1');
|
||||
await cli(['install', 'hosted-compatible-20260922', '--registry', registry, '--version', '1.9.0', '--path', roots[2]]); await expectGraph(roots[2], '1.9.0');
|
||||
checks.push('prerelease requires explicit exact pin; automatic update refuses downgrade; explicit lower pin remains exact');
|
||||
await publish('2.0.0', '>=1.6.0 <2', 'hosted-compatible-20260922', true);
|
||||
await page.reload(); const reconnect = page.waitForEvent('popup');
|
||||
// A named existing popup may be reused: reconnect using a fresh window.
|
||||
await popup.close(); await page.locator('#connect').click(); const second = await reconnect;
|
||||
await second.getByRole('button', { name: 'Allow connection' }).click();
|
||||
await page.getByText('CodeGraph connected', { exact: true }).waitFor();
|
||||
await page.evaluate(() => { state.project = '1'; navigate('/extensions/hosted-compatible-20260922'); return refreshSelections(); });
|
||||
await page.getByRole('button', { name: 'Update to 2.0.0' }).click();
|
||||
await page.waitForFunction(() => state.snapshot?.jobId === 3 && !state.snapshot.busy);
|
||||
assert.match(await page.evaluate(() => state.snapshot.error), /Extension activation failed/);
|
||||
await expectGraph(roots[1], '1.10.0'); assert.equal(fs.readFileSync(path.join(roots[1], 'codegraph.json'), 'utf8'), config);
|
||||
checks.push('compatible but broken update retains previous version and graph');
|
||||
await publish('3.0.0');
|
||||
await page.evaluate(() => refresh());
|
||||
await page.getByRole('button', { name: 'Update to 3.0.0' }).click();
|
||||
await page.waitForFunction(() => state.snapshot?.jobId === 4 && !state.snapshot.busy);
|
||||
assert.equal(await page.evaluate(() => state.snapshot.error), undefined); await expectGraph(roots[1], '3.0.0');
|
||||
await cli(['update', 'hosted-compatible-20260922', '--registry', registry, '--path', roots[2]]); await expectGraph(roots[2], '3.0.0');
|
||||
checks.push('browser and headless update to highest compatible stable; stale graph labels removed');
|
||||
// Exact URL and file install remain independent of automatic registry selection.
|
||||
await cli(['install', registry + '/api/download/hosted-compatible-20260922/1.9.0', '--path', roots[2]]); await expectGraph(roots[2], '1.9.0');
|
||||
const file = path.join(lab, 'pinned.cgext'); fs.writeFileSync(file, pinned.bytes);
|
||||
await cli(['install', file, '--path', roots[2]]); await expectGraph(roots[2], '1.10.0');
|
||||
await publish('1.10.0+fixture.1');
|
||||
await cli(['install', 'hosted-compatible-20260922', '--registry', registry, '--version', '1.10.0+fixture.1', '--path', roots[2]]);
|
||||
await expectGraph(roots[2], '1.10.0+fixture.1');
|
||||
checks.push('exact artifact URL/file semantics and build-metadata pin identity preserved');
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.locator('.compatibility').filter({ hasText: 'Selected 3.0.0' }).waitFor();
|
||||
assert.ok(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth));
|
||||
await page.screenshot({ path: path.join(out, 'mobile-selected.png'), fullPage: true });
|
||||
await page.getByRole('button', { name: 'Remove', exact: true }).click();
|
||||
await page.waitForFunction(() => state.snapshot?.jobId === 5 && !state.snapshot.busy);
|
||||
await cli(['remove', 'hosted-compatible-20260922', '--path', roots[2]]);
|
||||
assert.deepEqual(await graph(roots[1]), []); assert.deepEqual(await graph(roots[2]), []);
|
||||
assert.deepEqual(errors, []); checks.push('removal cleans contributions; desktop/mobile display actual destination compatibility; no page errors');
|
||||
fs.writeFileSync(path.join(out, 'result.json'), JSON.stringify({ registry, checks, releases, commands, engineVersion: require('../../package.json').version, platform: process.platform, browser: browser.version() }, null, 2));
|
||||
console.log(JSON.stringify({ checks, commands: commands.length }));
|
||||
} catch (error) {
|
||||
await page?.screenshot({ path: path.join(out, 'failure.png'), fullPage: true }).catch(() => {});
|
||||
fs.writeFileSync(path.join(out, 'failure.json'), JSON.stringify({ checks, error: String(error.stack).replace(/token=[a-f0-9]+/g, 'token=[redacted]') }, null, 2)); throw error;
|
||||
} finally { await browser?.close(); await https?.close(); await bridge?.close(); await server?.close(); fs.rmSync(lab, { recursive: true, force: true }); }
|
||||
})().catch(error => { console.error(String(error.stack).replace(/token=[a-f0-9]+/g, 'token=[redacted]')); if(error.cause) console.error(error.cause); process.exitCode = 1; });
|
||||
@@ -0,0 +1,36 @@
|
||||
// Actual deployed Worker HTTPS -> browser -> compiled installer -> pinned Drupal corpus.
|
||||
// Uses a previously reviewed/imported immutable artifact. Never mutates remote storage.
|
||||
const fs=require('node:fs'),path=require('node:path'),os=require('node:os'),assert=require('node:assert/strict'),cp=require('node:child_process'),crypto=require('node:crypto');
|
||||
const {DatabaseSync}=require('node:sqlite');
|
||||
const {chromium}=require(process.env.PLAYWRIGHT_MODULE||'playwright');
|
||||
const {CodeGraph}=require('../../dist');
|
||||
const {ExtensionManager}=require('../../dist/plugins/manager');
|
||||
const {startExtensionBridge}=require('../../dist/plugins/bridge');
|
||||
Object.assign(process.env,{CODEGRAPH_TELEMETRY:'0',CODEGRAPH_PARSE_WORKERS:'2',CODEGRAPH_RESOLVE_WORKERS:'0'});
|
||||
const root=path.resolve(__dirname,'../..'),out=path.resolve(process.env.OFFICIAL_BROWSER_OUTPUT||'.qa/hosted/drupal-browser');fs.mkdirSync(out,{recursive:true});
|
||||
const lab=fs.mkdtempSync(path.join(os.tmpdir(),'cg-official-browser-')),corpus=path.resolve(process.env.PATHAUTO_CORPUS||'../validation/pathauto'),pin='b97aadf47a37cff25f7d6105c3dade6778fccb47';
|
||||
const project=path.join(lab,'pathauto'),other=path.join(lab,'untouched'),state=path.join(lab,'registry'),artifact=path.join(root,'dist/extensions/drupal.cgext'),bytes=fs.readFileSync(artifact),checks=[],commands=[];
|
||||
fs.mkdirSync(project);fs.mkdirSync(other);fs.writeFileSync(path.join(other,'example.php'),'<?php function untouched() {}');
|
||||
cp.execFileSync('tar',['-x','-C',project],{input:cp.execFileSync('git',['-C',corpus,'archive',pin],{maxBuffer:64*1024*1024})});
|
||||
const extensionVersion=JSON.parse(bytes).package.version;
|
||||
const hash=b=>crypto.createHash('sha256').update(b).digest('hex');
|
||||
function snapshot(){const db=new DatabaseSync(path.join(project,'.codegraph/codegraph.db'),{readOnly:true});try{const nodes=db.prepare('SELECT * FROM nodes ORDER BY id').all().map(({updated_at,...n})=>n),edges=db.prepare('SELECT source,target,kind,metadata,line,col,provenance FROM edges ORDER BY source,target,kind,line,col,metadata').all();return {nodes,edges,pluginEdges:edges.filter(e=>JSON.parse(e.metadata||'{}').synthesizedBy==='drupal'),hash:hash(JSON.stringify({nodes,edges})),config:fs.existsSync(path.join(project,'codegraph.json'))?fs.readFileSync(path.join(project,'codegraph.json'),'utf8'):null};}finally{db.close();}}
|
||||
function verify(data){const definitions=[['/admin/config/search/path/settings','pathauto.routing.yml','buildForm','src/Form/PathautoSettingsForm.php','Drupal route handler'],['/admin/config/search/path/update_bulk','pathauto.routing.yml','buildForm','src/Form/PathautoBulkUpdateForm.php','Drupal route handler'],['getPunctuationCharacters','src/AliasCleaner.php','pathautoPunctuationCharsAlter','tests/modules/pathauto_custom_punctuation_test/src/Hook/PathautoCustomPunctuationTestHooks.php','Drupal hook pathauto_punctuation_chars_alter'],['PathautoGenerator','src/PathautoGenerator.php','AliasCleaner','src/AliasCleaner.php','Drupal service injection']];for(const [from,file,to,target,label]of definitions){const sources=new Set(data.nodes.filter(n=>n.name===from&&n.file_path===file).map(n=>n.id)),targets=new Set(data.nodes.filter(n=>n.name===to&&n.file_path===target).map(n=>n.id));assert.ok(data.pluginEdges.some(e=>sources.has(e.source)&&targets.has(e.target)&&JSON.parse(e.metadata).label===label),'Missing '+label+': '+from);}return definitions;}
|
||||
function pass(name){checks.push(name);fs.writeFileSync(path.join(out,'progress.json'),JSON.stringify({checks,commands},null,2));console.log('PASS',name);}
|
||||
async function cli(args){const command=[path.join(root,'marketplace/cloudflare/official-cli.mjs'),...args];const r=await new Promise((resolve,reject)=>{const c=cp.spawn(process.execPath,command,{timeout:60000});let log='';c.stdout.on('data',b=>log+=b);c.stderr.on('data',b=>log+=b);c.on('error',reject);c.on('close',exit=>resolve({exit,log}));});const log='operator-'+(commands.length+1)+'.log';fs.writeFileSync(path.join(out,log),r.log);commands.push({command:[process.execPath,...command],exit:r.exit,log,sha256:hash(r.log)});assert.equal(r.exit,0,r.log);return r.log;}
|
||||
let server,bridge,browser,page;
|
||||
(async()=>{try{
|
||||
const origin=process.env.HOSTED_REGISTRY;assert.ok(/^https:\/\/[-a-z0-9.]+\.workers\.dev$/.test(origin),'Explicit workers.dev origin required');
|
||||
const catalog=await(await fetch(origin+'/api/extensions')).json(),listing=catalog.find(x=>x.id==='drupal');assert.equal(listing.official,true);assert.equal(listing.publisherId,'codegraph');assert.equal(listing.integrity,hash(bytes));const downloaded=Buffer.from(await(await fetch(origin+'/api/download/drupal/'+extensionVersion)).arrayBuffer());assert.deepEqual(downloaded,bytes);pass('actual hosted catalog and immutable Drupal bytes match reviewed official policy');
|
||||
const g=await CodeGraph.init(project);try{const r=await g.indexAll();assert.equal(r.filesErrored,0);}finally{g.close();}assert.equal(snapshot().pluginEdges.length,0);
|
||||
bridge=await startExtensionBridge([other,project],origin);browser=await chromium.launch({executablePath:process.env.CHROME_PATH||'/opt/google/chrome/chrome',headless:true,args:['--no-sandbox']});page=await browser.newPage({viewport:{width:1440,height:1000}});page.setDefaultTimeout(60000);let popups=0;page.on('popup',()=>popups++);const pageErrors=[];page.on('pageerror',e=>pageErrors.push(e.message));
|
||||
await page.goto(bridge.connectionUrl);assert.equal(await page.evaluate(()=>isSecureContext),true);await page.getByRole('button',{name:'Official',exact:true}).click();await page.locator('.extension-card .official').waitFor();assert.equal(await page.locator('.extension-card').count(),1);assert.match(await page.locator('.extension-card .byline').innerText(),/CodeGraph.*OFFICIAL/s);pass('official catalog filter displays trusted CodeGraph Drupal identity');
|
||||
const popupReady=page.waitForEvent('popup');await page.locator('#connect').click();const popup=await popupReady;await popup.getByRole('button',{name:'Allow connection'}).click();await page.getByText('CodeGraph connected',{exact:true}).waitFor();await page.locator('#project').selectOption('1');await page.getByRole('link',{name:'Drupal',exact:true}).click();await page.locator('.compatibility').filter({hasText:'Selected '+extensionVersion}).waitFor();assert.equal(await page.locator('.metadata dd.integrity').first().innerText(),'codegraph');await page.getByRole('button',{name:'Install extension'}).click();await page.waitForFunction(()=>state.snapshot?.jobId===1&&!state.snapshot.busy);assert.equal(await page.evaluate(()=>state.snapshot.error),undefined);assert.equal(popups,1);const installed=snapshot(),flows=verify(installed);assert.ok(!CodeGraph.isInitialized(other));pass('one connection and one Install refresh only selected pinned Drupal project; four real route hook and DI edges verified');
|
||||
fs.writeFileSync(path.join(out,'graph-proof.json'),JSON.stringify({corpus:'pathauto',revision:pin,artifactSHA:hash(bytes),nodes:installed.nodes.length,edges:installed.edges.length,pluginEdges:installed.pluginEdges.length,hash:installed.hash,expected:flows},null,2));await page.screenshot({path:path.join(out,'desktop-installed.png'),fullPage:true});
|
||||
const manager=new ExtensionManager(project),realFetch=global.fetch;let tampered=false;global.fetch=async(...args)=>{const r=await realFetch(...args);if(String(args[0]).includes('/api/download/drupal/')){tampered=true;return new Response(Buffer.concat([Buffer.from(await r.arrayBuffer()),Buffer.from(' ')]),{status:r.status,headers:r.headers});}return r;};try{await assert.rejects(()=>manager.installFromRegistry({registry:origin,id:'drupal',version:extensionVersion,replaces:['drupal']}),/integrity/);}finally{global.fetch=realFetch;}assert.equal(tampered,true);assert.equal(snapshot().hash,installed.hash);assert.equal(snapshot().config,installed.config);pass('test-owned transport corruption after real HTTPS download is rejected; hosted immutable object untouched');
|
||||
const incompatible=JSON.parse(bytes);incompatible.package.codegraph.engines='>=9';await assert.rejects(()=>manager.install({bytes:Buffer.from(JSON.stringify(incompatible)),replaces:['drupal']}),/requires/);await assert.rejects(()=>manager.install({bytes:Buffer.from('{}'),replaces:['drupal']}));assert.equal(snapshot().hash,installed.hash);assert.equal(snapshot().config,installed.config);pass('incompatible and malformed installs preserve previously working Drupal graph');
|
||||
await page.getByRole('button',{name:'Disable',exact:true}).click();await page.getByRole('button',{name:'Enable',exact:true}).waitFor();assert.equal(snapshot().pluginEdges.length,0);pass('browser disable removes actual extension contributions');
|
||||
await page.getByRole('button',{name:'Enable',exact:true}).click();await page.getByRole('button',{name:'Disable',exact:true}).waitFor();verify(snapshot());await page.setViewportSize({width:390,height:844});await page.screenshot({path:path.join(out,'mobile-official.png'),fullPage:true});pass('browser enable restores expected graph and mobile official details');
|
||||
await page.getByRole('button',{name:'Remove',exact:true}).click();await page.getByText('Extension removed and graph refreshed',{exact:true}).first().waitFor();assert.equal(snapshot().pluginEdges.length,0);assert.equal(new ExtensionManager(project).list().length,0);assert.ok(!CodeGraph.isInitialized(other));assert.deepEqual(pageErrors,[]);pass('browser removal clears extension config and SQLite contributions; no page errors');
|
||||
fs.writeFileSync(path.join(out,'result.json'),JSON.stringify({revision:cp.execFileSync('git',['rev-parse','HEAD'],{encoding:'utf8'}).trim(),dirty:!!cp.execFileSync('git',['status','--porcelain'],{encoding:'utf8'}).trim(),platform:process.platform,origin,browser:browser.version(),corpus:{name:'pathauto',revision:pin},checks,commands,success:true},null,2));
|
||||
}catch(error){if(page)await page.screenshot({path:path.join(out,'failure.png'),fullPage:true}).catch(()=>{});fs.writeFileSync(path.join(out,'failure.json'),JSON.stringify({checks,commands,error:String(error.stack)},null,2));throw error;}finally{await browser?.close();await bridge?.close();await server?.close();fs.rmSync(lab,{recursive:true,force:true});}})().catch(e=>{console.error(e);process.exitCode=1;});
|
||||
@@ -0,0 +1,28 @@
|
||||
// Controlled writes only to a dedicated preview with a bounded publication window.
|
||||
const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict');
|
||||
const {generateKeyPairSync,sign,randomUUID,createHash}=require('node:crypto');
|
||||
const origin=process.env.HOSTED_REGISTRY,out=path.resolve(process.env.HOSTED_PACKAGE_OUTPUT||'.qa/hosted/packages');
|
||||
assert.match(origin||'',/^https:\/\/[a-z0-9.-]+\.workers\.dev$/);fs.mkdirSync(out,{recursive:true});
|
||||
const suffix=Date.now().toString(36),keys=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),checks=[],requests=[];
|
||||
const hash=b=>createHash('sha256').update(b).digest('hex');
|
||||
function artifact(id,version='1.0.0',size){const p={format:'codegraph-extension-1',package:{name:'@preview/'+id,version,main:'index.cjs',codegraph:{id,apiVersion:1,capabilities:['frameworks']}},files:{'index.cjs':'module.exports=()=>({frameworks:[]});'}};if(size){p.files['padding.txt']='';p.files['padding.txt']='x'.repeat(size-Buffer.byteLength(JSON.stringify(p)));}return Buffer.from(JSON.stringify(p));}
|
||||
function signed(bytes,key=keys,extra={}){const payload=JSON.stringify({name:'Hosted acceptance fixture',description:'Disposable preview contract check',publisher:'Preview validation',source:'https://github.com/colbymchenry/codegraph',readme:'Test fixture. Not a recommended extension.',artifact:bytes.toString('base64'),timestamp:Date.now(),nonce:randomUUID(),...extra});return{payload,publicKey:key.publicKey.export({format:'jwk'}),signature:sign('sha256',Buffer.from(payload),{key:key.privateKey,dsaEncoding:'ieee-p1363'}).toString('base64')};}
|
||||
let last=0;
|
||||
async function post(body,expected,headers={}){await new Promise(r=>setTimeout(r,Math.max(0,last+7500-Date.now())));last=Date.now();const start=new Date().toISOString(),t=performance.now(),r=await fetch(origin+'/api/publish',{method:'POST',headers,body:JSON.stringify(body)}),data=await r.json();requests.push({start,path:'/api/publish',status:r.status,wallMs:performance.now()-t,expected,result:data});fs.writeFileSync(path.join(out,'progress.json'),JSON.stringify({origin,checks,requests},null,2));assert.equal(r.status,expected,JSON.stringify(data));return data;}
|
||||
const passed=x=>{checks.push(x);console.log('PASS',x);};
|
||||
(async()=>{
|
||||
assert.equal((await(await fetch(origin+'/api/health')).json()).publishing,true);
|
||||
const id='hosted-max-'+suffix,large=artifact(id,'1.0.0',8*1024*1024);assert.equal(large.length,8*1024*1024);const published=await post(signed(large),201);assert.equal(published.integrity,hash(large));
|
||||
for(let i=0;i<3;i++){const start=new Date().toISOString(),t=performance.now(),r=await fetch(origin+'/api/download/'+id+'/1.0.0?sample='+i),bytes=Buffer.from(await r.arrayBuffer());assert.equal(r.status,200);assert.equal(bytes.length,large.length);assert.equal(hash(bytes),hash(large));requests.push({start,path:'/api/download/'+id+'/1.0.0?sample='+i,status:r.status,wallMs:performance.now()-t,bytes:bytes.length,sha256:hash(bytes),cacheControl:r.headers.get('cache-control')});}passed('full 8 MiB signed upload and three byte-identical provider downloads');
|
||||
await post(signed(Buffer.concat([large,Buffer.from(' ')])),400);passed('8 MiB plus one byte rejected by trusted package validator');
|
||||
await post(signed(artifact('spoof-'+suffix),keys,{official:true,publisherId:'codegraph'}),400);passed('signed community official/publisher spoof rejected');
|
||||
await post({...signed(artifact('signature-'+suffix)),signature:'AAAA'},400);await post(signed(Buffer.from('{}')),400);passed('invalid signature and malformed package rejected');
|
||||
await post(signed(artifact(id,'2.0.0'),generateKeyPairSync('ec',{namedCurve:'prime256v1'})),400);await post(signed(artifact(id,'1.0.0')),400);passed('ownership and immutable version reject without replacing bytes');
|
||||
await post(signed(artifact('origin-'+suffix)),403,{Origin:'https://attacker.example'});passed('foreign browser origin rejected by provider');
|
||||
// Simultaneous first claims through independent requests, without spoofing Cloudflare IP headers.
|
||||
await new Promise(r=>setTimeout(r,6500));
|
||||
const race='race-'+suffix,one=signed(artifact(race)),two=signed(artifact(race),generateKeyPairSync('ec',{namedCurve:'prime256v1'}));
|
||||
const rr=await Promise.all([one,two].map(async body=>{const start=new Date().toISOString(),r=await fetch(origin+'/api/publish',{method:'POST',body:JSON.stringify(body)}),data=await r.json();requests.push({start,path:'/api/publish',status:r.status,result:data});return r.status;}));assert.deepEqual(rr.sort(),[201,400]);passed('actual concurrent provider ownership/version claim has exactly one winner');
|
||||
const release=await(await fetch(origin+'/api/extensions/'+id)).json();assert.equal(release.length,1);assert.equal(release[0].integrity,hash(large));assert.equal(hash(Buffer.from(await(await fetch(origin+'/api/download/'+id+'/1.0.0')).arrayBuffer())),hash(large));passed('all rejected requests retain original catalog and bytes');
|
||||
fs.writeFileSync(path.join(out,'result.json'),JSON.stringify({origin,checks,requests,large:{id,bytes:large.length,sha256:hash(large)},success:true},null,2));
|
||||
})().catch(e=>{fs.writeFileSync(path.join(out,'failure.json'),JSON.stringify({origin,checks,requests,error:String(e.stack)},null,2));console.error(e);process.exitCode=1;});
|
||||
Reference in New Issue
Block a user