@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
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Permit offer database queries.
2
+ * Role grant offer database queries.
3
3
  *
4
- * Covers the offer side of the consentful-permits flow: create (with
4
+ * Covers the offer side of the consentful-role-grants flow: create (with
5
5
  * re-offer upsert), decline, retract, list, find-pending, sweep-expired,
6
- * and the atomic `query_accept_offer` that bridges offer → permit.
6
+ * and the atomic `query_accept_offer` that bridges offer → role_grant.
7
7
  *
8
8
  * IDOR guards are expressed in each helper's signature — decline/accept
9
9
  * require the recipient's `to_account_id`, retract requires the grantor's
@@ -12,18 +12,18 @@
12
12
  * @module
13
13
  */
14
14
  import { assert_row } from '../db/assert_row.js';
15
- import { PERMIT_OFFER_SCOPE_SENTINEL_UUID, } from './permit_offer_schema.js';
15
+ import { ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN, ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID, } from './role_grant_offer_schema.js';
16
16
  import { query_audit_log } from './audit_log_queries.js';
17
17
  /**
18
18
  * Error thrown by offer-lifecycle queries when the offer is in a non-pending
19
19
  * state (accepted / declined / retracted / superseded) and therefore not
20
- * actionable. Distinct from `PermitOfferExpiredError` — expiry has its own
20
+ * actionable. Distinct from `RoleGrantOfferExpiredError` — expiry has its own
21
21
  * user-facing story ("ask the grantor to re-send") so it travels separately.
22
22
  */
23
- export class PermitOfferAlreadyTerminalError extends Error {
23
+ export class RoleGrantOfferAlreadyTerminalError extends Error {
24
24
  constructor(offer_id) {
25
25
  super(`Offer ${offer_id} is already in a terminal state`);
26
- this.name = 'PermitOfferAlreadyTerminalError';
26
+ this.name = 'RoleGrantOfferAlreadyTerminalError';
27
27
  }
28
28
  }
29
29
  /**
@@ -32,10 +32,10 @@ export class PermitOfferAlreadyTerminalError extends Error {
32
32
  * must not be accepted, even in the race window between expiry and the
33
33
  * sweep stamping the audit event.
34
34
  */
35
- export class PermitOfferExpiredError extends Error {
35
+ export class RoleGrantOfferExpiredError extends Error {
36
36
  constructor(offer_id) {
37
37
  super(`Offer ${offer_id} has expired`);
38
- this.name = 'PermitOfferExpiredError';
38
+ this.name = 'RoleGrantOfferExpiredError';
39
39
  }
40
40
  }
41
41
  /**
@@ -44,14 +44,14 @@ export class PermitOfferExpiredError extends Error {
44
44
  * (IDOR guard) — the standard 404-over-403 pattern that avoids disclosing
45
45
  * whether an offer id exists.
46
46
  */
47
- export class PermitOfferNotFoundError extends Error {
47
+ export class RoleGrantOfferNotFoundError extends Error {
48
48
  constructor(offer_id) {
49
49
  super(`Offer ${offer_id} not found`);
50
- this.name = 'PermitOfferNotFoundError';
50
+ this.name = 'RoleGrantOfferNotFoundError';
51
51
  }
52
52
  }
53
53
  /**
54
- * Error thrown when a grantor attempts to offer a permit to their own account.
54
+ * Error thrown when a grantor attempts to offer a role_grant to their own account.
55
55
  *
56
56
  * Enforced via a single SELECT on the grantor's `actor.account_id` (rather
57
57
  * than via a CHECK constraint or a denormalized column). Resolving from the
@@ -59,40 +59,40 @@ export class PermitOfferNotFoundError extends Error {
59
59
  * recipient account may host many actors, but the grantor → account binding
60
60
  * remains 1:1 by definition of `actor`.
61
61
  */
62
- export class PermitOfferSelfTargetError extends Error {
62
+ export class RoleGrantOfferSelfTargetError extends Error {
63
63
  constructor() {
64
- super('Cannot offer a permit to your own account');
65
- this.name = 'PermitOfferSelfTargetError';
64
+ super('Cannot offer a role_grant to your own account');
65
+ this.name = 'RoleGrantOfferSelfTargetError';
66
66
  }
67
67
  }
68
68
  /**
69
69
  * Error thrown when an actor-targeted offer is being accepted by an actor
70
- * other than `offer.to_actor_id`. Distinct from `PermitOfferNotFoundError`
70
+ * other than `offer.to_actor_id`. Distinct from `RoleGrantOfferNotFoundError`
71
71
  * (the IDOR mask): once an offer has been resolved to the recipient account,
72
72
  * a wrong-actor accept on a same-account actor is a contract violation, not
73
73
  * a privacy boundary — surface a specific error so the client UI can
74
74
  * distinguish "this offer isn't for you" from "no such offer".
75
75
  */
76
- export class PermitOfferActorMismatchError extends Error {
76
+ export class RoleGrantOfferActorMismatchError extends Error {
77
77
  constructor(offer_id) {
78
78
  super(`Offer ${offer_id} is targeted to a different actor on this account`);
79
- this.name = 'PermitOfferActorMismatchError';
79
+ this.name = 'RoleGrantOfferActorMismatchError';
80
80
  }
81
81
  }
82
82
  /**
83
- * Error thrown when `query_permit_offer_create` is called with a
83
+ * Error thrown when `query_role_grant_offer_create` is called with a
84
84
  * `to_actor_id` that does not exist or does not belong to `to_account_id`.
85
85
  * Surfaces the actor↔account binding mismatch at the boundary instead of
86
86
  * letting the FK silently disagree with the recipient field.
87
87
  */
88
- export class PermitOfferActorAccountMismatchError extends Error {
88
+ export class RoleGrantOfferActorAccountMismatchError extends Error {
89
89
  constructor() {
90
90
  super('to_actor_id does not belong to to_account_id');
91
- this.name = 'PermitOfferActorAccountMismatchError';
91
+ this.name = 'RoleGrantOfferActorAccountMismatchError';
92
92
  }
93
93
  }
94
94
  /**
95
- * Create a new permit offer, or refresh an existing pending offer for the
95
+ * Create a new role_grant offer, or refresh an existing pending offer for the
96
96
  * same `(to_account_id, role, scope_id, from_actor_id)` tuple.
97
97
  *
98
98
  * Re-offer semantics: a second call by the same grantor with the same
@@ -104,7 +104,7 @@ export class PermitOfferActorAccountMismatchError extends Error {
104
104
  * a distinct row — multiple pending grantors coexist. After a terminal
105
105
  * state, a re-offer is a fresh INSERT.
106
106
  *
107
- * Self-offer rejection: throws `PermitOfferSelfTargetError` if the offering
107
+ * Self-offer rejection: throws `RoleGrantOfferSelfTargetError` if the offering
108
108
  * actor belongs to the recipient account.
109
109
  *
110
110
  * Actor-targeted offers: when `to_actor_id` is supplied,
@@ -113,11 +113,11 @@ export class PermitOfferActorAccountMismatchError extends Error {
113
113
  * `target_actor_id` null even when the recipient binding is known at
114
114
  * offer time. The actor↔account binding is verified here in one SELECT.
115
115
  *
116
- * @mutates `permit_offer` table - inserts a new offer or upserts the matching pending row
117
- * @throws PermitOfferSelfTargetError if the offering actor belongs to `to_account_id`
118
- * @throws PermitOfferActorAccountMismatchError if `to_actor_id` is set but does not belong to `to_account_id`
116
+ * @mutates `role_grant_offer` table - inserts a new offer or upserts the matching pending row
117
+ * @throws RoleGrantOfferSelfTargetError if the offering actor belongs to `to_account_id`
118
+ * @throws RoleGrantOfferActorAccountMismatchError if `to_actor_id` is set but does not belong to `to_account_id`
119
119
  */
120
- export const query_permit_offer_create = async (deps, input) => {
120
+ export const query_role_grant_offer_create = async (deps, input) => {
121
121
  // Self-target check resolves the **grantor** actor's account and
122
122
  // compares against to_account_id. This is multi-actor-correct:
123
123
  // a single account may host many actors, and self-target means
@@ -129,18 +129,24 @@ export const query_permit_offer_create = async (deps, input) => {
129
129
  // actor wasn't the offering one.)
130
130
  const grantor = await deps.db.query_one(`SELECT account_id FROM actor WHERE id = $1`, [input.from_actor_id]);
131
131
  if (grantor && grantor.account_id === input.to_account_id) {
132
- throw new PermitOfferSelfTargetError();
132
+ throw new RoleGrantOfferSelfTargetError();
133
133
  }
134
134
  if (input.to_actor_id != null) {
135
135
  const target = await deps.db.query_one(`SELECT account_id FROM actor WHERE id = $1`, [input.to_actor_id]);
136
136
  if (!target || target.account_id !== input.to_account_id) {
137
- throw new PermitOfferActorAccountMismatchError();
137
+ throw new RoleGrantOfferActorAccountMismatchError();
138
138
  }
139
139
  }
140
- const row = await deps.db.query_one(`INSERT INTO permit_offer
141
- (from_actor_id, to_account_id, to_actor_id, role, scope_id, message, expires_at)
142
- VALUES ($1, $2, $3, $4, $5, $6, $7)
143
- ON CONFLICT (to_account_id, role, COALESCE(scope_id, '${PERMIT_OFFER_SCOPE_SENTINEL_UUID}'::uuid), from_actor_id)
140
+ const row = await deps.db.query_one(`INSERT INTO role_grant_offer
141
+ (from_actor_id, to_account_id, to_actor_id, role, scope_kind, scope_id, message, expires_at)
142
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
143
+ ON CONFLICT (
144
+ to_account_id,
145
+ role,
146
+ COALESCE(scope_kind, '${ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN}'),
147
+ COALESCE(scope_id, '${ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID}'::uuid),
148
+ from_actor_id
149
+ )
144
150
  WHERE accepted_at IS NULL AND declined_at IS NULL AND retracted_at IS NULL AND superseded_at IS NULL
145
151
  DO UPDATE SET
146
152
  to_actor_id = EXCLUDED.to_actor_id,
@@ -151,18 +157,19 @@ export const query_permit_offer_create = async (deps, input) => {
151
157
  input.to_account_id,
152
158
  input.to_actor_id ?? null,
153
159
  input.role,
160
+ input.scope_kind ?? null,
154
161
  input.scope_id ?? null,
155
162
  input.message ?? null,
156
163
  input.expires_at.toISOString(),
157
164
  ]);
158
- return assert_row(row, 'INSERT INTO permit_offer');
165
+ return assert_row(row, 'INSERT INTO role_grant_offer');
159
166
  };
160
167
  /**
161
168
  * Mark an offer declined.
162
169
  *
163
170
  * Guarded by `to_account_id` (IDOR). Returns `null` if the offer does not
164
171
  * exist or belongs to a different account. Throws
165
- * `PermitOfferAlreadyTerminalError` if the offer exists for the caller but
172
+ * `RoleGrantOfferAlreadyTerminalError` if the offer exists for the caller but
166
173
  * is already in a terminal state.
167
174
  *
168
175
  * Returns the declined offer with the grantor's `from_account_id` joined
@@ -171,12 +178,12 @@ export const query_permit_offer_create = async (deps, input) => {
171
178
  * grantor account), satisfying the "both populated → same account"
172
179
  * invariant the audit-log column comments describe.
173
180
  *
174
- * @mutates `permit_offer` row - sets `declined_at` and `decline_reason`
175
- * @throws PermitOfferAlreadyTerminalError if the offer is already accepted, declined, retracted, or superseded
181
+ * @mutates `role_grant_offer` row - sets `declined_at` and `decline_reason`
182
+ * @throws RoleGrantOfferAlreadyTerminalError if the offer is already accepted, declined, retracted, or superseded
176
183
  */
177
- export const query_permit_offer_decline = async (deps, offer_id, to_account_id, reason) => {
184
+ export const query_role_grant_offer_decline = async (deps, offer_id, to_account_id, reason) => {
178
185
  const updated = await deps.db.query_one(`WITH updated AS (
179
- UPDATE permit_offer
186
+ UPDATE role_grant_offer
180
187
  SET declined_at = NOW(), decline_reason = $3
181
188
  WHERE id = $1
182
189
  AND to_account_id = $2
@@ -198,14 +205,14 @@ export const query_permit_offer_decline = async (deps, offer_id, to_account_id,
198
205
  *
199
206
  * Guarded by `from_actor_id` (IDOR). Returns `null` if the offer does not
200
207
  * exist or was issued by a different actor. Throws
201
- * `PermitOfferAlreadyTerminalError` if the offer exists for this grantor
208
+ * `RoleGrantOfferAlreadyTerminalError` if the offer exists for this grantor
202
209
  * but is already in a terminal state.
203
210
  *
204
- * @mutates `permit_offer` row - sets `retracted_at`
205
- * @throws PermitOfferAlreadyTerminalError if the offer is already accepted, declined, retracted, or superseded
211
+ * @mutates `role_grant_offer` row - sets `retracted_at`
212
+ * @throws RoleGrantOfferAlreadyTerminalError if the offer is already accepted, declined, retracted, or superseded
206
213
  */
207
- export const query_permit_offer_retract = async (deps, offer_id, from_actor_id) => {
208
- const updated = await deps.db.query_one(`UPDATE permit_offer
214
+ export const query_role_grant_offer_retract = async (deps, offer_id, from_actor_id) => {
215
+ const updated = await deps.db.query_one(`UPDATE role_grant_offer
209
216
  SET retracted_at = NOW()
210
217
  WHERE id = $1
211
218
  AND from_actor_id = $2
@@ -231,11 +238,11 @@ const resolve_terminal_or_missing = async (deps, offer_id, scope) => {
231
238
  conditions.push(`from_actor_id = $${idx++}`);
232
239
  params.push(scope.from_actor_id);
233
240
  }
234
- const row = await deps.db.query_one(`SELECT * FROM permit_offer WHERE ${conditions.join(' AND ')}`, params);
241
+ const row = await deps.db.query_one(`SELECT * FROM role_grant_offer WHERE ${conditions.join(' AND ')}`, params);
235
242
  if (!row)
236
243
  return null;
237
244
  if (row.accepted_at || row.declined_at || row.retracted_at || row.superseded_at) {
238
- throw new PermitOfferAlreadyTerminalError(offer_id);
245
+ throw new RoleGrantOfferAlreadyTerminalError(offer_id);
239
246
  }
240
247
  return null;
241
248
  };
@@ -244,10 +251,10 @@ const resolve_terminal_or_missing = async (deps, offer_id, scope) => {
244
251
  *
245
252
  * Expired offers are filtered server-side (`expires_at > NOW()`) so the
246
253
  * inbox never surfaces a row that can no longer be accepted. The periodic
247
- * sweep (`query_permit_offer_sweep_expired`) handles audit tombstoning.
254
+ * sweep (`query_role_grant_offer_sweep_expired`) handles audit tombstoning.
248
255
  */
249
- export const query_permit_offer_list = async (deps, to_account_id) => {
250
- return deps.db.query(`SELECT * FROM permit_offer
256
+ export const query_role_grant_offer_list = async (deps, to_account_id) => {
257
+ return deps.db.query(`SELECT * FROM role_grant_offer
251
258
  WHERE to_account_id = $1
252
259
  AND accepted_at IS NULL
253
260
  AND declined_at IS NULL
@@ -261,8 +268,8 @@ export const query_permit_offer_list = async (deps, to_account_id) => {
261
268
  *
262
269
  * Includes terminal offers — used by the grantor-side admin / history view.
263
270
  */
264
- export const query_permit_offer_history_for_account = async (deps, account_id, limit = 100, offset = 0) => {
265
- return deps.db.query(`SELECT o.* FROM permit_offer o
271
+ export const query_role_grant_offer_history_for_account = async (deps, account_id, limit = 100, offset = 0) => {
272
+ return deps.db.query(`SELECT o.* FROM role_grant_offer o
266
273
  LEFT JOIN actor a ON a.id = o.from_actor_id
267
274
  WHERE o.to_account_id = $1 OR a.account_id = $1
268
275
  ORDER BY o.created_at DESC
@@ -272,8 +279,8 @@ export const query_permit_offer_history_for_account = async (deps, account_id, l
272
279
  * Look up a pending offer by id. Returns `null` if the offer is terminal,
273
280
  * expired (server-side filter), or missing.
274
281
  */
275
- export const query_permit_offer_find_pending = async (deps, offer_id) => {
276
- const row = await deps.db.query_one(`SELECT * FROM permit_offer
282
+ export const query_role_grant_offer_find_pending = async (deps, offer_id) => {
283
+ const row = await deps.db.query_one(`SELECT * FROM role_grant_offer
277
284
  WHERE id = $1
278
285
  AND accepted_at IS NULL
279
286
  AND declined_at IS NULL
@@ -285,13 +292,13 @@ export const query_permit_offer_find_pending = async (deps, offer_id) => {
285
292
  /**
286
293
  * Return pending offers whose `expires_at` has passed.
287
294
  *
288
- * Callers fire `permit_offer_expire` audit events for each row. The schema
295
+ * Callers fire `role_grant_offer_expire` audit events for each row. The schema
289
296
  * does not tombstone the row, so callers are responsible for their own
290
- * idempotency (e.g. check whether a `permit_offer_expire` audit event
297
+ * idempotency (e.g. check whether a `role_grant_offer_expire` audit event
291
298
  * already exists for the offer id).
292
299
  */
293
- export const query_permit_offer_sweep_expired = async (deps) => {
294
- return deps.db.query(`SELECT * FROM permit_offer
300
+ export const query_role_grant_offer_sweep_expired = async (deps) => {
301
+ return deps.db.query(`SELECT * FROM role_grant_offer
295
302
  WHERE accepted_at IS NULL
296
303
  AND declined_at IS NULL
297
304
  AND retracted_at IS NULL
@@ -300,35 +307,35 @@ export const query_permit_offer_sweep_expired = async (deps) => {
300
307
  ORDER BY expires_at ASC`);
301
308
  };
302
309
  /**
303
- * Accept an offer atomically: mark accepted, insert the permit, stamp
304
- * `resulting_permit_id`, supersede sibling pending offers for the same
305
- * `(to_account, role, scope)`, and emit `permit_offer_accept` +
306
- * `permit_grant` + one `permit_offer_supersede` per sibling. Must run
310
+ * Accept an offer atomically: mark accepted, insert the role_grant, stamp
311
+ * `resulting_role_grant_id`, supersede sibling pending offers for the same
312
+ * `(to_account, role, scope)`, and emit `role_grant_offer_accept` +
313
+ * `role_grant_create` + one `role_grant_offer_supersede` per sibling. Must run
307
314
  * inside a transaction — the caller's route spec should declare
308
315
  * `transaction: true` (or wrap explicitly).
309
316
  *
310
317
  * Idempotent on race: if a second concurrent call observes the offer
311
- * already accepted, returns the existing permit rather than creating a
318
+ * already accepted, returns the existing role_grant rather than creating a
312
319
  * duplicate or throwing.
313
320
  *
314
321
  * Error map:
315
- * - `PermitOfferNotFoundError` — offer does not exist, or belongs to a
322
+ * - `RoleGrantOfferNotFoundError` — offer does not exist, or belongs to a
316
323
  * different recipient (IDOR guard). The offer row is untouched.
317
- * - `PermitOfferAlreadyTerminalError` — offer is declined, retracted, or
324
+ * - `RoleGrantOfferAlreadyTerminalError` — offer is declined, retracted, or
318
325
  * superseded.
319
- * - `PermitOfferExpiredError` — offer is pending but past `expires_at`.
326
+ * - `RoleGrantOfferExpiredError` — offer is pending but past `expires_at`.
320
327
  *
321
328
  * Sibling supersede is what closes the "accept a pre-revoke sibling offer
322
329
  * to bypass a revoke" path: once A is accepted, B/C/... can no longer be
323
- * accepted even if the resulting permit is later revoked.
330
+ * accepted even if the resulting role_grant is later revoked.
324
331
  *
325
- * @mutates `permit_offer` row - stamps `accepted_at` and `resulting_permit_id`
326
- * @mutates `permit` table - inserts the resulting permit (idempotent on race)
327
- * @mutates `permit_offer` siblings - stamps `superseded_at` on every other pending offer for the tuple
328
- * @mutates `audit_log` table - emits `permit_offer_accept` + `permit_grant` + one `permit_offer_supersede` per sibling
329
- * @throws PermitOfferNotFoundError if the offer is missing or belongs to another recipient
330
- * @throws PermitOfferAlreadyTerminalError if the offer is declined, retracted, or superseded
331
- * @throws PermitOfferExpiredError if the offer is pending but past `expires_at`
332
+ * @mutates `role_grant_offer` row - stamps `accepted_at` and `resulting_role_grant_id`
333
+ * @mutates `role_grant` table - inserts the resulting role_grant (idempotent on race)
334
+ * @mutates `role_grant_offer` siblings - stamps `superseded_at` on every other pending offer for the tuple
335
+ * @mutates `audit_log` table - emits `role_grant_offer_accept` + `role_grant_create` + one `role_grant_offer_supersede` per sibling
336
+ * @throws RoleGrantOfferNotFoundError if the offer is missing or belongs to another recipient
337
+ * @throws RoleGrantOfferAlreadyTerminalError if the offer is declined, retracted, or superseded
338
+ * @throws RoleGrantOfferExpiredError if the offer is pending but past `expires_at`
332
339
  * @throws Error if the accepting `actor_id` does not belong to `to_account_id`, or invariant assertions fail
333
340
  */
334
341
  export const query_accept_offer = async (deps, input) => {
@@ -336,31 +343,31 @@ export const query_accept_offer = async (deps, input) => {
336
343
  // Claim the offer with a row-level lock. Subsequent concurrent callers
337
344
  // block on the lock until this transaction commits/rolls back; after commit
338
345
  // they see the new state (accepted or terminal) and branch idempotently.
339
- // We defer writing `accepted_at` until the permit row exists — the
340
- // `permit_offer_permit_iff_accepted` CHECK constraint demands both be set
346
+ // We defer writing `accepted_at` until the role_grant row exists — the
347
+ // `role_grant_offer_role_grant_iff_accepted` CHECK constraint demands both be set
341
348
  // (or neither) at row-visibility time.
342
- const locked = await deps.db.query_one(`SELECT * FROM permit_offer
349
+ const locked = await deps.db.query_one(`SELECT * FROM role_grant_offer
343
350
  WHERE id = $1 AND to_account_id = $2
344
351
  FOR UPDATE`, [offer_id, to_account_id]);
345
352
  if (!locked) {
346
- throw new PermitOfferNotFoundError(offer_id);
353
+ throw new RoleGrantOfferNotFoundError(offer_id);
347
354
  }
348
355
  if (locked.accepted_at) {
349
- // Race winner already committed; return the pre-existing permit.
350
- // `permit_offer_permit_iff_accepted` CHECK guarantees resulting_permit_id is non-null.
351
- const permit = assert_row(await deps.db.query_one(`SELECT * FROM permit WHERE id = $1`, [
352
- locked.resulting_permit_id,
353
- ]), 'resulting_permit lookup');
356
+ // Race winner already committed; return the pre-existing role_grant.
357
+ // `role_grant_offer_role_grant_iff_accepted` CHECK guarantees resulting_role_grant_id is non-null.
358
+ const role_grant = assert_row(await deps.db.query_one(`SELECT * FROM role_grant WHERE id = $1`, [
359
+ locked.resulting_role_grant_id,
360
+ ]), 'resulting_role_grant lookup');
354
361
  // Multi-actor guard: two actors on the same recipient account may
355
362
  // both race an account-grain offer — the loser must not silently
356
- // receive the winner's permit (which would tell them "you got it"
357
- // while the actor on the permit is someone else). Treat the offer
363
+ // receive the winner's role_grant (which would tell them "you got it"
364
+ // while the actor on the role_grant is someone else). Treat the offer
358
365
  // as terminal for the loser.
359
- if (permit.actor_id !== actor_id) {
360
- throw new PermitOfferAlreadyTerminalError(offer_id);
366
+ if (role_grant.actor_id !== actor_id) {
367
+ throw new RoleGrantOfferAlreadyTerminalError(offer_id);
361
368
  }
362
369
  return {
363
- permit,
370
+ role_grant,
364
371
  offer: locked,
365
372
  created: false,
366
373
  superseded_offers: [],
@@ -368,13 +375,13 @@ export const query_accept_offer = async (deps, input) => {
368
375
  };
369
376
  }
370
377
  if (locked.declined_at || locked.retracted_at || locked.superseded_at) {
371
- throw new PermitOfferAlreadyTerminalError(offer_id);
378
+ throw new RoleGrantOfferAlreadyTerminalError(offer_id);
372
379
  }
373
380
  // Expiry check AFTER the accepted-path: a validly-accepted offer past its
374
- // expires_at still returns the permit idempotently. Only pending offers
381
+ // expires_at still returns the role_grant idempotently. Only pending offers
375
382
  // past expiry reach this branch.
376
383
  if (new Date(locked.expires_at) <= new Date()) {
377
- throw new PermitOfferExpiredError(offer_id);
384
+ throw new RoleGrantOfferExpiredError(offer_id);
378
385
  }
379
386
  // Actor-targeted offer gate. When the offer is account-grain
380
387
  // (`to_actor_id IS NULL`) any actor on `to_account_id` may accept and
@@ -385,7 +392,7 @@ export const query_accept_offer = async (deps, input) => {
385
392
  //
386
393
  // Ordering contract: this check fires *before* the cross-account
387
394
  // `actor_check` SELECT below. A wrong-actor accept on an actor-grain
388
- // offer surfaces as `PermitOfferActorMismatchError` regardless of
395
+ // offer surfaces as `RoleGrantOfferActorMismatchError` regardless of
389
396
  // whether the supplied `actor_id` belongs to `to_account_id` — the
390
397
  // actor-grain binding is the tighter constraint and dominates. The
391
398
  // cross-account `Error` only fires for account-grain offers (or
@@ -393,42 +400,48 @@ export const query_accept_offer = async (deps, input) => {
393
400
  // the actor turns out not to be on the account, which is unreachable
394
401
  // under the FK invariant but stays as defense-in-depth).
395
402
  if (locked.to_actor_id != null && locked.to_actor_id !== actor_id) {
396
- throw new PermitOfferActorMismatchError(offer_id);
403
+ throw new RoleGrantOfferActorMismatchError(offer_id);
397
404
  }
398
405
  // Verify the accepting actor belongs to the recipient account.
399
406
  // Defense-in-depth: the action handler passes `auth.actor.id` which is
400
407
  // already session-bound, but enforcing the invariant here protects
401
408
  // direct callers (tests, future consumers) from cross-account binding
402
- // bugs that would silently grant a permit to the wrong actor.
409
+ // bugs that would silently grant a role_grant to the wrong actor.
403
410
  const actor_check = await deps.db.query_one(`SELECT id FROM actor WHERE id = $1 AND account_id = $2`, [actor_id, to_account_id]);
404
411
  if (!actor_check) {
405
412
  throw new Error(`Accepting actor ${actor_id} does not belong to account ${to_account_id} (offer ${offer_id})`);
406
413
  }
407
- // Insert the permit. Uses the normal grant idempotency — if another
408
- // code path already granted the same (actor, role, scope), reuse it.
409
- const granted_permit = await deps.db.query_one(`INSERT INTO permit (actor_id, role, scope_id, granted_by, source_offer_id)
410
- VALUES ($1, $2, $3, $4, $5)
411
- ON CONFLICT (actor_id, role, COALESCE(scope_id, '${PERMIT_OFFER_SCOPE_SENTINEL_UUID}'::uuid))
414
+ // Insert the role_grant. Uses the normal grant idempotency — if another
415
+ // code path already granted the same (actor, role, scope_kind, scope), reuse it.
416
+ const granted_role_grant = await deps.db.query_one(`INSERT INTO role_grant (actor_id, role, scope_kind, scope_id, granted_by, source_offer_id)
417
+ VALUES ($1, $2, $3, $4, $5, $6)
418
+ ON CONFLICT (
419
+ actor_id,
420
+ role,
421
+ COALESCE(scope_kind, '${ROLE_GRANT_OFFER_SCOPE_KIND_GLOBAL_TOKEN}'),
422
+ COALESCE(scope_id, '${ROLE_GRANT_OFFER_SCOPE_SENTINEL_UUID}'::uuid)
423
+ )
412
424
  WHERE revoked_at IS NULL
413
425
  DO NOTHING
414
- RETURNING *`, [actor_id, locked.role, locked.scope_id, locked.from_actor_id, locked.id]);
415
- let permit;
416
- if (granted_permit) {
417
- permit = granted_permit;
426
+ RETURNING *`, [actor_id, locked.role, locked.scope_kind, locked.scope_id, locked.from_actor_id, locked.id]);
427
+ let role_grant;
428
+ if (granted_role_grant) {
429
+ role_grant = granted_role_grant;
418
430
  }
419
431
  else {
420
- const existing = await deps.db.query_one(`SELECT * FROM permit
432
+ const existing = await deps.db.query_one(`SELECT * FROM role_grant
421
433
  WHERE actor_id = $1
422
434
  AND role = $2
423
- AND scope_id IS NOT DISTINCT FROM $3
424
- AND revoked_at IS NULL`, [actor_id, locked.role, locked.scope_id]);
425
- permit = assert_row(existing, 'query_accept_offer idempotent permit lookup');
435
+ AND scope_kind IS NOT DISTINCT FROM $3
436
+ AND scope_id IS NOT DISTINCT FROM $4
437
+ AND revoked_at IS NULL`, [actor_id, locked.role, locked.scope_kind, locked.scope_id]);
438
+ role_grant = assert_row(existing, 'query_accept_offer idempotent role_grant lookup');
426
439
  }
427
440
  // Single UPDATE sets both sides of the CHECK constraint at once.
428
- const offer_accepted = await deps.db.query_one(`UPDATE permit_offer
429
- SET accepted_at = NOW(), resulting_permit_id = $2
441
+ const offer_accepted = await deps.db.query_one(`UPDATE role_grant_offer
442
+ SET accepted_at = NOW(), resulting_role_grant_id = $2
430
443
  WHERE id = $1
431
- RETURNING *`, [locked.id, permit.id]);
444
+ RETURNING *`, [locked.id, role_grant.id]);
432
445
  const offer = assert_row(offer_accepted, 'mark offer accepted');
433
446
  // Supersede sibling pending offers for the same (to_account, role, scope).
434
447
  // Forecloses the "accept this other sibling later to get the role back
@@ -436,7 +449,7 @@ export const query_accept_offer = async (deps, input) => {
436
449
  // is obsoleted by the accept. CTE joins `actor` to surface each sibling's
437
450
  // grantor `account_id` for the caller's notification fan-out.
438
451
  const superseded = await deps.db.query(`WITH updated AS (
439
- UPDATE permit_offer
452
+ UPDATE role_grant_offer
440
453
  SET superseded_at = NOW()
441
454
  WHERE to_account_id = $1
442
455
  AND role = $2
@@ -451,13 +464,13 @@ export const query_accept_offer = async (deps, input) => {
451
464
  SELECT u.*, grantor.account_id AS from_account_id
452
465
  FROM updated u
453
466
  JOIN actor grantor ON grantor.id = u.from_actor_id`, [to_account_id, offer.role, offer.scope_id, offer.id]);
454
- // Emit audit events in-transaction (atomic with the permit insert).
455
- // `RETURNING *` after the SET guarantees `offer.resulting_permit_id === permit.id`.
467
+ // Emit audit events in-transaction (atomic with the role_grant insert).
468
+ // `RETURNING *` after the SET guarantees `offer.resulting_role_grant_id === role_grant.id`.
456
469
  // Accept binds the actor deterministically — populate both target
457
- // columns to mirror `permit_grant` (the in-tx pair) so forensic
470
+ // columns to mirror `role_grant_create` (the in-tx pair) so forensic
458
471
  // queries don't have to split between the two events.
459
472
  const offer_accept_event = await query_audit_log(deps, {
460
- event_type: 'permit_offer_accept',
473
+ event_type: 'role_grant_offer_accept',
461
474
  actor_id,
462
475
  account_id: to_account_id,
463
476
  target_account_id: to_account_id,
@@ -465,18 +478,18 @@ export const query_accept_offer = async (deps, input) => {
465
478
  ip: ip ?? null,
466
479
  metadata: {
467
480
  offer_id: offer.id,
468
- permit_id: permit.id,
481
+ role_grant_id: role_grant.id,
469
482
  role: offer.role,
470
483
  scope_id: offer.scope_id,
471
484
  },
472
485
  });
473
- // `permit_grant` is the canonical actor-bound-subject event — the
474
- // permit just bound to this actor. On self-accept the actor and the
486
+ // `role_grant_create` is the canonical actor-bound-subject event — the
487
+ // role_grant just bound to this actor. On self-accept the actor and the
475
488
  // target are the same identity; on admin direct-grant (separate code
476
489
  // path) they differ. Either way `target_actor_id` carries the
477
490
  // grantee for actor-grain forensics.
478
- const permit_grant_event = await query_audit_log(deps, {
479
- event_type: 'permit_grant',
491
+ const role_grant_create_event = await query_audit_log(deps, {
492
+ event_type: 'role_grant_create',
480
493
  actor_id,
481
494
  account_id: to_account_id,
482
495
  target_account_id: to_account_id,
@@ -484,7 +497,7 @@ export const query_accept_offer = async (deps, input) => {
484
497
  ip: ip ?? null,
485
498
  metadata: {
486
499
  role: offer.role,
487
- permit_id: permit.id,
500
+ role_grant_id: role_grant.id,
488
501
  scope_id: offer.scope_id,
489
502
  source_offer_id: offer.id,
490
503
  },
@@ -495,7 +508,7 @@ export const query_accept_offer = async (deps, input) => {
495
508
  // when the sibling was actor-targeted, account-grain (null) when it
496
509
  // was account-level.
497
510
  supersede_events.push(await query_audit_log(deps, {
498
- event_type: 'permit_offer_supersede',
511
+ event_type: 'role_grant_offer_supersede',
499
512
  actor_id,
500
513
  account_id: to_account_id,
501
514
  target_account_id: to_account_id,
@@ -511,10 +524,10 @@ export const query_accept_offer = async (deps, input) => {
511
524
  }));
512
525
  }
513
526
  return {
514
- permit,
527
+ role_grant,
515
528
  offer,
516
529
  created: true,
517
530
  superseded_offers: superseded,
518
- audit_events: [offer_accept_event, permit_grant_event, ...supersede_events],
531
+ audit_events: [offer_accept_event, role_grant_create_event, ...supersede_events],
519
532
  };
520
533
  };