@fuzdev/fuz_app 0.55.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 (331) hide show
  1. package/dist/actions/CLAUDE.md +211 -155
  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 +19 -0
  6. package/dist/actions/action_codegen.d.ts.map +1 -1
  7. package/dist/actions/action_codegen.js +20 -14
  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 +110 -44
  11. package/dist/actions/action_rpc.d.ts.map +1 -1
  12. package/dist/actions/action_rpc.js +92 -287
  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 +44 -38
  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 +2 -10
  34. package/dist/actions/register_ws_endpoint.d.ts.map +1 -1
  35. package/dist/actions/register_ws_endpoint.js +32 -10
  36. package/dist/actions/transports_ws_auth_guard.d.ts +1 -1
  37. package/dist/actions/transports_ws_auth_guard.js +1 -1
  38. package/dist/actions/transports_ws_backend.d.ts +1 -1
  39. package/dist/actions/transports_ws_backend.js +1 -1
  40. package/dist/auth/CLAUDE.md +673 -442
  41. package/dist/auth/account_action_specs.d.ts +28 -7
  42. package/dist/auth/account_action_specs.d.ts.map +1 -1
  43. package/dist/auth/account_action_specs.js +7 -7
  44. package/dist/auth/account_actions.d.ts +8 -14
  45. package/dist/auth/account_actions.d.ts.map +1 -1
  46. package/dist/auth/account_actions.js +26 -32
  47. package/dist/auth/account_queries.d.ts +46 -13
  48. package/dist/auth/account_queries.d.ts.map +1 -1
  49. package/dist/auth/account_queries.js +73 -33
  50. package/dist/auth/account_routes.d.ts +4 -3
  51. package/dist/auth/account_routes.d.ts.map +1 -1
  52. package/dist/auth/account_routes.js +58 -33
  53. package/dist/auth/account_schema.d.ts +46 -54
  54. package/dist/auth/account_schema.d.ts.map +1 -1
  55. package/dist/auth/account_schema.js +21 -48
  56. package/dist/auth/admin_action_specs.d.ts +55 -21
  57. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  58. package/dist/auth/admin_action_specs.js +42 -26
  59. package/dist/auth/admin_actions.d.ts +14 -21
  60. package/dist/auth/admin_actions.d.ts.map +1 -1
  61. package/dist/auth/admin_actions.js +47 -44
  62. package/dist/auth/audit_emitter.d.ts +160 -0
  63. package/dist/auth/audit_emitter.d.ts.map +1 -0
  64. package/dist/auth/audit_emitter.js +83 -0
  65. package/dist/auth/audit_log_queries.d.ts +17 -87
  66. package/dist/auth/audit_log_queries.d.ts.map +1 -1
  67. package/dist/auth/audit_log_queries.js +17 -96
  68. package/dist/auth/audit_log_routes.d.ts +1 -1
  69. package/dist/auth/audit_log_routes.d.ts.map +1 -1
  70. package/dist/auth/audit_log_routes.js +7 -3
  71. package/dist/auth/audit_log_schema.d.ts +48 -42
  72. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  73. package/dist/auth/audit_log_schema.js +56 -43
  74. package/dist/auth/auth_guard_resolver.d.ts +44 -0
  75. package/dist/auth/auth_guard_resolver.d.ts.map +1 -0
  76. package/dist/auth/auth_guard_resolver.js +56 -0
  77. package/dist/auth/bootstrap_account.d.ts +7 -7
  78. package/dist/auth/bootstrap_account.d.ts.map +1 -1
  79. package/dist/auth/bootstrap_account.js +7 -7
  80. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  81. package/dist/auth/bootstrap_routes.js +11 -10
  82. package/dist/auth/cleanup.d.ts +20 -26
  83. package/dist/auth/cleanup.d.ts.map +1 -1
  84. package/dist/auth/cleanup.js +33 -47
  85. package/dist/auth/credential_type_schema.d.ts +115 -0
  86. package/dist/auth/credential_type_schema.d.ts.map +1 -0
  87. package/dist/auth/credential_type_schema.js +127 -0
  88. package/dist/auth/daemon_token_middleware.d.ts +1 -1
  89. package/dist/auth/daemon_token_middleware.js +3 -3
  90. package/dist/auth/ddl.d.ts +2 -2
  91. package/dist/auth/ddl.d.ts.map +1 -1
  92. package/dist/auth/ddl.js +6 -6
  93. package/dist/auth/deps.d.ts +7 -32
  94. package/dist/auth/deps.d.ts.map +1 -1
  95. package/dist/auth/grant_path_schema.d.ts +117 -0
  96. package/dist/auth/grant_path_schema.d.ts.map +1 -0
  97. package/dist/auth/grant_path_schema.js +137 -0
  98. package/dist/auth/invite_queries.d.ts +12 -1
  99. package/dist/auth/invite_queries.d.ts.map +1 -1
  100. package/dist/auth/invite_queries.js +12 -1
  101. package/dist/auth/invite_schema.d.ts +1 -1
  102. package/dist/auth/invite_schema.d.ts.map +1 -1
  103. package/dist/auth/invite_schema.js +1 -1
  104. package/dist/auth/middleware.d.ts.map +1 -1
  105. package/dist/auth/middleware.js +5 -2
  106. package/dist/auth/migrations.d.ts +22 -7
  107. package/dist/auth/migrations.d.ts.map +1 -1
  108. package/dist/auth/migrations.js +64 -25
  109. package/dist/auth/request_context.d.ts +157 -170
  110. package/dist/auth/request_context.d.ts.map +1 -1
  111. package/dist/auth/request_context.js +224 -268
  112. package/dist/auth/{permit_offer_action_specs.d.ts → role_grant_offer_action_specs.d.ts} +130 -100
  113. package/dist/auth/role_grant_offer_action_specs.d.ts.map +1 -0
  114. package/dist/auth/role_grant_offer_action_specs.js +262 -0
  115. package/dist/auth/role_grant_offer_actions.d.ts +104 -0
  116. package/dist/auth/role_grant_offer_actions.d.ts.map +1 -0
  117. package/dist/auth/{permit_offer_actions.js → role_grant_offer_actions.js} +153 -140
  118. package/dist/auth/{permit_offer_notifications.d.ts → role_grant_offer_notifications.d.ts} +80 -70
  119. package/dist/auth/role_grant_offer_notifications.d.ts.map +1 -0
  120. package/dist/auth/role_grant_offer_notifications.js +182 -0
  121. package/dist/auth/{permit_offer_queries.d.ts → role_grant_offer_queries.d.ts} +64 -64
  122. package/dist/auth/role_grant_offer_queries.d.ts.map +1 -0
  123. package/dist/auth/{permit_offer_queries.js → role_grant_offer_queries.js} +136 -123
  124. package/dist/auth/role_grant_offer_schema.d.ts +150 -0
  125. package/dist/auth/role_grant_offer_schema.d.ts.map +1 -0
  126. package/dist/auth/{permit_offer_schema.js → role_grant_offer_schema.js} +55 -36
  127. package/dist/auth/role_grant_queries.d.ts +231 -0
  128. package/dist/auth/role_grant_queries.d.ts.map +1 -0
  129. package/dist/auth/role_grant_queries.js +320 -0
  130. package/dist/auth/role_schema.d.ts +150 -40
  131. package/dist/auth/role_schema.d.ts.map +1 -1
  132. package/dist/auth/role_schema.js +144 -45
  133. package/dist/auth/scope_kind_schema.d.ts +96 -0
  134. package/dist/auth/scope_kind_schema.d.ts.map +1 -0
  135. package/dist/auth/scope_kind_schema.js +94 -0
  136. package/dist/auth/self_service_role_action_specs.d.ts +4 -1
  137. package/dist/auth/self_service_role_action_specs.d.ts.map +1 -1
  138. package/dist/auth/self_service_role_action_specs.js +2 -2
  139. package/dist/auth/self_service_role_actions.d.ts +35 -29
  140. package/dist/auth/self_service_role_actions.d.ts.map +1 -1
  141. package/dist/auth/self_service_role_actions.js +58 -48
  142. package/dist/auth/session_cookie.d.ts +43 -6
  143. package/dist/auth/session_cookie.d.ts.map +1 -1
  144. package/dist/auth/session_cookie.js +31 -5
  145. package/dist/auth/session_middleware.d.ts +37 -3
  146. package/dist/auth/session_middleware.d.ts.map +1 -1
  147. package/dist/auth/session_middleware.js +33 -7
  148. package/dist/auth/signup_routes.d.ts.map +1 -1
  149. package/dist/auth/signup_routes.js +48 -19
  150. package/dist/auth/standard_action_specs.d.ts +2 -2
  151. package/dist/auth/standard_action_specs.js +4 -4
  152. package/dist/auth/standard_rpc_actions.d.ts +23 -19
  153. package/dist/auth/standard_rpc_actions.d.ts.map +1 -1
  154. package/dist/auth/standard_rpc_actions.js +12 -12
  155. package/dist/db/migrate.d.ts +1 -1
  156. package/dist/db/migrate.js +1 -1
  157. package/dist/dev/setup.d.ts +2 -2
  158. package/dist/dev/setup.d.ts.map +1 -1
  159. package/dist/dev/setup.js +4 -4
  160. package/dist/env/load.d.ts +1 -1
  161. package/dist/env/load.js +1 -1
  162. package/dist/hono_context.d.ts +27 -45
  163. package/dist/hono_context.d.ts.map +1 -1
  164. package/dist/hono_context.js +14 -28
  165. package/dist/http/CLAUDE.md +235 -121
  166. package/dist/http/auth_shape.d.ts +191 -0
  167. package/dist/http/auth_shape.d.ts.map +1 -0
  168. package/dist/http/auth_shape.js +237 -0
  169. package/dist/http/common_routes.js +3 -3
  170. package/dist/http/db_routes.d.ts +4 -0
  171. package/dist/http/db_routes.d.ts.map +1 -1
  172. package/dist/http/db_routes.js +44 -7
  173. package/dist/http/error_schemas.d.ts +56 -34
  174. package/dist/http/error_schemas.d.ts.map +1 -1
  175. package/dist/http/error_schemas.js +63 -28
  176. package/dist/http/pending_effects.d.ts +71 -18
  177. package/dist/http/pending_effects.d.ts.map +1 -1
  178. package/dist/http/pending_effects.js +87 -18
  179. package/dist/http/proxy.d.ts +52 -5
  180. package/dist/http/proxy.d.ts.map +1 -1
  181. package/dist/http/proxy.js +92 -14
  182. package/dist/http/route_spec.d.ts +89 -75
  183. package/dist/http/route_spec.d.ts.map +1 -1
  184. package/dist/http/route_spec.js +54 -72
  185. package/dist/http/schema_helpers.d.ts +3 -14
  186. package/dist/http/schema_helpers.d.ts.map +1 -1
  187. package/dist/http/schema_helpers.js +2 -14
  188. package/dist/http/surface.d.ts +2 -10
  189. package/dist/http/surface.d.ts.map +1 -1
  190. package/dist/http/surface.js +3 -4
  191. package/dist/http/surface_query.d.ts +39 -35
  192. package/dist/http/surface_query.d.ts.map +1 -1
  193. package/dist/http/surface_query.js +79 -36
  194. package/dist/primitive_schemas.d.ts +39 -0
  195. package/dist/primitive_schemas.d.ts.map +1 -0
  196. package/dist/primitive_schemas.js +40 -0
  197. package/dist/realtime/sse_auth_guard.d.ts +5 -5
  198. package/dist/realtime/sse_auth_guard.js +9 -9
  199. package/dist/runtime/mock.d.ts +1 -1
  200. package/dist/runtime/mock.js +1 -1
  201. package/dist/server/app_backend.d.ts +14 -11
  202. package/dist/server/app_backend.d.ts.map +1 -1
  203. package/dist/server/app_backend.js +12 -8
  204. package/dist/server/app_server.d.ts +7 -7
  205. package/dist/server/app_server.d.ts.map +1 -1
  206. package/dist/server/app_server.js +35 -40
  207. package/dist/server/validate_nginx.d.ts +1 -1
  208. package/dist/server/validate_nginx.js +1 -1
  209. package/dist/testing/CLAUDE.md +50 -38
  210. package/dist/testing/admin_integration.d.ts +5 -6
  211. package/dist/testing/admin_integration.d.ts.map +1 -1
  212. package/dist/testing/admin_integration.js +87 -85
  213. package/dist/testing/app_server.d.ts +11 -14
  214. package/dist/testing/app_server.d.ts.map +1 -1
  215. package/dist/testing/app_server.js +16 -15
  216. package/dist/testing/assertions.d.ts.map +1 -1
  217. package/dist/testing/assertions.js +2 -1
  218. package/dist/testing/attack_surface.d.ts.map +1 -1
  219. package/dist/testing/attack_surface.js +15 -9
  220. package/dist/testing/audit_completeness.d.ts +2 -2
  221. package/dist/testing/audit_completeness.d.ts.map +1 -1
  222. package/dist/testing/audit_completeness.js +36 -36
  223. package/dist/testing/auth_apps.d.ts +5 -4
  224. package/dist/testing/auth_apps.d.ts.map +1 -1
  225. package/dist/testing/auth_apps.js +22 -19
  226. package/dist/testing/data_exposure.d.ts.map +1 -1
  227. package/dist/testing/data_exposure.js +5 -5
  228. package/dist/testing/db.d.ts +1 -1
  229. package/dist/testing/db.d.ts.map +1 -1
  230. package/dist/testing/db.js +4 -4
  231. package/dist/testing/db_entities.d.ts +22 -0
  232. package/dist/testing/db_entities.d.ts.map +1 -0
  233. package/dist/testing/db_entities.js +28 -0
  234. package/dist/testing/entities.d.ts +8 -7
  235. package/dist/testing/entities.d.ts.map +1 -1
  236. package/dist/testing/entities.js +21 -18
  237. package/dist/testing/integration.d.ts.map +1 -1
  238. package/dist/testing/integration.js +13 -14
  239. package/dist/testing/integration_helpers.d.ts +4 -4
  240. package/dist/testing/integration_helpers.d.ts.map +1 -1
  241. package/dist/testing/integration_helpers.js +20 -18
  242. package/dist/testing/middleware.d.ts +4 -4
  243. package/dist/testing/middleware.d.ts.map +1 -1
  244. package/dist/testing/middleware.js +12 -11
  245. package/dist/testing/rpc_attack_surface.d.ts.map +1 -1
  246. package/dist/testing/rpc_attack_surface.js +40 -24
  247. package/dist/testing/rpc_round_trip.d.ts +1 -1
  248. package/dist/testing/rpc_round_trip.d.ts.map +1 -1
  249. package/dist/testing/rpc_round_trip.js +14 -13
  250. package/dist/testing/sse_round_trip.d.ts +3 -4
  251. package/dist/testing/sse_round_trip.d.ts.map +1 -1
  252. package/dist/testing/sse_round_trip.js +7 -11
  253. package/dist/testing/standard.d.ts +1 -1
  254. package/dist/testing/stubs.d.ts +25 -0
  255. package/dist/testing/stubs.d.ts.map +1 -1
  256. package/dist/testing/stubs.js +43 -2
  257. package/dist/testing/surface_invariants.d.ts +2 -2
  258. package/dist/testing/ws_round_trip.d.ts +12 -13
  259. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  260. package/dist/testing/ws_round_trip.js +19 -11
  261. package/dist/ui/AdminAccounts.svelte +23 -20
  262. package/dist/ui/AdminOverview.svelte +15 -13
  263. package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
  264. package/dist/ui/{AdminPermitHistory.svelte → AdminRoleGrantHistory.svelte} +12 -12
  265. package/dist/ui/AdminRoleGrantHistory.svelte.d.ts +4 -0
  266. package/dist/ui/AdminRoleGrantHistory.svelte.d.ts.map +1 -0
  267. package/dist/ui/BootstrapForm.svelte +1 -1
  268. package/dist/ui/CLAUDE.md +60 -60
  269. package/dist/ui/{PermitOfferForm.svelte → RoleGrantOfferForm.svelte} +27 -26
  270. package/dist/ui/{PermitOfferForm.svelte.d.ts → RoleGrantOfferForm.svelte.d.ts} +7 -7
  271. package/dist/ui/RoleGrantOfferForm.svelte.d.ts.map +1 -0
  272. package/dist/ui/{PermitOfferHistory.svelte → RoleGrantOfferHistory.svelte} +12 -12
  273. package/dist/ui/{PermitOfferHistory.svelte.d.ts → RoleGrantOfferHistory.svelte.d.ts} +4 -4
  274. package/dist/ui/RoleGrantOfferHistory.svelte.d.ts.map +1 -0
  275. package/dist/ui/{PermitOfferInbox.svelte → RoleGrantOfferInbox.svelte} +14 -14
  276. package/dist/ui/{PermitOfferInbox.svelte.d.ts → RoleGrantOfferInbox.svelte.d.ts} +4 -4
  277. package/dist/ui/RoleGrantOfferInbox.svelte.d.ts.map +1 -0
  278. package/dist/ui/SignupForm.svelte +1 -1
  279. package/dist/ui/SurfaceExplorer.svelte +35 -15
  280. package/dist/ui/SurfaceExplorer.svelte.d.ts.map +1 -1
  281. package/dist/ui/account_sessions_state.svelte.d.ts +2 -3
  282. package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
  283. package/dist/ui/account_sessions_state.svelte.js +2 -3
  284. package/dist/ui/admin_accounts_state.svelte.d.ts +18 -18
  285. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  286. package/dist/ui/admin_accounts_state.svelte.js +16 -16
  287. package/dist/ui/admin_rpc_adapters.d.ts +20 -20
  288. package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
  289. package/dist/ui/admin_rpc_adapters.js +17 -17
  290. package/dist/ui/admin_sessions_state.svelte.d.ts +2 -2
  291. package/dist/ui/admin_sessions_state.svelte.js +2 -2
  292. package/dist/ui/audit_log_state.svelte.d.ts +7 -7
  293. package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
  294. package/dist/ui/audit_log_state.svelte.js +6 -6
  295. package/dist/ui/auth_state.svelte.d.ts +3 -3
  296. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  297. package/dist/ui/auth_state.svelte.js +6 -6
  298. package/dist/ui/format_scope.d.ts +2 -2
  299. package/dist/ui/format_scope.js +2 -2
  300. package/dist/ui/{permit_offers_state.svelte.d.ts → role_grant_offers_state.svelte.d.ts} +30 -30
  301. package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -0
  302. package/dist/ui/{permit_offers_state.svelte.js → role_grant_offers_state.svelte.js} +18 -18
  303. package/dist/ui/ui_format.js +2 -2
  304. package/package.json +3 -3
  305. package/dist/auth/permit_offer_action_specs.d.ts.map +0 -1
  306. package/dist/auth/permit_offer_action_specs.js +0 -258
  307. package/dist/auth/permit_offer_actions.d.ts +0 -110
  308. package/dist/auth/permit_offer_actions.d.ts.map +0 -1
  309. package/dist/auth/permit_offer_notifications.d.ts.map +0 -1
  310. package/dist/auth/permit_offer_notifications.js +0 -182
  311. package/dist/auth/permit_offer_queries.d.ts.map +0 -1
  312. package/dist/auth/permit_offer_schema.d.ts +0 -125
  313. package/dist/auth/permit_offer_schema.d.ts.map +0 -1
  314. package/dist/auth/permit_queries.d.ts +0 -222
  315. package/dist/auth/permit_queries.d.ts.map +0 -1
  316. package/dist/auth/permit_queries.js +0 -305
  317. package/dist/auth/require_keeper.d.ts +0 -20
  318. package/dist/auth/require_keeper.d.ts.map +0 -1
  319. package/dist/auth/require_keeper.js +0 -35
  320. package/dist/auth/route_guards.d.ts +0 -27
  321. package/dist/auth/route_guards.d.ts.map +0 -1
  322. package/dist/auth/route_guards.js +0 -38
  323. package/dist/auth/session_lifecycle.d.ts +0 -37
  324. package/dist/auth/session_lifecycle.d.ts.map +0 -1
  325. package/dist/auth/session_lifecycle.js +0 -29
  326. package/dist/ui/AdminPermitHistory.svelte.d.ts +0 -4
  327. package/dist/ui/AdminPermitHistory.svelte.d.ts.map +0 -1
  328. package/dist/ui/PermitOfferForm.svelte.d.ts.map +0 -1
  329. package/dist/ui/PermitOfferHistory.svelte.d.ts.map +0 -1
  330. package/dist/ui/PermitOfferInbox.svelte.d.ts.map +0 -1
  331. package/dist/ui/permit_offers_state.svelte.d.ts.map +0 -1
@@ -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
 
@@ -97,41 +110,37 @@ wrapper). See `../auth/signup_routes.ts`.
97
110
  2. **Query validation** — `spec.query` → `validated_query`; mismatch
98
111
  returns 400 `ERROR_INVALID_QUERY_PARAMS`
99
112
  3. **Pre-validation auth guards** — `require_auth` (401
100
- `ERROR_AUTHENTICATION_REQUIRED`) for any non-public route. Fires
101
- before any body parsing so unauthenticated callers never see
102
- route-shape information from input parse failures. The
103
- `AuthGuardResolver` (e.g. `fuz_auth_guard_resolver` from
104
- `../auth/route_guards.ts`) returns this set as
105
- `pre_validation: Array<MiddlewareHandler>`.
106
- 4. **Authorization phase** — when the route's input schema declares
107
- `acting?: ActingActor` or `spec.auth.type` is `'role'` / `'keeper'`,
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'`,
108
125
  resolves the acting actor against `c.var.account_id` (set by the
109
- auth middleware) plus the raw `acting` value extracted from query
110
- (GET) or pre-parsed JSON body (mutating methods), builds
111
- `RequestContext` via `build_request_context`, and sets
112
- `REQUEST_CONTEXT_KEY`. Resolution failures return 400
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
113
131
  `ERROR_ACTOR_REQUIRED` (with `available[]`) or
114
132
  `ERROR_ACTOR_NOT_ON_ACCOUNT` (or 500 `ERROR_NO_ACTORS_ON_ACCOUNT`
115
- when the actor enumeration came back empty, 500 `ERROR_ACCOUNT_VANISHED`
116
- on torn account/actor reads after a successful resolve) before the
117
- handler runs. Account-grain
118
- routes skip this phase; their handlers see no `RequestContext` (or
119
- one with `actor: null`, depending on the helper). The pre-parsed body
120
- lands on `c.var.cached_request_body` (see step 6) so the subsequent
121
- input-validation step reads from there instead of re-parsing.
122
- 5. **Post-authorization auth guards** — `require_role(role)` /
123
- `require_keeper` (403 `ERROR_INSUFFICIENT_PERMISSIONS` /
124
- `ERROR_KEEPER_REQUIRES_DAEMON_TOKEN`). Reads `REQUEST_CONTEXT_KEY`
125
- populated by step 4. The resolver returns this set as
126
- `post_authorization: Array<MiddlewareHandler>`.
127
- 6. **Input validation** — JSON body parsed + validated; mismatch returns
128
- 400 `ERROR_INVALID_JSON_BODY` (not JSON) or `ERROR_INVALID_REQUEST_BODY`
129
- (schema failure with `issues`). Skipped on GET and `z.null()` inputs.
130
- On mutating methods, the parse result is shared with the authorization
131
- phase's pre-parse via `c.var.cached_request_body`
132
- (`CACHED_REQUEST_BODY_KEY` from `hono_context.ts`) — the cache is
133
- fuz_app-owned, not Hono's internal `bodyCache`, so a future Hono
134
- internals refactor can't break our second-parse-avoidance contract.
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)`.
135
144
  7. **Handler** — wrapped in transaction when `use_transaction` (see
136
145
  above), receives `RouteContext`
137
146
  8. **DEV-only output + error validation** — wraps the handler (see below)
@@ -148,15 +157,21 @@ wrapper). See `../auth/signup_routes.ts`.
148
157
  the JSON-RPC dispatcher keeps its own `{jsonrpc, id, error: {code,
149
158
  message, data}}` envelope on the RPC mount
150
159
 
151
- The auth-before-validation order matches the RPC dispatcher
152
- (`actions/action_rpc.ts`) so HTTP RPC and REST surface failures with
153
- the same priority: 401 403 400 → handler.
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.
154
167
 
155
168
  Duplicate `method path` pairs throw at registration.
156
169
 
157
- Validated values are accessed via `get_route_input<T>(c)`,
158
- `get_route_params<T>(c)`, `get_route_query<T>(c)` — typed helpers that
159
- 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.
160
175
 
161
176
  ### DEV-only output + error validation
162
177
 
@@ -192,35 +207,40 @@ Output Validation.
192
207
 
193
208
  - `ERROR_*` snake*case string constants — single source of truth; use
194
209
  `.literal(ERROR*\*)` in Zod schemas and inline checks in handlers
195
- - `ApiError`, `ValidationError`, `PermissionError`, `KeeperError`,
196
- `RateLimitError`, `PayloadTooLargeError`, `ForeignKeyError` — standard
197
- shapes
210
+ - `ApiError`, `ValidationError`, `PermissionError`,
211
+ `CredentialTypeRequiredError`, `RateLimitError`, `PayloadTooLargeError`,
212
+ `ForeignKeyError` — standard shapes
198
213
  - `RouteErrorSchemas = Partial<Record<number, z.ZodType>>`
199
214
  - `RateLimitKey = 'ip' | 'account' | 'both'`
200
215
 
201
216
  All standard shapes use `z.looseObject` — intentional because multiple
202
217
  producers (middleware + handler) can emit different extra fields at the
203
218
  same status code. The `error` string literal is the contract; extra keys
204
- (`required_role`, `retry_after`, `detail`) are diagnostic.
219
+ (`required_roles`, `required_credential_types`, `retry_after`, `detail`)
220
+ are diagnostic.
205
221
 
206
222
  Pair every schema with the `z.infer` type export (`export type ApiError = z.infer<typeof ApiError>`).
207
223
 
208
224
  ### Three-layer error-schema merge
209
225
 
210
- `merge_error_schemas(spec, middleware_errors?, acting_aware?)` (in `schema_helpers.ts`)
226
+ `merge_error_schemas(spec, middleware_errors?)` (in `schema_helpers.ts`)
211
227
  merges three layers, later overrides earlier at the same status code:
212
228
 
213
- 1. **Derived** — from `derive_error_schemas({auth, has_input?, has_params?, has_query?, rate_limit?, acting_aware?})`:
229
+ 1. **Derived** — from `derive_error_schemas({auth, has_input?, has_params?, has_query?, rate_limit?})`:
214
230
  - `has_input || has_params || has_query` → 400 `ValidationError`
215
- - `auth.type === 'authenticated'` → 401 `ApiError`
216
- - `auth.type === 'role'` → 401 `ApiError` + 403 `PermissionError`
217
- - `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])`)
218
238
  - `rate_limit` → 429 `RateLimitError`
219
- - `acting_aware` → widens 400 to a union with `ActorRequiredError` /
239
+ - `auth.actor !== 'none'` → widens 400 to a union with `ActorRequiredError` /
220
240
  `ActorNotOnAccountError` and adds 500 union of `NoActorsOnAccountError`
221
241
  / `AccountVanishedError`. Mirrors what the dispatcher's authorization
222
242
  phase actually emits on routes whose input declares `acting?: ActingActor`
223
- or whose auth requires permits — so DEV-mode error-schema validation in
243
+ (per registry-time invariant 2) — so DEV-mode error-schema validation in
224
244
  `wrap_output_validation` doesn't reject the auth phase's body.
225
245
  2. **Middleware** — from `MiddlewareSpec.errors` that apply to the route's
226
246
  path (via `middleware_applies`)
@@ -228,37 +248,49 @@ merges three layers, later overrides earlier at the same status code:
228
248
 
229
249
  Routes typically only need `errors` for handler-specific codes (404, 409, 422).
230
250
 
231
- `acting_aware` is computed at the call site (`apply_route_specs` /
232
- `generate_app_surface`) via the optional `is_acting_aware?: (spec) => boolean`
233
- callback. Computation lives in the consumer because the canonical
234
- "input declares `acting?: ActingActor`" check uses reference equality with
235
- the canonical `ActingActor` Zod schema in `auth/account_schema.ts`, and
236
- `http/` stays auth-agnostic. fuz_app's `create_app_server` wires
237
- `(spec) => is_actor_implying_auth(spec.auth) || input_schema_declares_acting(spec.input)`
238
- consumers building on raw `apply_route_specs` opt in by passing the
239
- same predicate (or a narrower one). When the callback is omitted the
240
- flag defaults to false so frameworks not using fuz_app's auth phase don't
241
- get fuz_app-specific shapes on their derived surface.
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.
242
275
 
243
276
  ### `ERROR_*` constants by category
244
277
 
245
278
  - **Validation**: `ERROR_INVALID_REQUEST_BODY`, `ERROR_INVALID_JSON_BODY`,
246
279
  `ERROR_INVALID_ROUTE_PARAMS`, `ERROR_INVALID_QUERY_PARAMS`
247
280
  - **Auth**: `ERROR_AUTHENTICATION_REQUIRED`, `ERROR_INSUFFICIENT_PERMISSIONS`,
248
- `ERROR_RATE_LIMIT_EXCEEDED`, `ERROR_INVALID_CREDENTIALS`,
249
- `ERROR_PAYLOAD_TOO_LARGE`
281
+ `ERROR_CREDENTIAL_TYPE_REQUIRED`, `ERROR_RATE_LIMIT_EXCEEDED`,
282
+ `ERROR_INVALID_CREDENTIALS`, `ERROR_PAYLOAD_TOO_LARGE`
250
283
  - **Origin + bearer**: `ERROR_FORBIDDEN_ORIGIN`, `ERROR_FORBIDDEN_REFERER`,
251
284
  `ERROR_BEARER_REJECTED_BROWSER`, `ERROR_INVALID_TOKEN`, `ERROR_ACCOUNT_NOT_FOUND`
252
- - **Keeper/daemon**: `ERROR_KEEPER_REQUIRES_DAEMON_TOKEN`,
253
- `ERROR_INVALID_DAEMON_TOKEN`, `ERROR_KEEPER_ACCOUNT_NOT_CONFIGURED`,
254
- `ERROR_KEEPER_ACCOUNT_NOT_FOUND`
285
+ - **Keeper/daemon**: `ERROR_INVALID_DAEMON_TOKEN`,
286
+ `ERROR_KEEPER_ACCOUNT_NOT_CONFIGURED`, `ERROR_KEEPER_ACCOUNT_NOT_FOUND`
255
287
  - **Bootstrap**: `ERROR_ALREADY_BOOTSTRAPPED`, `ERROR_TOKEN_FILE_MISSING`,
256
288
  `ERROR_BOOTSTRAP_NOT_CONFIGURED`
257
289
  - **Signup/invites**: `ERROR_NO_MATCHING_INVITE`, `ERROR_SIGNUP_CONFLICT`,
258
290
  `ERROR_INVITE_NOT_FOUND`, `ERROR_INVITE_MISSING_IDENTIFIER`,
259
291
  `ERROR_INVITE_DUPLICATE`, `ERROR_INVITE_ACCOUNT_EXISTS_USERNAME`,
260
292
  `ERROR_INVITE_ACCOUNT_EXISTS_EMAIL`
261
- - **Admin**: `ERROR_ROLE_NOT_WEB_GRANTABLE`, `ERROR_PERMIT_NOT_FOUND`,
293
+ - **Admin**: `ERROR_ROLE_NOT_WEB_GRANTABLE`, `ERROR_ROLE_GRANT_NOT_FOUND`,
262
294
  `ERROR_INVALID_EVENT_TYPE`
263
295
  - **DB browser**: `ERROR_FOREIGN_KEY_VIOLATION`, `ERROR_TABLE_NOT_FOUND`,
264
296
  `ERROR_TABLE_NO_PRIMARY_KEY`, `ERROR_ROW_NOT_FOUND`
@@ -288,7 +320,7 @@ Key helpers:
288
320
  `'*'`, exact, `'/api/*'` prefix (handles `prefix.slice(0, -1)` so
289
321
  `/api/*` also matches the bare `/api`)
290
322
  - `merge_error_schemas(spec, middleware_errors?)` — three-layer merge
291
- described above
323
+ described above.
292
324
 
293
325
  ## Surface Generation
294
326
 
@@ -326,8 +358,7 @@ Key helpers:
326
358
  — `description`, `sensitivity`, and probes `safeParse(undefined)` to
327
359
  detect `optional` + `has_default`
328
360
  - `events_to_surface(event_specs)` — SSE events surface as `{method, description, channel, params_schema}`
329
- - RPC methods surface through `map_action_auth` (from `actions/action_bridge.ts`;
330
- 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.
331
362
 
332
363
  `create_app_surface_spec(options)` = `generate_app_surface(options)` plus
333
364
  the source specs, for tests that need to iterate over raw specs.
@@ -341,11 +372,21 @@ No side effects, no state — filters and groupings over `AppSurface`:
341
372
  - `filter_routes_by_prefix(prefix)` / `filter_routes_with_input` /
342
373
  `filter_routes_with_params` / `filter_routes_with_query` /
343
374
  `filter_mutation_routes` / `filter_rate_limited_routes`
344
- - `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.
345
376
  - `format_route_key(route)` → `'METHOD /path'`
346
377
  - `surface_auth_summary(surface)` — counts per auth type, roles broken
347
378
  out by name
348
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
+
349
390
  Consumer code (tests, attack-surface helpers, `SurfaceExplorer.svelte`)
350
391
  should reach for these rather than inlining `.filter` chains.
351
392
 
@@ -383,11 +424,27 @@ Must run **before** auth and rate-limiting middleware. See the root
383
424
  - `parse_proxy_entry(entry)` — accepts `'127.0.0.1'`, `'::1'`,
384
425
  `'10.0.0.0/8'`, `'fe80::/10'`. Throws on invalid IPs, NaN/negative/
385
426
  over-range prefix, non-network-aligned CIDRs, or bad input
386
- - `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
387
439
  mismatched address families for CIDR matches
388
440
  - `resolve_client_ip(forwarded_for, proxies)` — walks **right-to-left**,
389
- skipping trusted entries. First untrusted wins. If all entries are
390
- 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
391
448
  - `create_proxy_middleware(options)` + `create_proxy_middleware_spec(options)` —
392
449
  three-branch logic:
393
450
  1. No XFF → use connection IP directly
@@ -398,10 +455,11 @@ Must run **before** auth and rate-limiting middleware. See the root
398
455
  - `get_client_ip(c)` — returns `'unknown'` when the proxy middleware
399
456
  hasn't run
400
457
 
401
- Non-standard proxies that include ports in XFF entries (`203.0.113.1:8080`)
402
- fail `distinctRemoteAddr` and are treated as untrusted safe default but
403
- rate-limiting keys the port-suffixed string. nginx and cloud LBs don't do
404
- 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.
405
463
 
406
464
  ### Origin/Referer allowlist — `origin.ts`
407
465
 
@@ -553,31 +611,87 @@ Converters:
553
611
 
554
612
  ## Pending Effects
555
613
 
556
- `emit_after_commit(ctx, fn)` in `pending_effects.ts` is the canonical
557
- post-commit fan-out helper. Used for WS sends (`NotificationSender.send_to_account`
558
- for permit-offer notifications — see `../auth/CLAUDE.md` §WS notifications) and any side effect that must run only
559
- after the transaction commits.
614
+ Two queues, one timing contract each:
560
615
 
561
616
  ```typescript
562
- interface PendingEffectsContext {
617
+ interface EmitAfterCommitContext {
563
618
  log: Logger;
564
- pending_effects: Array<Promise<void>>;
619
+ post_commit_effects: Array<() => void | Promise<void>>;
565
620
  }
566
621
 
567
- 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>>
568
625
  ```
569
626
 
570
- 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
+ ```
571
665
 
572
- - The enqueued promise **never rejects** — `fn` is wrapped in `try/catch`
573
- and failures go to `ctx.log.error`. One failing send cannot starve
574
- sibling sends in the same batch, nor corrupt the already-committed
575
- response
576
- - Also safe under test mode's `await_pending_effects: true` (which runs
577
- `Promise.all(pending_effects)`) because the promise always resolves
578
- - Structurally satisfied by both `RouteContext` (HTTP) and `ActionContext`
579
- (RPC) they share the `{log, pending_effects}` shape, which is why
580
- 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/`.
581
695
 
582
696
  WS sends are **not** wrapped by `create_validated_broadcaster` (that only
583
697
  guards SSE `broadcast(channel, data)`). Zod input schemas on
@@ -618,7 +732,7 @@ a generic table browser; the factory is domain-agnostic.
618
732
  or table missing (`ERROR_TABLE_NOT_FOUND`), 409 on FK violation (pg
619
733
  error code `23503`)
620
734
 
621
- 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
622
736
  `VALID_SQL_IDENTIFIER` regex, and every table name gets
623
737
  `assert_valid_sql_identifier()` before string-interpolating into SQL —
624
738
  the identifier validation is the only reason the interpolation is safe.