@fuzdev/fuz_app 0.125.0 → 0.127.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 (94) hide show
  1. package/dist/actions/CLAUDE.md +56 -20
  2. package/dist/actions/action_rpc.d.ts +14 -11
  3. package/dist/actions/action_rpc.d.ts.map +1 -1
  4. package/dist/actions/action_rpc.js +5 -4
  5. package/dist/actions/connection_closer.d.ts +13 -3
  6. package/dist/actions/connection_closer.d.ts.map +1 -1
  7. package/dist/actions/connection_closer.js +7 -5
  8. package/dist/actions/register_action_ws.d.ts +18 -13
  9. package/dist/actions/register_action_ws.d.ts.map +1 -1
  10. package/dist/actions/register_action_ws.js +8 -6
  11. package/dist/actions/register_ws_endpoint.d.ts +6 -3
  12. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  13. package/dist/actions/register_ws_endpoint.js +6 -3
  14. package/dist/actions/transports.d.ts +9 -0
  15. package/dist/actions/transports.d.ts.map +1 -1
  16. package/dist/actions/transports.js +9 -0
  17. package/dist/actions/transports_ws_backend.d.ts +15 -0
  18. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  19. package/dist/actions/transports_ws_backend.js +25 -5
  20. package/dist/actions/ws_endpoint_spec.d.ts +24 -4
  21. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  22. package/dist/actions/ws_endpoint_spec.js +15 -1
  23. package/dist/auth/middleware.d.ts +29 -1
  24. package/dist/auth/middleware.d.ts.map +1 -1
  25. package/dist/auth/middleware.js +35 -1
  26. package/dist/http/CLAUDE.md +3 -3
  27. package/dist/http/surface.d.ts +12 -9
  28. package/dist/http/surface.d.ts.map +1 -1
  29. package/dist/rate_limiter.d.ts +43 -3
  30. package/dist/rate_limiter.d.ts.map +1 -1
  31. package/dist/rate_limiter.js +27 -3
  32. package/dist/realtime/sse_auth_guard.d.ts +2 -1
  33. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  34. package/dist/realtime/sse_auth_guard.js +4 -2
  35. package/dist/realtime/subscriber_registry.d.ts +12 -0
  36. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  37. package/dist/realtime/subscriber_registry.js +18 -2
  38. package/dist/server/app_server.d.ts +128 -55
  39. package/dist/server/app_server.d.ts.map +1 -1
  40. package/dist/server/app_server.js +110 -61
  41. package/dist/server/app_server_context.d.ts +17 -9
  42. package/dist/server/app_server_context.d.ts.map +1 -1
  43. package/dist/testing/CLAUDE.md +83 -25
  44. package/dist/testing/app_server.d.ts +6 -3
  45. package/dist/testing/app_server.d.ts.map +1 -1
  46. package/dist/testing/app_server.js +20 -20
  47. package/dist/testing/connection_closer_helpers.d.ts +10 -4
  48. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  49. package/dist/testing/connection_closer_helpers.js +4 -0
  50. package/dist/testing/cross_backend/capabilities.d.ts +3 -3
  51. package/dist/testing/cross_backend/default_backend_configs.d.ts +12 -12
  52. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  53. package/dist/testing/cross_backend/default_backend_configs.js +11 -10
  54. package/dist/testing/cross_backend/full_spine_mount.d.ts +35 -11
  55. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  56. package/dist/testing/cross_backend/full_spine_mount.js +26 -9
  57. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +16 -1
  58. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  59. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +5 -4
  60. package/dist/testing/cross_backend/spine_surface_constants.d.ts +43 -3
  61. package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
  62. package/dist/testing/cross_backend/spine_surface_constants.js +43 -3
  63. package/dist/testing/cross_backend/testing_backdoor.d.ts +15 -1
  64. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  65. package/dist/testing/cross_backend/testing_backdoor.js +51 -6
  66. package/dist/testing/cross_backend/testing_reset_actions.d.ts +6 -3
  67. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  68. package/dist/testing/cross_backend/testing_reset_actions.js +6 -3
  69. package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -1
  70. package/dist/testing/cross_backend/testing_server_bun.js +5 -1
  71. package/dist/testing/cross_backend/testing_server_core.d.ts +72 -41
  72. package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -1
  73. package/dist/testing/cross_backend/testing_server_core.js +22 -14
  74. package/dist/testing/cross_backend/testing_server_deno.js +1 -1
  75. package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -1
  76. package/dist/testing/cross_backend/testing_server_node.js +20 -7
  77. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +15 -2
  78. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  79. package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -8
  80. package/dist/testing/cross_backend/ws_action_rate_limit.d.ts +26 -0
  81. package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -0
  82. package/dist/testing/cross_backend/ws_action_rate_limit.js +261 -0
  83. package/dist/testing/middleware.d.ts.map +1 -1
  84. package/dist/testing/middleware.js +4 -3
  85. package/dist/testing/stubs.d.ts +17 -4
  86. package/dist/testing/stubs.d.ts.map +1 -1
  87. package/dist/testing/stubs.js +46 -13
  88. package/dist/testing/transports/ws_transport.d.ts +7 -0
  89. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  90. package/dist/testing/transports/ws_transport.js +1 -1
  91. package/dist/testing/ws_round_trip.d.ts +43 -37
  92. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  93. package/dist/testing/ws_round_trip.js +28 -46
  94. package/package.json +2 -2
@@ -14,7 +14,7 @@ import { type SessionOptions } from '../auth/session_cookie.js';
14
14
  import type { BootstrapAccountSuccess } from '../auth/bootstrap_account.js';
15
15
  import type { EventSpec } from '../realtime/sse.js';
16
16
  import { type AuditLogSse } from '../realtime/sse_auth_guard.js';
17
- import { type RateLimiter } from '../rate_limiter.js';
17
+ import { RateLimiter, type RateLimiterMode } from '../rate_limiter.js';
18
18
  import type { DaemonTokenState } from '../auth/daemon_token.js';
19
19
  import type { MigrationResult } from '../db/migrate.js';
20
20
  import type { AppBackend } from './app_backend.js';
@@ -25,7 +25,7 @@ import { type AppSurfaceSpec, type RpcEndpointSpec } from '../http/surface.js';
25
25
  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
- import type { WsEndpointSpec } from '../actions/ws_endpoint_spec.js';
28
+ import { type WsEndpointSpec } from '../actions/ws_endpoint_spec.js';
29
29
  import type { BackendWebsocketTransport } from '../actions/transports_ws_backend.js';
30
30
  import { type AuthCleanupScheduleOptions } from '../auth/cleanup.js';
31
31
  /**
@@ -95,10 +95,21 @@ export interface AppServerOptions {
95
95
  trusted_proxies: Array<string>;
96
96
  get_connection_ip: (c: Context) => string | undefined;
97
97
  };
98
+ /**
99
+ * What an omitted rate limiter becomes — `'enforced'` (the default) builds
100
+ * a default instance for each `*_rate_limiter` option left out,
101
+ * `'disabled_for_testing'` leaves each of them `null`. An explicit option
102
+ * (an instance or `null`) always wins over the mode. The disabled mode adds
103
+ * one surface warning, and prints a stderr banner once per module instance
104
+ * (`announce_rate_limiters_disabled`). Never `'disabled_for_testing'` in production — see
105
+ * `RateLimiterMode`.
106
+ */
107
+ rate_limiters?: RateLimiterMode;
98
108
  /**
99
109
  * Per-IP rate limiter for login + password change — the distributed-spray
100
110
  * backstop. Omit or `undefined` to use a default limiter (5 attempts per
101
- * 15 minutes). Pass `null` to explicitly disable rate limiting.
111
+ * 15 minutes) under `rate_limiters: 'enforced'`, none under
112
+ * `'disabled_for_testing'`. Pass `null` to explicitly disable rate limiting.
102
113
  * Also available on `AppServerContext` for route factory callbacks.
103
114
  *
104
115
  * One instance per auth surface, not one shared across all four. These
@@ -110,32 +121,34 @@ export interface AppServerOptions {
110
121
  */
111
122
  login_ip_rate_limiter?: RateLimiter | null;
112
123
  /**
113
- * Per-IP rate limiter for signup. Omit or `undefined` to use a default
114
- * limiter (5 attempts per 15 minutes). Pass `null` to explicitly disable.
124
+ * Per-IP rate limiter for signup. Omit or `undefined` for the mode's
125
+ * default (5 attempts per 15 minutes under `rate_limiters: 'enforced'`).
126
+ * Pass `null` to explicitly disable.
115
127
  * Separate from `login_ip_rate_limiter` because an open-signup deployment
116
128
  * lets any unauthenticated caller spend it. Also available on
117
129
  * `AppServerContext` for route factory callbacks.
118
130
  */
119
131
  signup_ip_rate_limiter?: RateLimiter | null;
120
132
  /**
121
- * Per-IP rate limiter for bootstrap. Omit or `undefined` to use a default
122
- * limiter (5 attempts per 15 minutes). Pass `null` to explicitly disable.
133
+ * Per-IP rate limiter for bootstrap. Omit or `undefined` for the mode's
134
+ * default (5 attempts per 15 minutes under `rate_limiters: 'enforced'`).
135
+ * Pass `null` to explicitly disable.
123
136
  * Separate from `login_ip_rate_limiter` so a fumbled bootstrap token can't
124
137
  * spend the operator's login budget. Wired directly into the factory-managed
125
138
  * bootstrap route; also on `AppServerContext` for symmetry.
126
139
  */
127
140
  bootstrap_ip_rate_limiter?: RateLimiter | null;
128
141
  /**
129
- * Per-account rate limiter for login attempts.
130
- * Omit or `undefined` to use a default limiter (10 attempts per 30 minutes).
131
- * Pass `null` to explicitly disable rate limiting.
142
+ * Per-account rate limiter for login attempts. Omit or `undefined` for the
143
+ * mode's default (10 attempts per 30 minutes under
144
+ * `rate_limiters: 'enforced'`). Pass `null` to explicitly disable.
132
145
  * Also available on `AppServerContext` for route factory callbacks.
133
146
  */
134
147
  login_account_rate_limiter?: RateLimiter | null;
135
148
  /**
136
149
  * Per-account rate limiter for signup attempts, keyed by submitted username.
137
- * Omit or `undefined` to use a default limiter (10 attempts per 30 minutes).
138
- * Pass `null` to explicitly disable rate limiting.
150
+ * Omit or `undefined` for the mode's default (10 attempts per 30 minutes
151
+ * under `rate_limiters: 'enforced'`). Pass `null` to explicitly disable.
139
152
  * Also available on `AppServerContext` for route factory callbacks.
140
153
  */
141
154
  signup_account_rate_limiter?: RateLimiter | null;
@@ -143,20 +156,23 @@ export interface AppServerOptions {
143
156
  * Per-IP rate limiter for the action dispatchers (HTTP RPC + WebSocket).
144
157
  * Consulted for actions whose spec declares `rate_limit: 'ip'` or `'both'`.
145
158
  * Same limiter applies across transports — one budget per action.
146
- * Omit or `undefined` to use a default limiter (600 attempts per
147
- * 15 minutes — permissive). Pass `null` to explicitly disable.
148
- * Also available on `AppServerContext` for consumers wiring
149
- * `register_action_ws`.
159
+ * Omit or `undefined` for the mode's default (600 attempts per 15 minutes
160
+ * — permissive — under `rate_limiters: 'enforced'`). Pass `null` to
161
+ * explicitly disable.
162
+ * Threaded into every endpoint mounted from `rpc_endpoints` and
163
+ * `ws_endpoints`; also available on `AppServerContext`.
150
164
  */
151
165
  action_ip_rate_limiter?: RateLimiter | null;
152
166
  /**
153
- * Per-actor rate limiter for the action dispatchers (HTTP RPC + WebSocket).
154
- * Consulted for actions whose spec declares `rate_limit: 'account'` or
155
- * `'both'`. Keyed on `request_context.actor.id` (post-auth).
156
- * Omit or `undefined` to use a default limiter (1200 attempts per
157
- * 15 minutes — permissive). Pass `null` to explicitly disable.
158
- * Also available on `AppServerContext` for consumers wiring
159
- * `register_action_ws`.
167
+ * Per-account rate limiter for the action dispatchers (HTTP RPC +
168
+ * WebSocket). Consulted for actions whose spec declares
169
+ * `rate_limit: 'account'` or `'both'`. Keyed on the authenticated account's
170
+ * id, so every actor and credential of one account shares a budget.
171
+ * Omit or `undefined` for the mode's default (1200 attempts per 15 minutes
172
+ * — permissive — under `rate_limiters: 'enforced'`). Pass `null` to
173
+ * explicitly disable.
174
+ * Threaded into every endpoint mounted from `rpc_endpoints` and
175
+ * `ws_endpoints`; also available on `AppServerContext`.
160
176
  */
161
177
  action_account_rate_limiter?: RateLimiter | null;
162
178
  /**
@@ -179,7 +195,13 @@ export interface AppServerOptions {
179
195
  * Called after all middleware is ready.
180
196
  */
181
197
  create_route_specs: (context: AppServerContext) => Array<RouteSpec>;
182
- /** Optional: transform middleware specs before applying. */
198
+ /**
199
+ * Optional: transform middleware specs before applying.
200
+ *
201
+ * The endpoint auth-scope check (`assert_endpoint_in_auth_scope`) reads
202
+ * `AUTH_MIDDLEWARE_PATH`, not the transformed specs — a transform that
203
+ * moves or drops the auth middleware is not followed by it.
204
+ */
183
205
  transform_middleware?: (specs: Array<MiddlewareSpec>) => Array<MiddlewareSpec>;
184
206
  /**
185
207
  * Enable factory-managed audit log SSE.
@@ -226,50 +248,79 @@ export interface AppServerOptions {
226
248
  event_specs?: Array<EventSpec>;
227
249
  /**
228
250
  * RPC endpoint specs — single source of truth for both surface generation
229
- * *and* live dispatch. Each entry is mounted via `create_rpc_endpoint`
230
- * against the assembled Hono app, so consumers no longer call
251
+ * *and* live dispatch. Each entry becomes route specs via
252
+ * `create_rpc_endpoint`, applied with the other routes, so consumers don't call
231
253
  * `create_rpc_endpoint` themselves inside `create_route_specs`.
232
254
  *
233
255
  * Accepts either an array (evaluated eagerly) or a factory
234
256
  * `(ctx: AppServerContext) => Array<RpcEndpointSpec>` (evaluated after the
235
257
  * server context is assembled). Use the factory form when action lists
236
258
  * depend on `ctx.deps` — e.g. `create_standard_rpc_actions(ctx.deps)`.
259
+ *
260
+ * Each path must sit inside the auth middleware's scope
261
+ * (`AUTH_MIDDLEWARE_PATH`, `'/api/*'`) — assembly throws otherwise, since
262
+ * an endpoint outside it would see every caller as unauthenticated.
237
263
  */
238
264
  rpc_endpoints?: Array<RpcEndpointSpec> | ((context: AppServerContext) => Array<RpcEndpointSpec>);
239
265
  /**
240
- * Hono adapter's `upgradeWebSocket` helper. Required whenever
241
- * `ws_endpoints` resolves to a non-empty array — `create_app_server`
242
- * throws at assembly otherwise. Omit (along with `ws_endpoints`)
243
- * when the consumer doesn't mount any WS endpoints. The same
244
- * adapter helper services every `WsEndpointSpec` mounted from
245
- * `ws_endpoints` — one adapter per app.
266
+ * Returns the Hono runtime adapter's `upgradeWebSocket` helper for the
267
+ * assembled app. `create_app_server` calls it once, with the app it
268
+ * builds (the same instance as `AppServer.app`), after creating the app
269
+ * and before mounting `ws_endpoints` — and only when `ws_endpoints`
270
+ * resolves non-empty, so it is never called for a server with no
271
+ * WebSocket endpoints. Required in that case: assembly throws without it.
272
+ * One helper services every mounted endpoint.
273
+ *
274
+ * A factory rather than the helper itself because Node's adapter needs
275
+ * the app: `@hono/node-ws` has no module-level helper, only
276
+ * `createNodeWebSocket({app})`, and `create_app_server` is what builds
277
+ * the app. Deno and Bun ignore the argument:
278
+ *
279
+ * ```ts
280
+ * import {upgradeWebSocket} from '@hono/deno'; // or '@hono/bun'
281
+ * create_upgrade_websocket: () => upgradeWebSocket,
282
+ * ```
246
283
  *
247
- * For Deno or Bun, `import {upgradeWebSocket} from '@hono/deno'` (or
248
- * `'@hono/bun'`). Node's `@hono/node-ws` has no module-level helper -
249
- * `createNodeWebSocket({app})` needs the app this function builds - so
250
- * on Node omit this and `ws_endpoints`, then mount on the returned
251
- * `app` with `register_ws_endpoint` and call `injectWebSocket(server)`
252
- * after `serve()`. Test harnesses use `create_stub_upgrade` from
253
- * `$lib/testing/ws_round_trip.ts`.
284
+ * Node keeps the adapter so it can attach to the server after `serve()`:
285
+ *
286
+ * ```ts
287
+ * let node_ws: NodeWebSocket | undefined;
288
+ * const server = await create_app_server({
289
+ * ...,
290
+ * create_upgrade_websocket: (app) => (node_ws = createNodeWebSocket({app})).upgradeWebSocket,
291
+ * });
292
+ * node_ws?.injectWebSocket(serve({fetch: server.app.fetch, port}));
293
+ * ```
294
+ *
295
+ * Test harnesses pass `() => stub.upgradeWebSocket` with
296
+ * `create_stub_upgrade` from `testing/ws_round_trip.ts`.
254
297
  */
255
- upgradeWebSocket?: UpgradeWebSocket;
298
+ create_upgrade_websocket?: (app: Hono) => UpgradeWebSocket;
256
299
  /**
257
300
  * WebSocket endpoint specs — single source of truth for both surface
258
301
  * generation *and* live dispatch. Each entry is auto-mounted via
259
- * `register_ws_endpoint` against the assembled Hono app, so
260
- * consumers no longer call `register_ws_endpoint` themselves.
302
+ * `register_ws_endpoint` against the assembled Hono app on every
303
+ * runtime, so consumers don't call `register_ws_endpoint` themselves.
261
304
  *
262
305
  * Accepts either an array (evaluated eagerly) or a factory
263
306
  * `(ctx: AppServerContext) => ReadonlyArray<WsEndpointSpec>`
264
307
  * (evaluated after the server context is assembled). Use the factory
265
- * form when action lists depend on `ctx.deps` /
266
- * `ctx.action_*_rate_limiter` — e.g. when spreading
267
- * `create_standard_rpc_actions(ctx.deps, ...)` over WS.
308
+ * form when action lists depend on `ctx.deps` — e.g. when spreading
309
+ * `create_standard_rpc_actions({...ctx.deps, notification_sender})` over WS.
268
310
  *
269
- * When non-empty, `upgradeWebSocket` must be supplied (throws
311
+ * When non-empty, `create_upgrade_websocket` must be supplied (throws
270
312
  * otherwise). A factory returning `[]` does NOT trip the check —
271
313
  * feature-flag gated WS surfaces stay safe.
272
314
  *
315
+ * Every mount gets what the HTTP surface gets: the auth middleware (a
316
+ * path outside `AUTH_MIDDLEWARE_PATH` throws at assembly, like
317
+ * `rpc_endpoints`), the server's `allowed_origins` unless the spec
318
+ * narrows them, and the same `action_ip_rate_limiter` /
319
+ * `action_account_rate_limiter` instances the RPC endpoints charge — one
320
+ * budget per action across both transports. The upgrade routes register
321
+ * after the route specs and before `post_route_middleware` and
322
+ * `static_serving`, so neither runs ahead of an upgrade.
323
+ *
273
324
  * Duplicate `path` values across two `WsEndpointSpec`s throw at
274
325
  * mount time (Hono would silently shadow them otherwise).
275
326
  *
@@ -353,10 +404,27 @@ export interface AppServer {
353
404
  */
354
405
  ws_endpoints: Readonly<Record<string, BackendWebsocketTransport>>;
355
406
  /**
356
- * Shut the server's resources down: stop the auth cleanup schedule when
357
- * the `auth_cleanup` option started one — waiting for a pass in progress —
358
- * then close the database connection (`AppBackend.close`). That wait has
359
- * no bound of its own; a shutdown with a deadline races it.
407
+ * Shut the server's resources down, in order:
408
+ *
409
+ * 1. stop the auth cleanup schedule when the `auth_cleanup` option started
410
+ * one, waiting for a pass in progress — that wait has no bound of its
411
+ * own, so a shutdown with a deadline races it
412
+ * 2. close every live connection on `deps.connection_closer`
413
+ * (`close_all_sockets`) — each WebSocket with `WS_CLOSE_GOING_AWAY`, each
414
+ * audit stream — including transports supplied through
415
+ * `WsEndpointSpec.transport` or added by hand, since they are registered
416
+ * on the backend's closer and the backend is going away; a close that
417
+ * throws is logged and the shutdown goes on
418
+ * 3. `dispose` the rate limiters `create_app_server` built for omitted
419
+ * options — never one passed in, which its caller owns
420
+ * 4. close the database connection (`AppBackend.close`)
421
+ *
422
+ * Idempotent: a second call settles with the first one's outcome (a failed
423
+ * close is not retried), and concurrent calls share one shutdown.
424
+ *
425
+ * Stop accepting new HTTP connections first — without awaiting the
426
+ * listener's full drain, since live WebSockets hold it open until `close`
427
+ * ends them — so no new request reaches a closed database.
360
428
  */
361
429
  close: () => Promise<void>;
362
430
  }
@@ -385,9 +453,11 @@ export declare const DEFAULT_MAX_BODY_SIZE: number;
385
453
  /**
386
454
  * Create a fully assembled Hono app with auth, middleware, and routes.
387
455
  *
388
- * Handles the assembly lifecycle: proxy middleware → auth middleware →
389
- * bootstrap status → route specs → surface generation → Hono app assembly →
390
- * static serving. Database migrations belong to the backend lifecycle —
456
+ * Handles the assembly lifecycle: rate limiters → audit SSE → proxy
457
+ * middleware → auth middleware → bootstrap status → route specs (consumer,
458
+ * bootstrap, RPC endpoints, surface) → surface generation → Hono app assembly
459
+ * (routes, then WS endpoints) → post-route middleware → static serving → the
460
+ * auth cleanup schedule. Database migrations belong to the backend lifecycle —
391
461
  * pass `migration_namespaces` to `create_app_backend`.
392
462
  *
393
463
  * When `audit_log_sse` is set, the SSE registry's listener is registered via
@@ -401,8 +471,11 @@ export declare const DEFAULT_MAX_BODY_SIZE: number;
401
471
  * to assert the invariant.
402
472
  *
403
473
  * When `auth_cleanup` is set, the auth cleanup schedule starts as the last
404
- * step of assembly — so an assembly that throws leaves no schedule running —
405
- * and the returned `close` stops it before closing the database.
474
+ * step of assembly — so an assembly that throws leaves no schedule running.
475
+ *
476
+ * The returned `close` owns shutdown: it stops that schedule, closes every
477
+ * live connection on the backend's closer, disposes the rate limiters built
478
+ * here, and closes the database (`AppServer.close`).
406
479
  *
407
480
  * @returns assembled Hono app, backend, surface build, and bootstrap status
408
481
  */
@@ -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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"}
@@ -14,7 +14,7 @@ import { z } from 'zod';
14
14
  import { session_cookie_options } from '../auth/session_cookie.js';
15
15
  import { create_audit_log_sse, audit_log_event_specs } from '../realtime/sse_auth_guard.js';
16
16
  import { BaseServerEnv } from './env.js';
17
- import { create_rate_limiter, default_login_account_rate_limit, default_action_account_rate_limit, default_action_ip_rate_limit } from '../rate_limiter.js';
17
+ import { RateLimiter, announce_rate_limiters_disabled, default_login_ip_rate_limit, default_login_account_rate_limit, default_action_account_rate_limit, default_action_ip_rate_limit } from '../rate_limiter.js';
18
18
  // Side-effect import: augments Hono's ContextVariableMap so consumers
19
19
  // that import app_server get type-safe c.get('auth_session_id') etc.
20
20
  import '../hono_context.js';
@@ -26,12 +26,13 @@ import { apply_middleware_specs, apply_route_specs, prefix_route_specs } from '.
26
26
  import { check_bootstrap_status, create_bootstrap_route_specs } from '../auth/bootstrap_routes.js';
27
27
  import { create_surface_route_spec } from '../http/common_routes.js';
28
28
  import { flush_pending_effects, flush_post_commit_effects } from '../http/pending_effects.js';
29
- import { create_auth_middleware_specs } from '../auth/middleware.js';
29
+ import { assert_endpoint_in_auth_scope, create_auth_middleware_specs } from '../auth/middleware.js';
30
30
  import { fuz_auth_guard_resolver } from '../auth/auth_guard_resolver.js';
31
31
  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 { resolve_ws_endpoints } from '../actions/ws_endpoint_spec.js';
35
36
  import { create_ws_auth_guard } from '../actions/transports_ws_auth_guard.js';
36
37
  import { start_auth_cleanup } from '../auth/cleanup.js';
37
38
  /**
@@ -62,9 +63,11 @@ export const DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
62
63
  /**
63
64
  * Create a fully assembled Hono app with auth, middleware, and routes.
64
65
  *
65
- * Handles the assembly lifecycle: proxy middleware → auth middleware →
66
- * bootstrap status → route specs → surface generation → Hono app assembly →
67
- * static serving. Database migrations belong to the backend lifecycle —
66
+ * Handles the assembly lifecycle: rate limiters → audit SSE → proxy
67
+ * middleware → auth middleware → bootstrap status → route specs (consumer,
68
+ * bootstrap, RPC endpoints, surface) → surface generation → Hono app assembly
69
+ * (routes, then WS endpoints) → post-route middleware → static serving → the
70
+ * auth cleanup schedule. Database migrations belong to the backend lifecycle —
68
71
  * pass `migration_namespaces` to `create_app_backend`.
69
72
  *
70
73
  * When `audit_log_sse` is set, the SSE registry's listener is registered via
@@ -78,8 +81,11 @@ export const DEFAULT_MAX_BODY_SIZE = 1024 * 1024;
78
81
  * to assert the invariant.
79
82
  *
80
83
  * 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.
84
+ * step of assembly — so an assembly that throws leaves no schedule running.
85
+ *
86
+ * The returned `close` owns shutdown: it stops that schedule, closes every
87
+ * live connection on the backend's closer, disposes the rate limiters built
88
+ * here, and closes the database (`AppServer.close`).
83
89
  *
84
90
  * @returns assembled Hono app, backend, surface build, and bootstrap status
85
91
  */
@@ -87,30 +93,31 @@ export const create_app_server = async (options) => {
87
93
  const { backend } = options;
88
94
  const { deps } = backend;
89
95
  const { log } = deps;
90
- // Rate limiter defaults (undefined = default, null = disable)
91
- // One instance per surface — see `AppServerOptions.login_ip_rate_limiter`
92
- // for why these are not a single shared bucket.
93
- const login_ip_rate_limiter = options.login_ip_rate_limiter === undefined
94
- ? create_rate_limiter()
95
- : options.login_ip_rate_limiter;
96
- const signup_ip_rate_limiter = options.signup_ip_rate_limiter === undefined
97
- ? create_rate_limiter()
98
- : options.signup_ip_rate_limiter;
99
- const bootstrap_ip_rate_limiter = options.bootstrap_ip_rate_limiter === undefined
100
- ? create_rate_limiter()
101
- : options.bootstrap_ip_rate_limiter;
102
- const login_account_rate_limiter = options.login_account_rate_limiter === undefined
103
- ? create_rate_limiter(default_login_account_rate_limit)
104
- : options.login_account_rate_limiter;
105
- const signup_account_rate_limiter = options.signup_account_rate_limiter === undefined
106
- ? create_rate_limiter(default_login_account_rate_limit)
107
- : options.signup_account_rate_limiter;
108
- const action_ip_rate_limiter = options.action_ip_rate_limiter === undefined
109
- ? create_rate_limiter(default_action_ip_rate_limit)
110
- : options.action_ip_rate_limiter;
111
- const action_account_rate_limiter = options.action_account_rate_limiter === undefined
112
- ? create_rate_limiter(default_action_account_rate_limit)
113
- : options.action_account_rate_limiter;
96
+ // Rate limiters: an explicit option (instance or `null`) wins; an omitted
97
+ // one is a default instance under 'enforced', `null` under
98
+ // 'disabled_for_testing'. One instance per surface — see
99
+ // `AppServerOptions.login_ip_rate_limiter` for why these are not a single
100
+ // shared bucket. The ones built here are the server's to dispose on close.
101
+ const rate_limiter_mode = options.rate_limiters ?? 'enforced';
102
+ const built_rate_limiters = [];
103
+ const resolve_rate_limiter = (option, defaults) => {
104
+ if (option !== undefined)
105
+ return option;
106
+ if (rate_limiter_mode === 'disabled_for_testing') {
107
+ announce_rate_limiters_disabled();
108
+ return null;
109
+ }
110
+ const limiter = new RateLimiter({ ...defaults });
111
+ built_rate_limiters.push(limiter);
112
+ return limiter;
113
+ };
114
+ const login_ip_rate_limiter = resolve_rate_limiter(options.login_ip_rate_limiter, default_login_ip_rate_limit);
115
+ const signup_ip_rate_limiter = resolve_rate_limiter(options.signup_ip_rate_limiter, default_login_ip_rate_limit);
116
+ const bootstrap_ip_rate_limiter = resolve_rate_limiter(options.bootstrap_ip_rate_limiter, default_login_ip_rate_limit);
117
+ const login_account_rate_limiter = resolve_rate_limiter(options.login_account_rate_limiter, default_login_account_rate_limit);
118
+ const signup_account_rate_limiter = resolve_rate_limiter(options.signup_account_rate_limiter, default_login_account_rate_limit);
119
+ const action_ip_rate_limiter = resolve_rate_limiter(options.action_ip_rate_limiter, default_action_ip_rate_limit);
120
+ const action_account_rate_limiter = resolve_rate_limiter(options.action_account_rate_limiter, default_action_account_rate_limit);
114
121
  // Factory-managed audit SSE — registers a listener on the bound emitter
115
122
  // so SSE fan-out runs alongside the consumer's `on_audit_event`
116
123
  // without rebuilding `AppDeps`, and adds the registry to the backend's
@@ -195,6 +202,7 @@ export const create_app_server = async (options) => {
195
202
  : options.rpc_endpoints;
196
203
  if (resolved_rpc_endpoints) {
197
204
  for (const endpoint of resolved_rpc_endpoints) {
205
+ assert_endpoint_in_auth_scope('rpc_endpoints', endpoint.path);
198
206
  factory_routes.push(...create_rpc_endpoint({
199
207
  path: endpoint.path,
200
208
  actions: endpoint.actions,
@@ -210,9 +218,24 @@ export const create_app_server = async (options) => {
210
218
  // live Hono `app` (origin / auth / role / authorization middleware +
211
219
  // the `app.get(path, ...)` upgrade route), and `app` does not exist
212
220
  // until the assembly phase below.
213
- const resolved_ws_endpoints = typeof options.ws_endpoints === 'function'
221
+ // A spec without `allowed_origins` takes the server's list — resolved here,
222
+ // once, so the mount and the surface read the same patterns.
223
+ const resolved_ws_endpoints = resolve_ws_endpoints(typeof options.ws_endpoints === 'function'
214
224
  ? options.ws_endpoints(context)
215
- : options.ws_endpoints;
225
+ : options.ws_endpoints, options.allowed_origins);
226
+ // the endpoints to mount and the adapter factory they need — null when none resolve
227
+ let ws_mount = null;
228
+ if (resolved_ws_endpoints?.length) {
229
+ const create_upgrade_websocket = options.create_upgrade_websocket;
230
+ if (create_upgrade_websocket === undefined) {
231
+ throw new Error('create_app_server: ws_endpoints resolved non-empty but create_upgrade_websocket is missing. ' +
232
+ "Pass a factory returning the Hono adapter's upgradeWebSocket helper.");
233
+ }
234
+ for (const endpoint of resolved_ws_endpoints) {
235
+ assert_endpoint_in_auth_scope('ws_endpoints', endpoint.path);
236
+ }
237
+ ws_mount = { endpoints: resolved_ws_endpoints, create_upgrade_websocket };
238
+ }
216
239
  // Surface route (default: enabled)
217
240
  if (options.surface_route !== false) {
218
241
  factory_routes.push(create_surface_route_spec(surface_ref));
@@ -260,20 +283,35 @@ export const create_app_server = async (options) => {
260
283
  });
261
284
  }
262
285
  }
263
- // One diagnostic per surface — a deployment that disables login's limiter
264
- // but keeps signup's is a different posture than one that disables both,
265
- // and a single collapsed warning hid which surface was open.
266
- for (const [name, limiter] of [
267
- ['login', login_ip_rate_limiter],
268
- ['signup', signup_ip_rate_limiter],
269
- ['bootstrap', bootstrap_ip_rate_limiter]
270
- ]) {
271
- if (limiter === null) {
272
- config_diagnostics.push({
273
- level: 'warning',
274
- category: 'config',
275
- message: `${name} IP rate limiter explicitly disabled (null)`
276
- });
286
+ if (rate_limiter_mode === 'disabled_for_testing') {
287
+ // one warning for the mode rather than one per limiter it nulled
288
+ config_diagnostics.push({
289
+ level: 'warning',
290
+ category: 'security',
291
+ message: "rate limiters disabled for testing (rate_limiters: 'disabled_for_testing') — " +
292
+ 'every limiter not passed explicitly is off; never use in production'
293
+ });
294
+ }
295
+ else {
296
+ // One diagnostic per explicitly-null limiter — a deployment that disables
297
+ // login's limiter but keeps signup's is a different posture than one that
298
+ // disables both, and a single collapsed warning hid which surface was open.
299
+ for (const [name, limiter] of [
300
+ ['login IP', login_ip_rate_limiter],
301
+ ['signup IP', signup_ip_rate_limiter],
302
+ ['bootstrap IP', bootstrap_ip_rate_limiter],
303
+ ['login account', login_account_rate_limiter],
304
+ ['signup account', signup_account_rate_limiter],
305
+ ['action IP', action_ip_rate_limiter],
306
+ ['action account', action_account_rate_limiter]
307
+ ]) {
308
+ if (limiter === null) {
309
+ config_diagnostics.push({
310
+ level: 'warning',
311
+ category: 'security',
312
+ message: `${name} rate limiter explicitly disabled (null)`
313
+ });
314
+ }
277
315
  }
278
316
  }
279
317
  if (config_diagnostics.length) {
@@ -347,11 +385,10 @@ export const create_app_server = async (options) => {
347
385
  // adjacent to the consumer routes and ahead of the static fallback —
348
386
  // matches the "WS mount is route registration" mental model.
349
387
  const mounted_ws_endpoints = {};
350
- if (resolved_ws_endpoints?.length) {
351
- if (options.upgradeWebSocket === undefined) {
352
- throw new Error('create_app_server: ws_endpoints resolved non-empty but upgradeWebSocket is missing. ' +
353
- "Pass the Hono adapter's upgradeWebSocket helper as a top-level option.");
354
- }
388
+ if (ws_mount) {
389
+ // the adapter helper — built from this app, which Node's
390
+ // `createNodeWebSocket({app})` needs, and only when there is something to mount
391
+ const upgrade_websocket = ws_mount.create_upgrade_websocket(app);
355
392
  // Cross-surface collision: `register_ws_endpoint` mounts a `GET path`
356
393
  // upgrade route. If a `RouteSpec` already registered `GET path`,
357
394
  // Hono's last-wins semantics would silently shadow the consumer's
@@ -371,7 +408,7 @@ export const create_app_server = async (options) => {
371
408
  // (even with explicit `auth_guard: false`) cannot opt out. To
372
409
  // disable, every spec sharing the transport must pass `auth_guard: false`.
373
410
  const guarded_transports = new WeakSet();
374
- for (const endpoint of resolved_ws_endpoints) {
411
+ for (const endpoint of ws_mount.endpoints) {
375
412
  if (seen_paths.has(endpoint.path)) {
376
413
  throw new Error(`create_app_server: duplicate ws_endpoints path: ${endpoint.path}`);
377
414
  }
@@ -382,7 +419,7 @@ export const create_app_server = async (options) => {
382
419
  const { transport: endpoint_transport } = register_ws_endpoint({
383
420
  app,
384
421
  path: endpoint.path,
385
- upgradeWebSocket: options.upgradeWebSocket,
422
+ upgradeWebSocket: upgrade_websocket,
386
423
  allowed_origins: endpoint.allowed_origins,
387
424
  db: deps.db,
388
425
  actions: endpoint.actions,
@@ -429,6 +466,24 @@ export const create_app_server = async (options) => {
429
466
  const auth_cleanup = options.auth_cleanup
430
467
  ? start_auth_cleanup(deps, options.auth_cleanup === true ? undefined : options.auth_cleanup)
431
468
  : null;
469
+ // One shutdown, shared by every call — see `AppServer.close` for the order.
470
+ let closing = null;
471
+ const shutdown = async () => {
472
+ // the schedule first, so a pass in progress finishes on a live pool
473
+ if (auth_cleanup)
474
+ await auth_cleanup.stop();
475
+ // then the connections, so none outlives the server — a failure here
476
+ // must not keep the database open
477
+ try {
478
+ deps.connection_closer.close_all_sockets();
479
+ }
480
+ catch (error) {
481
+ log.error('create_app_server: closing live connections on shutdown failed:', error);
482
+ }
483
+ for (const limiter of built_rate_limiters)
484
+ limiter.dispose();
485
+ await backend.close();
486
+ };
432
487
  return {
433
488
  app,
434
489
  surface_spec,
@@ -436,12 +491,6 @@ export const create_app_server = async (options) => {
436
491
  migration_results: backend.migration_results,
437
492
  audit_sse,
438
493
  ws_endpoints: mounted_ws_endpoints,
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
494
+ close: () => (closing ??= shutdown())
446
495
  };
447
496
  };
@@ -26,22 +26,30 @@ export interface AppServerContext {
26
26
  bootstrap_status: BootstrapStatus;
27
27
  session_options: SessionOptions<string>;
28
28
  /**
29
- * Per-IP login + password-change rate limiter (from options). `null` when
30
- * not configured. One instance per auth surface — see
31
- * `AppServerOptions.login_ip_rate_limiter` for why these aren't shared.
29
+ * Per-IP login + password-change rate limiter. One instance per auth
30
+ * surface — see `AppServerOptions.login_ip_rate_limiter` for why these
31
+ * aren't shared.
32
+ *
33
+ * Each limiter here is what `create_app_server` resolved: the instance
34
+ * its option passed, else a default instance under
35
+ * `rate_limiters: 'enforced'` (the default). `null` when disabled — the
36
+ * option was an explicit `null`, or the mode is `'disabled_for_testing'`.
32
37
  */
33
38
  login_ip_rate_limiter: RateLimiter | null;
34
- /** Per-IP signup rate limiter (from options). `null` when not configured. */
39
+ /** Per-IP signup rate limiter. `null` when disabled. */
35
40
  signup_ip_rate_limiter: RateLimiter | null;
36
- /** Per-IP bootstrap rate limiter (from options). `null` when not configured. */
41
+ /** Per-IP bootstrap rate limiter. `null` when disabled. */
37
42
  bootstrap_ip_rate_limiter: RateLimiter | null;
38
- /** Per-account login rate limiter (from options). `null` when not configured. */
43
+ /** Per-account login rate limiter. `null` when disabled. */
39
44
  login_account_rate_limiter: RateLimiter | null;
40
- /** Per-account signup rate limiter (from options). `null` when not configured. */
45
+ /** Per-account signup rate limiter. `null` when disabled. */
41
46
  signup_account_rate_limiter: RateLimiter | null;
42
- /** Per-IP action-dispatcher rate limiter — shared across HTTP RPC + WS. `null` when not configured. */
47
+ /** Per-IP action-dispatcher rate limiter — shared across HTTP RPC + WS. `null` when disabled. */
43
48
  action_ip_rate_limiter: RateLimiter | null;
44
- /** Per-actor action-dispatcher rate limiter — shared across HTTP RPC + WS. `null` when not configured. */
49
+ /**
50
+ * Per-account action-dispatcher rate limiter, keyed on the authenticated
51
+ * account's id — shared across HTTP RPC + WS. `null` when disabled.
52
+ */
45
53
  action_account_rate_limiter: RateLimiter | null;
46
54
  /**
47
55
  * Factory-managed audit log SSE. Non-null when the `audit_log_sse`