@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.
- package/dist/actions/CLAUDE.md +301 -73
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +16 -1
- package/dist/actions/connection_closer.d.ts +136 -51
- package/dist/actions/connection_closer.d.ts.map +1 -1
- package/dist/actions/connection_closer.js +133 -34
- package/dist/actions/register_action_ws.d.ts +176 -24
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +581 -241
- package/dist/actions/register_ws_endpoint.d.ts +1 -0
- package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
- package/dist/actions/register_ws_endpoint.js +1 -0
- package/dist/actions/socket.svelte.d.ts +43 -9
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +59 -8
- package/dist/actions/transports.d.ts +55 -1
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports.js +55 -1
- package/dist/actions/transports_ws_auth_guard.d.ts +53 -67
- package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
- package/dist/actions/transports_ws_auth_guard.js +87 -111
- package/dist/actions/transports_ws_backend.d.ts +212 -30
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +327 -70
- package/dist/actions/ws_endpoint_spec.d.ts +26 -13
- package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
- package/dist/auth/CLAUDE.md +125 -31
- package/dist/auth/account_actions.d.ts +12 -18
- package/dist/auth/account_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +47 -33
- package/dist/auth/account_routes.d.ts +5 -11
- package/dist/auth/account_routes.d.ts.map +1 -1
- package/dist/auth/account_routes.js +40 -36
- package/dist/auth/account_schema.d.ts +27 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +22 -0
- package/dist/auth/admin_actions.d.ts +8 -17
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +33 -21
- package/dist/auth/api_token_queries.d.ts +23 -2
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +39 -3
- package/dist/auth/audit_emitter.d.ts +72 -44
- package/dist/auth/audit_emitter.d.ts.map +1 -1
- package/dist/auth/audit_emitter.js +60 -16
- package/dist/auth/audit_log_route_schema.js +7 -7
- package/dist/auth/audit_log_routes.d.ts +31 -6
- package/dist/auth/audit_log_routes.d.ts.map +1 -1
- package/dist/auth/audit_log_routes.js +85 -11
- package/dist/auth/audit_log_schema.d.ts +48 -0
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +58 -0
- package/dist/auth/bootstrap_account.d.ts +3 -2
- package/dist/auth/bootstrap_account.d.ts.map +1 -1
- package/dist/auth/bootstrap_account.js +3 -3
- package/dist/auth/cleanup.d.ts +219 -30
- package/dist/auth/cleanup.d.ts.map +1 -1
- package/dist/auth/cleanup.js +260 -45
- package/dist/auth/deps.d.ts +29 -1
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +27 -0
- package/dist/auth/resolved_auth.d.ts +105 -0
- package/dist/auth/resolved_auth.d.ts.map +1 -0
- package/dist/auth/resolved_auth.js +124 -0
- package/dist/auth/role_grant_offer_ddl.d.ts +4 -2
- package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_ddl.js +10 -3
- package/dist/auth/role_grant_offer_queries.d.ts +16 -7
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +33 -15
- package/dist/auth/role_grant_offer_schema.d.ts +9 -0
- package/dist/auth/role_grant_offer_schema.d.ts.map +1 -1
- package/dist/auth/session_middleware.d.ts +8 -1
- package/dist/auth/session_middleware.d.ts.map +1 -1
- package/dist/auth/session_middleware.js +9 -3
- package/dist/auth/session_queries.d.ts +31 -6
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +31 -6
- package/dist/auth/standard_rpc_actions.d.ts +9 -9
- package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
- package/dist/auth/standard_rpc_actions.js +3 -3
- package/dist/db/CLAUDE.md +7 -1
- package/dist/db/migrate.d.ts +8 -13
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +9 -38
- package/dist/db/status.d.ts +58 -7
- package/dist/db/status.d.ts.map +1 -1
- package/dist/db/status.js +205 -97
- package/dist/http/CLAUDE.md +7 -3
- package/dist/http/db_routes.d.ts +6 -2
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +2 -2
- package/dist/periodic.d.ts +80 -0
- package/dist/periodic.d.ts.map +1 -0
- package/dist/periodic.js +122 -0
- package/dist/realtime/sse_auth_guard.d.ts +79 -49
- package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
- package/dist/realtime/sse_auth_guard.js +103 -73
- package/dist/realtime/subscriber_registry.d.ts +119 -8
- package/dist/realtime/subscriber_registry.d.ts.map +1 -1
- package/dist/realtime/subscriber_registry.js +184 -25
- package/dist/runtime/deno.d.ts.map +1 -1
- package/dist/runtime/deno.js +9 -6
- package/dist/runtime/deps.d.ts +3 -2
- package/dist/runtime/deps.d.ts.map +1 -1
- package/dist/runtime/secure_file.d.ts +23 -9
- package/dist/runtime/secure_file.d.ts.map +1 -1
- package/dist/runtime/secure_file.js +29 -10
- package/dist/server/app_backend.d.ts +13 -2
- package/dist/server/app_backend.d.ts.map +1 -1
- package/dist/server/app_backend.js +5 -2
- package/dist/server/app_server.d.ts +55 -15
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +41 -13
- package/dist/testing/CLAUDE.md +140 -48
- package/dist/testing/app_server.d.ts +2 -2
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +11 -2
- package/dist/testing/audit_drift_guard.d.ts +3 -52
- package/dist/testing/audit_drift_guard.d.ts.map +1 -1
- package/dist/testing/audit_drift_guard.js +2 -46
- package/dist/testing/connection_closer_helpers.d.ts +9 -22
- package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
- package/dist/testing/connection_closer_helpers.js +11 -17
- package/dist/testing/cross_backend/capabilities.d.ts +67 -13
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +27 -12
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +21 -0
- package/dist/testing/cross_backend/expected_schema.json +1 -0
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
- 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 +4 -1
- package/dist/testing/cross_backend/session_cap.d.ts +13 -0
- package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
- package/dist/testing/cross_backend/session_cap.js +55 -19
- package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
- package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/sse_round_trip.js +93 -26
- package/dist/testing/cross_backend/testing_server_bun.js +4 -4
- 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 -2
- package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
- package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
- package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
- package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
- package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/cross_backend/ws_round_trip.js +156 -19
- package/dist/testing/sse_round_trip.d.ts +6 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/stubs.d.ts +1 -1
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +6 -4
- package/dist/testing/transports/sse_transport.d.ts +1 -1
- package/dist/testing/transports/ws_client.d.ts +7 -0
- package/dist/testing/transports/ws_client.d.ts.map +1 -1
- package/dist/testing/transports/ws_raw_client.d.ts +51 -0
- package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
- package/dist/testing/transports/ws_raw_client.js +156 -0
- package/dist/testing/transports/ws_transport.d.ts +28 -0
- package/dist/testing/transports/ws_transport.d.ts.map +1 -1
- package/dist/testing/transports/ws_transport.js +45 -1
- package/dist/testing/ws_round_trip.d.ts +8 -3
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +19 -4
- package/dist/ui/AdminAccounts.svelte +4 -3
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminOverview.svelte +3 -1
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +22 -6
- package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +16 -0
- package/dist/ui/auth_state.svelte.d.ts +25 -1
- package/dist/ui/auth_state.svelte.d.ts.map +1 -1
- package/dist/ui/auth_state.svelte.js +28 -1
- 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`),
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
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
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
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
|
-
/**
|
|
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`.
|
|
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":"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,EAKN,KAAK,WAAW,EAChB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGhE,OAAO,oBAAoB,CAAC;AAE5B,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEhF,OAAO,EAEN,KAAK,cAAc,EAEnB,KAAK,eAAe,EACpB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAIN,KAAK,SAAS,EACd,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAGN,KAAK,eAAe,EACpB,MAAM,6BAA6B,CAAC;AASrC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;
|
|
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
|
|
36
|
-
import {
|
|
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`.
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
};
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -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. `
|
|
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(
|
|
111
|
-
- `assert_close_call(call, method, id)` — pins `{method, id}` on a recorded close call
|
|
112
|
-
- `RecordedClose` — `{method: 'session' | 'token' | 'account', id
|
|
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`,
|
|
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)
|
|
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
|
-
|
|
523
|
-
(
|
|
524
|
-
the
|
|
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
|
-
|
|
1094
|
-
|
|
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
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
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
|
-
(`
|
|
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.
|
|
1171
|
-
|
|
1172
|
-
|
|
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);
|
|
1176
|
-
_own_ sessions are revoked (`account_session_revoke_all
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
`
|
|
1182
|
-
`
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
|
1691
|
-
|
|
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).
|