@fuzdev/fuz_app 0.117.1 → 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 +125 -31
- 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 +72 -44
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +60 -16
- 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/bootstrap_account.d.ts +3 -2
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +3 -3
- package/dist/auth/cleanup.d.ts +219 -30
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +260 -45
- package/dist/auth/deps.d.ts +29 -1
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +27 -0
- 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/role_grant_offer_ddl.d.ts +4 -2
- package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_ddl.js +10 -3
- package/dist/auth/role_grant_offer_queries.d.ts +16 -7
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +33 -15
- package/dist/auth/role_grant_offer_schema.d.ts +9 -0
- package/dist/auth/role_grant_offer_schema.d.ts.map +1 -1
- 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/migrate.d.ts +8 -13
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +9 -38
- package/dist/db/status.d.ts +58 -7
- package/dist/db/status.d.ts.map +1 -1
- package/dist/db/status.js +205 -97
- 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/runtime/deno.d.ts.map +1 -1
- package/dist/runtime/deno.js +9 -6
- package/dist/runtime/deps.d.ts +3 -2
- package/dist/runtime/deps.d.ts.map +1 -1
- package/dist/runtime/secure_file.d.ts +23 -9
- package/dist/runtime/secure_file.d.ts.map +1 -1
- package/dist/runtime/secure_file.js +29 -10
- package/dist/server/app_backend.d.ts +13 -2
- 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 +3 -52
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +2 -46
- 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/expected_schema.json +1 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +4 -1
- package/dist/testing/cross_backend/session_cap.d.ts +13 -0
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/session_cap.js +55 -19
- package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +93 -26
- package/dist/testing/cross_backend/testing_server_bun.js +4 -4
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -2
- package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
- package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +156 -19
- package/dist/testing/sse_round_trip.d.ts +6 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/stubs.d.ts +1 -1
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +6 -4
- package/dist/testing/transports/sse_transport.d.ts +1 -1
- package/dist/testing/transports/ws_client.d.ts +7 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -1
- package/dist/testing/transports/ws_raw_client.d.ts +51 -0
- package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
- package/dist/testing/transports/ws_raw_client.js +156 -0
- package/dist/testing/transports/ws_transport.d.ts +28 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -1
- package/dist/testing/transports/ws_transport.js +45 -1
- package/dist/testing/ws_round_trip.d.ts +8 -3
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +19 -4
- package/dist/ui/AdminAccounts.svelte +4 -3
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +3 -1
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +22 -6
- package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +16 -0
- package/dist/ui/auth_state.svelte.d.ts +25 -1
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +28 -1
- package/package.json +2 -2
package/dist/auth/cleanup.d.ts
CHANGED
|
@@ -1,71 +1,260 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Periodic auth cleanup — sweeps expired sessions and role_grant offers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* `run_auth_cleanup` is one pass: it deletes expired sessions and closes the
|
|
5
|
+
* connections they opened (`cleanup_expired_sessions`), then audits newly
|
|
6
|
+
* expired offers (`cleanup_expired_role_grant_offers`), returning both counts.
|
|
7
|
+
* `start_auth_cleanup` schedules it — the one call a server makes, and what
|
|
8
|
+
* `create_app_server`'s `auth_cleanup` option makes for it. The per-pass
|
|
9
|
+
* pieces stay exported for a consumer that wants its own cadence or error
|
|
10
|
+
* isolation per sweep. Twin of the Rust spine's `fuz_auth::auth_cleanup`.
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
* (`query_session_cleanup_expired`, `query_role_grant_offer_sweep_expired`);
|
|
10
|
-
* `cleanup_expired_role_grant_offers` here wraps the latter with the required
|
|
11
|
-
* `role_grant_offer_expire` audit emission and is the piece most likely to be
|
|
12
|
-
* reused in a consumer's bespoke scheduler.
|
|
12
|
+
* ## Expired sessions: the sweep closes what it sweeps
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* `
|
|
14
|
+
* A session past `expires_at` fails every validity check, but the connections
|
|
15
|
+
* it opened do not read it again: a WebSocket re-authorizes the actor per
|
|
16
|
+
* message, never the session, and an SSE stream is authorized once at open.
|
|
17
|
+
* While the expired row lingers, `account_session_revoke` is the per-session
|
|
18
|
+
* handle on those connections — it closes for the row it deletes. The sweep
|
|
19
|
+
* deletes that row, so it takes over the close: every session it deletes has
|
|
20
|
+
* its connections closed through `AuthCleanupDeps.connection_closer`, the same
|
|
21
|
+
* close a revocation makes (`WS_CLOSE_SESSION_REVOKED` on a WebSocket; an SSE
|
|
22
|
+
* stream ends). Otherwise a swept session's connections would be reachable
|
|
23
|
+
* only by an account-wide revocation.
|
|
24
|
+
*
|
|
25
|
+
* The contract that follows: **a connection outlives its session's expiry by
|
|
26
|
+
* at most one cleanup interval** (plus the pass itself). In a process that
|
|
27
|
+
* does not schedule the sweep, expiry ends no connection at all. The contract
|
|
28
|
+
* is for sessions only: an expired API token's row is not swept, so nothing
|
|
29
|
+
* here closes a connection opened on one — and `account_token_revoke` still
|
|
30
|
+
* finds the row to close for.
|
|
31
|
+
*
|
|
32
|
+
* The closes run after the delete's commit, which is the rule every
|
|
33
|
+
* revocation close keeps (see `actions/connection_closer.ts`). An expired
|
|
34
|
+
* session is refused with or without its row, so the order is not what
|
|
35
|
+
* protects a client rechecking on the close or a connection still opening; it
|
|
36
|
+
* keeps the sweep inside the one invariant admission leans on — a close that
|
|
37
|
+
* missed a registration belongs to a write the re-read can see — rather than
|
|
38
|
+
* on the re-read's clock. A commit whose outcome is unknown still closes: the
|
|
39
|
+
* sessions are expired either way.
|
|
40
|
+
*
|
|
41
|
+
* That is why the delete runs in its own transaction rather than as one
|
|
42
|
+
* pool-level statement. The ids are in hand before the commit is sent, so a
|
|
43
|
+
* connection lost on the delete's response rolls the delete back for the next
|
|
44
|
+
* pass, and one lost on the commit's still closes — no failure leaves rows
|
|
45
|
+
* deleted with their ids unread. A commit that fails outright has closed too;
|
|
46
|
+
* the rows it left are deleted by the next pass, whose closes then find
|
|
47
|
+
* nothing.
|
|
48
|
+
*
|
|
49
|
+
* One pass closes for every session it swept; a session with no live
|
|
50
|
+
* connection costs a lookup and closes nothing. The pass logs one count, not
|
|
51
|
+
* a line per session.
|
|
52
|
+
*
|
|
53
|
+
* No audit row is written for a swept session: expiry is not an event anyone
|
|
54
|
+
* performed, and the session was already refused everywhere. So the audit
|
|
55
|
+
* listeners never see it, and the direct close is the only one.
|
|
56
|
+
*
|
|
57
|
+
* One limit: the closes reach this process's transports, as every revocation
|
|
58
|
+
* close does — a second process on the same database keeps the connections it
|
|
59
|
+
* holds on a session this one swept.
|
|
60
|
+
*
|
|
61
|
+
* ## Offer idempotency
|
|
62
|
+
*
|
|
63
|
+
* An expired pending offer has no terminal state — expiry is computed from
|
|
64
|
+
* `expires_at`, never written — so the offer sweep claims rows instead: it
|
|
65
|
+
* stamps `role_grant_offer.expire_audited_at` in the same transaction as each
|
|
66
|
+
* row's `role_grant_offer_expire` audit row, so every expiry is audited
|
|
67
|
+
* exactly once — a second run finds nothing, and concurrent runs claim
|
|
68
|
+
* disjoint rows (row locks serialize them on the re-checked stamp). A failed
|
|
69
|
+
* audit insert rolls the stamps back with it and the next run retries. A
|
|
70
|
+
* re-offer clears the stamp, so the refreshed offer's own expiry is audited
|
|
71
|
+
* too.
|
|
72
|
+
*
|
|
73
|
+
* ## Scheduling
|
|
74
|
+
*
|
|
75
|
+
* `start_auth_cleanup` runs the pass beside the server: a pass at once, then
|
|
76
|
+
* one per interval (`DEFAULT_AUTH_CLEANUP_INTERVAL_MS` unless the consumer has
|
|
77
|
+
* a reason to differ), until its `stop` is called. A failed pass — the
|
|
78
|
+
* database out of reach — is logged and retried on the next interval.
|
|
79
|
+
*
|
|
80
|
+
* Start it once migrations have run (the startup pass reads the auth tables),
|
|
81
|
+
* with the app's bound `AuditEmitter` so the expiry audits reach its listeners
|
|
82
|
+
* and the closer its revocation handlers close through. `AppDeps` is all of
|
|
83
|
+
* that, so the standard assembly is one option:
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* const server = await create_app_server({backend, auth_cleanup: true, ...});
|
|
87
|
+
* // on shutdown: stops the schedule, then closes the database
|
|
88
|
+
* await server.close();
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* and a hand-assembled server calls it directly:
|
|
92
|
+
*
|
|
93
|
+
* ```ts
|
|
94
|
+
* const auth_cleanup = start_auth_cleanup(backend.deps);
|
|
95
|
+
* // on shutdown, before the database closes
|
|
96
|
+
* await auth_cleanup.stop();
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* The loop is `start_periodic` (`periodic.ts`), which keeps the Rust spine's
|
|
100
|
+
* schedule (`fuz_sys::periodic`): passes never overlap, and a pass that
|
|
101
|
+
* overruns its interval delays the next one instead of bursting to catch up.
|
|
102
|
+
* It differs on shutdown. The Rust loop drops a pass in progress when its
|
|
103
|
+
* token is cancelled; a promise cannot be dropped, so `stop` ends the schedule
|
|
104
|
+
* at once and its promise resolves when the pass in progress — if there is
|
|
105
|
+
* one — has finished. Awaiting it before closing the database lets that pass
|
|
106
|
+
* end on a live pool rather than fail on a closed one.
|
|
18
107
|
*
|
|
19
108
|
* @module
|
|
20
109
|
*/
|
|
21
110
|
import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
22
111
|
import type { QueryDeps } from '../db/query_deps.js';
|
|
112
|
+
import { type PeriodicTask } from '../periodic.js';
|
|
113
|
+
import type { ConnectionCloser } from '../actions/connection_closer.js';
|
|
23
114
|
import type { AuditEmitter } from './audit_emitter.js';
|
|
24
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* How often `start_auth_cleanup` runs a pass after its startup pass
|
|
117
|
+
* (10 minutes), unless the consumer passes its own. Equal to the Rust spine's
|
|
118
|
+
* `DEFAULT_AUTH_CLEANUP_INTERVAL`.
|
|
119
|
+
*
|
|
120
|
+
* An expired session or offer is already refused by every check that reads
|
|
121
|
+
* it, so the interval bounds three things: how long a connection outlives
|
|
122
|
+
* the expiry of the session it opened on, how late an offer expiry's audit
|
|
123
|
+
* row lands, and how long dead rows linger.
|
|
124
|
+
*/
|
|
125
|
+
export declare const DEFAULT_AUTH_CLEANUP_INTERVAL_MS: number;
|
|
126
|
+
/**
|
|
127
|
+
* Dependencies for the cleanup helpers. `AppDeps` satisfies it structurally,
|
|
128
|
+
* so a server passes `backend.deps`.
|
|
129
|
+
*/
|
|
25
130
|
export interface AuthCleanupDeps extends QueryDeps {
|
|
26
131
|
log: Logger;
|
|
27
132
|
/**
|
|
28
|
-
* Bound audit emitter. `cleanup_expired_role_grant_offers` writes
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* production wiring always has a bound emitter on
|
|
32
|
-
* tests that need a no-op pass
|
|
133
|
+
* Bound audit emitter. `cleanup_expired_role_grant_offers` writes its audit
|
|
134
|
+
* rows in its own transaction and fans each one out through `audit.notify`
|
|
135
|
+
* after the commit, so listeners never see an event that rolled back.
|
|
136
|
+
* Required — production wiring always has a bound emitter on
|
|
137
|
+
* `AppDeps.audit`, and tests that need a no-op pass
|
|
138
|
+
* `create_test_audit_emitter()`.
|
|
33
139
|
*/
|
|
34
140
|
audit: AuditEmitter;
|
|
141
|
+
/**
|
|
142
|
+
* Closes the connections of each session the sweep deletes. Pass the
|
|
143
|
+
* closer the app's revocation handlers hold — `AppDeps.connection_closer`,
|
|
144
|
+
* the `RealtimeCloser` over every transport it serves. Required: a sweep
|
|
145
|
+
* with nothing to close through would strand the swept sessions'
|
|
146
|
+
* connections. `noop_connection_closer` is for a database no other
|
|
147
|
+
* process holds connections on — a sweep run beside a live server from
|
|
148
|
+
* another process deletes the rows that server's own pass would have
|
|
149
|
+
* closed for.
|
|
150
|
+
*/
|
|
151
|
+
connection_closer: ConnectionCloser;
|
|
35
152
|
}
|
|
36
|
-
/**
|
|
153
|
+
/** Counts from one `run_auth_cleanup` pass. The Rust `AuthCleanupResult` is the same shape. */
|
|
37
154
|
export interface AuthCleanupResult {
|
|
38
|
-
/** Number of expired session rows deleted. */
|
|
155
|
+
/** Number of expired session rows deleted — each one's connections closed. */
|
|
39
156
|
expired_sessions: number;
|
|
40
157
|
/** Number of expired role_grant offer rows audit-stamped. */
|
|
41
158
|
expired_offers: number;
|
|
42
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Delete every expired session and close the connections each one opened,
|
|
162
|
+
* returning the count of sessions deleted.
|
|
163
|
+
*
|
|
164
|
+
* The delete runs in its own transaction and the closes follow its commit,
|
|
165
|
+
* so on a successful pass every close is for a row already gone (see the
|
|
166
|
+
* module doc's § Expired sessions). Each swept session is closed by its id —
|
|
167
|
+
* the session-token hash the transports key on — so another session of the
|
|
168
|
+
* same account keeps its connections. A pass that closed something logs the
|
|
169
|
+
* counts at `info`. No audit row is written.
|
|
170
|
+
*
|
|
171
|
+
* A closer that throws for one session does not spare the rest: every swept
|
|
172
|
+
* session is attempted and each failure is logged, as the post-commit queue
|
|
173
|
+
* does for a revocation's close.
|
|
174
|
+
*
|
|
175
|
+
* Twin of the Rust `cleanup_expired_sessions`.
|
|
176
|
+
*
|
|
177
|
+
* @returns how many expired sessions were deleted
|
|
178
|
+
* @mutates `auth_session` table - deletes every row past `expires_at`
|
|
179
|
+
* @throws Error from opening the transaction, the delete, or the commit. The first two close nothing and delete nothing. A failed commit is thrown **after** the closes: the sessions the delete returned are expired whether or not the commit landed, so their connections are closed either way, and the next pass deletes any row the commit left.
|
|
180
|
+
*/
|
|
181
|
+
export declare const cleanup_expired_sessions: (deps: AuthCleanupDeps) => Promise<number>;
|
|
43
182
|
/**
|
|
44
183
|
* Sweep expired role_grant offers and emit one `role_grant_offer_expire` audit
|
|
45
|
-
* event per row.
|
|
184
|
+
* event per newly expired row.
|
|
46
185
|
*
|
|
47
|
-
*
|
|
186
|
+
* One transaction: `query_role_grant_offer_sweep_expired` claims the expired,
|
|
187
|
+
* not-yet-audited offers (stamping `expire_audited_at`) and each claimed row's
|
|
188
|
+
* audit row is inserted beside it. After the commit, each event fans out
|
|
189
|
+
* through `deps.audit.notify` — per-listener throws are isolated there, so one
|
|
190
|
+
* bad listener never starves the rest. Twin of the Rust
|
|
191
|
+
* `cleanup_expired_role_grant_offers`.
|
|
192
|
+
*
|
|
193
|
+
* Returns the count of offers audited. The offer rows themselves are
|
|
48
194
|
* preserved — offers carry audit value for the history view even after
|
|
49
195
|
* expiry, and accepted rows are the provenance for the resulting role_grant
|
|
50
196
|
* (deleting expired rows would not threaten that, but keeping them uniform
|
|
51
197
|
* with the retention policy for terminal rows is simpler).
|
|
52
198
|
*
|
|
199
|
+
* @mutates `role_grant_offer` rows - stamps `expire_audited_at` on each swept offer
|
|
53
200
|
* @mutates `audit_log` table - inserts one `role_grant_offer_expire` row per swept offer
|
|
201
|
+
* @throws Error if the claim or an audit insert fails — the transaction rolls back, so no offer is stamped and the next run retries
|
|
54
202
|
*/
|
|
55
203
|
export declare const cleanup_expired_role_grant_offers: (deps: AuthCleanupDeps) => Promise<number>;
|
|
56
204
|
/**
|
|
57
|
-
* Run every auth cleanup sweep — expired sessions
|
|
58
|
-
* offers — and return
|
|
205
|
+
* Run every auth cleanup sweep — expired sessions, then expired role_grant
|
|
206
|
+
* offers — and return both counts. Twin of the Rust `run_auth_cleanup`.
|
|
207
|
+
*
|
|
208
|
+
* The session sweep (`cleanup_expired_sessions`) commits its delete and
|
|
209
|
+
* closes the swept sessions' connections before the offer sweep starts, so a
|
|
210
|
+
* failed offer sweep takes neither back; a session sweep whose commit failed
|
|
211
|
+
* has closed too, and the offer sweep does not run.
|
|
59
212
|
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* directly if you need finer error isolation.
|
|
213
|
+
* `start_auth_cleanup` is the scheduler over this; call it directly for a
|
|
214
|
+
* one-off pass or from a scheduler of your own. Errors are re-thrown so the
|
|
215
|
+
* caller can log or alert; use the per-sweep helpers (`cleanup_expired_sessions`,
|
|
216
|
+
* `cleanup_expired_role_grant_offers`) for error isolation per sweep.
|
|
65
217
|
*
|
|
66
218
|
* @mutates `auth_session` table - deletes expired sessions
|
|
219
|
+
* @mutates `role_grant_offer` rows - stamps `expire_audited_at` on expired offers
|
|
67
220
|
* @mutates `audit_log` table - emits `role_grant_offer_expire` rows for expired offers
|
|
68
|
-
* @throws Error re-thrown from
|
|
221
|
+
* @throws Error re-thrown from the first sweep that fails (no per-sweep isolation here)
|
|
69
222
|
*/
|
|
70
223
|
export declare const run_auth_cleanup: (deps: AuthCleanupDeps) => Promise<AuthCleanupResult>;
|
|
224
|
+
/** Options for `start_auth_cleanup`. */
|
|
225
|
+
export interface AuthCleanupScheduleOptions {
|
|
226
|
+
/**
|
|
227
|
+
* Milliseconds between the start of one pass and the start of the next.
|
|
228
|
+
* Default `DEFAULT_AUTH_CLEANUP_INTERVAL_MS`. Must be a positive finite
|
|
229
|
+
* number no greater than `PERIODIC_INTERVAL_MS_MAX`.
|
|
230
|
+
*/
|
|
231
|
+
interval_ms?: number;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Run `run_auth_cleanup` beside the server: a pass now, then one every
|
|
235
|
+
* `interval_ms`, until `stop` is called.
|
|
236
|
+
*
|
|
237
|
+
* The auth surface's upkeep in one call — start it once migrations have run
|
|
238
|
+
* and stop it before the database closes (see the module doc's § Scheduling).
|
|
239
|
+
* `create_app_server`'s `auth_cleanup` option does both.
|
|
240
|
+
*
|
|
241
|
+
* Built on `start_periodic`, which fixes the loop's rules: the startup pass
|
|
242
|
+
* starts before this returns, passes never overlap, a slow pass delays the
|
|
243
|
+
* schedule rather than bursting, a failed pass — the database out of reach —
|
|
244
|
+
* is logged at `warn` with its error and retried on the next interval, the
|
|
245
|
+
* timer is unref'd, and `stop` ends the schedule at once and resolves when a
|
|
246
|
+
* pass in progress has finished. A pass that swept something logs its counts
|
|
247
|
+
* at `info`, and one that closed a swept session's connections logs that
|
|
248
|
+
* count too.
|
|
249
|
+
*
|
|
250
|
+
* Twin of the Rust `spawn_auth_cleanup` over `fuz_sys::periodic::spawn_periodic`.
|
|
251
|
+
* The schedule is the same; shutdown differs — the Rust task drops a pass in
|
|
252
|
+
* progress when its token is cancelled, which a promise cannot do.
|
|
253
|
+
*
|
|
254
|
+
* @param deps - what a pass needs; `backend.deps` in a server
|
|
255
|
+
* @param options - the interval
|
|
256
|
+
* @returns the schedule's `stop`
|
|
257
|
+
* @throws Error if `interval_ms` is not a positive finite number or exceeds `PERIODIC_INTERVAL_MS_MAX` — at the call site, before any pass starts
|
|
258
|
+
*/
|
|
259
|
+
export declare const start_auth_cleanup: (deps: AuthCleanupDeps, options?: AuthCleanupScheduleOptions) => PeriodicTask;
|
|
71
260
|
//# sourceMappingURL=cleanup.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanup.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cleanup.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cleanup.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cleanup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4GG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAkB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAGxE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAIvD;;;;;;;;;GASG;AACH,eAAO,MAAM,gCAAgC,QAAiB,CAAC;AAE/D;;;GAGG;AACH,MAAM,WAAW,eAAgB,SAAQ,SAAS;IACjD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;;;OAOG;IACH,KAAK,EAAE,YAAY,CAAC;IACpB;;;;;;;;;OASG;IACH,iBAAiB,EAAE,gBAAgB,CAAC;CACpC;AAED,+FAA+F;AAC/F,MAAM,WAAW,iBAAiB;IACjC,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,CAAC;IACzB,6DAA6D;IAC7D,cAAc,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,wBAAwB,GAAU,MAAM,eAAe,KAAG,OAAO,CAAC,MAAM,CAqCpF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iCAAiC,GAAU,MAAM,eAAe,KAAG,OAAO,CAAC,MAAM,CAgC7F,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,gBAAgB,GAAU,MAAM,eAAe,KAAG,OAAO,CAAC,iBAAiB,CAIvF,CAAC;AAEF,wCAAwC;AACxC,MAAM,WAAW,0BAA0B;IAC1C;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,eAAO,MAAM,kBAAkB,GAC9B,MAAM,eAAe,EACrB,UAAS,0BAA+B,KACtC,YAWD,CAAC"}
|
package/dist/auth/cleanup.js
CHANGED
|
@@ -1,77 +1,292 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Periodic auth cleanup — sweeps expired sessions and role_grant offers.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* `run_auth_cleanup` is one pass: it deletes expired sessions and closes the
|
|
5
|
+
* connections they opened (`cleanup_expired_sessions`), then audits newly
|
|
6
|
+
* expired offers (`cleanup_expired_role_grant_offers`), returning both counts.
|
|
7
|
+
* `start_auth_cleanup` schedules it — the one call a server makes, and what
|
|
8
|
+
* `create_app_server`'s `auth_cleanup` option makes for it. The per-pass
|
|
9
|
+
* pieces stay exported for a consumer that wants its own cadence or error
|
|
10
|
+
* isolation per sweep. Twin of the Rust spine's `fuz_auth::auth_cleanup`.
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
* (`query_session_cleanup_expired`, `query_role_grant_offer_sweep_expired`);
|
|
10
|
-
* `cleanup_expired_role_grant_offers` here wraps the latter with the required
|
|
11
|
-
* `role_grant_offer_expire` audit emission and is the piece most likely to be
|
|
12
|
-
* reused in a consumer's bespoke scheduler.
|
|
12
|
+
* ## Expired sessions: the sweep closes what it sweeps
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* `
|
|
14
|
+
* A session past `expires_at` fails every validity check, but the connections
|
|
15
|
+
* it opened do not read it again: a WebSocket re-authorizes the actor per
|
|
16
|
+
* message, never the session, and an SSE stream is authorized once at open.
|
|
17
|
+
* While the expired row lingers, `account_session_revoke` is the per-session
|
|
18
|
+
* handle on those connections — it closes for the row it deletes. The sweep
|
|
19
|
+
* deletes that row, so it takes over the close: every session it deletes has
|
|
20
|
+
* its connections closed through `AuthCleanupDeps.connection_closer`, the same
|
|
21
|
+
* close a revocation makes (`WS_CLOSE_SESSION_REVOKED` on a WebSocket; an SSE
|
|
22
|
+
* stream ends). Otherwise a swept session's connections would be reachable
|
|
23
|
+
* only by an account-wide revocation.
|
|
24
|
+
*
|
|
25
|
+
* The contract that follows: **a connection outlives its session's expiry by
|
|
26
|
+
* at most one cleanup interval** (plus the pass itself). In a process that
|
|
27
|
+
* does not schedule the sweep, expiry ends no connection at all. The contract
|
|
28
|
+
* is for sessions only: an expired API token's row is not swept, so nothing
|
|
29
|
+
* here closes a connection opened on one — and `account_token_revoke` still
|
|
30
|
+
* finds the row to close for.
|
|
31
|
+
*
|
|
32
|
+
* The closes run after the delete's commit, which is the rule every
|
|
33
|
+
* revocation close keeps (see `actions/connection_closer.ts`). An expired
|
|
34
|
+
* session is refused with or without its row, so the order is not what
|
|
35
|
+
* protects a client rechecking on the close or a connection still opening; it
|
|
36
|
+
* keeps the sweep inside the one invariant admission leans on — a close that
|
|
37
|
+
* missed a registration belongs to a write the re-read can see — rather than
|
|
38
|
+
* on the re-read's clock. A commit whose outcome is unknown still closes: the
|
|
39
|
+
* sessions are expired either way.
|
|
40
|
+
*
|
|
41
|
+
* That is why the delete runs in its own transaction rather than as one
|
|
42
|
+
* pool-level statement. The ids are in hand before the commit is sent, so a
|
|
43
|
+
* connection lost on the delete's response rolls the delete back for the next
|
|
44
|
+
* pass, and one lost on the commit's still closes — no failure leaves rows
|
|
45
|
+
* deleted with their ids unread. A commit that fails outright has closed too;
|
|
46
|
+
* the rows it left are deleted by the next pass, whose closes then find
|
|
47
|
+
* nothing.
|
|
48
|
+
*
|
|
49
|
+
* One pass closes for every session it swept; a session with no live
|
|
50
|
+
* connection costs a lookup and closes nothing. The pass logs one count, not
|
|
51
|
+
* a line per session.
|
|
52
|
+
*
|
|
53
|
+
* No audit row is written for a swept session: expiry is not an event anyone
|
|
54
|
+
* performed, and the session was already refused everywhere. So the audit
|
|
55
|
+
* listeners never see it, and the direct close is the only one.
|
|
56
|
+
*
|
|
57
|
+
* One limit: the closes reach this process's transports, as every revocation
|
|
58
|
+
* close does — a second process on the same database keeps the connections it
|
|
59
|
+
* holds on a session this one swept.
|
|
60
|
+
*
|
|
61
|
+
* ## Offer idempotency
|
|
62
|
+
*
|
|
63
|
+
* An expired pending offer has no terminal state — expiry is computed from
|
|
64
|
+
* `expires_at`, never written — so the offer sweep claims rows instead: it
|
|
65
|
+
* stamps `role_grant_offer.expire_audited_at` in the same transaction as each
|
|
66
|
+
* row's `role_grant_offer_expire` audit row, so every expiry is audited
|
|
67
|
+
* exactly once — a second run finds nothing, and concurrent runs claim
|
|
68
|
+
* disjoint rows (row locks serialize them on the re-checked stamp). A failed
|
|
69
|
+
* audit insert rolls the stamps back with it and the next run retries. A
|
|
70
|
+
* re-offer clears the stamp, so the refreshed offer's own expiry is audited
|
|
71
|
+
* too.
|
|
72
|
+
*
|
|
73
|
+
* ## Scheduling
|
|
74
|
+
*
|
|
75
|
+
* `start_auth_cleanup` runs the pass beside the server: a pass at once, then
|
|
76
|
+
* one per interval (`DEFAULT_AUTH_CLEANUP_INTERVAL_MS` unless the consumer has
|
|
77
|
+
* a reason to differ), until its `stop` is called. A failed pass — the
|
|
78
|
+
* database out of reach — is logged and retried on the next interval.
|
|
79
|
+
*
|
|
80
|
+
* Start it once migrations have run (the startup pass reads the auth tables),
|
|
81
|
+
* with the app's bound `AuditEmitter` so the expiry audits reach its listeners
|
|
82
|
+
* and the closer its revocation handlers close through. `AppDeps` is all of
|
|
83
|
+
* that, so the standard assembly is one option:
|
|
84
|
+
*
|
|
85
|
+
* ```ts
|
|
86
|
+
* const server = await create_app_server({backend, auth_cleanup: true, ...});
|
|
87
|
+
* // on shutdown: stops the schedule, then closes the database
|
|
88
|
+
* await server.close();
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* and a hand-assembled server calls it directly:
|
|
92
|
+
*
|
|
93
|
+
* ```ts
|
|
94
|
+
* const auth_cleanup = start_auth_cleanup(backend.deps);
|
|
95
|
+
* // on shutdown, before the database closes
|
|
96
|
+
* await auth_cleanup.stop();
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* The loop is `start_periodic` (`periodic.ts`), which keeps the Rust spine's
|
|
100
|
+
* schedule (`fuz_sys::periodic`): passes never overlap, and a pass that
|
|
101
|
+
* overruns its interval delays the next one instead of bursting to catch up.
|
|
102
|
+
* It differs on shutdown. The Rust loop drops a pass in progress when its
|
|
103
|
+
* token is cancelled; a promise cannot be dropped, so `stop` ends the schedule
|
|
104
|
+
* at once and its promise resolves when the pass in progress — if there is
|
|
105
|
+
* one — has finished. Awaiting it before closing the database lets that pass
|
|
106
|
+
* end on a live pool rather than fail on a closed one.
|
|
18
107
|
*
|
|
19
108
|
* @module
|
|
20
109
|
*/
|
|
110
|
+
import { start_periodic } from '../periodic.js';
|
|
21
111
|
import { query_session_cleanup_expired } from './session_queries.js';
|
|
22
112
|
import { query_role_grant_offer_sweep_expired } from './role_grant_offer_queries.js';
|
|
113
|
+
import { query_audit_log } from './audit_log_queries.js';
|
|
114
|
+
/**
|
|
115
|
+
* How often `start_auth_cleanup` runs a pass after its startup pass
|
|
116
|
+
* (10 minutes), unless the consumer passes its own. Equal to the Rust spine's
|
|
117
|
+
* `DEFAULT_AUTH_CLEANUP_INTERVAL`.
|
|
118
|
+
*
|
|
119
|
+
* An expired session or offer is already refused by every check that reads
|
|
120
|
+
* it, so the interval bounds three things: how long a connection outlives
|
|
121
|
+
* the expiry of the session it opened on, how late an offer expiry's audit
|
|
122
|
+
* row lands, and how long dead rows linger.
|
|
123
|
+
*/
|
|
124
|
+
export const DEFAULT_AUTH_CLEANUP_INTERVAL_MS = 10 * 60 * 1000;
|
|
125
|
+
/**
|
|
126
|
+
* Delete every expired session and close the connections each one opened,
|
|
127
|
+
* returning the count of sessions deleted.
|
|
128
|
+
*
|
|
129
|
+
* The delete runs in its own transaction and the closes follow its commit,
|
|
130
|
+
* so on a successful pass every close is for a row already gone (see the
|
|
131
|
+
* module doc's § Expired sessions). Each swept session is closed by its id —
|
|
132
|
+
* the session-token hash the transports key on — so another session of the
|
|
133
|
+
* same account keeps its connections. A pass that closed something logs the
|
|
134
|
+
* counts at `info`. No audit row is written.
|
|
135
|
+
*
|
|
136
|
+
* A closer that throws for one session does not spare the rest: every swept
|
|
137
|
+
* session is attempted and each failure is logged, as the post-commit queue
|
|
138
|
+
* does for a revocation's close.
|
|
139
|
+
*
|
|
140
|
+
* Twin of the Rust `cleanup_expired_sessions`.
|
|
141
|
+
*
|
|
142
|
+
* @returns how many expired sessions were deleted
|
|
143
|
+
* @mutates `auth_session` table - deletes every row past `expires_at`
|
|
144
|
+
* @throws Error from opening the transaction, the delete, or the commit. The first two close nothing and delete nothing. A failed commit is thrown **after** the closes: the sessions the delete returned are expired whether or not the commit landed, so their connections are closed either way, and the next pass deletes any row the commit left.
|
|
145
|
+
*/
|
|
146
|
+
export const cleanup_expired_sessions = async (deps) => {
|
|
147
|
+
let swept = [];
|
|
148
|
+
let failed = false;
|
|
149
|
+
let failure;
|
|
150
|
+
try {
|
|
151
|
+
await deps.db.transaction(async (tx) => {
|
|
152
|
+
// the ids are in hand before the commit is sent: a connection lost on
|
|
153
|
+
// the delete's response rolls the delete back for the next pass, and
|
|
154
|
+
// one lost on the commit's still closes below
|
|
155
|
+
swept = await query_session_cleanup_expired({ db: tx });
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
// thrown before the delete answered, `swept` is empty and nothing closes;
|
|
160
|
+
// thrown after, the commit failed or its answer was lost
|
|
161
|
+
failed = true;
|
|
162
|
+
failure = error;
|
|
163
|
+
}
|
|
164
|
+
// the transaction has settled. The closes are synchronous, so nothing can
|
|
165
|
+
// interleave between the first and the last
|
|
166
|
+
let closed_connections = 0;
|
|
167
|
+
for (const session_token_hash of swept) {
|
|
168
|
+
try {
|
|
169
|
+
closed_connections += deps.connection_closer.close_sockets_for_session(session_token_hash);
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
// keep going — one transport's failure must not leave another
|
|
173
|
+
// expired session's connections open
|
|
174
|
+
deps.log.error("auth cleanup failed to close an expired session's connections:", error);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (closed_connections > 0) {
|
|
178
|
+
deps.log.info('auth cleanup closed the connections of expired sessions', {
|
|
179
|
+
expired_sessions: swept.length,
|
|
180
|
+
closed_connections
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if (failed)
|
|
184
|
+
throw failure;
|
|
185
|
+
return swept.length;
|
|
186
|
+
};
|
|
23
187
|
/**
|
|
24
188
|
* Sweep expired role_grant offers and emit one `role_grant_offer_expire` audit
|
|
25
|
-
* event per row.
|
|
189
|
+
* event per newly expired row.
|
|
190
|
+
*
|
|
191
|
+
* One transaction: `query_role_grant_offer_sweep_expired` claims the expired,
|
|
192
|
+
* not-yet-audited offers (stamping `expire_audited_at`) and each claimed row's
|
|
193
|
+
* audit row is inserted beside it. After the commit, each event fans out
|
|
194
|
+
* through `deps.audit.notify` — per-listener throws are isolated there, so one
|
|
195
|
+
* bad listener never starves the rest. Twin of the Rust
|
|
196
|
+
* `cleanup_expired_role_grant_offers`.
|
|
26
197
|
*
|
|
27
|
-
* Returns the count of offers
|
|
198
|
+
* Returns the count of offers audited. The offer rows themselves are
|
|
28
199
|
* preserved — offers carry audit value for the history view even after
|
|
29
200
|
* expiry, and accepted rows are the provenance for the resulting role_grant
|
|
30
201
|
* (deleting expired rows would not threaten that, but keeping them uniform
|
|
31
202
|
* with the retention policy for terminal rows is simpler).
|
|
32
203
|
*
|
|
204
|
+
* @mutates `role_grant_offer` rows - stamps `expire_audited_at` on each swept offer
|
|
33
205
|
* @mutates `audit_log` table - inserts one `role_grant_offer_expire` row per swept offer
|
|
206
|
+
* @throws Error if the claim or an audit insert fails — the transaction rolls back, so no offer is stamped and the next run retries
|
|
34
207
|
*/
|
|
35
208
|
export const cleanup_expired_role_grant_offers = async (deps) => {
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
209
|
+
const events = await deps.db.transaction(async (tx) => {
|
|
210
|
+
const expired = await query_role_grant_offer_sweep_expired({ db: tx });
|
|
211
|
+
const written = [];
|
|
212
|
+
for (const offer of expired) {
|
|
213
|
+
// `role_grant_offer_expire` populates `target_actor_id` only when the
|
|
214
|
+
// offer was actor-targeted (`to_actor_id` set at create time).
|
|
215
|
+
// Account-grain offers (no `to_actor_id`) never bound to a
|
|
216
|
+
// specific actor and leave the field null. No request is behind a
|
|
217
|
+
// sweep, so `ip` stays null.
|
|
218
|
+
written.push(await query_audit_log({ db: tx }, {
|
|
219
|
+
event_type: 'role_grant_offer_expire',
|
|
220
|
+
actor_id: offer.from_actor_id,
|
|
221
|
+
target_account_id: offer.to_account_id,
|
|
222
|
+
target_actor_id: offer.to_actor_id,
|
|
223
|
+
ip: null,
|
|
224
|
+
metadata: {
|
|
225
|
+
offer_id: offer.id,
|
|
226
|
+
role: offer.role,
|
|
227
|
+
scope_id: offer.scope_id
|
|
228
|
+
}
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
return written;
|
|
232
|
+
});
|
|
233
|
+
for (const event of events)
|
|
234
|
+
deps.audit.notify(event);
|
|
235
|
+
return events.length;
|
|
58
236
|
};
|
|
59
237
|
/**
|
|
60
|
-
* Run every auth cleanup sweep — expired sessions
|
|
61
|
-
* offers — and return
|
|
238
|
+
* Run every auth cleanup sweep — expired sessions, then expired role_grant
|
|
239
|
+
* offers — and return both counts. Twin of the Rust `run_auth_cleanup`.
|
|
240
|
+
*
|
|
241
|
+
* The session sweep (`cleanup_expired_sessions`) commits its delete and
|
|
242
|
+
* closes the swept sessions' connections before the offer sweep starts, so a
|
|
243
|
+
* failed offer sweep takes neither back; a session sweep whose commit failed
|
|
244
|
+
* has closed too, and the offer sweep does not run.
|
|
62
245
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* directly if you need finer error isolation.
|
|
246
|
+
* `start_auth_cleanup` is the scheduler over this; call it directly for a
|
|
247
|
+
* one-off pass or from a scheduler of your own. Errors are re-thrown so the
|
|
248
|
+
* caller can log or alert; use the per-sweep helpers (`cleanup_expired_sessions`,
|
|
249
|
+
* `cleanup_expired_role_grant_offers`) for error isolation per sweep.
|
|
68
250
|
*
|
|
69
251
|
* @mutates `auth_session` table - deletes expired sessions
|
|
252
|
+
* @mutates `role_grant_offer` rows - stamps `expire_audited_at` on expired offers
|
|
70
253
|
* @mutates `audit_log` table - emits `role_grant_offer_expire` rows for expired offers
|
|
71
|
-
* @throws Error re-thrown from
|
|
254
|
+
* @throws Error re-thrown from the first sweep that fails (no per-sweep isolation here)
|
|
72
255
|
*/
|
|
73
256
|
export const run_auth_cleanup = async (deps) => {
|
|
74
|
-
const expired_sessions = await
|
|
257
|
+
const expired_sessions = await cleanup_expired_sessions(deps);
|
|
75
258
|
const expired_offers = await cleanup_expired_role_grant_offers(deps);
|
|
76
259
|
return { expired_sessions, expired_offers };
|
|
77
260
|
};
|
|
261
|
+
/**
|
|
262
|
+
* Run `run_auth_cleanup` beside the server: a pass now, then one every
|
|
263
|
+
* `interval_ms`, until `stop` is called.
|
|
264
|
+
*
|
|
265
|
+
* The auth surface's upkeep in one call — start it once migrations have run
|
|
266
|
+
* and stop it before the database closes (see the module doc's § Scheduling).
|
|
267
|
+
* `create_app_server`'s `auth_cleanup` option does both.
|
|
268
|
+
*
|
|
269
|
+
* Built on `start_periodic`, which fixes the loop's rules: the startup pass
|
|
270
|
+
* starts before this returns, passes never overlap, a slow pass delays the
|
|
271
|
+
* schedule rather than bursting, a failed pass — the database out of reach —
|
|
272
|
+
* is logged at `warn` with its error and retried on the next interval, the
|
|
273
|
+
* timer is unref'd, and `stop` ends the schedule at once and resolves when a
|
|
274
|
+
* pass in progress has finished. A pass that swept something logs its counts
|
|
275
|
+
* at `info`, and one that closed a swept session's connections logs that
|
|
276
|
+
* count too.
|
|
277
|
+
*
|
|
278
|
+
* Twin of the Rust `spawn_auth_cleanup` over `fuz_sys::periodic::spawn_periodic`.
|
|
279
|
+
* The schedule is the same; shutdown differs — the Rust task drops a pass in
|
|
280
|
+
* progress when its token is cancelled, which a promise cannot do.
|
|
281
|
+
*
|
|
282
|
+
* @param deps - what a pass needs; `backend.deps` in a server
|
|
283
|
+
* @param options - the interval
|
|
284
|
+
* @returns the schedule's `stop`
|
|
285
|
+
* @throws Error if `interval_ms` is not a positive finite number or exceeds `PERIODIC_INTERVAL_MS_MAX` — at the call site, before any pass starts
|
|
286
|
+
*/
|
|
287
|
+
export const start_auth_cleanup = (deps, options = {}) => start_periodic('auth cleanup', options.interval_ms ?? DEFAULT_AUTH_CLEANUP_INTERVAL_MS, async () => {
|
|
288
|
+
const swept = await run_auth_cleanup(deps);
|
|
289
|
+
if (swept.expired_sessions > 0 || swept.expired_offers > 0) {
|
|
290
|
+
deps.log.info('auth cleanup swept expired rows', swept);
|
|
291
|
+
}
|
|
292
|
+
}, deps.log);
|