@fuzdev/fuz_app 0.117.1 → 0.119.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/dist/actions/CLAUDE.md +301 -73
  2. package/dist/actions/action_rpc.d.ts.map +1 -1
  3. package/dist/actions/action_rpc.js +16 -1
  4. package/dist/actions/connection_closer.d.ts +136 -51
  5. package/dist/actions/connection_closer.d.ts.map +1 -1
  6. package/dist/actions/connection_closer.js +133 -34
  7. package/dist/actions/register_action_ws.d.ts +176 -24
  8. package/dist/actions/register_action_ws.d.ts.map +1 -1
  9. package/dist/actions/register_action_ws.js +581 -241
  10. package/dist/actions/register_ws_endpoint.d.ts +1 -0
  11. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  12. package/dist/actions/register_ws_endpoint.js +1 -0
  13. package/dist/actions/socket.svelte.d.ts +43 -9
  14. package/dist/actions/socket.svelte.d.ts.map +1 -1
  15. package/dist/actions/socket.svelte.js +59 -8
  16. package/dist/actions/transports.d.ts +55 -1
  17. package/dist/actions/transports.d.ts.map +1 -1
  18. package/dist/actions/transports.js +55 -1
  19. package/dist/actions/transports_ws_auth_guard.d.ts +53 -67
  20. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  21. package/dist/actions/transports_ws_auth_guard.js +87 -111
  22. package/dist/actions/transports_ws_backend.d.ts +212 -30
  23. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  24. package/dist/actions/transports_ws_backend.js +327 -70
  25. package/dist/actions/ws_endpoint_spec.d.ts +26 -13
  26. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  27. package/dist/auth/CLAUDE.md +125 -31
  28. package/dist/auth/account_actions.d.ts +12 -18
  29. package/dist/auth/account_actions.d.ts.map +1 -1
  30. package/dist/auth/account_actions.js +47 -33
  31. package/dist/auth/account_routes.d.ts +5 -11
  32. package/dist/auth/account_routes.d.ts.map +1 -1
  33. package/dist/auth/account_routes.js +40 -36
  34. package/dist/auth/account_schema.d.ts +27 -0
  35. package/dist/auth/account_schema.d.ts.map +1 -1
  36. package/dist/auth/account_schema.js +22 -0
  37. package/dist/auth/admin_actions.d.ts +8 -17
  38. package/dist/auth/admin_actions.d.ts.map +1 -1
  39. package/dist/auth/admin_actions.js +33 -21
  40. package/dist/auth/api_token_queries.d.ts +23 -2
  41. package/dist/auth/api_token_queries.d.ts.map +1 -1
  42. package/dist/auth/api_token_queries.js +39 -3
  43. package/dist/auth/audit_emitter.d.ts +72 -44
  44. package/dist/auth/audit_emitter.d.ts.map +1 -1
  45. package/dist/auth/audit_emitter.js +60 -16
  46. package/dist/auth/audit_log_route_schema.js +7 -7
  47. package/dist/auth/audit_log_routes.d.ts +31 -6
  48. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  49. package/dist/auth/audit_log_routes.js +85 -11
  50. package/dist/auth/audit_log_schema.d.ts +48 -0
  51. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  52. package/dist/auth/audit_log_schema.js +58 -0
  53. package/dist/auth/bootstrap_account.d.ts +3 -2
  54. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  55. package/dist/auth/bootstrap_account.js +3 -3
  56. package/dist/auth/cleanup.d.ts +219 -30
  57. package/dist/auth/cleanup.d.ts.map +1 -1
  58. package/dist/auth/cleanup.js +260 -45
  59. package/dist/auth/deps.d.ts +29 -1
  60. package/dist/auth/deps.d.ts.map +1 -1
  61. package/dist/auth/migrations.d.ts.map +1 -1
  62. package/dist/auth/migrations.js +27 -0
  63. package/dist/auth/resolved_auth.d.ts +105 -0
  64. package/dist/auth/resolved_auth.d.ts.map +1 -0
  65. package/dist/auth/resolved_auth.js +124 -0
  66. package/dist/auth/role_grant_offer_ddl.d.ts +4 -2
  67. package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
  68. package/dist/auth/role_grant_offer_ddl.js +10 -3
  69. package/dist/auth/role_grant_offer_queries.d.ts +16 -7
  70. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  71. package/dist/auth/role_grant_offer_queries.js +33 -15
  72. package/dist/auth/role_grant_offer_schema.d.ts +9 -0
  73. package/dist/auth/role_grant_offer_schema.d.ts.map +1 -1
  74. package/dist/auth/session_middleware.d.ts +8 -1
  75. package/dist/auth/session_middleware.d.ts.map +1 -1
  76. package/dist/auth/session_middleware.js +9 -3
  77. package/dist/auth/session_queries.d.ts +31 -6
  78. package/dist/auth/session_queries.d.ts.map +1 -1
  79. package/dist/auth/session_queries.js +31 -6
  80. package/dist/auth/standard_rpc_actions.d.ts +9 -9
  81. package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
  82. package/dist/auth/standard_rpc_actions.js +3 -3
  83. package/dist/db/CLAUDE.md +7 -1
  84. package/dist/db/migrate.d.ts +8 -13
  85. package/dist/db/migrate.d.ts.map +1 -1
  86. package/dist/db/migrate.js +9 -38
  87. package/dist/db/status.d.ts +58 -7
  88. package/dist/db/status.d.ts.map +1 -1
  89. package/dist/db/status.js +205 -97
  90. package/dist/http/CLAUDE.md +7 -3
  91. package/dist/http/db_routes.d.ts +6 -2
  92. package/dist/http/db_routes.d.ts.map +1 -1
  93. package/dist/http/db_routes.js +2 -2
  94. package/dist/periodic.d.ts +80 -0
  95. package/dist/periodic.d.ts.map +1 -0
  96. package/dist/periodic.js +122 -0
  97. package/dist/realtime/sse_auth_guard.d.ts +79 -49
  98. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  99. package/dist/realtime/sse_auth_guard.js +103 -73
  100. package/dist/realtime/subscriber_registry.d.ts +119 -8
  101. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  102. package/dist/realtime/subscriber_registry.js +184 -25
  103. package/dist/runtime/deno.d.ts.map +1 -1
  104. package/dist/runtime/deno.js +9 -6
  105. package/dist/runtime/deps.d.ts +3 -2
  106. package/dist/runtime/deps.d.ts.map +1 -1
  107. package/dist/runtime/secure_file.d.ts +23 -9
  108. package/dist/runtime/secure_file.d.ts.map +1 -1
  109. package/dist/runtime/secure_file.js +29 -10
  110. package/dist/server/app_backend.d.ts +13 -2
  111. package/dist/server/app_backend.d.ts.map +1 -1
  112. package/dist/server/app_backend.js +5 -2
  113. package/dist/server/app_server.d.ts +55 -15
  114. package/dist/server/app_server.d.ts.map +1 -1
  115. package/dist/server/app_server.js +41 -13
  116. package/dist/testing/CLAUDE.md +140 -48
  117. package/dist/testing/app_server.d.ts +2 -2
  118. package/dist/testing/app_server.d.ts.map +1 -1
  119. package/dist/testing/app_server.js +11 -2
  120. package/dist/testing/audit_drift_guard.d.ts +3 -52
  121. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  122. package/dist/testing/audit_drift_guard.js +2 -46
  123. package/dist/testing/connection_closer_helpers.d.ts +9 -22
  124. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  125. package/dist/testing/connection_closer_helpers.js +11 -17
  126. package/dist/testing/cross_backend/capabilities.d.ts +67 -13
  127. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  128. package/dist/testing/cross_backend/capabilities.js +27 -12
  129. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  130. package/dist/testing/cross_backend/default_backend_configs.js +21 -0
  131. package/dist/testing/cross_backend/expected_schema.json +1 -0
  132. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  133. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
  134. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  135. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +4 -1
  136. package/dist/testing/cross_backend/session_cap.d.ts +13 -0
  137. package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
  138. package/dist/testing/cross_backend/session_cap.js +55 -19
  139. package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
  140. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
  141. package/dist/testing/cross_backend/sse_round_trip.js +93 -26
  142. package/dist/testing/cross_backend/testing_server_bun.js +4 -4
  143. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  144. package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -2
  145. package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
  146. package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
  147. package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
  148. package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
  149. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  150. package/dist/testing/cross_backend/ws_round_trip.js +156 -19
  151. package/dist/testing/sse_round_trip.d.ts +6 -4
  152. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  153. package/dist/testing/stubs.d.ts +1 -1
  154. package/dist/testing/stubs.d.ts.map +1 -1
  155. package/dist/testing/stubs.js +6 -4
  156. package/dist/testing/transports/sse_transport.d.ts +1 -1
  157. package/dist/testing/transports/ws_client.d.ts +7 -0
  158. package/dist/testing/transports/ws_client.d.ts.map +1 -1
  159. package/dist/testing/transports/ws_raw_client.d.ts +51 -0
  160. package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
  161. package/dist/testing/transports/ws_raw_client.js +156 -0
  162. package/dist/testing/transports/ws_transport.d.ts +28 -0
  163. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  164. package/dist/testing/transports/ws_transport.js +45 -1
  165. package/dist/testing/ws_round_trip.d.ts +8 -3
  166. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  167. package/dist/testing/ws_round_trip.js +19 -4
  168. package/dist/ui/AdminAccounts.svelte +4 -3
  169. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  170. package/dist/ui/AdminOverview.svelte +3 -1
  171. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  172. package/dist/ui/CLAUDE.md +22 -6
  173. package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
  174. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  175. package/dist/ui/admin_accounts_state.svelte.js +16 -0
  176. package/dist/ui/auth_state.svelte.d.ts +25 -1
  177. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  178. package/dist/ui/auth_state.svelte.js +28 -1
  179. package/package.json +2 -2
@@ -26,7 +26,8 @@ import { type RouteSpec } from '../http/route_spec.js';
26
26
  import type { MiddlewareSpec } from '../http/middleware_spec.js';
27
27
  import { type BootstrapStatus } from '../auth/bootstrap_routes.js';
28
28
  import type { WsEndpointSpec } from '../actions/ws_endpoint_spec.js';
29
- import { BackendWebsocketTransport } from '../actions/transports_ws_backend.js';
29
+ import type { BackendWebsocketTransport } from '../actions/transports_ws_backend.js';
30
+ import { type AuthCleanupScheduleOptions } from '../auth/cleanup.js';
30
31
  /**
31
32
  * Context passed to `on_effect_error` when a pending effect rejects.
32
33
  */
@@ -185,12 +186,13 @@ export interface AppServerOptions {
185
186
  *
186
187
  * When truthy, creates an `AuditLogSse` instance internally, registers the SSE
187
188
  * listener via `backend.deps.audit.add_listener` (composing with the
188
- * consumer's `on_audit_event` callback rather than rebuilding `AppDeps`), and
189
- * auto-includes `audit_log_event_specs` in the surface. The result is exposed
190
- * on `AppServerContext` (for route factories) and `AppServer` (for the caller),
191
- * always typed as `AuditLogSse | null` — when this option is set, the field
192
- * is non-null. Use `require_audit_sse(ctx)` to assert the invariant in
193
- * route factories that depend on it.
189
+ * consumer's `on_audit_event` callback rather than rebuilding `AppDeps`),
190
+ * adds its registry to `backend.deps.connection_closer` so revocations close
191
+ * its streams, and auto-includes `audit_log_event_specs` in the surface. The
192
+ * result is exposed on `AppServerContext` (for route factories) and
193
+ * `AppServer` (for the caller), always typed as `AuditLogSse | null` — when
194
+ * this option is set, the field is non-null. Use `require_audit_sse(ctx)` to
195
+ * assert the invariant in route factories that depend on it.
194
196
  *
195
197
  * Pass `true` for defaults (admin role), or `{role: 'custom'}` for a custom role.
196
198
  * Omit to wire audit SSE manually.
@@ -198,6 +200,28 @@ export interface AppServerOptions {
198
200
  audit_log_sse?: true | {
199
201
  role?: string;
200
202
  };
203
+ /**
204
+ * Schedule the auth cleanup beside the server: `start_auth_cleanup` over
205
+ * `backend.deps` — a pass as assembly finishes, then one per interval —
206
+ * stopped by `AppServer.close`. Each pass deletes expired sessions and
207
+ * closes the connections they opened through `deps.connection_closer`
208
+ * (every WebSocket transport and audit stream this server mounts), and
209
+ * audits each newly expired role_grant offer once.
210
+ *
211
+ * **Off by default; a production server should turn it on.** Without it
212
+ * nothing sweeps: expired session rows accumulate, a WebSocket or audit
213
+ * stream outlives the expiry of the session it opened on until it
214
+ * disconnects, and an offer's expiry is never audited. With it, a
215
+ * connection outlives its session's expiry by at most one interval. The
216
+ * Rust spine's servers all schedule the twin (`spawn_auth_cleanup`).
217
+ *
218
+ * Pass `true` for the default interval
219
+ * (`DEFAULT_AUTH_CLEANUP_INTERVAL_MS`), or `{interval_ms}` for another.
220
+ * Leave it off in an in-process test harness — a background pass deletes
221
+ * rows and writes audit rows mid-test — and in a process that runs
222
+ * `run_auth_cleanup` on a schedule of its own.
223
+ */
224
+ auth_cleanup?: boolean | AuthCleanupScheduleOptions;
201
225
  /** SSE event specs for surface generation. Defaults to `[]` (no SSE events). */
202
226
  event_specs?: Array<EventSpec>;
203
227
  /**
@@ -245,13 +269,17 @@ export interface AppServerOptions {
245
269
  * Duplicate `path` values across two `WsEndpointSpec`s throw at
246
270
  * mount time (Hono would silently shadow them otherwise).
247
271
  *
272
+ * Every mounted transport is added to `deps.connection_closer`, so
273
+ * the revocation handlers' post-commit closes reach its sockets —
274
+ * unconditionally, since those closes are what ends a revoked
275
+ * credential's connections.
276
+ *
248
277
  * Each spec's `auth_guard?` defaults to `true` — the factory
249
- * composes `create_ws_auth_guard` + `create_ws_logout_closer`
250
- * against the mounted transport and registers them via
251
- * `deps.audit.add_listener`. Wiring is deduped by transport
252
- * **reference identity** so two specs sharing one
253
- * `BackendWebsocketTransport` instance get a single pair of
254
- * listeners; wrapped / proxied transports dedupe as separate
278
+ * registers `create_ws_auth_guard` against the mounted transport via
279
+ * `deps.audit.add_listener`, repeating the close on the audit row.
280
+ * Wiring is deduped by transport **reference identity** so two specs
281
+ * sharing one `BackendWebsocketTransport` instance get a single
282
+ * listener; wrapped / proxied transports dedupe as separate
255
283
  * entries (set `auth_guard: false` on duplicates and compose
256
284
  * against the underlying transport once).
257
285
  */
@@ -320,7 +348,12 @@ export interface AppServer {
320
348
  * Empty when no `ws_endpoints` were mounted.
321
349
  */
322
350
  ws_endpoints: Readonly<Record<string, BackendWebsocketTransport>>;
323
- /** Close the database connection. Propagated from `AppBackend`. */
351
+ /**
352
+ * Shut the server's resources down: stop the auth cleanup schedule when
353
+ * the `auth_cleanup` option started one — waiting for a pass in progress —
354
+ * then close the database connection (`AppBackend.close`). That wait has
355
+ * no bound of its own; a shutdown with a deadline races it.
356
+ */
324
357
  close: () => Promise<void>;
325
358
  }
326
359
  /**
@@ -354,12 +387,19 @@ export declare const DEFAULT_MAX_BODY_SIZE: number;
354
387
  * pass `migration_namespaces` to `create_app_backend`.
355
388
  *
356
389
  * When `audit_log_sse` is set, the SSE registry's listener is registered via
357
- * `backend.deps.audit.add_listener` — no shallow-copy of `AppDeps`. The
390
+ * `backend.deps.audit.add_listener` — no shallow-copy of `AppDeps`. That
391
+ * registry and every WebSocket transport mounted from `ws_endpoints` are added
392
+ * to `backend.deps.connection_closer`, so the revocation handlers built from
393
+ * `ctx.deps` close their connections with no further wiring. The
358
394
  * `audit_sse` field on the returned `AppServer` (and the
359
395
  * `AppServerContext` passed to `create_route_specs`) is non-null in that
360
396
  * case; consumers can call `require_audit_sse(ctx)` / `require_audit_sse(server)`
361
397
  * to assert the invariant.
362
398
  *
399
+ * When `auth_cleanup` is set, the auth cleanup schedule starts as the last
400
+ * step of assembly — so an assembly that throws leaves no schedule running —
401
+ * and the returned `close` stops it before closing the database.
402
+ *
363
403
  * @returns assembled Hono app, backend, surface build, and bootstrap status
364
404
  */
365
405
  export declare const create_app_server: (options: AppServerOptions) => Promise<AppServer>;
@@ -1 +1 @@
1
- {"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/app_server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/app_server.ts"],"names":[],"mappings":"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"}
@@ -32,8 +32,8 @@ import { create_fuz_authorization_handler } from '../auth/request_context.js';
32
32
  import { ERROR_PAYLOAD_TOO_LARGE } from '../http/error_schemas.js';
33
33
  import { create_rpc_endpoint } from '../actions/action_rpc.js';
34
34
  import { register_ws_endpoint } from '../actions/register_ws_endpoint.js';
35
- import { create_ws_auth_guard, create_ws_logout_closer } from '../actions/transports_ws_auth_guard.js';
36
- import { BackendWebsocketTransport } from '../actions/transports_ws_backend.js';
35
+ import { create_ws_auth_guard } from '../actions/transports_ws_auth_guard.js';
36
+ import { start_auth_cleanup } from '../auth/cleanup.js';
37
37
  /**
38
38
  * Assert that `audit_sse` was wired by `create_app_server` and return it
39
39
  * as a non-null `AuditLogSse`. Throws a labelled error when the
@@ -68,12 +68,19 @@ export const DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
68
68
  * pass `migration_namespaces` to `create_app_backend`.
69
69
  *
70
70
  * When `audit_log_sse` is set, the SSE registry's listener is registered via
71
- * `backend.deps.audit.add_listener` — no shallow-copy of `AppDeps`. The
71
+ * `backend.deps.audit.add_listener` — no shallow-copy of `AppDeps`. That
72
+ * registry and every WebSocket transport mounted from `ws_endpoints` are added
73
+ * to `backend.deps.connection_closer`, so the revocation handlers built from
74
+ * `ctx.deps` close their connections with no further wiring. The
72
75
  * `audit_sse` field on the returned `AppServer` (and the
73
76
  * `AppServerContext` passed to `create_route_specs`) is non-null in that
74
77
  * case; consumers can call `require_audit_sse(ctx)` / `require_audit_sse(server)`
75
78
  * to assert the invariant.
76
79
  *
80
+ * When `auth_cleanup` is set, the auth cleanup schedule starts as the last
81
+ * step of assembly — so an assembly that throws leaves no schedule running —
82
+ * and the returned `close` stops it before closing the database.
83
+ *
77
84
  * @returns assembled Hono app, backend, surface build, and bootstrap status
78
85
  */
79
86
  export const create_app_server = async (options) => {
@@ -106,11 +113,13 @@ export const create_app_server = async (options) => {
106
113
  : options.action_account_rate_limiter;
107
114
  // Factory-managed audit SSE — registers a listener on the bound emitter
108
115
  // so SSE fan-out runs alongside the consumer's `on_audit_event`
109
- // without rebuilding `AppDeps`.
116
+ // without rebuilding `AppDeps`, and adds the registry to the backend's
117
+ // closer so revocations close its streams.
110
118
  const audit_sse = options.audit_log_sse
111
119
  ? create_audit_log_sse({
112
120
  log,
113
- role: typeof options.audit_log_sse === 'object' ? options.audit_log_sse.role : undefined
121
+ role: typeof options.audit_log_sse === 'object' ? options.audit_log_sse.role : undefined,
122
+ connection_closer: deps.connection_closer
114
123
  })
115
124
  : null;
116
125
  if (audit_sse) {
@@ -281,7 +290,10 @@ export const create_app_server = async (options) => {
281
290
  // committed transaction). Both queues drain here, after the handler
282
291
  // (and any wrapping `db.transaction`) returns. In test mode both are
283
292
  // awaited before the response returns; in production, eager-queue
284
- // rejections are reported via `on_effect_error`.
293
+ // rejections are reported via `on_effect_error`. Either way the
294
+ // deferred thunks are invoked without waiting for the eager writes to
295
+ // settle — a revocation's connection close must not sit behind a slow
296
+ // audit INSERT.
285
297
  app.use('*', async (c, next) => {
286
298
  c.set('pending_effects', []);
287
299
  c.set('post_commit_effects', []);
@@ -293,8 +305,11 @@ export const create_app_server = async (options) => {
293
305
  const deferred = c.var.post_commit_effects;
294
306
  if (eager.length || deferred.length) {
295
307
  if (options.await_pending_effects) {
308
+ // run the deferred thunks before awaiting the eager writes,
309
+ // so neither mode holds them behind a write
310
+ const deferred_flush = flush_post_commit_effects(deferred, log);
296
311
  await flush_pending_effects(eager, log);
297
- await flush_post_commit_effects(deferred, log);
312
+ await deferred_flush;
298
313
  }
299
314
  else {
300
315
  const error_ctx = { method: c.req.method, path: c.req.path };
@@ -348,7 +363,7 @@ export const create_app_server = async (options) => {
348
363
  }
349
364
  const seen_paths = new Set();
350
365
  // Dedupe `auth_guard` wiring by transport reference — two specs
351
- // sharing one transport instance get a single pair of listeners,
366
+ // sharing one transport instance get a single listener,
352
367
  // otherwise revocation events would fire `close_sockets_for_*`
353
368
  // twice per event (idempotent on the transport but log-spammy).
354
369
  // Cross-spec OR-semantics: any spec with `auth_guard !== false`
@@ -364,15 +379,17 @@ export const create_app_server = async (options) => {
364
379
  throw new Error(`create_app_server: ws_endpoints path collides with a GET RouteSpec: ${endpoint.path}`);
365
380
  }
366
381
  seen_paths.add(endpoint.path);
367
- const endpoint_transport = endpoint.transport ?? new BackendWebsocketTransport();
368
- register_ws_endpoint({
382
+ const { transport: endpoint_transport } = register_ws_endpoint({
369
383
  app,
370
384
  path: endpoint.path,
371
385
  upgradeWebSocket: options.upgradeWebSocket,
372
386
  allowed_origins: endpoint.allowed_origins,
373
387
  db: deps.db,
374
388
  actions: endpoint.actions,
375
- transport: endpoint_transport,
389
+ transport: endpoint.transport,
390
+ // every revocation handler's close reaches this transport
391
+ connection_closer: deps.connection_closer,
392
+ max_connections_per_account: endpoint.max_connections_per_account,
376
393
  heartbeat: endpoint.heartbeat,
377
394
  artificial_delay: endpoint.artificial_delay,
378
395
  max_message_bytes: endpoint.max_message_bytes,
@@ -387,7 +404,6 @@ export const create_app_server = async (options) => {
387
404
  if (endpoint.auth_guard !== false && !guarded_transports.has(endpoint_transport)) {
388
405
  guarded_transports.add(endpoint_transport);
389
406
  deps.audit.add_listener(create_ws_auth_guard(endpoint_transport, log));
390
- deps.audit.add_listener(create_ws_logout_closer(endpoint_transport, log));
391
407
  }
392
408
  if (endpoint.extra_audit_handlers?.length) {
393
409
  for (const handler of endpoint.extra_audit_handlers) {
@@ -407,6 +423,12 @@ export const create_app_server = async (options) => {
407
423
  app.use('/*', mw);
408
424
  }
409
425
  }
426
+ // Auth cleanup — opt-in, and last: nothing after it can throw, so a failed
427
+ // assembly never leaves a schedule running. Its closes go through
428
+ // `deps.connection_closer`, which the mounts above have filled.
429
+ const auth_cleanup = options.auth_cleanup
430
+ ? start_auth_cleanup(deps, options.auth_cleanup === true ? undefined : options.auth_cleanup)
431
+ : null;
410
432
  return {
411
433
  app,
412
434
  surface_spec,
@@ -414,6 +436,12 @@ export const create_app_server = async (options) => {
414
436
  migration_results: backend.migration_results,
415
437
  audit_sse,
416
438
  ws_endpoints: mounted_ws_endpoints,
417
- close: backend.close
439
+ close: auth_cleanup
440
+ ? async () => {
441
+ // the schedule first, so a pass in progress finishes on a live pool
442
+ await auth_cleanup.stop();
443
+ await backend.close();
444
+ }
445
+ : backend.close
418
446
  };
419
447
  };
@@ -31,10 +31,10 @@ time (never runtime), where a throwing guard would break `vite build`.
31
31
  - `create_stub_db()` — real `Db` whose `client.query` yields `{rows: []}` and `transaction(fn)` synchronously calls `fn(inner_stub_db)`. Safe for `apply_route_specs`'s declarative transaction wrapper.
32
32
  - `stub_handler()` — fresh `Response('stub')`.
33
33
  - `stub_mw` — pass-through middleware (`async (_c, next) => next()`).
34
- - `stub_app_deps` — frozen `AppDeps`, every capability throwing, `audit` a no-op `AuditEmitter` from `create_test_audit_emitter`.
35
- - `create_stub_app_deps()` — factory: fresh `AppDeps` with no-op keyring/password/`delete_file`, a `read_secure_file` that throws ENOENT (the no-token-file state), a `create_noop_stub` DB, silent `Logger`, no-op `audit`.
34
+ - `stub_app_deps` — frozen `AppDeps`, every capability throwing, `audit` a no-op `AuditEmitter` from `create_test_audit_emitter`, `connection_closer` an empty `RealtimeCloser`.
35
+ - `create_stub_app_deps()` — factory: fresh `AppDeps` with no-op keyring/password/`delete_file`, a `read_secure_file` that throws ENOENT (the no-token-file state), a `create_noop_stub` DB, silent `Logger`, no-op `audit`, an empty `connection_closer`.
36
36
  - `create_test_audit_emitter()` — no-op `AuditEmitter`; `emit` / `emit_role_grant_target` no-op, `emit_pool` resolves immediately, `notify` no-op, `add_listener` throws (use `create_recording_audit_emitter` for a listener-accepting emitter), `listener_count` returns 0.
37
- - `create_stub_audit_sse()` — no-op `AuditLogSse` for surface-test wiring without booting real SSE. `subscribe` returns a no-op cleanup; `on_audit_event` no-op; `registry` is a fresh `SubscriberRegistry` (live `.size` / `.close_*` for registry-state tests, isolated per call). For real SSE plumbing build via `create_audit_log_sse` against `create_test_app`.
37
+ - `create_stub_audit_sse()` — no-op `AuditLogSse` for surface-test wiring without booting real SSE. `on_audit_event` no-op (nothing is broadcast); `registry` is a fresh `SubscriberRegistry` (live `.count` / `.close_*` for registry-state tests, isolated per call). For real SSE plumbing build via `create_audit_log_sse` against `create_test_app`.
38
38
  - `create_stub_api_middleware({include_daemon_token?})` — stub `MiddlewareSpec[]` matching `create_auth_middleware_specs`'s output (origin/session/request_context/bearer_auth, optional daemon_token) for surface generation without booting real auth. See `auth/CLAUDE.md` §Middleware for the real stack.
39
39
  - `create_stub_app_server_context(session_options)` — stub `AppServerContext`; rate limiters null, `bootstrap_status.available: false`, `app_settings.open_signup: false`.
40
40
  - `create_test_app_surface_spec(options)` — builds an `AppSurfaceSpec` mirroring `create_app_server`'s route assembly (consumer routes + stub middleware + surface generation). `CreateTestAppSurfaceSpecOptions`: `session_options`, `create_route_specs`, `env_schema?`, `event_specs?`, `rpc_endpoints?`, `ws_endpoints?`, `transform_middleware?`, `bootstrap?`. Bootstrap is opt-in (symmetric with `create_app_server` — omit to skip; pass the same value as prod to mount routes at `bootstrap.route_prefix ?? '/api/account'`). Single source of truth for attack-surface tests — track `create_app_server` wiring changes here.
@@ -100,16 +100,14 @@ mismatch.
100
100
  ### `audit_drift_guard.ts` — audit-emission validation
101
101
 
102
102
  - `install_audit_drift_guard()` — `beforeEach` resets + `afterEach` zero-checks `audit_metadata_validation_failures` + `audit_unknown_event_type_failures` counters from `auth/audit_log_queries.ts`. Call once at the top of any `describe_db` block firing audit emits — production validation is fail-open, so without this any regression shipping a typo'd `event_type` or undeclared metadata field is silent. Pair with `await_pending_effects: true` (the `create_test_app` default) so fire-and-forget audit writes complete by response time.
103
- - `create_emit_ordering_audit_factory<E>(seq_ref, events_ref, build_inner)` — returns an `AuditFactory` wrapping `build_inner({db, log})` so every `emit` pushes `{kind: 'emit', at: seq.value++}` into a shared sequence + events array. Pass through `create_test_app({audit_factory: …})` — the test backend invokes it with its `{db, log}` and lands the wrapped emitter on `deps.audit`. Generic `E extends {kind: string; at: number}` so the events array typechecks against the caller's own `close` / custom marker shape. Pair with `create_recording_closer(seq_ref)` for close-vs-emit ordering tests. Scope is `emit` only — `emit_role_grant_target`, `emit_pool`, `notify` forward to the inner emitter unwrapped.
104
- - `AuditEmitMarker` — `{kind: 'emit'; at: number}`, the marker type pushed.
105
103
  - `create_recording_audit_emitter(calls_ref?)` — no-op `AuditEmitter` pushing every `emit` and `emit_pool` call into `calls`. Pass `calls_ref` to write into a caller-owned array; omit to let the helper allocate. Returns `{emitter, calls}` — destructure `emitter` as the `audit` dep and read `calls` to assert. Replaces per-file capturing emitters previously duplicated across `password_change.test.ts`, `audit_log.test.ts`, etc.
106
104
  - `RecordingAuditEmitter` — `{emitter: AuditEmitter; calls: Array<AuditLogInput>}`.
107
105
 
108
106
  ### `connection_closer_helpers.ts` — `ConnectionCloser` test doubles
109
107
 
110
- - `create_recording_closer(seq_ref?)` — `{closer, calls}`; every method on `closer` records `{method, id, at}` into `calls`. Pass `seq_ref` to share the sequence counter with `create_emit_ordering_audit_factory` so close + emit markers compose for ordering tests.
111
- - `assert_close_call(call, method, id)` — pins `{method, id}` on a recorded close call without baking in the `at: N` sequence number. Use at every "did the closer fire?" site; reserve `at: N` assertions for the dedicated ordering test paired with the capture helper.
112
- - `RecordedClose` — `{method: 'session' | 'token' | 'account', id, at}`.
108
+ - `create_recording_closer()` — `{closer, calls}`; every method on `closer` records `{method, id}` into `calls` and closes nothing. To see a handler's closes, add it to the backend's closer (`ctx.deps.connection_closer.add(closer)`) or build the factory's deps with it. The closes are queued post-commit, so they have run by the time a `create_test_app` response is in hand (`await_pending_effects: true` flushes both queues).
109
+ - `assert_close_call(call, method, id)` — pins `{method, id}` on a recorded close call; handles the missing-element case.
110
+ - `RecordedClose` — `{method: 'session' | 'token' | 'account', id}`.
113
111
  - `RecordingCloser` — `{closer, calls}`.
114
112
 
115
113
  ## Database — `db.ts`
@@ -171,7 +169,7 @@ pre-keeper, lock unflipped), use `create_test_app_for_bootstrap` — pair with
171
169
  Types:
172
170
 
173
171
  - `TestAppServer extends AppBackend` — adds `account`, `actor`, `api_token`, `session_cookie`, `keyring`, `cleanup()`.
174
- - `TestAppServerOptions` — `session_options` (required), optional `db`, `db_type`, `migration_namespaces`, `password`, `username`, `password_value`, `roles`, `audit_factory`. `migration_namespaces` runs extra namespaces after auth in the auto-created PGlite (mirrors `create_app_backend`); mutually exclusive with `db` (caller-migrated) — passing both throws. The optional `audit_factory` defaults to `default_audit_factory` (no-listener `create_audit_emitter` over the test backend's `{db, log}`); pass a custom factory to compose `on_audit_event` / `audit_log_config`, wrap with `emit_decorator` (via `create_emit_ordering_audit_factory`), or otherwise replace the emitter. Mirrors `CreateAppBackendOptions` end-to-end — the previous `on_audit_event` / `audit_log_config` sugar was removed alongside the production rename.
172
+ - `TestAppServerOptions` — `session_options` (required), optional `db`, `db_type`, `migration_namespaces`, `password`, `username`, `password_value`, `roles`, `audit_factory`. `migration_namespaces` runs extra namespaces after auth in the auto-created PGlite (mirrors `create_app_backend`); mutually exclusive with `db` (caller-migrated) — passing both throws. The optional `audit_factory` defaults to `default_audit_factory` (no-listener `create_audit_emitter` over the test backend's `{db, log}`); pass a custom factory to compose `on_audit_event` / `audit_log_config`, instrument `emit` with `emit_decorator`, or otherwise replace the emitter. Mirrors `CreateAppBackendOptions` end-to-end — the previous `on_audit_event` / `audit_log_config` sugar was removed alongside the production rename.
175
173
  - `CreateTestAppOptions extends TestAppServerOptions` — adds `create_route_specs` (required), `rpc_endpoints?: RpcEndpointsSuiteOption` (top-level only — single source of truth, symmetric with the suite-level option), `bootstrap?: BootstrapServerOptions` (top-level only — same precedent as `rpc_endpoints`), and `app_options?: SuiteAppOptions` (`Partial<AppServerOptions>` excluding the five fields the helper manages: `backend`, `session_options`, `create_route_specs`, `rpc_endpoints`, `bootstrap`).
176
174
  - `TestAccount` — `{account, actor, session_cookie, api_token, create_session_headers, create_bearer_headers}`.
177
175
  - `TestApp` — `{app, backend, surface_spec, surface, route_specs, create_session_headers, create_bearer_headers, create_daemon_token_headers, create_account, cleanup}`.
@@ -186,6 +184,11 @@ state (no rotation, keeper already set),
186
184
  the response returns so tests can assert on side effects inline), and silent
187
185
  logger. Override via `app_options`.
188
186
 
187
+ `auth_cleanup` is left at its default, **off**, here and in
188
+ `create_test_app_for_bootstrap`: a background pass would delete rows and
189
+ write audit rows mid-test. A suite that opts in through `app_options` has the
190
+ schedule stopped by `cleanup()`, which is the assembled server's `close`.
191
+
189
192
  A fresh Hono app is created on every call because middleware closures bind
190
193
  to the server's deps (db, keyring). Hono assembly is cheap (~10–50ms);
191
194
  PGlite WASM caching in `db.ts` is where the real savings are.
@@ -515,13 +518,14 @@ bootstrapped keeper) based on `method.auth`; keeper uses the daemon token.
515
518
  Per SSE route: open stream with matching auth, assert the
516
519
  `SSE_CONNECTED_COMMENT` comment, fire a consumer-supplied `trigger()`,
517
520
  validate the next `data:` frame as `{method, params}` against declared
518
- `EventSpec`s, then (by default) fire `POST /api/account/sessions/revoke-all`
519
- and assert the stream closes within 2s.
521
+ `EventSpec`s, then (by default) dispatch the `account_session_revoke_all` RPC
522
+ method and assert the stream closes within 2s.
520
523
 
521
524
  `SseRouteTestSpec` per route: `{path, trigger, event_specs?, assert_closes_on_revoke?}`.
522
- Pass `on_audit_event` on the suite options to wire a close-on-revoke guard
523
- (e.g. via `create_sse_auth_guard`) for consumer SSE registries — without it,
524
- the revoke assertion hangs because the guard never fires.
525
+ A consumer registry closes on the revoke when its route joined it to the
526
+ backend's closer (`ctx.deps.connection_closer.add(create_sse_connection_closer(registry))`)
527
+ or when the suite's `on_audit_event` option wires a guard (e.g. `create_sse_auth_guard`); with
528
+ neither, the revoke assertion fails.
525
529
 
526
530
  Frame reading is delegated to the shared `create_sse_frame_reader`
527
531
  (`transports/sse_frame_reader.ts`) — `\n\n` framing, a 2s per-read timeout
@@ -539,12 +543,12 @@ skipped (the Node test runtime has no `@hono/node-ws` adapter).
539
543
 
540
544
  Three layers:
541
545
 
542
- 1. **Primitives** — `create_fake_ws()`, `create_fake_hono_context(opts)`, `create_stub_upgrade()`, `MinimalActionEnvironment`, `dispatch_ws_message(on_message, event, ws)`.
543
- 2. **Harness** — `create_ws_test_harness({actions, transport?, heartbeat?, log?, on_socket_open?, on_socket_close?})` → `WsTestHarness`. `connect(identity?)` is async and resolves after `on_socket_open` completes, so broadcasts sent immediately after `await harness.connect()` reach the client. The harness threads its own `create_stub_db()` into the dispatcher's `db` slot so handlers declaring `side_effects: true` execute under the same transaction wrap they would in production (the stub's `transaction(fn)` synchronously calls `fn(stub_db)`); domain deps reach handlers via factory closures, the same way HTTP RPC factories already wire them. Audit fan-out runs through whatever `audit` emitter the consumer supplied to its action factory closure (typically `create_test_audit_emitter()` for unit harnesses).
546
+ 1. **Primitives** — `create_fake_ws()`, `create_fake_hono_context(opts)`, `create_stub_upgrade()`, `MinimalActionEnvironment`, `dispatch_ws_message(on_message, event, ws)`. The fake context sets the keys the production auth middleware sets — `AUTH_SESSION_TOKEN_HASH_KEY` included, as `hash_session_token(auth_session_id)` — because the dispatcher reads the credential through `get_resolved_auth(c)`; a hand-built upgrade context must do the same.
547
+ 2. **Harness** — `create_ws_test_harness({actions, transport?, heartbeat?, log?, on_socket_open?, on_socket_close?})` → `WsTestHarness`. `connect(identity?)` is async and resolves after the connection is admitted and `on_socket_open` completes, so broadcasts sent immediately after `await harness.connect()` reach the client. The harness pre-bakes its `RequestContext` (`TEST_CONTEXT_PRESET_KEY`), which also skips the admission credential re-read — there are no session or token rows behind a harness identity — so tests of the re-read itself drive `register_action_ws` with a real database and no preset (`src/test/actions/register_action_ws.admission.db.test.ts`). The harness threads its own `create_stub_db()` into the dispatcher's `db` slot so handlers declaring `side_effects: true` execute under the same transaction wrap they would in production (the stub's `transaction(fn)` synchronously calls `fn(stub_db)`); domain deps reach handlers via factory closures, the same way HTTP RPC factories already wire them. Audit fan-out runs through whatever `audit` emitter the consumer supplied to its action factory closure (typically `create_test_audit_emitter()` for unit harnesses).
544
548
  3. **Round-trip helpers** — predicates + wire-frame types live in `transports/ws_client.ts` (shared with the cross-process `ws_transport.ts` impl): `is_notification(method)`, `is_notification_with<P>(method, match)` (type-guard combinator — narrows `wait_for` return type), `is_response_for(id)`, `JsonrpcNotificationFrame<P>` / `JsonrpcSuccessResponseFrame<R>` / `JsonrpcErrorResponseFrame<D>` (typed wire-frame shapes distinct from the runtime Zod schemas in `http/jsonrpc.ts` — generic over `params` / `result` / `data` so tests narrow without casts). `build_broadcast_api<TApi>({harness, specs})` (in `ws_round_trip.ts`) wires a typed broadcast API against the harness transport.
545
549
 
546
550
  `WsClient` (in `transports/ws_client.ts`):
547
- `{send, request<R>, close, messages, wait_for, wait_for_close, close_code}`.
551
+ `{send, request<R>, close, messages, wait_for, wait_for_close, close_code, close_reason}`.
548
552
  The harness's `connect()` returns this shape; the cross-process
549
553
  `create_ws_transport` in `transports/ws_transport.ts` implements the same
550
554
  interface so assertion helpers and suite bodies work against either impl.
@@ -553,7 +557,9 @@ socket within the timeout, `false` on timeout (and `true` immediately when
553
557
  already closed) — the signal for server-initiated close (e.g. an auth-guard
554
558
  revocation), distinct from client-initiated `close()`. Mirrors the SSE frame
555
559
  reader's `wait_for_close`. `close_code` is the connection's close code once
556
- closed (`1006` for a drop without a close frame), `null` while open.
560
+ closed (`1006` for a drop without a close frame), `null` while open;
561
+ `close_reason` is the text sent with it (empty when the close carried none),
562
+ `null` while open.
557
563
  `request` throws with code + message + data on error frames (so asserting `result.foo` on a
558
564
  failed request surfaces the real cause, not a `Cannot read property 'foo'
559
565
  of undefined`). `wait_for(predicate, timeout_ms?)` checks already-received
@@ -915,9 +921,10 @@ source of truth for wire-shape conformance.
915
921
  - `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
916
922
  (the **gating** flags `ws` / `sse` / `cell_crud` / `cell_relations` /
917
923
  `cell_gated_create` / `account_lifecycle` / `fact_serving` / `ready` /
918
- `account_status` / `oversized_reject_closes_connection` / `peer_request` —
924
+ `account_status` / `oversized_reject_closes_connection` / `peer_request` /
925
+ `ws_handshake_pipelining` / `ws_account_actions` / `ws_self_revocation_reply` —
919
926
  each has a `test_if`
920
- reader; `peer_request` (server-initiated requests — the ActionPeer
927
+ reader (`ws_self_revocation_reply` forks one case's assertion instead); `peer_request` (server-initiated requests — the ActionPeer
921
928
  `peer/ping` round-trip) is `true` for both the Rust spine and the TS spine
922
929
  (`BackendWebsocketTransport.request_connection`); `ts_default_capabilities`
923
930
  keeps it `false` like sse/ready until a backend wires the `peer/ping` HTTP +
@@ -962,6 +969,17 @@ source of truth for wire-shape conformance.
962
969
  smuggling probe (`true` Node/Deno/Rust — they close on an oversized reject;
963
970
  `false` Bun — `Bun.serve` drains + keepalives but frames correctly, so the
964
971
  suite's no-desync half still runs).
972
+ `ws_handshake_pipelining` gates the WS round-trip suite's
973
+ frames-with-the-handshake case (`true` Node/Deno/Rust — frames written in the
974
+ same TCP write as the upgrade request reach the socket; `false` Bun, whose
975
+ HTTP parser answers such a request `400`, and `false` in-process, where there
976
+ is no socket to write raw bytes to).
977
+ `ws_account_actions` gates the WS round-trip suite's self-revocation case
978
+ (the WS endpoint mounts the account actions — `true` on fuz_app's own spine
979
+ presets, `false` in the family defaults), and `ws_self_revocation_reply`
980
+ says what that case requires: the reply ahead of the close (`true`, the TS
981
+ family) or the close alone (`false`, the Rust family, whose socket loop
982
+ drops the response once the connection is closed — a recorded divergence).
965
983
 
966
984
  ### `cross_backend/standard.ts` — `describe_standard_cross_process_tests`
967
985
 
@@ -1084,20 +1102,32 @@ base_url, ws_path, origin?, rpc_path?, max_message_bytes?})` opens a live
1084
1102
  `WebSocket` via `create_ws_transport` (the `ws` npm package) and asserts
1085
1103
  cases against the upgrade stack `register_ws_endpoint` wires (origin →
1086
1104
  `require_auth` → dispatch): authed upgrade round-trips `heartbeat`,
1105
+ requests sent the moment the socket opens are all answered (both spines admit a
1106
+ connection only after re-reading its credential; frames that arrive first wait
1107
+ for that, they are not dropped) — and, gated on
1108
+ `capabilities.ws_handshake_pipelining`, the same with the ordering forced:
1109
+ frames written in the same TCP write as the upgrade request (`connect_raw_ws`)
1110
+ are all answered,
1087
1111
  `heartbeat` admits only the parameterless shapes (a declared param →
1088
1112
  `invalid_params`), a message past `max_message_bytes` (default
1089
1113
  `DEFAULT_WS_MAX_MESSAGE_BYTES`) closes with `WS_CLOSE_MESSAGE_TOO_BIG`,
1090
1114
  anonymous upgrade refused, disallowed-origin upgrade refused, and — gated on
1091
1115
  `rpc_path` — a live socket drops when the account's sessions are revoked
1092
- mid-connection (`account_session_revoke_all` over the keeper session channel
1093
- emits `session_revoke_all`, which `create_ws_auth_guard` closes on; asserted
1094
- via `WsClient.wait_for_close`). Per-connection auth is enforced **at upgrade
1116
+ mid-connection (`account_session_revoke_all` over the keeper session channel;
1117
+ asserted via `WsClient.wait_for_close`) and when its account is deleted
1118
+ (`account_delete` by the keeper, also gated on
1119
+ `capabilities.account_lifecycle`; the close code is
1120
+ `WS_CLOSE_SESSION_REVOKED`). Per-connection auth is enforced **at upgrade
1095
1121
  time**, so the negative upgrade cases assert the upgrade itself rejects, not
1096
- a per-message error; the close-on-revoke case proves the audit-fed guard is
1097
- the revocation seam for an already-open socket, since per-message dispatch
1098
- never re-checks credential validity. Omit `rpc_path` to skip the close case
1099
- (consumers without the standard account actions on their RPC endpoint).
1100
- **Consumer-agnostic** — it drives only the `heartbeat` protocol action
1122
+ a per-message error; the close cases prove a revocation closes an
1123
+ already-open socket, since per-message dispatch never re-checks credential
1124
+ validity. Omit `rpc_path` to skip the close cases (consumers without the
1125
+ standard account and admin actions on their RPC endpoint). One case is gated
1126
+ on `capabilities.ws_account_actions` (the WS endpoint mounts the account
1127
+ actions): a socket that revokes its own sessions over that socket is closed
1128
+ with `WS_CLOSE_SESSION_REVOKED`, and — where the backend declares
1129
+ `capabilities.ws_self_revocation_reply` — reads the reply first.
1130
+ Otherwise **consumer-agnostic** — it drives only the `heartbeat` protocol action
1101
1131
  (guaranteed on every WS endpoint by `assert_ws_endpoints_include_protocol_actions`),
1102
1132
  so it validates the transport without touching domain WS methods. Gated on
1103
1133
  `capabilities.ws`; cross-process only (needs a real bound socket — wire from
@@ -1114,7 +1144,9 @@ the seven server-initiated notifications (`received` → recipient,
1114
1144
  superseded sibling's grantor on **both** the accept- and revoke-cascade paths).
1115
1145
  `describe_role_grant_offer_notification_ws_tests({setup_test, capabilities,
1116
1146
  base_url, ws_path})` opens the affected counterparty's socket
1117
- (`create_ws_transport`), drives the lifecycle RPC over `fixture.transport`, then
1147
+ (`create_admitted_ws_transport` — a targeted notification reaches only an
1148
+ admitted connection, and the handshake alone doesn't say it is one), drives the
1149
+ lifecycle RPC over `fixture.transport`, then
1118
1150
  strict-parses the delivered frame against its canonical params schema from
1119
1151
  `auth/role_grant_offer_notifications.ts` (the guard against serialization drift —
1120
1152
  field / null / datetime / the flat revoke shape / the supersede `reason` +
@@ -1159,6 +1191,48 @@ action (manifest-excluded), so parity is behavioral here, not via the manifest
1159
1191
  gate. Cross-process only; fuz_app's own wiring is
1160
1192
  `src/test/cross_backend/peer_ping_ws.cross.test.ts`.
1161
1193
 
1194
+ ### `cross_backend/ws_connection_cap.ts` — `describe_ws_connection_cap_cross_tests`
1195
+
1196
+ Real-upgrade parity for the **per-account WebSocket connection cap** — the
1197
+ evict-oldest policy both spines apply (`BackendWebsocketTransport`'s
1198
+ `max_connections_per_account`, the Rust `fuz_realtime` `ConnectionRegistry`).
1199
+ The close code, its reason, and which socket closes are wire contract — a
1200
+ client reads `WS_CLOSE_CONNECTION_LIMIT` as "superseded, don't reconnect" — so
1201
+ this is the pin that runs on both.
1202
+ `describe_ws_connection_cap_cross_tests({setup_test, capabilities, base_url,
1203
+ ws_path, origin?, max_connections_per_account?})` opens every socket with the
1204
+ fresh-per-test keeper's session cookies (one account), each answering a
1205
+ `heartbeat` before the next opens (`create_admitted_ws_transport`) so the
1206
+ backend registered them in order and
1207
+ "oldest" names the same socket on both spines. Cases: one connection past the
1208
+ cap closes exactly the oldest with `WS_CLOSE_CONNECTION_LIMIT` + reason
1209
+ `connection limit` while every other socket, the newest included, keeps
1210
+ dispatching — and the session then upgrades again (a revoked session couldn't),
1211
+ superseding the next-oldest; and a socket held open while a cap's worth of
1212
+ others open and close in sequence is never evicted (a backend that leaks closed
1213
+ connections in its registry counts them toward the cap, which is what this
1214
+ catches). `max_connections_per_account` defaults to
1215
+ `DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT`, the cap both spines apply unless
1216
+ overridden; pass the consumer's value when it sets one, `null` (cap disabled)
1217
+ to skip. **Consumer-agnostic** like the WS round-trip suite (`heartbeat` only).
1218
+ A separate call rather than more cases on that suite because each case opens
1219
+ more than a cap's worth of real sockets. Gated on `capabilities.ws`;
1220
+ cross-process only. fuz_app's own wiring is
1221
+ `src/test/cross_backend/ws_connection_cap.cross.test.ts`.
1222
+
1223
+ ### `cross_backend/session_cap.ts` — `describe_session_cap_cross_tests`
1224
+
1225
+ Parity for the per-account session cap over real HTTP: logging in past
1226
+ `max_sessions` (default `DEFAULT_MAX_SESSIONS`) never refuses a login and
1227
+ evicts the oldest session.
1228
+ `describe_session_cap_cross_tests({setup_test, max_sessions?, login_path?,
1229
+ status_path?, ws?})` — pass `ws` (`{base_url, ws_path, origin?}`) to also
1230
+ assert that the evicted session's socket is closed with
1231
+ `WS_CLOSE_SESSION_REVOKED` once the evicting login commits; omitted, that case
1232
+ is skipped, so a caller passes it only for a backend with `capabilities.ws`.
1233
+ Cross-process only. fuz_app's own wiring is
1234
+ `src/test/cross_backend/session_cap.cross.test.ts`.
1235
+
1162
1236
  ### `cross_backend/sse_round_trip.ts` — `describe_cross_process_sse_tests`
1163
1237
 
1164
1238
  Cross-process counterpart to the in-process `sse_round_trip.ts` harness —
@@ -1167,23 +1241,26 @@ endpoint via `create_sse_transport` (built-in `fetch` + `TextDecoder`, no
1167
1241
  dep), threading the fresh-per-test keeper's session cookie. Kept a separate
1168
1242
  call (not folded into `describe_standard_cross_process_tests`) for the same
1169
1243
  reason the WS suite is — it needs raw `base_url` / `sse_path` the standard
1170
- bundle doesn't carry. Up to three cases, mirroring the in-process audit-log
1171
- self-test: the stream emits the `: connected` comment; a minted secondary's
1172
- sessions are revoked over the keeper's admin channel (`admin_session_revoke_all`),
1244
+ bundle doesn't carry. The cases mirror the in-process audit-log self-test:
1245
+ the stream emits the `: connected` comment; a minted secondary's sessions are
1246
+ revoked over the keeper's admin channel (`admin_session_revoke_all`),
1173
1247
  broadcasting one `session_revoke_all` audit `data:` frame **without** closing
1174
1248
  the keeper's stream (target ≠ subscriber — secondary minted before the stream
1175
- opens so its `create_account` audit events stay off it); and the subscriber's
1176
- _own_ sessions are revoked (`account_session_revoke_all`) so the audit guard
1177
- drops the live stream (asserted via `SseTransport.wait_for_close`). The
1178
- data-frame + close cases gate on `rpc_path` (they drive the standard
1179
- account/admin actions); all cases gate on `capabilities.sse`. Cross-process
1180
- only — wire from a `*.cross.test.ts`. fuz*app's own wiring is
1181
- `src/test/cross_backend/sse.cross.test.ts`; only the TS spines advertise
1182
- `sse` (they wire `audit_log_sse`), so the Rust `spine_stub` cases `.skip`.
1183
- That file also registers one `xfail_until` (only when `sse: false`) asserting
1184
- the stream \_can't* open on a spine without SSE — a self-cleaning tripwire for
1185
- the spine that should grow it, distinct from the consumer-legit capability
1186
- skip the shared suite emits.
1249
+ opens so its `create_account` audit events stay off it); the subscriber's
1250
+ _own_ sessions are revoked, all of them (`account_session_revoke_all`, the
1251
+ account-wide close) and then just the one (`account_session_revoke`, the
1252
+ session-scoped close), each dropping the live stream (asserted via
1253
+ `SseTransport.wait_for_close`); the keeper soft-deletes a second admin whose
1254
+ stream is open (`account_delete`, also gated on
1255
+ `capabilities.account_lifecycle`), dropping it; and one stream past the per-session cap
1256
+ (`max_per_scope`, default `AUDIT_LOG_SSE_MAX_PER_SCOPE`; `null` skips) on one
1257
+ session closes the oldest and leaves the rest open. The data-frame + close
1258
+ cases gate on `rpc_path` (they drive the standard account/admin actions); all
1259
+ cases gate on `capabilities.sse`. Cross-process only — wire from a
1260
+ `*.cross.test.ts`. fuz_app's own wiring is
1261
+ `src/test/cross_backend/sse.cross.test.ts`, which runs on every backend — the
1262
+ TS spines wire `audit_log_sse` and the Rust `spine_stub` serves the same
1263
+ stream.
1187
1264
 
1188
1265
  ### `cross_backend/cell_crud.ts` + `cell_relations.ts` + `cell_gated_create.ts` — cell parity suites
1189
1266
 
@@ -1292,7 +1369,8 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
1292
1369
  transport carries the keeper session cookie in its jar after this call
1293
1370
  resolves.
1294
1371
  - `testing/transports/ws_client.ts` — shared `WsClient` interface (`send` /
1295
- `request` / `close` / `messages` / `wait_for` / `wait_for_close` / `close_code`),
1372
+ `request` / `close` / `messages` / `wait_for` / `wait_for_close` / `close_code` /
1373
+ `close_reason`),
1296
1374
  wire-frame types, and
1297
1375
  predicates (`is_notification`, `is_response_for`, ...). Both in-process
1298
1376
  (`ws_round_trip.ts`) and cross-process (`ws_transport.ts`) impls satisfy
@@ -1309,6 +1387,20 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
1309
1387
  Threads the keeper cookie onto the upgrade so per-action auth succeeds on
1310
1388
  the first message. `on_request` (attached at construction, before the upgrade
1311
1389
  completes) answers server-initiated requests via the shared `deliver_inbound`.
1390
+ An open socket is not yet an *admitted* connection — both spines answer the
1391
+ handshake, then re-read the credential, then admit, and deliver no
1392
+ server-initiated message until then. `create_admitted_ws_transport(options)`
1393
+ opens and round-trips `heartbeat` (response id `WS_ADMISSION_PROBE_ID`) so the
1394
+ connection is admitted when it resolves; suites that wait for a push, or need
1395
+ connections registered in a known order, open through it.
1396
+ - `testing/transports/ws_raw_client.ts` — `connect_raw_ws({port, hostname?, path, headers?, pipelined?})`
1397
+ → `RawWsClient` (`{frames, send_text, wait_for, destroy}`), a WebSocket client
1398
+ over a raw `node:net` socket for the two things a client library can't do:
1399
+ **withhold the close frame** (it never answers one) and put frames in the
1400
+ server's hands **with the handshake** (`pipelined` — written in the same TCP
1401
+ write as the upgrade request, so before the connection can have been
1402
+ admitted). Small unfragmented text frames only. Helpers `is_raw_ws_close`,
1403
+ `raw_ws_close_code`, `RAW_WS_OPCODE_TEXT` / `_CLOSE`.
1312
1404
  - `testing/transports/sse_frame_reader.ts` — `create_sse_frame_reader(reader, default_timeout_ms?)`,
1313
1405
  the transport-agnostic SSE framing core over a
1314
1406
  `ReadableStreamDefaultReader<Uint8Array>`: `\n\n` framing, per-read timeout,
@@ -1658,7 +1750,7 @@ re-roll the serve / daemon-info / WS-attach / drain boilerplate:
1658
1750
  - `testing/cross_backend/full_spine_mount.ts` — `build_full_spine_rpc_actions(deps, options)` / `full_spine_rpc_endpoints(ctx, options)` — the **full** live RPC mount: the declared bundle **plus** the off-declared-surface families the binary live-mounts (`_testing_*` backdoors, the cell verb set, the opt-in `actor_lookup` / `actor_search` resolvers). Single-sources what was an inline assembly in `testing_spine_server.ts`, so the binary and the `spine_method_coverage` reconciliation test build the same list. Also `$lib`-free.
1659
1751
  - `testing/cross_backend/ts_spine_backend_config.ts` — `ts_spine_node_backend_config()` / `ts_spine_deno_backend_config()` / `ts_spine_bun_backend_config()` presets (in-memory PGlite, no external infra), the TS analog of `rust_spine_stub_backend_config()`.
1660
1752
 
1661
- fuz_app's own binary wiring (`src/test/cross_backend/testing_spine_server{,_node,_deno,_bun}.ts`) is the worked example: ~one `build_app` over `create_app_backend` + `create_app_server` + `full_spine_rpc_endpoints` + a WS mount, reusing `default_spine_surface`. The `_node`/`_deno`/`_bun` entries differ only in which adapter they wire — `build_spine_app` is runtime-agnostic.
1753
+ fuz_app's own binary wiring (`src/test/cross_backend/testing_spine_server{,_node,_deno,_bun}.ts`) is the worked example: ~one `build_app` over `create_app_backend` + `create_app_server` + `full_spine_rpc_endpoints` + a WS mount, reusing `default_spine_surface`. The `_node`/`_deno`/`_bun` entries differ only in which adapter they wire — `build_spine_app` is runtime-agnostic. It leaves `create_app_server`'s `auth_cleanup` off, as the Rust `testing_spine_stub` leaves its twin unscheduled: a background pass would delete rows and write audit rows under a running suite. A consumer's cross-process test binary may schedule it when nothing its suites seed is already expired (the conformance table's `expired_session` principal seeds a backdated session row); an in-process harness leaves it off.
1662
1754
 
1663
1755
  ### Live-method coverage reconciliation — `method_coverage.ts`
1664
1756
 
@@ -1687,8 +1779,8 @@ no-DB reconciliation test `spine_method_coverage.test.ts`, which enumerates
1687
1779
  The in-process `ws_round_trip` harness stays (it drives the dispatcher
1688
1780
  against a fake upgrade, no wire), but the real-upgrade coverage now lives in
1689
1781
  the cross-process `cross_backend/ws_round_trip.ts` suite below — including
1690
- close-on-revoke (`WsClient.wait_for_close` asserts the audit-guard drops a
1691
- live socket on `session_revoke_all`).
1782
+ close-on-revoke (`WsClient.wait_for_close` asserts a live socket is dropped
1783
+ once `account_session_revoke_all` commits).
1692
1784
 
1693
1785
  `audit_completeness` is in-process by design (FK-structural introspection
1694
1786
  beyond the `audit_log_list` RPC reads — structurally in-process).