@fuzdev/fuz_app 0.140.0 → 0.142.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 (311) hide show
  1. package/dist/actions/CLAUDE.md +72 -22
  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 +44 -4
  6. package/dist/actions/connection_closer.d.ts.map +1 -1
  7. package/dist/actions/connection_closer.js +33 -3
  8. package/dist/actions/perform_action.d.ts +10 -0
  9. package/dist/actions/perform_action.d.ts.map +1 -1
  10. package/dist/actions/perform_action.js +23 -9
  11. package/dist/actions/register_action_ws.d.ts +42 -6
  12. package/dist/actions/register_action_ws.d.ts.map +1 -1
  13. package/dist/actions/register_action_ws.js +87 -19
  14. package/dist/actions/register_ws_endpoint.d.ts +15 -6
  15. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  16. package/dist/actions/register_ws_endpoint.js +10 -4
  17. package/dist/actions/socket.svelte.d.ts +19 -4
  18. package/dist/actions/socket.svelte.d.ts.map +1 -1
  19. package/dist/actions/socket.svelte.js +31 -4
  20. package/dist/actions/transports.d.ts +30 -1
  21. package/dist/actions/transports.d.ts.map +1 -1
  22. package/dist/actions/transports.js +30 -1
  23. package/dist/actions/transports_ws_auth_guard.d.ts +13 -6
  24. package/dist/actions/transports_ws_auth_guard.d.ts.map +1 -1
  25. package/dist/actions/transports_ws_auth_guard.js +24 -10
  26. package/dist/actions/transports_ws_backend.d.ts +51 -5
  27. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  28. package/dist/actions/transports_ws_backend.js +62 -9
  29. package/dist/actions/ws_endpoint_spec.d.ts +11 -10
  30. package/dist/actions/ws_endpoint_spec.d.ts.map +1 -1
  31. package/dist/auth/CLAUDE.md +44 -22
  32. package/dist/auth/account_actions.d.ts.map +1 -1
  33. package/dist/auth/account_actions.js +12 -1
  34. package/dist/auth/account_route_schema.d.ts +1 -0
  35. package/dist/auth/account_route_schema.d.ts.map +1 -1
  36. package/dist/auth/account_route_schema.js +12 -0
  37. package/dist/auth/account_routes.d.ts +7 -2
  38. package/dist/auth/account_routes.d.ts.map +1 -1
  39. package/dist/auth/account_routes.js +240 -187
  40. package/dist/auth/admin_action_specs.d.ts +6 -1
  41. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  42. package/dist/auth/admin_action_specs.js +6 -1
  43. package/dist/auth/admin_actions.d.ts.map +1 -1
  44. package/dist/auth/admin_actions.js +14 -2
  45. package/dist/auth/api_token.d.ts +20 -0
  46. package/dist/auth/api_token.d.ts.map +1 -1
  47. package/dist/auth/api_token.js +21 -1
  48. package/dist/auth/api_token_queries.d.ts +22 -4
  49. package/dist/auth/api_token_queries.d.ts.map +1 -1
  50. package/dist/auth/api_token_queries.js +11 -6
  51. package/dist/auth/audit_log_routes.d.ts +10 -0
  52. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  53. package/dist/auth/audit_log_routes.js +20 -0
  54. package/dist/auth/audit_log_schema.d.ts +30 -5
  55. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  56. package/dist/auth/audit_log_schema.js +30 -0
  57. package/dist/auth/bearer_auth.d.ts +4 -2
  58. package/dist/auth/bearer_auth.d.ts.map +1 -1
  59. package/dist/auth/bearer_auth.js +19 -8
  60. package/dist/auth/bootstrap_route_schema.d.ts +1 -0
  61. package/dist/auth/bootstrap_route_schema.d.ts.map +1 -1
  62. package/dist/auth/bootstrap_route_schema.js +1 -0
  63. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  64. package/dist/auth/bootstrap_routes.js +85 -76
  65. package/dist/auth/cell_action_specs.d.ts +7 -3
  66. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  67. package/dist/auth/cell_action_specs.js +9 -5
  68. package/dist/auth/cell_actions.d.ts +3 -3
  69. package/dist/auth/cell_actions.d.ts.map +1 -1
  70. package/dist/auth/cell_actions.js +67 -38
  71. package/dist/auth/cell_authorize.d.ts +69 -8
  72. package/dist/auth/cell_authorize.d.ts.map +1 -1
  73. package/dist/auth/cell_authorize.js +89 -9
  74. package/dist/auth/cell_field_action_specs.d.ts +5 -4
  75. package/dist/auth/cell_field_action_specs.d.ts.map +1 -1
  76. package/dist/auth/cell_field_action_specs.js +7 -7
  77. package/dist/auth/cell_field_actions.d.ts +7 -5
  78. package/dist/auth/cell_field_actions.d.ts.map +1 -1
  79. package/dist/auth/cell_field_actions.js +24 -22
  80. package/dist/auth/cell_grant_action_specs.d.ts +6 -0
  81. package/dist/auth/cell_grant_action_specs.d.ts.map +1 -1
  82. package/dist/auth/cell_grant_action_specs.js +6 -0
  83. package/dist/auth/cell_grant_actions.d.ts +3 -1
  84. package/dist/auth/cell_grant_actions.d.ts.map +1 -1
  85. package/dist/auth/cell_grant_actions.js +16 -2
  86. package/dist/auth/cell_item_action_specs.d.ts +5 -4
  87. package/dist/auth/cell_item_action_specs.d.ts.map +1 -1
  88. package/dist/auth/cell_item_action_specs.js +7 -7
  89. package/dist/auth/cell_item_actions.d.ts +6 -5
  90. package/dist/auth/cell_item_actions.d.ts.map +1 -1
  91. package/dist/auth/cell_item_actions.js +23 -22
  92. package/dist/auth/cell_relation_list_limit.d.ts +20 -0
  93. package/dist/auth/cell_relation_list_limit.d.ts.map +1 -0
  94. package/dist/auth/cell_relation_list_limit.js +27 -0
  95. package/dist/auth/cell_relation_visibility.d.ts +4 -3
  96. package/dist/auth/cell_relation_visibility.d.ts.map +1 -1
  97. package/dist/auth/cell_relation_visibility.js +6 -5
  98. package/dist/auth/cell_steward.d.ts +45 -0
  99. package/dist/auth/cell_steward.d.ts.map +1 -0
  100. package/dist/auth/cell_steward.js +53 -0
  101. package/dist/auth/cleanup.d.ts +9 -9
  102. package/dist/auth/cleanup.js +9 -9
  103. package/dist/auth/credential_deadline.d.ts +95 -0
  104. package/dist/auth/credential_deadline.d.ts.map +1 -0
  105. package/dist/auth/credential_deadline.js +139 -0
  106. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  107. package/dist/auth/daemon_token_middleware.js +4 -1
  108. package/dist/auth/middleware.d.ts +90 -12
  109. package/dist/auth/middleware.d.ts.map +1 -1
  110. package/dist/auth/middleware.js +115 -16
  111. package/dist/auth/request_context.d.ts +8 -2
  112. package/dist/auth/request_context.d.ts.map +1 -1
  113. package/dist/auth/request_context.js +20 -4
  114. package/dist/auth/resolved_auth.d.ts +13 -2
  115. package/dist/auth/resolved_auth.d.ts.map +1 -1
  116. package/dist/auth/resolved_auth.js +5 -4
  117. package/dist/auth/role_grant_offer_action_specs.d.ts +5 -0
  118. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -1
  119. package/dist/auth/role_grant_offer_action_specs.js +6 -1
  120. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -1
  121. package/dist/auth/role_grant_offer_actions.js +23 -16
  122. package/dist/auth/role_grant_offer_queries.d.ts +8 -6
  123. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
  124. package/dist/auth/role_grant_offer_queries.js +16 -8
  125. package/dist/auth/session_cookie.d.ts +4 -0
  126. package/dist/auth/session_cookie.d.ts.map +1 -1
  127. package/dist/auth/session_cookie.js +3 -2
  128. package/dist/auth/session_middleware.d.ts +1 -1
  129. package/dist/auth/session_middleware.d.ts.map +1 -1
  130. package/dist/auth/session_middleware.js +4 -1
  131. package/dist/auth/session_queries.d.ts +19 -2
  132. package/dist/auth/session_queries.d.ts.map +1 -1
  133. package/dist/auth/session_queries.js +8 -2
  134. package/dist/auth/signup_route_schema.d.ts.map +1 -1
  135. package/dist/auth/signup_route_schema.js +1 -0
  136. package/dist/auth/signup_routes.d.ts +4 -3
  137. package/dist/auth/signup_routes.d.ts.map +1 -1
  138. package/dist/auth/signup_routes.js +157 -134
  139. package/dist/auth/token_scope.d.ts +7 -1
  140. package/dist/auth/token_scope.d.ts.map +1 -1
  141. package/dist/auth/token_scope.js +8 -2
  142. package/dist/db/CLAUDE.md +32 -19
  143. package/dist/db/cell_ddl.d.ts +1 -2
  144. package/dist/db/cell_ddl.d.ts.map +1 -1
  145. package/dist/db/cell_ddl.js +1 -2
  146. package/dist/db/cell_field_queries.d.ts +20 -18
  147. package/dist/db/cell_field_queries.d.ts.map +1 -1
  148. package/dist/db/cell_field_queries.js +28 -22
  149. package/dist/db/cell_item_queries.d.ts +20 -14
  150. package/dist/db/cell_item_queries.d.ts.map +1 -1
  151. package/dist/db/cell_item_queries.js +28 -18
  152. package/dist/db/cell_moderation.d.ts +67 -0
  153. package/dist/db/cell_moderation.d.ts.map +1 -0
  154. package/dist/db/cell_moderation.js +66 -0
  155. package/dist/db/cell_queries.d.ts +44 -14
  156. package/dist/db/cell_queries.d.ts.map +1 -1
  157. package/dist/db/cell_queries.js +68 -45
  158. package/dist/db/cell_view_sql.d.ts +102 -0
  159. package/dist/db/cell_view_sql.d.ts.map +1 -0
  160. package/dist/db/cell_view_sql.js +112 -0
  161. package/dist/db/db_pg.d.ts +1 -1
  162. package/dist/db/db_pg.js +1 -1
  163. package/dist/hono_context.d.ts +23 -0
  164. package/dist/hono_context.d.ts.map +1 -1
  165. package/dist/hono_context.js +19 -0
  166. package/dist/http/CLAUDE.md +41 -15
  167. package/dist/http/auth_shape.d.ts +5 -2
  168. package/dist/http/auth_shape.d.ts.map +1 -1
  169. package/dist/http/auth_shape.js +5 -2
  170. package/dist/http/ip_canonical.d.ts +30 -3
  171. package/dist/http/ip_canonical.d.ts.map +1 -1
  172. package/dist/http/ip_canonical.js +50 -3
  173. package/dist/http/middleware_spec.d.ts +16 -0
  174. package/dist/http/middleware_spec.d.ts.map +1 -1
  175. package/dist/http/origin.d.ts +113 -18
  176. package/dist/http/origin.d.ts.map +1 -1
  177. package/dist/http/origin.js +247 -127
  178. package/dist/http/route_spec.d.ts +21 -0
  179. package/dist/http/route_spec.d.ts.map +1 -1
  180. package/dist/http/schema_helpers.d.ts +22 -0
  181. package/dist/http/schema_helpers.d.ts.map +1 -1
  182. package/dist/http/schema_helpers.js +24 -0
  183. package/dist/http/surface.d.ts +6 -1
  184. package/dist/http/surface.d.ts.map +1 -1
  185. package/dist/http/surface.js +12 -5
  186. package/dist/primitive_schemas.d.ts +10 -1
  187. package/dist/primitive_schemas.d.ts.map +1 -1
  188. package/dist/primitive_schemas.js +11 -2
  189. package/dist/rate_limiter.d.ts +179 -12
  190. package/dist/rate_limiter.d.ts.map +1 -1
  191. package/dist/rate_limiter.js +352 -42
  192. package/dist/realtime/sse_auth_guard.d.ts +22 -6
  193. package/dist/realtime/sse_auth_guard.d.ts.map +1 -1
  194. package/dist/realtime/sse_auth_guard.js +31 -12
  195. package/dist/realtime/subscriber_registry.d.ts +10 -1
  196. package/dist/realtime/subscriber_registry.d.ts.map +1 -1
  197. package/dist/realtime/subscriber_registry.js +10 -1
  198. package/dist/server/app_server.d.ts +20 -7
  199. package/dist/server/app_server.d.ts.map +1 -1
  200. package/dist/server/app_server.js +8 -3
  201. package/dist/server/env.d.ts +2 -1
  202. package/dist/server/env.d.ts.map +1 -1
  203. package/dist/server/env.js +7 -17
  204. package/dist/server/serve_fact_route.d.ts +8 -4
  205. package/dist/server/serve_fact_route.d.ts.map +1 -1
  206. package/dist/server/serve_fact_route.js +16 -6
  207. package/dist/testing/CLAUDE.md +164 -25
  208. package/dist/testing/adversarial_headers.d.ts.map +1 -1
  209. package/dist/testing/adversarial_headers.js +13 -6
  210. package/dist/testing/connection_closer_helpers.d.ts +6 -1
  211. package/dist/testing/connection_closer_helpers.d.ts.map +1 -1
  212. package/dist/testing/connection_closer_helpers.js +4 -0
  213. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
  214. package/dist/testing/cross_backend/account_lifecycle.js +37 -2
  215. package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
  216. package/dist/testing/cross_backend/body_size.js +14 -65
  217. package/dist/testing/cross_backend/capabilities.d.ts +26 -2
  218. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  219. package/dist/testing/cross_backend/capabilities.js +9 -3
  220. package/dist/testing/cross_backend/cell_gated_create.d.ts +38 -0
  221. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
  222. package/dist/testing/cross_backend/cell_gated_create.js +401 -5
  223. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  224. package/dist/testing/cross_backend/cell_relations.js +145 -2
  225. package/dist/testing/cross_backend/conformance_case.d.ts +11 -3
  226. package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -1
  227. package/dist/testing/cross_backend/conformance_case.js +6 -0
  228. package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -1
  229. package/dist/testing/cross_backend/conformance_table.js +20 -19
  230. package/dist/testing/cross_backend/credential_deadline.d.ts +25 -0
  231. package/dist/testing/cross_backend/credential_deadline.d.ts.map +1 -0
  232. package/dist/testing/cross_backend/credential_deadline.js +185 -0
  233. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  234. package/dist/testing/cross_backend/default_backend_configs.js +13 -2
  235. package/dist/testing/cross_backend/expected_schema.json +0 -1
  236. package/dist/testing/cross_backend/full_spine_mount.d.ts +3 -0
  237. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  238. package/dist/testing/cross_backend/full_spine_mount.js +7 -1
  239. package/dist/testing/cross_backend/login_security.d.ts +16 -0
  240. package/dist/testing/cross_backend/login_security.d.ts.map +1 -1
  241. package/dist/testing/cross_backend/login_security.js +154 -0
  242. package/dist/testing/cross_backend/narrowed_api_token.d.ts +23 -0
  243. package/dist/testing/cross_backend/narrowed_api_token.d.ts.map +1 -0
  244. package/dist/testing/cross_backend/narrowed_api_token.js +40 -0
  245. package/dist/testing/cross_backend/origin.d.ts.map +1 -1
  246. package/dist/testing/cross_backend/origin.js +6 -26
  247. package/dist/testing/cross_backend/origin_conformance.d.ts +152 -0
  248. package/dist/testing/cross_backend/origin_conformance.d.ts.map +1 -0
  249. package/dist/testing/cross_backend/origin_conformance.js +136 -0
  250. package/dist/testing/cross_backend/origin_conformance.json +1679 -0
  251. package/dist/testing/cross_backend/origin_scope.d.ts +16 -0
  252. package/dist/testing/cross_backend/origin_scope.d.ts.map +1 -0
  253. package/dist/testing/cross_backend/origin_scope.js +140 -0
  254. package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
  255. package/dist/testing/cross_backend/query_shape.js +24 -2
  256. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts +35 -0
  257. package/dist/testing/cross_backend/role_grant_cross_helpers.d.ts.map +1 -0
  258. package/dist/testing/cross_backend/role_grant_cross_helpers.js +64 -0
  259. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts +8 -0
  260. package/dist/testing/cross_backend/role_grant_offer_notification_ws.d.ts.map +1 -1
  261. package/dist/testing/cross_backend/role_grant_offer_notification_ws.js +44 -33
  262. package/dist/testing/cross_backend/role_grant_participation.d.ts.map +1 -1
  263. package/dist/testing/cross_backend/role_grant_participation.js +64 -0
  264. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  265. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -4
  266. package/dist/testing/cross_backend/spine_surface_constants.d.ts +20 -1
  267. package/dist/testing/cross_backend/spine_surface_constants.d.ts.map +1 -1
  268. package/dist/testing/cross_backend/spine_surface_constants.js +23 -1
  269. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  270. package/dist/testing/cross_backend/testing_backdoor.js +20 -7
  271. package/dist/testing/cross_backend/testing_reset_actions.d.ts +180 -5
  272. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  273. package/dist/testing/cross_backend/testing_reset_actions.js +154 -5
  274. package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
  275. package/dist/testing/cross_backend/token_scope_surface.js +25 -21
  276. package/dist/testing/cross_backend/transport_composition.d.ts +60 -0
  277. package/dist/testing/cross_backend/transport_composition.d.ts.map +1 -0
  278. package/dist/testing/cross_backend/transport_composition.js +364 -0
  279. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  280. package/dist/testing/cross_backend/ts_spine_backend_config.js +21 -6
  281. package/dist/testing/cross_backend/ws_action_rate_limit.d.ts.map +1 -1
  282. package/dist/testing/cross_backend/ws_action_rate_limit.js +21 -1
  283. package/dist/testing/cross_backend/ws_role_gated.d.ts +38 -0
  284. package/dist/testing/cross_backend/ws_role_gated.d.ts.map +1 -0
  285. package/dist/testing/cross_backend/ws_role_gated.js +142 -0
  286. package/dist/testing/entities.d.ts +23 -0
  287. package/dist/testing/entities.d.ts.map +1 -1
  288. package/dist/testing/entities.js +37 -0
  289. package/dist/testing/integration.d.ts.map +1 -1
  290. package/dist/testing/integration.js +3 -1
  291. package/dist/testing/middleware.d.ts +10 -0
  292. package/dist/testing/middleware.d.ts.map +1 -1
  293. package/dist/testing/middleware.js +31 -7
  294. package/dist/testing/schema_introspect.js +1 -1
  295. package/dist/testing/schema_parity.d.ts +4 -3
  296. package/dist/testing/schema_parity.d.ts.map +1 -1
  297. package/dist/testing/schema_parity.js +4 -3
  298. package/dist/testing/schema_ready_fixture.d.ts +2 -4
  299. package/dist/testing/schema_ready_fixture.d.ts.map +1 -1
  300. package/dist/testing/schema_ready_fixture.js +2 -4
  301. package/dist/testing/stubs.d.ts +16 -3
  302. package/dist/testing/stubs.d.ts.map +1 -1
  303. package/dist/testing/stubs.js +20 -7
  304. package/dist/testing/surface_invariants.js +2 -2
  305. package/dist/ui/auth_state.svelte.d.ts +10 -0
  306. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  307. package/dist/ui/auth_state.svelte.js +22 -4
  308. package/package.json +1 -1
  309. package/dist/db/cell_history_ddl.d.ts +0 -38
  310. package/dist/db/cell_history_ddl.d.ts.map +0 -1
  311. package/dist/db/cell_history_ddl.js +0 -61
@@ -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
 
@@ -1386,7 +1421,14 @@ moderation_required}`, with `moderation` set per the verdict (`pending`+private
1386
1421
  **visible** one **403**s; plain parentless creates stay open. The moderate
1387
1422
  suite then proves the `pending → approved | rejected` transition: an admin
1388
1423
  (root manager) approves → public, the **author** self-approve → 403
1389
- (anti-self-approval), a non-viewer → 404, reject → stays private. The
1424
+ (anti-self-approval), a non-viewer → 404, reject → stays private. It also
1425
+ pins the moderation read gate (a pending or rejected post reaches no
1426
+ non-steward reader past its author and admins on `cell_get`, `cell_list`,
1427
+ `cell_item_list`, and fact serving, even flipped public or granted while
1428
+ approved), `cell_grant_create`'s
1429
+ 403 `cell_grant_unapproved`, and the steward arm (a non-admin root owner — a
1430
+ non-`space` root the policy leaves open — reads the queue, `cell_get`, and
1431
+ the fact, and moderates, but still 404s on clone and create-under). The
1390
1432
  authorizer _behavior_ adds no method/wire shape, so schema + manifest parity
1391
1433
  are **blind** to a divergence — these are the only gates that catch one (the
1392
1434
  `cell_moderate` verb itself is manifest-covered). In-process hook coverage is
@@ -1565,6 +1607,27 @@ are `src/test/auth/cell_crud_parity.db.test.ts`
1565
1607
  payload-expiry tests never reached. Daemon-token-gated like its siblings; the
1566
1608
  Rust mirror is `fuz_testing::create_testing_mint_session_action_spec`.
1567
1609
 
1610
+ Also in the module and **not** bundled: `_testing_origin_check`
1611
+ (`create_testing_origin_check_action()`, `run_testing_origin_check(input)`,
1612
+ `TestingOriginCheckInput` / `TestingOriginCheckOutput`) — answers the origin
1613
+ allowlist's parse and request check for one `{patterns, origin}` pair:
1614
+ `{ok: true, allowed}` or `{ok: false, errors: [{index?, kind}]}`. Pure, no
1615
+ database. Kept out of the bundle so a consumer whose Rust binary doesn't
1616
+ mount the mirror (`fuz_testing::create_testing_origin_check_action_spec`)
1617
+ keeps an exact action-manifest gate; the spine binary appends it in
1618
+ `build_full_spine_rpc_actions`.
1619
+
1620
+ Also **not** bundled: `_testing_shorten_credential_expiry`
1621
+ (`create_testing_shorten_credential_expiry_action()`) — pulls an account's
1622
+ `auth_session` and `api_token` expiries in to `seconds` (1–60) from now,
1623
+ shorten-only (`LEAST`, an eternal token gaining one), answering
1624
+ `{sessions, api_tokens}` counts. Applied before a connection opens, since
1625
+ the deadline is read when the credential resolves. Backs
1626
+ `describe_credential_deadline_cross_tests`; appended in
1627
+ `build_full_spine_rpc_actions` for the same manifest reason as
1628
+ `_testing_origin_check`. The Rust mirror is
1629
+ `fuz_testing::create_testing_shorten_credential_expiry_action_spec`.
1630
+
1568
1631
  Also in the module — but **not** bundled by `create_testing_actions` —
1569
1632
  `_testing_action_manifest` (+ `create_testing_action_manifest_action(mounted)`):
1570
1633
  dumps the live RPC registry as a normalized `ActionManifest` for the
@@ -1611,19 +1674,95 @@ serve a body-parity gate (its per-spine coverage stays in the two surface
1611
1674
  censuses). Cross-process only; fuz_app's own wiring is
1612
1675
  `src/test/cross_backend/token_scope_surface.cross.test.ts`.
1613
1676
 
1677
+ ### Front-door contract — `cross_backend/transport_composition.ts`
1678
+
1679
+ `describe_transport_composition_cross_tests({setup_test, capabilities, base_url,
1680
+ rpc_path?, origin?, rest_path?, password_path?, ws_path?, sse_path?})` — the per-transport
1681
+ composition table: what each transport answers before per-action dispatch,
1682
+ when one request trips several refusals. Rows are REST `POST` (session-only
1683
+ `/api/account/logout`), RPC `POST`, RPC `GET`, the WS upgrade (gated on
1684
+ `capabilities.ws`) and the audit stream (`capabilities.sse`); columns are a
1685
+ foreign `Origin` with the keeper's session (→ 403 `forbidden_origin`), an
1686
+ over-cap declared length with a foreign `Origin` and no credential (→ 413
1687
+ `payload_too_large`, body-carrying rows only), no credential (→ 401
1688
+ `authentication_required`), and a narrowed bearer with no `Origin` (→ 403
1689
+ `token_scope_required` naming `rpc:<method>` on RPC and `surface:ws_upgrade` on
1690
+ the upgrade; 403 `credential_type_required` on the session-only REST route and
1691
+ audit stream); a `/password` cell pins that route's 401 beside `/logout`'s.
1692
+ Each case asserts the status and the body — flat `{error}` or the JSON-RPC
1693
+ envelope's `error.data`. A WS refusal's body is required where the backend
1694
+ declares `capabilities.ws_refusal_body`; where it doesn't (Node's
1695
+ `@hono/node-ws` drops the body), an empty body passes on its status and a sent
1696
+ one is still asserted. The table
1697
+ is the exported `transport_composition_cases` data. Given `base_url`, every
1698
+ probe is written to the socket (`raw_http_exchange`) and an over-cap request
1699
+ sends its head alone; with `base_url: null` the probes go through the
1700
+ fixture's transport and the WS rows skip. Not in the standard bundle. fuz_app's
1701
+ own wiring is `src/test/cross_backend/transport_composition.cross.test.ts` and
1702
+ the in-process `src/test/auth/transport_composition_parity.db.test.ts`.
1703
+
1704
+ `cross_backend/narrowed_api_token.ts` — `mint_narrowed_api_token({app,
1705
+ rpc_path, session_headers, methods, name?})` mints a method-scoped api token
1706
+ through the production `account_token_create` path and returns the raw bearer
1707
+ (throws when the mint is refused). Shared by the front-door table, the
1708
+ token-scope surface suite, and the conformance table's `scoped_token`
1709
+ principal.
1710
+
1711
+ ### Credential deadline parity — `cross_backend/credential_deadline.ts`
1712
+
1713
+ `describe_credential_deadline_cross_tests({setup_test, capabilities, base_url,
1714
+ ws_path, sse_path?, rpc_path?})` — a connection closes when the credential it
1715
+ opened on expires: each case pulls a fresh account's credential expiries in to
1716
+ two seconds with `_testing_shorten_credential_expiry` before the open, then
1717
+ checks the connection still dispatches short of the deadline and closes at it.
1718
+ A WebSocket on a session and one on an API token (`capabilities.ws`) close with
1719
+ `WS_CLOSE_SESSION_REVOKED` and `WS_CLOSE_CREDENTIAL_EXPIRED_REASON`; an audit
1720
+ stream on a session (`capabilities.sse`) ends; and a second seam call asking
1721
+ for a later expiry moves nothing (the seam only shortens). API token × audit
1722
+ stream is not reachable over the wire — the stock stream is session-only — so
1723
+ the Rust spine pins it on a widened gate and the TS twin shares one deadline
1724
+ wait across both transports. Cross-process only; fuz_app's own wiring is
1725
+ `src/test/cross_backend/credential_deadline.cross.test.ts`.
1726
+
1727
+ ### Origin check scope parity — `cross_backend/origin_scope.ts`
1728
+
1729
+ `describe_origin_scope_cross_tests({setup_test, capabilities, probe_path?})` —
1730
+ the origin check covers paths outside `/api`: on `GET /ready` (`probe_path`), a
1731
+ foreign `Origin` → 403 `forbidden_origin`, an absent or allowed one → 200. Gated
1732
+ on `capabilities.ready`. Runs both legs (the in-process
1733
+ `cross_backend/origin_scope_parity.db.test.ts` + the cross-process
1734
+ `origin_scope.cross.test.ts`).
1735
+
1614
1736
  ### Origin verification parity — `cross_backend/origin.ts`
1615
1737
 
1616
1738
  `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
1739
+ imperative Origin-verification suite: absent `Origin` → request passes
1740
+ (non-browser direct access); the disallowed-`Origin` 403 is the front-door
1741
+ table's. Imperative (not a conformance-table row) because absent-Origin needs
1742
+ `fresh_transport({origin: null})`. Runs both legs (the
1622
1743
  in-process `auth/origin_parity.db.test.ts` + the cross-process
1623
1744
  `origin.cross.test.ts`). The promotion surfaced a twin-impl divergence — the
1624
1745
  Rust spine returned a plain-text body — now converged to the canonical TS
1625
1746
  `{error: "forbidden_origin"}` via `fuz_http::forbidden_origin_response()`.
1626
1747
 
1748
+ ### Origin allowlist conformance — `cross_backend/origin_conformance.ts`
1749
+
1750
+ `cross_backend/origin_conformance.json` is the shared table both spines' origin
1751
+ allowlist must satisfy: **parse** rows `{raw, ok: true}` or `{raw, errors:
1752
+ [{index?, kind}]}` (every refusal, 1-based position, `AllowedOriginsErrorKind`)
1753
+ and **match** rows `{patterns, origin, allowed}` (`origin: null` — no
1754
+ `Origin` — passes). A row may carry a `note`. `load_origin_conformance()` reads
1755
+ and validates it (`OriginConformance`), `to_origin_conformance_checks(table)`
1756
+ flattens it into `_testing_origin_check` inputs with their expected answers, and
1757
+ `assert_origin_conformance(checks, answers, backend)` fails once listing every
1758
+ row that disagrees. `describe_origin_conformance_cross_tests({setup_test,
1759
+ rpc_path?})` sends every row to a backend's `_testing_origin_check` over the
1760
+ keeper's daemon-token channel; the TS unit leg runs the same rows through
1761
+ `run_testing_origin_check` (`src/test/http/origin.conformance.test.ts`), so the
1762
+ Rust spine is held to the table without reading the file. Ungated; edit the
1763
+ table by hand — it is the statement of the grammar, not generated output.
1764
+ fuz_app's own wiring is `src/test/cross_backend/origin_conformance.cross.test.ts`.
1765
+
1627
1766
  ### CORS simple-request parity — `cross_backend/simple_request.ts`
1628
1767
 
1629
1768
  `describe_simple_request_cross_tests({setup_test, rpc_path?, login_path?, logout_path?})`
@@ -1714,9 +1853,9 @@ so one fixture is the cross-impl contract): the TS spine via
1714
1853
  `import.meta.url` constant off `spine_surface_constants.ts`; the Rust
1715
1854
  `testing_spine_stub` via the absolute path `rust_spine_stub_backend_config`
1716
1855
  passes through `FUZ_RUST_SPINE_STUB_EXPECTED_SCHEMA_PATH`. The fixture covers the full
1717
- spine bootstrap (auth + cell + cell_history + fact) and is regenerated +
1856
+ spine bootstrap (auth + cell + fact) and is regenerated +
1718
1857
  drift-guarded by `src/test/cross_backend/spine_expected_schema.db.test.ts`
1719
- (`UPDATE_SCHEMA_READY=1`, then `gro format`).
1858
+ (`UPDATE_SCHEMA_READY=1`).
1720
1859
 
1721
1860
  ### Testing-backdoor credential gate — `cross_backend/testing_backdoor.ts`
1722
1861
 
@@ -1935,7 +2074,7 @@ re-roll the serve / daemon-info / WS-attach / drain boilerplate:
1935
2074
  - `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
2075
  - `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
2076
 
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.
2077
+ 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
2078
 
1940
2079
  ### Live-method coverage reconciliation — `method_coverage.ts`
1941
2080
 
@@ -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":"account_lifecycle.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/account_lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAmC9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E;;;;;GAKG;AACH,MAAM,MAAM,gCAAgC,GAAG,uCAAuC,CAAC;AAEvF,eAAO,MAAM,sCAAsC,GAClD,SAAS,gCAAgC,KACvC,IAoUF,CAAC"}
1
+ {"version":3,"file":"account_lifecycle.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/account_lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAuC9B,OAAO,KAAK,EAAE,uCAAuC,EAAE,MAAM,YAAY,CAAC;AAG1E;;;;;GAKG;AACH,MAAM,MAAM,gCAAgC,GAAG,uCAAuC,CAAC;AAEvF,eAAO,MAAM,sCAAsC,GAClD,SAAS,gCAAgC,KACvC,IAqZF,CAAC"}
@@ -2,7 +2,9 @@ import '../assert_dev_env.js';
2
2
  /**
3
3
  * Cross-backend parity suite for the account-lifecycle admin verbs:
4
4
  * `account_delete` (soft), `account_undelete` (reactivation), and
5
- * `account_purge` (keeper hard-delete), plus the keeper guard.
5
+ * `account_purge` (keeper hard-delete), plus the keeper guard and the
6
+ * session-only self-delete (an API token can't end its own account; an admin
7
+ * token still deletes another).
6
8
  *
7
9
  * Like the cell suites, these verbs can't ride the generic
8
10
  * `describe_rpc_round_trip_tests`: they're stateful and destructive (a
@@ -16,7 +18,8 @@ import '../assert_dev_env.js';
16
18
  */
17
19
  import { describe, assert } from 'vitest';
18
20
  import { AccountDeleteOutput, AccountUndeleteOutput, AccountPurgeOutput, AdminAccountListOutput, AuditLogListOutput, ERROR_CANNOT_DELETE_KEEPER } from '../../auth/admin_action_specs.js';
19
- import { ERROR_ACCOUNT_NOT_FOUND, ERROR_AUTHENTICATION_REQUIRED } from '../../http/error_schemas.js';
21
+ import { ERROR_ACCOUNT_NOT_FOUND, ERROR_AUTHENTICATION_REQUIRED, ERROR_CREDENTIAL_TYPE_REQUIRED } from '../../http/error_schemas.js';
22
+ import { JSONRPC_ERROR_CODES } from '../../http/jsonrpc_errors.js';
20
23
  import { test_if } from './capabilities.js';
21
24
  import { cross_rpc_call, error_reason, expect_output } from './cell_cross_helpers.js';
22
25
  import { assert_iso8601_seconds } from './wire_shapes.js';
@@ -55,6 +58,38 @@ export const describe_account_lifecycle_cross_tests = (options) => {
55
58
  assert.strictEqual(purge.ok, false, 'purge of keeper account must be refused');
56
59
  assert.strictEqual(error_reason(purge), ERROR_CANNOT_DELETE_KEEPER);
57
60
  });
61
+ test_if(capabilities.account_lifecycle, 'self-delete is session-only; an admin bearer still deletes another account', async () => {
62
+ const fixture = await setup_test();
63
+ const member = await fixture.create_account({ username: 'lifecycle_self_bearer' });
64
+ // The member's own bearer may not end its account (handler-enforced,
65
+ // the dispatcher's credential-type refusal).
66
+ const refused = await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, 'account_delete', {}, member.create_bearer_headers());
67
+ assert.strictEqual(refused.ok, false, 'a bearer self-delete must be refused');
68
+ assert.strictEqual(refused.error?.code, JSONRPC_ERROR_CODES.forbidden);
69
+ assert.strictEqual(error_reason(refused), ERROR_CREDENTIAL_TYPE_REQUIRED);
70
+ assert.deepStrictEqual(refused.error?.data
71
+ ?.required_credential_types, ['session']);
72
+ // An explicit own id is self in either case (`Uuid` admits both) — the
73
+ // keeper's too, whose refusal is the credential one, not the keeper guard.
74
+ for (const [label, account_id, headers] of [
75
+ ['member', member.account.id, member.create_bearer_headers()],
76
+ ['keeper', fixture.account.id, fixture.create_bearer_headers()]
77
+ ]) {
78
+ const upper = await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, 'account_delete', { account_id: account_id.toUpperCase() }, headers);
79
+ assert.strictEqual(upper.ok, false, `${label}: an uppercased own id is self`);
80
+ assert.deepStrictEqual(upper.error, refused.error, label);
81
+ }
82
+ // Nothing was tombstoned: the member's session still verifies.
83
+ const still = await cross_rpc_call(fixture.fresh_transport(), rpc_path, 'account_verify', undefined, member.create_session_headers());
84
+ assert.ok(still.ok, 'the refused self-delete leaves the account live');
85
+ // The keeper's bearer (admin-capable) deletes another account.
86
+ const deleted = expect_output(await cross_rpc_call(fixture.fresh_transport({ origin: null }), rpc_path, 'account_delete', { account_id: member.account.id }, fixture.create_bearer_headers()), AccountDeleteOutput);
87
+ assert.strictEqual(deleted.deleted, true);
88
+ // A self-delete over a session goes through.
89
+ const self = await fixture.create_account({ username: 'lifecycle_self_session' });
90
+ const self_deleted = expect_output(await cross_rpc_call(fixture.fresh_transport(), rpc_path, 'account_delete', {}, self.create_session_headers()), AccountDeleteOutput);
91
+ assert.strictEqual(self_deleted.deleted, true);
92
+ });
58
93
  test_if(capabilities.account_lifecycle, 'fail-closed: a soft-deleted account’s session + bearer credentials no longer authenticate', async () => {
59
94
  const fixture = await setup_test();
60
95
  const victim = await fixture.create_account({ username: 'lifecycle_failclosed' });
@@ -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"}