permdock 0.5.0 → 0.7.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 +1 -0
- package/dist/{2020-C9VN-x8c.js → 2020-CJbO8Bvz.js} +1 -3
- package/dist/a2a/index.d.ts +5 -5
- package/dist/a2a/index.js +4 -4
- package/dist/{adoption-scan-B4R5fnMU.js → adoption-scan-D-feE7BE.js} +1 -1
- package/dist/agent/index.d.ts +3 -3
- package/dist/agent/index.js +1 -1
- package/dist/ai-sdk/index.d.ts +21 -6
- package/dist/ai-sdk/index.js +19 -9
- package/dist/{approval-prompt-Djo_PyZc.d.ts → approval-prompt-K0faOLFp.d.ts} +2 -2
- package/dist/approvals/index.d.ts +4 -4
- package/dist/approvals/index.js +4 -4
- package/dist/{approvals-DJ6Yd5z1.js → approvals-CMRfsbBh.js} +74 -48
- package/dist/{arazzo-B6lebmiQ.js → arazzo-ClrF3PVb.js} +2 -2
- package/dist/{arazzo-BWhNF-k0.js → arazzo-rdeJ8n1J.js} +4 -4
- package/dist/{args-IIolxxKA.js → args-CC3OwyHW.js} +112 -2
- package/dist/astro/index.d.ts +6 -6
- package/dist/astro/index.js +2 -2
- package/dist/{audience-Dg8XsRaO.js → audience-BrMs8c4o.js} +1 -1
- package/dist/{auth-DRLEizbL.js → auth-CscUX8Sy.js} +3 -4
- package/dist/authzen/index.d.ts +162 -22
- package/dist/authzen/index.js +315 -146
- package/dist/{automation-BMZPIVUM.d.ts → automation-DOtAFWdy.d.ts} +3 -3
- package/dist/{automation-DVjOckjV.js → automation-ySsxp_Yp.js} +1 -1
- package/dist/base-url-BQbddZO5.js +9 -0
- package/dist/better-auth/index.d.ts +2 -2
- package/dist/better-auth/index.js +2 -2
- package/dist/better-supabase/index.d.ts +50 -8
- package/dist/better-supabase/index.js +94 -32
- package/dist/{bind-CZzhx-Vj.js → bind-D1nO6fiC.js} +5 -5
- package/dist/{bind--9EFVYWF.d.ts → bind-JDuvxjuW.d.ts} +7 -7
- package/dist/{build-instance-BMnAudHn.js → build-instance-DxE7czo9.js} +1 -1
- package/dist/{canonical-json-BC8_nMTw.js → canonical-json-DwilZ1iB.js} +6 -3
- package/dist/{capability-BSslw1QV.d.ts → capability-D4UC9TTW.d.ts} +2 -2
- package/dist/{capability-N1K9IpqS.js → capability-L7NnfhiE.js} +1 -1
- package/dist/{casbin-Cr7RLtA3.js → casbin-BHijLuHY.js} +2 -2
- package/dist/{casbin-Dyu2gnUD.js → casbin-BW9pS47J.js} +1 -1
- package/dist/{casl-64gpT-8x.js → casl-CmJlgziS.js} +2 -2
- package/dist/{casl-DjEDhTCP.js → casl-UbI8v-Wq.js} +1 -1
- package/dist/catalog/index.d.ts +70 -10
- package/dist/catalog/index.js +112 -6
- package/dist/catalog-BCCjSAdH.js +1 -0
- package/dist/{catalog-R_A__RsH.js → catalog-BaMQfQ6Y.js} +3 -3
- package/dist/{catalog-BECMPrwc.js → catalog-D69WhFii.js} +43 -13
- package/dist/{catalog-doc-C9wjsfNY.js → catalog-doc-CV9s6B1S.js} +28 -5
- package/dist/{catalog-fingerprint-CCP0-yQS.js → catalog-fingerprint-C144oW4M.js} +1 -1
- package/dist/{create-BWRSXgOF.js → challenge-nGWma08S.js} +122 -25
- package/dist/chat/index.d.ts +1 -1
- package/dist/chat/index.js +2 -2
- package/dist/chat-sdk/index.d.ts +3 -3
- package/dist/checks-BC_0mPIq.js +37 -0
- package/dist/claude-agent/index.d.ts +3 -3
- package/dist/claude-agent/index.js +2 -2
- package/dist/clerk/index.d.ts +3 -3
- package/dist/clerk/index.js +1 -1
- package/dist/cli/bin.js +2 -2
- package/dist/cli/index.d.ts +3 -3
- package/dist/cli/index.js +3 -3
- package/dist/cloud/index.d.ts +127 -11
- package/dist/cloud/index.js +251 -3
- package/dist/{cloud-CIoHNevm.js → cloud-CO8pYtwr.js} +5 -5
- package/dist/{codemod-Cxvvc70T.js → codemod-BP6n8N9L.js} +3 -3
- package/dist/{codemod-Bmj9dWMH.js → codemod-i4N2BhDc.js} +3 -3
- package/dist/{collect-CrZJJnEF.js → collect-BaWeMjwV.js} +17 -8
- package/dist/{collect-DloHbDVa.js → collect-DNm3oKOk.js} +6 -5
- package/dist/collect-k_Dulxr5.js +1 -0
- package/dist/compile/index.d.ts +1 -1
- package/dist/compile/index.js +1 -1
- package/dist/{compile-CMqMx_IN.js → compile-BAEO5bOk.js} +2 -2
- package/dist/{compile-Ckm2HqBZ.d.ts → compile-COM2VKuN.d.ts} +3 -3
- package/dist/{completion-BqhRBJD8.js → completion-BNdVf7X2.js} +3 -3
- package/dist/{config-bs_kZeYq.js → config-CoQHDrDi.js} +7 -3
- package/dist/{config-B46rm9sc.js → config-DGZzMV1B.js} +2 -2
- package/dist/{config-report-DUgj62Ai.js → config-report-Cm7UITNA.js} +3 -3
- package/dist/connector-schema-_VCSsdtD.js +433 -0
- package/dist/{context-bbFHR-0D.js → context--uM9V3Ui.js} +2 -2
- package/dist/convex/index.d.ts +5 -5
- package/dist/convex/index.js +2 -2
- package/dist/core-B5-w6Kw9.js +335 -0
- package/dist/core-CQkE8Esr.d.ts +179 -0
- package/dist/{create-0VZ7Pdiu.d.ts → create-B2-CQqCI.d.ts} +5 -5
- package/dist/{create-C3b4B5eF.js → create-Cb86Z8Im.js} +9 -8
- package/dist/{create-DnrmLnLW.js → create-DJrlhEp9.js} +5 -5
- package/dist/{create-BFfm17UA.d.ts → create-QHuuPw3q.d.ts} +7 -7
- package/dist/{create-BDS7wc3y.d.ts → create-UTnUVETj.d.ts} +30 -10
- package/dist/{credential-DM2um3ha.js → credential-CIM7wHwA.js} +4 -4
- package/dist/{credentials-DPyHmYz5.js → credentials-BpuTwd7a.js} +1 -1
- package/dist/{deciding-columns-Sq12mEA6.js → deciding-columns-DRo2QM6E.js} +13 -2
- package/dist/{delegation-DaXVdDhM.js → delegation-C5rOdTp5.js} +5 -5
- package/dist/delivery/index.d.ts +5 -5
- package/dist/delivery/index.js +4 -3
- package/dist/{delivery-DCAgNyCn.js → delivery-rwOKLWRm.js} +51 -2
- package/dist/{denied-text-D8jDHgsl.d.ts → denied-text-D4qkEtFz.d.ts} +2 -2
- package/dist/{describe-BybMRY7A.js → describe-DZlnEtLy.js} +1 -0
- package/dist/{describe-BZUsixiL.d.ts → describe-O0SwYIK8.d.ts} +2 -2
- package/dist/{diff-CxZo9pTq.js → diff-Btr1c1Ow.js} +2 -2
- package/dist/{diff-DCePiN6M.js → diff-DzT-xD13.js} +10 -10
- package/dist/{doctor-DFjUxf2f.js → doctor-BU1szn63.js} +241 -49
- package/dist/{doctor-CxiwSfqH.js → doctor-BoK_y0KO.js} +4 -4
- package/dist/drizzle/index.d.ts +3 -3
- package/dist/drizzle/index.js +1 -1
- package/dist/elysia/index.d.ts +5 -5
- package/dist/elysia/index.js +3 -3
- package/dist/{errors-C2PXIv4c.d.ts → errors-B_Qg9D3e.d.ts} +2 -2
- package/dist/{errors-CWTY6uih.js → errors-DpP_U6Bf.js} +1 -0
- package/dist/{evaluate-_R8QkWaf.js → evaluate-B_oqFbWC.js} +130 -34
- package/dist/{evaluations-16UqqQ6W.d.ts → evaluations-BCddEOfO.d.ts} +4 -4
- package/dist/{evaluations-DZ2CbV5k.js → evaluations-DReoiVBw.js} +4 -4
- package/dist/eve/index.d.ts +5 -5
- package/dist/eve/index.js +3 -3
- package/dist/express/index.d.ts +5 -5
- package/dist/express/index.js +3 -3
- package/dist/fastify/index.d.ts +5 -5
- package/dist/fastify/index.js +3 -3
- package/dist/{fetch-adapter-qKJBuRqy.d.ts → fetch-adapter-CuhyYwC1.d.ts} +4 -4
- package/dist/{fetch-adapter-g0ypLkID.js → fetch-adapter-De-u6vhJ.js} +3 -3
- package/dist/{fields-D9tPVUUS.js → fields-ClcQLrVc.js} +48 -13
- package/dist/{files-C5ZI27eV.js → files-acUbNwWt.js} +15 -2
- package/dist/{fixtures-B7XrLeY7.js → fixtures-BTMW6zgU.js} +2 -2
- package/dist/{flags-C3DktBDY.js → flags-aaeRAdiD.js} +2 -2
- package/dist/{from-snapshot-D2lZt4Dd.js → from-snapshot-Bz469dIh.js} +17 -10
- package/dist/generate-3FNO5P5Q.js +159 -0
- package/dist/{grantee-Cn1HyUsI.js → grantee-BrRANrpT.js} +14 -4
- package/dist/{grantee-conditions-CcXf8hMM.js → grantee-conditions-C6P1Tvol.js} +1 -1
- package/dist/{grantee-match-DHArqY82.js → grantee-match-0RtHWQDx.js} +3 -3
- package/dist/h3/index.d.ts +4 -4
- package/dist/h3/index.js +3 -3
- package/dist/{helpers-DTcZOzhF.js → helpers-DY3vXuGs.js} +3 -3
- package/dist/hono/index.d.ts +5 -5
- package/dist/hono/index.js +3 -3
- package/dist/{hosted-DBDX0M-P.d.ts → hosted-BmjzxQhp.d.ts} +14 -2
- package/dist/{hosted-BZIAYvon.js → hosted-XBgU6RGT.js} +34 -12
- package/dist/{http-DXJ37RDA.js → http-Ca3Sr5x1.js} +1 -1
- package/dist/{import-DMmOG27O.js → import-Bz0GefWg.js} +5 -5
- package/dist/{import-CNV7I2Dw.js → import-DB2EG_iy.js} +3 -3
- package/dist/{index-CxGUswIC.d.ts → index-BE8y3moU.d.ts} +50 -8
- package/dist/{index-Chrs5fQ-.d.ts → index-BSfRl0hZ.d.ts} +6 -6
- package/dist/{index-DOVf7RJv.d.ts → index-BTkwrK7v.d.ts} +155 -17
- package/dist/index-CMiPH2Tv.d.ts +206 -0
- package/dist/{index-SGdIQyzc.d.ts → index-DH9DUDEM.d.ts} +18 -3
- package/dist/{index-BcHcPyIo.d.ts → index-DLwcuCN_.d.ts} +86 -7
- package/dist/index.d.ts +18 -17
- package/dist/index.js +27 -25
- package/dist/{init-GG6Jh8HD.js → init-5TXNji88.js} +5 -5
- package/dist/{init-DeMYsnYJ.js → init-B-4t1NzC.js} +4 -4
- package/dist/{instance-options-Omfd793B.d.ts → instance-options-CU0YQCLH.d.ts} +1 -1
- package/dist/{instance-snapshot-Cxq4ygFc.js → instance-snapshot-CjcwnEiV.js} +20 -14
- package/dist/jwt/index.d.ts +83 -5
- package/dist/jwt/index.js +143 -5
- package/dist/{kernel-5JatXyND.js → kernel-DVExUwkp.js} +138 -12
- package/dist/kysely/index.d.ts +3 -3
- package/dist/kysely/index.js +1 -1
- package/dist/load-DrMHXzgN.js +1 -0
- package/dist/{load-DFS_ry0W.js → load-DxroEzH1.js} +2 -2
- package/dist/{login-Dfe5RlYk.js → login-ClSJh5t5.js} +4 -4
- package/dist/{login-imhuwvn3.js → login-WC9HPJpU.js} +6 -10
- package/dist/{manifest-C8IzW3Do.js → manifest-B79YuF2T.js} +6 -2
- package/dist/{manifest-mx5ghSFM.d.ts → manifest-BVhExYhw.d.ts} +23 -2
- package/dist/{manifest-CtKR3GXL.js → manifest-Dv6q4i5s.js} +54 -35
- package/dist/{manifest-schema-C0kK_8V0.js → manifest-schema-Cv492Qev.js} +37 -7
- package/dist/{map-claims-D3oRmTk0.js → map-claims-etGIS_Dv.js} +14 -11
- package/dist/{map-error-TCAdw-eW.js → map-error-BQGyDm_M.js} +1 -1
- package/dist/mcp/index.d.ts +64 -10
- package/dist/mcp/index.js +151 -22
- package/dist/{migrate-BShP7Vf4.js → migrate-Dodel0IJ.js} +77 -24
- package/dist/nest/index.d.ts +5 -5
- package/dist/nest/index.js +3 -3
- package/dist/next/index.d.ts +9 -9
- package/dist/next/index.js +5 -5
- package/dist/next/plugin.d.ts +1 -1
- package/dist/next/plugin.js +2 -2
- package/dist/node/index.d.ts +5 -5
- package/dist/node/index.js +2 -2
- package/dist/openai/index.d.ts +5 -5
- package/dist/openai/index.js +3 -3
- package/dist/openapi/index.d.ts +14 -4
- package/dist/openapi/index.js +1 -1
- package/dist/{openapi-ahr8rHu3.js → openapi-DMocl1bh.js} +4 -4
- package/dist/{openapi-B7HZ0f-r.js → openapi-Ddb4628T.js} +2 -7
- package/dist/{openapi-DTaEmTSW.js → openapi-ztVaux6j.js} +8 -8
- package/dist/{operation-findings-DgATDpvh.js → operation-findings-kRcEp8lE.js} +1 -1
- package/dist/{operations-CxybZErY.d.ts → operations-BNLes61x.d.ts} +1 -1
- package/dist/orpc/index.d.ts +6 -6
- package/dist/orpc/index.js +4 -4
- package/dist/otel/index.d.ts +3 -3
- package/dist/otel/index.js +1 -1
- package/dist/{output-Dyw8fyPF.js → output-DgSDLrta.js} +7 -3
- package/dist/{output-files-DhRBBE4n.js → output-files-CCMdcFWb.js} +2 -2
- package/dist/output-files-DPAY7-GX.js +1 -0
- package/dist/{ownership-BatFgAA4.js → ownership-Z2LpxsGa.js} +1349 -883
- package/dist/parse-Ddhz2xf4.js +148 -0
- package/dist/{parse-local-manifest-CVSs6J9I.js → parse-local-manifest-T1Q8X3A2.js} +24 -1
- package/dist/{parse-local-manifest-DgqtruPn.d.ts → parse-local-manifest-e4mdwAKd.d.ts} +1 -1
- package/dist/pdp/index.d.ts +3 -3
- package/dist/pdp/index.js +7 -3
- package/dist/peer-12Q5I_RH.js +976 -0
- package/dist/{peer-CRvC7Hg1.js → peer-DOzzMfZL.js} +1 -1
- package/dist/{permdock-B9JxkBwC.js → permdock-BfWEeMqk.js} +177 -49
- package/dist/{permdock-D7eeLJER.d.ts → permdock-Dco6dyCK.d.ts} +34 -5
- package/dist/{permission-registry-Br4jSNVi.js → permission-registry-DpPtSKNZ.js} +1 -1
- package/dist/{vocabulary-D6lPdb4Z.d.ts → permission-types-DhNEmoRG.d.ts} +4 -62
- package/dist/{permissions-oEWfl-kl.d.ts → permissions-CoqCOfJ3.d.ts} +2 -1
- package/dist/{permissions-B-wCP5ZC.js → permissions-CyH2MV_B.js} +1 -1
- package/dist/{permissions-CZKiUfFM.js → permissions-DArULThq.js} +4 -4
- package/dist/pkce-5h8NgMQJ.js +21 -0
- package/dist/{policies-CO1oOymN.js → policies-B2lZIurj.js} +3 -3
- package/dist/{policy-Czo4n3Hs.d.ts → policy-B7dg_yzu.d.ts} +2 -2
- package/dist/{policy-DLZJ5gAp.js → policy-DaJy4nK4.js} +17 -8
- package/dist/{policy-from-catalog-DzhEc3Y9.js → policy-from-catalog-DFWYkNdu.js} +9 -9
- package/dist/{policy-types-DzwivBgR.d.ts → policy-types-B-F-qAZk.d.ts} +109 -5
- package/dist/postgres-store-BL9M_CA1.js +1001 -0
- package/dist/{postgrest-NUqZT-RB.d.ts → postgrest-CYacM_u7.d.ts} +12 -1
- package/dist/{postgrest-CW0IqC-U.js → postgrest-r5faDIzD.js} +47 -5
- package/dist/{powersync-BHfuehf3.js → powersync-13odBwiM.js} +3 -3
- package/dist/powersync-53ryOZip.js +1059 -0
- package/dist/powersync-CExtTLyA.js +1 -0
- package/dist/preset/index.js +1 -1
- package/dist/{preset-DZdU0tne.js → preset-CHTbd-DQ.js} +2 -2
- package/dist/prisma/index.d.ts +2 -2
- package/dist/prisma/index.js +1 -1
- package/dist/{problem-details-DXlt-vks.d.ts → problem-details-Tcfmj3bV.d.ts} +1 -1
- package/dist/prompt-DwtVemER.js +935 -0
- package/dist/{protected-DEPHc2xV.d.ts → protected-BwZYL1X_.d.ts} +6 -5
- package/dist/{protected-DfjY5mLA.js → protected-DXE3cQYg.js} +2 -2
- package/dist/{provider-CC-u3bYP.js → provider-BQsTYYy0.js} +2 -2
- package/dist/{provider-DMVt6yYn.d.ts → provider-Csk-THAn.d.ts} +1 -1
- package/dist/{provider-store-B5Yrkhzr.js → provider-store-IgqQipCO.js} +2 -2
- package/dist/{pull-BgIfno6w.js → pull-CN5OLxrF.js} +7 -7
- package/dist/pull-Cty6Ovyu.js +1 -0
- package/dist/{push-CmFVBOeI.js → push-tVdG8dUL.js} +54 -14
- package/dist/{pushed-policy-BImMTTIM.d.ts → pushed-policy-BbcY9r_4.d.ts} +1 -1
- package/dist/{pushed-policy-DobP7AHI.js → pushed-policy-CmYeSQqq.js} +1 -1
- package/dist/react/index.d.ts +6 -6
- package/dist/react/index.js +3 -3
- package/dist/react/provider-client.d.ts +1 -1
- package/dist/react/provider-client.js +1 -1
- package/dist/react-native/index.d.ts +25 -11
- package/dist/react-native/index.js +214 -24
- package/dist/{refs-K6eWUJ0H.js → refs-CNJBiTFo.js} +15 -1
- package/dist/{replay-D7iKcLFe.d.ts → replay-DCDD_ZfL.d.ts} +1 -1
- package/dist/{request-context-BTRTG7OT.js → request-context-BykKLmyE.js} +2 -2
- package/dist/resend/index.d.ts +4 -4
- package/dist/{retire-CALjIQ7e.js → retire-Wj6dkyDE.js} +2 -2
- package/dist/{rls-BA8an5Ap.js → rls-Ck_PocSp.js} +32 -8
- package/dist/{rls-j6t43t7n.js → rls-CxBZb4rZ.js} +478 -534
- package/dist/{role-builder-CXbis5-X.js → role-builder-C1VJzvcr.js} +3 -3
- package/dist/rolldown-runtime-C3I60J0A.js +4 -0
- package/dist/{rpc-aPyNi6tA.js → rpc-Bk1bE-ch.js} +1 -1
- package/dist/run-Bz7DXGEv.js +1 -0
- package/dist/{run-ticket-sxFI6LAk.js → run-ticket-B5d_63Ma.js} +22 -1
- package/dist/{run-ticket-BjhTOi6u.d.ts → run-ticket-BpNi7BCi.d.ts} +8 -3
- package/dist/{run-DOfMtc9V.js → run-uLD2aWuO.js} +7 -7
- package/dist/{runtime-D3VTAiCA.js → runtime-Vif3_-zf.js} +1 -1
- package/dist/{saas-FO-Q8oj0.js → saas-Az_Kz-4D.js} +7 -7
- package/dist/{scan-BOI5tJ-6.js → scan-DSljDE1Y.js} +1 -1
- package/dist/{schema-yLuBWWsh.js → schema-gIcCx7zM.js} +1 -1
- package/dist/scim/index.d.ts +3 -2
- package/dist/scim/index.js +3 -1
- package/dist/scim-C7JYhySg.js +44 -0
- package/dist/{scim-ClHtpyV3.js → scim-YNieBO1U.js} +3 -537
- package/dist/{scopes-CUPSjo9q.js → scopes-BHsc-wp-.js} +1 -1
- package/dist/server/index.d.ts +11 -11
- package/dist/server/index.js +7 -7
- package/dist/{server-DqCT4CgZ.js → server-Bnga6I50.js} +3 -3
- package/dist/{server-types-BLd4MnBX.d.ts → server-types-C4gZUEZy.d.ts} +9 -9
- package/dist/{shared-DvvFzYXe.js → shared-C8LjUdVU.js} +1 -1
- package/dist/{shared-CD11nBbC.js → shared-fMKOv8hP.js} +46 -39
- package/dist/{sink-Bj_lDUDq.d.ts → sink-Cp_Eot9W.d.ts} +4 -4
- package/dist/{skills-B6lVdD_W.js → skills-Dcjw9_1C.js} +5 -5
- package/dist/{skills-CTYwkl44.js → skills-g_3BS9NC.js} +3 -3
- package/dist/{snapshot-CXUWfsNy.js → snapshot-CbGmYiJx.js} +4 -4
- package/dist/{snapshot-cache-DZHQ0KF6.js → snapshot-cache-DT2Kx1F8.js} +1 -1
- package/dist/{snapshot-cache-DqK9eDmM.d.ts → snapshot-cache-Pahd3IYJ.d.ts} +1 -1
- package/dist/solid/index.d.ts +6 -5
- package/dist/solid/index.js +4 -4
- package/dist/solid-start/index.d.ts +6 -6
- package/dist/solid-start/index.js +3 -3
- package/dist/{sources-vs5dGOsS.js → sources-B-zYB4GS.js} +111 -25
- package/dist/{sources-BV9j0DeZ.d.ts → sources-DEEuGb8S.d.ts} +63 -14
- package/dist/sources-qoIJQrJv.js +11 -0
- package/dist/{sql-B5T1IL83.js → sql-B96movIM.js} +5 -1
- package/dist/sql-CDEPCA0g.d.ts +10 -0
- package/dist/{src-DLfUWFAI.js → src-DLFrtrrC.js} +154 -53
- package/dist/ssf/index.d.ts +4 -4
- package/dist/ssf/index.js +640 -211
- package/dist/{standard-webhooks-6lg8v_is.d.ts → standard-webhooks-B_qRg0ws.d.ts} +2 -2
- package/dist/{store-4u_hxjWa.js → store-DSP9oGvh.js} +15 -7
- package/dist/{store-options-BqEakYRa.js → store-options-B_poyYvg.js} +6 -6
- package/dist/{stores-ClZUgeoP.d.ts → stores-BAZwSYZA.d.ts} +6 -5
- package/dist/{stores-BbsD4fAR.js → stores-BqEuHC05.js} +2 -2
- package/dist/{stream-7-xmnf3U.d.ts → stream-2Vwhl7Er.d.ts} +2 -2
- package/dist/{style-Uao76wO7.js → style-Cqo7-73x.js} +1 -0
- package/dist/{style-DiKWnAvn.js → style-Ds1gNm77.js} +1 -1
- package/dist/supabase/index.d.ts +8 -7
- package/dist/supabase/index.js +5 -5
- package/dist/supabase/middleware.d.ts +6 -6
- package/dist/supabase/middleware.js +6 -6
- package/dist/{supabase-Dxlp8A94.js → supabase--0aJdLEd.js} +9 -7
- package/dist/{supabase-9pqI57pW.js → supabase-Bqp7iIY2.js} +3 -3
- package/dist/supabase-hook-DJxC4PoP.js +1 -0
- package/dist/{supabase-hook-DdDQhMlw.js → supabase-hook-Dg0SrQWR.js} +1069 -123
- package/dist/supautils-MESd81bF.js +689 -0
- package/dist/{support-session-C6xJQYut.js → support-session-Cna49-3f.js} +2 -2
- package/dist/svelte/index.d.ts +5 -5
- package/dist/svelte/index.js +5 -5
- package/dist/svelte/runtime.d.ts +3 -3
- package/dist/svelte/runtime.js +3 -3
- package/dist/svelte/source.d.ts +5 -5
- package/dist/svelte/source.js +2 -2
- package/dist/sveltekit/index.d.ts +6 -6
- package/dist/sveltekit/index.js +2 -2
- package/dist/tanstack-start/index.d.ts +6 -6
- package/dist/tanstack-start/index.js +2 -2
- package/dist/{tenancy-DN2QAx9z.js → tenancy-CmeBQ6Nv.js} +12 -3
- package/dist/terminal/index.d.ts +6 -6
- package/dist/terminal/index.js +3 -3
- package/dist/testing/core.d.ts +2 -0
- package/dist/testing/core.js +1 -0
- package/dist/testing/fixtures/index.d.ts +12 -0
- package/dist/testing/fixtures/index.js +770 -0
- package/dist/testing/index.d.ts +41 -174
- package/dist/testing/index.js +241 -731
- package/dist/testing/saas/index.d.ts +5 -5
- package/dist/testing/saas/index.js +2 -2
- package/dist/testing/saas/permissions.d.ts +1 -1
- package/dist/testing/saas/permissions.js +1 -1
- package/dist/text-diff-DhITNCnD.js +444 -0
- package/dist/{token-BLZcjQgt.js → token-2qYDvXz2.js} +1 -1
- package/dist/{tool-refusal-CMLN5xhn.js → tool-refusal-reAhIzdl.js} +1 -1
- package/dist/trpc/index.d.ts +6 -6
- package/dist/trpc/index.js +3 -3
- package/dist/{types-5Ut_pbKy.d.ts → types-BWZybqs9.d.ts} +112 -8
- package/dist/{types-CZTnPFzM.d.ts → types-BXMYB7BY.d.ts} +2 -2
- package/dist/{types-CdlxsYQh.d.ts → types-BfVmNJO5.d.ts} +14 -3
- package/dist/{types-BqAKcRNW.d.ts → types-CIeZcG8v.d.ts} +2 -2
- package/dist/{types-BNORmDM8.d.ts → types-CKV71kKh.d.ts} +4 -4
- package/dist/{types-tIgIeIKJ.d.ts → types-CQVEQawv.d.ts} +4 -4
- package/dist/{types-BjV-fqcA.d.ts → types-CeIscIKp.d.ts} +4 -4
- package/dist/{types-_sHy8BQf.d.ts → types-CfiKC5aN.d.ts} +1 -1
- package/dist/{types-ClZaWzfb.d.ts → types-D3A19w4A.d.ts} +8 -4
- package/dist/{types-C8-KNsFf.d.ts → types-DBHftjgS.d.ts} +15 -8
- package/dist/{types-CCpc2Dcv.d.ts → types-DC44-hqb.d.ts} +2 -2
- package/dist/{types-fbZ7JEXr.d.ts → types-DXmwu78Z.d.ts} +4 -4
- package/dist/{types-DiQjupG2.d.ts → types-cTos-H7p.d.ts} +2 -2
- package/dist/{types-DX0LaKTb.d.ts → types-g7Oy6Zg4.d.ts} +3 -3
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +2 -2
- package/dist/{usage-T5mXq98v.js → usage-Bybmck7f.js} +7 -7
- package/dist/{usage-DHtE8j4E.js → usage-CDaLXCfL.js} +2 -2
- package/dist/usage-y-kBNdqs.js +1 -0
- package/dist/{validation-error-Bg5Tpluk.d.ts → validation-error-CQpoquwj.d.ts} +1 -1
- package/dist/{verify-DbCL9bnB.js → verify-pe8rL_Qw.js} +350 -83
- package/dist/{views-DTIIO-oQ.js → views-BB8E9dTP.js} +1 -1
- package/dist/vocabulary-Bg3eeT2H.d.ts +67 -0
- package/dist/vue/index.d.ts +6 -5
- package/dist/vue/index.js +4 -4
- package/dist/{waiter-CKy4MQSY.d.ts → waiter-DwEyouan.d.ts} +2 -2
- package/dist/watch-Bd9mx3rd.js +1 -0
- package/dist/{watch-CszoTJMq.js → watch-C-4VmFlL.js} +3 -3
- package/dist/webmcp/index.d.ts +2 -2
- package/dist/webmcp/index.js +2 -2
- package/dist/{where-scope-Dv-WTP-w.js → where-scope-wq2coopv.js} +41 -21
- package/dist/{who-can-D0QoYbEb.js → who-can-BwNsI_Js.js} +6 -6
- package/dist/workflow/index.d.ts +3 -3
- package/dist/workflow/index.js +5 -5
- package/dist/workflow-sdk/index.d.ts +5 -5
- package/dist/{wrap-Bd1Tejur.d.ts → wrap-B6R0cMRL.d.ts} +2 -2
- package/dist/{wrap-gX61EtpB.js → wrap-CiSc_KCq.js} +8 -2
- package/package.json +22 -11
- package/schemas/cli-result-v1.json +1 -0
- package/schemas/cloud-api-v1.json +152 -7
- package/schemas/connector-v1.json +8 -0
- package/schemas/supabase-claims-v1.json +5 -0
- package/schemas/supabase-manifest-v1.json +276 -75
- package/skills/permdock/SKILL.md +1 -0
- package/skills/permdock-agents/references/runtimes.md +3 -1
- package/skills/permdock-approvals/references/stores-and-resume.md +2 -1
- package/skills/permdock-data/references/queries-and-rls.md +5 -2
- package/skills/permdock-tenancy/SKILL.md +1 -1
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +2 -0
- package/skills/permdock-wire/references/adapters.md +14 -0
- package/skills/permdock-wire/references/policy-options.md +1 -0
- package/dist/catalog-DjhMYYCq.js +0 -1
- package/dist/collect-FlXJlhus.js +0 -1
- package/dist/index-D8EovfsP.d.ts +0 -91
- package/dist/load-BxEtwlBS.js +0 -1
- package/dist/output-files-B3IZJksC.js +0 -1
- package/dist/parse-Cxq0WxrA.js +0 -53
- package/dist/peer-C8swJ2GH.js +0 -50
- package/dist/powersync-CbzkqL_M.js +0 -1
- package/dist/powersync-bYUDLOAO.js +0 -538
- package/dist/prompt-DfZLJLIn.js +0 -32
- package/dist/pull-ByOks_Tg.js +0 -1
- package/dist/run-CNsApDpd.js +0 -1
- package/dist/supabase-hook-CXcGpHqq.js +0 -1
- package/dist/supautils-B6HYL9eL.js +0 -91
- package/dist/text-diff-BQcUapOb.js +0 -15
- package/dist/usage-CHOtwL19.js +0 -1
- package/dist/watch-Cb-NcaUw.js +0 -1
|
@@ -1,30 +1,30 @@
|
|
|
1
1
|
import { t as compact } from "./compact-CAf557ZB.js";
|
|
2
|
-
import { c as listPermissions, u as renamedKeys } from "./permission-registry-
|
|
2
|
+
import { c as listPermissions, u as renamedKeys } from "./permission-registry-DpPtSKNZ.js";
|
|
3
3
|
import { i as dateAt } from "./time-DkWjdL98.js";
|
|
4
|
-
import {
|
|
5
|
-
import { o as
|
|
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";
|
|
4
|
+
import { p as scopeField } from "./tenancy-CmeBQ6Nv.js";
|
|
5
|
+
import { a as describeError, i as configError, o as exitCodeOfError, u as usageResult } from "./errors-DpP_U6Bf.js";
|
|
8
6
|
import { t as policyRowConditionKeys } from "./row-conditions-h2HythXQ.js";
|
|
9
|
-
import {
|
|
10
|
-
import "./
|
|
11
|
-
import { i as qualifySqlTable, r as escapeSqlIdent
|
|
12
|
-
import
|
|
13
|
-
import { t as policyFromCatalog } from "./policy-from-catalog-
|
|
7
|
+
import { o as sqlFiles } from "./files-D_JIzUrP.js";
|
|
8
|
+
import { c as requirePolicy } from "./load-DxroEzH1.js";
|
|
9
|
+
import { i as qualifySqlTable, r as escapeSqlIdent } from "./sql-B96movIM.js";
|
|
10
|
+
import "./sources-B-zYB4GS.js";
|
|
11
|
+
import { t as policyFromCatalog } from "./policy-from-catalog-DFWYkNdu.js";
|
|
14
12
|
import { t as beforeFirst } from "./text-BMFRfBqc.js";
|
|
15
13
|
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-
|
|
14
|
+
import { i as requirePeer, n as packageManagerOf, t as addLine } from "./peer-12Q5I_RH.js";
|
|
17
15
|
import { t as POLICY_NOT_PUBLISHED } from "./problem-types-CJ2325SZ.js";
|
|
18
|
-
import { t as fetchProjectPolicy } from "./pull-
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import { a as
|
|
27
|
-
import {
|
|
16
|
+
import { t as fetchProjectPolicy } from "./pull-CN5OLxrF.js";
|
|
17
|
+
import { n as MIGRATION_DIRS } from "./constants-DcJABbLW.js";
|
|
18
|
+
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-CCMdcFWb.js";
|
|
19
|
+
import { b as rbacScaffold, c as approvalsSql, d as resolveSchemas, f as schemasRefusal, o as supabaseHookSql, p as declarativeSql, s as permissionHoldersSql, t as grantsLabel, u as isSupabaseSchemas, y as parseRbacAuthorize } from "./supabase-hook-Dg0SrQWR.js";
|
|
20
|
+
import { A as quoteQualified, D as qualified$1, F as scopeTypeOf, O as quoteIdent$1, _ as subjectClaimJsonSql, c as SESSION_LIVE_HELPER, g as sessionLiveHelperSql, j as quoteTable, k as quoteLiteral, r as HELPERS, t as CUSTOM_ROLES, y as subjectIdSql } from "./shared-fMKOv8hP.js";
|
|
21
|
+
import { D as byKey, F as collectGrants, H as graphHelper, N as breakGlassHolder, P as breakGlassKey, Z as memberIdsHelper, _ as seedSql, a as assignmentSql, d as permissionHelpersSql, et as permittedIdsHelper, g as helpersSql, h as accessSql, n as ownershipSql, nt as scopeSources, p as permittedByPermissionHelper, rt as scopeTable } from "./ownership-Z2LpxsGa.js";
|
|
22
|
+
import { a as splitUndiffed } from "./deciding-columns-DRo2QM6E.js";
|
|
23
|
+
import { a as readOnlyActorKinds, d as connectPgPool, n as actorMayWriteSql, o as readOnlyActorPolicies, r as assemblePolicies, t as ACTOR_MAY_WRITE } from "./policies-B2lZIurj.js";
|
|
24
|
+
import { C as INDEX_COLUMNS_SQL, D as readTableIndexFacts, E as pruneIndexTargets, S as COLUMNS_SQL, T as indexesSql, a as graphSql, b as compileGrants, c as verifyResult, d as tableNames, g as helperCallKeys, h as callsHelper, n as runRlsVerify, o as rlsFinding, p as HELPER_TABLES, s as verifyIgnores, w as indexTargets, x as tableFor } from "./verify-pe8rL_Qw.js";
|
|
25
|
+
import { a as fieldViewsSql, i as fieldViews, n as columnGrantSql, o as rowBranches, r as columnRevokeSql } from "./fields-ClcQLrVc.js";
|
|
26
|
+
import { a as generateOptions, c as buildContext, i as rowHelpersSql, l as DROPPED_LIST, n as inheritTargets, o as isRefusal, r as levelKeys, s as splitsPart, t as supautilsRejections, u as rebuildPlan } from "./supautils-MESd81bF.js";
|
|
27
|
+
import { readFileSync } from "node:fs";
|
|
28
28
|
import { basename, delimiter, join } from "node:path";
|
|
29
29
|
import { spawn } from "node:child_process";
|
|
30
30
|
//#region src/cli/rls/advisors.ts
|
|
@@ -159,142 +159,160 @@ async function runRlsAdvisors(input) {
|
|
|
159
159
|
};
|
|
160
160
|
}
|
|
161
161
|
//#endregion
|
|
162
|
-
//#region src/cli/rls/
|
|
163
|
-
/**
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
/** Declared role names, and those a tenant admin may compose into custom roles. */
|
|
180
|
-
function customRoleNames(policy, requires) {
|
|
181
|
-
const declared = [...roleNames(policy)].toSorted();
|
|
182
|
-
const assignable = declared.filter((name) => policy.rolesByName.get(name)?.assignable ?? listRoles(policy.vocabulary.roles).some((leaf) => leaf.key === name && leaf.assignable));
|
|
183
|
-
const renamed = renamedKeys(policy.vocabulary.permissions);
|
|
184
|
-
const leaves = listPermissions(policy.permissions);
|
|
185
|
-
const manage = leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key);
|
|
186
|
-
return {
|
|
187
|
-
declared,
|
|
188
|
-
assignable,
|
|
189
|
-
...renamed.size === 0 ? {} : { renamed: Object.fromEntries(renamed) },
|
|
190
|
-
permissions: leaves.map((leaf) => leaf.key).toSorted(),
|
|
191
|
-
...manage.length === 0 ? {} : { manage: manage.toSorted() },
|
|
192
|
-
...requires === void 0 ? {} : { requires },
|
|
193
|
-
...policy.levels === void 0 ? {} : { levels: true }
|
|
194
|
-
};
|
|
162
|
+
//#region src/cli/rls/bindings.ts
|
|
163
|
+
/**
|
|
164
|
+
* The table each resource with row policies binds to. With `helpersOnly`,
|
|
165
|
+
* a resource `rls.tables` does not name has no table and no binding.
|
|
166
|
+
*/
|
|
167
|
+
function tableBindings(resources, tables, helpersOnly) {
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const resource of [...new Set(resources)].toSorted()) {
|
|
170
|
+
const named = tables !== void 0 && Object.hasOwn(tables, resource);
|
|
171
|
+
if (helpersOnly && !named) continue;
|
|
172
|
+
out.push({
|
|
173
|
+
resource,
|
|
174
|
+
table: qualifySqlTable(tableFor(resource, tables)),
|
|
175
|
+
inferred: !named
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
195
179
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
return name;
|
|
202
|
-
});
|
|
180
|
+
/** The generate notes for `bindings`: the inferred ones, and where to see them all. */
|
|
181
|
+
function bindingNotes(bindings, withDatabase) {
|
|
182
|
+
const inferred = bindings.filter((binding) => binding.inferred);
|
|
183
|
+
if (inferred.length === 0) return [];
|
|
184
|
+
return [`table bindings (no rls.tables entry, so the table is named after the resource): ${inferred.map((binding) => `${binding.resource} -> ${binding.table}`).join(", ")}${withDatabase ? "" : "; run permdock rls explain, or generate with --db-url-stdin, to check each table exists"}`];
|
|
203
185
|
}
|
|
204
|
-
/**
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
const sources = authorize === "database" ? memberSources : void 0;
|
|
217
|
-
const scopes = scopeList(policy.scopes);
|
|
218
|
-
const suspension = checkSuspension(rls?.suspension, scopes);
|
|
219
|
-
const hookRoles = input.config.supabase?.hook?.roles;
|
|
220
|
-
const roles = rls?.roles ?? (hookRoles === false ? void 0 : hookRoles);
|
|
221
|
-
if (roles !== void 0 && input.rbac) return { refusal: "rls generate --rbac supabase creates its own user_roles (user_id, role app_role); drop rls.roles (or supabase.hook.roles) or the --rbac flag" };
|
|
222
|
-
const requires = writeRequirement(policy, rls?.customRoleWrites?.requires);
|
|
223
|
-
if (requires.error !== void 0) return { refusal: requires.error };
|
|
224
|
-
const readers = rls?.trustedReaders;
|
|
225
|
-
if (readers !== void 0 && (!Array.isArray(readers) || !readers.every((name) => typeof name === "string" && name !== ""))) return { refusal: "rls.trustedReaders must list Postgres role names, such as ['service_role']" };
|
|
226
|
-
const ownership = ownershipRules(policy, scopes);
|
|
227
|
-
const graph = graphPlan(policy);
|
|
228
|
-
const apiKeys = apiKeysPlan(rls?.apiKeys, new Set(roleNames(policy)), Object.fromEntries(renamedKeys(policy.vocabulary.permissions)));
|
|
229
|
-
return {
|
|
230
|
-
ctx: {
|
|
231
|
-
dialect: input.dialect,
|
|
232
|
-
scopes,
|
|
233
|
-
tenantClaim: rls?.tenantClaim ?? "tenant_id",
|
|
234
|
-
...roleMemberScopesOf(rls?.migrate),
|
|
235
|
-
...rls?.roleValues === void 0 ? {} : { roleValues: rls.roleValues },
|
|
236
|
-
...rls?.tenants === "all" ? { tenants: "all" } : {},
|
|
237
|
-
gucPrefix: input.gucPrefix ?? rls?.gucPrefix ?? "app",
|
|
238
|
-
...rls?.actions === void 0 ? {} : { actions: rls.actions },
|
|
239
|
-
inlineFunctions: input.inlineFunctions || rls?.inlineFunctions === true,
|
|
240
|
-
schema,
|
|
241
|
-
authorize,
|
|
242
|
-
roleClaim: rls?.roleClaim ?? "user_role",
|
|
243
|
-
tenantType: input.tenantType ?? rls?.tenantType ?? "uuid",
|
|
244
|
-
...rls?.teamType === void 0 ? {} : { teamType: rls.teamType },
|
|
245
|
-
...rls?.scopeTypes === void 0 ? {} : { scopeTypes: rls.scopeTypes },
|
|
246
|
-
...memberships === void 0 ? {} : { memberships },
|
|
247
|
-
...sources === void 0 || sources.length === 0 ? {} : { sources },
|
|
248
|
-
...memberSources === void 0 || memberSources.length === 0 ? {} : { memberSources },
|
|
249
|
-
...suspension === void 0 ? {} : { suspension },
|
|
250
|
-
...roles === void 0 ? {} : { roles },
|
|
251
|
-
...input.customRoles === true || customRolesOn(rls) ? { customRoles: {
|
|
252
|
-
...customRoleNames(policy, requires.keys),
|
|
253
|
-
...rls?.customRoleWrites?.roles === void 0 ? {} : { table: rls.customRoleWrites.roles }
|
|
254
|
-
} } : {},
|
|
255
|
-
...input.capabilities === true || rls?.capabilities === true ? { capabilities: true } : {},
|
|
256
|
-
...apiKeys === void 0 ? {} : { apiKeys },
|
|
257
|
-
...rls?.anonymousSignIns === "deny" ? { anonymousSignIns: "deny" } : {},
|
|
258
|
-
...ownership === void 0 ? {} : { ownership },
|
|
259
|
-
...rls?.ownershipTriggers === void 0 ? {} : { skipOwnershipTriggers: skippedOwnershipTriggers(rls.ownershipTriggers, scopes) },
|
|
260
|
-
...rls?.assignments === void 0 ? {} : { assignments: rls.assignments === true ? { tables: [] } : {
|
|
261
|
-
tables: rls.assignments.tables ?? [],
|
|
262
|
-
...rls.assignments.ownRole === void 0 ? {} : { ownRole: rls.assignments.ownRole }
|
|
263
|
-
} },
|
|
264
|
-
...options.fieldsMode === void 0 ? {} : { fields: options.fieldsMode },
|
|
265
|
-
...graph.size === 0 ? {} : { graph: {
|
|
266
|
-
closures: closureDepths(graph),
|
|
267
|
-
resources: policy.resources,
|
|
268
|
-
...rls?.tables === void 0 ? {} : { tables: rls.tables }
|
|
269
|
-
} }
|
|
270
|
-
},
|
|
271
|
-
graph,
|
|
272
|
-
ownership,
|
|
273
|
-
schema,
|
|
274
|
-
authorize
|
|
275
|
-
};
|
|
186
|
+
/**
|
|
187
|
+
* The generate notes for the bindings whose table no project SQL file
|
|
188
|
+
* creates. A project with no `create table` in those files gets none: its
|
|
189
|
+
* schema lives elsewhere (an ORM, another repository).
|
|
190
|
+
*/
|
|
191
|
+
function unknownTableNotes(bindings, created) {
|
|
192
|
+
if (created.size === 0) return [];
|
|
193
|
+
return bindings.filter((binding) => !created.has(binding.table) && !created.has(binding.table.toLowerCase())).map((binding) => `resource ${binding.resource} is bound to ${binding.table}, which no SQL file in the migration or schema folders creates; name its table in rls.tables, or set rls.tables.${binding.resource}: false when it has none`);
|
|
194
|
+
}
|
|
195
|
+
/** The inferred bindings whose table `facts` shows missing. */
|
|
196
|
+
function missingBindings(bindings, columns) {
|
|
197
|
+
return bindings.filter((binding) => binding.inferred && columns[binding.table] === void 0);
|
|
276
198
|
}
|
|
277
199
|
//#endregion
|
|
278
|
-
//#region src/cli/rls/
|
|
279
|
-
|
|
280
|
-
|
|
200
|
+
//#region src/cli/rls/trigger-notes.ts
|
|
201
|
+
const CREATE = /^create trigger\s+("[^"]+"|[\w$]+)[^;]*?\bon\s+("[^"]+"(?:\."[^"]+")?|[\w$.]+)/gmu;
|
|
202
|
+
function unquote(name) {
|
|
203
|
+
return name.replaceAll("\"", "");
|
|
204
|
+
}
|
|
205
|
+
function reasonOf(name) {
|
|
206
|
+
if (name === "permdock_custom_role_name_guard") return "customRoleWrites.roles: a tenant role cannot take a declared or shared role's name";
|
|
207
|
+
if (name === "permdock_custom_role_cascade") return "customRoleWrites.roles: a rename or delete of a role carries its grants";
|
|
208
|
+
if (name.startsWith("permdock_sync_custom_role")) return "customRoles.from.sync: each write to the legacy table rebuilds the role's grants";
|
|
209
|
+
if (name.startsWith("permdock_holders") || name.startsWith("permdock_transfer_only")) return "ownershipTriggers; set rls.ownershipTriggers.<scope>: false to skip";
|
|
210
|
+
if (name === "permdock_assignment") return "assignments: a role assignment is checked against the caller's own roles";
|
|
211
|
+
if (name.startsWith("permdock_closure")) return "the relationship closure table follows its source table";
|
|
212
|
+
return "generated by rls generate";
|
|
213
|
+
}
|
|
214
|
+
/** The triggers `sql` creates, in order, each with the option behind it. */
|
|
215
|
+
function generatedTriggers(sql) {
|
|
216
|
+
const found = [];
|
|
217
|
+
for (const match of sql.matchAll(CREATE)) {
|
|
218
|
+
const name = unquote(match.slice(1, 2).join(""));
|
|
219
|
+
const table = unquote(match.slice(2, 3).join(""));
|
|
220
|
+
found.push({
|
|
221
|
+
name,
|
|
222
|
+
table,
|
|
223
|
+
reason: reasonOf(name)
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return found;
|
|
281
227
|
}
|
|
282
|
-
/**
|
|
283
|
-
function
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
228
|
+
/** One generate note per trigger. */
|
|
229
|
+
function triggerNotes(sql) {
|
|
230
|
+
return generatedTriggers(sql).map((trigger) => `generated trigger ${trigger.name} on ${trigger.table} (${trigger.reason})`);
|
|
231
|
+
}
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/cli/rls/explain.ts
|
|
234
|
+
function holds(table) {
|
|
235
|
+
if (table.tenant === void 0) return ["global"];
|
|
236
|
+
return [
|
|
237
|
+
"tenant",
|
|
238
|
+
...table.shared === void 0 ? [] : ["shared"],
|
|
239
|
+
"global"
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
function explainRls(input) {
|
|
243
|
+
const { config, ctx, generated } = input;
|
|
244
|
+
const rls = config.rls;
|
|
245
|
+
const bindings = tableBindings(generated.compiled.rowColumns.map((item) => item.resource), tableNames(rls?.tables), generated.helpersOnly);
|
|
246
|
+
const sources = ctx.memberSources ?? ctx.sources ?? [];
|
|
247
|
+
const roles = rls?.customRoleWrites?.roles;
|
|
248
|
+
const wanted = rls?.rowHelpers;
|
|
249
|
+
const resources = wanted === void 0 ? [] : wanted === true ? [...new Set(generated.compiled.branches.flatMap((branch) => branch.resource === void 0 ? [] : [branch.resource]))].toSorted() : [...new Set(wanted)];
|
|
289
250
|
return {
|
|
290
|
-
|
|
291
|
-
|
|
251
|
+
schema: input.schema,
|
|
252
|
+
authorize: input.authorize,
|
|
253
|
+
scopes: ctx.scopes.map((scope) => scope.within === void 0 ? {
|
|
254
|
+
name: scope.name,
|
|
255
|
+
key: scope.key ?? scope.name
|
|
256
|
+
} : {
|
|
257
|
+
name: scope.name,
|
|
258
|
+
key: scope.key ?? scope.name,
|
|
259
|
+
within: scope.within
|
|
260
|
+
}),
|
|
261
|
+
bindings: bindings.map((binding) => {
|
|
262
|
+
const via = ctx.vias?.[binding.resource];
|
|
263
|
+
return via === void 0 ? binding : Object.assign({}, binding, { via: `${via.helper} over ${via.table}.${via.column} (${binding.table}.${via.key})` });
|
|
264
|
+
}),
|
|
265
|
+
membershipSources: sources.map((source) => ({
|
|
266
|
+
table: source.sql.table,
|
|
267
|
+
select: source.sql.select("$1")
|
|
268
|
+
})),
|
|
269
|
+
...roles === void 0 ? {} : { customRoles: {
|
|
270
|
+
table: roles.table,
|
|
271
|
+
key: roles.key,
|
|
272
|
+
...roles.tenant === void 0 ? {} : { tenant: roles.tenant },
|
|
273
|
+
...roles.shared === void 0 ? {} : { shared: roles.shared },
|
|
274
|
+
...roles.skip === void 0 ? {} : { skip: roles.skip },
|
|
275
|
+
holds: holds(roles)
|
|
276
|
+
} },
|
|
277
|
+
rowHelpers: resources.map((resource) => ({
|
|
278
|
+
resource,
|
|
279
|
+
permissions: [...new Set(generated.compiled.branches.flatMap((branch) => branch.resource === resource && branch.coverage !== true ? [branch.permissionKey] : []))].toSorted()
|
|
280
|
+
})),
|
|
281
|
+
triggers: generatedTriggers(input.text)
|
|
292
282
|
};
|
|
293
283
|
}
|
|
294
|
-
/**
|
|
295
|
-
function
|
|
296
|
-
const
|
|
297
|
-
|
|
284
|
+
/** The text report of `explain`. */
|
|
285
|
+
function describeExplain(explain) {
|
|
286
|
+
const lines = [
|
|
287
|
+
`schema ${explain.schema}, authorize ${explain.authorize}`,
|
|
288
|
+
`scopes: ${explain.scopes.map((scope) => `${scope.name} (${scope.key}${scope.within === void 0 ? "" : `, within ${scope.within}`})`).join(", ")}`,
|
|
289
|
+
"",
|
|
290
|
+
"table bindings:",
|
|
291
|
+
...explain.bindings.map((binding) => ` ${binding.resource} -> ${binding.table}${binding.inferred ? " (named after the resource; set rls.tables to change)" : ""}${binding.via === void 0 ? "" : ` via ${binding.via}`}${binding.exists === false ? " MISSING" : ""}`)
|
|
292
|
+
];
|
|
293
|
+
if (explain.membershipSources.length > 0) {
|
|
294
|
+
lines.push("", "membership sources:");
|
|
295
|
+
for (const source of explain.membershipSources) lines.push(` ${source.table}`, ` ${source.select.replaceAll("\n", "\n ")}`);
|
|
296
|
+
}
|
|
297
|
+
if (explain.customRoles !== void 0) {
|
|
298
|
+
const roles = explain.customRoles;
|
|
299
|
+
const columns = [
|
|
300
|
+
`key ${roles.key}`,
|
|
301
|
+
...roles.tenant === void 0 ? [] : [`tenant ${roles.tenant}`],
|
|
302
|
+
...roles.shared === void 0 ? [] : [`shared ${roles.shared}`],
|
|
303
|
+
...roles.skip === void 0 ? [] : [`skip ${roles.skip}`]
|
|
304
|
+
];
|
|
305
|
+
lines.push("", `custom roles: ${roles.table} (${columns.join(", ")}); holds ${roles.holds.join(", ")} roles`);
|
|
306
|
+
}
|
|
307
|
+
if (explain.rowHelpers.length > 0) {
|
|
308
|
+
lines.push("", "row helpers:");
|
|
309
|
+
for (const helper of explain.rowHelpers) lines.push(` ${helper.resource}: ${helper.permissions.join(", ")}`);
|
|
310
|
+
}
|
|
311
|
+
if (explain.triggers.length > 0) {
|
|
312
|
+
lines.push("", "generated triggers:");
|
|
313
|
+
for (const trigger of explain.triggers) lines.push(` ${trigger.name} on ${trigger.table} (${trigger.reason})`);
|
|
314
|
+
}
|
|
315
|
+
return lines.join("\n");
|
|
298
316
|
}
|
|
299
317
|
//#endregion
|
|
300
318
|
//#region src/cli/rls/generate-output.ts
|
|
@@ -312,8 +330,8 @@ function moveUndiffed(sql, label, part) {
|
|
|
312
330
|
}
|
|
313
331
|
/** Why `--split` cannot be written for `mode`, or `undefined`. */
|
|
314
332
|
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");
|
|
333
|
+
const seedsWithoutOut = plan.split.includes("seeds") && plan.seedsOut === void 0 && !plan.noSeeds && !plan.seedView;
|
|
334
|
+
const helpersWithoutSeeds = plan.split.includes("helpers") && !plan.noSeeds && !plan.split.includes("seeds") && !plan.seedFunction && !plan.seedView;
|
|
317
335
|
switch (mode) {
|
|
318
336
|
case "pg-delta":
|
|
319
337
|
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})`;
|
|
@@ -342,6 +360,7 @@ function outputFiles(plan) {
|
|
|
342
360
|
if (split === void 0) {
|
|
343
361
|
if (input.grantsOut !== void 0) return "rls generate --grants-out needs --split with the helpers or hook part";
|
|
344
362
|
if (input.seedsOut !== void 0) return "rls generate --seeds-out needs --split with the seeds part";
|
|
363
|
+
if (input.noSeeds === true) return "rls generate --no-seeds needs --split: without it the seeds are part of the one file";
|
|
345
364
|
return [{
|
|
346
365
|
part: "rls",
|
|
347
366
|
rel: plan.outRel,
|
|
@@ -358,11 +377,15 @@ function outputFiles(plan) {
|
|
|
358
377
|
explicitOut: input.out !== void 0,
|
|
359
378
|
outRel: plan.outRel,
|
|
360
379
|
grantsOut: input.grantsOut,
|
|
361
|
-
seedsOut: input.seedsOut
|
|
380
|
+
seedsOut: input.seedsOut,
|
|
381
|
+
noSeeds: input.noSeeds === true,
|
|
382
|
+
seedFunction: input.config.rls?.seeds === "function",
|
|
383
|
+
seedView: input.config.rls?.seeds === "view"
|
|
362
384
|
});
|
|
363
385
|
if (refused !== void 0) return refused;
|
|
364
386
|
const at = (part, schema = plan.schema) => pgDelta === void 0 || placeholder ? partPath(plan.outRel, part) : pgDeltaPath(pgDelta.schemaDir, part, schema);
|
|
365
387
|
if (input.grantsOut !== void 0 && !split.includes("hook") && !split.includes("helpers")) return "rls generate --grants-out needs the helpers or hook part in --split";
|
|
388
|
+
if (input.noSeeds === true && input.seedsOut !== void 0) return "rls generate --no-seeds and --seeds-out exclude each other: drop one";
|
|
366
389
|
if (input.seedsOut !== void 0 && !split.includes("seeds")) return "rls generate --seeds-out needs the seeds part in --split";
|
|
367
390
|
if (split.includes("hook") && input.config.supabase?.hook === void 0) return "rls generate --split hook needs supabase.hook in permdock.config.ts";
|
|
368
391
|
const sql = plan.sql();
|
|
@@ -393,6 +416,10 @@ function outputFiles(plan) {
|
|
|
393
416
|
break;
|
|
394
417
|
}
|
|
395
418
|
case "seeds": {
|
|
419
|
+
if (input.noSeeds === true) {
|
|
420
|
+
if (relocated !== void 0 && split.includes("policies")) return "rls generate --no-seeds cannot drop the seeds part under pg-delta with rls.realtime or rls.storage: their policies live in it";
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
396
423
|
const out = input.seedsOut;
|
|
397
424
|
if (out !== void 0 && isSeedsDirectory(input.cwd, out)) {
|
|
398
425
|
const planned = seedsMigration(input.cwd, out, SEEDS_MARKER, seeds, input.io.now?.() ?? dateAt());
|
|
@@ -438,6 +465,12 @@ function outputFiles(plan) {
|
|
|
438
465
|
}
|
|
439
466
|
default: return part;
|
|
440
467
|
}
|
|
468
|
+
if (pgDelta !== void 0 && !placeholder) {
|
|
469
|
+
for (const [index, file] of files.entries()) if (file.part !== "seeds" || input.seedsOut === void 0) files[index] = {
|
|
470
|
+
...file,
|
|
471
|
+
text: declarativeSql(file.text)
|
|
472
|
+
};
|
|
473
|
+
}
|
|
441
474
|
if (input.grantsOut !== void 0) files.push({
|
|
442
475
|
part: "grants",
|
|
443
476
|
rel: input.grantsOut,
|
|
@@ -447,48 +480,6 @@ function outputFiles(plan) {
|
|
|
447
480
|
}
|
|
448
481
|
//#endregion
|
|
449
482
|
//#region src/cli/rls/backfill.ts
|
|
450
|
-
const PREFIX$1 = "PermDock CLI";
|
|
451
|
-
/** A column of the roles-table row `r`. */
|
|
452
|
-
function col(name) {
|
|
453
|
-
return `r.${quoteIdent$1(name)}`;
|
|
454
|
-
}
|
|
455
|
-
function schemaOf(table) {
|
|
456
|
-
return table.includes(".") ? beforeFirst(table, ".") : "public";
|
|
457
|
-
}
|
|
458
|
-
/** The roles-table column a reference joins on, after checking it points at `roles`. */
|
|
459
|
-
function roleRef(role, table, roles, label) {
|
|
460
|
-
const pairs = Object.entries(role.on);
|
|
461
|
-
const [pair] = pairs;
|
|
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' }`);
|
|
463
|
-
const through = role.through.includes(".") ? role.through : `${schemaOf(table)}.${role.through}`;
|
|
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}`);
|
|
466
|
-
const [ref, id] = pair;
|
|
467
|
-
return {
|
|
468
|
-
ref,
|
|
469
|
-
id
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* A legacy key as SQL the way `mapKey` maps it: `rls.migrate.keys`, then
|
|
474
|
-
* `renamed`, then the longest `rls.migrate.prefixes` match, and a former key
|
|
475
|
-
* read as its current key.
|
|
476
|
-
*/
|
|
477
|
-
function legacyKeySql(expr, migrate, renamed) {
|
|
478
|
-
const exact = new Map(Object.entries(renamed));
|
|
479
|
-
for (const [from, to] of Object.entries(migrate?.keys ?? {})) exact.set(from, to);
|
|
480
|
-
const prefixes = Object.entries(migrate?.prefixes ?? {}).toSorted(([a], [b]) => b.length - a.length || byCodeUnit(a, b));
|
|
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)`;
|
|
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);
|
|
484
|
-
return currentKeyExpr(renames, mapped);
|
|
485
|
-
}
|
|
486
|
-
function sourceSql(source, table, label) {
|
|
487
|
-
return roleColumn(source, table, "x", {
|
|
488
|
-
label,
|
|
489
|
-
prefix: PREFIX$1
|
|
490
|
-
}).lookup;
|
|
491
|
-
}
|
|
492
483
|
/**
|
|
493
484
|
* The `--backfill-out` migration: every custom role in the application's
|
|
494
485
|
* roles table that has no rows in `custom_role_permissions` or
|
|
@@ -498,73 +489,10 @@ function sourceSql(source, table, label) {
|
|
|
498
489
|
* nothing, so a role edited after the copy keeps its edits.
|
|
499
490
|
*/
|
|
500
491
|
function backfillSql(ctx, from, migrate) {
|
|
501
|
-
const
|
|
502
|
-
const
|
|
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`);
|
|
505
|
-
const roles = custom.table;
|
|
506
|
-
if (roles === void 0) throw configError(`${PREFIX$1}: rls.customRoles.from needs rls.customRoleWrites.roles, the roles table each row belongs to`);
|
|
507
|
-
const leveled = custom.levels === true;
|
|
508
|
-
if (from.permissions.level !== void 0 && !leveled) throw configError(`${PREFIX$1}: rls.customRoles.from.permissions.level is set, but no resource declares levels`);
|
|
509
|
-
const tenantType = tenantTypeOf(ctx);
|
|
510
|
-
const rolesTable = quoteQualified(roles.table);
|
|
511
|
-
const tenant = roles.tenant === void 0 ? `null::${tenantType}` : `${col(roles.tenant)}::${tenantType}`;
|
|
512
|
-
const scope = roles.scope === void 0 ? `case when ${tenant} is null then 'global' else ${quoteLiteral(root)} end` : `case when ${tenant} is null then 'global' else ${col(roles.scope)}::text end`;
|
|
513
|
-
const scopeId = roles.id === void 0 ? "null::text" : `${col(roles.id)}::text`;
|
|
514
|
-
const skipped = roles.skip === void 0 ? "false" : `coalesce(${col(roles.skip)}, false)`;
|
|
515
|
-
const renamed = custom.renamed ?? {};
|
|
516
|
-
const renames = Object.entries(renamed).toSorted(byKey);
|
|
517
|
-
const current = (expr) => currentKeyExpr(renames, expr);
|
|
518
|
-
const known = sqlTextArray([...custom.permissions ?? [], ...renames.map(([former]) => former)]);
|
|
519
|
-
const declared = sqlTextArray(custom.declared);
|
|
520
|
-
const scopes = sqlTextArray(ctx.scopes.map((item) => item.name));
|
|
521
|
-
const ceiling = qualified(ctx, CUSTOM_ROLES.ceiling);
|
|
522
|
-
const rp = qualified(ctx, "role_permissions");
|
|
523
|
-
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
524
|
-
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
525
|
-
const replace = qualified(ctx, CUSTOM_ROLES.trustedReplace);
|
|
526
|
-
const p = from.permissions;
|
|
527
|
-
const pTable = qualifySqlTable(p.table);
|
|
528
|
-
const pRef = roleRef(p.role, p.table, roles, "rls.customRoles.from.permissions.role");
|
|
529
|
-
const legacy = sourceSql(p.permission, pTable, "rls.customRoles.from.permissions.permission");
|
|
530
|
-
const effect = p.effect === void 0 ? "'allow'" : `coalesce(lower(x.${quoteIdent$1(p.effect)}::text), 'allow')`;
|
|
531
|
-
const level = p.level === void 0 ? "null::text" : `nullif(x.${quoteIdent$1(p.level)}::text, '')`;
|
|
532
|
-
const inCeiling = (key) => `exists (select 1 from ${ceiling} c where c.scope = v.scope and c.effect = 'allow' and ${current("c.permission")} = ${key})`;
|
|
533
|
-
const levelCheck = leveled ? `
|
|
534
|
-
when m.effect = 'allow' and m.level is not null and not exists (
|
|
535
|
-
select 1 from ${ceiling} c
|
|
536
|
-
where c.scope = v.scope and c.effect = 'allow' and ${current("c.permission")} = m.key
|
|
537
|
-
and split_part(c.grant_key, '@', 2) = m.level
|
|
538
|
-
) then 'level ' || m.level || ' is not declared'` : "";
|
|
539
|
-
const allowEntry = leveled ? "j.key || coalesce('@' || j.level, '')" : "j.key";
|
|
540
|
-
const i = from.includes;
|
|
541
|
-
const iTable = i === void 0 ? void 0 : qualifySqlTable(i.table);
|
|
542
|
-
const iRef = i === void 0 || iTable === void 0 ? void 0 : roleRef(i.role, i.table, roles, "rls.customRoles.from.includes.role");
|
|
543
|
-
const included = i === void 0 || iTable === void 0 ? void 0 : sourceSql(i.include, iTable, "rls.customRoles.from.includes.include");
|
|
544
|
-
const refs = [`${col(pRef.id)} as permissions_ref`, ...iRef === void 0 ? [] : [`${col(iRef.id)} as includes_ref`]].join(", ");
|
|
545
|
-
const includeBlock = i === void 0 || iTable === void 0 || iRef === void 0 || included === void 0 ? " v_include := null;" : ` with raw as (
|
|
546
|
-
select ${included} as role
|
|
547
|
-
from ${quoteTable(iTable)} x
|
|
548
|
-
where x.${quoteIdent$1(iRef.ref)} = v.includes_ref
|
|
549
|
-
),
|
|
550
|
-
judged as (
|
|
551
|
-
select n.role, case
|
|
552
|
-
when n.role is null or not (n.role = any(${declared})) then 'not a declared role'
|
|
553
|
-
when exists (
|
|
554
|
-
select 1 from ${rp} rp
|
|
555
|
-
where rp.role = n.role and rp.effect = 'allow'
|
|
556
|
-
and not ${inCeiling(current("rp.permission"))}
|
|
557
|
-
) then 'allows permissions outside the ceiling of ' || v.scope
|
|
558
|
-
end as dropped
|
|
559
|
-
from raw n
|
|
560
|
-
)
|
|
561
|
-
select array_agg(distinct j.role) filter (where j.dropped is null),
|
|
562
|
-
array_agg(v.role || ': includes ' || coalesce(j.role, 'null') || ', ' || j.dropped) filter (where j.dropped is not null)
|
|
563
|
-
into v_include, v_lost
|
|
564
|
-
from judged j;
|
|
565
|
-
v_dropped := v_dropped || coalesce(v_lost, '{}'::text[]);`;
|
|
492
|
+
const plan = rebuildPlan(ctx, from, migrate);
|
|
493
|
+
const { rolesTable, perms, includes, replace, scopes } = plan;
|
|
566
494
|
return `${BACKFILL_MARKER} schema=${ctx.schema ?? "permdock"}
|
|
567
|
-
-- custom roles copied from ${qualifySqlTable(roles.table)} (rls.customRoles.from) through ${replace}
|
|
495
|
+
-- custom roles copied from ${qualifySqlTable(plan.roles.table)} (rls.customRoles.from) through ${replace}
|
|
568
496
|
-- a role that already has rows is left as it is; entries PermDock cannot hold are dropped and listed in one warning
|
|
569
497
|
do $permdock$
|
|
570
498
|
declare
|
|
@@ -578,9 +506,9 @@ declare
|
|
|
578
506
|
v_present integer := 0;
|
|
579
507
|
begin
|
|
580
508
|
for v in
|
|
581
|
-
|
|
509
|
+
${plan.identity}
|
|
582
510
|
from ${rolesTable} r
|
|
583
|
-
where
|
|
511
|
+
where ${plan.eligible}
|
|
584
512
|
order by 4, 2, 1, 3
|
|
585
513
|
loop
|
|
586
514
|
if v.role is null or not (v.scope = 'global' or v.scope = any(${scopes})) then
|
|
@@ -597,30 +525,7 @@ begin
|
|
|
597
525
|
v_present := v_present + 1;
|
|
598
526
|
continue;
|
|
599
527
|
end if;
|
|
600
|
-
|
|
601
|
-
select ${legacy} as legacy, ${effect} as effect, ${level} as level
|
|
602
|
-
from ${quoteTable(pTable)} x
|
|
603
|
-
where x.${quoteIdent$1(pRef.ref)} = v.permissions_ref
|
|
604
|
-
),
|
|
605
|
-
mapped as (
|
|
606
|
-
select n.legacy, ${legacyKeySql("n.legacy", migrate, renamed)} as key, n.effect, n.level
|
|
607
|
-
from raw n
|
|
608
|
-
),
|
|
609
|
-
judged as (
|
|
610
|
-
select m.*, case
|
|
611
|
-
when m.effect not in ('allow', 'deny') then 'effect ' || m.effect || ' is not allow or deny'
|
|
612
|
-
when m.key is null or not (m.key = any(${known})) then 'not a declared permission'
|
|
613
|
-
when m.effect = 'allow' and not ${inCeiling("m.key")} then 'outside the ceiling of ' || v.scope${levelCheck}
|
|
614
|
-
end as dropped
|
|
615
|
-
from mapped m
|
|
616
|
-
)
|
|
617
|
-
select array_agg(distinct ${allowEntry}) filter (where j.dropped is null and j.effect = 'allow'),
|
|
618
|
-
array_agg(distinct j.key) filter (where j.dropped is null and j.effect = 'deny'),
|
|
619
|
-
array_agg(v.role || ': ' || coalesce(j.legacy, 'null') || ', ' || j.dropped) filter (where j.dropped is not null)
|
|
620
|
-
into v_allow, v_deny, v_lost
|
|
621
|
-
from judged j;
|
|
622
|
-
v_dropped := v_dropped || coalesce(v_lost, '{}'::text[]);
|
|
623
|
-
${includeBlock}
|
|
528
|
+
${plan.collect}
|
|
624
529
|
if v_allow is null and v_deny is null and v_include is null then
|
|
625
530
|
continue;
|
|
626
531
|
end if;
|
|
@@ -629,7 +534,7 @@ ${includeBlock}
|
|
|
629
534
|
end loop;
|
|
630
535
|
raise notice 'permdock backfill: copied % custom role(s), % already present', v_copied, v_present;
|
|
631
536
|
if cardinality(v_dropped) > 0 then
|
|
632
|
-
raise warning 'permdock backfill: dropped % entr(ies) PermDock cannot hold: %', cardinality(v_dropped),
|
|
537
|
+
raise warning 'permdock backfill: dropped % entr(ies) PermDock cannot hold: %', cardinality(v_dropped), ${DROPPED_LIST};
|
|
633
538
|
end if;
|
|
634
539
|
end
|
|
635
540
|
$permdock$;
|
|
@@ -639,7 +544,7 @@ $permdock$;
|
|
|
639
544
|
//#region src/cli/rls/break-glass.ts
|
|
640
545
|
/** The audit table every generated break-glass read writes to. Part of the SQL contract. */
|
|
641
546
|
const BREAK_GLASS_AUDIT = "permdock_break_glass_audit";
|
|
642
|
-
const IDENT$
|
|
547
|
+
const IDENT$2 = /^[a-z][a-z0-9_]*$/u;
|
|
643
548
|
/**
|
|
644
549
|
* The resources a `breakGlass` grant targets, with the table each reads. A
|
|
645
550
|
* break-glass grant is non-portable, so `compileGrants` never turns it into a
|
|
@@ -688,7 +593,7 @@ function rowsFilter(ctx, entry) {
|
|
|
688
593
|
const asked = `p_permission = ${quoteLiteral(grant.permission)}`;
|
|
689
594
|
if (grant.scope === "anyone") {
|
|
690
595
|
if (grant.root === void 0) return [`(${asked})`];
|
|
691
|
-
return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified(ctx, memberIdsHelper(grant.root))}()))`];
|
|
596
|
+
return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified$1(ctx, memberIdsHelper(grant.root))}()))`];
|
|
692
597
|
}
|
|
693
598
|
if (grant.scope !== "global" && grant.column === void 0) return [];
|
|
694
599
|
return [`(${asked} and ${accessSql(ctx, grant.scope, breakGlassKey(grant.permission), grant.column)})`];
|
|
@@ -696,11 +601,11 @@ function rowsFilter(ctx, entry) {
|
|
|
696
601
|
return arms.length === 0 ? "false" : [...new Set(arms)].join("\n or ");
|
|
697
602
|
}
|
|
698
603
|
function functionName$1(resource) {
|
|
699
|
-
if (!IDENT$
|
|
604
|
+
if (!IDENT$2.test(resource)) throw configError(`PermDock CLI: unsafe break-glass resource '${resource}'`);
|
|
700
605
|
return `permdock_break_glass_${resource}`;
|
|
701
606
|
}
|
|
702
607
|
function auditSql$1(ctx) {
|
|
703
|
-
const audit = qualified(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
|
|
608
|
+
const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
|
|
704
609
|
return `-- break-glass audit: one row per session that read restricted data
|
|
705
610
|
-- RLS never compiles a break-glass override, so restricted rows stay denied to a direct read
|
|
706
611
|
create table if not exists ${audit} (
|
|
@@ -715,8 +620,8 @@ alter table ${audit} enable row level security;
|
|
|
715
620
|
revoke all on table ${audit} from anon, authenticated, public;`;
|
|
716
621
|
}
|
|
717
622
|
function readFunctionSql(ctx, entry) {
|
|
718
|
-
const fn = qualified(ctx, functionName$1(entry.resource));
|
|
719
|
-
const audit = qualified(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
|
|
623
|
+
const fn = qualified$1(ctx, functionName$1(entry.resource));
|
|
624
|
+
const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
|
|
720
625
|
const table = quoteQualified(entry.table);
|
|
721
626
|
const session = subjectClaimJsonSql(ctx, "break_glass");
|
|
722
627
|
const subject = subjectIdSql(ctx);
|
|
@@ -770,7 +675,7 @@ function breakGlassSql(ctx, entries) {
|
|
|
770
675
|
//#endregion
|
|
771
676
|
//#region src/cli/rls/emit.ts
|
|
772
677
|
const HEADER = "-- generated by permdock rls generate\n-- fail-closed: never target a bypass role\n";
|
|
773
|
-
const TRUSTED_READER_GRANT = /^grant execute on function [^\n]+\.permdock_trusted_role_permissions
|
|
678
|
+
const TRUSTED_READER_GRANT = /^grant usage on schema "[^"\n]+" to [^\n;]+;\ngrant execute on function [^\n]+\.permdock_trusted_role_permissions(?:_by_id)?\([^)\n]*\) to [^\n;]+;$/gmu;
|
|
774
679
|
function assertNoServiceRole(text) {
|
|
775
680
|
if (/\bservice_role\b/iu.test(text.replace(TRUSTED_READER_GRANT, ""))) throw new Error("PermDock CLI: generated RLS must never emit service_role");
|
|
776
681
|
}
|
|
@@ -982,7 +887,7 @@ function declaredScope(facts, scope, where) {
|
|
|
982
887
|
* relationship or a `where` on the same permission never widens it.
|
|
983
888
|
*/
|
|
984
889
|
function permitted(ctx, value, scope, key) {
|
|
985
|
-
return `${value} in (select x::text from ${qualified(ctx, permittedByPermissionHelper(scope))}(${quoteLiteral(key)}) x)`;
|
|
890
|
+
return `${value} in (select x::text from ${qualified$1(ctx, permittedByPermissionHelper(scope))}(${quoteLiteral(key)}) x)`;
|
|
986
891
|
}
|
|
987
892
|
function escapeRegex(text) {
|
|
988
893
|
return text.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
|
|
@@ -1099,165 +1004,6 @@ function realtimeStoragePolicies(ctx, facts, config, warnings = []) {
|
|
|
1099
1004
|
return policies;
|
|
1100
1005
|
}
|
|
1101
1006
|
//#endregion
|
|
1102
|
-
//#region src/cli/rls/rows.ts
|
|
1103
|
-
/** `permitted_<resource>_rows(p_permission)`: the resource's row ids the subject may act on with one permission. */
|
|
1104
|
-
function rowsHelper(resource) {
|
|
1105
|
-
const name = graphSqlName(resource);
|
|
1106
|
-
const helper = `permitted_${name ?? resource}_rows`;
|
|
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`);
|
|
1108
|
-
return helper;
|
|
1109
|
-
}
|
|
1110
|
-
/** The branches a row check of a permission reads: its allows and denies on rows that exist, for signed-in callers. */
|
|
1111
|
-
function rowBranchesOf(branches, resource) {
|
|
1112
|
-
const out = /* @__PURE__ */ new Map();
|
|
1113
|
-
for (const branch of branches) {
|
|
1114
|
-
if (branch.resource !== resource || branch.command === "insert" || branch.coverage === true || !branch.roles.includes("authenticated")) continue;
|
|
1115
|
-
out.set(branch.permissionKey, [...out.get(branch.permissionKey) ?? [], branch]);
|
|
1116
|
-
}
|
|
1117
|
-
return out;
|
|
1118
|
-
}
|
|
1119
|
-
function usingOf(branches) {
|
|
1120
|
-
const parts = branches.map((branch) => branchClauses(branch).using ?? "true");
|
|
1121
|
-
return parts.length === 0 ? void 0 : orSql(parts);
|
|
1122
|
-
}
|
|
1123
|
-
function permissionCase(key, branches) {
|
|
1124
|
-
const allow = usingOf(branches.filter((branch) => branch.effect === "allow"));
|
|
1125
|
-
if (allow === void 0) return;
|
|
1126
|
-
const deny = usingOf(branches.filter((branch) => branch.effect === "deny"));
|
|
1127
|
-
const granted = `coalesce((${allow}), false)`;
|
|
1128
|
-
return ` when ${quoteLiteral(key)} then ${deny === void 0 ? granted : `${granted} and (${deny}) is not true`}`;
|
|
1129
|
-
}
|
|
1130
|
-
function forUserSql(ctx, fn, helper) {
|
|
1131
|
-
const user = ctx.dialect === "supabase" ? "uuid" : "text";
|
|
1132
|
-
const signature = `${user}, text, jsonb`;
|
|
1133
|
-
const head = `create or replace function ${fn}(p_user ${user}, p_permission text, p_claims jsonb default '{}')
|
|
1134
|
-
returns setof text
|
|
1135
|
-
language plpgsql
|
|
1136
|
-
volatile
|
|
1137
|
-
security definer
|
|
1138
|
-
set search_path = ''
|
|
1139
|
-
as $$`;
|
|
1140
|
-
const tail = `revoke execute on function ${fn}(${signature}) from public, anon, authenticated;`;
|
|
1141
|
-
if (ctx.dialect === "supabase") return `${head}
|
|
1142
|
-
declare
|
|
1143
|
-
v_claims text := pg_catalog.current_setting('request.jwt.claims', true);
|
|
1144
|
-
begin
|
|
1145
|
-
perform pg_catalog.set_config(
|
|
1146
|
-
'request.jwt.claims',
|
|
1147
|
-
(coalesce(p_claims, '{}'::jsonb) || pg_catalog.jsonb_build_object('sub', p_user::text, 'role', 'authenticated'))::text,
|
|
1148
|
-
true
|
|
1149
|
-
);
|
|
1150
|
-
return query select r.id from ${helper}(p_permission) r(id);
|
|
1151
|
-
perform pg_catalog.set_config('request.jwt.claims', coalesce(v_claims, ''), true);
|
|
1152
|
-
end
|
|
1153
|
-
$$;
|
|
1154
|
-
${tail}`;
|
|
1155
|
-
const prefix = ctx.gucPrefix;
|
|
1156
|
-
return `${head}
|
|
1157
|
-
declare
|
|
1158
|
-
v_saved jsonb := '{}'::jsonb;
|
|
1159
|
-
v_key text;
|
|
1160
|
-
v_value text;
|
|
1161
|
-
begin
|
|
1162
|
-
for v_key, v_value in
|
|
1163
|
-
select 'user_id', p_user
|
|
1164
|
-
union all
|
|
1165
|
-
select c.key, c.value from pg_catalog.jsonb_each_text(coalesce(p_claims, '{}'::jsonb)) c where c.key <> 'user_id'
|
|
1166
|
-
loop
|
|
1167
|
-
v_saved := v_saved || pg_catalog.jsonb_build_object(v_key, coalesce(pg_catalog.current_setting(${quoteLiteral(`${prefix}.`)} || v_key, true), ''));
|
|
1168
|
-
perform pg_catalog.set_config(${quoteLiteral(`${prefix}.`)} || v_key, v_value, true);
|
|
1169
|
-
end loop;
|
|
1170
|
-
return query select r.id from ${helper}(p_permission) r(id);
|
|
1171
|
-
for v_key, v_value in select s.key, s.value from pg_catalog.jsonb_each_text(v_saved) s loop
|
|
1172
|
-
perform pg_catalog.set_config(${quoteLiteral(`${prefix}.`)} || v_key, v_value, true);
|
|
1173
|
-
end loop;
|
|
1174
|
-
end
|
|
1175
|
-
$$;
|
|
1176
|
-
${tail}`;
|
|
1177
|
-
}
|
|
1178
|
-
function inheritTargets(policy) {
|
|
1179
|
-
const out = /* @__PURE__ */ new Set();
|
|
1180
|
-
for (const grant of policy.grants) for (const item of flattenGrantee(grant.to)) if (item.kind === "inherit") out.add(item.resource);
|
|
1181
|
-
return [...out].toSorted();
|
|
1182
|
-
}
|
|
1183
|
-
function stubSql(ctx, resource) {
|
|
1184
|
-
return `create or replace function ${qualified(ctx, rowsHelper(resource))}(p_permission text)
|
|
1185
|
-
returns setof text
|
|
1186
|
-
language sql
|
|
1187
|
-
stable
|
|
1188
|
-
security definer
|
|
1189
|
-
set search_path = ''
|
|
1190
|
-
as $$
|
|
1191
|
-
select null::text where false
|
|
1192
|
-
$$;`;
|
|
1193
|
-
}
|
|
1194
|
-
function rowSql(ctx, rows, table, where) {
|
|
1195
|
-
const fn = qualified(ctx, rows.replace(/_rows$/u, "_row"));
|
|
1196
|
-
return `create or replace function ${fn}(p_row ${table}, p_permission text)
|
|
1197
|
-
returns boolean
|
|
1198
|
-
language sql
|
|
1199
|
-
stable
|
|
1200
|
-
security definer
|
|
1201
|
-
set search_path = ''
|
|
1202
|
-
as $$
|
|
1203
|
-
select exists (select 1 from (select (p_row).*) r where ${where})
|
|
1204
|
-
$$;
|
|
1205
|
-
revoke execute on function ${fn}(${table}, text) from public, anon;
|
|
1206
|
-
grant execute on function ${fn}(${table}, text) to authenticated;`;
|
|
1207
|
-
}
|
|
1208
|
-
function resourceSql(ctx, node, branches, tables) {
|
|
1209
|
-
const name = rowsHelper(node.name);
|
|
1210
|
-
const fn = qualified(ctx, name);
|
|
1211
|
-
const table = quoteQualified(tableFor(node.name, tables));
|
|
1212
|
-
const cases = [...branches.keys()].toSorted().flatMap((key) => {
|
|
1213
|
-
const sql = permissionCase(key, branches.get(key) ?? []);
|
|
1214
|
-
return sql === void 0 ? [] : [sql];
|
|
1215
|
-
});
|
|
1216
|
-
const where = cases.length === 0 ? "false" : `case p_permission\n${cases.join("\n")}\n else false\n end`;
|
|
1217
|
-
const rows = `-- ${node.name}: ids of the rows the caller may act on with p_permission, as the table's policies decide it
|
|
1218
|
-
create or replace function ${fn}(p_permission text)
|
|
1219
|
-
returns setof text
|
|
1220
|
-
language sql
|
|
1221
|
-
stable
|
|
1222
|
-
security definer
|
|
1223
|
-
set search_path = ''
|
|
1224
|
-
as $$
|
|
1225
|
-
select ${quoteIdent$1(node.id)}::text from ${table}
|
|
1226
|
-
where ${where}
|
|
1227
|
-
$$;
|
|
1228
|
-
revoke execute on function ${fn}(text) from public, anon;
|
|
1229
|
-
grant execute on function ${fn}(text) to authenticated;
|
|
1230
|
-
${rowSql(ctx, name, table, where)}`;
|
|
1231
|
-
if (ctx.dialect === "neon") return rows;
|
|
1232
|
-
return `${rows}
|
|
1233
|
-
-- the same rows for a user the caller names, with claims it supplies; no client role may execute it
|
|
1234
|
-
${forUserSql(ctx, qualified(ctx, `${name}_for`), fn)}`;
|
|
1235
|
-
}
|
|
1236
|
-
/**
|
|
1237
|
-
* `permitted_<resource>_rows(p_permission)` for each resource `select`
|
|
1238
|
-
* names (or every resource a grant reaches with `true`): the ids of its rows
|
|
1239
|
-
* the caller may read, update or delete with that permission, from the same
|
|
1240
|
-
* compiled allows and denies the table policies use, so closure walks, link
|
|
1241
|
-
* hops, the restricted stop, `includes`, groups and `requires` all apply. A
|
|
1242
|
-
* `security definer` helper, so hand-written policies and functions call it
|
|
1243
|
-
* without restating the graph. Outside the `neon` dialect a
|
|
1244
|
-
* `permitted_<resource>_rows_for(p_user, p_permission, p_claims)` form runs it
|
|
1245
|
-
* as the named user, with the claims the caller supplies, for trusted SQL.
|
|
1246
|
-
*/
|
|
1247
|
-
function rowHelpersSql(ctx, policy, branches, select, tables, warnings) {
|
|
1248
|
-
const names = select === true ? [...new Set(branches.flatMap((branch) => branch.resource === void 0 ? [] : [branch.resource]))].toSorted() : [...new Set(select)];
|
|
1249
|
-
const called = inheritTargets(policy);
|
|
1250
|
-
for (const name of called) if (!names.includes(name)) names.push(name);
|
|
1251
|
-
const chunks = called.map((name) => stubSql(ctx, name));
|
|
1252
|
-
for (const name of names) {
|
|
1253
|
-
const node = policy.resources.get(name);
|
|
1254
|
-
if (node === void 0) throw configError(`PermDock CLI: rls.rowHelpers names '${name}', which the policy does not declare`);
|
|
1255
|
-
chunks.push(resourceSql(ctx, node, rowBranchesOf(branches, name), tables));
|
|
1256
|
-
}
|
|
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");
|
|
1258
|
-
return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
|
|
1259
|
-
}
|
|
1260
|
-
//#endregion
|
|
1261
1007
|
//#region src/cli/rls/generate-policies.ts
|
|
1262
1008
|
const SERVICE_TABLES_HEADER = "-- Supabase Realtime and Storage: rls.realtime and rls.storage. RLS on both tables is Supabase's.";
|
|
1263
1009
|
/**
|
|
@@ -1293,12 +1039,13 @@ function generatePolicies(input, policy, options, built, warnings) {
|
|
|
1293
1039
|
const { ctx } = built;
|
|
1294
1040
|
const { fieldsMode, revokeColumns } = options;
|
|
1295
1041
|
databaseModeWarnings(policy, built, warnings);
|
|
1296
|
-
const compiled = compileGrants(policy, ctx, rls?.tables, warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
|
|
1042
|
+
const compiled = compileGrants(policy, ctx, tableNames(rls?.tables), warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
|
|
1297
1043
|
const helpersOnly = input.helpersOnly === true || rls?.helpersOnly === true;
|
|
1298
1044
|
if (helpersOnly && (input.target !== "sql" || revokeColumns)) return { refusal: "rls generate --helpers-only needs --target sql and no --revoke-columns: the table grants are hand-written, so revoke the restricted columns there" };
|
|
1299
1045
|
const views = fieldsMode === void 0 ? [] : fieldViews(policy, ctx, compiled.branches, {
|
|
1300
1046
|
revokeColumns,
|
|
1301
|
-
helpersOnly
|
|
1047
|
+
helpersOnly,
|
|
1048
|
+
mask: fieldsMode === "mask"
|
|
1302
1049
|
}, warnings);
|
|
1303
1050
|
const force = input.force === true || rls?.force === true;
|
|
1304
1051
|
if (force && views.some((view) => view.companion !== void 0)) warnings.push("--force with --revoke-columns: each <table>_visible_fields companion reads as its owner, so its owner needs BYPASSRLS or the restricted columns read as null");
|
|
@@ -1308,7 +1055,7 @@ function generatePolicies(input, policy, options, built, warnings) {
|
|
|
1308
1055
|
...policyName === void 0 ? {} : { name: policyName }
|
|
1309
1056
|
});
|
|
1310
1057
|
const actorKinds = readOnlyActorKinds(rls?.readOnlyActors);
|
|
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`);
|
|
1058
|
+
if (actorKinds !== void 0 && helpersOnly) warnings.push(`rls.readOnlyActors with --helpers-only writes no policies: add \`and (select ${qualified$1(ctx, ACTOR_MAY_WRITE)}())\` to each hand-written insert, update and delete policy, or a restrictive policy that calls it`);
|
|
1312
1059
|
const withActors = (items) => actorKinds === void 0 ? items : [...items, ...readOnlyActorPolicies(items, subjectClaimJsonSql(ctx, "act"), actorKinds)];
|
|
1313
1060
|
let tablePolicies;
|
|
1314
1061
|
try {
|
|
@@ -1341,7 +1088,7 @@ function generatePolicies(input, policy, options, built, warnings) {
|
|
|
1341
1088
|
}
|
|
1342
1089
|
//#endregion
|
|
1343
1090
|
//#region src/cli/rls/shims.ts
|
|
1344
|
-
const NAME = /^[a-z_][a-z0-9_]*$/u;
|
|
1091
|
+
const NAME$1 = /^[a-z_][a-z0-9_]*$/u;
|
|
1345
1092
|
/**
|
|
1346
1093
|
* The key a legacy caller passes, mapped the way `rls migrate` maps it:
|
|
1347
1094
|
* `keys` and `renamed` exactly, then the longest `prefixes` entry.
|
|
@@ -1424,7 +1171,7 @@ function grantKeys(keys, scope, effect) {
|
|
|
1424
1171
|
}
|
|
1425
1172
|
/** The instances of `scope` the caller holds an unconditional allow of the key in, minus those it holds a deny in. */
|
|
1426
1173
|
function idsSql(ctx, keys, scope) {
|
|
1427
|
-
const helper = qualified(ctx, permittedIdsHelper(scope));
|
|
1174
|
+
const helper = qualified$1(ctx, permittedIdsHelper(scope));
|
|
1428
1175
|
return ` select a.id
|
|
1429
1176
|
from ${grantKeys(keys, scope, "allow")}
|
|
1430
1177
|
cross join lateral ${helper}(g.grant_key) a(id)
|
|
@@ -1434,7 +1181,7 @@ function idsSql(ctx, keys, scope) {
|
|
|
1434
1181
|
cross join lateral ${helper}(g.grant_key) d(id)`;
|
|
1435
1182
|
}
|
|
1436
1183
|
function hasSql(ctx, keys) {
|
|
1437
|
-
const has = qualified(ctx, HELPERS.has);
|
|
1184
|
+
const has = qualified$1(ctx, HELPERS.has);
|
|
1438
1185
|
return `exists (select 1 from ${grantKeys(keys, "global", "allow")} where ${has}(g.grant_key))
|
|
1439
1186
|
and not exists (select 1 from ${grantKeys(keys, "global", "deny")} where ${has}(g.grant_key))`;
|
|
1440
1187
|
}
|
|
@@ -1452,14 +1199,14 @@ function shim(ctx, config, keys, fn, helper) {
|
|
|
1452
1199
|
case "membership": {
|
|
1453
1200
|
const scope = scopeOf(ctx, helper.scope);
|
|
1454
1201
|
const type = scopeTypeOf(ctx, scope);
|
|
1455
|
-
return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified(ctx, memberIdsHelper(scope))}()), false)`);
|
|
1202
|
+
return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified$1(ctx, memberIdsHelper(scope))}()), false)`);
|
|
1456
1203
|
}
|
|
1457
1204
|
case "role": {
|
|
1458
1205
|
const scope = scopeOf(ctx, helper.scope);
|
|
1459
1206
|
const type = scopeTypeOf(ctx, scope);
|
|
1460
1207
|
const values = Object.entries(ctx.roleValues ?? {});
|
|
1461
1208
|
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)`);
|
|
1209
|
+
return shimFunction(fn, `p_id ${type}, p_role text`, `${type}, text`, "boolean", ` select coalesce(p_id in (select ${qualified$1(ctx, memberIdsHelper(scope))}(array[${role}])), false)`);
|
|
1463
1210
|
}
|
|
1464
1211
|
case "global": return shimFunction(fn, "p_key text", "text", "boolean", ` select ${hasSql(ctx, keys)}`);
|
|
1465
1212
|
case "scoped": {
|
|
@@ -1489,7 +1236,7 @@ function shimsSql(ctx, config, shims, renamed, grants) {
|
|
|
1489
1236
|
};
|
|
1490
1237
|
const chunks = [`-- permdock shims: legacy helper names over the permdock helpers; drop each once doctor reports no caller (PD056)`];
|
|
1491
1238
|
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}'`);
|
|
1239
|
+
if (!NAME$1.test(name)) throw configError(`PermDock CLI: unsafe rls.migrate helper name '${name}'`);
|
|
1493
1240
|
chunks.push(shim(ctx, config, keys, `${quoteIdent$1(schema)}.${name}`, helper));
|
|
1494
1241
|
}
|
|
1495
1242
|
return `${chunks.join("\n\n")}\n`;
|
|
@@ -1508,7 +1255,7 @@ const AUDITED = [
|
|
|
1508
1255
|
CUSTOM_ROLES.includes,
|
|
1509
1256
|
"user_roles"
|
|
1510
1257
|
];
|
|
1511
|
-
const IDENT = /^[a-z_][a-z0-9_$]*$/u;
|
|
1258
|
+
const IDENT$1 = /^[a-z_][a-z0-9_$]*$/u;
|
|
1512
1259
|
/** The tables `sql` creates in `schema`, by name without the schema. */
|
|
1513
1260
|
function generatedTables(sql, schema) {
|
|
1514
1261
|
const prefix = `create table if not exists ${quoteIdent$1(schema)}.`;
|
|
@@ -1570,7 +1317,7 @@ function auditSql(audit, schema, tables) {
|
|
|
1570
1317
|
if (listed !== void 0) for (const table of listed) knownTable(table, tables, "rls.audit.tables");
|
|
1571
1318
|
const targets = listed ?? AUDITED.filter((table) => tables.includes(table));
|
|
1572
1319
|
const args = Object.entries(audit.args ?? {}).map(([name, value]) => {
|
|
1573
|
-
if (!IDENT.test(name)) throw configError(`${PREFIX}: rls.audit.args names ${JSON.stringify(name)}, which is not a lower-case parameter name`);
|
|
1320
|
+
if (!IDENT$1.test(name)) throw configError(`${PREFIX}: rls.audit.args names ${JSON.stringify(name)}, which is not a lower-case parameter name`);
|
|
1574
1321
|
return `, ${quoteIdent$1(name)} => ${quoteLiteral(value)}`;
|
|
1575
1322
|
});
|
|
1576
1323
|
if (targets.length === 0) return "";
|
|
@@ -1633,14 +1380,15 @@ function sectionsSql(input, policy, options, built, generated, jsonSchema, warni
|
|
|
1633
1380
|
} catch (cause) {
|
|
1634
1381
|
return { refusal: describeError(cause) };
|
|
1635
1382
|
}
|
|
1636
|
-
|
|
1383
|
+
if (typeof rls?.approvals === "object" && rls.approvals.delegations === true && ctx.authorize !== "database") warnings.push("rls.approvals.delegations with --authorize jwt: permdock_is_approver cannot read a delegator's permissions from the caller's token, so in SQL a cover never satisfies a holder() approver; approvalsHandler still does");
|
|
1384
|
+
const graphed = graphSql(ctx, graph, tableNames(rls?.tables));
|
|
1637
1385
|
let rowHelpers = "";
|
|
1638
1386
|
if (rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0) try {
|
|
1639
|
-
rowHelpers = rowHelpersSql(ctx, policy, [...options.fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], rls?.tables, warnings);
|
|
1387
|
+
rowHelpers = rowHelpersSql(ctx, policy, [...options.fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], tableNames(rls?.tables), warnings);
|
|
1640
1388
|
} catch (cause) {
|
|
1641
1389
|
return { refusal: describeError(cause) };
|
|
1642
1390
|
}
|
|
1643
|
-
const breakGlass = breakGlassSql(ctx, breakGlassEntries(policy, rls?.tables));
|
|
1391
|
+
const breakGlass = breakGlassSql(ctx, breakGlassEntries(policy, tableNames(rls?.tables)));
|
|
1644
1392
|
return [
|
|
1645
1393
|
approvals,
|
|
1646
1394
|
owned === "" ? void 0 : owned,
|
|
@@ -1715,6 +1463,14 @@ function preambleSql(input, policy, options, built, generated, jsonSchema, warni
|
|
|
1715
1463
|
audit
|
|
1716
1464
|
};
|
|
1717
1465
|
}
|
|
1466
|
+
function seedsBody(ctx, schema, rows) {
|
|
1467
|
+
switch (ctx.seeds) {
|
|
1468
|
+
case "function": return `select ${quoteQualified(`${schema}.seed`)}();`;
|
|
1469
|
+
case "view": return "-- rls.seeds: 'view': the role_permissions rows are in the helpers part";
|
|
1470
|
+
case void 0: return seedSql(ctx, rows);
|
|
1471
|
+
default: return ctx.seeds;
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1718
1474
|
function forceWarnings({ graph }, warnings) {
|
|
1719
1475
|
for (const entry of graph.values()) {
|
|
1720
1476
|
const own = [...entry.relations].filter((name) => !("edge" in (entry.node.relations[name] ?? {})));
|
|
@@ -1795,18 +1551,144 @@ function generateSql(input, policy, options, built, warnings) {
|
|
|
1795
1551
|
...generated,
|
|
1796
1552
|
...emitted,
|
|
1797
1553
|
backfill: checked.backfill,
|
|
1798
|
-
indexes: indexTargets(ctx, tableColumns(generated.compiled.rowColumns, rls?.tables, helpersOnly, warnings)),
|
|
1554
|
+
indexes: indexTargets(ctx, tableColumns(generated.compiled.rowColumns, tableNames(rls?.tables), helpersOnly, warnings)),
|
|
1799
1555
|
parts: (indexes) => ({
|
|
1800
1556
|
policies: dialectRoles(withHelperTables(emitSql(policies, "", force, views), helperTables), ctx.dialect),
|
|
1801
1557
|
declarativePolicies: dialectRoles(emitSql(policies, "", force, views), ctx.dialect),
|
|
1802
1558
|
...guardedTables === void 0 ? {} : { servicePolicies: dialectRoles(guardedTables, ctx.dialect) },
|
|
1803
1559
|
helpers: dialectRoles(emitSql([], preamble, false, helpersOnly ? views : []), ctx.dialect),
|
|
1804
|
-
seeds: `${SEEDS_MARKER} schema=${built.schema}\n${
|
|
1560
|
+
seeds: `${SEEDS_MARKER} schema=${built.schema}\n${seedsBody(ctx, built.schema, generated.compiled.rolePermissions)}\n${seedsSplit ? audit : ""}`,
|
|
1805
1561
|
indexes: `${INDEXES_MARKER}\n${indexesSql(indexes)}\n`
|
|
1806
1562
|
})
|
|
1807
1563
|
};
|
|
1808
1564
|
}
|
|
1809
1565
|
//#endregion
|
|
1566
|
+
//#region src/cli/rls/index-scan.ts
|
|
1567
|
+
/** An index PermDock generated (`permdock_<table>_<columns>_idx`), in the indexes part or in a migration derived from it: it never counts as serving its own target. */
|
|
1568
|
+
const GENERATED_INDEX = /\bcreate\s+index\s+(?:if\s+not\s+exists\s+)?"?permdock_\w*_idx"?[^;]*;/giu;
|
|
1569
|
+
const IDENT = String.raw`(?:"(?:[^"]|"")+"|[A-Za-z_][\w$]*)`;
|
|
1570
|
+
const NAME = String.raw`${IDENT}(?:\s*\.\s*${IDENT})?`;
|
|
1571
|
+
const CREATE_INDEX = new RegExp(String.raw`\bcreate\s+(?:unique\s+)?index\s+(?:concurrently\s+)?(?:if\s+not\s+exists\s+)?(?:${IDENT}\s+)?on\s+(?:only\s+)?(${NAME})\s*(?:using\s+\w+\s*)?\(`, "giu");
|
|
1572
|
+
const ADD_KEY = new RegExp(String.raw`\balter\s+table\s+(?:if\s+exists\s+)?(?:only\s+)?(${NAME})\s+add\s+(?:constraint\s+${IDENT}\s+)?(?:unique|primary\s+key)\s*(?:nulls\s+not\s+distinct\s*)?\(`, "giu");
|
|
1573
|
+
const CREATE_TABLE = new RegExp(String.raw`\bcreate\s+(?:unlogged\s+|temp(?:orary)?\s+)?table\s+(?:if\s+not\s+exists\s+)?(${NAME})\s*\(`, "giu");
|
|
1574
|
+
const TABLE_NAME = new RegExp(String.raw`\bcreate\s+(?:unlogged\s+)?table\s+(?:if\s+not\s+exists\s+)?(${NAME})`, "giu");
|
|
1575
|
+
const TABLE_KEY = new RegExp(String.raw`^(?:constraint\s+${IDENT}\s+)?(?:unique|primary\s+key)\s*(?:nulls\s+not\s+distinct\s*)?\(`, "iu");
|
|
1576
|
+
const COLUMN_KEY = new RegExp(String.raw`^(${IDENT})\s+[^,]*?\b(?:primary\s+key|unique)\b`, "iu");
|
|
1577
|
+
const NOT_A_COLUMN = /^(?:constraint|check|foreign|exclude|like)\b/iu;
|
|
1578
|
+
function bare(part) {
|
|
1579
|
+
const trimmed = part.trim();
|
|
1580
|
+
return trimmed.startsWith("\"") ? trimmed.slice(1, -1).replaceAll("\"\"", "\"") : trimmed.toLowerCase();
|
|
1581
|
+
}
|
|
1582
|
+
/** `schema.table`, with `public` for an unqualified name. */
|
|
1583
|
+
function qualified(raw) {
|
|
1584
|
+
const parts = raw.split(".").map(bare);
|
|
1585
|
+
return parts.length === 1 ? `public.${parts.join("")}` : parts.join(".");
|
|
1586
|
+
}
|
|
1587
|
+
function stripComments(sql) {
|
|
1588
|
+
return sql.replaceAll(/\/\*[\s\S]*?\*\//gu, " ").replaceAll(/--[^\n]*/gu, " ");
|
|
1589
|
+
}
|
|
1590
|
+
/** The text between the `(` that ends `match` and its closing `)`, and the index after it. */
|
|
1591
|
+
function group(text, open) {
|
|
1592
|
+
let depth = 1;
|
|
1593
|
+
for (let index = open; index < text.length; index += 1) {
|
|
1594
|
+
const char = text[index];
|
|
1595
|
+
if (char === "'" || char === "\"") {
|
|
1596
|
+
const close = text.indexOf(char, index + 1);
|
|
1597
|
+
if (close === -1) return;
|
|
1598
|
+
index = close;
|
|
1599
|
+
} else if (char === "(") depth += 1;
|
|
1600
|
+
else if (char === ")") {
|
|
1601
|
+
depth -= 1;
|
|
1602
|
+
if (depth === 0) return {
|
|
1603
|
+
body: text.slice(open, index),
|
|
1604
|
+
end: index + 1
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
function splitTop(body) {
|
|
1610
|
+
const items = [];
|
|
1611
|
+
let depth = 0;
|
|
1612
|
+
let from = 0;
|
|
1613
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
1614
|
+
const char = body[index];
|
|
1615
|
+
if (char === "'" || char === "\"") index = Math.max(body.indexOf(char, index + 1), index);
|
|
1616
|
+
else if (char === "(") depth += 1;
|
|
1617
|
+
else if (char === ")") depth -= 1;
|
|
1618
|
+
else if (char === "," && depth === 0) {
|
|
1619
|
+
items.push(body.slice(from, index));
|
|
1620
|
+
from = index + 1;
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
items.push(body.slice(from));
|
|
1624
|
+
return items.map((item) => item.trim());
|
|
1625
|
+
}
|
|
1626
|
+
/** The first capture group of a match; every pattern here captures it. */
|
|
1627
|
+
function captured(match) {
|
|
1628
|
+
return match.slice(1, 2).join("");
|
|
1629
|
+
}
|
|
1630
|
+
/** The index just past a match. */
|
|
1631
|
+
function endOf(match) {
|
|
1632
|
+
return match.index + match[0].length;
|
|
1633
|
+
}
|
|
1634
|
+
/** The plain key columns of an index column list, cut at the first expression. */
|
|
1635
|
+
function keyColumns(body) {
|
|
1636
|
+
const columns = [];
|
|
1637
|
+
for (const item of splitTop(body)) {
|
|
1638
|
+
const match = new RegExp(`^(${IDENT})(?:\\s|$)`, "u").exec(item);
|
|
1639
|
+
if (match === null || item.startsWith("(")) break;
|
|
1640
|
+
columns.push(bare(captured(match)));
|
|
1641
|
+
}
|
|
1642
|
+
return columns;
|
|
1643
|
+
}
|
|
1644
|
+
/** Key columns of each index, unique constraint and primary key the SQL creates, per `schema.table`. */
|
|
1645
|
+
function indexKeysFromSql(sql) {
|
|
1646
|
+
const text = stripComments(sql);
|
|
1647
|
+
const keys = {};
|
|
1648
|
+
const add = (table, columns) => {
|
|
1649
|
+
if (columns.length > 0) (keys[qualified(table)] ??= []).push(columns);
|
|
1650
|
+
};
|
|
1651
|
+
for (const pattern of [CREATE_INDEX, ADD_KEY]) for (const match of text.matchAll(pattern)) {
|
|
1652
|
+
const found = group(text, endOf(match));
|
|
1653
|
+
if (found !== void 0) add(captured(match), keyColumns(found.body));
|
|
1654
|
+
}
|
|
1655
|
+
for (const match of text.matchAll(CREATE_TABLE)) {
|
|
1656
|
+
const found = group(text, endOf(match));
|
|
1657
|
+
if (found === void 0) continue;
|
|
1658
|
+
for (const item of splitTop(found.body)) if (TABLE_KEY.test(item)) {
|
|
1659
|
+
const inner = group(item, item.indexOf("(") + 1);
|
|
1660
|
+
if (inner !== void 0) add(captured(match), keyColumns(inner.body));
|
|
1661
|
+
} else if (!NOT_A_COLUMN.test(item)) {
|
|
1662
|
+
const column = COLUMN_KEY.exec(item)?.[1];
|
|
1663
|
+
if (column !== void 0) add(captured(match), [bare(column)]);
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
return keys;
|
|
1667
|
+
}
|
|
1668
|
+
/** The index keys of the SQL files under the migration and schema folders of `cwd`. */
|
|
1669
|
+
function indexKeysFromProject(cwd) {
|
|
1670
|
+
const all = {};
|
|
1671
|
+
for (const file of sqlFiles(cwd, [...MIGRATION_DIRS])) {
|
|
1672
|
+
const keys = indexKeysFromSql(readFileSync(file, "utf8").replaceAll(GENERATED_INDEX, ""));
|
|
1673
|
+
for (const [table, list] of Object.entries(keys)) (all[table] ??= []).push(...list);
|
|
1674
|
+
}
|
|
1675
|
+
return all;
|
|
1676
|
+
}
|
|
1677
|
+
/** Every `schema.table` the SQL creates, temporary tables left out. */
|
|
1678
|
+
function tablesFromSql(sql) {
|
|
1679
|
+
return new Set([...stripComments(sql).matchAll(TABLE_NAME)].map((match) => qualified(captured(match))));
|
|
1680
|
+
}
|
|
1681
|
+
/** Every `schema.table` the SQL files under the migration and schema folders of `cwd` create. */
|
|
1682
|
+
function tablesFromProject(cwd) {
|
|
1683
|
+
const all = /* @__PURE__ */ new Set();
|
|
1684
|
+
for (const file of sqlFiles(cwd, [...MIGRATION_DIRS])) for (const table of tablesFromSql(readFileSync(file, "utf8"))) all.add(table);
|
|
1685
|
+
return all;
|
|
1686
|
+
}
|
|
1687
|
+
/** The targets no index in `keys` leads with: its key columns start with the target's, as `pruneIndexTargets` reads a database. */
|
|
1688
|
+
function withoutServedTargets(targets, keys) {
|
|
1689
|
+
return targets.filter((target) => !(keys[target.table] ?? []).some((key) => target.columns.every((column, at) => key[at] === column)));
|
|
1690
|
+
}
|
|
1691
|
+
//#endregion
|
|
1810
1692
|
//#region src/cli/rls/generate.ts
|
|
1811
1693
|
function loadPolicy(cwd, config, from) {
|
|
1812
1694
|
const policyPath = from === "drizzle" ? config.policy : from ?? config.policy;
|
|
@@ -1884,8 +1766,19 @@ async function runRlsGenerate(input) {
|
|
|
1884
1766
|
const { ctx } = built;
|
|
1885
1767
|
const { text, migration, migrationRel, helpersOnly } = generated;
|
|
1886
1768
|
let indexes = generated.indexes;
|
|
1769
|
+
const bindings = tableBindings(generated.compiled.rowColumns.map((item) => item.resource), tableNames(input.config.rls?.tables), helpersOnly);
|
|
1770
|
+
if (input.config.rls?.tables === void 0) warnings.push(...bindingNotes(bindings, input.db !== void 0));
|
|
1771
|
+
warnings.push(...triggerNotes(text));
|
|
1772
|
+
if (ctx.seeds === "function") warnings.push(`run select ${built.schema}.seed(); after each deploy`);
|
|
1773
|
+
if (input.db === void 0) {
|
|
1774
|
+
indexes = withoutServedTargets(indexes, indexKeysFromProject(input.cwd));
|
|
1775
|
+
warnings.push(...unknownTableNotes(bindings, tablesFromProject(input.cwd)));
|
|
1776
|
+
}
|
|
1887
1777
|
if (input.db !== void 0) try {
|
|
1888
|
-
const
|
|
1778
|
+
const facts = await readTableIndexFacts(input.db, [.../* @__PURE__ */ new Set([...indexes.map((target) => target.table), ...bindings.map((binding) => binding.table)])], input.connect);
|
|
1779
|
+
const missing = missingBindings(bindings, facts.columns);
|
|
1780
|
+
if (missing.length > 0) return refused(`rls generate: ${missing.map((binding) => `resource ${binding.resource} is bound to ${binding.table}, which the database does not have`).join("; ")}; name the table in rls.tables, or set rls.tables.<resource>: false for a resource with no table`);
|
|
1781
|
+
const pruned = pruneIndexTargets(indexes, facts);
|
|
1889
1782
|
indexes = pruned.targets;
|
|
1890
1783
|
warnings.push(...pruned.warnings);
|
|
1891
1784
|
} catch (cause) {
|
|
@@ -1907,10 +1800,19 @@ async function runRlsGenerate(input) {
|
|
|
1907
1800
|
keys: {
|
|
1908
1801
|
permissions: listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key),
|
|
1909
1802
|
rowConditions: [...policyRowConditionKeys(policy)],
|
|
1803
|
+
levels: levelKeys(policy),
|
|
1910
1804
|
renamed: Object.fromEntries(renamedKeys(policy.vocabulary.permissions))
|
|
1911
1805
|
},
|
|
1912
1806
|
schema: built.schema,
|
|
1913
1807
|
helpersOnly,
|
|
1808
|
+
explain: explainRls({
|
|
1809
|
+
config: input.config,
|
|
1810
|
+
ctx,
|
|
1811
|
+
schema: built.schema,
|
|
1812
|
+
authorize: built.authorize,
|
|
1813
|
+
generated,
|
|
1814
|
+
text
|
|
1815
|
+
}),
|
|
1914
1816
|
...extras
|
|
1915
1817
|
};
|
|
1916
1818
|
const planned = outputFiles({
|
|
@@ -2034,7 +1936,7 @@ function missingIndexes(expected, actual) {
|
|
|
2034
1936
|
return expected.indexes.flatMap((target) => {
|
|
2035
1937
|
const column = target.columns[0];
|
|
2036
1938
|
const columns = actual.columns?.[target.table];
|
|
2037
|
-
return column === void 0 || columns !== void 0 && !columns.includes(column) || actual.leadingColumns[target.table]?.includes(column) === true ? [] : [
|
|
1939
|
+
return column === void 0 || columns !== void 0 && !columns.includes(column) || actual.leadingColumns[target.table]?.includes(column) === true ? [] : [rlsFinding("index-missing", target.table, column, `${target.table}: no index starts with ${column}, which the policies or helpers filter on; add it, or write it with rls generate --split ...,indexes`, "warning")];
|
|
2038
1940
|
});
|
|
2039
1941
|
}
|
|
2040
1942
|
function sameList(left, right) {
|
|
@@ -2042,42 +1944,42 @@ function sameList(left, right) {
|
|
|
2042
1944
|
const b = right.toSorted();
|
|
2043
1945
|
return a.length === b.length && a.every((item, index) => item === b[index]);
|
|
2044
1946
|
}
|
|
2045
|
-
/** One
|
|
1947
|
+
/** One finding per difference; policy expressions are not compared, since Postgres rewrites their text. */
|
|
2046
1948
|
function diffRls(expected, actual, options = {}) {
|
|
2047
1949
|
const out = [];
|
|
2048
1950
|
const found = new Map(actual.policies.map((item) => [`${item.table}\u0000${item.name}`, item]));
|
|
2049
1951
|
for (const want of expected.policies) {
|
|
2050
1952
|
const have = found.get(`${want.table}\u0000${want.name}`);
|
|
2051
1953
|
if (have === void 0) {
|
|
2052
|
-
out.push(`${want.table}: policy ${want.name} is missing`);
|
|
1954
|
+
out.push(rlsFinding("policy-missing", want.table, want.name, `${want.table}: policy ${want.name} is missing`));
|
|
2053
1955
|
continue;
|
|
2054
1956
|
}
|
|
2055
|
-
if (have.command !== want.command) out.push(`${want.table}: policy ${want.name} is for ${have.command}, expected ${want.command}`);
|
|
2056
|
-
if (have.permissive !== want.permissive) out.push(`${want.table}: policy ${want.name} is ${have.permissive ? "permissive" : "restrictive"}, expected ${want.permissive ? "permissive" : "restrictive"}`);
|
|
2057
|
-
if (!sameList(have.roles, want.roles)) out.push(`${want.table}: policy ${want.name} applies to ${have.roles.join(", ")}, expected ${want.roles.join(", ")}`);
|
|
1957
|
+
if (have.command !== want.command) out.push(rlsFinding("policy-command", want.table, want.name, `${want.table}: policy ${want.name} is for ${have.command}, expected ${want.command}`));
|
|
1958
|
+
if (have.permissive !== want.permissive) out.push(rlsFinding("policy-mode", want.table, want.name, `${want.table}: policy ${want.name} is ${have.permissive ? "permissive" : "restrictive"}, expected ${want.permissive ? "permissive" : "restrictive"}`));
|
|
1959
|
+
if (!sameList(have.roles, want.roles)) out.push(rlsFinding("policy-roles", want.table, want.name, `${want.table}: policy ${want.name} applies to ${have.roles.join(", ")}, expected ${want.roles.join(", ")}`));
|
|
2058
1960
|
}
|
|
2059
1961
|
const wanted = new Set(expected.policies.map((item) => `${item.table}\u0000${item.name}`));
|
|
2060
|
-
for (const have of actual.policies) if (expected.tables.includes(have.table) && !wanted.has(`${have.table}\u0000${have.name}`)) out.push(`${have.table}: policy ${have.name} is not generated (${have.permissive ? "permissive" : "restrictive"} ${have.command}); a permissive one widens access`);
|
|
1962
|
+
for (const have of actual.policies) if (expected.tables.includes(have.table) && !wanted.has(`${have.table}\u0000${have.name}`)) out.push(rlsFinding("policy-extra", have.table, have.name, `${have.table}: policy ${have.name} is not generated (${have.permissive ? "permissive" : "restrictive"} ${have.command}); a permissive one widens access`));
|
|
2061
1963
|
for (const table of expected.tables) {
|
|
2062
1964
|
if (actual.rlsEnabled[table] !== true) {
|
|
2063
|
-
out.push(actual.rlsEnabled[table] === void 0 ? `${table}: table is missing` : `${table}: row level security is disabled`);
|
|
1965
|
+
out.push(actual.rlsEnabled[table] === void 0 ? rlsFinding("table-missing", table, "table", `${table}: table is missing`) : rlsFinding("rls-disabled", table, "table", `${table}: row level security is disabled`));
|
|
2064
1966
|
continue;
|
|
2065
1967
|
}
|
|
2066
1968
|
for (const role of ROLES) {
|
|
2067
1969
|
const want = expected.grants[table]?.[role] ?? [];
|
|
2068
1970
|
const have = actual.grants[table]?.[role] ?? [];
|
|
2069
|
-
for (const privilege of want) if (!have.includes(privilege) && !(privilege === "select" && options.columnGrants === true)) out.push(`${table}: ${role} lacks ${privilege}, so the policy answers 42501 instead of filtering`);
|
|
2070
|
-
for (const privilege of have) if (!want.includes(privilege)) out.push(`${table}: ${role} holds ${privilege}, which no generated policy allows`);
|
|
1971
|
+
for (const privilege of want) if (!have.includes(privilege) && !(privilege === "select" && options.columnGrants === true)) out.push(rlsFinding("grant-missing", table, `${role}.${privilege}`, `${table}: ${role} lacks ${privilege}, so the policy answers 42501 instead of filtering`));
|
|
1972
|
+
for (const privilege of have) if (!want.includes(privilege)) out.push(rlsFinding("grant-extra", table, `${role}.${privilege}`, `${table}: ${role} holds ${privilege}, which no generated policy allows`));
|
|
2071
1973
|
}
|
|
2072
1974
|
}
|
|
2073
1975
|
for (const helper of expected.helpers) {
|
|
2074
1976
|
const have = actual.helpers[helper];
|
|
2075
1977
|
if (have === void 0) {
|
|
2076
|
-
out.push(`${helper}: helper is missing`);
|
|
1978
|
+
out.push(rlsFinding("helper-missing", helper, "helper", `${helper}: helper is missing`));
|
|
2077
1979
|
continue;
|
|
2078
1980
|
}
|
|
2079
|
-
if (!have.securityDefiner) out.push(`${helper}: helper is not security definer`);
|
|
2080
|
-
if (!have.emptySearchPath) out.push(`${helper}: helper does not set search_path = ''`);
|
|
1981
|
+
if (!have.securityDefiner) out.push(rlsFinding("helper-definer", helper, "helper", `${helper}: helper is not security definer`));
|
|
1982
|
+
if (!have.emptySearchPath) out.push(rlsFinding("helper-search-path", helper, "helper", `${helper}: helper does not set search_path = ''`));
|
|
2081
1983
|
}
|
|
2082
1984
|
return out;
|
|
2083
1985
|
}
|
|
@@ -2098,12 +2000,6 @@ const HELPERS_SQL = `select n.nspname || '.' || p.proname as target, p.prosecdef
|
|
|
2098
2000
|
from pg_proc p
|
|
2099
2001
|
join pg_namespace n on n.oid = p.pronamespace
|
|
2100
2002
|
where n.nspname || '.' || p.proname = any($1::text[])`;
|
|
2101
|
-
const INDEXES_SQL = `select n.nspname || '.' || c.relname as target, a.attname as leading
|
|
2102
|
-
from pg_index i
|
|
2103
|
-
join pg_class c on c.oid = i.indrelid
|
|
2104
|
-
join pg_namespace n on n.oid = c.relnamespace
|
|
2105
|
-
join pg_attribute a on a.attrelid = i.indrelid and a.attnum = i.indkey[0]
|
|
2106
|
-
where i.indpred is null and n.nspname || '.' || c.relname = any($1::text[])`;
|
|
2107
2003
|
function commandOf(cmd) {
|
|
2108
2004
|
const text = String(cmd ?? "").toLowerCase();
|
|
2109
2005
|
return text === "*" ? "all" : text;
|
|
@@ -2122,13 +2018,16 @@ async function introspectRls(db, expected, connect = connectIntrospect) {
|
|
|
2122
2018
|
client.query(TABLES_SQL, [tables]),
|
|
2123
2019
|
client.query(GRANTS_SQL, [tables]),
|
|
2124
2020
|
client.query(HELPERS_SQL, [[...expected.helpers]]),
|
|
2125
|
-
client.query(
|
|
2021
|
+
client.query(INDEX_COLUMNS_SQL, [indexed]),
|
|
2126
2022
|
client.query(COLUMNS_SQL, [indexed])
|
|
2127
2023
|
]);
|
|
2128
2024
|
const columnsOf = {};
|
|
2129
2025
|
for (const row of columns.rows) (columnsOf[String(row["target"])] ??= []).push(String(row["name"]));
|
|
2130
2026
|
const leadingColumns = {};
|
|
2131
|
-
for (const row of indexes.rows)
|
|
2027
|
+
for (const row of indexes.rows) {
|
|
2028
|
+
const lead = (Array.isArray(row["columns"]) ? row["columns"] : []).at(0);
|
|
2029
|
+
if (typeof lead === "string") (leadingColumns[String(row["target"])] ??= []).push(lead);
|
|
2030
|
+
}
|
|
2132
2031
|
const byTable = {};
|
|
2133
2032
|
for (const row of grants.rows) {
|
|
2134
2033
|
const table = String(row["target"]);
|
|
@@ -2176,7 +2075,7 @@ function unguardedWrites(policies, fn) {
|
|
|
2176
2075
|
const permissive = applying.filter((policy) => policy.restrictive !== true && (policy.roles ?? []).some((role) => role === "authenticated" || role === "public"));
|
|
2177
2076
|
if (permissive.length === 0) continue;
|
|
2178
2077
|
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)`);
|
|
2078
|
+
if (!(applying.some((policy) => policy.restrictive === true && calls(policy.expression)) || permissive.every((policy) => calls(policy.expression)))) out.push(rlsFinding("write-unguarded", table, command, `${table}: ${command} policies for authenticated do not call ${fn}(), so a read-only support or impersonation session can write (rls.readOnlyActors)`));
|
|
2180
2079
|
}
|
|
2181
2080
|
return out;
|
|
2182
2081
|
}
|
|
@@ -2205,26 +2104,37 @@ function inScope(table) {
|
|
|
2205
2104
|
function seedLine(row) {
|
|
2206
2105
|
return `${row.role} ${row.effect} ${row.grantKey} on ${row.scope}`;
|
|
2207
2106
|
}
|
|
2107
|
+
function seedKey(line) {
|
|
2108
|
+
return line.replace(" on ", " ").replaceAll(" ", "/");
|
|
2109
|
+
}
|
|
2208
2110
|
/** Drift is an error; `info` names RLS tables whose policies call no PermDock helper. */
|
|
2209
2111
|
function diffMixed(expected, actual) {
|
|
2210
2112
|
const drift = [];
|
|
2113
|
+
const seeds = `${expected.schema}.role_permissions`;
|
|
2211
2114
|
const want = new Set(expected.seeds.map(seedLine));
|
|
2212
2115
|
const have = new Set(actual.seeds.map(seedLine));
|
|
2213
|
-
for (const line of want) if (!have.has(line)) drift.push(`${
|
|
2214
|
-
for (const line of have) if (!want.has(line)) drift.push(`${
|
|
2116
|
+
for (const line of want) if (!have.has(line)) drift.push(rlsFinding("seed-missing", seeds, seedKey(line), `${seeds}: missing ${line}`));
|
|
2117
|
+
for (const line of have) if (!want.has(line)) drift.push(rlsFinding("seed-extra", seeds, seedKey(line), `${seeds}: unexpected ${line}`));
|
|
2215
2118
|
const permissions = new Set(expected.permissions);
|
|
2216
2119
|
const conditioned = new Set(expected.rowConditions);
|
|
2120
|
+
const levels = new Map((expected.levels ?? []).map((level) => [level.key, level]));
|
|
2121
|
+
const notes = [];
|
|
2217
2122
|
const covered = /* @__PURE__ */ new Set();
|
|
2218
2123
|
for (const policy of actual.policies) {
|
|
2219
2124
|
if (callsHelper(policy.expression)) covered.add(policy.table);
|
|
2220
2125
|
const blind = new Set(helperCallKeys(policy.expression, true));
|
|
2221
|
-
for (const key of helperCallKeys(policy.expression))
|
|
2222
|
-
|
|
2126
|
+
for (const key of helperCallKeys(policy.expression)) {
|
|
2127
|
+
const level = levels.get(key);
|
|
2128
|
+
if (level !== void 0) {
|
|
2129
|
+
if (level.conditioned) notes.push(rlsFinding("level-unconditioned", policy.table, `${policy.name}/${key}`, `${policy.table}: policy ${policy.name} passes ${key}, which skips the level's row condition, so holders of it are denied there; use ${level.rowHelper}(${policy.table.slice(policy.table.lastIndexOf(".") + 1)}, '${level.permission}')`, "info"));
|
|
2130
|
+
} else if (!permissions.has(key)) drift.push(rlsFinding("undeclared-key", policy.table, `${policy.name}/${key}`, `${policy.table}: policy ${policy.name} passes ${key}, which the policy does not declare, so it always denies`));
|
|
2131
|
+
else if (conditioned.has(key) && blind.has(key)) drift.push(rlsFinding("condition-blind", policy.table, `${policy.name}/${key}`, `${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`));
|
|
2132
|
+
}
|
|
2223
2133
|
}
|
|
2224
2134
|
if (expected.readOnlyActors === true) drift.push(...unguardedWrites(actual.policies, ACTOR_MAY_WRITE));
|
|
2225
2135
|
return {
|
|
2226
2136
|
drift,
|
|
2227
|
-
info: actual.rlsTables.filter((table) => !covered.has(table)).map((table) => `${table}: no policy calls a PermDock helper
|
|
2137
|
+
info: [...notes, ...actual.rlsTables.filter((table) => !covered.has(table)).map((table) => rlsFinding("helper-uncalled", table, "table", `${table}: no policy calls a PermDock helper`, "info"))]
|
|
2228
2138
|
};
|
|
2229
2139
|
}
|
|
2230
2140
|
const ALL_POLICIES_SQL = `select schemaname || '.' || tablename as target, policyname,
|
|
@@ -2270,12 +2180,15 @@ async function introspectMixed(db, schema, connect = connectIntrospect) {
|
|
|
2270
2180
|
}
|
|
2271
2181
|
//#endregion
|
|
2272
2182
|
//#region src/cli/rls/rls.ts
|
|
2273
|
-
|
|
2183
|
+
/** `rls migrate` and `rls import` read the pgsql-parser 18 AST. */
|
|
2184
|
+
const PGSQL_PARSER_MAJOR = 18;
|
|
2185
|
+
const RLS_HELP = `permdock rls generate | import | verify | migrate | explain
|
|
2274
2186
|
|
|
2275
2187
|
generate Compile the policy into RLS policies and helpers
|
|
2276
2188
|
import Turn existing policies (--sql or --db-url-stdin) into a permissions module
|
|
2277
2189
|
verify Run the fixtures, or compare a live database with --introspect
|
|
2278
2190
|
migrate Rewrite the rls.migrate helpers' calls onto the generated helpers
|
|
2191
|
+
explain Show the tables, membership sources, custom roles and triggers the SQL binds to
|
|
2279
2192
|
|
|
2280
2193
|
Run permdock rls <command> --help for its flags.
|
|
2281
2194
|
Never emits service_role, except in the execute grant rls.trustedReaders asks for. memberOf compiles through the dialect memberships mapping.
|
|
@@ -2318,11 +2231,48 @@ function generateInput(input, target, dialect) {
|
|
|
2318
2231
|
...isSupabaseSchemas(input.schemas) ? { schemas: input.schemas } : {},
|
|
2319
2232
|
...input.grantsOut === void 0 ? {} : { grantsOut: input.grantsOut },
|
|
2320
2233
|
...input.seedsOut === void 0 ? {} : { seedsOut: input.seedsOut },
|
|
2234
|
+
...input.noSeeds === true ? { noSeeds: true } : {},
|
|
2321
2235
|
...input.backfillOut === void 0 ? {} : { backfillOut: input.backfillOut },
|
|
2322
2236
|
helpersOnly: input.helpersOnly,
|
|
2323
2237
|
shims: input.shims === true
|
|
2324
2238
|
};
|
|
2325
2239
|
}
|
|
2240
|
+
async function explain(input) {
|
|
2241
|
+
const dialect = asDialect(input.dialect ?? input.config.rls?.dialect);
|
|
2242
|
+
if (dialect === void 0) return {
|
|
2243
|
+
code: 2,
|
|
2244
|
+
output: "rls explain --dialect (or rls.dialect) must be supabase, neon or guc"
|
|
2245
|
+
};
|
|
2246
|
+
const generated = await runRlsGenerate({
|
|
2247
|
+
...generateInput(input, "sql", dialect),
|
|
2248
|
+
check: false,
|
|
2249
|
+
write: false
|
|
2250
|
+
});
|
|
2251
|
+
if (generated.explain === void 0) return {
|
|
2252
|
+
code: generated.code,
|
|
2253
|
+
output: generated.output
|
|
2254
|
+
};
|
|
2255
|
+
let report = generated.explain;
|
|
2256
|
+
if (input.db !== void 0) try {
|
|
2257
|
+
const facts = await readTableIndexFacts(input.db, report.bindings.map((binding) => binding.table), input.connect);
|
|
2258
|
+
report = {
|
|
2259
|
+
...report,
|
|
2260
|
+
bindings: report.bindings.map((binding) => ({
|
|
2261
|
+
...binding,
|
|
2262
|
+
exists: facts.columns[binding.table] !== void 0
|
|
2263
|
+
}))
|
|
2264
|
+
};
|
|
2265
|
+
} catch (cause) {
|
|
2266
|
+
return {
|
|
2267
|
+
code: exitCodeOfError(cause),
|
|
2268
|
+
output: describeError(cause)
|
|
2269
|
+
};
|
|
2270
|
+
}
|
|
2271
|
+
return {
|
|
2272
|
+
code: 0,
|
|
2273
|
+
output: input.json ? JSON.stringify(report, null, 2) : describeExplain(report)
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2326
2276
|
async function migrate(input) {
|
|
2327
2277
|
const config = input.config.rls?.migrate;
|
|
2328
2278
|
if (config === void 0) return {
|
|
@@ -2347,8 +2297,8 @@ async function migrate(input) {
|
|
|
2347
2297
|
code: 2,
|
|
2348
2298
|
output: generated.output
|
|
2349
2299
|
};
|
|
2350
|
-
await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls migrate", input.cwd);
|
|
2351
|
-
const { migrateTarget, runRlsMigrate } = await import("./migrate-
|
|
2300
|
+
await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls migrate", input.cwd, PGSQL_PARSER_MAJOR);
|
|
2301
|
+
const { migrateTarget, runRlsMigrate } = await import("./migrate-Dodel0IJ.js");
|
|
2352
2302
|
return runRlsMigrate({
|
|
2353
2303
|
cwd: input.cwd,
|
|
2354
2304
|
sql: input.sql,
|
|
@@ -2365,7 +2315,7 @@ async function migrate(input) {
|
|
|
2365
2315
|
});
|
|
2366
2316
|
}
|
|
2367
2317
|
/** `verify --introspect` with `--helpers-only`: helpers and seeds exactly, hand-written policies by their keys. */
|
|
2368
|
-
async function introspectHelpersOnly(db, expected, generated, connect, readOnlyActors) {
|
|
2318
|
+
async function introspectHelpersOnly(db, expected, generated, connect, readOnlyActors, ignore) {
|
|
2369
2319
|
const schema = generated.schema ?? "permdock";
|
|
2370
2320
|
try {
|
|
2371
2321
|
const actual = await introspectRls(db, expected, connect);
|
|
@@ -2376,31 +2326,25 @@ async function introspectHelpersOnly(db, expected, generated, connect, readOnlyA
|
|
|
2376
2326
|
seeds: generated.seeds ?? [],
|
|
2377
2327
|
permissions: generated.keys?.permissions ?? [],
|
|
2378
2328
|
rowConditions: generated.keys?.rowConditions ?? [],
|
|
2329
|
+
levels: generated.keys?.levels ?? [],
|
|
2379
2330
|
readOnlyActors
|
|
2380
2331
|
}, await introspectMixed(db, schema, connect));
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
output: [
|
|
2386
|
-
...drift,
|
|
2332
|
+
return verifyResult({
|
|
2333
|
+
findings: [
|
|
2334
|
+
...helpers,
|
|
2335
|
+
...mixed.drift,
|
|
2387
2336
|
...warnings,
|
|
2388
|
-
...info
|
|
2389
|
-
]
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
...warnings,
|
|
2394
|
-
...info,
|
|
2395
|
-
`introspected ${String(expected.helpers.length)} helper(s) and ${String(generated.seeds?.length ?? 0)} seeded row(s), helpers only: no drift`
|
|
2396
|
-
].join("\n")
|
|
2397
|
-
};
|
|
2337
|
+
...mixed.info
|
|
2338
|
+
],
|
|
2339
|
+
ignore,
|
|
2340
|
+
summary: `introspected ${String(expected.helpers.length)} helper(s) and ${String(generated.seeds?.length ?? 0)} seeded row(s), helpers only: no drift`
|
|
2341
|
+
});
|
|
2398
2342
|
} catch (cause) {
|
|
2399
2343
|
return usageResult(cause);
|
|
2400
2344
|
}
|
|
2401
2345
|
}
|
|
2402
2346
|
/** `verify --introspect`: the catalogs against what `generate` would write with the same flags. */
|
|
2403
|
-
async function introspect(input) {
|
|
2347
|
+
async function introspect(input, ignore) {
|
|
2404
2348
|
if (input.db === void 0) return {
|
|
2405
2349
|
code: 2,
|
|
2406
2350
|
output: "PermDock CLI: rls verify --introspect needs a database: --db-url-stdin, DATABASE_URL or SUPABASE_DB_URL"
|
|
@@ -2420,19 +2364,15 @@ async function introspect(input) {
|
|
|
2420
2364
|
output: generated.output
|
|
2421
2365
|
};
|
|
2422
2366
|
const expected = expectedRls(generated.policies, generated.text, generated.indexes);
|
|
2423
|
-
if (generated.helpersOnly === true) return introspectHelpersOnly(input.db, expected, generated, input.connect, readOnlyActorKinds(input.config.rls?.readOnlyActors) !== void 0);
|
|
2367
|
+
if (generated.helpersOnly === true) return introspectHelpersOnly(input.db, expected, generated, input.connect, readOnlyActorKinds(input.config.rls?.readOnlyActors) !== void 0, ignore);
|
|
2424
2368
|
try {
|
|
2425
2369
|
const actual = await introspectRls(input.db, expected, input.connect);
|
|
2426
|
-
const drift = diffRls(expected, actual, { columnGrants: (input.fields ?? input.config.rls?.fields)
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
};
|
|
2432
|
-
return {
|
|
2433
|
-
code: 0,
|
|
2434
|
-
output: `${warnings.map((line) => `${line}\n`).join("")}introspected ${String(expected.policies.length)} policies on ${String(expected.tables.length)} table(s) and ${String(expected.helpers.length)} helper(s): no drift`
|
|
2435
|
-
};
|
|
2370
|
+
const drift = diffRls(expected, actual, { columnGrants: (input.fields ?? input.config.rls?.fields) !== void 0 });
|
|
2371
|
+
return verifyResult({
|
|
2372
|
+
findings: [...drift, ...missingIndexes(expected, actual)],
|
|
2373
|
+
ignore,
|
|
2374
|
+
summary: `introspected ${String(expected.policies.length)} policies on ${String(expected.tables.length)} table(s) and ${String(expected.helpers.length)} helper(s): no drift`
|
|
2375
|
+
});
|
|
2436
2376
|
} catch (cause) {
|
|
2437
2377
|
return usageResult(cause);
|
|
2438
2378
|
}
|
|
@@ -2467,8 +2407,8 @@ async function runRls(input) {
|
|
|
2467
2407
|
});
|
|
2468
2408
|
}
|
|
2469
2409
|
case "import": {
|
|
2470
|
-
await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls import", input.cwd);
|
|
2471
|
-
const { runRlsImport } = await import("./import-
|
|
2410
|
+
await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls import", input.cwd, PGSQL_PARSER_MAJOR);
|
|
2411
|
+
const { runRlsImport } = await import("./import-Bz0GefWg.js");
|
|
2472
2412
|
return runRlsImport({
|
|
2473
2413
|
cwd: input.cwd,
|
|
2474
2414
|
config: input.config,
|
|
@@ -2490,7 +2430,8 @@ async function runRls(input) {
|
|
|
2490
2430
|
...input.db === void 0 ? {} : { db: input.db },
|
|
2491
2431
|
...input.exec === void 0 ? {} : { exec: input.exec }
|
|
2492
2432
|
});
|
|
2493
|
-
|
|
2433
|
+
const ignore = verifyIgnores(input.config, input.ignore);
|
|
2434
|
+
if (input.introspect) return introspect(input, ignore);
|
|
2494
2435
|
const format = input.format ?? "node";
|
|
2495
2436
|
if (format !== "node" && format !== "pgtap") return {
|
|
2496
2437
|
code: 2,
|
|
@@ -2502,13 +2443,16 @@ async function runRls(input) {
|
|
|
2502
2443
|
format,
|
|
2503
2444
|
tree: input.tree,
|
|
2504
2445
|
io: input.io,
|
|
2446
|
+
...input.out === void 0 ? {} : { out: input.out },
|
|
2505
2447
|
...input.fixtures === void 0 ? {} : { fixtures: input.fixtures },
|
|
2506
2448
|
...input.db === void 0 ? {} : { db: input.db },
|
|
2507
2449
|
...input.from === void 0 ? {} : { from: input.from },
|
|
2508
|
-
...input.connect === void 0 ? {} : { connect: input.connect }
|
|
2450
|
+
...input.connect === void 0 ? {} : { connect: input.connect },
|
|
2451
|
+
ignore
|
|
2509
2452
|
});
|
|
2510
2453
|
}
|
|
2511
2454
|
case "migrate": return migrate(input);
|
|
2455
|
+
case "explain": return explain(input);
|
|
2512
2456
|
default: return {
|
|
2513
2457
|
code: 2,
|
|
2514
2458
|
output: RLS_HELP
|