wse-client 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +38 -5
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -14,7 +14,7 @@ High-performance WebSocket server built in Rust with native clustering, E2E encr
14
14
  | Feature | Details |
15
15
  |---------|---------|
16
16
  | **Rust core** | tokio async runtime, tungstenite WebSocket transport, dedicated thread pool, zero GIL on the data path |
17
- | **JWT authentication** | Rust-native HS256 validation during handshake (0.01ms), cookie + Authorization header extraction |
17
+ | **JWT authentication** | HS256, RS256, ES256 algorithms via jsonwebtoken crate. Validated during handshake (0.01ms), cookie + Authorization header extraction, key rotation, kid validation |
18
18
  | **Protocol negotiation** | `client_hello`/`server_hello` handshake with feature discovery, capability advertisement, version agreement |
19
19
  | **Topic subscriptions** | Per-connection topic subscriptions with automatic cleanup on disconnect |
20
20
  | **Pre-framed broadcast** | WebSocket frame built once, shared via Arc across all connections, single allocation per broadcast |
@@ -29,6 +29,9 @@ High-performance WebSocket server built in Rust with native clustering, E2E encr
29
29
  | **Compression** | zlib for client-facing messages above threshold (default 1024 bytes) |
30
30
  | **MessagePack** | Opt-in binary transport via `?format=msgpack`, roughly 2x faster serialization, 30% smaller |
31
31
  | **Message signing** | Selective HMAC-SHA256 signing for critical operations, nonce-based replay prevention |
32
+ | **Queue groups** | Round-robin dispatch within named groups for load-balanced worker pools |
33
+ | **Topic ACL** | Per-connection allow/deny glob patterns for topic access control |
34
+ | **Graceful drain** | `drain()` sends Close frame to all clients, rejects new connections, notifies cluster peers |
32
35
 
33
36
  ### End-to-End Encryption
34
37
 
@@ -55,6 +58,7 @@ High-performance WebSocket server built in Rust with native clustering, E2E encr
55
58
  | **Circuit breaker** | 10 failures to open, 60s reset, 3 half-open probe calls |
56
59
  | **Dead letter queue** | 1000-entry ring buffer for failed cluster sends |
57
60
  | **Presence sync** | PresenceUpdate/PresenceFull frames, CRDT last-write-wins conflict resolution |
61
+ | **Topology API** | `cluster_info()` returns connected peer list with address, instance_id, status |
58
62
 
59
63
  ### Presence Tracking
60
64
 
@@ -99,7 +103,7 @@ High-performance WebSocket server built in Rust with native clustering, E2E encr
99
103
 
100
104
  | Feature | Details |
101
105
  |---------|---------|
102
- | **Origin validation** | `ALLOWED_ORIGINS` env var, rejects unlisted origins with close code 4403 |
106
+ | **Origin validation** | Configure in reverse proxy (nginx/Caddy) to prevent CSWSH |
103
107
  | **Cookie auth** | `access_token` HTTP-only cookie with `Secure + SameSite=Lax` (OWASP recommended for browsers) |
104
108
  | **Frame protection** | 1 MB max frame size, serde_json parsing (no eval), escaped user IDs in server_ready |
105
109
  | **Cluster frame protection** | zstd decompression output capped at 1 MB (MAX_FRAME_SIZE), protocol version validation |
@@ -163,9 +167,14 @@ token = rust_jwt_encode(
163
167
  | `host` | required | Bind address |
164
168
  | `port` | required | Bind port |
165
169
  | `max_connections` | 1000 | Maximum concurrent WebSocket connections |
166
- | `jwt_secret` | None | HS256 secret for JWT validation (bytes, min 32 bytes). `None` disables authentication |
170
+ | `jwt_secret` | None | JWT key for validation. HS256: shared secret (bytes, min 32). RS256/ES256: PEM public key. `None` disables auth |
167
171
  | `jwt_issuer` | None | Expected `iss` claim. Skipped if `None` |
168
172
  | `jwt_audience` | None | Expected `aud` claim. Skipped if `None` |
173
+ | `jwt_cookie_name` | "access_token" | Cookie name for JWT token extraction |
174
+ | `jwt_previous_secret` | None | Previous key for zero-downtime rotation. HS256: previous secret. RS256/ES256: previous public key PEM |
175
+ | `jwt_key_id` | None | Expected `kid` header claim. Rejects tokens with mismatched key ID |
176
+ | `jwt_algorithm` | None | JWT algorithm: `"HS256"` (default), `"RS256"`, or `"ES256"` |
177
+ | `jwt_private_key` | None | PEM private key for RS256/ES256 token encoding. Not needed for HS256 |
169
178
  | `max_inbound_queue_size` | 131072 | Drain mode bounded queue capacity |
170
179
  | `recovery_enabled` | False | Enable per-topic message recovery buffers |
171
180
  | `recovery_buffer_size` | 128 | Ring buffer slots per topic (rounded to power-of-2) |
@@ -176,7 +185,6 @@ token = rust_jwt_encode(
176
185
  | `presence_max_data_size` | 4096 | Max bytes for a user's presence metadata |
177
186
  | `presence_max_members` | 0 | Max tracked members per topic (0 = unlimited) |
178
187
  | `max_outbound_queue_bytes` | 16777216 | Per-connection outbound buffer limit (bytes, default 16 MB). Messages dropped when exceeded |
179
- | `jwt_cookie_name` | "access_token" | Cookie name for JWT token extraction |
180
188
  | `rate_limit_capacity` | 100000.0 | Token bucket capacity per connection |
181
189
  | `rate_limit_refill` | 10000.0 | Token bucket refill rate per second |
182
190
  | `max_message_size` | 1048576 | Maximum WebSocket frame size in bytes (default 1 MB) |
@@ -250,6 +258,7 @@ server.broadcast(topic, text) # Fan-out to topic subscribers
250
258
  ```python
251
259
  server.subscribe_connection(conn_id, ["prices", "news"]) # Subscribe to topics
252
260
  server.subscribe_connection(conn_id, ["chat"], {"status": "online"}) # Subscribe with presence data
261
+ server.subscribe_connection(conn_id, ["tasks"], queue_group="workers") # Subscribe with queue group (round-robin)
253
262
  server.unsubscribe_connection(conn_id, ["news"]) # Unsubscribe from specific topics
254
263
  server.unsubscribe_connection(conn_id, None) # Unsubscribe from all topics
255
264
  server.get_topic_subscriber_count("prices") # Subscriber count for a topic
@@ -257,6 +266,26 @@ server.get_topic_subscriber_count("prices") # Subscrib
257
266
 
258
267
  Subscriptions are cleaned up automatically on disconnect. In cluster mode, interest changes are propagated to peers via SUB/UNSUB frames.
259
268
 
269
+ **Queue groups**: connections in the same `queue_group` receive messages round-robin instead of fanout. Normal subscribers (no queue group) still receive all messages. Useful for distributing work across a pool of consumers.
270
+
271
+ ### Topic ACL
272
+
273
+ Per-connection topic access control with glob pattern matching.
274
+
275
+ ```python
276
+ # Allow only "user:*" topics, deny everything else
277
+ server.set_topic_acl(conn_id, allow=["user:*"])
278
+
279
+ # Allow "data:*" but deny "data:internal:*"
280
+ server.set_topic_acl(conn_id, allow=["data:*"], deny=["data:internal:*"])
281
+
282
+ # Must be called before subscribe_connection
283
+ server.subscribe_connection(conn_id, ["data:prices"]) # allowed
284
+ server.subscribe_connection(conn_id, ["data:internal:audit"]) # denied
285
+ ```
286
+
287
+ Deny patterns take precedence over allow patterns. Supports `*` (any characters) and `?` (single character) wildcards. Applied at subscribe time.
288
+
260
289
  ### Presence Tracking
261
290
 
262
291
  Requires `presence_enabled=True` in the constructor.
@@ -317,6 +346,7 @@ server.connect_cluster(
317
346
 
318
347
  server.cluster_connected() # True if connected to at least one peer
319
348
  server.cluster_peers_count() # Number of active peer connections
349
+ server.cluster_info() # List of connected peers (address, instance_id, connected)
320
350
  ```
321
351
 
322
352
  Nodes form a full TCP mesh automatically. The cluster protocol uses a custom binary frame format with an 8-byte header, 12 message types, and capability negotiation during handshake. Features:
@@ -364,11 +394,14 @@ health = server.health_snapshot()
364
394
  server.get_connection_count() # Lock-free AtomicUsize read
365
395
  server.get_connections() # List all connection IDs (snapshot)
366
396
  server.disconnect(conn_id) # Force-disconnect a connection
397
+ server.drain(close_code=4300, close_reason="shutting down", timeout=10) # Graceful drain
367
398
  server.inbound_queue_depth() # Events waiting to be drained
368
399
  server.inbound_dropped_count() # Events dropped due to full queue
369
400
  server.get_cluster_dlq_entries() # Retrieve failed cluster messages from dead letter queue
370
401
  ```
371
402
 
403
+ `drain()` sends a WebSocket Close frame to all connected clients and rejects new connections. The drain wait runs as a separate task, so the command processor stays responsive. Use for zero-downtime deployments and rolling restarts.
404
+
372
405
  ---
373
406
 
374
407
  ## Security
@@ -382,7 +415,7 @@ Token delivery:
382
415
  - **Backend clients**: `Authorization: Bearer <token>` header and/or `access_token` cookie
383
416
  - **API clients**: `Authorization: Bearer <token>` header
384
417
 
385
- Required claims: `sub` (user ID), `exp` (expiration), `iat` (issued at). Optional: `iss`, `aud` (validated if configured).
418
+ Required claims: `sub` (user ID), `exp` (expiration). Recommended: `iat` (issued at). Optional: `iss`, `aud` (validated if configured).
386
419
 
387
420
  ### End-to-End Encryption
388
421
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wse-client",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "WSE (WebSocket Engine) React client. Type-safe hooks, auto-reconnect, offline queue, E2E encryption.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",