@fuzdev/fuz_app 0.133.0 → 0.135.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/dist/actions/CLAUDE.md +91 -9
  2. package/dist/actions/register_action_ws.d.ts +23 -3
  3. package/dist/actions/register_action_ws.d.ts.map +1 -1
  4. package/dist/actions/register_action_ws.js +57 -37
  5. package/dist/actions/transports.d.ts +23 -0
  6. package/dist/actions/transports.d.ts.map +1 -1
  7. package/dist/actions/transports.js +23 -0
  8. package/dist/actions/transports_ws_backend.d.ts +132 -9
  9. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  10. package/dist/actions/transports_ws_backend.js +236 -33
  11. package/dist/actions/ws_endpoint_spec.d.ts +9 -0
  12. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  13. package/dist/actions/ws_outbound_writer.d.ts +206 -0
  14. package/dist/actions/ws_outbound_writer.d.ts.map +1 -0
  15. package/dist/actions/ws_outbound_writer.js +431 -0
  16. package/dist/auth/CLAUDE.md +2 -2
  17. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  18. package/dist/auth/audit_log_routes.js +3 -1
  19. package/dist/auth/cell_actions.js +1 -1
  20. package/dist/auth/role_schema.d.ts +14 -3
  21. package/dist/auth/role_schema.d.ts.map +1 -1
  22. package/dist/auth/role_schema.js +14 -2
  23. package/dist/db/CLAUDE.md +10 -7
  24. package/dist/db/create_db.d.ts +8 -6
  25. package/dist/db/create_db.d.ts.map +1 -1
  26. package/dist/db/create_db.js +11 -10
  27. package/dist/db/db.d.ts +9 -7
  28. package/dist/db/db.d.ts.map +1 -1
  29. package/dist/db/db.js +5 -4
  30. package/dist/db/db_pglet.d.ts +43 -0
  31. package/dist/db/db_pglet.d.ts.map +1 -0
  32. package/dist/db/db_pglet.js +55 -0
  33. package/dist/db/migrate.d.ts +1 -1
  34. package/dist/db/migrate.js +2 -2
  35. package/dist/http/CLAUDE.md +2 -1
  36. package/dist/http/body_limit.d.ts +61 -0
  37. package/dist/http/body_limit.d.ts.map +1 -0
  38. package/dist/http/body_limit.js +87 -0
  39. package/dist/http/db_routes.js +2 -2
  40. package/dist/http/error_schemas.d.ts +2 -0
  41. package/dist/http/error_schemas.d.ts.map +1 -1
  42. package/dist/http/error_schemas.js +2 -0
  43. package/dist/realtime/outbound_budget.d.ts +91 -0
  44. package/dist/realtime/outbound_budget.d.ts.map +1 -0
  45. package/dist/realtime/outbound_budget.js +108 -0
  46. package/dist/realtime/sse.d.ts +36 -3
  47. package/dist/realtime/sse.d.ts.map +1 -1
  48. package/dist/realtime/sse.js +58 -6
  49. package/dist/realtime/sse_constants.d.ts +12 -0
  50. package/dist/realtime/sse_constants.d.ts.map +1 -1
  51. package/dist/realtime/sse_constants.js +12 -0
  52. package/dist/server/app_server.d.ts.map +1 -1
  53. package/dist/server/app_server.js +3 -6
  54. package/dist/testing/CLAUDE.md +87 -28
  55. package/dist/testing/admin_integration.js +1 -1
  56. package/dist/testing/app_server.d.ts +5 -5
  57. package/dist/testing/app_server.d.ts.map +1 -1
  58. package/dist/testing/app_server.js +21 -20
  59. package/dist/testing/audit_completeness.d.ts +1 -1
  60. package/dist/testing/audit_completeness.js +1 -1
  61. package/dist/testing/audit_drift_guard.d.ts +1 -1
  62. package/dist/testing/audit_drift_guard.js +1 -1
  63. package/dist/testing/cross_backend/body_size_smuggling.d.ts +9 -9
  64. package/dist/testing/cross_backend/body_size_smuggling.d.ts.map +1 -1
  65. package/dist/testing/cross_backend/body_size_smuggling.js +110 -60
  66. package/dist/testing/cross_backend/capabilities.d.ts +18 -15
  67. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  68. package/dist/testing/cross_backend/capabilities.js +1 -0
  69. package/dist/testing/cross_backend/default_backend_configs.d.ts +5 -5
  70. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  71. package/dist/testing/cross_backend/default_backend_configs.js +6 -6
  72. package/dist/testing/cross_backend/in_process_setup.d.ts +2 -2
  73. package/dist/testing/cross_backend/in_process_setup.js +3 -3
  74. package/dist/testing/cross_backend/role_grant_role_grammar.d.ts +6 -0
  75. package/dist/testing/cross_backend/role_grant_role_grammar.d.ts.map +1 -0
  76. package/dist/testing/cross_backend/role_grant_role_grammar.js +117 -0
  77. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts +2 -3
  78. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  79. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +2 -3
  80. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +1 -1
  81. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  82. package/dist/testing/cross_backend/testing_backdoor.js +8 -4
  83. package/dist/testing/cross_backend/testing_reset_actions.d.ts +84 -4
  84. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  85. package/dist/testing/cross_backend/testing_reset_actions.js +102 -3
  86. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +2 -2
  87. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  88. package/dist/testing/cross_backend/ts_spine_backend_config.js +9 -7
  89. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts +29 -0
  90. package/dist/testing/cross_backend/ws_outbound_overflow.d.ts.map +1 -0
  91. package/dist/testing/cross_backend/ws_outbound_overflow.js +211 -0
  92. package/dist/testing/db.d.ts +49 -60
  93. package/dist/testing/db.d.ts.map +1 -1
  94. package/dist/testing/db.js +104 -101
  95. package/dist/testing/integration.js +1 -1
  96. package/dist/testing/rate_limiting.d.ts +2 -1
  97. package/dist/testing/rate_limiting.d.ts.map +1 -1
  98. package/dist/testing/rate_limiting.js +2 -2
  99. package/dist/testing/schema_introspect.d.ts +10 -2
  100. package/dist/testing/schema_introspect.d.ts.map +1 -1
  101. package/dist/testing/schema_introspect.js +15 -7
  102. package/dist/testing/sse_round_trip.d.ts +1 -1
  103. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  104. package/dist/testing/sse_round_trip.js +2 -2
  105. package/dist/testing/stubs.js +1 -1
  106. package/dist/testing/transports/ws_transport.d.ts +4 -2
  107. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  108. package/dist/testing/transports/ws_transport.js +2 -1
  109. package/dist/testing/ws_round_trip.d.ts +32 -2
  110. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  111. package/dist/testing/ws_round_trip.js +26 -4
  112. package/package.json +6 -1
@@ -10,36 +10,44 @@ import '../assert_dev_env.js';
10
10
  * body bytes would be parsed as the start of the next request — a classic
11
11
  * request-smuggling vector. This suite probes the boundary by **pipelining**:
12
12
  * it opens a raw TCP socket and sends, in one write, an oversized `POST`
13
- * immediately followed by a second `GET`. The assertion forks on the backend's
14
- * declared `oversized_reject_closes_connection` capability:
13
+ * immediately followed by a second `GET`. What it requires forks on the
14
+ * backend's declared `oversized_reject_closes_connection` capability:
15
15
  *
16
- * - **Closes (Node / Deno / hyper)** — the reject closes the socket with the
17
- * GET bytes unconsumed, so **at most one** response comes back. `<= 1` rather
18
- * than "exactly the 413" because the impls close differently at the TCP level
19
- * (node-server graceful close delivers the 413 first; hyper's RST can drop the
20
- * in-flight 413 before the client reads it), so demanding a cleanly-read 413
21
- * would be flaky.
22
- * - **Drains + keepalives (Bun)** — `Bun.serve` reads the full declared
23
- * `Content-Length` body and answers the *correctly-framed* pipelined GET, so
24
- * **two** responses come back. This is **not** a smuggle: the GET is delimited
25
- * by the body's `Content-Length`, not the unread body reinterpreted as a
26
- * request — Bun answers it with a clean `400` (`missing method`), not the `x`
27
- * body bytes reparsed. The security property asserted here is **no desync**
28
- * (`<= 2`): a real desync would reframe the 1 MiB of `x` into bogus request
29
- * lines and push the count past two.
16
+ * - **Closes (Node / Deno / hyper)** — the pipelined GET goes unanswered, so
17
+ * **at most one** response comes back. `<= 1` rather than "exactly the 413"
18
+ * because the impls close differently at the TCP level (a graceful close
19
+ * delivers the 413 first; hyper's RST can drop the in-flight 413 before the
20
+ * client reads it), so demanding a cleanly-read 413 would be flaky. Node
21
+ * closes at once with the body unread — `create_app_server`'s body limit
22
+ * (`http/body_limit.ts`) refuses on the header and answers
23
+ * `Connection: close`, which is what makes `@hono/node-server` close. Deno
24
+ * closes once the whole declared body has arrived, which this probe's
25
+ * single write delivers; a client that withholds the body holds a Deno
26
+ * connection open, and no request behind the body is served either way.
27
+ * - **Keeps the connection (Bun)** — `Bun.serve` sends that same 413, header
28
+ * included, then keeps the socket open, skips the declared body, and serves
29
+ * the GET. This is not a smuggle — the GET is delimited by the POST's
30
+ * `Content-Length`, not carved out of the body — so the property held here
31
+ * is **no desync**: the bytes read back are **exactly two** well-formed
32
+ * responses, the first the 413 and the second the same status and body the
33
+ * backend gives that GET on a connection of its own. Body bytes parsed as a
34
+ * request would show up as a different second response or as extra ones.
35
+ * Fewer than two fails too, with a message naming the capability: a backend
36
+ * on this arm that closes after the 413 belongs on the other one.
30
37
  *
31
- * Either way the oversized body is rejected *with* a 413 — pinned reliably over
32
- * `fetch` by `describe_body_size_cross_tests`; this test owns only the
33
- * connection-handling half. A **positive control** (two pipelined requests →
34
- * `>= 2` responses) proves a second response *would* be seen if a trailing
35
- * request were processed — without it the close-posture `<= 1` would be vacuous
36
- * on a server that never reuses connections — and that the counter isn't
37
- * undercounting.
38
+ * The oversized body is rejected *with* a 413 on every backend — pinned
39
+ * reliably over `fetch` by `describe_body_size_cross_tests`; this test owns
40
+ * only the connection-handling half. A **positive control** (two pipelined
41
+ * requests → `>= 2` responses) proves a second response *would* be seen if a
42
+ * trailing request were processed — without it the close-posture `<= 1` would
43
+ * be vacuous on a server that never reuses connections — and that the counter
44
+ * isn't undercounting.
38
45
  *
39
46
  * Raw-socket by necessity (the `FetchTransport` can't pipeline two requests on
40
47
  * one connection), so — unlike `body_size.ts` — this is **cross-process only**
41
- * (no in-process leg; there is no socket in-process). The connection-close half
42
- * is capability-gated; the no-desync half holds on every spine.
48
+ * (no in-process leg; there is no socket in-process — the in-process pin is
49
+ * that the reject never reads the request body, in
50
+ * `server/create_app_server.db.test.ts`).
43
51
  *
44
52
  * Cited property: `docs/security.md` §"Body Size Limiting" (connection handling
45
53
  * on oversized reject).
@@ -51,6 +59,12 @@ import { describe, test, assert } from 'vitest';
51
59
  import { SPINE_RPC_PATH } from './spine_surface_constants.js';
52
60
  /** The shared 1 MiB cap (see `body_size.ts` for why it's a local constant). */
53
61
  const BODY_LIMIT_DEFAULT_BYTES = 1024 * 1024;
62
+ /**
63
+ * How long each raw exchange waits for the server to finish answering. A
64
+ * backend that keeps the connection never closes it, so its exchange runs the
65
+ * whole window.
66
+ */
67
+ const RAW_READ_TIMEOUT_MS = 2000;
54
68
  /**
55
69
  * Open a raw TCP socket to `base_url`, write `request_bytes` once, and collect
56
70
  * everything the server sends back until it closes the connection or
@@ -93,6 +107,39 @@ const send_raw = (base_url, request_bytes, read_timeout_ms) => new Promise((reso
93
107
  * server genuinely returns 2.
94
108
  */
95
109
  const count_responses = (raw) => (raw.match(/HTTP\/1\.[01] \d{3}/g) ?? []).length;
110
+ /**
111
+ * Split a raw byte stream into the HTTP responses it holds, each framed on
112
+ * its own `Content-Length` (a response without one runs to the end of the
113
+ * stream). The stream is `latin1`-decoded, so a character is a byte.
114
+ *
115
+ * @returns the responses in order, or `null` when the bytes are not a
116
+ * sequence of whole responses — text where a status line belongs, a head
117
+ * with no end, a body shorter than its declared length
118
+ */
119
+ const parse_responses = (raw) => {
120
+ const responses = [];
121
+ let offset = 0;
122
+ while (offset < raw.length) {
123
+ const head_end = raw.indexOf('\r\n\r\n', offset);
124
+ if (head_end === -1)
125
+ return null;
126
+ const [status_line, ...header_lines] = raw.slice(offset, head_end).split('\r\n');
127
+ const status_match = /^HTTP\/1\.[01] (\d{3})(?: |$)/.exec(status_line);
128
+ if (!status_match)
129
+ return null;
130
+ const body_start = head_end + 4;
131
+ const length_line = header_lines.find((line) => /^content-length:/i.test(line));
132
+ const body_end = length_line === undefined
133
+ ? raw.length
134
+ : body_start + Number.parseInt(length_line.slice(length_line.indexOf(':') + 1), 10);
135
+ // also false for a `NaN` length
136
+ if (!(body_end >= body_start && body_end <= raw.length))
137
+ return null;
138
+ responses.push({ status: Number(status_match[1]), body: raw.slice(body_start, body_end) });
139
+ offset = body_end;
140
+ }
141
+ return responses;
142
+ };
96
143
  export const describe_body_size_smuggling_cross_tests = (options) => {
97
144
  const { base_url } = options;
98
145
  const rpc_path = options.rpc_path ?? SPINE_RPC_PATH;
@@ -106,58 +153,61 @@ export const describe_body_size_smuggling_cross_tests = (options) => {
106
153
  test('control: two pipelined requests → ≥2 responses (connection reuse is real)', async () => {
107
154
  const two_requests = `GET ${rpc_path} HTTP/1.1\r\nHost: ${host}\r\n\r\n` +
108
155
  `GET ${rpc_path} HTTP/1.1\r\nHost: ${host}\r\nConnection: close\r\n\r\n`;
109
- const response = await send_raw(base_url, two_requests, 2000);
156
+ const response = await send_raw(base_url, two_requests, RAW_READ_TIMEOUT_MS);
110
157
  const n = count_responses(response);
111
158
  assert.ok(n >= 2, `expected ≥2 responses on one keep-alive connection (got ${n}); without ` +
112
159
  `connection reuse — or with an undercounting matcher — the smuggling ` +
113
160
  `assertion below is not a real signal. Raw head: ${response.slice(0, 120)}`);
114
161
  });
115
- test('oversized POST + pipelined GET → smuggled request not processed', async () => {
162
+ test('oversized POST + pipelined GET → no request smuggled', async () => {
116
163
  const oversized_len = BODY_LIMIT_DEFAULT_BYTES + 1024;
117
- // One write: an over-cap POST (rejected on Content-Length, body never
118
- // read) immediately followed by a GET. If the server wrongly drained
119
- // the unread body it would reach + answer this GET — a smuggle.
164
+ const pipelined_get = `GET ${rpc_path} HTTP/1.1\r\nHost: ${host}\r\n\r\n`;
165
+ // One write: an over-cap POST (rejected on Content-Length) immediately
166
+ // followed by a GET.
120
167
  const payload = `POST ${rpc_path} HTTP/1.1\r\n` +
121
168
  `Host: ${host}\r\n` +
122
169
  `Content-Type: application/json\r\n` +
123
170
  `Content-Length: ${oversized_len}\r\n` +
124
171
  `\r\n` +
125
172
  'x'.repeat(oversized_len) +
126
- `GET ${rpc_path} HTTP/1.1\r\nHost: ${host}\r\n\r\n`;
127
- const response = await send_raw(base_url, payload, 2000);
173
+ pipelined_get;
174
+ const response = await send_raw(base_url, payload, RAW_READ_TIMEOUT_MS);
128
175
  const n = count_responses(response);
129
176
  if (closes_connection) {
130
- // Strong posture (Node / Deno / hyper): the reject closes the
131
- // connection *without reading the body*, so the pipelined GET is
132
- // never reached — **at most one** response comes back (the 413, or
133
- // none if the close raced the read). A *second* response would mean
134
- // the body was drained and the GET processed. We assert `<= 1` rather
135
- // than "exactly the 413" because the impls close differently at the
136
- // TCP level — node-server closes gracefully (413 delivered first),
137
- // hyper sends an RST that can drop the in-flight 413 before the client
138
- // reads it — and demanding a cleanly-read 413 would be flaky. The
139
- // 413-ness itself is pinned reliably (over `fetch`) by
140
- // `describe_body_size_cross_tests`. The control above proves a second
141
- // response *would* be seen if the GET were processed, so `<= 1` is a
142
- // real signal, not vacuous.
177
+ // The reject closes the connection, so the pipelined GET is never
178
+ // reached — **at most one** response comes back (the 413, or none if
179
+ // the close raced the read). A *second* response means the connection
180
+ // was kept and the GET served. The control above proves a second
181
+ // response *would* be seen, so `<= 1` is a real signal.
143
182
  assert.ok(n <= 1, `expected at most one response (oversized reject closes the connection; the ` +
144
183
  `pipelined GET must not be reached). Saw ${n}. Raw head: ${response.slice(0, 120)}`);
184
+ return;
145
185
  }
146
- else {
147
- // Drain-and-keepalive posture (Bun): `Bun.serve` reads the full
148
- // declared `Content-Length` body and keeps the socket alive, so it
149
- // answers the *correctly-framed* pipelined GET — **two** responses
150
- // (the 413 + a well-formed reply to the GET). This is not a smuggle:
151
- // the GET is delimited by the body's `Content-Length`, not the unread
152
- // body reinterpreted as request bytes. The security property here is
153
- // **no desync** — the body bytes are never reparsed as request(s). A
154
- // real desync would reframe the 1 MiB of `x` into bogus request
155
- // lines, pushing the count past two; `<= 2` is the no-desync bound,
156
- // and the control above proves the counter isn't undercounting.
157
- assert.ok(n <= 2, `expected at most two responses (the 413 + one framed reply to the ` +
158
- `pipelined GET); more means the oversized body was reparsed as requests ` +
159
- `(a desync). Saw ${n}. Raw head: ${response.slice(0, 120)}`);
160
- }
186
+ // The backend keeps the connection (Bun): the 413, then its answer to
187
+ // the GET, and nothing else. Hold it to **no desync** — the body bytes
188
+ // were skipped as a body, never parsed as a request.
189
+ assert.ok(n >= 2, `expected two responses (the 413 + the reply to the pipelined GET) from a ` +
190
+ `backend declared to keep the connection after an oversized-body 413. Saw ` +
191
+ `${n} within ${RAW_READ_TIMEOUT_MS}ms. If this backend closes the connection ` +
192
+ `after the 413, declare \`oversized_reject_closes_connection: true\` for it ` +
193
+ `(this suite's \`closes_connection\`). Raw head: ${response.slice(0, 120)}`);
194
+ assert.ok(n <= 2, `expected exactly two responses (the 413 + one reply to the pipelined GET); ` +
195
+ `more means the oversized body was parsed as requests (a desync). ` +
196
+ `Saw ${n}. Raw head: ${response.slice(0, 120)}`);
197
+ const responses = parse_responses(response);
198
+ assert.ok(responses !== null, `expected the bytes read back to be whole HTTP responses, each framed on its ` +
199
+ `Content-Length. Raw head: ${response.slice(0, 240)}`);
200
+ assert.strictEqual(responses.length, n, `expected one status line per framed response; a difference means a status ` +
201
+ `line sits inside another response's body. Raw head: ${response.slice(0, 240)}`);
202
+ const [first, second] = responses;
203
+ assert.strictEqual(first.status, 413, 'the first response is the oversized reject');
204
+ // A request carved out of the body would not be this GET, so the reply
205
+ // must be the one the same GET gets on its own connection.
206
+ const alone = parse_responses(await send_raw(base_url, `GET ${rpc_path} HTTP/1.1\r\nHost: ${host}\r\nConnection: close\r\n\r\n`, RAW_READ_TIMEOUT_MS));
207
+ assert.ok(alone?.length === 1, 'expected one response to the GET sent alone');
208
+ assert.deepStrictEqual(second, alone[0], `expected the second response to be the backend's reply to the pipelined ` +
209
+ `GET (same status and body as that GET sent alone); anything else means ` +
210
+ `the bytes after the 413 were not framed as that request`);
161
211
  });
162
212
  });
163
213
  };
@@ -123,22 +123,25 @@ export interface BackendCapabilities {
123
123
  */
124
124
  readonly account_status: boolean;
125
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).
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`.
134
136
  *
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".
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".
142
145
  */
143
146
  readonly oversized_reject_closes_connection: boolean;
144
147
  /**
@@ -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;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"}
@@ -61,6 +61,7 @@ export const in_process_capabilities = Object.freeze({
61
61
  fact_serving: true,
62
62
  ready: true,
63
63
  account_status: true,
64
+ // Nothing reads it in-process: the smuggling probe needs a real socket.
64
65
  oversized_reject_closes_connection: true,
65
66
  peer_request: false,
66
67
  // Cross-process-only, like `peer_request`: the case needs a real bound socket
@@ -11,11 +11,11 @@ import '../assert_dev_env.js';
11
11
  *
12
12
  * Defaults baked in by family:
13
13
  *
14
- * - **TS** — `'memory://'` PGlite, 30s startup window. Its shape notes
14
+ * - **TS** — `'memory://'` (in-memory pglet), 30s startup window. Its shape notes
15
15
  * (`ts_default_shape_notes`) record `trusted_proxy: false` /
16
16
  * `login_rate_limit: false` — the TS canonical path leaves those limiters
17
17
  * null in test mode.
18
- * - **Rust** — caller-supplied real Postgres URL (PGlite isn't reachable
18
+ * - **Rust** — caller-supplied real Postgres URL (an in-process WASM database isn't reachable
19
19
  * from `tokio-postgres`), 120s startup window (cargo first-build cost), the
20
20
  * `FUZ_TESTING_RESET_DB_ON_STARTUP=true` self-wipe gate, and shape notes
21
21
  * (`rust_default_shape_notes`) recording `trusted_proxy: true` +
@@ -82,7 +82,7 @@ export interface MakeDefaultTsBackendConfigOptions {
82
82
  readonly port: number;
83
83
  /** argv passed to the spawn (first entry is the binary). */
84
84
  readonly start_command: ReadonlyArray<string>;
85
- /** Defaults to `'memory://'` (in-memory PGlite). */
85
+ /** Defaults to `'memory://'` (in-memory pglet). */
86
86
  readonly database_url?: string;
87
87
  /** Merged on top of the generic env baseline; later keys win. */
88
88
  readonly extra_env?: Readonly<Record<string, string>>;
@@ -109,7 +109,7 @@ export interface MakeDefaultTsBackendConfigOptions {
109
109
  }
110
110
  /**
111
111
  * Shared builder for TS-family backends (Deno + Node). Owns the common
112
- * env baseline (NODE_ENV, HOST, PORT, in-memory PGlite, cookie keys,
112
+ * env baseline (NODE_ENV, HOST, PORT, in-memory pglet, cookie keys,
113
113
  * bootstrap token path) so per-backend factories only declare what
114
114
  * genuinely differs.
115
115
  */
@@ -122,7 +122,7 @@ export interface MakeDefaultRustBackendConfigOptions {
122
122
  /** argv passed to the spawn (first entry is the binary). */
123
123
  readonly start_command: ReadonlyArray<string>;
124
124
  /**
125
- * Required — Rust needs real Postgres (PGlite isn't reachable from
125
+ * Required — Rust needs real Postgres (an in-process WASM database isn't reachable from
126
126
  * `tokio-postgres`). Consumers typically supply
127
127
  * `'postgres://localhost/{repo}_test_{name}'`.
128
128
  */
@@ -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,9 +32,9 @@ 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.
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
38
  oversized_reject_closes_connection: true,
39
39
  // Off by default like `sse`/`ready`: the WS backend transport *can* initiate
40
40
  // server→client requests (`BackendWebsocketTransport.request_connection`),
@@ -76,8 +76,8 @@ export const rust_default_capabilities = Object.freeze({
76
76
  // The Rust `account_router` bundles `/status` into the account routes, so
77
77
  // every Rust spine serving the account surface serves it.
78
78
  account_status: true,
79
- // hyper sends an RST on the oversized-body reject — the connection closes
80
- // and the pipelined request is never reached.
79
+ // hyper closes the connection on the oversized-body reject, so a pipelined
80
+ // request is never reached.
81
81
  oversized_reject_closes_connection: true,
82
82
  // Server-initiated requests landed Rust-first canonical — the Rust spine
83
83
  // drives the `peer/ping` round-trip, so the family opts into the suite.
@@ -131,7 +131,7 @@ const build_default_bootstrap = (paths, overrides) => ({
131
131
  });
132
132
  /**
133
133
  * Shared builder for TS-family backends (Deno + Node). Owns the common
134
- * env baseline (NODE_ENV, HOST, PORT, in-memory PGlite, cookie keys,
134
+ * env baseline (NODE_ENV, HOST, PORT, in-memory pglet, cookie keys,
135
135
  * bootstrap token path) so per-backend factories only declare what
136
136
  * genuinely differs.
137
137
  */
@@ -44,8 +44,8 @@ export interface InProcessSetupOptions extends CreateTestAppOptions {
44
44
  * `_testing_reset` cradle so suite bodies read
45
45
  * `fixture.extra_accounts[username]` uniformly regardless of transport.
46
46
  *
47
- * The describe-level `auth_integration_truncate_tables` / pglite WASM
48
- * cache lifecycle stays in `create_pglite_factory` / `create_describe_db`
47
+ * The describe-level `auth_integration_truncate_tables` / WASM database
48
+ * cache lifecycle stays in `create_default_db_factory` / `create_describe_db`
49
49
  * (`testing/db.ts`) — `default_in_process_setup` doesn't manage db state
50
50
  * beyond what `create_test_app` already does.
51
51
  */
@@ -32,14 +32,14 @@ const in_process_fetch_transport = (app) => {
32
32
  * `_testing_reset` cradle so suite bodies read
33
33
  * `fixture.extra_accounts[username]` uniformly regardless of transport.
34
34
  *
35
- * The describe-level `auth_integration_truncate_tables` / pglite WASM
36
- * cache lifecycle stays in `create_pglite_factory` / `create_describe_db`
35
+ * The describe-level `auth_integration_truncate_tables` / WASM database
36
+ * cache lifecycle stays in `create_default_db_factory` / `create_describe_db`
37
37
  * (`testing/db.ts`) — `default_in_process_setup` doesn't manage db state
38
38
  * beyond what `create_test_app` already does.
39
39
  */
40
40
  export const default_in_process_setup = (options) => async () => {
41
41
  // Per-test fresh db. When `options.migration_namespaces` is set,
42
- // `create_test_app` provisions an auth+extras PGlite (e.g. the cell
42
+ // `create_test_app` provisions an auth+extras database (e.g. the cell
43
43
  // layer); otherwise the auth-only default. Either way the factory
44
44
  // resets + re-migrates on each `create`, so the per-test keeper
45
45
  // bootstrap below lands on a clean DB.
@@ -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
  /**
@@ -18,7 +18,7 @@ import '../assert_dev_env.js';
18
18
  * admits with the entry's `moderation_required` folded into the verdict. The
19
19
  * root's `data` arrives in `input.root_data` (the handler read it in-tx), so
20
20
  * the predicate is **pure** — no DB read of its own (which also dodges the
21
- * single-connection PGlite deadlock a separate handle would hit).
21
+ * single-connection in-memory deadlock a separate handle would hit — pglet, PGlite).
22
22
  *
23
23
  * @module
24
24
  */
@@ -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;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"}