Back to Black Site
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.

tests/account-sync-ui.mjs
// 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();}