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
|
@@ -0,0 +1,1016 @@
|
|
|
1
|
+
import { a as sole } from "./compact-CAf557ZB.js";
|
|
2
|
+
import { a as hasConditionOp, c as isConditionRef } from "./refs-f3imMjNW.js";
|
|
3
|
+
import { u as requiresStandIn } from "./snapshot-Dc9CHPME.js";
|
|
4
|
+
import { c as listPermissions, h as formerKeys } from "./permission-registry-BsblpB-z.js";
|
|
5
|
+
import { t as canonicalJson } from "./canonical-json-CFKyFOke.js";
|
|
6
|
+
import { p as scopeField } from "./tenancy-DshjqY0n.js";
|
|
7
|
+
import { u as policyLevels } from "./hosted-CBHgkzDl.js";
|
|
8
|
+
import { t as requiresApproval } from "./approval-required-TzNB9oQA.js";
|
|
9
|
+
import { _ as leveled, p as andWhere } from "./permdock-L26udHSk.js";
|
|
10
|
+
import { o as jsonSchemaOf } from "./catalog-doc-DWtg0gGR.js";
|
|
11
|
+
import { r as qualifySqlTable } from "./sql-t8teWOfo.js";
|
|
12
|
+
import { F as quoteQualified, I as quoteTable, N as quoteIdent, P as quoteLiteral, T as columnTypesOf, _ as subjectClaimJsonSql, j as helperSchema, s as signedIn, v as subjectClaimSql, w as arrayColumnsOf } from "./shared-1uhI-e-b.js";
|
|
13
|
+
import { A as sqlFunctionNames, E as collectGrants, K as scopeTable, O as compileConditionSql, T as breakGlassKey, X as connectPgPool, a as accessSql, c as capabilityAccessSql, i as requiresSql, k as contextRefs, w as breakGlassHolder, x as apiKeyAllowsCall } from "./permission-keys-B26jeyUv.js";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
//#region src/cli/rls/indexes.ts
|
|
16
|
+
/** Postgres truncates identifiers longer than this, in bytes. */
|
|
17
|
+
const MAX_IDENT = 63;
|
|
18
|
+
/**
|
|
19
|
+
* The row fields `condition` compares with the subject, a claim or a
|
|
20
|
+
* membership (`ownerId: principal.id`, `memberOf`), through `and` and `or`:
|
|
21
|
+
* the selective comparisons an index serves. A comparison with a constant
|
|
22
|
+
* (`status: 'sent'`) narrows rows the scope key already found, a negation
|
|
23
|
+
* scans anyway, and a dotted field is a JSON path a plain index does not serve.
|
|
24
|
+
*/
|
|
25
|
+
function indexedFields(condition) {
|
|
26
|
+
if (condition === void 0) return [];
|
|
27
|
+
switch (condition.op) {
|
|
28
|
+
case "and":
|
|
29
|
+
case "or": return condition.conditions.flatMap(indexedFields);
|
|
30
|
+
case "memberOf": return plainField(condition.field);
|
|
31
|
+
case "eq":
|
|
32
|
+
case "in": return isConditionRef(condition.value) ? plainField(condition.field) : [];
|
|
33
|
+
case "not":
|
|
34
|
+
case "sqlFunction":
|
|
35
|
+
case "opaque":
|
|
36
|
+
case "related":
|
|
37
|
+
case "ne":
|
|
38
|
+
case "gt":
|
|
39
|
+
case "gte":
|
|
40
|
+
case "lt":
|
|
41
|
+
case "lte":
|
|
42
|
+
case "contains":
|
|
43
|
+
case "notIn":
|
|
44
|
+
case "isNull":
|
|
45
|
+
case "liveSession": return [];
|
|
46
|
+
default: return condition;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function plainField(field) {
|
|
50
|
+
return field.includes(".") ? [] : [field];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Every index the generated SQL reads through: the row columns policies
|
|
54
|
+
* filter on (scope keys and selective condition fields), and the membership
|
|
55
|
+
* tables' user column, followed by the scope column the helpers select.
|
|
56
|
+
*/
|
|
57
|
+
function indexTargets(ctx, rowColumns) {
|
|
58
|
+
const targets = /* @__PURE__ */ new Map();
|
|
59
|
+
const add = (table, columns) => {
|
|
60
|
+
const target = {
|
|
61
|
+
table: qualifySqlTable(table),
|
|
62
|
+
columns
|
|
63
|
+
};
|
|
64
|
+
targets.set(`${target.table}\u0000${columns[0] ?? ""}`, target);
|
|
65
|
+
};
|
|
66
|
+
for (const { table, column } of rowColumns) add(table, [column]);
|
|
67
|
+
for (const scope of ctx.scopes) {
|
|
68
|
+
const mapped = scopeTable(ctx, scope.name);
|
|
69
|
+
if (mapped !== void 0) add(mapped.table.table, [mapped.table.user, mapped.column]);
|
|
70
|
+
}
|
|
71
|
+
for (const source of [...ctx.sources ?? [], ...ctx.memberSources ?? []]) {
|
|
72
|
+
add(source.sql.table, [source.sql.user]);
|
|
73
|
+
const users = source.sql.userThrough;
|
|
74
|
+
if (users !== void 0) add(users.table, [users.key]);
|
|
75
|
+
}
|
|
76
|
+
return [...targets.values()].toSorted((a, b) => `${a.table}.${a.columns.join(",")}` < `${b.table}.${b.columns.join(",")}` ? -1 : 1);
|
|
77
|
+
}
|
|
78
|
+
/** A stable index name under Postgres' identifier limit. */
|
|
79
|
+
function indexName(target) {
|
|
80
|
+
const name = `permdock_${target.table.slice(target.table.lastIndexOf(".") + 1)}_${target.columns.join("_")}_idx`;
|
|
81
|
+
if (Buffer.byteLength(name) <= MAX_IDENT) return name;
|
|
82
|
+
return `permdock_${createHash("sha256").update(name).digest("hex").slice(0, 8)}_idx`;
|
|
83
|
+
}
|
|
84
|
+
/** `create index if not exists` for every target, one per line. */
|
|
85
|
+
function indexesSql(targets) {
|
|
86
|
+
return targets.map((target) => `create index if not exists ${quoteIdent(indexName(target))} on ${quoteTable(target.table)} (${target.columns.map(quoteIdent).join(", ")});`).join("\n");
|
|
87
|
+
}
|
|
88
|
+
/** Every live column of the tables in `$1` (`schema.table`), as `target` and `name`. */
|
|
89
|
+
const COLUMNS_SQL = `select n.nspname || '.' || c.relname as target, a.attname as name
|
|
90
|
+
from pg_attribute a
|
|
91
|
+
join pg_class c on c.oid = a.attrelid
|
|
92
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
93
|
+
where a.attnum > 0 and not a.attisdropped
|
|
94
|
+
and n.nspname || '.' || c.relname = any($1::text[])`;
|
|
95
|
+
const INDEX_COLUMNS_SQL = `select n.nspname || '.' || c.relname as target,
|
|
96
|
+
array(
|
|
97
|
+
select a.attname::text
|
|
98
|
+
from unnest(i.indkey) with ordinality k(attnum, ord)
|
|
99
|
+
left join pg_attribute a on a.attrelid = i.indrelid and a.attnum = k.attnum and k.attnum > 0
|
|
100
|
+
where k.ord <= i.indnkeyatts
|
|
101
|
+
order by k.ord
|
|
102
|
+
) as columns
|
|
103
|
+
from pg_index i
|
|
104
|
+
join pg_class c on c.oid = i.indrelid
|
|
105
|
+
join pg_namespace n on n.oid = c.relnamespace
|
|
106
|
+
where i.indpred is null and n.nspname || '.' || c.relname = any($1::text[])`;
|
|
107
|
+
function strings(value) {
|
|
108
|
+
return Array.isArray(value) ? value.map((item) => typeof item === "string" ? item : null) : [];
|
|
109
|
+
}
|
|
110
|
+
/** Reads the columns and index key columns of `tables` (`schema.table`). */
|
|
111
|
+
async function readTableIndexFacts(db, tables, connect = (url) => connectPgPool(url, "permdock rls generate --db")) {
|
|
112
|
+
const client = await connect(db);
|
|
113
|
+
try {
|
|
114
|
+
const [columns, indexes] = await Promise.all([client.query(COLUMNS_SQL, [[...tables]]), client.query(INDEX_COLUMNS_SQL, [[...tables]])]);
|
|
115
|
+
const byTable = {};
|
|
116
|
+
for (const row of columns.rows) (byTable[String(row["target"])] ??= []).push(String(row["name"]));
|
|
117
|
+
const keys = {};
|
|
118
|
+
for (const row of indexes.rows) {
|
|
119
|
+
const list = strings(row["columns"]);
|
|
120
|
+
const end = list.indexOf(null);
|
|
121
|
+
const lead = end === -1 ? list : list.slice(0, end);
|
|
122
|
+
if (lead.length > 0) (keys[String(row["target"])] ??= []).push(lead);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
columns: byTable,
|
|
126
|
+
indexes: keys
|
|
127
|
+
};
|
|
128
|
+
} finally {
|
|
129
|
+
await client.end();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Drops the targets a database already serves or cannot hold: a target on a
|
|
134
|
+
* table or column the database lacks (with a warning), and one an existing
|
|
135
|
+
* index leads with (its key columns start with the target's).
|
|
136
|
+
*/
|
|
137
|
+
function pruneIndexTargets(targets, facts) {
|
|
138
|
+
const warnings = [];
|
|
139
|
+
return {
|
|
140
|
+
targets: targets.filter((target) => {
|
|
141
|
+
const columns = facts.columns[target.table];
|
|
142
|
+
if (columns === void 0) {
|
|
143
|
+
warnings.push(`no index suggested on ${target.table}: the database has no such table`);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
const absent = target.columns.filter((column) => !columns.includes(column));
|
|
147
|
+
if (absent.length > 0) {
|
|
148
|
+
warnings.push(`no index suggested on ${target.table} (${target.columns.join(", ")}): the table has no column ${absent.join(", ")}`);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
return !(facts.indexes[target.table] ?? []).some((index) => target.columns.every((column, at) => index[at] === column));
|
|
152
|
+
}),
|
|
153
|
+
warnings
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
//#endregion
|
|
157
|
+
//#region src/cli/sql/compile.ts
|
|
158
|
+
/**
|
|
159
|
+
* The levels an allow reaches: every level without a row condition, else the
|
|
160
|
+
* levels whose condition equals its condition.
|
|
161
|
+
*/
|
|
162
|
+
function reachedLevels(where, levels) {
|
|
163
|
+
if (where === void 0) return levels.map(([name]) => name);
|
|
164
|
+
const held = canonicalJson(where);
|
|
165
|
+
return levels.filter(([, condition]) => canonicalJson(condition) === held).map(([name]) => name);
|
|
166
|
+
}
|
|
167
|
+
function levelReachOf(policy, keys) {
|
|
168
|
+
const reach = /* @__PURE__ */ new Map();
|
|
169
|
+
for (const [entry, grantKey] of keys) {
|
|
170
|
+
const { item } = entry;
|
|
171
|
+
const { grant } = item;
|
|
172
|
+
if (item.access.kind !== "role" || grant.effect !== "allow" || grant.permission.kind !== "instance" || grant.level !== void 0) continue;
|
|
173
|
+
const levels = policyLevels(policy, grant.permission.resource);
|
|
174
|
+
if (levels.length > 0) {
|
|
175
|
+
const set = reach.get(grantKey) ?? /* @__PURE__ */ new Set();
|
|
176
|
+
for (const name of reachedLevels(grant.where, levels)) set.add(name);
|
|
177
|
+
reach.set(grantKey, set);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const out = [];
|
|
181
|
+
for (const grantKey of new Set(keys.values())) {
|
|
182
|
+
const at = grantKey.lastIndexOf("@");
|
|
183
|
+
const base = at === -1 ? grantKey : grantKey.slice(0, at);
|
|
184
|
+
const level = at === -1 ? void 0 : grantKey.slice(at + 1);
|
|
185
|
+
for (const name of reach.get(base) ?? []) if (level === void 0 || level === name) out.push([grantKey, name]);
|
|
186
|
+
}
|
|
187
|
+
return out.toSorted(([a, x], [b, y]) => a === b ? x < y ? -1 : 1 : a < b ? -1 : 1);
|
|
188
|
+
}
|
|
189
|
+
/** The SQL command `action` compiles to: `rls.actions` first, then the six default verbs. */
|
|
190
|
+
function commandFor(action, actions) {
|
|
191
|
+
if (actions !== void 0 && Object.hasOwn(actions, action)) {
|
|
192
|
+
const configured = actions[action];
|
|
193
|
+
return configured === "none" ? void 0 : configured;
|
|
194
|
+
}
|
|
195
|
+
switch (action) {
|
|
196
|
+
case "read":
|
|
197
|
+
case "list":
|
|
198
|
+
case "get": return "select";
|
|
199
|
+
case "create": return "insert";
|
|
200
|
+
case "update": return "update";
|
|
201
|
+
case "delete": return "delete";
|
|
202
|
+
default: return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function tableFor(resource, tables) {
|
|
206
|
+
return tables?.[resource] ?? resource;
|
|
207
|
+
}
|
|
208
|
+
function ancestorsOf(policy, name) {
|
|
209
|
+
const names = [];
|
|
210
|
+
let current = policy.resources.get(name);
|
|
211
|
+
while (current?.parent !== void 0 && !names.includes(current.parent.resource)) {
|
|
212
|
+
names.push(current.parent.resource);
|
|
213
|
+
current = policy.resources.get(current.parent.resource);
|
|
214
|
+
}
|
|
215
|
+
return names;
|
|
216
|
+
}
|
|
217
|
+
function membershipField(policy, resource, holder) {
|
|
218
|
+
if (resource === void 0) return;
|
|
219
|
+
if (resource.name === holder) return resource.id ?? "id";
|
|
220
|
+
let current = resource;
|
|
221
|
+
const seen = /* @__PURE__ */ new Set();
|
|
222
|
+
while (current?.parent !== void 0 && !seen.has(current.name)) {
|
|
223
|
+
seen.add(current.name);
|
|
224
|
+
if (current.parent.resource === holder) return current.parent.field;
|
|
225
|
+
current = policy.resources.get(current.parent.resource);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function resourceCondition(item, policy, ctx) {
|
|
229
|
+
if (item.access.kind !== "resource") throw new Error("PermDock CLI: resourceCondition needs a resource role");
|
|
230
|
+
const { role, resource: roleResource } = item.access;
|
|
231
|
+
const target = policy.resources.get(item.grant.permission.resource);
|
|
232
|
+
const holders = [roleResource, ...ancestorsOf(policy, roleResource)];
|
|
233
|
+
const hops = [];
|
|
234
|
+
for (const [index, holder] of holders.entries()) {
|
|
235
|
+
const field = membershipField(policy, target, holder);
|
|
236
|
+
if (field === void 0) continue;
|
|
237
|
+
if (index > 0 && ctx.memberships?.resource?.[holder] === void 0) continue;
|
|
238
|
+
hops.push({
|
|
239
|
+
op: "memberOf",
|
|
240
|
+
scope: "resource",
|
|
241
|
+
field,
|
|
242
|
+
roles: [role],
|
|
243
|
+
resource: holder
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return sole(hops) ?? {
|
|
247
|
+
op: "or",
|
|
248
|
+
conditions: hops
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* The access a link capability has to a resource-scoped grant, over the same
|
|
253
|
+
* holders as `resourceCondition`. It reads the claim, so ancestor hops need
|
|
254
|
+
* no memberships table. `undefined` when no holder is on the row's chain.
|
|
255
|
+
*/
|
|
256
|
+
function capabilityAccess(item, policy, ctx) {
|
|
257
|
+
if (item.access.kind !== "resource") return;
|
|
258
|
+
const { role, resource: roleResource } = item.access;
|
|
259
|
+
const kinds = ctx.ownership?.kinds[role];
|
|
260
|
+
if (kinds !== void 0 && !kinds.includes("link")) return;
|
|
261
|
+
const target = policy.resources.get(item.grant.permission.resource);
|
|
262
|
+
const hops = [];
|
|
263
|
+
for (const holder of [roleResource, ...ancestorsOf(policy, roleResource)]) {
|
|
264
|
+
const field = membershipField(policy, target, holder);
|
|
265
|
+
if (field !== void 0) hops.push(capabilityAccessSql(ctx, field, holder, role, item.grant.permission.key));
|
|
266
|
+
}
|
|
267
|
+
if (hops.length === 0) return;
|
|
268
|
+
return hops.length === 1 ? hops[0] : hops.map(wrapSql).join(" or ");
|
|
269
|
+
}
|
|
270
|
+
function prepare(item, tables, warnings, skipClosures, actions, rowActions = false) {
|
|
271
|
+
const { grant, label } = item;
|
|
272
|
+
if (grant.breakGlass !== void 0) {
|
|
273
|
+
warnings.push(`break-glass grant ${label}/${grant.permission.key} reads through permdock_break_glass_${grant.permission.resource}, not a policy`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (grant.closure !== void 0 || grant.portable === false) {
|
|
277
|
+
if (skipClosures) {
|
|
278
|
+
warnings.push(`skipped non-portable grant ${label}/${grant.permission.key}`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
throw new Error(`PermDock CLI: closure grant ${label}/${grant.permission.key} is not portable; rewrite it or pass --skip-closures`);
|
|
282
|
+
}
|
|
283
|
+
const context = [.../* @__PURE__ */ new Set([...contextRefs(item.where), ...contextRefs(grant.check)])];
|
|
284
|
+
if (context.length > 0) {
|
|
285
|
+
if (skipClosures) {
|
|
286
|
+
warnings.push(`skipped grant ${label}/${grant.permission.key}: it reads ${context.join(", ")}, which is not in the token`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
throw new Error(`PermDock CLI: grant ${label}/${grant.permission.key} reads ${context.join(", ")}; request context is not in the token, so RLS cannot compile it. Move the value to a server-set claim (principal.claims.*) or pass --skip-closures (permdock doctor PD027)`);
|
|
290
|
+
}
|
|
291
|
+
if (requiresApproval(grant.approval)) {
|
|
292
|
+
warnings.push(`skipped approval grant ${label}/${grant.permission.key}`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const command = commandFor(grant.permission.action, actions);
|
|
296
|
+
const table = tableFor(grant.permission.resource, tables);
|
|
297
|
+
if (command === void 0) {
|
|
298
|
+
if (item.access.kind !== "role" && rowActions) {
|
|
299
|
+
warnings.push(`no policy for ${label}/${grant.permission.key}: action '${grant.permission.action}' has no SQL command (rls.actions); only permitted_<resource>_rows answers it`);
|
|
300
|
+
return {
|
|
301
|
+
item,
|
|
302
|
+
command: "none",
|
|
303
|
+
table,
|
|
304
|
+
...item.where === void 0 ? {} : { using: item.where }
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
if (item.access.kind !== "role") {
|
|
308
|
+
warnings.push(`skipped ${label}/${grant.permission.key}: action '${grant.permission.action}' has no SQL command (rls.actions) and only role grants are seeded`);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (grant.validity !== void 0 && grant.effect === "allow") {
|
|
312
|
+
warnings.push(`skipped ${label}/${grant.permission.key}: a role_permissions row cannot carry validFrom / validUntil, so a time-bounded grant on an action with no SQL command is not seeded`);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
warnings.push(`no policy for ${label}/${grant.permission.key}: action '${grant.permission.action}' has no SQL command (rls.actions); seeded for permdock_has and permitted_<scope>_ids`);
|
|
316
|
+
return {
|
|
317
|
+
item,
|
|
318
|
+
command: "none",
|
|
319
|
+
table,
|
|
320
|
+
...item.where === void 0 ? {} : { using: item.where },
|
|
321
|
+
...grant.check === void 0 ? {} : { check: grant.check }
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const using = command === "insert" ? void 0 : item.where;
|
|
325
|
+
const check = command === "insert" ? grant.check ?? item.where : command === "update" ? grant.check ?? item.where : void 0;
|
|
326
|
+
return {
|
|
327
|
+
item,
|
|
328
|
+
command,
|
|
329
|
+
table,
|
|
330
|
+
...using === void 0 ? {} : { using },
|
|
331
|
+
...check === void 0 ? {} : { check }
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
function signature(entry, byFields) {
|
|
335
|
+
const base = [
|
|
336
|
+
entry.item.grant.effect,
|
|
337
|
+
entry.using ?? null,
|
|
338
|
+
entry.check ?? null
|
|
339
|
+
];
|
|
340
|
+
return JSON.stringify(byFields ? [...base, entry.item.grant.fields ?? null] : base);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Grant keys per permission: the key is the permission, split into
|
|
344
|
+
* `permission#n` when role grants carry different portable conditions (or
|
|
345
|
+
* effects), one key per condition group; a group whose grants set `group`
|
|
346
|
+
* is `permission#<group>` instead. With field views, grants that differ
|
|
347
|
+
* only in `fields` get their own keys too, so a view can tell them apart.
|
|
348
|
+
*/
|
|
349
|
+
function assignKeys(entries, byFields) {
|
|
350
|
+
const groups = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const entry of entries) {
|
|
352
|
+
if (entry.item.access.kind !== "role") continue;
|
|
353
|
+
const key = entry.item.grant.permission.key;
|
|
354
|
+
const byCondition = groups.get(key) ?? /* @__PURE__ */ new Map();
|
|
355
|
+
const sig = signature(entry, byFields);
|
|
356
|
+
byCondition.set(sig, [...byCondition.get(sig) ?? [], entry]);
|
|
357
|
+
groups.set(key, byCondition);
|
|
358
|
+
}
|
|
359
|
+
const keys = /* @__PURE__ */ new Map();
|
|
360
|
+
for (const [permission, byCondition] of groups) {
|
|
361
|
+
const lists = [...byCondition.values()];
|
|
362
|
+
const named = /* @__PURE__ */ new Set();
|
|
363
|
+
for (const [index, list] of lists.entries()) {
|
|
364
|
+
const group = groupOf(permission, list);
|
|
365
|
+
if (group !== void 0 && named.has(group)) throw new Error(`PermDock CLI: grants of ${permission} with different conditions share group '${group}'; a group names one condition`);
|
|
366
|
+
if (group !== void 0) named.add(group);
|
|
367
|
+
const grantKey = group !== void 0 ? `${permission}#${group}` : lists.length === 1 ? permission : `${permission}#${index + 1}`;
|
|
368
|
+
for (const entry of list) keys.set(entry, grantKey);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return keys;
|
|
372
|
+
}
|
|
373
|
+
function groupOf(permission, list) {
|
|
374
|
+
const names = [...new Set(list.flatMap((entry) => entry.item.grant.group === void 0 ? [] : [entry.item.grant.group]))].toSorted();
|
|
375
|
+
if (names.length > 1) throw new Error(`PermDock CLI: grants of ${permission} with one condition name the groups ${names.join(", ")}; give them one group`);
|
|
376
|
+
return names[0];
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* One entry per level for every allow of an assignable role on a resource
|
|
380
|
+
* with levels, keyed `<grant key>@<level>`. Only `permdock_custom_keys`
|
|
381
|
+
* hands these keys out, to a custom role whose stored allow names the level.
|
|
382
|
+
*/
|
|
383
|
+
function levelEntries(policy, ctx, keys, tables, warnings, skipClosures) {
|
|
384
|
+
const custom = ctx.customRoles;
|
|
385
|
+
if (custom?.levels !== true) return [];
|
|
386
|
+
const assignable = new Set(custom.assignable);
|
|
387
|
+
const out = [];
|
|
388
|
+
for (const [entry, grantKey] of Array.from(keys)) {
|
|
389
|
+
const { item } = entry;
|
|
390
|
+
const { grant } = item;
|
|
391
|
+
if (item.access.kind !== "role" || grant.effect !== "allow" || grant.permission.kind !== "instance" || !assignable.has(item.access.role)) continue;
|
|
392
|
+
if (grant.permission.key.includes("@")) throw new Error(`PermDock CLI: permission key '${grant.permission.key}' holds '@', which separates a level in custom-role grant keys`);
|
|
393
|
+
for (const [level, condition] of policyLevels(policy, grant.permission.resource)) {
|
|
394
|
+
const where = andWhere(item.where, condition);
|
|
395
|
+
const prepared = prepare({
|
|
396
|
+
...item,
|
|
397
|
+
grant: leveled(grant, level, condition),
|
|
398
|
+
...where === void 0 ? {} : { where }
|
|
399
|
+
}, tables, warnings, skipClosures, ctx.actions);
|
|
400
|
+
if (prepared !== void 0) {
|
|
401
|
+
keys.set(prepared, `${grantKey}@${level}`);
|
|
402
|
+
out.push(prepared);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
return out;
|
|
407
|
+
}
|
|
408
|
+
function compileOptional(condition, ctx) {
|
|
409
|
+
return condition === void 0 ? void 0 : compileConditionSql(condition, ctx);
|
|
410
|
+
}
|
|
411
|
+
function noteConditions(entry, warnings) {
|
|
412
|
+
const { label, grant } = entry.item;
|
|
413
|
+
const names = [...sqlFunctionNames(entry.using), ...sqlFunctionNames(entry.check)];
|
|
414
|
+
if (names.length > 0) warnings.push(`sqlFunction ${[...new Set(names)].join(", ")} on ${label}/${grant.permission.key} is portable via twin`);
|
|
415
|
+
if (hasConditionOp(entry.using, "opaque") || hasConditionOp(entry.check, "opaque")) warnings.push(`opaque SQL on ${label}/${grant.permission.key} is untestable app-side`);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Compiles every grant (role and top-level) to branches whose role check is
|
|
419
|
+
* a helper call keyed by grant key. Nothing in a branch is per-row except the
|
|
420
|
+
* portable row condition and resource-membership joins.
|
|
421
|
+
*/
|
|
422
|
+
/** A Supabase token from a permanent user: anonymous sign-ins carry `is_anonymous: true`. */
|
|
423
|
+
const PERMANENT_USER = `((select auth.jwt()) ->> 'is_anonymous') is distinct from 'true'`;
|
|
424
|
+
function compileGrants(policy, ctx, tables, warnings, skipClosures, rowActions = false) {
|
|
425
|
+
const items = collectGrants(policy);
|
|
426
|
+
const entries = items.flatMap((item) => {
|
|
427
|
+
const entry = prepare(item, tables, warnings, skipClosures, ctx.actions, rowActions);
|
|
428
|
+
return entry === void 0 ? [] : [entry];
|
|
429
|
+
});
|
|
430
|
+
const keys = assignKeys(entries, ctx.fields === "views");
|
|
431
|
+
entries.push(...levelEntries(policy, ctx, keys, tables, warnings, skipClosures));
|
|
432
|
+
const conditionedKeys = /* @__PURE__ */ new Set();
|
|
433
|
+
for (const [entry, grantKey] of keys) if (entry.using !== void 0 || entry.check !== void 0 || entry.item.grant.validity !== void 0 || entry.item.grant.requires !== void 0) conditionedKeys.add(grantKey);
|
|
434
|
+
const rows = /* @__PURE__ */ new Map();
|
|
435
|
+
for (const item of items) {
|
|
436
|
+
const holder = item.grant.breakGlass === void 0 ? void 0 : breakGlassHolder(item);
|
|
437
|
+
if (holder !== void 0) {
|
|
438
|
+
const row = {
|
|
439
|
+
role: holder.role,
|
|
440
|
+
permission: item.grant.permission.key,
|
|
441
|
+
grantKey: breakGlassKey(item.grant.permission.key),
|
|
442
|
+
scope: holder.scope,
|
|
443
|
+
effect: item.grant.effect
|
|
444
|
+
};
|
|
445
|
+
rows.set(`${row.role}\u0000${row.grantKey}\u0000${row.scope}`, row);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const branches = [];
|
|
449
|
+
const actionBranches = [];
|
|
450
|
+
const rowColumns = /* @__PURE__ */ new Map();
|
|
451
|
+
const filters = (resource, table, column) => {
|
|
452
|
+
rowColumns.set(`${table}\u0000${column}`, {
|
|
453
|
+
resource,
|
|
454
|
+
table,
|
|
455
|
+
column
|
|
456
|
+
});
|
|
457
|
+
};
|
|
458
|
+
const typed = /* @__PURE__ */ new Map();
|
|
459
|
+
const contextFor = (name) => {
|
|
460
|
+
const known = typed.get(name);
|
|
461
|
+
if (known !== void 0) return known;
|
|
462
|
+
const node = policy.resources.get(name);
|
|
463
|
+
const schema = node === void 0 ? void 0 : jsonSchemaOf(node);
|
|
464
|
+
const next = {
|
|
465
|
+
...ctx,
|
|
466
|
+
columnTypes: columnTypesOf(schema),
|
|
467
|
+
arrayColumns: arrayColumnsOf(schema)
|
|
468
|
+
};
|
|
469
|
+
typed.set(name, next);
|
|
470
|
+
return next;
|
|
471
|
+
};
|
|
472
|
+
for (const entry of entries) {
|
|
473
|
+
const { item, table } = entry;
|
|
474
|
+
const { grant, access, label } = item;
|
|
475
|
+
if (entry.command === "none") {
|
|
476
|
+
const grantKey = keys.get(entry);
|
|
477
|
+
if (access.kind === "role" && grantKey !== void 0) {
|
|
478
|
+
const row = {
|
|
479
|
+
role: access.role,
|
|
480
|
+
permission: grant.permission.key,
|
|
481
|
+
grantKey,
|
|
482
|
+
scope: access.scope,
|
|
483
|
+
effect: grant.effect
|
|
484
|
+
};
|
|
485
|
+
rows.set(`${row.role}\u0000${row.grantKey}\u0000${row.scope}`, row);
|
|
486
|
+
}
|
|
487
|
+
if (!rowActions) continue;
|
|
488
|
+
}
|
|
489
|
+
const actionOnly = entry.command === "none";
|
|
490
|
+
const command = actionOnly ? "select" : entry.command;
|
|
491
|
+
const target = actionOnly ? actionBranches : branches;
|
|
492
|
+
const rowCtx = {
|
|
493
|
+
...contextFor(item.grant.permission.resource),
|
|
494
|
+
permission: grant.permission.key
|
|
495
|
+
};
|
|
496
|
+
noteConditions(entry, warnings);
|
|
497
|
+
for (const field of [...indexedFields(entry.using), ...indexedFields(entry.check)]) filters(grant.permission.resource, table, field);
|
|
498
|
+
const grantKey = keys.get(entry);
|
|
499
|
+
let accessExpr;
|
|
500
|
+
if (access.kind === "role" && grantKey !== void 0) {
|
|
501
|
+
const column = access.scope === "global" ? void 0 : scopeField(policy.resources.get(grant.permission.resource), access.scope, policy.scopes);
|
|
502
|
+
accessExpr = accessSql(ctx, access.scope, grantKey, column);
|
|
503
|
+
if (column !== void 0) filters(grant.permission.resource, table, column);
|
|
504
|
+
const row = {
|
|
505
|
+
role: access.role,
|
|
506
|
+
permission: grant.permission.key,
|
|
507
|
+
grantKey,
|
|
508
|
+
scope: access.scope,
|
|
509
|
+
effect: grant.effect
|
|
510
|
+
};
|
|
511
|
+
rows.set(`${row.role}\u0000${row.grantKey}\u0000${row.scope}`, row);
|
|
512
|
+
}
|
|
513
|
+
const linkOnly = access.kind === "resource" && ctx.capabilities === true && ctx.memberships?.resource?.[access.resource] === void 0;
|
|
514
|
+
if (access.kind === "actor") accessExpr = `${subjectClaimSql(ctx, "client_id")} is not null or ${subjectClaimJsonSql(ctx, "act")} is not null`;
|
|
515
|
+
if (access.kind === "resource" && !linkOnly) accessExpr = compileConditionSql(resourceCondition(item, policy, ctx), {
|
|
516
|
+
...ctx,
|
|
517
|
+
permission: grant.permission.key
|
|
518
|
+
});
|
|
519
|
+
if (linkOnly) warnings.push(`no ${access.resource} memberships table: only link capabilities reach ${label}/${grant.permission.key}`);
|
|
520
|
+
const validity = validitySql(grant.validity);
|
|
521
|
+
accessExpr = andSql(accessExpr, validity);
|
|
522
|
+
for (const key of grant.requires ?? []) accessExpr = andSql(accessExpr, requiresSql(ctx, key, policy.resources.get(grant.permission.resource)));
|
|
523
|
+
if (ctx.anonymousSignIns === "deny" && access.kind !== "anyone") accessExpr = andSql(accessExpr, PERMANENT_USER);
|
|
524
|
+
if (ctx.apiKeys !== void 0 && grant.effect === "allow" && access.kind !== "anyone" && access.kind !== "role") {
|
|
525
|
+
const calls = (requiresStandIn(grant) ? grant.requires ?? [] : [grant.permission.key, ...grant.requires ?? []]).map((key) => `(select ${apiKeyAllowsCall(ctx, quoteLiteral(key))})`);
|
|
526
|
+
accessExpr = andSql(accessExpr, calls.length === 1 ? calls[0] ?? "" : `(${calls.join(" and ")})`);
|
|
527
|
+
}
|
|
528
|
+
const using = compileOptional(entry.using, rowCtx);
|
|
529
|
+
const check = compileOptional(entry.check, rowCtx);
|
|
530
|
+
const fields = grant.fields === void 0 ? {} : { fields: grant.fields };
|
|
531
|
+
if (!linkOnly) target.push({
|
|
532
|
+
table,
|
|
533
|
+
command,
|
|
534
|
+
effect: grant.effect,
|
|
535
|
+
roles: access.kind === "anyone" ? ["anon", "authenticated"] : ["authenticated"],
|
|
536
|
+
label,
|
|
537
|
+
resource: grant.permission.resource,
|
|
538
|
+
permissionKey: grant.permission.key,
|
|
539
|
+
...fields,
|
|
540
|
+
...grantKey === void 0 ? {} : { grantKey },
|
|
541
|
+
...accessExpr === void 0 ? {} : { access: accessExpr },
|
|
542
|
+
...using === void 0 ? {} : { using },
|
|
543
|
+
...check === void 0 ? {} : { check }
|
|
544
|
+
});
|
|
545
|
+
const linked = ctx.capabilities === true ? capabilityAccess(item, policy, ctx) : void 0;
|
|
546
|
+
if (linked !== void 0) target.push({
|
|
547
|
+
table,
|
|
548
|
+
command,
|
|
549
|
+
effect: grant.effect,
|
|
550
|
+
roles: ["anon"],
|
|
551
|
+
label,
|
|
552
|
+
resource: grant.permission.resource,
|
|
553
|
+
permissionKey: grant.permission.key,
|
|
554
|
+
...fields,
|
|
555
|
+
access: andSql(linked, validity) ?? linked,
|
|
556
|
+
...using === void 0 ? {} : { using },
|
|
557
|
+
...check === void 0 ? {} : { check }
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
return {
|
|
561
|
+
branches: ensureSelectCoverage(branches, warnings),
|
|
562
|
+
actionBranches,
|
|
563
|
+
rolePermissions: withAliasRows(policy, [...rows.values()]),
|
|
564
|
+
conditionedKeys,
|
|
565
|
+
rowColumns: [...rowColumns.values()],
|
|
566
|
+
levelReach: ctx.customRoles?.levels === true ? levelReachOf(policy, keys) : []
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
/**
|
|
570
|
+
* Each row again under every key its permission was renamed from, so SQL that
|
|
571
|
+
* still calls `permdock_has('<old key>')` keeps the access the current key
|
|
572
|
+
* has. A split grant key (`key#n`) keeps its suffix; break-glass rows are not
|
|
573
|
+
* copied.
|
|
574
|
+
*/
|
|
575
|
+
function withAliasRows(policy, rows) {
|
|
576
|
+
const former = new Map(listPermissions(policy.permissions).map((leaf) => [leaf.key, formerKeys(leaf)]));
|
|
577
|
+
const out = [...rows];
|
|
578
|
+
for (const row of rows) {
|
|
579
|
+
const suffix = row.grantKey.slice(row.permission.length);
|
|
580
|
+
if (!row.grantKey.startsWith(row.permission) || suffix !== "" && (!/^#[a-z0-9][a-z0-9_-]*$/u.test(suffix) || suffix === "#break-glass")) continue;
|
|
581
|
+
for (const old of former.get(row.permission) ?? []) out.push({
|
|
582
|
+
...row,
|
|
583
|
+
permission: old,
|
|
584
|
+
grantKey: `${old}${suffix}`
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
return out;
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Postgres needs SELECT access to find rows for UPDATE / DELETE and for
|
|
591
|
+
* RETURNING, so a table with only update or delete grants gets matching
|
|
592
|
+
* SELECT branches.
|
|
593
|
+
*/
|
|
594
|
+
function ensureSelectCoverage(branches, warnings) {
|
|
595
|
+
const extra = [];
|
|
596
|
+
const readable = new Set(branches.filter((item) => item.command === "select" && item.effect === "allow").map((item) => item.table));
|
|
597
|
+
const covered = /* @__PURE__ */ new Set();
|
|
598
|
+
for (const item of branches) {
|
|
599
|
+
if (item.effect !== "allow" || item.command !== "update" && item.command !== "delete" || readable.has(item.table)) continue;
|
|
600
|
+
const { check: _check, ...rest } = item;
|
|
601
|
+
extra.push({
|
|
602
|
+
...rest,
|
|
603
|
+
command: "select",
|
|
604
|
+
coverage: true
|
|
605
|
+
});
|
|
606
|
+
if (!covered.has(item.table)) {
|
|
607
|
+
covered.add(item.table);
|
|
608
|
+
warnings.push(`added SELECT coverage for ${item.table} (${item.permissionKey})`);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return [...branches, ...extra];
|
|
612
|
+
}
|
|
613
|
+
/** True when `sql` is one parenthesised expression, so AND / OR need not wrap it again. */
|
|
614
|
+
function isWrapped(sql) {
|
|
615
|
+
if (!sql.startsWith("(") || !sql.endsWith(")")) return false;
|
|
616
|
+
let depth = 0;
|
|
617
|
+
let quoted = false;
|
|
618
|
+
for (let index = 0; index < sql.length; index += 1) {
|
|
619
|
+
const ch = sql[index];
|
|
620
|
+
if (ch === "'") quoted = !quoted;
|
|
621
|
+
else if (!quoted && ch === "(") depth += 1;
|
|
622
|
+
else if (!quoted && ch === ")") {
|
|
623
|
+
depth -= 1;
|
|
624
|
+
if (depth === 0 && index < sql.length - 1) return false;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return depth === 0;
|
|
628
|
+
}
|
|
629
|
+
function wrapSql(sql) {
|
|
630
|
+
return isWrapped(sql) ? sql : `(${sql})`;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* The grant's `validFrom` / `validUntil` as a clock check: `from` inclusive,
|
|
634
|
+
* `until` exclusive, matching the in-process evaluator.
|
|
635
|
+
*/
|
|
636
|
+
function validitySql(validity) {
|
|
637
|
+
if (validity === void 0) return;
|
|
638
|
+
return andSql(validity.from === void 0 ? void 0 : `now() >= to_timestamp(${validity.from})`, validity.until === void 0 ? void 0 : `now() < to_timestamp(${validity.until})`);
|
|
639
|
+
}
|
|
640
|
+
function andSql(...parts) {
|
|
641
|
+
const present = parts.filter((part) => part !== void 0 && part !== "true");
|
|
642
|
+
if (present.length === 0) return;
|
|
643
|
+
return present.length === 1 ? present[0] : present.map(wrapSql).join(" and ");
|
|
644
|
+
}
|
|
645
|
+
/** `USING` and `WITH CHECK` for a branch: its access check ANDed with its row conditions. */
|
|
646
|
+
function branchClauses(branch) {
|
|
647
|
+
const using = branch.command === "insert" ? void 0 : andSql(branch.access, branch.using) ?? "true";
|
|
648
|
+
const check = branch.command === "insert" || branch.command === "update" ? andSql(branch.access, branch.check) ?? "true" : void 0;
|
|
649
|
+
return {
|
|
650
|
+
...using === void 0 ? {} : { using },
|
|
651
|
+
...check === void 0 ? {} : { check }
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
//#endregion
|
|
655
|
+
//#region src/cli/rls/policies.ts
|
|
656
|
+
/** Each database role gets its own policy, so no role sees two permissive policies for one command. */
|
|
657
|
+
const AUDIENCES = ["authenticated", "anon"];
|
|
658
|
+
const DEFAULT_POLICY_NAME = "{table}_{op}";
|
|
659
|
+
const DEFAULT_PER_ROLE_NAME = "{role}_{permission}";
|
|
660
|
+
const PLACEHOLDER = /\{(table|op|role|permission)\}/gu;
|
|
661
|
+
function sanitize(name) {
|
|
662
|
+
return name.replaceAll(/[^A-Za-z0-9_]/g, "_");
|
|
663
|
+
}
|
|
664
|
+
function render(template, values) {
|
|
665
|
+
return sanitize(template.replaceAll(PLACEHOLDER, (_match, key) => values[key]));
|
|
666
|
+
}
|
|
667
|
+
/** Rejects a template that cannot name every policy of the chosen shape uniquely. */
|
|
668
|
+
function assertPolicyName(template, perRole) {
|
|
669
|
+
if (!perRole && /\{(role|permission)\}/u.test(template)) throw new Error("PermDock CLI: --policy-name {role} and {permission} need --policy-per-role; a collapsed policy covers several roles");
|
|
670
|
+
if (!perRole && !(template.includes("{table}") && template.includes("{op}"))) throw new Error("PermDock CLI: --policy-name needs {table} and {op} so each table and command gets its own policy");
|
|
671
|
+
}
|
|
672
|
+
function orSql(parts) {
|
|
673
|
+
const distinct = [...new Set(parts)];
|
|
674
|
+
if (distinct.includes("true")) return "true";
|
|
675
|
+
return sole(distinct) ?? distinct.map(wrapSql).join(" or ");
|
|
676
|
+
}
|
|
677
|
+
function negate(sql) {
|
|
678
|
+
return sql === void 0 ? void 0 : `(${sql}) is not true`;
|
|
679
|
+
}
|
|
680
|
+
function policyOf(name, branch, using, check) {
|
|
681
|
+
const deny = branch.effect === "deny";
|
|
682
|
+
const policy = {
|
|
683
|
+
name,
|
|
684
|
+
table: branch.table,
|
|
685
|
+
command: branch.command,
|
|
686
|
+
effect: branch.effect,
|
|
687
|
+
roles: branch.roles
|
|
688
|
+
};
|
|
689
|
+
const finalUsing = deny ? negate(using) : using;
|
|
690
|
+
const finalCheck = deny ? negate(check) : check;
|
|
691
|
+
if (finalUsing !== void 0) policy.using = finalUsing;
|
|
692
|
+
if (finalCheck !== void 0) policy.check = finalCheck;
|
|
693
|
+
return policy;
|
|
694
|
+
}
|
|
695
|
+
function uniqueName(names, name) {
|
|
696
|
+
const seen = names.get(name) ?? 0;
|
|
697
|
+
names.set(name, seen + 1);
|
|
698
|
+
return seen === 0 ? name : `${name}_${seen + 1}`;
|
|
699
|
+
}
|
|
700
|
+
function perRolePolicies(branches, template) {
|
|
701
|
+
const names = /* @__PURE__ */ new Map();
|
|
702
|
+
return branches.map((branch) => {
|
|
703
|
+
const clauses = branchClauses(branch);
|
|
704
|
+
const deny = branch.effect === "deny";
|
|
705
|
+
const base = render(template, {
|
|
706
|
+
table: branch.table,
|
|
707
|
+
op: branch.command,
|
|
708
|
+
role: branch.label,
|
|
709
|
+
permission: branch.permissionKey
|
|
710
|
+
});
|
|
711
|
+
const name = `${deny ? "deny_" : ""}${base}${branch.coverage === true ? "_select_coverage" : ""}`;
|
|
712
|
+
return policyOf(uniqueName(names, name), branch, clauses.using, clauses.check);
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Branches of one grant-key group share their row condition, so the group
|
|
717
|
+
* becomes one OR branch whose access ORs the helper calls of each scope.
|
|
718
|
+
*/
|
|
719
|
+
function mergeGroups(branches) {
|
|
720
|
+
const merged = [];
|
|
721
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
722
|
+
for (const branch of branches) {
|
|
723
|
+
const id = branch.grantKey === void 0 ? void 0 : `${branch.table}\u0000${branch.command}\u0000${branch.grantKey}\u0000${branch.coverage === true}`;
|
|
724
|
+
const entry = id === void 0 ? void 0 : byKey.get(id);
|
|
725
|
+
if (entry === void 0) {
|
|
726
|
+
const created = {
|
|
727
|
+
branch,
|
|
728
|
+
accesses: branch.access === void 0 ? [] : [branch.access]
|
|
729
|
+
};
|
|
730
|
+
if (id !== void 0) byKey.set(id, created);
|
|
731
|
+
merged.push(created);
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
if (branch.access !== void 0 && !entry.accesses.includes(branch.access)) entry.accesses.push(branch.access);
|
|
735
|
+
}
|
|
736
|
+
return merged.map(({ branch, accesses }) => accesses.length <= 1 ? branch : withAccess(branch, orSql(accesses)));
|
|
737
|
+
}
|
|
738
|
+
function withAccess(branch, access) {
|
|
739
|
+
return {
|
|
740
|
+
...branch,
|
|
741
|
+
access
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function collapsedPolicies(branches, template) {
|
|
745
|
+
const groups = /* @__PURE__ */ new Map();
|
|
746
|
+
for (const branch of mergeGroups(branches)) {
|
|
747
|
+
const id = `${branch.table}\u0000${branch.command}\u0000${branch.effect}`;
|
|
748
|
+
groups.set(id, [...groups.get(id) ?? [], branch]);
|
|
749
|
+
}
|
|
750
|
+
const names = /* @__PURE__ */ new Map();
|
|
751
|
+
return [...groups.values()].flatMap((group) => AUDIENCES.flatMap((audience) => {
|
|
752
|
+
const members = group.filter((branch) => branch.roles.includes(audience));
|
|
753
|
+
const [first] = members;
|
|
754
|
+
if (first === void 0) return [];
|
|
755
|
+
const deny = first.effect === "deny";
|
|
756
|
+
const clauses = members.map(branchClauses);
|
|
757
|
+
const usings = clauses.flatMap((item) => item.using === void 0 ? [] : [item.using]);
|
|
758
|
+
const checks = clauses.flatMap((item) => item.check === void 0 ? [] : [item.check]);
|
|
759
|
+
const base = render(template, {
|
|
760
|
+
table: first.table,
|
|
761
|
+
op: first.command,
|
|
762
|
+
role: first.label,
|
|
763
|
+
permission: first.permissionKey
|
|
764
|
+
});
|
|
765
|
+
return [policyOf(uniqueName(names, `${deny ? "deny_" : ""}${base}${audience === "anon" ? "_anon" : ""}`), {
|
|
766
|
+
...first,
|
|
767
|
+
roles: [audience]
|
|
768
|
+
}, usings.length === 0 ? void 0 : orSql(usings), checks.length === 0 ? void 0 : orSql(checks))];
|
|
769
|
+
}));
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Assembles policies. The default is one PERMISSIVE policy per table,
|
|
773
|
+
* command and database role (Splinter `multiple_permissive_policies` stays
|
|
774
|
+
* quiet), with denies in one RESTRICTIVE policy per table, command and role.
|
|
775
|
+
*/
|
|
776
|
+
function assemblePolicies(branches, shape) {
|
|
777
|
+
const template = shape.name ?? (shape.perRole ? DEFAULT_PER_ROLE_NAME : DEFAULT_POLICY_NAME);
|
|
778
|
+
assertPolicyName(template, shape.perRole);
|
|
779
|
+
return shape.perRole ? perRolePolicies(branches, template) : collapsedPolicies(branches, template);
|
|
780
|
+
}
|
|
781
|
+
const DEFAULT_READ_ONLY_ACTORS = ["support", "impersonation"];
|
|
782
|
+
const ACTOR_KIND = /^[a-z][a-z0-9_-]*$/u;
|
|
783
|
+
/** The actor kinds `rls.readOnlyActors` names, or `undefined` when it is off. */
|
|
784
|
+
function readOnlyActorKinds(setting) {
|
|
785
|
+
if (setting === void 0 || setting === false) return;
|
|
786
|
+
const kinds = setting === true ? DEFAULT_READ_ONLY_ACTORS : setting;
|
|
787
|
+
if (kinds.length === 0 || kinds.some((kind) => !ACTOR_KIND.test(kind))) throw new Error("PermDock CLI: rls.readOnlyActors must be true or a non-empty list of actor kinds such as 'support'");
|
|
788
|
+
return [...new Set(kinds)];
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* One restrictive policy per table and write command the allow policies
|
|
792
|
+
* grant to `authenticated`: the write passes unless the token's `act` claim
|
|
793
|
+
* (`act`, as SQL `jsonb`) is a session of one of `kinds` that is not
|
|
794
|
+
* `read_only: false`. Reads are never touched.
|
|
795
|
+
*/
|
|
796
|
+
function readOnlyActorPolicies(policies, act, kinds) {
|
|
797
|
+
const list = `array[${kinds.map((kind) => `'${kind}'`).join(", ")}]::text[]`;
|
|
798
|
+
const passes = `not coalesce(${kinds.includes("support") ? `(${act} ->> 'kind') = any(${list}) or ((${act} ->> 'kind') is null and ${act} ? 'session_id')` : `(${act} ->> 'kind') = any(${list})`}, false) or coalesce((${act} ->> 'read_only') = 'false', false)`;
|
|
799
|
+
const seen = /* @__PURE__ */ new Set();
|
|
800
|
+
const out = [];
|
|
801
|
+
for (const policy of policies) {
|
|
802
|
+
const key = `${policy.table}\u0000${policy.command}`;
|
|
803
|
+
if (policy.effect !== "allow" || policy.command === "select" || !policy.roles.includes("authenticated") || seen.has(key)) continue;
|
|
804
|
+
seen.add(key);
|
|
805
|
+
out.push({
|
|
806
|
+
name: sanitize(`${policy.table}_${policy.command}_read_only_actors`),
|
|
807
|
+
table: policy.table,
|
|
808
|
+
command: policy.command,
|
|
809
|
+
effect: "deny",
|
|
810
|
+
roles: ["authenticated"],
|
|
811
|
+
...policy.command === "insert" ? {} : { using: passes },
|
|
812
|
+
...policy.command === "delete" ? {} : { check: passes }
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
return out;
|
|
816
|
+
}
|
|
817
|
+
//#endregion
|
|
818
|
+
//#region src/cli/rls/fields.ts
|
|
819
|
+
/** Names of the field-view objects. Part of the SQL contract. */
|
|
820
|
+
const FIELD_VIEWS = {
|
|
821
|
+
/** `<table>_visible`: the `security_invoker` view clients read. */
|
|
822
|
+
view: "_visible",
|
|
823
|
+
/** `<table>_visible_fields`: the owner-rights companion `--revoke-columns` adds. */
|
|
824
|
+
companion: "_visible_fields",
|
|
825
|
+
/** The companion's row-key column, joined to the base table's key. */
|
|
826
|
+
key: "permdock_key",
|
|
827
|
+
/** Marks the companion so doctor PD022 knows it reads as its owner on purpose. */
|
|
828
|
+
comment: "permdock:field-companion"
|
|
829
|
+
};
|
|
830
|
+
function covers(branch, column) {
|
|
831
|
+
return branch.fields === void 0 || branch.fields.includes(column);
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* A read grant that only shapes columns: a field-limited deny (it never
|
|
835
|
+
* denies the row) or an allow with an empty list (it grants nothing). With
|
|
836
|
+
* field views it leaves the row policy and lives in the view's masks.
|
|
837
|
+
*/
|
|
838
|
+
function fieldOnly(branch) {
|
|
839
|
+
if (branch.command !== "select" || branch.fields === void 0) return false;
|
|
840
|
+
return branch.effect === "deny" || branch.fields.length === 0;
|
|
841
|
+
}
|
|
842
|
+
/** Branches the row policies keep when field views compile. */
|
|
843
|
+
function rowBranches(branches) {
|
|
844
|
+
return branches.filter((branch) => !fieldOnly(branch));
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Whether the reader is signed in, for branches with no helper call: a grant
|
|
848
|
+
* to any signed-in user relies on `to authenticated` in its policy, which a
|
|
849
|
+
* view does not have.
|
|
850
|
+
*/
|
|
851
|
+
function audience(ctx, branch) {
|
|
852
|
+
if (branch.access !== void 0 || branch.roles.length !== 1) return;
|
|
853
|
+
if (branch.roles[0] === "authenticated") return signedIn(ctx);
|
|
854
|
+
return branch.roles[0] === "anon" ? `not (${signedIn(ctx)})` : void 0;
|
|
855
|
+
}
|
|
856
|
+
function clause(ctx, branch) {
|
|
857
|
+
return andSql(audience(ctx, branch), branchClauses(branch).using) ?? "true";
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* `can(permission, row, { field })` in SQL: an allow that covers the column
|
|
861
|
+
* holds for the row and no covering deny does. Every access term is the
|
|
862
|
+
* uncorrelated helper call the row policy uses, so it runs once per statement.
|
|
863
|
+
*/
|
|
864
|
+
function maskSql(ctx, allows, denies, column) {
|
|
865
|
+
const allowed = allows.filter((branch) => covers(branch, column)).map((branch) => clause(ctx, branch));
|
|
866
|
+
if (allowed.length === 0) return "false";
|
|
867
|
+
const denied = denies.filter((branch) => covers(branch, column)).map((branch) => clause(ctx, branch));
|
|
868
|
+
const allow = orSql(allowed);
|
|
869
|
+
if (denied.length === 0) return allow;
|
|
870
|
+
const deny = orSql(denied);
|
|
871
|
+
return allow === "true" ? `not ${wrapSql(deny)}` : `${wrapSql(allow)} and not ${wrapSql(deny)}`;
|
|
872
|
+
}
|
|
873
|
+
function schemaColumns(policy, resource) {
|
|
874
|
+
const node = policy.resources.get(resource);
|
|
875
|
+
const schema = node === void 0 ? null : jsonSchemaOf(node);
|
|
876
|
+
const properties = schema !== null && typeof schema === "object" ? schema.properties : void 0;
|
|
877
|
+
return properties !== null && typeof properties === "object" ? Object.keys(properties) : void 0;
|
|
878
|
+
}
|
|
879
|
+
function viewName(table, suffix) {
|
|
880
|
+
return `${table}${suffix}`;
|
|
881
|
+
}
|
|
882
|
+
/**
|
|
883
|
+
* `<helper schema>.<table>_visible_fields`: outside the Data API's schemas,
|
|
884
|
+
* so its owner rights reach clients only through the `_visible` join.
|
|
885
|
+
*/
|
|
886
|
+
function companionName(ctx, table) {
|
|
887
|
+
const base = table.startsWith("public.") ? table.slice(7) : table.replaceAll(".", "_");
|
|
888
|
+
return `${helperSchema(ctx)}.${viewName(base, FIELD_VIEWS.companion)}`;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* One view per table whose read grants limit fields. A column is restricted
|
|
892
|
+
* when some read allow lists fields without it or some read deny lists it;
|
|
893
|
+
* every other column, and the row key, passes through unchanged.
|
|
894
|
+
*/
|
|
895
|
+
function fieldViews(policy, ctx, branches, options, warnings) {
|
|
896
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
897
|
+
for (const branch of branches) {
|
|
898
|
+
if (branch.command !== "select" || branch.coverage === true) continue;
|
|
899
|
+
byTable.set(branch.table, [...byTable.get(branch.table) ?? [], branch]);
|
|
900
|
+
}
|
|
901
|
+
const views = [];
|
|
902
|
+
for (const [table, reads] of byTable) {
|
|
903
|
+
const allows = reads.filter((branch) => branch.effect === "allow" && branch.fields?.length !== 0);
|
|
904
|
+
const denies = reads.filter((branch) => branch.effect === "deny");
|
|
905
|
+
const limited = reads.some((branch) => branch.fields !== void 0);
|
|
906
|
+
const resource = reads.find((branch) => branch.resource !== void 0)?.resource;
|
|
907
|
+
if (!limited || allows.length === 0 || resource === void 0) continue;
|
|
908
|
+
const names = schemaColumns(policy, resource);
|
|
909
|
+
if (names === void 0) throw new Error(`PermDock CLI: --fields views needs the ${resource} schema's JSON Schema (Standard JSON Schema) to list the columns of ${table}`);
|
|
910
|
+
const key = policy.resources.get(resource)?.id ?? "id";
|
|
911
|
+
const restricted = (column) => column !== key && (allows.some((branch) => branch.fields !== void 0 && !branch.fields.includes(column)) || denies.some((branch) => branch.fields?.includes(column) === true));
|
|
912
|
+
const columns = names.map((name) => {
|
|
913
|
+
quoteIdent(name);
|
|
914
|
+
return restricted(name) ? {
|
|
915
|
+
name,
|
|
916
|
+
mask: maskSql(ctx, allows, denies, name)
|
|
917
|
+
} : { name };
|
|
918
|
+
});
|
|
919
|
+
if (columns.every((column) => column.mask === void 0)) continue;
|
|
920
|
+
const view = viewName(table, FIELD_VIEWS.view);
|
|
921
|
+
if (allows.some((branch) => branch.fields !== void 0 && !branch.fields.includes(key))) warnings.push(`field view ${view}: the key ${key} passes through, although a read grant on ${resource} omits it`);
|
|
922
|
+
const hidden = columns.filter((column) => column.mask !== void 0).map((column) => column.name);
|
|
923
|
+
if (options.helpersOnly === true) warnings.push(`field view ${view}: ${table} still returns ${hidden.join(", ")} to direct reads; revoke select on those columns from client roles in your own grants, or read them through a function`);
|
|
924
|
+
else if (!options.revokeColumns) warnings.push(`field view ${view}: ${table} still returns ${hidden.join(", ")} to direct reads; add --revoke-columns so clients read through the view`);
|
|
925
|
+
views.push({
|
|
926
|
+
resource,
|
|
927
|
+
table,
|
|
928
|
+
view,
|
|
929
|
+
key,
|
|
930
|
+
columns,
|
|
931
|
+
roles: reads.some((branch) => branch.roles.includes("anon")) ? ["anon", "authenticated"] : ["authenticated"],
|
|
932
|
+
...options.revokeColumns ? { companion: companionName(ctx, table) } : {}
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
return views;
|
|
936
|
+
}
|
|
937
|
+
/** Columns of `view`'s table that `anon` and `authenticated` keep on the base table with `--revoke-columns`. */
|
|
938
|
+
function readableColumns(view) {
|
|
939
|
+
return view.columns.filter((column) => column.mask === void 0).map((column) => column.name);
|
|
940
|
+
}
|
|
941
|
+
function restrictedColumns(view) {
|
|
942
|
+
return view.columns.filter((column) => column.mask !== void 0).map((column) => column.name);
|
|
943
|
+
}
|
|
944
|
+
function columnList(names) {
|
|
945
|
+
return names.map(quoteIdent).join(", ");
|
|
946
|
+
}
|
|
947
|
+
function selectList(lines) {
|
|
948
|
+
return lines.map((line) => ` ${line}`).join(",\n");
|
|
949
|
+
}
|
|
950
|
+
function grantView(name, roles) {
|
|
951
|
+
return [`revoke all on table ${quoteQualified(name)} from anon, authenticated, public;`, `grant select on table ${quoteQualified(name)} to ${roles.join(", ")};`].join("\n");
|
|
952
|
+
}
|
|
953
|
+
function inlineViewSql(view) {
|
|
954
|
+
const targets = view.columns.map((column) => column.mask === void 0 ? quoteIdent(column.name) : `case when ${column.mask} then ${quoteIdent(column.name)} end as ${quoteIdent(column.name)}`);
|
|
955
|
+
return `create or replace view ${quoteQualified(view.view)} with (security_invoker = true) as
|
|
956
|
+
select
|
|
957
|
+
${selectList(targets)}
|
|
958
|
+
from ${quoteQualified(view.table)};`;
|
|
959
|
+
}
|
|
960
|
+
function companionSql(view, companion) {
|
|
961
|
+
const restricted = view.columns.filter((column) => column.mask !== void 0);
|
|
962
|
+
const targets = [`${quoteIdent(view.key)} as ${quoteIdent(FIELD_VIEWS.key)}`, ...restricted.map((column) => `case when ${column.mask} then ${quoteIdent(column.name)} end as ${quoteIdent(column.name)}`)];
|
|
963
|
+
const any = orSql(restricted.map((column) => column.mask ?? "false"));
|
|
964
|
+
const name = companion.slice(0, companion.indexOf("."));
|
|
965
|
+
const schema = quoteIdent(name);
|
|
966
|
+
return `${name === "public" ? "" : `create schema if not exists ${schema};
|
|
967
|
+
revoke all on schema ${schema} from public;
|
|
968
|
+
grant usage on schema ${schema} to ${view.roles.join(", ")};
|
|
969
|
+
`}create or replace view ${quoteQualified(companion)} with (security_barrier = true) as
|
|
970
|
+
select
|
|
971
|
+
${selectList(targets)}
|
|
972
|
+
from ${quoteQualified(view.table)}
|
|
973
|
+
where ${any};
|
|
974
|
+
comment on view ${quoteQualified(companion)} is ${quoteLiteral(`${FIELD_VIEWS.comment} ${view.view}`)};`;
|
|
975
|
+
}
|
|
976
|
+
function joinedViewSql(view, companion) {
|
|
977
|
+
const targets = view.columns.map((column) => column.mask === void 0 ? `t.${quoteIdent(column.name)}` : `f.${quoteIdent(column.name)}`);
|
|
978
|
+
return `create or replace view ${quoteQualified(view.view)} with (security_invoker = true) as
|
|
979
|
+
select
|
|
980
|
+
${selectList(targets)}
|
|
981
|
+
from ${quoteQualified(view.table)} t
|
|
982
|
+
left join ${quoteQualified(companion)} f on f.${quoteIdent(FIELD_VIEWS.key)} = t.${quoteIdent(view.key)};`;
|
|
983
|
+
}
|
|
984
|
+
/** `select` on only the unrestricted columns (and the key) of the base table, for `--revoke-columns`. */
|
|
985
|
+
function columnGrantSql(view, role) {
|
|
986
|
+
return `grant select (${columnList(readableColumns(view))}) on table ${quoteQualified(view.table)} to ${role};`;
|
|
987
|
+
}
|
|
988
|
+
function columnRevokeSql(view) {
|
|
989
|
+
return `revoke select (${columnList(restrictedColumns(view))}) on table ${quoteQualified(view.table)} from anon, authenticated;`;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* The view SQL: the `security_invoker` view, and with `--revoke-columns` the
|
|
993
|
+
* owner-rights companion it joins for the restricted columns. The companion
|
|
994
|
+
* masks every column with the full field decision and keeps only rows where
|
|
995
|
+
* one is readable, so reading it directly shows nothing `pick` would not.
|
|
996
|
+
*/
|
|
997
|
+
function fieldViewsSql(views) {
|
|
998
|
+
return views.map((view) => {
|
|
999
|
+
const head = `-- field view over ${view.table}: ${restrictedColumns(view).join(", ")} read as null unless a read grant covers the column for the row`;
|
|
1000
|
+
if (view.companion === void 0) return [
|
|
1001
|
+
head,
|
|
1002
|
+
inlineViewSql(view),
|
|
1003
|
+
grantView(view.view, view.roles)
|
|
1004
|
+
].join("\n");
|
|
1005
|
+
return [
|
|
1006
|
+
head,
|
|
1007
|
+
`-- ${view.companion} reads ${view.table} as its owner; ${view.view} reads the rows as the caller and joins it`,
|
|
1008
|
+
companionSql(view, view.companion),
|
|
1009
|
+
grantView(view.companion, view.roles),
|
|
1010
|
+
joinedViewSql(view, view.companion),
|
|
1011
|
+
grantView(view.view, view.roles)
|
|
1012
|
+
].join("\n");
|
|
1013
|
+
}).join("\n\n");
|
|
1014
|
+
}
|
|
1015
|
+
//#endregion
|
|
1016
|
+
export { indexTargets as _, fieldViewsSql as a, readTableIndexFacts as b, assemblePolicies as c, readOnlyActorPolicies as d, branchClauses as f, COLUMNS_SQL as g, tableFor as h, fieldViews as i, orSql as l, compileGrants as m, columnGrantSql as n, rowBranches as o, commandFor as p, columnRevokeSql as r, viewName as s, FIELD_VIEWS as t, readOnlyActorKinds as u, indexesSql as v, pruneIndexTargets as y };
|