Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/deploy-writer.yml
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ jobs:
mkdir -p "$RUNNER_TEMP/writer-tools"
cp tools/native-publisher/{package.json,pnpm-lock.yaml} "$RUNNER_TEMP/writer-tools/"
(cd "$RUNNER_TEMP/writer-tools" && NPM_CONFIG_USERCONFIG=/dev/null NPM_CONFIG_GLOBALCONFIG=/dev/null pnpm --ignore-workspace install --frozen-lockfile --ignore-scripts --ignore-pnpmfile)
"$RUNNER_TEMP/writer-tools/node_modules/.bin/wrangler" pages functions build apps/writer/functions --outfile apps/writer/dist/_worker.js
"$RUNNER_TEMP/writer-tools/node_modules/.bin/wrangler" pages functions build apps/writer/functions --outdir apps/writer/dist/_worker.js
cp -a apps/writer/dist/. "$RUNNER_TEMP/writer-build/publish/"
node apps/writer/scripts/native-publisher.mjs inventory --dist "$RUNNER_TEMP/writer-build/publish" --plan "$RUNNER_TEMP/writer-build/plan.json" > "$RUNNER_TEMP/writer-build/inventory.json"
# Official npm ORIGINAL DATA, root-lock SRI verified; never R2 writes.
Expand Down
6 changes: 3 additions & 3 deletions apps/writer/scripts/native-publisher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export function assertInventoryCoverage(inventory, sdkTrust) {
for (const [name, pin] of Object.entries(sdkTrust?.nativeAssets ?? {})) {
if (!inventory.some((asset) => asset.sha256 === pin.sha256 && asset.bytes === pin.bytes)) throw new Error(`Missing exact SDK native/SQLite bytes for ${name}; refusing`);
}
if (sdkTrust && !inventory.some(asset => asset.path === "_worker.js")) throw new Error("Missing precompiled Pages Worker; refusing");
if (sdkTrust && !inventory.some(asset => asset.path === "_worker.js" || asset.path === "_worker.js/index.js")) throw new Error("Missing precompiled Pages Worker; refusing");
const present = new Set(inventory.map((asset) => classifyAsset(asset.path)));
const missing = requiredAssetCategories.filter((category) => !present.has(category));
if (missing.length > 0) throw new Error(`Built assets are missing required categories: ${missing.join(", ")}; refusing to upload.`);
Expand All @@ -122,7 +122,7 @@ export function parseDeploymentId(output) {
export async function captureDeployment({ output, target, sha, accountId, apiToken, fetchImpl = fetch }) {
const prefix = parseDeploymentId(output);
if (!prefix || !accountId || !apiToken) throw new Error("Refusing: missing unique deployment URL or Cloudflare credentials");
const response = await fetchImpl(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/pages/projects/mdbase-writer/deployments?per_page=100`, {
const response = await fetchImpl(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/pages/projects/mdbase-writer/deployments?per_page=20`, {
headers: { Authorization: `Bearer ${apiToken}` }, redirect: "error", signal: AbortSignal.timeout(30000),
});
if (!response.ok) throw new Error(`Deployment ID lookup failed: HTTP ${response.status}`);
Expand All @@ -139,7 +139,7 @@ function publicationEvidenceComplete(inventory, verification, deploymentId, sour
const policy = verification.policyEvidence;
if (policy?.metadata?.deployment?.id !== deploymentId || policy.metadata.deployment.sourceSha !== sourceSha || policy.behavior?.length !== 4 || verification.runtimeVerified?.length !== 2 || !/^[0-9a-f]{64}$/.test(policy.sourceConfigSha256 ?? "")) return false;
const covered = policy.coveredControls ?? [];
if (covered.length !== 2 || !covered.some(a => a.path === "_routes.json") || !covered.some(a => a.path === "_worker.js")) return false;
if (!covered.some(a => a.path === "_routes.json") || !covered.some(a => a.path === "_worker.js" || a.path === "_worker.js/index.js") || covered.some(a => a.path !== "_routes.json" && a.path !== "_worker.js" && !a.path.startsWith("_worker.js/"))) return false;
const seen = [...(verification.verified ?? []), ...covered];
return seen.length === inventory.length && new Set(seen.map(a => a.path)).size === seen.length && inventory.every(asset => seen.some(item => item.path === asset.path && item.sha256 === asset.sha256 && item.bytes === asset.bytes));
}
Expand Down
3 changes: 2 additions & 1 deletion apps/writer/scripts/native-publisher.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,8 @@ test("receipt cannot pass without both a deployment ID and exact served bytes",
test("API deployment ID must match the unique URL, source and Pages channel", async () => {
const deployment = { id: "12345678-1234-1234-1234-123456789abc", url: "https://abcdef12.mdbase-writer.pages.dev", deployment_trigger: { metadata: { commit_hash: FULL_SHA, branch: "staging" } } };
const args = { output: deployment.url, target: "staging", sha: FULL_SHA, accountId: "fixture-account", apiToken: "fixture-not-a-secret" };
const fetchImpl = async (_url, options) => {
const fetchImpl = async (url, options) => {
assert.equal(url, "https://api.cloudflare.com/client/v4/accounts/fixture-account/pages/projects/mdbase-writer/deployments?per_page=20");
assert.equal(options.redirect, "error");
return { ok: true, json: async () => ({ success: true, result: [deployment] }) };
};
Expand Down
7 changes: 4 additions & 3 deletions scripts/lib/writer-hosting-policy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ export async function readHostingMetadata({policy,plan,deployment,preflight,acco
try{assertHostingMetadata(policy,result,plan,deployment,preflight);}catch(error){error.safeEvidence=result;throw error;}return result;
}
export function coveredHostingControls(policy,inventory) {
const routes=inventory.find(r=>r.path==='_routes.json'),worker=inventory.find(r=>r.path==='_worker.js');
check(routes?.sha256===policy.routesSha256&&worker?.bytes>0,'immutable source routes/precompiled Worker coverage');
return [routes,worker].map(row=>({...row,evidence:'immutable-upload-inventory + source-config + scoped-deployment/current-project-config + runtime-behavior (NOT live Worker-source GET)'}));
const routes=inventory.find(r=>r.path==='_routes.json'),workers=inventory.filter(r=>r.path==='_worker.js'||r.path.startsWith('_worker.js/'));
const entry=workers.find(r=>r.path==='_worker.js'||r.path==='_worker.js/index.js');
check(routes?.sha256===policy.routesSha256&&entry?.bytes>0,'immutable source routes/precompiled Worker coverage');
return [routes,...workers].map(row=>({...row,evidence:'immutable-upload-inventory + source-config + scoped-deployment/current-project-config + runtime-behavior (NOT live Worker-source GET)'}));
}
27 changes: 20 additions & 7 deletions scripts/native-tool-compat.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Offline compiler compatibility only; never deploy, no Cloudflare credentials.
// SYNTHETIC Functions compatibility only; never app/SDK build or deployment.
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { cp, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { execFileSync, spawnSync } from 'node:child_process';
import { cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { sourceHostingPolicy, uploadConfiguration } from './lib/writer-hosting-policy.mjs';
import { resolve, join } from 'node:path';
Expand All @@ -22,8 +22,21 @@ try {
assert.deepEqual(parsed.compatibility_flags,[]);
}
console.log('PASS Wrangler4.114 documented config parser: isolated SOURCE binding/date in both Pages channels');
execFileSync(join(root,'node_modules/.bin/wrangler'),['pages','functions','build',join(repo,'apps/writer/functions'),'--outfile',join(root,'_worker.js')],{cwd:root,env,stdio:'inherit'});
const worker=await readFile(join(root,'_worker.js'),'utf8');
assert.ok(worker.includes('WRITER_ASSETS') && worker.includes('application/wasm'));
console.log('PASS isolated integrity-locked Wrangler 4.114.0 --ignore-scripts + Pages Functions precompile (NO deploy)');
// Exercise the SAME output mode selected by the real build workflow, but with
// a tiny non-product fixture. Parse only; NEVER execute its generated Worker.
const workflow=await readFile(join(repo,'.github/workflows/deploy-writer.yml'),'utf8');
const flag=workflow.match(/pages functions build apps\/writer\/functions (--out(?:file|dir)) apps\/writer\/dist\/_worker\.js/)[1];
const functions=join(root,'synthetic-functions'),output=join(root,'_worker.js');
await mkdir(functions);
await writeFile(join(functions,'index.js'),'export const onRequestGet = ({env}) => new Response("SYNTHETIC_WRITER_FUNCTIONS", {headers:{"Content-Type":"application/wasm","X-Synthetic-Binding":typeof env.WRITER_ASSETS}});');
execFileSync(join(root,'node_modules/.bin/wrangler'),['pages','functions','build','--help'],{cwd:root,env,stdio:'inherit'});
execFileSync(join(root,'node_modules/.bin/wrangler'),['pages','functions','build',functions,flag,output],{cwd:root,env,stdio:'inherit'});
const directory=(await stat(output)).isDirectory();
const worker=await readFile(directory?join(output,'index.js'):output,'utf8');
const syntax=spawnSync(process.execPath,['--input-type=module','--check'],{input:worker,env,encoding:'utf8'});
console.log(JSON.stringify({kind:'SYNTHETIC UNIT DATA ONLY',officialWrangler:'4.114.0',flag,directory,multipartMetadata:worker.includes('Content-Disposition: form-data; name="metadata"'),moduleSyntaxExit:syntax.status}));
assert.equal(syntax.status,0,'compiled output must be JavaScript, NOT a multipart upload body');
assert.ok(directory,'official advanced-mode _worker.js/index.js layout');
assert.ok(worker.includes('SYNTHETIC_WRITER_FUNCTIONS') && worker.includes('WRITER_ASSETS') && worker.includes('application/wasm'));
console.log('PASS official advanced-mode JavaScript from SYNTHETIC Functions; NO product build/deploy');
} finally {await rm(root,{recursive:true,force:true});}
4 changes: 2 additions & 2 deletions scripts/qualify-native-staging.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ try{
run('pnpm',['--ignore-workspace','install','--frozen-lockfile','--ignore-scripts','--ignore-pnpmfile'],tools);
assert.equal(JSON.parse(await readFile(join(tools,'node_modules/wrangler/package.json'))).version,'4.114.0');
await writeFile(join(tools,'wrangler.json'),JSON.stringify(uploadConfiguration(await sourceHostingPolicy(repo))));
run(join(tools,'node_modules/.bin/wrangler'),['pages','functions','build',join(repo,'apps/writer/functions'),'--outfile',join(dist,'_worker.js')],tools);
run(join(tools,'node_modules/.bin/wrangler'),['pages','functions','build',join(repo,'apps/writer/functions'),'--outdir',join(dist,'_worker.js')],tools);
}
// verify mode is POST-ONLY: never app rebuild or Functions recompilation.
run('node',['apps/writer/scripts/validate-manifest.mjs',join(dist,'.well-known/mdbase-app.json')]);
Expand All @@ -61,7 +61,7 @@ try{
const offlineShell=await assertWriterOfflineShell({directory:dist,repoRoot:repo,sdkTrust});
assert.deepEqual(offlineShell.inventory.map(({path,bytes,sha256})=>({path,bytes,sha256})),inventory.map(({path,bytes,sha256})=>({path,bytes,sha256})));
coveredHostingControls(await sourceHostingPolicy(repo),inventory);
const js=await Promise.all(inventory.filter(a=>/\.m?js$/.test(a.path)&&a.path!=='_worker.js').map(a=>readFile(join(dist,a.path),'utf8')));
const js=await Promise.all(inventory.filter(a=>/\.m?js$/.test(a.path)&&a.path!=='_worker.js'&&!a.path.startsWith('_worker.js/')).map(a=>readFile(join(dist,a.path),'utf8')));
for(const literal of [sdkTrust.release.cpOrigin,sdkTrust.release.logOrigin,sdkTrust.release.assetSha256,sdkTrust.release.source.commit,sdkTrust.editorOrigin])assert.ok(js.some(s=>s.includes(literal)),'missing exact compiled staging trust/Editor binding');
await mkdir(join(repo,'test-results'),{recursive:true});
await writeFile(join(repo,'test-results/writer-staging-qualification.json'),JSON.stringify({...evidence,mode,offlineShell,inventory},null,2));
Expand Down
22 changes: 20 additions & 2 deletions tests/writer-hosting-policy.test.mjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { readFile, writeFile } from 'node:fs/promises';
import { readFile, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { inventoryDirectory } from '../scripts/lib/native-publish-bytes.mjs';
import { assertRuntimeOriginals, deriveRuntimeOriginals, runtimeSourcePins } from '../scripts/lib/writer-runtime-originals.mjs';
import { sourceHostingPolicy, uploadConfiguration, readHostingMetadata, assertHostingMetadata } from '../scripts/lib/writer-hosting-policy.mjs';
import { createReceipt } from '../apps/writer/scripts/native-publisher.mjs';
import { assertInventoryCoverage, createReceipt } from '../apps/writer/scripts/native-publisher.mjs';
import { verifyServedAssets } from '../apps/writer/scripts/verify-served.mjs';
import { hostingFixture } from './writer-hosting-fixture.mjs';
const origin='https://staging.mdbase-writer.pages.dev';
Expand All @@ -27,6 +27,24 @@ test('known uploaded routes and Worker clear ONLY with source config, actual met
assert.equal(createReceipt(args).status,'verified');
assert.equal(createReceipt({...args,verification:{ok:true,policy_verified:true,verified:f.inventory,failures:[],unverified:[]}}).status,'failed-or-incomplete');
});
test('official advanced-mode Worker directory retains EVERY module/map in inventory and hosting evidence',async t=>{
const f=await hostingFixture(t); // SYNTHETIC data only, not compiled app output.
await rm(join(f.publish,'_worker.js'));
for(const [name,body] of Object.entries({'index.js':'export default {};','module.mjs':'export {};','index.js.map':'{}'}))await f.put(join(f.publish,'_worker.js',name),body);
const inventory=await inventoryDirectory(f.publish);
assert.doesNotThrow(()=>assertInventoryCoverage(inventory,{nativeAssets:{}}));
const result=await verifyServedAssets({origin,inventory,publication:f.publication,fetch:f.fetch,attempts:1});
assert.equal(result.ok,true,JSON.stringify(result));
assert.deepEqual(result.policyEvidence.coveredControls.map(a=>a.path).sort(),['_routes.json','_worker.js/index.js','_worker.js/index.js.map','_worker.js/module.mjs']);
assert.ok(result.verified.every(a=>!a.path.startsWith('_worker.js/')));
const args={target:'staging',source:{sha:f.plan.sha,tree:f.plan.tree},releaseRef:'release/native-staging',sdkTrust:{},inventory,deploymentId:f.deployment.id,verification:result};
assert.equal(createReceipt(args).status,'verified');
const omitted=structuredClone(result);omitted.policyEvidence.coveredControls.pop();
assert.equal(createReceipt({...args,verification:omitted}).status,'failed-or-incomplete');
const changed=structuredClone(result);changed.policyEvidence.coveredControls[1].sha256='0'.repeat(64);
assert.equal(createReceipt({...args,verification:changed}).status,'failed-or-incomplete');
assert.throws(()=>assertInventoryCoverage(inventory.filter(a=>a.path!=='_worker.js/index.js'),{nativeAssets:{}}),/Missing precompiled Pages Worker/);
});
for(const field of ['bucket','compatibility','deployment']) test('even exact static bytes refuse wrong '+field+' metadata',async t=>{
const f=await hostingFixture(t),metadata=structuredClone(f.metadata);
if(field==='bucket') metadata.configuration.r2_buckets.WRITER_ASSETS.name='wrong-bucket';
Expand Down