@fuzdev/fuz_app 0.54.0 → 0.56.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 (348) hide show
  1. package/dist/actions/CLAUDE.md +214 -103
  2. package/dist/actions/action_bridge.d.ts +8 -5
  3. package/dist/actions/action_bridge.d.ts.map +1 -1
  4. package/dist/actions/action_bridge.js +1 -11
  5. package/dist/actions/action_codegen.d.ts +32 -0
  6. package/dist/actions/action_codegen.d.ts.map +1 -1
  7. package/dist/actions/action_codegen.js +35 -15
  8. package/dist/actions/action_registry.d.ts.map +1 -1
  9. package/dist/actions/action_registry.js +5 -2
  10. package/dist/actions/action_rpc.d.ts +141 -22
  11. package/dist/actions/action_rpc.d.ts.map +1 -1
  12. package/dist/actions/action_rpc.js +106 -187
  13. package/dist/actions/action_spec.d.ts +55 -16
  14. package/dist/actions/action_spec.d.ts.map +1 -1
  15. package/dist/actions/action_spec.js +16 -11
  16. package/dist/actions/action_types.d.ts +28 -60
  17. package/dist/actions/action_types.d.ts.map +1 -1
  18. package/dist/actions/action_types.js +13 -5
  19. package/dist/actions/broadcast_api.d.ts +2 -2
  20. package/dist/actions/broadcast_api.js +2 -2
  21. package/dist/actions/compile_action_registry.d.ts +50 -0
  22. package/dist/actions/compile_action_registry.d.ts.map +1 -0
  23. package/dist/actions/compile_action_registry.js +69 -0
  24. package/dist/actions/heartbeat.d.ts +8 -4
  25. package/dist/actions/heartbeat.d.ts.map +1 -1
  26. package/dist/actions/heartbeat.js +5 -4
  27. package/dist/actions/perform_action.d.ts +145 -0
  28. package/dist/actions/perform_action.d.ts.map +1 -0
  29. package/dist/actions/perform_action.js +258 -0
  30. package/dist/actions/register_action_ws.d.ts +46 -40
  31. package/dist/actions/register_action_ws.d.ts.map +1 -1
  32. package/dist/actions/register_action_ws.js +101 -159
  33. package/dist/actions/register_ws_endpoint.d.ts +15 -10
  34. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  35. package/dist/actions/register_ws_endpoint.js +54 -7
  36. package/dist/actions/transports.d.ts.map +1 -1
  37. package/dist/actions/transports.js +0 -4
  38. package/dist/actions/transports_ws_auth_guard.d.ts +1 -1
  39. package/dist/actions/transports_ws_auth_guard.js +1 -1
  40. package/dist/actions/transports_ws_backend.d.ts +1 -1
  41. package/dist/actions/transports_ws_backend.js +1 -1
  42. package/dist/auth/CLAUDE.md +794 -410
  43. package/dist/auth/account_action_specs.d.ts +28 -7
  44. package/dist/auth/account_action_specs.d.ts.map +1 -1
  45. package/dist/auth/account_action_specs.js +7 -7
  46. package/dist/auth/account_actions.d.ts +7 -13
  47. package/dist/auth/account_actions.d.ts.map +1 -1
  48. package/dist/auth/account_actions.js +26 -35
  49. package/dist/auth/account_queries.d.ts +52 -16
  50. package/dist/auth/account_queries.d.ts.map +1 -1
  51. package/dist/auth/account_queries.js +87 -38
  52. package/dist/auth/account_routes.d.ts +9 -11
  53. package/dist/auth/account_routes.d.ts.map +1 -1
  54. package/dist/auth/account_routes.js +118 -46
  55. package/dist/auth/account_schema.d.ts +46 -35
  56. package/dist/auth/account_schema.d.ts.map +1 -1
  57. package/dist/auth/account_schema.js +21 -28
  58. package/dist/auth/admin_action_specs.d.ts +100 -32
  59. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  60. package/dist/auth/admin_action_specs.js +64 -33
  61. package/dist/auth/admin_actions.d.ts +13 -19
  62. package/dist/auth/admin_actions.d.ts.map +1 -1
  63. package/dist/auth/admin_actions.js +37 -41
  64. package/dist/auth/audit_emitter.d.ts +160 -0
  65. package/dist/auth/audit_emitter.d.ts.map +1 -0
  66. package/dist/auth/audit_emitter.js +83 -0
  67. package/dist/auth/audit_log_queries.d.ts +17 -48
  68. package/dist/auth/audit_log_queries.d.ts.map +1 -1
  69. package/dist/auth/audit_log_queries.js +20 -56
  70. package/dist/auth/audit_log_routes.d.ts +1 -1
  71. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  72. package/dist/auth/audit_log_routes.js +7 -3
  73. package/dist/auth/audit_log_schema.d.ts +92 -32
  74. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  75. package/dist/auth/audit_log_schema.js +75 -46
  76. package/dist/auth/auth_guard_resolver.d.ts +44 -0
  77. package/dist/auth/auth_guard_resolver.d.ts.map +1 -0
  78. package/dist/auth/auth_guard_resolver.js +56 -0
  79. package/dist/auth/bearer_auth.d.ts +9 -7
  80. package/dist/auth/bearer_auth.d.ts.map +1 -1
  81. package/dist/auth/bearer_auth.js +13 -21
  82. package/dist/auth/bootstrap_account.d.ts +7 -7
  83. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  84. package/dist/auth/bootstrap_account.js +7 -7
  85. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  86. package/dist/auth/bootstrap_routes.js +11 -10
  87. package/dist/auth/cleanup.d.ts +20 -26
  88. package/dist/auth/cleanup.d.ts.map +1 -1
  89. package/dist/auth/cleanup.js +33 -42
  90. package/dist/auth/credential_type_schema.d.ts +115 -0
  91. package/dist/auth/credential_type_schema.d.ts.map +1 -0
  92. package/dist/auth/credential_type_schema.js +127 -0
  93. package/dist/auth/daemon_token_middleware.d.ts +23 -11
  94. package/dist/auth/daemon_token_middleware.d.ts.map +1 -1
  95. package/dist/auth/daemon_token_middleware.js +28 -22
  96. package/dist/auth/ddl.d.ts +2 -2
  97. package/dist/auth/ddl.d.ts.map +1 -1
  98. package/dist/auth/ddl.js +6 -6
  99. package/dist/auth/deps.d.ts +7 -18
  100. package/dist/auth/deps.d.ts.map +1 -1
  101. package/dist/auth/grant_path_schema.d.ts +117 -0
  102. package/dist/auth/grant_path_schema.d.ts.map +1 -0
  103. package/dist/auth/grant_path_schema.js +137 -0
  104. package/dist/auth/invite_queries.d.ts +12 -1
  105. package/dist/auth/invite_queries.d.ts.map +1 -1
  106. package/dist/auth/invite_queries.js +12 -1
  107. package/dist/auth/invite_schema.d.ts +1 -1
  108. package/dist/auth/invite_schema.d.ts.map +1 -1
  109. package/dist/auth/invite_schema.js +1 -1
  110. package/dist/auth/middleware.d.ts.map +1 -1
  111. package/dist/auth/middleware.js +9 -4
  112. package/dist/auth/migrations.d.ts +37 -14
  113. package/dist/auth/migrations.d.ts.map +1 -1
  114. package/dist/auth/migrations.js +79 -32
  115. package/dist/auth/request_context.d.ts +331 -61
  116. package/dist/auth/request_context.d.ts.map +1 -1
  117. package/dist/auth/request_context.js +378 -95
  118. package/dist/auth/{permit_offer_action_specs.d.ts → role_grant_offer_action_specs.d.ts} +163 -94
  119. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -0
  120. package/dist/auth/role_grant_offer_action_specs.js +262 -0
  121. package/dist/auth/role_grant_offer_actions.d.ts +104 -0
  122. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -0
  123. package/dist/auth/role_grant_offer_actions.js +473 -0
  124. package/dist/auth/{permit_offer_notifications.d.ts → role_grant_offer_notifications.d.ts} +90 -70
  125. package/dist/auth/role_grant_offer_notifications.d.ts.map +1 -0
  126. package/dist/auth/role_grant_offer_notifications.js +182 -0
  127. package/dist/auth/role_grant_offer_queries.d.ts +242 -0
  128. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -0
  129. package/dist/auth/role_grant_offer_queries.js +533 -0
  130. package/dist/auth/role_grant_offer_schema.d.ts +150 -0
  131. package/dist/auth/role_grant_offer_schema.d.ts.map +1 -0
  132. package/dist/auth/{permit_offer_schema.js → role_grant_offer_schema.js} +60 -36
  133. package/dist/auth/role_grant_queries.d.ts +231 -0
  134. package/dist/auth/role_grant_queries.d.ts.map +1 -0
  135. package/dist/auth/role_grant_queries.js +320 -0
  136. package/dist/auth/role_schema.d.ts +150 -40
  137. package/dist/auth/role_schema.d.ts.map +1 -1
  138. package/dist/auth/role_schema.js +144 -45
  139. package/dist/auth/scope_kind_schema.d.ts +96 -0
  140. package/dist/auth/scope_kind_schema.d.ts.map +1 -0
  141. package/dist/auth/scope_kind_schema.js +94 -0
  142. package/dist/auth/self_service_role_action_specs.d.ts +6 -1
  143. package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
  144. package/dist/auth/self_service_role_action_specs.js +3 -1
  145. package/dist/auth/self_service_role_actions.d.ts +34 -27
  146. package/dist/auth/self_service_role_actions.d.ts.map +1 -1
  147. package/dist/auth/self_service_role_actions.js +68 -48
  148. package/dist/auth/session_cookie.d.ts +43 -6
  149. package/dist/auth/session_cookie.d.ts.map +1 -1
  150. package/dist/auth/session_cookie.js +31 -5
  151. package/dist/auth/session_middleware.d.ts +37 -3
  152. package/dist/auth/session_middleware.d.ts.map +1 -1
  153. package/dist/auth/session_middleware.js +33 -7
  154. package/dist/auth/signup_routes.d.ts.map +1 -1
  155. package/dist/auth/signup_routes.js +48 -19
  156. package/dist/auth/standard_action_specs.d.ts +2 -2
  157. package/dist/auth/standard_action_specs.js +4 -4
  158. package/dist/auth/standard_rpc_actions.d.ts +23 -19
  159. package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
  160. package/dist/auth/standard_rpc_actions.js +12 -12
  161. package/dist/db/migrate.d.ts +12 -8
  162. package/dist/db/migrate.d.ts.map +1 -1
  163. package/dist/db/migrate.js +10 -7
  164. package/dist/dev/setup.d.ts +2 -2
  165. package/dist/dev/setup.d.ts.map +1 -1
  166. package/dist/dev/setup.js +9 -7
  167. package/dist/env/load.d.ts +1 -1
  168. package/dist/env/load.js +1 -1
  169. package/dist/hono_context.d.ts +64 -5
  170. package/dist/hono_context.d.ts.map +1 -1
  171. package/dist/hono_context.js +38 -2
  172. package/dist/http/CLAUDE.md +264 -87
  173. package/dist/http/auth_shape.d.ts +191 -0
  174. package/dist/http/auth_shape.d.ts.map +1 -0
  175. package/dist/http/auth_shape.js +237 -0
  176. package/dist/http/common_routes.js +3 -3
  177. package/dist/http/db_routes.d.ts +4 -0
  178. package/dist/http/db_routes.d.ts.map +1 -1
  179. package/dist/http/db_routes.js +44 -7
  180. package/dist/http/error_schemas.d.ts +132 -19
  181. package/dist/http/error_schemas.d.ts.map +1 -1
  182. package/dist/http/error_schemas.js +132 -40
  183. package/dist/http/jsonrpc_errors.d.ts +27 -2
  184. package/dist/http/jsonrpc_errors.d.ts.map +1 -1
  185. package/dist/http/jsonrpc_errors.js +26 -2
  186. package/dist/http/pending_effects.d.ts +71 -18
  187. package/dist/http/pending_effects.d.ts.map +1 -1
  188. package/dist/http/pending_effects.js +87 -18
  189. package/dist/http/proxy.d.ts +52 -5
  190. package/dist/http/proxy.d.ts.map +1 -1
  191. package/dist/http/proxy.js +92 -14
  192. package/dist/http/route_spec.d.ts +113 -41
  193. package/dist/http/route_spec.d.ts.map +1 -1
  194. package/dist/http/route_spec.js +130 -52
  195. package/dist/http/schema_helpers.d.ts +3 -2
  196. package/dist/http/schema_helpers.d.ts.map +1 -1
  197. package/dist/http/schema_helpers.js +9 -2
  198. package/dist/http/surface.d.ts +2 -1
  199. package/dist/http/surface.d.ts.map +1 -1
  200. package/dist/http/surface.js +1 -2
  201. package/dist/http/surface_query.d.ts +39 -35
  202. package/dist/http/surface_query.d.ts.map +1 -1
  203. package/dist/http/surface_query.js +79 -36
  204. package/dist/primitive_schemas.d.ts +39 -0
  205. package/dist/primitive_schemas.d.ts.map +1 -0
  206. package/dist/primitive_schemas.js +40 -0
  207. package/dist/realtime/sse_auth_guard.d.ts +5 -5
  208. package/dist/realtime/sse_auth_guard.js +9 -9
  209. package/dist/runtime/mock.d.ts +1 -1
  210. package/dist/runtime/mock.js +1 -1
  211. package/dist/server/app_backend.d.ts +14 -11
  212. package/dist/server/app_backend.d.ts.map +1 -1
  213. package/dist/server/app_backend.js +12 -8
  214. package/dist/server/app_server.d.ts +7 -7
  215. package/dist/server/app_server.d.ts.map +1 -1
  216. package/dist/server/app_server.js +36 -31
  217. package/dist/server/validate_nginx.d.ts +1 -1
  218. package/dist/server/validate_nginx.js +1 -1
  219. package/dist/testing/CLAUDE.md +73 -55
  220. package/dist/testing/admin_integration.d.ts +5 -6
  221. package/dist/testing/admin_integration.d.ts.map +1 -1
  222. package/dist/testing/admin_integration.js +100 -96
  223. package/dist/testing/adversarial_headers.js +1 -1
  224. package/dist/testing/app_server.d.ts +11 -14
  225. package/dist/testing/app_server.d.ts.map +1 -1
  226. package/dist/testing/app_server.js +18 -17
  227. package/dist/testing/assertions.d.ts.map +1 -1
  228. package/dist/testing/assertions.js +2 -1
  229. package/dist/testing/attack_surface.d.ts.map +1 -1
  230. package/dist/testing/attack_surface.js +15 -9
  231. package/dist/testing/audit_completeness.d.ts +2 -2
  232. package/dist/testing/audit_completeness.d.ts.map +1 -1
  233. package/dist/testing/audit_completeness.js +53 -39
  234. package/dist/testing/auth_apps.d.ts +5 -4
  235. package/dist/testing/auth_apps.d.ts.map +1 -1
  236. package/dist/testing/auth_apps.js +28 -22
  237. package/dist/testing/data_exposure.d.ts.map +1 -1
  238. package/dist/testing/data_exposure.js +5 -5
  239. package/dist/testing/db.d.ts +1 -1
  240. package/dist/testing/db.d.ts.map +1 -1
  241. package/dist/testing/db.js +4 -4
  242. package/dist/testing/db_entities.d.ts +22 -0
  243. package/dist/testing/db_entities.d.ts.map +1 -0
  244. package/dist/testing/db_entities.js +28 -0
  245. package/dist/testing/entities.d.ts +10 -8
  246. package/dist/testing/entities.d.ts.map +1 -1
  247. package/dist/testing/entities.js +22 -18
  248. package/dist/testing/integration.d.ts.map +1 -1
  249. package/dist/testing/integration.js +13 -14
  250. package/dist/testing/integration_helpers.d.ts +8 -6
  251. package/dist/testing/integration_helpers.d.ts.map +1 -1
  252. package/dist/testing/integration_helpers.js +29 -23
  253. package/dist/testing/middleware.d.ts +15 -11
  254. package/dist/testing/middleware.d.ts.map +1 -1
  255. package/dist/testing/middleware.js +75 -32
  256. package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
  257. package/dist/testing/rpc_attack_surface.js +40 -24
  258. package/dist/testing/rpc_helpers.d.ts.map +1 -1
  259. package/dist/testing/rpc_helpers.js +3 -1
  260. package/dist/testing/rpc_round_trip.d.ts +1 -1
  261. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  262. package/dist/testing/rpc_round_trip.js +14 -13
  263. package/dist/testing/sse_round_trip.d.ts +3 -4
  264. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  265. package/dist/testing/sse_round_trip.js +7 -11
  266. package/dist/testing/standard.d.ts +1 -1
  267. package/dist/testing/stubs.d.ts +25 -0
  268. package/dist/testing/stubs.d.ts.map +1 -1
  269. package/dist/testing/stubs.js +43 -2
  270. package/dist/testing/surface_invariants.d.ts +2 -2
  271. package/dist/testing/ws_round_trip.d.ts +12 -13
  272. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  273. package/dist/testing/ws_round_trip.js +24 -12
  274. package/dist/ui/AdminAccounts.svelte +23 -20
  275. package/dist/ui/AdminOverview.svelte +15 -13
  276. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  277. package/dist/ui/{AdminPermitHistory.svelte → AdminRoleGrantHistory.svelte} +12 -12
  278. package/dist/ui/AdminRoleGrantHistory.svelte.d.ts +4 -0
  279. package/dist/ui/AdminRoleGrantHistory.svelte.d.ts.map +1 -0
  280. package/dist/ui/BootstrapForm.svelte +1 -1
  281. package/dist/ui/CLAUDE.md +65 -59
  282. package/dist/ui/{PermitOfferForm.svelte → RoleGrantOfferForm.svelte} +37 -22
  283. package/dist/ui/RoleGrantOfferForm.svelte.d.ts +20 -0
  284. package/dist/ui/RoleGrantOfferForm.svelte.d.ts.map +1 -0
  285. package/dist/ui/{PermitOfferHistory.svelte → RoleGrantOfferHistory.svelte} +12 -12
  286. package/dist/ui/{PermitOfferHistory.svelte.d.ts → RoleGrantOfferHistory.svelte.d.ts} +4 -4
  287. package/dist/ui/RoleGrantOfferHistory.svelte.d.ts.map +1 -0
  288. package/dist/ui/{PermitOfferInbox.svelte → RoleGrantOfferInbox.svelte} +14 -14
  289. package/dist/ui/{PermitOfferInbox.svelte.d.ts → RoleGrantOfferInbox.svelte.d.ts} +4 -4
  290. package/dist/ui/RoleGrantOfferInbox.svelte.d.ts.map +1 -0
  291. package/dist/ui/SignupForm.svelte +1 -1
  292. package/dist/ui/SurfaceExplorer.svelte +35 -15
  293. package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
  294. package/dist/ui/account_sessions_state.svelte.d.ts +2 -3
  295. package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
  296. package/dist/ui/account_sessions_state.svelte.js +2 -3
  297. package/dist/ui/admin_accounts_state.svelte.d.ts +25 -18
  298. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  299. package/dist/ui/admin_accounts_state.svelte.js +28 -17
  300. package/dist/ui/admin_rpc_adapters.d.ts +20 -20
  301. package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
  302. package/dist/ui/admin_rpc_adapters.js +17 -17
  303. package/dist/ui/admin_sessions_state.svelte.d.ts +2 -2
  304. package/dist/ui/admin_sessions_state.svelte.js +2 -2
  305. package/dist/ui/audit_log_state.svelte.d.ts +7 -7
  306. package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
  307. package/dist/ui/audit_log_state.svelte.js +6 -6
  308. package/dist/ui/auth_state.svelte.d.ts +3 -3
  309. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  310. package/dist/ui/auth_state.svelte.js +6 -6
  311. package/dist/ui/format_scope.d.ts +2 -2
  312. package/dist/ui/format_scope.js +2 -2
  313. package/dist/ui/{permit_offers_state.svelte.d.ts → role_grant_offers_state.svelte.d.ts} +39 -31
  314. package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -0
  315. package/dist/ui/{permit_offers_state.svelte.js → role_grant_offers_state.svelte.js} +25 -19
  316. package/dist/ui/ui_format.js +2 -2
  317. package/package.json +3 -3
  318. package/dist/auth/permit_offer_action_specs.d.ts.map +0 -1
  319. package/dist/auth/permit_offer_action_specs.js +0 -227
  320. package/dist/auth/permit_offer_actions.d.ts +0 -110
  321. package/dist/auth/permit_offer_actions.d.ts.map +0 -1
  322. package/dist/auth/permit_offer_actions.js +0 -452
  323. package/dist/auth/permit_offer_notifications.d.ts.map +0 -1
  324. package/dist/auth/permit_offer_notifications.js +0 -182
  325. package/dist/auth/permit_offer_queries.d.ts +0 -183
  326. package/dist/auth/permit_offer_queries.d.ts.map +0 -1
  327. package/dist/auth/permit_offer_queries.js +0 -408
  328. package/dist/auth/permit_offer_schema.d.ts +0 -103
  329. package/dist/auth/permit_offer_schema.d.ts.map +0 -1
  330. package/dist/auth/permit_queries.d.ts +0 -210
  331. package/dist/auth/permit_queries.d.ts.map +0 -1
  332. package/dist/auth/permit_queries.js +0 -294
  333. package/dist/auth/require_keeper.d.ts +0 -20
  334. package/dist/auth/require_keeper.d.ts.map +0 -1
  335. package/dist/auth/require_keeper.js +0 -35
  336. package/dist/auth/route_guards.d.ts +0 -21
  337. package/dist/auth/route_guards.d.ts.map +0 -1
  338. package/dist/auth/route_guards.js +0 -32
  339. package/dist/auth/session_lifecycle.d.ts +0 -37
  340. package/dist/auth/session_lifecycle.d.ts.map +0 -1
  341. package/dist/auth/session_lifecycle.js +0 -29
  342. package/dist/ui/AdminPermitHistory.svelte.d.ts +0 -4
  343. package/dist/ui/AdminPermitHistory.svelte.d.ts.map +0 -1
  344. package/dist/ui/PermitOfferForm.svelte.d.ts +0 -14
  345. package/dist/ui/PermitOfferForm.svelte.d.ts.map +0 -1
  346. package/dist/ui/PermitOfferHistory.svelte.d.ts.map +0 -1
  347. package/dist/ui/PermitOfferInbox.svelte.d.ts.map +0 -1
  348. package/dist/ui/permit_offers_state.svelte.d.ts.map +0 -1
@@ -1,22 +1,46 @@
1
1
  /**
2
- * Request context middleware and permit checking helpers.
2
+ * Request context middleware and role_grant checking helpers.
3
3
  *
4
- * Builds `{ account, actor, permits }` from a session cookie
5
- * for every authenticated request. Downstream handlers check
6
- * permits, never flags.
4
+ * Two-phase identity resolution:
7
5
  *
8
- * `build_request_context` is the shared helper used by session,
9
- * bearer, and daemon token middleware to resolve account → actor → permits.
10
- * `refresh_permits` reloads permits on an existing context.
6
+ * 1. **Authentication (middleware)** `create_request_context_middleware`,
7
+ * `bearer_auth`, and `daemon_token_middleware` validate the credential
8
+ * (session cookie, bearer token, daemon token) and set `c.var.account_id`
9
+ * + `c.var.credential_type` on the Hono context. They do not resolve
10
+ * an acting actor or load role_grants; `REQUEST_CONTEXT_KEY` stays null at
11
+ * this stage, so account-grain identity is the only thing known.
12
+ * 2. **Authorization (route-spec wrapper / RPC dispatcher)** — after input
13
+ * validation, the per-route layer inspects the route. If the input
14
+ * schema declared `acting?: ActingActor` (reference equality with the
15
+ * canonical `ActingActor` schema) or the auth requires role_grants
16
+ * (`role` / `keeper`), `apply_authorization_phase` resolves the actor
17
+ * against `c.var.account_id` plus the validated `acting` value via
18
+ * `resolve_acting_actor`, builds the `{account, actor, role_grants}`
19
+ * context via `build_request_context`, and sets it on
20
+ * `REQUEST_CONTEXT_KEY` before auth guards fire. Authenticated routes
21
+ * that don't need an actor still get an account-only context via
22
+ * `build_account_context` so handler signatures stay uniform.
23
+ *
24
+ * Account-grain operations (logout, password_change, account_verify,
25
+ * etc.) declare neither `acting` nor role_grant-requiring auth, so no actor
26
+ * is resolved and their handlers see a `RequestContext` with
27
+ * `actor: null` + empty `role_grants`. They never trigger `actor_required`,
28
+ * which is what makes multi-actor logout work without first picking a
29
+ * persona.
30
+ *
31
+ * `build_request_context` loads `account → actor → role_grants` and verifies
32
+ * the `actor.account_id === account.id` binding. `refresh_role_grants`
33
+ * reloads role_grants on an existing context.
11
34
  *
12
35
  * @module
13
36
  */
14
- import { is_permit_active } from './account_schema.js';
37
+ import { is_role_grant_active } from './account_schema.js';
15
38
  import { hash_session_token, session_touch_fire_and_forget, query_session_get_valid, } from './session_queries.js';
16
- import { query_actor_by_account, query_account_by_id } from './account_queries.js';
17
- import { query_permit_find_active_for_actor } from './permit_queries.js';
18
- import { AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY } from '../hono_context.js';
19
- import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, } from '../http/error_schemas.js';
39
+ import { query_account_by_id, query_actor_by_id, query_actors_by_account, } from './account_queries.js';
40
+ import { query_role_grant_find_active_for_actor } from './role_grant_queries.js';
41
+ import { ACCOUNT_ID_KEY, AUTH_API_TOKEN_ID_KEY, CREDENTIAL_TYPE_KEY, TEST_CONTEXT_PRESET_KEY, } from '../hono_context.js';
42
+ import { is_public_auth, needs_actor } from '../http/auth_shape.js';
43
+ import { ERROR_AUTHENTICATION_REQUIRED, ERROR_INSUFFICIENT_PERMISSIONS, ERROR_CREDENTIAL_TYPE_REQUIRED, ERROR_ACTOR_REQUIRED, ERROR_ACTOR_NOT_ON_ACCOUNT, ERROR_NO_ACTORS_ON_ACCOUNT, ERROR_ACCOUNT_VANISHED, } from '../http/error_schemas.js';
20
44
  /** Hono context variable name for the request context. */
21
45
  export const REQUEST_CONTEXT_KEY = 'request_context';
22
46
  /**
@@ -41,25 +65,26 @@ export const get_request_context = (c) => {
41
65
  /**
42
66
  * Get the request context, throwing if unauthenticated.
43
67
  *
44
- * Use in route handlers where auth middleware guarantees a context exists
45
- * (i.e., routes with `auth: {type: 'authenticated'}` or stricter).
46
- * Prefer this over `get_request_context(c)!` for explicit error handling.
68
+ * Use in route handlers where the dispatcher's authorization phase guarantees
69
+ * a context exists (i.e., routes with `auth: {type: 'authenticated'}` or
70
+ * stricter). Prefer this over `get_request_context(c)!` for explicit error
71
+ * handling.
47
72
  *
48
73
  * @param c - the Hono context
49
74
  * @returns the request context (never null)
50
- * @throws Error if no request context is set (middleware misconfiguration)
75
+ * @throws Error if no request context is set (dispatcher misconfiguration)
51
76
  */
52
77
  export const require_request_context = (c) => {
53
78
  const ctx = get_request_context(c);
54
79
  if (!ctx) {
55
- throw new Error('require_request_context: no request context — is auth middleware applied?');
80
+ throw new Error('require_request_context: no request context — is the dispatcher authorization phase wired?');
56
81
  }
57
82
  return ctx;
58
83
  };
59
84
  /**
60
- * Check if a request context has an active permit for a given role.
85
+ * Check if a request context has an active role_grant for a given role.
61
86
  *
62
- * Checks the permits already loaded in the context (no DB query).
87
+ * Checks the role_grants already loaded in the context (no DB query).
63
88
  * Null-tolerant — `null` ctx (unauthenticated) returns `false`. Symmetric
64
89
  * with `has_scoped_role` / `has_any_scoped_role` so the three helpers
65
90
  * compose freely in the same predicate (e.g.
@@ -68,43 +93,47 @@ export const require_request_context = (c) => {
68
93
  * @param ctx - the request context, or `null` for unauthenticated callers
69
94
  * @param role - the role to check
70
95
  * @param now - current time (defaults to `new Date()`, pass for testability and hot-path efficiency)
71
- * @returns `true` if the actor has an active permit for the role
96
+ * @returns `true` if the actor has an active role_grant for the role
72
97
  */
73
- export const has_role = (ctx, role, now = new Date()) => ctx?.permits.some((p) => p.role === role && is_permit_active(p, now)) ?? false;
98
+ export const has_role = (ctx, role, now = new Date()) => ctx?.role_grants.some((p) => p.role === role && is_role_grant_active(p, now)) ?? false;
74
99
  /**
75
- * Whether the request context holds an active permit for `role` at `scope_id`.
100
+ * Whether the request context holds an active role_grant for `role` at `scope_id`.
76
101
  *
77
- * Walks the in-memory `ctx.permits` snapshot loaded once per request by
78
- * `create_request_context_middleware`; zero DB roundtrip per check. The
79
- * "freshness" framing of a SQL re-query is illusory because the race window
80
- * is between predicate and the actual mutation, not predicate and middleware
81
- * load. Closing that race needs a transactional re-check inside the
82
- * UPDATE/INSERT, which neither style provides.
102
+ * Walks the in-memory `ctx.role_grants` snapshot loaded once per request by
103
+ * the route-spec / RPC dispatcher's authorization phase (when the route
104
+ * declares `acting?: ActingActor` or has role_grant-requiring auth); zero DB
105
+ * roundtrip per check. The "freshness" framing of a SQL re-query is
106
+ * illusory because the race window is between predicate and the actual
107
+ * mutation, not predicate and authorization load. Closing that race needs
108
+ * a transactional re-check inside the UPDATE/INSERT, which neither style
109
+ * provides.
83
110
  *
84
- * Null-tolerant — `null` ctx (unauthenticated) returns `false`. Same
85
- * convention as `has_role`; lets the helper drop into `auth: 'public'`
86
- * handlers without a manual narrow. See `cell_authorize` for the
87
- * resource-side analog.
111
+ * Null-tolerant — `null` ctx (unauthenticated) and account-grain
112
+ * contexts (`actor: null`, empty `role_grants`) both return `false`. Same
113
+ * convention as `has_role`; lets the helper drop into public
114
+ * (`{account: 'none', actor: 'none'}`) and account-grain
115
+ * (`{account: 'required', actor: 'none'}`) handlers without a manual
116
+ * narrow. See `cell_authorize` for the resource-side analog.
88
117
  *
89
- * `scope_id` semantics: in-memory `permit.scope_id` is `string | null`, so
118
+ * `scope_id` semantics: in-memory `role_grant.scope_id` is `string | null`, so
90
119
  * JS `===` matches the SQL `IS NOT DISTINCT FROM` semantics exactly:
91
120
  *
92
- * - `scope_id === null` matches global permits (`scope_id IS NULL`).
93
- * - `scope_id === '<uuid>'` matches permits bound to that exact scope.
121
+ * - `scope_id === null` matches global role_grants (`scope_id IS NULL`).
122
+ * - `scope_id === '<uuid>'` matches role_grants bound to that exact scope.
94
123
  *
95
124
  * @param ctx - the request context, or `null` for unauthenticated callers
96
125
  * @param role - the role to check
97
126
  * @param scope_id - the scope to check (`null` for global)
98
127
  * @param now - current time (defaults to `new Date()`, pass for testability and hot-path efficiency)
99
- * @returns `true` iff the actor holds an active permit for the role at the requested scope
128
+ * @returns `true` iff the actor holds an active role_grant for the role at the requested scope
100
129
  */
101
130
  export const has_scoped_role = (ctx, role, scope_id, now = new Date()) => {
102
131
  if (!ctx)
103
132
  return false;
104
- return ctx.permits.some((p) => p.role === role && p.scope_id === scope_id && is_permit_active(p, now));
133
+ return ctx.role_grants.some((p) => p.role === role && p.scope_id === scope_id && is_role_grant_active(p, now));
105
134
  };
106
135
  /**
107
- * Whether the request context holds an active permit for any role in `roles`
136
+ * Whether the request context holds an active role_grant for any role in `roles`
108
137
  * at `scope_id`. Empty `roles` short-circuits to `false` — documents intent
109
138
  * at the call site ("zero roles trivially admit no-one"). Same scope and
110
139
  * null-tolerance semantics as `has_scoped_role`.
@@ -113,65 +142,95 @@ export const has_scoped_role = (ctx, role, scope_id, now = new Date()) => {
113
142
  * @param roles - the roles that would admit the caller (any-of)
114
143
  * @param scope_id - the scope to check (`null` for global)
115
144
  * @param now - current time (defaults to `new Date()`, pass for testability)
116
- * @returns `true` iff the actor holds an active permit for any role in `roles` at the requested scope
145
+ * @returns `true` iff the actor holds an active role_grant for any role in `roles` at the requested scope
117
146
  */
118
147
  export const has_any_scoped_role = (ctx, roles, scope_id, now = new Date()) => {
119
148
  if (!ctx)
120
149
  return false;
121
150
  if (roles.length === 0)
122
151
  return false;
123
- return ctx.permits.some((p) => roles.includes(p.role) && p.scope_id === scope_id && is_permit_active(p, now));
152
+ return ctx.role_grants.some((p) => roles.includes(p.role) && p.scope_id === scope_id && is_role_grant_active(p, now));
153
+ };
154
+ /**
155
+ * Resolve the acting actor for an authenticated request.
156
+ *
157
+ * Called from the route-spec / RPC dispatcher's authorization phase
158
+ * with the authenticated account id and the validated `acting` value
159
+ * (from the request payload). Applies the uniform resolution rules:
160
+ *
161
+ * - `acting_actor_id` omitted + 1 actor → use it.
162
+ * - `acting_actor_id` omitted + 0 actors → `no_actors` (defensive —
163
+ * signup / bootstrap always create an actor in the same tx, so this
164
+ * is a server error).
165
+ * - `acting_actor_id` omitted + multiple actors → `actor_required` with
166
+ * the available list so the client can prompt; never pick silently.
167
+ * - `acting_actor_id` present + matches an actor on the account → use it.
168
+ * - `acting_actor_id` present + does not match → `actor_not_on_account`.
169
+ * The available list is intentionally not echoed in this branch (treat
170
+ * as opaque rejection).
171
+ *
172
+ * @param deps - query dependencies
173
+ * @param account_id - the authenticated account
174
+ * @param acting_actor_id - the requested acting actor id, or `undefined`
175
+ */
176
+ export const resolve_acting_actor = async (deps, account_id, acting_actor_id) => {
177
+ const actors = await query_actors_by_account(deps, account_id);
178
+ if (actors.length === 0)
179
+ return { ok: false, reason: 'no_actors' };
180
+ if (acting_actor_id == null) {
181
+ if (actors.length === 1)
182
+ return { ok: true, actor_id: actors[0].id };
183
+ return {
184
+ ok: false,
185
+ reason: 'actor_required',
186
+ available: actors.map((a) => ({ id: a.id, name: a.name })),
187
+ };
188
+ }
189
+ const match = actors.find((a) => a.id === acting_actor_id);
190
+ if (!match)
191
+ return { ok: false, reason: 'actor_not_on_account' };
192
+ return { ok: true, actor_id: match.id };
124
193
  };
125
194
  /**
126
- * Create middleware that builds the request context from a session cookie.
195
+ * Create middleware that authenticates the account from a session cookie.
127
196
  *
128
- * Reads the session identity (set by session middleware), looks up
129
- * the `auth_session`, loads account + actor + active permits, and
130
- * sets the `RequestContext` on the Hono context.
197
+ * Reads the session identity (set by session middleware), looks up the
198
+ * `auth_session`, and on a valid session sets `c.var.auth_account_id`,
199
+ * `CREDENTIAL_TYPE_KEY = 'session'`, and `AUTH_SESSION_TOKEN_HASH_KEY`.
200
+ * Touches the session (fire-and-forget). Does not load actor or role_grants;
201
+ * `REQUEST_CONTEXT_KEY` is left null — the route-spec / RPC dispatcher
202
+ * authorization phase resolves the acting actor and builds the full
203
+ * `RequestContext` when the route needs one.
131
204
  *
132
- * If the session is invalid or the account is not found, the context
133
- * is set to `null` (unauthenticated). No 401 is returned — use
134
- * `require_role` or `require_auth` for enforcement.
205
+ * Invalid / missing session leaves all keys null and calls `next()`
206
+ * `require_auth` / `require_role` enforce.
135
207
  *
136
208
  * @param deps - query dependencies (pool-level db for middleware)
137
209
  * @param log - the logger instance
138
210
  * @param session_context_key - the Hono context key where session middleware stored the session token
139
- * @mutates Hono context - sets `REQUEST_CONTEXT_KEY`, `CREDENTIAL_TYPE_KEY`, `AUTH_SESSION_TOKEN_HASH_KEY`, and `AUTH_API_TOKEN_ID_KEY`
211
+ * @mutates Hono context - sets `ACCOUNT_ID_KEY`, `CREDENTIAL_TYPE_KEY`, `AUTH_SESSION_TOKEN_HASH_KEY`, and `AUTH_API_TOKEN_ID_KEY`
140
212
  */
141
213
  export const create_request_context_middleware = (deps, log, session_context_key = 'auth_session_id') => {
142
214
  return async (c, next) => {
215
+ c.set(REQUEST_CONTEXT_KEY, null);
216
+ c.set(ACCOUNT_ID_KEY, null);
217
+ c.set(CREDENTIAL_TYPE_KEY, null);
218
+ c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
219
+ c.set(AUTH_API_TOKEN_ID_KEY, null);
143
220
  const session_token = c.get(session_context_key) ?? null;
144
221
  if (!session_token) {
145
- c.set(REQUEST_CONTEXT_KEY, null);
146
- c.set(CREDENTIAL_TYPE_KEY, null);
147
- c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
148
- c.set(AUTH_API_TOKEN_ID_KEY, null);
149
222
  await next();
150
223
  return;
151
224
  }
152
225
  const token_hash = hash_session_token(session_token);
153
226
  const session = await query_session_get_valid(deps, token_hash);
154
227
  if (!session) {
155
- c.set(REQUEST_CONTEXT_KEY, null);
156
- c.set(CREDENTIAL_TYPE_KEY, null);
157
- c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
158
- c.set(AUTH_API_TOKEN_ID_KEY, null);
159
- await next();
160
- return;
161
- }
162
- const ctx = await build_request_context(deps, session.account_id);
163
- if (!ctx) {
164
- c.set(REQUEST_CONTEXT_KEY, null);
165
- c.set(CREDENTIAL_TYPE_KEY, null);
166
- c.set(AUTH_SESSION_TOKEN_HASH_KEY, null);
167
- c.set(AUTH_API_TOKEN_ID_KEY, null);
168
228
  await next();
169
229
  return;
170
230
  }
171
- c.set(REQUEST_CONTEXT_KEY, ctx);
231
+ c.set(ACCOUNT_ID_KEY, session.account_id);
172
232
  c.set(CREDENTIAL_TYPE_KEY, 'session');
173
233
  c.set(AUTH_SESSION_TOKEN_HASH_KEY, token_hash);
174
- c.set(AUTH_API_TOKEN_ID_KEY, null);
175
234
  // Touch session (fire-and-forget, don't block the request)
176
235
  void session_touch_fire_and_forget(deps, token_hash, c.var.pending_effects, log);
177
236
  await next();
@@ -180,71 +239,295 @@ export const create_request_context_middleware = (deps, log, session_context_key
180
239
  /**
181
240
  * Middleware that requires authentication.
182
241
  *
183
- * Returns 401 if no request context is set.
242
+ * Returns 401 if the auth middleware did not set `c.var.auth_account_id`.
184
243
  */
185
244
  export const require_auth = async (c, next) => {
186
- const ctx = get_request_context(c);
187
- if (!ctx) {
245
+ if (c.get(ACCOUNT_ID_KEY) == null) {
188
246
  return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
189
247
  }
190
248
  await next();
191
249
  };
192
250
  /**
193
- * Create middleware that requires a specific role.
251
+ * Create middleware that requires the actor to hold any of the given
252
+ * roles globally (`scope_id IS NULL`).
253
+ *
254
+ * Returns 401 if unauthenticated, 403 if none of the roles are present.
255
+ * Reads `REQUEST_CONTEXT_KEY` because role-gated routes always run the
256
+ * dispatcher's authorization phase before this guard (the phase sets
257
+ * the actor-bound `RequestContext`).
194
258
  *
195
- * Returns 401 if unauthenticated, 403 if the role is missing.
259
+ * Uses `has_any_scoped_role(ctx, roles, null)` so the gate matches
260
+ * **global / unscoped role_grants only**. A scoped role_grant
261
+ * (`{role: 'admin', scope_id: <some uuid>}`) does not unlock route-spec
262
+ * gates that are inherently global. The same scope-aware check is
263
+ * mirrored in `actions/action_rpc.ts` (HTTP RPC dispatcher) and
264
+ * `actions/register_action_ws.ts` (WS dispatcher) so all three
265
+ * transports agree.
196
266
  *
197
- * @param role - the required role
267
+ * Multi-role disjunction (any-of) lets `auth.roles: ['admin', 'steward']`
268
+ * specs translate to one middleware that admits either role. Single-role
269
+ * routes pass `[role_name]`; the array shape is uniform.
270
+ *
271
+ * @param roles - the roles to admit (any-of)
198
272
  */
199
- export const require_role = (role) => {
273
+ export const require_role = (roles) => {
200
274
  return async (c, next) => {
275
+ if (c.get(ACCOUNT_ID_KEY) == null) {
276
+ return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
277
+ }
201
278
  const ctx = get_request_context(c);
202
- if (!ctx) {
279
+ if (!ctx || !has_any_scoped_role(ctx, roles, null)) {
280
+ return c.json({ error: ERROR_INSUFFICIENT_PERMISSIONS, required_roles: roles }, 403);
281
+ }
282
+ await next();
283
+ };
284
+ };
285
+ /**
286
+ * Create middleware that requires the request's `credential_type` to be
287
+ * one of the given values.
288
+ *
289
+ * Returns 401 if unauthenticated, 403 with
290
+ * `ERROR_CREDENTIAL_TYPE_REQUIRED` + `required_credential_types` echoing
291
+ * the spec's allowlist when the wire-side credential isn't in it.
292
+ * Body shape is symmetric with the role gate (`ERROR_INSUFFICIENT_PERMISSIONS` +
293
+ * `required_roles`) and matches what the RPC dispatcher's post-auth
294
+ * gate emits for the same condition. Today's only credential gate is
295
+ * keeper (`['daemon_token']`); future gates (`agent_token`,
296
+ * `group_actor_token`) reuse this literal and label themselves through
297
+ * the array.
298
+ *
299
+ * @param credential_types - allowed credential types (any-of)
300
+ */
301
+ export const require_credential_types = (credential_types) => {
302
+ return async (c, next) => {
303
+ if (c.get(ACCOUNT_ID_KEY) == null) {
203
304
  return c.json({ error: ERROR_AUTHENTICATION_REQUIRED }, 401);
204
305
  }
205
- if (!has_role(ctx, role)) {
206
- return c.json({ error: ERROR_INSUFFICIENT_PERMISSIONS, required_role: role }, 403);
306
+ const credential_type = c.get(CREDENTIAL_TYPE_KEY) ?? null;
307
+ if (!credential_type || !credential_types.includes(credential_type)) {
308
+ return c.json({
309
+ error: ERROR_CREDENTIAL_TYPE_REQUIRED,
310
+ required_credential_types: credential_types,
311
+ }, 403);
207
312
  }
208
313
  await next();
209
314
  };
210
315
  };
211
316
  /**
212
- * Reload active permits from the database, returning a new request context.
317
+ * Reload active role_grants from the database, returning a new request context.
213
318
  *
214
- * Useful for long-lived WebSocket connections where permits may change
319
+ * Useful for long-lived WebSocket connections where role_grants may change
215
320
  * (grant or revoke) during the connection lifetime. Call periodically
216
321
  * or after receiving a revocation signal.
217
322
  *
218
- * Returns a new `RequestContext` with updated permits — the original
219
- * context is not mutated, making concurrent calls safe.
323
+ * Returns a new `RequestContext` with updated role_grants — the original
324
+ * context is not mutated, making concurrent calls safe. Throws when
325
+ * `ctx.actor` is null; account-grain contexts have no role_grants to refresh.
220
326
  *
221
327
  * @param ctx - the request context to refresh
222
328
  * @param deps - query dependencies
223
- * @returns a new `RequestContext` with fresh permits
329
+ * @returns a new `RequestContext` with fresh role_grants
330
+ * @throws Error when called on an account-grain context (`actor: null`)
224
331
  */
225
- export const refresh_permits = async (ctx, deps) => {
226
- const permits = await query_permit_find_active_for_actor(deps, ctx.actor.id);
227
- return { ...ctx, permits };
332
+ export const refresh_role_grants = async (ctx, deps) => {
333
+ if (!ctx.actor) {
334
+ throw new Error('refresh_role_grants: account-grain context has no actor / role_grants to refresh');
335
+ }
336
+ const role_grants = await query_role_grant_find_active_for_actor(deps, ctx.actor.id);
337
+ return { ...ctx, role_grants };
228
338
  };
229
339
  /**
230
- * Build a full `RequestContext` from an account id.
340
+ * Build a full `RequestContext` from an account id and an explicit
341
+ * actor id (already resolved via `resolve_acting_actor`).
342
+ *
343
+ * Loads `account` + the named `actor` + the actor's active role_grants.
344
+ * Verifies the `actor.account_id === account.id` binding so downstream
345
+ * handlers can trust `ctx.actor.account_id === ctx.account.id`. Returns
346
+ * `null` when the account is missing, the actor is missing, or the
347
+ * actor doesn't belong to the supplied account.
231
348
  *
232
- * Shared helper used by session, bearer, and daemon token middleware,
233
- * as well as WebSocket upgrade handlers. Does the account actor → permits
234
- * lookup pipeline and returns the composed context, or `null` if
235
- * the account or actor is not found.
349
+ * Called by the route-spec / RPC dispatcher's authorization phase for
350
+ * routes that need an acting actor; account-grain routes use
351
+ * `build_account_context` instead.
236
352
  *
237
353
  * @param deps - query dependencies
238
354
  * @param account_id - the account to build context for
239
- * @returns a request context, or `null` if account/actor not found
355
+ * @param actor_id - the actor this request acts as
356
+ * @returns a request context, or `null` if account/actor not found or mismatched
240
357
  */
241
- export const build_request_context = async (deps, account_id) => {
358
+ export const build_request_context = async (deps, account_id, actor_id) => {
242
359
  const account = await query_account_by_id(deps, account_id);
243
360
  if (!account)
244
361
  return null;
245
- const actor = await query_actor_by_account(deps, account.id);
362
+ const actor = await query_actor_by_id(deps, actor_id);
246
363
  if (!actor)
247
364
  return null;
248
- const permits = await query_permit_find_active_for_actor(deps, actor.id);
249
- return { account, actor, permits };
365
+ if (actor.account_id !== account.id)
366
+ return null;
367
+ const role_grants = await query_role_grant_find_active_for_actor(deps, actor.id);
368
+ return { account, actor, role_grants };
369
+ };
370
+ /**
371
+ * Build an account-only `RequestContext` (no actor, no role_grants) from
372
+ * an account id.
373
+ *
374
+ * Used by the dispatcher's authorization phase for authenticated routes
375
+ * that don't need an acting actor — account-grain operations (logout,
376
+ * password change, account self-service). Lets handlers read
377
+ * `auth.account.id` / `auth.account.username` uniformly with role_grant-bound
378
+ * routes; the cost is one extra `query_account_by_id` per request.
379
+ *
380
+ * Returns `null` when the account row is missing (e.g. deleted between
381
+ * the auth middleware's session lookup and the dispatcher) — caller
382
+ * surfaces that as a 500 since it represents a torn read.
383
+ *
384
+ * @param deps - query dependencies
385
+ * @param account_id - the account to build context for
386
+ * @returns an account-only request context, or `null` if the account is missing
387
+ */
388
+ export const build_account_context = async (deps, account_id) => {
389
+ const account = await query_account_by_id(deps, account_id);
390
+ if (!account)
391
+ return null;
392
+ return { account, actor: null, role_grants: [] };
393
+ };
394
+ /**
395
+ * Apply the dispatcher's authorization phase against the flat-record
396
+ * `RouteAuth` shape. Shared by the route-spec wrapper, the HTTP RPC
397
+ * dispatcher, and the per-message WS dispatcher. Phase order:
398
+ * pre-validation 401 → input validation 400 → authorization phase →
399
+ * post-authorization 403.
400
+ *
401
+ * Pure data — the function does not touch a Hono context. Each transport
402
+ * passes `account_id` (extracted from its own credential surface) and
403
+ * binds the returned `AuthorizationResult` to its wire shape. The REST
404
+ * pipeline additionally writes `REQUEST_CONTEXT_KEY` on `c` for downstream
405
+ * `require_role` / `require_credential_types` middleware that still reads
406
+ * the resolved context off the Hono context.
407
+ *
408
+ * Branching by `auth.account` × `auth.actor`:
409
+ *
410
+ * - Both `'none'` → `{ok: true, request_context: null}`. Public actions
411
+ * never see a `RequestContext`.
412
+ * - `account_id == null` on any non-public route → same null
413
+ * `request_context`. The `'required'` callers were already rejected at
414
+ * the pre-validation gate in the dispatcher; only genuine anonymous
415
+ * access on an `'optional'` axis lands here.
416
+ * - `actor === 'none'` → builds account-only context via
417
+ * `build_account_context`. Null lookup → `account_vanished` 500 failure.
418
+ * - `actor === 'required'` → resolves the actor from `acting_value` (or
419
+ * single-actor account); failures map to 400 / 500.
420
+ * - `actor === 'optional'` → same as `'required'` except multi-actor
421
+ * accounts without an `acting` value fall back to account-only context
422
+ * (no `actor_required` 400). Bad `acting` ids still 400.
423
+ *
424
+ * 500 branches stay distinct: `ERROR_NO_ACTORS_ON_ACCOUNT` (signup
425
+ * invariant violation), `ERROR_ACCOUNT_VANISHED` (torn read after
426
+ * resolve).
427
+ */
428
+ export const apply_authorization_phase = async (deps, account_id, auth, acting_value) => {
429
+ if (is_public_auth(auth))
430
+ return { ok: true, request_context: null };
431
+ if (account_id == null) {
432
+ // Optional-auth route hit without a credential — leave `RequestContext`
433
+ // null so the handler can branch on it. `'required'` callers already
434
+ // got rejected at the pre-validation gate.
435
+ return { ok: true, request_context: null };
436
+ }
437
+ if (!needs_actor(auth)) {
438
+ const ctx = await build_account_context(deps, account_id);
439
+ if (!ctx)
440
+ return { ok: false, status: 500, body: { error: ERROR_ACCOUNT_VANISHED } };
441
+ return { ok: true, request_context: ctx };
442
+ }
443
+ // actor 'required' or 'optional' — resolve.
444
+ const acting = await resolve_acting_actor(deps, account_id, acting_value);
445
+ if (!acting.ok) {
446
+ if (acting.reason === 'actor_required') {
447
+ if (auth.actor === 'optional') {
448
+ // Multi-actor account, no pick — fall back to account-only context.
449
+ const ctx = await build_account_context(deps, account_id);
450
+ if (!ctx)
451
+ return { ok: false, status: 500, body: { error: ERROR_ACCOUNT_VANISHED } };
452
+ return { ok: true, request_context: ctx };
453
+ }
454
+ return {
455
+ ok: false,
456
+ status: 400,
457
+ body: { error: ERROR_ACTOR_REQUIRED, available: acting.available },
458
+ };
459
+ }
460
+ if (acting.reason === 'actor_not_on_account') {
461
+ return { ok: false, status: 400, body: { error: ERROR_ACTOR_NOT_ON_ACCOUNT } };
462
+ }
463
+ return { ok: false, status: 500, body: { error: ERROR_NO_ACTORS_ON_ACCOUNT } };
464
+ }
465
+ const ctx = await build_request_context(deps, account_id, acting.actor_id);
466
+ if (!ctx)
467
+ return { ok: false, status: 500, body: { error: ERROR_ACCOUNT_VANISHED } };
468
+ return { ok: true, request_context: ctx };
469
+ };
470
+ /**
471
+ * Create the route-spec authorization handler used by `apply_route_specs`.
472
+ *
473
+ * Reads `acting` off `c.var.validated_input` (or `c.var.validated_query`
474
+ * for GET routes) — input validation runs first, so the authorization
475
+ * phase consumes the typed Zod field instead of pre-parsing the body.
476
+ * Public routes (`auth.account === 'none' && auth.actor === 'none'`)
477
+ * skip the phase entirely.
478
+ *
479
+ * Per registry-time invariant 2, `auth.actor !== 'none'` ⟺ the input
480
+ * (or query) schema declares `acting?: ActingActor` — so reading from
481
+ * `c.var.validated_input.acting` / `c.var.validated_query.acting` is
482
+ * type-safe.
483
+ *
484
+ * Resolved contexts land on `REQUEST_CONTEXT_KEY` so the post-authorization
485
+ * REST middleware (`require_role`, `require_credential_types`) reads the
486
+ * actor-bound context off `c.var`. The HTTP RPC and WS dispatchers consume
487
+ * the `apply_authorization_phase` outcome directly without round-tripping
488
+ * through `c.var`.
489
+ */
490
+ export const create_fuz_authorization_handler = (deps) => {
491
+ return async (c, spec) => {
492
+ // Test escape hatch: harnesses that pre-populate `REQUEST_CONTEXT_KEY`
493
+ // flag `TEST_CONTEXT_PRESET_KEY = true` so the authorization phase
494
+ // trusts the supplied context instead of running DB-backed resolution.
495
+ // Production middleware never sets this flag.
496
+ if (c.get(TEST_CONTEXT_PRESET_KEY))
497
+ return;
498
+ if (is_public_auth(spec.auth))
499
+ return;
500
+ const acting_value = needs_actor(spec.auth) ? extract_validated_acting(c) : undefined;
501
+ const account_id = c.get(ACCOUNT_ID_KEY) ?? null;
502
+ const result = await apply_authorization_phase(deps, account_id, spec.auth, acting_value);
503
+ if (!result.ok)
504
+ return c.json(result.body, result.status);
505
+ if (result.request_context !== null) {
506
+ c.set(REQUEST_CONTEXT_KEY, result.request_context);
507
+ }
508
+ // `request_context: null` — public action or unauthenticated optional axis.
509
+ // Leave `REQUEST_CONTEXT_KEY` null; downstream `require_role` /
510
+ // `require_credential_types` enforce.
511
+ return;
512
+ };
513
+ };
514
+ /**
515
+ * Read `acting` off the validated input (or validated query) on the Hono
516
+ * context. Input/query validation runs before the authorization phase,
517
+ * so this reads a typed Zod field — not the raw body.
518
+ *
519
+ * Returns `undefined` when `validated_input` / `validated_query` isn't
520
+ * set or doesn't carry `acting`. Per registry-time invariant 2, the
521
+ * dispatcher only calls this when `auth.actor !== 'none'`, which by
522
+ * the biconditional means the input schema declares
523
+ * `acting?: ActingActor`.
524
+ */
525
+ const extract_validated_acting = (c) => {
526
+ const validated_input = c.get('validated_input');
527
+ if (validated_input && typeof validated_input.acting === 'string')
528
+ return validated_input.acting;
529
+ const validated_query = c.get('validated_query');
530
+ if (validated_query && typeof validated_query.acting === 'string')
531
+ return validated_query.acting;
532
+ return undefined;
250
533
  };