@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
|
@@ -1,65 +1,150 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* **Why this exists.**
|
|
6
|
-
* (`actions/perform_action.ts`) reloads
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
2
|
+
* Closing live connections — WebSocket and SSE — when the credential behind
|
|
3
|
+
* them is revoked.
|
|
4
|
+
*
|
|
5
|
+
* **Why this exists.** A WebSocket and an SSE stream are authorized once, at
|
|
6
|
+
* open. Per-message WebSocket dispatch (`actions/perform_action.ts`) reloads
|
|
7
|
+
* role grants, never the session or token, and a one-way stream re-reads
|
|
8
|
+
* nothing. So a revocation takes effect on an open connection only when
|
|
9
|
+
* something closes it. `ConnectionCloser` is that capability, and every
|
|
10
|
+
* revocation handler holds one: the session and token revokes, the revoke-alls,
|
|
11
|
+
* account delete and purge, `/logout`, `/password`, and the session and token
|
|
12
|
+
* caps for what they evict. The auth cleanup holds one too
|
|
13
|
+
* (`AuthCleanupDeps.connection_closer`): its session sweep closes the
|
|
14
|
+
* connections of each expired session it deletes, after that delete commits.
|
|
15
|
+
*
|
|
16
|
+
* ## Closing after commit
|
|
17
|
+
*
|
|
18
|
+
* A revocation runs inside a request transaction, and it must not close a
|
|
19
|
+
* connection until that transaction commits. `queue_connection_close` defers
|
|
20
|
+
* the close onto the request's post-commit queue (`emit_after_commit`): it
|
|
21
|
+
* runs after the commit and is dropped on rollback, so a revocation that never
|
|
22
|
+
* happened closes nothing.
|
|
23
|
+
*
|
|
24
|
+
* Two things rest on that order. A client told "revoked" rechecks its session
|
|
25
|
+
* straight away, and before the commit the row it probes is still there. And
|
|
26
|
+
* connection admission registers a connection and then re-reads its
|
|
27
|
+
* credential (`revalidate_resolved_auth`): a close that missed the
|
|
28
|
+
* registration must belong to a revocation the re-read can see, which holds
|
|
29
|
+
* only if the close came after the commit.
|
|
30
|
+
*
|
|
31
|
+
* ## Why the handlers close directly
|
|
32
|
+
*
|
|
33
|
+
* The audit listeners (`create_ws_auth_guard`, `create_sse_auth_guard`) close
|
|
34
|
+
* connections too, but only once the audit row is written, and that write is
|
|
35
|
+
* pool-routed and fail-open. The handler's own close is the one that runs
|
|
36
|
+
* whatever happens to the audit write, so the closer it holds must reach every
|
|
37
|
+
* transport whose connections outlive their credential. `RealtimeCloser` is
|
|
38
|
+
* that fan-out, and `create_app_backend` puts one on `AppDeps`.
|
|
39
|
+
*
|
|
40
|
+
* Twin of the Rust spine's `SocketRevoker`, `SocketCloseTarget`,
|
|
41
|
+
* `queue_socket_close`, and `RealtimeRevoker`.
|
|
35
42
|
*
|
|
36
43
|
* @module
|
|
37
44
|
*/
|
|
45
|
+
import type { Uuid } from '@fuzdev/fuz_util/id.ts';
|
|
46
|
+
import { type EmitAfterCommitContext } from '../http/pending_effects.js';
|
|
38
47
|
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
48
|
+
* Close live connections by identity.
|
|
49
|
+
*
|
|
50
|
+
* Each method returns how many connections it closed. Closing is idempotent:
|
|
51
|
+
* a connection already gone isn't counted, so a handler's close and an audit
|
|
52
|
+
* listener's close on the same revocation compose. Implementations are
|
|
53
|
+
* synchronous.
|
|
54
|
+
*
|
|
55
|
+
* One connection that fails to close must not spare the rest: an
|
|
56
|
+
* implementation attempts every matching connection, then throws the first
|
|
57
|
+
* error it met.
|
|
58
|
+
*
|
|
59
|
+
* `BackendWebsocketTransport` satisfies this structurally, and
|
|
60
|
+
* `create_sse_connection_closer` adapts a `SubscriberRegistry`.
|
|
43
61
|
*/
|
|
44
62
|
export interface ConnectionCloser {
|
|
45
|
-
/**
|
|
46
|
-
* Close every connection authenticated with a session whose blake3
|
|
47
|
-
* hash matches `session_token_hash`. Idempotent — calling on an
|
|
48
|
-
* already-closed session is a no-op.
|
|
49
|
-
*/
|
|
63
|
+
/** Close every connection authenticated by the session whose blake3 hash is `session_token_hash`. */
|
|
50
64
|
close_sockets_for_session: (session_token_hash: string) => number;
|
|
65
|
+
/** Close every connection authenticated by the API token `api_token_id`. */
|
|
66
|
+
close_sockets_for_token: (api_token_id: string) => number;
|
|
51
67
|
/**
|
|
52
|
-
* Close every connection
|
|
53
|
-
*
|
|
68
|
+
* Close every connection of `account_id`, whatever credential opened it —
|
|
69
|
+
* the close for a revocation that invalidates all of an account's
|
|
70
|
+
* credentials.
|
|
54
71
|
*/
|
|
55
|
-
|
|
72
|
+
close_sockets_for_account: (account_id: Uuid) => number;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Which connections a revocation closes — the argument of
|
|
76
|
+
* `queue_connection_close`, one variant per `ConnectionCloser` method.
|
|
77
|
+
*/
|
|
78
|
+
export type ConnectionCloseTarget = {
|
|
79
|
+
kind: 'session';
|
|
80
|
+
session_token_hash: string;
|
|
81
|
+
} | {
|
|
82
|
+
kind: 'token';
|
|
83
|
+
api_token_id: string;
|
|
84
|
+
} | {
|
|
85
|
+
kind: 'account';
|
|
86
|
+
account_id: Uuid;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Close the connections `target` names, now.
|
|
90
|
+
*
|
|
91
|
+
* A revocation handler calls `queue_connection_close` instead, which runs this
|
|
92
|
+
* after the commit.
|
|
93
|
+
*
|
|
94
|
+
* @returns how many connections closed
|
|
95
|
+
*/
|
|
96
|
+
export declare const close_connections: (closer: ConnectionCloser, target: ConnectionCloseTarget) => number;
|
|
97
|
+
/**
|
|
98
|
+
* Queue a connection close on `ctx`'s post-commit queue, so it runs only after
|
|
99
|
+
* the request's transaction commits and never when the handler throws.
|
|
100
|
+
*
|
|
101
|
+
* The one way a revocation handler closes connections — see the module doc for
|
|
102
|
+
* why an in-transaction revocation must not close inline.
|
|
103
|
+
*
|
|
104
|
+
* In a handler with no wrapping transaction a throw still drops the queued
|
|
105
|
+
* close while an autocommit write stays; close with `close_connections`
|
|
106
|
+
* straight after the write there.
|
|
107
|
+
*
|
|
108
|
+
* @param ctx - the route or action context of the request doing the revoking
|
|
109
|
+
* @param closer - the closer to run, `deps.connection_closer` in a handler
|
|
110
|
+
* @param target - the connections to close
|
|
111
|
+
* @mutates `ctx.post_commit_effects` - appends the close
|
|
112
|
+
*/
|
|
113
|
+
export declare const queue_connection_close: (ctx: EmitAfterCommitContext, closer: ConnectionCloser, target: ConnectionCloseTarget) => void;
|
|
114
|
+
/**
|
|
115
|
+
* A `ConnectionCloser` that closes nothing — for a backend with no
|
|
116
|
+
* live-connection surface, where a revocation has nothing to close.
|
|
117
|
+
*/
|
|
118
|
+
export declare const noop_connection_closer: ConnectionCloser;
|
|
119
|
+
/**
|
|
120
|
+
* One `ConnectionCloser` over every live-connection transport of a backend.
|
|
121
|
+
*
|
|
122
|
+
* Each close fans out to every member and returns the sum of what each
|
|
123
|
+
* closed. A revocation handler holds this rather than one transport, so its
|
|
124
|
+
* close reaches the WebSocket transports and the SSE registries alike.
|
|
125
|
+
*
|
|
126
|
+
* A member that throws does not stop the fan-out: every member is still
|
|
127
|
+
* called, and the first error is thrown once they all have been — so the
|
|
128
|
+
* caller (the post-commit flush, an audit listener) logs it.
|
|
129
|
+
*/
|
|
130
|
+
export interface RealtimeCloser extends ConnectionCloser {
|
|
56
131
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
132
|
+
* Add a transport's closer. Idempotent by reference, so two endpoints
|
|
133
|
+
* sharing one transport add it once.
|
|
134
|
+
*
|
|
135
|
+
* Members must be the same instances connections are registered on — a
|
|
136
|
+
* closer over a fresh registry closes nothing.
|
|
62
137
|
*/
|
|
63
|
-
|
|
138
|
+
add: (closer: ConnectionCloser) => void;
|
|
64
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Create an empty `RealtimeCloser`.
|
|
142
|
+
*
|
|
143
|
+
* `create_app_backend` creates the one on `AppDeps.connection_closer`, and
|
|
144
|
+
* `create_app_server` adds each WebSocket transport it mounts and its audit
|
|
145
|
+
* stream registry. A consumer that mounts a transport by hand adds it through
|
|
146
|
+
* the `connection_closer` option of `register_ws_endpoint` /
|
|
147
|
+
* `create_audit_log_sse`, or with `add`.
|
|
148
|
+
*/
|
|
149
|
+
export declare const create_realtime_closer: () => RealtimeCloser;
|
|
65
150
|
//# sourceMappingURL=connection_closer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection_closer.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/connection_closer.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"connection_closer.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/connection_closer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAGnD,OAAO,EAAqB,KAAK,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AAE5F;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,gBAAgB;IAChC,qGAAqG;IACrG,yBAAyB,EAAE,CAAC,kBAAkB,EAAE,MAAM,KAAK,MAAM,CAAC;IAClE,4EAA4E;IAC5E,uBAAuB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1D;;;;OAIG;IACH,yBAAyB,EAAE,CAAC,UAAU,EAAE,IAAI,KAAK,MAAM,CAAC;CACxD;AAED;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAC9B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,kBAAkB,EAAE,MAAM,CAAA;CAAE,GAC/C;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,GACvC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,UAAU,EAAE,IAAI,CAAA;CAAE,CAAC;AAEzC;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,GAC7B,QAAQ,gBAAgB,EACxB,QAAQ,qBAAqB,KAC3B,MAWF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,GAClC,KAAK,sBAAsB,EAC3B,QAAQ,gBAAgB,EACxB,QAAQ,qBAAqB,KAC3B,IAIF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,sBAAsB,EAAE,gBAInC,CAAC;AAEH;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAe,SAAQ,gBAAgB;IACvD;;;;;;OAMG;IACH,GAAG,EAAE,CAAC,MAAM,EAAE,gBAAgB,KAAK,IAAI,CAAC;CACxC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,QAAO,cAgCzC,CAAC"}
|
|
@@ -1,38 +1,137 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* **Why this exists.**
|
|
6
|
-
* (`actions/perform_action.ts`) reloads
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* `
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
2
|
+
* Closing live connections — WebSocket and SSE — when the credential behind
|
|
3
|
+
* them is revoked.
|
|
4
|
+
*
|
|
5
|
+
* **Why this exists.** A WebSocket and an SSE stream are authorized once, at
|
|
6
|
+
* open. Per-message WebSocket dispatch (`actions/perform_action.ts`) reloads
|
|
7
|
+
* role grants, never the session or token, and a one-way stream re-reads
|
|
8
|
+
* nothing. So a revocation takes effect on an open connection only when
|
|
9
|
+
* something closes it. `ConnectionCloser` is that capability, and every
|
|
10
|
+
* revocation handler holds one: the session and token revokes, the revoke-alls,
|
|
11
|
+
* account delete and purge, `/logout`, `/password`, and the session and token
|
|
12
|
+
* caps for what they evict. The auth cleanup holds one too
|
|
13
|
+
* (`AuthCleanupDeps.connection_closer`): its session sweep closes the
|
|
14
|
+
* connections of each expired session it deletes, after that delete commits.
|
|
15
|
+
*
|
|
16
|
+
* ## Closing after commit
|
|
17
|
+
*
|
|
18
|
+
* A revocation runs inside a request transaction, and it must not close a
|
|
19
|
+
* connection until that transaction commits. `queue_connection_close` defers
|
|
20
|
+
* the close onto the request's post-commit queue (`emit_after_commit`): it
|
|
21
|
+
* runs after the commit and is dropped on rollback, so a revocation that never
|
|
22
|
+
* happened closes nothing.
|
|
23
|
+
*
|
|
24
|
+
* Two things rest on that order. A client told "revoked" rechecks its session
|
|
25
|
+
* straight away, and before the commit the row it probes is still there. And
|
|
26
|
+
* connection admission registers a connection and then re-reads its
|
|
27
|
+
* credential (`revalidate_resolved_auth`): a close that missed the
|
|
28
|
+
* registration must belong to a revocation the re-read can see, which holds
|
|
29
|
+
* only if the close came after the commit.
|
|
30
|
+
*
|
|
31
|
+
* ## Why the handlers close directly
|
|
32
|
+
*
|
|
33
|
+
* The audit listeners (`create_ws_auth_guard`, `create_sse_auth_guard`) close
|
|
34
|
+
* connections too, but only once the audit row is written, and that write is
|
|
35
|
+
* pool-routed and fail-open. The handler's own close is the one that runs
|
|
36
|
+
* whatever happens to the audit write, so the closer it holds must reach every
|
|
37
|
+
* transport whose connections outlive their credential. `RealtimeCloser` is
|
|
38
|
+
* that fan-out, and `create_app_backend` puts one on `AppDeps`.
|
|
39
|
+
*
|
|
40
|
+
* Twin of the Rust spine's `SocketRevoker`, `SocketCloseTarget`,
|
|
41
|
+
* `queue_socket_close`, and `RealtimeRevoker`.
|
|
35
42
|
*
|
|
36
43
|
* @module
|
|
37
44
|
*/
|
|
38
|
-
|
|
45
|
+
import { UnreachableError } from '@fuzdev/fuz_util/error.ts';
|
|
46
|
+
import { emit_after_commit } from '../http/pending_effects.js';
|
|
47
|
+
/**
|
|
48
|
+
* Close the connections `target` names, now.
|
|
49
|
+
*
|
|
50
|
+
* A revocation handler calls `queue_connection_close` instead, which runs this
|
|
51
|
+
* after the commit.
|
|
52
|
+
*
|
|
53
|
+
* @returns how many connections closed
|
|
54
|
+
*/
|
|
55
|
+
export const close_connections = (closer, target) => {
|
|
56
|
+
switch (target.kind) {
|
|
57
|
+
case 'session':
|
|
58
|
+
return closer.close_sockets_for_session(target.session_token_hash);
|
|
59
|
+
case 'token':
|
|
60
|
+
return closer.close_sockets_for_token(target.api_token_id);
|
|
61
|
+
case 'account':
|
|
62
|
+
return closer.close_sockets_for_account(target.account_id);
|
|
63
|
+
default:
|
|
64
|
+
throw new UnreachableError(target);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Queue a connection close on `ctx`'s post-commit queue, so it runs only after
|
|
69
|
+
* the request's transaction commits and never when the handler throws.
|
|
70
|
+
*
|
|
71
|
+
* The one way a revocation handler closes connections — see the module doc for
|
|
72
|
+
* why an in-transaction revocation must not close inline.
|
|
73
|
+
*
|
|
74
|
+
* In a handler with no wrapping transaction a throw still drops the queued
|
|
75
|
+
* close while an autocommit write stays; close with `close_connections`
|
|
76
|
+
* straight after the write there.
|
|
77
|
+
*
|
|
78
|
+
* @param ctx - the route or action context of the request doing the revoking
|
|
79
|
+
* @param closer - the closer to run, `deps.connection_closer` in a handler
|
|
80
|
+
* @param target - the connections to close
|
|
81
|
+
* @mutates `ctx.post_commit_effects` - appends the close
|
|
82
|
+
*/
|
|
83
|
+
export const queue_connection_close = (ctx, closer, target) => {
|
|
84
|
+
emit_after_commit(ctx, () => {
|
|
85
|
+
close_connections(closer, target);
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* A `ConnectionCloser` that closes nothing — for a backend with no
|
|
90
|
+
* live-connection surface, where a revocation has nothing to close.
|
|
91
|
+
*/
|
|
92
|
+
export const noop_connection_closer = Object.freeze({
|
|
93
|
+
close_sockets_for_session: () => 0,
|
|
94
|
+
close_sockets_for_token: () => 0,
|
|
95
|
+
close_sockets_for_account: () => 0
|
|
96
|
+
});
|
|
97
|
+
/**
|
|
98
|
+
* Create an empty `RealtimeCloser`.
|
|
99
|
+
*
|
|
100
|
+
* `create_app_backend` creates the one on `AppDeps.connection_closer`, and
|
|
101
|
+
* `create_app_server` adds each WebSocket transport it mounts and its audit
|
|
102
|
+
* stream registry. A consumer that mounts a transport by hand adds it through
|
|
103
|
+
* the `connection_closer` option of `register_ws_endpoint` /
|
|
104
|
+
* `create_audit_log_sse`, or with `add`.
|
|
105
|
+
*/
|
|
106
|
+
export const create_realtime_closer = () => {
|
|
107
|
+
const members = new Set();
|
|
108
|
+
const close_all = (close) => {
|
|
109
|
+
let count = 0;
|
|
110
|
+
let failed = false;
|
|
111
|
+
let first_error;
|
|
112
|
+
for (const member of members) {
|
|
113
|
+
try {
|
|
114
|
+
count += close(member);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
// keep going — one transport's failure must not leave another's
|
|
118
|
+
// connections open on a revoked credential
|
|
119
|
+
if (!failed) {
|
|
120
|
+
failed = true;
|
|
121
|
+
first_error = error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (failed)
|
|
126
|
+
throw first_error;
|
|
127
|
+
return count;
|
|
128
|
+
};
|
|
129
|
+
return Object.freeze({
|
|
130
|
+
add: (closer) => {
|
|
131
|
+
members.add(closer);
|
|
132
|
+
},
|
|
133
|
+
close_sockets_for_session: (session_token_hash) => close_all((member) => member.close_sockets_for_session(session_token_hash)),
|
|
134
|
+
close_sockets_for_token: (api_token_id) => close_all((member) => member.close_sockets_for_token(api_token_id)),
|
|
135
|
+
close_sockets_for_account: (account_id) => close_all((member) => member.close_sockets_for_account(account_id))
|
|
136
|
+
});
|
|
137
|
+
};
|