@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
@@ -122,25 +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
- * On an oversized-body `413` reject the backend **closes the connection
127
- * without reading the body** (the defense-in-depth posture), rather than
128
- * draining the declared `Content-Length` and keeping the socket alive.
129
- * Gates the strong half of `describe_body_size_smuggling_cross_tests`: when
130
- * `true`, the pipelined GET is never reached (at most one response); when
131
- * `false`, the suite instead asserts the weaker but still-load-bearing
132
- * no-desync property (the body is framed on `Content-Length`, not reparsed
133
- * as request bytes).
134
- *
135
- * `true` for the Node / Deno (`@hono/node-server` graceful close) and Rust
136
- * (hyper RST) backends; `false` for Bun — `Bun.serve` reads the full body
137
- * and processes the correctly-framed pipelined request even when the `413`
138
- * carries `Connection: close`. Bun is not insecure (no desync — it answers
139
- * the cleanly-delimited GET with a proper `400`); the flag records the
140
- * connection-handling divergence so the suite stays green without losing the
141
- * smuggle detector. See `docs/security.md` §"Body Size Limiting".
142
- */
143
- readonly oversized_reject_closes_connection: boolean;
144
125
  /**
145
126
  * The backend can **initiate** a JSON-RPC request to a connected client
146
127
  * and await its typed reply (the server→client request/response direction
@@ -171,8 +152,9 @@ export interface BackendCapabilities {
171
152
  * anything.
172
153
  *
173
154
  * `true` for Node (`ws`), Deno, and the Rust spine (hyper hands the
174
- * leftover bytes to the upgraded stream); `false` for Bun, whose HTTP
175
- * 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
176
158
  * without it still runs the suite's client-library case (requests sent the
177
159
  * moment the socket opens), which pins the same contract without forcing
178
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;;;;;;;;;;;;;;;;;OAiBG;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,mBAsBpC,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,7 +60,6 @@ export const in_process_capabilities = Object.freeze({
61
60
  fact_serving: true,
62
61
  ready: true,
63
62
  account_status: true,
64
- oversized_reject_closes_connection: true,
65
63
  peer_request: false,
66
64
  // Cross-process-only, like `peer_request`: the case needs a real bound socket
67
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":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAChF,OAAO,EAA4B,KAAK,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAYhG,2CAA2C;AAC3C,MAAM,WAAW,yBAAyB;IACzC,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACjD;AAED;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GACjC,SAAS,yBAAyB,KAChC,MAAM,CAAC,MAAM,EAAE,MAAM,CAKtB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAmCpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,mBA6BtC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,EAAE,iBAIrC,CAAC;AAeH,MAAM,WAAW,iCAAiC;IACjD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C,mDAAmD;IACnD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,6CAA6C;IAC7C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,iCAAiC,KACrC,aAoCF,CAAC;AAEF,MAAM,WAAW,mCAAmC;IACnD,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,sCAAsC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4DAA4D;IAC5D,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC9C;;;;OAIG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACtD,+CAA+C;IAC/C,QAAQ,CAAC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IAC5C,wEAAwE;IACxE,QAAQ,CAAC,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAClC,sEAAsE;IACtE,QAAQ,CAAC,mBAAmB,CAAC,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/D;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAC5C,MAAM,mCAAmC,KACvC,aA4CF,CAAC"}
@@ -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/Deno close the socket on an oversized-body reject (the default
36
- // posture). A Bun-served consumer overrides to `false` (see the bun spine
37
- // config) — fail-loud rather than silently skipping the smuggle detector.
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 sends an RST on the oversized-body reject — the connection closes
80
- // and the pipelined 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,
@@ -0,0 +1,6 @@
1
+ import '../assert_dev_env.js';
2
+ import type { RpcPathCrossSuiteOptions } from './setup.js';
3
+ /** Options for the role-grammar suite (the standard RPC-dispatched shape). */
4
+ export type RoleGrantRoleGrammarCrossTestOptions = RpcPathCrossSuiteOptions;
5
+ export declare const describe_role_grant_role_grammar_cross_tests: (options: RoleGrantRoleGrammarCrossTestOptions) => void;
6
+ //# sourceMappingURL=role_grant_role_grammar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role_grant_role_grammar.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_role_grammar.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsC9B,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAG3D,8EAA8E;AAC9E,MAAM,MAAM,oCAAoC,GAAG,wBAAwB,CAAC;AAoC5E,eAAO,MAAM,4CAA4C,GACxD,SAAS,oCAAoC,KAC3C,IAoGF,CAAC"}
@@ -0,0 +1,117 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-backend parity suite for an **off-grammar `role`** on the grant verbs.
4
+ *
5
+ * `role_grant_assign` and `role_grant_offer_create` declare `role` as
6
+ * `RoleName` (lowercase letters and underscores, no leading or trailing
7
+ * underscore, at most 64 characters). A role string outside that grammar is refused at input
8
+ * validation — JSON-RPC `invalid_params`, with no `data.reason` — and writes
9
+ * no audit row: a role is caller-chosen text until it passes the grammar, and
10
+ * the grant verbs record the role they refuse in their failure rows.
11
+ *
12
+ * - **refused at validation** — each off-grammar role, on each verb, answers
13
+ * `invalid_params` with no reason, so the refusal is the input schema's and
14
+ * not a grant-path or authorizer one. One of them is all lowercase letters
15
+ * and one character past the length cap: the cap is part of the grammar, so
16
+ * a failure row never carries an arbitrarily long caller-chosen string.
17
+ * - **no audit row** — after the refusals, no grant audit row carries an
18
+ * off-grammar role. The control keeps that read from being vacuous: an
19
+ * on-grammar role no registry holds is refused past validation with
20
+ * `role_not_web_grantable`, the refusal a failure row records — as is one
21
+ * exactly at the length cap, which pins the boundary.
22
+ *
23
+ * Exists because the action-manifest parity gate captures no param schemas
24
+ * while the Rust spine parses params by hand — a spine that let an off-grammar
25
+ * role reach its grant path would answer with a different error and write the
26
+ * caller's text into the audit log. Both verbs are on every spine's standard
27
+ * surface, so the suite is ungated. Cross-process only: it reads the audit log
28
+ * through `_testing_drain_effects`, which the spine binaries mount.
29
+ *
30
+ * @module
31
+ */
32
+ import { assert, describe, test } from 'vitest';
33
+ import { ERROR_ROLE_NOT_WEB_GRANTABLE } from '../../http/error_schemas.js';
34
+ import { JSONRPC_ERROR_CODES } from '../../http/jsonrpc_errors.js';
35
+ import { cross_rpc_call, error_reason } from './cell_cross_helpers.js';
36
+ import { SPINE_RPC_PATH } from './spine_surface_constants.js';
37
+ const NIL_UUID = '00000000-0000-0000-0000-000000000000';
38
+ /**
39
+ * The longest role name the grammar admits — an independent copy of the
40
+ * producers' `ROLE_NAME_LENGTH_MAX`, so a cap moved on one spine fails here.
41
+ */
42
+ const ROLE_NAME_LENGTH_MAX = 64;
43
+ /** All lowercase letters, so only its length is off the grammar. */
44
+ const OVER_LENGTH_ROLE = 'a'.repeat(ROLE_NAME_LENGTH_MAX + 1);
45
+ /** On the grammar at exactly the length cap, held by no registry. */
46
+ const AT_LENGTH_ROLE = 'a'.repeat(ROLE_NAME_LENGTH_MAX);
47
+ /** Each breaks one rule of the `RoleName` grammar. */
48
+ const OFF_GRAMMAR_ROLES = [
49
+ 'Admin', // uppercase
50
+ 'role-name', // hyphen
51
+ 'role1', // digit
52
+ '_role', // leading underscore
53
+ 'role_', // trailing underscore
54
+ 'role name', // space
55
+ '', // empty
56
+ OVER_LENGTH_ROLE // on the alphabet, one past the length cap
57
+ ];
58
+ /** On the grammar, held by no registry. */
59
+ const UNREGISTERED_ROLE = 'not_a_registered_role';
60
+ const GRANT_METHODS = ['role_grant_assign', 'role_grant_offer_create'];
61
+ /** The audit event types the grant verbs write. */
62
+ const GRANT_EVENT_TYPES = ['role_grant_create', 'role_grant_offer_create'];
63
+ export const describe_role_grant_role_grammar_cross_tests = (options) => {
64
+ const { setup_test } = options;
65
+ const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
66
+ describe('grant verbs refuse an off-grammar role at validation', { timeout: 30_000 }, () => {
67
+ for (const method of GRANT_METHODS) {
68
+ test(`${method}: an off-grammar role is invalid_params with no reason`, async () => {
69
+ const fixture = await setup_test();
70
+ const t = fixture.fresh_transport();
71
+ for (const role of OFF_GRAMMAR_ROLES) {
72
+ const res = await cross_rpc_call(t, rpc_path, method, { to_account_id: NIL_UUID, role }, fixture.create_session_headers());
73
+ const label = `${method} role=${JSON.stringify(role)}`;
74
+ assert.strictEqual(res.ok, false, `${label}: expected a refusal`);
75
+ assert.strictEqual(res.error?.code, JSONRPC_ERROR_CODES.invalid_params, label);
76
+ // a validation refusal, not a grant-path or authorizer one
77
+ assert.strictEqual(error_reason(res), undefined, `${label}: no data.reason`);
78
+ }
79
+ });
80
+ }
81
+ test('an off-grammar role writes no audit row; an unregistered on-grammar one is refused past validation', async () => {
82
+ const fixture = await setup_test();
83
+ const t = fixture.fresh_transport();
84
+ const session = fixture.create_session_headers();
85
+ for (const method of GRANT_METHODS) {
86
+ for (const role of OFF_GRAMMAR_ROLES) {
87
+ const res = await cross_rpc_call(t, rpc_path, method, { to_account_id: NIL_UUID, role }, session);
88
+ assert.strictEqual(res.error?.code, JSONRPC_ERROR_CODES.invalid_params, `${method} ${JSON.stringify(role)}`);
89
+ }
90
+ }
91
+ // control: the same call with an on-grammar role reaches the grant-path gate
92
+ const control = await cross_rpc_call(t, rpc_path, 'role_grant_assign', { to_account_id: NIL_UUID, role: UNREGISTERED_ROLE }, session);
93
+ assert.strictEqual(error_reason(control), ERROR_ROLE_NOT_WEB_GRANTABLE);
94
+ // and the length cap is inclusive: a role exactly at it is on the grammar
95
+ const at_length = await cross_rpc_call(t, rpc_path, 'role_grant_assign', { to_account_id: NIL_UUID, role: AT_LENGTH_ROLE }, session);
96
+ assert.strictEqual(error_reason(at_length), ERROR_ROLE_NOT_WEB_GRANTABLE, 'a role at the length cap passes validation');
97
+ // a barrier before reading the log: both spines write audit rows off
98
+ // the request path
99
+ const drained = await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, '_testing_drain_effects', undefined, fixture.create_daemon_token_headers());
100
+ assert.ok(drained.ok, `_testing_drain_effects failed: ${JSON.stringify(drained.error)}`);
101
+ for (const event_type of GRANT_EVENT_TYPES) {
102
+ const listed = await cross_rpc_call(t, rpc_path, 'audit_log_list', { event_type }, session);
103
+ assert.ok(listed.ok, `audit_log_list failed: ${JSON.stringify(listed.error)}`);
104
+ const events = listed.result.events;
105
+ const roles = events.map((event) => event.metadata?.role);
106
+ for (const role of roles) {
107
+ assert.ok(typeof role !== 'string' || !OFF_GRAMMAR_ROLES.includes(role), `${event_type}: an audit row records the off-grammar role ${JSON.stringify(role)}`);
108
+ }
109
+ if (event_type === 'role_grant_create') {
110
+ // the controls' failure rows are in the same read, so it is not vacuous
111
+ assert.ok(roles.includes(UNREGISTERED_ROLE), 'the control refusal wrote its failure row');
112
+ assert.ok(roles.includes(AT_LENGTH_ROLE), 'the at-cap refusal wrote its failure row');
113
+ }
114
+ }
115
+ });
116
+ });
117
+ };
@@ -5,9 +5,8 @@ export declare const RUST_SPINE_STUB_BIN_ENV = "FUZ_TESTING_RUST_SPINE_STUB_BIN"
5
5
  /**
6
6
  * Env var overriding the stub's Postgres URL when `database_url` is omitted —
7
7
  * e.g. a scratch cluster on a spare port, keeping the run off the machine's
8
- * default server. Pair it with the libpq `PGHOST` / `PGPORT` the runner's
9
- * idempotent `createdb` reads, so the database it ensures is the one the URL
10
- * names.
8
+ * default server. A runner that creates the stub's database before the spawn
9
+ * must create the one this URL names, on the server it names.
11
10
  */
12
11
  export declare const RUST_SPINE_STUB_DATABASE_URL_ENV = "FUZ_TESTING_RUST_SPINE_STUB_DATABASE_URL";
13
12
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"rust_spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/rust_spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA0C9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAYzD,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,6CAA6C,CAAC;AAE3F;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,6CAA6C,CAAC;AAqBnG,kGAAkG;AAClG,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,0FAA0F;AAC1F,eAAO,MAAM,oCAAoC,sDACG,CAAC;AAErD,MAAM,WAAW,6BAA6B;IAC7C,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACjD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,GAC1C,UAAS,6BAAkC,KACzC,aAmDF,CAAC"}
1
+ {"version":3,"file":"rust_spine_stub_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/rust_spine_stub_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA0C9B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAYzD,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,6CAA6C,CAAC;AAE3F;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,6CAA6C,CAAC;AAqBnG,kGAAkG;AAClG,eAAO,MAAM,4BAA4B,OAAO,CAAC;AAEjD,0FAA0F;AAC1F,eAAO,MAAM,oCAAoC,sDACG,CAAC;AAErD,MAAM,WAAW,6BAA6B;IAC7C,8DAA8D;IAC9D,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C;;;;;;OAMG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;IAClC;;;;;;OAMG;IACH,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,8BAA8B,CAAC,EAAE,MAAM,CAAC;CACjD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,GAC1C,UAAS,6BAAkC,KACzC,aAmDF,CAAC"}
@@ -45,9 +45,8 @@ export const RUST_SPINE_STUB_BIN_ENV = 'FUZ_TESTING_RUST_SPINE_STUB_BIN';
45
45
  /**
46
46
  * Env var overriding the stub's Postgres URL when `database_url` is omitted —
47
47
  * e.g. a scratch cluster on a spare port, keeping the run off the machine's
48
- * default server. Pair it with the libpq `PGHOST` / `PGPORT` the runner's
49
- * idempotent `createdb` reads, so the database it ensures is the one the URL
50
- * names.
48
+ * default server. A runner that creates the stub's database before the spawn
49
+ * must create the one this URL names, on the server it names.
51
50
  */
52
51
  export const RUST_SPINE_STUB_DATABASE_URL_ENV = 'FUZ_TESTING_RUST_SPINE_STUB_DATABASE_URL';
53
52
  /**
@@ -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;AAwE9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AA0ExE,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"}
@@ -4,10 +4,11 @@ import '../assert_dev_env.js';
4
4
  * actions.
5
5
  *
6
6
  * `_testing_reset` / `_testing_mint_session` / `_testing_put_fact` /
7
- * `_testing_schema_snapshot` / `_testing_migration_tracker` /
8
- * `_testing_action_manifest` are privileged test-binary actions the production
9
- * wire never exposes — three direct DB writes (full auth wipe, forged session
10
- * row, raw fact insert) plus three introspection reads (the live schema, the
7
+ * `_testing_flood_notifications` / `_testing_schema_snapshot` /
8
+ * `_testing_migration_tracker` / `_testing_action_manifest` are privileged
9
+ * test-binary actions the production wire never exposes — direct DB writes
10
+ * (full auth wipe, forged session row, raw fact insert), a flood of the
11
+ * caller's own connection, and introspection reads (the live schema, the
11
12
  * `schema_version` migration tracker, and the live RPC registry — the highest
12
13
  * info-leak of the set were the gate to break). Their only structural fence
13
14
  * is the **daemon-token** credential gate on
@@ -76,8 +77,13 @@ const NIL_UUID = '00000000-0000-0000-0000-000000000000';
76
77
  */
77
78
  const backdoor_methods = [
78
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 },
79
82
  { method: '_testing_mint_session', params: { account_id: NIL_UUID, expires_in_seconds: -60 } },
80
83
  { method: '_testing_put_fact', params: { content: 'backdoor-probe' } },
84
+ // An empty burst — valid input, so it clears the 400 phase; a refused
85
+ // caller never reaches the handler that would queue it.
86
+ { method: '_testing_flood_notifications', params: { count: 0, size: 0 } },
81
87
  // The schema-dump read — `exclude_tables` is optional, so `{}` is valid
82
88
  // and clears the 400 phase like the writes above.
83
89
  { method: '_testing_schema_snapshot', params: {} },