@fuzdev/fuz_app 0.118.0 → 0.120.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 (169) hide show
  1. package/dist/actions/CLAUDE.md +301 -73
  2. package/dist/actions/action_rpc.d.ts.map +1 -1
  3. package/dist/actions/action_rpc.js +16 -1
  4. package/dist/actions/connection_closer.d.ts +136 -51
  5. package/dist/actions/connection_closer.d.ts.map +1 -1
  6. package/dist/actions/connection_closer.js +133 -34
  7. package/dist/actions/register_action_ws.d.ts +176 -24
  8. package/dist/actions/register_action_ws.d.ts.map +1 -1
  9. package/dist/actions/register_action_ws.js +581 -241
  10. package/dist/actions/register_ws_endpoint.d.ts +1 -0
  11. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  12. package/dist/actions/register_ws_endpoint.js +1 -0
  13. package/dist/actions/socket.svelte.d.ts +43 -9
  14. package/dist/actions/socket.svelte.d.ts.map +1 -1
  15. package/dist/actions/socket.svelte.js +59 -8
  16. package/dist/actions/transports.d.ts +55 -1
  17. package/dist/actions/transports.d.ts.map +1 -1
  18. package/dist/actions/transports.js +55 -1
  19. package/dist/actions/transports_ws_auth_guard.d.ts +53 -67
  20. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  21. package/dist/actions/transports_ws_auth_guard.js +87 -111
  22. package/dist/actions/transports_ws_backend.d.ts +212 -30
  23. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  24. package/dist/actions/transports_ws_backend.js +327 -70
  25. package/dist/actions/ws_endpoint_spec.d.ts +26 -13
  26. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  27. package/dist/auth/CLAUDE.md +113 -32
  28. package/dist/auth/account_actions.d.ts +12 -18
  29. package/dist/auth/account_actions.d.ts.map +1 -1
  30. package/dist/auth/account_actions.js +47 -33
  31. package/dist/auth/account_routes.d.ts +5 -11
  32. package/dist/auth/account_routes.d.ts.map +1 -1
  33. package/dist/auth/account_routes.js +40 -36
  34. package/dist/auth/account_schema.d.ts +27 -0
  35. package/dist/auth/account_schema.d.ts.map +1 -1
  36. package/dist/auth/account_schema.js +22 -0
  37. package/dist/auth/admin_actions.d.ts +8 -17
  38. package/dist/auth/admin_actions.d.ts.map +1 -1
  39. package/dist/auth/admin_actions.js +33 -21
  40. package/dist/auth/api_token_queries.d.ts +23 -2
  41. package/dist/auth/api_token_queries.d.ts.map +1 -1
  42. package/dist/auth/api_token_queries.js +39 -3
  43. package/dist/auth/audit_emitter.d.ts +61 -38
  44. package/dist/auth/audit_emitter.d.ts.map +1 -1
  45. package/dist/auth/audit_emitter.js +57 -14
  46. package/dist/auth/audit_log_route_schema.js +7 -7
  47. package/dist/auth/audit_log_routes.d.ts +31 -6
  48. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  49. package/dist/auth/audit_log_routes.js +85 -11
  50. package/dist/auth/audit_log_schema.d.ts +48 -0
  51. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  52. package/dist/auth/audit_log_schema.js +58 -0
  53. package/dist/auth/cleanup.d.ts +203 -28
  54. package/dist/auth/cleanup.d.ts.map +1 -1
  55. package/dist/auth/cleanup.js +222 -26
  56. package/dist/auth/deps.d.ts +31 -3
  57. package/dist/auth/deps.d.ts.map +1 -1
  58. package/dist/auth/resolved_auth.d.ts +105 -0
  59. package/dist/auth/resolved_auth.d.ts.map +1 -0
  60. package/dist/auth/resolved_auth.js +124 -0
  61. package/dist/auth/session_middleware.d.ts +8 -1
  62. package/dist/auth/session_middleware.d.ts.map +1 -1
  63. package/dist/auth/session_middleware.js +9 -3
  64. package/dist/auth/session_queries.d.ts +31 -6
  65. package/dist/auth/session_queries.d.ts.map +1 -1
  66. package/dist/auth/session_queries.js +31 -6
  67. package/dist/auth/standard_rpc_actions.d.ts +9 -9
  68. package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
  69. package/dist/auth/standard_rpc_actions.js +3 -3
  70. package/dist/db/CLAUDE.md +19 -8
  71. package/dist/db/fact_disk_storage.d.ts +10 -8
  72. package/dist/db/fact_disk_storage.d.ts.map +1 -1
  73. package/dist/db/fact_disk_storage.js +13 -17
  74. package/dist/db/fact_store.d.ts +39 -38
  75. package/dist/db/fact_store.d.ts.map +1 -1
  76. package/dist/db/fact_store.js +48 -86
  77. package/dist/db/file_fact_url.d.ts +6 -6
  78. package/dist/db/file_fact_url.js +6 -6
  79. package/dist/db/status.d.ts +58 -1
  80. package/dist/db/status.d.ts.map +1 -1
  81. package/dist/db/status.js +205 -77
  82. package/dist/http/CLAUDE.md +7 -3
  83. package/dist/http/db_routes.d.ts +6 -2
  84. package/dist/http/db_routes.d.ts.map +1 -1
  85. package/dist/http/db_routes.js +2 -2
  86. package/dist/periodic.d.ts +80 -0
  87. package/dist/periodic.d.ts.map +1 -0
  88. package/dist/periodic.js +122 -0
  89. package/dist/realtime/sse_auth_guard.d.ts +79 -49
  90. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  91. package/dist/realtime/sse_auth_guard.js +103 -73
  92. package/dist/realtime/subscriber_registry.d.ts +119 -8
  93. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  94. package/dist/realtime/subscriber_registry.js +184 -25
  95. package/dist/server/app_backend.d.ts +12 -1
  96. package/dist/server/app_backend.d.ts.map +1 -1
  97. package/dist/server/app_backend.js +5 -2
  98. package/dist/server/app_server.d.ts +55 -15
  99. package/dist/server/app_server.d.ts.map +1 -1
  100. package/dist/server/app_server.js +41 -13
  101. package/dist/server/serve_fact_route.d.ts +10 -7
  102. package/dist/server/serve_fact_route.d.ts.map +1 -1
  103. package/dist/server/serve_fact_route.js +11 -8
  104. package/dist/testing/CLAUDE.md +140 -48
  105. package/dist/testing/app_server.d.ts +2 -2
  106. package/dist/testing/app_server.d.ts.map +1 -1
  107. package/dist/testing/app_server.js +11 -2
  108. package/dist/testing/audit_drift_guard.d.ts +1 -48
  109. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  110. package/dist/testing/audit_drift_guard.js +0 -42
  111. package/dist/testing/connection_closer_helpers.d.ts +9 -22
  112. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  113. package/dist/testing/connection_closer_helpers.js +11 -17
  114. package/dist/testing/cross_backend/capabilities.d.ts +67 -13
  115. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  116. package/dist/testing/cross_backend/capabilities.js +27 -12
  117. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  118. package/dist/testing/cross_backend/default_backend_configs.js +21 -0
  119. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  120. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
  121. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  122. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +4 -1
  123. package/dist/testing/cross_backend/session_cap.d.ts +13 -0
  124. package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
  125. package/dist/testing/cross_backend/session_cap.js +55 -19
  126. package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
  127. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
  128. package/dist/testing/cross_backend/sse_round_trip.js +93 -26
  129. package/dist/testing/cross_backend/testing_server_bun.js +4 -4
  130. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  131. package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -2
  132. package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
  133. package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
  134. package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
  135. package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
  136. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  137. package/dist/testing/cross_backend/ws_round_trip.js +156 -19
  138. package/dist/testing/sse_round_trip.d.ts +6 -4
  139. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  140. package/dist/testing/stubs.d.ts +1 -1
  141. package/dist/testing/stubs.d.ts.map +1 -1
  142. package/dist/testing/stubs.js +6 -4
  143. package/dist/testing/transports/sse_transport.d.ts +1 -1
  144. package/dist/testing/transports/ws_client.d.ts +7 -0
  145. package/dist/testing/transports/ws_client.d.ts.map +1 -1
  146. package/dist/testing/transports/ws_raw_client.d.ts +51 -0
  147. package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
  148. package/dist/testing/transports/ws_raw_client.js +156 -0
  149. package/dist/testing/transports/ws_transport.d.ts +28 -0
  150. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  151. package/dist/testing/transports/ws_transport.js +45 -1
  152. package/dist/testing/ws_round_trip.d.ts +8 -3
  153. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  154. package/dist/testing/ws_round_trip.js +19 -4
  155. package/dist/ui/AdminAccounts.svelte +4 -3
  156. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  157. package/dist/ui/AdminOverview.svelte +3 -1
  158. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  159. package/dist/ui/CLAUDE.md +22 -6
  160. package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
  161. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  162. package/dist/ui/admin_accounts_state.svelte.js +16 -0
  163. package/dist/ui/auth_state.svelte.d.ts +25 -1
  164. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  165. package/dist/ui/auth_state.svelte.js +28 -1
  166. package/package.json +2 -2
  167. package/dist/server/file_fact_fetcher.d.ts +0 -42
  168. package/dist/server/file_fact_fetcher.d.ts.map +0 -1
  169. package/dist/server/file_fact_fetcher.js +0 -60
@@ -1,7 +1,6 @@
1
1
  import './assert_dev_env.js';
2
2
  import { assert, beforeEach, afterEach } from 'vitest';
3
3
  import { get_audit_metadata_validation_failures, get_audit_unknown_event_type_failures, reset_audit_metadata_validation_failures, reset_audit_unknown_event_type_failures } from '../auth/audit_log_queries.js';
4
- import { create_audit_emitter } from '../auth/audit_emitter.js';
5
4
  /**
6
5
  * Register per-test `beforeEach` + `afterEach` hooks that catch any audit
7
6
  * emission with a metadata shape that fails its `audit_metadata_schemas`
@@ -95,44 +94,3 @@ export const create_recording_audit_emitter = (calls_ref) => {
95
94
  };
96
95
  return { emitter, calls };
97
96
  };
98
- /**
99
- * Build an `audit_factory` that produces a real `create_audit_emitter`
100
- * with its `emit` decorated to push a `{kind: 'emit', at: seq.value++}`
101
- * marker into a shared sequence + events array. Used by the close-vs-emit
102
- * ordering test to compose against a shared sequence counter (typically
103
- * `create_recording_closer(seq_ref)` capturing eager-close calls).
104
- *
105
- * Pass the returned factory through `create_test_app({audit_factory: …})`
106
- * — the test backend invokes it with its constructed `{db, log}` and
107
- * lands the decorated emitter on `backend.deps.audit`. Production
108
- * handlers dereference `deps.audit.emit` at call time, so the decorator
109
- * sees every subsequent handler invocation. The underlying `emit` still
110
- * runs — the decorator records the call, it does not suppress side
111
- * effects.
112
- *
113
- * **Scope — both `emit` and `emit_role_grant_target`.** The decorator
114
- * is captured by `emit_role_grant_target`'s closure inside
115
- * `create_audit_emitter` (and re-exposed as the outer `emit` slot), so
116
- * role-grant-shape emissions land in `events_ref` alongside bare `emit`
117
- * calls. `emit_pool` and `notify` are not decorated — they take
118
- * `AuditLogInput` / `AuditLogEvent` directly without going through
119
- * `emit`, so `emit_pool` writes skip capture. Close-firing handlers all reach for
120
- * `emit` or `emit_role_grant_target`, so the ordering test sees them
121
- * regardless of which entry point a future refactor picks.
122
- *
123
- * Optionally accept `extra_options` to thread `on_audit_event` /
124
- * `audit_log_config` into the inner emitter — useful when a test wants
125
- * both ordering capture and a real SSE/WS guard wired into the same
126
- * emitter chain.
127
- */
128
- export const create_emit_ordering_audit_factory = (seq_ref, events_ref, extra_options) => {
129
- return ({ db, log }) => create_audit_emitter({
130
- ...extra_options,
131
- db,
132
- log,
133
- emit_decorator: (inner) => (ctx, input) => {
134
- events_ref.push({ kind: 'emit', at: seq_ref.value++ });
135
- inner(ctx, input);
136
- }
137
- });
138
- };
@@ -1,15 +1,9 @@
1
1
  import './assert_dev_env.js';
2
2
  import type { ConnectionCloser } from '../actions/connection_closer.js';
3
- /**
4
- * Record of a single `ConnectionCloser` method invocation. `at` is the
5
- * value of a monotonically-increasing sequence counter at the time of
6
- * the call — pair with `create_emit_ordering_audit_factory` to record both
7
- * close + audit emit calls into the same sequence for ordering tests.
8
- */
3
+ /** Record of a single `ConnectionCloser` method invocation. */
9
4
  export interface RecordedClose {
10
5
  method: 'session' | 'token' | 'account';
11
6
  id: string;
12
- at: number;
13
7
  }
14
8
  export interface RecordingCloser {
15
9
  closer: ConnectionCloser;
@@ -17,24 +11,17 @@ export interface RecordingCloser {
17
11
  }
18
12
  /**
19
13
  * Build a `ConnectionCloser` that records every call into `calls` rather
20
- * than touching real transports. Each method returns 1 ("one socket
21
- * closed") regardless of whether a real socket exists — handlers
22
- * typically ignore the return value.
14
+ * than touching real transports. Each method returns 1 ("one connection
15
+ * closed") regardless of whether a real connection exists — handlers
16
+ * ignore the return value.
23
17
  *
24
- * Pass `seq_ref` to share the sequence counter with a sibling
25
- * `create_emit_ordering_audit_factory` so tests can pin close-vs-emit
26
- * ordering at the handler call site. Without `seq_ref`, the closer
27
- * uses a fresh internal counter — `at: N` values within a single test
28
- * are meaningful, but cannot be compared against audit emit ordering.
18
+ * To see a handler's closes, add the closer to the backend's
19
+ * `deps.connection_closer` (`RealtimeCloser.add`), or build the action
20
+ * factory's deps with it.
29
21
  */
30
- export declare const create_recording_closer: (seq_ref?: {
31
- value: number;
32
- }) => RecordingCloser;
22
+ export declare const create_recording_closer: () => RecordingCloser;
33
23
  /**
34
- * Pin `{method, id}` on a single recorded close call without baking in
35
- * the `at: N` sequence number. Use at every "did the closer fire?"
36
- * assertion site; the sequence number is only meaningful for dedicated
37
- * ordering tests (paired with `create_emit_ordering_audit_factory`).
24
+ * Pin `{method, id}` on a single recorded close call.
38
25
  *
39
26
  * Throws via `assert.ok` if `call` is `undefined` — index a recorded
40
27
  * `calls` array directly (`calls[0]`) and let this helper handle the
@@ -1 +1 @@
1
- {"version":3,"file":"connection_closer_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/connection_closer_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAI7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAExE;;;;;GAKG;AACH,MAAM,WAAW,aAAa;IAC7B,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IACxC,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;CAC5B;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB,GAAI,UAAU;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,KAAG,eAkBrE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,aAAa,GAAG,SAAS,EAC/B,QAAQ,SAAS,GAAG,OAAO,GAAG,SAAS,EACvC,IAAI,MAAM,KACR,IAIF,CAAC"}
1
+ {"version":3,"file":"connection_closer_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/connection_closer_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAI7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAExE,+DAA+D;AAC/D,MAAM,WAAW,aAAa;IAC7B,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IACxC,EAAE,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,QAAO,eAiB1C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,aAAa,GAAG,SAAS,EAC/B,QAAQ,SAAS,GAAG,OAAO,GAAG,SAAS,EACvC,IAAI,MAAM,KACR,IAIF,CAAC"}
@@ -2,40 +2,34 @@ import './assert_dev_env.js';
2
2
  import { assert } from 'vitest';
3
3
  /**
4
4
  * Build a `ConnectionCloser` that records every call into `calls` rather
5
- * than touching real transports. Each method returns 1 ("one socket
6
- * closed") regardless of whether a real socket exists — handlers
7
- * typically ignore the return value.
5
+ * than touching real transports. Each method returns 1 ("one connection
6
+ * closed") regardless of whether a real connection exists — handlers
7
+ * ignore the return value.
8
8
  *
9
- * Pass `seq_ref` to share the sequence counter with a sibling
10
- * `create_emit_ordering_audit_factory` so tests can pin close-vs-emit
11
- * ordering at the handler call site. Without `seq_ref`, the closer
12
- * uses a fresh internal counter — `at: N` values within a single test
13
- * are meaningful, but cannot be compared against audit emit ordering.
9
+ * To see a handler's closes, add the closer to the backend's
10
+ * `deps.connection_closer` (`RealtimeCloser.add`), or build the action
11
+ * factory's deps with it.
14
12
  */
15
- export const create_recording_closer = (seq_ref) => {
13
+ export const create_recording_closer = () => {
16
14
  const calls = [];
17
- const seq = seq_ref ?? { value: 0 };
18
15
  const closer = {
19
16
  close_sockets_for_session: (id) => {
20
- calls.push({ method: 'session', id, at: seq.value++ });
17
+ calls.push({ method: 'session', id });
21
18
  return 1;
22
19
  },
23
20
  close_sockets_for_token: (id) => {
24
- calls.push({ method: 'token', id, at: seq.value++ });
21
+ calls.push({ method: 'token', id });
25
22
  return 1;
26
23
  },
27
24
  close_sockets_for_account: (id) => {
28
- calls.push({ method: 'account', id, at: seq.value++ });
25
+ calls.push({ method: 'account', id });
29
26
  return 1;
30
27
  }
31
28
  };
32
29
  return { closer, calls };
33
30
  };
34
31
  /**
35
- * Pin `{method, id}` on a single recorded close call without baking in
36
- * the `at: N` sequence number. Use at every "did the closer fire?"
37
- * assertion site; the sequence number is only meaningful for dedicated
38
- * ordering tests (paired with `create_emit_ordering_audit_factory`).
32
+ * Pin `{method, id}` on a single recorded close call.
39
33
  *
40
34
  * Throws via `assert.ok` if `call` is `undefined` — index a recorded
41
35
  * `calls` array directly (`calls[0]`) and let this helper handle the
@@ -2,9 +2,9 @@ import '../assert_dev_env.js';
2
2
  /**
3
3
  * Backend wiring facts recorded for documentation — **not** gating flags.
4
4
  *
5
- * The companion to `BackendCapabilities`: where each capability flag has a
6
- * `test_if(capabilities.X, ...)` reader that skips a suite the backend doesn't
7
- * implement, nothing reads these. They record middleware / limiter wiring that
5
+ * The companion to `BackendCapabilities`: where each capability flag is read
6
+ * by a suite (a `test_if(capabilities.X, ...)` gate, or a fork of one case's
7
+ * assertion), nothing reads these. They record middleware / limiter wiring that
8
8
  * differs between the TS and Rust families (a backend-shape record) but gates
9
9
  * no cross test today. They live in their own type precisely so
10
10
  * `BackendCapabilities` stops claiming gating power it doesn't have — fold a
@@ -42,15 +42,17 @@ export interface BackendShapeNotes {
42
42
  export interface BackendCapabilities {
43
43
  /**
44
44
  * WebSocket transport is reachable end-to-end. Gates the cross-process
45
- * WS round-trip suite; the in-process `describe_ws_round_trip_tests`
46
- * runs against `register_action_ws` directly and ignores this flag.
45
+ * WS round-trip suite and the per-account connection-cap suite
46
+ * (`describe_ws_connection_cap_cross_tests`); the in-process
47
+ * `describe_ws_round_trip_tests` runs against `register_action_ws`
48
+ * directly and ignores this flag.
47
49
  */
48
50
  readonly ws: boolean;
49
51
  /**
50
52
  * SSE transport is reachable end-to-end. Gates the cross-process SSE
51
53
  * suite (`describe_cross_process_sse_tests` — connect, audit data frame,
52
- * close-on-revoke); in-process SSE uses the `on_audit_event` hook and
53
- * ignores this flag.
54
+ * close-on-revoke, the per-session stream cap); in-process SSE uses the
55
+ * `on_audit_event` hook and ignores this flag.
54
56
  */
55
57
  readonly sse: boolean;
56
58
  /**
@@ -159,6 +161,55 @@ export interface BackendCapabilities {
159
161
  * coverage.
160
162
  */
161
163
  readonly peer_request: boolean;
164
+ /**
165
+ * The backend's HTTP server accepts WebSocket frames written in the same
166
+ * TCP write as the upgrade request, handing them to the socket once the
167
+ * handshake completes. Gates the `frames sent with the handshake` case in
168
+ * `describe_cross_process_ws_tests` — the deterministic form of "frames
169
+ * that arrive before the connection is admitted wait for admission", since
170
+ * those frames are in the backend's hands before it can have admitted
171
+ * anything.
172
+ *
173
+ * `true` for Node (`ws`), Deno, and the Rust spine (hyper hands the
174
+ * leftover bytes to the upgraded stream); `false` for Bun, whose HTTP
175
+ * parser answers such a request `400` before any handler runs. A backend
176
+ * without it still runs the suite's client-library case (requests sent the
177
+ * moment the socket opens), which pins the same contract without forcing
178
+ * the ordering.
179
+ */
180
+ readonly ws_handshake_pipelining: boolean;
181
+ /**
182
+ * The backend's WebSocket endpoint mounts the self-service account actions
183
+ * (`account_session_revoke_all` among them), so a socket can revoke the
184
+ * session it is running on. Gates the self-revocation case in
185
+ * `describe_cross_process_ws_tests`: the socket reads the reply to its
186
+ * request, then is closed with `WS_CLOSE_SESSION_REVOKED`.
187
+ *
188
+ * `true` on fuz_app's own spine presets — the TS spine binary mounts
189
+ * `create_account_actions` on `/api/ws`, and the Rust `testing_spine_stub`
190
+ * serves one action registry on RPC and WS. `false` in the family defaults:
191
+ * a consumer's WS endpoint carries whatever its domain mounts, so it opts in
192
+ * once the account actions are there.
193
+ */
194
+ readonly ws_account_actions: boolean;
195
+ /**
196
+ * A socket that revokes the credential it is running on reads the reply to
197
+ * that request **before** the revocation's close. Read by the
198
+ * self-revocation case in `describe_cross_process_ws_tests` (which
199
+ * `ws_account_actions` gates): with it `true` the case requires the reply
200
+ * frame ahead of the `WS_CLOSE_SESSION_REVOKED` close; with it `false` the
201
+ * case requires the close alone.
202
+ *
203
+ * `true` for the TS family — `register_action_ws` sends the response and
204
+ * only then flushes the request's post-commit queue, where the close is.
205
+ * `false` for the Rust family: its dispatch runs the post-commit queue
206
+ * before handing the response to the socket loop, and the loop, finding the
207
+ * connection already closed, writes the close frame and drops the response.
208
+ * The caller's revocation still took effect; it is not told so. The flag
209
+ * records that divergence so the suite stays green on both spines without
210
+ * hiding it — flip it once the Rust spine answers first.
211
+ */
212
+ readonly ws_self_revocation_reply: boolean;
162
213
  /**
163
214
  * A test `CellCreateAuthorize` policy is live-mounted on the backend's cell
164
215
  * layer — creating a `kind: 'gated'` cell requires the `participant` role
@@ -177,12 +228,15 @@ export interface BackendCapabilities {
177
228
  /**
178
229
  * Capability declarations for the in-process Hono transport. Nearly every
179
230
  * flag is `true` because in-process testing exercises the full backend
180
- * with no missing optional behaviors. The one exception is `peer_request`:
181
- * `describe_peer_ping_ws_tests` is cross-process-only (it needs a real bound
182
- * socket with an `on_request` responder via `create_ws_transport`), so the
183
- * in-process driver never runs it — the transport itself supports
184
- * server-initiated requests. Cross-process consumers declare each flag
185
- * explicitly per backend.
231
+ * with no missing optional behaviors. The exceptions are the flags whose
232
+ * cases are cross-process-only: `peer_request` (`describe_peer_ping_ws_tests`
233
+ * needs a real bound socket with an `on_request` responder via
234
+ * `create_ws_transport`, so the in-process driver never runs it — the
235
+ * transport itself supports server-initiated requests),
236
+ * `ws_handshake_pipelining` (raw bytes on a real socket),
237
+ * `ws_account_actions` (a real socket's reply-then-close ordering), and
238
+ * `cell_gated_create`. Cross-process consumers declare each flag explicitly
239
+ * per backend.
186
240
  */
187
241
  export declare const in_process_capabilities: BackendCapabilities;
188
242
  /**
@@ -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;AAwC9B;;;;;;;;;;;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;;;;OAIG;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;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAcpC,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;;;;;;;;;;;;;;;;;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;;;;;;;;;;;;OAYG;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"}
@@ -7,25 +7,29 @@ import '../assert_dev_env.js';
7
7
  * implement. No `if (config.name === 'rust')` branches anywhere — name-
8
8
  * checking is a code smell that says capability vocabulary is missing.
9
9
  *
10
- * In-process Hono via `default_in_process_setup` declares every
11
- * capability `true` (see `in_process_capabilities`). Cross-process
12
- * backends opt in per-flag on their `BackendConfig`.
10
+ * In-process Hono via `default_in_process_setup` declares every capability
11
+ * its transport can exercise (see `in_process_capabilities` — the
12
+ * cross-process-only flags stay `false`). Cross-process backends opt in
13
+ * per-flag on their `BackendConfig`.
13
14
  *
14
15
  * **Where the per-backend declarations live** (this file owns only the
15
16
  * vocabulary + the in-process preset):
16
17
  *
17
- * - `in_process_capabilities` — here; every flag `true`.
18
+ * - `in_process_capabilities` — here; every flag `true` except the
19
+ * cross-process-only ones.
18
20
  * - `ts_default_capabilities` / `rust_default_capabilities` — consumer-facing
19
21
  * family defaults, in `default_backend_configs.ts` (full literals, so adding
20
22
  * a capability is a compile error until each family declares it).
21
23
  * - `ts_spine_capabilities` / `ts_spine_bun_capabilities` — fuz_app's own TS
22
24
  * spine presets, in `ts_spine_backend_config.ts` (deltas off the family
23
- * default; Bun flips `oversized_reject_closes_connection`).
25
+ * default; Bun flips `oversized_reject_closes_connection` and
26
+ * `ws_handshake_pipelining`).
24
27
  * - `rust_spine_stub_capabilities` — fuz_app's Rust spine-stub preset, in
25
28
  * `rust_spine_stub_backend_config.ts` (delta off the rust family default).
26
29
  *
27
30
  * **Gating flags vs shape notes.** `BackendCapabilities` holds only flags a
28
- * suite actually gates on (each has a `test_if(capabilities.X, ...)` reader).
31
+ * suite actually gates on (each has a `test_if(capabilities.X, ...)` reader,
32
+ * or — `ws_self_revocation_reply` — forks one case's assertion).
29
33
  * Wiring facts that gate nothing — `bearer_auth` / `trusted_proxy` /
30
34
  * `login_rate_limit` — live in the parallel `BackendShapeNotes` record
31
35
  * (`in_process_shape_notes` here, `ts_default_shape_notes` /
@@ -38,12 +42,15 @@ import { test } from 'vitest';
38
42
  /**
39
43
  * Capability declarations for the in-process Hono transport. Nearly every
40
44
  * flag is `true` because in-process testing exercises the full backend
41
- * with no missing optional behaviors. The one exception is `peer_request`:
42
- * `describe_peer_ping_ws_tests` is cross-process-only (it needs a real bound
43
- * socket with an `on_request` responder via `create_ws_transport`), so the
44
- * in-process driver never runs it — the transport itself supports
45
- * server-initiated requests. Cross-process consumers declare each flag
46
- * explicitly per backend.
45
+ * with no missing optional behaviors. The exceptions are the flags whose
46
+ * cases are cross-process-only: `peer_request` (`describe_peer_ping_ws_tests`
47
+ * needs a real bound socket with an `on_request` responder via
48
+ * `create_ws_transport`, so the in-process driver never runs it — the
49
+ * transport itself supports server-initiated requests),
50
+ * `ws_handshake_pipelining` (raw bytes on a real socket),
51
+ * `ws_account_actions` (a real socket's reply-then-close ordering), and
52
+ * `cell_gated_create`. Cross-process consumers declare each flag explicitly
53
+ * per backend.
47
54
  */
48
55
  export const in_process_capabilities = Object.freeze({
49
56
  ws: true,
@@ -56,6 +63,14 @@ export const in_process_capabilities = Object.freeze({
56
63
  account_status: true,
57
64
  oversized_reject_closes_connection: true,
58
65
  peer_request: false,
66
+ // Cross-process-only, like `peer_request`: the case needs a real bound socket
67
+ // to write raw bytes to.
68
+ ws_handshake_pipelining: false,
69
+ // Cross-process-only: the case reads a real socket's frames in wire order.
70
+ ws_account_actions: false,
71
+ // Read only by the case `ws_account_actions` gates; the in-process
72
+ // dispatcher does answer first.
73
+ ws_self_revocation_reply: true,
59
74
  // Cross-process-only: the test `CellCreateAuthorize` policy is mounted on the
60
75
  // spine binaries' full mount + the Rust stub, not the in-process default app.
61
76
  cell_gated_create: false
@@ -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,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAChF,OAAO,EAA4B,KAAK,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAQhG;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBA2BpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,mBAsBtC,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,EAAE,sBAAsB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACjF,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAChF,OAAO,EAA4B,KAAK,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAQhG;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAsCpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,EAAE,mBAgCtC,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"}
@@ -41,6 +41,17 @@ export const ts_default_capabilities = Object.freeze({
41
41
  // the HTTP RPC endpoint (the no-transport case). The TS spine config opts in;
42
42
  // a generic consumer enables it once it wires that mount.
43
43
  peer_request: false,
44
+ // Node (`ws`) and Deno hand frames pipelined with the upgrade request to the
45
+ // socket. A Bun-served consumer overrides to `false` (see the bun spine
46
+ // config) — `Bun.serve` answers such a request `400`.
47
+ ws_handshake_pipelining: true,
48
+ // Off by default: a consumer's WS endpoint mounts its own domain's actions.
49
+ // fuz_app's spine configs opt in (the spine binary mounts the account
50
+ // actions on `/api/ws`); a consumer does once its endpoint carries them.
51
+ ws_account_actions: false,
52
+ // `register_action_ws` sends a request's response before it flushes the
53
+ // post-commit queue a self-revocation's close is on.
54
+ ws_self_revocation_reply: true,
44
55
  // Off by default — the `cell_gated_create` test policy is a fuz_app test
45
56
  // fixture, mounted only on the spine binaries (`ts_spine_*`), not generic
46
57
  // consumers.
@@ -70,6 +81,16 @@ export const rust_default_capabilities = Object.freeze({
70
81
  // Server-initiated requests landed Rust-first canonical — the Rust spine
71
82
  // drives the `peer/ping` round-trip, so the family opts into the suite.
72
83
  peer_request: true,
84
+ // hyper hands bytes pipelined after the upgrade request to the upgraded
85
+ // stream, where they wait for the read loop that starts at admission.
86
+ ws_handshake_pipelining: true,
87
+ // Off by default, as on the TS family: a consumer opts in once its WS
88
+ // endpoint mounts the account actions. The `testing_spine_stub` preset does.
89
+ ws_account_actions: false,
90
+ // The Rust dispatch runs the post-commit queue — the self-revocation's
91
+ // close — before the socket loop writes the response, and the loop then
92
+ // drops it. A divergence from the TS spine; see the flag's doc.
93
+ ws_self_revocation_reply: false,
73
94
  // Off by default — the `testing_spine_stub` preset opts in (it mounts the
74
95
  // `TestCellGatedCreateAuthorize` policy); a generic Rust consumer doesn't.
75
96
  cell_gated_create: false
@@ -1 +1 @@
1
- {"version":3,"file":"role_grant_offer_notification_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_offer_notification_ws.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAqE9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAmC,MAAM,YAAY,CAAC;AAK7E,iFAAiF;AACjF,MAAM,WAAW,uCAAuC;IACvD,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,uEAAuE;IACvE,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;;;;;;;GAOG;AACH,eAAO,MAAM,+CAA+C,GAC3D,SAAS,uCAAuC,KAC9C,IA8VF,CAAC"}
1
+ {"version":3,"file":"role_grant_offer_notification_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/role_grant_offer_notification_ws.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAqE9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAmC,MAAM,YAAY,CAAC;AAK7E,iFAAiF;AACjF,MAAM,WAAW,uCAAuC;IACvD,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,uEAAuE;IACvE,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;;;;;;;GAOG;AACH,eAAO,MAAM,+CAA+C,GAC3D,SAAS,uCAAuC,KAC9C,IAuWF,CAAC"}
@@ -38,7 +38,7 @@ import '../assert_dev_env.js';
38
38
  * guard), and accept stays idempotent on the role_grant while still superseding
39
39
  * pending siblings. Gated on `capabilities.ws`.
40
40
  *
41
- * Cross-process only: `create_ws_transport` needs a real bound socket, so wire
41
+ * Cross-process only: `create_admitted_ws_transport` needs a real bound socket, so wire
42
42
  * it from a `*.cross.test.ts` file, never an in-process setup. Authed cookies
43
43
  * come from the per-account session minted by `fixture.create_account` /
44
44
  * `fixture.create_session_headers`.
@@ -47,7 +47,7 @@ import '../assert_dev_env.js';
47
47
  */
48
48
  import { assert, describe } from 'vitest';
49
49
  import { rpc_call } from '../rpc_helpers.js';
50
- import { create_ws_transport } from '../transports/ws_transport.js';
50
+ import { create_admitted_ws_transport } from '../transports/ws_transport.js';
51
51
  import { is_notification_with } from '../transports/ws_client.js';
52
52
  import { ROLE_ADMIN } from '../../auth/role_schema.js';
53
53
  import { ROLE_GRANT_OFFER_RECEIVED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_ACCEPTED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_DECLINED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_RETRACTED_NOTIFICATION_METHOD, ROLE_GRANT_OFFER_SUPERSEDE_NOTIFICATION_METHOD, ROLE_GRANT_REVOKE_NOTIFICATION_METHOD, RoleGrantOfferReceivedParams, RoleGrantOfferAcceptedParams, RoleGrantOfferDeclinedParams, RoleGrantOfferRetractedParams, RoleGrantOfferSupersedeParams, RoleGrantRevokeParams } from '../../auth/role_grant_offer_notifications.js';
@@ -65,10 +65,19 @@ const RPC_PATH = '/api/rpc';
65
65
  export const describe_role_grant_offer_notification_ws_tests = (options) => {
66
66
  const { setup_test, capabilities, base_url, ws_path } = options;
67
67
  // -- shared helpers -------------------------------------------------------
68
- /** Open a WS transport for a single session cookie (`<name>=<value>`). */
68
+ /**
69
+ * Open a WS transport for a single session cookie (`<name>=<value>`),
70
+ * admitted before it resolves: a targeted notification is delivered only to
71
+ * an admitted connection, and the handshake alone doesn't say it is one.
72
+ */
69
73
  const open_ws = (cookie) => {
70
74
  assert.ok(cookie, 'expected a session cookie for the WS upgrade');
71
- return create_ws_transport({ base_url, ws_path, cookies: [cookie], origin: base_url });
75
+ return create_admitted_ws_transport({
76
+ base_url,
77
+ ws_path,
78
+ cookies: [cookie],
79
+ origin: base_url
80
+ });
72
81
  };
73
82
  /** Drive a JSON-RPC call over the fixture's HTTP transport. */
74
83
  const rpc = (fixture, method, params, headers) => rpc_call({ app: fixture.transport, path: RPC_PATH, method, params, headers });
@@ -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;AASzD,uGAAuG;AACvG,eAAO,MAAM,uBAAuB,oCAAoC,CAAC;AAEzE;;;;;;GAMG;AACH,eAAO,MAAM,gCAAgC,6CAA6C,CAAC;AAE3F;;;;;GAKG;AACH,eAAO,MAAM,wCAAwC,6CAA6C,CAAC;AAkBnG,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;CAClC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,GAC1C,UAAS,6BAAkC,KACzC,aAkDF,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;AASzD,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;CAClC;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,8BAA8B,GAC1C,UAAS,6BAAkC,KACzC,aAkDF,CAAC"}
@@ -61,7 +61,9 @@ export const RUST_SPINE_STUB_EXPECTED_SCHEMA_PATH_ENV = 'FUZ_RUST_SPINE_STUB_EXP
61
61
  * Capabilities for the Rust `testing_spine_stub` — `rust_default_capabilities`
62
62
  * plus `sse` (the stub serves `GET /api/admin/audit/stream` over the spine
63
63
  * `fuz_realtime::SseRegistry` + audit listener), `ready` (it live-mounts
64
- * `/ready` over the env-supplied fixture path), and `cell_gated_create` (it
64
+ * `/ready` over the env-supplied fixture path), `ws_account_actions` (it
65
+ * serves one action registry on `/api/rpc` and `/api/ws`), and
66
+ * `cell_gated_create` (it
65
67
  * mounts the `TestCellGatedCreateAuthorize` policy on its cell layer). Named
66
68
  * (not inline) so every spine preset is greppable, mirroring
67
69
  * `ts_spine_capabilities`.
@@ -70,6 +72,7 @@ const rust_spine_stub_capabilities = Object.freeze({
70
72
  ...rust_default_capabilities,
71
73
  sse: true,
72
74
  ready: true,
75
+ ws_account_actions: true,
73
76
  cell_gated_create: true
74
77
  });
75
78
  /** Default listening port — slots beside zzz's 1175/1176; matches the binary's `DEFAULT_PORT`. */
@@ -14,6 +14,19 @@ export interface SessionCapCrossTestOptions {
14
14
  readonly login_path?: string;
15
15
  /** REST account-status route path. Default `/api/account/status`. */
16
16
  readonly status_path?: string;
17
+ /**
18
+ * Where the backend's WebSocket endpoint is, for the case that an evicted
19
+ * session's socket is closed. Pass it when the backend has a WS transport
20
+ * (`capabilities.ws`); omitted, that case is skipped.
21
+ */
22
+ readonly ws?: {
23
+ /** Base URL the backend is reachable at (e.g. `http://localhost:1178`). */
24
+ readonly base_url: string;
25
+ /** WebSocket endpoint path on the backend (e.g. `/api/ws`). */
26
+ readonly ws_path: string;
27
+ /** Origin for the upgrade. Defaults to `base_url`. */
28
+ readonly origin?: string;
29
+ };
17
30
  }
18
31
  export declare const describe_session_cap_cross_tests: (options: SessionCapCrossTestOptions) => void;
19
32
  //# sourceMappingURL=session_cap.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"session_cap.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/session_cap.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAwD9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,gDAAgD;AAChD,MAAM,WAAW,0BAA0B;IAC1C,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,eAAO,MAAM,gCAAgC,GAAI,SAAS,0BAA0B,KAAG,IA6DtF,CAAC"}
1
+ {"version":3,"file":"session_cap.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/session_cap.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6D9B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,gDAAgD;AAChD,MAAM,WAAW,0BAA0B;IAC1C,2EAA2E;IAC3E,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,2DAA2D;IAC3D,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B;;;;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,gCAAgC,GAAI,SAAS,0BAA0B,KAAG,IA4FtF,CAAC"}
@@ -3,15 +3,11 @@ import '../assert_dev_env.js';
3
3
  * Cross-backend parity suite for the **per-account concurrent-session cap**
4
4
  * over real HTTP.
5
5
  *
6
- * The cap is a shipped control on the TS spine (`query_session_enforce_limit`,
7
- * `DEFAULT_MAX_SESSIONS`) that the Rust spine went without — its session INSERT
8
- * carried no eviction, so every Rust-backed deployment served unlimited
9
- * concurrent sessions per account. That divergence survived because nothing crossed the
10
- * wire on it — the TS half is pinned by in-process tests
11
- * (`session_middleware.lifecycle.db.test.ts`, `session_token_limits.integration.db.test.ts`)
12
- * whose Rust counterparts simply didn't exist to fail. This suite is the pin
13
- * that runs on *both*, so the next spine to lose the cap fails here rather than
14
- * in production. Two properties:
6
+ * Both spines cap concurrent sessions per account (the TS
7
+ * `query_session_enforce_limit` / `DEFAULT_MAX_SESSIONS`, the Rust
8
+ * `create_capped_session`). Each spine's own tests pin its half in-process;
9
+ * nothing in them crosses the wire, so a spine that lost the cap would fail
10
+ * no shared test. This suite is the pin that runs on *both*. It pins:
15
11
  *
16
12
  * - **the cap bounds concurrent sessions, by eviction not refusal** — logging in
17
13
  * `max_sessions + 1` times succeeds every time (a login is never denied for
@@ -19,6 +15,11 @@ import '../assert_dev_env.js';
19
15
  * - **the evicted session is the oldest** — the first login's cookie no longer
20
16
  * authenticates once the cap is exceeded. A spine with no cap authenticates it
21
17
  * fine, which is exactly the defect this catches.
18
+ * - **an evicted session's socket is closed** (when `ws` is supplied) — the cap
19
+ * deletes the session row, and a WebSocket never re-reads its session, so
20
+ * the login that evicts it must close the socket it opened
21
+ * (`WS_CLOSE_SESSION_REVOKED`), once the eviction has committed. A spine
22
+ * that only deletes the row leaves the socket running on a dead session.
22
23
  *
23
24
  * **Why `max_sessions + 1` logins and not `max_sessions`.** The starting session
24
25
  * count isn't zero and isn't guaranteed equal across impls — `create_account`
@@ -48,16 +49,32 @@ import '../assert_dev_env.js';
48
49
  * @module
49
50
  */
50
51
  import { describe, test, assert } from 'vitest';
52
+ import { heartbeat_action_spec } from '../../actions/heartbeat.js';
53
+ import { WS_CLOSE_SESSION_REVOKED } from '../../actions/transports.js';
51
54
  import { DEFAULT_MAX_SESSIONS } from '../../auth/account_route_schema.js';
52
55
  import { DEFAULT_TEST_PASSWORD } from '../test_credentials.js';
56
+ import { create_ws_transport } from '../transports/ws_transport.js';
57
+ import { test_if } from './capabilities.js';
53
58
  export const describe_session_cap_cross_tests = (options) => {
54
- const { setup_test } = options;
59
+ const { setup_test, ws } = options;
55
60
  const max_sessions = options.max_sessions ?? DEFAULT_MAX_SESSIONS;
56
61
  const login_path = options.login_path ?? '/api/account/login';
57
62
  const status_path = options.status_path ?? '/api/account/status';
58
63
  // Fresh-keeper-per-test wipes the DB between tests, so a literal username
59
64
  // never collides (see `setup.ts`).
60
65
  const username = 'session_cap_user';
66
+ /** Log `username` in on a fresh transport — one login, one session, one jar. */
67
+ const login = async (fixture, label) => {
68
+ const transport = fixture.fresh_transport();
69
+ const res = await transport(login_path, {
70
+ method: 'POST',
71
+ headers: { 'content-type': 'application/json' },
72
+ body: JSON.stringify({ username, password: DEFAULT_TEST_PASSWORD })
73
+ });
74
+ assert.strictEqual(res.status, 200, `${label} must succeed — the cap evicts, it never refuses`);
75
+ assert.ok(transport.cookies().length > 0, `${label} must set a session cookie on its jar`);
76
+ return transport;
77
+ };
61
78
  describe('per-account session cap parity', () => {
62
79
  test(`logging in ${max_sessions + 1} times evicts the oldest session (newest cookie resolves, first does not)`, async () => {
63
80
  const fixture = await setup_test();
@@ -69,15 +86,7 @@ export const describe_session_cap_cross_tests = (options) => {
69
86
  // look alike. Held oldest-first.
70
87
  const sessions = [];
71
88
  for (let i = 0; i < max_sessions + 1; i++) {
72
- const transport = fixture.fresh_transport();
73
- const res = await transport(login_path, {
74
- method: 'POST',
75
- headers: { 'content-type': 'application/json' },
76
- body: JSON.stringify({ username, password: DEFAULT_TEST_PASSWORD })
77
- });
78
- assert.strictEqual(res.status, 200, `login ${i + 1}/${max_sessions + 1} must succeed — the cap evicts, it never refuses`);
79
- assert.ok(transport.cookies().length > 0, `login ${i + 1} must set a session cookie on its jar`);
80
- sessions.push(transport);
89
+ sessions.push(await login(fixture, `login ${i + 1}/${max_sessions + 1}`));
81
90
  }
82
91
  /** `GET /status` on one login's transport: 200 authenticated, 401 not. */
83
92
  const status_on = async (transport) => {
@@ -89,5 +98,32 @@ export const describe_session_cap_cross_tests = (options) => {
89
98
  assert.strictEqual(await status_on(sessions[0]), 401, `the oldest session must be evicted once the account exceeds ${max_sessions} sessions ` +
90
99
  '(a spine with no cap authenticates this cookie)');
91
100
  });
101
+ // `max_sessions` further logins leave the watched session outside the
102
+ // newest `max_sessions`, so it is evicted whatever sessions the account
103
+ // held before it (`create_account` seeds one on some cradles).
104
+ test_if(ws !== undefined, 'a session the cap evicts has its open socket closed', async () => {
105
+ const fixture = await setup_test();
106
+ await fixture.create_account({ username, password_value: DEFAULT_TEST_PASSWORD });
107
+ const victim = await login(fixture, 'the login whose socket is watched');
108
+ const client = await create_ws_transport({
109
+ base_url: ws.base_url,
110
+ ws_path: ws.ws_path,
111
+ cookies: victim.cookies(),
112
+ origin: ws.origin
113
+ });
114
+ try {
115
+ // admitted and dispatching before the evicting logins
116
+ await client.request(1, heartbeat_action_spec.method, {});
117
+ for (let i = 0; i < max_sessions; i++) {
118
+ await login(fixture, `evicting login ${i + 1}/${max_sessions}`);
119
+ }
120
+ const closed = await client.wait_for_close(2000);
121
+ assert.ok(closed, `the socket of a session evicted past ${max_sessions} sessions did not close within 2s`);
122
+ assert.strictEqual(client.close_code, WS_CLOSE_SESSION_REVOKED);
123
+ }
124
+ finally {
125
+ await client.close();
126
+ }
127
+ });
92
128
  });
93
129
  };