@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.
- package/dist/actions/CLAUDE.md +56 -20
- package/dist/actions/action_rpc.d.ts +14 -11
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +5 -4
- package/dist/actions/connection_closer.d.ts +13 -3
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +7 -5
- package/dist/actions/register_action_ws.d.ts +18 -13
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +8 -6
- package/dist/actions/register_ws_endpoint.d.ts +6 -3
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +6 -3
- package/dist/actions/transports.d.ts +9 -0
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +9 -0
- package/dist/actions/transports_ws_backend.d.ts +15 -0
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +25 -5
- package/dist/actions/ws_endpoint_spec.d.ts +24 -4
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/actions/ws_endpoint_spec.js +15 -1
- package/dist/auth/middleware.d.ts +29 -1
- package/dist/auth/middleware.d.ts.map +1 -1
- package/dist/auth/middleware.js +35 -1
- package/dist/http/CLAUDE.md +3 -3
- package/dist/http/surface.d.ts +12 -9
- package/dist/http/surface.d.ts.map +1 -1
- package/dist/rate_limiter.d.ts +43 -3
- package/dist/rate_limiter.d.ts.map +1 -1
- package/dist/rate_limiter.js +27 -3
- package/dist/realtime/sse_auth_guard.d.ts +2 -1
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +4 -2
- package/dist/realtime/subscriber_registry.d.ts +12 -0
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +18 -2
- package/dist/server/app_server.d.ts +128 -55
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +110 -61
- package/dist/server/app_server_context.d.ts +17 -9
- package/dist/server/app_server_context.d.ts.map +1 -1
- package/dist/testing/CLAUDE.md +83 -25
- package/dist/testing/app_server.d.ts +6 -3
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +20 -20
- package/dist/testing/connection_closer_helpers.d.ts +10 -4
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +4 -0
- package/dist/testing/cross_backend/capabilities.d.ts +3 -3
- package/dist/testing/cross_backend/default_backend_configs.d.ts +12 -12
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +11 -10
- package/dist/testing/cross_backend/full_spine_mount.d.ts +35 -11
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +26 -9
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +16 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +5 -4
- package/dist/testing/cross_backend/spine_surface_constants.d.ts +43 -3
- package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
- package/dist/testing/cross_backend/spine_surface_constants.js +43 -3
- package/dist/testing/cross_backend/testing_backdoor.d.ts +15 -1
- package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_backdoor.js +51 -6
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +6 -3
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +6 -3
- package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_server_bun.js +5 -1
- package/dist/testing/cross_backend/testing_server_core.d.ts +72 -41
- package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_server_core.js +22 -14
- package/dist/testing/cross_backend/testing_server_deno.js +1 -1
- package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_server_node.js +20 -7
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +15 -2
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -8
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts +26 -0
- package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_action_rate_limit.js +261 -0
- package/dist/testing/middleware.d.ts.map +1 -1
- package/dist/testing/middleware.js +4 -3
- package/dist/testing/stubs.d.ts +17 -4
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +46 -13
- package/dist/testing/transports/ws_transport.d.ts +7 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -1
- package/dist/testing/transports/ws_transport.js +1 -1
- package/dist/testing/ws_round_trip.d.ts +43 -37
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +28 -46
- 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
|
|
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
|
|
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)
|
|
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`
|
|
114
|
-
*
|
|
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`
|
|
122
|
-
*
|
|
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
|
-
*
|
|
131
|
-
* Pass `null` to explicitly disable
|
|
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`
|
|
138
|
-
* Pass `null` to explicitly disable
|
|
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`
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
* `
|
|
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-
|
|
154
|
-
* Consulted for actions whose spec declares
|
|
155
|
-
* `'both'`. Keyed on
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
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
|
-
/**
|
|
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
|
|
230
|
-
*
|
|
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
|
|
241
|
-
* `
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
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
|
-
*
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
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
|
-
|
|
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
|
|
260
|
-
* consumers
|
|
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.
|
|
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, `
|
|
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
|
|
357
|
-
*
|
|
358
|
-
*
|
|
359
|
-
*
|
|
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:
|
|
389
|
-
*
|
|
390
|
-
*
|
|
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
|
-
*
|
|
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":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,IAAI,EAAE,KAAK,OAAO,EAAE,MAAM,MAAM,CAAC;AAG1C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAEN,KAAK,cAAc,EAEnB,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AAC5E,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AACpD,OAAO,EAGN,KAAK,WAAW,EAChB,MAAM,+BAA+B,CAAC;AAEvC,OAAO,
|
|
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 {
|
|
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:
|
|
66
|
-
*
|
|
67
|
-
*
|
|
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
|
-
*
|
|
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
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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 (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
|
39
|
+
/** Per-IP signup rate limiter. `null` when disabled. */
|
|
35
40
|
signup_ip_rate_limiter: RateLimiter | null;
|
|
36
|
-
/** Per-IP bootstrap rate limiter
|
|
41
|
+
/** Per-IP bootstrap rate limiter. `null` when disabled. */
|
|
37
42
|
bootstrap_ip_rate_limiter: RateLimiter | null;
|
|
38
|
-
/** Per-account login rate limiter
|
|
43
|
+
/** Per-account login rate limiter. `null` when disabled. */
|
|
39
44
|
login_account_rate_limiter: RateLimiter | null;
|
|
40
|
-
/** Per-account signup rate limiter
|
|
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
|
|
47
|
+
/** Per-IP action-dispatcher rate limiter — shared across HTTP RPC + WS. `null` when disabled. */
|
|
43
48
|
action_ip_rate_limiter: RateLimiter | null;
|
|
44
|
-
/**
|
|
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`
|