@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
@@ -2,63 +2,64 @@
2
2
  * WebSocket JSON-RPC dispatch — the low-level WS transport binding.
3
3
  *
4
4
  * Most consumers should mount WS endpoints via `register_ws_endpoint`
5
- * (`actions/register_ws_endpoint.ts`), which wraps this function with the standard
6
- * upgrade stack (origin check + auth + optional role). This module stays
7
- * exported as the lower-level entry point for tests that drive the
8
- * dispatcher directly via `create_ws_test_harness`.
5
+ * (`actions/register_ws_endpoint.ts`), which wraps this function with the
6
+ * standard upgrade stack (origin check + auth + optional role). This
7
+ * module stays exported as the lower-level entry point for tests that
8
+ * drive the dispatcher directly via `create_ws_test_harness`.
9
9
  *
10
10
  * Symmetric to `create_rpc_endpoint` (from `actions/action_rpc.ts`):
11
- * consumer supplies action specs + a handler map, the dispatcher parses the
12
- * envelope, checks per-action auth, validates input, invokes the handler with
13
- * a per-request context, and writes the response.
14
- *
15
- * Extracted from zzz's `register_websocket_actions` to converge pattern drift
16
- * across consumers (zzz, tx, undying). Broadcast-style notifications remain
17
- * domain-shaped today this module only covers per-request dispatch + the
18
- * socket-scoped `ctx.notify` + per-socket `ctx.signal`. See
19
- * `BackendWebsocketTransport.send` for broadcast.
11
+ * both transports parse their wire envelope, then call the shared
12
+ * `perform_action` core (`actions/perform_action.ts`) for the post-parse
13
+ * pipeline. WS-specific concerns connection lifecycle, heartbeat,
14
+ * cancel-notification interception, socket-scoped notify — stay in this
15
+ * module; everything else (auth gates, input validation, authorization
16
+ * phase, rate limiting, transactional dispatch, DEV output validation,
17
+ * thrown-error normalization) is shared.
20
18
  *
21
19
  * ## Auth expectations
22
20
  *
23
- * The consumer is responsible for rejecting unauthenticated upgrades *before*
24
- * routing to this handler (fuz_app's `require_auth` middleware, or
25
- * `register_ws_endpoint` which wires it for you). Inside the dispatcher,
26
- * `get_request_context(c)` is treated as guaranteed non-null and per-action
27
- * auth is enforced on each message.
21
+ * The consumer is responsible for rejecting unauthenticated upgrades
22
+ * *before* routing to this handler (fuz_app's `require_auth` middleware,
23
+ * or `register_ws_endpoint` which wires it for you). Per-action auth
24
+ * runs inside `perform_action` on every message via the same gates HTTP
25
+ * RPC uses.
28
26
  *
29
27
  * @module
30
28
  */
31
- import { DEV } from 'esm-env';
32
29
  import { wait } from '@fuzdev/fuz_util/async.js';
33
30
  import { Logger } from '@fuzdev/fuz_util/log.js';
34
- import { get_request_context, has_role } from '../auth/request_context.js';
31
+ import { get_request_context, require_request_context, } from '../auth/request_context.js';
35
32
  import { hash_session_token } from '../auth/session_queries.js';
36
- import { ROLE_KEEPER } from '../auth/role_schema.js';
37
33
  import { get_client_ip } from '../http/proxy.js';
38
- import { JSONRPC_VERSION } from '../http/jsonrpc.js';
39
- import { jsonrpc_error_messages, ThrownJsonrpcError } from '../http/jsonrpc_errors.js';
40
- import { create_jsonrpc_error_response, create_jsonrpc_error_response_from_thrown, create_jsonrpc_notification, to_jsonrpc_message_id, to_jsonrpc_params, is_jsonrpc_request, } from '../http/jsonrpc_helpers.js';
41
- import { CREDENTIAL_TYPE_KEY, AUTH_API_TOKEN_ID_KEY } from '../hono_context.js';
34
+ import { flush_pending_effects, flush_post_commit_effects } from '../http/pending_effects.js';
35
+ import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
36
+ import { create_jsonrpc_error_response, create_jsonrpc_notification, to_jsonrpc_message_id, to_jsonrpc_params, is_jsonrpc_request, } from '../http/jsonrpc_helpers.js';
37
+ import { CREDENTIAL_TYPE_KEY, AUTH_API_TOKEN_ID_KEY, TEST_CONTEXT_PRESET_KEY, } from '../hono_context.js';
42
38
  import {} from './action_types.js';
39
+ import { compile_action_registry } from './compile_action_registry.js';
43
40
  import { cancel_action_spec, CancelNotificationParams } from './cancel.js';
44
41
  import { WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT } from './transports.js';
45
42
  import { BackendWebsocketTransport } from './transports_ws_backend.js';
43
+ import { perform_action, perform_action_result_to_envelope } from './perform_action.js';
46
44
  /** Default inactivity window before the server closes a silent socket. */
47
45
  export const DEFAULT_SERVER_HEARTBEAT_TIMEOUT = 60_000;
48
46
  /**
49
- * Mount a JSON-RPC WebSocket endpoint that dispatches to the supplied handler
50
- * map. Per-request context is built from the base + consumer-provided
51
- * `RegisterActionWsOptions.extend_context`.
47
+ * Mount a JSON-RPC WebSocket endpoint that dispatches via the shared
48
+ * `perform_action` core.
52
49
  *
53
50
  * Wire behavior:
54
51
  * - Batch JSON-RPC is rejected (single-message only).
55
52
  * - Notifications (method + no id) are silently dropped per JSON-RPC spec.
56
- * - Per-action auth: `public` / `authenticated` pass through (upgrade auth
57
- * already verified identity); `keeper` requires `daemon_token` credential
58
- * type *and* the keeper role; role-based `{role}` requires the named role
59
- * via `has_role`, matching the HTTP path in `actions/action_rpc.ts`.
60
- * - DEV mode validates handler output against the spec's `output` schema and
61
- * warns on mismatches.
53
+ * Exception: `cancel` notifications abort the matching pending request's
54
+ * `ctx.signal` before bubbling out.
55
+ * - Per-message dispatch goes through `perform_action`: pre-validation
56
+ * auth (401) input validation (400) authorization phase →
57
+ * post-authorization auth (403) rate limit (429) handler (with
58
+ * transaction wrap iff `spec.side_effects: true`) → DEV output validation.
59
+ * - Authorization phase runs **per message** — role_grant changes during a
60
+ * connection lifetime are picked up on the next message without any
61
+ * in-place refresh. Authentication invalidation closes the socket via
62
+ * `create_ws_auth_guard`.
62
63
  *
63
64
  * @returns the transport (supplied or freshly created) — retain it to wire
64
65
  * `create_ws_auth_guard` or broadcast on audit events.
@@ -67,23 +68,14 @@ export const DEFAULT_SERVER_HEARTBEAT_TIMEOUT = 60_000;
67
68
  * in the transport's internal maps via `add_connection` / `remove_connection`
68
69
  */
69
70
  export const register_action_ws = (options) => {
70
- const { path, app, upgradeWebSocket, actions, extend_context, transport = new BackendWebsocketTransport(), heartbeat = true, artificial_delay = 0, log = new Logger('[ws]'), on_socket_open, on_socket_close, action_ip_rate_limiter = null, action_account_rate_limiter = null, } = options;
71
- // Fan the unified actions array into the two lookups the dispatcher
72
- // consults at message time. Keeping them internal means the composable
73
- // `{spec, handler}` tuple remains the only shape consumers name.
74
- const spec_by_method = new Map();
75
- const handlers = {};
76
- for (const action of actions) {
77
- spec_by_method.set(action.spec.method, action.spec);
78
- if (action.handler)
79
- handlers[action.spec.method] = action.handler;
80
- // Reject account-keyed rate limiting on public actions — the dispatcher
81
- // has no actor to key on. Mirrors the HTTP RPC registration check.
82
- if ((action.spec.rate_limit === 'account' || action.spec.rate_limit === 'both') &&
83
- action.spec.auth === 'public') {
84
- throw new Error(`WS action "${action.spec.method}" declares rate_limit: '${action.spec.rate_limit}' but auth: 'public' — no actor available for account-keyed limiting. Use 'ip' or change auth.`);
85
- }
86
- }
71
+ const { path, app, upgradeWebSocket, actions, db, transport = new BackendWebsocketTransport(), heartbeat = true, artificial_delay = 0, log = new Logger('[ws]'), on_socket_open, on_socket_close, action_ip_rate_limiter = null, action_account_rate_limiter = null, } = options;
72
+ // Build the dispatcher's per-method lookup. Only request_response
73
+ // specs with a handler reach `action_map` perform_action is the
74
+ // only site that calls handlers, and it requires an `RpcAction`.
75
+ // Other kinds (`remote_notification` like `cancel`, `local_call`)
76
+ // are registry-only on WS; the cancel handler reads
77
+ // `cancel_action_spec.method` directly.
78
+ const { action_map } = compile_action_registry(actions, 'WS action');
87
79
  const heartbeat_enabled = heartbeat !== false;
88
80
  const heartbeat_config = typeof heartbeat === 'object' ? heartbeat : {};
89
81
  const heartbeat_timeout = heartbeat_config.timeout ?? DEFAULT_SERVER_HEARTBEAT_TIMEOUT;
@@ -92,14 +84,14 @@ export const register_action_ws = (options) => {
92
84
  // dead-because-unresponsive that closing is arguably correct.
93
85
  const heartbeat_tick_interval = Math.max(100, Math.floor(heartbeat_timeout / 2));
94
86
  app.get(path, upgradeWebSocket((c) => {
95
- // Upgrade-time auth extraction `require_auth` middleware has already
96
- // rejected unauthenticated requests, so request_context is guaranteed
97
- // non-null by the time we get here.
98
- const request_context = get_request_context(c);
99
- const account_id = request_context.account.id;
100
- // Resolved at upgrade every message on this socket shares the
101
- // same client IP, so we capture once and reuse for rate-limit
102
- // keying. `'unknown'` if the proxy middleware wasn't in the stack.
87
+ // Upgrade-time identity capture. `require_auth` middleware has
88
+ // already rejected unauthenticated upgrades, so request_context is
89
+ // non-null here. Per-message dispatch reads `account_id` +
90
+ // `credential_type` from this closure; the live request_context is
91
+ // only used by the test-preset escape hatch (perform_action runs
92
+ // the authorization phase fresh on every message in production).
93
+ const upgrade_context = require_request_context(c);
94
+ const account_id = upgrade_context.account.id;
103
95
  const client_ip = get_client_ip(c);
104
96
  const credential_type = c.get(CREDENTIAL_TYPE_KEY);
105
97
  // Session-based connections have a token hash for targeted revocation.
@@ -110,6 +102,12 @@ export const register_action_ws = (options) => {
110
102
  // `close_sockets_for_token` to tear down just this socket on
111
103
  // `token_revoke` without affecting the account's other sockets.
112
104
  const api_token_id = c.get(AUTH_API_TOKEN_ID_KEY);
105
+ // Test escape hatch — captured once at upgrade. perform_action
106
+ // honors it per-message so harnesses with pre-baked
107
+ // `RequestContext` skip the live authorization phase.
108
+ const upgrade_preset = c.get(TEST_CONTEXT_PRESET_KEY)
109
+ ? { request_context: get_request_context(c) }
110
+ : undefined;
113
111
  // Per-socket abort controller — fires on socket close, chained into
114
112
  // every in-flight handler's per-request controller via
115
113
  // `AbortSignal.any`. Keeping both signals lets the client
@@ -128,9 +126,9 @@ export const register_action_ws = (options) => {
128
126
  // down; `BackendWebsocketTransport.#revoke_connection` clears the
129
127
  // identity map before Hono fires onClose.
130
128
  const identity = { token_hash, account_id, api_token_id };
131
- // Captured on open, consumed on close. Null before onOpen fires or
132
- // when a consumer never opens (e.g. immediate disconnect).
133
- let captured_connection_id = null;
129
+ // Captured on open, consumed on close. Undefined before onOpen
130
+ // fires or when a consumer never opens (e.g. immediate disconnect).
131
+ let captured_connection_id;
134
132
  // Receive-silence watchdog. Seeded to open-time so the first window is
135
133
  // exempt (cold-start grace — avoid killing mid-handshake sockets).
136
134
  // Bumped by onMessage. Any incoming activity counts, not just
@@ -240,80 +238,19 @@ export const register_action_ws = (options) => {
240
238
  return;
241
239
  }
242
240
  const { method, id, params } = json;
243
- // Per-action auth checkenforce auth level from spec.
244
- const spec = spec_by_method.get(method);
245
- if (!spec) {
241
+ // Per-action method lookupreturn method_not_found before
242
+ // we engage the dispatch machinery. Specs without a handler
243
+ // (client-only / dispatcher-handled) miss action_map and
244
+ // surface as method_not_found just like unknown methods.
245
+ const action = action_map.get(method);
246
+ if (!action) {
246
247
  ws.send(JSON.stringify(create_jsonrpc_error_response(id, jsonrpc_error_messages.method_not_found(method))));
247
248
  return;
248
249
  }
249
- const { auth } = spec;
250
- if (auth === 'keeper') {
251
- if (credential_type !== 'daemon_token' || !has_role(request_context, ROLE_KEEPER)) {
252
- ws.send(JSON.stringify(create_jsonrpc_error_response(id, jsonrpc_error_messages.forbidden('keeper actions require daemon_token credential with keeper role'))));
253
- return;
254
- }
255
- }
256
- else if (typeof auth === 'object' && auth !== null) {
257
- if (!has_role(request_context, auth.role)) {
258
- ws.send(JSON.stringify(create_jsonrpc_error_response(id, jsonrpc_error_messages.forbidden(`requires role: ${auth.role}`))));
259
- return;
260
- }
261
- }
262
- // Rate limit — throttle-requests semantics, mirrors the HTTP RPC
263
- // dispatcher. Same limiters are shared across transports so an
264
- // attacker can't bypass the budget by switching from RPC to WS.
265
- const rate_limit = spec.rate_limit;
266
- if (rate_limit) {
267
- const ip_check = action_ip_rate_limiter && (rate_limit === 'ip' || rate_limit === 'both');
268
- const account_check = action_account_rate_limiter && (rate_limit === 'account' || rate_limit === 'both');
269
- const send_rate_limited = (retry_after) => {
270
- ws.send(JSON.stringify(create_jsonrpc_error_response(id, jsonrpc_error_messages.rate_limited('rate limited', { retry_after }))));
271
- };
272
- if (ip_check) {
273
- const result = action_ip_rate_limiter.check(client_ip);
274
- if (!result.allowed) {
275
- send_rate_limited(result.retry_after);
276
- return;
277
- }
278
- }
279
- if (account_check) {
280
- const result = action_account_rate_limiter.check(request_context.actor.id);
281
- if (!result.allowed) {
282
- send_rate_limited(result.retry_after);
283
- return;
284
- }
285
- }
286
- if (ip_check)
287
- action_ip_rate_limiter.record(client_ip);
288
- if (account_check)
289
- action_account_rate_limiter.record(request_context.actor.id);
290
- }
291
- // Look up handler — method is validated against spec_by_method above.
292
- const handler = handlers[method];
293
- if (!handler) {
294
- ws.send(JSON.stringify(create_jsonrpc_error_response(id, jsonrpc_error_messages.method_not_found(method))));
295
- return;
296
- }
297
- // Validate input against spec schema.
298
- const parsed = spec.input.safeParse(params);
299
- if (!parsed.success) {
300
- ws.send(JSON.stringify(create_jsonrpc_error_response(id, jsonrpc_error_messages.invalid_params(`invalid params for ${method}`, {
301
- issues: parsed.error.issues,
302
- }))));
303
- return;
304
- }
305
- const validated_input = parsed.data;
306
250
  if (artificial_delay > 0) {
307
251
  log.debug(`throttling ${artificial_delay}ms`);
308
252
  await wait(artificial_delay);
309
253
  }
310
- // Socket-scoped notification — routes to originator only, not
311
- // broadcast. Same helper used in `on_socket_open` so both
312
- // paths share one code path for send-and-log-on-failure.
313
- // Future work: other audiences — account-scoped,
314
- // ACL-filtered, broadcast — likely via a transport-level
315
- // policy hook.
316
- const notify = notify_socket(ws);
317
254
  // Per-request controller — fires on explicit `cancel` or on
318
255
  // socket close (via the socket_abort_controller chain below).
319
256
  // Registered before dispatch so a cancel arriving mid-handler
@@ -322,40 +259,45 @@ export const register_action_ws = (options) => {
322
259
  // null-abort the wrong handler.
323
260
  const request_controller = new AbortController();
324
261
  pending_controllers.set(id, request_controller);
325
- const base = {
326
- request_id: id,
327
- // Populated in `onOpen` before any message can dispatch —
328
- // non-null assertion is safe.
329
- connection_id: captured_connection_id,
330
- notify,
331
- signal: AbortSignal.any([socket_abort_controller.signal, request_controller.signal]),
332
- };
333
- const ctx = extend_context(base, c);
262
+ // Per-message side-effect queues. `pending_effects` collects
263
+ // eager fire-and-forget pool writes (audit emits, etc.);
264
+ // `post_commit_effects` collects deferred thunks pushed
265
+ // via `emit_after_commit` (WS notifications). Both flush
266
+ // in the same try/finally so the next message sees a clean
267
+ // slate.
268
+ const pending_effects = [];
269
+ const post_commit_effects = [];
270
+ const notify = notify_socket(ws);
271
+ const signal = AbortSignal.any([
272
+ socket_abort_controller.signal,
273
+ request_controller.signal,
274
+ ]);
334
275
  try {
335
- const output = await handler(validated_input, ctx);
336
- // DEV-only output validation — catches handler bugs during development.
337
- if (DEV) {
338
- const output_parsed = spec.output.safeParse(output);
339
- if (!output_parsed.success) {
340
- log.error(`output validation failed for ${method}:`, output_parsed.error.issues);
341
- }
342
- }
343
- // Send result directly — null stays null, matching the HTTP RPC path.
344
- ws.send(JSON.stringify({ jsonrpc: JSONRPC_VERSION, id, result: output }));
345
- }
346
- catch (error) {
347
- if (error instanceof ThrownJsonrpcError) {
348
- // Expected handler outcome (conflict, not_found, invalid_params, ...).
349
- // Log at debug without the stack — the throw site is part of protocol, not a bug.
350
- log.debug('handler error:', method, `${error.code} ${error.message}`);
351
- }
352
- else {
353
- log.error('handler error:', method, error);
354
- }
355
- ws.send(JSON.stringify(create_jsonrpc_error_response_from_thrown(id, error)));
276
+ const result = await perform_action({
277
+ action,
278
+ raw_params: params,
279
+ request_id: id,
280
+ account_id,
281
+ credential_type,
282
+ client_ip,
283
+ signal,
284
+ notify,
285
+ connection_id: captured_connection_id,
286
+ preset: upgrade_preset,
287
+ }, {
288
+ db,
289
+ pending_effects,
290
+ post_commit_effects,
291
+ log,
292
+ action_ip_rate_limiter,
293
+ action_account_rate_limiter,
294
+ });
295
+ ws.send(JSON.stringify(perform_action_result_to_envelope(id, result)));
356
296
  }
357
297
  finally {
358
298
  pending_controllers.delete(id);
299
+ await flush_pending_effects(pending_effects, log);
300
+ await flush_post_commit_effects(post_commit_effects, log);
359
301
  }
360
302
  },
361
303
  onClose: async (event, ws) => {
@@ -7,7 +7,12 @@
7
7
  * 1. `verify_request_source(allowed_origins)` — reject disallowed origins
8
8
  * before the upgrade handshake runs.
9
9
  * 2. `require_auth` — reject unauthenticated upgrades.
10
- * 3. Optional `require_role(required_role)`for endpoints gated to a
10
+ * 3. **Authorization phase**resolve the acting actor against the
11
+ * authenticated account plus an optional `?acting=<uuid>` query string,
12
+ * and build the `RequestContext` that per-message dispatch reads.
13
+ * Multi-actor accounts must supply `?acting` to pick a persona;
14
+ * single-actor accounts work without it.
15
+ * 4. Optional `require_role(required_role)` — for endpoints gated to a
11
16
  * specific role.
12
17
  *
13
18
  * Then delegates to `register_action_ws` for per-message JSON-RPC
@@ -17,9 +22,8 @@
17
22
  */
18
23
  import type { RoleName } from '../auth/role_schema.js';
19
24
  import { type RegisterActionWsOptions, type RegisterActionWsResult } from './register_action_ws.js';
20
- import type { BaseHandlerContext } from './action_types.js';
21
25
  /** Options for `register_ws_endpoint`. */
22
- export interface RegisterWsEndpointOptions<TCtx extends BaseHandlerContext> extends RegisterActionWsOptions<TCtx> {
26
+ export interface RegisterWsEndpointOptions extends RegisterActionWsOptions {
23
27
  /**
24
28
  * Origin allowlist regexes — typically parsed from the `ALLOWED_ORIGINS`
25
29
  * env var via `parse_allowed_origins`. Passed straight to
@@ -27,23 +31,24 @@ export interface RegisterWsEndpointOptions<TCtx extends BaseHandlerContext> exte
27
31
  */
28
32
  allowed_origins: Array<RegExp>;
29
33
  /**
30
- * Role required to upgrade. Omit for any authenticated account (`require_auth`
31
- * alone); set to e.g. `ROLE_ADMIN` to gate the endpoint behind a role. The
32
- * per-action `auth` in each spec still applies at dispatch time — this is
33
- * a coarse upgrade-time gate.
34
+ * Role required to upgrade. Omit for any authenticated account
35
+ * (`require_auth` + actor resolution alone); set to e.g. `ROLE_ADMIN`
36
+ * to gate the endpoint behind a role. The per-action `auth` in each
37
+ * spec still applies at dispatch time — this is a coarse upgrade-time
38
+ * gate.
34
39
  */
35
40
  required_role?: RoleName;
36
41
  }
37
42
  /**
38
43
  * Mount a WebSocket endpoint with the standard upgrade stack (origin check
39
- * + auth + optional role) and JSON-RPC dispatch.
44
+ * + auth + actor resolution + optional role) and JSON-RPC dispatch.
40
45
  *
41
46
  * Returns the `BackendWebsocketTransport` (supplied or freshly
42
47
  * created), same as `register_action_ws` — retain it to wire
43
48
  * `create_ws_auth_guard` on `on_audit_event` or to broadcast.
44
49
  *
45
- * @mutates options.app - applies origin/auth/role middleware via `app.use`,
50
+ * @mutates options.app - applies origin/auth/authorization/role middleware via `app.use`,
46
51
  * then registers the `GET path` route via the inner `register_action_ws`
47
52
  */
48
- export declare const register_ws_endpoint: <TCtx extends BaseHandlerContext>(options: RegisterWsEndpointOptions<TCtx>) => RegisterActionWsResult;
53
+ export declare const register_ws_endpoint: (options: RegisterWsEndpointOptions) => RegisterActionWsResult;
49
54
  //# sourceMappingURL=register_ws_endpoint.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"register_ws_endpoint.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_ws_endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAEN,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,MAAM,yBAAyB,CAAC;AACjC,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,mBAAmB,CAAC;AAE1D,0CAA0C;AAC1C,MAAM,WAAW,yBAAyB,CACzC,IAAI,SAAS,kBAAkB,CAC9B,SAAQ,uBAAuB,CAAC,IAAI,CAAC;IACtC;;;;OAIG;IACH,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,QAAQ,CAAC;CACzB;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,GAAI,IAAI,SAAS,kBAAkB,EACnE,SAAS,yBAAyB,CAAC,IAAI,CAAC,KACtC,sBAUF,CAAC"}
1
+ {"version":3,"file":"register_ws_endpoint.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_ws_endpoint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAYH,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,wBAAwB,CAAC;AAGrD,OAAO,EAEN,KAAK,uBAAuB,EAC5B,KAAK,sBAAsB,EAC3B,MAAM,yBAAyB,CAAC;AAEjC,0CAA0C;AAC1C,MAAM,WAAW,yBAA0B,SAAQ,uBAAuB;IACzE;;;;OAIG;IACH,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B;;;;;;OAMG;IACH,aAAa,CAAC,EAAE,QAAQ,CAAC;CACzB;AAgDD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,oBAAoB,GAChC,SAAS,yBAAyB,KAChC,sBAmBF,CAAC"}
@@ -7,7 +7,12 @@
7
7
  * 1. `verify_request_source(allowed_origins)` — reject disallowed origins
8
8
  * before the upgrade handshake runs.
9
9
  * 2. `require_auth` — reject unauthenticated upgrades.
10
- * 3. Optional `require_role(required_role)`for endpoints gated to a
10
+ * 3. **Authorization phase**resolve the acting actor against the
11
+ * authenticated account plus an optional `?acting=<uuid>` query string,
12
+ * and build the `RequestContext` that per-message dispatch reads.
13
+ * Multi-actor accounts must supply `?acting` to pick a persona;
14
+ * single-actor accounts work without it.
15
+ * 4. Optional `require_role(required_role)` — for endpoints gated to a
11
16
  * specific role.
12
17
  *
13
18
  * Then delegates to `register_action_ws` for per-message JSON-RPC
@@ -16,26 +21,68 @@
16
21
  * @module
17
22
  */
18
23
  import { Logger } from '@fuzdev/fuz_util/log.js';
19
- import { require_auth, require_role } from '../auth/request_context.js';
24
+ import { apply_authorization_phase, REQUEST_CONTEXT_KEY, require_auth, require_role, } from '../auth/request_context.js';
20
25
  import { verify_request_source } from '../http/origin.js';
26
+ import { ACCOUNT_ID_KEY, TEST_CONTEXT_PRESET_KEY } from '../hono_context.js';
21
27
  import { register_action_ws, } from './register_action_ws.js';
28
+ /** Synthesized auth shape for WS upgrade: account + actor both required. */
29
+ const WS_UPGRADE_AUTH = {
30
+ account: 'required',
31
+ actor: 'required',
32
+ };
33
+ /**
34
+ * Upgrade-time authorization middleware. Resolves the acting actor for
35
+ * the WS connection (single-actor default; multi-actor must supply
36
+ * `?acting=<uuid>`) and builds the `RequestContext` that per-message
37
+ * dispatch reads. Returns 400 on resolution failure.
38
+ *
39
+ * Sets `REQUEST_CONTEXT_KEY` on resolved outcomes so the inner
40
+ * `register_action_ws` reads the upgrade-time context via
41
+ * `require_request_context(c)`. Honors the test-preset escape hatch the
42
+ * same way the REST and HTTP RPC binders do.
43
+ */
44
+ const create_ws_authorization_middleware = (db) => {
45
+ return async (c, next) => {
46
+ // Test escape hatch — harnesses pre-populate `REQUEST_CONTEXT_KEY`
47
+ // + flag `TEST_CONTEXT_PRESET_KEY = true`. Production middleware
48
+ // never sets this flag.
49
+ if (c.get(TEST_CONTEXT_PRESET_KEY)) {
50
+ await next();
51
+ return;
52
+ }
53
+ const acting_param = c.req.query('acting');
54
+ const account_id = c.get(ACCOUNT_ID_KEY) ?? null;
55
+ const result = await apply_authorization_phase({ db }, account_id, WS_UPGRADE_AUTH, acting_param ?? undefined);
56
+ if (!result.ok)
57
+ return c.json(result.body, result.status);
58
+ if (result.request_context !== null) {
59
+ c.set(REQUEST_CONTEXT_KEY, result.request_context);
60
+ }
61
+ // `request_context: null` is unreachable here — `WS_UPGRADE_AUTH` is
62
+ // `account: 'required', actor: 'required'`, and `require_auth` ran
63
+ // upstream, so neither the public nor the unauthenticated branch
64
+ // resolves through this middleware.
65
+ await next();
66
+ };
67
+ };
22
68
  /**
23
69
  * Mount a WebSocket endpoint with the standard upgrade stack (origin check
24
- * + auth + optional role) and JSON-RPC dispatch.
70
+ * + auth + actor resolution + optional role) and JSON-RPC dispatch.
25
71
  *
26
72
  * Returns the `BackendWebsocketTransport` (supplied or freshly
27
73
  * created), same as `register_action_ws` — retain it to wire
28
74
  * `create_ws_auth_guard` on `on_audit_event` or to broadcast.
29
75
  *
30
- * @mutates options.app - applies origin/auth/role middleware via `app.use`,
76
+ * @mutates options.app - applies origin/auth/authorization/role middleware via `app.use`,
31
77
  * then registers the `GET path` route via the inner `register_action_ws`
32
78
  */
33
79
  export const register_ws_endpoint = (options) => {
34
- const { app, path, allowed_origins, required_role, log = new Logger('[ws]'), ...rest } = options;
80
+ const { app, path, allowed_origins, db, required_role, log = new Logger('[ws]'), ...rest } = options;
35
81
  app.use(path, verify_request_source(allowed_origins));
36
82
  app.use(path, require_auth);
83
+ app.use(path, create_ws_authorization_middleware(db));
37
84
  if (required_role !== undefined) {
38
- app.use(path, require_role(required_role));
85
+ app.use(path, require_role([required_role]));
39
86
  }
40
- return register_action_ws({ app, path, log, ...rest });
87
+ return register_action_ws({ app, path, db, log, ...rest });
41
88
  };
@@ -1 +1 @@
1
- {"version":3,"file":"transports.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EACX,gCAAgC,EAChC,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAE5B,mDAAmD;AACnD,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAC7C,sEAAsE;AACtE,eAAO,MAAM,iCAAiC,OAAO,CAAC;AACtD,yEAAyE;AACzE,eAAO,MAAM,iCAAiC,OAAO,CAAC;AAKtD,eAAO,MAAM,aAAa,aAAa,CAAC;AACxC,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACzB,cAAc,EAAE,aAAa,CAAC;IAE9B,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/F,IAAI,CACH,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IACxC,IAAI,CACH,OAAO,EAAE,gCAAgC,EACzC,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,gCAAgC,GAAG,IAAI,CAAC,CAAC;IACpD,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,qBAAa,UAAU;;IAItB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAQ;IAE/B;;;;;OAKG;IACH,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAS9C;;;;;OAKG;IACH,qBAAqB,CAAC,cAAc,EAAE,aAAa,GAAG,IAAI;IAM1D;;;;;;OAMG;IACH,aAAa,CAAC,cAAc,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI;IAO/D,QAAQ,IAAI,OAAO,GAAG,IAAI;IAM1B,qBAAqB,IAAI,SAAS,GAAG,IAAI;IAIzC,0BAA0B,IAAI,aAAa,GAAG,IAAI;IAIlD,qBAAqB,CAAC,cAAc,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI;CA2CtE"}
1
+ {"version":3,"file":"transports.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/transports.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EACX,gCAAgC,EAChC,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAE5B,mDAAmD;AACnD,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAC7C,sEAAsE;AACtE,eAAO,MAAM,iCAAiC,OAAO,CAAC;AACtD,yEAAyE;AACzE,eAAO,MAAM,iCAAiC,OAAO,CAAC;AAKtD,eAAO,MAAM,aAAa,aAAa,CAAC;AACxC,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACzB,cAAc,EAAE,aAAa,CAAC;IAE9B,IAAI,CAAC,OAAO,EAAE,cAAc,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;IAC/F,IAAI,CACH,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CAAC;IACxC,IAAI,CACH,OAAO,EAAE,gCAAgC,EACzC,OAAO,CAAC,EAAE,oBAAoB,GAC5B,OAAO,CAAC,gCAAgC,GAAG,IAAI,CAAC,CAAC;IACpD,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,qBAAa,UAAU;;IAItB;;;OAGG;IACH,cAAc,EAAE,OAAO,CAAQ;IAE/B;;;;;OAKG;IACH,kBAAkB,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI;IAQ9C;;;;;OAKG;IACH,qBAAqB,CAAC,cAAc,EAAE,aAAa,GAAG,IAAI;IAM1D;;;;;;OAMG;IACH,aAAa,CAAC,cAAc,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI;IAO/D,QAAQ,IAAI,OAAO,GAAG,IAAI;IAM1B,qBAAqB,IAAI,SAAS,GAAG,IAAI;IAIzC,0BAA0B,IAAI,aAAa,GAAG,IAAI;IAIlD,qBAAqB,CAAC,cAAc,EAAE,aAAa,GAAG,SAAS,GAAG,IAAI;CAwCtE"}
@@ -32,7 +32,6 @@ export class Transports {
32
32
  */
33
33
  register_transport(transport) {
34
34
  this.#transport_by_name.set(transport.transport_name, transport); // TODO maybe ensure unregistering of any previous transport?
35
- // Set current transport if not already set
36
35
  if (!this.#current_transport) {
37
36
  this.#current_transport = transport;
38
37
  }
@@ -87,7 +86,6 @@ export class Transports {
87
86
  return null;
88
87
  }
89
88
  #get_first_ready(transport_name) {
90
- // First try the specified transport(s) if provided
91
89
  if (transport_name) {
92
90
  const transport_names = Array.isArray(transport_name) ? transport_name : [transport_name];
93
91
  for (const transport_name of transport_names) {
@@ -97,11 +95,9 @@ export class Transports {
97
95
  }
98
96
  }
99
97
  }
100
- // Then try the current transport if it's ready
101
98
  if (this.#current_transport?.is_ready()) {
102
99
  return this.#current_transport;
103
100
  }
104
- // Finally, try any other available transport
105
101
  for (const transport of this.#transport_by_name.values()) {
106
102
  if (transport.is_ready()) {
107
103
  return transport;
@@ -31,7 +31,7 @@ export type AuditEventHandler = (event: AuditLogEvent) => void;
31
31
  * - `session_revoke_all` / `token_revoke_all` / `password_change` — close every socket
32
32
  * for the affected account (all credentials invalidated).
33
33
  *
34
- * `permit_revoke` is intentionally omitted: the WS transport does not track
34
+ * `role_grant_revoke` is intentionally omitted: the WS transport does not track
35
35
  * per-connection role requirements, so role-scoped disconnection would
36
36
  * require either closing all sockets (too aggressive) or new tracking
37
37
  * (out of scope). Consumers that need it compose their own callback.
@@ -18,7 +18,7 @@
18
18
  * - `session_revoke_all` / `token_revoke_all` / `password_change` — close every socket
19
19
  * for the affected account (all credentials invalidated).
20
20
  *
21
- * `permit_revoke` is intentionally omitted: the WS transport does not track
21
+ * `role_grant_revoke` is intentionally omitted: the WS transport does not track
22
22
  * per-connection role requirements, so role-scoped disconnection would
23
23
  * require either closing all sockets (too aggressive) or new tracking
24
24
  * (out of scope). Consumers that need it compose their own callback.
@@ -97,7 +97,7 @@ export declare class BackendWebsocketTransport implements FilterableBroadcastTra
97
97
  * Broadcast to connections whose identity satisfies a predicate.
98
98
  *
99
99
  * Used by the broadcast API when a consumer supplies a subscription ACL hook
100
- * (e.g. tx's `tx_run_created` only reaches the account that owns the run).
100
+ * (e.g. zap's `zap_run_created` only reaches the account that owns the run).
101
101
  * When no ACL is needed, callers should prefer `send(message)` / `#broadcast`
102
102
  * to skip the per-connection predicate overhead.
103
103
  *
@@ -146,7 +146,7 @@ export class BackendWebsocketTransport {
146
146
  * Broadcast to connections whose identity satisfies a predicate.
147
147
  *
148
148
  * Used by the broadcast API when a consumer supplies a subscription ACL hook
149
- * (e.g. tx's `tx_run_created` only reaches the account that owns the run).
149
+ * (e.g. zap's `zap_run_created` only reaches the account that owns the run).
150
150
  * When no ACL is needed, callers should prefer `send(message)` / `#broadcast`
151
151
  * to skip the per-connection predicate overhead.
152
152
  *