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,480 @@
|
|
|
1
|
+
# Adapter factories
|
|
2
|
+
|
|
3
|
+
Every server adapter exports `createPermDock`. The import path names the framework. Agent runtime adapters (AI SDK, Claude Agent SDK, Eve, OpenAI Agents SDK, MCP, A2A, WebMCP, terminal) are in the `permdock-agents` skill; ORM adapters and Convex in the `permdock-data` skill. React has no factory: it exports hooks and `<Protected>` from `permdock/react`.
|
|
4
|
+
|
|
5
|
+
## Next.js — `permdock/next`
|
|
6
|
+
|
|
7
|
+
File: `src/permdock/server.ts`
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { createPermDock } from "permdock/next";
|
|
11
|
+
import { policy } from "../policy";
|
|
12
|
+
|
|
13
|
+
export const {
|
|
14
|
+
getPermDock,
|
|
15
|
+
getPermission,
|
|
16
|
+
requireAccess,
|
|
17
|
+
PermDockProvider,
|
|
18
|
+
permdockHandler,
|
|
19
|
+
} = createPermDock(policy, {
|
|
20
|
+
subject: async () => getUser(),
|
|
21
|
+
});
|
|
22
|
+
```
|
|
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` and only permission hooks suspend.
|
|
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'`:
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
// src/permdock/snapshot.ts
|
|
30
|
+
import { cacheLife, cacheTag } from 'next/cache';
|
|
31
|
+
import { snapshotFor } from 'permdock';
|
|
32
|
+
import { cacheLifeFor, snapshotTag } from 'permdock/next';
|
|
33
|
+
|
|
34
|
+
export async function loadSnapshot(org: string) {
|
|
35
|
+
'use cache: private';
|
|
36
|
+
const claims = await getClaims(); // verified locally against JWKS
|
|
37
|
+
const snapshot = snapshotFor(policy, claims, { tenant: org });
|
|
38
|
+
cacheLife(cacheLifeFor(snapshot));
|
|
39
|
+
cacheTag(snapshotTag(claims?.sub));
|
|
40
|
+
return snapshot;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// app/[org]/layout.tsx: keep it synchronous
|
|
44
|
+
<PermDockProvider snapshotPromise={params.then(({ org }) => loadSnapshot(org))}>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
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.dev/docs/guides/next-cache-components).
|
|
48
|
+
|
|
49
|
+
## Hono — `permdock/hono`
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { createPermDock } from "permdock/hono";
|
|
53
|
+
|
|
54
|
+
export const { permdock, protect, permdockHandler } = createPermDock(policy, {
|
|
55
|
+
subject: (c) => c.get("user"),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
app.use("*", permdock());
|
|
59
|
+
app.delete(
|
|
60
|
+
"/posts/:id",
|
|
61
|
+
protect(permissions.post.delete, (c) => loadPost(c)),
|
|
62
|
+
handler,
|
|
63
|
+
);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Every HTTP adapter takes `tenant` (for example `(c) => c.req.param('org')`), resolved again on each `protect` where route params exist, plus `limits` (a `LimitStore` for quota grants) and `pdp` (`createPermDock` from `permdock/pdp`). A `protect` loader's result is validated against the resource schema; pass `{ trusted: true }` as the third argument only when the loader returns a row the server loaded itself. `assert` inside a handler becomes the same 403 Problem Details as a guard denial; no `onError` wiring is needed.
|
|
67
|
+
|
|
68
|
+
Streams and sockets: pass `revocations: memoryRevocationFeed()` (from `permdock`) and open a connection after `protect` succeeded. `sse` drops items the subscriber cannot read and ends with an `event: permdock` frame on revocation; await it last. `socket` closes a WebSocket with `1008`. Check inbound socket messages with `conn.check(permission, data)`, which validates the message data; a denial keeps the socket open.
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
app.get(
|
|
72
|
+
"/projects/:id/events",
|
|
73
|
+
protect(permissions.project.read, loadProject),
|
|
74
|
+
async (c) => {
|
|
75
|
+
const conn = await connection(c, {
|
|
76
|
+
permission: permissions.project.read,
|
|
77
|
+
data: c.get("permdockData"),
|
|
78
|
+
});
|
|
79
|
+
return streamSSE(c, async (stream) => {
|
|
80
|
+
await sse(conn, stream, projectEvents(conn.signal), {
|
|
81
|
+
items: permissions.project.read,
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Publish `revocations.revoke({ principal, tenant, kind: 'changed' })` from your role-edit code so open connections revalidate.
|
|
89
|
+
|
|
90
|
+
## React (Vite) — `permdock/react`
|
|
91
|
+
|
|
92
|
+
No factory. The server builds a snapshot (`permdock.snapshot()` or `fromSnapshot` on snapshot JSON) and the client wraps the tree:
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { PermDockProvider, Protected, usePermission } from "permdock/react";
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`permissions.ts` may be imported on the client. `policy.ts` may not.
|
|
99
|
+
|
|
100
|
+
## AuthZEN — `permdock/authzen`
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { createPermDock } from "permdock/authzen";
|
|
104
|
+
|
|
105
|
+
export const { permdockHandler } = createPermDock(policy, {
|
|
106
|
+
subject: fromBearer,
|
|
107
|
+
resources: {
|
|
108
|
+
post: { load: (id) => loadPost(id), list: () => listPosts() },
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
One Fetch handler serves `POST /access/v1/evaluation`, `/evaluations`, `/search/action`, `/search/resource`, `/search/subject` and `GET /.well-known/authzen-configuration`. PEP authentication is required (`401` unless `anonymous: true`). The body `subject` is the evaluation principal when the PEP is trusted (default). Unknown actions return `decision: false` with `context.reason: 'unknown-permission'`. Omit `subjects.list` to drop search/subject from discovery. A custom PDP that evaluates delegated callers outside `createPermDock` calls `coveredByDelegation(permission, delegation, resourceId, hasActor)` from `permdock` instead of re-implementing scope, `authorization_details` and GNAP `access` matching.
|
|
114
|
+
|
|
115
|
+
## OpenAPI — `permdock/openapi`
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
import { createPermDock } from "permdock/openapi";
|
|
119
|
+
|
|
120
|
+
const { describe, securitySchemes, overlay } = createPermDock(policy, {
|
|
121
|
+
scheme: { name: "oauth", type: "oauth2", flows: { authorizationCode: {} } },
|
|
122
|
+
target: "3.2",
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`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
|
+
|
|
128
|
+
## React Native — `permdock/react-native`
|
|
129
|
+
|
|
130
|
+
No factory. Same hooks and `<Protected>` as `permdock/react`, plus `storage` so Expo Router `Stack.Protected` can answer on the first frame:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { PermDockProvider, usePermission } from "permdock/react-native";
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`storage` is `{ getItem, setItem, removeItem }` (MMKV, SecureStore, AsyncStorage). `snapshotUrl` revalidates in the background. `permdock.clear()` drops the persisted snapshot on sign-out. Do not import `policy.ts` on the client.
|
|
137
|
+
|
|
138
|
+
## Express — `permdock/express`
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { createPermDock } from "permdock/express";
|
|
142
|
+
|
|
143
|
+
export const { permdock, protect, errorHandler } = createPermDock(policy, {
|
|
144
|
+
subject: (req) => req.user ?? null,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
app.use(permdock());
|
|
148
|
+
app.delete(
|
|
149
|
+
"/posts/:id",
|
|
150
|
+
protect(permissions.post.delete, (req) => loadPost(req.params.id)),
|
|
151
|
+
handler,
|
|
152
|
+
);
|
|
153
|
+
app.use(errorHandler());
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Converts `IncomingMessage` to Fetch, then delegates to `permdock/server`. Denials are `403 application/problem+json`; anonymous callers get `401` plus `WWW-Authenticate`.
|
|
157
|
+
|
|
158
|
+
## Fastify — `permdock/fastify`
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
import { createPermDock } from "permdock/fastify";
|
|
162
|
+
|
|
163
|
+
export const { permdock, protect } = createPermDock(policy, {
|
|
164
|
+
subject: (request) => request.user ?? null,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
await app.register(permdock);
|
|
168
|
+
app.delete(
|
|
169
|
+
"/posts/:id",
|
|
170
|
+
{
|
|
171
|
+
preHandler: protect(permissions.post.delete, (request) =>
|
|
172
|
+
loadPost(request.params.id),
|
|
173
|
+
),
|
|
174
|
+
},
|
|
175
|
+
handler,
|
|
176
|
+
);
|
|
177
|
+
```
|
|
178
|
+
|
|
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`.
|
|
180
|
+
|
|
181
|
+
## Elysia — `permdock/elysia`
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { createPermDock } from "permdock/elysia";
|
|
185
|
+
|
|
186
|
+
export const { permdock, protect } = createPermDock(policy, {
|
|
187
|
+
subject: ({ store }) => store.user ?? null,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const app = new Elysia().use(permdock()).delete("/posts/:id", handler, {
|
|
191
|
+
beforeHandle: protect(permissions.post.delete, ({ params }) =>
|
|
192
|
+
loadPost(params.id),
|
|
193
|
+
),
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Fetch-native plugin via `derive`. Denials are `403 application/problem+json`; anonymous callers get `401` plus `WWW-Authenticate`. For `.ws` routes, `connection(ws, { permission })` in `open` returns the socket's connection (memoised); it closes the socket with `1008` when revoked.
|
|
198
|
+
|
|
199
|
+
## Nest — `permdock/nest`
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { createPermDock } from "permdock/nest";
|
|
203
|
+
|
|
204
|
+
export const { PermDockModule, PermDockGuard, Protect, InjectPermDock } =
|
|
205
|
+
createPermDock(policy, {
|
|
206
|
+
subject: (request) => request.user ?? null,
|
|
207
|
+
});
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Register `PermDockGuard` as `APP_GUARD` with `useExisting`. `Protect` attaches a permission (and optional loader) to a handler or class. 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
|
+
|
|
212
|
+
## Node — `permdock/node`
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
import { createPermDock } from "permdock/node";
|
|
216
|
+
|
|
217
|
+
export const { permdock, protect, send, permdockHandler } = createPermDock(
|
|
218
|
+
policy,
|
|
219
|
+
{
|
|
220
|
+
subject: (req) => userFromCookie(req.headers.cookie),
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Converts `IncomingMessage` to Fetch, then delegates to `permdock/server`. Denials are `403 application/problem+json`; anonymous callers get `401` plus `WWW-Authenticate`. Express and Nest reuse `toRequest` / `fromResponse`. The body is read only when consumed, so a body parser after `permdock(req)` still works. There is no error hook: wrap handlers that call `assert` and `send(res, problemFromError(error))` (from `permdock/server`) when it returns a `Response`.
|
|
226
|
+
|
|
227
|
+
## tRPC — `permdock/trpc`
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { createPermDock } from "permdock/trpc";
|
|
231
|
+
|
|
232
|
+
export const { permdock, protect, permdockHandler } = createPermDock(policy, {
|
|
233
|
+
subject: (opts) => opts.ctx.user ?? null,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const procedure = t.procedure.use(permdock());
|
|
237
|
+
procedure
|
|
238
|
+
.input(z.object({ id: z.string() }))
|
|
239
|
+
.use(protect(permissions.post.delete, ({ input }) => loadPost(input.id)))
|
|
240
|
+
.mutation(({ ctx }) => deletePost(ctx.permdockData));
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
`protect` throws `TRPCError` (`FORBIDDEN`, `BAD_REQUEST`, `UNAUTHORIZED`) with Problem Details as `cause`; pass `errorFormatter` to `initTRPC.create` so clients see it under `data`. `ctx.permdock.assert` in a resolver maps to the same error. Read the tenant from each procedure's input (`tenant: (opts) => opts.input?.org`) so a batch never shares one; `request: (ctx) => ctx.req` names the Web `Request` when the context holds it elsewhere. `openapi.security(permission)` is the `trpc-to-openapi` meta fragment. On a subscription, `protect(permission, load, { items })` ends the iterator with `UNAUTHORIZED` or `FORBIDDEN` when the session is revoked or the permission re-denied, and drops items the subscriber cannot read; pass `revocations`.
|
|
244
|
+
|
|
245
|
+
## oRPC — `permdock/orpc`
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import { createPermDock } from "permdock/orpc";
|
|
249
|
+
|
|
250
|
+
export const { permdock, protect, permdockHandler } = createPermDock(policy, {
|
|
251
|
+
subject: ({ context }) => context.user ?? null,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const base = os.$context<Context>().use(permdock());
|
|
255
|
+
base
|
|
256
|
+
.input(z.object({ id: z.string() }))
|
|
257
|
+
.use(protect(permissions.post.delete, ({ input }) => loadPost(input.id)))
|
|
258
|
+
.handler(({ context }) => deletePost(context.permdockData));
|
|
259
|
+
```
|
|
260
|
+
|
|
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.
|
|
262
|
+
|
|
263
|
+
## Vue — `permdock/vue`
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
import { permdockPlugin, Protected, usePermission } from "permdock/vue";
|
|
267
|
+
|
|
268
|
+
createApp(App).use(permdockPlugin, { snapshot, endpoint: "/api/permdock" });
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Composables return refs (`allowed`, `status`, `decision`). Do not import `policy.ts` on the client.
|
|
272
|
+
|
|
273
|
+
## Svelte — `permdock/svelte`
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
import { setPermDock, permission, Protected } from "permdock/svelte";
|
|
277
|
+
|
|
278
|
+
setPermDock({ snapshot, endpoint: "/api/permdock" });
|
|
279
|
+
const canEdit = permission(permissions.post.update, () => post);
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Stores are readable (`$canEdit.allowed`). Do not import `policy.ts` on the client.
|
|
283
|
+
|
|
284
|
+
## Solid — `permdock/solid`
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { PermDockProvider, Protected, usePermission } from 'permdock/solid';
|
|
288
|
+
|
|
289
|
+
<PermDockProvider snapshot={snapshot} endpoint="/api/permdock">
|
|
290
|
+
<App />
|
|
291
|
+
</PermDockProvider>
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`usePermission` takes an accessor for instance data (`() => post`). Do not import `policy.ts` on the client.
|
|
295
|
+
|
|
296
|
+
## OpenTelemetry — `permdock/otel`
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
import { instrument } from "permdock/otel";
|
|
300
|
+
|
|
301
|
+
instrument(permdock, {
|
|
302
|
+
logger: { info: console.info, warn: console.warn },
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
HTTP adapters accept the same options as `otel`. `@opentelemetry/api` is optional; without it the adapter writes only through `logger`.
|
|
307
|
+
|
|
308
|
+
## SCIM — `permdock/scim`
|
|
309
|
+
|
|
310
|
+
No factory. Mount `scimHandler` and pass `directoryMembershipSource(store)` as `memberships`:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
import {
|
|
314
|
+
scimHandler,
|
|
315
|
+
memoryDirectoryStore,
|
|
316
|
+
directoryMembershipSource,
|
|
317
|
+
tenantFromPath,
|
|
318
|
+
} from "permdock/scim";
|
|
319
|
+
|
|
320
|
+
const directory = memoryDirectoryStore();
|
|
321
|
+
export const scim = scimHandler({
|
|
322
|
+
store: directory,
|
|
323
|
+
tenant: (request) => tenantFromPath(request),
|
|
324
|
+
token: { hash: "sha256", lookup: (tenant) => hashFor(tenant) },
|
|
325
|
+
});
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
The handler writes users and groups. It never decides. Unknown or non-assignable role names are stored and dropped when memberships are read. Pass the app's `revocations` feed so a deprovisioned member's open streams revalidate and close.
|
|
329
|
+
|
|
330
|
+
## Cloud — `permdock/cloud`
|
|
331
|
+
|
|
332
|
+
No factory for a `PermDock`. `cloud({ url, key, environment })` returns `approvals`, `sink`, `snapshots` and `policies` to pass into any adapter, plus the environment `issuer` (`<url>/v1/environments/<environment>`) and its `jwks` URL; `cloudEndpoints({ url, environment })` computes the same two without a key. It never implements `MembershipSource` or `RoleSource` and never decides.
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
import { cloud } from "permdock/cloud";
|
|
336
|
+
|
|
337
|
+
const permdockCloud = cloud({
|
|
338
|
+
url: process.env.PERMDOCK_CLOUD_URL,
|
|
339
|
+
key: process.env.PERMDOCK_CLOUD_KEY,
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
export const { getPermDock } = createPermDock(policy, {
|
|
343
|
+
subject,
|
|
344
|
+
store: permdockCloud.approvals,
|
|
345
|
+
sink: permdockCloud.sink,
|
|
346
|
+
snapshots: permdockCloud.snapshots,
|
|
347
|
+
});
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Hosted grants are opt-in per permission. List the permissions a Cloud admin may grant in `definePolicy(..., { hostable: [permissions.auditLog.read] })`, pass `verifier: joseTokenVerifier({ jwks: cloudEndpoints({ url, environment }).jwks })` to `cloud()` (the policy document's issuer and audience are both the environment URL), forward `policies: permdockCloud.policies`, and call `permdockCloud.policies.refresh()` on a timer. Never mark a permission `hostable` when `permdock rls` compiles its table (`permdock doctor` PD020).
|
|
351
|
+
|
|
352
|
+
`PERMDOCK_CLOUD_URL` and `PERMDOCK_CLOUD_KEY` are server-only. Production `PERMDOCK_CLOUD_URL` is `https://api.permdock.com`. The dashboard is `https://app.permdock.com`; the read-only MCP server is `https://mcp.permdock.com`. A Cloud outage leaves directory memberships at their last synced state.
|
|
353
|
+
|
|
354
|
+
## Better Auth — `permdock/better-auth`
|
|
355
|
+
|
|
356
|
+
```ts
|
|
357
|
+
import {
|
|
358
|
+
subjectFromBetterAuth,
|
|
359
|
+
betterAuthRoleSource,
|
|
360
|
+
} from "permdock/better-auth";
|
|
361
|
+
|
|
362
|
+
const session = await auth.api.getSession({ headers });
|
|
363
|
+
const subject = await subjectFromBetterAuth(auth, session);
|
|
364
|
+
const permdock = await createPermDock(policy, subject, {
|
|
365
|
+
customRoles: betterAuthRoleSource(auth),
|
|
366
|
+
});
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
Pass the server `getSession` result only. A null session is anonymous. Never call `hasPermission` on the request path.
|
|
370
|
+
|
|
371
|
+
## Clerk — `permdock/clerk`
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
import { subjectFromClerk } from "permdock/clerk";
|
|
375
|
+
|
|
376
|
+
export const { getPermDock } = createPermDock(policy, {
|
|
377
|
+
subject: async () => subjectFromClerk(await auth()),
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
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.
|
|
382
|
+
|
|
383
|
+
## Supabase — `permdock/supabase`
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
import { subjectFromSupabase, authorizeSql } from "permdock/supabase";
|
|
387
|
+
|
|
388
|
+
const subject = subjectFromSupabase(claims, {
|
|
389
|
+
roles: "user_role",
|
|
390
|
+
tenant: "tenant_id",
|
|
391
|
+
memberships: "memberships",
|
|
392
|
+
declared: ["member", "admin"],
|
|
393
|
+
});
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
Write the claims with `permdock supabase hook generate`; `permdock supabase inspect --json` prints the manifest (helper names, tenant claim, budget, claims written) a package such as better-supabase reads before it writes Storage or Realtime policies against the helpers. 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.dev/docs/adapters/rls)); the generic RLS workflow is in the `permdock-data` skill.
|
|
397
|
+
|
|
398
|
+
With better-supabase, map its session with `subjectFromSupabaseSession(session, { plans: 'features' })`: the active tenant's `features` become `principal.plans`. 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` narrows that to read-only permissions. 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
|
+
|
|
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.supabaseContext.jwtClaims` in its Hono adapter). 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
|
+
|
|
402
|
+
Share links reach RLS through `exchangeCapability`; see the `permdock-credentials` skill.
|
|
403
|
+
|
|
404
|
+
Supabase specifics for generated RLS (the generic flags and parity checks are in the `permdock-data` skill):
|
|
405
|
+
|
|
406
|
+
- 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 hook's `supabase_auth_admin` grants with `--grants-out` into a migration created by `supabase migration new` after the first `db diff`; `db diff` drops those grants. Keep the helpers part ahead of every file that calls the helpers in `schema_paths` (`permdock doctor` PD042, PD043).
|
|
408
|
+
- `--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
|
+
- 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 })`.
|
|
410
|
+
- 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
|
+
|
|
412
|
+
## Supabase middleware pipeline — `permdock/supabase/middleware`
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
import { pipeline } from "@supabase/middleware";
|
|
416
|
+
import { withClaims } from "@supabase/server/middleware/claims";
|
|
417
|
+
import { subjectFromSupabase } from "permdock/supabase";
|
|
418
|
+
import { createPermDock } from "permdock/supabase/middleware";
|
|
419
|
+
|
|
420
|
+
const { withPermDock, permdockHandler } = createPermDock(policy, {
|
|
421
|
+
subject: (ctx) =>
|
|
422
|
+
subjectFromSupabase(ctx.jwtClaims, {
|
|
423
|
+
roles: "user_role",
|
|
424
|
+
declared: ["member", "admin"],
|
|
425
|
+
}),
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// ctx.permdock inside the handler
|
|
429
|
+
export default {
|
|
430
|
+
fetch: pipeline([withClaims(), withPermDock()], async (req, ctx) =>
|
|
431
|
+
ctx.permdock.can(permissions.post.update, await loadPost(req))
|
|
432
|
+
? Response.json({ ok: true })
|
|
433
|
+
: Response.json({ ok: false }, { status: 403 }),
|
|
434
|
+
),
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
// or a route guard with Problem Details
|
|
438
|
+
pipeline(
|
|
439
|
+
[
|
|
440
|
+
withClaims(),
|
|
441
|
+
withPermDock({
|
|
442
|
+
protect: permissions.post.publish,
|
|
443
|
+
data: (_ctx, req) => loadPost(req),
|
|
444
|
+
}),
|
|
445
|
+
],
|
|
446
|
+
handler,
|
|
447
|
+
);
|
|
448
|
+
|
|
449
|
+
// AuthZEN evaluations
|
|
450
|
+
pipeline([withClaims()], permdockHandler());
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
`@supabase/middleware` is the optional peer (already installed with `@supabase/server`). `withPermDock` requires `jwtClaims` upstream: place it after `withClaims` or `withRequiredClaims`, never first. Run `ctx.supabaseAdmin` / `withPostgresAdminClient` only after `ctx.permdock.assert(...)`; they bypass RLS.
|
|
454
|
+
|
|
455
|
+
## Remote PDP — `permdock/pdp`
|
|
456
|
+
|
|
457
|
+
```ts
|
|
458
|
+
import { createPermDock, remotePdp } from "permdock/pdp";
|
|
459
|
+
|
|
460
|
+
const policy = definePolicy(permissions, {
|
|
461
|
+
roles: [member],
|
|
462
|
+
subject: (user) => user && { id: user.id, roles: user.roles },
|
|
463
|
+
providers: [
|
|
464
|
+
remotePdp({ url: process.env.PDP_URL, auth: { bearer: () => token } }),
|
|
465
|
+
],
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const permdock = await createPermDock(policy, user);
|
|
469
|
+
await permdock.can(permissions.post.read, post);
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Use `createPermDock` from `permdock/pdp` when `providers` is set. Core `can` / `decide` stay synchronous and deny delegated permissions with `pdp-unavailable`; on the PDP instance `filter` and `where` are async too.
|
|
473
|
+
|
|
474
|
+
For OpenFGA or SpiceDB, use `openfga({ url, storeId, map })` or `spicedb({ url, token, map })` as the provider. `map` is one `[permission, (subject, row) => tuple]` pair per delegated permission; `filter` and `where` then use `list-objects` / `LookupResources` ids, so the tuple ids must equal the resource's `id` field. The app writes the tuples.
|
|
475
|
+
|
|
476
|
+
## Collect-only frameworks (no package)
|
|
477
|
+
|
|
478
|
+
Nuxt, Astro, React Router, TanStack Start and Effect have no `permdock/<name>` entry. Add `createPermDockUnplugin.vite()` from `permdock/unplugin` (or `.webpack` / `.esbuild`). Runtime is `permdock/server` or the matching HTTP adapter, plus `permdock/vue`, `permdock/react` or `permdock/svelte` on the client. Effect Schema is a Standard Schema; Effect HttpApi uses Overlay, not a hook.
|
|
479
|
+
|
|
480
|
+
Remaining work follows the names on the adapter page under `/docs/adapters/<name>`. Do not invent identifiers.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Policy options and CLI starting points
|
|
2
|
+
|
|
3
|
+
Owning pages: [policies](https://permdock.dev/docs/concepts/policies), [decisions](https://permdock.dev/docs/concepts/decisions), [CLI](https://permdock.dev/docs/cli).
|
|
4
|
+
|
|
5
|
+
## Usage limits
|
|
6
|
+
|
|
7
|
+
A `limit: { count, per }` grant needs `limits: memoryLimitStore()` (or the app's `LimitStore`) on `createPermDock`; without it the grant denies with `limit-unavailable`. `can` never consumes a unit: a mutation that spends one calls `decide` or `assert`.
|
|
8
|
+
|
|
9
|
+
- `mode: 'soft'` keeps granting past the count with an `over-limit` obligation; `alertAt: 0.8` adds a `near-limit` warning.
|
|
10
|
+
- A granted decision carries `quota: { remaining, resetsAt }`. An exhausted hard limit answers `429` with `Retry-After` and `RateLimit` headers; for the header format, defer to the `ratelimit-headers` spec skill (`npx skills add ScaleDockHQ/scaledock-skills --skill ratelimit-headers`).
|
|
11
|
+
- Run `testLimitStore` from `permdock/testing` on a custom store.
|
|
12
|
+
|
|
13
|
+
## Hiding rows
|
|
14
|
+
|
|
15
|
+
On a resource whose ids must not confirm a row exists (a private repository, a patient record), set `disclosure: 'hide'`: a denied row then answers `404` like a missing one.
|
|
16
|
+
|
|
17
|
+
## Plan gates
|
|
18
|
+
|
|
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
|
+
|
|
21
|
+
## Validity windows
|
|
22
|
+
|
|
23
|
+
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).
|
|
24
|
+
|
|
25
|
+
## Step-up
|
|
26
|
+
|
|
27
|
+
Hardware-key step-up is `to: assurance({ amr: ['hwk'], maxAge: 300 })` and renders `/step-up-required`.
|
|
28
|
+
|
|
29
|
+
## Existing apps
|
|
30
|
+
|
|
31
|
+
Adopting PermDock in an app with its own keys, SQL helpers and tokens is a step-by-step guide ([existing apps](https://permdock.dev/docs/getting-started/existing-apps)):
|
|
32
|
+
|
|
33
|
+
- 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
|
+
- Put application-owned metadata in `meta.x`; PermDock carries it and never reads it.
|
|
35
|
+
- Rename a key later with `definePermissions(tree, { renamed: { "old.key": "new.key" } })`. Remove the alias only when `permdock doctor` PD055 and PD056 are quiet; `permdock diff` reports the removal as the breaking `alias-removed`.
|
|
36
|
+
- Generate helpers beside legacy SQL with `permdock rls generate --helpers-only --shims`, then rewrite policies with `permdock rls migrate --write`.
|
|
37
|
+
|
|
38
|
+
## CLI starting points
|
|
39
|
+
|
|
40
|
+
- 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
|
+
- When the repo has Arazzo workflows, run `permdock arazzo check --doc <arazzo> --openapi <doc>`.
|
|
42
|
+
- 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.
|