@fuzdev/fuz_app 0.139.0 → 0.141.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 +62 -17
- package/dist/actions/action_rpc.d.ts +5 -1
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +9 -1
- package/dist/actions/connection_closer.d.ts +25 -2
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +8 -1
- package/dist/actions/register_action_ws.d.ts +42 -6
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +87 -19
- package/dist/actions/register_ws_endpoint.d.ts +15 -6
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +5 -2
- package/dist/actions/socket.svelte.d.ts +19 -4
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +31 -4
- package/dist/actions/transports.d.ts +30 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +30 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +13 -6
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +24 -10
- package/dist/actions/transports_ws_backend.d.ts +51 -5
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +62 -9
- package/dist/actions/ws_endpoint_spec.d.ts +11 -10
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +11 -9
- package/dist/auth/account_route_schema.d.ts +1 -0
- package/dist/auth/account_route_schema.d.ts.map +1 -1
- package/dist/auth/account_route_schema.js +4 -0
- package/dist/auth/api_token.d.ts +20 -0
- package/dist/auth/api_token.d.ts.map +1 -1
- package/dist/auth/api_token.js +21 -1
- package/dist/auth/api_token_queries.d.ts +22 -4
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +11 -6
- package/dist/auth/audit_log_routes.d.ts +10 -0
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +20 -0
- package/dist/auth/audit_log_schema.d.ts +30 -5
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +30 -0
- package/dist/auth/bearer_auth.d.ts +4 -2
- package/dist/auth/bearer_auth.d.ts.map +1 -1
- package/dist/auth/bearer_auth.js +19 -8
- package/dist/auth/bootstrap_route_schema.d.ts +1 -0
- package/dist/auth/bootstrap_route_schema.d.ts.map +1 -1
- package/dist/auth/bootstrap_route_schema.js +1 -0
- package/dist/auth/cleanup.d.ts +9 -9
- package/dist/auth/cleanup.js +9 -9
- package/dist/auth/credential_deadline.d.ts +95 -0
- package/dist/auth/credential_deadline.d.ts.map +1 -0
- package/dist/auth/credential_deadline.js +139 -0
- package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
- package/dist/auth/daemon_token_middleware.js +4 -1
- package/dist/auth/middleware.d.ts +90 -12
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +115 -16
- package/dist/auth/request_context.d.ts +4 -2
- package/dist/auth/request_context.d.ts.map +1 -1
- package/dist/auth/request_context.js +14 -3
- package/dist/auth/resolved_auth.d.ts +13 -2
- package/dist/auth/resolved_auth.d.ts.map +1 -1
- package/dist/auth/resolved_auth.js +5 -4
- package/dist/auth/session_cookie.d.ts +4 -0
- package/dist/auth/session_cookie.d.ts.map +1 -1
- package/dist/auth/session_cookie.js +3 -2
- package/dist/auth/session_middleware.d.ts +1 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +4 -1
- package/dist/auth/session_queries.d.ts +19 -2
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +8 -2
- package/dist/auth/signup_route_schema.d.ts.map +1 -1
- package/dist/auth/signup_route_schema.js +1 -0
- package/dist/hono_context.d.ts +23 -0
- package/dist/hono_context.d.ts.map +1 -1
- package/dist/hono_context.js +19 -0
- package/dist/http/CLAUDE.md +41 -15
- package/dist/http/middleware_spec.d.ts +16 -0
- package/dist/http/middleware_spec.d.ts.map +1 -1
- package/dist/http/origin.d.ts +113 -18
- package/dist/http/origin.d.ts.map +1 -1
- package/dist/http/origin.js +247 -127
- package/dist/http/route_spec.d.ts +21 -0
- package/dist/http/route_spec.d.ts.map +1 -1
- package/dist/http/schema_helpers.d.ts +22 -0
- package/dist/http/schema_helpers.d.ts.map +1 -1
- package/dist/http/schema_helpers.js +24 -0
- package/dist/http/surface.d.ts +6 -1
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +12 -5
- package/dist/realtime/sse_auth_guard.d.ts +22 -6
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +31 -12
- package/dist/realtime/subscriber_registry.d.ts +10 -1
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +10 -1
- package/dist/server/app_server.d.ts +20 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +8 -3
- package/dist/server/env.d.ts +2 -1
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +7 -17
- package/dist/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +1 -0
- package/dist/testing/CLAUDE.md +154 -22
- package/dist/testing/adversarial_headers.d.ts.map +1 -1
- package/dist/testing/adversarial_headers.js +13 -6
- package/dist/testing/connection_closer_helpers.d.ts +6 -1
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +4 -0
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +14 -65
- package/dist/testing/cross_backend/capabilities.d.ts +26 -2
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +9 -3
- package/dist/testing/cross_backend/conformance_case.d.ts +11 -3
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_case.js +6 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
- package/dist/testing/cross_backend/conformance_table.js +20 -19
- package/dist/testing/cross_backend/credential_deadline.d.ts +25 -0
- package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -0
- package/dist/testing/cross_backend/credential_deadline.js +185 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +13 -2
- package/dist/testing/cross_backend/full_spine_mount.d.ts +3 -0
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +7 -1
- package/dist/testing/cross_backend/narrowed_api_token.d.ts +23 -0
- package/dist/testing/cross_backend/narrowed_api_token.d.ts.map +1 -0
- package/dist/testing/cross_backend/narrowed_api_token.js +40 -0
- package/dist/testing/cross_backend/origin.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin.js +6 -26
- package/dist/testing/cross_backend/origin_conformance.d.ts +152 -0
- package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin_conformance.js +136 -0
- package/dist/testing/cross_backend/origin_conformance.json +1679 -0
- package/dist/testing/cross_backend/origin_scope.d.ts +16 -0
- package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin_scope.js +140 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +35 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/role_grant_cross_helpers.js +64 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +8 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +44 -33
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -4
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +19 -0
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +22 -0
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +20 -7
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +180 -5
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +154 -5
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_scope_surface.js +25 -21
- package/dist/testing/cross_backend/transport_composition.d.ts +60 -0
- package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -0
- package/dist/testing/cross_backend/transport_composition.js +364 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +21 -6
- package/dist/testing/cross_backend/ws_role_gated.d.ts +38 -0
- package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_role_gated.js +142 -0
- package/dist/testing/entities.d.ts +23 -0
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +37 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +3 -1
- package/dist/testing/middleware.d.ts +10 -0
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +31 -7
- package/dist/testing/stubs.d.ts +16 -3
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +20 -7
- package/dist/testing/surface_invariants.js +2 -2
- package/package.json +12 -12
|
@@ -28,11 +28,16 @@
|
|
|
28
28
|
* two cannot drift into closing a revoked token's socket while leaving its
|
|
29
29
|
* stream open.
|
|
30
30
|
*
|
|
31
|
-
* `role`
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
31
|
+
* `role` closes the target account's sockets whose endpoint gated the upgrade
|
|
32
|
+
* on the revoked role (`close_sockets_for_role`), with
|
|
33
|
+
* `WS_CLOSE_PERMISSION_REVOKED`, when the revoked grant was global
|
|
34
|
+
* (`to_global_role_revocation` — a role gate admits on global grants only).
|
|
35
|
+
* `perform_action` re-authorizes every *request* against its own action, but
|
|
36
|
+
* the role gate is the only check on what the socket *receives*: broadcasts
|
|
37
|
+
* and notifications reach every admitted connection of the endpoint with no
|
|
38
|
+
* per-recipient filter, so a socket left open past the revocation would keep
|
|
39
|
+
* receiving what the gate exists to withhold. The role is the only revocation
|
|
40
|
+
* no handler closes for, so this guard is its one closer.
|
|
36
41
|
*
|
|
37
42
|
* For standard WS endpoints mounted via `AppServerOptions.ws_endpoints`,
|
|
38
43
|
* `create_app_server` registers the guard automatically per
|
|
@@ -45,7 +50,7 @@
|
|
|
45
50
|
* @module
|
|
46
51
|
*/
|
|
47
52
|
import { UnreachableError } from '@fuzdev/fuz_util/error.ts';
|
|
48
|
-
import { to_revocation_scope } from '../auth/audit_log_schema.js';
|
|
53
|
+
import { to_global_role_revocation, to_revocation_scope } from '../auth/audit_log_schema.js';
|
|
49
54
|
/**
|
|
50
55
|
* Create an audit event handler that closes connections on a successful
|
|
51
56
|
* revocation row, dispatching on the event's `RevocationScope`:
|
|
@@ -55,7 +60,9 @@ import { to_revocation_scope } from '../auth/audit_log_schema.js';
|
|
|
55
60
|
* - `account` → `close_sockets_for_account(target_account_id ?? account_id)`
|
|
56
61
|
* — `logout`, `password_change`, `session_revoke_all`, `token_revoke_all`,
|
|
57
62
|
* `account_delete`, `account_purge`
|
|
58
|
-
* - `role` →
|
|
63
|
+
* - `role` → `close_sockets_for_role(target_account_id ?? account_id,
|
|
64
|
+
* metadata.role)` when the revoked grant was global; a scoped revoke closes
|
|
65
|
+
* nothing (module doc)
|
|
59
66
|
* - `none` → nothing — every other event, and any event type fuz_app does not
|
|
60
67
|
* define
|
|
61
68
|
*
|
|
@@ -81,10 +88,17 @@ export const create_ws_auth_guard = (closer, log) => {
|
|
|
81
88
|
switch (scope) {
|
|
82
89
|
case 'none':
|
|
83
90
|
return;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
case 'role': {
|
|
92
|
+
// global grants only — a role gate never admitted on a scoped one
|
|
93
|
+
const revoked = to_global_role_revocation(event);
|
|
94
|
+
if (!revoked)
|
|
95
|
+
return;
|
|
96
|
+
const closed = closer.close_sockets_for_role(revoked.account_id, revoked.role);
|
|
97
|
+
if (closed > 0) {
|
|
98
|
+
log.info(`WS auth guard: closed ${closed} socket(s) gated on ${revoked.role} for account ${revoked.account_id} (${event.event_type})`);
|
|
99
|
+
}
|
|
87
100
|
return;
|
|
101
|
+
}
|
|
88
102
|
case 'session': {
|
|
89
103
|
const session_id = event.metadata?.session_id;
|
|
90
104
|
if (typeof session_id !== 'string' || session_id.length === 0)
|
|
@@ -124,7 +124,10 @@ import { type PeerRequestOptions, type PeerRequestOutcome } from './peer_request
|
|
|
124
124
|
* One record per connection. `token_hash` is set for cookie-session
|
|
125
125
|
* connections, `api_token_id` for bearer (`api_token`) connections, and
|
|
126
126
|
* both are null for daemon-token connections (reachable only via
|
|
127
|
-
* `BackendWebsocketTransport.close_sockets_for_account`).
|
|
127
|
+
* `BackendWebsocketTransport.close_sockets_for_account`). `required_roles`
|
|
128
|
+
* is what the connection's endpoint gated the upgrade on, kept per
|
|
129
|
+
* registration because endpoints with different gates can share one
|
|
130
|
+
* transport.
|
|
128
131
|
*/
|
|
129
132
|
export interface ConnectionIdentity {
|
|
130
133
|
/** Blake3 session token hash, or null for non-session credentials. */
|
|
@@ -133,6 +136,11 @@ export interface ConnectionIdentity {
|
|
|
133
136
|
account_id: Uuid;
|
|
134
137
|
/** `api_token.id` for bearer-authenticated connections, else null. */
|
|
135
138
|
api_token_id: string | null;
|
|
139
|
+
/**
|
|
140
|
+
* The roles the endpoint required at upgrade (any-of), or empty for an
|
|
141
|
+
* ungated endpoint — what `close_sockets_for_role` matches on.
|
|
142
|
+
*/
|
|
143
|
+
required_roles: ReadonlyArray<string>;
|
|
136
144
|
}
|
|
137
145
|
/**
|
|
138
146
|
* Structural capability for transports that can broadcast with a
|
|
@@ -227,8 +235,9 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
|
|
|
227
235
|
*
|
|
228
236
|
* The entry is closeable at once: a `close_sockets_for_*` matching
|
|
229
237
|
* `token_hash` / `account_id` / `api_token_id` removes it, aborts
|
|
230
|
-
* `abort_controller`, and closes `ws` with `WS_CLOSE_SESSION_REVOKED
|
|
231
|
-
*
|
|
238
|
+
* `abort_controller`, and closes `ws` with `WS_CLOSE_SESSION_REVOKED`
|
|
239
|
+
* (`close_sockets_for_role`, matching `required_roles`, with
|
|
240
|
+
* `WS_CLOSE_PERMISSION_REVOKED`); the later `admit` then refuses. Until admitted it evicts nothing, is not
|
|
232
241
|
* counted toward the per-account cap, and receives no message.
|
|
233
242
|
*
|
|
234
243
|
* The caller owns the entry until it is admitted: a refusal closes it with
|
|
@@ -246,11 +255,27 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
|
|
|
246
255
|
* @param api_token_id - `api_token.id` for bearer connections, else `null`
|
|
247
256
|
* @param abort_controller - the socket-scoped controller to abort when the
|
|
248
257
|
* transport closes this connection (revocation or cap eviction)
|
|
258
|
+
* @param required_roles - the roles the endpoint gated the upgrade on, for
|
|
259
|
+
* `close_sockets_for_role`; empty for an ungated endpoint
|
|
249
260
|
* @returns the freshly assigned `connection_id` (branded `Uuid`)
|
|
250
261
|
* @mutates this - inserts an un-admitted entry into `#connections`, unless
|
|
251
262
|
* the transport is closing
|
|
252
263
|
*/
|
|
253
|
-
register_pending(ws: WSContext, token_hash: string | null, account_id: Uuid, api_token_id?: string | null, abort_controller?: AbortController | null): Uuid;
|
|
264
|
+
register_pending(ws: WSContext, token_hash: string | null, account_id: Uuid, api_token_id?: string | null, abort_controller?: AbortController | null, required_roles?: ReadonlyArray<string>): Uuid;
|
|
265
|
+
/**
|
|
266
|
+
* The close code the transport closed a still-pending registration with —
|
|
267
|
+
* `WS_CLOSE_SESSION_REVOKED` for a credential revocation,
|
|
268
|
+
* `WS_CLOSE_PERMISSION_REVOKED` for a role close, `WS_CLOSE_GOING_AWAY`
|
|
269
|
+
* for the shutdown — read once: the record is dropped by this call (and by
|
|
270
|
+
* `remove_connection`). `null` when the transport did not close it while
|
|
271
|
+
* pending. For a caller whose `admit` was refused, to say why; the twin of
|
|
272
|
+
* the reason the Rust `ConnectionRegistry::admit` refuses with.
|
|
273
|
+
*
|
|
274
|
+
* @param connection_id - the id `register_pending` returned
|
|
275
|
+
* @returns the close code, or `null`
|
|
276
|
+
* @mutates this - drops the recorded code
|
|
277
|
+
*/
|
|
278
|
+
take_pending_close_code(connection_id: Uuid): number | null;
|
|
254
279
|
/**
|
|
255
280
|
* Admit a pending connection — phase two (module doc).
|
|
256
281
|
*
|
|
@@ -294,12 +319,14 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
|
|
|
294
319
|
* connection is always admitted — unless the transport is closing: then it
|
|
295
320
|
* is born closed, as in `register_pending`, and evicts nothing.
|
|
296
321
|
*
|
|
322
|
+
* @param required_roles - the roles the endpoint gated the connection on,
|
|
323
|
+
* for `close_sockets_for_role`; empty for an ungated one
|
|
297
324
|
* @returns the freshly assigned `connection_id` (branded `Uuid`)
|
|
298
325
|
* @mutates this - inserts an admitted entry into `#connections`; past the
|
|
299
326
|
* cap, first removes the account's oldest admitted connections and closes
|
|
300
327
|
* their `WSContext`
|
|
301
328
|
*/
|
|
302
|
-
add_connection(ws: WSContext, token_hash: string | null, account_id: Uuid, api_token_id?: string | null, abort_controller?: AbortController | null): Uuid;
|
|
329
|
+
add_connection(ws: WSContext, token_hash: string | null, account_id: Uuid, api_token_id?: string | null, abort_controller?: AbortController | null, required_roles?: ReadonlyArray<string>): Uuid;
|
|
303
330
|
/**
|
|
304
331
|
* Remove a connection and its auth tracking data, pending or admitted, by
|
|
305
332
|
* the id `register_pending` / `add_connection` returned. Idempotent — safe
|
|
@@ -376,6 +403,25 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
|
|
|
376
403
|
* `WS_CLOSE_SESSION_REVOKED`
|
|
377
404
|
*/
|
|
378
405
|
close_sockets_for_token(api_token_id: string): number;
|
|
406
|
+
/**
|
|
407
|
+
* Close the sockets of `account_id` whose endpoint required `role`,
|
|
408
|
+
* including a registration still pending admission — the close for a
|
|
409
|
+
* revoked global grant of a role the upgrade gate admitted on. Each closes
|
|
410
|
+
* with `WS_CLOSE_PERMISSION_REVOKED`, not a revocation's code: the
|
|
411
|
+
* credential is still good.
|
|
412
|
+
*
|
|
413
|
+
* **Over-closes an any-of gate.** An endpoint gated on several roles admits
|
|
414
|
+
* an account holding any one of them, but this closes on the loss of any
|
|
415
|
+
* listed role, without checking whether the account still holds another.
|
|
416
|
+
* The client can reconnect when it still qualifies. The account's sockets
|
|
417
|
+
* on endpoints that did not list `role`, and on ungated ones, stay open.
|
|
418
|
+
*
|
|
419
|
+
* @returns the number of sockets closed
|
|
420
|
+
* @mutates this - removes matching connections, aborts their registered
|
|
421
|
+
* controllers, and closes their underlying `WSContext` with
|
|
422
|
+
* `WS_CLOSE_PERMISSION_REVOKED`
|
|
423
|
+
*/
|
|
424
|
+
close_sockets_for_role(account_id: Uuid, role: string): number;
|
|
379
425
|
/**
|
|
380
426
|
* Close every connection, pending or admitted, with `WS_CLOSE_GOING_AWAY` —
|
|
381
427
|
* the shutdown close `AppServer.close` runs. Not a revocation: the client
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transports_ws_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_backend.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiHG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACjD,OAAO,EAAe,KAAK,IAAI,EAAE,MAAM,yBAAyB,CAAC;AAEjE,OAAO,KAAK,EAEX,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,oBAAoB,EACpB,eAAe,EACf,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,
|
|
1
|
+
{"version":3,"file":"transports_ws_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_backend.ts"],"names":[],"mappings":"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"}
|
|
@@ -117,7 +117,7 @@ import { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
|
117
117
|
import { create_uuid } from '../primitive_schemas.js';
|
|
118
118
|
import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
|
|
119
119
|
import { create_jsonrpc_error_response, create_jsonrpc_request, to_jsonrpc_message_id, is_jsonrpc_request } from '../http/jsonrpc_helpers.js';
|
|
120
|
-
import { WS_CLOSE_CONNECTION_LIMIT, WS_CLOSE_GOING_AWAY, WS_CLOSE_GOING_AWAY_REASON, WS_CLOSE_OUTBOUND_OVERFLOW, WS_CLOSE_OUTBOUND_OVERFLOW_REASON, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON, utf8_length } from './transports.js';
|
|
120
|
+
import { WS_CLOSE_CONNECTION_LIMIT, WS_CLOSE_GOING_AWAY, WS_CLOSE_GOING_AWAY_REASON, WS_CLOSE_OUTBOUND_OVERFLOW, WS_CLOSE_OUTBOUND_OVERFLOW_REASON, WS_CLOSE_PERMISSION_REVOKED, WS_CLOSE_PERMISSION_REVOKED_REASON, WS_CLOSE_SESSION_REVOKED, WS_CLOSE_SESSION_REVOKED_REASON, utf8_length } from './transports.js';
|
|
121
121
|
import { PendingPeerRequests } from './peer_request.js';
|
|
122
122
|
import { WsOutboundWriter } from './ws_outbound_writer.js';
|
|
123
123
|
/** Type guard for `FilterableBroadcastTransport`. */
|
|
@@ -163,6 +163,10 @@ export class BackendWebsocketTransport {
|
|
|
163
163
|
// close arrives (`remove_connection`) or the close deadline passes — so the
|
|
164
164
|
// deadline timer is cleared when the socket goes.
|
|
165
165
|
#closing_writers = new Map();
|
|
166
|
+
// The close code each registration the transport closed while it was still
|
|
167
|
+
// pending got, until `take_pending_close_code` or `remove_connection` drops
|
|
168
|
+
// it — so a refused admission can say why it was refused.
|
|
169
|
+
#pending_close_codes = new Map();
|
|
166
170
|
// set once by `close_all_sockets`, never cleared — every later
|
|
167
171
|
// registration is born closed (module doc, "Shutdown")
|
|
168
172
|
#closing = false;
|
|
@@ -197,8 +201,9 @@ export class BackendWebsocketTransport {
|
|
|
197
201
|
*
|
|
198
202
|
* The entry is closeable at once: a `close_sockets_for_*` matching
|
|
199
203
|
* `token_hash` / `account_id` / `api_token_id` removes it, aborts
|
|
200
|
-
* `abort_controller`, and closes `ws` with `WS_CLOSE_SESSION_REVOKED
|
|
201
|
-
*
|
|
204
|
+
* `abort_controller`, and closes `ws` with `WS_CLOSE_SESSION_REVOKED`
|
|
205
|
+
* (`close_sockets_for_role`, matching `required_roles`, with
|
|
206
|
+
* `WS_CLOSE_PERMISSION_REVOKED`); the later `admit` then refuses. Until admitted it evicts nothing, is not
|
|
202
207
|
* counted toward the per-account cap, and receives no message.
|
|
203
208
|
*
|
|
204
209
|
* The caller owns the entry until it is admitted: a refusal closes it with
|
|
@@ -216,14 +221,36 @@ export class BackendWebsocketTransport {
|
|
|
216
221
|
* @param api_token_id - `api_token.id` for bearer connections, else `null`
|
|
217
222
|
* @param abort_controller - the socket-scoped controller to abort when the
|
|
218
223
|
* transport closes this connection (revocation or cap eviction)
|
|
224
|
+
* @param required_roles - the roles the endpoint gated the upgrade on, for
|
|
225
|
+
* `close_sockets_for_role`; empty for an ungated endpoint
|
|
219
226
|
* @returns the freshly assigned `connection_id` (branded `Uuid`)
|
|
220
227
|
* @mutates this - inserts an un-admitted entry into `#connections`, unless
|
|
221
228
|
* the transport is closing
|
|
222
229
|
*/
|
|
223
|
-
register_pending(ws, token_hash, account_id, api_token_id = null, abort_controller = null) {
|
|
230
|
+
register_pending(ws, token_hash, account_id, api_token_id = null, abort_controller = null, required_roles = []) {
|
|
224
231
|
if (this.#closing)
|
|
225
232
|
return this.#close_born_closed(ws, abort_controller);
|
|
226
|
-
return this.#insert(ws, token_hash, account_id, api_token_id, abort_controller, false);
|
|
233
|
+
return this.#insert(ws, { token_hash, account_id, api_token_id, required_roles }, abort_controller, false);
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The close code the transport closed a still-pending registration with —
|
|
237
|
+
* `WS_CLOSE_SESSION_REVOKED` for a credential revocation,
|
|
238
|
+
* `WS_CLOSE_PERMISSION_REVOKED` for a role close, `WS_CLOSE_GOING_AWAY`
|
|
239
|
+
* for the shutdown — read once: the record is dropped by this call (and by
|
|
240
|
+
* `remove_connection`). `null` when the transport did not close it while
|
|
241
|
+
* pending. For a caller whose `admit` was refused, to say why; the twin of
|
|
242
|
+
* the reason the Rust `ConnectionRegistry::admit` refuses with.
|
|
243
|
+
*
|
|
244
|
+
* @param connection_id - the id `register_pending` returned
|
|
245
|
+
* @returns the close code, or `null`
|
|
246
|
+
* @mutates this - drops the recorded code
|
|
247
|
+
*/
|
|
248
|
+
take_pending_close_code(connection_id) {
|
|
249
|
+
const code = this.#pending_close_codes.get(connection_id);
|
|
250
|
+
if (code === undefined)
|
|
251
|
+
return null;
|
|
252
|
+
this.#pending_close_codes.delete(connection_id);
|
|
253
|
+
return code;
|
|
227
254
|
}
|
|
228
255
|
/**
|
|
229
256
|
* Admit a pending connection — phase two (module doc).
|
|
@@ -281,24 +308,26 @@ export class BackendWebsocketTransport {
|
|
|
281
308
|
* connection is always admitted — unless the transport is closing: then it
|
|
282
309
|
* is born closed, as in `register_pending`, and evicts nothing.
|
|
283
310
|
*
|
|
311
|
+
* @param required_roles - the roles the endpoint gated the connection on,
|
|
312
|
+
* for `close_sockets_for_role`; empty for an ungated one
|
|
284
313
|
* @returns the freshly assigned `connection_id` (branded `Uuid`)
|
|
285
314
|
* @mutates this - inserts an admitted entry into `#connections`; past the
|
|
286
315
|
* cap, first removes the account's oldest admitted connections and closes
|
|
287
316
|
* their `WSContext`
|
|
288
317
|
*/
|
|
289
|
-
add_connection(ws, token_hash, account_id, api_token_id = null, abort_controller = null) {
|
|
318
|
+
add_connection(ws, token_hash, account_id, api_token_id = null, abort_controller = null, required_roles = []) {
|
|
290
319
|
if (this.#closing)
|
|
291
320
|
return this.#close_born_closed(ws, abort_controller);
|
|
292
321
|
if (this.#max_connections_per_account !== null) {
|
|
293
322
|
this.#evict_oldest_for_account(account_id, this.#max_connections_per_account);
|
|
294
323
|
}
|
|
295
|
-
return this.#insert(ws, token_hash, account_id, api_token_id, abort_controller, true);
|
|
324
|
+
return this.#insert(ws, { token_hash, account_id, api_token_id, required_roles }, abort_controller, true);
|
|
296
325
|
}
|
|
297
|
-
#insert(ws,
|
|
326
|
+
#insert(ws, identity, abort_controller, admitted) {
|
|
298
327
|
const connection_id = create_uuid();
|
|
299
328
|
this.#connections.set(connection_id, {
|
|
300
329
|
ws,
|
|
301
|
-
identity
|
|
330
|
+
identity,
|
|
302
331
|
admitted,
|
|
303
332
|
abort_controller,
|
|
304
333
|
writer: new WsOutboundWriter({
|
|
@@ -333,6 +362,7 @@ export class BackendWebsocketTransport {
|
|
|
333
362
|
* requests as `connection_gone`
|
|
334
363
|
*/
|
|
335
364
|
remove_connection(connection_id) {
|
|
365
|
+
this.#pending_close_codes.delete(connection_id);
|
|
336
366
|
this.#connections.get(connection_id)?.writer.dispose();
|
|
337
367
|
// a connection the transport closed: its socket is gone now
|
|
338
368
|
const closing = this.#closing_writers.get(connection_id);
|
|
@@ -451,6 +481,27 @@ export class BackendWebsocketTransport {
|
|
|
451
481
|
close_sockets_for_token(api_token_id) {
|
|
452
482
|
return this.#close_where((id) => id.api_token_id === api_token_id);
|
|
453
483
|
}
|
|
484
|
+
/**
|
|
485
|
+
* Close the sockets of `account_id` whose endpoint required `role`,
|
|
486
|
+
* including a registration still pending admission — the close for a
|
|
487
|
+
* revoked global grant of a role the upgrade gate admitted on. Each closes
|
|
488
|
+
* with `WS_CLOSE_PERMISSION_REVOKED`, not a revocation's code: the
|
|
489
|
+
* credential is still good.
|
|
490
|
+
*
|
|
491
|
+
* **Over-closes an any-of gate.** An endpoint gated on several roles admits
|
|
492
|
+
* an account holding any one of them, but this closes on the loss of any
|
|
493
|
+
* listed role, without checking whether the account still holds another.
|
|
494
|
+
* The client can reconnect when it still qualifies. The account's sockets
|
|
495
|
+
* on endpoints that did not list `role`, and on ungated ones, stay open.
|
|
496
|
+
*
|
|
497
|
+
* @returns the number of sockets closed
|
|
498
|
+
* @mutates this - removes matching connections, aborts their registered
|
|
499
|
+
* controllers, and closes their underlying `WSContext` with
|
|
500
|
+
* `WS_CLOSE_PERMISSION_REVOKED`
|
|
501
|
+
*/
|
|
502
|
+
close_sockets_for_role(account_id, role) {
|
|
503
|
+
return this.#close_where((id) => id.account_id === account_id && id.required_roles.includes(role), WS_CLOSE_PERMISSION_REVOKED, WS_CLOSE_PERMISSION_REVOKED_REASON);
|
|
504
|
+
}
|
|
454
505
|
/**
|
|
455
506
|
* Close every connection, pending or admitted, with `WS_CLOSE_GOING_AWAY` —
|
|
456
507
|
* the shutdown close `AppServer.close` runs. Not a revocation: the client
|
|
@@ -511,6 +562,8 @@ export class BackendWebsocketTransport {
|
|
|
511
562
|
}
|
|
512
563
|
#end_connection(connection_id, entry, code, reason, backlog = 'flush') {
|
|
513
564
|
this.#cleanup_connection(connection_id);
|
|
565
|
+
if (!entry.admitted)
|
|
566
|
+
this.#pending_close_codes.set(connection_id, code);
|
|
514
567
|
// abort before the close so a handler observing `ctx.signal` bails out
|
|
515
568
|
// without waiting for the close handshake
|
|
516
569
|
entry.abort_controller?.abort();
|
|
@@ -46,10 +46,12 @@ export interface WsEndpointSpec {
|
|
|
46
46
|
*/
|
|
47
47
|
actions: ReadonlyArray<Action>;
|
|
48
48
|
/**
|
|
49
|
-
* Roles permitted to upgrade — any-of disjunction
|
|
50
|
-
* to skip the upgrade-time role gate; per-action
|
|
51
|
-
* still applies at dispatch time via
|
|
52
|
-
* `[ROLE_ADMIN]` for an admin-only WS endpoint.
|
|
49
|
+
* Roles permitted to upgrade — any-of disjunction, global grants only.
|
|
50
|
+
* Omit (or pass `[]`) to skip the upgrade-time role gate; per-action
|
|
51
|
+
* `auth` on each spec still applies at dispatch time via
|
|
52
|
+
* `perform_action`. Pass `[ROLE_ADMIN]` for an admin-only WS endpoint.
|
|
53
|
+
* Held for the socket's life — see `register_ws_endpoint`'s
|
|
54
|
+
* `required_roles`.
|
|
53
55
|
*/
|
|
54
56
|
required_roles?: ReadonlyArray<RoleName>;
|
|
55
57
|
/**
|
|
@@ -133,18 +135,17 @@ export interface WsEndpointSpec {
|
|
|
133
135
|
* Set `false` when a consumer needs to compose their own callback
|
|
134
136
|
* from scratch — or to opt out of the auto-wiring entirely.
|
|
135
137
|
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* wanting role-revoke disconnection use `extra_audit_handlers`.
|
|
138
|
+
* The guard is also the one closer of a role revocation: a revoked
|
|
139
|
+
* global grant of a role in `required_roles` closes the account's
|
|
140
|
+
* sockets on this endpoint with `WS_CLOSE_PERMISSION_REVOKED`. With the
|
|
141
|
+
* guard off for a transport, nothing closes them.
|
|
141
142
|
*/
|
|
142
143
|
auth_guard?: boolean;
|
|
143
144
|
/**
|
|
144
145
|
* Extra audit-event handlers registered via `deps.audit.add_listener`
|
|
145
146
|
* AFTER the standard `auth_guard` wiring (when enabled). By the time
|
|
146
147
|
* these run, the standard guard may have already closed sockets. Use
|
|
147
|
-
* for
|
|
148
|
+
* for custom analytics, a consumer's own revocation events, etc.
|
|
148
149
|
*
|
|
149
150
|
* Never deduped — consumer-owned; pass the same handler twice and it
|
|
150
151
|
* fires twice.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws_endpoint_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/ws_endpoint_spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,KAAK,EACX,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAEvE;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC9B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B
|
|
1
|
+
{"version":3,"file":"ws_endpoint_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/ws_endpoint_spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,KAAK,EACX,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AAEvE;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC9B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;OAOG;IACH,eAAe,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACzC;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;;;OAMG;IACH,2BAA2B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5C;;;;;;;OAOG;IACH,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC7C,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAmCG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;OAQG;IACH,oBAAoB,CAAC,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;CACxD;AAED,yFAAyF;AACzF,MAAM,MAAM,sBAAsB,GAAG,cAAc,GAAG;IAAE,eAAe,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC;AAEjG;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,GAChC,cAAc,aAAa,CAAC,cAAc,CAAC,GAAG,SAAS,EACvD,iBAAiB,aAAa,CAAC,MAAM,CAAC,KACpC,KAAK,CAAC,sBAAsB,CAAC,GAAG,SAI/B,CAAC"}
|
package/dist/auth/CLAUDE.md
CHANGED
|
@@ -35,7 +35,7 @@ over the pool so rows persist when request transactions roll back. See root
|
|
|
35
35
|
- `auth/session_cookie.ts` — `SessionOptions<T>`, `parse_session`, `process_session_cookie` (`'none' | 'clear'` — no refresh action; the 30-day lifetime is an absolute cap signed once at mint), `create_session_config`, `fuz_session_config`, `SESSION_AGE_MAX`.
|
|
36
36
|
- `auth/password.ts` — `Password`, `PasswordProvided`, `PasswordHashDeps`, `PASSWORD_LENGTH_MIN` (12, OWASP), `PASSWORD_LENGTH_MAX` (300).
|
|
37
37
|
- `auth/password_argon2.ts` — `hash_password`, `verify_password`, `verify_dummy`, `argon2_password_deps`.
|
|
38
|
-
- `auth/api_token.ts` — `API_TOKEN_PREFIX` (`secret_fuz_token_`), `hash_api_token`, `generate_api_token
|
|
38
|
+
- `auth/api_token.ts` — `API_TOKEN_PREFIX` (`secret_fuz_token_`), `hash_api_token`, `generate_api_token`, and the shape precheck `is_api_token_shaped` (`API_TOKEN_REGEX`: the prefix plus exactly `API_TOKEN_BODY_LENGTH` = 43 base64url chars).
|
|
39
39
|
- `auth/daemon_token.ts` — `DaemonToken`, `DAEMON_TOKEN_HEADER` (`X-Daemon-Token`), `generate_daemon_token`, `validate_daemon_token`, `DaemonTokenState`.
|
|
40
40
|
- `auth/bootstrap_account.ts` — `bootstrap_account` (one-shot, `bootstrap_lock`-protected).
|
|
41
41
|
|
|
@@ -74,7 +74,7 @@ Convention — `*_schema.ts` is Zod-only; `*_ddl.ts` holds DDL strings.
|
|
|
74
74
|
|
|
75
75
|
### Queries
|
|
76
76
|
|
|
77
|
-
All take `deps: QueryDeps = {db}` first; `query_validate_api_token` adds `log
|
|
77
|
+
All take `deps: QueryDeps = {db}` first; `query_validate_api_token` adds `log` and returns a `ValidatedApiToken` — the row with `scope` parsed, refusing (returning `undefined` for) a row whose stored scope is unreadable.
|
|
78
78
|
|
|
79
79
|
- `auth/account_queries.ts` — account CRUD, actor resolution, password update with verify-write race guard, paged `query_admin_account_list`.
|
|
80
80
|
- `auth/actor_lookup_queries.ts` — batched `actor` ⨝ `account` for the labels arc.
|
|
@@ -219,11 +219,12 @@ Everything else listed under §RPC action surfaces.
|
|
|
219
219
|
|
|
220
220
|
### Middleware
|
|
221
221
|
|
|
222
|
-
- `auth/middleware.ts` — `create_auth_middleware_specs(deps, options)` assembles `[origin, session, request_context, bearer_auth]` + optional `daemon_token` (the content-type refusal isn't a middleware: `apply_route_specs` checks every `POST` route, see `http/simple_request.ts`). `AUTH_MIDDLEWARE_PATH` (`'/api/*'`) is where `create_app_server` mounts the stack;
|
|
222
|
+
- `auth/middleware.ts` — `create_auth_middleware_specs(deps, options)` assembles `[origin, session, request_context, bearer_auth]` + optional `daemon_token` (the content-type refusal isn't a middleware: `apply_route_specs` checks every `POST` route, see `http/simple_request.ts`). The origin check mounts on every path (`ORIGIN_MIDDLEWARE_PATH`, `'*'`), less its `origin_carve_outs` — each a `MiddlewareCarveOut` (`{path, reason}`, copied and frozen at assembly by `freeze_origin_carve_outs`, refused by `assert_origin_carve_outs_valid` without a reason or when it overlaps the auth scope — `middleware_paths_overlap` against `AUTH_MIDDLEWARE_PATH`, which holds every credential-reading and credential-minting route — and recorded on the origin spec so the surface leaves its `403` off the carved routes); `AUTH_MIDDLEWARE_PATH` (`'/api/*'`) is where `create_app_server` mounts the rest of the stack; assembly asserts lean on it: `assert_endpoint_in_auth_scope` (an `rpc_endpoints` / `ws_endpoints` path, or the bootstrap route under `bootstrap.route_prefix`, must sit inside it — so no carve-out can reach one), `assert_credential_routes_in_auth_scope(route_specs)` (every registered route that reads a credential — `auth.account` not `'none'`, or marked `reads_credential`, as account status is — or is marked `mints_credential` — login, signup, bootstrap — must sit inside it too, so a consumer prefixing the relative account or signup specs outside `/api/` is refused), and `assert_middleware_stack_mounted(stack_specs, middleware_specs)`, run on a `transform_middleware` result against the pre-transform stack (`[proxy, ...auth]`) — each stack spec, matched by its `handler` closure (not its spoofable `name`), mounted exactly once at its original path (`'*'` for the trusted proxy and the origin check, `AUTH_MIDDLEWARE_PATH` for the rest of auth), in its original order; other specs may sit anywhere, including before the proxy and between the auth layers. The proxy is in the stack because dropping it turns every `get_client_ip` into `'unknown'`, one shared bucket for every IP-keyed limiter. The transform receives shallow copies in a fresh array, so an in-place mutation can't rewrite the originals the check reads. The check pins where those layers sit, not what added layers do — a layer you add runs with full context access. Both run in `create_test_app_surface_spec` too.
|
|
223
223
|
- `auth/request_context.ts` — `RequestContext`, `resolve_acting_actor`, `build_request_context`, predicates (`has_role`, `has_scoped_role`, `has_any_scoped_role` — the dispatcher's role gate and every builtin-role check read the **global** grant via `has_scoped_role(_, role, null)` / `has_any_scoped_role(_, roles, null)`; scope-blind `has_role` is never the gate for a builtin role), guards (`require_auth`, `require_role`, `require_credential_types`, `require_token_scope`), `token_scope_surface_denial` (the direct-call form, for surfaces that aren't route specs), `refresh_role_grants`.
|
|
224
224
|
- `auth/session_middleware.ts` — `process_session_cookie` integration, `create_session_and_set_cookie` (shared by login / signup / bootstrap; returns the token hashes of the sessions the cap evicted, which the caller closes after its commit — only login passes a cap).
|
|
225
|
-
- `auth/resolved_auth.ts` — `ResolvedAuth` (the resolved credential as one value: `account_id`, `credential_type`, `token_hash`, `api_token_id`), `get_resolved_auth(c)` (gathers it from the context keys the auth middleware set), and `revalidate_resolved_auth(deps, resolved)`, the no-touch re-read a long-lived connection runs once it is registered: session row / token row (by id) still live and still on the account, account not soft-deleted; a daemon token re-reads its account only. Reuses the middleware's own queries and predicates. Called by the WS upgrade (`actions/register_action_ws.ts`) and the audit SSE route between registering a connection pending and admitting it. Twin of the Rust `revalidate_resolved_auth`.
|
|
226
|
-
- `auth/
|
|
225
|
+
- `auth/resolved_auth.ts` — `ResolvedAuth` (the resolved credential as one value: `account_id`, `credential_type`, `token_hash`, `api_token_id`, `credential_deadline`), `get_resolved_auth(c)` (gathers it from the context keys the auth middleware set), and `revalidate_resolved_auth(deps, resolved)`, the no-touch re-read a long-lived connection runs once it is registered: session row / token row (by id) still live and still on the account, account not soft-deleted; a daemon token re-reads its account only. Reuses the middleware's own queries and predicates. Called by the WS upgrade (`actions/register_action_ws.ts`) and the audit SSE route between registering a connection pending and admitting it. Twin of the Rust `revalidate_resolved_auth`.
|
|
226
|
+
- `auth/credential_deadline.ts` — when a resolved credential stops authenticating, which a WebSocket or audit stream opened on it closes at (expiry writes nothing a revocation listener hears): `credential_deadline_after` (the earliest remaining lifetime, anchored to `performance.now()`), `EXPIRES_IN_MS_SQL` (a row's `expires_at - NOW()`, read on the database clock in the statement that checks it, so the session and token validation queries return `expires_in_ms`), and `schedule_credential_deadline` (a chain of timers of at most `CREDENTIAL_DEADLINE_TIMER_CAP_MS`, an hour — a 30-day session is past `setTimeout`'s ceiling — each wake-up also checking a wall-clock copy of the deadline, so a host suspend, which stops the monotonic clock, delays the close by at most one timer after the resume). The middleware sets `CREDENTIAL_DEADLINE_KEY`: session — the earlier of the cookie's signed expiry (`SESSION_COOKIE_EXPIRES_AT_KEY`, set by the session middleware) and the row's; bearer — the row's, none for an eternal token; daemon token — none. Twin of the Rust `ResolvedAuth::credential_deadline`.
|
|
227
|
+
- `auth/bearer_auth.ts` — soft-fail bearer middleware; rejects when `Origin` or `Referer` present (browser context), and soft-fails a value that is not `is_api_token_shaped` before any query. The scope it sets is the one the query parsed — there is no fallback.
|
|
227
228
|
- `auth/daemon_token_middleware.ts` — `create_daemon_token_middleware(state, deps, log)` — the credential **consumer** only (soft-fail validation, keeper account resolution). The producer (`write_daemon_token` / `start_daemon_token_rotation`) lives in `testing/daemon_token_rotation.ts` behind the dev-env guard — no production assembly mints daemon tokens, mirroring the Rust spine's `fuz_testing`-confined producer. Soft-fails — discards the credential (pass-through, no own 401/503) on **every** non-success path: browser context (`Origin`/`Referer` present), malformed/invalid token, and valid-token-but-no-keeper all `next()` through to the dispatcher's `credential_type_required` (403) gate, mirroring the bearer guard and the Rust spine's `resolve.rs`. Daemon tokens are loopback-only, so browser context never arises in practice — the discard is defense-in-depth.
|
|
228
229
|
|
|
229
230
|
See root ../../../CLAUDE.md §Middleware Ordering for canonical assembly
|
|
@@ -913,10 +914,11 @@ session, and once the sweep deletes an expired row `account_session_revoke`
|
|
|
913
914
|
has nothing left to close for — so `cleanup_expired_sessions` closes each
|
|
914
915
|
swept session's connections itself, by session id
|
|
915
916
|
(`connection_closer.close_sockets_for_session`), which is why
|
|
916
|
-
`query_session_cleanup_expired` returns the deleted ids.
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
917
|
+
`query_session_cleanup_expired` returns the deleted ids. The close repeats
|
|
918
|
+
one already made: every connection closes at its credential's deadline
|
|
919
|
+
(`auth/credential_deadline.ts`) in the process holding it, sweep or no sweep.
|
|
920
|
+
An expired API token's row is not swept; its connections close at their
|
|
921
|
+
deadline (`account_token_revoke` still finds the row).
|
|
920
922
|
|
|
921
923
|
- The delete runs in its own transaction and the closes follow the commit —
|
|
922
924
|
the rule every revocation close keeps (`actions/CLAUDE.md` §Connection
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"account_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAQvD,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAC3C,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,4CAA4C;AAC5C,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;kBAI9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,4EAA4E;AAC5E,eAAO,MAAM,iCAAiC;;;iBAG5C,CAAC;AACH,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAElG,oFAAoF;AACpF,eAAO,MAAM,UAAU;;;kBAGrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD,6EAA6E;AAC7E,eAAO,MAAM,WAAW;;kBAEtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,2EAA2E;AAC3E,eAAO,MAAM,WAAW,WAAW,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,wFAAwF;AACxF,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD,sHAAsH;AACtH,eAAO,MAAM,mBAAmB;;;kBAG9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,uGAAuG;AACvG,eAAO,MAAM,oBAAoB;;;;kBAI/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,8CAA8C;AAC9C,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B
|
|
1
|
+
{"version":3,"file":"account_route_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/account_route_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAQvD,kFAAkF;AAClF,eAAO,MAAM,kBAAkB,WAAW,CAAC;AAC3C,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,4CAA4C;AAC5C,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;kBAI9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,4EAA4E;AAC5E,eAAO,MAAM,iCAAiC;;;iBAG5C,CAAC;AACH,MAAM,MAAM,iCAAiC,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iCAAiC,CAAC,CAAC;AAElG,oFAAoF;AACpF,eAAO,MAAM,UAAU;;;kBAGrB,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD,6EAA6E;AAC7E,eAAO,MAAM,WAAW;;kBAEtB,CAAC;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,2EAA2E;AAC3E,eAAO,MAAM,WAAW,WAAW,CAAC;AACpC,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD,wFAAwF;AACxF,eAAO,MAAM,YAAY;;;kBAGvB,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD,sHAAsH;AACtH,eAAO,MAAM,mBAAmB;;;kBAG9B,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,uGAAuG;AACvG,eAAO,MAAM,oBAAoB;;;;kBAI/B,CAAC;AACH,MAAM,MAAM,oBAAoB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAExE;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,IAAI,CAAC;AAEtC,8CAA8C;AAC9C,eAAO,MAAM,kBAAkB,KAAK,CAAC;AAErC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAaD,CAAC;AAEvC,8EAA8E;AAC9E,MAAM,WAAW,wBAAwB;IACxC,8FAA8F;IAC9F,0BAA0B,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,2BAA2B,GACvC,SAAS,wBAAwB,KAC/B,CACF,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,EAC1B,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAqD1B,CAAC"}
|
|
@@ -85,6 +85,9 @@ export const account_status_route_shape = {
|
|
|
85
85
|
method: 'GET',
|
|
86
86
|
path: '/status',
|
|
87
87
|
auth: { account: 'none', actor: 'none' },
|
|
88
|
+
// no gate — the handler answers an anonymous caller with its own 401 — but
|
|
89
|
+
// it reads the account the auth middleware resolved
|
|
90
|
+
reads_credential: true,
|
|
88
91
|
description: 'Current account info (unauthenticated: 401 with bootstrap status)',
|
|
89
92
|
input: AccountStatusInput,
|
|
90
93
|
output: AccountStatusOutput,
|
|
@@ -115,6 +118,7 @@ export const create_account_route_shapes = (options) => [
|
|
|
115
118
|
method: 'POST',
|
|
116
119
|
path: '/login',
|
|
117
120
|
auth: { account: 'none', actor: 'none' },
|
|
121
|
+
mints_credential: true,
|
|
118
122
|
description: 'Exchange credentials for session',
|
|
119
123
|
input: LoginInput,
|
|
120
124
|
output: LoginOutput,
|
package/dist/auth/api_token.d.ts
CHANGED
|
@@ -11,6 +11,26 @@
|
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
/** Prefix for all fuz API tokens (enables secret scanning). */
|
|
13
13
|
export declare const API_TOKEN_PREFIX = "secret_fuz_token_";
|
|
14
|
+
/** Random bytes encoded into the raw token body (256 bits). */
|
|
15
|
+
export declare const API_TOKEN_ENTROPY_BYTES = 32;
|
|
16
|
+
/** Length of the unpadded base64url body that encodes `API_TOKEN_ENTROPY_BYTES`. */
|
|
17
|
+
export declare const API_TOKEN_BODY_LENGTH: number;
|
|
18
|
+
/**
|
|
19
|
+
* Regex for a raw API token: `API_TOKEN_PREFIX` plus exactly
|
|
20
|
+
* `API_TOKEN_BODY_LENGTH` base64url characters. Matches the format produced
|
|
21
|
+
* by `generate_api_token`.
|
|
22
|
+
*/
|
|
23
|
+
export declare const API_TOKEN_REGEX: RegExp;
|
|
24
|
+
/**
|
|
25
|
+
* Whether `raw` has the shape of a raw API token — the bearer path's precheck,
|
|
26
|
+
* so a value that cannot be a token costs no database work. A `true` says
|
|
27
|
+
* nothing about whether the token exists. Twin of the Rust spine's
|
|
28
|
+
* `is_api_token_shaped`.
|
|
29
|
+
*
|
|
30
|
+
* @param raw - the value from the `Authorization: Bearer` header
|
|
31
|
+
* @returns `true` when `raw` matches `API_TOKEN_REGEX`
|
|
32
|
+
*/
|
|
33
|
+
export declare const is_api_token_shaped: (raw: string) => boolean;
|
|
14
34
|
/**
|
|
15
35
|
* Regex for the public API token id (e.g. `tok_abC0_d-3xyzA`). Twelve
|
|
16
36
|
* base64url characters after the `tok_` prefix. Matches the format produced
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api_token.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/api_token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,sBAAsB,CAAC;AAEpD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,QAA4B,CAAC;AAE5D,8CAA8C;AAC9C,eAAO,MAAM,UAAU,aAAuC,CAAC;AAC/D,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,KAAG,MAA4B,CAAC;AAE5E;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,QAAO;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAMpF,CAAC"}
|
|
1
|
+
{"version":3,"file":"api_token.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/api_token.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB,sBAAsB,CAAC;AAEpD,+DAA+D;AAC/D,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAE1C,oFAAoF;AACpF,eAAO,MAAM,qBAAqB,QAA+C,CAAC;AAElF;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAE3B,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,KAAK,MAAM,KAAG,OAAoC,CAAC;AAEvF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,QAA4B,CAAC;AAE5D,8CAA8C;AAC9C,eAAO,MAAM,UAAU,aAAuC,CAAC;AAC/D,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAEpD;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,MAAM,KAAG,MAA4B,CAAC;AAE5E;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,QAAO;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAMpF,CAAC"}
|
package/dist/auth/api_token.js
CHANGED
|
@@ -13,6 +13,26 @@ import { hash_blake3 } from '@fuzdev/fuz_util/hash_blake3.ts';
|
|
|
13
13
|
import { generate_random_base64url } from '../crypto.js';
|
|
14
14
|
/** Prefix for all fuz API tokens (enables secret scanning). */
|
|
15
15
|
export const API_TOKEN_PREFIX = 'secret_fuz_token_';
|
|
16
|
+
/** Random bytes encoded into the raw token body (256 bits). */
|
|
17
|
+
export const API_TOKEN_ENTROPY_BYTES = 32;
|
|
18
|
+
/** Length of the unpadded base64url body that encodes `API_TOKEN_ENTROPY_BYTES`. */
|
|
19
|
+
export const API_TOKEN_BODY_LENGTH = Math.ceil((API_TOKEN_ENTROPY_BYTES * 4) / 3);
|
|
20
|
+
/**
|
|
21
|
+
* Regex for a raw API token: `API_TOKEN_PREFIX` plus exactly
|
|
22
|
+
* `API_TOKEN_BODY_LENGTH` base64url characters. Matches the format produced
|
|
23
|
+
* by `generate_api_token`.
|
|
24
|
+
*/
|
|
25
|
+
export const API_TOKEN_REGEX = new RegExp(`^${API_TOKEN_PREFIX}[A-Za-z0-9_-]{${API_TOKEN_BODY_LENGTH}}$`);
|
|
26
|
+
/**
|
|
27
|
+
* Whether `raw` has the shape of a raw API token — the bearer path's precheck,
|
|
28
|
+
* so a value that cannot be a token costs no database work. A `true` says
|
|
29
|
+
* nothing about whether the token exists. Twin of the Rust spine's
|
|
30
|
+
* `is_api_token_shaped`.
|
|
31
|
+
*
|
|
32
|
+
* @param raw - the value from the `Authorization: Bearer` header
|
|
33
|
+
* @returns `true` when `raw` matches `API_TOKEN_REGEX`
|
|
34
|
+
*/
|
|
35
|
+
export const is_api_token_shaped = (raw) => API_TOKEN_REGEX.test(raw);
|
|
16
36
|
/**
|
|
17
37
|
* Regex for the public API token id (e.g. `tok_abC0_d-3xyzA`). Twelve
|
|
18
38
|
* base64url characters after the `tok_` prefix. Matches the format produced
|
|
@@ -37,7 +57,7 @@ export const hash_api_token = (token) => hash_blake3(token);
|
|
|
37
57
|
* @returns the raw token, a public id, and the blake3 hash for storage
|
|
38
58
|
*/
|
|
39
59
|
export const generate_api_token = () => {
|
|
40
|
-
const raw = generate_random_base64url();
|
|
60
|
+
const raw = generate_random_base64url(API_TOKEN_ENTROPY_BYTES);
|
|
41
61
|
const token = `${API_TOKEN_PREFIX}${raw}`;
|
|
42
62
|
const token_hash = hash_api_token(token);
|
|
43
63
|
const id = `tok_${raw.slice(0, 12)}`;
|
|
@@ -7,6 +7,22 @@ import type { Logger } from '@fuzdev/fuz_util/log.ts';
|
|
|
7
7
|
import type { QueryDeps } from '../db/query_deps.js';
|
|
8
8
|
import type { ApiToken } from './account_schema.js';
|
|
9
9
|
import { type TokenScope } from './token_scope.js';
|
|
10
|
+
/**
|
|
11
|
+
* A live `api_token` row as `query_validate_api_token` returns it: the stored
|
|
12
|
+
* `scope` document already parsed, so a caller holds the narrowing the
|
|
13
|
+
* credential was minted with and never decides what an unreadable one means.
|
|
14
|
+
* The Rust twin's `ApiTokenRow` carries the parsed scope the same way.
|
|
15
|
+
*/
|
|
16
|
+
export type ValidatedApiToken = Omit<ApiToken, 'scope'> & {
|
|
17
|
+
scope: TokenScope;
|
|
18
|
+
/**
|
|
19
|
+
* Milliseconds until the token expires — `expires_at - NOW()`, measured on
|
|
20
|
+
* the database clock in the statement that found it live
|
|
21
|
+
* (`EXPIRES_IN_MS_SQL`); `null` for an eternal token. The Rust twin's
|
|
22
|
+
* `ApiTokenRow.expires_in`.
|
|
23
|
+
*/
|
|
24
|
+
expires_in_ms: number | null;
|
|
25
|
+
};
|
|
10
26
|
/** Extended deps for `query_validate_api_token` which needs a logger. */
|
|
11
27
|
export interface ApiTokenQueryDeps extends QueryDeps {
|
|
12
28
|
log: Logger;
|
|
@@ -32,21 +48,23 @@ export declare const API_TOKEN_COLUMNS: readonly ["id", "account_id", "name", "t
|
|
|
32
48
|
*/
|
|
33
49
|
export declare const query_create_api_token: (deps: QueryDeps, id: string, account_id: string, name: string, token_hash: string, scope: TokenScope, expires_at?: Date | null) => Promise<ApiToken>;
|
|
34
50
|
/**
|
|
35
|
-
* Validate a raw API token and return the token record.
|
|
51
|
+
* Validate a raw API token and return the token record with its scope parsed.
|
|
36
52
|
*
|
|
37
53
|
* Hashes the token with blake3, looks up the hash, and checks
|
|
38
|
-
* expiration
|
|
54
|
+
* expiration, reading how long the token has left on the database clock in
|
|
55
|
+
* the same statement. A row whose stored scope is unreadable is refused like an
|
|
56
|
+
* unknown hash. Updates `last_used_at` and `last_used_ip` on success
|
|
39
57
|
* (fire-and-forget — errors logged, never thrown).
|
|
40
58
|
*
|
|
41
59
|
* @param deps - query dependencies with logger
|
|
42
60
|
* @param raw_token - the raw API token from the Authorization header
|
|
43
61
|
* @param ip - the client IP address (for audit)
|
|
44
62
|
* @param pending_effects - optional array to register the usage-tracking effect for later awaiting
|
|
45
|
-
* @returns the token record if valid, or `undefined`
|
|
63
|
+
* @returns the token record with its parsed scope if valid, or `undefined`
|
|
46
64
|
* @mutates `api_token` row - fire-and-forget UPDATE of `last_used_at` / `last_used_ip` on a valid token
|
|
47
65
|
* @mutates `pending_effects` - pushes the in-flight tracking promise when provided
|
|
48
66
|
*/
|
|
49
|
-
export declare const query_validate_api_token: (deps: ApiTokenQueryDeps, raw_token: string, ip: string | undefined, pending_effects: Array<Promise<void>> | undefined) => Promise<
|
|
67
|
+
export declare const query_validate_api_token: (deps: ApiTokenQueryDeps, raw_token: string, ip: string | undefined, pending_effects: Array<Promise<void>> | undefined) => Promise<ValidatedApiToken | undefined>;
|
|
50
68
|
/**
|
|
51
69
|
* Look up the owning account of a live API token — one that still exists and
|
|
52
70
|
* has not expired — by its public id.
|