@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
@@ -18,15 +18,15 @@ as their first arg.
18
18
  Pure, I/O-free operations. Framework-dependent middleware lives in later
19
19
  sections.
20
20
 
21
- | Module | Exports |
22
- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
- | `keyring.ts` | `Keyring`, `create_keyring`, `validate_keyring`, `create_validated_keyring`, `ValidatedKeyringResult` |
24
- | `session_cookie.ts` | `SessionOptions<T>`, `SessionCookieOptions`, `SESSION_COOKIE_OPTIONS`, `SESSION_AGE_MAX`, `ParsedSession`, `ProcessSessionResult`, `parse_session`, `create_session_cookie_value`, `process_session_cookie`, `create_session_config`, `fuz_session_config` |
25
- | `password.ts` | `Password`, `PasswordProvided`, `PasswordHashDeps`, `PASSWORD_LENGTH_MIN` (12, OWASP), `PASSWORD_LENGTH_MAX` (300) |
26
- | `password_argon2.ts` | `hash_password`, `verify_password`, `verify_dummy`, `argon2_password_deps` |
27
- | `api_token.ts` | `API_TOKEN_PREFIX` (`secret_fuz_token_`), `hash_api_token`, `generate_api_token` |
28
- | `daemon_token.ts` | `DaemonToken` (Zod), `DAEMON_TOKEN_HEADER` (`X-Daemon-Token`), `generate_daemon_token`, `validate_daemon_token`, `DaemonTokenState` |
29
- | `bootstrap_account.ts` | `bootstrap_account`, `BootstrapAccountDeps`, `BootstrapAccountInput`, `BootstrapAccountSuccess`, `BootstrapAccountFailure`, `BootstrapAccountResult` |
21
+ | Module | Exports |
22
+ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
23
+ | `keyring.ts` | `Keyring`, `create_keyring`, `validate_keyring`, `create_validated_keyring`, `ValidatedKeyringResult` |
24
+ | `session_cookie.ts` | `SessionOptions<T>`, `SessionCookieOptions`, `SESSION_COOKIE_OPTIONS`, `SESSION_AGE_MAX`, `SESSION_REFRESH_THRESHOLD_S`, `ParsedSession`, `ProcessSessionResult`, `parse_session`, `create_session_cookie_value`, `process_session_cookie`, `create_session_config`, `fuz_session_config` |
25
+ | `password.ts` | `Password`, `PasswordProvided`, `PasswordHashDeps`, `PASSWORD_LENGTH_MIN` (12, OWASP), `PASSWORD_LENGTH_MAX` (300) |
26
+ | `password_argon2.ts` | `hash_password`, `verify_password`, `verify_dummy`, `argon2_password_deps` |
27
+ | `api_token.ts` | `API_TOKEN_PREFIX` (`secret_fuz_token_`), `hash_api_token`, `generate_api_token` |
28
+ | `daemon_token.ts` | `DaemonToken` (Zod), `DAEMON_TOKEN_HEADER` (`X-Daemon-Token`), `generate_daemon_token`, `validate_daemon_token`, `DaemonTokenState` |
29
+ | `bootstrap_account.ts` | `bootstrap_account`, `BootstrapAccountDeps`, `BootstrapAccountInput`, `BootstrapAccountSuccess`, `BootstrapAccountFailure`, `BootstrapAccountResult` |
30
30
 
31
31
  Design notes:
32
32
 
@@ -41,7 +41,13 @@ Design notes:
41
41
  `string` for session-id references (server-side sessions, per-session
42
42
  revocation), `number` for direct account-id references (no server state).
43
43
  The canonical fuz pattern is `SessionOptions<string>` via
44
- `create_session_config(name)`.
44
+ `create_session_config(name)`. `SessionOptions.max_age` is the single
45
+ source of truth for cookie lifetime — drives both the signed `expires_at`
46
+ and the HTTP `Max-Age` attribute. `process_session_cookie` re-signs on
47
+ key rotation **or** when within `refresh_threshold_seconds` (default
48
+ `SESSION_REFRESH_THRESHOLD_S` = 1 day) of expiry, mirroring the DB-side
49
+ `AUTH_SESSION_EXTEND_THRESHOLD_MS` so a continuously-active user's
50
+ cookie tracks their server session.
45
51
  - **Password** has two schemas deliberately. `Password` enforces the current
46
52
  length policy (used at account creation and password change);
47
53
  `PasswordProvided` is minimal (`min(1)`) for login / verification so a
@@ -61,7 +67,7 @@ Design notes:
61
67
  - **Bootstrap account** is one-shot; protected by the `bootstrap_lock` table
62
68
  via atomic `UPDATE ... WHERE id = 1 AND bootstrapped = false RETURNING id`.
63
69
  Token read + password hash happen outside the transaction (CPU + I/O);
64
- lock acquisition + account + actor + two permits (`keeper` and `admin`)
70
+ lock acquisition + account + actor + two role_grants (`keeper` and `admin`)
65
71
  happen inside. On commit, the token file is deleted — if that fails,
66
72
  `token_file_deleted: false` is returned and the caller is expected to
67
73
  surface an error (the `/bootstrap` handler throws so the operator gets a
@@ -70,25 +76,26 @@ Design notes:
70
76
 
71
77
  ## Schemas, types, and DDL
72
78
 
73
- | Module | What's inside |
74
- | ------------------------------- | ----------------------------------------------------------------------------------------- |
75
- | `account_schema.ts` | Runtime types + client-safe Zod schemas for identity entities |
76
- | `role_schema.ts` | Role vocabulary and extensibility |
77
- | `ddl.ts` | Raw `CREATE TABLE` / index / seed SQL strings |
78
- | `invite_schema.ts` | `Invite`, `InviteJson`, `InviteWithUsernamesJson`, `CreateInviteInput` |
79
- | `app_settings_schema.ts` | `AppSettings`, `AppSettingsJson`, `AppSettingsWithUsernameJson`, `UpdateAppSettingsInput` |
80
- | `audit_log_schema.ts` | Event-type enum, per-type metadata schemas, table DDL |
81
- | `permit_offer_schema.ts` | Permit offer DDL, types, and client-safe schemas |
82
- | `permit_offer_notifications.ts` | WS notification specs for the consentful-permits lifecycle |
79
+ | Module | What's inside |
80
+ | ----------------------------------- | ----------------------------------------------------------------------------------------- |
81
+ | `account_schema.ts` | Runtime types + client-safe Zod schemas for identity entities |
82
+ | `role_schema.ts` | Role vocabulary and extensibility |
83
+ | `ddl.ts` | Raw `CREATE TABLE` / index / seed SQL strings |
84
+ | `invite_schema.ts` | `Invite`, `InviteJson`, `InviteWithUsernamesJson`, `CreateInviteInput` |
85
+ | `app_settings_schema.ts` | `AppSettings`, `AppSettingsJson`, `AppSettingsWithUsernameJson`, `UpdateAppSettingsInput` |
86
+ | `audit_log_schema.ts` | Event-type enum, per-type metadata schemas, table DDL |
87
+ | `role_grant_offer_schema.ts` | Role grant offer DDL, types, and client-safe schemas |
88
+ | `role_grant_offer_notifications.ts` | WS notification specs for the consentful-role-grant lifecycle |
83
89
 
84
90
  ### Identity entities (`account_schema.ts`)
85
91
 
86
92
  - `Account` (primary identity, holds `password_hash`), `Actor` (the entity
87
- that acts — owns cells, holds permits, appears in audit trails; an account
93
+ that acts — owns cells, holds role_grants, appears in audit trails; an account
88
94
  may host one or more actors, with the dispatcher's authorization phase
89
95
  resolving the acting actor per-request via `acting?: ActingActor` on
90
- inputs), `Permit` (time-bounded, revocable grant of a role to an
91
- actor — carries `scope_id`, `source_offer_id`, `revoked_reason`),
96
+ inputs), `RoleGrant` (time-bounded, revocable grant of a role to an
97
+ actor — carries `scope_kind` + `scope_id` paired-null,
98
+ `source_offer_id`, `revoked_reason`),
92
99
  `AuthSession` (server-side, keyed by blake3), `ApiToken`.
93
100
  - Every `id` / `*_id` field on entity interfaces, `*Json` schemas, and
94
101
  `*Input` types is branded `Uuid` (from `@fuzdev/fuz_util/uuid.js`), except
@@ -98,44 +105,137 @@ Design notes:
98
105
  `UsernameProvided`: `min(1).max(255)` — permissive for login/lookup so
99
106
  tightening creation rules won't lock out existing users.
100
107
  - `Email`: `z.email()`.
101
- - `PERMIT_REVOKED_REASON_LENGTH_MAX = 500` — bounds both the admin input
102
- and the `permit_revoke` WS payload.
108
+ - `ROLE_GRANT_REVOKED_REASON_LENGTH_MAX = 500` — bounds both the admin input
109
+ and the `role_grant_revoke` WS payload.
103
110
  - Client-safe Zod schemas (every exported schema has a same-named `z.infer`
104
111
  type export):
105
112
  - `SessionAccountJson` — strips sensitive fields from `Account`
106
113
  - `AuthSessionJson` — `id` is the blake3 hash (safe for client)
107
114
  - `ClientApiTokenJson` — excludes `token_hash`
108
- - `PermitSummaryJson` — the client-safe permit shape carried by
115
+ - `RoleGrantSummaryJson` — the client-safe role_grant shape carried by
109
116
  `GET /api/account/status` and the admin account listing; includes
110
- `scope_id` so clients can make per-scope auth decisions. Excludes
117
+ `scope_kind` + `scope_id` (paired-null) so clients can make
118
+ per-scope auth decisions. Excludes
111
119
  `revoked_at` / `revoked_by` / `revoked_reason` because the callers
112
- that return it already filter to active permits.
120
+ that return it already filter to active role_grants.
113
121
  - `ActorSummaryJson`
114
122
  - `AdminAccountJson` extends `SessionAccountJson` with `updated_at` / `updated_by`
115
- - `PendingOfferSummaryJson` — narrower than `PermitOfferJson`; omits
123
+ - `PendingOfferSummaryJson` — narrower than `RoleGrantOfferJson`; omits
116
124
  `message` and `decline_reason` so cross-admin visibility of the listing
117
125
  does not expose grantor-authored text beyond what the audit log
118
126
  discloses. `from_username` is resolved server-side so admins can see
119
127
  whose pending offer is blocking a "+ role" button.
120
- - `AdminAccountEntryJson` — composes `{account, actor, permits, pending_offers}`
128
+ - `AdminAccountEntryJson` — composes `{account, actor, role_grants, pending_offers}`
121
129
  - Converters: `to_session_account(account)`, `to_admin_account(account)`,
122
- `is_permit_active(p, now?)`.
123
- - Input types: `CreateAccountInput`, `GrantPermitInput` (with optional
124
- `scope_id`, `source_offer_id`).
130
+ `is_role_grant_active(p, now?)`.
131
+ - Input types: `CreateAccountInput`, `CreateRoleGrantInput` (with optional
132
+ `scope_kind`, `scope_id`, `source_offer_id` — `scope_kind` paired-null
133
+ with `scope_id` per the `role_grant_scope_kind_paired` CHECK).
134
+
135
+ ### Scope-kind system (`scope_kind_schema.ts`)
136
+
137
+ Open string registry tagging the polymorphic `role_grant.scope_id` /
138
+ `role_grant_offer.scope_id` with a machine-readable kind. Mirrors the open
139
+ registry pattern used for `RoleName` / `AuditEventTypeName` /
140
+ `CredentialType`.
141
+
142
+ - `SCOPE_KIND_NAME_REGEX` / `ScopeKindName`: lowercase letters and
143
+ underscores (`^[a-z][a-z_]*[a-z]$|^[a-z]$`), no leading/trailing
144
+ underscore. Same shape as `RoleName`. Uppercase `'GLOBAL'` is
145
+ structurally rejected — it appears only as an index-side token in
146
+ `COALESCE(scope_kind, 'GLOBAL')` inside the partial unique indexes,
147
+ never as a column value.
148
+ - `ScopeKindMeta`: `{description?: string}` — admin-UI-facing copy.
149
+ Open shape so v2 can extend without breaking change.
150
+ - `create_scope_kind_schema(consumer_kinds: Record<string, ScopeKindMeta>)`
151
+ → `{ScopeKind, scope_kinds: ReadonlyMap}`. No builtins. Construction-
152
+ time guards: regex on every name, duplicate detection. Empty registry
153
+ returns `z.never()` — every parse fails. Pass the result into
154
+ `create_role_schema` to validate `RoleSpec.applicable_scope_kinds`
155
+ entries (informative-only in v1; INSERT-time `(role, scope_kind)`
156
+ enforcement reserved for v2).
157
+ - Encoding: paired-null with `scope_id`. Both null = global, both
158
+ non-null = scoped, mismatch rejected by the
159
+ `role_grant_scope_kind_paired` / `role_grant_offer_scope_kind_paired` CHECK
160
+ constraints.
161
+
162
+ ### Credential-type system (`credential_type_schema.ts`)
163
+
164
+ Open string registry over the credential types that can authenticate a
165
+ request. Three builtins (`session`, `api_token`, `daemon_token`); the
166
+ wire-validated `CredentialType` Zod enum in `hono_context.ts` mirrors
167
+ those three. Mirrors the open-registry pattern used for `RoleName` /
168
+ `ScopeKindName` / `GrantPathName` / `AuditEventTypeName`.
169
+
170
+ - `CREDENTIAL_TYPE_NAME_REGEX` / `CredentialTypeName`: lowercase letters
171
+ and underscores. Same shape as `RoleName`.
172
+ - `CREDENTIAL_TYPE_SESSION` / `CREDENTIAL_TYPE_API_TOKEN` /
173
+ `CREDENTIAL_TYPE_DAEMON_TOKEN` — the three builtin literals. The
174
+ constant is named `_API_TOKEN` (not `_BEARER`) so wire literal and
175
+ the `api_token` storage table stay in lockstep.
176
+ - `BUILTIN_CREDENTIAL_TYPES` const tuple, `BuiltinCredentialType` Zod
177
+ enum, `BUILTIN_CREDENTIAL_TYPE_META` admin-UI-facing descriptions.
178
+ - `create_credential_type_schema(consumer_types?)`
179
+ → `{CredentialType, credential_types: ReadonlyMap}`. Builtins always
180
+ present; consumer collisions / regex failures / duplicates throw at
181
+ construction. Pass the result into `create_role_schema`'s optional
182
+ `credential_types` parameter to validate every
183
+ `RoleSpec.required_credential_types` entry at construction time.
184
+
185
+ ### Grant-path system (`grant_path_schema.ts`)
186
+
187
+ Open string registry over the surfaces through which a role can be
188
+ granted. Four builtins (`admin`, `self_service`, `system`, `bootstrap`).
189
+
190
+ - `GRANT_PATH_NAME_REGEX` / `GrantPathName`: lowercase letters and
191
+ underscores, mirrors `RoleName`.
192
+ - `GRANT_PATH_ADMIN` / `_SELF_SERVICE` / `_SYSTEM` / `_BOOTSTRAP` —
193
+ builtin literal constants.
194
+ - `BUILTIN_GRANT_PATHS` const tuple, `BuiltinGrantPath` Zod enum,
195
+ `BUILTIN_GRANT_PATH_META` descriptions.
196
+ - `create_grant_path_schema(consumer_paths?)`
197
+ → `{GrantPath, grant_paths: ReadonlyMap}`. Same construction-time
198
+ guards as the credential-type schema. Pass the result into
199
+ `create_role_schema`'s optional `grant_paths` parameter to validate
200
+ every `RoleSpec.grant_paths` entry at construction time.
201
+
202
+ Drives downstream defaults:
203
+
204
+ - `admin_actions.grantable_roles` ⊇ `{role : 'admin' ∈ grant_paths}`.
205
+ - `self_service_role_actions` default eligibility ⊇
206
+ `{role : 'self_service' ∈ grant_paths}`.
125
207
 
126
208
  ### Role system (`role_schema.ts`)
127
209
 
210
+ `RoleSpec` is the structured per-role configuration that replaced the
211
+ flat `RoleOptions` shape (no `requires_daemon_token` / `web_grantable`
212
+ booleans). Each role declares the credential types its holders must
213
+ use, the scope kinds it applies to, and the grant paths through which
214
+ it can be granted; the factory validates every cross-axis field
215
+ against the corresponding open registries at construction time.
216
+
128
217
  - `RoleName`: lowercase letters + underscores, no leading/trailing
129
218
  underscore.
130
- - `ROLE_KEEPER = 'keeper'` (requires daemon token, not `web_grantable`).
131
- - `ROLE_ADMIN = 'admin'` (web-grantable).
132
- - `BUILTIN_ROLES`, `BuiltinRole` (Zod enum).
133
- - `RoleOptions`: `requires_daemon_token`, `web_grantable` (defaults `false`
134
- and `true`).
135
- - `BUILTIN_ROLE_OPTIONS` — fixed, not overridable by consumers.
136
- - `create_role_schema(app_roles)` call once at startup; returns `{Role, role_options}`.
137
- Collisions with builtin names throw at construction. Used by middleware
138
- to check `requires_daemon_token` and by admin UI to filter `web_grantable`.
219
+ - `ROLE_KEEPER = 'keeper'` bootstrap-only via daemon token; `grant_paths: ['bootstrap']`,
220
+ `required_credential_types: ['daemon_token']`.
221
+ - `ROLE_ADMIN = 'admin'` — admin-grantable; `grant_paths: ['admin']`.
222
+ - `BUILTIN_ROLES`, `BuiltinRole` (Zod enum), `BUILTIN_ROLE_SPECS_BY_NAME`
223
+ (`ReadonlyMap<string, RoleSpec>`) — not overridable by consumers.
224
+ - `RoleSpec`: `{name, description?, required_credential_types?, applicable_scope_kinds?, grant_paths?}`
225
+ every cross-axis field is an open-registry string array. Empty
226
+ arrays carry meaning (`grant_paths: []` role unreachable through
227
+ any registered path; `applicable_scope_kinds: []` global only).
228
+ - `create_role_schema(consumer_roles, options?)` — call once at startup;
229
+ returns `{Role, role_specs}`. Construction-time guards: name regex,
230
+ duplicate detection, builtin-collision rejection, registry-membership
231
+ check on every `required_credential_types` / `applicable_scope_kinds` /
232
+ `grant_paths` entry when the corresponding registry is supplied via
233
+ `options.{credential_types, scope_kinds, grant_paths}`. Omitting a
234
+ registry skips its membership check (incremental adoption hatch).
235
+ - `role_has_grant_path(role_specs, role, path)` /
236
+ `list_roles_with_grant_path(role_specs, path)` — predicate /
237
+ filter helpers used by `admin_actions` and
238
+ `self_service_role_actions` to derive their default eligibility.
139
239
 
140
240
  ### Raw DDL (`ddl.ts`)
141
241
 
@@ -145,8 +245,13 @@ Separated from runtime types to isolate DDL concerns. Consumed by
145
245
  - `ACCOUNT_SCHEMA` (plus `ACCOUNT_EMAIL_INDEX`, `ACCOUNT_USERNAME_CI_INDEX`
146
246
  — both case-insensitive partial uniques)
147
247
  - `ACTOR_SCHEMA`, `ACTOR_INDEX`
148
- - `PERMIT_SCHEMA`, `PERMIT_INDEXES` — v0 has `permit_actor_role_active_unique`
149
- which is replaced in v1 with the scope-aware `permit_actor_role_scope_active_unique`
248
+ - `ROLE_GRANT_SCHEMA`, `ROLE_GRANT_INDEXES` — v0 has `role_grant_actor_role_active_unique`
249
+ which is replaced in v1 with the scope-aware
250
+ `role_grant_actor_role_scope_active_unique` keyed on
251
+ `(actor_id, role, COALESCE(scope_kind, 'GLOBAL'), COALESCE(scope_id, sentinel))`.
252
+ v1 also adds `scope_kind TEXT NULL` (paired-null with `scope_id` via
253
+ the `role_grant_scope_kind_paired` CHECK; idempotent DO-block guards
254
+ re-runs).
150
255
  - `AUTH_SESSION_SCHEMA`, `AUTH_SESSION_INDEXES`
151
256
  - `API_TOKEN_SCHEMA`, `API_TOKEN_INDEX`
152
257
  - `BOOTSTRAP_LOCK_SCHEMA`, `BOOTSTRAP_LOCK_SEED` — seeded as `bootstrapped`
@@ -161,50 +266,50 @@ Separated from runtime types to isolate DDL concerns. Consumed by
161
266
 
162
267
  #### Audit event types
163
268
 
164
- `AUDIT_EVENT_TYPES` — 21 events covering auth + permit + offer + invite +
165
- settings mutations. Offer lifecycle: `permit_offer_create` / `_accept` /
269
+ `AUDIT_EVENT_TYPES` — 21 events covering auth + role_grant + offer + invite +
270
+ settings mutations. Offer lifecycle: `role_grant_offer_create` / `_accept` /
166
271
  `_decline` / `_retract` / `_expire` / `_supersede`. `AuditEventType` is the
167
272
  Zod enum; `AuditOutcome` is `'success' | 'failure'`.
168
273
 
169
- | Event type |
170
- | ------------------------ |
171
- | `login` |
172
- | `logout` |
173
- | `bootstrap` |
174
- | `signup` |
175
- | `password_change` |
176
- | `session_revoke` |
177
- | `session_revoke_all` |
178
- | `token_create` |
179
- | `token_revoke` |
180
- | `token_revoke_all` |
181
- | `permit_grant` |
182
- | `permit_revoke` |
183
- | `permit_offer_create` |
184
- | `permit_offer_accept` |
185
- | `permit_offer_decline` |
186
- | `permit_offer_retract` |
187
- | `permit_offer_expire` |
188
- | `permit_offer_supersede` |
189
- | `invite_create` |
190
- | `invite_delete` |
191
- | `app_settings_update` |
274
+ | Event type |
275
+ | ---------------------------- |
276
+ | `login` |
277
+ | `logout` |
278
+ | `bootstrap` |
279
+ | `signup` |
280
+ | `password_change` |
281
+ | `session_revoke` |
282
+ | `session_revoke_all` |
283
+ | `token_create` |
284
+ | `token_revoke` |
285
+ | `token_revoke_all` |
286
+ | `role_grant_create` |
287
+ | `role_grant_revoke` |
288
+ | `role_grant_offer_create` |
289
+ | `role_grant_offer_accept` |
290
+ | `role_grant_offer_decline` |
291
+ | `role_grant_offer_retract` |
292
+ | `role_grant_offer_expire` |
293
+ | `role_grant_offer_supersede` |
294
+ | `invite_create` |
295
+ | `invite_delete` |
296
+ | `app_settings_update` |
192
297
 
193
298
  #### Metadata schemas
194
299
 
195
300
  - `AUDIT_METADATA_SCHEMAS` — per-type `z.looseObject`. Notable shapes:
196
- - `permit_grant` — `scope_id`, optional `permit_id` (failed grants
197
- omit — `web_grantable` denial never produces a row), optional
301
+ - `role_grant_create` — `scope_id`, optional `role_grant_id` (failed grants
302
+ omit — admin-grant-path denial never produces a row), optional
198
303
  `source_offer_id`, optional `self_service` (set by
199
304
  `self_service_role_actions.ts`; declared on the schema rather than
200
305
  riding on `z.looseObject` so the field is part of the documented surface).
201
- - `permit_revoke` — `scope_id`, optional `reason`, optional
306
+ - `role_grant_revoke` — `scope_id`, optional `reason`, optional
202
307
  `self_service` (same self-service toggle).
203
- - `permit_offer_create` — optional `offer_id` (failed creates omit).
204
- - `permit_offer_supersede` — `reason: 'sibling_accepted' | 'permit_revoked' | 'scope_destroyed'`
205
- plus `cause_id` (accepted offer id, revoked permit id, or destroyed
308
+ - `role_grant_offer_create` — optional `offer_id` (failed creates omit).
309
+ - `role_grant_offer_supersede` — `reason: 'sibling_accepted' | 'role_grant_revoked' | 'scope_destroyed'`
310
+ plus `cause_id` (accepted offer id, revoked role_grant id, or destroyed
206
311
  parent scope row id respectively). The `scope_destroyed` variant is
207
- emitted by callers of `query_permit_revoke_for_scope` when a polymorphic
312
+ emitted by callers of `query_role_grant_revoke_for_scope` when a polymorphic
208
313
  parent scope row is deleted.
209
314
  - `AuditLogEvent` (row); `AuditLogInput<T extends string = AuditEventType>`
210
315
  (narrow metadata when `T` is builtin, generic record otherwise);
@@ -213,16 +318,16 @@ Zod enum; `AuditOutcome` is `'success' | 'failure'`.
213
318
  side so client codegen can import it without dragging in the query layer).
214
319
  `target_actor_id` lives parallel to `target_account_id` on both row
215
320
  and input. **Rule** — `target_actor_id` is populated when the event
216
- subject is bound to a specific actor. Concretely: `permit_revoke`
217
- and `permit_grant` (admin direct-grant, self-service toggle, and
321
+ subject is bound to a specific actor. Concretely: `role_grant_revoke`
322
+ and `role_grant_create` (admin direct-grant, self-service toggle, and
218
323
  in-tx accept all populate both target columns — the grantee is the
219
- subject regardless of initiator), in-tx `permit_offer_accept` on
220
- accept, and `permit_offer_decline` always populate both target
324
+ subject regardless of initiator), in-tx `role_grant_offer_accept` on
325
+ accept, and `role_grant_offer_decline` always populate both target
221
326
  columns (decline joins `from_account_id` into the RETURNING so the
222
327
  "both populated → same account" invariant holds uniformly).
223
- Offer-shape events (`permit_offer_create`, `_expire`, `_retract`,
328
+ Offer-shape events (`role_grant_offer_create`, `_expire`, `_retract`,
224
329
  `_supersede`) populate `target_actor_id` when the offer was
225
- actor-targeted at create time (`permit_offer.to_actor_id` set),
330
+ actor-targeted at create time (`role_grant_offer.to_actor_id` set),
226
331
  null when the offer was account-grain (any actor on
227
332
  `to_account_id` may accept). Account-shape events (login, logout,
228
333
  signup, bootstrap, password change, session/token revoke,
@@ -230,33 +335,33 @@ Zod enum; `AuditOutcome` is `'success' | 'failure'`.
230
335
  `target_actor_id` **and** `actor_id` — the operation is performed
231
336
  by the account, and a multi-actor user must be able to log out
232
337
  (or change password, or revoke sessions) without first picking an
233
- acting actor. Permit/admin/offer events keep recording the
338
+ acting actor. Role-grant/admin/offer events keep recording the
234
339
  initiator's actor in `actor_id`.
235
340
  SSE/WS socket-close keys on `target_account_id ?? account_id`
236
341
  (sessions stay account-grain at the routing layer even though
237
342
  they bind to a specific actor at request-context resolution time —
238
343
  see request_context.ts).
239
- - **Actor-targetable offers** — `permit_offer.to_actor_id` is the
344
+ - **Actor-targetable offers** — `role_grant_offer.to_actor_id` is the
240
345
  optional column that flips an offer from account-grain (null,
241
- default) to actor-grain (non-null). `query_permit_offer_create`
346
+ default) to actor-grain (non-null). `query_role_grant_offer_create`
242
347
  validates the actor↔account binding in one SELECT and rejects with
243
- `PermitOfferActorAccountMismatchError` when the supplied actor isn't
348
+ `RoleGrantOfferActorAccountMismatchError` when the supplied actor isn't
244
349
  on `to_account_id`. `query_accept_offer` rejects wrong-actor accepts
245
- on actor-targeted offers with `PermitOfferActorMismatchError` —
246
- surfaced to RPC callers as `permit_offer_actor_mismatch`. Closes the
350
+ on actor-targeted offers with `RoleGrantOfferActorMismatchError` —
351
+ surfaced to RPC callers as `role_grant_offer_actor_mismatch`. Closes the
247
352
  audit hole where offer-shape events left `target_actor_id` null even
248
353
  when the recipient binding was known at offer time.
249
- - **`emit_permit_target_event` helper** — the canonical entry point
250
- for permit-shape audit emissions. Takes `(ctx, auth, deps, {event_type,
251
- target_account_id, target_actor_id, metadata, outcome?})` and lifts
252
- the `actor_id` / `account_id` / `ip` boilerplate that every
253
- `permit_*` audit emit site repeats. Use this instead of
254
- `audit_log_fire_and_forget` for any event populating one of the
255
- `target_*_id` columns; reach for the lower-level helper only when
256
- the event is non-permit-shape (e.g., `app_settings_update`,
257
- bootstrap, signup).
354
+ - **`AuditEmitter.emit_role_grant_target` method** — the canonical entry
355
+ point for role-grant-shape audit emissions. Takes
356
+ `(ctx, auth, {event_type, target_account_id, target_actor_id, metadata, outcome?})`
357
+ and lifts the `actor_id` / `account_id` / `ip` boilerplate that every
358
+ `role_grant_*` audit emit site repeats. Use this instead of
359
+ `deps.audit.emit` for any event populating one of the
360
+ `target_*_id` columns; reach for the lower-level `emit` only when the
361
+ event is non-role-grant-shape (e.g., `app_settings_update`, bootstrap,
362
+ signup).
258
363
  - Client-safe: `AuditLogEventJson`, `AuditLogEventWithUsernamesJson`,
259
- `PermitHistoryEventJson`, `AdminSessionJson`.
364
+ `RoleGrantHistoryEventJson`, `AdminSessionJson`.
260
365
  - `get_audit_metadata(event)` type-narrows after checking `event_type`.
261
366
  - DDL: `AUDIT_LOG_SCHEMA` (includes monotonically-increasing `seq SERIAL`
262
367
  for cursor-based gap fill), `AUDIT_LOG_INDEXES`.
@@ -264,14 +369,15 @@ target_account_id, target_actor_id, metadata, outcome?})` and lifts
264
369
  builds an `AuditLogConfig` merging builtins with consumer event-type
265
370
  strings keyed to a Zod schema (validates metadata) or `null` (registers
266
371
  without validation). Pass the result to `create_app_backend({audit_log_config})`
267
- — it lands on `AppDeps.audit_log_config` and `audit_log_fire_and_forget`
268
- reads it off the deps bundle automatically (defaults to
372
+ — it gets captured inside the bound `AppDeps.audit` emitter, and every
373
+ call to `audit.emit` validates against it (defaults to
269
374
  `BUILTIN_AUDIT_LOG_CONFIG` when absent). `query_audit_log` still accepts
270
375
  the trailing `config` positional arg for in-transaction emit sites that
271
- don't have `AppDeps`. Builtin collisions and `AuditEventTypeName`
272
- format failures throw at construction. The DB column is `TEXT NOT NULL`
273
- (no enum), so consumer types round-trip through list queries, the
274
- `audit_log_list` RPC, and SSE identically to builtins.
376
+ hold a transaction-scoped DB only. Builtin collisions and
377
+ `AuditEventTypeName` format failures throw at construction. The DB
378
+ column is `TEXT NOT NULL` (no enum), so consumer types round-trip
379
+ through list queries, the `audit_log_list` RPC, and SSE identically to
380
+ builtins.
275
381
  `AuditLogEvent.event_type` (row interface), `AuditLogEventJson.event_type`,
276
382
  and the `audit_log_list` filter input are all `AuditEventTypeName`
277
383
  (regex-validated string) — widened from the closed enum so consumer rows
@@ -290,67 +396,76 @@ target_account_id, target_actor_id, metadata, outcome?})` and lifts
290
396
  accidental mutation (bugs, test cross-contamination, cast escapes)
291
397
  into loud TypeErrors — not a security boundary.
292
398
 
293
- ### Permit offer (`permit_offer_schema.ts`)
399
+ ### Role grant offer (`role_grant_offer_schema.ts`)
294
400
 
295
- The consentful-permits surface. Key constants:
401
+ The consentful-role-grants surface. Key constants:
296
402
 
297
- - `PERMIT_OFFER_SCOPE_SENTINEL_UUID = '00000000-…'` — all-zeros UUID used
403
+ - `ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID = '00000000-…'` — all-zeros UUID used
298
404
  inside `COALESCE(scope_id, sentinel)` in partial unique indexes to collapse
299
405
  NULL scopes into a comparable value. Without this, Postgres's NULL-in-
300
406
  unique-index quirk would allow duplicate global pending offers.
301
- - `PERMIT_OFFER_MESSAGE_LENGTH_MAX = 500`.
302
- - `PERMIT_OFFER_DEFAULT_TTL_MS` = 30 days (GitHub org-invite parity).
407
+ - `ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN = 'GLOBAL'` — index-side token
408
+ for the global case in the partial unique indexes. Uppercase, so it
409
+ cannot collide with consumer-declared `ScopeKindName` values
410
+ (lowercase by regex). Never a column value — both null encodes
411
+ global at the row level.
412
+ - `ROLE_GRANT_OFFER_MESSAGE_LENGTH_MAX = 500`.
413
+ - `ROLE_GRANT_OFFER_DEFAULT_TTL_MS` = 30 days (GitHub org-invite parity).
303
414
 
304
415
  DDL:
305
416
 
306
- - `PERMIT_OFFER_SCHEMA` carries four nullable terminal timestamps:
417
+ - `ROLE_GRANT_OFFER_SCHEMA` carries four nullable terminal timestamps:
307
418
  `accepted_at`, `declined_at`, `retracted_at`, **`superseded_at`** (fourth
308
- terminal — obsoleted by sibling accept or revoke of the resulting permit).
309
- Three CHECK constraints:
310
- - `permit_offer_single_terminal` — at most one terminal timestamp set.
311
- - `permit_offer_permit_iff_accepted` — `(accepted_at IS NOT NULL) = (resulting_permit_id IS NOT NULL)`.
312
- - `permit_offer_reason_iff_declined` — `decline_reason` only on declined rows.
313
- - `PERMIT_OFFER_PENDING_UNIQUE_INDEX` — partial unique on
314
- `(to_account_id, role, COALESCE(scope_id, sentinel), from_actor_id)`
419
+ terminal — obsoleted by sibling accept or revoke of the resulting role_grant).
420
+ Four CHECK constraints:
421
+ - `role_grant_offer_single_terminal` — at most one terminal timestamp set.
422
+ - `role_grant_offer_role_grant_iff_accepted` — `(accepted_at IS NOT NULL) = (resulting_role_grant_id IS NOT NULL)`.
423
+ - `role_grant_offer_reason_iff_declined` — `decline_reason` only on declined rows.
424
+ - `role_grant_offer_scope_kind_paired` — `(scope_kind IS NULL) = (scope_id IS NULL)`
425
+ (both null = global, both non-null = scoped, mismatch rejected).
426
+ - `ROLE_GRANT_OFFER_PENDING_UNIQUE_INDEX` — partial unique on
427
+ `(to_account_id, role, COALESCE(scope_kind, 'GLOBAL'), COALESCE(scope_id, sentinel), from_actor_id)`
315
428
  where all four terminal timestamps are null. Including `from_actor_id`
316
429
  lets multiple grantors coexist (teacher A and B can both offer the same
317
430
  student role). A same-grantor re-offer upserts the pending row. The
318
- `ON CONFLICT` target in `query_permit_offer_create` must match this
319
- expression literally.
320
- - `PERMIT_OFFER_INBOX_INDEX` `(to_account_id, expires_at)` partial on
431
+ `ON CONFLICT` target in `query_role_grant_offer_create` must match this
432
+ expression literally; the paired-null CHECK keeps the two COALESCE
433
+ expressions in lockstep so global rows collide identically whether the
434
+ scope columns are written or omitted.
435
+ - `ROLE_GRANT_OFFER_INBOX_INDEX` — `(to_account_id, expires_at)` partial on
321
436
  pending rows, soonest-expiry first.
322
437
 
323
438
  Types:
324
439
 
325
- - `PermitOffer` (row), `SupersededOffer` (row + `from_account_id` joined
326
- via `actor` — carried so callers fan out `permit_offer_supersede`
440
+ - `RoleGrantOffer` (row), `SupersededOffer` (row + `from_account_id` joined
441
+ via `actor` — carried so callers fan out `role_grant_offer_supersede`
327
442
  notifications without a second round trip).
328
- - `CreatePermitOfferInput` (`expires_at` is required — query layer applies
443
+ - `CreateRoleGrantOfferInput` (`expires_at` is required — query layer applies
329
444
  no default).
330
- - `PermitOfferJson` (with `.meta({description})` on every field) paired
331
- with `to_permit_offer_json(offer)`.
445
+ - `RoleGrantOfferJson` (with `.meta({description})` on every field) paired
446
+ with `to_role_grant_offer_json(offer)`.
332
447
 
333
- ### WS notifications (`permit_offer_notifications.ts`)
448
+ ### WS notifications (`role_grant_offer_notifications.ts`)
334
449
 
335
450
  Six `RemoteNotificationActionSpec`s fan notifications to affected sockets:
336
451
 
337
- | Method | Fires to | Payload |
338
- | ------------------------ | ---------------------------------- | --------------------------------------------------------------------- |
339
- | `permit_offer_received` | Recipient | `{offer: PermitOfferJson}` |
340
- | `permit_offer_retracted` | Recipient | `{offer: PermitOfferJson}` |
341
- | `permit_offer_accepted` | Grantor | `{offer: PermitOfferJson}` |
342
- | `permit_offer_declined` | Grantor | `{offer: PermitOfferJson}` (decline reason on `offer.decline_reason`) |
343
- | `permit_offer_supersede` | Grantor (sibling / revoked-permit) | `{offer, reason: 'sibling_accepted' \| 'permit_revoked', cause_id}` |
344
- | `permit_revoke` | Revokee | `{permit_id, role, scope_id, reason?}` |
452
+ | Method | Fires to | Payload |
453
+ | ---------------------------- | -------------------------------------- | ------------------------------------------------------------------------ |
454
+ | `role_grant_offer_received` | Recipient | `{offer: RoleGrantOfferJson}` |
455
+ | `role_grant_offer_retracted` | Recipient | `{offer: RoleGrantOfferJson}` |
456
+ | `role_grant_offer_accepted` | Grantor | `{offer: RoleGrantOfferJson}` |
457
+ | `role_grant_offer_declined` | Grantor | `{offer: RoleGrantOfferJson}` (decline reason on `offer.decline_reason`) |
458
+ | `role_grant_offer_supersede` | Grantor (sibling / revoked-role_grant) | `{offer, reason: 'sibling_accepted' \| 'role_grant_revoked', cause_id}` |
459
+ | `role_grant_revoke` | Revokee | `{role_grant_id, role, scope_id, reason?}` |
345
460
 
346
- Method constants: `PERMIT_OFFER_RECEIVED_NOTIFICATION_METHOD`,
461
+ Method constants: `ROLE_GRANT_OFFER_RECEIVED_NOTIFICATION_METHOD`,
347
462
  `_RETRACTED_`, `_ACCEPTED_`, `_DECLINED_`, `_SUPERSEDE_`,
348
- `PERMIT_REVOKE_NOTIFICATION_METHOD`. Zod params schemas with inferred type
349
- exports: `PermitOfferReceivedParams`, `_RetractedParams`, `_AcceptedParams`,
350
- `_DeclinedParams`, `_SupersedeParams`, `PermitRevokeParams`. Notification
351
- builders: `build_permit_offer_received_notification(params)` etc.
463
+ `ROLE_GRANT_REVOKE_NOTIFICATION_METHOD`. Zod params schemas with inferred type
464
+ exports: `RoleGrantOfferReceivedParams`, `_RetractedParams`, `_AcceptedParams`,
465
+ `_DeclinedParams`, `_SupersedeParams`, `RoleGrantRevokeParams`. Notification
466
+ builders: `build_role_grant_offer_received_notification(params)` etc.
352
467
 
353
- `PERMIT_OFFER_NOTIFICATION_SPECS: Array<EventSpec>` — pass to
468
+ `ROLE_GRANT_OFFER_NOTIFICATION_SPECS: Array<EventSpec>` — pass to
354
469
  `create_app_server`'s `event_specs` so the attack surface reflects them
355
470
  and DEV-mode `create_validated_broadcaster` catches payload drift.
356
471
 
@@ -359,7 +474,7 @@ and DEV-mode `create_validated_broadcaster` catches payload drift.
359
474
  structurally satisfies it (its signature accepts the broader
360
475
  `JsonrpcMessageFromServerToClient`, contravariantly compatible). Target
361
476
  account travels via the send argument, not the payload — `revoked_by` is
362
- deliberately not in the `permit_revoke` payload (the revokee doesn't need
477
+ deliberately not in the `role_grant_revoke` payload (the revokee doesn't need
363
478
  to learn the admin's identity).
364
479
 
365
480
  ## Queries
@@ -377,8 +492,12 @@ CRUD + listing:
377
492
  indexes).
378
493
  - `query_account_by_username_or_email(deps, input)` — if `@` in input, tries
379
494
  email first; else username first. Single login field accepting either.
380
- - `query_update_account_password`, `query_delete_account` (cascades to
381
- actors, permits, sessions, tokens).
495
+ - `query_update_account_password(deps, id, new_hash, updated_by, expected_hash) → boolean`
496
+ conditional UPDATE keyed on `password_hash = expected_hash`; closes the
497
+ verify-write race where two concurrent password changes both verify
498
+ against the pre-update hash (loaded by the auth phase outside the
499
+ txn). Returns `false` when the racer already moved the row.
500
+ - `query_delete_account` — cascades to actors, role_grants, sessions, tokens.
382
501
  - `query_account_has_any` — used by bootstrap for belt-and-suspenders check.
383
502
  - `query_actors_by_account` — list every actor on an account, ordered
384
503
  by `created_at`. Used by `resolve_acting_actor` to pick the unique
@@ -386,18 +505,24 @@ CRUD + listing:
386
505
  account has multiple actors.
387
506
  - `query_actor_by_id` — direct lookup by id; preferred when the caller
388
507
  already has an actor id in scope.
389
- - `query_admin_account_list` — composes accounts + actors + active permits +
390
- pending inbound offers with **four flat queries** instead of N+1. Pending
391
- offers exclude `message` on purpose (cross-admin visibility). Returns
392
- `Array<AdminAccountEntryJson>`, sorted by `created_at`.
393
-
394
- ### `permit_queries.ts`
395
-
396
- - `query_grant_permit` idempotent; `ON CONFLICT` target and fallback
508
+ - `query_admin_account_list(deps, options?)` — composes accounts + actors +
509
+ active role_grants + pending inbound offers. Paged (`limit` defaults to
510
+ `ADMIN_ACCOUNT_LIST_DEFAULT_LIMIT`; pass `limit: null` for unbounded
511
+ internal use). Two round-trips: 1 (account page) → 3 parallel scoped to
512
+ `account_ids`. The role_grants and offers queries push the page bound
513
+ through to the DB via `actor_id IN (SELECT id FROM actor WHERE
514
+ account_id = ANY(...))` so `actor.id`s never round-trip back to the
515
+ application. Pending offers exclude `message` on purpose (cross-admin
516
+ visibility). Returns `Array<AdminAccountEntryJson>`, sorted by
517
+ `created_at`.
518
+
519
+ ### `role_grant_queries.ts`
520
+
521
+ - `query_create_role_grant` — idempotent; `ON CONFLICT` target and fallback
397
522
  `SELECT` both use `COALESCE(scope_id, sentinel)`. The fallback `SELECT`
398
523
  uses `IS NOT DISTINCT FROM` (plain `=` would miss the NULL-scope conflict
399
524
  case).
400
- - `query_permit_find_active_role_for_actor(deps, permit_id, actor_id)` —
525
+ - `query_role_grant_find_active_role_for_actor(deps, role_grant_id, actor_id)` —
401
526
  actor-scoped read, so IDOR protection is consistent with revoke.
402
527
  Returns `{role, account_id}` (the actor's `account_id` joined in) or
403
528
  `null`. The `account_id` flows into the audit envelope's
@@ -406,92 +531,92 @@ CRUD + listing:
406
531
  the revoke handler into one read closes the small TOCTOU window
407
532
  where the actor row could be deleted between the IDOR check and the
408
533
  actor lookup.
409
- - **`query_revoke_permit(deps, permit_id, actor_id, revoked_by, reason?)`** —
410
- actor-scoped IDOR guard (returns `null` if the permit belongs to a
411
- different actor). Supersedes pending offers for the revoked permit's
534
+ - **`query_revoke_role_grant(deps, role_grant_id, actor_id, revoked_by, reason?)`** —
535
+ actor-scoped IDOR guard (returns `null` if the role_grant belongs to a
536
+ different actor). Supersedes pending offers for the revoked role_grant's
412
537
  `(to_account, role, scope)` in the **same transaction** via a CTE that
413
538
  joins `actor` to surface each sibling's `from_account_id`. Returns
414
- `RevokePermitResult = {id, role, scope_id, superseded_offers}`. Closes the
539
+ `RevokeRoleGrantResult = {id, role, scope_id, superseded_offers}`. Closes the
415
540
  "accept a pre-revoke offer to bypass the revoke" path — the stale offer
416
541
  becomes terminal at revoke time.
417
- - `query_permit_find_active_for_actor`, `query_permit_list_for_actor`.
418
- - `query_permit_has_role(deps, actor_id, role, scope_id?)` — `IS NOT DISTINCT FROM`
542
+ - `query_role_grant_find_active_for_actor`, `query_role_grant_list_for_actor`.
543
+ - `query_role_grant_has_role(deps, actor_id, role, scope_id?)` — `IS NOT DISTINCT FROM`
419
544
  handles the NULL case. Omitted scope matches `scope_id IS NULL` (pre-scope
420
545
  callers keep semantics). Use only when checking an arbitrary `actor_id`
421
546
  that isn't the request actor (e.g., post-mutation verification, scripts,
422
547
  audit-time checks). For the request actor, prefer `has_scoped_role` /
423
- `has_any_scoped_role` on the in-memory `auth.permits` snapshot.
424
- - `query_permit_find_account_id_for_role(deps, role)` — joins
425
- permit → actor → account, returns first match. Used by daemon token
548
+ `has_any_scoped_role` on the in-memory `auth.role_grants` snapshot.
549
+ - `query_role_grant_find_account_id_for_role(deps, role)` — joins
550
+ role_grant → actor → account, returns first match. Used by daemon token
426
551
  middleware to resolve the keeper account.
427
- - `query_permit_revoke_role(deps, actor_id, role, ...)` — revokes every
428
- active permit for `(actor, role)` across all scopes and supersedes all
552
+ - `query_role_grant_revoke_role(deps, actor_id, role, ...)` — revokes every
553
+ active role_grant for `(actor, role)` across all scopes and supersedes all
429
554
  matching pending offers. Returns `RevokeRoleResult = {revoked, superseded_offers}`.
430
- - **`query_permit_revoke_for_scope(deps, scope_id, revoked_by, reason?)`** —
555
+ - **`query_role_grant_revoke_for_scope(deps, scope_id, revoked_by, reason?)`** —
431
556
  parent-scope cascade for polymorphic `scope_id` consumers. Revokes every
432
- active permit at `scope_id` (role-agnostic) and supersedes every pending
557
+ active role_grant at `scope_id` (role-agnostic) and supersedes every pending
433
558
  offer at `scope_id` (tuple-matched and orphan, undifferentiated) in the
434
559
  caller's transaction. Returns `RevokeForScopeResult = {revoked, superseded_offers}`
435
560
  — `revoked` carries both `actor_id` (drives `target_actor_id` audit
436
561
  envelopes) and `account_id` (drives `target_account_id` for socket-close
437
562
  fan-out); `superseded_offers` carries `from_account_id`. Caller emits
438
- `permit_offer_supersede` audits with `reason: 'scope_destroyed'` and
563
+ `role_grant_offer_supersede` audits with `reason: 'scope_destroyed'` and
439
564
  `cause_id: <destroyed scope row id>` per superseded offer (the cause is
440
- the scope deletion, not any individual permit revoke). Use from a
441
- consumer's parent-row delete handler when `permit.scope_id` /
442
- `permit_offer.scope_id` reference rows in a polymorphic table the
565
+ the scope deletion, not any individual role_grant revoke). Use from a
566
+ consumer's parent-row delete handler when `role_grant.scope_id` /
567
+ `role_grant_offer.scope_id` reference rows in a polymorphic table the
443
568
  consumer is about to drop.
444
569
 
445
- ### `permit_offer_queries.ts`
570
+ ### `role_grant_offer_queries.ts`
446
571
 
447
572
  Error classes (all extend `Error` with stable `.name` — never use
448
573
  `instanceof` against plain messages):
449
574
 
450
- - `PermitOfferSelfTargetError` — grantor offered themselves. Enforced
575
+ - `RoleGrantOfferSelfTargetError` — grantor offered themselves. Enforced
451
576
  via a single SELECT on the grantor's `actor.account_id` in
452
- `query_permit_offer_create` (resolving from the grantor side keeps
577
+ `query_role_grant_offer_create` (resolving from the grantor side keeps
453
578
  the check multi-actor-correct — the grantor → account binding stays
454
579
  1:1 by definition of `actor`, while the recipient account may host
455
580
  many actors under multi-actor).
456
- - `PermitOfferAlreadyTerminalError` — offer exists for the caller but is
581
+ - `RoleGrantOfferAlreadyTerminalError` — offer exists for the caller but is
457
582
  accepted / declined / retracted / superseded.
458
- - `PermitOfferExpiredError` — pending but past `expires_at` (distinct from
583
+ - `RoleGrantOfferExpiredError` — pending but past `expires_at` (distinct from
459
584
  terminal; different user-facing story: "ask the grantor to re-send").
460
- - `PermitOfferNotFoundError` — not found or belongs to a different recipient
585
+ - `RoleGrantOfferNotFoundError` — not found or belongs to a different recipient
461
586
  (standard 404-over-403 IDOR mask; callers never reveal which).
462
587
 
463
588
  Queries:
464
589
 
465
- - `query_permit_offer_create` — INSERT with upsert-on-pending keyed by
590
+ - `query_role_grant_offer_create` — INSERT with upsert-on-pending keyed by
466
591
  `(to_account, role, scope, from_actor)`. Same-grantor re-offer refreshes
467
592
  `message` + `expires_at` only. A terminal-state row with the same tuple
468
593
  does not block a fresh INSERT.
469
- - `query_permit_offer_decline(deps, id, to_account_id, reason)` — IDOR
594
+ - `query_role_grant_offer_decline(deps, id, to_account_id, reason)` — IDOR
470
595
  guarded by `to_account_id`. `resolve_terminal_or_missing` helper
471
596
  distinguishes "not found / different recipient" from "already terminal".
472
- - `query_permit_offer_retract(deps, id, from_actor_id)` — IDOR guarded by
597
+ - `query_role_grant_offer_retract(deps, id, from_actor_id)` — IDOR guarded by
473
598
  grantor actor.
474
- - `query_permit_offer_list(deps, to_account_id)` — pending + non-expired +
599
+ - `query_role_grant_offer_list(deps, to_account_id)` — pending + non-expired +
475
600
  non-superseded, soonest expiry first.
476
- - `query_permit_offer_history_for_account(deps, account_id, limit?, offset?)` —
601
+ - `query_role_grant_offer_history_for_account(deps, account_id, limit?, offset?)` —
477
602
  both directions (recipient or grantor), includes terminal rows, newest
478
603
  first.
479
- - `query_permit_offer_find_pending`.
480
- - `query_permit_offer_sweep_expired` — returns pending offers past
481
- `expires_at`; the caller emits `permit_offer_expire` audit events
604
+ - `query_role_grant_offer_find_pending`.
605
+ - `query_role_grant_offer_sweep_expired` — returns pending offers past
606
+ `expires_at`; the caller emits `role_grant_offer_expire` audit events
482
607
  per-row (no tombstone — caller is responsible for idempotency).
483
608
  - **`query_accept_offer(deps, input)`** — atomic, must run inside a
484
609
  transaction. Row-locks with `SELECT ... FOR UPDATE` (concurrent callers
485
610
  block until commit / rollback, then branch idempotently). Inserts the
486
- permit with normal idempotency (`ON CONFLICT DO NOTHING`), stamps
487
- `accepted_at` + `resulting_permit_id` in one UPDATE (satisfying the
488
- `permit_offer_permit_iff_accepted` CHECK), supersedes sibling pending
611
+ role_grant with normal idempotency (`ON CONFLICT DO NOTHING`), stamps
612
+ `accepted_at` + `resulting_role_grant_id` in one UPDATE (satisfying the
613
+ `role_grant_offer_role_grant_iff_accepted` CHECK), supersedes sibling pending
489
614
  offers for `(to_account, role, scope)` via CTE joined to `actor` for
490
- grantor `account_id`, and emits `permit_offer_accept` + `permit_grant`
491
- - one `permit_offer_supersede` per sibling. On race, returns the
492
- pre-existing permit with `created: false` and empty `superseded_offers`
493
- / `audit_events`. Error map: `PermitOfferNotFoundError`,
494
- `PermitOfferAlreadyTerminalError`, `PermitOfferExpiredError`. Sibling
615
+ grantor `account_id`, and emits `role_grant_offer_accept` + `role_grant_create`
616
+ - one `role_grant_offer_supersede` per sibling. On race, returns the
617
+ pre-existing role_grant with `created: false` and empty `superseded_offers`
618
+ / `audit_events`. Error map: `RoleGrantOfferNotFoundError`,
619
+ `RoleGrantOfferAlreadyTerminalError`, `RoleGrantOfferExpiredError`. Sibling
495
620
  supersede is what forecloses the "accept a pre-revoke sibling later to
496
621
  get the role back" path.
497
622
 
@@ -547,8 +672,15 @@ Server-side sessions, keyed by blake3 hash of the session token:
547
672
  - `query_invite_find_unclaimed_match(deps, email, username)` — three scoping
548
673
  modes: email-only invite needs signup-email match; username-only invite
549
674
  needs signup-username match; both-field invite requires both to match.
550
- - `query_invite_claim` — sets `claimed_by` + `claimed_at` only if still
551
- unclaimed. Return is a boolean for race-detection.
675
+ - **`query_invite_claim_unscoped`** — sets `claimed_by` + `claimed_at` only
676
+ if still unclaimed. Return is a boolean for race-detection. The
677
+ `_unscoped` suffix is the safety signal — the SQL only checks the row
678
+ state, not whether the claiming account's email/username matches the
679
+ invite. Production scoping is enforced upstream in `signup_routes.ts`
680
+ via `query_invite_find_unclaimed_match`. Mirrors the
681
+ `query_session_revoke_by_hash_unscoped` precedent — there is no scoped
682
+ sibling because scoping is provided by a separate find query, not an
683
+ alternate variant of this query.
552
684
  - `query_invite_list_all`, `query_invite_list_all_with_usernames` (joins to
553
685
  `actor` for `created_by_username` and `account` for `claimed_by_username`).
554
686
  - `query_invite_delete_unclaimed` — IDOR not a concern (admin-only surface),
@@ -583,34 +715,73 @@ run'` if the seed somehow missed (defensive — migrations always seed).
583
715
  `target_actor_id` is on the row but not currently joined to actor
584
716
  for a name; the admin viewer will resolve via `actor_lookup` /
585
717
  `actor.name` when the actor-grain forensics pass lands.
586
- - `query_audit_log_list_for_account`, `query_audit_log_list_permit_history`
587
- (filters to `permit_grant` / `permit_revoke`).
718
+ - `query_audit_log_list_role_grant_history` (filters to `role_grant_create` / `role_grant_revoke`).
588
719
  - `query_audit_log_cleanup_before`.
589
- - **`audit_log_fire_and_forget(route, input, deps)`**
590
- writes to `route.background_db` (pool-level), so audit entries persist
591
- even when the request transaction rolls back. `deps` is the shared
592
- `AuditEmitDeps` bundle (`{log, on_audit_event, audit_log_config?}`)
593
- from `auth/deps.ts`, so call sites pass the surrounding deps object
594
- directly. Bundling replaces the prior 5-arg positional signature;
595
- consumers that forgot the trailing `config` would silently fall back
596
- to `BUILTIN_AUDIT_LOG_CONFIG`. Write and `on_audit_event` callback
597
- failures are logged separately. Pushes onto `route.pending_effects`
598
- for test flushing.
720
+ - **Audit fan-out runs through `AppDeps.audit`** (the bound emitter built
721
+ by `create_audit_emitter` at backend assembly see §`audit_emitter.ts`).
722
+ `audit.emit(ctx, input)` writes via the captured pool, so audit entries
723
+ persist even when the request transaction rolls back. The emitter
724
+ closes over `on_audit_event` + `audit_log_config` so handlers can never
725
+ silently fall back to the builtin config or a stale callback. Write
726
+ failures and listener-callback failures are logged separately. Pushes
727
+ onto `ctx.pending_effects` for test flushing.
728
+
729
+ ### `audit_emitter.ts`
730
+
731
+ `AuditEmitter` is the bound capability that lives on `AppDeps.audit`,
732
+ built once at `create_app_backend` time.
733
+
734
+ Four methods:
735
+
736
+ - `emit(ctx, input)` — fire-and-forget pool write. Pushes the in-flight
737
+ promise onto `ctx.pending_effects`; errors logged, never thrown.
738
+ Returns `void` (the promise handle is already on `pending_effects`).
739
+ - `emit_role_grant_target(ctx, auth, input)` — wrapper that lifts
740
+ `actor_id` / `account_id` / `ip` boilerplate. Use for any event
741
+ populating one of the `target_*_id` columns; reach for `emit` only on
742
+ non-role-grant-shape events (`app_settings_update`, bootstrap, signup).
743
+ - `emit_pool(input)` — awaitable pool write for code paths without a
744
+ `pending_effects` queue (cleanup sweeps, ad-hoc maintenance scripts).
745
+ Same write-then-notify semantics as `emit`; errors logged + swallowed.
746
+ - `notify(event)` — fan out an already-written audit row to the listener
747
+ chain. Used by `query_accept_offer`'s in-transaction audit batch (see
748
+ the role-grant-offer accept handler) — the row is already in the DB,
749
+ this just walks the chain.
750
+
751
+ Per-call `ctx` shape:
752
+
753
+ - `emit` requires `{pending_effects: Array<Promise<void>>}` — the eager
754
+ queue only. Both `RouteContext` and `ActionContext` satisfy this
755
+ structurally; `audit.emit` pushes its in-flight pool-write promise
756
+ onto the eager queue. See `../http/CLAUDE.md` §Pending Effects for
757
+ the eager / deferred split.
758
+ - `emit_role_grant_target` adds `client_ip: string` (also on `ActionContext`;
759
+ REST handlers pass `{pending_effects, client_ip: get_client_ip(c)}`).
760
+
761
+ `on_event_chain` is the mutable subscriber list. `create_app_server`
762
+ appends `audit_sse.on_audit_event` here when `audit_log_sse` is enabled,
763
+ without rebuilding `AppDeps`.
599
764
 
600
765
  ### `migrations.ts`
601
766
 
602
- - `AUTH_MIGRATION_NAMESPACE = 'fuz_auth'`, `AUTH_MIGRATION_NS` (pre-composed).
767
+ - `AUTH_MIGRATION_NAMESPACE = 'fuz_auth'`, `AUTH_MIGRATION_NS` (pre-composed), `RESERVED_MIGRATION_NAMESPACES: ReadonlyArray<string>` (membership list `create_app_backend` rejects on; consumer-discoverable instead of probing the runtime throw).
603
768
  - `AUTH_MIGRATIONS`:
604
769
  - **v0 `full_auth_schema`** — every table + index + seed for the v1
605
- identity system (account, actor, permit, auth_session, api_token,
770
+ identity system (account, actor, role_grant, auth_session, api_token,
606
771
  audit_log, bootstrap_lock, invite, app_settings). All
607
772
  `IF NOT EXISTS` — idempotent replay.
608
- - **v1 `permit_offer_and_scoped_permits`** — adds `permit_offer` table
609
- plus its two partial indexes; adds `permit.scope_id` /
610
- `permit.source_offer_id` / `permit.revoked_reason`; drops
611
- `permit_actor_role_active_unique` and installs scope-aware
612
- `permit_actor_role_scope_active_unique` using the
613
- `PERMIT_OFFER_SCOPE_SENTINEL_UUID`.
773
+ - **v1 `role_grant_offer_and_scoped_role_grants`** — adds `role_grant_offer` table
774
+ plus its two partial indexes; adds `role_grant.scope_id` /
775
+ `role_grant.scope_kind` / `role_grant.source_offer_id` /
776
+ `role_grant.revoked_reason`; installs the
777
+ `role_grant_scope_kind_paired` CHECK (DO-block guarded for re-runs
778
+ since Postgres has no `ADD CONSTRAINT IF NOT EXISTS` for CHECKs);
779
+ drops `role_grant_actor_role_active_unique` (and the prior
780
+ `role_grant_actor_role_scope_active_unique` if present) and installs the
781
+ scope-kind-aware variant keyed on
782
+ `(actor_id, role, COALESCE(scope_kind, 'GLOBAL'), COALESCE(scope_id, sentinel))`.
783
+ `role_grant_offer` is created with `scope_kind` already in the CREATE
784
+ TABLE (its CHECK + index are inline, not ALTERed).
614
785
  - Forward-only (no down). Migrations are `{name, up}` objects; the name
615
786
  surfaces in error messages.
616
787
 
@@ -636,10 +807,16 @@ by `sequence`, then enforces:
636
807
  While fuz_app is pre-stable, migration bodies, names, and positions can
637
808
  change freely between versions and consumers upgrading across a schema
638
809
  change are expected to drop and re-bootstrap their dev/test databases.
639
- Once the schema is declared stable, a hard append-only-after-publish rule
640
- will apply (with the cliff called out in that release's notes). Until
641
- then bias toward editing the existing migration entries rather than
642
- appending patch migrations.
810
+ **No consumer has a stable production DB at the time of writing** —
811
+ vissiones, zap, mageguild, undying, and fuz_template are all dev-mode
812
+ only. The pre-stable contract assumes this; once a consumer ships a
813
+ production DB, the upgrade story changes shape (operator-side
814
+ migrations, double-emit windows, etc.) and the schema-stability
815
+ declaration becomes load-bearing. Bias toward editing existing
816
+ migration entries rather than appending patch migrations until that
817
+ declaration lands. Once the schema is declared stable, a hard
818
+ append-only-after-publish rule will apply (with the cliff called out in
819
+ that release's notes).
643
820
 
644
821
  `MigrationError` is the only error class thrown from `run_migrations` /
645
822
  `baseline`; branch on `.kind` (never on message text). Kinds:
@@ -711,7 +888,7 @@ assembly order. Two-phase identity:
711
888
 
712
889
  - **Authentication** runs in middleware (session / bearer / daemon
713
890
  token). Sets `c.var.account_id` + `CREDENTIAL_TYPE_KEY` on a valid
714
- credential. Account-only — never loads actor or permits, never
891
+ credential. Account-only — never loads actor or role_grants, never
715
892
  populates `REQUEST_CONTEXT_KEY`. **Production-middleware invariant**:
716
893
  no production middleware on the auth path (session / bearer / daemon
717
894
  token) populates `REQUEST_CONTEXT_KEY`; identity-related context vars
@@ -726,42 +903,55 @@ assembly order. Two-phase identity:
726
903
  actor resolution; production code that consults
727
904
  `REQUEST_CONTEXT_KEY` is reading test escape-hatch state, never live
728
905
  middleware output.
729
- - **Authorization** runs in the route-spec wrapper / RPC dispatcher
730
- before input validation (matches the RPC dispatcher's order so 401 /
731
- 403 surface ahead of `invalid_params`). When the route's input
732
- declares `acting?: ActingActor` or its auth requires permits
733
- (`role` / `keeper`), the authorization phase calls
734
- `resolve_acting_actor` over the raw `acting` value extracted from
735
- query (GET) or pre-parsed body (mutating methods), builds the
736
- actor-bound `RequestContext`, and sets `REQUEST_CONTEXT_KEY` before
737
- the role / keeper guards fire. Account-grain routes skip resolution
738
- and run with `RequestContext.actor: null`. Resolution failures come
739
- back as `AuthorizationFailure` (`{status, body}`) the auth domain
740
- stops short of constructing a `Response` so each transport binds the
741
- same failure to its wire shape: REST emits `c.json(body, status)`;
742
- the WS upgrade does the same; the RPC dispatcher folds it into a
743
- JSON-RPC envelope (`{jsonrpc, id, error: {code, message, data}}`)
744
- with `error.message` carrying the reason string and
745
- `error.data: {reason, ...rest}` flattening any diagnostic fields
746
- (e.g. `available[]` for `actor_required`). The two 500 reasons the
747
- phase emits are kept distinct: `no_actors_on_account` names a signup
748
- invariant violation (`resolve_acting_actor` enumerated zero actors);
749
- `account_vanished` names a torn-read race (`build_request_context` /
750
- `build_account_context` returned null after a successful resolve
751
- the account or actor row was deleted between credential validation
752
- and the dispatcher's follow-up read). See the root
753
- `../../../CLAUDE.md` § Cleanest architecture takes priority for the
754
- rationale.
906
+ - **Authorization** runs after input validation (matches the dispatcher's
907
+ 401 400 403 order so unauthenticated callers don't leak
908
+ `invalid_params` for methods with required input, and the authorization
909
+ phase reads `acting` as a typed Zod field rather than the raw body).
910
+ When the route's input declares `acting?: ActingActor` or its auth
911
+ requires role_grants (`role` / `credential_types`), the authorization
912
+ phase calls `resolve_acting_actor` over the validated `acting` value
913
+ and builds the actor-bound `RequestContext`. Account-grain routes
914
+ skip resolution and run with `RequestContext.actor: null`.
915
+ `apply_authorization_phase` is pure data it takes
916
+ `account_id: string | null` and returns `AuthorizationResult`
917
+ (`{ok: true, request_context: RequestContext | null} | {ok: false, status, body}`)
918
+ without touching the Hono context.
919
+ Public actions and the unauthenticated-optional axis collapse to
920
+ `request_context: null`; resolved actor / account-only contexts set it
921
+ non-null. The REST wrapper (`create_fuz_authorization_handler`) sets
922
+ `REQUEST_CONTEXT_KEY` when `request_context !== null` for downstream
923
+ `require_role` / `require_credential_types`; the HTTP RPC and WS
924
+ dispatchers consume the resolved context directly via `perform_action`.
925
+ Resolution failures surface as `{ok: false, status, body}` the auth
926
+ domain stops short of constructing a `Response` so each transport
927
+ binds the same failure to its wire shape: REST emits
928
+ `c.json(body, status)`; the WS upgrade does the same; the RPC + WS
929
+ dispatchers fold it into a JSON-RPC envelope inside `perform_action`
930
+ (`{jsonrpc, id, error: {code, message, data}}`) with `error.message`
931
+ carrying the reason string and `error.data: {reason, ...rest}`
932
+ flattening any diagnostic fields (e.g. `available[]` for
933
+ `actor_required`). The 500 reasons stay distinct in `body.error`:
934
+ `no_actors_on_account` (signup invariant violation —
935
+ `resolve_acting_actor` enumerated zero actors); `account_vanished`
936
+ (torn-read race — `build_request_context` / `build_account_context`
937
+ returned null after a successful resolve, meaning the account or
938
+ actor row was deleted between credential validation and the
939
+ follow-up read). The named per-error shape `AuthorizationFailureBody`
940
+ is still exported for callers that want to bind the failure body
941
+ by type. See the root `../../../CLAUDE.md` § Cleanest architecture
942
+ takes priority for the rationale.
755
943
 
756
944
  Session parsing is separate from auth enforcement — login / bootstrap
757
- participate in cookie refresh without being blocked. `require_auth` /
758
- `require_role` / `require_keeper` are the gates.
945
+ participate in cookie refresh without being blocked. `require_auth`,
946
+ `require_role(roles)`, and `require_credential_types(types)` are the
947
+ gates; the keeper case composes the credential-type gate with the role
948
+ gate (no dedicated `require_keeper` helper — see `request_context.ts`).
759
949
 
760
950
  ### `request_context.ts`
761
951
 
762
- - `RequestContext = {account, actor: Actor | null, permits}`. `actor`
763
- is null on account-grain routes (no `acting`, no permit-requiring
764
- auth); `permits` is empty in that case.
952
+ - `RequestContext = {account, actor: Actor | null, role_grants}`. `actor`
953
+ is null on account-grain routes (no `acting`, no role_grant-requiring
954
+ auth); `role_grants` is empty in that case.
765
955
  - `REQUEST_CONTEXT_KEY` — Hono context variable name.
766
956
  - **`AUTH_SESSION_TOKEN_HASH_KEY`** — holds the blake3 session hash. Set on
767
957
  successful session lookup; `null` for unauthenticated or non-session
@@ -770,19 +960,21 @@ participate in cookie refresh without being blocked. `require_auth` /
770
960
  stream on `session_revoke`).
771
961
  - `get_request_context(c)`, `require_request_context(c)` (throws on
772
962
  misuse — handler ran without authorization phase wiring).
773
- - **In-memory permit predicates** — `has_role(ctx, role, now?)`,
963
+ - **In-memory role_grant predicates** — `has_role(ctx, role, now?)`,
774
964
  `has_scoped_role(ctx, role, scope_id, now?)`,
775
965
  `has_any_scoped_role(ctx, roles, scope_id, now?)`. All three take
776
966
  `RequestContext | null` and return `false` for null ctx and for
777
- account-grain ctx (`actor: null`, empty `permits`); they drop into
778
- `auth: 'public'` and account-grain handlers without a manual narrow.
779
- `scope_id === null` matches global permits only; UUID matches that
967
+ account-grain ctx (`actor: null`, empty `role_grants`); they drop into
968
+ public (`{account: 'none', actor: 'none'}`) and account-grain
969
+ (`{account: 'required', actor: 'none'}`) handlers without a manual
970
+ narrow.
971
+ `scope_id === null` matches global role_grants only; UUID matches that
780
972
  exact scope. Empty `roles` short-circuits `has_any_scoped_role` to
781
973
  `false`. Decide-time predicates only — the predicate / mutation
782
- race window is the same as the SQL `query_permit_has_role` style and
974
+ race window is the same as the SQL `query_role_grant_has_role` style and
783
975
  only a transactional re-check inside the UPDATE/INSERT closes it.
784
976
  - `build_request_context(deps, account_id, actor_id)` — loads
785
- `account` + the named `actor` + active permits. Verifies
977
+ `account` + the named `actor` + active role_grants. Verifies
786
978
  `actor.account_id === account.id`; returns `null` when the account
787
979
  or actor is missing, or when they don't bind to each other. Called
788
980
  by the authorization phase after `resolve_acting_actor` succeeds —
@@ -796,19 +988,35 @@ participate in cookie refresh without being blocked. `require_auth` /
796
988
  available list when multi-actor and `acting` is missing;
797
989
  `actor_not_on_account` when supplied id doesn't belong; `no_actors`
798
990
  defensively.
799
- - `refresh_permits(ctx, deps)` — reloads permits without mutating the
991
+ - `refresh_role_grants(ctx, deps)` — reloads role_grants without mutating the
800
992
  original (concurrent-safe). Useful for long-lived WebSocket
801
993
  connections that have an acting actor.
802
994
  - `create_request_context_middleware(deps, log, session_context_key?)` —
803
995
  validates the session and sets `c.var.account_id` +
804
996
  `CREDENTIAL_TYPE_KEY = 'session'` + `AUTH_SESSION_TOKEN_HASH_KEY`.
805
- Touches the session fire-and-forget. Does not load actor / permits.
997
+ Touches the session fire-and-forget. Does not load actor / role_grants.
806
998
  - `require_auth` — 401 (`ERROR_AUTHENTICATION_REQUIRED`) when
807
999
  `account_id` is null. Does not require an acting actor.
808
- - `require_role(role)` — 401 on no auth, 403
809
- (`ERROR_INSUFFICIENT_PERMISSIONS` + `required_role`) when permits
810
- don't carry the role. Implies the authorization phase ran (a
811
- role-gated route always resolves an actor).
1000
+ - `require_role(roles: ReadonlyArray<string>)` — 401 on no auth, 403
1001
+ (`ERROR_INSUFFICIENT_PERMISSIONS` + `required_roles: ReadonlyArray<string>`)
1002
+ when role_grants don't carry any of `roles` at **global / unscoped**
1003
+ scope. Implies the authorization phase ran (a role-gated route always
1004
+ resolves an actor). Implemented via `has_any_scoped_role(ctx, roles, null)`
1005
+ — a scoped role_grant (`{role: 'admin', scope_id: <uuid>}`) does **not**
1006
+ unlock unscoped role gates. Single-role specs pass `[role_name]`;
1007
+ multi-role specs pass `[r1, r2, ...]` for any-of disjunction. The
1008
+ same scope-aware semantics are mirrored in the HTTP RPC dispatcher
1009
+ (`actions/action_rpc.ts`), the WS dispatcher
1010
+ (`actions/register_action_ws.ts`), and the admin bypasses inside
1011
+ `role_grant_offer_actions.ts` so all four sites agree.
1012
+ - `require_credential_types(types: ReadonlyArray<string>)` — 401 on no
1013
+ auth, 403 (`ERROR_CREDENTIAL_TYPE_REQUIRED` + `required_credential_types`
1014
+ echoing the spec's allowlist — symmetric with the role gate's
1015
+ `required_roles`) when `c.var.credential_type` is not in `types`.
1016
+ Composed with `require_role` for keeper specs (credential gate runs
1017
+ before role gate per `auth_guard_resolver.ts`). Replaces the deleted
1018
+ `require_keeper` helper — keeper is now a composable shape:
1019
+ `{roles: ['keeper'], credential_types: ['daemon_token']}`.
812
1020
 
813
1021
  ### `bearer_auth.ts`
814
1022
 
@@ -825,32 +1033,27 @@ participate in cookie refresh without being blocked. `require_auth` /
825
1033
  - Rate limiter: `record` before async DB work to close the TOCTOU window;
826
1034
  `reset` on valid token.
827
1035
 
828
- ### `require_keeper.ts`
829
-
830
- Two-part type guard:
831
-
832
- 1. `credential_type` must be `'daemon_token'` (not session, not API token).
833
- A session cookie from the bootstrap account still fails this check.
834
- 2. Active `keeper` permit.
1036
+ ### Keeper auth (no dedicated module)
835
1037
 
836
- Returns 401 on no context, 403 (`ERROR_KEEPER_REQUIRES_DAEMON_TOKEN` or
837
- `ERROR_INSUFFICIENT_PERMISSIONS`) otherwise.
1038
+ Keeper is a composable `RouteAuth` shape, not a dedicated guard:
1039
+ `{account: 'required', actor: 'required', roles: ['keeper'],
1040
+ credential_types: ['daemon_token']}`. The two-part check is
1041
+ `require_credential_types(['daemon_token'])` (403
1042
+ `ERROR_CREDENTIAL_TYPE_REQUIRED` + `required_credential_types: ['daemon_token']`)
1043
+ followed by `require_role(['keeper'])` (403
1044
+ `ERROR_INSUFFICIENT_PERMISSIONS`).
838
1045
 
839
- ### `session_middleware.ts` + `session_lifecycle.ts`
840
-
841
- `session_middleware.ts`:
1046
+ ### `session_middleware.ts`
842
1047
 
843
1048
  - `get_session_cookie`, `set_session_cookie`, `clear_session_cookie`.
844
1049
  - `create_session_middleware(keyring, options)` — always sets the
845
1050
  identity on context (null when invalid/missing) for type-safe reads.
846
1051
  Acts on `process_session_cookie`'s `action` (`'clear'` / `'refresh'` /
847
1052
  `'none'`).
848
-
849
- `session_lifecycle.ts` — shared by login and bootstrap:
850
-
851
1053
  - `create_session_and_set_cookie({keyring, deps, c, account_id, session_options, max_sessions?})` —
852
- generates token, hashes, persists `auth_session`, optionally enforces
853
- per-account cap, signs the cookie.
1054
+ shared by login, signup, and bootstrap: generates token, hashes,
1055
+ persists `auth_session`, optionally enforces per-account cap, signs
1056
+ the cookie.
854
1057
 
855
1058
  ### `daemon_token_middleware.ts`
856
1059
 
@@ -859,7 +1062,7 @@ Returns 401 on no context, 403 (`ERROR_KEEPER_REQUIRES_DAEMON_TOKEN` or
859
1062
  or `null` if `$HOME` unset.
860
1063
  - `write_daemon_token(runtime, path, token)` — atomic (temp + rename);
861
1064
  `chmod 0600` if available.
862
- - `resolve_keeper_account_id(deps)` — wraps `query_permit_find_account_id_for_role(ROLE_KEEPER)`.
1065
+ - `resolve_keeper_account_id(deps)` — wraps `query_role_grant_find_account_id_for_role(ROLE_KEEPER)`.
863
1066
  - `start_daemon_token_rotation(runtime, deps, options, log)` — writes initial
864
1067
  token, resolves keeper, sets up interval. Returns `{state, stop}`. The
865
1068
  interval guard `writing` skips the next rotation if the prior write is
@@ -911,18 +1114,17 @@ Session-based auth route specs. Factory: `create_account_route_specs(deps, optio
911
1114
  `account_verify` RPC action — that surface carries the typed
912
1115
  `SessionAccountJson` payload.
913
1116
  - `create_account_status_route_spec(options?)` — `GET /api/account/status`
914
- returns `{account, actor, permits}` on 200 or 401 with optional
1117
+ returns `{account, actor, role_grants}` on 200 or 401 with optional
915
1118
  `bootstrap_available` flag. `actor` is the caller's own
916
1119
  `ActorSummaryJson` so clients don't need to derive `actor_id` from
917
- the permit list. Lets the frontend fetch both session state
1120
+ the role_grant list. Lets the frontend fetch both session state
918
1121
  and bootstrap availability in one request (eliminates a separate `/health`
919
1122
  round trip).
920
1123
 
921
- Post-2026-04-23 RPC migration: session listing/revoke + revoke-all
922
- and API token CRUD live in `account_actions.ts` (see
923
- `account_session_list` / `_revoke` / `_revoke_all`,
924
- `account_token_create` / `_list` / `_revoke` below). Each keeps its
925
- guards (IDOR via `query_session_revoke_for_account` /
1124
+ Session listing/revoke + revoke-all and API token CRUD live in
1125
+ `account_actions.ts` (see `account_session_list` / `_revoke` /
1126
+ `_revoke_all`, `account_token_create` / `_list` / `_revoke` below).
1127
+ Each keeps its guards (IDOR via `query_session_revoke_for_account` /
926
1128
  `query_revoke_api_token_for_account`; `Blake3Hash` on session ids;
927
1129
  `ApiTokenId` regex on token ids; `max_tokens` enforcement via
928
1130
  `query_api_token_enforce_limit`).
@@ -959,31 +1161,34 @@ Constants:
959
1161
  - `POST /signup` — `transaction: false` (manages its own). When
960
1162
  `app_settings.open_signup` is false, requires a matching unclaimed invite.
961
1163
  On `open_signup: true` path, no invite check.
962
- - Transaction body: `query_create_account_with_actor` → `query_invite_claim`
1164
+ - Transaction body: `query_create_account_with_actor` → `query_invite_claim_unscoped`
963
1165
  (if invite present; throws `SignupConflictError` on race — another claim
964
1166
  won) → `create_session_and_set_cookie`. Catches
965
1167
  `is_pg_unique_violation(e)` → 409 `ERROR_SIGNUP_CONFLICT` (username or
966
1168
  email already exists).
967
1169
  - Error shapes: 403 `ERROR_NO_MATCHING_INVITE`, 409 `ERROR_SIGNUP_CONFLICT`.
968
1170
 
969
- ### `route_guards.ts`
1171
+ ### `auth_guard_resolver.ts`
970
1172
 
971
- `fuz_auth_guard_resolver: AuthGuardResolver` — maps `RouteAuth` discriminants
972
- (`'none'` | `'authenticated'` | `'role'` | `'keeper'`) to middleware arrays.
1173
+ `fuz_auth_guard_resolver: AuthGuardResolver` — maps the four-axis
1174
+ `RouteAuth` shape to two-phase middleware arrays. `pre_validation`
1175
+ gets `require_auth` when `account === 'required'` or `actor === 'required'`;
1176
+ `post_authorization` gets `require_credential_types(types)` when
1177
+ `credential_types?.length` and `require_role(roles)` when `roles?.length`.
973
1178
  Injected into `apply_route_specs` so the generic HTTP framework stays
974
1179
  auth-agnostic (see `../http/CLAUDE.md` §Validation pipeline for where it plugs in).
975
1180
 
976
- ### `audit_log_routes.ts` (post-RPC-migration state)
1181
+ ### `audit_log_routes.ts`
977
1182
 
978
- The 2026-04-22 RPC migration moved audit-log list + permit-history reads
979
- (plus admin session listing) to `admin_actions.ts`. The sole remaining
980
- REST concern is the optional SSE stream:
1183
+ Audit-log list + role_grant-history reads (plus admin session listing)
1184
+ live on the RPC surface in `admin_actions.ts`. The REST surface this
1185
+ module produces is now just the optional SSE stream:
981
1186
 
982
1187
  - **`GET /audit/stream`** — optional, wired only when
983
1188
  `AuditLogRouteOptions.stream` is passed. Streams aren't an RPC concern.
984
1189
  Uses `AUTH_SESSION_TOKEN_HASH_KEY` for SSE `scope` identity (so
985
1190
  `session_revoke` can close only that session's stream); `groups: [account_id]`
986
- for coarse close on `permit_revoke` / `session_revoke_all` / `password_change`.
1191
+ for coarse close on `role_grant_revoke` / `session_revoke_all` / `password_change`.
987
1192
 
988
1193
  `create_audit_log_route_specs(options?)` — returns an empty array when
989
1194
  `options.stream` is not set; `required_role` defaults to `'admin'`.
@@ -999,7 +1204,7 @@ Each surface is split across two files:
999
1204
  (no per-method `*_METHOD` string constants — read `.method` off the spec),
1000
1205
  and `all_*_action_specs: Array<RequestResponseActionSpec>` codegen-ready
1001
1206
  registry. Plus any reason-string constants exported to the wire contract
1002
- (e.g. `ERROR_OFFER_*` for permit offers).
1207
+ (e.g. `ERROR_ROLE_GRANT_OFFER_*` for role_grant offers).
1003
1208
  - `*_actions.ts` — `create_*_actions(deps, options) => Array<RpcAction>` factory
1004
1209
  containing handler closures, the `*ActionDeps` / `*ActionOptions` interfaces,
1005
1210
  and any handler-only helpers. Imports the specs from its sibling.
@@ -1010,25 +1215,29 @@ skips the handler module's transitive query-layer deps.
1010
1215
 
1011
1216
  ### `admin_action_specs.ts` + `admin_actions.ts` — eleven admin-only RPC actions
1012
1217
 
1013
- Authorization is **spec-level** (`auth: {role: 'admin'}`) so the dispatcher
1014
- enforces admin before the handler runs. `permit_revoke` in
1015
- `permit_offer_actions.ts` uses the same spec-level gate even though its
1016
- sibling methods are authenticated-but-not-admin the dispatcher checks
1017
- auth per-spec, so mixed-auth endpoints compose cleanly.
1018
-
1019
- | Spec | Side effects | Rate limit | Input | Output |
1020
- | -------------------------------------- | ------------ | ----------- | --------------------------------------------------------- | ----------------------------- |
1021
- | `admin_account_list_action_spec` | false | | `z.void()` | `{accounts, grantable_roles}` |
1022
- | `admin_session_list_action_spec` | false | | `z.void()` | `{sessions}` |
1023
- | `admin_session_revoke_all_action_spec` | true | `'account'` | `{account_id}` | `{ok, count}` |
1024
- | `admin_token_revoke_all_action_spec` | true | `'account'` | `{account_id}` | `{ok, count}` |
1025
- | `audit_log_list_action_spec` | false | | `{event_type?, account_id?, limit?, offset?, since_seq?}` | `{events}` |
1026
- | `audit_log_permit_history_action_spec` | false | | `{limit?, offset?}` | `{events}` |
1027
- | `invite_create_action_spec` | true | `'account'` | `{email?, username?}` | `{ok, invite}` |
1028
- | `invite_list_action_spec` | false | | `z.void()` | `{invites}` |
1029
- | `invite_delete_action_spec` | true | `'account'` | `{invite_id}` | `{ok}` |
1030
- | `app_settings_get_action_spec` | false | | `z.void()` | `{settings}` |
1031
- | `app_settings_update_action_spec` | true | `'account'` | `{open_signup}` | `{ok, settings}` |
1218
+ Authorization is **spec-level** every admin spec declares
1219
+ `auth: {account: 'required', actor: 'required', roles: ['admin']}` so
1220
+ the dispatcher enforces admin before the handler runs. `role_grant_revoke`
1221
+ in `role_grant_offer_actions.ts` uses the same spec-level gate even
1222
+ though its sibling methods are authenticated-but-not-admin the
1223
+ dispatcher checks auth per-spec, so mixed-auth endpoints compose
1224
+ cleanly. Every admin input declares `acting?: ActingActor` per
1225
+ registry-time invariant 2 (the `actor !== 'none' input declares
1226
+ acting?: ActingActor` biconditional).
1227
+
1228
+ | Spec | Side effects | Rate limit | Input | Output |
1229
+ | ------------------------------------------ | ------------ | ----------- | --------------------------------------------------------- | ----------------------------- |
1230
+ | `admin_account_list_action_spec` | false | | `{limit?, offset?}` | `{accounts, grantable_roles}` |
1231
+ | `admin_session_list_action_spec` | false | | `z.void()` | `{sessions}` |
1232
+ | `admin_session_revoke_all_action_spec` | true | `'account'` | `{account_id}` | `{ok, count}` |
1233
+ | `admin_token_revoke_all_action_spec` | true | `'account'` | `{account_id}` | `{ok, count}` |
1234
+ | `audit_log_list_action_spec` | false | | `{event_type?, account_id?, limit?, offset?, since_seq?}` | `{events}` |
1235
+ | `audit_log_role_grant_history_action_spec` | false | | `{limit?, offset?}` | `{events}` |
1236
+ | `invite_create_action_spec` | true | `'account'` | `{email?, username?}` | `{ok, invite}` |
1237
+ | `invite_list_action_spec` | false | | `z.void()` | `{invites}` |
1238
+ | `invite_delete_action_spec` | true | `'account'` | `{invite_id}` | `{ok}` |
1239
+ | `app_settings_get_action_spec` | false | | `z.void()` | `{settings}` |
1240
+ | `app_settings_update_action_spec` | true | `'account'` | `{open_signup}` | `{ok, settings}` |
1032
1241
 
1033
1242
  Mutating admin specs declare `rate_limit: 'account'` — keyed on the
1034
1243
  admin's `request_context.actor.id`. The dispatcher's per-action hook
@@ -1040,8 +1249,7 @@ per actor — permissive enough for any human admin workflow, slow enough
1040
1249
  that scripted oracles surface in audit. Tighten downstream via
1041
1250
  `AppServerOptions.action_account_rate_limiter`.
1042
1251
 
1043
- `AUDIT_LOG_LIST_LIMIT_MAX = 200` — page size clamp (mirrors the former REST
1044
- route).
1252
+ `AUDIT_LOG_LIST_LIMIT_MAX = 200` — page size clamp. `ADMIN_ACCOUNT_LIST_DEFAULT_LIMIT = 50` / `ADMIN_ACCOUNT_LIST_LIMIT_MAX = 200` — same shape on `admin_account_list`.
1045
1253
 
1046
1254
  Error reasons returned via `error.data.reason`:
1047
1255
 
@@ -1056,9 +1264,9 @@ Audit events fired by handlers (all pass `ip: ctx.client_ip` for
1056
1264
  transport-uniform forensics — matches the REST convention and the
1057
1265
  self-service `account_actions.ts` surface):
1058
1266
 
1059
- - `session_revoke_all` / `token_revoke_all` via `audit_log_fire_and_forget`
1060
- (mirrors the former REST behavior). Both also emit an
1061
- `outcome: 'failure'` row on the `ERROR_ACCOUNT_NOT_FOUND` 404 path for
1267
+ - `session_revoke_all` / `token_revoke_all` via `deps.audit.emit`. Both
1268
+ also emit an `outcome: 'failure'` row on the `ERROR_ACCOUNT_NOT_FOUND`
1269
+ 404 path for
1062
1270
  forensic visibility — `target_account_id` is null (FK to `account`
1063
1271
  rejects references to missing ids), and the probed id is preserved
1064
1272
  under `metadata.attempted_account_id`. Metadata schema widening in
@@ -1069,8 +1277,9 @@ self-service `account_actions.ts` surface):
1069
1277
 
1070
1278
  Closure state:
1071
1279
 
1072
- - `grantable_roles` is derived once from `options.roles?.role_options ?? BUILTIN_ROLE_OPTIONS`
1073
- (the `web_grantable` subset) and closed over by the `admin_account_list` handler.
1280
+ - `grantable_roles` is derived once from `options.roles?.role_specs ?? BUILTIN_ROLE_SPECS_BY_NAME`
1281
+ via `list_roles_with_grant_path(_, GRANT_PATH_ADMIN)` and closed over
1282
+ by the `admin_account_list` handler.
1074
1283
  - `options.app_settings` — when provided, captured by the
1075
1284
  `app_settings_get` / `app_settings_update` handlers. Update handler
1076
1285
  **mutates the ref** (`open_signup`, `updated_at`, `updated_by`) so
@@ -1082,70 +1291,79 @@ Closure state:
1082
1291
  `all_admin_action_specs: Array<RequestResponseActionSpec>` — codegen-ready
1083
1292
  registry of all eleven specs (always includes the two app-settings specs).
1084
1293
 
1085
- Deps: `AdminActionDeps = AuditEmitDeps` — the shared `Pick<AppDeps, 'log' | 'on_audit_event' | 'audit_log_config'>` slice every audit-emitting site picks (defined in `auth/deps.ts`). The `audit_log_config` slot flows through to `audit_log_fire_and_forget` so consumer-extended event-type metadata gets validated.
1294
+ Deps: `Pick<RouteFactoryDeps, 'log' | 'audit'>` `log` for handler-side reporting, `audit` for the bound emitter (which captures `on_audit_event` + the optional `AuditLogConfig` so consumer-extended event-type metadata gets validated).
1086
1295
 
1087
- ### `permit_offer_action_specs.ts` + `permit_offer_actions.ts` — seven RPC actions
1296
+ ### `role_grant_offer_action_specs.ts` + `role_grant_offer_actions.ts` — seven RPC actions
1088
1297
 
1089
- > **Hazard — admin `permit_offer_create` does not auto-accept.** The action
1090
- > returns `{offer}` only — no `permit` is inserted. Acceptance is a separate
1091
- > RPC call (`permit_offer_accept`); admin-side tests that need to materialize
1092
- > a permit synchronously call `query_accept_offer` directly (see the
1298
+ > **Hazard — admin `role_grant_offer_create` does not auto-accept.** The action
1299
+ > returns `{offer}` only — no `role_grant` is inserted. Acceptance is a separate
1300
+ > RPC call (`role_grant_offer_accept`); admin-side tests that need to materialize
1301
+ > a role_grant synchronously call `query_accept_offer` directly (see the
1093
1302
  > `offer_and_accept` helper in `testing/admin_integration.ts`). The CHANGELOG
1094
- > v0.31 entry "admin grant_permit routes emit offers instead of direct
1303
+ > v0.31 entry "admin create_role_grant routes emit offers instead of direct
1095
1304
  > grants" was the first signal of this two-step flow; consumers reading the
1096
1305
  > standard admin suite assume auto-accept and have to redesign their tests
1097
1306
  > when they discover otherwise. If you need direct grant for a programmatic
1098
- > path that already proves consent, reach for `query_grant_permit` rather
1307
+ > path that already proves consent, reach for `query_create_role_grant` rather
1099
1308
  > than the RPC action.
1100
1309
 
1101
- Six offer-lifecycle methods plus `permit_revoke`. Authorization is a mix:
1102
-
1103
- - `permit_offer_create` `auth: 'authenticated'`. The **`web_grantable`
1104
- gate runs first**, then the `PermitOfferCreateAuthorize` callback
1105
- (default: caller holds the offered role globally). Consumers can only
1106
- tighten, never loosen past `web_grantable`.
1107
- - `permit_offer_accept` / `_decline` / `_retract` — `authenticated`; IDOR
1108
- guards in the `query_*` layer.
1109
- - `permit_offer_list` / `_history` `side_effects: false` so GET-addressable;
1110
- **input-dependent elevation** `'authenticated'` at the spec level so
1111
- any caller reaches their own inbox, then the handler requires admin
1112
- when `{account_id}` refers to another account. The spec can't express
1113
- this because auth runs before input parsing.
1114
- `permit_offer_history` accepts `limit` (1–500, default 100) + `offset`.
1115
- - **`permit_revoke`** — spec-level `auth: {role: 'admin'}`; the RPC
1116
- dispatcher rejects non-admin callers before the handler runs. Keys on
1117
- **`actor_id`, not `account_id`** permits are actor-scoped and deriving
1118
- actor from account collapses under multi-actor accounts.
1310
+ Six offer-lifecycle methods plus `role_grant_revoke`. Every input
1311
+ declares `acting?: ActingActor` so every spec maps to
1312
+ `{account: 'required', actor: 'required', ...}` per registry-time
1313
+ invariant 2. Authorization tier is the differentiator:
1314
+
1315
+ - `role_grant_offer_create` `auth: {account: 'required', actor: 'required'}`.
1316
+ The **admin-grant-path gate runs first** (the offered role's
1317
+ `RoleSpec.grant_paths` must include `'admin'` /
1318
+ `GRANT_PATH_ADMIN`), then the `RoleGrantOfferCreateAuthorize`
1319
+ callback (default: caller holds the offered role globally).
1320
+ Consumers can only tighten, never loosen past the admin-grant-path
1321
+ gate.
1322
+ - `role_grant_offer_accept` / `_decline` / `_retract`
1323
+ `{account: 'required', actor: 'required'}`; IDOR guards in the
1324
+ `query_*` layer.
1325
+ - `role_grant_offer_list` / `_history` `side_effects: false` so GET-addressable;
1326
+ **input-dependent elevation**`{account: 'required', actor: 'required'}`
1327
+ at the spec level so any caller reaches their own inbox, then the
1328
+ handler requires admin when `{account_id}` refers to another account.
1329
+ The spec can't express this because auth runs before input parsing.
1330
+ `role_grant_offer_history` accepts `limit` (1–500, default 100) + `offset`.
1331
+ - **`role_grant_revoke`** — spec-level
1332
+ `auth: {account: 'required', actor: 'required', roles: ['admin']}`;
1333
+ the RPC dispatcher rejects non-admin callers before the handler runs.
1334
+ Keys on **`actor_id`, not `account_id`** — role_grants are
1335
+ actor-scoped and deriving actor from account collapses under
1336
+ multi-actor accounts.
1119
1337
 
1120
1338
  Every input row below also carries the shared `acting?: ActingActor`
1121
1339
  field that the dispatcher's authorization phase reads off the raw
1122
1340
  params (omitted from the table for brevity).
1123
1341
 
1124
- | Spec | Input | Output |
1125
- | ---------------------------------- | ---------------------------------------------------------- | ------------------------------------------ |
1126
- | `permit_offer_create_action_spec` | `{to_account_id, to_actor_id?, role, scope_id?, message?}` | `{offer}` |
1127
- | `permit_offer_accept_action_spec` | `{offer_id}` | `{permit_id, offer, superseded_offer_ids}` |
1128
- | `permit_offer_decline_action_spec` | `{offer_id, reason?}` | `{ok}` |
1129
- | `permit_offer_retract_action_spec` | `{offer_id}` | `{ok}` |
1130
- | `permit_offer_list_action_spec` | `{account_id?}` | `{offers}` |
1131
- | `permit_offer_history_action_spec` | `{account_id?, limit?, offset?}` | `{offers}` |
1132
- | `permit_revoke_action_spec` | `{actor_id, permit_id, reason?}` | `{ok, revoked}` |
1342
+ | Spec | Input | Output |
1343
+ | -------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------- |
1344
+ | `role_grant_offer_create_action_spec` | `{to_account_id, to_actor_id?, role, scope_id?, message?}` | `{offer}` |
1345
+ | `role_grant_offer_accept_action_spec` | `{offer_id}` | `{role_grant_id, offer, superseded_offer_ids}` |
1346
+ | `role_grant_offer_decline_action_spec` | `{offer_id, reason?}` | `{ok}` |
1347
+ | `role_grant_offer_retract_action_spec` | `{offer_id}` | `{ok}` |
1348
+ | `role_grant_offer_list_action_spec` | `{account_id?}` | `{offers}` |
1349
+ | `role_grant_offer_history_action_spec` | `{account_id?, limit?, offset?}` | `{offers}` |
1350
+ | `role_grant_revoke_action_spec` | `{actor_id, role_grant_id, reason?}` | `{ok, revoked}` |
1133
1351
 
1134
1352
  Error reason constants (exported as `as const` literals):
1135
1353
 
1136
- - `ERROR_OFFER_SELF_TARGET` (`'offer_self_target'`)
1137
- - `ERROR_OFFER_TERMINAL` (`'offer_terminal'`)
1138
- - `ERROR_OFFER_EXPIRED` (`'offer_expired'`)
1139
- - `ERROR_OFFER_NOT_FOUND` (`'offer_not_found'` — 404-over-403 IDOR mask)
1140
- - `ERROR_OFFER_ROLE_NOT_GRANTABLE` (`'offer_role_not_grantable'`)
1141
- - `ERROR_OFFER_NOT_AUTHORIZED` (`'offer_not_authorized'`)
1142
- - `ERROR_OFFER_ACTOR_ACCOUNT_MISMATCH` (`'offer_actor_account_mismatch'` —
1143
- `permit_offer_create` was called with a `to_actor_id` that does not
1354
+ - `ERROR_ROLE_GRANT_OFFER_SELF_TARGET` (`'role_grant_offer_self_target'`)
1355
+ - `ERROR_ROLE_GRANT_OFFER_TERMINAL` (`'role_grant_offer_terminal'`)
1356
+ - `ERROR_ROLE_GRANT_OFFER_EXPIRED` (`'role_grant_offer_expired'`)
1357
+ - `ERROR_ROLE_GRANT_OFFER_NOT_FOUND` (`'role_grant_offer_not_found'` — 404-over-403 IDOR mask)
1358
+ - `ERROR_ROLE_GRANT_OFFER_ROLE_NOT_GRANTABLE` (`'role_grant_offer_role_not_grantable'`)
1359
+ - `ERROR_ROLE_GRANT_OFFER_NOT_AUTHORIZED` (`'role_grant_offer_not_authorized'`)
1360
+ - `ERROR_ROLE_GRANT_OFFER_ACTOR_ACCOUNT_MISMATCH` (`'role_grant_offer_actor_account_mismatch'` —
1361
+ `role_grant_offer_create` was called with a `to_actor_id` that does not
1144
1362
  belong to `to_account_id`)
1145
- - `ERROR_OFFER_ACTOR_MISMATCH` (`'offer_actor_mismatch'` —
1363
+ - `ERROR_ROLE_GRANT_OFFER_ACTOR_MISMATCH` (`'role_grant_offer_actor_mismatch'` —
1146
1364
  actor-targeted offer was accepted by an actor other than `to_actor_id`)
1147
1365
 
1148
- Plus re-uses from `../http/error_schemas.ts`: `ERROR_PERMIT_NOT_FOUND`,
1366
+ Plus re-uses from `../http/error_schemas.ts`: `ERROR_ROLE_GRANT_NOT_FOUND`,
1149
1367
  `ERROR_ROLE_NOT_WEB_GRANTABLE`, `ERROR_INSUFFICIENT_PERMISSIONS`,
1150
1368
  `ERROR_ACCOUNT_NOT_FOUND`.
1151
1369
 
@@ -1154,80 +1372,85 @@ Each spec declares the reason codes its handler may surface (see
1154
1372
  domain reasons returned via `error.data.reason` are listed; standard
1155
1373
  transport errors (validation, auth, rate-limit) stay implicit. Drift
1156
1374
  between declared reasons and handler throws is caught by
1157
- `../../test/auth/permit_offer_actions.error_reasons.test.ts`.
1375
+ `../../test/auth/role_grant_offer_actions.error_reasons.test.ts`.
1158
1376
 
1159
1377
  Failure-outcome audit events emitted (success and failure rows both carry
1160
1378
  `ip: ctx.client_ip` — uniform with the admin and self-service surfaces):
1161
1379
 
1162
- - `permit_offer_create` failure — `web_grantable` denial, `authorize`
1380
+ - `role_grant_offer_create` failure — admin-grant-path denial, `authorize`
1163
1381
  denial, self-target rejection, and actor-account mismatch all emit
1164
1382
  the same audit row via `emit_create_failure_audit`. `target_account_id`
1165
1383
  carries `input.to_account_id`; `target_actor_id` echoes
1166
1384
  `input.to_actor_id` when supplied so failure rows match the
1167
1385
  success-shape envelope of actor-targeted offers (null on
1168
1386
  account-grain offers — see audit_log_schema rule).
1169
- - `permit_revoke` failure — `web_grantable` denial after IDOR / role
1387
+ - `role_grant_revoke` failure — admin-grant-path denial after IDOR / role
1170
1388
  lookup succeeded. The admin-role-denied path (pre-IDOR) emits no audit,
1171
1389
  matching the middleware auth-guard precedent. `target_account_id` +
1172
1390
  `target_actor_id` both populated (the IDOR-passing branch resolves
1173
1391
  the target actor before the gate; the subject is an actor-bound
1174
- permit).
1392
+ role_grant).
1175
1393
 
1176
1394
  WS notifications (post-commit via `emit_after_commit` from
1177
1395
  `../http/pending_effects.js` — swallows exceptions so one failed send
1178
1396
  can't starve others; see `../http/CLAUDE.md` §Pending Effects):
1179
1397
 
1180
- - Create → `permit_offer_received` to recipient.
1181
- - Retract → `permit_offer_retracted` to recipient.
1182
- - Accept → `permit_offer_accepted` to grantor + one
1183
- `permit_offer_supersede` per superseded sibling to that sibling's grantor.
1184
- - Decline → `permit_offer_declined` to grantor.
1185
- - Revoke → `permit_revoke` to revokee + one `permit_offer_supersede` per
1398
+ - Create → `role_grant_offer_received` to recipient.
1399
+ - Retract → `role_grant_offer_retracted` to recipient.
1400
+ - Accept → `role_grant_offer_accepted` to grantor + one
1401
+ `role_grant_offer_supersede` per superseded sibling to that sibling's grantor.
1402
+ - Decline → `role_grant_offer_declined` to grantor.
1403
+ - Revoke → `role_grant_revoke` to revokee + one `role_grant_offer_supersede` per
1186
1404
  superseded sibling.
1187
1405
 
1188
- Deps: `PermitOfferActionDeps extends AuditEmitDeps & {notification_sender?: NotificationSender | null}`.
1189
- Notification sender is optional — when absent, WS fan-out is silently
1190
- skipped (DB-only side effects still happen).
1406
+ Deps: `Pick<RouteFactoryDeps, 'log' | 'audit'> & {notification_sender?: NotificationSender | null}`
1407
+ inline on the param. Notification sender is optional — when absent, WS
1408
+ fan-out is silently skipped (DB-only side effects still happen).
1191
1409
 
1192
1410
  Options:
1193
1411
 
1194
- - `roles?: RoleSchemaResult` — drives `web_grantable` lookup (defaults to
1195
- `BUILTIN_ROLE_OPTIONS`).
1412
+ - `roles?: RoleSchemaResult` — drives the admin-grant-path lookup
1413
+ (`role_has_grant_path(_, role, GRANT_PATH_ADMIN)`); defaults to
1414
+ `BUILTIN_ROLE_SPECS_BY_NAME`.
1196
1415
  - `default_ttl_ms?: number` — applied to new offers (defaults to
1197
- `PERMIT_OFFER_DEFAULT_TTL_MS`).
1198
- - `authorize?: PermitOfferCreateAuthorize` — custom policy for
1199
- `permit_offer_create`. Signature:
1416
+ `ROLE_GRANT_OFFER_DEFAULT_TTL_MS`).
1417
+ - `authorize?: RoleGrantOfferCreateAuthorize` — custom policy for
1418
+ `role_grant_offer_create`. Signature:
1200
1419
  `(auth, input: {to_account_id, role, scope_id}, deps: Pick<RouteFactoryDeps, 'log'>, ctx: ActionContext) => boolean | Promise<boolean>`.
1201
1420
  Pre-built option: `authorize_admin_or_holder` admits any admin and
1202
1421
  otherwise falls back to the symmetric default (caller must hold the
1203
1422
  offered role globally). Drop into
1204
- `create_permit_offer_actions({authorize: authorize_admin_or_holder})`
1423
+ `create_role_grant_offer_actions({authorize: authorize_admin_or_holder})`
1205
1424
  or any factory that forwards `authorize` (e.g. `create_standard_rpc_actions`)
1206
- for the common "admins offer anything web_grantable; users offer what
1207
- they hold" pattern.
1425
+ for the common "admins offer anything on the admin grant path; users
1426
+ offer what they hold" pattern.
1208
1427
 
1209
- `all_permit_offer_action_specs: Array<RequestResponseActionSpec>` —
1428
+ `all_role_grant_offer_action_specs: Array<RequestResponseActionSpec>` —
1210
1429
  codegen-ready registry.
1211
1430
 
1212
- ### `standard_rpc_actions.ts` — combined admin + permit-offer + account factory
1431
+ ### `standard_rpc_actions.ts` — combined admin + role-grant-offer + account factory
1213
1432
 
1214
1433
  `create_standard_rpc_actions(deps, options)` spreads
1215
- `create_admin_actions`, `create_permit_offer_actions`, and
1434
+ `create_admin_actions`, `create_role_grant_offer_actions`, and
1216
1435
  `create_account_actions` into a single `Array<RpcAction>` — the
1217
1436
  canonical fuz_app "standard" RPC surface (25 actions with
1218
1437
  `app_settings` wired, 23 without). Consumers that want a narrower
1219
1438
  surface drop down to the per-domain factories directly.
1220
1439
 
1221
- Option routing: `roles` is shared between admin and permit-offer;
1440
+ Option routing: `roles` is shared between admin and role-grant-offer;
1222
1441
  `app_settings` flows to admin only; `default_ttl_ms` and `authorize`
1223
- flow to permit-offer only; `max_tokens` flows to account only;
1224
- `notification_sender` is wired through to permit-offer (admin +
1442
+ flow to role-grant-offer only; `max_tokens` flows to account only;
1443
+ `notification_sender` is wired through to role-grant-offer (admin +
1225
1444
  account ignore it).
1226
1445
 
1227
1446
  `StandardRpcActionsOptions` composes `AdminActionOptions` +
1228
- `PermitOfferActionOptions` + `AccountActionOptions`.
1229
- `StandardRpcActionsDeps` is the same shape as `PermitOfferActionDeps`
1230
- `log`, `on_audit_event`, optional `notification_sender`.
1447
+ `RoleGrantOfferActionOptions` + `AccountActionOptions`.
1448
+ `StandardRpcActionsDeps extends Pick<RouteFactoryDeps, 'log' | 'audit'>`
1449
+ plus optional `notification_sender` consumed only by the
1450
+ role-grant-offer sub-factory; admin and account sub-factories ignore it.
1451
+ The interface is declared inline rather than aliased so future
1452
+ role-grant-offer-internal deps additions can't silently widen the
1453
+ standard surface.
1231
1454
 
1232
1455
  Pair this with `create_app_server`'s `rpc_endpoints` factory form
1233
1456
  (`(ctx) => Array<RpcEndpointSpec>`) so the combined action list gets
@@ -1237,7 +1460,7 @@ again in `create_route_specs`. See `../../../docs/usage.md` §Server
1237
1460
  Assembly.
1238
1461
 
1239
1462
  Pre-bundle consumers spread `create_admin_actions` and
1240
- `create_permit_offer_actions` separately, then also
1463
+ `create_role_grant_offer_actions` separately, then also
1241
1464
  `create_account_actions`. The bundled helper replaces all three —
1242
1465
  bundling account actions into the "standard" surface is deliberate:
1243
1466
  the admin integration suite exercises `account_token_create` /
@@ -1247,7 +1470,7 @@ consumer wiring the admin surface without account actions will hit
1247
1470
 
1248
1471
  Frontend mirror: `all_standard_action_specs` (in
1249
1472
  `./standard_action_specs.ts`) bundles `all_admin_action_specs +
1250
- all_permit_offer_action_specs + all_account_action_specs` into one
1473
+ all_role_grant_offer_action_specs + all_account_action_specs` into one
1251
1474
  `ReadonlyArray<RequestResponseActionSpec>` for typed-client codegen
1252
1475
  and `create_frontend_rpc_client({specs})` wiring. Self-service role
1253
1476
  specs are not included (opt-in, app-specific `eligible_roles`) —
@@ -1264,8 +1487,10 @@ that was `/api/account/*` is on the RPC endpoint.
1264
1487
  status-only probe, the RPC action returns `SessionAccountJson` for
1265
1488
  programmatic callers.
1266
1489
 
1267
- Authorization is **spec-level** (`auth: 'authenticated'`). Revoke operations
1268
- are account-scoped via `query_session_revoke_for_account` /
1490
+ Authorization is **spec-level**
1491
+ `auth: {account: 'required', actor: 'none'}` (no `acting` on input, so
1492
+ the actor axis stays `'none'` per registry-time invariant 2). Revoke
1493
+ operations are account-scoped via `query_session_revoke_for_account` /
1269
1494
  `query_revoke_api_token_for_account` — passing another account's session
1270
1495
  or token id returns `revoked: false` rather than revealing whether the id
1271
1496
  exists.
@@ -1283,12 +1508,12 @@ exists.
1283
1508
  `session_id` validates as `Blake3Hash`; `token_id` validates as
1284
1509
  `ApiTokenId` (`tok_[A-Za-z0-9_-]{12}`).
1285
1510
 
1286
- Audit events emitted (via `audit_log_fire_and_forget` with `ip: ctx.client_ip`):
1511
+ Audit events emitted (via `deps.audit.emit` with `ip: ctx.client_ip`):
1287
1512
  `session_revoke`, `session_revoke_all`, `token_create`, `token_revoke`. The
1288
1513
  IP is the resolved trusted-proxy value from `ActionContext.client_ip`,
1289
1514
  matching the REST handler convention.
1290
1515
 
1291
- Deps: `AccountActionDeps = AuditEmitDeps`.
1516
+ Deps: `Pick<RouteFactoryDeps, 'log' | 'audit'>`.
1292
1517
  Options: `{max_tokens?: number | null}` — defaults to `DEFAULT_MAX_TOKENS`
1293
1518
  from `account_routes.ts`; `null` disables the cap.
1294
1519
 
@@ -1304,45 +1529,50 @@ Zod schemas, the `satisfies RequestResponseActionSpec` literal, the
1304
1529
  `*_actions.ts` factory imports the spec and pairs it with the handler.
1305
1530
 
1306
1531
  One static `request_response` action — `self_service_role_set` — that
1307
- takes `{role, enabled: boolean}` and toggles a global permit on the
1532
+ takes `{role, enabled: boolean}` and toggles a global role_grant on the
1308
1533
  caller. Idempotent in both directions: `changed: false` when the
1309
1534
  post-call state already matched the request (already-held when
1310
1535
  enabling; not-held when disabling). Output is `{ok, enabled, changed}` —
1311
1536
  `enabled` echoes the post-call state for self-describing responses.
1312
1537
  Audit metadata carries `self_service: true` so admin reviewers can
1313
- distinguish self-toggled permits from admin grants/offers. The
1314
- `permit_grant` / `permit_revoke` metadata schemas declare
1538
+ distinguish self-toggled role_grants from admin grants/offers. The
1539
+ `role_grant_create` / `role_grant_revoke` metadata schemas declare
1315
1540
  `self_service: z.boolean().optional()` explicitly, so the field is
1316
1541
  part of the documented surface rather than riding on `z.looseObject`
1317
1542
  permissiveness.
1318
1543
 
1319
1544
  Method name is static — `role` lives in the input, not the method
1320
- name. Mirrors the `permit_offer_create({role})` precedent. Per-role
1545
+ name. Mirrors the `role_grant_offer_create({role})` precedent. Per-role
1321
1546
  parameterized methods would break the `satisfies RequestResponseActionSpec`
1322
1547
  codegen invariant and grow the surface linearly per role.
1323
1548
 
1324
1549
  `create_self_service_role_actions(deps, options)`:
1325
1550
 
1326
- - `eligible_roles: ReadonlyArray<string>` — required allowlist. Roles
1327
- outside the list are rejected with `forbidden` + reason
1551
+ - `eligible_roles?: ReadonlyArray<string>` — optional override
1552
+ allowlist. When omitted, eligibility is derived from
1553
+ `roles.role_specs` (or `BUILTIN_ROLE_SPECS_BY_NAME` when `roles` is
1554
+ also omitted) by selecting every role whose `RoleSpec.grant_paths`
1555
+ includes `'self_service'` (`GRANT_PATH_SELF_SERVICE`). Roles outside
1556
+ the eligible set are rejected with `forbidden` + reason
1328
1557
  `role_not_self_service_eligible` (exported as
1329
1558
  `ERROR_ROLE_NOT_SELF_SERVICE_ELIGIBLE`). The eligibility check fires
1330
1559
  before the `enabled` branch — same rejection regardless of direction.
1331
- - `roles?: RoleSchemaResult` — optional. When supplied, every entry in
1332
- `eligible_roles` is checked against `roles.role_options` at factory
1333
- time so typos throw at startup instead of at first call.
1560
+ - `roles?: RoleSchemaResult` — drives default-eligibility derivation
1561
+ from `RoleSpec.grant_paths`. When `eligible_roles` is also supplied,
1562
+ every entry is checked against `roles.role_specs` at factory time so
1563
+ typos throw at startup instead of at first call.
1334
1564
 
1335
1565
  Grant branch uses `has_scoped_role(auth, role, null)` for a
1336
1566
  benign-TOCTOU pre-check (distinguishes new grant from idempotent
1337
- re-grant) — reads from the in-memory `auth.permits` snapshot, no DB
1338
- roundtrip — then `query_grant_permit` for the actual insert. Revoke branch filters
1339
- `query_permit_find_active_for_actor` in JS for the matching
1567
+ re-grant) — reads from the in-memory `auth.role_grants` snapshot, no DB
1568
+ roundtrip — then `query_create_role_grant` for the actual insert. Revoke branch filters
1569
+ `query_role_grant_find_active_for_actor` in JS for the matching
1340
1570
  `(actor, role, scope_id IS NULL)` row before calling
1341
- `query_revoke_permit`. Bundle is **not** included in
1571
+ `query_revoke_role_grant`. Bundle is **not** included in
1342
1572
  `create_standard_rpc_actions` — `eligible_roles` is app-specific, opt-in,
1343
1573
  spread alongside the standard bundle when needed.
1344
1574
 
1345
- Deps: `SelfServiceRoleActionDeps = AuditEmitDeps`.
1575
+ Deps: `Pick<RouteFactoryDeps, 'log' | 'audit'>`.
1346
1576
 
1347
1577
  `all_self_service_role_action_specs: ReadonlyArray<RequestResponseActionSpec>` —
1348
1578
  codegen-ready registry of the single unified spec.
@@ -1351,53 +1581,54 @@ codegen-ready registry of the single unified spec.
1351
1581
 
1352
1582
  `cleanup.ts` — periodic auth maintenance:
1353
1583
 
1354
- - `AuthCleanupDeps = QueryDeps & {log, on_audit_event?}`.
1355
- - `cleanup_expired_permit_offers(deps)` wraps `query_permit_offer_sweep_expired`,
1356
- emits one `permit_offer_expire` audit row per expired offer. Per-row
1357
- `on_audit_event` exceptions are logged and swallowed; one failed callback
1358
- does not starve siblings. Audit-write failures are also logged and skipped
1359
- (not re-thrown) so sibling sweeps still complete.
1584
+ - `AuthCleanupDeps = QueryDeps & {log, audit: AuditEmitter}`.
1585
+ Requiredproduction wiring always has a bound emitter (built once
1586
+ at `create_app_backend`); tests that need a no-op pass
1587
+ `create_test_audit_emitter()` from `testing/stubs.ts`. Single slot
1588
+ carries both row persistence and SSE/WS fan-out.
1589
+ - `cleanup_expired_role_grant_offers(deps)` wraps `query_role_grant_offer_sweep_expired`,
1590
+ emits one `role_grant_offer_expire` audit row per expired offer
1591
+ through `audit.emit_pool` (the captured pool + config + chain). Both
1592
+ write errors and per-listener throws are logged + swallowed inside
1593
+ `emit_pool`, so a single bad row never starves sibling sweeps.
1360
1594
  - `run_auth_cleanup(deps)` — one-shot consumer entry point: expired
1361
1595
  sessions + expired offers. Returns `{expired_sessions, expired_offers}`.
1362
1596
  **Re-throws sweep errors** so the caller's scheduler can log / alert.
1363
1597
  Call from `setInterval` / cron / similar.
1364
1598
 
1365
- Idempotency: the audit log has no tombstone on `permit_offer_expire`, so
1599
+ Idempotency: the audit log has no tombstone on `role_grant_offer_expire`, so
1366
1600
  concurrent sweep runs double-audit. Deploy a single scheduled invocation
1367
1601
  per instance — matches `query_session_cleanup_expired`'s expected pattern.
1368
1602
  Expired offer rows are **preserved** (not deleted) — they carry audit value
1369
1603
  for the history view, and accepted rows are the provenance for the
1370
- resulting permit.
1604
+ resulting role_grant.
1371
1605
 
1372
1606
  ## Deps
1373
1607
 
1374
1608
  `deps.ts` defines:
1375
1609
 
1376
- - **`AppDeps`** — the stateless capabilities bundle. Eight members:
1610
+ - **`AppDeps`** — the stateless capabilities bundle. Seven members:
1377
1611
  - `stat`, `read_text_file`, `delete_file` — filesystem.
1378
1612
  - `keyring: Keyring` — HMAC-SHA256 signing.
1379
1613
  - `password: PasswordHashDeps` — use `argon2_password_deps` in production.
1380
1614
  - `db: Db` — pool-level instance (middleware uses this; route handlers
1381
1615
  get a transaction-scoped `Db` via `RouteContext`).
1382
1616
  - `log: Logger`.
1383
- - `on_audit_event: (event) => void` — fires after every successful audit
1384
- INSERT. Wire to SSE broadcast for realtime audit streams. Defaults to
1385
- noop when unwired. Flows automatically through every factory that
1386
- receives `deps` / `RouteFactoryDeps`.
1387
- - `audit_log_config?: AuditLogConfig` optional consumer-extended audit
1388
- config from `create_audit_log_config({extra_events})`. Wired into
1389
- `audit_log_fire_and_forget` via the deps bundle so consumer event-type
1390
- metadata gets validated. Absent defaults to `BUILTIN_AUDIT_LOG_CONFIG`.
1391
- Pass at the backend via `create_app_backend({audit_log_config})`.
1617
+ - `audit: AuditEmitter` — bound emitter built once at `create_app_backend`
1618
+ via `create_audit_emitter`. Closes over the pool, the
1619
+ `on_audit_event` subscriber chain, and the optional
1620
+ `AuditLogConfig` so handlers reach `audit.emit(ctx, input)` /
1621
+ `audit.emit_role_grant_target(ctx, auth, input)` and never see the
1622
+ pool. Pass `on_audit_event` and `audit_log_config` to
1623
+ `create_app_backend` both fold into `audit`'s closure and the slot
1624
+ is the single seam for SSE/WS fan-out (additional listeners append
1625
+ via `audit.on_event_chain.push(...)` at server assembly).
1392
1626
  - **`RouteFactoryDeps = Omit<AppDeps, 'db'>`** — for route factories. Route
1393
1627
  handlers receive DB access via `RouteContext`, so factories don't capture
1394
1628
  a pool-level `Db`.
1395
- - **`AuditEmitDeps = Pick<AppDeps, 'log' | 'on_audit_event' | 'audit_log_config'>`**
1396
- the slice every audit-emitting site needs. Used by `audit_log_fire_and_forget`
1397
- / `emit_permit_target_event` (the primitives) and aliased by every
1398
- action-factory deps type (`AdminActionDeps`, `AccountActionDeps`,
1399
- `PermitOfferActionDeps`, `SelfServiceRoleActionDeps`) so the five
1400
- factories stop spelling the same `Pick` independently.
1629
+
1630
+ Action factories take `Pick<RouteFactoryDeps, 'log' | 'audit'>` directly
1631
+ (role-grant-offer adds `notification_sender?` inline).
1401
1632
 
1402
1633
  See root `../../../CLAUDE.md` §AppDeps Vocabulary for the
1403
1634
  capability / options / runtime-state split across the whole project.