@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
@@ -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"}
@@ -0,0 +1,131 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-backend parity suite for the **per-account WebSocket connection cap**
4
+ * over a real upgrade.
5
+ *
6
+ * Both spines bound an account's concurrent WebSocket connections
7
+ * (`BackendWebsocketTransport`'s `max_connections_per_account` on the TS side,
8
+ * the Rust `fuz_realtime` `ConnectionRegistry` on the other; both default to
9
+ * `DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT`) with the same policy: **evict-oldest**
10
+ * — a connection past the cap is always admitted, and the account's oldest is
11
+ * closed with `WS_CLOSE_CONNECTION_LIMIT` and the reason `connection limit`.
12
+ * A client reads that close as "superseded": closed until the user acts, not
13
+ * revoked and not reconnecting (`FrontendWebsocketClient.superseded`). The
14
+ * code, the reason, and which socket closes are therefore wire contract, and
15
+ * this suite is the pin that runs on both. Two cases:
16
+ *
17
+ * - **the oldest is superseded, and it is not a revocation** — on one session,
18
+ * opening one socket past the cap closes exactly the first with the
19
+ * connection-limit code and reason; every other socket, the newest included,
20
+ * stays open and still dispatches. The session then upgrades again, which a
21
+ * revoked session could not — and that upgrade supersedes the next-oldest
22
+ * in turn.
23
+ * - **a closed connection frees its slot** — one socket held open while a
24
+ * cap's worth of others open and close in sequence is never evicted. A
25
+ * backend that leaks closed connections in its registry counts them toward
26
+ * the cap and closes the held socket, which is what this catches.
27
+ *
28
+ * Sockets open in sequence, each answering a `heartbeat` before the next
29
+ * opens (`create_admitted_ws_transport`), so the backend registered them in
30
+ * that order and "oldest" names the same socket on both spines (an upgrade
31
+ * answers its handshake before the connection is registered and admitted, so
32
+ * the handshake alone doesn't order them).
33
+ *
34
+ * **Consumer-agnostic**, like `describe_cross_process_ws_tests`: it drives
35
+ * only the `heartbeat` protocol action, present on every WS endpoint. Gated on
36
+ * `capabilities.ws`; cross-process only (`create_ws_transport` needs a real
37
+ * bound socket). Each case opens more than a cap's worth of real sockets,
38
+ * which is why it's a separate call rather than more cases on the WS
39
+ * round-trip suite.
40
+ *
41
+ * `$lib`-free by contract (relative specifiers only), like the sibling
42
+ * cross-backend suites.
43
+ *
44
+ * @module
45
+ */
46
+ import { assert, describe } from 'vitest';
47
+ import { heartbeat_action_spec } from '../../actions/heartbeat.js';
48
+ import { WS_CLOSE_CONNECTION_LIMIT } from '../../actions/transports.js';
49
+ import { DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT } from '../../actions/transports_ws_backend.js';
50
+ import { create_admitted_ws_transport } from '../transports/ws_transport.js';
51
+ import { test_if } from './capabilities.js';
52
+ /** The close reason both spines send with `WS_CLOSE_CONNECTION_LIMIT`. */
53
+ const CONNECTION_LIMIT_REASON = 'connection limit';
54
+ /**
55
+ * Register the WebSocket connection-cap parity suite over a real upgrade:
56
+ * one connection past the cap closes the account's oldest with
57
+ * `WS_CLOSE_CONNECTION_LIMIT` and nothing else, and a closed connection
58
+ * frees its slot.
59
+ */
60
+ export const describe_ws_connection_cap_cross_tests = (options) => {
61
+ const { setup_test, capabilities, base_url, ws_path, origin } = options;
62
+ const max = options.max_connections_per_account === undefined
63
+ ? DEFAULT_MAX_CONNECTIONS_PER_ACCOUNT
64
+ : options.max_connections_per_account;
65
+ const enabled = capabilities.ws && max !== null;
66
+ /**
67
+ * Open a socket the backend has admitted (a `heartbeat` round trip), so
68
+ * the connections are registered in the order they were opened.
69
+ */
70
+ const open_registered = (cookies) => create_admitted_ws_transport({ base_url, ws_path, cookies, origin });
71
+ const assert_superseded = async (client, label) => {
72
+ const closed = await client.wait_for_close(2000);
73
+ assert.ok(closed, `${label} did not close within 2s of the cap overflow`);
74
+ assert.strictEqual(client.close_code, WS_CLOSE_CONNECTION_LIMIT, `${label} close code`);
75
+ assert.strictEqual(client.close_reason, CONNECTION_LIMIT_REASON, `${label} close reason`);
76
+ };
77
+ /** Every client still open and answering — a fresh request id per call via `id`. */
78
+ const assert_all_answer = async (clients, id, message) => {
79
+ const results = await Promise.all(clients.map((client) => client.request(id, heartbeat_action_spec.method, {})));
80
+ assert.deepStrictEqual(results, clients.map(() => ({})), message);
81
+ assert.deepStrictEqual(clients.map((client) => client.close_code), clients.map(() => null), message);
82
+ };
83
+ // each case opens more than a cap's worth of real sockets in sequence, so its
84
+ // time scales with the cap and the backend's upgrade cost
85
+ describe('per-account websocket connection cap parity', { timeout: 30_000 }, () => {
86
+ test_if(enabled, 'one connection past the cap closes the oldest with WS_CLOSE_CONNECTION_LIMIT, and only it', async () => {
87
+ const fixture = await setup_test();
88
+ const cookies = fixture.transport.cookies();
89
+ const sockets = [];
90
+ try {
91
+ for (let i = 0; i < max; i++) {
92
+ sockets.push(await open_registered(cookies));
93
+ }
94
+ // at the cap, nothing has closed
95
+ await assert_all_answer(sockets, 'at_cap', 'every socket at the cap stays open');
96
+ // one past it: always admitted, never refused
97
+ sockets.push(await open_registered(cookies));
98
+ const [oldest, ...survivors] = sockets;
99
+ await assert_superseded(oldest, 'the oldest socket');
100
+ await assert_all_answer(survivors, 'past_cap', 'only the oldest may close; every other socket, the newest included, keeps answering');
101
+ // Not a revocation — the session still authenticates an upgrade
102
+ // (a revoked one is refused there). That upgrade is one more past
103
+ // the cap, so it supersedes the next-oldest.
104
+ sockets.push(await open_registered(cookies));
105
+ await assert_superseded(survivors[0], 'the next-oldest socket');
106
+ await assert_all_answer(sockets.slice(2), 'past_cap_again', 'the session keeps its newest sockets after a second overflow');
107
+ }
108
+ finally {
109
+ await Promise.all(sockets.map((client) => client.close().catch(() => { })));
110
+ }
111
+ });
112
+ test_if(enabled, 'a closed connection frees its slot', async () => {
113
+ const fixture = await setup_test();
114
+ const cookies = fixture.transport.cookies();
115
+ const held = await open_registered(cookies);
116
+ try {
117
+ // A cap's worth of connections, one at a time, each closed before
118
+ // the next opens — the account never holds more than a few at once,
119
+ // however late the backend notices each close.
120
+ for (let i = 0; i < max; i++) {
121
+ const client = await open_registered(cookies);
122
+ await client.close();
123
+ }
124
+ await assert_all_answer([held], 'held', 'closed connections must not count toward the cap — the held socket was evicted');
125
+ }
126
+ finally {
127
+ await held.close().catch(() => { });
128
+ }
129
+ });
130
+ });
131
+ };
@@ -37,7 +37,8 @@ export interface CrossProcessWsTestOptions {
37
37
  * `heartbeat` round-trip, `heartbeat`'s parameterless-shape contract, an
38
38
  * oversized message closing with `WS_CLOSE_MESSAGE_TOO_BIG`, anonymous-upgrade
39
39
  * refusal, disallowed-origin refusal, and — when `rpc_path` is supplied —
40
- * session-revocation closing the live socket.
40
+ * session revocation and account deletion closing the live socket, plus a
41
+ * self-revoking socket reading its reply before its close.
41
42
  */
42
43
  export declare const describe_cross_process_ws_tests: (options: CrossProcessWsTestOptions) => void;
43
44
  //# sourceMappingURL=ws_round_trip.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAoE9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,iEAAiE;AACjE,MAAM,WAAW,yBAAyB;IACzC;;;;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,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,GAAI,SAAS,yBAAyB,KAAG,IAwIpF,CAAC"}
1
+ {"version":3,"file":"ws_round_trip.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/ws_round_trip.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6F9B,OAAO,EAAE,KAAK,mBAAmB,EAAW,MAAM,mBAAmB,CAAC;AACtE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAK5C,iEAAiE;AACjE,MAAM,WAAW,yBAAyB;IACzC;;;;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,oEAAoE;IACpE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B;;;;;OAKG;IACH,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,+BAA+B,GAAI,SAAS,yBAAyB,KAAG,IA+RpF,CAAC"}