@fuzdev/fuz_app 0.134.0 → 0.135.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/dist/actions/CLAUDE.md +91 -9
  2. package/dist/actions/register_action_ws.d.ts +23 -3
  3. package/dist/actions/register_action_ws.d.ts.map +1 -1
  4. package/dist/actions/register_action_ws.js +57 -37
  5. package/dist/actions/transports.d.ts +23 -0
  6. package/dist/actions/transports.d.ts.map +1 -1
  7. package/dist/actions/transports.js +23 -0
  8. package/dist/actions/transports_ws_backend.d.ts +132 -9
  9. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  10. package/dist/actions/transports_ws_backend.js +236 -33
  11. package/dist/actions/ws_endpoint_spec.d.ts +9 -0
  12. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  13. package/dist/actions/ws_outbound_writer.d.ts +206 -0
  14. package/dist/actions/ws_outbound_writer.d.ts.map +1 -0
  15. package/dist/actions/ws_outbound_writer.js +431 -0
  16. package/dist/auth/CLAUDE.md +2 -2
  17. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  18. package/dist/auth/audit_log_routes.js +3 -1
  19. package/dist/auth/cell_actions.js +1 -1
  20. package/dist/auth/role_schema.d.ts +14 -3
  21. package/dist/auth/role_schema.d.ts.map +1 -1
  22. package/dist/auth/role_schema.js +14 -2
  23. package/dist/db/migrate.d.ts +1 -1
  24. package/dist/db/migrate.js +2 -2
  25. package/dist/http/CLAUDE.md +2 -1
  26. package/dist/http/body_limit.d.ts +61 -0
  27. package/dist/http/body_limit.d.ts.map +1 -0
  28. package/dist/http/body_limit.js +87 -0
  29. package/dist/http/db_routes.js +2 -2
  30. package/dist/http/error_schemas.d.ts +2 -0
  31. package/dist/http/error_schemas.d.ts.map +1 -1
  32. package/dist/http/error_schemas.js +2 -0
  33. package/dist/realtime/outbound_budget.d.ts +91 -0
  34. package/dist/realtime/outbound_budget.d.ts.map +1 -0
  35. package/dist/realtime/outbound_budget.js +108 -0
  36. package/dist/realtime/sse.d.ts +36 -3
  37. package/dist/realtime/sse.d.ts.map +1 -1
  38. package/dist/realtime/sse.js +58 -6
  39. package/dist/realtime/sse_constants.d.ts +12 -0
  40. package/dist/realtime/sse_constants.d.ts.map +1 -1
  41. package/dist/realtime/sse_constants.js +12 -0
  42. package/dist/server/app_server.d.ts.map +1 -1
  43. package/dist/server/app_server.js +3 -6
  44. package/dist/testing/CLAUDE.md +70 -10
  45. package/dist/testing/admin_integration.js +1 -1
  46. package/dist/testing/audit_completeness.d.ts +1 -1
  47. package/dist/testing/audit_completeness.js +1 -1
  48. package/dist/testing/audit_drift_guard.d.ts +1 -1
  49. package/dist/testing/audit_drift_guard.js +1 -1
  50. package/dist/testing/cross_backend/body_size_smuggling.d.ts +9 -9
  51. package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
  52. package/dist/testing/cross_backend/body_size_smuggling.js +110 -60
  53. package/dist/testing/cross_backend/capabilities.d.ts +18 -15
  54. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  55. package/dist/testing/cross_backend/capabilities.js +1 -0
  56. package/dist/testing/cross_backend/default_backend_configs.js +5 -5
  57. package/dist/testing/cross_backend/role_grant_role_grammar.d.ts +6 -0
  58. package/dist/testing/cross_backend/role_grant_role_grammar.d.ts.map +1 -0
  59. package/dist/testing/cross_backend/role_grant_role_grammar.js +117 -0
  60. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +2 -3
  61. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  62. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +2 -3
  63. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  64. package/dist/testing/cross_backend/testing_backdoor.js +8 -4
  65. package/dist/testing/cross_backend/testing_reset_actions.d.ts +82 -2
  66. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  67. package/dist/testing/cross_backend/testing_reset_actions.js +101 -2
  68. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  69. package/dist/testing/cross_backend/ts_spine_backend_config.js +9 -7
  70. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts +29 -0
  71. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -0
  72. package/dist/testing/cross_backend/ws_outbound_overflow.js +211 -0
  73. package/dist/testing/integration.js +1 -1
  74. package/dist/testing/schema_introspect.d.ts +10 -2
  75. package/dist/testing/schema_introspect.d.ts.map +1 -1
  76. package/dist/testing/schema_introspect.js +15 -7
  77. package/dist/testing/transports/ws_transport.d.ts +4 -2
  78. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  79. package/dist/testing/transports/ws_transport.js +2 -1
  80. package/dist/testing/ws_round_trip.d.ts +32 -2
  81. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  82. package/dist/testing/ws_round_trip.js +26 -4
  83. package/package.json +1 -1
@@ -4,12 +4,30 @@
4
4
  * Provides generic helpers for creating SSE response streams
5
5
  * and a notification type aligned with JSON-RPC 2.0.
6
6
  *
7
+ * ## Outbound bound
8
+ *
9
+ * `SseStream.send` never waits on its client: each frame is written to the
10
+ * response stream, which holds it until the client reads. What a stream holds
11
+ * unread is kept within `SSE_OUTBOUND_QUEUE_BYTES` — each frame charged its
12
+ * bytes as written (`data: ` prefix and terminator included) plus
13
+ * `OUTBOUND_FRAME_OVERHEAD_BYTES`, and a frame always accepted when nothing is
14
+ * unread, so a single frame larger than the budget is still deliverable. A
15
+ * frame that does not fit **ends the stream**: it is not written, what was
16
+ * unread is dropped, the body ends with no final frame, and the stream's
17
+ * `on_close` listeners run. One `warn` line names the stream's account (when
18
+ * the caller passed it) and what was unread; no audit row is written, since each would itself be fanned to every
19
+ * stream. A client that is reading reconnects and misses the frames in
20
+ * between. The twin of the Rust spine's `fuz_realtime` SSE bound — the same
21
+ * budget and charge.
22
+ *
7
23
  * @module
8
24
  */
9
25
  import { streamSSE } from 'hono/streaming';
10
26
  import { z } from 'zod';
11
27
  import { DEV } from 'esm-env';
12
- import { SSE_CONNECTED_COMMENT } from './sse_constants.js';
28
+ import { utf8_length } from '../actions/transports.js';
29
+ import { OutboundBudget } from './outbound_budget.js';
30
+ import { SSE_CONNECTED_COMMENT, SSE_OUTBOUND_QUEUE_BYTES } from './sse_constants.js';
13
31
  const is_node_connection_stream = (value) => {
14
32
  if (typeof value !== 'object' || value === null)
15
33
  return false;
@@ -85,10 +103,15 @@ const watch_client_disconnect = (c, on_gone) => {
85
103
  * Uses `hono_stream.write()` directly (not `writeSSE`) to avoid
86
104
  * Hono's HTML callback resolution — keeps the same `data: JSON\n\n` format.
87
105
  *
88
- * @param log - logger for serialization and `on_close` listener errors
106
+ * What the stream holds unread is bounded by `SSE_OUTBOUND_QUEUE_BYTES`; a
107
+ * frame past it ends the stream (module doc, "Outbound bound").
108
+ *
109
+ * @param log - logger for serialization and `on_close` listener errors, and
110
+ * for the warning when the stream ends on its outbound budget
111
+ * @param options - what the overflow warning names
89
112
  * @returns object with the streaming `Response` and an `SseStream` controller
90
113
  */
91
- export const create_sse_response = (c, log) => {
114
+ export const create_sse_response = (c, log, options) => {
92
115
  const { promise, resolve } = Promise.withResolvers();
93
116
  const close_listeners = [];
94
117
  let resolved = false;
@@ -107,24 +130,53 @@ export const create_sse_response = (c, log) => {
107
130
  }
108
131
  };
109
132
  let sse_stream;
133
+ // charges the frames written to the stream and not yet read from it
134
+ const budget = new OutboundBudget(SSE_OUTBOUND_QUEUE_BYTES);
110
135
  const response = streamSSE(c, async (hono_stream) => {
136
+ // Write one frame within the budget, or end the stream. The write
137
+ // settles once the response body has taken the frame, which frees its
138
+ // charge.
139
+ const write = (frame) => {
140
+ const bytes = utf8_length(frame);
141
+ if (!budget.admit(bytes)) {
142
+ // same line as the Rust spine's registry, so one grep finds either
143
+ log.warn('sse: outbound queue overflow, ending the stream', {
144
+ account_id: options?.account_id ?? null,
145
+ ...budget.to_overflow(bytes)
146
+ });
147
+ budget.clear();
148
+ // drops what the stream still holds and ends the body; `do_close`
149
+ // runs as its abort listener
150
+ hono_stream.abort();
151
+ do_close();
152
+ return;
153
+ }
154
+ void hono_stream.write(frame).then(() => {
155
+ // a stream that ended has had its charges cleared
156
+ if (!resolved && !hono_stream.aborted)
157
+ budget.release(bytes);
158
+ });
159
+ };
111
160
  sse_stream = {
112
161
  send(data) {
113
162
  if (resolved || hono_stream.aborted)
114
163
  return;
164
+ let frame;
115
165
  try {
116
166
  // JSON.stringify (no space arg) never produces literal newlines,
117
167
  // so single-line `data:` framing is safe per the SSE spec.
118
- void hono_stream.write(`data: ${JSON.stringify(data)}\n\n`);
168
+ frame = `data: ${JSON.stringify(data)}\n\n`;
119
169
  }
120
170
  catch (e) {
121
171
  log.error('send failed to serialize data:', e);
172
+ return;
122
173
  }
174
+ write(frame);
123
175
  },
124
176
  comment(text) {
125
177
  if (resolved || hono_stream.aborted)
126
178
  return;
127
- void hono_stream.write(`: ${text}\n`);
179
+ write(`: ${text}\n`);
128
180
  },
129
181
  close: do_close,
130
182
  on_close(fn) {
@@ -143,7 +195,7 @@ export const create_sse_response = (c, log) => {
143
195
  hono_stream.onAbort(do_close);
144
196
  // flush an SSE comment to push headers through proxies (Vite, nginx),
145
197
  // ensuring EventSource fires onopen without waiting for the first data event
146
- void hono_stream.write(SSE_CONNECTED_COMMENT);
198
+ write(SSE_CONNECTED_COMMENT);
147
199
  await promise;
148
200
  });
149
201
  // after the callback above has built `sse_stream`, which runs synchronously
@@ -14,4 +14,16 @@
14
14
  * SSE tests assert the stream emits this on connect.
15
15
  */
16
16
  export declare const SSE_CONNECTED_COMMENT = ": connected\n\n";
17
+ /**
18
+ * The byte budget of what one SSE stream holds for its client to read
19
+ * (`realtime/sse.ts` module doc, "Outbound bound").
20
+ *
21
+ * 1 MiB — equal to the default cap on one request body
22
+ * (`DEFAULT_MAX_BODY_SIZE`). An audit row is bounded by the request that
23
+ * produced it, so on a server that keeps that default the budget holds a burst
24
+ * of ordinary rows or one at the largest. The value is fixed and does not
25
+ * follow the server's body limit. Same value as the Rust spine's
26
+ * `SSE_OUTBOUND_QUEUE_BYTES`.
27
+ */
28
+ export declare const SSE_OUTBOUND_QUEUE_BYTES: number;
17
29
  //# sourceMappingURL=sse_constants.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sse_constants.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,oBAAoB,CAAC"}
1
+ {"version":3,"file":"sse_constants.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/realtime/sse_constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,oBAAoB,CAAC;AAEvD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,wBAAwB,QAAc,CAAC"}
@@ -14,3 +14,15 @@
14
14
  * SSE tests assert the stream emits this on connect.
15
15
  */
16
16
  export const SSE_CONNECTED_COMMENT = `: connected\n\n`;
17
+ /**
18
+ * The byte budget of what one SSE stream holds for its client to read
19
+ * (`realtime/sse.ts` module doc, "Outbound bound").
20
+ *
21
+ * 1 MiB — equal to the default cap on one request body
22
+ * (`DEFAULT_MAX_BODY_SIZE`). An audit row is bounded by the request that
23
+ * produced it, so on a server that keeps that default the budget holds a burst
24
+ * of ordinary rows or one at the largest. The value is fixed and does not
25
+ * follow the server's body limit. Same value as the Rust spine's
26
+ * `SSE_OUTBOUND_QUEUE_BYTES`.
27
+ */
28
+ export const SSE_OUTBOUND_QUEUE_BYTES = 1024 * 1024;
@@ -1 +1 @@
1
- {"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/app_server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"app_server.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/app_server.ts"],"names":[],"mappings":"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"}
@@ -9,7 +9,6 @@
9
9
  */
10
10
  import { Hono } from 'hono';
11
11
  import { logger } from 'hono/logger';
12
- import { bodyLimit } from 'hono/body-limit';
13
12
  import { z } from 'zod';
14
13
  import { session_cookie_options } from '../auth/session_cookie.js';
15
14
  import { create_audit_log_sse, create_sse_connection_closer, audit_log_event_specs } from '../realtime/sse_auth_guard.js';
@@ -29,7 +28,7 @@ import { flush_pending_effects, flush_post_commit_effects } from '../http/pendin
29
28
  import { assert_endpoint_in_auth_scope, assert_middleware_stack_mounted, create_auth_middleware_specs } from '../auth/middleware.js';
30
29
  import { fuz_auth_guard_resolver } from '../auth/auth_guard_resolver.js';
31
30
  import { create_fuz_authorization_handler } from '../auth/request_context.js';
32
- import { ERROR_PAYLOAD_TOO_LARGE } from '../http/error_schemas.js';
31
+ import { create_body_limit_middleware } from '../http/body_limit.js';
33
32
  import { create_rpc_endpoint } from '../actions/action_rpc.js';
34
33
  import { register_ws_endpoint } from '../actions/register_ws_endpoint.js';
35
34
  import { resolve_ws_endpoints } from '../actions/ws_endpoint_spec.js';
@@ -483,10 +482,7 @@ const assemble_app_server = async (options, releases) => {
483
482
  // Default 1 MiB; pass null to disable.
484
483
  if (options.max_body_size !== null) {
485
484
  const max_size = options.max_body_size ?? DEFAULT_MAX_BODY_SIZE;
486
- app.use(bodyLimit({
487
- maxSize: max_size,
488
- onError: (c) => c.json({ error: ERROR_PAYLOAD_TOO_LARGE }, 413)
489
- }));
485
+ app.use(create_body_limit_middleware(max_size));
490
486
  }
491
487
  apply_middleware_specs(app, middleware_specs);
492
488
  const authorize = create_fuz_authorization_handler({ db: deps.db });
@@ -524,6 +520,7 @@ const assemble_app_server = async (options, releases) => {
524
520
  // added below instead, so the remover is the server's
525
521
  connection_closer: null,
526
522
  max_connections_per_account: endpoint.max_connections_per_account,
523
+ outbound_queue_bytes: endpoint.outbound_queue_bytes,
527
524
  heartbeat: endpoint.heartbeat,
528
525
  artificial_delay: endpoint.artificial_delay,
529
526
  max_message_bytes: endpoint.max_message_bytes,
@@ -223,7 +223,7 @@ test-only by construction.
223
223
 
224
224
  ### `schema_introspect.ts` — `query_schema_snapshot`
225
225
 
226
- - `query_schema_snapshot(db, options?)` — introspects a live DB into a deterministic `SchemaSnapshot` via `pg_catalog` + `information_schema`. Captures tables, columns (with `udt_name` to distinguish int4/int8), indexes (`indexdef`), constraints (`pg_get_constraintdef`), sequences, and enum types (`pg_enum` labels in declared `enumsortorder`, so a `cell_visibility` label-set/order drift is gated). The `schema_version` migration tracker is always excluded — it's framework bookkeeping, not domain schema, and impls organize migration namespaces differently. Twinned by `fuz_db::query_schema_snapshot` (Rust); the `_testing_schema_snapshot` RPC's wire validator is the shared `SchemaSnapshot`, so the enum field must serialize on both sides.
226
+ - `query_schema_snapshot(db, options?)` — introspects a live DB into a deterministic `SchemaSnapshot` via `pg_catalog` + `information_schema`. Captures tables, columns (with `udt_name` to distinguish int4/int8), indexes (`indexdef`), constraints (`pg_get_constraintdef`), sequences, and enum types (`pg_enum` labels in declared `enumsortorder`, so a `cell_visibility` label-set/order drift is gated). The `schema_version` migration tracker is always excluded — it's framework bookkeeping, not domain schema; twin impls keep it identical (same namespaces, names and partitioning), which `query_migration_tracker` + `assert_migration_trackers_equal` gate separately. Twinned by `fuz_db::query_schema_snapshot` (Rust); the `_testing_schema_snapshot` RPC's wire validator is the shared `SchemaSnapshot`, so the enum field must serialize on both sides.
227
227
  - `SchemaSnapshot` — the Zod schema is canonical (co-located in `schema_introspect.ts`; the cross-impl `_testing_schema_snapshot` RPC action reuses it as its wire validator, and the type is `z.infer`'d from it). Fully JSON-serializable; every collection deterministically sorted on capture so structural equality is stable across runs.
228
228
  - `query_migration_tracker(db)` / `MigrationTracker` — the migration-_identity_ twin of the snapshot: reads the `schema_version` tracker rows (`[{namespace, name, sequence}]`) the snapshot deliberately _excludes_. `MigrationTracker` is the `_testing_migration_tracker` RPC's wire validator; pairs with `assert_migration_trackers_equal` to gate that the two spines record byte-identical migration identity (the swap-freely invariant), catching a migration-name or namespace-partitioning drift the provenance-agnostic schema snapshot can't see.
229
229
 
@@ -235,7 +235,7 @@ test-only by construction.
235
235
  - `SchemaDiff` — tagged-union per drift kind: `table_only_in`, `column_only_in`, `column_field_differs`, `index_only_in`, `index_definition_differs`, `constraint_only_in`, `constraint_differs`, `sequence_only_in`, `sequence_data_type_differs`, `enum_only_in`, `enum_labels_differ` (enum labels compared positionally — declared order is significant).
236
236
  - `diff_migration_trackers(a, b)` / `format_migration_tracker_diffs(diffs, labels?)` / `assert_migration_trackers_equal(a, b, labels?)` — the migration-identity twins of the snapshot diff/format/assert, over two `MigrationTracker`s. `MigrationTrackerDiff` is keyed on `(namespace, name)` (the `schema_version` PK): `tracker_row_only_in` (a migration rename or namespace re-partition) and `tracker_sequence_differs` (a re-order).
237
237
 
238
- fuz_app's own spine gates this cross-process via the `cross_backend_parity` project (`schema_parity.cross.test.ts` + the dual-spawn `global_setup_schema_parity.ts`), diffing the TS spine ↔ `testing_spine_stub` full schema — `npm run test:cross:parity`. The forge has its own deno↔rust parity gate.
238
+ fuz_app's own spine gates this cross-process via the `cross_backend_parity` project (`schema_parity.cross.test.ts` + the dual-spawn `global_setup_schema_parity.ts`), diffing the TS spine ↔ `testing_spine_stub` full schema — `npm run test:cross:parity`. Both legs run on PostgreSQL there: index definitions are compared as the engine renders them, and pglet (what `memory://` gives a TS backend) leaves sort direction, `NULLS` placement, operator classes and `NULLS NOT DISTINCT` out of `indexdef`, so a snapshot taken on it differs from a PostgreSQL one with identical DDL. The forge has its own deno↔rust parity gate.
239
239
 
240
240
  ## Cross-impl RPC-manifest parity
241
241
 
@@ -552,7 +552,7 @@ cover real sockets.
552
552
 
553
553
  Three layers:
554
554
 
555
- 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. `create_stub_upgrade()` stands in for a runtime adapter's `upgradeWebSocket`: it passes a request without `Upgrade: websocket` to `next()`, and records an upgrade request — its real `Context` and the endpoint's events factory — in `upgrades`, answering `200`. So a test can send `app.request('/api/ws', {headers: {upgrade: 'websocket', cookie | authorization, origin, 'x-forwarded-for'}})` through an assembled app's whole middleware chain and build the socket's events from the `Context` that chain produced (`src/test/server/create_app_server_ws_endpoints.dispatch.db.test.ts`); `get_create_events()` still returns the last mounted factory, for a hand-built context.
555
+ 1. **Primitives** — `create_fake_ws(options?)` (a real `WSContext` over capture arrays `sends` / `closes`, plus `socket` — the fake runtime socket behind `ws.raw`, shaped like Node's `ws`: a `bufferedAmount` the transport's outbound writer reads for backpressure, `stalled` (start it with `{stalled: true}`) to model a client that stopped reading, and `terminate` / `terminated` for the frameless cut; `readyState` reads open until closed or terminated), `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. `create_stub_upgrade()` stands in for a runtime adapter's `upgradeWebSocket`: it passes a request without `Upgrade: websocket` to `next()`, and records an upgrade request — its real `Context` and the endpoint's events factory — in `upgrades`, answering `200`. So a test can send `app.request('/api/ws', {headers: {upgrade: 'websocket', cookie | authorization, origin, 'x-forwarded-for'}})` through an assembled app's whole middleware chain and build the socket's events from the `Context` that chain produced (`src/test/server/create_app_server_ws_endpoints.dispatch.db.test.ts`); `get_create_events()` still returns the last mounted factory, for a hand-built context.
556
556
  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).
557
557
  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).
558
558
 
@@ -971,10 +971,13 @@ source of truth for wire-shape conformance.
971
971
  case — `GET /api/account/status` is bundled into `create_account_route_specs`,
972
972
  so every spine serves it (`true`); the gate fails loud if a declaring backend
973
973
  doesn't mount it (replacing the old runtime 404-sniff-skip).
974
- `oversized_reject_closes_connection` gates the strong half of the body-size
975
- smuggling probe (`true` Node/Deno/Rust — they close on an oversized reject;
976
- `false` Bun — `Bun.serve` drains + keepalives but frames correctly, so the
977
- suite's no-desync half still runs).
974
+ `oversized_reject_closes_connection` picks the arm of the body-size
975
+ smuggling probe (`true` Node/Deno/Rust — the connection closes after an
976
+ oversized-body 413, at once on Node and once the declared body has arrived
977
+ on Deno, so the pipelined request goes unanswered; `false` Bun —
978
+ `Bun.serve` keeps the connection and serves the next request, correctly
979
+ framed, so the suite requires no desync there instead: exactly the 413 and
980
+ the reply to the pipelined request, which a backend that closes fails).
978
981
  `ws_handshake_pipelining` gates the WS round-trip suite's
979
982
  frames-with-the-handshake case (`true` Node/Deno/Rust — frames written in the
980
983
  same TCP write as the upgrade request reach the socket; `false` Bun, whose
@@ -1218,6 +1221,36 @@ ws_path})`. The wire literals are independent copies of the producers'
1218
1221
  constants, so a value changed on one spine fails here. Cross-process only;
1219
1222
  fuz_app's own wiring is `src/test/cross_backend/ws_inbound_dispatch.cross.test.ts`.
1220
1223
 
1224
+ ### `cross_backend/ws_outbound_overflow.ts` — `describe_ws_outbound_overflow_cross_tests`
1225
+
1226
+ Real-upgrade parity for the **per-connection outbound bound** — the byte
1227
+ budget both spines hold a WebSocket's waiting frames to
1228
+ (`BackendWebsocketTransport`'s `outbound_queue_bytes`, the Rust `fuz_realtime`
1229
+ outbound queue; same default, same per-frame charge). The close code and its
1230
+ reason are wire contract — a client reads `WS_CLOSE_OUTBOUND_OVERFLOW` as "you
1231
+ fell behind, reconnect" — so this is the pin that runs on both.
1232
+ `describe_ws_outbound_overflow_cross_tests({setup_test, capabilities, base_url,
1233
+ ws_path, origin?, rpc_path?})` drives `_testing_flood_notifications`, which
1234
+ queues a burst onto the caller's own connection in one synchronous run, so the
1235
+ burst waits at once whether or not the client reads and the overflow is
1236
+ deterministic. Cases: a burst within the budget arrives whole and in order
1237
+ ahead of its response, the socket left open — a small one, and one of a few
1238
+ mebibytes in megabyte frames, most of the default budget, which waits on the backend's write pump where megabyte frames outrun the socket's buffer (Deno; Node and Bun write the burst through); a burst far past it closes the
1239
+ socket with the code and the exact reason, no response for the flooding request
1240
+ arrives, and any flood frames that did arrive are a contiguous prefix of the
1241
+ burst — while a second connection on the same account keeps dispatching; a
1242
+ burst past the action's own size limit is refused `invalid_params` with the
1243
+ socket left open; and, with `rpc_path`, the action over HTTP refuses
1244
+ `invalid_params` with reason `no_connection`. Not asserted, because each
1245
+ implementation decides them: how many frames accepted before the overflow
1246
+ reach the client, and the boundary a burst of tiny frames overflows at. The
1247
+ flood action is daemon-token-gated, so the flooding socket upgrades with the
1248
+ daemon token and no `Origin` (`create_ws_transport`'s `origin: null`). The wire
1249
+ literals are independent copies of the producers' constants. Gated on
1250
+ `capabilities.ws`; cross-process only, and only for a backend that mounts the
1251
+ flood action on its WS endpoint. fuz_app's own wiring is
1252
+ `src/test/cross_backend/ws_outbound_overflow.cross.test.ts`.
1253
+
1221
1254
  ### `cross_backend/ws_connection_cap.ts` — `describe_ws_connection_cap_cross_tests`
1222
1255
 
1223
1256
  Real-upgrade parity for the **per-account WebSocket connection cap** — the
@@ -1408,7 +1441,7 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
1408
1441
  request (`{result}` / `{error}` / `undefined` to swallow), the path
1409
1442
  `describe_peer_ping_ws_tests` drives. With no `on_request` the seam is inert
1410
1443
  (a server-initiated request flows to `messages` as before).
1411
- - `testing/transports/ws_transport.ts` — `create_ws_transport({base_url, ws_path, cookies, origin?, on_request?, headers?})`
1444
+ - `testing/transports/ws_transport.ts` — `create_ws_transport({base_url, ws_path, cookies, origin?, on_request?, headers?})` (`origin: null` sends no `Origin`, which a daemon-token upgrade needs)
1412
1445
  builds a real-upgrade WS client using the `ws` npm package (optional
1413
1446
  peerDep; consumers wiring cross-process tests `npm install --save-dev ws`).
1414
1447
  Threads the keeper cookie onto the upgrade so per-action auth succeeds on
@@ -1496,6 +1529,17 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
1496
1529
  emitter, so the shared suite body can call the barrier on every backend
1497
1530
  uniformly.
1498
1531
 
1532
+ Also bundled: `_testing_flood_notifications`
1533
+ (`create_testing_flood_notifications_action()`) — queues `count`
1534
+ notifications of `size` payload bytes each (method `_testing_flood`, params
1535
+ `{index, payload}`) onto the caller's own WebSocket connection in one
1536
+ synchronous run and answers `{queued: count}`, so a suite can push a
1537
+ connection's outbound queue past its byte budget from outside. Refused with
1538
+ `invalid_params` past `TESTING_FLOOD_MAX_COUNT`, `TESTING_FLOOD_MAX_SIZE`, or
1539
+ `TESTING_FLOOD_MAX_TOTAL_BYTES` (`count` × `size`), and over HTTP (reason
1540
+ `no_connection` — there is no connection to queue onto). The Rust mirror is
1541
+ `fuz_testing::create_testing_flood_notifications_action_spec`.
1542
+
1499
1543
  Also bundled: `_testing_mint_session` — mints a backdated-expiry
1500
1544
  `auth_session` row for an account (via `mint_test_session` in `app_server.ts`)
1501
1545
  and returns its signed cookie value (future-dated payload). Backs the
@@ -1601,6 +1645,22 @@ module's own tests (`account_status.test.ts`, `action_rpc.test.ts`,
1601
1645
  `serve_fact_route.db.test.ts`); fuz_app's own wiring is
1602
1646
  `cross_backend/query_shape.cross.test.ts`.
1603
1647
 
1648
+ ### Grant-verb role grammar parity — `cross_backend/role_grant_role_grammar.ts`
1649
+
1650
+ `describe_role_grant_role_grammar_cross_tests({setup_test, rpc_path?})` — an
1651
+ off-grammar `role` (uppercase, hyphen, digit, leading or trailing underscore,
1652
+ space, empty, or all lowercase and one character past the length cap) on `role_grant_assign` and `role_grant_offer_create` answers
1653
+ `invalid_params` with no `data.reason`, and after the refusals no
1654
+ `role_grant_create` / `role_grant_offer_create` audit row carries one of those
1655
+ strings. The control — an on-grammar role no registry holds, refused past
1656
+ validation with `role_not_web_grantable` — keeps the audit read from being
1657
+ vacuous, and the same refusal for a role exactly at the length cap pins that
1658
+ boundary. Exists because the action-manifest parity gate captures no param
1659
+ schemas while the Rust spine parses params by hand, and the grant verbs write
1660
+ the role they refuse into their failure rows. Ungated; cross-process only (it
1661
+ reads the audit log behind `_testing_drain_effects`). fuz_app's own wiring is
1662
+ `src/test/cross_backend/role_grant_role_grammar.cross.test.ts`.
1663
+
1604
1664
  ### API-token lifetime parity — `cross_backend/token_lifetime.ts`
1605
1665
 
1606
1666
  `describe_token_lifetime_cross_tests({setup_test, rpc_path?})` — the
@@ -1645,8 +1705,8 @@ drift-guarded by `src/test/cross_backend/spine_expected_schema.db.test.ts`
1645
1705
 
1646
1706
  `describe_testing_backdoor_cross_tests({setup_test, rpc_path?, ws?})` —
1647
1707
  the negative-credential parity suite for the `_testing_*` backdoor actions.
1648
- For each `_testing_*` action except `_testing_drain_effects` (the three
1649
- privileged writes plus the three introspection reads) it fires three principals over real HTTP: **anonymous** →
1708
+ For each `_testing_*` action except `_testing_drain_effects` (the privileged
1709
+ writes, the connection flood, and the introspection reads) it fires three principals over real HTTP: **anonymous** →
1650
1710
  401 `unauthenticated`, **session** → 403 `forbidden` `credential_type_required`,
1651
1711
  **bearer** → 403 `forbidden` `credential_type_required` — and, when `ws`
1652
1712
  (`{base_url, ws_path, origin?}`) is passed, sends each on a socket opened with
@@ -5,7 +5,7 @@ import './assert_dev_env.js';
5
5
  * `describe_standard_admin_integration_tests` creates a composable test suite
6
6
  * that exercises admin account listing, role_grant grant/revoke (via the RPC
7
7
  * surface — see `role_grant_offer_create` / `role_grant_revoke`), session/token
8
- * management, and audit log routes against a real PGlite database.
8
+ * management, and audit log routes against a real database.
9
9
  *
10
10
  * Consumers call it with their route factory, session config, role schema,
11
11
  * and RPC endpoint specs — all admin route tests come for free.
@@ -34,7 +34,7 @@ export interface AuditCompletenessTestOptions {
34
34
  * Composable audit log completeness test suite.
35
35
  *
36
36
  * Verifies that every auth mutation route produces the correct audit log
37
- * event type. Exercises routes via HTTP requests against a real PGlite
37
+ * event type. Exercises routes via HTTP requests against a real
38
38
  * database, then reads events back through the `audit_log_list` RPC
39
39
  * (the production observation path the admin UI consumes).
40
40
  *
@@ -96,7 +96,7 @@ const json_session_headers = (fixture, extra) => fixture.create_session_headers(
96
96
  * Composable audit log completeness test suite.
97
97
  *
98
98
  * Verifies that every auth mutation route produces the correct audit log
99
- * event type. Exercises routes via HTTP requests against a real PGlite
99
+ * event type. Exercises routes via HTTP requests against a real
100
100
  * database, then reads events back through the `audit_log_list` RPC
101
101
  * (the production observation path the admin UI consumes).
102
102
  *
@@ -36,7 +36,7 @@ export interface RecordingAuditEmitter {
36
36
  * Build a no-op `AuditEmitter` that records every `emit`, `emit_pool`, and
37
37
  * `emit_role_grant_target` call into `calls` as an `AuditLogInput`. Use to
38
38
  * capture audit metadata shapes in unit tests (e.g. password change failure
39
- * outcome, role-grant create denial) without standing up the full PGlite +
39
+ * outcome, role-grant create denial) without standing up a database and the
40
40
  * `query_audit_log` pipeline.
41
41
  *
42
42
  * **Capture scope — all four production fan-out shapes.**
@@ -34,7 +34,7 @@ export const install_audit_drift_guard = () => {
34
34
  * Build a no-op `AuditEmitter` that records every `emit`, `emit_pool`, and
35
35
  * `emit_role_grant_target` call into `calls` as an `AuditLogInput`. Use to
36
36
  * capture audit metadata shapes in unit tests (e.g. password change failure
37
- * outcome, role-grant create denial) without standing up the full PGlite +
37
+ * outcome, role-grant create denial) without standing up a database and the
38
38
  * `query_audit_log` pipeline.
39
39
  *
40
40
  * **Capture scope — all four production fan-out shapes.**
@@ -6,15 +6,15 @@ export interface BodySizeSmugglingCrossTestOptions {
6
6
  /** RPC endpoint path to target. Default `/api/rpc`. */
7
7
  readonly rpc_path?: string;
8
8
  /**
9
- * Whether the backend closes the connection on an oversized-body reject
10
- * without reading the body (`capabilities.oversized_reject_closes_connection`).
11
- * `true` (default) demands the strong posture — the pipelined GET is never
12
- * reached, so **at most one** response comes back. `false` (Bun) relaxes to
13
- * the no-desync property: the body is drained on `Content-Length` and the
14
- * pipelined GET is framed correctly, so **at most two** responses come back
15
- * and the body bytes are never reparsed as a request. Default `true` so a
16
- * consumer that forgets to declare the flag fails loud rather than silently
17
- * accepting a drain.
9
+ * Whether the backend closes the connection after an oversized-body `413`
10
+ * (`capabilities.oversized_reject_closes_connection`). `true` (default)
11
+ * requires that the pipelined GET goes unanswered — at most one response.
12
+ * `false` (Bun) requires no desync instead: exactly two well-formed
13
+ * responses, the 413 and one matching what the backend answers that GET
14
+ * sent alone — so `rpc_path` must answer a bare `GET` with a stable status
15
+ * and body, and a backend that closes after the 413 fails. Default `true`
16
+ * so a consumer that leaves the flag out fails loud on a backend that
17
+ * keeps the connection.
18
18
  */
19
19
  readonly closes_connection?: boolean;
20
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"body_size_smuggling.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size_smuggling.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAwD9B,4EAA4E;AAC5E,MAAM,WAAW,iCAAiC;IACjD,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CACrC;AAqDD,eAAO,MAAM,wCAAwC,GACpD,SAAS,iCAAiC,KACxC,IAiFF,CAAC"}
1
+ {"version":3,"file":"body_size_smuggling.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size_smuggling.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAgE9B,4EAA4E;AAC5E,MAAM,WAAW,iCAAiC;IACjD,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,OAAO,CAAC;CACrC;AAkGD,eAAO,MAAM,wCAAwC,GACpD,SAAS,iCAAiC,KACxC,IA0GF,CAAC"}