From 939e0026463e063293b864c0d42e3cbc615e8e8d Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Tue, 27 Oct 2026 15:19:53 +0800 Subject: [PATCH] Verify original character pack hashes while allowing independent editorial covers --- Dockerfile | 2 +- apps/waterfall-wiki/Dockerfile | 2 +- .../scripts/import-character-packs.mjs | 17 ++++++++++------- .../scripts/verify-character-pack.mjs | 6 ++++++ .../tests/character-pack.test.mjs | 3 +++ 5 files changed, 21 insertions(+), 9 deletions(-) create mode 100644 apps/waterfall-wiki/scripts/verify-character-pack.mjs create mode 100644 apps/waterfall-wiki/tests/character-pack.test.mjs diff --git a/Dockerfile b/Dockerfile index 0a98de4..43501a2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,7 +30,7 @@ COPY --from=build --chown=node:node /app/.next/standalone/ ./ COPY --from=build --chown=node:node /app/dist/worker.cjs /app/dist/bootstrap.cjs /app/dist/publish-learning-series.cjs /app/dist/auth-cli.cjs ./ COPY --from=build --chown=node:node /app/db/ ./db/ COPY --chown=node:node apps/waterfall-wiki/scripts/container-health.mjs ./ -COPY --chown=node:node apps/waterfall-wiki/scripts/import-character-packs.mjs ./scripts/import-character-packs.mjs +COPY --chown=node:node apps/waterfall-wiki/scripts/import-character-packs.mjs apps/waterfall-wiki/scripts/verify-character-pack.mjs ./scripts/ COPY --chown=node:node apps/waterfall-wiki/seed/characters/ ./seed/characters/ USER node EXPOSE 3000 diff --git a/apps/waterfall-wiki/Dockerfile b/apps/waterfall-wiki/Dockerfile index 0a98de4..43501a2 100644 --- a/apps/waterfall-wiki/Dockerfile +++ b/apps/waterfall-wiki/Dockerfile @@ -30,7 +30,7 @@ COPY --from=build --chown=node:node /app/.next/standalone/ ./ COPY --from=build --chown=node:node /app/dist/worker.cjs /app/dist/bootstrap.cjs /app/dist/publish-learning-series.cjs /app/dist/auth-cli.cjs ./ COPY --from=build --chown=node:node /app/db/ ./db/ COPY --chown=node:node apps/waterfall-wiki/scripts/container-health.mjs ./ -COPY --chown=node:node apps/waterfall-wiki/scripts/import-character-packs.mjs ./scripts/import-character-packs.mjs +COPY --chown=node:node apps/waterfall-wiki/scripts/import-character-packs.mjs apps/waterfall-wiki/scripts/verify-character-pack.mjs ./scripts/ COPY --chown=node:node apps/waterfall-wiki/seed/characters/ ./seed/characters/ USER node EXPOSE 3000 diff --git a/apps/waterfall-wiki/scripts/import-character-packs.mjs b/apps/waterfall-wiki/scripts/import-character-packs.mjs index 91abc98..49b0a23 100644 --- a/apps/waterfall-wiki/scripts/import-character-packs.mjs +++ b/apps/waterfall-wiki/scripts/import-character-packs.mjs @@ -1,17 +1,20 @@ import {readFile,readdir} from 'node:fs/promises'; import {fileURLToPath} from 'node:url'; import {join,dirname} from 'node:path'; +import {createHash} from 'node:crypto'; +import {requiredPackImages} from './verify-character-pack.mjs'; const root=join(dirname(fileURLToPath(import.meta.url)),'..','seed','characters'); const base=process.env.PROMPT_IMPORT_BASE_URL||'http://127.0.0.1:3000'; const packs=JSON.parse(await readFile(join(root,'packs.json'),'utf8')); async function verify(pack){ - const response=await fetch(`${base}/api/feed?section=images&q=${encodeURIComponent(pack.title)}`); - if(!response.ok)throw new Error(`Cannot verify feed: ${response.status}`); - const post=(await response.json()).posts.find(p=>p.slug===pack.slug); - const images=post?.assets.filter(a=>a.kind==='image'); - if(images?.length!==pack.count)throw new Error(`Wrong image count for ${pack.slug}`); - for(const image of images){const media=await fetch(`${base}/api/media/${image.id}`,{method:'HEAD'});if(!media.ok||media.headers.get('content-type')!=='image/png')throw new Error(`Missing PNG ${image.id}`);} - console.log(`Verified: /p/${pack.slug} (${images.length} stored PNGs)`); + let cursor='',post;const seen=new Set(); + do{const response=await fetch(base+'/api/feed?'+new URLSearchParams({section:'images',...(cursor?{cursor}:{})}));if(!response.ok)throw Error('Cannot verify character feed: '+response.status);const page=await response.json();post=page.posts.find(p=>p.slug===pack.slug);cursor=page.nextCursor||'';if(cursor&&seen.has(cursor))throw Error('Character feed cursor did not advance');seen.add(cursor);}while(!post&&cursor); + if(!post)throw Error('Character pack missing: '+pack.slug); + const files=(await readdir(join(root,pack.directory))).filter(n=>n.endsWith('.png')).sort();if(files.length!==pack.count)throw Error('Seed file count mismatch: '+pack.slug); + const hashes=await Promise.all(files.map(async file=>createHash('sha256').update(await readFile(join(root,pack.directory,file))).digest('hex'))); + const originals=requiredPackImages(hashes,post.assets.filter(a=>a.kind==='image')); + for(const image of originals){const media=await fetch(`${base}/api/media/${image.id}`,{method:'HEAD'});if(!media.ok||media.headers.get('content-type')!=='image/png')throw Error('Original PNG unavailable');} + console.log(`Verified: /p/${pack.slug} (${originals.length} original PNGs preserved; editorial covers allowed)`); } for(const pack of packs){ const existing=await fetch(`${base}/p/${pack.slug}`); diff --git a/apps/waterfall-wiki/scripts/verify-character-pack.mjs b/apps/waterfall-wiki/scripts/verify-character-pack.mjs new file mode 100644 index 0000000..ab037a1 --- /dev/null +++ b/apps/waterfall-wiki/scripts/verify-character-pack.mjs @@ -0,0 +1,6 @@ +/** Extra editorial covers are allowed; every original seed image must remain attached. */ +export function requiredPackImages(hashes,images){ + const remaining=[...images],originals=[]; + for(const hash of hashes){const index=remaining.findIndex(image=>image.sha256===hash);if(index<0)throw Error('Original seed image missing from character pack');originals.push(...remaining.splice(index,1));} + return originals; +} diff --git a/apps/waterfall-wiki/tests/character-pack.test.mjs b/apps/waterfall-wiki/tests/character-pack.test.mjs new file mode 100644 index 0000000..8f349c7 --- /dev/null +++ b/apps/waterfall-wiki/tests/character-pack.test.mjs @@ -0,0 +1,3 @@ +import {test} from 'node:test';import assert from 'node:assert/strict';import {requiredPackImages} from '../scripts/verify-character-pack.mjs'; +test('editorial covers do not invalidate preserved original packs',()=>{const originals=[{id:'a',sha256:'hash-a'},{id:'b',sha256:'hash-b'}];assert.deepEqual(requiredPackImages(['hash-a','hash-b'],[{id:'cover',sha256:'cover'},...originals]),originals)}); +test('extra images cannot disguise missing or replaced originals',()=>{assert.throws(()=>requiredPackImages(['a','b'],[{sha256:'a'},{sha256:'x'},{sha256:'y'}]));assert.throws(()=>requiredPackImages(['a','a'],[{sha256:'a'}]));assert.equal(requiredPackImages(['a','a'],[{sha256:'a'},{sha256:'a'}]).length,2)});