diff --git a/marketplace/cloudflare/hosted-ops-test.mjs b/marketplace/cloudflare/hosted-ops-test.mjs index c2fa2ad5..ce98fa51 100644 --- a/marketplace/cloudflare/hosted-ops-test.mjs +++ b/marketplace/cloudflare/hosted-ops-test.mjs @@ -1,5 +1,5 @@ import fs from 'node:fs';import os from 'node:os';import path from 'node:path';import assert from 'node:assert/strict'; -import {reviewedConfig,checkVersion} from './hosted-ops.mjs';import {hash} from './official.mjs'; +import {reviewedConfig,checkVersion,latestDeployment} from './hosted-ops.mjs';import {hash} from './official.mjs'; const dir=fs.mkdtempSync(path.join(os.tmpdir(),'hosted-ops-')),file=path.join(dir,'config.json'); const c={account_id:'a'.repeat(32),name:'codegraph-marketplace-preview-test',routes:[],vars:{PUBLISHING_ENABLED:'false'},d1_databases:[{binding:'DB',database_name:'codegraph-marketplace-preview-test',database_id:'11111111-1111-1111-1111-111111111111',remote:true}],r2_buckets:[{binding:'PACKAGES',bucket_name:'codegraph-marketplace-preview-test',remote:true}]}; let count=0;function saved(v){const raw=JSON.stringify(v);fs.writeFileSync(file,raw);return hash(raw);} @@ -7,4 +7,7 @@ try{assert.equal(reviewedConfig(file,saved(c)).account_id,c.account_id);count++; for(const change of [x=>x.vars.PUBLISHING_ENABLED='true',x=>x.d1_databases[0].remote=false,x=>x.r2_buckets[0].remote=false,x=>x.d1_databases[0].database_name='codegraph-telemetry',x=>x.d1_databases[0].database_id='00000000-0000-0000-0000-000000000000',x=>x.routes.push('example.com/*')]){const v=structuredClone(c);change(v);assert.throws(()=>reviewedConfig(file,saved(v)));count++;} const version={resources:{bindings:[{name:'DB',type:'d1',id:c.d1_databases[0].database_id},{name:'PACKAGES',type:'r2_bucket',bucket_name:c.r2_buckets[0].bucket_name},{name:'PUBLISHING_ENABLED',type:'plain_text',text:'false'},{name:'ASSETS',type:'assets'}]}};checkVersion(c,version);count++; for(const change of [x=>x.resources.bindings[0].id='other',x=>x.resources.bindings[1].bucket_name='other',x=>x.resources.bindings[2].text='true',x=>x.resources.bindings.push({name:'OTHER',type:'service'})]){const v=structuredClone(version);change(v);assert.throws(()=>checkVersion(c,v));count++;} +const old={id:'old',created_on:'2026-09-22T17:00:00Z'},current={id:'new',created_on:'2026-09-22T21:00:00Z'}; +for(const order of [[old,current],[current,old]]){assert.equal(latestDeployment(order).id,'new');count++;} +for(const invalid of [[],[{}],[old,{...old,id:'tie'}]]){assert.throws(()=>latestDeployment(invalid));count++;} console.log(JSON.stringify({passed:count,scope:'operator admission/config/live-binding guards; no remote calls'}));}finally{fs.rmSync(dir,{recursive:true,force:true});} diff --git a/marketplace/cloudflare/hosted-ops.mjs b/marketplace/cloudflare/hosted-ops.mjs index 8f130e9c..8c00bb42 100644 --- a/marketplace/cloudflare/hosted-ops.mjs +++ b/marketplace/cloudflare/hosted-ops.mjs @@ -17,6 +17,12 @@ export function checkVersion(c,version) { const b=version.resources?.bindings||[]; if(!b.some(x=>x.name==='DB'&&x.type==='d1'&&(x.id||x.database_id)===c.d1_databases[0].database_id)||!b.some(x=>x.name==='PACKAGES'&&x.type==='r2_bucket'&&x.bucket_name===c.r2_buckets[0].bucket_name)||!b.some(x=>x.name==='PUBLISHING_ENABLED'&&x.text==='false')||b.some(x=>!['DB','PACKAGES','PUBLISHING_ENABLED','ASSETS'].includes(x.name)))throw Error('Live deployment bindings/publication state differ from reviewed target'); } +export function latestDeployment(deployments) { + if(!Array.isArray(deployments)||!deployments.length||deployments.some(x=>!Number.isFinite(Date.parse(x.created_on))))throw Error('Cannot establish current deployment timestamp'); + const ordered=[...deployments].sort((a,b)=>Date.parse(b.created_on)-Date.parse(a.created_on)); + if(ordered[1]&&Date.parse(ordered[0].created_on)===Date.parse(ordered[1].created_on))throw Error('Ambiguous current deployment'); + return ordered[0]; +} export async function connect(configPath,configHash,{detached=false}={}) { const c=reviewedConfig(configPath,configHash); const cli=path.join(here,'node_modules/wrangler/bin/wrangler.js'); @@ -26,7 +32,7 @@ export async function connect(configPath,configHash,{detached=false}={}) { try{deployments=command('deployments','list');}catch(e){if(!detached||!String(e.stderr).includes('10007'))throw Error('Cannot verify live deployment; no binding operation started');deployments=[];} let version; if(detached){if(!/-restore(?:-|$)/.test(c.name)||deployments.length)throw Error('Restore must use a new detached restore Worker name');} - else {const current=deployments[0];if(current?.versions?.length!==1||current.versions[0].percentage!==100)throw Error('Require one fully deployed version');version=command('versions','view',current.versions[0].version_id);checkVersion(c,version);} + else {const current=latestDeployment(deployments);if(current?.versions?.length!==1||current.versions[0].percentage!==100)throw Error('Require one fully deployed version');version=command('versions','view',current.versions[0].version_id);checkVersion(c,version);} const proxy=await getPlatformProxy({configPath,remoteBindings:true,persist:false}); return {c,db:proxy.env.DB,bucket:proxy.env.PACKAGES,version,close:()=>proxy.dispose()}; } diff --git a/scripts/validation/cloudflare-hosted-packages-resume.cjs b/scripts/validation/cloudflare-hosted-packages-resume.cjs new file mode 100644 index 00000000..4f88ce51 --- /dev/null +++ b/scripts/validation/cloudflare-hosted-packages-resume.cjs @@ -0,0 +1,17 @@ +// Finish only the interrupted race/final-read checks; verify earlier receipts without republishing 8 MiB. +const fs=require('node:fs'),path=require('node:path'),assert=require('node:assert/strict'); +const {generateKeyPairSync,sign,randomUUID,createHash}=require('node:crypto'); +const [source,destination]=process.argv.slice(2),raw=fs.readFileSync(source),prior=JSON.parse(raw),origin=process.env.HOSTED_REGISTRY; +assert.equal(origin,prior.origin);assert.match(origin||'',/^https:\/\/[a-z0-9.-]+\.workers\.dev$/); +const hash=b=>createHash('sha256').update(b).digest('hex'); +(async()=>{ + assert.equal(prior.requests.length,11);for(const r of prior.requests)assert.equal(r.status,r.expected??200); + const listing=prior.requests[0].result;assert.equal(prior.requests[0].status,201); + for(const r of prior.requests.slice(1,4)){assert.equal(r.bytes,8388608);assert.equal(r.sha256,listing.integrity);} + assert.match(prior.requests[4].result.error,/exceeds 8 MiB/);assert.match(prior.requests[5].result.error,/Official/);assert.match(prior.requests[6].result.error,/signature/);assert.match(prior.requests[7].result.error,/Invalid extension package/);assert.match(prior.requests[8].result.error,/another publisher/);assert.match(prior.requests[9].result.error,/immutable/);assert.equal(prior.requests[10].status,403); + const id='race-resume-'+Date.now().toString(36),bytes=Buffer.from(JSON.stringify({format:'codegraph-extension-1',package:{name:'@preview/'+id,version:'1.0.0',main:'index.cjs',codegraph:{id,apiVersion:1,capabilities:['frameworks']}},files:{'index.cjs':'module.exports=()=>({frameworks:[]});'}})); + const requests=await Promise.all([1,2].map(async()=>{const key=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),payload=JSON.stringify({name:'Hosted race fixture',description:'Disposable concurrency acceptance',publisher:'Preview validation',readme:'Test fixture. Not a recommended extension.',source:'https://github.com/colbymchenry/codegraph',artifact:bytes.toString('base64'),timestamp:Date.now(),nonce:randomUUID()}),body={payload,publicKey:key.publicKey.export({format:'jwk'}),signature:sign('sha256',Buffer.from(payload),{key:key.privateKey,dsaEncoding:'ieee-p1363'}).toString('base64')},start=new Date().toISOString(),r=await fetch(origin+'/api/publish',{method:'POST',body:JSON.stringify(body)});return{start,status:r.status,result:await r.json()};}));assert.deepEqual(requests.map(r=>r.status).sort(),[201,400]); + const race=await(await fetch(origin+'/api/extensions/'+id)).json();assert.equal(race.length,1);assert.equal(race[0].publisherId,requests.find(r=>r.status===201).result.publisherId);assert.equal(hash(Buffer.from(await(await fetch(origin+'/api/download/'+id+'/1.0.0')).arrayBuffer())),hash(bytes)); + const versions=await(await fetch(origin+'/api/extensions/'+listing.id)).json();assert.deepEqual(versions,[listing]);const r=await fetch(origin+'/api/download/'+listing.id+'/1.0.0');assert.equal(r.status,200);const downloaded=Buffer.from(await r.arrayBuffer());assert.equal(downloaded.length,8388608);assert.equal(hash(downloaded),listing.integrity); + const result={origin,source,sourceSha256:hash(raw),reusedRequestCount:prior.requests.length,requests,large:{id:listing.id,bytes:downloaded.length,sha256:listing.integrity},checks:['all 11 interrupted-attempt response receipts verified','actual concurrent provider ownership/version claim has exactly one winner','all rejected requests retained original catalog and 8 MiB bytes'],success:true};fs.mkdirSync(path.dirname(destination),{recursive:true});fs.writeFileSync(destination,JSON.stringify(result,null,2));console.log(JSON.stringify(result)); +})().catch(e=>{console.error(e);process.exitCode=1;});