63 lines
4.3 KiB
TypeScript
63 lines
4.3 KiB
TypeScript
// Run inside the disposable packaged container with only these synthetic keys.
|
|
// Phases let the parent recreate the container against the same disposable data.
|
|
import { finalizeEvent, getPublicKey } from 'nostr-tools';
|
|
const base = 'http://127.0.0.1:3000';
|
|
const key = new Uint8Array(32).fill(1);
|
|
const other = new Uint8Array(32).fill(2);
|
|
const body = '<!doctype html><script>throw new Error("must not execute")</script><p>Blossom qualification, synthetic data only.</p>';
|
|
const bytes = new TextEncoder().encode(body);
|
|
const hash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), x => x.toString(16).padStart(2, '0')).join('');
|
|
const phase = Deno.env.get('BLOSSOM_FIXTURE_PHASE') ?? 'seed';
|
|
|
|
function auth(action: string, secret=key, server='127.0.0.1', expires=300, scopedHash=hash) {
|
|
const now = Math.floor(Date.now()/1000);
|
|
return 'Nostr ' + btoa(JSON.stringify(finalizeEvent({ kind:24242, created_at:now, content:'Local synthetic qualification only', tags:[['t',action],['x',scopedHash],['server',server],['expiration',String(now+expires)]] },secret)));
|
|
}
|
|
async function check(label: string, expected: number, path: string, init={}) {
|
|
const r=await fetch(base+path,init);
|
|
if(r.status!==expected) throw new Error(`${label}: expected ${expected}, got ${r.status}: ${await r.text()}`);
|
|
console.log(`PASS ${label}: ${r.status}`);return r;
|
|
}
|
|
async function denied(label: string, path: string, init={}) {
|
|
const r=await fetch(base+path,init);
|
|
if(![401,403].includes(r.status)) throw new Error(`${label}: expected 401/403, got ${r.status}: ${await r.text()}`);
|
|
console.log(`PASS ${label}: ${r.status}`);return r;
|
|
}
|
|
const upload=(token?:string)=>({method:'PUT',headers:{'content-type':'text/html',...(token?{authorization:token}:{})},body});
|
|
|
|
async function verifyStored() {
|
|
const read=await check('read stored bytes',200,'/'+hash);
|
|
if(await read.text()!==body) throw new Error('Stored bytes differ');
|
|
if(!read.headers.get('content-security-policy')?.includes('sandbox') || read.headers.get('content-disposition')!=='attachment') throw new Error('Active content not sandboxed');
|
|
console.log('PASS exact bytes and sandboxed attachment');
|
|
await check('anonymous list denied',401,'/list/'+getPublicKey(key));
|
|
await denied('other identity cannot list owner','/list/'+getPublicKey(key),{headers:{authorization:auth('list',other)}});
|
|
const listed=await (await check('owner list',200,'/list/'+getPublicKey(key),{headers:{authorization:auth('list')}})).json();
|
|
if(!Array.isArray(listed)||!listed.some(file=>file.sha256===hash&&file.size===bytes.length)) throw new Error('Owner list omitted preserved fixture');
|
|
}
|
|
|
|
if(phase==='seed') {
|
|
await check('unauthenticated upload denied',401,'/upload',upload());
|
|
await denied('unlisted identity denied','/upload',upload(auth('upload',other)));
|
|
await denied('wrong host denied','/upload',upload(auth('upload',key,'wrong.invalid')));
|
|
await denied('expired token denied','/upload',upload(auth('upload',key,'127.0.0.1',-300)));
|
|
const stored=await (await check('signed profile upload',201,'/upload',upload(auth('upload')))).json();
|
|
if(stored.sha256!==hash || stored.size!==bytes.length) throw new Error('Wrong descriptor');
|
|
await verifyStored();
|
|
await check('mirror disabled',403,'/mirror',{method:'PUT',headers:{authorization:auth('upload')}});
|
|
await check('canonical signer provider',200,'/nostr-provider.js');
|
|
await check('health',200,'/healthz');
|
|
console.log('PRESERVE_HASH '+hash);
|
|
} else if(phase==='verify') {
|
|
await verifyStored();
|
|
await denied('anonymous delete denied','/'+hash,{method:'DELETE'});
|
|
await denied('unlisted identity delete denied','/'+hash,{method:'DELETE',headers:{authorization:auth('delete',other)}});
|
|
await denied('wrong-host delete denied','/'+hash,{method:'DELETE',headers:{authorization:auth('delete',key,'wrong.invalid')}});
|
|
await denied('wrong-hash delete denied','/'+hash,{method:'DELETE',headers:{authorization:auth('delete',key,'127.0.0.1',300,'0'.repeat(64))}});
|
|
await check('fixture retained after delete negatives',200,'/'+hash);
|
|
console.log('PRESERVED_AFTER_RESTART '+hash);
|
|
} else if(phase==='cleanup') {
|
|
await check('owner-authorized fixture cleanup',204,'/'+hash,{method:'DELETE',headers:{authorization:auth('delete')}});
|
|
await check('fixture absent after explicit cleanup',404,'/'+hash);
|
|
} else throw new Error(`Unknown BLOSSOM_FIXTURE_PHASE: ${phase}`);
|