@fuzdev/fuz_app 0.140.0 → 0.141.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 +62 -17
- package/dist/actions/action_rpc.d.ts +5 -1
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +9 -1
- package/dist/actions/connection_closer.d.ts +25 -2
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +8 -1
- package/dist/actions/register_action_ws.d.ts +42 -6
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +87 -19
- package/dist/actions/register_ws_endpoint.d.ts +15 -6
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +5 -2
- package/dist/actions/socket.svelte.d.ts +19 -4
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +31 -4
- package/dist/actions/transports.d.ts +30 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +30 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +13 -6
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +24 -10
- package/dist/actions/transports_ws_backend.d.ts +51 -5
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +62 -9
- package/dist/actions/ws_endpoint_spec.d.ts +11 -10
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +11 -9
- package/dist/auth/account_route_schema.d.ts +1 -0
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +4 -0
- package/dist/auth/api_token.d.ts +20 -0
- package/dist/auth/api_token.d.ts.map +1 -1
- package/dist/auth/api_token.js +21 -1
- package/dist/auth/api_token_queries.d.ts +22 -4
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +11 -6
- package/dist/auth/audit_log_routes.d.ts +10 -0
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +20 -0
- package/dist/auth/audit_log_schema.d.ts +30 -5
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +30 -0
- package/dist/auth/bearer_auth.d.ts +4 -2
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +19 -8
- package/dist/auth/bootstrap_route_schema.d.ts +1 -0
- package/dist/auth/bootstrap_route_schema.d.ts.map +1 -1
- package/dist/auth/bootstrap_route_schema.js +1 -0
- package/dist/auth/cleanup.d.ts +9 -9
- package/dist/auth/cleanup.js +9 -9
- package/dist/auth/credential_deadline.d.ts +95 -0
- package/dist/auth/credential_deadline.d.ts.map +1 -0
- package/dist/auth/credential_deadline.js +139 -0
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +4 -1
- package/dist/auth/middleware.d.ts +90 -12
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +115 -16
- package/dist/auth/request_context.d.ts +4 -2
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +14 -3
- package/dist/auth/resolved_auth.d.ts +13 -2
- package/dist/auth/resolved_auth.d.ts.map +1 -1
- package/dist/auth/resolved_auth.js +5 -4
- package/dist/auth/session_cookie.d.ts +4 -0
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +3 -2
- package/dist/auth/session_middleware.d.ts +1 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +4 -1
- package/dist/auth/session_queries.d.ts +19 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +8 -2
- package/dist/auth/signup_route_schema.d.ts.map +1 -1
- package/dist/auth/signup_route_schema.js +1 -0
- package/dist/hono_context.d.ts +23 -0
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +19 -0
- package/dist/http/CLAUDE.md +41 -15
- package/dist/http/middleware_spec.d.ts +16 -0
- package/dist/http/middleware_spec.d.ts.map +1 -1
- package/dist/http/origin.d.ts +113 -18
- package/dist/http/origin.d.ts.map +1 -1
- package/dist/http/origin.js +247 -127
- package/dist/http/route_spec.d.ts +21 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/schema_helpers.d.ts +22 -0
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +24 -0
- package/dist/http/surface.d.ts +6 -1
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +12 -5
- package/dist/realtime/sse_auth_guard.d.ts +22 -6
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +31 -12
- package/dist/realtime/subscriber_registry.d.ts +10 -1
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +10 -1
- package/dist/server/app_server.d.ts +20 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +8 -3
- package/dist/server/env.d.ts +2 -1
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +7 -17
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +1 -0
- package/dist/testing/CLAUDE.md +154 -22
- package/dist/testing/adversarial_headers.d.ts.map +1 -1
- package/dist/testing/adversarial_headers.js +13 -6
- package/dist/testing/connection_closer_helpers.d.ts +6 -1
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +4 -0
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +14 -65
- package/dist/testing/cross_backend/capabilities.d.ts +26 -2
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +9 -3
- package/dist/testing/cross_backend/conformance_case.d.ts +11 -3
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +6 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +20 -19
- package/dist/testing/cross_backend/credential_deadline.d.ts +25 -0
- package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -0
- package/dist/testing/cross_backend/credential_deadline.js +185 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +13 -2
- package/dist/testing/cross_backend/full_spine_mount.d.ts +3 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +7 -1
- package/dist/testing/cross_backend/narrowed_api_token.d.ts +23 -0
- package/dist/testing/cross_backend/narrowed_api_token.d.ts.map +1 -0
- package/dist/testing/cross_backend/narrowed_api_token.js +40 -0
- package/dist/testing/cross_backend/origin.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin.js +6 -26
- package/dist/testing/cross_backend/origin_conformance.d.ts +152 -0
- package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin_conformance.js +136 -0
- package/dist/testing/cross_backend/origin_conformance.json +1679 -0
- package/dist/testing/cross_backend/origin_scope.d.ts +16 -0
- package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin_scope.js +140 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +35 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.js +64 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +8 -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 +44 -33
- 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 +6 -4
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +19 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +22 -0
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +20 -7
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +180 -5
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +154 -5
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +25 -21
- package/dist/testing/cross_backend/transport_composition.d.ts +60 -0
- package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -0
- package/dist/testing/cross_backend/transport_composition.js +364 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +21 -6
- package/dist/testing/cross_backend/ws_role_gated.d.ts +38 -0
- package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_role_gated.js +142 -0
- package/dist/testing/entities.d.ts +23 -0
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +37 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +3 -1
- package/dist/testing/middleware.d.ts +10 -0
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +31 -7
- package/dist/testing/stubs.d.ts +16 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +20 -7
- package/dist/testing/surface_invariants.js +2 -2
- package/package.json +1 -1
package/dist/actions/CLAUDE.md
CHANGED
|
@@ -358,11 +358,12 @@ and `allow_fallback: boolean` (default `true`). Explicit
|
|
|
358
358
|
|
|
359
359
|
### WS close codes (`actions/transports.ts`)
|
|
360
360
|
|
|
361
|
-
- `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.
|
|
361
|
+
- `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 — and for a socket whose credential expired while open, with reason `WS_CLOSE_CREDENTIAL_EXPIRED_REASON` (`Credential expired`), which the client treats as the same `revoked` state.
|
|
362
362
|
- `WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT = 4002` — client observed receive-silence past its receive timeout (`resolve_heartbeat_receive_timeout`).
|
|
363
363
|
- `WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT = 4003` — server observed receive-silence past `DEFAULT_SERVER_HEARTBEAT_TIMEOUT` (60s).
|
|
364
364
|
- `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.
|
|
365
365
|
- `WS_CLOSE_OUTBOUND_OVERFLOW = 4005` — the server had more waiting to be written to this socket than its outbound budget holds (reason `WS_CLOSE_OUTBOUND_OVERFLOW_REASON`, `outbound queue overflow`); what was waiting is dropped. Not a revocation, and nothing took the connection's place: the client reconnects under its ordinary backoff. Twin of the Rust spine's constant.
|
|
366
|
+
- `WS_CLOSE_PERMISSION_REVOKED = 4006` — the socket's account lost a role its endpoint's `required_roles` lists: a revoked global grant (reason `WS_CLOSE_PERMISSION_REVOKED_REASON`, `Permission revoked`), mid-life or at admission. Nothing is sent after it. Not a revocation of the credential: the client stays `closed` until `connect()` is called, without setting `revoked` or `superseded`, and rejects pending and queued requests `forbidden`. Twin of the Rust spine's constant.
|
|
366
367
|
- `WS_CLOSE_GOING_AWAY = 1001` — the server is shutting down (reason `WS_CLOSE_GOING_AWAY_REASON`, `Server shutting down`): `close_all_sockets`, which `AppServer.close` runs. Also the close for an upgrade the shutdown refuses — pending at the close-all, or born closed after it. Not a revocation: the client reconnects under its ordinary backoff.
|
|
367
368
|
- `WS_CLOSE_MESSAGE_TOO_BIG = 1009` — server received a message over its `max_message_bytes` cap (RFC 6455 "Message Too Big").
|
|
368
369
|
- `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.
|
|
@@ -409,8 +410,10 @@ client; `null` skips the client-side measurement.
|
|
|
409
410
|
One map keyed by `connection_id` (branded `Uuid`), in registration order:
|
|
410
411
|
`#connections: Map<Uuid, ConnectionEntry>`, each entry `{ws, identity,
|
|
411
412
|
admitted, abort_controller, writer}`. `identity` is the `ConnectionIdentity`
|
|
412
|
-
`{token_hash, account_id, api_token_id}` (session sets
|
|
413
|
-
sets `api_token_id`, daemon-token sets both null
|
|
413
|
+
`{token_hash, account_id, api_token_id, required_roles}` (session sets
|
|
414
|
+
`token_hash`, bearer sets `api_token_id`, daemon-token sets both null;
|
|
415
|
+
`required_roles` is what the endpoint gated the upgrade on, per registration,
|
|
416
|
+
since endpoints with different gates can share one transport).
|
|
414
417
|
|
|
415
418
|
The id is the connection's only handle — `register_pending` / `add_connection`
|
|
416
419
|
return it and `admit` / `remove_connection` / `is_registered` take it. There is
|
|
@@ -426,7 +429,7 @@ revocation closes nothing and the socket is then admitted on a dead credential.
|
|
|
426
429
|
The transport splits registration to close it (the twin of the Rust
|
|
427
430
|
`fuz_realtime` `ConnectionRegistry`, same method names):
|
|
428
431
|
|
|
429
|
-
- `register_pending(ws, token_hash, account_id, api_token_id?, abort_controller?)`
|
|
432
|
+
- `register_pending(ws, token_hash, account_id, api_token_id?, abort_controller?, required_roles?)`
|
|
430
433
|
inserts the entry **un-admitted**. Every `close_sockets_for_*` matches it
|
|
431
434
|
from that instant. Otherwise it is inert: no `send` broadcast,
|
|
432
435
|
`broadcast_filtered` (the predicate never sees it), `send_to_account`, or
|
|
@@ -434,6 +437,9 @@ The transport splits registration to close it (the twin of the Rust
|
|
|
434
437
|
`is_ready`, and `is_registered` don't see it
|
|
435
438
|
(`get_pending_connection_count` does); the cap neither counts nor evicts it.
|
|
436
439
|
- the caller re-reads the credential (`revalidate_resolved_auth`).
|
|
440
|
+
- `take_pending_close_code(connection_id)` — after a refused `admit`, the
|
|
441
|
+
close code the transport closed the pending entry with (4001, 4006, 1001),
|
|
442
|
+
read once; what `register_action_ws` logs the refusal by.
|
|
437
443
|
- `admit(connection_id)` returns `false` when the entry is gone — a revocation
|
|
438
444
|
closed it while pending — and evicts nothing; otherwise it enforces the cap
|
|
439
445
|
(counting admitted connections only) and marks the entry admitted.
|
|
@@ -555,6 +561,11 @@ each closed connection's controller):
|
|
|
555
561
|
- `close_sockets_for_token(api_token_id)`
|
|
556
562
|
- `close_sockets_for_account(account_id)` — coarse, covers session + bearer + daemon-token
|
|
557
563
|
|
|
564
|
+
`close_sockets_for_role(account_id, role)` closes the account's connections
|
|
565
|
+
whose `required_roles` include `role`, pending included, with
|
|
566
|
+
`WS_CLOSE_PERMISSION_REVOKED` instead — over-closing an any-of gate (the loss
|
|
567
|
+
of any listed role closes, whether or not the account still holds another).
|
|
568
|
+
|
|
558
569
|
`close_all_sockets()` closes every connection, pending and admitted, with
|
|
559
570
|
`WS_CLOSE_GOING_AWAY` instead — the shutdown close, removing and aborting each
|
|
560
571
|
the same way — and leaves the transport **closing** for good (`is_closing()`):
|
|
@@ -610,13 +621,17 @@ interface ConnectionCloser {
|
|
|
610
621
|
close_sockets_for_session: (session_token_hash: string) => number;
|
|
611
622
|
close_sockets_for_token: (api_token_id: string) => number;
|
|
612
623
|
close_sockets_for_account: (account_id: Uuid) => number;
|
|
624
|
+
close_sockets_for_role: (account_id: Uuid, role: string) => number; // a revoked global grant
|
|
613
625
|
close_all_sockets: () => number; // shutdown, never a revocation
|
|
614
626
|
}
|
|
615
627
|
```
|
|
616
628
|
|
|
617
629
|
`BackendWebsocketTransport` satisfies it structurally;
|
|
618
|
-
`create_sse_connection_closer(registry)` (`realtime/sse_auth_guard.ts`) adapts
|
|
619
|
-
a `SubscriberRegistry` (`close_all_sockets` is `SubscriberRegistry.close_all
|
|
630
|
+
`create_sse_connection_closer(registry, required_role)` (`realtime/sse_auth_guard.ts`) adapts
|
|
631
|
+
a `SubscriberRegistry` (`close_all_sockets` is `SubscriberRegistry.close_all`;
|
|
632
|
+
`close_sockets_for_role` closes the account's streams when `role` is the
|
|
633
|
+
registry's gating role). No handler closes for a role: a role revocation's
|
|
634
|
+
closes come from the audit listeners alone.
|
|
620
635
|
Closing is idempotent and synchronous. `close_all_sockets` is the one method no
|
|
621
636
|
revocation calls: `AppServer.close` runs it on `deps.connection_closer` to end
|
|
622
637
|
every live connection of the backend at shutdown, and it has no
|
|
@@ -632,7 +647,8 @@ which puts the close on the request's post-commit queue (`emit_after_commit`).
|
|
|
632
647
|
The close runs once the handler's transaction has committed and is discarded
|
|
633
648
|
when the handler throws — a rolled-back revocation closes nothing. `target` is
|
|
634
649
|
a `ConnectionCloseTarget` (`{kind: 'session', session_token_hash}` /
|
|
635
|
-
`{kind: 'token', api_token_id}` / `{kind: 'account', account_id}`
|
|
650
|
+
`{kind: 'token', api_token_id}` / `{kind: 'account', account_id}` /
|
|
651
|
+
`{kind: 'role', account_id, role}`);
|
|
636
652
|
`close_connections(closer, target)` is the immediate form the queue runs. Twin
|
|
637
653
|
of `queue_socket_close` / `SocketCloseTarget`.
|
|
638
654
|
|
|
@@ -702,8 +718,14 @@ guard reads too — one declaration, so the two can't drift:
|
|
|
702
718
|
- `account` (`session_revoke_all`, `token_revoke_all`, `password_change`,
|
|
703
719
|
`logout`, `account_delete`, `account_purge`) →
|
|
704
720
|
`close_sockets_for_account(target_account_id ?? account_id)`
|
|
705
|
-
- `role` (`role_grant_revoke`) →
|
|
706
|
-
|
|
721
|
+
- `role` (`role_grant_revoke`) → `close_sockets_for_role(target_account_id ??
|
|
722
|
+
account_id, metadata.role)` when the revoked grant was global
|
|
723
|
+
(`to_global_role_revocation`); a scoped revoke closes nothing. Per-message
|
|
724
|
+
dispatch re-authorizes each request against its own action, but nothing
|
|
725
|
+
re-checks what a socket admitted by a role gate receives — every push its
|
|
726
|
+
endpoint sends — so the socket closes, with `WS_CLOSE_PERMISSION_REVOKED`.
|
|
727
|
+
The SSE guard closes its streams on the same rows. No handler closes for a
|
|
728
|
+
role, so this guard is the one closer.
|
|
707
729
|
- `none` → nothing — every other event, and any event type fuz_app doesn't
|
|
708
730
|
define
|
|
709
731
|
|
|
@@ -774,9 +796,8 @@ Cross-surface collisions (same `GET <path>` on both `RouteSpec` and
|
|
|
774
796
|
`GET /api/:resource` vs `/api/ws`) is not detected — Hono's specific-before-wildcard routing keeps
|
|
775
797
|
those working but avoid the overlap.
|
|
776
798
|
|
|
777
|
-
`auth_guard
|
|
778
|
-
|
|
779
|
-
`extra_audit_handlers` when needed. When multiple specs share a transport,
|
|
799
|
+
`auth_guard` is also the one closer of a role revocation: with it off for a
|
|
800
|
+
transport, a revoked role closes none of its sockets. When multiple specs share a transport,
|
|
780
801
|
**any** spec with `auth_guard !== false` wires the guard for that
|
|
781
802
|
transport (OR-semantics).
|
|
782
803
|
|
|
@@ -829,7 +850,11 @@ audit stream shares, read off the keys the auth middleware set), then
|
|
|
829
850
|
meanwhile, closes the socket with `WS_CLOSE_SESSION_REVOKED` and the reason a
|
|
830
851
|
revoked socket gets; a re-check that throws closes it with
|
|
831
852
|
`WS_CLOSE_INTERNAL_ERROR` (`internal error`) — fail closed, never admitted
|
|
832
|
-
unchecked.
|
|
853
|
+
unchecked. An endpoint with `required_roles` re-reads them beside the
|
|
854
|
+
credential — the acting actor's grants afresh (`refresh_role_grants`), a global
|
|
855
|
+
grant of any listed role admitting, as the gate in front did — and refuses a
|
|
856
|
+
lost role with `WS_CLOSE_PERMISSION_REVOKED`; a failed role read is the same
|
|
857
|
+
1011. During shutdown a refusal is the shutdown's instead: an upgrade
|
|
833
858
|
pending at the close-all was closed by it with `WS_CLOSE_GOING_AWAY` and its
|
|
834
859
|
controller aborted, so the refusal after the re-read — even one the closing
|
|
835
860
|
database failed — writes nothing more; an upgrade that registers after it is
|
|
@@ -881,8 +906,9 @@ client withholds its close frame. So every server-side close removes the
|
|
|
881
906
|
connection from the transport and aborts the socket's controller **at close
|
|
882
907
|
time**: the transport does both for a revocation or a cap eviction, and
|
|
883
908
|
`register_action_ws`'s `end_socket` does both — through
|
|
884
|
-
`transport.close_connection` — for a heartbeat timeout,
|
|
885
|
-
oversized message, a failed `on_socket_open`, and a refused
|
|
909
|
+
`transport.close_connection` — for a heartbeat timeout, a credential
|
|
910
|
+
expiry, an oversized message, a failed `on_socket_open`, and a refused
|
|
911
|
+
admission.
|
|
886
912
|
`onMessage` then drops any frame whose socket has ended — no dispatch, no
|
|
887
913
|
error reply. A connection the transport no longer holds though nothing closed
|
|
888
914
|
its socket (a bare `transport.remove_connection`; `transport.is_registered` is
|
|
@@ -895,7 +921,10 @@ heartbeat activity.
|
|
|
895
921
|
**Per-message authorization phase.** `perform_action` calls
|
|
896
922
|
`apply_authorization_phase` per-message (HTTP and WS uniformly). Role grant
|
|
897
923
|
changes during a connection lifetime are picked up on the next message —
|
|
898
|
-
no in-place refresh
|
|
924
|
+
no in-place refresh. A revoked global grant of a role the endpoint's
|
|
925
|
+
`required_roles` lists closes the socket (`WS_CLOSE_PERMISSION_REVOKED`,
|
|
926
|
+
through `create_ws_auth_guard`), since nothing else re-checks what it
|
|
927
|
+
receives. Authentication
|
|
899
928
|
invalidation (a session or token revoke, `password_change`, a logout, an
|
|
900
929
|
account delete, a cap eviction) still closes the socket — the revoking
|
|
901
930
|
handler through `connection_closer`, `create_ws_auth_guard` again on the
|
|
@@ -953,6 +982,17 @@ Tick interval is `timeout / 2`, so event-loop blockage pauses the timer itself.
|
|
|
953
982
|
A timeout unregisters the connection and aborts its signal along with the
|
|
954
983
|
`WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT` close.
|
|
955
984
|
|
|
985
|
+
**Credential deadline.** From admission, a timer closes the socket when the
|
|
986
|
+
credential it upgraded on expires (`ResolvedAuth.credential_deadline` — the
|
|
987
|
+
earlier of a session's cookie and row expiry, an API token's row; none for an
|
|
988
|
+
eternal token or the daemon token): `WS_CLOSE_SESSION_REVOKED` with
|
|
989
|
+
`WS_CLOSE_CREDENTIAL_EXPIRED_REASON`, through `end_socket`. Expiry writes
|
|
990
|
+
nothing a revocation listener hears, so the socket watches its own deadline.
|
|
991
|
+
The wait is `schedule_credential_deadline`'s chain of timers of at most an
|
|
992
|
+
hour (a 30-day session is past `setTimeout`'s ceiling), each also checking the
|
|
993
|
+
wall clock so a host suspend does not extend the deadline, cancelled when the
|
|
994
|
+
socket ends. Twin of the Rust loop's `WsLoopOptions::credential_deadline`.
|
|
995
|
+
|
|
956
996
|
Two abort signals composed via `AbortSignal.any`:
|
|
957
997
|
|
|
958
998
|
- `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`.
|
|
@@ -1049,11 +1089,16 @@ recent frame each way — every frame, including heartbeats and the responses
|
|
|
1049
1089
|
Reconnect policy (exponential backoff): `delay = DEFAULT_RECONNECT_DELAY * DEFAULT_BACKOFF_FACTOR ** (attempts-1)`,
|
|
1050
1090
|
capped at `DEFAULT_RECONNECT_DELAY_MAX`. Every other close reconnects —
|
|
1051
1091
|
`WS_CLOSE_OUTBOUND_OVERFLOW` included, the server's notice that this client
|
|
1052
|
-
fell behind and missed messages.
|
|
1092
|
+
fell behind and missed messages. Three close codes never reconnect:
|
|
1053
1093
|
|
|
1054
1094
|
- `WS_CLOSE_SESSION_REVOKED` is **terminal** — sets `revoked`, no reconnect
|
|
1055
1095
|
loop on 401, and `connect()` becomes a no-op. Pending and queued requests
|
|
1056
1096
|
reject `unauthenticated`.
|
|
1097
|
+
- `WS_CLOSE_PERMISSION_REVOKED` is **closed until the user acts** too — status
|
|
1098
|
+
`closed`, no reconnect scheduled (a reconnect would be refused at the role
|
|
1099
|
+
gate), neither `revoked` nor `superseded` set (the credential is still
|
|
1100
|
+
good), pending and queued requests reject `forbidden`. `last_close_code`
|
|
1101
|
+
says why; `connect()` reopens.
|
|
1057
1102
|
- `WS_CLOSE_CONNECTION_LIMIT` is **closed until the user acts** — sets
|
|
1058
1103
|
`superseded` (reactive), status `closed`, no reconnect scheduled: the server
|
|
1059
1104
|
closed this socket to admit a newer one on the same account, and
|
|
@@ -286,7 +286,11 @@ export interface CreateRpcEndpointOptions {
|
|
|
286
286
|
*
|
|
287
287
|
* The RouteSpecs use `auth: {type: 'none'}` because auth is checked per-action
|
|
288
288
|
* inside the dispatcher, and `transaction: false` because transaction scope
|
|
289
|
-
* is per-action (mutations get a transaction, reads get pool).
|
|
289
|
+
* is per-action (mutations get a transaction, reads get pool). They are
|
|
290
|
+
* marked `reads_credential`, since the dispatcher reads the resolved
|
|
291
|
+
* credential, so assembly refuses the pair mounted outside the auth
|
|
292
|
+
* middleware's scope (`assert_credential_routes_in_auth_scope`) whether it
|
|
293
|
+
* arrives as `rpc_endpoints` or through `create_route_specs`.
|
|
290
294
|
*
|
|
291
295
|
* @param options - endpoint path, actions, logger, and the two action rate limiters
|
|
292
296
|
* @returns route specs (GET + POST) ready for `apply_route_specs`
|
|
@@ -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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"}
|
|
@@ -114,7 +114,11 @@ const jsonrpc_error_envelope = (id, error) => ({
|
|
|
114
114
|
*
|
|
115
115
|
* The RouteSpecs use `auth: {type: 'none'}` because auth is checked per-action
|
|
116
116
|
* inside the dispatcher, and `transaction: false` because transaction scope
|
|
117
|
-
* is per-action (mutations get a transaction, reads get pool).
|
|
117
|
+
* is per-action (mutations get a transaction, reads get pool). They are
|
|
118
|
+
* marked `reads_credential`, since the dispatcher reads the resolved
|
|
119
|
+
* credential, so assembly refuses the pair mounted outside the auth
|
|
120
|
+
* middleware's scope (`assert_credential_routes_in_auth_scope`) whether it
|
|
121
|
+
* arrives as `rpc_endpoints` or through `create_route_specs`.
|
|
118
122
|
*
|
|
119
123
|
* @param options - endpoint path, actions, logger, and the two action rate limiters
|
|
120
124
|
* @returns route specs (GET + POST) ready for `apply_route_specs`
|
|
@@ -260,6 +264,9 @@ export const create_rpc_endpoint = (options) => {
|
|
|
260
264
|
method: 'POST',
|
|
261
265
|
path: endpoint_path,
|
|
262
266
|
auth: { account: 'none', actor: 'none' }, // per-action auth inside dispatcher
|
|
267
|
+
// the dispatcher reads the resolved credential per action, so the
|
|
268
|
+
// endpoint stays inside the auth middleware's scope however it mounts
|
|
269
|
+
reads_credential: true,
|
|
263
270
|
handler: post_handler,
|
|
264
271
|
description: `JSON-RPC 2.0 endpoint — ${actions.length} method${actions.length === 1 ? '' : 's'}`,
|
|
265
272
|
input: z.null(), // dispatcher owns body parsing; rpc_endpoints surface has the real schemas
|
|
@@ -270,6 +277,7 @@ export const create_rpc_endpoint = (options) => {
|
|
|
270
277
|
method: 'GET',
|
|
271
278
|
path: endpoint_path,
|
|
272
279
|
auth: { account: 'none', actor: 'none' }, // per-action auth inside dispatcher
|
|
280
|
+
reads_credential: true,
|
|
273
281
|
handler: get_handler,
|
|
274
282
|
description: `JSON-RPC 2.0 endpoint (cacheable reads) — ${actions.length} method${actions.length === 1 ? '' : 's'}`,
|
|
275
283
|
input: z.null(), // params from query string, validated by dispatcher
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* something closes it. `ConnectionCloser` is that capability, and every
|
|
10
10
|
* revocation handler holds one: the session and token revokes, the revoke-alls,
|
|
11
11
|
* account delete and purge, `/logout`, `/password`, and the session and token
|
|
12
|
-
* caps for what they evict.
|
|
12
|
+
* caps for what they evict. A role revocation is the exception: it closes
|
|
13
|
+
* only the connections an endpoint gated on that role, and only through the
|
|
14
|
+
* audit listeners (`create_ws_auth_guard`, `create_sse_auth_guard`) — no
|
|
15
|
+
* handler holds a closer for it. The auth cleanup holds one too
|
|
13
16
|
* (`AuthCleanupDeps.connection_closer`): its session sweep closes the
|
|
14
17
|
* connections of each expired session it deletes, after that delete commits.
|
|
15
18
|
*
|
|
@@ -70,6 +73,20 @@ export interface ConnectionCloser {
|
|
|
70
73
|
* credentials.
|
|
71
74
|
*/
|
|
72
75
|
close_sockets_for_account: (account_id: Uuid) => number;
|
|
76
|
+
/**
|
|
77
|
+
* Close the connections of `account_id` whose endpoint gated them on
|
|
78
|
+
* `role` — the close for a revoked **global** grant of that role (a scoped
|
|
79
|
+
* grant never admits at a role gate, so revoking one closes nothing; the
|
|
80
|
+
* caller filters, `to_global_role_revocation`). A WebSocket gets
|
|
81
|
+
* `WS_CLOSE_PERMISSION_REVOKED`, not a revocation's code: the credential is
|
|
82
|
+
* still good.
|
|
83
|
+
*
|
|
84
|
+
* **Over-closes an any-of gate**: a connection whose endpoint listed
|
|
85
|
+
* several roles closes on the loss of any of them, whether or not the
|
|
86
|
+
* account still holds another. The client reconnects into the gate if it
|
|
87
|
+
* still qualifies.
|
|
88
|
+
*/
|
|
89
|
+
close_sockets_for_role: (account_id: Uuid, role: string) => number;
|
|
73
90
|
/**
|
|
74
91
|
* Close every connection, whoever opened it — the shutdown close
|
|
75
92
|
* `AppServer.close` runs. Not a revocation: a WebSocket gets
|
|
@@ -82,8 +99,10 @@ export interface ConnectionCloser {
|
|
|
82
99
|
}
|
|
83
100
|
/**
|
|
84
101
|
* Which connections a revocation closes — the argument of
|
|
85
|
-
* `queue_connection_close`, one variant per
|
|
102
|
+
* `queue_connection_close`, one variant per revocation close of
|
|
86
103
|
* `ConnectionCloser` (`close_all_sockets` is shutdown, never a revocation).
|
|
104
|
+
* No handler queues `role` today: the audit listeners are a role's only
|
|
105
|
+
* closer.
|
|
87
106
|
*/
|
|
88
107
|
export type ConnectionCloseTarget = {
|
|
89
108
|
kind: 'session';
|
|
@@ -94,6 +113,10 @@ export type ConnectionCloseTarget = {
|
|
|
94
113
|
} | {
|
|
95
114
|
kind: 'account';
|
|
96
115
|
account_id: Uuid;
|
|
116
|
+
} | {
|
|
117
|
+
kind: 'role';
|
|
118
|
+
account_id: Uuid;
|
|
119
|
+
role: string;
|
|
97
120
|
};
|
|
98
121
|
/**
|
|
99
122
|
* Close the connections `target` names, now.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection_closer.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/connection_closer.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"connection_closer.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/connection_closer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAGpD,OAAO,EAAqB,KAAK,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAE5F;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,gBAAgB;IAChC,qGAAqG;IACrG,yBAAyB,EAAE,CAAC,kBAAkB,EAAE,MAAM,KAAK,MAAM,CAAC;IAClE,4EAA4E;IAC5E,uBAAuB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1D;;;;OAIG;IACH,yBAAyB,EAAE,CAAC,UAAU,EAAE,IAAI,KAAK,MAAM,CAAC;IACxD;;;;;;;;;;;;OAYG;IACH,sBAAsB,EAAE,CAAC,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACnE;;;;;;;OAOG;IACH,iBAAiB,EAAE,MAAM,MAAM,CAAC;CAChC;AAED;;;;;;GAMG;AACH,MAAM,MAAM,qBAAqB,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,IAAI,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpD;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAC7B,QAAQ,gBAAgB,EACxB,QAAQ,qBAAqB,KAC3B,MAaF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,GAClC,KAAK,sBAAsB,EAC3B,QAAQ,gBAAgB,EACxB,QAAQ,qBAAqB,KAC3B,IAIF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,gBAMnC,CAAC;AAEH;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,cAAe,SAAQ,gBAAgB;IACvD;;;;;;;;;;;;;;;OAeG;IACH,GAAG,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,MAAM,IAAI,CAAC;IAC9C,2EAA2E;IAC3E,YAAY,EAAE,MAAM,MAAM,CAAC;CAC3B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,QAAO,cA8CzC,CAAC"}
|
|
@@ -9,7 +9,10 @@
|
|
|
9
9
|
* something closes it. `ConnectionCloser` is that capability, and every
|
|
10
10
|
* revocation handler holds one: the session and token revokes, the revoke-alls,
|
|
11
11
|
* account delete and purge, `/logout`, `/password`, and the session and token
|
|
12
|
-
* caps for what they evict.
|
|
12
|
+
* caps for what they evict. A role revocation is the exception: it closes
|
|
13
|
+
* only the connections an endpoint gated on that role, and only through the
|
|
14
|
+
* audit listeners (`create_ws_auth_guard`, `create_sse_auth_guard`) — no
|
|
15
|
+
* handler holds a closer for it. The auth cleanup holds one too
|
|
13
16
|
* (`AuthCleanupDeps.connection_closer`): its session sweep closes the
|
|
14
17
|
* connections of each expired session it deletes, after that delete commits.
|
|
15
18
|
*
|
|
@@ -60,6 +63,8 @@ export const close_connections = (closer, target) => {
|
|
|
60
63
|
return closer.close_sockets_for_token(target.api_token_id);
|
|
61
64
|
case 'account':
|
|
62
65
|
return closer.close_sockets_for_account(target.account_id);
|
|
66
|
+
case 'role':
|
|
67
|
+
return closer.close_sockets_for_role(target.account_id, target.role);
|
|
63
68
|
default:
|
|
64
69
|
throw new UnreachableError(target);
|
|
65
70
|
}
|
|
@@ -93,6 +98,7 @@ export const noop_connection_closer = Object.freeze({
|
|
|
93
98
|
close_sockets_for_session: () => 0,
|
|
94
99
|
close_sockets_for_token: () => 0,
|
|
95
100
|
close_sockets_for_account: () => 0,
|
|
101
|
+
close_sockets_for_role: () => 0,
|
|
96
102
|
close_all_sockets: () => 0
|
|
97
103
|
});
|
|
98
104
|
/**
|
|
@@ -150,6 +156,7 @@ export const create_realtime_closer = () => {
|
|
|
150
156
|
close_sockets_for_session: (session_token_hash) => fan_out((member) => member.close_sockets_for_session(session_token_hash)),
|
|
151
157
|
close_sockets_for_token: (api_token_id) => fan_out((member) => member.close_sockets_for_token(api_token_id)),
|
|
152
158
|
close_sockets_for_account: (account_id) => fan_out((member) => member.close_sockets_for_account(account_id)),
|
|
159
|
+
close_sockets_for_role: (account_id, role) => fan_out((member) => member.close_sockets_for_role(account_id, role)),
|
|
153
160
|
close_all_sockets: () => fan_out((member) => member.close_all_sockets())
|
|
154
161
|
});
|
|
155
162
|
};
|
|
@@ -54,10 +54,19 @@
|
|
|
54
54
|
* admits a revoked credential; `docs/security.md` §Connection Admission says
|
|
55
55
|
* what that rests on.
|
|
56
56
|
*
|
|
57
|
+
* An endpoint gated on `required_roles` re-reads them beside the credential:
|
|
58
|
+
* its upgrade passed the role gate before the socket registered, so a role
|
|
59
|
+
* revocation landing in between would close nothing (`close_sockets_for_role`
|
|
60
|
+
* matches registrations), and the socket would be admitted without the role
|
|
61
|
+
* for its lifetime. The re-read reads the acting actor's grants afresh and
|
|
62
|
+
* admits on a global grant of any listed role, as the gate did.
|
|
63
|
+
*
|
|
57
64
|
* A refused upgrade is closed with `WS_CLOSE_SESSION_REVOKED` (4001, the
|
|
58
|
-
* reason a revoked socket gets)
|
|
59
|
-
*
|
|
60
|
-
*
|
|
65
|
+
* reason a revoked socket gets) for a dead credential,
|
|
66
|
+
* `WS_CLOSE_PERMISSION_REVOKED` (4006) for a lost role, or
|
|
67
|
+
* `WS_CLOSE_INTERNAL_ERROR` (1011) when either re-read itself failed — fail
|
|
68
|
+
* closed, never admitted unchecked. The handshake has already answered `101`,
|
|
69
|
+
* so a refusal can only be a close frame.
|
|
61
70
|
*
|
|
62
71
|
* Once the server is shutting down (`AppServer.close` ran the transport's
|
|
63
72
|
* `close_all_sockets`) a refusal is `WS_CLOSE_GOING_AWAY` instead, so the
|
|
@@ -75,8 +84,17 @@
|
|
|
75
84
|
* `PRE_ADMISSION_QUEUE_BYTES_FACTOR` × `max_message_bytes` bytes; past either
|
|
76
85
|
* the socket is closed.
|
|
77
86
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
87
|
+
* ## Credential deadline
|
|
88
|
+
*
|
|
89
|
+
* Expiry writes nothing a revocation listener could hear, so the socket
|
|
90
|
+
* watches its own credential's expiry (`ResolvedAuth.credential_deadline`,
|
|
91
|
+
* the earlier of a session's cookie and row, an API token's row, none for a
|
|
92
|
+
* daemon token or an eternal token). From admission, a timer closes it at the
|
|
93
|
+
* deadline with `WS_CLOSE_SESSION_REVOKED` (4001) and
|
|
94
|
+
* `WS_CLOSE_CREDENTIAL_EXPIRED_REASON`. The wait is a chain of timers of at
|
|
95
|
+
* most an hour (`schedule_credential_deadline`), so a session's 30 days — past
|
|
96
|
+
* `setTimeout`'s ceiling — does not fire at once, and each wake-up checks the
|
|
97
|
+
* wall clock too, so a host suspend does not extend the deadline.
|
|
80
98
|
*
|
|
81
99
|
* ## A server-closed socket dispatches nothing
|
|
82
100
|
*
|
|
@@ -98,6 +116,7 @@ import type { Hono } from 'hono';
|
|
|
98
116
|
import type { UpgradeWebSocket, WSContext } from 'hono/ws';
|
|
99
117
|
import { type Logger as LoggerType } from '@fuzdev/fuz_util/log.ts';
|
|
100
118
|
import type { Uuid } from '../primitive_schemas.js';
|
|
119
|
+
import type { RoleName } from '../auth/role_schema.js';
|
|
101
120
|
import type { RateLimiter } from '../rate_limiter.js';
|
|
102
121
|
import type { Db } from '../db/db.js';
|
|
103
122
|
import { type Action } from './action_types.js';
|
|
@@ -272,6 +291,19 @@ export interface RegisterActionWsOptions {
|
|
|
272
291
|
* emitter owns the pool.
|
|
273
292
|
*/
|
|
274
293
|
db: Db;
|
|
294
|
+
/**
|
|
295
|
+
* The roles the endpoint's upgrade gate requires (any-of) — what
|
|
296
|
+
* `register_ws_endpoint` mounts as `require_role(required_roles)` ahead of
|
|
297
|
+
* this handler. Recorded on every registration
|
|
298
|
+
* (`ConnectionIdentity.required_roles`), so a revoked global grant of a
|
|
299
|
+
* listed role closes the socket (`close_sockets_for_role`), and re-read at
|
|
300
|
+
* admission against the acting actor's grants, so a revocation that landed
|
|
301
|
+
* between the gate and the registration refuses the upgrade with
|
|
302
|
+
* `WS_CLOSE_PERMISSION_REVOKED`. This handler does not gate on it before
|
|
303
|
+
* the upgrade; mount the 403 gate in front (`register_ws_endpoint` does).
|
|
304
|
+
* Omit, or pass `[]`, for an ungated endpoint.
|
|
305
|
+
*/
|
|
306
|
+
required_roles?: ReadonlyArray<RoleName>;
|
|
275
307
|
/**
|
|
276
308
|
* Existing transport to register connections with. When omitted, a fresh
|
|
277
309
|
* one is created and returned in the result. Pass your own to keep a
|
|
@@ -411,8 +443,12 @@ export interface RegisterActionWsResult {
|
|
|
411
443
|
* on the audit row.
|
|
412
444
|
* - Admission re-reads the credential once, after the handshake: a socket
|
|
413
445
|
* whose session or token was revoked while it was upgrading closes with
|
|
414
|
-
* `WS_CLOSE_SESSION_REVOKED`,
|
|
446
|
+
* `WS_CLOSE_SESSION_REVOKED`, one that lost a role `required_roles` lists
|
|
447
|
+
* with `WS_CLOSE_PERMISSION_REVOKED`, and one whose re-check failed with
|
|
415
448
|
* `WS_CLOSE_INTERNAL_ERROR`. Frames sent before admission wait for it.
|
|
449
|
+
* - A revoked global grant of a role `required_roles` lists closes the
|
|
450
|
+
* account's sockets on this endpoint with `WS_CLOSE_PERMISSION_REVOKED`,
|
|
451
|
+
* through `create_ws_auth_guard`; nothing more is sent on them.
|
|
416
452
|
* - A socket the server has closed dispatches nothing more, whether or not
|
|
417
453
|
* its client answers the close.
|
|
418
454
|
* - Every frame sent — responses, error frames, notifications — goes through
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"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"}
|