Skip to content

REST API & Configuration

1. Controller

Source: src/controllers/websocket-clients/websocket-client.controller.tsExtends: BaseControllerBase Path: /socket/websocket/clients (defined in RestPaths.WEBSOCKET_CLIENTS) DI Dependencies: SignalEventService

typescript
// packages/signal/src/controllers/websocket-clients/websocket-client.controller.ts
@controller({ path: RestPaths.WEBSOCKET_CLIENTS })
export class WebSocketClientController extends BaseController {
  constructor(
    @inject({
      key: BindingKeys.build({
        namespace: BindingNamespaces.SERVICE,
        key: SignalEventService.name,
      }),
    })
    private readonly _signalEventService: SignalEventService,
  ) {
    super({ scope: WebSocketClientController.name });
  }
}

2. REST Endpoints

Full path prefix: /v1/api/socket/websocket/clients

MethodPathAuthRoute KeyDescription
GET/statusNoneGET_STATUSServer readiness and connected client count
GET/JWT, BasicGET_CLIENTSList all connected clients
GET/{clientId}JWT, BasicGET_CLIENT_BY_IDGet specific client details
POST/broadcastJWT, BasicBROADCASTBroadcast message to all clients
POST/rooms/{roomName}/sendJWT, BasicSEND_TO_ROOMSend message to a room
POST/{clientId}/sendJWT, BasicSEND_TO_CLIENTSend message to a specific client
POST/{clientId}/disconnectJWT, BasicDISCONNECT_CLIENTForce-disconnect a client

3. Route Definitions (OpenAPI)

Source: src/controllers/websocket-clients/definitions.ts

All routes are defined using IAuthenticateRouteConfig with Zod schemas for request validation and OpenAPI documentation.

Shared Schemas

typescript
// packages/signal/src/controllers/websocket-clients/definitions.ts
const ClientIdParam = z.object({
  clientId: z.string().openapi({
    param: { name: 'clientId', in: 'path', description: 'WebSocket client ID' },
  }),
});

const RoomNameParam = z.object({
  roomName: z.string().openapi({
    param: { name: 'roomName', in: 'path', description: 'WebSocket room name' },
  }),
});

const MessageBody = z.object({
  topic: z.string(),
  data: z.any(),
});

GET /status

No authentication required.

Response:

json
{ "isReady": true, "clientCount": 5 }

Response schema: z.object({ isReady: z.boolean(), clientCount: z.number() })

Controller logic:

typescript
const isReady = this._signalEventService.isReady();
const clientCount = isReady ? this._signalEventService.getClientCount() : 0;
return context.json({ isReady, clientCount }, HTTP.ResultCodes.RS_2.Ok);

GET / (List Clients)

Authentication: JWT or Basic.

Response:

json
{
  "clients": [
    {
      "id": "abc123",
      "state": "authenticated",
      "encrypted": true,
      "rooms": ["ws-default"],
      "connectedAt": 1704067200000,
      "lastActivity": 1704067260000,
      "metadata": {}
    }
  ]
}

Response schema: z.object({ clients: z.array(WebSocketClientInfoSchema) })

GET /

Authentication: JWT or Basic. Returns 404 if client not found.

Response schema: WebSocketClientInfoSchema

Controller logic:

typescript
const client = this._signalEventService.getClientInfo({ clientId });
if (!client) {
  return context.json({ error: 'Client not found' }, HTTP.ResultCodes.RS_4.NotFound);
}
return context.json(client, HTTP.ResultCodes.RS_2.Ok);

POST /broadcast

Authentication: JWT or Basic.

Request body:

json
{ "topic": "order.updated", "data": { "orderId": "123" } }

Request schema: MessageBodyz.object({ topic: z.string(), data: z.any() })

Response: { "success": true }

POST /rooms/{roomName}/send

Authentication: JWT or Basic.

Path params: roomName (string) Request body: Same as broadcast (MessageBody) Response: { "success": true }

POST /{clientId}/send

Authentication: JWT or Basic.

Path params: clientId (string) Request body: Same as broadcast (MessageBody) Response: { "success": true }

POST /{clientId}/disconnect

Authentication: JWT or Basic. Returns 404 if client not found.

Response:

json
// Success
{ "success": true }

// Not found
{ "error": "Client not found" }

Controller logic:

typescript
const success = this._signalEventService.disconnectClient({ clientId });
if (!success) {
  return context.json({ error: 'Client not found' }, HTTP.ResultCodes.RS_4.NotFound);
}
return context.json({ success: true }, HTTP.ResultCodes.RS_2.Ok);

4. Security Policy

RuleEnforcement
Mandatory encryptionrequireEncryption: true — clients must complete ECDH handshake during authentication
No public key = rejectAuthentication without clientPublicKey field causes handshake rejection
JWT requiredOnly Bearer token authentication is supported for WebSocket via JWTTokenService
REST authREST endpoints accept both JWT (Authorization: Bearer <token>) and Basic (Authorization: Basic <base64>) strategies
Per-client keysEach client gets a unique ephemeral ECDH-derived AES-256-GCM key
Forward secrecyNew ECDH key pair per connection; session keys are never reused
Key cleanupAES key is deleted from server memory when client disconnects
Outbound encryptionoutboundTransformer automatically encrypts all outgoing messages (except connected and error)
Inbound validationmessageHandler validates { iv, ct } structure before attempting decryption

5. Standalone Binary Compilation

The compile script creates a standalone Linux x64 binary for deployment in debian:12-slim Docker containers:

bash
#!/bin/sh
bun build \
  --compile \
  --minify-whitespace \
  --minify-syntax \
  --sourcemap \
  --target=bun-linux-x64 \
  ./dist/index.js \
  --outfile ./dist/signal

Since @venizia/ignis 0.0.7-7 removed heavy optional dependencies (socket.io, bullmq, etc.) from the barrel export, the compile script is straightforward with no stub workarounds or --external flags needed.

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.