@fuzdev/fuz_app 0.118.0 → 0.119.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/CLAUDE.md +301 -73
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +16 -1
- package/dist/actions/connection_closer.d.ts +136 -51
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +133 -34
- package/dist/actions/register_action_ws.d.ts +176 -24
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +581 -241
- package/dist/actions/register_ws_endpoint.d.ts +1 -0
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +1 -0
- package/dist/actions/socket.svelte.d.ts +43 -9
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +59 -8
- package/dist/actions/transports.d.ts +55 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +55 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +53 -67
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +87 -111
- package/dist/actions/transports_ws_backend.d.ts +212 -30
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +327 -70
- package/dist/actions/ws_endpoint_spec.d.ts +26 -13
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +113 -32
- package/dist/auth/account_actions.d.ts +12 -18
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +47 -33
- package/dist/auth/account_routes.d.ts +5 -11
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +40 -36
- package/dist/auth/account_schema.d.ts +27 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +22 -0
- package/dist/auth/admin_actions.d.ts +8 -17
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +33 -21
- package/dist/auth/api_token_queries.d.ts +23 -2
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +39 -3
- package/dist/auth/audit_emitter.d.ts +61 -38
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +57 -14
- package/dist/auth/audit_log_route_schema.js +7 -7
- package/dist/auth/audit_log_routes.d.ts +31 -6
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +85 -11
- package/dist/auth/audit_log_schema.d.ts +48 -0
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +58 -0
- package/dist/auth/cleanup.d.ts +203 -28
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +222 -26
- package/dist/auth/deps.d.ts +28 -0
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/resolved_auth.d.ts +105 -0
- package/dist/auth/resolved_auth.d.ts.map +1 -0
- package/dist/auth/resolved_auth.js +124 -0
- package/dist/auth/session_middleware.d.ts +8 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +9 -3
- package/dist/auth/session_queries.d.ts +31 -6
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +31 -6
- package/dist/auth/standard_rpc_actions.d.ts +9 -9
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +3 -3
- package/dist/db/CLAUDE.md +7 -1
- package/dist/db/status.d.ts +58 -1
- package/dist/db/status.d.ts.map +1 -1
- package/dist/db/status.js +205 -77
- package/dist/http/CLAUDE.md +7 -3
- package/dist/http/db_routes.d.ts +6 -2
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +2 -2
- package/dist/periodic.d.ts +80 -0
- package/dist/periodic.d.ts.map +1 -0
- package/dist/periodic.js +122 -0
- package/dist/realtime/sse_auth_guard.d.ts +79 -49
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +103 -73
- package/dist/realtime/subscriber_registry.d.ts +119 -8
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +184 -25
- package/dist/server/app_backend.d.ts +12 -1
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +5 -2
- package/dist/server/app_server.d.ts +55 -15
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +41 -13
- package/dist/testing/CLAUDE.md +140 -48
- package/dist/testing/app_server.d.ts +2 -2
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +11 -2
- package/dist/testing/audit_drift_guard.d.ts +1 -48
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +0 -42
- package/dist/testing/connection_closer_helpers.d.ts +9 -22
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +11 -17
- package/dist/testing/cross_backend/capabilities.d.ts +67 -13
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +27 -12
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +21 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +4 -1
- package/dist/testing/cross_backend/session_cap.d.ts +13 -0
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/session_cap.js +55 -19
- package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +93 -26
- package/dist/testing/cross_backend/testing_server_bun.js +4 -4
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -2
- package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
- package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +156 -19
- package/dist/testing/sse_round_trip.d.ts +6 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/stubs.d.ts +1 -1
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +6 -4
- package/dist/testing/transports/sse_transport.d.ts +1 -1
- package/dist/testing/transports/ws_client.d.ts +7 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -1
- package/dist/testing/transports/ws_raw_client.d.ts +51 -0
- package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
- package/dist/testing/transports/ws_raw_client.js +156 -0
- package/dist/testing/transports/ws_transport.d.ts +28 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -1
- package/dist/testing/transports/ws_transport.js +45 -1
- package/dist/testing/ws_round_trip.d.ts +8 -3
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +19 -4
- package/dist/ui/AdminAccounts.svelte +4 -3
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +3 -1
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +22 -6
- package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +16 -0
- package/dist/ui/auth_state.svelte.d.ts +25 -1
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +28 -1
- package/package.json +1 -1
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -16,9 +16,9 @@ documents the cross-cutting invariants that don't fit on any single symbol.
|
|
|
16
16
|
|
|
17
17
|
## AppDeps split
|
|
18
18
|
|
|
19
|
-
- **Capabilities** — `AppDeps` — stateless, injectable per env: `read_secure_file` (hardened bootstrap-token read), `delete_file`, `keyring`, `password`, `db`, `log`, `audit
|
|
19
|
+
- **Capabilities** — `AppDeps` — stateless, injectable per env: `read_secure_file` (hardened bootstrap-token read), `delete_file`, `keyring`, `password`, `db`, `log`, `audit`, `connection_closer` (the `RealtimeCloser` every revocation handler closes live connections through — `actions/CLAUDE.md` §Connection closer).
|
|
20
20
|
- **Route caps** — `RouteFactoryDeps` — `Omit<AppDeps, 'db'>`; handlers get `db` via `RouteContext`.
|
|
21
|
-
- **Action caps** — `ActionFactoryDeps` (`auth/deps.ts`) — the `{log, audit}` shape action factories take; `RouteFactoryDeps`/`AppDeps` satisfy it structurally (role-grant-offer adds `notification_sender?`). The two pure-read actor factories (`actor_lookup`/`actor_search`) take only `{log: Logger}`.
|
|
21
|
+
- **Action caps** — `ActionFactoryDeps` (`auth/deps.ts`) — the `{log, audit}` shape action factories take; `RouteFactoryDeps`/`AppDeps` satisfy it structurally (role-grant-offer adds `notification_sender?`). The factories whose handlers end credentials (`create_account_actions`, `create_admin_actions`, and the standard bundle) take `RevokingActionFactoryDeps` — `ActionFactoryDeps` plus a required `connection_closer`. The two pure-read actor factories (`actor_lookup`/`actor_search`) take only `{log: Logger}`.
|
|
22
22
|
- **Parameters** — `*Options` — static startup values, per-factory.
|
|
23
23
|
- **Runtime state** — inline ref — mutable values: `bootstrap_status`, `DaemonTokenState`. NOT in deps or options.
|
|
24
24
|
|
|
@@ -56,7 +56,7 @@ Cross-cutting notes that don't live on any single symbol:
|
|
|
56
56
|
|
|
57
57
|
Convention — `*_schema.ts` is Zod-only; `*_ddl.ts` holds DDL strings.
|
|
58
58
|
|
|
59
|
-
- `auth/account_schema.ts` — `Account`, `Actor`, `RoleGrant`, `AuthSession`, `ApiToken` + client-safe JSON shapes.
|
|
59
|
+
- `auth/account_schema.ts` — `Account`, `Actor`, `RoleGrant`, `AuthSession`, `ApiToken` + client-safe JSON shapes. Grant predicates over wire fields: `is_role_grant_active` (not revoked, not expired) and `has_global_role(role_grants, role, now?)` (an active grant of the role with `scope_id === null` — the bare-list form of `has_scoped_role(ctx, role, null)`, used client-side by `AuthState`).
|
|
60
60
|
- `auth/role_schema.ts` — `RoleName`, `RoleSpec`, `ROLE_KEEPER`, `ROLE_ADMIN`, `is_builtin_role`, `create_role_schema`, `builtin_role_specs_by_name`, `role_has_grant_path`, `list_roles_with_grant_path`.
|
|
61
61
|
- `auth/scope_kind_schema.ts` — `ScopeKindName`, `create_scope_kind_schema` (open registry, no builtins).
|
|
62
62
|
- `auth/credential_type_schema.ts` — `CredentialTypeName`, `CREDENTIAL_TYPE_SESSION` / `_API_TOKEN` / `_DAEMON_TOKEN`, `create_credential_type_schema`.
|
|
@@ -81,8 +81,8 @@ All take `deps: QueryDeps = {db}` first; `query_validate_api_token` adds `log`.
|
|
|
81
81
|
- `auth/actor_search_queries.ts` — case-insensitive prefix search on `actor.name`, scope-filtered when not admin.
|
|
82
82
|
- `auth/role_grant_queries.ts` — idempotent create, IDOR-guarded revoke (with in-tx supersede), scope-aware lookup, role/account predicates, `query_role_grant_revoke_for_scope` parent-scope cascade.
|
|
83
83
|
- `auth/role_grant_offer_queries.ts` — offer create/decline/retract/list/history/sweep, atomic `query_accept_offer` with sibling supersede; error classes `RoleGrantOfferSelfTargetError` / `_AlreadyTerminalError` / `_ExpiredError` / `_NotFoundError` / `_ActorAccountMismatchError` / `_ActorMismatchError`.
|
|
84
|
-
- `auth/session_queries.ts` — server-side sessions (blake3-hashed), `query_session_revoke_by_hash_unscoped` (logout only), `query_session_enforce_limit` (transaction-required). No touch/renewal query — `expires_at` is an absolute cap set at mint (`AUTH_SESSION_LIFETIME_MS`).
|
|
85
|
-
- `auth/api_token_queries.ts` — token validation with fire-and-forget usage tracking, IDOR-guarded revoke, `query_api_token_enforce_limit` (transaction-required).
|
|
84
|
+
- `auth/session_queries.ts` — server-side sessions (blake3-hashed), `query_session_revoke_by_hash_unscoped` (logout only), `query_session_enforce_limit` (transaction-required; returns the evicted sessions' ids — their token hashes — so the caller closes their connections after the commit), `query_session_cleanup_expired` (returns the swept sessions' ids for the same close — `cleanup_expired_sessions` is its caller). No touch/renewal query — `expires_at` is an absolute cap set at mint (`AUTH_SESSION_LIFETIME_MS`).
|
|
85
|
+
- `auth/api_token_queries.ts` — token validation with fire-and-forget usage tracking, `query_api_token_live_account` (the by-id, no-touch re-check read; shares the `API_TOKEN_IS_LIVE` predicate with the validate query so the two can't drift), IDOR-guarded revoke, `query_api_token_enforce_limit` (transaction-required; returns the evicted tokens' ids for the same post-commit close).
|
|
86
86
|
- `auth/invite_queries.ts` — invite create/find/claim/list/delete; `query_invite_claim_unscoped` (scoping enforced upstream by `_find_unclaimed_match_for_update`, which runs inside the signup tx with `FOR UPDATE` so find + claim are atomic).
|
|
87
87
|
- `auth/app_settings_queries.ts` — load/update for the single-row settings table.
|
|
88
88
|
- `auth/audit_log_queries.ts` — `query_audit_log` (in-tx insert), `_list` / `_list_with_usernames` / `_list_role_grant_history` / `_cleanup_before`, drift counters (`get_audit_metadata_validation_failures` / `get_audit_unknown_event_type_failures`).
|
|
@@ -123,20 +123,43 @@ upstream in `auth/signup_routes.ts` via `query_invite_find_unclaimed_match_for_u
|
|
|
123
123
|
`audit_factory` callback over `create_audit_emitter`; closes over the pool +
|
|
124
124
|
its registered listeners + optional `AuditLogConfig`. Six methods:
|
|
125
125
|
|
|
126
|
-
- `emit(ctx, input)` — fire-and-forget pool write, pushes to `ctx.pending_effects`
|
|
126
|
+
- `emit(ctx, input)` — fire-and-forget pool write, pushes to `ctx.pending_effects`; a success row's listener fan-out goes on `ctx.post_commit_effects`
|
|
127
127
|
- `emit_role_grant_target(ctx, auth, input)` — lifts `actor_id` / `account_id` / `ip` boilerplate for role-grant-shape events
|
|
128
|
-
- `emit_pool(input)` — awaitable pool write for code paths without
|
|
128
|
+
- `emit_pool(input)` — awaitable pool write for code paths without a request context (ad-hoc maintenance scripts; not for success audits paired with a mutation — those write in-tx and `notify` post-commit). Writes, then notifies, whatever the outcome
|
|
129
129
|
- `notify(event)` — fan out an already-written row to listeners (used by in-tx audit batches like `query_accept_offer.audit_events`)
|
|
130
130
|
- `add_listener(listener)` — append-only listener registration (twin of Rust `fuz_auth` `AuditEmitter::add_listener`)
|
|
131
131
|
- `listener_count()` — registered-listener count, for tests / diagnostics
|
|
132
132
|
|
|
133
133
|
Listeners are closure-private and append-only. `create_app_server` registers
|
|
134
|
-
the audit-log SSE listener and per-endpoint WS auth guards
|
|
134
|
+
the audit-log SSE listener and per-endpoint WS auth guards via
|
|
135
135
|
`add_listener` so SSE + WS fan-out compose on top of the consumer's
|
|
136
136
|
`on_audit_event` callback without shallow-copying `AppDeps`. `notify` iterates a
|
|
137
137
|
snapshot so a listener registered mid-fan-out fires only on the next event
|
|
138
138
|
(converges with the Rust twin's cloned vec).
|
|
139
139
|
|
|
140
|
+
**When listeners hear about a row.** `emit` always writes at once, on the pool
|
|
141
|
+
(the row survives a rollback). The fan-out depends on the outcome: a
|
|
142
|
+
**failure** row is announced as soon as it is written — the attempt happened
|
|
143
|
+
whatever the transaction does; a **success** row is announced only after the
|
|
144
|
+
request's transaction commits (a thunk on `ctx.post_commit_effects` that awaits
|
|
145
|
+
the write) and never when the handler throws. The listeners act on a success
|
|
146
|
+
row — the revocation guards close connections, the audit stream broadcasts it
|
|
147
|
+
— so announcing one before its commit would close while the revoked credential
|
|
148
|
+
still reads as valid, and a rolled-back revocation would close anyway. The
|
|
149
|
+
Rust spine gets the same order by writing a success row in the transaction and
|
|
150
|
+
queueing its fan-out behind the commit. `AuditEmitterContext` is therefore
|
|
151
|
+
`{pending_effects, post_commit_effects}` — `RouteContext` and `ActionContext`
|
|
152
|
+
both satisfy it.
|
|
153
|
+
|
|
154
|
+
**Revocation scopes** (`auth/audit_log_schema.ts`) —
|
|
155
|
+
`audit_event_revocation_scopes` declares, per builtin event type, what a
|
|
156
|
+
successful row invalidates (`RevocationScope`: `none` / `session` / `token` /
|
|
157
|
+
`account` / `role`), and `to_revocation_scope(event_type)` reads it (an unknown
|
|
158
|
+
event type is `none`). Typed over `AuditEventType`, so a new event type does not
|
|
159
|
+
compile until it declares its scope. Both revocation listeners
|
|
160
|
+
(`create_ws_auth_guard`, `create_sse_auth_guard`) dispatch on it. Twin of the
|
|
161
|
+
`revocation_scope` column on the Rust `AUDIT_EVENT_SPECS`.
|
|
162
|
+
|
|
140
163
|
**Drift counters** (`auth/audit_log_queries.ts`) — `audit_metadata_validation_failures`
|
|
141
164
|
and `audit_unknown_event_type_failures` are process-wide, fail-open
|
|
142
165
|
(write the row anyway). Independent in implementation; under the factory
|
|
@@ -147,7 +170,7 @@ they track the same config. Sample via `get_*`; `reset_*` are test-only.
|
|
|
147
170
|
- `auth/account_routes.ts` — `POST /login` / `/logout` / `/password`, `GET /verify` (nginx `auth_request` shim), `GET /api/account/status`. Constants: `DEFAULT_MAX_SESSIONS = 5`, `DEFAULT_MAX_TOKENS = 10`, `DEFAULT_LOGIN_FAIL_FLOOR_MS = 250`, `DEFAULT_LOGIN_FAIL_JITTER_MS = 25`.
|
|
148
171
|
- `auth/bootstrap_routes.ts` — `POST /bootstrap` + `check_bootstrap_status`; `BootstrapStatus` runtime ref.
|
|
149
172
|
- `auth/signup_routes.ts` — `POST /signup` (open or invite-gated).
|
|
150
|
-
- `auth/audit_log_routes.ts` — optional `GET /audit/stream` (SSE); list/history are on the RPC surface.
|
|
173
|
+
- `auth/audit_log_routes.ts` — optional `GET /audit/stream` (SSE); list/history are on the RPC surface. Takes `stream: {registry, log}` (an `AuditLogSse` satisfies it). The handler admits the stream in two phases — register pending on the `SubscriberRegistry`, re-read the credential (`revalidate_resolved_auth`) and the acting actor's role grants, admit — so a revocation racing the request opens no stream: 401 / 403 from the re-reads, or a 200 whose body is the connect comment alone when the pending registration was closed.
|
|
151
174
|
- `auth/auth_guard_resolver.ts` — `fuz_auth_guard_resolver` injected into `apply_route_specs` so the framework stays auth-agnostic. Maps each `RouteAuth` axis to a guard and is the single site every route spec traverses, so it also carries the two invariants the schema can't (a `required_scope` that isn't a well-formed capability, and one on an unrestricted route).
|
|
152
175
|
|
|
153
176
|
**Hono-free route shapes.** Each cookie/SSE-coupled route module has a sibling
|
|
@@ -176,6 +199,12 @@ tokens (force re-auth everywhere), then clears the session cookie. Declares
|
|
|
176
199
|
`credential_types: ['session']` (see ../../../docs/security.md
|
|
177
200
|
§Credential-channel gating).
|
|
178
201
|
|
|
202
|
+
**The routes close what they revoke, after the commit.** `/logout` and
|
|
203
|
+
`/password` queue an account-wide close, and `/login` queues a session close
|
|
204
|
+
for each session its cap evicted (`queue_connection_close` on
|
|
205
|
+
`deps.connection_closer`). The queue runs once the route's transaction has
|
|
206
|
+
committed and is dropped if the handler throws.
|
|
207
|
+
|
|
179
208
|
REST-only post RPC migration: `/login`, `/logout`, `/password`, `/signup`,
|
|
180
209
|
`/bootstrap`, `/verify` (empty-body shim), optional `/audit/stream`.
|
|
181
210
|
Everything else listed under §RPC action surfaces.
|
|
@@ -184,7 +213,8 @@ Everything else listed under §RPC action surfaces.
|
|
|
184
213
|
|
|
185
214
|
- `auth/middleware.ts` — `create_auth_middleware_specs(deps, options)` assembles `[origin, session, request_context, bearer_auth]` + optional `daemon_token`.
|
|
186
215
|
- `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`.
|
|
187
|
-
- `auth/session_middleware.ts` — `process_session_cookie` integration, `create_session_and_set_cookie` (shared by login / signup / bootstrap).
|
|
216
|
+
- `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).
|
|
217
|
+
- `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`.
|
|
188
218
|
- `auth/bearer_auth.ts` — soft-fail bearer middleware; rejects when `Origin` or `Referer` present (browser context).
|
|
189
219
|
- `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.
|
|
190
220
|
|
|
@@ -586,11 +616,13 @@ Closure state:
|
|
|
586
616
|
directly (`query_app_settings_load`/`query_app_settings_update`);
|
|
587
617
|
`auth/signup_routes.ts` reads the current value fresh per request. There is no
|
|
588
618
|
`options.app_settings` ref.
|
|
589
|
-
- `
|
|
590
|
-
`admin_session_revoke_all` / `admin_token_revoke_all`
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
619
|
+
- `deps.connection_closer` (required, `RevokingActionFactoryDeps`) —
|
|
620
|
+
`admin_session_revoke_all` / `admin_token_revoke_all` / `account_delete` /
|
|
621
|
+
`account_purge` queue a close of the target account's live connections
|
|
622
|
+
(`queue_connection_close`), run after the handler's transaction commits and
|
|
623
|
+
dropped if it throws. The close does not depend on the audit write; the
|
|
624
|
+
audit listeners repeat it on the announced row. Failure outcomes queue no
|
|
625
|
+
close.
|
|
594
626
|
|
|
595
627
|
Failure-outcome audit rows: `admin_session_revoke_all` and `_token_revoke_all`
|
|
596
628
|
emit an `outcome: 'failure'` row on `ERROR_ACCOUNT_NOT_FOUND` for forensic
|
|
@@ -751,11 +783,14 @@ gated event also records `credential_type` in metadata (mirrors REST
|
|
|
751
783
|
`password_change`).
|
|
752
784
|
|
|
753
785
|
Options: `max_tokens?: number | null` (defaults to `DEFAULT_MAX_TOKENS`;
|
|
754
|
-
`null` disables)
|
|
755
|
-
|
|
756
|
-
|
|
786
|
+
`null` disables). Deps: `RevokingActionFactoryDeps` — `log`, `audit`, and the
|
|
787
|
+
required `connection_closer`. Each revoking handler queues its close
|
|
788
|
+
(`queue_connection_close`: one session, one token, or the account), which runs
|
|
789
|
+
after the handler's transaction commits and is dropped if it throws;
|
|
790
|
+
`account_token_create` queues a token close for each token its cap evicts.
|
|
791
|
+
Failure outcomes (`revoked: false`) queue no close — mirrors the listeners'
|
|
757
792
|
`outcome === 'failure'` guard so attacker-guessable ids can't target
|
|
758
|
-
arbitrary
|
|
793
|
+
arbitrary connections.
|
|
759
794
|
|
|
760
795
|
### Standard RPC bundle
|
|
761
796
|
|
|
@@ -768,8 +803,8 @@ app-settings methods are always wired). Frontend mirror is
|
|
|
768
803
|
|
|
769
804
|
Option routing — `roles` is shared between admin + role-grant-offer;
|
|
770
805
|
`default_ttl_ms` + `authorize` → role-grant-offer
|
|
771
|
-
only; `max_tokens` → account only
|
|
772
|
-
|
|
806
|
+
only; `max_tokens` → account only. On `deps`: `notification_sender` →
|
|
807
|
+
role-grant-offer only; `connection_closer` (required) → admin + account.
|
|
773
808
|
|
|
774
809
|
Pair with `create_app_server`'s `rpc_endpoints` factory form
|
|
775
810
|
`(ctx) => Array<RpcEndpointSpec>` so the combined action list gets
|
|
@@ -841,12 +876,62 @@ drops the hand-maintained method-name mappings:
|
|
|
841
876
|
|
|
842
877
|
## Cleanup
|
|
843
878
|
|
|
844
|
-
`auth/cleanup.ts` —
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
879
|
+
`auth/cleanup.ts` — twin of the Rust `fuz_auth::auth_cleanup` module.
|
|
880
|
+
|
|
881
|
+
- `run_auth_cleanup(deps)` — one pass: `cleanup_expired_sessions`, then
|
|
882
|
+
`cleanup_expired_role_grant_offers`; returns `AuthCleanupResult`
|
|
883
|
+
(`{expired_sessions, expired_offers}`, the same shape on both spines).
|
|
884
|
+
Re-throws the first sweep's error. Expired offer rows are preserved (audit
|
|
885
|
+
value for the history view).
|
|
886
|
+
- `start_auth_cleanup(deps, {interval_ms?})` → `{stop}` — the scheduler: a
|
|
887
|
+
pass at once, then one per interval (`DEFAULT_AUTH_CLEANUP_INTERVAL_MS`, ten
|
|
888
|
+
minutes), until `stop()`. `create_app_server`'s `auth_cleanup` option calls
|
|
889
|
+
it with `backend.deps` and chains `stop` into `AppServer.close`. **Off by
|
|
890
|
+
default** there; a production server turns it on, and an in-process test
|
|
891
|
+
harness leaves it off (a background pass deletes rows and writes audit rows
|
|
892
|
+
mid-test).
|
|
893
|
+
|
|
894
|
+
`AuthCleanupDeps` is `{db, log, audit, connection_closer}`, all required —
|
|
895
|
+
`AppDeps` satisfies it. Tests pass `create_test_audit_emitter()` from
|
|
896
|
+
`testing/stubs.ts` and, where nothing is to be closed, `noop_connection_closer`.
|
|
897
|
+
The sweep belongs in the process that holds the connections: run from another
|
|
898
|
+
process against a live server's database, it deletes the rows that server's
|
|
899
|
+
own pass would have closed for.
|
|
900
|
+
|
|
901
|
+
**The session sweep closes what it sweeps.** A connection never re-reads its
|
|
902
|
+
session, and once the sweep deletes an expired row `account_session_revoke`
|
|
903
|
+
has nothing left to close for — so `cleanup_expired_sessions` closes each
|
|
904
|
+
swept session's connections itself, by session id
|
|
905
|
+
(`connection_closer.close_sockets_for_session`), which is why
|
|
906
|
+
`query_session_cleanup_expired` returns the deleted ids. With the cleanup
|
|
907
|
+
scheduled, a connection outlives its **session's** expiry by at most one
|
|
908
|
+
interval. An expired API token's row is not swept and its connection is not
|
|
909
|
+
closed (`account_token_revoke` still finds the row).
|
|
910
|
+
|
|
911
|
+
- The delete runs in its own transaction and the closes follow the commit —
|
|
912
|
+
the rule every revocation close keeps (`actions/CLAUDE.md` §Connection
|
|
913
|
+
closer).
|
|
914
|
+
- A transaction that fails once the delete has answered still closes: the
|
|
915
|
+
sessions are expired either way, the error is thrown after the closes, and
|
|
916
|
+
the next pass deletes the rows the commit left.
|
|
917
|
+
- A closer that throws for one session is logged and the rest still close.
|
|
918
|
+
- No audit row is written for a swept session, so no listener repeats the
|
|
919
|
+
close.
|
|
920
|
+
|
|
921
|
+
**The schedule** is `start_periodic` (`periodic.ts`, twin of the Rust
|
|
922
|
+
`fuz_sys::periodic::spawn_periodic`), which owns the loop's rules:
|
|
923
|
+
`start_auth_cleanup` is that loop over `run_auth_cleanup` plus the swept-counts
|
|
924
|
+
log. Passes never overlap, and a pass that overruns its interval delays the
|
|
925
|
+
next one (which starts when the slow one ends, the one after a full interval
|
|
926
|
+
later) rather than bursting. A failed pass is logged at `warn` with its error
|
|
927
|
+
and retried on the next interval. The timer is unref'd. An interval that is
|
|
928
|
+
not a positive finite number, or exceeds `PERIODIC_INTERVAL_MS_MAX` (the
|
|
929
|
+
largest delay a timer honors), throws at the call. **Shutdown differs from
|
|
930
|
+
Rust**: its task drops a pass in progress; `stop()` ends the schedule at once
|
|
931
|
+
and its promise resolves when the pass in progress has finished — await it
|
|
932
|
+
before closing the database. The wait has no bound of its own.
|
|
933
|
+
|
|
934
|
+
**Offer idempotency.** `cleanup_expired_role_grant_offers` runs one transaction:
|
|
850
935
|
`query_role_grant_offer_sweep_expired` claims the expired pending offers not
|
|
851
936
|
yet audited by stamping `role_grant_offer.expire_audited_at` (`UPDATE …
|
|
852
937
|
RETURNING`), and each claimed row's `role_grant_offer_expire` audit row is
|
|
@@ -859,8 +944,4 @@ is not a lifecycle state (outside the terminal CHECK and the pending
|
|
|
859
944
|
predicate) and stays off the wire (`to_role_grant_offer_json` drops it); the
|
|
860
945
|
create upsert's re-offer clears it, so a refreshed offer's own expiry is
|
|
861
946
|
audited too. The `role_grant_offer_expire_sweep` partial index serves the
|
|
862
|
-
claim.
|
|
863
|
-
|
|
864
|
-
`AuthCleanupDeps` requires `audit: AuditEmitter` — production wiring always
|
|
865
|
-
has a bound emitter; tests pass `create_test_audit_emitter()` from
|
|
866
|
-
`testing/stubs.ts`.
|
|
947
|
+
claim.
|
|
@@ -17,14 +17,18 @@
|
|
|
17
17
|
* so passing another account's session or token id returns `revoked: false`
|
|
18
18
|
* rather than revealing whether the id exists.
|
|
19
19
|
*
|
|
20
|
+
* Every handler that ends a credential closes the connections it opened,
|
|
21
|
+
* after the handler's transaction commits (`queue_connection_close` on
|
|
22
|
+
* `deps.connection_closer`): the two session revokes, the token revoke, and
|
|
23
|
+
* `account_token_create` for the tokens its cap evicts.
|
|
24
|
+
*
|
|
20
25
|
* Counterpart to `auth/account_routes.ts`, which keeps the cookie-lifecycle flows
|
|
21
26
|
* (`login`, `logout`, `password`, `signup`, `bootstrap`) on REST.
|
|
22
27
|
*
|
|
23
28
|
* @module
|
|
24
29
|
*/
|
|
25
30
|
import { type RpcAction } from '../actions/action_rpc.js';
|
|
26
|
-
import type {
|
|
27
|
-
import type { ActionFactoryDeps } from './deps.js';
|
|
31
|
+
import type { RevokingActionFactoryDeps } from './deps.js';
|
|
28
32
|
/** Options for `create_account_actions`. */
|
|
29
33
|
export interface AccountActionOptions {
|
|
30
34
|
/**
|
|
@@ -34,27 +38,17 @@ export interface AccountActionOptions {
|
|
|
34
38
|
* `DEFAULT_MAX_TOKENS`; pass `null` to disable the cap.
|
|
35
39
|
*/
|
|
36
40
|
max_tokens?: number | null;
|
|
37
|
-
/**
|
|
38
|
-
* Live-connection closer — when set, `account_session_revoke` /
|
|
39
|
-
* `_session_revoke_all` / `account_token_revoke` handlers eagerly close
|
|
40
|
-
* affected WebSocket sockets BEFORE emitting the corresponding audit
|
|
41
|
-
* event. Closes the audit-failure-leaks-WS surface: the listener-based
|
|
42
|
-
* close (`transports_ws_auth_guard`) only fires after the audit INSERT
|
|
43
|
-
* succeeds, so a DB error would leave live sockets stale. `BackendWebsocketTransport`
|
|
44
|
-
* satisfies this interface structurally; consumers pass their transport
|
|
45
|
-
* instance directly. When absent, only the listener-based close runs.
|
|
46
|
-
* Mirrors `zzz_server`'s handler-side `close_sockets_for_*` calls.
|
|
47
|
-
*/
|
|
48
|
-
connection_closer?: ConnectionCloser | null;
|
|
49
41
|
}
|
|
50
42
|
/**
|
|
51
43
|
* Create the self-service account RPC actions.
|
|
52
44
|
*
|
|
53
|
-
* @param deps - `
|
|
54
|
-
* audit rows via the captured
|
|
55
|
-
* `on_audit_event` fan-out and the
|
|
45
|
+
* @param deps - `RevokingActionFactoryDeps` (`log`, `audit`,
|
|
46
|
+
* `connection_closer`). `audit.emit` writes audit rows via the captured
|
|
47
|
+
* pool; the bound emitter encapsulates `on_audit_event` fan-out and the
|
|
48
|
+
* optional `AuditLogConfig`. `connection_closer` closes a revoked
|
|
49
|
+
* credential's live connections after the revocation commits.
|
|
56
50
|
* @param options - per-factory configuration
|
|
57
51
|
* @returns the `RpcAction` array to spread into a `create_rpc_endpoint` call
|
|
58
52
|
*/
|
|
59
|
-
export declare const create_account_actions: (deps:
|
|
53
|
+
export declare const create_account_actions: (deps: RevokingActionFactoryDeps, options?: AccountActionOptions) => Array<RpcAction>;
|
|
60
54
|
//# sourceMappingURL=account_actions.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_actions.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"account_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_actions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAsC,KAAK,SAAS,EAAE,MAAM,0BAA0B,CAAC;AAsB9F,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC;AAyB3D,4CAA4C;AAC5C,MAAM,WAAW,oBAAoB;IACpC;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,yBAAyB,EAC/B,UAAS,oBAAyB,KAChC,KAAK,CAAC,SAAS,CA8KjB,CAAC"}
|
|
@@ -17,12 +17,18 @@
|
|
|
17
17
|
* so passing another account's session or token id returns `revoked: false`
|
|
18
18
|
* rather than revealing whether the id exists.
|
|
19
19
|
*
|
|
20
|
+
* Every handler that ends a credential closes the connections it opened,
|
|
21
|
+
* after the handler's transaction commits (`queue_connection_close` on
|
|
22
|
+
* `deps.connection_closer`): the two session revokes, the token revoke, and
|
|
23
|
+
* `account_token_create` for the tokens its cap evicts.
|
|
24
|
+
*
|
|
20
25
|
* Counterpart to `auth/account_routes.ts`, which keeps the cookie-lifecycle flows
|
|
21
26
|
* (`login`, `logout`, `password`, `signup`, `bootstrap`) on REST.
|
|
22
27
|
*
|
|
23
28
|
* @module
|
|
24
29
|
*/
|
|
25
30
|
import { rpc_action } from '../actions/action_rpc.js';
|
|
31
|
+
import { queue_connection_close } from '../actions/connection_closer.js';
|
|
26
32
|
import { to_session_account } from './account_schema.js';
|
|
27
33
|
import { query_session_list_for_account, query_session_revoke_for_account, query_session_revoke_all_for_account } from './session_queries.js';
|
|
28
34
|
import { query_api_token_enforce_limit, query_api_token_list_for_account, query_create_api_token, query_revoke_api_token_for_account } from './api_token_queries.js';
|
|
@@ -35,14 +41,17 @@ import { account_verify_action_spec, account_session_list_action_spec, account_s
|
|
|
35
41
|
/**
|
|
36
42
|
* Create the self-service account RPC actions.
|
|
37
43
|
*
|
|
38
|
-
* @param deps - `
|
|
39
|
-
* audit rows via the captured
|
|
40
|
-
* `on_audit_event` fan-out and the
|
|
44
|
+
* @param deps - `RevokingActionFactoryDeps` (`log`, `audit`,
|
|
45
|
+
* `connection_closer`). `audit.emit` writes audit rows via the captured
|
|
46
|
+
* pool; the bound emitter encapsulates `on_audit_event` fan-out and the
|
|
47
|
+
* optional `AuditLogConfig`. `connection_closer` closes a revoked
|
|
48
|
+
* credential's live connections after the revocation commits.
|
|
41
49
|
* @param options - per-factory configuration
|
|
42
50
|
* @returns the `RpcAction` array to spread into a `create_rpc_endpoint` call
|
|
43
51
|
*/
|
|
44
52
|
export const create_account_actions = (deps, options = {}) => {
|
|
45
|
-
const { max_tokens = DEFAULT_MAX_TOKENS
|
|
53
|
+
const { max_tokens = DEFAULT_MAX_TOKENS } = options;
|
|
54
|
+
const { connection_closer } = deps;
|
|
46
55
|
const verify_handler = (_input, ctx) => {
|
|
47
56
|
return to_session_account(ctx.auth.account);
|
|
48
57
|
};
|
|
@@ -52,20 +61,18 @@ export const create_account_actions = (deps, options = {}) => {
|
|
|
52
61
|
};
|
|
53
62
|
const session_revoke_handler = async (input, ctx) => {
|
|
54
63
|
const revoked = await query_session_revoke_for_account(ctx, input.session_id, ctx.auth.account.id);
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (revoked && connection_closer) {
|
|
68
|
-
connection_closer.close_sockets_for_session(input.session_id);
|
|
64
|
+
// Close the connections this session opened, once the revoke has
|
|
65
|
+
// committed — queued, so it never runs before the commit or for a
|
|
66
|
+
// revoke that rolled back, and it does not depend on the audit write
|
|
67
|
+
// below. Only on success: a failed revoke carries a caller-supplied
|
|
68
|
+
// session_id, and closing on it would let any account close another's
|
|
69
|
+
// connections by guessing a hash. The audit listener repeats the close
|
|
70
|
+
// on the announced row; closing is idempotent.
|
|
71
|
+
if (revoked) {
|
|
72
|
+
queue_connection_close(ctx, connection_closer, {
|
|
73
|
+
kind: 'session',
|
|
74
|
+
session_token_hash: input.session_id
|
|
75
|
+
});
|
|
69
76
|
}
|
|
70
77
|
deps.audit.emit(ctx, {
|
|
71
78
|
event_type: 'session_revoke',
|
|
@@ -79,17 +86,14 @@ export const create_account_actions = (deps, options = {}) => {
|
|
|
79
86
|
};
|
|
80
87
|
const session_revoke_all_handler = async (_input, ctx) => {
|
|
81
88
|
const count = await query_session_revoke_all_for_account(ctx, ctx.auth.account.id);
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (connection_closer) {
|
|
91
|
-
connection_closer.close_sockets_for_account(ctx.auth.account.id);
|
|
92
|
-
}
|
|
89
|
+
// Post-commit close — see session_revoke_handler. Queued regardless of
|
|
90
|
+
// `count` (today `count >= 1` always — the caller is using a session
|
|
91
|
+
// it is revoking), symmetric with the admin revoke-all handlers in
|
|
92
|
+
// `admin_actions.ts`, where `count: 0` is a real outcome.
|
|
93
|
+
queue_connection_close(ctx, connection_closer, {
|
|
94
|
+
kind: 'account',
|
|
95
|
+
account_id: ctx.auth.account.id
|
|
96
|
+
});
|
|
93
97
|
deps.audit.emit(ctx, {
|
|
94
98
|
event_type: 'session_revoke_all',
|
|
95
99
|
account_id: ctx.auth.account.id,
|
|
@@ -109,7 +113,14 @@ export const create_account_actions = (deps, options = {}) => {
|
|
|
109
113
|
const expires_at = token_lifetime_to_expires_at(input.lifetime);
|
|
110
114
|
await query_create_api_token(ctx, id, ctx.auth.account.id, input.name, token_hash, scope, expires_at);
|
|
111
115
|
if (max_tokens != null) {
|
|
112
|
-
await query_api_token_enforce_limit(ctx, ctx.auth.account.id, max_tokens);
|
|
116
|
+
const evicted = await query_api_token_enforce_limit(ctx, ctx.auth.account.id, max_tokens);
|
|
117
|
+
// The cap deleted the evicted rows; the connections they opened end
|
|
118
|
+
// only when closed, and only after the commit — the same post-commit
|
|
119
|
+
// close the revocations use. No audit row: there is no eviction
|
|
120
|
+
// event, and `token_create` records the mint that caused it.
|
|
121
|
+
for (const api_token_id of evicted) {
|
|
122
|
+
queue_connection_close(ctx, connection_closer, { kind: 'token', api_token_id });
|
|
123
|
+
}
|
|
113
124
|
}
|
|
114
125
|
deps.audit.emit(ctx, {
|
|
115
126
|
event_type: 'token_create',
|
|
@@ -145,9 +156,12 @@ export const create_account_actions = (deps, options = {}) => {
|
|
|
145
156
|
};
|
|
146
157
|
const token_revoke_handler = async (input, ctx) => {
|
|
147
158
|
const revoked = await query_revoke_api_token_for_account(ctx, input.token_id, ctx.auth.account.id);
|
|
148
|
-
//
|
|
149
|
-
if (revoked
|
|
150
|
-
connection_closer
|
|
159
|
+
// Post-commit close, on success only — see session_revoke_handler.
|
|
160
|
+
if (revoked) {
|
|
161
|
+
queue_connection_close(ctx, connection_closer, {
|
|
162
|
+
kind: 'token',
|
|
163
|
+
api_token_id: input.token_id
|
|
164
|
+
});
|
|
151
165
|
}
|
|
152
166
|
deps.audit.emit(ctx, {
|
|
153
167
|
event_type: 'token_revoke',
|
|
@@ -25,7 +25,6 @@ import type { SessionOptions } from './session_cookie.js';
|
|
|
25
25
|
import { type RouteSpec } from '../http/route_spec.js';
|
|
26
26
|
import { type RateLimiter } from '../rate_limiter.js';
|
|
27
27
|
import type { RouteFactoryDeps } from './deps.js';
|
|
28
|
-
import type { ConnectionCloser } from '../actions/connection_closer.js';
|
|
29
28
|
/**
|
|
30
29
|
* Create the account status route spec.
|
|
31
30
|
*
|
|
@@ -112,15 +111,6 @@ export interface AccountRouteOptions extends AuthSessionRouteOptions {
|
|
|
112
111
|
* jitter while keeping the floor. Default `DEFAULT_LOGIN_FAIL_JITTER_MS`.
|
|
113
112
|
*/
|
|
114
113
|
login_fail_jitter_ms?: number;
|
|
115
|
-
/**
|
|
116
|
-
* Live-connection closer — when set, the `logout` and `password` handlers
|
|
117
|
-
* eagerly close affected WebSocket sockets for the account BEFORE
|
|
118
|
-
* emitting the corresponding audit event. Mirrors the self-service
|
|
119
|
-
* action surface (see `AccountActionOptions.connection_closer`). When
|
|
120
|
-
* absent, only the listener-based close (`transports_ws_auth_guard`
|
|
121
|
-
* registered via `audit.add_listener`) runs.
|
|
122
|
-
*/
|
|
123
|
-
connection_closer?: ConnectionCloser | null;
|
|
124
114
|
/**
|
|
125
115
|
* Runtime bootstrap status for the bundled `GET /status` route — when
|
|
126
116
|
* `available`, its unauthenticated 401 carries `bootstrap_available: true`
|
|
@@ -143,7 +133,11 @@ export interface AccountRouteOptions extends AuthSessionRouteOptions {
|
|
|
143
133
|
* every account surface serves it, matching the Rust `account_router`.
|
|
144
134
|
* Self-service session/token management is on `auth/account_actions.ts`.
|
|
145
135
|
*
|
|
146
|
-
*
|
|
136
|
+
* `/login` closes the connections of the sessions its cap evicts, and
|
|
137
|
+
* `/logout` and `/password` close the account's, each after the route's
|
|
138
|
+
* transaction commits (`queue_connection_close` on `deps.connection_closer`).
|
|
139
|
+
*
|
|
140
|
+
* @param deps - stateless capabilities (keyring, password, log, audit, connection_closer)
|
|
147
141
|
* @param options - per-factory configuration (session_options, login_ip_rate_limiter, login_account_rate_limiter, bootstrap_status)
|
|
148
142
|
* @returns route specs (not yet applied to Hono)
|
|
149
143
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA4B1D,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"account_routes.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AA4B1D,OAAO,EAAmB,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAExE,OAAO,EAAgC,KAAK,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAIlD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,gCAAgC,GAAI,UAAU,oBAAoB,KAAG,SA4EhF,CAAC;AAEH,iDAAiD;AACjD,MAAM,WAAW,oBAAoB;IACpC,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8FAA8F;IAC9F,gBAAgB,CAAC,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1C;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,MAAM,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,4BAA4B,KAAK,CAAC;AAQ/C;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,uBAAuB;IACvC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;CACxC;AAED;;GAEG;AACH,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IACnE;;;;;;;OAOG;IACH,qBAAqB,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1C,4FAA4F;IAC5F,0BAA0B,EAAE,WAAW,GAAG,IAAI,CAAC;IAC/C,2FAA2F;IAC3F,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;CAC1C;AAID;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,0BAA0B,GACtC,MAAM,gBAAgB,EACtB,SAAS,mBAAmB,KAC1B,KAAK,CAAC,SAAS,CA2RjB,CAAC"}
|
|
@@ -32,6 +32,7 @@ import { ACCOUNT_ID_KEY, CREDENTIAL_TYPE_KEY } from '../hono_context.js';
|
|
|
32
32
|
import { get_route_input } from '../http/route_spec.js';
|
|
33
33
|
import { get_client_ip } from '../http/client_ip.js';
|
|
34
34
|
import { rate_limit_exceeded_response } from '../rate_limiter.js';
|
|
35
|
+
import { queue_connection_close } from '../actions/connection_closer.js';
|
|
35
36
|
import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INVALID_CREDENTIALS } from '../http/error_schemas.js';
|
|
36
37
|
/**
|
|
37
38
|
* Create the account status route spec.
|
|
@@ -154,13 +155,17 @@ const login_fail_delay = (floor_ms, jitter_ms) => {
|
|
|
154
155
|
* every account surface serves it, matching the Rust `account_router`.
|
|
155
156
|
* Self-service session/token management is on `auth/account_actions.ts`.
|
|
156
157
|
*
|
|
157
|
-
*
|
|
158
|
+
* `/login` closes the connections of the sessions its cap evicts, and
|
|
159
|
+
* `/logout` and `/password` close the account's, each after the route's
|
|
160
|
+
* transaction commits (`queue_connection_close` on `deps.connection_closer`).
|
|
161
|
+
*
|
|
162
|
+
* @param deps - stateless capabilities (keyring, password, log, audit, connection_closer)
|
|
158
163
|
* @param options - per-factory configuration (session_options, login_ip_rate_limiter, login_account_rate_limiter, bootstrap_status)
|
|
159
164
|
* @returns route specs (not yet applied to Hono)
|
|
160
165
|
*/
|
|
161
166
|
export const create_account_route_specs = (deps, options) => {
|
|
162
|
-
const { keyring, password } = deps;
|
|
163
|
-
const { session_options, login_ip_rate_limiter, login_account_rate_limiter, max_sessions = DEFAULT_MAX_SESSIONS, login_fail_floor_ms = DEFAULT_LOGIN_FAIL_FLOOR_MS, login_fail_jitter_ms = DEFAULT_LOGIN_FAIL_JITTER_MS,
|
|
167
|
+
const { keyring, password, connection_closer } = deps;
|
|
168
|
+
const { session_options, login_ip_rate_limiter, login_account_rate_limiter, max_sessions = DEFAULT_MAX_SESSIONS, login_fail_floor_ms = DEFAULT_LOGIN_FAIL_FLOOR_MS, login_fail_jitter_ms = DEFAULT_LOGIN_FAIL_JITTER_MS, bootstrap_status } = options;
|
|
164
169
|
const [verify_shape, login_shape, logout_shape, password_shape] = create_account_route_shapes({
|
|
165
170
|
login_account_rate_limited: login_account_rate_limiter !== null
|
|
166
171
|
});
|
|
@@ -250,7 +255,7 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
250
255
|
// that address. See `RateLimiter.reset`.
|
|
251
256
|
if (login_account_rate_limiter)
|
|
252
257
|
login_account_rate_limiter.reset(account_rate_key);
|
|
253
|
-
await create_session_and_set_cookie({
|
|
258
|
+
const evicted = await create_session_and_set_cookie({
|
|
254
259
|
keyring,
|
|
255
260
|
deps: route,
|
|
256
261
|
c,
|
|
@@ -258,6 +263,15 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
258
263
|
session_options,
|
|
259
264
|
max_sessions
|
|
260
265
|
});
|
|
266
|
+
// The sessions the cap evicted are gone from the table, but not
|
|
267
|
+
// from the connections they opened — a WebSocket keeps the
|
|
268
|
+
// authority it resolved at upgrade, and an SSE stream never
|
|
269
|
+
// rechecks. Close them once the eviction has committed. No audit
|
|
270
|
+
// row: there is no eviction event, and the `login` row records
|
|
271
|
+
// the cause.
|
|
272
|
+
for (const session_token_hash of evicted) {
|
|
273
|
+
queue_connection_close(route, connection_closer, { kind: 'session', session_token_hash });
|
|
274
|
+
}
|
|
261
275
|
deps.audit.emit(route, {
|
|
262
276
|
event_type: 'login',
|
|
263
277
|
account_id: account.id,
|
|
@@ -274,22 +288,18 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
274
288
|
if (session_token) {
|
|
275
289
|
const token_hash = hash_session_token(session_token);
|
|
276
290
|
await query_session_revoke_by_hash_unscoped(route, token_hash);
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
// session-targeted) to match the Rust
|
|
281
|
-
// and the sibling `/password` handler
|
|
282
|
-
// self-initiated account-grain operation
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
// don't introduce a throw here without acknowledging the trade.
|
|
290
|
-
if (connection_closer) {
|
|
291
|
-
connection_closer.close_sockets_for_account(ctx.account.id);
|
|
292
|
-
}
|
|
291
|
+
// Close this account's live connections once the revoke has
|
|
292
|
+
// committed — queued, so it never runs before the commit or on
|
|
293
|
+
// rollback, and it does not depend on the fail-open audit write
|
|
294
|
+
// below. Account-wide (not session-targeted) to match the Rust
|
|
295
|
+
// `account_logout` handler and the sibling `/password` handler
|
|
296
|
+
// — logout is a self-initiated account-grain operation. The
|
|
297
|
+
// audit listeners repeat the same close on the announced
|
|
298
|
+
// `logout` row; closing is idempotent.
|
|
299
|
+
queue_connection_close(route, connection_closer, {
|
|
300
|
+
kind: 'account',
|
|
301
|
+
account_id: ctx.account.id
|
|
302
|
+
});
|
|
293
303
|
}
|
|
294
304
|
clear_session_cookie(c, session_options);
|
|
295
305
|
// Account-grain operation — no `actor_id` (which actor was
|
|
@@ -389,22 +399,16 @@ export const create_account_route_specs = (deps, options) => {
|
|
|
389
399
|
// revoke all sessions and API tokens (force re-auth everywhere)
|
|
390
400
|
const sessions_revoked = await query_session_revoke_all_for_account(route, ctx.account.id);
|
|
391
401
|
const tokens_revoked = await query_revoke_all_api_tokens_for_account(route, ctx.account.id);
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
//
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
// (`transports_ws_auth_guard` on the `password_change` event)
|
|
403
|
-
// runs the same close afterward; idempotent on the second pass.
|
|
404
|
-
// Mirrors `zzz_server::account::password_inner`.
|
|
405
|
-
if (connection_closer) {
|
|
406
|
-
connection_closer.close_sockets_for_account(ctx.account.id);
|
|
407
|
-
}
|
|
402
|
+
// Close every live connection of this account once the password
|
|
403
|
+
// update and the revoke-all cascade have committed — this route
|
|
404
|
+
// runs with the default `transaction: true`, and the queued close
|
|
405
|
+
// never runs before that commit or on rollback. It does not
|
|
406
|
+
// depend on the fail-open audit write below; the audit listeners
|
|
407
|
+
// repeat it on the announced `password_change` row.
|
|
408
|
+
queue_connection_close(route, connection_closer, {
|
|
409
|
+
kind: 'account',
|
|
410
|
+
account_id: ctx.account.id
|
|
411
|
+
});
|
|
408
412
|
clear_session_cookie(c, session_options);
|
|
409
413
|
// Account-grain operation — no `actor_id`. The password is
|
|
410
414
|
// account-level state; which per-request actor was resolved
|