permdock 0.3.0 → 0.4.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 (323) hide show
  1. package/dist/a2a/index.d.ts +7 -5
  2. package/dist/a2a/index.js +14 -21
  3. package/dist/ai-sdk/index.d.ts +18 -6
  4. package/dist/ai-sdk/index.js +8 -5
  5. package/dist/app-data-CV8pqqMi.js +59 -0
  6. package/dist/approvals/index.d.ts +2 -2
  7. package/dist/approvals/index.js +3 -3
  8. package/dist/{approvals-DbYAuNkG.js → approvals-DZL76lKW.js} +35 -19
  9. package/dist/{approvers-DT4AVyIg.js → approvers-CqPOLD-o.js} +1 -1
  10. package/dist/{arazzo-BAgw9ap6.js → arazzo-C4BcskbX.js} +6 -5
  11. package/dist/{arazzo-CKwwqFWz.js → arazzo-CD8d0WLb.js} +2 -2
  12. package/dist/authzen/index.d.ts +3 -3
  13. package/dist/authzen/index.js +161 -23
  14. package/dist/better-auth/index.d.ts +2 -2
  15. package/dist/better-auth/index.js +6 -19
  16. package/dist/better-supabase/index.d.ts +112 -87
  17. package/dist/better-supabase/index.js +257 -156
  18. package/dist/{bind-C_ob3LKa.d.ts → bind-BzqG8SMb.d.ts} +16 -5
  19. package/dist/{bind-CuzGytRf.js → bind-kk-uyCwH.js} +14 -6
  20. package/dist/build-instance-DIIV9DbC.js +51 -0
  21. package/dist/{canonical-json-DemiK4Vp.js → canonical-json-CFKyFOke.js} +1 -3
  22. package/dist/{capability-2kYTMWDv.d.ts → capability-CksyCWDH.d.ts} +2 -2
  23. package/dist/{capability-4qPdO9gk.js → capability-txATtKV4.js} +3 -2
  24. package/dist/catalog/index.d.ts +1 -1
  25. package/dist/catalog/index.js +1 -1
  26. package/dist/catalog-6yWAX6Ps.js +2 -0
  27. package/dist/{catalog-dFZdWCGN.js → catalog-WHG4kBq7.js} +2 -2
  28. package/dist/{catalog-doc-DdGm82XT.js → catalog-doc-DWtg0gGR.js} +17 -10
  29. package/dist/{catalog-DWF5cYqB.js → catalog-tZUyOfOv.js} +4 -4
  30. package/dist/claude-agent/index.d.ts +4 -4
  31. package/dist/claude-agent/index.js +4 -5
  32. package/dist/clerk/index.d.ts +3 -3
  33. package/dist/clerk/index.js +8 -33
  34. package/dist/cli/bin.js +1 -1
  35. package/dist/cli/index.d.ts +3 -3
  36. package/dist/cli/index.js +3 -3
  37. package/dist/cloud/index.d.ts +4 -4
  38. package/dist/cloud/index.js +9 -15
  39. package/dist/{cloud-CB_ELYuX.js → cloud-C7bSDgX4.js} +2 -2
  40. package/dist/{cloud-DWLXO2af.js → cloud-PeBN3IEn.js} +5 -5
  41. package/dist/{collect-DSTE3ml9.js → collect-2SrB7lfk.js} +6 -8
  42. package/dist/collect-Bw2Jdq1q.js +2 -0
  43. package/dist/{collect-DClJsheJ.js → collect-CQx4hX4N.js} +3 -3
  44. package/dist/{commands-DSxCqG9A.js → commands-Cpip4ULx.js} +13 -13
  45. package/dist/{compact-y6vfu2_f.js → compact-CAf557ZB.js} +11 -1
  46. package/dist/compile/index.d.ts +1 -1
  47. package/dist/compile/index.js +1 -1
  48. package/dist/{compile-Ba4blROR.js → compile-CcEu2ZQ1.js} +6 -5
  49. package/dist/{compile-BkRyBDBw.d.ts → compile-CtTP7eur.d.ts} +2 -2
  50. package/dist/{config-BJiM-xAm.js → config-BIjREpyE.js} +4 -4
  51. package/dist/{config-Dnszbv-6.js → config-DLt6BvV7.js} +2 -2
  52. package/dist/{config-report-DMvWKUxL.js → config-report--Xb9APJS.js} +5 -6
  53. package/dist/{connection-CU8MQxRi.js → connection-DUsczpB5.js} +3 -2
  54. package/dist/constants-DcJABbLW.js +20 -0
  55. package/dist/{context-Bcy85rGi.js → context-DieZVJXu.js} +16 -5
  56. package/dist/convex/index.d.ts +4 -4
  57. package/dist/convex/index.js +14 -16
  58. package/dist/create-BBO24vj6.js +324 -0
  59. package/dist/{credential-8P81ff4u.js → credential-kue0qpbm.js} +4 -4
  60. package/dist/deciding-columns-Sq12mEA6.js +123 -0
  61. package/dist/delegation-input-FTxMK3ji.js +19 -0
  62. package/dist/denied-text-DfRk15uw.d.ts +19 -0
  63. package/dist/{describe-DMYiCjJV.d.ts → describe-C3ldA9os.d.ts} +11 -2
  64. package/dist/{describe-B_gVZEVi.js → describe-Cgkp7eP0.js} +5 -4
  65. package/dist/{diff-DBIhz_f2.js → diff-Cnghh_Ip.js} +2 -2
  66. package/dist/{diff-2VaeChzX.js → diff-rpqoe7Ag.js} +9 -9
  67. package/dist/{doctor-BwCAMLx_.js → doctor-CuVJvhV_.js} +2 -2
  68. package/dist/doctor-owlv6ddh.js +2166 -0
  69. package/dist/drizzle/index.d.ts +2 -2
  70. package/dist/drizzle/index.js +6 -14
  71. package/dist/elysia/index.d.ts +4 -4
  72. package/dist/elysia/index.js +7 -7
  73. package/dist/{errors-DPdqI50v.d.ts → errors-OD_4c__H.d.ts} +2 -2
  74. package/dist/{errors-Cz5zn0mW.js → errors-drnfxYtm.js} +3 -3
  75. package/dist/{evaluations-tz6D9A2G.js → evaluations-C9YJsv3H.js} +45 -7
  76. package/dist/eve/index.d.ts +5 -5
  77. package/dist/eve/index.js +5 -5
  78. package/dist/express/index.d.ts +4 -4
  79. package/dist/express/index.js +8 -7
  80. package/dist/fastify/index.d.ts +15 -5
  81. package/dist/fastify/index.js +9 -29
  82. package/dist/fields-fwfXN01n.js +1016 -0
  83. package/dist/{files-DWUN9PzI.js → files-D_JIzUrP.js} +12 -3
  84. package/dist/{fixtures-ht28Cplr.js → fixtures-h2eG2OZV.js} +5 -6
  85. package/dist/{from-snapshot-CTLijvdm.js → from-snapshot-7REQZe0H.js} +32 -22
  86. package/dist/{generate-CH7ThJ14.js → generate-CNU1pIOo.js} +1 -3
  87. package/dist/grantee-CXY-0C87.js +254 -0
  88. package/dist/grantee-conditions-MQs5NIuX.js +270 -0
  89. package/dist/grantee-match-fxYp9Nmt.js +141 -0
  90. package/dist/{helpers-H_KARDVC.js → helpers-C9YmyBIa.js} +7 -6
  91. package/dist/hono/index.d.ts +4 -4
  92. package/dist/hono/index.js +8 -8
  93. package/dist/hosted-CBHgkzDl.js +655 -0
  94. package/dist/{http-SO7qGttf.js → http-BBoKefeS.js} +35 -2
  95. package/dist/{http-BIhmLfCc.js → http-BEbdMZ05.js} +2 -24
  96. package/dist/{rls-import-BTFVk3jH.js → import-BFIlhetD.js} +86 -80
  97. package/dist/{openapi-import-cmwetbzw.js → import-UHk1N7vH.js} +6 -7
  98. package/dist/{index-C8z4RjQV.d.ts → index-BwMe94Xq.d.ts} +1 -1
  99. package/dist/{index-0nNqHspG.d.ts → index-DgM7w96K.d.ts} +2 -2
  100. package/dist/{index-C7Tw7MaW.d.ts → index-Dz7didpC.d.ts} +3 -3
  101. package/dist/{index-BNc7Wer_.d.ts → index-UdVQgq48.d.ts} +105 -38
  102. package/dist/index.d.ts +15 -13
  103. package/dist/index.js +22 -22
  104. package/dist/{instance-options-jARdwfPN.d.ts → instance-options-plkrsAog.d.ts} +1 -1
  105. package/dist/{json-schema-BsAflUGW.js → json-schema-DBkdUuCO.js} +1 -3
  106. package/dist/jwt/index.d.ts +4 -4
  107. package/dist/jwt/index.js +13 -13
  108. package/dist/{kernel-_RRHmnMY.js → kernel-CB3LrIJM.js} +81 -32
  109. package/dist/kysely/index.d.ts +2 -2
  110. package/dist/kysely/index.js +6 -14
  111. package/dist/{live-options-H2eG8dfJ.js → live-options-BaAFCIiu.js} +16 -3
  112. package/dist/load-B_shHrmd.js +2 -0
  113. package/dist/{load-DhWEB9P1.js → load-C3_oTHyO.js} +11 -3
  114. package/dist/{manifest-iaoBXHKc.js → manifest-BngWKryL.js} +5 -18
  115. package/dist/{map-claims-Z4QZOiFF.js → map-claims-AVlBaJoa.js} +7 -16
  116. package/dist/{map-error-pgUWNNtG.js → map-error-BE27PYkE.js} +5 -5
  117. package/dist/{markers-B0hYLvMD.js → markers-CiwAOZtO.js} +6 -2
  118. package/dist/{may-use-DndQ5tQ-.js → may-use-BOn_dR_w.js} +4 -3
  119. package/dist/mcp/index.d.ts +26 -9
  120. package/dist/mcp/index.js +456 -446
  121. package/dist/{rls-migrate-CN4TlJTO.js → migrate-BwlB5-Wk.js} +75 -15
  122. package/dist/nest/index.d.ts +4 -4
  123. package/dist/nest/index.js +11 -12
  124. package/dist/next/index.d.ts +31 -8
  125. package/dist/next/index.js +29 -33
  126. package/dist/next/plugin.d.ts +1 -1
  127. package/dist/next/plugin.js +1 -1
  128. package/dist/node/index.d.ts +4 -4
  129. package/dist/node/index.js +4 -4
  130. package/dist/ocsf-uYFXGVw2.js +115 -0
  131. package/dist/openai/index.d.ts +5 -5
  132. package/dist/openai/index.js +4 -4
  133. package/dist/openapi/index.d.ts +3 -3
  134. package/dist/openapi/index.js +1 -1
  135. package/dist/{openapi-CqqEbHsL.js → openapi-BJdBmYDG.js} +19 -23
  136. package/dist/{openapi-BqRBvAng.js → openapi-DBzHQbov.js} +2 -5
  137. package/dist/{openapi-l0xYHkcv.js → openapi-DcQ1Kg40.js} +3 -3
  138. package/dist/orpc/index.d.ts +5 -5
  139. package/dist/orpc/index.js +9 -11
  140. package/dist/otel/index.d.ts +2 -2
  141. package/dist/otel/index.js +26 -23
  142. package/dist/output-files-CgIsvdSa.js +140 -0
  143. package/dist/{rls-ownership-RuFqaqGz.js → ownership-BgnbOJFT.js} +387 -379
  144. package/dist/{package-root-zGkxO3oN.js → package-root-DtNnpmUd.js} +3 -2
  145. package/dist/{parse-BuyjxhZV.js → parse-CVFQHk6c.js} +3 -3
  146. package/dist/{parse-local-manifest-BrMozJCo.js → parse-local-manifest-C-q02Pal.js} +2 -4
  147. package/dist/{parse-local-manifest-JLnaBH0S.d.ts → parse-local-manifest-DvYUR0QL.d.ts} +1 -1
  148. package/dist/pdp/index.d.ts +2 -2
  149. package/dist/pdp/index.js +8 -9
  150. package/dist/{permdock-CbgvLnuo.js → permdock-L26udHSk.js} +1387 -1220
  151. package/dist/{rls-fields-74oZPXWL.js → permission-keys-B26jeyUv.js} +1289 -2066
  152. package/dist/permission-registry-BsblpB-z.js +385 -0
  153. package/dist/{permissions-DttnqqxC.d.ts → permission-types-Dg39z9Kv.d.ts} +59 -31
  154. package/dist/{permissions-RJrovw98.js → permissions-CCJ6lmxB.js} +3 -3
  155. package/dist/{permissions-CDgvKG5L.d.ts → permissions-COH-KRgT.d.ts} +2 -2
  156. package/dist/{policy-IB6yxpC3.d.ts → policy-types-sp9P1iIV.d.ts} +661 -582
  157. package/dist/{policy-D2W7uv19.d.ts → policy-ur0BBK_e.d.ts} +2 -2
  158. package/dist/{powersync-DliZK_-y.js → powersync-BFSbSElQ.js} +2 -2
  159. package/dist/powersync-DCDgZgfZ.js +2 -0
  160. package/dist/{powersync-BylawD_U.js → powersync-cZe4kzhL.js} +26 -27
  161. package/dist/prisma/index.d.ts +1 -1
  162. package/dist/prisma/index.js +6 -12
  163. package/dist/{problem-B43ztKIm.js → problem-CnWraVhC.js} +8 -7
  164. package/dist/{problem-details-_AMOYz-w.js → problem-details-DECUnXAn.js} +2 -2
  165. package/dist/{problem-details-DX075l9V.d.ts → problem-details-rIIo8B2C.d.ts} +1 -1
  166. package/dist/{protected-BdnIEFkY.d.ts → protected-B3_P7xuw.d.ts} +9 -6
  167. package/dist/{protected-utU-SLuy.js → protected-BN7BUdY1.js} +20 -8
  168. package/dist/{provider-CSQd7m92.js → provider-Bi-0HNL3.js} +23 -10
  169. package/dist/{provider-DZY281jO.d.ts → provider-kwHC9CZD.d.ts} +1 -1
  170. package/dist/{provider-store-Cfbvo4DE.js → provider-store-Ctt-81Ni.js} +6 -3
  171. package/dist/react/index.d.ts +7 -7
  172. package/dist/react/index.js +5 -5
  173. package/dist/react/provider-client.d.ts +1 -1
  174. package/dist/react/provider-client.js +1 -1
  175. package/dist/react-native/index.d.ts +20 -11
  176. package/dist/react-native/index.js +33 -17
  177. package/dist/{refs-KYb4ntaz.js → refs-f3imMjNW.js} +1 -1
  178. package/dist/relation-walk-BXnlLtg2.js +186 -0
  179. package/dist/relations-CvmSpG67.js +163 -0
  180. package/dist/{replay-CYyiLiwO.d.ts → replay-COpmZUfx.d.ts} +1 -1
  181. package/dist/request-context-BVM7aWNs.js +71 -0
  182. package/dist/request-context-C2_1lchh.d.ts +5 -0
  183. package/dist/resource-relations-D1Un3pcz.js +243 -0
  184. package/dist/retire-BFcxL6un.js +240 -0
  185. package/dist/{rls-B4ur8nKQ.js → rls-55DQTuf5.js} +14 -2
  186. package/dist/{rls-DQtdgTHB.js → rls-BEktUaqg.js} +884 -484
  187. package/dist/{policy-xyWg7QdR.js → role-builder-B4GbKM-i.js} +235 -541
  188. package/dist/{rpc-DL9Sqvb1.js → rpc-nYC3cR_u.js} +15 -5
  189. package/dist/{run-DKDbM4Du.js → run-DMcNW_8T.js} +3 -3
  190. package/dist/{runtime-CB8gQFvS.js → runtime-D_tMXnsq.js} +1 -1
  191. package/dist/{saas-DhpJjnBA.js → saas-DtaA4JuW.js} +9 -8
  192. package/dist/{openapi-schema--ARjyfQr.js → schema-Bol6gqXA.js} +3 -5
  193. package/dist/{schema-D3MnDMxa.js → schema-DjQBcAP9.js} +4 -2
  194. package/dist/scim/index.d.ts +1 -1
  195. package/dist/scim/index.js +1 -1
  196. package/dist/{scim-BFGBBAqR.js → scim-vFR1O5oG.js} +6 -12
  197. package/dist/{scopes-CVdPrWN-.js → scopes-tvpa34ep.js} +10 -2
  198. package/dist/server/index.d.ts +18 -8
  199. package/dist/server/index.js +8 -8
  200. package/dist/{server-DlZ28Oz5.js → server-ZYEHtNHS.js} +4 -4
  201. package/dist/{create-B1d519XK.d.ts → server-types-DdqBwZN_.d.ts} +69 -33
  202. package/dist/shared-1uhI-e-b.js +294 -0
  203. package/dist/{shared-DRu8VFVM.js → shared-JhMeOsDF.js} +1 -1
  204. package/dist/{sink-Bd9PSyfq.js → sink-BTtmfPnE.js} +1 -1
  205. package/dist/{sink-B1fZzIMf.d.ts → sink-CPN4zLfs.d.ts} +1 -1
  206. package/dist/{skills-Biq5uup3.js → skills-BZdRQkxs.js} +4 -3
  207. package/dist/{skills-Bx-U0irG.js → skills-Dle4ZoLF.js} +2 -2
  208. package/dist/{snapshot-Enxd0n-s.js → snapshot-Dc9CHPME.js} +116 -13
  209. package/dist/{snapshot-cache-D6jLelln.js → snapshot-cache-Cod40D-X.js} +1 -1
  210. package/dist/{snapshot-cache-B-LxUahj.d.ts → snapshot-cache-J5T7t_vD.d.ts} +1 -1
  211. package/dist/solid/index.d.ts +22 -22
  212. package/dist/solid/index.js +40 -10
  213. package/dist/{sources-4p0-4xQm.js → sources-B1-b-R0t.js} +25 -26
  214. package/dist/{sources-BdOYLw9j.d.ts → sources-Dljy06zL.d.ts} +6 -2
  215. package/dist/{sql-xdg5f4Fi.js → sql-t8teWOfo.js} +19 -3
  216. package/dist/{src-BDwEZWrZ.js → src-Bb7no-hS.js} +194 -123
  217. package/dist/ssf/index.d.ts +3 -3
  218. package/dist/ssf/index.js +58 -61
  219. package/dist/store-9KO4BlkB.d.ts +147 -0
  220. package/dist/{store-DMAWwAJV.js → store-ILkLUj_o.js} +4 -4
  221. package/dist/{store-options-ByhZq1jV.js → store-options-CbUHDoSe.js} +16 -7
  222. package/dist/{stores-BT2pSCub.js → stores-BHXdSbw9.js} +7 -3
  223. package/dist/stores-Cxz_80g-.d.ts +31 -0
  224. package/dist/{stream-DDpvynSB.js → stream-BMui98KK.js} +2 -1
  225. package/dist/{stream-D1RXubw2.d.ts → stream-Dwsg6WwE.d.ts} +2 -2
  226. package/dist/{subject-settings-BwBmCdTR.js → subject-settings-CkEwrw3W.js} +23 -5
  227. package/dist/supabase/index.d.ts +4 -4
  228. package/dist/supabase/index.js +3 -3
  229. package/dist/supabase/middleware.d.ts +11 -5
  230. package/dist/supabase/middleware.js +10 -7
  231. package/dist/{supabase-C0qESOa-.js → supabase-C1TWlnYt.js} +36 -52
  232. package/dist/{supabase-Boqb9a_Y.js → supabase-Dbqr141s.js} +2 -2
  233. package/dist/supabase-hook-BULg7Ut_.js +2 -0
  234. package/dist/{supabase-hook-CSLQ4zJ3.js → supabase-hook-Cva3ikXz.js} +731 -782
  235. package/dist/{supautils-Cn4R4cJ4.js → supautils-B6HYL9eL.js} +2 -2
  236. package/dist/svelte/Protected.svelte +9 -2
  237. package/dist/svelte/index.d.ts +6 -6
  238. package/dist/svelte/index.js +25 -11
  239. package/dist/svelte/runtime.d.ts +10 -3
  240. package/dist/svelte/runtime.js +4 -3
  241. package/dist/svelte/source.d.ts +6 -6
  242. package/dist/svelte/source.js +4 -4
  243. package/dist/{validity-uOp8UpYn.js → tenancy-DshjqY0n.js} +3 -39
  244. package/dist/terminal/index.d.ts +5 -5
  245. package/dist/terminal/index.js +18 -34
  246. package/dist/testing/index.d.ts +89 -77
  247. package/dist/testing/index.js +1276 -1262
  248. package/dist/testing/saas/index.d.ts +4 -4
  249. package/dist/testing/saas/index.js +2 -2
  250. package/dist/testing/saas/permissions.d.ts +1 -1
  251. package/dist/testing/saas/permissions.js +1 -1
  252. package/dist/thenable-QSmvt-vZ.js +65 -0
  253. package/dist/{token-CIWxJOCh.js → token-Bx9qy30p.js} +1 -1
  254. package/dist/tool-refusal-CQ7OEia9.js +41 -0
  255. package/dist/trpc/index.d.ts +5 -5
  256. package/dist/trpc/index.js +7 -8
  257. package/dist/{types-DbjRN55U.d.ts → types-7rJVLWe5.d.ts} +82 -4
  258. package/dist/{types-aqOHTHcp.d.ts → types-Br9IXUBG.d.ts} +1 -1
  259. package/dist/types-ByUo56un.d.ts +88 -0
  260. package/dist/{types-Cq_H8N_X.d.ts → types-C0dee0Qf.d.ts} +2 -2
  261. package/dist/{types-DxiCGmmd.d.ts → types-CIlTojhq.d.ts} +2 -2
  262. package/dist/{types-QCuYJh4q.d.ts → types-CUcFZfpU.d.ts} +1 -1
  263. package/dist/{types-C97IskBw.d.ts → types-CqrjB9o6.d.ts} +1 -1
  264. package/dist/{types-BnIEz1sU.d.ts → types-DHrmBJLH.d.ts} +24 -6
  265. package/dist/{types-Dnemwd77.d.ts → types-DpWfQdSh.d.ts} +5 -1
  266. package/dist/{types-Bvc4oWT6.d.ts → types-DvupZE0w.d.ts} +1 -1
  267. package/dist/unplugin/index.d.ts +1 -1
  268. package/dist/unplugin/index.js +1 -1
  269. package/dist/{usage-GTixvDDU.js → usage-4G6t643F.js} +2 -2
  270. package/dist/{usage-ItS7nLcl.js → usage-CG9nOYZw.js} +7 -7
  271. package/dist/usage-iFLJ0-rr.js +2 -0
  272. package/dist/{validation-error-Ba264OaJ.d.ts → validation-error-BSSw0l_8.d.ts} +1 -1
  273. package/dist/{validation-error-CnjZ4t92.js → validation-error-ZfUHbKvf.js} +1 -1
  274. package/dist/validity-dVrUEPJt.js +62 -0
  275. package/dist/{verifier-BxBo6I44.js → verifier-DMr_Zmb_.js} +51 -15
  276. package/dist/{rls-verify-CVQTHTh5.js → verify-BztUB3GX.js} +668 -239
  277. package/dist/{version-BvacVTZR.js → version-BEogC-rB.js} +1 -1
  278. package/dist/{views-Soe7bGSC.js → views-CJsrBS1f.js} +23 -1
  279. package/dist/{vocabulary-DA8a8zif.d.ts → vocabulary-hhEFLFoL.d.ts} +15 -6
  280. package/dist/vue/index.d.ts +24 -19
  281. package/dist/vue/index.js +24 -8
  282. package/dist/watch-C8TpsAl4.js +2 -0
  283. package/dist/{watch-DVBJ9cBq.js → watch-zeucB7eF.js} +6 -6
  284. package/dist/webmcp/index.d.ts +2 -2
  285. package/dist/webmcp/index.js +13 -37
  286. package/dist/wrap-Cr93qqdP.d.ts +14 -0
  287. package/dist/wrap-DWsU_5Bd.js +26 -0
  288. package/package.json +8 -8
  289. package/schemas/catalog-v1.json +6 -0
  290. package/skills/permdock-credentials/SKILL.md +1 -1
  291. package/skills/permdock-credentials/references/tokens-keys-links.md +7 -2
  292. package/skills/permdock-data/references/queries-and-rls.md +4 -0
  293. package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +1 -0
  294. package/skills/permdock-wire/SKILL.md +2 -2
  295. package/skills/permdock-wire/references/adapters.md +9 -1
  296. package/skills/permdock-wire/references/policy-options.md +10 -1
  297. package/dist/catalog-CRW5Mx_B.js +0 -2
  298. package/dist/collect-DSWOk19R.js +0 -2
  299. package/dist/create-CMwIl-w2.js +0 -253
  300. package/dist/delegation-D8pUEuce.js +0 -103
  301. package/dist/doctor-collect-CHk6XWOA.js +0 -665
  302. package/dist/doctor-igIiM_Hl.js +0 -1194
  303. package/dist/evaluation-items-CfCCejQd.js +0 -40
  304. package/dist/fields-BMl1RxoX.js +0 -26
  305. package/dist/grantee-Cv5Sogfv.js +0 -602
  306. package/dist/hosted-DJQw46hn.js +0 -266
  307. package/dist/instance-options-Dhhcnm3y.js +0 -22
  308. package/dist/load-DwJUafYN.js +0 -2
  309. package/dist/map-AcxK9o_D.js +0 -147
  310. package/dist/permissions-DVoF6dMt.js +0 -662
  311. package/dist/powersync-e_AZrKbg.js +0 -2
  312. package/dist/protected-xAr24FEC.d.ts +0 -40
  313. package/dist/relations-cjOL01hZ.js +0 -505
  314. package/dist/rls-graph-CHwx0V8q.js +0 -435
  315. package/dist/rls-shared-Cewd_-zI.js +0 -495
  316. package/dist/store-jOBYpiXC.d.ts +0 -36
  317. package/dist/stores-C0IMQrGI.d.ts +0 -52
  318. package/dist/supabase-config-BpPXt3x1.js +0 -472
  319. package/dist/supabase-hook-6sYLXg_u.js +0 -2
  320. package/dist/thenable-okc2xDsj.js +0 -10
  321. package/dist/types-CKNSyIf_.d.ts +0 -52
  322. package/dist/usage-DorAJiU_.js +0 -2
  323. package/dist/watch-BwxLTYMZ.js +0 -2
@@ -1,27 +1,31 @@
1
- import { n as isReadonlyArray, r as sole, t as compact } from "./compact-y6vfu2_f.js";
1
+ import { a as sole, n as isReadonlyArray, r as isRecord, t as compact } from "./compact-CAf557ZB.js";
2
2
  import { n as freezeDeep, t as freezeCopy } from "./freeze-CrU8nQeX.js";
3
- import { E as ownGet, T as isForbiddenKey, _ as listPermissions, a as formerKeys, c as getResource, d as isEdgeRelation, f as isFieldRelation, i as findPermission, m as isPrincipalRelation, o as formerScopes, r as expandRelation } from "./permissions-DVoF6dMt.js";
4
- import { a as hasConditionOp } from "./refs-KYb4ntaz.js";
5
- import { d as normalizeApproval, f as normalizeAssurance, g as applyQuota, i as declaredRoleNames$1, l as levelCondition, m as role, n as assertApprovalRelations, s as grantList, u as levelNames, v as normalizeWhere } from "./policy-xyWg7QdR.js";
6
- import { n as PermDockValidationError } from "./validation-error-CnjZ4t92.js";
7
- import { a as wireDenials } from "./problem-details-_AMOYz-w.js";
8
- import { a as deniedMessage, i as approvalMessage, n as PermDockDeniedError, t as PermDockApprovalRequiredError } from "./errors-Cz5zn0mW.js";
3
+ import { n as isThenable, o as isForbiddenKey, s as ownGet, t as ignoreRejection } from "./thenable-QSmvt-vZ.js";
4
+ import { a as hasConditionOp } from "./refs-f3imMjNW.js";
5
+ import { d as assertApprovalRelations, l as applyQuota, m as normalizeWhere, n as role, p as normalizeApproval, t as normalizeAssurance } from "./role-builder-B4GbKM-i.js";
6
+ import { n as PermDockValidationError } from "./validation-error-ZfUHbKvf.js";
7
+ import { a as wireDenials } from "./problem-details-DECUnXAn.js";
8
+ import { a as deniedMessage, i as approvalMessage, n as PermDockDeniedError, t as PermDockApprovalRequiredError } from "./errors-drnfxYtm.js";
9
9
  import { n as sameId } from "./ids-DC4V4CFu.js";
10
- import { a as normalizeMemberships, c as rootScope, d as scopeList, f as subjectForPermission, i as keepsPermission, l as scopeChain, m as tenantOf, o as resolveScope, s as rootMembershipId, u as scopeIdOf } from "./scopes-CVdPrWN-.js";
11
- import { a as whereFromGrants, c as rowIdOf, d as isArazzoSimulateInput, f as simulateArazzo, i as signSnapshot, l as rowValues, p as evaluateCondition, r as snapshotGrant, s as isRowPair, t as buildSnapshot } from "./snapshot-Enxd0n-s.js";
10
+ import { i as readAppData } from "./app-data-CV8pqqMi.js";
11
+ import { a as normalizeMemberships, c as rootScope, d as scopeList, f as subjectForPermission, i as keepsPermission, l as scopeChain, m as tenantOf, o as resolveScope, s as rootMembershipId, u as scopeIdOf } from "./scopes-tvpa34ep.js";
12
+ import { _ as evaluateCondition, a as whereFromGrants, c as coversRequirements, d as isRowPair, f as rowIdOf, g as simulateArazzo, h as isArazzoSimulateInput, i as signSnapshot, l as delegatedPermissions, p as rowValues, r as snapshotGrant, s as coveredByDelegation, t as buildSnapshot, u as requiresStandIn } from "./snapshot-Dc9CHPME.js";
12
13
  import { t as parseDuration } from "./duration-xK2jGFVc.js";
13
- import { D as synthesiseRole, E as listRoles, S as findRole, _ as resourceRoleCondition, a as authenticated, d as matchGrantee, i as assurance, m as relationCondition, o as combineWhere, r as asGrantee, s as flattenGrantee, w as isRole } from "./grantee-Cv5Sogfv.js";
14
- import { t as canonicalJson } from "./canonical-json-DemiK4Vp.js";
15
- import { n as pickVisible, r as sanitizeContext, t as grantCoversField } from "./fields-BMl1RxoX.js";
16
- import { a as isMembershipExpired, d as partitionsOf, f as relatesTo, g as tenantsOf, h as scopeField, i as inTeam, o as matchScopedMembership, p as resolveActiveTenant, r as heldRoleNamesIn, t as isActive, u as nowSeconds } from "./validity-uOp8UpYn.js";
14
+ import { a as getResource, c as listPermissions, g as formerScopes, h as formerKeys, n as findPermission } from "./permission-registry-BsblpB-z.js";
15
+ import { a as expandRelation, c as isEdgeRelation, l as isFieldRelation, u as isPrincipalRelation } from "./resource-relations-D1Un3pcz.js";
16
+ import { a as authenticated, b as synthesiseRole, g as isRole, i as assurance, m as findRole, o as flattenGrantee, r as asGrantee, v as listRoles, x as treeXSchema, y as sourceRoleMeta } from "./grantee-CXY-0C87.js";
17
+ import { a as sanitizeContext, i as pickVisible, r as grantCoversField, t as isActive } from "./validity-dVrUEPJt.js";
18
+ import { t as canonicalJson } from "./canonical-json-CFKyFOke.js";
19
+ import { o as resourceRoleCondition, r as relationCondition } from "./grantee-conditions-MQs5NIuX.js";
20
+ import { c as nowSeconds, d as resolveActiveTenant, i as matchScopedMembership, l as partitionsOf, m as tenantsOf, n as inTeam, p as scopeField, r as isMembershipExpired, t as heldRoleNamesIn, u as relatesTo } from "./tenancy-DshjqY0n.js";
21
+ import { a as declaredRoleNames, c as levelCondition, f as policyX, l as levelNames, r as mergeHostedGrants, s as grantList, t as isPortableCondition } from "./hosted-CBHgkzDl.js";
17
22
  import { t as requiresApproval } from "./approval-required-TzNB9oQA.js";
18
- import { r as mergeHostedGrants, t as isPortableCondition } from "./hosted-DJQw46hn.js";
19
- import { n as isThenable, t as ignoreRejection } from "./thenable-okc2xDsj.js";
20
23
  import { t as byCodePoint } from "./compare-pfyVnBjE.js";
21
- import { a as relationWalk, i as relationReader, n as pendingRelations, o as resolveRelated, r as relationId } from "./relations-cjOL01hZ.js";
22
- import { i as requiresStandIn, n as coversRequirements, r as delegatedPermissions, t as coveredByDelegation } from "./delegation-D8pUEuce.js";
24
+ import { n as relationId, r as relationReader, t as pendingRelations } from "./relations-CvmSpG67.js";
25
+ import { n as matchGrantee, t as combineWhere } from "./grantee-match-fxYp9Nmt.js";
23
26
  import { t as resourceIdOf } from "./resource-ref-CaS9yVPi.js";
24
- import { n as payloadDigest, r as versionOf, t as decisionToken } from "./token-CIWxJOCh.js";
27
+ import { n as payloadDigest, r as versionOf, t as decisionToken } from "./token-Bx9qy30p.js";
28
+ import { n as resolveRelated, t as relationWalk } from "./relation-walk-BXnlLtg2.js";
25
29
  import { i as isSubject, r as isPrincipal, t as anonymousSubject } from "./subject-C6dLxqHU.js";
26
30
  //#region src/core/approval-policies.ts
27
31
  /** The denial detail when an `ApprovalPolicySource` fails: every call an allow would grant denies with reason `approval`. */
@@ -32,9 +36,6 @@ function memoryApprovalPolicies(policies) {
32
36
  return entries.filter((entry) => entry.tenant === void 0 || query.tenants.includes(entry.tenant));
33
37
  } });
34
38
  }
35
- function isRecord(value) {
36
- return value !== null && typeof value === "object" && !Array.isArray(value);
37
- }
38
39
  function isStringList(value) {
39
40
  return Array.isArray(value) && value.every((item) => typeof item === "string");
40
41
  }
@@ -458,7 +459,7 @@ function resolveCustomRole(policy, role) {
458
459
  const ceiling = ceilingGrants(policy, scope);
459
460
  const ceilingSet = new Set(ceiling);
460
461
  const ceilingKeys = new Set(ceiling.map((grant) => grant.permission.key));
461
- const declared = declaredRoleNames$1(policy);
462
+ const declared = declaredRoleNames(policy);
462
463
  const all = grantList(policy).filter((grant) => grant.hosted === void 0);
463
464
  const dropped = [];
464
465
  const renamed = [];
@@ -607,7 +608,7 @@ function wellFormed(role) {
607
608
  }
608
609
  /** Every custom role's resolved grants, once per instance. Malformed roles resolve to nothing. */
609
610
  function customGrantsFor(policy, roles) {
610
- const declared = declaredRoleNames$1(policy);
611
+ const declared = declaredRoleNames(policy);
611
612
  const out = [];
612
613
  for (const role of roles) {
613
614
  if (!wellFormed(role) || declared.has(role.name)) continue;
@@ -620,7 +621,7 @@ function customGrantsFor(policy, roles) {
620
621
  }
621
622
  /** The well-formed tenant (not platform) custom roles; a declared role name is never one. */
622
623
  function tenantCustomRoles(policy, roles) {
623
- const declared = declaredRoleNames$1(policy);
624
+ const declared = declaredRoleNames(policy);
624
625
  return roles.filter((role) => wellFormed(role) && !isGlobalCustomRole(role) && !declared.has(role.name));
625
626
  }
626
627
  /** A membership at the role's scope, inside its tenant (and instance, when pinned), naming it. */
@@ -893,6 +894,63 @@ function activate(policy, subject, input, now = nowSeconds()) {
893
894
  });
894
895
  }
895
896
  //#endregion
897
+ //#region src/core/evaluation-alternatives.ts
898
+ function alternativesFor(evaluate, policy, permission, subject, now, env) {
899
+ return listPermissions(policy.permissions).filter((leaf) => leaf.resource === permission.resource && leaf.key !== permission.key).filter((leaf) => {
900
+ return evaluate(policy, subject, leaf, void 0, {
901
+ trusted: true,
902
+ source: "simulate",
903
+ now
904
+ }, {
905
+ ...env,
906
+ emit: false,
907
+ skipAlternatives: true,
908
+ outcomeOnly: true
909
+ }).outcome === "granted";
910
+ });
911
+ }
912
+ function inheritedVerdict(evaluate, policy, subject, resource, id, key, now, env) {
913
+ const leaf = findPermission(policy.permissions, key);
914
+ const loaded = env.relations?.row({
915
+ resource,
916
+ id
917
+ });
918
+ if (leaf === void 0 || loaded === void 0) return "relation-unavailable";
919
+ if (loaded === "pending" || loaded === "failed") return "relation-unavailable";
920
+ if (loaded.row === null) return false;
921
+ const decision = evaluate(policy, subject, leaf, loaded.row, {
922
+ trusted: true,
923
+ source: "simulate",
924
+ now
925
+ }, {
926
+ ...env,
927
+ emit: false,
928
+ skipAlternatives: true,
929
+ outcomeOnly: true
930
+ });
931
+ if (decision.outcome === "granted") return true;
932
+ const reasons = decision.outcome === "denied" ? decision.denials.map((denial) => denial.reason) : [];
933
+ return reasons.includes("relation-unavailable") ? "relation-unavailable" : reasons.includes("relation-depth") ? "relation-depth" : false;
934
+ }
935
+ //#endregion
936
+ //#region src/core/evaluation-trace.ts
937
+ function skip(tracer, grant, permissionKey, why) {
938
+ tracer?.skipped.push({
939
+ role: grant.role,
940
+ permission: permissionKey,
941
+ effect: grant.effect,
942
+ why
943
+ });
944
+ }
945
+ function traceOf(tracer) {
946
+ return {
947
+ evaluated: tracer.evaluated,
948
+ allows: tracer.allows,
949
+ denies: tracer.denies,
950
+ skipped: tracer.skipped
951
+ };
952
+ }
953
+ //#endregion
896
954
  //#region src/core/events.ts
897
955
  function emitSafe(listeners, payload, errors) {
898
956
  for (const listener of listeners) try {
@@ -958,15 +1016,20 @@ function finish(policy, subject, permission, data, decision, options, env, trust
958
1016
  matched: decision.outcome === "granted" ? compact({
959
1017
  role: decision.matched.role,
960
1018
  permission: decision.matched.permission,
1019
+ name: decision.matched.name,
1020
+ meta: decision.matched.meta,
961
1021
  to: decision.matched.to,
962
1022
  hosted: decision.matched.hosted,
963
1023
  breakGlass: decision.matched.breakGlass
964
1024
  }) : decision.outcome === "approval-required" ? compact({
965
1025
  role: decision.grant.role,
966
1026
  permission: decision.grant.permission,
1027
+ name: decision.grant.name,
1028
+ meta: decision.grant.meta,
967
1029
  to: decision.grant.to,
968
1030
  hosted: decision.grant.hosted
969
1031
  }) : void 0,
1032
+ obligations: decision.outcome === "granted" && (decision.obligations?.length ?? 0) > 0 ? decision.obligations : void 0,
970
1033
  purpose: Array.isArray(subject.context["purpose"]) && subject.context["purpose"].length > 0 ? subject.context["purpose"] : void 0,
971
1034
  reason: typeof subject.context["reason"] === "string" && subject.context["reason"] !== "" ? subject.context["reason"] : void 0,
972
1035
  denials: decision.outcome === "denied" ? wireDenials(decision.denials) : void 0,
@@ -990,59 +1053,132 @@ function finish(policy, subject, permission, data, decision, options, env, trust
990
1053
  }
991
1054
  }
992
1055
  //#endregion
993
- //#region src/core/validation.ts
994
- function validateBoundary(permission, resource, data, mode, trusted, boundary = "manual") {
995
- if (!(mode === "always" || mode === "boundary" && !trusted)) return data;
996
- if (resource?.schema === void 0) {
997
- if (mode === "always") throw new PermDockValidationError({
998
- code: "no-schema",
999
- permission: permission.key,
1000
- resource: permission.resource,
1001
- boundary,
1002
- message: `${permission.key}: no schema for resource ${permission.resource}.`
1056
+ //#region src/core/grant-conditions.ts
1057
+ function evaluateGrantCondition(grant, permission, current, next, subject, now, scopes, relations, permitted) {
1058
+ let unknown;
1059
+ const onOpaque = () => {
1060
+ unknown ??= "opaque-condition";
1061
+ };
1062
+ const related = (condition, row) => {
1063
+ if (relations === void 0) {
1064
+ unknown ??= "relation-unavailable";
1065
+ return false;
1066
+ }
1067
+ const verdict = resolveRelated(condition, row, subject, now, relations, permitted);
1068
+ if (verdict === "relation-depth" || verdict === "relation-unavailable") {
1069
+ unknown ??= verdict;
1070
+ return false;
1071
+ }
1072
+ return verdict;
1073
+ };
1074
+ if (!grant.portable && grant.closure !== void 0) try {
1075
+ const result = grant.closure(next ?? current, {
1076
+ subject,
1077
+ actor: subject.actor,
1078
+ delegation: subject.delegation,
1079
+ context: subject.context
1003
1080
  });
1004
- return data;
1081
+ if (isThenable(result)) return {
1082
+ matched: false,
1083
+ reason: "closure-error",
1084
+ cause: result
1085
+ };
1086
+ return { matched: result === true };
1087
+ } catch (error) {
1088
+ return {
1089
+ matched: false,
1090
+ reason: "closure-error",
1091
+ cause: error
1092
+ };
1005
1093
  }
1006
- const result = resource.schema["~standard"].validate(data);
1007
- if (isThenable(result)) {
1008
- ignoreRejection(result);
1009
- throw new PermDockValidationError({
1010
- code: "async-schema",
1011
- permission: permission.key,
1012
- resource: permission.resource,
1013
- boundary,
1014
- message: `${permission.key}: schema for ${permission.resource} is async.`
1015
- });
1094
+ if (grant.where !== void 0) {
1095
+ if (permission.kind === "collection") return {
1096
+ matched: false,
1097
+ reason: "condition"
1098
+ };
1099
+ if (current === void 0) return {
1100
+ matched: false,
1101
+ reason: "condition"
1102
+ };
1103
+ if (grant.where.op === "opaque" || grant.check?.op === "opaque") return {
1104
+ matched: false,
1105
+ reason: "opaque-condition"
1106
+ };
1107
+ const matched = evaluateCondition(grant.where, current, subject, now, scopes, related, onOpaque);
1108
+ if (unknown !== void 0) return {
1109
+ matched: false,
1110
+ reason: unknown
1111
+ };
1112
+ if (!matched) return {
1113
+ matched: false,
1114
+ reason: "condition"
1115
+ };
1016
1116
  }
1017
- if ("issues" in result && result.issues !== void 0) throw new PermDockValidationError({
1018
- code: "invalid-data",
1019
- permission: permission.key,
1020
- resource: permission.resource,
1021
- issues: result.issues,
1022
- boundary,
1023
- message: validationMessage(permission, resource, result.issues)
1024
- });
1025
- return result.value;
1117
+ const checkCondition = grant.check ?? (permission.action === "update" ? grant.where : void 0);
1118
+ if (checkCondition !== void 0) {
1119
+ if (checkCondition.op === "opaque") return {
1120
+ matched: false,
1121
+ reason: "opaque-condition"
1122
+ };
1123
+ if (next === void 0) return {
1124
+ matched: false,
1125
+ reason: "condition"
1126
+ };
1127
+ const matched = evaluateCondition(checkCondition, next, subject, now, scopes, related, onOpaque);
1128
+ if (unknown !== void 0) return {
1129
+ matched: false,
1130
+ reason: unknown
1131
+ };
1132
+ if (!matched) return {
1133
+ matched: false,
1134
+ reason: "condition"
1135
+ };
1136
+ }
1137
+ return { matched: true };
1026
1138
  }
1027
- function validationMessage(permission, resource, issues) {
1028
- const parts = issues.map((issue) => {
1029
- const path = issue.path?.map((item) => typeof item === "object" && "key" in item ? String(item.key) : String(item)).join(".") ?? "/";
1030
- return `invalid ${resource.name} data at ${path}: ${issue.message}`;
1139
+ /** A grant whose condition could not be answered; a deny with one denies. */
1140
+ function isUnevaluable(reason) {
1141
+ return reason === "relation-depth" || reason === "relation-unavailable" || reason === "closure-error" || reason === "opaque-condition";
1142
+ }
1143
+ function shouldConsumeQuota(source, simulated) {
1144
+ if (simulated) return false;
1145
+ switch (source) {
1146
+ case "can":
1147
+ case "filter":
1148
+ case "simulate":
1149
+ case "explain": return false;
1150
+ case "decide":
1151
+ case "assert":
1152
+ case "endpoint":
1153
+ case "adapter":
1154
+ case "approval":
1155
+ case void 0: return true;
1156
+ default: return source;
1157
+ }
1158
+ }
1159
+ function isDelegatedPermission(policy, permission) {
1160
+ const providers = policy.providers;
1161
+ if (providers === void 0 || providers.length === 0) return false;
1162
+ for (const provider of providers) if (provider.handles(permission)) return true;
1163
+ return false;
1164
+ }
1165
+ /** The `MatchedGrant` view of a grant, shared by `matched` and the trace. */
1166
+ function matchedOf(grant, permissionKey, breakGlass) {
1167
+ return compact({
1168
+ role: grant.role,
1169
+ permission: permissionKey,
1170
+ name: grant.name,
1171
+ meta: grant.meta,
1172
+ to: grant.to,
1173
+ where: grant.where,
1174
+ check: grant.check,
1175
+ approval: grant.approval,
1176
+ hosted: grant.hosted,
1177
+ breakGlass
1031
1178
  });
1032
- return `${permission.key}: ${parts.join("; ")}.`;
1033
1179
  }
1034
1180
  //#endregion
1035
- //#region src/core/evaluate.ts
1036
- /**
1037
- * A collection write proposes `next`; an update pair may move `next` out of
1038
- * the scope `current` is in. Both must stay in scope.
1039
- */
1040
- function matchWriteScope(match, instance, current, next) {
1041
- const first = match(instance ? current : next);
1042
- if (!first.ok || !instance || next === current) return first;
1043
- const moved = match(next);
1044
- return moved.ok ? first : moved;
1045
- }
1181
+ //#region src/core/role-loading.ts
1046
1182
  function isGlobalRole(value) {
1047
1183
  return value !== null && typeof value === "object" && Reflect.get(value, "scope") === "global";
1048
1184
  }
@@ -1136,13 +1272,12 @@ function expandRoleNames(names, declared, custom, tenant) {
1136
1272
  unknown
1137
1273
  };
1138
1274
  }
1139
- function declaredRoleNames(policy) {
1140
- return policy.index.declaredRoles;
1141
- }
1142
- const NEVER = {
1275
+ //#endregion
1276
+ //#region src/core/requires-conditions.ts
1277
+ const NEVER = freezeDeep({
1143
1278
  op: "or",
1144
1279
  conditions: []
1145
- };
1280
+ });
1146
1281
  function isUnconditionedAllow(grant, now) {
1147
1282
  return grant.where === void 0 && grant.check === void 0 && grant.closure === void 0 && grant.portable && grant.purpose === void 0 && grant.viaOnly === void 0 && grant.requires === void 0 && isActive(grant.validity, now);
1148
1283
  }
@@ -1220,277 +1355,165 @@ function keyRequirement(policy, subject, key, resource, now, customRoles, custom
1220
1355
  conditions: parts
1221
1356
  };
1222
1357
  }
1223
- function alternativesFor(policy, permission, subject, now, env) {
1224
- return listPermissions(policy.permissions).filter((leaf) => leaf.resource === permission.resource && leaf.key !== permission.key).filter((leaf) => {
1225
- return evaluate(policy, subject, leaf, void 0, {
1226
- trusted: true,
1227
- source: "simulate",
1228
- now
1229
- }, {
1230
- ...env,
1231
- emit: false,
1232
- skipAlternatives: true,
1233
- outcomeOnly: true
1234
- }).outcome === "granted";
1235
- });
1236
- }
1237
- function inheritedVerdict(policy, subject, resource, id, key, now, env) {
1238
- const leaf = findPermission(policy.permissions, key);
1239
- const loaded = env.relations?.row({
1240
- resource,
1241
- id
1242
- });
1243
- if (leaf === void 0 || loaded === void 0) return "relation-unavailable";
1244
- if (loaded === "pending" || loaded === "failed") return "relation-unavailable";
1245
- if (loaded.row === null) return false;
1246
- const decision = evaluate(policy, subject, leaf, loaded.row, {
1247
- trusted: true,
1248
- source: "simulate",
1249
- now
1250
- }, {
1251
- ...env,
1252
- emit: false,
1253
- skipAlternatives: true,
1254
- outcomeOnly: true
1255
- });
1256
- if (decision.outcome === "granted") return true;
1257
- const reasons = decision.outcome === "denied" ? decision.denials.map((denial) => denial.reason) : [];
1258
- return reasons.includes("relation-unavailable") ? "relation-unavailable" : reasons.includes("relation-depth") ? "relation-depth" : false;
1259
- }
1260
- function evaluateGrantCondition(grant, permission, current, next, subject, now, scopes, relations, permitted) {
1261
- let unknown;
1262
- const onOpaque = () => {
1263
- unknown ??= "opaque-condition";
1264
- };
1265
- const related = (condition, row) => {
1266
- if (relations === void 0) {
1267
- unknown ??= "relation-unavailable";
1268
- return false;
1269
- }
1270
- const verdict = resolveRelated(condition, row, subject, now, relations, permitted);
1271
- if (verdict === "relation-depth" || verdict === "relation-unavailable") {
1272
- unknown ??= verdict;
1273
- return false;
1274
- }
1275
- return verdict;
1276
- };
1277
- if (!grant.portable && grant.closure !== void 0) try {
1278
- const result = grant.closure(next ?? current, {
1279
- subject,
1280
- actor: subject.actor,
1281
- delegation: subject.delegation,
1282
- context: subject.context
1358
+ //#endregion
1359
+ //#region src/core/validation.ts
1360
+ function validateBoundary(permission, resource, data, mode, trusted, boundary = "manual") {
1361
+ if (!(mode === "always" || mode === "boundary" && !trusted)) return data;
1362
+ if (resource?.schema === void 0) {
1363
+ if (mode === "always") throw new PermDockValidationError({
1364
+ code: "no-schema",
1365
+ permission: permission.key,
1366
+ resource: permission.resource,
1367
+ boundary,
1368
+ message: `${permission.key}: no schema for resource ${permission.resource}.`
1283
1369
  });
1284
- if (isThenable(result)) return {
1285
- matched: false,
1286
- reason: "closure-error",
1287
- cause: result
1288
- };
1289
- return { matched: result === true };
1290
- } catch (error) {
1291
- return {
1292
- matched: false,
1293
- reason: "closure-error",
1294
- cause: error
1295
- };
1296
- }
1297
- if (grant.where !== void 0) {
1298
- if (permission.kind === "collection") return {
1299
- matched: false,
1300
- reason: "condition"
1301
- };
1302
- if (current === void 0) return {
1303
- matched: false,
1304
- reason: "condition"
1305
- };
1306
- if (grant.where.op === "opaque" || grant.check?.op === "opaque") return {
1307
- matched: false,
1308
- reason: "opaque-condition"
1309
- };
1310
- const matched = evaluateCondition(grant.where, current, subject, now, scopes, related, onOpaque);
1311
- if (unknown !== void 0) return {
1312
- matched: false,
1313
- reason: unknown
1314
- };
1315
- if (!matched) return {
1316
- matched: false,
1317
- reason: "condition"
1318
- };
1370
+ return data;
1319
1371
  }
1320
- const checkCondition = grant.check ?? (permission.action === "update" ? grant.where : void 0);
1321
- if (checkCondition !== void 0) {
1322
- if (checkCondition.op === "opaque") return {
1323
- matched: false,
1324
- reason: "opaque-condition"
1325
- };
1326
- if (next === void 0) return {
1327
- matched: false,
1328
- reason: "condition"
1329
- };
1330
- const matched = evaluateCondition(checkCondition, next, subject, now, scopes, related, onOpaque);
1331
- if (unknown !== void 0) return {
1332
- matched: false,
1333
- reason: unknown
1334
- };
1335
- if (!matched) return {
1336
- matched: false,
1337
- reason: "condition"
1338
- };
1372
+ const result = resource.schema["~standard"].validate(data);
1373
+ if (isThenable(result)) {
1374
+ ignoreRejection(result);
1375
+ throw new PermDockValidationError({
1376
+ code: "async-schema",
1377
+ permission: permission.key,
1378
+ resource: permission.resource,
1379
+ boundary,
1380
+ message: `${permission.key}: schema for ${permission.resource} is async.`
1381
+ });
1339
1382
  }
1340
- return { matched: true };
1341
- }
1342
- /** A grant whose condition could not be answered; a deny with one denies. */
1343
- function isUnevaluable(reason) {
1344
- return reason === "relation-depth" || reason === "relation-unavailable" || reason === "closure-error" || reason === "opaque-condition";
1383
+ if ("issues" in result && result.issues !== void 0) throw new PermDockValidationError({
1384
+ code: "invalid-data",
1385
+ permission: permission.key,
1386
+ resource: permission.resource,
1387
+ issues: result.issues,
1388
+ boundary,
1389
+ message: validationMessage(permission, resource, result.issues)
1390
+ });
1391
+ return result.value;
1345
1392
  }
1346
- function shouldConsumeQuota(source, simulated) {
1347
- if (simulated) return false;
1348
- switch (source) {
1349
- case "can":
1350
- case "filter":
1351
- case "simulate":
1352
- case "explain": return false;
1353
- case "decide":
1354
- case "assert":
1355
- case "endpoint":
1356
- case "adapter":
1357
- case "approval":
1358
- case void 0: return true;
1359
- default: return source;
1360
- }
1393
+ function validationMessage(permission, resource, issues) {
1394
+ const parts = issues.map((issue) => {
1395
+ const path = issue.path?.map((item) => typeof item === "object" && "key" in item ? String(item.key) : String(item)).join(".") ?? "/";
1396
+ return `invalid ${resource.name} data at ${path}: ${issue.message}`;
1397
+ });
1398
+ return `${permission.key}: ${parts.join("; ")}.`;
1361
1399
  }
1362
- function isDelegatedPermission(policy, permission) {
1363
- const providers = policy.providers;
1364
- if (providers === void 0 || providers.length === 0) return false;
1365
- for (const provider of providers) if (provider.handles(permission)) return true;
1366
- return false;
1400
+ //#endregion
1401
+ //#region src/core/evaluate.ts
1402
+ /**
1403
+ * A collection write proposes `next`; an update pair may move `next` out of
1404
+ * the scope `current` is in. Both must stay in scope.
1405
+ */
1406
+ function matchWriteScope(match, instance, current, next) {
1407
+ const first = match(instance ? current : next);
1408
+ if (!first.ok || !instance || next === current) return first;
1409
+ const moved = match(next);
1410
+ return moved.ok ? first : moved;
1367
1411
  }
1368
- /** The `MatchedGrant` view of a grant, shared by `matched` and the trace. */
1369
- function matchedOf(grant, permissionKey, breakGlass) {
1370
- return compact({
1371
- role: grant.role,
1372
- permission: permissionKey,
1373
- name: grant.name,
1374
- to: grant.to,
1375
- where: grant.where,
1376
- check: grant.check,
1377
- approval: grant.approval,
1378
- hosted: grant.hosted,
1379
- breakGlass
1412
+ function complete(call, raw, membership) {
1413
+ const { env, permission, tracer } = call;
1414
+ const decision = env.outcomeOnly !== true && raw.outcome === "denied" && raw.permission === void 0 ? {
1415
+ ...raw,
1416
+ permission: permission.key
1417
+ } : raw;
1418
+ const final = env.outcomeOnly === true ? decision : freezeDeep(tracer === void 0 ? decision : {
1419
+ ...decision,
1420
+ trace: traceOf(tracer)
1380
1421
  });
1422
+ finish(call.policy, call.given, permission, call.current, final, call.options, env, call.options.trusted === true, membership);
1423
+ return final;
1381
1424
  }
1382
- function skip(tracer, grant, permissionKey, why) {
1383
- tracer?.skipped.push({
1384
- role: grant.role,
1385
- permission: permissionKey,
1386
- effect: grant.effect,
1387
- why
1425
+ function deniedNow(call, denial) {
1426
+ return complete(call, {
1427
+ outcome: "denied",
1428
+ denials: [denial],
1429
+ alternatives: []
1388
1430
  });
1389
1431
  }
1390
- function traceOf(tracer) {
1391
- return {
1392
- evaluated: tracer.evaluated,
1393
- allows: tracer.allows,
1394
- denies: tracer.denies,
1395
- skipped: tracer.skipped
1396
- };
1432
+ function deniedWithAlternatives(state, denials) {
1433
+ return complete(state, {
1434
+ outcome: "denied",
1435
+ denials,
1436
+ alternatives: state.env.skipAlternatives ? [] : alternativesFor(evaluate, state.policy, state.permission, state.subject, state.now, state.env)
1437
+ });
1397
1438
  }
1398
- function evaluate(policy, given, permission, data, options, env) {
1399
- const subject = subjectForPermission(given, permission.key);
1400
- const now = options.now ?? nowSeconds();
1401
- const trusted = options.trusted === true;
1402
- const resource = getResource(policy.permissions, permission.resource);
1403
- let { current, next } = rowValues(permission, data);
1404
- const tracer = options.explain === true ? {
1405
- evaluated: 0,
1406
- allows: [],
1407
- denies: [],
1408
- skipped: []
1409
- } : void 0;
1410
- const complete = (decision, membership) => {
1411
- const final = env.outcomeOnly === true ? decision : freezeDeep(tracer === void 0 ? decision : {
1412
- ...decision,
1413
- trace: traceOf(tracer)
1414
- });
1415
- finish(policy, given, permission, current, final, options, env, trusted, membership);
1416
- return final;
1417
- };
1439
+ /** The rows after boundary validation; on `invalid`, the rows validated before the failure. */
1440
+ function validatedRows(call, data, rows) {
1441
+ const { permission, policy, options } = call;
1442
+ if (data === void 0 && !(permission.kind === "instance" && policy.validate === "always")) return rows;
1443
+ const check = (value) => validateBoundary(permission, call.resource, value, policy.validate, options.trusted === true, options.boundary ?? "manual");
1444
+ let current = rows.current;
1418
1445
  try {
1419
- if (data !== void 0 || permission.kind === "instance" && policy.validate === "always") {
1420
- const validated = validateBoundary(permission, resource, permission.kind === "instance" && isRowPair(data) ? data.current : data, policy.validate, trusted, options.boundary ?? "manual");
1421
- if (permission.kind === "instance" && isRowPair(data)) {
1422
- current = validated;
1423
- next = validateBoundary(permission, resource, data.next, policy.validate, trusted, options.boundary ?? "manual");
1424
- } else if (permission.kind === "instance") {
1425
- current = validated;
1426
- next = validated;
1427
- } else next = validated;
1446
+ if (permission.kind === "instance" && isRowPair(data)) {
1447
+ current = check(data.current);
1448
+ return {
1449
+ current,
1450
+ next: check(data.next)
1451
+ };
1428
1452
  }
1453
+ const validated = check(data);
1454
+ return permission.kind === "instance" ? {
1455
+ current: validated,
1456
+ next: validated
1457
+ } : {
1458
+ current,
1459
+ next: validated
1460
+ };
1429
1461
  } catch (error) {
1430
- if (error instanceof PermDockValidationError && error.code === "invalid-data") return complete({
1431
- outcome: "denied",
1432
- denials: [{
1433
- role: null,
1434
- reason: "validation",
1435
- detail: error
1436
- }],
1437
- alternatives: []
1438
- });
1462
+ if (error instanceof PermDockValidationError && error.code === "invalid-data") return {
1463
+ current,
1464
+ next: rows.next,
1465
+ invalid: error
1466
+ };
1439
1467
  throw error;
1440
1468
  }
1441
- if (isDelegatedPermission(policy, permission)) return complete({
1442
- outcome: "denied",
1443
- denials: [{
1444
- role: null,
1445
- reason: "pdp-unavailable",
1446
- detail: "use permdock/pdp createPermDock"
1447
- }],
1448
- alternatives: []
1449
- });
1450
- const scopes = scopeList(policy.scopes);
1451
- const declared = declaredRoleNames(policy);
1452
- const principalRoles = subject.principal?.roles ?? [];
1453
- const globalNames = expandRoleNames(principalRoles, declared, env.customRoles, subject.principal?.tenant);
1454
- if (subject.principal !== null && globalNames.unknown.length > 0) emitSafe(env.listeners.auth, {
1469
+ }
1470
+ function heldRoles(call, scopes, declared) {
1471
+ const { subject, env } = call;
1472
+ const global = expandRoleNames(subject.principal?.roles ?? [], declared, env.customRoles, subject.principal?.tenant);
1473
+ if (subject.principal !== null && global.unknown.length > 0) emitSafe(env.listeners.auth, {
1455
1474
  reason: "unknown-role",
1456
1475
  source: "roles"
1457
1476
  }, env.listeners);
1477
+ const matching = new Set(global.roles);
1458
1478
  const denials = [];
1459
- const allows = [];
1460
- const matchingRoles = new Set(globalNames.roles);
1461
1479
  for (const membership of subject.principal?.memberships ?? []) {
1462
1480
  if (!inTeam(membership, scopes, env.team)) continue;
1463
1481
  const expanded = expandRoleNames(membership.roles, declared, env.customRoles, tenantOf(membership, scopes));
1464
- for (const name of expanded.roles) matchingRoles.add(name);
1482
+ for (const name of expanded.roles) matching.add(name);
1465
1483
  for (const name of expanded.unknown) denials.push({
1466
1484
  role: name,
1467
1485
  reason: "unknown-role"
1468
1486
  });
1469
1487
  }
1470
- const actorVias = actorRequiredVias(policy.index.supports);
1471
- if (actorVias.size > 0 && subject.actor === void 0 && (subject.principal?.memberships ?? []).some((membership) => isSupportMembership(membership, actorVias) && !isMembershipExpired(membership, now) && inTeam(membership, scopes, env.team))) return complete({
1472
- outcome: "denied",
1473
- denials: [{
1474
- role: null,
1475
- reason: "actor-required"
1476
- }],
1477
- alternatives: []
1478
- });
1479
- const purposes = purposesOf(subject);
1480
- const forPermission = policy.index.grantsByKey.get(permission.key) ?? [];
1481
- const candidates = forPermission.filter((grant) => grant.breakGlass === void 0).map((grant) => ({ grant }));
1482
- for (const item of env.customGrants) if (item.grant.permission.key === permission.key) candidates.push({
1488
+ return {
1489
+ global,
1490
+ matching,
1491
+ denials
1492
+ };
1493
+ }
1494
+ /** A support membership the policy says needs an actor, held without one. */
1495
+ function lacksActor(call, scopes) {
1496
+ const { subject, now, env } = call;
1497
+ const actorVias = actorRequiredVias(call.policy.index.supports);
1498
+ return actorVias.size > 0 && subject.actor === void 0 && (subject.principal?.memberships ?? []).some((membership) => isSupportMembership(membership, actorVias) && !isMembershipExpired(membership, now) && inTeam(membership, scopes, env.team));
1499
+ }
1500
+ function candidatesFor(call) {
1501
+ const { permission } = call;
1502
+ const candidates = (call.policy.index.grantsByKey.get(permission.key) ?? []).filter((grant) => grant.breakGlass === void 0).map((grant) => ({ grant }));
1503
+ for (const item of call.env.customGrants) if (item.grant.permission.key === permission.key) candidates.push({
1483
1504
  grant: item.grant,
1484
1505
  custom: item.role
1485
1506
  });
1486
- let breakGlassGrant;
1487
- let breakGlassObligations = [];
1488
- const breakGlassOverrides = /* @__PURE__ */ new Set();
1489
- let breakGlassDenial;
1490
- for (const grant of forPermission) {
1507
+ return candidates;
1508
+ }
1509
+ function breakGlassFor(call, scopes) {
1510
+ const { permission, subject, now, tracer } = call;
1511
+ const overrides = /* @__PURE__ */ new Set();
1512
+ let denial;
1513
+ for (const grant of call.policy.index.grantsByKey.get(permission.key) ?? []) {
1491
1514
  if (grant.breakGlass === void 0) continue;
1492
1515
  if (tracer !== void 0) tracer.evaluated += 1;
1493
- if (!matchGrantee(grant.to, subject, now, resource, scopes).matched) {
1516
+ if (!matchGrantee(grant.to, subject, now, call.resource, scopes).matched) {
1494
1517
  skip(tracer, grant, permission.key, "grantee");
1495
1518
  continue;
1496
1519
  }
@@ -1499,13 +1522,14 @@ function evaluate(policy, given, permission, data, options, env) {
1499
1522
  skip(tracer, grant, permission.key, "break-glass-inactive");
1500
1523
  continue;
1501
1524
  }
1502
- for (const name of grant.breakGlass.overrides) breakGlassOverrides.add(name);
1503
- if (result.kind === "granted") {
1504
- breakGlassGrant = grant;
1505
- breakGlassObligations = result.obligations;
1506
- break;
1507
- }
1508
- breakGlassDenial ??= result.reason === "insufficient-user-authentication" ? {
1525
+ for (const name of grant.breakGlass.overrides) overrides.add(name);
1526
+ if (result.kind === "granted") return {
1527
+ grant,
1528
+ obligations: result.obligations,
1529
+ overrides,
1530
+ denial
1531
+ };
1532
+ denial ??= result.reason === "insufficient-user-authentication" ? {
1509
1533
  role: null,
1510
1534
  reason: result.reason,
1511
1535
  to: result.to
@@ -1514,241 +1538,231 @@ function evaluate(policy, given, permission, data, options, env) {
1514
1538
  reason: result.reason
1515
1539
  };
1516
1540
  }
1517
- const holdsCustom = (custom) => isGlobalCustomRole(custom) ? holdsGlobalCustomRole(principalRoles, custom) : (subject.principal?.memberships ?? []).some((membership) => inTeam(membership, scopes, env.team) && holdsCustomRole(membership, custom, scopes));
1518
- const walkRole = env.relations?.available === true ? (membership, row) => {
1541
+ return {
1542
+ grant: void 0,
1543
+ obligations: [],
1544
+ overrides,
1545
+ denial
1546
+ };
1547
+ }
1548
+ function walkRoleFor(call) {
1549
+ const { env } = call;
1550
+ if (env.relations?.available !== true) return;
1551
+ return (membership, row) => {
1519
1552
  const on = membership.on;
1520
- const condition = on === void 0 ? void 0 : resourceRoleCondition(resource, on.resource, [on.id], policy.resources);
1521
- return condition === void 0 || env.relations === void 0 ? false : resolveRelated(condition, row, subject, now, env.relations);
1522
- } : void 0;
1523
- for (const { grant, custom } of candidates) {
1524
- const displayRole = grant.role;
1525
- if (tracer !== void 0) tracer.evaluated += 1;
1526
- if (grant.viaOnly !== void 0 && !(subject.principal?.memberships ?? []).some((membership) => membership.via === grant.viaOnly && !isMembershipExpired(membership, now) && inTeam(membership, scopes, env.team))) {
1527
- skip(tracer, grant, permission.key, "via-only");
1528
- continue;
1529
- }
1530
- const required = grant.purpose;
1531
- if (required !== void 0 && !purposes.some((purpose) => required.includes(purpose))) {
1532
- skip(tracer, grant, permission.key, "purpose");
1533
- continue;
1534
- }
1535
- const granteeMatch = matchGrantee(grant.to, subject, now, resource, scopes, policy.resources, grant.effect === "deny");
1536
- if (!granteeMatch.matched) {
1537
- const reason = granteeMatch.reason ?? "no-grant";
1538
- if (reason === "not-entitled" && !flattenGrantee(grant.to).every((item) => item.kind !== "role" || (custom === void 0 ? item.scope === "global" ? globalNames.roles.includes(item.role) : matchingRoles.has(item.role) : holdsCustom(custom)))) {
1539
- skip(tracer, grant, permission.key, "grantee");
1540
- continue;
1541
- }
1542
- denials.push((reason === "insufficient-user-authentication" || reason === "not-entitled") && grant.to !== void 0 ? {
1543
- role: displayRole,
1544
- reason,
1545
- to: grant.to
1546
- } : {
1547
- role: displayRole,
1548
- reason
1549
- });
1550
- continue;
1551
- }
1552
- const roleItems = flattenGrantee(grant.to).filter((item) => item.kind === "role");
1553
- let scopeMembership;
1554
- let roleOk = roleItems.length === 0;
1555
- if (roleItems.length > 0) {
1556
- if (subject.principal === null) {
1557
- denials.push({
1558
- role: displayRole,
1559
- reason: "anonymous"
1560
- });
1561
- continue;
1562
- }
1563
- let allHeld = true;
1564
- for (const roleItem of roleItems) {
1565
- if (!(custom === void 0 ? roleItem.scope === "global" ? globalNames.roles.includes(roleItem.role) : matchingRoles.has(roleItem.role) : holdsCustom(custom))) {
1566
- skip(tracer, grant, permission.key, "role");
1567
- allHeld = false;
1568
- break;
1569
- }
1570
- const matchRow = (row) => matchScopedMembership(subject, roleItem.scope, roleItem.role, row, scopes, resource, policy.resources, now, (membership) => {
1571
- if (custom === void 0) return expandRoleNames(membership.roles, declared, env.customRoles, tenantOf(membership, scopes)).roles;
1572
- return holdsCustomRole(membership, custom, scopes) ? [custom.name] : [];
1573
- }, env.team, walkRole, permission.kind === "instance", options.scope);
1574
- const scopeMatch = matchWriteScope(matchRow, permission.kind === "instance", current, next);
1575
- if (!scopeMatch.ok) {
1576
- denials.push({
1577
- role: roleItem.role,
1578
- reason: scopeMatch.reason
1579
- });
1580
- allHeld = false;
1581
- break;
1582
- }
1583
- scopeMembership = scopeMatch.membership ?? scopeMembership;
1584
- }
1585
- roleOk = allHeld;
1586
- }
1587
- if (!roleOk) continue;
1588
- if (!isActive(grant.validity, now)) {
1589
- if (grant.effect === "allow") denials.push({
1590
- role: displayRole,
1591
- reason: "inactive-grant",
1592
- detail: grant.validity
1593
- });
1594
- else skip(tracer, grant, permission.key, "validity");
1595
- continue;
1596
- }
1597
- const requirement = grant.requires === void 0 || grant.effect !== "allow" ? void 0 : requirementCondition(policy, subject, grant.requires, resource, now, env.customRoles, env.customGrants);
1598
- const merged = freezeDeep(compact({
1599
- ...grant,
1600
- where: combineWhere(combineWhere(grant.where, granteeMatch.where), requirement)
1601
- }));
1602
- const condition = evaluateGrantCondition(merged, permission, current, next, subject, now, scopes, env.relations, (target, id, key) => inheritedVerdict(policy, given, target, id, key, now, env));
1603
- if (!condition.matched && grant.effect === "deny" && isUnevaluable(condition.reason) && grantCoversField(grant.fields, options.field, grant.effect)) {
1604
- if (condition.reason === "closure-error") emitSafe(env.listeners.error, condition.cause ?? /* @__PURE__ */ new Error("closure-error"), env.listeners);
1605
- tracer?.denies.push(matchedOf(merged, permission.key));
1606
- return complete({
1607
- outcome: "denied",
1608
- denials: [{
1609
- role: displayRole,
1610
- reason: condition.reason ?? "relation-unavailable"
1611
- }],
1612
- alternatives: []
1613
- });
1553
+ const condition = on === void 0 ? void 0 : resourceRoleCondition(call.resource, on.resource, [on.id], call.policy.resources);
1554
+ return condition === void 0 || env.relations === void 0 ? false : resolveRelated(condition, row, call.subject, call.now, env.relations);
1555
+ };
1556
+ }
1557
+ function holdsCustom(state, custom) {
1558
+ return isGlobalCustomRole(custom) ? holdsGlobalCustomRole(state.subject.principal?.roles ?? [], custom) : (state.subject.principal?.memberships ?? []).some((membership) => inTeam(membership, state.scopes, state.env.team) && holdsCustomRole(membership, custom, state.scopes));
1559
+ }
1560
+ function holdsRole(state, item, custom) {
1561
+ if (custom !== void 0) return holdsCustom(state, custom);
1562
+ return item.scope === "global" ? state.roles.global.roles.includes(item.role) : state.roles.matching.has(item.role);
1563
+ }
1564
+ /**
1565
+ * Whether the subject holds every role the grantee names, at a scope the row
1566
+ * (and a write's next row) is in. `held: false` when a denial or skip was recorded.
1567
+ */
1568
+ function roleScope(state, grant, custom) {
1569
+ const { subject, permission, tracer } = state;
1570
+ const roleItems = flattenGrantee(grant.to).filter((item) => item.kind === "role");
1571
+ if (roleItems.length === 0) return { held: true };
1572
+ if (subject.principal === null) {
1573
+ state.denials.push({
1574
+ role: grant.role,
1575
+ reason: "anonymous"
1576
+ });
1577
+ return { held: false };
1578
+ }
1579
+ let membership;
1580
+ for (const roleItem of roleItems) {
1581
+ if (!holdsRole(state, roleItem, custom)) {
1582
+ skip(tracer, grant, permission.key, "role");
1583
+ return { held: false };
1614
1584
  }
1615
- if (!condition.matched) {
1616
- if (condition.reason === "closure-error") emitSafe(env.listeners.error, condition.cause ?? /* @__PURE__ */ new Error("closure-error"), env.listeners);
1617
- denials.push({
1618
- role: displayRole,
1619
- reason: condition.reason ?? "condition",
1620
- detail: condition.cause
1585
+ const matchRow = (row) => matchScopedMembership(subject, roleItem.scope, roleItem.role, row, state.scopes, state.resource, state.policy.resources, state.now, (held) => {
1586
+ if (custom === void 0) return expandRoleNames(held.roles, state.declared, state.env.customRoles, tenantOf(held, state.scopes)).roles;
1587
+ return holdsCustomRole(held, custom, state.scopes) ? [custom.name] : [];
1588
+ }, state.env.team, state.walkRole, permission.kind === "instance", state.options.scope);
1589
+ const scopeMatch = matchWriteScope(matchRow, permission.kind === "instance", state.current, state.next);
1590
+ if (!scopeMatch.ok) {
1591
+ state.denials.push({
1592
+ role: roleItem.role,
1593
+ reason: scopeMatch.reason
1621
1594
  });
1622
- continue;
1623
- }
1624
- if (!grantCoversField(grant.fields, options.field, grant.effect)) {
1625
- skip(tracer, grant, permission.key, "field");
1626
- continue;
1595
+ return { held: false };
1627
1596
  }
1628
- if (grant.effect === "deny") {
1629
- tracer?.denies.push(matchedOf(merged, permission.key));
1630
- if (grant.name !== void 0 && breakGlassOverrides.has(grant.name)) {
1631
- if (breakGlassGrant !== void 0) continue;
1632
- if (breakGlassDenial !== void 0) return complete({
1633
- outcome: "denied",
1634
- denials: [breakGlassDenial],
1635
- alternatives: env.skipAlternatives ? [] : alternativesFor(policy, permission, subject, now, env)
1636
- });
1637
- }
1638
- return complete({
1639
- outcome: "denied",
1640
- denials: [grant.name === void 0 ? {
1641
- role: displayRole,
1642
- reason: "deny"
1643
- } : {
1644
- role: displayRole,
1645
- reason: "deny",
1646
- detail: { name: grant.name }
1647
- }],
1648
- alternatives: env.skipAlternatives ? [] : alternativesFor(policy, permission, subject, now, env)
1649
- });
1650
- }
1651
- tracer?.allows.push(matchedOf(merged, permission.key));
1652
- allows.push(scopeMembership === void 0 ? { grant: merged } : {
1653
- grant: merged,
1654
- membership: scopeMembership
1597
+ membership = scopeMatch.membership ?? membership;
1598
+ }
1599
+ return membership === void 0 ? { held: true } : {
1600
+ held: true,
1601
+ membership
1602
+ };
1603
+ }
1604
+ /** Whether the grantee matches; records the denial or skip when it does not. */
1605
+ function granteeOf(state, grant, custom) {
1606
+ const { subject, now, permission } = state;
1607
+ if (grant.viaOnly !== void 0 && !(subject.principal?.memberships ?? []).some((membership) => membership.via === grant.viaOnly && !isMembershipExpired(membership, now) && inTeam(membership, state.scopes, state.env.team))) {
1608
+ skip(state.tracer, grant, permission.key, "via-only");
1609
+ return;
1610
+ }
1611
+ const required = grant.purpose;
1612
+ if (required !== void 0 && !state.purposes.some((purpose) => required.includes(purpose))) {
1613
+ skip(state.tracer, grant, permission.key, "purpose");
1614
+ return;
1615
+ }
1616
+ const granteeMatch = matchGrantee(grant.to, subject, now, state.resource, state.scopes, state.policy.resources, grant.effect === "deny");
1617
+ if (granteeMatch.matched) return granteeMatch;
1618
+ const reason = granteeMatch.reason ?? "no-grant";
1619
+ if (reason === "not-entitled" && !flattenGrantee(grant.to).every((item) => item.kind !== "role" || holdsRole(state, item, custom))) {
1620
+ skip(state.tracer, grant, permission.key, "grantee");
1621
+ return;
1622
+ }
1623
+ state.denials.push((reason === "insufficient-user-authentication" || reason === "not-entitled") && grant.to !== void 0 ? {
1624
+ role: grant.role,
1625
+ reason,
1626
+ to: grant.to
1627
+ } : {
1628
+ role: grant.role,
1629
+ reason
1630
+ });
1631
+ }
1632
+ /** One candidate grant: records an allow, a denial or a skip, or returns the decision a deny ends with. */
1633
+ function evaluateCandidate(state, { grant, custom }, breakGlass) {
1634
+ const { policy, subject, permission, now, env, tracer } = state;
1635
+ const displayRole = grant.role;
1636
+ if (tracer !== void 0) tracer.evaluated += 1;
1637
+ const granteeMatch = granteeOf(state, grant, custom);
1638
+ if (granteeMatch === void 0) return;
1639
+ const scoped = roleScope(state, grant, custom);
1640
+ if (!scoped.held) return;
1641
+ if (!isActive(grant.validity, now)) {
1642
+ if (grant.effect === "allow") state.denials.push({
1643
+ role: displayRole,
1644
+ reason: "inactive-grant",
1645
+ detail: grant.validity
1655
1646
  });
1647
+ else skip(tracer, grant, permission.key, "validity");
1648
+ return;
1656
1649
  }
1657
- if (breakGlassGrant !== void 0) {
1658
- tracer?.allows.push(matchedOf(breakGlassGrant, permission.key, true));
1659
- allows.push({
1660
- grant: breakGlassGrant,
1661
- obligations: breakGlassObligations,
1662
- breakGlass: true
1650
+ const requirement = grant.requires === void 0 || grant.effect !== "allow" ? void 0 : requirementCondition(policy, subject, grant.requires, state.resource, now, env.customRoles, env.customGrants);
1651
+ const merged = freezeDeep(compact({
1652
+ ...grant,
1653
+ where: combineWhere(combineWhere(grant.where, granteeMatch.where), requirement)
1654
+ }));
1655
+ const condition = evaluateGrantCondition(merged, permission, state.current, state.next, subject, now, state.scopes, env.relations, (target, id, key) => inheritedVerdict(evaluate, policy, state.given, target, id, key, now, env));
1656
+ if (!condition.matched && condition.reason === "closure-error") emitSafe(env.listeners.error, condition.cause ?? /* @__PURE__ */ new Error("closure-error"), env.listeners);
1657
+ if (!condition.matched && grant.effect === "deny" && isUnevaluable(condition.reason) && grantCoversField(grant.fields, state.options.field, grant.effect)) {
1658
+ tracer?.denies.push(matchedOf(merged, permission.key));
1659
+ return deniedNow(state, {
1660
+ role: displayRole,
1661
+ reason: condition.reason ?? "relation-unavailable"
1663
1662
  });
1664
- } else if (breakGlassDenial !== void 0) denials.unshift(breakGlassDenial);
1665
- if (allows.length === 0) {
1666
- const reason = denials[0]?.reason ?? (subject.principal === null ? "anonymous" : globalNames.unknown.length > 0 && globalNames.roles.length === 0 ? "unknown-role" : "no-grant");
1667
- return complete({
1668
- outcome: "denied",
1669
- denials: denials.length > 0 ? denials : [{
1670
- role: null,
1671
- reason
1672
- }],
1673
- alternatives: env.skipAlternatives ? [] : alternativesFor(policy, permission, subject, now, env)
1663
+ }
1664
+ if (!condition.matched) {
1665
+ state.denials.push({
1666
+ role: displayRole,
1667
+ reason: condition.reason ?? "condition",
1668
+ detail: condition.cause
1674
1669
  });
1670
+ return;
1675
1671
  }
1676
- if (subject.stale === true && (policy.fresh ?? []).includes(permission.key)) return complete({
1677
- outcome: "denied",
1678
- denials: [{
1679
- role: null,
1680
- reason: "stale-credentials"
1681
- }],
1682
- alternatives: []
1683
- });
1684
- const ceiling = delegatedPermissions(policy.delegations, subject, matchingRoles, now);
1685
- if (ceiling !== void 0 && !ceiling.has(permission.key)) return complete({
1686
- outcome: "denied",
1687
- denials: [{
1688
- role: null,
1689
- reason: "not-delegated"
1690
- }],
1691
- alternatives: env.skipAlternatives ? [] : alternativesFor(policy, permission, subject, now, env)
1672
+ if (!grantCoversField(grant.fields, state.options.field, grant.effect)) {
1673
+ skip(tracer, grant, permission.key, "field");
1674
+ return;
1675
+ }
1676
+ if (grant.effect === "deny") {
1677
+ tracer?.denies.push(matchedOf(merged, permission.key));
1678
+ if (grant.name !== void 0 && breakGlass.overrides.has(grant.name)) {
1679
+ if (breakGlass.grant !== void 0) return;
1680
+ if (breakGlass.denial !== void 0) return deniedWithAlternatives(state, [breakGlass.denial]);
1681
+ }
1682
+ return deniedWithAlternatives(state, [grant.name === void 0 ? {
1683
+ role: displayRole,
1684
+ reason: "deny"
1685
+ } : {
1686
+ role: displayRole,
1687
+ reason: "deny",
1688
+ detail: { name: grant.name }
1689
+ }]);
1690
+ }
1691
+ tracer?.allows.push(matchedOf(merged, permission.key));
1692
+ state.allows.push(scoped.membership === void 0 ? { grant: merged } : {
1693
+ grant: merged,
1694
+ membership: scoped.membership
1692
1695
  });
1696
+ }
1697
+ function noAllowDecision(state) {
1698
+ const { denials, subject, roles } = state;
1699
+ const reason = denials[0]?.reason ?? (subject.principal === null ? "anonymous" : roles.global.unknown.length > 0 && roles.global.roles.length === 0 ? "unknown-role" : "no-grant");
1700
+ return deniedWithAlternatives(state, denials.length > 0 ? denials : [{
1701
+ role: null,
1702
+ reason
1703
+ }]);
1704
+ }
1705
+ /**
1706
+ * The allows a delegation leaves usable. A policy delegation is a ceiling for
1707
+ * the actor: outside it, nothing is delegated; inside it, a token delegation
1708
+ * on the call must still cover.
1709
+ */
1710
+ function delegatedAllows(state) {
1711
+ const { policy, subject, permission, current } = state;
1712
+ const ceiling = delegatedPermissions(policy.delegations, subject, state.roles.matching, state.now);
1713
+ if (ceiling !== void 0 && !ceiling.has(permission.key)) return deniedWithAlternatives(state, [{
1714
+ role: null,
1715
+ reason: "not-delegated"
1716
+ }]);
1693
1717
  const unscoped = subject.actor !== void 0 && ceiling === void 0;
1694
1718
  let delegationMiss = coveredByDelegation(permission, subject.delegation, resourceIdOf(current), unscoped);
1695
- const requirementsCovered = allows.filter((candidate) => coversRequirements(policy.permissions, candidate.grant.requires, subject.delegation, resourceIdOf(current), unscoped));
1696
- let usable = delegationMiss === void 0 ? requirementsCovered : requirementsCovered.filter((candidate) => requiresStandIn(candidate.grant));
1719
+ const requirementsCovered = state.allows.filter((candidate) => coversRequirements(policy.permissions, candidate.grant.requires, subject.delegation, resourceIdOf(current), unscoped));
1720
+ const usable = delegationMiss === void 0 ? requirementsCovered : requirementsCovered.filter((candidate) => requiresStandIn(candidate.grant));
1697
1721
  if (delegationMiss === "not-delegated" && usable.length > 0) delegationMiss = void 0;
1698
1722
  if (delegationMiss === void 0 && usable.length === 0) delegationMiss = "not-delegated";
1699
- if (delegationMiss !== void 0) return complete({
1700
- outcome: "denied",
1701
- denials: [{
1702
- role: null,
1703
- reason: delegationMiss
1704
- }],
1705
- alternatives: env.skipAlternatives ? [] : alternativesFor(policy, permission, subject, now, env)
1706
- });
1707
- const approvalPolicies = env.approvalPolicies;
1708
- if (approvalPolicies === "failed" && usable.length > 0) return complete({
1709
- outcome: "denied",
1710
- denials: [{
1711
- role: null,
1712
- reason: "approval",
1713
- detail: APPROVAL_POLICY_UNAVAILABLE
1714
- }],
1715
- alternatives: []
1716
- });
1717
- const approvalOf = (candidate) => approvalPolicies === void 0 || approvalPolicies === "failed" ? candidate.grant.approval : tightenApproval(candidate.grant.approval, approvalPolicies, {
1718
- permission: permission.key,
1719
- tenant: decisionTenant(subject, candidate.membership),
1720
- subject,
1721
- current,
1722
- next,
1723
- now,
1724
- scopes: scopeList(policy.scopes)
1723
+ if (delegationMiss !== void 0) return deniedWithAlternatives(state, [{
1724
+ role: null,
1725
+ reason: delegationMiss
1726
+ }]);
1727
+ return usable;
1728
+ }
1729
+ function approvalOf(state, candidate) {
1730
+ const approvalPolicies = state.env.approvalPolicies;
1731
+ return approvalPolicies === void 0 || approvalPolicies === "failed" ? candidate.grant.approval : tightenApproval(candidate.grant.approval, approvalPolicies, {
1732
+ permission: state.permission.key,
1733
+ tenant: decisionTenant(state.subject, candidate.membership),
1734
+ subject: state.subject,
1735
+ current: state.current,
1736
+ next: state.next,
1737
+ now: state.now,
1738
+ scopes: state.scopes
1725
1739
  });
1726
- const quotaDenials = [];
1727
- let matchedAllow;
1728
- let approval;
1729
- let quotaState = {};
1740
+ }
1741
+ /** The first usable allow whose quota admits the call, or the quota denials. */
1742
+ function quotaAllow(state, usable) {
1743
+ const { subject, env } = state;
1744
+ const denials = [];
1730
1745
  for (const candidate of usable) {
1731
- approval = approvalOf(candidate);
1732
- const consume = !requiresApproval(approval) && shouldConsumeQuota(options.source, env.simulated);
1746
+ const approval = approvalOf(state, candidate);
1733
1747
  const quota = applyQuota({
1734
1748
  store: env.limits,
1735
1749
  cache: env.limitCache,
1736
1750
  grant: candidate.grant,
1737
- permissionKey: permission.key,
1751
+ permissionKey: state.permission.key,
1738
1752
  subjectId: subject.principal?.id ?? "",
1739
1753
  tenant: subject.principal?.tenant,
1740
- now,
1741
- consume
1754
+ now: state.now,
1755
+ consume: !requiresApproval(approval) && shouldConsumeQuota(state.options.source, env.simulated)
1742
1756
  });
1743
- if (quota.ok) {
1744
- matchedAllow = candidate;
1745
- quotaState = compact({
1757
+ if (quota.ok) return {
1758
+ allow: candidate,
1759
+ approval,
1760
+ quota: compact({
1746
1761
  quota: quota.quota,
1747
1762
  obligations: quota.obligations
1748
- });
1749
- break;
1750
- }
1751
- quotaDenials.push(quota.reason === "limit" ? {
1763
+ })
1764
+ };
1765
+ denials.push(quota.reason === "limit" ? {
1752
1766
  role: candidate.grant.role,
1753
1767
  reason: "limit",
1754
1768
  detail: quota.detail
@@ -1757,11 +1771,10 @@ function evaluate(policy, given, permission, data, options, env) {
1757
1771
  reason: quota.reason
1758
1772
  });
1759
1773
  }
1760
- if (matchedAllow === void 0) return complete({
1761
- outcome: "denied",
1762
- denials: quotaDenials.length > 0 ? quotaDenials : denials,
1763
- alternatives: env.skipAlternatives ? [] : alternativesFor(policy, permission, subject, now, env)
1764
- });
1774
+ return { denials };
1775
+ }
1776
+ function allowedDecision(state, allow, approval, quotaState) {
1777
+ const { policy, subject, permission, resource, env, current, next } = state;
1765
1778
  const resourceId = permission.kind === "collection" ? "*" : rowIdOf(current, resource?.id);
1766
1779
  const version = approval !== void 0 && approval !== "human" && approval.staleOn === "resource-change" && resource?.version !== void 0 ? versionOf(current, resource.version) : void 0;
1767
1780
  const token = env.simulated ? "pd1.simulated" : env.outcomeOnly === true ? "pd1.outcome" : decisionToken({
@@ -1774,11 +1787,15 @@ function evaluate(policy, given, permission, data, options, env) {
1774
1787
  payload: resourceId === "*" && (next ?? current) !== void 0 ? payloadDigest(next ?? current) : void 0
1775
1788
  });
1776
1789
  const matched = compact({
1777
- ...matchedOf(matchedAllow.grant, permission.key, matchedAllow.breakGlass),
1790
+ ...matchedOf(allow.grant, permission.key, allow.breakGlass),
1778
1791
  approval
1779
1792
  });
1780
- const obligations = [...quotaState.obligations ?? [], ...matchedAllow.obligations ?? []];
1781
- return complete(requiresApproval(approval) ? {
1793
+ const obligations = [
1794
+ ...quotaState.obligations ?? [],
1795
+ ...allow.obligations ?? [],
1796
+ ...allow.grant.obligations ?? []
1797
+ ];
1798
+ return complete(state, requiresApproval(approval) ? {
1782
1799
  outcome: "approval-required",
1783
1800
  grant: matched,
1784
1801
  reason: "human",
@@ -1790,7 +1807,91 @@ function evaluate(policy, given, permission, data, options, env) {
1790
1807
  token,
1791
1808
  ...quotaState,
1792
1809
  ...obligations.length === 0 ? {} : { obligations }
1793
- }, matchedAllow.membership);
1810
+ }, allow.membership);
1811
+ }
1812
+ function evaluate(policy, given, permission, data, options, env) {
1813
+ const call = {
1814
+ policy,
1815
+ given,
1816
+ subject: subjectForPermission(given, permission.key),
1817
+ permission,
1818
+ options,
1819
+ env,
1820
+ now: options.now ?? nowSeconds(),
1821
+ resource: getResource(policy.permissions, permission.resource),
1822
+ tracer: options.explain === true ? {
1823
+ evaluated: 0,
1824
+ allows: [],
1825
+ denies: [],
1826
+ skipped: []
1827
+ } : void 0
1828
+ };
1829
+ const rows = validatedRows(call, data, rowValues(permission, data));
1830
+ if (rows.invalid !== void 0) return deniedNow({
1831
+ ...call,
1832
+ current: rows.current
1833
+ }, {
1834
+ role: null,
1835
+ reason: "validation",
1836
+ detail: rows.invalid
1837
+ });
1838
+ const settled = {
1839
+ ...call,
1840
+ current: rows.current
1841
+ };
1842
+ if (isDelegatedPermission(policy, permission)) return deniedNow(settled, {
1843
+ role: null,
1844
+ reason: "pdp-unavailable",
1845
+ detail: "use permdock/pdp createPermDock"
1846
+ });
1847
+ const scopes = scopeList(policy.scopes);
1848
+ const declared = declaredRoleNames(policy);
1849
+ const roles = heldRoles(call, scopes, declared);
1850
+ if (lacksActor(call, scopes)) return deniedNow(settled, {
1851
+ role: null,
1852
+ reason: "actor-required"
1853
+ });
1854
+ const state = {
1855
+ ...call,
1856
+ current: rows.current,
1857
+ next: rows.next,
1858
+ scopes,
1859
+ declared,
1860
+ roles,
1861
+ purposes: purposesOf(call.subject),
1862
+ walkRole: walkRoleFor(call),
1863
+ denials: roles.denials,
1864
+ allows: []
1865
+ };
1866
+ const candidates = candidatesFor(call);
1867
+ const breakGlass = breakGlassFor(call, scopes);
1868
+ for (const candidate of candidates) {
1869
+ const decision = evaluateCandidate(state, candidate, breakGlass);
1870
+ if (decision !== void 0) return decision;
1871
+ }
1872
+ if (breakGlass.grant !== void 0) {
1873
+ state.tracer?.allows.push(matchedOf(breakGlass.grant, permission.key, true));
1874
+ state.allows.push({
1875
+ grant: breakGlass.grant,
1876
+ obligations: breakGlass.obligations,
1877
+ breakGlass: true
1878
+ });
1879
+ } else if (breakGlass.denial !== void 0) state.denials.unshift(breakGlass.denial);
1880
+ if (state.allows.length === 0) return noAllowDecision(state);
1881
+ if (state.subject.stale === true && (policy.fresh ?? []).includes(permission.key)) return deniedNow(state, {
1882
+ role: null,
1883
+ reason: "stale-credentials"
1884
+ });
1885
+ const usable = delegatedAllows(state);
1886
+ if (!Array.isArray(usable)) return usable;
1887
+ if (env.approvalPolicies === "failed" && usable.length > 0) return deniedNow(state, {
1888
+ role: null,
1889
+ reason: "approval",
1890
+ detail: APPROVAL_POLICY_UNAVAILABLE
1891
+ });
1892
+ const chosen = quotaAllow(state, usable);
1893
+ if ("denials" in chosen) return deniedWithAlternatives(state, chosen.denials.length > 0 ? chosen.denials : state.denials);
1894
+ return allowedDecision(state, chosen.allow, chosen.approval, chosen.quota);
1794
1895
  }
1795
1896
  //#endregion
1796
1897
  //#region src/core/ownership.ts
@@ -1907,7 +2008,7 @@ function coversInstance(membership, scopes, scope, id, within) {
1907
2008
  }
1908
2009
  /** Roles the principal holds at the instance, at an ancestor named in `within`, or globally. */
1909
2010
  function rolesAtInstance(policy, principal, scopes, scope, id, within, now) {
1910
- const declared = declaredRoleNames$1(policy);
2011
+ const declared = declaredRoleNames(policy);
1911
2012
  const names = new Set((principal.roles ?? []).filter((name) => declared.has(name)));
1912
2013
  for (const membership of principal.memberships ?? []) if (!isMembershipExpired(membership, now) && coversInstance(membership, scopes, scope, id, within)) for (const name of membership.roles) names.add(name);
1913
2014
  return rankRoles(policy, [...names]);
@@ -2007,7 +2108,7 @@ function decideRoleChange(policy, principal, scopes, change, customRoles, author
2007
2108
  }
2008
2109
  const id = change.id ?? "";
2009
2110
  const binding = bindingOf(policy, name);
2010
- const declared = declaredRoleNames$1(policy).has(name);
2111
+ const declared = declaredRoleNames(policy).has(name);
2011
2112
  const named = declared || global ? [] : tenantCustomRoles(policy, customRoles).filter((role) => role.name === name);
2012
2113
  if (!declared && named.length === 0) {
2013
2114
  deny("unknown-role");
@@ -2068,7 +2169,7 @@ function decideRoleChange(policy, principal, scopes, change, customRoles, author
2068
2169
  const revocable = kind === "revoke" && custom !== void 0 && allowed.inert?.has(name);
2069
2170
  if (!allowed.assignable.has(name) && revocable !== true) deny("not-assignable-by");
2070
2171
  else if (custom === void 0 && usesAssigns(policy) && !allowed.manage) {
2071
- const held = global ? rankRoles(policy, (principal.roles ?? []).filter((role) => declaredRoleNames$1(policy).has(role))) : rolesAtInstance(policy, principal, scopes, scope, id, within, now);
2172
+ const held = global ? rankRoles(policy, (principal.roles ?? []).filter((role) => declaredRoleNames(policy).has(role))) : rolesAtInstance(policy, principal, scopes, scope, id, within, now);
2072
2173
  by = held.find((role) => bindingOf(policy, role)?.assigns?.includes(name) === true) ?? null;
2073
2174
  if (by === null) deny("not-assignable-by", { held });
2074
2175
  }
@@ -2088,658 +2189,767 @@ function decideRoleChange(policy, principal, scopes, change, customRoles, author
2088
2189
  return done(by);
2089
2190
  }
2090
2191
  //#endregion
2091
- //#region src/core/who-can.ts
2092
- const MAX_ROUNDS = 48;
2093
- async function settle$1(cache) {
2094
- for (let round = 0; round < MAX_ROUNDS; round += 1) {
2095
- const pending = pendingRelations(cache);
2096
- if (pending.length === 0) return;
2097
- await Promise.all(pending);
2192
+ //#region src/core/held-roles.ts
2193
+ /**
2194
+ * Role names held in `tenant` on live memberships, global roles included, in
2195
+ * rank order. With `scope`, only the roles of that scope's memberships (and
2196
+ * instance `id`).
2197
+ */
2198
+ function heldRoleNames(policy, subject, tenant, only, now) {
2199
+ if (subject.principal === null) return [];
2200
+ const scopes = scopeList(policy.scopes);
2201
+ const scope = only?.scope === void 0 ? void 0 : resolveScope(scopes, only.scope);
2202
+ if (only?.scope !== void 0 && scope === void 0) return [];
2203
+ const names = new Set(scope === void 0 ? subject.principal.roles ?? [] : []);
2204
+ for (const membership of subject.principal.memberships ?? []) {
2205
+ if (isMembershipExpired(membership, now) || membership.keep !== void 0) continue;
2206
+ if (!(scope === void 0 ? tenantOf(membership, scopes) === tenant : membership.scope === scope && (only?.id === void 0 || sameId(membership.id, only.id)))) continue;
2207
+ for (const role of membership.roles) names.add(role);
2098
2208
  }
2209
+ return rankRoles(policy, [...names]);
2099
2210
  }
2100
- /** The holders on each instance a graph relation reads, or `undefined` when the graph cannot say. */
2101
- async function walkHolders(condition, row, reader, cache) {
2102
- let walk = relationWalk(condition, row, reader);
2103
- if (walk === "relation-unavailable") {
2104
- await settle$1(cache);
2105
- walk = relationWalk(condition, row, reader);
2106
- }
2107
- if (walk === void 0) return [];
2108
- if (walk === "relation-depth" || walk === "relation-unavailable") return;
2109
- const { ids } = walk;
2110
- const read = () => ids.map((id) => reader.holders({
2111
- resource: condition.resource,
2112
- id,
2113
- relation: condition.relation
2211
+ function roleLeaf(policy, name) {
2212
+ const binding = policy.rolesByName.get(name);
2213
+ return findRole(policy.vocabulary?.roles, name) ?? synthesiseRole(name, compact({
2214
+ on: typeof binding?.on === "string" ? binding.on : void 0,
2215
+ assignable: binding?.assignable,
2216
+ meta: binding?.meta
2114
2217
  }));
2115
- let answers = read();
2116
- if (answers.includes("pending")) {
2117
- await settle$1(cache);
2118
- answers = read();
2119
- }
2120
- const out = [];
2121
- for (const [index, answer] of answers.entries()) {
2122
- const id = ids[index];
2123
- if (answer === "pending" || answer === "failed" || id === void 0) return;
2124
- out.push({
2125
- id,
2126
- holders: answer
2127
- });
2128
- }
2129
- return out;
2130
2218
  }
2131
- async function discoverRole(item, allow, ctx) {
2132
- if (typeof item.scope !== "string" || item.scope === "global") {
2133
- ctx.incomplete();
2134
- return;
2135
- }
2136
- const key = scopeField(ctx.resource, item.scope, ctx.scopes);
2137
- const id = key === void 0 ? void 0 : relationId(ownGet(ctx.row, key));
2138
- if (id === void 0) return;
2139
- const members = await ctx.list(item.scope, id);
2140
- if (members === null) {
2141
- ctx.incomplete();
2142
- return;
2143
- }
2144
- for (const member of members.filter((entry) => entry.membership.roles.includes(item.role))) ctx.add(member.id, allow ? {
2145
- kind: "role",
2146
- role: item.role,
2147
- membership: member.membership
2148
- } : void 0, member.membership);
2219
+ //#endregion
2220
+ //#region src/core/snapshot-grants.ts
2221
+ /** A grant on a relation with a `period` is decided on the server, where the clock is. */
2222
+ function periodBound(policy, grant) {
2223
+ const relations = policy.resources.get(grant.permission.resource)?.relations;
2224
+ return flattenGrantee(grant.to).some((item) => {
2225
+ if (item.kind !== "relation") return false;
2226
+ const spec = relations?.[item.relation];
2227
+ return isPrincipalRelation(spec) && spec.period !== void 0;
2228
+ });
2149
2229
  }
2150
- function holderVia(holder, at, share, group) {
2151
- if (!share) return {
2152
- kind: "relation",
2153
- ...at
2154
- };
2155
- return compact({
2156
- kind: "share",
2157
- ...at,
2158
- expiresAt: holder.expiresAt,
2159
- group
2230
+ function includePrefixes(include) {
2231
+ if (include === void 0) return;
2232
+ return include.map((item) => {
2233
+ if ("key" in item && typeof item.key === "string") return item.key;
2234
+ const first = listPermissions(item)[0];
2235
+ if (first === void 0) return "";
2236
+ const parts = first.key.split(".");
2237
+ parts.pop();
2238
+ return parts.join(".");
2160
2239
  });
2161
2240
  }
2162
2241
  /**
2163
- * Principals holding `group`'s relation, through at most `budget` more groups
2164
- * of its resource; `undefined` when a read failed. `memo` keeps each group's
2165
- * members per budget across one discovery, so a group shared by many paths is
2166
- * read once; a result cut short by a cycle is path-specific and not kept.
2242
+ * The grant with its grantee's row condition. A graph relation needs the
2243
+ * `RelationSource`, which snapshots and `where()` do not carry, so the grant
2244
+ * becomes server-only (`portable: false`) there.
2167
2245
  */
2168
- async function groupMembers(group, ctx, budget, seen, memo) {
2169
- const key = JSON.stringify([
2170
- group.resource,
2171
- group.id,
2172
- group.relation
2173
- ]);
2174
- const memoKey = `${budget}:${key}`;
2175
- const known = memo.get(memoKey);
2176
- if (known !== void 0) return {
2177
- members: known,
2178
- cut: false
2179
- };
2180
- let holders = ctx.reader.holders(group);
2181
- if (holders === "pending") {
2182
- await settle$1(ctx.cache);
2183
- holders = ctx.reader.holders(group);
2184
- }
2185
- if (holders === "pending" || holders === "failed") return;
2186
- const path = new Set(seen).add(key);
2187
- const out = /* @__PURE__ */ new Set();
2188
- let cut = false;
2189
- for (const holder of holders) {
2190
- if ("principal" in holder) {
2191
- out.add(holder.principal.id);
2192
- continue;
2193
- }
2194
- const nested = JSON.stringify([
2195
- holder.group.resource,
2196
- holder.group.id,
2197
- holder.group.relation
2198
- ]);
2199
- const same = holder.group.resource === group.resource;
2200
- if (path.has(nested)) {
2201
- cut = true;
2202
- continue;
2203
- }
2204
- if (same && budget <= 0) continue;
2205
- const walk = await groupMembers(holder.group, ctx, same ? budget - 1 : 16, path, memo);
2206
- if (walk === void 0) return;
2207
- cut ||= walk.cut;
2208
- for (const member of walk.members) out.add(member);
2209
- }
2210
- const members = [...out];
2211
- if (!cut) memo.set(memoKey, members);
2212
- return {
2213
- members,
2214
- cut
2215
- };
2246
+ function graphAware(grant, where) {
2247
+ const combined = combineWhere(grant.where, where);
2248
+ const graph = hasConditionOp(combined, "related");
2249
+ const inherits = flattenGrantee(grant.to).some((item) => item.kind === "inherit");
2250
+ return freezeDeep(compact({
2251
+ ...grant,
2252
+ where: combined,
2253
+ portable: graph ? false : grant.portable,
2254
+ graph: graph && grant.portable && !inherits ? true : void 0
2255
+ }));
2216
2256
  }
2217
- async function discoverGraph(condition, allow, ctx) {
2218
- const nodes = await walkHolders(condition, ctx.row, ctx.reader, ctx.cache);
2219
- if (nodes === void 0) {
2220
- ctx.incomplete();
2221
- return;
2222
- }
2223
- const node = ctx.policy.resources.get(condition.resource);
2224
- const memo = /* @__PURE__ */ new Map();
2225
- for (const entry of nodes) for (const holder of entry.holders) {
2226
- const at = {
2227
- resource: condition.resource,
2228
- relation: holder.relation,
2229
- id: entry.id
2230
- };
2231
- const share = isEdgeRelation(node?.relations[holder.relation]);
2232
- if ("principal" in holder) {
2233
- ctx.add(holder.principal.id, allow ? holderVia(holder, at, share) : void 0);
2234
- continue;
2235
- }
2236
- const walk = await groupMembers(holder.group, ctx, holder.group.resource === condition.resource ? 15 : 16, /* @__PURE__ */ new Set(), memo);
2237
- if (walk === void 0) {
2238
- ctx.incomplete();
2257
+ /**
2258
+ * `where()` keeps a grant that is server-only because it reads the graph:
2259
+ * its `related` nodes compile to subqueries given relation mappings.
2260
+ */
2261
+ function whereGrant(grant) {
2262
+ return grant.graph === true ? freezeDeep({
2263
+ ...grant,
2264
+ portable: true
2265
+ }) : grant;
2266
+ }
2267
+ /**
2268
+ * Every grant the subject may reach, once per membership that holds all of
2269
+ * its scoped roles, so a client can check that membership's tenant, team or
2270
+ * resource. A scoped role held only globally reaches nothing.
2271
+ */
2272
+ function collectSnapshotGrants(policy, subject, customRoles, now = nowSeconds(), customGrants = customGrantsFor(policy, customRoles), mode = "held") {
2273
+ const skip = (grant, match) => mode === "held" ? !match.matched : match.matched || match.reason !== "not-entitled" || grant.effect !== "allow";
2274
+ const scopes = scopeList(policy.scopes);
2275
+ const declared = declaredRoleNames(policy);
2276
+ const global = expandRoleNames(subject.principal?.roles ?? [], declared, customRoles, subject.principal?.tenant);
2277
+ const held = (subject.principal?.memberships ?? []).map((membership) => ({
2278
+ membership,
2279
+ roles: new Set(expandRoleNames(membership.roles, declared, customRoles, tenantOf(membership, scopes)).roles)
2280
+ }));
2281
+ const holds = (entry, item) => entry.roles.has(item.role) && (typeof item.scope === "string" ? entry.membership.scope === item.scope : entry.membership.on !== void 0);
2282
+ const requirementOf = (grant, resource) => grant.requires === void 0 || grant.effect !== "allow" ? void 0 : requirementCondition(policy, subject, grant.requires, resource, now, customRoles, customGrants);
2283
+ const out = [];
2284
+ for (const grant of grantList(policy)) {
2285
+ if (mode === "held" && !coversRequirements(policy.permissions, grant.requires, subject.delegation)) continue;
2286
+ const resource = getResource(policy.permissions, grant.permission.resource);
2287
+ const match = matchGrantee(grant.to, subject, now, resource, scopes, policy.resources);
2288
+ if (skip(grant, match)) continue;
2289
+ const roleItems = flattenGrantee(grant.to).filter((item) => item.kind === "role");
2290
+ if (roleItems.length > 0 && subject.principal === null) continue;
2291
+ if (!roleItems.every((item) => item.scope !== "global" || global.roles.includes(item.role))) continue;
2292
+ const merged = graphAware(grant, combineWhere(match.where, requirementOf(grant, resource)));
2293
+ const scoped = roleItems.filter((item) => item.scope !== "global");
2294
+ if (scoped.length === 0) {
2295
+ out.push({ grant: merged });
2239
2296
  continue;
2240
2297
  }
2241
- for (const member of walk.members) ctx.add(member, allow ? holderVia(holder, at, share, holder.group) : void 0);
2242
- }
2243
- }
2244
- async function discoverRelation(item, allow, ctx, expanded = false) {
2245
- const condition = relationCondition(item, ctx.resource, ctx.scopes, { resources: ctx.policy.resources });
2246
- if (condition?.op === "related") {
2247
- await discoverGraph(condition, allow, ctx);
2248
- return;
2249
- }
2250
- const spec = ctx.resource?.relations[item.relation];
2251
- if (spec?.includes !== void 0 && !expanded) {
2252
- for (const name of expandRelation(ctx.resource, item.relation)) await discoverRelation({
2253
- ...item,
2254
- relation: name
2255
- }, allow, ctx, true);
2256
- return;
2298
+ for (const entry of held) if (keepsPermission(entry.membership, grant.permission.key) && scoped.every((item) => item.kind === "role" && holds(entry, item))) out.push({
2299
+ grant: merged,
2300
+ membership: entry.membership
2301
+ });
2257
2302
  }
2258
- const via = allow ? {
2259
- kind: "relation",
2260
- resource: ctx.permission.resource,
2261
- relation: item.relation,
2262
- id: relationId(ownGet(ctx.row, ctx.resource?.id ?? "id")) ?? ""
2263
- } : void 0;
2264
- if (isFieldRelation(spec) && spec.memberOf !== void 0) {
2265
- const scope = resolveScope(ctx.scopes, spec.memberOf);
2266
- const id = relationId(ownGet(ctx.row, spec.field));
2267
- const members = scope === void 0 || id === void 0 ? [] : await ctx.list(scope, id);
2268
- if (members === null) {
2269
- ctx.incomplete();
2270
- return;
2271
- }
2272
- for (const member of members) ctx.add(member.id, via, member.membership);
2273
- return;
2303
+ for (const { grant, role } of customGrants) {
2304
+ const globalHeld = holdsGlobalCustomRole(subject.principal?.roles, role);
2305
+ const holders = held.filter((entry) => keepsPermission(entry.membership, grant.permission.key) && holdsCustomRole(entry.membership, role, scopes));
2306
+ if (holders.length === 0 && !globalHeld) continue;
2307
+ const resource = getResource(policy.permissions, grant.permission.resource);
2308
+ const match = matchGrantee(grant.to, subject, now, resource, scopes, policy.resources);
2309
+ if (skip(grant, match)) continue;
2310
+ const merged = graphAware(grant, combineWhere(match.where, requirementOf(grant, resource)));
2311
+ if (globalHeld) out.push({ grant: merged });
2312
+ for (const entry of holders) out.push({
2313
+ grant: merged,
2314
+ membership: entry.membership
2315
+ });
2274
2316
  }
2275
- const column = isFieldRelation(spec) ? spec.field : isPrincipalRelation(spec) ? spec.principal : void 0;
2276
- const holder = column === void 0 ? void 0 : relationId(ownGet(ctx.row, column));
2277
- if (holder !== void 0) ctx.add(holder, via);
2317
+ const fresh = policy.fresh ?? [];
2318
+ if (mode === "held" && subject.stale === true && fresh.length > 0) return out.filter(({ grant }) => grant.effect === "deny" || !fresh.includes(grant.permission.key));
2319
+ return out;
2278
2320
  }
2279
- function discover(grant, item, ctx) {
2280
- const allow = grant.effect === "allow";
2281
- switch (item.kind) {
2282
- case "role": return discoverRole(item, allow, ctx);
2283
- case "relation": return discoverRelation(item, allow, ctx);
2284
- case "anyone":
2285
- case "authenticated":
2286
- case "plan":
2287
- case "actor":
2288
- case "assurance":
2289
- case "inherit":
2290
- ctx.incomplete();
2291
- return Promise.resolve();
2292
- default: return unknownKind(item, ctx);
2293
- }
2321
+ //#endregion
2322
+ //#region src/core/assignable.ts
2323
+ /**
2324
+ * Declared roles a tenant admin may hand out or compose, before any
2325
+ * intersection: `assignable` roles, plus every role some `assigns` lists.
2326
+ */
2327
+ function assignableCandidates(policy) {
2328
+ const listed = new Set(policy.roles.flatMap((binding) => binding.assigns ?? []));
2329
+ const names = [...listRoles(policy.vocabulary?.roles).filter((leaf) => leaf.assignable || listed.has(leaf.key)).map((leaf) => leaf.key), ...policy.roles.filter((binding) => binding.assignable || listed.has(binding.name)).map((binding) => binding.name)];
2330
+ return rankRoles(policy, [...new Set(names)]).map((name) => roleLeaf(policy, name));
2294
2331
  }
2295
- /** Holders of a grantee kind this build does not know cannot be listed. */
2296
- function unknownKind(_item, ctx) {
2297
- ctx.incomplete();
2298
- return Promise.resolve();
2332
+ /**
2333
+ * The levels a held allow of a permission covers: its own `level`, every
2334
+ * level when it has no row condition, or the levels whose condition equals
2335
+ * its condition.
2336
+ */
2337
+ function heldLevels(policy, grant, names) {
2338
+ if (grant.level !== void 0) return [grant.level];
2339
+ if (grant.where === void 0) return names;
2340
+ const held = canonicalJson(grant.where);
2341
+ return names.filter((name) => {
2342
+ const condition = levelCondition(policy, grant.permission.resource, name);
2343
+ return condition !== void 0 && canonicalJson(condition) === held;
2344
+ });
2299
2345
  }
2300
- function memberLister(source, scopes) {
2301
- const listed = /* @__PURE__ */ new Map();
2302
- const load = async (scope, id) => {
2303
- if (source?.list === void 0) return null;
2304
- try {
2305
- return (await source.list({
2306
- scope,
2307
- id
2308
- })).flatMap((entry) => typeof entry.principal?.id === "string" ? normalizeMemberships([entry.membership], scopes).map((membership) => ({
2309
- id: entry.principal.id,
2310
- membership
2311
- })) : []);
2312
- } catch {
2313
- return null;
2314
- }
2315
- };
2316
- return (scope, id) => {
2317
- const key = JSON.stringify([scope, id]);
2318
- const known = listed.get(key) ?? load(scope, id);
2319
- listed.set(key, known);
2320
- return known;
2346
+ /**
2347
+ * What the subject may hand out in `tenant`: declared assignable roles and the
2348
+ * tenant custom-role ceiling, narrowed by `allowed` (`RoleSource.assignable`),
2349
+ * then intersected with the permissions the subject holds there. Holding a
2350
+ * role or a granted permission with `meta.manageRoles` lifts the intersection.
2351
+ */
2352
+ function assignableIn(policy, subject, customRoles, customGrants, tenant, allowed, now = nowSeconds(), global = false) {
2353
+ const principal = subject.principal;
2354
+ if (principal === null) return {
2355
+ roles: [],
2356
+ permissions: [],
2357
+ levels: {},
2358
+ manage: false,
2359
+ inert: []
2321
2360
  };
2322
- }
2323
- async function whoCan(input) {
2324
- const { policy, permission, row } = input;
2325
- if (permission.kind !== "instance" || row === null || typeof row !== "object") return freezeDeep({
2326
- permission: permission.key,
2327
- holders: [],
2328
- complete: false
2329
- });
2330
2361
  const scopes = scopeList(policy.scopes);
2331
- let complete = !input.customGrants.some((item) => item.grant.permission.key === permission.key);
2332
- const found = /* @__PURE__ */ new Map();
2333
- const ctx = {
2334
- policy,
2335
- scopes,
2336
- resource: getResource(policy.permissions, permission.resource),
2337
- permission,
2338
- row,
2339
- reader: input.reader,
2340
- cache: input.cache,
2341
- list: memberLister(input.memberships, scopes),
2342
- add(id, via, membership) {
2343
- const entry = found.get(id) ?? {
2344
- memberships: [],
2345
- via: []
2346
- };
2347
- found.set(id, entry);
2348
- if (via !== void 0) entry.via.push(via);
2349
- if (membership !== void 0) entry.memberships.push(membership);
2350
- },
2351
- incomplete() {
2352
- complete = false;
2353
- }
2354
- };
2355
- await Promise.all((policy.index.grantsByKey.get(permission.key) ?? []).flatMap((grant) => flattenGrantee(grant.to).map((item) => discover(grant, item, ctx))));
2356
- const root = rootScope(scopes);
2357
- const tenantKey = root === void 0 ? void 0 : scopeField(getResource(policy.permissions, permission.resource), root, scopes);
2358
- const tenant = tenantKey === void 0 ? void 0 : relationId(ownGet(row, tenantKey));
2359
- const env = {
2362
+ const scoped = tenant === void 0 ? subject : freezeDeep(compact({
2363
+ ...subject,
2364
+ principal: compact({
2365
+ ...principal,
2366
+ tenant: resolveActiveTenant(principal, tenant, scopes, now)
2367
+ })
2368
+ }));
2369
+ const heldGrants = collectSnapshotGrants(policy, scoped, customRoles, now, customGrants).filter((item) => item.grant.effect === "allow" && (item.membership === void 0 || !global && tenantOf(item.membership, scopes) === tenant && !isMembershipExpired(item.membership, now))).map((item) => item.grant);
2370
+ const heldKeys = new Set(heldGrants.map((grant) => grant.permission.key));
2371
+ const heldNames = heldRoleNames(policy, subject, tenant, void 0, now);
2372
+ const quiet = {
2360
2373
  emit: false,
2361
2374
  simulated: true,
2362
2375
  skipAlternatives: true,
2363
- customRoles: [],
2364
- customGrants: [],
2376
+ customRoles,
2377
+ customGrants,
2365
2378
  listeners: emptyListeners(),
2366
2379
  sink: void 0,
2367
2380
  limits: void 0,
2368
2381
  limitCache: /* @__PURE__ */ new Map(),
2369
- team: input.team,
2370
- relations: input.reader
2382
+ team: void 0
2371
2383
  };
2372
- const now = nowSeconds();
2373
- const ordered = [...found.entries()].toSorted(([a], [b]) => byCodePoint(a, b));
2374
- const decideAll = () => ordered.map(([id, entry]) => {
2375
- const subject = freezeDeep({
2376
- principal: {
2377
- id,
2378
- roles: [],
2379
- memberships: entry.memberships,
2380
- ...tenant === void 0 ? {} : { tenant }
2381
- },
2382
- context: {}
2383
- });
2384
- return evaluate(policy, subject, permission, row, {
2385
- source: "simulate",
2386
- trusted: true,
2387
- now
2388
- }, env);
2384
+ const manage = heldNames.some((name) => roleMeta(policy, name)?.manageRoles === true) || listPermissions(policy.permissions).some((leaf) => leaf.meta.manageRoles === true && evaluate(policy, scoped, leaf, void 0, {
2385
+ source: "simulate",
2386
+ trusted: true,
2387
+ now
2388
+ }, quiet).outcome === "granted");
2389
+ const narrowed = allowed === void 0 ? void 0 : new Set(allowed);
2390
+ const candidates = assignableCandidates(policy).filter((leaf) => narrowed === void 0 || narrowed.has(leaf.key));
2391
+ const held = new Set(heldNames);
2392
+ const listed = usesAssigns(policy) ? new Set(heldNames.flatMap((name) => policy.rolesByName.get(name)?.assigns ?? [])) : void 0;
2393
+ const roles = manage ? candidates : candidates.filter((leaf) => {
2394
+ if (listed !== void 0) return listed.has(leaf.key);
2395
+ if (held.has(leaf.key)) return true;
2396
+ const keys = [...roleAllowKeys(policy, leaf.key)];
2397
+ return keys.length > 0 && keys.every((key) => heldKeys.has(key));
2389
2398
  });
2390
- let decisions = decideAll();
2391
- if (pendingRelations(input.cache).length > 0) {
2392
- await settle$1(input.cache);
2393
- decisions = decideAll();
2399
+ const root = global ? "global" : rootScope(scopes);
2400
+ const ceiling = new Set((root === void 0 ? [] : ceilingGrants(policy, root, allowed)).map((grant) => grant.permission.key));
2401
+ const permissions = listPermissions(policy.permissions).filter((leaf) => ceiling.has(leaf.key) && (manage || heldKeys.has(leaf.key)));
2402
+ const offeredLevels = (key, names) => {
2403
+ if (manage) return names;
2404
+ const reached = new Set(heldGrants.filter((grant) => grant.permission.key === key).flatMap((grant) => heldLevels(policy, grant, names)));
2405
+ return names.filter((name) => reached.has(name));
2406
+ };
2407
+ const levels = {};
2408
+ for (const leaf of permissions) {
2409
+ const names = leaf.kind === "instance" ? levelNames(policy, leaf.resource) : [];
2410
+ if (names.length > 0) levels[leaf.key] = offeredLevels(leaf.key, names);
2394
2411
  }
2395
- const holders = [];
2396
- for (const [index, [id, entry]] of ordered.entries()) {
2397
- const decision = decisions[index];
2398
- if (decision === void 0 || decision.outcome === "denied") {
2399
- if (decision?.outcome === "denied" && decision.denials.some((denial) => denial.reason === "relation-unavailable" || denial.reason === "relation-depth")) complete = false;
2412
+ const ceilingKeys = /* @__PURE__ */ new Map();
2413
+ const mayHandOut = (scope, grant) => {
2414
+ const key = grant.permission.key;
2415
+ let keys = ceilingKeys.get(scope);
2416
+ if (keys === void 0) {
2417
+ keys = new Set(ceilingGrants(policy, scope, allowed).map((item) => item.permission.key));
2418
+ ceilingKeys.set(scope, keys);
2419
+ }
2420
+ if (!keys.has(key) || !(manage || heldKeys.has(key))) return false;
2421
+ const names = grant.permission.kind === "instance" ? levelNames(policy, grant.permission.resource) : [];
2422
+ if (names.length === 0) return true;
2423
+ const offered = offeredLevels(key, names);
2424
+ return heldLevels(policy, grant, names).every((name) => offered.includes(name));
2425
+ };
2426
+ const custom = global || tenant === void 0 ? {
2427
+ roles: [],
2428
+ inert: []
2429
+ } : assignableCustomRoles(policy, customRoles, tenant, manage ? void 0 : roles, mayHandOut);
2430
+ return {
2431
+ roles: [...roles, ...custom.roles],
2432
+ permissions,
2433
+ levels,
2434
+ manage,
2435
+ inert: custom.inert
2436
+ };
2437
+ }
2438
+ /**
2439
+ * The tenant's custom roles the subject may hand out: held at a scope it may
2440
+ * assign declared roles at (any scope with `meta.manageRoles`), allowing at
2441
+ * least one permission after the ceiling, and only permissions and levels it
2442
+ * may hand out at that scope. A role that allows nothing is `inert`.
2443
+ */
2444
+ function assignableCustomRoles(policy, customRoles, tenant, declared, mayHandOut) {
2445
+ const scopes = scopeList(policy.scopes);
2446
+ const root = rootScope(scopes);
2447
+ const assignsAt = declared === void 0 ? void 0 : new Set(declared.map((leaf) => leaf.on === void 0 ? root : resolveScope(scopes, leaf.on)));
2448
+ const out = /* @__PURE__ */ new Map();
2449
+ const inert = /* @__PURE__ */ new Set();
2450
+ for (const role of tenantCustomRoles(policy, customRoles)) {
2451
+ if (role.tenant !== tenant || out.has(role.name)) continue;
2452
+ const at = customRoleScope(role, scopes);
2453
+ if (at === void 0 || assignsAt !== void 0 && !assignsAt.has(at)) continue;
2454
+ const { grants } = resolveCustomRole(policy, role);
2455
+ if (!grants.some((grant) => grant.effect === "allow")) {
2456
+ inert.add(role.name);
2400
2457
  continue;
2401
2458
  }
2402
- holders.push({
2403
- principal: { id },
2404
- via: entry.via
2459
+ if (grants.every((grant) => grant.effect === "deny" || mayHandOut(at, grant))) out.set(role.name, synthesiseRole(role.name, compact({
2460
+ on: at,
2461
+ assignable: true,
2462
+ meta: sourceRoleMeta(role.meta, treeXSchema(policy.vocabulary?.roles))
2463
+ })));
2464
+ }
2465
+ return {
2466
+ roles: [...out.values()].toSorted((a, b) => a.key.localeCompare(b.key)),
2467
+ inert: [...inert].filter((name) => !out.has(name)).toSorted()
2468
+ };
2469
+ }
2470
+ //#endregion
2471
+ //#region src/core/instance-snapshot.ts
2472
+ /** The one path from a resolved subject to a `Snapshot`; pure apart from the default clock. */
2473
+ function snapshotOf(policy, subject, options) {
2474
+ const now = options.now ?? nowSeconds();
2475
+ const customGrants = options.customGrants ?? customGrantsFor(policy, options.customRoles);
2476
+ const roles = heldRoleNames(policy, subject, subject.principal?.tenant, void 0, now);
2477
+ const audiences = audiencesOf(policy, roles);
2478
+ return buildSnapshot(compact({
2479
+ subject,
2480
+ roles,
2481
+ audiences: audiences.length === 0 ? void 0 : audiences,
2482
+ notEntitled: collectSnapshotGrants(policy, subject, options.customRoles, now, customGrants, "not-entitled"),
2483
+ grants: collectSnapshotGrants(policy, subject, options.customRoles, now, customGrants).map((item) => periodBound(policy, item.grant) ? Object.assign({}, item, { grant: freezeDeep({
2484
+ ...item.grant,
2485
+ portable: false
2486
+ }) }) : item),
2487
+ include: includePrefixes(options.include),
2488
+ tenants: options.tenants,
2489
+ simulated: options.simulated,
2490
+ now: Math.floor(now),
2491
+ vocabulary: policy.vocabulary,
2492
+ scopes: snapshotScopes(policy),
2493
+ idOf: (name) => getResource(policy.permissions, name)?.id,
2494
+ delegated: delegatedPermissions(policy.delegations, subject, new Set(roles), now),
2495
+ assignable: (tenant) => {
2496
+ const found = assignableIn(policy, subject, options.customRoles, customGrants, tenant, options.assignable?.get(tenant), now);
2497
+ return compact({
2498
+ tenant,
2499
+ roles: found.roles.map((leaf) => leaf.key),
2500
+ permissions: found.permissions,
2501
+ levels: Object.keys(found.levels).length === 0 ? void 0 : found.levels
2502
+ });
2503
+ }
2504
+ }));
2505
+ }
2506
+ /** The policy's scopes as snapshots and `permdock cloud push` carry them, with the resources each partitions. */
2507
+ function snapshotScopes(policy) {
2508
+ if (policy.scopes.length === 0) return;
2509
+ const resources = [...policy.resources.values()];
2510
+ return policy.scopes.map((scope) => {
2511
+ const nodes = resources.filter((node) => partitionsOf(node, policy.scopes).includes(scope.name));
2512
+ const partitioned = nodes.map((node) => node.name);
2513
+ const fields = Object.fromEntries(nodes.flatMap((node) => {
2514
+ const field = scopeField(node, scope.name, policy.scopes);
2515
+ return field === void 0 || field === scope.key ? [] : [[node.name, field]];
2516
+ }));
2517
+ return compact({
2518
+ name: scope.name,
2519
+ key: scope.key ?? "",
2520
+ within: scope.within,
2521
+ resources: partitioned.length === 0 ? void 0 : partitioned,
2522
+ fields: Object.keys(fields).length === 0 ? void 0 : fields
2405
2523
  });
2524
+ });
2525
+ }
2526
+ //#endregion
2527
+ //#region src/core/assert-decision.ts
2528
+ /** `assert`'s outcome: the granted decision, or the error a refusal throws after `onDenied` runs. */
2529
+ function assertDecision(policy, subject, permission, data, decision, onDenied) {
2530
+ if (decision.outcome === "granted") return decision;
2531
+ if (onDenied !== void 0) onDenied(decision);
2532
+ const resource = getResource(policy.permissions, permission.resource);
2533
+ const resourceId = data !== null && typeof data === "object" ? data[resource?.id ?? "id"] : void 0;
2534
+ const resourceRef = compact({
2535
+ type: permission.resource,
2536
+ id: resourceId === void 0 ? void 0 : String(resourceId)
2537
+ });
2538
+ if (decision.outcome === "approval-required") throw new PermDockApprovalRequiredError({
2539
+ decision,
2540
+ permission: permission.key,
2541
+ scope: permission.scope,
2542
+ resource: resourceRef,
2543
+ message: approvalMessage(permission.key, decision.reason, decision.token)
2544
+ });
2545
+ if (decision.denials.some((denial) => denial.reason === "validation")) {
2546
+ const detail = decision.denials[0]?.detail;
2547
+ if (detail instanceof PermDockValidationError) throw detail;
2406
2548
  }
2407
- return freezeDeep({
2549
+ throw new PermDockDeniedError({
2550
+ decision,
2408
2551
  permission: permission.key,
2409
- holders,
2410
- complete
2552
+ scope: permission.scope,
2553
+ resource: resourceRef,
2554
+ subject,
2555
+ message: deniedMessage(permission.key, subject.principal?.id, decision.denials, decision.alternatives.map((leaf) => leaf.key)),
2556
+ ...data === void 0 || resource === void 0 ? {} : { disclosure: resource.disclosure }
2411
2557
  });
2412
2558
  }
2413
2559
  //#endregion
2414
- //#region src/core/instance.ts
2415
- /** Each round reads one more layer: link hops, then the chain, then nested groups. */
2416
- const LOAD_ROUNDS = 48;
2417
- /** A grant on a relation with a `period` is decided on the server, where the clock is. */
2418
- function periodBound(policy, grant) {
2419
- const relations = policy.resources.get(grant.permission.resource)?.relations;
2420
- return flattenGrantee(grant.to).some((item) => {
2421
- if (item.kind !== "relation") return false;
2422
- const spec = relations?.[item.relation];
2423
- return isPrincipalRelation(spec) && spec.period !== void 0;
2560
+ //#region src/core/instance-env.ts
2561
+ /** The environment `derive(options)` builds on: `base` with the sources `options` replaces loaded again. */
2562
+ function deriveEnv(policy, subject, base, relationCache, options) {
2563
+ const scopes = scopeList(policy.scopes);
2564
+ const tenants = tenantsOf(subject.principal, scopes);
2565
+ const auth = [];
2566
+ const named = options.customRoles === void 0 ? void 0 : roleSourceFor(options.customRoles, subject, auth);
2567
+ const roleSource = options.customRoles === void 0 ? base.roleSource : named;
2568
+ const roles = options.customRoles === void 0 ? base.customRoles : customRolesFor(named, tenants, auth, subject.principal === null ? void 0 : subject.principal.roles ?? [], (tenant) => heldRoleNamesIn(subject.principal, scopes, tenant));
2569
+ const names = options.customRoles === void 0 ? base.assignable : assignableNamesFor(named, tenants, auth);
2570
+ const approvals = options.approvalPolicies === void 0 ? base.approvalPolicies : approvalPoliciesFor(policy, options.approvalPolicies, tenants, auth);
2571
+ const envWith = (loadedRoles, loadedNames, loadedApprovals) => compact({
2572
+ ...base,
2573
+ customRoles: loadedRoles,
2574
+ roleSource,
2575
+ assignable: loadedNames,
2576
+ approvalPolicies: loadedApprovals,
2577
+ relations: options.relations ?? base.relations,
2578
+ relationCache: options.relations === void 0 ? relationCache : void 0,
2579
+ queuedAuth: auth,
2580
+ queuedErrors: []
2424
2581
  });
2582
+ if (isThenable(roles) || isThenable(names) || isThenable(approvals)) return Promise.all([
2583
+ roles,
2584
+ names,
2585
+ approvals
2586
+ ]).then(([loadedRoles, loadedNames, loadedApprovals]) => envWith(loadedRoles, loadedNames, loadedApprovals));
2587
+ return envWith(roles, names, approvals);
2425
2588
  }
2426
- function includePrefixes(include) {
2427
- if (include === void 0) return;
2428
- return include.map((item) => {
2429
- if ("key" in item && typeof item.key === "string") return item.key;
2430
- const first = listPermissions(item)[0];
2431
- if (first === void 0) return "";
2432
- const parts = first.key.split(".");
2433
- parts.pop();
2434
- return parts.join(".");
2589
+ //#endregion
2590
+ //#region src/core/simulate-preview.ts
2591
+ function previewSubject(policy, subject, preview) {
2592
+ const previewRoles = preview.roles?.map((item) => typeof item === "string" ? item : item.key);
2593
+ const kinds = subject.principal === null ? void 0 : applyRoleKinds(policy, previewRoles ?? subject.principal.roles, preview.memberships === void 0 ? subject.principal.memberships ?? [] : normalizeMemberships(preview.memberships, scopeList(policy.scopes)));
2594
+ const principal = subject.principal === null || kinds === void 0 ? null : freezeDeep(compact({
2595
+ ...subject.principal,
2596
+ roles: kinds.roles,
2597
+ memberships: preview.memberships === void 0 && subject.principal.memberships === void 0 ? void 0 : kinds.memberships,
2598
+ tenant: preview.tenant ?? subject.principal.tenant
2599
+ }));
2600
+ return freezeDeep({
2601
+ ...subject,
2602
+ principal
2435
2603
  });
2436
2604
  }
2437
- /**
2438
- * Role names held in `tenant` on live memberships, global roles included, in
2439
- * rank order. With `scope`, only the roles of that scope's memberships (and
2440
- * instance `id`).
2441
- */
2442
- function heldRoleNames(policy, subject, tenant, only, now) {
2443
- if (subject.principal === null) return [];
2444
- const scopes = scopeList(policy.scopes);
2445
- const scope = only?.scope === void 0 ? void 0 : resolveScope(scopes, only.scope);
2446
- if (only?.scope !== void 0 && scope === void 0) return [];
2447
- const names = new Set(scope === void 0 ? subject.principal.roles ?? [] : []);
2448
- for (const membership of subject.principal.memberships ?? []) {
2449
- if (isMembershipExpired(membership, now) || membership.keep !== void 0) continue;
2450
- if (!(scope === void 0 ? tenantOf(membership, scopes) === tenant : membership.scope === scope && (only?.id === void 0 || sameId(membership.id, only.id)))) continue;
2451
- for (const role of membership.roles) names.add(role);
2605
+ /** The one `simulate` event for a batch: the worst decision, with the counts. */
2606
+ function simulateEvent(policy, subject, env, evaluated) {
2607
+ const counts = {
2608
+ granted: 0,
2609
+ denied: 0,
2610
+ approvalRequired: 0
2611
+ };
2612
+ for (const [, , decision] of evaluated) if (decision.outcome === "granted") counts.granted += 1;
2613
+ else if (decision.outcome === "approval-required") counts.approvalRequired += 1;
2614
+ else counts.denied += 1;
2615
+ const worst = evaluated.find(([, , decision]) => decision.outcome === "denied") ?? evaluated.find(([, , decision]) => decision.outcome === "approval-required") ?? evaluated[0];
2616
+ if (worst === void 0) return;
2617
+ const [permission, data, decision] = worst;
2618
+ finish(policy, subject, permission, data, decision, {
2619
+ source: "simulate",
2620
+ trusted: true
2621
+ }, {
2622
+ ...env,
2623
+ emit: true
2624
+ }, true, void 0, counts);
2625
+ }
2626
+ //#endregion
2627
+ //#region src/core/who-can.ts
2628
+ const MAX_ROUNDS = 48;
2629
+ async function settle$1(cache) {
2630
+ for (let round = 0; round < MAX_ROUNDS; round += 1) {
2631
+ const pending = pendingRelations(cache);
2632
+ if (pending.length === 0) return;
2633
+ await Promise.all(pending);
2452
2634
  }
2453
- return rankRoles(policy, [...names]);
2454
2635
  }
2455
- function roleLeaf(policy, name) {
2456
- const binding = policy.rolesByName.get(name);
2457
- return findRole(policy.vocabulary?.roles, name) ?? synthesiseRole(name, compact({
2458
- on: typeof binding?.on === "string" ? binding.on : void 0,
2459
- assignable: binding?.assignable,
2460
- meta: binding?.meta
2636
+ /** The holders on each instance a graph relation reads, or `undefined` when the graph cannot say. */
2637
+ async function walkHolders(condition, row, reader, cache) {
2638
+ let walk = relationWalk(condition, row, reader);
2639
+ if (walk === "relation-unavailable") {
2640
+ await settle$1(cache);
2641
+ walk = relationWalk(condition, row, reader);
2642
+ }
2643
+ if (walk === void 0) return [];
2644
+ if (walk === "relation-depth" || walk === "relation-unavailable") return;
2645
+ const { ids } = walk;
2646
+ const read = () => ids.map((id) => reader.holders({
2647
+ resource: condition.resource,
2648
+ id,
2649
+ relation: condition.relation
2461
2650
  }));
2651
+ let answers = read();
2652
+ if (answers.includes("pending")) {
2653
+ await settle$1(cache);
2654
+ answers = read();
2655
+ }
2656
+ const out = [];
2657
+ for (const [index, answer] of answers.entries()) {
2658
+ const id = ids[index];
2659
+ if (answer === "pending" || answer === "failed" || id === void 0) return;
2660
+ out.push({
2661
+ id,
2662
+ holders: answer
2663
+ });
2664
+ }
2665
+ return out;
2462
2666
  }
2463
- /**
2464
- * The grant with its grantee's row condition. A graph relation needs the
2465
- * `RelationSource`, which snapshots and `where()` do not carry, so the grant
2466
- * becomes server-only (`portable: false`) there.
2467
- */
2468
- function graphAware(grant, where) {
2469
- const combined = combineWhere(grant.where, where);
2470
- const graph = hasConditionOp(combined, "related");
2471
- const inherits = flattenGrantee(grant.to).some((item) => item.kind === "inherit");
2472
- return freezeDeep(compact({
2473
- ...grant,
2474
- where: combined,
2475
- portable: graph ? false : grant.portable,
2476
- graph: graph && grant.portable && !inherits ? true : void 0
2477
- }));
2667
+ async function discoverRole(item, allow, ctx) {
2668
+ if (typeof item.scope !== "string" || item.scope === "global") {
2669
+ ctx.incomplete();
2670
+ return;
2671
+ }
2672
+ const key = scopeField(ctx.resource, item.scope, ctx.scopes);
2673
+ const id = key === void 0 ? void 0 : relationId(ownGet(ctx.row, key));
2674
+ if (id === void 0) return;
2675
+ const members = await ctx.list(item.scope, id);
2676
+ if (members === null) {
2677
+ ctx.incomplete();
2678
+ return;
2679
+ }
2680
+ for (const member of members.filter((entry) => entry.membership.roles.includes(item.role))) ctx.add(member.id, allow ? {
2681
+ kind: "role",
2682
+ role: item.role,
2683
+ membership: member.membership
2684
+ } : void 0, member.membership);
2478
2685
  }
2479
- /**
2480
- * `where()` keeps a grant that is server-only because it reads the graph:
2481
- * its `related` nodes compile to subqueries given relation mappings.
2482
- */
2483
- function whereGrant(grant) {
2484
- return grant.graph === true ? freezeDeep({
2485
- ...grant,
2486
- portable: true
2487
- }) : grant;
2686
+ function holderVia(holder, at, share, group) {
2687
+ if (!share) return {
2688
+ kind: "relation",
2689
+ ...at
2690
+ };
2691
+ return compact({
2692
+ kind: "share",
2693
+ ...at,
2694
+ expiresAt: holder.expiresAt,
2695
+ group
2696
+ });
2488
2697
  }
2489
2698
  /**
2490
- * Every grant the subject may reach, once per membership that holds all of
2491
- * its scoped roles, so a client can check that membership's tenant, team or
2492
- * resource. A scoped role held only globally reaches nothing.
2699
+ * Principals holding `group`'s relation, through at most `budget` more groups
2700
+ * of its resource; `undefined` when a read failed. `memo` keeps each group's
2701
+ * members per budget across one discovery, so a group shared by many paths is
2702
+ * read once; a result cut short by a cycle is path-specific and not kept.
2493
2703
  */
2494
- function collectSnapshotGrants(policy, subject, customRoles, now = nowSeconds(), customGrants = customGrantsFor(policy, customRoles), mode = "held") {
2495
- const skip = (grant, match) => mode === "held" ? !match.matched : match.matched || match.reason !== "not-entitled" || grant.effect !== "allow";
2496
- const scopes = scopeList(policy.scopes);
2497
- const declared = declaredRoleNames(policy);
2498
- const global = expandRoleNames(subject.principal?.roles ?? [], declared, customRoles, subject.principal?.tenant);
2499
- const held = (subject.principal?.memberships ?? []).map((membership) => ({
2500
- membership,
2501
- roles: new Set(expandRoleNames(membership.roles, declared, customRoles, tenantOf(membership, scopes)).roles)
2502
- }));
2503
- const holds = (entry, item) => entry.roles.has(item.role) && (typeof item.scope === "string" ? entry.membership.scope === item.scope : entry.membership.on !== void 0);
2504
- const requirementOf = (grant, resource) => grant.requires === void 0 || grant.effect !== "allow" ? void 0 : requirementCondition(policy, subject, grant.requires, resource, now, customRoles, customGrants);
2505
- const out = [];
2506
- for (const grant of grantList(policy)) {
2507
- if (mode === "held" && !coversRequirements(policy.permissions, grant.requires, subject.delegation)) continue;
2508
- const resource = getResource(policy.permissions, grant.permission.resource);
2509
- const match = matchGrantee(grant.to, subject, now, resource, scopes, policy.resources);
2510
- if (skip(grant, match)) continue;
2511
- const roleItems = flattenGrantee(grant.to).filter((item) => item.kind === "role");
2512
- if (roleItems.length > 0 && subject.principal === null) continue;
2513
- if (!roleItems.every((item) => item.scope !== "global" || global.roles.includes(item.role))) continue;
2514
- const merged = graphAware(grant, combineWhere(match.where, requirementOf(grant, resource)));
2515
- const scoped = roleItems.filter((item) => item.scope !== "global");
2516
- if (scoped.length === 0) {
2517
- out.push({ grant: merged });
2704
+ async function groupMembers(group, ctx, budget, seen, memo) {
2705
+ const key = JSON.stringify([
2706
+ group.resource,
2707
+ group.id,
2708
+ group.relation
2709
+ ]);
2710
+ const memoKey = `${budget}:${key}`;
2711
+ const known = memo.get(memoKey);
2712
+ if (known !== void 0) return {
2713
+ members: known,
2714
+ cut: false
2715
+ };
2716
+ let holders = ctx.reader.holders(group);
2717
+ if (holders === "pending") {
2718
+ await settle$1(ctx.cache);
2719
+ holders = ctx.reader.holders(group);
2720
+ }
2721
+ if (holders === "pending" || holders === "failed") return;
2722
+ const path = new Set(seen).add(key);
2723
+ const out = /* @__PURE__ */ new Set();
2724
+ let cut = false;
2725
+ for (const holder of holders) {
2726
+ if ("principal" in holder) {
2727
+ out.add(holder.principal.id);
2518
2728
  continue;
2519
2729
  }
2520
- for (const entry of held) if (keepsPermission(entry.membership, grant.permission.key) && scoped.every((item) => item.kind === "role" && holds(entry, item))) out.push({
2521
- grant: merged,
2522
- membership: entry.membership
2523
- });
2730
+ const nested = JSON.stringify([
2731
+ holder.group.resource,
2732
+ holder.group.id,
2733
+ holder.group.relation
2734
+ ]);
2735
+ const same = holder.group.resource === group.resource;
2736
+ if (path.has(nested)) {
2737
+ cut = true;
2738
+ continue;
2739
+ }
2740
+ if (same && budget <= 0) continue;
2741
+ const walk = await groupMembers(holder.group, ctx, same ? budget - 1 : 16, path, memo);
2742
+ if (walk === void 0) return;
2743
+ cut ||= walk.cut;
2744
+ for (const member of walk.members) out.add(member);
2524
2745
  }
2525
- for (const { grant, role } of customGrants) {
2526
- const globalHeld = holdsGlobalCustomRole(subject.principal?.roles, role);
2527
- const holders = held.filter((entry) => keepsPermission(entry.membership, grant.permission.key) && holdsCustomRole(entry.membership, role, scopes));
2528
- if (holders.length === 0 && !globalHeld) continue;
2529
- const resource = getResource(policy.permissions, grant.permission.resource);
2530
- const match = matchGrantee(grant.to, subject, now, resource, scopes, policy.resources);
2531
- if (skip(grant, match)) continue;
2532
- const merged = graphAware(grant, combineWhere(match.where, requirementOf(grant, resource)));
2533
- if (globalHeld) out.push({ grant: merged });
2534
- for (const entry of holders) out.push({
2535
- grant: merged,
2536
- membership: entry.membership
2537
- });
2746
+ const members = [...out];
2747
+ if (!cut) memo.set(memoKey, members);
2748
+ return {
2749
+ members,
2750
+ cut
2751
+ };
2752
+ }
2753
+ async function discoverGraph(condition, allow, ctx) {
2754
+ const nodes = await walkHolders(condition, ctx.row, ctx.reader, ctx.cache);
2755
+ if (nodes === void 0) {
2756
+ ctx.incomplete();
2757
+ return;
2758
+ }
2759
+ const node = ctx.policy.resources.get(condition.resource);
2760
+ const memo = /* @__PURE__ */ new Map();
2761
+ for (const entry of nodes) for (const holder of entry.holders) {
2762
+ const at = {
2763
+ resource: condition.resource,
2764
+ relation: holder.relation,
2765
+ id: entry.id
2766
+ };
2767
+ const share = isEdgeRelation(node?.relations[holder.relation]);
2768
+ if ("principal" in holder) {
2769
+ ctx.add(holder.principal.id, allow ? holderVia(holder, at, share) : void 0);
2770
+ continue;
2771
+ }
2772
+ const walk = await groupMembers(holder.group, ctx, holder.group.resource === condition.resource ? 15 : 16, /* @__PURE__ */ new Set(), memo);
2773
+ if (walk === void 0) {
2774
+ ctx.incomplete();
2775
+ continue;
2776
+ }
2777
+ for (const member of walk.members) ctx.add(member, allow ? holderVia(holder, at, share, holder.group) : void 0);
2538
2778
  }
2539
- const fresh = policy.fresh ?? [];
2540
- if (mode === "held" && subject.stale === true && fresh.length > 0) return out.filter(({ grant }) => grant.effect === "deny" || !fresh.includes(grant.permission.key));
2541
- return out;
2542
2779
  }
2543
- /**
2544
- * Declared roles a tenant admin may hand out or compose, before any
2545
- * intersection: `assignable` roles, plus every role some `assigns` lists.
2546
- */
2547
- function assignableCandidates(policy) {
2548
- const listed = new Set(policy.roles.flatMap((binding) => binding.assigns ?? []));
2549
- const names = [...listRoles(policy.vocabulary?.roles).filter((leaf) => leaf.assignable || listed.has(leaf.key)).map((leaf) => leaf.key), ...policy.roles.filter((binding) => binding.assignable || listed.has(binding.name)).map((binding) => binding.name)];
2550
- return rankRoles(policy, [...new Set(names)]).map((name) => roleLeaf(policy, name));
2780
+ async function discoverRelation(item, allow, ctx, expanded = false) {
2781
+ const condition = relationCondition(item, ctx.resource, ctx.scopes, { resources: ctx.policy.resources });
2782
+ if (condition?.op === "related") {
2783
+ await discoverGraph(condition, allow, ctx);
2784
+ return;
2785
+ }
2786
+ const spec = ctx.resource?.relations[item.relation];
2787
+ if (spec?.includes !== void 0 && !expanded) {
2788
+ for (const name of expandRelation(ctx.resource, item.relation)) await discoverRelation({
2789
+ ...item,
2790
+ relation: name
2791
+ }, allow, ctx, true);
2792
+ return;
2793
+ }
2794
+ const via = allow ? {
2795
+ kind: "relation",
2796
+ resource: ctx.permission.resource,
2797
+ relation: item.relation,
2798
+ id: relationId(ownGet(ctx.row, ctx.resource?.id ?? "id")) ?? ""
2799
+ } : void 0;
2800
+ if (isFieldRelation(spec) && spec.memberOf !== void 0) {
2801
+ const scope = resolveScope(ctx.scopes, spec.memberOf);
2802
+ const id = relationId(ownGet(ctx.row, spec.field));
2803
+ const members = scope === void 0 || id === void 0 ? [] : await ctx.list(scope, id);
2804
+ if (members === null) {
2805
+ ctx.incomplete();
2806
+ return;
2807
+ }
2808
+ for (const member of members) ctx.add(member.id, via, member.membership);
2809
+ return;
2810
+ }
2811
+ const column = isFieldRelation(spec) ? spec.field : isPrincipalRelation(spec) ? spec.principal : void 0;
2812
+ const holder = column === void 0 ? void 0 : relationId(ownGet(ctx.row, column));
2813
+ if (holder !== void 0) ctx.add(holder, via);
2551
2814
  }
2552
- /**
2553
- * The levels a held allow of a permission covers: its own `level`, every
2554
- * level when it has no row condition, or the levels whose condition equals
2555
- * its condition.
2556
- */
2557
- function heldLevels(policy, grant, names) {
2558
- if (grant.level !== void 0) return [grant.level];
2559
- if (grant.where === void 0) return names;
2560
- const held = canonicalJson(grant.where);
2561
- return names.filter((name) => {
2562
- const condition = levelCondition(policy, grant.permission.resource, name);
2563
- return condition !== void 0 && canonicalJson(condition) === held;
2564
- });
2815
+ function discover(grant, item, ctx) {
2816
+ const allow = grant.effect === "allow";
2817
+ switch (item.kind) {
2818
+ case "role": return discoverRole(item, allow, ctx);
2819
+ case "relation": return discoverRelation(item, allow, ctx);
2820
+ case "anyone":
2821
+ case "authenticated":
2822
+ case "plan":
2823
+ case "actor":
2824
+ case "assurance":
2825
+ case "inherit":
2826
+ ctx.incomplete();
2827
+ return Promise.resolve();
2828
+ default: return unknownKind(item, ctx);
2829
+ }
2565
2830
  }
2566
- /**
2567
- * What the subject may hand out in `tenant`: declared assignable roles and the
2568
- * tenant custom-role ceiling, narrowed by `allowed` (`RoleSource.assignable`),
2569
- * then intersected with the permissions the subject holds there. Holding a
2570
- * role or a granted permission with `meta.manageRoles` lifts the intersection.
2571
- */
2572
- function assignableIn(policy, subject, customRoles, customGrants, tenant, allowed, now = nowSeconds(), global = false) {
2573
- const principal = subject.principal;
2574
- if (principal === null) return {
2575
- roles: [],
2576
- permissions: [],
2577
- levels: {},
2578
- manage: false,
2579
- inert: []
2831
+ /** Holders of a grantee kind this build does not know cannot be listed. */
2832
+ function unknownKind(_item, ctx) {
2833
+ ctx.incomplete();
2834
+ return Promise.resolve();
2835
+ }
2836
+ function memberLister(source, scopes) {
2837
+ const listed = /* @__PURE__ */ new Map();
2838
+ const load = async (scope, id) => {
2839
+ if (source?.list === void 0) return null;
2840
+ try {
2841
+ return (await source.list({
2842
+ scope,
2843
+ id
2844
+ })).flatMap((entry) => typeof entry.principal?.id === "string" ? normalizeMemberships([entry.membership], scopes).map((membership) => ({
2845
+ id: entry.principal.id,
2846
+ membership
2847
+ })) : []);
2848
+ } catch {
2849
+ return null;
2850
+ }
2851
+ };
2852
+ return (scope, id) => {
2853
+ const key = JSON.stringify([scope, id]);
2854
+ const known = listed.get(key) ?? load(scope, id);
2855
+ listed.set(key, known);
2856
+ return known;
2580
2857
  };
2858
+ }
2859
+ async function whoCan(input) {
2860
+ const { policy, permission, row } = input;
2861
+ if (permission.kind !== "instance" || row === null || typeof row !== "object") return freezeDeep({
2862
+ permission: permission.key,
2863
+ holders: [],
2864
+ complete: false
2865
+ });
2581
2866
  const scopes = scopeList(policy.scopes);
2582
- const scoped = tenant === void 0 ? subject : freezeDeep(compact({
2583
- ...subject,
2584
- principal: compact({
2585
- ...principal,
2586
- tenant: resolveActiveTenant(principal, tenant, scopes, now)
2587
- })
2588
- }));
2589
- const heldGrants = collectSnapshotGrants(policy, scoped, customRoles, now, customGrants).filter((item) => item.grant.effect === "allow" && (item.membership === void 0 || !global && tenantOf(item.membership, scopes) === tenant && !isMembershipExpired(item.membership, now))).map((item) => item.grant);
2590
- const heldKeys = new Set(heldGrants.map((grant) => grant.permission.key));
2591
- const heldNames = heldRoleNames(policy, subject, tenant, void 0, now);
2592
- const quiet = {
2867
+ let complete = !input.customGrants.some((item) => item.grant.permission.key === permission.key);
2868
+ const found = /* @__PURE__ */ new Map();
2869
+ const ctx = {
2870
+ policy,
2871
+ scopes,
2872
+ resource: getResource(policy.permissions, permission.resource),
2873
+ permission,
2874
+ row,
2875
+ reader: input.reader,
2876
+ cache: input.cache,
2877
+ list: memberLister(input.memberships, scopes),
2878
+ add(id, via, membership) {
2879
+ const entry = found.get(id) ?? {
2880
+ memberships: [],
2881
+ via: []
2882
+ };
2883
+ found.set(id, entry);
2884
+ if (via !== void 0) entry.via.push(via);
2885
+ if (membership !== void 0) entry.memberships.push(membership);
2886
+ },
2887
+ incomplete() {
2888
+ complete = false;
2889
+ }
2890
+ };
2891
+ await Promise.all((policy.index.grantsByKey.get(permission.key) ?? []).flatMap((grant) => flattenGrantee(grant.to).map((item) => discover(grant, item, ctx))));
2892
+ const root = rootScope(scopes);
2893
+ const tenantKey = root === void 0 ? void 0 : scopeField(getResource(policy.permissions, permission.resource), root, scopes);
2894
+ const tenant = tenantKey === void 0 ? void 0 : relationId(ownGet(row, tenantKey));
2895
+ const env = {
2593
2896
  emit: false,
2594
2897
  simulated: true,
2595
2898
  skipAlternatives: true,
2596
- customRoles,
2597
- customGrants,
2899
+ customRoles: [],
2900
+ customGrants: [],
2598
2901
  listeners: emptyListeners(),
2599
2902
  sink: void 0,
2600
2903
  limits: void 0,
2601
2904
  limitCache: /* @__PURE__ */ new Map(),
2602
- team: void 0
2905
+ team: input.team,
2906
+ relations: input.reader
2603
2907
  };
2604
- const manage = heldNames.some((name) => roleMeta(policy, name)?.manageRoles === true) || listPermissions(policy.permissions).some((leaf) => leaf.meta.manageRoles === true && evaluate(policy, scoped, leaf, void 0, {
2605
- source: "simulate",
2606
- trusted: true,
2607
- now
2608
- }, quiet).outcome === "granted");
2609
- const narrowed = allowed === void 0 ? void 0 : new Set(allowed);
2610
- const candidates = assignableCandidates(policy).filter((leaf) => narrowed === void 0 || narrowed.has(leaf.key));
2611
- const held = new Set(heldNames);
2612
- const listed = usesAssigns(policy) ? new Set(heldNames.flatMap((name) => policy.rolesByName.get(name)?.assigns ?? [])) : void 0;
2613
- const roles = manage ? candidates : candidates.filter((leaf) => {
2614
- if (listed !== void 0) return listed.has(leaf.key);
2615
- if (held.has(leaf.key)) return true;
2616
- const keys = [...roleAllowKeys(policy, leaf.key)];
2617
- return keys.length > 0 && keys.every((key) => heldKeys.has(key));
2908
+ const now = nowSeconds();
2909
+ const ordered = [...found.entries()].toSorted(([a], [b]) => byCodePoint(a, b));
2910
+ const decideAll = () => ordered.map(([id, entry]) => {
2911
+ const subject = freezeDeep({
2912
+ principal: {
2913
+ id,
2914
+ roles: [],
2915
+ memberships: entry.memberships,
2916
+ ...tenant === void 0 ? {} : { tenant }
2917
+ },
2918
+ context: {}
2919
+ });
2920
+ return evaluate(policy, subject, permission, row, {
2921
+ source: "simulate",
2922
+ trusted: true,
2923
+ now
2924
+ }, env);
2618
2925
  });
2619
- const root = global ? "global" : rootScope(scopes);
2620
- const ceiling = new Set((root === void 0 ? [] : ceilingGrants(policy, root, allowed)).map((grant) => grant.permission.key));
2621
- const permissions = listPermissions(policy.permissions).filter((leaf) => ceiling.has(leaf.key) && (manage || heldKeys.has(leaf.key)));
2622
- const offeredLevels = (key, names) => {
2623
- if (manage) return names;
2624
- const reached = new Set(heldGrants.filter((grant) => grant.permission.key === key).flatMap((grant) => heldLevels(policy, grant, names)));
2625
- return names.filter((name) => reached.has(name));
2626
- };
2627
- const levels = {};
2628
- for (const leaf of permissions) {
2629
- const names = leaf.kind === "instance" ? levelNames(policy, leaf.resource) : [];
2630
- if (names.length > 0) levels[leaf.key] = offeredLevels(leaf.key, names);
2926
+ let decisions = decideAll();
2927
+ if (pendingRelations(input.cache).length > 0) {
2928
+ await settle$1(input.cache);
2929
+ decisions = decideAll();
2631
2930
  }
2632
- const ceilingKeys = /* @__PURE__ */ new Map();
2633
- const mayHandOut = (scope, grant) => {
2634
- const key = grant.permission.key;
2635
- let keys = ceilingKeys.get(scope);
2636
- if (keys === void 0) {
2637
- keys = new Set(ceilingGrants(policy, scope, allowed).map((item) => item.permission.key));
2638
- ceilingKeys.set(scope, keys);
2639
- }
2640
- if (!keys.has(key) || !(manage || heldKeys.has(key))) return false;
2641
- const names = grant.permission.kind === "instance" ? levelNames(policy, grant.permission.resource) : [];
2642
- if (names.length === 0) return true;
2643
- const offered = offeredLevels(key, names);
2644
- return heldLevels(policy, grant, names).every((name) => offered.includes(name));
2645
- };
2646
- const custom = global || tenant === void 0 ? {
2647
- roles: [],
2648
- inert: []
2649
- } : assignableCustomRoles(policy, customRoles, tenant, manage ? void 0 : roles, mayHandOut);
2650
- return {
2651
- roles: [...roles, ...custom.roles],
2652
- permissions,
2653
- levels,
2654
- manage,
2655
- inert: custom.inert
2656
- };
2657
- }
2658
- /**
2659
- * The tenant's custom roles the subject may hand out: held at a scope it may
2660
- * assign declared roles at (any scope with `meta.manageRoles`), allowing at
2661
- * least one permission after the ceiling, and only permissions and levels it
2662
- * may hand out at that scope. A role that allows nothing is `inert`.
2663
- */
2664
- function assignableCustomRoles(policy, customRoles, tenant, declared, mayHandOut) {
2665
- const scopes = scopeList(policy.scopes);
2666
- const root = rootScope(scopes);
2667
- const assignsAt = declared === void 0 ? void 0 : new Set(declared.map((leaf) => leaf.on === void 0 ? root : resolveScope(scopes, leaf.on)));
2668
- const out = /* @__PURE__ */ new Map();
2669
- const inert = /* @__PURE__ */ new Set();
2670
- for (const role of tenantCustomRoles(policy, customRoles)) {
2671
- if (role.tenant !== tenant || out.has(role.name)) continue;
2672
- const at = customRoleScope(role, scopes);
2673
- if (at === void 0 || assignsAt !== void 0 && !assignsAt.has(at)) continue;
2674
- const { grants } = resolveCustomRole(policy, role);
2675
- if (!grants.some((grant) => grant.effect === "allow")) {
2676
- inert.add(role.name);
2931
+ const holders = [];
2932
+ for (const [index, [id, entry]] of ordered.entries()) {
2933
+ const decision = decisions[index];
2934
+ if (decision === void 0 || decision.outcome === "denied") {
2935
+ if (decision?.outcome === "denied" && decision.denials.some((denial) => denial.reason === "relation-unavailable" || denial.reason === "relation-depth")) complete = false;
2677
2936
  continue;
2678
2937
  }
2679
- if (grants.every((grant) => grant.effect === "deny" || mayHandOut(at, grant))) out.set(role.name, synthesiseRole(role.name, {
2680
- on: at,
2681
- assignable: true
2682
- }));
2683
- }
2684
- return {
2685
- roles: [...out.values()].toSorted((a, b) => a.key.localeCompare(b.key)),
2686
- inert: [...inert].filter((name) => !out.has(name)).toSorted()
2687
- };
2688
- }
2689
- /** The one path from a resolved subject to a `Snapshot`; pure apart from the default clock. */
2690
- function snapshotOf(policy, subject, options) {
2691
- const now = options.now ?? nowSeconds();
2692
- const customGrants = options.customGrants ?? customGrantsFor(policy, options.customRoles);
2693
- const roles = heldRoleNames(policy, subject, subject.principal?.tenant, void 0, now);
2694
- const audiences = audiencesOf(policy, roles);
2695
- return buildSnapshot(compact({
2696
- subject,
2697
- roles,
2698
- audiences: audiences.length === 0 ? void 0 : audiences,
2699
- notEntitled: collectSnapshotGrants(policy, subject, options.customRoles, now, customGrants, "not-entitled"),
2700
- grants: collectSnapshotGrants(policy, subject, options.customRoles, now, customGrants).map((item) => periodBound(policy, item.grant) ? Object.assign({}, item, { grant: freezeDeep({
2701
- ...item.grant,
2702
- portable: false
2703
- }) }) : item),
2704
- include: includePrefixes(options.include),
2705
- tenants: options.tenants,
2706
- simulated: options.simulated,
2707
- now: Math.floor(now),
2708
- vocabulary: policy.vocabulary,
2709
- scopes: snapshotScopes(policy),
2710
- idOf: (name) => getResource(policy.permissions, name)?.id,
2711
- delegated: delegatedPermissions(policy.delegations, subject, new Set(roles), now),
2712
- assignable: (tenant) => {
2713
- const found = assignableIn(policy, subject, options.customRoles, customGrants, tenant, options.assignable?.get(tenant), now);
2714
- return compact({
2715
- tenant,
2716
- roles: found.roles.map((leaf) => leaf.key),
2717
- permissions: found.permissions,
2718
- levels: Object.keys(found.levels).length === 0 ? void 0 : found.levels
2719
- });
2720
- }
2721
- }));
2722
- }
2723
- /** The policy's scopes as snapshots and `permdock cloud push` carry them, with the resources each partitions. */
2724
- function snapshotScopes(policy) {
2725
- if (policy.scopes.length === 0) return;
2726
- const resources = [...policy.resources.values()];
2727
- return policy.scopes.map((scope) => {
2728
- const nodes = resources.filter((node) => partitionsOf(node, policy.scopes).includes(scope.name));
2729
- const partitioned = nodes.map((node) => node.name);
2730
- const fields = Object.fromEntries(nodes.flatMap((node) => {
2731
- const field = scopeField(node, scope.name, policy.scopes);
2732
- return field === void 0 || field === scope.key ? [] : [[node.name, field]];
2733
- }));
2734
- return compact({
2735
- name: scope.name,
2736
- key: scope.key ?? "",
2737
- within: scope.within,
2738
- resources: partitioned.length === 0 ? void 0 : partitioned,
2739
- fields: Object.keys(fields).length === 0 ? void 0 : fields
2938
+ holders.push({
2939
+ principal: { id },
2940
+ via: entry.via
2740
2941
  });
2942
+ }
2943
+ return freezeDeep({
2944
+ permission: permission.key,
2945
+ holders,
2946
+ complete
2741
2947
  });
2742
2948
  }
2949
+ //#endregion
2950
+ //#region src/core/instance.ts
2951
+ /** Each round reads one more layer: link hops, then the chain, then nested groups. */
2952
+ const LOAD_ROUNDS = 48;
2743
2953
  function buildInstance(policy, subject, envBase, team) {
2744
2954
  const listeners = emptyListeners();
2745
2955
  const queuedAuth = [...envBase.queuedAuth];
@@ -2763,27 +2973,6 @@ function buildInstance(policy, subject, envBase, team) {
2763
2973
  relations,
2764
2974
  ...envBase.approvalPolicies === void 0 ? {} : { approvalPolicies: envBase.approvalPolicies }
2765
2975
  });
2766
- /** The one `simulate` event for a batch: the worst decision, with the counts. */
2767
- const simulateEvent = (evaluated) => {
2768
- const counts = {
2769
- granted: 0,
2770
- denied: 0,
2771
- approvalRequired: 0
2772
- };
2773
- for (const [, , decision] of evaluated) if (decision.outcome === "granted") counts.granted += 1;
2774
- else if (decision.outcome === "approval-required") counts.approvalRequired += 1;
2775
- else counts.denied += 1;
2776
- const worst = evaluated.find(([, , decision]) => decision.outcome === "denied") ?? evaluated.find(([, , decision]) => decision.outcome === "approval-required") ?? evaluated[0];
2777
- if (worst === void 0) return;
2778
- const [permission, data, decision] = worst;
2779
- finish(policy, subject, permission, data, decision, {
2780
- source: "simulate",
2781
- trusted: true
2782
- }, {
2783
- ...envFor(false),
2784
- emit: true
2785
- }, true, void 0, counts);
2786
- };
2787
2976
  const decideImpl = (permission, data, options) => evaluate(policy, subject, permission, data, options ?? {}, envFor(options?.source !== "simulate", options?.source === "can"));
2788
2977
  const explainImpl = (permission, data, options) => {
2789
2978
  return decideImpl(permission, data, {
@@ -2803,39 +2992,10 @@ function buildInstance(policy, subject, envBase, team) {
2803
2992
  }
2804
2993
  };
2805
2994
  const assertImpl = (permission, data, options) => {
2806
- const decision = decideImpl(permission, data, {
2995
+ return assertDecision(policy, subject, permission, data, decideImpl(permission, data, {
2807
2996
  ...options,
2808
2997
  source: options?.source ?? "assert"
2809
- });
2810
- if (decision.outcome === "granted") return decision;
2811
- const onDenied = options?.onDenied ?? policy.onDenied;
2812
- if (onDenied !== void 0) onDenied(decision);
2813
- const resource = getResource(policy.permissions, permission.resource);
2814
- const resourceId = data !== null && typeof data === "object" ? data[resource?.id ?? "id"] : void 0;
2815
- const resourceRef = compact({
2816
- type: permission.resource,
2817
- id: resourceId === void 0 ? void 0 : String(resourceId)
2818
- });
2819
- if (decision.outcome === "approval-required") throw new PermDockApprovalRequiredError({
2820
- decision,
2821
- permission: permission.key,
2822
- scope: permission.scope,
2823
- resource: resourceRef,
2824
- message: approvalMessage(permission.key, decision.reason, decision.token)
2825
- });
2826
- if (decision.denials.some((denial) => denial.reason === "validation")) {
2827
- const detail = decision.denials[0]?.detail;
2828
- if (detail instanceof PermDockValidationError) throw detail;
2829
- }
2830
- throw new PermDockDeniedError({
2831
- decision,
2832
- permission: permission.key,
2833
- scope: permission.scope,
2834
- resource: resourceRef,
2835
- subject,
2836
- message: deniedMessage(permission.key, subject.principal?.id, decision.denials, decision.alternatives.map((leaf) => leaf.key)),
2837
- ...data === void 0 || resource === void 0 ? {} : { disclosure: resource.disclosure }
2838
- });
2998
+ }), options?.onDenied ?? policy.onDenied);
2839
2999
  };
2840
3000
  const instance = {
2841
3001
  can: canImpl,
@@ -2878,6 +3038,7 @@ function buildInstance(policy, subject, envBase, team) {
2878
3038
  token: "pd1.filter"
2879
3039
  }) : freezeDeep({
2880
3040
  outcome: "denied",
3041
+ permission: permission.key,
2881
3042
  denials: [{
2882
3043
  role: null,
2883
3044
  reason: "no-grant"
@@ -2970,27 +3131,15 @@ function buildInstance(policy, subject, envBase, team) {
2970
3131
  };
2971
3132
  if (isReadonlyArray(input)) {
2972
3133
  const decisions = input.map(([permission, data]) => quietly(permission, data));
2973
- simulateEvent(evaluated);
3134
+ simulateEvent(policy, subject, envFor(false), evaluated);
2974
3135
  return decisions;
2975
3136
  }
2976
3137
  if (isArazzoSimulateInput(input)) {
2977
3138
  const plan = simulateArazzo(input, input.permissions ?? policy.permissions, quietly);
2978
- simulateEvent(evaluated);
3139
+ simulateEvent(policy, subject, envFor(false), evaluated);
2979
3140
  return plan;
2980
3141
  }
2981
- const preview = input;
2982
- const previewRoles = preview.roles?.map((item) => typeof item === "string" ? item : item.key);
2983
- const kinds = subject.principal === null ? void 0 : applyRoleKinds(policy, previewRoles ?? subject.principal.roles, preview.memberships === void 0 ? subject.principal.memberships ?? [] : normalizeMemberships(preview.memberships, scopeList(policy.scopes)));
2984
- const previewPrincipal = subject.principal === null || kinds === void 0 ? null : freezeDeep(compact({
2985
- ...subject.principal,
2986
- roles: kinds.roles,
2987
- memberships: preview.memberships === void 0 && subject.principal.memberships === void 0 ? void 0 : kinds.memberships,
2988
- tenant: preview.tenant ?? subject.principal.tenant
2989
- }));
2990
- return buildInstance(policy, freezeDeep({
2991
- ...subject,
2992
- principal: previewPrincipal
2993
- }), {
3142
+ return buildInstance(policy, previewSubject(policy, subject, input), {
2994
3143
  ...envBase,
2995
3144
  simulated: true,
2996
3145
  relationCache
@@ -3089,32 +3238,8 @@ function buildInstance(policy, subject, envBase, team) {
3089
3238
  return activate(policy, subject, input, nowSeconds());
3090
3239
  },
3091
3240
  derive(options) {
3092
- const scopes = scopeList(policy.scopes);
3093
- const tenants = tenantsOf(subject.principal, scopes);
3094
- const auth = [];
3095
- const named = options.customRoles === void 0 ? void 0 : roleSourceFor(options.customRoles, subject, auth);
3096
- const roleSource = options.customRoles === void 0 ? envBase.roleSource : named;
3097
- const roles = options.customRoles === void 0 ? envBase.customRoles : customRolesFor(named, tenants, auth, subject.principal === null ? void 0 : subject.principal.roles ?? [], (tenant) => heldRoleNamesIn(subject.principal, scopes, tenant));
3098
- const names = options.customRoles === void 0 ? envBase.assignable : assignableNamesFor(named, tenants, auth);
3099
- const approvals = options.approvalPolicies === void 0 ? envBase.approvalPolicies : approvalPoliciesFor(policy, options.approvalPolicies, tenants, auth);
3100
- const relationSource = options.relations ?? envBase.relations;
3101
- const build = (loadedRoles, loadedNames, loadedApprovals) => buildInstance(policy, subject, compact({
3102
- ...envBase,
3103
- customRoles: loadedRoles,
3104
- roleSource,
3105
- assignable: loadedNames,
3106
- approvalPolicies: loadedApprovals,
3107
- relations: relationSource,
3108
- relationCache: options.relations === void 0 ? relationCache : void 0,
3109
- queuedAuth: auth,
3110
- queuedErrors: []
3111
- }), team);
3112
- if (isThenable(roles) || isThenable(names) || isThenable(approvals)) return Promise.all([
3113
- roles,
3114
- names,
3115
- approvals
3116
- ]).then(([loadedRoles, loadedNames, loadedApprovals]) => build(loadedRoles, loadedNames, loadedApprovals));
3117
- return build(roles, names, approvals);
3241
+ const derived = deriveEnv(policy, subject, envBase, relationCache, options);
3242
+ return isThenable(derived) ? Promise.resolve(derived).then((env) => buildInstance(policy, subject, env, team)) : buildInstance(policy, subject, derived, team);
3118
3243
  },
3119
3244
  subject
3120
3245
  };
@@ -3159,6 +3284,10 @@ function scopesReaching(policy, permission) {
3159
3284
  function challengeScope(policy, permission) {
3160
3285
  return coarseScopesFor(policy, permission)[0] ?? permission.scope;
3161
3286
  }
3287
+ /** Whether a token holding `granted` carries any of `scopes`. */
3288
+ function hasScope(granted, scopes) {
3289
+ return scopes.some((scope) => granted?.includes(scope) === true);
3290
+ }
3162
3291
  //#endregion
3163
3292
  //#region src/core/resolve-subject.ts
3164
3293
  /**
@@ -3197,7 +3326,7 @@ function assemblePrincipal(policy, user, options, auth) {
3197
3326
  } catch {
3198
3327
  principal = null;
3199
3328
  }
3200
- const contextResult = resolveContext(policy, user, context, auth);
3329
+ const contextResult = withRequestContext(options.context, resolveContext(policy, user, context, auth));
3201
3330
  let memberships = principal?.memberships ?? [];
3202
3331
  let live = false;
3203
3332
  const source = options.memberships === void 0 ? void 0 : asMembershipSource(options.memberships);
@@ -3281,7 +3410,7 @@ function finishSubject(policy, assembled, context, input, options, extra) {
3281
3410
  }));
3282
3411
  const scopes = scopeList(policy.scopes);
3283
3412
  const held = heldTenantOf(assembled.principal);
3284
- const kinds = applyRoleKinds(policy, assembled.principal.roles, normalizeMemberships(input, scopes));
3413
+ const kinds = applyRoleKinds(policy, assembled.principal.roles, checkMembershipX(normalizeMemberships(input, scopes), policyX(policy)?.membership, extra.auth));
3285
3414
  const roles = held === void 0 ? kinds.roles : [];
3286
3415
  const memberships = held === void 0 ? kinds.memberships : outsideHeld(held, options) ? [] : kinds.memberships.filter((membership) => tenantOf(membership, scopes) === held);
3287
3416
  const plans = [.../* @__PURE__ */ new Set([...assembled.principal.plans ?? [], ...extra.plans])];
@@ -3303,6 +3432,30 @@ function finishSubject(policy, assembled, context, input, options, extra) {
3303
3432
  liveSession: assembled.liveSession
3304
3433
  }));
3305
3434
  }
3435
+ /** Runs the policy's membership schema over each `x`; an invalid one is dropped and reported, never the membership. */
3436
+ function checkMembershipX(memberships, schema, auth) {
3437
+ if (schema === void 0) return memberships;
3438
+ let dropped = false;
3439
+ const checked = memberships.map((membership) => {
3440
+ if (membership.x === void 0) return membership;
3441
+ const x = readAppData(schema, membership.x);
3442
+ if (x === void 0) {
3443
+ dropped = true;
3444
+ const { x: _dropped, ...rest } = membership;
3445
+ return Object.freeze(rest);
3446
+ }
3447
+ return Object.freeze({
3448
+ ...membership,
3449
+ x: freezeCopy(x)
3450
+ });
3451
+ });
3452
+ if (dropped) auth.push({
3453
+ reason: "schema",
3454
+ source: "memberships",
3455
+ cause: "x"
3456
+ });
3457
+ return checked;
3458
+ }
3306
3459
  function activeTenantOf(policy, assembled, memberships, options) {
3307
3460
  if (assembled.principal === null) return;
3308
3461
  const held = heldTenantOf(assembled.principal);
@@ -3390,6 +3543,17 @@ function resolveContext(policy, user, fallback, auth) {
3390
3543
  return {};
3391
3544
  }
3392
3545
  }
3546
+ function withRequestContext(request, resolved) {
3547
+ if (request === void 0) return resolved;
3548
+ const base = sanitizeContext(request);
3549
+ return isThenable(resolved) ? resolved.then((value) => ({
3550
+ ...base,
3551
+ ...value
3552
+ })) : {
3553
+ ...base,
3554
+ ...resolved
3555
+ };
3556
+ }
3393
3557
  function resolveSubject(policy, user, options, auth) {
3394
3558
  const assembled = assemblePrincipal(policy, user, options, auth);
3395
3559
  const memberships = settle(assembled.memberships, [], () => {
@@ -3406,16 +3570,19 @@ function resolveSubject(policy, user, options, auth) {
3406
3570
  Promise.resolve(stale)
3407
3571
  ]).then(async ([context, resolved, isStale]) => finishSubject(policy, assembled, context, resolved, options, {
3408
3572
  stale: isStale,
3409
- plans: await withPlans(resolved)
3573
+ plans: await withPlans(resolved),
3574
+ auth
3410
3575
  }));
3411
3576
  const plans = withPlans(memberships);
3412
3577
  if (isThenable(plans)) return Promise.resolve(plans).then((resolved) => finishSubject(policy, assembled, assembled.context, memberships, options, {
3413
3578
  stale,
3414
- plans: resolved
3579
+ plans: resolved,
3580
+ auth
3415
3581
  }));
3416
3582
  return finishSubject(policy, assembled, assembled.context, memberships, options, {
3417
3583
  stale,
3418
- plans
3584
+ plans,
3585
+ auth
3419
3586
  });
3420
3587
  }
3421
3588
  //#endregion
@@ -3476,4 +3643,4 @@ function createPermDock(policy, user, options = {}) {
3476
3643
  return instantiate(policy, subject, options, auth);
3477
3644
  }
3478
3645
  //#endregion
3479
- export { countHolders as C, memoryApprovalPolicies as D, approvalPoliciesFor as E, validateApprovalPolicy as O, composeMemberships as S, APPROVAL_POLICY_UNAVAILABLE as T, leveled as _, collectSnapshotGrants as a, validateCustomRole as b, rankRoles as c, breakGlass as d, supportAccess as f, customRoleScope as g, customRoleClaim as h, scopesReaching as i, usesAssigns as l, ceilingGrants as m, resolveSubject as n, snapshotOf as o, andWhere as p, challengeScope as r, snapshotScopes as s, createPermDock as t, declaredRoleNames as u, membershipsClaim as v, isExternallyManaged as w, claimsFirst as x, resolveCustomRole as y };
3646
+ export { countHolders as C, memoryApprovalPolicies as D, approvalPoliciesFor as E, validateApprovalPolicy as O, composeMemberships as S, APPROVAL_POLICY_UNAVAILABLE as T, leveled as _, scopesReaching as a, validateCustomRole as b, collectSnapshotGrants as c, breakGlass as d, supportAccess as f, customRoleScope as g, customRoleClaim as h, hasScope as i, rankRoles as l, ceilingGrants as m, resolveSubject as n, snapshotOf as o, andWhere as p, challengeScope as r, snapshotScopes as s, createPermDock as t, usesAssigns as u, membershipsClaim as v, isExternallyManaged as w, claimsFirst as x, resolveCustomRole as y };