@fuzdev/fuz_app 0.118.0 → 0.120.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.
- package/dist/actions/CLAUDE.md +301 -73
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +16 -1
- package/dist/actions/connection_closer.d.ts +136 -51
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +133 -34
- package/dist/actions/register_action_ws.d.ts +176 -24
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +581 -241
- package/dist/actions/register_ws_endpoint.d.ts +1 -0
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +1 -0
- package/dist/actions/socket.svelte.d.ts +43 -9
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +59 -8
- package/dist/actions/transports.d.ts +55 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +55 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +53 -67
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +87 -111
- package/dist/actions/transports_ws_backend.d.ts +212 -30
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +327 -70
- package/dist/actions/ws_endpoint_spec.d.ts +26 -13
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +113 -32
- package/dist/auth/account_actions.d.ts +12 -18
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +47 -33
- package/dist/auth/account_routes.d.ts +5 -11
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +40 -36
- package/dist/auth/account_schema.d.ts +27 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +22 -0
- package/dist/auth/admin_actions.d.ts +8 -17
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +33 -21
- package/dist/auth/api_token_queries.d.ts +23 -2
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +39 -3
- package/dist/auth/audit_emitter.d.ts +61 -38
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +57 -14
- package/dist/auth/audit_log_route_schema.js +7 -7
- package/dist/auth/audit_log_routes.d.ts +31 -6
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +85 -11
- package/dist/auth/audit_log_schema.d.ts +48 -0
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +58 -0
- package/dist/auth/cleanup.d.ts +203 -28
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +222 -26
- package/dist/auth/deps.d.ts +31 -3
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/resolved_auth.d.ts +105 -0
- package/dist/auth/resolved_auth.d.ts.map +1 -0
- package/dist/auth/resolved_auth.js +124 -0
- package/dist/auth/session_middleware.d.ts +8 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +9 -3
- package/dist/auth/session_queries.d.ts +31 -6
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +31 -6
- package/dist/auth/standard_rpc_actions.d.ts +9 -9
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +3 -3
- package/dist/db/CLAUDE.md +19 -8
- package/dist/db/fact_disk_storage.d.ts +10 -8
- package/dist/db/fact_disk_storage.d.ts.map +1 -1
- package/dist/db/fact_disk_storage.js +13 -17
- package/dist/db/fact_store.d.ts +39 -38
- package/dist/db/fact_store.d.ts.map +1 -1
- package/dist/db/fact_store.js +48 -86
- package/dist/db/file_fact_url.d.ts +6 -6
- package/dist/db/file_fact_url.js +6 -6
- package/dist/db/status.d.ts +58 -1
- package/dist/db/status.d.ts.map +1 -1
- package/dist/db/status.js +205 -77
- package/dist/http/CLAUDE.md +7 -3
- package/dist/http/db_routes.d.ts +6 -2
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +2 -2
- package/dist/periodic.d.ts +80 -0
- package/dist/periodic.d.ts.map +1 -0
- package/dist/periodic.js +122 -0
- package/dist/realtime/sse_auth_guard.d.ts +79 -49
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +103 -73
- package/dist/realtime/subscriber_registry.d.ts +119 -8
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +184 -25
- package/dist/server/app_backend.d.ts +12 -1
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +5 -2
- package/dist/server/app_server.d.ts +55 -15
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +41 -13
- package/dist/server/serve_fact_route.d.ts +10 -7
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +11 -8
- package/dist/testing/CLAUDE.md +140 -48
- package/dist/testing/app_server.d.ts +2 -2
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +11 -2
- package/dist/testing/audit_drift_guard.d.ts +1 -48
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +0 -42
- package/dist/testing/connection_closer_helpers.d.ts +9 -22
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +11 -17
- package/dist/testing/cross_backend/capabilities.d.ts +67 -13
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +27 -12
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +21 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +4 -1
- package/dist/testing/cross_backend/session_cap.d.ts +13 -0
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/session_cap.js +55 -19
- package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +93 -26
- package/dist/testing/cross_backend/testing_server_bun.js +4 -4
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -2
- package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
- package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +156 -19
- package/dist/testing/sse_round_trip.d.ts +6 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/stubs.d.ts +1 -1
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +6 -4
- package/dist/testing/transports/sse_transport.d.ts +1 -1
- package/dist/testing/transports/ws_client.d.ts +7 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -1
- package/dist/testing/transports/ws_raw_client.d.ts +51 -0
- package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
- package/dist/testing/transports/ws_raw_client.js +156 -0
- package/dist/testing/transports/ws_transport.d.ts +28 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -1
- package/dist/testing/transports/ws_transport.js +45 -1
- package/dist/testing/ws_round_trip.d.ts +8 -3
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +19 -4
- package/dist/ui/AdminAccounts.svelte +4 -3
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +3 -1
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +22 -6
- package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +16 -0
- package/dist/ui/auth_state.svelte.d.ts +25 -1
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +28 -1
- package/package.json +2 -2
- package/dist/server/file_fact_fetcher.d.ts +0 -42
- package/dist/server/file_fact_fetcher.d.ts.map +0 -1
- package/dist/server/file_fact_fetcher.js +0 -60
package/dist/actions/CLAUDE.md
CHANGED
|
@@ -234,6 +234,12 @@ e.g. when consumers like zzz throw their own `ThrownJsonrpcError`)
|
|
|
234
234
|
preserves code + data verbatim. Generic throws become `internal_error` 500;
|
|
235
235
|
message is the raw error under `DEV`, "internal server error" otherwise.
|
|
236
236
|
|
|
237
|
+
A result `c.json` cannot serialize (a `bigint`, a cycle) answers the same
|
|
238
|
+
`internal_error` 500 rather than throwing. By then `perform_action` has
|
|
239
|
+
committed, and a throw would have the route wrapper
|
|
240
|
+
(`dispatch_with_post_commit_rollback`) discard the handler's
|
|
241
|
+
`post_commit_effects` — the connection closes of a revocation that did land.
|
|
242
|
+
|
|
237
243
|
### Per-request handler shape
|
|
238
244
|
|
|
239
245
|
Unified across HTTP RPC + WS via `ActionContext`:
|
|
@@ -348,16 +354,19 @@ and `allow_fallback: boolean` (default `true`). Explicit
|
|
|
348
354
|
|
|
349
355
|
### WS close codes (`actions/transports.ts`)
|
|
350
356
|
|
|
351
|
-
- `WS_CLOSE_SESSION_REVOKED = 4001` — server revoked auth; client enters permanent `revoked` state, no reconnect.
|
|
357
|
+
- `WS_CLOSE_SESSION_REVOKED = 4001` — server revoked auth (reason `WS_CLOSE_SESSION_REVOKED_REASON`, `Session revoked`); client enters permanent `revoked` state, no reconnect. Also the close for an upgrade refused at admission because its credential was revoked mid-upgrade — indistinguishable on the wire by design.
|
|
352
358
|
- `WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT = 4002` — client observed receive-silence past its receive timeout (`resolve_heartbeat_receive_timeout`).
|
|
353
359
|
- `WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT = 4003` — server observed receive-silence past `DEFAULT_SERVER_HEARTBEAT_TIMEOUT` (60s).
|
|
360
|
+
- `WS_CLOSE_CONNECTION_LIMIT = 4004` — server closed this socket to admit a newer one on the same account, past the per-account connection cap (reason `connection limit`). Not a revocation: the client stays `closed` with `superseded` set and no reconnect, until `connect()` is called. Twin of the Rust spine's constant.
|
|
354
361
|
- `WS_CLOSE_MESSAGE_TOO_BIG = 1009` — server received a message over its `max_message_bytes` cap (RFC 6455 "Message Too Big").
|
|
362
|
+
- `WS_CLOSE_POLICY_VIOLATION = 1008` — a socket sent too much while it was still opening (before admission and `on_socket_open` completed): past `MAX_PRE_ADMISSION_FRAMES` frames or the `PRE_ADMISSION_QUEUE_BYTES_FACTOR` byte budget.
|
|
363
|
+
- `WS_CLOSE_INTERNAL_ERROR = 1011` — the admission credential re-check itself failed (reason `internal error`), `on_socket_open` threw (reason `socket bootstrap failed`), or the socket's connection was removed from the transport without a close (reason `connection unregistered`). Not a revocation: the client reconnects under its ordinary backoff. Twin of the Rust spine's constant.
|
|
355
364
|
|
|
356
365
|
### Transport modules
|
|
357
366
|
|
|
358
367
|
- `actions/transports_http.ts` — `frontend_http_rpc`; thin `fetch` adapter, POST default, GET on `has_side_effects(method) === false`.
|
|
359
368
|
- `actions/transports_ws.ts` — `frontend_websocket_rpc`; thin adapter over `WebsocketRpcConnection` (default impl: `FrontendWebsocketClient`). Answers inbound server→client requests: a built-in `peer_ping_responder` for `peer/ping` (zero-wiring liveness), any other request routed through `peer.receive` with the response sent back over the socket (the frontend half of the ActionPeer receive loop).
|
|
360
|
-
- `actions/transports_ws_backend.ts` — `backend_websocket_rpc`; server-side WS with session tracking; satisfies `FilterableBroadcastTransport`. Server→client requests via `request_connection` (correlation registry in `actions/peer_request.ts`).
|
|
369
|
+
- `actions/transports_ws_backend.ts` — `backend_websocket_rpc`; server-side WS with session tracking, two-phase registration (`register_pending` / `admit`), and the per-account cap; satisfies `FilterableBroadcastTransport`. Server→client requests via `request_connection` (correlation registry in `actions/peer_request.ts`).
|
|
361
370
|
|
|
362
371
|
`FrontendHttpTransport` on non-OK HTTP returns the body as-is when it's a
|
|
363
372
|
JSON-RPC error response for the request (same `id` compared as strings since GET sends it as a query param, or `null`) — the
|
|
@@ -390,22 +399,90 @@ client; `null` skips the client-side measurement.
|
|
|
390
399
|
|
|
391
400
|
### `BackendWebsocketTransport` — server-side WS state
|
|
392
401
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
402
|
+
One map keyed by `connection_id` (branded `Uuid`), in registration order:
|
|
403
|
+
`#connections: Map<Uuid, ConnectionEntry>`, each entry `{ws, identity,
|
|
404
|
+
admitted, abort_controller}`. `identity` is the `ConnectionIdentity`
|
|
405
|
+
`{token_hash, account_id, api_token_id}` (session sets `token_hash`, bearer
|
|
406
|
+
sets `api_token_id`, daemon-token sets both null).
|
|
407
|
+
|
|
408
|
+
The id is the connection's only handle — `register_pending` / `add_connection`
|
|
409
|
+
return it and `admit` / `remove_connection` / `is_registered` take it. There is
|
|
410
|
+
no socket → id lookup: a runtime adapter may hand each socket event its own
|
|
411
|
+
`WSContext` (`hono/bun` builds one per event), so the object a close event
|
|
412
|
+
carries can't identify the connection. `register_action_ws` captures the id on
|
|
413
|
+
open and removes by it.
|
|
414
|
+
|
|
415
|
+
**Two-phase registration.** An upgrade resolves its credential before the
|
|
416
|
+
socket exists, and a revocation closes only the connections *registered* under
|
|
417
|
+
what it revoked — so a one-step registration leaves a window in which a
|
|
418
|
+
revocation closes nothing and the socket is then admitted on a dead credential.
|
|
419
|
+
The transport splits registration to close it (the twin of the Rust
|
|
420
|
+
`fuz_realtime` `ConnectionRegistry`, same method names):
|
|
421
|
+
|
|
422
|
+
- `register_pending(ws, token_hash, account_id, api_token_id?, abort_controller?)`
|
|
423
|
+
inserts the entry **un-admitted**. Every `close_sockets_for_*` matches it
|
|
424
|
+
from that instant. Otherwise it is inert: no `send` broadcast,
|
|
425
|
+
`broadcast_filtered` (the predicate never sees it), `send_to_account`, or
|
|
426
|
+
`request_connection` (`connection_gone`) reaches it; `get_connection_count`,
|
|
427
|
+
`is_ready`, and `is_registered` don't see it
|
|
428
|
+
(`get_pending_connection_count` does); the cap neither counts nor evicts it.
|
|
429
|
+
- the caller re-reads the credential (`revalidate_resolved_auth`).
|
|
430
|
+
- `admit(connection_id)` returns `false` when the entry is gone — a revocation
|
|
431
|
+
closed it while pending — and evicts nothing; otherwise it enforces the cap
|
|
432
|
+
(counting admitted connections only) and marks the entry admitted.
|
|
433
|
+
|
|
434
|
+
Nothing is queued for a pending entry, so a server-initiated message sent
|
|
435
|
+
between the handshake and admission is not delivered — the same as one sent
|
|
436
|
+
before the handshake. `add_connection` is the one-step form (register and admit
|
|
437
|
+
together) for a connection with nothing revocable behind it: tests and
|
|
438
|
+
harnesses that register a socket directly. An upgrade that authenticated a session or token
|
|
439
|
+
uses the two-phase form.
|
|
440
|
+
|
|
441
|
+
**Abort at close time.** The `abort_controller` passed at registration is the
|
|
442
|
+
socket's per-connection controller. The transport aborts it when it closes the
|
|
443
|
+
connection — a revocation or a cap eviction — so in-flight handlers see
|
|
444
|
+
`signal.aborted` at once, not when the close handshake lands.
|
|
445
|
+
`remove_connection` only removes: it neither closes nor aborts.
|
|
446
|
+
|
|
447
|
+
**Per-account connection cap.** `new BackendWebsocketTransport({max_connections_per_account})`
|
|
448
|
+
— default `DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT` (50), `null` disables, anything
|
|
449
|
+
but a positive integer throws. The policy is **evict-oldest**: admission always
|
|
450
|
+
admits the new connection, first closing the account's oldest admitted ones
|
|
451
|
+
(registration order) with `WS_CLOSE_CONNECTION_LIMIT` so the account never
|
|
452
|
+
holds more than the cap. It keys on `account_id` across every credential type —
|
|
453
|
+
session, bearer, and daemon-token connections all count, none exempt. Refusing
|
|
454
|
+
the newcomer instead would let half-open sockets lock a user out with their
|
|
455
|
+
own dead connections, and let a stolen credential hold every slot ahead of the
|
|
456
|
+
real user. The check runs at **admission** (`admit`, or `add_connection`),
|
|
457
|
+
which `register_action_ws` reaches in `onOpen` — after every upgrade gate
|
|
458
|
+
(origin, auth, token scope, role) and the credential re-read — so a refused or
|
|
459
|
+
just-revoked request never closes someone's socket. An evicted connection
|
|
460
|
+
leaves the transport's map at once — no broadcast or peer request reaches it,
|
|
461
|
+
its pending peer requests drain as `connection_gone`, it stops counting toward
|
|
462
|
+
the cap, its controller is aborted, and `register_action_ws` dispatches nothing
|
|
463
|
+
more on it — while the socket itself is only closed once the close handshake
|
|
464
|
+
completes. Its `on_socket_close` fires when the close lands, and the
|
|
465
|
+
`remove_connection` before it is a no-op. An eviction logs
|
|
466
|
+
`ws: connection cap closed oldest` at
|
|
467
|
+
info with `{account_id, closed, max}` (the Rust spine's line) through the
|
|
468
|
+
transport's `log` (a `[ws]` logger when unset, `null` silences;
|
|
469
|
+
`register_action_ws` passes its own to the transport it creates). The cap is per transport:
|
|
470
|
+
endpoints sharing one transport share one count per account. The twin of the
|
|
471
|
+
Rust `fuz_realtime` `ConnectionRegistry`'s cap — same default, same policy,
|
|
472
|
+
same close code and reason — pinned on both by the
|
|
473
|
+
`describe_ws_connection_cap_cross_tests` suite.
|
|
398
474
|
|
|
399
475
|
Targeted closure (all return socket count closed, use
|
|
400
|
-
`WS_CLOSE_SESSION_REVOKED
|
|
476
|
+
`WS_CLOSE_SESSION_REVOKED`, match pending and admitted entries alike, and abort
|
|
477
|
+
each closed connection's controller):
|
|
401
478
|
|
|
402
479
|
- `close_sockets_for_session(token_hash)`
|
|
403
480
|
- `close_sockets_for_token(api_token_id)`
|
|
404
481
|
- `close_sockets_for_account(account_id)` — coarse, covers session + bearer + daemon-token
|
|
405
482
|
|
|
406
|
-
Fan-out: `send(notification)` broadcasts to every
|
|
407
|
-
`broadcast_filtered(message, predicate)` runs per-connection ACL
|
|
408
|
-
over `ConnectionIdentity`; `send_to_account` wraps `broadcast_filtered` and
|
|
483
|
+
Fan-out, to admitted connections only: `send(notification)` broadcasts to every
|
|
484
|
+
connection; `broadcast_filtered(message, predicate)` runs per-connection ACL
|
|
485
|
+
predicate over `ConnectionIdentity`; `send_to_account` wraps `broadcast_filtered` and
|
|
409
486
|
structurally satisfies `NotificationSender` (see `auth/CLAUDE.md` §WS
|
|
410
487
|
notifications). `send(request)` is a misuse (a request has no single
|
|
411
488
|
broadcast target) and returns an error envelope.
|
|
@@ -434,60 +511,101 @@ Return values are bookkeeping, not delivery receipts — `0` means no live
|
|
|
434
511
|
sockets, non-zero means `ws.send` did not throw. Durable delivery requires
|
|
435
512
|
persistence + rehydration by the consumer.
|
|
436
513
|
|
|
437
|
-
## WS auth guard (`actions/transports_ws_auth_guard.ts`)
|
|
438
|
-
|
|
439
|
-
Closes WS sockets on audit revoke events — per-message dispatch doesn't
|
|
440
|
-
re-check session/token validity, so this guard is the revocation seam for
|
|
441
|
-
open connections.
|
|
442
|
-
|
|
443
|
-
`create_ws_auth_guard(transport, log)` returns an `on_audit_event` callback.
|
|
444
|
-
For standard WS endpoints mounted via `AppServerOptions.ws_endpoints`,
|
|
445
|
-
`create_app_server` registers this guard via `backend.deps.audit.add_listener`
|
|
446
|
-
automatically (per `WsEndpointSpec.auth_guard`). For custom wiring, register
|
|
447
|
-
inside the consumer's `audit_factory` body.
|
|
448
|
-
|
|
449
|
-
`ws_disconnect_event_types` (ReadonlySet): `session_revoke`, `token_revoke`,
|
|
450
|
-
`session_revoke_all`, `token_revoke_all`, `password_change`.
|
|
451
|
-
`role_grant_revoke` is intentionally **omitted** — the WS transport doesn't
|
|
452
|
-
track per-connection role requirements, so role-scoped disconnection would
|
|
453
|
-
require either closing all sockets (too aggressive) or new per-connection
|
|
454
|
-
role tracking (out of scope). Consumers that need it compose their own
|
|
455
|
-
callback.
|
|
456
|
-
|
|
457
|
-
`outcome === 'failure'` events are ignored — they carry attacker-controlled
|
|
458
|
-
identifiers. Reacting to them would let an authenticated caller close
|
|
459
|
-
another user's socket by guessing a session hash or token id.
|
|
460
|
-
|
|
461
|
-
`create_ws_logout_closer(transport, log)` is the sibling helper for
|
|
462
|
-
user-initiated `logout` events — kept separate because
|
|
463
|
-
`ws_disconnect_event_types` deliberately omits `logout` (admin-initiated
|
|
464
|
-
revocations use `session_revoke`, while `logout` is the user-initiated
|
|
465
|
-
case). Closes via `close_sockets_for_account(event.account_id)`.
|
|
466
|
-
|
|
467
514
|
## Connection closer (`actions/connection_closer.ts`)
|
|
468
515
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
516
|
+
A WebSocket and an SSE stream are authorized once, at open, so a revocation
|
|
517
|
+
takes effect on an open connection only when something closes it.
|
|
518
|
+
`ConnectionCloser` is that capability — the twin of the Rust spine's
|
|
519
|
+
`SocketRevoker`:
|
|
472
520
|
|
|
473
521
|
```ts
|
|
474
522
|
interface ConnectionCloser {
|
|
475
523
|
close_sockets_for_session: (session_token_hash: string) => number;
|
|
476
524
|
close_sockets_for_token: (api_token_id: string) => number;
|
|
477
|
-
close_sockets_for_account: (account_id:
|
|
525
|
+
close_sockets_for_account: (account_id: Uuid) => number;
|
|
478
526
|
}
|
|
479
527
|
```
|
|
480
528
|
|
|
481
|
-
`BackendWebsocketTransport` satisfies
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
the
|
|
490
|
-
|
|
529
|
+
`BackendWebsocketTransport` satisfies it structurally;
|
|
530
|
+
`create_sse_connection_closer(registry)` (`realtime/sse_auth_guard.ts`) adapts
|
|
531
|
+
a `SubscriberRegistry`. Closing is idempotent and synchronous.
|
|
532
|
+
|
|
533
|
+
**Every revocation closes after its commit.** A handler never calls a closer
|
|
534
|
+
inline: it calls `queue_connection_close(ctx, deps.connection_closer, target)`,
|
|
535
|
+
which puts the close on the request's post-commit queue (`emit_after_commit`).
|
|
536
|
+
The close runs once the handler's transaction has committed and is discarded
|
|
537
|
+
when the handler throws — a rolled-back revocation closes nothing. `target` is
|
|
538
|
+
a `ConnectionCloseTarget` (`{kind: 'session', session_token_hash}` /
|
|
539
|
+
`{kind: 'token', api_token_id}` / `{kind: 'account', account_id}`);
|
|
540
|
+
`close_connections(closer, target)` is the immediate form the queue runs. Twin
|
|
541
|
+
of `queue_socket_close` / `SocketCloseTarget`.
|
|
542
|
+
|
|
543
|
+
The order is load-bearing for admission: a connection registers, then re-reads
|
|
544
|
+
its credential, so a close that missed the registration must belong to a
|
|
545
|
+
revocation the re-read can see — true only if the close came after the commit
|
|
546
|
+
(../../../docs/security.md §Closing on Revocation).
|
|
547
|
+
|
|
548
|
+
The close does not depend on the audit write, which is pool-routed and
|
|
549
|
+
fail-open. Failure outcomes (`revoked: false`, a 404) queue no close — they
|
|
550
|
+
carry caller-supplied ids.
|
|
551
|
+
|
|
552
|
+
**One closer for every transport.** `create_realtime_closer()` returns a
|
|
553
|
+
`RealtimeCloser` — a `ConnectionCloser` plus `add(closer)` (idempotent by
|
|
554
|
+
reference) — whose every close fans out to each member and returns the sum.
|
|
555
|
+
`create_app_backend` puts one on `AppDeps.connection_closer`; `create_app_server`
|
|
556
|
+
adds each mounted WS transport and the audit stream registry. Twin of
|
|
557
|
+
`RealtimeRevoker`. A member that throws does not stop the fan-out — every
|
|
558
|
+
member is called, then the first error is thrown for the caller to log;
|
|
559
|
+
`BackendWebsocketTransport` and `SubscriberRegistry.close_by_identity` treat a
|
|
560
|
+
throwing socket or stream the same way. `noop_connection_closer` is a `ConnectionCloser` that closes
|
|
561
|
+
nothing, for hand-built action-factory deps on a backend with no
|
|
562
|
+
live-connection surface.
|
|
563
|
+
|
|
564
|
+
It is required where it is read, so it can't be forgotten:
|
|
565
|
+
`RevokingActionFactoryDeps.connection_closer` (`create_account_actions`,
|
|
566
|
+
`create_admin_actions`, `create_standard_rpc_actions`), `AppDeps` /
|
|
567
|
+
`RouteFactoryDeps` (`create_account_route_specs`),
|
|
568
|
+
`AuthCleanupDeps.connection_closer` (the auth cleanup's session sweep closes
|
|
569
|
+
the connections of each expired session it deletes — `auth/CLAUDE.md`
|
|
570
|
+
§Cleanup), and the
|
|
571
|
+
`connection_closer: RealtimeCloser | null` option of `register_action_ws` /
|
|
572
|
+
`register_ws_endpoint` / `create_audit_log_sse`, which add their transport to it
|
|
573
|
+
(`null` is the explicit opt-out).
|
|
574
|
+
|
|
575
|
+
## WS auth guard (`actions/transports_ws_auth_guard.ts`)
|
|
576
|
+
|
|
577
|
+
The audit-listener half of revocation: `create_ws_auth_guard(closer, log)`
|
|
578
|
+
returns an `on_audit_event` callback that repeats a revocation's close when its
|
|
579
|
+
audit row is announced. It covers a revocation emitted by code that holds no
|
|
580
|
+
closer; for the builtin handlers it is a repeat of the close they already
|
|
581
|
+
queued. A success row is announced only after its request's transaction commits
|
|
582
|
+
(`auth/CLAUDE.md` §Audit emitter), so the guard's close is post-commit too. Its
|
|
583
|
+
closes reach a connection still pending admission.
|
|
584
|
+
|
|
585
|
+
`closer` is any `ConnectionCloser` — a `BackendWebsocketTransport`, or a
|
|
586
|
+
`RealtimeCloser` to cover every transport from one listener. For standard WS
|
|
587
|
+
endpoints mounted via `AppServerOptions.ws_endpoints`, `create_app_server`
|
|
588
|
+
registers the guard via `backend.deps.audit.add_listener` automatically (per
|
|
589
|
+
`WsEndpointSpec.auth_guard`). For custom wiring, register inside the consumer's
|
|
590
|
+
`audit_factory` body.
|
|
591
|
+
|
|
592
|
+
It dispatches on the `RevocationScope` each event declares in
|
|
593
|
+
`audit_event_revocation_scopes` (`auth/audit_log_schema.ts`), the table the SSE
|
|
594
|
+
guard reads too — one declaration, so the two can't drift:
|
|
595
|
+
|
|
596
|
+
- `session` (`session_revoke`) → `close_sockets_for_session(metadata.session_id)`
|
|
597
|
+
- `token` (`token_revoke`) → `close_sockets_for_token(metadata.token_id)`
|
|
598
|
+
- `account` (`session_revoke_all`, `token_revoke_all`, `password_change`,
|
|
599
|
+
`logout`, `account_delete`, `account_purge`) →
|
|
600
|
+
`close_sockets_for_account(target_account_id ?? account_id)`
|
|
601
|
+
- `role` (`role_grant_revoke`) → nothing, deliberately: per-message dispatch
|
|
602
|
+
re-authorizes, so the next message is refused. The SSE guard closes here.
|
|
603
|
+
- `none` → nothing — every other event, and any event type fuz_app doesn't
|
|
604
|
+
define
|
|
605
|
+
|
|
606
|
+
`outcome === 'failure'` events are ignored — they carry attacker-controlled
|
|
607
|
+
identifiers. Reacting to them would let an authenticated caller close
|
|
608
|
+
another user's socket by guessing a session hash or token id.
|
|
491
609
|
|
|
492
610
|
## WebSocket dispatch — three layered entry points
|
|
493
611
|
|
|
@@ -508,10 +626,19 @@ but `upgradeWebSocket` is missing. A factory returning `[]` does NOT trip
|
|
|
508
626
|
the check, so feature-flag gated WS surfaces stay safe.
|
|
509
627
|
|
|
510
628
|
`WsEndpointSpec` fields: `path`, `allowed_origins`, `actions`,
|
|
511
|
-
`required_roles?`, `transport?`, `
|
|
629
|
+
`required_roles?`, `transport?`, `max_connections_per_account?`, `heartbeat?`,
|
|
630
|
+
`artificial_delay?`, `max_message_bytes?`,
|
|
512
631
|
`on_socket_open?`, `on_socket_close?`, `auth_guard?` (default `true`,
|
|
513
632
|
deduped by reference identity via `WeakSet<BackendWebsocketTransport>`),
|
|
514
|
-
`extra_audit_handlers?`.
|
|
633
|
+
`extra_audit_handlers?`. Every mounted transport is added to
|
|
634
|
+
`deps.connection_closer`, whatever `auth_guard` says — the flag opts out of
|
|
635
|
+
the audit listener, not of the handlers' closes.
|
|
636
|
+
|
|
637
|
+
`max_connections_per_account` sets the per-account connection cap on the
|
|
638
|
+
transport the mount creates (`RegisterActionWsOptions` carries the same field).
|
|
639
|
+
It throws alongside `transport` — a supplied transport carries its own cap, set
|
|
640
|
+
where it was constructed, and an option that silently did nothing would read as
|
|
641
|
+
enforced.
|
|
515
642
|
|
|
516
643
|
Mounted transport reachable at `app_server.ws_endpoints[path]`
|
|
517
644
|
(`Readonly<Record<string, BackendWebsocketTransport>>`). Duplicate paths
|
|
@@ -542,7 +669,8 @@ Composes the standard upgrade stack:
|
|
|
542
669
|
5. Delegates to `register_action_ws`
|
|
543
670
|
|
|
544
671
|
Extends `RegisterActionWsOptions` with `allowed_origins` and optional
|
|
545
|
-
`required_roles
|
|
672
|
+
`required_roles`, and inherits its required `connection_closer`. Returns
|
|
673
|
+
`{transport}`. Most consumers reach for
|
|
546
674
|
`ws_endpoints` above; this is the entry test harnesses use when they need
|
|
547
675
|
the upgrade stack without `create_app_server`'s full assembly.
|
|
548
676
|
|
|
@@ -558,14 +686,79 @@ HTTP RPC also calls. `register_action_ws` owns only WS-specific concerns:
|
|
|
558
686
|
- **Cancel-notification interception** — `{request_id → AbortController}` map; aborts the matching pending controller before the cancel bubbles past the dispatcher
|
|
559
687
|
- **Socket-scoped notify** — `(method, params) => ws.send(notification)`, threaded into `perform_action` as `notify`
|
|
560
688
|
- **Composed abort signal** — `AbortSignal.any([socket_close, per_request_cancel])`, threaded as `signal`
|
|
561
|
-
- **Connection lifecycle** — `transport.
|
|
689
|
+
- **Connection lifecycle** — admission (`transport.register_pending` → credential re-read → `transport.admit`, which enforces the per-account connection cap) / `remove_connection`, `on_socket_open` / `_close` hooks, server heartbeat
|
|
690
|
+
- **The inbound gate** — frames queue until the socket is admitted and `on_socket_open` has completed; frames on a socket the server closed are dropped
|
|
691
|
+
|
|
692
|
+
**Admission.** `onOpen` registers the connection pending (synchronously, with
|
|
693
|
+
the socket's `AbortController`), re-reads the credential captured at upgrade
|
|
694
|
+
(`revalidate_resolved_auth({db}, resolved)` — session row / token row / account
|
|
695
|
+
row, no touch; `resolved` is `get_resolved_auth(c)`, the one derivation the
|
|
696
|
+
audit stream shares, read off the keys the auth middleware set), then
|
|
697
|
+
`transport.admit`s. A dead credential, or a pending entry a revocation closed
|
|
698
|
+
meanwhile, closes the socket with `WS_CLOSE_SESSION_REVOKED` and the reason a
|
|
699
|
+
revoked socket gets; a re-check that throws closes it with
|
|
700
|
+
`WS_CLOSE_INTERNAL_ERROR` (`internal error`) — fail closed, never admitted
|
|
701
|
+
unchecked. A refused socket gets nothing else written to it, evicts nothing,
|
|
702
|
+
and runs neither lifecycle hook. An upgrade with no resolved credential on its
|
|
703
|
+
context, or one on another account than its request context, throws before the
|
|
704
|
+
socket exists. The test-preset escape hatch
|
|
705
|
+
(`TEST_CONTEXT_PRESET_KEY`) skips the re-read — a pre-baked credential has no
|
|
706
|
+
rows behind it — so `create_ws_test_harness` admits synchronously. Admission
|
|
707
|
+
leaves no interleaving that admits a revoked credential, because every
|
|
708
|
+
revocation closes only after its commit — see
|
|
709
|
+
../../../docs/security.md §Connection Admission and §Closing on Revocation.
|
|
710
|
+
|
|
711
|
+
`connection_closer: RealtimeCloser | null` is a required option: the endpoint's
|
|
712
|
+
transport is added to it, so the revocation handlers' post-commit closes reach
|
|
713
|
+
the sockets opened here. `null` is for a transport no revocation has to reach
|
|
714
|
+
(a harness driving the dispatcher directly).
|
|
715
|
+
|
|
716
|
+
Because `onOpen` awaits, an adapter can deliver `onMessage` before admission
|
|
717
|
+
(`@hono/node-ws` doesn't await `onOpen` at all). Those frames are **queued, not
|
|
718
|
+
dispatched** — nothing runs on a credential that has not been re-read — and
|
|
719
|
+
dispatch in arrival order, back to back, once the socket is admitted and
|
|
720
|
+
`on_socket_open` has completed; a refusal drops them unread. The Rust spine
|
|
721
|
+
gets the same ordering from starting its read loop at admission, with the
|
|
722
|
+
kernel socket buffer as the queue; here the queue is in memory, bounded by
|
|
723
|
+
count (`MAX_PRE_ADMISSION_FRAMES`, 256) and by bytes
|
|
724
|
+
(`PRE_ADMISSION_QUEUE_BYTES_FACTOR`, 4, times the endpoint's
|
|
725
|
+
`max_message_bytes`) — the frame that would pass either closes the socket with
|
|
726
|
+
`WS_CLOSE_POLICY_VIOLATION` (reason `WS_CLOSE_PRE_ADMISSION_OVERFLOW_REASON`).
|
|
727
|
+
The byte budget is what bounds memory: pending sockets are not capped per
|
|
728
|
+
account. A consumer that raises the client's durable queue above the frame cap
|
|
729
|
+
makes a full reopen flush overflow it. The `onMessage` call for a queued frame
|
|
730
|
+
returns a promise that settles when the frame was dispatched or dropped.
|
|
731
|
+
|
|
732
|
+
The queue holds **every** inbound frame, replies to server-initiated requests
|
|
733
|
+
included — so an `on_socket_open` hook that awaits
|
|
734
|
+
`transport.request_connection` on its own connection gets no reply until it
|
|
735
|
+
returns (it times out); start such a request without awaiting it.
|
|
736
|
+
|
|
737
|
+
**A server-closed socket dispatches nothing.** An adapter can keep delivering
|
|
738
|
+
frames after the server calls `ws.close(…)`, until the close handshake
|
|
739
|
+
completes — `@hono/node-ws` does, for up to the `ws` close timeout when the
|
|
740
|
+
client withholds its close frame. So every server-side close removes the
|
|
741
|
+
connection from the transport and aborts the socket's controller **at close
|
|
742
|
+
time**: the transport does both for a revocation or a cap eviction, and
|
|
743
|
+
`register_action_ws`'s `end_socket` does both for a heartbeat timeout, an
|
|
744
|
+
oversized message, a failed `on_socket_open`, and a refused admission.
|
|
745
|
+
`onMessage` then drops any frame whose socket has ended — no dispatch, no
|
|
746
|
+
error reply. A connection the transport no longer holds though nothing closed
|
|
747
|
+
its socket (a bare `transport.remove_connection`; `transport.is_registered` is
|
|
748
|
+
the twin of the Rust `is_registered`) is ended on its next frame with
|
|
749
|
+
`WS_CLOSE_INTERNAL_ERROR` (`connection unregistered`), so it can't linger as a
|
|
750
|
+
socket that reads everything, answers nothing, and — being chatty — is never
|
|
751
|
+
reaped by the heartbeat. Only a frame that is queued or dispatched counts as
|
|
752
|
+
heartbeat activity.
|
|
562
753
|
|
|
563
754
|
**Per-message authorization phase.** `perform_action` calls
|
|
564
755
|
`apply_authorization_phase` per-message (HTTP and WS uniformly). Role grant
|
|
565
756
|
changes during a connection lifetime are picked up on the next message —
|
|
566
757
|
no in-place refresh, no socket-close on `role_grant_revoke`. Authentication
|
|
567
|
-
invalidation (
|
|
568
|
-
still closes the socket
|
|
758
|
+
invalidation (a session or token revoke, `password_change`, a logout, an
|
|
759
|
+
account delete, a cap eviction) still closes the socket — the revoking
|
|
760
|
+
handler through `connection_closer`, `create_ws_auth_guard` again on the
|
|
761
|
+
audit row.
|
|
569
762
|
|
|
570
763
|
Per-message side-effect queues: `pending_effects` (eager) drains via
|
|
571
764
|
`flush_pending_effects`; `post_commit_effects` (deferred — pushed by
|
|
@@ -573,26 +766,46 @@ handlers via `emit_after_commit`) drains via `flush_post_commit_effects`.
|
|
|
573
766
|
Both flush in the same `try/finally` that releases the request controller,
|
|
574
767
|
so fire-and-forget audit / notification effects pushed by the handler
|
|
575
768
|
complete (or reject visibly) before the next message dispatches. The
|
|
769
|
+
deferred thunks are invoked first and the eager writes awaited after, so a
|
|
770
|
+
revocation's close never waits on a slow audit INSERT — held behind one, the
|
|
771
|
+
socket would stay open on a credential already committed gone and dispatch its
|
|
772
|
+
next frame. The
|
|
576
773
|
deferred queue is **discarded on rollback** before it reaches that flush (a
|
|
577
774
|
rolled-back message fires no post-commit effect). See `http/CLAUDE.md`
|
|
578
775
|
§Pending Effects.
|
|
579
776
|
|
|
777
|
+
**Reply before flush.** The response is sent before either queue flushes. A
|
|
778
|
+
handler that revokes the credential its own socket runs on queues a close of
|
|
779
|
+
that socket on `post_commit_effects`, so the caller reads its reply and then the
|
|
780
|
+
`WS_CLOSE_SESSION_REVOKED` close — flushing first would strand it without an
|
|
781
|
+
answer. (The Rust spine does flush first, and drops the response: a divergence
|
|
782
|
+
the cross-backend WS suite records as `capabilities.ws_self_revocation_reply`.)
|
|
783
|
+
|
|
580
784
|
**Lifecycle hooks.** `on_socket_open({ws, connection_id, identity, notify, signal})`
|
|
581
|
-
fires
|
|
582
|
-
awaited; throws log +
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
785
|
+
fires once the connection is admitted and before the first message dispatches
|
|
786
|
+
(frames sent meanwhile wait); awaited; throws log + an `internal_error` frame +
|
|
787
|
+
close with `WS_CLOSE_INTERNAL_ERROR` `'socket bootstrap failed'`, unregistering
|
|
788
|
+
the connection.
|
|
789
|
+
`on_socket_close({ws, connection_id, identity})` fires — only for a socket that
|
|
790
|
+
was admitted — after
|
|
791
|
+
`transport.remove_connection(connection_id)` — a slow hook never holds a dead
|
|
792
|
+
entry that broadcasts still target and the per-account cap still counts.
|
|
793
|
+
`connection_id` and `identity` are the values captured at open, so the hook
|
|
794
|
+
reads them the same way after a client close, a revocation, or a connection-cap
|
|
795
|
+
eviction; its `ws` is the close event's context, which on some adapters is not
|
|
796
|
+
the object `on_socket_open` received. Errors logged and swallowed.
|
|
586
797
|
|
|
587
798
|
**Server-side heartbeat** (`heartbeat?: boolean | ServerHeartbeatOptions`):
|
|
588
799
|
default-on, 60s silence timeout. Any inbound message resets
|
|
589
|
-
`last_receive_time` — chatty clients never trip it.
|
|
590
|
-
after
|
|
591
|
-
so event-loop blockage pauses the timer itself.
|
|
800
|
+
`last_receive_time` — chatty clients never trip it. The timer starts at
|
|
801
|
+
admission, and the first timeout window after it is exempt (cold-start grace).
|
|
802
|
+
Tick interval is `timeout / 2`, so event-loop blockage pauses the timer itself.
|
|
803
|
+
A timeout unregisters the connection and aborts its signal along with the
|
|
804
|
+
`WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT` close.
|
|
592
805
|
|
|
593
806
|
Two abort signals composed via `AbortSignal.any`:
|
|
594
807
|
|
|
595
|
-
- `socket_abort_controller` — per-socket, fires
|
|
808
|
+
- `socket_abort_controller` — per-socket, fires when the socket ends: at the server's close (revocation, cap eviction, heartbeat timeout, …) or the client's. Registered with the transport, which aborts it on the closes it makes. Drives every handler's `ctx.signal`.
|
|
596
809
|
- `pending_controllers: Map<JsonrpcRequestId, AbortController>` — per-request. Registered before dispatch, cleared in `finally` so late cancels for a completed id (or a reused id) can't null-abort the wrong handler. Unknown cancels no-op.
|
|
597
810
|
|
|
598
811
|
## Protocol actions (`actions/protocol.ts`)
|
|
@@ -684,8 +897,21 @@ recent frame each way — every frame, including heartbeats and the responses
|
|
|
684
897
|
`request` consumes — for diagnostics UI.
|
|
685
898
|
|
|
686
899
|
Reconnect policy (exponential backoff): `delay = DEFAULT_RECONNECT_DELAY * DEFAULT_BACKOFF_FACTOR ** (attempts-1)`,
|
|
687
|
-
capped at `DEFAULT_RECONNECT_DELAY_MAX`.
|
|
688
|
-
|
|
900
|
+
capped at `DEFAULT_RECONNECT_DELAY_MAX`. Two close codes never reconnect:
|
|
901
|
+
|
|
902
|
+
- `WS_CLOSE_SESSION_REVOKED` is **terminal** — sets `revoked`, no reconnect
|
|
903
|
+
loop on 401, and `connect()` becomes a no-op. Pending and queued requests
|
|
904
|
+
reject `unauthenticated`.
|
|
905
|
+
- `WS_CLOSE_CONNECTION_LIMIT` is **closed until the user acts** — sets
|
|
906
|
+
`superseded` (reactive), status `closed`, no reconnect scheduled: the server
|
|
907
|
+
closed this socket to admit a newer one on the same account, and
|
|
908
|
+
reconnecting would close that one in turn, so clients past the cap would
|
|
909
|
+
close each other in a loop. The credential is still good, so `revoked` stays
|
|
910
|
+
`false` and an explicit `connect()` reopens and clears `superseded`. Pending
|
|
911
|
+
and queued requests reject `service_unavailable`; requests made while
|
|
912
|
+
superseded buffer in the durable queue like any call on a closed client. UI
|
|
913
|
+
reads `superseded` to say why the connection is closed and offer the
|
|
914
|
+
reconnect.
|
|
689
915
|
|
|
690
916
|
Live policy swaps (behave like constructor — whole policy atomic, missing
|
|
691
917
|
fields fall back to defaults, not "keep current"): `set_reconnect`,
|
|
@@ -693,7 +919,9 @@ fields fall back to defaults, not "keep current"): `set_reconnect`,
|
|
|
693
919
|
|
|
694
920
|
`SocketStatus = 'initial' | 'connecting' | 'connected' | 'reconnecting' | 'closed'`.
|
|
695
921
|
`socket_status_to_async_status(status, revoked)` collapses to fuz_util's
|
|
696
|
-
4-way `AsyncStatus
|
|
922
|
+
4-way `AsyncStatus`: `closed` reads `failure` when revoked and `initial`
|
|
923
|
+
otherwise — a `superseded` close included, since the client is not trying and
|
|
924
|
+
`connect()` reopens it.
|
|
697
925
|
|
|
698
926
|
## RPC client (`actions/rpc_client.ts`)
|
|
699
927
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action_rpc.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_rpc.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"action_rpc.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_rpc.ts"],"names":[],"mappings":"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"}
|
|
@@ -133,6 +133,10 @@ export const create_rpc_endpoint = (options) => {
|
|
|
133
133
|
* 3. Hand off to `perform_action` for the post-parse pipeline.
|
|
134
134
|
* 4. Bind the result to `c.json` — `'ok'` returns the result envelope,
|
|
135
135
|
* `'error'` returns the error envelope at the `result.status` HTTP code.
|
|
136
|
+
* A result that cannot be serialized answers `internal_error` rather
|
|
137
|
+
* than throwing: `perform_action` has already committed, and a throw
|
|
138
|
+
* here would have the route wrapper discard the post-commit effects
|
|
139
|
+
* the handler queued — a revocation's connection closes among them.
|
|
136
140
|
*
|
|
137
141
|
* @param restrict_to_reads - `true` for GET (rejects `side_effects: true` actions)
|
|
138
142
|
*/
|
|
@@ -186,7 +190,18 @@ export const create_rpc_endpoint = (options) => {
|
|
|
186
190
|
// is verified by `jsonrpc_error_code_to_http_status`).
|
|
187
191
|
return c.json(envelope, result.status);
|
|
188
192
|
}
|
|
189
|
-
|
|
193
|
+
try {
|
|
194
|
+
return c.json(envelope);
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
// The handler's output is not JSON (a `bigint`, a cycle). Its
|
|
198
|
+
// transaction has committed, so this must not propagate: the route
|
|
199
|
+
// wrapper reads a throw as a rollback and discards
|
|
200
|
+
// `post_commit_effects`, which would drop the closes of a
|
|
201
|
+
// revocation that did land.
|
|
202
|
+
log.error(`action result is not serializable: ${method_name}`, err);
|
|
203
|
+
return c.json(jsonrpc_error_envelope(id, jsonrpc_error_messages.internal_error(dev_only(err instanceof Error ? err.message : undefined))), jsonrpc_error_code_to_http_status(JSONRPC_ERROR_CODES.internal_error));
|
|
204
|
+
}
|
|
190
205
|
};
|
|
191
206
|
// POST handler — parse JSON-RPC envelope from body
|
|
192
207
|
const post_handler = async (c, route) => {
|