@fuzdev/fuz_app 0.63.0 → 0.64.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 +124 -11
- package/dist/actions/connection_closer.d.ts +68 -0
- package/dist/actions/connection_closer.d.ts.map +1 -0
- package/dist/actions/connection_closer.js +41 -0
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +23 -2
- package/dist/actions/register_ws_endpoint.d.ts +11 -9
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +5 -5
- package/dist/actions/transports_ws_auth_guard.d.ts +24 -8
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +23 -7
- package/dist/actions/ws_endpoint_spec.d.ts +119 -0
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -0
- package/dist/actions/ws_endpoint_spec.js +13 -0
- package/dist/auth/CLAUDE.md +79 -15
- package/dist/auth/account_action_specs.d.ts +1 -1
- package/dist/auth/account_actions.d.ts +13 -0
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +31 -1
- package/dist/auth/account_routes.d.ts +12 -2
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +55 -8
- package/dist/auth/account_schema.d.ts +3 -3
- package/dist/auth/admin_action_specs.d.ts +8 -8
- package/dist/auth/admin_actions.d.ts +11 -0
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +25 -0
- package/dist/auth/audit_emitter.d.ts +56 -12
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +38 -12
- package/dist/auth/audit_log_schema.d.ts +5 -3
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +5 -3
- package/dist/auth/bootstrap_routes.d.ts +1 -1
- package/dist/auth/invite_schema.d.ts +2 -2
- package/dist/auth/signup_routes.d.ts +1 -1
- package/dist/auth/standard_rpc_actions.d.ts +1 -0
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +1 -0
- package/dist/http/CLAUDE.md +26 -10
- package/dist/http/ip_canonical.d.ts +99 -0
- package/dist/http/ip_canonical.d.ts.map +1 -0
- package/dist/http/ip_canonical.js +191 -0
- package/dist/http/origin.d.ts +13 -5
- package/dist/http/origin.d.ts.map +1 -1
- package/dist/http/origin.js +13 -31
- package/dist/http/pending_effects.d.ts +1 -1
- package/dist/http/pending_effects.js +1 -1
- package/dist/http/proxy.d.ts +13 -5
- package/dist/http/proxy.d.ts.map +1 -1
- package/dist/http/proxy.js +15 -23
- package/dist/http/surface.d.ts +50 -0
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/http/surface.js +27 -1
- package/dist/primitive_schemas.d.ts +20 -4
- package/dist/primitive_schemas.d.ts.map +1 -1
- package/dist/primitive_schemas.js +25 -4
- package/dist/realtime/sse_auth_guard.d.ts +16 -4
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +15 -3
- package/dist/server/app_backend.d.ts +66 -19
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +57 -34
- package/dist/server/app_server.d.ts +60 -0
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +95 -2
- package/dist/server/startup.d.ts.map +1 -1
- package/dist/server/startup.js +12 -0
- package/dist/testing/CLAUDE.md +64 -28
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +4 -5
- package/dist/testing/adversarial_headers.d.ts +6 -0
- package/dist/testing/adversarial_headers.d.ts.map +1 -1
- package/dist/testing/adversarial_headers.js +13 -5
- package/dist/testing/app_server.d.ts +33 -32
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +4 -13
- package/dist/testing/attack_surface.d.ts +8 -7
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +12 -8
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +3 -5
- package/dist/testing/audit_drift_guard.d.ts +116 -0
- package/dist/testing/audit_drift_guard.d.ts.map +1 -0
- package/dist/testing/audit_drift_guard.js +134 -0
- package/dist/testing/connection_closer_helpers.d.ts +44 -0
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -0
- package/dist/testing/connection_closer_helpers.js +48 -0
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +7 -9
- package/dist/testing/rate_limiting.js +4 -4
- package/dist/testing/rpc_helpers.d.ts +2 -1
- package/dist/testing/rpc_helpers.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +6 -8
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +12 -6
- package/dist/testing/stubs.d.ts +11 -0
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +4 -0
- package/dist/testing/surface_invariants.d.ts +66 -1
- package/dist/testing/surface_invariants.d.ts.map +1 -1
- package/dist/testing/surface_invariants.js +103 -1
- package/dist/ui/SurfaceExplorer.svelte +161 -2
- package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/actions/CLAUDE.md
CHANGED
|
@@ -469,9 +469,18 @@ persistence + rehydration by the consumer.
|
|
|
469
469
|
|
|
470
470
|
## WS auth guard (`transports_ws_auth_guard.ts`)
|
|
471
471
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
472
|
+
Closes WS sockets on audit revoke events — per-message dispatch doesn't
|
|
473
|
+
re-check session/token validity, so this guard is the revocation seam
|
|
474
|
+
for open connections. Module TSDoc carries the full rationale.
|
|
475
|
+
|
|
476
|
+
`create_ws_auth_guard(transport, log)` returns an `on_audit_event` callback.
|
|
477
|
+
For standard WS endpoints mounted via `AppServerOptions.ws_endpoints`,
|
|
478
|
+
`create_app_server` composes this guard onto
|
|
479
|
+
`backend.deps.audit.on_event_chain` automatically (per
|
|
480
|
+
`WsEndpointSpec.auth_guard`). For custom wiring, append it inside the
|
|
481
|
+
consumer's `audit_factory` body (or via `audit.on_event_chain.push(...)`
|
|
482
|
+
post-assembly). Mirrors the SSE guard in `realtime/sse_auth_guard.ts`
|
|
483
|
+
but targets the WS transport.
|
|
475
484
|
|
|
476
485
|
`ws_disconnect_event_types` (ReadonlySet): `session_revoke`,
|
|
477
486
|
`token_revoke`, `session_revoke_all`, `token_revoke_all`, `password_change`.
|
|
@@ -510,27 +519,124 @@ Same `outcome === 'failure'` guard as `create_ws_auth_guard`. Closes via
|
|
|
510
519
|
`close_sockets_for_account(event.account_id)` — `logout` is always
|
|
511
520
|
self-service, so there is no `target_account_id` to fall back on.
|
|
512
521
|
|
|
513
|
-
##
|
|
522
|
+
## Connection closer (`connection_closer.ts`)
|
|
523
|
+
|
|
524
|
+
Narrow structural capability for handler-side eager WS socket closure
|
|
525
|
+
on revocation — the belt+suspenders layer that complements the audit-
|
|
526
|
+
listener guards above.
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
interface ConnectionCloser {
|
|
530
|
+
close_sockets_for_session: (session_token_hash: string) => number;
|
|
531
|
+
close_sockets_for_token: (api_token_id: string) => number;
|
|
532
|
+
close_sockets_for_account: (account_id: string) => number;
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
`BackendWebsocketTransport` satisfies this structurally — consumers
|
|
537
|
+
pass the transport instance directly (same shape as
|
|
538
|
+
`NotificationSender`). Wired into `AccountRouteOptions.connection_closer`
|
|
539
|
+
(logout / password), `AccountActionOptions.connection_closer`
|
|
540
|
+
(`account_session_revoke` / `_revoke_all` / `account_token_revoke`),
|
|
541
|
+
and `AdminActionOptions.connection_closer`
|
|
542
|
+
(`admin_session_revoke_all` / `admin_token_revoke_all`). Each handler
|
|
543
|
+
calls the appropriate `close_sockets_for_*` method synchronously
|
|
544
|
+
BEFORE the audit emit so revocation lands even on audit INSERT
|
|
545
|
+
failure. Listener-based close
|
|
546
|
+
(`create_ws_auth_guard` / `create_ws_logout_closer`) stays as a
|
|
547
|
+
fail-safe for out-of-band emit sites; `close_sockets_for_*` is
|
|
548
|
+
idempotent. Failure outcomes (`revoked: false`, 404 not-found) skip
|
|
549
|
+
the eager close — matches the listener's `outcome === 'failure'`
|
|
550
|
+
guard so attacker-guessable ids cannot be used to target arbitrary
|
|
551
|
+
sockets. Mirrors `zzz_server`'s handler-side `close_sockets_for_*`
|
|
552
|
+
calls (landed 2026-05-16; see
|
|
553
|
+
`~/dev/grimoire/lore/fuz_app/TODO_AUTH.md` § Audit-driven WS
|
|
554
|
+
revocation: handler-side belt+suspenders).
|
|
514
555
|
|
|
515
|
-
|
|
556
|
+
## WebSocket dispatch
|
|
516
557
|
|
|
517
|
-
|
|
558
|
+
Three layered entry points, in decreasing abstraction:
|
|
559
|
+
|
|
560
|
+
### `create_app_server.ws_endpoints` (`server/app_server.ts`) — canonical
|
|
561
|
+
|
|
562
|
+
The top-level mount surface — mirror of `rpc_endpoints` for WebSocket
|
|
563
|
+
endpoints. Accepts either an array of `WsEndpointSpec` or a factory
|
|
564
|
+
`(ctx: AppServerContext) => ReadonlyArray<WsEndpointSpec>`; the factory
|
|
565
|
+
form runs after the server context is assembled so action lists can
|
|
566
|
+
depend on `ctx.deps` / `ctx.action_*_rate_limiter`. Each entry is
|
|
567
|
+
auto-mounted via `register_ws_endpoint` against the assembled Hono app,
|
|
568
|
+
so consumers no longer call `register_ws_endpoint` themselves in their
|
|
569
|
+
server-assembly module.
|
|
570
|
+
|
|
571
|
+
`upgradeWebSocket` (the Hono adapter helper) is supplied once at the
|
|
572
|
+
top level — `create_app_server` throws when `ws_endpoints` resolves
|
|
573
|
+
non-empty but `upgradeWebSocket` is missing. A factory returning `[]`
|
|
574
|
+
does NOT trip the check, so feature-flag gated WS surfaces stay safe.
|
|
575
|
+
|
|
576
|
+
Per-endpoint `WsEndpointSpec` fields:
|
|
577
|
+
|
|
578
|
+
- `path` — Hono mount path
|
|
579
|
+
- `allowed_origins` — origin allowlist regexes (parsed via `parse_allowed_origins`)
|
|
580
|
+
- `actions` — the `ReadonlyArray<Action>` to dispatch (spread `protocol_actions` first)
|
|
581
|
+
- `required_roles?: ReadonlyArray<RoleName>` — any-of upgrade-time role gate; omit or `[]` to skip
|
|
582
|
+
- `transport?: BackendWebsocketTransport` — supplied or auto-created; returned on `AppServer.ws_endpoints[path]` either way
|
|
583
|
+
- `heartbeat?`, `artificial_delay?`, `on_socket_open?`, `on_socket_close?` — passed through to `register_ws_endpoint`
|
|
584
|
+
- `auth_guard?: boolean` — default `true`; auto-composes `create_ws_auth_guard` + `create_ws_logout_closer` against the endpoint's transport and appends them to `deps.audit.on_event_chain`. The wiring is deduped by **reference identity** (`WeakSet<BackendWebsocketTransport>`), so two `WsEndpointSpec`s sharing one `BackendWebsocketTransport` instance still get a single pair of listeners. Wrapped / DI-proxied transports dedupe as separate entries — set `auth_guard: false` on duplicates and compose `create_ws_auth_guard` / `create_ws_logout_closer` against the underlying transport once
|
|
585
|
+
- `extra_audit_handlers?: ReadonlyArray<AuditEventHandler>` — appended after the standard guards; consumer-owned, never deduped
|
|
586
|
+
|
|
587
|
+
`auth_guard: true` does NOT close sockets on `role_grant_revoke`
|
|
588
|
+
(deliberate — per-connection role tracking is out of scope). Consumers
|
|
589
|
+
that need role-revoke disconnection wire it via `extra_audit_handlers`.
|
|
590
|
+
|
|
591
|
+
The mounted transport is reachable at `app_server.ws_endpoints[path]` —
|
|
592
|
+
a `Readonly<Record<string, BackendWebsocketTransport>>` keyed by mount
|
|
593
|
+
path. Use this handle for fan-out (`send_to_account`) and broadcast.
|
|
594
|
+
|
|
595
|
+
Duplicate paths across `WsEndpointSpec`s throw at mount time
|
|
596
|
+
(`'create_app_server: duplicate ws_endpoints path: <path>'`), closing
|
|
597
|
+
the route-shadow hole Hono's silent `app.get` overwriting would leave.
|
|
598
|
+
Cross-surface collisions are also detected — registering `GET <path>`
|
|
599
|
+
as both a `RouteSpec` and a `WsEndpointSpec` throws
|
|
600
|
+
`'create_app_server: ws_endpoints path collides with a GET RouteSpec: <path>'`
|
|
601
|
+
at mount time. Exact-string match only; pattern overlap (e.g. a
|
|
602
|
+
`RouteSpec` at `GET /api/:resource` vs `WsEndpointSpec` at `/api/ws`)
|
|
603
|
+
is not detected — Hono's specific-before-wildcard routing keeps those
|
|
604
|
+
working, but if you need certainty avoid the overlap.
|
|
605
|
+
|
|
606
|
+
`auth_guard` semantics when multiple specs share a transport: **any**
|
|
607
|
+
spec with `auth_guard !== false` wires the guard for that transport
|
|
608
|
+
(OR-semantics, order-independent). To opt out for a shared transport,
|
|
609
|
+
every sibling spec must pass `auth_guard: false`. Documented on the
|
|
610
|
+
`WsEndpointSpec.auth_guard` TSDoc.
|
|
611
|
+
|
|
612
|
+
`AppSurfaceWsEndpoint.methods` surfaces `request_response` + `remote_notification`
|
|
613
|
+
specs only — `local_call` specs are filtered out because they don't
|
|
614
|
+
dispatch over WS (`compile_action_registry` routes only
|
|
615
|
+
`request_response` with a handler into `action_map`; `local_call` is
|
|
616
|
+
frontend-side registry metadata).
|
|
617
|
+
|
|
618
|
+
### `register_ws_endpoint` (`register_ws_endpoint.ts`) — middle tier
|
|
518
619
|
|
|
519
620
|
Composes the standard upgrade stack:
|
|
520
621
|
|
|
521
622
|
1. `verify_request_source(allowed_origins)`
|
|
522
623
|
2. `require_auth`
|
|
523
624
|
3. upgrade-time authorization phase — resolves the acting actor and seeds `REQUEST_CONTEXT_KEY` for the inner `register_action_ws`'s upgrade-time identity capture
|
|
524
|
-
4. optional `require_role(
|
|
625
|
+
4. optional `require_role(required_roles)` — any-of disjunction
|
|
525
626
|
5. delegates to `register_action_ws`
|
|
526
627
|
|
|
527
|
-
Extends `RegisterActionWsOptions` with `allowed_origins:
|
|
528
|
-
and optional `
|
|
529
|
-
`
|
|
530
|
-
each spec still applies at dispatch time via
|
|
628
|
+
Extends `RegisterActionWsOptions` with `allowed_origins: ReadonlyArray<RegExp>`
|
|
629
|
+
and optional `required_roles: ReadonlyArray<RoleName>`. Returns
|
|
630
|
+
`{transport}`. Note: `required_roles` is a **coarse upgrade-time gate**
|
|
631
|
+
— per-action `auth` in each spec still applies at dispatch time via
|
|
632
|
+
`perform_action`. Pass `[]` (or omit) to skip the role gate.
|
|
531
633
|
(`verify_request_source` and `require_auth` / `require_role` are from
|
|
532
634
|
`auth/`; see `auth/CLAUDE.md` §Middleware for their semantics.)
|
|
533
635
|
|
|
636
|
+
Most consumers reach for the higher-level `ws_endpoints` option above —
|
|
637
|
+
this is the entry test harnesses use when they need the upgrade stack
|
|
638
|
+
without `create_app_server`'s full assembly.
|
|
639
|
+
|
|
534
640
|
### `register_action_ws` (`register_action_ws.ts`) — lower-level
|
|
535
641
|
|
|
536
642
|
Exposed for tests (`create_ws_test_harness`) that need to drive the
|
|
@@ -945,6 +1051,13 @@ HTTP transport is **not** registered. `local_call` specs in `specs`
|
|
|
945
1051
|
silently no-op because `lookup_action_handler` always returns
|
|
946
1052
|
`undefined`; this factory targets wire-dispatched actions.
|
|
947
1053
|
|
|
1054
|
+
`all_standard_action_specs` is transport-agnostic — when a consumer
|
|
1055
|
+
spreads `create_standard_rpc_actions(ctx.deps, opts)` into both
|
|
1056
|
+
`rpc_endpoints` AND `ws_endpoints` on `create_app_server`, every method
|
|
1057
|
+
is reachable on both transports and `transport_for_method` can route
|
|
1058
|
+
per-call (e.g. return `'frontend_websocket_rpc'` for `account_*` /
|
|
1059
|
+
`admin_*` methods to bind them to the live WS connection).
|
|
1060
|
+
|
|
948
1061
|
`transport_for_method` and `on_action_event` are pure pass-throughs to
|
|
949
1062
|
`create_rpc_client` — exposed so consumers needing per-method routing
|
|
950
1063
|
(zap-style WS-for-actions / HTTP-for-rest split) or per-dispatch event
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow structural capability for closing live WebSocket connections
|
|
3
|
+
* tied to a session token hash, API token id, or account id.
|
|
4
|
+
*
|
|
5
|
+
* **Why this exists.** Per-message authorization phase on WebSocket
|
|
6
|
+
* (`actions/perform_action.ts`) reloads role_grants from the DB on every
|
|
7
|
+
* message but does NOT re-query session / token validity — that
|
|
8
|
+
* trade-off keeps chatty connections fast. The cost: revocation
|
|
9
|
+
* doesn't actually disconnect open sockets unless something closes
|
|
10
|
+
* them. `transports_ws_auth_guard.ts` is the listener-based seam
|
|
11
|
+
* (audit-event → close), but it only fires after the audit INSERT
|
|
12
|
+
* succeeds — if the INSERT fails (DB error, pool exhausted, handler
|
|
13
|
+
* dies mid-flight) the listener never runs and the live socket keeps
|
|
14
|
+
* working with a stale `RequestContext` until disconnect.
|
|
15
|
+
*
|
|
16
|
+
* Used by self-service revocation handlers (`account_session_revoke` /
|
|
17
|
+
* `_revoke_all`, `account_token_revoke`, `logout`, `password`) and the
|
|
18
|
+
* admin revoke-all handlers (`admin_session_revoke_all`,
|
|
19
|
+
* `admin_token_revoke_all`) to eagerly drop affected sockets BEFORE
|
|
20
|
+
* emitting the corresponding audit event. The audit listener stays as
|
|
21
|
+
* a fail-safe for out-of-band emit sites (admin tools, scheduled
|
|
22
|
+
* jobs, SSE-driven flows). `close_sockets_for_*` is idempotent so the
|
|
23
|
+
* second pass is a no-op.
|
|
24
|
+
*
|
|
25
|
+
* Mirrors `zzz_server`'s `close_sockets_for_*` calls in
|
|
26
|
+
* `account.rs::logout_inner` / `_password_inner` /
|
|
27
|
+
* `handlers/account.rs::handle_account_session_revoke[_all]` /
|
|
28
|
+
* `_token_revoke` (landed 2026-05-16); see
|
|
29
|
+
* `~/dev/grimoire/lore/fuz_app/TODO_AUTH.md` §Audit-driven WS
|
|
30
|
+
* revocation: handler-side belt+suspenders for the cross-backend
|
|
31
|
+
* parity record.
|
|
32
|
+
*
|
|
33
|
+
* `BackendWebsocketTransport` satisfies this interface structurally,
|
|
34
|
+
* so consumers pass their transport instance directly (same shape as
|
|
35
|
+
* `NotificationSender`). The interface stays local so handlers don't
|
|
36
|
+
* couple to the concrete transport, and tests can inject a capturing
|
|
37
|
+
* stub with no WS machinery.
|
|
38
|
+
*
|
|
39
|
+
* @module
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* Narrow capability — three idempotent socket-close methods, each
|
|
43
|
+
* returning the number of sockets actually closed (zero when none
|
|
44
|
+
* matched). Callers typically ignore the return value (used by
|
|
45
|
+
* telemetry / tests).
|
|
46
|
+
*/
|
|
47
|
+
export interface ConnectionCloser {
|
|
48
|
+
/**
|
|
49
|
+
* Close every connection authenticated with a session whose blake3
|
|
50
|
+
* hash matches `session_token_hash`. Idempotent — calling on an
|
|
51
|
+
* already-closed session is a no-op.
|
|
52
|
+
*/
|
|
53
|
+
close_sockets_for_session: (session_token_hash: string) => number;
|
|
54
|
+
/**
|
|
55
|
+
* Close every connection authenticated with the given API token id.
|
|
56
|
+
* Idempotent — calling on an already-revoked token is a no-op.
|
|
57
|
+
*/
|
|
58
|
+
close_sockets_for_token: (api_token_id: string) => number;
|
|
59
|
+
/**
|
|
60
|
+
* Close every connection bound to `account_id`, regardless of
|
|
61
|
+
* credential type (session / api_token / daemon_token). Coarse
|
|
62
|
+
* closure used when every credential on an account is invalidated
|
|
63
|
+
* — password change, session-revoke-all, token-revoke-all, logout.
|
|
64
|
+
* Idempotent.
|
|
65
|
+
*/
|
|
66
|
+
close_sockets_for_account: (account_id: string) => number;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=connection_closer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"connection_closer.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/connection_closer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAChC;;;;OAIG;IACH,yBAAyB,EAAE,CAAC,kBAAkB,EAAE,MAAM,KAAK,MAAM,CAAC;IAClE;;;OAGG;IACH,uBAAuB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1D;;;;;;OAMG;IACH,yBAAyB,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;CAC1D"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Narrow structural capability for closing live WebSocket connections
|
|
3
|
+
* tied to a session token hash, API token id, or account id.
|
|
4
|
+
*
|
|
5
|
+
* **Why this exists.** Per-message authorization phase on WebSocket
|
|
6
|
+
* (`actions/perform_action.ts`) reloads role_grants from the DB on every
|
|
7
|
+
* message but does NOT re-query session / token validity — that
|
|
8
|
+
* trade-off keeps chatty connections fast. The cost: revocation
|
|
9
|
+
* doesn't actually disconnect open sockets unless something closes
|
|
10
|
+
* them. `transports_ws_auth_guard.ts` is the listener-based seam
|
|
11
|
+
* (audit-event → close), but it only fires after the audit INSERT
|
|
12
|
+
* succeeds — if the INSERT fails (DB error, pool exhausted, handler
|
|
13
|
+
* dies mid-flight) the listener never runs and the live socket keeps
|
|
14
|
+
* working with a stale `RequestContext` until disconnect.
|
|
15
|
+
*
|
|
16
|
+
* Used by self-service revocation handlers (`account_session_revoke` /
|
|
17
|
+
* `_revoke_all`, `account_token_revoke`, `logout`, `password`) and the
|
|
18
|
+
* admin revoke-all handlers (`admin_session_revoke_all`,
|
|
19
|
+
* `admin_token_revoke_all`) to eagerly drop affected sockets BEFORE
|
|
20
|
+
* emitting the corresponding audit event. The audit listener stays as
|
|
21
|
+
* a fail-safe for out-of-band emit sites (admin tools, scheduled
|
|
22
|
+
* jobs, SSE-driven flows). `close_sockets_for_*` is idempotent so the
|
|
23
|
+
* second pass is a no-op.
|
|
24
|
+
*
|
|
25
|
+
* Mirrors `zzz_server`'s `close_sockets_for_*` calls in
|
|
26
|
+
* `account.rs::logout_inner` / `_password_inner` /
|
|
27
|
+
* `handlers/account.rs::handle_account_session_revoke[_all]` /
|
|
28
|
+
* `_token_revoke` (landed 2026-05-16); see
|
|
29
|
+
* `~/dev/grimoire/lore/fuz_app/TODO_AUTH.md` §Audit-driven WS
|
|
30
|
+
* revocation: handler-side belt+suspenders for the cross-backend
|
|
31
|
+
* parity record.
|
|
32
|
+
*
|
|
33
|
+
* `BackendWebsocketTransport` satisfies this interface structurally,
|
|
34
|
+
* so consumers pass their transport instance directly (same shape as
|
|
35
|
+
* `NotificationSender`). The interface stays local so handlers don't
|
|
36
|
+
* couple to the concrete transport, and tests can inject a capturing
|
|
37
|
+
* stub with no WS machinery.
|
|
38
|
+
*
|
|
39
|
+
* @module
|
|
40
|
+
*/
|
|
41
|
+
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,MAAM,CAAC;AAC/B,OAAO,KAAK,EAAC,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEzD,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAC1E,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAUjD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAgBpD,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AACpC,OAAO,EAAC,KAAK,MAAM,EAAC,MAAM,mBAAmB,CAAC;AAI9C,OAAO,EAAC,yBAAyB,EAAE,KAAK,kBAAkB,EAAC,MAAM,4BAA4B,CAAC;AAG9F,YAAY,EAAC,MAAM,EAAC,CAAC;AAErB,0EAA0E;AAC1E,eAAO,MAAM,gCAAgC,QAAS,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IACjC,qFAAqF;IACrF,EAAE,EAAE,SAAS,CAAC;IACd,4EAA4E;IAC5E,aAAa,EAAE,IAAI,CAAC;IACpB,oDAAoD;IACpD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;;;OAGG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,wFAAwF;IACxF,MAAM,EAAE,WAAW,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IAClC,+CAA+C;IAC/C,EAAE,EAAE,SAAS,CAAC;IACd,2CAA2C;IAC3C,aAAa,EAAE,IAAI,CAAC;IACpB,kGAAkG;IAClG,QAAQ,EAAE,kBAAkB,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACtC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wCAAwC;AACxC,MAAM,WAAW,uBAAuB;IACvC,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,GAAG,EAAE,IAAI,CAAC;IACV,iEAAiE;IACjE,gBAAgB,EAAE,gBAAgB,CAAC;IACnC;;;;;;;OAOG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;;;;;OASG;IACH,EAAE,EAAE,EAAE,CAAC;IACP;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC7C,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5C;;;;;OAKG;IACH,2BAA2B,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CACjD;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB;IACtC,yEAAyE;IACzE,SAAS,EAAE,yBAAyB,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,uBAAuB,KAAG,
|
|
1
|
+
{"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,MAAM,CAAC;AAC/B,OAAO,KAAK,EAAC,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEzD,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAC1E,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAUjD,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,oBAAoB,CAAC;AAgBpD,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AACpC,OAAO,EAAC,KAAK,MAAM,EAAC,MAAM,mBAAmB,CAAC;AAI9C,OAAO,EAAC,yBAAyB,EAAE,KAAK,kBAAkB,EAAC,MAAM,4BAA4B,CAAC;AAG9F,YAAY,EAAC,MAAM,EAAC,CAAC;AAErB,0EAA0E;AAC1E,eAAO,MAAM,gCAAgC,QAAS,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IACjC,qFAAqF;IACrF,EAAE,EAAE,SAAS,CAAC;IACd,4EAA4E;IAC5E,aAAa,EAAE,IAAI,CAAC;IACpB,oDAAoD;IACpD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;;;OAGG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,wFAAwF;IACxF,MAAM,EAAE,WAAW,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IAClC,+CAA+C;IAC/C,EAAE,EAAE,SAAS,CAAC;IACd,2CAA2C;IAC3C,aAAa,EAAE,IAAI,CAAC;IACpB,kGAAkG;IAClG,QAAQ,EAAE,kBAAkB,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACtC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wCAAwC;AACxC,MAAM,WAAW,uBAAuB;IACvC,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,GAAG,EAAE,IAAI,CAAC;IACV,iEAAiE;IACjE,gBAAgB,EAAE,gBAAgB,CAAC;IACnC;;;;;;;OAOG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;;;;;OASG;IACH,EAAE,EAAE,EAAE,CAAC;IACP;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC7C,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;OAMG;IACH,sBAAsB,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IAC5C;;;;;OAKG;IACH,2BAA2B,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CACjD;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB;IACtC,yEAAyE;IACzE,SAAS,EAAE,yBAAyB,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,kBAAkB,GAAI,SAAS,uBAAuB,KAAG,sBA4VrE,CAAC"}
|
|
@@ -90,6 +90,14 @@ export const register_action_ws = (options) => {
|
|
|
90
90
|
// `credential_type` from this closure; the live request_context is
|
|
91
91
|
// only used by the test-preset escape hatch (perform_action runs
|
|
92
92
|
// the authorization phase fresh on every message in production).
|
|
93
|
+
//
|
|
94
|
+
// Per-message dispatch reloads role_grants via the authorization
|
|
95
|
+
// phase but does NOT re-query session / token validity — those
|
|
96
|
+
// are checked once at upgrade. Revocation enforcement therefore
|
|
97
|
+
// lives outside this dispatcher, in the audit-driven WS auth
|
|
98
|
+
// guard (`transports_ws_auth_guard.ts`). Without that guard wired
|
|
99
|
+
// into the audit chain, `session_revoke` / `token_revoke` are
|
|
100
|
+
// no-ops for existing WS connections.
|
|
93
101
|
const upgrade_context = require_request_context(c);
|
|
94
102
|
const account_id = upgrade_context.account.id;
|
|
95
103
|
const client_ip = get_client_ip(c);
|
|
@@ -263,8 +271,21 @@ export const register_action_ws = (options) => {
|
|
|
263
271
|
// eager fire-and-forget pool writes (audit emits, etc.);
|
|
264
272
|
// `post_commit_effects` collects deferred thunks pushed
|
|
265
273
|
// via `emit_after_commit` (WS notifications). Both flush
|
|
266
|
-
// in the
|
|
267
|
-
//
|
|
274
|
+
// in the `finally` so the next message sees a clean slate.
|
|
275
|
+
//
|
|
276
|
+
// Ordering invariant — reply-before-flush is load-bearing.
|
|
277
|
+
// Handlers that revoke their own credential
|
|
278
|
+
// (`session_revoke_all`, `token_revoke` of the calling
|
|
279
|
+
// bearer) audit-emit events whose listener chain — wired
|
|
280
|
+
// by the WS auth guard in `transports_ws_auth_guard.ts` —
|
|
281
|
+
// closes this socket when the audit row writes. The
|
|
282
|
+
// synchronous `ws.send` on the success path returns
|
|
283
|
+
// before any close can fire (the DB write that triggers
|
|
284
|
+
// the chain is async — even in production with
|
|
285
|
+
// `await_pending_effects: false`, the listener chain only
|
|
286
|
+
// runs after the row lands). Inverting the order —
|
|
287
|
+
// flushing the queues before the send — would silently
|
|
288
|
+
// strand the caller without a reply.
|
|
268
289
|
const pending_effects = [];
|
|
269
290
|
const post_commit_effects = [];
|
|
270
291
|
const notify = notify_socket(ws);
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* and build the `RequestContext` that per-message dispatch reads.
|
|
13
13
|
* Multi-actor accounts must supply `?acting` to pick a persona;
|
|
14
14
|
* single-actor accounts work without it.
|
|
15
|
-
* 4. Optional `require_role(
|
|
16
|
-
*
|
|
15
|
+
* 4. Optional `require_role(required_roles)` — for endpoints gated to a
|
|
16
|
+
* non-empty any-of set of roles.
|
|
17
17
|
*
|
|
18
18
|
* Then delegates to `register_action_ws` for per-message JSON-RPC
|
|
19
19
|
* dispatch.
|
|
@@ -29,15 +29,17 @@ export interface RegisterWsEndpointOptions extends RegisterActionWsOptions {
|
|
|
29
29
|
* env var via `parse_allowed_origins`. Passed straight to
|
|
30
30
|
* `verify_request_source`.
|
|
31
31
|
*/
|
|
32
|
-
allowed_origins:
|
|
32
|
+
allowed_origins: ReadonlyArray<RegExp>;
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
34
|
+
* Roles permitted to upgrade — any-of disjunction (matches the
|
|
35
|
+
* underlying `require_role` semantics). Omit (or pass `[]`) for any
|
|
36
|
+
* authenticated account (`require_auth` + actor resolution alone);
|
|
37
|
+
* set to e.g. `[ROLE_ADMIN]` to gate the endpoint behind a single role
|
|
38
|
+
* or `[ROLE_ADMIN, ROLE_KEEPER]` to permit either. The per-action
|
|
39
|
+
* `auth` in each spec still applies at dispatch time — this is a coarse
|
|
40
|
+
* upgrade-time gate.
|
|
39
41
|
*/
|
|
40
|
-
|
|
42
|
+
required_roles?: ReadonlyArray<RoleName>;
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
43
45
|
* Mount a WebSocket endpoint with the standard upgrade stack (origin check
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register_ws_endpoint.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_ws_endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAYH,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;AAGrD,OAAO,EAEN,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,0CAA0C;AAC1C,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACzE;;;;OAIG;IACH,eAAe,EAAE,
|
|
1
|
+
{"version":3,"file":"register_ws_endpoint.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_ws_endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAYH,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;AAGrD,OAAO,EAEN,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,0CAA0C;AAC1C,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACzE;;;;OAIG;IACH,eAAe,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;CACzC;AAgDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,GAChC,SAAS,yBAAyB,KAChC,sBAmBF,CAAC"}
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* and build the `RequestContext` that per-message dispatch reads.
|
|
13
13
|
* Multi-actor accounts must supply `?acting` to pick a persona;
|
|
14
14
|
* single-actor accounts work without it.
|
|
15
|
-
* 4. Optional `require_role(
|
|
16
|
-
*
|
|
15
|
+
* 4. Optional `require_role(required_roles)` — for endpoints gated to a
|
|
16
|
+
* non-empty any-of set of roles.
|
|
17
17
|
*
|
|
18
18
|
* Then delegates to `register_action_ws` for per-message JSON-RPC
|
|
19
19
|
* dispatch.
|
|
@@ -77,12 +77,12 @@ const create_ws_authorization_middleware = (db) => {
|
|
|
77
77
|
* then registers the `GET path` route via the inner `register_action_ws`
|
|
78
78
|
*/
|
|
79
79
|
export const register_ws_endpoint = (options) => {
|
|
80
|
-
const { app, path, allowed_origins, db,
|
|
80
|
+
const { app, path, allowed_origins, db, required_roles, log = new Logger('[ws]'), ...rest } = options;
|
|
81
81
|
app.use(path, verify_request_source(allowed_origins));
|
|
82
82
|
app.use(path, require_auth);
|
|
83
83
|
app.use(path, create_ws_authorization_middleware(db));
|
|
84
|
-
if (
|
|
85
|
-
app.use(path, require_role(
|
|
84
|
+
if (required_roles?.length) {
|
|
85
|
+
app.use(path, require_role(required_roles));
|
|
86
86
|
}
|
|
87
87
|
return register_action_ws({ app, path, db, log, ...rest });
|
|
88
88
|
};
|
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WebSocket auth guard — bridges audit events to `BackendWebsocketTransport`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* **Why this exists.** `register_action_ws` captures `account_id` and
|
|
5
|
+
* `credential_type` at upgrade time and reuses them for every message.
|
|
6
|
+
* `perform_action`'s per-message authorization phase reloads role_grants
|
|
7
|
+
* from the DB, but session and token VALIDITY are not re-queried — that
|
|
8
|
+
* trade-off keeps chatty WS connections fast. The cost: nothing in the
|
|
9
|
+
* dispatch path notices when a session is revoked or a token is rotated.
|
|
10
|
+
* This guard is the enforcement mechanism — it listens on the audit
|
|
11
|
+
* chain and closes affected sockets when revocation events fire, so
|
|
12
|
+
* revocation actually takes effect on existing connections. Without it,
|
|
13
|
+
* `session_revoke` / `token_revoke` are no-ops for open WS connections.
|
|
7
14
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
15
|
+
* Mirror of `realtime/sse_auth_guard.ts` for the backend WebSocket
|
|
16
|
+
* transport. Dispatches audit events to the right `close_sockets_for_*`
|
|
17
|
+
* method so consumers do not re-implement the switch themselves.
|
|
18
|
+
*
|
|
19
|
+
* For standard WS endpoints mounted via `AppServerOptions.ws_endpoints`,
|
|
20
|
+
* `create_app_server` composes the guard automatically per
|
|
21
|
+
* `WsEndpointSpec.auth_guard`. For custom wiring, append the handler
|
|
22
|
+
* inside the consumer's `audit_factory` body (or via
|
|
23
|
+
* `audit.on_event_chain.push(...)` post-assembly).
|
|
10
24
|
*
|
|
11
25
|
* @module
|
|
12
26
|
*/
|
|
@@ -14,7 +28,7 @@ import type { Logger } from '@fuzdev/fuz_util/log.js';
|
|
|
14
28
|
import type { AuditLogEvent } from '../auth/audit_log_schema.js';
|
|
15
29
|
import type { BackendWebsocketTransport } from './transports_ws_backend.js';
|
|
16
30
|
/**
|
|
17
|
-
* Audit-event callback shape — the function `
|
|
31
|
+
* Audit-event callback shape — the function `CreateAuditEmitterOptions.on_audit_event`
|
|
18
32
|
* accepts and that the helpers in this module return.
|
|
19
33
|
*
|
|
20
34
|
* Exported so consumers composing multiple handlers (typically
|
|
@@ -46,8 +60,10 @@ export declare const ws_disconnect_event_types: ReadonlySet<string>;
|
|
|
46
60
|
* user close another user's socket by guessing a session hash or token id.
|
|
47
61
|
*
|
|
48
62
|
* @param log - logger for disconnect events (info level on non-zero closures)
|
|
49
|
-
* @returns an `on_audit_event` callback suitable for `
|
|
50
|
-
*
|
|
63
|
+
* @returns an `on_audit_event` callback suitable for `create_audit_emitter`'s
|
|
64
|
+
* `on_audit_event` slot, or for appending onto
|
|
65
|
+
* `audit.on_event_chain` post-assembly. The returned callback mutates
|
|
66
|
+
* `transport` (closing matching sockets via
|
|
51
67
|
* `close_sockets_for_session` / `_token` / `_account`) on every relevant event.
|
|
52
68
|
*/
|
|
53
69
|
export declare const create_ws_auth_guard: (transport: BackendWebsocketTransport, log: Logger) => AuditEventHandler;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transports_ws_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_auth_guard.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"transports_ws_auth_guard.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports_ws_auth_guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAEpD,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,6BAA6B,CAAC;AAC/D,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,4BAA4B,CAAC;AAE1E;;;;;;;;GAQG;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,CAAC;AAE/D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,yBAAyB,EAAE,WAAW,CAAC,MAAM,CAMxD,CAAC;AAEH;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,oBAAoB,GAChC,WAAW,yBAAyB,EACpC,KAAK,MAAM,KACT,iBA6CF,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,eAAO,MAAM,uBAAuB,GACnC,WAAW,yBAAyB,EACpC,KAAK,MAAM,KACT,iBAaF,CAAC"}
|
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* WebSocket auth guard — bridges audit events to `BackendWebsocketTransport`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* **Why this exists.** `register_action_ws` captures `account_id` and
|
|
5
|
+
* `credential_type` at upgrade time and reuses them for every message.
|
|
6
|
+
* `perform_action`'s per-message authorization phase reloads role_grants
|
|
7
|
+
* from the DB, but session and token VALIDITY are not re-queried — that
|
|
8
|
+
* trade-off keeps chatty WS connections fast. The cost: nothing in the
|
|
9
|
+
* dispatch path notices when a session is revoked or a token is rotated.
|
|
10
|
+
* This guard is the enforcement mechanism — it listens on the audit
|
|
11
|
+
* chain and closes affected sockets when revocation events fire, so
|
|
12
|
+
* revocation actually takes effect on existing connections. Without it,
|
|
13
|
+
* `session_revoke` / `token_revoke` are no-ops for open WS connections.
|
|
7
14
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
15
|
+
* Mirror of `realtime/sse_auth_guard.ts` for the backend WebSocket
|
|
16
|
+
* transport. Dispatches audit events to the right `close_sockets_for_*`
|
|
17
|
+
* method so consumers do not re-implement the switch themselves.
|
|
18
|
+
*
|
|
19
|
+
* For standard WS endpoints mounted via `AppServerOptions.ws_endpoints`,
|
|
20
|
+
* `create_app_server` composes the guard automatically per
|
|
21
|
+
* `WsEndpointSpec.auth_guard`. For custom wiring, append the handler
|
|
22
|
+
* inside the consumer's `audit_factory` body (or via
|
|
23
|
+
* `audit.on_event_chain.push(...)` post-assembly).
|
|
10
24
|
*
|
|
11
25
|
* @module
|
|
12
26
|
*/
|
|
@@ -39,8 +53,10 @@ export const ws_disconnect_event_types = new Set([
|
|
|
39
53
|
* user close another user's socket by guessing a session hash or token id.
|
|
40
54
|
*
|
|
41
55
|
* @param log - logger for disconnect events (info level on non-zero closures)
|
|
42
|
-
* @returns an `on_audit_event` callback suitable for `
|
|
43
|
-
*
|
|
56
|
+
* @returns an `on_audit_event` callback suitable for `create_audit_emitter`'s
|
|
57
|
+
* `on_audit_event` slot, or for appending onto
|
|
58
|
+
* `audit.on_event_chain` post-assembly. The returned callback mutates
|
|
59
|
+
* `transport` (closing matching sockets via
|
|
44
60
|
* `close_sockets_for_session` / `_token` / `_account`) on every relevant event.
|
|
45
61
|
*/
|
|
46
62
|
export const create_ws_auth_guard = (transport, log) => {
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `WsEndpointSpec` — the canonical WebSocket endpoint declaration consumed
|
|
3
|
+
* by `create_app_server`'s `ws_endpoints` option (mirror of `RpcEndpointSpec`
|
|
4
|
+
* for HTTP RPC).
|
|
5
|
+
*
|
|
6
|
+
* Lives in its own module so both `server/app_server.ts` (which mounts
|
|
7
|
+
* endpoints from these specs) and `http/surface.ts` (which threads the
|
|
8
|
+
* resolved spec list into surface generation) can import it without a
|
|
9
|
+
* cycle between the two.
|
|
10
|
+
*
|
|
11
|
+
* @module
|
|
12
|
+
*/
|
|
13
|
+
import type { Action } from './action_types.js';
|
|
14
|
+
import type { RoleName } from '../auth/role_schema.js';
|
|
15
|
+
import type { BackendWebsocketTransport } from './transports_ws_backend.js';
|
|
16
|
+
import type { ServerHeartbeatOptions, SocketCloseContext, SocketOpenContext } from './register_action_ws.js';
|
|
17
|
+
import type { AuditEventHandler } from './transports_ws_auth_guard.js';
|
|
18
|
+
/**
|
|
19
|
+
* Declarative description of a WebSocket endpoint to be auto-mounted by
|
|
20
|
+
* `create_app_server`.
|
|
21
|
+
*
|
|
22
|
+
* Single source of truth for mount + surface — the same array drives
|
|
23
|
+
* `register_ws_endpoint`-style upgrade wiring AND the `surface.ws_endpoints`
|
|
24
|
+
* slot emitted into `AppSurface`, so consumers cannot drift their declared
|
|
25
|
+
* actions from what dispatch actually serves.
|
|
26
|
+
*/
|
|
27
|
+
export interface WsEndpointSpec {
|
|
28
|
+
/** Hono mount path (e.g. `/api/ws`). */
|
|
29
|
+
path: string;
|
|
30
|
+
/**
|
|
31
|
+
* Origin allowlist regexes — typically parsed via `parse_allowed_origins`.
|
|
32
|
+
* Passed straight to `verify_request_source` on upgrade.
|
|
33
|
+
*/
|
|
34
|
+
allowed_origins: ReadonlyArray<RegExp>;
|
|
35
|
+
/**
|
|
36
|
+
* The actions registered on this endpoint. Spread `protocol_actions`
|
|
37
|
+
* from `actions/protocol.ts` first to complete the
|
|
38
|
+
* disconnect-detection + per-request cancel pairing with the frontend
|
|
39
|
+
* client.
|
|
40
|
+
*/
|
|
41
|
+
actions: ReadonlyArray<Action>;
|
|
42
|
+
/**
|
|
43
|
+
* Roles permitted to upgrade — any-of disjunction. Omit (or pass `[]`)
|
|
44
|
+
* to skip the upgrade-time role gate; per-action `auth` on each spec
|
|
45
|
+
* still applies at dispatch time via `perform_action`. Pass
|
|
46
|
+
* `[ROLE_ADMIN]` for a zap-style admin-only WS endpoint.
|
|
47
|
+
*/
|
|
48
|
+
required_roles?: ReadonlyArray<RoleName>;
|
|
49
|
+
/**
|
|
50
|
+
* Existing transport to register connections with. Auto-created when
|
|
51
|
+
* omitted. Either way the mounted transport is reachable on
|
|
52
|
+
* `AppServer.ws_endpoints[path]` for broadcast / fan-out.
|
|
53
|
+
*/
|
|
54
|
+
transport?: BackendWebsocketTransport;
|
|
55
|
+
/**
|
|
56
|
+
* Server-side heartbeat policy. Default-on (60s receive-silence
|
|
57
|
+
* timeout). Set `false` only when an upstream stack (TCP keepalive,
|
|
58
|
+
* Cloudflare idle timeout) already owns disconnect detection.
|
|
59
|
+
*/
|
|
60
|
+
heartbeat?: boolean | ServerHeartbeatOptions;
|
|
61
|
+
/** Optional per-message delay for testing loading states. */
|
|
62
|
+
artificial_delay?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Called once per socket after `transport.add_connection` but before
|
|
65
|
+
* the first message dispatches. See
|
|
66
|
+
* `RegisterActionWsOptions.on_socket_open`.
|
|
67
|
+
*/
|
|
68
|
+
on_socket_open?: (ctx: SocketOpenContext) => void | Promise<void>;
|
|
69
|
+
/**
|
|
70
|
+
* Called once per socket on close, before `transport.remove_connection`.
|
|
71
|
+
* See `RegisterActionWsOptions.on_socket_close`.
|
|
72
|
+
*/
|
|
73
|
+
on_socket_close?: (ctx: SocketCloseContext) => void | Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Default `true` — auto-composes `create_ws_auth_guard` +
|
|
76
|
+
* `create_ws_logout_closer` against this endpoint's transport and
|
|
77
|
+
* appends them to `deps.audit.on_event_chain`. Wiring is deduped by
|
|
78
|
+
* transport **reference identity** (`WeakSet<BackendWebsocketTransport>`),
|
|
79
|
+
* so two `WsEndpointSpec`s sharing the exact same instance get a
|
|
80
|
+
* single pair of listeners.
|
|
81
|
+
*
|
|
82
|
+
* **Shared-transport OR-semantics.** When multiple `WsEndpointSpec`s
|
|
83
|
+
* share one transport, the guard is wired iff **any** of those specs
|
|
84
|
+
* has `auth_guard !== false`. To opt out for a shared transport,
|
|
85
|
+
* every sibling spec must pass `auth_guard: false`. The default is
|
|
86
|
+
* "fail safe" — easier to enable than disable, and predictable
|
|
87
|
+
* regardless of spec order.
|
|
88
|
+
*
|
|
89
|
+
* Reference-identity dedupe means **wrapped or proxied transports
|
|
90
|
+
* dedupe as separate entries** — a consumer threading every
|
|
91
|
+
* transport through a tracing / DI / metrics shim will get a fresh
|
|
92
|
+
* pair of listeners per shimmed reference, even when the underlying
|
|
93
|
+
* transport is the same. If you wrap or proxy, set `auth_guard:
|
|
94
|
+
* false` on the duplicate `WsEndpointSpec`s and compose
|
|
95
|
+
* `create_ws_auth_guard` / `create_ws_logout_closer` against the
|
|
96
|
+
* underlying transport once.
|
|
97
|
+
*
|
|
98
|
+
* Set `false` when a consumer needs to compose their own callback
|
|
99
|
+
* from scratch — or to opt out of the auto-wiring entirely.
|
|
100
|
+
*
|
|
101
|
+
* NOTE: does NOT close sockets on `role_grant_revoke` — that omission
|
|
102
|
+
* is deliberate (per-connection role tracking is out of scope). A user
|
|
103
|
+
* whose admin role is revoked keeps their socket open; the next message
|
|
104
|
+
* gets `forbidden` from the per-message authorization phase. Consumers
|
|
105
|
+
* wanting role-revoke disconnection use `extra_audit_handlers`.
|
|
106
|
+
*/
|
|
107
|
+
auth_guard?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Extra audit-event handlers appended to `deps.audit.on_event_chain`
|
|
110
|
+
* AFTER the standard `auth_guard` wiring (when enabled). By the time
|
|
111
|
+
* these run, the standard guards may have already closed sockets. Use
|
|
112
|
+
* for role-revoke disconnection, custom analytics, etc.
|
|
113
|
+
*
|
|
114
|
+
* Never deduped — consumer-owned; pass the same handler twice and it
|
|
115
|
+
* fires twice.
|
|
116
|
+
*/
|
|
117
|
+
extra_audit_handlers?: ReadonlyArray<AuditEventHandler>;
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=ws_endpoint_spec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws_endpoint_spec.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/ws_endpoint_spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,mBAAmB,CAAC;AAC9C,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,4BAA4B,CAAC;AAC1E,OAAO,KAAK,EACX,sBAAsB,EACtB,kBAAkB,EAClB,iBAAiB,EACjB,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,+BAA+B,CAAC;AAErE;;;;;;;;GAQG;AACH,MAAM,WAAW,cAAc;IAC9B,wCAAwC;IACxC,IAAI,EAAE,MAAM,CAAC;IACb;;;OAGG;IACH,eAAe,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACvC;;;;;OAKG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACzC;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;OAIG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC7C,6DAA6D;IAC7D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAgCG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB;;;;;;;;OAQG;IACH,oBAAoB,CAAC,EAAE,aAAa,CAAC,iBAAiB,CAAC,CAAC;CACxD"}
|