@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
@@ -11,11 +11,47 @@
11
11
  * @module
12
12
  */
13
13
  import { z } from 'zod';
14
- /** The credential types that can authenticate a request. */
15
- export const CREDENTIAL_TYPES = ['session', 'api_token', 'daemon_token'];
14
+ import { CREDENTIAL_TYPE_API_TOKEN, CREDENTIAL_TYPE_DAEMON_TOKEN, CREDENTIAL_TYPE_SESSION, } from './auth/credential_type_schema.js';
15
+ /**
16
+ * The credential types that can authenticate a request — the closed set
17
+ * of fuz_app builtins. The open registry on top
18
+ * (`create_credential_type_schema(consumer_types)`) is consulted at
19
+ * registry time by `create_role_schema` for `RoleSpec.required_credential_types`
20
+ * validation; the wire-validated `CredentialType` enum here stays
21
+ * narrow because middleware only ever sets one of the three builtins.
22
+ */
23
+ export const CREDENTIAL_TYPES = [
24
+ CREDENTIAL_TYPE_SESSION,
25
+ CREDENTIAL_TYPE_API_TOKEN,
26
+ CREDENTIAL_TYPE_DAEMON_TOKEN,
27
+ ];
16
28
  /** Credential type — how a request was authenticated. */
17
29
  export const CredentialType = z.enum(CREDENTIAL_TYPES);
18
30
  /** Hono context variable name for the credential type. */
19
31
  export const CREDENTIAL_TYPE_KEY = 'credential_type';
20
32
  /** Hono context variable name for the authenticated API token id. */
21
33
  export const AUTH_API_TOKEN_ID_KEY = 'auth_api_token_id';
34
+ /**
35
+ * Hono context variable name for the authenticated account id.
36
+ *
37
+ * Set by the auth middleware (session, bearer, or daemon token) on a valid
38
+ * credential. `null` for unauthenticated requests. The route-spec wrapper /
39
+ * RPC dispatcher's authorization phase reads this when resolving the acting
40
+ * actor; account-grain auth guards (`require_auth`) and account-grain handlers
41
+ * read it directly.
42
+ */
43
+ export const ACCOUNT_ID_KEY = 'auth_account_id';
44
+ /**
45
+ * Hono context variable name for the test-harness pre-baked context flag.
46
+ *
47
+ * Test harnesses (`create_test_app_from_specs`, `create_fake_hono_context`,
48
+ * the WS round-trip `connect()` helper, plus per-test middleware that
49
+ * pre-populates `REQUEST_CONTEXT_KEY`) set this to `true` so
50
+ * `apply_authorization_phase` skips its DB-backed actor resolution and
51
+ * trusts the supplied `RequestContext`. Production middleware never sets
52
+ * this key — only test code does. The flag is the explicit escape hatch
53
+ * that replaced the implicit "is `REQUEST_CONTEXT_KEY` already set?" probe,
54
+ * so that future production code consulting `REQUEST_CONTEXT_KEY` cannot
55
+ * silently bypass the live build.
56
+ */
57
+ export const TEST_CONTEXT_PRESET_KEY = 'test_context_preset';
@@ -14,22 +14,23 @@ see `../../docs/architecture.md`.
14
14
 
15
15
  ## Module Map
16
16
 
17
- | File | Role |
18
- | -------------------- | ------------------------------------------------------------------------- |
19
- | `route_spec.ts` | `RouteSpec` + `apply_route_specs`, validation pipeline, transactions |
20
- | `error_schemas.ts` | `ERROR_*` constants, standard error shapes, `derive_error_schemas` |
21
- | `schema_helpers.ts` | Shared Zod introspection (null/strict/surface/merge/middleware-applies) |
22
- | `middleware_spec.ts` | `MiddlewareSpec` interface |
23
- | `surface.ts` | `AppSurface`, `AppSurfaceSpec`, `generate_app_surface`, diagnostics |
24
- | `surface_query.ts` | Pure filters/groupings over `AppSurface` |
25
- | `proxy.ts` | Trusted-proxy middleware, CIDR parsing, rightmost-first XFF resolution |
26
- | `origin.ts` | Origin/Referer allowlist middleware with wildcard patterns |
27
- | `jsonrpc.ts` | JSON-RPC 2.0 envelope schemas (MCP superset), `JsonrpcErrorCode`, `_meta` |
28
- | `jsonrpc_errors.ts` | `ThrownJsonrpcError`, `jsonrpc_errors` throwers, HTTP-status mappings |
29
- | `jsonrpc_helpers.ts` | Message builders, type guards, input/result normalizers |
30
- | `common_routes.ts` | Health check + authenticated server-status + surface route specs |
31
- | `db_routes.ts` | Generic keeper-only table browser route specs (public schema) |
32
- | `pending_effects.ts` | `emit_after_commit(ctx, fn)` + `PendingEffectsContext` |
17
+ | File | Role |
18
+ | -------------------- | ------------------------------------------------------------------------------------------------------ |
19
+ | `route_spec.ts` | `RouteSpec` + `apply_route_specs`, validation pipeline, transactions |
20
+ | `auth_shape.ts` | Canonical `RouteAuth` Zod schema + cross-axis invariants + predicates |
21
+ | `error_schemas.ts` | `ERROR_*` constants, standard error shapes, `derive_error_schemas` |
22
+ | `schema_helpers.ts` | Shared Zod introspection (null/strict/surface/merge/middleware-applies) |
23
+ | `middleware_spec.ts` | `MiddlewareSpec` interface |
24
+ | `surface.ts` | `AppSurface`, `AppSurfaceSpec`, `generate_app_surface`, diagnostics |
25
+ | `surface_query.ts` | Pure filters/groupings over `AppSurface` |
26
+ | `proxy.ts` | Trusted-proxy middleware, CIDR parsing, rightmost-first XFF resolution |
27
+ | `origin.ts` | Origin/Referer allowlist middleware with wildcard patterns |
28
+ | `jsonrpc.ts` | JSON-RPC 2.0 envelope schemas (MCP superset), `JsonrpcErrorCode`, `_meta` |
29
+ | `jsonrpc_errors.ts` | `ThrownJsonrpcError`, `jsonrpc_errors` throwers, HTTP-status mappings |
30
+ | `jsonrpc_helpers.ts` | Message builders, type guards, input/result normalizers |
31
+ | `common_routes.ts` | Health check + authenticated server-status + surface route specs |
32
+ | `db_routes.ts` | Generic keeper-only table browser route specs (public schema) |
33
+ | `pending_effects.ts` | `emit_after_commit` + `flush_pending_effects` + `flush_post_commit_effects` + `EmitAfterCommitContext` |
33
34
 
34
35
  ## Route Spec System
35
36
 
@@ -41,7 +42,7 @@ by `generate_app_surface`. Same-shaped data, different consumers.
41
42
 
42
43
  - `method` — `'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'`
43
44
  - `path` — Hono path (supports `:param` segments)
44
- - `auth: RouteAuth` — `{type: 'none' | 'authenticated' | 'role'; role} | {type: 'keeper'}`
45
+ - `auth: RouteAuth` — flat record `{account, actor, roles?, credential_types?}` from `auth_shape.ts`. Each axis is `'none' | 'optional' | 'required'`. Same shape governs `ActionSpec.auth` (see `../actions/CLAUDE.md`).
45
46
  - `handler: RouteHandler` — `(c: Context, route: RouteContext) => Response | Promise<Response>`
46
47
  - `description` — free-text, surfaced in `AppSurface`
47
48
  - `params?: z.ZodObject` — strict-object schema for URL path params
@@ -62,19 +63,31 @@ The second handler argument is always a `RouteContext`:
62
63
 
63
64
  ```typescript
64
65
  interface RouteContext {
65
- db: Db; // transaction-scoped for mutations, pool-level for reads
66
- background_db: Db; // always pool-level for fire-and-forget effects
67
- pending_effects: Array<Promise<void>>;
66
+ db: Db; // transaction-scoped when `transaction: true`, pool-level otherwise
67
+ pending_effects: Array<Promise<void>>; // eager pool writes already in flight
68
+ post_commit_effects: Array<() => void | Promise<void>>; // deferred — push via `emit_after_commit`
68
69
  }
69
70
  ```
70
71
 
71
72
  - **`route.db`** — use for the handler's main DB work. Wrapped in a transaction
72
- when `transaction: true` (the default for non-GET). Do NOT use for
73
- fire-and-forget effects that must outlive the transaction.
74
- - **`route.background_db`** — use for audit logs, session touches, token
75
- tracking. Always pool-level, never rolled back.
76
- - **`route.pending_effects`** push promises for post-response flushing.
77
- Prefer `emit_after_commit` from `pending_effects.ts` for WS fan-out.
73
+ when `transaction: true` (the default for non-GET); routes that opt out
74
+ (`transaction: false`, e.g. signup / bootstrap) get the pool here directly
75
+ and may call `route.db.transaction(...)` for their own scope.
76
+ - **`route.pending_effects`** direct push for eager fire-and-forget pool
77
+ writes (audit, session touch, api-token usage tracking). Push the in-flight
78
+ `Promise<void>` to register it for test-mode flushing.
79
+ - **`route.post_commit_effects`** — do not push directly; reach for
80
+ `emit_after_commit` from `pending_effects.ts`. The helper pushes a
81
+ thunk that the flush middleware invokes after the handler returns,
82
+ closing the microtask-ordering window that an eager
83
+ `Promise.resolve().then(fn)` leaves open inside the wrapping
84
+ `db.transaction`.
85
+
86
+ Pool-level fire-and-forget writes (audit logs, etc.) run through the bound
87
+ `AppDeps.audit` capability — see `../auth/CLAUDE.md` §Deps. Handlers that
88
+ need rollback-resilient writes call `deps.audit.emit(route, input)`, which
89
+ captures the pool inside the bound emitter so the row lands even when
90
+ the handler's transaction rolls back.
78
91
 
79
92
  ### Declarative transactions
80
93
 
@@ -91,27 +104,74 @@ wrapper). See `../auth/signup_routes.ts`.
91
104
 
92
105
  `apply_route_specs` assembles the following middleware chain per spec:
93
106
 
94
- 1. **Auth guards** — `resolve_auth_guards(spec.auth)`, injected via
95
- `AuthGuardResolver` (use `fuz_auth_guard_resolver` from `../auth/route_guards.ts`)
96
- 2. **Params validation** — `spec.params` → `validated_params` context var;
97
- mismatch returns 400 `ERROR_INVALID_ROUTE_PARAMS` with Zod `issues`
98
- 3. **Query validation** — `spec.query` → `validated_query`; mismatch returns
99
- 400 `ERROR_INVALID_QUERY_PARAMS`
100
- 4. **Input validation** JSON body parsed + validated; mismatch returns 400
101
- `ERROR_INVALID_JSON_BODY` (not JSON) or `ERROR_INVALID_REQUEST_BODY`
102
- (schema failure with `issues`). Skipped on GET and `z.null()` inputs
103
- 5. **Handler** wrapped in transaction when `use_transaction` (see above),
104
- receives `RouteContext`
105
- 6. **DEV-only output + error validation** — wraps the handler (see below)
106
- 7. **Error catch** — catches `ThrownJsonrpcError` maps to HTTP status +
107
- JSON-RPC error body; catches generic `Error` 500 `internal_error`
108
- (message only included in DEV)
107
+ 1. **Params validation** — `spec.params` `validated_params` context
108
+ var; mismatch returns 400 `ERROR_INVALID_ROUTE_PARAMS` with Zod
109
+ `issues`
110
+ 2. **Query validation** `spec.query` `validated_query`; mismatch
111
+ returns 400 `ERROR_INVALID_QUERY_PARAMS`
112
+ 3. **Pre-validation auth guards** — `require_auth` (401
113
+ `ERROR_AUTHENTICATION_REQUIRED`) when `auth.account === 'required'`
114
+ or `auth.actor === 'required'`. Fires before any body parsing so
115
+ unauthenticated callers never see route-shape information from
116
+ input parse failures. The `AuthGuardResolver` (e.g.
117
+ `fuz_auth_guard_resolver` from `../auth/auth_guard_resolver.ts`) returns
118
+ this set as `pre_validation: Array<MiddlewareHandler>`.
119
+ 4. **Input validation** — JSON body parsed + validated; mismatch returns
120
+ 400 `ERROR_INVALID_JSON_BODY` (not JSON) or `ERROR_INVALID_REQUEST_BODY`
121
+ (schema failure with `issues`). Skipped on GET and `z.null()` inputs.
122
+ The validated input lands on `c.var.validated_input` so the
123
+ authorization phase reads `acting` as a typed Zod field.
124
+ 5. **Authorization phase** — when `spec.auth.actor !== 'none'`,
125
+ resolves the acting actor against `c.var.account_id` (set by the
126
+ auth middleware) plus `validated_input.acting` (or
127
+ `validated_query.acting` for GET routes), builds `RequestContext`
128
+ via `build_request_context`, and sets `REQUEST_CONTEXT_KEY`. When
129
+ `auth.account !== 'none' && auth.actor === 'none'`, an account-only
130
+ context is built. Resolution failures return 400
131
+ `ERROR_ACTOR_REQUIRED` (with `available[]`) or
132
+ `ERROR_ACTOR_NOT_ON_ACCOUNT` (or 500 `ERROR_NO_ACTORS_ON_ACCOUNT`
133
+ on signup-invariant violation, 500 `ERROR_ACCOUNT_VANISHED` on
134
+ torn account/actor reads after a successful resolve). Public
135
+ routes (`account: 'none' && actor: 'none'`) skip this phase
136
+ entirely.
137
+ 6. **Post-authorization auth guards** — `require_credential_types(types)`
138
+ (403 `ERROR_CREDENTIAL_TYPE_REQUIRED` with `required_credential_types: ReadonlyArray<string>`)
139
+ fires first when `auth.credential_types?.length`; `require_role(roles)` (403
140
+ `ERROR_INSUFFICIENT_PERMISSIONS` with `required_roles: ReadonlyArray<string>`)
141
+ fires next when `auth.roles?.length`. Both read
142
+ `REQUEST_CONTEXT_KEY` populated by step 5. Multi-role specs admit
143
+ any-of via `has_any_scoped_role(ctx, roles, null)`.
144
+ 7. **Handler** — wrapped in transaction when `use_transaction` (see
145
+ above), receives `RouteContext`
146
+ 8. **DEV-only output + error validation** — wraps the handler (see below)
147
+ 9. **Error catch** — catches `ThrownJsonrpcError` → maps to HTTP status +
148
+ the flat REST `ApiError` body (`{error: <reason>, message?, ...rest_data}`);
149
+ catches generic `Error` → 500 `{error: 'internal_error', message?}`
150
+ (message only included in DEV). The reason string comes from
151
+ `err.data.reason` when set (consumer-supplied canonical reason
152
+ override) or from `jsonrpc_error_code_to_name(err.code)` (e.g.
153
+ `-32003 → 'not_found'`). The flat shape matches what middleware
154
+ and direct handlers emit (`c.json({error: ERROR_FOO}, status)`,
155
+ `c.json(failure.body, status)` from the dispatcher's authorization
156
+ phase) — REST callers see one envelope across every emit site, while
157
+ the JSON-RPC dispatcher keeps its own `{jsonrpc, id, error: {code,
158
+ message, data}}` envelope on the RPC mount
159
+
160
+ Ordering: **401 → 400 → 403 → handler**. Mirrors the RPC dispatcher
161
+ (`actions/action_rpc.ts`) so HTTP RPC and REST fail with the same
162
+ priority. The alternative (403-before-400) was rejected because
163
+ defense-in-depth via attack-surface obscurity is illusory when the
164
+ surface is published in `library.json` codegen anyway. The trade-off
165
+ is that an authenticated-but-unauthorized caller can distinguish 400
166
+ from 403.
109
167
 
110
168
  Duplicate `method path` pairs throw at registration.
111
169
 
112
- Validated values are accessed via `get_route_input<T>(c)`,
113
- `get_route_params<T>(c)`, `get_route_query<T>(c)` — typed helpers that
114
- read the `validated_*` context vars.
170
+ Validated values are accessed via `get_route_input(c, schema)`,
171
+ `get_route_params(c, schema)`, `get_route_query(c, schema)` — pass the
172
+ matching Zod schema and the return type infers as `z.infer<typeof
173
+ schema>`. Each helper also has a `<T>(c)` overload (no schema arg) for
174
+ callers who don't have the schema in scope.
115
175
 
116
176
  ### DEV-only output + error validation
117
177
 
@@ -147,16 +207,17 @@ Output Validation.
147
207
 
148
208
  - `ERROR_*` snake*case string constants — single source of truth; use
149
209
  `.literal(ERROR*\*)` in Zod schemas and inline checks in handlers
150
- - `ApiError`, `ValidationError`, `PermissionError`, `KeeperError`,
151
- `RateLimitError`, `PayloadTooLargeError`, `ForeignKeyError` — standard
152
- shapes
210
+ - `ApiError`, `ValidationError`, `PermissionError`,
211
+ `CredentialTypeRequiredError`, `RateLimitError`, `PayloadTooLargeError`,
212
+ `ForeignKeyError` — standard shapes
153
213
  - `RouteErrorSchemas = Partial<Record<number, z.ZodType>>`
154
214
  - `RateLimitKey = 'ip' | 'account' | 'both'`
155
215
 
156
216
  All standard shapes use `z.looseObject` — intentional because multiple
157
217
  producers (middleware + handler) can emit different extra fields at the
158
218
  same status code. The `error` string literal is the contract; extra keys
159
- (`required_role`, `retry_after`, `detail`) are diagnostic.
219
+ (`required_roles`, `required_credential_types`, `retry_after`, `detail`)
220
+ are diagnostic.
160
221
 
161
222
  Pair every schema with the `z.infer` type export (`export type ApiError = z.infer<typeof ApiError>`).
162
223
 
@@ -165,37 +226,71 @@ Pair every schema with the `z.infer` type export (`export type ApiError = z.infe
165
226
  `merge_error_schemas(spec, middleware_errors?)` (in `schema_helpers.ts`)
166
227
  merges three layers, later overrides earlier at the same status code:
167
228
 
168
- 1. **Derived** — from `derive_error_schemas(auth, has_input, has_params, has_query, rate_limit)`:
229
+ 1. **Derived** — from `derive_error_schemas({auth, has_input?, has_params?, has_query?, rate_limit?})`:
169
230
  - `has_input || has_params || has_query` → 400 `ValidationError`
170
- - `auth.type === 'authenticated'` → 401 `ApiError`
171
- - `auth.type === 'role'` → 401 `ApiError` + 403 `PermissionError`
172
- - `auth.type === 'keeper'` → 401 `ApiError` + 403 `KeeperError`
231
+ - `auth.account === 'required'` or `auth.actor === 'required'` → 401 `ApiError`
232
+ - `auth.roles?.length` → 403 `PermissionError` (carries `required_roles: ReadonlyArray<string>`)
233
+ - `auth.credential_types?.length` → 403 `CredentialTypeRequiredError`
234
+ (carries `required_credential_types: ReadonlyArray<string>` echoing
235
+ the spec's allowlist — symmetric with `PermissionError`'s
236
+ `required_roles`; literal is `ERROR_CREDENTIAL_TYPE_REQUIRED`; both
237
+ gates set yields a `z.union([PermissionError, CredentialTypeRequiredError])`)
173
238
  - `rate_limit` → 429 `RateLimitError`
239
+ - `auth.actor !== 'none'` → widens 400 to a union with `ActorRequiredError` /
240
+ `ActorNotOnAccountError` and adds 500 union of `NoActorsOnAccountError`
241
+ / `AccountVanishedError`. Mirrors what the dispatcher's authorization
242
+ phase actually emits on routes whose input declares `acting?: ActingActor`
243
+ (per registry-time invariant 2) — so DEV-mode error-schema validation in
244
+ `wrap_output_validation` doesn't reject the auth phase's body.
174
245
  2. **Middleware** — from `MiddlewareSpec.errors` that apply to the route's
175
246
  path (via `middleware_applies`)
176
247
  3. **Explicit** — `RouteSpec.errors` — always wins
177
248
 
178
249
  Routes typically only need `errors` for handler-specific codes (404, 409, 422).
179
250
 
251
+ Actor-failure folding reads `spec.auth.actor !== 'none'` directly —
252
+ per registry-time invariant 2 (`actor !== 'none' ⟺ input declares
253
+ acting?: ActingActor`), the auth-shape axis is the single source of
254
+ truth.
255
+
256
+ **Framework-emitted vs consumer-authored.** The error-schema derivation
257
+ above is sound because the framework authors the errors at fixed
258
+ middleware sites — 401 from `require_auth`, 400 from
259
+ `create_input_validation`, 403 from `require_role` /
260
+ `require_credential_types`, 429 from rate limiters. Auto-derivation
261
+ documents the framework's own emissions; consumers tighten via
262
+ `RouteSpec.errors` when their handler narrows the surface.
263
+
264
+ The same auto-derivation pattern is **not** appropriate for consumer-
265
+ authored inputs (or handler outputs). A consumer's spec declares the
266
+ exact `acting?: ActingActor` slot, and the framework reads it back via
267
+ reference-equality to drive the authorization phase — auto-extending
268
+ schemas at registration time would obscure the source of truth ("did
269
+ the spec declare this, or did the framework graft it on?") and quietly
270
+ shadow consumer fields named `acting` that aren't the canonical
271
+ `ActingActor`. The asymmetry is the design rule: derive what the
272
+ framework emits, never what the consumer authors. The keeper
273
+ `db_routes` bug (an early consumer registration failure caught by
274
+ invariant 2's throw) was the empirical confirmation.
275
+
180
276
  ### `ERROR_*` constants by category
181
277
 
182
278
  - **Validation**: `ERROR_INVALID_REQUEST_BODY`, `ERROR_INVALID_JSON_BODY`,
183
279
  `ERROR_INVALID_ROUTE_PARAMS`, `ERROR_INVALID_QUERY_PARAMS`
184
280
  - **Auth**: `ERROR_AUTHENTICATION_REQUIRED`, `ERROR_INSUFFICIENT_PERMISSIONS`,
185
- `ERROR_RATE_LIMIT_EXCEEDED`, `ERROR_INVALID_CREDENTIALS`,
186
- `ERROR_PAYLOAD_TOO_LARGE`
281
+ `ERROR_CREDENTIAL_TYPE_REQUIRED`, `ERROR_RATE_LIMIT_EXCEEDED`,
282
+ `ERROR_INVALID_CREDENTIALS`, `ERROR_PAYLOAD_TOO_LARGE`
187
283
  - **Origin + bearer**: `ERROR_FORBIDDEN_ORIGIN`, `ERROR_FORBIDDEN_REFERER`,
188
284
  `ERROR_BEARER_REJECTED_BROWSER`, `ERROR_INVALID_TOKEN`, `ERROR_ACCOUNT_NOT_FOUND`
189
- - **Keeper/daemon**: `ERROR_KEEPER_REQUIRES_DAEMON_TOKEN`,
190
- `ERROR_INVALID_DAEMON_TOKEN`, `ERROR_KEEPER_ACCOUNT_NOT_CONFIGURED`,
191
- `ERROR_KEEPER_ACCOUNT_NOT_FOUND`
285
+ - **Keeper/daemon**: `ERROR_INVALID_DAEMON_TOKEN`,
286
+ `ERROR_KEEPER_ACCOUNT_NOT_CONFIGURED`, `ERROR_KEEPER_ACCOUNT_NOT_FOUND`
192
287
  - **Bootstrap**: `ERROR_ALREADY_BOOTSTRAPPED`, `ERROR_TOKEN_FILE_MISSING`,
193
288
  `ERROR_BOOTSTRAP_NOT_CONFIGURED`
194
289
  - **Signup/invites**: `ERROR_NO_MATCHING_INVITE`, `ERROR_SIGNUP_CONFLICT`,
195
290
  `ERROR_INVITE_NOT_FOUND`, `ERROR_INVITE_MISSING_IDENTIFIER`,
196
291
  `ERROR_INVITE_DUPLICATE`, `ERROR_INVITE_ACCOUNT_EXISTS_USERNAME`,
197
292
  `ERROR_INVITE_ACCOUNT_EXISTS_EMAIL`
198
- - **Admin**: `ERROR_ROLE_NOT_WEB_GRANTABLE`, `ERROR_PERMIT_NOT_FOUND`,
293
+ - **Admin**: `ERROR_ROLE_NOT_WEB_GRANTABLE`, `ERROR_ROLE_GRANT_NOT_FOUND`,
199
294
  `ERROR_INVALID_EVENT_TYPE`
200
295
  - **DB browser**: `ERROR_FOREIGN_KEY_VIOLATION`, `ERROR_TABLE_NOT_FOUND`,
201
296
  `ERROR_TABLE_NO_PRIMARY_KEY`, `ERROR_ROW_NOT_FOUND`
@@ -225,7 +320,7 @@ Key helpers:
225
320
  `'*'`, exact, `'/api/*'` prefix (handles `prefix.slice(0, -1)` so
226
321
  `/api/*` also matches the bare `/api`)
227
322
  - `merge_error_schemas(spec, middleware_errors?)` — three-layer merge
228
- described above
323
+ described above.
229
324
 
230
325
  ## Surface Generation
231
326
 
@@ -263,8 +358,7 @@ Key helpers:
263
358
  — `description`, `sensitivity`, and probes `safeParse(undefined)` to
264
359
  detect `optional` + `has_default`
265
360
  - `events_to_surface(event_specs)` — SSE events surface as `{method, description, channel, params_schema}`
266
- - RPC methods surface through `map_action_auth` (from `actions/action_bridge.ts`;
267
- see `../actions/CLAUDE.md` §HTTP bridge) so `ActionAuth` translates to the shared `RouteAuth` shape
361
+ - RPC methods surface their `RouteAuth` directly — same shape on both `ActionSpec.auth` and `RouteSpec.auth`, no translation step.
268
362
 
269
363
  `create_app_surface_spec(options)` = `generate_app_surface(options)` plus
270
364
  the source specs, for tests that need to iterate over raw specs.
@@ -278,11 +372,21 @@ No side effects, no state — filters and groupings over `AppSurface`:
278
372
  - `filter_routes_by_prefix(prefix)` / `filter_routes_with_input` /
279
373
  `filter_routes_with_params` / `filter_routes_with_query` /
280
374
  `filter_mutation_routes` / `filter_rate_limited_routes`
281
- - `routes_by_auth_type(surface)` — `Map<'none' | 'authenticated' | 'keeper' | 'role:NAME', Array<AppSurfaceRoute>>`
375
+ - `routes_by_auth_type(surface)` — `Map<RouteAuthCategory, Array<AppSurfaceRoute>>` where `RouteAuthCategory = 'none' | 'authenticated' | 'optional' | 'keeper' | 'role:<name>' | 'other'`. Multi-role specs appear under each of their role buckets; the `'optional'` and `'other'` buckets exist for shapes that don't fit the four-axis categorical view.
282
376
  - `format_route_key(route)` → `'METHOD /path'`
283
377
  - `surface_auth_summary(surface)` — counts per auth type, roles broken
284
378
  out by name
285
379
 
380
+ The per-route auth predicates these filters compose over (`is_public_auth`,
381
+ `is_role_auth`, `is_credential_gated_auth`, `is_keeper_auth`,
382
+ `is_plain_authenticated_auth`, plus `needs_actor` / `needs_account`)
383
+ live in `auth_shape.ts` next to the canonical `RouteAuth` schema —
384
+ import them from there, not from this module. Same predicates back the
385
+ dispatcher's authorization phase, the route-spec auth-guard resolver,
386
+ `derive_error_schemas`'s actor-failure folding, and the testing
387
+ harnesses, so every consumer that branches on the four-axis shape
388
+ shares one source of truth.
389
+
286
390
  Consumer code (tests, attack-surface helpers, `SurfaceExplorer.svelte`)
287
391
  should reach for these rather than inlining `.filter` chains.
288
392
 
@@ -320,11 +424,27 @@ Must run **before** auth and rate-limiting middleware. See the root
320
424
  - `parse_proxy_entry(entry)` — accepts `'127.0.0.1'`, `'::1'`,
321
425
  `'10.0.0.0/8'`, `'fe80::/10'`. Throws on invalid IPs, NaN/negative/
322
426
  over-range prefix, non-network-aligned CIDRs, or bad input
323
- - `is_trusted_ip(ip, proxies)`normalizes before matching; skips
427
+ - **`validate_ip_strict(ip)`**defensive validator for any IP string
428
+ read from an untrusted source. Hono's `distinctRemoteAddr` is lax —
429
+ classifies anything-with-colons as `'IPv6'`, and
430
+ `convertIPv6ToBinary` silently accepts `'[::1]:8080'`, `'::1\n'`,
431
+ etc. as binary-valid IPv6. The two-layer check here (character-set
432
+ pre-filter + round-trip through `convertIPv*ToBinary`) closes both
433
+ holes: returns `'IPv4' | 'IPv6'` on a strictly-valid bare literal,
434
+ `undefined` on anything else.
435
+ - `is_trusted_ip(ip, proxies)` — normalizes before matching; uses
436
+ `validate_ip_strict` to reject malformed input up front (without it,
437
+ CIDR proxies would surface a 500 from a thrown
438
+ `convertIPv6ToBinary` on entries like `'203.0.113.1:8080'`); skips
324
439
  mismatched address families for CIDR matches
325
440
  - `resolve_client_ip(forwarded_for, proxies)` — walks **right-to-left**,
326
- skipping trusted entries. First untrusted wins. If all entries are
327
- trusted, returns the leftmost (edge case, likely misconfigured)
441
+ skipping trusted entries AND any entry that fails strict validation
442
+ (closes the rate-limit-key poisoning surface where an attacker who
443
+ controls XFF and transits through a trusted proxy could rotate
444
+ garbage strings to evade per-IP limits). First untrusted +
445
+ strictly-valid wins. If everything is trusted-or-malformed, returns
446
+ the leftmost strictly-valid entry, or `undefined` to let the
447
+ middleware fall back to the connection IP
328
448
  - `create_proxy_middleware(options)` + `create_proxy_middleware_spec(options)` —
329
449
  three-branch logic:
330
450
  1. No XFF → use connection IP directly
@@ -335,10 +455,11 @@ Must run **before** auth and rate-limiting middleware. See the root
335
455
  - `get_client_ip(c)` — returns `'unknown'` when the proxy middleware
336
456
  hasn't run
337
457
 
338
- Non-standard proxies that include ports in XFF entries (`203.0.113.1:8080`)
339
- fail `distinctRemoteAddr` and are treated as untrusted safe default but
340
- rate-limiting keys the port-suffixed string. nginx and cloud LBs don't do
341
- this.
458
+ Tradeoff for the strict validation: legitimate non-standard proxies
459
+ that include ports in XFF entries (`203.0.113.1:8080`) lose per-client
460
+ distinction in rate limiting and collapse to the proxy's connection
461
+ IP (one bucket for everyone behind that proxy). nginx + cloud LBs
462
+ don't include ports — bounded by operator configuration in practice.
342
463
 
343
464
  ### Origin/Referer allowlist — `origin.ts`
344
465
 
@@ -490,31 +611,87 @@ Converters:
490
611
 
491
612
  ## Pending Effects
492
613
 
493
- `emit_after_commit(ctx, fn)` in `pending_effects.ts` is the canonical
494
- post-commit fan-out helper. Used for WS sends (`NotificationSender.send_to_account`
495
- for permit-offer notifications — see `../auth/CLAUDE.md` §WS notifications) and any side effect that must run only
496
- after the transaction commits.
614
+ Two queues, one timing contract each:
497
615
 
498
616
  ```typescript
499
- interface PendingEffectsContext {
617
+ interface EmitAfterCommitContext {
500
618
  log: Logger;
501
- pending_effects: Array<Promise<void>>;
619
+ post_commit_effects: Array<() => void | Promise<void>>;
502
620
  }
503
621
 
504
- emit_after_commit(ctx, () => notification_sender.send_to_account(account_id, msg));
622
+ // `RouteContext` and `ActionContext` carry both:
623
+ // pending_effects: Array<Promise<void>>
624
+ // post_commit_effects: Array<() => void | Promise<void>>
505
625
  ```
506
626
 
507
- Key properties:
627
+ - **`pending_effects: Array<Promise<void>>`** — eager. Producers push the
628
+ in-flight `Promise<void>` for fire-and-forget pool writes already
629
+ running: audit emits via `AppDeps.audit`, session-touch UPDATE,
630
+ api-token usage tracking. The pool write is rollback-resilient by
631
+ virtue of running outside the request transaction; pushing the
632
+ in-flight handle lets test mode (`await_pending_effects: true`) await
633
+ it. Drain rule: `flush_pending_effects(effects, log, on_rejection?)`.
634
+ - **`post_commit_effects: Array<() => void | Promise<void>>`** —
635
+ deferred. Producers go through `emit_after_commit(ctx, fn)` exclusively;
636
+ raw thunks should not be pushed directly. The flush middleware (in
637
+ `server/app_server.ts` and the per-message WS dispatcher in
638
+ `actions/register_action_ws.ts`) is the only site that invokes each
639
+ thunk, after the wrapping `db.transaction` (and the rest of the
640
+ handler chain) has resolved. Drain rule: `flush_post_commit_effects(effects, log)`.
641
+
642
+ ### Why split
643
+
644
+ Both shapes used to coexist on a single `Array<PendingEffect>` discriminated
645
+ union. The shapes encode different contracts — eager pushers say "wait
646
+ for this work that's already started"; thunk pushers say "run this after
647
+ the handler returns" — and burying both behind one field made
648
+ `c.var.pending_effects.push(x)` ambiguous at the call site. Splitting
649
+ turns the field name into the contract.
650
+
651
+ ### Why `emit_after_commit` defers
652
+
653
+ The thunk shape is **load-bearing for correctness**. Pushing
654
+ `Promise.resolve().then(fn)` onto an eager queue — what
655
+ `emit_after_commit` used to do — schedules `fn` as a microtask that
656
+ drains _before_ the wrapping `await db.query('COMMIT')` resumes, so a
657
+ rolled-back transaction would leak a notification for state that never
658
+ landed. The thunk defers the work to flush time; the `try/finally` in the
659
+ flush middleware runs after the handler (and any wrapping transaction)
660
+ returns.
661
+
662
+ ```typescript
663
+ emit_after_commit(ctx, () => notification_sender.send_to_account(account_id, msg));
664
+ ```
508
665
 
509
- - The enqueued promise **never rejects** — `fn` is wrapped in `try/catch`
510
- and failures go to `ctx.log.error`. One failing send cannot starve
511
- sibling sends in the same batch, nor corrupt the already-committed
512
- response
513
- - Also safe under test mode's `await_pending_effects: true` (which runs
514
- `Promise.all(pending_effects)`) because the promise always resolves
515
- - Structurally satisfied by both `RouteContext` (HTTP) and `ActionContext`
516
- (RPC) they share the `{log, pending_effects}` shape, which is why
517
- this helper lives in `http/` rather than `actions/` or `auth/`
666
+ Used for WS sends (`NotificationSender.send_to_account` for
667
+ role-grant-offer notifications see `../auth/CLAUDE.md` §WS notifications)
668
+ and any side effect that must run only after the transaction commits.
669
+
670
+ ### Key properties
671
+
672
+ - **The flush owns the safety net.** `flush_post_commit_effects` wraps
673
+ every thunk in `try/catch` and routes errors through `ctx.log.error`,
674
+ so one failing send cannot starve sibling effects in the same batch
675
+ nor corrupt the already-committed response. Per-thunk `try/catch`
676
+ inside `emit_after_commit` would skip directly-pushed thunks (e.g.
677
+ tests); centralizing the wrap in the flush closes that gap.
678
+ - **Test mode (`await_pending_effects: true`) flushes both queues.**
679
+ Eager: `await flush_pending_effects(pending_effects, log)`. Deferred:
680
+ `await flush_post_commit_effects(post_commit_effects, log)`. Both
681
+ complete before the response returns. Production mode wraps the same
682
+ helpers in `void ...` and threads `on_effect_error` into
683
+ `flush_pending_effects`'s `on_rejection` callback for fan-out.
684
+ - **Same drain location for both.** The outer flush middleware
685
+ (`server/app_server.ts`) and the per-message WS flush handle the two
686
+ queues adjacent to each other. The deferred queue does not drain inside
687
+ the route-spec wrapper / `perform_action` — that would tighten the
688
+ "post-commit" timing further but would force three drain sites (REST
689
+ wrapper, RPC dispatcher, WS dispatcher) to gain timing no current
690
+ consumer needs (the WS-fan-out use case is happy with post-handler).
691
+ - Structurally satisfied by both `RouteContext` (HTTP) and
692
+ `ActionContext` (RPC + WS) — they share the `{log, post_commit_effects}`
693
+ shape, which is why this helper lives in `http/` rather than
694
+ `actions/` or `auth/`.
518
695
 
519
696
  WS sends are **not** wrapped by `create_validated_broadcaster` (that only
520
697
  guards SSE `broadcast(channel, data)`). Zod input schemas on
@@ -555,7 +732,7 @@ a generic table browser; the factory is domain-agnostic.
555
732
  or table missing (`ERROR_TABLE_NOT_FOUND`), 409 on FK violation (pg
556
733
  error code `23503`)
557
734
 
558
- All four routes use `{type: 'keeper'}` auth. Param schemas use
735
+ All four routes use the keeper auth shape (`{account: 'required', actor: 'required', roles: ['keeper'], credential_types: ['daemon_token']}`). Param schemas use
559
736
  `VALID_SQL_IDENTIFIER` regex, and every table name gets
560
737
  `assert_valid_sql_identifier()` before string-interpolating into SQL —
561
738
  the identifier validation is the only reason the interpolation is safe.