mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 22:02:49 +08:00
feat: inspect immutable extension source and gate hosted publication [skip ci]
This commit is contained in:
@@ -12,6 +12,10 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### New Features
|
||||
|
||||
- Inspect the exact extension package and every bundled source file before installing, with verified fingerprints and clear repository provenance.
|
||||
|
||||
- Add a trusted, pinned Drupal import command for the Cloudflare registry, with target/plan verification, atomic owner claims, official backup preservation and local Worker/browser graph acceptance.
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# Marketplace budget and source policy
|
||||
|
||||
The September 23 target is **$5/month for CodeGraph**, with 500–1,000 package downloads/day and few uploads. Keep the existing Workers Paid + D1 + private R2 design for this preview. No new subscription, migration, paid logging, alert, scheduler or notification has been enabled. The $5 is a planning target, not a hard cap or a promise about the shared account invoice.
|
||||
|
||||
## Cost fit
|
||||
|
||||
Thirty-day model: 15,000 or 30,000 installs/downloads; one separate source inspection per download; six catalog/detail/compatibility API calls and four HTML/JS/CSS calls conservatively charged as Worker invocations per download; 30 uploads at the full 8 MiB maximum. No cache credit. Source-file paging and downloading an already inspected Blob are client-only. Reopening inspection can fetch again. A companion's frequent local status polling is local; repeated catalog refreshes must be counted if usage differs. This is deliberately a read-heavy allowance, not a measured traffic distribution.
|
||||
|
||||
| Monthly measure | 500 downloads/day | 1,000 downloads/day |
|
||||
|---|---:|---:|
|
||||
| Package + source fetches | 30,000 | 60,000 |
|
||||
| Worker requests, including assets | 180,030 | 360,030 |
|
||||
| Projected CPU-ms | 1,721,370 | 3,431,370 |
|
||||
| Included request budget used | 1.80% | 3.60% |
|
||||
| Included CPU budget used | 5.74% | 11.44% |
|
||||
| R2 Class B reads, including upload verification allowance | 30,030 | 60,030 |
|
||||
| New R2 Class A writes | 30 | 30 |
|
||||
| Max package transfer, decimal GB | 251.66 | 503.32 |
|
||||
|
||||
CPU inputs: 26 ms per package fetch (largest of three previous deployed 8 MiB samples); 9 ms per metadata read (largest captured); 379 ms per upload (one previous sample). Assets use an explicit 2 ms upper-planning allowance, not a new measurement. The deployed config runs the Worker first only for `/api/*`; successful direct assets are free, so counting all four overstates this part. `node scripts/validation/marketplace-budget.cjs` saves the arithmetic, assumptions and D1 bounds. Actual workload, catalogs and concurrency can differ. No large upload was repeated on the provider for this estimate.
|
||||
|
||||
[Workers pricing](https://developers.cloudflare.com/workers/platform/pricing/) currently includes 10 million requests and 30 million CPU-ms in the existing $5 base; excess usage is $0.30/million requests and $0.02/million CPU-ms. With sufficient shared allowances remaining, this marketplace's projected request/CPU usage adds **$0 overage** and fits the $5 base. If those allowances were already exhausted by other applications, this modeled Worker workload alone adds approximately $0.0884 / $0.1766, before storage and other services. These are projections, not invoice quotes.
|
||||
|
||||
[R2 Standard](https://developers.cloudflare.com/r2/pricing/) includes 10 GB-month, 1 million Class A and 10 million Class B operations; egress is free. Main + restore currently contain 17,484,420 package bytes. Thirty additional maximum-size packages add 251,658,240 bytes: total 269,142,660 bytes before new backups. Beyond included usage, rates are $0.015/GB-month, $4.50/million A and $0.36/million B; billing units round up. Crossing an exhausted operation allowance can therefore add a whole billing unit even with few uploads. Do not quote fractional R2 cents as an exact invoice. This month's growth is not an indefinite storage cap.
|
||||
|
||||
[D1 pricing](https://developers.cloudflare.com/d1/platform/pricing/) includes 25 billion read rows, 50 million write rows and 5 GB across the account on Paid. The model bounds metadata reads against both the current 17 releases and the existing 1,000-release cap. The preview databases were 94,208 bytes each at the accepted readback. **Unrelated telemetry already occupied roughly 10 GB at that readback**, so an account-total $5 promise would be unsound even if marketplace queries fit. We did not inspect or change that database or fetch an invoice. Account usage, rounding, taxes, other applications, backups and logging remain separate. Additional retention is not silently free.
|
||||
|
||||
## Controls prepared, not account changes
|
||||
|
||||
- Keep `PUBLISHING_ENABLED=false`. Any separately authorized acceptance uses the existing absolute `PUBLISHING_UNTIL` deadline and an explicit close. Future publication also requires the reviewed source policy below. An allowlist is not a billing cap.
|
||||
- Retain the existing `limits.cpu_ms=1000`, 8 MiB package / 12 MiB signed envelope, 256-file bound and 10 submissions/IP/minute. The CPU cap covered the measured 379 ms upload but is per invocation; request volume can still accumulate charges. IP limits do not prevent distributed abuse. No new rate-limit product is proposed.
|
||||
- Proposed owner-only alert: **Manage Account → Billing → Billable Usage → Create budget alert**, name `CodeGraph shared-account overage warning`, threshold **$1 usage-based spend**, recipient **Colby's approved account email only**, description `Warn about variable charges beyond recurring subscriptions; includes unrelated account workloads`. This is an initial warning at the first additional dollar, not a $5 invoice cap. The recurring Workers fee is separate. [Budget alerts](https://developers.cloudflare.com/billing/manage/budget-alerts/) are informational, account-wide and reset each billing cycle. No notification was created or sent; Klaus/Colby must approve account settings and recipient before applying.
|
||||
- Before general publication, inspect existing account Billable Usage and any alerts; avoid duplicate alerts. Review marketplace request/CPU and D1/R2 usage manually at 50% and 80% of shared included allowances (5M/8M Worker requests, 15M/24M CPU-ms, 5/8 GB-month R2, 0.5M/0.8M A, 5M/8M B). These are review thresholds, not newly installed automated notifications. If $5 must be inviolable, this pay-as-you-go architecture cannot certify it.
|
||||
- The preview runs the Worker first for `/api/*`; directly served assets can be free. The model conservatively counts all four assets anyway. Existing observability settings remain unchanged; no paid caching/logging product is added. Usage alert latency and unknown shared usage preclude a hard cap.
|
||||
|
||||
## Exact source inspection
|
||||
|
||||
Every detail page has **View selected version source**, available before connection or installation. It downloads the selected immutable `.cgext`, checks SHA-256 and id/version, rejects unsafe paths, oversized/invalid content and excessive files, then displays the manifest and every bundled file using `textContent`. HTML/JavaScript are never evaluated for display. Large files are paged; full files download as `.txt`, and the verified original package can be downloaded without another network fetch.
|
||||
|
||||
Inspection pins id/version/digest in that page session. Connecting a destination, changing compatibility or refreshing a catalog must not silently switch a reviewed install/update. A pending/failed/stale review blocks that action until the new selection is inspected. The existing trusted installer revalidates package integrity and selected version again. Navigation reload starts a new review session; this is not a persistent approval or security certificate. Files inside the package are visible, including bundled dependencies; unrestricted v1 runtime code could still read files, use the network or dynamically load other code. Source availability does not certify safe behavior.
|
||||
|
||||
Older immutable entries retain their original data. Their packaged bytes can be verified, while repository correspondence is explicitly **unverified under the current source policy**. Historical official provenance is shown as historical. No old package/listing is rewritten and no live fixture is deleted.
|
||||
|
||||
## Future hosted publication contract
|
||||
|
||||
The Cloudflare production Worker compiles `marketplace/source-reviews.json`, initially empty. A new signed submission must include canonical `source=https://github.com/owner/repo`, a full `sourceRevision` commit SHA and safe `sourcePath` to a **committed `.cgext` with identical bytes**. Approval also binds the exact publisher SPKI identity, extension id, semantic version and package SHA. Client-supplied `sourceReview`, official status or provenance is rejected. The server adds its trusted proof only after successful matching. R2/ownership/version/signature rules are unchanged. New official policy entries require the same proof; only the two exact historical Drupal hashes are grandfathered without relabeling them.
|
||||
|
||||
A public URL alone, branch/tag, private/unavailable repository, another version, unrelated artifact, changed digest or different publisher is insufficient. The narrow first contract deliberately requires the packaged runtime code itself to be committed, so bundled/transpiled dependencies can be compared byte for byte. It does **not** prove a build from a separate original TypeScript repository, establish code authorship, license compliance, benign intent or continuing repository availability. Public checks are made at the recorded review time; a repository can later disappear. The immutable registry package remains inspectable. Refresh the public check just before authorizing publication, and keep the reviewed code/release decision separate from the byte-match result.
|
||||
|
||||
Prepare a review (read-only; no login or build):
|
||||
|
||||
```sh
|
||||
node marketplace/cloudflare/review-source.mjs \
|
||||
./extension.cgext https://github.com/owner/repo FULL_40_CHARACTER_COMMIT \
|
||||
packages/extension.cgext PUBLISHER_SPKI_SHA256 ./source-review.json
|
||||
```
|
||||
|
||||
The tool uses unauthenticated requests only to fixed GitHub API/raw hosts, rejects redirects, bounds each response and applies timeouts. It checks public repository identity, exact commit and full artifact equality. It does not run package scripts, install dependencies, execute code or fetch arbitrary publisher URLs. GitHub outage/rate limits fail review rather than falling back to a URL claim. Review the generated record and add it to the checked-in policy in a normal code review. The operator must check provenance and code independently, then build/review a deployment before opening publication. The command itself cannot grant publication or official status. An operator can change trusted policy code, so this is an explicit trusted admission process, not self-attestation.
|
||||
|
||||
This gate targets the hosted Cloudflare registry. The portable Node backend remains a local authoring/legacy acceptance service and does not enforce this new policy; **do not expose its publisher endpoint as a substitute production registry**. Its migration is a separate prerequisite if chosen for hosting. Existing fixture tests explicitly simulate operator source approvals inside the isolated test Worker; production ignores those bindings. Source-policy tests separately exercise explicit positive/negative records. No public admin/fetch endpoint was introduced.
|
||||
|
||||
## $0 fallback and remaining decisions
|
||||
|
||||
If the $5 target becomes a strict zero-cost requirement, a separately approved alternative is a reviewed static catalog and `.cgext` files on [Cloudflare static assets](https://developers.cloudflare.com/workers/static-assets/billing-and-limitations/) or [GitHub release assets](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases). Direct static requests have no Worker CPU; source-backed review occurs before catalog generation. That trades immediate self-service publication for a review/build workflow and must verify static per-file limits and aggregate deployment limits. It is a documented fallback, not a parallel migration or authorization to create a release.
|
||||
|
||||
Keep this existing Paid preview for the modeled traffic if the shared-account usage review supports it. General publication remains closed. Peak memory/concurrency, independent backup retention/RPO/RTO, pagination beyond 1,000 entries, fixture cleanup, custom domain and release/distribution policy remain open. No selective semantic skipping is enabled and the earlier no-go remains.
|
||||
@@ -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,publishingUntil,initialize=true}={}) {
|
||||
export async function startLocal({state,testing=false,boundary,port=0,publishing=true,publishingUntil,initialize=true,sourceReviews=[],fixtureSourceApproval=false}={}) {
|
||||
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),...(publishingUntil===undefined?{}:{PUBLISHING_UNTIL:publishingUntil})},serviceBindings:{
|
||||
bindings:{...(testing?{TEST_SOURCE_REVIEWS:JSON.stringify(sourceReviews),TEST_FIXTURE_SOURCE_APPROVAL:String(fixtureSourceApproval)}:{}),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;
|
||||
|
||||
@@ -12,9 +12,9 @@ const root=path.resolve('../..'),out=path.resolve(process.env.OFFICIAL_OUTPUT||r
|
||||
const lab=fs.mkdtempSync(path.join(os.tmpdir(),'cg-official-')),checks=[],commands=[],services=[];
|
||||
const artifact=path.join(root,'dist/extensions/drupal.cgext'),bytes=fs.readFileSync(artifact);
|
||||
const keys=generateKeyPairSync('ec',{namedCurve:'prime256v1'});
|
||||
const servicesStart=async name=>{const state=path.join(lab,name),s=await startLocal({state});services.push(s);s.state=state;s.target={mode:'local',state,registryId:await s.db.prepare("SELECT value FROM registry_meta WHERE key='id'").first('value'),engineVersion:'1.6.0'};return s;};
|
||||
const servicesStart=async name=>{const state=path.join(lab,name),s=await startLocal({state,testing:true,fixtureSourceApproval:true});services.push(s);s.state=state;s.target={mode:'local',state,registryId:await s.db.prepare("SELECT value FROM registry_meta WHERE key='id'").first('value'),engineVersion:'1.6.0'};return s;};
|
||||
function pass(name){checks.push(name);fs.writeFileSync(path.join(out,'progress.json'),JSON.stringify({checks,commands},null,2));console.log('PASS',name);}
|
||||
function signed(artifact,extra={}){const payload=JSON.stringify({name:'Community',description:'Test',publisher:'Community',readme:'Test',source:'https://example.com/source',artifact:artifact.toString('base64'),timestamp:Date.now(),nonce:randomUUID(),...extra});return {payload,publicKey:keys.publicKey.export({format:'jwk'}),signature:sign('sha256',Buffer.from(payload),{key:keys.privateKey,dsaEncoding:'ieee-p1363'}).toString('base64')};}
|
||||
function signed(artifact,extra={}){const payload=JSON.stringify({name:'Community',description:'Test',publisher:'Community',readme:'Test',source:'https://github.com/fixture/source',sourceRevision:'a'.repeat(40),sourcePath:'fixture.cgext',artifact:artifact.toString('base64'),timestamp:Date.now(),nonce:randomUUID(),...extra});return {payload,publicKey:keys.publicKey.export({format:'jwk'}),signature:sign('sha256',Buffer.from(payload),{key:keys.privateKey,dsaEncoding:'ieee-p1363'}).toString('base64')};}
|
||||
async function submit(s,body,status){const r=await fetch('http://127.0.0.1:'+s.port+'/api/publish',{method:'POST',body:JSON.stringify(body)});assert.equal(r.status,status,await r.clone().text());return r.json();}
|
||||
async function cli(args,exit=0){const r=await new Promise((resolve,reject)=>{const c=spawn(process.execPath,['official-cli.mjs',...args],{timeout:30000});let stdout='',stderr='';c.stdout.on('data',b=>stdout+=b);c.stderr.on('data',b=>stderr+=b);c.on('error',reject);c.on('close',code=>resolve({exit:code,stdout,stderr}));});const log='command-'+(commands.length+1)+'.log';fs.writeFileSync(path.join(out,log),r.stdout+r.stderr);commands.push({command:[process.execPath,'official-cli.mjs',...args],exit:r.exit,sha256:hash(r.stdout+r.stderr),log});assert.equal(r.exit,exit,r.stderr);return r.stdout;}
|
||||
try{
|
||||
|
||||
@@ -6,6 +6,8 @@ import {createRequire} from 'node:module';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {parsePackage}=require('../../dist/plugins/package');
|
||||
const semver=require('semver');
|
||||
const {requireSourceReview}=require('../source-review.cjs');
|
||||
const sourcePolicy=require('../source-reviews.json');
|
||||
const policy=JSON.parse(fs.readFileSync(new URL('./official-policy.json',import.meta.url),'utf8'));
|
||||
export const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
|
||||
export function validateTarget(target) {
|
||||
@@ -21,11 +23,15 @@ function approved(bytes,engineVersion) {
|
||||
if(hash(bytes)!==release.sha256||bytes.length!==release.bytes||pkg.name!==release.packageName)throw Error('Official artifact integrity/provenance does not match reviewed policy');
|
||||
const entry=JSON.parse(bytes).files[pkg.main];
|
||||
if(hash(entry)!==release.entrySha256)throw Error('Official entry point provenance mismatch');
|
||||
return {pkg,release};
|
||||
// Exact historical artifacts remain immutable. New official versions require the same public-byte proof.
|
||||
const legacy={ '0.1.0':'e5015747d02fe50d6af607b6c44f1db9cf7969a0070866ed03c7643377a459c8', '0.1.1':'e5ed73bee51524585e4f4da1d5017fdee3037a3fa71d8f08edab6c318fedc59a' };
|
||||
let sourceReview;
|
||||
if(pkg.codegraph.id!=='drupal'||legacy[pkg.version]!==hash(bytes))sourceReview=requireSourceReview(sourcePolicy.reviews,pkg,hash(bytes),policy.publisherId,{source:release.repository,sourceRevision:release.sourceRevision,sourcePath:release.packagePath});
|
||||
return {pkg,release,sourceReview};
|
||||
}
|
||||
function listingFor(bytes,engineVersion,publishedAt) {
|
||||
const {pkg,release:r}=approved(bytes,engineVersion);
|
||||
return {id:r.id,version:r.version,name:r.name,description:r.description,publisher:policy.publisher,publisherId:policy.publisherId,official:true,
|
||||
const {pkg,release:r,sourceReview}=approved(bytes,engineVersion);
|
||||
return {...(sourceReview?{sourceReview}:{}),id:r.id,version:r.version,name:r.name,description:r.description,publisher:policy.publisher,publisherId:policy.publisherId,official:true,
|
||||
readme:r.readme,source:r.repository+'/tree/'+r.sourceRevision+'/extensions/drupal',apiVersion:pkg.codegraph.apiVersion,engines:pkg.codegraph.engines??'*',capabilities:pkg.codegraph.capabilities,
|
||||
integrity:r.sha256,publishedAt,provenance:{repository:r.repository,revision:r.sourceRevision,path:r.sourcePath,sourceSha256:r.sourceSha256,entrySha256:r.entrySha256}};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Read-only public provenance review. Never authenticates, follows redirects or executes files.
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {createHash} from 'node:crypto';
|
||||
import {createRequire} from 'node:module';
|
||||
import {pathToFileURL} from 'node:url';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {parsePackage}=require('../../dist/plugins/package');
|
||||
const {repository:validRepository,sourcePath}=require('../source-review.cjs');
|
||||
const hash=b=>createHash('sha256').update(b).digest('hex');
|
||||
async function bounded(url,limit,request) {
|
||||
const response=await request(url,{redirect:'error',credentials:'omit',headers:{'Accept':'application/vnd.github+json','User-Agent':'CodeGraph-source-review'},signal:AbortSignal.timeout(20000)});
|
||||
if(!response.ok)throw Error(`Public source is unavailable (${response.status}); no review issued`);
|
||||
if(Number(response.headers.get('content-length'))>limit)throw Error('Public source exceeds size bound');
|
||||
const reader=response.body.getReader(),chunks=[];let size=0;
|
||||
try{while(true){const {done,value}=await reader.read();if(done)break;size+=value.length;if(size>limit)throw Error('Public source exceeds size bound');chunks.push(Buffer.from(value));}}finally{await reader.cancel();}
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
export async function reviewSource({artifact,repository,revision,source,publisherId},request=fetch) {
|
||||
if(!validRepository(repository)||!/^[a-f0-9]{40}$/.test(revision||'')||!sourcePath(source)||!(/^[a-f0-9]{64}$/.test(publisherId||'')||publisherId==='codegraph'))throw Error('Expected canonical public GitHub repository, full commit SHA, safe .cgext path and exact publisher identity');
|
||||
if(fs.statSync(artifact).size>8*1024*1024)throw Error('Package exceeds 8 MiB');
|
||||
const bytes=fs.readFileSync(artifact),pkg=parsePackage(bytes).package;
|
||||
const repo=repository.slice('https://github.com/'.length);
|
||||
const metadata=JSON.parse(await bounded('https://api.github.com/repos/'+repo,512*1024,request));
|
||||
if(metadata.private!==false||metadata.full_name!==repo||metadata.html_url!==repository)throw Error('Repository is private, redirected or does not match the requested public identity');
|
||||
const commit=JSON.parse(await bounded('https://api.github.com/repos/'+repo+'/git/commits/'+revision,2*1024*1024,request));
|
||||
if(commit.sha!==revision)throw Error('Source commit does not match immutable revision');
|
||||
const remote=await bounded('https://raw.githubusercontent.com/'+repo+'/'+revision+'/'+source,8*1024*1024,request);
|
||||
if(!remote.equals(bytes))throw Error('Public committed package does not match artifact bytes');
|
||||
return {format:'codegraph-source-review-1',id:pkg.codegraph.id,version:pkg.version,integrity:hash(bytes),publisherId,repository,revision,path:source,checkedAt:new Date().toISOString()};
|
||||
}
|
||||
if(process.argv[1]&&import.meta.url===pathToFileURL(path.resolve(process.argv[1])).href) {
|
||||
const [artifact,repository,revision,source,publisherId,out]=process.argv.slice(2);
|
||||
if(!out)throw Error('Usage: node review-source.mjs artifact.cgext https://github.com/owner/repo FULL_COMMIT path/package.cgext PUBLISHER_ID output.json');
|
||||
const result=await reviewSource({artifact,repository,revision,source,publisherId});
|
||||
fs.writeFileSync(out,JSON.stringify(result,null,2)+'\n',{flag:'wx'});
|
||||
console.log('Exact public package bytes verified. Review this record before adding it to marketplace/source-reviews.json. No publication or build was executed.');
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import fs from 'node:fs';import os from 'node:os';import path from 'node:path';import assert from 'node:assert/strict';import {createHash,generateKeyPairSync,sign,randomUUID} from 'node:crypto';import {createRequire} from 'node:module';
|
||||
import {startLocal} from './local.mjs';import {reviewSource} from './review-source.mjs';
|
||||
const require=createRequire(import.meta.url),{requireSourceReview}=require('../source-review.cjs');
|
||||
const out=path.resolve(process.env.SOURCE_OUTPUT||'../../.qa/source-budget/policy');fs.mkdirSync(out,{recursive:true});
|
||||
const lab=fs.mkdtempSync(path.join(os.tmpdir(),'cg-source-policy-')),checks=[],key=generateKeyPairSync('ec',{namedCurve:'prime256v1'}),hash=b=>createHash('sha256').update(b).digest('hex');
|
||||
const publisherId=hash(key.publicKey.export({format:'der',type:'spki'})),repository='https://github.com/review/example',revision='a'.repeat(40),source='packages/demo.cgext';
|
||||
const pkg={format:'codegraph-extension-1',package:{name:'source-demo',version:'1.0.0',main:'index.cjs',codegraph:{id:'source-demo',apiVersion:1,capabilities:['frameworks']}},files:{'index.cjs':'module.exports=()=>({frameworks:[]});','vendor/dependency.cjs':'module.exports = 42;'}};
|
||||
const bytes=Buffer.from(JSON.stringify(pkg)),artifact=path.join(lab,'demo.cgext');fs.writeFileSync(artifact,bytes);
|
||||
const options={artifact,repository,revision,source,publisherId};
|
||||
function mock({privateRepo=false,status=200,wrongCommit=false,wrongBytes=false}={}){let n=0;return async(url,opts)=>{assert.equal(opts.redirect,'error');assert.equal(opts.credentials,'omit');assert.ok(!opts.headers.Authorization);n++;assert.ok(url.startsWith(n===3?'https://raw.githubusercontent.com/review/example/':'https://api.github.com/repos/review/example'));return new Response(n===1?JSON.stringify({private:privateRepo,full_name:'review/example',html_url:repository}):n===2?JSON.stringify({sha:wrongCommit?'b'.repeat(40):revision}):wrongBytes?'tampered':bytes,{status:n===1?status:200});};}
|
||||
function pass(name){checks.push(name);console.log('PASS',name);fs.writeFileSync(path.join(out,'progress.json'),JSON.stringify({checks},null,2));}
|
||||
let server,normal;
|
||||
try{
|
||||
const review=await reviewSource(options,mock());assert.equal(review.integrity,hash(bytes));pass('bounded public repository commit and entire package byte review (mocked provider contract)');
|
||||
for(const overrides of [{privateRepo:true},{status:404},{status:403},{wrongCommit:true},{wrongBytes:true}])await assert.rejects(()=>reviewSource(options,mock(overrides)));pass('private unavailable rate-limited wrong-commit and unrelated package sources rejected');
|
||||
for(const override of [{repository:'https://example.com/source'},{repository:'https://github.com/review/example/redirect'},{revision:'main'},{source:'../secret.cgext'},{source:'packages/%2e%2e.cgext'}])await assert.rejects(()=>reviewSource({...options,...override},()=>{throw Error('network must not run');}));pass('source host revision and traversal validated before any fetch');
|
||||
const claim={source:repository,sourceRevision:revision,sourcePath:source};
|
||||
assert.equal(requireSourceReview([review],pkg.package,hash(bytes),publisherId,claim).verification,'public-committed-package-bytes');
|
||||
for(const [p,h,owner,c] of [[pkg.package,'0'.repeat(64),publisherId,claim],[{...pkg.package,version:'2.0.0'},hash(bytes),publisherId,claim],[pkg.package,hash(bytes),'1'.repeat(64),claim],[pkg.package,hash(bytes),publisherId,{...claim,sourceRevision:'b'.repeat(40)}]])assert.throws(()=>requireSourceReview([review],p,h,owner,c));pass('review binds exact id version artifact publisher repository and revision');
|
||||
server=await startLocal({state:path.join(lab,'reviewed'),testing:true,sourceReviews:[review]});
|
||||
const signed=(override={},signKey=key)=>{const payload=JSON.stringify({name:'Source demo',description:'Reviewed source',publisher:'Test',readme:'Test',...claim,artifact:bytes.toString('base64'),timestamp:Date.now(),nonce:randomUUID(),...override});return {payload,publicKey:signKey.publicKey.export({format:'jwk'}),signature:sign('sha256',Buffer.from(payload),{key:signKey.privateKey,dsaEncoding:'ieee-p1363'}).toString('base64')};};
|
||||
let count=0;const post=async(s,body,expected)=>{const r=await fetch('http://127.0.0.1:'+s.port+'/api/publish',{method:'POST',headers:{'CF-Connecting-IP':'192.0.2.'+(++count)},body:JSON.stringify(body)});const data=await r.json();assert.equal(r.status,expected,JSON.stringify(data));return data;};
|
||||
await post(server,signed({source:'https://example.com/placeholder'}),400);await post(server,signed({sourceRevision:'main'}),400);await post(server,signed({sourcePath:'other.cgext'}),400);await post(server,signed({sourceReview:review}),400);pass('real workerd rejects missing mismatched and spoofed source provenance');
|
||||
const bad=JSON.parse(bytes);bad.files['index.cjs']+='\n// changed';await post(server,signed({artifact:Buffer.from(JSON.stringify(bad)).toString('base64')}),400);pass('changed artifact cannot reuse reviewed source admission');
|
||||
await post(server,{...signed(),signature:'AAAA'},400);await post(server,signed({official:true}),400);pass('signature and official spoof defenses remain active');
|
||||
assert.equal((await server.db.prepare('SELECT count(*) n FROM releases').first()).n,0);assert.equal((await server.bucket.list()).objects.length,0);pass('failed source admission writes no owner release nonce or package');
|
||||
const listing=await post(server,signed(),201);assert.equal(listing.sourceReview.integrity,hash(bytes));assert.equal(listing.official,false);pass('real workerd publishes trusted source proof from operator policy only');
|
||||
await post(server,signed(),400);await post(server,signed({},generateKeyPairSync('ec',{namedCurve:'prime256v1'})),400);assert.deepEqual(Buffer.from(await(await fetch('http://127.0.0.1:'+server.port+'/api/download/source-demo/1.0.0')).arrayBuffer()),bytes);pass('immutable bytes ownership and reviewed listing persist after refused replacements');
|
||||
normal=await startLocal({state:path.join(lab,'production'),sourceReviews:[review]});await post(normal,signed(),400);pass('production Worker ignores test binding policies and defaults to empty approved list');
|
||||
fs.writeFileSync(path.join(out,'summary.json'),JSON.stringify({success:true,checks,review,scope:'Public provider responses mocked; actual local workerd+D1+R2 admission exercised'},null,2));
|
||||
}finally{await server?.close();await normal?.close();fs.rmSync(lab,{recursive:true,force:true});}
|
||||
@@ -1,3 +1,3 @@
|
||||
import {startLocal} from './local.mjs';
|
||||
const server=await startLocal({state:process.argv[2],testing:true,boundary:async req=>{process.send({phase:new URL(req.url).pathname.slice(1)});return new Promise(()=>{});}});
|
||||
const server=await startLocal({state:process.argv[2],testing:true,fixtureSourceApproval:true,boundary:async req=>{process.send({phase:new URL(req.url).pathname.slice(1)});return new Promise(()=>{});}});
|
||||
process.send({ready:true,port:server.port});
|
||||
|
||||
@@ -1,3 +1,13 @@
|
||||
// Test-only entrypoint. Production wrangler.jsonc targets worker.ts instead.
|
||||
import { createHandler } from './worker';
|
||||
export default {fetch(request:Request,env:any){return createHandler(async phase=>{if(request.headers.get('x-test-kill')===phase)await env.TEST_BOUNDARY.fetch(new Request('http://boundary/'+phase));if(request.headers.get('x-test-failure')===phase)throw Error('Injected service boundary failure');})(request,env);}};
|
||||
import {createHandler,digest} from './worker';
|
||||
export default {async fetch(request:Request,env:any){
|
||||
let reviews=JSON.parse(env.TEST_SOURCE_REVIEWS||'[]');
|
||||
// Existing storage/lifecycle fixtures use a simulated operator approval. Source-policy
|
||||
// tests leave this OFF and supply explicit positive/negative reviewed records instead.
|
||||
if(env.TEST_FIXTURE_SOURCE_APPROVAL==='true'&&new URL(request.url).pathname==='/api/publish')try{
|
||||
const raw=await request.clone().json() as any,p=JSON.parse(raw.payload),bytes=Uint8Array.from(atob(p.artifact),c=>c.charCodeAt(0)),pkg=JSON.parse(new TextDecoder().decode(bytes)).package;
|
||||
const key=await crypto.subtle.importKey('jwk',raw.publicKey,{name:'ECDSA',namedCurve:'P-256'},true,['verify']);
|
||||
reviews=[{format:'codegraph-source-review-1',id:pkg.codegraph.id,version:pkg.version,integrity:await digest(bytes),publisherId:await digest(await crypto.subtle.exportKey('spki',key)),repository:p.source,revision:p.sourceRevision,path:p.sourcePath,checkedAt:'2026-09-23T00:00:00Z'}];
|
||||
}catch{} // Authoritative request validation still rejects malformed signatures/packages.
|
||||
return createHandler(async phase=>{if(request.headers.get('x-test-kill')===phase)await env.TEST_BOUNDARY.fetch(new Request('http://boundary/'+phase));if(request.headers.get('x-test-failure')===phase)throw Error('Injected service boundary failure');},reviews)(request,env);
|
||||
}};
|
||||
|
||||
@@ -15,7 +15,7 @@ function artifact(id='worker-demo',version='1.0.0',size){
|
||||
return Buffer.from(JSON.stringify(p));
|
||||
}
|
||||
function signed(bytes,key=keys,nonce=randomUUID()){
|
||||
const payload=JSON.stringify({name:'Worker demo',description:'Worker-runtime validation',publisher:'Test',readme:'Test',source:'https://example.com/source',artifact:bytes.toString('base64'),timestamp:Date.now(),nonce});
|
||||
const payload=JSON.stringify({name:'Worker demo',description:'Worker-runtime validation',publisher:'Test',readme:'Test',source:'https://github.com/fixture/source',sourceRevision:'a'.repeat(40),sourcePath:'fixture.cgext',artifact:bytes.toString('base64'),timestamp:Date.now(),nonce});
|
||||
return {payload,publicKey:key.publicKey.export({format:'jwk'}),signature:sign('sha256',Buffer.from(payload),{key:key.privateKey,dsaEncoding:'ieee-p1363'}).toString('base64')};
|
||||
}
|
||||
let sequence=0;
|
||||
@@ -24,7 +24,7 @@ async function request(server,body,extra={},expected=201){
|
||||
const result=await response.json();assert.equal(response.status,expected,JSON.stringify(result));return{result,ms:performance.now()-start};
|
||||
}
|
||||
const list=async s=>(await fetch('http://127.0.0.1:'+s.port+'/api/extensions')).json();
|
||||
const start=async state=>{const s=await startLocal({state,testing:true});services.push(s);return s;};
|
||||
const start=async state=>{const s=await startLocal({state,testing:true,fixtureSourceApproval:true});services.push(s);return s;};
|
||||
function passed(name){checks.push(name);fs.writeFileSync(path.join(out,'progress.json'),JSON.stringify({checks,receipts,updated:new Date().toISOString()},null,2));console.log('PASS',name);}
|
||||
async function child(state){
|
||||
const c=fork(new URL('./test-child.mjs',import.meta.url),[state],{stdio:['ignore','pipe','pipe','ipc']});let stdout='',stderr='';c.stdout.on('data',b=>stdout+=b);c.stderr.on('data',b=>stderr+=b);
|
||||
@@ -65,7 +65,7 @@ async function child(state){
|
||||
}
|
||||
passed('real test-owned process kills after R2 and D1 boundaries recover in fresh runtime');
|
||||
// Production entry point ignores test-only fault headers.
|
||||
const production=await startLocal({state:path.join(lab,'production')});services.push(production);await request(production,signed(artifact('prod')),{'x-test-failure':'before-object'});passed('production bundle has no test failure control');const disabled=await startLocal({state:path.join(lab,'disabled'),publishing:false});services.push(disabled);await request(disabled,signed(artifact('disabled')),{},503);assert.deepEqual(await list(disabled),[]);passed('publication stays disabled until explicitly enabled');
|
||||
const production=await startLocal({state:path.join(lab,'production')});services.push(production);const prod=await request(production,signed(artifact('prod')),{'x-test-failure':'before-object'},400);assert.match(prod.result.error,/operator-reviewed/);passed('production ignores test fault headers and refuses unreviewed publication');const disabled=await startLocal({state:path.join(lab,'disabled'),publishing:false});services.push(disabled);await request(disabled,signed(artifact('disabled')),{},503);assert.deepEqual(await list(disabled),[]);passed('publication stays disabled until explicitly enabled');
|
||||
fs.writeFileSync(path.join(out,'result.json'),JSON.stringify({revision:execFileSync('git',['rev-parse','HEAD'],{cwd:root,encoding:'utf8'}).trim(),dirty:!!execFileSync('git',['status','--porcelain'],{cwd:root,encoding:'utf8'}).trim(),platform:process.platform,node:process.version,checks,receipts,snapshot,largePackage:{bytes:large.length,wallMilliseconds:timing.ms,note:'Local wall time includes storage I/O; not Cloudflare CPU billing measurement'},success:true},null,2));
|
||||
console.log('PASS',checks.length,'Worker-runtime checks');
|
||||
}finally{await Promise.allSettled(services.map(s=>s.close()));fs.rmSync(lab,{recursive:true,force:true});}})().catch(error=>{fs.writeFileSync(path.join(out,'failure.json'),JSON.stringify({checks,receipts,error:String(error.stack)},null,2));console.error(error);process.exitCode=1;});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import * as semver from 'semver';
|
||||
import sourcePolicy from '../source-reviews.json';
|
||||
import { requireSourceReview } from '../source-review.cjs';
|
||||
import { parsePackage, MAX_PACKAGE_BYTES } from '../../src/plugins/package-validation';
|
||||
|
||||
// Worker's bindings are injected by Cloudflare/Miniflare; no Node/native SQLite imports.
|
||||
@@ -30,25 +32,26 @@ async function readBody(request:Request) {
|
||||
const bytes=new Uint8Array(size);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.length;}
|
||||
return JSON.parse(new TextDecoder().decode(bytes));
|
||||
}
|
||||
export async function validateSubmission(raw:any) {
|
||||
export async function validateSubmission(raw:any, reviews:any[] = sourcePolicy.reviews) {
|
||||
try {
|
||||
if(typeof raw?.payload!=='string'||raw.payload.length>MAX_PACKAGE_BYTES*1.5||typeof raw.signature!=='string'||raw.publicKey?.kty!=='EC'||raw.publicKey?.crv!=='P-256'||raw.publicKey.d)throw Error('Invalid signed submission');
|
||||
const key=await crypto.subtle.importKey('jwk',raw.publicKey,{name:'ECDSA',namedCurve:'P-256'},true,['verify']);
|
||||
if(!await crypto.subtle.verify({name:'ECDSA',hash:'SHA-256'},key,decodeBase64(raw.signature),utf8.encode(raw.payload)))throw Error('Publisher signature is invalid');
|
||||
const p=JSON.parse(raw.payload);
|
||||
if(p.official===true||p.publisherId!==undefined||p.provenance!==undefined)throw Error('Official status and publisher identity require the trusted operator path');
|
||||
if(p.official===true||p.publisherId!==undefined||p.provenance!==undefined||p.sourceReview!==undefined)throw Error('Official status and publisher identity require the trusted operator path');
|
||||
if(!Number.isFinite(p.timestamp)||Math.abs(Date.now()-p.timestamp)>600000||typeof p.nonce!=='string'||!p.nonce||p.nonce.length>200)throw Error('Submission expired or invalid nonce');
|
||||
for(const field of ['name','description','publisher','readme','source','artifact'])if(typeof p[field]!=='string')throw Error('Missing '+field);
|
||||
if(!p.name.trim()||p.name.length>80||!p.publisher.trim()||p.publisher.length>80||p.description.length>240||p.readme.length>30000)throw Error('Listing fields exceed allowed length');
|
||||
const source=new URL(p.source);if(source.protocol!=='https:'||source.username||source.password)throw Error('Source repository must be an HTTPS URL');
|
||||
const bytes=decodeBase64(p.artifact),pkg=parsePackage(bytes).package;
|
||||
const integrity=await digest(bytes),publisherId=await digest(await crypto.subtle.exportKey('spki',key));
|
||||
const listing={id:pkg.codegraph.id,version:pkg.version,name:p.name,description:p.description,publisher:p.publisher,publisherId,official:false,readme:p.readme,source:p.source,apiVersion:pkg.codegraph.apiVersion,engines:pkg.codegraph.engines??'*',capabilities:pkg.codegraph.capabilities,integrity,publishedAt:new Date().toISOString()};
|
||||
const sourceReview=requireSourceReview(reviews,pkg,integrity,publisherId,p);
|
||||
const listing={sourceReview,id:pkg.codegraph.id,version:pkg.version,name:p.name,description:p.description,publisher:p.publisher,publisherId,official:false,readme:p.readme,source:p.source,apiVersion:pkg.codegraph.apiVersion,engines:pkg.codegraph.engines??'*',capabilities:pkg.codegraph.capabilities,integrity,publishedAt:new Date().toISOString()};
|
||||
return {bytes,listing,nonce:p.nonce};
|
||||
}catch(error){throw new InputError(error instanceof Error?error.message:String(error));}
|
||||
}
|
||||
// Hooks are injected by the isolated test entrypoint, never controlled by request/env in production.
|
||||
export function createHandler(hook: (phase:string)=>Promise<void> = async()=>{}) {
|
||||
export function createHandler(hook: (phase:string)=>Promise<void> = async()=>{}, reviews:any[] = sourcePolicy.reviews) {
|
||||
return async function handle(request:Request,env:any):Promise<Response> {
|
||||
try {
|
||||
const url=new URL(request.url);
|
||||
@@ -90,7 +93,7 @@ export function createHandler(hook: (phase:string)=>Promise<void> = async()=>{})
|
||||
const rateKey=await digest(utf8.encode(ip+':'+minute));
|
||||
const rate=await env.DB.batch([env.DB.prepare('DELETE FROM rate_limits WHERE expires<?').bind(Date.now()),env.DB.prepare('INSERT INTO rate_limits VALUES(?,?,1) ON CONFLICT(key) DO UPDATE SET count=count+1 RETURNING count').bind(rateKey,(minute+2)*60000)]);
|
||||
if(rate[1].results[0].count>10)return json(429,{error:'Too many submissions; try again in a minute'});
|
||||
const {bytes,listing,nonce}=await validateSubmission(await readBody(request));
|
||||
const {bytes,listing,nonce}=await validateSubmission(await readBody(request),reviews);
|
||||
const owner=await env.DB.prepare('SELECT publisher FROM extensions WHERE id=?').bind(listing.id).first('publisher');
|
||||
if(owner&&owner!==listing.publisherId)throw new InputError('This extension id belongs to another publisher');
|
||||
if(await env.DB.prepare('SELECT 1 FROM releases WHERE id=? AND version=?').bind(listing.id,listing.version).first())throw new InputError('This release version is immutable and already exists');
|
||||
|
||||
@@ -90,21 +90,23 @@ function renderCards() {
|
||||
`${e.name} ${e.description} ${e.publisher}`.toLowerCase().includes(state.search.toLowerCase()));
|
||||
document.querySelector('#catalog-items').innerHTML = `<div class="catalog">${filtered.length ? filtered.map(card).join('') : `<div class="empty">${state.tab === 'installed' && !state.snapshot ? 'Connect CodeGraph to see your installed extensions.' : state.search ? 'No extensions match your search.' : 'No extensions installed yet.'}</div>`}${state.tab !== 'installed' ? '<aside class="author-card"><span class="eyebrow">FOR BUILDERS</span><div class="author-art" aria-hidden="true">{↗}</div><h2>Your framework.<br>Your contribution.</h2><p>Turn what you know into an extension everyone can use. Start with the public API and publish your first release.</p><a data-link href="/publish">Build an extension <span>↗</span></a></aside>' : ''}</div>`;
|
||||
}
|
||||
function safeRepositoryLink(value) { try { const u=new URL(value); if(u.protocol==='https:'&&!u.username&&!u.password)return `<a href="${escape(u.href)}" target="_blank" rel="noopener noreferrer">View repository claim ↗</a>`; } catch {} return 'Unavailable'; }
|
||||
function renderDetail(id) {
|
||||
const item = state.catalog.find(item => item.id === id);
|
||||
const e = item && selected(item);
|
||||
if (!e) { app.innerHTML = '<section class="section detail"><h1>Extension not found</h1><a data-link href="/">Back to marketplace</a></section>'; return; }
|
||||
const current = installed(id);
|
||||
const known = releaseKnown(id);
|
||||
app.innerHTML = `<section class="section detail"><div class="breadcrumb"><a data-link href="/">Marketplace</a> / ${escape(e.name)}</div><div class="detail-layout"><article><div class="card-top"><div class="extension-icon">${id === 'drupal' ? icon : escape(e.name.slice(0,1))}</div><div><h2>${escape(e.name)}</h2><div class="byline">${escape(e.publisher)} ${e.official ? '<span class="official">✓ OFFICIAL</span>' : 'Community publisher'}</div></div></div><p class="card-copy">${escape(e.description)}</p><div class="tags">${e.capabilities.map(c => `<span class="tag">${c === 'frameworks' ? 'Framework support' : 'Semantic analysis'}</span>`).join('')}</div><div class="readme">${escape(e.readme)}</div></article><aside class="metadata"><span class="eyebrow">${current ? (current.enabled === false ? 'INSTALLED · DISABLED' : 'INSTALLED') : 'READY FOR YOUR PROJECT'}</span>${button(e,true)}<p class="small muted compatibility">${escape(compatibility(e))}</p>${progress()}<p class="small muted">${project() ? `Destination: ${escape(project().name)}` : 'Connect CodeGraph once to install.'}</p><p class="small muted">Extensions run with your local permissions. Review the publisher and source before installing.</p><dl>${current ? `<dt>Installed version</dt><dd data-installed-version>${escape(current.version)}</dd>` : ''}<dt>${state.snapshot ? 'Selected version' : 'Latest catalog version'}</dt><dd data-selected-version>${escape(releaseVersion(e))}</dd>${known ? `<dt>CodeGraph compatibility</dt><dd>${escape(e.engines)}</dd><dt>Source</dt><dd><a href="${escape(e.source)}" target="_blank" rel="noopener">View source repository ↗</a></dd><dt>Published</dt><dd>${escape(new Date(e.publishedAt).toLocaleDateString(undefined,{year:'numeric',month:'long',day:'numeric'}))}</dd><dt>Publisher identity</dt><dd class="integrity">${escape(e.publisherId)}</dd><dt>Release fingerprint</dt><dd class="integrity">${escape(e.integrity)}</dd>` : ''}</dl>${known ? `<a class="text-link" href="/api/download/${escape(e.id)}/${escape(e.version)}">Download for CLI install ↓</a>` : ''}</aside></div></section>`;
|
||||
app.innerHTML = `<section class="section detail"><div class="breadcrumb"><a data-link href="/">Marketplace</a> / ${escape(e.name)}</div><div class="detail-layout"><article><div class="card-top"><div class="extension-icon">${id === 'drupal' ? icon : escape(e.name.slice(0,1))}</div><div><h2>${escape(e.name)}</h2><div class="byline">${escape(e.publisher)} ${e.official ? '<span class="official">✓ OFFICIAL</span>' : 'Community publisher'}</div></div></div><p class="card-copy">${escape(e.description)}</p><div class="tags">${e.capabilities.map(c => `<span class="tag">${c === 'frameworks' ? 'Framework support' : 'Semantic analysis'}</span>`).join('')}</div><div class="readme">${escape(e.readme)}</div>${sourcePanel(e)}</article><aside class="metadata"><span class="eyebrow">${current ? (current.enabled === false ? 'INSTALLED · DISABLED' : 'INSTALLED') : 'READY FOR YOUR PROJECT'}</span>${button(e,true)}<p><a class="text-link" href="#package-source">Inspect this package’s source ↓</a></p><p class="small muted compatibility">${escape(compatibility(e))}</p>${progress()}<p class="small muted">${project() ? `Destination: ${escape(project().name)}` : 'Connect CodeGraph once to install.'}</p><p class="small muted">Extensions run with your local permissions. Review the publisher and source before installing.</p><dl>${current ? `<dt>Installed version</dt><dd data-installed-version>${escape(current.version)}</dd>` : ''}<dt>${state.snapshot ? 'Selected version' : 'Latest catalog version'}</dt><dd data-selected-version>${escape(releaseVersion(e))}</dd>${known ? `<dt>CodeGraph compatibility</dt><dd>${escape(e.engines)}</dd><dt>Publisher repository claim</dt><dd>${safeRepositoryLink(e.source)}</dd><dt>Published</dt><dd>${escape(new Date(e.publishedAt).toLocaleDateString(undefined,{year:'numeric',month:'long',day:'numeric'}))}</dd><dt>Publisher identity</dt><dd class="integrity">${escape(e.publisherId)}</dd><dt>Release fingerprint</dt><dd class="integrity">${escape(e.integrity)}</dd>` : ''}</dl>${known ? `<a class="text-link" href="/api/download/${escape(e.id)}/${escape(e.version)}">Download for CLI install ↓</a>` : ''}</aside></div></section>`;
|
||||
}
|
||||
function renderPublish() {
|
||||
app.innerHTML = `<section class="section publish"><a data-link class="breadcrumb" href="/">← Marketplace</a><h1>Share what you know.</h1><p class="publish-intro">Give CodeGraph the framework knowledge your team needs. Upload a versioned extension and publish it to the community.</p><div class="publish-layout"><form id="publish-form"><div class="upload"><label for="artifact">Your extension package</label><p class="small muted">Upload the .cgext file from CodeGraph’s extension pack command.</p><input type="file" id="artifact" accept=".cgext,application/json" required><p id="package-summary" class="small"></p></div><div class="form-row"><div><label for="name">Extension name</label><input id="name" maxlength="80" placeholder="Your framework" required></div><div><label for="publisher">Publisher name</label><input id="publisher" maxlength="80" placeholder="Your name or team" required></div></div><label for="description">Short description</label><input id="description" maxlength="240" placeholder="What does this extension help people understand?" required><label for="source">Source repository</label><input id="source" type="url" placeholder="https://github.com/your-team/your-extension" required><label for="readme">Documentation</label><textarea id="readme" rows="8" maxlength="30000" placeholder="Supported patterns, configuration, examples, and known limitations." required></textarea><div class="form-actions"><button class="primary" type="submit">Publish release <span>↗</span></button><span class="small muted">Published versions are immutable.<br>Community publication does not confer official status.</span></div><p id="publish-result" role="status"></p></form><aside class="aside-notes"><span class="eyebrow">BEFORE YOU PUBLISH</span><h3>Build. Test. Share.</h3><p>Package framework or semantic contributions, and test deterministic indexing before publishing.</p><h3>Your publisher identity</h3><p>Your browser creates a signing key for your releases. Back it up to publish updates from another browser. Your key never leaves this browser during publication.</p><button id="backup-key">Back up publisher key ↓</button><label for="restore-key">Restore an existing publisher key</label><input type="file" id="restore-key" accept=".json"><p class="small">Keep your backup private. Anyone with it can publish your extensions.</p></aside></div></section>`;
|
||||
app.innerHTML = `<section class="section publish"><a data-link class="breadcrumb" href="/">← Marketplace</a><h1>Share what you know.</h1><p class="publish-intro">Give CodeGraph the framework knowledge your team needs. Upload a versioned extension and publish it to the community.</p><div class="publish-layout"><form id="publish-form"><div class="upload"><label for="artifact">Your extension package</label><p class="small muted">Upload the .cgext file from CodeGraph’s extension pack command.</p><input type="file" id="artifact" accept=".cgext,application/json" required><p id="package-summary" class="small"></p></div><div class="form-row"><div><label for="name">Extension name</label><input id="name" maxlength="80" placeholder="Your framework" required></div><div><label for="publisher">Publisher name</label><input id="publisher" maxlength="80" placeholder="Your name or team" required></div></div><label for="description">Short description</label><input id="description" maxlength="240" placeholder="What does this extension help people understand?" required><label for="source">Source repository</label><input id="source" type="url" placeholder="https://github.com/your-team/your-extension" required><label for="source-revision">Public source commit</label><input id="source-revision" pattern="[a-f0-9]{40}" placeholder="Full 40-character commit SHA" required><label for="source-path">Committed package path</label><input id="source-path" placeholder="packages/example-1.0.0.cgext" required><p class="small">Future publication requires an operator-reviewed copy of these exact package bytes in this public commit. Placeholder, private and mismatched sources are rejected. General publication remains disabled on the hosted preview.</p><label for="readme">Documentation</label><textarea id="readme" rows="8" maxlength="30000" placeholder="Supported patterns, configuration, examples, and known limitations." required></textarea><div class="form-actions"><button class="primary" type="submit">Publish release <span>↗</span></button><span class="small muted">Published versions are immutable.<br>Community publication does not confer official status.</span></div><p id="publish-result" role="status"></p></form><aside class="aside-notes"><span class="eyebrow">BEFORE YOU PUBLISH</span><h3>Build. Test. Share.</h3><p>Package framework or semantic contributions, and test deterministic indexing before publishing.</p><h3>Your publisher identity</h3><p>Your browser creates a signing key for your releases. Back it up to publish updates from another browser. Your key never leaves this browser during publication.</p><button id="backup-key">Back up publisher key ↓</button><label for="restore-key">Restore an existing publisher key</label><input type="file" id="restore-key" accept=".json"><p class="small">Keep your backup private. Anyone with it can publish your extensions.</p></aside></div></section>`;
|
||||
}
|
||||
function render() {
|
||||
if (location.pathname === '/publish') renderPublish();
|
||||
else if (location.pathname.startsWith('/extensions/')) renderDetail(decodeURIComponent(location.pathname.split('/')[2]));
|
||||
else renderCatalog();
|
||||
paintSource();
|
||||
}
|
||||
async function refresh() {
|
||||
state.catalog = await api('/api/extensions');
|
||||
@@ -169,6 +171,8 @@ async function act(action, id) {
|
||||
if (action === 'install' || action === 'update') {
|
||||
const release = state.selections[id]?.release;
|
||||
if (!release) { toast('Wait for compatibility selection or reconnect.'); return; }
|
||||
const reviewed=inspection.reviewed.get(id);
|
||||
if(reviewed&&(reviewed.pending||reviewed.identity!==sourceIdentity(release))){toast('Source review does not match the selected release. View its source again before installing.');return;}
|
||||
command.selected = { version: release.version, integrity: release.integrity };
|
||||
if (extension.official && id === 'drupal') command.replaces = ['drupal'];
|
||||
}
|
||||
@@ -221,12 +225,80 @@ document.addEventListener('submit',async event=>{
|
||||
const jwk=await signingKey(), key=await crypto.subtle.importKey('jwk',jwk,{name:'ECDSA',namedCurve:'P-256'},false,['sign']);
|
||||
const publicKey={kty:jwk.kty,crv:jwk.crv,x:jwk.x,y:jwk.y};
|
||||
const fields=Object.fromEntries(['name','publisher','description','source','readme'].map(id=>[id,document.getElementById(id).value]));
|
||||
const payload=JSON.stringify({...fields,artifact,timestamp:Date.now(),nonce:crypto.randomUUID()});
|
||||
const payload=JSON.stringify({...fields,sourceRevision:document.getElementById('source-revision').value,sourcePath:document.getElementById('source-path').value,artifact,timestamp:Date.now(),nonce:crypto.randomUUID()});
|
||||
const signature=base64(new Uint8Array(await crypto.subtle.sign({name:'ECDSA',hash:'SHA-256'},key,new TextEncoder().encode(payload))));
|
||||
status.textContent='Validating and publishing your release…';
|
||||
const result=await api('/api/publish',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({payload,publicKey,signature})});
|
||||
state.catalog=await api('/api/extensions');toast('Release published. Back up your publisher key to keep control of updates.');navigate('/extensions/'+result.id);void refreshSelections();
|
||||
}catch(e){status.textContent=e.message;}finally{submit.disabled=false;}
|
||||
});
|
||||
// Package inspection never imports or evaluates extension code. Only verified bytes reach textContent.
|
||||
const inspection = { reviewed:new Map(), request:0, active:null, error:'', loading:false, file:'', offset:0 };
|
||||
const sourcePageSize = 64000;
|
||||
function sourceIdentity(e) { return `${e.id}\n${e.version}\n${e.integrity}`; }
|
||||
function safeSourcePath(p) {
|
||||
return typeof p==='string' && p.length>0 && p.length<240 && !/[\\:\x00-\x1f\x7f\u202a-\u202e\u2066-\u2069]/.test(p) && !p.startsWith('/') && p!=='package.json' && /\.(?:[cm]?js|json|md|txt)$/.test(p) && p.split('/').every(x=>x&&x!=='.'&&x!=='..'&&!x.startsWith('.'));
|
||||
}
|
||||
async function verifiedSource(response, release) {
|
||||
if(!response.ok)throw Error(`Package source unavailable (HTTP ${response.status}).`);
|
||||
const max=8*1024*1024;
|
||||
if(Number(response.headers.get('content-length'))>max)throw Error('Package exceeds 8 MiB.');
|
||||
const reader=response.body.getReader(),parts=[];let size=0;
|
||||
try{while(true){const {value,done}=await reader.read();if(done)break;size+=value.length;if(size>max)throw Error('Package exceeds 8 MiB.');parts.push(value);}}finally{await reader.cancel();}
|
||||
const bytes=new Uint8Array(size);let pos=0;for(const part of parts){bytes.set(part,pos);pos+=part.length;}
|
||||
const digest=[...new Uint8Array(await crypto.subtle.digest('SHA-256',bytes))].map(b=>b.toString(16).padStart(2,'0')).join('');
|
||||
if(!/^[a-f0-9]{64}$/.test(release.integrity)||digest!==release.integrity)throw Error('Package fingerprint changed. Source is not verified; installation from this review is blocked.');
|
||||
const pkg=JSON.parse(new TextDecoder('utf-8',{fatal:true,ignoreBOM:true}).decode(bytes));
|
||||
if(pkg?.format!=='codegraph-extension-1'||pkg.package?.codegraph?.id!==release.id||pkg.package?.version!==release.version||!pkg.files||Array.isArray(pkg.files)||typeof pkg.files!=='object')throw Error('Package identity or format does not match the selected release.');
|
||||
const entries=Object.entries(pkg.files);
|
||||
if(entries.length>256||entries.some(([p,s])=>!safeSourcePath(p)||typeof s!=='string'||s.includes('\0'))||!safeSourcePath(pkg.package.main)||!Object.hasOwn(pkg.files,pkg.package.main)||!/[cm]?js$/.test(pkg.package.main))throw Error('Package has an unsafe path, invalid text or missing entry point.');
|
||||
return {release:{...release},bytes,files:{'package.json':JSON.stringify(pkg.package,null,2),...pkg.files},main:pkg.package.main};
|
||||
}
|
||||
function sourcePanel(e) {
|
||||
const active=inspection.active?.release.id===e.id?inspection.active:null;
|
||||
return `<section id="package-source" class="source-inspection" aria-label="Package source"><h3>Inspect packaged source</h3><p class="small">Read every bundled file before installing. Source availability is not a security review. Extensions run with your local permissions.</p><button data-view-source="${escape(e.id)}" ${releaseKnown(e.id)&&!inspection.loading?'':'disabled'}>${inspection.loading?'Checking package…':'View selected version source'}</button><p class="small" role="status" id="source-status">${escape(inspection.error)}</p>${active?`<p class="source-verified">Verified package bytes · ${escape(active.release.id)} ${escape(active.release.version)}</p><p class="integrity small">SHA-256 ${escape(active.release.integrity)}</p><p class="small">All ${Object.keys(active.files).length} packaged files are available below, including bundled dependencies. Runtime code may still access your files or network.</p>${sourceProvenance(active.release)}<label for="source-file">File</label><select id="source-file">${Object.keys(active.files).sort().map(p=>`<option value="${escape(p)}" ${inspection.file===p?'selected':''}>${escape(p)}${p===active.main?' (entry point)':''}</option>`).join('')}</select><p class="small" id="source-range"></p><pre id="source-content" tabindex="0" aria-label="Inert package source"></pre><div class="source-actions"><button id="source-prev">Previous section</button><button id="source-next">Next section</button><button id="source-file-download">Download file as text</button><button id="source-package-download">Download verified package</button></div>`:''}</section>`;
|
||||
}
|
||||
function sourceProvenance(e) {
|
||||
const r=e.sourceReview;
|
||||
const valid=r?.verification==='public-committed-package-bytes'&&r.integrity===e.integrity&&/^https:\/\/github\.com\/[\w-]+\/[\w.-]+$/.test(r.repository)&&/^[a-f0-9]{40}$/.test(r.revision)&&typeof r.path==='string'&&r.path.split('/').every(p=>p&&p!=='.'&&p!=='..');
|
||||
if(valid)return `<p class="small">Operator checked the public committed package on ${escape(r.checkedAt)}. <a href="${escape(r.repository+'/blob/'+r.revision+'/'+r.path.split('/').map(encodeURIComponent).join('/'))}" target="_blank" rel="noopener noreferrer">Repository snapshot ↗</a><br>Commit: ${escape(r.revision)}. This proves packaged-byte correspondence at review time, not a reproducible build or malware safety.</p>`;
|
||||
const p=e.provenance;
|
||||
return `<p class="small source-unverified">Repository correspondence unverified under the current source policy.${p?.revision?` Historical publisher provenance: commit ${escape(p.revision)}, path ${escape(p.path)}.`:''} The package fingerprint above verifies the downloaded release only.</p>`;
|
||||
}
|
||||
function paintSource() {
|
||||
const el=document.querySelector('#source-content'),active=inspection.active;if(!el||!active)return;
|
||||
const source=active.files[inspection.file];if(typeof source!=='string')return;
|
||||
el.textContent=source.slice(inspection.offset,inspection.offset+sourcePageSize);
|
||||
document.querySelector('#source-range').textContent=`Characters ${inspection.offset+1}–${Math.min(source.length,inspection.offset+sourcePageSize)} of ${source.length}. Displayed as text; never executed.`;
|
||||
document.querySelector('#source-prev').disabled=inspection.offset===0;
|
||||
document.querySelector('#source-next').disabled=inspection.offset+sourcePageSize>=source.length;
|
||||
}
|
||||
async function inspectSource(id) {
|
||||
const catalog=state.catalog.find(e=>e.id===id);if(!catalog||!releaseKnown(id))return;
|
||||
const e={...selected(catalog)},identity=sourceIdentity(e),request=++inspection.request,epoch=state.selectionEpoch;
|
||||
inspection.loading=true;inspection.error='';inspection.active=null;inspection.reviewed.set(id,{identity,pending:true});render();
|
||||
try {
|
||||
const verified=await verifiedSource(await fetch(`/api/download/${encodeURIComponent(id)}/${encodeURIComponent(e.version)}`,{redirect:'error'}),e);
|
||||
if(request!==inspection.request||epoch!==state.selectionEpoch||!releaseKnown(id)||sourceIdentity(selected(state.catalog.find(x=>x.id===id)||{}))!==identity||location.pathname!==`/extensions/${id}`)throw Error('Selection changed while loading. Inspect the current selected version before installing.');
|
||||
inspection.active=verified;inspection.file=verified.main;inspection.offset=0;
|
||||
inspection.reviewed.set(id,{identity,pending:false});
|
||||
}catch(error){if(request===inspection.request)inspection.error=error.message;}
|
||||
finally{if(request===inspection.request){inspection.loading=false;render();}}
|
||||
}
|
||||
function downloadInspected(file) {
|
||||
const a=inspection.active;if(!a)return;
|
||||
const link=document.createElement('a');link.href=URL.createObjectURL(new Blob([file?a.files[inspection.file]:a.bytes],{type:'text/plain;charset=utf-8'}));
|
||||
link.download=file?inspection.file.replace(/\//g,'_')+'.txt':`${a.release.id}-${a.release.version}.cgext`;
|
||||
link.click();setTimeout(()=>URL.revokeObjectURL(link.href),1000);
|
||||
}
|
||||
document.addEventListener('click',event=>{
|
||||
const view=event.target.closest('[data-view-source]');if(view){void inspectSource(view.dataset.viewSource);return;}
|
||||
if(event.target.id==='source-prev'){inspection.offset=Math.max(0,inspection.offset-sourcePageSize);paintSource();}
|
||||
if(event.target.id==='source-next'){inspection.offset+=sourcePageSize;paintSource();}
|
||||
if(event.target.id==='source-file-download')downloadInspected(true);
|
||||
if(event.target.id==='source-package-download')downloadInspected(false);
|
||||
});
|
||||
document.addEventListener('change',event=>{if(event.target.id==='source-file'){inspection.file=event.target.value;inspection.offset=0;paintSource();}});
|
||||
|
||||
app.innerHTML='<div class="loading">Loading extensions…</div>';
|
||||
refresh().catch(e=>{if(document.querySelector('#publish-form')){toast(e.message);return;}app.innerHTML=`<section class="section detail"><h1>Marketplace unavailable</h1><p>${escape(e.message)}</p><button id="retry">Try again</button></section>`;document.querySelector('#retry').onclick=()=>location.reload();});
|
||||
|
||||
@@ -2,3 +2,5 @@
|
||||
@media(min-width:1500px){.hero{padding-top:80px;padding-bottom:70px}.hero h1{font-size:84px}}
|
||||
@media(max-width:1000px){.nav{padding:0 28px}nav{gap:18px}.nav-label,nav>a:last-of-type{display:none}.hero{padding:45px 28px;gap:30px}.hero h1{font-size:57px;letter-spacing:-3px}.section{padding-left:28px;padding-right:28px}.catalog{grid-template-columns:1.6fr 1fr}.detail-layout{gap:35px;grid-template-columns:1fr 260px}footer{padding:26px 28px}.publish-layout{gap:25px}}
|
||||
@media(max-width:700px){.nav{height:78px;padding:0 20px}.wordmark{font-size:23px}.mark{font-size:30px}nav>a{display:none}.connect-button{padding:10px;font-size:10px}.hero{grid-template-columns:1fr;padding:35px 20px;gap:25px}.hero h1{font-size:49px;letter-spacing:-2.8px;margin:23px 0 18px}.hero p{font-size:15px;line-height:1.7}.connection-panel{padding:23px}.connection-panel h3{margin-top:17px;font-size:22px}.section{padding:0 20px 35px}.toolbar{align-items:flex-start;flex-direction:column;gap:22px}.search{width:100%}.tab.active:after{bottom:-9px}.catalog{grid-template-columns:1fr}.extension-card{padding:22px}.card-top{gap:14px}.extension-icon{height:55px;width:55px;flex-basis:55px;border-radius:10px}.card-top h2{font-size:27px}.byline{gap:8px;font-size:10px}.version{font-size:10px}.graph{padding:5px}.graph-node{font-size:9px;padding:8px}.author-card{padding:23px}.author-art{margin:0 0 22px}.author-card h2{font-size:29px}.note-row{flex-wrap:wrap;gap:17px;margin-top:25px}footer{padding:25px 20px;flex-wrap:wrap;gap:17px}footer>span:nth-child(2){width:100%;order:2}footer a:nth-last-child(2){font-size:10px}.detail{padding-top:25px}.detail-layout{grid-template-columns:1fr;gap:30px}.detail .card-top h2{font-size:38px}.metadata{order:-1}.metadata .primary{margin:0}.publish{padding-top:25px}.publish h1{font-size:43px}.publish-layout{grid-template-columns:1fr}.form-row{grid-template-columns:1fr;gap:0}.aside-notes{order:-1}.aside-notes p{margin:5px 0}.form-actions{align-items:flex-start;flex-direction:column}dialog{padding:30px 22px}dialog h2{font-size:33px}.catalog-label{font-size:9px}.connection-panel .eyebrow{font-size:10px}}
|
||||
|
||||
.source-inspection{margin-top:32px;border-top:1px solid #d6d3c8;padding-top:24px;min-width:0}.source-inspection h3{font-size:22px}.source-inspection select{width:100%;max-width:100%}.source-inspection pre{background:#171c27;color:#e3e9f3;padding:20px;border-radius:8px;font:13px/1.6 ui-monospace,monospace;white-space:pre;overflow:auto;max-height:480px;max-width:100%;tab-size:2;unicode-bidi:plaintext}.source-inspection p{overflow-wrap:anywhere}.source-verified{color:#176146;font-weight:600}.source-unverified{padding:12px;background:#fff1d8;border-radius:8px}.source-actions{display:flex;flex-wrap:wrap;gap:8px}.source-actions button{font-size:12px}.detail-layout article{min-width:0}@media(max-width:700px){.source-inspection pre{padding:12px;font-size:12px}.source-actions button{flex:1 1 45%}}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
// Trusted publication policy only; never accept a policy record from a submission.
|
||||
const repository = value => typeof value === 'string' && /^https:\/\/github\.com\/[A-Za-z0-9_-]+\/[A-Za-z0-9_][A-Za-z0-9_.-]*$/.test(value) && !value.endsWith('.git');
|
||||
const sourcePath = value => typeof value === 'string' && value.length < 240 && /^[A-Za-z0-9_./-]+\.cgext$/.test(value) && value.split('/').every(p => p && p !== '.' && p !== '..' && !p.startsWith('.'));
|
||||
function requireSourceReview(reviews, pkg, integrity, publisherId, claim) {
|
||||
if (!repository(claim?.source) || !/^[a-f0-9]{40}$/.test(claim?.sourceRevision || '') || !sourcePath(claim?.sourcePath)) throw Error('Source requires a public GitHub repository, full immutable commit SHA and committed .cgext path');
|
||||
const review = reviews.find(r => r.id === pkg.codegraph.id && r.version === pkg.version && r.integrity === integrity && r.publisherId === publisherId);
|
||||
if (!review || review.format !== 'codegraph-source-review-1' || review.repository !== claim.source || review.revision !== claim.sourceRevision || review.path !== claim.sourcePath || !Number.isFinite(Date.parse(review.checkedAt))) throw Error('This exact publisher/version/package needs an operator-reviewed public source snapshot before publication');
|
||||
return {format:review.format, repository:review.repository, revision:review.revision, path:review.path, integrity:review.integrity, checkedAt:review.checkedAt, verification:'public-committed-package-bytes'};
|
||||
}
|
||||
module.exports = {repository, sourcePath, requireSourceReview};
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"format": "codegraph-source-policy-1",
|
||||
"reviews": []
|
||||
}
|
||||
@@ -2,5 +2,5 @@
|
||||
const path=require('node:path');
|
||||
exports.startMarketplaceServer=async options=>{
|
||||
const {startLocal}=await import('../../marketplace/cloudflare/local.mjs');
|
||||
return startLocal({state:path.join(path.dirname(options.database),'cloudflare-state')});
|
||||
return startLocal({testing:true,fixtureSourceApproval:true,state:path.join(path.dirname(options.database),'cloudflare-state')});
|
||||
};
|
||||
|
||||
@@ -30,7 +30,7 @@ async function publish(version, engines = '>=1.6.0 <2', id = 'compatible-demo',
|
||||
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() });
|
||||
source: 'https://github.com/fixture/source', sourceRevision:'a'.repeat(40), sourcePath:'fixture.cgext', 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') };
|
||||
const res = await fetch(registry + '/api/publish', { method: 'POST', body: JSON.stringify(body) });
|
||||
assert.equal(res.status, 201, await res.clone().text());
|
||||
|
||||
@@ -63,7 +63,7 @@ function routes(root) {
|
||||
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('#source').fill('https://github.com/fixture/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) {
|
||||
@@ -74,7 +74,8 @@ function routes(root) {
|
||||
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');
|
||||
assert.equal(await page.locator('#source').inputValue(), 'https://github.com/fixture/recovery-source');
|
||||
await page.locator('#source-revision').fill('a'.repeat(40)); await page.locator('#source-path').fill('fixture.cgext');
|
||||
await page.getByRole('button', { name: 'Publish release' }).click();
|
||||
console.log('Submitted', version);
|
||||
await page.waitForURL('**/extensions/browser-example');
|
||||
@@ -89,7 +90,8 @@ function routes(root) {
|
||||
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);
|
||||
for (const [id, value] of Object.entries({ name: 'Attempted takeover', publisher: 'Other publisher', description: 'Test', source: 'https://github.com/fixture/other', readme: 'Test' })) await other.locator('#' + id).fill(value);
|
||||
await other.locator('#source-revision').fill('a'.repeat(40)); await other.locator('#source-path').fill('fixture.cgext');
|
||||
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');
|
||||
@@ -98,6 +100,7 @@ function routes(root) {
|
||||
const body = route.request().postDataJSON(); body.signature = 'invalid';
|
||||
await route.continue({ postData: JSON.stringify(body) });
|
||||
});
|
||||
await other.locator('#source-revision').fill('a'.repeat(40)); await other.locator('#source-path').fill('fixture.cgext');
|
||||
await other.getByRole('button', { name: 'Publish release' }).click();
|
||||
await other.locator('#publish-result').filter({ hasText: /signature/i }).waitFor();
|
||||
await other.unroute('**/api/publish');
|
||||
@@ -105,7 +108,8 @@ function routes(root) {
|
||||
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.locator('#source').fill('https://github.com/fixture/invalid');
|
||||
await other.locator('#source-revision').fill('a'.repeat(40)); await other.locator('#source-path').fill('fixture.cgext');
|
||||
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')).json()).length, 1);
|
||||
@@ -188,10 +192,11 @@ function routes(root) {
|
||||
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);
|
||||
for (const [id, value] of Object.entries({ name: 'Browser example', publisher: 'Recovery publisher', description: 'Outage check', source: 'https://github.com/fixture/source', readme: 'Outage check' })) await page.locator('#'+id).fill(value);
|
||||
await page.locator('#package-summary').filter({ hasText: 'v4.0.0' }).waitFor();
|
||||
await server.close(); server = undefined;
|
||||
await page.getByRole('button', { name: 'Publish release' }).click();
|
||||
await page.locator('#source-revision').fill('a'.repeat(40)); await page.locator('#source-path').fill('fixture.cgext');
|
||||
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]), []);
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// Auditable projection, not a load test or account invoice prediction.
|
||||
const fs=require('node:fs'),path=require('node:path');
|
||||
const rows=[15000,30000].map(downloads=>{
|
||||
const sourceViews=downloads,metadata=downloads*6,assets=downloads*4,uploads=30;
|
||||
const requests=downloads+sourceViews+metadata+assets+uploads;
|
||||
// 26 ms is the largest of three hosted 8 MiB downloads, 9 ms largest captured catalog read.
|
||||
// Asset 2 ms is a stated planning allowance, not a new measurement. Upload 379 ms measured once.
|
||||
const cpuMs=(downloads+sourceViews)*26+metadata*9+assets*2+uploads*379;
|
||||
return {downloads,sourceViews,metadataRequests:metadata,assetRequests:assets,uploads,workerRequests:requests,cpuMs,workerAllowancePercent:{requests:requests/10000000*100,cpu:cpuMs/30000000*100},r2ClassB:downloads+sourceViews+uploads,r2ClassA:uploads,
|
||||
d1ReadRowsCurrentUpper:metadata*18+(downloads+sourceViews)*2+assets+uploads*1000,
|
||||
d1ReadRows1000ReleaseUpper:metadata*1001+(downloads+sourceViews)*2+assets+uploads*1000,
|
||||
maxMonthlyPackageTransferGB:(downloads+sourceViews)*8388608/1e9,
|
||||
storageAfter30MaxSizeUploadsBytes:17484420+uploads*8388608,
|
||||
workerMarginalUSDIfAllowancesAlreadyExhausted:requests/1e6*0.30+cpuMs/1e6*0.02};
|
||||
});
|
||||
const output={date:'2026-09-23',basePlanUSD:5,periodDays:30,assumptions:'One source view, six API reads and four conservatively Worker-charged asset requests per download; thirty maximum-size uploads. No cache savings. Existing restored snapshot unchanged, no new provider copies.',rows,limitations:['Account-wide allowances and unrelated workloads unknown','Taxes, paid logs, abuse, background polling, backups and future storage growth excluded','CPU samples are not throughput or peak-memory proof','R2 billable units round up; marginal fractional arithmetic is not an exact invoice'],sources:['https://developers.cloudflare.com/workers/platform/pricing/','https://developers.cloudflare.com/r2/pricing/','https://developers.cloudflare.com/d1/platform/pricing/']};
|
||||
const out=path.resolve(process.argv[2]||'.qa/source-budget/cost-model.json');fs.mkdirSync(path.dirname(out),{recursive:true});fs.writeFileSync(out,JSON.stringify(output,null,2)+'\n');console.log(JSON.stringify(rows,null,2));
|
||||
@@ -0,0 +1,44 @@
|
||||
// Real local workerd + D1/R2 -> browser source inspection -> compiled managed installer.
|
||||
const fs=require('node:fs'),os=require('node:os'),path=require('node:path'),assert=require('node:assert/strict'),crypto=require('node:crypto');
|
||||
const {chromium}=require(process.env.PLAYWRIGHT_MODULE||'playwright');
|
||||
const {startExtensionBridge}=require('../../dist/plugins/bridge');
|
||||
const {DatabaseSync}=require('node:sqlite');
|
||||
const out=path.resolve(process.env.SOURCE_BROWSER_OUTPUT||'.qa/source-budget/browser');fs.mkdirSync(out,{recursive:true});
|
||||
const lab=fs.mkdtempSync(path.join(os.tmpdir(),'cg-source-browser-')),root=path.join(lab,'project');fs.mkdirSync(root);fs.writeFileSync(path.join(root,'app.ts'),'export function run() {}');
|
||||
Object.assign(process.env,{CODEGRAPH_TELEMETRY:'0',CODEGRAPH_PARSE_WORKERS:'2'});
|
||||
const keys=crypto.generateKeyPairSync('ec',{namedCurve:'prime256v1'}),hash=b=>crypto.createHash('sha256').update(b).digest('hex'),checks=[];
|
||||
const repository='https://github.com/fixture/source',revision='a'.repeat(40),source='package.cgext';
|
||||
function artifact(version,engines='*',broken=false){return Buffer.from(JSON.stringify({format:'codegraph-extension-1',package:{name:'source-example',version,main:'index.cjs',codegraph:{id:'source-example',apiVersion:1,engines,capabilities:['frameworks']}},files:{'index.cjs':`globalThis.__packageExecuted = true; module.exports=()=>({frameworks:[{name:'source',languages:['typescript'],detect:()=>true,resolve:()=>null,extract(file){${broken?"throw Error('Rejected source update');":''}return {nodes:[{id:'plugin:source-example:route:'+file,kind:'route',name:'/inspected-${version}',qualifiedName:file+'::source',filePath:file,language:'typescript',startLine:1,endLine:1,startColumn:0,endColumn:0,updatedAt:0}],references:[]}}}]});`,'vendor/dependency.cjs':'// bundled dependency\nmodule.exports = 42;','readme.md':'<script>window.__packageExecuted=true</script><img src=x onerror="window.__packageExecuted=true">','large.txt':'source-line\n'.repeat(9000)}}));}
|
||||
const artifacts=[artifact('1.0.0'),artifact('9.0.0','>=9'),artifact('2.0.0'),artifact('3.0.0','*',true)];
|
||||
const publisherId=hash(keys.publicKey.export({format:'der',type:'spki'}));
|
||||
const reviews=artifacts.map(b=>({format:'codegraph-source-review-1',id:'source-example',version:JSON.parse(b).package.version,integrity:hash(b),publisherId,repository,revision,path:source,checkedAt:'2026-09-23T00:00:00Z'}));
|
||||
function pass(name){checks.push(name);console.log('PASS',name);fs.writeFileSync(path.join(out,'progress.json'),JSON.stringify({checks},null,2));}
|
||||
function routes(){const f=path.join(root,'.codegraph/codegraph.db');if(!fs.existsSync(f))return [];const db=new DatabaseSync(f,{readOnly:true});try{return db.prepare("SELECT name FROM nodes WHERE kind='route'").all().map(n=>n.name);}finally{db.close();}}
|
||||
let server,bridge,browser,page;
|
||||
(async()=>{try{
|
||||
const {startLocal}=await import('../../marketplace/cloudflare/local.mjs');server=await startLocal({state:path.join(lab,'state'),testing:true,sourceReviews:reviews});const origin='http://127.0.0.1:'+server.port;
|
||||
async function publish(bytes){const payload=JSON.stringify({name:'Source example',description:'Inspect all packaged code',publisher:'Fixture publisher',readme:'Source inspection acceptance',source:repository,sourceRevision:revision,sourcePath:source,artifact:bytes.toString('base64'),timestamp:Date.now(),nonce:crypto.randomUUID()});const r=await fetch(origin+'/api/publish',{method:'POST',body:JSON.stringify({payload,publicKey:keys.publicKey.export({format:'jwk'}),signature:crypto.sign('sha256',Buffer.from(payload),{key:keys.privateKey,dsaEncoding:'ieee-p1363'}).toString('base64')})});assert.equal(r.status,201,await r.clone().text());return r.json();}
|
||||
await publish(artifacts[0]);await publish(artifacts[1]);
|
||||
bridge=await startExtensionBridge([root],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},acceptDownloads:true});page.setDefaultTimeout(30000);const errors=[];page.on('pageerror',e=>errors.push(e.message));
|
||||
await page.goto(origin+'/extensions/source-example');await page.locator('[data-view-source]').click();await page.locator('#source-content').waitFor();assert.match(await page.locator('.source-verified').innerText(),/9.0.0/);assert.equal(await page.evaluate(()=>state.snapshot),null);assert.deepEqual(routes(),[]);assert.equal(await page.locator('#source-file option').count(),5);pass('unconnected browser lists every runtime dependency and manifest before any install');
|
||||
await page.locator('#source-file').selectOption('readme.md');assert.equal(await page.locator('#source-content').innerText(),JSON.parse(artifacts[1]).files['readme.md']);assert.equal(await page.locator('#source-content script, #source-content img').count(),0);assert.equal(await page.evaluate(()=>window.__packageExecuted),undefined);pass('HTML and executable extension source render inertly as literal text');
|
||||
await page.locator('#source-file').selectOption('large.txt');assert.equal((await page.locator('#source-content').innerText()).length,64000);await page.locator('#source-next').click();assert.match(await page.locator('#source-range').innerText(),/64001/);pass('large source content is paged without truncating downloadable files');
|
||||
const dl=page.waitForEvent('download');await page.locator('#source-package-download').click();const downloaded=await dl;await downloaded.saveAs(path.join(out,'verified-download.cgext'));assert.equal(hash(fs.readFileSync(path.join(out,'verified-download.cgext'))),hash(artifacts[1]));pass('source download uses the same verified immutable bytes');
|
||||
assert.match(await page.locator('.source-inspection').innerText(),/Operator checked.*public committed package/s);await page.locator('#source-file').selectOption('index.cjs');await page.screenshot({path:path.join(out,'desktop-source.png'),fullPage:true});
|
||||
// Connect after inspecting incompatible catalog 9.0.0. The engine chooses 1.0.0.
|
||||
await page.locator('#connect').click();await page.locator('#connection-link').fill(bridge.connectionUrl);const pop=page.waitForEvent('popup');await page.locator('#open-companion').click();const popup=await pop;await popup.getByRole('button',{name:'Allow connection'}).click();await page.locator('.compatibility').filter({hasText:'Selected 1.0.0'}).waitFor();
|
||||
await page.getByRole('button',{name:'Install extension'}).click();await page.getByText('Source review does not match the selected release. View its source again before installing.').waitFor();assert.deepEqual(routes(),[]);pass('connecting a destination cannot substitute its compatible version for previously reviewed catalog bytes');
|
||||
// Inspect the compatible version, then publish another immutable selection.
|
||||
await page.locator('[data-view-source]').click();await page.locator('.source-verified').filter({hasText:'1.0.0'}).waitFor();
|
||||
await publish(artifacts[2]);await page.evaluate(()=>refresh());await page.locator('.compatibility').filter({hasText:'Selected 2.0.0'}).waitFor();await page.getByRole('button',{name:'Install extension'}).click();await page.getByText('Source review does not match the selected release. View its source again before installing.').waitFor();assert.deepEqual(routes(),[]);pass('catalog selection changing after inspection blocks install rather than switching versions');
|
||||
// Hold a download, change selection epoch, then release it: stale completion cannot certify a review.
|
||||
let release;await page.route('**/api/download/source-example/2.0.0',async r=>{await new Promise(resolve=>release=resolve);await r.continue();});await page.locator('[data-view-source]').click();await page.waitForFunction(()=>inspection.loading);while(!release)await new Promise(r=>setTimeout(r,10));await page.evaluate(()=>refreshSelections());release();await page.getByText('Selection changed while loading. Inspect the current selected version before installing.').waitFor();await page.unroute('**/api/download/source-example/2.0.0');assert.equal(await page.evaluate(()=>inspection.active),null);pass('in-flight source selection race cannot certify stale bytes');
|
||||
await page.route('**/api/download/source-example/2.0.0',r=>r.fulfill({status:200,body:Buffer.concat([artifacts[2],Buffer.from(' ')])}));await page.locator('[data-view-source]').click();await page.getByText(/Package fingerprint changed/).waitFor();assert.equal(await page.locator('#source-content').count(),0);await page.unroute('**/api/download/source-example/2.0.0');pass('download corruption hides content and blocks review-dependent installation');
|
||||
await page.locator('[data-view-source]').click();await page.locator('.source-verified').filter({hasText:'2.0.0'}).waitFor();await page.getByRole('button',{name:'Install extension'}).click();await page.getByRole('button',{name:'Disable',exact:true}).waitFor();assert.deepEqual(routes(),['/inspected-2.0.0']);pass('reviewed exact version installs once and produces actual SQLite graph');
|
||||
await page.setViewportSize({width:390,height:844});await page.screenshot({path:path.join(out,'mobile-source.png'),fullPage:true});assert.ok(await page.evaluate(()=>document.documentElement.scrollWidth<=innerWidth));pass('mobile file inspection fits viewport and preserves literal source');
|
||||
await publish(artifacts[3]);await page.evaluate(()=>refresh());await page.locator('.compatibility').filter({hasText:'Selected 3.0.0'}).waitFor();await page.getByRole('button',{name:'Update to 3.0.0'}).click();assert.deepEqual(routes(),['/inspected-2.0.0']);await page.locator('[data-view-source]').click();await page.locator('.source-verified').filter({hasText:'3.0.0'}).waitFor();await page.getByRole('button',{name:'Update to 3.0.0'}).click();await page.waitForFunction(()=>state.snapshot?.error&&!state.snapshot.busy);assert.deepEqual(routes(),['/inspected-2.0.0']);pass('review required for changed update; actual throwing update preserves working graph');
|
||||
await page.getByRole('button',{name:'Disable',exact:true}).click();await page.getByRole('button',{name:'Enable',exact:true}).waitFor();assert.deepEqual(routes(),[]);await page.getByRole('button',{name:'Remove',exact:true}).click();await page.getByText('Extension removed and graph refreshed',{exact:true}).first().waitFor();assert.deepEqual(routes(),[]);pass('disable and removal retain managed lifecycle behavior');
|
||||
// Exercise malformed package identity/path/display bounds using the real browser verifier, no package execution.
|
||||
const negative=await page.evaluate(async()=>{const results=[];for(const kind of ['identity','path','binary','files','size']){const p={format:'codegraph-extension-1',package:{version:'1.0.0',main:'index.cjs',codegraph:{id:'check'}},files:{'index.cjs':'module.exports={}'}};if(kind==='identity')p.package.version='2.0.0';if(kind==='path')p.files['../escape.cjs']='x';if(kind==='binary')p.files['index.cjs']='\0';if(kind==='files')for(let i=0;i<257;i++)p.files[i+'.txt']='x';const bytes=new TextEncoder().encode(JSON.stringify(p)),integrity=[...new Uint8Array(await crypto.subtle.digest('SHA-256',bytes))].map(b=>b.toString(16).padStart(2,'0')).join('');try{await verifiedSource(new Response(bytes,{headers:kind==='size'?{'content-length':String(8*1024*1024+1)}:{}}),{id:'check',version:'1.0.0',integrity});results.push(false);}catch{results.push(true);}}return results;});assert.deepEqual(negative,[true,true,true,true,true]);pass('browser verifier rejects identity traversal NUL excessive files and size violations');
|
||||
assert.deepEqual(errors,[]);fs.writeFileSync(path.join(out,'summary.json'),JSON.stringify({success:true,checks,browser:browser.version(),sourcePolicy:'explicit test records, no public provider verification claim',packageHashes:artifacts.map(hash)},null,2));
|
||||
}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)},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;});
|
||||
Reference in New Issue
Block a user