@fuzdev/fuz_app 0.140.0 → 0.141.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 (182) hide show
  1. package/dist/actions/CLAUDE.md +62 -17
  2. package/dist/actions/action_rpc.d.ts +5 -1
  3. package/dist/actions/action_rpc.d.ts.map +1 -1
  4. package/dist/actions/action_rpc.js +9 -1
  5. package/dist/actions/connection_closer.d.ts +25 -2
  6. package/dist/actions/connection_closer.d.ts.map +1 -1
  7. package/dist/actions/connection_closer.js +8 -1
  8. package/dist/actions/register_action_ws.d.ts +42 -6
  9. package/dist/actions/register_action_ws.d.ts.map +1 -1
  10. package/dist/actions/register_action_ws.js +87 -19
  11. package/dist/actions/register_ws_endpoint.d.ts +15 -6
  12. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  13. package/dist/actions/register_ws_endpoint.js +5 -2
  14. package/dist/actions/socket.svelte.d.ts +19 -4
  15. package/dist/actions/socket.svelte.d.ts.map +1 -1
  16. package/dist/actions/socket.svelte.js +31 -4
  17. package/dist/actions/transports.d.ts +30 -1
  18. package/dist/actions/transports.d.ts.map +1 -1
  19. package/dist/actions/transports.js +30 -1
  20. package/dist/actions/transports_ws_auth_guard.d.ts +13 -6
  21. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  22. package/dist/actions/transports_ws_auth_guard.js +24 -10
  23. package/dist/actions/transports_ws_backend.d.ts +51 -5
  24. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  25. package/dist/actions/transports_ws_backend.js +62 -9
  26. package/dist/actions/ws_endpoint_spec.d.ts +11 -10
  27. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  28. package/dist/auth/CLAUDE.md +11 -9
  29. package/dist/auth/account_route_schema.d.ts +1 -0
  30. package/dist/auth/account_route_schema.d.ts.map +1 -1
  31. package/dist/auth/account_route_schema.js +4 -0
  32. package/dist/auth/api_token.d.ts +20 -0
  33. package/dist/auth/api_token.d.ts.map +1 -1
  34. package/dist/auth/api_token.js +21 -1
  35. package/dist/auth/api_token_queries.d.ts +22 -4
  36. package/dist/auth/api_token_queries.d.ts.map +1 -1
  37. package/dist/auth/api_token_queries.js +11 -6
  38. package/dist/auth/audit_log_routes.d.ts +10 -0
  39. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  40. package/dist/auth/audit_log_routes.js +20 -0
  41. package/dist/auth/audit_log_schema.d.ts +30 -5
  42. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  43. package/dist/auth/audit_log_schema.js +30 -0
  44. package/dist/auth/bearer_auth.d.ts +4 -2
  45. package/dist/auth/bearer_auth.d.ts.map +1 -1
  46. package/dist/auth/bearer_auth.js +19 -8
  47. package/dist/auth/bootstrap_route_schema.d.ts +1 -0
  48. package/dist/auth/bootstrap_route_schema.d.ts.map +1 -1
  49. package/dist/auth/bootstrap_route_schema.js +1 -0
  50. package/dist/auth/cleanup.d.ts +9 -9
  51. package/dist/auth/cleanup.js +9 -9
  52. package/dist/auth/credential_deadline.d.ts +95 -0
  53. package/dist/auth/credential_deadline.d.ts.map +1 -0
  54. package/dist/auth/credential_deadline.js +139 -0
  55. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  56. package/dist/auth/daemon_token_middleware.js +4 -1
  57. package/dist/auth/middleware.d.ts +90 -12
  58. package/dist/auth/middleware.d.ts.map +1 -1
  59. package/dist/auth/middleware.js +115 -16
  60. package/dist/auth/request_context.d.ts +4 -2
  61. package/dist/auth/request_context.d.ts.map +1 -1
  62. package/dist/auth/request_context.js +14 -3
  63. package/dist/auth/resolved_auth.d.ts +13 -2
  64. package/dist/auth/resolved_auth.d.ts.map +1 -1
  65. package/dist/auth/resolved_auth.js +5 -4
  66. package/dist/auth/session_cookie.d.ts +4 -0
  67. package/dist/auth/session_cookie.d.ts.map +1 -1
  68. package/dist/auth/session_cookie.js +3 -2
  69. package/dist/auth/session_middleware.d.ts +1 -1
  70. package/dist/auth/session_middleware.d.ts.map +1 -1
  71. package/dist/auth/session_middleware.js +4 -1
  72. package/dist/auth/session_queries.d.ts +19 -2
  73. package/dist/auth/session_queries.d.ts.map +1 -1
  74. package/dist/auth/session_queries.js +8 -2
  75. package/dist/auth/signup_route_schema.d.ts.map +1 -1
  76. package/dist/auth/signup_route_schema.js +1 -0
  77. package/dist/hono_context.d.ts +23 -0
  78. package/dist/hono_context.d.ts.map +1 -1
  79. package/dist/hono_context.js +19 -0
  80. package/dist/http/CLAUDE.md +41 -15
  81. package/dist/http/middleware_spec.d.ts +16 -0
  82. package/dist/http/middleware_spec.d.ts.map +1 -1
  83. package/dist/http/origin.d.ts +113 -18
  84. package/dist/http/origin.d.ts.map +1 -1
  85. package/dist/http/origin.js +247 -127
  86. package/dist/http/route_spec.d.ts +21 -0
  87. package/dist/http/route_spec.d.ts.map +1 -1
  88. package/dist/http/schema_helpers.d.ts +22 -0
  89. package/dist/http/schema_helpers.d.ts.map +1 -1
  90. package/dist/http/schema_helpers.js +24 -0
  91. package/dist/http/surface.d.ts +6 -1
  92. package/dist/http/surface.d.ts.map +1 -1
  93. package/dist/http/surface.js +12 -5
  94. package/dist/realtime/sse_auth_guard.d.ts +22 -6
  95. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  96. package/dist/realtime/sse_auth_guard.js +31 -12
  97. package/dist/realtime/subscriber_registry.d.ts +10 -1
  98. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  99. package/dist/realtime/subscriber_registry.js +10 -1
  100. package/dist/server/app_server.d.ts +20 -7
  101. package/dist/server/app_server.d.ts.map +1 -1
  102. package/dist/server/app_server.js +8 -3
  103. package/dist/server/env.d.ts +2 -1
  104. package/dist/server/env.d.ts.map +1 -1
  105. package/dist/server/env.js +7 -17
  106. package/dist/server/serve_fact_route.d.ts.map +1 -1
  107. package/dist/server/serve_fact_route.js +1 -0
  108. package/dist/testing/CLAUDE.md +154 -22
  109. package/dist/testing/adversarial_headers.d.ts.map +1 -1
  110. package/dist/testing/adversarial_headers.js +13 -6
  111. package/dist/testing/connection_closer_helpers.d.ts +6 -1
  112. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  113. package/dist/testing/connection_closer_helpers.js +4 -0
  114. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  115. package/dist/testing/cross_backend/body_size.js +14 -65
  116. package/dist/testing/cross_backend/capabilities.d.ts +26 -2
  117. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  118. package/dist/testing/cross_backend/capabilities.js +9 -3
  119. package/dist/testing/cross_backend/conformance_case.d.ts +11 -3
  120. package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
  121. package/dist/testing/cross_backend/conformance_case.js +6 -0
  122. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  123. package/dist/testing/cross_backend/conformance_table.js +20 -19
  124. package/dist/testing/cross_backend/credential_deadline.d.ts +25 -0
  125. package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -0
  126. package/dist/testing/cross_backend/credential_deadline.js +185 -0
  127. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  128. package/dist/testing/cross_backend/default_backend_configs.js +13 -2
  129. package/dist/testing/cross_backend/full_spine_mount.d.ts +3 -0
  130. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  131. package/dist/testing/cross_backend/full_spine_mount.js +7 -1
  132. package/dist/testing/cross_backend/narrowed_api_token.d.ts +23 -0
  133. package/dist/testing/cross_backend/narrowed_api_token.d.ts.map +1 -0
  134. package/dist/testing/cross_backend/narrowed_api_token.js +40 -0
  135. package/dist/testing/cross_backend/origin.d.ts.map +1 -1
  136. package/dist/testing/cross_backend/origin.js +6 -26
  137. package/dist/testing/cross_backend/origin_conformance.d.ts +152 -0
  138. package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -0
  139. package/dist/testing/cross_backend/origin_conformance.js +136 -0
  140. package/dist/testing/cross_backend/origin_conformance.json +1679 -0
  141. package/dist/testing/cross_backend/origin_scope.d.ts +16 -0
  142. package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -0
  143. package/dist/testing/cross_backend/origin_scope.js +140 -0
  144. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +35 -0
  145. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -0
  146. package/dist/testing/cross_backend/role_grant_cross_helpers.js +64 -0
  147. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +8 -0
  148. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  149. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +44 -33
  150. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  151. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -4
  152. package/dist/testing/cross_backend/spine_surface_constants.d.ts +19 -0
  153. package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
  154. package/dist/testing/cross_backend/spine_surface_constants.js +22 -0
  155. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  156. package/dist/testing/cross_backend/testing_backdoor.js +20 -7
  157. package/dist/testing/cross_backend/testing_reset_actions.d.ts +180 -5
  158. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  159. package/dist/testing/cross_backend/testing_reset_actions.js +154 -5
  160. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  161. package/dist/testing/cross_backend/token_scope_surface.js +25 -21
  162. package/dist/testing/cross_backend/transport_composition.d.ts +60 -0
  163. package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -0
  164. package/dist/testing/cross_backend/transport_composition.js +364 -0
  165. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  166. package/dist/testing/cross_backend/ts_spine_backend_config.js +21 -6
  167. package/dist/testing/cross_backend/ws_role_gated.d.ts +38 -0
  168. package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -0
  169. package/dist/testing/cross_backend/ws_role_gated.js +142 -0
  170. package/dist/testing/entities.d.ts +23 -0
  171. package/dist/testing/entities.d.ts.map +1 -1
  172. package/dist/testing/entities.js +37 -0
  173. package/dist/testing/integration.d.ts.map +1 -1
  174. package/dist/testing/integration.js +3 -1
  175. package/dist/testing/middleware.d.ts +10 -0
  176. package/dist/testing/middleware.d.ts.map +1 -1
  177. package/dist/testing/middleware.js +31 -7
  178. package/dist/testing/stubs.d.ts +16 -3
  179. package/dist/testing/stubs.d.ts.map +1 -1
  180. package/dist/testing/stubs.js +20 -7
  181. package/dist/testing/surface_invariants.js +2 -2
  182. package/package.json +1 -1
@@ -33,11 +33,11 @@ time (never runtime), where a throwing guard would break `vite build`.
33
33
  - `stub_mw` — pass-through middleware (`async (_c, next) => next()`).
34
34
  - `create_stub_app_deps()` — factory: fresh `AppDeps` with no-op keyring/password/`delete_file`, a `read_secure_file` that throws ENOENT (the no-token-file state), a `create_noop_stub` DB, silent `Logger`, no-op `audit`, an empty `connection_closer`.
35
35
  - `create_test_audit_emitter()` — no-op `AuditEmitter`; `emit` / `emit_role_grant_target` no-op, `emit_pool` resolves immediately, `notify` no-op, `add_listener` throws (use `create_recording_audit_emitter` for a listener-accepting emitter), `listener_count` returns 0, `drain_inflight` resolves immediately.
36
- - `create_stub_audit_sse()` — no-op `AuditLogSse` for surface-test wiring without booting real SSE. `on_audit_event` no-op (nothing is broadcast); `registry` is a fresh `SubscriberRegistry` (live `.count` / `.close_*` for registry-state tests, isolated per call). For real SSE plumbing build via `create_audit_log_sse` against `create_test_app`.
37
- - `create_stub_api_middleware({include_daemon_token?})` — stub `MiddlewareSpec[]` matching `create_auth_middleware_specs`'s output (origin/session/request_context/bearer_auth, optional daemon_token) for surface generation without booting real auth — the same names, paths, order, and declared `errors` (`origin` declares a 403; `bearer_auth` and `daemon_token` declare none, since both soft-fail through `next()`), pinned against the real factory by `auth/middleware.mounted.test.ts`. The surface merges middleware errors into every route under the path, so a stub declaring more would widen the test surface with statuses the live server never returns. Each spec gets its own pass-through handler (not the shared `stub_mw`), so `assert_middleware_stack_mounted` tells the layers apart. See `auth/CLAUDE.md` §Middleware for the real stack.
36
+ - `create_stub_audit_sse()` — no-op `AuditLogSse` for surface-test wiring without booting real SSE. `on_audit_event` no-op (nothing is broadcast); `required_role` is `admin`; `registry` is a fresh `SubscriberRegistry` (live `.count` / `.close_*` for registry-state tests, isolated per call). For real SSE plumbing build via `create_audit_log_sse` against `create_test_app`.
37
+ - `create_stub_api_middleware({include_daemon_token?, origin_carve_outs?})` — stub `MiddlewareSpec[]` matching `create_auth_middleware_specs`'s output (origin/session/request_context/bearer_auth, optional daemon_token) for surface generation without booting real auth — the same names, paths, order, and declared `errors` (`origin` declares a 403; `bearer_auth` and `daemon_token` declare none, since both soft-fail through `next()`), pinned against the real factory by `auth/middleware.mounted.test.ts`. The surface merges middleware errors into every route under the path, so a stub declaring more would widen the test surface with statuses the live server never returns. Each spec gets its own pass-through handler (not the shared `stub_mw`), so `assert_middleware_stack_mounted` tells the layers apart. See `auth/CLAUDE.md` §Middleware for the real stack.
38
38
  - `create_stub_app_backend(options?)` — stub `AppBackend` with no database: `create_stub_app_deps()` (or `options.deps`), no migration results, a `close` that does nothing (or `options.close`). For assembling a server in tests that never reach the database.
39
39
  - `create_stub_app_server_context(session_options)` — stub `AppServerContext` over `create_stub_app_backend()`; rate limiters null, `bootstrap_status.available: false`.
40
- - `create_test_app_surface_spec(options)` — builds an `AppSurfaceSpec` mirroring `create_app_server`'s route assembly, in its order (consumer routes, then bootstrap routes, RPC endpoint routes, and `GET /api/surface`, + stub middleware + surface generation). `GET /api/surface` is listed only with `surface_route: true`, as `create_app_server` mounts it only when opted in — pass the same value to both so the surface-derived suites probe it exactly when the live server serves it. `daemon_token: true` lists the `daemon_token` layer `create_app_server` mounts when given a `daemon_token_state` (`create_test_app` always passes one, and `default_in_process_suite_options` sets it, along with `app_options.surface_route`); `testing/test_app_server.db.test.ts` pins the stub's routes (order, auth, error schemas, applicable middleware) and middleware names against `create_test_app`'s for every `surface_route` setting. Its middleware list mirrors the real one — a stub `trusted_proxy` spec at `'*'`, then the stub auth stack — so every route's `applicable_middleware` starts with `trusted_proxy` (a route outside `/api/*` now lists just `['trusted_proxy']`), as on the live server. `CreateTestAppSurfaceSpecOptions`: `session_options`, `create_route_specs`, `env_schema?`, `event_specs?`, `rpc_endpoints?`, `ws_endpoints?`, `allowed_origins?`, `transform_middleware?`, `bootstrap?`, `surface_route?`, `daemon_token?`. Throws, like `create_app_server`, on an endpoint path outside `AUTH_MIDDLEWARE_PATH` (`assert_endpoint_in_auth_scope`) and on a `transform_middleware` that moves, drops, duplicates, reorders, wraps, or replaces the stub proxy spec or a stub auth spec (`assert_middleware_stack_mounted`); the transform receives shallow copies in a fresh array, so an in-place mutation can't slip past the check. Throws on a `ws_endpoints` spec with no `allowed_origins` of its own when no `allowed_origins` option is given (there is no server list to default from). Bootstrap is opt-in (symmetric with `create_app_server` — omit to skip; pass the same value as prod to mount routes at `bootstrap.route_prefix ?? '/api/account'`). Single source of truth for attack-surface tests — track `create_app_server` wiring changes here.
40
+ - `create_test_app_surface_spec(options)` — builds an `AppSurfaceSpec` mirroring `create_app_server`'s route assembly, in its order (consumer routes, then bootstrap routes, RPC endpoint routes, and `GET /api/surface`, + stub middleware + surface generation). `GET /api/surface` is listed only with `surface_route: true`, as `create_app_server` mounts it only when opted in — pass the same value to both so the surface-derived suites probe it exactly when the live server serves it. `daemon_token: true` lists the `daemon_token` layer `create_app_server` mounts when given a `daemon_token_state` (`create_test_app` always passes one, and `default_in_process_suite_options` sets it, along with `app_options.surface_route`); `testing/test_app_server.db.test.ts` pins the stub's routes (order, auth, error schemas, applicable middleware) and middleware names against `create_test_app`'s for every `surface_route` setting. Its middleware list mirrors the real one — a stub `trusted_proxy` spec at `'*'`, then the stub auth stack — so every route's `applicable_middleware` starts with `trusted_proxy` (a route outside `/api/*` lists `['trusted_proxy', 'origin']`, since the origin check covers every path, and a route under an origin carve-out drops `origin`), as on the live server. `CreateTestAppSurfaceSpecOptions`: `session_options`, `create_route_specs`, `env_schema?`, `event_specs?`, `rpc_endpoints?`, `ws_endpoints?`, `allowed_origins?`, `origin_carve_outs?`, `transform_middleware?`, `bootstrap?`, `surface_route?`, `daemon_token?`. Throws, like `create_app_server`, on an endpoint path outside `AUTH_MIDDLEWARE_PATH` (`assert_endpoint_in_auth_scope`), on a route that reads or mints a credential outside it (`assert_credential_routes_in_auth_scope`), on an origin carve-out that overlaps that scope (`assert_origin_carve_outs_valid`), and on a `transform_middleware` that moves, drops, duplicates, reorders, wraps, or replaces the stub proxy spec or a stub auth spec (`assert_middleware_stack_mounted`); the transform receives shallow copies in a fresh array, so an in-place mutation can't slip past the check. Throws on a `ws_endpoints` spec with no `allowed_origins` of its own when no `allowed_origins` option is given (there is no server list to default from). Bootstrap is opt-in (symmetric with `create_app_server` — omit to skip; pass the same value as prod to mount routes at `bootstrap.route_prefix ?? '/api/account'`). Single source of truth for attack-surface tests — track `create_app_server` wiring changes here.
41
41
 
42
42
  Throwing stubs surface mock escape: a test that accidentally reaches into
43
43
  stub territory breaks immediately with a label-scoped error rather than
@@ -51,12 +51,15 @@ set only the fields the test cares about. `create_test_*` prefix avoids
51
51
  collisions with real `account_queries.ts` factories. Override types widen
52
52
  branded `Uuid` fields to `string` so tests pass literal ids without per-site
53
53
  casts — the factory brands internally. Exported as `TestAccountOverrides` /
54
- `TestActorOverrides` / `TestRoleGrantOverrides` / `TestAuditEventOverrides`.
54
+ `TestActorOverrides` / `TestRoleGrantOverrides` / `TestAuditEventOverrides` /
55
+ `TestValidatedApiTokenOverrides`.
55
56
 
56
57
  - `create_test_account(overrides?)` — `{id: 'acct-test', username: 'test_user', …}`
57
58
  - `create_test_actor(overrides?)` — `{id: 'actor-test', account_id: 'acct-test', …}`
58
59
  - `create_test_role_grant(overrides?)` — `{id: 'role-grant-test', actor_id: 'actor-test', role: 'admin', scope_id: null, …}`
59
60
  - `create_test_context(role_grants?)` — `{account, actor, role_grants}`; pass `[{role: 'keeper'}, {role: 'admin'}]` for multi-role.
61
+ - `create_test_raw_api_token(label?)` — a well-formed raw bearer value (`secret_fuz_token_` + `label` padded with `A` to 43 chars) that passes `is_api_token_shaped`, so a mocked-lookup test reaches token validation; distinct labels give distinct tokens.
62
+ - `create_test_validated_api_token(overrides?)` — a `ValidatedApiToken` (what `query_validate_api_token` returns) with a `full` scope; override `scope` to test a narrowed one.
60
63
  - `create_test_audit_event(overrides?)` — `{id: 'evt-test', event_type: 'login', outcome: 'success', …}`, for SSE guard / audit tests.
61
64
 
62
65
  ### `mock_fs.ts` — in-memory filesystem
@@ -106,9 +109,9 @@ mismatch.
106
109
 
107
110
  ### `connection_closer_helpers.ts` — `ConnectionCloser` test doubles
108
111
 
109
- - `create_recording_closer()` — `{closer, calls}`; every method on `closer` records `{method, id}` into `calls` and closes nothing (`close_all_sockets` records `{method: 'all', id: null}`). To see a handler's closes, add it to the backend's closer (`ctx.deps.connection_closer.add(closer)`) or build the factory's deps with it. The closes are queued post-commit, so they have run by the time a `create_test_app` response is in hand (`await_pending_effects: true` flushes both queues).
112
+ - `create_recording_closer()` — `{closer, calls}`; every method on `closer` records `{method, id}` into `calls` and closes nothing (`close_sockets_for_role` records `{method: 'role', id, role}`, `close_all_sockets` `{method: 'all', id: null}`). To see a handler's closes, add it to the backend's closer (`ctx.deps.connection_closer.add(closer)`) or build the factory's deps with it. The closes are queued post-commit, so they have run by the time a `create_test_app` response is in hand (`await_pending_effects: true` flushes both queues).
110
113
  - `assert_close_call(call, method, id)` — pins `{method, id}` on a recorded close call; handles the missing-element case.
111
- - `RecordedClose` — `{method: 'session' | 'token' | 'account', id: string} | {method: 'all', id: null}`.
114
+ - `RecordedClose` — `{method: 'session' | 'token' | 'account', id: string} | {method: 'role', id: string, role: string} | {method: 'all', id: null}`.
112
115
  - `RecordingCloser` — `{closer, calls}`.
113
116
 
114
117
  ## Database — `db.ts`
@@ -483,7 +486,7 @@ file that imports from `middleware.ts` gets these mocks globally. Pair with
483
486
  - `BearerAuthTestOptions`, `BearerAuthTestCase` — test-case table shape for the bearer auth runner.
484
487
  - `create_bearer_auth_mocks(tc)` — configures the module-level mocks per test case; returns spy references.
485
488
  - `TEST_CLIENT_IP = '127.0.0.1'` — IP set by the proxy stub in `create_bearer_auth_test_app`.
486
- - `create_bearer_auth_test_app(tc)` — Hono app with bearer middleware + echo route at `/api/test` returning `{ok, account_id, credential_type, api_token_id, request_context_set}` — the account-grain identity bearer auth writes, plus a flag for tests that pre-populate `REQUEST_CONTEXT_KEY` via `pre_context`.
489
+ - `create_bearer_auth_test_app(tc)` — Hono app with bearer middleware + echo route at `/api/test` returning `{ok, account_id, credential_type, api_token_id, token_scope, credential_deadline_in_ms, request_context_set}` (`credential_deadline_in_ms` — how far ahead `CREDENTIAL_DEADLINE_KEY` lies, `null` for none; `BearerAuthTestCase.expected_credential_deadline_ms` asserts it) — the account-grain identity bearer auth writes, plus a flag for tests that pre-populate `REQUEST_CONTEXT_KEY` via `pre_context`.
487
490
  - `describe_bearer_auth_cases(suite_name, cases)` — table-driven runner; one `test()` per case; asserts status, error, body fields, `api_token_id`, context preservation.
488
491
  - `TEST_MIDDLEWARE_PATH = '/api/test'` — path used by the echo route in the stack factory.
489
492
  - `create_test_middleware_stack_app(options?)` — real proxy + origin + bearer middleware for integration-shape testing. Echo route returns `{ok, client_ip, has_context}`.
@@ -532,7 +535,7 @@ method and assert the stream closes within 2s.
532
535
 
533
536
  `SseRouteTestSpec` per route: `{path, trigger, event_specs?, assert_closes_on_revoke?}`.
534
537
  A consumer registry closes on the revoke when its route joined it to the
535
- backend's closer (`ctx.deps.connection_closer.add(create_sse_connection_closer(registry))`)
538
+ backend's closer (`ctx.deps.connection_closer.add(create_sse_connection_closer(registry, role))`)
536
539
  or when the suite's `on_audit_event` option wires a guard (e.g. `create_sse_auth_guard`); with
537
540
  neither, the revoke assertion fails.
538
541
 
@@ -929,9 +932,10 @@ source of truth for wire-shape conformance.
929
932
  (the **gating** flags `ws` / `sse` / `cell_crud` / `cell_relations` /
930
933
  `cell_gated_create` / `account_lifecycle` / `fact_serving` / `ready` /
931
934
  `account_status` / `peer_request` /
932
- `ws_handshake_pipelining` / `ws_account_actions` / `ws_self_revocation_reply` —
933
- each has a `test_if`
934
- reader (`ws_self_revocation_reply` forks one case's assertion instead); `peer_request` (server-initiated requests — the ActionPeer
935
+ `ws_handshake_pipelining` / `ws_account_actions` / `ws_self_revocation_reply` /
936
+ `ws_role_gated` / `ws_refusal_body` — each has a `test_if`
937
+ reader (`ws_self_revocation_reply` forks one case's assertion instead, and
938
+ `ws_refusal_body` how the front-door table reads a WS refusal); `peer_request` (server-initiated requests — the ActionPeer
935
939
  `peer/ping` round-trip) is `true` for both the Rust spine and the TS spine
936
940
  (`BackendWebsocketTransport.request_connection`); `ts_default_capabilities`
937
941
  keeps it `false` like sse/ready until a backend wires the `peer/ping` HTTP +
@@ -1034,7 +1038,7 @@ error_reason?, fields?, headers?, equivalence_group?}, note?, xfail?}`. A case i
1034
1038
  resolves its `input`/`output` from the live registry (RPC) or `RouteSpec`
1035
1039
  (the 6 REST auth routes), so the case never carries a schema. `as` is the
1036
1040
  closed `ConformancePrincipal` enum (`keeper` / `daemon` / `invalid_daemon` /
1037
- `daemon_browser` / `token` / `bearer_browser` / `anonymous` /
1041
+ `daemon_browser` / `token` / `bearer_browser` / `malformed_bearer` / `scoped_token` / `anonymous` /
1038
1042
  `fresh_non_admin` / `role_holder` / `wrong_role` / `expired_session`) —
1039
1043
  fixture accessors, never inline credential minting.
1040
1044
  `invalid_daemon` sends a malformed `X-Daemon-Token` alongside the keeper
@@ -1054,8 +1058,11 @@ error_reason?, fields?, headers?, equivalence_group?}, note?, xfail?}`. A case i
1054
1058
  `bearer_browser` is the bearer twin: a _valid_ bearer api-token in a browser
1055
1059
  context (default `Origin` present), fresh jar (no session) — the bearer
1056
1060
  middleware discards it, so an authed action 401s, wire-indistinguishable from
1057
- `anonymous` (pinned by the `browser_bearer_replay` equivalence group). The
1061
+ `anonymous` (pinned by the `bearer_soft_fail` equivalence group). The
1058
1062
  `token` principal is the honored counterpart (suppresses `Origin`).
1063
+ `malformed_bearer` is the keeper's token minus its last character over a
1064
+ no-Origin, empty-jar transport — refused by the shape precheck before any
1065
+ database work, so it joins `bearer_soft_fail` too.
1059
1066
  `expired_session` is the keeper behind an expired server-side session
1060
1067
  (`fixture.mint_expired_session()`: a backdated `auth_session` row behind a
1061
1068
  still-valid signed cookie, so the DB-row expiry gate is what refuses it).
@@ -1157,7 +1164,7 @@ the seven server-initiated notifications (`received` → recipient,
1157
1164
  `role_grant_revoke` → revokee, and `role_grant_offer_supersede` → each
1158
1165
  superseded sibling's grantor on **both** the accept- and revoke-cascade paths).
1159
1166
  `describe_role_grant_offer_notification_ws_tests({setup_test, capabilities,
1160
- base_url, ws_path})` opens the affected counterparty's socket
1167
+ base_url, ws_path, ws_required_roles?})` opens the affected counterparty's socket
1161
1168
  (`create_admitted_ws_transport` — a targeted notification reaches only an
1162
1169
  admitted connection, and the handshake alone doesn't say it is one), drives the
1163
1170
  lifecycle RPC over `fixture.transport`, then
@@ -1177,8 +1184,36 @@ Sends queue on the post-commit drain, so `WsClient.wait_for` polls + waits
1177
1184
  (method + predicate filter ignores unrelated frames). Accounts are seeded with
1178
1185
  `ROLE_ADMIN` (the only admin-grantable role) so they can open an admin-gated WS
1179
1186
  where one exists (forge) and be offered the role; harmless on the auth-only
1180
- spine. Gated on `capabilities.ws`; cross-process only. fuz_app's own wiring is
1181
- `src/test/cross_backend/role_grant_offer_notification_ws.cross.test.ts`.
1187
+ spine. A backend whose `ws_path` is gated on `ROLE_ADMIN` declares it in
1188
+ `ws_required_roles`: the revoke case then revokes the revokee's only admin
1189
+ grant and expects its socket to close with `WS_CLOSE_PERMISSION_REVOKED` (the
1190
+ notification may or may not land first, and is held to the flat shape when it
1191
+ does), where an ungated socket expects the frame. The offer/accept fixtures are `create_pending_role_grant_offer` /
1192
+ `create_active_role_grant` (`cross_backend/role_grant_cross_helpers.ts`).
1193
+ Gated on `capabilities.ws`; cross-process only. fuz_app's own wiring is
1194
+ `src/test/cross_backend/role_grant_offer_notification_ws.cross.test.ts`, and
1195
+ `role_grant_offer_notification_ws_gated.cross.test.ts` runs it again over the
1196
+ spine binaries' role-gated endpoint with `ws_required_roles` declared.
1197
+
1198
+ ### `cross_backend/ws_role_gated.ts` — `describe_ws_role_gated_cross_tests`
1199
+
1200
+ Real-upgrade parity for a **role-gated WebSocket losing its role**: a revoked
1201
+ global grant of a role an endpoint's `required_roles` lists closes the account's
1202
+ sockets on that endpoint with `WS_CLOSE_PERMISSION_REVOKED` (and its reason),
1203
+ while the account's ungated socket on the same transport stays open,
1204
+ dispatching, and receiving the account's pushes, and a re-upgrade is refused at
1205
+ the gate (403); a scoped revoke of a listed role closes nothing — the case
1206
+ first opens the gated socket on a global grant of `scoped_role` alone, which
1207
+ proves the endpoint lists it. That a closed registration is sent nothing more
1208
+ is beyond what a client sees once it has read the close frame; each spine's
1209
+ transport tests pin it.
1210
+ `describe_ws_role_gated_cross_tests({setup_test, capabilities, base_url,
1211
+ ws_path, gated_ws_path, gated_roles, scoped_role, rpc_path?})` — `gated_roles`
1212
+ must hold `ROLE_ADMIN` and `scoped_role` (a role an admin can grant scoped).
1213
+ fuz_app's spine binaries mount `SPINE_ROLE_GATED_WS_PATH` gated on
1214
+ `SPINE_ROLE_GATED_WS_ROLES` (`admin` + `participant`). Gated on
1215
+ `capabilities.ws` and `capabilities.ws_role_gated`; cross-process only.
1216
+ fuz_app's own wiring is `src/test/cross_backend/ws_role_gated.cross.test.ts`.
1182
1217
 
1183
1218
  ### `cross_backend/peer_ping_ws.ts` — `describe_peer_ping_ws_tests`
1184
1219
 
@@ -1565,6 +1600,27 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
1565
1600
  payload-expiry tests never reached. Daemon-token-gated like its siblings; the
1566
1601
  Rust mirror is `fuz_testing::create_testing_mint_session_action_spec`.
1567
1602
 
1603
+ Also in the module and **not** bundled: `_testing_origin_check`
1604
+ (`create_testing_origin_check_action()`, `run_testing_origin_check(input)`,
1605
+ `TestingOriginCheckInput` / `TestingOriginCheckOutput`) — answers the origin
1606
+ allowlist's parse and request check for one `{patterns, origin}` pair:
1607
+ `{ok: true, allowed}` or `{ok: false, errors: [{index?, kind}]}`. Pure, no
1608
+ database. Kept out of the bundle so a consumer whose Rust binary doesn't
1609
+ mount the mirror (`fuz_testing::create_testing_origin_check_action_spec`)
1610
+ keeps an exact action-manifest gate; the spine binary appends it in
1611
+ `build_full_spine_rpc_actions`.
1612
+
1613
+ Also **not** bundled: `_testing_shorten_credential_expiry`
1614
+ (`create_testing_shorten_credential_expiry_action()`) — pulls an account's
1615
+ `auth_session` and `api_token` expiries in to `seconds` (1–60) from now,
1616
+ shorten-only (`LEAST`, an eternal token gaining one), answering
1617
+ `{sessions, api_tokens}` counts. Applied before a connection opens, since
1618
+ the deadline is read when the credential resolves. Backs
1619
+ `describe_credential_deadline_cross_tests`; appended in
1620
+ `build_full_spine_rpc_actions` for the same manifest reason as
1621
+ `_testing_origin_check`. The Rust mirror is
1622
+ `fuz_testing::create_testing_shorten_credential_expiry_action_spec`.
1623
+
1568
1624
  Also in the module — but **not** bundled by `create_testing_actions` —
1569
1625
  `_testing_action_manifest` (+ `create_testing_action_manifest_action(mounted)`):
1570
1626
  dumps the live RPC registry as a normalized `ActionManifest` for the
@@ -1611,19 +1667,95 @@ serve a body-parity gate (its per-spine coverage stays in the two surface
1611
1667
  censuses). Cross-process only; fuz_app's own wiring is
1612
1668
  `src/test/cross_backend/token_scope_surface.cross.test.ts`.
1613
1669
 
1670
+ ### Front-door contract — `cross_backend/transport_composition.ts`
1671
+
1672
+ `describe_transport_composition_cross_tests({setup_test, capabilities, base_url,
1673
+ rpc_path?, origin?, rest_path?, password_path?, ws_path?, sse_path?})` — the per-transport
1674
+ composition table: what each transport answers before per-action dispatch,
1675
+ when one request trips several refusals. Rows are REST `POST` (session-only
1676
+ `/api/account/logout`), RPC `POST`, RPC `GET`, the WS upgrade (gated on
1677
+ `capabilities.ws`) and the audit stream (`capabilities.sse`); columns are a
1678
+ foreign `Origin` with the keeper's session (→ 403 `forbidden_origin`), an
1679
+ over-cap declared length with a foreign `Origin` and no credential (→ 413
1680
+ `payload_too_large`, body-carrying rows only), no credential (→ 401
1681
+ `authentication_required`), and a narrowed bearer with no `Origin` (→ 403
1682
+ `token_scope_required` naming `rpc:<method>` on RPC and `surface:ws_upgrade` on
1683
+ the upgrade; 403 `credential_type_required` on the session-only REST route and
1684
+ audit stream); a `/password` cell pins that route's 401 beside `/logout`'s.
1685
+ Each case asserts the status and the body — flat `{error}` or the JSON-RPC
1686
+ envelope's `error.data`. A WS refusal's body is required where the backend
1687
+ declares `capabilities.ws_refusal_body`; where it doesn't (Node's
1688
+ `@hono/node-ws` drops the body), an empty body passes on its status and a sent
1689
+ one is still asserted. The table
1690
+ is the exported `transport_composition_cases` data. Given `base_url`, every
1691
+ probe is written to the socket (`raw_http_exchange`) and an over-cap request
1692
+ sends its head alone; with `base_url: null` the probes go through the
1693
+ fixture's transport and the WS rows skip. Not in the standard bundle. fuz_app's
1694
+ own wiring is `src/test/cross_backend/transport_composition.cross.test.ts` and
1695
+ the in-process `src/test/auth/transport_composition_parity.db.test.ts`.
1696
+
1697
+ `cross_backend/narrowed_api_token.ts` — `mint_narrowed_api_token({app,
1698
+ rpc_path, session_headers, methods, name?})` mints a method-scoped api token
1699
+ through the production `account_token_create` path and returns the raw bearer
1700
+ (throws when the mint is refused). Shared by the front-door table, the
1701
+ token-scope surface suite, and the conformance table's `scoped_token`
1702
+ principal.
1703
+
1704
+ ### Credential deadline parity — `cross_backend/credential_deadline.ts`
1705
+
1706
+ `describe_credential_deadline_cross_tests({setup_test, capabilities, base_url,
1707
+ ws_path, sse_path?, rpc_path?})` — a connection closes when the credential it
1708
+ opened on expires: each case pulls a fresh account's credential expiries in to
1709
+ two seconds with `_testing_shorten_credential_expiry` before the open, then
1710
+ checks the connection still dispatches short of the deadline and closes at it.
1711
+ A WebSocket on a session and one on an API token (`capabilities.ws`) close with
1712
+ `WS_CLOSE_SESSION_REVOKED` and `WS_CLOSE_CREDENTIAL_EXPIRED_REASON`; an audit
1713
+ stream on a session (`capabilities.sse`) ends; and a second seam call asking
1714
+ for a later expiry moves nothing (the seam only shortens). API token × audit
1715
+ stream is not reachable over the wire — the stock stream is session-only — so
1716
+ the Rust spine pins it on a widened gate and the TS twin shares one deadline
1717
+ wait across both transports. Cross-process only; fuz_app's own wiring is
1718
+ `src/test/cross_backend/credential_deadline.cross.test.ts`.
1719
+
1720
+ ### Origin check scope parity — `cross_backend/origin_scope.ts`
1721
+
1722
+ `describe_origin_scope_cross_tests({setup_test, capabilities, probe_path?})` —
1723
+ the origin check covers paths outside `/api`: on `GET /ready` (`probe_path`), a
1724
+ foreign `Origin` → 403 `forbidden_origin`, an absent or allowed one → 200. Gated
1725
+ on `capabilities.ready`. Runs both legs (the in-process
1726
+ `cross_backend/origin_scope_parity.db.test.ts` + the cross-process
1727
+ `origin_scope.cross.test.ts`).
1728
+
1614
1729
  ### Origin verification parity — `cross_backend/origin.ts`
1615
1730
 
1616
1731
  `describe_origin_cross_tests({setup_test, rpc_path?})` — the
1617
- imperative Origin-verification suite: disallowed `Origin` → 403 `forbidden_origin` (refused
1618
- before dispatch), absent `Origin` → request passes (non-browser direct access).
1619
- Imperative (not a conformance-table row) because origin rejection is
1620
- middleware-level flat-REST, not the JSON-RPC envelope the table runner expects,
1621
- and absent-Origin needs `fresh_transport({origin: null})`. Runs both legs (the
1732
+ imperative Origin-verification suite: absent `Origin` → request passes
1733
+ (non-browser direct access); the disallowed-`Origin` 403 is the front-door
1734
+ table's. Imperative (not a conformance-table row) because absent-Origin needs
1735
+ `fresh_transport({origin: null})`. Runs both legs (the
1622
1736
  in-process `auth/origin_parity.db.test.ts` + the cross-process
1623
1737
  `origin.cross.test.ts`). The promotion surfaced a twin-impl divergence — the
1624
1738
  Rust spine returned a plain-text body — now converged to the canonical TS
1625
1739
  `{error: "forbidden_origin"}` via `fuz_http::forbidden_origin_response()`.
1626
1740
 
1741
+ ### Origin allowlist conformance — `cross_backend/origin_conformance.ts`
1742
+
1743
+ `cross_backend/origin_conformance.json` is the shared table both spines' origin
1744
+ allowlist must satisfy: **parse** rows `{raw, ok: true}` or `{raw, errors:
1745
+ [{index?, kind}]}` (every refusal, 1-based position, `AllowedOriginsErrorKind`)
1746
+ and **match** rows `{patterns, origin, allowed}` (`origin: null` — no
1747
+ `Origin` — passes). A row may carry a `note`. `load_origin_conformance()` reads
1748
+ and validates it (`OriginConformance`), `to_origin_conformance_checks(table)`
1749
+ flattens it into `_testing_origin_check` inputs with their expected answers, and
1750
+ `assert_origin_conformance(checks, answers, backend)` fails once listing every
1751
+ row that disagrees. `describe_origin_conformance_cross_tests({setup_test,
1752
+ rpc_path?})` sends every row to a backend's `_testing_origin_check` over the
1753
+ keeper's daemon-token channel; the TS unit leg runs the same rows through
1754
+ `run_testing_origin_check` (`src/test/http/origin.conformance.test.ts`), so the
1755
+ Rust spine is held to the table without reading the file. Ungated; edit the
1756
+ table by hand — it is the statement of the grammar, not generated output.
1757
+ fuz_app's own wiring is `src/test/cross_backend/origin_conformance.cross.test.ts`.
1758
+
1627
1759
  ### CORS simple-request parity — `cross_backend/simple_request.ts`
1628
1760
 
1629
1761
  `describe_simple_request_cross_tests({setup_test, rpc_path?, login_path?, logout_path?})`
@@ -1935,7 +2067,7 @@ re-roll the serve / daemon-info / WS-attach / drain boilerplate:
1935
2067
  - `testing/cross_backend/full_spine_mount.ts` — `build_full_spine_rpc_actions(deps, options)` — the **full** live RPC mount: the declared bundle **plus** the off-declared-surface families the binary live-mounts (`_testing_*` backdoors, the cell verb set, the opt-in `actor_lookup` / `actor_search` resolvers). `build_full_spine_mount(deps, options)` builds it once for both of the binary's endpoints, returning `FullSpineMount` (`{rpc_actions, ws_actions}`): the WS list is `protocol_actions` plus every RPC action not already among them, twinning the Rust `testing_spine_stub`, which serves one registry on RPC and WS (the one difference is `cancel` — a registered protocol action on TS, owned by the read loop on Rust). Single-sources the mount so the binary and the `spine_method_coverage` reconciliation test build the same list.
1936
2068
  - `testing/cross_backend/ts_spine_backend_config.ts` — `ts_spine_node_backend_config()` / `ts_spine_deno_backend_config()` / `ts_spine_bun_backend_config()` presets (in-memory pglet, no external infra), the TS analog of `rust_spine_stub_backend_config()`.
1937
2069
 
1938
- fuz_app's own binary wiring (`src/test/cross_backend/testing_spine_server{,_node,_deno,_bun}.ts`) is the worked example: ~one `build_app` over `create_app_backend` + `create_app_server` (`rate_limiters: 'disabled_for_testing'` plus whichever login / action limiters its env toggles built, `build_full_spine_mount`'s two lists on one `rpc_endpoints` entry and one `ws_endpoints` entry, whose `transport` is also the role-grant-offer `notification_sender`), reusing `default_spine_surface`. `src/test/cross_backend/testing_spine_server.db.test.ts` builds it with the Node adapter in-process and asserts its surface lists `/api/ws` and passes the surface invariants. The `_node`/`_deno`/`_bun` entries differ only in which adapter they wire — `build_spine_app` is runtime-agnostic. It leaves `create_app_server`'s `auth_cleanup` off, as the Rust `testing_spine_stub` leaves its twin unscheduled: a background pass would delete rows and write audit rows under a running suite. A consumer's cross-process test binary may schedule it when nothing its suites seed is already expired (the conformance table's `expired_session` principal seeds a backdated session row); an in-process harness leaves it off.
2070
+ fuz_app's own binary wiring (`src/test/cross_backend/testing_spine_server{,_node,_deno,_bun}.ts`) is the worked example: ~one `build_app` over `create_app_backend` + `create_app_server` (`rate_limiters: 'disabled_for_testing'` plus whichever login / action limiters its env toggles built, `build_full_spine_mount`'s two lists on one `rpc_endpoints` entry and two `ws_endpoints` entries — `/api/ws` and the role-gated `SPINE_ROLE_GATED_WS_PATH` — sharing one `transport`, which is also the role-grant-offer `notification_sender`), reusing `default_spine_surface`. `src/test/cross_backend/testing_spine_server.db.test.ts` builds it with the Node adapter in-process and asserts its surface lists both WS endpoints (the gated one with its `required_roles`) and passes the surface invariants. The `_node`/`_deno`/`_bun` entries differ only in which adapter they wire — `build_spine_app` is runtime-agnostic. It leaves `create_app_server`'s `auth_cleanup` off, as the Rust `testing_spine_stub` leaves its twin unscheduled: a background pass would delete rows and write audit rows under a running suite. A consumer's cross-process test binary may schedule it when nothing its suites seed is already expired (the conformance table's `expired_session` principal seeds a backdated session row); an in-process harness leaves it off.
1939
2071
 
1940
2072
  ### Live-method coverage reconciliation — `method_coverage.ts`
1941
2073
 
@@ -1 +1 @@
1
- {"version":3,"file":"adversarial_headers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/adversarial_headers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAY7B,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAG7B,OAAO,EAGN,KAAK,0BAA0B,EAC/B,MAAM,iBAAiB,CAAC;AAIzB,+DAA+D;AAC/D,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+GAA+G;IAC/G,qBAAqB,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;IAClC,qGAAqG;IACrG,oBAAoB,EAAE,QAAQ,GAAG,YAAY,CAAC;CAC9C;AAID;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iCAAiC,GAC7C,gBAAgB,MAAM,KACpB,KAAK,CAAC,qBAAqB,CAiE7B,CAAC;AAIF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qCAAqC,GACjD,YAAY,MAAM,EAClB,SAAS,0BAA0B,EACnC,gBAAgB,MAAM,EACtB,cAAc,KAAK,CAAC,qBAAqB,CAAC,KACxC,IAkCF,CAAC"}
1
+ {"version":3,"file":"adversarial_headers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/adversarial_headers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAY7B,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAG7B,OAAO,EAGN,KAAK,0BAA0B,EAC/B,MAAM,iBAAiB,CAAC;AAYzB,+DAA+D;AAC/D,MAAM,WAAW,qBAAqB;IACrC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,eAAe,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,+GAA+G;IAC/G,qBAAqB,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;IAClC,qGAAqG;IACrG,oBAAoB,EAAE,QAAQ,GAAG,YAAY,CAAC;CAC9C;AAID;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iCAAiC,GAC7C,gBAAgB,MAAM,KACpB,KAAK,CAAC,qBAAqB,CAiE7B,CAAC;AAIF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qCAAqC,GACjD,YAAY,MAAM,EAClB,SAAS,0BAA0B,EACnC,gBAAgB,MAAM,EACtB,cAAc,KAAK,CAAC,qBAAqB,CAAC,KACxC,IAkCF,CAAC"}
@@ -10,6 +10,13 @@ import './assert_dev_env.js';
10
10
  import { test, assert, describe } from 'vitest';
11
11
  import { ApiError, ERROR_FORBIDDEN_ORIGIN } from '../http/error_schemas.js';
12
12
  import { create_test_middleware_stack_app, TEST_MIDDLEWARE_PATH } from './middleware.js';
13
+ import { create_test_raw_api_token } from './entities.js';
14
+ /**
15
+ * A well-formed bearer value — it passes the `is_api_token_shaped` precheck,
16
+ * so a `not_called` case proves an earlier layer short-circuited, not that the
17
+ * token's shape was refused.
18
+ */
19
+ const ADVERSARIAL_BEARER_TOKEN = create_test_raw_api_token('adversarial');
13
20
  // --- Standard adversarial header cases ---
14
21
  /**
15
22
  * 7 standard adversarial header cases applicable to any middleware stack.
@@ -26,7 +33,7 @@ export const create_standard_adversarial_cases = (allowed_origin) => [
26
33
  {
27
34
  name: 'bearer token with Origin header is rejected before token validation',
28
35
  headers: {
29
- Authorization: 'Bearer secret_fuz_token_test',
36
+ Authorization: `Bearer ${ADVERSARIAL_BEARER_TOKEN}`,
30
37
  Origin: 'https://attacker.com'
31
38
  },
32
39
  expected_status: 403,
@@ -36,7 +43,7 @@ export const create_standard_adversarial_cases = (allowed_origin) => [
36
43
  {
37
44
  name: 'bearer token with allowed Origin — bearer silently discarded (browser context)',
38
45
  headers: {
39
- Authorization: 'Bearer secret_fuz_token_test',
46
+ Authorization: `Bearer ${ADVERSARIAL_BEARER_TOKEN}`,
40
47
  Origin: allowed_origin
41
48
  },
42
49
  expected_status: 200,
@@ -51,7 +58,7 @@ export const create_standard_adversarial_cases = (allowed_origin) => [
51
58
  {
52
59
  name: 'empty Origin header is rejected by origin middleware before bearer auth (defense-in-depth)',
53
60
  headers: {
54
- Authorization: 'Bearer secret_fuz_token_test',
61
+ Authorization: `Bearer ${ADVERSARIAL_BEARER_TOKEN}`,
55
62
  Origin: ''
56
63
  },
57
64
  expected_status: 403,
@@ -61,7 +68,7 @@ export const create_standard_adversarial_cases = (allowed_origin) => [
61
68
  {
62
69
  name: 'lowercase bearer scheme is recognized (case-insensitive per RFC 7235), soft-fails',
63
70
  headers: {
64
- Authorization: 'bearer secret_fuz_token_test'
71
+ Authorization: `bearer ${ADVERSARIAL_BEARER_TOKEN}`
65
72
  },
66
73
  expected_status: 200,
67
74
  validate_expectation: 'called'
@@ -72,7 +79,7 @@ export const create_standard_adversarial_cases = (allowed_origin) => [
72
79
  // treats Referer presence as a browser-context indicator and discards
73
80
  // the token, so the request reaches the route as unauthenticated.
74
81
  headers: {
75
- Authorization: 'Bearer secret_fuz_token_test',
82
+ Authorization: `Bearer ${ADVERSARIAL_BEARER_TOKEN}`,
76
83
  Referer: 'https://attacker.com/page'
77
84
  },
78
85
  expected_status: 200,
@@ -81,7 +88,7 @@ export const create_standard_adversarial_cases = (allowed_origin) => [
81
88
  {
82
89
  name: 'bearer token with allowed Referer (no Origin) — bearer silently discarded (browser context)',
83
90
  headers: {
84
- Authorization: 'Bearer secret_fuz_token_test',
91
+ Authorization: `Bearer ${ADVERSARIAL_BEARER_TOKEN}`,
85
92
  Referer: `${allowed_origin}/page`
86
93
  },
87
94
  expected_status: 200,
@@ -2,11 +2,16 @@ import './assert_dev_env.js';
2
2
  import type { ConnectionCloser } from '../actions/connection_closer.js';
3
3
  /**
4
4
  * Record of a single `ConnectionCloser` method invocation — a credential
5
- * close carries the id it was given, `close_all_sockets` (`'all'`) none.
5
+ * close carries the id it was given, a role close (`'role'`) the account id
6
+ * and the role, `close_all_sockets` (`'all'`) none.
6
7
  */
7
8
  export type RecordedClose = {
8
9
  method: 'session' | 'token' | 'account';
9
10
  id: string;
11
+ } | {
12
+ method: 'role';
13
+ id: string;
14
+ role: string;
10
15
  } | {
11
16
  method: 'all';
12
17
  id: null;
@@ -1 +1 @@
1
- {"version":3,"file":"connection_closer_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/connection_closer_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAI7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAExE;;;GAGG;AACH,MAAM,MAAM,aAAa,GACxB;IAAE,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC;AAEvF,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,QAAO,eAqB1C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,aAAa,GAAG,SAAS,EAC/B,QAAQ,aAAa,CAAC,QAAQ,CAAC,EAC/B,IAAI,MAAM,GAAG,IAAI,KACf,IAIF,CAAC"}
1
+ {"version":3,"file":"connection_closer_helpers.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/connection_closer_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAI7B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAExE;;;;GAIG;AACH,MAAM,MAAM,aAAa,GACtB;IAAE,MAAM,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,MAAM,EAAE,KAAK,CAAC;IAAC,EAAE,EAAE,IAAI,CAAA;CAAE,CAAC;AAE/B,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,gBAAgB,CAAC;IACzB,KAAK,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,uBAAuB,QAAO,eAyB1C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,GAC7B,MAAM,aAAa,GAAG,SAAS,EAC/B,QAAQ,aAAa,CAAC,QAAQ,CAAC,EAC/B,IAAI,MAAM,GAAG,IAAI,KACf,IAIF,CAAC"}
@@ -25,6 +25,10 @@ export const create_recording_closer = () => {
25
25
  calls.push({ method: 'account', id });
26
26
  return 1;
27
27
  },
28
+ close_sockets_for_role: (id, role) => {
29
+ calls.push({ method: 'role', id, role });
30
+ return 1;
31
+ },
28
32
  close_all_sockets: () => {
29
33
  calls.push({ method: 'all', id: null });
30
34
  return 1;
@@ -1 +1 @@
1
- {"version":3,"file":"body_size.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA8F9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AAGxE;;;;;GAKG;AACH,MAAM,WAAW,wBAAyB,SAAQ,wBAAwB;IACzE;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AA2HD,eAAO,MAAM,8BAA8B,GAAI,SAAS,wBAAwB,KAAG,IAyLlF,CAAC"}
1
+ {"version":3,"file":"body_size.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/body_size.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA6F9B,OAAO,KAAK,EAAE,wBAAwB,EAAe,MAAM,YAAY,CAAC;AAGxE;;;;;GAKG;AACH,MAAM,WAAW,wBAAyB,SAAQ,wBAAwB;IACzE;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAwHD,eAAO,MAAM,8BAA8B,GAAI,SAAS,wBAAwB,KAAG,IA+HlF,CAAC"}
@@ -9,8 +9,8 @@ import '../assert_dev_env.js';
9
9
  * `Content-Length` over the cap is refused on the header, *before* auth,
10
10
  * origin, or dispatch run (middleware step 4). Each impl unit-tests this in
11
11
  * isolation; this suite pins the cross-impl agreement on the status + body
12
- * shape, on the exact `>` cap boundary (not an off-by-one divergence), and on
13
- * the cap answering ahead of the origin check and of auth. The cases:
12
+ * shape, and on the exact `>` cap boundary (not an off-by-one divergence). The
13
+ * cases:
14
14
  *
15
15
  * - **over-limit POST (cap + 1 byte) → 413** `payload_too_large`, refused
16
16
  * before any handler runs (the limit fires ahead of origin verification + the
@@ -25,11 +25,10 @@ import '../assert_dev_env.js';
25
25
  * - **under-limit POST (small) → 200** — a small, well-formed authenticated
26
26
  * `account_verify` envelope sails through to a successful handler response,
27
27
  * the positive control that the route works for normal traffic.
28
- * - **over-limit POST with a disallowed `Origin` → 413**, not the origin
29
- * check's 403, and **over-limit POST with no credentials → 413**, not 401:
30
- * the cap answers a declared length ahead of both. The control — the same
31
- * disallowed `Origin` on an under-limit body → 403 `forbidden_origin` —
32
- * shows the origin check still runs.
28
+ * - The cap answering a declared length ahead of the origin check and auth —
29
+ * over-cap with a disallowed `Origin` and no credential → 413 — is pinned per
30
+ * transport by the front-door table (`transport_composition.ts`), beside the
31
+ * under-cap 403 that shows the origin check still runs.
33
32
  * - **over-limit chunked POST (no `Content-Length`), authenticated → 413** —
34
33
  * a body with no declared length is refused by the bytes that arrive.
35
34
  *
@@ -83,7 +82,7 @@ import '../assert_dev_env.js';
83
82
  */
84
83
  import { describe, test, assert } from 'vitest';
85
84
  import { account_verify_action_spec } from '../../auth/account_action_specs.js';
86
- import { ERROR_FORBIDDEN_ORIGIN, ERROR_PAYLOAD_TOO_LARGE } from '../../http/error_schemas.js';
85
+ import { ERROR_PAYLOAD_TOO_LARGE } from '../../http/error_schemas.js';
87
86
  import { raw_http_exchange, raw_http_parse_response } from '../transports/raw_http.js';
88
87
  import { SPINE_RPC_PATH } from './spine_surface_constants.js';
89
88
  /**
@@ -111,8 +110,6 @@ const sized_envelope = (id, target_bytes) => {
111
110
  };
112
111
  /** A small, well-formed nullary `account_verify` body — well under the cap. */
113
112
  const small_envelope = (id) => JSON.stringify({ jsonrpc: '2.0', method: account_verify_action_spec.method, id });
114
- /** An `Origin` no backend's allowlist admits. */
115
- const DISALLOWED_ORIGIN = 'http://evil.com';
116
113
  /** Chunk size of the streamed over-cap body. */
117
114
  const STREAM_CHUNK_BYTES = 64 * 1024;
118
115
  /**
@@ -191,19 +188,15 @@ export const describe_body_size_cross_tests = (options) => {
191
188
  body
192
189
  });
193
190
  /**
194
- * POST with a declared `Content-Length` one byte over the cap. Over a
195
- * socket only the head is sent; in-process, the whole body.
196
- *
197
- * `headers` carries the credential and any `Origin`, which defaults to the
198
- * backend's own. `anonymous` picks the cookie-jar-free transport in-process;
199
- * a raw head carries only what `headers` names either way.
191
+ * POST as the keeper, with the backend's own `Origin` and a declared
192
+ * `Content-Length` one byte over the cap. Over a socket only the head is
193
+ * sent; in-process, the whole body.
200
194
  */
201
- const post_over_limit_declared = async (fixture, request) => {
202
- const { id, headers, anonymous } = request;
195
+ const post_over_limit_declared = async (fixture, id) => {
196
+ const headers = fixture.create_session_headers();
203
197
  const content_length = BODY_LIMIT_DEFAULT_BYTES + 1;
204
198
  if (base_url === null) {
205
- const transport = anonymous ? fixture.fresh_transport() : fixture.transport;
206
- return to_sized_response(await transport(rpc_path, {
199
+ return to_sized_response(await fixture.transport(rpc_path, {
207
200
  method: 'POST',
208
201
  headers: { ...headers, 'content-type': 'application/json' },
209
202
  body: sized_envelope(id, content_length)
@@ -258,11 +251,7 @@ export const describe_body_size_cross_tests = (options) => {
258
251
  // Keeper session cookie + the default allowed Origin, so neither auth
259
252
  // nor origin is the cause — only the size. One byte over the cap: both
260
253
  // impls reject on a strict `>`.
261
- const res = await post_over_limit_declared(fixture, {
262
- id: 'over-limit',
263
- headers: fixture.create_session_headers(),
264
- anonymous: false
265
- });
254
+ const res = await post_over_limit_declared(fixture, 'over-limit');
266
255
  assert_payload_too_large(res, 'a body one byte over the cap must be rejected with 413');
267
256
  });
268
257
  test('at-limit POST (exactly the cap) → not size-rejected', async () => {
@@ -281,46 +270,6 @@ export const describe_body_size_cross_tests = (options) => {
281
270
  const res = await post(fixture, small_envelope('under-limit'));
282
271
  assert.strictEqual(res.status, 200, 'an under-limit authenticated request must pass');
283
272
  });
284
- test('over-limit POST with a disallowed Origin → 413, not 403', async () => {
285
- const fixture = await setup_test();
286
- // Keeper session + a rogue Origin: the origin check would answer 403,
287
- // and the cap, which reads only the declared length, answers first.
288
- const res = await post_over_limit_declared(fixture, {
289
- id: 'over-limit-evil-origin',
290
- headers: { ...fixture.create_session_headers(), origin: DISALLOWED_ORIGIN },
291
- anonymous: false
292
- });
293
- assert_payload_too_large(res, 'a declared length over the cap must answer 413 ahead of the origin check');
294
- });
295
- test('under-limit POST with a disallowed Origin → 403 forbidden_origin', async () => {
296
- const fixture = await setup_test();
297
- // The control for the case above: the same request minus the size is
298
- // refused by the origin check, so the 413 there is the cap answering
299
- // first, not the origin check being absent.
300
- const res = await fixture.transport(rpc_path, {
301
- method: 'POST',
302
- headers: {
303
- ...fixture.create_session_headers(),
304
- origin: DISALLOWED_ORIGIN,
305
- 'content-type': 'application/json'
306
- },
307
- body: small_envelope('under-limit-evil-origin')
308
- });
309
- assert.strictEqual(res.status, 403, 'a disallowed Origin on an under-limit body is refused');
310
- const body = (await res.json().catch(() => undefined));
311
- assert.strictEqual(body?.error, ERROR_FORBIDDEN_ORIGIN);
312
- });
313
- test('over-limit POST with no credentials → 413, not 401', async () => {
314
- const fixture = await setup_test();
315
- // No cookie, no token: the dispatcher would answer 401, and the cap
316
- // answers first.
317
- const res = await post_over_limit_declared(fixture, {
318
- id: 'over-limit-anonymous',
319
- headers: {},
320
- anonymous: true
321
- });
322
- assert_payload_too_large(res, 'a declared length over the cap must answer 413 ahead of auth');
323
- });
324
273
  test('over-limit chunked POST (no Content-Length), authenticated → 413', async () => {
325
274
  const fixture = await setup_test();
326
275
  // Keeper session + the default allowed Origin, so nothing but the size
@@ -208,6 +208,30 @@ export interface BackendCapabilities {
208
208
  * mount it.
209
209
  */
210
210
  readonly cell_gated_create: boolean;
211
+ /**
212
+ * The backend mounts a second, role-gated WebSocket endpoint at
213
+ * `SPINE_ROLE_GATED_WS_PATH`, gated on `SPINE_ROLE_GATED_WS_ROLES`, beside
214
+ * its ungated `ws_path`. Gates `describe_ws_role_gated_cross_tests`: a
215
+ * revoked global grant of a listed role closes the account's gated socket
216
+ * with `WS_CLOSE_PERMISSION_REVOKED`, its ungated socket stays open and
217
+ * still receives the account's pushes, a re-upgrade is refused at the gate,
218
+ * and a scoped revoke closes nothing. `true` only on the reference spine binaries that
219
+ * mount that endpoint (the TS spine binary + the Rust `testing_spine_stub`).
220
+ */
221
+ readonly ws_role_gated: boolean;
222
+ /**
223
+ * A refused WebSocket upgrade carries its refusal body — the flat
224
+ * `{error, ...}` the same refusal answers over plain HTTP. Not a gate: it
225
+ * forks how `describe_transport_composition_cross_tests` reads its WS rows.
226
+ * `true`, an empty refusal body fails the case; `false`, an empty body is
227
+ * allowed (the case pins the status alone) and a non-empty one is still
228
+ * asserted.
229
+ *
230
+ * `false` on Node: `@hono/node-ws` writes a refused upgrade to the raw
231
+ * socket with `Content-Length: 0`, dropping the response body. `true` on
232
+ * Deno, Bun, and the Rust spine, which send the response as built.
233
+ */
234
+ readonly ws_refusal_body: boolean;
211
235
  }
212
236
  /**
213
237
  * Capability declarations for the in-process Hono transport. Nearly every
@@ -218,8 +242,8 @@ export interface BackendCapabilities {
218
242
  * `create_ws_transport`, so the in-process driver never runs it — the
219
243
  * transport itself supports server-initiated requests),
220
244
  * `ws_handshake_pipelining` (raw bytes on a real socket),
221
- * `ws_account_actions` (a real socket's reply-then-close ordering), and
222
- * `cell_gated_create`. Cross-process consumers declare each flag explicitly
245
+ * `ws_account_actions` (a real socket's reply-then-close ordering),
246
+ * `cell_gated_create`, and `ws_role_gated`. Cross-process consumers declare each flag explicitly
223
247
  * per backend.
224
248
  */
225
249
  export declare const in_process_capabilities: BackendCapabilities;
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA2C9B;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC1C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,wBAAwB,EAAE,OAAO,CAAC;IAC3C;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;CACpC;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBAqBpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/capabilities.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA2C9B;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,iBAAiB;IACjC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;OAIG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACnC;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB;;;;;;;OAOG;IACH,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B;;;;;;;;;;OAUG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;OAQG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB;;;;;;;;OAQG;IACH,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC;;;;;;;;;;;;;;;;;;OAkBG;IACH,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;IAC/B;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC1C;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,kBAAkB,EAAE,OAAO,CAAC;IACrC;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,wBAAwB,EAAE,OAAO,CAAC;IAC3C;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,iBAAiB,EAAE,OAAO,CAAC;IACpC;;;;;;;;;OASG;IACH,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;CAClC;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,EAAE,mBA2BpC,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,EAAE,iBAInC,CAAC;AAEH;;;;;;;;GAQG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,OAAO,EAAE,MAAM,MAAM,EAAE,IAAI,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAG,IAMrF,CAAC"}