@fuzdev/fuz_app 0.117.1 → 0.119.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 (179) 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 +125 -31
  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 +72 -44
  44. package/dist/auth/audit_emitter.d.ts.map +1 -1
  45. package/dist/auth/audit_emitter.js +60 -16
  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/bootstrap_account.d.ts +3 -2
  54. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  55. package/dist/auth/bootstrap_account.js +3 -3
  56. package/dist/auth/cleanup.d.ts +219 -30
  57. package/dist/auth/cleanup.d.ts.map +1 -1
  58. package/dist/auth/cleanup.js +260 -45
  59. package/dist/auth/deps.d.ts +29 -1
  60. package/dist/auth/deps.d.ts.map +1 -1
  61. package/dist/auth/migrations.d.ts.map +1 -1
  62. package/dist/auth/migrations.js +27 -0
  63. package/dist/auth/resolved_auth.d.ts +105 -0
  64. package/dist/auth/resolved_auth.d.ts.map +1 -0
  65. package/dist/auth/resolved_auth.js +124 -0
  66. package/dist/auth/role_grant_offer_ddl.d.ts +4 -2
  67. package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
  68. package/dist/auth/role_grant_offer_ddl.js +10 -3
  69. package/dist/auth/role_grant_offer_queries.d.ts +16 -7
  70. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  71. package/dist/auth/role_grant_offer_queries.js +33 -15
  72. package/dist/auth/role_grant_offer_schema.d.ts +9 -0
  73. package/dist/auth/role_grant_offer_schema.d.ts.map +1 -1
  74. package/dist/auth/session_middleware.d.ts +8 -1
  75. package/dist/auth/session_middleware.d.ts.map +1 -1
  76. package/dist/auth/session_middleware.js +9 -3
  77. package/dist/auth/session_queries.d.ts +31 -6
  78. package/dist/auth/session_queries.d.ts.map +1 -1
  79. package/dist/auth/session_queries.js +31 -6
  80. package/dist/auth/standard_rpc_actions.d.ts +9 -9
  81. package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
  82. package/dist/auth/standard_rpc_actions.js +3 -3
  83. package/dist/db/CLAUDE.md +7 -1
  84. package/dist/db/migrate.d.ts +8 -13
  85. package/dist/db/migrate.d.ts.map +1 -1
  86. package/dist/db/migrate.js +9 -38
  87. package/dist/db/status.d.ts +58 -7
  88. package/dist/db/status.d.ts.map +1 -1
  89. package/dist/db/status.js +205 -97
  90. package/dist/http/CLAUDE.md +7 -3
  91. package/dist/http/db_routes.d.ts +6 -2
  92. package/dist/http/db_routes.d.ts.map +1 -1
  93. package/dist/http/db_routes.js +2 -2
  94. package/dist/periodic.d.ts +80 -0
  95. package/dist/periodic.d.ts.map +1 -0
  96. package/dist/periodic.js +122 -0
  97. package/dist/realtime/sse_auth_guard.d.ts +79 -49
  98. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  99. package/dist/realtime/sse_auth_guard.js +103 -73
  100. package/dist/realtime/subscriber_registry.d.ts +119 -8
  101. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  102. package/dist/realtime/subscriber_registry.js +184 -25
  103. package/dist/runtime/deno.d.ts.map +1 -1
  104. package/dist/runtime/deno.js +9 -6
  105. package/dist/runtime/deps.d.ts +3 -2
  106. package/dist/runtime/deps.d.ts.map +1 -1
  107. package/dist/runtime/secure_file.d.ts +23 -9
  108. package/dist/runtime/secure_file.d.ts.map +1 -1
  109. package/dist/runtime/secure_file.js +29 -10
  110. package/dist/server/app_backend.d.ts +13 -2
  111. package/dist/server/app_backend.d.ts.map +1 -1
  112. package/dist/server/app_backend.js +5 -2
  113. package/dist/server/app_server.d.ts +55 -15
  114. package/dist/server/app_server.d.ts.map +1 -1
  115. package/dist/server/app_server.js +41 -13
  116. package/dist/testing/CLAUDE.md +140 -48
  117. package/dist/testing/app_server.d.ts +2 -2
  118. package/dist/testing/app_server.d.ts.map +1 -1
  119. package/dist/testing/app_server.js +11 -2
  120. package/dist/testing/audit_drift_guard.d.ts +3 -52
  121. package/dist/testing/audit_drift_guard.d.ts.map +1 -1
  122. package/dist/testing/audit_drift_guard.js +2 -46
  123. package/dist/testing/connection_closer_helpers.d.ts +9 -22
  124. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  125. package/dist/testing/connection_closer_helpers.js +11 -17
  126. package/dist/testing/cross_backend/capabilities.d.ts +67 -13
  127. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  128. package/dist/testing/cross_backend/capabilities.js +27 -12
  129. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  130. package/dist/testing/cross_backend/default_backend_configs.js +21 -0
  131. package/dist/testing/cross_backend/expected_schema.json +1 -0
  132. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  133. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +13 -4
  134. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  135. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +4 -1
  136. package/dist/testing/cross_backend/session_cap.d.ts +13 -0
  137. package/dist/testing/cross_backend/session_cap.d.ts.map +1 -1
  138. package/dist/testing/cross_backend/session_cap.js +55 -19
  139. package/dist/testing/cross_backend/sse_round_trip.d.ts +10 -2
  140. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -1
  141. package/dist/testing/cross_backend/sse_round_trip.js +93 -26
  142. package/dist/testing/cross_backend/testing_server_bun.js +4 -4
  143. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  144. package/dist/testing/cross_backend/ts_spine_backend_config.js +10 -2
  145. package/dist/testing/cross_backend/ws_connection_cap.d.ts +35 -0
  146. package/dist/testing/cross_backend/ws_connection_cap.d.ts.map +1 -0
  147. package/dist/testing/cross_backend/ws_connection_cap.js +131 -0
  148. package/dist/testing/cross_backend/ws_round_trip.d.ts +2 -1
  149. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -1
  150. package/dist/testing/cross_backend/ws_round_trip.js +156 -19
  151. package/dist/testing/sse_round_trip.d.ts +6 -4
  152. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  153. package/dist/testing/stubs.d.ts +1 -1
  154. package/dist/testing/stubs.d.ts.map +1 -1
  155. package/dist/testing/stubs.js +6 -4
  156. package/dist/testing/transports/sse_transport.d.ts +1 -1
  157. package/dist/testing/transports/ws_client.d.ts +7 -0
  158. package/dist/testing/transports/ws_client.d.ts.map +1 -1
  159. package/dist/testing/transports/ws_raw_client.d.ts +51 -0
  160. package/dist/testing/transports/ws_raw_client.d.ts.map +1 -0
  161. package/dist/testing/transports/ws_raw_client.js +156 -0
  162. package/dist/testing/transports/ws_transport.d.ts +28 -0
  163. package/dist/testing/transports/ws_transport.d.ts.map +1 -1
  164. package/dist/testing/transports/ws_transport.js +45 -1
  165. package/dist/testing/ws_round_trip.d.ts +8 -3
  166. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  167. package/dist/testing/ws_round_trip.js +19 -4
  168. package/dist/ui/AdminAccounts.svelte +4 -3
  169. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  170. package/dist/ui/AdminOverview.svelte +3 -1
  171. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  172. package/dist/ui/CLAUDE.md +22 -6
  173. package/dist/ui/admin_accounts_state.svelte.d.ts +15 -1
  174. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  175. package/dist/ui/admin_accounts_state.svelte.js +16 -0
  176. package/dist/ui/auth_state.svelte.d.ts +25 -1
  177. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  178. package/dist/ui/auth_state.svelte.js +28 -1
  179. package/package.json +2 -2
@@ -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
  };
@@ -27,11 +27,19 @@ export interface CrossProcessSseTestOptions {
27
27
  readonly rpc_path?: string;
28
28
  /** Origin for the stream request. Defaults to `base_url`. */
29
29
  readonly origin?: string;
30
+ /**
31
+ * The backend's per-session audit-stream cap, which the cap case opens one
32
+ * stream past. Defaults to `AUDIT_LOG_SSE_MAX_PER_SCOPE`, the cap both
33
+ * spines apply unless overridden; `null` (a backend with the cap disabled)
34
+ * skips the case.
35
+ */
36
+ readonly max_per_scope?: number | null;
30
37
  }
31
38
  /**
32
- * Register the cross-process SSE round-trip suite. Up to four cases over a
39
+ * Register the cross-process SSE round-trip suite. Its cases run over a
33
40
  * real streaming `fetch`: connected-comment, audit data frame, account-wide
34
- * close-on-revoke, and session-scoped close-on-revoke.
41
+ * close-on-revoke, session-scoped close-on-revoke, close on account delete,
42
+ * and the per-session cap.
35
43
  */
36
44
  export declare const describe_cross_process_sse_tests: (options: CrossProcessSseTestOptions) => void;
37
45
  //# sourceMappingURL=sse_round_trip.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sse_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/sse_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA+D9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,kEAAkE;AAClE,MAAM,WAAW,0BAA0B;IAC1C;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CACzB;AA0BD;;;;GAIG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,0BAA0B,KAAG,IAwKtF,CAAC"}
1
+ {"version":3,"file":"sse_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/sse_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA8E9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,kEAAkE;AAClE,MAAM,WAAW,0BAA0B;IAC1C;;;;;;OAMG;IACH,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC;IAC/B,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;AA0BD;;;;;GAKG;AACH,eAAO,MAAM,gCAAgC,GAAI,SAAS,0BAA0B,KAAG,IA2PtF,CAAC"}
@@ -11,7 +11,7 @@ import '../assert_dev_env.js';
11
11
  * (`describe_standard_cross_process_tests`) omits SSE by design, so consumers
12
12
  * call this alongside it (paralleling `describe_cross_process_ws_tests`).
13
13
  *
14
- * Four cases, mirroring the in-process SSE self-test against fuz_app's
14
+ * Its cases follow the in-process SSE self-test against fuz_app's
15
15
  * standard audit-log stream:
16
16
  *
17
17
  * 1. **connects** — the stream opens and emits the `: connected` comment.
@@ -23,20 +23,28 @@ import '../assert_dev_env.js';
23
23
  * stream opens so `create_account`'s own audit events (invite / signup /
24
24
  * login / token) don't land on it.
25
25
  * 3. **close-on-revoke, account-wide** (gated on `rpc_path`) — the subscriber's
26
- * *own* sessions are revoked (`account_session_revoke_all`), so the
27
- * `session_revoke_all` event targets the keeper and the audit guard drops
28
- * the live stream via the account-wide `close_for_account` path. Asserted
29
- * via `SseTransport.wait_for_close`.
26
+ * *own* sessions are revoked (`account_session_revoke_all`), which closes
27
+ * every stream of the account once the revoke commits (the handler's
28
+ * close, repeated by the audit guard on the `session_revoke_all` row).
29
+ * Asserted via `SseTransport.wait_for_close`.
30
30
  * 4. **close-on-revoke, session-scoped** (gated on `rpc_path`) — the
31
31
  * subscriber's *own* single session is revoked (`account_session_revoke`),
32
- * so the `session_revoke` event drops the stream via the session-hash-scoped
33
- * `close_for_session` path (the distinct primitive cases 2–3 don't reach).
32
+ * which closes only the stream registered under that session's hash (the
33
+ * distinct close cases 2–3 don't reach).
34
+ * 5. **close on account delete** (gated on `rpc_path` and
35
+ * `capabilities.account_lifecycle`) — the keeper soft-deletes a second
36
+ * admin whose stream is open; the delete revokes its credentials and
37
+ * closes its stream.
38
+ * 6. **per-session cap** — one stream past `max_per_scope` on one session
39
+ * closes the oldest (evict-oldest, no final frame) and leaves every other
40
+ * stream open. Needs no RPC: the cap runs inside the subscribe itself.
34
41
  *
35
- * The close-on-revoke matrix is layered: cases 3–4 exercise the account-wide
36
- * and session-scoped paths cross-process; the remaining union events
37
- * (`token_revoke_all` / `logout` / `password_change`, all account-wide; and
38
- * `role_grant_revoke`, role-matched) are covered by the spine's `fuz_realtime`
39
- * SSE-registry unit tests and the in-process guard self-test, so a cross-process
42
+ * The close-on-revoke matrix is layered: cases 3–5 exercise the account-wide
43
+ * and session-scoped paths cross-process; the remaining revocation events
44
+ * (`token_revoke_all` / `logout` / `password_change` / `account_purge`, all
45
+ * account-wide; `token_revoke`, token-scoped; and `role_grant_revoke`,
46
+ * role-matched) are covered by the spine's `fuz_realtime` SSE-registry unit
47
+ * tests and the in-process guard self-test, so a cross-process
40
48
  * `token_revoke_all`-with-zero-tokens case (which may emit no audit row) stays
41
49
  * out to keep the spawned-backend suite non-flaky.
42
50
  *
@@ -49,8 +57,9 @@ import '../assert_dev_env.js';
49
57
  */
50
58
  import { assert, describe } from 'vitest';
51
59
  import { account_session_list_action_spec, account_session_revoke_action_spec, account_session_revoke_all_action_spec } from '../../auth/account_action_specs.js';
52
- import { admin_session_revoke_all_action_spec } from '../../auth/admin_action_specs.js';
53
- import { audit_log_event_specs } from '../../realtime/sse_auth_guard.js';
60
+ import { account_delete_action_spec, admin_session_revoke_all_action_spec } from '../../auth/admin_action_specs.js';
61
+ import { ROLE_ADMIN } from '../../auth/role_schema.js';
62
+ import { AUDIT_LOG_SSE_MAX_PER_SCOPE, audit_log_event_specs } from '../../realtime/sse_auth_guard.js';
54
63
  import { SSE_CONNECTED_COMMENT } from '../../realtime/sse_constants.js';
55
64
  import { create_sse_transport } from '../transports/sse_transport.js';
56
65
  import { create_rpc_post_init } from '../rpc_helpers.js';
@@ -73,13 +82,15 @@ const assert_audit_data_frame = (frame) => {
73
82
  assert.ok(result.success, `audit data frame params mismatch for '${String(payload.method)}': ${result.success ? '' : JSON.stringify(result.error.issues)}`);
74
83
  };
75
84
  /**
76
- * Register the cross-process SSE round-trip suite. Up to four cases over a
85
+ * Register the cross-process SSE round-trip suite. Its cases run over a
77
86
  * real streaming `fetch`: connected-comment, audit data frame, account-wide
78
- * close-on-revoke, and session-scoped close-on-revoke.
87
+ * close-on-revoke, session-scoped close-on-revoke, close on account delete,
88
+ * and the per-session cap.
79
89
  */
80
90
  export const describe_cross_process_sse_tests = (options) => {
81
91
  const { setup_test, capabilities, base_url, rpc_path, origin } = options;
82
92
  const sse_path = options.sse_path ?? DEFAULT_SSE_PATH;
93
+ const max_per_scope = options.max_per_scope === undefined ? AUDIT_LOG_SSE_MAX_PER_SCOPE : options.max_per_scope;
83
94
  describe('cross-process sse', () => {
84
95
  test_if(capabilities.sse, 'connects and emits the connected comment', async () => {
85
96
  const fixture = await setup_test();
@@ -127,8 +138,9 @@ export const describe_cross_process_sse_tests = (options) => {
127
138
  await sse.close();
128
139
  }
129
140
  });
130
- // Revoke the subscriber's OWN sessions → `session_revoke_all` targets the
131
- // keeper, so the audit guard closes the live stream.
141
+ // Revoke the subscriber's OWN sessions → the handler closes the keeper's
142
+ // streams once the revoke commits (the audit guard repeats the close on
143
+ // the `session_revoke_all` row).
132
144
  test_if(capabilities.sse && rpc_path !== undefined, 'stream closes when the subscriber sessions are revoked', async () => {
133
145
  const fixture = await setup_test();
134
146
  const sse = await create_sse_transport({
@@ -150,14 +162,15 @@ export const describe_cross_process_sse_tests = (options) => {
150
162
  }
151
163
  });
152
164
  // Single `session_revoke` of the subscriber's OWN session → the
153
- // session-hash-scoped close path (`close_for_session` / the TS guard's
154
- // `close_by_identity(session_id)`). The keeper holds exactly one session,
155
- // so revoking it by its blake3 hash drops the stream opened under it.
156
- // This is the close-on-revoke path the account-wide cases above don't
157
- // exercise; the remaining union events (`token_revoke_all` / `logout` /
158
- // `password_change`) share the account-wide `close_for_account` path the
159
- // `session_revoke_all` case already covers, and `role_grant_revoke`'s
160
- // role-matched path is covered by `fuz_realtime`'s SSE registry unit tests.
165
+ // session-hash-scoped close (the Rust registry's `close_for_session`, the
166
+ // TS registry's `close_by_identity(session_hash)`). The keeper holds
167
+ // exactly one session, so revoking it by its blake3 hash drops the stream
168
+ // opened under it. This is the close-on-revoke path the account-wide case
169
+ // above doesn't exercise; the remaining account-wide events
170
+ // (`token_revoke_all` / `logout` / `password_change` / `account_purge`)
171
+ // share the close the `session_revoke_all` and account-delete cases cover,
172
+ // and `token_revoke`'s token-scoped and `role_grant_revoke`'s role-matched
173
+ // closes are covered by `fuz_realtime`'s SSE registry unit tests.
161
174
  test_if(capabilities.sse && rpc_path !== undefined, 'stream closes on a single session_revoke of the subscriber session', async () => {
162
175
  const fixture = await setup_test();
163
176
  const sse = await create_sse_transport({
@@ -183,5 +196,59 @@ export const describe_cross_process_sse_tests = (options) => {
183
196
  await sse.close();
184
197
  }
185
198
  });
199
+ // A soft-deleted account's credentials stop authenticating, and a stream
200
+ // never rechecks its own — so the delete must close it. The keeper
201
+ // deletes a second admin whose stream is open; the keeper's own admin
202
+ // grant keeps the delete clear of the last-admin guard.
203
+ test_if(capabilities.sse && capabilities.account_lifecycle && rpc_path !== undefined, 'stream closes when the subscriber account is deleted', async () => {
204
+ const fixture = await setup_test();
205
+ const target = await fixture.create_account({
206
+ username: 'sse_delete_target',
207
+ roles: [ROLE_ADMIN]
208
+ });
209
+ const cookie = target.create_session_headers().cookie;
210
+ assert.ok(cookie, 'expected a session cookie for the target account');
211
+ const sse = await create_sse_transport({ base_url, sse_path, cookies: [cookie], origin });
212
+ try {
213
+ const first = await sse.read_frame();
214
+ assert.strictEqual(first + '\n\n', SSE_CONNECTED_COMMENT, 'first frame must be the connected comment');
215
+ const res = await fixture.transport(rpc_path, create_rpc_post_init(account_delete_action_spec.method, {
216
+ account_id: target.account.id
217
+ }));
218
+ assert.strictEqual(res.status, 200, `account_delete RPC failed (status=${res.status})`);
219
+ const closed = await sse.wait_for_close(2000);
220
+ assert.ok(closed, 'stream did not close within 2s after account_delete');
221
+ }
222
+ finally {
223
+ await sse.close();
224
+ }
225
+ });
226
+ // One stream past the cap on one session → the oldest closes, the newest
227
+ // stays open. Opened in sequence, each awaited to its connect comment, so
228
+ // the server registered them in order and "oldest" is stream 0 on both
229
+ // spines. The evicted stream ends with no final frame, so a browser
230
+ // `EventSource` would reconnect — this asserts the server side only.
231
+ test_if(capabilities.sse && max_per_scope !== null, 'one stream past the per-session cap closes the oldest', async () => {
232
+ const fixture = await setup_test();
233
+ const cookies = fixture.transport.cookies();
234
+ const streams = [];
235
+ try {
236
+ for (let i = 0; i <= max_per_scope; i++) {
237
+ const sse = await create_sse_transport({ base_url, sse_path, cookies, origin });
238
+ streams.push(sse);
239
+ const first = await sse.read_frame();
240
+ assert.strictEqual(first + '\n\n', SSE_CONNECTED_COMMENT, `stream ${i}: first frame must be the connected comment`);
241
+ }
242
+ const [oldest, ...survivors] = streams;
243
+ const closed = await oldest.wait_for_close(2000);
244
+ assert.ok(closed, 'the oldest stream did not close within 2s of the cap overflow');
245
+ // only the oldest — every later stream on the session stays open
246
+ const survivors_closed = await Promise.all(survivors.map((sse) => sse.wait_for_close(200)));
247
+ assert.deepStrictEqual(survivors_closed, survivors.map(() => false), 'only the oldest stream may close; the rest of the session stays open');
248
+ }
249
+ finally {
250
+ await Promise.all(streams.map((sse) => sse.close()));
251
+ }
252
+ });
186
253
  });
187
254
  };
@@ -53,10 +53,10 @@ export const create_bun_testing_adapter = () => ({
53
53
  // hang), and the client runtime — a single server-closed WS is
54
54
  // necessary and sufficient. Client-initiated close or leaving the
55
55
  // socket open both stop cleanly in ~0ms. In this suite the trigger is
56
- // `create_ws_auth_guard` closing the socket on `session_revoke_all`
57
- // (the `ws.cross.test.ts` close-on-revoke case) — the only
58
- // server-initiated WS close, which is why teardown hangs there and
59
- // not under HTTP-only or client-closed WS traffic.
56
+ // any server-initiated WS close — a revocation's (the `ws.cross.test.ts`
57
+ // close-on-revoke cases), a cap eviction's, an oversized message's —
58
+ // which is why teardown hangs after those and not under HTTP-only or
59
+ // client-closed WS traffic.
60
60
  //
61
61
  // So initiate a force-close (`true` drops active connections, no
62
62
  // drain) but DON'T await it: awaiting hangs `start_testing_server`'s
@@ -1 +1 @@
1
- {"version":3,"file":"ts_spine_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ts_spine_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AASzD,8GAA8G;AAC9G,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AAkC3D,6FAA6F;AAC7F,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,gDAAgD;AAChD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,uDAAuD,CAAC;AAEvF,MAAM,WAAW,2BAA2B;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAC3C;AAWD;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAmBF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAmBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAgCF,CAAC"}
1
+ {"version":3,"file":"ts_spine_backend_config.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ts_spine_backend_config.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AASzD,8GAA8G;AAC9G,eAAO,MAAM,gBAAgB,6BAA6B,CAAC;AA0C3D,6FAA6F;AAC7F,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,iDAAiD;AACjD,eAAO,MAAM,0BAA0B,OAAO,CAAC;AAE/C,gDAAgD;AAChD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,yDAAyD;AACzD,eAAO,MAAM,mBAAmB,wDAAwD,CAAC;AAEzF,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,uDAAuD,CAAC;AAEvF,MAAM,WAAW,2BAA2B;IAC3C,mFAAmF;IACnF,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,8DAA8D;IAC9D,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;;;;;;OASG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;CAC3C;AAWD;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAmBF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,GACvC,UAAS,2BAAgC,KACvC,aAmBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,4BAA4B,GACxC,UAAS,2BAAgC,KACvC,aAgCF,CAAC"}
@@ -9,6 +9,7 @@ export const TS_SPINE_DIR_ENV = 'FUZ_TESTING_TS_SPINE_DIR';
9
9
  * (the binary wires `audit_log_sse`), `ready` (the binary live-mounts the
10
10
  * `/ready` deploy gate in `build_spine_app`), `peer_request` (the backend
11
11
  * WS transport's `request_connection` path drives server→client `peer/ping`),
12
+ * `ws_account_actions` (the binary mounts the account actions on `/api/ws`),
12
13
  * and `cell_gated_create` (the binary's `full_spine_mount` mounts the
13
14
  * `test_cell_gated_create_authorize` policy).
14
15
  */
@@ -17,11 +18,13 @@ const ts_spine_capabilities = Object.freeze({
17
18
  sse: true,
18
19
  ready: true,
19
20
  peer_request: true,
21
+ ws_account_actions: true,
20
22
  cell_gated_create: true
21
23
  });
22
24
  /**
23
25
  * Capabilities for the **Bun** spine binary — `ts_spine_capabilities` with
24
- * `oversized_reject_closes_connection: false`. `Bun.serve` drains the declared
26
+ * `oversized_reject_closes_connection: false` and
27
+ * `ws_handshake_pipelining: false`. `Bun.serve` drains the declared
25
28
  * `Content-Length` of an oversized-body `413` reject and keeps the socket
26
29
  * alive (processing the correctly-framed pipelined request) even when the
27
30
  * response carries `Connection: close`, unlike `@hono/node-server` / Deno /
@@ -29,10 +32,15 @@ const ts_spine_capabilities = Object.freeze({
29
32
  * there is no desync — but the smuggling suite's strong "connection closes"
30
33
  * assertion doesn't hold; this flag routes Bun onto the suite's no-desync arm.
31
34
  * See `docs/security.md` §"Body Size Limiting".
35
+ *
36
+ * `Bun.serve` also answers `400` to an upgrade request that carries WebSocket
37
+ * frames in the same TCP write, before any handler runs, so the WS suite's
38
+ * frames-with-the-handshake case has nothing to drive there.
32
39
  */
33
40
  const ts_spine_bun_capabilities = Object.freeze({
34
41
  ...ts_spine_capabilities,
35
- oversized_reject_closes_connection: false
42
+ oversized_reject_closes_connection: false,
43
+ ws_handshake_pipelining: false
36
44
  });
37
45
  /** Default port for the Node TS spine binary — slots beside the Rust `spine_stub` (1177). */
38
46
  export const TS_SPINE_NODE_DEFAULT_PORT = 1178;
@@ -0,0 +1,35 @@
1
+ import '../assert_dev_env.js';
2
+ import { type BackendCapabilities } from './capabilities.js';
3
+ import type { SetupTest } from './setup.js';
4
+ /** Options for the WebSocket connection-cap parity suite. */
5
+ export interface WsConnectionCapCrossTestOptions {
6
+ /**
7
+ * Per-test fixture producer (`default_cross_process_setup(handle)`). Every
8
+ * socket in a case upgrades with the fresh-per-test keeper's session
9
+ * cookies, so they all land on one account.
10
+ */
11
+ readonly setup_test: SetupTest;
12
+ /** Backend capability flags; every case gates on `capabilities.ws`. */
13
+ readonly capabilities: BackendCapabilities;
14
+ /** Base URL the backend is reachable at (e.g. `http://localhost:1178`). */
15
+ readonly base_url: string;
16
+ /** WebSocket endpoint path on the backend (e.g. `/api/ws`). */
17
+ readonly ws_path: string;
18
+ /** Origin for the upgrades. Defaults to `base_url`. */
19
+ readonly origin?: string;
20
+ /**
21
+ * The backend's per-account connection cap, which each case opens past.
22
+ * Defaults to `DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT`, the cap both spines
23
+ * apply unless overridden; pass the consumer's value when it sets one, or
24
+ * `null` (a backend with the cap disabled) to skip the cases.
25
+ */
26
+ readonly max_connections_per_account?: number | null;
27
+ }
28
+ /**
29
+ * Register the WebSocket connection-cap parity suite over a real upgrade:
30
+ * one connection past the cap closes the account's oldest with
31
+ * `WS_CLOSE_CONNECTION_LIMIT` and nothing else, and a closed connection
32
+ * frees its slot.
33
+ */
34
+ export declare const describe_ws_connection_cap_cross_tests: (options: WsConnectionCapCrossTestOptions) => void;
35
+ //# sourceMappingURL=ws_connection_cap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws_connection_cap.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_connection_cap.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAsD9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,6DAA6D;AAC7D,MAAM,WAAW,+BAA+B;IAC/C;;;;OAIG;IACH,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;IACzB,uDAAuD;IACvD,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,2BAA2B,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACrD;AAED;;;;;GAKG;AACH,eAAO,MAAM,sCAAsC,GAClD,SAAS,+BAA+B,KACtC,IA4GF,CAAC"}