@fuzdev/fuz_app 0.135.0 → 0.137.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 (104) hide show
  1. package/dist/actions/CLAUDE.md +2 -2
  2. package/dist/actions/action_spec.d.ts +0 -48
  3. package/dist/actions/action_spec.d.ts.map +1 -1
  4. package/dist/actions/transports_ws_backend.d.ts +1 -1
  5. package/dist/actions/transports_ws_backend.js +1 -1
  6. package/dist/auth/CLAUDE.md +8 -2
  7. package/dist/auth/account_action_specs.d.ts +7 -0
  8. package/dist/auth/account_action_specs.d.ts.map +1 -1
  9. package/dist/auth/account_action_specs.js +12 -3
  10. package/dist/auth/account_schema.d.ts +1 -6
  11. package/dist/auth/account_schema.d.ts.map +1 -1
  12. package/dist/auth/account_schema.js +1 -0
  13. package/dist/auth/actor_search_action_specs.d.ts +1 -1
  14. package/dist/auth/actor_search_action_specs.d.ts.map +1 -1
  15. package/dist/auth/actor_search_action_specs.js +3 -6
  16. package/dist/auth/audit_emitter.d.ts +4 -0
  17. package/dist/auth/audit_emitter.d.ts.map +1 -1
  18. package/dist/auth/audit_emitter.js +18 -2
  19. package/dist/auth/cell_action_specs.d.ts +11 -1
  20. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  21. package/dist/auth/cell_action_specs.js +18 -5
  22. package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
  23. package/dist/auth/cell_item_action_specs.js +2 -4
  24. package/dist/auth/password.d.ts +2 -2
  25. package/dist/auth/password.d.ts.map +1 -1
  26. package/dist/auth/password.js +10 -12
  27. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  28. package/dist/auth/role_grant_offer_action_specs.js +9 -9
  29. package/dist/auth/role_grant_offer_notifications.d.ts.map +1 -1
  30. package/dist/auth/role_grant_offer_notifications.js +2 -1
  31. package/dist/auth/role_grant_offer_schema.d.ts +1 -1
  32. package/dist/auth/role_grant_offer_schema.d.ts.map +1 -1
  33. package/dist/auth/role_grant_offer_schema.js +4 -7
  34. package/dist/auth/role_schema.d.ts.map +1 -1
  35. package/dist/auth/role_schema.js +5 -4
  36. package/dist/auth/scope_kind_schema.d.ts +10 -0
  37. package/dist/auth/scope_kind_schema.d.ts.map +1 -1
  38. package/dist/auth/scope_kind_schema.js +15 -3
  39. package/dist/cli/args.d.ts +21 -15
  40. package/dist/cli/args.d.ts.map +1 -1
  41. package/dist/cli/args.js +40 -16
  42. package/dist/cli/daemon.d.ts +0 -5
  43. package/dist/cli/daemon.d.ts.map +1 -1
  44. package/dist/cli/help.d.ts +45 -6
  45. package/dist/cli/help.d.ts.map +1 -1
  46. package/dist/cli/help.js +81 -4
  47. package/dist/env/load.d.ts +1 -1
  48. package/dist/env/load.js +1 -1
  49. package/dist/http/CLAUDE.md +1 -1
  50. package/dist/http/auth_shape.d.ts +0 -27
  51. package/dist/http/auth_shape.d.ts.map +1 -1
  52. package/dist/http/body_limit.d.ts +27 -14
  53. package/dist/http/body_limit.d.ts.map +1 -1
  54. package/dist/http/body_limit.js +29 -16
  55. package/dist/http/db_routes.d.ts.map +1 -1
  56. package/dist/http/db_routes.js +5 -3
  57. package/dist/primitive_schemas.d.ts +38 -4
  58. package/dist/primitive_schemas.d.ts.map +1 -1
  59. package/dist/primitive_schemas.js +77 -13
  60. package/dist/server/x_accel.d.ts +0 -1
  61. package/dist/server/x_accel.d.ts.map +1 -1
  62. package/dist/testing/CLAUDE.md +36 -19
  63. package/dist/testing/audit_drift_guard.d.ts +18 -5
  64. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  65. package/dist/testing/audit_drift_guard.js +24 -8
  66. package/dist/testing/cross_backend/action_manifest.d.ts +0 -14
  67. package/dist/testing/cross_backend/action_manifest.d.ts.map +1 -1
  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 +92 -147
  74. package/dist/testing/cross_backend/capabilities.d.ts +3 -24
  75. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  76. package/dist/testing/cross_backend/capabilities.js +1 -4
  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/testing_backdoor.d.ts.map +1 -1
  85. package/dist/testing/cross_backend/testing_backdoor.js +2 -0
  86. package/dist/testing/cross_backend/testing_reset_actions.d.ts +5 -9
  87. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  88. package/dist/testing/cross_backend/testing_reset_actions.js +5 -0
  89. package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -1
  90. package/dist/testing/cross_backend/token_lifetime.js +34 -1
  91. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  92. package/dist/testing/cross_backend/ts_spine_backend_config.js +7 -14
  93. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -1
  94. package/dist/testing/cross_backend/ws_outbound_overflow.js +65 -0
  95. package/dist/testing/schema_introspect.d.ts +24 -4
  96. package/dist/testing/schema_introspect.d.ts.map +1 -1
  97. package/dist/testing/schema_introspect.js +27 -4
  98. package/dist/testing/transports/raw_http.d.ts +78 -0
  99. package/dist/testing/transports/raw_http.d.ts.map +1 -0
  100. package/dist/testing/transports/raw_http.js +141 -0
  101. package/package.json +7 -5
  102. package/dist/env/resolve.d.ts +0 -163
  103. package/dist/env/resolve.d.ts.map +0 -1
  104. package/dist/env/resolve.js +0 -267
@@ -122,28 +122,6 @@ export interface BackendCapabilities {
122
122
  * explicitly (a backend that deliberately omits the route).
123
123
  */
124
124
  readonly account_status: boolean;
125
- /**
126
- * After an oversized-body `413` the backend **closes the connection**, so a
127
- * request that follows on the same connection is never served. Read by
128
- * `describe_body_size_smuggling_cross_tests`, which pipelines a `GET` behind
129
- * an oversized `POST`: with it `true` the `GET` must go unanswered (at most
130
- * one response); with it `false` the suite requires **exactly two**
131
- * responses — the 413, then the backend's own answer to that `GET`, which
132
- * shows the next request was framed on `Content-Length` and the body bytes
133
- * were not parsed as a request. Fewer than two fails as well: a backend
134
- * declared `false` that closes after the 413 fails the suite until the flag
135
- * is flipped to `true`.
136
- *
137
- * `true` for Node (closes at once, the body unread), Deno (closes once the
138
- * declared body has arrived — observed on Deno 2.8.3) and the Rust spine
139
- * (hyper closes at once); `false` for Bun. `Bun.serve` sends the 413
140
- * with its `Connection: close` header and then keeps the connection open,
141
- * skips the declared body, and serves the request after it — observed on
142
- * Bun 1.3.11, whatever the handler does with the request body. Requests
143
- * stay correctly framed there; what is missing is the close. See
144
- * `docs/security.md` §"Body Size Limiting".
145
- */
146
- readonly oversized_reject_closes_connection: boolean;
147
125
  /**
148
126
  * The backend can **initiate** a JSON-RPC request to a connected client
149
127
  * and await its typed reply (the server→client request/response direction
@@ -174,8 +152,9 @@ export interface BackendCapabilities {
174
152
  * anything.
175
153
  *
176
154
  * `true` for Node (`ws`), Deno, and the Rust spine (hyper hands the
177
- * leftover bytes to the upgraded stream); `false` for Bun, whose HTTP
178
- * parser answers such a request `400` before any handler runs. A backend
155
+ * leftover bytes to the upgraded stream); `false` for a Bun backend that
156
+ * upgrades after an asynchronous check, as one behind auth does — `Bun.serve`
157
+ * answers such a request `400` (observed on Bun 1.4.3). A backend
179
158
  * without it still runs the suite's client-library case (requests sent the
180
159
  * moment the socket opens), which pins the same contract without forcing
181
160
  * the ordering.
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA4C9B;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,QAAQ,CAAC,kCAAkC,EAAE,OAAO,CAAC;IACrD;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;;;OAeG;IACH,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC1C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,wBAAwB,EAAE,OAAO,CAAC;IAC3C;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAuBpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA2C9B;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC1C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,wBAAwB,EAAE,OAAO,CAAC;IAC3C;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAqBpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
@@ -22,8 +22,7 @@ import '../assert_dev_env.js';
22
22
  * a capability is a compile error until each family declares it).
23
23
  * - `ts_spine_capabilities` / `ts_spine_bun_capabilities` — fuz_app's own TS
24
24
  * spine presets, in `ts_spine_backend_config.ts` (deltas off the family
25
- * default; Bun flips `oversized_reject_closes_connection` and
26
- * `ws_handshake_pipelining`).
25
+ * default; Bun flips `ws_handshake_pipelining`).
27
26
  * - `rust_spine_stub_capabilities` — fuz_app's Rust spine-stub preset, in
28
27
  * `rust_spine_stub_backend_config.ts` (delta off the rust family default).
29
28
  *
@@ -61,8 +60,6 @@ export const in_process_capabilities = Object.freeze({
61
60
  fact_serving: true,
62
61
  ready: true,
63
62
  account_status: true,
64
- // Nothing reads it in-process: the smuggling probe needs a real socket.
65
- oversized_reject_closes_connection: true,
66
63
  peer_request: false,
67
64
  // Cross-process-only, like `peer_request`: the case needs a real bound socket
68
65
  // to write raw bytes to.
@@ -1 +1 @@
1
- {"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAyD9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E,eAAO,MAAM,8BAA8B,GAC1C,SAAS,uCAAuC,KAC9C,IA+cF,CAAC"}
1
+ {"version":3,"file":"cell_crud.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/cell_crud.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6D9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E,eAAO,MAAM,8BAA8B,GAC1C,SAAS,uCAAuC,KAC9C,IA6fF,CAAC"}
@@ -32,7 +32,10 @@ import '../assert_dev_env.js';
32
32
  * `kind` inside `data` still gets the authz error (404 / 403), never the
33
33
  * kind error — authz runs before the input-shape checks, on both spines;
34
34
  * - an empty-string `kind` is rejected on create (`cell_kind_empty`) and is
35
- * accepted as a list filter that matches nothing.
35
+ * accepted as a list filter that matches nothing;
36
+ * - a `kind` of exactly `CELL_KIND_LENGTH_MAX` UTF-16 code units is stored
37
+ * whole, and one unit past it is `invalid_params` on create and as a list
38
+ * filter.
36
39
  *
37
40
  * The visibility-manage-tier 403 (`cell_visibility_manage_only`) needs a
38
41
  * non-owner editor, which only a `cell_grant` can produce, so it lives in
@@ -42,7 +45,7 @@ import '../assert_dev_env.js';
42
45
  * @module
43
46
  */
44
47
  import { describe, assert } from 'vitest';
45
- import { CellCreateOutput, CellDeleteOutput, CellGetOutput, CellListOutput, CellUpdateOutput } from '../../auth/cell_action_specs.js';
48
+ import { CELL_KIND_LENGTH_MAX, CellCreateOutput, CellDeleteOutput, CellGetOutput, CellListOutput, CellUpdateOutput } from '../../auth/cell_action_specs.js';
46
49
  import { test_if } from './capabilities.js';
47
50
  import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
48
51
  import { assert_iso8601_seconds, assert_iso8601_seconds_nullable } from './wire_shapes.js';
@@ -239,5 +242,31 @@ export const describe_cell_crud_cross_tests = (options) => {
239
242
  const listed = expect_output(await cross_rpc_call(t, rpc_path, 'cell_list', { kind: '' }, owner_headers), CellListOutput);
240
243
  assert.ok(!listed.cells.some((c) => c.id === note.id), 'empty-kind filter matched a non-empty-kind cell');
241
244
  });
245
+ test_if(capabilities.cell_crud, 'kind length cap: stored whole at the cap; one unit past it is invalid_params on create and list', async () => {
246
+ const fixture = await setup_test();
247
+ const owner = await fixture.create_account({ username: 'cell_kind_cap' });
248
+ const t = fixture.fresh_transport();
249
+ const owner_headers = owner.create_session_headers();
250
+ // an astral character costs two UTF-16 code units on both spines
251
+ const astral_at_cap = '\u{1F600}'.repeat(CELL_KIND_LENGTH_MAX / 2);
252
+ for (const kind of ['k'.repeat(CELL_KIND_LENGTH_MAX), astral_at_cap]) {
253
+ assert.strictEqual(kind.length, CELL_KIND_LENGTH_MAX);
254
+ const created = expect_output(await cross_rpc_call(t, rpc_path, 'cell_create', { kind, data: {} }, owner_headers), CellCreateOutput).cell;
255
+ assert.strictEqual(created.kind, kind, 'a kind at the cap must be stored whole');
256
+ const listed = expect_output(await cross_rpc_call(t, rpc_path, 'cell_list', { kind }, owner_headers), CellListOutput);
257
+ assert.ok(listed.cells.some((c) => c.id === created.id), 'a kind filter at the cap must match the cell that carries it');
258
+ }
259
+ for (const kind of ['k'.repeat(CELL_KIND_LENGTH_MAX + 1), astral_at_cap + 'k']) {
260
+ for (const [method, params] of [
261
+ ['cell_create', { kind, data: {} }],
262
+ ['cell_list', { kind }]
263
+ ]) {
264
+ const refused = await cross_rpc_call(t, rpc_path, method, params, owner_headers);
265
+ assert.ok(!refused.ok, `${method} admitted a ${kind.length}-unit kind`);
266
+ assert.strictEqual(refused.error?.code, -32602);
267
+ assert.strictEqual(error_reason(refused), undefined, 'a schema refusal carries no reason');
268
+ }
269
+ }
270
+ });
242
271
  });
243
272
  };
@@ -92,12 +92,12 @@ import { z } from 'zod';
92
92
  export declare const ConformancePrincipal: z.ZodEnum<{
93
93
  keeper: "keeper";
94
94
  token: "token";
95
+ anonymous: "anonymous";
95
96
  daemon: "daemon";
96
97
  invalid_daemon: "invalid_daemon";
97
98
  daemon_browser: "daemon_browser";
98
99
  bearer_browser: "bearer_browser";
99
100
  scoped_token: "scoped_token";
100
- anonymous: "anonymous";
101
101
  fresh_non_admin: "fresh_non_admin";
102
102
  role_holder: "role_holder";
103
103
  wrong_role: "wrong_role";
@@ -122,12 +122,12 @@ export declare const ConformanceCaseRequest: z.ZodObject<{
122
122
  as: z.ZodEnum<{
123
123
  keeper: "keeper";
124
124
  token: "token";
125
+ anonymous: "anonymous";
125
126
  daemon: "daemon";
126
127
  invalid_daemon: "invalid_daemon";
127
128
  daemon_browser: "daemon_browser";
128
129
  bearer_browser: "bearer_browser";
129
130
  scoped_token: "scoped_token";
130
- anonymous: "anonymous";
131
131
  fresh_non_admin: "fresh_non_admin";
132
132
  role_holder: "role_holder";
133
133
  wrong_role: "wrong_role";
@@ -179,12 +179,12 @@ export declare const ConformanceCase: z.ZodObject<{
179
179
  as: z.ZodEnum<{
180
180
  keeper: "keeper";
181
181
  token: "token";
182
+ anonymous: "anonymous";
182
183
  daemon: "daemon";
183
184
  invalid_daemon: "invalid_daemon";
184
185
  daemon_browser: "daemon_browser";
185
186
  bearer_browser: "bearer_browser";
186
187
  scoped_token: "scoped_token";
187
- anonymous: "anonymous";
188
188
  fresh_non_admin: "fresh_non_admin";
189
189
  role_holder: "role_holder";
190
190
  wrong_role: "wrong_role";
@@ -1 +1 @@
1
- {"version":3,"file":"credential_header_robustness.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/credential_header_robustness.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAyD9B,qGAAqG;AACrG,MAAM,WAAW,0CAA0C;IAC1D,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAwFD,eAAO,MAAM,iDAAiD,GAC7D,SAAS,0CAA0C,KACjD,IAkHF,CAAC"}
1
+ {"version":3,"file":"credential_header_robustness.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/credential_header_robustness.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA4D9B,qGAAqG;AACrG,MAAM,WAAW,0CAA0C;IAC1D,mFAAmF;IACnF,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC9B;AAgDD,eAAO,MAAM,iDAAiD,GAC7D,SAAS,0CAA0C,KACjD,IAkHF,CAAC"}
@@ -46,55 +46,19 @@ import '../assert_dev_env.js';
46
46
  *
47
47
  * @module
48
48
  */
49
- import { connect } from 'node:net';
50
49
  import { describe, test, assert } from 'vitest';
51
50
  import { DAEMON_TOKEN_HEADER } from '../../auth/daemon_token.js';
51
+ import { raw_http_count_responses, raw_http_exchange, raw_http_first_status } from '../transports/raw_http.js';
52
52
  import { SPINE_RPC_PATH } from './spine_surface_constants.js';
53
53
  /** A well-formed UUID that never names a real row. */
54
54
  const NIL_UUID = '00000000-0000-0000-0000-000000000000';
55
55
  /** A value that is never a real credential — used for the conflicting / garbage copies. */
56
56
  const GARBAGE_TOKEN = 'not-a-real-credential-value';
57
- /**
58
- * Open a raw TCP socket to `base_url`, write `request_bytes` once, and collect
59
- * everything the server sends back until it closes or `read_timeout_ms`
60
- * elapses. Write errors are swallowed (a server closing mid-write on a rejected
61
- * request is expected behavior); what matters is what we read back. Mirrors
62
- * `body_size_smuggling.ts`'s `send_raw`.
63
- */
64
- const send_raw = (base_url, request_bytes, read_timeout_ms) => new Promise((resolve) => {
65
- const url = new URL(base_url);
66
- const port = Number(url.port) || (url.protocol === 'https:' ? 443 : 80);
67
- const socket = connect({ host: url.hostname, port });
68
- let received = '';
69
- let settled = false;
70
- const finish = () => {
71
- if (settled)
72
- return;
73
- settled = true;
74
- clearTimeout(timer);
75
- socket.destroy();
76
- resolve(received);
77
- };
78
- const timer = setTimeout(finish, read_timeout_ms);
79
- socket.setEncoding('latin1');
80
- socket.on('connect', () => socket.write(request_bytes));
81
- socket.on('data', (chunk) => {
82
- received += chunk;
83
- });
84
- socket.on('error', () => { }); // EPIPE/ECONNRESET on mid-write close is expected
85
- socket.on('close', finish);
86
- });
87
- /**
88
- * Count HTTP response status lines in a raw byte stream. Unanchored so a
89
- * second (smuggled) response concatenated after the first body — which carries
90
- * no trailing newline — is still counted; see `body_size_smuggling.ts`.
91
- */
92
- const count_responses = (raw) => (raw.match(/HTTP\/1\.[01] \d{3}/g) ?? []).length;
93
- /** The status code of the first HTTP response in a raw stream, or `null` if none. */
94
- const first_status = (raw) => {
95
- const match = /HTTP\/1\.[01] (\d{3})/.exec(raw);
96
- return match ? Number(match[1]) : null;
97
- };
57
+ /** How long each raw exchange waits for the server to finish answering. */
58
+ const RAW_READ_TIMEOUT_MS = 2000;
59
+ /** Write `request_bytes` to a fresh raw socket and read until the server closes. */
60
+ const send_raw = async (base_url, request_bytes) => (await raw_http_exchange(base_url, request_bytes, { read_timeout_ms: RAW_READ_TIMEOUT_MS }))
61
+ .received;
98
62
  const is_success_status = (status) => status >= 200 && status < 300;
99
63
  /**
100
64
  * Frame a raw HTTP/1.1 POST with the given extra header lines + JSON body.
@@ -124,18 +88,18 @@ export const describe_credential_header_robustness_cross_tests = (options) => {
124
88
  describe('credential-header robustness', () => {
125
89
  test('duplicate X-Daemon-Token (valid + conflicting) never escalates → one non-2xx response', async () => {
126
90
  const req = rpc_post(host, rpc_path, [`${DAEMON_TOKEN_HEADER}: ${daemon_token}`, `${DAEMON_TOKEN_HEADER}: ${GARBAGE_TOKEN}`], purge_body('dup-daemon'));
127
- const raw = await send_raw(base_url, req, 2000);
128
- assert.strictEqual(count_responses(raw), 1, `a duplicated X-Daemon-Token must yield exactly one response (no desync). Raw head: ${raw.slice(0, 120)}`);
129
- const status = first_status(raw);
91
+ const raw = await send_raw(base_url, req);
92
+ assert.strictEqual(raw_http_count_responses(raw), 1, `a duplicated X-Daemon-Token must yield exactly one response (no desync). Raw head: ${raw.slice(0, 120)}`);
93
+ const status = raw_http_first_status(raw);
130
94
  assert.ok(status !== null, `expected an HTTP response. Raw head: ${raw.slice(0, 120)}`);
131
95
  assert.ok(!is_success_status(status), `a duplicated daemon token must not perform a keeper operation; whichever copy wins, ` +
132
96
  `the result must be non-2xx (got ${status})`);
133
97
  });
134
98
  test('duplicate Authorization bearer headers (both garbage) do not authenticate → one non-2xx response', async () => {
135
99
  const req = rpc_post(host, rpc_path, [`Authorization: Bearer ${GARBAGE_TOKEN}`, `Authorization: Bearer ${GARBAGE_TOKEN}-2`], purge_body('dup-bearer'));
136
- const raw = await send_raw(base_url, req, 2000);
137
- assert.strictEqual(count_responses(raw), 1, `duplicate Authorization headers must yield exactly one response (no desync). Raw head: ${raw.slice(0, 120)}`);
138
- const status = first_status(raw);
100
+ const raw = await send_raw(base_url, req);
101
+ assert.strictEqual(raw_http_count_responses(raw), 1, `duplicate Authorization headers must yield exactly one response (no desync). Raw head: ${raw.slice(0, 120)}`);
102
+ const status = raw_http_first_status(raw);
139
103
  assert.ok(status !== null, `expected an HTTP response. Raw head: ${raw.slice(0, 120)}`);
140
104
  assert.ok(!is_success_status(status), `garbage bearer tokens (even duplicated) must not authenticate (got ${status})`);
141
105
  });
@@ -154,10 +118,10 @@ export const describe_credential_header_robustness_cross_tests = (options) => {
154
118
  ];
155
119
  for (const { label, value } of malformed) {
156
120
  const req = rpc_post(host, rpc_path, [`${DAEMON_TOKEN_HEADER}: ${value}`], purge_body('ctrl-char'));
157
- const raw = await send_raw(base_url, req, 2000);
158
- assert.ok(count_responses(raw) <= 1, `${label}: a control-char-injected credential header must not frame a second ` +
121
+ const raw = await send_raw(base_url, req);
122
+ assert.ok(raw_http_count_responses(raw) <= 1, `${label}: a control-char-injected credential header must not frame a second ` +
159
123
  `request (no smuggle). Raw head: ${raw.slice(0, 120)}`);
160
- const status = first_status(raw);
124
+ const status = raw_http_first_status(raw);
161
125
  assert.ok(status === null || !is_success_status(status), `${label}: a malformed credential header must never authenticate (got ${status})`);
162
126
  }
163
127
  });
@@ -166,12 +130,12 @@ export const describe_credential_header_robustness_cross_tests = (options) => {
166
130
  // rejected (4xx / 431 / connection close). Its own outcome is don't-care.
167
131
  const oversized = 'a'.repeat(64 * 1024);
168
132
  const req = rpc_post(host, rpc_path, [`${DAEMON_TOKEN_HEADER}: ${oversized}`], purge_body('oversized'));
169
- await send_raw(base_url, req, 2000);
133
+ await send_raw(base_url, req);
170
134
  // The property: the server survives it. A normal request on a fresh
171
135
  // connection must still get a response (no crash, no wedged accept loop).
172
136
  const probe = `GET ${rpc_path} HTTP/1.1\r\nHost: ${host}\r\nConnection: close\r\n\r\n`;
173
- const raw = await send_raw(base_url, probe, 2000);
174
- assert.ok(first_status(raw) !== null, `server must still respond after an oversized-header request (no crash / wedge). Raw head: ${raw.slice(0, 120)}`);
137
+ const raw = await send_raw(base_url, probe);
138
+ assert.ok(raw_http_first_status(raw) !== null, `server must still respond after an oversized-header request (no crash / wedge). Raw head: ${raw.slice(0, 120)}`);
175
139
  });
176
140
  });
177
141
  };
@@ -1 +1 @@
1
- {"version":3,"file":"default_backend_configs.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_backend_configs.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"default_backend_configs.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/default_backend_configs.ts"],"names":[],"mappings":"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"}
@@ -32,10 +32,6 @@ export const ts_default_capabilities = Object.freeze({
32
32
  // `GET /api/account/status` is bundled into `create_account_route_specs`, so
33
33
  // every TS backend mounting account routes serves it.
34
34
  account_status: true,
35
- // Node and Deno close the connection after an oversized-body `413`. A
36
- // Bun-served consumer overrides to `false` (see the bun spine config) —
37
- // `Bun.serve` keeps the connection and serves the next request.
38
- oversized_reject_closes_connection: true,
39
35
  // Off by default like `sse`/`ready`: the WS backend transport *can* initiate
40
36
  // server→client requests (`BackendWebsocketTransport.request_connection`),
41
37
  // but driving the full `peer/ping` suite also needs `peer/ping` mounted on
@@ -44,7 +40,8 @@ export const ts_default_capabilities = Object.freeze({
44
40
  peer_request: false,
45
41
  // Node (`ws`) and Deno hand frames pipelined with the upgrade request to the
46
42
  // socket. A Bun-served consumer overrides to `false` (see the bun spine
47
- // config) — `Bun.serve` answers such a request `400`.
43
+ // config) — `Bun.serve` answers such a request `400` when the upgrade
44
+ // follows an asynchronous check.
48
45
  ws_handshake_pipelining: true,
49
46
  // Off by default: a consumer's WS endpoint mounts its own domain's actions.
50
47
  // fuz_app's spine configs opt in (the spine binary mounts the account
@@ -76,9 +73,6 @@ export const rust_default_capabilities = Object.freeze({
76
73
  // The Rust `account_router` bundles `/status` into the account routes, so
77
74
  // every Rust spine serving the account surface serves it.
78
75
  account_status: true,
79
- // hyper closes the connection on the oversized-body reject, so a pipelined
80
- // request is never reached.
81
- oversized_reject_closes_connection: true,
82
76
  // Server-initiated requests landed Rust-first canonical — the Rust spine
83
77
  // drives the `peer/ping` round-trip, so the family opts into the suite.
84
78
  peer_request: true,
@@ -1 +1 @@
1
- {"version":3,"file":"testing_backdoor.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_backdoor.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAyE9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AA6ExE,kEAAkE;AAClE,MAAM,WAAW,+BAAgC,SAAQ,wBAAwB;IAChF;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE;QACb,2EAA2E;QAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,+DAA+D;QAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,sDAAsD;QACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACF;AAED,eAAO,MAAM,qCAAqC,GACjD,SAAS,+BAA+B,KACtC,IAyEF,CAAC"}
1
+ {"version":3,"file":"testing_backdoor.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_backdoor.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAyE9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AA+ExE,kEAAkE;AAClE,MAAM,WAAW,+BAAgC,SAAQ,wBAAwB;IAChF;;;;OAIG;IACH,QAAQ,CAAC,EAAE,CAAC,EAAE;QACb,2EAA2E;QAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;QAC1B,+DAA+D;QAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;QACzB,sDAAsD;QACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KACzB,CAAC;CACF;AAED,eAAO,MAAM,qCAAqC,GACjD,SAAS,+BAA+B,KACtC,IAyEF,CAAC"}
@@ -77,6 +77,8 @@ const NIL_UUID = '00000000-0000-0000-0000-000000000000';
77
77
  */
78
78
  const backdoor_methods = [
79
79
  { method: '_testing_reset', params: {} },
80
+ // The audit barrier — its input is void, so the call carries no params.
81
+ { method: '_testing_drain_effects', params: undefined },
80
82
  { method: '_testing_mint_session', params: { account_id: NIL_UUID, expires_in_seconds: -60 } },
81
83
  { method: '_testing_put_fact', params: { content: 'backdoor-probe' } },
82
84
  // An empty burst — valid input, so it clears the 400 phase; a refused
@@ -118,15 +118,6 @@ export declare const testing_reset_action_spec: {
118
118
  password_value: z.ZodOptional<z.ZodString>;
119
119
  roles: z.ZodArray<z.ZodString>;
120
120
  }, z.core.$strict>>>;
121
- /**
122
- * Additional actor names to seed on the **keeper** account, beyond
123
- * its single bootstrap actor. Drives the multi-actor `acting`
124
- * selector branches (omitted `acting` + >1 actor ⇒ `actor_required`
125
- * with the `available[]` list) that are otherwise unreachable
126
- * cross-process — account creation only ever mints one actor, and no
127
- * production wire path adds a second. Bootstrap-cradle seeding, same
128
- * rationale as `extra_accounts`.
129
- */
130
121
  extra_actors: z.ZodOptional<z.ZodArray<z.ZodString>>;
131
122
  }, z.core.$strict>;
132
123
  readonly output: z.ZodObject<{
@@ -379,6 +370,11 @@ export declare const create_testing_flood_notifications_action: () => RpcAction;
379
370
  * shapes match by design). Optional `exclude_tables` drops documented
380
371
  * divergences from both sides before comparison.
381
372
  *
373
+ * A backend running on pglet (a TS backend config's `memory://` default)
374
+ * answers with an error when its schema holds an index: the snapshot exists
375
+ * to be compared, and pglet's index definitions omit the modifiers a
376
+ * comparison needs (`query_schema_snapshot`). Run the backend on PostgreSQL.
377
+ *
382
378
  * `auth` gates on the daemon-token credential, matching `_testing_reset`.
383
379
  */
384
380
  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":"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"}
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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"}
@@ -397,6 +397,11 @@ rpc_action(testing_flood_notifications_action_spec, ({ count, size }, ctx) => {
397
397
  * shapes match by design). Optional `exclude_tables` drops documented
398
398
  * divergences from both sides before comparison.
399
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
+ *
400
405
  * `auth` gates on the daemon-token credential, matching `_testing_reset`.
401
406
  */
402
407
  export const testing_schema_snapshot_action_spec = {
@@ -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;AAwDzD,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,25 +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
25
  * `ws_handshake_pipelining: false`.
27
26
  *
28
- * `Bun.serve` does not close the connection after an oversized-body `413`:
29
- * the response carries `Connection: close`, and Bun keeps the socket open,
30
- * skips the declared body, and serves the request that follows it (observed
31
- * on Bun 1.3.11). The next request is framed on `Content-Length`, so nothing
32
- * is desynchronized, but the smuggling suite's "the pipelined request is never
33
- * served" assertion does not hold; the flag puts Bun on the suite's no-desync
34
- * arm, which requires exactly the 413 and the reply to the pipelined request.
35
- * See `docs/security.md` §"Body Size Limiting".
36
- *
37
- * `Bun.serve` also answers `400` to an upgrade request that carries WebSocket
38
- * frames in the same TCP write, before any handler runs, so the WS suite's
39
- * 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).
40
34
  */
41
35
  const ts_spine_bun_capabilities = Object.freeze({
42
36
  ...ts_spine_capabilities,
43
- oversized_reject_closes_connection: false,
44
37
  ws_handshake_pipelining: false
45
38
  });
46
39
  /** Default port for the Node TS spine binary — slots beside the Rust `spine_stub` (1177). */
@@ -1 +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;AAiE9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AAEtE,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,YAAY,CAAC;AAiCzD,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,IA0LF,CAAC"}
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"}
@@ -32,6 +32,13 @@ import '../assert_dev_env.js';
32
32
  * `invalid_params`, and the socket stays open.
33
33
  * - **no connection** — over HTTP RPC the action refuses with `invalid_params`
34
34
  * and reason `no_connection`, rather than answering as though it flooded.
35
+ * - **the action's limits** — over HTTP RPC a call at each of the action's
36
+ * limits (`count`, `size`, `count` × `size`) is refused for having no
37
+ * connection, and a call one past each is refused `invalid_params` with no
38
+ * reason. Both spines check the limits before they look for the connection
39
+ * and before they build a payload, so the boundaries are read off the
40
+ * refusal and no burst is ever allocated — which pins the two spines'
41
+ * limits equal.
35
42
  *
36
43
  * What the suite does **not** assert, because it is each implementation's own:
37
44
  * how many frames accepted before the overflow reach the client (the Rust
@@ -77,6 +84,50 @@ const LARGE_FITS = { count: 3, size: MIB };
77
84
  const OVERFLOWS = { count: 32, size: MIB };
78
85
  /** One mebibyte past the action's `count` × `size` limit. */
79
86
  const REFUSED = { count: 65, size: MIB };
87
+ /** The most notifications one flood call may ask for. */
88
+ const FLOOD_MAX_COUNT = 65_536;
89
+ /** The largest payload one flood call may ask for. */
90
+ const FLOOD_MAX_SIZE = 16 * MIB;
91
+ /** The most payload bytes (`count` × `size`) one flood call may ask for. */
92
+ const FLOOD_MAX_TOTAL_BYTES = 64 * MIB;
93
+ /**
94
+ * A `count` and `size`, each within its own limit, whose product is exactly
95
+ * one byte past `FLOOD_MAX_TOTAL_BYTES` (the factors of 2^26 + 1).
96
+ */
97
+ const FLOOD_ONE_BYTE_PAST_TOTAL = { count: 265, size: 253_241 };
98
+ /**
99
+ * Calls at and one past each of the flood action's limits. Each "past" call
100
+ * holds the other axis at zero, or both axes within their own limit, so one
101
+ * limit alone decides it.
102
+ */
103
+ const FLOOD_LIMIT_CASES = [
104
+ { name: 'count at its limit', params: { count: FLOOD_MAX_COUNT, size: 1 }, within: true },
105
+ { name: 'count past its limit', params: { count: FLOOD_MAX_COUNT + 1, size: 0 }, within: false },
106
+ { name: 'size at its limit', params: { count: 1, size: FLOOD_MAX_SIZE }, within: true },
107
+ { name: 'size past its limit', params: { count: 0, size: FLOOD_MAX_SIZE + 1 }, within: false },
108
+ {
109
+ name: 'count × size at its limit, both axes at theirs or under',
110
+ params: { count: FLOOD_MAX_TOTAL_BYTES / FLOOD_MAX_SIZE, size: FLOOD_MAX_SIZE },
111
+ within: true
112
+ },
113
+ {
114
+ name: 'count × size at its limit, count at its own',
115
+ params: { count: FLOOD_MAX_COUNT, size: FLOOD_MAX_TOTAL_BYTES / FLOOD_MAX_COUNT },
116
+ within: true
117
+ },
118
+ {
119
+ name: 'count × size past its limit, each axis within its own',
120
+ params: { count: FLOOD_MAX_COUNT, size: FLOOD_MAX_TOTAL_BYTES / FLOOD_MAX_COUNT + 1 },
121
+ within: false
122
+ },
123
+ {
124
+ // the smallest step past the product limit: the row above overshoots it by
125
+ // `FLOOD_MAX_COUNT` bytes, so alone it admits a limit that many bytes loose
126
+ name: 'count × size one byte past its limit',
127
+ params: FLOOD_ONE_BYTE_PAST_TOTAL,
128
+ within: false
129
+ }
130
+ ];
80
131
  /** How long the test waits for the megabyte burst and its response. */
81
132
  const LARGE_BURST_TIMEOUT_MS = 15_000;
82
133
  /** How long the test waits for the overflow close. */
@@ -207,5 +258,19 @@ export const describe_ws_outbound_overflow_cross_tests = (options) => {
207
258
  assert.strictEqual(res.error?.code, INVALID_PARAMS);
208
259
  assert.strictEqual(res.error?.data?.reason, REASON_NO_CONNECTION);
209
260
  });
261
+ test_if(rpc_path !== undefined, 'over HTTP a flood call at each limit is refused for no connection, one past it for its params', async () => {
262
+ const fixture = await setup_test();
263
+ const transport = fixture.fresh_transport({ origin: null });
264
+ // the hand-factored case is only "one byte past" for this limit
265
+ assert.strictEqual(FLOOD_ONE_BYTE_PAST_TOTAL.count * FLOOD_ONE_BYTE_PAST_TOTAL.size, FLOOD_MAX_TOTAL_BYTES + 1);
266
+ for (const { name, params, within } of FLOOD_LIMIT_CASES) {
267
+ const res = await cross_rpc_call(transport, rpc_path, FLOOD_METHOD, params, fixture.create_daemon_token_headers());
268
+ assert.strictEqual(res.ok, false, `${name}: expected a refusal over HTTP`);
269
+ assert.strictEqual(res.error?.code, INVALID_PARAMS, name);
270
+ // within the limits the call gets as far as the connection check;
271
+ // past one it is refused before that, with no reason
272
+ assert.strictEqual(res.error?.data?.reason, within ? REASON_NO_CONNECTION : undefined, `${name}: ${JSON.stringify(params)}`);
273
+ }
274
+ });
210
275
  });
211
276
  };