@fuzdev/fuz_app 0.93.0 → 0.95.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 (200) hide show
  1. package/README.md +2 -2
  2. package/dist/actions/action_codegen.d.ts +18 -18
  3. package/dist/actions/action_codegen.js +30 -30
  4. package/dist/actions/action_event.js +1 -1
  5. package/dist/actions/action_event_helpers.d.ts +1 -1
  6. package/dist/actions/action_event_types.d.ts +1 -1
  7. package/dist/actions/action_rpc.d.ts +2 -2
  8. package/dist/actions/broadcast_api.d.ts +1 -1
  9. package/dist/actions/broadcast_api.js +1 -1
  10. package/dist/actions/perform_action.d.ts +2 -2
  11. package/dist/actions/register_action_ws.d.ts +2 -2
  12. package/dist/actions/register_action_ws.js +2 -2
  13. package/dist/actions/register_ws_endpoint.js +1 -1
  14. package/dist/actions/request_tracker.svelte.d.ts +1 -1
  15. package/dist/actions/request_tracker.svelte.js +1 -1
  16. package/dist/actions/rpc_client.d.ts +1 -1
  17. package/dist/actions/socket.svelte.d.ts +2 -2
  18. package/dist/actions/transports_ws.js +1 -1
  19. package/dist/actions/transports_ws_auth_guard.d.ts +1 -1
  20. package/dist/actions/transports_ws_backend.d.ts +1 -1
  21. package/dist/actions/transports_ws_backend.js +2 -2
  22. package/dist/auth/account_action_specs.js +1 -1
  23. package/dist/auth/account_schema.d.ts +1 -1
  24. package/dist/auth/account_schema.js +1 -1
  25. package/dist/auth/actor_lookup_action_specs.js +1 -1
  26. package/dist/auth/actor_lookup_queries.d.ts +1 -1
  27. package/dist/auth/actor_search_action_specs.js +1 -1
  28. package/dist/auth/actor_search_queries.d.ts +1 -1
  29. package/dist/auth/admin_action_specs.js +1 -1
  30. package/dist/auth/api_token.js +1 -1
  31. package/dist/auth/api_token_queries.d.ts +1 -1
  32. package/dist/auth/app_settings_schema.d.ts +1 -1
  33. package/dist/auth/app_settings_schema.js +1 -1
  34. package/dist/auth/audit_emitter.d.ts +2 -2
  35. package/dist/auth/audit_log_routes.d.ts +1 -1
  36. package/dist/auth/audit_log_schema.d.ts +1 -1
  37. package/dist/auth/audit_log_schema.js +2 -2
  38. package/dist/auth/bearer_auth.d.ts +1 -1
  39. package/dist/auth/bootstrap_account.d.ts +1 -1
  40. package/dist/auth/bootstrap_route_schema.js +1 -1
  41. package/dist/auth/bootstrap_routes.d.ts +1 -1
  42. package/dist/auth/bootstrap_routes.js +1 -1
  43. package/dist/auth/cell_action_specs.js +2 -2
  44. package/dist/auth/cell_audit_action_specs.js +1 -1
  45. package/dist/auth/cell_audit_metadata.js +1 -1
  46. package/dist/auth/cell_field_action_specs.js +1 -1
  47. package/dist/auth/cell_field_audit_metadata.js +1 -1
  48. package/dist/auth/cell_grant_action_specs.js +1 -1
  49. package/dist/auth/cell_grant_audit_metadata.js +1 -1
  50. package/dist/auth/cell_item_action_specs.d.ts +1 -1
  51. package/dist/auth/cell_item_action_specs.js +3 -3
  52. package/dist/auth/cell_item_audit_metadata.js +1 -1
  53. package/dist/auth/cell_relation_visibility.d.ts +1 -1
  54. package/dist/auth/cleanup.d.ts +1 -1
  55. package/dist/auth/daemon_token_middleware.d.ts +1 -1
  56. package/dist/auth/deps.d.ts +2 -2
  57. package/dist/auth/invite_schema.d.ts +1 -1
  58. package/dist/auth/invite_schema.js +1 -1
  59. package/dist/auth/request_context.d.ts +1 -1
  60. package/dist/auth/role_grant_offer_action_specs.js +1 -1
  61. package/dist/auth/role_grant_offer_notifications.d.ts +1 -1
  62. package/dist/auth/role_grant_offer_notifications.js +1 -1
  63. package/dist/auth/role_grant_offer_queries.d.ts +1 -1
  64. package/dist/auth/role_grant_offer_schema.d.ts +1 -1
  65. package/dist/auth/role_grant_offer_schema.js +1 -1
  66. package/dist/auth/role_grant_queries.d.ts +1 -1
  67. package/dist/auth/session_queries.d.ts +1 -1
  68. package/dist/auth/session_queries.js +1 -1
  69. package/dist/auth/signup_route_schema.js +1 -1
  70. package/dist/cli/args.d.ts +1 -1
  71. package/dist/cli/args.js +2 -2
  72. package/dist/cli/help.d.ts +1 -1
  73. package/dist/cli/help.js +1 -1
  74. package/dist/cli/logger.d.ts +1 -1
  75. package/dist/cli/logger.js +1 -1
  76. package/dist/db/CLAUDE.md +1 -1
  77. package/dist/db/cell_audit_queries.d.ts +1 -1
  78. package/dist/db/cell_field_queries.d.ts +1 -1
  79. package/dist/db/cell_grant_queries.d.ts +1 -1
  80. package/dist/db/cell_item_queries.d.ts +1 -1
  81. package/dist/db/cell_queries.d.ts +3 -3
  82. package/dist/db/cell_queries.js +1 -1
  83. package/dist/db/fact_disk_storage.d.ts +2 -2
  84. package/dist/db/fact_disk_storage.js +4 -4
  85. package/dist/db/fact_queries.d.ts +1 -1
  86. package/dist/db/fact_store.d.ts +3 -3
  87. package/dist/db/fact_store.js +2 -2
  88. package/dist/db/file_fact_url.d.ts +1 -1
  89. package/dist/db/file_fact_url.js +1 -1
  90. package/dist/db/migrate.js +1 -1
  91. package/dist/db/status.js +1 -1
  92. package/dist/http/auth_shape.js +2 -2
  93. package/dist/http/common_routes.d.ts +1 -1
  94. package/dist/http/db_routes.d.ts +1 -1
  95. package/dist/http/origin.js +1 -1
  96. package/dist/http/pending_effects.d.ts +1 -1
  97. package/dist/http/proxy.d.ts +1 -1
  98. package/dist/http/route_spec.d.ts +1 -1
  99. package/dist/rate_limiter.js +1 -1
  100. package/dist/realtime/sse.d.ts +1 -1
  101. package/dist/realtime/sse_auth_guard.d.ts +1 -1
  102. package/dist/runtime/deno.js +1 -1
  103. package/dist/server/app_backend.d.ts +1 -1
  104. package/dist/server/app_backend.js +1 -1
  105. package/dist/server/env.js +1 -1
  106. package/dist/server/fact_write.d.ts +2 -2
  107. package/dist/server/fact_write.js +1 -1
  108. package/dist/server/serve_fact_route.d.ts +1 -1
  109. package/dist/server/serve_fact_route.js +2 -2
  110. package/dist/server/startup.d.ts +1 -1
  111. package/dist/testing/CLAUDE.md +61 -9
  112. package/dist/testing/adversarial_input.js +1 -1
  113. package/dist/testing/app_server.d.ts +1 -1
  114. package/dist/testing/app_server.js +1 -1
  115. package/dist/testing/auth_apps.js +1 -1
  116. package/dist/testing/cross_backend/bench/bench_report.d.ts +1 -1
  117. package/dist/testing/cross_backend/bench/bench_report.js +2 -2
  118. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +1 -1
  119. package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +1 -1
  120. package/dist/testing/cross_backend/bootstrap_backend.d.ts +1 -1
  121. package/dist/testing/cross_backend/capabilities.d.ts +25 -4
  122. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  123. package/dist/testing/cross_backend/capabilities.js +9 -4
  124. package/dist/testing/cross_backend/cell_relations.js +1 -1
  125. package/dist/testing/cross_backend/conformance_case.d.ts +4 -0
  126. package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
  127. package/dist/testing/cross_backend/conformance_case.js +26 -0
  128. package/dist/testing/cross_backend/conformance_table.d.ts +21 -0
  129. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  130. package/dist/testing/cross_backend/conformance_table.js +109 -7
  131. package/dist/testing/cross_backend/create_cross_backend_global_setup.d.ts +1 -1
  132. package/dist/testing/cross_backend/create_dual_spawn_global_setup.d.ts +1 -1
  133. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  134. package/dist/testing/cross_backend/default_backend_configs.js +7 -0
  135. package/dist/testing/cross_backend/default_spine_surface.d.ts +1 -1
  136. package/dist/testing/cross_backend/fact_serving.js +1 -1
  137. package/dist/testing/cross_backend/peer_ping_ws.d.ts +23 -0
  138. package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -0
  139. package/dist/testing/cross_backend/peer_ping_ws.js +209 -0
  140. package/dist/testing/cross_backend/setup.d.ts +1 -1
  141. package/dist/testing/cross_backend/setup.js +1 -1
  142. package/dist/testing/cross_backend/testing_reset_actions.js +2 -2
  143. package/dist/testing/cross_backend/testing_server_core.d.ts +1 -1
  144. package/dist/testing/cross_backend/testing_server_core.js +1 -1
  145. package/dist/testing/cross_backend/ws_round_trip.js +1 -1
  146. package/dist/testing/db.js +4 -4
  147. package/dist/testing/middleware.js +1 -1
  148. package/dist/testing/role_grant_helpers.d.ts +1 -1
  149. package/dist/testing/rpc_helpers.d.ts +18 -1
  150. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  151. package/dist/testing/rpc_helpers.js +24 -2
  152. package/dist/testing/schema_generators.d.ts +1 -1
  153. package/dist/testing/schema_generators.js +1 -1
  154. package/dist/testing/stubs.js +1 -1
  155. package/dist/testing/testing_rate_limiter.d.ts +1 -1
  156. package/dist/testing/testing_rate_limiter.js +1 -1
  157. package/dist/testing/transports/bootstrap.d.ts +1 -1
  158. package/dist/testing/transports/bootstrap.js +1 -1
  159. package/dist/testing/transports/ws_client.d.ts +40 -1
  160. package/dist/testing/transports/ws_client.d.ts.map +1 -1
  161. package/dist/testing/transports/ws_client.js +46 -1
  162. package/dist/testing/transports/ws_transport.d.ts +10 -1
  163. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  164. package/dist/testing/transports/ws_transport.js +6 -10
  165. package/dist/testing/ws_round_trip.d.ts +13 -3
  166. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  167. package/dist/testing/ws_round_trip.js +16 -12
  168. package/dist/ui/AdminSurface.svelte +1 -1
  169. package/dist/ui/AppShell.svelte +1 -1
  170. package/dist/ui/BootstrapForm.svelte +1 -1
  171. package/dist/ui/CLAUDE.md +1 -1
  172. package/dist/ui/ConfirmButton.svelte +2 -2
  173. package/dist/ui/ConfirmButton.svelte.d.ts +1 -1
  174. package/dist/ui/LoginForm.svelte +1 -1
  175. package/dist/ui/MenuLink.svelte +1 -1
  176. package/dist/ui/MenuLink.svelte.d.ts +1 -1
  177. package/dist/ui/PopoverButton.svelte +1 -1
  178. package/dist/ui/PopoverButton.svelte.d.ts +1 -1
  179. package/dist/ui/SignupForm.svelte +1 -1
  180. package/dist/ui/account_sessions_state.svelte.js +1 -1
  181. package/dist/ui/admin_accounts_state.svelte.d.ts +1 -1
  182. package/dist/ui/admin_accounts_state.svelte.js +1 -1
  183. package/dist/ui/admin_invites_state.svelte.d.ts +1 -1
  184. package/dist/ui/admin_invites_state.svelte.js +1 -1
  185. package/dist/ui/admin_sessions_state.svelte.d.ts +1 -1
  186. package/dist/ui/app_settings_state.svelte.js +1 -1
  187. package/dist/ui/async_slot.svelte.d.ts +2 -2
  188. package/dist/ui/async_slot.svelte.js +2 -2
  189. package/dist/ui/audit_log_state.svelte.js +1 -1
  190. package/dist/ui/auth_state.svelte.d.ts +1 -1
  191. package/dist/ui/auth_state.svelte.js +3 -3
  192. package/dist/ui/format_scope.js +1 -1
  193. package/dist/ui/popover.svelte.d.ts +1 -1
  194. package/dist/ui/popover.svelte.js +2 -2
  195. package/dist/ui/position_helpers.js +1 -1
  196. package/dist/ui/role_grant_offers_state.svelte.js +1 -1
  197. package/dist/ui/sidebar_state.svelte.js +1 -1
  198. package/dist/ui/table_state.svelte.d.ts +1 -1
  199. package/dist/ui/table_state.svelte.js +2 -2
  200. package/package.json +2 -2
@@ -16,11 +16,17 @@ import '../assert_dev_env.js';
16
16
  * row runs `as` resolves to a `TestFixture` accessor via
17
17
  * `resolve_principal` — no inline credential minting.
18
18
  *
19
+ * Every response also passes an always-on **no-fingerprint** invariant
20
+ * (`assert_no_fingerprint_headers` over `FINGERPRINT_HEADERS` — `Server` /
21
+ * `X-Powered-By` / `WWW-Authenticate` must stay absent on both spines), and
22
+ * a row may pin further header expectations via `expect.headers`. Headers are
23
+ * deliberately kept out of the equivalence-group `{status, body}` comparison.
24
+ *
19
25
  * @module
20
26
  */
21
27
  import { assert, describe, test } from 'vitest';
22
28
  import { find_auth_route, rest_auth_route_suffixes, } from '../integration_helpers.js';
23
- import { find_rpc_action, rpc_call, resolve_rpc_endpoints_for_setup, } from '../rpc_helpers.js';
29
+ import { find_rpc_action, headers_to_record, rpc_call, resolve_rpc_endpoints_for_setup, } from '../rpc_helpers.js';
24
30
  import {} from './conformance_case.js';
25
31
  import { xfail_until } from './xfail.js';
26
32
  /**
@@ -91,19 +97,70 @@ const assert_fields = (actual, fields, label) => {
91
97
  assert.deepEqual(record[key], expected, `${label}: field '${key}'`);
92
98
  }
93
99
  };
100
+ /**
101
+ * Response headers that fingerprint the backend implementation or framework.
102
+ * Neither spine emits these; the runner asserts they stay absent on EVERY
103
+ * conformance response so a framework upgrade or consumer middleware that adds
104
+ * (say) `Server:` to one spine can't silently become a backend-identifying
105
+ * oracle. Lowercased — matched against the lowercased-key snapshot
106
+ * `headers_to_record` produces.
107
+ */
108
+ export const FINGERPRINT_HEADERS = [
109
+ 'server',
110
+ 'x-powered-by',
111
+ 'www-authenticate',
112
+ ];
113
+ /**
114
+ * Assert a response carries none of the `FINGERPRINT_HEADERS`. Run on every
115
+ * case unconditionally — the always-on no-fingerprint floor.
116
+ */
117
+ export const assert_no_fingerprint_headers = (headers, label) => {
118
+ for (const name of FINGERPRINT_HEADERS) {
119
+ assert.ok(!(name in headers), `${label}: response leaked backend-fingerprinting header '${name}: ${headers[name]}' — ` +
120
+ `Server / X-Powered-By / WWW-Authenticate must stay absent on both spines.`);
121
+ }
122
+ };
123
+ /**
124
+ * Assert each declared header expectation: a string value must be present and
125
+ * equal (header name matched case-insensitively), `null` must be absent. The
126
+ * negative-space twin for headers — `expect.headers` pins a header beyond the
127
+ * always-on no-fingerprint floor.
128
+ */
129
+ export const assert_expected_headers = (headers, expected, label) => {
130
+ for (const [name, value] of Object.entries(expected)) {
131
+ const key = name.toLowerCase();
132
+ if (value === null) {
133
+ assert.ok(!(key in headers), `${label}: header '${name}' expected absent but present as '${headers[key]}'`);
134
+ }
135
+ else {
136
+ assert.strictEqual(headers[key], value, `${label}: header '${name}'`);
137
+ }
138
+ }
139
+ };
140
+ /**
141
+ * Run a case's header invariants: the always-on no-fingerprint floor on every
142
+ * response plus any declared `expect.headers`. Header assertions are kept off
143
+ * the equivalence-group `{status, body}` normalized response — `Set-Cookie` /
144
+ * `Date` legitimately vary, so headers are never compared for byte-identity.
145
+ */
146
+ const assert_response_headers = (headers, c) => {
147
+ assert_no_fingerprint_headers(headers, c.name);
148
+ if (c.expect.headers)
149
+ assert_expected_headers(headers, c.expect.headers, c.name);
150
+ };
94
151
  const is_success_status = (status) => status >= 200 && status < 300;
95
152
  /**
96
153
  * Run one conformance case end-to-end: resolve the principal, dispatch the
97
- * request, and assert the expected status / reason / fields.
154
+ * request, and assert the expected status / reason / fields. Returns the
155
+ * normalized response so the caller can feed an equivalence group.
98
156
  */
99
157
  const run_case = async (c, options, resolved_rpc_endpoints) => {
100
158
  const fixture = await options.setup_test();
101
159
  const { transport, headers, suppress_default_origin } = await resolve_principal(fixture, c.request.as, options.principals);
102
160
  if (c.request.method.startsWith('/')) {
103
- await run_rest_case(c, options, transport, headers);
104
- return;
161
+ return run_rest_case(c, options, transport, headers);
105
162
  }
106
- await run_rpc_case(c, transport, headers, suppress_default_origin, resolved_rpc_endpoints);
163
+ return run_rpc_case(c, transport, headers, suppress_default_origin, resolved_rpc_endpoints);
107
164
  };
108
165
  /** Dispatch + assert a case targeting an RPC method. */
109
166
  const run_rpc_case = async (c, transport, headers, suppress_default_origin, resolved_rpc_endpoints) => {
@@ -121,6 +178,7 @@ const run_rpc_case = async (c, transport, headers, suppress_default_origin, reso
121
178
  ...(c.request.verb && { verb: c.request.verb }),
122
179
  ...(suppress_default_origin && { suppress_default_origin: true }),
123
180
  });
181
+ assert_response_headers(res.headers, c);
124
182
  if (is_success_status(c.expect.status)) {
125
183
  assert.ok(res.ok, `${c.name}: expected success (${c.expect.status}) but got error ${JSON.stringify(res.ok ? undefined : res.error)}`);
126
184
  assert.strictEqual(res.status, c.expect.status, `${c.name}: status`);
@@ -128,7 +186,7 @@ const run_rpc_case = async (c, transport, headers, suppress_default_origin, reso
128
186
  assert.ok(parsed.success, `${c.name}: result does not match spec.output: ${JSON.stringify(parsed.success ? undefined : parsed.error.issues)}`);
129
187
  if (c.expect.fields)
130
188
  assert_fields(res.result, c.expect.fields, c.name);
131
- return;
189
+ return { status: res.status, body: res.result };
132
190
  }
133
191
  assert.ok(!res.ok, `${c.name}: expected error status ${c.expect.status} but got success`);
134
192
  assert.strictEqual(res.status, c.expect.status, `${c.name}: error status`);
@@ -145,6 +203,7 @@ const run_rpc_case = async (c, transport, headers, suppress_default_origin, reso
145
203
  }
146
204
  if (c.expect.fields)
147
205
  assert_fields(res.error.data, c.expect.fields, c.name);
206
+ return { status: res.status, body: res.error };
148
207
  };
149
208
  /** Dispatch + assert a case targeting one of the 6 REST auth routes. */
150
209
  const run_rest_case = async (c, options, transport, headers) => {
@@ -165,6 +224,7 @@ const run_rest_case = async (c, options, transport, headers) => {
165
224
  c.request.params !== undefined && { body: JSON.stringify(c.request.params) }),
166
225
  };
167
226
  const response = await transport(route.path, init);
227
+ assert_response_headers(headers_to_record(response.headers), c);
168
228
  assert.strictEqual(response.status, c.expect.status, `${c.name}: status`);
169
229
  const body = await response.json().catch(() => undefined);
170
230
  if (is_success_status(c.expect.status)) {
@@ -177,6 +237,7 @@ const run_rest_case = async (c, options, transport, headers) => {
177
237
  }
178
238
  if (c.expect.fields)
179
239
  assert_fields(body, c.expect.fields, c.name);
240
+ return { status: response.status, body };
180
241
  };
181
242
  /**
182
243
  * Register a `describe` block running every `ConformanceCase` as a
@@ -186,10 +247,28 @@ const run_rest_case = async (c, options, transport, headers) => {
186
247
  */
187
248
  export const describe_conformance_table_tests = (options) => {
188
249
  const resolved_rpc_endpoints = resolve_rpc_endpoints_for_setup(options.rpc_endpoints, options.session_options);
250
+ // Per-group normalized responses, populated as the member case tests run
251
+ // and consulted by the per-group byte-identity assertions registered after
252
+ // the case loop — vitest runs a describe's tests in registration order, so
253
+ // the group assertions execute last, after every member has recorded.
254
+ const equivalence_groups = new Map();
255
+ const group_names = new Set();
256
+ for (const c of options.cases) {
257
+ if (c.expect.equivalence_group)
258
+ group_names.add(c.expect.equivalence_group);
259
+ }
189
260
  describe(options.suite_name ?? 'conformance table', () => {
190
261
  for (const c of options.cases) {
191
262
  const label = c.note ? `${c.name} — ${c.note}` : c.name;
192
- const body = () => run_case(c, options, resolved_rpc_endpoints);
263
+ const group = c.expect.equivalence_group;
264
+ const body = async () => {
265
+ const normalized = await run_case(c, options, resolved_rpc_endpoints);
266
+ if (group) {
267
+ const members = equivalence_groups.get(group) ?? [];
268
+ members.push({ name: c.name, normalized });
269
+ equivalence_groups.set(group, members);
270
+ }
271
+ };
193
272
  if (c.xfail) {
194
273
  xfail_until(c.xfail.tracking_id, c.xfail.reason, label, body);
195
274
  }
@@ -197,5 +276,28 @@ export const describe_conformance_table_tests = (options) => {
197
276
  test(label, body);
198
277
  }
199
278
  }
279
+ // Negative-space gate: every member of an equivalence group must have
280
+ // produced a byte-identical `{status, body}`. This holds the impl under
281
+ // test to "a prober cannot distinguish these masked paths" — the
282
+ // promotion of a masked pair (wrong-password ≡ account-not-found,
283
+ // found-but-unauthorized ≡ not-found) from "same status + reason" to
284
+ // "wire-indistinguishable". Runs per impl, so each spine is held to it.
285
+ for (const group of group_names) {
286
+ test(`equivalence group '${group}' — members are wire-indistinguishable`, () => {
287
+ const members = equivalence_groups.get(group) ?? [];
288
+ assert.ok(members.length >= 2, `equivalence group '${group}': expected >= 2 members to have recorded, got ` +
289
+ `${members.length} — a group pins indistinguishability between paths, so it ` +
290
+ `needs at least a pair (check the member cases ran and weren't all xfail).`);
291
+ const [first, ...rest] = members;
292
+ // `members.length >= 2` guarantees `first` is defined; the explicit
293
+ // assert narrows it for the typechecker (noUncheckedIndexedAccess).
294
+ assert.ok(first);
295
+ for (const member of rest) {
296
+ assert.deepStrictEqual(member.normalized, first.normalized, `equivalence group '${group}': '${member.name}' is distinguishable from ` +
297
+ `'${first.name}' — a prober could tell these masked paths apart. The ` +
298
+ `normalized {status, body} must be byte-identical across all members.`);
299
+ }
300
+ });
301
+ }
200
302
  });
201
303
  };
@@ -13,7 +13,7 @@ import '../assert_dev_env.js';
13
13
  *
14
14
  * ```ts
15
15
  * import {create_cross_backend_global_setup} from
16
- * '@fuzdev/fuz_app/testing/cross_backend/create_cross_backend_global_setup.js';
16
+ * '@fuzdev/fuz_app/testing/cross_backend/create_cross_backend_global_setup.ts';
17
17
  * import {deno_backend_config, rust_backend_config} from './my_backend_config.js';
18
18
  * import './cross_test_types.js'; // augments inject('backend_handle')
19
19
  *
@@ -17,7 +17,7 @@ import '../assert_dev_env.js';
17
17
  *
18
18
  * ```ts
19
19
  * import {create_dual_spawn_global_setup} from
20
- * '@fuzdev/fuz_app/testing/cross_backend/create_dual_spawn_global_setup.js';
20
+ * '@fuzdev/fuz_app/testing/cross_backend/create_dual_spawn_global_setup.ts';
21
21
  * import {deno_backend_config, rust_backend_config} from './my_backend_config.js';
22
22
  * import './cross_test_types.js'; // augments the two provide keys
23
23
  *
@@ -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":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAC,mBAAmB,EAAE,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAA2B,KAAK,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAQ9F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAiBpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,mBAgBtC,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,oDAAoD;IACpD,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"}
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,EAAC,sBAAsB,EAAE,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAC/E,OAAO,KAAK,EAAC,mBAAmB,EAAE,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAC9E,OAAO,EAA2B,KAAK,gBAAgB,EAAC,MAAM,+BAA+B,CAAC;AAQ9F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAqBpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,mBAmBtC,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,oDAAoD;IACpD,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"}
@@ -35,6 +35,10 @@ export const ts_default_capabilities = Object.freeze({
35
35
  // posture). A Bun-served consumer overrides to `false` (see the bun spine
36
36
  // config) — fail-loud rather than silently skipping the smuggle detector.
37
37
  oversized_reject_closes_connection: true,
38
+ // The TS server can't yet initiate server→client requests (the
39
+ // `BackendWebsocketTransport.send()` request path is the deferred
40
+ // twin-impl convergence), so the TS family skips the peer/ping suite.
41
+ peer_request: false,
38
42
  });
39
43
  /**
40
44
  * Capabilities for the Rust family. The non-gating wiring facts live in
@@ -57,6 +61,9 @@ export const rust_default_capabilities = Object.freeze({
57
61
  // hyper sends an RST on the oversized-body reject — the connection closes
58
62
  // and the pipelined request is never reached.
59
63
  oversized_reject_closes_connection: true,
64
+ // Server-initiated requests landed Rust-first canonical — the Rust spine
65
+ // drives the `peer/ping` round-trip, so the family opts into the suite.
66
+ peer_request: true,
60
67
  });
61
68
  /**
62
69
  * Shape notes for TS-family backends — wiring facts, not gating flags.
@@ -23,7 +23,7 @@ import '../assert_dev_env.js';
23
23
  *
24
24
  * @module
25
25
  */
26
- import type { Logger } from '@fuzdev/fuz_util/log.js';
26
+ import type { Logger } from '@fuzdev/fuz_util/log.ts';
27
27
  import type { NotificationSender } from '../../auth/role_grant_offer_notifications.js';
28
28
  import { type RoleSchemaResult } from '../../auth/role_schema.js';
29
29
  import { type SessionOptions } from '../../auth/session_cookie.js';
@@ -38,7 +38,7 @@ import '../assert_dev_env.js';
38
38
  */
39
39
  import { describe, assert } from 'vitest';
40
40
  import { z } from 'zod';
41
- import { FactHashSchema } from '@fuzdev/fuz_util/fact_hash.js';
41
+ import { FactHashSchema } from '@fuzdev/fuz_util/fact_hash.ts';
42
42
  import { CellCreateOutput } from '../../auth/cell_action_specs.js';
43
43
  import { test_if } from './capabilities.js';
44
44
  import { cross_rpc_call, expect_output } from './cell_cross_helpers.js';
@@ -0,0 +1,23 @@
1
+ import '../assert_dev_env.js';
2
+ import { type BackendCapabilities } from './capabilities.js';
3
+ import type { SetupTest } from './setup.js';
4
+ /** Configuration for {@link describe_peer_ping_ws_tests}. */
5
+ export interface PeerPingWsTestOptions {
6
+ /** Per-test fixture producer (`default_cross_process_setup(handle, ...)`). */
7
+ readonly setup_test: SetupTest;
8
+ /** Backend capability flags; every case gates on `capabilities.peer_request`. */
9
+ readonly capabilities: BackendCapabilities;
10
+ /** Base URL the backend is reachable at (e.g. `http://localhost:1177`). */
11
+ readonly base_url: string;
12
+ /** WebSocket endpoint path on the backend (e.g. `/api/ws`). */
13
+ readonly ws_path: string;
14
+ }
15
+ /**
16
+ * Register the server-initiated `peer/ping` suite — a positive round-trip
17
+ * plus the security negatives the design doc's §Security surface mandates
18
+ * (unsolicited-response rejection, per-connection id isolation, never-reply
19
+ * `Timeout`, wrong-shape reply rejection, plus client-error forwarding and
20
+ * the HTTP no-transport path). Gated on `capabilities.peer_request`.
21
+ */
22
+ export declare const describe_peer_ping_ws_tests: (options: PeerPingWsTestOptions) => void;
23
+ //# sourceMappingURL=peer_ping_ws.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"peer_ping_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/peer_ping_ws.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAkD9B,OAAO,EAAC,KAAK,mBAAmB,EAAU,MAAM,mBAAmB,CAAC;AACpE,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,YAAY,CAAC;AAkB1C,6DAA6D;AAC7D,MAAM,WAAW,qBAAqB;IACrC,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,iFAAiF;IACjF,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CACzB;AAED;;;;;;GAMG;AACH,eAAO,MAAM,2BAA2B,GAAI,SAAS,qBAAqB,KAAG,IAmN5E,CAAC"}
@@ -0,0 +1,209 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-process **server-initiated `peer/ping`** suite — the machinery
4
+ * proof for `ActionPeer` (a backend initiating a JSON-RPC request to a
5
+ * connected client and awaiting its typed reply). The sibling of the
6
+ * one-way notification suite (`role_grant_offer_notification_ws.ts`),
7
+ * extended from server→client *notifications* to server→client
8
+ * *request/response*.
9
+ *
10
+ * `peer/ping` is `initiator: both`: the client→server direction already
11
+ * exists as `heartbeat`; the new server→client direction is what this
12
+ * exercises. The observable trigger is the client invoking the `peer/ping`
13
+ * **action** over its own socket — the handler turns around and *initiates*
14
+ * a `peer/ping` request back to that socket, awaits the client's echo,
15
+ * validates it against `PingResponse`, and returns the validated shape. So
16
+ * one client RPC drives the whole round-trip, and every outcome
17
+ * (success / `Timeout` / wrong-shape / client-error) surfaces as that RPC's
18
+ * wire response — directly assertable.
19
+ *
20
+ * The client side attaches an `on_request` responder **at construction**
21
+ * (via `create_ws_transport`'s seam) so the server-initiated request is
22
+ * answered as soon as it arrives. Security negatives use the raw
23
+ * `WsClient.send` to inject unsolicited / cross-connection frames.
24
+ *
25
+ * **Per-spec auth.** `peer/ping` is `auth: public` (a liveness echo is
26
+ * non-sensitive — see the design doc); the WS upgrade itself still
27
+ * authenticates, so the suite drives it over the keeper's session.
28
+ *
29
+ * Gated on `capabilities.peer_request` — `true` only for the Rust spine
30
+ * (server-initiated requests landed Rust-first canonical); the TS family
31
+ * skips until its server transport's request path lands (deferred twin-impl
32
+ * convergence). Cross-process only: `create_ws_transport` needs a real bound
33
+ * socket, so wire it from a `*.cross.test.ts`.
34
+ *
35
+ * @module
36
+ */
37
+ import { assert, describe } from 'vitest';
38
+ import { rpc_call } from '../rpc_helpers.js';
39
+ import { create_ws_transport } from '../transports/ws_transport.js';
40
+ import { is_response_for, } from '../transports/ws_client.js';
41
+ import { create_jsonrpc_request, is_jsonrpc_request } from '../../http/jsonrpc_helpers.js';
42
+ import { test_if } from './capabilities.js';
43
+ /** JSON-RPC endpoint path — matches the spine's `/api/rpc` (and the forge's). */
44
+ const RPC_PATH = '/api/rpc';
45
+ /** Wire method (both directions). Mirrors the Rust `PEER_PING_METHOD`. */
46
+ const PEER_PING_METHOD = 'peer/ping';
47
+ // Stable `data.reason` discriminators — the cross-impl wire contract, kept
48
+ // as an independent TS-side copy of the Rust `REASON_PEER_*` constants
49
+ // (parity by test, not codegen).
50
+ const REASON_PEER_TIMEOUT = 'peer_timeout';
51
+ const REASON_PEER_INVALID_REPLY = 'peer_ping_invalid_reply';
52
+ const REASON_PEER_NO_TRANSPORT = 'peer_no_transport';
53
+ /** A reply payload that satisfies the Rust `PingResponse` shape. */
54
+ const valid_reply = (nonce) => ({ nonce, protocol_version: 1 });
55
+ /**
56
+ * Register the server-initiated `peer/ping` suite — a positive round-trip
57
+ * plus the security negatives the design doc's §Security surface mandates
58
+ * (unsolicited-response rejection, per-connection id isolation, never-reply
59
+ * `Timeout`, wrong-shape reply rejection, plus client-error forwarding and
60
+ * the HTTP no-transport path). Gated on `capabilities.peer_request`.
61
+ */
62
+ export const describe_peer_ping_ws_tests = (options) => {
63
+ const { setup_test, capabilities, base_url, ws_path } = options;
64
+ // -- shared helpers -------------------------------------------------------
65
+ /** Open a WS transport for a session cookie, with an optional responder. */
66
+ const open_ws = (cookie, on_request) => {
67
+ assert.ok(cookie, 'expected a session cookie for the WS upgrade');
68
+ return create_ws_transport({
69
+ base_url,
70
+ ws_path,
71
+ cookies: [cookie],
72
+ origin: base_url,
73
+ on_request,
74
+ });
75
+ };
76
+ /** A responder that echoes a valid `PingResponse` for every `peer/ping`. */
77
+ const echo_responder = (req) => {
78
+ const nonce = req.params?.nonce ?? 0;
79
+ return { result: valid_reply(nonce) };
80
+ };
81
+ /**
82
+ * Invoke `peer/ping` over the socket and return the **raw** response
83
+ * frame (success or error) so the negatives can assert on
84
+ * `error.data.reason`. The client wait is generous (longer than any
85
+ * server-side `timeout_ms`) so the server's own outcome is what we read.
86
+ */
87
+ const ping_raw = (ws, id, params) => ws
88
+ .send(create_jsonrpc_request(PEER_PING_METHOD, params, id))
89
+ .then(() => ws.wait_for(is_response_for(id), 8000));
90
+ const error_reason = (frame) => frame.error.data?.reason;
91
+ // -- tests ----------------------------------------------------------------
92
+ describe('peer/ping server-initiated request (cross-process)', () => {
93
+ test_if(capabilities.peer_request, 'a client peer/ping round-trips: server pings back, validates the echo, returns it', async () => {
94
+ const fixture = await setup_test();
95
+ const ws = await open_ws(fixture.create_session_headers().cookie, echo_responder);
96
+ try {
97
+ const result = await ws.request(1, PEER_PING_METHOD, { nonce: 42 }, 8000);
98
+ assert.strictEqual(result.nonce, 42, 'the server echoes the issued nonce back');
99
+ assert.strictEqual(typeof result.protocol_version, 'number');
100
+ }
101
+ finally {
102
+ await ws.close();
103
+ }
104
+ });
105
+ test_if(capabilities.peer_request, 'a never-replying client yields a server-side Timeout (the liveness signal)', async () => {
106
+ const fixture = await setup_test();
107
+ // Responder swallows the ping (returns nothing) — the half-open peer.
108
+ const ws = await open_ws(fixture.create_session_headers().cookie, () => undefined);
109
+ try {
110
+ // Short server-side deadline so the negative runs fast.
111
+ const frame = await ping_raw(ws, 2, { nonce: 1, timeout_ms: 300 });
112
+ assert.ok('error' in frame, 'a swallowed ping must error, not succeed');
113
+ assert.strictEqual(error_reason(frame), REASON_PEER_TIMEOUT);
114
+ }
115
+ finally {
116
+ await ws.close();
117
+ }
118
+ });
119
+ test_if(capabilities.peer_request, 'a wrong-shape reply is rejected at the validation boundary, never handed to the caller', async () => {
120
+ const fixture = await setup_test();
121
+ // Reply that fails `PingResponse` validation (nonce wrong type,
122
+ // protocol_version missing).
123
+ const ws = await open_ws(fixture.create_session_headers().cookie, () => ({
124
+ result: { nonce: 'not-a-number' },
125
+ }));
126
+ try {
127
+ const frame = await ping_raw(ws, 3, { nonce: 1, timeout_ms: 2000 });
128
+ assert.ok('error' in frame, 'a malformed reply must error, not return garbage');
129
+ assert.strictEqual(error_reason(frame), REASON_PEER_INVALID_REPLY);
130
+ }
131
+ finally {
132
+ await ws.close();
133
+ }
134
+ });
135
+ test_if(capabilities.peer_request, 'a client JSON-RPC error reply surfaces as the action error (ClientError forwarded)', async () => {
136
+ const fixture = await setup_test();
137
+ const ws = await open_ws(fixture.create_session_headers().cookie, () => ({
138
+ error: { code: -32603, message: 'client refused', data: { reason: 'client_says_no' } },
139
+ }));
140
+ try {
141
+ const frame = await ping_raw(ws, 4, { nonce: 1, timeout_ms: 2000 });
142
+ assert.ok('error' in frame, 'a client error reply must surface as an error');
143
+ assert.strictEqual(frame.error.code, -32603);
144
+ assert.strictEqual(error_reason(frame), 'client_says_no');
145
+ }
146
+ finally {
147
+ await ws.close();
148
+ }
149
+ });
150
+ test_if(capabilities.peer_request, 'an unsolicited response is dropped: it resolves nothing and the socket survives', async () => {
151
+ const fixture = await setup_test();
152
+ const ws = await open_ws(fixture.create_session_headers().cookie, echo_responder);
153
+ try {
154
+ // Inject a reply for an id the server never issued on this socket.
155
+ await ws.send({ jsonrpc: '2.0', id: 's999999', result: valid_reply(7) });
156
+ // A legitimate round-trip still works → the junk frame neither
157
+ // crashed the read loop nor corrupted the pending registry.
158
+ const result = await ws.request(5, PEER_PING_METHOD, { nonce: 99 }, 8000);
159
+ assert.strictEqual(result.nonce, 99);
160
+ }
161
+ finally {
162
+ await ws.close();
163
+ }
164
+ });
165
+ test_if(capabilities.peer_request, 'a reply on socket B never resolves a request issued on socket A (per-connection isolation)', async () => {
166
+ const fixture = await setup_test();
167
+ // A has NO responder, so the inbound server-initiated request is
168
+ // surfaced as a message A can observe (to learn its server id);
169
+ // A never answers it.
170
+ const ws_a = await open_ws(fixture.create_session_headers().cookie);
171
+ const ws_b = await open_ws(fixture.create_session_headers().cookie, echo_responder);
172
+ try {
173
+ // A invokes peer/ping with a short server-side deadline; don't
174
+ // await yet — observe the server→client request first.
175
+ const ping_promise = ping_raw(ws_a, 6, { nonce: 1, timeout_ms: 1200 });
176
+ // The server-initiated request A received (id "sN", nonce 1).
177
+ const inbound = await ws_a.wait_for((m) => is_jsonrpc_request(m) && m.method === PEER_PING_METHOD, 5000);
178
+ // B replies for A's id — a cross-connection echo. Correct
179
+ // isolation drops it (not pending on B), so A still times out.
180
+ await ws_b.send({
181
+ jsonrpc: '2.0',
182
+ id: inbound.id,
183
+ result: valid_reply(inbound.params.nonce),
184
+ });
185
+ const frame = await ping_promise;
186
+ assert.ok('error' in frame, "B's cross-connection reply must not resolve A's request");
187
+ assert.strictEqual(error_reason(frame), REASON_PEER_TIMEOUT);
188
+ }
189
+ finally {
190
+ await ws_a.close();
191
+ await ws_b.close();
192
+ }
193
+ });
194
+ test_if(capabilities.peer_request, 'peer/ping over HTTP (no return socket) is rejected as no-transport', async () => {
195
+ const fixture = await setup_test();
196
+ const res = await rpc_call({
197
+ app: fixture.transport,
198
+ path: RPC_PATH,
199
+ method: PEER_PING_METHOD,
200
+ params: { nonce: 1 },
201
+ headers: fixture.create_session_headers(),
202
+ });
203
+ assert.isFalse(res.ok, 'HTTP peer/ping has no socket to ping');
204
+ if (!res.ok) {
205
+ assert.strictEqual(res.error.data?.reason, REASON_PEER_NO_TRANSPORT);
206
+ }
207
+ });
208
+ });
209
+ };
@@ -1,5 +1,5 @@
1
1
  import '../assert_dev_env.js';
2
- import { Uuid } from '@fuzdev/fuz_util/id.js';
2
+ import { Uuid } from '@fuzdev/fuz_util/id.ts';
3
3
  import type { TestAccount } from '../app_server.js';
4
4
  import type { BackendCapabilities } from './capabilities.js';
5
5
  import { type FetchTransport } from '../transports/fetch_transport.js';
@@ -22,7 +22,7 @@ import '../assert_dev_env.js';
22
22
  * @module
23
23
  */
24
24
  import { z } from 'zod';
25
- import { Uuid } from '@fuzdev/fuz_util/id.js';
25
+ import { Uuid } from '@fuzdev/fuz_util/id.ts';
26
26
  import { DAEMON_TOKEN_HEADER } from '../../auth/daemon_token.js';
27
27
  import { USERNAME_LENGTH_MAX } from '../../primitive_schemas.js';
28
28
  import { DEFAULT_TEST_PASSWORD } from '../test_credentials.js';
@@ -66,8 +66,8 @@ import '../assert_dev_env.js';
66
66
  * @module
67
67
  */
68
68
  import { z } from 'zod';
69
- import { Uuid } from '@fuzdev/fuz_util/id.js';
70
- import { fact_hash_bytes, FactHashSchema } from '@fuzdev/fuz_util/fact_hash.js';
69
+ import { Uuid } from '@fuzdev/fuz_util/id.ts';
70
+ import { fact_hash_bytes, FactHashSchema } from '@fuzdev/fuz_util/fact_hash.ts';
71
71
  import { rpc_action } from '../../actions/action_rpc.js';
72
72
  import { query_put_fact } from '../../db/fact_queries.js';
73
73
  import { ROLE_ADMIN, ROLE_KEEPER } from '../../auth/role_schema.js';
@@ -28,7 +28,7 @@ import '../assert_dev_env.js';
28
28
  */
29
29
  import type { Context, Hono } from 'hono';
30
30
  import type { UpgradeWebSocket } from 'hono/ws';
31
- import { type Logger as LoggerType } from '@fuzdev/fuz_util/log.js';
31
+ import { type Logger as LoggerType } from '@fuzdev/fuz_util/log.ts';
32
32
  import type { RuntimeDeps } from '../../runtime/deps.js';
33
33
  /**
34
34
  * Adapter-built handle to a bound HTTP server.
@@ -1,5 +1,5 @@
1
1
  import '../assert_dev_env.js';
2
- import { Logger } from '@fuzdev/fuz_util/log.js';
2
+ import { Logger } from '@fuzdev/fuz_util/log.ts';
3
3
  import { write_daemon_info, read_daemon_info, is_daemon_running } from '../../cli/daemon.js';
4
4
  /**
5
5
  * Loopback bind hosts — the only ones the test binary may serve on. It ships
@@ -41,7 +41,7 @@ import '../assert_dev_env.js';
41
41
  * @module
42
42
  */
43
43
  import { assert, describe } from 'vitest';
44
- import { assert_rejects } from '@fuzdev/fuz_util/testing.js';
44
+ import { assert_rejects } from '@fuzdev/fuz_util/testing.ts';
45
45
  import { heartbeat_action_spec } from '../../actions/heartbeat.js';
46
46
  import { account_session_revoke_all_action_spec } from '../../auth/account_action_specs.js';
47
47
  import { create_ws_transport } from '../transports/ws_transport.js';
@@ -8,9 +8,9 @@ import './assert_dev_env.js';
8
8
  *
9
9
  * @example
10
10
  * ```ts
11
- * import {create_pglite_factory, create_pg_factory} from '@fuzdev/fuz_app/testing/db.js';
12
- * import {run_migrations} from '@fuzdev/fuz_app/db/migrate.js';
13
- * import {auth_migration_ns} from '@fuzdev/fuz_app/auth/migrations.js';
11
+ * import {create_pglite_factory, create_pg_factory} from '@fuzdev/fuz_app/testing/db.ts';
12
+ * import {run_migrations} from '@fuzdev/fuz_app/db/migrate.ts';
13
+ * import {auth_migration_ns} from '@fuzdev/fuz_app/auth/migrations.ts';
14
14
  *
15
15
  * const init_schema = async (db) => {
16
16
  * await run_migrations(db, [auth_migration_ns]);
@@ -23,7 +23,7 @@ import './assert_dev_env.js';
23
23
  * @module
24
24
  */
25
25
  import { describe, beforeAll, beforeEach, afterAll } from 'vitest';
26
- import { to_error_message } from '@fuzdev/fuz_util/error.js';
26
+ import { to_error_message } from '@fuzdev/fuz_util/error.ts';
27
27
  import { create_pglite_db } from '../db/db_pglite.js';
28
28
  import { assert_valid_sql_identifier } from '../db/sql_identifier.js';
29
29
  import { create_pg_db } from '../db/db_pg.js';
@@ -10,7 +10,7 @@ import './assert_dev_env.js';
10
10
  */
11
11
  import { vi, test, assert, describe } from 'vitest';
12
12
  import { Hono } from 'hono';
13
- import { Logger } from '@fuzdev/fuz_util/log.js';
13
+ import { Logger } from '@fuzdev/fuz_util/log.ts';
14
14
  import { create_bearer_auth_middleware } from '../auth/bearer_auth.js';
15
15
  import { query_validate_api_token } from '../auth/api_token_queries.js';
16
16
  import { query_account_by_id, query_actor_by_id, query_actors_by_account, } from '../auth/account_queries.js';
@@ -1,5 +1,5 @@
1
1
  import './assert_dev_env.js';
2
- import type { Uuid } from '@fuzdev/fuz_util/id.js';
2
+ import type { Uuid } from '@fuzdev/fuz_util/id.ts';
3
3
  import type { TestAccount, TestApp } from './app_server.js';
4
4
  import type { TestFixture } from './cross_backend/setup.js';
5
5
  import { type RpcCallArgs } from './rpc_helpers.js';