permdock 0.3.1 → 0.4.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/dist/a2a/index.d.ts +7 -5
- package/dist/a2a/index.js +14 -21
- package/dist/ai-sdk/index.d.ts +18 -6
- package/dist/ai-sdk/index.js +8 -5
- package/dist/app-data-CV8pqqMi.js +59 -0
- package/dist/approvals/index.d.ts +2 -2
- package/dist/approvals/index.js +3 -3
- package/dist/{approvals-DbYAuNkG.js → approvals-DZL76lKW.js} +35 -19
- package/dist/{approvers-DT4AVyIg.js → approvers-CqPOLD-o.js} +1 -1
- package/dist/{arazzo-BAgw9ap6.js → arazzo-C4BcskbX.js} +6 -5
- package/dist/{arazzo-C9chp3ST.js → arazzo-CD8d0WLb.js} +2 -2
- package/dist/authzen/index.d.ts +3 -3
- package/dist/authzen/index.js +161 -23
- package/dist/better-auth/index.d.ts +2 -2
- package/dist/better-auth/index.js +6 -19
- package/dist/better-supabase/index.d.ts +112 -87
- package/dist/better-supabase/index.js +257 -156
- package/dist/{bind-C_ob3LKa.d.ts → bind-BzqG8SMb.d.ts} +16 -5
- package/dist/{bind-CuzGytRf.js → bind-kk-uyCwH.js} +14 -6
- package/dist/build-instance-DIIV9DbC.js +51 -0
- package/dist/{canonical-json-DemiK4Vp.js → canonical-json-CFKyFOke.js} +1 -3
- package/dist/{capability-2kYTMWDv.d.ts → capability-CksyCWDH.d.ts} +2 -2
- package/dist/{capability-4qPdO9gk.js → capability-txATtKV4.js} +3 -2
- package/dist/catalog/index.d.ts +1 -1
- package/dist/catalog/index.js +1 -1
- package/dist/catalog-6yWAX6Ps.js +2 -0
- package/dist/{catalog-CYj-mX2O.js → catalog-WHG4kBq7.js} +2 -2
- package/dist/{catalog-doc-DdGm82XT.js → catalog-doc-DWtg0gGR.js} +17 -10
- package/dist/{catalog-DWF5cYqB.js → catalog-tZUyOfOv.js} +4 -4
- package/dist/claude-agent/index.d.ts +4 -4
- package/dist/claude-agent/index.js +4 -5
- package/dist/clerk/index.d.ts +3 -3
- package/dist/clerk/index.js +8 -33
- package/dist/cli/bin.js +1 -1
- package/dist/cli/index.d.ts +3 -3
- package/dist/cli/index.js +3 -3
- package/dist/cloud/index.d.ts +4 -4
- package/dist/cloud/index.js +9 -15
- package/dist/{cloud-CfwZ39Rl.js → cloud-C7bSDgX4.js} +2 -2
- package/dist/{cloud-DWLXO2af.js → cloud-PeBN3IEn.js} +5 -5
- package/dist/{collect-DSTE3ml9.js → collect-2SrB7lfk.js} +6 -8
- package/dist/collect-Bw2Jdq1q.js +2 -0
- package/dist/{collect-CHB22Sx4.js → collect-CQx4hX4N.js} +3 -3
- package/dist/{commands-BeaGNj_o.js → commands-Cpip4ULx.js} +13 -13
- package/dist/{compact-y6vfu2_f.js → compact-CAf557ZB.js} +11 -1
- package/dist/compile/index.d.ts +1 -1
- package/dist/compile/index.js +1 -1
- package/dist/{compile-Ba4blROR.js → compile-CcEu2ZQ1.js} +6 -5
- package/dist/{compile-BkRyBDBw.d.ts → compile-CtTP7eur.d.ts} +2 -2
- package/dist/{config-BJiM-xAm.js → config-BIjREpyE.js} +4 -4
- package/dist/{config-ir40y8m0.js → config-DLt6BvV7.js} +2 -2
- package/dist/{config-report-CdCgbRnV.js → config-report--Xb9APJS.js} +5 -6
- package/dist/{connection-CU8MQxRi.js → connection-DUsczpB5.js} +3 -2
- package/dist/constants-DcJABbLW.js +20 -0
- package/dist/{context-Bcy85rGi.js → context-DieZVJXu.js} +16 -5
- package/dist/convex/index.d.ts +4 -4
- package/dist/convex/index.js +14 -16
- package/dist/create-BBO24vj6.js +324 -0
- package/dist/{credential-8P81ff4u.js → credential-kue0qpbm.js} +4 -4
- package/dist/deciding-columns-Sq12mEA6.js +123 -0
- package/dist/delegation-input-FTxMK3ji.js +19 -0
- package/dist/denied-text-DfRk15uw.d.ts +19 -0
- package/dist/{describe-DMYiCjJV.d.ts → describe-C3ldA9os.d.ts} +11 -2
- package/dist/{describe-B_gVZEVi.js → describe-Cgkp7eP0.js} +5 -4
- package/dist/{diff-C9RDjjhs.js → diff-Cnghh_Ip.js} +2 -2
- package/dist/{diff-2VaeChzX.js → diff-rpqoe7Ag.js} +9 -9
- package/dist/{doctor-Djh6JDEk.js → doctor-CuVJvhV_.js} +2 -2
- package/dist/doctor-owlv6ddh.js +2166 -0
- package/dist/drizzle/index.d.ts +2 -2
- package/dist/drizzle/index.js +6 -14
- package/dist/elysia/index.d.ts +4 -4
- package/dist/elysia/index.js +7 -7
- package/dist/{errors-DPdqI50v.d.ts → errors-OD_4c__H.d.ts} +2 -2
- package/dist/{errors-Cz5zn0mW.js → errors-drnfxYtm.js} +3 -3
- package/dist/{evaluations-tz6D9A2G.js → evaluations-C9YJsv3H.js} +45 -7
- package/dist/eve/index.d.ts +5 -5
- package/dist/eve/index.js +5 -5
- package/dist/express/index.d.ts +4 -4
- package/dist/express/index.js +8 -7
- package/dist/fastify/index.d.ts +15 -5
- package/dist/fastify/index.js +9 -29
- package/dist/fields-fwfXN01n.js +1016 -0
- package/dist/{files-DWUN9PzI.js → files-D_JIzUrP.js} +12 -3
- package/dist/{fixtures-ht28Cplr.js → fixtures-h2eG2OZV.js} +5 -6
- package/dist/{from-snapshot-CTLijvdm.js → from-snapshot-7REQZe0H.js} +32 -22
- package/dist/{generate-CH7ThJ14.js → generate-CNU1pIOo.js} +1 -3
- package/dist/grantee-CXY-0C87.js +254 -0
- package/dist/grantee-conditions-MQs5NIuX.js +270 -0
- package/dist/grantee-match-fxYp9Nmt.js +141 -0
- package/dist/{helpers-H_KARDVC.js → helpers-C9YmyBIa.js} +7 -6
- package/dist/hono/index.d.ts +4 -4
- package/dist/hono/index.js +8 -8
- package/dist/hosted-CBHgkzDl.js +655 -0
- package/dist/{http-SO7qGttf.js → http-BBoKefeS.js} +35 -2
- package/dist/{http-BIhmLfCc.js → http-BEbdMZ05.js} +2 -24
- package/dist/{rls-import-4lLW_pbb.js → import-BFIlhetD.js} +86 -80
- package/dist/{openapi-import-cmwetbzw.js → import-UHk1N7vH.js} +6 -7
- package/dist/{index-C8z4RjQV.d.ts → index-BwMe94Xq.d.ts} +1 -1
- package/dist/{index-0nNqHspG.d.ts → index-DgM7w96K.d.ts} +2 -2
- package/dist/{index-C7Tw7MaW.d.ts → index-Dz7didpC.d.ts} +3 -3
- package/dist/{index-BNc7Wer_.d.ts → index-UdVQgq48.d.ts} +105 -38
- package/dist/index.d.ts +15 -13
- package/dist/index.js +22 -22
- package/dist/{instance-options-jARdwfPN.d.ts → instance-options-plkrsAog.d.ts} +1 -1
- package/dist/{json-schema-BsAflUGW.js → json-schema-DBkdUuCO.js} +1 -3
- package/dist/jwt/index.d.ts +4 -4
- package/dist/jwt/index.js +13 -13
- package/dist/{kernel-_RRHmnMY.js → kernel-CB3LrIJM.js} +81 -32
- package/dist/kysely/index.d.ts +2 -2
- package/dist/kysely/index.js +6 -14
- package/dist/{live-options-H2eG8dfJ.js → live-options-BaAFCIiu.js} +16 -3
- package/dist/load-B_shHrmd.js +2 -0
- package/dist/{load-DhWEB9P1.js → load-C3_oTHyO.js} +11 -3
- package/dist/{manifest-iaoBXHKc.js → manifest-BngWKryL.js} +5 -18
- package/dist/{map-claims-Z4QZOiFF.js → map-claims-AVlBaJoa.js} +7 -16
- package/dist/{map-error-pgUWNNtG.js → map-error-BE27PYkE.js} +5 -5
- package/dist/{markers-B0hYLvMD.js → markers-CiwAOZtO.js} +6 -2
- package/dist/{may-use-DndQ5tQ-.js → may-use-BOn_dR_w.js} +4 -3
- package/dist/mcp/index.d.ts +26 -9
- package/dist/mcp/index.js +456 -446
- package/dist/{rls-migrate-CN4TlJTO.js → migrate-BwlB5-Wk.js} +75 -15
- package/dist/nest/index.d.ts +4 -4
- package/dist/nest/index.js +11 -12
- package/dist/next/index.d.ts +31 -8
- package/dist/next/index.js +29 -33
- package/dist/next/plugin.d.ts +1 -1
- package/dist/next/plugin.js +1 -1
- package/dist/node/index.d.ts +4 -4
- package/dist/node/index.js +4 -4
- package/dist/ocsf-uYFXGVw2.js +115 -0
- package/dist/openai/index.d.ts +5 -5
- package/dist/openai/index.js +4 -4
- package/dist/openapi/index.d.ts +3 -3
- package/dist/openapi/index.js +1 -1
- package/dist/{openapi-CqqEbHsL.js → openapi-BJdBmYDG.js} +19 -23
- package/dist/{openapi-BqRBvAng.js → openapi-DBzHQbov.js} +2 -5
- package/dist/{openapi-CQTsgKoB.js → openapi-DcQ1Kg40.js} +3 -3
- package/dist/orpc/index.d.ts +5 -5
- package/dist/orpc/index.js +9 -11
- package/dist/otel/index.d.ts +2 -2
- package/dist/otel/index.js +26 -23
- package/dist/output-files-CgIsvdSa.js +140 -0
- package/dist/{rls-ownership-DcYlZ09B.js → ownership-BgnbOJFT.js} +387 -379
- package/dist/{package-root-zGkxO3oN.js → package-root-DtNnpmUd.js} +3 -2
- package/dist/{parse-BuyjxhZV.js → parse-CVFQHk6c.js} +3 -3
- package/dist/{parse-local-manifest-BrMozJCo.js → parse-local-manifest-C-q02Pal.js} +2 -4
- package/dist/{parse-local-manifest-JLnaBH0S.d.ts → parse-local-manifest-DvYUR0QL.d.ts} +1 -1
- package/dist/pdp/index.d.ts +2 -2
- package/dist/pdp/index.js +8 -9
- package/dist/{permdock-CbgvLnuo.js → permdock-L26udHSk.js} +1387 -1220
- package/dist/{rls-fields-CZOMlwHl.js → permission-keys-B26jeyUv.js} +1268 -2053
- package/dist/permission-registry-BsblpB-z.js +385 -0
- package/dist/{permissions-DttnqqxC.d.ts → permission-types-Dg39z9Kv.d.ts} +59 -31
- package/dist/{permissions-RJrovw98.js → permissions-CCJ6lmxB.js} +3 -3
- package/dist/{permissions-CDgvKG5L.d.ts → permissions-COH-KRgT.d.ts} +2 -2
- package/dist/{policy-IB6yxpC3.d.ts → policy-types-sp9P1iIV.d.ts} +661 -582
- package/dist/{policy-D2W7uv19.d.ts → policy-ur0BBK_e.d.ts} +2 -2
- package/dist/{powersync-DE19b9dT.js → powersync-BFSbSElQ.js} +2 -2
- package/dist/powersync-DCDgZgfZ.js +2 -0
- package/dist/{powersync-Dv1_aWF7.js → powersync-cZe4kzhL.js} +26 -27
- package/dist/prisma/index.d.ts +1 -1
- package/dist/prisma/index.js +6 -12
- package/dist/{problem-B43ztKIm.js → problem-CnWraVhC.js} +8 -7
- package/dist/{problem-details-_AMOYz-w.js → problem-details-DECUnXAn.js} +2 -2
- package/dist/{problem-details-DX075l9V.d.ts → problem-details-rIIo8B2C.d.ts} +1 -1
- package/dist/{protected-BdnIEFkY.d.ts → protected-B3_P7xuw.d.ts} +9 -6
- package/dist/{protected-utU-SLuy.js → protected-BN7BUdY1.js} +20 -8
- package/dist/{provider-CSQd7m92.js → provider-Bi-0HNL3.js} +23 -10
- package/dist/{provider-DZY281jO.d.ts → provider-kwHC9CZD.d.ts} +1 -1
- package/dist/{provider-store-Cfbvo4DE.js → provider-store-Ctt-81Ni.js} +6 -3
- package/dist/react/index.d.ts +7 -7
- package/dist/react/index.js +5 -5
- package/dist/react/provider-client.d.ts +1 -1
- package/dist/react/provider-client.js +1 -1
- package/dist/react-native/index.d.ts +20 -11
- package/dist/react-native/index.js +33 -17
- package/dist/{refs-KYb4ntaz.js → refs-f3imMjNW.js} +1 -1
- package/dist/relation-walk-BXnlLtg2.js +186 -0
- package/dist/relations-CvmSpG67.js +163 -0
- package/dist/{replay-CYyiLiwO.d.ts → replay-COpmZUfx.d.ts} +1 -1
- package/dist/request-context-BVM7aWNs.js +71 -0
- package/dist/request-context-C2_1lchh.d.ts +5 -0
- package/dist/resource-relations-D1Un3pcz.js +243 -0
- package/dist/retire-BFcxL6un.js +240 -0
- package/dist/{rls-P-BeBx0n.js → rls-55DQTuf5.js} +14 -2
- package/dist/{rls-DiinlRML.js → rls-BEktUaqg.js} +884 -484
- package/dist/{policy-xyWg7QdR.js → role-builder-B4GbKM-i.js} +235 -541
- package/dist/{rpc-DL9Sqvb1.js → rpc-nYC3cR_u.js} +15 -5
- package/dist/{run-BWc9qqvE.js → run-DMcNW_8T.js} +3 -3
- package/dist/{runtime-CB8gQFvS.js → runtime-D_tMXnsq.js} +1 -1
- package/dist/{saas-DhpJjnBA.js → saas-DtaA4JuW.js} +9 -8
- package/dist/{openapi-schema--ARjyfQr.js → schema-Bol6gqXA.js} +3 -5
- package/dist/{schema-D3MnDMxa.js → schema-DjQBcAP9.js} +4 -2
- package/dist/scim/index.d.ts +1 -1
- package/dist/scim/index.js +1 -1
- package/dist/{scim-BFGBBAqR.js → scim-vFR1O5oG.js} +6 -12
- package/dist/{scopes-CVdPrWN-.js → scopes-tvpa34ep.js} +10 -2
- package/dist/server/index.d.ts +18 -8
- package/dist/server/index.js +8 -8
- package/dist/{server-DlZ28Oz5.js → server-ZYEHtNHS.js} +4 -4
- package/dist/{create-B1d519XK.d.ts → server-types-DdqBwZN_.d.ts} +69 -33
- package/dist/shared-1uhI-e-b.js +294 -0
- package/dist/{shared-DRu8VFVM.js → shared-JhMeOsDF.js} +1 -1
- package/dist/{sink-Bd9PSyfq.js → sink-BTtmfPnE.js} +1 -1
- package/dist/{sink-B1fZzIMf.d.ts → sink-CPN4zLfs.d.ts} +1 -1
- package/dist/{skills-Biq5uup3.js → skills-BZdRQkxs.js} +4 -3
- package/dist/{skills-C5CbhhpG.js → skills-Dle4ZoLF.js} +2 -2
- package/dist/{snapshot-Enxd0n-s.js → snapshot-Dc9CHPME.js} +116 -13
- package/dist/{snapshot-cache-D6jLelln.js → snapshot-cache-Cod40D-X.js} +1 -1
- package/dist/{snapshot-cache-B-LxUahj.d.ts → snapshot-cache-J5T7t_vD.d.ts} +1 -1
- package/dist/solid/index.d.ts +22 -22
- package/dist/solid/index.js +40 -10
- package/dist/{sources-4p0-4xQm.js → sources-B1-b-R0t.js} +25 -26
- package/dist/{sources-BdOYLw9j.d.ts → sources-Dljy06zL.d.ts} +6 -2
- package/dist/{sql-xdg5f4Fi.js → sql-t8teWOfo.js} +19 -3
- package/dist/{src-BDwEZWrZ.js → src-Bb7no-hS.js} +194 -123
- package/dist/ssf/index.d.ts +3 -3
- package/dist/ssf/index.js +58 -61
- package/dist/store-9KO4BlkB.d.ts +147 -0
- package/dist/{store-DMAWwAJV.js → store-ILkLUj_o.js} +4 -4
- package/dist/{store-options-ByhZq1jV.js → store-options-CbUHDoSe.js} +16 -7
- package/dist/{stores-BT2pSCub.js → stores-BHXdSbw9.js} +7 -3
- package/dist/stores-Cxz_80g-.d.ts +31 -0
- package/dist/{stream-DDpvynSB.js → stream-BMui98KK.js} +2 -1
- package/dist/{stream-D1RXubw2.d.ts → stream-Dwsg6WwE.d.ts} +2 -2
- package/dist/{subject-settings-BwBmCdTR.js → subject-settings-CkEwrw3W.js} +23 -5
- package/dist/supabase/index.d.ts +4 -4
- package/dist/supabase/index.js +3 -3
- package/dist/supabase/middleware.d.ts +11 -5
- package/dist/supabase/middleware.js +10 -7
- package/dist/{supabase-C0qESOa-.js → supabase-C1TWlnYt.js} +36 -52
- package/dist/{supabase-s4o9s1O1.js → supabase-Dbqr141s.js} +2 -2
- package/dist/supabase-hook-BULg7Ut_.js +2 -0
- package/dist/{supabase-hook-ntETYCQu.js → supabase-hook-Cva3ikXz.js} +730 -781
- package/dist/{supautils-u8jXTGrt.js → supautils-B6HYL9eL.js} +2 -2
- package/dist/svelte/Protected.svelte +9 -2
- package/dist/svelte/index.d.ts +6 -6
- package/dist/svelte/index.js +25 -11
- package/dist/svelte/runtime.d.ts +10 -3
- package/dist/svelte/runtime.js +4 -3
- package/dist/svelte/source.d.ts +6 -6
- package/dist/svelte/source.js +4 -4
- package/dist/{validity-uOp8UpYn.js → tenancy-DshjqY0n.js} +3 -39
- package/dist/terminal/index.d.ts +5 -5
- package/dist/terminal/index.js +18 -34
- package/dist/testing/index.d.ts +89 -77
- package/dist/testing/index.js +1276 -1262
- package/dist/testing/saas/index.d.ts +4 -4
- 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/thenable-QSmvt-vZ.js +65 -0
- package/dist/{token-CIWxJOCh.js → token-Bx9qy30p.js} +1 -1
- package/dist/tool-refusal-CQ7OEia9.js +41 -0
- package/dist/trpc/index.d.ts +5 -5
- package/dist/trpc/index.js +7 -8
- package/dist/{types-DbjRN55U.d.ts → types-7rJVLWe5.d.ts} +82 -4
- package/dist/{types-aqOHTHcp.d.ts → types-Br9IXUBG.d.ts} +1 -1
- package/dist/types-ByUo56un.d.ts +88 -0
- package/dist/{types-Cq_H8N_X.d.ts → types-C0dee0Qf.d.ts} +2 -2
- package/dist/{types-DxiCGmmd.d.ts → types-CIlTojhq.d.ts} +2 -2
- package/dist/{types-QCuYJh4q.d.ts → types-CUcFZfpU.d.ts} +1 -1
- package/dist/{types-C97IskBw.d.ts → types-CqrjB9o6.d.ts} +1 -1
- package/dist/{types-BnIEz1sU.d.ts → types-DHrmBJLH.d.ts} +24 -6
- package/dist/{types-Dnemwd77.d.ts → types-DpWfQdSh.d.ts} +5 -1
- package/dist/{types-Bvc4oWT6.d.ts → types-DvupZE0w.d.ts} +1 -1
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/{usage-B0YuTQRO.js → usage-4G6t643F.js} +2 -2
- package/dist/{usage-ItS7nLcl.js → usage-CG9nOYZw.js} +7 -7
- package/dist/usage-iFLJ0-rr.js +2 -0
- package/dist/{validation-error-Ba264OaJ.d.ts → validation-error-BSSw0l_8.d.ts} +1 -1
- package/dist/{validation-error-CnjZ4t92.js → validation-error-ZfUHbKvf.js} +1 -1
- package/dist/validity-dVrUEPJt.js +62 -0
- package/dist/{verifier-BxBo6I44.js → verifier-DMr_Zmb_.js} +51 -15
- package/dist/{rls-verify-CMv9H3wV.js → verify-BztUB3GX.js} +668 -239
- package/dist/{version-BvacVTZR.js → version-BEogC-rB.js} +1 -1
- package/dist/{views-Soe7bGSC.js → views-CJsrBS1f.js} +23 -1
- package/dist/{vocabulary-DA8a8zif.d.ts → vocabulary-hhEFLFoL.d.ts} +15 -6
- package/dist/vue/index.d.ts +24 -19
- package/dist/vue/index.js +24 -8
- package/dist/watch-C8TpsAl4.js +2 -0
- package/dist/{watch-DVBJ9cBq.js → watch-zeucB7eF.js} +6 -6
- package/dist/webmcp/index.d.ts +2 -2
- package/dist/webmcp/index.js +13 -37
- package/dist/wrap-Cr93qqdP.d.ts +14 -0
- package/dist/wrap-DWsU_5Bd.js +26 -0
- package/package.json +8 -8
- package/schemas/catalog-v1.json +6 -0
- package/skills/permdock-credentials/SKILL.md +1 -1
- package/skills/permdock-credentials/references/tokens-keys-links.md +7 -2
- package/skills/permdock-data/references/queries-and-rls.md +4 -0
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +1 -0
- package/skills/permdock-wire/SKILL.md +2 -2
- package/skills/permdock-wire/references/adapters.md +9 -1
- package/skills/permdock-wire/references/policy-options.md +10 -1
- package/dist/catalog-CRW5Mx_B.js +0 -2
- package/dist/collect-DSWOk19R.js +0 -2
- package/dist/create-CMwIl-w2.js +0 -253
- package/dist/delegation-D8pUEuce.js +0 -103
- package/dist/doctor-DvH0IQff.js +0 -1194
- package/dist/doctor-collect-CjdO0wQX.js +0 -665
- package/dist/evaluation-items-CfCCejQd.js +0 -40
- package/dist/fields-BMl1RxoX.js +0 -26
- package/dist/grantee-Cv5Sogfv.js +0 -602
- package/dist/hosted-DJQw46hn.js +0 -266
- package/dist/instance-options-Dhhcnm3y.js +0 -22
- package/dist/load-DwJUafYN.js +0 -2
- package/dist/map-AcxK9o_D.js +0 -147
- package/dist/permissions-DVoF6dMt.js +0 -662
- package/dist/powersync-CrBX8hY6.js +0 -2
- package/dist/protected-xAr24FEC.d.ts +0 -40
- package/dist/relations-cjOL01hZ.js +0 -505
- package/dist/rls-graph-CHwx0V8q.js +0 -435
- package/dist/rls-shared-Cewd_-zI.js +0 -495
- package/dist/store-jOBYpiXC.d.ts +0 -36
- package/dist/stores-C0IMQrGI.d.ts +0 -52
- package/dist/supabase-config-Bj3iDH2I.js +0 -472
- package/dist/supabase-hook-gd3s8bxB.js +0 -2
- package/dist/thenable-okc2xDsj.js +0 -10
- package/dist/types-CKNSyIf_.d.ts +0 -52
- package/dist/usage-DorAJiU_.js +0 -2
- package/dist/watch-BwxLTYMZ.js +0 -2
|
@@ -1,27 +1,28 @@
|
|
|
1
|
-
import { t as compact } from "./compact-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { i as describeError, o as usageResult } from "./errors-CWfoNg9N.js";
|
|
1
|
+
import { t as compact } from "./compact-CAf557ZB.js";
|
|
2
|
+
import { d as scopeList, o as resolveScope } from "./scopes-tvpa34ep.js";
|
|
3
|
+
import { c as listPermissions, u as renamedKeys } from "./permission-registry-BsblpB-z.js";
|
|
4
|
+
import { o as flattenGrantee, v as listRoles } from "./grantee-CXY-0C87.js";
|
|
5
|
+
import { p as scopeField } from "./tenancy-DshjqY0n.js";
|
|
6
|
+
import { a as exitCodeOf, i as describeError, n as cliErrorKind, o as usageResult } from "./errors-CWfoNg9N.js";
|
|
7
7
|
import { t as policyRowConditionKeys } from "./row-conditions-h2HythXQ.js";
|
|
8
|
-
import {
|
|
8
|
+
import { c as requirePolicy } from "./load-C3_oTHyO.js";
|
|
9
9
|
import "./budget-NbEtbJC_.js";
|
|
10
|
-
import { n as escapeSqlIdent } from "./sql-
|
|
11
|
-
import "./sources-
|
|
12
|
-
import { i as
|
|
10
|
+
import { l as sqlTextArray, n as escapeSqlIdent, r as qualifySqlTable } from "./sql-t8teWOfo.js";
|
|
11
|
+
import { l as roleColumn } from "./sources-B1-b-R0t.js";
|
|
12
|
+
import { i as INDEXES_MARKER, n as GRANTS_MARKER, o as SEEDS_MARKER, t as BACKFILL_MARKER } from "./markers-CiwAOZtO.js";
|
|
13
13
|
import { n as requirePeer } from "./peer-BOr_kJE1.js";
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { a as
|
|
19
|
-
import {
|
|
20
|
-
import { n as
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
14
|
+
import { c as rbacScaffold, l as resolveAuthorize, o as supabaseHookSql, p as supabaseConfig, s as parseRbacAuthorize, t as grantsLabel } from "./supabase-hook-Cva3ikXz.js";
|
|
15
|
+
import { C as parseMembershipsFlag, E as scopeTypeOf, F as quoteQualified, I as quoteTable, M as qualified, N as quoteIdent$1, O as tenantTypeOf, P as quoteLiteral, _ as subjectClaimJsonSql, a as customRolesOn, c as SESSION_LIVE_HELPER, g as sessionLiveHelperSql, r as HELPERS, t as CUSTOM_ROLES, y as subjectIdSql } from "./shared-1uhI-e-b.js";
|
|
16
|
+
import { a as parseSplit, c as seedsMigration, i as isSeedsDirectory, l as writeOutputFiles, n as driftOf, o as partPath, s as pgDeltaPath, t as dataMigrationFile } from "./output-files-CgIsvdSa.js";
|
|
17
|
+
import { a as splitUndiffed } from "./deciding-columns-Sq12mEA6.js";
|
|
18
|
+
import { B as memberIdsHelper, C as apiKeysPlan, D as roleNames, E as collectGrants, G as scopeSources, K as scopeTable, M as graphHelper, N as graphSqlName, T as breakGlassKey, U as permittedIdsHelper, X as connectPgPool, Y as checkSuspension, a as accessSql, o as helpersSql, r as permittedByPermissionHelper, s as seedSql, t as permissionHelpersSql, v as currentKeyExpr, w as breakGlassHolder } from "./permission-keys-B26jeyUv.js";
|
|
19
|
+
import { n as ownershipSql, r as assignmentSql, t as ownershipRules } from "./ownership-BgnbOJFT.js";
|
|
20
|
+
import { _ as indexTargets, a as fieldViewsSql, b as readTableIndexFacts, c as assemblePolicies, d as readOnlyActorPolicies, f as branchClauses, g as COLUMNS_SQL, h as tableFor, i as fieldViews, l as orSql, m as compileGrants, n as columnGrantSql, o as rowBranches, r as columnRevokeSql, u as readOnlyActorKinds, v as indexesSql, y as pruneIndexTargets } from "./fields-fwfXN01n.js";
|
|
21
|
+
import { a as graphSql, c as callsHelper, i as graphPlan, l as helperCallKeys, n as runRlsVerify, o as HELPER_TABLES, r as closureDepths } from "./verify-BztUB3GX.js";
|
|
22
|
+
import { t as supautilsRejections } from "./supautils-B6HYL9eL.js";
|
|
23
|
+
import { basename, delimiter, join } from "node:path";
|
|
23
24
|
import { spawnSync } from "node:child_process";
|
|
24
|
-
//#region src/cli/rls
|
|
25
|
+
//#region src/cli/rls/advisors.ts
|
|
25
26
|
/** The PermDock doctor check that reports the same problem in migrations. */
|
|
26
27
|
const ADVISOR_CODES = {
|
|
27
28
|
function_search_path_mutable: "PD048",
|
|
@@ -127,6 +128,255 @@ function runRlsAdvisors(input) {
|
|
|
127
128
|
};
|
|
128
129
|
}
|
|
129
130
|
//#endregion
|
|
131
|
+
//#region src/cli/rls/generate-context.ts
|
|
132
|
+
/** `rls.customRoleWrites.requires` as permission keys, or why it names none. */
|
|
133
|
+
function writeRequirement(policy, requires) {
|
|
134
|
+
if (requires === void 0) return {};
|
|
135
|
+
const leaves = listPermissions(policy.permissions);
|
|
136
|
+
const keys = requires === "manageRoles" ? leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key) : [...requires];
|
|
137
|
+
if (keys.length === 0) return { error: requires === "manageRoles" ? "rls.customRoleWrites.requires is 'manageRoles', but no permission declares meta.manageRoles" : "rls.customRoleWrites.requires names no permission" };
|
|
138
|
+
const known = new Set(leaves.map((leaf) => leaf.key));
|
|
139
|
+
const unknown = keys.filter((key) => !known.has(key));
|
|
140
|
+
if (unknown.length > 0) return { error: `rls.customRoleWrites.requires names undeclared permissions: ${unknown.join(", ")}` };
|
|
141
|
+
return { keys: [...new Set(keys)].toSorted() };
|
|
142
|
+
}
|
|
143
|
+
/** Declared role names, and those a tenant admin may compose into custom roles. */
|
|
144
|
+
function customRoleNames(policy, requires) {
|
|
145
|
+
const declared = [...roleNames(policy)].toSorted();
|
|
146
|
+
const assignable = declared.filter((name) => policy.rolesByName.get(name)?.assignable ?? listRoles(policy.vocabulary.roles).some((leaf) => leaf.key === name && leaf.assignable));
|
|
147
|
+
const renamed = renamedKeys(policy.vocabulary.permissions);
|
|
148
|
+
const leaves = listPermissions(policy.permissions);
|
|
149
|
+
const manage = leaves.filter((leaf) => leaf.meta.manageRoles === true).map((leaf) => leaf.key);
|
|
150
|
+
return {
|
|
151
|
+
declared,
|
|
152
|
+
assignable,
|
|
153
|
+
...renamed.size === 0 ? {} : { renamed: Object.fromEntries(renamed) },
|
|
154
|
+
permissions: leaves.map((leaf) => leaf.key).toSorted(),
|
|
155
|
+
...manage.length === 0 ? {} : { manage: manage.toSorted() },
|
|
156
|
+
...requires === void 0 ? {} : { requires },
|
|
157
|
+
...policy.levels === void 0 ? {} : { levels: true }
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function skippedOwnershipTriggers(triggers, scopes) {
|
|
161
|
+
if (triggers === false) return "all";
|
|
162
|
+
return Object.keys(triggers).map((key) => {
|
|
163
|
+
const name = resolveScope(scopes, key);
|
|
164
|
+
if (name === void 0) throw new Error(`PermDock CLI: rls.ownershipTriggers.${key} names a scope the policy does not declare`);
|
|
165
|
+
return name;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/** The SQL context every emitter reads, from the flags, `rls` config and the policy. */
|
|
169
|
+
function buildContext(input, policy, options) {
|
|
170
|
+
const rls = input.config.rls;
|
|
171
|
+
const memberships = parseMembershipsFlag(input.memberships) ?? rls?.memberships;
|
|
172
|
+
if (input.rbac && input.dialect !== "supabase") return { refusal: `rls generate --rbac supabase needs --dialect supabase (got ${input.dialect})` };
|
|
173
|
+
const schema = input.rbacSchema ?? rls?.schema ?? rls?.rbac?.schema ?? "permdock";
|
|
174
|
+
const authorize = resolveAuthorize(input.config, {
|
|
175
|
+
authorize: input.authorize,
|
|
176
|
+
rbac: input.rbac,
|
|
177
|
+
memberships
|
|
178
|
+
});
|
|
179
|
+
const memberSources = rls?.membershipSources ?? input.config.supabase?.hook?.memberships;
|
|
180
|
+
const sources = authorize === "database" ? memberSources : void 0;
|
|
181
|
+
const scopes = scopeList(policy.scopes);
|
|
182
|
+
const suspension = checkSuspension(rls?.suspension, scopes);
|
|
183
|
+
const hookRoles = input.config.supabase?.hook?.roles;
|
|
184
|
+
const roles = rls?.roles ?? (hookRoles === false ? void 0 : hookRoles);
|
|
185
|
+
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" };
|
|
186
|
+
const requires = writeRequirement(policy, rls?.customRoleWrites?.requires);
|
|
187
|
+
if (requires.error !== void 0) return { refusal: requires.error };
|
|
188
|
+
const readers = rls?.trustedReaders;
|
|
189
|
+
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']" };
|
|
190
|
+
const ownership = ownershipRules(policy, scopes);
|
|
191
|
+
const graph = graphPlan(policy);
|
|
192
|
+
const apiKeys = apiKeysPlan(rls?.apiKeys, new Set(roleNames(policy)), Object.fromEntries(renamedKeys(policy.vocabulary.permissions)));
|
|
193
|
+
return {
|
|
194
|
+
ctx: {
|
|
195
|
+
dialect: input.dialect,
|
|
196
|
+
scopes,
|
|
197
|
+
tenantClaim: rls?.tenantClaim ?? "tenant_id",
|
|
198
|
+
...rls?.tenants === "all" ? { tenants: "all" } : {},
|
|
199
|
+
gucPrefix: input.gucPrefix ?? rls?.gucPrefix ?? "app",
|
|
200
|
+
...rls?.actions === void 0 ? {} : { actions: rls.actions },
|
|
201
|
+
inlineFunctions: input.inlineFunctions || rls?.inlineFunctions === true,
|
|
202
|
+
schema,
|
|
203
|
+
authorize,
|
|
204
|
+
roleClaim: rls?.roleClaim ?? "user_role",
|
|
205
|
+
tenantType: input.tenantType ?? rls?.tenantType ?? "uuid",
|
|
206
|
+
...rls?.teamType === void 0 ? {} : { teamType: rls.teamType },
|
|
207
|
+
...rls?.scopeTypes === void 0 ? {} : { scopeTypes: rls.scopeTypes },
|
|
208
|
+
...memberships === void 0 ? {} : { memberships },
|
|
209
|
+
...sources === void 0 || sources.length === 0 ? {} : { sources },
|
|
210
|
+
...memberSources === void 0 || memberSources.length === 0 ? {} : { memberSources },
|
|
211
|
+
...suspension === void 0 ? {} : { suspension },
|
|
212
|
+
...roles === void 0 ? {} : { roles },
|
|
213
|
+
...input.customRoles === true || customRolesOn(rls) ? { customRoles: {
|
|
214
|
+
...customRoleNames(policy, requires.keys),
|
|
215
|
+
...rls?.customRoleWrites?.roles === void 0 ? {} : { table: rls.customRoleWrites.roles }
|
|
216
|
+
} } : {},
|
|
217
|
+
...input.capabilities === true || rls?.capabilities === true ? { capabilities: true } : {},
|
|
218
|
+
...apiKeys === void 0 ? {} : { apiKeys },
|
|
219
|
+
...rls?.anonymousSignIns === "deny" ? { anonymousSignIns: "deny" } : {},
|
|
220
|
+
...ownership === void 0 ? {} : { ownership },
|
|
221
|
+
...rls?.ownershipTriggers === void 0 ? {} : { skipOwnershipTriggers: skippedOwnershipTriggers(rls.ownershipTriggers, scopes) },
|
|
222
|
+
...rls?.assignments === void 0 ? {} : { assignments: rls.assignments === true ? { tables: [] } : {
|
|
223
|
+
tables: rls.assignments.tables ?? [],
|
|
224
|
+
...rls.assignments.ownRole === void 0 ? {} : { ownRole: rls.assignments.ownRole }
|
|
225
|
+
} },
|
|
226
|
+
...options.fieldsMode === void 0 ? {} : { fields: options.fieldsMode },
|
|
227
|
+
...graph.size === 0 ? {} : { graph: {
|
|
228
|
+
closures: closureDepths(graph),
|
|
229
|
+
resources: policy.resources,
|
|
230
|
+
...rls?.tables === void 0 ? {} : { tables: rls.tables }
|
|
231
|
+
} }
|
|
232
|
+
},
|
|
233
|
+
graph,
|
|
234
|
+
ownership,
|
|
235
|
+
schema,
|
|
236
|
+
authorize
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
//#endregion
|
|
240
|
+
//#region src/cli/rls/generate-options.ts
|
|
241
|
+
function isRefusal(value) {
|
|
242
|
+
return "refusal" in value;
|
|
243
|
+
}
|
|
244
|
+
/** The checks that need no policy: `--fields` and `--revoke-columns`. */
|
|
245
|
+
function generateOptions(input) {
|
|
246
|
+
const rls = input.config.rls;
|
|
247
|
+
const fieldsMode = input.fields ?? rls?.fields;
|
|
248
|
+
if (fieldsMode !== void 0 && fieldsMode !== "views") return { refusal: `rls generate --fields must be views (got ${fieldsMode})` };
|
|
249
|
+
const revokeColumns = input.revokeColumns === true || rls?.revokeColumns === true;
|
|
250
|
+
if (revokeColumns && fieldsMode === void 0) return { refusal: "rls generate --revoke-columns needs --fields views" };
|
|
251
|
+
return {
|
|
252
|
+
fieldsMode,
|
|
253
|
+
revokeColumns
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** Whether `--split` names `part`, which then leaves the helpers or the warnings. */
|
|
257
|
+
function splitsPart(raw, part) {
|
|
258
|
+
const split = parseSplit(raw);
|
|
259
|
+
return typeof split !== "string" && split?.includes(part) === true;
|
|
260
|
+
}
|
|
261
|
+
//#endregion
|
|
262
|
+
//#region src/cli/rls/generate-output.ts
|
|
263
|
+
function moveUndiffed(sql, label, part) {
|
|
264
|
+
const { kept, moved } = splitUndiffed(sql);
|
|
265
|
+
const [first = "", ...rest] = kept.split("\n");
|
|
266
|
+
return {
|
|
267
|
+
sql: [
|
|
268
|
+
first,
|
|
269
|
+
`-- what supabase db diff drops from this part is in ${label}`,
|
|
270
|
+
...rest
|
|
271
|
+
].join("\n"),
|
|
272
|
+
grants: `-- the privileges and view options supabase db diff drops from the ${part} part\n${moved.join("\n")}`
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
/** The files `generate` writes: one, or one per `--split` part, plus the hook's grants. */
|
|
276
|
+
function outputFiles(plan) {
|
|
277
|
+
const { input } = plan;
|
|
278
|
+
const split = parseSplit(input.split);
|
|
279
|
+
if (typeof split === "string") return split;
|
|
280
|
+
if (split?.includes("policies") === true && plan.helpersOnly) return "rls generate --helpers-only writes no policies part; drop it from --split";
|
|
281
|
+
if (split === void 0) {
|
|
282
|
+
if (input.grantsOut !== void 0) return "rls generate --grants-out needs --split with the helpers or hook part";
|
|
283
|
+
if (input.seedsOut !== void 0) return "rls generate --seeds-out needs --split with the seeds part";
|
|
284
|
+
return [{
|
|
285
|
+
part: "rls",
|
|
286
|
+
rel: plan.outRel,
|
|
287
|
+
text: plan.text
|
|
288
|
+
}];
|
|
289
|
+
}
|
|
290
|
+
if (input.target !== "sql") return "rls generate --split needs --target sql";
|
|
291
|
+
const pgDelta = input.out === void 0 && input.config.rls?.out === void 0 ? supabaseConfig(input.cwd).pgDelta : void 0;
|
|
292
|
+
if (pgDelta === void 0 && !plan.outRel.includes("{part}")) return `rls generate --split needs {part} in --out, for example supabase/schemas/identity/056_permdock_{part}.sql (got ${plan.outRel})`;
|
|
293
|
+
if (pgDelta !== void 0 && split.includes("helpers") && !split.includes("seeds")) return "rls generate --split helpers under pg-delta needs the seeds part with --seeds-out: pg-delta rejects the role_permissions rows in a declarative file";
|
|
294
|
+
if (pgDelta !== void 0 && split.includes("seeds") && input.seedsOut === void 0) return "rls generate --split seeds under pg-delta needs --seeds-out: pg-delta does not diff the role_permissions rows, so they go in a migration";
|
|
295
|
+
const at = (part, schema = plan.schema) => pgDelta === void 0 ? partPath(plan.outRel, part) : pgDeltaPath(pgDelta.schemaDir, part, schema);
|
|
296
|
+
if (input.grantsOut !== void 0 && !split.includes("hook") && !split.includes("helpers")) return "rls generate --grants-out needs the helpers or hook part in --split";
|
|
297
|
+
if (input.seedsOut !== void 0 && !split.includes("seeds")) return "rls generate --seeds-out needs the seeds part in --split";
|
|
298
|
+
if (split.includes("hook") && input.config.supabase?.hook === void 0) return "rls generate --split hook needs supabase.hook in permdock.config.ts";
|
|
299
|
+
const sql = plan.sql();
|
|
300
|
+
const relocated = pgDelta === void 0 ? void 0 : sql.servicePolicies;
|
|
301
|
+
if (relocated !== void 0 && split.includes("policies") && !split.includes("seeds")) return "rls generate --split policies under pg-delta with rls.realtime or rls.storage needs the seeds part with --seeds-out: the shadow database has no realtime.messages or storage.objects, so their policies go in that migration";
|
|
302
|
+
const seeds = relocated === void 0 || !split.includes("policies") ? sql.seeds : `${sql.seeds}\n${relocated}`;
|
|
303
|
+
const files = [];
|
|
304
|
+
const label = grantsLabel(input.grantsOut);
|
|
305
|
+
let grantsMarker = `${GRANTS_MARKER} schema=${plan.schema}`;
|
|
306
|
+
const grants = [];
|
|
307
|
+
for (const part of split) switch (part) {
|
|
308
|
+
case "helpers": {
|
|
309
|
+
if (label === void 0) {
|
|
310
|
+
files.push({
|
|
311
|
+
part,
|
|
312
|
+
rel: at(part),
|
|
313
|
+
text: sql.helpers
|
|
314
|
+
});
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
const moved = moveUndiffed(sql.helpers, label, part);
|
|
318
|
+
files.push({
|
|
319
|
+
part,
|
|
320
|
+
rel: at(part),
|
|
321
|
+
text: moved.sql
|
|
322
|
+
});
|
|
323
|
+
grants.push(moved.grants);
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
case "seeds": {
|
|
327
|
+
const out = input.seedsOut;
|
|
328
|
+
if (out !== void 0 && isSeedsDirectory(input.cwd, out)) {
|
|
329
|
+
const planned = seedsMigration(input.cwd, out, SEEDS_MARKER, seeds, input.io.now?.() ?? /* @__PURE__ */ new Date());
|
|
330
|
+
if (!planned.current || input.check) files.push({
|
|
331
|
+
part,
|
|
332
|
+
rel: planned.rel,
|
|
333
|
+
text: seeds
|
|
334
|
+
});
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
files.push({
|
|
338
|
+
part,
|
|
339
|
+
rel: out ?? at(part),
|
|
340
|
+
text: seeds
|
|
341
|
+
});
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
case "indexes":
|
|
345
|
+
files.push({
|
|
346
|
+
part,
|
|
347
|
+
rel: at(part),
|
|
348
|
+
text: sql.indexes
|
|
349
|
+
});
|
|
350
|
+
break;
|
|
351
|
+
case "policies":
|
|
352
|
+
files.push({
|
|
353
|
+
part,
|
|
354
|
+
rel: at(part),
|
|
355
|
+
text: pgDelta === void 0 ? sql.policies : sql.declarativePolicies
|
|
356
|
+
});
|
|
357
|
+
break;
|
|
358
|
+
case "hook": {
|
|
359
|
+
const hook = supabaseHookSql(plan.scopes, input.config, {}, label);
|
|
360
|
+
files.push({
|
|
361
|
+
part,
|
|
362
|
+
rel: at(part, hook.manifest.hook.schema),
|
|
363
|
+
text: hook.sql
|
|
364
|
+
});
|
|
365
|
+
const [marker = grantsMarker, ...rest] = hook.grants.split("\n");
|
|
366
|
+
grantsMarker = marker;
|
|
367
|
+
grants.push(rest.join("\n").trimEnd());
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
default: return part;
|
|
371
|
+
}
|
|
372
|
+
if (input.grantsOut !== void 0) files.push({
|
|
373
|
+
part: "grants",
|
|
374
|
+
rel: input.grantsOut,
|
|
375
|
+
text: `${[grantsMarker, ...grants].join("\n")}\n`
|
|
376
|
+
});
|
|
377
|
+
return files;
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
130
380
|
//#region src/cli/approval-schema.ts
|
|
131
381
|
/** An RFC 3339 instant as `Temporal.Instant` prints it; pg_jsonschema asserts no `format`. */
|
|
132
382
|
const INSTANT = String.raw`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$`;
|
|
@@ -249,7 +499,7 @@ const APPROVAL_REQUEST_SCHEMA = {
|
|
|
249
499
|
}
|
|
250
500
|
};
|
|
251
501
|
//#endregion
|
|
252
|
-
//#region src/cli/rls
|
|
502
|
+
//#region src/cli/rls/approvals.ts
|
|
253
503
|
/** The objects `rls.approvals` adds. Names are part of the SQL contract `supabaseApprovalStore` calls. */
|
|
254
504
|
const APPROVAL_STORE = {
|
|
255
505
|
table: "approval_requests",
|
|
@@ -315,8 +565,8 @@ $permdock$;\n`;
|
|
|
315
565
|
* resolved by the server, which grants its own role.
|
|
316
566
|
*/
|
|
317
567
|
function approvalStoreSql(ctx, jsonSchema = false) {
|
|
318
|
-
const table = qualified
|
|
319
|
-
const fn = (name) => qualified
|
|
568
|
+
const table = qualified(ctx, APPROVAL_STORE.table);
|
|
569
|
+
const fn = (name) => qualified(ctx, name);
|
|
320
570
|
const index = (name) => `"${APPROVAL_STORE.table}_${name}"`;
|
|
321
571
|
return `-- approval store: ApprovalRequest bodies, opened, resolved and consumed through the functions below; supabaseApprovalStore calls them
|
|
322
572
|
create table if not exists ${table} (
|
|
@@ -514,7 +764,7 @@ function mirrorSet(table, mirror, body) {
|
|
|
514
764
|
* grants and policies stay the app's.
|
|
515
765
|
*/
|
|
516
766
|
function adoptedApprovalStoreSql(ctx, adopt, jsonSchema = false) {
|
|
517
|
-
const table =
|
|
767
|
+
const table = quoteQualified(adopt.table);
|
|
518
768
|
const base = adopt.table.split(".").at(-1) ?? adopt.table;
|
|
519
769
|
const token = quoteIdent$1(adopt.token ?? "token");
|
|
520
770
|
const body = quoteIdent$1(adopt.body ?? "body");
|
|
@@ -524,7 +774,7 @@ function adoptedApprovalStoreSql(ctx, adopt, jsonSchema = false) {
|
|
|
524
774
|
if (open !== "insert" && open !== "attach") throw new Error(`PermDock CLI: rls.approvals.open must be 'insert' or 'attach', got '${String(open)}'`);
|
|
525
775
|
const schema = adopt.schema;
|
|
526
776
|
if (schema !== void 0 && (typeof schema !== "string" || schema === "")) throw new Error("PermDock CLI: rls.approvals.schema must be a schema name");
|
|
527
|
-
const fn = (name) => schema === void 0 ? qualified
|
|
777
|
+
const fn = (name) => schema === void 0 ? qualified(ctx, name) : `${quoteIdent$1(schema)}.${name}`;
|
|
528
778
|
const index = (name) => quoteIdent$1(`${base}_permdock_${name}`);
|
|
529
779
|
const status = `a.${body} ->> 'status'`;
|
|
530
780
|
const expires = `(a.${body} ->> 'expiresAt')::timestamptz`;
|
|
@@ -695,10 +945,200 @@ ${serverOnly(fn(APPROVAL_STORE.cancel), "jsonb, text, text, text")}
|
|
|
695
945
|
`;
|
|
696
946
|
}
|
|
697
947
|
//#endregion
|
|
698
|
-
//#region src/cli/rls
|
|
948
|
+
//#region src/cli/rls/backfill.ts
|
|
949
|
+
const PREFIX$1 = "PermDock CLI";
|
|
950
|
+
/** A column of the roles-table row `r`. */
|
|
951
|
+
function col(name) {
|
|
952
|
+
return `r.${quoteIdent$1(name)}`;
|
|
953
|
+
}
|
|
954
|
+
function schemaOf(table) {
|
|
955
|
+
return table.includes(".") ? table.split(".")[0] ?? "public" : "public";
|
|
956
|
+
}
|
|
957
|
+
/** The roles-table column a reference joins on, after checking it points at `roles`. */
|
|
958
|
+
function roleRef(role, table, roles, label) {
|
|
959
|
+
const pairs = Object.entries(role.on);
|
|
960
|
+
const [pair] = pairs;
|
|
961
|
+
if (pair === void 0 || pairs.length !== 1) throw new Error(`${PREFIX$1}: ${label}.on must map exactly one column of ${table} to ${role.through}, for example { role_id: 'id' }`);
|
|
962
|
+
const through = role.through.includes(".") ? role.through : `${schemaOf(table)}.${role.through}`;
|
|
963
|
+
if (qualifySqlTable(through) !== qualifySqlTable(roles.table)) throw new Error(`${PREFIX$1}: ${label}.through must be rls.customRoleWrites.roles.table (${roles.table}), got ${role.through}`);
|
|
964
|
+
if (role.column !== roles.key) throw new Error(`${PREFIX$1}: ${label}.column must be rls.customRoleWrites.roles.key (${roles.key}), got ${role.column}`);
|
|
965
|
+
const [ref, id] = pair;
|
|
966
|
+
return {
|
|
967
|
+
ref,
|
|
968
|
+
id
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* A legacy key as SQL the way `mapKey` maps it: `rls.migrate.keys`, then
|
|
973
|
+
* `renamed`, then the longest `rls.migrate.prefixes` match, and a former key
|
|
974
|
+
* read as its current key.
|
|
975
|
+
*/
|
|
976
|
+
function legacyKeySql(expr, migrate, renamed) {
|
|
977
|
+
const exact = new Map(Object.entries(renamed));
|
|
978
|
+
for (const [from, to] of Object.entries(migrate?.keys ?? {})) exact.set(from, to);
|
|
979
|
+
const prefixes = Object.entries(migrate?.prefixes ?? {}).toSorted(([a], [b]) => b.length - a.length || (a < b ? -1 : a > b ? 1 : 0));
|
|
980
|
+
const prefixed = prefixes.length === 0 ? expr : `(case ${prefixes.map(([from, to]) => `when left(${expr}, ${String(from.length)}) = ${quoteLiteral(from)} then ${quoteLiteral(to)} || substr(${expr}, ${String(from.length + 1)})`).join(" ")} else ${expr} end)`;
|
|
981
|
+
const mapped = exact.size === 0 ? prefixed : `(case ${expr} ${[...exact.entries()].toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([from, to]) => `when ${quoteLiteral(from)} then ${quoteLiteral(to)}`).join(" ")} else ${prefixed} end)`;
|
|
982
|
+
const renames = Object.entries(renamed).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
983
|
+
return currentKeyExpr(renames, mapped);
|
|
984
|
+
}
|
|
985
|
+
function sourceSql(source, table, label) {
|
|
986
|
+
return roleColumn(source, table, "x", {
|
|
987
|
+
label,
|
|
988
|
+
prefix: PREFIX$1
|
|
989
|
+
}).lookup;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* The `--backfill-out` migration: every custom role in the application's
|
|
993
|
+
* roles table that has no rows in `custom_role_permissions` or
|
|
994
|
+
* `custom_role_includes` yet is saved through
|
|
995
|
+
* `permdock_trusted_replace_custom_role_grants`, with the entries PermDock
|
|
996
|
+
* cannot hold left out and listed in one warning. Running it again copies
|
|
997
|
+
* nothing, so a role edited after the copy keeps its edits.
|
|
998
|
+
*/
|
|
999
|
+
function backfillSql(ctx, from, migrate) {
|
|
1000
|
+
const custom = ctx.customRoles;
|
|
1001
|
+
const root = ctx.scopes[0]?.name;
|
|
1002
|
+
if (custom === void 0 || root === void 0) throw new Error(`${PREFIX$1}: rls.customRoles.from needs custom roles on`);
|
|
1003
|
+
if (ctx.authorize !== "database") throw new Error(`${PREFIX$1}: rls.customRoles.from needs database mode; in jwt mode custom roles ride the token, so build the grants claim with customRoleClaim`);
|
|
1004
|
+
const roles = custom.table;
|
|
1005
|
+
if (roles === void 0) throw new Error(`${PREFIX$1}: rls.customRoles.from needs rls.customRoleWrites.roles, the roles table each row belongs to`);
|
|
1006
|
+
const leveled = custom.levels === true;
|
|
1007
|
+
if (from.permissions.level !== void 0 && !leveled) throw new Error(`${PREFIX$1}: rls.customRoles.from.permissions.level is set, but no resource declares levels`);
|
|
1008
|
+
const tenantType = tenantTypeOf(ctx);
|
|
1009
|
+
const rolesTable = quoteQualified(roles.table);
|
|
1010
|
+
const tenant = roles.tenant === void 0 ? `null::${tenantType}` : `${col(roles.tenant)}::${tenantType}`;
|
|
1011
|
+
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`;
|
|
1012
|
+
const scopeId = roles.id === void 0 ? "null::text" : `${col(roles.id)}::text`;
|
|
1013
|
+
const skipped = roles.skip === void 0 ? "false" : `coalesce(${col(roles.skip)}, false)`;
|
|
1014
|
+
const renamed = custom.renamed ?? {};
|
|
1015
|
+
const renames = Object.entries(renamed).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
1016
|
+
const current = (expr) => currentKeyExpr(renames, expr);
|
|
1017
|
+
const known = sqlTextArray([...custom.permissions ?? [], ...renames.map(([former]) => former)]);
|
|
1018
|
+
const declared = sqlTextArray(custom.declared);
|
|
1019
|
+
const scopes = sqlTextArray(ctx.scopes.map((item) => item.name));
|
|
1020
|
+
const ceiling = qualified(ctx, CUSTOM_ROLES.ceiling);
|
|
1021
|
+
const rp = qualified(ctx, "role_permissions");
|
|
1022
|
+
const perms = qualified(ctx, CUSTOM_ROLES.permissions);
|
|
1023
|
+
const includes = qualified(ctx, CUSTOM_ROLES.includes);
|
|
1024
|
+
const replace = qualified(ctx, CUSTOM_ROLES.trustedReplace);
|
|
1025
|
+
const p = from.permissions;
|
|
1026
|
+
const pTable = qualifySqlTable(p.table);
|
|
1027
|
+
const pRef = roleRef(p.role, p.table, roles, "rls.customRoles.from.permissions.role");
|
|
1028
|
+
const legacy = sourceSql(p.permission, pTable, "rls.customRoles.from.permissions.permission");
|
|
1029
|
+
const effect = p.effect === void 0 ? "'allow'" : `coalesce(lower(x.${quoteIdent$1(p.effect)}::text), 'allow')`;
|
|
1030
|
+
const level = p.level === void 0 ? "null::text" : `nullif(x.${quoteIdent$1(p.level)}::text, '')`;
|
|
1031
|
+
const inCeiling = (key) => `exists (select 1 from ${ceiling} c where c.scope = v.scope and c.effect = 'allow' and ${current("c.permission")} = ${key})`;
|
|
1032
|
+
const levelCheck = leveled ? `
|
|
1033
|
+
when m.effect = 'allow' and m.level is not null and not exists (
|
|
1034
|
+
select 1 from ${ceiling} c
|
|
1035
|
+
where c.scope = v.scope and c.effect = 'allow' and ${current("c.permission")} = m.key
|
|
1036
|
+
and split_part(c.grant_key, '@', 2) = m.level
|
|
1037
|
+
) then 'level ' || m.level || ' is not declared'` : "";
|
|
1038
|
+
const allowEntry = leveled ? "j.key || coalesce('@' || j.level, '')" : "j.key";
|
|
1039
|
+
const i = from.includes;
|
|
1040
|
+
const iTable = i === void 0 ? void 0 : qualifySqlTable(i.table);
|
|
1041
|
+
const iRef = i === void 0 || iTable === void 0 ? void 0 : roleRef(i.role, i.table, roles, "rls.customRoles.from.includes.role");
|
|
1042
|
+
const included = i === void 0 || iTable === void 0 ? void 0 : sourceSql(i.include, iTable, "rls.customRoles.from.includes.include");
|
|
1043
|
+
const refs = [`${col(pRef.id)} as permissions_ref`, ...iRef === void 0 ? [] : [`${col(iRef.id)} as includes_ref`]].join(", ");
|
|
1044
|
+
const includeBlock = i === void 0 || iTable === void 0 || iRef === void 0 || included === void 0 ? " v_include := null;" : ` with raw as (
|
|
1045
|
+
select ${included} as role
|
|
1046
|
+
from ${quoteTable(iTable)} x
|
|
1047
|
+
where x.${quoteIdent$1(iRef.ref)} = v.includes_ref
|
|
1048
|
+
),
|
|
1049
|
+
judged as (
|
|
1050
|
+
select n.role, case
|
|
1051
|
+
when n.role is null or not (n.role = any(${declared})) then 'not a declared role'
|
|
1052
|
+
when exists (
|
|
1053
|
+
select 1 from ${rp} rp
|
|
1054
|
+
where rp.role = n.role and rp.effect = 'allow'
|
|
1055
|
+
and not ${inCeiling(current("rp.permission"))}
|
|
1056
|
+
) then 'allows permissions outside the ceiling of ' || v.scope
|
|
1057
|
+
end as dropped
|
|
1058
|
+
from raw n
|
|
1059
|
+
)
|
|
1060
|
+
select array_agg(distinct j.role) filter (where j.dropped is null),
|
|
1061
|
+
array_agg(v.role || ': includes ' || coalesce(j.role, 'null') || ', ' || j.dropped) filter (where j.dropped is not null)
|
|
1062
|
+
into v_include, v_lost
|
|
1063
|
+
from judged j;
|
|
1064
|
+
v_dropped := v_dropped || coalesce(v_lost, '{}'::text[]);`;
|
|
1065
|
+
return `${BACKFILL_MARKER} schema=${ctx.schema ?? "permdock"}
|
|
1066
|
+
-- custom roles copied from ${qualifySqlTable(roles.table)} (rls.customRoles.from) through ${replace}
|
|
1067
|
+
-- a role that already has rows is left as it is; entries PermDock cannot hold are dropped and listed in one warning
|
|
1068
|
+
do $permdock$
|
|
1069
|
+
declare
|
|
1070
|
+
v record;
|
|
1071
|
+
v_allow text[];
|
|
1072
|
+
v_deny text[];
|
|
1073
|
+
v_include text[];
|
|
1074
|
+
v_lost text[];
|
|
1075
|
+
v_dropped text[] := '{}'::text[];
|
|
1076
|
+
v_copied integer := 0;
|
|
1077
|
+
v_present integer := 0;
|
|
1078
|
+
begin
|
|
1079
|
+
for v in
|
|
1080
|
+
select ${tenant} as tenant_id, ${scope} as scope, ${scopeId} as scope_id, ${col(roles.key)}::text as role, ${refs}
|
|
1081
|
+
from ${rolesTable} r
|
|
1082
|
+
where not ${skipped} and not (${col(roles.key)}::text = any(${declared}))
|
|
1083
|
+
order by 4, 2, 1, 3
|
|
1084
|
+
loop
|
|
1085
|
+
if v.role is null or not (v.scope = 'global' or v.scope = any(${scopes})) then
|
|
1086
|
+
v_dropped := v_dropped || (coalesce(v.role, 'null') || ': scope ' || coalesce(v.scope, 'null') || ' is not declared');
|
|
1087
|
+
continue;
|
|
1088
|
+
end if;
|
|
1089
|
+
if exists (
|
|
1090
|
+
select 1 from ${perms} c
|
|
1091
|
+
where c.tenant_id is not distinct from v.tenant_id and c.scope = v.scope and c.scope_id is not distinct from v.scope_id and c.role = v.role
|
|
1092
|
+
) or exists (
|
|
1093
|
+
select 1 from ${includes} c
|
|
1094
|
+
where c.tenant_id is not distinct from v.tenant_id and c.scope = v.scope and c.scope_id is not distinct from v.scope_id and c.role = v.role
|
|
1095
|
+
) then
|
|
1096
|
+
v_present := v_present + 1;
|
|
1097
|
+
continue;
|
|
1098
|
+
end if;
|
|
1099
|
+
with raw as (
|
|
1100
|
+
select ${legacy} as legacy, ${effect} as effect, ${level} as level
|
|
1101
|
+
from ${quoteTable(pTable)} x
|
|
1102
|
+
where x.${quoteIdent$1(pRef.ref)} = v.permissions_ref
|
|
1103
|
+
),
|
|
1104
|
+
mapped as (
|
|
1105
|
+
select n.legacy, ${legacyKeySql("n.legacy", migrate, renamed)} as key, n.effect, n.level
|
|
1106
|
+
from raw n
|
|
1107
|
+
),
|
|
1108
|
+
judged as (
|
|
1109
|
+
select m.*, case
|
|
1110
|
+
when m.effect not in ('allow', 'deny') then 'effect ' || m.effect || ' is not allow or deny'
|
|
1111
|
+
when m.key is null or not (m.key = any(${known})) then 'not a declared permission'
|
|
1112
|
+
when m.effect = 'allow' and not ${inCeiling("m.key")} then 'outside the ceiling of ' || v.scope${levelCheck}
|
|
1113
|
+
end as dropped
|
|
1114
|
+
from mapped m
|
|
1115
|
+
)
|
|
1116
|
+
select array_agg(distinct ${allowEntry}) filter (where j.dropped is null and j.effect = 'allow'),
|
|
1117
|
+
array_agg(distinct j.key) filter (where j.dropped is null and j.effect = 'deny'),
|
|
1118
|
+
array_agg(v.role || ': ' || coalesce(j.legacy, 'null') || ', ' || j.dropped) filter (where j.dropped is not null)
|
|
1119
|
+
into v_allow, v_deny, v_lost
|
|
1120
|
+
from judged j;
|
|
1121
|
+
v_dropped := v_dropped || coalesce(v_lost, '{}'::text[]);
|
|
1122
|
+
${includeBlock}
|
|
1123
|
+
if v_allow is null and v_deny is null and v_include is null then
|
|
1124
|
+
continue;
|
|
1125
|
+
end if;
|
|
1126
|
+
perform ${replace}(v.tenant_id, v.scope, v.scope_id, v.role, v_allow, v_deny, v_include);
|
|
1127
|
+
v_copied := v_copied + 1;
|
|
1128
|
+
end loop;
|
|
1129
|
+
raise notice 'permdock backfill: copied % custom role(s), % already present', v_copied, v_present;
|
|
1130
|
+
if cardinality(v_dropped) > 0 then
|
|
1131
|
+
raise warning 'permdock backfill: dropped % entr(ies) PermDock cannot hold: %', cardinality(v_dropped), array_to_string(v_dropped[1:50], '; ');
|
|
1132
|
+
end if;
|
|
1133
|
+
end
|
|
1134
|
+
$permdock$;
|
|
1135
|
+
`;
|
|
1136
|
+
}
|
|
1137
|
+
//#endregion
|
|
1138
|
+
//#region src/cli/rls/break-glass.ts
|
|
699
1139
|
/** The audit table every generated break-glass read writes to. Part of the SQL contract. */
|
|
700
1140
|
const BREAK_GLASS_AUDIT = "permdock_break_glass_audit";
|
|
701
|
-
const IDENT = /^[a-z][a-z0-9_]*$/u;
|
|
1141
|
+
const IDENT$1 = /^[a-z][a-z0-9_]*$/u;
|
|
702
1142
|
/**
|
|
703
1143
|
* The resources a `breakGlass` grant targets, with the table each reads. A
|
|
704
1144
|
* break-glass grant is non-portable, so `compileGrants` never turns it into a
|
|
@@ -747,22 +1187,19 @@ function rowsFilter(ctx, entry) {
|
|
|
747
1187
|
const asked = `p_permission = ${quoteLiteral(grant.permission)}`;
|
|
748
1188
|
if (grant.scope === "anyone") {
|
|
749
1189
|
if (grant.root === void 0) return [`(${asked})`];
|
|
750
|
-
return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified
|
|
1190
|
+
return grant.column === void 0 ? [] : [`(${asked} and ${quoteIdent$1(grant.column)} in (select ${qualified(ctx, memberIdsHelper(grant.root))}()))`];
|
|
751
1191
|
}
|
|
752
1192
|
if (grant.scope !== "global" && grant.column === void 0) return [];
|
|
753
1193
|
return [`(${asked} and ${accessSql(ctx, grant.scope, breakGlassKey(grant.permission), grant.column)})`];
|
|
754
1194
|
});
|
|
755
1195
|
return arms.length === 0 ? "false" : [...new Set(arms)].join("\n or ");
|
|
756
1196
|
}
|
|
757
|
-
function functionName(resource) {
|
|
758
|
-
if (!IDENT.test(resource)) throw new Error(`PermDock CLI: unsafe break-glass resource '${resource}'`);
|
|
1197
|
+
function functionName$1(resource) {
|
|
1198
|
+
if (!IDENT$1.test(resource)) throw new Error(`PermDock CLI: unsafe break-glass resource '${resource}'`);
|
|
759
1199
|
return `permdock_break_glass_${resource}`;
|
|
760
1200
|
}
|
|
761
|
-
function
|
|
762
|
-
|
|
763
|
-
}
|
|
764
|
-
function auditSql(ctx) {
|
|
765
|
-
const audit = qualified$1(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
|
|
1201
|
+
function auditSql$1(ctx) {
|
|
1202
|
+
const audit = qualified(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
|
|
766
1203
|
return `-- break-glass audit: one row per session that read restricted data
|
|
767
1204
|
-- RLS never compiles a break-glass override, so restricted rows stay denied to a direct read
|
|
768
1205
|
create table if not exists ${audit} (
|
|
@@ -777,9 +1214,9 @@ alter table ${audit} enable row level security;
|
|
|
777
1214
|
revoke all on table ${audit} from anon, authenticated, public;`;
|
|
778
1215
|
}
|
|
779
1216
|
function readFunctionSql(ctx, entry) {
|
|
780
|
-
const fn = qualified
|
|
781
|
-
const audit = qualified
|
|
782
|
-
const table =
|
|
1217
|
+
const fn = qualified(ctx, functionName$1(entry.resource));
|
|
1218
|
+
const audit = qualified(ctx, quoteIdent$1(BREAK_GLASS_AUDIT));
|
|
1219
|
+
const table = quoteQualified(entry.table);
|
|
783
1220
|
const session = subjectClaimJsonSql(ctx, "break_glass");
|
|
784
1221
|
const subject = subjectIdSql(ctx);
|
|
785
1222
|
return `-- ${entry.resource}: reads restricted rows through a checked, audited break-glass session
|
|
@@ -827,10 +1264,10 @@ grant execute on function ${fn}(text) to authenticated;`;
|
|
|
827
1264
|
*/
|
|
828
1265
|
function breakGlassSql(ctx, entries) {
|
|
829
1266
|
if (entries.length === 0) return "";
|
|
830
|
-
return [auditSql(ctx), ...entries.map((entry) => readFunctionSql(ctx, entry))].join("\n\n");
|
|
1267
|
+
return [auditSql$1(ctx), ...entries.map((entry) => readFunctionSql(ctx, entry))].join("\n\n");
|
|
831
1268
|
}
|
|
832
1269
|
//#endregion
|
|
833
|
-
//#region src/cli/rls
|
|
1270
|
+
//#region src/cli/rls/emit.ts
|
|
834
1271
|
const HEADER = "-- generated by permdock rls generate\n-- fail-closed: never target a bypass role\n";
|
|
835
1272
|
const TRUSTED_READER_GRANT = /^grant execute on function [^\n]+\.permdock_trusted_role_permissions\([^)\n]*\) to [^\n;]+;$/gmu;
|
|
836
1273
|
function assertNoServiceRole(text) {
|
|
@@ -855,7 +1292,7 @@ function tablesOf(policies) {
|
|
|
855
1292
|
/** Enable (and force) row level security, then the table grants for the commands a policy allows. */
|
|
856
1293
|
function tableSql(table, policies, force, views) {
|
|
857
1294
|
const closed = views.find((view) => view.table === table && view.companion !== void 0);
|
|
858
|
-
const target =
|
|
1295
|
+
const target = quoteQualified(table);
|
|
859
1296
|
const lines = [`alter table ${target} enable row level security;`];
|
|
860
1297
|
if (force) lines.push(`alter table ${target} force row level security;`);
|
|
861
1298
|
lines.push(`revoke all on table ${target} from anon, authenticated;`);
|
|
@@ -875,7 +1312,7 @@ function tableSql(table, policies, force, views) {
|
|
|
875
1312
|
}
|
|
876
1313
|
/** `drop policy if exists` and `create policy` for one policy, without the table's grants. */
|
|
877
1314
|
function policySql(item) {
|
|
878
|
-
const target =
|
|
1315
|
+
const target = quoteQualified(item.table);
|
|
879
1316
|
const lines = [
|
|
880
1317
|
`drop policy if exists ${quoteIdent$1(item.name)} on ${target};`,
|
|
881
1318
|
`create policy ${quoteIdent$1(item.name)}`,
|
|
@@ -1018,7 +1455,7 @@ function defaultOut(target) {
|
|
|
1018
1455
|
}
|
|
1019
1456
|
}
|
|
1020
1457
|
//#endregion
|
|
1021
|
-
//#region src/cli/rls
|
|
1458
|
+
//#region src/cli/rls/realtime-storage.ts
|
|
1022
1459
|
const SCOPE_SEGMENT = /^\{([a-z][a-z0-9_]*)\}$/u;
|
|
1023
1460
|
const LITERAL_SEGMENT = /^[^{}:\s]+$/u;
|
|
1024
1461
|
const TOPIC = "(select realtime.topic())";
|
|
@@ -1044,7 +1481,7 @@ function declaredScope(facts, scope, where) {
|
|
|
1044
1481
|
* relationship or a `where` on the same permission never widens it.
|
|
1045
1482
|
*/
|
|
1046
1483
|
function permitted(ctx, value, scope, key) {
|
|
1047
|
-
return `${value} in (select x::text from ${qualified
|
|
1484
|
+
return `${value} in (select x::text from ${qualified(ctx, permittedByPermissionHelper(scope))}(${quoteLiteral(key)}) x)`;
|
|
1048
1485
|
}
|
|
1049
1486
|
function escapeRegex(text) {
|
|
1050
1487
|
return text.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
|
|
@@ -1161,7 +1598,7 @@ function realtimeStoragePolicies(ctx, facts, config, warnings = []) {
|
|
|
1161
1598
|
return policies;
|
|
1162
1599
|
}
|
|
1163
1600
|
//#endregion
|
|
1164
|
-
//#region src/cli/rls
|
|
1601
|
+
//#region src/cli/rls/rows.ts
|
|
1165
1602
|
/** `permitted_<resource>_rows(p_permission)`: the resource's row ids the subject may act on with one permission. */
|
|
1166
1603
|
function rowsHelper(resource) {
|
|
1167
1604
|
const name = graphSqlName(resource);
|
|
@@ -1243,7 +1680,7 @@ function inheritTargets(policy) {
|
|
|
1243
1680
|
return [...out].toSorted();
|
|
1244
1681
|
}
|
|
1245
1682
|
function stubSql(ctx, resource) {
|
|
1246
|
-
return `create or replace function ${qualified
|
|
1683
|
+
return `create or replace function ${qualified(ctx, rowsHelper(resource))}(p_permission text)
|
|
1247
1684
|
returns setof text
|
|
1248
1685
|
language sql
|
|
1249
1686
|
stable
|
|
@@ -1254,7 +1691,7 @@ as $$
|
|
|
1254
1691
|
$$;`;
|
|
1255
1692
|
}
|
|
1256
1693
|
function rowSql(ctx, rows, table, where) {
|
|
1257
|
-
const fn = qualified
|
|
1694
|
+
const fn = qualified(ctx, rows.replace(/_rows$/u, "_row"));
|
|
1258
1695
|
return `create or replace function ${fn}(p_row ${table}, p_permission text)
|
|
1259
1696
|
returns boolean
|
|
1260
1697
|
language sql
|
|
@@ -1269,8 +1706,8 @@ grant execute on function ${fn}(${table}, text) to authenticated;`;
|
|
|
1269
1706
|
}
|
|
1270
1707
|
function resourceSql(ctx, node, branches, tables) {
|
|
1271
1708
|
const name = rowsHelper(node.name);
|
|
1272
|
-
const fn = qualified
|
|
1273
|
-
const table =
|
|
1709
|
+
const fn = qualified(ctx, name);
|
|
1710
|
+
const table = quoteQualified(tableFor(node.name, tables));
|
|
1274
1711
|
const cases = [...branches.keys()].toSorted().flatMap((key) => {
|
|
1275
1712
|
const sql = permissionCase(key, branches.get(key) ?? []);
|
|
1276
1713
|
return sql === void 0 ? [] : [sql];
|
|
@@ -1293,7 +1730,7 @@ ${rowSql(ctx, name, table, where)}`;
|
|
|
1293
1730
|
if (ctx.dialect === "neon") return rows;
|
|
1294
1731
|
return `${rows}
|
|
1295
1732
|
-- the same rows for a user the caller names, with claims it supplies; no client role may execute it
|
|
1296
|
-
${forUserSql(ctx, qualified
|
|
1733
|
+
${forUserSql(ctx, qualified(ctx, `${name}_for`), fn)}`;
|
|
1297
1734
|
}
|
|
1298
1735
|
/**
|
|
1299
1736
|
* `permitted_<resource>_rows(p_permission)` for each resource `select`
|
|
@@ -1320,43 +1757,125 @@ function rowHelpersSql(ctx, policy, branches, select, tables, warnings) {
|
|
|
1320
1757
|
return chunks.length === 0 ? "" : `${chunks.join("\n\n")}\n`;
|
|
1321
1758
|
}
|
|
1322
1759
|
//#endregion
|
|
1323
|
-
//#region src/cli/rls-
|
|
1324
|
-
const
|
|
1760
|
+
//#region src/cli/rls/generate-policies.ts
|
|
1761
|
+
const SERVICE_TABLES_HEADER = "-- Supabase Realtime and Storage: rls.realtime and rls.storage. RLS on both tables is Supabase's.";
|
|
1325
1762
|
/**
|
|
1326
|
-
*
|
|
1327
|
-
*
|
|
1763
|
+
* A `realtime.messages` or `storage.objects` policy that applies only where
|
|
1764
|
+
* the table exists. The Realtime and Storage services create those tables, so
|
|
1765
|
+
* a local stack with the service off, and pg-delta's shadow database, have
|
|
1766
|
+
* none.
|
|
1328
1767
|
*/
|
|
1329
|
-
function
|
|
1330
|
-
const
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1768
|
+
function guardedPolicySql(item) {
|
|
1769
|
+
const body = policySql(item).trimEnd().split("\n").map((line) => ` ${line}`).join("\n");
|
|
1770
|
+
return `do $permdock$
|
|
1771
|
+
begin
|
|
1772
|
+
if to_regclass(${quoteLiteral(qualifySqlTable(item.table))}) is not null then
|
|
1773
|
+
${body}
|
|
1774
|
+
end if;
|
|
1775
|
+
end
|
|
1776
|
+
$permdock$;
|
|
1777
|
+
`;
|
|
1338
1778
|
}
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
return
|
|
1779
|
+
/** Appends the `realtime.messages` and `storage.objects` policies, which never get `tableSql`. */
|
|
1780
|
+
function withHelperTables(sql, helperTables) {
|
|
1781
|
+
if (helperTables === void 0) return sql;
|
|
1782
|
+
const tables = `${helperTables.trimEnd()}\n`;
|
|
1783
|
+
return sql.trim() === "" ? tables : `${sql.trimEnd()}\n\n${tables}`;
|
|
1342
1784
|
}
|
|
1343
|
-
function
|
|
1344
|
-
if (
|
|
1785
|
+
function databaseModeWarnings(policy, { ctx }, warnings) {
|
|
1786
|
+
if (ctx.authorize !== "database") return;
|
|
1787
|
+
for (const { name } of ctx.scopes) if (policy.grants.some((grant) => grant.scope === name) && scopeTable(ctx, name) === void 0 && scopeSources(ctx, name).length === 0) warnings.push(`${name}-scoped grants read the ${name} memberships table in database mode: set rls.memberships.scopes.${name}, rls.membershipSources or supabase.hook.memberships (or --memberships for the first scope), otherwise they deny`);
|
|
1345
1788
|
}
|
|
1346
|
-
/**
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1789
|
+
/** The grants compiled into table policies, and the Realtime and Storage policies beside them. */
|
|
1790
|
+
function generatePolicies(input, policy, options, built, warnings) {
|
|
1791
|
+
const rls = input.config.rls;
|
|
1792
|
+
const { ctx } = built;
|
|
1793
|
+
const { fieldsMode, revokeColumns } = options;
|
|
1794
|
+
databaseModeWarnings(policy, built, warnings);
|
|
1795
|
+
const compiled = compileGrants(policy, ctx, rls?.tables, warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
|
|
1796
|
+
const helpersOnly = input.helpersOnly === true || rls?.helpersOnly === true;
|
|
1797
|
+
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" };
|
|
1798
|
+
const views = fieldsMode === void 0 ? [] : fieldViews(policy, ctx, compiled.branches, {
|
|
1799
|
+
revokeColumns,
|
|
1800
|
+
helpersOnly
|
|
1801
|
+
}, warnings);
|
|
1802
|
+
const force = input.force === true || rls?.force === true;
|
|
1803
|
+
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");
|
|
1804
|
+
const policyName = input.policyName ?? rls?.policyName;
|
|
1805
|
+
const assembled = helpersOnly ? [] : assemblePolicies(fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), {
|
|
1806
|
+
perRole: input.policyPerRole === true || rls?.policyPerRole === true,
|
|
1807
|
+
...policyName === void 0 ? {} : { name: policyName }
|
|
1808
|
+
});
|
|
1809
|
+
const actorKinds = readOnlyActorKinds(rls?.readOnlyActors);
|
|
1810
|
+
if (actorKinds !== void 0 && helpersOnly) warnings.push("rls.readOnlyActors adds policies, and --helpers-only writes none: add the restrictive read-only policies to the hand-written ones");
|
|
1811
|
+
const withActors = (items) => actorKinds === void 0 ? items : [...items, ...readOnlyActorPolicies(items, subjectClaimJsonSql(ctx, "act"), actorKinds)];
|
|
1812
|
+
let tablePolicies;
|
|
1813
|
+
try {
|
|
1814
|
+
tablePolicies = realtimeStoragePolicies(ctx, {
|
|
1815
|
+
scopes: new Set(ctx.scopes.map((scope) => scope.name)),
|
|
1816
|
+
keys: new Set(listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key)),
|
|
1817
|
+
rowConditions: new Set(policyRowConditionKeys(policy))
|
|
1818
|
+
}, compact({
|
|
1819
|
+
realtime: rls?.realtime,
|
|
1820
|
+
storage: rls?.storage
|
|
1821
|
+
}), warnings);
|
|
1822
|
+
} catch (cause) {
|
|
1823
|
+
return { refusal: describeError(cause) };
|
|
1824
|
+
}
|
|
1825
|
+
if (tablePolicies.length > 0 && helpersOnly) warnings.push("rls.realtime and rls.storage add policies, and --helpers-only writes none: no realtime.messages or storage.objects policy is written");
|
|
1826
|
+
const servicePolicies = tablePolicies.length === 0 || helpersOnly ? [] : withActors(tablePolicies);
|
|
1827
|
+
const generated = {
|
|
1828
|
+
compiled,
|
|
1829
|
+
helpersOnly,
|
|
1830
|
+
force,
|
|
1831
|
+
views,
|
|
1832
|
+
policies: withActors(assembled)
|
|
1833
|
+
};
|
|
1834
|
+
if (servicePolicies.length === 0) return generated;
|
|
1835
|
+
return {
|
|
1836
|
+
...generated,
|
|
1837
|
+
helperTables: [SERVICE_TABLES_HEADER, ...servicePolicies.map(policySql)].join("\n"),
|
|
1838
|
+
guardedTables: [`${SERVICE_TABLES_HEADER} A stack without the service has no table, so each policy is skipped there.`, ...servicePolicies.map(guardedPolicySql)].join("\n")
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
//#endregion
|
|
1842
|
+
//#region src/cli/rls/shims.ts
|
|
1843
|
+
const NAME = /^[a-z_][a-z0-9_]*$/u;
|
|
1844
|
+
/**
|
|
1845
|
+
* The key a legacy caller passes, mapped the way `rls migrate` maps it:
|
|
1846
|
+
* `keys` and `renamed` exactly, then the longest `prefixes` entry.
|
|
1847
|
+
*/
|
|
1848
|
+
function mappedKey(config, renamed) {
|
|
1849
|
+
const exact = {
|
|
1850
|
+
...renamed,
|
|
1851
|
+
...config.keys
|
|
1852
|
+
};
|
|
1853
|
+
const prefixes = Object.entries(config.prefixes ?? {}).toSorted(([a], [b]) => b.length - a.length);
|
|
1854
|
+
const fallback = prefixes.length === 0 ? "p_key" : `case ${prefixes.map(([from, to]) => `when pg_catalog.starts_with(p_key, ${quoteLiteral(from)}) then ${quoteLiteral(to)} || pg_catalog.substr(p_key, ${String(from.length + 1)})`).join(" ")} else p_key end`;
|
|
1855
|
+
if (Object.keys(exact).length === 0) return fallback;
|
|
1856
|
+
return `coalesce(${quoteLiteral(JSON.stringify(exact))}::jsonb ->> p_key, ${fallback})`;
|
|
1857
|
+
}
|
|
1858
|
+
function scopeOf(ctx, scope) {
|
|
1859
|
+
if (!ctx.scopes.some((item) => item.name === scope)) throw new Error(`PermDock CLI: rls.migrate helper scope '${scope}' is not a declared scope`);
|
|
1860
|
+
return scope;
|
|
1861
|
+
}
|
|
1862
|
+
function addKey(list, key) {
|
|
1863
|
+
if (!list.includes(key)) list.push(key);
|
|
1864
|
+
}
|
|
1865
|
+
/**
|
|
1866
|
+
* The grant keys behind each permission, per scope: the keys of its
|
|
1867
|
+
* unconditional allows, and every deny key. A shim cannot apply a row
|
|
1868
|
+
* condition or a validity window, so a conditional allow answers nothing and
|
|
1869
|
+
* a conditional deny always subtracts; the conditioned keys are listed apart.
|
|
1870
|
+
* A break-glass key answers only the break-glass read, so it is left out too.
|
|
1871
|
+
* Rows seeded under a former key are left out; the shim maps a former key to
|
|
1872
|
+
* the current one first.
|
|
1873
|
+
*/
|
|
1874
|
+
function shimGrants(rows, conditioned, renamed) {
|
|
1875
|
+
const byScope = /* @__PURE__ */ new Map();
|
|
1876
|
+
for (const row of rows.toSorted((a, b) => a.grantKey < b.grantKey ? -1 : a.grantKey > b.grantKey ? 1 : 0)) {
|
|
1877
|
+
if (Object.hasOwn(renamed, row.permission) || row.effect === "allow" && row.grantKey === breakGlassKey(row.permission)) continue;
|
|
1878
|
+
const scope = byScope.get(row.scope) ?? {};
|
|
1360
1879
|
byScope.set(row.scope, scope);
|
|
1361
1880
|
const keys = (Object.hasOwn(scope, row.permission) ? scope[row.permission] : void 0) ?? {
|
|
1362
1881
|
allow: [],
|
|
@@ -1404,7 +1923,7 @@ function grantKeys(keys, scope, effect) {
|
|
|
1404
1923
|
}
|
|
1405
1924
|
/** The instances of `scope` the caller holds an unconditional allow of the key in, minus those it holds a deny in. */
|
|
1406
1925
|
function idsSql(ctx, keys, scope) {
|
|
1407
|
-
const helper = qualified
|
|
1926
|
+
const helper = qualified(ctx, permittedIdsHelper(scope));
|
|
1408
1927
|
return ` select a.id
|
|
1409
1928
|
from ${grantKeys(keys, scope, "allow")}
|
|
1410
1929
|
cross join lateral ${helper}(g.grant_key) a(id)
|
|
@@ -1414,7 +1933,7 @@ function idsSql(ctx, keys, scope) {
|
|
|
1414
1933
|
cross join lateral ${helper}(g.grant_key) d(id)`;
|
|
1415
1934
|
}
|
|
1416
1935
|
function hasSql(ctx, keys) {
|
|
1417
|
-
const has = qualified
|
|
1936
|
+
const has = qualified(ctx, HELPERS.has);
|
|
1418
1937
|
return `exists (select 1 from ${grantKeys(keys, "global", "allow")} where ${has}(g.grant_key))
|
|
1419
1938
|
and not exists (select 1 from ${grantKeys(keys, "global", "deny")} where ${has}(g.grant_key))`;
|
|
1420
1939
|
}
|
|
@@ -1432,7 +1951,7 @@ function shim(ctx, config, keys, fn, helper) {
|
|
|
1432
1951
|
case "membership": {
|
|
1433
1952
|
const scope = scopeOf(ctx, helper.scope);
|
|
1434
1953
|
const type = scopeTypeOf(ctx, scope);
|
|
1435
|
-
return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified
|
|
1954
|
+
return shimFunction(fn, `p_id ${type}`, type, "boolean", ` select coalesce(p_id in (select ${qualified(ctx, memberIdsHelper(scope))}()), false)`);
|
|
1436
1955
|
}
|
|
1437
1956
|
case "global": return shimFunction(fn, "p_key text", "text", "boolean", ` select ${hasSql(ctx, keys)}`);
|
|
1438
1957
|
case "scoped": {
|
|
@@ -1468,222 +1987,111 @@ function shimsSql(ctx, config, shims, renamed, grants) {
|
|
|
1468
1987
|
return `${chunks.join("\n\n")}\n`;
|
|
1469
1988
|
}
|
|
1470
1989
|
//#endregion
|
|
1471
|
-
//#region src/cli/rls
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1990
|
+
//#region src/cli/rls/triggers.ts
|
|
1991
|
+
const PREFIX = "PermDock CLI";
|
|
1992
|
+
const EVENTS = /* @__PURE__ */ new Set([
|
|
1993
|
+
"insert",
|
|
1994
|
+
"update",
|
|
1995
|
+
"delete",
|
|
1996
|
+
"truncate"
|
|
1997
|
+
]);
|
|
1998
|
+
const AUDITED = [
|
|
1999
|
+
CUSTOM_ROLES.permissions,
|
|
2000
|
+
CUSTOM_ROLES.includes,
|
|
2001
|
+
"user_roles"
|
|
2002
|
+
];
|
|
2003
|
+
const IDENT = /^[a-z_][a-z0-9_$]*$/u;
|
|
2004
|
+
/** The tables `sql` creates in `schema`, by name without the schema. */
|
|
2005
|
+
function generatedTables(sql, schema) {
|
|
2006
|
+
const prefix = `create table if not exists ${quoteIdent$1(schema)}.`;
|
|
2007
|
+
const found = /* @__PURE__ */ new Set();
|
|
2008
|
+
for (const line of sql.split("\n")) {
|
|
2009
|
+
if (!line.startsWith(prefix)) continue;
|
|
2010
|
+
const name = /^"?([A-Za-z_][\w$]*)"?/u.exec(line.slice(prefix.length))?.[1];
|
|
2011
|
+
if (name !== void 0) found.add(name);
|
|
2012
|
+
}
|
|
2013
|
+
return [...found].toSorted();
|
|
2014
|
+
}
|
|
2015
|
+
function functionName(name, label) {
|
|
2016
|
+
if (typeof name !== "string" || name.split(".").length !== 2) throw new Error(`${PREFIX}: ${label} must be a schema-qualified function such as audit.log_change, got ${JSON.stringify(name)}`);
|
|
2017
|
+
return quoteTable(name);
|
|
2018
|
+
}
|
|
2019
|
+
function knownTable(table, tables, label) {
|
|
2020
|
+
if (!tables.includes(table)) throw new Error(`${PREFIX}: ${label} names ${table}, which rls generate does not create with this config; it creates ${tables.join(", ")}`);
|
|
2021
|
+
}
|
|
2022
|
+
function triggerSql(table, trigger, label) {
|
|
2023
|
+
if (typeof trigger.name !== "string" || trigger.name === "" || trigger.name.toLowerCase().startsWith("permdock_")) throw new Error(`${PREFIX}: ${label}.name must be a name outside permdock_, which generated triggers use, got ${JSON.stringify(trigger.name)}`);
|
|
2024
|
+
if (trigger.when !== "before" && trigger.when !== "after") throw new Error(`${PREFIX}: ${label}.when must be before or after`);
|
|
2025
|
+
const events = trigger.events;
|
|
2026
|
+
if (events.length === 0 || !events.every((event) => EVENTS.has(event))) throw new Error(`${PREFIX}: ${label}.events must list insert, update, delete or truncate`);
|
|
2027
|
+
const level = trigger.level ?? "row";
|
|
2028
|
+
if (level !== "row" && level !== "statement") throw new Error(`${PREFIX}: ${label}.level must be row or statement`);
|
|
2029
|
+
if (level === "row" && events.includes("truncate")) throw new Error(`${PREFIX}: ${label} fires on truncate, which Postgres allows only with level: 'statement'`);
|
|
2030
|
+
const fn = functionName(trigger.function, `${label}.function`);
|
|
2031
|
+
const args = (trigger.args ?? []).map((arg) => quoteLiteral(arg));
|
|
2032
|
+
const name = quoteIdent$1(trigger.name);
|
|
2033
|
+
return `drop trigger if exists ${name} on ${table};
|
|
2034
|
+
create trigger ${name}
|
|
2035
|
+
${trigger.when} ${[...new Set(events)].join(" or ")} on ${table}
|
|
2036
|
+
for each ${level} execute function ${fn}(${args.join(", ")});`;
|
|
1516
2037
|
}
|
|
1517
2038
|
/**
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1520
|
-
*
|
|
2039
|
+
* `rls.triggers`: the application's triggers on generated tables, after the
|
|
2040
|
+
* tables. A table this run does not create is an error, so a trigger never
|
|
2041
|
+
* silently goes missing.
|
|
1521
2042
|
*/
|
|
1522
|
-
function
|
|
1523
|
-
if (
|
|
1524
|
-
const
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
2043
|
+
function triggersSql(triggers, schema, tables) {
|
|
2044
|
+
if (triggers === void 0) return "";
|
|
2045
|
+
const chunks = [];
|
|
2046
|
+
for (const [table, list] of Object.entries(triggers).toSorted(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
2047
|
+
knownTable(table, tables, `rls.triggers.${table}`);
|
|
2048
|
+
const target = `${quoteIdent$1(schema)}.${quoteIdent$1(table)}`;
|
|
2049
|
+
for (const [index, trigger] of list.entries()) chunks.push(triggerSql(target, trigger, `rls.triggers.${table}[${String(index)}]`));
|
|
1529
2050
|
}
|
|
1530
|
-
|
|
1531
|
-
return rowColumns;
|
|
2051
|
+
return chunks.length === 0 ? "" : `-- rls.triggers: triggers the application owns on generated tables\n${chunks.join("\n")}\n`;
|
|
1532
2052
|
}
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
const
|
|
1542
|
-
if (
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
const policy = await loadPolicy(input.cwd, input.config, input.from);
|
|
1548
|
-
const memberships = parseMembershipsFlag(input.memberships) ?? rls?.memberships;
|
|
1549
|
-
if (input.rbac && input.dialect !== "supabase") return {
|
|
1550
|
-
code: 2,
|
|
1551
|
-
output: `rls generate --rbac supabase needs --dialect supabase (got ${input.dialect})`,
|
|
1552
|
-
text: ""
|
|
1553
|
-
};
|
|
1554
|
-
const schema = input.rbacSchema ?? rls?.schema ?? rls?.rbac?.schema ?? "permdock";
|
|
1555
|
-
const authorize = resolveAuthorize(input.config, {
|
|
1556
|
-
authorize: input.authorize,
|
|
1557
|
-
rbac: input.rbac,
|
|
1558
|
-
memberships
|
|
1559
|
-
});
|
|
1560
|
-
const memberSources = rls?.membershipSources ?? input.config.supabase?.hook?.memberships;
|
|
1561
|
-
const sources = authorize === "database" ? memberSources : void 0;
|
|
1562
|
-
const scopes = scopeList(policy.scopes);
|
|
1563
|
-
const suspension = checkSuspension(rls?.suspension, scopes);
|
|
1564
|
-
const hookRoles = input.config.supabase?.hook?.roles;
|
|
1565
|
-
const roles = rls?.roles ?? (hookRoles === false ? void 0 : hookRoles);
|
|
1566
|
-
if (roles !== void 0 && input.rbac) return {
|
|
1567
|
-
code: 2,
|
|
1568
|
-
output: "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",
|
|
1569
|
-
text: ""
|
|
1570
|
-
};
|
|
1571
|
-
const requires = writeRequirement(policy, rls?.customRoleWrites?.requires);
|
|
1572
|
-
if (requires.error !== void 0) return {
|
|
1573
|
-
code: 2,
|
|
1574
|
-
output: requires.error,
|
|
1575
|
-
text: ""
|
|
1576
|
-
};
|
|
1577
|
-
const readers = rls?.trustedReaders;
|
|
1578
|
-
if (readers !== void 0 && (!Array.isArray(readers) || !readers.every((name) => typeof name === "string" && name !== ""))) return {
|
|
1579
|
-
code: 2,
|
|
1580
|
-
output: "rls.trustedReaders must list Postgres role names, such as ['service_role']",
|
|
1581
|
-
text: ""
|
|
1582
|
-
};
|
|
1583
|
-
const ownership = ownershipRules(policy, scopes);
|
|
1584
|
-
const graph = graphPlan(policy);
|
|
1585
|
-
const apiKeys = apiKeysPlan(rls?.apiKeys, new Set(roleNames(policy)), Object.fromEntries(renamedKeys(policy.vocabulary.permissions)));
|
|
1586
|
-
const ctx = {
|
|
1587
|
-
dialect: input.dialect,
|
|
1588
|
-
scopes,
|
|
1589
|
-
tenantClaim: rls?.tenantClaim ?? "tenant_id",
|
|
1590
|
-
...rls?.tenants === "all" ? { tenants: "all" } : {},
|
|
1591
|
-
gucPrefix: input.gucPrefix ?? rls?.gucPrefix ?? "app",
|
|
1592
|
-
...rls?.actions === void 0 ? {} : { actions: rls.actions },
|
|
1593
|
-
inlineFunctions: input.inlineFunctions || rls?.inlineFunctions === true,
|
|
1594
|
-
schema,
|
|
1595
|
-
authorize,
|
|
1596
|
-
roleClaim: rls?.roleClaim ?? "user_role",
|
|
1597
|
-
tenantType: input.tenantType ?? rls?.tenantType ?? "uuid",
|
|
1598
|
-
...rls?.teamType === void 0 ? {} : { teamType: rls.teamType },
|
|
1599
|
-
...rls?.scopeTypes === void 0 ? {} : { scopeTypes: rls.scopeTypes },
|
|
1600
|
-
...memberships === void 0 ? {} : { memberships },
|
|
1601
|
-
...sources === void 0 || sources.length === 0 ? {} : { sources },
|
|
1602
|
-
...memberSources === void 0 || memberSources.length === 0 ? {} : { memberSources },
|
|
1603
|
-
...suspension === void 0 ? {} : { suspension },
|
|
1604
|
-
...roles === void 0 ? {} : { roles },
|
|
1605
|
-
...input.customRoles === true || rls?.customRoles === true ? { customRoles: {
|
|
1606
|
-
...customRoleNames(policy, requires.keys),
|
|
1607
|
-
...rls?.customRoleWrites?.roles === void 0 ? {} : { table: rls.customRoleWrites.roles }
|
|
1608
|
-
} } : {},
|
|
1609
|
-
...input.capabilities === true || rls?.capabilities === true ? { capabilities: true } : {},
|
|
1610
|
-
...apiKeys === void 0 ? {} : { apiKeys },
|
|
1611
|
-
...rls?.anonymousSignIns === "deny" ? { anonymousSignIns: "deny" } : {},
|
|
1612
|
-
...ownership === void 0 ? {} : { ownership },
|
|
1613
|
-
...rls?.ownershipTriggers === void 0 ? {} : { skipOwnershipTriggers: rls.ownershipTriggers === false ? "all" : Object.keys(rls.ownershipTriggers).map((key) => {
|
|
1614
|
-
const name = resolveScope(scopes, key);
|
|
1615
|
-
if (name === void 0) throw new Error(`PermDock CLI: rls.ownershipTriggers.${key} names a scope the policy does not declare`);
|
|
1616
|
-
return name;
|
|
1617
|
-
}) },
|
|
1618
|
-
...rls?.assignments === void 0 ? {} : { assignments: rls.assignments === true ? { tables: [] } : {
|
|
1619
|
-
tables: rls.assignments.tables ?? [],
|
|
1620
|
-
...rls.assignments.ownRole === void 0 ? {} : { ownRole: rls.assignments.ownRole }
|
|
1621
|
-
} },
|
|
1622
|
-
...fieldsMode === void 0 ? {} : { fields: fieldsMode },
|
|
1623
|
-
...graph.size === 0 ? {} : { graph: {
|
|
1624
|
-
closures: closureDepths(graph),
|
|
1625
|
-
resources: policy.resources,
|
|
1626
|
-
...rls?.tables === void 0 ? {} : { tables: rls.tables }
|
|
1627
|
-
} }
|
|
1628
|
-
};
|
|
1629
|
-
if (ctx.anonymousSignIns === "deny" && ctx.dialect !== "supabase") return {
|
|
1630
|
-
code: 2,
|
|
1631
|
-
output: `rls.anonymousSignIns needs --dialect supabase (got ${ctx.dialect})`,
|
|
1632
|
-
text: ""
|
|
1633
|
-
};
|
|
1634
|
-
const jsonSchema = rls?.jsonSchema;
|
|
1635
|
-
if (jsonSchema !== void 0 && jsonSchema !== "auto" && typeof jsonSchema !== "boolean") return {
|
|
1636
|
-
code: 2,
|
|
1637
|
-
output: `rls.jsonSchema must be 'auto', true or false (got ${JSON.stringify(jsonSchema)})`,
|
|
1638
|
-
text: ""
|
|
1639
|
-
};
|
|
1640
|
-
const warnings = [];
|
|
1641
|
-
if (jsonSchema !== void 0 && jsonSchema !== false && (rls?.approvals === void 0 || rls.approvals === false)) warnings.push("rls.jsonSchema constrains the approval store's body, which needs rls.approvals: true; nothing else is jsonb");
|
|
1642
|
-
if (authorize === "database") {
|
|
1643
|
-
for (const { name } of ctx.scopes) if (policy.grants.some((grant) => grant.scope === name) && scopeTable(ctx, name) === void 0 && scopeSources(ctx, name).length === 0) warnings.push(`${name}-scoped grants read the ${name} memberships table in database mode: set rls.memberships.scopes.${name}, rls.membershipSources or supabase.hook.memberships (or --memberships for the first scope), otherwise they deny`);
|
|
1644
|
-
}
|
|
1645
|
-
const compiled = compileGrants(policy, ctx, rls?.tables, warnings, input.skipClosures, rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0);
|
|
1646
|
-
const helpersOnly = input.helpersOnly === true || rls?.helpersOnly === true;
|
|
1647
|
-
if (helpersOnly && (input.target !== "sql" || revokeColumns)) return {
|
|
1648
|
-
code: 2,
|
|
1649
|
-
output: "rls generate --helpers-only needs --target sql and no --revoke-columns: the table grants are hand-written, so revoke the restricted columns there",
|
|
1650
|
-
text: ""
|
|
1651
|
-
};
|
|
1652
|
-
const views = fieldsMode === void 0 ? [] : fieldViews(policy, ctx, compiled.branches, {
|
|
1653
|
-
revokeColumns,
|
|
1654
|
-
helpersOnly
|
|
1655
|
-
}, warnings);
|
|
1656
|
-
const force = input.force === true || rls?.force === true;
|
|
1657
|
-
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");
|
|
1658
|
-
const policyName = input.policyName ?? rls?.policyName;
|
|
1659
|
-
const assembled = helpersOnly ? [] : assemblePolicies(fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), {
|
|
1660
|
-
perRole: input.policyPerRole === true || rls?.policyPerRole === true,
|
|
1661
|
-
...policyName === void 0 ? {} : { name: policyName }
|
|
2053
|
+
/**
|
|
2054
|
+
* `rls.audit`: one call of the audit function per generated table, which
|
|
2055
|
+
* registers the table with the application's audit module. Statements that
|
|
2056
|
+
* run code, so they travel with the seeds.
|
|
2057
|
+
*/
|
|
2058
|
+
function auditSql(audit, schema, tables) {
|
|
2059
|
+
if (audit === void 0) return "";
|
|
2060
|
+
const fn = functionName(audit.function, "rls.audit.function");
|
|
2061
|
+
const listed = audit.tables;
|
|
2062
|
+
if (listed !== void 0) for (const table of listed) knownTable(table, tables, "rls.audit.tables");
|
|
2063
|
+
const targets = listed ?? AUDITED.filter((table) => tables.includes(table));
|
|
2064
|
+
const args = Object.entries(audit.args ?? {}).map(([name, value]) => {
|
|
2065
|
+
if (!IDENT.test(name)) throw new Error(`${PREFIX}: rls.audit.args names ${JSON.stringify(name)}, which is not a lower-case parameter name`);
|
|
2066
|
+
return `, ${quoteIdent$1(name)} => ${quoteLiteral(value)}`;
|
|
1662
2067
|
});
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
2068
|
+
if (targets.length === 0) return "";
|
|
2069
|
+
return `-- rls.audit: generated tables registered with ${audit.function}
|
|
2070
|
+
${targets.map((table) => `select ${fn}(${quoteLiteral(`${quoteIdent$1(schema)}.${quoteIdent$1(table)}`)}::regclass${args.join("")});`).join("\n")}
|
|
2071
|
+
`;
|
|
2072
|
+
}
|
|
2073
|
+
//#endregion
|
|
2074
|
+
//#region src/cli/rls/generate-sql.ts
|
|
2075
|
+
/** The checks that need the SQL context: `--backfill-out`, `rls.anonymousSignIns`, `rls.jsonSchema`. */
|
|
2076
|
+
function preflight(input, { ctx }) {
|
|
2077
|
+
const rls = input.config.rls;
|
|
2078
|
+
const from = typeof rls?.customRoles === "object" ? rls.customRoles.from : void 0;
|
|
2079
|
+
if (input.backfillOut !== void 0 && from === void 0) return { refusal: "rls generate --backfill-out needs rls.customRoles.from in permdock.config.ts" };
|
|
2080
|
+
let backfill;
|
|
2081
|
+
if (from !== void 0) try {
|
|
2082
|
+
backfill = backfillSql(ctx, from, rls?.migrate);
|
|
1676
2083
|
} catch (cause) {
|
|
1677
|
-
return {
|
|
1678
|
-
code: 2,
|
|
1679
|
-
output: describeError(cause),
|
|
1680
|
-
text: ""
|
|
1681
|
-
};
|
|
2084
|
+
return { refusal: describeError(cause) };
|
|
1682
2085
|
}
|
|
1683
|
-
if (
|
|
1684
|
-
const
|
|
1685
|
-
|
|
1686
|
-
|
|
2086
|
+
if (ctx.anonymousSignIns === "deny" && ctx.dialect !== "supabase") return { refusal: `rls.anonymousSignIns needs --dialect supabase (got ${ctx.dialect})` };
|
|
2087
|
+
const jsonSchema = rls?.jsonSchema;
|
|
2088
|
+
if (jsonSchema !== void 0 && jsonSchema !== "auto" && typeof jsonSchema !== "boolean") return { refusal: `rls.jsonSchema must be 'auto', true or false (got ${JSON.stringify(jsonSchema)})` };
|
|
2089
|
+
return {
|
|
2090
|
+
backfill,
|
|
2091
|
+
jsonSchema
|
|
2092
|
+
};
|
|
2093
|
+
}
|
|
2094
|
+
function rbacPart(input, policy, { ctx, schema, authorize }, warnings) {
|
|
1687
2095
|
const rootMapped = ctx.scopes[0] === void 0 ? void 0 : scopeTable(ctx, ctx.scopes[0].name);
|
|
1688
2096
|
const rootTable = rootMapped === void 0 ? void 0 : {
|
|
1689
2097
|
...rootMapped.table,
|
|
@@ -1702,51 +2110,66 @@ async function runRlsGenerate(input) {
|
|
|
1702
2110
|
}) : void 0;
|
|
1703
2111
|
if (rbac !== void 0) warnings.push(...rbac.warnings);
|
|
1704
2112
|
if (input.rbac) warnings.push("the token hook that writes user_role and memberships comes from permdock supabase hook generate");
|
|
2113
|
+
return rbac;
|
|
2114
|
+
}
|
|
2115
|
+
/** The ownership, approval store, graph, row-helper and break-glass sections. */
|
|
2116
|
+
function sectionsSql(input, policy, options, built, generated, jsonSchema, warnings) {
|
|
2117
|
+
const rls = input.config.rls;
|
|
2118
|
+
const { ctx, graph, ownership } = built;
|
|
2119
|
+
const { compiled } = generated;
|
|
1705
2120
|
const owned = [ownershipSql(ctx), assignmentSql(ctx)].filter((part) => part !== "").join("\n");
|
|
1706
2121
|
if (ctx.assignments !== void 0 && (ownership === void 0 || ownership.assigns.length === 0)) warnings.push("rls.assignments needs a role that declares assigns: no assignment trigger is written");
|
|
1707
2122
|
let approvals;
|
|
1708
2123
|
try {
|
|
1709
2124
|
approvals = rls?.approvals === true ? approvalStoreSql(ctx, jsonSchema ?? false) : typeof rls?.approvals === "object" ? adoptedApprovalStoreSql(ctx, rls.approvals, jsonSchema ?? false) : void 0;
|
|
1710
2125
|
} catch (cause) {
|
|
1711
|
-
return {
|
|
1712
|
-
code: 2,
|
|
1713
|
-
output: describeError(cause),
|
|
1714
|
-
text: ""
|
|
1715
|
-
};
|
|
2126
|
+
return { refusal: describeError(cause) };
|
|
1716
2127
|
}
|
|
1717
2128
|
const graphed = graphSql(ctx, graph, rls?.tables);
|
|
1718
2129
|
let rowHelpers = "";
|
|
1719
2130
|
if (rls?.rowHelpers !== void 0 || inheritTargets(policy).length > 0) try {
|
|
1720
|
-
rowHelpers = rowHelpersSql(ctx, policy, [...fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], rls?.tables, warnings);
|
|
2131
|
+
rowHelpers = rowHelpersSql(ctx, policy, [...options.fieldsMode === void 0 ? compiled.branches : rowBranches(compiled.branches), ...compiled.actionBranches], rls?.rowHelpers ?? [], rls?.tables, warnings);
|
|
1721
2132
|
} catch (cause) {
|
|
1722
|
-
return {
|
|
1723
|
-
code: 2,
|
|
1724
|
-
output: describeError(cause),
|
|
1725
|
-
text: ""
|
|
1726
|
-
};
|
|
2133
|
+
return { refusal: describeError(cause) };
|
|
1727
2134
|
}
|
|
1728
2135
|
const breakGlass = breakGlassSql(ctx, breakGlassEntries(policy, rls?.tables));
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
2136
|
+
return [
|
|
2137
|
+
approvals,
|
|
2138
|
+
owned === "" ? void 0 : owned,
|
|
2139
|
+
graphed === "" ? void 0 : graphed,
|
|
2140
|
+
breakGlass === "" ? void 0 : breakGlass,
|
|
2141
|
+
rowHelpers === "" ? void 0 : rowHelpers
|
|
2142
|
+
];
|
|
2143
|
+
}
|
|
2144
|
+
/** Whether `anon` calls a helper: `rls.anonExecute`, an anon field view or an anon branch reading the user id. */
|
|
2145
|
+
function anonExecutes(input, { compiled, views }) {
|
|
2146
|
+
return input.config.rls?.anonExecute === true || views.some((view) => view.roles.includes("anon")) || compiled.branches.some((branch) => branch.roles.includes("anon") && [
|
|
1739
2147
|
branch.access,
|
|
1740
2148
|
branch.using,
|
|
1741
2149
|
branch.check
|
|
1742
2150
|
].some((sql) => sql?.includes("permdock_user_id") === true));
|
|
2151
|
+
}
|
|
2152
|
+
/** Everything before the table policies: helpers, sections, shims, scaffold and triggers. */
|
|
2153
|
+
function preambleSql(input, policy, options, built, generated, jsonSchema, warnings) {
|
|
2154
|
+
const rls = input.config.rls;
|
|
2155
|
+
const { ctx } = built;
|
|
2156
|
+
const { compiled } = generated;
|
|
2157
|
+
const rbac = rbacPart(input, policy, built, warnings);
|
|
2158
|
+
const sections = sectionsSql(input, policy, options, built, generated, jsonSchema, warnings);
|
|
2159
|
+
if (isRefusal(sections)) return sections;
|
|
2160
|
+
const configured = rls?.shims;
|
|
2161
|
+
const shimsConfig = typeof configured === "object" ? configured : configured === true || input.shims === true ? {} : void 0;
|
|
2162
|
+
if (shimsConfig !== void 0 && rls?.migrate === void 0) return { refusal: "PermDock CLI: rls generate --shims needs rls.migrate.helpers in permdock.config.ts" };
|
|
2163
|
+
const renamed = Object.fromEntries(renamedKeys(policy.vocabulary.permissions));
|
|
2164
|
+
const grants = shimGrants(compiled.rolePermissions, compiled.conditionedKeys, renamed);
|
|
2165
|
+
const anonExecute = anonExecutes(input, generated);
|
|
1743
2166
|
const usesLiveSession = compiled.branches.some((branch) => [
|
|
1744
2167
|
branch.access,
|
|
1745
2168
|
branch.using,
|
|
1746
2169
|
branch.check
|
|
1747
2170
|
].some((sql) => sql?.includes(SESSION_LIVE_HELPER) === true));
|
|
1748
2171
|
const shims = shimsConfig === void 0 || rls?.migrate === void 0 ? void 0 : shimsSql(ctx, rls.migrate, shimsConfig, renamed, grants);
|
|
1749
|
-
const
|
|
2172
|
+
const core = [
|
|
1750
2173
|
rbac?.head,
|
|
1751
2174
|
helpersSql(ctx, compiled.rolePermissions, {
|
|
1752
2175
|
levelReach: compiled.levelReach,
|
|
@@ -1758,18 +2181,56 @@ async function runRlsGenerate(input) {
|
|
|
1758
2181
|
}),
|
|
1759
2182
|
permissionHelpersSql(ctx, grants, renamed, anonExecute),
|
|
1760
2183
|
usesLiveSession ? sessionLiveHelperSql(ctx) : void 0,
|
|
1761
|
-
|
|
1762
|
-
owned === "" ? void 0 : owned,
|
|
1763
|
-
graphed === "" ? void 0 : graphed,
|
|
1764
|
-
breakGlass === "" ? void 0 : breakGlass,
|
|
1765
|
-
rowHelpers === "" ? void 0 : rowHelpers,
|
|
2184
|
+
...sections,
|
|
1766
2185
|
shims,
|
|
1767
2186
|
rbac?.tail
|
|
1768
2187
|
].filter((part) => part !== void 0).join("\n");
|
|
1769
|
-
|
|
2188
|
+
const created = generatedTables(core, built.schema);
|
|
2189
|
+
let ownTriggers;
|
|
2190
|
+
let audit;
|
|
2191
|
+
try {
|
|
2192
|
+
ownTriggers = triggersSql(rls?.triggers, built.schema, created);
|
|
2193
|
+
audit = auditSql(rls?.audit, built.schema, created);
|
|
2194
|
+
} catch (cause) {
|
|
2195
|
+
return { refusal: describeError(cause) };
|
|
2196
|
+
}
|
|
2197
|
+
const seedsSplit = splitsPart(input.split, "seeds");
|
|
2198
|
+
return {
|
|
2199
|
+
preamble: [
|
|
2200
|
+
core,
|
|
2201
|
+
ownTriggers,
|
|
2202
|
+
seedsSplit ? "" : audit
|
|
2203
|
+
].filter((part) => part !== "").join("\n"),
|
|
2204
|
+
audit
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
function forceWarnings({ graph }, warnings) {
|
|
2208
|
+
for (const entry of graph.values()) {
|
|
1770
2209
|
const own = [...entry.relations].filter((name) => !("edge" in (entry.node.relations[name] ?? {})));
|
|
1771
2210
|
if (own.length > 0) warnings.push(`--force: ${entry.node.name} relations ${own.join(", ")} are read from the ${entry.node.name} table itself, so ${graphHelper(entry.node.name)} would recurse through its own policy; keep ${entry.node.name} unforced or move them to edge tables`);
|
|
1772
2211
|
}
|
|
2212
|
+
}
|
|
2213
|
+
/**
|
|
2214
|
+
* The row columns that belong to a table. With `rls.tables` set, a resource
|
|
2215
|
+
* it does not name is a table only when generated policies are written for
|
|
2216
|
+
* it; with `--helpers-only` it has none, so it gets no index.
|
|
2217
|
+
*/
|
|
2218
|
+
function tableColumns(rowColumns, tables, helpersOnly, warnings) {
|
|
2219
|
+
if (tables === void 0) return rowColumns;
|
|
2220
|
+
const unmapped = [...new Set(rowColumns.map((item) => item.resource).filter((resource) => !Object.hasOwn(tables, resource)))].toSorted();
|
|
2221
|
+
if (unmapped.length === 0) return rowColumns;
|
|
2222
|
+
if (helpersOnly) {
|
|
2223
|
+
warnings.push(`no index is suggested for resources without an rls.tables entry: ${unmapped.join(", ")}; map each one that is a table`);
|
|
2224
|
+
return rowColumns.filter((item) => Object.hasOwn(tables, item.resource));
|
|
2225
|
+
}
|
|
2226
|
+
warnings.push(`policies for resources without an rls.tables entry target a table named after the resource: ${unmapped.map((resource) => qualifySqlTable(resource)).join(", ")}; map each one to its table`);
|
|
2227
|
+
return rowColumns;
|
|
2228
|
+
}
|
|
2229
|
+
/** The `--target` file, and for Drizzle and Prisma the SQL migration beside it. */
|
|
2230
|
+
function emitTarget(input, built, generated, preamble) {
|
|
2231
|
+
const rls = input.config.rls;
|
|
2232
|
+
const { ctx } = built;
|
|
2233
|
+
const { policies, force, views, helperTables } = generated;
|
|
1773
2234
|
const outRel = input.out ?? rls?.out ?? defaultOut(input.target);
|
|
1774
2235
|
const migration = input.target === "sql" ? "" : dialectRoles(migrationSql(policies, withHelperTables(preamble, helperTables), force, views), ctx.dialect);
|
|
1775
2236
|
const migrationRel = migration === "" ? void 0 : migrationOut(outRel);
|
|
@@ -1795,14 +2256,77 @@ async function runRlsGenerate(input) {
|
|
|
1795
2256
|
break;
|
|
1796
2257
|
default: return input.target;
|
|
1797
2258
|
}
|
|
1798
|
-
|
|
2259
|
+
return {
|
|
2260
|
+
text,
|
|
2261
|
+
migration,
|
|
2262
|
+
migrationRel,
|
|
2263
|
+
outRel
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
/** The generated SQL for one run, before `--db` prunes the index targets. */
|
|
2267
|
+
function generateSql(input, policy, options, built, warnings) {
|
|
2268
|
+
const rls = input.config.rls;
|
|
2269
|
+
const { ctx } = built;
|
|
2270
|
+
const checked = preflight(input, built);
|
|
2271
|
+
if (isRefusal(checked)) return checked;
|
|
2272
|
+
const { jsonSchema } = checked;
|
|
2273
|
+
if (jsonSchema !== void 0 && jsonSchema !== false && (rls?.approvals === void 0 || rls.approvals === false)) warnings.push("rls.jsonSchema constrains the approval store's body, which needs rls.approvals: true; nothing else is jsonb");
|
|
2274
|
+
const generated = generatePolicies(input, policy, options, built, warnings);
|
|
2275
|
+
if (isRefusal(generated)) return generated;
|
|
2276
|
+
const assembled = preambleSql(input, policy, options, built, generated, jsonSchema, warnings);
|
|
2277
|
+
if (isRefusal(assembled)) return assembled;
|
|
2278
|
+
const { preamble, audit } = assembled;
|
|
2279
|
+
if (generated.force) forceWarnings(built, warnings);
|
|
2280
|
+
const emitted = emitTarget(input, built, generated, preamble);
|
|
2281
|
+
const { policies, force, views, helperTables, guardedTables, helpersOnly } = generated;
|
|
2282
|
+
const seedsSplit = splitsPart(input.split, "seeds");
|
|
2283
|
+
return {
|
|
2284
|
+
...generated,
|
|
2285
|
+
...emitted,
|
|
2286
|
+
backfill: checked.backfill,
|
|
2287
|
+
indexes: indexTargets(ctx, tableColumns(generated.compiled.rowColumns, rls?.tables, helpersOnly, warnings)),
|
|
2288
|
+
parts: (indexes) => ({
|
|
2289
|
+
policies: dialectRoles(withHelperTables(emitSql(policies, "", force, views), helperTables), ctx.dialect),
|
|
2290
|
+
declarativePolicies: dialectRoles(emitSql(policies, "", force, views), ctx.dialect),
|
|
2291
|
+
...guardedTables === void 0 ? {} : { servicePolicies: dialectRoles(guardedTables, ctx.dialect) },
|
|
2292
|
+
helpers: dialectRoles(emitSql([], preamble, false, helpersOnly ? views : []), ctx.dialect),
|
|
2293
|
+
seeds: `${SEEDS_MARKER} schema=${built.schema}\n${seedSql(ctx, generated.compiled.rolePermissions)}\n${seedsSplit ? audit : ""}`,
|
|
2294
|
+
indexes: `${INDEXES_MARKER}\n${indexesSql(indexes)}\n`
|
|
2295
|
+
})
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
//#endregion
|
|
2299
|
+
//#region src/cli/rls/generate.ts
|
|
2300
|
+
function loadPolicy(cwd, config, from) {
|
|
2301
|
+
const policyPath = from === "drizzle" ? config.policy : from ?? config.policy;
|
|
2302
|
+
return requirePolicy(cwd, policyPath, "rls generate needs policy in permdock.config.ts or --from");
|
|
2303
|
+
}
|
|
2304
|
+
function refused(output) {
|
|
2305
|
+
return {
|
|
2306
|
+
code: 2,
|
|
2307
|
+
output,
|
|
2308
|
+
text: ""
|
|
2309
|
+
};
|
|
2310
|
+
}
|
|
2311
|
+
async function runRlsGenerate(input) {
|
|
2312
|
+
const options = generateOptions(input);
|
|
2313
|
+
if (isRefusal(options)) return refused(options.refusal);
|
|
2314
|
+
const policy = await loadPolicy(input.cwd, input.config, input.from);
|
|
2315
|
+
const built = buildContext(input, policy, options);
|
|
2316
|
+
if (isRefusal(built)) return refused(built.refusal);
|
|
2317
|
+
const warnings = [];
|
|
2318
|
+
const generated = generateSql(input, policy, options, built, warnings);
|
|
2319
|
+
if (isRefusal(generated)) return refused(generated.refusal);
|
|
2320
|
+
const { ctx } = built;
|
|
2321
|
+
const { text, migration, migrationRel, helpersOnly } = generated;
|
|
2322
|
+
let indexes = generated.indexes;
|
|
1799
2323
|
if (input.db !== void 0) try {
|
|
1800
2324
|
const pruned = pruneIndexTargets(indexes, await readTableIndexFacts(input.db, [...new Set(indexes.map((target) => target.table))], input.connect));
|
|
1801
2325
|
indexes = pruned.targets;
|
|
1802
2326
|
warnings.push(...pruned.warnings);
|
|
1803
2327
|
} catch (cause) {
|
|
1804
2328
|
return {
|
|
1805
|
-
code:
|
|
2329
|
+
code: exitCodeOf(cliErrorKind(cause)),
|
|
1806
2330
|
output: describeError(cause),
|
|
1807
2331
|
text: ""
|
|
1808
2332
|
};
|
|
@@ -1813,33 +2337,26 @@ async function runRlsGenerate(input) {
|
|
|
1813
2337
|
code: 0,
|
|
1814
2338
|
output: warnings.join("\n"),
|
|
1815
2339
|
text,
|
|
1816
|
-
policies,
|
|
1817
|
-
seeds: compiled.rolePermissions,
|
|
2340
|
+
policies: generated.policies,
|
|
2341
|
+
seeds: generated.compiled.rolePermissions,
|
|
1818
2342
|
indexes,
|
|
1819
2343
|
keys: {
|
|
1820
2344
|
permissions: listPermissions(policy.vocabulary.permissions).map((leaf) => leaf.key),
|
|
1821
2345
|
rowConditions: [...policyRowConditionKeys(policy)],
|
|
1822
2346
|
renamed: Object.fromEntries(renamedKeys(policy.vocabulary.permissions))
|
|
1823
2347
|
},
|
|
1824
|
-
schema,
|
|
2348
|
+
schema: built.schema,
|
|
1825
2349
|
helpersOnly,
|
|
1826
2350
|
...extras
|
|
1827
2351
|
};
|
|
1828
2352
|
const planned = outputFiles({
|
|
1829
2353
|
input,
|
|
1830
|
-
outRel,
|
|
2354
|
+
outRel: generated.outRel,
|
|
1831
2355
|
text,
|
|
1832
2356
|
helpersOnly,
|
|
1833
|
-
sql: () => (
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
...guardedTables === void 0 ? {} : { servicePolicies: dialectRoles(guardedTables, ctx.dialect) },
|
|
1837
|
-
helpers: dialectRoles(emitSql([], preamble, false, helpersOnly ? views : []), ctx.dialect),
|
|
1838
|
-
seeds: `${SEEDS_MARKER} schema=${schema}\n${seedSql(ctx, compiled.rolePermissions)}\n`,
|
|
1839
|
-
indexes: `${INDEXES_MARKER}\n${indexesSql(indexes)}\n`
|
|
1840
|
-
}),
|
|
1841
|
-
scopes,
|
|
1842
|
-
schema
|
|
2357
|
+
sql: () => generated.parts(indexes),
|
|
2358
|
+
scopes: ctx.scopes,
|
|
2359
|
+
schema: built.schema
|
|
1843
2360
|
});
|
|
1844
2361
|
if (typeof planned === "string") return {
|
|
1845
2362
|
code: 2,
|
|
@@ -1853,6 +2370,18 @@ async function runRlsGenerate(input) {
|
|
|
1853
2370
|
rel: migrationRel,
|
|
1854
2371
|
text: migration
|
|
1855
2372
|
});
|
|
2373
|
+
if (input.backfillOut !== void 0 && generated.backfill !== void 0) {
|
|
2374
|
+
const file = dataMigrationFile({
|
|
2375
|
+
cwd: input.cwd,
|
|
2376
|
+
out: input.backfillOut,
|
|
2377
|
+
part: "backfill",
|
|
2378
|
+
marker: BACKFILL_MARKER,
|
|
2379
|
+
text: generated.backfill,
|
|
2380
|
+
now: input.io.now?.() ?? /* @__PURE__ */ new Date(),
|
|
2381
|
+
check: input.check
|
|
2382
|
+
});
|
|
2383
|
+
if (file !== void 0) files.push(file);
|
|
2384
|
+
}
|
|
1856
2385
|
if (input.check) {
|
|
1857
2386
|
const drift = driftOf(input.cwd, files);
|
|
1858
2387
|
return drift.length === 0 ? {
|
|
@@ -1867,7 +2396,7 @@ async function runRlsGenerate(input) {
|
|
|
1867
2396
|
...extras
|
|
1868
2397
|
};
|
|
1869
2398
|
}
|
|
1870
|
-
const written =
|
|
2399
|
+
const written = writeOutputFiles(input.cwd, files);
|
|
1871
2400
|
const extra = warnings.length === 0 ? "" : `\n${warnings.join("\n")}`;
|
|
1872
2401
|
return {
|
|
1873
2402
|
code: 0,
|
|
@@ -1876,141 +2405,8 @@ async function runRlsGenerate(input) {
|
|
|
1876
2405
|
...extras
|
|
1877
2406
|
};
|
|
1878
2407
|
}
|
|
1879
|
-
/** Whether `--split` names `part`, which then leaves the helpers or the warnings. */
|
|
1880
|
-
const SERVICE_TABLES_HEADER = "-- Supabase Realtime and Storage: rls.realtime and rls.storage. RLS on both tables is Supabase's.";
|
|
1881
|
-
/**
|
|
1882
|
-
* A `realtime.messages` or `storage.objects` policy that applies only where
|
|
1883
|
-
* the table exists. The Realtime and Storage services create those tables, so
|
|
1884
|
-
* a local stack with the service off, and pg-delta's shadow database, have
|
|
1885
|
-
* none.
|
|
1886
|
-
*/
|
|
1887
|
-
function guardedPolicySql(item) {
|
|
1888
|
-
const body = policySql(item).trimEnd().split("\n").map((line) => ` ${line}`).join("\n");
|
|
1889
|
-
return `do $permdock$
|
|
1890
|
-
begin
|
|
1891
|
-
if to_regclass(${quoteLiteral(qualifiedTable$1(item.table))}) is not null then
|
|
1892
|
-
${body}
|
|
1893
|
-
end if;
|
|
1894
|
-
end
|
|
1895
|
-
$permdock$;
|
|
1896
|
-
`;
|
|
1897
|
-
}
|
|
1898
|
-
/** Appends the `realtime.messages` and `storage.objects` policies, which never get `tableSql`. */
|
|
1899
|
-
function withHelperTables(sql, helperTables) {
|
|
1900
|
-
if (helperTables === void 0) return sql;
|
|
1901
|
-
const tables = `${helperTables.trimEnd()}\n`;
|
|
1902
|
-
return sql.trim() === "" ? tables : `${sql.trimEnd()}\n\n${tables}`;
|
|
1903
|
-
}
|
|
1904
|
-
function splitsPart(raw, part) {
|
|
1905
|
-
const split = parseSplit(raw);
|
|
1906
|
-
return typeof split !== "string" && split?.includes(part) === true;
|
|
1907
|
-
}
|
|
1908
|
-
/** The files `generate` writes: one, or one per `--split` part, plus the hook's grants. */
|
|
1909
|
-
function outputFiles(plan) {
|
|
1910
|
-
const { input } = plan;
|
|
1911
|
-
const split = parseSplit(input.split);
|
|
1912
|
-
if (typeof split === "string") return split;
|
|
1913
|
-
if (split?.includes("policies") === true && plan.helpersOnly) return "rls generate --helpers-only writes no policies part; drop it from --split";
|
|
1914
|
-
if (split === void 0) {
|
|
1915
|
-
if (input.grantsOut !== void 0) return "rls generate --grants-out needs --split with the helpers or hook part";
|
|
1916
|
-
if (input.seedsOut !== void 0) return "rls generate --seeds-out needs --split with the seeds part";
|
|
1917
|
-
return [{
|
|
1918
|
-
part: "rls",
|
|
1919
|
-
rel: plan.outRel,
|
|
1920
|
-
text: plan.text
|
|
1921
|
-
}];
|
|
1922
|
-
}
|
|
1923
|
-
if (input.target !== "sql") return "rls generate --split needs --target sql";
|
|
1924
|
-
const pgDelta = input.out === void 0 && input.config.rls?.out === void 0 ? supabaseConfig(input.cwd).pgDelta : void 0;
|
|
1925
|
-
if (pgDelta === void 0 && !plan.outRel.includes("{part}")) return `rls generate --split needs {part} in --out, for example supabase/schemas/identity/056_permdock_{part}.sql (got ${plan.outRel})`;
|
|
1926
|
-
if (pgDelta !== void 0 && split.includes("helpers") && !split.includes("seeds")) return "rls generate --split helpers under pg-delta needs the seeds part with --seeds-out: pg-delta rejects the role_permissions rows in a declarative file";
|
|
1927
|
-
if (pgDelta !== void 0 && split.includes("seeds") && input.seedsOut === void 0) return "rls generate --split seeds under pg-delta needs --seeds-out: pg-delta does not diff the role_permissions rows, so they go in a migration";
|
|
1928
|
-
const at = (part, schema = plan.schema) => pgDelta === void 0 ? partPath(plan.outRel, part) : pgDeltaPath(pgDelta.schemaDir, part, schema);
|
|
1929
|
-
if (input.grantsOut !== void 0 && !split.includes("hook") && !split.includes("helpers")) return "rls generate --grants-out needs the helpers or hook part in --split";
|
|
1930
|
-
if (input.seedsOut !== void 0 && !split.includes("seeds")) return "rls generate --seeds-out needs the seeds part in --split";
|
|
1931
|
-
if (split.includes("hook") && input.config.supabase?.hook === void 0) return "rls generate --split hook needs supabase.hook in permdock.config.ts";
|
|
1932
|
-
const sql = plan.sql();
|
|
1933
|
-
const relocated = pgDelta === void 0 ? void 0 : sql.servicePolicies;
|
|
1934
|
-
if (relocated !== void 0 && split.includes("policies") && !split.includes("seeds")) return "rls generate --split policies under pg-delta with rls.realtime or rls.storage needs the seeds part with --seeds-out: the shadow database has no realtime.messages or storage.objects, so their policies go in that migration";
|
|
1935
|
-
const seeds = relocated === void 0 || !split.includes("policies") ? sql.seeds : `${sql.seeds}\n${relocated}`;
|
|
1936
|
-
const files = [];
|
|
1937
|
-
const label = grantsLabel(input.grantsOut);
|
|
1938
|
-
let grantsMarker = `${GRANTS_MARKER} schema=${plan.schema}`;
|
|
1939
|
-
const grants = [];
|
|
1940
|
-
for (const part of split) switch (part) {
|
|
1941
|
-
case "helpers": {
|
|
1942
|
-
if (label === void 0) {
|
|
1943
|
-
files.push({
|
|
1944
|
-
part,
|
|
1945
|
-
rel: at(part),
|
|
1946
|
-
text: sql.helpers
|
|
1947
|
-
});
|
|
1948
|
-
break;
|
|
1949
|
-
}
|
|
1950
|
-
const moved = moveUndiffed(sql.helpers, label, part);
|
|
1951
|
-
files.push({
|
|
1952
|
-
part,
|
|
1953
|
-
rel: at(part),
|
|
1954
|
-
text: moved.sql
|
|
1955
|
-
});
|
|
1956
|
-
grants.push(moved.grants);
|
|
1957
|
-
break;
|
|
1958
|
-
}
|
|
1959
|
-
case "seeds": {
|
|
1960
|
-
const out = input.seedsOut;
|
|
1961
|
-
if (out !== void 0 && isSeedsDirectory(input.cwd, out)) {
|
|
1962
|
-
const planned = seedsMigration(input.cwd, out, SEEDS_MARKER, seeds, input.io.now?.() ?? /* @__PURE__ */ new Date());
|
|
1963
|
-
if (!planned.current || input.check) files.push({
|
|
1964
|
-
part,
|
|
1965
|
-
rel: planned.rel,
|
|
1966
|
-
text: seeds
|
|
1967
|
-
});
|
|
1968
|
-
break;
|
|
1969
|
-
}
|
|
1970
|
-
files.push({
|
|
1971
|
-
part,
|
|
1972
|
-
rel: out ?? at(part),
|
|
1973
|
-
text: seeds
|
|
1974
|
-
});
|
|
1975
|
-
break;
|
|
1976
|
-
}
|
|
1977
|
-
case "indexes":
|
|
1978
|
-
files.push({
|
|
1979
|
-
part,
|
|
1980
|
-
rel: at(part),
|
|
1981
|
-
text: sql.indexes
|
|
1982
|
-
});
|
|
1983
|
-
break;
|
|
1984
|
-
case "policies":
|
|
1985
|
-
files.push({
|
|
1986
|
-
part,
|
|
1987
|
-
rel: at(part),
|
|
1988
|
-
text: pgDelta === void 0 ? sql.policies : sql.declarativePolicies
|
|
1989
|
-
});
|
|
1990
|
-
break;
|
|
1991
|
-
case "hook": {
|
|
1992
|
-
const hook = supabaseHookSql(plan.scopes, input.config, {}, label);
|
|
1993
|
-
files.push({
|
|
1994
|
-
part,
|
|
1995
|
-
rel: at(part, hook.manifest.hook.schema),
|
|
1996
|
-
text: hook.sql
|
|
1997
|
-
});
|
|
1998
|
-
const [marker = grantsMarker, ...rest] = hook.grants.split("\n");
|
|
1999
|
-
grantsMarker = marker;
|
|
2000
|
-
grants.push(rest.join("\n").trimEnd());
|
|
2001
|
-
break;
|
|
2002
|
-
}
|
|
2003
|
-
default: return part;
|
|
2004
|
-
}
|
|
2005
|
-
if (input.grantsOut !== void 0) files.push({
|
|
2006
|
-
part: "grants",
|
|
2007
|
-
rel: input.grantsOut,
|
|
2008
|
-
text: `${[grantsMarker, ...grants].join("\n")}\n`
|
|
2009
|
-
});
|
|
2010
|
-
return files;
|
|
2011
|
-
}
|
|
2012
2408
|
//#endregion
|
|
2013
|
-
//#region src/cli/rls
|
|
2409
|
+
//#region src/cli/rls/introspect.ts
|
|
2014
2410
|
const ROLES = ["anon", "authenticated"];
|
|
2015
2411
|
/** Neon's `anonymous` is the role `rls generate` writes as `anon` for the other dialects. */
|
|
2016
2412
|
function liveRole(role) {
|
|
@@ -2023,7 +2419,7 @@ const PRIVILEGES = [
|
|
|
2023
2419
|
"delete"
|
|
2024
2420
|
];
|
|
2025
2421
|
/** `posts` and `"app"."posts"` as `public.posts` and `app.posts`. */
|
|
2026
|
-
function
|
|
2422
|
+
function normalizeTableName(table) {
|
|
2027
2423
|
const parts = table.split(".").map((part) => part.replaceAll("\"", ""));
|
|
2028
2424
|
return parts.length === 1 ? `public.${parts[0] ?? ""}` : parts.join(".");
|
|
2029
2425
|
}
|
|
@@ -2037,19 +2433,19 @@ function helpersOf(sql) {
|
|
|
2037
2433
|
return [...found];
|
|
2038
2434
|
}
|
|
2039
2435
|
function expectedRls(policies, sql, indexes = []) {
|
|
2040
|
-
const tables = [...new Set(policies.map((item) =>
|
|
2436
|
+
const tables = [...new Set(policies.map((item) => normalizeTableName(item.table)))];
|
|
2041
2437
|
const grants = {};
|
|
2042
2438
|
for (const table of tables) {
|
|
2043
2439
|
const byRole = {};
|
|
2044
2440
|
for (const role of ROLES) {
|
|
2045
|
-
const commands = new Set(policies.filter((item) =>
|
|
2441
|
+
const commands = new Set(policies.filter((item) => normalizeTableName(item.table) === table && item.effect === "allow" && item.roles.includes(role)).map((item) => item.command));
|
|
2046
2442
|
byRole[role] = PRIVILEGES.filter((privilege) => commands.has(privilege));
|
|
2047
2443
|
}
|
|
2048
2444
|
grants[table] = byRole;
|
|
2049
2445
|
}
|
|
2050
2446
|
return {
|
|
2051
2447
|
policies: policies.map((item) => ({
|
|
2052
|
-
table:
|
|
2448
|
+
table: normalizeTableName(item.table),
|
|
2053
2449
|
name: item.name,
|
|
2054
2450
|
command: item.command,
|
|
2055
2451
|
permissive: item.effect === "allow",
|
|
@@ -2060,7 +2456,7 @@ function expectedRls(policies, sql, indexes = []) {
|
|
|
2060
2456
|
helpers: helpersOf(sql),
|
|
2061
2457
|
indexes: indexes.map((target) => ({
|
|
2062
2458
|
...target,
|
|
2063
|
-
table:
|
|
2459
|
+
table: normalizeTableName(target.table)
|
|
2064
2460
|
}))
|
|
2065
2461
|
};
|
|
2066
2462
|
}
|
|
@@ -2278,7 +2674,7 @@ async function introspectMixed(db, schema, connect = connectIntrospect) {
|
|
|
2278
2674
|
}
|
|
2279
2675
|
}
|
|
2280
2676
|
//#endregion
|
|
2281
|
-
//#region src/cli/rls.ts
|
|
2677
|
+
//#region src/cli/rls/rls.ts
|
|
2282
2678
|
const RLS_HELP = `permdock rls generate | import | verify | migrate
|
|
2283
2679
|
|
|
2284
2680
|
generate --target drizzle|sql|prisma --dialect supabase|neon|guc
|
|
@@ -2288,15 +2684,16 @@ const RLS_HELP = `permdock rls generate | import | verify | migrate
|
|
|
2288
2684
|
[--capabilities] [--fields views [--revoke-columns]]
|
|
2289
2685
|
[--out <path>] [--check] [--skip-closures] [--inline-functions] [--force] [--guc-prefix app]
|
|
2290
2686
|
[--split helpers,seeds,policies,hook --out <dir>/056_permdock_{part}.sql]
|
|
2291
|
-
[--grants-out <file>|-] [--seeds-out <file>|-]
|
|
2687
|
+
[--grants-out <file>|-] [--seeds-out <file>|-] [--backfill-out <file>|<dir>/|-]
|
|
2292
2688
|
[--helpers-only] [--shims] [--db $DATABASE_URL]
|
|
2293
2689
|
import --sql schema.sql | --db $DATABASE_URL --out src/permissions.generated.ts
|
|
2294
2690
|
[--schema zod|valibot|arktype] [--memberships <table>:tenant,user,role]
|
|
2295
2691
|
verify [--db $DATABASE_URL] [--fixtures rls.fixtures.ts] [--format pgtap|node] [--tree]
|
|
2296
2692
|
[--introspect --db $DATABASE_URL, with the generate flags]
|
|
2297
2693
|
[--advisors [--db $DATABASE_URL] [--json]]: supabase db advisors, security lints
|
|
2298
|
-
migrate --sql <dir> [--write] [--json], with the generate flags
|
|
2299
|
-
rewrites the rls.migrate helpers' calls in policies onto the generated helpers
|
|
2694
|
+
migrate --sql <dir> [--write] [--json] [--retire-out <file>|<dir>/|-], with the generate flags
|
|
2695
|
+
rewrites the rls.migrate helpers' calls in policies onto the generated helpers;
|
|
2696
|
+
--retire-out drops each rls.migrate.tables table and its refresh triggers once unread
|
|
2300
2697
|
|
|
2301
2698
|
Never emits service_role, except in the execute grant rls.trustedReaders asks for. memberOf compiles through the dialect memberships mapping.
|
|
2302
2699
|
`;
|
|
@@ -2334,6 +2731,7 @@ function generateInput(input, target, dialect) {
|
|
|
2334
2731
|
...input.split === void 0 ? {} : { split: input.split },
|
|
2335
2732
|
...input.grantsOut === void 0 ? {} : { grantsOut: input.grantsOut },
|
|
2336
2733
|
...input.seedsOut === void 0 ? {} : { seedsOut: input.seedsOut },
|
|
2734
|
+
...input.backfillOut === void 0 ? {} : { backfillOut: input.backfillOut },
|
|
2337
2735
|
helpersOnly: input.helpersOnly,
|
|
2338
2736
|
shims: input.shims === true
|
|
2339
2737
|
};
|
|
@@ -2363,14 +2761,16 @@ async function migrate(input) {
|
|
|
2363
2761
|
output: generated.output
|
|
2364
2762
|
};
|
|
2365
2763
|
await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls migrate", input.cwd);
|
|
2366
|
-
const { migrateTarget, runRlsMigrate } = await import("./
|
|
2764
|
+
const { migrateTarget, runRlsMigrate } = await import("./migrate-BwlB5-Wk.js");
|
|
2367
2765
|
return runRlsMigrate({
|
|
2368
2766
|
cwd: input.cwd,
|
|
2369
2767
|
sql: input.sql,
|
|
2370
2768
|
config,
|
|
2371
2769
|
target: migrateTarget(generated),
|
|
2372
2770
|
write: input.write,
|
|
2373
|
-
json: input.json
|
|
2771
|
+
json: input.json,
|
|
2772
|
+
retireOut: input.retireOut,
|
|
2773
|
+
...input.io.now === void 0 ? {} : { now: input.io.now() }
|
|
2374
2774
|
});
|
|
2375
2775
|
}
|
|
2376
2776
|
/** `verify --introspect` with `--helpers-only`: helpers and seeds exactly, hand-written policies by their keys. */
|
|
@@ -2471,7 +2871,7 @@ async function runRls(input) {
|
|
|
2471
2871
|
}
|
|
2472
2872
|
case "import": {
|
|
2473
2873
|
await requirePeer(() => import("pgsql-parser"), "pgsql-parser", "permdock rls import", input.cwd);
|
|
2474
|
-
const { runRlsImport } = await import("./
|
|
2874
|
+
const { runRlsImport } = await import("./import-BFIlhetD.js");
|
|
2475
2875
|
return runRlsImport({
|
|
2476
2876
|
cwd: input.cwd,
|
|
2477
2877
|
config: input.config,
|