@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
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Role grant database queries.
3
+ *
4
+ * Role grants are time-bounded, revocable grants of a role to an actor.
5
+ * The system is safe by default — no role_grant, no capability.
6
+ *
7
+ * @module
8
+ */
9
+ import { assert_row } from '../db/assert_row.js';
10
+ import { ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN, ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID, } from './role_grant_offer_schema.js';
11
+ /**
12
+ * Grant a role_grant to an actor.
13
+ * Idempotent — if an active role_grant already exists for this actor, role, and
14
+ * scope, returns the existing role_grant instead of creating a duplicate.
15
+ *
16
+ * The `ON CONFLICT` target and the fallback `SELECT` both collapse `NULL`
17
+ * scopes via the same sentinel + index-side `'GLOBAL'` token used by the
18
+ * partial unique index (`role_grant_actor_role_scope_active_unique`). The
19
+ * `IS NOT DISTINCT FROM` form on the fallback is deliberate — plain `=`
20
+ * would miss the NULL-scope case where the conflict fired.
21
+ *
22
+ * `scope_kind` is paired-null with `scope_id` per the
23
+ * `role_grant_scope_kind_paired` CHECK; mismatched pairs raise at the DB
24
+ * layer rather than producing silent rows.
25
+ *
26
+ * @param deps - query dependencies
27
+ * @param input - the role_grant fields
28
+ * @returns the created or existing active role_grant
29
+ * @mutates `role_grant` table - inserts a row when no active role_grant matches `(actor_id, role, scope_kind, scope_id)`
30
+ */
31
+ export const query_create_role_grant = async (deps, input) => {
32
+ const inserted = await deps.db.query_one(`INSERT INTO role_grant (actor_id, role, scope_kind, scope_id, expires_at, granted_by, source_offer_id)
33
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
34
+ ON CONFLICT (
35
+ actor_id,
36
+ role,
37
+ COALESCE(scope_kind, '${ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN}'),
38
+ COALESCE(scope_id, '${ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID}'::uuid)
39
+ )
40
+ WHERE revoked_at IS NULL
41
+ DO NOTHING
42
+ RETURNING *`, [
43
+ input.actor_id,
44
+ input.role,
45
+ input.scope_kind ?? null,
46
+ input.scope_id ?? null,
47
+ input.expires_at?.toISOString() ?? null,
48
+ input.granted_by ?? null,
49
+ input.source_offer_id ?? null,
50
+ ]);
51
+ if (inserted)
52
+ return inserted;
53
+ // Active role_grant already exists — return it (idempotent grant).
54
+ const existing = await deps.db.query_one(`SELECT * FROM role_grant
55
+ WHERE actor_id = $1
56
+ AND role = $2
57
+ AND scope_kind IS NOT DISTINCT FROM $3
58
+ AND scope_id IS NOT DISTINCT FROM $4
59
+ AND revoked_at IS NULL`, [input.actor_id, input.role, input.scope_kind ?? null, input.scope_id ?? null]);
60
+ return assert_row(existing, 'idempotent role_grant grant');
61
+ };
62
+ /**
63
+ * Look up the role of an active role_grant (constrained to a specific
64
+ * actor) plus the actor's `account_id`.
65
+ *
66
+ * Used by admin routes to inspect the role_grant's role before acting
67
+ * (e.g., enforcing the admin-grant-path gate on revoke). The actor constraint
68
+ * mirrors `query_revoke_role_grant` so IDOR protection is consistent:
69
+ * a caller can only see role_grants belonging to the target actor.
70
+ *
71
+ * The JOIN to `actor` collapses what used to be a second
72
+ * `query_actor_by_id` round-trip in the revoke handler into one read,
73
+ * which closes the small TOCTOU window where the actor row could be
74
+ * deleted between the IDOR check and the actor lookup. The `account_id`
75
+ * is needed by the audit envelope's `target_account_id` field and the
76
+ * SSE/WS socket-close fan-out targeting.
77
+ *
78
+ * Returns `null` if the role_grant is not found, already revoked, or
79
+ * belongs to a different actor.
80
+ *
81
+ * @param deps - query dependencies
82
+ * @param role_grant_id - the role_grant id to look up
83
+ * @param actor_id - the actor that must own the role_grant
84
+ * @returns `{role, account_id}` on a match, or `null`
85
+ */
86
+ export const query_role_grant_find_active_role_for_actor = async (deps, role_grant_id, actor_id) => {
87
+ const row = await deps.db.query_one(`SELECT role_grant.role, actor.account_id
88
+ FROM role_grant
89
+ JOIN actor ON actor.id = role_grant.actor_id
90
+ WHERE role_grant.id = $1 AND role_grant.actor_id = $2 AND role_grant.revoked_at IS NULL`, [role_grant_id, actor_id]);
91
+ return row ?? null;
92
+ };
93
+ /**
94
+ * Revoke a role_grant by id, constrained to a specific actor.
95
+ *
96
+ * Requires `actor_id` to prevent cross-account revocation (IDOR guard).
97
+ * Returns `null` if the role_grant is not found, already revoked, or belongs
98
+ * to a different actor.
99
+ *
100
+ * Supersedes any pending offers for the revoked role_grant's
101
+ * `(to_account, role, scope)` in the same transaction. Prevents the
102
+ * "accept a pre-revoke offer to bypass the revoke" path — any stale
103
+ * offer becomes terminal at revoke time. A fresh post-revoke grant
104
+ * requires the grantor to call `query_role_grant_offer_create` again.
105
+ *
106
+ * @param deps - query dependencies
107
+ * @param role_grant_id - the role_grant to revoke
108
+ * @param actor_id - the actor that must own the role_grant
109
+ * @param revoked_by - the actor who revoked it (for audit trail)
110
+ * @param reason - optional free-form reason, stamped on `role_grant.revoked_reason` and surfaced to the revokee notification.
111
+ * @mutates `role_grant` row - sets `revoked_at`, `revoked_by`, and `revoked_reason`
112
+ * @mutates `role_grant_offer` rows - stamps `superseded_at` on every pending sibling for the same `(account, role, scope)`
113
+ */
114
+ export const query_revoke_role_grant = async (deps, role_grant_id, actor_id, revoked_by, reason) => {
115
+ const rows = await deps.db.query(`UPDATE role_grant SET revoked_at = NOW(), revoked_by = $3, revoked_reason = $4
116
+ WHERE id = $1 AND actor_id = $2 AND revoked_at IS NULL
117
+ RETURNING id, role, scope_kind, scope_id`, [role_grant_id, actor_id, revoked_by ?? null, reason ?? null]);
118
+ const revoked = rows[0];
119
+ if (!revoked)
120
+ return null;
121
+ // CTE joins `actor` after the UPDATE so each superseded row carries the
122
+ // grantor's `account_id` — callers fan out `role_grant_offer_supersede`
123
+ // notifications to that account without a second round-trip. The match
124
+ // keys on `scope_id` only because the (scope_kind, scope_id) pair-CHECK
125
+ // makes scope_kind a function of scope_id; matching on both adds no
126
+ // new selectivity in v1.
127
+ const superseded_offers = await deps.db.query(`WITH updated AS (
128
+ UPDATE role_grant_offer o
129
+ SET superseded_at = NOW()
130
+ FROM actor a
131
+ WHERE a.id = $1
132
+ AND o.to_account_id = a.account_id
133
+ AND o.role = $2
134
+ AND o.scope_id IS NOT DISTINCT FROM $3
135
+ AND o.accepted_at IS NULL
136
+ AND o.declined_at IS NULL
137
+ AND o.retracted_at IS NULL
138
+ AND o.superseded_at IS NULL
139
+ RETURNING o.*
140
+ )
141
+ SELECT u.*, grantor.account_id AS from_account_id
142
+ FROM updated u
143
+ JOIN actor grantor ON grantor.id = u.from_actor_id`, [actor_id, revoked.role, revoked.scope_id]);
144
+ return {
145
+ id: revoked.id,
146
+ role: revoked.role,
147
+ scope_kind: revoked.scope_kind,
148
+ scope_id: revoked.scope_id,
149
+ superseded_offers,
150
+ };
151
+ };
152
+ /**
153
+ * Find all active (non-revoked, non-expired) role_grants for an actor.
154
+ */
155
+ export const query_role_grant_find_active_for_actor = async (deps, actor_id) => {
156
+ return deps.db.query(`SELECT * FROM role_grant
157
+ WHERE actor_id = $1
158
+ AND revoked_at IS NULL
159
+ AND (expires_at IS NULL OR expires_at > NOW())
160
+ ORDER BY created_at`, [actor_id]);
161
+ };
162
+ /**
163
+ * Check if an actor has an active role_grant for a given role.
164
+ *
165
+ * The `scope_id` parameter selects between global and scoped checks:
166
+ * - Omitted or `null` — matches a global role_grant (`scope_id IS NULL`).
167
+ * Pre-scope callers keep their existing semantics.
168
+ * - A scope uuid — matches a role_grant bound to that exact scope.
169
+ *
170
+ * The `IS NOT DISTINCT FROM` comparison handles the NULL case uniformly.
171
+ */
172
+ export const query_role_grant_has_role = async (deps, actor_id, role, scope_id) => {
173
+ const row = await deps.db.query_one(`SELECT EXISTS(
174
+ SELECT 1 FROM role_grant
175
+ WHERE actor_id = $1
176
+ AND role = $2
177
+ AND scope_id IS NOT DISTINCT FROM $3
178
+ AND revoked_at IS NULL
179
+ AND (expires_at IS NULL OR expires_at > NOW())
180
+ ) AS exists`, [actor_id, role, scope_id ?? null]);
181
+ return row?.exists ?? false;
182
+ };
183
+ /**
184
+ * List all role_grants for an actor (including revoked/expired).
185
+ */
186
+ export const query_role_grant_list_for_actor = async (deps, actor_id) => {
187
+ return deps.db.query(`SELECT * FROM role_grant WHERE actor_id = $1 ORDER BY created_at DESC`, [actor_id]);
188
+ };
189
+ /**
190
+ * Find the account ID of an account that holds an active role_grant for a given role.
191
+ *
192
+ * Joins role_grant → actor → account. Returns the first match, or `null` if none.
193
+ *
194
+ * @param deps - query dependencies
195
+ * @param role - the role to search for
196
+ * @returns the account ID, or `null`
197
+ */
198
+ export const query_role_grant_find_account_id_for_role = async (deps, role) => {
199
+ const row = await deps.db.query_one(`SELECT a.id AS account_id
200
+ FROM role_grant p
201
+ JOIN actor act ON act.id = p.actor_id
202
+ JOIN account a ON a.id = act.account_id
203
+ WHERE p.role = $1
204
+ AND p.revoked_at IS NULL
205
+ AND (p.expires_at IS NULL OR p.expires_at > NOW())
206
+ LIMIT 1`, [role]);
207
+ return row?.account_id ?? null;
208
+ };
209
+ /**
210
+ * Revoke every active role_grant bound to a scope and supersede every pending
211
+ * offer at the scope, in one cascade.
212
+ *
213
+ * Use this from a consumer's parent-scope delete handler (e.g., classroom
214
+ * deletion) — `role_grant.scope_id` and `role_grant_offer.scope_id` are polymorphic
215
+ * with no FK constraint by design, so a parent row deletion would otherwise
216
+ * orphan role_grants and offers. The cascade is **role-agnostic**: anything
217
+ * attached to the destroyed scope is cleaned up.
218
+ *
219
+ * Both updates run as separate statements inside the caller's transaction
220
+ * (mirrors `query_role_grant_revoke_role`'s shape). The two halves are
221
+ * independent — orphan pending offers can exist at a scope with no active
222
+ * role_grants, so the supersede half always runs even when no role_grant was
223
+ * revoked.
224
+ *
225
+ * @param deps - query dependencies
226
+ * @param scope_id - the scope whose role_grants and offers to terminate
227
+ * @param revoked_by - the actor performing the cascade (audit trail)
228
+ * @param reason - optional free-form reason, stamped on `role_grant.revoked_reason`.
229
+ * @returns the revoked role_grants (with `account_id` for fan-out) and superseded offers (with `from_account_id` for fan-out)
230
+ * @mutates `role_grant` table - sets `revoked_at`/`revoked_by`/`revoked_reason` on every active row at `scope_id`
231
+ * @mutates `role_grant_offer` table - stamps `superseded_at` on every pending row at `scope_id`
232
+ */
233
+ export const query_role_grant_revoke_for_scope = async (deps, scope_id, revoked_by, reason) => {
234
+ // Revoke every active role_grant at the scope. CTE returns `actor_id` directly
235
+ // from the role_grant row (drives `target_actor_id` audit envelopes); a join
236
+ // against `actor` resolves `account_id` for `target_account_id`
237
+ // + WS/SSE socket-close fan-out, all in one round-trip.
238
+ const revoked = await deps.db.query(`WITH updated AS (
239
+ UPDATE role_grant
240
+ SET revoked_at = NOW(), revoked_by = $2, revoked_reason = $3
241
+ WHERE scope_id = $1 AND revoked_at IS NULL
242
+ RETURNING id, role, scope_kind, scope_id, actor_id
243
+ )
244
+ SELECT u.id AS role_grant_id, u.role, u.scope_kind, u.scope_id, u.actor_id, a.account_id
245
+ FROM updated u
246
+ JOIN actor a ON a.id = u.actor_id`, [scope_id, revoked_by ?? null, reason ?? null]);
247
+ // Supersede every pending offer at the scope — tuple-matched or orphan,
248
+ // no distinction. The cause of every supersede in this cascade is the
249
+ // scope deletion; offers tuple-matched to a revoked role_grant are not
250
+ // tagged separately because the revoke is itself a consequence of the
251
+ // scope going away.
252
+ const superseded_offers = await deps.db.query(`WITH updated AS (
253
+ UPDATE role_grant_offer o
254
+ SET superseded_at = NOW()
255
+ WHERE o.scope_id = $1
256
+ AND o.accepted_at IS NULL
257
+ AND o.declined_at IS NULL
258
+ AND o.retracted_at IS NULL
259
+ AND o.superseded_at IS NULL
260
+ RETURNING o.*
261
+ )
262
+ SELECT u.*, grantor.account_id AS from_account_id
263
+ FROM updated u
264
+ JOIN actor grantor ON grantor.id = u.from_actor_id`, [scope_id]);
265
+ return { revoked, superseded_offers };
266
+ };
267
+ /**
268
+ * Revoke every active role_grant an actor holds for a given role.
269
+ *
270
+ * With scoped role_grants a single actor+role tuple can hold several active
271
+ * role_grants (one per scope), so this revokes all of them. Pass
272
+ * `query_revoke_role_grant(role_grant_id, ...)` when a single scoped role_grant
273
+ * is the target.
274
+ *
275
+ * Also supersedes pending offers for the actor's account across every
276
+ * scope of this role (the actor can no longer hold the role, so any
277
+ * pending offer of the same role is a bypass vector).
278
+ *
279
+ * @param deps - query dependencies
280
+ * @param actor_id - the actor whose role_grants to revoke
281
+ * @param role - the role to revoke
282
+ * @param revoked_by - the actor who revoked it (for audit trail)
283
+ * @param reason - optional free-form reason, stamped on `role_grant.revoked_reason`.
284
+ * @returns the list of revoked role_grants (empty if none were active) and superseded pending offers
285
+ * @mutates `role_grant` table - sets `revoked_at`/`revoked_by`/`revoked_reason` on every active row for `(actor, role)`
286
+ * @mutates `role_grant_offer` table - stamps `superseded_at` on every matching pending offer
287
+ */
288
+ export const query_role_grant_revoke_role = async (deps, actor_id, role, revoked_by, reason) => {
289
+ // CTE pulls the revokee's `account_id` via a join on `actor` so callers
290
+ // can address the revokee without an extra round-trip.
291
+ const revoked = await deps.db.query(`WITH updated AS (
292
+ UPDATE role_grant
293
+ SET revoked_at = NOW(), revoked_by = $3, revoked_reason = $4
294
+ WHERE actor_id = $1 AND role = $2 AND revoked_at IS NULL
295
+ RETURNING id, role, scope_kind, scope_id, actor_id
296
+ )
297
+ SELECT u.id AS role_grant_id, u.role, u.scope_kind, u.scope_id, a.account_id
298
+ FROM updated u
299
+ JOIN actor a ON a.id = u.actor_id`, [actor_id, role, revoked_by ?? null, reason ?? null]);
300
+ if (revoked.length === 0) {
301
+ return { revoked: [], superseded_offers: [] };
302
+ }
303
+ const superseded_offers = await deps.db.query(`WITH updated AS (
304
+ UPDATE role_grant_offer o
305
+ SET superseded_at = NOW()
306
+ FROM actor a
307
+ WHERE a.id = $1
308
+ AND o.to_account_id = a.account_id
309
+ AND o.role = $2
310
+ AND o.accepted_at IS NULL
311
+ AND o.declined_at IS NULL
312
+ AND o.retracted_at IS NULL
313
+ AND o.superseded_at IS NULL
314
+ RETURNING o.*
315
+ )
316
+ SELECT u.*, grantor.account_id AS from_account_id
317
+ FROM updated u
318
+ JOIN actor grantor ON grantor.id = u.from_actor_id`, [actor_id, role]);
319
+ return { revoked, superseded_offers };
320
+ };
@@ -1,80 +1,190 @@
1
1
  /**
2
- * Role system — builtin roles, role options, and extensible role schema factory.
2
+ * Role system — builtin roles, role specs, and extensible role schema factory.
3
3
  *
4
- * Defines the authorization policy vocabulary: which roles exist, what
5
- * capabilities they require (daemon token, web grantability), and a factory
6
- * for extending with app-defined roles.
4
+ * Defines the authorization policy vocabulary: which roles exist, their
5
+ * required credential types, the scope kinds each role applies to, and
6
+ * the grant paths through which each role can be granted. Each role
7
+ * gets a structured `RoleSpec`; the factory `create_role_schema` merges
8
+ * builtins with consumer-declared specs and validates every cross-axis
9
+ * field against the corresponding open registries
10
+ * (`create_credential_type_schema`, `create_scope_kind_schema`,
11
+ * `create_grant_path_schema`) at construction time so misconfigurations
12
+ * fire at server startup, not at first call.
13
+ *
14
+ * `RoleSpec` carries the four cross-axis fields that the dispatcher
15
+ * branches on: credential type, scope kind, grant path, and the
16
+ * role-name itself. v1 keeps the cross-axis fields informative-only
17
+ * (registry-membership validation, no INSERT-time enforcement); v2 may
18
+ * add `(role, scope_kind)` enforcement once the shape is clear from
19
+ * real consumer usage.
7
20
  *
8
21
  * @module
9
22
  */
10
23
  import { z } from 'zod';
24
+ import { type CredentialTypeSchemaResult } from './credential_type_schema.js';
25
+ import { type GrantPathSchemaResult } from './grant_path_schema.js';
26
+ import type { ScopeKindSchemaResult } from './scope_kind_schema.js';
11
27
  /** Valid role name: lowercase letters and underscores, no leading/trailing underscore. */
12
28
  export declare const RoleName: z.ZodString;
13
29
  export type RoleName = z.infer<typeof RoleName>;
14
30
  /** System-level role. Requires daemon token (filesystem proof). Controls the keep. */
15
31
  export declare const ROLE_KEEPER = "keeper";
16
- /** App-level administrative role. Web-grantable, manages users and content. */
32
+ /** App-level administrative role. Granted via the admin path. */
17
33
  export declare const ROLE_ADMIN = "admin";
18
34
  /** The builtin role names as a const tuple. */
19
35
  export declare const BUILTIN_ROLES: readonly ["keeper", "admin"];
20
36
  /** Zod schema for builtin roles only. */
21
37
  export declare const BuiltinRole: z.ZodEnum<{
22
- keeper: "keeper";
23
38
  admin: "admin";
39
+ keeper: "keeper";
24
40
  }>;
25
41
  export type BuiltinRole = z.infer<typeof BuiltinRole>;
26
42
  /**
27
43
  * Configuration for a role.
28
44
  *
29
- * Builtin roles have fixed configs. App-defined roles get sensible defaults
30
- * (`requires_daemon_token: false`, `web_grantable: true`).
45
+ * Each role declares the credential types its holders must use, the
46
+ * scope kinds it applies to, and the grant paths through which it can
47
+ * be granted. Every cross-axis field is an open-registry string array —
48
+ * `required_credential_types` against `create_credential_type_schema`,
49
+ * `applicable_scope_kinds` against `create_scope_kind_schema`,
50
+ * `grant_paths` against `create_grant_path_schema`. Pass the registry
51
+ * results to `create_role_schema` and every entry is checked at
52
+ * construction time.
53
+ *
54
+ * Empty arrays carry meaning:
55
+ *
56
+ * - `required_credential_types: []` — any authenticated credential type
57
+ * may exercise the role (the default for app-defined roles).
58
+ * - `applicable_scope_kinds: []` — the role applies at the global scope
59
+ * only (no `scope_kind` / `scope_id` set on its role_grants). This is the
60
+ * default for app-defined roles; consumers add scope kinds explicitly.
61
+ * - `grant_paths: []` — the role has no grant path declared in this
62
+ * registry; it is unreachable through admin / self-service / system
63
+ * flows. Only useful for diagnostic snapshotting.
64
+ *
65
+ * Builtins (`keeper`, `admin`) ship preconfigured in
66
+ * `BUILTIN_ROLE_SPECS_BY_NAME`.
31
67
  */
32
- export interface RoleOptions {
33
- /** If true, exercising this role requires daemon token authentication. Only `keeper` for now. */
34
- requires_daemon_token?: boolean;
35
- /** If true, admins can grant this role via the web UI. Default `true`. */
36
- web_grantable?: boolean;
68
+ export interface RoleSpec {
69
+ /** Unique role name. Must match `RoleName` regex; collisions with builtins throw. */
70
+ name: string;
71
+ /** Admin-UI-facing copy describing the role's intent. */
72
+ description?: string;
73
+ /**
74
+ * Credential types whose holders are permitted to exercise this
75
+ * role. Each entry is checked at construction time against the
76
+ * `credential_types` registry passed to `create_role_schema`. Empty
77
+ * array = any authenticated credential type.
78
+ */
79
+ required_credential_types?: ReadonlyArray<string>;
80
+ /**
81
+ * Scope kinds at which this role's role_grants may be granted. Each
82
+ * entry is checked at construction time against the `scope_kinds`
83
+ * registry passed to `create_role_schema`. Empty array = global only.
84
+ * v1 keeps this informative-only (no INSERT-time enforcement).
85
+ */
86
+ applicable_scope_kinds?: ReadonlyArray<string>;
87
+ /**
88
+ * Grant paths through which this role can be granted. Each entry is
89
+ * checked at construction time against the `grant_paths` registry
90
+ * passed to `create_role_schema`. Drives downstream defaults:
91
+ *
92
+ * - `admin_actions.grantable_roles` ⊇ {role : `'admin'` ∈ grant_paths}
93
+ * - `self_service_role_actions` default eligibility ⊇ {role : `'self_service'` ∈ grant_paths}
94
+ *
95
+ * Empty array = role is not granted via any registered path (only
96
+ * exists for diagnostic / future use).
97
+ */
98
+ grant_paths?: ReadonlyArray<string>;
37
99
  }
38
100
  /**
39
- * Builtin role configs. Not overridable by consumers.
40
- *
41
- * Typed `ReadonlyMap` for the contract but JS Maps don't honor
42
- * `Object.freeze` for `.set` / `.delete` / `.clear` (they mutate internal
43
- * slots, not own properties), so freeze adds no runtime guard here. Read
44
- * once at startup by `create_role_schema` and the admin / permit-offer
45
- * action factories; runtime mutation has no effect on already-built role
46
- * schemas.
101
+ * Builtin role specs, keyed by role name. Not overridable by consumers
102
+ * — read once at startup by `create_role_schema` and the action
103
+ * factories that fall back to builtins when no consumer `roles` is
104
+ * supplied. `ReadonlyMap` encodes the contract; runtime mutation has
105
+ * no effect on already-built role schemas (the factory copies entries
106
+ * into a fresh `Map`).
47
107
  */
48
- export declare const BUILTIN_ROLE_OPTIONS: ReadonlyMap<string, Required<RoleOptions>>;
49
- /** The result of `create_role_schema` a Zod schema and config map for all roles. */
108
+ export declare const BUILTIN_ROLE_SPECS_BY_NAME: ReadonlyMap<string, RoleSpec>;
109
+ /** Optional registries to validate `RoleSpec` cross-axis fields against at construction time. */
110
+ export interface CreateRoleSchemaOptions {
111
+ /** Pass `create_credential_type_schema()` to validate `RoleSpec.required_credential_types` entries. */
112
+ credential_types?: CredentialTypeSchemaResult;
113
+ /** Pass `create_scope_kind_schema()` to validate `RoleSpec.applicable_scope_kinds` entries. */
114
+ scope_kinds?: ScopeKindSchemaResult;
115
+ /** Pass `create_grant_path_schema()` to validate `RoleSpec.grant_paths` entries. */
116
+ grant_paths?: GrantPathSchemaResult;
117
+ }
118
+ /** The result of `create_role_schema` — a Zod schema and spec map for all roles. */
50
119
  export interface RoleSchemaResult {
51
- /** Zod schema that validates role strings. Use at I/O boundaries (grant endpoint, permit queries). */
120
+ /** Zod schema that validates role strings. Use at I/O boundaries (grant endpoint, role_grant queries). */
52
121
  Role: z.ZodType<string>;
53
- /** Options for every role (builtins + app-defined). Keyed by role name. */
54
- role_options: ReadonlyMap<string, Required<RoleOptions>>;
122
+ /** Specs for every role (builtins + app-defined). Keyed by role name. */
123
+ role_specs: ReadonlyMap<string, RoleSpec>;
55
124
  }
56
125
  /**
57
- * Create a role schema and config map that extends the builtins with app-defined roles.
126
+ * Create a role schema and spec map that extends the builtins with
127
+ * app-defined roles.
128
+ *
129
+ * Call once at server init. The returned `Role` schema validates role
130
+ * strings at I/O boundaries (grant endpoint, role_grant queries). The
131
+ * `role_specs` map is read by middleware for `required_credential_types`
132
+ * checks and by admin / self-service factories to derive their default
133
+ * eligibility filters from `RoleSpec.grant_paths`.
134
+ *
135
+ * Construction-time guards (all fire on misconfiguration):
58
136
  *
59
- * Call once at server init. The returned `Role` schema validates role strings
60
- * at I/O boundaries (grant endpoint, permit queries). The `role_options` map
61
- * is used by middleware to check `requires_daemon_token` and by admin UI to
62
- * filter `web_grantable` roles.
137
+ * 1. Every `consumer_roles[i].name` matches `RoleName` regex.
138
+ * 2. No two consumer roles share a name.
139
+ * 3. No consumer role collides with a builtin (`keeper` / `admin`).
140
+ * 4. When `options.credential_types` is supplied, every entry in
141
+ * `required_credential_types` is registered in that map.
142
+ * 5. When `options.scope_kinds` is supplied, every entry in
143
+ * `applicable_scope_kinds` is registered in that map. (Builtins
144
+ * declare empty `applicable_scope_kinds`, so they pass any registry.)
145
+ * 6. When `options.grant_paths` is supplied, every entry in
146
+ * `grant_paths` is registered in that map. (Builtins use only
147
+ * `'admin'` and `'bootstrap'`, both of which are builtin grant
148
+ * paths, so they pass the default registry from
149
+ * `create_grant_path_schema()`.)
63
150
  *
64
- * @param app_roles - app-defined roles with optional config overrides
65
- * @returns `{Role, role_options}` Zod schema and full config map
151
+ * @param consumer_roles - app-defined role specs
152
+ * @param options - optional registries for cross-axis validation
153
+ * @returns `{Role, role_specs}` — Zod schema and full spec map
66
154
  *
67
- * @throws Error if any `app_roles` key fails the `RoleName` regex or collides with a builtin role
155
+ * @throws Error if any `consumer_roles` entry fails any of the construction-time guards above
68
156
  *
69
157
  * @example
70
158
  * ```ts
71
- * // visiones
72
- * const {Role, role_options} = create_role_schema({
73
- * teacher: {},
159
+ * // visiones — opt into all four registries for full construction-time validation
160
+ * const credential_types = create_credential_type_schema();
161
+ * const scope_kinds = create_scope_kind_schema({
162
+ * classroom: {description: 'A classroom — teacher and student role_grants scope here.'},
74
163
  * });
75
- * // Role validates 'keeper' | 'admin' | 'teacher'
76
- * // role_options has all 3 entries with defaults applied
164
+ * const grant_paths = create_grant_path_schema();
165
+ *
166
+ * const {Role, role_specs} = create_role_schema(
167
+ * [
168
+ * {
169
+ * name: 'teacher',
170
+ * description: 'Educator role. Web-grantable; applies at classroom scope.',
171
+ * grant_paths: ['admin'],
172
+ * applicable_scope_kinds: ['classroom'],
173
+ * },
174
+ * ],
175
+ * {credential_types, scope_kinds, grant_paths},
176
+ * );
77
177
  * ```
78
178
  */
79
- export declare const create_role_schema: <T extends string>(app_roles: Record<T, RoleOptions>) => RoleSchemaResult;
179
+ export declare const create_role_schema: (consumer_roles: ReadonlyArray<RoleSpec>, options?: CreateRoleSchemaOptions) => RoleSchemaResult;
180
+ /**
181
+ * Predicate over a `RoleSpec` map: does the named role include the given
182
+ * grant path? Returns `false` for unknown roles. Used by
183
+ * `admin_actions.create_admin_actions` (path = `'admin'`) and
184
+ * `self_service_role_actions.create_self_service_role_actions` (path =
185
+ * `'self_service'`) to derive their default eligibility filters.
186
+ */
187
+ export declare const role_has_grant_path: (role_specs: ReadonlyMap<string, RoleSpec>, role: string, grant_path: string) => boolean;
188
+ /** Filter helper: list every role whose `grant_paths` includes the given path. */
189
+ export declare const list_roles_with_grant_path: (role_specs: ReadonlyMap<string, RoleSpec>, grant_path: string) => Array<string>;
80
190
  //# sourceMappingURL=role_schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"role_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,0FAA0F;AAC1F,eAAO,MAAM,QAAQ,aAKnB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAIhD,sFAAsF;AACtF,eAAO,MAAM,WAAW,WAAW,CAAC;AAEpC,+EAA+E;AAC/E,eAAO,MAAM,UAAU,UAAU,CAAC;AAElC,+CAA+C;AAC/C,eAAO,MAAM,aAAa,8BAAqC,CAAC;AAEhE,yCAAyC;AACzC,eAAO,MAAM,WAAW;;;EAAwB,CAAC;AACjD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAItD;;;;;GAKG;AACH,MAAM,WAAW,WAAW;IAC3B,iGAAiG;IACjG,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,0EAA0E;IAC1E,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,CAG1E,CAAC;AAEH,sFAAsF;AACtF,MAAM,WAAW,gBAAgB;IAChC,sGAAsG;IACtG,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB,2EAA2E;IAC3E,YAAY,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC;CACzD;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,MAAM,EAClD,WAAW,MAAM,CAAC,CAAC,EAAE,WAAW,CAAC,KAC/B,gBAwBF,CAAC"}
1
+ {"version":3,"file":"role_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/role_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,EAEN,KAAK,0BAA0B,EAC/B,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAGN,KAAK,qBAAqB,EAC1B,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,wBAAwB,CAAC;AAElE,0FAA0F;AAC1F,eAAO,MAAM,QAAQ,aAKnB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC;AAIhD,sFAAsF;AACtF,eAAO,MAAM,WAAW,WAAW,CAAC;AAEpC,iEAAiE;AACjE,eAAO,MAAM,UAAU,UAAU,CAAC;AAElC,+CAA+C;AAC/C,eAAO,MAAM,aAAa,8BAAqC,CAAC;AAEhE,yCAAyC;AACzC,eAAO,MAAM,WAAW;;;EAAwB,CAAC;AACjD,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,QAAQ;IACxB,qFAAqF;IACrF,IAAI,EAAE,MAAM,CAAC;IACb,yDAAyD;IACzD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,yBAAyB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAClD;;;;;OAKG;IACH,sBAAsB,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC/C;;;;;;;;;;OAUG;IACH,WAAW,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACpC;AAED;;;;;;;GAOG;AACH,eAAO,MAAM,0BAA0B,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAuBnE,CAAC;AAEH,iGAAiG;AACjG,MAAM,WAAW,uBAAuB;IACvC,uGAAuG;IACvG,gBAAgB,CAAC,EAAE,0BAA0B,CAAC;IAC9C,+FAA+F;IAC/F,WAAW,CAAC,EAAE,qBAAqB,CAAC;IACpC,oFAAoF;IACpF,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACpC;AAED,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAChC,0GAA0G;IAC1G,IAAI,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACxB,yEAAyE;IACzE,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;CAC1C;AAkBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,eAAO,MAAM,kBAAkB,GAC9B,gBAAgB,aAAa,CAAC,QAAQ,CAAC,EACvC,UAAS,uBAA4B,KACnC,gBA2CF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,GAC/B,YAAY,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,EACzC,MAAM,MAAM,EACZ,YAAY,MAAM,KAChB,OAGF,CAAC;AAEF,kFAAkF;AAClF,eAAO,MAAM,0BAA0B,GACtC,YAAY,WAAW,CAAC,MAAM,EAAE,QAAQ,CAAC,EACzC,YAAY,MAAM,KAChB,KAAK,CAAC,MAAM,CAMd,CAAC"}