UNPKG

@bsv/authsocket

Version:

Mutually Authenticated Web Socket (Server-side)

183 lines 6.96 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.AuthSocket = exports.AuthSocketServer = void 0; const socket_io_1 = require("socket.io"); const sdk_1 = require("@bsv/sdk"); const SocketServerTransport_js_1 = require("./SocketServerTransport.js"); /** * A server-side wrapper for Socket.IO that integrates BRC-103 mutual authentication * to ensure secure, identity-aware communication between clients and the server. * * This class functions as a drop-in replacement for the `Server` class from Socket.IO, * with added support for: * - Automatic BRC-103 handshake for secure client authentication. * - Management of authenticated client sessions, avoiding redundant handshakes. * - Event-based communication through signed and verified BRC-103 messages. * * Features: * - Tracks client connections and their associated `Peer` and `AuthSocket` instances. * - Allows broadcasting messages to all authenticated clients. * - Provides a seamless API for developers by wrapping Socket.IO functionality. **/ class AuthSocketServer { /** * @param httpServer - The underlying HTTP server * @param options - Contains both standard Socket.IO server config and BRC-103 config. */ constructor(httpServer, options) { this.options = options; /** * Map from socket.id -> peer info * * Once we discover the identity key, we store `identityKey` * for that connection to skip re-handshaking. */ this.peers = new Map(); this.connectionCallbacks = []; this.realIo = new socket_io_1.Server(httpServer, options); // Listen for new connections this.realIo.on('connection', (socket) => { this.handleNewConnection(socket); }); } on(eventName, callback) { // We only override the 'connection' event. For other events, pass them through if (eventName === 'connection') { this.connectionCallbacks.push(callback); } else { this.realIo.on(eventName, callback); } } /** * Provide a classic pass-through to `io.emit(...)`. * * Under the hood, we sign a separate BRC-103 AuthMessage for each * authenticated peer. We'll embed eventName + data in the payload. */ emit(eventName, data) { this.peers.forEach(({ peer, authSocket, identityKey }) => { const payload = this.encodeEventPayload(eventName, data); peer.toPeer(payload, identityKey).catch(err => { // log or handle error console.error(err); }); }); } /** * If the developer needs direct access to the underlying raw Socket.IO server, * we can provide a getter. */ // public rawIo(): IoServer { // return this.realIo // } async handleNewConnection(socket) { const transport = new SocketServerTransport_js_1.SocketServerTransport(socket); // Create a new Peer for this client const peer = new sdk_1.Peer(this.options.wallet, transport, this.options.requestedCertificates, this.options.sessionManager); const authSocket = new AuthSocket(socket, peer, (sockId, identityKey) => { // Callback: once the AuthSocket learns identityKey from a 'general' message, store it const info = this.peers.get(sockId); if (info) { info.identityKey = identityKey; } }); this.peers.set(socket.id, { peer, authSocket, identityKey: undefined }); // Handle disconnection socket.on('disconnect', () => { this.peers.delete(socket.id); }); // Fire any onConnection callbacks this.connectionCallbacks.forEach(cb => cb(authSocket)); } encodeEventPayload(eventName, data) { const obj = { eventName, data }; return Array.from(Buffer.from(JSON.stringify(obj), 'utf8')); } } exports.AuthSocketServer = AuthSocketServer; /** * A wrapper around a real `IoSocket` used by a server that performs BRC-103 * signing and verification via the Peer class. */ class AuthSocket { constructor(ioSocket, peer, /** * A function the server passes in so we can * notify it once we discover the peer's identity key. */ onIdentityKeyDiscovered) { this.ioSocket = ioSocket; this.peer = peer; this.onIdentityKeyDiscovered = onIdentityKeyDiscovered; // We store event callbacks for re-dispatch this.eventCallbacks = new Map(); // Listen for 'general' messages from the Peer this.peer.listenForGeneralMessages((senderPublicKey, payload) => { // Capture the newly discovered identity key if not known yet if (!this.peerIdentityKey) { this.peerIdentityKey = senderPublicKey; this.onIdentityKeyDiscovered(this.ioSocket.id, senderPublicKey); } // The payload is a number[] representing JSON for { eventName, data } const { eventName, data } = this.decodeEventPayload(payload); const cbs = this.eventCallbacks.get(eventName); if (!cbs) return; for (const cb of cbs) { cb(data); } }); } /** * Register a callback for an event name, just like `socket.on(...)`. */ on(eventName, callback) { const arr = this.eventCallbacks.get(eventName) || []; arr.push(callback); this.eventCallbacks.set(eventName, arr); } /** * Emulate `socket.emit(eventName, data)`. * We'll sign a BRC-103 `general` message via Peer, * embedding the event name & data in the payload. * * If we do not yet have the peer's identity key (handshake not done?), * the Peer will attempt the handshake. Once known, subsequent calls * will pass identityKey to skip the initial handshake. */ async emit(eventName, data) { const encoded = this.encodeEventPayload(eventName, data); await this.peer.toPeer(encoded, this.peerIdentityKey); } /** * The Socket.IO 'id' */ get id() { return this.ioSocket.id; } /** * The client's identity key, if discovered */ get identityKey() { return this.peerIdentityKey; } ///////////////////////////// // Internal ///////////////////////////// encodeEventPayload(eventName, data) { const json = JSON.stringify({ eventName, data }); return Array.from(Buffer.from(json, 'utf8')); } decodeEventPayload(payload) { try { const str = Buffer.from(payload).toString('utf8'); return JSON.parse(str); } catch { return { eventName: '_unknown', data: null }; } } } exports.AuthSocket = AuthSocket; //# sourceMappingURL=AuthSocketServer.js.map