Verify original character pack hashes while allowing independent editorial covers
waterfall-wiki CI and deployment / verify-and-deploy (push) Successful in 17m46s
waterfall-wiki CI and deployment / verify-and-deploy (push) Successful in 17m46s
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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)});
|
||||
Reference in New Issue
Block a user