@fuzdev/fuz_app 0.64.0 → 0.66.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 (223) hide show
  1. package/dist/actions/CLAUDE.md +510 -946
  2. package/dist/actions/action_codegen.d.ts +1 -1
  3. package/dist/actions/action_codegen.js +1 -1
  4. package/dist/actions/action_event_data.d.ts +1 -1
  5. package/dist/actions/broadcast_api.d.ts +1 -1
  6. package/dist/actions/broadcast_api.js +1 -1
  7. package/dist/actions/cancel.d.ts +2 -2
  8. package/dist/actions/cancel.js +3 -3
  9. package/dist/actions/connection_closer.d.ts +1 -4
  10. package/dist/actions/connection_closer.d.ts.map +1 -1
  11. package/dist/actions/connection_closer.js +1 -4
  12. package/dist/actions/register_action_ws.d.ts +2 -2
  13. package/dist/actions/register_ws_endpoint.d.ts +1 -1
  14. package/dist/actions/transports_ws_auth_guard.d.ts +1 -2
  15. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  16. package/dist/actions/transports_ws_auth_guard.js +1 -2
  17. package/dist/auth/CLAUDE.md +570 -1871
  18. package/dist/auth/account_schema.d.ts +1 -1
  19. package/dist/auth/account_schema.d.ts.map +1 -1
  20. package/dist/auth/api_token_queries.js +1 -1
  21. package/dist/auth/audit_log_ddl.d.ts +1 -1
  22. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  23. package/dist/auth/audit_log_ddl.js +1 -1
  24. package/dist/auth/audit_log_schema.js +2 -2
  25. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  26. package/dist/auth/bootstrap_account.js +1 -5
  27. package/dist/auth/bootstrap_routes.d.ts +7 -1
  28. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  29. package/dist/auth/bootstrap_routes.js +15 -11
  30. package/dist/auth/daemon_token_middleware.d.ts +15 -5
  31. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  32. package/dist/auth/daemon_token_middleware.js +24 -15
  33. package/dist/auth/invite_queries.d.ts +17 -7
  34. package/dist/auth/invite_queries.d.ts.map +1 -1
  35. package/dist/auth/invite_queries.js +19 -8
  36. package/dist/auth/keyring.d.ts +6 -6
  37. package/dist/auth/keyring.js +8 -8
  38. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  39. package/dist/auth/role_grant_offer_actions.js +4 -2
  40. package/dist/auth/signup_routes.d.ts +47 -1
  41. package/dist/auth/signup_routes.d.ts.map +1 -1
  42. package/dist/auth/signup_routes.js +103 -52
  43. package/dist/db/create_db.d.ts.map +1 -1
  44. package/dist/db/create_db.js +13 -0
  45. package/dist/dev/setup.d.ts +2 -2
  46. package/dist/dev/setup.js +3 -3
  47. package/dist/env/resolve.d.ts +44 -7
  48. package/dist/env/resolve.d.ts.map +1 -1
  49. package/dist/env/resolve.js +94 -27
  50. package/dist/http/CLAUDE.md +243 -522
  51. package/dist/http/error_schemas.d.ts +0 -4
  52. package/dist/http/error_schemas.d.ts.map +1 -1
  53. package/dist/http/error_schemas.js +0 -4
  54. package/dist/http/ip_canonical.d.ts +5 -4
  55. package/dist/http/ip_canonical.d.ts.map +1 -1
  56. package/dist/http/ip_canonical.js +8 -4
  57. package/dist/http/jsonrpc.d.ts +23 -7
  58. package/dist/http/jsonrpc.d.ts.map +1 -1
  59. package/dist/http/jsonrpc.js +19 -3
  60. package/dist/http/origin.d.ts +1 -1
  61. package/dist/http/origin.js +1 -1
  62. package/dist/http/surface.d.ts +9 -2
  63. package/dist/http/surface.d.ts.map +1 -1
  64. package/dist/runtime/mock.d.ts +1 -1
  65. package/dist/runtime/mock.js +2 -2
  66. package/dist/server/app_server.d.ts +41 -10
  67. package/dist/server/app_server.d.ts.map +1 -1
  68. package/dist/server/app_server.js +10 -4
  69. package/dist/server/env.d.ts +7 -7
  70. package/dist/server/env.d.ts.map +1 -1
  71. package/dist/server/env.js +14 -14
  72. package/dist/server/static.d.ts +4 -4
  73. package/dist/server/static.js +7 -7
  74. package/dist/testing/CLAUDE.md +740 -418
  75. package/dist/testing/admin_integration.d.ts +18 -23
  76. package/dist/testing/admin_integration.d.ts.map +1 -1
  77. package/dist/testing/admin_integration.js +230 -216
  78. package/dist/testing/app_server.d.ts +141 -39
  79. package/dist/testing/app_server.d.ts.map +1 -1
  80. package/dist/testing/app_server.js +157 -44
  81. package/dist/testing/audit_completeness.d.ts +25 -22
  82. package/dist/testing/audit_completeness.d.ts.map +1 -1
  83. package/dist/testing/audit_completeness.js +198 -159
  84. package/dist/testing/bootstrap_success.d.ts +28 -0
  85. package/dist/testing/bootstrap_success.d.ts.map +1 -0
  86. package/dist/testing/bootstrap_success.js +144 -0
  87. package/dist/testing/cross_backend/backend_config.d.ts +113 -0
  88. package/dist/testing/cross_backend/backend_config.d.ts.map +1 -0
  89. package/dist/testing/cross_backend/backend_config.js +1 -0
  90. package/dist/testing/cross_backend/bench/bench_report.d.ts +46 -0
  91. package/dist/testing/cross_backend/bench/bench_report.d.ts.map +1 -0
  92. package/dist/testing/cross_backend/bench/bench_report.js +83 -0
  93. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts +44 -0
  94. package/dist/testing/cross_backend/bench/run_cross_impl_bench.d.ts.map +1 -0
  95. package/dist/testing/cross_backend/bench/run_cross_impl_bench.js +38 -0
  96. package/dist/testing/cross_backend/bench/scenario.d.ts +57 -0
  97. package/dist/testing/cross_backend/bench/scenario.d.ts.map +1 -0
  98. package/dist/testing/cross_backend/bench/scenario.js +28 -0
  99. package/dist/testing/cross_backend/bootstrap_backend.d.ts +41 -0
  100. package/dist/testing/cross_backend/bootstrap_backend.d.ts.map +1 -0
  101. package/dist/testing/cross_backend/bootstrap_backend.js +34 -0
  102. package/dist/testing/cross_backend/build_test_backend_paths.d.ts +24 -0
  103. package/dist/testing/cross_backend/build_test_backend_paths.d.ts.map +1 -0
  104. package/dist/testing/cross_backend/build_test_backend_paths.js +33 -0
  105. package/dist/testing/cross_backend/capabilities.d.ts +65 -0
  106. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -0
  107. package/dist/testing/cross_backend/capabilities.js +47 -0
  108. package/dist/testing/cross_backend/default_backend_configs.d.ts +122 -0
  109. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -0
  110. package/dist/testing/cross_backend/default_backend_configs.js +111 -0
  111. package/dist/testing/cross_backend/default_secrets.d.ts +40 -0
  112. package/dist/testing/cross_backend/default_secrets.d.ts.map +1 -0
  113. package/dist/testing/cross_backend/default_secrets.js +39 -0
  114. package/dist/testing/cross_backend/default_spine_surface.d.ts +64 -0
  115. package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -0
  116. package/dist/testing/cross_backend/default_spine_surface.js +121 -0
  117. package/dist/testing/cross_backend/setup.d.ts +451 -0
  118. package/dist/testing/cross_backend/setup.d.ts.map +1 -0
  119. package/dist/testing/cross_backend/setup.js +581 -0
  120. package/dist/testing/cross_backend/spawn_backend.d.ts +58 -0
  121. package/dist/testing/cross_backend/spawn_backend.d.ts.map +1 -0
  122. package/dist/testing/cross_backend/spawn_backend.js +229 -0
  123. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts +66 -0
  124. package/dist/testing/cross_backend/spine_stub_backend_config.d.ts.map +1 -0
  125. package/dist/testing/cross_backend/spine_stub_backend_config.js +49 -0
  126. package/dist/testing/cross_backend/sse_round_trip.d.ts +37 -0
  127. package/dist/testing/cross_backend/sse_round_trip.d.ts.map +1 -0
  128. package/dist/testing/cross_backend/sse_round_trip.js +137 -0
  129. package/dist/testing/cross_backend/standard.d.ts +96 -0
  130. package/dist/testing/cross_backend/standard.d.ts.map +1 -0
  131. package/dist/testing/cross_backend/standard.js +49 -0
  132. package/dist/testing/cross_backend/testing_reset_actions.d.ts +171 -0
  133. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -0
  134. package/dist/testing/cross_backend/testing_reset_actions.js +213 -0
  135. package/dist/testing/cross_backend/testing_server_bun.d.ts +5 -0
  136. package/dist/testing/cross_backend/testing_server_bun.d.ts.map +1 -0
  137. package/dist/testing/cross_backend/testing_server_bun.js +59 -0
  138. package/dist/testing/cross_backend/testing_server_core.d.ts +140 -0
  139. package/dist/testing/cross_backend/testing_server_core.d.ts.map +1 -0
  140. package/dist/testing/cross_backend/testing_server_core.js +68 -0
  141. package/dist/testing/cross_backend/testing_server_deno.d.ts +5 -0
  142. package/dist/testing/cross_backend/testing_server_deno.d.ts.map +1 -0
  143. package/dist/testing/cross_backend/testing_server_deno.js +37 -0
  144. package/dist/testing/cross_backend/testing_server_node.d.ts +5 -0
  145. package/dist/testing/cross_backend/testing_server_node.d.ts.map +1 -0
  146. package/dist/testing/cross_backend/testing_server_node.js +50 -0
  147. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts +72 -0
  148. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -0
  149. package/dist/testing/cross_backend/ts_spine_backend_config.js +112 -0
  150. package/dist/testing/cross_backend/ws_round_trip.d.ts +35 -0
  151. package/dist/testing/cross_backend/ws_round_trip.d.ts.map +1 -0
  152. package/dist/testing/cross_backend/ws_round_trip.js +113 -0
  153. package/dist/testing/data_exposure.d.ts +11 -14
  154. package/dist/testing/data_exposure.d.ts.map +1 -1
  155. package/dist/testing/data_exposure.js +123 -146
  156. package/dist/testing/db_entities.d.ts +22 -1
  157. package/dist/testing/db_entities.d.ts.map +1 -1
  158. package/dist/testing/db_entities.js +24 -1
  159. package/dist/testing/integration.d.ts +56 -21
  160. package/dist/testing/integration.d.ts.map +1 -1
  161. package/dist/testing/integration.js +294 -319
  162. package/dist/testing/integration_helpers.d.ts +16 -6
  163. package/dist/testing/integration_helpers.d.ts.map +1 -1
  164. package/dist/testing/integration_helpers.js +7 -7
  165. package/dist/testing/mock_fs.d.ts.map +1 -1
  166. package/dist/testing/mock_fs.js +0 -2
  167. package/dist/testing/rate_limiting.d.ts.map +1 -1
  168. package/dist/testing/rate_limiting.js +9 -0
  169. package/dist/testing/role_grant_helpers.d.ts +31 -0
  170. package/dist/testing/role_grant_helpers.d.ts.map +1 -0
  171. package/dist/testing/role_grant_helpers.js +46 -0
  172. package/dist/testing/round_trip.d.ts +20 -16
  173. package/dist/testing/round_trip.d.ts.map +1 -1
  174. package/dist/testing/round_trip.js +61 -86
  175. package/dist/testing/rpc_helpers.d.ts +10 -4
  176. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  177. package/dist/testing/rpc_helpers.js +1 -1
  178. package/dist/testing/rpc_round_trip.d.ts +24 -21
  179. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  180. package/dist/testing/rpc_round_trip.js +87 -104
  181. package/dist/testing/schema_introspect.d.ts +106 -0
  182. package/dist/testing/schema_introspect.d.ts.map +1 -0
  183. package/dist/testing/schema_introspect.js +123 -0
  184. package/dist/testing/schema_parity.d.ts +144 -0
  185. package/dist/testing/schema_parity.d.ts.map +1 -0
  186. package/dist/testing/schema_parity.js +233 -0
  187. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  188. package/dist/testing/sse_round_trip.js +1 -68
  189. package/dist/testing/standard.d.ts +56 -25
  190. package/dist/testing/standard.d.ts.map +1 -1
  191. package/dist/testing/standard.js +62 -5
  192. package/dist/testing/stubs.d.ts +21 -6
  193. package/dist/testing/stubs.d.ts.map +1 -1
  194. package/dist/testing/stubs.js +33 -23
  195. package/dist/testing/testing_rate_limiter.d.ts +59 -0
  196. package/dist/testing/testing_rate_limiter.d.ts.map +1 -0
  197. package/dist/testing/testing_rate_limiter.js +74 -0
  198. package/dist/testing/transports/bootstrap.d.ts +52 -0
  199. package/dist/testing/transports/bootstrap.d.ts.map +1 -0
  200. package/dist/testing/transports/bootstrap.js +70 -0
  201. package/dist/testing/transports/fetch_transport.d.ts +81 -0
  202. package/dist/testing/transports/fetch_transport.d.ts.map +1 -0
  203. package/dist/testing/transports/fetch_transport.js +74 -0
  204. package/dist/testing/transports/sse_frame_reader.d.ts +41 -0
  205. package/dist/testing/transports/sse_frame_reader.d.ts.map +1 -0
  206. package/dist/testing/transports/sse_frame_reader.js +84 -0
  207. package/dist/testing/transports/sse_transport.d.ts +54 -0
  208. package/dist/testing/transports/sse_transport.d.ts.map +1 -0
  209. package/dist/testing/transports/sse_transport.js +51 -0
  210. package/dist/testing/transports/ws_client.d.ts +108 -0
  211. package/dist/testing/transports/ws_client.d.ts.map +1 -0
  212. package/dist/testing/transports/ws_client.js +56 -0
  213. package/dist/testing/transports/ws_transport.d.ts +43 -0
  214. package/dist/testing/transports/ws_transport.d.ts.map +1 -0
  215. package/dist/testing/transports/ws_transport.js +169 -0
  216. package/dist/testing/ws_round_trip.d.ts +21 -103
  217. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  218. package/dist/testing/ws_round_trip.js +42 -40
  219. package/dist/ui/CLAUDE.md +5 -3
  220. package/dist/ui/MenuLink.svelte +16 -16
  221. package/dist/ui/MenuLink.svelte.d.ts +13 -4
  222. package/dist/ui/MenuLink.svelte.d.ts.map +1 -1
  223. package/package.json +10 -4
@@ -0,0 +1,581 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Per-test fixture protocol shared by in-process and cross-process
4
+ * transports.
5
+ *
6
+ * Each standard suite body takes a required
7
+ * `setup_test: () => Promise<TestFixture>` callback and invokes it once
8
+ * per test. The fixture carries everything a test needs to fire requests
9
+ * and assert on a single bootstrapped keeper account — transport,
10
+ * account / actor identity, three header builders, a multi-account mint
11
+ * factory, and (in-process only) the in-memory keyring + raw backend.
12
+ *
13
+ * `default_in_process_setup(options)` wraps `create_test_app` into the
14
+ * `SetupTest` contract. The cross-process sibling
15
+ * (`default_cross_process_setup`) lands alongside the spawn-a-backend
16
+ * transport plumbing — it implements the same contract by spawning a
17
+ * binary and bootstrapping over real HTTP.
18
+ *
19
+ * @module
20
+ */
21
+ import { z } from 'zod';
22
+ import { Uuid } from '@fuzdev/fuz_util/id.js';
23
+ import { ROLE_KEEPER } from '../../auth/role_schema.js';
24
+ import { DAEMON_TOKEN_HEADER } from '../../auth/daemon_token.js';
25
+ import { USERNAME_LENGTH_MAX } from '../../primitive_schemas.js';
26
+ import { create_test_app, create_test_account_with_credentials, DEFAULT_TEST_PASSWORD, } from '../app_server.js';
27
+ import { create_test_app_surface_spec } from '../stubs.js';
28
+ import { http_transport, } from '../rpc_helpers.js';
29
+ import { in_process_capabilities } from './capabilities.js';
30
+ import { create_fetch_transport } from '../transports/fetch_transport.js';
31
+ /**
32
+ * Build an `ExtraAccountFixture` from a seeded `{account, actor,
33
+ * api_token, session_cookie}` bundle and the session cookie name.
34
+ *
35
+ * Same shape produced by either path that seeds bootstrap-time
36
+ * secondaries: in-process via `create_test_account_with_credentials`
37
+ * against the live backend's DB, or cross-process via the
38
+ * `_testing_reset` RPC's `extra_accounts` output. Both call this
39
+ * helper so the fixture-side header builders + field plumbing stays
40
+ * in one place.
41
+ */
42
+ const build_extra_account_fixture = (seeded, cookie_name) => ({
43
+ account: seeded.account,
44
+ actor: seeded.actor,
45
+ api_token: seeded.api_token,
46
+ session_cookie: seeded.session_cookie,
47
+ create_session_headers: (extra) => ({
48
+ cookie: `${cookie_name}=${seeded.session_cookie}`,
49
+ ...extra,
50
+ }),
51
+ create_bearer_headers: (extra) => ({
52
+ authorization: `Bearer ${seeded.api_token}`,
53
+ ...extra,
54
+ }),
55
+ });
56
+ /**
57
+ * Wrap a Hono-style app into a `FetchTransport`-shaped object so the
58
+ * shared `TestFixtureBase.transport` type holds for both in-process and
59
+ * cross-process setups. In-process has no real cookie jar — the no-op
60
+ * `cookies()` returns `[]`; in-process tests build cookies via
61
+ * `fixture.create_session_headers()` instead.
62
+ */
63
+ const in_process_fetch_transport = (app) => {
64
+ const call = http_transport(app);
65
+ const transport = ((url, init) => call(url, init));
66
+ return Object.assign(transport, { cookies: () => [] });
67
+ };
68
+ /**
69
+ * Build a `SetupTest` that creates a fresh `TestApp` per call via
70
+ * `create_test_app` and projects it into the `TestFixture` shape.
71
+ *
72
+ * Same factory inputs `create_test_app` already takes — this helper
73
+ * is a projection layer, not a new lifecycle. fuz_app's own `src/test/`
74
+ * and consumer suites pass `default_in_process_setup({...factory_inputs})`
75
+ * in place of the old per-suite factory-input bundle. The `extra_accounts`
76
+ * slot (see `InProcessSetupOptions`) seeds bootstrap-time secondaries
77
+ * directly via `create_test_account_with_credentials` against the same
78
+ * DB the keeper just landed on — mirrors the cross-process
79
+ * `_testing_reset` cradle so suite bodies read
80
+ * `fixture.extra_accounts[username]` uniformly regardless of transport.
81
+ *
82
+ * The describe-level `auth_integration_truncate_tables` / pglite WASM
83
+ * cache lifecycle stays in `create_pglite_factory` / `create_describe_db`
84
+ * (`testing/db.js`) — `default_in_process_setup` doesn't manage db state
85
+ * beyond what `create_test_app` already does.
86
+ */
87
+ export const default_in_process_setup = (options) => async () => {
88
+ const test_app = await create_test_app(options);
89
+ // Seed bootstrap-time secondaries against the same DB the keeper
90
+ // just landed on. Direct-insert is the only path for roles whose
91
+ // `grant_paths` excludes `'admin'` (e.g. `ROLE_KEEPER`) — see
92
+ // `ExtraAccountSpec` for why this bypass is bootstrap-cradle-only.
93
+ const extra_accounts = {};
94
+ const { cookie_name } = options.session_options;
95
+ for (const spec of options.extra_accounts ?? []) {
96
+ const seeded = await create_test_account_with_credentials({
97
+ db: test_app.backend.deps.db,
98
+ keyring: test_app.backend.keyring,
99
+ session_options: options.session_options,
100
+ password: test_app.backend.deps.password,
101
+ username: spec.username,
102
+ password_value: spec.password_value,
103
+ roles: [...spec.roles],
104
+ });
105
+ extra_accounts[spec.username] = build_extra_account_fixture(seeded, cookie_name);
106
+ }
107
+ return {
108
+ in_process: true,
109
+ transport: in_process_fetch_transport(test_app.app),
110
+ // In-process the wrapper is stateless and never auto-adds Origin —
111
+ // `options` is accepted for API symmetry with cross-process but
112
+ // has no observable effect.
113
+ fresh_transport: () => in_process_fetch_transport(test_app.app),
114
+ account: test_app.backend.account,
115
+ actor: test_app.backend.actor,
116
+ create_session_headers: test_app.create_session_headers,
117
+ create_bearer_headers: test_app.create_bearer_headers,
118
+ create_daemon_token_headers: test_app.create_daemon_token_headers,
119
+ create_account: test_app.create_account,
120
+ extra_accounts,
121
+ keyring: test_app.backend.keyring,
122
+ backend_internals: test_app.backend,
123
+ };
124
+ };
125
+ /**
126
+ * Strip the non-serializable members so the result can be passed to
127
+ * vitest's `project.provide`. Call in `globalSetup` before provide.
128
+ */
129
+ export const serialize_bootstrapped_handle = (handle) => ({
130
+ config: handle.config,
131
+ daemon_token: handle.daemon_token,
132
+ keeper_account: handle.keeper_account,
133
+ keeper_actor: handle.keeper_actor,
134
+ keeper_cookies: [...handle.keeper_cookies],
135
+ });
136
+ /**
137
+ * Rebuild a usable handle from the serialized subset. Synthesizes a
138
+ * fresh {@link FetchTransport} primed with the keeper's `Set-Cookie`
139
+ * values so `_testing_reset` and other keeper-authenticated calls work.
140
+ * The returned shape omits `child` and `teardown` — lifecycle stays
141
+ * with `globalSetup`; tests that try to teardown themselves wouldn't
142
+ * have a serializable reference anyway.
143
+ */
144
+ export const reconstruct_bootstrapped_handle = (serialized) => ({
145
+ config: serialized.config,
146
+ daemon_token: serialized.daemon_token,
147
+ keeper_account: serialized.keeper_account,
148
+ keeper_actor: serialized.keeper_actor,
149
+ keeper_cookies: serialized.keeper_cookies,
150
+ keeper_transport: create_fetch_transport({
151
+ base_url: serialized.config.base_url,
152
+ initial_cookies: serialized.keeper_cookies,
153
+ }),
154
+ });
155
+ /**
156
+ * Structural subset of `SignupOutput` the runner cares about. Looser
157
+ * than the canonical `auth/signup_routes.ts` schema — kept local so this
158
+ * module doesn't pull the full auth-domain schema into its dep graph.
159
+ */
160
+ const SignupResponseShape = z.object({
161
+ ok: z.literal(true),
162
+ account: z.object({ id: Uuid, username: z.string() }),
163
+ actor: z.object({ id: Uuid }),
164
+ });
165
+ /** Structural subset of `account_token_create`'s output. */
166
+ const TokenCreateResponseShape = z.object({
167
+ token: z.string(),
168
+ id: z.string(),
169
+ });
170
+ /**
171
+ * Per-test username generator for the *default* `fixture.create_account()`
172
+ * call shape (no caller-supplied username). PID + timestamp + counter
173
+ * keeps the generated username unique across vitest workers, parallel
174
+ * suites within one worker, and reruns of the same suite. The suffix
175
+ * is base36-encoded to stay compact; long prefixes are truncated so
176
+ * the total never exceeds `USERNAME_LENGTH_MAX` (39).
177
+ *
178
+ * Caller-supplied usernames pass through *as-is* now that fresh-keeper-
179
+ * per-test wipes the DB between tests — hardcoded names work and tests
180
+ * can reference accounts by their literal name.
181
+ */
182
+ let username_counter = 0;
183
+ const PID_BASE36 = process.pid.toString(36);
184
+ const generate_default_username = () => {
185
+ const suffix = `_${PID_BASE36}_${Date.now().toString(36)}_${(++username_counter).toString(36)}`;
186
+ const max_prefix = USERNAME_LENGTH_MAX - suffix.length;
187
+ const prefix = 'test_user';
188
+ const safe_prefix = prefix.length > max_prefix ? prefix.slice(0, max_prefix) : prefix;
189
+ return `${safe_prefix}${suffix}`;
190
+ };
191
+ /**
192
+ * POST a JSON-RPC call via the supplied transport and return the raw
193
+ * `result` field. Throws with a labeled error on HTTP failure or RPC
194
+ * error envelope. Used by the cross-process setup harness for
195
+ * keeper-driven and per-test RPC plumbing — the setup module talks to
196
+ * the running backend at the wire level rather than via the
197
+ * spec-driven `rpc_call_for_spec` because each call is internal to the
198
+ * harness and doesn't need spec-shape validation (callers narrow the
199
+ * `unknown` result against the shape they expect).
200
+ *
201
+ * `extra_headers` covers the daemon-token auth case for
202
+ * `_testing_reset`; `Content-Type: application/json` is always set.
203
+ * The JSON-RPC `id` mirrors `method` so server-side logs correlate
204
+ * cleanly.
205
+ */
206
+ const rpc_via_transport = async (transport, rpc_path, method, params, backend_name, extra_headers) => {
207
+ const response = await transport(rpc_path, {
208
+ method: 'POST',
209
+ headers: { 'Content-Type': 'application/json', ...extra_headers },
210
+ body: JSON.stringify({ jsonrpc: '2.0', method, params, id: method }),
211
+ });
212
+ if (!response.ok) {
213
+ const body = await response.text().catch(() => '<unreadable>');
214
+ throw new Error(`${method}(${backend_name}) HTTP failed: status=${response.status} body=${body}`);
215
+ }
216
+ const raw = (await response.json());
217
+ if (raw.error) {
218
+ throw new Error(`${method}(${backend_name}) RPC error: ${JSON.stringify(raw.error)}`);
219
+ }
220
+ return raw.result;
221
+ };
222
+ /** Structural subset of `_testing_reset`'s output. */
223
+ const TestingResetResponseShape = z.object({
224
+ account: z.object({ id: Uuid, username: z.string() }),
225
+ actor: z.object({ id: Uuid }),
226
+ api_token: z.string(),
227
+ session_cookie: z.string(),
228
+ extra_accounts: z.array(z.object({
229
+ account: z.object({ id: Uuid, username: z.string() }),
230
+ actor: z.object({ id: Uuid }),
231
+ api_token: z.string(),
232
+ session_cookie: z.string(),
233
+ })),
234
+ });
235
+ /**
236
+ * Fire the `_testing_reset` RPC action over the keeper's daemon-token
237
+ * channel. Wipes the DB, re-seeds a fresh keeper (with any
238
+ * `extra_keeper_roles`), and seeds any caller-requested
239
+ * `extra_accounts`. Returns the new credentials so the per-test fixture
240
+ * can close over them.
241
+ */
242
+ const fire_testing_reset = async (handle, options) => {
243
+ const raw = await rpc_via_transport(handle.keeper_transport, handle.config.rpc_path, '_testing_reset', {
244
+ extra_keeper_roles: options.extra_keeper_roles ?? [],
245
+ extra_accounts: (options.extra_accounts ?? []).map((spec) => ({
246
+ username: spec.username,
247
+ ...(spec.password_value !== undefined && { password_value: spec.password_value }),
248
+ roles: [...spec.roles],
249
+ })),
250
+ }, handle.config.name, { [DAEMON_TOKEN_HEADER]: handle.daemon_token });
251
+ const parsed = TestingResetResponseShape.safeParse(raw);
252
+ if (!parsed.success) {
253
+ throw new Error(`_testing_reset(${handle.config.name}) returned unexpected result: ${JSON.stringify(raw)} (${parsed.error.message})`);
254
+ }
255
+ return {
256
+ keeper: {
257
+ account: parsed.data.account,
258
+ actor: parsed.data.actor,
259
+ api_token: parsed.data.api_token,
260
+ session_cookie: parsed.data.session_cookie,
261
+ },
262
+ extra_accounts: parsed.data.extra_accounts,
263
+ };
264
+ };
265
+ /**
266
+ * Extract the named cookie's value from `transport.cookies()`. The jar
267
+ * stores `name=value` heads; this peels the value side for the named
268
+ * cookie. Throws when the cookie is missing — every authenticated
269
+ * mint should land one in the jar, so absence is a setup bug.
270
+ */
271
+ const extract_cookie_value = (transport, cookie_name, backend_name) => {
272
+ for (const raw of transport.cookies()) {
273
+ const eq = raw.indexOf('=');
274
+ if (eq <= 0)
275
+ continue;
276
+ if (raw.slice(0, eq).trim() === cookie_name) {
277
+ return raw.slice(eq + 1);
278
+ }
279
+ }
280
+ throw new Error(`session cookie '${cookie_name}' missing from ${backend_name} transport jar after auth — ` +
281
+ `got ${JSON.stringify(transport.cookies())}`);
282
+ };
283
+ /**
284
+ * Mint an account via invite-gated `POST /signup` + `POST /login` on a
285
+ * fresh `FetchTransport`, then create an API token via the
286
+ * `account_token_create` RPC so the returned account has both session
287
+ * + bearer credentials.
288
+ *
289
+ * The keeper (admin via bootstrap, holds both `ROLE_KEEPER` +
290
+ * `ROLE_ADMIN`) creates a username-scoped invite via `invite_create`
291
+ * RPC; signup claims the invite atomically. Lets the cross-process
292
+ * harness stay on the production `open_signup: false` default —
293
+ * mirroring real-user signup semantics rather than synthetically
294
+ * opening signup for the duration of the suite.
295
+ *
296
+ * Signup and login both fire so the per-test fixture exercises both
297
+ * production code paths — signup mints the account + initial session;
298
+ * login replaces the cookie with a fresh one (so any login-specific
299
+ * post-conditions hold). See §Open Q10 for the design rationale.
300
+ */
301
+ const mint_account = async (handle, options) => {
302
+ const transport = create_fetch_transport({ base_url: handle.config.base_url });
303
+ // Caller-supplied usernames pass through as-is — fresh-keeper-per-test
304
+ // wipes the DB between tests, so hardcoded names (e.g. `'eve_attacker'`,
305
+ // `'user_two'`) don't collide. Default to a unique generated name when
306
+ // the caller doesn't care.
307
+ const username = options.username ?? generate_default_username();
308
+ // Use the shared `DEFAULT_TEST_PASSWORD` so the cross-process bootstrap
309
+ // can never drift from the in-process default — the integration suite's
310
+ // hardcoded login bodies also import the same constant, so a future
311
+ // divergence becomes a typecheck miss instead of a runtime password
312
+ // mismatch (which previously silently 401'd ~20 login tests).
313
+ const password = options.password_value ?? DEFAULT_TEST_PASSWORD;
314
+ // Keeper creates a username-scoped invite so the signup below can claim
315
+ // it. The keeper holds `ROLE_ADMIN` from bootstrap (see
316
+ // `bootstrap_account.ts` — both `ROLE_KEEPER` and `ROLE_ADMIN` grants
317
+ // are created in the bootstrap transaction), so `invite_create` (admin-
318
+ // only) authorizes without any extra grants.
319
+ const invite_result = (await rpc_via_transport(handle.keeper_transport, handle.config.rpc_path, 'invite_create', { username }, handle.config.name));
320
+ if (!invite_result?.invite?.id) {
321
+ throw new Error(`invite_create(${handle.config.name}, username=${username}) returned unexpected result: ` +
322
+ JSON.stringify(invite_result));
323
+ }
324
+ const signup_response = await transport('/api/account/signup', {
325
+ method: 'POST',
326
+ headers: { 'Content-Type': 'application/json' },
327
+ body: JSON.stringify({ username, password }),
328
+ });
329
+ if (!signup_response.ok) {
330
+ const body = await signup_response.text().catch(() => '<unreadable>');
331
+ throw new Error(`signup(${handle.config.name}) failed: status=${signup_response.status} body=${body}`);
332
+ }
333
+ const signup_raw = await signup_response.json();
334
+ const parsed = SignupResponseShape.safeParse(signup_raw);
335
+ if (!parsed.success) {
336
+ throw new Error(`signup(${handle.config.name}) returned unexpected body: ${JSON.stringify(signup_raw)} (${parsed.error.message})`);
337
+ }
338
+ const login_response = await transport('/api/account/login', {
339
+ method: 'POST',
340
+ headers: { 'Content-Type': 'application/json' },
341
+ body: JSON.stringify({ username, password }),
342
+ });
343
+ if (!login_response.ok) {
344
+ const body = await login_response.text().catch(() => '<unreadable>');
345
+ throw new Error(`login(${handle.config.name}) failed: status=${login_response.status} body=${body}`);
346
+ }
347
+ // Drain the body so the connection releases — Hono's login returns
348
+ // `{ok: true}` and we already have the cookie via the jar.
349
+ await login_response.arrayBuffer().catch(() => undefined);
350
+ const token_result = await rpc_via_transport(transport, handle.config.rpc_path, 'account_token_create', {}, handle.config.name);
351
+ const token_parsed = TokenCreateResponseShape.safeParse(token_result);
352
+ if (!token_parsed.success) {
353
+ throw new Error(`account_token_create(${handle.config.name}) returned unexpected result: ${JSON.stringify(token_result)}`);
354
+ }
355
+ return {
356
+ transport,
357
+ account: parsed.data.account,
358
+ actor: parsed.data.actor,
359
+ session_cookie: extract_cookie_value(transport, handle.config.cookie_name, handle.config.name),
360
+ api_token: token_parsed.data.token,
361
+ };
362
+ };
363
+ /**
364
+ * Grant additional roles to a per-test account by driving the production
365
+ * `role_grant_offer_create` (keeper) + `role_grant_offer_accept`
366
+ * (account) consent flow. After this returns, the account holds a real
367
+ * `role_grant` row for each role — indistinguishable from a production
368
+ * grant. Costs ~2 RPCs (~30-50ms) per role.
369
+ *
370
+ * Used by `fixture.create_account({roles: [...]})`. Roles whose
371
+ * `RoleSpec.grant_paths` don't include `'admin'` reject at
372
+ * offer-create time and surface a loud RPC error — those roles must be
373
+ * declared via `extra_accounts` instead (bootstrap-time seeding).
374
+ */
375
+ const grant_roles_via_offer_accept = async (handle, minted, roles) => {
376
+ for (const role of roles) {
377
+ const offer_result = (await rpc_via_transport(handle.keeper_transport, handle.config.rpc_path, 'role_grant_offer_create', { to_account_id: minted.account.id, role }, `${handle.config.name}, role=${role}`));
378
+ if (!offer_result?.offer?.id) {
379
+ throw new Error(`role_grant_offer_create(${handle.config.name}, role=${role}) returned unexpected result: ` +
380
+ JSON.stringify(offer_result));
381
+ }
382
+ const offer_id = offer_result.offer.id;
383
+ const accept_result = await rpc_via_transport(minted.transport, handle.config.rpc_path, 'role_grant_offer_accept', { offer_id }, `${handle.config.name}, role=${role}`);
384
+ if (!accept_result) {
385
+ throw new Error(`role_grant_offer_accept(${handle.config.name}, role=${role}) returned unexpected result: ` +
386
+ JSON.stringify(accept_result));
387
+ }
388
+ }
389
+ };
390
+ /**
391
+ * Build a keeper-authenticated `FetchTransport` that closes over the
392
+ * supplied session cookie. Used by the per-test fixture so each call to
393
+ * `setup_test()` builds a transport carrying the freshly re-seeded
394
+ * keeper's cookie (not the original `globalSetup` keeper's, which is
395
+ * stale after `_testing_reset` wipes it).
396
+ */
397
+ const create_keeper_transport = (handle, cookie_name, session_cookie) => create_fetch_transport({
398
+ base_url: handle.config.base_url,
399
+ initial_cookies: [`${cookie_name}=${session_cookie}`],
400
+ });
401
+ /**
402
+ * Build a `SetupTest` against a spawned + bootstrapped backend.
403
+ *
404
+ * Per-test body (unconditional reset — fresh keeper every test):
405
+ *
406
+ * 1. Fire `_testing_reset` via the keeper's daemon-token channel. The
407
+ * action wipes auth tables, seeds a fresh keeper (with
408
+ * `extra_keeper_roles` applied), seeds any `extra_accounts`, and
409
+ * returns the new credentials.
410
+ * 2. Build the `TestFixture` closing over the new keeper as the
411
+ * fixture's primary `account` / `actor` (matching in-process
412
+ * semantics). `fixture.extra_accounts[username]` exposes any
413
+ * bootstrap-time secondaries.
414
+ * 3. `fixture.create_account()` mints additional *post-bootstrap*
415
+ * accounts via the production signup + login flow (invite → signup
416
+ * → login → token). Roles go through offer/accept (production
417
+ * consent path).
418
+ *
419
+ * No `reset: boolean` opt-in — every test runs against a freshly
420
+ * bootstrapped keeper. This converges in-process and cross-process
421
+ * keeper lifetimes; mutation-cascade tests (password change,
422
+ * revoke-all) and hardcoded-username signup tests work uniformly.
423
+ */
424
+ export const default_cross_process_setup = (handle, options) => {
425
+ const extra_keeper_roles = options?.extra_keeper_roles ?? [];
426
+ const extra_account_specs = options?.extra_accounts ?? [];
427
+ const { cookie_name } = handle.config;
428
+ return async () => {
429
+ const { keeper, extra_accounts: seeded_extras } = await fire_testing_reset(handle, {
430
+ extra_keeper_roles,
431
+ extra_accounts: extra_account_specs,
432
+ });
433
+ // Rebuild the keeper transport with the new session cookie — the
434
+ // reset action wiped the `globalSetup` keeper's auth_session row,
435
+ // so the handle's `keeper_transport` is now signing requests with
436
+ // a stale cookie. The new transport closes over the fresh
437
+ // `session_cookie` for any keeper-acting calls this test makes
438
+ // (e.g. `fixture.create_account()`'s `invite_create` step).
439
+ const keeper_transport = create_keeper_transport(handle, cookie_name, keeper.session_cookie);
440
+ const refreshed_handle = {
441
+ ...handle,
442
+ keeper_transport,
443
+ keeper_account: keeper.account,
444
+ keeper_actor: keeper.actor,
445
+ keeper_cookies: [`${cookie_name}=${keeper.session_cookie}`],
446
+ };
447
+ const create_session_headers = (extra) => ({
448
+ cookie: `${cookie_name}=${keeper.session_cookie}`,
449
+ ...extra,
450
+ });
451
+ const create_bearer_headers = (extra) => ({
452
+ authorization: `Bearer ${keeper.api_token}`,
453
+ ...extra,
454
+ });
455
+ const create_daemon_token_headers = (extra) => ({
456
+ [DAEMON_TOKEN_HEADER]: handle.daemon_token,
457
+ ...extra,
458
+ });
459
+ const create_account = async (account_options) => {
460
+ const other = await mint_account(refreshed_handle, {
461
+ ...(account_options?.username !== undefined && { username: account_options.username }),
462
+ ...(account_options?.password_value !== undefined && {
463
+ password_value: account_options.password_value,
464
+ }),
465
+ });
466
+ if (account_options?.roles && account_options.roles.length > 0) {
467
+ await grant_roles_via_offer_accept(refreshed_handle, other, account_options.roles);
468
+ }
469
+ return {
470
+ account: other.account,
471
+ actor: other.actor,
472
+ session_cookie: other.session_cookie,
473
+ api_token: other.api_token,
474
+ create_session_headers: (extra) => ({
475
+ cookie: `${cookie_name}=${other.session_cookie}`,
476
+ ...extra,
477
+ }),
478
+ create_bearer_headers: (extra) => ({
479
+ authorization: `Bearer ${other.api_token}`,
480
+ ...extra,
481
+ }),
482
+ };
483
+ };
484
+ const extra_accounts = {};
485
+ for (let i = 0; i < extra_account_specs.length; i++) {
486
+ const spec = extra_account_specs[i];
487
+ const seeded = seeded_extras[i];
488
+ if (!spec || !seeded)
489
+ continue;
490
+ extra_accounts[spec.username] = build_extra_account_fixture(seeded, cookie_name);
491
+ }
492
+ // Per-test transport — fresh jar carrying the new keeper's cookie
493
+ // so requests authenticate as the keeper without callers having to
494
+ // thread cookies manually. Tests acting as the fresh keeper use
495
+ // this transport directly; tests minting secondaries thread the
496
+ // secondary's transport via `fixture.create_account()`.
497
+ const transport = create_fetch_transport({
498
+ base_url: handle.config.base_url,
499
+ initial_cookies: [`${cookie_name}=${keeper.session_cookie}`],
500
+ });
501
+ return {
502
+ in_process: false,
503
+ transport,
504
+ fresh_transport: (fresh_options) => create_fetch_transport({
505
+ base_url: handle.config.base_url,
506
+ ...(fresh_options?.origin !== undefined && { origin: fresh_options.origin }),
507
+ }),
508
+ account: keeper.account,
509
+ actor: keeper.actor,
510
+ create_session_headers,
511
+ create_bearer_headers,
512
+ create_daemon_token_headers,
513
+ create_account,
514
+ extra_accounts,
515
+ };
516
+ };
517
+ };
518
+ // NOTE: bootstrap config is read from `options.bootstrap` — top-level slot,
519
+ // single source of truth for both the surface spec (so the route appears in
520
+ // `expected_public_routes` / attack-surface iteration) AND the live
521
+ // `create_test_app` (so the route exists at dispatch time and returns 403
522
+ // `ERROR_ALREADY_BOOTSTRAPPED` matching its declared 403 schema). Same
523
+ // precedent as `rpc_endpoints`. Discriminated union shape (`mode: 'disabled'`
524
+ // | `'surface_only'` | `'live'`) replaces the old `token_path: string | null`
525
+ // overload that conflated three deployment intents on one channel.
526
+ /**
527
+ * Build the full in-process suite bundle in a single helper invocation.
528
+ * Output covers `{setup_test, surface_source, capabilities}` plus every
529
+ * factory input the Tier 1 suites read at their top level
530
+ * (`session_options`, `create_route_specs`, `rpc_endpoints`) — so the
531
+ * call site spreads once and adds only suite-specific extras
532
+ * (`roles`, `skip_routes`, `input_overrides`, `db_factories`, ...).
533
+ *
534
+ * ```ts
535
+ * // Suite-extras-free call: helper output is the entire options bag.
536
+ * describe_round_trip_validation(default_in_process_suite_options({
537
+ * session_options,
538
+ * create_route_specs,
539
+ * rpc_endpoints: [rpc_endpoint_spec],
540
+ * }));
541
+ *
542
+ * // With suite-specific extras: spread and add.
543
+ * describe_standard_admin_integration_tests({
544
+ * ...default_in_process_suite_options({
545
+ * session_options, create_route_specs, rpc_endpoints,
546
+ * extra_keeper_roles: [ROLE_ADMIN],
547
+ * }),
548
+ * roles,
549
+ * });
550
+ * ```
551
+ *
552
+ * Suites that don't read `session_options` / `rpc_endpoints` at their
553
+ * top level (`round_trip`, `data_exposure`) accept the spread anyway —
554
+ * excess properties on spread sources aren't checked by TS, and the
555
+ * uniform shape keeps consumer call sites mechanical.
556
+ */
557
+ export const default_in_process_suite_options = (options) => ({
558
+ setup_test: default_in_process_setup({
559
+ session_options: options.session_options,
560
+ create_route_specs: options.create_route_specs,
561
+ rpc_endpoints: options.rpc_endpoints,
562
+ bootstrap: options.bootstrap,
563
+ app_options: options.app_options,
564
+ roles: [ROLE_KEEPER, ...(options.extra_keeper_roles ?? [])],
565
+ extra_accounts: options.extra_accounts,
566
+ }),
567
+ surface_source: options.surface_source ??
568
+ create_test_app_surface_spec({
569
+ session_options: options.session_options,
570
+ create_route_specs: options.create_route_specs,
571
+ rpc_endpoints: options.rpc_endpoints,
572
+ // Mirror what `create_test_app` → `create_app_server` will mount.
573
+ // Both helpers read from the top-level `bootstrap` slot so surface
574
+ // and live app stay in sync by construction.
575
+ bootstrap: options.bootstrap,
576
+ }),
577
+ capabilities: in_process_capabilities,
578
+ session_options: options.session_options,
579
+ create_route_specs: options.create_route_specs,
580
+ rpc_endpoints: options.rpc_endpoints,
581
+ });
@@ -0,0 +1,58 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Spawn a test backend binary, wait for it to come up, and return a
4
+ * handle the test harness drives.
5
+ *
6
+ * Lifecycle:
7
+ *
8
+ * 1. Write the bootstrap token (`config.bootstrap.token`) to
9
+ * `config.bootstrap.token_path` so the binary picks it up at startup.
10
+ * 2. `child_process.spawn(...)` the binary with `detached: true` —
11
+ * creates a new process group so a `SIGTERM` to the negative PID
12
+ * tears down any descendants the binary spawned (PTYs, child
13
+ * workers). vitest worker death + Ctrl+C handlers also fire the
14
+ * group teardown so ports never strand.
15
+ * 3. Poll `{base_url}{health_path}` until it returns 2xx or
16
+ * `startup_timeout_ms` elapses.
17
+ * 4. Read `config.bootstrap.daemon_token_path` to load the binary's
18
+ * deterministic daemon token; thread it onto `BackendHandle` so
19
+ * `_testing_reset` and other keeper-credential calls can authenticate.
20
+ *
21
+ * Bootstrapping (`POST /api/account/bootstrap`) is a separate concern —
22
+ * the caller composes `bootstrap()` from `../transports/bootstrap.ts`
23
+ * against a `FetchTransport` built around `handle.config.base_url`.
24
+ * Splitting the two keeps `spawn_backend` consumer-agnostic — fuz_app
25
+ * knows nothing about specific binary contents.
26
+ *
27
+ * @module
28
+ */
29
+ import { type ChildProcess } from 'node:child_process';
30
+ import type { BackendConfig } from './backend_config.js';
31
+ /** Handle returned by `spawn_backend` — passed to per-test setup helpers. */
32
+ export interface BackendHandle {
33
+ /** The config used to spawn this backend. Carried for diagnostic + downstream access. */
34
+ readonly config: BackendConfig;
35
+ /** Child process reference — exposed for diagnostic logging only. */
36
+ readonly child: ChildProcess;
37
+ /**
38
+ * Deterministic daemon token captured from
39
+ * `config.bootstrap.daemon_token_path` after the binary booted.
40
+ * `default_cross_process_setup` builds keeper-daemon-token headers
41
+ * from this for `_testing_reset` calls.
42
+ */
43
+ readonly daemon_token: string;
44
+ /**
45
+ * SIGTERM the child's process group, drain stderr, await exit. Idempotent —
46
+ * calls after the first are no-ops.
47
+ */
48
+ readonly teardown: () => Promise<void>;
49
+ }
50
+ /**
51
+ * Spawn `config.start_command` and return a handle once the binary is
52
+ * health-probe-ready and the daemon-token file is readable.
53
+ *
54
+ * Errors at any stage SIGTERM the child group before rethrowing — the
55
+ * caller never sees a half-started backend.
56
+ */
57
+ export declare const spawn_backend: (config: BackendConfig) => Promise<BackendHandle>;
58
+ //# sourceMappingURL=spawn_backend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"spawn_backend.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/spawn_backend.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAQ,KAAK,YAAY,EAAC,MAAM,oBAAoB,CAAC;AAI5D,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,qBAAqB,CAAC;AAIvD,6EAA6E;AAC7E,MAAM,WAAW,aAAa;IAC7B,yFAAyF;IACzF,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;IAC7B;;;;;OAKG;IACH,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACvC;AAmID;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GAAU,QAAQ,aAAa,KAAG,OAAO,CAAC,aAAa,CA8FhF,CAAC"}