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,27 +1,32 @@
|
|
|
1
1
|
import { t as compact } from "./compact-CAf557ZB.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { c as listPermissions, u as renamedKeys } from "./permission-registry-Br4jSNVi.js";
|
|
3
|
+
import { i as dateAt } from "./time-DkWjdL98.js";
|
|
4
|
+
import { d as scopeList, o as resolveScope } from "./scopes-CUPSjo9q.js";
|
|
5
|
+
import { o as flattenGrantee, v as listRoles } from "./grantee-hxrUsbpM.js";
|
|
6
|
+
import { p as scopeField } from "./tenancy-DN2QAx9z.js";
|
|
7
|
+
import { a as describeError, i as configError, o as exitCodeOfError, u as usageResult } from "./errors-CWTY6uih.js";
|
|
7
8
|
import { t as policyRowConditionKeys } from "./row-conditions-h2HythXQ.js";
|
|
8
|
-
import { c as requirePolicy } from "./load-
|
|
9
|
+
import { c as requirePolicy } from "./load-DFS_ry0W.js";
|
|
9
10
|
import "./budget-NbEtbJC_.js";
|
|
10
|
-
import {
|
|
11
|
-
import { l as roleColumn } from "./sources-
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
11
|
+
import { i as qualifySqlTable, r as escapeSqlIdent, u as sqlTextArray } from "./sql-B5T1IL83.js";
|
|
12
|
+
import { l as roleColumn } from "./sources-vs5dGOsS.js";
|
|
13
|
+
import { t as policyFromCatalog } from "./policy-from-catalog-C2gMa2Dj.js";
|
|
14
|
+
import { t as beforeFirst } from "./text-BMFRfBqc.js";
|
|
15
|
+
import { i as INDEXES_MARKER, n as GRANTS_MARKER, o as SEEDS_MARKER, t as BACKFILL_MARKER } from "./markers-C6joDI3o.js";
|
|
16
|
+
import { i as requirePeer, n as packageManagerOf, t as addLine } from "./peer-C8swJ2GH.js";
|
|
17
|
+
import { t as POLICY_NOT_PUBLISHED } from "./problem-types-CJ2325SZ.js";
|
|
18
|
+
import { t as fetchProjectPolicy } from "./pull-jEwbf80J.js";
|
|
19
|
+
import { a as isSeedsDirectory, c as parseSplit, d as seedsMigration, f as writeOutputFiles, l as partPath, n as dataMigrationFile, r as driftOf, u as pgDeltaPath } from "./output-files-DhRBBE4n.js";
|
|
20
|
+
import { c as approvalsSql, d as resolveAuthorize, h as schemasRefusal, l as parseRbacAuthorize, m as resolveSchemas, o as supabaseHookSql, p as isSupabaseSchemas, s as permissionHoldersSql, t as grantsLabel, u as rbacScaffold } from "./supabase-hook-BMoEhZir.js";
|
|
21
|
+
import { C as parseMembershipsFlag, E as scopeTypeOf, F as quoteQualified, I as quoteTable, M as qualified, N as quoteIdent$1, O as tenantTypeOf, P as quoteLiteral, _ as subjectClaimJsonSql, a as customRolesOn, c as SESSION_LIVE_HELPER, g as sessionLiveHelperSql, r as HELPERS, t as CUSTOM_ROLES, y as subjectIdSql } from "./shared-BAK8t4Uo.js";
|
|
17
22
|
import { a as splitUndiffed } from "./deciding-columns-Sq12mEA6.js";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
import { _ as
|
|
21
|
-
import { a as
|
|
23
|
+
import { a as readOnlyActorKinds, c as branchClauses, d as connectPgPool, i as orSql, n as actorMayWriteSql, o as readOnlyActorPolicies, r as assemblePolicies, t as ACTOR_MAY_WRITE } from "./policies-DP2nqRoF.js";
|
|
24
|
+
import { A as collectGrants, C as byKey, D as apiKeysPlan, G as memberIdsHelper, I as graphHelper, J as permittedIdsHelper, L as graphSqlName, O as breakGlassHolder, S as byCodeUnit, X as scopeSources, Z as scopeTable, b as currentKeyExpr, et as checkSuspension, f as permittedByPermissionHelper, g as seedSql, h as helpersSql, i as assignmentSql, j as roleNames, k as breakGlassKey, m as accessSql, n as ownershipSql, t as ownershipRules, u as permissionHelpersSql, x as rolesTableScope } from "./ownership-DqaJNJPn.js";
|
|
25
|
+
import { _ as indexesSql, a as graphSql, c as callsHelper, g as indexTargets, h as COLUMNS_SQL, i as graphPlan, l as helperCallKeys, m as tableFor, n as runRlsVerify, o as HELPER_TABLES, p as compileGrants, r as closureDepths, v as pruneIndexTargets, y as readTableIndexFacts } from "./verify-BaAKx7t6.js";
|
|
26
|
+
import { a as fieldViewsSql, i as fieldViews, n as columnGrantSql, o as rowBranches, r as columnRevokeSql } from "./fields-OYG_JR78.js";
|
|
22
27
|
import { t as supautilsRejections } from "./supautils-B6HYL9eL.js";
|
|
23
28
|
import { basename, delimiter, join } from "node:path";
|
|
24
|
-
import {
|
|
29
|
+
import { spawn } from "node:child_process";
|
|
25
30
|
//#region src/cli/rls/advisors.ts
|
|
26
31
|
/** The PermDock doctor check that reports the same problem in migrations. */
|
|
27
32
|
const ADVISOR_CODES = {
|
|
@@ -32,19 +37,41 @@ const ADVISOR_CODES = {
|
|
|
32
37
|
anon_security_definer_function_executable: "PD049",
|
|
33
38
|
authenticated_security_definer_function_executable: "PD046"
|
|
34
39
|
};
|
|
35
|
-
const defaultExec = (command, args, options) => {
|
|
36
|
-
const
|
|
40
|
+
const defaultExec = (command, args, options) => new Promise((resolve) => {
|
|
41
|
+
const child = spawn(command, [...args], {
|
|
37
42
|
cwd: options.cwd,
|
|
38
43
|
env: options.env,
|
|
39
|
-
|
|
44
|
+
stdio: [
|
|
45
|
+
"ignore",
|
|
46
|
+
"pipe",
|
|
47
|
+
"pipe"
|
|
48
|
+
],
|
|
49
|
+
...options.signal === void 0 ? {} : { signal: options.signal }
|
|
40
50
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
let stdout = "";
|
|
52
|
+
let stderr = "";
|
|
53
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => {
|
|
54
|
+
stdout += chunk;
|
|
55
|
+
});
|
|
56
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
57
|
+
stderr += chunk;
|
|
58
|
+
});
|
|
59
|
+
child.once("error", (error) => {
|
|
60
|
+
resolve({
|
|
61
|
+
status: null,
|
|
62
|
+
stdout,
|
|
63
|
+
stderr,
|
|
64
|
+
error
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
child.once("close", (status) => {
|
|
68
|
+
resolve({
|
|
69
|
+
status,
|
|
70
|
+
stdout,
|
|
71
|
+
stderr
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|
|
48
75
|
function text(value, key) {
|
|
49
76
|
const field = typeof value === "object" && value !== null ? Reflect.get(value, key) : void 0;
|
|
50
77
|
return typeof field === "string" ? field : "";
|
|
@@ -65,16 +92,19 @@ function findingsOf(value) {
|
|
|
65
92
|
return code === void 0 ? finding : Object.assign(finding, { code });
|
|
66
93
|
});
|
|
67
94
|
}
|
|
68
|
-
|
|
95
|
+
function installHint(agent) {
|
|
96
|
+
return `rls verify --advisors runs the Supabase CLI: install it with ${addLine(agent, ["supabase"], { dev: true })} (or brew install supabase/tap/supabase)`;
|
|
97
|
+
}
|
|
69
98
|
/**
|
|
70
|
-
* `supabase db advisors --type security` against `--db` or the local stack
|
|
71
|
-
*
|
|
99
|
+
* `supabase db advisors --type security` against `--db-url` or the local stack,
|
|
100
|
+
* from the project's `node_modules/.bin` first, then `PATH`. Exit 1 on a WARN
|
|
101
|
+
* or ERROR row, 2 when the CLI is missing or fails.
|
|
72
102
|
*/
|
|
73
|
-
function runRlsAdvisors(input) {
|
|
103
|
+
async function runRlsAdvisors(input) {
|
|
74
104
|
const exec = input.exec ?? defaultExec;
|
|
75
105
|
const bin = join(input.cwd, "node_modules", ".bin");
|
|
76
106
|
const path = input.env["PATH"];
|
|
77
|
-
const result = exec("supabase", [
|
|
107
|
+
const result = await exec("supabase", [
|
|
78
108
|
"db",
|
|
79
109
|
"advisors",
|
|
80
110
|
...input.db === void 0 ? ["--local"] : ["--db-url", input.db],
|
|
@@ -87,11 +117,12 @@ function runRlsAdvisors(input) {
|
|
|
87
117
|
env: {
|
|
88
118
|
...input.env,
|
|
89
119
|
PATH: path === void 0 ? bin : `${bin}${delimiter}${path}`
|
|
90
|
-
}
|
|
120
|
+
},
|
|
121
|
+
...input.signal === void 0 ? {} : { signal: input.signal }
|
|
91
122
|
});
|
|
92
123
|
if (result.error !== void 0) return {
|
|
93
124
|
code: 2,
|
|
94
|
-
output:
|
|
125
|
+
output: installHint(await packageManagerOf(input.cwd))
|
|
95
126
|
};
|
|
96
127
|
let parsed;
|
|
97
128
|
try {
|
|
@@ -129,6 +160,11 @@ function runRlsAdvisors(input) {
|
|
|
129
160
|
}
|
|
130
161
|
//#endregion
|
|
131
162
|
//#region src/cli/rls/generate-context.ts
|
|
163
|
+
/** The scopes an `rls.migrate` `role` helper names, so `rls generate` writes their role-filtered member helper. */
|
|
164
|
+
function roleMemberScopesOf(migrate) {
|
|
165
|
+
const scopes = [...new Set(Object.values(migrate?.helpers ?? {}).flatMap((helper) => helper.form === "role" ? [helper.scope] : []))].toSorted();
|
|
166
|
+
return scopes.length === 0 ? {} : { roleMemberScopes: scopes };
|
|
167
|
+
}
|
|
132
168
|
/** `rls.customRoleWrites.requires` as permission keys, or why it names none. */
|
|
133
169
|
function writeRequirement(policy, requires) {
|
|
134
170
|
if (requires === void 0) return {};
|
|
@@ -161,7 +197,7 @@ function skippedOwnershipTriggers(triggers, scopes) {
|
|
|
161
197
|
if (triggers === false) return "all";
|
|
162
198
|
return Object.keys(triggers).map((key) => {
|
|
163
199
|
const name = resolveScope(scopes, key);
|
|
164
|
-
if (name === void 0) throw
|
|
200
|
+
if (name === void 0) throw configError(`PermDock CLI: rls.ownershipTriggers.${key} names a scope the policy does not declare`);
|
|
165
201
|
return name;
|
|
166
202
|
});
|
|
167
203
|
}
|
|
@@ -195,6 +231,8 @@ function buildContext(input, policy, options) {
|
|
|
195
231
|
dialect: input.dialect,
|
|
196
232
|
scopes,
|
|
197
233
|
tenantClaim: rls?.tenantClaim ?? "tenant_id",
|
|
234
|
+
...roleMemberScopesOf(rls?.migrate),
|
|
235
|
+
...rls?.roleValues === void 0 ? {} : { roleValues: rls.roleValues },
|
|
198
236
|
...rls?.tenants === "all" ? { tenants: "all" } : {},
|
|
199
237
|
gucPrefix: input.gucPrefix ?? rls?.gucPrefix ?? "app",
|
|
200
238
|
...rls?.actions === void 0 ? {} : { actions: rls.actions },
|
|
@@ -272,7 +310,30 @@ function moveUndiffed(sql, label, part) {
|
|
|
272
310
|
grants: `-- the privileges and view options supabase db diff drops from the ${part} part\n${moved.join("\n")}`
|
|
273
311
|
};
|
|
274
312
|
}
|
|
275
|
-
/**
|
|
313
|
+
/** Why `--split` cannot be written for `mode`, or `undefined`. */
|
|
314
|
+
function splitRefusal(mode, plan) {
|
|
315
|
+
const seedsWithoutOut = plan.split.includes("seeds") && plan.seedsOut === void 0;
|
|
316
|
+
const helpersWithoutSeeds = plan.split.includes("helpers") && !plan.split.includes("seeds");
|
|
317
|
+
switch (mode) {
|
|
318
|
+
case "pg-delta":
|
|
319
|
+
if (plan.explicitOut && !plan.placeholder) return `rls generate --split needs {part} in --out, or no --out for pg-delta's per-schema layout (got ${plan.outRel})`;
|
|
320
|
+
if (plan.grantsOut !== void 0) return "rls generate --grants-out is for --schemas migra: pg-delta keeps the grants in each part, so drop --grants-out";
|
|
321
|
+
if (helpersWithoutSeeds) return "rls generate --split helpers under pg-delta needs the seeds part with --seeds-out: pg-delta rejects the role_permissions rows in a declarative file";
|
|
322
|
+
if (seedsWithoutOut) return "rls generate --split seeds under pg-delta needs --seeds-out: pg-delta does not diff the role_permissions rows, so they go in a migration";
|
|
323
|
+
return;
|
|
324
|
+
case "migra":
|
|
325
|
+
if (!plan.placeholder) return `rls generate --split under migra needs {part} in --out, for example supabase/schemas/identity/056_permdock_{part}.sql (got ${plan.outRel})`;
|
|
326
|
+
if (helpersWithoutSeeds) return "rls generate --split helpers under migra needs the seeds part with --seeds-out: supabase db diff does not carry the role_permissions rows from a schema file";
|
|
327
|
+
if (seedsWithoutOut) return "rls generate --split seeds under migra needs --seeds-out: supabase db diff does not carry the role_permissions rows, so they go in a migration";
|
|
328
|
+
return;
|
|
329
|
+
case "migrations":
|
|
330
|
+
if (!plan.placeholder) return `rls generate --split needs {part} in --out, for example supabase/migrations/20260101000000_permdock_{part}.sql (got ${plan.outRel})`;
|
|
331
|
+
if (plan.grantsOut !== void 0) return "rls generate --grants-out is for --schemas migra: a hand-written migration keeps its grants, so drop --grants-out";
|
|
332
|
+
return;
|
|
333
|
+
default: return mode;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/** The files `generate` writes: one, or one per `--split` part, plus the hook's grants under migra. */
|
|
276
337
|
function outputFiles(plan) {
|
|
277
338
|
const { input } = plan;
|
|
278
339
|
const split = parseSplit(input.split);
|
|
@@ -288,17 +349,25 @@ function outputFiles(plan) {
|
|
|
288
349
|
}];
|
|
289
350
|
}
|
|
290
351
|
if (input.target !== "sql") return "rls generate --split needs --target sql";
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
352
|
+
const schemas = resolveSchemas(input.cwd, input.config, input.schemas);
|
|
353
|
+
const pgDelta = schemas.mode === "pg-delta" ? schemas : void 0;
|
|
354
|
+
const placeholder = plan.outRel.includes("{part}");
|
|
355
|
+
const refused = splitRefusal(schemas.mode, {
|
|
356
|
+
split,
|
|
357
|
+
placeholder,
|
|
358
|
+
explicitOut: input.out !== void 0,
|
|
359
|
+
outRel: plan.outRel,
|
|
360
|
+
grantsOut: input.grantsOut,
|
|
361
|
+
seedsOut: input.seedsOut
|
|
362
|
+
});
|
|
363
|
+
if (refused !== void 0) return refused;
|
|
364
|
+
const at = (part, schema = plan.schema) => pgDelta === void 0 || placeholder ? partPath(plan.outRel, part) : pgDeltaPath(pgDelta.schemaDir, part, schema);
|
|
296
365
|
if (input.grantsOut !== void 0 && !split.includes("hook") && !split.includes("helpers")) return "rls generate --grants-out needs the helpers or hook part in --split";
|
|
297
366
|
if (input.seedsOut !== void 0 && !split.includes("seeds")) return "rls generate --seeds-out needs the seeds part in --split";
|
|
298
367
|
if (split.includes("hook") && input.config.supabase?.hook === void 0) return "rls generate --split hook needs supabase.hook in permdock.config.ts";
|
|
299
368
|
const sql = plan.sql();
|
|
300
369
|
const relocated = pgDelta === void 0 ? void 0 : sql.servicePolicies;
|
|
301
|
-
if (relocated !== void 0 && split.includes("policies") && !split.includes("seeds")) return "rls generate --split policies under pg-delta with rls.realtime or rls.storage needs the seeds part with --seeds-out: the shadow database has no realtime.messages or storage.objects, so their policies go in that migration";
|
|
370
|
+
if (relocated !== void 0 && split.includes("policies") && !split.includes("seeds")) return "rls generate --split policies under pg-delta with rls.realtime or rls.storage needs the seeds part with --seeds-out: the declarative sync shadow database has no realtime.messages or storage.objects, so their policies go in that migration";
|
|
302
371
|
const seeds = relocated === void 0 || !split.includes("policies") ? sql.seeds : `${sql.seeds}\n${relocated}`;
|
|
303
372
|
const files = [];
|
|
304
373
|
const label = grantsLabel(input.grantsOut);
|
|
@@ -326,7 +395,7 @@ function outputFiles(plan) {
|
|
|
326
395
|
case "seeds": {
|
|
327
396
|
const out = input.seedsOut;
|
|
328
397
|
if (out !== void 0 && isSeedsDirectory(input.cwd, out)) {
|
|
329
|
-
const planned = seedsMigration(input.cwd, out, SEEDS_MARKER, seeds, input.io.now?.() ??
|
|
398
|
+
const planned = seedsMigration(input.cwd, out, SEEDS_MARKER, seeds, input.io.now?.() ?? dateAt());
|
|
330
399
|
if (!planned.current || input.check) files.push({
|
|
331
400
|
part,
|
|
332
401
|
rel: planned.rel,
|
|
@@ -377,574 +446,6 @@ function outputFiles(plan) {
|
|
|
377
446
|
return files;
|
|
378
447
|
}
|
|
379
448
|
//#endregion
|
|
380
|
-
//#region src/cli/approval-schema.ts
|
|
381
|
-
/** An RFC 3339 instant as `Temporal.Instant` prints it; pg_jsonschema asserts no `format`. */
|
|
382
|
-
const INSTANT = String.raw`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$`;
|
|
383
|
-
const STRINGS = {
|
|
384
|
-
type: "array",
|
|
385
|
-
items: { type: "string" }
|
|
386
|
-
};
|
|
387
|
-
/**
|
|
388
|
-
* `schemas/approval-request-v1.json`: the `ApprovalRequest` an approval store
|
|
389
|
-
* holds. Self-contained (`$defs`, no remote `$ref`) so `rls.jsonSchema` can
|
|
390
|
-
* inline it into a pg_jsonschema check constraint.
|
|
391
|
-
*/
|
|
392
|
-
const APPROVAL_REQUEST_SCHEMA = {
|
|
393
|
-
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
394
|
-
$id: "https://permdock.com/schemas/approval-request-v1.json",
|
|
395
|
-
title: "PermDock approval request",
|
|
396
|
-
description: "An ApprovalRequest as an ApprovalStore holds it, wire format v1. Unknown properties are allowed so a v1 minor release can add one.",
|
|
397
|
-
type: "object",
|
|
398
|
-
required: [
|
|
399
|
-
"v",
|
|
400
|
-
"token",
|
|
401
|
-
"permission",
|
|
402
|
-
"scope",
|
|
403
|
-
"resource",
|
|
404
|
-
"subject",
|
|
405
|
-
"detail",
|
|
406
|
-
"createdAt",
|
|
407
|
-
"expiresAt",
|
|
408
|
-
"status"
|
|
409
|
-
],
|
|
410
|
-
properties: {
|
|
411
|
-
v: { const: 1 },
|
|
412
|
-
token: {
|
|
413
|
-
type: "string",
|
|
414
|
-
minLength: 1
|
|
415
|
-
},
|
|
416
|
-
permission: {
|
|
417
|
-
type: "string",
|
|
418
|
-
minLength: 1
|
|
419
|
-
},
|
|
420
|
-
scope: { type: "string" },
|
|
421
|
-
resource: {
|
|
422
|
-
type: "object",
|
|
423
|
-
required: ["type"],
|
|
424
|
-
properties: {
|
|
425
|
-
type: { type: "string" },
|
|
426
|
-
id: { type: "string" }
|
|
427
|
-
}
|
|
428
|
-
},
|
|
429
|
-
subject: { $ref: "#/$defs/subject" },
|
|
430
|
-
membership: {
|
|
431
|
-
type: "object",
|
|
432
|
-
required: ["roles"],
|
|
433
|
-
properties: { roles: STRINGS }
|
|
434
|
-
},
|
|
435
|
-
approvers: { type: "object" },
|
|
436
|
-
detail: { type: "string" },
|
|
437
|
-
adapter: { type: "string" },
|
|
438
|
-
createdAt: { $ref: "#/$defs/instant" },
|
|
439
|
-
expiresAt: { $ref: "#/$defs/instant" },
|
|
440
|
-
status: { enum: [
|
|
441
|
-
"pending",
|
|
442
|
-
"approved",
|
|
443
|
-
"rejected",
|
|
444
|
-
"expired"
|
|
445
|
-
] },
|
|
446
|
-
approvals: {
|
|
447
|
-
type: "array",
|
|
448
|
-
items: { $ref: "#/$defs/signature" }
|
|
449
|
-
},
|
|
450
|
-
resolvedAt: { $ref: "#/$defs/instant" },
|
|
451
|
-
resolvedBy: { type: "string" },
|
|
452
|
-
vouched: { type: "string" },
|
|
453
|
-
note: { type: "string" },
|
|
454
|
-
consumedAt: { $ref: "#/$defs/instant" }
|
|
455
|
-
},
|
|
456
|
-
$defs: {
|
|
457
|
-
instant: {
|
|
458
|
-
type: "string",
|
|
459
|
-
pattern: INSTANT
|
|
460
|
-
},
|
|
461
|
-
subject: {
|
|
462
|
-
type: "object",
|
|
463
|
-
required: ["principal"],
|
|
464
|
-
properties: {
|
|
465
|
-
principal: { anyOf: [{ type: "null" }, {
|
|
466
|
-
type: "object",
|
|
467
|
-
required: ["id", "roles"],
|
|
468
|
-
properties: {
|
|
469
|
-
id: { type: "string" },
|
|
470
|
-
roles: STRINGS,
|
|
471
|
-
tenant: { type: "string" }
|
|
472
|
-
}
|
|
473
|
-
}] },
|
|
474
|
-
actor: {
|
|
475
|
-
type: "object",
|
|
476
|
-
required: ["id", "kind"],
|
|
477
|
-
properties: {
|
|
478
|
-
id: { type: "string" },
|
|
479
|
-
kind: { type: "string" }
|
|
480
|
-
}
|
|
481
|
-
},
|
|
482
|
-
session: { type: "string" },
|
|
483
|
-
delegation: { type: "object" }
|
|
484
|
-
}
|
|
485
|
-
},
|
|
486
|
-
signature: {
|
|
487
|
-
type: "object",
|
|
488
|
-
required: ["by", "at"],
|
|
489
|
-
properties: {
|
|
490
|
-
by: { type: "string" },
|
|
491
|
-
at: { $ref: "#/$defs/instant" },
|
|
492
|
-
stage: {
|
|
493
|
-
type: "integer",
|
|
494
|
-
minimum: 0
|
|
495
|
-
},
|
|
496
|
-
vouched: { type: "string" }
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
};
|
|
501
|
-
//#endregion
|
|
502
|
-
//#region src/cli/rls/approvals.ts
|
|
503
|
-
/** The objects `rls.approvals` adds. Names are part of the SQL contract `supabaseApprovalStore` calls. */
|
|
504
|
-
const APPROVAL_STORE = {
|
|
505
|
-
table: "approval_requests",
|
|
506
|
-
open: "permdock_approval_open",
|
|
507
|
-
get: "permdock_approval_get",
|
|
508
|
-
resolve: "permdock_approval_resolve",
|
|
509
|
-
consume: "permdock_approval_consume",
|
|
510
|
-
list: "permdock_approval_list",
|
|
511
|
-
expire: "permdock_approval_expire",
|
|
512
|
-
cancel: "permdock_approval_cancel"
|
|
513
|
-
};
|
|
514
|
-
/** `body` as the row's filter columns hold it. */
|
|
515
|
-
const COLUMNS = `p_request ->> 'token',
|
|
516
|
-
p_request,
|
|
517
|
-
p_request ->> 'status',
|
|
518
|
-
p_request #>> '{subject,principal,tenant}',
|
|
519
|
-
p_request #>> '{subject,principal,id}',
|
|
520
|
-
p_request #>> '{subject,actor,id}',
|
|
521
|
-
p_request #>> '{subject,session}',
|
|
522
|
-
coalesce(jsonb_array_length(p_request -> 'approvals'), 0),
|
|
523
|
-
p_request ->> 'createdAt',
|
|
524
|
-
(p_request ->> 'expiresAt')::timestamptz,
|
|
525
|
-
(p_request ->> 'consumedAt')::timestamptz`;
|
|
526
|
-
/** The `ApprovalListFilter` in `p_filter`, over the row aliased `a`. */
|
|
527
|
-
const FILTER = `(p_filter ->> 'status' is null or a.status = p_filter ->> 'status')
|
|
528
|
-
and (p_filter ->> 'tenant' is null or a.tenant = p_filter ->> 'tenant')
|
|
529
|
-
and (p_filter ->> 'principalId' is null or a.principal_id = p_filter ->> 'principalId')
|
|
530
|
-
and (p_filter ->> 'actorId' is null or a.actor_id = p_filter ->> 'actorId')
|
|
531
|
-
and (p_filter ->> 'session' is null or a.session = p_filter ->> 'session')`;
|
|
532
|
-
function serverOnly(fn, args) {
|
|
533
|
-
return `revoke execute on function ${fn}(${args}) from public, anon, authenticated;`;
|
|
534
|
-
}
|
|
535
|
-
const BODY_CHECK = `${APPROVAL_STORE.table}_body_schema`;
|
|
536
|
-
/**
|
|
537
|
-
* `rls.jsonSchema`: a pg_jsonschema check that `body` matches
|
|
538
|
-
* `approval-request-v1.json`, added `not valid` and then validated. `auto`
|
|
539
|
-
* adds it only where the extension is available.
|
|
540
|
-
*/
|
|
541
|
-
function bodySchemaSql(table, mode, body = "body", check = BODY_CHECK) {
|
|
542
|
-
const statements = [
|
|
543
|
-
"create schema if not exists extensions;",
|
|
544
|
-
"create extension if not exists pg_jsonschema with schema extensions;",
|
|
545
|
-
`alter table ${table} drop constraint if exists "${check}";`,
|
|
546
|
-
`alter table ${table} add constraint "${check}" check (extensions.jsonb_matches_schema(${quoteLiteral(JSON.stringify(APPROVAL_REQUEST_SCHEMA))}::json, ${body})) not valid;`,
|
|
547
|
-
`alter table ${table} validate constraint "${check}";`
|
|
548
|
-
];
|
|
549
|
-
const comment = "-- approval store: body matches schemas/approval-request-v1.json through pg_jsonschema";
|
|
550
|
-
if (mode === true) return `\n${comment}\n${statements.join("\n")}\n`;
|
|
551
|
-
return `\n${comment}, where the extension is available
|
|
552
|
-
do $permdock$
|
|
553
|
-
begin
|
|
554
|
-
if exists (select 1 from pg_available_extensions where name = 'pg_jsonschema') then
|
|
555
|
-
${statements.map((statement) => ` ${statement}`).join("\n")}
|
|
556
|
-
end if;
|
|
557
|
-
end
|
|
558
|
-
$permdock$;\n`;
|
|
559
|
-
}
|
|
560
|
-
/**
|
|
561
|
-
* The approval store (`rls.approvals`): one table of `ApprovalRequest`
|
|
562
|
-
* bodies with the columns a list filters on, and one `security definer`
|
|
563
|
-
* function per `ApprovalStore` method, each one atomic statement. No client
|
|
564
|
-
* role may read the table or execute the functions: approvals are opened and
|
|
565
|
-
* resolved by the server, which grants its own role.
|
|
566
|
-
*/
|
|
567
|
-
function approvalStoreSql(ctx, jsonSchema = false) {
|
|
568
|
-
const table = qualified(ctx, APPROVAL_STORE.table);
|
|
569
|
-
const fn = (name) => qualified(ctx, name);
|
|
570
|
-
const index = (name) => `"${APPROVAL_STORE.table}_${name}"`;
|
|
571
|
-
return `-- approval store: ApprovalRequest bodies, opened, resolved and consumed through the functions below; supabaseApprovalStore calls them
|
|
572
|
-
create table if not exists ${table} (
|
|
573
|
-
token text primary key,
|
|
574
|
-
body jsonb not null,
|
|
575
|
-
status text not null check (status in ('pending', 'approved', 'rejected', 'expired')),
|
|
576
|
-
tenant text,
|
|
577
|
-
principal_id text,
|
|
578
|
-
actor_id text,
|
|
579
|
-
session text,
|
|
580
|
-
approvals integer not null default 0,
|
|
581
|
-
created_at text not null,
|
|
582
|
-
expires_at timestamptz not null,
|
|
583
|
-
consumed_at timestamptz
|
|
584
|
-
);
|
|
585
|
-
create index if not exists ${index("page")} on ${table} (created_at collate "C", token collate "C");
|
|
586
|
-
create index if not exists ${index("pending")} on ${table} (status, expires_at);
|
|
587
|
-
alter table ${table} enable row level security;
|
|
588
|
-
revoke all on table ${table} from anon, authenticated, public;
|
|
589
|
-
${jsonSchema === false ? "" : bodySchemaSql(table, jsonSchema)}
|
|
590
|
-
-- open a request; a repeated ask keeps the open record and replaces only an expired one
|
|
591
|
-
create or replace function ${fn(APPROVAL_STORE.open)}(p_request jsonb)
|
|
592
|
-
returns void
|
|
593
|
-
language sql
|
|
594
|
-
volatile
|
|
595
|
-
security definer
|
|
596
|
-
set search_path = ''
|
|
597
|
-
as $$
|
|
598
|
-
insert into ${table} as a (token, body, status, tenant, principal_id, actor_id, session, approvals, created_at, expires_at, consumed_at)
|
|
599
|
-
values (
|
|
600
|
-
${COLUMNS}
|
|
601
|
-
)
|
|
602
|
-
on conflict (token) do update
|
|
603
|
-
set body = excluded.body, status = excluded.status, tenant = excluded.tenant,
|
|
604
|
-
principal_id = excluded.principal_id, actor_id = excluded.actor_id, session = excluded.session,
|
|
605
|
-
approvals = excluded.approvals, created_at = excluded.created_at,
|
|
606
|
-
expires_at = excluded.expires_at, consumed_at = excluded.consumed_at
|
|
607
|
-
where a.status = 'expired' or a.expires_at <= now()
|
|
608
|
-
$$;
|
|
609
|
-
${serverOnly(fn(APPROVAL_STORE.open), "jsonb")}
|
|
610
|
-
|
|
611
|
-
create or replace function ${fn(APPROVAL_STORE.get)}(p_token text)
|
|
612
|
-
returns jsonb
|
|
613
|
-
language sql
|
|
614
|
-
stable
|
|
615
|
-
security definer
|
|
616
|
-
set search_path = ''
|
|
617
|
-
as $$
|
|
618
|
-
select a.body from ${table} a where a.token = p_token
|
|
619
|
-
$$;
|
|
620
|
-
${serverOnly(fn(APPROVAL_STORE.get), "text")}
|
|
621
|
-
|
|
622
|
-
-- write a verdict over the record it was computed from: still pending, unexpired, with p_seen approvals
|
|
623
|
-
create or replace function ${fn(APPROVAL_STORE.resolve)}(p_token text, p_next jsonb, p_seen integer)
|
|
624
|
-
returns jsonb
|
|
625
|
-
language sql
|
|
626
|
-
volatile
|
|
627
|
-
security definer
|
|
628
|
-
set search_path = ''
|
|
629
|
-
as $$
|
|
630
|
-
update ${table} a
|
|
631
|
-
set body = p_next,
|
|
632
|
-
status = p_next ->> 'status',
|
|
633
|
-
approvals = coalesce(jsonb_array_length(p_next -> 'approvals'), 0)
|
|
634
|
-
where a.token = p_token
|
|
635
|
-
and a.status = 'pending'
|
|
636
|
-
and a.expires_at > now()
|
|
637
|
-
and a.approvals = p_seen
|
|
638
|
-
returning a.body
|
|
639
|
-
$$;
|
|
640
|
-
${serverOnly(fn(APPROVAL_STORE.resolve), "text, jsonb, integer")}
|
|
641
|
-
|
|
642
|
-
-- consume an approved, unexpired, unconsumed request once
|
|
643
|
-
create or replace function ${fn(APPROVAL_STORE.consume)}(p_token text, p_now text)
|
|
644
|
-
returns jsonb
|
|
645
|
-
language sql
|
|
646
|
-
volatile
|
|
647
|
-
security definer
|
|
648
|
-
set search_path = ''
|
|
649
|
-
as $$
|
|
650
|
-
update ${table} a
|
|
651
|
-
set consumed_at = p_now::timestamptz,
|
|
652
|
-
body = a.body || jsonb_build_object('consumedAt', p_now)
|
|
653
|
-
where a.token = p_token
|
|
654
|
-
and a.status = 'approved'
|
|
655
|
-
and a.consumed_at is null
|
|
656
|
-
and a.expires_at > p_now::timestamptz
|
|
657
|
-
returning a.body
|
|
658
|
-
$$;
|
|
659
|
-
${serverOnly(fn(APPROVAL_STORE.consume), "text, text")}
|
|
660
|
-
|
|
661
|
-
-- one page in (createdAt, token) order after the cursor position
|
|
662
|
-
create or replace function ${fn(APPROVAL_STORE.list)}(p_filter jsonb, p_after_created text, p_after_token text, p_limit integer)
|
|
663
|
-
returns setof jsonb
|
|
664
|
-
language sql
|
|
665
|
-
stable
|
|
666
|
-
security definer
|
|
667
|
-
set search_path = ''
|
|
668
|
-
as $$
|
|
669
|
-
select a.body
|
|
670
|
-
from ${table} a
|
|
671
|
-
where ${FILTER}
|
|
672
|
-
and (p_after_created is null
|
|
673
|
-
or (a.created_at collate "C", a.token collate "C") > (p_after_created collate "C", p_after_token collate "C"))
|
|
674
|
-
order by a.created_at collate "C", a.token collate "C"
|
|
675
|
-
limit p_limit
|
|
676
|
-
$$;
|
|
677
|
-
${serverOnly(fn(APPROVAL_STORE.list), "jsonb, text, text, integer")}
|
|
678
|
-
|
|
679
|
-
create or replace function ${fn(APPROVAL_STORE.expire)}(p_now text)
|
|
680
|
-
returns integer
|
|
681
|
-
language sql
|
|
682
|
-
volatile
|
|
683
|
-
security definer
|
|
684
|
-
set search_path = ''
|
|
685
|
-
as $$
|
|
686
|
-
with expired as (
|
|
687
|
-
update ${table} a
|
|
688
|
-
set status = 'expired', body = a.body || '{"status":"expired"}'::jsonb
|
|
689
|
-
where a.status = 'pending' and a.expires_at <= p_now::timestamptz
|
|
690
|
-
returning 1
|
|
691
|
-
)
|
|
692
|
-
select count(*)::integer from expired
|
|
693
|
-
$$;
|
|
694
|
-
${serverOnly(fn(APPROVAL_STORE.expire), "text")}
|
|
695
|
-
|
|
696
|
-
-- reject every pending request the filter matches, as system:<by>
|
|
697
|
-
create or replace function ${fn(APPROVAL_STORE.cancel)}(p_filter jsonb, p_by text, p_note text, p_now text)
|
|
698
|
-
returns integer
|
|
699
|
-
language sql
|
|
700
|
-
volatile
|
|
701
|
-
security definer
|
|
702
|
-
set search_path = ''
|
|
703
|
-
as $$
|
|
704
|
-
with cancelled as (
|
|
705
|
-
update ${table} a
|
|
706
|
-
set status = 'rejected',
|
|
707
|
-
body = (a.body - 'note') || jsonb_strip_nulls(jsonb_build_object(
|
|
708
|
-
'status', 'rejected', 'resolvedAt', p_now, 'resolvedBy', 'system:' || p_by, 'note', p_note
|
|
709
|
-
))
|
|
710
|
-
where a.status = 'pending'
|
|
711
|
-
and ${FILTER.replaceAll("\n ", "\n ")}
|
|
712
|
-
returning 1
|
|
713
|
-
)
|
|
714
|
-
select count(*)::integer from cancelled
|
|
715
|
-
$$;
|
|
716
|
-
${serverOnly(fn(APPROVAL_STORE.cancel), "jsonb, text, text, text")}
|
|
717
|
-
`;
|
|
718
|
-
}
|
|
719
|
-
/** The request fields an adopted table may mirror into its own columns, as SQL over a body. */
|
|
720
|
-
const MIRRORS = {
|
|
721
|
-
status: (body) => `${body} ->> 'status'`,
|
|
722
|
-
permission: (body) => `${body} ->> 'permission'`,
|
|
723
|
-
tenant: (body) => `${body} #>> '{subject,principal,tenant}'`,
|
|
724
|
-
principalId: (body) => `${body} #>> '{subject,principal,id}'`,
|
|
725
|
-
actorId: (body) => `${body} #>> '{subject,actor,id}'`,
|
|
726
|
-
session: (body) => `${body} #>> '{subject,session}'`,
|
|
727
|
-
approvals: (body) => `coalesce(jsonb_array_length(${body} -> 'approvals'), 0)`,
|
|
728
|
-
createdAt: (body) => `${body} ->> 'createdAt'`,
|
|
729
|
-
expiresAt: (body) => `${body} ->> 'expiresAt'`,
|
|
730
|
-
resolvedAt: (body) => `${body} ->> 'resolvedAt'`,
|
|
731
|
-
resolvedBy: (body) => `${body} ->> 'resolvedBy'`,
|
|
732
|
-
consumedAt: (body) => `${body} ->> 'consumedAt'`
|
|
733
|
-
};
|
|
734
|
-
function mirrorsOf(adopt) {
|
|
735
|
-
const out = [];
|
|
736
|
-
for (const [field, column] of Object.entries(adopt.mirror ?? {})) {
|
|
737
|
-
if (!Object.hasOwn(MIRRORS, field)) throw new Error(`PermDock CLI: rls.approvals.mirror.${field} is not a request field; use ${Object.keys(MIRRORS).join(", ")}`);
|
|
738
|
-
if (typeof column !== "string") throw new TypeError(`PermDock CLI: rls.approvals.mirror.${field} must be a column name`);
|
|
739
|
-
out.push([column, field]);
|
|
740
|
-
}
|
|
741
|
-
return out;
|
|
742
|
-
}
|
|
743
|
-
/** One row of `table` from `values` (column, SQL), each converted to the column's own type. */
|
|
744
|
-
function populate(table, values) {
|
|
745
|
-
return `pg_catalog.jsonb_populate_record(null::${table}, pg_catalog.jsonb_build_object(${values.map(([column, sql]) => `${quoteLiteral(column)}, ${sql}`).join(", ")}))`;
|
|
746
|
-
}
|
|
747
|
-
/** The mirrored columns' values for the request `body`. */
|
|
748
|
-
function mirrorRow(table, mirror, body) {
|
|
749
|
-
return populate(table, mirror.map(([column, field]) => [column, MIRRORS[field](body)]));
|
|
750
|
-
}
|
|
751
|
-
/** `, (<mirrored columns>) = (select …)` for an update that writes `body`; empty without mirrors. */
|
|
752
|
-
function mirrorSet(table, mirror, body) {
|
|
753
|
-
if (mirror.length === 0) return "";
|
|
754
|
-
const columns = mirror.map(([column]) => quoteIdent$1(column));
|
|
755
|
-
const picked = columns.map((column) => `r.${column}`).join(", ");
|
|
756
|
-
return `,\n ${columns.length === 1 ? columns[0] ?? "" : `(${columns.join(", ")})`} = (select ${picked} from ${mirrorRow(table, mirror, body)} r)`;
|
|
757
|
-
}
|
|
758
|
-
/**
|
|
759
|
-
* The approval store over a table the app already has (`rls.approvals:
|
|
760
|
-
* { table, token?, body?, mirror? }`): the same `permdock_approval_*`
|
|
761
|
-
* functions, reading every field from the request body in `body`, keyed by
|
|
762
|
-
* the text column `token`, and copying the fields `mirror` names into the
|
|
763
|
-
* app's own columns, converted to their types, on every write. The table's
|
|
764
|
-
* grants and policies stay the app's.
|
|
765
|
-
*/
|
|
766
|
-
function adoptedApprovalStoreSql(ctx, adopt, jsonSchema = false) {
|
|
767
|
-
const table = quoteQualified(adopt.table);
|
|
768
|
-
const base = adopt.table.split(".").at(-1) ?? adopt.table;
|
|
769
|
-
const token = quoteIdent$1(adopt.token ?? "token");
|
|
770
|
-
const body = quoteIdent$1(adopt.body ?? "body");
|
|
771
|
-
const mirror = mirrorsOf(adopt);
|
|
772
|
-
for (const [column] of mirror) if (column === (adopt.token ?? "token") || column === (adopt.body ?? "body")) throw new Error(`PermDock CLI: rls.approvals.mirror names the ${column} column, which already holds the token or the body`);
|
|
773
|
-
const open = adopt.open ?? "insert";
|
|
774
|
-
if (open !== "insert" && open !== "attach") throw new Error(`PermDock CLI: rls.approvals.open must be 'insert' or 'attach', got '${String(open)}'`);
|
|
775
|
-
const schema = adopt.schema;
|
|
776
|
-
if (schema !== void 0 && (typeof schema !== "string" || schema === "")) throw new Error("PermDock CLI: rls.approvals.schema must be a schema name");
|
|
777
|
-
const fn = (name) => schema === void 0 ? qualified(ctx, name) : `${quoteIdent$1(schema)}.${name}`;
|
|
778
|
-
const index = (name) => quoteIdent$1(`${base}_permdock_${name}`);
|
|
779
|
-
const status = `a.${body} ->> 'status'`;
|
|
780
|
-
const expires = `(a.${body} ->> 'expiresAt')::timestamptz`;
|
|
781
|
-
const created = `(a.${body} ->> 'createdAt')`;
|
|
782
|
-
const filter = `(p_filter ->> 'status' is null or ${status} = p_filter ->> 'status')
|
|
783
|
-
and (p_filter ->> 'tenant' is null or a.${body} #>> '{subject,principal,tenant}' = p_filter ->> 'tenant')
|
|
784
|
-
and (p_filter ->> 'principalId' is null or a.${body} #>> '{subject,principal,id}' = p_filter ->> 'principalId')
|
|
785
|
-
and (p_filter ->> 'actorId' is null or a.${body} #>> '{subject,actor,id}' = p_filter ->> 'actorId')
|
|
786
|
-
and (p_filter ->> 'session' is null or a.${body} #>> '{subject,session}' = p_filter ->> 'session')`;
|
|
787
|
-
const columns = [
|
|
788
|
-
token,
|
|
789
|
-
body,
|
|
790
|
-
...mirror.map(([column]) => quoteIdent$1(column))
|
|
791
|
-
];
|
|
792
|
-
const inserted = populate(table, [
|
|
793
|
-
[adopt.token ?? "token", "p_request ->> 'token'"],
|
|
794
|
-
[adopt.body ?? "body", "p_request"],
|
|
795
|
-
...mirror.map(([column, field]) => [column, MIRRORS[field]("p_request")])
|
|
796
|
-
]);
|
|
797
|
-
const insertSql = `-- open a request; a repeated ask keeps the open record and replaces only an expired one
|
|
798
|
-
create or replace function ${fn(APPROVAL_STORE.open)}(p_request jsonb)
|
|
799
|
-
returns void
|
|
800
|
-
language sql
|
|
801
|
-
volatile
|
|
802
|
-
security definer
|
|
803
|
-
set search_path = ''
|
|
804
|
-
as $$
|
|
805
|
-
insert into ${table} as a (${columns.join(", ")})
|
|
806
|
-
select ${columns.map((column) => `r.${column}`).join(", ")}
|
|
807
|
-
from ${inserted} r
|
|
808
|
-
on conflict (${token}) do update
|
|
809
|
-
set ${columns.filter((column) => column !== token).map((column) => `${column} = excluded.${column}`).join(", ")}
|
|
810
|
-
where ${status} = 'expired' or ${expires} <= now()
|
|
811
|
-
$$;`;
|
|
812
|
-
const attachSql = `create or replace function ${fn(APPROVAL_STORE.open)}(p_request jsonb)
|
|
813
|
-
returns void
|
|
814
|
-
language plpgsql
|
|
815
|
-
volatile
|
|
816
|
-
security definer
|
|
817
|
-
set search_path = ''
|
|
818
|
-
as $$
|
|
819
|
-
begin
|
|
820
|
-
update ${table} a
|
|
821
|
-
set ${body} = p_request${mirrorSet(table, mirror, "p_request")}
|
|
822
|
-
where a.${token} = p_request ->> 'token'
|
|
823
|
-
and (a.${body} is null or ${status} = 'expired' or ${expires} <= now());
|
|
824
|
-
if not found and not exists (select 1 from ${table} a where a.${token} = p_request ->> 'token') then
|
|
825
|
-
raise exception using
|
|
826
|
-
errcode = 'P0002',
|
|
827
|
-
message = 'permdock: no row of ${adopt.table.replaceAll("'", "''")} holds approval token ' || (p_request ->> 'token'),
|
|
828
|
-
hint = 'insert the application row with this token before opening the request';
|
|
829
|
-
end if;
|
|
830
|
-
end;
|
|
831
|
-
$$;`;
|
|
832
|
-
return `-- approval store adopted onto ${adopt.table}: ApprovalRequest bodies in ${adopt.body ?? "body"}, keyed by ${adopt.token ?? "token"}; supabaseApprovalStore calls the functions below
|
|
833
|
-
alter table ${table} add column if not exists ${token} text;
|
|
834
|
-
alter table ${table} add column if not exists ${body} jsonb;
|
|
835
|
-
create unique index if not exists ${index("token")} on ${table} (${token});
|
|
836
|
-
create index if not exists ${index("page")} on ${table} ((${body} ->> 'createdAt') collate "C", ${token} collate "C");
|
|
837
|
-
create index if not exists ${index("status")} on ${table} ((${body} ->> 'status'));
|
|
838
|
-
${jsonSchema === false ? "" : bodySchemaSql(table, jsonSchema, body, `${base}_permdock_body_schema`)}
|
|
839
|
-
${open === "attach" ? attachSql : insertSql}
|
|
840
|
-
${serverOnly(fn(APPROVAL_STORE.open), "jsonb")}
|
|
841
|
-
|
|
842
|
-
create or replace function ${fn(APPROVAL_STORE.get)}(p_token text)
|
|
843
|
-
returns jsonb
|
|
844
|
-
language sql
|
|
845
|
-
stable
|
|
846
|
-
security definer
|
|
847
|
-
set search_path = ''
|
|
848
|
-
as $$
|
|
849
|
-
select a.${body} from ${table} a where a.${token} = p_token
|
|
850
|
-
$$;
|
|
851
|
-
${serverOnly(fn(APPROVAL_STORE.get), "text")}
|
|
852
|
-
|
|
853
|
-
-- write a verdict over the record it was computed from: still pending, unexpired, with p_seen approvals
|
|
854
|
-
create or replace function ${fn(APPROVAL_STORE.resolve)}(p_token text, p_next jsonb, p_seen integer)
|
|
855
|
-
returns jsonb
|
|
856
|
-
language sql
|
|
857
|
-
volatile
|
|
858
|
-
security definer
|
|
859
|
-
set search_path = ''
|
|
860
|
-
as $$
|
|
861
|
-
update ${table} a
|
|
862
|
-
set ${body} = p_next${mirrorSet(table, mirror, "p_next")}
|
|
863
|
-
where a.${token} = p_token
|
|
864
|
-
and ${status} = 'pending'
|
|
865
|
-
and ${expires} > now()
|
|
866
|
-
and coalesce(jsonb_array_length(a.${body} -> 'approvals'), 0) = p_seen
|
|
867
|
-
returning a.${body}
|
|
868
|
-
$$;
|
|
869
|
-
${serverOnly(fn(APPROVAL_STORE.resolve), "text, jsonb, integer")}
|
|
870
|
-
|
|
871
|
-
-- consume an approved, unexpired, unconsumed request once
|
|
872
|
-
create or replace function ${fn(APPROVAL_STORE.consume)}(p_token text, p_now text)
|
|
873
|
-
returns jsonb
|
|
874
|
-
language sql
|
|
875
|
-
volatile
|
|
876
|
-
security definer
|
|
877
|
-
set search_path = ''
|
|
878
|
-
as $$
|
|
879
|
-
update ${table} a
|
|
880
|
-
set ${body} = a.${body} || jsonb_build_object('consumedAt', p_now)${mirrorSet(table, mirror, `(a.${body} || jsonb_build_object('consumedAt', p_now))`)}
|
|
881
|
-
where a.${token} = p_token
|
|
882
|
-
and ${status} = 'approved'
|
|
883
|
-
and a.${body} ->> 'consumedAt' is null
|
|
884
|
-
and ${expires} > p_now::timestamptz
|
|
885
|
-
returning a.${body}
|
|
886
|
-
$$;
|
|
887
|
-
${serverOnly(fn(APPROVAL_STORE.consume), "text, text")}
|
|
888
|
-
|
|
889
|
-
-- one page in (createdAt, token) order after the cursor position
|
|
890
|
-
create or replace function ${fn(APPROVAL_STORE.list)}(p_filter jsonb, p_after_created text, p_after_token text, p_limit integer)
|
|
891
|
-
returns setof jsonb
|
|
892
|
-
language sql
|
|
893
|
-
stable
|
|
894
|
-
security definer
|
|
895
|
-
set search_path = ''
|
|
896
|
-
as $$
|
|
897
|
-
select a.${body}
|
|
898
|
-
from ${table} a
|
|
899
|
-
where a.${body} is not null
|
|
900
|
-
and ${filter}
|
|
901
|
-
and (p_after_created is null
|
|
902
|
-
or (${created} collate "C", a.${token} collate "C") > (p_after_created collate "C", p_after_token collate "C"))
|
|
903
|
-
order by ${created} collate "C", a.${token} collate "C"
|
|
904
|
-
limit p_limit
|
|
905
|
-
$$;
|
|
906
|
-
${serverOnly(fn(APPROVAL_STORE.list), "jsonb, text, text, integer")}
|
|
907
|
-
|
|
908
|
-
create or replace function ${fn(APPROVAL_STORE.expire)}(p_now text)
|
|
909
|
-
returns integer
|
|
910
|
-
language sql
|
|
911
|
-
volatile
|
|
912
|
-
security definer
|
|
913
|
-
set search_path = ''
|
|
914
|
-
as $$
|
|
915
|
-
with expired as (
|
|
916
|
-
update ${table} a
|
|
917
|
-
set ${body} = a.${body} || '{"status":"expired"}'::jsonb${mirrorSet(table, mirror, `(a.${body} || '{"status":"expired"}'::jsonb)`)}
|
|
918
|
-
where ${status} = 'pending' and ${expires} <= p_now::timestamptz
|
|
919
|
-
returning 1
|
|
920
|
-
)
|
|
921
|
-
select count(*)::integer from expired
|
|
922
|
-
$$;
|
|
923
|
-
${serverOnly(fn(APPROVAL_STORE.expire), "text")}
|
|
924
|
-
|
|
925
|
-
-- reject every pending request the filter matches, as system:<by>
|
|
926
|
-
create or replace function ${fn(APPROVAL_STORE.cancel)}(p_filter jsonb, p_by text, p_note text, p_now text)
|
|
927
|
-
returns integer
|
|
928
|
-
language sql
|
|
929
|
-
volatile
|
|
930
|
-
security definer
|
|
931
|
-
set search_path = ''
|
|
932
|
-
as $$
|
|
933
|
-
with cancelled as (
|
|
934
|
-
update ${table} a
|
|
935
|
-
set ${body} = (a.${body} - 'note') || jsonb_strip_nulls(jsonb_build_object(
|
|
936
|
-
'status', 'rejected', 'resolvedAt', p_now, 'resolvedBy', 'system:' || p_by, 'note', p_note
|
|
937
|
-
))${mirrorSet(table, mirror, `((a.${body} - 'note') || jsonb_strip_nulls(jsonb_build_object('status', 'rejected', 'resolvedAt', p_now, 'resolvedBy', 'system:' || p_by, 'note', p_note)))`)}
|
|
938
|
-
where ${status} = 'pending'
|
|
939
|
-
and ${filter.replaceAll("\n ", "\n ")}
|
|
940
|
-
returning 1
|
|
941
|
-
)
|
|
942
|
-
select count(*)::integer from cancelled
|
|
943
|
-
$$;
|
|
944
|
-
${serverOnly(fn(APPROVAL_STORE.cancel), "jsonb, text, text, text")}
|
|
945
|
-
`;
|
|
946
|
-
}
|
|
947
|
-
//#endregion
|
|
948
449
|
//#region src/cli/rls/backfill.ts
|
|
949
450
|
const PREFIX$1 = "PermDock CLI";
|
|
950
451
|
/** A column of the roles-table row `r`. */
|
|
@@ -952,16 +453,16 @@ function col(name) {
|
|
|
952
453
|
return `r.${quoteIdent$1(name)}`;
|
|
953
454
|
}
|
|
954
455
|
function schemaOf(table) {
|
|
955
|
-
return table.includes(".") ? table
|
|
456
|
+
return table.includes(".") ? beforeFirst(table, ".") : "public";
|
|
956
457
|
}
|
|
957
458
|
/** The roles-table column a reference joins on, after checking it points at `roles`. */
|
|
958
459
|
function roleRef(role, table, roles, label) {
|
|
959
460
|
const pairs = Object.entries(role.on);
|
|
960
461
|
const [pair] = pairs;
|
|
961
|
-
if (pair === void 0 || pairs.length !== 1) throw
|
|
462
|
+
if (pair === void 0 || pairs.length !== 1) throw configError(`${PREFIX$1}: ${label}.on must map exactly one column of ${table} to ${role.through}, for example { role_id: 'id' }`);
|
|
962
463
|
const through = role.through.includes(".") ? role.through : `${schemaOf(table)}.${role.through}`;
|
|
963
|
-
if (qualifySqlTable(through) !== qualifySqlTable(roles.table)) throw
|
|
964
|
-
if (role.column !== roles.key) throw
|
|
464
|
+
if (qualifySqlTable(through) !== qualifySqlTable(roles.table)) throw configError(`${PREFIX$1}: ${label}.through must be rls.customRoleWrites.roles.table (${roles.table}), got ${role.through}`);
|
|
465
|
+
if (role.column !== roles.key) throw configError(`${PREFIX$1}: ${label}.column must be rls.customRoleWrites.roles.key (${roles.key}), got ${role.column}`);
|
|
965
466
|
const [ref, id] = pair;
|
|
966
467
|
return {
|
|
967
468
|
ref,
|
|
@@ -976,10 +477,10 @@ function roleRef(role, table, roles, label) {
|
|
|
976
477
|
function legacyKeySql(expr, migrate, renamed) {
|
|
977
478
|
const exact = new Map(Object.entries(renamed));
|
|
978
479
|
for (const [from, to] of Object.entries(migrate?.keys ?? {})) exact.set(from, to);
|
|
979
|
-
const prefixes = Object.entries(migrate?.prefixes ?? {}).toSorted(([a], [b]) => b.length - a.length || (a
|
|
480
|
+
const prefixes = Object.entries(migrate?.prefixes ?? {}).toSorted(([a], [b]) => b.length - a.length || byCodeUnit(a, b));
|
|
980
481
|
const prefixed = prefixes.length === 0 ? expr : `(case ${prefixes.map(([from, to]) => `when left(${expr}, ${String(from.length)}) = ${quoteLiteral(from)} then ${quoteLiteral(to)} || substr(${expr}, ${String(from.length + 1)})`).join(" ")} else ${expr} end)`;
|
|
981
|
-
const mapped = exact.size === 0 ? prefixed : `(case ${expr} ${[...exact.entries()].toSorted(
|
|
982
|
-
const renames = Object.entries(renamed).toSorted(
|
|
482
|
+
const mapped = exact.size === 0 ? prefixed : `(case ${expr} ${[...exact.entries()].toSorted(byKey).map(([from, to]) => `when ${quoteLiteral(from)} then ${quoteLiteral(to)}`).join(" ")} else ${prefixed} end)`;
|
|
483
|
+
const renames = Object.entries(renamed).toSorted(byKey);
|
|
983
484
|
return currentKeyExpr(renames, mapped);
|
|
984
485
|
}
|
|
985
486
|
function sourceSql(source, table, label) {
|
|
@@ -999,20 +500,20 @@ function sourceSql(source, table, label) {
|
|
|
999
500
|
function backfillSql(ctx, from, migrate) {
|
|
1000
501
|
const custom = ctx.customRoles;
|
|
1001
502
|
const root = ctx.scopes[0]?.name;
|
|
1002
|
-
if (custom === void 0 || root === void 0) throw
|
|
1003
|
-
if (ctx.authorize !== "database") throw
|
|
503
|
+
if (custom === void 0 || root === void 0) throw configError(`${PREFIX$1}: rls.customRoles.from needs custom roles on`);
|
|
504
|
+
if (ctx.authorize !== "database") throw configError(`${PREFIX$1}: rls.customRoles.from needs database mode; in jwt mode custom roles ride the token, so build the grants claim with customRoleClaim`);
|
|
1004
505
|
const roles = custom.table;
|
|
1005
|
-
if (roles === void 0) throw
|
|
506
|
+
if (roles === void 0) throw configError(`${PREFIX$1}: rls.customRoles.from needs rls.customRoleWrites.roles, the roles table each row belongs to`);
|
|
1006
507
|
const leveled = custom.levels === true;
|
|
1007
|
-
if (from.permissions.level !== void 0 && !leveled) throw
|
|
508
|
+
if (from.permissions.level !== void 0 && !leveled) throw configError(`${PREFIX$1}: rls.customRoles.from.permissions.level is set, but no resource declares levels`);
|
|
1008
509
|
const tenantType = tenantTypeOf(ctx);
|
|
1009
510
|
const rolesTable = quoteQualified(roles.table);
|
|
1010
511
|
const tenant = roles.tenant === void 0 ? `null::${tenantType}` : `${col(roles.tenant)}::${tenantType}`;
|
|
1011
|
-
const scope = roles
|
|
512
|
+
const scope = rolesTableScope(roles, tenant, root, col);
|
|
1012
513
|
const scopeId = roles.id === void 0 ? "null::text" : `${col(roles.id)}::text`;
|
|
1013
514
|
const skipped = roles.skip === void 0 ? "false" : `coalesce(${col(roles.skip)}, false)`;
|
|
1014
515
|
const renamed = custom.renamed ?? {};
|
|
1015
|
-
const renames = Object.entries(renamed).toSorted(
|
|
516
|
+
const renames = Object.entries(renamed).toSorted(byKey);
|
|
1016
517
|
const current = (expr) => currentKeyExpr(renames, expr);
|
|
1017
518
|
const known = sqlTextArray([...custom.permissions ?? [], ...renames.map(([former]) => former)]);
|
|
1018
519
|
const declared = sqlTextArray(custom.declared);
|
|
@@ -1195,7 +696,7 @@ function rowsFilter(ctx, entry) {
|
|
|
1195
696
|
return arms.length === 0 ? "false" : [...new Set(arms)].join("\n or ");
|
|
1196
697
|
}
|
|
1197
698
|
function functionName$1(resource) {
|
|
1198
|
-
if (!IDENT$1.test(resource)) throw
|
|
699
|
+
if (!IDENT$1.test(resource)) throw configError(`PermDock CLI: unsafe break-glass resource '${resource}'`);
|
|
1199
700
|
return `permdock_break_glass_${resource}`;
|
|
1200
701
|
}
|
|
1201
702
|
function auditSql$1(ctx) {
|
|
@@ -1234,8 +735,8 @@ begin
|
|
|
1234
735
|
or session ->> 'v' <> '1'
|
|
1235
736
|
or session ->> 'permission' <> p_permission
|
|
1236
737
|
or coalesce(session ->> 'reason', '') = ''
|
|
1237
|
-
or jsonb_typeof(session -> '
|
|
1238
|
-
or (session ->> '
|
|
738
|
+
or jsonb_typeof(session -> 'expires_at') <> 'number'
|
|
739
|
+
or (session ->> 'expires_at')::numeric <= extract(epoch from now())
|
|
1239
740
|
then
|
|
1240
741
|
raise exception 'PermDock: no valid break-glass session for %', p_permission
|
|
1241
742
|
using errcode = '42501';
|
|
@@ -1367,7 +868,7 @@ function pascalCase(name) {
|
|
|
1367
868
|
}
|
|
1368
869
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
1369
870
|
function identifier(name, what) {
|
|
1370
|
-
if (!IDENTIFIER.test(name)) throw
|
|
871
|
+
if (!IDENTIFIER.test(name)) throw configError(`PermDock CLI: ${what} ${JSON.stringify(name)} is not an identifier`);
|
|
1371
872
|
return name;
|
|
1372
873
|
}
|
|
1373
874
|
function quoted(text) {
|
|
@@ -1431,7 +932,7 @@ function emitPrisma(policies, options = {}) {
|
|
|
1431
932
|
""
|
|
1432
933
|
];
|
|
1433
934
|
for (const item of policies) {
|
|
1434
|
-
if (item.effect === "deny") throw
|
|
935
|
+
if (item.effect === "deny") throw configError(`PermDock CLI: Prisma policy blocks are permissive only, so ${item.name} (a deny) needs --target sql`);
|
|
1435
936
|
lines.push(`policy_${item.command} ${identifier(item.name, "policy name")} {`, ` target = ${modelOf(item.table)}`, ` roles = [${item.roles.join(", ")}]`);
|
|
1436
937
|
if (item.using !== void 0) lines.push(` using = ${JSON.stringify(item.using)}`);
|
|
1437
938
|
if (item.check !== void 0) lines.push(` withCheck = ${JSON.stringify(item.check)}`);
|
|
@@ -1461,18 +962,18 @@ const LITERAL_SEGMENT = /^[^{}:\s]+$/u;
|
|
|
1461
962
|
const TOPIC = "(select realtime.topic())";
|
|
1462
963
|
function policyName(prefix, name, command) {
|
|
1463
964
|
const full = `permdock_${prefix}_${name.replaceAll(/[^A-Za-z0-9]+/gu, "_").replaceAll(/^_|_$/gu, "")}_${command}`;
|
|
1464
|
-
if (new TextEncoder().encode(full).length > 63) throw
|
|
965
|
+
if (new TextEncoder().encode(full).length > 63) throw configError(`PermDock CLI: the ${prefix} policy name ${full} is longer than Postgres's 63 bytes; shorten '${name}'`);
|
|
1465
966
|
return full;
|
|
1466
967
|
}
|
|
1467
968
|
function grantKey(facts, ref, where, warnings) {
|
|
1468
969
|
if (ref === void 0) return;
|
|
1469
970
|
const key = typeof ref === "object" && ref !== null ? Reflect.get(ref, "key") : ref;
|
|
1470
|
-
if (typeof key !== "string" || !facts.keys.has(key)) throw
|
|
971
|
+
if (typeof key !== "string" || !facts.keys.has(key)) throw configError(`PermDock CLI: ${where} must be a permission reference from the definitions (got ${JSON.stringify(key)})`);
|
|
1471
972
|
if (facts.rowConditions.has(key)) warnings.push(`${where} is ${key}, whose grants carry row conditions (a relation, a where or another condition): the policy admits only the instances its role allows without a condition reach, through permitted_<scope>_ids_by_permission`);
|
|
1472
973
|
return key;
|
|
1473
974
|
}
|
|
1474
975
|
function declaredScope(facts, scope, where) {
|
|
1475
|
-
if (!facts.scopes.has(scope)) throw
|
|
976
|
+
if (!facts.scopes.has(scope)) throw configError(`PermDock CLI: ${where} names the scope '${scope}', which definePolicy({ scopes }) does not declare`);
|
|
1476
977
|
return scope;
|
|
1477
978
|
}
|
|
1478
979
|
/**
|
|
@@ -1498,7 +999,7 @@ function realtimePolicies(ctx, facts, realtime, warnings) {
|
|
|
1498
999
|
}];
|
|
1499
1000
|
});
|
|
1500
1001
|
const [placeholder] = placeholders;
|
|
1501
|
-
if (placeholder === void 0 || placeholders.length > 1 || segments.some((segment, index) => index !== placeholder.index && !LITERAL_SEGMENT.test(segment))) throw
|
|
1002
|
+
if (placeholder === void 0 || placeholders.length > 1 || segments.some((segment, index) => index !== placeholder.index && !LITERAL_SEGMENT.test(segment))) throw configError(`PermDock CLI: ${where} must be ':'-separated segments with exactly one '{<scope>}' segment, such as 'org:{organization}:chat'`);
|
|
1502
1003
|
const scope = declaredScope(facts, placeholder.scope, where);
|
|
1503
1004
|
const regex = `^${segments.map((segment, index) => index === placeholder.index ? "[^:]+" : escapeRegex(segment)).join(":")}$`;
|
|
1504
1005
|
const id = `split_part(${TOPIC}, ':', ${String(placeholder.index + 1)})`;
|
|
@@ -1507,7 +1008,7 @@ function realtimePolicies(ctx, facts, realtime, warnings) {
|
|
|
1507
1008
|
and ${permitted(ctx, id, scope, key)}`;
|
|
1508
1009
|
const read = grantKey(facts, topic.read, `${where}.read`, warnings);
|
|
1509
1010
|
const write = grantKey(facts, topic.write, `${where}.write`, warnings);
|
|
1510
|
-
if (read === void 0) throw
|
|
1011
|
+
if (read === void 0) throw configError(`PermDock CLI: ${where}.read is required`);
|
|
1511
1012
|
const policies = [{
|
|
1512
1013
|
name: policyName("realtime", pattern, "select"),
|
|
1513
1014
|
table: "realtime.messages",
|
|
@@ -1529,7 +1030,7 @@ function realtimePolicies(ctx, facts, realtime, warnings) {
|
|
|
1529
1030
|
}
|
|
1530
1031
|
function folderOf(bucket, where) {
|
|
1531
1032
|
const folder = bucket.folder ?? 1;
|
|
1532
|
-
if (!Number.isInteger(folder) || folder < 1) throw
|
|
1033
|
+
if (!Number.isInteger(folder) || folder < 1) throw configError(`PermDock CLI: ${where}.folder must be a positive integer (got ${String(folder)})`);
|
|
1533
1034
|
return folder;
|
|
1534
1035
|
}
|
|
1535
1036
|
function storagePolicies(ctx, facts, storage, warnings) {
|
|
@@ -1574,7 +1075,7 @@ function storagePolicies(ctx, facts, storage, warnings) {
|
|
|
1574
1075
|
...command === "select" || command === "delete" ? {} : { check: expression }
|
|
1575
1076
|
}];
|
|
1576
1077
|
});
|
|
1577
|
-
if (policies.length === 0) throw
|
|
1078
|
+
if (policies.length === 0) throw configError(`PermDock CLI: ${where} needs read, write or delete`);
|
|
1578
1079
|
return policies;
|
|
1579
1080
|
});
|
|
1580
1081
|
}
|
|
@@ -1588,11 +1089,11 @@ function storagePolicies(ctx, facts, storage, warnings) {
|
|
|
1588
1089
|
*/
|
|
1589
1090
|
function realtimeStoragePolicies(ctx, facts, config, warnings = []) {
|
|
1590
1091
|
if (config.realtime === void 0 && config.storage === void 0) return [];
|
|
1591
|
-
if (ctx.dialect !== "supabase") throw
|
|
1092
|
+
if (ctx.dialect !== "supabase") throw configError(`PermDock CLI: rls.realtime and rls.storage need --dialect supabase (got ${ctx.dialect})`);
|
|
1592
1093
|
const policies = [...config.realtime === void 0 ? [] : realtimePolicies(ctx, facts, config.realtime, warnings), ...config.storage === void 0 ? [] : storagePolicies(ctx, facts, config.storage, warnings)];
|
|
1593
1094
|
const seen = /* @__PURE__ */ new Set();
|
|
1594
1095
|
for (const { name } of policies) {
|
|
1595
|
-
if (seen.has(name)) throw
|
|
1096
|
+
if (seen.has(name)) throw configError(`PermDock CLI: two rls.realtime or rls.storage entries both name the policy ${name}; rename one`);
|
|
1596
1097
|
seen.add(name);
|
|
1597
1098
|
}
|
|
1598
1099
|
return policies;
|
|
@@ -1603,7 +1104,7 @@ function realtimeStoragePolicies(ctx, facts, config, warnings = []) {
|
|
|
1603
1104
|
function rowsHelper(resource) {
|
|
1604
1105
|
const name = graphSqlName(resource);
|
|
1605
1106
|
const helper = `permitted_${name ?? resource}_rows`;
|
|
1606
|
-
if (name === void 0 || new TextEncoder().encode(`${helper}_for`).length > 63) throw
|
|
1107
|
+
if (name === void 0 || new TextEncoder().encode(`${helper}_for`).length > 63) throw configError(`PermDock CLI: rls.rowHelpers cannot name a helper for resource '${resource}'; use a SQL name of at most 45 bytes`);
|
|
1607
1108
|
return helper;
|
|
1608
1109
|
}
|
|
1609
1110
|
/** The branches a row check of a permission reads: its allows and denies on rows that exist, for signed-in callers. */
|
|
@@ -1750,7 +1251,7 @@ function rowHelpersSql(ctx, policy, branches, select, tables, warnings) {
|
|
|
1750
1251
|
const chunks = called.map((name) => stubSql(ctx, name));
|
|
1751
1252
|
for (const name of names) {
|
|
1752
1253
|
const node = policy.resources.get(name);
|
|
1753
|
-
if (node === void 0) throw
|
|
1254
|
+
if (node === void 0) throw configError(`PermDock CLI: rls.rowHelpers names '${name}', which the policy does not declare`);
|
|
1754
1255
|
chunks.push(resourceSql(ctx, node, rowBranchesOf(branches, name), tables));
|
|
1755
1256
|
}
|
|
1756
1257
|
if (chunks.length > 0 && ctx.dialect === "neon") warnings.push("rls.rowHelpers: the neon dialect reads the subject from auth.session(), which SQL cannot set, so no permitted_<resource>_rows_for is written");
|
|
@@ -1807,7 +1308,7 @@ function generatePolicies(input, policy, options, built, warnings) {
|
|
|
1807
1308
|
...policyName === void 0 ? {} : { name: policyName }
|
|
1808
1309
|
});
|
|
1809
1310
|
const actorKinds = readOnlyActorKinds(rls?.readOnlyActors);
|
|
1810
|
-
if (actorKinds !== void 0 && helpersOnly) warnings.push(
|
|
1311
|
+
if (actorKinds !== void 0 && helpersOnly) warnings.push(`rls.readOnlyActors with --helpers-only writes no policies: add \`and (select ${qualified(ctx, ACTOR_MAY_WRITE)}())\` to each hand-written insert, update and delete policy, or a restrictive policy that calls it`);
|
|
1811
1312
|
const withActors = (items) => actorKinds === void 0 ? items : [...items, ...readOnlyActorPolicies(items, subjectClaimJsonSql(ctx, "act"), actorKinds)];
|
|
1812
1313
|
let tablePolicies;
|
|
1813
1314
|
try {
|
|
@@ -1856,7 +1357,7 @@ function mappedKey(config, renamed) {
|
|
|
1856
1357
|
return `coalesce(${quoteLiteral(JSON.stringify(exact))}::jsonb ->> p_key, ${fallback})`;
|
|
1857
1358
|
}
|
|
1858
1359
|
function scopeOf(ctx, scope) {
|
|
1859
|
-
if (!ctx.scopes.some((item) => item.name === scope)) throw
|
|
1360
|
+
if (!ctx.scopes.some((item) => item.name === scope)) throw configError(`PermDock CLI: rls.migrate helper scope '${scope}' is not a declared scope`);
|
|
1860
1361
|
return scope;
|
|
1861
1362
|
}
|
|
1862
1363
|
function addKey(list, key) {
|
|
@@ -1953,6 +1454,13 @@ function shim(ctx, config, keys, fn, helper) {
|
|
|
1953
1454
|
const type = scopeTypeOf(ctx, scope);
|
|
1954
1455
|
return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified(ctx, memberIdsHelper(scope))}()), false)`);
|
|
1955
1456
|
}
|
|
1457
|
+
case "role": {
|
|
1458
|
+
const scope = scopeOf(ctx, helper.scope);
|
|
1459
|
+
const type = scopeTypeOf(ctx, scope);
|
|
1460
|
+
const values = Object.entries(ctx.roleValues ?? {});
|
|
1461
|
+
const role = values.length === 0 ? "p_role" : `case p_role ${values.map(([from, to]) => `when ${quoteLiteral(from)} then ${quoteLiteral(to)}`).join(" ")} else p_role end`;
|
|
1462
|
+
return shimFunction(fn, `p_id ${type}, p_role text`, `${type}, text`, "boolean", ` select coalesce(p_id in (select ${qualified(ctx, memberIdsHelper(scope))}(array[${role}])), false)`);
|
|
1463
|
+
}
|
|
1956
1464
|
case "global": return shimFunction(fn, "p_key text", "text", "boolean", ` select ${hasSql(ctx, keys)}`);
|
|
1957
1465
|
case "scoped": {
|
|
1958
1466
|
const literals = /* @__PURE__ */ new Map();
|
|
@@ -1980,8 +1488,8 @@ function shimsSql(ctx, config, shims, renamed, grants) {
|
|
|
1980
1488
|
map: (scope) => `${quoteLiteral(shimMap(grants, scope))}::jsonb`
|
|
1981
1489
|
};
|
|
1982
1490
|
const chunks = [`-- permdock shims: legacy helper names over the permdock helpers; drop each once doctor reports no caller (PD056)`];
|
|
1983
|
-
for (const [name, helper] of Object.entries(config.helpers).toSorted(
|
|
1984
|
-
if (!NAME.test(name)) throw
|
|
1491
|
+
for (const [name, helper] of Object.entries(config.helpers).toSorted(byKey)) {
|
|
1492
|
+
if (!NAME.test(name)) throw configError(`PermDock CLI: unsafe rls.migrate helper name '${name}'`);
|
|
1985
1493
|
chunks.push(shim(ctx, config, keys, `${quoteIdent$1(schema)}.${name}`, helper));
|
|
1986
1494
|
}
|
|
1987
1495
|
return `${chunks.join("\n\n")}\n`;
|
|
@@ -2013,20 +1521,20 @@ function generatedTables(sql, schema) {
|
|
|
2013
1521
|
return [...found].toSorted();
|
|
2014
1522
|
}
|
|
2015
1523
|
function functionName(name, label) {
|
|
2016
|
-
if (typeof name !== "string" || name.split(".").length !== 2) throw
|
|
1524
|
+
if (typeof name !== "string" || name.split(".").length !== 2) throw configError(`${PREFIX}: ${label} must be a schema-qualified function such as audit.log_change, got ${JSON.stringify(name)}`);
|
|
2017
1525
|
return quoteTable(name);
|
|
2018
1526
|
}
|
|
2019
1527
|
function knownTable(table, tables, label) {
|
|
2020
|
-
if (!tables.includes(table)) throw
|
|
1528
|
+
if (!tables.includes(table)) throw configError(`${PREFIX}: ${label} names ${table}, which rls generate does not create with this config; it creates ${tables.join(", ")}`);
|
|
2021
1529
|
}
|
|
2022
1530
|
function triggerSql(table, trigger, label) {
|
|
2023
|
-
if (typeof trigger.name !== "string" || trigger.name === "" || trigger.name.toLowerCase().startsWith("permdock_")) throw
|
|
2024
|
-
if (trigger.when !== "before" && trigger.when !== "after") throw
|
|
1531
|
+
if (typeof trigger.name !== "string" || trigger.name === "" || trigger.name.toLowerCase().startsWith("permdock_")) throw configError(`${PREFIX}: ${label}.name must be a name outside permdock_, which generated triggers use, got ${JSON.stringify(trigger.name)}`);
|
|
1532
|
+
if (trigger.when !== "before" && trigger.when !== "after") throw configError(`${PREFIX}: ${label}.when must be before or after`);
|
|
2025
1533
|
const events = trigger.events;
|
|
2026
|
-
if (events.length === 0 || !events.every((event) => EVENTS.has(event))) throw
|
|
1534
|
+
if (events.length === 0 || !events.every((event) => EVENTS.has(event))) throw configError(`${PREFIX}: ${label}.events must list insert, update, delete or truncate`);
|
|
2027
1535
|
const level = trigger.level ?? "row";
|
|
2028
|
-
if (level !== "row" && level !== "statement") throw
|
|
2029
|
-
if (level === "row" && events.includes("truncate")) throw
|
|
1536
|
+
if (level !== "row" && level !== "statement") throw configError(`${PREFIX}: ${label}.level must be row or statement`);
|
|
1537
|
+
if (level === "row" && events.includes("truncate")) throw configError(`${PREFIX}: ${label} fires on truncate, which Postgres allows only with level: 'statement'`);
|
|
2030
1538
|
const fn = functionName(trigger.function, `${label}.function`);
|
|
2031
1539
|
const args = (trigger.args ?? []).map((arg) => quoteLiteral(arg));
|
|
2032
1540
|
const name = quoteIdent$1(trigger.name);
|
|
@@ -2043,7 +1551,7 @@ create trigger ${name}
|
|
|
2043
1551
|
function triggersSql(triggers, schema, tables) {
|
|
2044
1552
|
if (triggers === void 0) return "";
|
|
2045
1553
|
const chunks = [];
|
|
2046
|
-
for (const [table, list] of Object.entries(triggers).toSorted(
|
|
1554
|
+
for (const [table, list] of Object.entries(triggers).toSorted(byKey)) {
|
|
2047
1555
|
knownTable(table, tables, `rls.triggers.${table}`);
|
|
2048
1556
|
const target = `${quoteIdent$1(schema)}.${quoteIdent$1(table)}`;
|
|
2049
1557
|
for (const [index, trigger] of list.entries()) chunks.push(triggerSql(target, trigger, `rls.triggers.${table}[${String(index)}]`));
|
|
@@ -2062,7 +1570,7 @@ function auditSql(audit, schema, tables) {
|
|
|
2062
1570
|
if (listed !== void 0) for (const table of listed) knownTable(table, tables, "rls.audit.tables");
|
|
2063
1571
|
const targets = listed ?? AUDITED.filter((table) => tables.includes(table));
|
|
2064
1572
|
const args = Object.entries(audit.args ?? {}).map(([name, value]) => {
|
|
2065
|
-
if (!IDENT.test(name)) throw
|
|
1573
|
+
if (!IDENT.test(name)) throw configError(`${PREFIX}: rls.audit.args names ${JSON.stringify(name)}, which is not a lower-case parameter name`);
|
|
2066
1574
|
return `, ${quoteIdent$1(name)} => ${quoteLiteral(value)}`;
|
|
2067
1575
|
});
|
|
2068
1576
|
if (targets.length === 0) return "";
|
|
@@ -2121,7 +1629,7 @@ function sectionsSql(input, policy, options, built, generated, jsonSchema, warni
|
|
|
2121
1629
|
if (ctx.assignments !== void 0 && (ownership === void 0 || ownership.assigns.length === 0)) warnings.push("rls.assignments needs a role that declares assigns: no assignment trigger is written");
|
|
2122
1630
|
let approvals;
|
|
2123
1631
|
try {
|
|
2124
|
-
approvals =
|
|
1632
|
+
approvals = approvalsSql(ctx, rls?.approvals, jsonSchema ?? false);
|
|
2125
1633
|
} catch (cause) {
|
|
2126
1634
|
return { refusal: describeError(cause) };
|
|
2127
1635
|
}
|
|
@@ -2163,6 +1671,7 @@ function preambleSql(input, policy, options, built, generated, jsonSchema, warni
|
|
|
2163
1671
|
const renamed = Object.fromEntries(renamedKeys(policy.vocabulary.permissions));
|
|
2164
1672
|
const grants = shimGrants(compiled.rolePermissions, compiled.conditionedKeys, renamed);
|
|
2165
1673
|
const anonExecute = anonExecutes(input, generated);
|
|
1674
|
+
const actorKinds = readOnlyActorKinds(rls?.readOnlyActors);
|
|
2166
1675
|
const usesLiveSession = compiled.branches.some((branch) => [
|
|
2167
1676
|
branch.access,
|
|
2168
1677
|
branch.using,
|
|
@@ -2180,6 +1689,8 @@ function preambleSql(input, policy, options, built, generated, jsonSchema, warni
|
|
|
2180
1689
|
...rls?.trustedReaders === void 0 ? {} : { trustedReaders: rls.trustedReaders }
|
|
2181
1690
|
}),
|
|
2182
1691
|
permissionHelpersSql(ctx, grants, renamed, anonExecute),
|
|
1692
|
+
permissionHoldersSql(ctx) || void 0,
|
|
1693
|
+
actorKinds === void 0 ? void 0 : actorMayWriteSql(ctx, actorKinds),
|
|
2183
1694
|
usesLiveSession ? sessionLiveHelperSql(ctx) : void 0,
|
|
2184
1695
|
...sections,
|
|
2185
1696
|
shims,
|
|
@@ -2263,7 +1774,7 @@ function emitTarget(input, built, generated, preamble) {
|
|
|
2263
1774
|
outRel
|
|
2264
1775
|
};
|
|
2265
1776
|
}
|
|
2266
|
-
/** The generated SQL for one run, before `--db` prunes the index targets. */
|
|
1777
|
+
/** The generated SQL for one run, before `--db-url-stdin` prunes the index targets. */
|
|
2267
1778
|
function generateSql(input, policy, options, built, warnings) {
|
|
2268
1779
|
const rls = input.config.rls;
|
|
2269
1780
|
const { ctx } = built;
|
|
@@ -2299,7 +1810,53 @@ function generateSql(input, policy, options, built, warnings) {
|
|
|
2299
1810
|
//#region src/cli/rls/generate.ts
|
|
2300
1811
|
function loadPolicy(cwd, config, from) {
|
|
2301
1812
|
const policyPath = from === "drizzle" ? config.policy : from ?? config.policy;
|
|
2302
|
-
return requirePolicy(cwd, policyPath, "rls generate needs policy in permdock.config.ts or --
|
|
1813
|
+
return requirePolicy(cwd, policyPath, "rls generate needs policy in permdock.config.ts or --policy");
|
|
1814
|
+
}
|
|
1815
|
+
const PROJECT_COMMAND = "permdock rls generate";
|
|
1816
|
+
/**
|
|
1817
|
+
* The policy `policyFromCatalog` rebuilds from the project's pushed catalog
|
|
1818
|
+
* and policy; a project with no pushed policy fails with `policy-not-published`.
|
|
1819
|
+
*/
|
|
1820
|
+
async function projectPolicyOf(input, project) {
|
|
1821
|
+
const fetched = await fetchProjectPolicy({
|
|
1822
|
+
command: PROJECT_COMMAND,
|
|
1823
|
+
url: input.url,
|
|
1824
|
+
project,
|
|
1825
|
+
json: input.json === true,
|
|
1826
|
+
env: input.io.env ?? process.env,
|
|
1827
|
+
io: input.io
|
|
1828
|
+
});
|
|
1829
|
+
if (!fetched.ok) return {
|
|
1830
|
+
...fetched.result,
|
|
1831
|
+
text: ""
|
|
1832
|
+
};
|
|
1833
|
+
const { catalog, policy } = fetched.value;
|
|
1834
|
+
if (policy === void 0) {
|
|
1835
|
+
const detail = `project ${project} has a pushed catalog but no policy; push from a project whose permdock.config.ts names one`;
|
|
1836
|
+
return {
|
|
1837
|
+
code: 1,
|
|
1838
|
+
output: input.json === true ? JSON.stringify({
|
|
1839
|
+
type: POLICY_NOT_PUBLISHED,
|
|
1840
|
+
title: "Policy not published",
|
|
1841
|
+
detail,
|
|
1842
|
+
command: PROJECT_COMMAND,
|
|
1843
|
+
exitCode: 1
|
|
1844
|
+
}, null, 2) : `${PROJECT_COMMAND}: ${detail}`,
|
|
1845
|
+
text: ""
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
try {
|
|
1849
|
+
return policyFromCatalog(catalog, policy);
|
|
1850
|
+
} catch (cause) {
|
|
1851
|
+
return {
|
|
1852
|
+
code: 1,
|
|
1853
|
+
output: `${PROJECT_COMMAND}: the pushed policy does not rebuild: ${describeError(cause)}`,
|
|
1854
|
+
text: ""
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
function isOutcome(value) {
|
|
1859
|
+
return "text" in value && "code" in value;
|
|
2303
1860
|
}
|
|
2304
1861
|
function refused(output) {
|
|
2305
1862
|
return {
|
|
@@ -2311,7 +1868,14 @@ function refused(output) {
|
|
|
2311
1868
|
async function runRlsGenerate(input) {
|
|
2312
1869
|
const options = generateOptions(input);
|
|
2313
1870
|
if (isRefusal(options)) return refused(options.refusal);
|
|
2314
|
-
|
|
1871
|
+
if (input.project !== void 0 && input.from !== void 0 && input.from !== "drizzle") return refused("rls generate takes the policy from --project or from a --policy module, not both");
|
|
1872
|
+
let policy;
|
|
1873
|
+
if (input.project === void 0) policy = await loadPolicy(input.cwd, input.config, input.from);
|
|
1874
|
+
else {
|
|
1875
|
+
const loaded = await projectPolicyOf(input, input.project);
|
|
1876
|
+
if (isOutcome(loaded)) return loaded;
|
|
1877
|
+
policy = loaded;
|
|
1878
|
+
}
|
|
2315
1879
|
const built = buildContext(input, policy, options);
|
|
2316
1880
|
if (isRefusal(built)) return refused(built.refusal);
|
|
2317
1881
|
const warnings = [];
|
|
@@ -2326,7 +1890,7 @@ async function runRlsGenerate(input) {
|
|
|
2326
1890
|
warnings.push(...pruned.warnings);
|
|
2327
1891
|
} catch (cause) {
|
|
2328
1892
|
return {
|
|
2329
|
-
code:
|
|
1893
|
+
code: exitCodeOfError(cause),
|
|
2330
1894
|
output: describeError(cause),
|
|
2331
1895
|
text: ""
|
|
2332
1896
|
};
|
|
@@ -2377,8 +1941,9 @@ async function runRlsGenerate(input) {
|
|
|
2377
1941
|
part: "backfill",
|
|
2378
1942
|
marker: BACKFILL_MARKER,
|
|
2379
1943
|
text: generated.backfill,
|
|
2380
|
-
now: input.io.now?.() ??
|
|
2381
|
-
check: input.check
|
|
1944
|
+
now: input.io.now?.() ?? dateAt(),
|
|
1945
|
+
check: input.check,
|
|
1946
|
+
reserved: files.map((other) => other.rel)
|
|
2382
1947
|
});
|
|
2383
1948
|
if (file !== void 0) files.push(file);
|
|
2384
1949
|
}
|
|
@@ -2397,10 +1962,11 @@ async function runRlsGenerate(input) {
|
|
|
2397
1962
|
};
|
|
2398
1963
|
}
|
|
2399
1964
|
const written = writeOutputFiles(input.cwd, files);
|
|
2400
|
-
const
|
|
1965
|
+
const notes = [...written.wrote.length === 0 ? [] : [`wrote ${written.wrote.join(", ")}`], ...warnings];
|
|
1966
|
+
if (written.printed !== "") for (const note of notes) input.io.stderr(note);
|
|
2401
1967
|
return {
|
|
2402
1968
|
code: 0,
|
|
2403
|
-
output:
|
|
1969
|
+
output: written.printed === "" ? notes.join("\n") : written.printed,
|
|
2404
1970
|
text,
|
|
2405
1971
|
...extras
|
|
2406
1972
|
};
|
|
@@ -2591,6 +2157,29 @@ async function introspectRls(db, expected, connect = connectIntrospect) {
|
|
|
2591
2157
|
await client.end();
|
|
2592
2158
|
}
|
|
2593
2159
|
}
|
|
2160
|
+
const WRITES = [
|
|
2161
|
+
"insert",
|
|
2162
|
+
"update",
|
|
2163
|
+
"delete"
|
|
2164
|
+
];
|
|
2165
|
+
/**
|
|
2166
|
+
* Each table and write command whose permissive policies reach
|
|
2167
|
+
* `authenticated` (or `public`) without the read-only actor check: no
|
|
2168
|
+
* restrictive policy for the command calls `permdock_actor_may_write()`, and
|
|
2169
|
+
* some permissive one does not.
|
|
2170
|
+
*/
|
|
2171
|
+
function unguardedWrites(policies, fn) {
|
|
2172
|
+
const out = [];
|
|
2173
|
+
const tables = [...new Set(policies.map((policy) => policy.table))];
|
|
2174
|
+
for (const table of tables) for (const command of WRITES) {
|
|
2175
|
+
const applying = policies.filter((policy) => policy.table === table && (policy.command === command || policy.command === "all"));
|
|
2176
|
+
const permissive = applying.filter((policy) => policy.restrictive !== true && (policy.roles ?? []).some((role) => role === "authenticated" || role === "public"));
|
|
2177
|
+
if (permissive.length === 0) continue;
|
|
2178
|
+
const calls = (expression) => expression.includes(fn);
|
|
2179
|
+
if (!(applying.some((policy) => policy.restrictive === true && calls(policy.expression)) || permissive.every((policy) => calls(policy.expression)))) out.push(`${table}: ${command} policies for authenticated do not call ${fn}(), so a read-only support or impersonation session can write (rls.readOnlyActors)`);
|
|
2180
|
+
}
|
|
2181
|
+
return out;
|
|
2182
|
+
}
|
|
2594
2183
|
const SYSTEM_SCHEMAS = [
|
|
2595
2184
|
"pg_catalog",
|
|
2596
2185
|
"information_schema",
|
|
@@ -2610,7 +2199,7 @@ const SYSTEM_SCHEMAS = [
|
|
|
2610
2199
|
"supabase_migrations"
|
|
2611
2200
|
];
|
|
2612
2201
|
function inScope(table) {
|
|
2613
|
-
const schema = table
|
|
2202
|
+
const schema = beforeFirst(table, ".");
|
|
2614
2203
|
return HELPER_TABLES.some((item) => item === table) || !(SYSTEM_SCHEMAS.some((item) => item === schema) || schema.startsWith("pg_"));
|
|
2615
2204
|
}
|
|
2616
2205
|
function seedLine(row) {
|
|
@@ -2632,13 +2221,16 @@ function diffMixed(expected, actual) {
|
|
|
2632
2221
|
for (const key of helperCallKeys(policy.expression)) if (!permissions.has(key)) drift.push(`${policy.table}: policy ${policy.name} passes ${key}, which the policy does not declare, so it always denies`);
|
|
2633
2222
|
else if (conditioned.has(key) && blind.has(key)) drift.push(`${policy.table}: policy ${policy.name} passes ${key}, whose grants carry row conditions the helpers do not check: the policy grants more than the application does`);
|
|
2634
2223
|
}
|
|
2224
|
+
if (expected.readOnlyActors === true) drift.push(...unguardedWrites(actual.policies, ACTOR_MAY_WRITE));
|
|
2635
2225
|
return {
|
|
2636
2226
|
drift,
|
|
2637
2227
|
info: actual.rlsTables.filter((table) => !covered.has(table)).map((table) => `${table}: no policy calls a PermDock helper`)
|
|
2638
2228
|
};
|
|
2639
2229
|
}
|
|
2640
2230
|
const ALL_POLICIES_SQL = `select schemaname || '.' || tablename as target, policyname,
|
|
2641
|
-
coalesce(qual, '') || ' ' || coalesce(with_check, '') as expression
|
|
2231
|
+
coalesce(qual, '') || ' ' || coalesce(with_check, '') as expression,
|
|
2232
|
+
lower(cmd) as command, permissive = 'RESTRICTIVE' as restrictive,
|
|
2233
|
+
array_to_string(roles, ',') as roles
|
|
2642
2234
|
from pg_policies`;
|
|
2643
2235
|
const RLS_TABLES_SQL = `select n.nspname || '.' || c.relname as target
|
|
2644
2236
|
from pg_class c
|
|
@@ -2665,7 +2257,10 @@ async function introspectMixed(db, schema, connect = connectIntrospect) {
|
|
|
2665
2257
|
policies: policies.rows.map((row) => ({
|
|
2666
2258
|
table: String(row["target"]),
|
|
2667
2259
|
name: String(row["policyname"]),
|
|
2668
|
-
expression: String(row["expression"])
|
|
2260
|
+
expression: String(row["expression"]),
|
|
2261
|
+
command: String(row["command"]),
|
|
2262
|
+
restrictive: row["restrictive"] === true,
|
|
2263
|
+
roles: String(row["roles"] ?? "").split(",").filter((role) => role !== "")
|
|
2669
2264
|
})).filter((row) => inScope(row.table)),
|
|
2670
2265
|
rlsTables: tables.rows.map((row) => String(row["target"])).filter(inScope).toSorted()
|
|
2671
2266
|
};
|
|
@@ -2677,24 +2272,12 @@ async function introspectMixed(db, schema, connect = connectIntrospect) {
|
|
|
2677
2272
|
//#region src/cli/rls/rls.ts
|
|
2678
2273
|
const RLS_HELP = `permdock rls generate | import | verify | migrate
|
|
2679
2274
|
|
|
2680
|
-
generate
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
[--capabilities] [--fields views [--revoke-columns]]
|
|
2685
|
-
[--out <path>] [--check] [--skip-closures] [--inline-functions] [--force] [--guc-prefix app]
|
|
2686
|
-
[--split helpers,seeds,policies,hook --out <dir>/056_permdock_{part}.sql]
|
|
2687
|
-
[--grants-out <file>|-] [--seeds-out <file>|-] [--backfill-out <file>|<dir>/|-]
|
|
2688
|
-
[--helpers-only] [--shims] [--db $DATABASE_URL]
|
|
2689
|
-
import --sql schema.sql | --db $DATABASE_URL --out src/permissions.generated.ts
|
|
2690
|
-
[--schema zod|valibot|arktype] [--memberships <table>:tenant,user,role]
|
|
2691
|
-
verify [--db $DATABASE_URL] [--fixtures rls.fixtures.ts] [--format pgtap|node] [--tree]
|
|
2692
|
-
[--introspect --db $DATABASE_URL, with the generate flags]
|
|
2693
|
-
[--advisors [--db $DATABASE_URL] [--json]]: supabase db advisors, security lints
|
|
2694
|
-
migrate --sql <dir> [--write] [--json] [--retire-out <file>|<dir>/|-], with the generate flags
|
|
2695
|
-
rewrites the rls.migrate helpers' calls in policies onto the generated helpers;
|
|
2696
|
-
--retire-out drops each rls.migrate.tables table and its refresh triggers once unread
|
|
2275
|
+
generate Compile the policy into RLS policies and helpers
|
|
2276
|
+
import Turn existing policies (--sql or --db-url-stdin) into a permissions module
|
|
2277
|
+
verify Run the fixtures, or compare a live database with --introspect
|
|
2278
|
+
migrate Rewrite the rls.migrate helpers' calls onto the generated helpers
|
|
2697
2279
|
|
|
2280
|
+
Run permdock rls <command> --help for its flags.
|
|
2698
2281
|
Never emits service_role, except in the execute grant rls.trustedReaders asks for. memberOf compiles through the dialect memberships mapping.
|
|
2699
2282
|
`;
|
|
2700
2283
|
function asTarget(value) {
|
|
@@ -2719,6 +2302,9 @@ function generateInput(input, target, dialect) {
|
|
|
2719
2302
|
io: input.io,
|
|
2720
2303
|
...input.out === void 0 ? {} : { out: input.out },
|
|
2721
2304
|
...input.from === void 0 ? {} : { from: input.from },
|
|
2305
|
+
...input.project === void 0 ? {} : { project: input.project },
|
|
2306
|
+
...input.url === void 0 ? {} : { url: input.url },
|
|
2307
|
+
json: input.json,
|
|
2722
2308
|
...input.memberships === void 0 ? {} : { memberships: input.memberships },
|
|
2723
2309
|
...input.gucPrefix === void 0 ? {} : { gucPrefix: input.gucPrefix },
|
|
2724
2310
|
policyPerRole: input.policyPerRole,
|
|
@@ -2729,6 +2315,7 @@ function generateInput(input, target, dialect) {
|
|
|
2729
2315
|
...input.fields === void 0 ? {} : { fields: input.fields },
|
|
2730
2316
|
revokeColumns: input.revokeColumns,
|
|
2731
2317
|
...input.split === void 0 ? {} : { split: input.split },
|
|
2318
|
+
...isSupabaseSchemas(input.schemas) ? { schemas: input.schemas } : {},
|
|
2732
2319
|
...input.grantsOut === void 0 ? {} : { grantsOut: input.grantsOut },
|
|
2733
2320
|
...input.seedsOut === void 0 ? {} : { seedsOut: input.seedsOut },
|
|
2734
2321
|
...input.backfillOut === void 0 ? {} : { backfillOut: input.backfillOut },
|
|
@@ -2761,20 +2348,24 @@ async function migrate(input) {
|
|
|
2761
2348
|
output: generated.output
|
|
2762
2349
|
};
|
|
2763
2350
|
await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls migrate", input.cwd);
|
|
2764
|
-
const { migrateTarget, runRlsMigrate } = await import("./migrate-
|
|
2351
|
+
const { migrateTarget, runRlsMigrate } = await import("./migrate-CIq4-E9F.js");
|
|
2765
2352
|
return runRlsMigrate({
|
|
2766
2353
|
cwd: input.cwd,
|
|
2767
2354
|
sql: input.sql,
|
|
2768
2355
|
config,
|
|
2769
|
-
target:
|
|
2356
|
+
target: {
|
|
2357
|
+
...migrateTarget(generated),
|
|
2358
|
+
...input.config.rls?.roleValues === void 0 ? {} : { roleValues: input.config.rls.roleValues }
|
|
2359
|
+
},
|
|
2770
2360
|
write: input.write,
|
|
2361
|
+
force: input.forceRewrite === true,
|
|
2771
2362
|
json: input.json,
|
|
2772
2363
|
retireOut: input.retireOut,
|
|
2773
2364
|
...input.io.now === void 0 ? {} : { now: input.io.now() }
|
|
2774
2365
|
});
|
|
2775
2366
|
}
|
|
2776
2367
|
/** `verify --introspect` with `--helpers-only`: helpers and seeds exactly, hand-written policies by their keys. */
|
|
2777
|
-
async function introspectHelpersOnly(db, expected, generated, connect) {
|
|
2368
|
+
async function introspectHelpersOnly(db, expected, generated, connect, readOnlyActors) {
|
|
2778
2369
|
const schema = generated.schema ?? "permdock";
|
|
2779
2370
|
try {
|
|
2780
2371
|
const actual = await introspectRls(db, expected, connect);
|
|
@@ -2784,7 +2375,8 @@ async function introspectHelpersOnly(db, expected, generated, connect) {
|
|
|
2784
2375
|
schema,
|
|
2785
2376
|
seeds: generated.seeds ?? [],
|
|
2786
2377
|
permissions: generated.keys?.permissions ?? [],
|
|
2787
|
-
rowConditions: generated.keys?.rowConditions ?? []
|
|
2378
|
+
rowConditions: generated.keys?.rowConditions ?? [],
|
|
2379
|
+
readOnlyActors
|
|
2788
2380
|
}, await introspectMixed(db, schema, connect));
|
|
2789
2381
|
const drift = [...helpers, ...mixed.drift];
|
|
2790
2382
|
const info = mixed.info.map((line) => `info: ${line}`);
|
|
@@ -2811,7 +2403,7 @@ async function introspectHelpersOnly(db, expected, generated, connect) {
|
|
|
2811
2403
|
async function introspect(input) {
|
|
2812
2404
|
if (input.db === void 0) return {
|
|
2813
2405
|
code: 2,
|
|
2814
|
-
output: "PermDock CLI: rls verify --introspect needs --db"
|
|
2406
|
+
output: "PermDock CLI: rls verify --introspect needs a database: --db-url-stdin, DATABASE_URL or SUPABASE_DB_URL"
|
|
2815
2407
|
};
|
|
2816
2408
|
const dialect = asDialect(input.dialect ?? input.config.rls?.dialect);
|
|
2817
2409
|
if (dialect === void 0) return {
|
|
@@ -2828,7 +2420,7 @@ async function introspect(input) {
|
|
|
2828
2420
|
output: generated.output
|
|
2829
2421
|
};
|
|
2830
2422
|
const expected = expectedRls(generated.policies, generated.text, generated.indexes);
|
|
2831
|
-
if (generated.helpersOnly === true) return introspectHelpersOnly(input.db, expected, generated, input.connect);
|
|
2423
|
+
if (generated.helpersOnly === true) return introspectHelpersOnly(input.db, expected, generated, input.connect, readOnlyActorKinds(input.config.rls?.readOnlyActors) !== void 0);
|
|
2832
2424
|
try {
|
|
2833
2425
|
const actual = await introspectRls(input.db, expected, input.connect);
|
|
2834
2426
|
const drift = diffRls(expected, actual, { columnGrants: (input.fields ?? input.config.rls?.fields) === "views" });
|
|
@@ -2851,6 +2443,11 @@ async function runRls(input) {
|
|
|
2851
2443
|
code: 2,
|
|
2852
2444
|
output: RLS_HELP
|
|
2853
2445
|
};
|
|
2446
|
+
const badSchemas = schemasRefusal(`rls ${action}`, input.schemas);
|
|
2447
|
+
if (badSchemas !== void 0) return {
|
|
2448
|
+
code: 2,
|
|
2449
|
+
output: badSchemas
|
|
2450
|
+
};
|
|
2854
2451
|
switch (action) {
|
|
2855
2452
|
case "generate": {
|
|
2856
2453
|
const target = asTarget(input.target);
|
|
@@ -2871,7 +2468,7 @@ async function runRls(input) {
|
|
|
2871
2468
|
}
|
|
2872
2469
|
case "import": {
|
|
2873
2470
|
await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls import", input.cwd);
|
|
2874
|
-
const { runRlsImport } = await import("./import-
|
|
2471
|
+
const { runRlsImport } = await import("./import-wyqXUyjm.js");
|
|
2875
2472
|
return runRlsImport({
|
|
2876
2473
|
cwd: input.cwd,
|
|
2877
2474
|
config: input.config,
|
|
@@ -2889,6 +2486,7 @@ async function runRls(input) {
|
|
|
2889
2486
|
cwd: input.cwd,
|
|
2890
2487
|
json: input.json,
|
|
2891
2488
|
env: input.io.env ?? process.env,
|
|
2489
|
+
...input.io.signal === void 0 ? {} : { signal: input.io.signal },
|
|
2892
2490
|
...input.db === void 0 ? {} : { db: input.db },
|
|
2893
2491
|
...input.exec === void 0 ? {} : { exec: input.exec }
|
|
2894
2492
|
});
|