{"version":3,"file":"AuthSocketServer.cjs","names":["IoServer","SocketServerTransport","Peer"],"sources":["../../src/AuthSocketServer.ts"],"sourcesContent":["import { Server as HttpServer } from 'node:http'\nimport { ServerOptions, Server as IoServer, Socket as IoSocket } from 'socket.io'\nimport {\n  WalletInterface,\n  Peer,\n  SessionManager,\n  AsyncSessionManager,\n  stringifyBRC100\n} from '@bsv/sdk'\nimport { SocketServerTransport } from './SocketServerTransport.js'\n\nexport type AuthSocketErrorPhase = 'authentication' | 'application' | 'connection' | 'send'\n\nexport interface AuthSocketErrorContext {\n  phase: AuthSocketErrorPhase\n  socketId?: string\n  eventName?: string\n}\n\nexport type AuthSocketErrorHandler = (\n  error: unknown,\n  context: AuthSocketErrorContext\n) => void | Promise<void>\n\nexport function decodeAuthSocketEventPayload(payload: number[]): { eventName: string; data: any } {\n  try {\n    const str = Buffer.from(payload).toString('utf8')\n    const decoded: unknown = JSON.parse(str)\n    if (\n      decoded === null ||\n      typeof decoded !== 'object' ||\n      Array.isArray(decoded) ||\n      typeof (decoded as { eventName?: unknown }).eventName !== 'string'\n    ) {\n      return { eventName: '_unknown', data: null }\n    }\n    return {\n      eventName: (decoded as { eventName: string }).eventName,\n      data: (decoded as { data?: unknown }).data\n    }\n  } catch {\n    return { eventName: '_unknown', data: null }\n  }\n}\n\nexport interface AuthSocketServerOptions extends Partial<ServerOptions> {\n  wallet: WalletInterface // The server's wallet for signing\n  requestedCertificates?: any // e.g. RequestedCertificateSet\n  /**\n   * Optional shared BRC-103 session store. Use an AsyncSessionManager backed by\n   * a shared database when more than one server replica handles connections.\n   */\n  sessionManager?: SessionManager | AsyncSessionManager\n  /** Maximum authentication messages processed concurrently by each socket. Defaults to 32. */\n  maxPendingAuthMessages?: number\n  /** Receives contained transport and application errors without exposing remote payloads. */\n  onError?: AuthSocketErrorHandler\n}\n\ninterface PeerInfo {\n  peer: Peer\n  authSocket: AuthSocket\n  identityKey?: string\n}\n\n/**\n * A server-side wrapper for Socket.IO that integrates BRC-103 mutual authentication\n * to ensure secure, identity-aware communication between clients and the server.\n *\n * This class functions as a drop-in replacement for the `Server` class from Socket.IO,\n * with added support for:\n * - Automatic BRC-103 handshake for secure client authentication.\n * - Management of authenticated client sessions, avoiding redundant handshakes.\n * - Event-based communication through signed and verified BRC-103 messages.\n *\n * Features:\n * - Tracks client connections and their associated `Peer` and `AuthSocket` instances.\n * - Allows broadcasting messages to all authenticated clients.\n * - Provides a seamless API for developers by wrapping Socket.IO functionality.\n **/\nexport class AuthSocketServer {\n  // The real Socket.IO server underneath\n  private readonly realIo: IoServer\n\n  /**\n   * Map from socket.id -> peer info\n   *\n   * Once we discover the identity key, we store `identityKey`\n   * for that connection to skip re-handshaking.\n   */\n  private readonly peers = new Map<string, PeerInfo>()\n  private readonly connectionCallbacks: Array<(socket: AuthSocket) => void | Promise<void>> = []\n  private closePromise?: Promise<void>\n\n  /**\n   * @param httpServer - The underlying HTTP server\n   * @param options - Contains both standard Socket.IO server config and BRC-103 config.\n   */\n  constructor(\n    httpServer: HttpServer,\n    private readonly options: AuthSocketServerOptions\n  ) {\n    const {\n      wallet: _wallet,\n      requestedCertificates: _requestedCertificates,\n      sessionManager: _sessionManager,\n      maxPendingAuthMessages: _maxPendingAuthMessages,\n      onError: _onError,\n      ...serverOptions\n    } = options\n    this.realIo = new IoServer(httpServer, serverOptions)\n\n    // Listen for new connections\n    this.realIo.on('connection', (socket: IoSocket) => {\n      try {\n        this.handleNewConnection(socket)\n      } catch (error) {\n        this.reportError(error, { phase: 'connection', socketId: socket.id })\n        this.disconnectSafely(socket)\n      }\n    })\n  }\n\n  /**\n   * A direct pass-through to `io.on('connection', cb)`,\n   * but the callback is invoked with an AuthSocket instead.\n   */\n  public on(eventName: 'connection', callback: (socket: AuthSocket) => void | Promise<void>): void\n  public on(eventName: string, callback: (data: any) => void | Promise<void>): void\n  public on(eventName: string, callback: (data: any) => void | Promise<void>): void {\n    // We only override the 'connection' event. For other events, pass them through\n    if (eventName === 'connection') {\n      this.connectionCallbacks.push(callback as (socket: AuthSocket) => void | Promise<void>)\n    } else {\n      this.realIo.on(eventName, callback)\n    }\n  }\n\n  /**\n   * Provide a classic pass-through to `io.emit(...)`.\n   *\n   * Under the hood, we sign a separate BRC-103 AuthMessage for each\n   * authenticated peer. We'll embed eventName + data in the payload.\n   */\n  public emit(eventName: string, data: any) {\n    let payload: number[]\n    try {\n      payload = this.encodeEventPayload(eventName, data)\n    } catch (error) {\n      this.reportError(error, { phase: 'send', eventName })\n      return\n    }\n    this.peers.forEach(({ peer, identityKey }) => {\n      peer.toPeer(payload, identityKey).catch(err => {\n        this.reportError(err, { phase: 'send', eventName })\n      })\n    })\n  }\n\n  /**\n   * Emit only to connections whose cryptographically authenticated peer\n   * identity matches the requested identity key.\n   *\n   * This is safer than application-level \"room\" names for private delivery:\n   * a client cannot subscribe itself to another identity because the routing\n   * decision uses the key discovered by the BRC-103 handshake.\n   *\n   * @returns the number of authenticated connections selected for delivery\n   */\n  public emitToIdentity(identityKey: string, eventName: string, data: any): number {\n    let selected = 0\n    let payload: number[]\n    try {\n      payload = this.encodeEventPayload(eventName, data)\n    } catch (error) {\n      this.reportError(error, { phase: 'send', eventName })\n      return selected\n    }\n    this.peers.forEach(({ peer, identityKey: authenticatedIdentityKey }) => {\n      if (authenticatedIdentityKey !== identityKey) return\n      selected += 1\n      peer.toPeer(payload, authenticatedIdentityKey).catch(err => {\n        this.reportError(err, { phase: 'send', eventName })\n      })\n    })\n    return selected\n  }\n\n  /**\n   * Stops accepting connections, disconnects active sockets, and closes the\n   * attached HTTP server. Repeated calls share the same shutdown operation.\n   */\n  public close(): Promise<void> {\n    this.closePromise ??= this.realIo.close().then(() => {\n      this.peers.clear()\n      this.connectionCallbacks.length = 0\n    })\n    return this.closePromise\n  }\n\n  /**\n   * If the developer needs direct access to the underlying raw Socket.IO server,\n   * we can provide a getter.\n   */\n  // public rawIo(): IoServer {\n  //   return this.realIo\n  // }\n\n  private handleNewConnection(socket: IoSocket): void {\n    const transport = new SocketServerTransport(socket, {\n      maxPendingMessages: this.options.maxPendingAuthMessages,\n      onError: error => {\n        this.reportError(error, { phase: 'authentication', socketId: socket.id })\n      }\n    })\n\n    // Create a new Peer for this client\n    const peer = new Peer(\n      this.options.wallet,\n      transport,\n      this.options.requestedCertificates,\n      this.options.sessionManager\n    )\n\n    const authSocket = new AuthSocket(\n      socket,\n      peer,\n      (sockId, identityKey) => {\n        // Callback: once the AuthSocket learns identityKey from a 'general' message, store it\n        const info = this.peers.get(sockId)\n        if (info) {\n          info.identityKey = identityKey\n        }\n      },\n      (error, context) => {\n        this.reportError(error, context)\n      }\n    )\n\n    this.peers.set(socket.id, { peer, authSocket, identityKey: undefined })\n\n    // Handle disconnection\n    socket.on('disconnect', () => {\n      this.peers.delete(socket.id)\n    })\n\n    // Fire any onConnection callbacks\n    void (async () => {\n      for (const callback of this.connectionCallbacks) {\n        await callback(authSocket)\n      }\n    })().catch(error => {\n      this.reportError(error, { phase: 'connection', socketId: socket.id })\n      this.disconnectSafely(socket)\n    })\n  }\n\n  private encodeEventPayload(eventName: string, data: any): number[] {\n    const obj = { eventName, data }\n    return Array.from(Buffer.from(stringifyBRC100(obj), 'utf8'))\n  }\n\n  private reportError(error: unknown, context: AuthSocketErrorContext): void {\n    void Promise.resolve()\n      .then(async () => await this.options.onError?.(error, context))\n      .catch(() => {})\n  }\n\n  private disconnectSafely(socket: IoSocket): void {\n    try {\n      socket.disconnect(true)\n    } catch {\n      // The original failure is already contained and reported.\n    }\n  }\n}\n\n/**\n * A wrapper around a real `IoSocket` used by a server that performs BRC-103\n * signing and verification via the Peer class.\n */\nexport class AuthSocket {\n  // We store event callbacks for re-dispatch\n  private readonly eventCallbacks: Map<string, Array<(data: any) => void | Promise<void>>> =\n    new Map()\n\n  /**\n   * Current known identity key of the server, if discovered\n   * (i.e. after the handshake yields a general message or\n   * or we've forced a getAuthenticatedSession).\n   */\n  private peerIdentityKey?: string\n\n  constructor(\n    public readonly ioSocket: IoSocket,\n    private readonly peer: Peer,\n    /**\n     * A function the server passes in so we can\n     * notify it once we discover the peer's identity key.\n     */\n    private readonly onIdentityKeyDiscovered: (socketId: string, identityKey: string) => void,\n    private readonly onError: AuthSocketErrorHandler = () => {}\n  ) {\n    // Listen for 'general' messages from the Peer\n    this.peer.listenForGeneralMessages(async (senderPublicKey, payload) => {\n      let eventName: string | undefined\n      try {\n        // Capture the newly discovered identity key if not known yet\n        if (!this.peerIdentityKey) {\n          this.peerIdentityKey = senderPublicKey\n          this.onIdentityKeyDiscovered(this.ioSocket.id, senderPublicKey)\n        }\n\n        // The payload is a number[] representing JSON for { eventName, data }\n        const decoded = this.decodeEventPayload(payload)\n        eventName = decoded.eventName\n        const cbs = this.eventCallbacks.get(eventName)\n        if (!cbs) return\n        for (const cb of cbs) {\n          const result = cb(decoded.data)\n          if (result != null && typeof (result as PromiseLike<void>).then === 'function') {\n            await result\n          }\n        }\n      } catch (error) {\n        this.reportError(error, { phase: 'application', socketId: this.id, eventName })\n        this.disconnectSafely()\n      }\n    })\n  }\n\n  /**\n   * Register a callback for an event name, just like `socket.on(...)`.\n   */\n  public on(eventName: string, callback: (data: any) => void | Promise<void>) {\n    const arr = this.eventCallbacks.get(eventName) || []\n    arr.push(callback)\n    this.eventCallbacks.set(eventName, arr)\n  }\n\n  /**\n   * Emulate `socket.emit(eventName, data)`.\n   * We'll sign a BRC-103 `general` message via Peer,\n   * embedding the event name & data in the payload.\n   *\n   * If we do not yet have the peer's identity key (handshake not done?),\n   * the Peer will attempt the handshake. Once known, subsequent calls\n   * will pass identityKey to skip the initial handshake.\n   */\n  public async emit(eventName: string, data: any): Promise<void> {\n    const encoded = this.encodeEventPayload(eventName, data)\n    await this.peer.toPeer(encoded, this.peerIdentityKey)\n  }\n\n  /**\n   * The Socket.IO 'id'\n   */\n  get id(): string {\n    return this.ioSocket.id\n  }\n\n  /**\n   * The client's identity key, if discovered\n   */\n  get identityKey(): string | undefined {\n    return this.peerIdentityKey\n  }\n\n  /////////////////////////////\n  // Internal\n  /////////////////////////////\n\n  private encodeEventPayload(eventName: string, data: any): number[] {\n    const json = stringifyBRC100({ eventName, data })\n    return Array.from(Buffer.from(json, 'utf8'))\n  }\n\n  private decodeEventPayload(payload: number[]): { eventName: string; data: any } {\n    return decodeAuthSocketEventPayload(payload)\n  }\n\n  private reportError(error: unknown, context: AuthSocketErrorContext): void {\n    void Promise.resolve()\n      .then(async () => await this.onError(error, context))\n      .catch(() => {})\n  }\n\n  private disconnectSafely(): void {\n    try {\n      this.ioSocket.disconnect(true)\n    } catch {\n      // The original failure is already contained and reported.\n    }\n  }\n}\n"],"mappings":"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"}