permdock 0.4.0 → 0.6.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 (515) hide show
  1. package/README.md +37 -35
  2. package/dist/{schema-Bol6gqXA.js → 2020-C9VN-x8c.js} +5 -130
  3. package/dist/a2a/index.d.ts +6 -6
  4. package/dist/a2a/index.js +14 -17
  5. package/dist/{adapter-context-mlWsVGGg.js → adapter-context-CRhqyzCk.js} +1 -1
  6. package/dist/adoption-scan-DCYe60Pi.js +107 -0
  7. package/dist/agent/index.d.ts +3 -0
  8. package/dist/agent/index.js +1 -0
  9. package/dist/ai-sdk/index.d.ts +18 -35
  10. package/dist/ai-sdk/index.js +37 -23
  11. package/dist/{app-data-CV8pqqMi.js → app-data-MuyzVqX8.js} +2 -1
  12. package/dist/approval-header-CJACtHkv.js +23 -0
  13. package/dist/approval-prompt-Dg5LeAcT.js +62 -0
  14. package/dist/approval-prompt-DvSSPQJX.d.ts +57 -0
  15. package/dist/approvals/index.d.ts +5 -3
  16. package/dist/approvals/index.js +8 -5
  17. package/dist/approvals-p1PGoVjb.js +995 -0
  18. package/dist/arazzo-41WuzHou.js +36 -0
  19. package/dist/{arazzo-C4BcskbX.js → arazzo-CaUVBn7D.js} +20 -14
  20. package/dist/args-IIolxxKA.js +197 -0
  21. package/dist/astro/index.d.ts +43 -0
  22. package/dist/astro/index.js +27 -0
  23. package/dist/audience-DabClFLQ.js +55 -0
  24. package/dist/auth-DRLEizbL.js +178 -0
  25. package/dist/authzen/index.d.ts +26 -6
  26. package/dist/authzen/index.js +94 -79
  27. package/dist/automation-BNM3TtDo.d.ts +114 -0
  28. package/dist/automation-DVjOckjV.js +157 -0
  29. package/dist/better-auth/index.d.ts +8 -6
  30. package/dist/better-auth/index.js +9 -8
  31. package/dist/better-supabase/index.d.ts +357 -30
  32. package/dist/better-supabase/index.js +936 -194
  33. package/dist/{bind-kk-uyCwH.js → bind-CAvgUuKs.js} +14 -10
  34. package/dist/{bind-BzqG8SMb.d.ts → bind-DOyrv6zi.d.ts} +12 -10
  35. package/dist/body-CH5of8Ne.js +10 -0
  36. package/dist/body-text-QcXok_SI.js +52 -0
  37. package/dist/{build-instance-DIIV9DbC.js → build-instance-Bu_eDqYw.js} +4 -3
  38. package/dist/{canonical-json-CFKyFOke.js → canonical-json-BC8_nMTw.js} +5 -3
  39. package/dist/{capability-CksyCWDH.d.ts → capability-Drg-1YhL.d.ts} +44 -25
  40. package/dist/{capability-txATtKV4.js → capability-N1K9IpqS.js} +68 -42
  41. package/dist/casbin-Cr7RLtA3.js +30 -0
  42. package/dist/casbin-Dyu2gnUD.js +234 -0
  43. package/dist/casl-64gpT-8x.js +24 -0
  44. package/dist/casl-DjEDhTCP.js +299 -0
  45. package/dist/catalog/index.d.ts +74 -2
  46. package/dist/catalog/index.js +51 -2
  47. package/dist/catalog-BoWglDPI.js +1 -0
  48. package/dist/catalog-DZVbrF-z.js +52 -0
  49. package/dist/{catalog-tZUyOfOv.js → catalog-HD2gf5mY.js} +51 -6
  50. package/dist/{catalog-doc-DWtg0gGR.js → catalog-doc-Cdn7-0pQ.js} +45 -23
  51. package/dist/catalog-fingerprint-CCP0-yQS.js +153 -0
  52. package/dist/chat/index.d.ts +2 -0
  53. package/dist/chat/index.js +224 -0
  54. package/dist/chat-sdk/index.d.ts +87 -0
  55. package/dist/chat-sdk/index.js +197 -0
  56. package/dist/claims-map-7oQ3ue4X.js +13 -0
  57. package/dist/claims-map-Ca-QX8C6.d.ts +32 -0
  58. package/dist/claude-agent/index.d.ts +7 -14
  59. package/dist/claude-agent/index.js +24 -27
  60. package/dist/clerk/index.d.ts +9 -8
  61. package/dist/clerk/index.js +9 -14
  62. package/dist/cli/bin.js +36 -17
  63. package/dist/cli/index.d.ts +19 -5
  64. package/dist/cli/index.js +164 -4
  65. package/dist/cloud/index.d.ts +102 -63
  66. package/dist/cloud/index.js +239 -340
  67. package/dist/cloud-ytQ3uviA.js +87 -0
  68. package/dist/codemod-5mpSpnUY.js +37 -0
  69. package/dist/codemod-ua2hHy7w.js +2084 -0
  70. package/dist/collect-Ci6akVwH.js +55 -0
  71. package/dist/collect-D7eQDffE.js +1 -0
  72. package/dist/{collect-2SrB7lfk.js → collect-mLTY4nwb.js} +75 -11
  73. package/dist/command-L8EstFNm.js +6 -0
  74. package/dist/compile/index.d.ts +1 -1
  75. package/dist/compile/index.js +1 -2
  76. package/dist/{compile-CtTP7eur.d.ts → compile-1bsAezL0.d.ts} +5 -3
  77. package/dist/{compile-CcEu2ZQ1.js → compile-CMqMx_IN.js} +11 -8
  78. package/dist/completion-BTFrHZhX.js +143 -0
  79. package/dist/config-Bj2Ixq3d.js +26 -0
  80. package/dist/{config-BIjREpyE.js → config-bs_kZeYq.js} +12 -7
  81. package/dist/{config-report--Xb9APJS.js → config-report-DSpsLpCE.js} +15 -5
  82. package/dist/{connection-DUsczpB5.js → connection-DjhIJSs2.js} +6 -5
  83. package/dist/{context-DieZVJXu.js → context-B_u3_bDJ.js} +2 -2
  84. package/dist/convex/index.d.ts +11 -10
  85. package/dist/convex/index.js +6 -4
  86. package/dist/create-B30OMUOJ.d.ts +107 -0
  87. package/dist/create-CBMmFie-.d.ts +138 -0
  88. package/dist/create-CGm28gyI.js +130 -0
  89. package/dist/create-CQWIpUG-.js +403 -0
  90. package/dist/{create-BBO24vj6.js → create-DCF2SvoT.js} +31 -32
  91. package/dist/create-D_NmPOdH.d.ts +110 -0
  92. package/dist/{credential-kue0qpbm.js → credential-BcoiejpY.js} +152 -31
  93. package/dist/credentials-DPyHmYz5.js +74 -0
  94. package/dist/delegation-j6-rc37o.js +286 -0
  95. package/dist/delivery/index.d.ts +5 -0
  96. package/dist/delivery/index.js +5 -0
  97. package/dist/delivery-DCAgNyCn.js +499 -0
  98. package/dist/delivery-event-xX4obB8F.js +40 -0
  99. package/dist/{denied-text-DfRk15uw.d.ts → denied-text-CiFQNuMV.d.ts} +5 -5
  100. package/dist/{describe-C3ldA9os.d.ts → describe-B5qgDvEW.d.ts} +2 -2
  101. package/dist/{describe-Cgkp7eP0.js → describe-BybMRY7A.js} +6 -2
  102. package/dist/device-BARQ8eYG.js +193 -0
  103. package/dist/{diff-rpqoe7Ag.js → diff-3jbx6Rm4.js} +60 -54
  104. package/dist/diff-CmuSD3vc.js +37 -0
  105. package/dist/doctor-3ZqNHX_G.js +44 -0
  106. package/dist/{doctor-owlv6ddh.js → doctor-BdZw53-k.js} +245 -50
  107. package/dist/drizzle/index.d.ts +5 -3
  108. package/dist/drizzle/index.js +2 -2
  109. package/dist/elysia/index.d.ts +16 -13
  110. package/dist/elysia/index.js +13 -12
  111. package/dist/env-DJZrPjRi.js +8 -0
  112. package/dist/errors-BDUwg2qC.js +27 -0
  113. package/dist/{errors-drnfxYtm.js → errors-BKy0EfhN.js} +5 -9
  114. package/dist/errors-BWXBMaKK.d.ts +11 -0
  115. package/dist/errors-CWTY6uih.js +143 -0
  116. package/dist/{errors-OD_4c__H.d.ts → errors-DH5QEGXh.d.ts} +2 -2
  117. package/dist/evaluate-CNClBvcj.js +1785 -0
  118. package/dist/evaluations-C_1vdSZc.d.ts +32 -0
  119. package/dist/{evaluations-C9YJsv3H.js → evaluations-beAooV0n.js} +145 -34
  120. package/dist/eve/index.d.ts +12 -16
  121. package/dist/eve/index.js +22 -28
  122. package/dist/event-time-CsegxXxB.js +23 -0
  123. package/dist/expiry-D4_YKRKD.js +52 -0
  124. package/dist/express/index.d.ts +13 -10
  125. package/dist/express/index.js +12 -11
  126. package/dist/fastify/index.d.ts +25 -22
  127. package/dist/fastify/index.js +13 -12
  128. package/dist/fetch-adapter-DD_gfmrC.js +120 -0
  129. package/dist/fetch-adapter-Dw0MTS3D.d.ts +31 -0
  130. package/dist/fields-OYG_JR78.js +203 -0
  131. package/dist/files-C5ZI27eV.js +124 -0
  132. package/dist/{fixtures-h2eG2OZV.js → fixtures-B7XrLeY7.js} +8 -7
  133. package/dist/flags-C3DktBDY.js +86 -0
  134. package/dist/{from-snapshot-7REQZe0H.js → from-snapshot-CTCMsFZk.js} +55 -52
  135. package/dist/{grantee-conditions-MQs5NIuX.js → grantee-conditions-CcXf8hMM.js} +4 -3
  136. package/dist/{grantee-CXY-0C87.js → grantee-hxrUsbpM.js} +16 -6
  137. package/dist/{grantee-match-fxYp9Nmt.js → grantee-match-DbENBHLB.js} +5 -4
  138. package/dist/h3/index.d.ts +37 -0
  139. package/dist/h3/index.js +35 -0
  140. package/dist/headers-BIjfMaoC.js +8 -0
  141. package/dist/headers-CIbDTUTK.d.ts +7 -0
  142. package/dist/{helpers-C9YmyBIa.js → helpers-CBYVMjwh.js} +45 -38
  143. package/dist/hono/index.d.ts +12 -9
  144. package/dist/hono/index.js +13 -12
  145. package/dist/hosted-B46-5LYE.js +315 -0
  146. package/dist/hosted-BPk51-kX.d.ts +78 -0
  147. package/dist/{http-DPuDaChb.d.ts → http-B_LphE5Y.d.ts} +1 -2
  148. package/dist/http-DXJ37RDA.js +46 -0
  149. package/dist/{http-BEbdMZ05.js → http-ujKm0JXE.js} +1 -2
  150. package/dist/{import-UHk1N7vH.js → import-CNV7I2Dw.js} +25 -20
  151. package/dist/{import-BFIlhetD.js → import-wyqXUyjm.js} +190 -53
  152. package/dist/index-B8YjkEZ4.d.ts +210 -0
  153. package/dist/index-C-6Letxg.d.ts +431 -0
  154. package/dist/index-CaxrMD1A.d.ts +1 -0
  155. package/dist/{index-BwMe94Xq.d.ts → index-Ch8LpNDp.d.ts} +34 -16
  156. package/dist/index-Cp_ilKi1.d.ts +91 -0
  157. package/dist/index-CueFnxQc.d.ts +110 -0
  158. package/dist/index-DuWxc5dt.d.ts +909 -0
  159. package/dist/index.d.ts +19 -15
  160. package/dist/index.js +31 -21
  161. package/dist/init-DeMYsnYJ.js +96 -0
  162. package/dist/init-GG6Jh8HD.js +95 -0
  163. package/dist/{instance-options-plkrsAog.d.ts → instance-options-BsVpyGIk.d.ts} +2 -2
  164. package/dist/instance-snapshot-BOEhcqIF.js +601 -0
  165. package/dist/instant-BluqDvyS.js +19 -0
  166. package/dist/internals-Dk8A8fX3.js +17 -0
  167. package/dist/{json-schema-DBkdUuCO.js → json-schema-B-vAwlde.js} +45 -6
  168. package/dist/json-schema-CN_iLFH2.d.ts +40 -0
  169. package/dist/jwt/index.d.ts +18 -11
  170. package/dist/jwt/index.js +156 -135
  171. package/dist/{kernel-CB3LrIJM.js → kernel-D0NVb1cK.js} +52 -24
  172. package/dist/kysely/index.d.ts +5 -3
  173. package/dist/kysely/index.js +2 -2
  174. package/dist/load-BxEtwlBS.js +1 -0
  175. package/dist/{load-C3_oTHyO.js → load-DFS_ry0W.js} +22 -6
  176. package/dist/{load-jose-BL6LH4bL.js → load-jose-DZ4dnjri.js} +1 -4
  177. package/dist/login-Dfe5RlYk.js +68 -0
  178. package/dist/login-imhuwvn3.js +254 -0
  179. package/dist/manifest-C8IzW3Do.js +42 -0
  180. package/dist/manifest-dvL2XEe1.js +370 -0
  181. package/dist/manifest-json-CAGzls-C.js +9 -0
  182. package/dist/{manifest-uDS9y5de.d.ts → manifest-mx5ghSFM.d.ts} +9 -2
  183. package/dist/manifest-schema-C0kK_8V0.js +399 -0
  184. package/dist/{map-claims-AVlBaJoa.js → map-claims-D3oRmTk0.js} +13 -10
  185. package/dist/{map-error-BE27PYkE.js → map-error-Dr70oA_3.js} +3 -3
  186. package/dist/{markers-CiwAOZtO.js → markers-C6joDI3o.js} +2 -1
  187. package/dist/mcp/index.d.ts +22 -12
  188. package/dist/mcp/index.js +57 -39
  189. package/dist/{migrate-BwlB5-Wk.js → migrate-CIq4-E9F.js} +53 -14
  190. package/dist/nest/index.d.ts +12 -9
  191. package/dist/nest/index.js +15 -14
  192. package/dist/next/index.d.ts +18 -21
  193. package/dist/next/index.js +13 -11
  194. package/dist/next/plugin.d.ts +1 -1
  195. package/dist/next/plugin.js +2 -2
  196. package/dist/node/index.d.ts +10 -10
  197. package/dist/node/index.js +7 -9
  198. package/dist/{ocsf-uYFXGVw2.js → ocsf-BFp-PZZA.js} +22 -11
  199. package/dist/openai/index.d.ts +9 -16
  200. package/dist/openai/index.js +17 -24
  201. package/dist/openapi/index.d.ts +3 -3
  202. package/dist/openapi/index.js +1 -1
  203. package/dist/{openapi-DBzHQbov.js → openapi-B7HZ0f-r.js} +8 -8
  204. package/dist/openapi-C4dDTVEd.js +138 -0
  205. package/dist/{openapi-BJdBmYDG.js → openapi-DD1SV-5-.js} +10 -9
  206. package/dist/operation-findings-DgATDpvh.js +36 -0
  207. package/dist/operations-X0VM7F3m.d.ts +59 -0
  208. package/dist/orpc/index.d.ts +13 -10
  209. package/dist/orpc/index.js +20 -15
  210. package/dist/otel/index.d.ts +3 -2
  211. package/dist/otel/index.js +30 -7
  212. package/dist/output-1im__-08.js +88 -0
  213. package/dist/output-files-B3IZJksC.js +1 -0
  214. package/dist/{output-files-CgIsvdSa.js → output-files-DhRBBE4n.js} +24 -26
  215. package/dist/{permission-keys-B26jeyUv.js → ownership-DqaJNJPn.js} +1045 -153
  216. package/dist/{package-root-DtNnpmUd.js → package-root-Dk_ETdkp.js} +1 -1
  217. package/dist/{parse-CVFQHk6c.js → parse-Cxq0WxrA.js} +2 -2
  218. package/dist/{parse-local-manifest-DvYUR0QL.d.ts → parse-local-manifest-5ODwqu8b.d.ts} +1 -1
  219. package/dist/{parse-local-manifest-C-q02Pal.js → parse-local-manifest-CVSs6J9I.js} +1 -1
  220. package/dist/parse-request-D_tUXG6M.js +26 -0
  221. package/dist/paths-DXSHAMIa.js +42 -0
  222. package/dist/pdp/index.d.ts +7 -3
  223. package/dist/pdp/index.js +45 -26
  224. package/dist/{peer-BOr_kJE1.js → peer-C8swJ2GH.js} +23 -4
  225. package/dist/peer-CRvC7Hg1.js +1 -0
  226. package/dist/permdock-BrAscV28.js +868 -0
  227. package/dist/permdock-SV8ZINYT.d.ts +502 -0
  228. package/dist/{permission-registry-BsblpB-z.js → permission-registry-Br4jSNVi.js} +3 -3
  229. package/dist/permissions-B-wCP5ZC.js +30 -0
  230. package/dist/{permissions-CCJ6lmxB.js → permissions-BPxlwVhN.js} +4 -3
  231. package/dist/{permissions-COH-KRgT.d.ts → permissions-u_KHg-sN.d.ts} +1 -2
  232. package/dist/policies-DP2nqRoF.js +295 -0
  233. package/dist/{policy-ur0BBK_e.d.ts → policy-77FSAq_V.d.ts} +2 -2
  234. package/dist/{hosted-CBHgkzDl.js → policy-DbM5YYtV.js} +17 -273
  235. package/dist/policy-from-catalog-C2gMa2Dj.js +450 -0
  236. package/dist/policy-module-CTlwMvLI.js +89 -0
  237. package/dist/{policy-types-sp9P1iIV.d.ts → policy-types-n0uRTsW8.d.ts} +460 -632
  238. package/dist/postgrest-CEgRbefE.d.ts +100 -0
  239. package/dist/postgrest-DAjUkF9w.js +575 -0
  240. package/dist/{powersync-cZe4kzhL.js → powersync-BkWWCS1K.js} +23 -24
  241. package/dist/powersync-DISt6SUe.js +62 -0
  242. package/dist/powersync-MMJ2mI2q.js +1 -0
  243. package/dist/preset/index.d.ts +146 -0
  244. package/dist/preset/index.js +1 -0
  245. package/dist/preset-DZdU0tne.js +1768 -0
  246. package/dist/principal-source-pHneukrx.js +103 -0
  247. package/dist/prisma/index.d.ts +6 -3
  248. package/dist/prisma/index.js +2 -2
  249. package/dist/{problem-CnWraVhC.js → problem-CTviyH5L.js} +13 -23
  250. package/dist/{problem-details-rIIo8B2C.d.ts → problem-details-B45V5d2b.d.ts} +1 -1
  251. package/dist/{problem-details-DECUnXAn.js → problem-details-cn5X30gZ.js} +43 -40
  252. package/dist/problem-types-CJ2325SZ.js +5 -0
  253. package/dist/{prompt-CXYhLfsS.js → prompt-DfZLJLIn.js} +2 -2
  254. package/dist/{protected-BN7BUdY1.js → protected-BlF0F6wL.js} +21 -4
  255. package/dist/{protected-B3_P7xuw.d.ts → protected-RFGpakN9.d.ts} +16 -8
  256. package/dist/{provider-Bi-0HNL3.js → provider-B1al-AZz.js} +2 -2
  257. package/dist/{provider-kwHC9CZD.d.ts → provider-DILL9oPK.d.ts} +1 -1
  258. package/dist/{provider-store-Ctt-81Ni.js → provider-store-DM6DNcta.js} +2 -2
  259. package/dist/pull-CvpINcMK.js +1 -0
  260. package/dist/pull-jEwbf80J.js +256 -0
  261. package/dist/push-BD4f15co.js +116 -0
  262. package/dist/pushed-policy-CqtxkV4G.js +238 -0
  263. package/dist/pushed-policy-DCW91KQU.d.ts +57 -0
  264. package/dist/react/index.d.ts +8 -8
  265. package/dist/react/index.js +6 -6
  266. package/dist/react/provider-client.d.ts +1 -1
  267. package/dist/react/provider-client.js +1 -2
  268. package/dist/react-native/index.d.ts +24 -72
  269. package/dist/react-native/index.js +29 -25
  270. package/dist/{refs-f3imMjNW.js → refs-K6eWUJ0H.js} +1 -1
  271. package/dist/{relation-walk-BXnlLtg2.js → relation-walk-3OyDBF0S.js} +2 -2
  272. package/dist/{relations-CvmSpG67.js → relations-BnCWnJGI.js} +3 -3
  273. package/dist/renames-CGj7YwcQ.js +175 -0
  274. package/dist/{replay-COpmZUfx.d.ts → replay-Bkm-Ne4l.d.ts} +1 -1
  275. package/dist/{replay-BsOyTrdV.js → replay-Du1tJjG7.js} +2 -1
  276. package/dist/{request-context-BVM7aWNs.js → request-context-C81Pip9n.js} +3 -3
  277. package/dist/request-schema-CMreVuFM.js +148 -0
  278. package/dist/resend/index.d.ts +126 -0
  279. package/dist/resend/index.js +379 -0
  280. package/dist/{resource-relations-D1Un3pcz.js → resource-relations-D_jw0BFx.js} +1 -1
  281. package/dist/{retire-BFcxL6un.js → retire-CALjIQ7e.js} +2 -2
  282. package/dist/{revocations-Cy06JNx2.d.ts → revocations-R07lFBnX.d.ts} +2 -0
  283. package/dist/rls-BAqP4q66.js +336 -0
  284. package/dist/{rls-BEktUaqg.js → rls-DTvYd-lI.js} +288 -690
  285. package/dist/{role-builder-B4GbKM-i.js → role-builder-uWQZ4v84.js} +14 -207
  286. package/dist/{rpc-nYC3cR_u.js → rpc-VSl5ylaQ.js} +7 -7
  287. package/dist/run-CUKuz0A8.js +723 -0
  288. package/dist/run-DDwUaJDU.js +1 -0
  289. package/dist/run-ticket-DocwaTKn.d.ts +124 -0
  290. package/dist/run-ticket-sxFI6LAk.js +337 -0
  291. package/dist/{runtime-D_tMXnsq.js → runtime-D3VTAiCA.js} +1 -1
  292. package/dist/{saas-DtaA4JuW.js → saas-BCXRuXca.js} +16 -14
  293. package/dist/scan-qvOe0hS3.js +29 -0
  294. package/dist/{schema-DjQBcAP9.js → schema-B4v0Qrac.js} +102 -8
  295. package/dist/schema-yLuBWWsh.js +130 -0
  296. package/dist/scim/index.d.ts +2 -2
  297. package/dist/scim/index.js +1 -2
  298. package/dist/{scim-vFR1O5oG.js → scim-ClHtpyV3.js} +208 -76
  299. package/dist/{scopes-tvpa34ep.js → scopes-CUPSjo9q.js} +12 -7
  300. package/dist/server/index.d.ts +12 -143
  301. package/dist/server/index.js +12 -9
  302. package/dist/server-C3WXAHZj.js +453 -0
  303. package/dist/{server-types-DdqBwZN_.d.ts → server-types-CKQiCnON.d.ts} +51 -68
  304. package/dist/{sha256-BO0swRXL.js → sha256-D9ihqmvd.js} +8 -1
  305. package/dist/{shared-1uhI-e-b.js → shared-BAK8t4Uo.js} +20 -9
  306. package/dist/{shared-JhMeOsDF.js → shared-DvvFzYXe.js} +8 -9
  307. package/dist/signals-D3NcTKUS.js +32 -0
  308. package/dist/sink-4-eQIRWf.d.ts +225 -0
  309. package/dist/sink-BEZSlMS_.js +957 -0
  310. package/dist/{skills-BZdRQkxs.js → skills-B6lVdD_W.js} +13 -6
  311. package/dist/skills-CTYwkl44.js +51 -0
  312. package/dist/snapshot-B7DkYg6g.js +269 -0
  313. package/dist/{snapshot-cache-Cod40D-X.js → snapshot-cache-DZHQ0KF6.js} +11 -5
  314. package/dist/{snapshot-cache-J5T7t_vD.d.ts → snapshot-cache-PXI-vGpY.d.ts} +7 -5
  315. package/dist/solid/index.d.ts +11 -12
  316. package/dist/solid/index.js +7 -7
  317. package/dist/solid-start/index.d.ts +52 -0
  318. package/dist/solid-start/index.js +49 -0
  319. package/dist/{sources-Dljy06zL.d.ts → sources-Xc4zkszI.d.ts} +7 -13
  320. package/dist/{sources-B1-b-R0t.js → sources-vs5dGOsS.js} +125 -119
  321. package/dist/{sql-t8teWOfo.js → sql-B5T1IL83.js} +13 -1
  322. package/dist/src-Cu2m75uo.js +1860 -0
  323. package/dist/ssf/index.d.ts +4 -4
  324. package/dist/ssf/index.js +384 -47
  325. package/dist/standard-webhooks-DgFFD-Ga.js +228 -0
  326. package/dist/standard-webhooks-rbD7LS6h.d.ts +88 -0
  327. package/dist/storage-C2bWdvJH.js +132 -0
  328. package/dist/{store-ILkLUj_o.js → store-BDutJkDL.js} +40 -53
  329. package/dist/{store-options-CbUHDoSe.js → store-options-u0ycEe8s.js} +26 -21
  330. package/dist/{stores-Cxz_80g-.d.ts → stores-BUGNbJTt.d.ts} +7 -8
  331. package/dist/{stores-BHXdSbw9.js → stores-CGhM8BN_.js} +2 -2
  332. package/dist/{stream-Dwsg6WwE.d.ts → stream-CeCMJmBB.d.ts} +2 -2
  333. package/dist/{stream-BMui98KK.js → stream-CvdUS9V1.js} +9 -2
  334. package/dist/style-DiKWnAvn.js +1 -0
  335. package/dist/{style-CTm1AXDT.js → style-Uao76wO7.js} +3 -2
  336. package/dist/subject-BGoM5o8B.js +17 -0
  337. package/dist/supabase/index.d.ts +23 -106
  338. package/dist/supabase/index.js +5 -4
  339. package/dist/supabase/middleware.d.ts +7 -6
  340. package/dist/supabase/middleware.js +13 -11
  341. package/dist/supabase-By_frk3U.js +92 -0
  342. package/dist/{supabase-hook-Cva3ikXz.js → supabase-hook-BMoEhZir.js} +960 -64
  343. package/dist/supabase-hook-CCw5m35C.js +1 -0
  344. package/dist/{supabase-C1TWlnYt.js → supabase-x5JtR2tc.js} +99 -206
  345. package/dist/support-session-C6xJQYut.js +158 -0
  346. package/dist/svelte/Protected.svelte +4 -4
  347. package/dist/svelte/index.d.ts +7 -7
  348. package/dist/svelte/index.js +10 -10
  349. package/dist/svelte/runtime.d.ts +3 -3
  350. package/dist/svelte/runtime.js +3 -3
  351. package/dist/svelte/source.d.ts +7 -7
  352. package/dist/svelte/source.js +4 -4
  353. package/dist/sveltekit/index.d.ts +51 -0
  354. package/dist/sveltekit/index.js +27 -0
  355. package/dist/tanstack-start/index.d.ts +51 -0
  356. package/dist/tanstack-start/index.js +40 -0
  357. package/dist/target-Bw37TFNA.js +51 -0
  358. package/dist/{tenancy-DshjqY0n.js → tenancy-DN2QAx9z.js} +5 -5
  359. package/dist/terminal/index.d.ts +11 -9
  360. package/dist/terminal/index.js +23 -287
  361. package/dist/testing/index.d.ts +311 -38
  362. package/dist/testing/index.js +3475 -201
  363. package/dist/testing/saas/index.d.ts +9 -8
  364. package/dist/testing/saas/index.js +2 -2
  365. package/dist/testing/saas/permissions.d.ts +1 -1
  366. package/dist/testing/saas/permissions.js +1 -2
  367. package/dist/text-BMFRfBqc.js +8 -0
  368. package/dist/{text-diff-BdkkYSbi.js → text-diff-BQcUapOb.js} +1 -1
  369. package/dist/thenable-yD7SabqG.js +31 -0
  370. package/dist/time-DkWjdL98.js +71 -0
  371. package/dist/{token-Bx9qy30p.js → token-BLZcjQgt.js} +15 -6
  372. package/dist/{tool-refusal-CQ7OEia9.js → tool-refusal-CMLN5xhn.js} +2 -2
  373. package/dist/trpc/index.d.ts +12 -9
  374. package/dist/trpc/index.js +16 -14
  375. package/dist/types-8RnwUAvE.d.ts +107 -0
  376. package/dist/{store-9KO4BlkB.d.ts → types-B3_ooZgZ.d.ts} +100 -75
  377. package/dist/{types-Br9IXUBG.d.ts → types-BdtSDbBO.d.ts} +77 -17
  378. package/dist/types-BtHtcKMr.d.ts +157 -0
  379. package/dist/{types-CqrjB9o6.d.ts → types-BzzaHxt3.d.ts} +23 -11
  380. package/dist/types-C791HFu9.d.ts +332 -0
  381. package/dist/types-C863OQiR.d.ts +57 -0
  382. package/dist/{types-ByUo56un.d.ts → types-CVZSNocA.d.ts} +23 -22
  383. package/dist/{types-DHrmBJLH.d.ts → types-CX7AvRqP.d.ts} +7 -7
  384. package/dist/types-C_IiEs64.d.ts +65 -0
  385. package/dist/{types-7rJVLWe5.d.ts → types-Dfv0aTER.d.ts} +63 -11
  386. package/dist/types-DqzsnlSA.js +67 -0
  387. package/dist/{types-DpWfQdSh.d.ts → types-Druu9wLM.d.ts} +46 -11
  388. package/dist/{types-CIlTojhq.d.ts → types-_WhUP7ud.d.ts} +9 -6
  389. package/dist/{types-CUcFZfpU.d.ts → types-lUlG3TVd.d.ts} +2 -1
  390. package/dist/unplugin/index.d.ts +1 -1
  391. package/dist/unplugin/index.js +2 -2
  392. package/dist/usage-CRVl9V3t.js +32 -0
  393. package/dist/usage-CZpSQP6L.js +1 -0
  394. package/dist/{usage-CG9nOYZw.js → usage-SveTRTyr.js} +54 -11
  395. package/dist/{validation-error-BSSw0l_8.d.ts → validation-error-DHUrAkku.d.ts} +1 -1
  396. package/dist/{validity-dVrUEPJt.js → validity-CZ7-y0Cj.js} +32 -15
  397. package/dist/{verifier-DMr_Zmb_.js → verifier-DHuh-okk.js} +18 -7
  398. package/dist/{verify-BztUB3GX.js → verify-BaAKx7t6.js} +645 -50
  399. package/dist/{version-BEogC-rB.js → version-CEbQ5UeM.js} +4 -3
  400. package/dist/version-DXkYUISx.js +1 -0
  401. package/dist/{views-CJsrBS1f.js → views-DTIIO-oQ.js} +22 -5
  402. package/dist/{permission-types-Dg39z9Kv.d.ts → vocabulary-CN8H_9UF.d.ts} +67 -2
  403. package/dist/vue/index.d.ts +9 -10
  404. package/dist/vue/index.js +8 -8
  405. package/dist/waiter-B986w1oK.d.ts +36 -0
  406. package/dist/waiter-BGUtGYoU.js +67 -0
  407. package/dist/watch--ovlPOc0.js +1 -0
  408. package/dist/{watch-zeucB7eF.js → watch-vea54PRw.js} +44 -5
  409. package/dist/webmcp/index.d.ts +2 -2
  410. package/dist/webmcp/index.js +3 -3
  411. package/dist/where-scope-C9vfEphl.js +306 -0
  412. package/dist/who-can-DWBAcaCQ.js +364 -0
  413. package/dist/wire-delegation-Cv3W2eaM.js +36 -0
  414. package/dist/wire-denial-C7PaVYEM.js +27 -0
  415. package/dist/workflow/index.d.ts +5 -0
  416. package/dist/workflow/index.js +223 -0
  417. package/dist/workflow-sdk/index.d.ts +56 -0
  418. package/dist/workflow-sdk/index.js +78 -0
  419. package/dist/{wrap-Cr93qqdP.d.ts → wrap-C5jC6sKI.d.ts} +2 -1
  420. package/dist/{wrap-DWsU_5Bd.js → wrap-gX61EtpB.js} +7 -6
  421. package/package.json +136 -37
  422. package/schemas/access-event-v1.json +118 -0
  423. package/schemas/approval-request-v1.json +34 -1
  424. package/schemas/approval-signature-v1.json +94 -0
  425. package/schemas/automation-event-v1.json +113 -0
  426. package/schemas/capability-v1.json +84 -0
  427. package/schemas/catalog-event-v1.json +87 -0
  428. package/schemas/catalog-v1.json +123 -12
  429. package/schemas/cli-commands-v1.json +164 -0
  430. package/schemas/cli-result-v1.json +96 -0
  431. package/schemas/cloud-api-v1.json +5407 -0
  432. package/schemas/connector-v1.json +408 -0
  433. package/schemas/credential-event-v1.json +81 -0
  434. package/schemas/credential-v1.json +134 -0
  435. package/schemas/decision-event-v1.json +288 -0
  436. package/schemas/delivery-event-v1.json +117 -0
  437. package/schemas/directory-event-v1.json +85 -0
  438. package/schemas/evidence-export-v1.json +54 -0
  439. package/schemas/membership-event-v1.json +93 -0
  440. package/schemas/policy-v1.json +68 -0
  441. package/schemas/pushed-policy-v1.json +236 -0
  442. package/schemas/run-v1.json +162 -0
  443. package/schemas/snapshot-v1.json +138 -0
  444. package/schemas/supabase-claims-v1.json +4 -4
  445. package/schemas/supabase-manifest-v1.json +5 -1
  446. package/schemas/support-session-v1.json +111 -0
  447. package/skills/permdock/SKILL.md +16 -12
  448. package/skills/permdock-agents/SKILL.md +40 -23
  449. package/skills/permdock-agents/references/operations-automations-chat.md +221 -0
  450. package/skills/permdock-agents/references/runtimes.md +22 -18
  451. package/skills/permdock-approvals/SKILL.md +29 -13
  452. package/skills/permdock-approvals/references/delivery-and-waiters.md +172 -0
  453. package/skills/permdock-approvals/references/stores-and-resume.md +21 -7
  454. package/skills/permdock-audit/SKILL.md +3 -3
  455. package/skills/permdock-credentials/SKILL.md +5 -5
  456. package/skills/permdock-credentials/references/tokens-keys-links.md +14 -10
  457. package/skills/permdock-data/SKILL.md +6 -6
  458. package/skills/permdock-data/references/queries-and-rls.md +10 -9
  459. package/skills/permdock-data/references/relationships.md +3 -3
  460. package/skills/permdock-tenancy/SKILL.md +20 -7
  461. package/skills/permdock-tenancy/references/contacts-and-support-sessions.md +122 -0
  462. package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +6 -4
  463. package/skills/permdock-wire/SKILL.md +17 -11
  464. package/skills/permdock-wire/references/adapters.md +139 -51
  465. package/skills/permdock-wire/references/policy-options.md +5 -5
  466. package/skills/permdock-wire/references/presets-and-recipes.md +58 -0
  467. package/dist/approvals-DZL76lKW.js +0 -240
  468. package/dist/arazzo-CD8d0WLb.js +0 -54
  469. package/dist/catalog-6yWAX6Ps.js +0 -2
  470. package/dist/catalog-WHG4kBq7.js +0 -48
  471. package/dist/cloud-C7bSDgX4.js +0 -50
  472. package/dist/cloud-PeBN3IEn.js +0 -118
  473. package/dist/collect-Bw2Jdq1q.js +0 -2
  474. package/dist/collect-CQx4hX4N.js +0 -83
  475. package/dist/commands-Cpip4ULx.js +0 -97
  476. package/dist/config-DLt6BvV7.js +0 -36
  477. package/dist/diff-Cnghh_Ip.js +0 -48
  478. package/dist/doctor-CuVJvhV_.js +0 -53
  479. package/dist/duration-xK2jGFVc.js +0 -25
  480. package/dist/errors-CWfoNg9N.js +0 -47
  481. package/dist/errors-DRnGZHzF.js +0 -30
  482. package/dist/expiry-BoBJ8Tvc.js +0 -7
  483. package/dist/fields-fwfXN01n.js +0 -1016
  484. package/dist/headers-BZSDieNT.js +0 -6
  485. package/dist/headers-Cf-zWvsO.d.ts +0 -6
  486. package/dist/ids-DC4V4CFu.js +0 -18
  487. package/dist/index-DgM7w96K.d.ts +0 -6
  488. package/dist/index-Dz7didpC.d.ts +0 -201
  489. package/dist/index-UdVQgq48.d.ts +0 -550
  490. package/dist/load-B_shHrmd.js +0 -2
  491. package/dist/manifest-BngWKryL.js +0 -770
  492. package/dist/may-use-BOn_dR_w.js +0 -78
  493. package/dist/openapi-DcQ1Kg40.js +0 -164
  494. package/dist/ownership-BgnbOJFT.js +0 -851
  495. package/dist/permdock-L26udHSk.js +0 -3646
  496. package/dist/powersync-BFSbSElQ.js +0 -62
  497. package/dist/powersync-DCDgZgfZ.js +0 -2
  498. package/dist/rls-55DQTuf5.js +0 -250
  499. package/dist/run-DMcNW_8T.js +0 -174
  500. package/dist/server-ZYEHtNHS.js +0 -268
  501. package/dist/sink-BTtmfPnE.js +0 -141
  502. package/dist/sink-CPN4zLfs.d.ts +0 -104
  503. package/dist/skills-Dle4ZoLF.js +0 -56
  504. package/dist/snapshot-Dc9CHPME.js +0 -1078
  505. package/dist/src-Bb7no-hS.js +0 -639
  506. package/dist/subject-C6dLxqHU.js +0 -24
  507. package/dist/supabase-Dbqr141s.js +0 -83
  508. package/dist/supabase-hook-BULg7Ut_.js +0 -2
  509. package/dist/thenable-QSmvt-vZ.js +0 -65
  510. package/dist/types-C0dee0Qf.d.ts +0 -197
  511. package/dist/types-DvupZE0w.d.ts +0 -9
  512. package/dist/usage-4G6t643F.js +0 -42
  513. package/dist/usage-iFLJ0-rr.js +0 -2
  514. package/dist/vocabulary-hhEFLFoL.d.ts +0 -65
  515. package/dist/watch-C8TpsAl4.js +0 -2
@@ -1,24 +1,44 @@
1
- import { r as principal } from "../refs-f3imMjNW.js";
2
- import { n as role, r as allow, u as memoryLimitStore } from "../role-builder-B4GbKM-i.js";
3
- import { c as snapshotFor, g as memoryRoleSource, s as definePermissions } from "../src-Bb7no-hS.js";
1
+ import { t as require__2020 } from "../2020-C9VN-x8c.js";
2
+ import { r as isRecord } from "../compact-CAf557ZB.js";
3
+ import { n as freezeDeep } from "../freeze-CrU8nQeX.js";
4
+ import { r as principal } from "../refs-K6eWUJ0H.js";
5
+ import { n as role, r as allow } from "../role-builder-uWQZ4v84.js";
6
+ import { E as memoryCustomRoleSource, _ as limits, s as snapshotFor, v as memoryLimitStore, x as approvalPoliciesFor } from "../src-Cu2m75uo.js";
4
7
  import { t as memoryRevocationFeed } from "../revocations-Bhr0aMIC.js";
5
- import { a as normalizeMemberships, d as scopeList } from "../scopes-tvpa34ep.js";
6
- import { _ as evaluateCondition, n as parseSnapshot } from "../snapshot-Dc9CHPME.js";
7
- import { _ as resource, c as listPermissions } from "../permission-registry-BsblpB-z.js";
8
- import { i as parsePolicyDocument, o as definePolicy, r as mergeHostedGrants } from "../hosted-CBHgkzDl.js";
9
- import { E as approvalPoliciesFor, b as validateCustomRole, t as createPermDock, v as membershipsClaim } from "../permdock-L26udHSk.js";
8
+ import { n as loadPrincipalByIdentity, t as loadPrincipal } from "../principal-source-pHneukrx.js";
9
+ import { _ as resource, c as listPermissions } from "../permission-registry-Br4jSNVi.js";
10
+ import { d as isoNow, f as nowMillis, p as nowSeconds, u as isoAt } from "../time-DkWjdL98.js";
11
+ import { r as parseAutomation } from "../automation-DVjOckjV.js";
12
+ import { a as normalizeMemberships, d as scopeList } from "../scopes-CUPSjo9q.js";
13
+ import { u as evaluateCondition } from "../delegation-j6-rc37o.js";
14
+ import { a as authenticated } from "../grantee-hxrUsbpM.js";
15
+ import { n as definePolicy } from "../policy-DbM5YYtV.js";
10
16
  import { t as byCodePoint } from "../compare-pfyVnBjE.js";
11
- import { n as fromSnapshot } from "../from-snapshot-7REQZe0H.js";
12
- import { o as parseCredential } from "../credential-kue0qpbm.js";
13
- import { s as problemFromDecision } from "../problem-CnWraVhC.js";
14
- import { l as APPROVAL_HEADER, r as memoryApprovalStore } from "../store-ILkLUj_o.js";
15
- import { o as resolveApproval } from "../helpers-C9YmyBIa.js";
16
- import { a as parseApiKey } from "../server-ZYEHtNHS.js";
17
- import { S as sha256Hex, n as directoryMembershipSource, r as scimHandler } from "../scim-vFR1O5oG.js";
17
+ import { a as mergeHostedGrants, o as parsePolicyDocument } from "../hosted-B46-5LYE.js";
18
+ import { t as definePermissions } from "../permissions-B-wCP5ZC.js";
19
+ import { C as validateCustomRole, y as membershipsClaim } from "../evaluate-CNClBvcj.js";
20
+ import { n as parseSnapshot } from "../snapshot-B7DkYg6g.js";
21
+ import { n as fromSnapshot } from "../from-snapshot-CTCMsFZk.js";
22
+ import { t as getPermDock } from "../permdock-BrAscV28.js";
23
+ import { t as APPROVAL_HEADER } from "../types-DqzsnlSA.js";
24
+ import { i as memoryApprovalStore } from "../store-BDutJkDL.js";
25
+ import { t as catalogFingerprint } from "../catalog-fingerprint-CCP0-yQS.js";
26
+ import { s as parseCredential } from "../credential-BcoiejpY.js";
27
+ import { s as problemFromDecision } from "../problem-CTviyH5L.js";
28
+ import { o as resolveApproval } from "../helpers-CBYVMjwh.js";
29
+ import { t as APPROVAL_REQUEST_SCHEMA } from "../request-schema-CMreVuFM.js";
30
+ import { t as catalogSchema } from "../schema-B4v0Qrac.js";
31
+ import { c as parseApiKey } from "../server-C3WXAHZj.js";
32
+ import { n as discoverAuthorizationServer } from "../device-BARQ8eYG.js";
33
+ import { T as sha256Hex, n as directoryMembershipSource, r as scimHandler } from "../scim-ClHtpyV3.js";
34
+ import { a as approverHeaders, r as cloudEndpoints, t as cloud } from "../create-CQWIpUG-.js";
18
35
  import { t as supabaseMembershipsBudget } from "../budget-NbEtbJC_.js";
19
- import { a as quoteSqlIdent, s as quoteSqlTable, t as SQL_IDENT } from "../sql-t8teWOfo.js";
20
- import { i as saasPermissions } from "../permissions-CCJ6lmxB.js";
21
- import { C as saasOrg, E as saasSeed, S as saasMemberships, a as saasProject, g as verifySaasSession, h as signSaasToken, k as saasPolicy, t as saasDoc, w as saasPrincipal, x as saasCustomRoles } from "../saas-DtaA4JuW.js";
36
+ import { c as quoteSqlTable, n as defaultSqlCommand, o as quoteSqlIdent, t as SQL_IDENT } from "../sql-B5T1IL83.js";
37
+ import { t as supabaseManifestSchema } from "../manifest-schema-C0kK_8V0.js";
38
+ import { t as PUSHED_POLICY_SCHEMA } from "../pushed-policy-CqtxkV4G.js";
39
+ import { i as saasPermissions } from "../permissions-BPxlwVhN.js";
40
+ import { C as saasOrg, E as saasSeed, S as saasMemberships, a as saasProject, g as verifySaasSession, h as signSaasToken, k as saasPolicy, t as saasDoc, w as saasPrincipal, x as saasCustomRoles } from "../saas-BCXRuXca.js";
41
+ import { t as POLICY_NOT_PUBLISHED } from "../problem-types-CJ2325SZ.js";
22
42
  import { afterAll, beforeAll, describe, expect, expectTypeOf, it, vi } from "vitest";
23
43
  //#region src/testing/describe-policy.ts
24
44
  function isOutcomeCell(value) {
@@ -51,7 +71,7 @@ function describePolicy(policy, config) {
51
71
  const permissions = listPermissions(policy.permissions);
52
72
  const permdocks = /* @__PURE__ */ new Map();
53
73
  beforeAll(async () => {
54
- for (const [name, user] of Object.entries(config.subjects)) permdocks.set(name, await createPermDock(policy, user, config.options));
74
+ for (const [name, user] of Object.entries(config.subjects)) permdocks.set(name, await getPermDock(policy, user, config.options));
55
75
  });
56
76
  it("covers every permission", () => {
57
77
  if (config.exhaustive === false) return;
@@ -63,7 +83,7 @@ function describePolicy(policy, config) {
63
83
  describe(permission.key, () => {
64
84
  if (!Object.values(spec).some((value) => !isOutcomeCell(value))) {
65
85
  for (const [subjectName, cell] of Object.entries(spec)) it(`${subjectName}`, async () => {
66
- const instance = permdocks.get(subjectName) ?? await createPermDock(policy, config.subjects[subjectName], config.options);
86
+ const instance = permdocks.get(subjectName) ?? await getPermDock(policy, config.subjects[subjectName], config.options);
67
87
  const data = permission.kind === "instance" ? Object.values(config.fixtures ?? {})[0] : void 0;
68
88
  assertCell(instance.decide(permission, data), cell);
69
89
  assertDeniedBy(instance, permission, data, cell);
@@ -74,7 +94,7 @@ function describePolicy(policy, config) {
74
94
  for (const [fixtureName, row] of Object.entries(spec)) {
75
95
  if (isOutcomeCell(row)) continue;
76
96
  for (const [subjectName, cell] of Object.entries(row)) it(`${fixtureName} / ${subjectName}`, async () => {
77
- const instance = permdocks.get(subjectName) ?? await createPermDock(policy, config.subjects[subjectName], config.options);
97
+ const instance = permdocks.get(subjectName) ?? await getPermDock(policy, config.subjects[subjectName], config.options);
78
98
  const fixture = config.fixtures?.[fixtureName];
79
99
  assertCell(instance.decide(permission, fixture), cell);
80
100
  assertDeniedBy(instance, permission, fixture, cell);
@@ -145,14 +165,13 @@ function statementSql(action, table, keyOnly) {
145
165
  const quoted = quoteSqlTable(table);
146
166
  const id = quoteSqlIdent("id");
147
167
  const back = keyOnly ? id : "*";
148
- switch (action) {
149
- case "read":
150
- case "list":
151
- case "get": return `select ${back} from ${quoted} where ${id} = $1`;
168
+ const command = defaultSqlCommand(action) ?? "select";
169
+ switch (command) {
170
+ case "select": return `select ${back} from ${quoted} where ${id} = $1`;
152
171
  case "update": return `update ${quoted} set ${id} = ${id} where ${id} = $1 returning ${back}`;
153
- case "create": return `insert into ${quoted} (${id}) values ($1) returning ${back}`;
172
+ case "insert": return `insert into ${quoted} (${id}) values ($1) returning ${back}`;
154
173
  case "delete": return `delete from ${quoted} where ${id} = $1 returning ${back}`;
155
- default: return `select ${back} from ${quoted} where ${id} = $1`;
174
+ default: return command;
156
175
  }
157
176
  }
158
177
  function valued(row) {
@@ -177,7 +196,7 @@ function dbOutcome(result) {
177
196
  if (result.code === "42501") return "rejected";
178
197
  return (result.rowCount ?? result.rows.length) > 0 ? "allowed" : "filtered";
179
198
  }
180
- async function rlsParity(policy, options) {
199
+ async function testRlsParity(policy, options) {
181
200
  const dialect = options.dialect ?? "guc";
182
201
  const gucPrefix = options.gucPrefix ?? "app";
183
202
  const tenantClaim = options.tenantClaim ?? "tenant_id";
@@ -186,12 +205,12 @@ async function rlsParity(policy, options) {
186
205
  const customRoles = options.customRoles ?? [];
187
206
  const scopes = scopeList(policy.scopes);
188
207
  async function runCase(fixture) {
189
- const permdock = await createPermDock(policy, toSubject(fixture.subject), { customRoles: memoryRoleSource(customRoles) });
208
+ const permdock = await getPermDock(policy, toSubject(fixture.subject), { customRoles: memoryCustomRoleSource(customRoles) });
190
209
  const granted = fixture.permission.kind === "collection" ? permdock.can(fixture.permission, fixture.row) : permdock.can(fixture.permission, fixture.row);
191
210
  let fromClient;
192
211
  if (options.snapshot === true) {
193
212
  const snapshot = permdock.snapshot();
194
- if (typeof snapshot !== "object" || snapshot instanceof Promise) throw new TypeError("PermDock: rlsParity needs an unsigned snapshot");
213
+ if (typeof snapshot !== "object" || snapshot instanceof Promise) throw new TypeError("PermDock: testRlsParity needs an unsigned snapshot");
195
214
  fromClient = fromSnapshot(parseSnapshot(JSON.stringify(snapshot))).can(fixture.permission, fixture.row);
196
215
  }
197
216
  await options.query("begin");
@@ -251,11 +270,11 @@ function subsetOf(small, large) {
251
270
  * A partial `where()` (a closure grant it cannot compile) may be refused or
252
271
  * return fewer rows, never more.
253
272
  */
254
- async function ormParity(policy, scenarios, options) {
273
+ async function testOrmParity(policy, scenarios, options) {
255
274
  const idField = options.id ?? "id";
256
275
  async function runCase(scenario) {
257
- const permdock = await createPermDock(policy, scenario.user, scenario.options);
258
- if (scenario.options?.relations !== void 0) await permdock.loadRelations(scenario.permission, scenario.rows);
276
+ const permdock = await getPermDock(policy, scenario.user, scenario.options);
277
+ if (scenario.options?.relations !== void 0) await permdock.prefetchRelations(scenario.permission, scenario.rows);
259
278
  const expected = sortedIds(permdock.filter(scenario.permission, scenario.rows).map((row) => row[idField]));
260
279
  const where = permdock.where(scenario.permission);
261
280
  try {
@@ -292,7 +311,7 @@ async function ormParity(policy, scenarios, options) {
292
311
  //#endregion
293
312
  //#region src/testing/client-parity.ts
294
313
  async function outcomes(policy, entry, options) {
295
- const server = await createPermDock(policy, entry.user, {
314
+ const server = await getPermDock(policy, entry.user, {
296
315
  ...entry.tenant === void 0 ? {} : { tenant: entry.tenant },
297
316
  ...options.customRoles === void 0 ? {} : { customRoles: options.customRoles }
298
317
  });
@@ -317,6 +336,31 @@ function testClientParity(policy, cases, options = {}) {
317
336
  });
318
337
  }
319
338
  //#endregion
339
+ //#region src/testing/legacy-parity.ts
340
+ /**
341
+ * Asserts that the policy answers each case as the application's legacy
342
+ * check does, or as the case's `expect` says where the difference is
343
+ * intended. Run it before enforcing a permission with `shadowMode`.
344
+ */
345
+ function testLegacyParity(policy, legacy, cases) {
346
+ for (const entry of cases) it(`legacy parity: ${entry.name}`, async () => {
347
+ const permdock = await getPermDock(policy, entry.user);
348
+ const ours = permdock.can(entry.permission, entry.row);
349
+ const theirs = legacy({
350
+ permission: entry.permission,
351
+ data: entry.row,
352
+ subject: permdock.subject
353
+ });
354
+ expect({
355
+ permission: entry.permission.key,
356
+ granted: ours
357
+ }).toEqual({
358
+ permission: entry.permission.key,
359
+ granted: entry.expect ?? theirs
360
+ });
361
+ });
362
+ }
363
+ //#endregion
320
364
  //#region src/testing/client-store.ts
321
365
  const p = saasPermissions;
322
366
  function snapshotOf(user, tenant, tenants) {
@@ -344,7 +388,7 @@ function leadSnapshot() {
344
388
  ]
345
389
  });
346
390
  }
347
- function json(value, status = 200) {
391
+ function json$1(value, status = 200) {
348
392
  return new Response(JSON.stringify(value), {
349
393
  status,
350
394
  headers: { "content-type": "application/json" }
@@ -391,7 +435,7 @@ function stubVerifier(tokens) {
391
435
  claims: { snapshot },
392
436
  header: {
393
437
  alg: "ES256",
394
- typ: "permdock-snapshot+jwt"
438
+ typ: "vnd.permdock.snapshot+jwt"
395
439
  }
396
440
  });
397
441
  } };
@@ -442,7 +486,7 @@ function testClientStore(name, createStore) {
442
486
  });
443
487
  it("fetches another tenant with one query string and commits it only on success", async () => {
444
488
  let fail = true;
445
- const net = recorder(() => fail ? json({ error: "down" }, 503) : json(snapshotOf("erin", "globex")));
489
+ const net = recorder(() => fail ? json$1({ error: "down" }, 503) : json$1(snapshotOf("erin", "globex")));
446
490
  const store = createStore({
447
491
  snapshot: snapshotOf("erin", "acme"),
448
492
  snapshotUrl: "/api/snapshot?v=1",
@@ -471,7 +515,7 @@ function testClientStore(name, createStore) {
471
515
  });
472
516
  const pending = store.get().refresh();
473
517
  expect(store.get().status()).toBe("pending");
474
- gate.resolve(json(snapshotOf("erin", "acme")));
518
+ gate.resolve(json$1(snapshotOf("erin", "acme")));
475
519
  await pending;
476
520
  expect(store.get().status()).toBe("ready");
477
521
  });
@@ -486,7 +530,7 @@ function testClientStore(name, createStore) {
486
530
  });
487
531
  const pending = store.get().refresh();
488
532
  store.get().clear();
489
- gate.resolve(json(snapshotOf("erin", "acme")));
533
+ gate.resolve(json$1(snapshotOf("erin", "acme")));
490
534
  await pending;
491
535
  await settle();
492
536
  expect(store.get().subject.principal).toBeNull();
@@ -504,7 +548,7 @@ function testClientStore(name, createStore) {
504
548
  });
505
549
  const pending = store.get().refresh({ tenant: "globex" });
506
550
  store.replace(snapshotOf("bob", "acme"));
507
- gate.resolve(json(snapshotOf("erin", "globex")));
551
+ gate.resolve(json$1(snapshotOf("erin", "globex")));
508
552
  await pending;
509
553
  await settle();
510
554
  expect(store.get().subject.principal?.id).toBe("bob");
@@ -516,7 +560,7 @@ function testClientStore(name, createStore) {
516
560
  const store = createStore({
517
561
  snapshot: {
518
562
  ...snapshotOf("erin", "acme"),
519
- expiresAt: 1
563
+ expiresAt: "1970-01-01T00:00:01.000Z"
520
564
  },
521
565
  tenant: "acme",
522
566
  fetch: net.fetch
@@ -557,7 +601,7 @@ function testClientStore(name, createStore) {
557
601
  snapshot: seed,
558
602
  snapshotUrl: "/api/snapshot",
559
603
  tenant: "acme",
560
- fetch: recorder(() => json(next)).fetch,
604
+ fetch: recorder(() => json$1(next)).fetch,
561
605
  verifier: stubVerifier(/* @__PURE__ */ new Map([[seed, snapshotOf("erin", "acme")], [next, snapshotOf("erin", "globex")]]))
562
606
  });
563
607
  await settle();
@@ -569,7 +613,7 @@ function testClientStore(name, createStore) {
569
613
  it("caches endpoint answers per tenant and settles the store status", async () => {
570
614
  const net = recorder((call) => {
571
615
  const body = call.body;
572
- return json({ evaluations: body.evaluations.map(() => ({
616
+ return json$1({ evaluations: body.evaluations.map(() => ({
573
617
  decision: false,
574
618
  context: { permdock: denied() }
575
619
  })) });
@@ -600,7 +644,7 @@ function testClientStore(name, createStore) {
600
644
  it("forgets endpoint answers when a new snapshot arrives", async () => {
601
645
  const net = recorder((call) => {
602
646
  const body = call.body;
603
- return json({ evaluations: body.evaluations.map(() => ({
647
+ return json$1({ evaluations: body.evaluations.map(() => ({
604
648
  decision: false,
605
649
  context: { permdock: denied() }
606
650
  })) });
@@ -677,7 +721,7 @@ function testClientStore(name, createStore) {
677
721
  }
678
722
  });
679
723
  it("does not notify subscribers synchronously from a status read", () => {
680
- const net = recorder(() => json({ evaluations: [] }));
724
+ const net = recorder(() => json$1({ evaluations: [] }));
681
725
  const store = createStore({
682
726
  snapshot: snapshotOf("gina", "acme"),
683
727
  endpoint: "/api/permdock",
@@ -696,7 +740,7 @@ function testClientStore(name, createStore) {
696
740
  //#endregion
697
741
  //#region src/testing/snapshot-fixture.ts
698
742
  async function snapshotFixture(policy, subject, options = {}) {
699
- const instance = await createPermDock(policy, subject, options.tenant === void 0 ? {} : { tenant: options.tenant });
743
+ const instance = await getPermDock(policy, subject, options.tenant === void 0 ? {} : { tenant: options.tenant });
700
744
  const target = options.simulated === true ? instance.simulate({}) : instance;
701
745
  if (Array.isArray(target)) throw new Error("PermDock: snapshotFixture expected a PermDock instance");
702
746
  const snapshot = options.include === void 0 && options.tenants === void 0 ? target.snapshot() : target.snapshot({
@@ -744,7 +788,7 @@ async function* ticks(org, signal) {
744
788
  function demotionKey(user, org) {
745
789
  return JSON.stringify([user, org]);
746
790
  }
747
- function createDomain$1() {
791
+ function createDomain$2() {
748
792
  const demoted = /* @__PURE__ */ new Set();
749
793
  const principalOf = (sub, path) => {
750
794
  const principal = saasPrincipal(sub, orgOf(path));
@@ -777,14 +821,14 @@ function createDomain$1() {
777
821
  };
778
822
  },
779
823
  org: orgOf,
780
- customRoles: memoryRoleSource(saasCustomRoles),
824
+ customRoles: memoryCustomRoleSource(saasCustomRoles),
781
825
  store: memoryApprovalStore(),
782
- limits: memoryLimitStore(),
826
+ limits: limits(memoryLimitStore()),
783
827
  project: (id) => id === void 0 ? null : saasSeed.projects.find((row) => row.id === id) ?? null
784
828
  };
785
829
  }
786
830
  async function approverSubject(user, tenant) {
787
- return (await createPermDock(saasPolicy, saasPrincipal(user, tenant), { tenant })).subject;
831
+ return (await getPermDock(saasPolicy, saasPrincipal(user, tenant), { tenant })).subject;
788
832
  }
789
833
  //#endregion
790
834
  //#region src/testing/http-adapter/wire.ts
@@ -991,7 +1035,7 @@ function testHttpAdapter(options) {
991
1035
  it.skipIf(reason !== void 0)(reason === void 0 ? title : `${title} (${reason})`, run, timeout);
992
1036
  };
993
1037
  describe(`${options.name}: real-life HTTP scenarios`, () => {
994
- const domain = createDomain$1();
1038
+ const domain = createDomain$2();
995
1039
  const tokens = /* @__PURE__ */ new Map();
996
1040
  let mounted;
997
1041
  beforeAll(async () => {
@@ -1168,7 +1212,7 @@ function testHttpAdapter(options) {
1168
1212
  });
1169
1213
  expect(denied.status).toBe(403);
1170
1214
  expect(field(denied.body, "type")).toBe("https://permdock.com/problems/denied");
1171
- const bob = await createPermDock(saasPolicy, saasPrincipal("bob", "acme"), { tenant: "acme" });
1215
+ const bob = await getPermDock(saasPolicy, saasPrincipal("bob", "acme"), { tenant: "acme" });
1172
1216
  const permission = saasPolicy.permissions.project.delete;
1173
1217
  const fromProtect = await readResult(problemFromDecision(bob.decide(permission, project$1("p2")), permission, bob.subject));
1174
1218
  expect(denied.status).toBe(fromProtect.status);
@@ -1304,7 +1348,7 @@ function testHttpAdapter(options) {
1304
1348
  });
1305
1349
  const end = await untilEnd(stream);
1306
1350
  expect(end.problem).toMatchObject({
1307
- type: "https://permdock.com/problems/unauthenticated",
1351
+ type: "about:blank",
1308
1352
  status: 401,
1309
1353
  detail: "session-revoked"
1310
1354
  });
@@ -1364,7 +1408,7 @@ function testHttpAdapter(options) {
1364
1408
  expect((await stream.next())?.event).toBe("message");
1365
1409
  const end = await untilEnd(stream);
1366
1410
  expect(end.problem).toMatchObject({
1367
- type: "https://permdock.com/problems/unauthenticated",
1411
+ type: "about:blank",
1368
1412
  detail: "expired"
1369
1413
  });
1370
1414
  expect(end.ended).toBe(true);
@@ -1470,12 +1514,12 @@ const SCENARIOS = [
1470
1514
  expected: "approval-required"
1471
1515
  }
1472
1516
  ];
1473
- function loadProject(args) {
1517
+ function loadProject$1(args) {
1474
1518
  const id = typeof args === "object" && args !== null ? args.id : void 0;
1475
1519
  if (id === "throws") throw new Error("project store unavailable");
1476
1520
  return saasSeed.projects.find((row) => row.id === id) ?? null;
1477
1521
  }
1478
- function createDomain() {
1522
+ function createDomain$1() {
1479
1523
  return {
1480
1524
  policy: saasPolicy,
1481
1525
  subject: (user, org) => {
@@ -1485,11 +1529,11 @@ function createDomain() {
1485
1529
  tools: {
1486
1530
  project_read: {
1487
1531
  permission: saasPermissions.project.read,
1488
- data: loadProject
1532
+ data: loadProject$1
1489
1533
  },
1490
1534
  api_keys_revoke_all: { permission: saasPermissions.apiKey.revokeAll }
1491
1535
  },
1492
- customRoles: memoryRoleSource(saasCustomRoles),
1536
+ customRoles: memoryCustomRoleSource(saasCustomRoles),
1493
1537
  store: memoryApprovalStore()
1494
1538
  };
1495
1539
  }
@@ -1499,7 +1543,7 @@ function createDomain() {
1499
1543
  */
1500
1544
  function testAgentAdapter(options) {
1501
1545
  describe(`${options.name}: agent adapter scenarios`, () => {
1502
- const domain = createDomain();
1546
+ const domain = createDomain$1();
1503
1547
  let mounted;
1504
1548
  beforeAll(async () => {
1505
1549
  mounted = await options.mount(domain);
@@ -1517,12 +1561,201 @@ function testAgentAdapter(options) {
1517
1561
  });
1518
1562
  }
1519
1563
  //#endregion
1564
+ //#region src/testing/agent-runtime.ts
1565
+ const alice = {
1566
+ user: "alice",
1567
+ org: "acme"
1568
+ };
1569
+ const erin = {
1570
+ user: "erin",
1571
+ org: "globex"
1572
+ };
1573
+ const mallory = {
1574
+ user: "mallory",
1575
+ org: "acme"
1576
+ };
1577
+ const OPERATIONS = [
1578
+ {
1579
+ name: "granted",
1580
+ title: "grants a member reading a project in their org",
1581
+ context: alice,
1582
+ operation: "project_read",
1583
+ input: { id: "p1" },
1584
+ expected: "granted"
1585
+ },
1586
+ {
1587
+ name: "denied",
1588
+ title: "denies a user with no membership",
1589
+ context: mallory,
1590
+ operation: "project_read",
1591
+ input: { id: "p1" },
1592
+ expected: "denied"
1593
+ },
1594
+ {
1595
+ name: "cross-tenant",
1596
+ title: "denies a row from another org",
1597
+ context: alice,
1598
+ operation: "project_read",
1599
+ input: { id: "g1" },
1600
+ expected: "denied"
1601
+ },
1602
+ {
1603
+ name: "anonymous",
1604
+ title: "denies a call with no user",
1605
+ context: {
1606
+ user: null,
1607
+ org: "acme"
1608
+ },
1609
+ operation: "project_read",
1610
+ input: { id: "p1" },
1611
+ expected: "denied"
1612
+ },
1613
+ {
1614
+ name: "subject-throws",
1615
+ title: "denies when the subject resolver throws",
1616
+ context: {
1617
+ user: AGENT_THROWING_USER,
1618
+ org: "acme"
1619
+ },
1620
+ operation: "project_read",
1621
+ input: { id: "p1" },
1622
+ expected: "denied"
1623
+ },
1624
+ {
1625
+ name: "loader-throws",
1626
+ title: "denies when the row loader throws",
1627
+ context: alice,
1628
+ operation: "project_read",
1629
+ input: { id: AGENT_THROWING_ROW },
1630
+ expected: "denied"
1631
+ },
1632
+ {
1633
+ name: "missing-row",
1634
+ title: "denies when the row does not exist",
1635
+ context: alice,
1636
+ operation: "project_read",
1637
+ input: { id: "missing" },
1638
+ expected: "denied"
1639
+ },
1640
+ {
1641
+ name: "approval-required",
1642
+ title: "asks for approval before revoking every API key",
1643
+ context: alice,
1644
+ operation: "api_keys_revoke_all",
1645
+ input: {},
1646
+ expected: "approval-required"
1647
+ },
1648
+ {
1649
+ name: "unmapped-operation",
1650
+ title: "denies an operation the map does not name",
1651
+ context: alice,
1652
+ operation: "drop_database",
1653
+ input: {},
1654
+ expected: "denied"
1655
+ }
1656
+ ];
1657
+ function loadProject(args) {
1658
+ const id = typeof args === "object" && args !== null ? args.id : void 0;
1659
+ if (id === "throws") throw new Error("project store unavailable");
1660
+ return saasSeed.projects.find((row) => row.id === id) ?? null;
1661
+ }
1662
+ function createDomain() {
1663
+ return {
1664
+ policy: saasPolicy,
1665
+ subject: (user, org) => {
1666
+ if (user === "throws") throw new Error("session store unavailable");
1667
+ return user === null ? null : saasPrincipal(user, org);
1668
+ },
1669
+ operations: {
1670
+ project_read: {
1671
+ permission: saasPermissions.project.read,
1672
+ data: loadProject
1673
+ },
1674
+ project_search: {
1675
+ permission: saasPermissions.project.list,
1676
+ results: saasPermissions.project.read
1677
+ },
1678
+ api_keys_revoke_all: { permission: saasPermissions.apiKey.revokeAll }
1679
+ },
1680
+ models: {
1681
+ standard: saasPermissions.integration.read,
1682
+ frontier: saasPermissions.analytics.read
1683
+ },
1684
+ customRoles: memoryCustomRoleSource(saasCustomRoles),
1685
+ store: memoryApprovalStore()
1686
+ };
1687
+ }
1688
+ function projects(...ids) {
1689
+ return ids.flatMap((id) => saasSeed.projects.filter((row) => row.id === id));
1690
+ }
1691
+ /**
1692
+ * Runs the `AgentPermDock` contract against a runtime: operations decided by
1693
+ * name, the listing hint, the model gate, the results filter and gateway
1694
+ * attribution. Every failure denies, and no method throws.
1695
+ */
1696
+ function testAgentRuntime(options) {
1697
+ describe(`${options.name}: agent runtime contract`, () => {
1698
+ const domain = createDomain();
1699
+ let mounted;
1700
+ beforeAll(async () => {
1701
+ mounted = await options.mount(domain);
1702
+ });
1703
+ afterAll(async () => {
1704
+ await mounted.close?.();
1705
+ });
1706
+ const agent = () => mounted.agent;
1707
+ const scenario = (name, title, run) => {
1708
+ const reason = options.skip?.[name];
1709
+ it.skipIf(reason !== void 0)(reason === void 0 ? title : `${title} (${reason})`, run);
1710
+ };
1711
+ for (const entry of OPERATIONS) scenario(entry.name, entry.title, async () => {
1712
+ const verdict = await agent().decideOperation(entry.operation, entry.input, entry.context);
1713
+ expect(verdict.outcome).toBe(entry.expected);
1714
+ });
1715
+ scenario("allowed-operations", "lists the operations a subject could be granted, and none for a stranger", async () => {
1716
+ const listed = await agent().allowedOperations(alice);
1717
+ expect(listed.has("project_read")).toBe(true);
1718
+ expect(listed.has("drop_database")).toBe(false);
1719
+ expect([...await agent().allowedOperations(mallory)]).toEqual([]);
1720
+ });
1721
+ scenario("model-gate", "grants a model by its permission and denies one the map does not name", async () => {
1722
+ expect((await agent().decideModel("frontier", erin)).outcome).toBe("granted");
1723
+ expect((await agent().decideModel("frontier", alice)).outcome).toBe("denied");
1724
+ expect((await agent().decideModel("unlisted", erin)).outcome).toBe("denied");
1725
+ expect((await agent().decideModel("standard", {
1726
+ user: AGENT_THROWING_USER,
1727
+ org: "acme"
1728
+ })).outcome).toBe("denied");
1729
+ });
1730
+ scenario("permitted-models", "returns the granted models in declaration order", async () => {
1731
+ expect(await agent().permittedModels(erin)).toEqual(["standard", "frontier"]);
1732
+ expect(await agent().permittedModels(alice)).toEqual(["standard"]);
1733
+ expect(await agent().permittedModels(mallory)).toEqual([]);
1734
+ });
1735
+ scenario("results-filter", "keeps the rows the results permission grants, and none for an unknown operation", async () => {
1736
+ const rows = projects("p1", "g1", "p2");
1737
+ expect((await agent().filterResults("project_search", rows, alice)).map((row) => row.id)).toEqual(["p1", "p2"]);
1738
+ expect(await agent().filterResults("project_search", rows, mallory)).toEqual([]);
1739
+ expect(await agent().filterResults("drop_database", rows, alice)).toEqual([]);
1740
+ });
1741
+ scenario("gateway-options", "attributes gateway usage to the principal, and to nobody for an anonymous call", async () => {
1742
+ const attributed = await agent().gatewayOptions(alice);
1743
+ expect(attributed.gateway.user).toBe("alice");
1744
+ const anonymous = await agent().gatewayOptions({
1745
+ user: null,
1746
+ org: "acme"
1747
+ });
1748
+ expect(anonymous.gateway.user).toBeUndefined();
1749
+ });
1750
+ });
1751
+ }
1752
+ //#endregion
1520
1753
  //#region src/testing/authzen.ts
1521
1754
  function sortKey(value) {
1522
1755
  return JSON.stringify(Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => byCodePoint(a, b))));
1523
1756
  }
1524
1757
  /** Runs AuthZEN 1.0 interop vectors against an Authorization API endpoint. */
1525
- function testAuthZen(handle, options) {
1758
+ function testAuthzen(handle, options) {
1526
1759
  const origin = options.origin ?? "https://pdp.example.com";
1527
1760
  const post = async (path, body) => {
1528
1761
  const response = await handle(new Request(`${origin}${path}`, {
@@ -1628,7 +1861,7 @@ const authzenTodoPolicy = definePolicy(authzenTodoPermissions, {
1628
1861
  role("evil_genius", [...everyone, allow(todo.can_update_todo)]),
1629
1862
  role("pep", [])
1630
1863
  ],
1631
- principal: (user) => {
1864
+ subject: (user) => {
1632
1865
  const id = user !== null && typeof user === "object" && "id" in user ? user.id : void 0;
1633
1866
  if (id === "pep") return {
1634
1867
  id: "pep",
@@ -1776,6 +2009,7 @@ function authzenTodoVectors() {
1776
2009
  function sampleApproval(token) {
1777
2010
  return {
1778
2011
  v: 1,
2012
+ id: `apr_${token}`,
1779
2013
  token,
1780
2014
  permission: "post.delete",
1781
2015
  scope: "post:delete",
@@ -1794,8 +2028,8 @@ function sampleApproval(token) {
1794
2028
  }
1795
2029
  },
1796
2030
  detail: "post.delete requires human approval.",
1797
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1798
- expiresAt: new Date(Date.now() + 36e5).toISOString(),
2031
+ createdAt: isoNow(),
2032
+ expiresAt: isoAt(nowMillis() + 36e5),
1799
2033
  status: "pending"
1800
2034
  };
1801
2035
  }
@@ -1813,7 +2047,7 @@ const secondApprover = {
1813
2047
  },
1814
2048
  context: {}
1815
2049
  };
1816
- const admin = {
2050
+ const admin$2 = {
1817
2051
  kind: "role",
1818
2052
  role: "admin",
1819
2053
  scope: "global"
@@ -1905,7 +2139,7 @@ function testApprovalStore(store, options = {}) {
1905
2139
  by: approver
1906
2140
  }))).rejects.toThrow(/not pending/);
1907
2141
  await store.create(sampleApproval("stale-token"));
1908
- const expired = await store.expire(new Date(Date.now() + 72e5));
2142
+ const expired = await store.expire({ epochMilliseconds: nowMillis() + 72e5 });
1909
2143
  expect(expired).toBeGreaterThanOrEqual(1);
1910
2144
  });
1911
2145
  it("pages list results with limit and an opaque cursor", async () => {
@@ -1920,7 +2154,7 @@ function testApprovalStore(store, options = {}) {
1920
2154
  id: "u_pager",
1921
2155
  roles: ["member"]
1922
2156
  } },
1923
- createdAt: new Date(base + index * 1e3).toISOString()
2157
+ createdAt: isoAt(base + index * 1e3)
1924
2158
  })));
1925
2159
  const first = await store.list({
1926
2160
  principalId: "u_pager",
@@ -1972,9 +2206,9 @@ function testApprovalStore(store, options = {}) {
1972
2206
  };
1973
2207
  const shapes = [
1974
2208
  ["self-human", void 0],
1975
- ["self-by", { by: admin }],
2209
+ ["self-by", { by: admin$2 }],
1976
2210
  ["self-distinct", {
1977
- by: admin,
2211
+ by: admin$2,
1978
2212
  distinct: true
1979
2213
  }]
1980
2214
  ];
@@ -1992,7 +2226,7 @@ function testApprovalStore(store, options = {}) {
1992
2226
  await store.create({
1993
2227
  ...sampleApproval("self-optout"),
1994
2228
  approvers: {
1995
- by: admin,
2229
+ by: admin$2,
1996
2230
  distinct: false
1997
2231
  }
1998
2232
  });
@@ -2006,7 +2240,7 @@ function testApprovalStore(store, options = {}) {
2006
2240
  await store.create({
2007
2241
  ...sampleApproval("quorum-token"),
2008
2242
  approvers: {
2009
- by: admin,
2243
+ by: admin$2,
2010
2244
  quorum: 2
2011
2245
  }
2012
2246
  });
@@ -2037,7 +2271,7 @@ function testApprovalStore(store, options = {}) {
2037
2271
  await store.create({
2038
2272
  ...sampleApproval("vouched-token"),
2039
2273
  approvers: {
2040
- by: admin,
2274
+ by: admin$2,
2041
2275
  quorum: 2
2042
2276
  }
2043
2277
  });
@@ -2075,7 +2309,7 @@ function testApprovalStore(store, options = {}) {
2075
2309
  await store.create({
2076
2310
  ...sampleApproval("veto-token"),
2077
2311
  approvers: {
2078
- by: admin,
2312
+ by: admin$2,
2079
2313
  quorum: 2
2080
2314
  }
2081
2315
  });
@@ -2095,7 +2329,7 @@ function testApprovalStore(store, options = {}) {
2095
2329
  context: {}
2096
2330
  };
2097
2331
  const escalation = {
2098
- after: "1h",
2332
+ after: 3600,
2099
2333
  to: {
2100
2334
  kind: "role",
2101
2335
  role: "auditor",
@@ -2105,7 +2339,7 @@ function testApprovalStore(store, options = {}) {
2105
2339
  await store.create({
2106
2340
  ...sampleApproval("fresh-escalation"),
2107
2341
  approvers: {
2108
- by: admin,
2342
+ by: admin$2,
2109
2343
  escalation
2110
2344
  }
2111
2345
  });
@@ -2115,9 +2349,9 @@ function testApprovalStore(store, options = {}) {
2115
2349
  }))).rejects.toThrow(/eligible/u);
2116
2350
  await store.create({
2117
2351
  ...sampleApproval("waited-escalation"),
2118
- createdAt: (/* @__PURE__ */ new Date(Date.now() - 72e5)).toISOString(),
2352
+ createdAt: isoAt(nowMillis() - 72e5),
2119
2353
  approvers: {
2120
- by: admin,
2354
+ by: admin$2,
2121
2355
  escalation
2122
2356
  }
2123
2357
  });
@@ -2136,7 +2370,7 @@ function testApprovalStore(store, options = {}) {
2136
2370
  stages: [{ by: {
2137
2371
  kind: "user",
2138
2372
  id: "u_10"
2139
- } }, { by: admin }]
2373
+ } }, { by: admin$2 }]
2140
2374
  }
2141
2375
  });
2142
2376
  await expect(Promise.resolve().then(() => store.resolve("stages-token", {
@@ -2206,49 +2440,2608 @@ function testApprovalPolicySource(source, options) {
2206
2440
  expect(other.filter((entry) => entry.tenant !== void 0)).toEqual([]);
2207
2441
  });
2208
2442
  }
2443
+ const reviewItem = {
2444
+ v: 1,
2445
+ id: "rev:u_1:post.delete",
2446
+ review: "rev",
2447
+ principal: "u_1",
2448
+ permission: "post.delete",
2449
+ via: ["member"],
2450
+ status: "pending"
2451
+ };
2452
+ /**
2453
+ * Checks an `AccessReviewStore`: create is kept, items page, a decision is
2454
+ * recorded once, only a revoked item can be marked applied, and close is once.
2455
+ */
2456
+ function testAccessReviewStore(store) {
2457
+ it("records a review, pages its items and stores one decision", async () => {
2458
+ const review = {
2459
+ v: 1,
2460
+ id: "rev",
2461
+ tenant: "o1",
2462
+ permissions: ["post.delete"],
2463
+ reviewers: {
2464
+ kind: "user",
2465
+ id: "u_9"
2466
+ },
2467
+ createdBy: "u_owner",
2468
+ createdAt: "2026-10-13T12:00:00.000Z",
2469
+ dueAt: "2026-12-31T00:00:00.000Z",
2470
+ status: "open",
2471
+ complete: true
2472
+ };
2473
+ await store.create(review, [reviewItem, {
2474
+ ...reviewItem,
2475
+ id: "rev:u_2:post.delete",
2476
+ principal: "u_2"
2477
+ }]);
2478
+ await store.create(review, []);
2479
+ expect((await store.get("rev"))?.status).toBe("open");
2480
+ const page = await store.items({
2481
+ review: "rev",
2482
+ limit: 1
2483
+ });
2484
+ expect(page.items).toHaveLength(1);
2485
+ expect(page.next).toEqual(expect.any(String));
2486
+ const kept = await store.decide("rev", reviewItem.id, {
2487
+ status: "kept",
2488
+ decidedBy: "u_9",
2489
+ decidedAt: "2026-10-14T12:00:00.000Z"
2490
+ });
2491
+ expect(kept.status).toBe("kept");
2492
+ await expect(Promise.resolve().then(() => store.decide("rev", reviewItem.id, {
2493
+ status: "revoked",
2494
+ decidedBy: "u_9",
2495
+ decidedAt: "2026-10-14T12:00:00.000Z"
2496
+ }))).rejects.toThrow(/decided/u);
2497
+ const revoked = await store.decide("rev", "rev:u_2:post.delete", {
2498
+ status: "revoked",
2499
+ decidedBy: "u_9",
2500
+ decidedAt: "2026-10-14T12:00:00.000Z"
2501
+ });
2502
+ expect(revoked.status).toBe("revoked");
2503
+ await expect(Promise.resolve().then(() => store.markApplied("rev", reviewItem.id, "2026-10-15T00:00:00.000Z"))).rejects.toThrow(/revoked/u);
2504
+ const applied = await store.markApplied("rev", "rev:u_2:post.delete", "2026-10-15T00:00:00.000Z");
2505
+ expect(applied.appliedAt).toBe("2026-10-15T00:00:00.000Z");
2506
+ expect((await store.close("rev")).status).toBe("closed");
2507
+ await expect(Promise.resolve().then(() => store.close("rev"))).rejects.toThrow(/closed/u);
2508
+ });
2509
+ }
2510
+ /**
2511
+ * Checks an `ApprovalDelegationSource`: entries are plain JSON, name the
2512
+ * delegate, and are active at `at`. A query for another delegate is empty.
2513
+ */
2514
+ function testApprovalDelegationSource(source, options) {
2515
+ it("answers with active plain delegations for the delegate", async () => {
2516
+ const at = options.at ?? isoNow();
2517
+ const entries = await source.delegationsFor({
2518
+ delegate: options.delegate,
2519
+ ...options.tenant === void 0 ? {} : { tenant: options.tenant },
2520
+ at
2521
+ });
2522
+ expect(Array.isArray(entries)).toBe(true);
2523
+ expect(entries.some((entry) => entry.delegate === options.delegate && entry.delegator === options.delegator)).toBe(true);
2524
+ const instant = Date.parse(at);
2525
+ for (const entry of entries) {
2526
+ expect(JSON.parse(JSON.stringify(entry))).toEqual(entry);
2527
+ expect(entry.delegate).toBe(options.delegate);
2528
+ expect(Date.parse(entry.from)).toBeLessThanOrEqual(instant);
2529
+ expect(Date.parse(entry.until)).toBeGreaterThan(instant);
2530
+ }
2531
+ expect(await source.delegationsFor({
2532
+ delegate: "__permdock_unknown__",
2533
+ ...options.tenant === void 0 ? {} : { tenant: options.tenant },
2534
+ at
2535
+ })).toEqual([]);
2536
+ });
2537
+ }
2538
+ /**
2539
+ * Checks an `IdempotencyStore`: a stored response comes back as written
2540
+ * until it expires, keys never share an entry, and a later `set` replaces an
2541
+ * earlier one.
2542
+ */
2543
+ function testIdempotencyStore(store) {
2544
+ it("replays a stored response per key and keeps keys apart", async () => {
2545
+ const key = `u_1\n${crypto.randomUUID()}`;
2546
+ const other = `u_2\n${crypto.randomUUID()}`;
2547
+ const response = {
2548
+ fingerprint: "approve apr_1 sha-256:abc",
2549
+ status: 200,
2550
+ headers: [["content-type", "application/json"]],
2551
+ body: "{\"status\":\"approved\"}"
2552
+ };
2553
+ const later = nowMillis() + 36e5;
2554
+ expect(await store.get(key)).toBeUndefined();
2555
+ await store.set(key, response, later);
2556
+ expect(await store.get(key)).toEqual(response);
2557
+ expect(await store.get(other)).toBeUndefined();
2558
+ const replaced = {
2559
+ ...response,
2560
+ status: 201
2561
+ };
2562
+ await store.set(key, replaced, later);
2563
+ expect(await store.get(key)).toEqual(replaced);
2564
+ });
2565
+ }
2209
2566
  //#endregion
2210
- //#region src/testing/conformance/where.ts
2211
- const WHERE_ROWS = [
2212
- {
2213
- id: "r1",
2214
- authorId: "u1",
2215
- score: 5,
2216
- title: "Plan 100%",
2217
- archived: false,
2218
- note: null
2219
- },
2220
- {
2221
- id: "r2",
2222
- authorId: "u2",
2223
- score: 10,
2224
- title: "plan_b",
2225
- archived: true,
2226
- note: "kept"
2567
+ //#region src/testing/conformance/runtime-stores.ts
2568
+ function testLimitStore(store) {
2569
+ it("counts down synchronously and fails closed when exhausted", async () => {
2570
+ const input = {
2571
+ key: "report.export",
2572
+ subjectId: "u_1",
2573
+ count: 2,
2574
+ per: 3600,
2575
+ now: { epochMilliseconds: 17e11 }
2576
+ };
2577
+ const first = await store.consume(input);
2578
+ expect(first.remaining).toBeGreaterThanOrEqual(0);
2579
+ const peeked = store.remaining(input);
2580
+ if (peeked !== void 0) {
2581
+ expect(typeof peeked.remaining).toBe("number");
2582
+ expect(peeked !== null && typeof peeked === "object" && "then" in peeked && typeof peeked.then === "function").toBe(false);
2583
+ }
2584
+ await store.consume(input);
2585
+ const exhausted = await store.consume(input);
2586
+ expect(exhausted.remaining).toBeLessThan(0);
2587
+ });
2588
+ }
2589
+ function testDecisionSink(sink) {
2590
+ it("accepts batches and never propagates write errors", async () => {
2591
+ await expect(Promise.resolve(sink.write([{
2592
+ type: "membership",
2593
+ at: isoNow(),
2594
+ source: "app",
2595
+ operation: "changed",
2596
+ principal: { id: "u_1" },
2597
+ roles: {
2598
+ added: ["admin"],
2599
+ removed: ["member"]
2600
+ }
2601
+ }]))).resolves.toBeUndefined();
2602
+ await expect(Promise.resolve(sink.write([]))).resolves.toBeUndefined();
2603
+ if (sink.flush !== void 0) {
2604
+ await expect(Promise.resolve(sink.flush())).resolves.toBeUndefined();
2605
+ await expect(Promise.resolve(sink.flush())).resolves.toBeUndefined();
2606
+ }
2607
+ });
2608
+ }
2609
+ function testSnapshotSource(source) {
2610
+ it("round-trips a snapshot", async () => {
2611
+ const snapshot = await source.get();
2612
+ expect(snapshot === null || snapshot === void 0).toBe(false);
2613
+ if (typeof snapshot === "object" && snapshot !== null && "v" in snapshot) expect(snapshot.v).toBe(1);
2614
+ if (source.subscribe !== void 0) source.subscribe(() => void 0)();
2615
+ });
2616
+ }
2617
+ function testPolicySource(source, options = {}) {
2618
+ const readCurrent = () => {
2619
+ const document = source.current();
2620
+ expect(document !== null && typeof document === "object" && "then" in document && typeof document.then === "function").toBe(false);
2621
+ return document;
2622
+ };
2623
+ it("returns null or a v1 policy document synchronously", () => {
2624
+ const document = readCurrent();
2625
+ if (document !== null) expect(parsePolicyDocument(document)).toEqual(document);
2626
+ });
2627
+ it("refreshes without rejecting and keeps current() synchronous", async () => {
2628
+ await expect(Promise.resolve(source.refresh())).resolves.toBeUndefined();
2629
+ const document = readCurrent();
2630
+ if (document !== null) expect(document.v).toBe(1);
2631
+ });
2632
+ if (options.policy !== void 0) {
2633
+ const policy = options.policy;
2634
+ it("merges only grants on hostable permissions", () => {
2635
+ const document = readCurrent();
2636
+ const merged = mergeHostedGrants(policy, document);
2637
+ for (const grant of merged.policy.grants) if (grant.hosted !== void 0) expect(policy.hostable).toContain(grant.permission.key);
2638
+ for (const dropped of merged.dropped) expect(dropped.kind).toBe("hosted-grant-dropped");
2639
+ });
2640
+ }
2641
+ }
2642
+ //#endregion
2643
+ //#region src/testing/cloud-api/platform.ts
2644
+ const CATALOG = "https://permdock.com/schemas/catalog-v1.json";
2645
+ const MANIFEST = "https://permdock.com/schemas/supabase-manifest-v1.json";
2646
+ const PUSHED_POLICY = "https://permdock.com/schemas/pushed-policy-v1.json";
2647
+ const OAUTH_FORM$1 = "application/x-www-form-urlencoded";
2648
+ /** The scopes the platform issuer grants a person's login. */
2649
+ const PLATFORM_SCOPES = {
2650
+ "projects:read": "List and read the projects the person belongs to",
2651
+ "policy:read": "Read a project's pushed catalog, manifest and policy",
2652
+ "catalog:write": "Push a catalog and policy with permdock cloud push"
2653
+ };
2654
+ const ref$1 = (name) => ({ $ref: `#/components/schemas/${name}` });
2655
+ const RATE_LIMIT_HEADERS = {
2656
+ RateLimit: {
2657
+ description: "Remaining quota per policy, draft-ietf-httpapi-ratelimit-headers-11 (`\"read\";r=59;t=30`)",
2658
+ schema: { type: "string" }
2227
2659
  },
2228
- {
2229
- id: "r3",
2230
- authorId: null,
2231
- score: null,
2232
- title: null,
2233
- archived: null,
2234
- note: null
2660
+ "RateLimit-Policy": {
2661
+ description: "The quota policy, draft-ietf-httpapi-ratelimit-headers-11 (`\"read\";q=60;w=60`)",
2662
+ schema: { type: "string" }
2235
2663
  }
2236
- ];
2237
- const WHERE_CASES = [
2238
- ["eq", {
2239
- op: "eq",
2240
- field: "authorId",
2241
- value: "u1"
2242
- }],
2243
- ["ne", {
2244
- op: "ne",
2245
- field: "authorId",
2246
- value: "u1"
2247
- }],
2248
- ["eq on a boolean", {
2249
- op: "eq",
2250
- field: "archived",
2251
- value: false
2664
+ };
2665
+ const limited = (description, schema) => ({
2666
+ description,
2667
+ headers: RATE_LIMIT_HEADERS,
2668
+ content: { "application/json": { schema } }
2669
+ });
2670
+ const problem$1 = (description, headers) => ({
2671
+ description,
2672
+ ...headers === void 0 ? {} : { headers },
2673
+ content: { "application/problem+json": { schema: ref$1("Problem") } }
2674
+ });
2675
+ const oauthJson = (description, schema) => ({
2676
+ description,
2677
+ content: { "application/json": { schema } }
2678
+ });
2679
+ const TOO_MANY = problem$1("Rate limited; retry after Retry-After seconds", {
2680
+ ...RATE_LIMIT_HEADERS,
2681
+ "Retry-After": {
2682
+ description: "Seconds to wait (RFC 9110)",
2683
+ schema: { type: "string" }
2684
+ }
2685
+ });
2686
+ const READ_SECURITY = (scope) => [{ platform: [scope] }, { cloudToken: [] }];
2687
+ const read = (scope, summary, responses) => ({ get: {
2688
+ security: READ_SECURITY(scope),
2689
+ summary,
2690
+ responses: {
2691
+ ...responses,
2692
+ "401": problem$1("Missing, expired or revoked token"),
2693
+ "403": problem$1("The token may not read this project; a PERMDOCK_CLOUD_TOKEN reads only its own environment"),
2694
+ "429": TOO_MANY
2695
+ }
2696
+ } });
2697
+ const PLATFORM_SCHEMAS = {
2698
+ AuthorizationServerMetadata: {
2699
+ type: "object",
2700
+ description: "RFC 8414 metadata of the platform issuer",
2701
+ required: [
2702
+ "issuer",
2703
+ "authorization_endpoint",
2704
+ "token_endpoint",
2705
+ "device_authorization_endpoint",
2706
+ "revocation_endpoint",
2707
+ "registration_endpoint",
2708
+ "scopes_supported",
2709
+ "code_challenge_methods_supported"
2710
+ ],
2711
+ properties: {
2712
+ issuer: { type: "string" },
2713
+ authorization_endpoint: { type: "string" },
2714
+ device_authorization_endpoint: { type: "string" },
2715
+ token_endpoint: { type: "string" },
2716
+ revocation_endpoint: { type: "string" },
2717
+ registration_endpoint: { type: "string" },
2718
+ scopes_supported: {
2719
+ type: "array",
2720
+ items: { type: "string" },
2721
+ contains: { const: "policy:read" }
2722
+ },
2723
+ code_challenge_methods_supported: {
2724
+ type: "array",
2725
+ items: { const: "S256" },
2726
+ minItems: 1
2727
+ },
2728
+ grant_types_supported: {
2729
+ type: "array",
2730
+ items: { type: "string" }
2731
+ },
2732
+ response_types_supported: {
2733
+ type: "array",
2734
+ items: { type: "string" }
2735
+ },
2736
+ token_endpoint_auth_methods_supported: {
2737
+ type: "array",
2738
+ items: { type: "string" }
2739
+ }
2740
+ }
2741
+ },
2742
+ ClientRegistration: {
2743
+ type: "object",
2744
+ description: "RFC 7591 client information response",
2745
+ required: ["client_id", "redirect_uris"],
2746
+ properties: {
2747
+ client_id: {
2748
+ type: "string",
2749
+ minLength: 1
2750
+ },
2751
+ client_id_issued_at: { type: "integer" },
2752
+ redirect_uris: {
2753
+ type: "array",
2754
+ items: { type: "string" }
2755
+ },
2756
+ token_endpoint_auth_method: { type: "string" },
2757
+ grant_types: {
2758
+ type: "array",
2759
+ items: { type: "string" }
2760
+ },
2761
+ client_name: { type: "string" },
2762
+ scope: { type: "string" }
2763
+ }
2764
+ },
2765
+ Project: {
2766
+ type: "object",
2767
+ description: "A Cloud project. A project is one environment: `id` is the `{env}` of /v1/environments/{env}.",
2768
+ required: [
2769
+ "id",
2770
+ "name",
2771
+ "environment"
2772
+ ],
2773
+ properties: {
2774
+ id: {
2775
+ type: "string",
2776
+ minLength: 1
2777
+ },
2778
+ name: { type: "string" },
2779
+ environment: {
2780
+ type: "string",
2781
+ minLength: 1
2782
+ },
2783
+ issuer: {
2784
+ type: "string",
2785
+ description: "The environment's own issuer for app tokens"
2786
+ },
2787
+ catalog: {
2788
+ type: "object",
2789
+ required: ["version", "fingerprint"],
2790
+ properties: {
2791
+ version: {
2792
+ type: "integer",
2793
+ minimum: 1
2794
+ },
2795
+ fingerprint: { type: "string" }
2796
+ }
2797
+ }
2798
+ }
2799
+ },
2800
+ ProjectPolicy: {
2801
+ type: "object",
2802
+ description: "What the project's last cloud push sent, with JSON member order kept as pushed, so permdock cloud pull writes the bytes the offline commands write.",
2803
+ required: [
2804
+ "v",
2805
+ "project",
2806
+ "version",
2807
+ "fingerprint",
2808
+ "permdockVersion",
2809
+ "catalog"
2810
+ ],
2811
+ properties: {
2812
+ v: { const: 1 },
2813
+ project: {
2814
+ type: "string",
2815
+ minLength: 1
2816
+ },
2817
+ version: {
2818
+ type: "integer",
2819
+ minimum: 1
2820
+ },
2821
+ fingerprint: {
2822
+ type: "string",
2823
+ description: "catalogFingerprint(catalog); equals catalog.fingerprint"
2824
+ },
2825
+ permdockVersion: {
2826
+ type: "string",
2827
+ description: "The permdock version that pushed"
2828
+ },
2829
+ catalog: { $ref: CATALOG },
2830
+ manifest: { $ref: MANIFEST },
2831
+ policy: { $ref: PUSHED_POLICY }
2832
+ }
2833
+ }
2834
+ };
2835
+ const PROJECT_ID = {
2836
+ name: "id",
2837
+ in: "path",
2838
+ required: true,
2839
+ description: "The project id, the same id as its environment",
2840
+ schema: {
2841
+ type: "string",
2842
+ minLength: 1
2843
+ }
2844
+ };
2845
+ /** Routes of the platform issuer and its read API; they sit at the API root, outside /v1/environments/{env}. */
2846
+ const PLATFORM_PATHS = {
2847
+ "/.well-known/oauth-authorization-server": { get: {
2848
+ security: [],
2849
+ summary: "RFC 8414 metadata of the platform issuer. For the issuer https://permdock.com/api it is served at https://permdock.com/.well-known/oauth-authorization-server/api (section 3.1) and here.",
2850
+ responses: { "200": oauthJson("The metadata", ref$1("AuthorizationServerMetadata")) }
2851
+ } },
2852
+ "/oauth/authorize": { get: {
2853
+ security: [],
2854
+ summary: "Authorization code with PKCE S256 for public clients, including the RFC 8252 loopback redirect http://127.0.0.1:{port}/callback",
2855
+ parameters: [
2856
+ ...[
2857
+ "response_type",
2858
+ "client_id",
2859
+ "redirect_uri",
2860
+ "code_challenge"
2861
+ ].map((name) => ({
2862
+ name,
2863
+ in: "query",
2864
+ required: true,
2865
+ schema: { type: "string" }
2866
+ })),
2867
+ {
2868
+ name: "code_challenge_method",
2869
+ in: "query",
2870
+ required: true,
2871
+ schema: { const: "S256" }
2872
+ },
2873
+ {
2874
+ name: "state",
2875
+ in: "query",
2876
+ schema: { type: "string" }
2877
+ },
2878
+ {
2879
+ name: "scope",
2880
+ in: "query",
2881
+ schema: { type: "string" }
2882
+ }
2883
+ ],
2884
+ responses: {
2885
+ "302": { description: "Redirect to the client with a code" },
2886
+ "400": oauthJson("invalid_request, including plain PKCE", ref$1("OAuthError"))
2887
+ }
2888
+ } },
2889
+ "/oauth/device_authorization": { post: {
2890
+ security: [{ publicClient: [] }],
2891
+ summary: "RFC 8628 device authorization for permdock login",
2892
+ requestBody: {
2893
+ required: true,
2894
+ content: { [OAUTH_FORM$1]: { schema: {
2895
+ type: "object",
2896
+ required: ["client_id"]
2897
+ } } }
2898
+ },
2899
+ responses: {
2900
+ "200": oauthJson("The codes", {
2901
+ type: "object",
2902
+ required: [
2903
+ "device_code",
2904
+ "user_code",
2905
+ "verification_uri",
2906
+ "expires_in"
2907
+ ],
2908
+ properties: {
2909
+ device_code: { type: "string" },
2910
+ user_code: { type: "string" },
2911
+ verification_uri: { type: "string" },
2912
+ verification_uri_complete: { type: "string" },
2913
+ expires_in: { type: "integer" },
2914
+ interval: { type: "integer" }
2915
+ }
2916
+ }),
2917
+ "400": oauthJson("invalid_request or invalid_client", ref$1("OAuthError"))
2918
+ }
2919
+ } },
2920
+ "/oauth/token": { post: {
2921
+ security: [{ publicClient: [] }],
2922
+ summary: "authorization_code with code_verifier, device_code and refresh_token for public clients",
2923
+ requestBody: {
2924
+ required: true,
2925
+ content: { [OAUTH_FORM$1]: { schema: {
2926
+ type: "object",
2927
+ required: ["grant_type"],
2928
+ properties: { grant_type: { enum: [
2929
+ "authorization_code",
2930
+ "urn:ietf:params:oauth:grant-type:device_code",
2931
+ "refresh_token"
2932
+ ] } }
2933
+ } } }
2934
+ },
2935
+ responses: {
2936
+ "200": oauthJson("The tokens", ref$1("TokenResponse")),
2937
+ "400": oauthJson("An RFC 6749 or RFC 8628 error", ref$1("OAuthError"))
2938
+ }
2939
+ } },
2940
+ "/oauth/revoke": { post: {
2941
+ security: [{ publicClient: [] }],
2942
+ summary: "RFC 7009 revocation; permdock logout sends the refresh token. An unknown token also answers 200.",
2943
+ requestBody: {
2944
+ required: true,
2945
+ content: { [OAUTH_FORM$1]: { schema: {
2946
+ type: "object",
2947
+ required: ["token"],
2948
+ properties: {
2949
+ token: { type: "string" },
2950
+ token_type_hint: { enum: ["access_token", "refresh_token"] }
2951
+ }
2952
+ } } }
2953
+ },
2954
+ responses: {
2955
+ "200": { description: "Revoked, or the token was not known" },
2956
+ "400": oauthJson("invalid_request", ref$1("OAuthError"))
2957
+ }
2958
+ } },
2959
+ "/oauth/register": { post: {
2960
+ security: [],
2961
+ summary: "RFC 7591 dynamic client registration for MCP clients of https://permdock.com/mcp: public clients with loopback or https redirect URIs",
2962
+ requestBody: {
2963
+ required: true,
2964
+ content: { "application/json": { schema: {
2965
+ type: "object",
2966
+ required: ["redirect_uris"],
2967
+ properties: {
2968
+ redirect_uris: {
2969
+ type: "array",
2970
+ items: { type: "string" },
2971
+ minItems: 1
2972
+ },
2973
+ token_endpoint_auth_method: { const: "none" },
2974
+ client_name: { type: "string" },
2975
+ scope: { type: "string" }
2976
+ }
2977
+ } } }
2978
+ },
2979
+ responses: {
2980
+ "201": oauthJson("The registered client", ref$1("ClientRegistration")),
2981
+ "400": oauthJson("invalid_redirect_uri or invalid_client_metadata", ref$1("OAuthError"))
2982
+ }
2983
+ } },
2984
+ "/v1/projects": read("projects:read", "The projects the token reads", { "200": limited("The projects", {
2985
+ type: "object",
2986
+ required: ["items"],
2987
+ properties: {
2988
+ items: {
2989
+ type: "array",
2990
+ items: ref$1("Project")
2991
+ },
2992
+ next: { type: "string" }
2993
+ }
2994
+ }) }),
2995
+ "/v1/projects/{id}": {
2996
+ parameters: [PROJECT_ID],
2997
+ ...read("projects:read", "One project", {
2998
+ "200": limited("The project", ref$1("Project")),
2999
+ "404": problem$1("No such project")
3000
+ })
3001
+ },
3002
+ "/v1/projects/{id}/policy": {
3003
+ parameters: [PROJECT_ID],
3004
+ ...read("policy:read", "The pushed catalog, manifest and policy, for permdock cloud pull and rls generate --project", {
3005
+ "200": limited("The pushed policy", ref$1("ProjectPolicy")),
3006
+ "404": problem$1(`No such project, or \`type\` ${POLICY_NOT_PUBLISHED}: nothing pushed yet`)
3007
+ })
3008
+ }
3009
+ };
3010
+ const PLATFORM_SECURITY_SCHEMES = {
3011
+ platform: {
3012
+ type: "oauth2",
3013
+ description: "A person's login at the platform issuer https://permdock.com/api: permdock login (device flow, RFC 8628, or the RFC 8252 loopback with PKCE) or an MCP client registered with RFC 7591.",
3014
+ flows: { authorizationCode: {
3015
+ authorizationUrl: "https://permdock.com/api/oauth/authorize",
3016
+ tokenUrl: "https://permdock.com/api/oauth/token",
3017
+ refreshUrl: "https://permdock.com/api/oauth/token",
3018
+ scopes: PLATFORM_SCOPES
3019
+ } }
3020
+ },
3021
+ cloudToken: {
3022
+ type: "http",
3023
+ scheme: "bearer",
3024
+ description: "PERMDOCK_CLOUD_TOKEN: a read-only Cloud key pinned to one environment, for CI. It reads that project and its policy, and nothing else."
3025
+ }
3026
+ };
3027
+ //#endregion
3028
+ //#region src/testing/cloud-api/platform-checks.ts
3029
+ const ITEM = /^"[^"]+"(?:;[a-z]+=-?\d+)+$/u;
3030
+ /** Whether a RateLimit or RateLimit-Policy field value lists items with the parameters `keys` (draft-ietf-httpapi-ratelimit-headers-11). */
3031
+ function rateLimitField(value, keys) {
3032
+ if (value === null || value.trim() === "") return false;
3033
+ return value.split(",").every((raw) => {
3034
+ const item = raw.trim();
3035
+ return ITEM.test(item) && keys.every((key) => item.includes(`;${key}=`));
3036
+ });
3037
+ }
3038
+ function rateLimited(response) {
3039
+ return rateLimitField(response.headers.get("ratelimit"), ["r", "t"]) && rateLimitField(response.headers.get("ratelimit-policy"), ["q", "w"]);
3040
+ }
3041
+ function problemType(response, body) {
3042
+ return (response.headers.get("content-type") ?? "").startsWith("application/problem+json") && isRecord(body) ? body["type"] : void 0;
3043
+ }
3044
+ /** The platform issuer and project read API checks of `testCloudApi`. */
3045
+ function testCloudPlatform(url, environment, options, fetchFn) {
3046
+ const project = options.project ?? environment;
3047
+ const read = (path, token) => fetchFn(`${url}${path}`, token === null ? {} : { headers: { authorization: `Bearer ${token}` } });
3048
+ const form = (path, fields) => fetchFn(`${url}${path}`, {
3049
+ method: "POST",
3050
+ headers: { "content-type": "application/x-www-form-urlencoded" },
3051
+ body: new URLSearchParams(fields).toString()
3052
+ });
3053
+ it("publishes RFC 8414 metadata for the platform issuer", async () => {
3054
+ const metadata = await discoverAuthorizationServer(url, { fetch: (input, init) => fetchFn(String(input), init) });
3055
+ expect(metadata).not.toBeNull();
3056
+ expect(metadata).toMatchObject({
3057
+ issuer: url,
3058
+ code_challenge_methods_supported: ["S256"]
3059
+ });
3060
+ const scopes = metadata?.["scopes_supported"];
3061
+ expect(scopes).toEqual(expect.arrayContaining(Object.keys(PLATFORM_SCOPES)));
3062
+ });
3063
+ it("lists and reads the project and its policy with RateLimit fields", async () => {
3064
+ const list = await read("/v1/projects", options.token);
3065
+ expect(list.status).toBe(200);
3066
+ expect(rateLimited(list)).toBe(true);
3067
+ const listed = await list.json();
3068
+ const items = isRecord(listed) && Array.isArray(listed["items"]) ? listed["items"] : [];
3069
+ expect(items).toContainEqual(expect.objectContaining({ id: project }));
3070
+ const one = await read(`/v1/projects/${encodeURIComponent(project)}`, options.token);
3071
+ expect(one.status).toBe(200);
3072
+ expect(rateLimited(one)).toBe(true);
3073
+ expect(await one.json()).toMatchObject({
3074
+ id: project,
3075
+ environment: project
3076
+ });
3077
+ const policy = await read(`/v1/projects/${encodeURIComponent(project)}/policy`, options.token);
3078
+ const body = await policy.json();
3079
+ if (policy.status === 404) {
3080
+ expect(problemType(policy, body)).toBe(POLICY_NOT_PUBLISHED);
3081
+ return;
3082
+ }
3083
+ expect(policy.status).toBe(200);
3084
+ expect(rateLimited(policy)).toBe(true);
3085
+ expect(isRecord(body)).toBe(true);
3086
+ const catalog = isRecord(body) ? body["catalog"] : void 0;
3087
+ expect(isRecord(catalog)).toBe(true);
3088
+ const fingerprint = isRecord(catalog) ? catalogFingerprint(catalog) : "";
3089
+ expect(body).toMatchObject({
3090
+ v: 1,
3091
+ project,
3092
+ fingerprint
3093
+ });
3094
+ });
3095
+ it("answers policy-not-published as Problem Details before the first push", async (context) => {
3096
+ if (options.unpublished === void 0) {
3097
+ context.skip();
3098
+ return;
3099
+ }
3100
+ const response = await read(`/v1/projects/${encodeURIComponent(options.unpublished.project)}/policy`, options.unpublished.token);
3101
+ expect(response.status).toBe(404);
3102
+ expect(problemType(response, await response.json())).toBe(POLICY_NOT_PUBLISHED);
3103
+ });
3104
+ it("answers 401 Problem Details to the read API without a token", async () => {
3105
+ const response = await read(`/v1/projects/${encodeURIComponent(project)}`, null);
3106
+ expect(response.status).toBe(401);
3107
+ expect(response.headers.get("content-type")).toMatch(/^application\/problem\+json/u);
3108
+ });
3109
+ it("refuses a catalog push with the read-only token", async () => {
3110
+ const response = await fetchFn(`${url}/v1/environments/${encodeURIComponent(project)}/catalog`, {
3111
+ method: "POST",
3112
+ headers: {
3113
+ authorization: `Bearer ${options.token}`,
3114
+ "content-type": "application/json"
3115
+ },
3116
+ body: JSON.stringify({
3117
+ fingerprint: "x",
3118
+ catalog: {}
3119
+ })
3120
+ });
3121
+ expect([401, 403]).toContain(response.status);
3122
+ });
3123
+ it("answers RFC 7009 revocation of an unknown token with 200", async () => {
3124
+ const response = await form("/oauth/revoke", {
3125
+ token: `unknown-${globalThis.crypto.randomUUID()}`,
3126
+ token_type_hint: "refresh_token",
3127
+ client_id: "permdock-cli"
3128
+ });
3129
+ expect(response.status).toBe(200);
3130
+ });
3131
+ it("registers an MCP client with a loopback redirect and refuses a plain http one", async (context) => {
3132
+ if (options.registration !== true) {
3133
+ context.skip();
3134
+ return;
3135
+ }
3136
+ const register = (redirect) => fetchFn(`${url}/oauth/register`, {
3137
+ method: "POST",
3138
+ headers: { "content-type": "application/json" },
3139
+ body: JSON.stringify({
3140
+ redirect_uris: [redirect],
3141
+ token_endpoint_auth_method: "none",
3142
+ client_name: "PermDock contract test"
3143
+ })
3144
+ });
3145
+ const loopback = await register("http://127.0.0.1:33418/callback");
3146
+ expect(loopback.status).toBe(201);
3147
+ expect(await loopback.json()).toMatchObject({ client_id: expect.any(String) });
3148
+ expect((await register("http://mcp.example.invalid/callback")).status).toBe(400);
3149
+ });
3150
+ }
3151
+ //#endregion
3152
+ //#region src/testing/cloud-api/connector-schema.ts
3153
+ var import__2020 = require__2020();
3154
+ /**
3155
+ * `schemas/connector-v1.json`: the body of a PermDock Cloud connector, one
3156
+ * `oneOf` branch per kind with a closed `config`. No branch holds a secret.
3157
+ */
3158
+ const CONNECTOR_SCHEMA = freezeDeep({
3159
+ $schema: "https://json-schema.org/draft/2020-12/schema",
3160
+ $id: "https://permdock.com/schemas/connector-v1.json",
3161
+ title: "PermDock Cloud connector",
3162
+ description: "A connector on PermDock Cloud's /connectors route, wire format v1: the closed list of kinds, one branch per kind. Trusted issuers have their own route (/trusted-issuers).",
3163
+ oneOf: [
3164
+ {
3165
+ description: "CloudEvents batches signed with Standard Webhooks.",
3166
+ type: "object",
3167
+ required: [
3168
+ "kind",
3169
+ "name",
3170
+ "config"
3171
+ ],
3172
+ properties: {
3173
+ id: {
3174
+ type: "string",
3175
+ minLength: 1,
3176
+ maxLength: 256
3177
+ },
3178
+ kind: { const: "webhook" },
3179
+ name: {
3180
+ type: "string",
3181
+ minLength: 1,
3182
+ maxLength: 200
3183
+ },
3184
+ config: {
3185
+ type: "object",
3186
+ additionalProperties: false,
3187
+ required: ["url"],
3188
+ properties: {
3189
+ url: {
3190
+ type: "string",
3191
+ pattern: "^https://"
3192
+ },
3193
+ types: {
3194
+ type: "array",
3195
+ items: {
3196
+ type: "string",
3197
+ minLength: 1
3198
+ },
3199
+ uniqueItems: true,
3200
+ description: "CloudEvents types to deliver; all when absent."
3201
+ }
3202
+ }
3203
+ }
3204
+ }
3205
+ },
3206
+ {
3207
+ description: "OTLP/HTTP log records and spans with the permdock.* attributes.",
3208
+ type: "object",
3209
+ required: [
3210
+ "kind",
3211
+ "name",
3212
+ "config"
3213
+ ],
3214
+ properties: {
3215
+ id: {
3216
+ type: "string",
3217
+ minLength: 1,
3218
+ maxLength: 256
3219
+ },
3220
+ kind: { const: "otlp" },
3221
+ name: {
3222
+ type: "string",
3223
+ minLength: 1,
3224
+ maxLength: 200
3225
+ },
3226
+ config: {
3227
+ type: "object",
3228
+ additionalProperties: false,
3229
+ required: ["endpoint"],
3230
+ properties: {
3231
+ endpoint: {
3232
+ type: "string",
3233
+ pattern: "^https://"
3234
+ },
3235
+ headers: {
3236
+ type: "object",
3237
+ additionalProperties: { type: "string" }
3238
+ },
3239
+ signals: {
3240
+ type: "array",
3241
+ items: { enum: ["logs", "traces"] },
3242
+ minItems: 1,
3243
+ uniqueItems: true
3244
+ }
3245
+ }
3246
+ }
3247
+ }
3248
+ },
3249
+ {
3250
+ description: "The OCSF projection posted to a SIEM ingest API.",
3251
+ type: "object",
3252
+ required: [
3253
+ "kind",
3254
+ "name",
3255
+ "config"
3256
+ ],
3257
+ properties: {
3258
+ id: {
3259
+ type: "string",
3260
+ minLength: 1,
3261
+ maxLength: 256
3262
+ },
3263
+ kind: { const: "siem" },
3264
+ name: {
3265
+ type: "string",
3266
+ minLength: 1,
3267
+ maxLength: 200
3268
+ },
3269
+ config: {
3270
+ type: "object",
3271
+ additionalProperties: false,
3272
+ required: ["destination", "endpoint"],
3273
+ properties: {
3274
+ destination: { enum: [
3275
+ "splunk-hec",
3276
+ "sentinel",
3277
+ "security-lake",
3278
+ "google-secops",
3279
+ "elastic",
3280
+ "datadog"
3281
+ ] },
3282
+ endpoint: {
3283
+ type: "string",
3284
+ pattern: "^https://"
3285
+ }
3286
+ }
3287
+ }
3288
+ }
3289
+ },
3290
+ {
3291
+ description: "A scheduled decision-log export.",
3292
+ type: "object",
3293
+ required: [
3294
+ "kind",
3295
+ "name",
3296
+ "config"
3297
+ ],
3298
+ properties: {
3299
+ id: {
3300
+ type: "string",
3301
+ minLength: 1,
3302
+ maxLength: 256
3303
+ },
3304
+ kind: { const: "evidence" },
3305
+ name: {
3306
+ type: "string",
3307
+ minLength: 1,
3308
+ maxLength: 200
3309
+ },
3310
+ config: {
3311
+ type: "object",
3312
+ additionalProperties: false,
3313
+ required: ["format", "destination"],
3314
+ properties: {
3315
+ format: { enum: [
3316
+ "jws",
3317
+ "ocsf",
3318
+ "csv"
3319
+ ] },
3320
+ destination: {
3321
+ type: "string",
3322
+ pattern: "^https://"
3323
+ },
3324
+ schedule: { enum: ["hourly", "daily"] }
3325
+ }
3326
+ }
3327
+ }
3328
+ },
3329
+ {
3330
+ description: "A hosted SCIM 2.0 relay for one tenant, replayed to the application's scimHandler.",
3331
+ type: "object",
3332
+ required: [
3333
+ "kind",
3334
+ "name",
3335
+ "config"
3336
+ ],
3337
+ properties: {
3338
+ id: {
3339
+ type: "string",
3340
+ minLength: 1,
3341
+ maxLength: 256
3342
+ },
3343
+ kind: { const: "scim" },
3344
+ name: {
3345
+ type: "string",
3346
+ minLength: 1,
3347
+ maxLength: 200
3348
+ },
3349
+ config: {
3350
+ type: "object",
3351
+ additionalProperties: false,
3352
+ required: ["tenant", "target"],
3353
+ properties: {
3354
+ tenant: {
3355
+ type: "string",
3356
+ minLength: 1,
3357
+ maxLength: 256
3358
+ },
3359
+ target: {
3360
+ type: "string",
3361
+ pattern: "^https://"
3362
+ },
3363
+ groupRoles: {
3364
+ type: "object",
3365
+ description: "Group id to declared assignable role.",
3366
+ additionalProperties: {
3367
+ type: "string",
3368
+ minLength: 1,
3369
+ maxLength: 256
3370
+ }
3371
+ }
3372
+ }
3373
+ }
3374
+ }
3375
+ },
3376
+ {
3377
+ description: "An SSF transmitter whose SETs the Cloud receives (POST /ssf/receive/:connector, or poll).",
3378
+ type: "object",
3379
+ required: [
3380
+ "kind",
3381
+ "name",
3382
+ "config"
3383
+ ],
3384
+ properties: {
3385
+ id: {
3386
+ type: "string",
3387
+ minLength: 1,
3388
+ maxLength: 256
3389
+ },
3390
+ kind: { const: "caep-transmitter" },
3391
+ name: {
3392
+ type: "string",
3393
+ minLength: 1,
3394
+ maxLength: 200
3395
+ },
3396
+ config: {
3397
+ type: "object",
3398
+ additionalProperties: false,
3399
+ required: ["issuer", "delivery"],
3400
+ properties: {
3401
+ issuer: {
3402
+ type: "string",
3403
+ pattern: "^https://"
3404
+ },
3405
+ jwks_uri: {
3406
+ type: "string",
3407
+ pattern: "^https://"
3408
+ },
3409
+ delivery: { enum: ["push", "poll"] },
3410
+ poll_endpoint: {
3411
+ type: "string",
3412
+ pattern: "^https://"
3413
+ }
3414
+ }
3415
+ }
3416
+ }
3417
+ },
3418
+ {
3419
+ description: "An OIDC upstream the Cloud issuer federates to with authorization code plus PKCE.",
3420
+ type: "object",
3421
+ required: [
3422
+ "kind",
3423
+ "name",
3424
+ "config"
3425
+ ],
3426
+ properties: {
3427
+ id: {
3428
+ type: "string",
3429
+ minLength: 1,
3430
+ maxLength: 256
3431
+ },
3432
+ kind: { const: "federated-upstream" },
3433
+ name: {
3434
+ type: "string",
3435
+ minLength: 1,
3436
+ maxLength: 200
3437
+ },
3438
+ config: {
3439
+ type: "object",
3440
+ additionalProperties: false,
3441
+ required: ["discovery", "client_id"],
3442
+ properties: {
3443
+ discovery: {
3444
+ type: "string",
3445
+ pattern: "^https://"
3446
+ },
3447
+ client_id: {
3448
+ type: "string",
3449
+ minLength: 1,
3450
+ maxLength: 256
3451
+ },
3452
+ scopes: {
3453
+ type: "array",
3454
+ items: {
3455
+ type: "string",
3456
+ minLength: 1
3457
+ },
3458
+ uniqueItems: true
3459
+ }
3460
+ }
3461
+ }
3462
+ }
3463
+ },
3464
+ {
3465
+ description: "Approval cards through the Vercel Chat SDK.",
3466
+ type: "object",
3467
+ required: [
3468
+ "kind",
3469
+ "name",
3470
+ "config"
3471
+ ],
3472
+ properties: {
3473
+ id: {
3474
+ type: "string",
3475
+ minLength: 1,
3476
+ maxLength: 256
3477
+ },
3478
+ kind: { const: "chat" },
3479
+ name: {
3480
+ type: "string",
3481
+ minLength: 1,
3482
+ maxLength: 200
3483
+ },
3484
+ config: {
3485
+ type: "object",
3486
+ additionalProperties: false,
3487
+ required: ["platform", "workspace"],
3488
+ properties: {
3489
+ platform: { enum: ["slack", "teams"] },
3490
+ workspace: {
3491
+ type: "string",
3492
+ minLength: 1,
3493
+ maxLength: 256
3494
+ }
3495
+ }
3496
+ }
3497
+ }
3498
+ },
3499
+ {
3500
+ description: "The Cloud's hosted email sender for approval and delivery messages.",
3501
+ type: "object",
3502
+ required: [
3503
+ "kind",
3504
+ "name",
3505
+ "config"
3506
+ ],
3507
+ properties: {
3508
+ id: {
3509
+ type: "string",
3510
+ minLength: 1,
3511
+ maxLength: 256
3512
+ },
3513
+ kind: { const: "email" },
3514
+ name: {
3515
+ type: "string",
3516
+ minLength: 1,
3517
+ maxLength: 200
3518
+ },
3519
+ config: {
3520
+ type: "object",
3521
+ additionalProperties: false,
3522
+ required: ["from"],
3523
+ properties: { from: {
3524
+ type: "string",
3525
+ pattern: "^[^@\\s]+@[^@\\s]+$"
3526
+ } }
3527
+ }
3528
+ }
3529
+ },
3530
+ {
3531
+ description: "Vercel OIDC federation tokens accepted as hosted ADS callers.",
3532
+ type: "object",
3533
+ required: [
3534
+ "kind",
3535
+ "name",
3536
+ "config"
3537
+ ],
3538
+ properties: {
3539
+ id: {
3540
+ type: "string",
3541
+ minLength: 1,
3542
+ maxLength: 256
3543
+ },
3544
+ kind: { const: "vercel-oidc" },
3545
+ name: {
3546
+ type: "string",
3547
+ minLength: 1,
3548
+ maxLength: 200
3549
+ },
3550
+ config: {
3551
+ type: "object",
3552
+ additionalProperties: false,
3553
+ required: ["team_id"],
3554
+ properties: {
3555
+ team_id: {
3556
+ type: "string",
3557
+ minLength: 1,
3558
+ maxLength: 256
3559
+ },
3560
+ project_id: {
3561
+ type: "string",
3562
+ minLength: 1,
3563
+ maxLength: 256
3564
+ },
3565
+ environment: { enum: [
3566
+ "production",
3567
+ "preview",
3568
+ "development"
3569
+ ] }
3570
+ }
3571
+ }
3572
+ }
3573
+ }
3574
+ ]
3575
+ });
3576
+ //#endregion
3577
+ //#region src/testing/cloud-api/document.ts
3578
+ const ENV = "/v1/environments/{env}";
3579
+ const COMPACT_JWS = String.raw`^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+$`;
3580
+ const APPROVAL_REQUEST = "https://permdock.com/schemas/approval-request-v1.json";
3581
+ const CONNECTOR = "https://permdock.com/schemas/connector-v1.json";
3582
+ const ref = (name) => ({ $ref: `#/components/schemas/${name}` });
3583
+ const problem = (description) => ({
3584
+ description,
3585
+ content: { "application/problem+json": { schema: ref("Problem") } }
3586
+ });
3587
+ const json = (description, schema) => ({
3588
+ description,
3589
+ content: { "application/json": { schema } }
3590
+ });
3591
+ const jwt = (description) => ({
3592
+ description,
3593
+ content: { "application/jwt": { schema: ref("CompactJws") } }
3594
+ });
3595
+ const empty = (description) => ({ description });
3596
+ const body = (schema, type = "application/json") => ({
3597
+ required: true,
3598
+ content: { [type]: { schema } }
3599
+ });
3600
+ const UNAUTHORISED = problem("Missing or unknown key");
3601
+ const FORBIDDEN = problem("A key of another kind");
3602
+ const keyed = (scheme) => (operation) => ({
3603
+ security: [{ [scheme]: [] }],
3604
+ ...operation,
3605
+ responses: {
3606
+ ...operation.responses,
3607
+ "401": UNAUTHORISED,
3608
+ "403": FORBIDDEN
3609
+ }
3610
+ });
3611
+ const client = keyed("client");
3612
+ const admin$1 = keyed("admin");
3613
+ const open = (operation) => ({
3614
+ security: [],
3615
+ ...operation
3616
+ });
3617
+ const header = (name, description) => ({
3618
+ name,
3619
+ in: "header",
3620
+ required: true,
3621
+ description,
3622
+ schema: {
3623
+ type: "string",
3624
+ minLength: 1
3625
+ }
3626
+ });
3627
+ const APPROVAL_TOKEN = header("PermDock-Approval", "The approval token; never in the URL");
3628
+ const ID = { $ref: "#/components/parameters/id" };
3629
+ const items = (schema) => ({
3630
+ type: "object",
3631
+ required: ["items"],
3632
+ properties: {
3633
+ items: {
3634
+ type: "array",
3635
+ items: schema
3636
+ },
3637
+ next: { type: "string" }
3638
+ }
3639
+ });
3640
+ function crud(name, schema, input) {
3641
+ return {
3642
+ [`${ENV}/${name}`]: {
3643
+ get: admin$1({
3644
+ summary: `List ${name}`,
3645
+ responses: { "200": json("The list", items(schema)) }
3646
+ }),
3647
+ post: admin$1({
3648
+ summary: `Create one of ${name}`,
3649
+ requestBody: body(input),
3650
+ responses: {
3651
+ "201": json("Stored", schema),
3652
+ "400": problem("Not a valid body")
3653
+ }
3654
+ })
3655
+ },
3656
+ [`${ENV}/${name}/{id}`]: {
3657
+ parameters: [ID],
3658
+ get: admin$1({
3659
+ summary: `Read one of ${name}`,
3660
+ responses: {
3661
+ "200": json("The item", schema),
3662
+ "404": problem("Unknown id")
3663
+ }
3664
+ }),
3665
+ delete: admin$1({
3666
+ summary: `Remove one of ${name}`,
3667
+ responses: {
3668
+ "204": empty("Removed"),
3669
+ "404": problem("Unknown id")
3670
+ }
3671
+ })
3672
+ }
3673
+ };
3674
+ }
3675
+ const withId = (schema) => ({ allOf: [schema, {
3676
+ type: "object",
3677
+ required: ["id"],
3678
+ properties: { id: { type: "string" } }
3679
+ }] });
3680
+ const SCHEMAS = {
3681
+ Problem: {
3682
+ type: "object",
3683
+ description: "RFC 9457 Problem Details",
3684
+ required: [
3685
+ "type",
3686
+ "title",
3687
+ "status"
3688
+ ],
3689
+ properties: {
3690
+ type: { type: "string" },
3691
+ title: { type: "string" },
3692
+ status: {
3693
+ type: "integer",
3694
+ minimum: 400,
3695
+ maximum: 599
3696
+ },
3697
+ detail: { type: "string" },
3698
+ instance: { type: "string" },
3699
+ code: { type: "string" },
3700
+ reason: { type: "string" }
3701
+ }
3702
+ },
3703
+ CompactJws: {
3704
+ type: "string",
3705
+ pattern: COMPACT_JWS
3706
+ },
3707
+ ApprovalRequest: { $ref: APPROVAL_REQUEST },
3708
+ ApprovalPage: items({ $ref: APPROVAL_REQUEST }),
3709
+ Verdict: {
3710
+ type: "object",
3711
+ required: ["status", "by"],
3712
+ properties: {
3713
+ status: { enum: ["approved", "rejected"] },
3714
+ by: { type: "object" },
3715
+ note: { type: "string" }
3716
+ }
3717
+ },
3718
+ CatalogPush: { oneOf: [{
3719
+ type: "object",
3720
+ required: [
3721
+ "version",
3722
+ "fingerprint",
3723
+ "findings"
3724
+ ],
3725
+ properties: {
3726
+ version: {
3727
+ type: "integer",
3728
+ minimum: 1
3729
+ },
3730
+ fingerprint: {
3731
+ type: "string",
3732
+ minLength: 1
3733
+ },
3734
+ findings: {
3735
+ type: "array",
3736
+ items: { type: "object" }
3737
+ }
3738
+ }
3739
+ }, {
3740
+ type: "object",
3741
+ required: [
3742
+ "version",
3743
+ "fingerprint",
3744
+ "unchanged"
3745
+ ],
3746
+ properties: {
3747
+ version: {
3748
+ type: "integer",
3749
+ minimum: 1
3750
+ },
3751
+ fingerprint: {
3752
+ type: "string",
3753
+ minLength: 1
3754
+ },
3755
+ unchanged: { const: true }
3756
+ }
3757
+ }] },
3758
+ HostedGrant: {
3759
+ type: "object",
3760
+ required: ["permission", "to"],
3761
+ properties: {
3762
+ id: { type: "string" },
3763
+ permission: {
3764
+ type: "string",
3765
+ minLength: 1
3766
+ },
3767
+ to: { type: "object" }
3768
+ }
3769
+ },
3770
+ Connector: { $ref: CONNECTOR },
3771
+ TrustedIssuer: {
3772
+ type: "object",
3773
+ required: [
3774
+ "issuer",
3775
+ "audiences",
3776
+ "purposes"
3777
+ ],
3778
+ properties: {
3779
+ id: { type: "string" },
3780
+ issuer: {
3781
+ type: "string",
3782
+ pattern: "^https://"
3783
+ },
3784
+ jwks_uri: {
3785
+ type: "string",
3786
+ pattern: "^https://"
3787
+ },
3788
+ discovery: {
3789
+ type: "string",
3790
+ pattern: "^https://"
3791
+ },
3792
+ audiences: {
3793
+ type: "array",
3794
+ minItems: 1,
3795
+ items: { type: "string" }
3796
+ },
3797
+ purposes: {
3798
+ type: "array",
3799
+ minItems: 1,
3800
+ uniqueItems: true,
3801
+ items: { enum: [
3802
+ "approver",
3803
+ "subject",
3804
+ "token-exchange"
3805
+ ] }
3806
+ }
3807
+ },
3808
+ oneOf: [{
3809
+ required: ["jwks_uri"],
3810
+ properties: { jwks_uri: true }
3811
+ }, {
3812
+ required: ["discovery"],
3813
+ properties: { discovery: true }
3814
+ }]
3815
+ },
3816
+ TrustedPep: {
3817
+ type: "object",
3818
+ properties: {
3819
+ id: { type: "string" },
3820
+ vercel: {
3821
+ type: "object",
3822
+ required: ["team_id"],
3823
+ additionalProperties: false,
3824
+ properties: {
3825
+ team_id: {
3826
+ type: "string",
3827
+ minLength: 1
3828
+ },
3829
+ project_id: {
3830
+ type: "string",
3831
+ minLength: 1
3832
+ },
3833
+ environment: { enum: [
3834
+ "production",
3835
+ "preview",
3836
+ "development"
3837
+ ] }
3838
+ }
3839
+ },
3840
+ client_id: {
3841
+ type: "string",
3842
+ minLength: 1
3843
+ }
3844
+ },
3845
+ oneOf: [{
3846
+ required: ["vercel"],
3847
+ properties: { vercel: true }
3848
+ }, {
3849
+ required: ["client_id"],
3850
+ properties: { client_id: true }
3851
+ }]
3852
+ },
3853
+ ApproverGroup: {
3854
+ type: "object",
3855
+ required: ["name", "members"],
3856
+ properties: {
3857
+ id: { type: "string" },
3858
+ name: {
3859
+ type: "string",
3860
+ minLength: 1
3861
+ },
3862
+ members: {
3863
+ type: "array",
3864
+ items: {
3865
+ type: "string",
3866
+ minLength: 1
3867
+ }
3868
+ }
3869
+ }
3870
+ },
3871
+ Assignment: {
3872
+ type: "object",
3873
+ required: ["tenant", "role"],
3874
+ properties: {
3875
+ id: { type: "string" },
3876
+ principal: { type: "string" },
3877
+ group: { type: "string" },
3878
+ tenant: { type: "string" },
3879
+ team: { type: "string" },
3880
+ role: {
3881
+ type: "string",
3882
+ minLength: 1
3883
+ }
3884
+ },
3885
+ oneOf: [{
3886
+ required: ["principal"],
3887
+ properties: { principal: true }
3888
+ }, {
3889
+ required: ["group"],
3890
+ properties: { group: true }
3891
+ }]
3892
+ },
3893
+ Created: {
3894
+ type: "object",
3895
+ required: ["id"],
3896
+ properties: { id: {
3897
+ type: "string",
3898
+ minLength: 1
3899
+ } }
3900
+ },
3901
+ Jwks: {
3902
+ type: "object",
3903
+ required: ["keys"],
3904
+ properties: { keys: {
3905
+ type: "array",
3906
+ items: { type: "object" }
3907
+ } }
3908
+ },
3909
+ Metadata: {
3910
+ type: "object",
3911
+ additionalProperties: true
3912
+ },
3913
+ TokenResponse: {
3914
+ type: "object",
3915
+ required: [
3916
+ "access_token",
3917
+ "token_type",
3918
+ "expires_in"
3919
+ ],
3920
+ properties: {
3921
+ access_token: { type: "string" },
3922
+ issued_token_type: { type: "string" },
3923
+ token_type: { const: "Bearer" },
3924
+ expires_in: {
3925
+ type: "integer",
3926
+ minimum: 1
3927
+ },
3928
+ refresh_token: { type: "string" },
3929
+ scope: { type: "string" }
3930
+ }
3931
+ },
3932
+ OAuthError: {
3933
+ type: "object",
3934
+ required: ["error"],
3935
+ properties: {
3936
+ error: { type: "string" },
3937
+ error_description: { type: "string" }
3938
+ }
3939
+ },
3940
+ SsfStream: {
3941
+ type: "object",
3942
+ required: ["stream_id"],
3943
+ properties: { stream_id: { type: "string" } }
3944
+ },
3945
+ SetPoll: {
3946
+ type: "object",
3947
+ required: ["sets"],
3948
+ properties: {
3949
+ sets: {
3950
+ type: "object",
3951
+ additionalProperties: ref("CompactJws")
3952
+ },
3953
+ moreAvailable: { type: "boolean" }
3954
+ }
3955
+ }
3956
+ };
3957
+ const APPROVALS = {
3958
+ [`${ENV}/approvals`]: {
3959
+ post: client({
3960
+ summary: "approvals.create",
3961
+ requestBody: body(ref("ApprovalRequest")),
3962
+ responses: {
3963
+ "200": json("The record already stored for this token", ref("ApprovalRequest")),
3964
+ "201": json("Stored", ref("ApprovalRequest")),
3965
+ "400": problem("Not a v1 approval request")
3966
+ }
3967
+ }),
3968
+ get: client({
3969
+ summary: "approvals.list",
3970
+ parameters: [...[
3971
+ "status",
3972
+ "principalId",
3973
+ "actorId",
3974
+ "tenant",
3975
+ "session",
3976
+ "cursor"
3977
+ ].map((name) => ({
3978
+ name,
3979
+ in: "query",
3980
+ schema: { type: "string" }
3981
+ })), {
3982
+ name: "limit",
3983
+ in: "query",
3984
+ schema: {
3985
+ type: "integer",
3986
+ minimum: 1
3987
+ }
3988
+ }],
3989
+ responses: { "200": json("One page", ref("ApprovalPage")) }
3990
+ })
3991
+ },
3992
+ [`${ENV}/approvals/by-token`]: { get: client({
3993
+ summary: "approvals.get",
3994
+ parameters: [APPROVAL_TOKEN],
3995
+ responses: {
3996
+ "200": json("The record", ref("ApprovalRequest")),
3997
+ "404": problem("Unknown token")
3998
+ }
3999
+ }) },
4000
+ [`${ENV}/approvals/{id}`]: {
4001
+ parameters: [ID],
4002
+ get: client({
4003
+ summary: "approvals.getById",
4004
+ responses: {
4005
+ "200": json("The record", ref("ApprovalRequest")),
4006
+ "404": problem("Unknown id")
4007
+ }
4008
+ })
4009
+ },
4010
+ [`${ENV}/approvals/by-token/resolve`]: { post: client({
4011
+ summary: "approvals.resolve",
4012
+ parameters: [
4013
+ APPROVAL_TOKEN,
4014
+ {
4015
+ ...header("PermDock-Approver", "The approver at+jwt; absent only for a system rejection"),
4016
+ required: false
4017
+ },
4018
+ {
4019
+ name: "If-Match",
4020
+ in: "header",
4021
+ schema: { type: "string" }
4022
+ }
4023
+ ],
4024
+ requestBody: body(ref("Verdict")),
4025
+ responses: {
4026
+ "200": json("The resolved record", ref("ApprovalRequest")),
4027
+ "404": problem("approval-not-found"),
4028
+ "409": problem("Not pending, or a stale ETag"),
4029
+ "410": problem("Expired")
4030
+ }
4031
+ }) },
4032
+ [`${ENV}/approvals/by-token/consume`]: { post: client({
4033
+ summary: "approvals.consume",
4034
+ parameters: [APPROVAL_TOKEN],
4035
+ responses: {
4036
+ "200": json("The approved record with consumedAt", ref("ApprovalRequest")),
4037
+ "409": problem("Not approved, already consumed or expired")
4038
+ }
4039
+ }) },
4040
+ [`${ENV}/approvals/expire`]: { post: client({
4041
+ summary: "approvals.expire",
4042
+ responses: { "200": json("Requests expired", {
4043
+ type: "object",
4044
+ required: ["expired"],
4045
+ properties: { expired: {
4046
+ type: "integer",
4047
+ minimum: 0
4048
+ } }
4049
+ }) }
4050
+ }) },
4051
+ [`${ENV}/approvals/cancel`]: { post: client({
4052
+ summary: "approvals.cancel",
4053
+ requestBody: body({
4054
+ type: "object",
4055
+ required: ["filter", "by"],
4056
+ properties: {
4057
+ filter: { type: "object" },
4058
+ by: {
4059
+ type: "string",
4060
+ minLength: 1
4061
+ },
4062
+ note: { type: "string" }
4063
+ }
4064
+ }),
4065
+ responses: { "200": json("Requests cancelled", {
4066
+ type: "object",
4067
+ required: ["cancelled"],
4068
+ properties: { cancelled: {
4069
+ type: "integer",
4070
+ minimum: 0
4071
+ } }
4072
+ }) }
4073
+ }) }
4074
+ };
4075
+ const RUNTIME = {
4076
+ [`${ENV}/decisions`]: { post: client({
4077
+ summary: "sink.write and sink.flush",
4078
+ requestBody: body({
4079
+ type: "array",
4080
+ items: { type: "object" }
4081
+ }, "application/cloudevents-batch+json"),
4082
+ responses: {
4083
+ "202": empty("Accepted"),
4084
+ "400": problem("Not a CloudEvents batch")
4085
+ }
4086
+ }) },
4087
+ [`${ENV}/snapshot`]: { get: client({
4088
+ summary: "snapshotsFor(token).get",
4089
+ parameters: [header("PermDock-Subject", "An at+jwt naming the subject, exp at most five minutes after iat")],
4090
+ responses: { "200": jwt("A vnd.permdock.snapshot+jwt") }
4091
+ }) },
4092
+ [`${ENV}/catalog`]: {
4093
+ post: {
4094
+ ...client({
4095
+ summary: "catalog.push and permdock cloud push",
4096
+ requestBody: body({
4097
+ type: "object",
4098
+ required: ["fingerprint", "catalog"],
4099
+ properties: {
4100
+ fingerprint: {
4101
+ type: "string",
4102
+ minLength: 1
4103
+ },
4104
+ catalog: { type: "object" },
4105
+ policy: { $ref: PUSHED_POLICY },
4106
+ manifest: { $ref: "https://permdock.com/schemas/supabase-manifest-v1.json" },
4107
+ permdockVersion: {
4108
+ type: "string",
4109
+ description: "The permdock version that pushed"
4110
+ }
4111
+ }
4112
+ }),
4113
+ responses: {
4114
+ "200": json("The fingerprint is already published", ref("CatalogPush")),
4115
+ "201": json("A new version and its drift findings", ref("CatalogPush")),
4116
+ "400": problem("Not a catalog, or a fingerprint that does not match it")
4117
+ }
4118
+ }),
4119
+ security: [{ client: [] }, { platform: ["catalog:write"] }]
4120
+ },
4121
+ get: client({
4122
+ summary: "The latest catalog",
4123
+ responses: {
4124
+ "200": json("The catalog", { type: "object" }),
4125
+ "404": problem("Nothing pushed yet")
4126
+ }
4127
+ })
4128
+ },
4129
+ [`${ENV}/catalog/versions`]: { get: client({
4130
+ summary: "Catalog versions",
4131
+ responses: { "200": json("Versions, newest first", items({
4132
+ type: "object",
4133
+ required: ["version", "fingerprint"],
4134
+ properties: {
4135
+ version: {
4136
+ type: "integer",
4137
+ minimum: 1
4138
+ },
4139
+ fingerprint: { type: "string" }
4140
+ }
4141
+ })) }
4142
+ }) },
4143
+ [`${ENV}/deliveries`]: { post: client({
4144
+ summary: "cloudDeliveryTransport",
4145
+ parameters: [header("Idempotency-Key", "The router's per-recipient key")],
4146
+ requestBody: body({
4147
+ type: "object",
4148
+ required: [
4149
+ "channel",
4150
+ "delivery",
4151
+ "target",
4152
+ "message"
4153
+ ],
4154
+ properties: {
4155
+ channel: { type: "string" },
4156
+ delivery: { type: "string" },
4157
+ target: {
4158
+ type: "object",
4159
+ required: ["address", "permission"]
4160
+ },
4161
+ message: { type: "object" }
4162
+ }
4163
+ }),
4164
+ responses: {
4165
+ "202": json("Accepted", ref("Created")),
4166
+ "400": problem("invalid-message"),
4167
+ "409": problem("A delivery with this key is in flight"),
4168
+ "422": problem("invalid-message"),
4169
+ "429": problem("rate-limited")
4170
+ }
4171
+ }) },
4172
+ [`${ENV}/sign`]: { post: client({
4173
+ summary: "cloudSigner",
4174
+ requestBody: body({
4175
+ type: "object",
4176
+ required: [
4177
+ "typ",
4178
+ "claims",
4179
+ "aud"
4180
+ ],
4181
+ properties: {
4182
+ typ: { type: "string" },
4183
+ claims: { type: "object" },
4184
+ aud: { anyOf: [{ type: "string" }, {
4185
+ type: "array",
4186
+ items: { type: "string" }
4187
+ }] },
4188
+ exp: { type: "integer" },
4189
+ iat: { type: "integer" },
4190
+ jti: { type: "string" }
4191
+ }
4192
+ }),
4193
+ responses: {
4194
+ "200": jwt("The compact JWS of the requested typ"),
4195
+ "400": problem("Missing audience or claims"),
4196
+ "422": problem("A typ the client key may not sign")
4197
+ }
4198
+ }) },
4199
+ [`${ENV}/policy`]: { get: client({
4200
+ summary: "policies.refresh",
4201
+ responses: {
4202
+ "200": jwt("A vnd.permdock.policy+jwt"),
4203
+ "404": problem("No hosted grants")
4204
+ }
4205
+ }) }
4206
+ };
4207
+ const AUTHORING = {
4208
+ [`${ENV}/hosted-grants`]: {
4209
+ get: admin$1({
4210
+ summary: "List hosted grants",
4211
+ responses: { "200": json("The grants", items(ref("HostedGrant"))) }
4212
+ }),
4213
+ post: admin$1({
4214
+ summary: "Author a hosted grant",
4215
+ requestBody: body(ref("HostedGrant")),
4216
+ responses: {
4217
+ "201": json("The stored grant", withId(ref("HostedGrant"))),
4218
+ "422": problem("A grant mergeHostedGrants would drop; reason names why")
4219
+ }
4220
+ })
4221
+ },
4222
+ [`${ENV}/hosted-grants/{id}`]: {
4223
+ parameters: [ID],
4224
+ delete: admin$1({
4225
+ summary: "Remove a hosted grant",
4226
+ responses: {
4227
+ "204": empty("Removed"),
4228
+ "404": problem("Unknown id")
4229
+ }
4230
+ })
4231
+ },
4232
+ [`${ENV}/directory/principals`]: {
4233
+ get: admin$1({
4234
+ summary: "List principals",
4235
+ responses: { "200": json("The list", items({ type: "object" })) }
4236
+ }),
4237
+ post: admin$1({
4238
+ summary: "Create a principal",
4239
+ requestBody: body({
4240
+ type: "object",
4241
+ required: ["subject"]
4242
+ }),
4243
+ responses: { "201": json("Created", ref("Created")) }
4244
+ })
4245
+ },
4246
+ [`${ENV}/directory/groups`]: {
4247
+ get: admin$1({
4248
+ summary: "List groups",
4249
+ responses: { "200": json("The list", items({ type: "object" })) }
4250
+ }),
4251
+ post: admin$1({
4252
+ summary: "Create a group",
4253
+ requestBody: body({
4254
+ type: "object",
4255
+ required: ["name"]
4256
+ }),
4257
+ responses: { "201": json("Created", ref("Created")) }
4258
+ })
4259
+ },
4260
+ ...crud("directory/assignments", ref("Assignment"), ref("Assignment")),
4261
+ ...crud("connectors", withId(ref("Connector")), ref("Connector")),
4262
+ ...crud("trusted-issuers", withId(ref("TrustedIssuer")), ref("TrustedIssuer")),
4263
+ ...crud("trusted-peps", withId(ref("TrustedPep")), ref("TrustedPep")),
4264
+ ...crud("approver-groups", withId(ref("ApproverGroup")), ref("ApproverGroup")),
4265
+ [`${ENV}/snapshots/invalidate`]: { post: admin$1({
4266
+ summary: "Invalidate edge snapshots and send CAEP session-revoked",
4267
+ requestBody: body({
4268
+ type: "object",
4269
+ required: ["reason"],
4270
+ properties: {
4271
+ principal: { type: "string" },
4272
+ session: { type: "string" },
4273
+ tenant: { type: "string" },
4274
+ reason: {
4275
+ type: "string",
4276
+ minLength: 1
4277
+ }
4278
+ }
4279
+ }),
4280
+ responses: { "202": empty("Accepted") }
4281
+ }) },
4282
+ [`${ENV}/export`]: { get: keyed("export")({
4283
+ summary: "The decision log",
4284
+ parameters: [{
4285
+ name: "format",
4286
+ in: "query",
4287
+ required: true,
4288
+ schema: { enum: [
4289
+ "jws",
4290
+ "ocsf",
4291
+ "csv",
4292
+ "otlp"
4293
+ ] }
4294
+ }],
4295
+ responses: { "200": {
4296
+ description: "The export",
4297
+ content: {
4298
+ "application/jwt": { schema: ref("CompactJws") },
4299
+ "application/json": { schema: { type: "array" } },
4300
+ "text/csv": { schema: { type: "string" } }
4301
+ }
4302
+ } }
4303
+ }) }
4304
+ };
4305
+ const OAUTH_FORM = "application/x-www-form-urlencoded";
4306
+ const IDENTITY = {
4307
+ [`${ENV}/oauth/authorize`]: { get: open({
4308
+ summary: "Authorization code with PKCE; S256 only",
4309
+ parameters: [
4310
+ ...[
4311
+ "response_type",
4312
+ "client_id",
4313
+ "redirect_uri",
4314
+ "code_challenge"
4315
+ ].map((name) => ({
4316
+ name,
4317
+ in: "query",
4318
+ required: true,
4319
+ schema: { type: "string" }
4320
+ })),
4321
+ {
4322
+ name: "code_challenge_method",
4323
+ in: "query",
4324
+ required: true,
4325
+ schema: { const: "S256" }
4326
+ },
4327
+ {
4328
+ name: "state",
4329
+ in: "query",
4330
+ schema: { type: "string" }
4331
+ },
4332
+ {
4333
+ name: "scope",
4334
+ in: "query",
4335
+ schema: { type: "string" }
4336
+ }
4337
+ ],
4338
+ responses: {
4339
+ "302": empty("Redirect to the client with a code"),
4340
+ "400": json("invalid_request, including plain PKCE", ref("OAuthError"))
4341
+ }
4342
+ }) },
4343
+ [`${ENV}/oauth/device_authorization`]: { post: {
4344
+ security: [{ oauthClient: [] }, { publicClient: [] }],
4345
+ summary: "RFC 8628 device authorization",
4346
+ requestBody: body({
4347
+ type: "object",
4348
+ required: ["client_id"]
4349
+ }, OAUTH_FORM),
4350
+ responses: {
4351
+ "200": json("The codes", {
4352
+ type: "object",
4353
+ required: [
4354
+ "device_code",
4355
+ "user_code",
4356
+ "verification_uri",
4357
+ "expires_in"
4358
+ ],
4359
+ properties: {
4360
+ device_code: { type: "string" },
4361
+ user_code: { type: "string" },
4362
+ verification_uri: { type: "string" },
4363
+ verification_uri_complete: { type: "string" },
4364
+ expires_in: { type: "integer" },
4365
+ interval: { type: "integer" }
4366
+ }
4367
+ }),
4368
+ "400": json("invalid_request or invalid_client", ref("OAuthError"))
4369
+ }
4370
+ } },
4371
+ [`${ENV}/oauth/token`]: { post: {
4372
+ security: [{ oauthClient: [] }, { publicClient: [] }],
4373
+ summary: "authorization_code, device_code, client_credentials and token exchange (confidential only), refresh_token",
4374
+ requestBody: body({
4375
+ type: "object",
4376
+ required: ["grant_type"],
4377
+ properties: { grant_type: { enum: [
4378
+ "authorization_code",
4379
+ "urn:ietf:params:oauth:grant-type:device_code",
4380
+ "client_credentials",
4381
+ "urn:ietf:params:oauth:grant-type:token-exchange",
4382
+ "refresh_token"
4383
+ ] } }
4384
+ }, OAUTH_FORM),
4385
+ responses: {
4386
+ "200": json("The tokens", ref("TokenResponse")),
4387
+ "400": json("An RFC 6749 or RFC 8628 error", ref("OAuthError")),
4388
+ "401": json("invalid_client", ref("OAuthError"))
4389
+ }
4390
+ } },
4391
+ [`${ENV}/.well-known/openid-configuration`]: { get: open({
4392
+ summary: "Issuer metadata",
4393
+ responses: { "200": json("The metadata", ref("Metadata")) }
4394
+ }) },
4395
+ [`${ENV}/.well-known/jwks.json`]: { get: open({
4396
+ summary: "The environment's JWK Set",
4397
+ responses: { "200": json("The JWK Set", ref("Jwks")) }
4398
+ }) }
4399
+ };
4400
+ const ads = (summary) => ({ post: {
4401
+ security: [{ adsCaller: [] }],
4402
+ summary,
4403
+ requestBody: body({ type: "object" }),
4404
+ responses: {
4405
+ "200": json("The AuthZEN response", { type: "object" }),
4406
+ "400": problem("Not an AuthZEN request"),
4407
+ "401": problem("No verified caller")
4408
+ }
4409
+ } });
4410
+ const ADS = {
4411
+ [`${ENV}/access/v1/evaluation`]: ads("AuthZEN evaluation"),
4412
+ [`${ENV}/access/v1/evaluations`]: ads("AuthZEN evaluations"),
4413
+ [`${ENV}/access/v1/search/subject`]: ads("AuthZEN subject search"),
4414
+ [`${ENV}/access/v1/search/resource`]: ads("AuthZEN resource search"),
4415
+ [`${ENV}/access/v1/search/action`]: ads("AuthZEN action search"),
4416
+ [`${ENV}/.well-known/authzen-configuration`]: { get: open({
4417
+ summary: "AuthZEN metadata",
4418
+ responses: { "200": json("The metadata", ref("Metadata")) }
4419
+ }) }
4420
+ };
4421
+ const ssf = (operation) => ({
4422
+ security: [{ ssfReceiver: [] }],
4423
+ ...operation
4424
+ });
4425
+ const STREAM_ID = {
4426
+ name: "stream_id",
4427
+ in: "query",
4428
+ schema: { type: "string" }
4429
+ };
4430
+ const SSF = {
4431
+ [`${ENV}/ssf/receive/{connector}`]: {
4432
+ parameters: [{
4433
+ name: "connector",
4434
+ in: "path",
4435
+ required: true,
4436
+ schema: { type: "string" }
4437
+ }],
4438
+ post: {
4439
+ security: [{ connectorStream: [] }],
4440
+ summary: "RFC 8935 push into the Cloud's SSF receiver",
4441
+ requestBody: body(ref("CompactJws"), "application/secevent+jwt"),
4442
+ responses: {
4443
+ "202": empty("Accepted"),
4444
+ "400": json("An RFC 8935 error", {
4445
+ type: "object",
4446
+ required: ["err"]
4447
+ }),
4448
+ "401": empty("Unknown stream credential")
4449
+ }
4450
+ }
4451
+ },
4452
+ [`${ENV}/.well-known/ssf-configuration`]: { get: open({
4453
+ summary: "SSF transmitter metadata",
4454
+ responses: { "200": json("The metadata", ref("Metadata")) }
4455
+ }) },
4456
+ [`${ENV}/ssf/streams`]: {
4457
+ get: ssf({
4458
+ summary: "Read streams",
4459
+ parameters: [STREAM_ID],
4460
+ responses: { "200": json("The stream", ref("SsfStream")) }
4461
+ }),
4462
+ post: ssf({
4463
+ summary: "Create a stream",
4464
+ requestBody: body({ type: "object" }),
4465
+ responses: { "201": json("The stream", ref("SsfStream")) }
4466
+ }),
4467
+ patch: ssf({
4468
+ summary: "Update a stream",
4469
+ requestBody: body(ref("SsfStream")),
4470
+ responses: { "200": json("The stream", ref("SsfStream")) }
4471
+ }),
4472
+ delete: ssf({
4473
+ summary: "Delete a stream",
4474
+ parameters: [STREAM_ID],
4475
+ responses: { "204": empty("Deleted") }
4476
+ })
4477
+ },
4478
+ [`${ENV}/ssf/status`]: {
4479
+ get: ssf({
4480
+ summary: "A stream's status",
4481
+ parameters: [STREAM_ID],
4482
+ responses: { "200": json("The status", {
4483
+ type: "object",
4484
+ required: ["status"]
4485
+ }) }
4486
+ }),
4487
+ post: ssf({
4488
+ summary: "Set a stream's status",
4489
+ requestBody: body({
4490
+ type: "object",
4491
+ required: ["stream_id", "status"]
4492
+ }),
4493
+ responses: { "200": json("The status", {
4494
+ type: "object",
4495
+ required: ["status"]
4496
+ }) }
4497
+ })
4498
+ },
4499
+ [`${ENV}/ssf/verify`]: { post: ssf({
4500
+ summary: "Send a verification event",
4501
+ requestBody: body({
4502
+ type: "object",
4503
+ required: ["stream_id"]
4504
+ }),
4505
+ responses: { "204": empty("Queued") }
4506
+ }) },
4507
+ [`${ENV}/ssf/poll`]: { post: ssf({
4508
+ summary: "RFC 8936 poll",
4509
+ requestBody: body({ type: "object" }),
4510
+ responses: {
4511
+ "200": json("Pending SETs", ref("SetPoll")),
4512
+ "400": json("invalid_request", {
4513
+ type: "object",
4514
+ required: ["err"]
4515
+ })
4516
+ }
4517
+ }) }
4518
+ };
4519
+ const scim = (summary) => ({
4520
+ security: [{ scimBearer: [] }],
4521
+ summary,
4522
+ responses: {
4523
+ "200": {
4524
+ description: "The SCIM response",
4525
+ content: { "application/scim+json": { schema: { type: "object" } } }
4526
+ },
4527
+ "401": empty("Not the connection's bearer")
4528
+ }
4529
+ });
4530
+ const SCIM = { "/scim/v2/{connection}/{resource}": {
4531
+ parameters: [{
4532
+ name: "connection",
4533
+ in: "path",
4534
+ required: true,
4535
+ schema: { type: "string" }
4536
+ }, {
4537
+ name: "resource",
4538
+ in: "path",
4539
+ required: true,
4540
+ description: "The SCIM path below the connection",
4541
+ schema: { type: "string" }
4542
+ }],
4543
+ get: scim("SCIM read"),
4544
+ post: scim("SCIM create"),
4545
+ put: scim("SCIM replace"),
4546
+ patch: scim("SCIM patch"),
4547
+ delete: scim("SCIM delete")
4548
+ } };
4549
+ function withEnv(paths) {
4550
+ return Object.fromEntries(Object.entries(paths).map(([path, item]) => {
4551
+ if (!path.startsWith(ENV)) return [path, item];
4552
+ const parameters = Array.isArray(item["parameters"]) ? item["parameters"] : [];
4553
+ return [path, {
4554
+ ...item,
4555
+ parameters: [{ $ref: "#/components/parameters/env" }, ...parameters]
4556
+ }];
4557
+ }));
4558
+ }
4559
+ const bearer = (description) => ({
4560
+ type: "http",
4561
+ scheme: "bearer",
4562
+ description
4563
+ });
4564
+ /**
4565
+ * `schemas/cloud-api-v1.json`: the PermDock Cloud HTTP API as an OpenAPI 3.1
4566
+ * document. The route tables in `adapters/cloud.mdx` and `docs:drift` keep it whole.
4567
+ */
4568
+ const CLOUD_API_DOCUMENT = freezeDeep({
4569
+ openapi: "3.1.1",
4570
+ info: {
4571
+ title: "PermDock Cloud API",
4572
+ version: "1.0.0",
4573
+ description: "The platform issuer and project read API the permdock CLI calls, the routes permdock/cloud calls, and the authoring, identity, ADS, SSF and SCIM routes a Cloud implementation serves. Errors are RFC 9457 Problem Details unless the route's own standard says otherwise."
4574
+ },
4575
+ jsonSchemaDialect: "https://spec.openapis.org/oas/3.1/dialect/base",
4576
+ servers: [{ url: "https://permdock.com/api" }],
4577
+ paths: withEnv({
4578
+ ...PLATFORM_PATHS,
4579
+ ...APPROVALS,
4580
+ ...RUNTIME,
4581
+ ...AUTHORING,
4582
+ ...IDENTITY,
4583
+ ...ADS,
4584
+ ...SSF,
4585
+ ...SCIM
4586
+ }),
4587
+ components: {
4588
+ parameters: {
4589
+ env: {
4590
+ name: "env",
4591
+ in: "path",
4592
+ required: true,
4593
+ schema: {
4594
+ type: "string",
4595
+ minLength: 1
4596
+ }
4597
+ },
4598
+ id: {
4599
+ name: "id",
4600
+ in: "path",
4601
+ required: true,
4602
+ schema: {
4603
+ type: "string",
4604
+ minLength: 1
4605
+ }
4606
+ }
4607
+ },
4608
+ schemas: {
4609
+ ...SCHEMAS,
4610
+ ...PLATFORM_SCHEMAS
4611
+ },
4612
+ securitySchemes: {
4613
+ client: bearer("The client key (PERMDOCK_CLOUD_KEY)"),
4614
+ admin: bearer("The admin key (PERMDOCK_CLOUD_ADMIN_KEY)"),
4615
+ export: bearer("An export key"),
4616
+ oauthClient: {
4617
+ type: "http",
4618
+ scheme: "basic",
4619
+ description: "A confidential OAuth client, client_secret_basic"
4620
+ },
4621
+ publicClient: {
4622
+ type: "apiKey",
4623
+ in: "query",
4624
+ name: "client_id",
4625
+ description: "A public OAuth client; token exchange refuses it"
4626
+ },
4627
+ adsCaller: bearer("A Vercel OIDC or client-credentials token for the environment"),
4628
+ ssfReceiver: bearer("A receiver's token for its stream"),
4629
+ connectorStream: bearer("The caep-transmitter connector's stream credential"),
4630
+ scimBearer: bearer("The SCIM connection's bearer"),
4631
+ ...PLATFORM_SECURITY_SCHEMES
4632
+ }
4633
+ }
4634
+ });
4635
+ //#endregion
4636
+ //#region src/testing/cloud-api/validate.ts
4637
+ const DOCUMENT_ID = "https://permdock.com/schemas/cloud-api-v1.json";
4638
+ const METHODS = [
4639
+ "get",
4640
+ "post",
4641
+ "put",
4642
+ "patch",
4643
+ "delete"
4644
+ ];
4645
+ function patternOf(template) {
4646
+ const source = template.split(/(\{[^}]+\})/u).map((part) => {
4647
+ if (part === "{resource}") return ".+";
4648
+ if (part.startsWith("{")) return "[^/]+";
4649
+ return part.replaceAll(/[.*+?^${}()|[\]\\]/gu, String.raw`\$&`);
4650
+ }).join("");
4651
+ return new RegExp(`^${source}$`, "u");
4652
+ }
4653
+ function routesOf(paths) {
4654
+ const routes = [];
4655
+ for (const [template, item] of Object.entries(paths)) for (const method of METHODS) {
4656
+ const responses = item[method]?.responses;
4657
+ if (isRecord(responses)) routes.push({
4658
+ method: method.toUpperCase(),
4659
+ template,
4660
+ pattern: patternOf(template),
4661
+ responses
4662
+ });
4663
+ }
4664
+ return routes.toSorted((a, b) => a.template.split("{").length - b.template.split("{").length);
4665
+ }
4666
+ function pointer(parts) {
4667
+ return parts.map((part) => part.replaceAll("~", "~0").replaceAll("/", "~1")).join("/");
4668
+ }
4669
+ /** A checker for responses of the PermDock Cloud API, compiled once from `CLOUD_API_DOCUMENT`. */
4670
+ function cloudApiValidator() {
4671
+ const ajv = new import__2020.Ajv2020({
4672
+ strict: false,
4673
+ allErrors: false,
4674
+ validateFormats: false
4675
+ });
4676
+ ajv.addSchema(APPROVAL_REQUEST_SCHEMA);
4677
+ ajv.addSchema(CONNECTOR_SCHEMA);
4678
+ ajv.addSchema(catalogSchema);
4679
+ ajv.addSchema(PUSHED_POLICY_SCHEMA);
4680
+ ajv.addSchema(supabaseManifestSchema);
4681
+ ajv.addSchema({
4682
+ ...CLOUD_API_DOCUMENT,
4683
+ $id: DOCUMENT_ID
4684
+ });
4685
+ const routes = routesOf(CLOUD_API_DOCUMENT["paths"]);
4686
+ const validators = /* @__PURE__ */ new Map();
4687
+ const validatorFor = (parts) => {
4688
+ const key = pointer(parts);
4689
+ if (!validators.has(key)) validators.set(key, ajv.getSchema(`${DOCUMENT_ID}#/${key}`));
4690
+ return validators.get(key);
4691
+ };
4692
+ return async (method, path, response) => {
4693
+ const name = `${method} ${path} ${String(response.status)}`;
4694
+ const route = routes.find((item) => item.method === method && item.pattern.test(path));
4695
+ if (route === void 0) return [`${method} ${path} is not in cloud-api-v1.json`];
4696
+ const spec = route.responses[String(response.status)];
4697
+ if (!isRecord(spec)) return [`${name}: cloud-api-v1.json documents no ${String(response.status)} for ${route.template}`];
4698
+ const content = spec["content"];
4699
+ if (!isRecord(content)) return [];
4700
+ const [type = ""] = (response.headers.get("content-type") ?? "").toLowerCase().split(";").map((part) => part.trim());
4701
+ if (!(type in content)) return [`${name}: content type "${type}", documented ${Object.keys(content).join(", ")}`];
4702
+ const text = await response.text();
4703
+ let body = text;
4704
+ if (type.endsWith("json")) try {
4705
+ body = JSON.parse(text);
4706
+ } catch {
4707
+ return [`${name}: the ${type} body is not JSON`];
4708
+ }
4709
+ const validate = validatorFor([
4710
+ "paths",
4711
+ route.template,
4712
+ method.toLowerCase(),
4713
+ "responses",
4714
+ String(response.status),
4715
+ "content",
4716
+ type,
4717
+ "schema"
4718
+ ]);
4719
+ if (validate === void 0 || validate(body)) return [];
4720
+ return [`${name}: ${ajv.errorsText(validate.errors)}`];
4721
+ };
4722
+ }
4723
+ //#endregion
4724
+ //#region src/testing/cloud-api/index.ts
4725
+ const REFUSED_TYPES = [
4726
+ "vnd.permdock.policy+jwt",
4727
+ "vnd.permdock.snapshot+jwt",
4728
+ "vnd.permdock.evidence+jwt",
4729
+ "at+jwt"
4730
+ ];
4731
+ const admin = {
4732
+ principal: {
4733
+ id: "u_9",
4734
+ roles: ["admin"]
4735
+ },
4736
+ context: {}
4737
+ };
4738
+ const member = {
4739
+ principal: {
4740
+ id: "u_2",
4741
+ roles: ["member"]
4742
+ },
4743
+ context: {}
4744
+ };
4745
+ function sample(token, overrides = {}) {
4746
+ return {
4747
+ v: 1,
4748
+ id: `apr_${token}`,
4749
+ token,
4750
+ permission: "post.delete",
4751
+ scope: "post:delete",
4752
+ resource: {
4753
+ type: "post",
4754
+ id: "42"
4755
+ },
4756
+ subject: { principal: {
4757
+ id: "u_1",
4758
+ roles: ["member"]
4759
+ } },
4760
+ detail: "post.delete requires human approval.",
4761
+ createdAt: isoAt(nowMillis()),
4762
+ expiresAt: isoAt(nowMillis() + 36e5),
4763
+ status: "pending",
4764
+ ...overrides
4765
+ };
4766
+ }
4767
+ const METADATA = "/.well-known/oauth-authorization-server";
4768
+ function trimSlash(value) {
4769
+ return value.endsWith("/") ? value.slice(0, -1) : value;
4770
+ }
4771
+ async function problemCode(response) {
4772
+ const body = await response.json();
4773
+ return isRecord(body) ? body["code"] : void 0;
4774
+ }
4775
+ /**
4776
+ * Runs the conformance runners and the HTTP contract checks against one
4777
+ * PermDock Cloud environment, and validates every response body against
4778
+ * `cloud-api-v1.json`. Point it at a test environment: it creates approvals,
4779
+ * sends decision events and deliveries to `.invalid` addresses, and pushes
4780
+ * the catalog the environment already holds.
4781
+ */
4782
+ function testCloudApi(baseUrl, keys, options) {
4783
+ const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
4784
+ const url = trimSlash(baseUrl);
4785
+ const prefix = trimSlash(new URL(url).pathname);
4786
+ const { issuer } = cloudEndpoints({
4787
+ url,
4788
+ ...options.environment === void 0 ? {} : { environment: options.environment }
4789
+ });
4790
+ const check = cloudApiValidator();
4791
+ const problems = [];
4792
+ const checks = [];
4793
+ const recording = async (input, init) => {
4794
+ const request = new Request(input, init);
4795
+ const response = await fetchFn(request);
4796
+ const { pathname } = new URL(request.url);
4797
+ const path = pathname === `${METADATA}${prefix}` ? METADATA : pathname.slice(prefix.length);
4798
+ checks.push(check(request.method, path, response.clone()).then((found) => {
4799
+ problems.push(...found);
4800
+ }));
4801
+ return response;
4802
+ };
4803
+ const client = cloud({
4804
+ url,
4805
+ key: keys.client,
4806
+ fetch: recording,
4807
+ approver: options.approver,
4808
+ ...options.environment === void 0 ? {} : { environment: options.environment },
4809
+ ...options.verifier === void 0 ? {} : { verifier: options.verifier }
4810
+ });
4811
+ const call = (path, key, init = {}) => {
4812
+ const headers = new Headers(init.headers);
4813
+ if (key !== null) headers.set("authorization", `Bearer ${key}`);
4814
+ if (init.body !== void 0 && !headers.has("content-type")) headers.set("content-type", "application/json");
4815
+ return recording(`${issuer}${path}`, {
4816
+ ...init,
4817
+ headers
4818
+ });
4819
+ };
4820
+ const tag = () => `cloud-api-${globalThis.crypto.randomUUID()}`;
4821
+ const resolveRaw = async (token, verdict, target) => {
4822
+ const approver = await approverHeaders(verdict, options.approver, issuer, target);
4823
+ return call("/approvals/by-token/resolve", keys.client, {
4824
+ method: "POST",
4825
+ headers: {
4826
+ [APPROVAL_HEADER]: token,
4827
+ ...approver
4828
+ },
4829
+ body: JSON.stringify(verdict)
4830
+ });
4831
+ };
4832
+ const current = async (token) => {
4833
+ const response = await call("/approvals/by-token", keys.client, { headers: { [APPROVAL_HEADER]: token } });
4834
+ expect(response.status).toBe(200);
4835
+ const request = await response.json();
4836
+ const etag = response.headers.get("etag");
4837
+ return etag === null ? { request } : {
4838
+ request,
4839
+ etag
4840
+ };
4841
+ };
4842
+ testApprovalStore(client.approvals);
4843
+ testDecisionSink(client.sink);
4844
+ testPolicySource(client.policies);
4845
+ if (options.subject !== void 0) {
4846
+ const subject = options.subject;
4847
+ testSnapshotSource({ get: async () => client.snapshotsFor(await subject()).get() });
4848
+ }
4849
+ if (options.authzen !== void 0) testAuthzen(async (request) => recording(request), {
4850
+ vectors: options.authzen.vectors,
4851
+ origin: issuer,
4852
+ ...options.authzen.headers === void 0 ? {} : { headers: options.authzen.headers }
4853
+ });
4854
+ if (options.platform !== void 0) testCloudPlatform(url, options.environment ?? "production", options.platform, recording);
4855
+ it("answers 401 Problem Details without a key", async () => {
4856
+ const response = await call("/approvals", null);
4857
+ expect(response.status).toBe(401);
4858
+ expect(response.headers.get("content-type")).toMatch(/^application\/problem\+json/u);
4859
+ });
4860
+ it("answers 403 to a key of another kind", async () => {
4861
+ expect((await call("/hosted-grants", keys.client)).status).toBe(403);
4862
+ if (keys.export !== void 0) expect((await call("/approvals", keys.export)).status).toBe(403);
4863
+ });
4864
+ it("sends an ETag on the approval read and refuses an approver token with a stale one", async () => {
4865
+ const token = tag();
4866
+ await client.approvals.create(sample(token));
4867
+ const { request, etag } = await current(token);
4868
+ expect(etag).toEqual(expect.any(String));
4869
+ const stale = await resolveRaw(token, {
4870
+ status: "approved",
4871
+ by: admin
4872
+ }, async () => ({
4873
+ request,
4874
+ etag: "\"stale\""
4875
+ }));
4876
+ expect(stale.status).toBe(409);
4877
+ expect((await client.approvals.get(token))?.status).toBe("pending");
4878
+ });
4879
+ it("answers the documented resolve statuses", async () => {
4880
+ const verdict = {
4881
+ status: "approved",
4882
+ by: admin
4883
+ };
4884
+ const unsigned = tag();
4885
+ await client.approvals.create(sample(unsigned));
4886
+ const missingApprover = await call("/approvals/by-token/resolve", keys.client, {
4887
+ method: "POST",
4888
+ headers: { [APPROVAL_HEADER]: unsigned },
4889
+ body: JSON.stringify(verdict)
4890
+ });
4891
+ expect(missingApprover.status).toBe(401);
4892
+ expect(await problemCode(missingApprover)).toBe("approver-unauthenticated");
4893
+ const gated = tag();
4894
+ await client.approvals.create(sample(gated, { approvers: { by: {
4895
+ kind: "role",
4896
+ role: "admin",
4897
+ scope: "global"
4898
+ } } }));
4899
+ const ineligible = await resolveRaw(gated, {
4900
+ status: "approved",
4901
+ by: member
4902
+ }, async () => current(gated));
4903
+ expect(ineligible.status).toBe(403);
4904
+ const once = tag();
4905
+ await client.approvals.create(sample(once));
4906
+ expect((await resolveRaw(once, verdict, async () => current(once))).status).toBe(200);
4907
+ expect((await resolveRaw(once, verdict, async () => current(once))).status).toBe(409);
4908
+ const expired = tag();
4909
+ await client.approvals.create(sample(expired, {
4910
+ createdAt: isoAt(nowMillis() - 72e5),
4911
+ expiresAt: isoAt(nowMillis() - 6e4)
4912
+ }));
4913
+ expect((await resolveRaw(expired, verdict, async () => current(expired))).status).toBe(410);
4914
+ const missing = tag();
4915
+ const notFound = await resolveRaw(missing, verdict, async () => ({ request: sample(missing) }));
4916
+ expect(notFound.status).toBe(404);
4917
+ });
4918
+ it("refuses to sign the Cloud's own token types and approver tokens", async () => {
4919
+ for (const typ of REFUSED_TYPES) {
4920
+ const response = await call("/sign", keys.client, {
4921
+ method: "POST",
4922
+ headers: { accept: "application/jwt" },
4923
+ body: JSON.stringify({
4924
+ typ,
4925
+ claims: { sub: "u_1" },
4926
+ aud: issuer
4927
+ })
4928
+ });
4929
+ expect([typ, response.status]).toEqual([typ, 422]);
4930
+ }
4931
+ });
4932
+ it("repeats a delivery for a repeated Idempotency-Key and requires one", async () => {
4933
+ const delivery = tag();
4934
+ const message = {
4935
+ channel: "email",
4936
+ delivery,
4937
+ target: {
4938
+ address: `${delivery}@example.invalid`,
4939
+ principal: "u_1",
4940
+ permission: "post.delete"
4941
+ },
4942
+ message: {
4943
+ subject: "PermDock contract test",
4944
+ text: "Ignore."
4945
+ }
4946
+ };
4947
+ const send = (headers) => call("/deliveries", keys.client, {
4948
+ method: "POST",
4949
+ headers,
4950
+ body: JSON.stringify(message)
4951
+ });
4952
+ const first = await send({ "idempotency-key": delivery });
4953
+ const second = await send({ "idempotency-key": delivery });
4954
+ expect(first.ok && second.ok).toBe(true);
4955
+ const [a, b] = await Promise.all([first.json(), second.json()]);
4956
+ expect(isRecord(a) && isRecord(b) && a["id"] === b["id"]).toBe(true);
4957
+ expect((await send({})).status).toBe(400);
4958
+ });
4959
+ it("answers unchanged for the catalog it already holds", async (context) => {
4960
+ const response = await call("/catalog", keys.client);
4961
+ if (response.status === 404) context.skip();
4962
+ expect(response.status).toBe(200);
4963
+ const catalog = await response.json();
4964
+ expect(isRecord(catalog)).toBe(true);
4965
+ const pushed = await client.catalog.push(catalog);
4966
+ expect(pushed).toMatchObject({ unchanged: true });
4967
+ });
4968
+ it("answers 422 with a reason for a hosted grant the engine would drop", async (context) => {
4969
+ if (keys.admin === void 0) context.skip();
4970
+ const response = await call("/hosted-grants", keys.admin ?? "", {
4971
+ method: "POST",
4972
+ body: JSON.stringify({
4973
+ permission: "permdock.cloud-api-test.missing",
4974
+ to: { kind: "authenticated" }
4975
+ })
4976
+ });
4977
+ expect(response.status).toBe(422);
4978
+ const body = await response.json();
4979
+ expect(isRecord(body) && body["reason"]).toBe("unknown-permission");
4980
+ });
4981
+ it("serves the snapshot as application/jwt only to a fresh subject token", async (context) => {
4982
+ if (options.subject === void 0) {
4983
+ context.skip();
4984
+ return;
4985
+ }
4986
+ const token = await options.subject();
4987
+ const get = (headers) => call("/snapshot", keys.client, { headers: {
4988
+ accept: "application/jwt",
4989
+ ...headers
4990
+ } });
4991
+ const first = await get({ "permdock-subject": token });
4992
+ expect(first.status).toBe(200);
4993
+ expect(first.headers.get("content-type")).toMatch(/^application\/jwt/u);
4994
+ expect((await get({ "permdock-subject": token })).status).toBe(401);
4995
+ expect((await get({})).status).toBe(401);
4996
+ });
4997
+ it("answers every request in the shapes cloud-api-v1.json documents", async () => {
4998
+ await Promise.all(checks);
4999
+ expect(problems).toEqual([]);
5000
+ });
5001
+ }
5002
+ //#endregion
5003
+ //#region src/testing/conformance/where.ts
5004
+ const WHERE_ROWS = [
5005
+ {
5006
+ id: "r1",
5007
+ authorId: "u1",
5008
+ score: 5,
5009
+ title: "Plan 100%",
5010
+ archived: false,
5011
+ note: null
5012
+ },
5013
+ {
5014
+ id: "r2",
5015
+ authorId: "u2",
5016
+ score: 10,
5017
+ title: "plan_b",
5018
+ archived: true,
5019
+ note: "kept"
5020
+ },
5021
+ {
5022
+ id: "r3",
5023
+ authorId: null,
5024
+ score: null,
5025
+ title: null,
5026
+ archived: null,
5027
+ note: null
5028
+ }
5029
+ ];
5030
+ const WHERE_CASES = [
5031
+ ["eq", {
5032
+ op: "eq",
5033
+ field: "authorId",
5034
+ value: "u1"
5035
+ }],
5036
+ ["ne", {
5037
+ op: "ne",
5038
+ field: "authorId",
5039
+ value: "u1"
5040
+ }],
5041
+ ["eq on a boolean", {
5042
+ op: "eq",
5043
+ field: "archived",
5044
+ value: false
2252
5045
  }],
2253
5046
  ["gt", {
2254
5047
  op: "gt",
@@ -2419,9 +5212,9 @@ const jwtFixtureTokens = {
2419
5212
  wrongIss: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6ImF0K2p3dCJ9.eyJzdWIiOiJ1XzEiLCJpc3MiOiJodHRwczovL2V2aWwuZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoyMDAwMDAwMDAwfQ.tIcLHLa9JLtKz1gqjRGdr5DFbUzRDs5KsoPsJCxspvbBcsFfRFG2G0XJnd1h4QHyZ_5B4OhY3W_jIIsnOPD9Bg",
2420
5213
  unknownKid: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoibm9wZSIsInR5cCI6ImF0K2p3dCJ9.eyJzdWIiOiJ1XzEiLCJpc3MiOiJodHRwczovL2xvZ2luLmV4YW1wbGUuY29tIiwiYXVkIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20iLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MjAwMDAwMDAwMH0.walgG5GvpCVBnkMtkdw-epi5c93JqfvAM_N4EWR9Tqhiw_cnzITnlQyoVfihECmV1fqtFrhwM1RCIRJvd38zAQ",
2421
5214
  none: "eyJhbGciOiJub25lIn0.eyJzdWIiOiJ1XzEiLCJpc3MiOiJodHRwczovL2xvZ2luLmV4YW1wbGUuY29tIiwiYXVkIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20iLCJleHAiOjIwMDAwMDAwMDAsImlhdCI6MTcwMDAwMDAwMH0.",
2422
- snapshot: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6InBlcm1kb2NrLXNuYXBzaG90K2p3dCJ9.eyJzbmFwc2hvdCI6eyJ2IjoyLCJpc3N1ZWRBdCI6MTcwMDAwMDAwMCwic3ViamVjdCI6eyJwcmluY2lwYWwiOnsiaWQiOiJ1XzEiLCJyb2xlcyI6WyJtZW1iZXIiXX0sImNvbnRleHQiOnt9fSwicm9sZXMiOlsibWVtYmVyIl0sImdyYW50cyI6W10sInRlbmFudHMiOltdfSwic3ViIjoidV8xIiwiaXNzIjoiaHR0cHM6Ly9hcHAuZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwcC5leGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoyMDAwMDAwMDAwLCJqdGkiOiJzbmFwX2ZpeHR1cmUifQ.4itaYctluoCwU-syytUxArtS_FRyNAti2SZxuSH-nKIMr7-Sb3h1Q3xugn_vPR1eupV4fVNIe6dH27_EiyDvCA",
2423
- approval: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6InBlcm1kb2NrLWFwcHJvdmFsK2p3dCJ9.eyJhcHByb3ZhbCI6eyJ0b2tlbiI6InBkMS5hYmMiLCJwZXJtaXNzaW9uIjoicG9zdC5kZWxldGUiLCJyZXNvdXJjZSI6eyJ0eXBlIjoicG9zdCIsImlkIjoiNDIifSwic3RhdHVzIjoiYXBwcm92ZWQifSwic3ViIjoidV8xIiwiaXNzIjoiaHR0cHM6Ly9hcHAuZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwcC5leGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoyMDAwMDAwMDAwLCJqdGkiOiJhcHJfZml4dHVyZSJ9.UHsoSzQCo68G7ee-EFqdzMLvmqaKlFIMdQKFuqZEM4yUQnJsJJTXbld_aCXcyfN-IdjF9cj2F5EHzsV4nLKbBg",
2424
- decisions: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6InBlcm1kb2NrLWRlY2lzaW9ucytqd3QifQ.eyJldmVudHMiOltdLCJpc3MiOiJodHRwczovL2FwcC5leGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoyMDAwMDAwMDAwLCJqdGkiOiJkZWNfZml4dHVyZSJ9.UZhJd72yFGxHOdjYbzcGYUPgYjvLwOPCtHKxEbLu56SdKSKUDCREXmEBI9WaCPKWjlTBnnIKMYwjG8I8RATJCg"
5215
+ snapshot: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6InZuZC5wZXJtZG9jay5zbmFwc2hvdCtqd3QifQ.eyJzbmFwc2hvdCI6eyJ2IjoxLCJzdWJqZWN0Ijp7InByaW5jaXBhbCI6eyJpZCI6InVfMSIsInJvbGVzIjpbIm1lbWJlciJdfSwiY29udGV4dCI6e319LCJyb2xlcyI6WyJtZW1iZXIiXSwiZ3JhbnRzIjpbXSwidGVuYW50cyI6W119LCJzdWIiOiJ1XzEiLCJpYXQiOjE3MDAwMDAwMDAsImp0aSI6InNuYXBfZml4dHVyZSIsImlzcyI6Imh0dHBzOi8vYXBwLmV4YW1wbGUuY29tIiwiYXVkIjoiaHR0cHM6Ly9hcHAuZXhhbXBsZS5jb20iLCJleHAiOjIwMDAwMDAwMDB9.41KijBuOn1Ehodp60-Pn6qx_WrHQ-BiVUmYy2vjcHjO3o1dopXxjnHJP7P1c-6ghYgrvucPu7hZrDjTtPhxWBw",
5216
+ approval: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6InZuZC5wZXJtZG9jay5hcHByb3ZhbCtqd3QifQ.eyJzdWIiOiJ1XzEiLCJ0b2tlbiI6InNoYS0yNTY6NDdERVFwajhIQlNhLV9USW1XLTVKQ2V1UWVSa201Tk1wSldaRzNoU3VGVSIsInJlc29sdmVkX2J5IjoidV8yIiwiYXBwcm92YWxzIjpbeyJieSI6InVfMiIsImF0IjoiMjAyMy0xMS0xNFQyMjoxMzoyMFoifV0sImF1dGhvcml6YXRpb25fZGV0YWlscyI6W3sidHlwZSI6InBvc3QiLCJhY3Rpb25zIjpbImRlbGV0ZSJdLCJpZGVudGlmaWVyIjoiNDIifV0sImlhdCI6MTcwMDAwMDAwMCwianRpIjoiYXByX2ZpeHR1cmUiLCJpc3MiOiJodHRwczovL2FwcC5leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHBzOi8vYXBwLmV4YW1wbGUuY29tIiwiZXhwIjoyMDAwMDAwMDAwfQ.Q76F3tDT2FrEnxD9TjdLfEayJEyyGUWHu6bTCCl5Flegcl9-o370agNVhcV03Khfidm-lK7gPg05ZiYcpCfyBQ",
5217
+ decisions: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6InZuZC5wZXJtZG9jay5ldmlkZW5jZStqd3QifQ.eyJldmVudHMiOltdLCJpYXQiOjE3MDAwMDAwMDAsImp0aSI6ImRlY19maXh0dXJlIiwiaXNzIjoiaHR0cHM6Ly9hcHAuZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwcC5leGFtcGxlLmNvbSIsImV4cCI6MjAwMDAwMDAwMH0.oGiFmCEFWUAEBtLO3F_rnGMSKBhb9rIxng4YLwRbBTA3fudrbJMcJD2BhNH99cjcBa1xCmADfdOnTc-Ltq-kAA"
2425
5218
  };
2426
5219
  //#endregion
2427
5220
  //#region src/testing/conformance/credentials.ts
@@ -2434,7 +5227,7 @@ function testReplayStore(store) {
2434
5227
  expect(await store.seen("jti-2")).toBe(false);
2435
5228
  });
2436
5229
  it("forgets a jti whose expiresAt has passed", async () => {
2437
- await store.remember("ttl-jti", Math.floor(Date.now() / 1e3) - 1);
5230
+ await store.remember("ttl-jti", nowSeconds() - 1);
2438
5231
  expect(await store.seen("ttl-jti")).toBe(false);
2439
5232
  });
2440
5233
  it("claims a key once under concurrency and releases it for a retry", async (context) => {
@@ -2442,7 +5235,7 @@ function testReplayStore(store) {
2442
5235
  context.skip();
2443
5236
  return;
2444
5237
  }
2445
- const expiresAt = Math.floor(Date.now() / 1e3) + 60;
5238
+ const expiresAt = nowSeconds() + 60;
2446
5239
  const results = await Promise.all([store.claim("claim-key", expiresAt), store.claim("claim-key", expiresAt)]);
2447
5240
  expect(results.toSorted()).toEqual([false, true]);
2448
5241
  expect(await store.seen("claim-key")).toBe(true);
@@ -2450,6 +5243,43 @@ function testReplayStore(store) {
2450
5243
  expect(await store.claim("claim-key", expiresAt)).toBe(true);
2451
5244
  });
2452
5245
  }
5246
+ function testSetQueue(queue) {
5247
+ it("returns pending SETs oldest first, at most max, per stream", async () => {
5248
+ await queue.enqueue("stream-a", "a1", "set-a1");
5249
+ await queue.enqueue("stream-a", "a2", "set-a2");
5250
+ await queue.enqueue("stream-b", "b1", "set-b1");
5251
+ expect(await queue.pending("stream-a", 10)).toEqual([{
5252
+ jti: "a1",
5253
+ set: "set-a1"
5254
+ }, {
5255
+ jti: "a2",
5256
+ set: "set-a2"
5257
+ }]);
5258
+ expect(await queue.pending("stream-a", 1)).toEqual([{
5259
+ jti: "a1",
5260
+ set: "set-a1"
5261
+ }]);
5262
+ expect(await queue.pending("stream-b", 10)).toEqual([{
5263
+ jti: "b1",
5264
+ set: "set-b1"
5265
+ }]);
5266
+ });
5267
+ it("keeps a SET pending until it is acknowledged or reported failed", async () => {
5268
+ await queue.enqueue("stream-c", "c1", "set-c1");
5269
+ await queue.enqueue("stream-c", "c2", "set-c2");
5270
+ await queue.enqueue("stream-c", "c3", "set-c3");
5271
+ await queue.ack("stream-c", "c1");
5272
+ await queue.fail("stream-c", "c2", {
5273
+ err: "invalid_key",
5274
+ description: "unknown kid"
5275
+ });
5276
+ await queue.ack("stream-other", "c3");
5277
+ expect(await queue.pending("stream-c", 10)).toEqual([{
5278
+ jti: "c3",
5279
+ set: "set-c3"
5280
+ }]);
5281
+ });
5282
+ }
2453
5283
  function testRevocationFeed(feed) {
2454
5284
  it("delivers each event to every subscriber until it unsubscribes", async () => {
2455
5285
  const first = [];
@@ -2636,8 +5466,8 @@ function testCredentialVerifier(verifier, options) {
2636
5466
  const key = await live();
2637
5467
  const before = parseCredential(await verifier.verify(key));
2638
5468
  const id = parseApiKey(key)?.id ?? "";
2639
- await verifier.touch?.(id, Math.floor(Date.now() / 1e3));
2640
- await verifier.touch?.("unknown-id", Math.floor(Date.now() / 1e3));
5469
+ await verifier.touch?.(id, { epochMilliseconds: nowMillis() });
5470
+ await verifier.touch?.("unknown-id", { epochMilliseconds: nowMillis() });
2641
5471
  expect(parseCredential(await verifier.verify(key))).toEqual(before);
2642
5472
  });
2643
5473
  if (options.revoke !== void 0) {
@@ -2655,7 +5485,7 @@ function testTokenSigner(signer, options) {
2655
5485
  snapshot: { v: 1 },
2656
5486
  sub: "u_1"
2657
5487
  }, {
2658
- typ: "permdock-snapshot+jwt",
5488
+ typ: "vnd.permdock.snapshot+jwt",
2659
5489
  audience: "https://app.example.com"
2660
5490
  });
2661
5491
  const header = decodeHeader(token);
@@ -2664,10 +5494,10 @@ function testTokenSigner(signer, options) {
2664
5494
  "kid",
2665
5495
  "typ"
2666
5496
  ]);
2667
- expect(header["typ"]).toBe("permdock-snapshot+jwt");
5497
+ expect(header["typ"]).toBe("vnd.permdock.snapshot+jwt");
2668
5498
  expect(header["alg"]).not.toBe("none");
2669
5499
  const verified = await options.verifier.verify(token, {
2670
- typ: "permdock-snapshot+jwt",
5500
+ typ: "vnd.permdock.snapshot+jwt",
2671
5501
  audience: "https://app.example.com"
2672
5502
  });
2673
5503
  expect(verified.ok).toBe(true);
@@ -2679,80 +5509,82 @@ function testTokenSigner(signer, options) {
2679
5509
  });
2680
5510
  }
2681
5511
  //#endregion
2682
- //#region src/testing/conformance/runtime-stores.ts
2683
- function testLimitStore(store) {
2684
- it("counts down synchronously and fails closed when exhausted", async () => {
2685
- const input = {
2686
- key: "report.export",
2687
- subjectId: "u_1",
2688
- count: 2,
2689
- per: "hour",
2690
- now: 17e8
2691
- };
2692
- const first = await store.consume(input);
2693
- expect(first.remaining).toBeGreaterThanOrEqual(0);
2694
- const peeked = store.remaining(input);
2695
- if (peeked !== void 0) {
2696
- expect(typeof peeked.remaining).toBe("number");
2697
- expect(peeked !== null && typeof peeked === "object" && "then" in peeked && typeof peeked.then === "function").toBe(false);
2698
- }
2699
- await store.consume(input);
2700
- const exhausted = await store.consume(input);
2701
- expect(exhausted.remaining).toBeLessThan(0);
2702
- });
5512
+ //#region src/testing/conformance/providers.ts
5513
+ const OUTCOMES = [
5514
+ "granted",
5515
+ "denied",
5516
+ "approval-required"
5517
+ ];
5518
+ const LOCAL = Object.freeze({
5519
+ outcome: "denied",
5520
+ denials: Object.freeze([Object.freeze({
5521
+ role: null,
5522
+ reason: "no-grant"
5523
+ })]),
5524
+ alternatives: Object.freeze([])
5525
+ });
5526
+ function expectDecision(decision) {
5527
+ expect(OUTCOMES).toContain(decision.outcome);
5528
+ if (decision.outcome === "denied") expect(decision.denials.length).toBeGreaterThan(0);
2703
5529
  }
2704
- function testDecisionSink(sink) {
2705
- it("accepts batches and never propagates write errors", async () => {
2706
- await expect(Promise.resolve(sink.write([{
2707
- type: "membership",
2708
- at: (/* @__PURE__ */ new Date()).toISOString(),
2709
- source: "app",
2710
- operation: "changed",
2711
- principal: { id: "u_1" },
2712
- roles: {
2713
- added: ["admin"],
2714
- removed: ["member"]
2715
- }
2716
- }]))).resolves.toBeUndefined();
2717
- await expect(Promise.resolve(sink.write([]))).resolves.toBeUndefined();
2718
- if (sink.flush !== void 0) {
2719
- await expect(Promise.resolve(sink.flush())).resolves.toBeUndefined();
2720
- await expect(Promise.resolve(sink.flush())).resolves.toBeUndefined();
2721
- }
5530
+ function testDecisionProvider(provider, options) {
5531
+ const request = Object.freeze({
5532
+ permission: options.permission,
5533
+ data: options.data,
5534
+ subject: options.subject,
5535
+ local: LOCAL
2722
5536
  });
2723
- }
2724
- function testSnapshotSource(source) {
2725
- it("round-trips a snapshot", async () => {
2726
- const snapshot = await source.get();
2727
- expect(snapshot === null || snapshot === void 0).toBe(false);
2728
- if (typeof snapshot === "object" && snapshot !== null && "v" in snapshot) expect(snapshot.v).toBe(1);
2729
- if (source.subscribe !== void 0) source.subscribe(() => void 0)();
5537
+ it("names itself and claims the permission synchronously", () => {
5538
+ expect(typeof provider.name).toBe("string");
5539
+ expect(provider.name.length).toBeGreaterThan(0);
5540
+ expect(provider.handles(options.permission)).toBe(true);
2730
5541
  });
2731
- }
2732
- function testPolicySource(source, options = {}) {
2733
- const readCurrent = () => {
2734
- const document = source.current();
2735
- expect(document !== null && typeof document === "object" && "then" in document && typeof document.then === "function").toBe(false);
2736
- return document;
2737
- };
2738
- it("returns null or a v1 policy document synchronously", () => {
2739
- const document = readCurrent();
2740
- if (document !== null) expect(parsePolicyDocument(document)).toEqual(document);
5542
+ it("answers decide with one of the three outcomes", async () => {
5543
+ expectDecision(await provider.decide(request));
2741
5544
  });
2742
- it("refreshes without rejecting and keeps current() synchronous", async () => {
2743
- await expect(Promise.resolve(source.refresh())).resolves.toBeUndefined();
2744
- const document = readCurrent();
2745
- if (document !== null) expect(document.v).toBe(1);
5545
+ it("lists permitted ids as non-empty strings, or null or undefined", async () => {
5546
+ const ids = provider.permitted === void 0 ? void 0 : await provider.permitted({
5547
+ permission: options.permission,
5548
+ subject: options.subject
5549
+ });
5550
+ expect(ids === null || ids === void 0 || ids.every((id) => typeof id === "string" && id.length > 0)).toBe(true);
2746
5551
  });
2747
- if (options.policy !== void 0) {
2748
- const policy = options.policy;
2749
- it("merges only grants on hostable permissions", () => {
2750
- const document = readCurrent();
2751
- const merged = mergeHostedGrants(policy, document);
2752
- for (const grant of merged.policy.grants) if (grant.hosted !== void 0) expect(policy.hostable).toContain(grant.permission.key);
2753
- for (const dropped of merged.dropped) expect(dropped.kind).toBe("hosted-grant-dropped");
5552
+ it("denies without throwing when its transport fails", async () => {
5553
+ const unavailable = options.unavailable;
5554
+ const decision = unavailable === void 0 ? void 0 : await unavailable.decide(request).catch(() => void 0);
5555
+ expect(unavailable === void 0 || decision?.outcome === "denied").toBe(true);
5556
+ });
5557
+ }
5558
+ function testSnapshotStorage(storage) {
5559
+ const key = `permdock.conformance.${globalThis.crypto.randomUUID()}`;
5560
+ const other = `${key}.other`;
5561
+ it("reads back what it stores, verbatim", async () => {
5562
+ const value = JSON.stringify({
5563
+ v: 1,
5564
+ note: "ünïcødé \u2028 line"
2754
5565
  });
2755
- }
5566
+ await storage.setItem(key, value);
5567
+ expect(await storage.getItem(key)).toBe(value);
5568
+ await storage.setItem(key, "replaced");
5569
+ expect(await storage.getItem(key)).toBe("replaced");
5570
+ await storage.removeItem(key);
5571
+ });
5572
+ it("answers null for a key it never stored or removed", async () => {
5573
+ expect(await storage.getItem(other)).toBeNull();
5574
+ await storage.setItem(key, "kept");
5575
+ await storage.removeItem(key);
5576
+ expect(await storage.getItem(key)).toBeNull();
5577
+ await storage.removeItem(other);
5578
+ });
5579
+ it("keeps keys apart", async () => {
5580
+ await storage.setItem(key, "a");
5581
+ await storage.setItem(other, "b");
5582
+ expect(await storage.getItem(key)).toBe("a");
5583
+ expect(await storage.getItem(other)).toBe("b");
5584
+ await storage.removeItem(key);
5585
+ expect(await storage.getItem(other)).toBe("b");
5586
+ await storage.removeItem(other);
5587
+ });
2756
5588
  }
2757
5589
  //#endregion
2758
5590
  //#region src/testing/conformance/relations.ts
@@ -2878,6 +5710,302 @@ function testRelationSource(source, options) {
2878
5710
  });
2879
5711
  }
2880
5712
  //#endregion
5713
+ //#region src/testing/conformance/principals.ts
5714
+ function isFrozenDeep(value) {
5715
+ if (value === null || typeof value !== "object") return true;
5716
+ return Object.isFrozen(value) && Object.values(value).every((item) => isFrozenDeep(item));
5717
+ }
5718
+ /**
5719
+ * Checks a `PrincipalSource`: `principal` answers each listed id with the
5720
+ * expected principal or `null`, never another id; `principalByIdentity`
5721
+ * resolves each listed identity to the expected principal id or `null`, and
5722
+ * an email matches trimmed and in any case.
5723
+ */
5724
+ function testPrincipalSource(source, options) {
5725
+ it("returns the principal with the asked id, or null", async () => {
5726
+ for (const entry of options.principals) {
5727
+ let found;
5728
+ try {
5729
+ found = await source.principal(entry.id, entry.tenant === void 0 ? {} : { tenant: entry.tenant });
5730
+ } catch {
5731
+ found = null;
5732
+ }
5733
+ expect(found === null || typeof found === "object").toBe(true);
5734
+ if (found !== null) expect(await loadPrincipal(source, entry.id, entry.tenant)).toEqual(found);
5735
+ if (entry.expect !== void 0) expect(found).toEqual(entry.expect);
5736
+ }
5737
+ });
5738
+ it("returns principals a caller cannot change", async () => {
5739
+ for (const entry of options.principals) {
5740
+ const found = await loadPrincipal(source, entry.id, entry.tenant);
5741
+ if (found !== null) expect(isFrozenDeep(found)).toBe(true);
5742
+ }
5743
+ });
5744
+ it("answers an empty or unknown id with null", async () => {
5745
+ expect(await loadPrincipal(source, "")).toBeNull();
5746
+ expect(await loadPrincipal(source, "permdock-conformance-unknown-principal")).toBeNull();
5747
+ });
5748
+ if (options.identities !== void 0 && options.identities.length > 0) {
5749
+ const identities = options.identities;
5750
+ it("resolves linked identities and nothing else", async () => {
5751
+ for (const entry of identities) {
5752
+ const found = await loadPrincipalByIdentity(source, entry.identity);
5753
+ expect(found?.id ?? null).toBe(entry.expect);
5754
+ }
5755
+ });
5756
+ it("matches an email trimmed and in any case", async () => {
5757
+ for (const entry of identities) {
5758
+ if (entry.identity.kind !== "email" || entry.expect === null) continue;
5759
+ const shouted = await loadPrincipalByIdentity(source, {
5760
+ kind: "email",
5761
+ value: ` ${entry.identity.value.toUpperCase()} `
5762
+ });
5763
+ expect(shouted?.id ?? null).toBe(entry.expect);
5764
+ }
5765
+ });
5766
+ }
5767
+ }
5768
+ //#endregion
5769
+ //#region src/testing/conformance/automations.ts
5770
+ async function automationOf(source, id, tenant) {
5771
+ try {
5772
+ return await source.automation(id, tenant === void 0 ? {} : { tenant });
5773
+ } catch {
5774
+ return null;
5775
+ }
5776
+ }
5777
+ /**
5778
+ * Checks an `AutomationSource`: `automation(id)` answers each listed id
5779
+ * with a valid Automation v1 record of that id or `null`, never another id;
5780
+ * a record held to one tenant is `null` when asked for another; an empty or
5781
+ * unknown id is `null`.
5782
+ */
5783
+ function testAutomationSource(source, options) {
5784
+ it("returns a valid v1 record with the asked id, or null", async () => {
5785
+ for (const entry of options.automations) {
5786
+ const found = await automationOf(source, entry.id, entry.tenant);
5787
+ if (found !== null) {
5788
+ const parsed = parseAutomation(found);
5789
+ expect(parsed).toEqual(found);
5790
+ expect(parsed?.id).toBe(entry.id);
5791
+ }
5792
+ if (entry.expect !== void 0) expect(found).toEqual(entry.expect);
5793
+ }
5794
+ });
5795
+ it("refuses a record held to another tenant", async () => {
5796
+ for (const entry of options.automations) {
5797
+ const found = parseAutomation(await automationOf(source, entry.id));
5798
+ if (found?.tenant === void 0) continue;
5799
+ expect(await automationOf(source, entry.id, `${found.tenant}-permdock-conformance-other`)).toBeNull();
5800
+ }
5801
+ });
5802
+ it("answers an empty or unknown id with null", async () => {
5803
+ expect(await automationOf(source, "")).toBeNull();
5804
+ expect(await automationOf(source, "permdock-conformance-unknown-automation")).toBeNull();
5805
+ });
5806
+ }
5807
+ async function sessionOf(source, id) {
5808
+ try {
5809
+ return await source.session(id);
5810
+ } catch {
5811
+ return null;
5812
+ }
5813
+ }
5814
+ /**
5815
+ * Checks a `SupportSessionSource`: `session(id)` answers each listed id
5816
+ * with a record of that id or `null`, never another id; `ended` is a
5817
+ * boolean when present; an empty or unknown id is `null`.
5818
+ */
5819
+ function testSupportSessionSource(source, options) {
5820
+ it("returns the session with the asked id, or null", async () => {
5821
+ for (const entry of options.sessions) {
5822
+ const found = await sessionOf(source, entry.id);
5823
+ expect(found === null || typeof found === "object").toBe(true);
5824
+ if (found !== null && typeof found === "object") {
5825
+ expect(Reflect.get(found, "id")).toBe(entry.id);
5826
+ const ended = Reflect.get(found, "ended");
5827
+ expect(ended === void 0 || typeof ended === "boolean").toBe(true);
5828
+ }
5829
+ if (entry.ended === null) expect(found).toBeNull();
5830
+ else if (entry.ended !== void 0) {
5831
+ expect(found).not.toBeNull();
5832
+ expect(found !== null && typeof found === "object" && Reflect.get(found, "ended") === true).toBe(entry.ended);
5833
+ }
5834
+ }
5835
+ });
5836
+ it("answers an empty or unknown id with null", async () => {
5837
+ expect(await sessionOf(source, "")).toBeNull();
5838
+ expect(await sessionOf(source, "permdock-conformance-unknown-session")).toBeNull();
5839
+ });
5840
+ }
5841
+ const SETTLED = Symbol("settled");
5842
+ /** Whether `promise` settles within `ms` milliseconds; a rejection counts as settled. */
5843
+ async function settlesWithin(promise, ms) {
5844
+ let timer;
5845
+ const timeout = new Promise((resolve) => {
5846
+ timer = setTimeout(() => {
5847
+ resolve(false);
5848
+ }, ms);
5849
+ });
5850
+ try {
5851
+ return await Promise.race([promise.then(() => SETTLED, () => SETTLED), timeout]).then((result) => result === SETTLED);
5852
+ } finally {
5853
+ clearTimeout(timer);
5854
+ }
5855
+ }
5856
+ /**
5857
+ * Checks an `ApprovalWaiter`: a wait ends when its token is woken and not
5858
+ * when another token is; waking a token nobody waits on resolves; a wait
5859
+ * whose `expiresAt` has passed ends; an aborted `signal` rejects the wait with its reason.
5860
+ * Pass `expiry: false` or `abort: false` for a waiter whose runner offers no
5861
+ * timer or no cancellation, `pending` when the waiter reads a store, and
5862
+ * `parked` when a wait parks asynchronously.
5863
+ */
5864
+ function testApprovalWaiter(waiter, options = {}) {
5865
+ const timeout = options.timeout ?? 1e3;
5866
+ const pending = async (label) => {
5867
+ return options.pending === void 0 ? `permdock-conformance-${label}` : await options.pending(label);
5868
+ };
5869
+ const parked = options.parked ?? (() => new Promise((resolve) => {
5870
+ setTimeout(resolve, 0);
5871
+ }));
5872
+ it("ends a wait when its token is woken, and only then", async () => {
5873
+ const woken = await pending("woken");
5874
+ const still = await pending("other");
5875
+ const waiting = waiter.wait(woken);
5876
+ const other = waiter.wait(still);
5877
+ await parked(woken);
5878
+ await parked(still);
5879
+ await waiter.wake(woken);
5880
+ expect(await settlesWithin(waiting, timeout)).toBe(true);
5881
+ await expect(waiting).resolves.toBeUndefined();
5882
+ expect(await settlesWithin(other, 20)).toBe(false);
5883
+ await waiter.wake(still);
5884
+ await expect(other).resolves.toBeUndefined();
5885
+ });
5886
+ it("resolves a wake for a token nobody waits on", async () => {
5887
+ await expect(Promise.resolve(waiter.wake("permdock-conformance-nobody"))).resolves.toBeUndefined();
5888
+ });
5889
+ if (options.expiry !== false) it("ends a wait whose expiresAt has passed", async () => {
5890
+ const waiting = waiter.wait(await pending("expired"), { expiresAt: { epochMilliseconds: nowMillis() - 1e3 } });
5891
+ expect(await settlesWithin(waiting, timeout)).toBe(true);
5892
+ await expect(waiting).resolves.toBeUndefined();
5893
+ });
5894
+ if (options.abort !== false) it("rejects a wait when its signal aborts", async () => {
5895
+ const token = await pending("aborted");
5896
+ const controller = new AbortController();
5897
+ const waiting = waiter.wait(token, { signal: controller.signal });
5898
+ await parked(token);
5899
+ const reason = /* @__PURE__ */ new Error("permdock-conformance-abort");
5900
+ controller.abort(reason);
5901
+ await expect(waiting).rejects.toBe(reason);
5902
+ });
5903
+ }
5904
+ //#endregion
5905
+ //#region src/testing/delivery-transport.ts
5906
+ const permissions = definePermissions({ notice: resource({ actions: ["read"] }) });
5907
+ const policy = definePolicy(permissions, {
5908
+ grants: [allow(permissions.notice.read, { to: authenticated() })],
5909
+ subject: (user) => user
5910
+ });
5911
+ async function grantedDecision() {
5912
+ const decision = (await getPermDock(policy, { id: "u_runner" })).decide(permissions.notice.read, { id: "n_runner" }, { trusted: true });
5913
+ if (decision.outcome !== "granted") throw new Error("testDeliveryTransport: the runner policy did not grant");
5914
+ return decision;
5915
+ }
5916
+ function key(suffix) {
5917
+ return {
5918
+ idempotencyKey: `pdd_runner_${suffix}`,
5919
+ delivery: "dlv_runner"
5920
+ };
5921
+ }
5922
+ /**
5923
+ * Conformance for a `DeliveryTransport`: it sends a checked target with its
5924
+ * idempotency key, keeps the key on a retry, refuses a target without a
5925
+ * granted decision before calling the provider, and maps provider errors to
5926
+ * retryable or final failures without rejecting.
5927
+ */
5928
+ function testDeliveryTransport(name, options) {
5929
+ const targetWith = async (decision) => ({
5930
+ channel: options.channel,
5931
+ address: options.address,
5932
+ principal: "u_runner",
5933
+ decision
5934
+ });
5935
+ const send = async (harness, target, suffix) => harness.transport.deliver(target, options.message, key(suffix));
5936
+ describe(`${name} (delivery transport)`, () => {
5937
+ it("names its channel and provider", () => {
5938
+ const { transport } = options.create();
5939
+ expect(transport.channel).toBe(options.channel);
5940
+ expect(typeof transport.name).toBe("string");
5941
+ expect(transport.name).not.toBe("");
5942
+ });
5943
+ it("sends a checked target with its idempotency key", async () => {
5944
+ const harness = options.create();
5945
+ const result = await send(harness, await targetWith(await grantedDecision()), "a");
5946
+ expect(result.status).toBe("sent");
5947
+ expect(await harness.received()).toEqual([{ idempotencyKey: "pdd_runner_a" }]);
5948
+ });
5949
+ it("passes the same key on a retry of the same delivery", async () => {
5950
+ const harness = options.create();
5951
+ const target = await targetWith(await grantedDecision());
5952
+ await send(harness, target, "b");
5953
+ await send(harness, target, "b");
5954
+ const received = await harness.received();
5955
+ expect(received.length).toBeGreaterThan(0);
5956
+ expect(received.every((entry) => entry.idempotencyKey === "pdd_runner_b")).toBe(true);
5957
+ });
5958
+ it("refuses a target without a granted decision and never calls the provider", async () => {
5959
+ const harness = options.create();
5960
+ for (const decision of [
5961
+ void 0,
5962
+ {
5963
+ outcome: "denied",
5964
+ denials: [],
5965
+ alternatives: []
5966
+ },
5967
+ { outcome: "approval-required" }
5968
+ ]) expect(await send(harness, await targetWith(decision), "c")).toMatchObject({
5969
+ status: "failed",
5970
+ cause: "invalid-recipient",
5971
+ retryable: false
5972
+ });
5973
+ expect(await harness.received()).toEqual([]);
5974
+ });
5975
+ it("maps a retryable provider error to a retryable failure", async () => {
5976
+ const harness = options.create();
5977
+ harness.fail("retryable");
5978
+ expect(await send(harness, await targetWith(await grantedDecision()), "d")).toMatchObject({
5979
+ status: "failed",
5980
+ retryable: true
5981
+ });
5982
+ });
5983
+ it("maps a final provider error to a final failure", async () => {
5984
+ const harness = options.create();
5985
+ harness.fail("final");
5986
+ expect(await send(harness, await targetWith(await grantedDecision()), "e")).toMatchObject({
5987
+ status: "failed",
5988
+ retryable: false
5989
+ });
5990
+ });
5991
+ it("resolves a provider throw as a retryable failure or rejects for the router to catch", async () => {
5992
+ const harness = options.create();
5993
+ harness.fail("throw");
5994
+ let result;
5995
+ try {
5996
+ result = await send(harness, await targetWith(await grantedDecision()), "f");
5997
+ } catch {
5998
+ result = void 0;
5999
+ }
6000
+ if (result !== void 0) expect(result).toMatchObject({
6001
+ status: "failed",
6002
+ retryable: true
6003
+ });
6004
+ expect(await harness.received()).toEqual([]);
6005
+ });
6006
+ });
6007
+ }
6008
+ //#endregion
2881
6009
  //#region src/testing/conformance/identity.ts
2882
6010
  function testSubjectResolver(resolver, options) {
2883
6011
  it("never throws and fails closed to anonymous", async () => {
@@ -2948,7 +6076,7 @@ function testEntitlementSource(source, options) {
2948
6076
  expect(await source.entitlementsFor(options.principal, {})).toEqual([]);
2949
6077
  });
2950
6078
  }
2951
- function testRoleSource(source, options) {
6079
+ function testCustomRoleSource(source, options) {
2952
6080
  it("only resolves declared role names", async () => {
2953
6081
  const declared = options.declared.map((item) => typeof item === "string" ? item : item.key);
2954
6082
  const roles = await source.rolesFor(options.tenant);
@@ -3046,7 +6174,7 @@ function testDirectoryStore(store, options) {
3046
6174
  const kinds = [];
3047
6175
  const token = "conformance-scim-token";
3048
6176
  const handle = scimHandler({
3049
- store,
6177
+ directory: store,
3050
6178
  tenant: home,
3051
6179
  token: {
3052
6180
  hash: "sha256",
@@ -3100,7 +6228,7 @@ function testSettingsSource(source, options) {
3100
6228
  const credentials = settings.credentials;
3101
6229
  if (credentials !== void 0) {
3102
6230
  expect(credentials.maxTtl === void 0 || Number.isFinite(credentials.maxTtl) && credentials.maxTtl >= 0).toBe(true);
3103
- expect(credentials.kinds === void 0 || credentials.kinds.every((kind) => kind === "user" || kind === "service")).toBe(true);
6231
+ expect(credentials.kinds === void 0 || credentials.kinds.every((kind) => kind === "user" || kind === "service" || kind === "feed")).toBe(true);
3104
6232
  }
3105
6233
  expect(JSON.parse(JSON.stringify(settings))).toEqual(settings);
3106
6234
  }
@@ -3137,6 +6265,17 @@ const ownerMembership = {
3137
6265
  managedBy: "idp",
3138
6266
  entitlements: ["seat:pro"]
3139
6267
  };
6268
+ const ownerMembershipClaim = {
6269
+ scope: ownerMembership.scope,
6270
+ id: ownerMembership.id,
6271
+ roles: ownerMembership.roles,
6272
+ via: ownerMembership.via,
6273
+ expires_at: ownerMembership.expiresAt,
6274
+ granted_by: ownerMembership.grantedBy,
6275
+ reason: ownerMembership.reason,
6276
+ managed_by: ownerMembership.managedBy,
6277
+ entitlements: ownerMembership.entitlements
6278
+ };
3140
6279
  const projectMembership = {
3141
6280
  scope: "project",
3142
6281
  id: project,
@@ -3290,7 +6429,7 @@ const supabaseClaimFixtures = {
3290
6429
  },
3291
6430
  authz_ver: 3
3292
6431
  },
3293
- options: { plans: "features" },
6432
+ options: { claims: { plans: "features" } },
3294
6433
  expect: {
3295
6434
  id,
3296
6435
  roles: [],
@@ -3315,7 +6454,7 @@ const supabaseClaimFixtures = {
3315
6454
  tenant_id: org,
3316
6455
  memberships: [
3317
6456
  {
3318
- ...ownerMembership,
6457
+ ...ownerMembershipClaim,
3319
6458
  grants: { analyst: ["post.read", "-post.delete"] }
3320
6459
  },
3321
6460
  projectMembership,
@@ -3683,4 +6822,139 @@ const supabaseHookManifestFixture = {
3683
6822
  }
3684
6823
  };
3685
6824
  //#endregion
3686
- export { AGENT_THROWING_ROW, AGENT_THROWING_USER, authzenTodoData, authzenTodoPermissions, authzenTodoPolicy, authzenTodoUsers, authzenTodoVectors, describePolicy, expectTypeOf, jwtFixtureAudience, jwtFixtureIssuer, jwtFixtureJwks, jwtFixtureTokens, ormParity, rlsParity, snapshotFixture, supabaseClaimFixtures, supabaseClaimVectors, supabaseHookManifestFixture, supabaseMembershipsBudget, testAgentAdapter, testApprovalPolicySource, testApprovalStore, testAuthZen, testClientParity, testClientStore, testCredentialVerifier, testDecisionSink, testDirectoryStore, testEntitlementSource, testHttpAdapter, testLimitStore, testMembershipSource, testPolicySource, testRelationSource, testReplayStore, testRevocationFeed, testRoleSource, testSettingsSource, testSnapshotSource, testSubjectResolver, testTokenSigner, testTokenVerifier, testWhereCompiler };
6825
+ //#region src/testing/problem-examples.ts
6826
+ /**
6827
+ * One RFC 9457 body per PermDock problem type, exactly as the HTTP adapters
6828
+ * write it for a `post.delete` request by subject `u_123`. A client tests its
6829
+ * error handling against these; `tests/standards/problem-details.test.ts`
6830
+ * rebuilds each from the code that emits it, and the docs show the same JSON.
6831
+ */
6832
+ const PROBLEM_EXAMPLES = Object.freeze({
6833
+ denied: {
6834
+ type: "https://permdock.com/problems/denied",
6835
+ title: "Permission denied",
6836
+ status: 403,
6837
+ detail: "post.delete denied for subject u_123: member (condition). Alternatives: post.read, post.update.",
6838
+ instance: "/posts/p_42",
6839
+ permission: "post.delete",
6840
+ scope: "post:delete",
6841
+ resource: { type: "post" },
6842
+ denials: [{
6843
+ role: "member",
6844
+ reason: "condition"
6845
+ }],
6846
+ alternatives: ["post.read", "post.update"]
6847
+ },
6848
+ "approval-required": {
6849
+ type: "https://permdock.com/problems/approval-required",
6850
+ title: "Approval required",
6851
+ status: 403,
6852
+ detail: "post.delete requires human approval (human). Token: pd1.eyJhbGciOiJFZERTQSJ9.",
6853
+ instance: "/posts/p_42",
6854
+ permission: "post.delete",
6855
+ scope: "post:delete",
6856
+ resource: { type: "post" },
6857
+ reason: "human",
6858
+ token: "pd1.eyJhbGciOiJFZERTQSJ9",
6859
+ approval: { at: "https://app.example/approvals" }
6860
+ },
6861
+ validation: {
6862
+ type: "https://permdock.com/problems/validation",
6863
+ title: "Invalid resource data",
6864
+ status: 400,
6865
+ detail: "invalid post",
6866
+ permission: "post.delete",
6867
+ issues: [{
6868
+ message: "title is required",
6869
+ path: ["title"]
6870
+ }]
6871
+ },
6872
+ "step-up-required": {
6873
+ type: "https://permdock.com/problems/step-up-required",
6874
+ title: "Step-up required",
6875
+ status: 401,
6876
+ detail: "post.delete denied for subject u_123: member (insufficient-user-authentication). Alternatives: post.read, post.update.",
6877
+ permission: "post.delete",
6878
+ scope: "post:delete",
6879
+ resource: { type: "post" },
6880
+ denials: [{
6881
+ role: "member",
6882
+ reason: "insufficient-user-authentication",
6883
+ to: {
6884
+ kind: "assurance",
6885
+ acr: ["urn:mfa"],
6886
+ maxAge: 600
6887
+ }
6888
+ }],
6889
+ alternatives: ["post.read", "post.update"],
6890
+ acrValues: ["urn:mfa"],
6891
+ maxAge: 600
6892
+ },
6893
+ "not-entitled": {
6894
+ type: "https://permdock.com/problems/not-entitled",
6895
+ title: "Plan upgrade required",
6896
+ status: 403,
6897
+ detail: "post.delete denied for subject u_123: admin (not-entitled). Alternatives: post.read, post.update.",
6898
+ permission: "post.delete",
6899
+ scope: "post:delete",
6900
+ resource: { type: "post" },
6901
+ denials: [{
6902
+ role: "admin",
6903
+ reason: "not-entitled",
6904
+ to: {
6905
+ kind: "plan",
6906
+ plan: "pro"
6907
+ }
6908
+ }],
6909
+ alternatives: ["post.read", "post.update"],
6910
+ plans: ["pro"]
6911
+ },
6912
+ "rate-limited": {
6913
+ type: "https://permdock.com/problems/rate-limited",
6914
+ title: "Rate limit exceeded",
6915
+ status: 429,
6916
+ detail: "post.delete denied for subject u_123: member (limit). Alternatives: post.read, post.update.",
6917
+ permission: "post.delete",
6918
+ scope: "post:delete",
6919
+ resource: { type: "post" },
6920
+ denials: [{
6921
+ role: "member",
6922
+ reason: "limit",
6923
+ detail: {
6924
+ count: 10,
6925
+ per: 60,
6926
+ resetsAt: "2030-03-17T17:46:40.000Z"
6927
+ }
6928
+ }],
6929
+ alternatives: ["post.read", "post.update"]
6930
+ },
6931
+ "limit-unavailable": {
6932
+ type: "https://permdock.com/problems/limit-unavailable",
6933
+ title: "Rate limit unavailable",
6934
+ status: 503,
6935
+ detail: "post.delete denied for subject u_123: member (limit-unavailable). Alternatives: post.read, post.update.",
6936
+ permission: "post.delete",
6937
+ scope: "post:delete",
6938
+ resource: { type: "post" },
6939
+ denials: [{
6940
+ role: "member",
6941
+ reason: "limit-unavailable"
6942
+ }],
6943
+ alternatives: ["post.read", "post.update"]
6944
+ },
6945
+ "invalid-signature": {
6946
+ type: "https://permdock.com/problems/invalid-signature",
6947
+ title: "Invalid signature",
6948
+ status: 403,
6949
+ detail: "HTTP Message Signature did not verify"
6950
+ },
6951
+ "approver-not-eligible": {
6952
+ type: "https://permdock.com/problems/approver-not-eligible",
6953
+ title: "Approver not eligible",
6954
+ status: 403,
6955
+ detail: "approver is the principal of this request",
6956
+ reason: "approver-is-principal"
6957
+ }
6958
+ });
6959
+ //#endregion
6960
+ export { AGENT_THROWING_ROW, AGENT_THROWING_USER, PROBLEM_EXAMPLES, authzenTodoData, authzenTodoPermissions, authzenTodoPolicy, authzenTodoUsers, authzenTodoVectors, describePolicy, expectTypeOf, jwtFixtureAudience, jwtFixtureIssuer, jwtFixtureJwks, jwtFixtureTokens, snapshotFixture, supabaseClaimFixtures, supabaseClaimVectors, supabaseHookManifestFixture, supabaseMembershipsBudget, testAccessReviewStore, testAgentAdapter, testAgentRuntime, testApprovalDelegationSource, testApprovalPolicySource, testApprovalStore, testApprovalWaiter, testAuthzen, testAutomationSource, testClientParity, testClientStore, testCloudApi, testCredentialVerifier, testCustomRoleSource, testDecisionProvider, testDecisionSink, testDeliveryTransport, testDirectoryStore, testEntitlementSource, testHttpAdapter, testIdempotencyStore, testLegacyParity, testLimitStore, testMembershipSource, testOrmParity, testPolicySource, testPrincipalSource, testRelationSource, testReplayStore, testRevocationFeed, testRlsParity, testSetQueue, testSettingsSource, testSnapshotSource, testSnapshotStorage, testSubjectResolver, testSupportSessionSource, testTokenSigner, testTokenVerifier, testWhereCompiler };