permdock 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -47
- package/dist/a2a/index.d.ts +7 -16
- package/dist/a2a/index.js +59 -98
- package/dist/adapter-context-mlWsVGGg.js +23 -0
- package/dist/ai-sdk/index.d.ts +37 -16
- package/dist/ai-sdk/index.js +30 -13
- package/dist/approvals/index.d.ts +3 -3
- package/dist/approvals/index.js +5 -4
- package/dist/{approvals-DaxY-Bdg.js → approvals-DbYAuNkG.js} +17 -11
- package/dist/approvers-DT4AVyIg.js +52 -0
- package/dist/{arazzo-CTeVuXr5.js → arazzo-BAgw9ap6.js} +2 -2
- package/dist/{arazzo-aoTkqYsJ.js → arazzo-cmvX2jDe.js} +2 -2
- package/dist/authzen/index.d.ts +4 -13
- package/dist/authzen/index.js +16 -50
- package/dist/better-auth/index.d.ts +20 -3
- package/dist/better-auth/index.js +27 -7
- package/dist/better-supabase/index.d.ts +206 -0
- package/dist/better-supabase/index.js +620 -0
- package/dist/bind-CVy3Espv.d.ts +25 -0
- package/dist/bind-VnYn2HKq.js +97 -0
- package/dist/boundary-BM8a_P6E.js +13 -0
- package/dist/boundary-BmHxVH1J.d.ts +15 -0
- package/dist/canonical-json-DemiK4Vp.js +26 -0
- package/dist/{capability-B68OFnZx.js → capability-4qPdO9gk.js} +7 -5
- package/dist/{capability-BDG_nkxt.d.ts → capability-Dq43tT1u.d.ts} +2 -2
- package/dist/catalog/index.d.ts +1 -1
- package/dist/catalog/index.js +1 -1
- package/dist/catalog-BjZSovXQ.js +2 -0
- package/dist/{catalog-doc-BSo_Q_-6.js → catalog-doc-Dsc9y7sd.js} +23 -12
- package/dist/{catalog-B0E6bggc.js → catalog-h2tlKFkq.js} +2 -2
- package/dist/{catalog-H-vFGwWK.js → catalog-z8q6bi_r.js} +4 -4
- package/dist/claude-agent/index.d.ts +5 -14
- package/dist/claude-agent/index.js +4 -10
- package/dist/clerk/index.d.ts +12 -3
- package/dist/clerk/index.js +26 -5
- package/dist/cli/bin.js +1 -1
- package/dist/cli/index.d.ts +3 -3
- package/dist/cli/index.js +2 -2
- package/dist/clients-C7zLFuWT.d.ts +12 -0
- package/dist/clients-DjU5ABMy.js +23 -0
- package/dist/cloud/index.d.ts +4 -5
- package/dist/cloud/index.js +20 -6
- package/dist/{cloud-BNQTrmrr.js → cloud-BGhHS-lY.js} +2 -2
- package/dist/{cloud-Cuxr6Czw.js → cloud-Bo3e9Ak3.js} +7 -6
- package/dist/{collect-O-tOtbnk.js → collect-DDDv_ESs.js} +194 -42
- package/dist/collect-DLlujdHC.js +83 -0
- package/dist/collect-rG2IQ3t0.js +2 -0
- package/dist/{commands-BMh_Etm2.js → commands-DWOrSQic.js} +16 -12
- package/dist/compile/index.d.ts +3 -0
- package/dist/compile/index.js +2 -0
- package/dist/{compile-D22iwvuV.js → compile-Ba4blROR.js} +85 -30
- package/dist/compile-CNnl6uIt.d.ts +94 -0
- package/dist/config-BJiM-xAm.js +157 -0
- package/dist/config-Bn00Z3ks.js +36 -0
- package/dist/config-report-Dw_JcUqT.js +44 -0
- package/dist/{connection-JvqwHODC.js → connection-CU8MQxRi.js} +1 -1
- package/dist/{context-CKEAabd7.js → context-Ttp0ekMT.js} +36 -27
- package/dist/convex/index.d.ts +9 -4
- package/dist/convex/index.js +14 -4
- package/dist/create-BWyx3jua.d.ts +245 -0
- package/dist/create-CPhDL6qx.js +253 -0
- package/dist/{credential-B_rkUNlY.js → credential-8P81ff4u.js} +26 -46
- package/dist/delegation-D8pUEuce.js +103 -0
- package/dist/{describe-Bt1YD5uS.js → describe-B_gVZEVi.js} +10 -15
- package/dist/{describe-DernyItf.d.ts → describe-PdoqESvg.d.ts} +9 -10
- package/dist/{diff-x5Il8LVa.js → diff-B8Tunc3o.js} +44 -11
- package/dist/{diff-BUzf5Poi.js → diff-CzqLxZ77.js} +2 -2
- package/dist/doctor-WpvG6p4k.js +1194 -0
- package/dist/doctor-collect-ByfYlNv1.js +664 -0
- package/dist/{doctor-Bqo6FwUa.js → doctor-rOr-4ZfS.js} +2 -2
- package/dist/drizzle/index.d.ts +4 -3
- package/dist/drizzle/index.js +3 -3
- package/dist/elysia/index.d.ts +6 -28
- package/dist/elysia/index.js +16 -60
- package/dist/{errors-DSMnAVNa.js → errors-CWfoNg9N.js} +8 -4
- package/dist/{errors-Iy71s7Oj.js → errors-Cz5zn0mW.js} +17 -71
- package/dist/{errors-DbZ1YOnb.d.ts → errors-DAqevpNk.d.ts} +8 -21
- package/dist/{errors-m3u9TmDu.js → errors-DRnGZHzF.js} +4 -1
- package/dist/evaluation-items-CfCCejQd.js +40 -0
- package/dist/{evaluations-DsAU9ppl.js → evaluations-tz6D9A2G.js} +28 -46
- package/dist/eve/index.d.ts +6 -15
- package/dist/eve/index.js +7 -12
- package/dist/expiry-BoBJ8Tvc.js +7 -0
- package/dist/express/index.d.ts +6 -29
- package/dist/express/index.js +28 -58
- package/dist/fastify/index.d.ts +6 -29
- package/dist/fastify/index.js +16 -65
- package/dist/fields-BMl1RxoX.js +26 -0
- package/dist/{files-B_wAGYmt.js → files-DWUN9PzI.js} +5 -1
- package/dist/{fixtures-Cvaw0gDG.js → fixtures-ht28Cplr.js} +5 -2
- package/dist/{freeze-y1_jQL4v.js → freeze-CrU8nQeX.js} +31 -2
- package/dist/{from-snapshot-BMGnhmgv.js → from-snapshot-C3sHpZNS.js} +65 -48
- package/dist/{grantee-Bv04_Vzf.js → grantee-Cv5Sogfv.js} +88 -7
- package/dist/graph-sql-B-dsAT6j.d.ts +29 -0
- package/dist/{headers-B5RRv3Xm.js → headers-BZSDieNT.js} +1 -1
- package/dist/{headers-C8SHQccD.d.ts → headers-Cf-zWvsO.d.ts} +1 -1
- package/dist/helpers-H_KARDVC.js +421 -0
- package/dist/hono/index.d.ts +6 -28
- package/dist/hono/index.js +11 -43
- package/dist/{hosted-BwKQ1BTA.js → hosted-DJQw46hn.js} +6 -5
- package/dist/{http-GgVApX1k.js → http-BIhmLfCc.js} +4 -20
- package/dist/http-SO7qGttf.js +44 -0
- package/dist/ids-DC4V4CFu.js +18 -0
- package/dist/{index-LF_hS-yn.d.ts → index-A0YNL8jZ.d.ts} +1 -1
- package/dist/index-CtE07CBF.d.ts +6 -0
- package/dist/{index-xIxJLwOQ.d.ts → index-D3vbkoSI.d.ts} +75 -7
- package/dist/{index-CltcQ4Bf.d.ts → index-D6_cSa1z.d.ts} +68 -5
- package/dist/index.d.ts +14 -9
- package/dist/index.js +22 -17
- package/dist/instance-options-BK_rNOSC.d.ts +11 -0
- package/dist/instance-options-Dhhcnm3y.js +22 -0
- package/dist/{parse-DPfIvFMU.js → json-schema-BsAflUGW.js} +28 -57
- package/dist/jwt/index.d.ts +10 -6
- package/dist/jwt/index.js +32 -13
- package/dist/kernel-GSQnMz3O.js +208 -0
- package/dist/kysely/index.d.ts +4 -3
- package/dist/kysely/index.js +3 -3
- package/dist/live-options-H2eG8dfJ.js +60 -0
- package/dist/{load-BZ24CpZP.js → load-DhWEB9P1.js} +16 -10
- package/dist/load-DwJUafYN.js +2 -0
- package/dist/manifest-iaoBXHKc.js +783 -0
- package/dist/manifest-uDS9y5de.d.ts +206 -0
- package/dist/{map-DowSO96j.js → map-AcxK9o_D.js} +4 -15
- package/dist/{map-claims-B_6I1Rn-.js → map-claims-Z4QZOiFF.js} +13 -4
- package/dist/map-error-pgUWNNtG.js +23 -0
- package/dist/may-use-DndQ5tQ-.js +77 -0
- package/dist/mcp/index.d.ts +54 -22
- package/dist/mcp/index.js +218 -178
- package/dist/nest/index.d.ts +26 -31
- package/dist/nest/index.js +75 -84
- package/dist/next/client.d.ts +7 -8
- package/dist/next/client.js +11 -9
- package/dist/next/index.d.ts +30 -47
- package/dist/next/index.js +45 -60
- package/dist/next/plugin.d.ts +1 -1
- package/dist/next/plugin.js +32 -1
- package/dist/node/index.d.ts +9 -28
- package/dist/node/index.js +11 -43
- package/dist/openai/index.d.ts +6 -15
- package/dist/openai/index.js +7 -20
- package/dist/openapi/index.d.ts +87 -4
- package/dist/openapi/index.js +3 -3
- package/dist/{openapi-CwePaF7C.js → openapi-Bbg4wGVL.js} +3 -3
- package/dist/{openapi-DcqM8nU7.js → openapi-BqRBvAng.js} +237 -5
- package/dist/{openapi-DWD0Uc_4.js → openapi-CqqEbHsL.js} +5 -5
- package/dist/{openapi-import-BWRUNNeW.js → openapi-import-cmwetbzw.js} +1 -1
- package/dist/{openapi-schema-D8j_T9tO.js → openapi-schema--ARjyfQr.js} +2 -2
- package/dist/orpc/index.d.ts +15 -28
- package/dist/orpc/index.js +74 -117
- package/dist/otel/index.d.ts +2 -2
- package/dist/parse-BuyjxhZV.js +53 -0
- package/dist/parse-local-manifest-BkGQNzAh.d.ts +10 -0
- package/dist/parse-local-manifest-BrMozJCo.js +36 -0
- package/dist/pdp/index.d.ts +2 -2
- package/dist/pdp/index.js +14 -127
- package/dist/{permdock-yoZ7EjxX.js → permdock-CzqXjCpU.js} +641 -126
- package/dist/{permissions-B8vCg_bI.d.ts → permissions-CDgvKG5L.d.ts} +2 -2
- package/dist/{permissions-CofRqesP.js → permissions-CpZrivYv.js} +3 -3
- package/dist/{permissions-DC7GROPA.js → permissions-DVoF6dMt.js} +77 -15
- package/dist/{permissions-B4apnahQ.d.ts → permissions-DttnqqxC.d.ts} +34 -5
- package/dist/{pins-CqE1jZuF.js → pins-8ILTftC9.js} +2 -2
- package/dist/{policy-CInmNGkM.d.ts → policy-CFZAr_Al.d.ts} +330 -37
- package/dist/{policy-Cq2fB8Q5.d.ts → policy-D8UFyERw.d.ts} +2 -2
- package/dist/{policy-ANp36PnI.js → policy-xyWg7QdR.js} +173 -22
- package/dist/powersync-A8x_Yyvh.js +2 -0
- package/dist/powersync-C7kLglqX.js +540 -0
- package/dist/powersync-yW1UUFzp.js +62 -0
- package/dist/prisma/index.d.ts +3 -2
- package/dist/prisma/index.js +45 -20
- package/dist/{problem-Cgp9-KZA.js → problem-B43ztKIm.js} +74 -109
- package/dist/problem-details-CkL50EEf.d.ts +7 -0
- package/dist/problem-details-_AMOYz-w.js +144 -0
- package/dist/{protected-BuPAycJT.d.ts → protected-CNsMzrVd.d.ts} +5 -4
- package/dist/protected-Cs6kTaHo.js +100 -0
- package/dist/protected-bZs6rbT1.d.ts +40 -0
- package/dist/{provider-wTAUda2v.d.ts → provider-Cjr62YMV.d.ts} +1 -1
- package/dist/provider-DLzpb2Xz.js +52 -0
- package/dist/provider-store-DNyC1y48.js +43 -0
- package/dist/react/index.d.ts +7 -5
- package/dist/react/index.js +5 -4
- package/dist/react/provider-client.d.ts +1 -1
- package/dist/react/provider-client.js +1 -1
- package/dist/react-native/index.d.ts +166 -10
- package/dist/react-native/index.js +607 -85
- package/dist/{refs-ChLSxbST.js → refs-KYb4ntaz.js} +4 -3
- package/dist/{relations-r4soIFZB.js → relations-cjOL01hZ.js} +110 -31
- package/dist/replay-BsOyTrdV.js +28 -0
- package/dist/replay-sqBGMnNs.d.ts +5 -0
- package/dist/resource-ref-CaS9yVPi.js +20 -0
- package/dist/revocations-Bhr0aMIC.js +27 -0
- package/dist/rls-BMNydpHk.js +2521 -0
- package/dist/{rls-BOFU3chG.js → rls-CEvs64Kj.js} +8 -3
- package/dist/rls-fields-BMasREhR.js +3502 -0
- package/dist/{rls-graph-DxQ39PJj.js → rls-graph-pc9fuXYm.js} +100 -36
- package/dist/{rls-import-CXV6LBlS.js → rls-import-D8F7j35Q.js} +7 -6
- package/dist/{rls-migrate-DrFPbf8p.js → rls-migrate-7wikfrVI.js} +6 -5
- package/dist/rls-ownership-B5Lkq62W.js +843 -0
- package/dist/rls-shared-aOQ4hGTO.js +493 -0
- package/dist/rls-verify-D8B7dOTh.js +745 -0
- package/dist/{row-check-BajTgQZ-.d.ts → row-check-Dt35czLy.d.ts} +12 -15
- package/dist/rpc-DOK54_CS.js +89 -0
- package/dist/{run-CJ6UvS3a.js → run-YXCJDNYk.js} +31 -12
- package/dist/runtime-CB8gQFvS.js +11 -0
- package/dist/{saas-BRrZzJkM.js → saas-Bj_FFkjN.js} +17 -7
- package/dist/{schema-P8yoma__.js → schema-D3MnDMxa.js} +8 -4
- package/dist/scim/index.d.ts +1 -1
- package/dist/scim/index.js +1 -1
- package/dist/{scim-9NFa0XVZ.js → scim-BFGBBAqR.js} +1 -1
- package/dist/{scopes-cKVp_WTw.js → scopes-CVdPrWN-.js} +35 -8
- package/dist/server/index.d.ts +22 -11
- package/dist/server/index.js +11 -8
- package/dist/{credentials-BgRX3WEg.js → server-DlZ28Oz5.js} +1 -1
- package/dist/shared-DRu8VFVM.js +123 -0
- package/dist/{sink-Cfr9-7YC.d.ts → sink-Gs6TTVa-.d.ts} +1 -1
- package/dist/skills-Biq5uup3.js +192 -0
- package/dist/{skills-C_lpV4Jx.js → skills-JaymNCdK.js} +10 -4
- package/dist/{snapshot-DE8dE_hz.js → snapshot-Enxd0n-s.js} +138 -214
- package/dist/snapshot-cache-D6jLelln.js +44 -0
- package/dist/snapshot-cache-DwQH8ufx.d.ts +45 -0
- package/dist/solid/index.d.ts +39 -9
- package/dist/solid/index.js +76 -85
- package/dist/sources-B0UvrJ21.js +534 -0
- package/dist/sources-BcJgw0lt.d.ts +192 -0
- package/dist/{sql-Bll1d7y0.js → sql-xdg5f4Fi.js} +6 -1
- package/dist/{src-CTNT-PBW.js → src-CSbUiozF.js} +149 -11
- package/dist/ssf/index.d.ts +3 -2
- package/dist/ssf/index.js +3 -29
- package/dist/store-DMAWwAJV.js +371 -0
- package/dist/{store-Bq90395i.d.ts → store-D_pjn91-.d.ts} +13 -4
- package/dist/store-options-yO0V6gRn.js +716 -0
- package/dist/{stores-CkG2HrSg.js → stores-4_sqiOA1.js} +10 -63
- package/dist/{stores-CvOnu_2T.d.ts → stores-DVR5NHLE.d.ts} +20 -9
- package/dist/{stream-Q_xlB6Ko.js → stream-DDpvynSB.js} +16 -2
- package/dist/{stream-BhMfZOwr.d.ts → stream-DlvG2hs-.d.ts} +2 -2
- package/dist/subject-settings-BwBmCdTR.js +128 -0
- package/dist/supabase/index.d.ts +122 -6
- package/dist/supabase/index.js +4 -3
- package/dist/supabase/middleware.d.ts +32 -27
- package/dist/supabase/middleware.js +56 -22
- package/dist/{supabase-D8UlBA7Z.js → supabase-CwafwxxF.js} +3 -3
- package/dist/{supabase-DTxGtYdi.js → supabase-DTBaC20O.js} +345 -367
- package/dist/supabase-config-Bs44dare.js +472 -0
- package/dist/{supabase-hook-D_OqGb_V.js → supabase-hook-BuxLQLpU.js} +994 -708
- package/dist/supabase-hook-D5qVd_D8.js +2 -0
- package/dist/supautils-DYZnzRWV.js +91 -0
- package/dist/svelte/PermissionBoundary.svelte +37 -0
- package/dist/svelte/Protected.svelte +8 -2
- package/dist/svelte/index.d.ts +9 -6
- package/dist/svelte/index.js +61 -7
- package/dist/svelte/runtime.d.ts +4 -3
- package/dist/svelte/runtime.js +4 -3
- package/dist/svelte/source.d.ts +9 -6
- package/dist/svelte/source.js +7 -4
- package/dist/terminal/index.d.ts +6 -14
- package/dist/terminal/index.js +44 -32
- package/dist/testing/index.d.ts +105 -16
- package/dist/testing/index.js +504 -40
- 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/token-CIWxJOCh.js +50 -0
- package/dist/trpc/index.d.ts +7 -26
- package/dist/trpc/index.js +17 -100
- package/dist/types-9AWO9EGt.d.ts +197 -0
- package/dist/{types-D6Z6-rKJ.d.ts → types-BPxgszlT.d.ts} +260 -22
- package/dist/{types-BvBk1wgn.d.ts → types-BVyImy5I.d.ts} +5 -3
- package/dist/{types-CxquUByI.d.ts → types-Bvc4oWT6.d.ts} +1 -1
- package/dist/types-C3s9xVJG.d.ts +48 -0
- package/dist/{types-BBK-exvJ.d.ts → types-Dnemwd77.d.ts} +7 -1
- package/dist/types-kxznc7cn.d.ts +52 -0
- package/dist/{types-B8w5PFtS.d.ts → types-vKyAMtcX.d.ts} +17 -1
- package/dist/{types-B9dmiiab.d.ts → types-vNSDF7Dq.d.ts} +1 -1
- package/dist/{sources-DOpW5Ila.d.ts → types-yF5LmQVD.d.ts} +86 -127
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +10 -9
- package/dist/{usage-1ozMubLY.js → usage-D07N9QJ3.js} +2 -2
- package/dist/usage-D82l1CwK.js +2 -0
- package/dist/{usage-CUnK6Ou9.js → usage-DG1oeCca.js} +225 -62
- package/dist/validation-error-CnjZ4t92.js +33 -0
- package/dist/validation-error-CtF23SrC.d.ts +26 -0
- package/dist/{validity-D0uRmJKJ.js → validity-uOp8UpYn.js} +57 -36
- package/dist/{verifier-B90agnhl.js → verifier-BxBo6I44.js} +10 -5
- package/dist/version-BvacVTZR.js +17 -0
- package/dist/views-Soe7bGSC.js +95 -0
- package/dist/{vocabulary-Dk0fs-Jk.d.ts → vocabulary-DA8a8zif.d.ts} +1 -1
- package/dist/vue/index.d.ts +35 -13
- package/dist/vue/index.js +86 -93
- package/dist/watch-Etoa_Mvr.js +2 -0
- package/dist/watch-UihESpsJ.js +128 -0
- package/dist/webmcp/index.d.ts +2 -2
- package/dist/webmcp/index.js +7 -13
- package/package.json +62 -36
- package/schemas/approval-request-v1.json +175 -0
- package/schemas/catalog-v1.json +29 -1
- package/schemas/supabase-claims-v1.json +6 -1
- package/schemas/supabase-manifest-v1.json +150 -6
- package/skills/permdock/SKILL.md +7 -7
- package/skills/permdock-agents/SKILL.md +5 -5
- package/skills/permdock-agents/references/runtimes.md +12 -10
- package/skills/permdock-approvals/SKILL.md +4 -4
- package/skills/permdock-approvals/references/stores-and-resume.md +15 -4
- package/skills/permdock-audit/SKILL.md +3 -3
- package/skills/permdock-credentials/SKILL.md +4 -4
- package/skills/permdock-credentials/references/tokens-keys-links.md +7 -2
- package/skills/permdock-data/SKILL.md +4 -4
- package/skills/permdock-data/references/queries-and-rls.md +14 -6
- package/skills/permdock-data/references/relationships.md +5 -3
- package/skills/permdock-tenancy/SKILL.md +9 -6
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +14 -8
- package/skills/permdock-wire/SKILL.md +4 -4
- package/skills/permdock-wire/references/adapters.md +42 -30
- package/skills/permdock-wire/references/policy-options.md +5 -3
- package/dist/approvers-aH0PJkmf.js +0 -20
- package/dist/catalog-idsHc0vo.js +0 -2
- package/dist/collect-B7_TIfmg.js +0 -49
- package/dist/collect-fRPAQIWb.js +0 -2
- package/dist/compile-CQbvyIBr.d.ts +0 -30
- package/dist/config-BPYmjTOp.js +0 -26
- package/dist/context-gtAwAILx.js +0 -6
- package/dist/create-BKF3dgFk.d.ts +0 -162
- package/dist/create-CSkhDU9T.js +0 -222
- package/dist/doctor-BGle8Gqi.js +0 -1314
- package/dist/helpers-DnSOrhB9.js +0 -682
- package/dist/index-fG6NSXlc.d.ts +0 -9
- package/dist/kernel-YOGCWNLf.js +0 -211
- package/dist/load-QOOm1z70.js +0 -2
- package/dist/manifest-DDQbC6K0.d.ts +0 -118
- package/dist/map-error-RJ3ZsUyA.js +0 -16
- package/dist/may-use-CnFEjVEp.js +0 -37
- package/dist/pg-4GJWDUxG.js +0 -953
- package/dist/plugin-4gr7IxFJ.js +0 -65
- package/dist/protected-B4K3BKoX.js +0 -138
- package/dist/protected-wY7nXhY9.d.ts +0 -23
- package/dist/provider-_wx46HpB.js +0 -48
- package/dist/rls-CJVja9Ww.js +0 -2119
- package/dist/rls-helpers-BxRuif0l.js +0 -1346
- package/dist/runtime-DP9ttFh1.js +0 -27
- package/dist/skills-Dx3QLnMY.js +0 -110
- package/dist/source-DoBBYJi2.js +0 -6
- package/dist/sources-DVHM3Mwq.js +0 -284
- package/dist/store-C6hFW6ta.js +0 -508
- package/dist/subject-settings-xt7V845g.js +0 -82
- package/dist/supabase-hook-DW-xP4hU.js +0 -2
- package/dist/types-DbWdEfcm.d.ts +0 -87
- package/dist/types-bva3i4Gf.d.ts +0 -98
- package/dist/types-i4BAOvdb.d.ts +0 -97
- package/dist/usage-PyEor6GA.js +0 -2
- package/dist/version-CnSh33Fj.js +0 -16
|
@@ -4,13 +4,13 @@ description: Enforces PermDock permissions in database queries and Postgres row-
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: ScaleDockHQ
|
|
7
|
-
homepage: https://permdock.
|
|
7
|
+
homepage: https://permdock.com/docs/adapters/rls
|
|
8
8
|
repository: https://github.com/ScaleDockHQ/permdock
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# PermDock data
|
|
12
12
|
|
|
13
|
-
The same portable conditions decide `can()` in memory, narrow list queries through `where()`, and compile to Postgres RLS. Keep them portable and prove the three agree ([conditions](https://permdock.
|
|
13
|
+
The same portable conditions decide `can()` in memory, narrow list queries through `where()`, and compile to Postgres RLS. Keep them portable and prove the three agree ([conditions](https://permdock.com/docs/concepts/conditions), [RLS](https://permdock.com/docs/adapters/rls)). Set up the policy and factory first with the `permdock-wire` skill (`npx skills add ScaleDockHQ/PermDock --skill permdock-wire`); named scopes and memberships come from the `permdock-tenancy` skill.
|
|
14
14
|
|
|
15
15
|
## Inputs (find out, or ask before starting)
|
|
16
16
|
|
|
@@ -38,7 +38,7 @@ The same portable conditions decide `can()` in memory, narrow list queries throu
|
|
|
38
38
|
✓ `permdock rls generate --check` passes in CI.
|
|
39
39
|
4. **Verify against a real database.** Run `permdock rls verify --db $DATABASE_URL` with fixtures, `--tree` for graph grants, and `--introspect` against a deployed database.
|
|
40
40
|
✓ Every verify run exits `0`.
|
|
41
|
-
5. **Prove parity in tests.** Add `ormParity(policy, scenarios, { run, id })` for each list endpoint and `rlsParity(policy, options)` for RLS tables, from `permdock/testing` ([testing adapter](https://permdock.
|
|
41
|
+
5. **Prove parity in tests.** Add `ormParity(policy, scenarios, { run, id })` for each list endpoint and `rlsParity(policy, options)` for RLS tables, from `permdock/testing` ([testing adapter](https://permdock.com/docs/adapters/testing)). Run `testRelationSource` on a custom `RelationSource`.
|
|
42
42
|
✓ The query and the database return exactly the rows `filter()` keeps.
|
|
43
43
|
|
|
44
44
|
## Verify before done
|
|
@@ -54,4 +54,4 @@ The same portable conditions decide `can()` in memory, narrow list queries throu
|
|
|
54
54
|
|
|
55
55
|
- [references/queries-and-rls.md](references/queries-and-rls.md): `toWhere` per ORM, `checkRow`, `withSubject`, Convex, `rls generate` / `import` / `verify` / `migrate` flags and config, field views, SQL as the authority.
|
|
56
56
|
- [references/relationships.md](references/relationships.md): `parent`, relation kinds, `restricted`, `links`, `relation(..., { through })`, `RelationSource`, `loadRelations`, `whoCan`, the closure table and `--tree`.
|
|
57
|
-
- Docs: [conditions](https://permdock.
|
|
57
|
+
- Docs: [conditions](https://permdock.com/docs/concepts/conditions), [relationships](https://permdock.com/docs/concepts/relationships), [RLS adapter](https://permdock.com/docs/adapters/rls), [`permdock rls`](https://permdock.com/docs/cli/rls), [Postgres RLS](https://permdock.com/docs/standards/postgres-rls).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Queries and generated RLS
|
|
2
2
|
|
|
3
|
-
Owning pages: [protected queries](https://permdock.
|
|
3
|
+
Owning pages: [protected queries](https://permdock.com/docs/concepts/policies#protected-queries), [RLS adapter](https://permdock.com/docs/adapters/rls), [`permdock rls`](https://permdock.com/docs/cli/rls), and the ORM pages [Drizzle](https://permdock.com/docs/adapters/drizzle), [Prisma](https://permdock.com/docs/adapters/prisma), [Kysely](https://permdock.com/docs/adapters/kysely), [Convex](https://permdock.com/docs/adapters/convex).
|
|
4
4
|
|
|
5
5
|
## List queries
|
|
6
6
|
|
|
@@ -35,12 +35,20 @@ pnpm exec permdock rls verify --db $DATABASE_URL --fixtures rls.fixtures.json
|
|
|
35
35
|
pnpm exec permdock rls verify --introspect --db $DATABASE_URL
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
- `--
|
|
38
|
+
- List the scope instances a permission reaches in process with `permittedIds(permdock, permission, scope, { within })`, never a loop of `can` over memberships with a made-up row.
|
|
39
|
+
- Generated policies call `permdock_has('<key>')` and one `permitted_<scope>_ids('<key>')` per declared scope, which Postgres runs once per statement. Hand-written policies that only need membership (an organization switcher) call `member_<scope>_ids()` instead of inventing a permission key. Trusted SQL that acts for a stored user (a job, a trigger, a deferred approval) calls `permdock_has_for(p_user, '<key>')` and `permitted_<scope>_ids_for(p_user, '<key>')` in `database` mode, or the generated `permitted_<resource>_rows_for`, never a hand-written function that rewrites `request.jwt.claims`. With `rls.rowHelpers`, a hand-written policy or function that needs the rows a permission reaches through the graph calls `permitted_<resource>_rows('<permission key>')` instead of restating closure walks, link hops and `includes`. A table's own `select` or `insert` policy calls the row-valued `permitted_<resource>_row(<table>, '<permission key>')` instead, so `insert ... returning` sees the new row. Hand-written policies and functions that apply no row condition call `permitted_<scope>_ids_by_permission('<permission key>')` or `permdock_has_permission('<permission key>')` instead of spelling a positional `#n` grant key. A screen or RPC that checks many permissions on one organization calls `permitted_<scope>_permission_keys(<id>)` once (or `(<id>, array['<key>', ...])` for a few keys) instead of one helper call per key. A role editor or admin screen lists a role's keys with `permdock_role_permissions(p_role, p_scope, p_tenant, p_scope_id)` (declared roles, and custom roles of the caller's tenant in `database` mode) and the catalog with `permdock_permission_keys()` (`permdock_permission_keys('<scope>')` for the keys a role on that scope can hold), instead of joining `role_permissions` and the custom-role tables by hand; server code reads any tenant's roles with `permdock_trusted_role_permissions`, granted through `rls.trustedReaders`. Set `--tenant-type` (or `rls.tenantType`, `rls.scopeTypes`) to the scope columns' types.
|
|
40
|
+
- `--authorize database` (default) reads the membership tables per statement; map each under `rls.memberships.scopes.<scope>` with `columns` for its id and its ancestors', and `via` so roles with `for` apply: a column name, or `{ value: 'staff' }` when every row has that kind (never add a generated column for it). `--authorize jwt` reads token claims and stays stale until the token refreshes; keep the token lifetime at 3600 seconds or less (PD019).
|
|
41
|
+
- The helpers narrow to the tenant claim (`rls.tenantClaim`) whenever the token carries one; the Supabase hook writes it from `activeFrom`. When the app takes the tenant from the URL and one session spans tenants, set `rls.tenants: 'all'` so the database admits every member tenant, and keep filtering each query by the URL's tenant.
|
|
42
|
+
- `--policy-per-role` keeps one policy per role for review. `--custom-roles` (or `rls.customRoles: true`) resolves tenant-defined roles inside the `permdock_ceiling` view. In `database` mode, save a custom role with `permdock_replace_custom_role_grants(tenant, scope, scope_id, role, allow, deny, include)` and rename or delete it with `permdock_rename_custom_role_grants` and `permdock_delete_custom_role_grants`, never with hand-written `security definer` functions or direct writes; failures raise `22023` or `42501` with the reason as the `hint`. A platform custom role passes a null tenant and scope `global`. Seeds, jobs and backends with no member caller use the `permdock_trusted_*` variants, which keep the definition checks; grant `execute` on them to a backend role in a migration of your own, never to `authenticated`. Set `rls.customRoleWrites.requires` to the permission your role editor asserts (or `'manageRoles'`) so the database refuses members who may not manage roles (`manage-roles`).
|
|
41
43
|
- `--force` (or `rls.force: true`) only when the app connects as the table owner. Create views over RLS tables `with (security_invoker = true)` (PD022).
|
|
42
44
|
- When read grants set `fields` and clients read tables directly, add `--fields views --revoke-columns`: clients read `<table>_visible`, whose restricted columns are null unless a grant covers them. PD030 names columns still readable.
|
|
43
|
-
- `rls.suspension` drops suspended users' roles and memberships of suspended scope instances, live in both modes.
|
|
45
|
+
- `rls.suspension` drops suspended users' roles and memberships of suspended scope instances, live in both modes. `scopes.<scope>.keep: [permissions.org.restore]` lets members of a suspended instance still hold those permissions (restore, cancel deletion, export) in the helpers, `authorize()`, the token hook and in process, where the membership carries `keep`; `doctor` PD061 lists them.
|
|
46
|
+
- When support or impersonation tokens (an `act` claim) reach the Data API, set `rls.readOnlyActors: true`: each table and write command the policies grant gets a restrictive `{table}_{op}_read_only_actors` policy that refuses the write unless `act.read_only` is `false`. Never hand-write that policy per table.
|
|
47
|
+
- When a backend queries Postgres for a verified API key, set `rls.apiKeys` and put the key in the `api_key` claim (`scopes` as permission keys, `tenant` and `roles` for a key that belongs to a tenant): the helpers cap every allow at the scopes and treat a tenant key without `sub` as a service principal of that tenant. A user key whose claim names a `tenant` is held to that tenant even with `rls.tenants: 'all'`, and `permdock_has` is false for it. Never write a `has_scope` check per policy.
|
|
48
|
+
- On Supabase every generated helper and policy reads the subject through `permdock_user_id()`: the `sub` in `request.jwt.claims`, and null for the empty `sub` of a tenant service key. It ignores the legacy `request.jwt.claim.sub` setting; SQL that impersonates a user sets `request.jwt.claims`. A hand-written policy such a token reaches calls `(select permdock.permdock_user_id())`, never `auth.uid()`, which fails the uuid cast.
|
|
49
|
+
- `rls.assignments` puts a `permdock_assignment` trigger on each mapped membership table, the global-roles table `rls.roles` and each table in `tables`; `ownRole: 'refuse'` (or a map of tables) also refuses a client write to the caller's own row. `doctor` PD064 names a guarded table no migration puts the trigger on.
|
|
50
|
+
- SQL that assigns roles calls `permdock_can_assign_any(role, tenant, scope, scope_id)`, which covers declared and custom roles; SQL acting later for a stored user (an invitation accept re-checking the inviter) calls `permdock_can_assign_any_for(user, ...)`. `permdock supabase inspect` lists both in the manifest's `rls.helpers`.
|
|
51
|
+
- Index suggestions (the `indexes` part of `--split`, or `index suggestion` warnings) cover only the columns the policies and helpers look rows up by. Add `--db $DATABASE_URL` to `generate` so it leaves out indexes an existing index already leads with and columns the table lacks; never hand-copy a suggestion that duplicates an existing composite index.
|
|
44
52
|
- Never emit `service_role`. Fixtures may carry `memberships`, `tenant` and `customRoles`.
|
|
45
53
|
|
|
46
54
|
## SQL as the authority
|
|
@@ -51,4 +59,4 @@ When hand-written policies call the app's own helpers (`org_ids_with_permission`
|
|
|
51
59
|
|
|
52
60
|
## Attributes RLS cannot see
|
|
53
61
|
|
|
54
|
-
A grant condition on `context.*` cannot be enforced by the database (PD027). Compare with a server-set `principal.claims.*` claim instead, never one from `user_metadata`. On Supabase, `supabase.hook.attrs` lists only server-owned columns or `app_metadata.<key>` entries clients cannot update (PD028). The hook, declarative schemas (`--split`) and `fromTable` / `fromJunction` membership sources are on the [Supabase adapter](https://permdock.
|
|
62
|
+
A grant condition on `context.*` cannot be enforced by the database (PD027). Compare with a server-set `principal.claims.*` claim instead, never one from `user_metadata`. On Supabase, `supabase.hook.attrs` lists only server-owned columns or `app_metadata.<key>` entries clients cannot update (PD028). A check that must refuse a sign-in (single sign-on enforcement, a locked account) goes in `supabase.hook.before` as a schema-qualified `(event jsonb) returns jsonb` function the generated hook calls first; never a second hook or a hand edit of the generated one. The hook, declarative schemas (`--split`) and `fromTable` / `fromJunction` membership sources are on the [Supabase adapter](https://permdock.com/docs/adapters/supabase) and [Supabase hook](https://permdock.com/docs/adapters/supabase-hook) pages.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Relationship graph
|
|
2
2
|
|
|
3
|
-
Owning page: [relationships](https://permdock.
|
|
3
|
+
Owning page: [relationships](https://permdock.com/docs/concepts/relationships). Named scopes are tenancy (fixed levels); the graph is the objects under them, with no fixed depth.
|
|
4
4
|
|
|
5
5
|
## Declare
|
|
6
6
|
|
|
@@ -30,7 +30,9 @@ export const permissions = definePermissions({
|
|
|
30
30
|
| Principal | `{ principal, period? }` | the row's column is the principal's id, inside `period` (a manager, an account delegate) |
|
|
31
31
|
| Implied | `{ includes: ['editor'] }` | the principal holds a listed relation |
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
`groups` lets an edge row name a team or another group: `{ column: 'kind', resources: { team: 'member' } }` when one column names the kind, or `{ resources: { team: { relation: 'member', subject: 'team_id' } } }` when the share table keeps each subject kind in its own typed column.
|
|
34
|
+
|
|
35
|
+
A resource that parents itself makes a chain. `restricted` names a boolean column: a restricted row is reached only by grants on itself, never through its ancestors or its links. `restricted: { field, stops: ['parent'] }` closes only the paths it lists (`'parent'` and link names), for example a folder hidden from shares above it but still readable by everyone with drive access; a closed link also stays closed for the rows below a restricted row, within 32 levels. `links` names to-one references a grant can cross with `through: ['folder', 'team']`.
|
|
34
36
|
|
|
35
37
|
## Grant
|
|
36
38
|
|
|
@@ -43,7 +45,7 @@ grants: [
|
|
|
43
45
|
],
|
|
44
46
|
```
|
|
45
47
|
|
|
46
|
-
`depth` defaults to 16 and is at most 32. An array in `to:` is an intersection, so "owner or viewer" is two `allow` grants. Prefer `relation()` over `where: { authorId: principal.id }` when the resource declares the relation.
|
|
48
|
+
Add `requires: permissions.file.read` to a graph grant to count a share only where the user also holds that permission through a role in the row's organization; a list (`requires: [permissions.file.read, permissions.file.download]`) requires each. `to: inherit(permissions.drive.read, { through: ['drive'] })` grants on a row wherever the subject may read the drive its `drive` link points to, by any grant of `drive.read`; the source needs `row`, and `rls generate` writes the target's `permitted_<resource>_rows`. `depth` defaults to 16 and is at most 32. An array in `to:` is an intersection, so "owner or viewer" is two `allow` grants. Prefer `relation()` over `where: { authorId: principal.id }` when the resource declares the relation.
|
|
47
49
|
|
|
48
50
|
## Load
|
|
49
51
|
|
|
@@ -4,13 +4,13 @@ description: Models tenants, memberships and role rules in PermDock. Use when de
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: ScaleDockHQ
|
|
7
|
-
homepage: https://permdock.
|
|
7
|
+
homepage: https://permdock.com/docs/concepts/scopes
|
|
8
8
|
repository: https://github.com/ScaleDockHQ/permdock
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# PermDock tenancy
|
|
12
12
|
|
|
13
|
-
A policy declares its scopes in order (an organization, the customers inside it). A role is held at one scope, a membership names one instance of that scope, and a grant reaches a row only through that scope's key. Roles never cascade between scopes ([named scopes](https://permdock.
|
|
13
|
+
A policy declares its scopes in order (an organization, the customers inside it). A role is held at one scope, a membership names one instance of that scope, and a grant reaches a row only through that scope's key. Roles never cascade between scopes ([named scopes](https://permdock.com/docs/concepts/scopes), [tenancy](https://permdock.com/docs/concepts/tenancy)). Set up the policy and factory first with the `permdock-wire` skill (`npx skills add ScaleDockHQ/PermDock --skill permdock-wire`).
|
|
14
14
|
|
|
15
15
|
## Inputs (find out, or ask before starting)
|
|
16
16
|
|
|
@@ -24,9 +24,9 @@ A policy declares its scopes in order (an organization, the customers inside it)
|
|
|
24
24
|
|
|
25
25
|
1. Memberships and custom roles come only from a `subjectFrom*` provider, the policy's `subject` or `context` function, a `MembershipSource` or a `RoleSource`. Never from a request body, an unsigned header, a model argument or a CLI flag.
|
|
26
26
|
2. A requested tenant with no matching membership is no tenant, never a default one. Team and tenant ids are identifiers, never display names.
|
|
27
|
-
3. No cascade. An organization owner sees no customer portal unless they also hold a contact membership, and a global role never reaches scoped rows.
|
|
27
|
+
3. No cascade. An organization owner sees no customer portal unless they also hold a contact membership, and a global role never reaches scoped rows. An instance check without a row answers from first-scope memberships only; a nested membership needs the row or `permdock.team(id)`. A collection check (`create`, `list`) still answers from any membership in the tenant; for a staff-only guard pass `{ scope: '<first scope>' }` as the decide option.
|
|
28
28
|
4. Role rules live on the role (`min`, `assigns`, `for`), not in handlers. `decideRoleChange` takes the actor from the instance and never writes.
|
|
29
|
-
5. A custom role never exceeds the ceiling of declared `assignable` roles in its scope. Its grants carry no condition, approval or limit of their own.
|
|
29
|
+
5. A custom role never exceeds the ceiling of declared `assignable` roles in its scope. Its grants carry no condition, approval or limit of their own; a `level` picks a condition the resource declares in code.
|
|
30
30
|
|
|
31
31
|
## Workflow
|
|
32
32
|
|
|
@@ -55,11 +55,14 @@ A policy declares its scopes in order (an organization, the customers inside it)
|
|
|
55
55
|
✓ `definePolicy` does not throw for a missing `memberOf` relation.
|
|
56
56
|
3. **Load memberships.** Return memberships as `{ scope, id, within, roles, via, expiresAt }` from the subject resolver, or pass a `MembershipSource` (one, or an array) as `memberships` on `createPermDock`. A person's access ending is the membership's `expiresAt`.
|
|
57
57
|
✓ `permdock doctor` reports no PD025 for the `doctor.memberships` fixture.
|
|
58
|
+
|
|
59
|
+
An organisation role that reaches only the principal's teams is an organisation role with `where: { team_id: { in: principal.claims.team_ids } }`, not a team role. On Supabase, write the claim with `supabase.hook.claims` ([team reach](https://permdock.com/docs/concepts/tenancy#organisation-roles-with-team-reach)).
|
|
60
|
+
|
|
58
61
|
4. **Put ownership on the roles.** Set `min: 1` on the role that manages each scope, `assigns` for who hands out what, `for: ['staff']` on admin-like roles, and `meta.audience` for the surface. Every server action that changes a membership calls `permdock.decideRoleChange(...)` with the target's current membership and the holder count before it writes. -> [references/ownership-and-custom-roles.md](references/ownership-and-custom-roles.md)
|
|
59
62
|
✓ PD026 is clean, and removing the last owner is denied with `last-holder`.
|
|
60
63
|
5. **Custom roles, when tenant admins define roles.** Keep declared roles small and mark them in `defineRoles` (`editor: { on: 'organization', assignable: true }`). Pass a `RoleSource` as `customRoles`, build the editor from `useAssignablePermissions()`, and save through a server action that runs `validateCustomRole`. -> [references/ownership-and-custom-roles.md](references/ownership-and-custom-roles.md#custom-roles)
|
|
61
64
|
✓ PD023 is clean, and saving a role with a permission outside the ceiling is refused.
|
|
62
|
-
6. **Test.** Add `describePolicy` rows for a member of another tenant, a contact reaching an organization row, and a role change that breaks `min` ([scenario testing](https://permdock.
|
|
65
|
+
6. **Test.** Add `describePolicy` rows for a member of another tenant, a contact reaching an organization row, and a role change that breaks `min` ([scenario testing](https://permdock.com/docs/guides/scenario-testing)). Run `testMembershipSource` and `testRoleSource` from `permdock/testing` on custom sources. For list queries and RLS over scoped tables, follow the `permdock-data` skill.
|
|
63
66
|
✓ The tests pass.
|
|
64
67
|
|
|
65
68
|
## Verify before done
|
|
@@ -74,4 +77,4 @@ A policy declares its scopes in order (an organization, the customers inside it)
|
|
|
74
77
|
## Reference index
|
|
75
78
|
|
|
76
79
|
- [references/ownership-and-custom-roles.md](references/ownership-and-custom-roles.md): role options, `decideRoleChange` input and denial reasons, the SQL objects RLS uses, and the custom-role shape, ceiling and editor.
|
|
77
|
-
- Docs: [named scopes](https://permdock.
|
|
80
|
+
- Docs: [named scopes](https://permdock.com/docs/concepts/scopes), [tenancy](https://permdock.com/docs/concepts/tenancy), [ownership](https://permdock.com/docs/concepts/ownership), [custom roles](https://permdock.com/docs/concepts/custom-roles), [extension interfaces](https://permdock.com/docs/concepts/extension-interfaces).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Ownership and custom roles
|
|
2
2
|
|
|
3
|
-
Owning pages: [ownership](https://permdock.
|
|
3
|
+
Owning pages: [ownership](https://permdock.com/docs/concepts/ownership) and [custom roles](https://permdock.com/docs/concepts/custom-roles).
|
|
4
4
|
|
|
5
5
|
## Role options
|
|
6
6
|
|
|
@@ -48,7 +48,11 @@ const result = permdock.decideRoleChange({
|
|
|
48
48
|
scope: "organization",
|
|
49
49
|
id: "o_acme",
|
|
50
50
|
target: { id: "u_bob", via: "staff", roles: ["member"] }, // their membership there now
|
|
51
|
-
holders:
|
|
51
|
+
holders: await countHolders(memberships, {
|
|
52
|
+
scope: "organization",
|
|
53
|
+
id,
|
|
54
|
+
role: "owner",
|
|
55
|
+
}), // from MembershipSource.list
|
|
52
56
|
});
|
|
53
57
|
// { outcome: 'granted', change, role } or { outcome: 'denied', change, denials }
|
|
54
58
|
```
|
|
@@ -57,7 +61,7 @@ The actor is the instance's own subject; nothing in the change stands in for it.
|
|
|
57
61
|
|
|
58
62
|
For a role on a nested scope that the subject holds nothing at, pass `within` and `{ trusted: true }` only after loading the ancestors from the app's own store.
|
|
59
63
|
|
|
60
|
-
Generated RLS applies the same rules at commit. Map `rls.memberships.scopes.<scope>.via` so the helpers apply `for`, and use `permdock_can_assign(role, organization_id::text)` in the membership table's own insert policy. The SQL is owned by the `permdock-data` skill.
|
|
64
|
+
Generated RLS applies the same rules at commit, with triggers on the scope's `rls.memberships` table or on the tables of its `fromTable` / `fromJunction` sources (base tables, not views). Map `rls.memberships.scopes.<scope>.via` so the helpers apply `for`, and use `permdock_can_assign(role, organization_id::text)` in the membership table's own insert policy, or `permdock_can_assign_any(role, organization_id, 'organization', organization_id::text)` when the role may be a custom one. Set `rls.assignments` to have triggers check every client write to the membership tables, the global-roles table `rls.roles` and an invitations table, and `ownRole: 'refuse'` to refuse a client write to the caller's own row; never keep a hand-written trigger for either. The SQL is owned by the `permdock-data` skill.
|
|
61
65
|
|
|
62
66
|
## Custom roles
|
|
63
67
|
|
|
@@ -68,16 +72,18 @@ type CustomRole = {
|
|
|
68
72
|
id?: string; // pins it to one instance
|
|
69
73
|
name: string;
|
|
70
74
|
includes?: string[]; // declared roles to start from
|
|
71
|
-
grants?: { permission: string; effect?: "allow" | "deny" }[];
|
|
75
|
+
grants?: { permission: string; effect?: "allow" | "deny"; level?: string }[];
|
|
72
76
|
};
|
|
73
77
|
```
|
|
74
78
|
|
|
75
79
|
- The ceiling of a scope is the code allows of the declared roles marked `assignable` in that scope. A custom role never exceeds it, and hosted grants never widen it.
|
|
76
80
|
- An own allow inherits the conditions of the ceiling grants for that permission. To keep a narrower condition ("own posts only"), include the declared role that has it.
|
|
81
|
+
- A resource may declare `levels: { own: { ownerId: principal.id }, team: { teamId: { in: principal.teamIds } }, all: {} }`. A grant `{ permission: 'job.read', level: 'team' }` ANDs that condition into the ceiling grant. An undeclared level, or a level on a collection action, is dropped as `unknown-level` and removes the permission; a level on a deny is `condition-not-allowed`. `permdock.assignableLevels(leaf)` lists the levels the subject may hand out.
|
|
77
82
|
- An own deny removes the permission from this custom role only. Deny it in code when it must win across roles.
|
|
78
|
-
- Pass a `RoleSource` (`memoryRoleSource(roles)` in tests) as `customRoles` on `createPermDock`.
|
|
83
|
+
- Pass a `RoleSource` (`memoryRoleSource(roles)` in tests), or a `RoleSourceFactory` `(subject) => RoleSource` when the source reads one principal's roles, as `customRoles` on `createPermDock` or any adapter. To add every role of a tenant on a role-management request, `await permdock.derive({ customRoles })` instead of building a second instance. Build it with `customRoleSource({ rolesOf })`, where `rolesOf(tenant)` reads every custom role of the tenant, not only the held ones: `assignableRoles` and `decideRoleChange` see only what the source returns. `{ read: 'held', policy }` skips the read on requests that never manage roles. Check it with `testRoleSource(source, { every })`.
|
|
84
|
+
- `permdock.assignableRoles({ tenant })` lists declared roles, then the tenant's custom roles whose every permission and level the subject may hand out at the custom role's scope. `decideRoleChange` assigns and revokes a custom role like a declared one, with no holder count; it inherits `for` and `exclusiveWith` from its included roles.
|
|
79
85
|
- `permdock.assignablePermissions({ tenant })` is the ceiling intersected with what the subject holds. `useAssignablePermissions()` in `permdock/react` builds the editor from it.
|
|
80
|
-
- The save action runs `validateCustomRole(policy, role)` and refuses the role when `ok` is false or `permissions` contains a key outside `assignablePermissions()`. `dropped` names each entry left out (`unknown-permission`, `outside-ceiling`, `condition-not-allowed`, `unknown-role`).
|
|
81
|
-
- A platform custom role (`scope: 'global'`, no `tenant`, `team` or `id`) is held through `principal.roles` and capped by the allows of declared global roles marked `assignable`. `RoleSource.globalRoles()` returns them.
|
|
86
|
+
- The save action runs `validateCustomRole(policy, role)` and refuses the role when `ok` is false or `permissions` contains a key outside `assignablePermissions()`. `dropped` names each entry left out (`unknown-permission`, `outside-ceiling`, `condition-not-allowed`, `unknown-level`, `unknown-role`).
|
|
87
|
+
- A platform custom role (`scope: 'global'`, no `tenant`, `team` or `id`) is held through `principal.roles` and capped by the allows of declared global roles marked `assignable`. `RoleSource.globalRoles({ held })` returns them; `customRoleSource(reader, { read: 'held', policy })` skips that read while every held global role is declared.
|
|
82
88
|
- A stored grant under a key renamed with `definePermissions(..., { renamed })` still resolves; `validateCustomRole` lists it in `renamed` as `{ from, to }`, and `permdock doctor` PD055 prints the `update` that rewrites it.
|
|
83
|
-
- For generated RLS, add `--custom-roles` (or `rls.customRoles: true`) to `permdock rls generate`. Platform roles are rows with `scope = 'global'` and a null `tenant_id`, or the top-level `role_grants` claim in `jwt` mode.
|
|
89
|
+
- For generated RLS, add `--custom-roles` (or `rls.customRoles: true`) to `permdock rls generate`. Platform roles are rows with `scope = 'global'` and a null `tenant_id`, or the top-level `role_grants` claim in `jwt` mode. With levels, `custom_role_permissions` gains a `level` column and the claim writes `key@level`.
|
|
@@ -4,13 +4,13 @@ description: Adds PermDock authorization to a TypeScript app, from definitions t
|
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: ScaleDockHQ
|
|
7
|
-
homepage: https://permdock.
|
|
7
|
+
homepage: https://permdock.com/docs/getting-started/quick-start
|
|
8
8
|
repository: https://github.com/ScaleDockHQ/permdock
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Wire PermDock
|
|
12
12
|
|
|
13
|
-
Three files and one guard: `src/permissions.ts` (definitions), `src/policy.ts` (rules) and the factory file, then one check on the path the user asked for ([quick start](https://permdock.
|
|
13
|
+
Three files and one guard: `src/permissions.ts` (definitions), `src/policy.ts` (rules) and the factory file, then one check on the path the user asked for ([quick start](https://permdock.com/docs/getting-started/quick-start)). Import paths and identifiers follow the [naming convention](https://permdock.com/docs/getting-started/naming).
|
|
14
14
|
|
|
15
15
|
## Inputs (find out, or ask before starting)
|
|
16
16
|
|
|
@@ -87,7 +87,7 @@ Three files and one guard: `src/permissions.ts` (definitions), `src/policy.ts` (
|
|
|
87
87
|
✓ The factory file compiles and exports the adapter's public members.
|
|
88
88
|
5. **Guard.** Add one check on the path the user asked for: `assert` or `protect` / `getPermission` with a permission reference on the server, `<Protected permission={permissions.post.update}>` inside `PermDockProvider` in React.
|
|
89
89
|
✓ That path cannot run without a `granted` decision, and the adapter handles a denial (Problem Details or fallback UI).
|
|
90
|
-
6. **Check.** Run `permdock collect`, `permdock catalog`, `permdock usage` and `permdock doctor`, and add `permdock collect --check` to CI. For a Vite SPA, Expo or another framework without `'use client'`, set `doctor.clientEntries` to the client source globs so PD001 checks them. Add a `describePolicy` suite from `permdock/testing` ([scenario testing](https://permdock.
|
|
90
|
+
6. **Check.** Run `permdock collect`, `permdock catalog`, `permdock usage` and `permdock doctor`, and add `permdock collect --check` to CI. For a Vite SPA, Expo or another framework without `'use client'`, set `doctor.clientEntries` to the client source globs so PD001 checks them. When client code lives outside `collect.srcPath`, list every source folder in `doctor.srcPath` instead of widening the catalog. Add a `describePolicy` suite from `permdock/testing` ([scenario testing](https://permdock.com/docs/guides/scenario-testing)). -> [references/policy-options.md](references/policy-options.md#cli-starting-points)
|
|
91
91
|
✓ Collect and doctor findings are fixed, or the three files typecheck and one matrix test passes.
|
|
92
92
|
|
|
93
93
|
## Next skills
|
|
@@ -113,4 +113,4 @@ Install each with `npx skills add ScaleDockHQ/PermDock --skill <name>` when the
|
|
|
113
113
|
- [references/adapters.md](references/adapters.md): factory shapes for Next.js, HTTP frameworks, RPC frameworks, UI frameworks, AuthZEN, OpenAPI, OpenTelemetry, SCIM, Cloud, Better Auth, Clerk, Supabase, the remote PDP and collect-only frameworks.
|
|
114
114
|
- [references/policy-options.md](references/policy-options.md): limits, disclosure, plans, validity windows, step-up, and CLI starting points (OpenAPI import, Arazzo, Cloud push).
|
|
115
115
|
- The `permdock` skill: the mental model, `explain` for an unexpected denial, and the docs MCP.
|
|
116
|
-
- Docs: [policies](https://permdock.
|
|
116
|
+
- Docs: [policies](https://permdock.com/docs/concepts/policies), [conditions](https://permdock.com/docs/concepts/conditions), [decisions](https://permdock.com/docs/concepts/decisions), [adapters](https://permdock.com/docs/adapters).
|
|
@@ -21,30 +21,27 @@ export const {
|
|
|
21
21
|
});
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
-
Needs Next.js 16.3 or later. Guard with `getPermission(permissions.post.update, post)` or `assert`. For a page or Server Action that must stop on a denial, `await requireAccess({ permission, data, tenant })` calls `forbidden()` (or `unauthorized()` for a signed-out user); enable `experimental.authInterrupts` and add `app/forbidden.tsx` and `app/unauthorized.tsx`. When only part of a page is gated, let a Server Component `assert` inside `<PermissionBoundary denied={...} approval={...}>` from `permdock/next/client` (with a `Suspense` inside it); `usePermissionBoundary()` in the fallback gives the permission, the approval `token` and `retry()`. Client components use `permdock/react` inside the server `PermDockProvider`, which never awaits: it streams a `snapshotPromise
|
|
24
|
+
Needs Next.js 16.3 or later. Guard with `getPermission(permissions.post.update, post)` or `assert`. For a page or Server Action that must stop on a denial, `await requireAccess({ permission, data, tenant })` calls `forbidden()` (or `unauthorized()` for a signed-out user); enable `experimental.authInterrupts` and add `app/forbidden.tsx` and `app/unauthorized.tsx`. When only part of a page is gated, let a Server Component `assert` inside `<PermissionBoundary denied={...} approval={...}>` from `permdock/next/client` (with a `Suspense` inside it); `usePermissionBoundary()` in the fallback gives the permission, the approval `token` and `retry()`. Client components use `permdock/react` inside the server `PermDockProvider`, which never awaits: it streams a `snapshotPromise`, and permission hooks answer `pending` until it lands (add `suspend` to make them suspend).
|
|
25
25
|
|
|
26
|
-
With `cacheComponents`, when permission UI (nav items, row actions) must be prefetched, the app owns the cache. Never put `'use cache'` inside PermDock calls, and never read `headers()` / `cookies()` in a function you mean to cache outside `'use cache: private'`:
|
|
26
|
+
With `cacheComponents`, when permission UI (nav items, row actions) must be prefetched, the app owns the cache directive and `getSnapshot` fills it. Without the factory, call `snapshotFor` and set `cacheLife(cacheLifeFor(snapshot))` and `cacheTag(snapshotTag(sub))` yourself. Never put `'use cache'` inside PermDock calls, and never read `headers()` / `cookies()` in a function you mean to cache outside `'use cache: private'`:
|
|
27
27
|
|
|
28
28
|
```tsx
|
|
29
29
|
// src/permdock/snapshot.ts
|
|
30
|
-
import {
|
|
31
|
-
import { snapshotFor } from 'permdock';
|
|
32
|
-
import { cacheLifeFor, snapshotTag } from 'permdock/next';
|
|
30
|
+
import { getSnapshot } from './server'; // the createPermDock factory from permdock/next
|
|
33
31
|
|
|
34
32
|
export async function loadSnapshot(org: string) {
|
|
35
33
|
'use cache: private';
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
cacheLife(cacheLifeFor(snapshot));
|
|
39
|
-
cacheTag(snapshotTag(claims?.sub));
|
|
40
|
-
return snapshot;
|
|
34
|
+
// Sets cacheLife(cacheLifeFor(snapshot)) and cacheTag(snapshotTag(sub), ...tags) in this scope.
|
|
35
|
+
return getSnapshot({ tenant: org, tags: [`org:${org}`] });
|
|
41
36
|
}
|
|
42
37
|
|
|
43
38
|
// app/[org]/layout.tsx: keep it synchronous
|
|
44
39
|
<PermDockProvider snapshotPromise={params.then(({ org }) => loadSnapshot(org))}>
|
|
45
40
|
```
|
|
46
41
|
|
|
47
|
-
|
|
42
|
+
Hooks under a `snapshotPromise` never suspend: they deny with `status: 'pending'` until it resolves, so render a skeleton for `pending` (or `<Protected pending>`); pass `suspend` to the provider only when readers should wait inside a Suspense boundary.
|
|
43
|
+
|
|
44
|
+
After a role change: `updateTag(snapshotTag(user))` in the Server Action; `revalidateTag(tag, { expire: 0 })` in a Route Handler (not `'max'`, which would keep serving the revoked grant). Pages whose permission UI must be instant export `instant = true`; a rarely visited admin page exports `prefetch = 'force-disabled'`. Resource links that should carry their gated actions use `<Link prefetch={true}>` with the check in a `'use cache: private'` function keyed on the resource id. In `proxy.ts`, use `mayAccess(policy, claims, permission, { tenant })` (optimistic, never a decision). Both reach only the acting browser; for other members, add an app-owned signal (Realtime, poll, SSE) that calls `router.refresh()`. Keep the `[org]` layout synchronous and never read `cookies()` outside the private-cached loader. With a slug in the URL (`[orgSlug]`), resolve it to the org id in a `'use cache'` lookup that reads no session, call `notFound()` for an unknown slug, and pass the id to `snapshotFor`, `requireAccess` and `getPermDock`. Export `const { POST, GET } = permdockHandler()` from `app/api/permdock/route.ts`, or pass `endpoint: false` when every client check is portable; then a closure grant read by `usePermission` is denied with reason `server-only` (`permdock doctor` PD044 warns). Guide: [Next.js Cache Components](https://permdock.com/docs/guides/next-cache-components).
|
|
48
45
|
|
|
49
46
|
## Hono — `permdock/hono`
|
|
50
47
|
|
|
@@ -87,6 +84,8 @@ app.get(
|
|
|
87
84
|
|
|
88
85
|
Publish `revocations.revoke({ principal, tenant, kind: 'changed' })` from your role-edit code so open connections revalidate.
|
|
89
86
|
|
|
87
|
+
For a framework loader (React Router, TanStack Start, SvelteKit, Nuxt), `createPermDock` from `permdock/server` returns `getSnapshot(request, { tenant })`: the snapshot a client provider takes, built from the request's own subject. Send it with `snapshotHeaders(snapshot)` from `permdock/server`, which sets a private `Cache-Control` from the snapshot's expiry. Docs: [loader snapshots](https://permdock.com/docs/adapters/server-kernel).
|
|
88
|
+
|
|
90
89
|
## React (Vite) — `permdock/react`
|
|
91
90
|
|
|
92
91
|
No factory. The server builds a snapshot (`permdock.snapshot()` or `fromSnapshot` on snapshot JSON) and the client wraps the tree:
|
|
@@ -125,6 +124,8 @@ const { describe, securitySchemes, overlay } = createPermDock(policy, {
|
|
|
125
124
|
|
|
126
125
|
`describe(permission)` returns `security` plus `x-permdock-permissions`. `overlay({ version: '1.2' })` emits the pinned Overlay 1.2 draft. `target: '3.3'` emits the pinned Security Profile draft next to `x-permdock-securityProfile`. `scheme.type: 'gnap'` throws and emits nothing. CLI: `permdock openapi emit --doc openapi.json`; add `--arity` when a generated MCP server or SDK needs `x-permdock-arity` (instance or collection, and the id path parameter).
|
|
127
126
|
|
|
127
|
+
In a contract package that must not import the policy, use `securityFor(permission, { scheme?, anyOf? })` and `permissionsExtension(permissions)` from `permdock/openapi`: they take permission references only and return the same `security` and `x-permdock-permissions` as the server's `openapi.security(permission)`. They never mark a permission public and omit `x-permdock-conditions` and `x-permdock-approval`; those need the policy.
|
|
128
|
+
|
|
128
129
|
## React Native — `permdock/react-native`
|
|
129
130
|
|
|
130
131
|
No factory. Same hooks and `<Protected>` as `permdock/react`, plus `storage` so Expo Router `Stack.Protected` can answer on the first frame:
|
|
@@ -133,7 +134,11 @@ No factory. Same hooks and `<Protected>` as `permdock/react`, plus `storage` so
|
|
|
133
134
|
import { PermDockProvider, usePermission } from "permdock/react-native";
|
|
134
135
|
```
|
|
135
136
|
|
|
136
|
-
`storage` is `{ getItem, setItem, removeItem }` (
|
|
137
|
+
`subjectId` is required: the signed-in user's id, or `null` when signed out. A stored snapshot of another principal is cleared. `storage` is `{ getItem, setItem, removeItem }`; `secureStoreStorage(SecureStore)` and `mmkvStorage(mmkv)` wrap Expo SecureStore and MMKV. `snapshotUrl` revalidates in the background; `subscribeForeground={appStateForeground(AppState)}` revalidates on return to the foreground. For Expo Router guards use `usePermissionGuard(permission, data?)`, which is `false` while `pending`, and `useSnapshotReady()` for a splash screen. `permdock.clear()` drops the persisted snapshot on sign-out. Do not import `policy.ts` on the client.
|
|
138
|
+
|
|
139
|
+
To answer guards from synced rows, write `localSnapshotManifest(policy)` (from `permdock`) to a JSON file at build time and pass `source={localSnapshot({ manifest, read, subscribe })}`. `read()` returns `{ principal: { id, tenant, roles, memberships, attributes }, customRoles }` from the local database; `subscribe` re-reads on row changes. It decides like the server snapshot except relation grantees (server-only) and the assignable lists (empty). Never import `policy.ts` into the app for this. Docs: [local snapshot](https://permdock.com/docs/adapters/react-native#local-snapshot).
|
|
140
|
+
|
|
141
|
+
For an offline app on PowerSync, set `powersync: { out: 'sync-config.yaml' }` in `permdock.config.ts` and run `permdock powersync generate`: one edition 3 Sync Stream per resource, from the `rls.tables` and `rls.memberships` the RLS generator reads. It under-syncs, never over-syncs: a resource with a deny grant gets no stream, and a grant with a global role, approval, break-glass, validity, request context or a condition without a Sync Streams form syncs nothing, each with a warning. The app reads what does not sync from the server. It also writes `permdock_*` streams for the user's own membership, global-role and custom-role rows; with `powersync.manifest` set it writes the manifest too, and `powersyncSource(db, { manifest, queries, read })` from `permdock/react-native` builds the local snapshot from `db.watch` queries. `permdock powersync verify --db $DATABASE_URL` fails when a stream holds a fixture row the policy denies; doctor PD058 flags a stale file or manifest. Doctor PD065 lists server-only grants read from React Native code, which the device denies offline. Docs: [`permdock powersync`](https://permdock.com/docs/cli/powersync).
|
|
137
142
|
|
|
138
143
|
## Express — `permdock/express`
|
|
139
144
|
|
|
@@ -153,7 +158,7 @@ app.delete(
|
|
|
153
158
|
app.use(errorHandler());
|
|
154
159
|
```
|
|
155
160
|
|
|
156
|
-
Converts `IncomingMessage` to Fetch, then delegates to `permdock/server`. Denials are `403 application/problem+json`; anonymous callers get `401` plus `WWW-Authenticate`.
|
|
161
|
+
Converts `IncomingMessage` to Fetch, then delegates to `permdock/server`. Denials are `403 application/problem+json`; anonymous callers get `401` plus `WWW-Authenticate`. A PermDock error thrown by `permdock()`, `protect` or the decision routes becomes Problem Details; any other error goes to `next(err)`.
|
|
157
162
|
|
|
158
163
|
## Fastify — `permdock/fastify`
|
|
159
164
|
|
|
@@ -176,7 +181,7 @@ app.delete(
|
|
|
176
181
|
);
|
|
177
182
|
```
|
|
178
183
|
|
|
179
|
-
Registers a `fastify-plugin`-style root plugin (`skip-override`) that decorates `request.permdock`. Denials are `403 application/problem+json`; anonymous callers get `401` plus `WWW-Authenticate`.
|
|
184
|
+
Registers a `fastify-plugin`-style root plugin (`skip-override`) that decorates `request.permdock`. A route with `config: { permdock: false }` (a health check) builds no instance. Denials are `403 application/problem+json`; anonymous callers get `401` plus `WWW-Authenticate`.
|
|
180
185
|
|
|
181
186
|
## Elysia — `permdock/elysia`
|
|
182
187
|
|
|
@@ -201,13 +206,16 @@ Fetch-native plugin via `derive`. Denials are `403 application/problem+json`; an
|
|
|
201
206
|
```ts
|
|
202
207
|
import { createPermDock } from "permdock/nest";
|
|
203
208
|
|
|
204
|
-
export const { PermDockModule,
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
209
|
+
export const { PermDockModule, Protect, InjectPermDock } = createPermDock(
|
|
210
|
+
policy,
|
|
211
|
+
{ subject: (request) => request.user ?? null },
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
@Module({ imports: [PermDockModule.forRoot({ guard: "global" })] })
|
|
215
|
+
export class AppModule {}
|
|
208
216
|
```
|
|
209
217
|
|
|
210
|
-
|
|
218
|
+
`forRoot({ guard: 'global' })` registers `PermDockGuard` as `APP_GUARD`. `Protect` attaches a permission (and optional loader) to a handler or class; `Protect(null)` only requires a principal. Rules sharing one loader function run it once per request. Without decorator syntax (erasable TypeScript only), apply decorators with `decorateMethod(Controller, 'update', Patch(':id'), Protect(permissions.post.update))`. Denials are `403 application/problem+json`; anonymous callers get `401` plus `WWW-Authenticate`. Works on `@nestjs/platform-express` and `@nestjs/platform-fastify`. `permdockHandler({ path: ':org/permdock' })` mounts the decision controller where you choose. In a gateway, call `connection(client, client.request, { permission })` in `handleConnection` and `.close()` in `handleDisconnect`; `PermDockGuard` then checks each `@SubscribeMessage` through it.
|
|
211
219
|
|
|
212
220
|
## Node — `permdock/node`
|
|
213
221
|
|
|
@@ -258,7 +266,7 @@ base
|
|
|
258
266
|
.handler(({ context }) => deletePost(context.permdockData));
|
|
259
267
|
```
|
|
260
268
|
|
|
261
|
-
`protect` throws `ORPCError` (`FORBIDDEN`, `BAD_REQUEST`, `UNAUTHORIZED`) with Problem Details as `data`, and so does `context.permdock.assert` in a handler. Read the tenant from each procedure's input. `openapi.protect` is the same guard; pass `openapi.security(permission)` to oRPC 2's `openapi({ spec })` metadata helper. Event iterators behind `protect(permission, load, { items })` end and filter like tRPC subscriptions.
|
|
269
|
+
`protect` throws `ORPCError` (`FORBIDDEN`, `BAD_REQUEST`, `UNAUTHORIZED`) with Problem Details as `data`, and so does `context.permdock.assert` in a handler. Read the tenant from each procedure's input. `openapi.protect` is the same guard; pass `openapi.security(permission)` to oRPC 2's `openapi({ spec })` metadata helper. Event iterators behind `protect(permission, load, { items })` end and filter like tRPC subscriptions. `protect` also attaches to `implement(contract)` procedures, including ones with a declared `output`. Declare `oc.errors({ FORBIDDEN: { status: 403, data: problemDetails } })` with `problemDetails` from `permdock/openapi` and `protect` throws through that constructor: clients get a defined error with `data: ProblemDetails`, and the generated OpenAPI documents the 403 body.
|
|
262
270
|
|
|
263
271
|
## Vue — `permdock/vue`
|
|
264
272
|
|
|
@@ -268,7 +276,7 @@ import { permdockPlugin, Protected, usePermission } from "permdock/vue";
|
|
|
268
276
|
createApp(App).use(permdockPlugin, { snapshot, endpoint: "/api/permdock" });
|
|
269
277
|
```
|
|
270
278
|
|
|
271
|
-
Composables return refs (`allowed`, `status`, `decision`). Do not import `policy.ts` on the client.
|
|
279
|
+
Composables return refs (`allowed`, `status`, `decision`). `PermissionBoundary` catches `PermDockDeniedError` and `PermDockApprovalRequiredError` thrown below it and renders its `denied` or `approval` slot. Do not import `policy.ts` on the client.
|
|
272
280
|
|
|
273
281
|
## Svelte — `permdock/svelte`
|
|
274
282
|
|
|
@@ -279,7 +287,7 @@ setPermDock({ snapshot, endpoint: "/api/permdock" });
|
|
|
279
287
|
const canEdit = permission(permissions.post.update, () => post);
|
|
280
288
|
```
|
|
281
289
|
|
|
282
|
-
Stores are readable (`$canEdit.allowed`). Do not import `policy.ts` on the client.
|
|
290
|
+
Stores are readable (`$canEdit.allowed`). `<PermissionBoundary>` (Svelte 5.3 or later) renders its `denied` or `approval` snippet for a denial thrown below it. Do not import `policy.ts` on the client.
|
|
283
291
|
|
|
284
292
|
## Solid — `permdock/solid`
|
|
285
293
|
|
|
@@ -291,7 +299,7 @@ import { PermDockProvider, Protected, usePermission } from 'permdock/solid';
|
|
|
291
299
|
</PermDockProvider>
|
|
292
300
|
```
|
|
293
301
|
|
|
294
|
-
`usePermission` takes an accessor for instance data (`() => post`). Do not import `policy.ts` on the client.
|
|
302
|
+
`usePermission` takes an accessor for instance data (`() => post`). `PermissionBoundary` renders its `denied` or `approval` prop for a denial thrown below it. Do not import `policy.ts` on the client.
|
|
295
303
|
|
|
296
304
|
## OpenTelemetry — `permdock/otel`
|
|
297
305
|
|
|
@@ -343,7 +351,6 @@ export const { getPermDock } = createPermDock(policy, {
|
|
|
343
351
|
subject,
|
|
344
352
|
store: permdockCloud.approvals,
|
|
345
353
|
sink: permdockCloud.sink,
|
|
346
|
-
snapshots: permdockCloud.snapshots,
|
|
347
354
|
});
|
|
348
355
|
```
|
|
349
356
|
|
|
@@ -366,7 +373,7 @@ const permdock = await createPermDock(policy, subject, {
|
|
|
366
373
|
});
|
|
367
374
|
```
|
|
368
375
|
|
|
369
|
-
Pass the server `getSession` result only. A null session is anonymous. Never call `hasPermission` on the request path.
|
|
376
|
+
Pass the server `getSession` result only. A null session is anonymous. Additional user fields reach `principal.claims` only through `schema`, because a user may edit their own. Memberships come from one `member` query through `auth.$context`. Never call `hasPermission` on the request path.
|
|
370
377
|
|
|
371
378
|
## Clerk — `permdock/clerk`
|
|
372
379
|
|
|
@@ -378,7 +385,7 @@ export const { getPermDock } = createPermDock(policy, {
|
|
|
378
385
|
});
|
|
379
386
|
```
|
|
380
387
|
|
|
381
|
-
Pass `auth()` or a verified session payload only. A plain `{ userId }` object is anonymous. `memberships: 'all'` loads organizations through the Clerk Backend API. `o:` plans and features from `pla` and `fea` hold only in the session organization.
|
|
388
|
+
Pass `auth()` or a verified session payload only. A plain `{ userId }` object is anonymous. `memberships: 'all'` loads organizations through the Clerk Backend API; create `createClerkSubjectResolver({ memberships: 'all', backend, cache: { ttl: '10s' } })` once per process to cache each user's list (at most 30 seconds, so a removed membership grants until then). `o:` plans and features from `pla` and `fea` hold only in the session organization.
|
|
382
389
|
|
|
383
390
|
## Supabase — `permdock/supabase`
|
|
384
391
|
|
|
@@ -393,20 +400,25 @@ const subject = subjectFromSupabase(claims, {
|
|
|
393
400
|
});
|
|
394
401
|
```
|
|
395
402
|
|
|
396
|
-
Write the claims with `permdock supabase hook generate`; `permdock supabase inspect --json` prints the manifest (helper names, tenant claim, budget, claims written)
|
|
403
|
+
Write the claims with `permdock supabase hook generate`; `permdock supabase inspect --json` prints the manifest (helper names, tenant claim, budget, claims written) which `authorizationProvider` from `permdock/better-supabase` turns into better-supabase's `authorization` config. No `@supabase/supabase-js` peer. Pair with `permdock rls generate`, or keep SQL as authority and run `permdock rls verify --db` against `sqlFunction` twins ([database-first](https://permdock.com/docs/adapters/rls)); the generic RLS workflow is in the `permdock-data` skill.
|
|
397
404
|
|
|
398
|
-
With better-supabase, map its session with `
|
|
405
|
+
With better-supabase, set `authorization: authorizationProvider({ manifest, catalog })` in `better-supabase.config.ts` (with `rls.customRoles` its `canAssign` and `canAssignFor` call `permdock_can_assign_any` and its `_for` form, so custom roles are assignable through better-supabase; regenerate the manifest after turning custom roles on), build bucket and topic `access` policies with `bucketPolicy` and `topicPolicy`, and map its session with `subjectFromBetterSupabase(session)` (all from `permdock/better-supabase`): the active tenant's `features` become `principal.plans`. Pass `plans: { keys }` when better-supabase's `entitlements.claim.keys` shortens feature keys, and `apiKeys: { permissions, manifest }` so an `apiKey` session maps to the key's credential subject instead of the anonymous one. For `createMcp` tools, put the permission in `meta` and spread `toolPolicy({ permdock })` into the options: tools without one are hidden and refused, and approval-required calls are refused because `createMcp` collects no approvals. Wrap a `CredentialProvider` with `credentialGuard(provider, { permdock, use, revoke? })` so PermDock decides each token use. `authorizationProvider({ ..., approver })` lets holders of that permission approve AI tool calls, never their own. better-supabase's `createAgentRuntime({ toolApproval })` takes `toolApproval` from `permdock/ai-sdk` as it is. Validate its claims with `betterSupabase.claims(supabaseClaims().extend(appSchema))` (`supabaseClaims` from `permdock/supabase`, any Standard Schema as `appSchema`) instead of a hand-written copy of the claim shape. A `supabase.hook.claims` function that needs the user's organizations calls `<schema>.member_organization_ids_for(user_id)` (generated by `rls generate`, executable by `supabase_auth_admin` only) instead of querying the membership tables itself. A token Supabase's OAuth server issued to a third-party app (`client_id`) becomes an `oauth-client` actor limited to its `scope`; `deny(permission, { to: actor('oauth-client') })` keeps such apps out, in the app and, through `rls generate`, in Postgres. A better-supabase support session (`act.kind: 'support'`) is actor kind `support` with `sessionId` and `readOnly`, and `actingAs` (`act.kind: 'impersonation'`) is kind `impersonation`; neither has a delegation, so they reach nothing until a policy delegation names `actor('support')` or `actor('impersonation')`, and `readOnly: true` (also when `read_only` is absent) narrows that to read-only permissions. Generated RLS reads memberships, not `act`, so a support token sent to the Data API writes as the user: add a restrictive `for update` (and `insert` / `delete`) policy per writable table that requires `act ->> 'read_only' = 'false'` when `act` is a support or session level ([support access](https://permdock.com/docs/guides/support-access)). With `rls.anonymousSignIns: 'deny'`, pass `anonymousSignIns: 'deny'` to the subject mapper too (doctor PD057). Check a deployed database against the generated policies with `permdock rls verify --introspect --db $DATABASE_URL`.
|
|
399
406
|
|
|
400
|
-
`claims` is whatever Supabase verified: `data.claims` from `supabase.auth.getClaims()` on an `@supabase/ssr` server client, or `jwtClaims` from `@supabase/server` (`ctx.jwtClaims` in `withSupabase`, `c.var.
|
|
407
|
+
`claims` is whatever Supabase verified: `data.claims` from `supabase.auth.getClaims()` on an `@supabase/ssr` server client, or `jwtClaims` from `@supabase/server` (`ctx.jwtClaims` in `withSupabase` or a `pipeline`, and `c.var.jwtClaims` after a framework bridge such as `toHono([withClaims(), withPermDock()])`). `@supabase/server/adapters/*` and `c.var.supabaseContext` are deprecated and removed on 1 December 2026; do not write new code against them ([Hono, H3, Elysia and NestJS](https://permdock.com/docs/adapters/supabase#hono-h3-elysia-and-nestjs)). Pass `null` for anonymous callers; never `getSession().access_token` (unverified). An API-key auth mode (`secret`, `publishable`) has `jwtClaims: null` and is the anonymous subject, not a user.
|
|
401
408
|
|
|
402
409
|
Share links reach RLS through `exchangeCapability`; see the `permdock-credentials` skill.
|
|
403
410
|
|
|
404
411
|
Supabase specifics for generated RLS (the generic flags and parity checks are in the `permdock-data` skill):
|
|
405
412
|
|
|
413
|
+
- Point `--seeds-out` at the migrations directory (`--seeds-out supabase/migrations/`) so `rls generate` writes a new timestamped seeds migration only when the `role_permissions` rows change; never rewrite an applied seeds migration in place.
|
|
406
414
|
- With declarative schemas on pg-delta (`[experimental.pgdelta] enabled = true`), run `permdock rls generate --target sql --split helpers,seeds,indexes,policies,hook --seeds-out supabase/migrations/<timestamp>_permdock_seeds.sql` with no `--out`: it writes `supabase/schemas/permdock/{helpers,indexes}.sql`, `public/policies/permdock.sql` and `permdock/functions/custom_access_token_hook.sql`, grants included. Then `supabase db schema declarative sync -f <name>` writes the migration; the seeds migration must sort after it. Never put the `role_permissions` rows in a schema file: pg-delta rejects data.
|
|
407
|
-
- With `supabase db diff` instead, generate `--split helpers,seeds,policies,hook --out supabase/schemas/identity/056_permdock_{part}.sql` and write the
|
|
415
|
+
- With `supabase db diff` instead, generate `--split helpers,seeds,policies,hook --out supabase/schemas/identity/056_permdock_{part}.sql` and write the schema and function privileges and the `security_invoker` view option with `--grants-out` into a migration created by `supabase migration new` after the first `db diff`; `db diff` drops those, so never append them to a migration by hand. Table grants and policies stay in the schema files, where `db diff` keeps them. Keep the helpers part ahead of every file that calls the helpers in `schema_paths` (`permdock doctor` PD042, PD043).
|
|
408
416
|
- `--authorize jwt` reads the hook's claims: enable the printed `[auth.hook.custom_access_token]` stanza in `supabase/config.toml`. The generated hook writes the canonical `memberships` claim from the `rls.memberships.scopes` tables, so do not hand-write one.
|
|
409
417
|
- For more than one membership table, declare `fromTable` / `fromJunction` sources once (from `permdock/supabase`, with a `query` on the server), pass the array as `memberships` (wrapped in `claimsFirst(sources, { version: authzVersion({ query }) })` to trust the token until it is truncated or stale), put the same sources under `supabase.hook.memberships` in `permdock.config.ts`, and run `permdock supabase hook generate`; add the printed `config.toml` block (`jwt_expiry = 900`). List member-removal and payout permissions in `definePolicy({ fresh })`.
|
|
418
|
+
- When the server has only supabase-js (no `SqlQuery`), never re-read memberships, global roles or custom roles with the admin client: grant `subject_for(p_user)` and `authz_version_for(p_user)` (written next to the hook) to the backend role, expose them through `public` wrappers when `permdock` is not an exposed schema, and use `postgrestSources(admin, { schema, fn, versionFn, policy })`: `claimsFirst(sources.memberships)` (with `onStale: 'reread'` a fresh token costs one version call, and with `policy` a token that claims a custom role costs one `subject_for` call), `customRoles: sources.customRoles(principal)`, and `await sources.subject(userId)` for a job acting for a stored user.
|
|
419
|
+
- When a role table stores a role id (`user_roles.role_id`, `organization_users.role_id` referencing `roles`), read the key through it with `{ through: 'roles', on: { role_id: 'id' }, column: 'key' }`: in `rls.roles`, in `fromTable` `columns.role` or `fromJunction` `roles`, and in an `rls.memberships` table's `role`. Never copy keys into the membership table. Keep tenant custom-role keys apart from declared role names.
|
|
420
|
+
- When one membership row holds roles in several columns (a `tier` and a `role_id`), list them instead of building a union view: `role: ['tier', { through: 'roles', on: { role_id: 'id' }, column: 'key' }]` in `rls.memberships` and `fromTable` `columns.role`, `roles: { sources: [...] }` in `fromJunction`. The row holds every non-null key, and the holder-count triggers stay on the real table.
|
|
421
|
+
- When a membership table names a profile instead of a login (`customer_contacts.contact_profile_id` referencing `contact_profiles`, whose `user_id` is the login), read the user through it with `fromJunction` `user` or `fromTable` `columns.user`: `{ through: 'contact_profiles', on: { contact_profile_id: 'id' }, column: 'user_id' }`. Never copy `user_id` onto the membership rows. Keep `contact_profiles.user_id` server-owned (PD028).
|
|
410
422
|
- For attribute conditions (`principal.claims.attrs.region`), list server-owned columns or `app_metadata.<key>` in `supabase.hook.attrs`, never `user_metadata`, and keep clients from updating those columns (`permdock doctor` PD028).
|
|
411
423
|
|
|
412
424
|
## Supabase middleware pipeline — `permdock/supabase/middleware`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Policy options and CLI starting points
|
|
2
2
|
|
|
3
|
-
Owning pages: [policies](https://permdock.
|
|
3
|
+
Owning pages: [policies](https://permdock.com/docs/concepts/policies), [decisions](https://permdock.com/docs/concepts/decisions), [CLI](https://permdock.com/docs/cli).
|
|
4
4
|
|
|
5
5
|
## Usage limits
|
|
6
6
|
|
|
@@ -18,6 +18,8 @@ On a resource whose ids must not confirm a row exists (a private repository, a p
|
|
|
18
18
|
|
|
19
19
|
Gate a paid feature with `to: [roles.admin, plans.pro]` (an array is an intersection). A held role without the plan is denied with `not-entitled`; the API answers `403` `/not-entitled` with `plans`, and `describe(decision)` returns `kind: 'upgrade'` with the same `plans` for an upgrade link.
|
|
20
20
|
|
|
21
|
+
Localise `describe` with `messages`. `messages.reasons` is a record per denial reason or a function `(reason, decision) => string | undefined` that also sees the denied decision; `undefined` keeps the reason code.
|
|
22
|
+
|
|
21
23
|
## Validity windows
|
|
22
24
|
|
|
23
25
|
Access that ends on a date (a contractor's engagement, a change freeze until launch) is `validFrom` / `validUntil` on the grant, RFC 3339 or Unix seconds. Outside the window an allow denies with `inactive-grant` and a deny does not apply; `where()` and generated RLS drop it, and `simulate(checks, { now })` previews a date. A person's access ending is the membership's `expiresAt` (see the `permdock-tenancy` skill).
|
|
@@ -28,7 +30,7 @@ Hardware-key step-up is `to: assurance({ amr: ['hwk'], maxAge: 300 })` and rende
|
|
|
28
30
|
|
|
29
31
|
## Existing apps
|
|
30
32
|
|
|
31
|
-
Adopting PermDock in an app with its own keys, SQL helpers and tokens is a step-by-step guide ([existing apps](https://permdock.
|
|
33
|
+
Adopting PermDock in an app with its own keys, SQL helpers and tokens is a step-by-step guide ([existing apps](https://permdock.com/docs/getting-started/existing-apps)):
|
|
32
34
|
|
|
33
35
|
- Keep the app's keys. Map verbs such as `view` to SQL with `rls.actions`, and give colliding resources a distinct `name` on `resource()`.
|
|
34
36
|
- Put application-owned metadata in `meta.x`; PermDock carries it and never reads it.
|
|
@@ -40,4 +42,4 @@ Adopting PermDock in an app with its own keys, SQL helpers and tokens is a step-
|
|
|
40
42
|
- When the app already has an OpenAPI document and no definitions yet, start from `permdock openapi import --doc <doc> --out src/permissions.generated.ts --schema zod` and review each action's `meta.inferredFrom`.
|
|
41
43
|
- When the repo has Arazzo workflows, run `permdock arazzo check --doc <arazzo> --openapi <doc>`.
|
|
42
44
|
- When the app uses PermDock Cloud, add `permdock cloud push` to the deploy step after the deploy, with `PERMDOCK_CLOUD_URL` and `PERMDOCK_CLOUD_KEY` from CI secrets. The Cloud is optional and never on the decision path.
|
|
43
|
-
- `permdock skills install` copies these skills into the project's agent folders.
|
|
45
|
+
- `permdock skills install` copies these skills into the project's agent folders, writing a symlinked folder once through its target; `permdock skills install --check` in CI fails when the installed skills differ from the package version.
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { n as isReadonlyArray } from "./compact-y6vfu2_f.js";
|
|
2
|
-
//#region src/core/approvers.ts
|
|
3
|
-
/** A named approver for `approval.by`, matched on the approver's principal id. */
|
|
4
|
-
function user(id) {
|
|
5
|
-
if (typeof id !== "string" || id === "") throw new Error("PermDock: user() needs a non-empty principal id");
|
|
6
|
-
return Object.freeze({
|
|
7
|
-
kind: "user",
|
|
8
|
-
id
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
function isUserApprover(value) {
|
|
12
|
-
return value !== null && typeof value === "object" && "kind" in value && value.kind === "user";
|
|
13
|
-
}
|
|
14
|
-
function flattenApprovers(input) {
|
|
15
|
-
if (input === void 0) return [];
|
|
16
|
-
if (isReadonlyArray(input)) return input;
|
|
17
|
-
return [input];
|
|
18
|
-
}
|
|
19
|
-
//#endregion
|
|
20
|
-
export { isUserApprover as n, user as r, flattenApprovers as t };
|
package/dist/catalog-idsHc0vo.js
DELETED