@bsv/authsocket
Version:
Mutually Authenticated Web Socket (Server-side)
190 lines • 6.99 kB
JavaScript
import { Server as IoServer } from 'socket.io';
import { Peer } from '@bsv/sdk';
import { SocketServerTransport } from './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.
**/
export class AuthSocketServer {
options;
// The real Socket.IO server underneath
realIo;
/**
* Map from socket.id -> peer info
*
* Once we discover the identity key, we store `identityKey`
* for that connection to skip re-handshaking.
*/
peers = new Map();
connectionCallbacks = [];
/**
* @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;
this.realIo = new IoServer(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(socket);
// Create a new Peer for this client
const peer = new 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'));
}
}
/**
* A wrapper around a real `IoSocket` used by a server that performs BRC-103
* signing and verification via the Peer class.
*/
export class AuthSocket {
ioSocket;
peer;
onIdentityKeyDiscovered;
// We store event callbacks for re-dispatch
eventCallbacks = new Map();
/**
* Current known identity key of the server, if discovered
* (i.e. after the handshake yields a general message or
* or we've forced a getAuthenticatedSession).
*/
peerIdentityKey;
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;
// 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 };
}
}
}
//# sourceMappingURL=AuthSocketServer.js.map