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
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { r as isRecord } from "./compact-CAf557ZB.js";
|
|
2
|
-
import { a as hasConditionOp } from "./refs-
|
|
3
|
-
import { c as rootScope, d as scopeList, l as scopeChain, o as resolveScope } from "./scopes-
|
|
4
|
-
import {
|
|
2
|
+
import { a as hasConditionOp } from "./refs-K6eWUJ0H.js";
|
|
3
|
+
import { c as rootScope, d as scopeList, l as scopeChain, o as resolveScope } from "./scopes-CUPSjo9q.js";
|
|
4
|
+
import { t as APPROVAL_REQUEST_SCHEMA } from "./request-schema-CMreVuFM.js";
|
|
5
|
+
import { a as describeError, i as configError } from "./errors-CWTY6uih.js";
|
|
5
6
|
import { i as readJsonFile, o as sqlFiles, s as writeFileEnsuringDir } from "./files-D_JIzUrP.js";
|
|
6
|
-
import {
|
|
7
|
-
import { c as requirePolicy } from "./load-
|
|
8
|
-
import { r as scopeColumn } from "./compile-
|
|
9
|
-
import { t as authorizeSql } from "./supabase-
|
|
7
|
+
import { a as SUPABASE_MANIFEST_SCHEMA } from "./version-CEbQ5UeM.js";
|
|
8
|
+
import { c as requirePolicy } from "./load-DFS_ry0W.js";
|
|
9
|
+
import { r as scopeColumn } from "./compile-CMqMx_IN.js";
|
|
10
|
+
import { t as authorizeSql } from "./supabase-x5JtR2tc.js";
|
|
10
11
|
import "./budget-NbEtbJC_.js";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
12
|
+
import { i as qualifySqlTable } from "./sql-B5T1IL83.js";
|
|
13
|
+
import { a as hookApiNames, d as keptKeys, t as AUTHZ_VERSION_TABLE, u as roleManifest } from "./sources-vs5dGOsS.js";
|
|
14
|
+
import { o as readMemberships } from "./postgrest-DAjUkF9w.js";
|
|
15
|
+
import { t as beforeFirst } from "./text-BMFRfBqc.js";
|
|
16
|
+
import { n as GRANTS_MARKER, r as HOOK_MARKER, s as hookMarkerFields } from "./markers-C6joDI3o.js";
|
|
17
|
+
import { n as formatManifestJson, t as MANIFEST_FILE } from "./manifest-json-CAGzls-C.js";
|
|
15
18
|
import { n as MIGRATION_DIRS } from "./constants-DcJABbLW.js";
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
19
|
+
import { f as writeOutputFiles, l as partPath, r as driftOf, u as pgDeltaPath } from "./output-files-DhRBBE4n.js";
|
|
20
|
+
import { E as scopeTypeOf, F as quoteQualified, I as quoteTable, M as qualified, N as quoteIdent, O as tenantTypeOf, P as quoteLiteral, a as customRolesOn, l as USER_ID_HELPER, r as HELPERS, y as subjectIdSql } from "./shared-BAK8t4Uo.js";
|
|
18
21
|
import { a as splitUndiffed, r as tableKey, t as decidingColumns } from "./deciding-columns-Sq12mEA6.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
22
|
+
import { a as readOnlyActorKinds, f as pgConnector, t as ACTOR_MAY_WRITE } from "./policies-DP2nqRoF.js";
|
|
23
|
+
import { B as hasPermittedUsers, E as apiKeyFields, G as memberIdsHelper, H as memberForHelper, J as permittedIdsHelper, K as memberRoleOf, Q as activeRowSql, U as memberForSources, W as memberForTable, Z as scopeTable, a as guardedTables, c as PERMISSION_HELPERS, d as permissionKeysForHelper, et as checkSuspension, f as permittedByPermissionHelper, j as roleNames, l as checkName, o as ASSIGNMENTS, q as permittedForHelper, r as holderSources, s as OWNERSHIP, t as ownershipRules, v as globalRoleRows, w as API_KEY_ALLOWS, y as globalRoleSource, z as hasMemberFor } from "./ownership-DqaJNJPn.js";
|
|
21
24
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
22
25
|
import { dirname, join, posix, resolve } from "node:path";
|
|
23
26
|
import { parse } from "smol-toml";
|
|
@@ -49,16 +52,24 @@ function readSupabaseConfig(cwd) {
|
|
|
49
52
|
const apiSchemas = strings(table(root["api"])?.["schemas"]);
|
|
50
53
|
const pgDelta = table(table(root["experimental"])?.["pgdelta"]);
|
|
51
54
|
const schemaPath = pgDelta?.["declarative_schema_path"];
|
|
55
|
+
const enabled = pgDelta?.["enabled"];
|
|
52
56
|
return {
|
|
53
57
|
ok: true,
|
|
54
58
|
config: {
|
|
55
59
|
...typeof expiry === "number" ? { jwtExpiry: expiry } : {},
|
|
56
60
|
...schemaPaths === void 0 ? {} : { schemaPaths },
|
|
57
61
|
...apiSchemas === void 0 ? {} : { apiSchemas },
|
|
58
|
-
...pgDelta
|
|
62
|
+
...pgDelta === void 0 ? {} : { pgDelta: {
|
|
63
|
+
...typeof enabled === "boolean" ? { enabled } : {},
|
|
64
|
+
schemaDir: declarativeSchemaDir(schemaPath)
|
|
65
|
+
} }
|
|
59
66
|
}
|
|
60
67
|
};
|
|
61
68
|
}
|
|
69
|
+
/** `supabase/` joined with `declarative_schema_path`, which defaults to `schemas`. */
|
|
70
|
+
function declarativeSchemaDir(schemaPath) {
|
|
71
|
+
return posix.join("supabase", typeof schemaPath === "string" ? schemaPath : "schemas").replace(/\/$/u, "");
|
|
72
|
+
}
|
|
62
73
|
/** The parsed config, or `{}` when it is missing or does not parse (PD045 reports that). */
|
|
63
74
|
function supabaseConfig(cwd) {
|
|
64
75
|
const read = readSupabaseConfig(cwd);
|
|
@@ -72,7 +83,7 @@ function pd045(cwd) {
|
|
|
72
83
|
code: "PD045",
|
|
73
84
|
severity: "warning",
|
|
74
85
|
message: `${SUPABASE_CONFIG} does not parse: ${read.error.replace(/\n[\s\S]*/u, "")}`,
|
|
75
|
-
fix: `fix the TOML syntax in ${SUPABASE_CONFIG}; until then
|
|
86
|
+
fix: `fix the TOML syntax in ${SUPABASE_CONFIG}; until then PermDock assumes Supabase's defaults for jwt_expiry, schema_paths and [experimental.pgdelta], and generates for pg-delta`
|
|
76
87
|
}];
|
|
77
88
|
}
|
|
78
89
|
//#endregion
|
|
@@ -156,11 +167,11 @@ const PERMDOCK_CLAIMS_SCHEMA = {
|
|
|
156
167
|
items: { type: "string" }
|
|
157
168
|
},
|
|
158
169
|
via: { type: "string" },
|
|
159
|
-
|
|
160
|
-
description: "
|
|
170
|
+
expires_at: {
|
|
171
|
+
description: "NumericDate (RFC 7519): seconds since the epoch.",
|
|
161
172
|
type: "number"
|
|
162
173
|
},
|
|
163
|
-
|
|
174
|
+
granted_by: { type: "string" },
|
|
164
175
|
reason: { type: "string" },
|
|
165
176
|
member: {
|
|
166
177
|
description: "The subgroup a membership source's group fills.",
|
|
@@ -171,7 +182,7 @@ const PERMDOCK_CLAIMS_SCHEMA = {
|
|
|
171
182
|
minLength: 1
|
|
172
183
|
} }
|
|
173
184
|
},
|
|
174
|
-
|
|
185
|
+
managed_by: { const: "idp" },
|
|
175
186
|
entitlements: {
|
|
176
187
|
type: "array",
|
|
177
188
|
items: { type: "string" }
|
|
@@ -309,7 +320,7 @@ function extraClaimsPlan(claims, tenantClaim) {
|
|
|
309
320
|
function beforePlan(before) {
|
|
310
321
|
const list = before === void 0 ? [] : typeof before === "string" ? [before] : before;
|
|
311
322
|
const bad = list.filter((fn) => typeof fn !== "string" || !CLAIM_FUNCTION.test(fn));
|
|
312
|
-
if (bad.length > 0) throw
|
|
323
|
+
if (bad.length > 0) throw configError(`PermDock CLI: supabase.hook.before must name schema-qualified functions such as auth_checks.before_token (got ${bad.map(String).join(", ")})`);
|
|
313
324
|
return [...new Set(list)];
|
|
314
325
|
}
|
|
315
326
|
function beforeSql(before) {
|
|
@@ -397,6 +408,38 @@ function hookClaims(parts) {
|
|
|
397
408
|
];
|
|
398
409
|
}
|
|
399
410
|
//#endregion
|
|
411
|
+
//#region src/cli/supabase-schemas.ts
|
|
412
|
+
const MODES = [
|
|
413
|
+
"pg-delta",
|
|
414
|
+
"migra",
|
|
415
|
+
"migrations"
|
|
416
|
+
];
|
|
417
|
+
function isSupabaseSchemas(value) {
|
|
418
|
+
return MODES.some((mode) => mode === value);
|
|
419
|
+
}
|
|
420
|
+
/** The `--schemas` refusal, or `undefined` when the value is one of the modes. */
|
|
421
|
+
function schemasRefusal(command, value) {
|
|
422
|
+
return value === void 0 || isSupabaseSchemas(value) ? void 0 : `${command} --schemas must be ${MODES.join(", ")} (got ${value})`;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* `--schemas`, else `supabase.schemas`, else `migra` when `config.toml`
|
|
426
|
+
* sets `[experimental.pgdelta] enabled = false`, else `pg-delta`: the default
|
|
427
|
+
* of new Supabase projects and of the next Supabase CLI major.
|
|
428
|
+
*/
|
|
429
|
+
function resolveSchemas(cwd, config, flag) {
|
|
430
|
+
const toml = supabaseConfig(cwd).pgDelta;
|
|
431
|
+
const mode = flag ?? config.supabase?.schemas ?? (toml?.enabled === false ? "migra" : "pg-delta");
|
|
432
|
+
switch (mode) {
|
|
433
|
+
case "pg-delta": return {
|
|
434
|
+
mode,
|
|
435
|
+
schemaDir: toml?.schemaDir ?? declarativeSchemaDir()
|
|
436
|
+
};
|
|
437
|
+
case "migra":
|
|
438
|
+
case "migrations": return { mode };
|
|
439
|
+
default: return mode;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
//#endregion
|
|
400
443
|
//#region src/cli/supabase-setup.ts
|
|
401
444
|
const HELPER_NAME = /^(?:permdock_has|permitted_[a-z][a-z0-9_]*_ids|member_[a-z][a-z0-9_]*_ids(?:_for)?)$/u;
|
|
402
445
|
function escape(text) {
|
|
@@ -435,7 +478,7 @@ function missingHelpersInFiles(cwd, config, manifest) {
|
|
|
435
478
|
for (const file of helperSources(cwd, config, manifest.hook.out)) for (const name of helpersDefined(readFileSync(file, "utf8"), manifest.helpers.schema)) found.add(name);
|
|
436
479
|
return manifest.helpers.functions.filter((name) => !found.has(name));
|
|
437
480
|
}
|
|
438
|
-
async function missingHelpersInDb(db, manifest, connect = pgConnector("permdock supabase hook generate --db")) {
|
|
481
|
+
async function missingHelpersInDb(db, manifest, connect = pgConnector("permdock supabase hook generate --db-url-stdin")) {
|
|
439
482
|
const client = await connect(db);
|
|
440
483
|
try {
|
|
441
484
|
const result = await client.query(`select p.proname from pg_proc p join pg_namespace n on n.oid = p.pronamespace
|
|
@@ -493,8 +536,17 @@ function pd039(input) {
|
|
|
493
536
|
});
|
|
494
537
|
const fixture = input.config.doctor?.claims;
|
|
495
538
|
if (fixture !== void 0) {
|
|
496
|
-
const parsed =
|
|
497
|
-
|
|
539
|
+
const parsed = readClaimsFixture(resolve(input.cwd, fixture));
|
|
540
|
+
if (!parsed.ok) {
|
|
541
|
+
findings.push({
|
|
542
|
+
code: "PD039",
|
|
543
|
+
severity: "warning",
|
|
544
|
+
message: `doctor.claims ${fixture} is not a JSON file doctor can read (${parsed.message}), so the claim budget went unchecked`,
|
|
545
|
+
fix: "point doctor.claims at a JSON array of sample claims"
|
|
546
|
+
});
|
|
547
|
+
return findings;
|
|
548
|
+
}
|
|
549
|
+
const samples = Array.isArray(parsed.value) ? parsed.value : [];
|
|
498
550
|
for (const claim of oversizedClaims(samples, input.manifest)) findings.push({
|
|
499
551
|
code: "PD039",
|
|
500
552
|
severity: "warning",
|
|
@@ -510,6 +562,19 @@ function pd039(input) {
|
|
|
510
562
|
}
|
|
511
563
|
return findings;
|
|
512
564
|
}
|
|
565
|
+
function readClaimsFixture(path) {
|
|
566
|
+
try {
|
|
567
|
+
return {
|
|
568
|
+
ok: true,
|
|
569
|
+
value: JSON.parse(readFileSync(path, "utf8"))
|
|
570
|
+
};
|
|
571
|
+
} catch (error) {
|
|
572
|
+
return {
|
|
573
|
+
ok: false,
|
|
574
|
+
message: error instanceof Error ? error.message : String(error)
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
}
|
|
513
578
|
//#endregion
|
|
514
579
|
//#region src/cli/rls/rbac.ts
|
|
515
580
|
/**
|
|
@@ -528,7 +593,7 @@ function resolveAuthorize(config, flags = {}) {
|
|
|
528
593
|
function parseRbacAuthorize(raw) {
|
|
529
594
|
if (raw === void 0) return;
|
|
530
595
|
if (raw === "database" || raw === "jwt") return raw;
|
|
531
|
-
throw
|
|
596
|
+
throw configError(`PermDock CLI: --authorize must be database or jwt, got '${raw}'`);
|
|
532
597
|
}
|
|
533
598
|
function hookUri(schema) {
|
|
534
599
|
return `pg-functions://postgres/${schema}/custom_access_token_hook`;
|
|
@@ -598,6 +663,7 @@ function memberForPlan(input) {
|
|
|
598
663
|
dialect: "supabase",
|
|
599
664
|
...input
|
|
600
665
|
}, name));
|
|
666
|
+
const holders = memberFor.filter((name) => hasPermittedUsers(input, name));
|
|
601
667
|
const entries = /* @__PURE__ */ new Map();
|
|
602
668
|
for (const name of memberFor) {
|
|
603
669
|
const mapped = memberForTable(input, name);
|
|
@@ -606,10 +672,11 @@ function memberForPlan(input) {
|
|
|
606
672
|
}
|
|
607
673
|
return {
|
|
608
674
|
memberFor,
|
|
675
|
+
holders,
|
|
609
676
|
memberships: [...entries.values()]
|
|
610
677
|
};
|
|
611
678
|
}
|
|
612
|
-
/** An `rls.memberships` table in the manifest's `
|
|
679
|
+
/** An `rls.memberships` table in the manifest's `tableMembershipSource` / `junctionMembershipSource` shape, for scope `name`. */
|
|
613
680
|
function mappedMembership(mapped, scopes, name) {
|
|
614
681
|
const id = scopeColumn(mapped, scopes, name) ?? "";
|
|
615
682
|
const within = {};
|
|
@@ -642,7 +709,7 @@ function mappedMembership(mapped, scopes, name) {
|
|
|
642
709
|
}
|
|
643
710
|
function positiveInteger(value, label) {
|
|
644
711
|
const number = typeof value === "string" ? Number(value) : value;
|
|
645
|
-
if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) throw
|
|
712
|
+
if (typeof number !== "number" || !Number.isInteger(number) || number <= 0) throw configError(`PermDock CLI: ${label} must be a positive integer`);
|
|
646
713
|
return number;
|
|
647
714
|
}
|
|
648
715
|
/**
|
|
@@ -653,12 +720,12 @@ function activeFromSql(input, root) {
|
|
|
653
720
|
const spec = input ?? `app_metadata.active_${root}`;
|
|
654
721
|
if (typeof spec === "string" && spec.startsWith("app_metadata.")) {
|
|
655
722
|
const key = spec.slice(13);
|
|
656
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw
|
|
723
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/u.test(key)) throw configError(`PermDock CLI: unsafe app_metadata key '${key}'`);
|
|
657
724
|
return { sql: `claims -> 'app_metadata' ->> ${quoteLiteral(key)}` };
|
|
658
725
|
}
|
|
659
726
|
const parsed = typeof spec === "string" ? (() => {
|
|
660
727
|
const dot = spec.lastIndexOf(".");
|
|
661
|
-
if (dot <= 0) throw
|
|
728
|
+
if (dot <= 0) throw configError(`PermDock CLI: --active-from must be app_metadata.<key> or <table>.<column>, got '${spec}'`);
|
|
662
729
|
return {
|
|
663
730
|
table: spec.slice(0, dot),
|
|
664
731
|
column: spec.slice(dot + 1)
|
|
@@ -684,16 +751,16 @@ function typedUsers(parts) {
|
|
|
684
751
|
].map((line) => `\n ${line}`).join("");
|
|
685
752
|
}
|
|
686
753
|
function checkSources(parts) {
|
|
687
|
-
if (parts.sources.length === 0) throw
|
|
754
|
+
if (parts.sources.length === 0) throw configError("PermDock CLI: supabase.hook.memberships needs at least one tableMembershipSource or junctionMembershipSource source");
|
|
688
755
|
for (const source of parts.sources) {
|
|
689
|
-
if (source.sql === void 0 || typeof source.sql.select !== "function") throw
|
|
756
|
+
if (source.sql === void 0 || typeof source.sql.select !== "function") throw configError("PermDock CLI: supabase.hook.memberships takes tableMembershipSource / junctionMembershipSource sources from permdock/supabase");
|
|
690
757
|
const scope = source.sql.scope;
|
|
691
758
|
if (scope === void 0) continue;
|
|
692
|
-
if (resolveScope(parts.scopes, scope) !== scope) throw
|
|
759
|
+
if (resolveScope(parts.scopes, scope) !== scope) throw configError(`PermDock CLI: the ${source.sql.table} source names scope '${scope}', which the policy does not declare by that name`);
|
|
693
760
|
const chain = scopeChain(parts.scopes, scope);
|
|
694
761
|
const holds = new Set(source.sql.holds ?? [scope]);
|
|
695
762
|
const missing = chain.filter((ancestor) => !holds.has(ancestor));
|
|
696
|
-
if (missing.length > 0) throw
|
|
763
|
+
if (missing.length > 0) throw configError(`PermDock CLI: the ${source.sql.table} source needs within columns for ${missing.join(", ")}: a ${scope} membership without every ancestor grants nothing`);
|
|
697
764
|
}
|
|
698
765
|
}
|
|
699
766
|
function entriesSql(parts) {
|
|
@@ -702,9 +769,9 @@ function entriesSql(parts) {
|
|
|
702
769
|
(${tenant}) as tenant,
|
|
703
770
|
jsonb_strip_nulls(jsonb_build_object(
|
|
704
771
|
'scope', s.scope, 'id', s.id, 'within', s.within, 'roles', s.roles, 'via', s.via,
|
|
705
|
-
'
|
|
772
|
+
'expires_at', s.expires_at, 'granted_by', s.granted_by, 'reason', s.reason,
|
|
706
773
|
'member', case when s.member_group is not null then jsonb_build_object('group', s.member_group) end,
|
|
707
|
-
'
|
|
774
|
+
'managed_by', s.managed_by, 'entitlements', s.seats${source.sql.keeps ? ", 'keep', s.keep" : ""}
|
|
708
775
|
)) as entry
|
|
709
776
|
from (
|
|
710
777
|
${source.sql.select(`v_user_${String(index)}`).replaceAll(/^/gmu, " ")}
|
|
@@ -785,7 +852,7 @@ function apiWrappersSql(parts) {
|
|
|
785
852
|
const source = quoteIdent(parts.schema);
|
|
786
853
|
const wrapper = (name, inner, params, args, types, returns) => {
|
|
787
854
|
const fn = `${quoteIdent(target)}.${quoteIdent(name)}`;
|
|
788
|
-
if (target === parts.schema && name === inner) throw
|
|
855
|
+
if (target === parts.schema && name === inner) throw configError(`PermDock CLI: supabase.hook.api would replace ${source}.${inner}: set api.schema or api.prefix`);
|
|
789
856
|
return `create or replace function ${fn}(${params})
|
|
790
857
|
returns ${returns}
|
|
791
858
|
language sql
|
|
@@ -856,9 +923,9 @@ as $$
|
|
|
856
923
|
'principal', jsonb_build_object('id', s.user_id),
|
|
857
924
|
'membership', jsonb_strip_nulls(jsonb_build_object(
|
|
858
925
|
'scope', s.scope, 'id', s.id, 'within', s.within, 'roles', s.roles, 'via', s.via,
|
|
859
|
-
'
|
|
926
|
+
'expires_at', s.expires_at, 'granted_by', s.granted_by, 'reason', s.reason,
|
|
860
927
|
'member', case when s.member_group is not null then jsonb_build_object('group', s.member_group) end,
|
|
861
|
-
'
|
|
928
|
+
'managed_by', s.managed_by, 'entitlements', s.seats${keep ? ", 'keep', s.keep" : ""}
|
|
862
929
|
))
|
|
863
930
|
) order by s.user_id, s.roles::text), '[]'::jsonb)
|
|
864
931
|
from (
|
|
@@ -868,9 +935,10 @@ $$;
|
|
|
868
935
|
revoke execute on function ${fn}(text, text) from public, anon, authenticated;`;
|
|
869
936
|
}
|
|
870
937
|
/**
|
|
871
|
-
* The custom roles `p_user` holds: tenant roles named on a
|
|
872
|
-
* scope (and instance) they live at, and platform roles
|
|
873
|
-
* roles, each with its grants and includes in `CustomRole`
|
|
938
|
+
* The custom roles `p_user` holds: tenant and shared roles named on a
|
|
939
|
+
* membership of the scope (and instance) they live at, and platform roles
|
|
940
|
+
* among the global roles, each with its grants and includes in `CustomRole`
|
|
941
|
+
* form. A shadowed tenant role comes along; `resolveCustomRole`'s callers drop it.
|
|
874
942
|
*/
|
|
875
943
|
function customRolesOfSql(parts) {
|
|
876
944
|
const helpers = quoteIdent(parts.helpers.schema);
|
|
@@ -881,6 +949,7 @@ function customRolesOfSql(parts) {
|
|
|
881
949
|
return `
|
|
882
950
|
select coalesce(jsonb_agg(jsonb_strip_nulls(jsonb_build_object(
|
|
883
951
|
'name', c.role,
|
|
952
|
+
'shared', case when c.tenant_id is null and c.scope <> 'global' then true end,
|
|
884
953
|
'tenant', c.tenant_id::text,
|
|
885
954
|
'scope', c.scope,
|
|
886
955
|
'id', c.scope_id,
|
|
@@ -911,11 +980,816 @@ function customRolesOfSql(parts) {
|
|
|
911
980
|
from jsonb_array_elements(memberships) m
|
|
912
981
|
where m -> 'roles' ? c.role
|
|
913
982
|
and m ->> 'scope' = c.scope
|
|
914
|
-
and
|
|
983
|
+
and (
|
|
984
|
+
(c.tenant_id is null and c.scope_id is null)
|
|
985
|
+
or c.tenant_id::text = case when m ->> 'scope' = ${root} then m ->> 'id' else m -> 'within' ->> ${root} end
|
|
986
|
+
)
|
|
915
987
|
and (c.scope_id is null or c.scope_id = m ->> 'id')
|
|
916
988
|
);`;
|
|
917
989
|
}
|
|
918
990
|
//#endregion
|
|
991
|
+
//#region src/cli/rls/approvals.ts
|
|
992
|
+
/** `rls.approvals`' approver check; better-supabase's `isApprover` template calls it. */
|
|
993
|
+
const IS_APPROVER = "permdock_is_approver";
|
|
994
|
+
/** The objects `rls.approvals` adds. Names are part of the SQL contract `supabaseApprovalStore` calls. */
|
|
995
|
+
const APPROVAL_STORE = {
|
|
996
|
+
table: "approval_requests",
|
|
997
|
+
open: "permdock_approval_open",
|
|
998
|
+
get: "permdock_approval_get",
|
|
999
|
+
resolve: "permdock_approval_resolve",
|
|
1000
|
+
consume: "permdock_approval_consume",
|
|
1001
|
+
list: "permdock_approval_list",
|
|
1002
|
+
expire: "permdock_approval_expire",
|
|
1003
|
+
cancel: "permdock_approval_cancel",
|
|
1004
|
+
isApprover: IS_APPROVER
|
|
1005
|
+
};
|
|
1006
|
+
/** `body` as the row's filter columns hold it. */
|
|
1007
|
+
const COLUMNS = `p_request ->> 'token',
|
|
1008
|
+
p_request,
|
|
1009
|
+
p_request ->> 'status',
|
|
1010
|
+
p_request #>> '{subject,principal,tenant}',
|
|
1011
|
+
p_request #>> '{subject,principal,id}',
|
|
1012
|
+
p_request #>> '{subject,actor,id}',
|
|
1013
|
+
p_request #>> '{subject,session}',
|
|
1014
|
+
coalesce(jsonb_array_length(p_request -> 'approvals'), 0),
|
|
1015
|
+
p_request ->> 'createdAt',
|
|
1016
|
+
(p_request ->> 'expiresAt')::timestamptz,
|
|
1017
|
+
(p_request ->> 'consumedAt')::timestamptz`;
|
|
1018
|
+
/** The `ApprovalListFilter` in `p_filter`, over the row aliased `a`. */
|
|
1019
|
+
const FILTER = `(p_filter ->> 'status' is null or a.status = p_filter ->> 'status')
|
|
1020
|
+
and (p_filter ->> 'tenant' is null or a.tenant = p_filter ->> 'tenant')
|
|
1021
|
+
and (p_filter ->> 'principalId' is null or a.principal_id = p_filter ->> 'principalId')
|
|
1022
|
+
and (p_filter ->> 'actorId' is null or a.actor_id = p_filter ->> 'actorId')
|
|
1023
|
+
and (p_filter ->> 'session' is null or a.session = p_filter ->> 'session')`;
|
|
1024
|
+
function serverOnly(fn, args) {
|
|
1025
|
+
return `revoke execute on function ${fn}(${args}) from public, anon, authenticated;`;
|
|
1026
|
+
}
|
|
1027
|
+
const BODY_CHECK = `${APPROVAL_STORE.table}_body_schema`;
|
|
1028
|
+
/**
|
|
1029
|
+
* `rls.jsonSchema`: a pg_jsonschema check that `body` matches
|
|
1030
|
+
* `approval-request-v1.json`, added `not valid` and then validated. `auto`
|
|
1031
|
+
* adds it only where the extension is available.
|
|
1032
|
+
*/
|
|
1033
|
+
function bodySchemaSql(table, mode, body = "body", check = BODY_CHECK) {
|
|
1034
|
+
const statements = [
|
|
1035
|
+
"create schema if not exists extensions;",
|
|
1036
|
+
"create extension if not exists pg_jsonschema with schema extensions;",
|
|
1037
|
+
`alter table ${table} drop constraint if exists "${check}";`,
|
|
1038
|
+
`alter table ${table} add constraint "${check}" check (extensions.jsonb_matches_schema(${quoteLiteral(JSON.stringify(APPROVAL_REQUEST_SCHEMA))}::json, ${body})) not valid;`,
|
|
1039
|
+
`alter table ${table} validate constraint "${check}";`
|
|
1040
|
+
];
|
|
1041
|
+
const comment = "-- approval store: body matches schemas/approval-request-v1.json through pg_jsonschema";
|
|
1042
|
+
if (mode === true) return `\n${comment}\n${statements.join("\n")}\n`;
|
|
1043
|
+
return `\n${comment}, where the extension is available
|
|
1044
|
+
do $permdock$
|
|
1045
|
+
begin
|
|
1046
|
+
if exists (select 1 from pg_available_extensions where name = 'pg_jsonschema') then
|
|
1047
|
+
${statements.map((statement) => ` ${statement}`).join("\n")}
|
|
1048
|
+
end if;
|
|
1049
|
+
end
|
|
1050
|
+
$permdock$;\n`;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* The approval store (`rls.approvals`): one table of `ApprovalRequest`
|
|
1054
|
+
* bodies with the columns a list filters on, and one `security definer`
|
|
1055
|
+
* function per `ApprovalStore` method, each one atomic statement. No client
|
|
1056
|
+
* role may read the table or execute the functions: approvals are opened and
|
|
1057
|
+
* resolved by the server, which grants its own role.
|
|
1058
|
+
*/
|
|
1059
|
+
function approvalStoreSql(ctx, jsonSchema = false) {
|
|
1060
|
+
const table = qualified(ctx, APPROVAL_STORE.table);
|
|
1061
|
+
const fn = (name) => qualified(ctx, name);
|
|
1062
|
+
const index = (name) => `"${APPROVAL_STORE.table}_${name}"`;
|
|
1063
|
+
return `-- approval store: ApprovalRequest bodies, opened, resolved and consumed through the functions below; supabaseApprovalStore calls them
|
|
1064
|
+
create table if not exists ${table} (
|
|
1065
|
+
token text primary key,
|
|
1066
|
+
body jsonb not null,
|
|
1067
|
+
status text not null check (status in ('pending', 'approved', 'rejected', 'expired')),
|
|
1068
|
+
tenant text,
|
|
1069
|
+
principal_id text,
|
|
1070
|
+
actor_id text,
|
|
1071
|
+
session text,
|
|
1072
|
+
approvals integer not null default 0,
|
|
1073
|
+
created_at text not null,
|
|
1074
|
+
expires_at timestamptz not null,
|
|
1075
|
+
consumed_at timestamptz
|
|
1076
|
+
);
|
|
1077
|
+
create index if not exists ${index("page")} on ${table} (created_at collate "C", token collate "C");
|
|
1078
|
+
create index if not exists ${index("pending")} on ${table} (status, expires_at);
|
|
1079
|
+
alter table ${table} enable row level security;
|
|
1080
|
+
revoke all on table ${table} from anon, authenticated, public;
|
|
1081
|
+
${jsonSchema === false ? "" : bodySchemaSql(table, jsonSchema)}
|
|
1082
|
+
-- open a request; a repeated ask keeps the open record and replaces only an expired one
|
|
1083
|
+
create or replace function ${fn(APPROVAL_STORE.open)}(p_request jsonb)
|
|
1084
|
+
returns void
|
|
1085
|
+
language sql
|
|
1086
|
+
volatile
|
|
1087
|
+
security definer
|
|
1088
|
+
set search_path = ''
|
|
1089
|
+
as $$
|
|
1090
|
+
insert into ${table} as a (token, body, status, tenant, principal_id, actor_id, session, approvals, created_at, expires_at, consumed_at)
|
|
1091
|
+
values (
|
|
1092
|
+
${COLUMNS}
|
|
1093
|
+
)
|
|
1094
|
+
on conflict (token) do update
|
|
1095
|
+
set body = excluded.body, status = excluded.status, tenant = excluded.tenant,
|
|
1096
|
+
principal_id = excluded.principal_id, actor_id = excluded.actor_id, session = excluded.session,
|
|
1097
|
+
approvals = excluded.approvals, created_at = excluded.created_at,
|
|
1098
|
+
expires_at = excluded.expires_at, consumed_at = excluded.consumed_at
|
|
1099
|
+
where a.status = 'expired' or a.expires_at <= now()
|
|
1100
|
+
$$;
|
|
1101
|
+
${serverOnly(fn(APPROVAL_STORE.open), "jsonb")}
|
|
1102
|
+
|
|
1103
|
+
create or replace function ${fn(APPROVAL_STORE.get)}(p_token text)
|
|
1104
|
+
returns jsonb
|
|
1105
|
+
language sql
|
|
1106
|
+
stable
|
|
1107
|
+
security definer
|
|
1108
|
+
set search_path = ''
|
|
1109
|
+
as $$
|
|
1110
|
+
select a.body from ${table} a where a.token = p_token
|
|
1111
|
+
$$;
|
|
1112
|
+
${serverOnly(fn(APPROVAL_STORE.get), "text")}
|
|
1113
|
+
|
|
1114
|
+
-- write a verdict over the record it was computed from: still pending, unexpired, with p_seen approvals
|
|
1115
|
+
create or replace function ${fn(APPROVAL_STORE.resolve)}(p_token text, p_next jsonb, p_seen integer)
|
|
1116
|
+
returns jsonb
|
|
1117
|
+
language sql
|
|
1118
|
+
volatile
|
|
1119
|
+
security definer
|
|
1120
|
+
set search_path = ''
|
|
1121
|
+
as $$
|
|
1122
|
+
update ${table} a
|
|
1123
|
+
set body = p_next,
|
|
1124
|
+
status = p_next ->> 'status',
|
|
1125
|
+
approvals = coalesce(jsonb_array_length(p_next -> 'approvals'), 0)
|
|
1126
|
+
where a.token = p_token
|
|
1127
|
+
and a.status = 'pending'
|
|
1128
|
+
and a.expires_at > now()
|
|
1129
|
+
and a.approvals = p_seen
|
|
1130
|
+
returning a.body
|
|
1131
|
+
$$;
|
|
1132
|
+
${serverOnly(fn(APPROVAL_STORE.resolve), "text, jsonb, integer")}
|
|
1133
|
+
|
|
1134
|
+
-- consume an approved, unexpired, unconsumed request once
|
|
1135
|
+
create or replace function ${fn(APPROVAL_STORE.consume)}(p_token text, p_now text)
|
|
1136
|
+
returns jsonb
|
|
1137
|
+
language sql
|
|
1138
|
+
volatile
|
|
1139
|
+
security definer
|
|
1140
|
+
set search_path = ''
|
|
1141
|
+
as $$
|
|
1142
|
+
update ${table} a
|
|
1143
|
+
set consumed_at = p_now::timestamptz,
|
|
1144
|
+
body = a.body || jsonb_build_object('consumedAt', p_now)
|
|
1145
|
+
where a.token = p_token
|
|
1146
|
+
and a.status = 'approved'
|
|
1147
|
+
and a.consumed_at is null
|
|
1148
|
+
and a.expires_at > p_now::timestamptz
|
|
1149
|
+
returning a.body
|
|
1150
|
+
$$;
|
|
1151
|
+
${serverOnly(fn(APPROVAL_STORE.consume), "text, text")}
|
|
1152
|
+
|
|
1153
|
+
-- one page in (createdAt, token) order after the cursor position
|
|
1154
|
+
create or replace function ${fn(APPROVAL_STORE.list)}(p_filter jsonb, p_after_created text, p_after_token text, p_limit integer)
|
|
1155
|
+
returns setof jsonb
|
|
1156
|
+
language sql
|
|
1157
|
+
stable
|
|
1158
|
+
security definer
|
|
1159
|
+
set search_path = ''
|
|
1160
|
+
as $$
|
|
1161
|
+
select a.body
|
|
1162
|
+
from ${table} a
|
|
1163
|
+
where ${FILTER}
|
|
1164
|
+
and (p_after_created is null
|
|
1165
|
+
or (a.created_at collate "C", a.token collate "C") > (p_after_created collate "C", p_after_token collate "C"))
|
|
1166
|
+
order by a.created_at collate "C", a.token collate "C"
|
|
1167
|
+
limit p_limit
|
|
1168
|
+
$$;
|
|
1169
|
+
${serverOnly(fn(APPROVAL_STORE.list), "jsonb, text, text, integer")}
|
|
1170
|
+
|
|
1171
|
+
create or replace function ${fn(APPROVAL_STORE.expire)}(p_now text)
|
|
1172
|
+
returns integer
|
|
1173
|
+
language sql
|
|
1174
|
+
volatile
|
|
1175
|
+
security definer
|
|
1176
|
+
set search_path = ''
|
|
1177
|
+
as $$
|
|
1178
|
+
with expired as (
|
|
1179
|
+
update ${table} a
|
|
1180
|
+
set status = 'expired', body = a.body || '{"status":"expired"}'::jsonb
|
|
1181
|
+
where a.status = 'pending' and a.expires_at <= p_now::timestamptz
|
|
1182
|
+
returning 1
|
|
1183
|
+
)
|
|
1184
|
+
select count(*)::integer from expired
|
|
1185
|
+
$$;
|
|
1186
|
+
${serverOnly(fn(APPROVAL_STORE.expire), "text")}
|
|
1187
|
+
|
|
1188
|
+
-- reject every pending request the filter matches, as system:<by>
|
|
1189
|
+
create or replace function ${fn(APPROVAL_STORE.cancel)}(p_filter jsonb, p_by text, p_note text, p_now text)
|
|
1190
|
+
returns integer
|
|
1191
|
+
language sql
|
|
1192
|
+
volatile
|
|
1193
|
+
security definer
|
|
1194
|
+
set search_path = ''
|
|
1195
|
+
as $$
|
|
1196
|
+
with cancelled as (
|
|
1197
|
+
update ${table} a
|
|
1198
|
+
set status = 'rejected',
|
|
1199
|
+
body = (a.body - 'note') || jsonb_strip_nulls(jsonb_build_object(
|
|
1200
|
+
'status', 'rejected', 'resolvedAt', p_now, 'resolvedBy', 'system:' || p_by, 'note', p_note
|
|
1201
|
+
))
|
|
1202
|
+
where a.status = 'pending'
|
|
1203
|
+
and ${FILTER.replaceAll("\n ", "\n ")}
|
|
1204
|
+
returning 1
|
|
1205
|
+
)
|
|
1206
|
+
select count(*)::integer from cancelled
|
|
1207
|
+
$$;
|
|
1208
|
+
${serverOnly(fn(APPROVAL_STORE.cancel), "jsonb, text, text, text")}
|
|
1209
|
+
`;
|
|
1210
|
+
}
|
|
1211
|
+
/**
|
|
1212
|
+
* `permdock_is_approver(p_approvers jsonb, p_tenant text default null)`: true
|
|
1213
|
+
* when the signed-in user matches the serialised approvers of a request
|
|
1214
|
+
* (`approvers.by`), for policies on the app's own approval views. A list
|
|
1215
|
+
* needs every item and `any-of` one of them; `user` compares the user id and
|
|
1216
|
+
* `holder(permission)` reads `permitted_<root>_ids_by_permission` in
|
|
1217
|
+
* `p_tenant`, or `permdock_has_permission` without one. Role, relation and
|
|
1218
|
+
* other grantee approvers need facts the database does not hold for every
|
|
1219
|
+
* membership source, so they match nobody.
|
|
1220
|
+
*/
|
|
1221
|
+
function isApproverSql(ctx) {
|
|
1222
|
+
const fn = qualified(ctx, APPROVAL_STORE.isApprover);
|
|
1223
|
+
const root = ctx.scopes[0]?.name;
|
|
1224
|
+
const inTenant = root === void 0 ? "false" : `exists (
|
|
1225
|
+
select 1
|
|
1226
|
+
from ${qualified(ctx, checkName(permittedByPermissionHelper(root)))}(p_approvers ->> 'permission') i(id)
|
|
1227
|
+
where i.id::text = p_tenant
|
|
1228
|
+
)`;
|
|
1229
|
+
return `
|
|
1230
|
+
-- approvers: whether the signed-in user matches a request's approvers.by; policies on approval views call it
|
|
1231
|
+
create or replace function ${fn}(p_approvers jsonb, p_tenant text default null)
|
|
1232
|
+
returns boolean
|
|
1233
|
+
language plpgsql
|
|
1234
|
+
stable
|
|
1235
|
+
security definer
|
|
1236
|
+
set search_path = ''
|
|
1237
|
+
as $$
|
|
1238
|
+
declare
|
|
1239
|
+
v_item jsonb;
|
|
1240
|
+
v_user text := ${subjectIdSql(ctx)}::text;
|
|
1241
|
+
begin
|
|
1242
|
+
if coalesce(v_user, '') = '' or p_approvers is null then
|
|
1243
|
+
return false;
|
|
1244
|
+
end if;
|
|
1245
|
+
if jsonb_typeof(p_approvers) = 'array' then
|
|
1246
|
+
if jsonb_array_length(p_approvers) = 0 then
|
|
1247
|
+
return false;
|
|
1248
|
+
end if;
|
|
1249
|
+
for v_item in select value from jsonb_array_elements(p_approvers) loop
|
|
1250
|
+
if not ${fn}(v_item, p_tenant) then
|
|
1251
|
+
return false;
|
|
1252
|
+
end if;
|
|
1253
|
+
end loop;
|
|
1254
|
+
return true;
|
|
1255
|
+
end if;
|
|
1256
|
+
if jsonb_typeof(p_approvers) <> 'object' then
|
|
1257
|
+
return false;
|
|
1258
|
+
end if;
|
|
1259
|
+
case p_approvers ->> 'kind'
|
|
1260
|
+
when 'user' then
|
|
1261
|
+
return coalesce(p_approvers ->> 'id' = v_user, false);
|
|
1262
|
+
when 'permission' then
|
|
1263
|
+
if p_tenant is null then
|
|
1264
|
+
return coalesce(${qualified(ctx, PERMISSION_HELPERS.has)}(p_approvers ->> 'permission'), false);
|
|
1265
|
+
end if;
|
|
1266
|
+
return ${inTenant};
|
|
1267
|
+
when 'any-of' then
|
|
1268
|
+
if jsonb_typeof(p_approvers -> 'of') <> 'array' then
|
|
1269
|
+
return false;
|
|
1270
|
+
end if;
|
|
1271
|
+
for v_item in select value from jsonb_array_elements(p_approvers -> 'of') loop
|
|
1272
|
+
if ${fn}(v_item, p_tenant) then
|
|
1273
|
+
return true;
|
|
1274
|
+
end if;
|
|
1275
|
+
end loop;
|
|
1276
|
+
return false;
|
|
1277
|
+
else
|
|
1278
|
+
return false;
|
|
1279
|
+
end case;
|
|
1280
|
+
end;
|
|
1281
|
+
$$;
|
|
1282
|
+
revoke execute on function ${fn}(jsonb, text) from public, anon;
|
|
1283
|
+
grant execute on function ${fn}(jsonb, text) to authenticated;
|
|
1284
|
+
`;
|
|
1285
|
+
}
|
|
1286
|
+
/**
|
|
1287
|
+
* Everything `rls.approvals` writes: the generated or adopted store, the
|
|
1288
|
+
* approver check and, with `delegations: true`, the covers.
|
|
1289
|
+
*/
|
|
1290
|
+
function approvalsSql(ctx, approvals, jsonSchema) {
|
|
1291
|
+
if (approvals === void 0 || approvals === false) return;
|
|
1292
|
+
if (approvals === true) return approvalStoreSql(ctx, jsonSchema) + isApproverSql(ctx);
|
|
1293
|
+
const { table, delegations } = approvals;
|
|
1294
|
+
if (delegations !== void 0 && typeof delegations !== "boolean") throw new TypeError("PermDock CLI: rls.approvals.delegations must be a boolean");
|
|
1295
|
+
let store;
|
|
1296
|
+
if (table === void 0) {
|
|
1297
|
+
const adoptOnly = [
|
|
1298
|
+
"token",
|
|
1299
|
+
"body",
|
|
1300
|
+
"open",
|
|
1301
|
+
"schema",
|
|
1302
|
+
"mirror"
|
|
1303
|
+
].find((key) => approvals[key] !== void 0);
|
|
1304
|
+
if (adoptOnly !== void 0) throw configError(`PermDock CLI: rls.approvals.${adoptOnly} adopts a table; set rls.approvals.table too`);
|
|
1305
|
+
store = approvalStoreSql(ctx, jsonSchema);
|
|
1306
|
+
} else store = adoptedApprovalStoreSql(ctx, {
|
|
1307
|
+
...approvals,
|
|
1308
|
+
table
|
|
1309
|
+
}, jsonSchema);
|
|
1310
|
+
return store + isApproverSql(ctx) + (delegations === true ? approvalDelegationsSql(ctx) : "");
|
|
1311
|
+
}
|
|
1312
|
+
/** The objects `rls.approvals.delegations` adds; `tableApprovalDelegationSource` reads the table. */
|
|
1313
|
+
const DELEGATIONS = {
|
|
1314
|
+
table: "approval_delegations",
|
|
1315
|
+
covers: "permdock_cover_approves"
|
|
1316
|
+
};
|
|
1317
|
+
/**
|
|
1318
|
+
* `rls.approvals.delegations`: one table of out-of-office covers that a
|
|
1319
|
+
* user writes for themselves, and a four-argument `permdock_is_approver`
|
|
1320
|
+
* that also answers for a delegate inside an active cover. One hop: a cover
|
|
1321
|
+
* is checked against the delegator's own approver rights, never the
|
|
1322
|
+
* delegator's covers. A delegator who is the requester never counts. A
|
|
1323
|
+
* `holder()` approver through a cover needs `rls.mode: "database"`, where
|
|
1324
|
+
* the `_for` helpers exist; in `jwt` mode only `user()` approvers pass.
|
|
1325
|
+
*/
|
|
1326
|
+
function approvalDelegationsSql(ctx) {
|
|
1327
|
+
const table = qualified(ctx, DELEGATIONS.table);
|
|
1328
|
+
const fn = qualified(ctx, APPROVAL_STORE.isApprover);
|
|
1329
|
+
const covers = qualified(ctx, DELEGATIONS.covers);
|
|
1330
|
+
const user = ctx.dialect === "supabase" ? "uuid" : "text";
|
|
1331
|
+
const me = `${subjectIdSql(ctx)}::text`;
|
|
1332
|
+
const defaultMe = subjectIdSql(ctx).replace(/^\(select (.+)\)$/su, "$1");
|
|
1333
|
+
const root = ctx.scopes[0]?.name;
|
|
1334
|
+
const member = (tenant) => root === void 0 ? "false" : `${tenant} in (select m.id::text from ${qualified(ctx, memberIdsHelper(root))}() m(id))`;
|
|
1335
|
+
const database = ctx.authorize === "database";
|
|
1336
|
+
const holderGlobal = database ? `coalesce(${qualified(ctx, PERMISSION_HELPERS.hasFor)}(p_delegator, p_approvers ->> 'permission'), false)` : "false";
|
|
1337
|
+
const holderTenant = database && root !== void 0 ? `exists (
|
|
1338
|
+
select 1
|
|
1339
|
+
from ${qualified(ctx, checkName(`${permittedByPermissionHelper(root)}_for`))}(p_delegator, p_approvers ->> 'permission') i(id)
|
|
1340
|
+
where i.id::text = p_tenant
|
|
1341
|
+
)` : "false";
|
|
1342
|
+
const policy = (name) => quoteIdent(`${DELEGATIONS.table}_${name}`);
|
|
1343
|
+
const own = `delegator::text = ${me} and (tenant is null or ${member("tenant")})`;
|
|
1344
|
+
return `
|
|
1345
|
+
-- approval delegations: out-of-office covers a user writes for themselves; tableApprovalDelegationSource and permdock_is_approver read them
|
|
1346
|
+
create table if not exists ${table} (
|
|
1347
|
+
id text primary key default gen_random_uuid()::text,
|
|
1348
|
+
delegator ${user} not null default (${defaultMe})::${user},
|
|
1349
|
+
delegate ${user} not null,
|
|
1350
|
+
tenant text,
|
|
1351
|
+
permissions text[],
|
|
1352
|
+
resources text[],
|
|
1353
|
+
"from" timestamptz not null default now(),
|
|
1354
|
+
"until" timestamptz not null,
|
|
1355
|
+
created_at timestamptz not null default now(),
|
|
1356
|
+
constraint ${quoteIdent(`${DELEGATIONS.table}_window`)} check ("from" < "until"),
|
|
1357
|
+
constraint ${quoteIdent(`${DELEGATIONS.table}_distinct`)} check (delegator <> delegate)
|
|
1358
|
+
);
|
|
1359
|
+
create index if not exists ${quoteIdent(`${DELEGATIONS.table}_delegate`)} on ${table} (delegate, "until");
|
|
1360
|
+
alter table ${table} enable row level security;
|
|
1361
|
+
revoke all on table ${table} from anon, public;
|
|
1362
|
+
grant select, insert, update, delete on table ${table} to authenticated;
|
|
1363
|
+
drop policy if exists ${policy("read")} on ${table};
|
|
1364
|
+
create policy ${policy("read")} on ${table}
|
|
1365
|
+
for select to authenticated
|
|
1366
|
+
using (delegator::text = ${me} or delegate::text = ${me});
|
|
1367
|
+
drop policy if exists ${policy("insert")} on ${table};
|
|
1368
|
+
create policy ${policy("insert")} on ${table}
|
|
1369
|
+
for insert to authenticated
|
|
1370
|
+
with check (${own});
|
|
1371
|
+
drop policy if exists ${policy("update")} on ${table};
|
|
1372
|
+
create policy ${policy("update")} on ${table}
|
|
1373
|
+
for update to authenticated
|
|
1374
|
+
using (delegator::text = ${me})
|
|
1375
|
+
with check (${own});
|
|
1376
|
+
drop policy if exists ${policy("delete")} on ${table};
|
|
1377
|
+
create policy ${policy("delete")} on ${table}
|
|
1378
|
+
for delete to authenticated
|
|
1379
|
+
using (delegator::text = ${me});
|
|
1380
|
+
|
|
1381
|
+
-- approval delegations: whether a delegator's own rights match approvers.by; only permdock_is_approver calls it
|
|
1382
|
+
create or replace function ${covers}(p_delegator ${user}, p_approvers jsonb, p_tenant text)
|
|
1383
|
+
returns boolean
|
|
1384
|
+
language plpgsql
|
|
1385
|
+
stable
|
|
1386
|
+
security definer
|
|
1387
|
+
set search_path = ''
|
|
1388
|
+
as $$
|
|
1389
|
+
declare
|
|
1390
|
+
v_item jsonb;
|
|
1391
|
+
begin
|
|
1392
|
+
if p_delegator is null or p_approvers is null then
|
|
1393
|
+
return false;
|
|
1394
|
+
end if;
|
|
1395
|
+
if jsonb_typeof(p_approvers) = 'array' then
|
|
1396
|
+
if jsonb_array_length(p_approvers) = 0 then
|
|
1397
|
+
return false;
|
|
1398
|
+
end if;
|
|
1399
|
+
for v_item in select value from jsonb_array_elements(p_approvers) loop
|
|
1400
|
+
if not ${covers}(p_delegator, v_item, p_tenant) then
|
|
1401
|
+
return false;
|
|
1402
|
+
end if;
|
|
1403
|
+
end loop;
|
|
1404
|
+
return true;
|
|
1405
|
+
end if;
|
|
1406
|
+
if jsonb_typeof(p_approvers) <> 'object' then
|
|
1407
|
+
return false;
|
|
1408
|
+
end if;
|
|
1409
|
+
case p_approvers ->> 'kind'
|
|
1410
|
+
when 'user' then
|
|
1411
|
+
return coalesce(p_approvers ->> 'id' = p_delegator::text, false);
|
|
1412
|
+
when 'permission' then
|
|
1413
|
+
if p_tenant is null then
|
|
1414
|
+
return ${holderGlobal};
|
|
1415
|
+
end if;
|
|
1416
|
+
return ${holderTenant};
|
|
1417
|
+
when 'any-of' then
|
|
1418
|
+
if jsonb_typeof(p_approvers -> 'of') <> 'array' then
|
|
1419
|
+
return false;
|
|
1420
|
+
end if;
|
|
1421
|
+
for v_item in select value from jsonb_array_elements(p_approvers -> 'of') loop
|
|
1422
|
+
if ${covers}(p_delegator, v_item, p_tenant) then
|
|
1423
|
+
return true;
|
|
1424
|
+
end if;
|
|
1425
|
+
end loop;
|
|
1426
|
+
return false;
|
|
1427
|
+
else
|
|
1428
|
+
return false;
|
|
1429
|
+
end case;
|
|
1430
|
+
end;
|
|
1431
|
+
$$;
|
|
1432
|
+
revoke execute on function ${covers}(${user}, jsonb, text) from public, anon, authenticated;
|
|
1433
|
+
|
|
1434
|
+
-- approvers with covers: the signed-in user, or a delegate inside an active cover of a delegator who is not the requester
|
|
1435
|
+
create or replace function ${fn}(p_approvers jsonb, p_tenant text, p_requester text, p_resource text)
|
|
1436
|
+
returns boolean
|
|
1437
|
+
language plpgsql
|
|
1438
|
+
stable
|
|
1439
|
+
security definer
|
|
1440
|
+
set search_path = ''
|
|
1441
|
+
as $$
|
|
1442
|
+
declare
|
|
1443
|
+
v_delegator ${user};
|
|
1444
|
+
v_user text := ${me};
|
|
1445
|
+
begin
|
|
1446
|
+
if ${fn}(p_approvers, p_tenant) then
|
|
1447
|
+
return true;
|
|
1448
|
+
end if;
|
|
1449
|
+
if coalesce(v_user, '') = '' or coalesce(p_requester, '') = '' then
|
|
1450
|
+
return false;
|
|
1451
|
+
end if;
|
|
1452
|
+
if p_tenant is not null and not (${member("p_tenant")}) then
|
|
1453
|
+
return false;
|
|
1454
|
+
end if;
|
|
1455
|
+
for v_delegator in
|
|
1456
|
+
select d.delegator
|
|
1457
|
+
from ${table} d
|
|
1458
|
+
where d.delegate::text = v_user
|
|
1459
|
+
and d.delegator::text <> v_user
|
|
1460
|
+
and d.delegator::text <> p_requester
|
|
1461
|
+
and d."from" <= now()
|
|
1462
|
+
and now() < d."until"
|
|
1463
|
+
and (d.tenant is null or d.tenant = p_tenant)
|
|
1464
|
+
and d.permissions is null
|
|
1465
|
+
and (d.resources is null or p_resource = any (d.resources))
|
|
1466
|
+
loop
|
|
1467
|
+
if ${covers}(v_delegator, p_approvers, p_tenant) then
|
|
1468
|
+
return true;
|
|
1469
|
+
end if;
|
|
1470
|
+
end loop;
|
|
1471
|
+
return false;
|
|
1472
|
+
end;
|
|
1473
|
+
$$;
|
|
1474
|
+
revoke execute on function ${fn}(jsonb, text, text, text) from public, anon;
|
|
1475
|
+
grant execute on function ${fn}(jsonb, text, text, text) to authenticated;
|
|
1476
|
+
`;
|
|
1477
|
+
}
|
|
1478
|
+
/** The request fields an adopted table may mirror into its own columns, as SQL over a body. */
|
|
1479
|
+
const MIRRORS = {
|
|
1480
|
+
status: (body) => `${body} ->> 'status'`,
|
|
1481
|
+
permission: (body) => `${body} ->> 'permission'`,
|
|
1482
|
+
tenant: (body) => `${body} #>> '{subject,principal,tenant}'`,
|
|
1483
|
+
principalId: (body) => `${body} #>> '{subject,principal,id}'`,
|
|
1484
|
+
actorId: (body) => `${body} #>> '{subject,actor,id}'`,
|
|
1485
|
+
session: (body) => `${body} #>> '{subject,session}'`,
|
|
1486
|
+
approvals: (body) => `coalesce(jsonb_array_length(${body} -> 'approvals'), 0)`,
|
|
1487
|
+
createdAt: (body) => `${body} ->> 'createdAt'`,
|
|
1488
|
+
expiresAt: (body) => `${body} ->> 'expiresAt'`,
|
|
1489
|
+
resolvedAt: (body) => `${body} ->> 'resolvedAt'`,
|
|
1490
|
+
resolvedBy: (body) => `${body} ->> 'resolvedBy'`,
|
|
1491
|
+
consumedAt: (body) => `${body} ->> 'consumedAt'`
|
|
1492
|
+
};
|
|
1493
|
+
function mirrorsOf(adopt) {
|
|
1494
|
+
const out = [];
|
|
1495
|
+
for (const [field, column] of Object.entries(adopt.mirror ?? {})) {
|
|
1496
|
+
if (!Object.hasOwn(MIRRORS, field)) throw configError(`PermDock CLI: rls.approvals.mirror.${field} is not a request field; use ${Object.keys(MIRRORS).join(", ")}`);
|
|
1497
|
+
if (typeof column !== "string") throw new TypeError(`PermDock CLI: rls.approvals.mirror.${field} must be a column name`);
|
|
1498
|
+
out.push([column, field]);
|
|
1499
|
+
}
|
|
1500
|
+
return out;
|
|
1501
|
+
}
|
|
1502
|
+
/** One row of `table` from `values` (column, SQL), each converted to the column's own type. */
|
|
1503
|
+
function populate(table, values) {
|
|
1504
|
+
return `pg_catalog.jsonb_populate_record(null::${table}, pg_catalog.jsonb_build_object(${values.map(([column, sql]) => `${quoteLiteral(column)}, ${sql}`).join(", ")}))`;
|
|
1505
|
+
}
|
|
1506
|
+
/** The mirrored columns' values for the request `body`. */
|
|
1507
|
+
function mirrorRow(table, mirror, body) {
|
|
1508
|
+
return populate(table, mirror.map(([column, field]) => [column, MIRRORS[field](body)]));
|
|
1509
|
+
}
|
|
1510
|
+
/** `, (<mirrored columns>) = (select …)` for an update that writes `body`; empty without mirrors. */
|
|
1511
|
+
function mirrorSet(table, mirror, body) {
|
|
1512
|
+
if (mirror.length === 0) return "";
|
|
1513
|
+
const columns = mirror.map(([column]) => quoteIdent(column));
|
|
1514
|
+
const picked = columns.map((column) => `r.${column}`).join(", ");
|
|
1515
|
+
return `,\n ${columns.length === 1 ? columns[0] ?? "" : `(${columns.join(", ")})`} = (select ${picked} from ${mirrorRow(table, mirror, body)} r)`;
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* The approval store over a table the app already has (`rls.approvals:
|
|
1519
|
+
* { table, token?, body?, mirror? }`): the same `permdock_approval_*`
|
|
1520
|
+
* functions, reading every field from the request body in `body`, keyed by
|
|
1521
|
+
* the text column `token`, and copying the fields `mirror` names into the
|
|
1522
|
+
* app's own columns, converted to their types, on every write. The table's
|
|
1523
|
+
* grants and policies stay the app's.
|
|
1524
|
+
*/
|
|
1525
|
+
function adoptedApprovalStoreSql(ctx, adopt, jsonSchema = false) {
|
|
1526
|
+
const table = quoteQualified(adopt.table);
|
|
1527
|
+
const base = adopt.table.split(".").at(-1) ?? adopt.table;
|
|
1528
|
+
const token = quoteIdent(adopt.token ?? "token");
|
|
1529
|
+
const body = quoteIdent(adopt.body ?? "body");
|
|
1530
|
+
const mirror = mirrorsOf(adopt);
|
|
1531
|
+
for (const [column] of mirror) if (column === (adopt.token ?? "token") || column === (adopt.body ?? "body")) throw configError(`PermDock CLI: rls.approvals.mirror names the ${column} column, which already holds the token or the body`);
|
|
1532
|
+
const open = adopt.open ?? "insert";
|
|
1533
|
+
if (open !== "insert" && open !== "attach") throw configError(`PermDock CLI: rls.approvals.open must be 'insert' or 'attach', got '${String(open)}'`);
|
|
1534
|
+
const schema = adopt.schema;
|
|
1535
|
+
if (schema !== void 0 && (typeof schema !== "string" || schema === "")) throw configError("PermDock CLI: rls.approvals.schema must be a schema name");
|
|
1536
|
+
const fn = (name) => schema === void 0 ? qualified(ctx, name) : `${quoteIdent(schema)}.${name}`;
|
|
1537
|
+
const index = (name) => quoteIdent(`${base}_permdock_${name}`);
|
|
1538
|
+
const status = `a.${body} ->> 'status'`;
|
|
1539
|
+
const expires = `(a.${body} ->> 'expiresAt')::timestamptz`;
|
|
1540
|
+
const created = `(a.${body} ->> 'createdAt')`;
|
|
1541
|
+
const filter = `(p_filter ->> 'status' is null or ${status} = p_filter ->> 'status')
|
|
1542
|
+
and (p_filter ->> 'tenant' is null or a.${body} #>> '{subject,principal,tenant}' = p_filter ->> 'tenant')
|
|
1543
|
+
and (p_filter ->> 'principalId' is null or a.${body} #>> '{subject,principal,id}' = p_filter ->> 'principalId')
|
|
1544
|
+
and (p_filter ->> 'actorId' is null or a.${body} #>> '{subject,actor,id}' = p_filter ->> 'actorId')
|
|
1545
|
+
and (p_filter ->> 'session' is null or a.${body} #>> '{subject,session}' = p_filter ->> 'session')`;
|
|
1546
|
+
const columns = [
|
|
1547
|
+
token,
|
|
1548
|
+
body,
|
|
1549
|
+
...mirror.map(([column]) => quoteIdent(column))
|
|
1550
|
+
];
|
|
1551
|
+
const inserted = populate(table, [
|
|
1552
|
+
[adopt.token ?? "token", "p_request ->> 'token'"],
|
|
1553
|
+
[adopt.body ?? "body", "p_request"],
|
|
1554
|
+
...mirror.map(([column, field]) => [column, MIRRORS[field]("p_request")])
|
|
1555
|
+
]);
|
|
1556
|
+
const insertSql = `-- open a request; a repeated ask keeps the open record and replaces only an expired one
|
|
1557
|
+
create or replace function ${fn(APPROVAL_STORE.open)}(p_request jsonb)
|
|
1558
|
+
returns void
|
|
1559
|
+
language sql
|
|
1560
|
+
volatile
|
|
1561
|
+
security definer
|
|
1562
|
+
set search_path = ''
|
|
1563
|
+
as $$
|
|
1564
|
+
insert into ${table} as a (${columns.join(", ")})
|
|
1565
|
+
select ${columns.map((column) => `r.${column}`).join(", ")}
|
|
1566
|
+
from ${inserted} r
|
|
1567
|
+
on conflict (${token}) do update
|
|
1568
|
+
set ${columns.filter((column) => column !== token).map((column) => `${column} = excluded.${column}`).join(", ")}
|
|
1569
|
+
where ${status} = 'expired' or ${expires} <= now()
|
|
1570
|
+
$$;`;
|
|
1571
|
+
const attachSql = `create or replace function ${fn(APPROVAL_STORE.open)}(p_request jsonb)
|
|
1572
|
+
returns void
|
|
1573
|
+
language plpgsql
|
|
1574
|
+
volatile
|
|
1575
|
+
security definer
|
|
1576
|
+
set search_path = ''
|
|
1577
|
+
as $$
|
|
1578
|
+
begin
|
|
1579
|
+
update ${table} a
|
|
1580
|
+
set ${body} = p_request${mirrorSet(table, mirror, "p_request")}
|
|
1581
|
+
where a.${token} = p_request ->> 'token'
|
|
1582
|
+
and (a.${body} is null or ${status} = 'expired' or ${expires} <= now());
|
|
1583
|
+
if not found and not exists (select 1 from ${table} a where a.${token} = p_request ->> 'token') then
|
|
1584
|
+
raise exception using
|
|
1585
|
+
errcode = 'P0002',
|
|
1586
|
+
message = 'permdock: no row of ${adopt.table.replaceAll("'", "''")} holds approval token ' || (p_request ->> 'token'),
|
|
1587
|
+
hint = 'insert the application row with this token before opening the request';
|
|
1588
|
+
end if;
|
|
1589
|
+
end;
|
|
1590
|
+
$$;`;
|
|
1591
|
+
return `-- approval store adopted onto ${adopt.table}: ApprovalRequest bodies in ${adopt.body ?? "body"}, keyed by ${adopt.token ?? "token"}; supabaseApprovalStore calls the functions below
|
|
1592
|
+
alter table ${table} add column if not exists ${token} text;
|
|
1593
|
+
alter table ${table} add column if not exists ${body} jsonb;
|
|
1594
|
+
create unique index if not exists ${index("token")} on ${table} (${token});
|
|
1595
|
+
create index if not exists ${index("page")} on ${table} ((${body} ->> 'createdAt') collate "C", ${token} collate "C");
|
|
1596
|
+
create index if not exists ${index("status")} on ${table} ((${body} ->> 'status'));
|
|
1597
|
+
${jsonSchema === false ? "" : bodySchemaSql(table, jsonSchema, body, `${base}_permdock_body_schema`)}
|
|
1598
|
+
${open === "attach" ? attachSql : insertSql}
|
|
1599
|
+
${serverOnly(fn(APPROVAL_STORE.open), "jsonb")}
|
|
1600
|
+
|
|
1601
|
+
create or replace function ${fn(APPROVAL_STORE.get)}(p_token text)
|
|
1602
|
+
returns jsonb
|
|
1603
|
+
language sql
|
|
1604
|
+
stable
|
|
1605
|
+
security definer
|
|
1606
|
+
set search_path = ''
|
|
1607
|
+
as $$
|
|
1608
|
+
select a.${body} from ${table} a where a.${token} = p_token
|
|
1609
|
+
$$;
|
|
1610
|
+
${serverOnly(fn(APPROVAL_STORE.get), "text")}
|
|
1611
|
+
|
|
1612
|
+
-- write a verdict over the record it was computed from: still pending, unexpired, with p_seen approvals
|
|
1613
|
+
create or replace function ${fn(APPROVAL_STORE.resolve)}(p_token text, p_next jsonb, p_seen integer)
|
|
1614
|
+
returns jsonb
|
|
1615
|
+
language sql
|
|
1616
|
+
volatile
|
|
1617
|
+
security definer
|
|
1618
|
+
set search_path = ''
|
|
1619
|
+
as $$
|
|
1620
|
+
update ${table} a
|
|
1621
|
+
set ${body} = p_next${mirrorSet(table, mirror, "p_next")}
|
|
1622
|
+
where a.${token} = p_token
|
|
1623
|
+
and ${status} = 'pending'
|
|
1624
|
+
and ${expires} > now()
|
|
1625
|
+
and coalesce(jsonb_array_length(a.${body} -> 'approvals'), 0) = p_seen
|
|
1626
|
+
returning a.${body}
|
|
1627
|
+
$$;
|
|
1628
|
+
${serverOnly(fn(APPROVAL_STORE.resolve), "text, jsonb, integer")}
|
|
1629
|
+
|
|
1630
|
+
-- consume an approved, unexpired, unconsumed request once
|
|
1631
|
+
create or replace function ${fn(APPROVAL_STORE.consume)}(p_token text, p_now text)
|
|
1632
|
+
returns jsonb
|
|
1633
|
+
language sql
|
|
1634
|
+
volatile
|
|
1635
|
+
security definer
|
|
1636
|
+
set search_path = ''
|
|
1637
|
+
as $$
|
|
1638
|
+
update ${table} a
|
|
1639
|
+
set ${body} = a.${body} || jsonb_build_object('consumedAt', p_now)${mirrorSet(table, mirror, `(a.${body} || jsonb_build_object('consumedAt', p_now))`)}
|
|
1640
|
+
where a.${token} = p_token
|
|
1641
|
+
and ${status} = 'approved'
|
|
1642
|
+
and a.${body} ->> 'consumedAt' is null
|
|
1643
|
+
and ${expires} > p_now::timestamptz
|
|
1644
|
+
returning a.${body}
|
|
1645
|
+
$$;
|
|
1646
|
+
${serverOnly(fn(APPROVAL_STORE.consume), "text, text")}
|
|
1647
|
+
|
|
1648
|
+
-- one page in (createdAt, token) order after the cursor position
|
|
1649
|
+
create or replace function ${fn(APPROVAL_STORE.list)}(p_filter jsonb, p_after_created text, p_after_token text, p_limit integer)
|
|
1650
|
+
returns setof jsonb
|
|
1651
|
+
language sql
|
|
1652
|
+
stable
|
|
1653
|
+
security definer
|
|
1654
|
+
set search_path = ''
|
|
1655
|
+
as $$
|
|
1656
|
+
select a.${body}
|
|
1657
|
+
from ${table} a
|
|
1658
|
+
where a.${body} is not null
|
|
1659
|
+
and ${filter}
|
|
1660
|
+
and (p_after_created is null
|
|
1661
|
+
or (${created} collate "C", a.${token} collate "C") > (p_after_created collate "C", p_after_token collate "C"))
|
|
1662
|
+
order by ${created} collate "C", a.${token} collate "C"
|
|
1663
|
+
limit p_limit
|
|
1664
|
+
$$;
|
|
1665
|
+
${serverOnly(fn(APPROVAL_STORE.list), "jsonb, text, text, integer")}
|
|
1666
|
+
|
|
1667
|
+
create or replace function ${fn(APPROVAL_STORE.expire)}(p_now text)
|
|
1668
|
+
returns integer
|
|
1669
|
+
language sql
|
|
1670
|
+
volatile
|
|
1671
|
+
security definer
|
|
1672
|
+
set search_path = ''
|
|
1673
|
+
as $$
|
|
1674
|
+
with expired as (
|
|
1675
|
+
update ${table} a
|
|
1676
|
+
set ${body} = a.${body} || '{"status":"expired"}'::jsonb${mirrorSet(table, mirror, `(a.${body} || '{"status":"expired"}'::jsonb)`)}
|
|
1677
|
+
where ${status} = 'pending' and ${expires} <= p_now::timestamptz
|
|
1678
|
+
returning 1
|
|
1679
|
+
)
|
|
1680
|
+
select count(*)::integer from expired
|
|
1681
|
+
$$;
|
|
1682
|
+
${serverOnly(fn(APPROVAL_STORE.expire), "text")}
|
|
1683
|
+
|
|
1684
|
+
-- reject every pending request the filter matches, as system:<by>
|
|
1685
|
+
create or replace function ${fn(APPROVAL_STORE.cancel)}(p_filter jsonb, p_by text, p_note text, p_now text)
|
|
1686
|
+
returns integer
|
|
1687
|
+
language sql
|
|
1688
|
+
volatile
|
|
1689
|
+
security definer
|
|
1690
|
+
set search_path = ''
|
|
1691
|
+
as $$
|
|
1692
|
+
with cancelled as (
|
|
1693
|
+
update ${table} a
|
|
1694
|
+
set ${body} = (a.${body} - 'note') || jsonb_strip_nulls(jsonb_build_object(
|
|
1695
|
+
'status', 'rejected', 'resolvedAt', p_now, 'resolvedBy', 'system:' || p_by, 'note', p_note
|
|
1696
|
+
))${mirrorSet(table, mirror, `((a.${body} - 'note') || jsonb_strip_nulls(jsonb_build_object('status', 'rejected', 'resolvedAt', p_now, 'resolvedBy', 'system:' || p_by, 'note', p_note)))`)}
|
|
1697
|
+
where ${status} = 'pending'
|
|
1698
|
+
and ${filter.replaceAll("\n ", "\n ")}
|
|
1699
|
+
returning 1
|
|
1700
|
+
)
|
|
1701
|
+
select count(*)::integer from cancelled
|
|
1702
|
+
$$;
|
|
1703
|
+
${serverOnly(fn(APPROVAL_STORE.cancel), "jsonb, text, text, text")}
|
|
1704
|
+
`;
|
|
1705
|
+
}
|
|
1706
|
+
//#endregion
|
|
1707
|
+
//#region src/cli/rls/permission-holders.ts
|
|
1708
|
+
/** `permitted_<scope>_users(p_permission, p_id)`. Names are part of the SQL contract. */
|
|
1709
|
+
function permittedUsersHelper(scope) {
|
|
1710
|
+
return checkName(`permitted_${scope}_users`);
|
|
1711
|
+
}
|
|
1712
|
+
/** The users with a membership row on the instance `p_id`, or `undefined` when the scope's memberships are not all in tables. */
|
|
1713
|
+
function candidatesSql(ctx, scope, user) {
|
|
1714
|
+
const mapped = scopeTable(ctx, scope);
|
|
1715
|
+
if (mapped !== void 0) return {
|
|
1716
|
+
sql: ` select m.${quoteIdent(mapped.table.user)}::${user}
|
|
1717
|
+
from ${quoteQualified(mapped.table.table)} m
|
|
1718
|
+
where m.${quoteIdent(mapped.column)} = p_id`,
|
|
1719
|
+
keys: []
|
|
1720
|
+
};
|
|
1721
|
+
const sources = holderSources(ctx, scope);
|
|
1722
|
+
if (sources === void 0 || sources === "not-tables") return;
|
|
1723
|
+
return {
|
|
1724
|
+
sql: sources.map((source, index) => ` select h.user_id::${user}
|
|
1725
|
+
from (
|
|
1726
|
+
${source.holders.rows(scope, { id: `v_key_${String(index)}` }).replaceAll(/^/gmu, " ")}
|
|
1727
|
+
) h`).join("\n union\n"),
|
|
1728
|
+
keys: sources.map((source, index) => `v_key_${String(index)} ${source.holders.idType}`)
|
|
1729
|
+
};
|
|
1730
|
+
}
|
|
1731
|
+
/** `language sql`, or PL/pgSQL when a source's id column needs `p_id` in a `%type` variable. */
|
|
1732
|
+
function functionBody(keys, query) {
|
|
1733
|
+
if (keys.length === 0) return `language sql
|
|
1734
|
+
stable
|
|
1735
|
+
security definer
|
|
1736
|
+
set search_path = ''
|
|
1737
|
+
as $$
|
|
1738
|
+
${query}
|
|
1739
|
+
$$;`;
|
|
1740
|
+
return `language plpgsql
|
|
1741
|
+
stable
|
|
1742
|
+
security definer
|
|
1743
|
+
set search_path = ''
|
|
1744
|
+
as $$
|
|
1745
|
+
declare
|
|
1746
|
+
${keys.map((key) => ` ${key} := p_id;`).join("\n")}
|
|
1747
|
+
begin
|
|
1748
|
+
return query
|
|
1749
|
+
${query.replaceAll(/^/gmu, " ")};
|
|
1750
|
+
end
|
|
1751
|
+
$$;`;
|
|
1752
|
+
}
|
|
1753
|
+
/**
|
|
1754
|
+
* One `permitted_<scope>_users(p_permission, p_id)` per scope in `database`
|
|
1755
|
+
* mode: the users who hold a permission key on one instance, read from the
|
|
1756
|
+
* scope's membership rows and the global-roles table and kept when
|
|
1757
|
+
* `permitted_<scope>_ids_by_permission_for` lists the instance or
|
|
1758
|
+
* `permdock_has_permission_for` answers, so denies, custom roles, expiry and
|
|
1759
|
+
* suspension apply as for the user. It names other users, so no client role
|
|
1760
|
+
* may execute it, and a backend role needs its own grant.
|
|
1761
|
+
*/
|
|
1762
|
+
function permissionHoldersSql(ctx) {
|
|
1763
|
+
if (ctx.authorize !== "database") return "";
|
|
1764
|
+
const user = ctx.dialect === "supabase" ? "uuid" : "text";
|
|
1765
|
+
const global = globalRoleRows(ctx);
|
|
1766
|
+
const hasFor = qualified(ctx, PERMISSION_HELPERS.hasFor);
|
|
1767
|
+
return `${ctx.scopes.map(({ name }) => {
|
|
1768
|
+
const candidates = hasPermittedUsers(ctx, name) ? candidatesSql(ctx, name, user) : void 0;
|
|
1769
|
+
if (candidates === void 0) return `-- ${name}: no memberships table or table source, so permitted_${name}_users is not written`;
|
|
1770
|
+
const fn = qualified(ctx, permittedUsersHelper(name));
|
|
1771
|
+
const ids = qualified(ctx, checkName(`${permittedByPermissionHelper(name)}_for`));
|
|
1772
|
+
const type = scopeTypeOf(ctx, name);
|
|
1773
|
+
return `-- the users who hold a permission on one ${name}: its members and the global-role holders for whom the _for helpers answer; no client role may execute it
|
|
1774
|
+
create or replace function ${fn}(p_permission text, p_id ${type})
|
|
1775
|
+
returns setof ${user}
|
|
1776
|
+
${functionBody(candidates.keys, ` select c.user_id
|
|
1777
|
+
from (
|
|
1778
|
+
${candidates.sql}
|
|
1779
|
+
union
|
|
1780
|
+
select ${global.userSql}::${user}
|
|
1781
|
+
from ${global.from}
|
|
1782
|
+
) c(user_id)
|
|
1783
|
+
where c.user_id is not null
|
|
1784
|
+
and (
|
|
1785
|
+
p_id in (select ${ids}(c.user_id, p_permission))
|
|
1786
|
+
or ${hasFor}(c.user_id, p_permission)
|
|
1787
|
+
)
|
|
1788
|
+
order by 1`)}
|
|
1789
|
+
revoke execute on function ${fn}(text, ${type}) from public, anon, authenticated;`;
|
|
1790
|
+
}).join("\n\n")}\n`;
|
|
1791
|
+
}
|
|
1792
|
+
//#endregion
|
|
919
1793
|
//#region src/cli/supabase-hook/sql.ts
|
|
920
1794
|
const MANAGED_TRIGGER = "permdock_protect_managed";
|
|
921
1795
|
function hookSql(parts) {
|
|
@@ -927,14 +1801,14 @@ function hookSql(parts) {
|
|
|
927
1801
|
from ${rows.from}
|
|
928
1802
|
where ${rows.userSql} = v_roles_user;`;
|
|
929
1803
|
const plan = parts.attrs;
|
|
930
|
-
const
|
|
1804
|
+
const tableMembershipSource = plan === void 0 || plan.columns.length === 0 || plan.table === void 0 ? "" : `
|
|
931
1805
|
select jsonb_strip_nulls(jsonb_build_object(${plan.columns.map((column) => `${quoteLiteral(column)}, to_jsonb(p.${quoteIdent(column)})`).join(", ")}))
|
|
932
1806
|
into attrs
|
|
933
1807
|
from ${quoteQualified(plan.table)} p
|
|
934
1808
|
where p.${quoteIdent(plan.id)} = v_attrs_user;`;
|
|
935
1809
|
const fromMeta = plan === void 0 || plan.meta.length === 0 ? "" : `
|
|
936
1810
|
attrs := coalesce(attrs, '{}'::jsonb) || jsonb_strip_nulls(jsonb_build_object(${plan.meta.map((key) => `${quoteLiteral(key)}, claims -> 'app_metadata' -> ${quoteLiteral(key)}`).join(", ")}));`;
|
|
937
|
-
const attrs = plan === void 0 ? "" : `${
|
|
1811
|
+
const attrs = plan === void 0 ? "" : `${tableMembershipSource}${fromMeta}
|
|
938
1812
|
if attrs is not null and attrs <> '{}'::jsonb then
|
|
939
1813
|
used := octet_length(attrs::text);
|
|
940
1814
|
if used > budget then
|
|
@@ -1032,7 +1906,7 @@ function extraGrantsSql(extra) {
|
|
|
1032
1906
|
}
|
|
1033
1907
|
function beforeGrantsSql(before, extra) {
|
|
1034
1908
|
const granted = new Set(extra.map((entry) => entry.fn.split(".")[0]));
|
|
1035
|
-
return [...[...new Set(before.map((fn) => fn
|
|
1909
|
+
return [...[...new Set(before.map((fn) => beforeFirst(fn, ".")))].filter((name) => !granted.has(name)).map((name) => `\ngrant usage on schema ${quoteIdent(name)} to supabase_auth_admin;`), ...before.map((fn) => `\ngrant execute on function ${quoteTable(fn)}(jsonb) to supabase_auth_admin;`)].join("");
|
|
1036
1910
|
}
|
|
1037
1911
|
/**
|
|
1038
1912
|
* Execute on each `member_<scope>_ids_for(uuid)` for the `hook.claims`
|
|
@@ -1104,7 +1978,7 @@ uri = "${hookUri(schema)}"`;
|
|
|
1104
1978
|
const BUDGET_MEASURE = "octet_length(memberships::text) + octet_length(attrs::text)";
|
|
1105
1979
|
function hookParts(scopes, config, overrides) {
|
|
1106
1980
|
const hook = config.supabase?.hook;
|
|
1107
|
-
if (hook === void 0) throw
|
|
1981
|
+
if (hook === void 0) throw configError("PermDock CLI: supabase hook generate needs supabase.hook in permdock.config.ts");
|
|
1108
1982
|
const root = rootScope(scopes) ?? "tenant";
|
|
1109
1983
|
const suspension = checkSuspension(hook.suspension ?? config.rls?.suspension, scopes);
|
|
1110
1984
|
const schema = overrides.schema ?? hook.schema ?? config.rls?.schema ?? "permdock";
|
|
@@ -1140,8 +2014,8 @@ function hookParts(scopes, config, overrides) {
|
|
|
1140
2014
|
})
|
|
1141
2015
|
}
|
|
1142
2016
|
};
|
|
1143
|
-
if (parts.attrs !== void 0 && parts.attrs.errors.length > 0) throw
|
|
1144
|
-
if (extraPlan.errors.length > 0) throw
|
|
2017
|
+
if (parts.attrs !== void 0 && parts.attrs.errors.length > 0) throw configError(`PermDock CLI: ${parts.attrs.errors.join("; ")}`);
|
|
2018
|
+
if (extraPlan.errors.length > 0) throw configError(`PermDock CLI: ${extraPlan.errors.join("; ")}`);
|
|
1145
2019
|
quoteIdent(parts.tenantClaim);
|
|
1146
2020
|
return parts;
|
|
1147
2021
|
}
|
|
@@ -1453,11 +2327,14 @@ function trustedHelpers(parts, config, types, tenantType, policy) {
|
|
|
1453
2327
|
const forUser = database ? [
|
|
1454
2328
|
helperEntry(HELPERS.hasFor, "p_user uuid, p_grant text", "boolean", []),
|
|
1455
2329
|
...scopeTypes.map(([scope, type]) => helperEntry(permittedForHelper(scope), "p_user uuid, p_grant text", `setof ${type}`, [])),
|
|
1456
|
-
...scopeTypes.map(([scope, type]) => helperEntry(permissionKeysForHelper(scope), `p_user uuid, p_id ${type}`, "setof text", []))
|
|
2330
|
+
...scopeTypes.map(([scope, type]) => helperEntry(permissionKeysForHelper(scope), `p_user uuid, p_id ${type}`, "setof text", [])),
|
|
2331
|
+
...scopeTypes.filter(([scope]) => parts.helpers.holders.includes(scope)).map(([scope, type]) => helperEntry(permittedUsersHelper(scope), `p_permission text, p_id ${type}`, "setof uuid", []))
|
|
1457
2332
|
] : [];
|
|
1458
2333
|
const keyed = [
|
|
1459
2334
|
helperEntry(USER_ID_HELPER, "", "uuid", client),
|
|
1460
2335
|
...config.rls?.apiKeys === void 0 ? [] : [helperEntry(API_KEY_ALLOWS, "p_grant text", "boolean", client)],
|
|
2336
|
+
...readOnlyActorKinds(config.rls?.readOnlyActors) === void 0 ? [] : [helperEntry(ACTOR_MAY_WRITE, "", "boolean", client)],
|
|
2337
|
+
...!config.rls?.approvals ? [] : [helperEntry(IS_APPROVER, typeof config.rls.approvals === "object" && config.rls.approvals.delegations === true ? "p_approvers jsonb, p_tenant text, p_requester text, p_resource text" : "p_approvers jsonb, p_tenant text", "boolean", client)],
|
|
1461
2338
|
...forUser
|
|
1462
2339
|
];
|
|
1463
2340
|
if (!(policy !== void 0 && (ownershipRules(policy, parts.scopes)?.assigns.length ?? 0) > 0)) return keyed;
|
|
@@ -1606,25 +2483,25 @@ function defaultOut(config) {
|
|
|
1606
2483
|
//#region src/cli/supabase-hook/supabase-hook.ts
|
|
1607
2484
|
const SUPABASE_HELP = `permdock supabase hook generate | inspect
|
|
1608
2485
|
|
|
1609
|
-
hook generate [--out
|
|
2486
|
+
hook generate [--out <file>] [--check] [--db-url-stdin] [--schemas pg-delta|migra|migrations]
|
|
1610
2487
|
[--active-from app_metadata.active_<scope>|<table>.<column>]
|
|
1611
2488
|
[--budget 1024] [--schema public] [--grants-out <file>|-]
|
|
1612
2489
|
inspect [--json] [--out [permdock.manifest.json]] [--check]
|
|
1613
2490
|
|
|
1614
|
-
Reads supabase.hook from permdock.config.ts: the
|
|
2491
|
+
Reads supabase.hook from permdock.config.ts: the tableMembershipSource / junctionMembershipSource sources the app
|
|
1615
2492
|
passes as memberships. hook generate emits custom_access_token_hook(jsonb), the grants it
|
|
1616
|
-
needs
|
|
1617
|
-
and
|
|
2493
|
+
needs, the permdock_authz_version table and its triggers. Under pg-delta (the default) --out defaults to
|
|
2494
|
+
supabase/schemas/<schema>/functions/custom_access_token_hook.sql and the grants stay in it; under
|
|
2495
|
+
--schemas migra, --grants-out takes the privileges supabase db diff drops. Never grants anything to
|
|
1618
2496
|
service_role unless supabase.hook.api is set, which grants its wrappers to it. inspect prints the manifest: helper schema, names and signatures, tenant claim,
|
|
1619
2497
|
budget, the claims the hook writes, the membership sources and the columns that decide them.
|
|
1620
2498
|
--out writes it as JSON, to permdock.manifest.json without a path; --check exits 1 when that
|
|
1621
2499
|
file differs.
|
|
1622
2500
|
`;
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
return config.supabase?.hook?.out !== void 0 || pgDelta === void 0 ? defaultOut(config) : pgDeltaPath(pgDelta.schemaDir, "hook", schema ?? config.supabase?.hook?.schema ?? config.rls?.schema ?? "permdock");
|
|
2501
|
+
/** `supabase.hook.out`, else pg-delta's per-schema path under pg-delta, else `supabase/permdock-hook.sql`. */
|
|
2502
|
+
function hookOut(cwd, config, schema, flag) {
|
|
2503
|
+
const schemas = resolveSchemas(cwd, config, flag);
|
|
2504
|
+
return config.supabase?.hook?.out !== void 0 || schemas.mode !== "pg-delta" ? defaultOut(config) : pgDeltaPath(schemas.schemaDir, "hook", schema ?? config.supabase?.hook?.schema ?? config.rls?.schema ?? "permdock");
|
|
1628
2505
|
}
|
|
1629
2506
|
/**
|
|
1630
2507
|
* The manifest `supabase inspect` prints. With `policy`, `rls.helpers` also
|
|
@@ -1632,7 +2509,14 @@ function hookOut(cwd, config, schema) {
|
|
|
1632
2509
|
* `assigns`.
|
|
1633
2510
|
*/
|
|
1634
2511
|
function supabaseHookManifest(scopes, config, overrides = {}, policy) {
|
|
1635
|
-
|
|
2512
|
+
const manifest = manifestOf(hookParts(scopes, config, overrides), overrides.out ?? defaultOut(config), config, policy);
|
|
2513
|
+
return overrides.schemas === void 0 ? manifest : {
|
|
2514
|
+
...manifest,
|
|
2515
|
+
hook: {
|
|
2516
|
+
...manifest.hook,
|
|
2517
|
+
schemas: overrides.schemas
|
|
2518
|
+
}
|
|
2519
|
+
};
|
|
1636
2520
|
}
|
|
1637
2521
|
/**
|
|
1638
2522
|
* The hook migration. With `grantsOut`, the statements `supabase db diff`
|
|
@@ -1697,7 +2581,7 @@ function inspectText(manifest) {
|
|
|
1697
2581
|
function manifestFile(cwd, rel, manifest, check) {
|
|
1698
2582
|
const path = resolve(cwd, rel);
|
|
1699
2583
|
if (!check) {
|
|
1700
|
-
writeFileEnsuringDir(path,
|
|
2584
|
+
writeFileEnsuringDir(path, formatManifestJson(manifest));
|
|
1701
2585
|
return {
|
|
1702
2586
|
code: 0,
|
|
1703
2587
|
output: `wrote ${rel}`
|
|
@@ -1734,6 +2618,13 @@ function loadHookPolicy(cwd, config) {
|
|
|
1734
2618
|
}
|
|
1735
2619
|
async function runSupabase(input) {
|
|
1736
2620
|
const [area, action] = input.rest;
|
|
2621
|
+
const badSchemas = schemasRefusal("supabase", input.schemas);
|
|
2622
|
+
if (badSchemas !== void 0) return {
|
|
2623
|
+
code: 2,
|
|
2624
|
+
output: badSchemas
|
|
2625
|
+
};
|
|
2626
|
+
const flag = isSupabaseSchemas(input.schemas) ? input.schemas : void 0;
|
|
2627
|
+
const schemas = resolveSchemas(input.cwd, input.config, flag);
|
|
1737
2628
|
const overrides = Object.fromEntries(Object.entries({
|
|
1738
2629
|
activeFrom: input.activeFrom,
|
|
1739
2630
|
budget: input.budget,
|
|
@@ -1743,7 +2634,8 @@ async function runSupabase(input) {
|
|
|
1743
2634
|
const policy = await loadHookPolicy(input.cwd, input.config);
|
|
1744
2635
|
const manifest = supabaseHookManifest(scopeList(policy.scopes), input.config, {
|
|
1745
2636
|
...overrides,
|
|
1746
|
-
out: hookOut(input.cwd, input.config, input.schema)
|
|
2637
|
+
out: hookOut(input.cwd, input.config, input.schema, flag),
|
|
2638
|
+
schemas: schemas.mode
|
|
1747
2639
|
}, policy);
|
|
1748
2640
|
if (input.out !== void 0 || input.check) return manifestFile(input.cwd, typeof input.out === "string" ? input.out : MANIFEST_FILE, manifest, input.check);
|
|
1749
2641
|
return {
|
|
@@ -1755,8 +2647,12 @@ async function runSupabase(input) {
|
|
|
1755
2647
|
code: 2,
|
|
1756
2648
|
output: SUPABASE_HELP
|
|
1757
2649
|
};
|
|
2650
|
+
if (input.grantsOut !== void 0 && schemas.mode !== "migra") return {
|
|
2651
|
+
code: 2,
|
|
2652
|
+
output: `supabase hook generate --grants-out is for --schemas migra: ${schemas.mode === "pg-delta" ? "pg-delta keeps the grants in the hook file" : "a hand-written migration keeps its grants"}, so drop --grants-out`
|
|
2653
|
+
};
|
|
1758
2654
|
const { sql, grants, manifest } = supabaseHookSql(await loadScopes(input.cwd, input.config), input.config, overrides, grantsLabel(input.grantsOut));
|
|
1759
|
-
const outRel = typeof input.out === "string" ? input.out : hookOut(input.cwd, input.config, input.schema);
|
|
2655
|
+
const outRel = typeof input.out === "string" ? input.out : hookOut(input.cwd, input.config, input.schema, flag);
|
|
1760
2656
|
const outPath = resolve(input.cwd, outRel);
|
|
1761
2657
|
const hook = input.config.supabase?.hook;
|
|
1762
2658
|
const grantsFile = input.grantsOut === void 0 ? [] : [{
|
|
@@ -1806,4 +2702,4 @@ async function runSupabase(input) {
|
|
|
1806
2702
|
};
|
|
1807
2703
|
}
|
|
1808
2704
|
//#endregion
|
|
1809
|
-
export { supabaseHookManifest as a,
|
|
2705
|
+
export { pd045 as _, supabaseHookManifest as a, approvalsSql as c, resolveAuthorize as d, pd039 as f, attrsPlan as g, schemasRefusal as h, runSupabase as i, parseRbacAuthorize as l, resolveSchemas as m, hookOut as n, supabaseHookSql as o, isSupabaseSchemas as p, loadScopes as r, permissionHoldersSql as s, grantsLabel as t, rbacScaffold as u, readSupabaseConfig as v, supabaseConfig as y };
|