zebra-report: deploy 72d5208 — log full pubkey on every role/spot event

This commit is contained in:
russell@unturf.com 2026-06-02 15:06:47 -04:00
parent 9f89786b87
commit c4e37d3cae
No known key found for this signature in database

View file

@ -722,6 +722,31 @@ function logLine(kind, msg){
logEl.appendChild(d); logEl.scrollTop=logEl.scrollHeight;
}
/* pubHex straight from a server message — prefers the explicit hex field,
* falls back to decoding the b64 form. Used so role/spot/boot/etc. log lines
* carry the actor's authoritative key (the server already verified the
* signature), independent of the local uuid→handle map. A spoof attempt
* is then visible in the log retroactively: same handle, different pubkey. */
function pubHexFromMsg(m, b64key, hexkey){
if (m && hexkey && m[hexkey]) return m[hexkey];
if (m && b64key && m[b64key]){
try {
const bin = atob(m[b64key].replace(/-/g,'+').replace(/_/g,'/'));
let h=''; for (let i=0;i<bin.length;i++) h += bin.charCodeAt(i).toString(16).padStart(2,'0');
return h;
} catch(_){}
}
return '';
}
/* "handle pubhex" or just "pubhex" if we don't have the member yet. Full
* hex — never truncated. */
function idTag(uuidOrHandle, pubHex){
const mm = (uuidOrHandle && members.get) ? members.get(uuidOrHandle) : null;
const handle = mm ? mm.handle : (uuidOrHandle || '');
if (!pubHex) return handle;
return handle ? (handle+' '+pubHex) : pubHex;
}
/* ==================================================================
* state-machine framework — pure transitions, testable in isolation.
*
@ -1639,11 +1664,15 @@ function broadcastSpotlight(){
if (typeof inRoleTransition !== 'undefined' && inRoleTransition) return;
try { send({ type: 'spotlight', key: spotlightKey() }); } catch(_){}
}
function logSpotlightChange(uuid, key){
const handle = (members.get(uuid) || {}).handle || shortHex(uuid);
if (!key){ logLine('', handle + ' looked away'); return; }
function logSpotlightChange(uuid, key, viewerPubHex){
/* viewerPubHex is the authoritative pubkey from the server's spotlight
* broadcast; for self-spotlight calls we fall back to my own key. */
const mm = members.get(uuid);
const pub = viewerPubHex || (uuid === myUUID && myKeys ? myKeys.pubHex : (mm && mm.pubkey ? pubHexFromMsg({pubkey:mm.pubkey},'pubkey') : ''));
const who = idTag(uuid, pub);
if (!key){ logLine('', who + ' looked away'); return; }
const [kind, pubHex] = key.split(':');
logLine('', handle + ' now viewing ' + ownerLabel(kind, pubHex));
logLine('', who + ' now viewing ' + ownerLabel(kind, pubHex) + (pubHex?' ['+pubHex+']':''));
}
function updateContainerVisibility(){
@ -2702,7 +2731,7 @@ async function handleSignal(raw){
roomEpoch = m.epoch; applyState(m.state); flushSfuStreams(); renderRoom(); break;
case 'peer-joined':
members.set(m.uuid, { uuid:m.uuid, pubkey:m.pubkey, handle:m.handle, role:m.role, joined_at: Date.now()/1000 });
logLine('', m.handle+' joined as '+m.role);
{ const pub = pubHexFromMsg(m, 'pubkey'); logLine('', idTag(m.uuid, pub)+' joined as '+m.role); }
/* if the original host rejoined during their grace window, the room
* is rescued — drop the "space closing" status from our top bar */
if (m.role === 'host'){
@ -2721,7 +2750,8 @@ async function handleSignal(raw){
case 'peer-left':
{
const left = members.get(m.uuid);
if (left) logLine('', left.handle+' left');
const pub = pubHexFromMsg(m, 'pubkey') || (left && left.pubkey ? pubHexFromMsg({pubkey:left.pubkey}, 'pubkey') : '');
if (left || pub) logLine('', idTag(m.uuid, pub)+' left');
/* if they were sharing screen or camera, drop the tiles */
if (left && left.pubkey){
try { const h = hex(unb64(left.pubkey)); removeScreenTile(h); removeCameraTile(h); } catch(_){}
@ -2750,32 +2780,36 @@ async function handleSignal(raw){
const next = m.key || '';
if (prev === next) break;
spotlights.set(m.uuid, next);
if (m.uuid !== myUUID) logSpotlightChange(m.uuid, next);
if (m.uuid !== myUUID) logSpotlightChange(m.uuid, next, pubHexFromMsg(m,'pubkey'));
reorderTiles();
}
break;
case 'hand-raised':
handraise.add(m.uuid); renderRoom();
{ const mm = members.get(m.uuid); if (mm) logLine('', mm.handle+' raised hand'); }
{ const pub = pubHexFromMsg(m, 'pubkey'); logLine('', idTag(m.uuid, pub)+' raised hand'); }
break;
case 'hand-lowered':
handraise.delete(m.uuid); renderRoom(); break;
handraise.delete(m.uuid); renderRoom();
{ const pub = pubHexFromMsg(m, 'pubkey'); logLine('', idTag(m.uuid, pub)+' lowered hand'); }
break;
case 'mic-invite':
if (m.to === myUUID){
outstandingInvite = { from:m.from, epoch:m.epoch };
const from = members.get(m.from);
$('invite-from').textContent = 'from '+(from?from.handle:shortHex(m.from));
$('invite-from').textContent = 'from '+(from?from.handle:m.from);
$('sec-invite').classList.remove('hidden');
logLine('', 'you were invited to the mic by '+(from?from.handle:m.from));
const fromPub = pubHexFromMsg(m, 'from_pubkey');
logLine('', 'you were invited to the mic by '+idTag(m.from, fromPub));
} else {
const from = members.get(m.from), to = members.get(m.to);
if (from && to) logLine('', from.handle+' invited '+to.handle+' to the mic');
const fromPub = pubHexFromMsg(m, 'from_pubkey');
const toPub = pubHexFromMsg(m, 'to_pubkey');
logLine('', idTag(m.from, fromPub)+' invited '+idTag(m.to, toPub)+' to the mic');
}
break;
case 'mic-invite-declined':
if (m.from === myUUID){
const to = members.get(m.to);
logLine('', (to?to.handle:m.to)+' declined the mic');
const toPub = pubHexFromMsg(m, 'to_pubkey');
logLine('', idTag(m.to, toPub)+' declined the mic');
}
break;
case 'role-change':
@ -2786,10 +2820,12 @@ async function handleSignal(raw){
mm.role = m.role;
roomEpoch = m.epoch;
if (m.role === 'host') hostUUID = m.uuid;
const subjPub = pubHexFromMsg(m, 'pubkey', 'pubkey_hex') || myKeys && (m.uuid===myUUID ? myKeys.pubHex : '');
const byPub = pubHexFromMsg(m, 'by_pubkey');
if (m.uuid === myUUID){
myRole = m.role;
roomMachines.call.send('ROLE_CHANGE', { role: m.role });
logLine('', 'you are now '+m.role);
logLine('', 'you are now '+m.role+(subjPub?' ['+subjPub+']':'')+(m.by?' by '+idTag(m.by, byPub):''));
setStatus('connected as '+myRole, 'ok');
const byMod = members.get(m.by);
const byTxt = byMod ? ' by '+byMod.handle : '';
@ -2801,7 +2837,7 @@ async function handleSignal(raw){
else if (m.role === 'host') showNotice('You are now the host.', 'info');
onRoleChanged(prev, m.role);
} else {
logLine('', mm.handle+' is now '+m.role);
logLine('', idTag(m.uuid, subjPub)+' is now '+m.role+(m.by?' by '+idTag(m.by, byPub):''));
/* mesh adjustments — fox: 'never drop people out of the mesh
* automatically'. Promote: connect if we can speak + they
* can speak + we don't already have them. Demote: do NOT
@ -2820,7 +2856,9 @@ async function handleSignal(raw){
{
const mm = members.get(m.uuid);
const by = members.get(m.by);
if (mm) logLine('', mm.handle+' was removed by '+(by?by.handle:'a mod'));
const victPub = pubHexFromMsg(m, 'pubkey');
const byPub = pubHexFromMsg(m, 'by_pubkey');
logLine('', idTag(m.uuid, victPub)+' was removed by '+(by||byPub?idTag(m.by, byPub):'a mod'));
if (m.uuid === myUUID){
/* server will close our socket; surface a clear notice and prevent
* the WS reconnect loop from auto-rejoining into a boot loop. Tear
@ -2842,17 +2880,18 @@ async function handleSignal(raw){
}
break;
case 'host-left':
logLine('', 'host left the space');
{ const pub = pubHexFromMsg(m, 'host_pubkey'); logLine('', 'host left the space — '+idTag(m.host_uuid, pub)); }
hostUUID = '';
break;
case 'host-promoted':
roomEpoch = m.epoch;
hostUUID = m.new_host_uuid;
{ const mm = members.get(m.new_host_uuid);
const pub = pubHexFromMsg(m, 'new_host_pubkey');
if (mm){ mm.role = 'host';
if (m.new_host_uuid === myUUID){ myRole = 'host'; setStatus('connected as host','ok'); onRoleChanged('cohost','host'); }
else { setStatus('connected as '+myRole, 'ok'); } /* clears the space-closing warning */
logLine('', (mm.handle||m.new_host_uuid)+' is now host'); }
logLine('', idTag(m.new_host_uuid, pub)+' is now host'); }
flushSfuStreams();
renderRoom();
reorderTiles();
@ -3547,8 +3586,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-02</span><br>
md5 <span class="stamp-md5">f908c19e47433151020f76df10fe81d0</span><br>
sha256 <span class="stamp-sha">f751785bdcb457d172dee013fbe0a445ee01b0aacdb20c2c1fe11352ef98111c</span><br>
md5 <span class="stamp-md5">c9a9641f134b920871292cd17598a637</span><br>
sha256 <span class="stamp-sha">fb1aa6ee1857cf33e50985473b98ad9648850abf82289b771c3ab2295db707b6</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>