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>
72 lines
3.4 KiB
JavaScript
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;
|
|
}
|