feat: integrate local Blossom, reviewed nsites and scoped app access

This commit is contained in:
archipelago
2026-10-08 09:12:40 -04:00
parent 05e999b117
commit 28a92fcc9b
39 changed files with 2060 additions and 50 deletions
+36
View File
@@ -0,0 +1,36 @@
// Run inside a disposable Blossom container with ONLY the synthetic profile below allowed.
// No real identity, external server or public relay is used. Retains one fixture for lifecycle checks.
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('');
function auth(action: string, secret=key, server='127.0.0.1', expires=300) {
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',hash],['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;
}
const upload=(token?:string)=>({method:'PUT',headers:{'content-type':'text/html',...(token?{authorization:token}:{})},body});
await check('unauthenticated upload denied',401,'/upload',upload());
await check('unlisted identity denied',401,'/upload',upload(auth('upload',other)));
await check('wrong host denied',401,'/upload',upload(auth('upload',key,'wrong.invalid')));
await check('expired token denied',401,'/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');
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 check('other identity cannot list owner',403,'/list/'+getPublicKey(key),{headers:{authorization:auth('list',other)}});
await check('owner list',200,'/list/'+getPublicKey(key),{headers:{authorization:auth('list')}});
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);
+40
View File
@@ -0,0 +1,40 @@
// Run against the disposable packaged UI forwarded to 127.0.0.1:48191.
// The signer and upload transport are mocked; no real keys or public endpoints.
const { chromium } = require('../../../neode-ui/node_modules/@playwright/test');
const { createHash } = require('node:crypto');
(async () => {
const browser = await chromium.launch({headless:true});
const page = await browser.newPage({viewport:{width:390,height:844}});
page.on('console',m=>console.log('browser:',m.text())); page.on('pageerror',e=>console.log('page error:',e.message));
const outgoing=[]; let uploads=0;
await page.route('**/*', async route => {
const u=new URL(route.request().url());
if(u.origin!=='http://127.0.0.1:48191'){outgoing.push(u.origin);return route.abort();}
if(u.pathname==='/nostr-provider.js')return route.fulfill({contentType:'application/javascript',body:`window.signCalls=[];window.chooseCalls=0;window.deny=true;window.archipelagoNostr={selectIdentity:async()=>{window.chooseCalls++}};window.nostr={getPublicKey:async()=>'${'a'.repeat(64)}',signEvent:async e=>{window.signCalls.push(e);if(window.deny)throw new Error('User declined signing');return {...e,pubkey:'${'a'.repeat(64)}',id:'${'b'.repeat(64)}',sig:'${'c'.repeat(128)}'}}};`});
if(u.pathname==='/upload'){
uploads++; const body=route.request().postDataBuffer();
const token=JSON.parse(Buffer.from(route.request().headers().authorization.slice(6),'base64').toString());
const hash=createHash('sha256').update(body).digest('hex');
if(!token.tags.some(t=>t[0]==='x'&&t[1]===hash)||!token.tags.some(t=>t[0]==='server'&&t[1]==='127.0.0.1'))throw Error('Auth scope mismatch');
return route.fulfill({contentType:'application/json',body:JSON.stringify({sha256:hash,size:body.length})});
}
return route.continue();
});
await page.goto('http://127.0.0.1:48191/');
await page.waitForFunction(()=>typeof window.nostr==='object');
if(!await page.locator('#upload').isDisabled())throw Error('Upload enabled before consent');
await page.locator('#identity').click();
await page.waitForFunction(()=>document.querySelector('#pubkey').textContent==='a'.repeat(64));
await page.locator('#file').setInputFiles({name:'local-fixture.txt',mimeType:'text/plain',buffer:Buffer.from('Synthetic local file')});
await page.locator('#approve').check(); await page.locator('#upload').click();
await page.waitForFunction(()=>document.querySelector('#status').textContent.includes('User declined'));
if(uploads!==0)throw Error('Uploaded despite signing refusal');
await page.evaluate(()=>window.deny=false);
await page.locator('#upload').click();
await page.waitForFunction(()=>document.querySelector('#status').textContent.includes('Stored on this node'));
if(uploads!==1 || outgoing.length)throw Error('Unexpected upload or external request');
if(await page.locator('#approve').isChecked())throw Error('Approval was retained after upload');
if(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth))throw Error('Mobile horizontal overflow');
console.log('PASS packaged local UI: identity chooser, explicit consent, signer denial, scoped upload, consent reset, mobile width, no external requests (mock signer/transport; real signer still pending)');
await browser.close();
})().catch(e=>{console.error(e);process.exit(1)});