deviant/scripts/t6-relay.mjs
walerikus 4c165e83e4 feat(t6-mvp): Phase 4b — self-custodial wallet agents + keyless relay (chipnet)
Deployed two-party shape of the Phase-4a T6Wallet protocol: a keyless WS relay
(t6-relay.mjs) + per-user agents (t6-wallet-agent.mjs, one key each) + a thin
per-role UI (design/t6-wallet/agent.html). No component holds both keys; the
relay holds none. Supersedes the server-custodial t6-wallet-server.mjs.

Validated on chipnet (two separate agent processes, real coins):
  open   cad8647c (50000/50000) · channel 68e4b36e
  A→B 8000  b8cbe855 (42000/58000)
  B→A 3000  2a3571c7 (45000/55000)
Both agents converge on the same on-chain state; pubkeys via hello handshake,
channelId adopted from the relay open announce. Spec §3.5/§4/§5/§7 + snapshot 0188.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-03 00:11:30 +02:00

72 lines
3.4 KiB
JavaScript

#!/usr/bin/env node
/**
* t6-relay.mjs — the T6 MVP Phase 4b KEYLESS relay.
*
* A dumb WebSocket message bus for two self-custodial T6 wallet agents. It holds NO keys and NO
* funds and never touches the chain: it only forwards small JSON messages between the two peers in
* a "room" — a `hello` (role + PUBLIC key), an `open` announce (channelId after genesis), a payment
* `proposal`, and a 64-byte state `sig`. Everything that crosses this relay is public data; the
* private keys stay in each agent. This is the deployed-shape stand-in for what would be a WebRTC
* or QR transport: the security model does not depend on trusting it (a malicious relay can drop or
* reorder messages — a liveness nuisance — but can never move funds or forge a board update, because
* it has no key and the board enforces monotonicity + both-party checkDataSig on-chain).
*
* Rooms: a client connects with `?room=<name>&role=<alice|bob>`. The relay forwards each message to
* the OTHER member of the same room. Content is opaque to the relay (it inspects nothing but room).
*
* Run: node scripts/t6-relay.mjs (default port 5188; no sandbox/network-to-chain needed)
*/
import { WebSocketServer } from 'ws';
import { URL } from 'node:url';
const PORT = Number((process.argv.find((a) => a.startsWith('--port=')) || '').split('=')[1] || 5188);
// room name -> { alice?: ws, bob?: ws }
const rooms = new Map();
const now = () => new Date().toISOString().slice(11, 19);
const log = (...a) => console.log(now(), ...a);
const wss = new WebSocketServer({ port: PORT });
log(`T6 keyless relay listening on ws://localhost:${PORT} (holds no keys, forwards only)`);
wss.on('connection', (ws, req) => {
let room, role;
try {
const u = new URL(req.url, `http://localhost:${PORT}`);
room = u.searchParams.get('room');
role = u.searchParams.get('role');
} catch { /* fallthrough */ }
if (!room || (role !== 'alice' && role !== 'bob')) {
ws.close(1008, 'need ?room=<name>&role=<alice|bob>');
return;
}
const slot = rooms.get(room) || {};
if (slot[role] && slot[role].readyState === 1) { ws.close(1013, `role ${role} already present in ${room}`); return; }
slot[role] = ws; rooms.set(room, slot);
ws.roomName = room; ws.role = role;
log(`+ ${role} joined room "${room}" (members: ${Object.keys(slot).filter((k) => slot[k]?.readyState === 1).join('+') || 'none'})`);
ws.send(JSON.stringify({ type: 'relay', ok: true, room, role, peerPresent: !!peerOf(room, role) }));
// tell the freshly-arrived peer's counterpart that someone showed up, so it can re-`hello`
const peer = peerOf(room, role);
if (peer) peer.send(JSON.stringify({ type: 'relay', peerJoined: role }));
ws.on('message', (data) => {
// opaque forward to the other member — the relay never parses the payload beyond routing
const peer = peerOf(room, role);
if (!peer || peer.readyState !== 1) return; // peer offline: drop (agents re-sync from the board)
peer.send(data.toString());
});
ws.on('close', () => {
const s = rooms.get(room);
if (s && s[role] === ws) { delete s[role]; if (!s.alice && !s.bob) rooms.delete(room); }
log(`- ${role} left room "${room}"`);
const p = peerOf(room, role);
if (p) p.send(JSON.stringify({ type: 'relay', peerLeft: role }));
});
});
function peerOf(room, role) {
const s = rooms.get(room); if (!s) return null;
const other = role === 'alice' ? s.bob : s.alice;
return other && other.readyState === 1 ? other : null;
}