SOURCE / PINNED RELEASE
Made of little things.
Black Site
- Release
- 666bc7c74ebd…
- Author-recorded commit
- 05e95af79676…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-d2225ddc391
Archive hash verified: 1544e072e245c4a8…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Two independent game sessions share only a controlled app-data relay, never local storage.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { createHash } from 'node:crypto';
const { chromium } = createRequire(import.meta.resolve('@napplet/conformance-cli/package.json'))('playwright');
const html = readFileSync(new URL('../dist/index.html',import.meta.url),'utf8');
const alice='a'.repeat(64), bob='b'.repeat(64), scope='c'.repeat(64);
const relay=new Map();let writes=0;
const browser=await chromium.launch({args:['--no-sandbox']});
const errors=[];
async function session() {
const page=await browser.newPage({viewport:{width:1100,height:900}});
page.on('pageerror',e=>errors.push(e.message));
await page.exposeFunction('fixtureQuery',filters=>({events:[...relay.values()].filter(event=>filters.some(f=>
f.kinds.includes(event.kind)&&(!f.authors||f.authors.includes(event.pubkey))&&(!f['#d']||f['#d'].includes(event.tags.find(t=>t[0]==='d')[1])))).map(event=>({event}))}));
await page.exposeFunction('fixturePublish',(template,author)=>{
const d=template.tags.find(t=>t[0]==='d')[1], key=author+':'+d, previous=relay.get(key);
assert.equal(template.kind,30078);assert.equal(template.tags.length,6);
assert.match(d,new RegExp('^soy.app-data/1:'+scope+':player-(armory|customization):(progress|choices)$'));
const body=JSON.parse(template.content);assert.equal(body.previous,previous?.id??null);
const event={...template,pubkey:author,id:createHash('sha256').update(JSON.stringify([author,template])).digest('hex'),sig:'0'.repeat(128)};
relay.set(key,event);writes++;
return {ok:true,event,relays:{'wss://storage.example':true}};
});
const fixture=`<script>
window.fixtureOwner='${alice}'; window.fixtureIncomplete=false; window.fixtureDenied=false;
const saved=new Map();
saved.set('blacksite.account.${alice}.profile',JSON.stringify({bank:30000,unlocked:[],armours:[],levels:{},armourLevels:{},armour:null}));
saved.set('blacksite.profile',JSON.stringify({bank:90000,unlocked:['smoke'],armours:[],levels:{},armourLevels:{},armour:null}));
window.napplet={
identity:{getPublicKey:async()=>window.fixtureOwner,onChanged:callback=>{window.fixtureChange=key=>{window.fixtureOwner=key;callback(key)};return{close(){}}}},
common:{getProfile:async key=>({ok:true,pubkey:key,profile:{name:key==='${alice}'?'Alice Player':'Bob Player'}})},
storage:{getItem:async key=>saved.get(key)??null,setItem:async(key,value)=>saved.set(key,value)},
shell:{onReady:callback=>{callback({capabilities:{appData:{profile:'soy.app-data/1',scope:'${scope}',maxContentBytes:16384,relays:['wss://storage.example'],defaultRelays:['wss://storage.example']}}});return{close(){}}}},
outbox:{query:async filters=>({...await window.fixtureQuery(filters),incomplete:window.fixtureIncomplete}),
publish:async template=>window.fixtureDenied?{ok:false,error:'consent-denied'}:window.fixturePublish(template,window.fixtureOwner)}
};
</script>`;
await page.setContent('<iframe sandbox="allow-scripts" style="position:fixed;inset:0;border:0;width:100%;height:100%"></iframe>');
await page.locator('iframe').evaluate((frame,source)=>frame.srcdoc=source,html.replace('<head>','<head>'+fixture));
const frame=await (await page.locator('iframe').elementHandle()).contentFrame();
await frame.locator('#menu.on').waitFor();
await frame.waitForFunction(()=>document.getElementById('bank').textContent==='FUNDS $30,000');
await frame.locator('#currentUser').click();
await frame.locator('#playerAccount [data-sync-group=armory] [data-sync=enable]').waitFor({state:'visible'});
await frame.locator('#playerAccount [data-sync-group=customization] [data-sync=enable]').waitFor({state:'visible'});
return {page,frame};
}
const record=(owner,collection,id)=>relay.get(owner+':soy.app-data/1:'+scope+':'+collection+':'+id);
const state=(frame,group)=>frame.locator(`#playerAccount [data-sync-group=${group}] p[role=status]`);
const action=(frame,group,id)=>frame.locator(`#playerAccount [data-sync-group=${group}] [data-sync=${id}]`);
const back=async(frame,id)=>frame.locator(`#${id} [data-go=menu]`).first().click();
async function save(frame,group){await action(frame,group,'save').click();await frame.waitForFunction(group=>document.querySelector(`#playerAccount [data-sync-group=${group}] p[role=status]`).textContent.includes('Saved to account')||document.querySelector(`#playerAccount [data-sync-group=${group}] p[role=status]`).textContent.includes('Choices saved to account'),group);}
try {
const first=await session(),f=first.frame;
assert.equal(writes,0,'restoring does not upload legacy saves');
await action(f,'armory','enable').click();await action(f,'customization','enable').click();
await action(f,'armory','import').click();
await back(f,'playerAccount');await f.locator('#menu [data-go=shop]').click();
await f.locator('[data-unlock=frag]').click();await f.locator('[data-armour=soft]').click();await f.locator('[data-up=m4a1]').click();
await back(f,'shop');await f.locator('#menu [data-go=character]').click();await f.locator('[data-char=sniper]').click();await back(f,'character');
await f.locator('#menu [data-go=loadout]').click();await f.locator('[data-kind=primary][data-id=ak47]').click();await f.locator('[data-kind=secondary][data-id=deagle]').click();await f.locator('[data-kind=throwable][data-id=smoke]').click();await back(f,'loadout');
await f.locator('#menu [data-go=maps]').click();await f.locator('[data-map=docks]').click();await back(f,'maps');await f.locator('#diffSlider').fill('2');
await f.locator('#currentUser').click();await save(f,'armory');await save(f,'customization');
assert.equal(relay.size,2,'armory and choices use independent application addresses');
const armory=JSON.parse(record(alice,'player-armory','progress').content).data;
assert.deepEqual(armory.unlockedThrowables,['frag','smoke']);assert.equal(armory.weaponLevels.m4a1,1);assert.equal(armory.ownedArmors[0],'soft');assert.equal(armory.bank,undefined);
const choices=JSON.parse(record(alice,'player-customization','choices').content).data;
assert.equal(choices.character,'sniper');assert.equal(choices.loadout.primary,'ak47');assert.equal(choices.loadout.throwable,'smoke');assert.equal(choices.equippedArmor,'soft');assert.equal(choices.map,'docks');assert.equal(choices.difficulty,'easy');
const second=await session(),s=second.frame;
await back(s,'playerAccount');assert.match(await s.locator('#menuChar').innerText(),/Sniper/i);assert.match(await s.locator('#menuLoadout').innerText(),/AK-47.*Desert Eagle.*Smoke/);
assert.equal(await s.locator('#bank').innerText(),'FUNDS $30,000','remote imports never deduct or restore funds');
await s.locator('#menu [data-go=shop]').click();assert.match(await s.locator('[data-armour=soft]').innerText(),/EQUIPPED - CLICK TO REMOVE/);await back(s,'shop');
await s.locator('#currentUser').click();await action(s,'customization','enable').click();await back(s,'playerAccount');await s.locator('#diffSlider').fill('0');await s.locator('#currentUser').click();await save(s,'customization');
await action(f,'customization','load').click();await f.waitForFunction(()=>document.querySelector('#diffName').textContent==='HARD');
// A denied save, partial read, unsupported revision and tombstone must keep local purchases.
await back(f,'playerAccount');await f.locator('#menu [data-go=shop]').click();await f.locator('[data-up=m4a1]').click();await back(f,'shop');await f.locator('#currentUser').click();
const count=writes;
await f.evaluate(()=>window.fixtureDenied=true);await action(f,'armory','save').click();
await f.waitForFunction(()=>document.querySelector('#playerAccount [data-sync-group=armory] p[role=status]').textContent.includes('not approved'));
assert.equal(writes,count);await f.evaluate(()=>{window.fixtureDenied=false;window.fixtureIncomplete=true});await action(f,'armory','load').click();await f.waitForFunction(()=>document.querySelector('#playerAccount [data-sync-group=armory] p[role=status]').textContent.includes('failed'));
assert.equal(writes,count);await action(f,'armory','save').click();
await f.waitForFunction(()=>document.querySelector('#playerAccount [data-sync-group=armory] p[role=status]').textContent.includes('failed'));assert.equal(writes,count);
await f.evaluate(()=>window.fixtureIncomplete=false);
const originalArmory=record(alice,'player-armory','progress');
const address=alice+':'+originalArmory.tags.find(t=>t[0]==='d')[1];
const incompatible=JSON.parse(originalArmory.content);incompatible.version=2;
relay.set(address,{...originalArmory,tags:originalArmory.tags.map(t=>t[0]==='v'?['v','2']:t),content:JSON.stringify(incompatible)});
await action(f,'armory','save').click();await f.waitForFunction(()=>document.querySelector('#playerAccount [data-sync-group=armory] p[role=status]').textContent.includes('cannot be loaded'));assert.equal(writes,count);
const tombstone=JSON.parse(originalArmory.content);tombstone.previous=originalArmory.id;tombstone.deleted=true;tombstone.data=null;
relay.set(address,{...originalArmory,content:JSON.stringify(tombstone)});
await action(f,'armory','save').click();await f.waitForFunction(()=>document.querySelector('#playerAccount [data-sync-group=armory] p[role=status]').textContent.includes('cannot be loaded'));assert.equal(writes,count);
relay.set(address,originalArmory);await save(f,'armory');
if(process.env.SYNC_SCREENSHOT) await first.page.locator('iframe').screenshot({path:process.env.SYNC_SCREENSHOT});
// Switch accounts during play: the mission remains owned by A until returning to menu.
await back(f,'playerAccount');await f.locator('#menu [data-go=deploy]').click();const original=await f.evaluate(()=>globalThis.BLACKSITE_STORE.get('blacksite.profile',{}).bank);
await f.evaluate(key=>window.fixtureChange(key),bob);await f.waitForFunction(()=>document.querySelector('.player-label').textContent==='Bob Player');
assert.equal(await f.evaluate(()=>globalThis.BLACKSITE_STORE.get('blacksite.profile',{}).bank),original);
await first.page.keyboard.press('Escape');await f.locator('#pauseQuit').click();await f.waitForFunction(()=>document.getElementById('bank').textContent==='FUNDS $0');
assert.deepEqual(await f.evaluate(()=>globalThis.BLACKSITE_STORE.get('blacksite.profile',{}).unlocked??[]),[]);
assert.equal(record(bob,'player-armory','progress'),undefined);
assert.deepEqual(errors,[]);
console.log('PASS account sync UI: independent device restore, two records, actual game choices, legacy import, denied saves, incomplete reads, incompatible revisions, tombstones, mission account isolation');
} finally {await browser.close();}
