wse-server 2.2.0__tar.gz → 2.3.0__tar.gz
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.
- {wse_server-2.2.0 → wse_server-2.3.0}/PKG-INFO +39 -6
- {wse_server-2.2.0 → wse_server-2.3.0}/README.md +38 -5
- {wse_server-2.2.0 → wse_server-2.3.0}/pyproject.toml +1 -1
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/Cargo.lock +377 -3
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/Cargo.toml +5 -1
- wse_server-2.3.0/rust/src/jwt.rs +1389 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/src/wse/cluster.rs +674 -115
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/src/wse/compression.rs +5 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/src/wse/presence.rs +11 -6
- wse_server-2.3.0/rust/src/wse/recovery.rs +940 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/src/wse/server.rs +816 -87
- {wse_server-2.2.0 → wse_server-2.3.0}/wse_server/__init__.py +1 -1
- {wse_server-2.2.0 → wse_server-2.3.0}/wse_server/_wse_accel.pyi +82 -4
- wse_server-2.2.0/rust/src/jwt.rs +0 -653
- wse_server-2.2.0/rust/src/wse/recovery.rs +0 -520
- {wse_server-2.2.0 → wse_server-2.3.0}/LICENSE +0 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/.cargo/config.toml +0 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/src/lib.rs +0 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/src/wse/filters.rs +0 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/src/wse/mod.rs +0 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/src/wse/queue.rs +0 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/src/wse/rate_limiter.rs +0 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/src/wse/reliability.rs +0 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/src/wse/security.rs +0 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/src/wse/sequencer.rs +0 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/rust/src/wse/transformer.rs +0 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/wse_server/core/__init__.py +0 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/wse_server/core/filters.py +0 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/wse_server/core/types.py +0 -0
- {wse_server-2.2.0 → wse_server-2.3.0}/wse_server/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: wse-server
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Classifier: Development Status :: 5 - Production/Stable
|
|
5
5
|
Classifier: Intended Audience :: Developers
|
|
6
6
|
Classifier: License :: OSI Approved :: MIT License
|
|
@@ -55,7 +55,7 @@ High-performance WebSocket server built in Rust with native clustering, E2E encr
|
|
|
55
55
|
| Feature | Details |
|
|
56
56
|
|---------|---------|
|
|
57
57
|
| **Rust core** | tokio async runtime, tungstenite WebSocket transport, dedicated thread pool, zero GIL on the data path |
|
|
58
|
-
| **JWT authentication** |
|
|
58
|
+
| **JWT authentication** | HS256, RS256, ES256 algorithms via jsonwebtoken crate. Validated during handshake (0.01ms), cookie + Authorization header extraction, key rotation, kid validation |
|
|
59
59
|
| **Protocol negotiation** | `client_hello`/`server_hello` handshake with feature discovery, capability advertisement, version agreement |
|
|
60
60
|
| **Topic subscriptions** | Per-connection topic subscriptions with automatic cleanup on disconnect |
|
|
61
61
|
| **Pre-framed broadcast** | WebSocket frame built once, shared via Arc across all connections, single allocation per broadcast |
|
|
@@ -70,6 +70,9 @@ High-performance WebSocket server built in Rust with native clustering, E2E encr
|
|
|
70
70
|
| **Compression** | zlib for client-facing messages above threshold (default 1024 bytes) |
|
|
71
71
|
| **MessagePack** | Opt-in binary transport via `?format=msgpack`, roughly 2x faster serialization, 30% smaller |
|
|
72
72
|
| **Message signing** | Selective HMAC-SHA256 signing for critical operations, nonce-based replay prevention |
|
|
73
|
+
| **Queue groups** | Round-robin dispatch within named groups for load-balanced worker pools |
|
|
74
|
+
| **Topic ACL** | Per-connection allow/deny glob patterns for topic access control |
|
|
75
|
+
| **Graceful drain** | `drain()` sends Close frame to all clients, rejects new connections, notifies cluster peers |
|
|
73
76
|
|
|
74
77
|
### End-to-End Encryption
|
|
75
78
|
|
|
@@ -96,6 +99,7 @@ High-performance WebSocket server built in Rust with native clustering, E2E encr
|
|
|
96
99
|
| **Circuit breaker** | 10 failures to open, 60s reset, 3 half-open probe calls |
|
|
97
100
|
| **Dead letter queue** | 1000-entry ring buffer for failed cluster sends |
|
|
98
101
|
| **Presence sync** | PresenceUpdate/PresenceFull frames, CRDT last-write-wins conflict resolution |
|
|
102
|
+
| **Topology API** | `cluster_info()` returns connected peer list with address, instance_id, status |
|
|
99
103
|
|
|
100
104
|
### Presence Tracking
|
|
101
105
|
|
|
@@ -140,7 +144,7 @@ High-performance WebSocket server built in Rust with native clustering, E2E encr
|
|
|
140
144
|
|
|
141
145
|
| Feature | Details |
|
|
142
146
|
|---------|---------|
|
|
143
|
-
| **Origin validation** |
|
|
147
|
+
| **Origin validation** | Configure in reverse proxy (nginx/Caddy) to prevent CSWSH |
|
|
144
148
|
| **Cookie auth** | `access_token` HTTP-only cookie with `Secure + SameSite=Lax` (OWASP recommended for browsers) |
|
|
145
149
|
| **Frame protection** | 1 MB max frame size, serde_json parsing (no eval), escaped user IDs in server_ready |
|
|
146
150
|
| **Cluster frame protection** | zstd decompression output capped at 1 MB (MAX_FRAME_SIZE), protocol version validation |
|
|
@@ -204,9 +208,14 @@ token = rust_jwt_encode(
|
|
|
204
208
|
| `host` | required | Bind address |
|
|
205
209
|
| `port` | required | Bind port |
|
|
206
210
|
| `max_connections` | 1000 | Maximum concurrent WebSocket connections |
|
|
207
|
-
| `jwt_secret` | None |
|
|
211
|
+
| `jwt_secret` | None | JWT key for validation. HS256: shared secret (bytes, min 32). RS256/ES256: PEM public key. `None` disables auth |
|
|
208
212
|
| `jwt_issuer` | None | Expected `iss` claim. Skipped if `None` |
|
|
209
213
|
| `jwt_audience` | None | Expected `aud` claim. Skipped if `None` |
|
|
214
|
+
| `jwt_cookie_name` | "access_token" | Cookie name for JWT token extraction |
|
|
215
|
+
| `jwt_previous_secret` | None | Previous key for zero-downtime rotation. HS256: previous secret. RS256/ES256: previous public key PEM |
|
|
216
|
+
| `jwt_key_id` | None | Expected `kid` header claim. Rejects tokens with mismatched key ID |
|
|
217
|
+
| `jwt_algorithm` | None | JWT algorithm: `"HS256"` (default), `"RS256"`, or `"ES256"` |
|
|
218
|
+
| `jwt_private_key` | None | PEM private key for RS256/ES256 token encoding. Not needed for HS256 |
|
|
210
219
|
| `max_inbound_queue_size` | 131072 | Drain mode bounded queue capacity |
|
|
211
220
|
| `recovery_enabled` | False | Enable per-topic message recovery buffers |
|
|
212
221
|
| `recovery_buffer_size` | 128 | Ring buffer slots per topic (rounded to power-of-2) |
|
|
@@ -217,7 +226,6 @@ token = rust_jwt_encode(
|
|
|
217
226
|
| `presence_max_data_size` | 4096 | Max bytes for a user's presence metadata |
|
|
218
227
|
| `presence_max_members` | 0 | Max tracked members per topic (0 = unlimited) |
|
|
219
228
|
| `max_outbound_queue_bytes` | 16777216 | Per-connection outbound buffer limit (bytes, default 16 MB). Messages dropped when exceeded |
|
|
220
|
-
| `jwt_cookie_name` | "access_token" | Cookie name for JWT token extraction |
|
|
221
229
|
| `rate_limit_capacity` | 100000.0 | Token bucket capacity per connection |
|
|
222
230
|
| `rate_limit_refill` | 10000.0 | Token bucket refill rate per second |
|
|
223
231
|
| `max_message_size` | 1048576 | Maximum WebSocket frame size in bytes (default 1 MB) |
|
|
@@ -291,6 +299,7 @@ server.broadcast(topic, text) # Fan-out to topic subscribers
|
|
|
291
299
|
```python
|
|
292
300
|
server.subscribe_connection(conn_id, ["prices", "news"]) # Subscribe to topics
|
|
293
301
|
server.subscribe_connection(conn_id, ["chat"], {"status": "online"}) # Subscribe with presence data
|
|
302
|
+
server.subscribe_connection(conn_id, ["tasks"], queue_group="workers") # Subscribe with queue group (round-robin)
|
|
294
303
|
server.unsubscribe_connection(conn_id, ["news"]) # Unsubscribe from specific topics
|
|
295
304
|
server.unsubscribe_connection(conn_id, None) # Unsubscribe from all topics
|
|
296
305
|
server.get_topic_subscriber_count("prices") # Subscriber count for a topic
|
|
@@ -298,6 +307,26 @@ server.get_topic_subscriber_count("prices") # Subscrib
|
|
|
298
307
|
|
|
299
308
|
Subscriptions are cleaned up automatically on disconnect. In cluster mode, interest changes are propagated to peers via SUB/UNSUB frames.
|
|
300
309
|
|
|
310
|
+
**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.
|
|
311
|
+
|
|
312
|
+
### Topic ACL
|
|
313
|
+
|
|
314
|
+
Per-connection topic access control with glob pattern matching.
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
# Allow only "user:*" topics, deny everything else
|
|
318
|
+
server.set_topic_acl(conn_id, allow=["user:*"])
|
|
319
|
+
|
|
320
|
+
# Allow "data:*" but deny "data:internal:*"
|
|
321
|
+
server.set_topic_acl(conn_id, allow=["data:*"], deny=["data:internal:*"])
|
|
322
|
+
|
|
323
|
+
# Must be called before subscribe_connection
|
|
324
|
+
server.subscribe_connection(conn_id, ["data:prices"]) # allowed
|
|
325
|
+
server.subscribe_connection(conn_id, ["data:internal:audit"]) # denied
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
Deny patterns take precedence over allow patterns. Supports `*` (any characters) and `?` (single character) wildcards. Applied at subscribe time.
|
|
329
|
+
|
|
301
330
|
### Presence Tracking
|
|
302
331
|
|
|
303
332
|
Requires `presence_enabled=True` in the constructor.
|
|
@@ -358,6 +387,7 @@ server.connect_cluster(
|
|
|
358
387
|
|
|
359
388
|
server.cluster_connected() # True if connected to at least one peer
|
|
360
389
|
server.cluster_peers_count() # Number of active peer connections
|
|
390
|
+
server.cluster_info() # List of connected peers (address, instance_id, connected)
|
|
361
391
|
```
|
|
362
392
|
|
|
363
393
|
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:
|
|
@@ -405,11 +435,14 @@ health = server.health_snapshot()
|
|
|
405
435
|
server.get_connection_count() # Lock-free AtomicUsize read
|
|
406
436
|
server.get_connections() # List all connection IDs (snapshot)
|
|
407
437
|
server.disconnect(conn_id) # Force-disconnect a connection
|
|
438
|
+
server.drain(close_code=4300, close_reason="shutting down", timeout=10) # Graceful drain
|
|
408
439
|
server.inbound_queue_depth() # Events waiting to be drained
|
|
409
440
|
server.inbound_dropped_count() # Events dropped due to full queue
|
|
410
441
|
server.get_cluster_dlq_entries() # Retrieve failed cluster messages from dead letter queue
|
|
411
442
|
```
|
|
412
443
|
|
|
444
|
+
`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.
|
|
445
|
+
|
|
413
446
|
---
|
|
414
447
|
|
|
415
448
|
## Security
|
|
@@ -423,7 +456,7 @@ Token delivery:
|
|
|
423
456
|
- **Backend clients**: `Authorization: Bearer <token>` header and/or `access_token` cookie
|
|
424
457
|
- **API clients**: `Authorization: Bearer <token>` header
|
|
425
458
|
|
|
426
|
-
Required claims: `sub` (user ID), `exp` (expiration)
|
|
459
|
+
Required claims: `sub` (user ID), `exp` (expiration). Recommended: `iat` (issued at). Optional: `iss`, `aud` (validated if configured).
|
|
427
460
|
|
|
428
461
|
### End-to-End Encryption
|
|
429
462
|
|
|
@@ -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** |
|
|
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** |
|
|
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 |
|
|
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)
|
|
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
|
|