@fuzdev/fuz_app 0.140.0 → 0.142.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 +72 -22
- 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 +44 -4
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +33 -3
- package/dist/actions/perform_action.d.ts +10 -0
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +23 -9
- 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 +10 -4
- 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 +44 -22
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +12 -1
- 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 +12 -0
- package/dist/auth/account_routes.d.ts +7 -2
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +240 -187
- package/dist/auth/admin_action_specs.d.ts +6 -1
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +6 -1
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +14 -2
- 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/bootstrap_routes.d.ts.map +1 -1
- package/dist/auth/bootstrap_routes.js +85 -76
- package/dist/auth/cell_action_specs.d.ts +7 -3
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +9 -5
- package/dist/auth/cell_actions.d.ts +3 -3
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +67 -38
- package/dist/auth/cell_authorize.d.ts +69 -8
- package/dist/auth/cell_authorize.d.ts.map +1 -1
- package/dist/auth/cell_authorize.js +89 -9
- package/dist/auth/cell_field_action_specs.d.ts +5 -4
- package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_field_action_specs.js +7 -7
- package/dist/auth/cell_field_actions.d.ts +7 -5
- package/dist/auth/cell_field_actions.d.ts.map +1 -1
- package/dist/auth/cell_field_actions.js +24 -22
- package/dist/auth/cell_grant_action_specs.d.ts +6 -0
- package/dist/auth/cell_grant_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_grant_action_specs.js +6 -0
- package/dist/auth/cell_grant_actions.d.ts +3 -1
- package/dist/auth/cell_grant_actions.d.ts.map +1 -1
- package/dist/auth/cell_grant_actions.js +16 -2
- package/dist/auth/cell_item_action_specs.d.ts +5 -4
- package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_item_action_specs.js +7 -7
- package/dist/auth/cell_item_actions.d.ts +6 -5
- package/dist/auth/cell_item_actions.d.ts.map +1 -1
- package/dist/auth/cell_item_actions.js +23 -22
- package/dist/auth/cell_relation_list_limit.d.ts +20 -0
- package/dist/auth/cell_relation_list_limit.d.ts.map +1 -0
- package/dist/auth/cell_relation_list_limit.js +27 -0
- package/dist/auth/cell_relation_visibility.d.ts +4 -3
- package/dist/auth/cell_relation_visibility.d.ts.map +1 -1
- package/dist/auth/cell_relation_visibility.js +6 -5
- package/dist/auth/cell_steward.d.ts +45 -0
- package/dist/auth/cell_steward.d.ts.map +1 -0
- package/dist/auth/cell_steward.js +53 -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 +8 -2
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +20 -4
- 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/role_grant_offer_action_specs.d.ts +5 -0
- package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_action_specs.js +6 -1
- package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_actions.js +23 -16
- package/dist/auth/role_grant_offer_queries.d.ts +8 -6
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +16 -8
- 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/auth/signup_routes.d.ts +4 -3
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +157 -134
- package/dist/auth/token_scope.d.ts +7 -1
- package/dist/auth/token_scope.d.ts.map +1 -1
- package/dist/auth/token_scope.js +8 -2
- package/dist/db/CLAUDE.md +32 -19
- package/dist/db/cell_ddl.d.ts +1 -2
- package/dist/db/cell_ddl.d.ts.map +1 -1
- package/dist/db/cell_ddl.js +1 -2
- package/dist/db/cell_field_queries.d.ts +20 -18
- package/dist/db/cell_field_queries.d.ts.map +1 -1
- package/dist/db/cell_field_queries.js +28 -22
- package/dist/db/cell_item_queries.d.ts +20 -14
- package/dist/db/cell_item_queries.d.ts.map +1 -1
- package/dist/db/cell_item_queries.js +28 -18
- package/dist/db/cell_moderation.d.ts +67 -0
- package/dist/db/cell_moderation.d.ts.map +1 -0
- package/dist/db/cell_moderation.js +66 -0
- package/dist/db/cell_queries.d.ts +44 -14
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +68 -45
- package/dist/db/cell_view_sql.d.ts +102 -0
- package/dist/db/cell_view_sql.d.ts.map +1 -0
- package/dist/db/cell_view_sql.js +112 -0
- package/dist/db/db_pg.d.ts +1 -1
- package/dist/db/db_pg.js +1 -1
- 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/auth_shape.d.ts +5 -2
- package/dist/http/auth_shape.d.ts.map +1 -1
- package/dist/http/auth_shape.js +5 -2
- package/dist/http/ip_canonical.d.ts +30 -3
- package/dist/http/ip_canonical.d.ts.map +1 -1
- package/dist/http/ip_canonical.js +50 -3
- 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/primitive_schemas.d.ts +10 -1
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +11 -2
- package/dist/rate_limiter.d.ts +179 -12
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +352 -42
- 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 +8 -4
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +16 -6
- package/dist/testing/CLAUDE.md +164 -25
- 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/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +37 -2
- 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/cell_gated_create.d.ts +38 -0
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_gated_create.js +401 -5
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +145 -2
- 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/expected_schema.json +0 -1
- 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/login_security.d.ts +16 -0
- package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
- package/dist/testing/cross_backend/login_security.js +154 -0
- 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/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/query_shape.js +24 -2
- 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/role_grant_participation.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_participation.js +64 -0
- 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 +20 -1
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +23 -1
- 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_action_rate_limit.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_action_rate_limit.js +21 -1
- 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/schema_introspect.js +1 -1
- package/dist/testing/schema_parity.d.ts +4 -3
- package/dist/testing/schema_parity.d.ts.map +1 -1
- package/dist/testing/schema_parity.js +4 -3
- package/dist/testing/schema_ready_fixture.d.ts +2 -4
- package/dist/testing/schema_ready_fixture.d.ts.map +1 -1
- package/dist/testing/schema_ready_fixture.js +2 -4
- 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/dist/ui/auth_state.svelte.d.ts +10 -0
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +22 -4
- package/package.json +1 -1
- package/dist/db/cell_history_ddl.d.ts +0 -38
- package/dist/db/cell_history_ddl.d.ts.map +0 -1
- package/dist/db/cell_history_ddl.js +0 -61
|
@@ -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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"}
|
|
@@ -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
|
*
|
|
@@ -96,8 +114,9 @@
|
|
|
96
114
|
*/
|
|
97
115
|
import { wait } from '@fuzdev/fuz_util/async.ts';
|
|
98
116
|
import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
99
|
-
import { get_request_context, require_request_context, token_scope_surface_denial } from '../auth/request_context.js';
|
|
117
|
+
import { get_request_context, has_any_scoped_role, refresh_role_grants, require_request_context, token_scope_surface_denial } from '../auth/request_context.js';
|
|
100
118
|
import { get_resolved_auth, revalidate_resolved_auth } from '../auth/resolved_auth.js';
|
|
119
|
+
import { schedule_credential_deadline } from '../auth/credential_deadline.js';
|
|
101
120
|
import { get_client_ip } from '../http/client_ip.js';
|
|
102
121
|
import { flush_pending_effects, flush_post_commit_effects } from '../http/pending_effects.js';
|
|
103
122
|
import { JsonrpcRequest } from '../http/jsonrpc.js';
|
|
@@ -107,7 +126,7 @@ import { TOKEN_SCOPE_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
|
|
|
107
126
|
import {} from './action_types.js';
|
|
108
127
|
import { compile_action_registry } from './compile_action_registry.js';
|
|
109
128
|
import { cancel_action_spec, CancelNotificationParams } from './cancel.js';
|
|
110
|
-
import { DEFAULT_WS_MAX_MESSAGE_BYTES, WS_CLOSE_INTERNAL_ERROR, WS_CLOSE_MESSAGE_TOO_BIG, WS_CLOSE_POLICY_VIOLATION, WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON, utf8_length, utf8_length_over } from './transports.js';
|
|
129
|
+
import { DEFAULT_WS_MAX_MESSAGE_BYTES, WS_CLOSE_GOING_AWAY, WS_CLOSE_INTERNAL_ERROR, WS_CLOSE_MESSAGE_TOO_BIG, WS_CLOSE_PERMISSION_REVOKED, WS_CLOSE_PERMISSION_REVOKED_REASON, WS_CLOSE_POLICY_VIOLATION, WS_CLOSE_CREDENTIAL_EXPIRED_REASON, WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON, utf8_length, utf8_length_over } from './transports.js';
|
|
111
130
|
import { BackendWebsocketTransport } from './transports_ws_backend.js';
|
|
112
131
|
import { audit_unmatched_peer_response } from './peer_request.js';
|
|
113
132
|
import { perform_action, perform_action_result_to_envelope } from './perform_action.js';
|
|
@@ -247,8 +266,12 @@ const is_peer_response = (json) => {
|
|
|
247
266
|
* on the audit row.
|
|
248
267
|
* - Admission re-reads the credential once, after the handshake: a socket
|
|
249
268
|
* whose session or token was revoked while it was upgrading closes with
|
|
250
|
-
* `WS_CLOSE_SESSION_REVOKED`,
|
|
269
|
+
* `WS_CLOSE_SESSION_REVOKED`, one that lost a role `required_roles` lists
|
|
270
|
+
* with `WS_CLOSE_PERMISSION_REVOKED`, and one whose re-check failed with
|
|
251
271
|
* `WS_CLOSE_INTERNAL_ERROR`. Frames sent before admission wait for it.
|
|
272
|
+
* - A revoked global grant of a role `required_roles` lists closes the
|
|
273
|
+
* account's sockets on this endpoint with `WS_CLOSE_PERMISSION_REVOKED`,
|
|
274
|
+
* through `create_ws_auth_guard`; nothing more is sent on them.
|
|
252
275
|
* - A socket the server has closed dispatches nothing more, whether or not
|
|
253
276
|
* its client answers the close.
|
|
254
277
|
* - Every frame sent — responses, error frames, notifications — goes through
|
|
@@ -279,7 +302,7 @@ export const register_action_ws = (options) => {
|
|
|
279
302
|
throw new Error(`register_action_ws: outbound_queue_bytes configures the transport created for ${options.path}, ` +
|
|
280
303
|
'but a transport was supplied — set the budget on that BackendWebsocketTransport instead');
|
|
281
304
|
}
|
|
282
|
-
const { path, app, upgradeWebSocket, actions, db, heartbeat = true, artificial_delay = 0, max_message_bytes = DEFAULT_WS_MAX_MESSAGE_BYTES, log = new Logger('[ws]'), on_socket_open, on_socket_close, action_ip_rate_limiter, action_account_rate_limiter } = options;
|
|
305
|
+
const { path, app, upgradeWebSocket, actions, db, heartbeat = true, artificial_delay = 0, max_message_bytes = DEFAULT_WS_MAX_MESSAGE_BYTES, log = new Logger('[ws]'), on_socket_open, on_socket_close, action_ip_rate_limiter, action_account_rate_limiter, required_roles = [] } = options;
|
|
283
306
|
const transport = options.transport ??
|
|
284
307
|
new BackendWebsocketTransport({
|
|
285
308
|
max_connections_per_account: options.max_connections_per_account,
|
|
@@ -402,7 +425,12 @@ export const register_action_ws = (options) => {
|
|
|
402
425
|
// read it — by the time the hook runs the transport record is gone,
|
|
403
426
|
// removed in `onClose` or earlier by a revocation, a connection-cap
|
|
404
427
|
// eviction, or another server-side close.
|
|
405
|
-
const identity = {
|
|
428
|
+
const identity = {
|
|
429
|
+
token_hash,
|
|
430
|
+
account_id,
|
|
431
|
+
api_token_id,
|
|
432
|
+
required_roles
|
|
433
|
+
};
|
|
406
434
|
// Captured on open, consumed on close. Undefined before onOpen
|
|
407
435
|
// fires or when a consumer never opens (e.g. immediate disconnect).
|
|
408
436
|
let captured_connection_id;
|
|
@@ -420,6 +448,10 @@ export const register_action_ws = (options) => {
|
|
|
420
448
|
// Whether the connection was ever admitted — gates `on_socket_close`,
|
|
421
449
|
// the counterpart of an `on_socket_open` a refused socket never ran.
|
|
422
450
|
let was_admitted = false;
|
|
451
|
+
// The code the transport closed the pending registration with, taken
|
|
452
|
+
// before `onClose` removes it: the client's answer to that close can
|
|
453
|
+
// arrive before the re-read returns.
|
|
454
|
+
let pending_close_code = null;
|
|
423
455
|
// Frames received while `opening`, in arrival order — dispatched once
|
|
424
456
|
// the socket is open, dropped if it ends first. Each carries the
|
|
425
457
|
// settlers of the promise its `onMessage` call returned, so a caller
|
|
@@ -445,12 +477,22 @@ export const register_action_ws = (options) => {
|
|
|
445
477
|
heartbeat_timer = null;
|
|
446
478
|
}
|
|
447
479
|
};
|
|
480
|
+
// The credential deadline: the socket closes when the credential it
|
|
481
|
+
// upgraded on expires (4001, `WS_CLOSE_CREDENTIAL_EXPIRED_REASON`) —
|
|
482
|
+
// expiry writes nothing a revocation listener hears. Started at
|
|
483
|
+
// admission, cancelled when the socket ends.
|
|
484
|
+
let cancel_credential_deadline = null;
|
|
485
|
+
const stop_credential_deadline = () => {
|
|
486
|
+
cancel_credential_deadline?.();
|
|
487
|
+
cancel_credential_deadline = null;
|
|
488
|
+
};
|
|
448
489
|
// The one place a socket's end is recorded, whoever ended it — the
|
|
449
490
|
// transport (revocation, cap eviction), `end_socket`, or `onClose` all
|
|
450
491
|
// abort this controller.
|
|
451
492
|
socket_abort_controller.signal.addEventListener('abort', () => {
|
|
452
493
|
phase = 'ended';
|
|
453
494
|
stop_heartbeat_timer();
|
|
495
|
+
stop_credential_deadline();
|
|
454
496
|
drop_queued_frames();
|
|
455
497
|
}, { once: true });
|
|
456
498
|
// End the socket from the server side: unregister it and abort its
|
|
@@ -505,7 +547,7 @@ export const register_action_ws = (options) => {
|
|
|
505
547
|
// refusing — the socket is then already closed.
|
|
506
548
|
const admit_upgrade = async (ws) => {
|
|
507
549
|
// Pending first, synchronously: from here every revocation finds it.
|
|
508
|
-
const connection_id = transport.register_pending(ws, token_hash, account_id, api_token_id, socket_abort_controller);
|
|
550
|
+
const connection_id = transport.register_pending(ws, token_hash, account_id, api_token_id, socket_abort_controller, required_roles);
|
|
509
551
|
captured_connection_id = connection_id;
|
|
510
552
|
// Born closed — the server is shutting down, and the transport has
|
|
511
553
|
// already closed the socket with `WS_CLOSE_GOING_AWAY` — or ended
|
|
@@ -520,10 +562,16 @@ export const register_action_ws = (options) => {
|
|
|
520
562
|
}
|
|
521
563
|
if (!upgrade_preset) {
|
|
522
564
|
let credential_is_live;
|
|
565
|
+
let role_is_held = true;
|
|
523
566
|
try {
|
|
524
567
|
// pool-level `db`: the statements must see every revocation
|
|
525
568
|
// committed before they start
|
|
526
569
|
credential_is_live = await revalidate_resolved_auth({ db }, resolved);
|
|
570
|
+
// Global grants only, like the gate in front: a scoped grant
|
|
571
|
+
// never admitted this socket.
|
|
572
|
+
if (credential_is_live && required_roles.length > 0) {
|
|
573
|
+
role_is_held = has_any_scoped_role(await refresh_role_grants(upgrade_context, { db }), required_roles, null);
|
|
574
|
+
}
|
|
527
575
|
}
|
|
528
576
|
catch (error) {
|
|
529
577
|
// fail closed — never admitted unchecked. During shutdown the
|
|
@@ -533,7 +581,7 @@ export const register_action_ws = (options) => {
|
|
|
533
581
|
log.info('ws upgrade admission: re-check ended by the shutdown', error);
|
|
534
582
|
}
|
|
535
583
|
else {
|
|
536
|
-
log.error('ws upgrade admission: credential re-check failed', error);
|
|
584
|
+
log.error('ws upgrade admission: credential or role re-check failed', error);
|
|
537
585
|
}
|
|
538
586
|
refuse_upgrade(ws, WS_CLOSE_INTERNAL_ERROR, 'internal error');
|
|
539
587
|
return null;
|
|
@@ -543,16 +591,24 @@ export const register_action_ws = (options) => {
|
|
|
543
591
|
refuse_upgrade(ws, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON);
|
|
544
592
|
return null;
|
|
545
593
|
}
|
|
594
|
+
if (!role_is_held) {
|
|
595
|
+
log.info('ws upgrade admission: role revoked during the upgrade', connection_id, account_id);
|
|
596
|
+
refuse_upgrade(ws, WS_CLOSE_PERMISSION_REVOKED, WS_CLOSE_PERMISSION_REVOKED_REASON);
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
546
599
|
}
|
|
547
600
|
// The cap's eviction happens here, after every gate and the re-read,
|
|
548
601
|
// so a refused upgrade never closes someone else's socket.
|
|
549
602
|
if (!transport.admit(connection_id)) {
|
|
550
|
-
//
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
603
|
+
// why the entry closed, from the code the transport closed it with
|
|
604
|
+
const code = pending_close_code ?? transport.take_pending_close_code(connection_id);
|
|
605
|
+
log.info(code === WS_CLOSE_PERMISSION_REVOKED
|
|
606
|
+
? 'ws upgrade admission: closed by a permission revocation while pending'
|
|
607
|
+
: code === WS_CLOSE_SESSION_REVOKED
|
|
608
|
+
? 'ws upgrade admission: closed by a credential revocation while pending'
|
|
609
|
+
: code === WS_CLOSE_GOING_AWAY || transport.is_closing()
|
|
610
|
+
? 'ws upgrade admission: closed by the shutdown while pending'
|
|
611
|
+
: 'ws upgrade admission: ended while pending', connection_id, account_id);
|
|
556
612
|
refuse_upgrade(ws, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON);
|
|
557
613
|
return null;
|
|
558
614
|
}
|
|
@@ -564,6 +620,14 @@ export const register_action_ws = (options) => {
|
|
|
564
620
|
if (connection_id === null)
|
|
565
621
|
return;
|
|
566
622
|
log.debug('ws opened', connection_id);
|
|
623
|
+
// Not on a socket that ended in the tick since `admit`, for the same
|
|
624
|
+
// reason as the heartbeat timer below.
|
|
625
|
+
if (!phase_is('ended')) {
|
|
626
|
+
cancel_credential_deadline = schedule_credential_deadline(resolved.credential_deadline, () => {
|
|
627
|
+
log.info('ws credential expired — closing', WS_CLOSE_SESSION_REVOKED, connection_id, identity.account_id);
|
|
628
|
+
end_socket(ws, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_CREDENTIAL_EXPIRED_REASON);
|
|
629
|
+
});
|
|
630
|
+
}
|
|
567
631
|
// Not on a socket that ended in the tick since `admit` (a revocation
|
|
568
632
|
// can land there): its abort listener has already run, so nothing
|
|
569
633
|
// would ever stop the timer.
|
|
@@ -894,8 +958,12 @@ export const register_action_ws = (options) => {
|
|
|
894
958
|
// the hook is awaited: the socket is gone, so a slow hook must not
|
|
895
959
|
// keep a dead entry that broadcasts still target and the
|
|
896
960
|
// per-account cap still counts.
|
|
897
|
-
if (captured_connection_id)
|
|
961
|
+
if (captured_connection_id) {
|
|
962
|
+
if (!was_admitted) {
|
|
963
|
+
pending_close_code = transport.take_pending_close_code(captured_connection_id);
|
|
964
|
+
}
|
|
898
965
|
transport.remove_connection(captured_connection_id);
|
|
966
|
+
}
|
|
899
967
|
if (on_socket_close && captured_connection_id && was_admitted) {
|
|
900
968
|
try {
|
|
901
969
|
await on_socket_close({
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
* Multi-actor accounts must supply `?acting` to pick a persona;
|
|
23
23
|
* single-actor accounts work without it.
|
|
24
24
|
* 5. Optional `require_role(required_roles)` — for endpoints gated to a
|
|
25
|
-
* non-empty any-of set of roles.
|
|
25
|
+
* non-empty any-of set of roles. `register_action_ws` records the roles on
|
|
26
|
+
* each registration and re-reads them at admission, so the gate holds for
|
|
27
|
+
* the socket's life (a revoked role closes it with
|
|
28
|
+
* `WS_CLOSE_PERMISSION_REVOKED`).
|
|
26
29
|
*
|
|
27
30
|
* Then delegates to `register_action_ws` for per-message JSON-RPC
|
|
28
31
|
* dispatch.
|
|
@@ -41,12 +44,18 @@ export interface RegisterWsEndpointOptions extends RegisterActionWsOptions {
|
|
|
41
44
|
allowed_origins: ReadonlyArray<RegExp>;
|
|
42
45
|
/**
|
|
43
46
|
* Roles permitted to upgrade — any-of disjunction (matches the
|
|
44
|
-
* underlying `require_role` semantics). Omit (or pass
|
|
45
|
-
* authenticated account (`require_auth` + actor resolution
|
|
46
|
-
* set to e.g. `[ROLE_ADMIN]` to gate the endpoint behind a single
|
|
47
|
-
* or `[ROLE_ADMIN, ROLE_KEEPER]` to permit either. The per-action
|
|
47
|
+
* underlying `require_role` semantics, global grants only). Omit (or pass
|
|
48
|
+
* `[]`) for any authenticated account (`require_auth` + actor resolution
|
|
49
|
+
* alone); set to e.g. `[ROLE_ADMIN]` to gate the endpoint behind a single
|
|
50
|
+
* role or `[ROLE_ADMIN, ROLE_KEEPER]` to permit either. The per-action
|
|
48
51
|
* `auth` in each spec still applies at dispatch time — this is a coarse
|
|
49
|
-
* upgrade-time gate
|
|
52
|
+
* upgrade-time gate on who may hold a socket, and so on who receives
|
|
53
|
+
* the endpoint's server pushes.
|
|
54
|
+
*
|
|
55
|
+
* It holds for the socket's life: `register_action_ws` re-reads it at
|
|
56
|
+
* admission, and a revoked global grant of a listed role closes the
|
|
57
|
+
* account's sockets here with `WS_CLOSE_PERMISSION_REVOKED` (any listed
|
|
58
|
+
* role — see `close_sockets_for_role` on the over-close).
|
|
50
59
|
*/
|
|
51
60
|
required_roles?: ReadonlyArray<RoleName>;
|
|
52
61
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register_ws_endpoint.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_ws_endpoint.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"register_ws_endpoint.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_ws_endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAcH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAGvD,OAAO,EAEN,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,0CAA0C;AAC1C,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACzE;;;;OAIG;IACH,eAAe,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC;;;;;;;;;;;;;;OAcG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;CACzC;AA6CD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,oBAAoB,GAChC,SAAS,yBAAyB,KAChC,sBAoBF,CAAC"}
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
* Multi-actor accounts must supply `?acting` to pick a persona;
|
|
23
23
|
* single-actor accounts work without it.
|
|
24
24
|
* 5. Optional `require_role(required_roles)` — for endpoints gated to a
|
|
25
|
-
* non-empty any-of set of roles.
|
|
25
|
+
* non-empty any-of set of roles. `register_action_ws` records the roles on
|
|
26
|
+
* each registration and re-reads them at admission, so the gate holds for
|
|
27
|
+
* the socket's life (a revoked role closes it with
|
|
28
|
+
* `WS_CLOSE_PERMISSION_REVOKED`).
|
|
26
29
|
*
|
|
27
30
|
* Then delegates to `register_action_ws` for per-message JSON-RPC
|
|
28
31
|
* dispatch.
|
|
@@ -32,6 +35,7 @@
|
|
|
32
35
|
import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
33
36
|
import { apply_authorization_phase, REQUEST_CONTEXT_KEY, require_auth, require_role, require_token_scope } from '../auth/request_context.js';
|
|
34
37
|
import { verify_request_source } from '../http/origin.js';
|
|
38
|
+
import { parse_acting } from '../http/auth_shape.js';
|
|
35
39
|
import { ACCOUNT_ID_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
|
|
36
40
|
import { register_action_ws } from './register_action_ws.js';
|
|
37
41
|
/** Synthesized auth shape for WS upgrade: account + actor both required. */
|
|
@@ -59,9 +63,11 @@ const create_ws_authorization_middleware = (db) => {
|
|
|
59
63
|
await next();
|
|
60
64
|
return;
|
|
61
65
|
}
|
|
62
|
-
|
|
66
|
+
// through the `ActingActor` schema like every other selector: lowercased,
|
|
67
|
+
// and a malformed value reads as omitted (`parse_acting`)
|
|
68
|
+
const acting = parse_acting(c.req.query('acting'));
|
|
63
69
|
const account_id = c.get(ACCOUNT_ID_KEY) ?? null;
|
|
64
|
-
const result = await apply_authorization_phase({ db }, account_id, WS_UPGRADE_AUTH,
|
|
70
|
+
const result = await apply_authorization_phase({ db }, account_id, WS_UPGRADE_AUTH, acting);
|
|
65
71
|
if (!result.ok)
|
|
66
72
|
return c.json(result.body, result.status);
|
|
67
73
|
if (result.request_context !== null) {
|
|
@@ -99,5 +105,5 @@ export const register_ws_endpoint = (options) => {
|
|
|
99
105
|
if (required_roles?.length) {
|
|
100
106
|
app.use(path, require_role(required_roles));
|
|
101
107
|
}
|
|
102
|
-
return register_action_ws({ app, path, db, log, ...rest });
|
|
108
|
+
return register_action_ws({ app, path, db, log, required_roles, ...rest });
|
|
103
109
|
};
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* Drop into any SvelteKit frontend as the underlying connection for
|
|
6
6
|
* `FrontendWebsocketTransport`. Handles auto-reconnect with exponential
|
|
7
7
|
* backoff, respects `WS_CLOSE_SESSION_REVOKED` (no reconnect loop after the
|
|
8
|
-
* server revokes auth)
|
|
8
|
+
* server revokes auth), `WS_CLOSE_CONNECTION_LIMIT` (no reconnect after the
|
|
9
9
|
* server closes this socket to admit a newer one — closed until the user
|
|
10
|
-
* acts),
|
|
10
|
+
* acts), and `WS_CLOSE_PERMISSION_REVOKED` (no reconnect after the account
|
|
11
|
+
* lost the role the endpoint requires — closed until the user acts), exposes
|
|
12
|
+
* reactive status for UI indicators, and ships three
|
|
11
13
|
* correctness primitives default-on:
|
|
12
14
|
*
|
|
13
15
|
* - `FrontendWebsocketClient.request` — promise-based JSON-RPC with
|
|
@@ -76,7 +78,7 @@ export declare const DEFAULT_QUEUE_MAX_SIZE = 100;
|
|
|
76
78
|
* - `reconnecting` — close fired; waiting out backoff before next attempt.
|
|
77
79
|
* - `closed` — socket is not open, and nothing is scheduled to reopen it.
|
|
78
80
|
* Terminal only when `revoked` is `true`; otherwise `connect()` reopens,
|
|
79
|
-
* including after a `superseded` close.
|
|
81
|
+
* including after a `superseded` or permission-revoked close.
|
|
80
82
|
*/
|
|
81
83
|
export type SocketStatus = 'initial' | 'connecting' | 'connected' | 'reconnecting' | 'closed';
|
|
82
84
|
export type SocketMessageHandler = (event: MessageEvent) => void;
|
|
@@ -153,6 +155,13 @@ export interface FrontendWebsocketClientOptions {
|
|
|
153
155
|
* closed with `superseded` set and no reconnect scheduled — the server closed
|
|
154
156
|
* this socket to admit a newer one on the same account, and reconnecting
|
|
155
157
|
* would close that one in turn. An explicit `connect()` reopens.
|
|
158
|
+
*
|
|
159
|
+
* A permission-revoked close (`WS_CLOSE_PERMISSION_REVOKED`) leaves the client
|
|
160
|
+
* closed with no reconnect scheduled, and neither `revoked` nor `superseded`
|
|
161
|
+
* set: the credential is still good, but the account lost the role the
|
|
162
|
+
* endpoint requires, so a reconnect would be refused at its role gate. Pending
|
|
163
|
+
* and queued requests reject `forbidden`. `last_close_code` says why; an
|
|
164
|
+
* explicit `connect()` reopens, for when the role has been granted again.
|
|
156
165
|
*/
|
|
157
166
|
export declare class FrontendWebsocketClient implements WebsocketConnection, Disposable {
|
|
158
167
|
#private;
|
|
@@ -339,6 +348,8 @@ export declare class FrontendWebsocketClient implements WebsocketConnection, Dis
|
|
|
339
348
|
* - `queue_overflow` — durable queue full
|
|
340
349
|
* - `service_unavailable` — socket not connected / closed / torn down
|
|
341
350
|
* mid-flight, including a `superseded` close
|
|
351
|
+
* - `forbidden` — in flight or queued at a `WS_CLOSE_PERMISSION_REVOKED`
|
|
352
|
+
* close
|
|
342
353
|
* - `internal_error` — `ws.send` threw (serialization, buffer full)
|
|
343
354
|
* - server's wire code verbatim — JSON-RPC error frame from peer
|
|
344
355
|
*/
|
|
@@ -361,7 +372,11 @@ export declare class FrontendWebsocketClient implements WebsocketConnection, Dis
|
|
|
361
372
|
* an explicit `connect()` reopens it; read `FrontendWebsocketClient.superseded`
|
|
362
373
|
* to tell the user why, and before connecting on an `initial` status without
|
|
363
374
|
* the user asking (connecting a superseded client on its own closes a newer
|
|
364
|
-
* socket in turn, which is the loop the close code exists to stop).
|
|
375
|
+
* socket in turn, which is the loop the close code exists to stop). A
|
|
376
|
+
* `WS_CLOSE_PERMISSION_REVOKED` close reads as `initial` the same way, with
|
|
377
|
+
* neither flag set: check `FrontendWebsocketClient.last_close_code` before
|
|
378
|
+
* connecting on your own, since without the role the upgrade is refused at
|
|
379
|
+
* the gate.
|
|
365
380
|
*
|
|
366
381
|
* @param revoked - whether the session has been permanently revoked
|
|
367
382
|
* (typically `FrontendWebsocketClient.revoked`)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"socket.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/socket.svelte.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"socket.svelte.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/socket.svelte.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* Drop into any SvelteKit frontend as the underlying connection for
|
|
6
6
|
* `FrontendWebsocketTransport`. Handles auto-reconnect with exponential
|
|
7
7
|
* backoff, respects `WS_CLOSE_SESSION_REVOKED` (no reconnect loop after the
|
|
8
|
-
* server revokes auth)
|
|
8
|
+
* server revokes auth), `WS_CLOSE_CONNECTION_LIMIT` (no reconnect after the
|
|
9
9
|
* server closes this socket to admit a newer one — closed until the user
|
|
10
|
-
* acts),
|
|
10
|
+
* acts), and `WS_CLOSE_PERMISSION_REVOKED` (no reconnect after the account
|
|
11
|
+
* lost the role the endpoint requires — closed until the user acts), exposes
|
|
12
|
+
* reactive status for UI indicators, and ships three
|
|
11
13
|
* correctness primitives default-on:
|
|
12
14
|
*
|
|
13
15
|
* - `FrontendWebsocketClient.request` — promise-based JSON-RPC with
|
|
@@ -29,7 +31,7 @@
|
|
|
29
31
|
import { BROWSER } from 'esm-env';
|
|
30
32
|
import { JSONRPC_VERSION } from '../http/jsonrpc.js';
|
|
31
33
|
import { JSONRPC_ERROR_CODES, ThrownJsonrpcError, jsonrpc_errors } from '../http/jsonrpc_errors.js';
|
|
32
|
-
import { WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT, WS_CLOSE_CONNECTION_LIMIT, WS_CLOSE_SESSION_REVOKED } from './transports.js';
|
|
34
|
+
import { WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT, WS_CLOSE_CONNECTION_LIMIT, WS_CLOSE_PERMISSION_REVOKED, WS_CLOSE_SESSION_REVOKED } from './transports.js';
|
|
33
35
|
import { cancel_action_spec } from './cancel.js';
|
|
34
36
|
import { heartbeat_action_spec } from './heartbeat.js';
|
|
35
37
|
/** Default WebSocket close code (normal closure). */
|
|
@@ -84,6 +86,13 @@ export const DEFAULT_QUEUE_MAX_SIZE = 100;
|
|
|
84
86
|
* closed with `superseded` set and no reconnect scheduled — the server closed
|
|
85
87
|
* this socket to admit a newer one on the same account, and reconnecting
|
|
86
88
|
* would close that one in turn. An explicit `connect()` reopens.
|
|
89
|
+
*
|
|
90
|
+
* A permission-revoked close (`WS_CLOSE_PERMISSION_REVOKED`) leaves the client
|
|
91
|
+
* closed with no reconnect scheduled, and neither `revoked` nor `superseded`
|
|
92
|
+
* set: the credential is still good, but the account lost the role the
|
|
93
|
+
* endpoint requires, so a reconnect would be refused at its role gate. Pending
|
|
94
|
+
* and queued requests reject `forbidden`. `last_close_code` says why; an
|
|
95
|
+
* explicit `connect()` reopens, for when the role has been granted again.
|
|
87
96
|
*/
|
|
88
97
|
export class FrontendWebsocketClient {
|
|
89
98
|
#url;
|
|
@@ -423,6 +432,8 @@ export class FrontendWebsocketClient {
|
|
|
423
432
|
* - `queue_overflow` — durable queue full
|
|
424
433
|
* - `service_unavailable` — socket not connected / closed / torn down
|
|
425
434
|
* mid-flight, including a `superseded` close
|
|
435
|
+
* - `forbidden` — in flight or queued at a `WS_CLOSE_PERMISSION_REVOKED`
|
|
436
|
+
* close
|
|
426
437
|
* - `internal_error` — `ws.send` threw (serialization, buffer full)
|
|
427
438
|
* - server's wire code verbatim — JSON-RPC error frame from peer
|
|
428
439
|
*/
|
|
@@ -743,6 +754,18 @@ export class FrontendWebsocketClient {
|
|
|
743
754
|
this.#reject_all('connection superseded', jsonrpc_errors.service_unavailable);
|
|
744
755
|
return;
|
|
745
756
|
}
|
|
757
|
+
// The account lost a role the endpoint requires. Reconnecting would be
|
|
758
|
+
// refused at the role gate, so stay closed until `connect()` is called.
|
|
759
|
+
// Not a revocation: the credential is still good, and nothing will
|
|
760
|
+
// reopen the socket on its own, so the queue is rejected with the pending
|
|
761
|
+
// requests.
|
|
762
|
+
if (event.code === WS_CLOSE_PERMISSION_REVOKED) {
|
|
763
|
+
this.status = 'closed';
|
|
764
|
+
this.#cancel_reconnect();
|
|
765
|
+
this.#reset_reconnect_counters();
|
|
766
|
+
this.#reject_all('permission revoked', jsonrpc_errors.forbidden);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
746
769
|
// Pending in-flight requests can't be correlated post-reconnect; reject
|
|
747
770
|
// them. Queue stays so the flush on reopen replays unsent work.
|
|
748
771
|
this.#reject_pending_only(`connection closed (code ${event.code})`, jsonrpc_errors.service_unavailable);
|
|
@@ -835,7 +858,11 @@ export class FrontendWebsocketClient {
|
|
|
835
858
|
* an explicit `connect()` reopens it; read `FrontendWebsocketClient.superseded`
|
|
836
859
|
* to tell the user why, and before connecting on an `initial` status without
|
|
837
860
|
* the user asking (connecting a superseded client on its own closes a newer
|
|
838
|
-
* socket in turn, which is the loop the close code exists to stop).
|
|
861
|
+
* socket in turn, which is the loop the close code exists to stop). A
|
|
862
|
+
* `WS_CLOSE_PERMISSION_REVOKED` close reads as `initial` the same way, with
|
|
863
|
+
* neither flag set: check `FrontendWebsocketClient.last_close_code` before
|
|
864
|
+
* connecting on your own, since without the role the upgrade is refused at
|
|
865
|
+
* the gate.
|
|
839
866
|
*
|
|
840
867
|
* @param revoked - whether the session has been permanently revoked
|
|
841
868
|
* (typically `FrontendWebsocketClient.revoked`)
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
import type { JsonrpcMessageFromClientToServer, JsonrpcMessageFromServerToClient, JsonrpcNotification, JsonrpcRequest, JsonrpcResponseOrError, JsonrpcErrorResponse } from '../http/jsonrpc.js';
|
|
11
11
|
/**
|
|
12
|
-
* WebSocket close code for session revocation
|
|
12
|
+
* WebSocket close code for session revocation — and for a credential that
|
|
13
|
+
* expired while the socket was open (`WS_CLOSE_CREDENTIAL_EXPIRED_REASON`).
|
|
13
14
|
*
|
|
14
15
|
* Also the close for an upgrade refused at admission because its credential
|
|
15
16
|
* was revoked while the upgrade was in flight (`register_action_ws`) — the
|
|
@@ -22,6 +23,15 @@ export declare const WS_CLOSE_SESSION_REVOKED = 4001;
|
|
|
22
23
|
* indistinguishable on the wire. The Rust spine sends the same text.
|
|
23
24
|
*/
|
|
24
25
|
export declare const WS_CLOSE_SESSION_REVOKED_REASON = "Session revoked";
|
|
26
|
+
/**
|
|
27
|
+
* The close reason sent with `WS_CLOSE_SESSION_REVOKED` when the socket's
|
|
28
|
+
* credential expired while it was open — its session or API token passed its
|
|
29
|
+
* expiry (`ResolvedAuth.credential_deadline`). The code is the revocation's on
|
|
30
|
+
* purpose: the client cannot reconnect on the credential either way, so
|
|
31
|
+
* `FrontendWebsocketClient` enters the same `revoked` state. The Rust spine
|
|
32
|
+
* sends the same text.
|
|
33
|
+
*/
|
|
34
|
+
export declare const WS_CLOSE_CREDENTIAL_EXPIRED_REASON = "Credential expired";
|
|
25
35
|
/** WebSocket close code — client timed out waiting for a response. */
|
|
26
36
|
export declare const WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT = 4002;
|
|
27
37
|
/** WebSocket close code — server timed out with no incoming activity. */
|
|
@@ -62,6 +72,25 @@ export declare const WS_CLOSE_OUTBOUND_OVERFLOW = 4005;
|
|
|
62
72
|
* sends the same text.
|
|
63
73
|
*/
|
|
64
74
|
export declare const WS_CLOSE_OUTBOUND_OVERFLOW_REASON = "outbound queue overflow";
|
|
75
|
+
/**
|
|
76
|
+
* WebSocket close code — the socket's account lost a role its endpoint
|
|
77
|
+
* requires (`register_ws_endpoint`'s `required_roles`): a global grant of one
|
|
78
|
+
* of those roles was revoked while the socket was open, or while its upgrade
|
|
79
|
+
* was in flight (the admission re-read, `register_action_ws`). Nothing more
|
|
80
|
+
* is sent on the socket.
|
|
81
|
+
*
|
|
82
|
+
* Distinct from `WS_CLOSE_SESSION_REVOKED`: the credential is still good, so
|
|
83
|
+
* a client must not enter its `revoked` state — but a reconnect would be
|
|
84
|
+
* refused at the role gate, so `FrontendWebsocketClient` stays closed until
|
|
85
|
+
* `connect()` is called and rejects its pending work as `forbidden`. The twin
|
|
86
|
+
* of the Rust spine's `WS_CLOSE_PERMISSION_REVOKED`.
|
|
87
|
+
*/
|
|
88
|
+
export declare const WS_CLOSE_PERMISSION_REVOKED = 4006;
|
|
89
|
+
/**
|
|
90
|
+
* The close reason sent with `WS_CLOSE_PERMISSION_REVOKED`, mid-life and at
|
|
91
|
+
* admission alike. The Rust spine sends the same text.
|
|
92
|
+
*/
|
|
93
|
+
export declare const WS_CLOSE_PERMISSION_REVOKED_REASON = "Permission revoked";
|
|
65
94
|
/**
|
|
66
95
|
* WebSocket close code — the server is shutting down (RFC 6455 §7.4.1 "Going
|
|
67
96
|
* Away"). Sent to every live socket by `close_all_sockets`, which
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transports.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EACX,gCAAgC,EAChC,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAE5B
|
|
1
|
+
{"version":3,"file":"transports.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,KAAK,EACX,gCAAgC,EAChC,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAE5B;;;;;;;GAOG;AACH,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAC7C;;;;GAIG;AACH,eAAO,MAAM,+BAA+B,oBAAoB,CAAC;AACjE;;;;;;;GAOG;AACH,eAAO,MAAM,kCAAkC,uBAAuB,CAAC;AACvE,sEAAsE;AACtE,eAAO,MAAM,iCAAiC,OAAO,CAAC;AACtD,yEAAyE;AACzE,eAAO,MAAM,iCAAiC,OAAO,CAAC;AACtD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAC9C;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAC/C;;;GAGG;AACH,eAAO,MAAM,iCAAiC,4BAA4B,CAAC;AAC3E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,2BAA2B,OAAO,CAAC;AAChD;;;GAGG;AACH,eAAO,MAAM,kCAAkC,uBAAuB,CAAC;AACvE;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,wDAAwD;AACxD,eAAO,MAAM,0BAA0B,yBAAyB,CAAC;AACjE;;;GAGG;AACH,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAC7C;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAC9C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,OAAO,CAAC;AAE5C;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,QAAc,CAAC;AAIxD;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,EAAE,WAAW,MAAM,KAAG,MAAM,GAAG,IAI3E,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,MAA8C,CAAC;AAK1F,eAAO,MAAM,aAAa,aAAa,CAAC;AACxC,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACzB,cAAc,EAAE,aAAa,CAAC;IAE9B,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/F,IAAI,CACH,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IACxC,IAAI,CACH,OAAO,EAAE,gCAAgC,EACzC,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,gCAAgC,GAAG,IAAI,CAAC,CAAC;IACpD,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,qBAAa,UAAU;;IAItB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAQ;IAE/B;;;;;OAKG;IACH,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAQ9C;;;;;OAKG;IACH,qBAAqB,CAAC,cAAc,EAAE,aAAa,GAAG,IAAI;IAM1D;;;;;;OAMG;IACH,aAAa,CAAC,cAAc,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI;IAO/D,QAAQ,IAAI,OAAO,GAAG,IAAI;IAM1B,qBAAqB,IAAI,SAAS,GAAG,IAAI;IAIzC,0BAA0B,IAAI,aAAa,GAAG,IAAI;IAIlD,qBAAqB,CAAC,cAAc,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI;CAwCtE"}
|