@bsv/authsocket-client
Version:
Mutually Authenticated Web Sockets Client
128 lines (114 loc) • 4.04 kB
text/typescript
import { io as realIo, Socket as IoClientSocket, ManagerOptions, SocketOptions } from 'socket.io-client'
import { RequestedCertificateSet, SessionManager, Peer, WalletInterface, Utils } from '@bsv/sdk'
import { SocketClientTransport } from './SocketClientTransport.js'
/**
* Internal class that wraps a Socket.IO client connection with BRC-103 mutual authentication,
* enabling secure and identity-aware communication with a server.
*/
class AuthSocketClientImpl {
public connected = false
public id: string = ''
public serverIdentityKey: string | undefined
private eventCallbacks = new Map<string, Array<(data: any) => void>>()
/**
* Creates an instance of AuthSocketClient.
*
* @param ioSocket - The underlying Socket.IO client socket instance.
* @param peer - The BRC-103 Peer instance responsible for managing authenticated
* communication, including message signing and verification.
*/
constructor(
private ioSocket: IoClientSocket,
private peer: Peer
) {
// Listen for 'connect' and 'disconnect' from underlying Socket.IO
this.ioSocket.on('connect', () => {
this.connected = true
this.id = this.ioSocket.id || ''
// Re-dispatch to dev if they've called "socket.on('connect', ...)"
this.fireEventCallbacks('connect')
})
this.ioSocket.on('disconnect', (reason) => {
this.connected = false
// Re-dispatch
this.fireEventCallbacks('disconnect', reason)
})
// Also listen for BRC-103 "general" messages
// We'll rely on peer.listenForGeneralMessages
this.peer.listenForGeneralMessages((senderKey, payload) => {
this.serverIdentityKey = senderKey
const { eventName, data } = this.decodeEventPayload(payload)
this.fireEventCallbacks(eventName, data)
})
}
on(eventName: string, callback: (data?: any) => void): this {
let arr = this.eventCallbacks.get(eventName)
if (!arr) {
arr = []
this.eventCallbacks.set(eventName, arr)
}
arr.push(callback)
return this
}
emit(eventName: string, data: any): this {
// We sign a BRC-103 "general" message and send to the server
// via peer.toPeer
const encoded = this.encodeEventPayload(eventName, data)
this.peer.toPeer(encoded, this.serverIdentityKey).catch(err => {
console.error(`BRC103IoClientSocket emit error for event "${eventName}":`, err)
})
return this
}
disconnect(): void {
this.serverIdentityKey = undefined
this.ioSocket.disconnect()
}
private fireEventCallbacks(eventName: string, data?: any) {
const cbs = this.eventCallbacks.get(eventName)
if (!cbs) return
for (const cb of cbs) {
cb(data)
}
}
private encodeEventPayload(eventName: string, data: any): number[] {
const obj = { eventName, data }
return Utils.toArray(JSON.stringify(obj), 'utf8')
}
private decodeEventPayload(payload: number[]): { eventName: string, data: any } {
try {
const str = Utils.toUTF8(payload)
return JSON.parse(str)
} catch {
return { eventName: '_unknown', data: undefined }
}
}
}
/**
* Factory function for creating a new AuthSocketClientImpl instance.
*
* @param url - The server URL
* @param opts - Contains wallet, requested certificates, and other optional settings
*/
export function AuthSocketClient(
url: string,
opts: {
wallet: WalletInterface
requestedCertificates?: RequestedCertificateSet
sessionManager?: SessionManager
managerOptions?: Partial<ManagerOptions & SocketOptions>
}
): AuthSocketClientImpl {
// 1) Create real socket.io-client connection
const socket = realIo(url, opts.managerOptions)
// 2) Create a BRC-103 transport for the new socket
const transport = new SocketClientTransport(socket)
// 3) Create a Peer
const peer = new Peer(
opts.wallet,
transport,
opts.requestedCertificates,
opts.sessionManager
)
// 4) Return our new AuthSocketClientImpl
return new AuthSocketClientImpl(socket, peer)
}