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.
Files changed (290) hide show
  1. package/README.md +268 -1
  2. package/dist/a2a/index.d.ts +178 -0
  3. package/dist/a2a/index.js +271 -0
  4. package/dist/ai-sdk/index.d.ts +74 -0
  5. package/dist/ai-sdk/index.js +137 -0
  6. package/dist/approval-required-TzNB9oQA.js +6 -0
  7. package/dist/approvals/index.d.ts +3 -0
  8. package/dist/approvals/index.js +4 -0
  9. package/dist/approvals-DaxY-Bdg.js +218 -0
  10. package/dist/approvers-aH0PJkmf.js +20 -0
  11. package/dist/arazzo-CTeVuXr5.js +70 -0
  12. package/dist/arazzo-aoTkqYsJ.js +54 -0
  13. package/dist/authzen/index.d.ts +48 -0
  14. package/dist/authzen/index.js +336 -0
  15. package/dist/better-auth/index.d.ts +92 -0
  16. package/dist/better-auth/index.js +324 -0
  17. package/dist/budget-JqcRqyhK.d.ts +11 -0
  18. package/dist/budget-NbEtbJC_.js +11 -0
  19. package/dist/capability-B68OFnZx.js +171 -0
  20. package/dist/capability-BDG_nkxt.d.ts +99 -0
  21. package/dist/catalog/index.d.ts +12 -0
  22. package/dist/catalog/index.js +2 -0
  23. package/dist/catalog-B0E6bggc.js +48 -0
  24. package/dist/catalog-H-vFGwWK.js +55 -0
  25. package/dist/catalog-doc-BSo_Q_-6.js +298 -0
  26. package/dist/catalog-idsHc0vo.js +2 -0
  27. package/dist/catalog-path-CnI0XTFK.js +12 -0
  28. package/dist/claude-agent/index.d.ts +81 -0
  29. package/dist/claude-agent/index.js +83 -0
  30. package/dist/clerk/index.d.ts +56 -0
  31. package/dist/clerk/index.js +280 -0
  32. package/dist/cli/bin.d.ts +1 -0
  33. package/dist/cli/bin.js +20 -0
  34. package/dist/cli/index.d.ts +46 -0
  35. package/dist/cli/index.js +5 -0
  36. package/dist/cloud/index.d.ts +91 -0
  37. package/dist/cloud/index.js +390 -0
  38. package/dist/cloud-BNQTrmrr.js +50 -0
  39. package/dist/cloud-Cuxr6Czw.js +117 -0
  40. package/dist/collect-B7_TIfmg.js +49 -0
  41. package/dist/collect-O-tOtbnk.js +350 -0
  42. package/dist/collect-fRPAQIWb.js +2 -0
  43. package/dist/commands-BMh_Etm2.js +93 -0
  44. package/dist/compact-y6vfu2_f.js +19 -0
  45. package/dist/compare-pfyVnBjE.js +12 -0
  46. package/dist/compile-CQbvyIBr.d.ts +30 -0
  47. package/dist/compile-D22iwvuV.js +558 -0
  48. package/dist/config-BPYmjTOp.js +26 -0
  49. package/dist/connection-JvqwHODC.js +194 -0
  50. package/dist/context-CKEAabd7.js +73 -0
  51. package/dist/context-gtAwAILx.js +6 -0
  52. package/dist/convex/index.d.ts +40 -0
  53. package/dist/convex/index.js +55 -0
  54. package/dist/create-BKF3dgFk.d.ts +162 -0
  55. package/dist/create-CSkhDU9T.js +222 -0
  56. package/dist/credential-B_rkUNlY.js +326 -0
  57. package/dist/credentials-BgRX3WEg.js +268 -0
  58. package/dist/describe-Bt1YD5uS.js +99 -0
  59. package/dist/describe-DernyItf.d.ts +37 -0
  60. package/dist/diff-BUzf5Poi.js +48 -0
  61. package/dist/diff-x5Il8LVa.js +418 -0
  62. package/dist/digest-B3kc_PgQ.d.ts +19 -0
  63. package/dist/digest-BI-6aa6d.js +27 -0
  64. package/dist/doctor-BGle8Gqi.js +1314 -0
  65. package/dist/doctor-Bqo6FwUa.js +53 -0
  66. package/dist/drizzle/index.d.ts +72 -0
  67. package/dist/drizzle/index.js +160 -0
  68. package/dist/duration-xK2jGFVc.js +25 -0
  69. package/dist/elysia/index.d.ts +71 -0
  70. package/dist/elysia/index.js +103 -0
  71. package/dist/errors-DSMnAVNa.js +43 -0
  72. package/dist/errors-DbZ1YOnb.d.ts +138 -0
  73. package/dist/errors-Iy71s7Oj.js +177 -0
  74. package/dist/errors-m3u9TmDu.js +27 -0
  75. package/dist/evaluations-DsAU9ppl.js +479 -0
  76. package/dist/eve/index.d.ts +95 -0
  77. package/dist/eve/index.js +181 -0
  78. package/dist/express/index.d.ts +46 -0
  79. package/dist/express/index.js +108 -0
  80. package/dist/fastify/index.d.ts +56 -0
  81. package/dist/fastify/index.js +141 -0
  82. package/dist/files-B_wAGYmt.js +121 -0
  83. package/dist/fixtures-Cvaw0gDG.js +64 -0
  84. package/dist/freeze-y1_jQL4v.js +31 -0
  85. package/dist/from-snapshot-BMGnhmgv.js +552 -0
  86. package/dist/generate-CH7ThJ14.js +173 -0
  87. package/dist/grantee-Bv04_Vzf.js +521 -0
  88. package/dist/headers-B5RRv3Xm.js +6 -0
  89. package/dist/headers-C8SHQccD.d.ts +6 -0
  90. package/dist/helpers-DnSOrhB9.js +682 -0
  91. package/dist/hono/index.d.ts +70 -0
  92. package/dist/hono/index.js +131 -0
  93. package/dist/hosted-BwKQ1BTA.js +265 -0
  94. package/dist/http-DPuDaChb.d.ts +13 -0
  95. package/dist/http-GgVApX1k.js +111 -0
  96. package/dist/index-CltcQ4Bf.d.ts +138 -0
  97. package/dist/index-LF_hS-yn.d.ts +177 -0
  98. package/dist/index-fG6NSXlc.d.ts +9 -0
  99. package/dist/index-xIxJLwOQ.d.ts +415 -0
  100. package/dist/index.d.ts +11 -0
  101. package/dist/index.js +21 -0
  102. package/dist/jwt/index.d.ts +87 -0
  103. package/dist/jwt/index.js +571 -0
  104. package/dist/kernel-YOGCWNLf.js +211 -0
  105. package/dist/kysely/index.d.ts +77 -0
  106. package/dist/kysely/index.js +129 -0
  107. package/dist/load-BZ24CpZP.js +69 -0
  108. package/dist/load-QOOm1z70.js +2 -0
  109. package/dist/load-jose-BL6LH4bL.js +11 -0
  110. package/dist/lru-8q4WZjBV.js +28 -0
  111. package/dist/manifest-DDQbC6K0.d.ts +118 -0
  112. package/dist/map-DowSO96j.js +158 -0
  113. package/dist/map-claims-B_6I1Rn-.js +360 -0
  114. package/dist/map-error-RJ3ZsUyA.js +16 -0
  115. package/dist/markers-B0hYLvMD.js +60 -0
  116. package/dist/may-use-CnFEjVEp.js +37 -0
  117. package/dist/mcp/index.d.ts +127 -0
  118. package/dist/mcp/index.js +563 -0
  119. package/dist/nest/index.d.ts +80 -0
  120. package/dist/nest/index.js +270 -0
  121. package/dist/next/client.d.ts +24 -0
  122. package/dist/next/client.js +31 -0
  123. package/dist/next/index.d.ts +110 -0
  124. package/dist/next/index.js +195 -0
  125. package/dist/next/plugin.d.ts +14 -0
  126. package/dist/next/plugin.js +2 -0
  127. package/dist/node/index.d.ts +43 -0
  128. package/dist/node/index.js +66 -0
  129. package/dist/openai/index.d.ts +71 -0
  130. package/dist/openai/index.js +105 -0
  131. package/dist/openapi/index.d.ts +93 -0
  132. package/dist/openapi/index.js +3 -0
  133. package/dist/openapi-CwePaF7C.js +164 -0
  134. package/dist/openapi-DWD0Uc_4.js +250 -0
  135. package/dist/openapi-DcqM8nU7.js +306 -0
  136. package/dist/openapi-import-BWRUNNeW.js +5959 -0
  137. package/dist/openapi-schema-D8j_T9tO.js +6902 -0
  138. package/dist/orpc/index.d.ts +64 -0
  139. package/dist/orpc/index.js +188 -0
  140. package/dist/otel/index.d.ts +13 -0
  141. package/dist/otel/index.js +202 -0
  142. package/dist/package-root-zGkxO3oN.js +30 -0
  143. package/dist/parse-DPfIvFMU.js +135 -0
  144. package/dist/pdp/index.d.ts +22 -0
  145. package/dist/pdp/index.js +795 -0
  146. package/dist/peer-BOr_kJE1.js +31 -0
  147. package/dist/permdock-yoZ7EjxX.js +2956 -0
  148. package/dist/permissions-B4apnahQ.d.ts +185 -0
  149. package/dist/permissions-B8vCg_bI.d.ts +52 -0
  150. package/dist/permissions-CofRqesP.js +142 -0
  151. package/dist/permissions-DC7GROPA.js +600 -0
  152. package/dist/pg-4GJWDUxG.js +953 -0
  153. package/dist/pins-CqE1jZuF.js +12 -0
  154. package/dist/plugin-4gr7IxFJ.js +65 -0
  155. package/dist/policy-ANp36PnI.js +971 -0
  156. package/dist/policy-CInmNGkM.d.ts +1959 -0
  157. package/dist/policy-Cq2fB8Q5.d.ts +20 -0
  158. package/dist/prisma/index.d.ts +135 -0
  159. package/dist/prisma/index.js +430 -0
  160. package/dist/problem-Cgp9-KZA.js +226 -0
  161. package/dist/prompt-CXYhLfsS.js +32 -0
  162. package/dist/protected-B4K3BKoX.js +138 -0
  163. package/dist/protected-BuPAycJT.d.ts +26 -0
  164. package/dist/protected-wY7nXhY9.d.ts +23 -0
  165. package/dist/provider-_wx46HpB.js +48 -0
  166. package/dist/provider-wTAUda2v.d.ts +6 -0
  167. package/dist/react/index.d.ts +6 -0
  168. package/dist/react/index.js +6 -0
  169. package/dist/react/provider-client.d.ts +2 -0
  170. package/dist/react/provider-client.js +3 -0
  171. package/dist/react-native/index.d.ts +42 -0
  172. package/dist/react-native/index.js +174 -0
  173. package/dist/refs-ChLSxbST.js +90 -0
  174. package/dist/relations-r4soIFZB.js +426 -0
  175. package/dist/revocations-Cy06JNx2.d.ts +21 -0
  176. package/dist/rls-BOFU3chG.js +233 -0
  177. package/dist/rls-CJVja9Ww.js +2119 -0
  178. package/dist/rls-graph-DxQ39PJj.js +371 -0
  179. package/dist/rls-helpers-BxRuif0l.js +1346 -0
  180. package/dist/rls-import-CXV6LBlS.js +942 -0
  181. package/dist/rls-migrate-DrFPbf8p.js +366 -0
  182. package/dist/row-check-BajTgQZ-.d.ts +43 -0
  183. package/dist/run-CJ6UvS3a.js +155 -0
  184. package/dist/runtime-DP9ttFh1.js +27 -0
  185. package/dist/saas-BRrZzJkM.js +1193 -0
  186. package/dist/schema-P8yoma__.js +296 -0
  187. package/dist/scim/index.d.ts +2 -0
  188. package/dist/scim/index.js +2 -0
  189. package/dist/scim-9NFa0XVZ.js +1445 -0
  190. package/dist/scopes-cKVp_WTw.js +212 -0
  191. package/dist/server/index.d.ts +123 -0
  192. package/dist/server/index.js +8 -0
  193. package/dist/sha256-BO0swRXL.js +150 -0
  194. package/dist/sink-Bd9PSyfq.js +141 -0
  195. package/dist/sink-Cfr9-7YC.d.ts +104 -0
  196. package/dist/skills-C_lpV4Jx.js +50 -0
  197. package/dist/skills-Dx3QLnMY.js +110 -0
  198. package/dist/snapshot-DE8dE_hz.js +1051 -0
  199. package/dist/solid/index.d.ts +63 -0
  200. package/dist/solid/index.js +197 -0
  201. package/dist/source-DoBBYJi2.js +6 -0
  202. package/dist/sources-DOpW5Ila.d.ts +347 -0
  203. package/dist/sources-DVHM3Mwq.js +284 -0
  204. package/dist/sql-Bll1d7y0.js +22 -0
  205. package/dist/src-CTNT-PBW.js +430 -0
  206. package/dist/ssf/index.d.ts +3 -0
  207. package/dist/ssf/index.js +622 -0
  208. package/dist/store-Bq90395i.d.ts +27 -0
  209. package/dist/store-C6hFW6ta.js +508 -0
  210. package/dist/stores-CkG2HrSg.js +140 -0
  211. package/dist/stores-CvOnu_2T.d.ts +41 -0
  212. package/dist/stream-BhMfZOwr.d.ts +12 -0
  213. package/dist/stream-Q_xlB6Ko.js +52 -0
  214. package/dist/style-CTm1AXDT.js +22 -0
  215. package/dist/subject-C6dLxqHU.js +24 -0
  216. package/dist/subject-settings-xt7V845g.js +82 -0
  217. package/dist/supabase/index.d.ts +67 -0
  218. package/dist/supabase/index.js +4 -0
  219. package/dist/supabase/middleware.d.ts +77 -0
  220. package/dist/supabase/middleware.js +68 -0
  221. package/dist/supabase-D8UlBA7Z.js +83 -0
  222. package/dist/supabase-DTxGtYdi.js +796 -0
  223. package/dist/supabase-hook-DW-xP4hU.js +2 -0
  224. package/dist/supabase-hook-D_OqGb_V.js +1528 -0
  225. package/dist/svelte/Protected.svelte +34 -0
  226. package/dist/svelte/index.d.ts +10 -0
  227. package/dist/svelte/index.js +53 -0
  228. package/dist/svelte/runtime.d.ts +6 -0
  229. package/dist/svelte/runtime.js +3 -0
  230. package/dist/svelte/source.d.ts +10 -0
  231. package/dist/svelte/source.js +8 -0
  232. package/dist/terminal/index.d.ts +171 -0
  233. package/dist/terminal/index.js +821 -0
  234. package/dist/testing/index.d.ts +656 -0
  235. package/dist/testing/index.js +3208 -0
  236. package/dist/testing/saas/index.d.ts +194 -0
  237. package/dist/testing/saas/index.js +3 -0
  238. package/dist/testing/saas/permissions.d.ts +2 -0
  239. package/dist/testing/saas/permissions.js +2 -0
  240. package/dist/text-diff-BdkkYSbi.js +15 -0
  241. package/dist/thenable-okc2xDsj.js +10 -0
  242. package/dist/timeout-D51niuQP.js +8 -0
  243. package/dist/trpc/index.d.ts +80 -0
  244. package/dist/trpc/index.js +205 -0
  245. package/dist/types-B8w5PFtS.d.ts +141 -0
  246. package/dist/types-B9dmiiab.d.ts +66 -0
  247. package/dist/types-BBK-exvJ.d.ts +177 -0
  248. package/dist/types-BvBk1wgn.d.ts +118 -0
  249. package/dist/types-CxquUByI.d.ts +9 -0
  250. package/dist/types-D6Z6-rKJ.d.ts +312 -0
  251. package/dist/types-DbWdEfcm.d.ts +87 -0
  252. package/dist/types-bva3i4Gf.d.ts +98 -0
  253. package/dist/types-i4BAOvdb.d.ts +97 -0
  254. package/dist/unplugin/index.d.ts +7 -0
  255. package/dist/unplugin/index.js +30 -0
  256. package/dist/usage-1ozMubLY.js +42 -0
  257. package/dist/usage-CUnK6Ou9.js +530 -0
  258. package/dist/usage-PyEor6GA.js +2 -0
  259. package/dist/validity-D0uRmJKJ.js +224 -0
  260. package/dist/verifier-B90agnhl.js +461 -0
  261. package/dist/version-CnSh33Fj.js +16 -0
  262. package/dist/vocabulary-Dk0fs-Jk.d.ts +56 -0
  263. package/dist/vue/index.d.ts +57 -0
  264. package/dist/vue/index.js +203 -0
  265. package/dist/webmcp/index.d.ts +75 -0
  266. package/dist/webmcp/index.js +313 -0
  267. package/package.json +423 -4
  268. package/rulesets/permdock.yaml +29 -0
  269. package/schemas/catalog-v1.json +507 -0
  270. package/schemas/openapi/oas-3.1.json +1298 -0
  271. package/schemas/openapi/oas-3.2.json +1516 -0
  272. package/schemas/openapi/overlay-1.1.json +79 -0
  273. package/schemas/supabase-claims-v1.json +139 -0
  274. package/schemas/supabase-manifest-v1.json +217 -0
  275. package/skills/permdock/SKILL.md +66 -0
  276. package/skills/permdock-agents/SKILL.md +77 -0
  277. package/skills/permdock-agents/references/runtimes.md +211 -0
  278. package/skills/permdock-approvals/SKILL.md +80 -0
  279. package/skills/permdock-approvals/references/stores-and-resume.md +83 -0
  280. package/skills/permdock-audit/SKILL.md +67 -0
  281. package/skills/permdock-credentials/SKILL.md +55 -0
  282. package/skills/permdock-credentials/references/tokens-keys-links.md +127 -0
  283. package/skills/permdock-data/SKILL.md +57 -0
  284. package/skills/permdock-data/references/queries-and-rls.md +54 -0
  285. package/skills/permdock-data/references/relationships.md +65 -0
  286. package/skills/permdock-tenancy/SKILL.md +77 -0
  287. package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +83 -0
  288. package/skills/permdock-wire/SKILL.md +116 -0
  289. package/skills/permdock-wire/references/adapters.md +480 -0
  290. package/skills/permdock-wire/references/policy-options.md +43 -0
@@ -0,0 +1,211 @@
1
+ # Agent runtimes
2
+
3
+ Every agent adapter exports `createPermDock` from `permdock/<runtime>` and takes `subject`, `actor`, `delegation`, `tools` and `store`. One permission per tool. Instance actions load the row through `data`; tool input arrives as `unknown`, so parse it before loading. The current adapter page is `https://permdock.dev/docs/adapters/<name>`.
4
+
5
+ ## AI SDK — `permdock/ai-sdk`
6
+
7
+ ```ts
8
+ import { createPermDock } from "permdock/ai-sdk";
9
+ import { z } from "zod";
10
+
11
+ const PostArgs = z.object({ id: z.string() });
12
+
13
+ export const { toolApproval, capabilityMiddleware, needsApproval } =
14
+ createPermDock(policy, {
15
+ subject: ({ runtimeContext }) => runtimeContext.user,
16
+ actor: ({ runtimeContext }) => ({
17
+ id: runtimeContext.agentId,
18
+ kind: "ai-sdk",
19
+ }),
20
+ delegation: () => ({ scopes: [permissions.post.delete.scope] }),
21
+ tools: {
22
+ delete_post: {
23
+ permission: permissions.post.delete,
24
+ data: (args) => loadPost(PostArgs.parse(args).id),
25
+ },
26
+ },
27
+ });
28
+ ```
29
+
30
+ Pass `toolApproval` into `generateText` / `ToolLoopAgent`. Wrap the model with `wrapLanguageModel({ model, middleware: capabilityMiddleware({ user }) })` per caller. Use `needsApproval(permissions.post.delete)` only on `WorkflowAgent`. `denied` maps to `'denied'`, `approval-required` to `'user-approval'`, and `'not-applicable'` is never returned ([AI SDK adapter](https://permdock.dev/docs/adapters/ai-sdk)).
31
+
32
+ ## Claude Agent SDK — `permdock/claude-agent`
33
+
34
+ ```ts
35
+ import { createPermDock } from "permdock/claude-agent";
36
+
37
+ export const { canUseTool, permissionRequestHook } = createPermDock(policy, {
38
+ subject: () => user,
39
+ actor: () => ({ id: "claude", kind: "claude-agent" }),
40
+ delegation: () => ({ scopes: [permissions.post.delete.scope] }),
41
+ tools: {
42
+ delete_post: {
43
+ permission: permissions.post.delete,
44
+ data: (args) => loadPost(args),
45
+ },
46
+ },
47
+ });
48
+ ```
49
+
50
+ `canUseTool` returns `{ behavior: 'allow', updatedInput }` or `{ behavior: 'deny', message }`; approval-required is a deny whose message carries the pending token. Pass a `store`: once a reviewer approves, the retried call is allowed exactly once. `mcp__` tools are trusted only from `mcpSources` (default `['sdk']`). Pass `permissionRequestHook` under `hooks.PermissionRequest` ([Claude Agent adapter](https://permdock.dev/docs/adapters/claude-agent)).
51
+
52
+ ## Eve — `permdock/eve`
53
+
54
+ ```ts
55
+ import { createPermDock } from "permdock/eve";
56
+
57
+ export const { approval, approvalFor, permdock } = createPermDock(policy, {
58
+ delegation: () => ({ scopes: [permissions.post.delete.scope] }),
59
+ tools: {
60
+ delete_post: {
61
+ permission: permissions.post.delete,
62
+ data: (args) => loadPost(args),
63
+ },
64
+ },
65
+ });
66
+ ```
67
+
68
+ Pass `approval` as the tool's `approval`. Default subject and actor read `session.auth.initiator` / `current`. `approval.request(ctx)` maps granted to Eve's `not-applicable` (continue), approval-required to `user-approval`, denied to `{ type: 'denied', reason }`; Eve's re-check of a call nobody approved is denied. `approval.response(ctx)` maps the responder through the same `subject`. Use a durable `store` when replicas share sessions ([Eve adapter](https://permdock.dev/docs/adapters/eve)).
69
+
70
+ ## OpenAI Agents SDK — `permdock/openai`
71
+
72
+ ```ts
73
+ import { createPermDock } from "permdock/openai";
74
+
75
+ export const { needsApproval, guardTools, resolveInterruptions, permdock } =
76
+ createPermDock(policy, {
77
+ subject: (ctx) => ctx.user,
78
+ actor: (ctx) => ({ id: ctx.agentId, kind: "openai" }),
79
+ delegation: (ctx) => ({ scopes: ctx.scopes }),
80
+ tools: {
81
+ delete_post: {
82
+ permission: permissions.post.delete,
83
+ data: (args) => loadPost(args),
84
+ },
85
+ },
86
+ });
87
+ ```
88
+
89
+ `needsApproval(permission)` is the tool's `needsApproval`; it reads `runContext.context` and is true unless granted. `guardTools` drops tools with no grant. `resolveInterruptions(state, interruptions, { context })` approves or rejects each pause and returns the still-pending `ApprovalRequest`s. On resume, rebuild the context from the session and use `RunState.fromStringWithContext` ([OpenAI adapter](https://permdock.dev/docs/adapters/openai)).
90
+
91
+ ## MCP — `permdock/mcp`
92
+
93
+ ```ts
94
+ import { createPermDock } from "permdock/mcp";
95
+
96
+ export const { protectServer } = createPermDock(policy, {
97
+ subject: (authInfo) => userFrom(authInfo), // or subjectFromMcp
98
+ requireAuthInfo: true, // HTTP behind bearer auth
99
+ store,
100
+ });
101
+
102
+ const server = protectServer(
103
+ new McpServer({ name: "posts", version: "1.0.0" }),
104
+ );
105
+ server.registerTool(
106
+ "delete_post",
107
+ {
108
+ permission: permissions.post.delete,
109
+ inputSchema: z.object({ id: z.string() }),
110
+ data: ({ id }) => loadPost(id),
111
+ },
112
+ handler,
113
+ );
114
+ ```
115
+
116
+ `actor.kind` is `'mcp-client'`, and `delegation` comes from the token's scopes and `authorization_details`. Every `registerTool` / `registerResource` / `registerPrompt` needs a `permission`. Lists are filtered per caller. A missing scope is an `insufficient_scope` step-up (HTTP `403`) naming only the scope the call needs. Set `resource` to the server's URL so tokens for another audience are refused. Denied calls return `isError: true` with Decision `structuredContent`. `approval-required` parks the call in `store` and returns `isError: true` with the token, or an `input_required` URL request when `approval.at` is set and the client declares URL elicitation; the retried call runs once after approval (token optional under `_meta["dev.permdock/approval"]`, never from tool arguments) ([MCP adapter](https://permdock.dev/docs/adapters/mcp)).
117
+
118
+ An MCP server that is not an SDK `McpServer` cannot be wrapped: narrow the tool's `meta` with `isPermission`, filter its tool list with `mayUse(permdock, permission)` from `permdock`, and decide each call with `permdock.decide(permission, args)` in its `authorize` hook. `mayUse` is a listing hint, never a decision.
119
+
120
+ ## A2A — `permdock/a2a`
121
+
122
+ ```ts
123
+ import { createPermDock } from "permdock/a2a";
124
+
125
+ export const { agentCard, extendedAgentCard, protectSkill } = createPermDock(
126
+ policy,
127
+ {
128
+ subject: (auth) => userFrom(auth),
129
+ card: {
130
+ name: "Posts agent",
131
+ url: "https://agent.example.com/a2a",
132
+ version: "1.0.0",
133
+ },
134
+ securitySchemes: { oauth: { type: "oauth2" } },
135
+ skills: {
136
+ summarise: { permission: permissions.post.read },
137
+ publish: {
138
+ permission: permissions.post.publish,
139
+ data: (task) => loadPost(task.postId),
140
+ },
141
+ },
142
+ },
143
+ );
144
+
145
+ app.get("/.well-known/agent-card.json", (c) => c.json(agentCard()));
146
+ app.get("/a2a/extended-card", (c) => c.json(extendedAgentCard(c.get("auth"))));
147
+ app.post(
148
+ "/a2a/tasks",
149
+ protectSkill((task) => task.skillId),
150
+ handleTask,
151
+ );
152
+ ```
153
+
154
+ Identity comes from transport auth, never the task body. `actor.kind` is `'a2a'`, with `delegation` from the caller's token. Each skill's `securityRequirements` name the permission's `scope`. The public card is never filtered; `extendedAgentCard(auth)` keeps only skills the caller has a grant for. An instance-level skill without `data` throws at startup ([A2A adapter](https://permdock.dev/docs/adapters/a2a)).
155
+
156
+ ## WebMCP — `permdock/webmcp`
157
+
158
+ Client entry. No factory. Register snapshot-allowed tools on `document.modelContext`:
159
+
160
+ ```ts
161
+ import { registerTools } from "permdock/webmcp";
162
+ import { approvalHeaders, usePermDock } from "permdock/react";
163
+
164
+ const permdock = usePermDock();
165
+ const controller = new AbortController();
166
+ registerTools(document.modelContext, permissions.post, {
167
+ permdock,
168
+ signal: controller.signal,
169
+ handlers: {
170
+ update: async ({ input, token }) =>
171
+ api.posts.update(input, { headers: approvalHeaders(token) }),
172
+ },
173
+ });
174
+ ```
175
+
176
+ Never import a policy into this entry. A missing `document.modelContext` is a no-op. Tools re-register when the snapshot changes. The client check is a filter: the server adapter behind `api.posts` makes the decision. `onApprovalRequired` opens the page's own confirmation dialog ([WebMCP adapter](https://permdock.dev/docs/adapters/webmcp)).
177
+
178
+ ## Web Bot Auth on HTTP adapters
179
+
180
+ ```ts
181
+ import {
182
+ createPermDock,
183
+ discoverViaSignatureAgent,
184
+ verifyWebBotAuth,
185
+ } from "permdock/hono";
186
+
187
+ const keys = discoverViaSignatureAgent({ allow: ["agents.example.com"] });
188
+
189
+ export const { permdock, protect } = createPermDock(policy, {
190
+ subject: (c) => c.get("user"),
191
+ webBotAuth: (request) => verifyWebBotAuth(request, { verify: true, keys }),
192
+ });
193
+ ```
194
+
195
+ A verified RFC 9421 signer becomes the actor with `kind: 'web-bot-auth'`; `delegation` comes from a bearer token on the same request, otherwise it is empty. A failed signature throws `InvalidSignatureError`, never an anonymous actor ([Web Bot Auth](https://permdock.dev/docs/standards/web-bot-auth)).
196
+
197
+ ## Terminal — `permdock/terminal`
198
+
199
+ ```ts
200
+ import { createPermDock } from "permdock/terminal";
201
+
202
+ export const { permdock, protect, filterCommands, format, exitCode } =
203
+ createPermDock(policy, {
204
+ subject: async ({ token }) => {
205
+ const jwt = await token(["env", "keychain", "ci-oidc", "device"]);
206
+ return jwt ? subjectFromJwt(jwt, { issuer, audience: "acme-cli" }) : null;
207
+ },
208
+ });
209
+ ```
210
+
211
+ For the app's own CLI; not the `permdock` binary. Never accept `--user` or `--actor` as identity. In CI, verify the job token with `subjectFromCiOidc(jwt, { provider: 'github', audience })` from `permdock/jwt`, which returns a `workload` principal, never a user. A `destructive` permission asks for the resource id to be typed, and exits `64` without a terminal unless `--yes` is passed; `--yes` never approves an `approval-required` call. `--dry-run` decides and exits with the outcome's code without running the action ([terminal adapter](https://permdock.dev/docs/adapters/terminal)).
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: permdock-approvals
3
+ description: Requires human approval on PermDock grants and resumes the approved call. Use when adding approval to a destructive or money-moving action (delete, publish, pay, refund, approve, transfer), choosing approvers with approval by, user(), relation(), distinct, quorum, ttl, escalation or staleOn, ordering approvers with mode and stages, loading approval rules as data with an ApprovalPolicySource, separating preparer and approver with exclusiveWith, mounting approvalsHandler with an ApprovalStore, resuming with the PermDock-Approval header, mapping approval-required to an agent runtime, MCP elicitation or AG-UI interrupt, or fixing doctor PD017, PD018 or PD024.
4
+ license: MIT
5
+ metadata:
6
+ author: ScaleDockHQ
7
+ homepage: https://permdock.dev/docs/security/approvals
8
+ repository: https://github.com/ScaleDockHQ/permdock
9
+ ---
10
+
11
+ # PermDock approvals
12
+
13
+ `approval-required` is the third outcome: the subject may do it, but a person other than the requester confirms each call. An approval never grants something the subject could not do, and a `deny` still wins ([approval security](https://permdock.dev/docs/security/approvals)). 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
+ - Which actions need a person: deletions, publishing, payments, refunds, access changes, anything an agent can repeat.
18
+ - Who may approve each one (a role, a plan, `assurance()`, one named user, or a relation on the row such as the expense's manager), how many approvers (quorum), whether several groups sign in order, how long an ask may wait, and who steps in when nobody answers.
19
+ - Whether organisations configure their own approval rules at runtime ("payments over 1000 need finance").
20
+ - Whether the approver judges the row's content (an amount, a text), so a changed row needs a new approval.
21
+ - Whether a user confirming their own agent's call is the intent anywhere.
22
+ - Where pending requests live: one process (`memoryApprovalStore`), the app's database, or PermDock Cloud.
23
+ - Which runtimes ask: HTTP, AI SDK, Claude Agent SDK, Eve, OpenAI Agents SDK, MCP, A2A, WebMCP, a chat UI.
24
+
25
+ ## Invariants
26
+
27
+ 1. The requester never approves by default. The actor is always refused. The principal is refused unless the grant sets `distinct: false`, and `requireDistinctApprover` on the handler refuses it even then.
28
+ 2. The approver comes from authentication on the approval surface, never from the request body, a tool argument or a link.
29
+ 3. One approval resumes one call. The token binds permission, resource, subject and actor, the resume re-runs `decide`, and `consume` spends the approval once.
30
+ 4. `can` returns `false` for `approval-required`. Only `decide`, `assert` and the adapters see the third outcome.
31
+ 5. Quorum, ttl and escalation live on the grant, not in the store. A grant's `ttl` can shorten the store's window, never extend it.
32
+ 6. A `relation()` approver is matched only through facts the approvals handler reads from the app's `RelationSource` at verdict time, never from a request body.
33
+ 7. Approval rules kept as data only add stages. A source that fails to load denies every call an allow would grant.
34
+
35
+ ## Workflow
36
+
37
+ 1. **Mark the grants.** Add `approval: 'human'` (anyone authenticated but the requester) or `approval: { by }` with the same selectors as `to`:
38
+
39
+ ```ts
40
+ allow(permissions.payout.release, {
41
+ approval: {
42
+ by: roles.finance,
43
+ quorum: 2,
44
+ ttl: "30m",
45
+ escalation: { after: "4h", to: roles.owner },
46
+ },
47
+ });
48
+ ```
49
+
50
+ For sign-off in order, use `mode: 'sequential'` with `stages: [{ by, quorum? }]` (or `mode: 'all'` for any order); one person signs one stage. `user('u_cfo')` names one person, and `relation(permissions.expense, 'manager')` names whoever holds the relation on the row.
51
+ Add `distinct: false` only where the user confirms their own agent's call and no money, deletion or access change sits behind it.
52
+ ✓ `permdock doctor` reports no PD017 (sensitive verb without approval), and every PD024 (`distinct: false`) is deliberate.
53
+
54
+ 2. **Bind content when it matters.** Declare `version: 'updatedAt'` (or a revision counter) on the `resource()` and add `staleOn: 'resource-change'` to the approval, so a changed row resumes as `stale-approval` and asks again.
55
+ ✓ `definePolicy` accepts the grant (it throws when the resource has no `version` or the action is a collection action).
56
+ 3. **Split workflow verbs.** Model `prepare`, `approve`, `pay`, `settle` and `submit` as separate leaves. Put the approval on `approve` / `pay`, and use `exclusiveWith` on `role()` so a preparer cannot also hold the approver role.
57
+ ✓ `permdock doctor` reports no PD018 (exclusive roles held together).
58
+ 4. **Store and handler.** Create one `ApprovalStore`, pass it as `store` to every adapter, and mount `approvalsHandler(store, { subject })` for approvers; add `relations` and `permissions` when a grant uses `relation()` approvers. -> [references/stores-and-resume.md](references/stores-and-resume.md)
59
+ ✓ A second authenticated user can approve; the actor and the requester get `403`.
60
+ 5. **Rules as data (optional).** Pass `approvalPolicies` (an `ApprovalPolicySource`, `memoryApprovalPolicies` in process) to `createPermDock` for approval rules organisations configure. -> [references/stores-and-resume.md](references/stores-and-resume.md#approval-policies-as-data)
61
+ ✓ `testApprovalPolicySource` from `permdock/testing` passes on the source.
62
+ 6. **Surface and resume.** Map the outcome to each runtime's approval hook and resume with the token: the `PermDock-Approval` header over HTTP, the runtime's own resume for agents. -> [references/stores-and-resume.md](references/stores-and-resume.md#surfaces)
63
+ ✓ The approved call runs once; a second resume is denied with `approval-consumed`.
64
+ 7. **Test.** Add a scenario test per approval grant: the ask, an approval by an eligible user, a refused self-approval, and the single resume ([scenario testing](https://permdock.dev/docs/guides/scenario-testing)). Run `testApprovalStore` on a custom store.
65
+ ✓ The tests pass.
66
+
67
+ ## Verify before done
68
+
69
+ - [ ] Every destructive or money-moving action an agent or a user can trigger has `approval`, or a `deny` on the same leaf.
70
+ - [ ] Each `distinct: false` (PD024) is a deliberate self-confirmation with nothing irreversible behind it.
71
+ - [ ] Roles that must not be held together set `exclusiveWith`, and PD018 is clean.
72
+ - [ ] The store survives the deployment: no `memoryApprovalStore` on serverless or across replicas.
73
+ - [ ] No resume token is read from model output or tool arguments, and each resume re-runs `decide`.
74
+ - [ ] A custom `ApprovalStore` passes `testApprovalStore` from `permdock/testing`.
75
+ - [ ] Grants with `relation()` approvers have a handler with `relations`, and nothing fills `verdict.relations` from client input.
76
+
77
+ ## Reference index
78
+
79
+ - [references/stores-and-resume.md](references/stores-and-resume.md): `approvalsHandler` routes, custom stores, relation approvers, approval policies as data, the token, resume details and denial causes, the runtime surfaces table, delivery to chat and email.
80
+ - Docs: [approval security](https://permdock.dev/docs/security/approvals), [approvals adapter](https://permdock.dev/docs/adapters/approvals), [wire formats](https://permdock.dev/docs/concepts/wire-formats).
@@ -0,0 +1,83 @@
1
+ # Approval stores and the resume flow
2
+
3
+ The API and the store contract are on the [approvals adapter](https://permdock.dev/docs/adapters/approvals); the token and the runtime table are on [approval security](https://permdock.dev/docs/security/approvals).
4
+
5
+ ## Store and handler
6
+
7
+ ```ts
8
+ import { approvalsHandler, memoryApprovalStore } from "permdock/approvals";
9
+
10
+ export const store = memoryApprovalStore({ ttl: 60 * 60 * 1000 }); // default: one hour
11
+
12
+ // Every agent and HTTP adapter takes the same option
13
+ export const { protect } = createPermDock(policy, { subject, store });
14
+
15
+ // Routes for approvers: GET /pending, GET /mine, GET /:token, POST /:token/approve, POST /:token/reject
16
+ const handler = approvalsHandler(store, {
17
+ subject: (request) => approverFromSession(request), // the approver, from real authentication
18
+ requireDistinctApprover: false, // true refuses the principal even where a grant sets distinct: false
19
+ relations, // RelationSource: needed when a grant uses relation() approvers
20
+ permissions, // the permission tree, for relation approvers reached through links
21
+ });
22
+ app.all("/permdock/approvals/*", (c) => handler(c.req.raw));
23
+ ```
24
+
25
+ - `memoryApprovalStore` is per process. On serverless or with several replicas, implement `ApprovalStore` over the app's database; the Drizzle recipe on the adapter page is the template, and `cloud().approvals` from `permdock/cloud` is the hosted implementation of the same interface.
26
+ - A custom store calls `assertApprover(request, by, requireDistinct, now, verdict.relations)` in `resolve` and computes the next record with `applyApprovalVerdict`, which records each signature's `stage`. `verdict.relations` holds `approverRelationKey` values that the handler, or `resolveApproval(store, token, verdict, { relations, permissions })`, computed with `approverRelations`; a store never derives them. `consume` must be atomic: two concurrent calls never both succeed.
27
+ - Run `testApprovalStore(store, { reopen })` from `permdock/testing` on every custom store.
28
+ - `cancelApprovals` rejects matching pending requests (for example when a session ends).
29
+
30
+ ## Approval policies as data
31
+
32
+ ```ts
33
+ import { memoryApprovalPolicies } from "permdock";
34
+
35
+ const permdock = await createPermDock(policy, user, {
36
+ approvalPolicies: memoryApprovalPolicies([
37
+ {
38
+ permission: "expense.pay",
39
+ tenant: "o_acme", // absent: every tenant
40
+ actors: ["agent"], // absent: every call
41
+ where: { op: "gt", field: "amount", value: 1000 },
42
+ approval: { by: "finance" }, // no escalation in data
43
+ },
44
+ ]),
45
+ });
46
+ ```
47
+
48
+ - Entries are read once per instance and combine with the grant's own approval as stages (`sequential` if any part is, else `all`); the shortest `ttl` wins.
49
+ - A throw or an invalid entry denies with detail `approval-policy-unavailable`. An unknown permission applies to nothing.
50
+ - Client snapshots do not see the entries; the server's `decide` adds the approval.
51
+
52
+ ## Resume
53
+
54
+ 1. `decide` returns `approval-required` with a deterministic `token`. The adapter calls `store.create`; `create` is idempotent per token.
55
+ 2. The runtime surfaces the ask (table below). A person answers through `approvalsHandler`, the runtime's own UI or the Cloud inbox.
56
+ 3. The original call is retried with the token. The adapter re-runs `decide`, recomputes the token, compares it, and calls `store.consume`. Only then does the tool or route run.
57
+
58
+ The token binds the permission key, the resource id (or a digest of the data when there is no id), the principal, the tenant, the actor and the matched grant's conditions. Under `staleOn: 'resource-change'` it also binds the row's `version`.
59
+
60
+ Over HTTP, the client retries the same request with the `PermDock-Approval: <token>` header. In React, `approvalHeaders(token)` builds that header and `useApproval(decision)` requests and polls an approval from the UI.
61
+
62
+ A bad resume is denied with detail `approval-not-found`, `approval-pending`, `approval-rejected`, `approval-expired`, `approval-consumed` or `approval-mismatch`; a changed row is `stale-approval`. `approvalsHandler` answers `403` for an ineligible approver (`approver-not-eligible`, the actor, or the principal without `distinct: false`) and `409` for a repeated one (`approver-repeated`).
63
+
64
+ ## Surfaces
65
+
66
+ | Runtime | Ask | Resume |
67
+ | --------------------------------------- | ----------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- |
68
+ | AI SDK `generateText` / `ToolLoopAgent` | `toolApproval` returns `'user-approval'` | The SDK's re-check of the same call is denied unless the store holds an approved record |
69
+ | AI SDK `WorkflowAgent` | `needsApproval(permission)` suspends the workflow | The workflow resumes; the token is recomputed |
70
+ | Claude Agent SDK | `canUseTool` denies with the token and records the request | The retry consumes the approved record once |
71
+ | Eve | `approval.request` returns `user-approval` | `approval.response` checks the responder |
72
+ | OpenAI Agents SDK | `needsApproval` pauses the run with `interruptions` | `resolveInterruptions` applies the store's verdict |
73
+ | MCP | `input_required` URL elicitation to `approval.at`, otherwise an `isError` result with the token | The retried call re-runs `decide` and consumes |
74
+ | HTTP adapters | `403` Problem Details, type `.../approval-required`, with `token` | Retry with `PermDock-Approval` |
75
+ | A2A | The task enters input-required with the token | The caller continues the task |
76
+ | WebMCP | The handler is not run; `onApprovalRequired` opens the page's dialog | The page calls the guarded route with the token |
77
+ | Chat UI over AG-UI | The backend emits the request as an AG-UI human-in-the-loop or custom event | The answer returns on the stream; the backend recomputes the token |
78
+
79
+ For AG-UI event shapes, defer to the `ag-ui` spec skill (`npx skills add ScaleDockHQ/scaledock-skills --skill ag-ui`).
80
+
81
+ ## Delivery
82
+
83
+ Notifying an approver is a listener on the `approval` event (`permdock.on('approval', …)`), never a package. A message or email carries only the token and links to a page where the approver signs in; the verdict goes through `approvalsHandler` with that session's subject. A link never approves on its own ([approvals adapter, Delivery](https://permdock.dev/docs/adapters/approvals#delivery)).
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: permdock-audit
3
+ description: Reviews an existing PermDock setup or a pull request that changes it. Use when auditing permissions, reviewing a PR that touches permissions.ts, policy.ts or an adapter guard, finding ungranted or unused leaves, missing human approvals on agent tools, closures that should be portable, validate never, untested grants, or mapping an app to OWASP ASI02 and ASI03. Use whenever the user asks to audit, review or harden PermDock.
4
+ license: MIT
5
+ metadata:
6
+ author: ScaleDockHQ
7
+ homepage: https://permdock.dev/docs/for-ai-agents
8
+ repository: https://github.com/ScaleDockHQ/permdock
9
+ ---
10
+
11
+ # Audit PermDock
12
+
13
+ Read the app's `permissions.ts`, `policy.ts`, factory file and every adapter guard, then write the findings as a Markdown checklist the user can turn into pull requests. Every finding names the file and the permission or role.
14
+
15
+ ## Inputs (find out, or ask before starting)
16
+
17
+ - The scope: the whole app, or one pull request against its base branch.
18
+ - Whether the `permdock` binary runs here (`permdock usage --json`, `permdock doctor --json`); otherwise walk the files and the catalog by hand.
19
+ - Which topic areas the app uses: agent runtimes, approvals, named scopes and custom roles, ORM queries or RLS, tokens, API keys or share links.
20
+
21
+ ## Invariants
22
+
23
+ 1. Report, never fix silently. Each finding is one line with a file and a permission or role.
24
+ 2. Anything that widens access or fails open leads the report.
25
+ 3. Expected outcomes in tests are written by hand, never computed by calling `decide` or `can`.
26
+ 4. A topic skill's Verify list is the checklist for its area; this skill does not repeat it.
27
+
28
+ ## Workflow
29
+
30
+ 1. **Inventory.** List every leaf from `definePermissions`, every role from `defineRoles` / `role()`, every plan from `definePlans`, and every grant from `definePolicy` (`grants`, role bindings, `delegations`). Note which entries import the policy (server, agent) and which import only the snapshot (client).
31
+ ✓ Every leaf, role, plan and grant is in the report, including leaves with zero grants.
32
+ 2. **Ungranted and unused.** A leaf no `allow` mentions is ungranted: grant it or delete it. A granted leaf never passed to `can`, `decide`, `assert`, `protect`, `tools` or `<Protected>` is unused; confirm with `usage --json` or a repo-wide search for the reference.
33
+ ✓ Every leaf is tagged granted and used, granted and unused, or ungranted.
34
+ 3. **Validation and identity.**
35
+ - `validate: 'never'` on a policy that sees HTTP bodies, MCP arguments or model output is a finding.
36
+ - Client entries (`permdock/react` and the client half of a framework adapter) import permissions and snapshots only.
37
+ - `subject` and `actor` come from the host session or `subjectFrom*`, never from tool arguments or a model-supplied id.
38
+ - A `limit` grant without `limits` on `createPermDock` always denies with `limit-unavailable`, and a mutation that only calls `can` never spends a unit.
39
+ - On Supabase, every `ctx.supabaseAdmin`, `withPostgresAdminClient` or `service_role` client sits behind `permdock.assert(...)` (or `withPermDock({ protect })`), and `subjectFromSupabase` receives `getClaims()` output or `ctx.jwtClaims`, never `getSession().access_token` or `user_metadata`.
40
+ ✓ Each check has a pass or a file-level finding.
41
+ 4. **Topic checks.** For each area the app uses, run the Verify list of its skill (`npx skills add ScaleDockHQ/PermDock --skill <name>`): `permdock-agents`, `permdock-approvals`, `permdock-tenancy`, `permdock-data`, `permdock-credentials`.
42
+ ✓ Every item of every applicable Verify list has a pass or a finding.
43
+ 5. **Tests.** No `describePolicy` suite, or one with `exhaustive: false`, is a finding: a new permission can ship untested. A custom `ApprovalStore`, `DecisionSink`, `MembershipSource`, `RoleSource`, `SnapshotSource`, `LimitStore`, `RelationSource` or `DirectoryStore` without its `test<Interface>` runner from `permdock/testing` is a finding.
44
+ ✓ Every policy and custom store has its runner, or a finding.
45
+ 6. **ASI02 and ASI03.** Map the inventory onto [OWASP Agentic](https://permdock.dev/docs/security/owasp-agentic); for the threat list itself, defer to the `owasp-agentic` spec skill (`npx skills add ScaleDockHQ/scaledock-skills --skill owasp-agentic`).
46
+ - **ASI02** (tool misuse): unmapped tools, a collection permission on a row handler, a destructive tool without `approval`, capability lists that include denied tools.
47
+ - **ASI03** (identity and privilege abuse): a model-supplied subject or actor, an actor used as approver, `distinct: false` on a sensitive action, a resume token not bound to permission, resource, subject and actor, API keys stored in plain text or created outside `decideCredential`, keys without expiry (PD029), and a `memberships` source applied to service-key subjects.
48
+ ✓ Each agent adapter in the app has an ASI02 line and an ASI03 line.
49
+ 7. **Pull request review**, when the scope is one PR. Review only what the diff changes:
50
+ - Run `permdock collect --check` and `permdock usage --json` on both sides and compare. New leaves with no grant, grants for leaves nobody checks, and new `undeclared` or `outsideInclude` findings are comments.
51
+ - A new or widened `allow` (a role gains a leaf, a `where` is dropped or loosened, a `to` becomes `anyone()` or `authenticated()`), a new policy `delegations` entry, or a removed `deny` needs a matching `describePolicy` row in the same PR.
52
+ - A new model-reachable tool, or a new destructive action on an existing one, goes through the `permdock-agents` and `permdock-approvals` Verify lists.
53
+ - A change to the `rls` config or generated RLS comes with `permdock rls generate --check` passing and an `rlsParity` or `rls verify` run.
54
+ - `permdock doctor --json` on the head branch reports no new error.
55
+ ✓ Every changed grant, leaf and tool has a comment on the changed line or an explicit pass.
56
+
57
+ ## Verify before done
58
+
59
+ - [ ] The report leads with blockers: fail-open paths, a trusted model subject, a destructive tool without approval.
60
+ - [ ] Then ungranted and unused leaves, then closures that could be portable, then test gaps.
61
+ - [ ] Every finding names a file and a permission or role.
62
+ - [ ] The report ends with suggested PRs, one finding per PR when the fixes are independent.
63
+
64
+ ## Reference index
65
+
66
+ - Topic Verify lists: the `permdock-agents`, `permdock-approvals`, `permdock-tenancy`, `permdock-data` and `permdock-credentials` skills.
67
+ - Docs: [OWASP Agentic mapping](https://permdock.dev/docs/security/owasp-agentic), [threat model](https://permdock.dev/docs/security/threat-model), [`permdock doctor`](https://permdock.dev/docs/cli/doctor), [`permdock usage`](https://permdock.dev/docs/cli/usage), [testing](https://permdock.dev/docs/adapters/testing).
@@ -0,0 +1,55 @@
1
+ ---
2
+ name: permdock-credentials
3
+ description: Resolves PermDock subjects from access tokens, API keys and share links. Use when verifying OAuth or OIDC access tokens with subjectFromJwt or an introspection response with subjectFromIntrospection, issuing personal access tokens, API keys or service accounts with decideCredential and resolving them with subjectFromApiKey, setting tenant key rules (maxTtl, kinds, allowNoExpiry) in a SettingsSource, minting guest or share links with signCapability and resolving them with subjectFromCapability, serving a link through RLS with exchangeCapability, or fixing doctor PD029 and on('auth') causes such as invalid-chain.
4
+ license: MIT
5
+ metadata:
6
+ author: ScaleDockHQ
7
+ homepage: https://permdock.dev/docs/concepts/authentication
8
+ repository: https://github.com/ScaleDockHQ/permdock
9
+ ---
10
+
11
+ # PermDock credentials
12
+
13
+ Authentication is upstream: core never verifies a token. A `subjectFrom*` resolver verifies the material and hands core a `Subject`, and anything it cannot verify becomes the anonymous subject, never a throw ([authentication](https://permdock.dev/docs/concepts/authentication)). 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
+ - Which credentials reach the app: browser sessions, OAuth or OIDC access tokens, introspected tokens, API keys, CI job tokens, share links.
18
+ - For tokens: the issuer, the resource identifier (audience) and the signing algorithms.
19
+ - For keys: personal (`user`) keys, service accounts (`service`), or both; where the hashes are stored; the tenant rules on lifetime and kinds.
20
+ - For links: what a link opens, who may redeem it, how long it lives, and whether Postgres RLS must serve the guest page.
21
+
22
+ ## Invariants
23
+
24
+ 1. Core never verifies a token or a session. Only `permdock/jwt`, `permdock/server` key helpers and the provider `subjectFrom*` helpers do.
25
+ 2. A resolver never throws. An unverifiable token, key or link is the anonymous subject, and the cause goes to `on('auth')`.
26
+ 3. Never build grants from user-editable claims (`user_metadata`), and never take a subject from a request body, an unsigned header, a CLI flag or a model argument.
27
+ 4. Store only `hashApiKey(key)` and show the key once. Every key lists its permissions and expires unless the tenant opts in with `allowNoExpiry`.
28
+ 5. A share link is a signed, expiring capability on one resource. It never holds a scope or global role, and RLS sees it only through `exchangeCapability`, never `service_role`.
29
+ 6. Token rules come from the spec skills (`npx skills add ScaleDockHQ/scaledock-skills --skill <name>`): `jwt` for JOSE, `oauth` for access tokens and introspection, `openid-connect` for ID tokens and Discovery.
30
+
31
+ ## Workflow
32
+
33
+ 1. **Verify access tokens.** Resolve the subject with `subjectFromJwt(token, { discovery, audience })` from `permdock/jwt`, or `subjectFromIntrospection` when the app already called introspection. -> [references/tokens-keys-links.md](references/tokens-keys-links.md)
34
+ ✓ An ID token, a token for another audience and an expired token each resolve to the anonymous subject.
35
+ 2. **Issue API keys, when the product has them.** Guard the creation endpoint with its own permission, call `decideCredential(permdock, request, { settings })`, store the hash with the returned `Credential`, and show the key once. -> [references/tokens-keys-links.md](references/tokens-keys-links.md#api-keys-and-service-accounts)
36
+ ✓ A service key wider than its creator is denied with `exceeds-creator`.
37
+ 3. **Resolve API keys.** Build `subjectFromApiKey({ verifier: apiKeyVerifier({ find }), permissions, owner, revoked, settings, sink })` and call it as `resolveKey(key, { tenant })`.
38
+ ✓ A revoked or expired key is anonymous, and a user key never exceeds its owner's current rights.
39
+ 4. **Share links, when the product has them.** Declare the link's roles on the resource, mint with `signCapability` behind a guard of its own, and resolve with `subjectFromCapability` and the tenant's `linkPolicy`. With Supabase RLS, exchange the link with `exchangeCapability` and generate with `--capabilities`. -> [references/tokens-keys-links.md](references/tokens-keys-links.md#share-links)
40
+ ✓ An expired, revoked or replayed (`once`) link resolves to the anonymous subject.
41
+ 5. **Test.** Add scenario tests for each credential kind: a valid one, a forged or expired one, and a key or link used past its permissions ([scenario testing](https://permdock.dev/docs/guides/scenario-testing)). Run `permdock doctor`.
42
+ ✓ The tests pass and PD029 is clean.
43
+
44
+ ## Verify before done
45
+
46
+ - [ ] `subjectFromJwt` uses Discovery or an explicit `jwks` plus `issuer`, sets `audience`, and allows only `Ed25519`, `ES256` or `PS256`.
47
+ - [ ] No subject, tenant or membership comes from `user_metadata`, a request body or an unsigned header.
48
+ - [ ] API keys are stored as hashes, created only through `decideCredential` behind their own guard, and expire (PD029).
49
+ - [ ] No `memberships` source is passed to `createPermDock` for service-key subjects.
50
+ - [ ] Share links expire, are minted behind their own guard, and reach RLS only through `exchangeCapability`.
51
+
52
+ ## Reference index
53
+
54
+ - [references/tokens-keys-links.md](references/tokens-keys-links.md): `subjectFromJwt` options, introspection, CI OIDC, `decideCredential`, key storage and resolution, tenant settings, `signCapability`, `subjectFromCapability`, `exchangeCapability`.
55
+ - Docs: [authentication](https://permdock.dev/docs/concepts/authentication), [subject](https://permdock.dev/docs/concepts/subject), [JWT adapter](https://permdock.dev/docs/adapters/jwt), [API keys](https://permdock.dev/docs/concepts/credentials), [link capabilities](https://permdock.dev/docs/concepts/capabilities), [JOSE](https://permdock.dev/docs/standards/jose).
@@ -0,0 +1,127 @@
1
+ # Tokens, API keys and share links
2
+
3
+ Owning pages: [JWT adapter](https://permdock.dev/docs/adapters/jwt), [authentication](https://permdock.dev/docs/concepts/authentication), [API keys](https://permdock.dev/docs/concepts/credentials), [link capabilities](https://permdock.dev/docs/concepts/capabilities).
4
+
5
+ ## Access tokens — `permdock/jwt`
6
+
7
+ ```ts
8
+ import { subjectFromJwt } from "permdock/jwt";
9
+
10
+ const subject = await subjectFromJwt(token, {
11
+ discovery: "https://issuer.example.com",
12
+ audience: "https://api.example.com",
13
+ });
14
+ ```
15
+
16
+ - Prefer `discovery: '<issuer>'` over a hand-copied `jwks` URL. Set `audience` to the resource identifier. Write algorithms as `Ed25519`, `ES256` or `PS256`; never `EdDSA`, `none` or `RSA1_5`.
17
+ - Keep the default `accept: 'access-token'`, so an ID token never authorises an API call.
18
+ - `jose` is an optional peer. A failure is the anonymous subject, never a throw; the cause is reported through `on('auth')`. A claimed `act` chain that does not nest is anonymous with cause `invalid-chain`.
19
+ - When the app already called RFC 7662 or RFC 9767 introspection, pass the response JSON to `subjectFromIntrospection`. `active` other than `true` is anonymous, and the HTTP call stays the app's.
20
+ - In CI, `subjectFromCiOidc(jwt, { provider: 'github', audience })` returns a `workload` principal, never a user.
21
+
22
+ ## API keys and service accounts
23
+
24
+ ```ts
25
+ import { decideCredential } from "permdock";
26
+ import { generateApiKey, hashApiKey } from "permdock/server";
27
+
28
+ const decision = await decideCredential(
29
+ permdock,
30
+ {
31
+ kind: "service", // or 'user'
32
+ id: "svc_01J8",
33
+ tenant: "o_1",
34
+ roles: ["developer"],
35
+ permissions: [permissions.repo.read],
36
+ expiresAt: Math.floor(Date.now() / 1000) + 30 * 86_400,
37
+ },
38
+ { settings },
39
+ );
40
+ if (decision.outcome === "granted") {
41
+ const key = generateApiKey(decision.credential.id);
42
+ await db.insert(apiKeys).values({
43
+ id: decision.credential.id,
44
+ hash: await hashApiKey(key),
45
+ credential: decision.credential,
46
+ });
47
+ return key; // shown once
48
+ }
49
+ ```
50
+
51
+ - Guard the endpoint that creates keys with its own permission (`apiKey.create`).
52
+ - Every key lists 1 to 64 permissions. A `user` key is its owner's live rights narrowed to the key; a `service` key is a `kind: 'service'` principal whose tenant, roles and permissions must be within the creator's `assignableRoles` / `assignablePermissions` (otherwise `exceeds-creator`). A key cannot mint keys.
53
+ - Tenant rules come from a `SettingsSource` (`memorySettings({ o_1: { credentials: { maxTtl, kinds, approval, allowNoExpiry } } })`). A key without `expiresAt` needs `allowNoExpiry`. `approval: true` makes creation `approval-required`; after approval, call `decideCredential` again with `approved: token`.
54
+ - Store only `hashApiKey(key)`. The key is `pdk_<id>_<secret><checksum>`; `parseApiKey` rejects a bad checksum before any lookup.
55
+
56
+ Resolve keys on each request:
57
+
58
+ ```ts
59
+ import { apiKeyVerifier, subjectFromApiKey } from "permdock/server";
60
+
61
+ const resolveKey = subjectFromApiKey({
62
+ verifier: apiKeyVerifier({ find: (id) => findKeyRow(id) }), // returns { credential, hash }
63
+ permissions,
64
+ owner: (id) => loadOwner(id), // the live owner of a user key
65
+ revoked: (id) => isRevoked(id),
66
+ settings,
67
+ sink,
68
+ });
69
+
70
+ const subject = await resolveKey(bearer, { tenant });
71
+ ```
72
+
73
+ Never pass a `memberships` source to `createPermDock` for a service-key subject: its one membership comes from the credential. `memoryCredentials()` is an in-process store for tests.
74
+
75
+ ## Share links
76
+
77
+ ```ts
78
+ import { signCapability } from "permdock";
79
+ import { subjectFromCapability } from "permdock/jwt";
80
+
81
+ // Declare the link's roles on the resource
82
+ role("guest", [allow(permissions.quote.read, { where: { status: "sent" } })], {
83
+ on: permissions.quote,
84
+ });
85
+
86
+ // Mint, behind a guard of its own (quote.share)
87
+ const token = await signCapability(
88
+ {
89
+ id: linkId,
90
+ on: { resource: permissions.quote, id: quote.id },
91
+ roles: ["guest"],
92
+ expiresAt,
93
+ },
94
+ signer,
95
+ { audience },
96
+ );
97
+
98
+ // Resolve
99
+ const subject = await subjectFromCapability(token, {
100
+ jwks,
101
+ issuer,
102
+ audience,
103
+ revoked,
104
+ replay,
105
+ viewer,
106
+ linkPolicy,
107
+ });
108
+ ```
109
+
110
+ - The result is a `kind: 'link'` principal with one `{ on, roles, via: 'link' }` membership. It never holds a scope or global role.
111
+ - `linkPolicy` returns the tenant's `LinkPolicy` (`maxLifetime`, `redeemers`, `once`). Passing the same policy to `signCapability` refuses to mint a link the resolver would refuse.
112
+ - Never an unguessable id without expiry, and never a security-definer RPC for the guest page.
113
+
114
+ With Supabase, let RLS serve the link through an exchange, never the link token itself:
115
+
116
+ ```ts
117
+ import { exchangeCapability } from "permdock/supabase";
118
+
119
+ const accessToken = await exchangeCapability(subject, {
120
+ key: signingJwk,
121
+ alg: "ES256",
122
+ kid: "permdock-links",
123
+ ttl: 300,
124
+ });
125
+ ```
126
+
127
+ `accessToken` is `role: 'anon'` with a `capability` claim and no `sub`, or `undefined` for anything but a live link. Generate the matching policies with `permdock rls generate --capabilities`.