permdock 0.0.0 → 0.1.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 (290) hide show
  1. package/README.md +268 -1
  2. package/dist/a2a/index.d.ts +178 -0
  3. package/dist/a2a/index.js +271 -0
  4. package/dist/ai-sdk/index.d.ts +74 -0
  5. package/dist/ai-sdk/index.js +137 -0
  6. package/dist/approval-required-TzNB9oQA.js +6 -0
  7. package/dist/approvals/index.d.ts +3 -0
  8. package/dist/approvals/index.js +4 -0
  9. package/dist/approvals-DaxY-Bdg.js +218 -0
  10. package/dist/approvers-aH0PJkmf.js +20 -0
  11. package/dist/arazzo-CTeVuXr5.js +70 -0
  12. package/dist/arazzo-aoTkqYsJ.js +54 -0
  13. package/dist/authzen/index.d.ts +48 -0
  14. package/dist/authzen/index.js +336 -0
  15. package/dist/better-auth/index.d.ts +92 -0
  16. package/dist/better-auth/index.js +324 -0
  17. package/dist/budget-JqcRqyhK.d.ts +11 -0
  18. package/dist/budget-NbEtbJC_.js +11 -0
  19. package/dist/capability-B68OFnZx.js +171 -0
  20. package/dist/capability-BDG_nkxt.d.ts +99 -0
  21. package/dist/catalog/index.d.ts +12 -0
  22. package/dist/catalog/index.js +2 -0
  23. package/dist/catalog-B0E6bggc.js +48 -0
  24. package/dist/catalog-H-vFGwWK.js +55 -0
  25. package/dist/catalog-doc-BSo_Q_-6.js +298 -0
  26. package/dist/catalog-idsHc0vo.js +2 -0
  27. package/dist/catalog-path-CnI0XTFK.js +12 -0
  28. package/dist/claude-agent/index.d.ts +81 -0
  29. package/dist/claude-agent/index.js +83 -0
  30. package/dist/clerk/index.d.ts +56 -0
  31. package/dist/clerk/index.js +280 -0
  32. package/dist/cli/bin.d.ts +1 -0
  33. package/dist/cli/bin.js +20 -0
  34. package/dist/cli/index.d.ts +46 -0
  35. package/dist/cli/index.js +5 -0
  36. package/dist/cloud/index.d.ts +91 -0
  37. package/dist/cloud/index.js +390 -0
  38. package/dist/cloud-BNQTrmrr.js +50 -0
  39. package/dist/cloud-Cuxr6Czw.js +117 -0
  40. package/dist/collect-B7_TIfmg.js +49 -0
  41. package/dist/collect-O-tOtbnk.js +350 -0
  42. package/dist/collect-fRPAQIWb.js +2 -0
  43. package/dist/commands-BMh_Etm2.js +93 -0
  44. package/dist/compact-y6vfu2_f.js +19 -0
  45. package/dist/compare-pfyVnBjE.js +12 -0
  46. package/dist/compile-CQbvyIBr.d.ts +30 -0
  47. package/dist/compile-D22iwvuV.js +558 -0
  48. package/dist/config-BPYmjTOp.js +26 -0
  49. package/dist/connection-JvqwHODC.js +194 -0
  50. package/dist/context-CKEAabd7.js +73 -0
  51. package/dist/context-gtAwAILx.js +6 -0
  52. package/dist/convex/index.d.ts +40 -0
  53. package/dist/convex/index.js +55 -0
  54. package/dist/create-BKF3dgFk.d.ts +162 -0
  55. package/dist/create-CSkhDU9T.js +222 -0
  56. package/dist/credential-B_rkUNlY.js +326 -0
  57. package/dist/credentials-BgRX3WEg.js +268 -0
  58. package/dist/describe-Bt1YD5uS.js +99 -0
  59. package/dist/describe-DernyItf.d.ts +37 -0
  60. package/dist/diff-BUzf5Poi.js +48 -0
  61. package/dist/diff-x5Il8LVa.js +418 -0
  62. package/dist/digest-B3kc_PgQ.d.ts +19 -0
  63. package/dist/digest-BI-6aa6d.js +27 -0
  64. package/dist/doctor-BGle8Gqi.js +1314 -0
  65. package/dist/doctor-Bqo6FwUa.js +53 -0
  66. package/dist/drizzle/index.d.ts +72 -0
  67. package/dist/drizzle/index.js +160 -0
  68. package/dist/duration-xK2jGFVc.js +25 -0
  69. package/dist/elysia/index.d.ts +71 -0
  70. package/dist/elysia/index.js +103 -0
  71. package/dist/errors-DSMnAVNa.js +43 -0
  72. package/dist/errors-DbZ1YOnb.d.ts +138 -0
  73. package/dist/errors-Iy71s7Oj.js +177 -0
  74. package/dist/errors-m3u9TmDu.js +27 -0
  75. package/dist/evaluations-DsAU9ppl.js +479 -0
  76. package/dist/eve/index.d.ts +95 -0
  77. package/dist/eve/index.js +181 -0
  78. package/dist/express/index.d.ts +46 -0
  79. package/dist/express/index.js +108 -0
  80. package/dist/fastify/index.d.ts +56 -0
  81. package/dist/fastify/index.js +141 -0
  82. package/dist/files-B_wAGYmt.js +121 -0
  83. package/dist/fixtures-Cvaw0gDG.js +64 -0
  84. package/dist/freeze-y1_jQL4v.js +31 -0
  85. package/dist/from-snapshot-BMGnhmgv.js +552 -0
  86. package/dist/generate-CH7ThJ14.js +173 -0
  87. package/dist/grantee-Bv04_Vzf.js +521 -0
  88. package/dist/headers-B5RRv3Xm.js +6 -0
  89. package/dist/headers-C8SHQccD.d.ts +6 -0
  90. package/dist/helpers-DnSOrhB9.js +682 -0
  91. package/dist/hono/index.d.ts +70 -0
  92. package/dist/hono/index.js +131 -0
  93. package/dist/hosted-BwKQ1BTA.js +265 -0
  94. package/dist/http-DPuDaChb.d.ts +13 -0
  95. package/dist/http-GgVApX1k.js +111 -0
  96. package/dist/index-CltcQ4Bf.d.ts +138 -0
  97. package/dist/index-LF_hS-yn.d.ts +177 -0
  98. package/dist/index-fG6NSXlc.d.ts +9 -0
  99. package/dist/index-xIxJLwOQ.d.ts +415 -0
  100. package/dist/index.d.ts +11 -0
  101. package/dist/index.js +21 -0
  102. package/dist/jwt/index.d.ts +87 -0
  103. package/dist/jwt/index.js +571 -0
  104. package/dist/kernel-YOGCWNLf.js +211 -0
  105. package/dist/kysely/index.d.ts +77 -0
  106. package/dist/kysely/index.js +129 -0
  107. package/dist/load-BZ24CpZP.js +69 -0
  108. package/dist/load-QOOm1z70.js +2 -0
  109. package/dist/load-jose-BL6LH4bL.js +11 -0
  110. package/dist/lru-8q4WZjBV.js +28 -0
  111. package/dist/manifest-DDQbC6K0.d.ts +118 -0
  112. package/dist/map-DowSO96j.js +158 -0
  113. package/dist/map-claims-B_6I1Rn-.js +360 -0
  114. package/dist/map-error-RJ3ZsUyA.js +16 -0
  115. package/dist/markers-B0hYLvMD.js +60 -0
  116. package/dist/may-use-CnFEjVEp.js +37 -0
  117. package/dist/mcp/index.d.ts +127 -0
  118. package/dist/mcp/index.js +563 -0
  119. package/dist/nest/index.d.ts +80 -0
  120. package/dist/nest/index.js +270 -0
  121. package/dist/next/client.d.ts +24 -0
  122. package/dist/next/client.js +31 -0
  123. package/dist/next/index.d.ts +110 -0
  124. package/dist/next/index.js +195 -0
  125. package/dist/next/plugin.d.ts +14 -0
  126. package/dist/next/plugin.js +2 -0
  127. package/dist/node/index.d.ts +43 -0
  128. package/dist/node/index.js +66 -0
  129. package/dist/openai/index.d.ts +71 -0
  130. package/dist/openai/index.js +105 -0
  131. package/dist/openapi/index.d.ts +93 -0
  132. package/dist/openapi/index.js +3 -0
  133. package/dist/openapi-CwePaF7C.js +164 -0
  134. package/dist/openapi-DWD0Uc_4.js +250 -0
  135. package/dist/openapi-DcqM8nU7.js +306 -0
  136. package/dist/openapi-import-BWRUNNeW.js +5959 -0
  137. package/dist/openapi-schema-D8j_T9tO.js +6902 -0
  138. package/dist/orpc/index.d.ts +64 -0
  139. package/dist/orpc/index.js +188 -0
  140. package/dist/otel/index.d.ts +13 -0
  141. package/dist/otel/index.js +202 -0
  142. package/dist/package-root-zGkxO3oN.js +30 -0
  143. package/dist/parse-DPfIvFMU.js +135 -0
  144. package/dist/pdp/index.d.ts +22 -0
  145. package/dist/pdp/index.js +795 -0
  146. package/dist/peer-BOr_kJE1.js +31 -0
  147. package/dist/permdock-yoZ7EjxX.js +2956 -0
  148. package/dist/permissions-B4apnahQ.d.ts +185 -0
  149. package/dist/permissions-B8vCg_bI.d.ts +52 -0
  150. package/dist/permissions-CofRqesP.js +142 -0
  151. package/dist/permissions-DC7GROPA.js +600 -0
  152. package/dist/pg-4GJWDUxG.js +953 -0
  153. package/dist/pins-CqE1jZuF.js +12 -0
  154. package/dist/plugin-4gr7IxFJ.js +65 -0
  155. package/dist/policy-ANp36PnI.js +971 -0
  156. package/dist/policy-CInmNGkM.d.ts +1959 -0
  157. package/dist/policy-Cq2fB8Q5.d.ts +20 -0
  158. package/dist/prisma/index.d.ts +135 -0
  159. package/dist/prisma/index.js +430 -0
  160. package/dist/problem-Cgp9-KZA.js +226 -0
  161. package/dist/prompt-CXYhLfsS.js +32 -0
  162. package/dist/protected-B4K3BKoX.js +138 -0
  163. package/dist/protected-BuPAycJT.d.ts +26 -0
  164. package/dist/protected-wY7nXhY9.d.ts +23 -0
  165. package/dist/provider-_wx46HpB.js +48 -0
  166. package/dist/provider-wTAUda2v.d.ts +6 -0
  167. package/dist/react/index.d.ts +6 -0
  168. package/dist/react/index.js +6 -0
  169. package/dist/react/provider-client.d.ts +2 -0
  170. package/dist/react/provider-client.js +3 -0
  171. package/dist/react-native/index.d.ts +42 -0
  172. package/dist/react-native/index.js +174 -0
  173. package/dist/refs-ChLSxbST.js +90 -0
  174. package/dist/relations-r4soIFZB.js +426 -0
  175. package/dist/revocations-Cy06JNx2.d.ts +21 -0
  176. package/dist/rls-BOFU3chG.js +233 -0
  177. package/dist/rls-CJVja9Ww.js +2119 -0
  178. package/dist/rls-graph-DxQ39PJj.js +371 -0
  179. package/dist/rls-helpers-BxRuif0l.js +1346 -0
  180. package/dist/rls-import-CXV6LBlS.js +942 -0
  181. package/dist/rls-migrate-DrFPbf8p.js +366 -0
  182. package/dist/row-check-BajTgQZ-.d.ts +43 -0
  183. package/dist/run-CJ6UvS3a.js +155 -0
  184. package/dist/runtime-DP9ttFh1.js +27 -0
  185. package/dist/saas-BRrZzJkM.js +1193 -0
  186. package/dist/schema-P8yoma__.js +296 -0
  187. package/dist/scim/index.d.ts +2 -0
  188. package/dist/scim/index.js +2 -0
  189. package/dist/scim-9NFa0XVZ.js +1445 -0
  190. package/dist/scopes-cKVp_WTw.js +212 -0
  191. package/dist/server/index.d.ts +123 -0
  192. package/dist/server/index.js +8 -0
  193. package/dist/sha256-BO0swRXL.js +150 -0
  194. package/dist/sink-Bd9PSyfq.js +141 -0
  195. package/dist/sink-Cfr9-7YC.d.ts +104 -0
  196. package/dist/skills-C_lpV4Jx.js +50 -0
  197. package/dist/skills-Dx3QLnMY.js +110 -0
  198. package/dist/snapshot-DE8dE_hz.js +1051 -0
  199. package/dist/solid/index.d.ts +63 -0
  200. package/dist/solid/index.js +197 -0
  201. package/dist/source-DoBBYJi2.js +6 -0
  202. package/dist/sources-DOpW5Ila.d.ts +347 -0
  203. package/dist/sources-DVHM3Mwq.js +284 -0
  204. package/dist/sql-Bll1d7y0.js +22 -0
  205. package/dist/src-CTNT-PBW.js +430 -0
  206. package/dist/ssf/index.d.ts +3 -0
  207. package/dist/ssf/index.js +622 -0
  208. package/dist/store-Bq90395i.d.ts +27 -0
  209. package/dist/store-C6hFW6ta.js +508 -0
  210. package/dist/stores-CkG2HrSg.js +140 -0
  211. package/dist/stores-CvOnu_2T.d.ts +41 -0
  212. package/dist/stream-BhMfZOwr.d.ts +12 -0
  213. package/dist/stream-Q_xlB6Ko.js +52 -0
  214. package/dist/style-CTm1AXDT.js +22 -0
  215. package/dist/subject-C6dLxqHU.js +24 -0
  216. package/dist/subject-settings-xt7V845g.js +82 -0
  217. package/dist/supabase/index.d.ts +67 -0
  218. package/dist/supabase/index.js +4 -0
  219. package/dist/supabase/middleware.d.ts +77 -0
  220. package/dist/supabase/middleware.js +68 -0
  221. package/dist/supabase-D8UlBA7Z.js +83 -0
  222. package/dist/supabase-DTxGtYdi.js +796 -0
  223. package/dist/supabase-hook-DW-xP4hU.js +2 -0
  224. package/dist/supabase-hook-D_OqGb_V.js +1528 -0
  225. package/dist/svelte/Protected.svelte +34 -0
  226. package/dist/svelte/index.d.ts +10 -0
  227. package/dist/svelte/index.js +53 -0
  228. package/dist/svelte/runtime.d.ts +6 -0
  229. package/dist/svelte/runtime.js +3 -0
  230. package/dist/svelte/source.d.ts +10 -0
  231. package/dist/svelte/source.js +8 -0
  232. package/dist/terminal/index.d.ts +171 -0
  233. package/dist/terminal/index.js +821 -0
  234. package/dist/testing/index.d.ts +656 -0
  235. package/dist/testing/index.js +3208 -0
  236. package/dist/testing/saas/index.d.ts +194 -0
  237. package/dist/testing/saas/index.js +3 -0
  238. package/dist/testing/saas/permissions.d.ts +2 -0
  239. package/dist/testing/saas/permissions.js +2 -0
  240. package/dist/text-diff-BdkkYSbi.js +15 -0
  241. package/dist/thenable-okc2xDsj.js +10 -0
  242. package/dist/timeout-D51niuQP.js +8 -0
  243. package/dist/trpc/index.d.ts +80 -0
  244. package/dist/trpc/index.js +205 -0
  245. package/dist/types-B8w5PFtS.d.ts +141 -0
  246. package/dist/types-B9dmiiab.d.ts +66 -0
  247. package/dist/types-BBK-exvJ.d.ts +177 -0
  248. package/dist/types-BvBk1wgn.d.ts +118 -0
  249. package/dist/types-CxquUByI.d.ts +9 -0
  250. package/dist/types-D6Z6-rKJ.d.ts +312 -0
  251. package/dist/types-DbWdEfcm.d.ts +87 -0
  252. package/dist/types-bva3i4Gf.d.ts +98 -0
  253. package/dist/types-i4BAOvdb.d.ts +97 -0
  254. package/dist/unplugin/index.d.ts +7 -0
  255. package/dist/unplugin/index.js +30 -0
  256. package/dist/usage-1ozMubLY.js +42 -0
  257. package/dist/usage-CUnK6Ou9.js +530 -0
  258. package/dist/usage-PyEor6GA.js +2 -0
  259. package/dist/validity-D0uRmJKJ.js +224 -0
  260. package/dist/verifier-B90agnhl.js +461 -0
  261. package/dist/version-CnSh33Fj.js +16 -0
  262. package/dist/vocabulary-Dk0fs-Jk.d.ts +56 -0
  263. package/dist/vue/index.d.ts +57 -0
  264. package/dist/vue/index.js +203 -0
  265. package/dist/webmcp/index.d.ts +75 -0
  266. package/dist/webmcp/index.js +313 -0
  267. package/package.json +423 -4
  268. package/rulesets/permdock.yaml +29 -0
  269. package/schemas/catalog-v1.json +507 -0
  270. package/schemas/openapi/oas-3.1.json +1298 -0
  271. package/schemas/openapi/oas-3.2.json +1516 -0
  272. package/schemas/openapi/overlay-1.1.json +79 -0
  273. package/schemas/supabase-claims-v1.json +139 -0
  274. package/schemas/supabase-manifest-v1.json +217 -0
  275. package/skills/permdock/SKILL.md +66 -0
  276. package/skills/permdock-agents/SKILL.md +77 -0
  277. package/skills/permdock-agents/references/runtimes.md +211 -0
  278. package/skills/permdock-approvals/SKILL.md +80 -0
  279. package/skills/permdock-approvals/references/stores-and-resume.md +83 -0
  280. package/skills/permdock-audit/SKILL.md +67 -0
  281. package/skills/permdock-credentials/SKILL.md +55 -0
  282. package/skills/permdock-credentials/references/tokens-keys-links.md +127 -0
  283. package/skills/permdock-data/SKILL.md +57 -0
  284. package/skills/permdock-data/references/queries-and-rls.md +54 -0
  285. package/skills/permdock-data/references/relationships.md +65 -0
  286. package/skills/permdock-tenancy/SKILL.md +77 -0
  287. package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +83 -0
  288. package/skills/permdock-wire/SKILL.md +116 -0
  289. package/skills/permdock-wire/references/adapters.md +480 -0
  290. package/skills/permdock-wire/references/policy-options.md +43 -0
@@ -0,0 +1,2956 @@
1
+ import { n as isReadonlyArray, r as sole, t as compact } from "./compact-y6vfu2_f.js";
2
+ import { n as freezeDeep, t as freezeCopy } from "./freeze-y1_jQL4v.js";
3
+ import { C as ownGet, S as isForbiddenKey, a as formerKeys, c as getResource, d as isFieldRelation, g as listPermissions, i as findPermission, p as isPrincipalRelation, r as expandRelation, u as isEdgeRelation } from "./permissions-DC7GROPA.js";
4
+ import { a as hasConditionOp } from "./refs-ChLSxbST.js";
5
+ import { d as role, h as normalizeWhere, i as declaredRoleNames$1, l as normalizeApproval, n as assertApprovalRelations, p as applyQuota, s as grantList, u as normalizeAssurance } from "./policy-ANp36PnI.js";
6
+ import { a as resolveScope, c as scopeChain, f as tenantOf, i as normalizeMemberships, l as scopeIdOf, o as rootMembershipId, r as findScope, s as rootScope, u as scopeList } from "./scopes-cKVp_WTw.js";
7
+ import { S as isRole, T as synthesiseRole, a as authenticated, b as findRole, f as relationCondition, h as resourceRoleCondition, i as assurance, l as matchGrantee, o as combineWhere, r as asGrantee, s as flattenGrantee, w as listRoles } from "./grantee-Bv04_Vzf.js";
8
+ import { a as approvalMessage, i as PermDockValidationError, n as PermDockDeniedError, o as deniedMessage, s as wireDenials, t as PermDockApprovalRequiredError } from "./errors-Iy71s7Oj.js";
9
+ import { a as whereFromGrants, c as versionOf, d as delegatedPermissions, f as resourceIdOf, g as evaluateCondition, h as simulateArazzo, i as snapshotGrant, l as canonicalJson, m as isArazzoSimulateInput, o as decisionToken, r as signSnapshot, s as payloadDigest, t as buildSnapshot, u as coveredByDelegation } from "./snapshot-DE8dE_hz.js";
10
+ import { t as parseDuration } from "./duration-xK2jGFVc.js";
11
+ import { a as matchScopedMembership, c as partitionsOf, f as tenantsOf, h as sanitizeContext, i as isMembershipExpired, l as relatesTo, m as pickVisible, p as grantCoversField, r as inTeam, s as nowSeconds, t as isActive, u as resolveActiveTenant } from "./validity-D0uRmJKJ.js";
12
+ import { t as requiresApproval } from "./approval-required-TzNB9oQA.js";
13
+ import { r as mergeHostedGrants, t as isPortableCondition } from "./hosted-BwKQ1BTA.js";
14
+ import { n as isThenable, t as ignoreRejection } from "./thenable-okc2xDsj.js";
15
+ import { t as byCodePoint } from "./compare-pfyVnBjE.js";
16
+ import { a as relationWalk, i as relationReader, n as pendingRelations, o as resolveRelated, r as relationId } from "./relations-r4soIFZB.js";
17
+ import { i as isSubject, r as isPrincipal, t as anonymousSubject } from "./subject-C6dLxqHU.js";
18
+ //#region src/core/approval-policies.ts
19
+ function memoryApprovalPolicies(policies) {
20
+ const entries = [...policies];
21
+ return Object.freeze({ approvalPoliciesFor(query) {
22
+ return entries.filter((entry) => entry.tenant === void 0 || query.tenants.includes(entry.tenant));
23
+ } });
24
+ }
25
+ function isRecord(value) {
26
+ return value !== null && typeof value === "object" && !Array.isArray(value);
27
+ }
28
+ function isStringList(value) {
29
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
30
+ }
31
+ /**
32
+ * One raw entry checked against the policy: `null` when it names no
33
+ * permission the policy declares (it applies to nothing), `undefined` when
34
+ * it is invalid.
35
+ */
36
+ function loadOne(policy, raw) {
37
+ if (!isRecord(raw) || typeof raw["permission"] !== "string") return;
38
+ const permission = findPermission(policy.permissions, raw["permission"]);
39
+ if (permission === void 0) return null;
40
+ const { tenant, actors, where, check, approval } = raw;
41
+ if (tenant !== void 0 && typeof tenant !== "string" || actors !== void 0 && !isStringList(actors) || where !== void 0 && !isPortableCondition(where) || check !== void 0 && !isPortableCondition(check) || isRecord(approval) && approval["escalation"] !== void 0 || approval !== "human" && !isRecord(approval)) return;
42
+ let normalized;
43
+ try {
44
+ normalized = normalizeApproval(approval, permission.key);
45
+ if (normalized !== void 0) assertApprovalRelations([{
46
+ permission,
47
+ approval: normalized
48
+ }], policy.resources);
49
+ } catch {
50
+ return;
51
+ }
52
+ if (normalized === void 0) return;
53
+ if (normalized !== "human" && normalized.staleOn !== void 0 && (permission.kind !== "instance" || policy.resources.get(permission.resource)?.version === void 0)) return;
54
+ return compact({
55
+ permission: permission.key,
56
+ tenant,
57
+ actors: actors === void 0 ? void 0 : new Set(actors),
58
+ where: where === void 0 ? void 0 : normalizeWhere(where),
59
+ check: check === void 0 ? void 0 : normalizeWhere(check),
60
+ approval: normalized
61
+ });
62
+ }
63
+ function loadAll(policy, raw) {
64
+ if (!Array.isArray(raw)) return "failed";
65
+ const out = [];
66
+ for (const item of raw) {
67
+ const loaded = loadOne(policy, item);
68
+ if (loaded === void 0) return "failed";
69
+ if (loaded !== null) out.push(loaded);
70
+ }
71
+ return out;
72
+ }
73
+ /** Reads the source once for `tenants`; a throw or an invalid entry loads as `"failed"`. */
74
+ function approvalPoliciesFor(policy, source, tenants, auth) {
75
+ if (source === void 0) return;
76
+ const threw = () => {
77
+ auth.push({
78
+ reason: "source-threw",
79
+ source: "approvalPolicies"
80
+ });
81
+ return "failed";
82
+ };
83
+ let raw;
84
+ try {
85
+ raw = source.approvalPoliciesFor({ tenants });
86
+ } catch {
87
+ return threw();
88
+ }
89
+ if (isThenable(raw)) return Promise.resolve(raw).then((value) => loadAll(policy, value), threw);
90
+ return loadAll(policy, raw);
91
+ }
92
+ function matches(entry, tenant, subject, current, next, now, scopes) {
93
+ if (entry.tenant !== void 0 && entry.tenant !== tenant) return false;
94
+ if (entry.actors !== void 0 && (subject.actor === void 0 || !entry.actors.has(subject.actor.kind))) return false;
95
+ if (entry.where !== void 0 && !evaluateCondition(entry.where, current, subject, now, scopes)) return false;
96
+ return entry.check === void 0 || evaluateCondition(entry.check, next ?? current, subject, now, scopes);
97
+ }
98
+ function asStages(approval) {
99
+ if (approval === "human") return [{ by: authenticated() }];
100
+ if (approval.stages !== void 0) return approval.stages;
101
+ return [compact({
102
+ by: approval.by ?? authenticated(),
103
+ quorum: approval.quorum
104
+ })];
105
+ }
106
+ function shorter(a, b) {
107
+ if (a === void 0) return b;
108
+ if (b === void 0) return a;
109
+ return (parseDuration(b) ?? Infinity) < (parseDuration(a) ?? Infinity) ? b : a;
110
+ }
111
+ /**
112
+ * `code` with every matching entry added as further stages: the result
113
+ * needs all of them (`'sequential'` when any part is ordered), keeps the
114
+ * code's escalation, the shortest `ttl`, and `distinct` unless every part
115
+ * opts out. Without a matching entry, `code` unchanged.
116
+ */
117
+ function tightenApproval(code, entries, input) {
118
+ const extra = entries.filter((entry) => entry.permission === input.permission && matches(entry, input.tenant, input.subject, input.current, input.next, input.now, input.scopes));
119
+ if (extra.length === 0) return code;
120
+ const parts = [...code === void 0 ? [] : [code], ...extra.map((entry) => entry.approval)];
121
+ const stages = [];
122
+ const seen = /* @__PURE__ */ new Set();
123
+ let sequential = false;
124
+ let distinct = false;
125
+ let staleOn;
126
+ let ttl;
127
+ for (const part of parts) {
128
+ for (const stage of asStages(part)) {
129
+ const key = JSON.stringify(stage);
130
+ if (!seen.has(key)) {
131
+ seen.add(key);
132
+ stages.push(stage);
133
+ }
134
+ }
135
+ if (part === "human") {
136
+ distinct = true;
137
+ continue;
138
+ }
139
+ sequential ||= part.mode === "sequential";
140
+ distinct ||= part.distinct !== false;
141
+ staleOn ??= part.staleOn;
142
+ ttl = shorter(ttl, part.ttl);
143
+ }
144
+ return compact({
145
+ mode: sequential ? "sequential" : "all",
146
+ stages,
147
+ distinct: distinct ? void 0 : false,
148
+ staleOn,
149
+ ttl,
150
+ escalation: code === void 0 || code === "human" ? void 0 : code.escalation
151
+ });
152
+ }
153
+ /** The tenant a matched allow applies in: its membership's root scope, else the active tenant. */
154
+ function decisionTenant(subject, membership) {
155
+ return (membership === void 0 ? void 0 : rootMembershipId(membership)) ?? subject.principal?.tenant;
156
+ }
157
+ //#endregion
158
+ //#region src/core/memberships.ts
159
+ /** Two memberships are the same row when everything but `roles` matches. */
160
+ function identity(membership) {
161
+ const within = Object.entries(membership.within ?? {}).toSorted(([a], [b]) => byCodePoint(a, b));
162
+ return JSON.stringify([
163
+ membership.scope ?? null,
164
+ membership.id ?? null,
165
+ within,
166
+ membership.on ?? null,
167
+ membership.tenant ?? null,
168
+ membership.team ?? null,
169
+ membership.via ?? null,
170
+ membership.expiresAt ?? null,
171
+ membership.grantedBy ?? null,
172
+ membership.reason ?? null,
173
+ membership.member?.group ?? null,
174
+ [...membership.eligible ?? []].toSorted(),
175
+ membership.managedBy ?? null,
176
+ [...membership.entitlements ?? []].toSorted()
177
+ ]);
178
+ }
179
+ /**
180
+ * Merges memberships from several sources: entries naming the same instance
181
+ * with the same `via`, expiry, owner and seats become one entry with the union
182
+ * of their roles. Entries that differ in any of those stay separate, so a
183
+ * staff role and a contact role in one instance keep their own `via`.
184
+ */
185
+ function mergeMemberships(lists) {
186
+ const merged = /* @__PURE__ */ new Map();
187
+ for (const list of lists) for (const membership of list) {
188
+ const key = identity(membership);
189
+ const found = merged.get(key);
190
+ merged.set(key, found === void 0 ? membership : {
191
+ ...found,
192
+ roles: [.../* @__PURE__ */ new Set([...found.roles, ...membership.roles])]
193
+ });
194
+ }
195
+ return [...merged.values()];
196
+ }
197
+ function all(values) {
198
+ return values.some((value) => isThenable(value)) ? Promise.all(values) : values;
199
+ }
200
+ function then(value, next) {
201
+ return isThenable(value) ? Promise.resolve(value).then(next) : next(value);
202
+ }
203
+ /**
204
+ * One `MembershipSource` over several: memberships are merged and
205
+ * de-duplicated, `list` concatenates the sources that can list, and `version`
206
+ * is the highest version any source reports. A source that throws makes the
207
+ * whole lookup throw, which the subject resolver turns into no memberships.
208
+ */
209
+ function composeMemberships(sources) {
210
+ const listing = sources.filter((source) => source.list !== void 0);
211
+ const versioned = sources.filter((source) => source.version !== void 0);
212
+ return {
213
+ membershipsFor(principal, options) {
214
+ return then(all(sources.map((source) => source.membershipsFor(principal, options))), (lists) => mergeMemberships(lists));
215
+ },
216
+ ...listing.length === 0 ? {} : { list(query) {
217
+ return then(all(listing.map((source) => source.list?.(query) ?? [])), (lists) => {
218
+ const seen = /* @__PURE__ */ new Map();
219
+ for (const entry of lists.flat()) {
220
+ const key = `${entry.principal.id}\u0000${identity(entry.membership)}`;
221
+ const found = seen.get(key);
222
+ seen.set(key, found === void 0 ? entry : {
223
+ principal: entry.principal,
224
+ membership: {
225
+ ...found.membership,
226
+ roles: [.../* @__PURE__ */ new Set([...found.membership.roles, ...entry.membership.roles])]
227
+ }
228
+ });
229
+ }
230
+ return [...seen.values()];
231
+ });
232
+ } },
233
+ ...versioned.length === 0 ? {} : { version(principal) {
234
+ return then(all(versioned.map((source) => source.version?.(principal))), (versions) => {
235
+ const known = versions.filter((value) => typeof value === "number" && Number.isFinite(value));
236
+ return known.length === 0 ? void 0 : Math.max(...known);
237
+ });
238
+ } }
239
+ };
240
+ }
241
+ /** A single source, or several composed. */
242
+ function asMembershipSource(input) {
243
+ return Array.isArray(input) ? composeMemberships(input) : input;
244
+ }
245
+ /**
246
+ * Trust the memberships the verified token carries, and read `source` only
247
+ * when the token marks them truncated. With `version`, a token minted before
248
+ * the latest membership change is stale for the policy's `fresh` permissions.
249
+ */
250
+ function claimsFirst(source, options = {}) {
251
+ const inner = asMembershipSource(source);
252
+ const version = options.version ?? (inner.version === void 0 ? void 0 : (principal) => inner.version?.(principal));
253
+ return {
254
+ membershipsFor: (principal, query) => inner.membershipsFor(principal, query),
255
+ ...inner.list === void 0 ? {} : { list: (query) => inner.list?.(query) ?? [] },
256
+ ...version === void 0 ? {} : { version },
257
+ claimsFirst: true
258
+ };
259
+ }
260
+ /** Whether the identity provider owns `membership`: the application must not add, change or remove it. */
261
+ function isExternallyManaged(membership) {
262
+ return membership.managedBy === "idp";
263
+ }
264
+ //#endregion
265
+ //#region src/core/custom-roles.ts
266
+ /** The current key for a stored key, current or former; `undefined` when neither. */
267
+ function currentKey(policy, raw) {
268
+ const leaf = findPermission(policy.permissions, raw);
269
+ if (leaf === void 0) return;
270
+ return leaf.key === raw || formerKeys(leaf).includes(raw) ? leaf.key : void 0;
271
+ }
272
+ const GRANT_KEYS = /* @__PURE__ */ new Set(["permission", "effect"]);
273
+ /**
274
+ * The scope a custom role is held at: `scope` (a name or alias), the second
275
+ * scope for the `team` input shape, else the first scope. `undefined` when
276
+ * the policy does not declare it; such a role resolves to nothing.
277
+ */
278
+ function customRoleScope(role, scopes) {
279
+ if (role.scope === "global") return "global";
280
+ if (role.scope !== void 0) return resolveScope(scopes, role.scope);
281
+ return resolveScope(scopes, role.team === void 0 ? "tenant" : "team");
282
+ }
283
+ /** The instance of its scope a custom role is pinned to, if any. */
284
+ function customRoleId(role) {
285
+ return role.scope === void 0 ? role.team : role.id;
286
+ }
287
+ /** A platform custom role: held through `principal.roles`, capped by the assignable global roles. */
288
+ function isGlobalCustomRole(role) {
289
+ return role.scope === "global";
290
+ }
291
+ function isCeilingScope(scope) {
292
+ return typeof scope === "string";
293
+ }
294
+ function soleRole(grant) {
295
+ const roles = flattenGrantee(grant.to).filter((item) => item.kind === "role");
296
+ const first = roles[0];
297
+ return roles.length === 1 && first?.kind === "role" ? first.role : void 0;
298
+ }
299
+ /** Permission keys a declared role's allows reach, hosted grants included. */
300
+ function roleAllowKeys(policy, name) {
301
+ return new Set(grantList(policy).filter((grant) => grant.effect === "allow" && soleRole(grant) === name).map((grant) => grant.permission.key));
302
+ }
303
+ function isAssignableRole(policy, name) {
304
+ const binding = policy.rolesByName.get(name);
305
+ if (binding !== void 0) return binding.assignable;
306
+ return findRole(policy.vocabulary?.roles, name)?.assignable === true;
307
+ }
308
+ /**
309
+ * Every scope's ceiling, keyed by scope name (`global` for platform custom
310
+ * roles): the code allows of declared `assignable` roles in that scope,
311
+ * everything a custom role of the scope may ever reach. Hosted grants never
312
+ * widen it. `assignable` narrows it to those role names (`RoleSource.assignable`).
313
+ */
314
+ function ceilings(policy, assignable) {
315
+ const allowed = assignable === void 0 ? void 0 : new Set(assignable);
316
+ const byScope = /* @__PURE__ */ new Map();
317
+ for (const grant of grantList(policy)) {
318
+ const name = soleRole(grant);
319
+ if (grant.effect !== "allow" || grant.hosted !== void 0 || !isCeilingScope(grant.scope) || name === void 0 || !isAssignableRole(policy, name) || allowed !== void 0 && !allowed.has(name)) continue;
320
+ const list = byScope.get(grant.scope) ?? [];
321
+ list.push(grant);
322
+ byScope.set(grant.scope, list);
323
+ }
324
+ return byScope;
325
+ }
326
+ function ceilingGrants(policy, scope, assignable) {
327
+ return ceilings(policy, assignable).get(scope) ?? [];
328
+ }
329
+ function retarget(grant, role, scope) {
330
+ const grantee = {
331
+ kind: "role",
332
+ role: role.name,
333
+ scope
334
+ };
335
+ const others = flattenGrantee(grant.to).filter((item) => item.kind !== "role");
336
+ return freezeDeep({
337
+ ...grant,
338
+ to: others.length === 0 ? grantee : [grantee, ...others],
339
+ role: role.name,
340
+ scope
341
+ });
342
+ }
343
+ /**
344
+ * The one resolver behind evaluation, snapshots and `validateCustomRole`.
345
+ * Included roles' grants plus the role's own allows, minus its denies, all
346
+ * intersected with the ceiling. An allow inherits the condition, approval and
347
+ * limit of the declared grant it comes from: an included assignable role's
348
+ * grant when there is one, otherwise every ceiling grant of that permission,
349
+ * together with the denies of the roles those grants belong to.
350
+ */
351
+ function resolveCustomRole(policy, role) {
352
+ const scope = customRoleScope(role, scopeList(policy.scopes));
353
+ if (scope === void 0) return freezeDeep({
354
+ grants: [],
355
+ dropped: [],
356
+ renamed: []
357
+ });
358
+ const ceiling = ceilingGrants(policy, scope);
359
+ const ceilingSet = new Set(ceiling);
360
+ const ceilingKeys = new Set(ceiling.map((grant) => grant.permission.key));
361
+ const declared = declaredRoleNames$1(policy);
362
+ const all = grantList(policy).filter((grant) => grant.hosted === void 0);
363
+ const dropped = [];
364
+ const renamed = [];
365
+ const seen = /* @__PURE__ */ new Set();
366
+ const drop = (entry) => {
367
+ const id = "role" in entry ? `role:${entry.role}` : `${entry.reason}:${entry.permission}`;
368
+ if (!seen.has(id)) {
369
+ seen.add(id);
370
+ dropped.push(entry);
371
+ }
372
+ };
373
+ const included = [];
374
+ const includedKeys = /* @__PURE__ */ new Set();
375
+ const allowKeys = /* @__PURE__ */ new Set();
376
+ const denyKeys = /* @__PURE__ */ new Set();
377
+ for (const name of Array.isArray(role.includes) ? role.includes : []) {
378
+ if (typeof name !== "string" || !declared.has(name)) {
379
+ drop({
380
+ role: String(name),
381
+ reason: "unknown-role"
382
+ });
383
+ continue;
384
+ }
385
+ for (const grant of all) {
386
+ if (soleRole(grant) !== name) continue;
387
+ const key = grant.permission.key;
388
+ if (grant.effect === "deny") {
389
+ if (grant.scope === scope) included.push(grant);
390
+ } else if (ceilingSet.has(grant)) {
391
+ included.push(grant);
392
+ includedKeys.add(key);
393
+ } else if (ceilingKeys.has(key)) allowKeys.add(key);
394
+ else drop({
395
+ permission: key,
396
+ reason: "outside-ceiling"
397
+ });
398
+ }
399
+ }
400
+ const entries = Array.isArray(role.grants) ? role.grants : [];
401
+ for (const item of entries) {
402
+ if (item === null || typeof item !== "object") {
403
+ drop({
404
+ permission: String(item),
405
+ reason: "unknown-permission"
406
+ });
407
+ continue;
408
+ }
409
+ const entry = item;
410
+ const raw = entry.permission;
411
+ const key = typeof raw === "string" ? currentKey(policy, raw) : void 0;
412
+ if (key === void 0) {
413
+ drop({
414
+ permission: String(raw),
415
+ reason: "unknown-permission"
416
+ });
417
+ continue;
418
+ }
419
+ if (key !== raw) renamed.push({
420
+ from: String(raw),
421
+ to: key
422
+ });
423
+ const effect = entry.effect ?? "allow";
424
+ if (Object.keys(entry).some((name) => !GRANT_KEYS.has(name)) || effect !== "allow" && effect !== "deny") {
425
+ drop({
426
+ permission: key,
427
+ reason: "condition-not-allowed"
428
+ });
429
+ denyKeys.add(key);
430
+ continue;
431
+ }
432
+ if (effect === "deny") {
433
+ denyKeys.add(key);
434
+ continue;
435
+ }
436
+ if (!ceilingKeys.has(key)) {
437
+ drop({
438
+ permission: key,
439
+ reason: "outside-ceiling"
440
+ });
441
+ continue;
442
+ }
443
+ allowKeys.add(key);
444
+ }
445
+ const out = [];
446
+ const push = (grant) => {
447
+ if (!out.includes(grant)) out.push(grant);
448
+ };
449
+ for (const grant of included) if (grant.effect === "deny" || !denyKeys.has(grant.permission.key)) push(grant);
450
+ for (const key of allowKeys) {
451
+ if (denyKeys.has(key) || includedKeys.has(key)) continue;
452
+ const sources = /* @__PURE__ */ new Set();
453
+ for (const grant of ceiling) if (grant.permission.key === key) {
454
+ push(grant);
455
+ sources.add(soleRole(grant) ?? "");
456
+ }
457
+ for (const grant of all) if (grant.effect === "deny" && grant.permission.key === key && sources.has(soleRole(grant) ?? "")) push(grant);
458
+ }
459
+ return freezeDeep({
460
+ grants: out.map((grant) => retarget(grant, role, scope)),
461
+ dropped,
462
+ renamed
463
+ });
464
+ }
465
+ function validateCustomRole(policy, role) {
466
+ const resolved = resolveCustomRole(policy, role);
467
+ const permissions = [...new Set(resolved.grants.filter((grant) => grant.effect === "allow").map((grant) => grant.permission.key))].toSorted();
468
+ return freezeDeep({
469
+ ok: resolved.dropped.length === 0,
470
+ permissions,
471
+ dropped: resolved.dropped,
472
+ renamed: resolved.renamed
473
+ });
474
+ }
475
+ function wellFormed(role) {
476
+ if (role === null || typeof role !== "object" || typeof role.name !== "string") return false;
477
+ if (role.scope === "global") return role.tenant === void 0 && role.team === void 0 && role.id === void 0;
478
+ return typeof role.tenant === "string" && (role.team === void 0 || typeof role.team === "string") && (role.scope === void 0 || typeof role.scope === "string") && (role.id === void 0 || typeof role.id === "string");
479
+ }
480
+ /** Every custom role's resolved grants, once per instance. Malformed roles resolve to nothing. */
481
+ function customGrantsFor(policy, roles) {
482
+ const declared = declaredRoleNames$1(policy);
483
+ const out = [];
484
+ for (const role of roles) {
485
+ if (!wellFormed(role) || declared.has(role.name)) continue;
486
+ for (const grant of resolveCustomRole(policy, role).grants) out.push({
487
+ grant,
488
+ role
489
+ });
490
+ }
491
+ return out;
492
+ }
493
+ /** A membership at the role's scope, inside its tenant (and instance, when pinned), naming it. */
494
+ function holdsCustomRole(membership, role, scopes) {
495
+ if (isGlobalCustomRole(role)) return false;
496
+ const scope = customRoleScope(role, scopes);
497
+ const id = customRoleId(role);
498
+ return scope !== void 0 && membership.scope === scope && tenantOf(membership, scopes) === role.tenant && (id === void 0 || membership.id === id) && membership.roles.includes(role.name);
499
+ }
500
+ /** Whether `principal.roles` names the platform custom role. */
501
+ function holdsGlobalCustomRole(roles, role) {
502
+ return isGlobalCustomRole(role) && (roles ?? []).includes(role.name);
503
+ }
504
+ /** A tenant's custom role inside that tenant, or a platform custom role anywhere. */
505
+ function isCustomRoleName(name, roles, tenant) {
506
+ return roles.some((item) => wellFormed(item) && item.name === name && (isGlobalCustomRole(item) || tenant !== void 0 && item.tenant === tenant));
507
+ }
508
+ /**
509
+ * The compact JWT form of custom roles for the `memberships[].grants` claim
510
+ * RLS reads in `jwt` mode: role name to entries, where `key` allows, `-key`
511
+ * denies and `@role` includes. Put a team custom role on its team membership.
512
+ * Pass `policy` to write keys stored under a former name as the current key.
513
+ */
514
+ function customRoleClaim(roles, policy) {
515
+ const keyOf = (raw) => policy === void 0 ? raw : currentKey(policy, raw) ?? raw;
516
+ const claim = {};
517
+ for (const role of roles) {
518
+ if (!wellFormed(role) || isForbiddenKey(role.name)) continue;
519
+ const entries = claim[role.name] ?? [];
520
+ const includes = Array.isArray(role.includes) ? role.includes : [];
521
+ const grants = Array.isArray(role.grants) ? role.grants : [];
522
+ for (const name of includes) entries.push(`@${name}`);
523
+ for (const grant of grants) entries.push(grant.effect === "deny" ? `-${keyOf(grant.permission)}` : keyOf(grant.permission));
524
+ claim[role.name] = entries;
525
+ }
526
+ return freezeDeep(claim);
527
+ }
528
+ /**
529
+ * The `memberships` claim RLS reads in `jwt` mode: each membership canonical,
530
+ * with the custom roles it holds as the compact `grants` map.
531
+ */
532
+ function membershipsClaim(memberships, customRoles, scopes) {
533
+ const claim = [];
534
+ for (const membership of normalizeMemberships(memberships, scopes)) {
535
+ const held = customRoles.filter((role) => wellFormed(role) && holdsCustomRole(membership, role, scopes));
536
+ claim.push(held.length === 0 ? membership : Object.assign({}, membership, { grants: customRoleClaim(held) }));
537
+ }
538
+ return claim;
539
+ }
540
+ //#endregion
541
+ //#region src/core/elevated.ts
542
+ function flattenPermissions(input) {
543
+ if (input !== null && typeof input === "object" && "key" in input && "action" in input) return [input];
544
+ if (isReadonlyArray(input)) return input.flatMap((item) => flattenPermissions(item));
545
+ return [...listPermissions(input)];
546
+ }
547
+ /**
548
+ * The only deny override: a break-glass grant overrides deny grants whose
549
+ * `name` it lists, and nothing else. It is an allow that stays non-portable
550
+ * (RLS never compiles it) and grants only once its `requires` are met.
551
+ */
552
+ function breakGlass(permission, options = {}) {
553
+ const purpose = options.requires?.purpose === void 0 || options.requires.purpose.length === 0 ? void 0 : Object.freeze([...new Set(options.requires.purpose)]);
554
+ const spec = compact({
555
+ overrides: Object.freeze([...new Set(options.overrides ?? [])]),
556
+ purpose,
557
+ reason: options.requires?.reason === true,
558
+ assurance: normalizeAssurance(options.requires?.assurance),
559
+ maxDuration: options.maxDuration,
560
+ obligations: Object.freeze([...new Set(options.obligations ?? [])])
561
+ });
562
+ const grants = flattenPermissions(permission).map((leaf) => freezeDeep(compact({
563
+ permission: leaf,
564
+ effect: "allow",
565
+ to: authenticated(),
566
+ portable: false,
567
+ breakGlass: spec
568
+ })));
569
+ return sole(grants) ?? grants;
570
+ }
571
+ function supportAccess(options) {
572
+ const via = options.role;
573
+ const forbidden = (options.forbid ?? []).flatMap((item) => flattenPermissions(item));
574
+ const binding = role(options.role, [], {
575
+ on: options.on ?? "tenant",
576
+ for: [via]
577
+ });
578
+ const denies = forbidden.map((leaf) => freezeDeep(compact({
579
+ permission: leaf,
580
+ effect: "deny",
581
+ to: authenticated(),
582
+ role: options.role,
583
+ scope: "global",
584
+ viaOnly: via,
585
+ portable: false
586
+ })));
587
+ const support = freezeDeep(compact({
588
+ role: options.role,
589
+ actorRequired: options.actorRequired === true,
590
+ consent: {
591
+ by: asGrantee(options.consent.by),
592
+ durations: Object.freeze([...new Set(options.consent.durations)])
593
+ },
594
+ group: options.group ?? "vendor-support"
595
+ }));
596
+ return freezeDeep({
597
+ ...binding,
598
+ grants: [...binding.grants, ...denies],
599
+ support
600
+ });
601
+ }
602
+ /** Whether the subject's authentication is fresh enough for an elevated grant. */
603
+ function assuranceMet(requirement, subject, now) {
604
+ if (requirement === void 0) return true;
605
+ const held = subject.principal?.assurance;
606
+ if (requirement.acr !== void 0 && requirement.acr.length > 0) {
607
+ const acr = held?.acr;
608
+ if (acr === void 0 || !requirement.acr.includes(acr)) return false;
609
+ }
610
+ if (requirement.amr !== void 0 && requirement.amr.length > 0) {
611
+ const amr = held?.amr ?? [];
612
+ if (!requirement.amr.every((method) => amr.includes(method))) return false;
613
+ }
614
+ if (requirement.maxAge !== void 0) {
615
+ const authTime = held?.authTime;
616
+ if (authTime === void 0 || now - authTime > requirement.maxAge) return false;
617
+ }
618
+ return true;
619
+ }
620
+ /** `context.purpose` as a list of strings; fail-closed to empty. */
621
+ function purposesOf(subject) {
622
+ const value = subject.context["purpose"];
623
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" && value !== "" ? [value] : [];
624
+ }
625
+ /**
626
+ * Whether a break-glass grant fires for this subject. It engages when the
627
+ * caller asserts a purpose (`context.purpose`); once engaged, an unlisted
628
+ * purpose, a missing reason or stale authentication denies with the matching
629
+ * reason, and everything met grants with the declared obligations.
630
+ */
631
+ function evaluateBreakGlass(spec, subject, now) {
632
+ const purposes = purposesOf(subject);
633
+ if (purposes.length === 0) return { kind: "inactive" };
634
+ const required = spec.purpose;
635
+ if (required !== void 0 && !purposes.some((purpose) => required.includes(purpose))) return {
636
+ kind: "denied",
637
+ reason: "purpose"
638
+ };
639
+ const reason = subject.context["reason"];
640
+ if (spec.reason && (typeof reason !== "string" || reason === "")) return {
641
+ kind: "denied",
642
+ reason: "reason-required"
643
+ };
644
+ if (spec.assurance !== void 0 && !assuranceMet(spec.assurance, subject, now)) return {
645
+ kind: "denied",
646
+ reason: "insufficient-user-authentication",
647
+ to: assurance(spec.assurance)
648
+ };
649
+ return {
650
+ kind: "granted",
651
+ obligations: [...spec.obligations.map((kind) => ({ kind })), ...typeof reason === "string" && reason !== "" ? [{
652
+ kind: "justify",
653
+ reason
654
+ }] : []]
655
+ };
656
+ }
657
+ /** Support memberships (`via: 'support'`) the subject holds that demand an actor. */
658
+ function actorRequiredVias(supports) {
659
+ return new Set(supports.filter((spec) => spec.actorRequired).map((spec) => spec.role));
660
+ }
661
+ /** Whether a membership is a support membership of one of the given kinds. */
662
+ function isSupportMembership(membership, vias) {
663
+ return membership.via !== void 0 && vias.has(membership.via);
664
+ }
665
+ /**
666
+ * Whether the caller's `within` names the same ancestors as the eligible
667
+ * membership. Omitted means "whatever the membership holds"; any entry that
668
+ * disagrees, or names an ancestor the membership lacks, refuses.
669
+ */
670
+ function withinAgrees(scopes, held, requested) {
671
+ if (requested === void 0) return true;
672
+ return Object.entries(requested).every(([key, id]) => {
673
+ const name = resolveScope(scopes, key) ?? key;
674
+ return held !== void 0 && Object.hasOwn(held, name) && held[name] === id;
675
+ });
676
+ }
677
+ function activationDenied(reason, to) {
678
+ return freezeDeep({
679
+ outcome: "denied",
680
+ denials: [to === void 0 ? {
681
+ role: null,
682
+ reason
683
+ } : {
684
+ role: null,
685
+ reason,
686
+ to
687
+ }],
688
+ alternatives: []
689
+ });
690
+ }
691
+ /**
692
+ * Requests a just-in-time role activation. It never writes: a granted
693
+ * decision carries the elevated membership under `elevation` for the app to
694
+ * write. `approval-required` comes back first when the role's `activation`
695
+ * sets `approval`. Fail-closed: an unknown role, an ineligible subject, a
696
+ * missing justification or stale authentication all deny.
697
+ */
698
+ function activate(policy, subject, input, now = nowSeconds()) {
699
+ const roleName = isRole(input.role) ? input.role.key : input.role;
700
+ const binding = policy.rolesByName.get(roleName);
701
+ const activation = binding?.activation;
702
+ const principal = subject.principal;
703
+ if (principal === null) return activationDenied("anonymous");
704
+ if (binding === void 0 || activation === void 0) return activationDenied("unknown-role");
705
+ const scopes = scopeList(policy.scopes);
706
+ const scope = resolveScope(scopes, input.scope) ?? input.scope;
707
+ const eligible = (principal.memberships ?? []).find((membership) => membership.scope === scope && membership.id === input.id && !isMembershipExpired(membership, now) && (membership.eligible ?? []).includes(roleName) && withinAgrees(scopes, membership.within, input.within));
708
+ if (eligible === void 0) return activationDenied("no-membership");
709
+ const within = eligible.within;
710
+ if (activation.justification === "required") {
711
+ const reason = input.reason;
712
+ if (typeof reason !== "string" || reason === "") return activationDenied("reason-required");
713
+ }
714
+ if (activation.assurance !== void 0 && !assuranceMet(activation.assurance, subject, now)) return activationDenied("insufficient-user-authentication", assurance(activation.assurance));
715
+ const requested = parseDuration(input.duration);
716
+ const cap = parseDuration(activation.maxDuration);
717
+ const seconds = cap === void 0 ? requested : Math.min(requested ?? cap, cap);
718
+ const membership = freezeDeep(compact({
719
+ scope,
720
+ id: input.id,
721
+ within,
722
+ roles: [roleName],
723
+ via: "elevated",
724
+ expiresAt: seconds === void 0 ? void 0 : Math.floor(now) + seconds,
725
+ grantedBy: principal.id,
726
+ reason: input.reason
727
+ }));
728
+ const token = decisionToken({
729
+ key: `activate:${roleName}`,
730
+ resourceId: within === void 0 ? `${scope}:${input.id}` : `${scope}:${input.id}@${canonicalJson(within)}`,
731
+ principal,
732
+ actor: subject.actor,
733
+ fingerprint: policy.fingerprint
734
+ });
735
+ if (activation.approval !== void 0) return freezeDeep({
736
+ outcome: "approval-required",
737
+ grant: {
738
+ role: roleName,
739
+ permission: `activate:${roleName}`
740
+ },
741
+ reason: "human",
742
+ token
743
+ });
744
+ return freezeDeep({
745
+ outcome: "granted",
746
+ subject,
747
+ matched: {
748
+ role: roleName,
749
+ permission: `activate:${roleName}`
750
+ },
751
+ token,
752
+ elevation: membership
753
+ });
754
+ }
755
+ //#endregion
756
+ //#region src/core/events.ts
757
+ function emitSafe(listeners, payload, errors) {
758
+ for (const listener of listeners) try {
759
+ listener(payload);
760
+ } catch (error) {
761
+ for (const handler of errors.error) try {
762
+ handler(error);
763
+ } catch {}
764
+ }
765
+ }
766
+ function emptyListeners() {
767
+ return {
768
+ decision: /* @__PURE__ */ new Set(),
769
+ denied: /* @__PURE__ */ new Set(),
770
+ approval: /* @__PURE__ */ new Set(),
771
+ auth: /* @__PURE__ */ new Set(),
772
+ error: /* @__PURE__ */ new Set()
773
+ };
774
+ }
775
+ function credentialRef(subject) {
776
+ const value = subject.principal?.["credential"];
777
+ if (value === null || typeof value !== "object") return;
778
+ const { id, kind } = value;
779
+ return typeof id === "string" && (kind === "user" || kind === "service") ? {
780
+ id,
781
+ kind
782
+ } : void 0;
783
+ }
784
+ function finish(policy, subject, permission, data, decision, options, env, trusted, membership, counts) {
785
+ const heard = env.sink !== void 0 || env.listeners.decision.size > 0 || decision.outcome === "denied" && env.listeners.denied.size > 0 || decision.outcome === "approval-required" && env.listeners.approval.size > 0;
786
+ if (!env.emit || !heard) return;
787
+ const resource = getResource(policy.permissions, permission.resource);
788
+ const resourceId = data !== null && typeof data === "object" ? data[resource?.id ?? "id"] : void 0;
789
+ const event = freezeDeep(compact({
790
+ type: "decision",
791
+ at: (/* @__PURE__ */ new Date()).toISOString(),
792
+ outcome: decision.outcome,
793
+ permission: permission.key,
794
+ scope: permission.scope,
795
+ resource: compact({
796
+ type: permission.resource,
797
+ id: resourceId === void 0 ? void 0 : String(resourceId)
798
+ }),
799
+ subject: compact({
800
+ principal: subject.principal === null ? null : compact({
801
+ id: subject.principal.id,
802
+ roles: subject.principal.roles ?? [],
803
+ tenant: subject.principal.tenant
804
+ }),
805
+ actor: subject.actor === void 0 ? void 0 : {
806
+ id: subject.actor.id,
807
+ kind: subject.actor.kind
808
+ },
809
+ delegation: subject.delegation === void 0 ? void 0 : compact({
810
+ scopes: subject.delegation.scopes,
811
+ authorizationDetails: subject.delegation.authorizationDetails
812
+ }),
813
+ credential: credentialRef(subject)
814
+ }),
815
+ tenant: subject.principal?.tenant,
816
+ membership,
817
+ via: membership?.via ?? null,
818
+ matched: decision.outcome === "granted" ? compact({
819
+ role: decision.matched.role,
820
+ permission: decision.matched.permission,
821
+ to: decision.matched.to,
822
+ hosted: decision.matched.hosted,
823
+ breakGlass: decision.matched.breakGlass
824
+ }) : decision.outcome === "approval-required" ? compact({
825
+ role: decision.grant.role,
826
+ permission: decision.grant.permission,
827
+ to: decision.grant.to,
828
+ hosted: decision.grant.hosted
829
+ }) : void 0,
830
+ purpose: Array.isArray(subject.context["purpose"]) && subject.context["purpose"].length > 0 ? subject.context["purpose"] : void 0,
831
+ reason: typeof subject.context["reason"] === "string" && subject.context["reason"] !== "" ? subject.context["reason"] : void 0,
832
+ denials: decision.outcome === "denied" ? wireDenials(decision.denials) : void 0,
833
+ alternatives: decision.outcome === "denied" ? decision.alternatives.map((leaf) => leaf.key) : void 0,
834
+ token: decision.outcome === "granted" || decision.outcome === "approval-required" ? decision.token : void 0,
835
+ trusted,
836
+ source: options.source ?? "decide",
837
+ adapter: options.adapter,
838
+ counts
839
+ }));
840
+ emitSafe(env.listeners.decision, event, env.listeners);
841
+ if (decision.outcome === "denied") emitSafe(env.listeners.denied, event, env.listeners);
842
+ if (decision.outcome === "approval-required") emitSafe(env.listeners.approval, event, env.listeners);
843
+ if (env.sink !== void 0) try {
844
+ const written = env.sink.write([event]);
845
+ if (isThenable(written)) written.catch((error) => {
846
+ emitSafe(env.listeners.error, error, env.listeners);
847
+ });
848
+ } catch (error) {
849
+ emitSafe(env.listeners.error, error, env.listeners);
850
+ }
851
+ }
852
+ //#endregion
853
+ //#region src/core/validation.ts
854
+ function validateBoundary(permission, resource, data, mode, trusted, boundary = "manual") {
855
+ if (!(mode === "always" || mode === "boundary" && !trusted)) return data;
856
+ if (resource?.schema === void 0) {
857
+ if (mode === "always") throw new PermDockValidationError({
858
+ code: "no-schema",
859
+ permission: permission.key,
860
+ resource: permission.resource,
861
+ boundary,
862
+ message: `${permission.key}: no schema for resource ${permission.resource}.`
863
+ });
864
+ return data;
865
+ }
866
+ const result = resource.schema["~standard"].validate(data);
867
+ if (isThenable(result)) {
868
+ ignoreRejection(result);
869
+ throw new PermDockValidationError({
870
+ code: "async-schema",
871
+ permission: permission.key,
872
+ resource: permission.resource,
873
+ boundary,
874
+ message: `${permission.key}: schema for ${permission.resource} is async.`
875
+ });
876
+ }
877
+ if ("issues" in result && result.issues !== void 0) throw new PermDockValidationError({
878
+ code: "invalid-data",
879
+ permission: permission.key,
880
+ resource: permission.resource,
881
+ issues: result.issues,
882
+ boundary,
883
+ message: validationMessage(permission, resource, result.issues)
884
+ });
885
+ return result.value;
886
+ }
887
+ function validationMessage(permission, resource, issues) {
888
+ const parts = issues.map((issue) => {
889
+ const path = issue.path?.map((item) => typeof item === "object" && "key" in item ? String(item.key) : String(item)).join(".") ?? "/";
890
+ return `invalid ${resource.name} data at ${path}: ${issue.message}`;
891
+ });
892
+ return `${permission.key}: ${parts.join("; ")}.`;
893
+ }
894
+ //#endregion
895
+ //#region src/core/evaluate.ts
896
+ /**
897
+ * A collection write proposes `next`; an update pair may move `next` out of
898
+ * the scope `current` is in. Both must stay in scope.
899
+ */
900
+ function matchWriteScope(match, instance, current, next) {
901
+ const first = match(instance ? current : next);
902
+ if (!first.ok || !instance || next === current) return first;
903
+ const moved = match(next);
904
+ return moved.ok ? first : moved;
905
+ }
906
+ function isRowPair(value) {
907
+ return value !== null && typeof value === "object" && "current" in value && "next" in value;
908
+ }
909
+ function isGlobalRole(value) {
910
+ return value !== null && typeof value === "object" && Reflect.get(value, "scope") === "global";
911
+ }
912
+ function onlyGlobal(roles) {
913
+ return Array.isArray(roles) ? roles.filter(isGlobalRole) : [];
914
+ }
915
+ function customRolesFor(source, tenants, auth, signedIn = false) {
916
+ if (source === void 0) return [];
917
+ const loaded = [];
918
+ for (const tenant of tenants) try {
919
+ loaded.push(source.rolesFor(tenant));
920
+ } catch {
921
+ auth.push({
922
+ reason: "source-threw",
923
+ source: "customRoles"
924
+ });
925
+ loaded.push([]);
926
+ }
927
+ if (signedIn && source.globalRoles !== void 0) try {
928
+ const roles = source.globalRoles();
929
+ loaded.push(isThenable(roles) ? roles.then(onlyGlobal) : onlyGlobal(roles));
930
+ } catch {
931
+ auth.push({
932
+ reason: "source-threw",
933
+ source: "customRoles"
934
+ });
935
+ loaded.push([]);
936
+ }
937
+ if (loaded.some((item) => isThenable(item))) return Promise.all(loaded.map((item) => Promise.resolve(item).catch(() => {
938
+ auth.push({
939
+ reason: "source-threw",
940
+ source: "customRoles"
941
+ });
942
+ return [];
943
+ }))).then((lists) => lists.flat());
944
+ return loaded.flat();
945
+ }
946
+ function stringList(value) {
947
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
948
+ }
949
+ /**
950
+ * `RoleSource.assignable` per tenant; `undefined` when the source has no
951
+ * `assignable`. A tenant whose call throws or answers junk assigns nothing.
952
+ */
953
+ function assignableNamesFor(source, tenants, auth) {
954
+ if (source?.assignable === void 0) return;
955
+ const failed = () => {
956
+ auth.push({
957
+ reason: "source-threw",
958
+ source: "customRoles"
959
+ });
960
+ return [];
961
+ };
962
+ const loaded = tenants.map((tenant) => {
963
+ try {
964
+ return source.assignable?.(tenant) ?? [];
965
+ } catch {
966
+ return failed();
967
+ }
968
+ });
969
+ const toMap = (lists) => new Map(tenants.map((tenant, index) => [tenant, stringList(lists[index])]));
970
+ if (loaded.some((item) => isThenable(item))) return Promise.all(loaded.map((item) => Promise.resolve(item).catch(failed))).then(toMap);
971
+ return toMap(loaded);
972
+ }
973
+ /**
974
+ * The declared role names among `names`. A custom role of `tenant` is known
975
+ * but contributes no declared name: its grants are resolved separately
976
+ * (`customGrantsFor`), bounded by the ceiling. A tenant's custom role is
977
+ * unknown inside another tenant and for a membership without one.
978
+ */
979
+ function expandRoleNames(names, declared, custom, tenant) {
980
+ const resolved = /* @__PURE__ */ new Set();
981
+ const unknown = [];
982
+ for (const name of names) if (declared.has(name)) resolved.add(name);
983
+ else if (!isCustomRoleName(name, custom, tenant)) unknown.push(name);
984
+ return {
985
+ roles: [...resolved],
986
+ unknown
987
+ };
988
+ }
989
+ function declaredRoleNames(policy) {
990
+ return policy.index.declaredRoles;
991
+ }
992
+ function alternativesFor(policy, permission, subject, now, env) {
993
+ return listPermissions(policy.permissions).filter((leaf) => leaf.resource === permission.resource && leaf.key !== permission.key).filter((leaf) => {
994
+ return evaluate(policy, subject, leaf, void 0, {
995
+ trusted: true,
996
+ source: "simulate",
997
+ now
998
+ }, {
999
+ ...env,
1000
+ emit: false,
1001
+ skipAlternatives: true
1002
+ }).outcome === "granted";
1003
+ });
1004
+ }
1005
+ function evaluateGrantCondition(grant, permission, current, next, subject, now, scopes, relations) {
1006
+ let unknown;
1007
+ const onOpaque = () => {
1008
+ unknown ??= "opaque-condition";
1009
+ };
1010
+ const related = (condition, row) => {
1011
+ if (relations === void 0) {
1012
+ unknown ??= "relation-unavailable";
1013
+ return false;
1014
+ }
1015
+ const verdict = resolveRelated(condition, row, subject, now, relations);
1016
+ if (verdict === "relation-depth" || verdict === "relation-unavailable") {
1017
+ unknown ??= verdict;
1018
+ return false;
1019
+ }
1020
+ return verdict;
1021
+ };
1022
+ if (!grant.portable && grant.closure !== void 0) try {
1023
+ const result = grant.closure(next ?? current, {
1024
+ subject,
1025
+ actor: subject.actor,
1026
+ delegation: subject.delegation,
1027
+ context: subject.context
1028
+ });
1029
+ if (isThenable(result)) return {
1030
+ matched: false,
1031
+ reason: "closure-error",
1032
+ cause: result
1033
+ };
1034
+ return { matched: result === true };
1035
+ } catch (error) {
1036
+ return {
1037
+ matched: false,
1038
+ reason: "closure-error",
1039
+ cause: error
1040
+ };
1041
+ }
1042
+ if (grant.where !== void 0) {
1043
+ if (permission.kind === "collection") return {
1044
+ matched: false,
1045
+ reason: "condition"
1046
+ };
1047
+ if (current === void 0) return {
1048
+ matched: false,
1049
+ reason: "condition"
1050
+ };
1051
+ if (grant.where.op === "opaque" || grant.check?.op === "opaque") return {
1052
+ matched: false,
1053
+ reason: "opaque-condition"
1054
+ };
1055
+ const matched = evaluateCondition(grant.where, current, subject, now, scopes, related, onOpaque);
1056
+ if (unknown !== void 0) return {
1057
+ matched: false,
1058
+ reason: unknown
1059
+ };
1060
+ if (!matched) return {
1061
+ matched: false,
1062
+ reason: "condition"
1063
+ };
1064
+ }
1065
+ const checkCondition = grant.check ?? (permission.action === "update" ? grant.where : void 0);
1066
+ if (checkCondition !== void 0) {
1067
+ if (checkCondition.op === "opaque") return {
1068
+ matched: false,
1069
+ reason: "opaque-condition"
1070
+ };
1071
+ if (next === void 0) return {
1072
+ matched: false,
1073
+ reason: "condition"
1074
+ };
1075
+ const matched = evaluateCondition(checkCondition, next, subject, now, scopes, related, onOpaque);
1076
+ if (unknown !== void 0) return {
1077
+ matched: false,
1078
+ reason: unknown
1079
+ };
1080
+ if (!matched) return {
1081
+ matched: false,
1082
+ reason: "condition"
1083
+ };
1084
+ }
1085
+ return { matched: true };
1086
+ }
1087
+ /** A grant whose condition could not be answered; a deny with one denies. */
1088
+ function isUnevaluable(reason) {
1089
+ return reason === "relation-depth" || reason === "relation-unavailable" || reason === "closure-error" || reason === "opaque-condition";
1090
+ }
1091
+ function shouldConsumeQuota(source, simulated) {
1092
+ if (simulated) return false;
1093
+ switch (source) {
1094
+ case "can":
1095
+ case "filter":
1096
+ case "simulate":
1097
+ case "explain": return false;
1098
+ case "decide":
1099
+ case "assert":
1100
+ case "endpoint":
1101
+ case "adapter":
1102
+ case "approval":
1103
+ case void 0: return true;
1104
+ default: return source;
1105
+ }
1106
+ }
1107
+ function isDelegatedPermission(policy, permission) {
1108
+ const providers = policy.providers;
1109
+ if (providers === void 0 || providers.length === 0) return false;
1110
+ for (const provider of providers) if (provider.handles(permission)) return true;
1111
+ return false;
1112
+ }
1113
+ /** The `MatchedGrant` view of a grant, shared by `matched` and the trace. */
1114
+ function matchedOf(grant, permissionKey, breakGlass) {
1115
+ return compact({
1116
+ role: grant.role,
1117
+ permission: permissionKey,
1118
+ name: grant.name,
1119
+ to: grant.to,
1120
+ where: grant.where,
1121
+ check: grant.check,
1122
+ approval: grant.approval,
1123
+ hosted: grant.hosted,
1124
+ breakGlass
1125
+ });
1126
+ }
1127
+ function skip(tracer, grant, permissionKey, why) {
1128
+ tracer?.skipped.push({
1129
+ role: grant.role,
1130
+ permission: permissionKey,
1131
+ effect: grant.effect,
1132
+ why
1133
+ });
1134
+ }
1135
+ function traceOf(tracer) {
1136
+ return {
1137
+ evaluated: tracer.evaluated,
1138
+ allows: tracer.allows,
1139
+ denies: tracer.denies,
1140
+ skipped: tracer.skipped
1141
+ };
1142
+ }
1143
+ function evaluate(policy, subject, permission, data, options, env) {
1144
+ const now = options.now ?? nowSeconds();
1145
+ const trusted = options.trusted === true;
1146
+ const resource = getResource(policy.permissions, permission.resource);
1147
+ let current = data;
1148
+ let next = data;
1149
+ const tracer = options.explain === true ? {
1150
+ evaluated: 0,
1151
+ allows: [],
1152
+ denies: [],
1153
+ skipped: []
1154
+ } : void 0;
1155
+ const complete = (decision, membership) => {
1156
+ const final = freezeDeep(tracer === void 0 ? decision : {
1157
+ ...decision,
1158
+ trace: traceOf(tracer)
1159
+ });
1160
+ finish(policy, subject, permission, current, final, options, env, trusted, membership);
1161
+ return final;
1162
+ };
1163
+ if (permission.kind === "instance" && isRowPair(data)) {
1164
+ current = data.current;
1165
+ next = data.next;
1166
+ }
1167
+ if (permission.kind === "collection") {
1168
+ current = void 0;
1169
+ next = data;
1170
+ }
1171
+ try {
1172
+ if (data !== void 0 || permission.kind === "instance" && policy.validate === "always") {
1173
+ const validated = validateBoundary(permission, resource, permission.kind === "instance" && isRowPair(data) ? data.current : data, policy.validate, trusted, options.boundary ?? "manual");
1174
+ if (permission.kind === "instance" && isRowPair(data)) {
1175
+ current = validated;
1176
+ next = validateBoundary(permission, resource, data.next, policy.validate, trusted, options.boundary ?? "manual");
1177
+ } else if (permission.kind === "instance") {
1178
+ current = validated;
1179
+ next = validated;
1180
+ } else next = validated;
1181
+ }
1182
+ } catch (error) {
1183
+ if (error instanceof PermDockValidationError && error.code === "invalid-data") return complete({
1184
+ outcome: "denied",
1185
+ denials: [{
1186
+ role: null,
1187
+ reason: "validation",
1188
+ detail: error
1189
+ }],
1190
+ alternatives: []
1191
+ });
1192
+ throw error;
1193
+ }
1194
+ if (isDelegatedPermission(policy, permission)) return complete({
1195
+ outcome: "denied",
1196
+ denials: [{
1197
+ role: null,
1198
+ reason: "pdp-unavailable",
1199
+ detail: "use permdock/pdp createPermDock"
1200
+ }],
1201
+ alternatives: []
1202
+ });
1203
+ const scopes = scopeList(policy.scopes);
1204
+ const declared = declaredRoleNames(policy);
1205
+ const principalRoles = subject.principal?.roles ?? [];
1206
+ const globalNames = expandRoleNames(principalRoles, declared, env.customRoles, subject.principal?.tenant);
1207
+ if (subject.principal !== null && globalNames.unknown.length > 0) emitSafe(env.listeners.auth, {
1208
+ reason: "unknown-role",
1209
+ source: "roles"
1210
+ }, env.listeners);
1211
+ const denials = [];
1212
+ const allows = [];
1213
+ const matchingRoles = new Set(globalNames.roles);
1214
+ for (const membership of subject.principal?.memberships ?? []) {
1215
+ if (!inTeam(membership, scopes, env.team)) continue;
1216
+ const expanded = expandRoleNames(membership.roles, declared, env.customRoles, tenantOf(membership, scopes));
1217
+ for (const name of expanded.roles) matchingRoles.add(name);
1218
+ for (const name of expanded.unknown) denials.push({
1219
+ role: name,
1220
+ reason: "unknown-role"
1221
+ });
1222
+ }
1223
+ const actorVias = actorRequiredVias(policy.index.supports);
1224
+ 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({
1225
+ outcome: "denied",
1226
+ denials: [{
1227
+ role: null,
1228
+ reason: "actor-required"
1229
+ }],
1230
+ alternatives: []
1231
+ });
1232
+ const purposes = purposesOf(subject);
1233
+ const forPermission = policy.index.grantsByKey.get(permission.key) ?? [];
1234
+ const candidates = forPermission.filter((grant) => grant.breakGlass === void 0).map((grant) => ({ grant }));
1235
+ for (const item of env.customGrants) if (item.grant.permission.key === permission.key) candidates.push({
1236
+ grant: item.grant,
1237
+ custom: item.role
1238
+ });
1239
+ let breakGlassGrant;
1240
+ let breakGlassObligations = [];
1241
+ const breakGlassOverrides = /* @__PURE__ */ new Set();
1242
+ let breakGlassDenial;
1243
+ for (const grant of forPermission) {
1244
+ if (grant.breakGlass === void 0) continue;
1245
+ if (tracer !== void 0) tracer.evaluated += 1;
1246
+ if (!matchGrantee(grant.to, subject, now, resource, scopes).matched) {
1247
+ skip(tracer, grant, permission.key, "grantee");
1248
+ continue;
1249
+ }
1250
+ const result = evaluateBreakGlass(grant.breakGlass, subject, now);
1251
+ if (result.kind === "inactive") {
1252
+ skip(tracer, grant, permission.key, "break-glass-inactive");
1253
+ continue;
1254
+ }
1255
+ for (const name of grant.breakGlass.overrides) breakGlassOverrides.add(name);
1256
+ if (result.kind === "granted") {
1257
+ breakGlassGrant = grant;
1258
+ breakGlassObligations = result.obligations;
1259
+ break;
1260
+ }
1261
+ breakGlassDenial ??= result.reason === "insufficient-user-authentication" ? {
1262
+ role: null,
1263
+ reason: result.reason,
1264
+ to: result.to
1265
+ } : {
1266
+ role: null,
1267
+ reason: result.reason
1268
+ };
1269
+ }
1270
+ const holdsCustom = (custom) => isGlobalCustomRole(custom) ? holdsGlobalCustomRole(principalRoles, custom) : (subject.principal?.memberships ?? []).some((membership) => inTeam(membership, scopes, env.team) && holdsCustomRole(membership, custom, scopes));
1271
+ const walkRole = env.relations?.available === true ? (membership, row) => {
1272
+ const on = membership.on;
1273
+ const condition = on === void 0 ? void 0 : resourceRoleCondition(resource, on.resource, [on.id], policy.resources);
1274
+ return condition === void 0 || env.relations === void 0 ? false : resolveRelated(condition, row, subject, now, env.relations);
1275
+ } : void 0;
1276
+ for (const { grant, custom } of candidates) {
1277
+ const displayRole = grant.role;
1278
+ if (tracer !== void 0) tracer.evaluated += 1;
1279
+ if (grant.viaOnly !== void 0 && !(subject.principal?.memberships ?? []).some((membership) => membership.via === grant.viaOnly && !isMembershipExpired(membership, now) && inTeam(membership, scopes, env.team))) {
1280
+ skip(tracer, grant, permission.key, "via-only");
1281
+ continue;
1282
+ }
1283
+ const required = grant.purpose;
1284
+ if (required !== void 0 && !purposes.some((purpose) => required.includes(purpose))) {
1285
+ skip(tracer, grant, permission.key, "purpose");
1286
+ continue;
1287
+ }
1288
+ const granteeMatch = matchGrantee(grant.to, subject, now, resource, scopes, policy.resources, grant.effect === "deny");
1289
+ if (!granteeMatch.matched) {
1290
+ const reason = granteeMatch.reason ?? "no-grant";
1291
+ 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)))) {
1292
+ skip(tracer, grant, permission.key, "grantee");
1293
+ continue;
1294
+ }
1295
+ denials.push((reason === "insufficient-user-authentication" || reason === "not-entitled") && grant.to !== void 0 ? {
1296
+ role: displayRole,
1297
+ reason,
1298
+ to: grant.to
1299
+ } : {
1300
+ role: displayRole,
1301
+ reason
1302
+ });
1303
+ continue;
1304
+ }
1305
+ const roleItems = flattenGrantee(grant.to).filter((item) => item.kind === "role");
1306
+ let scopeMembership;
1307
+ let roleOk = roleItems.length === 0;
1308
+ if (roleItems.length > 0) {
1309
+ if (subject.principal === null) {
1310
+ denials.push({
1311
+ role: displayRole,
1312
+ reason: "anonymous"
1313
+ });
1314
+ continue;
1315
+ }
1316
+ let allHeld = true;
1317
+ for (const roleItem of roleItems) {
1318
+ if (!(custom === void 0 ? roleItem.scope === "global" ? globalNames.roles.includes(roleItem.role) : matchingRoles.has(roleItem.role) : holdsCustom(custom))) {
1319
+ skip(tracer, grant, permission.key, "role");
1320
+ allHeld = false;
1321
+ break;
1322
+ }
1323
+ const matchRow = (row) => matchScopedMembership(subject, roleItem.scope, roleItem.role, row, scopes, resource, policy.resources, now, (membership) => {
1324
+ if (custom === void 0) return expandRoleNames(membership.roles, declared, env.customRoles, tenantOf(membership, scopes)).roles;
1325
+ return holdsCustomRole(membership, custom, scopes) ? [custom.name] : [];
1326
+ }, env.team, walkRole);
1327
+ const scopeMatch = matchWriteScope(matchRow, permission.kind === "instance", current, next);
1328
+ if (!scopeMatch.ok) {
1329
+ denials.push({
1330
+ role: roleItem.role,
1331
+ reason: scopeMatch.reason
1332
+ });
1333
+ allHeld = false;
1334
+ break;
1335
+ }
1336
+ scopeMembership = scopeMatch.membership ?? scopeMembership;
1337
+ }
1338
+ roleOk = allHeld;
1339
+ }
1340
+ if (!roleOk) continue;
1341
+ if (!isActive(grant.validity, now)) {
1342
+ if (grant.effect === "allow") denials.push({
1343
+ role: displayRole,
1344
+ reason: "inactive-grant",
1345
+ detail: grant.validity
1346
+ });
1347
+ else skip(tracer, grant, permission.key, "validity");
1348
+ continue;
1349
+ }
1350
+ const merged = freezeDeep(compact({
1351
+ ...grant,
1352
+ where: combineWhere(grant.where, granteeMatch.where)
1353
+ }));
1354
+ const condition = evaluateGrantCondition(merged, permission, current, next, subject, now, scopes, env.relations);
1355
+ if (!condition.matched && grant.effect === "deny" && isUnevaluable(condition.reason) && grantCoversField(grant.fields, options.field, grant.effect)) {
1356
+ if (condition.reason === "closure-error") emitSafe(env.listeners.error, condition.cause ?? /* @__PURE__ */ new Error("closure-error"), env.listeners);
1357
+ tracer?.denies.push(matchedOf(merged, permission.key));
1358
+ return complete({
1359
+ outcome: "denied",
1360
+ denials: [{
1361
+ role: displayRole,
1362
+ reason: condition.reason ?? "relation-unavailable"
1363
+ }],
1364
+ alternatives: []
1365
+ });
1366
+ }
1367
+ if (!condition.matched) {
1368
+ if (condition.reason === "closure-error") emitSafe(env.listeners.error, condition.cause ?? /* @__PURE__ */ new Error("closure-error"), env.listeners);
1369
+ denials.push({
1370
+ role: displayRole,
1371
+ reason: condition.reason ?? "condition",
1372
+ detail: condition.cause
1373
+ });
1374
+ continue;
1375
+ }
1376
+ if (!grantCoversField(grant.fields, options.field, grant.effect)) {
1377
+ skip(tracer, grant, permission.key, "field");
1378
+ continue;
1379
+ }
1380
+ if (grant.effect === "deny") {
1381
+ tracer?.denies.push(matchedOf(merged, permission.key));
1382
+ if (grant.name !== void 0 && breakGlassOverrides.has(grant.name)) {
1383
+ if (breakGlassGrant !== void 0) continue;
1384
+ if (breakGlassDenial !== void 0) return complete({
1385
+ outcome: "denied",
1386
+ denials: [breakGlassDenial],
1387
+ alternatives: env.skipAlternatives ? [] : alternativesFor(policy, permission, subject, now, env)
1388
+ });
1389
+ }
1390
+ return complete({
1391
+ outcome: "denied",
1392
+ denials: [grant.name === void 0 ? {
1393
+ role: displayRole,
1394
+ reason: "deny"
1395
+ } : {
1396
+ role: displayRole,
1397
+ reason: "deny",
1398
+ detail: { name: grant.name }
1399
+ }],
1400
+ alternatives: env.skipAlternatives ? [] : alternativesFor(policy, permission, subject, now, env)
1401
+ });
1402
+ }
1403
+ tracer?.allows.push(matchedOf(merged, permission.key));
1404
+ allows.push(scopeMembership === void 0 ? { grant: merged } : {
1405
+ grant: merged,
1406
+ membership: scopeMembership
1407
+ });
1408
+ }
1409
+ if (breakGlassGrant !== void 0) {
1410
+ tracer?.allows.push(matchedOf(breakGlassGrant, permission.key, true));
1411
+ allows.push({
1412
+ grant: breakGlassGrant,
1413
+ obligations: breakGlassObligations,
1414
+ breakGlass: true
1415
+ });
1416
+ } else if (breakGlassDenial !== void 0) denials.unshift(breakGlassDenial);
1417
+ if (allows.length === 0) {
1418
+ const reason = denials[0]?.reason ?? (subject.principal === null ? "anonymous" : globalNames.unknown.length > 0 && globalNames.roles.length === 0 ? "unknown-role" : "no-grant");
1419
+ return complete({
1420
+ outcome: "denied",
1421
+ denials: denials.length > 0 ? denials : [{
1422
+ role: null,
1423
+ reason
1424
+ }],
1425
+ alternatives: env.skipAlternatives ? [] : alternativesFor(policy, permission, subject, now, env)
1426
+ });
1427
+ }
1428
+ if (subject.stale === true && (policy.fresh ?? []).includes(permission.key)) return complete({
1429
+ outcome: "denied",
1430
+ denials: [{
1431
+ role: null,
1432
+ reason: "stale-credentials"
1433
+ }],
1434
+ alternatives: []
1435
+ });
1436
+ const ceiling = delegatedPermissions(policy.delegations, subject, matchingRoles, now);
1437
+ if (ceiling !== void 0 && !ceiling.has(permission.key)) return complete({
1438
+ outcome: "denied",
1439
+ denials: [{
1440
+ role: null,
1441
+ reason: "not-delegated"
1442
+ }],
1443
+ alternatives: env.skipAlternatives ? [] : alternativesFor(policy, permission, subject, now, env)
1444
+ });
1445
+ const delegationMiss = coveredByDelegation(permission, subject.delegation, resourceIdOf(current), subject.actor !== void 0 && ceiling === void 0);
1446
+ if (delegationMiss !== void 0) return complete({
1447
+ outcome: "denied",
1448
+ denials: [{
1449
+ role: null,
1450
+ reason: delegationMiss
1451
+ }],
1452
+ alternatives: env.skipAlternatives ? [] : alternativesFor(policy, permission, subject, now, env)
1453
+ });
1454
+ const approvalPolicies = env.approvalPolicies;
1455
+ if (approvalPolicies === "failed" && allows.length > 0) return complete({
1456
+ outcome: "denied",
1457
+ denials: [{
1458
+ role: null,
1459
+ reason: "approval",
1460
+ detail: "approval-policy-unavailable"
1461
+ }],
1462
+ alternatives: []
1463
+ });
1464
+ const approvalOf = (candidate) => approvalPolicies === void 0 || approvalPolicies === "failed" ? candidate.grant.approval : tightenApproval(candidate.grant.approval, approvalPolicies, {
1465
+ permission: permission.key,
1466
+ tenant: decisionTenant(subject, candidate.membership),
1467
+ subject,
1468
+ current,
1469
+ next,
1470
+ now,
1471
+ scopes: scopeList(policy.scopes)
1472
+ });
1473
+ const quotaDenials = [];
1474
+ let matchedAllow;
1475
+ let approval;
1476
+ let quotaState = {};
1477
+ for (const candidate of allows) {
1478
+ approval = approvalOf(candidate);
1479
+ const consume = !requiresApproval(approval) && shouldConsumeQuota(options.source, env.simulated);
1480
+ const quota = applyQuota({
1481
+ store: env.limits,
1482
+ cache: env.limitCache,
1483
+ grant: candidate.grant,
1484
+ permissionKey: permission.key,
1485
+ subjectId: subject.principal?.id ?? "",
1486
+ tenant: subject.principal?.tenant,
1487
+ now,
1488
+ consume
1489
+ });
1490
+ if (quota.ok) {
1491
+ matchedAllow = candidate;
1492
+ quotaState = compact({
1493
+ quota: quota.quota,
1494
+ obligations: quota.obligations
1495
+ });
1496
+ break;
1497
+ }
1498
+ quotaDenials.push(quota.reason === "limit" ? {
1499
+ role: candidate.grant.role,
1500
+ reason: "limit",
1501
+ detail: quota.detail
1502
+ } : {
1503
+ role: candidate.grant.role,
1504
+ reason: quota.reason
1505
+ });
1506
+ }
1507
+ if (matchedAllow === void 0) return complete({
1508
+ outcome: "denied",
1509
+ denials: quotaDenials.length > 0 ? quotaDenials : denials,
1510
+ alternatives: env.skipAlternatives ? [] : alternativesFor(policy, permission, subject, now, env)
1511
+ });
1512
+ const resourceId = permission.kind === "collection" ? "*" : current !== null && typeof current === "object" ? String(current[resource?.id ?? "id"] ?? "*") : "*";
1513
+ const version = approval !== void 0 && approval !== "human" && approval.staleOn === "resource-change" && resource?.version !== void 0 ? versionOf(current, resource.version) : void 0;
1514
+ const token = env.simulated ? "pd1.simulated" : decisionToken({
1515
+ key: permission.key,
1516
+ resourceId,
1517
+ principal: subject.principal,
1518
+ actor: subject.actor,
1519
+ fingerprint: policy.fingerprint,
1520
+ version,
1521
+ payload: resourceId === "*" && (next ?? current) !== void 0 ? payloadDigest(next ?? current) : void 0
1522
+ });
1523
+ const matched = compact({
1524
+ ...matchedOf(matchedAllow.grant, permission.key, matchedAllow.breakGlass),
1525
+ approval
1526
+ });
1527
+ const obligations = [...quotaState.obligations ?? [], ...matchedAllow.obligations ?? []];
1528
+ return complete(requiresApproval(approval) ? {
1529
+ outcome: "approval-required",
1530
+ grant: matched,
1531
+ reason: "human",
1532
+ token
1533
+ } : {
1534
+ outcome: "granted",
1535
+ subject,
1536
+ matched,
1537
+ token,
1538
+ ...quotaState,
1539
+ ...obligations.length === 0 ? {} : { obligations }
1540
+ }, matchedAllow.membership);
1541
+ }
1542
+ //#endregion
1543
+ //#region src/core/ownership.ts
1544
+ function bindingOf(policy, name) {
1545
+ return policy.rolesByName.get(name);
1546
+ }
1547
+ function roleMeta(policy, name) {
1548
+ return bindingOf(policy, name)?.meta ?? findRole(policy.vocabulary?.roles, name)?.meta;
1549
+ }
1550
+ /** A role with `for` is held only through a membership of one of those kinds. */
1551
+ function mayHoldVia(policy, role, via) {
1552
+ const kinds = bindingOf(policy, role)?.for;
1553
+ return kinds === void 0 || via !== void 0 && kinds.includes(via);
1554
+ }
1555
+ function sameList(a, b) {
1556
+ return a.length === b.length && a.every((item, index) => item === b[index]);
1557
+ }
1558
+ /**
1559
+ * The identity provider may only hand out what the application could: a
1560
+ * declared role with `assignable: false` (an owner, a platform role) never
1561
+ * rides in on a `managedBy: 'idp'` membership. A name the policy does not
1562
+ * declare stays, because only a tenant's custom role can give it meaning.
1563
+ */
1564
+ function mayHoldManaged(policy, role, membership) {
1565
+ return !isExternallyManaged(membership) || bindingOf(policy, role)?.assignable !== false;
1566
+ }
1567
+ /**
1568
+ * Drops every role a membership's kind may not hold (fail-closed: a missing
1569
+ * `via` is no kind) and every non-assignable role on an identity-provider
1570
+ * membership. Global roles have no kind, so a role with `for` held globally
1571
+ * is dropped too.
1572
+ */
1573
+ function applyRoleKinds(policy, roles, memberships) {
1574
+ const kinds = policy.roles.some((binding) => binding.for !== void 0);
1575
+ if (!kinds && !memberships.some(isExternallyManaged)) return {
1576
+ roles,
1577
+ memberships
1578
+ };
1579
+ return {
1580
+ roles: kinds ? roles?.filter((name) => mayHoldVia(policy, name, void 0)) ?? roles : roles,
1581
+ memberships: memberships.map((membership) => {
1582
+ const allowed = membership.roles.filter((name) => mayHoldVia(policy, name, membership.via) && mayHoldManaged(policy, name, membership));
1583
+ return sameList(allowed, membership.roles) ? membership : freezeDeep({
1584
+ ...membership,
1585
+ roles: allowed
1586
+ });
1587
+ })
1588
+ };
1589
+ }
1590
+ function usesAssigns(policy) {
1591
+ return policy.roles.some((binding) => binding.assigns !== void 0);
1592
+ }
1593
+ /** Declared role names in declaration order: bindings first, then vocabulary-only leaves. */
1594
+ function declarationOrder(policy) {
1595
+ const names = policy.roles.map((binding) => binding.name);
1596
+ const seen = new Set(names);
1597
+ for (const leaf of listRoles(policy.vocabulary?.roles)) if (!seen.has(leaf.key)) {
1598
+ names.push(leaf.key);
1599
+ seen.add(leaf.key);
1600
+ }
1601
+ return names;
1602
+ }
1603
+ /**
1604
+ * Each role's depth in the `assigns` graph: a role nobody else assigns is 0,
1605
+ * a role another assigns sits one below its deepest assigner. Cycles stop
1606
+ * after one pass per role.
1607
+ */
1608
+ function assignDepths(policy) {
1609
+ const names = declarationOrder(policy);
1610
+ const depth = new Map(names.map((name) => [name, 0]));
1611
+ for (let pass = 0; pass < names.length; pass += 1) {
1612
+ let changed = false;
1613
+ for (const binding of policy.roles) {
1614
+ const from = depth.get(binding.name) ?? 0;
1615
+ for (const target of binding.assigns ?? []) if (target !== binding.name && (depth.get(target) ?? 0) < from + 1) {
1616
+ depth.set(target, from + 1);
1617
+ changed = true;
1618
+ }
1619
+ }
1620
+ if (!changed) break;
1621
+ }
1622
+ return depth;
1623
+ }
1624
+ /**
1625
+ * `names` ordered by rank: assigners before the roles they assign, then
1626
+ * declaration order. Without an `assigns` graph the order is kept.
1627
+ */
1628
+ function rankRoles(policy, names) {
1629
+ if (!usesAssigns(policy)) return names;
1630
+ const order = declarationOrder(policy);
1631
+ const depth = assignDepths(policy);
1632
+ const index = (name) => {
1633
+ const found = order.indexOf(name);
1634
+ return found === -1 ? order.length : found;
1635
+ };
1636
+ return [...names].toSorted((a, b) => (depth.get(a) ?? Number.MAX_SAFE_INTEGER) - (depth.get(b) ?? Number.MAX_SAFE_INTEGER) || index(a) - index(b));
1637
+ }
1638
+ /** The distinct `meta.audience` values of `names`, in rank order. */
1639
+ function audiencesOf(policy, names) {
1640
+ const out = [];
1641
+ for (const name of rankRoles(policy, names)) {
1642
+ const audience = roleMeta(policy, name)?.audience;
1643
+ if (typeof audience === "string" && !out.includes(audience)) out.push(audience);
1644
+ }
1645
+ return out;
1646
+ }
1647
+ /** Whether `membership` is the instance itself or one of its ancestors named in `within`. */
1648
+ function coversInstance(membership, scopes, scope, id, within) {
1649
+ if (membership.scope === void 0 || membership.id === void 0) return false;
1650
+ if (membership.scope === scope) return membership.id === id;
1651
+ if (!scopeChain(scopes, scope).slice(1).includes(membership.scope)) return false;
1652
+ const wanted = within === void 0 ? void 0 : Object.entries(within).find(([key]) => resolveScope(scopes, key) === membership.scope)?.[1] ?? void 0;
1653
+ return wanted !== void 0 && scopeIdOf(membership, membership.scope) === wanted;
1654
+ }
1655
+ /** Roles the principal holds at the instance, at an ancestor named in `within`, or globally. */
1656
+ function rolesAtInstance(policy, principal, scopes, scope, id, within, now) {
1657
+ const declared = declaredRoleNames$1(policy);
1658
+ const names = new Set((principal.roles ?? []).filter((name) => declared.has(name)));
1659
+ 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);
1660
+ return rankRoles(policy, [...names]);
1661
+ }
1662
+ /** Whether the principal holds `role` on a live membership of exactly this instance. */
1663
+ function holdsAt(principal, role, scope, id, now) {
1664
+ return (principal.memberships ?? []).some((membership) => membership.scope === scope && membership.id === id && !isMembershipExpired(membership, now) && membership.roles.includes(role));
1665
+ }
1666
+ function conflictsWith(policy, role) {
1667
+ const out = new Set(bindingOf(policy, role)?.exclusiveWith ?? []);
1668
+ for (const binding of policy.roles) if (binding.exclusiveWith?.includes(role) === true) out.add(binding.name);
1669
+ return out;
1670
+ }
1671
+ /** What the change does to the instance's holder count. */
1672
+ function countRules(binding, role, holders, delta, deny) {
1673
+ if (binding === void 0 || delta === 0) return;
1674
+ const min = binding.min ?? 0;
1675
+ if (!(min > 0 || binding.max !== void 0 || binding.transferOnly === true)) return;
1676
+ if (!(holders !== void 0 && Number.isInteger(holders) && holders >= 0)) {
1677
+ deny(delta < 0 && min > 0 ? "last-holder" : delta > 0 && binding.max !== void 0 ? "max-holders" : "transfer-only", {
1678
+ role,
1679
+ holders: null
1680
+ });
1681
+ return;
1682
+ }
1683
+ const after = holders + delta;
1684
+ if (delta < 0 && min > 0 && after < min) deny("last-holder", {
1685
+ role,
1686
+ min,
1687
+ holders
1688
+ });
1689
+ if (delta > 0 && binding.max !== void 0 && after > binding.max) deny("max-holders", {
1690
+ role,
1691
+ max: binding.max,
1692
+ holders
1693
+ });
1694
+ if (binding.transferOnly === true && holders > 0 && after > 0) deny("transfer-only", {
1695
+ role,
1696
+ holders
1697
+ });
1698
+ }
1699
+ /**
1700
+ * A live membership of the principal on the instance or on an instance under
1701
+ * it: the one in-band fact that ties a nested instance to a tenant.
1702
+ */
1703
+ function heldAnchor(principal, scopes, scope, id, now) {
1704
+ return (principal.memberships ?? []).find((membership) => !isMembershipExpired(membership, now) && membership.scope !== void 0 && scopeChain(scopes, membership.scope).includes(scope) && scopeIdOf(membership, scope) === id && tenantOf(membership, scopes) !== void 0);
1705
+ }
1706
+ /**
1707
+ * The one rule check behind `permdock.decideRoleChange`. The actor is always
1708
+ * the instance's own principal; nothing in `change` can stand in for it.
1709
+ */
1710
+ function decideRoleChange(policy, principal, scopes, change, authority, now, options = {}) {
1711
+ const name = typeof change?.role === "string" ? change.role : change?.role?.key;
1712
+ const denials = [];
1713
+ const deny = (reason, detail) => {
1714
+ denials.push(compact({
1715
+ role: name ?? null,
1716
+ reason,
1717
+ detail
1718
+ }));
1719
+ };
1720
+ const done = (role) => freezeDeep(denials.length === 0 ? {
1721
+ outcome: "granted",
1722
+ change,
1723
+ role
1724
+ } : {
1725
+ outcome: "denied",
1726
+ change,
1727
+ denials
1728
+ });
1729
+ if (principal === null) {
1730
+ deny("anonymous");
1731
+ return done(null);
1732
+ }
1733
+ const kind = change?.kind;
1734
+ const target = change?.target;
1735
+ if (typeof name !== "string" || kind !== "assign" && kind !== "revoke" && kind !== "transfer" || typeof change.id !== "string" || change.id === "" || target === null || typeof target !== "object" || typeof target.id !== "string" || target.id === "") {
1736
+ deny("validation");
1737
+ return done(null);
1738
+ }
1739
+ const binding = bindingOf(policy, name);
1740
+ if (!declaredRoleNames$1(policy).has(name)) {
1741
+ deny("unknown-role");
1742
+ return done(null);
1743
+ }
1744
+ const scope = resolveScope(scopes, change.scope);
1745
+ const on = binding?.on ?? findRole(policy.vocabulary?.roles, name)?.on;
1746
+ const heldAt = typeof on === "string" ? resolveScope(scopes, on) : void 0;
1747
+ if (scope === void 0 || heldAt === void 0 || heldAt !== scope) {
1748
+ deny("scope", { expected: heldAt ?? null });
1749
+ return done(null);
1750
+ }
1751
+ const root = scopes[0]?.name;
1752
+ const supplied = change.within === void 0 ? void 0 : Object.entries(change.within).find(([key]) => resolveScope(scopes, key) === root)?.[1];
1753
+ let tenant;
1754
+ let within = change.within;
1755
+ if (scope === root) tenant = change.id;
1756
+ else {
1757
+ const anchor = heldAnchor(principal, scopes, scope, change.id, now);
1758
+ const held = anchor === void 0 ? void 0 : tenantOf(anchor, scopes);
1759
+ if (held !== void 0) {
1760
+ tenant = supplied === void 0 || supplied === held ? held : void 0;
1761
+ within = anchor?.within;
1762
+ } else if (options.trusted === true) tenant = supplied;
1763
+ }
1764
+ if (tenant === void 0) {
1765
+ deny("no-membership", {
1766
+ scope,
1767
+ id: change.id
1768
+ });
1769
+ return done(null);
1770
+ }
1771
+ if (target.managedBy === "idp") {
1772
+ deny("externally-managed");
1773
+ return done(null);
1774
+ }
1775
+ const self = target.id === principal.id;
1776
+ const targetRoles = isReadonlyArray(target.roles) ? target.roles : [];
1777
+ const already = targetRoles.includes(name);
1778
+ let by = null;
1779
+ if (kind === "transfer") {
1780
+ if (self) deny("self-demotion");
1781
+ if (holdsAt(principal, name, scope, change.id, now)) by = name;
1782
+ else deny("not-assignable-by", { holds: false });
1783
+ } else {
1784
+ if (self) deny(kind === "revoke" ? "self-demotion" : "not-assignable-by", { self: true });
1785
+ const allowed = authority(tenant);
1786
+ if (!allowed.assignable.has(name)) deny("not-assignable-by");
1787
+ else if (usesAssigns(policy) && !allowed.manage) {
1788
+ const held = rolesAtInstance(policy, principal, scopes, scope, change.id, within, now);
1789
+ by = held.find((role) => bindingOf(policy, role)?.assigns?.includes(name) === true) ?? null;
1790
+ if (by === null) deny("not-assignable-by", { held });
1791
+ }
1792
+ }
1793
+ if (kind !== "revoke" && !already) {
1794
+ if (!mayHoldVia(policy, name, target.via)) deny("not-allowed-for-membership", {
1795
+ via: target.via ?? null,
1796
+ for: binding?.for
1797
+ });
1798
+ const conflicts = conflictsWith(policy, name);
1799
+ const clash = targetRoles.filter((role) => conflicts.has(role));
1800
+ if (clash.length > 0) deny("conflicting-role", { with: clash });
1801
+ }
1802
+ const delta = kind === "assign" ? already ? 0 : 1 : kind === "revoke" ? targetRoles.length > 0 && !already ? 0 : -1 : already ? -1 : 0;
1803
+ countRules(binding, name, change.holders, delta, deny);
1804
+ return done(by);
1805
+ }
1806
+ //#endregion
1807
+ //#region src/core/who-can.ts
1808
+ const MAX_ROUNDS = 48;
1809
+ async function settle$1(cache) {
1810
+ for (let round = 0; round < MAX_ROUNDS; round += 1) {
1811
+ const pending = pendingRelations(cache);
1812
+ if (pending.length === 0) return;
1813
+ await Promise.all(pending);
1814
+ }
1815
+ }
1816
+ /** The holders on each instance a graph relation reads, or `undefined` when the graph cannot say. */
1817
+ async function walkHolders(condition, row, reader, cache) {
1818
+ let walk = relationWalk(condition, row, reader);
1819
+ if (walk === "relation-unavailable") {
1820
+ await settle$1(cache);
1821
+ walk = relationWalk(condition, row, reader);
1822
+ }
1823
+ if (walk === void 0) return [];
1824
+ if (walk === "relation-depth" || walk === "relation-unavailable") return;
1825
+ const { ids } = walk;
1826
+ const read = () => ids.map((id) => reader.holders({
1827
+ resource: condition.resource,
1828
+ id,
1829
+ relation: condition.relation
1830
+ }));
1831
+ let answers = read();
1832
+ if (answers.includes("pending")) {
1833
+ await settle$1(cache);
1834
+ answers = read();
1835
+ }
1836
+ const out = [];
1837
+ for (const [index, answer] of answers.entries()) {
1838
+ const id = ids[index];
1839
+ if (answer === "pending" || answer === "failed" || id === void 0) return;
1840
+ out.push({
1841
+ id,
1842
+ holders: answer
1843
+ });
1844
+ }
1845
+ return out;
1846
+ }
1847
+ async function discoverRole(item, allow, ctx) {
1848
+ if (typeof item.scope !== "string" || item.scope === "global") {
1849
+ ctx.incomplete();
1850
+ return;
1851
+ }
1852
+ const key = findScope(ctx.scopes, item.scope)?.key;
1853
+ const id = key === void 0 ? void 0 : relationId(ownGet(ctx.row, key));
1854
+ if (id === void 0) return;
1855
+ const members = await ctx.list(item.scope, id);
1856
+ if (members === null) {
1857
+ ctx.incomplete();
1858
+ return;
1859
+ }
1860
+ for (const member of members.filter((entry) => entry.membership.roles.includes(item.role))) ctx.add(member.id, allow ? {
1861
+ kind: "role",
1862
+ role: item.role,
1863
+ membership: member.membership
1864
+ } : void 0, member.membership);
1865
+ }
1866
+ function holderVia(holder, at, share, group) {
1867
+ if (!share) return {
1868
+ kind: "relation",
1869
+ ...at
1870
+ };
1871
+ return compact({
1872
+ kind: "share",
1873
+ ...at,
1874
+ expiresAt: holder.expiresAt,
1875
+ group
1876
+ });
1877
+ }
1878
+ /**
1879
+ * Principals holding `group`'s relation, through at most `budget` more groups
1880
+ * of its resource; `undefined` when a read failed. `memo` keeps each group's
1881
+ * members per budget across one discovery, so a group shared by many paths is
1882
+ * read once; a result cut short by a cycle is path-specific and not kept.
1883
+ */
1884
+ async function groupMembers(group, ctx, budget, seen, memo) {
1885
+ const key = JSON.stringify([
1886
+ group.resource,
1887
+ group.id,
1888
+ group.relation
1889
+ ]);
1890
+ const memoKey = `${budget}:${key}`;
1891
+ const known = memo.get(memoKey);
1892
+ if (known !== void 0) return {
1893
+ members: known,
1894
+ cut: false
1895
+ };
1896
+ let holders = ctx.reader.holders(group);
1897
+ if (holders === "pending") {
1898
+ await settle$1(ctx.cache);
1899
+ holders = ctx.reader.holders(group);
1900
+ }
1901
+ if (holders === "pending" || holders === "failed") return;
1902
+ const path = new Set(seen).add(key);
1903
+ const out = /* @__PURE__ */ new Set();
1904
+ let cut = false;
1905
+ for (const holder of holders) {
1906
+ if ("principal" in holder) {
1907
+ out.add(holder.principal.id);
1908
+ continue;
1909
+ }
1910
+ const nested = JSON.stringify([
1911
+ holder.group.resource,
1912
+ holder.group.id,
1913
+ holder.group.relation
1914
+ ]);
1915
+ const same = holder.group.resource === group.resource;
1916
+ if (path.has(nested)) {
1917
+ cut = true;
1918
+ continue;
1919
+ }
1920
+ if (same && budget <= 0) continue;
1921
+ const walk = await groupMembers(holder.group, ctx, same ? budget - 1 : 16, path, memo);
1922
+ if (walk === void 0) return;
1923
+ cut ||= walk.cut;
1924
+ for (const member of walk.members) out.add(member);
1925
+ }
1926
+ const members = [...out];
1927
+ if (!cut) memo.set(memoKey, members);
1928
+ return {
1929
+ members,
1930
+ cut
1931
+ };
1932
+ }
1933
+ async function discoverGraph(condition, allow, ctx) {
1934
+ const nodes = await walkHolders(condition, ctx.row, ctx.reader, ctx.cache);
1935
+ if (nodes === void 0) {
1936
+ ctx.incomplete();
1937
+ return;
1938
+ }
1939
+ const node = ctx.policy.resources.get(condition.resource);
1940
+ const memo = /* @__PURE__ */ new Map();
1941
+ for (const entry of nodes) for (const holder of entry.holders) {
1942
+ const at = {
1943
+ resource: condition.resource,
1944
+ relation: holder.relation,
1945
+ id: entry.id
1946
+ };
1947
+ const share = isEdgeRelation(node?.relations[holder.relation]);
1948
+ if ("principal" in holder) {
1949
+ ctx.add(holder.principal.id, allow ? holderVia(holder, at, share) : void 0);
1950
+ continue;
1951
+ }
1952
+ const walk = await groupMembers(holder.group, ctx, holder.group.resource === condition.resource ? 15 : 16, /* @__PURE__ */ new Set(), memo);
1953
+ if (walk === void 0) {
1954
+ ctx.incomplete();
1955
+ continue;
1956
+ }
1957
+ for (const member of walk.members) ctx.add(member, allow ? holderVia(holder, at, share, holder.group) : void 0);
1958
+ }
1959
+ }
1960
+ async function discoverRelation(item, allow, ctx, expanded = false) {
1961
+ const condition = relationCondition(item, ctx.resource, ctx.scopes, { resources: ctx.policy.resources });
1962
+ if (condition?.op === "related") {
1963
+ await discoverGraph(condition, allow, ctx);
1964
+ return;
1965
+ }
1966
+ const spec = ctx.resource?.relations[item.relation];
1967
+ if (spec?.includes !== void 0 && !expanded) {
1968
+ for (const name of expandRelation(ctx.resource, item.relation)) await discoverRelation({
1969
+ ...item,
1970
+ relation: name
1971
+ }, allow, ctx, true);
1972
+ return;
1973
+ }
1974
+ const via = allow ? {
1975
+ kind: "relation",
1976
+ resource: ctx.permission.resource,
1977
+ relation: item.relation,
1978
+ id: relationId(ownGet(ctx.row, ctx.resource?.id ?? "id")) ?? ""
1979
+ } : void 0;
1980
+ if (isFieldRelation(spec) && spec.memberOf !== void 0) {
1981
+ const scope = resolveScope(ctx.scopes, spec.memberOf);
1982
+ const id = relationId(ownGet(ctx.row, spec.field));
1983
+ const members = scope === void 0 || id === void 0 ? [] : await ctx.list(scope, id);
1984
+ if (members === null) {
1985
+ ctx.incomplete();
1986
+ return;
1987
+ }
1988
+ for (const member of members) ctx.add(member.id, via, member.membership);
1989
+ return;
1990
+ }
1991
+ const column = isFieldRelation(spec) ? spec.field : isPrincipalRelation(spec) ? spec.principal : void 0;
1992
+ const holder = column === void 0 ? void 0 : relationId(ownGet(ctx.row, column));
1993
+ if (holder !== void 0) ctx.add(holder, via);
1994
+ }
1995
+ function discover(grant, item, ctx) {
1996
+ const allow = grant.effect === "allow";
1997
+ switch (item.kind) {
1998
+ case "role": return discoverRole(item, allow, ctx);
1999
+ case "relation": return discoverRelation(item, allow, ctx);
2000
+ case "anyone":
2001
+ case "authenticated":
2002
+ case "plan":
2003
+ case "actor":
2004
+ case "assurance":
2005
+ ctx.incomplete();
2006
+ return Promise.resolve();
2007
+ default: return unknownKind(item, ctx);
2008
+ }
2009
+ }
2010
+ /** Holders of a grantee kind this build does not know cannot be listed. */
2011
+ function unknownKind(_item, ctx) {
2012
+ ctx.incomplete();
2013
+ return Promise.resolve();
2014
+ }
2015
+ function memberLister(source, scopes) {
2016
+ const listed = /* @__PURE__ */ new Map();
2017
+ const load = async (scope, id) => {
2018
+ if (source?.list === void 0) return null;
2019
+ try {
2020
+ return (await source.list({
2021
+ scope,
2022
+ id
2023
+ })).flatMap((entry) => typeof entry.principal?.id === "string" ? normalizeMemberships([entry.membership], scopes).map((membership) => ({
2024
+ id: entry.principal.id,
2025
+ membership
2026
+ })) : []);
2027
+ } catch {
2028
+ return null;
2029
+ }
2030
+ };
2031
+ return (scope, id) => {
2032
+ const key = JSON.stringify([scope, id]);
2033
+ const known = listed.get(key) ?? load(scope, id);
2034
+ listed.set(key, known);
2035
+ return known;
2036
+ };
2037
+ }
2038
+ async function whoCan(input) {
2039
+ const { policy, permission, row } = input;
2040
+ if (permission.kind !== "instance" || row === null || typeof row !== "object") return freezeDeep({
2041
+ permission: permission.key,
2042
+ holders: [],
2043
+ complete: false
2044
+ });
2045
+ const scopes = scopeList(policy.scopes);
2046
+ let complete = !input.customGrants.some((item) => item.grant.permission.key === permission.key);
2047
+ const found = /* @__PURE__ */ new Map();
2048
+ const ctx = {
2049
+ policy,
2050
+ scopes,
2051
+ resource: getResource(policy.permissions, permission.resource),
2052
+ permission,
2053
+ row,
2054
+ reader: input.reader,
2055
+ cache: input.cache,
2056
+ list: memberLister(input.memberships, scopes),
2057
+ add(id, via, membership) {
2058
+ const entry = found.get(id) ?? {
2059
+ memberships: [],
2060
+ via: []
2061
+ };
2062
+ found.set(id, entry);
2063
+ if (via !== void 0) entry.via.push(via);
2064
+ if (membership !== void 0) entry.memberships.push(membership);
2065
+ },
2066
+ incomplete() {
2067
+ complete = false;
2068
+ }
2069
+ };
2070
+ await Promise.all((policy.index.grantsByKey.get(permission.key) ?? []).flatMap((grant) => flattenGrantee(grant.to).map((item) => discover(grant, item, ctx))));
2071
+ const root = rootScope(scopes);
2072
+ const tenantKey = root === void 0 ? void 0 : findScope(scopes, root)?.key;
2073
+ const tenant = tenantKey === void 0 ? void 0 : relationId(ownGet(row, tenantKey));
2074
+ const env = {
2075
+ emit: false,
2076
+ simulated: true,
2077
+ skipAlternatives: true,
2078
+ customRoles: [],
2079
+ customGrants: [],
2080
+ listeners: emptyListeners(),
2081
+ sink: void 0,
2082
+ limits: void 0,
2083
+ limitCache: /* @__PURE__ */ new Map(),
2084
+ team: input.team,
2085
+ relations: input.reader
2086
+ };
2087
+ const now = nowSeconds();
2088
+ const ordered = [...found.entries()].toSorted(([a], [b]) => byCodePoint(a, b));
2089
+ const decideAll = () => ordered.map(([id, entry]) => {
2090
+ const subject = freezeDeep({
2091
+ principal: {
2092
+ id,
2093
+ roles: [],
2094
+ memberships: entry.memberships,
2095
+ ...tenant === void 0 ? {} : { tenant }
2096
+ },
2097
+ context: {}
2098
+ });
2099
+ return evaluate(policy, subject, permission, row, {
2100
+ source: "simulate",
2101
+ trusted: true,
2102
+ now
2103
+ }, env);
2104
+ });
2105
+ let decisions = decideAll();
2106
+ if (pendingRelations(input.cache).length > 0) {
2107
+ await settle$1(input.cache);
2108
+ decisions = decideAll();
2109
+ }
2110
+ const holders = [];
2111
+ for (const [index, [id, entry]] of ordered.entries()) {
2112
+ const decision = decisions[index];
2113
+ if (decision === void 0 || decision.outcome === "denied") {
2114
+ if (decision?.outcome === "denied" && decision.denials.some((denial) => denial.reason === "relation-unavailable" || denial.reason === "relation-depth")) complete = false;
2115
+ continue;
2116
+ }
2117
+ holders.push({
2118
+ principal: { id },
2119
+ via: entry.via
2120
+ });
2121
+ }
2122
+ return freezeDeep({
2123
+ permission: permission.key,
2124
+ holders,
2125
+ complete
2126
+ });
2127
+ }
2128
+ //#endregion
2129
+ //#region src/core/instance.ts
2130
+ /** Each round reads one more layer: link hops, then the chain, then nested groups. */
2131
+ const LOAD_ROUNDS = 48;
2132
+ /** A grant on a relation with a `period` is decided on the server, where the clock is. */
2133
+ function periodBound(policy, grant) {
2134
+ const relations = policy.resources.get(grant.permission.resource)?.relations;
2135
+ return flattenGrantee(grant.to).some((item) => {
2136
+ if (item.kind !== "relation") return false;
2137
+ const spec = relations?.[item.relation];
2138
+ return isPrincipalRelation(spec) && spec.period !== void 0;
2139
+ });
2140
+ }
2141
+ function includePrefixes(include) {
2142
+ if (include === void 0) return;
2143
+ return include.map((item) => {
2144
+ if ("key" in item && typeof item.key === "string") return item.key;
2145
+ const first = listPermissions(item)[0];
2146
+ if (first === void 0) return "";
2147
+ const parts = first.key.split(".");
2148
+ parts.pop();
2149
+ return parts.join(".");
2150
+ });
2151
+ }
2152
+ /**
2153
+ * Role names held in `tenant` on live memberships, global roles included, in
2154
+ * rank order. With `scope`, only the roles of that scope's memberships (and
2155
+ * instance `id`).
2156
+ */
2157
+ function heldRoleNames(policy, subject, tenant, only, now) {
2158
+ if (subject.principal === null) return [];
2159
+ const scopes = scopeList(policy.scopes);
2160
+ const scope = only?.scope === void 0 ? void 0 : resolveScope(scopes, only.scope);
2161
+ if (only?.scope !== void 0 && scope === void 0) return [];
2162
+ const names = new Set(scope === void 0 ? subject.principal.roles ?? [] : []);
2163
+ for (const membership of subject.principal.memberships ?? []) {
2164
+ if (isMembershipExpired(membership, now)) continue;
2165
+ if (!(scope === void 0 ? tenantOf(membership, scopes) === tenant : membership.scope === scope && (only?.id === void 0 || membership.id === only.id))) continue;
2166
+ for (const role of membership.roles) names.add(role);
2167
+ }
2168
+ return rankRoles(policy, [...names]);
2169
+ }
2170
+ function roleLeaf(policy, name) {
2171
+ const binding = policy.rolesByName.get(name);
2172
+ return findRole(policy.vocabulary?.roles, name) ?? synthesiseRole(name, compact({
2173
+ on: typeof binding?.on === "string" ? binding.on : void 0,
2174
+ assignable: binding?.assignable,
2175
+ meta: binding?.meta
2176
+ }));
2177
+ }
2178
+ /**
2179
+ * The grant with its grantee's row condition. A graph relation needs the
2180
+ * `RelationSource`, which snapshots and `where()` do not carry, so the grant
2181
+ * becomes server-only (`portable: false`) there.
2182
+ */
2183
+ function graphAware(grant, where) {
2184
+ const combined = combineWhere(grant.where, where);
2185
+ const graph = hasConditionOp(combined, "related");
2186
+ return freezeDeep(compact({
2187
+ ...grant,
2188
+ where: combined,
2189
+ portable: graph ? false : grant.portable,
2190
+ graph: graph && grant.portable ? true : void 0
2191
+ }));
2192
+ }
2193
+ /**
2194
+ * `where()` keeps a grant that is server-only because it reads the graph:
2195
+ * its `related` nodes compile to subqueries given relation mappings.
2196
+ */
2197
+ function whereGrant(grant) {
2198
+ return grant.graph === true ? freezeDeep({
2199
+ ...grant,
2200
+ portable: true
2201
+ }) : grant;
2202
+ }
2203
+ /**
2204
+ * Every grant the subject may reach, once per membership that holds all of
2205
+ * its scoped roles, so a client can check that membership's tenant, team or
2206
+ * resource. A scoped role held only globally reaches nothing.
2207
+ */
2208
+ function collectSnapshotGrants(policy, subject, customRoles, now = nowSeconds(), customGrants = customGrantsFor(policy, customRoles), mode = "held") {
2209
+ const skip = (grant, match) => mode === "held" ? !match.matched : match.matched || match.reason !== "not-entitled" || grant.effect !== "allow";
2210
+ const scopes = scopeList(policy.scopes);
2211
+ const declared = declaredRoleNames(policy);
2212
+ const global = expandRoleNames(subject.principal?.roles ?? [], declared, customRoles, subject.principal?.tenant);
2213
+ const held = (subject.principal?.memberships ?? []).map((membership) => ({
2214
+ membership,
2215
+ roles: new Set(expandRoleNames(membership.roles, declared, customRoles, tenantOf(membership, scopes)).roles)
2216
+ }));
2217
+ const holds = (entry, item) => entry.roles.has(item.role) && (typeof item.scope === "string" ? entry.membership.scope === item.scope : entry.membership.on !== void 0);
2218
+ const out = [];
2219
+ for (const grant of grantList(policy)) {
2220
+ const resource = getResource(policy.permissions, grant.permission.resource);
2221
+ const match = matchGrantee(grant.to, subject, now, resource, scopes, policy.resources);
2222
+ if (skip(grant, match)) continue;
2223
+ const roleItems = flattenGrantee(grant.to).filter((item) => item.kind === "role");
2224
+ if (roleItems.length > 0 && subject.principal === null) continue;
2225
+ if (!roleItems.every((item) => item.scope !== "global" || global.roles.includes(item.role))) continue;
2226
+ const merged = graphAware(grant, match.where);
2227
+ const scoped = roleItems.filter((item) => item.scope !== "global");
2228
+ if (scoped.length === 0) {
2229
+ out.push({ grant: merged });
2230
+ continue;
2231
+ }
2232
+ for (const entry of held) if (scoped.every((item) => item.kind === "role" && holds(entry, item))) out.push({
2233
+ grant: merged,
2234
+ membership: entry.membership
2235
+ });
2236
+ }
2237
+ for (const { grant, role } of customGrants) {
2238
+ const globalHeld = holdsGlobalCustomRole(subject.principal?.roles, role);
2239
+ const holders = held.filter((entry) => holdsCustomRole(entry.membership, role, scopes));
2240
+ if (holders.length === 0 && !globalHeld) continue;
2241
+ const resource = getResource(policy.permissions, grant.permission.resource);
2242
+ const match = matchGrantee(grant.to, subject, now, resource, scopes, policy.resources);
2243
+ if (skip(grant, match)) continue;
2244
+ const merged = graphAware(grant, match.where);
2245
+ if (globalHeld) out.push({ grant: merged });
2246
+ for (const entry of holders) out.push({
2247
+ grant: merged,
2248
+ membership: entry.membership
2249
+ });
2250
+ }
2251
+ const fresh = policy.fresh ?? [];
2252
+ if (mode === "held" && subject.stale === true && fresh.length > 0) return out.filter(({ grant }) => grant.effect === "deny" || !fresh.includes(grant.permission.key));
2253
+ return out;
2254
+ }
2255
+ /**
2256
+ * Declared roles a tenant admin may hand out or compose, before any
2257
+ * intersection: `assignable` roles, plus every role some `assigns` lists.
2258
+ */
2259
+ function assignableCandidates(policy) {
2260
+ const listed = new Set(policy.roles.flatMap((binding) => binding.assigns ?? []));
2261
+ 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)];
2262
+ return rankRoles(policy, [...new Set(names)]).map((name) => roleLeaf(policy, name));
2263
+ }
2264
+ /**
2265
+ * What the subject may hand out in `tenant`: declared assignable roles and the
2266
+ * tenant custom-role ceiling, narrowed by `allowed` (`RoleSource.assignable`),
2267
+ * then intersected with the permissions the subject holds there. Holding a
2268
+ * role or a granted permission with `meta.manageRoles` lifts the intersection.
2269
+ */
2270
+ function assignableIn(policy, subject, customRoles, customGrants, tenant, allowed, now = nowSeconds(), global = false) {
2271
+ const principal = subject.principal;
2272
+ if (principal === null) return {
2273
+ roles: [],
2274
+ permissions: [],
2275
+ manage: false
2276
+ };
2277
+ const scopes = scopeList(policy.scopes);
2278
+ const scoped = tenant === void 0 ? subject : freezeDeep(compact({
2279
+ ...subject,
2280
+ principal: compact({
2281
+ ...principal,
2282
+ tenant: resolveActiveTenant(principal, tenant, scopes, now)
2283
+ })
2284
+ }));
2285
+ const heldKeys = new Set(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.permission.key));
2286
+ const heldNames = heldRoleNames(policy, subject, tenant, void 0, now);
2287
+ const quiet = {
2288
+ emit: false,
2289
+ simulated: true,
2290
+ skipAlternatives: true,
2291
+ customRoles,
2292
+ customGrants,
2293
+ listeners: emptyListeners(),
2294
+ sink: void 0,
2295
+ limits: void 0,
2296
+ limitCache: /* @__PURE__ */ new Map(),
2297
+ team: void 0
2298
+ };
2299
+ 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, {
2300
+ source: "simulate",
2301
+ trusted: true,
2302
+ now
2303
+ }, quiet).outcome === "granted");
2304
+ const narrowed = allowed === void 0 ? void 0 : new Set(allowed);
2305
+ const candidates = assignableCandidates(policy).filter((leaf) => narrowed === void 0 || narrowed.has(leaf.key));
2306
+ const held = new Set(heldNames);
2307
+ const listed = usesAssigns(policy) ? new Set(heldNames.flatMap((name) => policy.rolesByName.get(name)?.assigns ?? [])) : void 0;
2308
+ const roles = manage ? candidates : candidates.filter((leaf) => {
2309
+ if (listed !== void 0) return listed.has(leaf.key);
2310
+ if (held.has(leaf.key)) return true;
2311
+ const keys = [...roleAllowKeys(policy, leaf.key)];
2312
+ return keys.length > 0 && keys.every((key) => heldKeys.has(key));
2313
+ });
2314
+ const root = global ? "global" : rootScope(scopes);
2315
+ const ceiling = new Set((root === void 0 ? [] : ceilingGrants(policy, root, allowed)).map((grant) => grant.permission.key));
2316
+ return {
2317
+ roles,
2318
+ permissions: listPermissions(policy.permissions).filter((leaf) => ceiling.has(leaf.key) && (manage || heldKeys.has(leaf.key))),
2319
+ manage
2320
+ };
2321
+ }
2322
+ /** The one path from a resolved subject to a `Snapshot`; pure apart from the default clock. */
2323
+ function snapshotOf(policy, subject, options) {
2324
+ const now = options.now ?? nowSeconds();
2325
+ const customGrants = options.customGrants ?? customGrantsFor(policy, options.customRoles);
2326
+ const roles = heldRoleNames(policy, subject, subject.principal?.tenant, void 0, now);
2327
+ const audiences = audiencesOf(policy, roles);
2328
+ return buildSnapshot(compact({
2329
+ subject,
2330
+ roles,
2331
+ audiences: audiences.length === 0 ? void 0 : audiences,
2332
+ notEntitled: collectSnapshotGrants(policy, subject, options.customRoles, now, customGrants, "not-entitled"),
2333
+ grants: collectSnapshotGrants(policy, subject, options.customRoles, now, customGrants).map((item) => periodBound(policy, item.grant) ? Object.assign({}, item, { grant: freezeDeep({
2334
+ ...item.grant,
2335
+ portable: false
2336
+ }) }) : item),
2337
+ include: includePrefixes(options.include),
2338
+ tenants: options.tenants,
2339
+ simulated: options.simulated,
2340
+ now: Math.floor(now),
2341
+ vocabulary: policy.vocabulary,
2342
+ scopes: snapshotScopes(policy),
2343
+ delegated: delegatedPermissions(policy.delegations, subject, new Set(roles), now),
2344
+ assignable: (tenant) => {
2345
+ const found = assignableIn(policy, subject, options.customRoles, customGrants, tenant, options.assignable?.get(tenant), now);
2346
+ return {
2347
+ tenant,
2348
+ roles: found.roles.map((leaf) => leaf.key),
2349
+ permissions: found.permissions
2350
+ };
2351
+ }
2352
+ }));
2353
+ }
2354
+ /** The policy's scopes as snapshots and `permdock cloud push` carry them, with the resources each partitions. */
2355
+ function snapshotScopes(policy) {
2356
+ if (policy.scopes.length === 0) return;
2357
+ const resources = [...policy.resources.values()];
2358
+ return policy.scopes.map((scope) => {
2359
+ const partitioned = resources.filter((node) => partitionsOf(node, policy.scopes).includes(scope.name)).map((node) => node.name);
2360
+ return compact({
2361
+ name: scope.name,
2362
+ key: scope.key ?? "",
2363
+ within: scope.within,
2364
+ resources: partitioned.length === 0 ? void 0 : partitioned
2365
+ });
2366
+ });
2367
+ }
2368
+ function buildInstance(policy, subject, envBase, team) {
2369
+ const listeners = emptyListeners();
2370
+ const queuedAuth = [...envBase.queuedAuth];
2371
+ const queuedErrors = [...envBase.queuedErrors ?? []];
2372
+ const customGrants = customGrantsFor(policy, envBase.customRoles);
2373
+ const relationCache = envBase.relationCache ?? /* @__PURE__ */ new Map();
2374
+ const relations = relationReader(envBase.relations, relationCache, policy.resources);
2375
+ const assignableAt = (tenant) => assignableIn(policy, subject, envBase.customRoles, customGrants, tenant, tenant === void 0 ? void 0 : envBase.assignable?.get(tenant));
2376
+ const envFor = (emit, skipAlternatives = false) => ({
2377
+ emit,
2378
+ simulated: envBase.simulated,
2379
+ skipAlternatives,
2380
+ customRoles: envBase.customRoles,
2381
+ customGrants,
2382
+ listeners,
2383
+ sink: envBase.sink,
2384
+ limits: envBase.limits,
2385
+ limitCache: envBase.limitCache,
2386
+ team,
2387
+ relations,
2388
+ ...envBase.approvalPolicies === void 0 ? {} : { approvalPolicies: envBase.approvalPolicies }
2389
+ });
2390
+ /** The one `simulate` event for a batch: the worst decision, with the counts. */
2391
+ const simulateEvent = (evaluated) => {
2392
+ const counts = {
2393
+ granted: 0,
2394
+ denied: 0,
2395
+ approvalRequired: 0
2396
+ };
2397
+ for (const [, , decision] of evaluated) if (decision.outcome === "granted") counts.granted += 1;
2398
+ else if (decision.outcome === "approval-required") counts.approvalRequired += 1;
2399
+ else counts.denied += 1;
2400
+ const worst = evaluated.find(([, , decision]) => decision.outcome === "denied") ?? evaluated.find(([, , decision]) => decision.outcome === "approval-required") ?? evaluated[0];
2401
+ if (worst === void 0) return;
2402
+ const [permission, data, decision] = worst;
2403
+ finish(policy, subject, permission, data, decision, {
2404
+ source: "simulate",
2405
+ trusted: true
2406
+ }, {
2407
+ ...envFor(false),
2408
+ emit: true
2409
+ }, true, void 0, counts);
2410
+ };
2411
+ const decideImpl = (permission, data, options) => evaluate(policy, subject, permission, data, options ?? {}, envFor(options?.source !== "simulate", options?.source === "can"));
2412
+ const explainImpl = (permission, data, options) => {
2413
+ return decideImpl(permission, data, {
2414
+ ...options,
2415
+ source: options?.source ?? "explain",
2416
+ explain: true
2417
+ });
2418
+ };
2419
+ const canImpl = (permission, data, options) => {
2420
+ try {
2421
+ return decideImpl(permission, data, {
2422
+ ...options,
2423
+ source: options?.source ?? "can"
2424
+ }).outcome === "granted";
2425
+ } catch {
2426
+ return false;
2427
+ }
2428
+ };
2429
+ const assertImpl = (permission, data, options) => {
2430
+ const decision = decideImpl(permission, data, {
2431
+ ...options,
2432
+ source: options?.source ?? "assert"
2433
+ });
2434
+ if (decision.outcome === "granted") return decision;
2435
+ const onDenied = options?.onDenied ?? policy.onDenied;
2436
+ if (onDenied !== void 0) onDenied(decision);
2437
+ const resource = getResource(policy.permissions, permission.resource);
2438
+ const resourceId = data !== null && typeof data === "object" ? data[resource?.id ?? "id"] : void 0;
2439
+ const resourceRef = compact({
2440
+ type: permission.resource,
2441
+ id: resourceId === void 0 ? void 0 : String(resourceId)
2442
+ });
2443
+ if (decision.outcome === "approval-required") throw new PermDockApprovalRequiredError({
2444
+ decision,
2445
+ permission: permission.key,
2446
+ scope: permission.scope,
2447
+ resource: resourceRef,
2448
+ message: approvalMessage(permission.key, decision.reason, decision.token)
2449
+ });
2450
+ if (decision.denials.some((denial) => denial.reason === "validation")) {
2451
+ const detail = decision.denials[0]?.detail;
2452
+ if (detail instanceof PermDockValidationError) throw detail;
2453
+ }
2454
+ throw new PermDockDeniedError({
2455
+ decision,
2456
+ permission: permission.key,
2457
+ scope: permission.scope,
2458
+ resource: resourceRef,
2459
+ subject,
2460
+ message: deniedMessage(permission.key, subject.principal?.id, decision.denials, decision.alternatives.map((leaf) => leaf.key))
2461
+ });
2462
+ };
2463
+ const instance = {
2464
+ can: canImpl,
2465
+ decide: decideImpl,
2466
+ assert: assertImpl,
2467
+ explain: explainImpl,
2468
+ permissions: policy.permissions,
2469
+ roles: policy.vocabulary?.roles ?? {},
2470
+ plans: policy.vocabulary?.plans ?? {},
2471
+ filter(permission, rows, options) {
2472
+ const allowed = [];
2473
+ let granted = 0;
2474
+ let denied = 0;
2475
+ let approvalRequired = 0;
2476
+ const quiet = envFor(false);
2477
+ const trusted = options?.trusted ?? true;
2478
+ const decideOptions = {
2479
+ ...options,
2480
+ source: "filter",
2481
+ trusted
2482
+ };
2483
+ for (const row of rows) {
2484
+ const decision = evaluate(policy, subject, permission, row, decideOptions, quiet);
2485
+ if (decision.outcome === "granted") {
2486
+ allowed.push(row);
2487
+ granted += 1;
2488
+ } else if (decision.outcome === "approval-required") approvalRequired += 1;
2489
+ else denied += 1;
2490
+ }
2491
+ const summary = granted > 0 ? freezeDeep({
2492
+ outcome: "granted",
2493
+ subject,
2494
+ matched: {
2495
+ role: "*",
2496
+ permission: permission.key
2497
+ },
2498
+ token: "pd1.filter"
2499
+ }) : freezeDeep({
2500
+ outcome: "denied",
2501
+ denials: [{
2502
+ role: null,
2503
+ reason: "no-grant"
2504
+ }],
2505
+ alternatives: []
2506
+ });
2507
+ finish(policy, subject, permission, rows[0], summary, decideOptions, {
2508
+ ...quiet,
2509
+ emit: true
2510
+ }, trusted, void 0, {
2511
+ granted,
2512
+ denied,
2513
+ approvalRequired
2514
+ });
2515
+ return allowed;
2516
+ },
2517
+ pick(permission, row, options) {
2518
+ if (row === null || typeof row !== "object") return {};
2519
+ if (canImpl(permission, row, options) !== true) return {};
2520
+ return pickVisible(row, (field) => {
2521
+ const next = compact({
2522
+ ...options,
2523
+ field
2524
+ });
2525
+ return canImpl(permission, row, next) === true;
2526
+ });
2527
+ },
2528
+ where(permission) {
2529
+ const grants = collectSnapshotGrants(policy, subject, envBase.customRoles, nowSeconds(), customGrants).filter((item) => item.grant.permission.key === permission.key).map((item) => snapshotGrant(whereGrant(item.grant), item.membership));
2530
+ return whereFromGrants(grants, compact({
2531
+ resource: permission.resource,
2532
+ resources: policy.resources,
2533
+ graph: envBase.relations !== void 0,
2534
+ scopes: scopeList(policy.scopes),
2535
+ partitioned: (name, key) => relatesTo(getResource(policy.permissions, permission.resource), key, name, scopeList(policy.scopes)),
2536
+ tenant: subject.principal?.tenant,
2537
+ team,
2538
+ now: nowSeconds(),
2539
+ subject
2540
+ }));
2541
+ },
2542
+ actions(resource, data, options) {
2543
+ const fromTree = listPermissions(resource);
2544
+ return (fromTree.length > 0 ? fromTree : "resource" in resource && typeof resource.resource === "string" ? listPermissions(policy.permissions).filter((item) => item.resource === resource.resource) : []).filter((item) => canImpl(item, data, options) === true);
2545
+ },
2546
+ async loadRelations(permission, rows) {
2547
+ if (envBase.relations === void 0) return;
2548
+ const quiet = {
2549
+ ...envFor(false),
2550
+ simulated: true,
2551
+ skipAlternatives: true
2552
+ };
2553
+ for (let round = 0; round < LOAD_ROUNDS; round += 1) {
2554
+ for (const row of rows) evaluate(policy, subject, permission, row, {
2555
+ source: "simulate",
2556
+ trusted: true
2557
+ }, quiet);
2558
+ const pending = pendingRelations(relationCache);
2559
+ if (pending.length === 0) return;
2560
+ await Promise.all(pending);
2561
+ }
2562
+ },
2563
+ whoCan(permission, row) {
2564
+ return whoCan({
2565
+ policy,
2566
+ permission,
2567
+ row,
2568
+ memberships: envBase.memberships,
2569
+ reader: relations,
2570
+ cache: relationCache,
2571
+ customGrants,
2572
+ team
2573
+ });
2574
+ },
2575
+ simulate: ((input, options) => {
2576
+ const evaluated = [];
2577
+ const quietly = (permission, data) => {
2578
+ const decision = evaluate(policy, subject, permission, data, compact({
2579
+ source: "simulate",
2580
+ trusted: true,
2581
+ now: options?.now
2582
+ }), envFor(false));
2583
+ evaluated.push([
2584
+ permission,
2585
+ data,
2586
+ decision
2587
+ ]);
2588
+ return decision;
2589
+ };
2590
+ if (isReadonlyArray(input)) {
2591
+ const decisions = input.map(([permission, data]) => quietly(permission, data));
2592
+ simulateEvent(evaluated);
2593
+ return decisions;
2594
+ }
2595
+ if (isArazzoSimulateInput(input)) {
2596
+ const plan = simulateArazzo(input, input.permissions ?? policy.permissions, quietly);
2597
+ simulateEvent(evaluated);
2598
+ return plan;
2599
+ }
2600
+ const preview = input;
2601
+ const previewRoles = preview.roles?.map((item) => typeof item === "string" ? item : item.key);
2602
+ 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)));
2603
+ const previewPrincipal = subject.principal === null || kinds === void 0 ? null : freezeDeep(compact({
2604
+ ...subject.principal,
2605
+ roles: kinds.roles,
2606
+ memberships: preview.memberships === void 0 && subject.principal.memberships === void 0 ? void 0 : kinds.memberships,
2607
+ tenant: preview.tenant ?? subject.principal.tenant
2608
+ }));
2609
+ return buildInstance(policy, freezeDeep({
2610
+ ...subject,
2611
+ principal: previewPrincipal
2612
+ }), {
2613
+ ...envBase,
2614
+ simulated: true,
2615
+ relationCache
2616
+ }, team);
2617
+ }),
2618
+ snapshot(options) {
2619
+ const snapshot = snapshotOf(policy, subject, compact({
2620
+ customRoles: envBase.customRoles,
2621
+ customGrants,
2622
+ assignable: envBase.assignable,
2623
+ include: options?.include,
2624
+ tenants: options?.tenants,
2625
+ simulated: envBase.simulated
2626
+ }));
2627
+ if (options?.signer !== void 0) return signSnapshot(snapshot, options.signer, options.audience);
2628
+ return snapshot;
2629
+ },
2630
+ on(event, handler) {
2631
+ const set = listeners[event];
2632
+ set.add(handler);
2633
+ if (event === "auth") for (const queued of queuedAuth) try {
2634
+ handler(queued);
2635
+ } catch (error) {
2636
+ emitSafe(listeners.error, error, listeners);
2637
+ }
2638
+ if (event === "error") for (const queued of queuedErrors) try {
2639
+ handler(queued);
2640
+ } catch {}
2641
+ return () => {
2642
+ set.delete(handler);
2643
+ };
2644
+ },
2645
+ tenant(id) {
2646
+ if (subject.principal === null) return buildInstance(policy, subject, {
2647
+ ...envBase,
2648
+ relationCache
2649
+ }, team);
2650
+ return buildInstance(policy, freezeDeep(compact({
2651
+ ...subject,
2652
+ principal: compact({
2653
+ ...subject.principal,
2654
+ tenant: resolveActiveTenant(subject.principal, id, scopeList(policy.scopes))
2655
+ })
2656
+ })), {
2657
+ ...envBase,
2658
+ relationCache
2659
+ }, team);
2660
+ },
2661
+ team(id) {
2662
+ return buildInstance(policy, subject, {
2663
+ ...envBase,
2664
+ relationCache
2665
+ }, id);
2666
+ },
2667
+ memberships() {
2668
+ return subject.principal?.memberships ?? [];
2669
+ },
2670
+ tenants() {
2671
+ return tenantsOf(subject.principal, scopeList(policy.scopes));
2672
+ },
2673
+ heldRoles(options) {
2674
+ return heldRoleNames(policy, subject, options?.tenant ?? subject.principal?.tenant, options?.scope === void 0 ? void 0 : compact({
2675
+ scope: options.scope,
2676
+ id: options.id
2677
+ })).map((name) => roleLeaf(policy, name));
2678
+ },
2679
+ audiences() {
2680
+ return audiencesOf(policy, heldRoleNames(policy, subject, subject.principal?.tenant));
2681
+ },
2682
+ assignableRoles(options) {
2683
+ return assignableAt(options?.tenant ?? subject.principal?.tenant).roles;
2684
+ },
2685
+ assignablePermissions(options) {
2686
+ if (options?.scope === "global") return assignableIn(policy, subject, envBase.customRoles, customGrants, void 0, void 0, nowSeconds(), true).permissions;
2687
+ const tenant = options?.tenant ?? subject.principal?.tenant;
2688
+ return tenant === void 0 ? [] : assignableAt(tenant).permissions;
2689
+ },
2690
+ decideRoleChange(change, options) {
2691
+ return decideRoleChange(policy, subject.principal, scopeList(policy.scopes), change, (tenant) => {
2692
+ const found = assignableAt(tenant);
2693
+ return {
2694
+ assignable: new Set(found.roles.map((leaf) => leaf.key)),
2695
+ manage: found.manage
2696
+ };
2697
+ }, nowSeconds(), options);
2698
+ },
2699
+ activate(input) {
2700
+ return activate(policy, subject, input, nowSeconds());
2701
+ },
2702
+ subject
2703
+ };
2704
+ return Object.freeze(instance);
2705
+ }
2706
+ //#endregion
2707
+ //#region src/core/resolve-subject.ts
2708
+ function assemblePrincipal(policy, user, options, auth) {
2709
+ let principal;
2710
+ let context = {};
2711
+ let actor = options.actor;
2712
+ let delegation = options.delegation;
2713
+ let session = options.session;
2714
+ let expiresAt = options.expiresAt;
2715
+ try {
2716
+ if (user === null || user === void 0) principal = policy.subject(user);
2717
+ else if (isSubject(user)) {
2718
+ principal = user.principal;
2719
+ context = user.context;
2720
+ actor = user.actor ?? actor;
2721
+ delegation = user.delegation ?? delegation;
2722
+ session = user.session ?? session;
2723
+ expiresAt = user.expiresAt ?? expiresAt;
2724
+ } else if (isPrincipal(user)) principal = user;
2725
+ else principal = policy.subject(user);
2726
+ } catch {
2727
+ principal = null;
2728
+ }
2729
+ const contextResult = resolveContext(policy, user, context, auth);
2730
+ let memberships = principal?.memberships ?? [];
2731
+ let live = false;
2732
+ const source = options.memberships === void 0 ? void 0 : asMembershipSource(options.memberships);
2733
+ const fromClaims = source?.claimsFirst === true && principal?.memberships !== void 0 && principal.membershipsTruncated !== true;
2734
+ if (principal !== null && source !== void 0 && !fromClaims) {
2735
+ live = true;
2736
+ try {
2737
+ memberships = source.membershipsFor(compact({
2738
+ id: principal.id,
2739
+ kind: principal.kind
2740
+ }), compact({ tenant: options.tenant }));
2741
+ } catch {
2742
+ auth.push({
2743
+ reason: "source-threw",
2744
+ source: "memberships"
2745
+ });
2746
+ memberships = [];
2747
+ }
2748
+ }
2749
+ return {
2750
+ principal,
2751
+ context: contextResult,
2752
+ actor,
2753
+ delegation,
2754
+ session,
2755
+ expiresAt,
2756
+ memberships,
2757
+ live
2758
+ };
2759
+ }
2760
+ function finishSubject(policy, assembled, context, input, options, extra) {
2761
+ if (assembled.principal === null) return freezeCopy(compact({
2762
+ ...anonymousSubject(context),
2763
+ actor: assembled.actor,
2764
+ delegation: assembled.delegation,
2765
+ session: assembled.session,
2766
+ expiresAt: assembled.expiresAt
2767
+ }));
2768
+ const scopes = scopeList(policy.scopes);
2769
+ const { roles, memberships } = applyRoleKinds(policy, assembled.principal.roles, normalizeMemberships(input, scopes));
2770
+ const plans = [.../* @__PURE__ */ new Set([...assembled.principal.plans ?? [], ...extra.plans])];
2771
+ const withMemberships = freezeCopy(compact({
2772
+ ...assembled.principal,
2773
+ roles,
2774
+ memberships,
2775
+ plans: plans.length === 0 ? assembled.principal.plans : plans,
2776
+ tenant: activeTenantOf(policy, assembled, memberships, options)
2777
+ }));
2778
+ return freezeCopy(compact({
2779
+ principal: withMemberships,
2780
+ actor: assembled.actor,
2781
+ delegation: assembled.delegation,
2782
+ context: freezeCopy(context),
2783
+ session: assembled.session,
2784
+ expiresAt: assembled.expiresAt,
2785
+ stale: extra.stale ? true : void 0
2786
+ }));
2787
+ }
2788
+ function activeTenantOf(policy, assembled, memberships, options) {
2789
+ if (assembled.principal === null) return;
2790
+ return resolveActiveTenant({
2791
+ ...assembled.principal,
2792
+ memberships
2793
+ }, options.tenant ?? assembled.principal.tenant, scopeList(policy.scopes));
2794
+ }
2795
+ function settle(value, fallback, onError) {
2796
+ if (isThenable(value)) return Promise.resolve(value).catch(() => {
2797
+ onError();
2798
+ return fallback;
2799
+ });
2800
+ return value;
2801
+ }
2802
+ /**
2803
+ * Whether the token's memberships are behind the source for `fresh`
2804
+ * permissions. Live memberships are never stale; token memberships are stale
2805
+ * unless a source reports a version the token's `authzVersion` has reached.
2806
+ */
2807
+ function staleness(policy, assembled, options, auth) {
2808
+ const principal = assembled.principal;
2809
+ if ((policy.fresh ?? []).length === 0 || principal === null || assembled.live) return false;
2810
+ const source = options.memberships === void 0 ? void 0 : asMembershipSource(options.memberships);
2811
+ const claimed = principal.authzVersion;
2812
+ if (source?.version === void 0 || typeof claimed !== "number") return true;
2813
+ const compare = (current) => current === void 0 || claimed < current;
2814
+ try {
2815
+ const current = source.version({ id: principal.id });
2816
+ return isThenable(current) ? settle(Promise.resolve(current).then(compare), true, () => {
2817
+ auth.push({
2818
+ reason: "source-threw",
2819
+ source: "memberships"
2820
+ });
2821
+ }) : compare(current);
2822
+ } catch {
2823
+ auth.push({
2824
+ reason: "source-threw",
2825
+ source: "memberships"
2826
+ });
2827
+ return true;
2828
+ }
2829
+ }
2830
+ function cleanNames(list) {
2831
+ return Array.isArray(list) ? list.filter((item) => typeof item === "string" && item !== "") : [];
2832
+ }
2833
+ function entitlementsOf(assembled, tenant, options, auth) {
2834
+ const source = options.entitlements;
2835
+ if (source === void 0 || assembled.principal === null) return [];
2836
+ try {
2837
+ const found = source.entitlementsFor({ id: assembled.principal.id }, compact({ tenant }));
2838
+ return isThenable(found) ? settle(Promise.resolve(found).then(cleanNames), [], () => {
2839
+ auth.push({
2840
+ reason: "source-threw",
2841
+ source: "entitlements"
2842
+ });
2843
+ }) : cleanNames(found);
2844
+ } catch {
2845
+ auth.push({
2846
+ reason: "source-threw",
2847
+ source: "entitlements"
2848
+ });
2849
+ return [];
2850
+ }
2851
+ }
2852
+ function resolveContext(policy, user, fallback, auth) {
2853
+ if (policy.context === void 0) return sanitizeContext(fallback);
2854
+ try {
2855
+ const loaded = policy.context(user);
2856
+ if (isThenable(loaded)) return loaded.then((value) => sanitizeContext(value), () => {
2857
+ auth.push({
2858
+ reason: "source-threw",
2859
+ source: "context"
2860
+ });
2861
+ return {};
2862
+ });
2863
+ return sanitizeContext(loaded);
2864
+ } catch {
2865
+ auth.push({
2866
+ reason: "source-threw",
2867
+ source: "context"
2868
+ });
2869
+ return {};
2870
+ }
2871
+ }
2872
+ function resolveSubject(policy, user, options, auth) {
2873
+ const assembled = assemblePrincipal(policy, user, options, auth);
2874
+ const memberships = settle(assembled.memberships, [], () => {
2875
+ auth.push({
2876
+ reason: "source-threw",
2877
+ source: "memberships"
2878
+ });
2879
+ });
2880
+ const stale = staleness(policy, assembled, options, auth);
2881
+ const withPlans = (resolved) => entitlementsOf(assembled, activeTenantOf(policy, assembled, applyRoleKinds(policy, assembled.principal?.roles, normalizeMemberships(resolved, scopeList(policy.scopes))).memberships, options), options, auth);
2882
+ if (isThenable(assembled.context) || isThenable(memberships) || isThenable(stale)) return Promise.all([
2883
+ Promise.resolve(assembled.context),
2884
+ Promise.resolve(memberships),
2885
+ Promise.resolve(stale)
2886
+ ]).then(async ([context, resolved, isStale]) => finishSubject(policy, assembled, context, resolved, options, {
2887
+ stale: isStale,
2888
+ plans: await withPlans(resolved)
2889
+ }));
2890
+ const plans = withPlans(memberships);
2891
+ if (isThenable(plans)) return Promise.resolve(plans).then((resolved) => finishSubject(policy, assembled, assembled.context, memberships, options, {
2892
+ stale,
2893
+ plans: resolved
2894
+ }));
2895
+ return finishSubject(policy, assembled, assembled.context, memberships, options, {
2896
+ stale,
2897
+ plans
2898
+ });
2899
+ }
2900
+ //#endregion
2901
+ //#region src/core/permdock.ts
2902
+ function hostedPolicy(policy, source) {
2903
+ if (source === void 0 || policy.hostable.length === 0) return {
2904
+ policy,
2905
+ errors: []
2906
+ };
2907
+ let document;
2908
+ try {
2909
+ document = source.current();
2910
+ } catch (error) {
2911
+ return {
2912
+ policy,
2913
+ errors: [error]
2914
+ };
2915
+ }
2916
+ const merged = mergeHostedGrants(policy, document);
2917
+ return {
2918
+ policy: merged.policy,
2919
+ errors: merged.dropped
2920
+ };
2921
+ }
2922
+ function instantiate(codePolicy, subject, options, auth) {
2923
+ const { policy, errors } = hostedPolicy(codePolicy, options.policies);
2924
+ const tenants = tenantsOf(subject.principal, scopeList(policy.scopes));
2925
+ const customRoles = customRolesFor(options.customRoles, tenants, auth, subject.principal !== null);
2926
+ const assignable = assignableNamesFor(options.customRoles, tenants, auth);
2927
+ const approvals = approvalPoliciesFor(policy, options.approvalPolicies, tenants, auth);
2928
+ const build = (roles, names, approvalPolicies) => buildInstance(policy, subject, compact({
2929
+ customRoles: roles,
2930
+ sink: options.sink,
2931
+ limits: options.limits,
2932
+ limitCache: /* @__PURE__ */ new Map(),
2933
+ simulated: false,
2934
+ roleSource: options.customRoles,
2935
+ assignable: names,
2936
+ queuedAuth: auth,
2937
+ queuedErrors: errors,
2938
+ relations: options.relations,
2939
+ approvalPolicies,
2940
+ memberships: options.memberships === void 0 ? void 0 : asMembershipSource(options.memberships)
2941
+ }));
2942
+ if (isThenable(customRoles) || isThenable(assignable) || isThenable(approvals)) return Promise.all([
2943
+ customRoles,
2944
+ assignable,
2945
+ approvals
2946
+ ]).then(([roles, names, loaded]) => build(roles, names, loaded));
2947
+ return build(customRoles, assignable, approvals);
2948
+ }
2949
+ function createPermDock(policy, user, options = {}) {
2950
+ const auth = [];
2951
+ const subject = resolveSubject(policy, user, options, auth);
2952
+ if (isThenable(subject)) return subject.then((resolved) => instantiate(policy, resolved, options, auth));
2953
+ return instantiate(policy, subject, options, auth);
2954
+ }
2955
+ //#endregion
2956
+ export { approvalPoliciesFor as _, snapshotScopes as a, supportAccess as c, membershipsClaim as d, resolveCustomRole as f, isExternallyManaged as g, composeMemberships as h, snapshotOf as i, customRoleClaim as l, claimsFirst as m, resolveSubject as n, declaredRoleNames as o, validateCustomRole as p, collectSnapshotGrants as r, breakGlass as s, createPermDock as t, customRoleScope as u, memoryApprovalPolicies as v };