@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
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
/**
|
|
11
|
-
* WebSocket close code for session revocation
|
|
11
|
+
* WebSocket close code for session revocation — and for a credential that
|
|
12
|
+
* expired while the socket was open (`WS_CLOSE_CREDENTIAL_EXPIRED_REASON`).
|
|
12
13
|
*
|
|
13
14
|
* Also the close for an upgrade refused at admission because its credential
|
|
14
15
|
* was revoked while the upgrade was in flight (`register_action_ws`) — the
|
|
@@ -21,6 +22,15 @@ export const WS_CLOSE_SESSION_REVOKED = 4001;
|
|
|
21
22
|
* indistinguishable on the wire. The Rust spine sends the same text.
|
|
22
23
|
*/
|
|
23
24
|
export const WS_CLOSE_SESSION_REVOKED_REASON = 'Session revoked';
|
|
25
|
+
/**
|
|
26
|
+
* The close reason sent with `WS_CLOSE_SESSION_REVOKED` when the socket's
|
|
27
|
+
* credential expired while it was open — its session or API token passed its
|
|
28
|
+
* expiry (`ResolvedAuth.credential_deadline`). The code is the revocation's on
|
|
29
|
+
* purpose: the client cannot reconnect on the credential either way, so
|
|
30
|
+
* `FrontendWebsocketClient` enters the same `revoked` state. The Rust spine
|
|
31
|
+
* sends the same text.
|
|
32
|
+
*/
|
|
33
|
+
export const WS_CLOSE_CREDENTIAL_EXPIRED_REASON = 'Credential expired';
|
|
24
34
|
/** WebSocket close code — client timed out waiting for a response. */
|
|
25
35
|
export const WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT = 4002;
|
|
26
36
|
/** WebSocket close code — server timed out with no incoming activity. */
|
|
@@ -61,6 +71,25 @@ export const WS_CLOSE_OUTBOUND_OVERFLOW = 4005;
|
|
|
61
71
|
* sends the same text.
|
|
62
72
|
*/
|
|
63
73
|
export const WS_CLOSE_OUTBOUND_OVERFLOW_REASON = 'outbound queue overflow';
|
|
74
|
+
/**
|
|
75
|
+
* WebSocket close code — the socket's account lost a role its endpoint
|
|
76
|
+
* requires (`register_ws_endpoint`'s `required_roles`): a global grant of one
|
|
77
|
+
* of those roles was revoked while the socket was open, or while its upgrade
|
|
78
|
+
* was in flight (the admission re-read, `register_action_ws`). Nothing more
|
|
79
|
+
* is sent on the socket.
|
|
80
|
+
*
|
|
81
|
+
* Distinct from `WS_CLOSE_SESSION_REVOKED`: the credential is still good, so
|
|
82
|
+
* a client must not enter its `revoked` state — but a reconnect would be
|
|
83
|
+
* refused at the role gate, so `FrontendWebsocketClient` stays closed until
|
|
84
|
+
* `connect()` is called and rejects its pending work as `forbidden`. The twin
|
|
85
|
+
* of the Rust spine's `WS_CLOSE_PERMISSION_REVOKED`.
|
|
86
|
+
*/
|
|
87
|
+
export const WS_CLOSE_PERMISSION_REVOKED = 4006;
|
|
88
|
+
/**
|
|
89
|
+
* The close reason sent with `WS_CLOSE_PERMISSION_REVOKED`, mid-life and at
|
|
90
|
+
* admission alike. The Rust spine sends the same text.
|
|
91
|
+
*/
|
|
92
|
+
export const WS_CLOSE_PERMISSION_REVOKED_REASON = 'Permission revoked';
|
|
64
93
|
/**
|
|
65
94
|
* WebSocket close code — the server is shutting down (RFC 6455 §7.4.1 "Going
|
|
66
95
|
* Away"). Sent to every live socket by `close_all_sockets`, which
|
|
@@ -28,11 +28,16 @@
|
|
|
28
28
|
* two cannot drift into closing a revoked token's socket while leaving its
|
|
29
29
|
* stream open.
|
|
30
30
|
*
|
|
31
|
-
* `role`
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
31
|
+
* `role` closes the target account's sockets whose endpoint gated the upgrade
|
|
32
|
+
* on the revoked role (`close_sockets_for_role`), with
|
|
33
|
+
* `WS_CLOSE_PERMISSION_REVOKED`, when the revoked grant was global
|
|
34
|
+
* (`to_global_role_revocation` — a role gate admits on global grants only).
|
|
35
|
+
* `perform_action` re-authorizes every *request* against its own action, but
|
|
36
|
+
* the role gate is the only check on what the socket *receives*: broadcasts
|
|
37
|
+
* and notifications reach every admitted connection of the endpoint with no
|
|
38
|
+
* per-recipient filter, so a socket left open past the revocation would keep
|
|
39
|
+
* receiving what the gate exists to withhold. The role is the only revocation
|
|
40
|
+
* no handler closes for, so this guard is its one closer.
|
|
36
41
|
*
|
|
37
42
|
* For standard WS endpoints mounted via `AppServerOptions.ws_endpoints`,
|
|
38
43
|
* `create_app_server` registers the guard automatically per
|
|
@@ -66,7 +71,9 @@ export type AuditEventHandler = (event: AuditLogEvent) => void;
|
|
|
66
71
|
* - `account` → `close_sockets_for_account(target_account_id ?? account_id)`
|
|
67
72
|
* — `logout`, `password_change`, `session_revoke_all`, `token_revoke_all`,
|
|
68
73
|
* `account_delete`, `account_purge`
|
|
69
|
-
* - `role` →
|
|
74
|
+
* - `role` → `close_sockets_for_role(target_account_id ?? account_id,
|
|
75
|
+
* metadata.role)` when the revoked grant was global; a scoped revoke closes
|
|
76
|
+
* nothing (module doc)
|
|
70
77
|
* - `none` → nothing — every other event, and any event type fuz_app does not
|
|
71
78
|
* define
|
|
72
79
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transports_ws_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_auth_guard.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"transports_ws_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAGtD,OAAO,EAGN,KAAK,aAAa,EAClB,MAAM,6BAA6B,CAAC;AACrC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;AAE/D;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,oBAAoB,GAAI,QAAQ,gBAAgB,EAAE,KAAK,MAAM,KAAG,iBA4D5E,CAAC"}
|
|
@@ -28,11 +28,16 @@
|
|
|
28
28
|
* two cannot drift into closing a revoked token's socket while leaving its
|
|
29
29
|
* stream open.
|
|
30
30
|
*
|
|
31
|
-
* `role`
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
31
|
+
* `role` closes the target account's sockets whose endpoint gated the upgrade
|
|
32
|
+
* on the revoked role (`close_sockets_for_role`), with
|
|
33
|
+
* `WS_CLOSE_PERMISSION_REVOKED`, when the revoked grant was global
|
|
34
|
+
* (`to_global_role_revocation` — a role gate admits on global grants only).
|
|
35
|
+
* `perform_action` re-authorizes every *request* against its own action, but
|
|
36
|
+
* the role gate is the only check on what the socket *receives*: broadcasts
|
|
37
|
+
* and notifications reach every admitted connection of the endpoint with no
|
|
38
|
+
* per-recipient filter, so a socket left open past the revocation would keep
|
|
39
|
+
* receiving what the gate exists to withhold. The role is the only revocation
|
|
40
|
+
* no handler closes for, so this guard is its one closer.
|
|
36
41
|
*
|
|
37
42
|
* For standard WS endpoints mounted via `AppServerOptions.ws_endpoints`,
|
|
38
43
|
* `create_app_server` registers the guard automatically per
|
|
@@ -45,7 +50,7 @@
|
|
|
45
50
|
* @module
|
|
46
51
|
*/
|
|
47
52
|
import { UnreachableError } from '@fuzdev/fuz_util/error.ts';
|
|
48
|
-
import { to_revocation_scope } from '../auth/audit_log_schema.js';
|
|
53
|
+
import { to_global_role_revocation, to_revocation_scope } from '../auth/audit_log_schema.js';
|
|
49
54
|
/**
|
|
50
55
|
* Create an audit event handler that closes connections on a successful
|
|
51
56
|
* revocation row, dispatching on the event's `RevocationScope`:
|
|
@@ -55,7 +60,9 @@ import { to_revocation_scope } from '../auth/audit_log_schema.js';
|
|
|
55
60
|
* - `account` → `close_sockets_for_account(target_account_id ?? account_id)`
|
|
56
61
|
* — `logout`, `password_change`, `session_revoke_all`, `token_revoke_all`,
|
|
57
62
|
* `account_delete`, `account_purge`
|
|
58
|
-
* - `role` →
|
|
63
|
+
* - `role` → `close_sockets_for_role(target_account_id ?? account_id,
|
|
64
|
+
* metadata.role)` when the revoked grant was global; a scoped revoke closes
|
|
65
|
+
* nothing (module doc)
|
|
59
66
|
* - `none` → nothing — every other event, and any event type fuz_app does not
|
|
60
67
|
* define
|
|
61
68
|
*
|
|
@@ -81,10 +88,17 @@ export const create_ws_auth_guard = (closer, log) => {
|
|
|
81
88
|
switch (scope) {
|
|
82
89
|
case 'none':
|
|
83
90
|
return;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
case 'role': {
|
|
92
|
+
// global grants only — a role gate never admitted on a scoped one
|
|
93
|
+
const revoked = to_global_role_revocation(event);
|
|
94
|
+
if (!revoked)
|
|
95
|
+
return;
|
|
96
|
+
const closed = closer.close_sockets_for_role(revoked.account_id, revoked.role);
|
|
97
|
+
if (closed > 0) {
|
|
98
|
+
log.info(`WS auth guard: closed ${closed} socket(s) gated on ${revoked.role} for account ${revoked.account_id} (${event.event_type})`);
|
|
99
|
+
}
|
|
87
100
|
return;
|
|
101
|
+
}
|
|
88
102
|
case 'session': {
|
|
89
103
|
const session_id = event.metadata?.session_id;
|
|
90
104
|
if (typeof session_id !== 'string' || session_id.length === 0)
|
|
@@ -124,7 +124,10 @@ import { type PeerRequestOptions, type PeerRequestOutcome } from './peer_request
|
|
|
124
124
|
* One record per connection. `token_hash` is set for cookie-session
|
|
125
125
|
* connections, `api_token_id` for bearer (`api_token`) connections, and
|
|
126
126
|
* both are null for daemon-token connections (reachable only via
|
|
127
|
-
* `BackendWebsocketTransport.close_sockets_for_account`).
|
|
127
|
+
* `BackendWebsocketTransport.close_sockets_for_account`). `required_roles`
|
|
128
|
+
* is what the connection's endpoint gated the upgrade on, kept per
|
|
129
|
+
* registration because endpoints with different gates can share one
|
|
130
|
+
* transport.
|
|
128
131
|
*/
|
|
129
132
|
export interface ConnectionIdentity {
|
|
130
133
|
/** Blake3 session token hash, or null for non-session credentials. */
|
|
@@ -133,6 +136,11 @@ export interface ConnectionIdentity {
|
|
|
133
136
|
account_id: Uuid;
|
|
134
137
|
/** `api_token.id` for bearer-authenticated connections, else null. */
|
|
135
138
|
api_token_id: string | null;
|
|
139
|
+
/**
|
|
140
|
+
* The roles the endpoint required at upgrade (any-of), or empty for an
|
|
141
|
+
* ungated endpoint — what `close_sockets_for_role` matches on.
|
|
142
|
+
*/
|
|
143
|
+
required_roles: ReadonlyArray<string>;
|
|
136
144
|
}
|
|
137
145
|
/**
|
|
138
146
|
* Structural capability for transports that can broadcast with a
|
|
@@ -227,8 +235,9 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
|
|
|
227
235
|
*
|
|
228
236
|
* The entry is closeable at once: a `close_sockets_for_*` matching
|
|
229
237
|
* `token_hash` / `account_id` / `api_token_id` removes it, aborts
|
|
230
|
-
* `abort_controller`, and closes `ws` with `WS_CLOSE_SESSION_REVOKED
|
|
231
|
-
*
|
|
238
|
+
* `abort_controller`, and closes `ws` with `WS_CLOSE_SESSION_REVOKED`
|
|
239
|
+
* (`close_sockets_for_role`, matching `required_roles`, with
|
|
240
|
+
* `WS_CLOSE_PERMISSION_REVOKED`); the later `admit` then refuses. Until admitted it evicts nothing, is not
|
|
232
241
|
* counted toward the per-account cap, and receives no message.
|
|
233
242
|
*
|
|
234
243
|
* The caller owns the entry until it is admitted: a refusal closes it with
|
|
@@ -246,11 +255,27 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
|
|
|
246
255
|
* @param api_token_id - `api_token.id` for bearer connections, else `null`
|
|
247
256
|
* @param abort_controller - the socket-scoped controller to abort when the
|
|
248
257
|
* transport closes this connection (revocation or cap eviction)
|
|
258
|
+
* @param required_roles - the roles the endpoint gated the upgrade on, for
|
|
259
|
+
* `close_sockets_for_role`; empty for an ungated endpoint
|
|
249
260
|
* @returns the freshly assigned `connection_id` (branded `Uuid`)
|
|
250
261
|
* @mutates this - inserts an un-admitted entry into `#connections`, unless
|
|
251
262
|
* the transport is closing
|
|
252
263
|
*/
|
|
253
|
-
register_pending(ws: WSContext, token_hash: string | null, account_id: Uuid, api_token_id?: string | null, abort_controller?: AbortController | null): Uuid;
|
|
264
|
+
register_pending(ws: WSContext, token_hash: string | null, account_id: Uuid, api_token_id?: string | null, abort_controller?: AbortController | null, required_roles?: ReadonlyArray<string>): Uuid;
|
|
265
|
+
/**
|
|
266
|
+
* The close code the transport closed a still-pending registration with —
|
|
267
|
+
* `WS_CLOSE_SESSION_REVOKED` for a credential revocation,
|
|
268
|
+
* `WS_CLOSE_PERMISSION_REVOKED` for a role close, `WS_CLOSE_GOING_AWAY`
|
|
269
|
+
* for the shutdown — read once: the record is dropped by this call (and by
|
|
270
|
+
* `remove_connection`). `null` when the transport did not close it while
|
|
271
|
+
* pending. For a caller whose `admit` was refused, to say why; the twin of
|
|
272
|
+
* the reason the Rust `ConnectionRegistry::admit` refuses with.
|
|
273
|
+
*
|
|
274
|
+
* @param connection_id - the id `register_pending` returned
|
|
275
|
+
* @returns the close code, or `null`
|
|
276
|
+
* @mutates this - drops the recorded code
|
|
277
|
+
*/
|
|
278
|
+
take_pending_close_code(connection_id: Uuid): number | null;
|
|
254
279
|
/**
|
|
255
280
|
* Admit a pending connection — phase two (module doc).
|
|
256
281
|
*
|
|
@@ -294,12 +319,14 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
|
|
|
294
319
|
* connection is always admitted — unless the transport is closing: then it
|
|
295
320
|
* is born closed, as in `register_pending`, and evicts nothing.
|
|
296
321
|
*
|
|
322
|
+
* @param required_roles - the roles the endpoint gated the connection on,
|
|
323
|
+
* for `close_sockets_for_role`; empty for an ungated one
|
|
297
324
|
* @returns the freshly assigned `connection_id` (branded `Uuid`)
|
|
298
325
|
* @mutates this - inserts an admitted entry into `#connections`; past the
|
|
299
326
|
* cap, first removes the account's oldest admitted connections and closes
|
|
300
327
|
* their `WSContext`
|
|
301
328
|
*/
|
|
302
|
-
add_connection(ws: WSContext, token_hash: string | null, account_id: Uuid, api_token_id?: string | null, abort_controller?: AbortController | null): Uuid;
|
|
329
|
+
add_connection(ws: WSContext, token_hash: string | null, account_id: Uuid, api_token_id?: string | null, abort_controller?: AbortController | null, required_roles?: ReadonlyArray<string>): Uuid;
|
|
303
330
|
/**
|
|
304
331
|
* Remove a connection and its auth tracking data, pending or admitted, by
|
|
305
332
|
* the id `register_pending` / `add_connection` returned. Idempotent — safe
|
|
@@ -376,6 +403,25 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
|
|
|
376
403
|
* `WS_CLOSE_SESSION_REVOKED`
|
|
377
404
|
*/
|
|
378
405
|
close_sockets_for_token(api_token_id: string): number;
|
|
406
|
+
/**
|
|
407
|
+
* Close the sockets of `account_id` whose endpoint required `role`,
|
|
408
|
+
* including a registration still pending admission — the close for a
|
|
409
|
+
* revoked global grant of a role the upgrade gate admitted on. Each closes
|
|
410
|
+
* with `WS_CLOSE_PERMISSION_REVOKED`, not a revocation's code: the
|
|
411
|
+
* credential is still good.
|
|
412
|
+
*
|
|
413
|
+
* **Over-closes an any-of gate.** An endpoint gated on several roles admits
|
|
414
|
+
* an account holding any one of them, but this closes on the loss of any
|
|
415
|
+
* listed role, without checking whether the account still holds another.
|
|
416
|
+
* The client can reconnect when it still qualifies. The account's sockets
|
|
417
|
+
* on endpoints that did not list `role`, and on ungated ones, stay open.
|
|
418
|
+
*
|
|
419
|
+
* @returns the number of sockets closed
|
|
420
|
+
* @mutates this - removes matching connections, aborts their registered
|
|
421
|
+
* controllers, and closes their underlying `WSContext` with
|
|
422
|
+
* `WS_CLOSE_PERMISSION_REVOKED`
|
|
423
|
+
*/
|
|
424
|
+
close_sockets_for_role(account_id: Uuid, role: string): number;
|
|
379
425
|
/**
|
|
380
426
|
* Close every connection, pending or admitted, with `WS_CLOSE_GOING_AWAY` —
|
|
381
427
|
* the shutdown close `AppServer.close` runs. Not a revocation: the client
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transports_ws_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiHG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,KAAK,EAEX,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,
|
|
1
|
+
{"version":3,"file":"transports_ws_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_backend.ts"],"names":[],"mappings":"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"}
|
|
@@ -117,7 +117,7 @@ import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
|
117
117
|
import { create_uuid } from '../primitive_schemas.js';
|
|
118
118
|
import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
|
|
119
119
|
import { create_jsonrpc_error_response, create_jsonrpc_request, to_jsonrpc_message_id, is_jsonrpc_request } from '../http/jsonrpc_helpers.js';
|
|
120
|
-
import { WS_CLOSE_CONNECTION_LIMIT, WS_CLOSE_GOING_AWAY, WS_CLOSE_GOING_AWAY_REASON, WS_CLOSE_OUTBOUND_OVERFLOW, WS_CLOSE_OUTBOUND_OVERFLOW_REASON, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON, utf8_length } from './transports.js';
|
|
120
|
+
import { WS_CLOSE_CONNECTION_LIMIT, WS_CLOSE_GOING_AWAY, WS_CLOSE_GOING_AWAY_REASON, WS_CLOSE_OUTBOUND_OVERFLOW, WS_CLOSE_OUTBOUND_OVERFLOW_REASON, WS_CLOSE_PERMISSION_REVOKED, WS_CLOSE_PERMISSION_REVOKED_REASON, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON, utf8_length } from './transports.js';
|
|
121
121
|
import { PendingPeerRequests } from './peer_request.js';
|
|
122
122
|
import { WsOutboundWriter } from './ws_outbound_writer.js';
|
|
123
123
|
/** Type guard for `FilterableBroadcastTransport`. */
|
|
@@ -163,6 +163,10 @@ export class BackendWebsocketTransport {
|
|
|
163
163
|
// close arrives (`remove_connection`) or the close deadline passes — so the
|
|
164
164
|
// deadline timer is cleared when the socket goes.
|
|
165
165
|
#closing_writers = new Map();
|
|
166
|
+
// The close code each registration the transport closed while it was still
|
|
167
|
+
// pending got, until `take_pending_close_code` or `remove_connection` drops
|
|
168
|
+
// it — so a refused admission can say why it was refused.
|
|
169
|
+
#pending_close_codes = new Map();
|
|
166
170
|
// set once by `close_all_sockets`, never cleared — every later
|
|
167
171
|
// registration is born closed (module doc, "Shutdown")
|
|
168
172
|
#closing = false;
|
|
@@ -197,8 +201,9 @@ export class BackendWebsocketTransport {
|
|
|
197
201
|
*
|
|
198
202
|
* The entry is closeable at once: a `close_sockets_for_*` matching
|
|
199
203
|
* `token_hash` / `account_id` / `api_token_id` removes it, aborts
|
|
200
|
-
* `abort_controller`, and closes `ws` with `WS_CLOSE_SESSION_REVOKED
|
|
201
|
-
*
|
|
204
|
+
* `abort_controller`, and closes `ws` with `WS_CLOSE_SESSION_REVOKED`
|
|
205
|
+
* (`close_sockets_for_role`, matching `required_roles`, with
|
|
206
|
+
* `WS_CLOSE_PERMISSION_REVOKED`); the later `admit` then refuses. Until admitted it evicts nothing, is not
|
|
202
207
|
* counted toward the per-account cap, and receives no message.
|
|
203
208
|
*
|
|
204
209
|
* The caller owns the entry until it is admitted: a refusal closes it with
|
|
@@ -216,14 +221,36 @@ export class BackendWebsocketTransport {
|
|
|
216
221
|
* @param api_token_id - `api_token.id` for bearer connections, else `null`
|
|
217
222
|
* @param abort_controller - the socket-scoped controller to abort when the
|
|
218
223
|
* transport closes this connection (revocation or cap eviction)
|
|
224
|
+
* @param required_roles - the roles the endpoint gated the upgrade on, for
|
|
225
|
+
* `close_sockets_for_role`; empty for an ungated endpoint
|
|
219
226
|
* @returns the freshly assigned `connection_id` (branded `Uuid`)
|
|
220
227
|
* @mutates this - inserts an un-admitted entry into `#connections`, unless
|
|
221
228
|
* the transport is closing
|
|
222
229
|
*/
|
|
223
|
-
register_pending(ws, token_hash, account_id, api_token_id = null, abort_controller = null) {
|
|
230
|
+
register_pending(ws, token_hash, account_id, api_token_id = null, abort_controller = null, required_roles = []) {
|
|
224
231
|
if (this.#closing)
|
|
225
232
|
return this.#close_born_closed(ws, abort_controller);
|
|
226
|
-
return this.#insert(ws, token_hash, account_id, api_token_id, abort_controller, false);
|
|
233
|
+
return this.#insert(ws, { token_hash, account_id, api_token_id, required_roles }, abort_controller, false);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The close code the transport closed a still-pending registration with —
|
|
237
|
+
* `WS_CLOSE_SESSION_REVOKED` for a credential revocation,
|
|
238
|
+
* `WS_CLOSE_PERMISSION_REVOKED` for a role close, `WS_CLOSE_GOING_AWAY`
|
|
239
|
+
* for the shutdown — read once: the record is dropped by this call (and by
|
|
240
|
+
* `remove_connection`). `null` when the transport did not close it while
|
|
241
|
+
* pending. For a caller whose `admit` was refused, to say why; the twin of
|
|
242
|
+
* the reason the Rust `ConnectionRegistry::admit` refuses with.
|
|
243
|
+
*
|
|
244
|
+
* @param connection_id - the id `register_pending` returned
|
|
245
|
+
* @returns the close code, or `null`
|
|
246
|
+
* @mutates this - drops the recorded code
|
|
247
|
+
*/
|
|
248
|
+
take_pending_close_code(connection_id) {
|
|
249
|
+
const code = this.#pending_close_codes.get(connection_id);
|
|
250
|
+
if (code === undefined)
|
|
251
|
+
return null;
|
|
252
|
+
this.#pending_close_codes.delete(connection_id);
|
|
253
|
+
return code;
|
|
227
254
|
}
|
|
228
255
|
/**
|
|
229
256
|
* Admit a pending connection — phase two (module doc).
|
|
@@ -281,24 +308,26 @@ export class BackendWebsocketTransport {
|
|
|
281
308
|
* connection is always admitted — unless the transport is closing: then it
|
|
282
309
|
* is born closed, as in `register_pending`, and evicts nothing.
|
|
283
310
|
*
|
|
311
|
+
* @param required_roles - the roles the endpoint gated the connection on,
|
|
312
|
+
* for `close_sockets_for_role`; empty for an ungated one
|
|
284
313
|
* @returns the freshly assigned `connection_id` (branded `Uuid`)
|
|
285
314
|
* @mutates this - inserts an admitted entry into `#connections`; past the
|
|
286
315
|
* cap, first removes the account's oldest admitted connections and closes
|
|
287
316
|
* their `WSContext`
|
|
288
317
|
*/
|
|
289
|
-
add_connection(ws, token_hash, account_id, api_token_id = null, abort_controller = null) {
|
|
318
|
+
add_connection(ws, token_hash, account_id, api_token_id = null, abort_controller = null, required_roles = []) {
|
|
290
319
|
if (this.#closing)
|
|
291
320
|
return this.#close_born_closed(ws, abort_controller);
|
|
292
321
|
if (this.#max_connections_per_account !== null) {
|
|
293
322
|
this.#evict_oldest_for_account(account_id, this.#max_connections_per_account);
|
|
294
323
|
}
|
|
295
|
-
return this.#insert(ws, token_hash, account_id, api_token_id, abort_controller, true);
|
|
324
|
+
return this.#insert(ws, { token_hash, account_id, api_token_id, required_roles }, abort_controller, true);
|
|
296
325
|
}
|
|
297
|
-
#insert(ws,
|
|
326
|
+
#insert(ws, identity, abort_controller, admitted) {
|
|
298
327
|
const connection_id = create_uuid();
|
|
299
328
|
this.#connections.set(connection_id, {
|
|
300
329
|
ws,
|
|
301
|
-
identity
|
|
330
|
+
identity,
|
|
302
331
|
admitted,
|
|
303
332
|
abort_controller,
|
|
304
333
|
writer: new WsOutboundWriter({
|
|
@@ -333,6 +362,7 @@ export class BackendWebsocketTransport {
|
|
|
333
362
|
* requests as `connection_gone`
|
|
334
363
|
*/
|
|
335
364
|
remove_connection(connection_id) {
|
|
365
|
+
this.#pending_close_codes.delete(connection_id);
|
|
336
366
|
this.#connections.get(connection_id)?.writer.dispose();
|
|
337
367
|
// a connection the transport closed: its socket is gone now
|
|
338
368
|
const closing = this.#closing_writers.get(connection_id);
|
|
@@ -451,6 +481,27 @@ export class BackendWebsocketTransport {
|
|
|
451
481
|
close_sockets_for_token(api_token_id) {
|
|
452
482
|
return this.#close_where((id) => id.api_token_id === api_token_id);
|
|
453
483
|
}
|
|
484
|
+
/**
|
|
485
|
+
* Close the sockets of `account_id` whose endpoint required `role`,
|
|
486
|
+
* including a registration still pending admission — the close for a
|
|
487
|
+
* revoked global grant of a role the upgrade gate admitted on. Each closes
|
|
488
|
+
* with `WS_CLOSE_PERMISSION_REVOKED`, not a revocation's code: the
|
|
489
|
+
* credential is still good.
|
|
490
|
+
*
|
|
491
|
+
* **Over-closes an any-of gate.** An endpoint gated on several roles admits
|
|
492
|
+
* an account holding any one of them, but this closes on the loss of any
|
|
493
|
+
* listed role, without checking whether the account still holds another.
|
|
494
|
+
* The client can reconnect when it still qualifies. The account's sockets
|
|
495
|
+
* on endpoints that did not list `role`, and on ungated ones, stay open.
|
|
496
|
+
*
|
|
497
|
+
* @returns the number of sockets closed
|
|
498
|
+
* @mutates this - removes matching connections, aborts their registered
|
|
499
|
+
* controllers, and closes their underlying `WSContext` with
|
|
500
|
+
* `WS_CLOSE_PERMISSION_REVOKED`
|
|
501
|
+
*/
|
|
502
|
+
close_sockets_for_role(account_id, role) {
|
|
503
|
+
return this.#close_where((id) => id.account_id === account_id && id.required_roles.includes(role), WS_CLOSE_PERMISSION_REVOKED, WS_CLOSE_PERMISSION_REVOKED_REASON);
|
|
504
|
+
}
|
|
454
505
|
/**
|
|
455
506
|
* Close every connection, pending or admitted, with `WS_CLOSE_GOING_AWAY` —
|
|
456
507
|
* the shutdown close `AppServer.close` runs. Not a revocation: the client
|
|
@@ -511,6 +562,8 @@ export class BackendWebsocketTransport {
|
|
|
511
562
|
}
|
|
512
563
|
#end_connection(connection_id, entry, code, reason, backlog = 'flush') {
|
|
513
564
|
this.#cleanup_connection(connection_id);
|
|
565
|
+
if (!entry.admitted)
|
|
566
|
+
this.#pending_close_codes.set(connection_id, code);
|
|
514
567
|
// abort before the close so a handler observing `ctx.signal` bails out
|
|
515
568
|
// without waiting for the close handshake
|
|
516
569
|
entry.abort_controller?.abort();
|
|
@@ -46,10 +46,12 @@ export interface WsEndpointSpec {
|
|
|
46
46
|
*/
|
|
47
47
|
actions: ReadonlyArray<Action>;
|
|
48
48
|
/**
|
|
49
|
-
* Roles permitted to upgrade — any-of disjunction
|
|
50
|
-
* to skip the upgrade-time role gate; per-action
|
|
51
|
-
* still applies at dispatch time via
|
|
52
|
-
* `[ROLE_ADMIN]` for an admin-only WS endpoint.
|
|
49
|
+
* Roles permitted to upgrade — any-of disjunction, global grants only.
|
|
50
|
+
* Omit (or pass `[]`) to skip the upgrade-time role gate; per-action
|
|
51
|
+
* `auth` on each spec still applies at dispatch time via
|
|
52
|
+
* `perform_action`. Pass `[ROLE_ADMIN]` for an admin-only WS endpoint.
|
|
53
|
+
* Held for the socket's life — see `register_ws_endpoint`'s
|
|
54
|
+
* `required_roles`.
|
|
53
55
|
*/
|
|
54
56
|
required_roles?: ReadonlyArray<RoleName>;
|
|
55
57
|
/**
|
|
@@ -133,18 +135,17 @@ export interface WsEndpointSpec {
|
|
|
133
135
|
* Set `false` when a consumer needs to compose their own callback
|
|
134
136
|
* from scratch — or to opt out of the auto-wiring entirely.
|
|
135
137
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* wanting role-revoke disconnection use `extra_audit_handlers`.
|
|
138
|
+
* The guard is also the one closer of a role revocation: a revoked
|
|
139
|
+
* global grant of a role in `required_roles` closes the account's
|
|
140
|
+
* sockets on this endpoint with `WS_CLOSE_PERMISSION_REVOKED`. With the
|
|
141
|
+
* guard off for a transport, nothing closes them.
|
|
141
142
|
*/
|
|
142
143
|
auth_guard?: boolean;
|
|
143
144
|
/**
|
|
144
145
|
* Extra audit-event handlers registered via `deps.audit.add_listener`
|
|
145
146
|
* AFTER the standard `auth_guard` wiring (when enabled). By the time
|
|
146
147
|
* these run, the standard guard may have already closed sockets. Use
|
|
147
|
-
* for
|
|
148
|
+
* for custom analytics, a consumer's own revocation events, etc.
|
|
148
149
|
*
|
|
149
150
|
* Never deduped — consumer-owned; pass the same handler twice and it
|
|
150
151
|
* fires twice.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws_endpoint_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/ws_endpoint_spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,KAAK,EACX,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAEvE;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC9B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B
|
|
1
|
+
{"version":3,"file":"ws_endpoint_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/ws_endpoint_spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,KAAK,EACX,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAEvE;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC9B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACzC;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;;;OAMG;IACH,2BAA2B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC7C,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmCG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;OAQG;IACH,oBAAoB,CAAC,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;CACxD;AAED,yFAAyF;AACzF,MAAM,MAAM,sBAAsB,GAAG,cAAc,GAAG;IAAE,eAAe,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC;AAEjG;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAChC,cAAc,aAAa,CAAC,cAAc,CAAC,GAAG,SAAS,EACvD,iBAAiB,aAAa,CAAC,MAAM,CAAC,KACpC,KAAK,CAAC,sBAAsB,CAAC,GAAG,SAI/B,CAAC"}
|
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -35,7 +35,7 @@ over the pool so rows persist when request transactions roll back. See root
|
|
|
35
35
|
- `auth/session_cookie.ts` — `SessionOptions<T>`, `parse_session`, `process_session_cookie` (`'none' | 'clear'` — no refresh action; the 30-day lifetime is an absolute cap signed once at mint), `create_session_config`, `fuz_session_config`, `SESSION_AGE_MAX`.
|
|
36
36
|
- `auth/password.ts` — `Password`, `PasswordProvided`, `PasswordHashDeps`, `PASSWORD_LENGTH_MIN` (12, OWASP), `PASSWORD_LENGTH_MAX` (300).
|
|
37
37
|
- `auth/password_argon2.ts` — `hash_password`, `verify_password`, `verify_dummy`, `argon2_password_deps`.
|
|
38
|
-
- `auth/api_token.ts` — `API_TOKEN_PREFIX` (`secret_fuz_token_`), `hash_api_token`, `generate_api_token
|
|
38
|
+
- `auth/api_token.ts` — `API_TOKEN_PREFIX` (`secret_fuz_token_`), `hash_api_token`, `generate_api_token`, and the shape precheck `is_api_token_shaped` (`API_TOKEN_REGEX`: the prefix plus exactly `API_TOKEN_BODY_LENGTH` = 43 base64url chars).
|
|
39
39
|
- `auth/daemon_token.ts` — `DaemonToken`, `DAEMON_TOKEN_HEADER` (`X-Daemon-Token`), `generate_daemon_token`, `validate_daemon_token`, `DaemonTokenState`.
|
|
40
40
|
- `auth/bootstrap_account.ts` — `bootstrap_account` (one-shot, `bootstrap_lock`-protected).
|
|
41
41
|
|
|
@@ -74,7 +74,7 @@ Convention — `*_schema.ts` is Zod-only; `*_ddl.ts` holds DDL strings.
|
|
|
74
74
|
|
|
75
75
|
### Queries
|
|
76
76
|
|
|
77
|
-
All take `deps: QueryDeps = {db}` first; `query_validate_api_token` adds `log
|
|
77
|
+
All take `deps: QueryDeps = {db}` first; `query_validate_api_token` adds `log` and returns a `ValidatedApiToken` — the row with `scope` parsed, refusing (returning `undefined` for) a row whose stored scope is unreadable.
|
|
78
78
|
|
|
79
79
|
- `auth/account_queries.ts` — account CRUD, actor resolution, password update with verify-write race guard, paged `query_admin_account_list`.
|
|
80
80
|
- `auth/actor_lookup_queries.ts` — batched `actor` ⨝ `account` for the labels arc.
|
|
@@ -219,11 +219,12 @@ Everything else listed under §RPC action surfaces.
|
|
|
219
219
|
|
|
220
220
|
### Middleware
|
|
221
221
|
|
|
222
|
-
- `auth/middleware.ts` — `create_auth_middleware_specs(deps, options)` assembles `[origin, session, request_context, bearer_auth]` + optional `daemon_token` (the content-type refusal isn't a middleware: `apply_route_specs` checks every `POST` route, see `http/simple_request.ts`). `AUTH_MIDDLEWARE_PATH` (`'/api/*'`) is where `create_app_server` mounts the stack;
|
|
222
|
+
- `auth/middleware.ts` — `create_auth_middleware_specs(deps, options)` assembles `[origin, session, request_context, bearer_auth]` + optional `daemon_token` (the content-type refusal isn't a middleware: `apply_route_specs` checks every `POST` route, see `http/simple_request.ts`). The origin check mounts on every path (`ORIGIN_MIDDLEWARE_PATH`, `'*'`), less its `origin_carve_outs` — each a `MiddlewareCarveOut` (`{path, reason}`, copied and frozen at assembly by `freeze_origin_carve_outs`, refused by `assert_origin_carve_outs_valid` without a reason or when it overlaps the auth scope — `middleware_paths_overlap` against `AUTH_MIDDLEWARE_PATH`, which holds every credential-reading and credential-minting route — and recorded on the origin spec so the surface leaves its `403` off the carved routes); `AUTH_MIDDLEWARE_PATH` (`'/api/*'`) is where `create_app_server` mounts the rest of the stack; assembly asserts lean on it: `assert_endpoint_in_auth_scope` (an `rpc_endpoints` / `ws_endpoints` path, or the bootstrap route under `bootstrap.route_prefix`, must sit inside it — so no carve-out can reach one), `assert_credential_routes_in_auth_scope(route_specs)` (every registered route that reads a credential — `auth.account` not `'none'`, or marked `reads_credential`, as account status is — or is marked `mints_credential` — login, signup, bootstrap — must sit inside it too, so a consumer prefixing the relative account or signup specs outside `/api/` is refused), and `assert_middleware_stack_mounted(stack_specs, middleware_specs)`, run on a `transform_middleware` result against the pre-transform stack (`[proxy, ...auth]`) — each stack spec, matched by its `handler` closure (not its spoofable `name`), mounted exactly once at its original path (`'*'` for the trusted proxy and the origin check, `AUTH_MIDDLEWARE_PATH` for the rest of auth), in its original order; other specs may sit anywhere, including before the proxy and between the auth layers. The proxy is in the stack because dropping it turns every `get_client_ip` into `'unknown'`, one shared bucket for every IP-keyed limiter. The transform receives shallow copies in a fresh array, so an in-place mutation can't rewrite the originals the check reads. The check pins where those layers sit, not what added layers do — a layer you add runs with full context access. Both run in `create_test_app_surface_spec` too.
|
|
223
223
|
- `auth/request_context.ts` — `RequestContext`, `resolve_acting_actor`, `build_request_context`, predicates (`has_role`, `has_scoped_role`, `has_any_scoped_role` — the dispatcher's role gate and every builtin-role check read the **global** grant via `has_scoped_role(_, role, null)` / `has_any_scoped_role(_, roles, null)`; scope-blind `has_role` is never the gate for a builtin role), guards (`require_auth`, `require_role`, `require_credential_types`, `require_token_scope`), `token_scope_surface_denial` (the direct-call form, for surfaces that aren't route specs), `refresh_role_grants`.
|
|
224
224
|
- `auth/session_middleware.ts` — `process_session_cookie` integration, `create_session_and_set_cookie` (shared by login / signup / bootstrap; returns the token hashes of the sessions the cap evicted, which the caller closes after its commit — only login passes a cap).
|
|
225
|
-
- `auth/resolved_auth.ts` — `ResolvedAuth` (the resolved credential as one value: `account_id`, `credential_type`, `token_hash`, `api_token_id`), `get_resolved_auth(c)` (gathers it from the context keys the auth middleware set), and `revalidate_resolved_auth(deps, resolved)`, the no-touch re-read a long-lived connection runs once it is registered: session row / token row (by id) still live and still on the account, account not soft-deleted; a daemon token re-reads its account only. Reuses the middleware's own queries and predicates. Called by the WS upgrade (`actions/register_action_ws.ts`) and the audit SSE route between registering a connection pending and admitting it. Twin of the Rust `revalidate_resolved_auth`.
|
|
226
|
-
- `auth/
|
|
225
|
+
- `auth/resolved_auth.ts` — `ResolvedAuth` (the resolved credential as one value: `account_id`, `credential_type`, `token_hash`, `api_token_id`, `credential_deadline`), `get_resolved_auth(c)` (gathers it from the context keys the auth middleware set), and `revalidate_resolved_auth(deps, resolved)`, the no-touch re-read a long-lived connection runs once it is registered: session row / token row (by id) still live and still on the account, account not soft-deleted; a daemon token re-reads its account only. Reuses the middleware's own queries and predicates. Called by the WS upgrade (`actions/register_action_ws.ts`) and the audit SSE route between registering a connection pending and admitting it. Twin of the Rust `revalidate_resolved_auth`.
|
|
226
|
+
- `auth/credential_deadline.ts` — when a resolved credential stops authenticating, which a WebSocket or audit stream opened on it closes at (expiry writes nothing a revocation listener hears): `credential_deadline_after` (the earliest remaining lifetime, anchored to `performance.now()`), `EXPIRES_IN_MS_SQL` (a row's `expires_at - NOW()`, read on the database clock in the statement that checks it, so the session and token validation queries return `expires_in_ms`), and `schedule_credential_deadline` (a chain of timers of at most `CREDENTIAL_DEADLINE_TIMER_CAP_MS`, an hour — a 30-day session is past `setTimeout`'s ceiling — each wake-up also checking a wall-clock copy of the deadline, so a host suspend, which stops the monotonic clock, delays the close by at most one timer after the resume). The middleware sets `CREDENTIAL_DEADLINE_KEY`: session — the earlier of the cookie's signed expiry (`SESSION_COOKIE_EXPIRES_AT_KEY`, set by the session middleware) and the row's; bearer — the row's, none for an eternal token; daemon token — none. Twin of the Rust `ResolvedAuth::credential_deadline`.
|
|
227
|
+
- `auth/bearer_auth.ts` — soft-fail bearer middleware; rejects when `Origin` or `Referer` present (browser context), and soft-fails a value that is not `is_api_token_shaped` before any query. The scope it sets is the one the query parsed — there is no fallback.
|
|
227
228
|
- `auth/daemon_token_middleware.ts` — `create_daemon_token_middleware(state, deps, log)` — the credential **consumer** only (soft-fail validation, keeper account resolution). The producer (`write_daemon_token` / `start_daemon_token_rotation`) lives in `testing/daemon_token_rotation.ts` behind the dev-env guard — no production assembly mints daemon tokens, mirroring the Rust spine's `fuz_testing`-confined producer. Soft-fails — discards the credential (pass-through, no own 401/503) on **every** non-success path: browser context (`Origin`/`Referer` present), malformed/invalid token, and valid-token-but-no-keeper all `next()` through to the dispatcher's `credential_type_required` (403) gate, mirroring the bearer guard and the Rust spine's `resolve.rs`. Daemon tokens are loopback-only, so browser context never arises in practice — the discard is defense-in-depth.
|
|
228
229
|
|
|
229
230
|
See root ../../../CLAUDE.md §Middleware Ordering for canonical assembly
|
|
@@ -268,16 +269,33 @@ delete / list / clone / moderate`), error reasons, and `all_cell_action_specs`
|
|
|
268
269
|
manage grants or read the audit timeline. NULL
|
|
269
270
|
`created_by` (system origin) is admin-only for edit/manage (explicit
|
|
270
271
|
defense-in-depth). "Admin" on every tier is the **global** grant — a
|
|
271
|
-
scoped `admin` row bypasses nothing.
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
272
|
+
scoped `admin` row bypasses nothing. `can_view_cell`'s public and grant
|
|
273
|
+
arms also require the cell's `moderation` to admit an audience
|
|
274
|
+
(`db/cell_moderation.ts`: unmoderated or `approved`), so a pending or
|
|
275
|
+
rejected contribution is its author's and admins' alone; `can_edit_cell`'s
|
|
276
|
+
editor-grant arm requires the same admission. `can_steward_cell`
|
|
277
|
+
is the separate moderator read: the governing root's manager on a pending or
|
|
278
|
+
rejected contribution under a live root.
|
|
279
|
+
- `auth/cell_steward.ts` — `cell_steward_admits(deps, auth, cell)`: the steward
|
|
280
|
+
arm with its governing-root load, run only after `can_view_cell` denies. Its
|
|
281
|
+
module doc names where the arm applies (`cell_get`, `cell_moderate`, the
|
|
282
|
+
cell-scoped fact route, `cell_list`'s SQL) and where it must not (clone,
|
|
283
|
+
contribution parents, relation reads, `shared_with`, grant recompute).
|
|
284
|
+
- Strict relation-read visibility — every relation read drops edges whose far
|
|
285
|
+
endpoint the caller can't view, so a viewer of a parent can't enumerate
|
|
286
|
+
private linked cells by id. **Forward and reverse** `cell_field_list` /
|
|
287
|
+
`cell_item_list` filter in SQL before their limit
|
|
288
|
+
(`db/cell_view_sql.ts`'s `can_view_cell_sql`, bound through
|
|
289
|
+
`cell_viewer_from_auth`), so a page is up to `limit` edges whose other end
|
|
290
|
+
the caller can view — `CELL_RELATION_LIST_LIMIT_DEFAULT` (50) when omitted,
|
|
291
|
+
`CELL_RELATION_LIST_LIMIT_MAX` (500) at most
|
|
292
|
+
(`auth/cell_relation_list_limit.ts`), and hidden ends never crowd out
|
|
293
|
+
visible ones. The `cell_get` bundle (whose `*_truncated` counts raw edges)
|
|
294
|
+
and the clone walk read with `CELL_VIEWER_UNRESTRICTED` and filter after,
|
|
295
|
+
through `auth/cell_relation_visibility.ts`'s `filter_visible_target_ids(deps,
|
|
296
|
+
auth, ids)`: two queries for the whole id-set, the cells read through the
|
|
297
|
+
narrow `CELL_VIEW_COLUMNS` projection (`query_cell_view_load_many`), never
|
|
298
|
+
the payload.
|
|
281
299
|
- `auth/cell_actions.ts` — `create_cell_actions(deps)`, the generic
|
|
282
300
|
handlers. `cell_create` stamps `created_by` + writes the top-level `kind`
|
|
283
301
|
column; `path` writes are admin-only. It also resolves the directory tree:
|
|
@@ -295,7 +313,9 @@ auth, ids)`: batched strict relation-read filter. Every relation read —
|
|
|
295
313
|
parent-aware `authorize_create` dep (`CellCreateAuthorize` — wider input
|
|
296
314
|
`{kind, data, parent_id, root_id, root_data, scope_id}` than `validate_data`,
|
|
297
315
|
returning a `CellCreateVerdict` `{allow:false} | {allow:true, moderation_required}`)
|
|
298
|
-
gates create by capability; it runs after the validator + the parent-resolve
|
|
316
|
+
gates create by capability; it runs after the validator + the parent-resolve
|
|
317
|
+
(which **404**s `cell_not_found` on a missing or soft-deleted root as on a
|
|
318
|
+
missing parent, authorizer or not).
|
|
299
319
|
A **viewable**-parent / root-creation deny is **403** `cell_create_forbidden`
|
|
300
320
|
(the create-path **404** mask is reserved for a hidden parent); `Allow` folds
|
|
301
321
|
the moderation outcome (born `pending`+private vs `approved`+author-visibility).
|
|
@@ -305,7 +325,8 @@ auth, ids)`: batched strict relation-read filter. Every relation read —
|
|
|
305
325
|
single-connection deadlock of the in-memory drivers). `None`/omitted = open create (the default;
|
|
306
326
|
mirrors `validate_data`). `cell_moderate` is the `pending → approved | rejected`
|
|
307
327
|
transition, gated on `can_manage_cell` of the **governing root** (admin / root
|
|
308
|
-
owner — the author lands a 403, the anti-self-approval guard
|
|
328
|
+
owner — the author lands a 403, the anti-self-approval guard; a root owner
|
|
329
|
+
reaches the check through the steward arm); approve flips
|
|
309
330
|
`moderation → approved` + `visibility → public`, reject sets `rejected`.
|
|
310
331
|
- `auth/cell_grant_actions.ts` / `cell_field_actions.ts` /
|
|
311
332
|
`cell_item_actions.ts` / `cell_audit_actions.ts` — the relation + ACL +
|
|
@@ -587,7 +608,7 @@ reactivation has no self path because a soft-deleted account can't
|
|
|
587
608
|
authenticate.
|
|
588
609
|
|
|
589
610
|
- `admin_account_list_action_spec` — read; input `{limit?, offset?, include_deleted?}`; output `{accounts, grantable_roles}`. `include_deleted` (default false) surfaces soft-deleted rows (with `account.deleted_at` set) for the admin UI's reactivation view.
|
|
590
|
-
- `account_delete_action_spec` — mutation (soft delete); self-or-admin (`{account, actor}`, handler elevates to admin when `account_id` ≠ self); input `{account_id?}`; output `{ok, deleted}`. Tombstones account + actor(s), revokes sessions/tokens, emits `account_delete` + per-actor `actor_delete`. Refuses a keeper-role target (`ERROR_CANNOT_DELETE_KEEPER`) or the sole active admin (`ERROR_CANNOT_DELETE_LAST_ADMIN`) — both fail-loud with a failure-audit row.
|
|
611
|
+
- `account_delete_action_spec` — mutation (soft delete); self-or-admin (`{account, actor}`, handler elevates to admin when `account_id` ≠ self); the self path is session-only, handler-enforced ahead of the target guards (an API or daemon token gets the dispatcher's `ERROR_CREDENTIAL_TYPE_REQUIRED` 403 via `credential_type_required_error`), while an admin deleting another account keeps any credential; input `{account_id?}`; output `{ok, deleted}`. Tombstones account + actor(s), revokes sessions/tokens, emits `account_delete` + per-actor `actor_delete`. Refuses a keeper-role target (`ERROR_CANNOT_DELETE_KEEPER`) or the sole active admin (`ERROR_CANNOT_DELETE_LAST_ADMIN`) — both fail-loud with a failure-audit row.
|
|
591
612
|
- `account_purge_action_spec` — mutation (hard purge); keeper-only (`credential_types: ['daemon_token']` + `roles: ['keeper']`) + `confirm: true` + WARN; input `{account_id, confirm?}`; output `{ok, purged}`. Cascading delete; emits `account_purge` + per-actor `actor_purge` (identity snapshot survives in metadata since `audit_log` ids carry no FK). Refuses a keeper-role target (`ERROR_CANNOT_DELETE_KEEPER`) or the sole active admin (`ERROR_CANNOT_DELETE_LAST_ADMIN`).
|
|
592
613
|
- `account_undelete_action_spec` — mutation (reactivation, inverse of soft delete); admin-only (`roles: ['admin']` — no self path, a tombstoned account can't authenticate); input `{account_id}`; output `{ok, undeleted}`. Clears the `deleted_at` tombstone on the account + its actor(s), emits `account_undelete` + per-actor `actor_undelete`. Does not restore revoked sessions/tokens.
|
|
593
614
|
- `admin_session_list_action_spec` — read; input `{acting?}` (`.default({})`); output `{sessions}`.
|
|
@@ -913,10 +934,11 @@ session, and once the sweep deletes an expired row `account_session_revoke`
|
|
|
913
934
|
has nothing left to close for — so `cleanup_expired_sessions` closes each
|
|
914
935
|
swept session's connections itself, by session id
|
|
915
936
|
(`connection_closer.close_sockets_for_session`), which is why
|
|
916
|
-
`query_session_cleanup_expired` returns the deleted ids.
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
937
|
+
`query_session_cleanup_expired` returns the deleted ids. The close repeats
|
|
938
|
+
one already made: every connection closes at its credential's deadline
|
|
939
|
+
(`auth/credential_deadline.ts`) in the process holding it, sweep or no sweep.
|
|
940
|
+
An expired API token's row is not swept; its connections close at their
|
|
941
|
+
deadline (`account_token_revoke` still finds the row).
|
|
920
942
|
|
|
921
943
|
- The delete runs in its own transaction and the closes follow the commit —
|
|
922
944
|
the rule every revocation close keeps (`actions/CLAUDE.md` §Connection
|