permdock 0.4.0 → 0.6.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.
- package/README.md +37 -35
- package/dist/{schema-Bol6gqXA.js → 2020-C9VN-x8c.js} +5 -130
- package/dist/a2a/index.d.ts +6 -6
- package/dist/a2a/index.js +14 -17
- package/dist/{adapter-context-mlWsVGGg.js → adapter-context-CRhqyzCk.js} +1 -1
- package/dist/adoption-scan-DCYe60Pi.js +107 -0
- package/dist/agent/index.d.ts +3 -0
- package/dist/agent/index.js +1 -0
- package/dist/ai-sdk/index.d.ts +18 -35
- package/dist/ai-sdk/index.js +37 -23
- package/dist/{app-data-CV8pqqMi.js → app-data-MuyzVqX8.js} +2 -1
- package/dist/approval-header-CJACtHkv.js +23 -0
- package/dist/approval-prompt-Dg5LeAcT.js +62 -0
- package/dist/approval-prompt-DvSSPQJX.d.ts +57 -0
- package/dist/approvals/index.d.ts +5 -3
- package/dist/approvals/index.js +8 -5
- package/dist/approvals-p1PGoVjb.js +995 -0
- package/dist/arazzo-41WuzHou.js +36 -0
- package/dist/{arazzo-C4BcskbX.js → arazzo-CaUVBn7D.js} +20 -14
- package/dist/args-IIolxxKA.js +197 -0
- package/dist/astro/index.d.ts +43 -0
- package/dist/astro/index.js +27 -0
- package/dist/audience-DabClFLQ.js +55 -0
- package/dist/auth-DRLEizbL.js +178 -0
- package/dist/authzen/index.d.ts +26 -6
- package/dist/authzen/index.js +94 -79
- package/dist/automation-BNM3TtDo.d.ts +114 -0
- package/dist/automation-DVjOckjV.js +157 -0
- package/dist/better-auth/index.d.ts +8 -6
- package/dist/better-auth/index.js +9 -8
- package/dist/better-supabase/index.d.ts +357 -30
- package/dist/better-supabase/index.js +936 -194
- package/dist/{bind-kk-uyCwH.js → bind-CAvgUuKs.js} +14 -10
- package/dist/{bind-BzqG8SMb.d.ts → bind-DOyrv6zi.d.ts} +12 -10
- package/dist/body-CH5of8Ne.js +10 -0
- package/dist/body-text-QcXok_SI.js +52 -0
- package/dist/{build-instance-DIIV9DbC.js → build-instance-Bu_eDqYw.js} +4 -3
- package/dist/{canonical-json-CFKyFOke.js → canonical-json-BC8_nMTw.js} +5 -3
- package/dist/{capability-CksyCWDH.d.ts → capability-Drg-1YhL.d.ts} +44 -25
- package/dist/{capability-txATtKV4.js → capability-N1K9IpqS.js} +68 -42
- package/dist/casbin-Cr7RLtA3.js +30 -0
- package/dist/casbin-Dyu2gnUD.js +234 -0
- package/dist/casl-64gpT-8x.js +24 -0
- package/dist/casl-DjEDhTCP.js +299 -0
- package/dist/catalog/index.d.ts +74 -2
- package/dist/catalog/index.js +51 -2
- package/dist/catalog-BoWglDPI.js +1 -0
- package/dist/catalog-DZVbrF-z.js +52 -0
- package/dist/{catalog-tZUyOfOv.js → catalog-HD2gf5mY.js} +51 -6
- package/dist/{catalog-doc-DWtg0gGR.js → catalog-doc-Cdn7-0pQ.js} +45 -23
- package/dist/catalog-fingerprint-CCP0-yQS.js +153 -0
- package/dist/chat/index.d.ts +2 -0
- package/dist/chat/index.js +224 -0
- package/dist/chat-sdk/index.d.ts +87 -0
- package/dist/chat-sdk/index.js +197 -0
- package/dist/claims-map-7oQ3ue4X.js +13 -0
- package/dist/claims-map-Ca-QX8C6.d.ts +32 -0
- package/dist/claude-agent/index.d.ts +7 -14
- package/dist/claude-agent/index.js +24 -27
- package/dist/clerk/index.d.ts +9 -8
- package/dist/clerk/index.js +9 -14
- package/dist/cli/bin.js +36 -17
- package/dist/cli/index.d.ts +19 -5
- package/dist/cli/index.js +164 -4
- package/dist/cloud/index.d.ts +102 -63
- package/dist/cloud/index.js +239 -340
- package/dist/cloud-ytQ3uviA.js +87 -0
- package/dist/codemod-5mpSpnUY.js +37 -0
- package/dist/codemod-ua2hHy7w.js +2084 -0
- package/dist/collect-Ci6akVwH.js +55 -0
- package/dist/collect-D7eQDffE.js +1 -0
- package/dist/{collect-2SrB7lfk.js → collect-mLTY4nwb.js} +75 -11
- package/dist/command-L8EstFNm.js +6 -0
- package/dist/compile/index.d.ts +1 -1
- package/dist/compile/index.js +1 -2
- package/dist/{compile-CtTP7eur.d.ts → compile-1bsAezL0.d.ts} +5 -3
- package/dist/{compile-CcEu2ZQ1.js → compile-CMqMx_IN.js} +11 -8
- package/dist/completion-BTFrHZhX.js +143 -0
- package/dist/config-Bj2Ixq3d.js +26 -0
- package/dist/{config-BIjREpyE.js → config-bs_kZeYq.js} +12 -7
- package/dist/{config-report--Xb9APJS.js → config-report-DSpsLpCE.js} +15 -5
- package/dist/{connection-DUsczpB5.js → connection-DjhIJSs2.js} +6 -5
- package/dist/{context-DieZVJXu.js → context-B_u3_bDJ.js} +2 -2
- package/dist/convex/index.d.ts +11 -10
- package/dist/convex/index.js +6 -4
- package/dist/create-B30OMUOJ.d.ts +107 -0
- package/dist/create-CBMmFie-.d.ts +138 -0
- package/dist/create-CGm28gyI.js +130 -0
- package/dist/create-CQWIpUG-.js +403 -0
- package/dist/{create-BBO24vj6.js → create-DCF2SvoT.js} +31 -32
- package/dist/create-D_NmPOdH.d.ts +110 -0
- package/dist/{credential-kue0qpbm.js → credential-BcoiejpY.js} +152 -31
- package/dist/credentials-DPyHmYz5.js +74 -0
- package/dist/delegation-j6-rc37o.js +286 -0
- package/dist/delivery/index.d.ts +5 -0
- package/dist/delivery/index.js +5 -0
- package/dist/delivery-DCAgNyCn.js +499 -0
- package/dist/delivery-event-xX4obB8F.js +40 -0
- package/dist/{denied-text-DfRk15uw.d.ts → denied-text-CiFQNuMV.d.ts} +5 -5
- package/dist/{describe-C3ldA9os.d.ts → describe-B5qgDvEW.d.ts} +2 -2
- package/dist/{describe-Cgkp7eP0.js → describe-BybMRY7A.js} +6 -2
- package/dist/device-BARQ8eYG.js +193 -0
- package/dist/{diff-rpqoe7Ag.js → diff-3jbx6Rm4.js} +60 -54
- package/dist/diff-CmuSD3vc.js +37 -0
- package/dist/doctor-3ZqNHX_G.js +44 -0
- package/dist/{doctor-owlv6ddh.js → doctor-BdZw53-k.js} +245 -50
- package/dist/drizzle/index.d.ts +5 -3
- package/dist/drizzle/index.js +2 -2
- package/dist/elysia/index.d.ts +16 -13
- package/dist/elysia/index.js +13 -12
- package/dist/env-DJZrPjRi.js +8 -0
- package/dist/errors-BDUwg2qC.js +27 -0
- package/dist/{errors-drnfxYtm.js → errors-BKy0EfhN.js} +5 -9
- package/dist/errors-BWXBMaKK.d.ts +11 -0
- package/dist/errors-CWTY6uih.js +143 -0
- package/dist/{errors-OD_4c__H.d.ts → errors-DH5QEGXh.d.ts} +2 -2
- package/dist/evaluate-CNClBvcj.js +1785 -0
- package/dist/evaluations-C_1vdSZc.d.ts +32 -0
- package/dist/{evaluations-C9YJsv3H.js → evaluations-beAooV0n.js} +145 -34
- package/dist/eve/index.d.ts +12 -16
- package/dist/eve/index.js +22 -28
- package/dist/event-time-CsegxXxB.js +23 -0
- package/dist/expiry-D4_YKRKD.js +52 -0
- package/dist/express/index.d.ts +13 -10
- package/dist/express/index.js +12 -11
- package/dist/fastify/index.d.ts +25 -22
- package/dist/fastify/index.js +13 -12
- package/dist/fetch-adapter-DD_gfmrC.js +120 -0
- package/dist/fetch-adapter-Dw0MTS3D.d.ts +31 -0
- package/dist/fields-OYG_JR78.js +203 -0
- package/dist/files-C5ZI27eV.js +124 -0
- package/dist/{fixtures-h2eG2OZV.js → fixtures-B7XrLeY7.js} +8 -7
- package/dist/flags-C3DktBDY.js +86 -0
- package/dist/{from-snapshot-7REQZe0H.js → from-snapshot-CTCMsFZk.js} +55 -52
- package/dist/{grantee-conditions-MQs5NIuX.js → grantee-conditions-CcXf8hMM.js} +4 -3
- package/dist/{grantee-CXY-0C87.js → grantee-hxrUsbpM.js} +16 -6
- package/dist/{grantee-match-fxYp9Nmt.js → grantee-match-DbENBHLB.js} +5 -4
- package/dist/h3/index.d.ts +37 -0
- package/dist/h3/index.js +35 -0
- package/dist/headers-BIjfMaoC.js +8 -0
- package/dist/headers-CIbDTUTK.d.ts +7 -0
- package/dist/{helpers-C9YmyBIa.js → helpers-CBYVMjwh.js} +45 -38
- package/dist/hono/index.d.ts +12 -9
- package/dist/hono/index.js +13 -12
- package/dist/hosted-B46-5LYE.js +315 -0
- package/dist/hosted-BPk51-kX.d.ts +78 -0
- package/dist/{http-DPuDaChb.d.ts → http-B_LphE5Y.d.ts} +1 -2
- package/dist/http-DXJ37RDA.js +46 -0
- package/dist/{http-BEbdMZ05.js → http-ujKm0JXE.js} +1 -2
- package/dist/{import-UHk1N7vH.js → import-CNV7I2Dw.js} +25 -20
- package/dist/{import-BFIlhetD.js → import-wyqXUyjm.js} +190 -53
- package/dist/index-B8YjkEZ4.d.ts +210 -0
- package/dist/index-C-6Letxg.d.ts +431 -0
- package/dist/index-CaxrMD1A.d.ts +1 -0
- package/dist/{index-BwMe94Xq.d.ts → index-Ch8LpNDp.d.ts} +34 -16
- package/dist/index-Cp_ilKi1.d.ts +91 -0
- package/dist/index-CueFnxQc.d.ts +110 -0
- package/dist/index-DuWxc5dt.d.ts +909 -0
- package/dist/index.d.ts +19 -15
- package/dist/index.js +31 -21
- package/dist/init-DeMYsnYJ.js +96 -0
- package/dist/init-GG6Jh8HD.js +95 -0
- package/dist/{instance-options-plkrsAog.d.ts → instance-options-BsVpyGIk.d.ts} +2 -2
- package/dist/instance-snapshot-BOEhcqIF.js +601 -0
- package/dist/instant-BluqDvyS.js +19 -0
- package/dist/internals-Dk8A8fX3.js +17 -0
- package/dist/{json-schema-DBkdUuCO.js → json-schema-B-vAwlde.js} +45 -6
- package/dist/json-schema-CN_iLFH2.d.ts +40 -0
- package/dist/jwt/index.d.ts +18 -11
- package/dist/jwt/index.js +156 -135
- package/dist/{kernel-CB3LrIJM.js → kernel-D0NVb1cK.js} +52 -24
- package/dist/kysely/index.d.ts +5 -3
- package/dist/kysely/index.js +2 -2
- package/dist/load-BxEtwlBS.js +1 -0
- package/dist/{load-C3_oTHyO.js → load-DFS_ry0W.js} +22 -6
- package/dist/{load-jose-BL6LH4bL.js → load-jose-DZ4dnjri.js} +1 -4
- package/dist/login-Dfe5RlYk.js +68 -0
- package/dist/login-imhuwvn3.js +254 -0
- package/dist/manifest-C8IzW3Do.js +42 -0
- package/dist/manifest-dvL2XEe1.js +370 -0
- package/dist/manifest-json-CAGzls-C.js +9 -0
- package/dist/{manifest-uDS9y5de.d.ts → manifest-mx5ghSFM.d.ts} +9 -2
- package/dist/manifest-schema-C0kK_8V0.js +399 -0
- package/dist/{map-claims-AVlBaJoa.js → map-claims-D3oRmTk0.js} +13 -10
- package/dist/{map-error-BE27PYkE.js → map-error-Dr70oA_3.js} +3 -3
- package/dist/{markers-CiwAOZtO.js → markers-C6joDI3o.js} +2 -1
- package/dist/mcp/index.d.ts +22 -12
- package/dist/mcp/index.js +57 -39
- package/dist/{migrate-BwlB5-Wk.js → migrate-CIq4-E9F.js} +53 -14
- package/dist/nest/index.d.ts +12 -9
- package/dist/nest/index.js +15 -14
- package/dist/next/index.d.ts +18 -21
- package/dist/next/index.js +13 -11
- package/dist/next/plugin.d.ts +1 -1
- package/dist/next/plugin.js +2 -2
- package/dist/node/index.d.ts +10 -10
- package/dist/node/index.js +7 -9
- package/dist/{ocsf-uYFXGVw2.js → ocsf-BFp-PZZA.js} +22 -11
- package/dist/openai/index.d.ts +9 -16
- package/dist/openai/index.js +17 -24
- package/dist/openapi/index.d.ts +3 -3
- package/dist/openapi/index.js +1 -1
- package/dist/{openapi-DBzHQbov.js → openapi-B7HZ0f-r.js} +8 -8
- package/dist/openapi-C4dDTVEd.js +138 -0
- package/dist/{openapi-BJdBmYDG.js → openapi-DD1SV-5-.js} +10 -9
- package/dist/operation-findings-DgATDpvh.js +36 -0
- package/dist/operations-X0VM7F3m.d.ts +59 -0
- package/dist/orpc/index.d.ts +13 -10
- package/dist/orpc/index.js +20 -15
- package/dist/otel/index.d.ts +3 -2
- package/dist/otel/index.js +30 -7
- package/dist/output-1im__-08.js +88 -0
- package/dist/output-files-B3IZJksC.js +1 -0
- package/dist/{output-files-CgIsvdSa.js → output-files-DhRBBE4n.js} +24 -26
- package/dist/{permission-keys-B26jeyUv.js → ownership-DqaJNJPn.js} +1045 -153
- package/dist/{package-root-DtNnpmUd.js → package-root-Dk_ETdkp.js} +1 -1
- package/dist/{parse-CVFQHk6c.js → parse-Cxq0WxrA.js} +2 -2
- package/dist/{parse-local-manifest-DvYUR0QL.d.ts → parse-local-manifest-5ODwqu8b.d.ts} +1 -1
- package/dist/{parse-local-manifest-C-q02Pal.js → parse-local-manifest-CVSs6J9I.js} +1 -1
- package/dist/parse-request-D_tUXG6M.js +26 -0
- package/dist/paths-DXSHAMIa.js +42 -0
- package/dist/pdp/index.d.ts +7 -3
- package/dist/pdp/index.js +45 -26
- package/dist/{peer-BOr_kJE1.js → peer-C8swJ2GH.js} +23 -4
- package/dist/peer-CRvC7Hg1.js +1 -0
- package/dist/permdock-BrAscV28.js +868 -0
- package/dist/permdock-SV8ZINYT.d.ts +502 -0
- package/dist/{permission-registry-BsblpB-z.js → permission-registry-Br4jSNVi.js} +3 -3
- package/dist/permissions-B-wCP5ZC.js +30 -0
- package/dist/{permissions-CCJ6lmxB.js → permissions-BPxlwVhN.js} +4 -3
- package/dist/{permissions-COH-KRgT.d.ts → permissions-u_KHg-sN.d.ts} +1 -2
- package/dist/policies-DP2nqRoF.js +295 -0
- package/dist/{policy-ur0BBK_e.d.ts → policy-77FSAq_V.d.ts} +2 -2
- package/dist/{hosted-CBHgkzDl.js → policy-DbM5YYtV.js} +17 -273
- package/dist/policy-from-catalog-C2gMa2Dj.js +450 -0
- package/dist/policy-module-CTlwMvLI.js +89 -0
- package/dist/{policy-types-sp9P1iIV.d.ts → policy-types-n0uRTsW8.d.ts} +460 -632
- package/dist/postgrest-CEgRbefE.d.ts +100 -0
- package/dist/postgrest-DAjUkF9w.js +575 -0
- package/dist/{powersync-cZe4kzhL.js → powersync-BkWWCS1K.js} +23 -24
- package/dist/powersync-DISt6SUe.js +62 -0
- package/dist/powersync-MMJ2mI2q.js +1 -0
- package/dist/preset/index.d.ts +146 -0
- package/dist/preset/index.js +1 -0
- package/dist/preset-DZdU0tne.js +1768 -0
- package/dist/principal-source-pHneukrx.js +103 -0
- package/dist/prisma/index.d.ts +6 -3
- package/dist/prisma/index.js +2 -2
- package/dist/{problem-CnWraVhC.js → problem-CTviyH5L.js} +13 -23
- package/dist/{problem-details-rIIo8B2C.d.ts → problem-details-B45V5d2b.d.ts} +1 -1
- package/dist/{problem-details-DECUnXAn.js → problem-details-cn5X30gZ.js} +43 -40
- package/dist/problem-types-CJ2325SZ.js +5 -0
- package/dist/{prompt-CXYhLfsS.js → prompt-DfZLJLIn.js} +2 -2
- package/dist/{protected-BN7BUdY1.js → protected-BlF0F6wL.js} +21 -4
- package/dist/{protected-B3_P7xuw.d.ts → protected-RFGpakN9.d.ts} +16 -8
- package/dist/{provider-Bi-0HNL3.js → provider-B1al-AZz.js} +2 -2
- package/dist/{provider-kwHC9CZD.d.ts → provider-DILL9oPK.d.ts} +1 -1
- package/dist/{provider-store-Ctt-81Ni.js → provider-store-DM6DNcta.js} +2 -2
- package/dist/pull-CvpINcMK.js +1 -0
- package/dist/pull-jEwbf80J.js +256 -0
- package/dist/push-BD4f15co.js +116 -0
- package/dist/pushed-policy-CqtxkV4G.js +238 -0
- package/dist/pushed-policy-DCW91KQU.d.ts +57 -0
- package/dist/react/index.d.ts +8 -8
- package/dist/react/index.js +6 -6
- package/dist/react/provider-client.d.ts +1 -1
- package/dist/react/provider-client.js +1 -2
- package/dist/react-native/index.d.ts +24 -72
- package/dist/react-native/index.js +29 -25
- package/dist/{refs-f3imMjNW.js → refs-K6eWUJ0H.js} +1 -1
- package/dist/{relation-walk-BXnlLtg2.js → relation-walk-3OyDBF0S.js} +2 -2
- package/dist/{relations-CvmSpG67.js → relations-BnCWnJGI.js} +3 -3
- package/dist/renames-CGj7YwcQ.js +175 -0
- package/dist/{replay-COpmZUfx.d.ts → replay-Bkm-Ne4l.d.ts} +1 -1
- package/dist/{replay-BsOyTrdV.js → replay-Du1tJjG7.js} +2 -1
- package/dist/{request-context-BVM7aWNs.js → request-context-C81Pip9n.js} +3 -3
- package/dist/request-schema-CMreVuFM.js +148 -0
- package/dist/resend/index.d.ts +126 -0
- package/dist/resend/index.js +379 -0
- package/dist/{resource-relations-D1Un3pcz.js → resource-relations-D_jw0BFx.js} +1 -1
- package/dist/{retire-BFcxL6un.js → retire-CALjIQ7e.js} +2 -2
- package/dist/{revocations-Cy06JNx2.d.ts → revocations-R07lFBnX.d.ts} +2 -0
- package/dist/rls-BAqP4q66.js +336 -0
- package/dist/{rls-BEktUaqg.js → rls-DTvYd-lI.js} +288 -690
- package/dist/{role-builder-B4GbKM-i.js → role-builder-uWQZ4v84.js} +14 -207
- package/dist/{rpc-nYC3cR_u.js → rpc-VSl5ylaQ.js} +7 -7
- package/dist/run-CUKuz0A8.js +723 -0
- package/dist/run-DDwUaJDU.js +1 -0
- package/dist/run-ticket-DocwaTKn.d.ts +124 -0
- package/dist/run-ticket-sxFI6LAk.js +337 -0
- package/dist/{runtime-D_tMXnsq.js → runtime-D3VTAiCA.js} +1 -1
- package/dist/{saas-DtaA4JuW.js → saas-BCXRuXca.js} +16 -14
- package/dist/scan-qvOe0hS3.js +29 -0
- package/dist/{schema-DjQBcAP9.js → schema-B4v0Qrac.js} +102 -8
- package/dist/schema-yLuBWWsh.js +130 -0
- package/dist/scim/index.d.ts +2 -2
- package/dist/scim/index.js +1 -2
- package/dist/{scim-vFR1O5oG.js → scim-ClHtpyV3.js} +208 -76
- package/dist/{scopes-tvpa34ep.js → scopes-CUPSjo9q.js} +12 -7
- package/dist/server/index.d.ts +12 -143
- package/dist/server/index.js +12 -9
- package/dist/server-C3WXAHZj.js +453 -0
- package/dist/{server-types-DdqBwZN_.d.ts → server-types-CKQiCnON.d.ts} +51 -68
- package/dist/{sha256-BO0swRXL.js → sha256-D9ihqmvd.js} +8 -1
- package/dist/{shared-1uhI-e-b.js → shared-BAK8t4Uo.js} +20 -9
- package/dist/{shared-JhMeOsDF.js → shared-DvvFzYXe.js} +8 -9
- package/dist/signals-D3NcTKUS.js +32 -0
- package/dist/sink-4-eQIRWf.d.ts +225 -0
- package/dist/sink-BEZSlMS_.js +957 -0
- package/dist/{skills-BZdRQkxs.js → skills-B6lVdD_W.js} +13 -6
- package/dist/skills-CTYwkl44.js +51 -0
- package/dist/snapshot-B7DkYg6g.js +269 -0
- package/dist/{snapshot-cache-Cod40D-X.js → snapshot-cache-DZHQ0KF6.js} +11 -5
- package/dist/{snapshot-cache-J5T7t_vD.d.ts → snapshot-cache-PXI-vGpY.d.ts} +7 -5
- package/dist/solid/index.d.ts +11 -12
- package/dist/solid/index.js +7 -7
- package/dist/solid-start/index.d.ts +52 -0
- package/dist/solid-start/index.js +49 -0
- package/dist/{sources-Dljy06zL.d.ts → sources-Xc4zkszI.d.ts} +7 -13
- package/dist/{sources-B1-b-R0t.js → sources-vs5dGOsS.js} +125 -119
- package/dist/{sql-t8teWOfo.js → sql-B5T1IL83.js} +13 -1
- package/dist/src-Cu2m75uo.js +1860 -0
- package/dist/ssf/index.d.ts +4 -4
- package/dist/ssf/index.js +384 -47
- package/dist/standard-webhooks-DgFFD-Ga.js +228 -0
- package/dist/standard-webhooks-rbD7LS6h.d.ts +88 -0
- package/dist/storage-C2bWdvJH.js +132 -0
- package/dist/{store-ILkLUj_o.js → store-BDutJkDL.js} +40 -53
- package/dist/{store-options-CbUHDoSe.js → store-options-u0ycEe8s.js} +26 -21
- package/dist/{stores-Cxz_80g-.d.ts → stores-BUGNbJTt.d.ts} +7 -8
- package/dist/{stores-BHXdSbw9.js → stores-CGhM8BN_.js} +2 -2
- package/dist/{stream-Dwsg6WwE.d.ts → stream-CeCMJmBB.d.ts} +2 -2
- package/dist/{stream-BMui98KK.js → stream-CvdUS9V1.js} +9 -2
- package/dist/style-DiKWnAvn.js +1 -0
- package/dist/{style-CTm1AXDT.js → style-Uao76wO7.js} +3 -2
- package/dist/subject-BGoM5o8B.js +17 -0
- package/dist/supabase/index.d.ts +23 -106
- package/dist/supabase/index.js +5 -4
- package/dist/supabase/middleware.d.ts +7 -6
- package/dist/supabase/middleware.js +13 -11
- package/dist/supabase-By_frk3U.js +92 -0
- package/dist/{supabase-hook-Cva3ikXz.js → supabase-hook-BMoEhZir.js} +960 -64
- package/dist/supabase-hook-CCw5m35C.js +1 -0
- package/dist/{supabase-C1TWlnYt.js → supabase-x5JtR2tc.js} +99 -206
- package/dist/support-session-C6xJQYut.js +158 -0
- package/dist/svelte/Protected.svelte +4 -4
- package/dist/svelte/index.d.ts +7 -7
- package/dist/svelte/index.js +10 -10
- package/dist/svelte/runtime.d.ts +3 -3
- package/dist/svelte/runtime.js +3 -3
- package/dist/svelte/source.d.ts +7 -7
- package/dist/svelte/source.js +4 -4
- package/dist/sveltekit/index.d.ts +51 -0
- package/dist/sveltekit/index.js +27 -0
- package/dist/tanstack-start/index.d.ts +51 -0
- package/dist/tanstack-start/index.js +40 -0
- package/dist/target-Bw37TFNA.js +51 -0
- package/dist/{tenancy-DshjqY0n.js → tenancy-DN2QAx9z.js} +5 -5
- package/dist/terminal/index.d.ts +11 -9
- package/dist/terminal/index.js +23 -287
- package/dist/testing/index.d.ts +311 -38
- package/dist/testing/index.js +3475 -201
- package/dist/testing/saas/index.d.ts +9 -8
- package/dist/testing/saas/index.js +2 -2
- package/dist/testing/saas/permissions.d.ts +1 -1
- package/dist/testing/saas/permissions.js +1 -2
- package/dist/text-BMFRfBqc.js +8 -0
- package/dist/{text-diff-BdkkYSbi.js → text-diff-BQcUapOb.js} +1 -1
- package/dist/thenable-yD7SabqG.js +31 -0
- package/dist/time-DkWjdL98.js +71 -0
- package/dist/{token-Bx9qy30p.js → token-BLZcjQgt.js} +15 -6
- package/dist/{tool-refusal-CQ7OEia9.js → tool-refusal-CMLN5xhn.js} +2 -2
- package/dist/trpc/index.d.ts +12 -9
- package/dist/trpc/index.js +16 -14
- package/dist/types-8RnwUAvE.d.ts +107 -0
- package/dist/{store-9KO4BlkB.d.ts → types-B3_ooZgZ.d.ts} +100 -75
- package/dist/{types-Br9IXUBG.d.ts → types-BdtSDbBO.d.ts} +77 -17
- package/dist/types-BtHtcKMr.d.ts +157 -0
- package/dist/{types-CqrjB9o6.d.ts → types-BzzaHxt3.d.ts} +23 -11
- package/dist/types-C791HFu9.d.ts +332 -0
- package/dist/types-C863OQiR.d.ts +57 -0
- package/dist/{types-ByUo56un.d.ts → types-CVZSNocA.d.ts} +23 -22
- package/dist/{types-DHrmBJLH.d.ts → types-CX7AvRqP.d.ts} +7 -7
- package/dist/types-C_IiEs64.d.ts +65 -0
- package/dist/{types-7rJVLWe5.d.ts → types-Dfv0aTER.d.ts} +63 -11
- package/dist/types-DqzsnlSA.js +67 -0
- package/dist/{types-DpWfQdSh.d.ts → types-Druu9wLM.d.ts} +46 -11
- package/dist/{types-CIlTojhq.d.ts → types-_WhUP7ud.d.ts} +9 -6
- package/dist/{types-CUcFZfpU.d.ts → types-lUlG3TVd.d.ts} +2 -1
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +2 -2
- package/dist/usage-CRVl9V3t.js +32 -0
- package/dist/usage-CZpSQP6L.js +1 -0
- package/dist/{usage-CG9nOYZw.js → usage-SveTRTyr.js} +54 -11
- package/dist/{validation-error-BSSw0l_8.d.ts → validation-error-DHUrAkku.d.ts} +1 -1
- package/dist/{validity-dVrUEPJt.js → validity-CZ7-y0Cj.js} +32 -15
- package/dist/{verifier-DMr_Zmb_.js → verifier-DHuh-okk.js} +18 -7
- package/dist/{verify-BztUB3GX.js → verify-BaAKx7t6.js} +645 -50
- package/dist/{version-BEogC-rB.js → version-CEbQ5UeM.js} +4 -3
- package/dist/version-DXkYUISx.js +1 -0
- package/dist/{views-CJsrBS1f.js → views-DTIIO-oQ.js} +22 -5
- package/dist/{permission-types-Dg39z9Kv.d.ts → vocabulary-CN8H_9UF.d.ts} +67 -2
- package/dist/vue/index.d.ts +9 -10
- package/dist/vue/index.js +8 -8
- package/dist/waiter-B986w1oK.d.ts +36 -0
- package/dist/waiter-BGUtGYoU.js +67 -0
- package/dist/watch--ovlPOc0.js +1 -0
- package/dist/{watch-zeucB7eF.js → watch-vea54PRw.js} +44 -5
- package/dist/webmcp/index.d.ts +2 -2
- package/dist/webmcp/index.js +3 -3
- package/dist/where-scope-C9vfEphl.js +306 -0
- package/dist/who-can-DWBAcaCQ.js +364 -0
- package/dist/wire-delegation-Cv3W2eaM.js +36 -0
- package/dist/wire-denial-C7PaVYEM.js +27 -0
- package/dist/workflow/index.d.ts +5 -0
- package/dist/workflow/index.js +223 -0
- package/dist/workflow-sdk/index.d.ts +56 -0
- package/dist/workflow-sdk/index.js +78 -0
- package/dist/{wrap-Cr93qqdP.d.ts → wrap-C5jC6sKI.d.ts} +2 -1
- package/dist/{wrap-DWsU_5Bd.js → wrap-gX61EtpB.js} +7 -6
- package/package.json +136 -37
- package/schemas/access-event-v1.json +118 -0
- package/schemas/approval-request-v1.json +34 -1
- package/schemas/approval-signature-v1.json +94 -0
- package/schemas/automation-event-v1.json +113 -0
- package/schemas/capability-v1.json +84 -0
- package/schemas/catalog-event-v1.json +87 -0
- package/schemas/catalog-v1.json +123 -12
- package/schemas/cli-commands-v1.json +164 -0
- package/schemas/cli-result-v1.json +96 -0
- package/schemas/cloud-api-v1.json +5407 -0
- package/schemas/connector-v1.json +408 -0
- package/schemas/credential-event-v1.json +81 -0
- package/schemas/credential-v1.json +134 -0
- package/schemas/decision-event-v1.json +288 -0
- package/schemas/delivery-event-v1.json +117 -0
- package/schemas/directory-event-v1.json +85 -0
- package/schemas/evidence-export-v1.json +54 -0
- package/schemas/membership-event-v1.json +93 -0
- package/schemas/policy-v1.json +68 -0
- package/schemas/pushed-policy-v1.json +236 -0
- package/schemas/run-v1.json +162 -0
- package/schemas/snapshot-v1.json +138 -0
- package/schemas/supabase-claims-v1.json +4 -4
- package/schemas/supabase-manifest-v1.json +5 -1
- package/schemas/support-session-v1.json +111 -0
- package/skills/permdock/SKILL.md +16 -12
- package/skills/permdock-agents/SKILL.md +40 -23
- package/skills/permdock-agents/references/operations-automations-chat.md +221 -0
- package/skills/permdock-agents/references/runtimes.md +22 -18
- package/skills/permdock-approvals/SKILL.md +29 -13
- package/skills/permdock-approvals/references/delivery-and-waiters.md +172 -0
- package/skills/permdock-approvals/references/stores-and-resume.md +21 -7
- package/skills/permdock-audit/SKILL.md +3 -3
- package/skills/permdock-credentials/SKILL.md +5 -5
- package/skills/permdock-credentials/references/tokens-keys-links.md +14 -10
- package/skills/permdock-data/SKILL.md +6 -6
- package/skills/permdock-data/references/queries-and-rls.md +10 -9
- package/skills/permdock-data/references/relationships.md +3 -3
- package/skills/permdock-tenancy/SKILL.md +20 -7
- package/skills/permdock-tenancy/references/contacts-and-support-sessions.md +122 -0
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +6 -4
- package/skills/permdock-wire/SKILL.md +17 -11
- package/skills/permdock-wire/references/adapters.md +139 -51
- package/skills/permdock-wire/references/policy-options.md +5 -5
- package/skills/permdock-wire/references/presets-and-recipes.md +58 -0
- package/dist/approvals-DZL76lKW.js +0 -240
- package/dist/arazzo-CD8d0WLb.js +0 -54
- package/dist/catalog-6yWAX6Ps.js +0 -2
- package/dist/catalog-WHG4kBq7.js +0 -48
- package/dist/cloud-C7bSDgX4.js +0 -50
- package/dist/cloud-PeBN3IEn.js +0 -118
- package/dist/collect-Bw2Jdq1q.js +0 -2
- package/dist/collect-CQx4hX4N.js +0 -83
- package/dist/commands-Cpip4ULx.js +0 -97
- package/dist/config-DLt6BvV7.js +0 -36
- package/dist/diff-Cnghh_Ip.js +0 -48
- package/dist/doctor-CuVJvhV_.js +0 -53
- package/dist/duration-xK2jGFVc.js +0 -25
- package/dist/errors-CWfoNg9N.js +0 -47
- package/dist/errors-DRnGZHzF.js +0 -30
- package/dist/expiry-BoBJ8Tvc.js +0 -7
- package/dist/fields-fwfXN01n.js +0 -1016
- package/dist/headers-BZSDieNT.js +0 -6
- package/dist/headers-Cf-zWvsO.d.ts +0 -6
- package/dist/ids-DC4V4CFu.js +0 -18
- package/dist/index-DgM7w96K.d.ts +0 -6
- package/dist/index-Dz7didpC.d.ts +0 -201
- package/dist/index-UdVQgq48.d.ts +0 -550
- package/dist/load-B_shHrmd.js +0 -2
- package/dist/manifest-BngWKryL.js +0 -770
- package/dist/may-use-BOn_dR_w.js +0 -78
- package/dist/openapi-DcQ1Kg40.js +0 -164
- package/dist/ownership-BgnbOJFT.js +0 -851
- package/dist/permdock-L26udHSk.js +0 -3646
- package/dist/powersync-BFSbSElQ.js +0 -62
- package/dist/powersync-DCDgZgfZ.js +0 -2
- package/dist/rls-55DQTuf5.js +0 -250
- package/dist/run-DMcNW_8T.js +0 -174
- package/dist/server-ZYEHtNHS.js +0 -268
- package/dist/sink-BTtmfPnE.js +0 -141
- package/dist/sink-CPN4zLfs.d.ts +0 -104
- package/dist/skills-Dle4ZoLF.js +0 -56
- package/dist/snapshot-Dc9CHPME.js +0 -1078
- package/dist/src-Bb7no-hS.js +0 -639
- package/dist/subject-C6dLxqHU.js +0 -24
- package/dist/supabase-Dbqr141s.js +0 -83
- package/dist/supabase-hook-BULg7Ut_.js +0 -2
- package/dist/thenable-QSmvt-vZ.js +0 -65
- package/dist/types-C0dee0Qf.d.ts +0 -197
- package/dist/types-DvupZE0w.d.ts +0 -9
- package/dist/usage-4G6t643F.js +0 -42
- package/dist/usage-iFLJ0-rr.js +0 -2
- package/dist/vocabulary-hhEFLFoL.d.ts +0 -65
- package/dist/watch-C8TpsAl4.js +0 -2
|
@@ -0,0 +1,1785 @@
|
|
|
1
|
+
import { a as sole, n as isReadonlyArray, t as compact } from "./compact-CAf557ZB.js";
|
|
2
|
+
import { n as freezeDeep } from "./freeze-CrU8nQeX.js";
|
|
3
|
+
import { n as isForbiddenKey } from "./paths-DXSHAMIa.js";
|
|
4
|
+
import { n as role, t as normalizeAssurance } from "./role-builder-uWQZ4v84.js";
|
|
5
|
+
import { n as PermDockValidationError } from "./validation-error-ZfUHbKvf.js";
|
|
6
|
+
import { t as wireDenials } from "./wire-denial-C7PaVYEM.js";
|
|
7
|
+
import { o as sameId } from "./expiry-D4_YKRKD.js";
|
|
8
|
+
import { n as internalsOf } from "./internals-Dk8A8fX3.js";
|
|
9
|
+
import { a as getResource, c as listPermissions, h as formerKeys, m as annotationsFor, n as findPermission } from "./permission-registry-Br4jSNVi.js";
|
|
10
|
+
import { n as isThenable, t as ignoreRejection } from "./thenable-yD7SabqG.js";
|
|
11
|
+
import { i as readAppData } from "./app-data-MuyzVqX8.js";
|
|
12
|
+
import { a as durationOf, d as isoNow, o as epochSeconds, r as assertDuration, s as instantAt, t as INVALID_NOW } from "./time-DkWjdL98.js";
|
|
13
|
+
import { t as canonicalJson } from "./canonical-json-BC8_nMTw.js";
|
|
14
|
+
import { a as normalizeMemberships, d as scopeList, f as subjectForPermission, i as keepsPermission, m as tenantOf, o as resolveScope, s as rootMembershipId } from "./scopes-CUPSjo9q.js";
|
|
15
|
+
import { a as readOnlyActorRefuses, c as rowIdOf, i as delegatedPermissions, l as rowValues, n as coveredByDelegation, o as requiresStandIn, r as coversRequirements, s as isRowPair, u as evaluateCondition } from "./delegation-j6-rc37o.js";
|
|
16
|
+
import { a as authenticated, g as isRole, i as assurance, m as findRole, o as flattenGrantee, r as asGrantee } from "./grantee-hxrUsbpM.js";
|
|
17
|
+
import { a as grantCoversField, i as wireValidity, t as isActive } from "./validity-CZ7-y0Cj.js";
|
|
18
|
+
import { o as resourceRoleCondition } from "./grantee-conditions-CcXf8hMM.js";
|
|
19
|
+
import { c as nowSeconds, i as matchScopedMembership, l as partitionsOf, n as inTeam, p as scopeField, r as isMembershipExpired } from "./tenancy-DN2QAx9z.js";
|
|
20
|
+
import { a as levelCondition, r as grantList, t as declaredRoleNames } from "./policy-DbM5YYtV.js";
|
|
21
|
+
import { t as requiresApproval } from "./approval-required-TzNB9oQA.js";
|
|
22
|
+
import { i as versionOf, r as tokenPayload, t as decisionToken } from "./token-BLZcjQgt.js";
|
|
23
|
+
import { n as resolveRelated } from "./relation-walk-3OyDBF0S.js";
|
|
24
|
+
import { t as eventInstant } from "./event-time-CsegxXxB.js";
|
|
25
|
+
import { n as toWireDelegation } from "./wire-delegation-Cv3W2eaM.js";
|
|
26
|
+
import { n as matchGrantee, t as combineWhere } from "./grantee-match-DbENBHLB.js";
|
|
27
|
+
import { t as resourceIdOf } from "./resource-ref-CaS9yVPi.js";
|
|
28
|
+
//#region src/core/approval-state.ts
|
|
29
|
+
/** The denial detail when an `ApprovalPolicySource` threw or returned an invalid entry. */
|
|
30
|
+
const APPROVAL_POLICY_UNAVAILABLE = "approval-policy-unavailable";
|
|
31
|
+
/** The tenant a matched allow applies in: its membership's root scope, else the active tenant. */
|
|
32
|
+
function decisionTenant(subject, membership) {
|
|
33
|
+
return (membership === void 0 ? void 0 : rootMembershipId(membership)) ?? subject.principal?.tenant;
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/core/contact.ts
|
|
37
|
+
function isContact(principal) {
|
|
38
|
+
return principal?.kind === "contact";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A contact's memberships: only the `{ on }` ones count, each as
|
|
42
|
+
* `via: 'contact'` and without seats, so a role whose `for` excludes
|
|
43
|
+
* `contact` never applies and no `plan()` grantee matches.
|
|
44
|
+
*/
|
|
45
|
+
function contactMemberships(memberships) {
|
|
46
|
+
const kept = [];
|
|
47
|
+
for (const membership of memberships) {
|
|
48
|
+
if (membership.on === void 0) continue;
|
|
49
|
+
const { entitlements: _seats, ...rest } = membership;
|
|
50
|
+
kept.push(Object.freeze({
|
|
51
|
+
...rest,
|
|
52
|
+
via: "contact"
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
return kept;
|
|
56
|
+
}
|
|
57
|
+
function optsIn(policy, grant) {
|
|
58
|
+
if (grant.role === null) return false;
|
|
59
|
+
return policy.roles.find(({ name }) => name === grant.role)?.for?.includes("contact") === true;
|
|
60
|
+
}
|
|
61
|
+
/** Whether `subject` may use the allow `grant`: false only for a contact and a write its role does not open to contacts. */
|
|
62
|
+
function contactMayUse(policy, subject, grant) {
|
|
63
|
+
return grant.effect !== "allow" || !isContact(subject.principal) || annotationsFor(grant.permission).readOnlyHint || optsIn(policy, grant);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The allows a contact may use for `permission`: all of them for a
|
|
67
|
+
* read-only permission, otherwise only those from a role whose `for` lists
|
|
68
|
+
* `contact`. Every other subject keeps every allow.
|
|
69
|
+
*/
|
|
70
|
+
function contactAllows(policy, subject, permission, allows) {
|
|
71
|
+
if (!isContact(subject.principal) || annotationsFor(permission).readOnlyHint) return allows;
|
|
72
|
+
return allows.filter(({ grant }) => optsIn(policy, grant));
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/core/elevated.ts
|
|
76
|
+
function flattenPermissions(input) {
|
|
77
|
+
if (input !== null && typeof input === "object" && "key" in input && "action" in input) return [input];
|
|
78
|
+
if (isReadonlyArray(input)) return input.flatMap((item) => flattenPermissions(item));
|
|
79
|
+
return [...listPermissions(input)];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The only deny override: a break-glass grant overrides deny grants whose
|
|
83
|
+
* `name` it lists, and nothing else. It is an allow that stays non-portable
|
|
84
|
+
* (RLS never compiles it) and grants only once its `requires` are met.
|
|
85
|
+
*/
|
|
86
|
+
function breakGlass(permission, options = {}) {
|
|
87
|
+
if (options.maxDuration !== void 0) assertDuration(options.maxDuration, "breakGlass maxDuration");
|
|
88
|
+
const purpose = options.requires?.purpose === void 0 || options.requires.purpose.length === 0 ? void 0 : Object.freeze([...new Set(options.requires.purpose)]);
|
|
89
|
+
const spec = compact({
|
|
90
|
+
overrides: Object.freeze([...new Set(options.overrides ?? [])]),
|
|
91
|
+
purpose,
|
|
92
|
+
reason: options.requires?.reason === true,
|
|
93
|
+
assurance: normalizeAssurance(options.requires?.assurance),
|
|
94
|
+
maxDuration: options.maxDuration,
|
|
95
|
+
obligations: Object.freeze([...new Set(options.obligations ?? [])])
|
|
96
|
+
});
|
|
97
|
+
const grants = flattenPermissions(permission).map((leaf) => freezeDeep(compact({
|
|
98
|
+
permission: leaf,
|
|
99
|
+
effect: "allow",
|
|
100
|
+
to: authenticated(),
|
|
101
|
+
portable: false,
|
|
102
|
+
breakGlass: spec
|
|
103
|
+
})));
|
|
104
|
+
return sole(grants) ?? grants;
|
|
105
|
+
}
|
|
106
|
+
function supportAccess(options) {
|
|
107
|
+
for (const duration of options.consent.durations) assertDuration(duration, `supportAccess consent duration on role '${options.role}'`);
|
|
108
|
+
const via = options.role;
|
|
109
|
+
const forbidden = (options.forbid ?? []).flatMap((item) => flattenPermissions(item));
|
|
110
|
+
const binding = role(options.role, [], {
|
|
111
|
+
on: options.on ?? "tenant",
|
|
112
|
+
for: [via]
|
|
113
|
+
});
|
|
114
|
+
const denies = forbidden.map((leaf) => freezeDeep(compact({
|
|
115
|
+
permission: leaf,
|
|
116
|
+
effect: "deny",
|
|
117
|
+
to: authenticated(),
|
|
118
|
+
role: options.role,
|
|
119
|
+
scope: "global",
|
|
120
|
+
viaOnly: via,
|
|
121
|
+
portable: false
|
|
122
|
+
})));
|
|
123
|
+
const support = freezeDeep(compact({
|
|
124
|
+
role: options.role,
|
|
125
|
+
actorRequired: options.actorRequired === true,
|
|
126
|
+
consent: {
|
|
127
|
+
by: asGrantee(options.consent.by),
|
|
128
|
+
durations: Object.freeze([...new Set(options.consent.durations)])
|
|
129
|
+
},
|
|
130
|
+
group: options.group ?? "vendor-support"
|
|
131
|
+
}));
|
|
132
|
+
return freezeDeep({
|
|
133
|
+
...binding,
|
|
134
|
+
grants: [...binding.grants, ...denies],
|
|
135
|
+
support
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/** Whether the subject's authentication is fresh enough for an elevated grant. */
|
|
139
|
+
function assuranceMet(requirement, subject, now) {
|
|
140
|
+
if (requirement === void 0) return true;
|
|
141
|
+
const held = subject.principal?.assurance;
|
|
142
|
+
if (requirement.acr !== void 0 && requirement.acr.length > 0) {
|
|
143
|
+
const acr = held?.acr;
|
|
144
|
+
if (acr === void 0 || !requirement.acr.includes(acr)) return false;
|
|
145
|
+
}
|
|
146
|
+
if (requirement.amr !== void 0 && requirement.amr.length > 0) {
|
|
147
|
+
const amr = held?.amr ?? [];
|
|
148
|
+
if (!requirement.amr.every((method) => amr.includes(method))) return false;
|
|
149
|
+
}
|
|
150
|
+
if (requirement.maxAge !== void 0) {
|
|
151
|
+
const authTime = held?.authTime;
|
|
152
|
+
if (authTime === void 0 || now - authTime > requirement.maxAge) return false;
|
|
153
|
+
}
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
/** `context.purpose` as a list of strings; fail-closed to empty. */
|
|
157
|
+
function purposesOf(subject) {
|
|
158
|
+
const value = subject.context["purpose"];
|
|
159
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : typeof value === "string" && value !== "" ? [value] : [];
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Whether a break-glass grant fires for this subject. It engages when the
|
|
163
|
+
* caller asserts a purpose (`context.purpose`); once engaged, an unlisted
|
|
164
|
+
* purpose, a missing reason or stale authentication denies with the matching
|
|
165
|
+
* reason, and everything met grants with the declared obligations.
|
|
166
|
+
*/
|
|
167
|
+
function evaluateBreakGlass(spec, subject, now) {
|
|
168
|
+
const purposes = purposesOf(subject);
|
|
169
|
+
if (purposes.length === 0) return { kind: "inactive" };
|
|
170
|
+
const required = spec.purpose;
|
|
171
|
+
if (required !== void 0 && !purposes.some((purpose) => required.includes(purpose))) return {
|
|
172
|
+
kind: "denied",
|
|
173
|
+
reason: "purpose"
|
|
174
|
+
};
|
|
175
|
+
const reason = subject.context["reason"];
|
|
176
|
+
if (spec.reason && (typeof reason !== "string" || reason === "")) return {
|
|
177
|
+
kind: "denied",
|
|
178
|
+
reason: "reason-required"
|
|
179
|
+
};
|
|
180
|
+
if (spec.assurance !== void 0 && !assuranceMet(spec.assurance, subject, now)) return {
|
|
181
|
+
kind: "denied",
|
|
182
|
+
reason: "insufficient-user-authentication",
|
|
183
|
+
to: assurance(spec.assurance)
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
kind: "granted",
|
|
187
|
+
obligations: [...spec.obligations.map((kind) => ({ kind })), ...typeof reason === "string" && reason !== "" ? [{
|
|
188
|
+
kind: "justify",
|
|
189
|
+
reason
|
|
190
|
+
}] : []]
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/** Support memberships (`via: 'support'`) the subject holds that demand an actor. */
|
|
194
|
+
function actorRequiredVias(supports) {
|
|
195
|
+
return new Set(supports.filter((spec) => spec.actorRequired).map((spec) => spec.role));
|
|
196
|
+
}
|
|
197
|
+
/** Whether a membership is a support membership of one of the given kinds. */
|
|
198
|
+
function isSupportMembership(membership, vias) {
|
|
199
|
+
return membership.via !== void 0 && vias.has(membership.via);
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Whether the caller's `within` names the same ancestors as the eligible
|
|
203
|
+
* membership. Omitted means "whatever the membership holds"; any entry that
|
|
204
|
+
* disagrees, or names an ancestor the membership lacks, refuses.
|
|
205
|
+
*/
|
|
206
|
+
function withinAgrees(scopes, held, requested) {
|
|
207
|
+
if (requested === void 0) return true;
|
|
208
|
+
return Object.entries(requested).every(([key, id]) => {
|
|
209
|
+
const name = resolveScope(scopes, key) ?? key;
|
|
210
|
+
return held !== void 0 && Object.hasOwn(held, name) && held[name] === id;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
function activationDenied(reason, to) {
|
|
214
|
+
return freezeDeep({
|
|
215
|
+
outcome: "denied",
|
|
216
|
+
denials: [to === void 0 ? {
|
|
217
|
+
role: null,
|
|
218
|
+
reason
|
|
219
|
+
} : {
|
|
220
|
+
role: null,
|
|
221
|
+
reason,
|
|
222
|
+
to
|
|
223
|
+
}],
|
|
224
|
+
alternatives: []
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Requests a just-in-time role activation. It never writes: a granted
|
|
229
|
+
* decision carries the elevated membership under `elevation` for the app to
|
|
230
|
+
* write. `approval-required` comes back first when the role's `activation`
|
|
231
|
+
* sets `approval`. Fail-closed: an unknown role, an ineligible subject, a
|
|
232
|
+
* missing justification or stale authentication all deny.
|
|
233
|
+
*/
|
|
234
|
+
/**
|
|
235
|
+
* Requests a just-in-time activation of an eligible role for the subject of
|
|
236
|
+
* `permdock`. It never writes: a granted decision carries the elevated
|
|
237
|
+
* membership under `elevation` for the app to write, and
|
|
238
|
+
* `approval-required` comes back first when the role's `activation` sets
|
|
239
|
+
* `approval`. An instance built from a snapshot denies with `unsupported`.
|
|
240
|
+
*/
|
|
241
|
+
function activate(permdock, input) {
|
|
242
|
+
const internals = internalsOf(permdock);
|
|
243
|
+
if (internals === void 0) return freezeDeep({
|
|
244
|
+
outcome: "denied",
|
|
245
|
+
denials: [{
|
|
246
|
+
role: null,
|
|
247
|
+
reason: "unsupported"
|
|
248
|
+
}],
|
|
249
|
+
alternatives: []
|
|
250
|
+
});
|
|
251
|
+
return activateFor(internals.policy, internals.subject, input, internals.now());
|
|
252
|
+
}
|
|
253
|
+
function activateFor(policy, subject, input, now = nowSeconds()) {
|
|
254
|
+
const roleName = isRole(input.role) ? input.role.key : input.role;
|
|
255
|
+
const binding = policy.rolesByName.get(roleName);
|
|
256
|
+
const activation = binding?.activation;
|
|
257
|
+
const principal = subject.principal;
|
|
258
|
+
if (principal === null) return activationDenied("anonymous");
|
|
259
|
+
if (binding === void 0 || activation === void 0) return activationDenied("unknown-role");
|
|
260
|
+
const scopes = scopeList(policy.scopes);
|
|
261
|
+
const scope = resolveScope(scopes, input.scope) ?? input.scope;
|
|
262
|
+
const eligible = (principal.memberships ?? []).find((membership) => membership.scope === scope && sameId(membership.id, input.id) && !isMembershipExpired(membership, now) && (membership.eligible ?? []).includes(roleName) && withinAgrees(scopes, membership.within, input.within));
|
|
263
|
+
if (eligible === void 0) return activationDenied("no-membership");
|
|
264
|
+
const within = eligible.within;
|
|
265
|
+
if (activation.justification === "required") {
|
|
266
|
+
const reason = input.reason;
|
|
267
|
+
if (typeof reason !== "string" || reason === "") return activationDenied("reason-required");
|
|
268
|
+
}
|
|
269
|
+
if (activation.assurance !== void 0 && !assuranceMet(activation.assurance, subject, now)) return activationDenied("insufficient-user-authentication", assurance(activation.assurance));
|
|
270
|
+
const requested = durationOf(input.duration);
|
|
271
|
+
if (input.duration !== void 0 && requested === void 0) return activationDenied("validation");
|
|
272
|
+
const cap = durationOf(activation.maxDuration);
|
|
273
|
+
const seconds = cap === void 0 ? requested : Math.min(requested ?? cap, cap);
|
|
274
|
+
const membership = freezeDeep(compact({
|
|
275
|
+
scope,
|
|
276
|
+
id: input.id,
|
|
277
|
+
within,
|
|
278
|
+
roles: [roleName],
|
|
279
|
+
via: "elevated",
|
|
280
|
+
expiresAt: seconds === void 0 ? void 0 : Math.floor(now) + seconds,
|
|
281
|
+
grantedBy: principal.id,
|
|
282
|
+
reason: input.reason
|
|
283
|
+
}));
|
|
284
|
+
const token = decisionToken({
|
|
285
|
+
key: `activate:${roleName}`,
|
|
286
|
+
resourceId: within === void 0 ? `${scope}:${input.id}` : `${scope}:${input.id}@${canonicalJson(within)}`,
|
|
287
|
+
principal,
|
|
288
|
+
actor: subject.actor,
|
|
289
|
+
fingerprint: policy.fingerprint
|
|
290
|
+
});
|
|
291
|
+
if (activation.approval !== void 0) return freezeDeep({
|
|
292
|
+
outcome: "approval-required",
|
|
293
|
+
grant: {
|
|
294
|
+
role: roleName,
|
|
295
|
+
permission: `activate:${roleName}`
|
|
296
|
+
},
|
|
297
|
+
reason: "human",
|
|
298
|
+
token
|
|
299
|
+
});
|
|
300
|
+
return freezeDeep({
|
|
301
|
+
outcome: "granted",
|
|
302
|
+
subject,
|
|
303
|
+
matched: {
|
|
304
|
+
role: roleName,
|
|
305
|
+
permission: `activate:${roleName}`
|
|
306
|
+
},
|
|
307
|
+
token,
|
|
308
|
+
elevation: membership
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/core/grant-conditions.ts
|
|
313
|
+
function evaluateGrantCondition(grant, permission, current, next, subject, now, scopes, relations, permitted) {
|
|
314
|
+
let unknown;
|
|
315
|
+
const onOpaque = () => {
|
|
316
|
+
unknown ??= "opaque-condition";
|
|
317
|
+
};
|
|
318
|
+
const related = (condition, row) => {
|
|
319
|
+
if (relations === void 0) {
|
|
320
|
+
unknown ??= "relation-unavailable";
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
const verdict = resolveRelated(condition, row, subject, now, relations, permitted);
|
|
324
|
+
if (verdict === "relation-depth" || verdict === "relation-unavailable") {
|
|
325
|
+
unknown ??= verdict;
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
return verdict;
|
|
329
|
+
};
|
|
330
|
+
if (!grant.portable && grant.closure !== void 0) try {
|
|
331
|
+
const result = grant.closure(next ?? current, {
|
|
332
|
+
subject,
|
|
333
|
+
actor: subject.actor,
|
|
334
|
+
delegation: subject.delegation,
|
|
335
|
+
context: subject.context
|
|
336
|
+
});
|
|
337
|
+
if (isThenable(result)) return {
|
|
338
|
+
matched: false,
|
|
339
|
+
reason: "closure-error",
|
|
340
|
+
cause: result
|
|
341
|
+
};
|
|
342
|
+
return { matched: result === true };
|
|
343
|
+
} catch (error) {
|
|
344
|
+
return {
|
|
345
|
+
matched: false,
|
|
346
|
+
reason: "closure-error",
|
|
347
|
+
cause: error
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
const deny = grant.effect === "deny";
|
|
351
|
+
let whereMatched;
|
|
352
|
+
if (grant.where !== void 0) {
|
|
353
|
+
if (permission.kind === "collection") return {
|
|
354
|
+
matched: false,
|
|
355
|
+
reason: "condition"
|
|
356
|
+
};
|
|
357
|
+
if (current === void 0) return {
|
|
358
|
+
matched: false,
|
|
359
|
+
reason: "condition"
|
|
360
|
+
};
|
|
361
|
+
if (grant.where.op === "opaque" || grant.check?.op === "opaque") return {
|
|
362
|
+
matched: false,
|
|
363
|
+
reason: "opaque-condition"
|
|
364
|
+
};
|
|
365
|
+
whereMatched = evaluateCondition(grant.where, current, subject, now, scopes, related, onOpaque);
|
|
366
|
+
if (unknown !== void 0) return {
|
|
367
|
+
matched: false,
|
|
368
|
+
reason: unknown
|
|
369
|
+
};
|
|
370
|
+
if (whereMatched && deny) return { matched: true };
|
|
371
|
+
if (!whereMatched && !deny) return {
|
|
372
|
+
matched: false,
|
|
373
|
+
reason: "condition"
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
const checkCondition = grant.check ?? (permission.action === "update" ? grant.where : void 0);
|
|
377
|
+
if (checkCondition === void 0 && whereMatched === false) return {
|
|
378
|
+
matched: false,
|
|
379
|
+
reason: "condition"
|
|
380
|
+
};
|
|
381
|
+
if (checkCondition !== void 0) {
|
|
382
|
+
if (checkCondition.op === "opaque") return {
|
|
383
|
+
matched: false,
|
|
384
|
+
reason: "opaque-condition"
|
|
385
|
+
};
|
|
386
|
+
if (next === void 0) return {
|
|
387
|
+
matched: false,
|
|
388
|
+
reason: "condition"
|
|
389
|
+
};
|
|
390
|
+
const matched = evaluateCondition(checkCondition, next, subject, now, scopes, related, onOpaque);
|
|
391
|
+
if (unknown !== void 0) return {
|
|
392
|
+
matched: false,
|
|
393
|
+
reason: unknown
|
|
394
|
+
};
|
|
395
|
+
if (!matched) return {
|
|
396
|
+
matched: false,
|
|
397
|
+
reason: "condition"
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
return { matched: true };
|
|
401
|
+
}
|
|
402
|
+
/** A grant whose condition could not be answered; a deny with one denies. */
|
|
403
|
+
function isUnevaluable(reason) {
|
|
404
|
+
return reason === "relation-depth" || reason === "relation-unavailable" || reason === "closure-error" || reason === "opaque-condition";
|
|
405
|
+
}
|
|
406
|
+
function shouldConsumeQuota(call, simulated) {
|
|
407
|
+
if (simulated) return false;
|
|
408
|
+
switch (call) {
|
|
409
|
+
case "can":
|
|
410
|
+
case "filter":
|
|
411
|
+
case "simulate":
|
|
412
|
+
case "explain": return false;
|
|
413
|
+
case "decide":
|
|
414
|
+
case "assert":
|
|
415
|
+
case "endpoint":
|
|
416
|
+
case "adapter":
|
|
417
|
+
case "approval":
|
|
418
|
+
case void 0: return true;
|
|
419
|
+
default: return call;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
function isDelegatedPermission(policy, permission) {
|
|
423
|
+
const providers = policy.providers;
|
|
424
|
+
if (providers === void 0 || providers.length === 0) return false;
|
|
425
|
+
for (const provider of providers) if (provider.handles(permission)) return true;
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
/** The `MatchedGrant` view of a grant, shared by `matched` and the trace. */
|
|
429
|
+
function matchedOf(grant, permissionKey, breakGlass) {
|
|
430
|
+
return compact({
|
|
431
|
+
role: grant.role,
|
|
432
|
+
permission: permissionKey,
|
|
433
|
+
name: grant.name,
|
|
434
|
+
meta: grant.meta,
|
|
435
|
+
to: grant.to,
|
|
436
|
+
where: grant.where,
|
|
437
|
+
check: grant.check,
|
|
438
|
+
approval: grant.approval,
|
|
439
|
+
hosted: grant.hosted,
|
|
440
|
+
breakGlass
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
//#endregion
|
|
444
|
+
//#region src/core/evaluation-alternatives.ts
|
|
445
|
+
function alternativesFor(evaluate, policy, permission, subject, now, env) {
|
|
446
|
+
return listPermissions(policy.permissions).filter((leaf) => leaf.resource === permission.resource && leaf.key !== permission.key).filter((leaf) => {
|
|
447
|
+
return evaluate(policy, subject, leaf, void 0, {
|
|
448
|
+
trusted: true,
|
|
449
|
+
call: "simulate",
|
|
450
|
+
now: instantAt(now)
|
|
451
|
+
}, {
|
|
452
|
+
...env,
|
|
453
|
+
emit: false,
|
|
454
|
+
skipAlternatives: true,
|
|
455
|
+
outcomeOnly: true
|
|
456
|
+
}).outcome === "granted";
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
function inheritedVerdict(evaluate, policy, subject, resource, id, key, now, env) {
|
|
460
|
+
const leaf = findPermission(policy.permissions, key);
|
|
461
|
+
const loaded = env.relations?.row({
|
|
462
|
+
resource,
|
|
463
|
+
id
|
|
464
|
+
});
|
|
465
|
+
if (leaf === void 0 || loaded === void 0) return "relation-unavailable";
|
|
466
|
+
if (loaded === "pending" || loaded === "failed") return "relation-unavailable";
|
|
467
|
+
if (loaded.row === null) return false;
|
|
468
|
+
const decision = evaluate(policy, subject, leaf, loaded.row, {
|
|
469
|
+
trusted: true,
|
|
470
|
+
call: "simulate",
|
|
471
|
+
now: instantAt(now)
|
|
472
|
+
}, {
|
|
473
|
+
...env,
|
|
474
|
+
emit: false,
|
|
475
|
+
skipAlternatives: true,
|
|
476
|
+
outcomeOnly: true
|
|
477
|
+
});
|
|
478
|
+
if (decision.outcome === "granted") return true;
|
|
479
|
+
const reasons = decision.outcome === "denied" ? decision.denials.map((denial) => denial.reason) : [];
|
|
480
|
+
return reasons.includes("relation-unavailable") ? "relation-unavailable" : reasons.includes("relation-depth") ? "relation-depth" : false;
|
|
481
|
+
}
|
|
482
|
+
//#endregion
|
|
483
|
+
//#region src/core/evaluation-trace.ts
|
|
484
|
+
function skip(tracer, grant, permissionKey, why) {
|
|
485
|
+
tracer?.skipped.push({
|
|
486
|
+
role: grant.role,
|
|
487
|
+
permission: permissionKey,
|
|
488
|
+
effect: grant.effect,
|
|
489
|
+
why
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
function traceOf(tracer) {
|
|
493
|
+
return {
|
|
494
|
+
evaluated: tracer.evaluated,
|
|
495
|
+
allows: tracer.allows,
|
|
496
|
+
denies: tracer.denies,
|
|
497
|
+
skipped: tracer.skipped
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
//#endregion
|
|
501
|
+
//#region src/core/events.ts
|
|
502
|
+
function emitSafe(listeners, payload, errors) {
|
|
503
|
+
for (const listener of listeners) try {
|
|
504
|
+
listener(payload);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
for (const handler of errors.error) try {
|
|
507
|
+
handler(error);
|
|
508
|
+
} catch {}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
function emptyListeners() {
|
|
512
|
+
return {
|
|
513
|
+
decision: /* @__PURE__ */ new Set(),
|
|
514
|
+
denied: /* @__PURE__ */ new Set(),
|
|
515
|
+
approval: /* @__PURE__ */ new Set(),
|
|
516
|
+
auth: /* @__PURE__ */ new Set(),
|
|
517
|
+
error: /* @__PURE__ */ new Set()
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
function credentialRef(subject) {
|
|
521
|
+
const value = subject.principal?.["credential"];
|
|
522
|
+
if (value === null || typeof value !== "object") return;
|
|
523
|
+
const { id, kind } = value;
|
|
524
|
+
return typeof id === "string" && (kind === "user" || kind === "service") ? {
|
|
525
|
+
id,
|
|
526
|
+
kind
|
|
527
|
+
} : void 0;
|
|
528
|
+
}
|
|
529
|
+
/** `context.supportSession` counts only next to the `support` actor `subjectFromSupportSession` sets. */
|
|
530
|
+
function supportSessionRef(subject) {
|
|
531
|
+
const value = subject.context["supportSession"];
|
|
532
|
+
if (subject.actor?.kind !== "support" || value === null || typeof value !== "object" || !("id" in value) || typeof value.id !== "string" || value.id === "") return;
|
|
533
|
+
return { id: value.id };
|
|
534
|
+
}
|
|
535
|
+
function finish(policy, subject, permission, data, decision, options, env, trusted, membership, counts) {
|
|
536
|
+
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;
|
|
537
|
+
if (!env.emit || !heard) return;
|
|
538
|
+
const resource = getResource(policy.permissions, permission.resource);
|
|
539
|
+
const resourceId = data !== null && typeof data === "object" ? data[resource?.id ?? "id"] : void 0;
|
|
540
|
+
const event = freezeDeep(compact({
|
|
541
|
+
type: "decision",
|
|
542
|
+
at: isoNow(),
|
|
543
|
+
outcome: decision.outcome,
|
|
544
|
+
permission: permission.key,
|
|
545
|
+
scope: permission.scope,
|
|
546
|
+
resource: compact({
|
|
547
|
+
type: permission.resource,
|
|
548
|
+
id: resourceId === void 0 ? void 0 : String(resourceId)
|
|
549
|
+
}),
|
|
550
|
+
subject: compact({
|
|
551
|
+
principal: subject.principal === null ? null : compact({
|
|
552
|
+
id: subject.principal.id,
|
|
553
|
+
roles: subject.principal.roles ?? [],
|
|
554
|
+
tenant: subject.principal.tenant
|
|
555
|
+
}),
|
|
556
|
+
actor: subject.actor === void 0 ? void 0 : {
|
|
557
|
+
id: subject.actor.id,
|
|
558
|
+
kind: subject.actor.kind
|
|
559
|
+
},
|
|
560
|
+
delegation: toWireDelegation(subject.delegation),
|
|
561
|
+
credential: credentialRef(subject),
|
|
562
|
+
supportSession: supportSessionRef(subject)
|
|
563
|
+
}),
|
|
564
|
+
tenant: subject.principal?.tenant,
|
|
565
|
+
membership: membership === void 0 ? void 0 : compact({
|
|
566
|
+
...membership,
|
|
567
|
+
expiresAt: eventInstant(membership.expiresAt)
|
|
568
|
+
}),
|
|
569
|
+
via: membership?.via ?? null,
|
|
570
|
+
matched: decision.outcome === "granted" ? compact({
|
|
571
|
+
role: decision.matched.role,
|
|
572
|
+
permission: decision.matched.permission,
|
|
573
|
+
name: decision.matched.name,
|
|
574
|
+
meta: decision.matched.meta,
|
|
575
|
+
to: decision.matched.to,
|
|
576
|
+
hosted: decision.matched.hosted,
|
|
577
|
+
breakGlass: decision.matched.breakGlass
|
|
578
|
+
}) : decision.outcome === "approval-required" ? compact({
|
|
579
|
+
role: decision.grant.role,
|
|
580
|
+
permission: decision.grant.permission,
|
|
581
|
+
name: decision.grant.name,
|
|
582
|
+
meta: decision.grant.meta,
|
|
583
|
+
to: decision.grant.to,
|
|
584
|
+
hosted: decision.grant.hosted
|
|
585
|
+
}) : void 0,
|
|
586
|
+
obligations: decision.outcome === "granted" && (decision.obligations?.length ?? 0) > 0 ? decision.obligations : void 0,
|
|
587
|
+
purpose: Array.isArray(subject.context["purpose"]) && subject.context["purpose"].length > 0 ? subject.context["purpose"] : void 0,
|
|
588
|
+
reason: typeof subject.context["reason"] === "string" && subject.context["reason"] !== "" ? subject.context["reason"] : void 0,
|
|
589
|
+
denials: decision.outcome === "denied" ? wireDenials(decision.denials) : void 0,
|
|
590
|
+
alternatives: decision.outcome === "denied" ? decision.alternatives.map((leaf) => leaf.key) : void 0,
|
|
591
|
+
token: decision.outcome === "granted" || decision.outcome === "approval-required" ? decision.token : void 0,
|
|
592
|
+
trusted,
|
|
593
|
+
call: options.call ?? "decide",
|
|
594
|
+
adapter: options.adapter,
|
|
595
|
+
model: options.model === "" ? void 0 : options.model,
|
|
596
|
+
counts,
|
|
597
|
+
x: options.x === void 0 ? void 0 : readAppData(void 0, options.x)
|
|
598
|
+
}));
|
|
599
|
+
emitSafe(env.listeners.decision, event, env.listeners);
|
|
600
|
+
if (decision.outcome === "denied") emitSafe(env.listeners.denied, event, env.listeners);
|
|
601
|
+
if (decision.outcome === "approval-required") emitSafe(env.listeners.approval, event, env.listeners);
|
|
602
|
+
if (env.sink !== void 0) try {
|
|
603
|
+
const written = env.sink.write([event]);
|
|
604
|
+
if (isThenable(written)) written.catch((error) => {
|
|
605
|
+
emitSafe(env.listeners.error, error, env.listeners);
|
|
606
|
+
});
|
|
607
|
+
} catch (error) {
|
|
608
|
+
emitSafe(env.listeners.error, error, env.listeners);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
//#endregion
|
|
612
|
+
//#region src/core/evaluate/complete.ts
|
|
613
|
+
/** Every exit of `evaluate`: names the permission, freezes, attaches the trace and emits. */
|
|
614
|
+
function complete(call, raw, membership) {
|
|
615
|
+
const { env, permission, tracer } = call;
|
|
616
|
+
const decision = env.outcomeOnly !== true && raw.outcome === "denied" && raw.permission === void 0 ? {
|
|
617
|
+
...raw,
|
|
618
|
+
permission: permission.key
|
|
619
|
+
} : raw;
|
|
620
|
+
const final = env.outcomeOnly === true ? decision : freezeDeep(tracer === void 0 ? decision : {
|
|
621
|
+
...decision,
|
|
622
|
+
trace: traceOf(tracer)
|
|
623
|
+
});
|
|
624
|
+
finish(call.policy, call.given, permission, call.current, final, call.options, env, call.options.trusted === true, membership);
|
|
625
|
+
return final;
|
|
626
|
+
}
|
|
627
|
+
function deniedNow(call, denial) {
|
|
628
|
+
return complete(call, {
|
|
629
|
+
outcome: "denied",
|
|
630
|
+
denials: [denial],
|
|
631
|
+
alternatives: []
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
function deniedWithAlternatives(state, denials) {
|
|
635
|
+
return complete(state, {
|
|
636
|
+
outcome: "denied",
|
|
637
|
+
denials,
|
|
638
|
+
alternatives: state.env.skipAlternatives ? [] : alternativesFor(state.evaluate, state.policy, state.permission, state.subject, state.now, state.env)
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
//#endregion
|
|
642
|
+
//#region src/core/evaluate/allowed.ts
|
|
643
|
+
/** The `granted` or `approval-required` decision for the chosen allow, with its token. */
|
|
644
|
+
function allowedDecision(state, allow, approval, quotaState) {
|
|
645
|
+
const { policy, subject, permission, resource, env, current, next } = state;
|
|
646
|
+
const resourceId = permission.kind === "collection" ? "*" : rowIdOf(current, resource?.id);
|
|
647
|
+
const version = approval !== void 0 && approval !== "human" && approval.staleOn === "resource-change" && resource?.version !== void 0 ? versionOf(current, resource.version) : void 0;
|
|
648
|
+
const token = env.simulated ? "pd1.simulated" : env.outcomeOnly === true ? "pd1.outcome" : decisionToken({
|
|
649
|
+
key: permission.key,
|
|
650
|
+
resourceId,
|
|
651
|
+
principal: subject.principal,
|
|
652
|
+
actor: subject.actor,
|
|
653
|
+
fingerprint: policy.fingerprint,
|
|
654
|
+
version,
|
|
655
|
+
payload: tokenPayload(resourceId, next ?? current, state.options.args)
|
|
656
|
+
});
|
|
657
|
+
const matched = compact({
|
|
658
|
+
...matchedOf(allow.grant, permission.key, allow.breakGlass),
|
|
659
|
+
approval
|
|
660
|
+
});
|
|
661
|
+
const obligations = [
|
|
662
|
+
...quotaState.obligations ?? [],
|
|
663
|
+
...allow.obligations ?? [],
|
|
664
|
+
...allow.grant.obligations ?? []
|
|
665
|
+
];
|
|
666
|
+
return complete(state, requiresApproval(approval) ? {
|
|
667
|
+
outcome: "approval-required",
|
|
668
|
+
grant: matched,
|
|
669
|
+
reason: "human",
|
|
670
|
+
token
|
|
671
|
+
} : {
|
|
672
|
+
outcome: "granted",
|
|
673
|
+
subject,
|
|
674
|
+
matched,
|
|
675
|
+
token,
|
|
676
|
+
...quotaState,
|
|
677
|
+
...obligations.length === 0 ? {} : { obligations }
|
|
678
|
+
}, allow.membership);
|
|
679
|
+
}
|
|
680
|
+
//#endregion
|
|
681
|
+
//#region src/core/custom-roles.ts
|
|
682
|
+
/** The current key for a stored key, current or former; `undefined` when neither. */
|
|
683
|
+
function currentKey(policy, raw) {
|
|
684
|
+
const leaf = findPermission(policy.permissions, raw);
|
|
685
|
+
if (leaf === void 0) return;
|
|
686
|
+
return leaf.key === raw || formerKeys(leaf).includes(raw) ? leaf.key : void 0;
|
|
687
|
+
}
|
|
688
|
+
const GRANT_KEYS = /* @__PURE__ */ new Set([
|
|
689
|
+
"permission",
|
|
690
|
+
"effect",
|
|
691
|
+
"level"
|
|
692
|
+
]);
|
|
693
|
+
/** `where` narrowed by a level; the empty level (`{}`) leaves it as is. */
|
|
694
|
+
function andWhere(where, level) {
|
|
695
|
+
if (level.op === "and" && level.conditions.length === 0) return where;
|
|
696
|
+
return where === void 0 ? level : {
|
|
697
|
+
op: "and",
|
|
698
|
+
conditions: [where, level]
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
/** A ceiling grant narrowed by a level's condition and marked with the level. */
|
|
702
|
+
function leveled(grant, level, condition) {
|
|
703
|
+
const where = andWhere(grant.where, condition);
|
|
704
|
+
const check = grant.check === void 0 ? void 0 : andWhere(grant.check, condition);
|
|
705
|
+
return freezeDeep({
|
|
706
|
+
...grant,
|
|
707
|
+
level,
|
|
708
|
+
...where === void 0 ? {} : { where },
|
|
709
|
+
...check === void 0 ? {} : { check }
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* The scope a custom role is held at: `scope` (a name or alias), the second
|
|
714
|
+
* scope for the `team` input shape, else the first scope. `undefined` when
|
|
715
|
+
* the policy does not declare it; such a role resolves to nothing.
|
|
716
|
+
*/
|
|
717
|
+
function customRoleScope(role, scopes) {
|
|
718
|
+
if (role.scope === "global") return role.shared === true ? void 0 : "global";
|
|
719
|
+
if (role.scope !== void 0) return resolveScope(scopes, role.scope);
|
|
720
|
+
return resolveScope(scopes, role.team === void 0 ? "tenant" : "team");
|
|
721
|
+
}
|
|
722
|
+
/** The instance of its scope a custom role is pinned to, if any. */
|
|
723
|
+
function customRoleId(role) {
|
|
724
|
+
return role.scope === void 0 ? role.team : role.id;
|
|
725
|
+
}
|
|
726
|
+
/** A platform custom role: held through `principal.roles`, capped by the assignable global roles. */
|
|
727
|
+
function isGlobalCustomRole(role) {
|
|
728
|
+
return role.scope === "global";
|
|
729
|
+
}
|
|
730
|
+
/** A shared custom role: defined once for every tenant, held on memberships of its scope in any tenant. */
|
|
731
|
+
function isSharedCustomRole(role) {
|
|
732
|
+
return role.shared === true;
|
|
733
|
+
}
|
|
734
|
+
function isCeilingScope(scope) {
|
|
735
|
+
return typeof scope === "string";
|
|
736
|
+
}
|
|
737
|
+
function soleRole(grant) {
|
|
738
|
+
const roles = flattenGrantee(grant.to).filter((item) => item.kind === "role");
|
|
739
|
+
const first = roles[0];
|
|
740
|
+
return roles.length === 1 && first?.kind === "role" ? first.role : void 0;
|
|
741
|
+
}
|
|
742
|
+
/** Permission keys a declared role's allows reach, hosted grants included. */
|
|
743
|
+
function roleAllowKeys(policy, name) {
|
|
744
|
+
return new Set(grantList(policy).filter((grant) => grant.effect === "allow" && soleRole(grant) === name).map((grant) => grant.permission.key));
|
|
745
|
+
}
|
|
746
|
+
function isAssignableRole(policy, name) {
|
|
747
|
+
const binding = policy.rolesByName.get(name);
|
|
748
|
+
if (binding !== void 0) return binding.assignable;
|
|
749
|
+
return findRole(policy.vocabulary?.roles, name)?.assignable === true;
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Every scope's ceiling, keyed by scope name (`global` for platform custom
|
|
753
|
+
* roles): the code allows of declared `assignable` roles in that scope,
|
|
754
|
+
* everything a custom role of the scope may ever reach. Hosted grants never
|
|
755
|
+
* widen it. `assignable` narrows it to those role names (`CustomRoleSource.assignable`).
|
|
756
|
+
*/
|
|
757
|
+
function ceilings(policy, assignable) {
|
|
758
|
+
const allowed = assignable === void 0 ? void 0 : new Set(assignable);
|
|
759
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
760
|
+
for (const grant of grantList(policy)) {
|
|
761
|
+
const name = soleRole(grant);
|
|
762
|
+
if (grant.effect !== "allow" || grant.hosted !== void 0 || !isCeilingScope(grant.scope) || name === void 0 || !isAssignableRole(policy, name) || allowed !== void 0 && !allowed.has(name)) continue;
|
|
763
|
+
const list = byScope.get(grant.scope) ?? [];
|
|
764
|
+
list.push(grant);
|
|
765
|
+
byScope.set(grant.scope, list);
|
|
766
|
+
}
|
|
767
|
+
return byScope;
|
|
768
|
+
}
|
|
769
|
+
function ceilingGrants(policy, scope, assignable) {
|
|
770
|
+
return ceilings(policy, assignable).get(scope) ?? [];
|
|
771
|
+
}
|
|
772
|
+
function retarget(grant, role, scope) {
|
|
773
|
+
const grantee = {
|
|
774
|
+
kind: "role",
|
|
775
|
+
role: role.name,
|
|
776
|
+
scope
|
|
777
|
+
};
|
|
778
|
+
const others = flattenGrantee(grant.to).filter((item) => item.kind !== "role");
|
|
779
|
+
return freezeDeep({
|
|
780
|
+
...grant,
|
|
781
|
+
to: others.length === 0 ? grantee : [grantee, ...others],
|
|
782
|
+
role: role.name,
|
|
783
|
+
scope
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* The one resolver behind evaluation, snapshots and `validateCustomRole`.
|
|
788
|
+
* Included roles' grants plus the role's own allows, minus its denies, all
|
|
789
|
+
* intersected with the ceiling. An allow inherits the condition, approval and
|
|
790
|
+
* limit of the declared grant it comes from: an included assignable role's
|
|
791
|
+
* grant when there is one, otherwise every ceiling grant of that permission,
|
|
792
|
+
* together with the denies of the roles those grants belong to.
|
|
793
|
+
*/
|
|
794
|
+
function resolveCustomRole(policy, role) {
|
|
795
|
+
const scope = customRoleScope(role, scopeList(policy.scopes));
|
|
796
|
+
if (scope === void 0) return freezeDeep({
|
|
797
|
+
grants: [],
|
|
798
|
+
dropped: [],
|
|
799
|
+
renamed: []
|
|
800
|
+
});
|
|
801
|
+
const ceiling = ceilingGrants(policy, scope);
|
|
802
|
+
const ceilingSet = new Set(ceiling);
|
|
803
|
+
const ceilingKeys = new Set(ceiling.map((grant) => grant.permission.key));
|
|
804
|
+
const declared = declaredRoleNames(policy);
|
|
805
|
+
const all = grantList(policy).filter((grant) => grant.hosted === void 0);
|
|
806
|
+
const dropped = [];
|
|
807
|
+
const renamed = [];
|
|
808
|
+
const seen = /* @__PURE__ */ new Set();
|
|
809
|
+
const drop = (entry) => {
|
|
810
|
+
const id = "role" in entry ? `role:${entry.role}` : `${entry.reason}:${entry.permission}`;
|
|
811
|
+
if (!seen.has(id)) {
|
|
812
|
+
seen.add(id);
|
|
813
|
+
dropped.push(entry);
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
const included = [];
|
|
817
|
+
const includedKeys = /* @__PURE__ */ new Set();
|
|
818
|
+
const allowKeys = /* @__PURE__ */ new Map();
|
|
819
|
+
const denyKeys = /* @__PURE__ */ new Set();
|
|
820
|
+
const allowAll = (key) => {
|
|
821
|
+
allowKeys.set(key, "all");
|
|
822
|
+
};
|
|
823
|
+
for (const name of Array.isArray(role.includes) ? role.includes : []) {
|
|
824
|
+
if (typeof name !== "string" || !declared.has(name)) {
|
|
825
|
+
drop({
|
|
826
|
+
role: String(name),
|
|
827
|
+
reason: "unknown-role"
|
|
828
|
+
});
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
for (const grant of all) {
|
|
832
|
+
if (soleRole(grant) !== name) continue;
|
|
833
|
+
const key = grant.permission.key;
|
|
834
|
+
if (grant.effect === "deny") {
|
|
835
|
+
if (grant.scope === scope) included.push(grant);
|
|
836
|
+
} else if (ceilingSet.has(grant)) {
|
|
837
|
+
included.push(grant);
|
|
838
|
+
includedKeys.add(key);
|
|
839
|
+
} else if (ceilingKeys.has(key)) allowAll(key);
|
|
840
|
+
else drop({
|
|
841
|
+
permission: key,
|
|
842
|
+
reason: "outside-ceiling"
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const entries = Array.isArray(role.grants) ? role.grants : [];
|
|
847
|
+
for (const item of entries) {
|
|
848
|
+
if (item === null || typeof item !== "object") {
|
|
849
|
+
drop({
|
|
850
|
+
permission: String(item),
|
|
851
|
+
reason: "unknown-permission"
|
|
852
|
+
});
|
|
853
|
+
continue;
|
|
854
|
+
}
|
|
855
|
+
const entry = item;
|
|
856
|
+
const raw = entry.permission;
|
|
857
|
+
const key = typeof raw === "string" ? currentKey(policy, raw) : void 0;
|
|
858
|
+
if (key === void 0) {
|
|
859
|
+
drop({
|
|
860
|
+
permission: String(raw),
|
|
861
|
+
reason: "unknown-permission"
|
|
862
|
+
});
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (key !== raw) renamed.push({
|
|
866
|
+
from: String(raw),
|
|
867
|
+
to: key
|
|
868
|
+
});
|
|
869
|
+
const effect = entry.effect ?? "allow";
|
|
870
|
+
const extra = Object.keys(entry).some((name) => !GRANT_KEYS.has(name));
|
|
871
|
+
const level = entry.level;
|
|
872
|
+
if (extra || effect !== "allow" && effect !== "deny" || effect === "deny" && level !== void 0) {
|
|
873
|
+
drop({
|
|
874
|
+
permission: key,
|
|
875
|
+
reason: "condition-not-allowed"
|
|
876
|
+
});
|
|
877
|
+
denyKeys.add(key);
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if (effect === "deny") {
|
|
881
|
+
denyKeys.add(key);
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
let narrowing;
|
|
885
|
+
if (level !== void 0) {
|
|
886
|
+
const leaf = findPermission(policy.permissions, key);
|
|
887
|
+
const condition = typeof level === "string" && leaf?.kind === "instance" ? levelCondition(policy, leaf.resource, level) : void 0;
|
|
888
|
+
if (condition === void 0) {
|
|
889
|
+
drop({
|
|
890
|
+
permission: key,
|
|
891
|
+
reason: "unknown-level",
|
|
892
|
+
level: String(level)
|
|
893
|
+
});
|
|
894
|
+
denyKeys.add(key);
|
|
895
|
+
continue;
|
|
896
|
+
}
|
|
897
|
+
narrowing = [String(level), condition];
|
|
898
|
+
}
|
|
899
|
+
if (!ceilingKeys.has(key)) {
|
|
900
|
+
drop({
|
|
901
|
+
permission: key,
|
|
902
|
+
reason: "outside-ceiling"
|
|
903
|
+
});
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
const held = allowKeys.get(key);
|
|
907
|
+
if (narrowing === void 0) allowAll(key);
|
|
908
|
+
else if (held === void 0) allowKeys.set(key, new Map([narrowing]));
|
|
909
|
+
else if (held !== "all") held.set(...narrowing);
|
|
910
|
+
}
|
|
911
|
+
const out = [];
|
|
912
|
+
const push = (grant) => {
|
|
913
|
+
if (!out.includes(grant)) out.push(grant);
|
|
914
|
+
};
|
|
915
|
+
for (const grant of included) if (grant.effect === "deny" || !denyKeys.has(grant.permission.key)) push(grant);
|
|
916
|
+
for (const [key, levels] of allowKeys) {
|
|
917
|
+
if (denyKeys.has(key) || includedKeys.has(key)) continue;
|
|
918
|
+
const sources = /* @__PURE__ */ new Set();
|
|
919
|
+
for (const grant of ceiling) {
|
|
920
|
+
if (grant.permission.key !== key) continue;
|
|
921
|
+
sources.add(soleRole(grant) ?? "");
|
|
922
|
+
if (levels === "all") {
|
|
923
|
+
push(grant);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
for (const [level, condition] of levels) push(leveled(grant, level, condition));
|
|
927
|
+
}
|
|
928
|
+
for (const grant of all) if (grant.effect === "deny" && grant.permission.key === key && sources.has(soleRole(grant) ?? "")) push(grant);
|
|
929
|
+
}
|
|
930
|
+
return freezeDeep({
|
|
931
|
+
grants: out.map((grant) => retarget(grant, role, scope)),
|
|
932
|
+
dropped,
|
|
933
|
+
renamed
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
function validateCustomRole(policy, role, options = {}) {
|
|
937
|
+
if (!isSharedCustomRole(role) && !isGlobalCustomRole(role) && (options.shared ?? []).some((item) => wellFormed(item) && isSharedCustomRole(item) && item.name === role.name)) return freezeDeep({
|
|
938
|
+
ok: false,
|
|
939
|
+
permissions: [],
|
|
940
|
+
dropped: [{
|
|
941
|
+
role: role.name,
|
|
942
|
+
reason: "name-taken"
|
|
943
|
+
}],
|
|
944
|
+
renamed: []
|
|
945
|
+
});
|
|
946
|
+
const resolved = resolveCustomRole(policy, role);
|
|
947
|
+
const permissions = [...new Set(resolved.grants.filter((grant) => grant.effect === "allow").map((grant) => grant.permission.key))].toSorted();
|
|
948
|
+
return freezeDeep({
|
|
949
|
+
ok: resolved.dropped.length === 0,
|
|
950
|
+
permissions,
|
|
951
|
+
dropped: resolved.dropped,
|
|
952
|
+
renamed: resolved.renamed
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
function wellFormed(role) {
|
|
956
|
+
if (role === null || typeof role !== "object" || typeof role.name !== "string") return false;
|
|
957
|
+
if (role.shared !== void 0) return role.shared === true && role.tenant === void 0 && role.team === void 0 && role.id === void 0 && (role.scope === void 0 || typeof role.scope === "string" && role.scope !== "global");
|
|
958
|
+
if (role.scope === "global") return role.tenant === void 0 && role.team === void 0 && role.id === void 0;
|
|
959
|
+
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");
|
|
960
|
+
}
|
|
961
|
+
/**
|
|
962
|
+
* The well-formed roles, without a tenant role that a shared role of the same
|
|
963
|
+
* name shadows: the shared role is the one every tenant sees.
|
|
964
|
+
*/
|
|
965
|
+
function effectiveCustomRoles(roles) {
|
|
966
|
+
const formed = roles.filter(wellFormed);
|
|
967
|
+
const shared = new Set(formed.filter(isSharedCustomRole).map((role) => role.name));
|
|
968
|
+
return shared.size === 0 ? formed : formed.filter((role) => isSharedCustomRole(role) || isGlobalCustomRole(role) || !shared.has(role.name));
|
|
969
|
+
}
|
|
970
|
+
/** Every custom role's resolved grants, once per instance. Malformed roles resolve to nothing. */
|
|
971
|
+
function customGrantsFor(policy, roles) {
|
|
972
|
+
const declared = declaredRoleNames(policy);
|
|
973
|
+
const out = [];
|
|
974
|
+
for (const role of effectiveCustomRoles(roles)) {
|
|
975
|
+
if (declared.has(role.name)) continue;
|
|
976
|
+
for (const grant of resolveCustomRole(policy, role).grants) out.push({
|
|
977
|
+
grant,
|
|
978
|
+
role
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
return out;
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* The well-formed custom roles held on memberships: tenant and shared, not
|
|
985
|
+
* platform. A declared role name is never one, and a shared role shadows a
|
|
986
|
+
* tenant role of the same name.
|
|
987
|
+
*/
|
|
988
|
+
function tenantCustomRoles(policy, roles) {
|
|
989
|
+
const declared = declaredRoleNames(policy);
|
|
990
|
+
return effectiveCustomRoles(roles).filter((role) => !isGlobalCustomRole(role) && !declared.has(role.name));
|
|
991
|
+
}
|
|
992
|
+
/** Whether a custom role held on memberships belongs to `tenant`: its own, or shared by every tenant. */
|
|
993
|
+
function customRoleOf(role, tenant) {
|
|
994
|
+
return isSharedCustomRole(role) || sameId(role.tenant, tenant);
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* A membership at the role's scope, inside its tenant (and instance, when
|
|
998
|
+
* pinned), naming it. A shared role is held at its scope in any tenant.
|
|
999
|
+
*/
|
|
1000
|
+
function holdsCustomRole(membership, role, scopes) {
|
|
1001
|
+
if (isGlobalCustomRole(role) || !wellFormed(role)) return false;
|
|
1002
|
+
const scope = customRoleScope(role, scopes);
|
|
1003
|
+
const id = customRoleId(role);
|
|
1004
|
+
const tenant = tenantOf(membership, scopes);
|
|
1005
|
+
return scope !== void 0 && membership.scope === scope && (isSharedCustomRole(role) ? tenant !== void 0 : sameId(tenant, role.tenant)) && (id === void 0 || sameId(membership.id, id)) && membership.roles.includes(role.name);
|
|
1006
|
+
}
|
|
1007
|
+
/** Whether `principal.roles` names the platform custom role. */
|
|
1008
|
+
function holdsGlobalCustomRole(roles, role) {
|
|
1009
|
+
return isGlobalCustomRole(role) && (roles ?? []).includes(role.name);
|
|
1010
|
+
}
|
|
1011
|
+
/** A tenant's custom role inside that tenant, a shared role in any tenant, or a platform custom role anywhere. */
|
|
1012
|
+
function isCustomRoleName(name, roles, tenant) {
|
|
1013
|
+
return effectiveCustomRoles(roles).some((item) => item.name === name && (isGlobalCustomRole(item) || tenant !== void 0 && customRoleOf(item, tenant)));
|
|
1014
|
+
}
|
|
1015
|
+
/**
|
|
1016
|
+
* The compact JWT form of custom roles for the `memberships[].grants` claim
|
|
1017
|
+
* RLS reads in `jwt` mode: role name to entries, where `key` allows, `-key`
|
|
1018
|
+
* denies and `@role` includes. Put a team custom role on its team membership.
|
|
1019
|
+
* Pass `policy` to write keys stored under a former name as the current key.
|
|
1020
|
+
*/
|
|
1021
|
+
function customRoleClaim(roles, policy) {
|
|
1022
|
+
const keyOf = (raw) => policy === void 0 ? raw : currentKey(policy, raw) ?? raw;
|
|
1023
|
+
const claim = {};
|
|
1024
|
+
for (const role of roles) {
|
|
1025
|
+
if (!wellFormed(role) || isForbiddenKey(role.name)) continue;
|
|
1026
|
+
const entries = claim[role.name] ?? [];
|
|
1027
|
+
const includes = Array.isArray(role.includes) ? role.includes : [];
|
|
1028
|
+
const grants = Array.isArray(role.grants) ? role.grants : [];
|
|
1029
|
+
for (const name of includes) entries.push(`@${name}`);
|
|
1030
|
+
for (const grant of grants) {
|
|
1031
|
+
if (grant.effect === "deny") {
|
|
1032
|
+
entries.push(`-${keyOf(grant.permission)}`);
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
const level = grant.level;
|
|
1036
|
+
entries.push(typeof level === "string" && level !== "" ? `${keyOf(grant.permission)}@${level}` : keyOf(grant.permission));
|
|
1037
|
+
}
|
|
1038
|
+
claim[role.name] = entries;
|
|
1039
|
+
}
|
|
1040
|
+
return freezeDeep(claim);
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* The `memberships` claim RLS reads in `jwt` mode: each membership canonical,
|
|
1044
|
+
* with the custom roles it holds as the compact `grants` map.
|
|
1045
|
+
*/
|
|
1046
|
+
function membershipsClaim(memberships, customRoles, scopes) {
|
|
1047
|
+
const claim = [];
|
|
1048
|
+
const effective = effectiveCustomRoles(customRoles);
|
|
1049
|
+
for (const membership of normalizeMemberships(memberships, scopes)) {
|
|
1050
|
+
const held = effective.filter((role) => holdsCustomRole(membership, role, scopes));
|
|
1051
|
+
claim.push(held.length === 0 ? membership : Object.assign({}, membership, { grants: customRoleClaim(held) }));
|
|
1052
|
+
}
|
|
1053
|
+
return claim;
|
|
1054
|
+
}
|
|
1055
|
+
//#endregion
|
|
1056
|
+
//#region src/core/source-name.ts
|
|
1057
|
+
/** `{ name }` for a source that names itself, `{}` otherwise: what an auth event reports about it. */
|
|
1058
|
+
function sourceName(source) {
|
|
1059
|
+
return source?.name === void 0 ? {} : { name: source.name };
|
|
1060
|
+
}
|
|
1061
|
+
//#endregion
|
|
1062
|
+
//#region src/core/role-loading.ts
|
|
1063
|
+
function isGlobalRole(value) {
|
|
1064
|
+
return value !== null && typeof value === "object" && Reflect.get(value, "scope") === "global";
|
|
1065
|
+
}
|
|
1066
|
+
function onlyGlobal(roles) {
|
|
1067
|
+
return Array.isArray(roles) ? roles.filter(isGlobalRole) : [];
|
|
1068
|
+
}
|
|
1069
|
+
function isSharedRole(value) {
|
|
1070
|
+
return value !== null && typeof value === "object" && Reflect.get(value, "shared") === true;
|
|
1071
|
+
}
|
|
1072
|
+
function onlyShared(roles) {
|
|
1073
|
+
return Array.isArray(roles) ? roles.filter(isSharedRole) : [];
|
|
1074
|
+
}
|
|
1075
|
+
/** The `customRoles` option as a source: a factory is called with the resolved subject, and a throw reads none. */
|
|
1076
|
+
function roleSourceFor(option, subject, auth) {
|
|
1077
|
+
if (typeof option !== "function") return option;
|
|
1078
|
+
try {
|
|
1079
|
+
return option(subject);
|
|
1080
|
+
} catch {
|
|
1081
|
+
auth.push({
|
|
1082
|
+
reason: "source-threw",
|
|
1083
|
+
source: "customRoles"
|
|
1084
|
+
});
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
function customRolesFor(source, tenants, auth, globalHeld, heldIn) {
|
|
1089
|
+
if (source === void 0) return [];
|
|
1090
|
+
const loaded = [];
|
|
1091
|
+
for (const tenant of tenants) try {
|
|
1092
|
+
loaded.push(source.rolesFor(tenant, { held: heldIn(tenant) }));
|
|
1093
|
+
} catch {
|
|
1094
|
+
auth.push({
|
|
1095
|
+
reason: "source-threw",
|
|
1096
|
+
source: "customRoles",
|
|
1097
|
+
...sourceName(source)
|
|
1098
|
+
});
|
|
1099
|
+
loaded.push([]);
|
|
1100
|
+
}
|
|
1101
|
+
const once = (read, keep) => {
|
|
1102
|
+
try {
|
|
1103
|
+
const roles = read();
|
|
1104
|
+
loaded.push(isThenable(roles) ? roles.then(keep) : keep(roles));
|
|
1105
|
+
} catch {
|
|
1106
|
+
auth.push({
|
|
1107
|
+
reason: "source-threw",
|
|
1108
|
+
source: "customRoles",
|
|
1109
|
+
...sourceName(source)
|
|
1110
|
+
});
|
|
1111
|
+
loaded.push([]);
|
|
1112
|
+
}
|
|
1113
|
+
};
|
|
1114
|
+
if (globalHeld !== void 0 && source.globalRoles !== void 0) {
|
|
1115
|
+
const read = source.globalRoles.bind(source);
|
|
1116
|
+
once(() => read({ held: globalHeld }), onlyGlobal);
|
|
1117
|
+
}
|
|
1118
|
+
if (tenants.length > 0 && source.sharedRoles !== void 0) {
|
|
1119
|
+
const read = source.sharedRoles.bind(source);
|
|
1120
|
+
const held = [...new Set(tenants.flatMap((tenant) => heldIn(tenant)))];
|
|
1121
|
+
once(() => read({ held }), onlyShared);
|
|
1122
|
+
}
|
|
1123
|
+
if (loaded.some((item) => isThenable(item))) return Promise.all(loaded.map((item) => Promise.resolve(item).catch(() => {
|
|
1124
|
+
auth.push({
|
|
1125
|
+
reason: "source-threw",
|
|
1126
|
+
source: "customRoles",
|
|
1127
|
+
...sourceName(source)
|
|
1128
|
+
});
|
|
1129
|
+
return [];
|
|
1130
|
+
}))).then((lists) => lists.flat());
|
|
1131
|
+
return loaded.flat();
|
|
1132
|
+
}
|
|
1133
|
+
function stringList(value) {
|
|
1134
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
1135
|
+
}
|
|
1136
|
+
/**
|
|
1137
|
+
* `CustomRoleSource.assignable` per tenant; `undefined` when the source has no
|
|
1138
|
+
* `assignable`. A tenant whose call throws or answers junk assigns nothing.
|
|
1139
|
+
*/
|
|
1140
|
+
function assignableNamesFor(source, tenants, auth) {
|
|
1141
|
+
if (source?.assignable === void 0) return;
|
|
1142
|
+
const failed = () => {
|
|
1143
|
+
auth.push({
|
|
1144
|
+
reason: "source-threw",
|
|
1145
|
+
source: "customRoles",
|
|
1146
|
+
...sourceName(source)
|
|
1147
|
+
});
|
|
1148
|
+
return [];
|
|
1149
|
+
};
|
|
1150
|
+
const loaded = tenants.map((tenant) => {
|
|
1151
|
+
try {
|
|
1152
|
+
return source.assignable?.(tenant) ?? [];
|
|
1153
|
+
} catch {
|
|
1154
|
+
return failed();
|
|
1155
|
+
}
|
|
1156
|
+
});
|
|
1157
|
+
const toMap = (lists) => new Map(tenants.map((tenant, index) => [tenant, stringList(lists[index])]));
|
|
1158
|
+
if (loaded.some((item) => isThenable(item))) return Promise.all(loaded.map((item) => Promise.resolve(item).catch(failed))).then(toMap);
|
|
1159
|
+
return toMap(loaded);
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* The declared role names among `names`. A custom role of `tenant` is known
|
|
1163
|
+
* but contributes no declared name: its grants are resolved separately
|
|
1164
|
+
* (`customGrantsFor`), bounded by the ceiling. A tenant's custom role is
|
|
1165
|
+
* unknown inside another tenant and for a membership without one.
|
|
1166
|
+
*/
|
|
1167
|
+
function expandRoleNames(names, declared, custom, tenant) {
|
|
1168
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
1169
|
+
const unknown = [];
|
|
1170
|
+
for (const name of names) if (declared.has(name)) resolved.add(name);
|
|
1171
|
+
else if (!isCustomRoleName(name, custom, tenant)) unknown.push(name);
|
|
1172
|
+
return {
|
|
1173
|
+
roles: [...resolved],
|
|
1174
|
+
unknown
|
|
1175
|
+
};
|
|
1176
|
+
}
|
|
1177
|
+
//#endregion
|
|
1178
|
+
//#region src/core/validation.ts
|
|
1179
|
+
function validateBoundary(permission, resource, data, mode, trusted, boundary = "manual") {
|
|
1180
|
+
if (!(mode === "always" || mode === "boundary" && !trusted)) return data;
|
|
1181
|
+
if (resource?.schema === void 0) {
|
|
1182
|
+
if (mode === "always") throw new PermDockValidationError({
|
|
1183
|
+
code: "no-schema",
|
|
1184
|
+
permission: permission.key,
|
|
1185
|
+
resource: permission.resource,
|
|
1186
|
+
boundary,
|
|
1187
|
+
message: `${permission.key}: no schema for resource ${permission.resource}.`
|
|
1188
|
+
});
|
|
1189
|
+
return data;
|
|
1190
|
+
}
|
|
1191
|
+
const result = resource.schema["~standard"].validate(data);
|
|
1192
|
+
if (isThenable(result)) {
|
|
1193
|
+
ignoreRejection(result);
|
|
1194
|
+
throw new PermDockValidationError({
|
|
1195
|
+
code: "async-schema",
|
|
1196
|
+
permission: permission.key,
|
|
1197
|
+
resource: permission.resource,
|
|
1198
|
+
boundary,
|
|
1199
|
+
message: `${permission.key}: schema for ${permission.resource} is async.`
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
if ("issues" in result && result.issues !== void 0) throw new PermDockValidationError({
|
|
1203
|
+
code: "invalid-data",
|
|
1204
|
+
permission: permission.key,
|
|
1205
|
+
resource: permission.resource,
|
|
1206
|
+
issues: result.issues,
|
|
1207
|
+
boundary,
|
|
1208
|
+
message: validationMessage(permission, resource, result.issues)
|
|
1209
|
+
});
|
|
1210
|
+
return result.value;
|
|
1211
|
+
}
|
|
1212
|
+
function validationMessage(permission, resource, issues) {
|
|
1213
|
+
const parts = issues.map((issue) => {
|
|
1214
|
+
const path = issue.path?.map((item) => typeof item === "object" && "key" in item ? String(item.key) : String(item)).join(".") ?? "/";
|
|
1215
|
+
return `invalid ${resource.name} data at ${path}: ${issue.message}`;
|
|
1216
|
+
});
|
|
1217
|
+
return `${permission.key}: ${parts.join("; ")}.`;
|
|
1218
|
+
}
|
|
1219
|
+
//#endregion
|
|
1220
|
+
//#region src/core/evaluate/call.ts
|
|
1221
|
+
/** The rows after boundary validation; on `invalid`, the rows validated before the failure. */
|
|
1222
|
+
function validatedRows(call, data, rows) {
|
|
1223
|
+
const { permission, policy, options } = call;
|
|
1224
|
+
if (data === void 0 && !(permission.kind === "instance" && policy.validate === "always")) return rows;
|
|
1225
|
+
const check = (value) => validateBoundary(permission, call.resource, value, policy.validate, options.trusted === true, options.boundary ?? "manual");
|
|
1226
|
+
let current = rows.current;
|
|
1227
|
+
try {
|
|
1228
|
+
if (permission.kind === "instance" && isRowPair(data)) {
|
|
1229
|
+
current = check(data.current);
|
|
1230
|
+
return {
|
|
1231
|
+
current,
|
|
1232
|
+
next: check(data.next)
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
const validated = check(data);
|
|
1236
|
+
return permission.kind === "instance" ? {
|
|
1237
|
+
current: validated,
|
|
1238
|
+
next: validated
|
|
1239
|
+
} : {
|
|
1240
|
+
current,
|
|
1241
|
+
next: validated
|
|
1242
|
+
};
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
if (error instanceof PermDockValidationError && error.code === "invalid-data") return {
|
|
1245
|
+
current,
|
|
1246
|
+
next: rows.next,
|
|
1247
|
+
invalid: error
|
|
1248
|
+
};
|
|
1249
|
+
throw error;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
function heldRoles(call, scopes, declared) {
|
|
1253
|
+
const { subject, env } = call;
|
|
1254
|
+
const global = expandRoleNames(subject.principal?.roles ?? [], declared, env.customRoles, subject.principal?.tenant);
|
|
1255
|
+
if (subject.principal !== null && global.unknown.length > 0) emitSafe(env.listeners.auth, {
|
|
1256
|
+
reason: "unknown-role",
|
|
1257
|
+
source: "roles"
|
|
1258
|
+
}, env.listeners);
|
|
1259
|
+
const matching = new Set(global.roles);
|
|
1260
|
+
const denials = [];
|
|
1261
|
+
for (const membership of subject.principal?.memberships ?? []) {
|
|
1262
|
+
if (!inTeam(membership, scopes, env.team)) continue;
|
|
1263
|
+
const expanded = expandRoleNames(membership.roles, declared, env.customRoles, tenantOf(membership, scopes));
|
|
1264
|
+
for (const name of expanded.roles) matching.add(name);
|
|
1265
|
+
for (const name of expanded.unknown) denials.push({
|
|
1266
|
+
role: name,
|
|
1267
|
+
reason: "unknown-role"
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
return {
|
|
1271
|
+
global,
|
|
1272
|
+
matching,
|
|
1273
|
+
denials
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
/** A support membership the policy says needs an actor, held without one. */
|
|
1277
|
+
function lacksActor(call, scopes) {
|
|
1278
|
+
const { subject, now, env } = call;
|
|
1279
|
+
const actorVias = actorRequiredVias(call.policy.index.supports);
|
|
1280
|
+
return actorVias.size > 0 && subject.actor === void 0 && (subject.principal?.memberships ?? []).some((membership) => isSupportMembership(membership, actorVias) && !isMembershipExpired(membership, now) && inTeam(membership, scopes, env.team));
|
|
1281
|
+
}
|
|
1282
|
+
function walkRoleFor(call) {
|
|
1283
|
+
const { env } = call;
|
|
1284
|
+
if (env.relations?.available !== true) return;
|
|
1285
|
+
return (membership, row) => {
|
|
1286
|
+
const on = membership.on;
|
|
1287
|
+
const condition = on === void 0 ? void 0 : resourceRoleCondition(call.resource, on.resource, [on.id], call.policy.resources);
|
|
1288
|
+
return condition === void 0 || env.relations === void 0 ? false : resolveRelated(condition, row, call.subject, call.now, env.relations);
|
|
1289
|
+
};
|
|
1290
|
+
}
|
|
1291
|
+
//#endregion
|
|
1292
|
+
//#region src/core/requires-conditions.ts
|
|
1293
|
+
const NEVER = freezeDeep({
|
|
1294
|
+
op: "or",
|
|
1295
|
+
conditions: []
|
|
1296
|
+
});
|
|
1297
|
+
function isUnconditionedAllow(grant, now) {
|
|
1298
|
+
return grant.where === void 0 && grant.check === void 0 && grant.closure === void 0 && grant.portable && grant.purpose === void 0 && grant.viaOnly === void 0 && grant.requires === void 0 && isActive(grant.validity, now);
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* The row condition a grant's `requires` adds: the row's instance of some
|
|
1302
|
+
* scope is one where the subject holds `key` through a role grant without a
|
|
1303
|
+
* row condition, minus the instances a deny of `key` reaches at that scope.
|
|
1304
|
+
* `undefined` when the subject holds `key` globally that way, so every row
|
|
1305
|
+
* passes. The in-process mirror of the SQL `permdock_has_permission` and
|
|
1306
|
+
* `permitted_<scope>_ids_by_permission`.
|
|
1307
|
+
*/
|
|
1308
|
+
function requirementCondition(policy, subject, keys, resource, now, customRoles, customGrants) {
|
|
1309
|
+
let where;
|
|
1310
|
+
for (const key of keys) where = combineWhere(where, keyRequirement(policy, subject, key, resource, now, customRoles, customGrants));
|
|
1311
|
+
return where;
|
|
1312
|
+
}
|
|
1313
|
+
function keyRequirement(policy, subject, key, resource, now, customRoles, customGrants) {
|
|
1314
|
+
const principal = subject.principal;
|
|
1315
|
+
if (principal === null) return NEVER;
|
|
1316
|
+
const scopes = scopeList(policy.scopes);
|
|
1317
|
+
const declared = declaredRoleNames(policy);
|
|
1318
|
+
const global = new Set(expandRoleNames(principal.roles ?? [], declared, customRoles, principal.tenant).roles);
|
|
1319
|
+
const held = (principal.memberships ?? []).filter((membership) => !isMembershipExpired(membership, now) && keepsPermission(membership, key)).map((membership) => ({
|
|
1320
|
+
membership,
|
|
1321
|
+
roles: new Set(expandRoleNames(membership.roles, declared, customRoles, tenantOf(membership, scopes)).roles)
|
|
1322
|
+
}));
|
|
1323
|
+
const allowed = /* @__PURE__ */ new Map();
|
|
1324
|
+
const denied = /* @__PURE__ */ new Map();
|
|
1325
|
+
let globalAllow = false;
|
|
1326
|
+
let globalDeny = false;
|
|
1327
|
+
const note = (effect, membership) => {
|
|
1328
|
+
if (membership === void 0) {
|
|
1329
|
+
if (effect === "allow") globalAllow = true;
|
|
1330
|
+
else globalDeny = true;
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
if (membership.scope === void 0 || membership.id === void 0) return;
|
|
1334
|
+
const target = effect === "allow" ? allowed : denied;
|
|
1335
|
+
const ids = target.get(membership.scope) ?? /* @__PURE__ */ new Set();
|
|
1336
|
+
ids.add(membership.id);
|
|
1337
|
+
target.set(membership.scope, ids);
|
|
1338
|
+
};
|
|
1339
|
+
const applies = (grant) => grant.effect === "allow" ? isUnconditionedAllow(grant, now) : isActive(grant.validity, now);
|
|
1340
|
+
for (const grant of policy.index.grantsByKey.get(key) ?? []) {
|
|
1341
|
+
const items = flattenGrantee(grant.to);
|
|
1342
|
+
if (grant.breakGlass !== void 0 || !applies(grant) || items.length === 0 || !items.every((item) => item.kind === "role")) continue;
|
|
1343
|
+
const roles = items.flatMap((item) => item.kind === "role" ? [item] : []);
|
|
1344
|
+
if (!roles.every((item) => item.scope !== "global" || global.has(item.role))) continue;
|
|
1345
|
+
const scoped = roles.filter((item) => item.scope !== "global");
|
|
1346
|
+
if (scoped.length === 0) {
|
|
1347
|
+
note(grant.effect);
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
for (const entry of held) if (scoped.every((item) => typeof item.scope === "string" && entry.membership.scope === item.scope && entry.roles.has(item.role))) note(grant.effect, entry.membership);
|
|
1351
|
+
}
|
|
1352
|
+
for (const { grant, role } of customGrants) {
|
|
1353
|
+
if (grant.permission.key !== key || !applies(grant)) continue;
|
|
1354
|
+
if (holdsGlobalCustomRole(principal.roles, role)) note(grant.effect);
|
|
1355
|
+
for (const entry of held) if (holdsCustomRole(entry.membership, role, scopes)) note(grant.effect, entry.membership);
|
|
1356
|
+
}
|
|
1357
|
+
if (globalAllow && !globalDeny) return;
|
|
1358
|
+
const parts = [];
|
|
1359
|
+
for (const name of resource === void 0 ? [] : partitionsOf(resource, scopes)) {
|
|
1360
|
+
const field = scopeField(resource, name, scopes);
|
|
1361
|
+
const deniedIds = denied.get(name);
|
|
1362
|
+
const ids = [...allowed.get(name) ?? []].filter((id) => deniedIds?.has(id) !== true).toSorted();
|
|
1363
|
+
if (field !== void 0 && ids.length > 0) parts.push({
|
|
1364
|
+
op: "in",
|
|
1365
|
+
field,
|
|
1366
|
+
value: ids
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
return sole(parts) ?? {
|
|
1370
|
+
op: "or",
|
|
1371
|
+
conditions: parts
|
|
1372
|
+
};
|
|
1373
|
+
}
|
|
1374
|
+
//#endregion
|
|
1375
|
+
//#region src/core/evaluate/candidates.ts
|
|
1376
|
+
/**
|
|
1377
|
+
* A collection write proposes `next`; an update pair may move `next` out of
|
|
1378
|
+
* the scope `current` is in. An allow needs both in scope; a deny applies
|
|
1379
|
+
* when either is, so a move into or out of its scope stays denied.
|
|
1380
|
+
*/
|
|
1381
|
+
function matchWriteScope(match, instance, current, next, deny) {
|
|
1382
|
+
const first = match(instance ? current : next);
|
|
1383
|
+
if (!instance || next === current) return first;
|
|
1384
|
+
if (deny) return first.ok ? first : match(next);
|
|
1385
|
+
if (!first.ok) return first;
|
|
1386
|
+
const moved = match(next);
|
|
1387
|
+
return moved.ok ? first : moved;
|
|
1388
|
+
}
|
|
1389
|
+
/** The code grants and custom-role grants for the permission, break-glass grants aside. */
|
|
1390
|
+
function candidatesFor(call) {
|
|
1391
|
+
const { permission } = call;
|
|
1392
|
+
const candidates = (call.policy.index.grantsByKey.get(permission.key) ?? []).filter((grant) => grant.breakGlass === void 0).map((grant) => ({ grant }));
|
|
1393
|
+
for (const item of call.env.customGrants) if (item.grant.permission.key === permission.key) candidates.push({
|
|
1394
|
+
grant: item.grant,
|
|
1395
|
+
custom: item.role
|
|
1396
|
+
});
|
|
1397
|
+
return candidates;
|
|
1398
|
+
}
|
|
1399
|
+
function breakGlassFor(call, scopes) {
|
|
1400
|
+
const { permission, subject, now, tracer } = call;
|
|
1401
|
+
const overrides = /* @__PURE__ */ new Set();
|
|
1402
|
+
let denial;
|
|
1403
|
+
for (const grant of call.policy.index.grantsByKey.get(permission.key) ?? []) {
|
|
1404
|
+
if (grant.breakGlass === void 0) continue;
|
|
1405
|
+
if (tracer !== void 0) tracer.evaluated += 1;
|
|
1406
|
+
if (!matchGrantee(grant.to, subject, now, call.resource, scopes).matched) {
|
|
1407
|
+
skip(tracer, grant, permission.key, "grantee");
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
const result = evaluateBreakGlass(grant.breakGlass, subject, now);
|
|
1411
|
+
if (result.kind === "inactive") {
|
|
1412
|
+
skip(tracer, grant, permission.key, "break-glass-inactive");
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
for (const name of grant.breakGlass.overrides) overrides.add(name);
|
|
1416
|
+
if (result.kind === "granted") return {
|
|
1417
|
+
grant,
|
|
1418
|
+
obligations: result.obligations,
|
|
1419
|
+
overrides,
|
|
1420
|
+
denial
|
|
1421
|
+
};
|
|
1422
|
+
denial ??= result.reason === "insufficient-user-authentication" ? {
|
|
1423
|
+
role: null,
|
|
1424
|
+
reason: result.reason,
|
|
1425
|
+
to: result.to
|
|
1426
|
+
} : {
|
|
1427
|
+
role: null,
|
|
1428
|
+
reason: result.reason
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
return {
|
|
1432
|
+
grant: void 0,
|
|
1433
|
+
obligations: [],
|
|
1434
|
+
overrides,
|
|
1435
|
+
denial
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
function holdsCustom(state, custom) {
|
|
1439
|
+
return isGlobalCustomRole(custom) ? holdsGlobalCustomRole(state.subject.principal?.roles ?? [], custom) : (state.subject.principal?.memberships ?? []).some((membership) => inTeam(membership, state.scopes, state.env.team) && holdsCustomRole(membership, custom, state.scopes));
|
|
1440
|
+
}
|
|
1441
|
+
function holdsRole(state, item, custom) {
|
|
1442
|
+
if (custom !== void 0) return holdsCustom(state, custom);
|
|
1443
|
+
return item.scope === "global" ? state.roles.global.roles.includes(item.role) : state.roles.matching.has(item.role);
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Whether the subject holds every role the grantee names, at a scope the row
|
|
1447
|
+
* (and a write's next row) is in. `held: false` when a denial or skip was recorded.
|
|
1448
|
+
*/
|
|
1449
|
+
function roleScope(state, grant, custom) {
|
|
1450
|
+
const { subject, permission, tracer } = state;
|
|
1451
|
+
const roleItems = flattenGrantee(grant.to).filter((item) => item.kind === "role");
|
|
1452
|
+
if (roleItems.length === 0) return { held: true };
|
|
1453
|
+
if (subject.principal === null) {
|
|
1454
|
+
state.denials.push({
|
|
1455
|
+
role: grant.role,
|
|
1456
|
+
reason: "anonymous"
|
|
1457
|
+
});
|
|
1458
|
+
return { held: false };
|
|
1459
|
+
}
|
|
1460
|
+
let membership;
|
|
1461
|
+
for (const roleItem of roleItems) {
|
|
1462
|
+
if (!holdsRole(state, roleItem, custom)) {
|
|
1463
|
+
skip(tracer, grant, permission.key, "role");
|
|
1464
|
+
return { held: false };
|
|
1465
|
+
}
|
|
1466
|
+
const matchRow = (row) => matchScopedMembership(subject, roleItem.scope, roleItem.role, row, state.scopes, state.resource, state.policy.resources, state.now, (held) => {
|
|
1467
|
+
if (custom === void 0) return expandRoleNames(held.roles, state.declared, state.env.customRoles, tenantOf(held, state.scopes)).roles;
|
|
1468
|
+
return holdsCustomRole(held, custom, state.scopes) ? [custom.name] : [];
|
|
1469
|
+
}, state.env.team, state.walkRole, permission.kind === "instance", state.options.scope);
|
|
1470
|
+
const scopeMatch = matchWriteScope(matchRow, permission.kind === "instance", state.current, state.next, grant.effect === "deny");
|
|
1471
|
+
if (!scopeMatch.ok) {
|
|
1472
|
+
state.denials.push({
|
|
1473
|
+
role: roleItem.role,
|
|
1474
|
+
reason: scopeMatch.reason
|
|
1475
|
+
});
|
|
1476
|
+
return { held: false };
|
|
1477
|
+
}
|
|
1478
|
+
membership = scopeMatch.membership ?? membership;
|
|
1479
|
+
}
|
|
1480
|
+
return membership === void 0 ? { held: true } : {
|
|
1481
|
+
held: true,
|
|
1482
|
+
membership
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
/** Whether the grantee matches; records the denial or skip when it does not. */
|
|
1486
|
+
function granteeOf(state, grant, custom) {
|
|
1487
|
+
const { subject, now, permission } = state;
|
|
1488
|
+
if (grant.viaOnly !== void 0 && !(subject.principal?.memberships ?? []).some((membership) => membership.via === grant.viaOnly && !isMembershipExpired(membership, now) && inTeam(membership, state.scopes, state.env.team))) {
|
|
1489
|
+
skip(state.tracer, grant, permission.key, "via-only");
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
const required = grant.purpose;
|
|
1493
|
+
if (required !== void 0 && !state.purposes.some((purpose) => required.includes(purpose))) {
|
|
1494
|
+
skip(state.tracer, grant, permission.key, "purpose");
|
|
1495
|
+
return;
|
|
1496
|
+
}
|
|
1497
|
+
const granteeMatch = matchGrantee(grant.to, subject, now, state.resource, state.scopes, state.policy.resources, grant.effect === "deny");
|
|
1498
|
+
if (granteeMatch.matched) return granteeMatch;
|
|
1499
|
+
const reason = granteeMatch.reason ?? "no-grant";
|
|
1500
|
+
if (reason === "not-entitled" && !flattenGrantee(grant.to).every((item) => item.kind !== "role" || holdsRole(state, item, custom))) {
|
|
1501
|
+
skip(state.tracer, grant, permission.key, "grantee");
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
state.denials.push((reason === "insufficient-user-authentication" || reason === "not-entitled") && grant.to !== void 0 ? {
|
|
1505
|
+
role: grant.role,
|
|
1506
|
+
reason,
|
|
1507
|
+
to: grant.to
|
|
1508
|
+
} : {
|
|
1509
|
+
role: grant.role,
|
|
1510
|
+
reason
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
/** One candidate grant: records an allow, a denial or a skip, or returns the decision a deny ends with. */
|
|
1514
|
+
function evaluateCandidate(state, { grant, custom }, breakGlass) {
|
|
1515
|
+
const { policy, subject, permission, now, env, tracer } = state;
|
|
1516
|
+
const displayRole = grant.role;
|
|
1517
|
+
if (tracer !== void 0) tracer.evaluated += 1;
|
|
1518
|
+
const granteeMatch = granteeOf(state, grant, custom);
|
|
1519
|
+
if (granteeMatch === void 0) return;
|
|
1520
|
+
const scoped = roleScope(state, grant, custom);
|
|
1521
|
+
if (!scoped.held) return;
|
|
1522
|
+
if (!isActive(grant.validity, now)) {
|
|
1523
|
+
if (grant.effect === "allow") state.denials.push({
|
|
1524
|
+
role: displayRole,
|
|
1525
|
+
reason: "inactive-grant",
|
|
1526
|
+
detail: wireValidity(grant.validity) ?? {}
|
|
1527
|
+
});
|
|
1528
|
+
else skip(tracer, grant, permission.key, "validity");
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
const requirement = grant.requires === void 0 || grant.effect !== "allow" ? void 0 : requirementCondition(policy, subject, grant.requires, state.resource, now, env.customRoles, env.customGrants);
|
|
1532
|
+
const merged = freezeDeep(compact({
|
|
1533
|
+
...grant,
|
|
1534
|
+
where: combineWhere(combineWhere(grant.where, granteeMatch.where), requirement)
|
|
1535
|
+
}));
|
|
1536
|
+
const condition = evaluateGrantCondition(merged, permission, state.current, state.next, subject, now, state.scopes, env.relations, (target, id, key) => inheritedVerdict(state.evaluate, policy, state.given, target, id, key, now, env));
|
|
1537
|
+
if (!condition.matched && condition.reason === "closure-error") emitSafe(env.listeners.error, condition.cause ?? /* @__PURE__ */ new Error("closure-error"), env.listeners);
|
|
1538
|
+
if (!condition.matched && grant.effect === "deny" && isUnevaluable(condition.reason) && grantCoversField(grant.fields, state.options.field, grant.effect)) {
|
|
1539
|
+
tracer?.denies.push(matchedOf(merged, permission.key));
|
|
1540
|
+
return deniedNow(state, {
|
|
1541
|
+
role: displayRole,
|
|
1542
|
+
reason: condition.reason ?? "relation-unavailable"
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
if (!condition.matched) {
|
|
1546
|
+
state.denials.push({
|
|
1547
|
+
role: displayRole,
|
|
1548
|
+
reason: condition.reason ?? "condition",
|
|
1549
|
+
detail: condition.cause
|
|
1550
|
+
});
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (!grantCoversField(grant.fields, state.options.field, grant.effect)) {
|
|
1554
|
+
skip(tracer, grant, permission.key, "field");
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
if (grant.effect === "deny") {
|
|
1558
|
+
tracer?.denies.push(matchedOf(merged, permission.key));
|
|
1559
|
+
if (grant.name !== void 0 && breakGlass.overrides.has(grant.name)) {
|
|
1560
|
+
if (breakGlass.grant !== void 0) return;
|
|
1561
|
+
if (breakGlass.denial !== void 0) return deniedWithAlternatives(state, [breakGlass.denial]);
|
|
1562
|
+
}
|
|
1563
|
+
return deniedWithAlternatives(state, [grant.name === void 0 ? {
|
|
1564
|
+
role: displayRole,
|
|
1565
|
+
reason: "deny"
|
|
1566
|
+
} : {
|
|
1567
|
+
role: displayRole,
|
|
1568
|
+
reason: "deny",
|
|
1569
|
+
detail: { name: grant.name }
|
|
1570
|
+
}]);
|
|
1571
|
+
}
|
|
1572
|
+
tracer?.allows.push(matchedOf(merged, permission.key));
|
|
1573
|
+
state.allows.push(scoped.membership === void 0 ? { grant: merged } : {
|
|
1574
|
+
grant: merged,
|
|
1575
|
+
membership: scoped.membership
|
|
1576
|
+
});
|
|
1577
|
+
}
|
|
1578
|
+
/** Adds the break-glass allow, or puts its denial first, once every candidate has run. */
|
|
1579
|
+
function mergeBreakGlass(state, breakGlass) {
|
|
1580
|
+
if (breakGlass.grant !== void 0) {
|
|
1581
|
+
state.tracer?.allows.push(matchedOf(breakGlass.grant, state.permission.key, true));
|
|
1582
|
+
state.allows.push({
|
|
1583
|
+
grant: breakGlass.grant,
|
|
1584
|
+
obligations: breakGlass.obligations,
|
|
1585
|
+
breakGlass: true
|
|
1586
|
+
});
|
|
1587
|
+
} else if (breakGlass.denial !== void 0) state.denials.unshift(breakGlass.denial);
|
|
1588
|
+
}
|
|
1589
|
+
//#endregion
|
|
1590
|
+
//#region src/core/evaluate/post-allow.ts
|
|
1591
|
+
function noAllowDecision(state) {
|
|
1592
|
+
const { denials, subject, roles } = state;
|
|
1593
|
+
const reason = denials[0]?.reason ?? (subject.principal === null ? "anonymous" : roles.global.unknown.length > 0 && roles.global.roles.length === 0 ? "unknown-role" : "no-grant");
|
|
1594
|
+
return deniedWithAlternatives(state, denials.length > 0 ? denials : [{
|
|
1595
|
+
role: null,
|
|
1596
|
+
reason
|
|
1597
|
+
}]);
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* The allows a delegation leaves usable. A policy delegation is a ceiling for
|
|
1601
|
+
* the actor: outside it, nothing is delegated; inside it, a token delegation
|
|
1602
|
+
* on the call must still cover.
|
|
1603
|
+
*/
|
|
1604
|
+
function delegatedAllows(state) {
|
|
1605
|
+
const { policy, subject, permission, current } = state;
|
|
1606
|
+
const ceiling = delegatedPermissions(policy.delegations, subject, state.roles.matching, state.now);
|
|
1607
|
+
if (ceiling !== void 0 && !ceiling.has(permission.key) || readOnlyActorRefuses(subject.actor, permission)) return deniedWithAlternatives(state, [{
|
|
1608
|
+
role: null,
|
|
1609
|
+
reason: "not-delegated"
|
|
1610
|
+
}]);
|
|
1611
|
+
const unscoped = subject.actor !== void 0 && ceiling === void 0;
|
|
1612
|
+
let delegationMiss = coveredByDelegation(permission, subject.delegation, resourceIdOf(current), unscoped);
|
|
1613
|
+
const requirementsCovered = state.allows.filter((candidate) => coversRequirements(policy.permissions, candidate.grant.requires, subject.delegation, resourceIdOf(current), unscoped));
|
|
1614
|
+
const usable = delegationMiss === void 0 ? requirementsCovered : requirementsCovered.filter((candidate) => requiresStandIn(candidate.grant));
|
|
1615
|
+
if (delegationMiss === "not-delegated" && usable.length > 0) delegationMiss = void 0;
|
|
1616
|
+
if (delegationMiss === void 0 && usable.length === 0) delegationMiss = "not-delegated";
|
|
1617
|
+
if (delegationMiss !== void 0) return deniedWithAlternatives(state, [{
|
|
1618
|
+
role: null,
|
|
1619
|
+
reason: delegationMiss
|
|
1620
|
+
}]);
|
|
1621
|
+
return usable;
|
|
1622
|
+
}
|
|
1623
|
+
//#endregion
|
|
1624
|
+
//#region src/core/evaluate/approval.ts
|
|
1625
|
+
/** The allow's approval, tightened by the loaded `ApprovalPolicySource` entries. */
|
|
1626
|
+
function approvalOf(state, candidate) {
|
|
1627
|
+
const approvalPolicies = state.env.approvalPolicies;
|
|
1628
|
+
return approvalPolicies === void 0 || approvalPolicies === "failed" ? candidate.grant.approval : approvalPolicies.tighten(candidate.grant.approval, {
|
|
1629
|
+
permission: state.permission.key,
|
|
1630
|
+
tenant: decisionTenant(state.subject, candidate.membership),
|
|
1631
|
+
subject: state.subject,
|
|
1632
|
+
current: state.current,
|
|
1633
|
+
next: state.next,
|
|
1634
|
+
now: state.now,
|
|
1635
|
+
scopes: state.scopes,
|
|
1636
|
+
operation: state.options.operation?.meta
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
//#endregion
|
|
1640
|
+
//#region src/core/evaluate/quota.ts
|
|
1641
|
+
/** The quota verdict for one allow: free without a `limit`, unavailable without the `limits()` feature. */
|
|
1642
|
+
function quotaOf(state, grant, consume) {
|
|
1643
|
+
const { env, subject } = state;
|
|
1644
|
+
if (grant.limit === void 0) return { ok: true };
|
|
1645
|
+
if (env.limits === void 0) return {
|
|
1646
|
+
ok: false,
|
|
1647
|
+
reason: "limit-unavailable"
|
|
1648
|
+
};
|
|
1649
|
+
return env.limits.quota({
|
|
1650
|
+
cache: env.limitCache,
|
|
1651
|
+
limit: grant.limit,
|
|
1652
|
+
permissionKey: state.permission.key,
|
|
1653
|
+
subjectId: subject.principal?.id ?? "",
|
|
1654
|
+
tenant: subject.principal?.tenant,
|
|
1655
|
+
now: state.now,
|
|
1656
|
+
consume
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
/** The first usable allow whose quota admits the call, or the quota denials. */
|
|
1660
|
+
function quotaAllow(state, usable) {
|
|
1661
|
+
const denials = [];
|
|
1662
|
+
for (const candidate of usable) {
|
|
1663
|
+
const approval = approvalOf(state, candidate);
|
|
1664
|
+
const quota = quotaOf(state, candidate.grant, !requiresApproval(approval) && shouldConsumeQuota(state.options.call, state.env.simulated));
|
|
1665
|
+
if (quota.ok) return {
|
|
1666
|
+
allow: candidate,
|
|
1667
|
+
approval,
|
|
1668
|
+
quota: compact({
|
|
1669
|
+
quota: quota.quota,
|
|
1670
|
+
obligations: quota.obligations
|
|
1671
|
+
})
|
|
1672
|
+
};
|
|
1673
|
+
denials.push(quota.reason === "limit" ? {
|
|
1674
|
+
role: candidate.grant.role,
|
|
1675
|
+
reason: "limit",
|
|
1676
|
+
detail: quota.detail
|
|
1677
|
+
} : {
|
|
1678
|
+
role: candidate.grant.role,
|
|
1679
|
+
reason: quota.reason
|
|
1680
|
+
});
|
|
1681
|
+
}
|
|
1682
|
+
return { denials };
|
|
1683
|
+
}
|
|
1684
|
+
//#endregion
|
|
1685
|
+
//#region src/core/evaluate/index.ts
|
|
1686
|
+
/**
|
|
1687
|
+
* One decision, in stages: the call (clock, rows, delegated permission), the
|
|
1688
|
+
* subject (held roles, actor), the candidate grants (a matching deny ends
|
|
1689
|
+
* here), break-glass, freshness, delegation and contact, the approval and
|
|
1690
|
+
* quota of each usable allow, and the decision. Every exit goes through
|
|
1691
|
+
* `complete`, which freezes and emits.
|
|
1692
|
+
*/
|
|
1693
|
+
function evaluate(policy, given, permission, data, options, env) {
|
|
1694
|
+
const call = {
|
|
1695
|
+
policy,
|
|
1696
|
+
given,
|
|
1697
|
+
subject: subjectForPermission(given, permission.key),
|
|
1698
|
+
permission,
|
|
1699
|
+
options,
|
|
1700
|
+
env,
|
|
1701
|
+
now: epochSeconds(options.now),
|
|
1702
|
+
resource: getResource(policy.permissions, permission.resource),
|
|
1703
|
+
tracer: options.explain === true ? {
|
|
1704
|
+
evaluated: 0,
|
|
1705
|
+
allows: [],
|
|
1706
|
+
denies: [],
|
|
1707
|
+
skipped: []
|
|
1708
|
+
} : void 0,
|
|
1709
|
+
evaluate
|
|
1710
|
+
};
|
|
1711
|
+
if (!Number.isFinite(call.now)) return deniedNow({
|
|
1712
|
+
...call,
|
|
1713
|
+
now: epochSeconds(),
|
|
1714
|
+
current: data
|
|
1715
|
+
}, {
|
|
1716
|
+
role: null,
|
|
1717
|
+
reason: "validation",
|
|
1718
|
+
detail: INVALID_NOW
|
|
1719
|
+
});
|
|
1720
|
+
const rows = validatedRows(call, data, rowValues(permission, data));
|
|
1721
|
+
if (rows.invalid !== void 0) return deniedNow({
|
|
1722
|
+
...call,
|
|
1723
|
+
current: rows.current
|
|
1724
|
+
}, {
|
|
1725
|
+
role: null,
|
|
1726
|
+
reason: "validation",
|
|
1727
|
+
detail: rows.invalid
|
|
1728
|
+
});
|
|
1729
|
+
const settled = {
|
|
1730
|
+
...call,
|
|
1731
|
+
current: rows.current
|
|
1732
|
+
};
|
|
1733
|
+
if (isDelegatedPermission(policy, permission)) return deniedNow(settled, {
|
|
1734
|
+
role: null,
|
|
1735
|
+
reason: "pdp-unavailable",
|
|
1736
|
+
detail: "use permdock/pdp getPermDock"
|
|
1737
|
+
});
|
|
1738
|
+
const scopes = scopeList(policy.scopes);
|
|
1739
|
+
const declared = declaredRoleNames(policy);
|
|
1740
|
+
const roles = heldRoles(call, scopes, declared);
|
|
1741
|
+
if (lacksActor(call, scopes)) return deniedNow(settled, {
|
|
1742
|
+
role: null,
|
|
1743
|
+
reason: "actor-required"
|
|
1744
|
+
});
|
|
1745
|
+
const state = {
|
|
1746
|
+
...call,
|
|
1747
|
+
current: rows.current,
|
|
1748
|
+
next: rows.next,
|
|
1749
|
+
scopes,
|
|
1750
|
+
declared,
|
|
1751
|
+
roles,
|
|
1752
|
+
purposes: purposesOf(call.subject),
|
|
1753
|
+
walkRole: walkRoleFor(call),
|
|
1754
|
+
denials: roles.denials,
|
|
1755
|
+
allows: []
|
|
1756
|
+
};
|
|
1757
|
+
const breakGlass = breakGlassFor(call, scopes);
|
|
1758
|
+
for (const candidate of candidatesFor(call)) {
|
|
1759
|
+
const decision = evaluateCandidate(state, candidate, breakGlass);
|
|
1760
|
+
if (decision !== void 0) return decision;
|
|
1761
|
+
}
|
|
1762
|
+
mergeBreakGlass(state, breakGlass);
|
|
1763
|
+
if (state.allows.length === 0) return noAllowDecision(state);
|
|
1764
|
+
if (state.subject.stale === true && (policy.fresh ?? []).includes(permission.key)) return deniedNow(state, {
|
|
1765
|
+
role: null,
|
|
1766
|
+
reason: "stale-credentials"
|
|
1767
|
+
});
|
|
1768
|
+
const delegated = delegatedAllows(state);
|
|
1769
|
+
if (!Array.isArray(delegated)) return delegated;
|
|
1770
|
+
const usable = contactAllows(policy, state.subject, permission, delegated);
|
|
1771
|
+
if (usable.length === 0) return deniedWithAlternatives(state, [{
|
|
1772
|
+
role: null,
|
|
1773
|
+
reason: "not-delegated"
|
|
1774
|
+
}]);
|
|
1775
|
+
if (env.approvalPolicies === "failed") return deniedNow(state, {
|
|
1776
|
+
role: null,
|
|
1777
|
+
reason: "approval",
|
|
1778
|
+
detail: APPROVAL_POLICY_UNAVAILABLE
|
|
1779
|
+
});
|
|
1780
|
+
const chosen = quotaAllow(state, usable);
|
|
1781
|
+
if ("denials" in chosen) return deniedWithAlternatives(state, chosen.denials.length > 0 ? chosen.denials : state.denials);
|
|
1782
|
+
return allowedDecision(state, chosen.allow, chosen.approval, chosen.quota);
|
|
1783
|
+
}
|
|
1784
|
+
//#endregion
|
|
1785
|
+
export { contactMayUse as A, validateCustomRole as C, activate as D, finish as E, isContact as M, APPROVAL_POLICY_UNAVAILABLE as N, breakGlass as O, tenantCustomRoles as S, emptyListeners as T, isSharedCustomRole as _, expandRoleNames as a, resolveCustomRole as b, andWhere as c, customRoleClaim as d, customRoleId as f, holdsGlobalCustomRole as g, holdsCustomRole as h, customRolesFor as i, contactMemberships as j, supportAccess as k, ceilingGrants as l, customRoleScope as m, requirementCondition as n, roleSourceFor as o, customRoleOf as p, assignableNamesFor as r, sourceName as s, evaluate as t, customGrantsFor as u, leveled as v, emitSafe as w, roleAllowKeys as x, membershipsClaim as y };
|