@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.
Files changed (107) hide show
  1. package/dist/actions/CLAUDE.md +124 -11
  2. package/dist/actions/connection_closer.d.ts +68 -0
  3. package/dist/actions/connection_closer.d.ts.map +1 -0
  4. package/dist/actions/connection_closer.js +41 -0
  5. package/dist/actions/register_action_ws.d.ts.map +1 -1
  6. package/dist/actions/register_action_ws.js +23 -2
  7. package/dist/actions/register_ws_endpoint.d.ts +11 -9
  8. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  9. package/dist/actions/register_ws_endpoint.js +5 -5
  10. package/dist/actions/transports_ws_auth_guard.d.ts +24 -8
  11. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  12. package/dist/actions/transports_ws_auth_guard.js +23 -7
  13. package/dist/actions/ws_endpoint_spec.d.ts +119 -0
  14. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -0
  15. package/dist/actions/ws_endpoint_spec.js +13 -0
  16. package/dist/auth/CLAUDE.md +79 -15
  17. package/dist/auth/account_action_specs.d.ts +1 -1
  18. package/dist/auth/account_actions.d.ts +13 -0
  19. package/dist/auth/account_actions.d.ts.map +1 -1
  20. package/dist/auth/account_actions.js +31 -1
  21. package/dist/auth/account_routes.d.ts +12 -2
  22. package/dist/auth/account_routes.d.ts.map +1 -1
  23. package/dist/auth/account_routes.js +55 -8
  24. package/dist/auth/account_schema.d.ts +3 -3
  25. package/dist/auth/admin_action_specs.d.ts +8 -8
  26. package/dist/auth/admin_actions.d.ts +11 -0
  27. package/dist/auth/admin_actions.d.ts.map +1 -1
  28. package/dist/auth/admin_actions.js +25 -0
  29. package/dist/auth/audit_emitter.d.ts +56 -12
  30. package/dist/auth/audit_emitter.d.ts.map +1 -1
  31. package/dist/auth/audit_emitter.js +38 -12
  32. package/dist/auth/audit_log_schema.d.ts +5 -3
  33. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  34. package/dist/auth/audit_log_schema.js +5 -3
  35. package/dist/auth/bootstrap_routes.d.ts +1 -1
  36. package/dist/auth/invite_schema.d.ts +2 -2
  37. package/dist/auth/signup_routes.d.ts +1 -1
  38. package/dist/auth/standard_rpc_actions.d.ts +1 -0
  39. package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
  40. package/dist/auth/standard_rpc_actions.js +1 -0
  41. package/dist/http/CLAUDE.md +26 -10
  42. package/dist/http/ip_canonical.d.ts +99 -0
  43. package/dist/http/ip_canonical.d.ts.map +1 -0
  44. package/dist/http/ip_canonical.js +191 -0
  45. package/dist/http/origin.d.ts +13 -5
  46. package/dist/http/origin.d.ts.map +1 -1
  47. package/dist/http/origin.js +13 -31
  48. package/dist/http/pending_effects.d.ts +1 -1
  49. package/dist/http/pending_effects.js +1 -1
  50. package/dist/http/proxy.d.ts +13 -5
  51. package/dist/http/proxy.d.ts.map +1 -1
  52. package/dist/http/proxy.js +15 -23
  53. package/dist/http/surface.d.ts +50 -0
  54. package/dist/http/surface.d.ts.map +1 -1
  55. package/dist/http/surface.js +27 -1
  56. package/dist/primitive_schemas.d.ts +20 -4
  57. package/dist/primitive_schemas.d.ts.map +1 -1
  58. package/dist/primitive_schemas.js +25 -4
  59. package/dist/realtime/sse_auth_guard.d.ts +16 -4
  60. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  61. package/dist/realtime/sse_auth_guard.js +15 -3
  62. package/dist/server/app_backend.d.ts +66 -19
  63. package/dist/server/app_backend.d.ts.map +1 -1
  64. package/dist/server/app_backend.js +57 -34
  65. package/dist/server/app_server.d.ts +60 -0
  66. package/dist/server/app_server.d.ts.map +1 -1
  67. package/dist/server/app_server.js +95 -2
  68. package/dist/server/startup.d.ts.map +1 -1
  69. package/dist/server/startup.js +12 -0
  70. package/dist/testing/CLAUDE.md +64 -28
  71. package/dist/testing/admin_integration.d.ts.map +1 -1
  72. package/dist/testing/admin_integration.js +4 -5
  73. package/dist/testing/adversarial_headers.d.ts +6 -0
  74. package/dist/testing/adversarial_headers.d.ts.map +1 -1
  75. package/dist/testing/adversarial_headers.js +13 -5
  76. package/dist/testing/app_server.d.ts +33 -32
  77. package/dist/testing/app_server.d.ts.map +1 -1
  78. package/dist/testing/app_server.js +4 -13
  79. package/dist/testing/attack_surface.d.ts +8 -7
  80. package/dist/testing/attack_surface.d.ts.map +1 -1
  81. package/dist/testing/attack_surface.js +12 -8
  82. package/dist/testing/audit_completeness.d.ts.map +1 -1
  83. package/dist/testing/audit_completeness.js +3 -5
  84. package/dist/testing/audit_drift_guard.d.ts +116 -0
  85. package/dist/testing/audit_drift_guard.d.ts.map +1 -0
  86. package/dist/testing/audit_drift_guard.js +134 -0
  87. package/dist/testing/connection_closer_helpers.d.ts +44 -0
  88. package/dist/testing/connection_closer_helpers.d.ts.map +1 -0
  89. package/dist/testing/connection_closer_helpers.js +48 -0
  90. package/dist/testing/integration.d.ts.map +1 -1
  91. package/dist/testing/integration.js +7 -9
  92. package/dist/testing/rate_limiting.js +4 -4
  93. package/dist/testing/rpc_helpers.d.ts +2 -1
  94. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  95. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  96. package/dist/testing/rpc_round_trip.js +6 -8
  97. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  98. package/dist/testing/sse_round_trip.js +12 -6
  99. package/dist/testing/stubs.d.ts +11 -0
  100. package/dist/testing/stubs.d.ts.map +1 -1
  101. package/dist/testing/stubs.js +4 -0
  102. package/dist/testing/surface_invariants.d.ts +66 -1
  103. package/dist/testing/surface_invariants.d.ts.map +1 -1
  104. package/dist/testing/surface_invariants.js +103 -1
  105. package/dist/ui/SurfaceExplorer.svelte +161 -2
  106. package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
  107. package/package.json +1 -1
@@ -469,9 +469,18 @@ persistence + rehydration by the consumer.
469
469
 
470
470
  ## WS auth guard (`transports_ws_auth_guard.ts`)
471
471
 
472
- `create_ws_auth_guard(transport, log)` returns an `on_audit_event` callback
473
- wireable via `CreateAppBackendOptions.on_audit_event`. Mirrors the SSE
474
- guard in `realtime/sse_auth_guard.ts` but targets the WS transport.
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
- ## WebSocket dispatch
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
- Two layered entry points:
556
+ ## WebSocket dispatch
516
557
 
517
- ### `register_ws_endpoint` (`register_ws_endpoint.ts`) idiomatic
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([required_role])` (single-element array form)
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: Array<RegExp>`
528
- and optional `required_role: RoleName`. Returns `{transport}`. Note:
529
- `required_role` is a **coarse upgrade-time gate** — per-action `auth` in
530
- each spec still applies at dispatch time via `perform_action`.
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,sBAuUrE,CAAC"}
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 same try/finally so the next message sees a clean
267
- // slate.
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(required_role)` — for endpoints gated to a
16
- * specific role.
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: Array<RegExp>;
32
+ allowed_origins: ReadonlyArray<RegExp>;
33
33
  /**
34
- * Role required to upgrade. Omit for any authenticated account
35
- * (`require_auth` + actor resolution alone); set to e.g. `ROLE_ADMIN`
36
- * to gate the endpoint behind a role. The per-action `auth` in each
37
- * spec still applies at dispatch time this is a coarse upgrade-time
38
- * gate.
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
- required_role?: RoleName;
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,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,QAAQ,CAAC;CACzB;AAgDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,GAChC,SAAS,yBAAyB,KAChC,sBAmBF,CAAC"}
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(required_role)` — for endpoints gated to a
16
- * specific role.
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, required_role, log = new Logger('[ws]'), ...rest } = options;
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 (required_role !== undefined) {
85
- app.use(path, require_role([required_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
- * Mirror of `realtime/sse_auth_guard.ts` for the backend WebSocket transport.
5
- * Dispatches audit events to the right `close_sockets_for_*` method so
6
- * consumers do not re-implement the switch themselves.
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
- * Consumers wire it as `on_audit_event` on their `AppBackend` (or compose
9
- * it with other callbacks via `create_app_server`'s `audit_log_sse` path).
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 `CreateAppBackendOptions.on_audit_event`
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 `CreateAppBackendOptions`.
50
- * The returned callback mutates `transport` (closing matching sockets via
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;;;;;;;;;;;GAWG;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;;;;;;;;;;;;GAYG;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
+ {"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
- * Mirror of `realtime/sse_auth_guard.ts` for the backend WebSocket transport.
5
- * Dispatches audit events to the right `close_sockets_for_*` method so
6
- * consumers do not re-implement the switch themselves.
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
- * Consumers wire it as `on_audit_event` on their `AppBackend` (or compose
9
- * it with other callbacks via `create_app_server`'s `audit_log_sse` path).
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 `CreateAppBackendOptions`.
43
- * The returned callback mutates `transport` (closing matching sockets via
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"}