@fuzdev/fuz_app 0.134.0 → 0.136.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 (118) hide show
  1. package/dist/actions/CLAUDE.md +93 -11
  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 +133 -10
  9. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  10. package/dist/actions/transports_ws_backend.js +237 -34
  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 +10 -4
  17. package/dist/auth/account_action_specs.d.ts +7 -0
  18. package/dist/auth/account_action_specs.d.ts.map +1 -1
  19. package/dist/auth/account_action_specs.js +11 -1
  20. package/dist/auth/audit_emitter.d.ts +4 -0
  21. package/dist/auth/audit_emitter.d.ts.map +1 -1
  22. package/dist/auth/audit_emitter.js +18 -2
  23. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  24. package/dist/auth/audit_log_routes.js +3 -1
  25. package/dist/auth/cell_action_specs.d.ts +10 -0
  26. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  27. package/dist/auth/cell_action_specs.js +17 -3
  28. package/dist/auth/cell_actions.js +1 -1
  29. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  30. package/dist/auth/role_grant_offer_action_specs.js +7 -2
  31. package/dist/auth/role_schema.d.ts +14 -3
  32. package/dist/auth/role_schema.d.ts.map +1 -1
  33. package/dist/auth/role_schema.js +14 -2
  34. package/dist/auth/scope_kind_schema.d.ts +10 -0
  35. package/dist/auth/scope_kind_schema.d.ts.map +1 -1
  36. package/dist/auth/scope_kind_schema.js +11 -0
  37. package/dist/db/migrate.d.ts +1 -1
  38. package/dist/db/migrate.js +2 -2
  39. package/dist/env/load.d.ts +1 -1
  40. package/dist/env/load.js +1 -1
  41. package/dist/http/CLAUDE.md +2 -1
  42. package/dist/http/body_limit.d.ts +74 -0
  43. package/dist/http/body_limit.d.ts.map +1 -0
  44. package/dist/http/body_limit.js +100 -0
  45. package/dist/http/db_routes.d.ts.map +1 -1
  46. package/dist/http/db_routes.js +5 -3
  47. package/dist/http/error_schemas.d.ts +2 -0
  48. package/dist/http/error_schemas.d.ts.map +1 -1
  49. package/dist/http/error_schemas.js +2 -0
  50. package/dist/realtime/outbound_budget.d.ts +91 -0
  51. package/dist/realtime/outbound_budget.d.ts.map +1 -0
  52. package/dist/realtime/outbound_budget.js +108 -0
  53. package/dist/realtime/sse.d.ts +36 -3
  54. package/dist/realtime/sse.d.ts.map +1 -1
  55. package/dist/realtime/sse.js +58 -6
  56. package/dist/realtime/sse_constants.d.ts +12 -0
  57. package/dist/realtime/sse_constants.d.ts.map +1 -1
  58. package/dist/realtime/sse_constants.js +12 -0
  59. package/dist/server/app_server.d.ts.map +1 -1
  60. package/dist/server/app_server.js +3 -6
  61. package/dist/testing/CLAUDE.md +96 -19
  62. package/dist/testing/admin_integration.js +1 -1
  63. package/dist/testing/audit_completeness.d.ts +1 -1
  64. package/dist/testing/audit_completeness.js +1 -1
  65. package/dist/testing/audit_drift_guard.d.ts +19 -6
  66. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  67. package/dist/testing/audit_drift_guard.js +25 -9
  68. package/dist/testing/cross_backend/body_size.d.ts +18 -5
  69. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  70. package/dist/testing/cross_backend/body_size.js +233 -34
  71. package/dist/testing/cross_backend/body_size_smuggling.d.ts +0 -12
  72. package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
  73. package/dist/testing/cross_backend/body_size_smuggling.js +103 -108
  74. package/dist/testing/cross_backend/capabilities.d.ts +3 -21
  75. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  76. package/dist/testing/cross_backend/capabilities.js +1 -3
  77. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  78. package/dist/testing/cross_backend/cell_crud.js +31 -2
  79. package/dist/testing/cross_backend/conformance_case.d.ts +3 -3
  80. package/dist/testing/cross_backend/credential_header_robustness.d.ts.map +1 -1
  81. package/dist/testing/cross_backend/credential_header_robustness.js +18 -54
  82. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  83. package/dist/testing/cross_backend/default_backend_configs.js +2 -8
  84. package/dist/testing/cross_backend/role_grant_role_grammar.d.ts +6 -0
  85. package/dist/testing/cross_backend/role_grant_role_grammar.d.ts.map +1 -0
  86. package/dist/testing/cross_backend/role_grant_role_grammar.js +117 -0
  87. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +2 -3
  88. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  89. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +2 -3
  90. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  91. package/dist/testing/cross_backend/testing_backdoor.js +10 -4
  92. package/dist/testing/cross_backend/testing_reset_actions.d.ts +87 -2
  93. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  94. package/dist/testing/cross_backend/testing_reset_actions.js +106 -2
  95. package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -1
  96. package/dist/testing/cross_backend/token_lifetime.js +34 -1
  97. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  98. package/dist/testing/cross_backend/ts_spine_backend_config.js +8 -13
  99. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts +29 -0
  100. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -0
  101. package/dist/testing/cross_backend/ws_outbound_overflow.js +276 -0
  102. package/dist/testing/integration.js +1 -1
  103. package/dist/testing/schema_introspect.d.ts +30 -2
  104. package/dist/testing/schema_introspect.d.ts.map +1 -1
  105. package/dist/testing/schema_introspect.js +38 -7
  106. package/dist/testing/transports/raw_http.d.ts +78 -0
  107. package/dist/testing/transports/raw_http.d.ts.map +1 -0
  108. package/dist/testing/transports/raw_http.js +141 -0
  109. package/dist/testing/transports/ws_transport.d.ts +4 -2
  110. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  111. package/dist/testing/transports/ws_transport.js +2 -1
  112. package/dist/testing/ws_round_trip.d.ts +32 -2
  113. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  114. package/dist/testing/ws_round_trip.js +26 -4
  115. package/package.json +1 -1
  116. package/dist/env/resolve.d.ts +0 -163
  117. package/dist/env/resolve.d.ts.map +0 -1
  118. package/dist/env/resolve.js +0 -267
@@ -8,10 +8,12 @@ import '../assert_dev_env.js';
8
8
  * expired-by-construction server-side session for the expiry conformance
9
9
  * cases), **`_testing_put_fact`** (seed an embedded fact for the fact-serving
10
10
  * suite), **`_testing_schema_snapshot`** (introspect the live schema for
11
- * cross-impl parity diffing against a Rust backend's `fuz_db` snapshot), and
11
+ * cross-impl parity diffing against a Rust backend's `fuz_db` snapshot),
12
12
  * **`_testing_migration_tracker`** (dump the `schema_version` tracker rows for
13
13
  * cross-impl migration-identity parity — the provenance half the snapshot gate
14
- * excludes by design).
14
+ * excludes by design), and **`_testing_flood_notifications`** (queue a burst
15
+ * of notifications onto the caller's own WebSocket connection, to push its
16
+ * outbound queue past its byte budget).
15
17
  *
16
18
  * A further daemon-token action, **`_testing_action_manifest`** (dump the live
17
19
  * RPC registry for cross-impl method-set + auth-shape parity), lives here too
@@ -291,6 +293,84 @@ export declare const testing_put_fact_action_spec: {
291
293
  readonly async: true;
292
294
  readonly description: string;
293
295
  };
296
+ /** The wire method of each notification `_testing_flood_notifications` queues. */
297
+ export declare const TESTING_FLOOD_NOTIFICATION_METHOD = "_testing_flood";
298
+ /**
299
+ * The most notifications one `_testing_flood_notifications` call queues.
300
+ * Empty ones at this count are charged more than
301
+ * `DEFAULT_WS_OUTBOUND_QUEUE_BYTES` between them — each frame's envelope and
302
+ * its overhead — so a burst of tiny frames can overflow a connection on a
303
+ * default transport too. Same value as the Rust spine's.
304
+ */
305
+ export declare const TESTING_FLOOD_MAX_COUNT = 65536;
306
+ /** The largest `payload` one `_testing_flood_notifications` call builds, in bytes. */
307
+ export declare const TESTING_FLOOD_MAX_SIZE: number;
308
+ /**
309
+ * The most payload bytes one `_testing_flood_notifications` call builds across
310
+ * all its notifications — the bound on `count` × `size`. The handler
311
+ * serializes each notification without yielding, so the product is how long
312
+ * one call holds the event loop.
313
+ */
314
+ export declare const TESTING_FLOOD_MAX_TOTAL_BYTES: number;
315
+ /** The `data.reason` of the refusal `_testing_flood_notifications` gives a call over HTTP. */
316
+ export declare const ERROR_TESTING_FLOOD_NO_CONNECTION = "no_connection";
317
+ /**
318
+ * `_testing_flood_notifications` — queue a burst of notifications onto the
319
+ * **caller's own WebSocket connection**, all in one synchronous run, so a suite
320
+ * driving the server from outside can push a connection's outbound queue past
321
+ * its byte budget deterministically — without a second producer, and whether
322
+ * or not its client is reading: the connection's write pump gets no turn while
323
+ * the handler runs, so the burst waits at once. Past the budget the server
324
+ * closes the socket with `WS_CLOSE_OUTBOUND_OVERFLOW`.
325
+ *
326
+ * Each notification is
327
+ * `{"jsonrpc":"2.0","method":"_testing_flood","params":{"index":<i>,"payload":"<size × 'x'>"}}`
328
+ * with `index` counting from 0, so every frame is a little longer than `size`
329
+ * bytes and a client can tell which frames reached it.
330
+ *
331
+ * The result is `{queued: count}` — the number asked for, not the number that
332
+ * fit: when the burst overflows the queue the connection is closed and this
333
+ * response is never written. A burst that fits arrives in order, ahead of the
334
+ * response.
335
+ *
336
+ * A call past `TESTING_FLOOD_MAX_COUNT`, `TESTING_FLOOD_MAX_SIZE`, or
337
+ * `TESTING_FLOOD_MAX_TOTAL_BYTES` (`count` × `size`) is refused with
338
+ * `invalid_params` and queues nothing. Over HTTP there is no connection to
339
+ * queue onto, so the call is refused with `invalid_params`, reason
340
+ * `ERROR_TESTING_FLOOD_NO_CONNECTION`, rather than answering as though it had
341
+ * flooded something.
342
+ *
343
+ * `auth` gates on the daemon-token credential, matching `_testing_reset`. The
344
+ * Rust mirror is `fuz_testing::create_testing_flood_notifications_action_spec`.
345
+ */
346
+ export declare const testing_flood_notifications_action_spec: {
347
+ readonly method: "_testing_flood_notifications";
348
+ readonly kind: "request_response";
349
+ readonly initiator: "frontend";
350
+ readonly auth: {
351
+ readonly account: "required";
352
+ readonly actor: "none";
353
+ readonly credential_types: readonly ["daemon_token"];
354
+ };
355
+ readonly side_effects: false;
356
+ readonly input: z.ZodObject<{
357
+ count: z.ZodNumber;
358
+ size: z.ZodNumber;
359
+ }, z.core.$strict>;
360
+ readonly output: z.ZodObject<{
361
+ queued: z.ZodNumber;
362
+ }, z.core.$strict>;
363
+ readonly async: true;
364
+ readonly error_reasons: readonly ["no_connection"];
365
+ readonly description: string;
366
+ };
367
+ /**
368
+ * Build the standalone `_testing_flood_notifications` action (see the spec
369
+ * doc). The spine binary bundles it via `create_testing_actions`; mount it
370
+ * directly on an in-process WS endpoint whose suite drives an outbound
371
+ * overflow.
372
+ */
373
+ export declare const create_testing_flood_notifications_action: () => RpcAction;
294
374
  /**
295
375
  * `_testing_schema_snapshot` — introspect the live database into a normalized
296
376
  * `SchemaSnapshot` for cross-impl parity diffing. The cross-backend harness
@@ -299,6 +379,11 @@ export declare const testing_put_fact_action_spec: {
299
379
  * shapes match by design). Optional `exclude_tables` drops documented
300
380
  * divergences from both sides before comparison.
301
381
  *
382
+ * A backend running on pglet (a TS backend config's `memory://` default)
383
+ * answers with an error when its schema holds an index: the snapshot exists
384
+ * to be compared, and pglet's index definitions omit the modifiers a
385
+ * comparison needs (`query_schema_snapshot`). Run the backend on PostgreSQL.
386
+ *
302
387
  * `auth` gates on the daemon-token credential, matching `_testing_reset`.
303
388
  */
304
389
  export declare const testing_schema_snapshot_action_spec: {
@@ -1 +1 @@
1
- {"version":3,"file":"testing_reset_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_reset_actions.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmEG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,6BAA6B,CAAC;AAMzE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AAoCzC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;QAiBpC;;;;;;;;WAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWyC,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;CAWA,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;CAwBC,CAAC;AAE/C;;;;;;;;GAQG;AACH,eAAO,MAAM,mCAAmC,GAAI,OAAO,YAAY,KAAG,SAIvE,CAAC;AAEJ;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;CAeK,CAAC;AAE/C;;;;;;;;;GASG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWF,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,QAAO,SAGvD,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qCAAqC;;;;;;;;;;;;;;;;;;;;CAYJ,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,uCAAuC,QAAO,SAGzD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAaF,CAAC;AAkB/C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,qCAAqC,GACjD,SAAS,aAAa,CAAC,SAAS,CAAC,KAC/B,SAMF,CAAC;AAEF,4CAA4C;AAC5C,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACjD;;;;;;OAMG;IACH,QAAQ,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;IAC9C;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACxD;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,SAAS,2BAA2B,KAClC,KAAK,CAAC,SAAS,CA4IjB,CAAC"}
1
+ {"version":3,"file":"testing_reset_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_reset_actions.ts"],"names":[],"mappings":"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"}
@@ -8,10 +8,12 @@ import '../assert_dev_env.js';
8
8
  * expired-by-construction server-side session for the expiry conformance
9
9
  * cases), **`_testing_put_fact`** (seed an embedded fact for the fact-serving
10
10
  * suite), **`_testing_schema_snapshot`** (introspect the live schema for
11
- * cross-impl parity diffing against a Rust backend's `fuz_db` snapshot), and
11
+ * cross-impl parity diffing against a Rust backend's `fuz_db` snapshot),
12
12
  * **`_testing_migration_tracker`** (dump the `schema_version` tracker rows for
13
13
  * cross-impl migration-identity parity — the provenance half the snapshot gate
14
- * excludes by design).
14
+ * excludes by design), and **`_testing_flood_notifications`** (queue a burst
15
+ * of notifications onto the caller's own WebSocket connection, to push its
16
+ * outbound queue past its byte budget).
15
17
  *
16
18
  * A further daemon-token action, **`_testing_action_manifest`** (dump the live
17
19
  * RPC registry for cross-impl method-set + auth-shape parity), lives here too
@@ -72,6 +74,7 @@ import { Uuid } from '@fuzdev/fuz_util/id.ts';
72
74
  import { fact_hash_bytes } from '@fuzdev/fuz_util/fact_hash.ts';
73
75
  import { FactHashSchema } from '@fuzdev/fuz_util/hash_schemas.ts';
74
76
  import { rpc_action } from '../../actions/action_rpc.js';
77
+ import { jsonrpc_errors } from '../../http/jsonrpc_errors.js';
75
78
  import { emit_after_commit } from '../../http/pending_effects.js';
76
79
  import { protocol_action_specs } from '../../actions/protocol.js';
77
80
  import { query_put_fact } from '../../db/fact_queries.js';
@@ -291,6 +294,101 @@ export const testing_put_fact_action_spec = {
291
294
  description: 'Test-binary only — seed an embedded fact (blake3 of the UTF-8 content) and return its hash, ' +
292
295
  'so the cross-process fact-serving suite can store bytes without a DB handle.'
293
296
  };
297
+ /** The wire method of each notification `_testing_flood_notifications` queues. */
298
+ export const TESTING_FLOOD_NOTIFICATION_METHOD = '_testing_flood';
299
+ /**
300
+ * The most notifications one `_testing_flood_notifications` call queues.
301
+ * Empty ones at this count are charged more than
302
+ * `DEFAULT_WS_OUTBOUND_QUEUE_BYTES` between them — each frame's envelope and
303
+ * its overhead — so a burst of tiny frames can overflow a connection on a
304
+ * default transport too. Same value as the Rust spine's.
305
+ */
306
+ export const TESTING_FLOOD_MAX_COUNT = 65_536;
307
+ /** The largest `payload` one `_testing_flood_notifications` call builds, in bytes. */
308
+ export const TESTING_FLOOD_MAX_SIZE = 16 * 1024 * 1024;
309
+ /**
310
+ * The most payload bytes one `_testing_flood_notifications` call builds across
311
+ * all its notifications — the bound on `count` × `size`. The handler
312
+ * serializes each notification without yielding, so the product is how long
313
+ * one call holds the event loop.
314
+ */
315
+ export const TESTING_FLOOD_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
316
+ /** The `data.reason` of the refusal `_testing_flood_notifications` gives a call over HTTP. */
317
+ export const ERROR_TESTING_FLOOD_NO_CONNECTION = 'no_connection';
318
+ /**
319
+ * `_testing_flood_notifications` — queue a burst of notifications onto the
320
+ * **caller's own WebSocket connection**, all in one synchronous run, so a suite
321
+ * driving the server from outside can push a connection's outbound queue past
322
+ * its byte budget deterministically — without a second producer, and whether
323
+ * or not its client is reading: the connection's write pump gets no turn while
324
+ * the handler runs, so the burst waits at once. Past the budget the server
325
+ * closes the socket with `WS_CLOSE_OUTBOUND_OVERFLOW`.
326
+ *
327
+ * Each notification is
328
+ * `{"jsonrpc":"2.0","method":"_testing_flood","params":{"index":<i>,"payload":"<size × 'x'>"}}`
329
+ * with `index` counting from 0, so every frame is a little longer than `size`
330
+ * bytes and a client can tell which frames reached it.
331
+ *
332
+ * The result is `{queued: count}` — the number asked for, not the number that
333
+ * fit: when the burst overflows the queue the connection is closed and this
334
+ * response is never written. A burst that fits arrives in order, ahead of the
335
+ * response.
336
+ *
337
+ * A call past `TESTING_FLOOD_MAX_COUNT`, `TESTING_FLOOD_MAX_SIZE`, or
338
+ * `TESTING_FLOOD_MAX_TOTAL_BYTES` (`count` × `size`) is refused with
339
+ * `invalid_params` and queues nothing. Over HTTP there is no connection to
340
+ * queue onto, so the call is refused with `invalid_params`, reason
341
+ * `ERROR_TESTING_FLOOD_NO_CONNECTION`, rather than answering as though it had
342
+ * flooded something.
343
+ *
344
+ * `auth` gates on the daemon-token credential, matching `_testing_reset`. The
345
+ * Rust mirror is `fuz_testing::create_testing_flood_notifications_action_spec`.
346
+ */
347
+ export const testing_flood_notifications_action_spec = {
348
+ method: '_testing_flood_notifications',
349
+ kind: 'request_response',
350
+ initiator: 'frontend',
351
+ auth: TESTING_ACTION_AUTH,
352
+ side_effects: false,
353
+ input: z.strictObject({
354
+ count: z.number().int().min(0).max(TESTING_FLOOD_MAX_COUNT).meta({
355
+ description: 'How many notifications to queue.'
356
+ }),
357
+ size: z.number().int().min(0).max(TESTING_FLOOD_MAX_SIZE).meta({
358
+ description: "The byte length of each notification's `payload` string."
359
+ })
360
+ }),
361
+ output: z.strictObject({
362
+ queued: z.number().int().meta({
363
+ description: 'The number of notifications asked for — not the number that fit.'
364
+ })
365
+ }),
366
+ async: true,
367
+ error_reasons: [ERROR_TESTING_FLOOD_NO_CONNECTION],
368
+ description: "Test-binary only — queue a burst of notifications onto the caller's own WebSocket " +
369
+ 'connection in one synchronous run, to push its outbound queue past its byte budget.'
370
+ };
371
+ /**
372
+ * Build the standalone `_testing_flood_notifications` action (see the spec
373
+ * doc). The spine binary bundles it via `create_testing_actions`; mount it
374
+ * directly on an in-process WS endpoint whose suite drives an outbound
375
+ * overflow.
376
+ */
377
+ export const create_testing_flood_notifications_action = () =>
378
+ // not `async`: the burst must be queued in one synchronous run
379
+ rpc_action(testing_flood_notifications_action_spec, ({ count, size }, ctx) => {
380
+ if (count * size > TESTING_FLOOD_MAX_TOTAL_BYTES) {
381
+ throw jsonrpc_errors.invalid_params(`count * size must be at most ${TESTING_FLOOD_MAX_TOTAL_BYTES}`);
382
+ }
383
+ if (ctx.connection_id === undefined) {
384
+ throw jsonrpc_errors.invalid_params('_testing_flood_notifications needs a WebSocket connection', { reason: ERROR_TESTING_FLOOD_NO_CONNECTION });
385
+ }
386
+ const payload = 'x'.repeat(size);
387
+ for (let index = 0; index < count; index++) {
388
+ ctx.notify(TESTING_FLOOD_NOTIFICATION_METHOD, { index, payload });
389
+ }
390
+ return { queued: count };
391
+ });
294
392
  /**
295
393
  * `_testing_schema_snapshot` — introspect the live database into a normalized
296
394
  * `SchemaSnapshot` for cross-impl parity diffing. The cross-backend harness
@@ -299,6 +397,11 @@ export const testing_put_fact_action_spec = {
299
397
  * shapes match by design). Optional `exclude_tables` drops documented
300
398
  * divergences from both sides before comparison.
301
399
  *
400
+ * A backend running on pglet (a TS backend config's `memory://` default)
401
+ * answers with an error when its schema holds an index: the snapshot exists
402
+ * to be compared, and pglet's index definitions omit the modifiers a
403
+ * comparison needs (`query_schema_snapshot`). Run the backend on PostgreSQL.
404
+ *
302
405
  * `auth` gates on the daemon-token credential, matching `_testing_reset`.
303
406
  */
304
407
  export const testing_schema_snapshot_action_spec = {
@@ -555,6 +658,7 @@ export const create_testing_actions = (deps, options) => {
555
658
  return { hash };
556
659
  }),
557
660
  create_testing_drain_effects_action(deps.audit),
661
+ create_testing_flood_notifications_action(),
558
662
  create_testing_schema_snapshot_action(),
559
663
  create_testing_migration_tracker_action()
560
664
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"token_lifetime.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/token_lifetime.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA4C9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC7C,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAaD,eAAO,MAAM,mCAAmC,GAC/C,SAAS,6BAA6B,KACpC,IAsHF,CAAC"}
1
+ {"version":3,"file":"token_lifetime.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/token_lifetime.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAkD9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5C;;;;GAIG;AACH,MAAM,WAAW,6BAA6B;IAC7C,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAaD,eAAO,MAAM,mCAAmC,GAC/C,SAAS,6BAA6B,KACpC,IA6KF,CAAC"}
@@ -27,11 +27,16 @@ import '../assert_dev_env.js';
27
27
  * pin on the silently-ignoring-unknown-shapes hazard.
28
28
  * - **Out-of-range days are refused** — `days: 0` 400s identically, so the
29
29
  * two hand-rolled bounds checks can't drift.
30
+ * - **The `name` cap** — the same blind spot on another field of the same
31
+ * mint: a name of exactly `API_TOKEN_NAME_LENGTH_MAX` UTF-16 code units
32
+ * mints and reads back whole, and one unit past it is `invalid_params`,
33
+ * with an astral-plane name at each side of the boundary so the two spines
34
+ * count the same unit.
30
35
  *
31
36
  * @module
32
37
  */
33
38
  import { describe, test, assert } from 'vitest';
34
- import { account_token_create_action_spec, account_token_list_action_spec, TokenCreateOutput, TokenListOutput } from '../../auth/account_action_specs.js';
39
+ import { account_token_create_action_spec, account_token_list_action_spec, API_TOKEN_NAME_LENGTH_MAX, TokenCreateOutput, TokenListOutput } from '../../auth/account_action_specs.js';
35
40
  import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
36
41
  import { assert_iso8601_seconds } from './wire_shapes.js';
37
42
  import { SPINE_RPC_PATH } from './spine_surface_constants.js';
@@ -97,5 +102,33 @@ export const describe_token_lifetime_cross_tests = (options) => {
97
102
  assert.ok(!res.ok, 'days: 0 must be refused');
98
103
  assert.strictEqual(res.error?.code, -32602, `expected invalid_params: ${JSON.stringify(res.error)}`);
99
104
  });
105
+ test('a name at the length cap mints and reads back; one past it is invalid_params', async () => {
106
+ const fixture = await setup_test();
107
+ const astral = '\u{1F600}';
108
+ // the cap is odd: whole astral characters, plus one unit to land on it
109
+ const astral_at_cap = astral.repeat((API_TOKEN_NAME_LENGTH_MAX - 1) / 2) + 'a';
110
+ const mint = (name) => cross_rpc_call(fixture.transport, rpc_path, account_token_create_action_spec.method, { name, scope: { kind: 'full' }, lifetime: { kind: 'eternal' } }, fixture.create_session_headers());
111
+ const admitted = ['a'.repeat(API_TOKEN_NAME_LENGTH_MAX), astral_at_cap];
112
+ const minted_ids = [];
113
+ for (const name of admitted) {
114
+ assert.strictEqual(name.length, API_TOKEN_NAME_LENGTH_MAX);
115
+ minted_ids.push(expect_output(await mint(name), TokenCreateOutput).id);
116
+ }
117
+ const listed = expect_output(await cross_rpc_call(fixture.transport, rpc_path, account_token_list_action_spec.method, undefined, fixture.create_session_headers()), TokenListOutput);
118
+ for (const [index, id] of minted_ids.entries()) {
119
+ assert.strictEqual(listed.tokens.find((t) => t.id === id)?.name, admitted[index], 'a name at the cap must be stored whole');
120
+ }
121
+ for (const name of [
122
+ 'a'.repeat(API_TOKEN_NAME_LENGTH_MAX + 1),
123
+ // one astral character past the cap: two units over an even count
124
+ astral.repeat((API_TOKEN_NAME_LENGTH_MAX + 1) / 2),
125
+ astral_at_cap + 'a'
126
+ ]) {
127
+ const res = await mint(name);
128
+ assert.ok(!res.ok, `a ${name.length}-unit name must be refused`);
129
+ assert.strictEqual(res.error?.code, -32602, `expected invalid_params for a ${name.length}-unit name, got ${JSON.stringify(res.error)}`);
130
+ assert.strictEqual(error_reason(res), undefined, 'a schema refusal carries no reason');
131
+ }
132
+ });
100
133
  });
101
134
  };
@@ -1 +1 @@
1
- {"version":3,"file":"ts_spine_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ts_spine_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAsDzD,6FAA6F;AAC7F,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,gDAAgD;AAChD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,uDAAuD,CAAC;AAEvF,MAAM,WAAW,2BAA2B;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACjD;AAWD;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAoBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAoBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAiCF,CAAC"}
1
+ {"version":3,"file":"ts_spine_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ts_spine_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAiDzD,6FAA6F;AAC7F,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,gDAAgD;AAChD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,uDAAuD,CAAC;AAEvF,MAAM,WAAW,2BAA2B;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACjD;AAWD;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAoBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAoBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAiCF,CAAC"}
@@ -22,23 +22,18 @@ const ts_spine_capabilities = Object.freeze({
22
22
  });
23
23
  /**
24
24
  * Capabilities for the **Bun** spine binary — `ts_spine_capabilities` with
25
- * `oversized_reject_closes_connection: false` and
26
- * `ws_handshake_pipelining: false`. `Bun.serve` drains the declared
27
- * `Content-Length` of an oversized-body `413` reject and keeps the socket
28
- * alive (processing the correctly-framed pipelined request) even when the
29
- * response carries `Connection: close`, unlike `@hono/node-server` / Deno /
30
- * hyper, which close. Bun is not insecure — it frames on `Content-Length`, so
31
- * there is no desync — but the smuggling suite's strong "connection closes"
32
- * assertion doesn't hold; this flag routes Bun onto the suite's no-desync arm.
33
- * See `docs/security.md` §"Body Size Limiting".
25
+ * `ws_handshake_pipelining: false`.
34
26
  *
35
- * `Bun.serve` also answers `400` to an upgrade request that carries WebSocket
36
- * frames in the same TCP write, before any handler runs, so the WS suite's
37
- * frames-with-the-handshake case has nothing to drive there.
27
+ * `Bun.serve` answers `400` to an upgrade request that carries WebSocket
28
+ * frames in the same TCP write when the handler upgrades after an await that
29
+ * yields to the event loop — a timer, I/O; the spine's does, behind the auth
30
+ * middleware's database reads — so the WS suite's frames-with-the-handshake
31
+ * case has nothing to drive there (observed on Bun 1.4.3; an upgrade made
32
+ * synchronously in `fetch`, or after an await that settles in the same
33
+ * microtask run, takes the frames).
38
34
  */
39
35
  const ts_spine_bun_capabilities = Object.freeze({
40
36
  ...ts_spine_capabilities,
41
- oversized_reject_closes_connection: false,
42
37
  ws_handshake_pipelining: false
43
38
  });
44
39
  /** Default port for the Node TS spine binary — slots beside the Rust `spine_stub` (1177). */
@@ -0,0 +1,29 @@
1
+ import '../assert_dev_env.js';
2
+ import { type BackendCapabilities } from './capabilities.js';
3
+ import type { SetupTest } from './setup.js';
4
+ /** Options for the outbound-overflow parity suite. */
5
+ export interface WsOutboundOverflowCrossTestOptions {
6
+ /** Per-test fixture producer (`default_cross_process_setup(handle)`). */
7
+ readonly setup_test: SetupTest;
8
+ /** Backend capability flags; every WebSocket case gates on `capabilities.ws`. */
9
+ readonly capabilities: BackendCapabilities;
10
+ /** Base URL the backend is reachable at (e.g. `http://localhost:1178`). */
11
+ readonly base_url: string;
12
+ /** WebSocket endpoint path on the backend (e.g. `/api/ws`). */
13
+ readonly ws_path: string;
14
+ /** Origin for the session socket's upgrade. Defaults to `base_url`. */
15
+ readonly origin?: string;
16
+ /**
17
+ * The backend's RPC endpoint path, for the over-HTTP refusal. Omitted, that
18
+ * case is skipped.
19
+ */
20
+ readonly rpc_path?: string;
21
+ }
22
+ /**
23
+ * Register the outbound-overflow parity suite over a real upgrade: a burst
24
+ * within the budget arrives whole, one past it closes the socket with
25
+ * `WS_CLOSE_OUTBOUND_OVERFLOW` and no response, and the account's other
26
+ * connection is unaffected.
27
+ */
28
+ export declare const describe_ws_outbound_overflow_cross_tests: (options: WsOutboundOverflowCrossTestOptions) => void;
29
+ //# sourceMappingURL=ws_outbound_overflow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws_outbound_overflow.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_outbound_overflow.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAwE9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,YAAY,CAAC;AAoFzD,sDAAsD;AACtD,MAAM,WAAW,kCAAkC;IAClD,yEAAyE;IACzE,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED;;;;;GAKG;AACH,eAAO,MAAM,yCAAyC,GACrD,SAAS,kCAAkC,KACzC,IA0NF,CAAC"}