permdock 0.0.0 → 0.1.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 +268 -1
- package/dist/a2a/index.d.ts +178 -0
- package/dist/a2a/index.js +271 -0
- package/dist/ai-sdk/index.d.ts +74 -0
- package/dist/ai-sdk/index.js +137 -0
- package/dist/approval-required-TzNB9oQA.js +6 -0
- package/dist/approvals/index.d.ts +3 -0
- package/dist/approvals/index.js +4 -0
- package/dist/approvals-DaxY-Bdg.js +218 -0
- package/dist/approvers-aH0PJkmf.js +20 -0
- package/dist/arazzo-CTeVuXr5.js +70 -0
- package/dist/arazzo-aoTkqYsJ.js +54 -0
- package/dist/authzen/index.d.ts +48 -0
- package/dist/authzen/index.js +336 -0
- package/dist/better-auth/index.d.ts +92 -0
- package/dist/better-auth/index.js +324 -0
- package/dist/budget-JqcRqyhK.d.ts +11 -0
- package/dist/budget-NbEtbJC_.js +11 -0
- package/dist/capability-B68OFnZx.js +171 -0
- package/dist/capability-BDG_nkxt.d.ts +99 -0
- package/dist/catalog/index.d.ts +12 -0
- package/dist/catalog/index.js +2 -0
- package/dist/catalog-B0E6bggc.js +48 -0
- package/dist/catalog-H-vFGwWK.js +55 -0
- package/dist/catalog-doc-BSo_Q_-6.js +298 -0
- package/dist/catalog-idsHc0vo.js +2 -0
- package/dist/catalog-path-CnI0XTFK.js +12 -0
- package/dist/claude-agent/index.d.ts +81 -0
- package/dist/claude-agent/index.js +83 -0
- package/dist/clerk/index.d.ts +56 -0
- package/dist/clerk/index.js +280 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +20 -0
- package/dist/cli/index.d.ts +46 -0
- package/dist/cli/index.js +5 -0
- package/dist/cloud/index.d.ts +91 -0
- package/dist/cloud/index.js +390 -0
- package/dist/cloud-BNQTrmrr.js +50 -0
- package/dist/cloud-Cuxr6Czw.js +117 -0
- package/dist/collect-B7_TIfmg.js +49 -0
- package/dist/collect-O-tOtbnk.js +350 -0
- package/dist/collect-fRPAQIWb.js +2 -0
- package/dist/commands-BMh_Etm2.js +93 -0
- package/dist/compact-y6vfu2_f.js +19 -0
- package/dist/compare-pfyVnBjE.js +12 -0
- package/dist/compile-CQbvyIBr.d.ts +30 -0
- package/dist/compile-D22iwvuV.js +558 -0
- package/dist/config-BPYmjTOp.js +26 -0
- package/dist/connection-JvqwHODC.js +194 -0
- package/dist/context-CKEAabd7.js +73 -0
- package/dist/context-gtAwAILx.js +6 -0
- package/dist/convex/index.d.ts +40 -0
- package/dist/convex/index.js +55 -0
- package/dist/create-BKF3dgFk.d.ts +162 -0
- package/dist/create-CSkhDU9T.js +222 -0
- package/dist/credential-B_rkUNlY.js +326 -0
- package/dist/credentials-BgRX3WEg.js +268 -0
- package/dist/describe-Bt1YD5uS.js +99 -0
- package/dist/describe-DernyItf.d.ts +37 -0
- package/dist/diff-BUzf5Poi.js +48 -0
- package/dist/diff-x5Il8LVa.js +418 -0
- package/dist/digest-B3kc_PgQ.d.ts +19 -0
- package/dist/digest-BI-6aa6d.js +27 -0
- package/dist/doctor-BGle8Gqi.js +1314 -0
- package/dist/doctor-Bqo6FwUa.js +53 -0
- package/dist/drizzle/index.d.ts +72 -0
- package/dist/drizzle/index.js +160 -0
- package/dist/duration-xK2jGFVc.js +25 -0
- package/dist/elysia/index.d.ts +71 -0
- package/dist/elysia/index.js +103 -0
- package/dist/errors-DSMnAVNa.js +43 -0
- package/dist/errors-DbZ1YOnb.d.ts +138 -0
- package/dist/errors-Iy71s7Oj.js +177 -0
- package/dist/errors-m3u9TmDu.js +27 -0
- package/dist/evaluations-DsAU9ppl.js +479 -0
- package/dist/eve/index.d.ts +95 -0
- package/dist/eve/index.js +181 -0
- package/dist/express/index.d.ts +46 -0
- package/dist/express/index.js +108 -0
- package/dist/fastify/index.d.ts +56 -0
- package/dist/fastify/index.js +141 -0
- package/dist/files-B_wAGYmt.js +121 -0
- package/dist/fixtures-Cvaw0gDG.js +64 -0
- package/dist/freeze-y1_jQL4v.js +31 -0
- package/dist/from-snapshot-BMGnhmgv.js +552 -0
- package/dist/generate-CH7ThJ14.js +173 -0
- package/dist/grantee-Bv04_Vzf.js +521 -0
- package/dist/headers-B5RRv3Xm.js +6 -0
- package/dist/headers-C8SHQccD.d.ts +6 -0
- package/dist/helpers-DnSOrhB9.js +682 -0
- package/dist/hono/index.d.ts +70 -0
- package/dist/hono/index.js +131 -0
- package/dist/hosted-BwKQ1BTA.js +265 -0
- package/dist/http-DPuDaChb.d.ts +13 -0
- package/dist/http-GgVApX1k.js +111 -0
- package/dist/index-CltcQ4Bf.d.ts +138 -0
- package/dist/index-LF_hS-yn.d.ts +177 -0
- package/dist/index-fG6NSXlc.d.ts +9 -0
- package/dist/index-xIxJLwOQ.d.ts +415 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +21 -0
- package/dist/jwt/index.d.ts +87 -0
- package/dist/jwt/index.js +571 -0
- package/dist/kernel-YOGCWNLf.js +211 -0
- package/dist/kysely/index.d.ts +77 -0
- package/dist/kysely/index.js +129 -0
- package/dist/load-BZ24CpZP.js +69 -0
- package/dist/load-QOOm1z70.js +2 -0
- package/dist/load-jose-BL6LH4bL.js +11 -0
- package/dist/lru-8q4WZjBV.js +28 -0
- package/dist/manifest-DDQbC6K0.d.ts +118 -0
- package/dist/map-DowSO96j.js +158 -0
- package/dist/map-claims-B_6I1Rn-.js +360 -0
- package/dist/map-error-RJ3ZsUyA.js +16 -0
- package/dist/markers-B0hYLvMD.js +60 -0
- package/dist/may-use-CnFEjVEp.js +37 -0
- package/dist/mcp/index.d.ts +127 -0
- package/dist/mcp/index.js +563 -0
- package/dist/nest/index.d.ts +80 -0
- package/dist/nest/index.js +270 -0
- package/dist/next/client.d.ts +24 -0
- package/dist/next/client.js +31 -0
- package/dist/next/index.d.ts +110 -0
- package/dist/next/index.js +195 -0
- package/dist/next/plugin.d.ts +14 -0
- package/dist/next/plugin.js +2 -0
- package/dist/node/index.d.ts +43 -0
- package/dist/node/index.js +66 -0
- package/dist/openai/index.d.ts +71 -0
- package/dist/openai/index.js +105 -0
- package/dist/openapi/index.d.ts +93 -0
- package/dist/openapi/index.js +3 -0
- package/dist/openapi-CwePaF7C.js +164 -0
- package/dist/openapi-DWD0Uc_4.js +250 -0
- package/dist/openapi-DcqM8nU7.js +306 -0
- package/dist/openapi-import-BWRUNNeW.js +5959 -0
- package/dist/openapi-schema-D8j_T9tO.js +6902 -0
- package/dist/orpc/index.d.ts +64 -0
- package/dist/orpc/index.js +188 -0
- package/dist/otel/index.d.ts +13 -0
- package/dist/otel/index.js +202 -0
- package/dist/package-root-zGkxO3oN.js +30 -0
- package/dist/parse-DPfIvFMU.js +135 -0
- package/dist/pdp/index.d.ts +22 -0
- package/dist/pdp/index.js +795 -0
- package/dist/peer-BOr_kJE1.js +31 -0
- package/dist/permdock-yoZ7EjxX.js +2956 -0
- package/dist/permissions-B4apnahQ.d.ts +185 -0
- package/dist/permissions-B8vCg_bI.d.ts +52 -0
- package/dist/permissions-CofRqesP.js +142 -0
- package/dist/permissions-DC7GROPA.js +600 -0
- package/dist/pg-4GJWDUxG.js +953 -0
- package/dist/pins-CqE1jZuF.js +12 -0
- package/dist/plugin-4gr7IxFJ.js +65 -0
- package/dist/policy-ANp36PnI.js +971 -0
- package/dist/policy-CInmNGkM.d.ts +1959 -0
- package/dist/policy-Cq2fB8Q5.d.ts +20 -0
- package/dist/prisma/index.d.ts +135 -0
- package/dist/prisma/index.js +430 -0
- package/dist/problem-Cgp9-KZA.js +226 -0
- package/dist/prompt-CXYhLfsS.js +32 -0
- package/dist/protected-B4K3BKoX.js +138 -0
- package/dist/protected-BuPAycJT.d.ts +26 -0
- package/dist/protected-wY7nXhY9.d.ts +23 -0
- package/dist/provider-_wx46HpB.js +48 -0
- package/dist/provider-wTAUda2v.d.ts +6 -0
- package/dist/react/index.d.ts +6 -0
- package/dist/react/index.js +6 -0
- package/dist/react/provider-client.d.ts +2 -0
- package/dist/react/provider-client.js +3 -0
- package/dist/react-native/index.d.ts +42 -0
- package/dist/react-native/index.js +174 -0
- package/dist/refs-ChLSxbST.js +90 -0
- package/dist/relations-r4soIFZB.js +426 -0
- package/dist/revocations-Cy06JNx2.d.ts +21 -0
- package/dist/rls-BOFU3chG.js +233 -0
- package/dist/rls-CJVja9Ww.js +2119 -0
- package/dist/rls-graph-DxQ39PJj.js +371 -0
- package/dist/rls-helpers-BxRuif0l.js +1346 -0
- package/dist/rls-import-CXV6LBlS.js +942 -0
- package/dist/rls-migrate-DrFPbf8p.js +366 -0
- package/dist/row-check-BajTgQZ-.d.ts +43 -0
- package/dist/run-CJ6UvS3a.js +155 -0
- package/dist/runtime-DP9ttFh1.js +27 -0
- package/dist/saas-BRrZzJkM.js +1193 -0
- package/dist/schema-P8yoma__.js +296 -0
- package/dist/scim/index.d.ts +2 -0
- package/dist/scim/index.js +2 -0
- package/dist/scim-9NFa0XVZ.js +1445 -0
- package/dist/scopes-cKVp_WTw.js +212 -0
- package/dist/server/index.d.ts +123 -0
- package/dist/server/index.js +8 -0
- package/dist/sha256-BO0swRXL.js +150 -0
- package/dist/sink-Bd9PSyfq.js +141 -0
- package/dist/sink-Cfr9-7YC.d.ts +104 -0
- package/dist/skills-C_lpV4Jx.js +50 -0
- package/dist/skills-Dx3QLnMY.js +110 -0
- package/dist/snapshot-DE8dE_hz.js +1051 -0
- package/dist/solid/index.d.ts +63 -0
- package/dist/solid/index.js +197 -0
- package/dist/source-DoBBYJi2.js +6 -0
- package/dist/sources-DOpW5Ila.d.ts +347 -0
- package/dist/sources-DVHM3Mwq.js +284 -0
- package/dist/sql-Bll1d7y0.js +22 -0
- package/dist/src-CTNT-PBW.js +430 -0
- package/dist/ssf/index.d.ts +3 -0
- package/dist/ssf/index.js +622 -0
- package/dist/store-Bq90395i.d.ts +27 -0
- package/dist/store-C6hFW6ta.js +508 -0
- package/dist/stores-CkG2HrSg.js +140 -0
- package/dist/stores-CvOnu_2T.d.ts +41 -0
- package/dist/stream-BhMfZOwr.d.ts +12 -0
- package/dist/stream-Q_xlB6Ko.js +52 -0
- package/dist/style-CTm1AXDT.js +22 -0
- package/dist/subject-C6dLxqHU.js +24 -0
- package/dist/subject-settings-xt7V845g.js +82 -0
- package/dist/supabase/index.d.ts +67 -0
- package/dist/supabase/index.js +4 -0
- package/dist/supabase/middleware.d.ts +77 -0
- package/dist/supabase/middleware.js +68 -0
- package/dist/supabase-D8UlBA7Z.js +83 -0
- package/dist/supabase-DTxGtYdi.js +796 -0
- package/dist/supabase-hook-DW-xP4hU.js +2 -0
- package/dist/supabase-hook-D_OqGb_V.js +1528 -0
- package/dist/svelte/Protected.svelte +34 -0
- package/dist/svelte/index.d.ts +10 -0
- package/dist/svelte/index.js +53 -0
- package/dist/svelte/runtime.d.ts +6 -0
- package/dist/svelte/runtime.js +3 -0
- package/dist/svelte/source.d.ts +10 -0
- package/dist/svelte/source.js +8 -0
- package/dist/terminal/index.d.ts +171 -0
- package/dist/terminal/index.js +821 -0
- package/dist/testing/index.d.ts +656 -0
- package/dist/testing/index.js +3208 -0
- package/dist/testing/saas/index.d.ts +194 -0
- package/dist/testing/saas/index.js +3 -0
- package/dist/testing/saas/permissions.d.ts +2 -0
- package/dist/testing/saas/permissions.js +2 -0
- package/dist/text-diff-BdkkYSbi.js +15 -0
- package/dist/thenable-okc2xDsj.js +10 -0
- package/dist/timeout-D51niuQP.js +8 -0
- package/dist/trpc/index.d.ts +80 -0
- package/dist/trpc/index.js +205 -0
- package/dist/types-B8w5PFtS.d.ts +141 -0
- package/dist/types-B9dmiiab.d.ts +66 -0
- package/dist/types-BBK-exvJ.d.ts +177 -0
- package/dist/types-BvBk1wgn.d.ts +118 -0
- package/dist/types-CxquUByI.d.ts +9 -0
- package/dist/types-D6Z6-rKJ.d.ts +312 -0
- package/dist/types-DbWdEfcm.d.ts +87 -0
- package/dist/types-bva3i4Gf.d.ts +98 -0
- package/dist/types-i4BAOvdb.d.ts +97 -0
- package/dist/unplugin/index.d.ts +7 -0
- package/dist/unplugin/index.js +30 -0
- package/dist/usage-1ozMubLY.js +42 -0
- package/dist/usage-CUnK6Ou9.js +530 -0
- package/dist/usage-PyEor6GA.js +2 -0
- package/dist/validity-D0uRmJKJ.js +224 -0
- package/dist/verifier-B90agnhl.js +461 -0
- package/dist/version-CnSh33Fj.js +16 -0
- package/dist/vocabulary-Dk0fs-Jk.d.ts +56 -0
- package/dist/vue/index.d.ts +57 -0
- package/dist/vue/index.js +203 -0
- package/dist/webmcp/index.d.ts +75 -0
- package/dist/webmcp/index.js +313 -0
- package/package.json +423 -4
- package/rulesets/permdock.yaml +29 -0
- package/schemas/catalog-v1.json +507 -0
- package/schemas/openapi/oas-3.1.json +1298 -0
- package/schemas/openapi/oas-3.2.json +1516 -0
- package/schemas/openapi/overlay-1.1.json +79 -0
- package/schemas/supabase-claims-v1.json +139 -0
- package/schemas/supabase-manifest-v1.json +217 -0
- package/skills/permdock/SKILL.md +66 -0
- package/skills/permdock-agents/SKILL.md +77 -0
- package/skills/permdock-agents/references/runtimes.md +211 -0
- package/skills/permdock-approvals/SKILL.md +80 -0
- package/skills/permdock-approvals/references/stores-and-resume.md +83 -0
- package/skills/permdock-audit/SKILL.md +67 -0
- package/skills/permdock-credentials/SKILL.md +55 -0
- package/skills/permdock-credentials/references/tokens-keys-links.md +127 -0
- package/skills/permdock-data/SKILL.md +57 -0
- package/skills/permdock-data/references/queries-and-rls.md +54 -0
- package/skills/permdock-data/references/relationships.md +65 -0
- package/skills/permdock-tenancy/SKILL.md +77 -0
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +83 -0
- package/skills/permdock-wire/SKILL.md +116 -0
- package/skills/permdock-wire/references/adapters.md +480 -0
- package/skills/permdock-wire/references/policy-options.md +43 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: permdock-data
|
|
3
|
+
description: Enforces PermDock permissions in database queries and Postgres row-level security. Use when listing rows with permdock.where() and filter(), compiling conditions with toWhere from permdock/drizzle, permdock/prisma or permdock/kysely, guarding Convex functions with permdock/convex, generating or verifying RLS with permdock rls generate, verify or migrate, modelling folders, sub-teams or reporting lines with parent and relation() through a RelationSource, loadRelations or whoCan, or proving query and database parity with ormParity and rlsParity. Also for doctor PD027, PD028, PD031 and PD032.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: ScaleDockHQ
|
|
7
|
+
homepage: https://permdock.dev/docs/adapters/rls
|
|
8
|
+
repository: https://github.com/ScaleDockHQ/permdock
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# PermDock data
|
|
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.dev/docs/concepts/conditions), [RLS](https://permdock.dev/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
|
+
|
|
15
|
+
## Inputs (find out, or ask before starting)
|
|
16
|
+
|
|
17
|
+
- The ORM or query layer: Drizzle, Prisma, Kysely, Convex, or raw SQL.
|
|
18
|
+
- Whether Postgres RLS enforces access as well (clients reading tables directly, a data API), and whether the app's SQL or PermDock's policy is the authority.
|
|
19
|
+
- Whether existing policies call hand-written helpers.
|
|
20
|
+
- Object hierarchies: nested folders, sub-teams, reporting lines, account delegates, rows that must not inherit.
|
|
21
|
+
- A test database for parity runs (a container or a branch database).
|
|
22
|
+
|
|
23
|
+
## Invariants
|
|
24
|
+
|
|
25
|
+
1. Portable first. Write `where` conditions with the portable operators; a closure is for checks they cannot express, and it is filtered in memory, never in SQL or RLS.
|
|
26
|
+
2. A list query uses `where()` before the query and `filter()` after it. One row is loaded and checked with `assert` or the adapter's `protect`.
|
|
27
|
+
3. Generated RLS never emits `service_role`, and a request runs as `authenticated` or `anon`.
|
|
28
|
+
4. RLS cannot see request `context`. A condition the database must enforce compares a server-set `principal.claims.*` value, never `user_metadata`.
|
|
29
|
+
5. Graph grants walk at most 32 hops (16 by default) and deny with `relation-unavailable` when the relation source is missing or unloaded.
|
|
30
|
+
|
|
31
|
+
## Workflow
|
|
32
|
+
|
|
33
|
+
1. **Narrow list queries.** Compile `permdock.where(permission)` with the ORM's `toWhere`, then run `filter` and `pick` on the rows. -> [references/queries-and-rls.md](references/queries-and-rls.md)
|
|
34
|
+
✓ A user with no grant gets an empty list, and the query compiles without `non-portable-condition`.
|
|
35
|
+
2. **Model hierarchies, when the product has them.** Give the resource a `parent`, declare the relations principals hold, add `restricted` where rows must not inherit, and grant with `relation(resource, name, { through: 'parent' })`. Pass a `RelationSource` as `relations` and call `loadRelations` before `can` / `filter` when it is async. Use `whoCan` for share dialogs. -> [references/relationships.md](references/relationships.md)
|
|
36
|
+
✓ `permdock doctor --only graph` is clean, and a viewer of a parent folder can read a child document.
|
|
37
|
+
3. **Generate or adopt RLS.** Run `permdock rls generate` from the policy, or keep SQL as the authority with `sqlFunction` twins and `rls.functions`, or move hand-written helpers with `permdock rls migrate`. -> [references/queries-and-rls.md](references/queries-and-rls.md#generate-and-verify-rls)
|
|
38
|
+
✓ `permdock rls generate --check` passes in CI.
|
|
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
|
+
✓ 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.dev/docs/adapters/testing)). Run `testRelationSource` on a custom `RelationSource`.
|
|
42
|
+
✓ The query and the database return exactly the rows `filter()` keeps.
|
|
43
|
+
|
|
44
|
+
## Verify before done
|
|
45
|
+
|
|
46
|
+
- [ ] No closure only compares fields a portable operator covers (`eq`, `in`, `memberOf`, `sqlFunction`, …); rewrite those as JSON conditions.
|
|
47
|
+
- [ ] An owner-equals-principal `where` on a resource with a matching relation uses `relation()` instead.
|
|
48
|
+
- [ ] No `opaque` RLS grant wraps a named helper; use `sqlFunction` with a twin and map it in `rls.functions`.
|
|
49
|
+
- [ ] No condition the database must enforce reads `context.*` (PD027); on Supabase, `supabase.hook.attrs` lists only server-owned values (PD028).
|
|
50
|
+
- [ ] Graph declarations are walked by a grant (PD031) and nameable in SQL (PD032).
|
|
51
|
+
- [ ] Every list endpoint has an `ormParity` run, every RLS table an `rlsParity` or `rls verify --db` run, and a custom `RelationSource` passes `testRelationSource`.
|
|
52
|
+
|
|
53
|
+
## Reference index
|
|
54
|
+
|
|
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
|
+
- [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.dev/docs/concepts/conditions), [relationships](https://permdock.dev/docs/concepts/relationships), [RLS adapter](https://permdock.dev/docs/adapters/rls), [`permdock rls`](https://permdock.dev/docs/cli/rls), [Postgres RLS](https://permdock.dev/docs/standards/postgres-rls).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Queries and generated RLS
|
|
2
|
+
|
|
3
|
+
Owning pages: [protected queries](https://permdock.dev/docs/concepts/policies#protected-queries), [RLS adapter](https://permdock.dev/docs/adapters/rls), [`permdock rls`](https://permdock.dev/docs/cli/rls), and the ORM pages [Drizzle](https://permdock.dev/docs/adapters/drizzle), [Prisma](https://permdock.dev/docs/adapters/prisma), [Kysely](https://permdock.dev/docs/adapters/kysely), [Convex](https://permdock.dev/docs/adapters/convex).
|
|
4
|
+
|
|
5
|
+
## List queries
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { toWhere } from "permdock/drizzle"; // or permdock/prisma, permdock/kysely
|
|
9
|
+
|
|
10
|
+
export async function listPosts(permdock: PermDock) {
|
|
11
|
+
const read = permissions.post.read;
|
|
12
|
+
const rows = await db
|
|
13
|
+
.select()
|
|
14
|
+
.from(posts)
|
|
15
|
+
.where(toWhere(permdock.where(read), posts));
|
|
16
|
+
return permdock.filter(read, rows).map((row) => permdock.pick(read, row));
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- `permdock.where(permission)` narrows the query before it runs. It leaves out closure grants and marks the result `partial`; `filter` then drops what a closure refuses, and `pick` redacts fields.
|
|
21
|
+
- When nothing is granted, `toWhere` returns a constant false expression (no rows). A closure grant cannot compile: `toWhere` throws `PermDockValidationError`.
|
|
22
|
+
- `toWhere(condition, table, { columns })` maps schema fields to differently named columns. Graph grants need `{ relations: { closure: 'permdock.permdock_closure' } }`, otherwise a `related` node throws `non-portable-condition`.
|
|
23
|
+
- `checkRow` (Drizzle) answers one row as `{ found: false }` or `{ found: true, granted }`, so a handler can tell `404` from `403` with one query.
|
|
24
|
+
- Prisma also exports `permdockExtension`. Drizzle, Prisma and Kysely export `withSubject`, which runs a transaction as the subject (`set local role` plus claims) so RLS sees it; its `dialect` must match `rls generate`.
|
|
25
|
+
- Convex: `createPermDock` from `permdock/convex` returns `withPermDock` and `snapshotQuery`. Identity comes from `ctx` only; function arguments never influence the subject.
|
|
26
|
+
|
|
27
|
+
## Generate and verify RLS
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm exec permdock rls generate --target sql --dialect supabase --out migrations/rls.sql
|
|
31
|
+
pnpm exec permdock rls generate --target drizzle --dialect supabase
|
|
32
|
+
pnpm exec permdock rls import --sql migrations/rls.sql --out src/permissions.generated.ts
|
|
33
|
+
pnpm exec permdock rls verify --fixtures rls.fixtures.json
|
|
34
|
+
pnpm exec permdock rls verify --db $DATABASE_URL --fixtures rls.fixtures.json
|
|
35
|
+
pnpm exec permdock rls verify --introspect --db $DATABASE_URL
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- 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. Set `--tenant-type` (or `rls.tenantType`, `rls.scopeTypes`) to the scope columns' types.
|
|
39
|
+
- `--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. `--authorize jwt` reads token claims and stays stale until the token refreshes; keep the token lifetime at 3600 seconds or less (PD019).
|
|
40
|
+
- `--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.
|
|
41
|
+
- `--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
|
+
- 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.
|
|
44
|
+
- Never emit `service_role`. Fixtures may carry `memberships`, `tenant` and `customRoles`.
|
|
45
|
+
|
|
46
|
+
## SQL as the authority
|
|
47
|
+
|
|
48
|
+
When the database already holds the rules, skip `generate`. Map each helper in `rls.functions`, write `sqlFunction` twins in the policy, and fail CI on `rls verify --db`. `--inline-functions` inlines the twin for targets that cannot call a SQL function.
|
|
49
|
+
|
|
50
|
+
When hand-written policies call the app's own helpers (`org_ids_with_permission`, `has_org_permission`), map each under `rls.migrate.helpers` with its `form` (`ids`, `row`, `scoped`, `global`, `membership`) and the legacy keys under `rls.migrate.keys` or `prefixes`. Run `permdock rls migrate`, read the skipped calls, and apply with `--write`. Generate with `--helpers-only` while the policies stay hand-written. A `row-conditions` skip means that table's policy should be generated, not migrated.
|
|
51
|
+
|
|
52
|
+
## Attributes RLS cannot see
|
|
53
|
+
|
|
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.dev/docs/adapters/supabase) and [Supabase hook](https://permdock.dev/docs/adapters/supabase-hook) pages.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Relationship graph
|
|
2
|
+
|
|
3
|
+
Owning page: [relationships](https://permdock.dev/docs/concepts/relationships). Named scopes are tenancy (fixed levels); the graph is the objects under them, with no fixed depth.
|
|
4
|
+
|
|
5
|
+
## Declare
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
export const permissions = definePermissions({
|
|
9
|
+
doc: resource(Doc, {
|
|
10
|
+
actions: ["read", "update"],
|
|
11
|
+
parent: { field: "folderId", resource: "folder" },
|
|
12
|
+
relations: { owner: "ownerId" },
|
|
13
|
+
}),
|
|
14
|
+
folder: resource(Folder, {
|
|
15
|
+
actions: ["read", "share"],
|
|
16
|
+
parent: { field: "parentId", resource: "folder" },
|
|
17
|
+
relations: {
|
|
18
|
+
editor: { edge: "folder_editors" },
|
|
19
|
+
viewer: { edge: "folder_viewers", expiresAt: "expires_at" },
|
|
20
|
+
},
|
|
21
|
+
restricted: "restricted",
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
| Relation | Declared as | Held when |
|
|
27
|
+
| ---------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
|
28
|
+
| Field | `owner: 'ownerId'` | the row's field is the principal's id |
|
|
29
|
+
| Edge table | `{ edge, object?, subject?, expiresAt?, match?, groups? }` | a row of `edge` links the object and the principal, and has not expired |
|
|
30
|
+
| Principal | `{ principal, period? }` | the row's column is the principal's id, inside `period` (a manager, an account delegate) |
|
|
31
|
+
| Implied | `{ includes: ['editor'] }` | the principal holds a listed relation |
|
|
32
|
+
|
|
33
|
+
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. `links` names to-one references a grant can cross with `through: ['folder', 'team']`.
|
|
34
|
+
|
|
35
|
+
## Grant
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
grants: [
|
|
39
|
+
allow(permissions.doc.read, { to: relation(permissions.doc, 'owner') }),
|
|
40
|
+
allow(permissions.doc.read, {
|
|
41
|
+
to: relation(permissions.folder, 'viewer', { through: 'parent', depth: 16 }),
|
|
42
|
+
}),
|
|
43
|
+
],
|
|
44
|
+
```
|
|
45
|
+
|
|
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.
|
|
47
|
+
|
|
48
|
+
## Load
|
|
49
|
+
|
|
50
|
+
Pass `relations: memoryRelations(permissions, { rows, edges })` in tests, or a `RelationSource` (`ancestors`, `related`) over the app's tables, to `createPermDock`. Evaluation stays synchronous; with an async source, call `await permdock.loadRelations(permission, rows)` before `can` or `filter`. Without a source, or with one that threw, graph checks deny with `relation-unavailable`; a chain past `depth` denies with `relation-depth`. Answers are cached per instance, never at module level.
|
|
51
|
+
|
|
52
|
+
## Share dialogs
|
|
53
|
+
|
|
54
|
+
`const { holders, complete } = await permdock.whoCan(permission, row)` lists who holds a permission on one object and how. Show `complete: false` honestly: some holders could not be listed. `whoCan` never grants.
|
|
55
|
+
|
|
56
|
+
## In Postgres
|
|
57
|
+
|
|
58
|
+
`permdock rls generate` emits the `permdock_closure` table, its statement triggers and `permitted_<resource>_ids(relation)` helpers. Snapshots carry no graph: graph grants are `portable: false` on the client and go to the decision endpoint. Check with:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pnpm exec permdock rls verify --tree --db $DATABASE_URL
|
|
62
|
+
pnpm exec permdock doctor --only graph
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`--tree` seeds a generated object graph in a rolled-back transaction and compares it with `can()` per principal. PD031 flags a self-parent no `through: 'parent'` grant walks, or a `restricted` column no graph grant reaches; PD032 flags a graph resource RLS cannot name.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: permdock-tenancy
|
|
3
|
+
description: Models tenants, memberships and role rules in PermDock. Use when declaring named scopes (organizations, workspaces, customers inside an organization), loading memberships through a MembershipSource, putting a role on one scope, keeping an owner per organization with min, max or transferOnly, controlling who hands out which role with assigns and for, checking a membership change with decideRoleChange, letting tenant admins define custom roles with customRoles and validateCustomRole, or fixing doctor PD023, PD025 or PD026 and denials such as tenant-mismatch, last-holder or not-assignable-by.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: ScaleDockHQ
|
|
7
|
+
homepage: https://permdock.dev/docs/concepts/scopes
|
|
8
|
+
repository: https://github.com/ScaleDockHQ/permdock
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# PermDock tenancy
|
|
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.dev/docs/concepts/scopes), [tenancy](https://permdock.dev/docs/concepts/tenancy)). Set up the policy and factory first with the `permdock-wire` skill (`npx skills add ScaleDockHQ/PermDock --skill permdock-wire`).
|
|
14
|
+
|
|
15
|
+
## Inputs (find out, or ask before starting)
|
|
16
|
+
|
|
17
|
+
- The tenant levels, outermost first, and the row column that holds each id (`organization_id`, `customer_id`).
|
|
18
|
+
- Where memberships are stored and how the app loads them: token claims from a `subjectFrom*` provider, or the app's tables.
|
|
19
|
+
- The membership kinds (`via`): staff, contact, guest, partner, support.
|
|
20
|
+
- Which role manages each scope, who may hand out which role, and which roles a contact or guest may never hold.
|
|
21
|
+
- Whether tenant admins define their own roles.
|
|
22
|
+
|
|
23
|
+
## Invariants
|
|
24
|
+
|
|
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
|
+
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.
|
|
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.
|
|
30
|
+
|
|
31
|
+
## Workflow
|
|
32
|
+
|
|
33
|
+
1. **Declare the scopes.** Add them to `definePolicy` in order, each with its row `key` and, after the first, `within`:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
definePolicy(
|
|
37
|
+
{ permissions, roles },
|
|
38
|
+
{
|
|
39
|
+
scopes: {
|
|
40
|
+
organization: { key: "organization_id" },
|
|
41
|
+
customer: { key: "customer_id", within: "organization" },
|
|
42
|
+
},
|
|
43
|
+
roles: [
|
|
44
|
+
role(roles.admin, adminGrants, { on: "organization" }),
|
|
45
|
+
role(roles.contact, contactGrants, { on: "customer" }),
|
|
46
|
+
],
|
|
47
|
+
subject,
|
|
48
|
+
},
|
|
49
|
+
);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
✓ `definePolicy` accepts the scopes, and each role names one scope with `on`.
|
|
53
|
+
|
|
54
|
+
2. **Give resources their scope keys.** Every resource a scoped grant touches declares a `memberOf` relation per scope key it carries, for example `relations: { organization: { field: 'organization_id', memberOf: 'organization' } }`.
|
|
55
|
+
✓ `definePolicy` does not throw for a missing `memberOf` relation.
|
|
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
|
+
✓ `permdock doctor` reports no PD025 for the `doctor.memberships` fixture.
|
|
58
|
+
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
|
+
✓ PD026 is clean, and removing the last owner is denied with `last-holder`.
|
|
60
|
+
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
|
+
✓ 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.dev/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
|
+
✓ The tests pass.
|
|
64
|
+
|
|
65
|
+
## Verify before done
|
|
66
|
+
|
|
67
|
+
- [ ] No membership, tenant or role is read from a request body, tool argument, unsigned header or `user_metadata`.
|
|
68
|
+
- [ ] Each scope's managing role sets `min: 1` (PD026), and admin-like roles set `for` so contact, guest or partner memberships cannot hold them.
|
|
69
|
+
- [ ] No `assigns` list lets a role appoint a role ranked above it.
|
|
70
|
+
- [ ] Every server action that writes a membership role calls `decideRoleChange` with a `holders` count from the store, never a guess.
|
|
71
|
+
- [ ] Every custom-role save runs `validateCustomRole` and checks `assignablePermissions()`. No single assignable `admin` role makes the ceiling the whole product.
|
|
72
|
+
- [ ] Custom `MembershipSource` and `RoleSource` implementations pass `testMembershipSource` / `testRoleSource`.
|
|
73
|
+
|
|
74
|
+
## Reference index
|
|
75
|
+
|
|
76
|
+
- [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.dev/docs/concepts/scopes), [tenancy](https://permdock.dev/docs/concepts/tenancy), [ownership](https://permdock.dev/docs/concepts/ownership), [custom roles](https://permdock.dev/docs/concepts/custom-roles), [extension interfaces](https://permdock.dev/docs/concepts/extension-interfaces).
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Ownership and custom roles
|
|
2
|
+
|
|
3
|
+
Owning pages: [ownership](https://permdock.dev/docs/concepts/ownership) and [custom roles](https://permdock.dev/docs/concepts/custom-roles).
|
|
4
|
+
|
|
5
|
+
## Role options
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
const staff = { for: ["staff"], meta: { audience: "staff" } } as const;
|
|
9
|
+
|
|
10
|
+
roles: [
|
|
11
|
+
role(roles.owner, ownerGrants, {
|
|
12
|
+
on: "organization",
|
|
13
|
+
...staff,
|
|
14
|
+
min: 1,
|
|
15
|
+
assigns: ["owner", "admin", "member", "viewer", "contact"],
|
|
16
|
+
}),
|
|
17
|
+
role(roles.admin, adminGrants, {
|
|
18
|
+
on: "organization",
|
|
19
|
+
...staff,
|
|
20
|
+
assigns: ["member", "viewer", "contact"],
|
|
21
|
+
}),
|
|
22
|
+
role(roles.contact, contactGrants, {
|
|
23
|
+
on: "customer",
|
|
24
|
+
for: ["contact"],
|
|
25
|
+
meta: { audience: "portal" },
|
|
26
|
+
}),
|
|
27
|
+
];
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
| Option | Meaning |
|
|
31
|
+
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
32
|
+
| `min` | Fewest holders each scope instance keeps (default `0`). Removing the last owner is refused. |
|
|
33
|
+
| `max` | Most holders an instance may have. |
|
|
34
|
+
| `transferOnly` | Once an instance has holders, the role only moves from one holder to another. |
|
|
35
|
+
| `assigns` | Roles a holder may assign and revoke. Once any role declares `assigns`, this graph replaces the held-grant rule for `assignableRoles()`. |
|
|
36
|
+
| `for` | Membership kinds (`Membership.via`) that may hold the role. A membership with another `via`, or none, grants nothing through it. |
|
|
37
|
+
| `exclusiveWith` | Roles nobody may hold together with this one in the same instance (PD018). |
|
|
38
|
+
| `meta.audience` | The surface the role's holders use; `permdock.audiences()` and `snapshot.audiences` report it and never grant. |
|
|
39
|
+
|
|
40
|
+
`min`, `max` and `transferOnly` need a named-scope `on`.
|
|
41
|
+
|
|
42
|
+
## Checking a role change
|
|
43
|
+
|
|
44
|
+
```ts
|
|
45
|
+
const result = permdock.decideRoleChange({
|
|
46
|
+
kind: "assign", // 'assign' | 'revoke' | 'transfer'
|
|
47
|
+
role: roles.admin,
|
|
48
|
+
scope: "organization",
|
|
49
|
+
id: "o_acme",
|
|
50
|
+
target: { id: "u_bob", via: "staff", roles: ["member"] }, // their membership there now
|
|
51
|
+
holders: 1, // how many hold the role in the instance now, from the app's store
|
|
52
|
+
});
|
|
53
|
+
// { outcome: 'granted', change, role } or { outcome: 'denied', change, denials }
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The actor is the instance's own subject; nothing in the change stands in for it. `decideRoleChange` never writes. Without a valid `holders` count, a role with `min`, `max` or `transferOnly` is denied. Denial reasons: `last-holder`, `max-holders`, `transfer-only`, `not-assignable-by`, `self-demotion`, `not-allowed-for-membership`, `conflicting-role`.
|
|
57
|
+
|
|
58
|
+
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
|
+
|
|
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.
|
|
61
|
+
|
|
62
|
+
## Custom roles
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
type CustomRole = {
|
|
66
|
+
tenant: string; // absent when scope is 'global'
|
|
67
|
+
scope?: string; // default: the first scope; 'global' for a platform role
|
|
68
|
+
id?: string; // pins it to one instance
|
|
69
|
+
name: string;
|
|
70
|
+
includes?: string[]; // declared roles to start from
|
|
71
|
+
grants?: { permission: string; effect?: "allow" | "deny" }[];
|
|
72
|
+
};
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- 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
|
+
- 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.
|
|
77
|
+
- 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`.
|
|
79
|
+
- `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.
|
|
82
|
+
- 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.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: permdock-wire
|
|
3
|
+
description: Adds PermDock authorization to a TypeScript app, from definitions to the first guard. Use when installing permdock, adding permissions, roles, plans, RBAC or access control, writing definePermissions, definePolicy or createPermDock, guarding a route, Server Action, procedure or React component, adding usage limits, plan gates or time-limited grants, or running permdock collect and doctor for the first time.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: ScaleDockHQ
|
|
7
|
+
homepage: https://permdock.dev/docs/getting-started/quick-start
|
|
8
|
+
repository: https://github.com/ScaleDockHQ/permdock
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Wire PermDock
|
|
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.dev/docs/getting-started/quick-start)). Import paths and identifiers follow the [naming convention](https://permdock.dev/docs/getting-started/naming).
|
|
14
|
+
|
|
15
|
+
## Inputs (find out, or ask before starting)
|
|
16
|
+
|
|
17
|
+
- The framework: Next.js, Hono, Express, Fastify, Elysia, Nest, Node, tRPC, oRPC, an AuthZEN PDP, Vite with React, Vue, Svelte or Solid, React Native, or a collect-only framework (Nuxt, Astro, React Router, TanStack Start, Effect).
|
|
18
|
+
- The Standard Schema validator already in the repo: Zod, Valibot, ArkType, Effect Schema.
|
|
19
|
+
- Where the signed-in user comes from (the session or a verified token) and which roles and plans exist.
|
|
20
|
+
- The first path to guard.
|
|
21
|
+
|
|
22
|
+
## Invariants
|
|
23
|
+
|
|
24
|
+
1. Permissions are references (`permissions.post.update`), never strings. Roles and plans are typed leaves (`roles.owner`, `plans.pro`).
|
|
25
|
+
2. Outcomes are `granted`, `denied` or `approval-required`. Anything unknown, invalid or thrown denies, and a matching `deny` wins over every `allow`.
|
|
26
|
+
3. `policy.ts` is server-only. Client entries import `permissions.ts`, snapshots and hooks only.
|
|
27
|
+
4. The subject comes from a trusted resolver (`subjectFrom*` or the server session), never from a request body, header the client sets, or model output.
|
|
28
|
+
5. Portable `where` conditions first; a closure only when no portable operator expresses the rule.
|
|
29
|
+
|
|
30
|
+
## Workflow
|
|
31
|
+
|
|
32
|
+
1. **Detect.** Find the framework and the validator. Collect-only frameworks use `permdock/unplugin` for collect and `permdock/server` (or the matching HTTP adapter) at runtime; never add a per-vendor package.
|
|
33
|
+
✓ The adapter import path (`permdock/<framework>`) and the validator import are named.
|
|
34
|
+
2. **Define.** Create `src/permissions.ts`. Importable everywhere; no rules, no secrets.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import {
|
|
38
|
+
definePermissions,
|
|
39
|
+
defineRoles,
|
|
40
|
+
definePlans,
|
|
41
|
+
resource,
|
|
42
|
+
crud,
|
|
43
|
+
} from "permdock";
|
|
44
|
+
import { Post } from "./schemas";
|
|
45
|
+
|
|
46
|
+
export const permissions = definePermissions({
|
|
47
|
+
post: resource(Post, crud({ relations: { author: "authorId" } })),
|
|
48
|
+
});
|
|
49
|
+
export const roles = defineRoles({ member: {}, admin: {} });
|
|
50
|
+
export const plans = definePlans({ pro: {} });
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
✓ Every resource the first guard needs has a leaf; instance actions sit in `actions`, list and create in `collection`.
|
|
54
|
+
|
|
55
|
+
3. **Policy.** Create `src/policy.ts`. Prefer `grants` with `to:` selectors (`anyone()`, `authenticated()`, `relation()`, a role or plan leaf, `actor()`, `assurance()`); `role(roles.member, …)` sugar also works.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { definePolicy, role, allow, principal, relation } from "permdock";
|
|
59
|
+
import { permissions, roles } from "./permissions";
|
|
60
|
+
|
|
61
|
+
const member = role(roles.member, [
|
|
62
|
+
allow(permissions.post.read),
|
|
63
|
+
allow(permissions.post.list),
|
|
64
|
+
allow(permissions.post.create),
|
|
65
|
+
allow(permissions.post.update, {
|
|
66
|
+
to: relation(permissions.post, "author"),
|
|
67
|
+
}),
|
|
68
|
+
allow(permissions.post.delete, {
|
|
69
|
+
where: { authorId: principal.id },
|
|
70
|
+
approval: "human",
|
|
71
|
+
}),
|
|
72
|
+
]);
|
|
73
|
+
|
|
74
|
+
export const policy = definePolicy(
|
|
75
|
+
{ permissions, roles },
|
|
76
|
+
{
|
|
77
|
+
roles: [member],
|
|
78
|
+
principal: (user) => user && { id: user.id, roles: user.roles },
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Short policy features that need no other skill -> [references/policy-options.md](references/policy-options.md): usage limits, `disclosure: 'hide'`, plan gates, `validFrom` / `validUntil`, step-up with `assurance()`.
|
|
84
|
+
✓ At least one role grants the first guard's permission, and no client entry imports `policy.ts`.
|
|
85
|
+
|
|
86
|
+
4. **Factory.** Create `src/permdock/server.ts` (or the adapter's documented factory file) with `createPermDock` from `permdock/<framework>`. Copy the exact return names. -> [references/adapters.md](references/adapters.md)
|
|
87
|
+
✓ The factory file compiles and exports the adapter's public members.
|
|
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
|
+
✓ 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.dev/docs/guides/scenario-testing)). -> [references/policy-options.md](references/policy-options.md#cli-starting-points)
|
|
91
|
+
✓ Collect and doctor findings are fixed, or the three files typecheck and one matrix test passes.
|
|
92
|
+
|
|
93
|
+
## Next skills
|
|
94
|
+
|
|
95
|
+
Install each with `npx skills add ScaleDockHQ/PermDock --skill <name>` when the app needs it:
|
|
96
|
+
|
|
97
|
+
- `permdock-agents`: AI SDK, Claude Agent SDK, Eve, OpenAI Agents SDK, MCP, A2A and WebMCP tool maps, actors and `delegation`.
|
|
98
|
+
- `permdock-approvals`: `approval: { by }`, quorum, escalation, `staleOn`, approval stores and the resume flow.
|
|
99
|
+
- `permdock-tenancy`: named scopes, memberships, ownership rules (`min`, `assigns`, `for`) and custom roles.
|
|
100
|
+
- `permdock-data`: `where()` and `filter()` in ORM queries, generated RLS, relationship graphs, parity tests.
|
|
101
|
+
- `permdock-credentials`: `subjectFromJwt`, API keys and service accounts, share links.
|
|
102
|
+
- `permdock-audit`: review the result or a pull request.
|
|
103
|
+
|
|
104
|
+
## Verify before done
|
|
105
|
+
|
|
106
|
+
- [ ] No string permission keys in app code; every check passes a reference.
|
|
107
|
+
- [ ] `policy.ts` is imported only from server code, and PD001 is clean.
|
|
108
|
+
- [ ] The subject resolver reads only the server session or a verified token.
|
|
109
|
+
- [ ] `permdock collect --check` runs in CI, and a `describePolicy` suite covers the new grants.
|
|
110
|
+
|
|
111
|
+
## Reference index
|
|
112
|
+
|
|
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
|
+
- [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
|
+
- The `permdock` skill: the mental model, `explain` for an unexpected denial, and the docs MCP.
|
|
116
|
+
- Docs: [policies](https://permdock.dev/docs/concepts/policies), [conditions](https://permdock.dev/docs/concepts/conditions), [decisions](https://permdock.dev/docs/concepts/decisions), [adapters](https://permdock.dev/docs/adapters).
|