permdock 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/dist/{2020-C9VN-x8c.js → 2020-CJbO8Bvz.js} +1 -3
- package/dist/a2a/index.d.ts +7 -7
- package/dist/a2a/index.js +9 -9
- package/dist/action-result-CL-QCITA.d.ts +19 -0
- package/dist/{adapter-context-CRhqyzCk.js → adapter-context-CIQpQxbE.js} +8 -2
- package/dist/{adoption-scan-DCYe60Pi.js → adoption-scan-BLs67P1l.js} +5 -5
- package/dist/agent/index.d.ts +3 -3
- package/dist/agent/index.js +1 -1
- package/dist/ai-sdk/index.d.ts +31 -10
- package/dist/ai-sdk/index.js +22 -12
- package/dist/{app-data-MuyzVqX8.js → app-data-UNNm07jV.js} +10 -7
- package/dist/{approval-header-CJACtHkv.js → approval-header-8QV6MU4H.js} +6 -3
- package/dist/{approval-prompt-Dg5LeAcT.js → approval-prompt-B8hQY69x.js} +5 -6
- package/dist/{approval-prompt-DvSSPQJX.d.ts → approval-prompt-PYagyQUj.d.ts} +13 -11
- package/dist/approvals/index.d.ts +5 -5
- package/dist/approvals/index.js +7 -7
- package/dist/{approvals-p1PGoVjb.js → approvals-DnIBTuXE.js} +128 -78
- package/dist/{approvers-CqPOLD-o.js → approvers-B4r0N_Uz.js} +8 -2
- package/dist/{arazzo-41WuzHou.js → arazzo-3RhGfkAK.js} +2 -2
- package/dist/{arazzo-CaUVBn7D.js → arazzo-B5-ulWif.js} +6 -6
- package/dist/{args-IIolxxKA.js → args-0FrVs5EH.js} +125 -8
- package/dist/astro/index.d.ts +30 -12
- package/dist/astro/index.js +2 -2
- package/dist/{audience-DabClFLQ.js → audience-BAkBDMqn.js} +8 -8
- package/dist/{auth-DRLEizbL.js → auth--yPyZNgr.js} +21 -10
- package/dist/authzen/index.d.ts +171 -25
- package/dist/authzen/index.js +321 -149
- package/dist/{automation-BNM3TtDo.d.ts → automation-DqEoWHBc.d.ts} +42 -32
- package/dist/{automation-DVjOckjV.js → automation-x3KEUusw.js} +22 -21
- package/dist/base-url-BQbddZO5.js +9 -0
- package/dist/better-auth/index.d.ts +2 -2
- package/dist/better-auth/index.js +13 -7
- package/dist/better-supabase/index.d.ts +392 -220
- package/dist/better-supabase/index.js +293 -152
- package/dist/{bind-DOyrv6zi.d.ts → bind-Cp2hBHzP.d.ts} +27 -12
- package/dist/{bind-CAvgUuKs.js → bind-D_sZSVA3.js} +22 -10
- package/dist/{body-CH5of8Ne.js → body-CJQwYVa8.js} +3 -3
- package/dist/{body-text-QcXok_SI.js → body-text-DQhcmqdY.js} +8 -4
- package/dist/{boundary-BM8a_P6E.js → boundary-BV3O1eud.js} +8 -2
- package/dist/{boundary-BmHxVH1J.d.ts → boundary-C9ck-3uK.d.ts} +13 -4
- package/dist/{budget-NbEtbJC_.js → budget-BELja5A9.js} +6 -3
- package/dist/{budget-JqcRqyhK.d.ts → budget-hYsPz_yE.d.ts} +6 -3
- package/dist/{build-instance-Bu_eDqYw.js → build-instance-CNWaYJyr.js} +3 -3
- package/dist/{canonical-json-BC8_nMTw.js → canonical-json-DwilZ1iB.js} +6 -3
- package/dist/{capability-N1K9IpqS.js → capability-BqX94r07.js} +31 -25
- package/dist/{capability-Drg-1YhL.d.ts → capability-ChuiONQU.d.ts} +41 -29
- package/dist/{casbin-Dyu2gnUD.js → casbin-H0rKxBx2.js} +16 -8
- package/dist/{casbin-Cr7RLtA3.js → casbin-cbSbhT_k.js} +2 -2
- package/dist/{casl-64gpT-8x.js → casl-C4OINZHI.js} +2 -2
- package/dist/{casl-DjEDhTCP.js → casl-CHHoCldP.js} +19 -10
- package/dist/catalog/index.d.ts +104 -23
- package/dist/catalog/index.js +117 -8
- package/dist/{catalog-HD2gf5mY.js → catalog-BFpz2EmT.js} +57 -26
- package/dist/{catalog-DZVbrF-z.js → catalog-DCspHDc8.js} +3 -3
- package/dist/{catalog-doc-Cdn7-0pQ.js → catalog-doc-DjMKzJp9.js} +53 -16
- package/dist/{catalog-fingerprint-CCP0-yQS.js → catalog-fingerprint-e6aCWosn.js} +19 -15
- package/dist/catalog-mRQQQhNK.js +1 -0
- package/dist/catalog-operations-Dadrl7Z-.js +60 -0
- package/dist/{create-CQWIpUG-.js → challenge-DImNcR7B.js} +157 -39
- package/dist/chat/index.d.ts +1 -1
- package/dist/chat/index.js +13 -10
- package/dist/chat-sdk/index.d.ts +44 -26
- package/dist/chat-sdk/index.js +17 -17
- package/dist/checks-CGkP3Z7A.js +40 -0
- package/dist/{claims-map-7oQ3ue4X.js → claims-map-CKkoAAYF.js} +5 -2
- package/dist/{claims-map-Ca-QX8C6.d.ts → claims-map-D1Pb5oRq.d.ts} +8 -5
- package/dist/claude-agent/index.d.ts +6 -6
- package/dist/claude-agent/index.js +6 -3
- package/dist/clerk/index.d.ts +11 -7
- package/dist/clerk/index.js +6 -6
- package/dist/cli/bin.js +4 -4
- package/dist/cli/index.d.ts +82 -7
- package/dist/cli/index.js +6 -6
- package/dist/{clients-C7zLFuWT.d.ts → clients-C_NOET3z.d.ts} +5 -5
- package/dist/{clients-DjU5ABMy.js → clients-DYr3U86j.js} +3 -3
- package/dist/cloud/index.d.ts +186 -38
- package/dist/cloud/index.js +293 -31
- package/dist/{cloud-ytQ3uviA.js → cloud-BFlGMaak.js} +5 -6
- package/dist/{codemod-5mpSpnUY.js → codemod-DLQ9Zpo4.js} +3 -4
- package/dist/{codemod-ua2hHy7w.js → codemod-DWSN2erG.js} +523 -201
- package/dist/collect-B5BL3klN.js +1 -0
- package/dist/{collect-mLTY4nwb.js → collect-B7ZFAJU3.js} +36 -64
- package/dist/{collect-Ci6akVwH.js → collect-Bzv3le0A.js} +6 -6
- package/dist/{compare-pfyVnBjE.js → compare-DTfyUunF.js} +2 -2
- package/dist/compile/index.d.ts +2 -2
- package/dist/compile/index.js +1 -1
- package/dist/{compile-1bsAezL0.d.ts → compile-BFzysW3o.d.ts} +36 -12
- package/dist/{compile-CMqMx_IN.js → compile-EzEMB-7v.js} +77 -33
- package/dist/{completion-BTFrHZhX.js → completion-saoYiRFO.js} +17 -8
- package/dist/config-BwPBfN0t.js +108 -0
- package/dist/{config-Bj2Ixq3d.js → config-DBeXXmBF.js} +2 -2
- package/dist/{config-report-DSpsLpCE.js → config-report-BipSWA6V.js} +9 -6
- package/dist/config-shape-ykV0Vm2n.js +134 -0
- package/dist/{connection-DjhIJSs2.js → connection-B72dg5Q2.js} +23 -22
- package/dist/connector-schema-CYLQNMYq.js +434 -0
- package/dist/{context-B_u3_bDJ.js → context-BnIzxTNE.js} +4 -3
- package/dist/convex/index.d.ts +13 -7
- package/dist/convex/index.js +9 -6
- package/dist/core-H-mbhpZc.js +341 -0
- package/dist/core-hWIARPDz.d.ts +206 -0
- package/dist/{create-CBMmFie-.d.ts → create-BSNrVuxh.d.ts} +87 -34
- package/dist/{create-B30OMUOJ.d.ts → create-BVq1lNQE.d.ts} +49 -28
- package/dist/{create-D_NmPOdH.d.ts → create-CM1FciYG.d.ts} +37 -29
- package/dist/{create-CGm28gyI.js → create-Cq4Uox0f.js} +13 -9
- package/dist/{create-DCF2SvoT.js → create-_noSrY1T.js} +15 -21
- package/dist/{credential-BcoiejpY.js → credential-CyyyvCsL.js} +42 -39
- package/dist/{credentials-DPyHmYz5.js → credentials-CdDxp7jN.js} +6 -6
- package/dist/{deciding-columns-Sq12mEA6.js → deciding-columns-NB4wcyBf.js} +13 -2
- package/dist/{delegation-j6-rc37o.js → delegation-CDB6_keF.js} +31 -22
- package/dist/delivery/index.d.ts +5 -5
- package/dist/delivery/index.js +6 -5
- package/dist/{delivery-DCAgNyCn.js → delivery-BupfvJIV.js} +81 -20
- package/dist/{delivery-event-xX4obB8F.js → delivery-event-JioK5YbS.js} +6 -6
- package/dist/denied-text-CHNlVInN.d.ts +22 -0
- package/dist/{describe-BybMRY7A.js → describe-Bb4hGK5d.js} +6 -5
- package/dist/{describe-B5qgDvEW.d.ts → describe-ByS9LgHK.d.ts} +5 -5
- package/dist/{device-BARQ8eYG.js → device-BwbJNmb2.js} +11 -8
- package/dist/{diff-CmuSD3vc.js → diff-CN8IYhC_.js} +2 -2
- package/dist/{diff-3jbx6Rm4.js → diff-iZWV65nC.js} +23 -14
- package/dist/{digest-B3kc_PgQ.d.ts → digest-8Yx2CchM.d.ts} +3 -3
- package/dist/{doctor-BdZw53-k.js → doctor-CHD1M9R5.js} +485 -141
- package/dist/{doctor-3ZqNHX_G.js → doctor-D3wo-yuz.js} +4 -4
- package/dist/drizzle/index.d.ts +33 -19
- package/dist/drizzle/index.js +17 -9
- package/dist/elysia/index.d.ts +17 -11
- package/dist/elysia/index.js +5 -5
- package/dist/{errors-BKy0EfhN.js → errors-B6R_lg2a.js} +13 -4
- package/dist/{errors-BWXBMaKK.d.ts → errors-CHwoRRpB.d.ts} +4 -1
- package/dist/{errors-CWTY6uih.js → errors-Vpu6rKHl.js} +14 -10
- package/dist/{errors-DH5QEGXh.d.ts → errors-zi_4T-rx.d.ts} +22 -6
- package/dist/{evaluate-CNClBvcj.js → evaluate-Dd8Ta0Ie.js} +185 -80
- package/dist/{evaluations-beAooV0n.js → evaluations-IBceAeKT.js} +41 -20
- package/dist/{evaluations-C_1vdSZc.d.ts → evaluations-z8dQFDaN.d.ts} +12 -6
- package/dist/eve/index.d.ts +9 -6
- package/dist/eve/index.js +4 -4
- package/dist/{event-time-CsegxXxB.js → event-time-BGy48400.js} +8 -4
- package/dist/{expiry-D4_YKRKD.js → expiry-6XByK0dm.js} +8 -5
- package/dist/express/index.d.ts +9 -6
- package/dist/express/index.js +9 -6
- package/dist/fastify/index.d.ts +16 -10
- package/dist/fastify/index.js +8 -5
- package/dist/{fetch-adapter-Dw0MTS3D.d.ts → fetch-adapter-C1DCvpqV.d.ts} +9 -6
- package/dist/{fetch-adapter-DD_gfmrC.js → fetch-adapter-JL43jD3E.js} +14 -11
- package/dist/fields-D0vUI6dq.js +498 -0
- package/dist/{files-C5ZI27eV.js → files-B3AA05f1.js} +21 -5
- package/dist/{files-D_JIzUrP.js → files-BSjrtIHw.js} +11 -4
- package/dist/{fixtures-B7XrLeY7.js → fixtures-A1_sp_tR.js} +15 -6
- package/dist/{flags-C3DktBDY.js → flags-DXjoTVbS.js} +2 -2
- package/dist/{freeze-CrU8nQeX.js → freeze-jjan0lCv.js} +4 -1
- package/dist/{from-snapshot-CTCMsFZk.js → from-snapshot-DCakWRsr.js} +26 -16
- package/dist/gen-B5kvbAoZ.js +58 -0
- package/dist/gen-DWCYcMQM.js +827 -0
- package/dist/generate-CRVV5V7l.js +165 -0
- package/dist/generate-CezbCaTI.js +275 -0
- package/dist/{grantee-hxrUsbpM.js → grantee-Dke-9rd1.js} +4 -4
- package/dist/{grantee-conditions-CcXf8hMM.js → grantee-conditions-BrYPe-w0.js} +12 -12
- package/dist/{grantee-match-DbENBHLB.js → grantee-match-7RZZvoT_.js} +12 -6
- package/dist/{graph-sql-B-dsAT6j.d.ts → graph-sql-BwFC4Qjs.d.ts} +3 -3
- package/dist/h3/index.d.ts +10 -7
- package/dist/h3/index.js +3 -3
- package/dist/headers-B9jQxjtW.d.ts +10 -0
- package/dist/{helpers-CBYVMjwh.js → helpers-DJicaG6f.js} +40 -28
- package/dist/hono/index.d.ts +16 -10
- package/dist/hono/index.js +5 -5
- package/dist/{hosted-BPk51-kX.d.ts → hosted-Cmtxogd0.d.ts} +44 -20
- package/dist/{hosted-B46-5LYE.js → hosted-D0IzESu_.js} +68 -25
- package/dist/{http-ujKm0JXE.js → http-DH94eh6f.js} +4 -4
- package/dist/{http-BBoKefeS.js → http-DKLXrFGk.js} +13 -7
- package/dist/{http-DXJ37RDA.js → http-DOL33cO0.js} +11 -8
- package/dist/{import-wyqXUyjm.js → import-CST57XLp.js} +64 -51
- package/dist/{import-CNV7I2Dw.js → import-_YqhhwvX.js} +6 -6
- package/dist/{index-B8YjkEZ4.d.ts → index-B8s204GL.d.ts} +64 -52
- package/dist/{index-Ch8LpNDp.d.ts → index-BT5-1VsX.d.ts} +64 -25
- package/dist/{index-DuWxc5dt.d.ts → index-BW_0MZOS.d.ts} +388 -149
- package/dist/index-BhxEUY9J.d.ts +233 -0
- package/dist/index-Bv5iXPqU.d.ts +210 -0
- package/dist/{index-C-6Letxg.d.ts → index-_9UiQIkt.d.ts} +164 -77
- package/dist/index.d.ts +22 -21
- package/dist/index.js +35 -33
- package/dist/{init-GG6Jh8HD.js → init-Dz_PsHdv.js} +5 -5
- package/dist/{init-DeMYsnYJ.js → init-cE3A8aXq.js} +10 -10
- package/dist/{instance-options-BsVpyGIk.d.ts → instance-options-B0KXqTrJ.d.ts} +4 -4
- package/dist/{instance-snapshot-BOEhcqIF.js → instance-snapshot--JWiTrvO.js} +81 -56
- package/dist/{instant-BluqDvyS.js → instant-DK3Iz83e.js} +2 -2
- package/dist/{internals-Dk8A8fX3.js → internals-B3IgHrvo.js} +6 -3
- package/dist/{json-schema-B-vAwlde.js → json-schema-BPGa-RjQ.js} +9 -3
- package/dist/{json-schema-CN_iLFH2.d.ts → json-schema-BfXgnRNT.d.ts} +4 -4
- package/dist/jwt/index.d.ts +125 -17
- package/dist/jwt/index.js +157 -16
- package/dist/{kernel-D0NVb1cK.js → kernel-C-fYydV2.js} +151 -16
- package/dist/kysely/index.d.ts +49 -33
- package/dist/kysely/index.js +9 -7
- package/dist/{live-options-BaAFCIiu.js → live-options-BBIT68le.js} +2 -2
- package/dist/{load-DFS_ry0W.js → load-DjSU5KXO.js} +6 -6
- package/dist/load-Dm0pSYH3.js +1 -0
- package/dist/{login-Dfe5RlYk.js → login-D-8tMqzo.js} +4 -4
- package/dist/{login-imhuwvn3.js → login-DplQ0s84.js} +18 -15
- package/dist/{manifest-dvL2XEe1.js → manifest-Cgiq5oqc.js} +67 -37
- package/dist/{manifest-mx5ghSFM.d.ts → manifest-CzzWtnAb.d.ts} +144 -28
- package/dist/{manifest-schema-C0kK_8V0.js → manifest-schema-2r37XRhD.js} +80 -9
- package/dist/{manifest-C8IzW3Do.js → manifest-wg8WkkpD.js} +11 -7
- package/dist/{map-claims-D3oRmTk0.js → map-claims-BNWCJtmP.js} +27 -18
- package/dist/{map-error-Dr70oA_3.js → map-error-pasIGqMT.js} +7 -7
- package/dist/{markers-C6joDI3o.js → markers-BeHZ4x9Q.js} +20 -5
- package/dist/mcp/index.d.ts +154 -47
- package/dist/mcp/index.js +200 -41
- package/dist/{migrate-CIq4-E9F.js → migrate-B7oP_sQU.js} +102 -31
- package/dist/module-values-BJTBdo6y.js +107 -0
- package/dist/nest/index.d.ts +23 -13
- package/dist/nest/index.js +18 -11
- package/dist/next/client.d.ts +5 -2
- package/dist/next/client.js +1 -1
- package/dist/next/index.d.ts +72 -26
- package/dist/next/index.js +110 -6
- package/dist/next/plugin.d.ts +1 -1
- package/dist/next/plugin.js +4 -3
- package/dist/node/index.d.ts +5 -5
- package/dist/node/index.js +3 -3
- package/dist/{ocsf-BFp-PZZA.js → ocsf-TcjRBgIs.js} +15 -5
- package/dist/openai/index.d.ts +13 -7
- package/dist/openai/index.js +3 -3
- package/dist/openapi/index.d.ts +61 -30
- package/dist/openapi/index.js +2 -2
- package/dist/{openapi-B7HZ0f-r.js → openapi-BOsx0xn9.js} +34 -24
- package/dist/{openapi-C4dDTVEd.js → openapi-CBZUhA3l.js} +4 -5
- package/dist/{openapi-DD1SV-5-.js → openapi-VeHHU2l9.js} +23 -14
- package/dist/{operation-findings-DgATDpvh.js → operation-findings-CXtFzlKJ.js} +3 -3
- package/dist/{operations-X0VM7F3m.d.ts → operations-Ci_rT6Uo.d.ts} +10 -7
- package/dist/orpc/index.d.ts +68 -14
- package/dist/orpc/index.js +62 -13
- package/dist/otel/index.d.ts +7 -4
- package/dist/otel/index.js +8 -5
- package/dist/output-files-BHOIlrfD.js +1 -0
- package/dist/{output-files-DhRBBE4n.js → output-files-BIQ_9pIs.js} +29 -17
- package/dist/{output-1im__-08.js → output-tkju6U7R.js} +15 -7
- package/dist/ownership-B4L59T9u.js +1054 -0
- package/dist/parse-1_ZheTW5.js +151 -0
- package/dist/parse-local-manifest-BQhTlLQS.d.ts +10 -0
- package/dist/parse-local-manifest-DEiCF4ah.js +69 -0
- package/dist/{parse-request-D_tUXG6M.js → parse-request-CwCZpyjV.js} +7 -8
- package/dist/{paths-DXSHAMIa.js → paths-CUvURzHp.js} +4 -1
- package/dist/pdp/index.d.ts +9 -6
- package/dist/pdp/index.js +23 -12
- package/dist/{peer-CRvC7Hg1.js → peer-C9LvBCcb.js} +1 -1
- package/dist/peer-Q0fuZ6hx.js +982 -0
- package/dist/{permdock-SV8ZINYT.d.ts → permdock-B6Sf4T_S.d.ts} +270 -117
- package/dist/{permdock-BrAscV28.js → permdock-L-_I_3zP.js} +226 -74
- package/dist/{permission-registry-Br4jSNVi.js → permission-registry-B8tYxPIt.js} +24 -11
- package/dist/{vocabulary-CN8H_9UF.d.ts → permission-types-CAY-GU7e.d.ts} +130 -104
- package/dist/{permissions-u_KHg-sN.d.ts → permissions-Bx6UI8n1.d.ts} +6 -2
- package/dist/{permissions-B-wCP5ZC.js → permissions-CVXmRILq.js} +3 -3
- package/dist/{permissions-BPxlwVhN.js → permissions-mDcTWoBs.js} +8 -5
- package/dist/pg-DQTqRrou.js +60 -0
- package/dist/{pins-8ILTftC9.js → pins-nqarl3oE.js} +4 -1
- package/dist/pkce-CnmmYqUZ.js +21 -0
- package/dist/{policy-77FSAq_V.d.ts → policy-Ba4bDhaj.d.ts} +5 -5
- package/dist/{policy-DbM5YYtV.js → policy-CLQ8__GF.js} +49 -20
- package/dist/{policy-from-catalog-C2gMa2Dj.js → policy-from-catalog-CJgOm6aq.js} +28 -25
- package/dist/{policy-module-CTlwMvLI.js → policy-module-xym9fnSq.js} +12 -6
- package/dist/{policy-types-n0uRTsW8.d.ts → policy-types-CCiDRZBH.d.ts} +802 -334
- package/dist/postgres-store-DcyrPS-r.js +1028 -0
- package/dist/{postgrest-DAjUkF9w.js → postgrest-D4nuPXIQ.js} +88 -36
- package/dist/{postgrest-CEgRbefE.d.ts → postgrest-DicQyqm-.d.ts} +60 -30
- package/dist/powersync-DHNQX06W.js +1117 -0
- package/dist/{powersync-DISt6SUe.js → powersync-DXqGuq9A.js} +3 -3
- package/dist/powersync-Djf-zCpB.js +1 -0
- package/dist/preset/index.d.ts +38 -28
- package/dist/preset/index.js +1 -1
- package/dist/{preset-DZdU0tne.js → preset-BjLzeFpp.js} +58 -34
- package/dist/{principal-source-pHneukrx.js → principal-source-C6N60GtM.js} +10 -10
- package/dist/prisma/index.d.ts +69 -41
- package/dist/prisma/index.js +21 -17
- package/dist/{problem-CTviyH5L.js → problem-BVLHFK33.js} +41 -21
- package/dist/{problem-details-cn5X30gZ.js → problem-details-CLDgX1KV.js} +13 -4
- package/dist/{problem-types-CJ2325SZ.js → problem-types-DSsRfONQ.js} +4 -1
- package/dist/prompt-BYc6drYW.js +938 -0
- package/dist/protected-action-BE7N_Luo.d.ts +58 -0
- package/dist/protected-action-DUYqb-Ma.js +80 -0
- package/dist/{protected-BlF0F6wL.js → protected-action-QEA8arRB.js} +48 -14
- package/dist/{provider-B1al-AZz.js → provider-C55OGXJ7.js} +5 -4
- package/dist/{provider-DILL9oPK.d.ts → provider-DXfq5c2d.d.ts} +1 -1
- package/dist/{provider-store-DM6DNcta.js → provider-store-CaTv00tr.js} +13 -9
- package/dist/pull-B65ndFNZ.js +1 -0
- package/dist/{pull-jEwbf80J.js → pull-CwpGaQSi.js} +12 -11
- package/dist/{push-BD4f15co.js → push-DgssgzkK.js} +62 -16
- package/dist/{pushed-policy-DCW91KQU.d.ts → pushed-policy-D1Eoi6NM.d.ts} +12 -6
- package/dist/{pushed-policy-CqtxkV4G.js → pushed-policy-ITwYKvvH.js} +10 -7
- package/dist/react/index.d.ts +9 -8
- package/dist/react/index.js +7 -6
- package/dist/react/provider-client.d.ts +1 -1
- package/dist/react/provider-client.js +1 -1
- package/dist/react-native/index.d.ts +104 -51
- package/dist/react-native/index.js +394 -67
- package/dist/{refs-K6eWUJ0H.js → refs-C8BKxc_e.js} +17 -3
- package/dist/{relation-walk-3OyDBF0S.js → relation-walk-DK_gkgL7.js} +12 -9
- package/dist/{relations-BnCWnJGI.js → relations-BxvRvDHK.js} +3 -3
- package/dist/{replay-Du1tJjG7.js → replay-BU09ad4L.js} +1 -1
- package/dist/{replay-Bkm-Ne4l.d.ts → replay-DNftIb_R.d.ts} +1 -1
- package/dist/{request-context-C81Pip9n.js → request-context-C25ilqiJ.js} +8 -8
- package/dist/{request-schema-CMreVuFM.js → request-schema-DhX7UbfQ.js} +4 -1
- package/dist/resend/index.d.ts +34 -28
- package/dist/resend/index.js +29 -29
- package/dist/resolve-names-CF7xXLSG.js +201 -0
- package/dist/{resource-relations-D_jw0BFx.js → resource-relations-8-pCScBO.js} +18 -9
- package/dist/{retire-CALjIQ7e.js → retire-B2xffQgY.js} +15 -6
- package/dist/{revocations-R07lFBnX.d.ts → revocations-BaUXHM1N.d.ts} +6 -3
- package/dist/{rls-DTvYd-lI.js → rls-CNEH2_BG.js} +743 -624
- package/dist/{rls-BAqP4q66.js → rls-Ca8Jz9b5.js} +41 -10
- package/dist/{role-builder-uWQZ4v84.js → role-builder-BK2-T6oD.js} +41 -20
- package/dist/rolldown-runtime-C3I60J0A.js +4 -0
- package/dist/{row-conditions-h2HythXQ.js → row-conditions-BAGHOmVq.js} +4 -3
- package/dist/{rpc-VSl5ylaQ.js → rpc-fNqAIqQk.js} +8 -5
- package/dist/{run-CUKuz0A8.js → run-Bz26OTfO.js} +26 -23
- package/dist/run-D-JE8QWb.js +1 -0
- package/dist/{run-ticket-DocwaTKn.d.ts → run-ticket-DbWI9f99.d.ts} +33 -13
- package/dist/{run-ticket-sxFI6LAk.js → run-ticket-Dv7aF7Dk.js} +29 -8
- package/dist/{runtime-D3VTAiCA.js → runtime-BGnrOkVH.js} +5 -2
- package/dist/{saas-BCXRuXca.js → saas-iWnUdwCt.js} +42 -29
- package/dist/{scan-qvOe0hS3.js → scan-DCj1KQfn.js} +1 -1
- package/dist/{schema-B4v0Qrac.js → schema-BOJjqlWL.js} +3 -2
- package/dist/{schema-yLuBWWsh.js → schema-Dst9I9Gs.js} +9 -5
- package/dist/scim/index.d.ts +3 -2
- package/dist/scim/index.js +3 -1
- package/dist/scim-D8YupzX1.js +44 -0
- package/dist/{scim-ClHtpyV3.js → scim-jJBJ7g1Z.js} +35 -551
- package/dist/{scopes-CUPSjo9q.js → scopes-0Up_UA2t.js} +42 -24
- package/dist/server/index.d.ts +12 -12
- package/dist/server/index.js +13 -13
- package/dist/{server-C3WXAHZj.js → server-3cVlTLde.js} +31 -31
- package/dist/{server-types-CKQiCnON.d.ts → server-types-CIlpSA2G.d.ts} +81 -46
- package/dist/{sha256-D9ihqmvd.js → sha256-C-4WXKIu.js} +4 -3
- package/dist/{shared-BAK8t4Uo.js → shared-Afctg7B0.js} +100 -61
- package/dist/{shared-DvvFzYXe.js → shared-l0oGGetg.js} +9 -6
- package/dist/{signals-D3NcTKUS.js → signals-B1vHR3fN.js} +2 -2
- package/dist/{sink-BEZSlMS_.js → sink-D9_vh372.js} +32 -21
- package/dist/{sink-4-eQIRWf.d.ts → sink-Dvi7YZG_.d.ts} +44 -20
- package/dist/{skills-B6lVdD_W.js → skills-Bjj7BNEQ.js} +16 -12
- package/dist/{skills-CTYwkl44.js → skills-OuoMR49L.js} +3 -3
- package/dist/{snapshot-B7DkYg6g.js → snapshot-CXlzi69u.js} +21 -18
- package/dist/{snapshot-cache-PXI-vGpY.d.ts → snapshot-cache-BAXZ0KOp.d.ts} +21 -12
- package/dist/{snapshot-cache-DZHQ0KF6.js → snapshot-cache-Bx_4xC_6.js} +14 -11
- package/dist/solid/index.d.ts +55 -15
- package/dist/solid/index.js +46 -12
- package/dist/solid-start/index.d.ts +30 -12
- package/dist/solid-start/index.js +3 -3
- package/dist/sources-B1Os_YUu.d.ts +312 -0
- package/dist/{sources-vs5dGOsS.js → sources-CMAM-2A3.js} +156 -51
- package/dist/sources-qoIJQrJv.js +11 -0
- package/dist/{sql-B5T1IL83.js → sql-0WjxIp9g.js} +32 -7
- package/dist/sql-B80DFdpG.d.ts +10 -0
- package/dist/{src-Cu2m75uo.js → src-DZ0l9UAg.js} +305 -123
- package/dist/ssf/index.d.ts +4 -4
- package/dist/ssf/index.js +665 -224
- package/dist/{standard-webhooks-rbD7LS6h.d.ts → standard-webhooks-C-qX4mJW.d.ts} +7 -7
- package/dist/{standard-webhooks-DgFFD-Ga.js → standard-webhooks-D67a3nzk.js} +5 -5
- package/dist/{store-BDutJkDL.js → store-RNFvaJJv.js} +43 -29
- package/dist/{store-options-u0ycEe8s.js → store-options-Dil3lraY.js} +105 -15
- package/dist/stores-C0Kta5Ft.d.ts +59 -0
- package/dist/{stores-CGhM8BN_.js → stores-DxZWDXCs.js} +42 -5
- package/dist/{stream-CvdUS9V1.js → stream-CDvF0sJm.js} +5 -5
- package/dist/{stream-CeCMJmBB.d.ts → stream-ltuLdtTG.d.ts} +4 -3
- package/dist/{style-DiKWnAvn.js → style-DblOtSga.js} +1 -1
- package/dist/{style-Uao76wO7.js → style-Ds11Hmk4.js} +3 -1
- package/dist/{subject-settings-CkEwrw3W.js → subject-settings-8sH5HlII.js} +8 -4
- package/dist/{row-check-Dt35czLy.d.ts → subject-settings-vNW8qmUH.d.ts} +30 -18
- package/dist/supabase/index.d.ts +48 -37
- package/dist/supabase/index.js +6 -6
- package/dist/supabase/middleware.d.ts +19 -13
- package/dist/supabase/middleware.js +7 -7
- package/dist/{supabase-x5JtR2tc.js → supabase-BL1qJzzY.js} +82 -36
- package/dist/{supabase-By_frk3U.js → supabase-D39cuGPt.js} +3 -3
- package/dist/{supabase-hook-BMoEhZir.js → supabase-hook-B-yXnzUl.js} +1206 -286
- package/dist/supabase-hook-H-MtE9Wd.js +1 -0
- package/dist/supautils-BU-pAEpf.js +808 -0
- package/dist/{support-session-C6xJQYut.js → support-session-BsmPCbz8.js} +22 -22
- package/dist/svelte/PermissionBoundary.svelte +2 -6
- package/dist/svelte/Protected.svelte +2 -8
- package/dist/svelte/index.d.ts +9 -8
- package/dist/svelte/index.js +10 -9
- package/dist/svelte/runtime.d.ts +4 -4
- package/dist/svelte/runtime.js +4 -4
- package/dist/svelte/source.d.ts +9 -8
- package/dist/svelte/source.js +6 -5
- package/dist/sveltekit/index.d.ts +30 -12
- package/dist/sveltekit/index.js +2 -2
- package/dist/{ownership-DqaJNJPn.js → tables-DeMNEy6V.js} +1025 -1085
- package/dist/tanstack-start/index.d.ts +22 -10
- package/dist/tanstack-start/index.js +2 -2
- package/dist/{target-Bw37TFNA.js → target-qdEAnnhU.js} +6 -6
- package/dist/{tenancy-DN2QAx9z.js → tenancy-5d1zVBvK.js} +36 -21
- package/dist/terminal/index.d.ts +23 -14
- package/dist/terminal/index.js +17 -20
- package/dist/testing/core.d.ts +2 -0
- package/dist/testing/core.js +1 -0
- package/dist/testing/fixtures/index.d.ts +15 -0
- package/dist/testing/fixtures/index.js +831 -0
- package/dist/testing/index.d.ts +235 -285
- package/dist/testing/index.js +502 -927
- package/dist/testing/saas/index.d.ts +43 -27
- package/dist/testing/saas/index.js +2 -2
- package/dist/testing/saas/permissions.d.ts +1 -1
- package/dist/testing/saas/permissions.js +1 -1
- package/dist/text-diff-BtfSmdUo.js +444 -0
- package/dist/{thenable-yD7SabqG.js → thenable-BNSM1U2R.js} +12 -3
- package/dist/{time-DkWjdL98.js → time-nR1JesoH.js} +32 -8
- package/dist/{token-BLZcjQgt.js → token-DC9EQiet.js} +15 -13
- package/dist/{tool-refusal-CMLN5xhn.js → tool-refusal-Qpjs_W6M.js} +5 -2
- package/dist/trpc/index.d.ts +11 -8
- package/dist/trpc/index.js +15 -9
- package/dist/type-helpers-BIPIDa6a.d.ts +50 -0
- package/dist/{types-8RnwUAvE.d.ts → types-AMJ5qoU_.d.ts} +49 -30
- package/dist/{types-C_IiEs64.d.ts → types-B2mlGuhW.d.ts} +14 -11
- package/dist/{types-_WhUP7ud.d.ts → types-B4uqKDnp.d.ts} +25 -12
- package/dist/{types-CX7AvRqP.d.ts → types-BJk0BKRX.d.ts} +24 -14
- package/dist/types-B_d_g7T3.d.ts +262 -0
- package/dist/{types-BdtSDbBO.d.ts → types-B_tiYvk8.d.ts} +59 -32
- package/dist/{types-DqzsnlSA.js → types-CcUXciSj.js} +17 -5
- package/dist/{types-Dfv0aTER.d.ts → types-DC1Loeh3.d.ts} +449 -168
- package/dist/{types-lUlG3TVd.d.ts → types-DHKPcqzr.d.ts} +2 -2
- package/dist/{types-C791HFu9.d.ts → types-DjuMBNF9.d.ts} +68 -32
- package/dist/{types-C863OQiR.d.ts → types-HzqOtvsJ.d.ts} +23 -12
- package/dist/{types-Druu9wLM.d.ts → types-ZzRGoViD.d.ts} +77 -24
- package/dist/{types-BzzaHxt3.d.ts → types-ah9NlU6j.d.ts} +137 -48
- package/dist/{types-CVZSNocA.d.ts → types-djVzLlIV.d.ts} +31 -14
- package/dist/{types-BtHtcKMr.d.ts → types-yUuPR5Hy.d.ts} +47 -24
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +2 -2
- package/dist/usage-CYeXek0p.js +1 -0
- package/dist/{usage-SveTRTyr.js → usage-CxEzS4ey.js} +50 -26
- package/dist/{usage-CRVl9V3t.js → usage-ab0PdusX.js} +2 -2
- package/dist/{validation-error-ZfUHbKvf.js → validation-error-CYg2kJEw.js} +4 -1
- package/dist/{validation-error-DHUrAkku.d.ts → validation-error-YGg2mXyH.d.ts} +5 -2
- package/dist/{validity-CZ7-y0Cj.js → validity-Buc7w8E9.js} +13 -10
- package/dist/{verifier-DHuh-okk.js → verifier-DUV6pLqZ.js} +10 -4
- package/dist/{verify-BaAKx7t6.js → verify-DZeqoWu5.js} +417 -489
- package/dist/version-DMQ0dnN5.js +48 -0
- package/dist/version-DizhmD72.js +1 -0
- package/dist/{views-DTIIO-oQ.js → views-CD8wuL4Z.js} +19 -6
- package/dist/vocabulary-DEJ5j4mR.d.ts +70 -0
- package/dist/vue/index.d.ts +71 -21
- package/dist/vue/index.js +50 -13
- package/dist/{waiter-B986w1oK.d.ts → waiter-Bu9zAlE0.d.ts} +13 -10
- package/dist/{waiter-BGUtGYoU.js → waiter-aBLM63_f.js} +1 -1
- package/dist/{watch-vea54PRw.js → watch-Cx_WciLE.js} +8 -8
- package/dist/watch-DB42U1ib.js +1 -0
- package/dist/webmcp/index.d.ts +11 -11
- package/dist/webmcp/index.js +8 -5
- package/dist/{where-scope-C9vfEphl.js → where-scope-DlCdCM_8.js} +56 -30
- package/dist/{who-can-DWBAcaCQ.js → who-can-rI7dCR9Q.js} +23 -20
- package/dist/workflow/index.d.ts +3 -3
- package/dist/workflow/index.js +13 -13
- package/dist/workflow-sdk/index.d.ts +19 -13
- package/dist/workflow-sdk/index.js +6 -6
- package/dist/{wrap-gX61EtpB.js → wrap-B2wziJI_.js} +11 -5
- package/dist/{wrap-C5jC6sKI.d.ts → wrap-DlJE3gS_.d.ts} +8 -5
- package/package.json +25 -14
- package/schemas/catalog-v1.json +3 -0
- package/schemas/cli-result-v1.json +1 -0
- package/schemas/cloud-api-v1.json +152 -7
- package/schemas/connector-v1.json +8 -0
- package/schemas/supabase-claims-v1.json +5 -0
- package/schemas/supabase-manifest-v1.json +308 -75
- package/skills/permdock/SKILL.md +1 -0
- package/skills/permdock-agents/references/runtimes.md +3 -1
- package/skills/permdock-approvals/references/stores-and-resume.md +2 -1
- package/skills/permdock-credentials/references/tokens-keys-links.md +8 -6
- package/skills/permdock-data/references/queries-and-rls.md +6 -2
- package/skills/permdock-tenancy/SKILL.md +1 -1
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +2 -1
- package/skills/permdock-wire/references/adapters.md +28 -22
- package/skills/permdock-wire/references/policy-options.md +1 -0
- package/dist/catalog-BoWglDPI.js +0 -1
- package/dist/collect-D7eQDffE.js +0 -1
- package/dist/config-bs_kZeYq.js +0 -162
- package/dist/denied-text-CiFQNuMV.d.ts +0 -19
- package/dist/fields-OYG_JR78.js +0 -203
- package/dist/generate-CNU1pIOo.js +0 -171
- package/dist/headers-BIjfMaoC.js +0 -8
- package/dist/headers-CIbDTUTK.d.ts +0 -7
- package/dist/index-Cp_ilKi1.d.ts +0 -91
- package/dist/index-CueFnxQc.d.ts +0 -110
- package/dist/load-BxEtwlBS.js +0 -1
- package/dist/output-files-B3IZJksC.js +0 -1
- package/dist/package-root-Dk_ETdkp.js +0 -31
- package/dist/parse-Cxq0WxrA.js +0 -53
- package/dist/parse-local-manifest-5ODwqu8b.d.ts +0 -10
- package/dist/parse-local-manifest-CVSs6J9I.js +0 -34
- package/dist/peer-C8swJ2GH.js +0 -50
- package/dist/policies-DP2nqRoF.js +0 -295
- package/dist/powersync-BkWWCS1K.js +0 -538
- package/dist/powersync-MMJ2mI2q.js +0 -1
- package/dist/problem-details-B45V5d2b.d.ts +0 -7
- package/dist/prompt-DfZLJLIn.js +0 -32
- package/dist/protected-RFGpakN9.d.ts +0 -38
- package/dist/pull-CvpINcMK.js +0 -1
- package/dist/run-DDwUaJDU.js +0 -1
- package/dist/sources-Xc4zkszI.d.ts +0 -190
- package/dist/stores-BUGNbJTt.d.ts +0 -30
- package/dist/supabase-hook-CCw5m35C.js +0 -1
- package/dist/supautils-B6HYL9eL.js +0 -91
- package/dist/text-diff-BQcUapOb.js +0 -15
- package/dist/types-B3_ooZgZ.d.ts +0 -172
- package/dist/usage-CZpSQP6L.js +0 -1
- package/dist/version-CEbQ5UeM.js +0 -18
- package/dist/version-DXkYUISx.js +0 -1
- package/dist/watch--ovlPOc0.js +0 -1
package/dist/testing/index.js
CHANGED
|
@@ -1,312 +1,70 @@
|
|
|
1
|
-
import { t as require__2020 } from "../2020-C9VN-x8c.js";
|
|
2
1
|
import { r as isRecord } from "../compact-CAf557ZB.js";
|
|
3
|
-
import { n as freezeDeep } from "../freeze-
|
|
4
|
-
import {
|
|
5
|
-
import { n as role, r as allow } from "../role-builder-
|
|
6
|
-
import {
|
|
2
|
+
import { n as freezeDeep } from "../freeze-jjan0lCv.js";
|
|
3
|
+
import { i as principal } from "../refs-C8BKxc_e.js";
|
|
4
|
+
import { n as role, r as allow } from "../role-builder-BK2-T6oD.js";
|
|
5
|
+
import { A as memoryCustomRoleSource, S as memoryLimitStore, T as approvalPoliciesFor, d as snapshotFor, x as limits } from "../src-DZ0l9UAg.js";
|
|
7
6
|
import { t as memoryRevocationFeed } from "../revocations-Bhr0aMIC.js";
|
|
8
|
-
import { n as loadPrincipalByIdentity, t as loadPrincipal } from "../principal-source-
|
|
9
|
-
import {
|
|
10
|
-
import { d as isoNow, f as nowMillis, p as nowSeconds, u as isoAt } from "../time-
|
|
11
|
-
import { r as parseAutomation } from "../automation-
|
|
12
|
-
import {
|
|
13
|
-
import { u as evaluateCondition } from "../delegation-
|
|
14
|
-
import { a as authenticated } from "../grantee-
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import { t as definePermissions } from "../permissions-
|
|
19
|
-
import {
|
|
20
|
-
import { n as parseSnapshot } from "../snapshot-
|
|
21
|
-
import { n as fromSnapshot } from "../from-snapshot-
|
|
22
|
-
import { t as getPermDock } from "../permdock-
|
|
23
|
-
import { t as APPROVAL_HEADER } from "../types-
|
|
24
|
-
import { i as memoryApprovalStore } from "../store-
|
|
25
|
-
import { t as catalogFingerprint } from "../catalog-fingerprint-
|
|
26
|
-
import { s as parseCredential } from "../credential-
|
|
27
|
-
import {
|
|
28
|
-
import { o as resolveApproval } from "../helpers-
|
|
29
|
-
import { t as APPROVAL_REQUEST_SCHEMA } from "../request-schema-
|
|
30
|
-
import { t as catalogSchema } from "../schema-
|
|
31
|
-
import { c as parseApiKey } from "../server-
|
|
32
|
-
import { n as discoverAuthorizationServer } from "../device-
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import { t as
|
|
38
|
-
import { t as
|
|
39
|
-
import {
|
|
40
|
-
import {
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
7
|
+
import { n as loadPrincipalByIdentity, t as loadPrincipal } from "../principal-source-C6N60GtM.js";
|
|
8
|
+
import { v as resource } from "../permission-registry-B8tYxPIt.js";
|
|
9
|
+
import { d as isoNow, f as nowMillis, p as nowSeconds, u as isoAt } from "../time-nR1JesoH.js";
|
|
10
|
+
import { r as parseAutomation } from "../automation-x3KEUusw.js";
|
|
11
|
+
import { f as scopeList, o as normalizeMemberships } from "../scopes-0Up_UA2t.js";
|
|
12
|
+
import { u as evaluateCondition } from "../delegation-CDB6_keF.js";
|
|
13
|
+
import { a as authenticated } from "../grantee-Dke-9rd1.js";
|
|
14
|
+
import { t as byCodePoint } from "../compare-DTfyUunF.js";
|
|
15
|
+
import { r as definePolicy } from "../policy-CLQ8__GF.js";
|
|
16
|
+
import { c as mergeHostedGrants, l as parsePolicyDocument } from "../hosted-D0IzESu_.js";
|
|
17
|
+
import { t as definePermissions } from "../permissions-CVXmRILq.js";
|
|
18
|
+
import { T as validateCustomRole } from "../evaluate-Dd8Ta0Ie.js";
|
|
19
|
+
import { n as parseSnapshot } from "../snapshot-CXlzi69u.js";
|
|
20
|
+
import { n as fromSnapshot } from "../from-snapshot-DCakWRsr.js";
|
|
21
|
+
import { t as getPermDock } from "../permdock-L-_I_3zP.js";
|
|
22
|
+
import { t as APPROVAL_HEADER } from "../types-CcUXciSj.js";
|
|
23
|
+
import { i as memoryApprovalStore } from "../store-RNFvaJJv.js";
|
|
24
|
+
import { t as catalogFingerprint } from "../catalog-fingerprint-e6aCWosn.js";
|
|
25
|
+
import { s as parseCredential } from "../credential-CyyyvCsL.js";
|
|
26
|
+
import { c as problemFromDecision } from "../problem-BVLHFK33.js";
|
|
27
|
+
import { o as resolveApproval } from "../helpers-DJicaG6f.js";
|
|
28
|
+
import { t as APPROVAL_REQUEST_SCHEMA } from "../request-schema-DhX7UbfQ.js";
|
|
29
|
+
import { t as catalogSchema } from "../schema-BOJjqlWL.js";
|
|
30
|
+
import { c as parseApiKey } from "../server-3cVlTLde.js";
|
|
31
|
+
import { n as discoverAuthorizationServer } from "../device-BwbJNmb2.js";
|
|
32
|
+
import { t as CONNECTOR_SCHEMA } from "../connector-schema-CYLQNMYq.js";
|
|
33
|
+
import { l as sha256Hex, n as scimHandler, t as directoryMembershipSource } from "../scim-jJBJ7g1Z.js";
|
|
34
|
+
import { c as quoteSqlTable, o as quoteSqlIdent } from "../sql-0WjxIp9g.js";
|
|
35
|
+
import { i as cloudEndpoints, n as cloud, s as approverHeaders, t as bearerChallenge } from "../challenge-DImNcR7B.js";
|
|
36
|
+
import { t as supabaseMembershipsBudget } from "../budget-BELja5A9.js";
|
|
37
|
+
import { t as supabaseManifestSchema } from "../manifest-schema-2r37XRhD.js";
|
|
38
|
+
import { t as PUSHED_POLICY_SCHEMA } from "../pushed-policy-ITwYKvvH.js";
|
|
39
|
+
import { i as policyMatrixLayout, n as testRlsParity, r as policyMatrix, t as testOrmParity } from "../core-H-mbhpZc.js";
|
|
40
|
+
import { i as saasPermissions } from "../permissions-mDcTWoBs.js";
|
|
41
|
+
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-iWnUdwCt.js";
|
|
42
|
+
import { t as POLICY_NOT_PUBLISHED } from "../problem-types-DSsRfONQ.js";
|
|
43
|
+
import { t as require__2020 } from "../2020-CJbO8Bvz.js";
|
|
44
|
+
import { afterAll, assert, beforeAll, describe, expect, expectTypeOf, it, vi } from "vitest";
|
|
43
45
|
//#region src/testing/describe-policy.ts
|
|
44
|
-
|
|
45
|
-
return value === "granted" || value === "denied" || value === "approval-required" || value !== null && typeof value === "object" && ("denials" in value || "outcome" in value || "alternatives" in value || "obligations" in value || "deniedBy" in value);
|
|
46
|
-
}
|
|
47
|
-
/** Asserts `deniedBy` against the trace of a fresh `explain` call for the same arguments. */
|
|
48
|
-
function assertDeniedBy(instance, permission, data, cell) {
|
|
49
|
-
if (typeof cell !== "object" || cell.deniedBy === void 0) return;
|
|
50
|
-
const explained = instance.explain(permission, data);
|
|
51
|
-
expect(explained.outcome).toBe("denied");
|
|
52
|
-
expect(explained.trace.denies[0]?.name).toBe(cell.deniedBy);
|
|
53
|
-
}
|
|
54
|
-
function expectedOutcome(cell) {
|
|
55
|
-
return typeof cell === "string" ? cell : cell.outcome ?? "denied";
|
|
56
|
-
}
|
|
57
|
-
function assertCell(decision, cell) {
|
|
58
|
-
expect(decision.outcome).toBe(expectedOutcome(cell));
|
|
59
|
-
if (typeof cell === "object" && cell.denials !== void 0 && decision.outcome === "denied") for (const denial of cell.denials) expect(decision.denials.some((item) => item.reason === denial.reason && (denial.role === void 0 || item.role === denial.role))).toBe(true);
|
|
60
|
-
if (typeof cell === "object" && cell.alternatives !== void 0 && decision.outcome === "denied") expect(decision.alternatives.map((leaf) => leaf.key)).toEqual(cell.alternatives);
|
|
61
|
-
if (typeof cell === "object" && cell.obligations !== void 0 && decision.outcome === "granted") expect((decision.obligations ?? []).map((item) => item.kind)).toEqual(cell.obligations);
|
|
62
|
-
}
|
|
63
|
-
function assertSnapshotCell(instance, permission, data, cell) {
|
|
64
|
-
const outcome = expectedOutcome(cell);
|
|
65
|
-
if (outcome === "approval-required") return;
|
|
66
|
-
const client = fromSnapshot(JSON.parse(JSON.stringify(instance.snapshot())));
|
|
67
|
-
expect(client.can(permission, data), "snapshot client disagrees with the server").toBe(outcome === "granted");
|
|
68
|
-
}
|
|
46
|
+
/** `policyMatrix` under Vitest: one test per cell, plus the exhaustive check. */
|
|
69
47
|
function describePolicy(policy, config) {
|
|
70
48
|
describe("policy matrix", () => {
|
|
71
|
-
const
|
|
72
|
-
const permdocks = /* @__PURE__ */ new Map();
|
|
49
|
+
const state = {};
|
|
73
50
|
beforeAll(async () => {
|
|
74
|
-
|
|
51
|
+
state.report = await policyMatrix(policy, config);
|
|
75
52
|
});
|
|
76
53
|
it("covers every permission", () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
for (const permission of
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
assertCell(instance.decide(permission, data), cell);
|
|
89
|
-
assertDeniedBy(instance, permission, data, cell);
|
|
90
|
-
if (config.snapshot === true) assertSnapshotCell(instance, permission, data, cell);
|
|
91
|
-
});
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
for (const [fixtureName, row] of Object.entries(spec)) {
|
|
95
|
-
if (isOutcomeCell(row)) continue;
|
|
96
|
-
for (const [subjectName, cell] of Object.entries(row)) it(`${fixtureName} / ${subjectName}`, async () => {
|
|
97
|
-
const instance = permdocks.get(subjectName) ?? await getPermDock(policy, config.subjects[subjectName], config.options);
|
|
98
|
-
const fixture = config.fixtures?.[fixtureName];
|
|
99
|
-
assertCell(instance.decide(permission, fixture), cell);
|
|
100
|
-
assertDeniedBy(instance, permission, fixture, cell);
|
|
101
|
-
if (config.snapshot === true) assertSnapshotCell(instance, permission, fixture, cell);
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
//#endregion
|
|
109
|
-
//#region src/testing/rls-parity.ts
|
|
110
|
-
function toSubject(input) {
|
|
111
|
-
return {
|
|
112
|
-
principal: {
|
|
113
|
-
id: input.id,
|
|
114
|
-
roles: input.roles ?? [],
|
|
115
|
-
...input.tenant === void 0 ? {} : { tenant: input.tenant },
|
|
116
|
-
memberships: input.memberships ?? [],
|
|
117
|
-
...input.claims === void 0 ? {} : { claims: input.claims }
|
|
118
|
-
},
|
|
119
|
-
context: {}
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
function rowId(row) {
|
|
123
|
-
return row["id"];
|
|
124
|
-
}
|
|
125
|
-
function setting(name, value) {
|
|
126
|
-
return {
|
|
127
|
-
sql: "select set_config($1, $2, true)",
|
|
128
|
-
values: [name, value]
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
/**
|
|
132
|
-
* The session state the generated helpers read: JWT claims for `supabase`,
|
|
133
|
-
* GUCs for `guc` (roles as a comma list, memberships as JSON).
|
|
134
|
-
*/
|
|
135
|
-
function subjectSettings(dialect, subject, gucPrefix, tenantClaim, roleClaim, customRoles, scopes) {
|
|
136
|
-
const roles = subject.roles ?? [];
|
|
137
|
-
const memberships = membershipsClaim(subject.memberships ?? [], customRoles, scopes);
|
|
138
|
-
if (dialect === "supabase" || dialect === "neon") {
|
|
139
|
-
const claims = {
|
|
140
|
-
...subject.claims,
|
|
141
|
-
sub: subject.id,
|
|
142
|
-
role: "authenticated",
|
|
143
|
-
[roleClaim]: roles.length === 1 ? roles[0] : roles,
|
|
144
|
-
[tenantClaim]: subject.tenant,
|
|
145
|
-
memberships
|
|
146
|
-
};
|
|
147
|
-
return [setting("request.jwt.claims", JSON.stringify(claims))];
|
|
148
|
-
}
|
|
149
|
-
const settings = [
|
|
150
|
-
...Object.entries(subject.claims ?? {}).map(([name, value]) => {
|
|
151
|
-
if (!SQL_IDENT.test(name)) throw new Error(`PermDock: unsafe claim name '${name}'`);
|
|
152
|
-
return setting(`${gucPrefix}.${name}`, typeof value === "string" ? value : JSON.stringify(value));
|
|
153
|
-
}),
|
|
154
|
-
setting(`${gucPrefix}.user_id`, subject.id),
|
|
155
|
-
setting(`${gucPrefix}.${roleClaim}`, roles.join(",")),
|
|
156
|
-
setting(`${gucPrefix}.memberships`, JSON.stringify(memberships))
|
|
157
|
-
];
|
|
158
|
-
return subject.tenant === void 0 ? settings : [...settings, setting(`${gucPrefix}.${tenantClaim}`, subject.tenant)];
|
|
159
|
-
}
|
|
160
|
-
/**
|
|
161
|
-
* The statement a case runs. With field views it reads back only the key, so
|
|
162
|
-
* a base table whose restricted columns are revoked does not reject it.
|
|
163
|
-
*/
|
|
164
|
-
function statementSql(action, table, keyOnly) {
|
|
165
|
-
const quoted = quoteSqlTable(table);
|
|
166
|
-
const id = quoteSqlIdent("id");
|
|
167
|
-
const back = keyOnly ? id : "*";
|
|
168
|
-
const command = defaultSqlCommand(action) ?? "select";
|
|
169
|
-
switch (command) {
|
|
170
|
-
case "select": return `select ${back} from ${quoted} where ${id} = $1`;
|
|
171
|
-
case "update": return `update ${quoted} set ${id} = ${id} where ${id} = $1 returning ${back}`;
|
|
172
|
-
case "insert": return `insert into ${quoted} (${id}) values ($1) returning ${back}`;
|
|
173
|
-
case "delete": return `delete from ${quoted} where ${id} = $1 returning ${back}`;
|
|
174
|
-
default: return command;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
function valued(row) {
|
|
178
|
-
if (row === null || typeof row !== "object") return [];
|
|
179
|
-
const record = row;
|
|
180
|
-
return Object.keys(record).filter((name) => record[name] !== null && record[name] !== void 0).toSorted();
|
|
181
|
-
}
|
|
182
|
-
function isRead(action) {
|
|
183
|
-
return action === "read" || action === "get" || action === "list";
|
|
184
|
-
}
|
|
185
|
-
async function viewColumns(query, table, key) {
|
|
186
|
-
const id = quoteSqlIdent("id");
|
|
187
|
-
await query("savepoint permdock_fields");
|
|
188
|
-
const view = await query(`select * from ${quoteSqlTable(`${table}_visible`)} where ${id} = $1`, [key]);
|
|
189
|
-
if (view.code === void 0) return valued(view.rows[0]);
|
|
190
|
-
await query("rollback to savepoint permdock_fields");
|
|
191
|
-
if (view.code !== "42P01") return view.code;
|
|
192
|
-
const base = await query(`select * from ${quoteSqlTable(table)} where ${id} = $1`, [key]);
|
|
193
|
-
return base.code ?? valued(base.rows[0]);
|
|
194
|
-
}
|
|
195
|
-
function dbOutcome(result) {
|
|
196
|
-
if (result.code === "42501") return "rejected";
|
|
197
|
-
return (result.rowCount ?? result.rows.length) > 0 ? "allowed" : "filtered";
|
|
198
|
-
}
|
|
199
|
-
async function testRlsParity(policy, options) {
|
|
200
|
-
const dialect = options.dialect ?? "guc";
|
|
201
|
-
const gucPrefix = options.gucPrefix ?? "app";
|
|
202
|
-
const tenantClaim = options.tenantClaim ?? "tenant_id";
|
|
203
|
-
const roleClaim = options.roleClaim ?? "user_role";
|
|
204
|
-
const role = options.role ?? "authenticated";
|
|
205
|
-
const customRoles = options.customRoles ?? [];
|
|
206
|
-
const scopes = scopeList(policy.scopes);
|
|
207
|
-
async function runCase(fixture) {
|
|
208
|
-
const permdock = await getPermDock(policy, toSubject(fixture.subject), { customRoles: memoryCustomRoleSource(customRoles) });
|
|
209
|
-
const granted = fixture.permission.kind === "collection" ? permdock.can(fixture.permission, fixture.row) : permdock.can(fixture.permission, fixture.row);
|
|
210
|
-
let fromClient;
|
|
211
|
-
if (options.snapshot === true) {
|
|
212
|
-
const snapshot = permdock.snapshot();
|
|
213
|
-
if (typeof snapshot !== "object" || snapshot instanceof Promise) throw new TypeError("PermDock: testRlsParity needs an unsigned snapshot");
|
|
214
|
-
fromClient = fromSnapshot(parseSnapshot(JSON.stringify(snapshot))).can(fixture.permission, fixture.row);
|
|
215
|
-
}
|
|
216
|
-
await options.query("begin");
|
|
217
|
-
try {
|
|
218
|
-
await options.query(`set local role ${quoteSqlIdent(role === "anon" && dialect === "neon" ? "anonymous" : role)}`);
|
|
219
|
-
for (const item of subjectSettings(dialect, fixture.subject, gucPrefix, tenantClaim, roleClaim, customRoles, scopes)) await options.query(item.sql, item.values);
|
|
220
|
-
const database = dbOutcome(await options.query(statementSql(fixture.permission.action, fixture.table, options.fieldViews === true), [rowId(fixture.row)]));
|
|
221
|
-
const agrees = granted ? database === "allowed" : database === "filtered" || database === "rejected";
|
|
222
|
-
let fields;
|
|
223
|
-
if (options.fieldViews === true && fixture.permission.kind === "instance" && isRead(fixture.permission.action)) {
|
|
224
|
-
const key = policy.resources.get(fixture.permission.resource)?.id ?? "id";
|
|
225
|
-
const kept = new Set(granted ? valued(permdock.pick(fixture.permission, fixture.row)) : []);
|
|
226
|
-
if (granted && fixture.row[key] !== null && fixture.row[key] !== void 0) kept.add(key);
|
|
227
|
-
fields = {
|
|
228
|
-
app: [...kept].toSorted(),
|
|
229
|
-
database: await viewColumns(options.query, fixture.table, rowId(fixture.row))
|
|
230
|
-
};
|
|
54
|
+
for (const key of state.report?.missing ?? []) expect.fail(`missing matrix for ${key}`);
|
|
55
|
+
});
|
|
56
|
+
const layout = policyMatrixLayout(policy, config);
|
|
57
|
+
for (const permission of new Set(layout.map((cell) => cell.permission))) describe(permission, () => {
|
|
58
|
+
for (const cell of layout.filter((item) => item.permission === permission)) {
|
|
59
|
+
const title = cell.fixture === void 0 ? cell.subject : `${cell.fixture} / ${cell.subject}`;
|
|
60
|
+
it(title, () => {
|
|
61
|
+
const decided = state.report?.cells.find((item) => item.name === cell.name);
|
|
62
|
+
expect(decided, `cell ${cell.name} was not decided`).toBeDefined();
|
|
63
|
+
expect(decided?.failures).toStrictEqual([]);
|
|
64
|
+
});
|
|
231
65
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return {
|
|
235
|
-
name: fixture.name,
|
|
236
|
-
granted,
|
|
237
|
-
database,
|
|
238
|
-
...fromClient === void 0 ? {} : { snapshot: fromClient },
|
|
239
|
-
...fields === void 0 ? {} : { fields },
|
|
240
|
-
ok
|
|
241
|
-
};
|
|
242
|
-
} finally {
|
|
243
|
-
await options.query("rollback");
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
async function runAll(remaining, acc) {
|
|
247
|
-
const [head, ...tail] = remaining;
|
|
248
|
-
if (head === void 0) return acc;
|
|
249
|
-
return runAll(tail, [...acc, await runCase(head)]);
|
|
250
|
-
}
|
|
251
|
-
const results = await runAll(options.fixtures, []);
|
|
252
|
-
return {
|
|
253
|
-
ok: results.every((item) => item.ok),
|
|
254
|
-
results
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
//#endregion
|
|
258
|
-
//#region src/testing/orm-parity.ts
|
|
259
|
-
function sortedIds(values) {
|
|
260
|
-
return values.map(String).toSorted();
|
|
261
|
-
}
|
|
262
|
-
function subsetOf(small, large) {
|
|
263
|
-
return small.every((value) => large.includes(value));
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* ORM parity: for each scenario, the rows `permdock.filter()` keeps in memory
|
|
267
|
-
* must be exactly the rows the database returns for `toWhere(permdock.where())`.
|
|
268
|
-
* With a `relations` option, the graph is loaded for the rows first, so the
|
|
269
|
-
* in-memory side walks the same graph the database query does.
|
|
270
|
-
* A partial `where()` (a closure grant it cannot compile) may be refused or
|
|
271
|
-
* return fewer rows, never more.
|
|
272
|
-
*/
|
|
273
|
-
async function testOrmParity(policy, scenarios, options) {
|
|
274
|
-
const idField = options.id ?? "id";
|
|
275
|
-
async function runCase(scenario) {
|
|
276
|
-
const permdock = await getPermDock(policy, scenario.user, scenario.options);
|
|
277
|
-
if (scenario.options?.relations !== void 0) await permdock.prefetchRelations(scenario.permission, scenario.rows);
|
|
278
|
-
const expected = sortedIds(permdock.filter(scenario.permission, scenario.rows).map((row) => row[idField]));
|
|
279
|
-
const where = permdock.where(scenario.permission);
|
|
280
|
-
try {
|
|
281
|
-
const actual = sortedIds(await options.run({
|
|
282
|
-
scenario,
|
|
283
|
-
where
|
|
284
|
-
}));
|
|
285
|
-
const ok = where.partial ? subsetOf(actual, expected) : actual.length === expected.length && subsetOf(actual, expected);
|
|
286
|
-
return {
|
|
287
|
-
name: scenario.name,
|
|
288
|
-
expected,
|
|
289
|
-
actual,
|
|
290
|
-
partial: where.partial,
|
|
291
|
-
ok
|
|
292
|
-
};
|
|
293
|
-
} catch (error) {
|
|
294
|
-
return {
|
|
295
|
-
name: scenario.name,
|
|
296
|
-
expected,
|
|
297
|
-
actual: [],
|
|
298
|
-
partial: where.partial,
|
|
299
|
-
error: error instanceof Error ? error.message : String(error),
|
|
300
|
-
ok: where.partial
|
|
301
|
-
};
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
const results = [];
|
|
305
|
-
for (const scenario of scenarios) results.push(await runCase(scenario));
|
|
306
|
-
return {
|
|
307
|
-
ok: results.every((item) => item.ok),
|
|
308
|
-
results
|
|
309
|
-
};
|
|
66
|
+
});
|
|
67
|
+
});
|
|
310
68
|
}
|
|
311
69
|
//#endregion
|
|
312
70
|
//#region src/testing/client-parity.ts
|
|
@@ -322,8 +80,8 @@ async function outcomes(policy, entry, options) {
|
|
|
322
80
|
};
|
|
323
81
|
}
|
|
324
82
|
/**
|
|
325
|
-
* Asserts that `fromSnapshot(permdock.snapshot())` never grants what the
|
|
326
|
-
*
|
|
83
|
+
* Asserts that `fromSnapshot(permdock.snapshot())` never grants what the server
|
|
84
|
+
* denies, and matches it exactly unless a case is marked `stricter`.
|
|
327
85
|
*/
|
|
328
86
|
function testClientParity(policy, cases, options = {}) {
|
|
329
87
|
for (const entry of cases) it(`client parity: ${entry.name}`, async () => {
|
|
@@ -338,9 +96,9 @@ function testClientParity(policy, cases, options = {}) {
|
|
|
338
96
|
//#endregion
|
|
339
97
|
//#region src/testing/legacy-parity.ts
|
|
340
98
|
/**
|
|
341
|
-
* Asserts that the policy answers each case as the application's legacy
|
|
342
|
-
*
|
|
343
|
-
*
|
|
99
|
+
* Asserts that the policy answers each case as the application's legacy check
|
|
100
|
+
* does, or as the case's `expect` says where the difference is intended. Run it
|
|
101
|
+
* before enforcing a permission with `shadowMode`.
|
|
344
102
|
*/
|
|
345
103
|
function testLegacyParity(policy, legacy, cases) {
|
|
346
104
|
for (const entry of cases) it(`legacy parity: ${entry.name}`, async () => {
|
|
@@ -354,7 +112,7 @@ function testLegacyParity(policy, legacy, cases) {
|
|
|
354
112
|
expect({
|
|
355
113
|
permission: entry.permission.key,
|
|
356
114
|
granted: ours
|
|
357
|
-
}).
|
|
115
|
+
}).toStrictEqual({
|
|
358
116
|
permission: entry.permission.key,
|
|
359
117
|
granted: entry.expect ?? theirs
|
|
360
118
|
});
|
|
@@ -460,9 +218,9 @@ function unreachable() {
|
|
|
460
218
|
}
|
|
461
219
|
/**
|
|
462
220
|
* Race and cache scenarios for a client snapshot store: tenant switches,
|
|
463
|
-
* `replace` and `refresh`, logout and a second user during a refresh, stale
|
|
464
|
-
*
|
|
465
|
-
*
|
|
221
|
+
* `replace` and `refresh`, logout and a second user during a refresh, stale and
|
|
222
|
+
* signed snapshots, and endpoint answers cached per tenant. Runs against the
|
|
223
|
+
* saas domain; `createStore` wraps the store an adapter ships.
|
|
466
224
|
*/
|
|
467
225
|
function testClientStore(name, createStore) {
|
|
468
226
|
const acmeProject = saasProject("p1");
|
|
@@ -494,7 +252,7 @@ function testClientStore(name, createStore) {
|
|
|
494
252
|
fetch: net.fetch
|
|
495
253
|
});
|
|
496
254
|
await store.get().refresh({ tenant: "globex" });
|
|
497
|
-
expect(net.calls.map((call) => call.url)).
|
|
255
|
+
expect(net.calls.map((call) => call.url)).toStrictEqual(["/api/snapshot?v=1&tenant=globex"]);
|
|
498
256
|
expect(store.get().subject.principal?.tenant).toBe("acme");
|
|
499
257
|
expect(store.get().can(p.project.update, acmeProject)).toBe(true);
|
|
500
258
|
expect(store.get().status()).toBe("stale");
|
|
@@ -742,15 +500,36 @@ function testClientStore(name, createStore) {
|
|
|
742
500
|
async function snapshotFixture(policy, subject, options = {}) {
|
|
743
501
|
const instance = await getPermDock(policy, subject, options.tenant === void 0 ? {} : { tenant: options.tenant });
|
|
744
502
|
const target = options.simulated === true ? instance.simulate({}) : instance;
|
|
745
|
-
if (Array.isArray(target)) throw new
|
|
503
|
+
if (Array.isArray(target)) throw new TypeError("PermDock: snapshotFixture expected a PermDock instance");
|
|
746
504
|
const snapshot = options.include === void 0 && options.tenants === void 0 ? target.snapshot() : target.snapshot({
|
|
747
505
|
...options.include === void 0 ? {} : { include: options.include },
|
|
748
506
|
...options.tenants === void 0 ? {} : { tenants: options.tenants }
|
|
749
507
|
});
|
|
750
|
-
if (typeof snapshot === "string" || snapshot instanceof Promise) throw new
|
|
508
|
+
if (typeof snapshot === "string" || snapshot instanceof Promise) throw new TypeError("PermDock: snapshotFixture expected a JSON snapshot");
|
|
751
509
|
return snapshot;
|
|
752
510
|
}
|
|
753
511
|
//#endregion
|
|
512
|
+
//#region src/testing/seed-rows.ts
|
|
513
|
+
/**
|
|
514
|
+
* Inserts `factories.*` rows through the `query` function `testRlsParity`
|
|
515
|
+
* takes, one parameterised `insert` per row with the row's own keys as quoted
|
|
516
|
+
* column names. `undefined` fields are left to the column default. Run it as
|
|
517
|
+
* the table owner, before RLS applies. Resolves to the number of rows
|
|
518
|
+
* inserted.
|
|
519
|
+
*/
|
|
520
|
+
async function seedRows(query, rows) {
|
|
521
|
+
let inserted = 0;
|
|
522
|
+
for (const [table, list] of Object.entries(rows)) {
|
|
523
|
+
const target = quoteSqlTable(table);
|
|
524
|
+
for (const row of list) {
|
|
525
|
+
const columns = Object.keys(row).filter((key) => row[key] !== void 0);
|
|
526
|
+
await query(columns.length === 0 ? `insert into ${target} default values` : `insert into ${target} (${columns.map((column) => quoteSqlIdent(column)).join(", ")}) values (${columns.map((_, index) => `$${String(index + 1)}`).join(", ")})`, columns.map((column) => row[column]));
|
|
527
|
+
inserted += 1;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return inserted;
|
|
531
|
+
}
|
|
532
|
+
//#endregion
|
|
754
533
|
//#region src/testing/http-adapter/domain.ts
|
|
755
534
|
function project$1(id) {
|
|
756
535
|
const row = saasSeed.projects.find((candidate) => candidate.id === id);
|
|
@@ -1071,11 +850,12 @@ function testHttpAdapter(options) {
|
|
|
1071
850
|
return eventStream(await mounted.fetch(new Request(`http://app.test/${org}/projects/${id}/events`, { headers })));
|
|
1072
851
|
};
|
|
1073
852
|
scenario("tenant", "resolves the tenant from the path on every protect", async () => {
|
|
1074
|
-
|
|
853
|
+
const { status } = await send("alice", {
|
|
1075
854
|
op: "project.get",
|
|
1076
855
|
org: "acme",
|
|
1077
856
|
id: "p2"
|
|
1078
|
-
})
|
|
857
|
+
});
|
|
858
|
+
expect(status).toBe(200);
|
|
1079
859
|
expect((await send("alice", {
|
|
1080
860
|
op: "project.get",
|
|
1081
861
|
org: "globex",
|
|
@@ -1098,11 +878,12 @@ function testHttpAdapter(options) {
|
|
|
1098
878
|
})).status).toBe(401);
|
|
1099
879
|
});
|
|
1100
880
|
scenario("roles", "applies the role held in the requested org", async () => {
|
|
1101
|
-
|
|
881
|
+
const { status } = await send("alice", {
|
|
1102
882
|
op: "project.update",
|
|
1103
883
|
org: "acme",
|
|
1104
884
|
id: "p1"
|
|
1105
|
-
})
|
|
885
|
+
});
|
|
886
|
+
expect(status).toBe(200);
|
|
1106
887
|
expect((await send("alice", {
|
|
1107
888
|
op: "project.update",
|
|
1108
889
|
org: "globex",
|
|
@@ -1127,11 +908,12 @@ function testHttpAdapter(options) {
|
|
|
1127
908
|
name: "Contract",
|
|
1128
909
|
archived: false
|
|
1129
910
|
};
|
|
1130
|
-
|
|
911
|
+
const { status } = await send("dave", {
|
|
1131
912
|
op: "project.create",
|
|
1132
913
|
org: "acme",
|
|
1133
914
|
body
|
|
1134
|
-
})
|
|
915
|
+
});
|
|
916
|
+
expect(status).toBe(201);
|
|
1135
917
|
expect((await send("hank", {
|
|
1136
918
|
op: "project.create",
|
|
1137
919
|
org: "acme",
|
|
@@ -1139,29 +921,34 @@ function testHttpAdapter(options) {
|
|
|
1139
921
|
})).status).toBe(403);
|
|
1140
922
|
});
|
|
1141
923
|
scenario("plans", "gates a feature on the plan of the requested org", async () => {
|
|
1142
|
-
|
|
924
|
+
const { status } = await send("erin", {
|
|
1143
925
|
op: "analytics.read",
|
|
1144
926
|
org: "globex"
|
|
1145
|
-
})
|
|
927
|
+
});
|
|
928
|
+
expect(status).toBe(200);
|
|
1146
929
|
const upgrade = await send("erin", {
|
|
1147
930
|
op: "analytics.read",
|
|
1148
931
|
org: "acme"
|
|
1149
932
|
});
|
|
1150
933
|
expect(upgrade.status).toBe(403);
|
|
1151
|
-
expect(new Set(reasonsOf(upgrade.body))).
|
|
934
|
+
expect(new Set(reasonsOf(upgrade.body))).toStrictEqual(/* @__PURE__ */ new Set(["not-entitled"]));
|
|
1152
935
|
});
|
|
1153
936
|
scenario("expired", "ignores an expired membership", async () => {
|
|
1154
|
-
|
|
937
|
+
const { status } = await send("frank", {
|
|
1155
938
|
op: "project.get",
|
|
1156
939
|
org: "acme",
|
|
1157
940
|
id: "p1"
|
|
1158
|
-
})
|
|
941
|
+
});
|
|
942
|
+
expect(status).toBe(403);
|
|
1159
943
|
});
|
|
1160
944
|
scenario("limits", "counts a quota per subject and tenant", async () => {
|
|
1161
|
-
for (let index = 0; index < 5; index += 1)
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
945
|
+
for (let index = 0; index < 5; index += 1) {
|
|
946
|
+
const { status } = await send("erin", {
|
|
947
|
+
op: "apiKey.create",
|
|
948
|
+
org: "acme"
|
|
949
|
+
});
|
|
950
|
+
expect(status).toBe(201);
|
|
951
|
+
}
|
|
1165
952
|
const exhausted = await send("erin", {
|
|
1166
953
|
op: "apiKey.create",
|
|
1167
954
|
org: "acme"
|
|
@@ -1181,11 +968,12 @@ function testHttpAdapter(options) {
|
|
|
1181
968
|
name: "New",
|
|
1182
969
|
archived: false
|
|
1183
970
|
};
|
|
1184
|
-
|
|
971
|
+
const { status } = await send("bob", {
|
|
1185
972
|
op: "project.create",
|
|
1186
973
|
org: "acme",
|
|
1187
974
|
body: valid
|
|
1188
|
-
})
|
|
975
|
+
});
|
|
976
|
+
expect(status).toBe(201);
|
|
1189
977
|
const invalid = await send("bob", {
|
|
1190
978
|
op: "project.create",
|
|
1191
979
|
org: "acme",
|
|
@@ -1221,12 +1009,13 @@ function testHttpAdapter(options) {
|
|
|
1221
1009
|
"title",
|
|
1222
1010
|
"permission",
|
|
1223
1011
|
"denials"
|
|
1224
|
-
]) expect(field(denied.body, key)).
|
|
1225
|
-
|
|
1012
|
+
]) expect(field(denied.body, key)).toStrictEqual(field(fromProtect.body, key));
|
|
1013
|
+
const { status } = await send("bob", {
|
|
1226
1014
|
op: "project.delete",
|
|
1227
1015
|
org: "acme",
|
|
1228
1016
|
id: "p3"
|
|
1229
|
-
})
|
|
1017
|
+
});
|
|
1018
|
+
expect(status).toBe(204);
|
|
1230
1019
|
expect((await send("bob", {
|
|
1231
1020
|
op: "project.delete",
|
|
1232
1021
|
org: "acme",
|
|
@@ -1241,7 +1030,7 @@ function testHttpAdapter(options) {
|
|
|
1241
1030
|
expect(asked.status).toBe(403);
|
|
1242
1031
|
expect(field(asked.body, "type")).toBe("https://permdock.com/problems/approval-required");
|
|
1243
1032
|
const token = field(asked.body, "token");
|
|
1244
|
-
expect(
|
|
1033
|
+
expect(token).toBeTypeOf("string");
|
|
1245
1034
|
await resolveApproval(domain.store, String(token), {
|
|
1246
1035
|
status: "approved",
|
|
1247
1036
|
by: await approverSubject("carol", "acme")
|
|
@@ -1260,11 +1049,12 @@ function testHttpAdapter(options) {
|
|
|
1260
1049
|
})).status).toBe(204);
|
|
1261
1050
|
});
|
|
1262
1051
|
scenario("stale-approval", "ignores an approval header meant for another request", async () => {
|
|
1263
|
-
|
|
1052
|
+
const { status } = await send("bob", {
|
|
1264
1053
|
op: "project.get",
|
|
1265
1054
|
org: "acme",
|
|
1266
1055
|
id: "p1"
|
|
1267
|
-
}, "pd1.not-a-real-token")
|
|
1056
|
+
}, "pd1.not-a-real-token");
|
|
1057
|
+
expect(status).toBe(200);
|
|
1268
1058
|
});
|
|
1269
1059
|
scenario("evaluations", "answers the decision endpoint in the path tenant", async () => {
|
|
1270
1060
|
const result = await send("bob", {
|
|
@@ -1288,7 +1078,7 @@ function testHttpAdapter(options) {
|
|
|
1288
1078
|
});
|
|
1289
1079
|
expect(result.status).toBe(200);
|
|
1290
1080
|
const rows = field(result.body, "evaluations");
|
|
1291
|
-
expect(Array.isArray(rows) ? rows.map((row) => field(row, "decision")) : rows).
|
|
1081
|
+
expect(Array.isArray(rows) ? rows.map((row) => field(row, "decision")) : rows).toStrictEqual([true, false]);
|
|
1292
1082
|
});
|
|
1293
1083
|
scenario("upload", "leaves a multipart body to the framework parser", async () => {
|
|
1294
1084
|
const file = {
|
|
@@ -1302,22 +1092,24 @@ function testHttpAdapter(options) {
|
|
|
1302
1092
|
file
|
|
1303
1093
|
});
|
|
1304
1094
|
expect(uploaded.status).toBe(201);
|
|
1305
|
-
expect(uploaded.body).
|
|
1095
|
+
expect(uploaded.body).toStrictEqual({
|
|
1306
1096
|
name: "notes.txt",
|
|
1307
1097
|
size: 5
|
|
1308
1098
|
});
|
|
1309
|
-
|
|
1099
|
+
const { status } = await send("bob", {
|
|
1310
1100
|
op: "project.upload",
|
|
1311
1101
|
org: "acme",
|
|
1312
1102
|
id: "p2",
|
|
1313
1103
|
file
|
|
1314
|
-
})
|
|
1104
|
+
});
|
|
1105
|
+
expect(status).toBe(403);
|
|
1315
1106
|
});
|
|
1316
1107
|
scenario("sub-app", "protects a route inside a sub-app", async () => {
|
|
1317
|
-
|
|
1108
|
+
const { status } = await send("alice", {
|
|
1318
1109
|
op: "admin.members",
|
|
1319
1110
|
org: "acme"
|
|
1320
|
-
})
|
|
1111
|
+
});
|
|
1112
|
+
expect(status).toBe(200);
|
|
1321
1113
|
expect((await send("bob", {
|
|
1322
1114
|
op: "admin.members",
|
|
1323
1115
|
org: "acme"
|
|
@@ -1334,7 +1126,7 @@ function testHttpAdapter(options) {
|
|
|
1334
1126
|
org: entry.org,
|
|
1335
1127
|
id: entry.id
|
|
1336
1128
|
})).status));
|
|
1337
|
-
expect(statuses).
|
|
1129
|
+
expect(statuses).toStrictEqual(calls.map((entry) => entry.status));
|
|
1338
1130
|
});
|
|
1339
1131
|
scenario("stream-revoked", "closes an open stream when the session is revoked", async () => {
|
|
1340
1132
|
expect((await openStream(null, "acme", "p1")).status).toBe(401);
|
|
@@ -1389,7 +1181,7 @@ function testHttpAdapter(options) {
|
|
|
1389
1181
|
expect(frame?.event).toBe("message");
|
|
1390
1182
|
ids.push(String(field(JSON.parse(frame?.data ?? "{}"), "id")));
|
|
1391
1183
|
}
|
|
1392
|
-
expect(new Set(ids)).
|
|
1184
|
+
expect(new Set(ids)).toStrictEqual(/* @__PURE__ */ new Set([
|
|
1393
1185
|
"p1",
|
|
1394
1186
|
"p3",
|
|
1395
1187
|
"p4"
|
|
@@ -1716,10 +1508,11 @@ function testAgentRuntime(options) {
|
|
|
1716
1508
|
const listed = await agent().allowedOperations(alice);
|
|
1717
1509
|
expect(listed.has("project_read")).toBe(true);
|
|
1718
1510
|
expect(listed.has("drop_database")).toBe(false);
|
|
1719
|
-
expect([...await agent().allowedOperations(mallory)]).
|
|
1511
|
+
expect([...await agent().allowedOperations(mallory)]).toStrictEqual([]);
|
|
1720
1512
|
});
|
|
1721
1513
|
scenario("model-gate", "grants a model by its permission and denies one the map does not name", async () => {
|
|
1722
|
-
|
|
1514
|
+
const { outcome } = await agent().decideModel("frontier", erin);
|
|
1515
|
+
expect(outcome).toBe("granted");
|
|
1723
1516
|
expect((await agent().decideModel("frontier", alice)).outcome).toBe("denied");
|
|
1724
1517
|
expect((await agent().decideModel("unlisted", erin)).outcome).toBe("denied");
|
|
1725
1518
|
expect((await agent().decideModel("standard", {
|
|
@@ -1728,15 +1521,15 @@ function testAgentRuntime(options) {
|
|
|
1728
1521
|
})).outcome).toBe("denied");
|
|
1729
1522
|
});
|
|
1730
1523
|
scenario("permitted-models", "returns the granted models in declaration order", async () => {
|
|
1731
|
-
expect(await agent().permittedModels(erin)).
|
|
1732
|
-
expect(await agent().permittedModels(alice)).
|
|
1733
|
-
expect(await agent().permittedModels(mallory)).
|
|
1524
|
+
expect(await agent().permittedModels(erin)).toStrictEqual(["standard", "frontier"]);
|
|
1525
|
+
expect(await agent().permittedModels(alice)).toStrictEqual(["standard"]);
|
|
1526
|
+
expect(await agent().permittedModels(mallory)).toStrictEqual([]);
|
|
1734
1527
|
});
|
|
1735
1528
|
scenario("results-filter", "keeps the rows the results permission grants, and none for an unknown operation", async () => {
|
|
1736
1529
|
const rows = projects("p1", "g1", "p2");
|
|
1737
|
-
expect((await agent().filterResults("project_search", rows, alice)).map((row) => row.id)).
|
|
1738
|
-
expect(await agent().filterResults("project_search", rows, mallory)).
|
|
1739
|
-
expect(await agent().filterResults("drop_database", rows, alice)).
|
|
1530
|
+
expect((await agent().filterResults("project_search", rows, alice)).map((row) => row.id)).toStrictEqual(["p1", "p2"]);
|
|
1531
|
+
expect(await agent().filterResults("project_search", rows, mallory)).toStrictEqual([]);
|
|
1532
|
+
expect(await agent().filterResults("drop_database", rows, alice)).toStrictEqual([]);
|
|
1740
1533
|
});
|
|
1741
1534
|
scenario("gateway-options", "attributes gateway usage to the principal, and to nobody for an anonymous call", async () => {
|
|
1742
1535
|
const attributed = await agent().gatewayOptions(alice);
|
|
@@ -1786,7 +1579,7 @@ function testAuthzen(handle, options) {
|
|
|
1786
1579
|
}
|
|
1787
1580
|
for (const [index, vector] of (options.vectors.evaluations ?? []).entries()) it(`evaluations ${index}: ${vector.request.evaluations.length} items`, async () => {
|
|
1788
1581
|
const body = await post("/access/v1/evaluations", vector.request);
|
|
1789
|
-
expect(body.evaluations.map((row) => ({ decision: row.decision }))).
|
|
1582
|
+
expect(body.evaluations.map((row) => ({ decision: row.decision }))).toStrictEqual(vector.expected);
|
|
1790
1583
|
});
|
|
1791
1584
|
const search = options.vectors.search ?? {};
|
|
1792
1585
|
for (const kind of [
|
|
@@ -1795,10 +1588,13 @@ function testAuthzen(handle, options) {
|
|
|
1795
1588
|
"action"
|
|
1796
1589
|
]) for (const [index, vector] of (search[kind] ?? []).entries()) it(`search/${kind} ${index}`, async () => {
|
|
1797
1590
|
const body = await post(`/access/v1/search/${kind}`, vector.request);
|
|
1798
|
-
expect(body.results.map(sortKey).toSorted()).
|
|
1591
|
+
expect(body.results.map(sortKey).toSorted()).toStrictEqual(vector.expected.results.map(sortKey).toSorted());
|
|
1799
1592
|
});
|
|
1800
1593
|
}
|
|
1801
|
-
/**
|
|
1594
|
+
/**
|
|
1595
|
+
* The AuthZEN interop Todo directory: each user is addressable by email or by
|
|
1596
|
+
* identity.
|
|
1597
|
+
*/
|
|
1802
1598
|
const authzenTodoUsers = Object.freeze([
|
|
1803
1599
|
{
|
|
1804
1600
|
id: "rick@the-citadel.com",
|
|
@@ -1843,7 +1639,10 @@ const authzenTodoPermissions = definePermissions({
|
|
|
1843
1639
|
});
|
|
1844
1640
|
const todo = authzenTodoPermissions.todo;
|
|
1845
1641
|
const everyone = [allow(authzenTodoPermissions.user.can_read_user), allow(todo.can_read_todos)];
|
|
1846
|
-
/**
|
|
1642
|
+
/**
|
|
1643
|
+
* The interop Todo rules: viewers read, editors write their own, admins delete
|
|
1644
|
+
* and evil geniuses update anything.
|
|
1645
|
+
*/
|
|
1847
1646
|
const authzenTodoPolicy = definePolicy(authzenTodoPermissions, {
|
|
1848
1647
|
roles: [
|
|
1849
1648
|
role("viewer", everyone),
|
|
@@ -1878,7 +1677,10 @@ const TODOS = Object.freeze(authzenTodoUsers.map((user, index) => ({
|
|
|
1878
1677
|
id: `todo-${String(index + 1)}`,
|
|
1879
1678
|
ownerID: user.id
|
|
1880
1679
|
})));
|
|
1881
|
-
/**
|
|
1680
|
+
/**
|
|
1681
|
+
* Rows for `resources.todo.list` and records for `subjects.list` in the Todo
|
|
1682
|
+
* domain.
|
|
1683
|
+
*/
|
|
1882
1684
|
const authzenTodoData = Object.freeze({
|
|
1883
1685
|
todos: TODOS,
|
|
1884
1686
|
users: Object.freeze(authzenTodoUsers.map((user) => ({ id: user.id })))
|
|
@@ -1896,9 +1698,9 @@ function expectedFor(user, action, ownerID) {
|
|
|
1896
1698
|
}
|
|
1897
1699
|
}
|
|
1898
1700
|
/**
|
|
1899
|
-
* PermDock's vectors over the interop Todo domain, in the interop layout:
|
|
1900
|
-
*
|
|
1901
|
-
*
|
|
1701
|
+
* PermDock's vectors over the interop Todo domain, in the interop layout: every
|
|
1702
|
+
* user against every action on their own and on Rick's todo, one batch per
|
|
1703
|
+
* user, and the three searches.
|
|
1902
1704
|
*/
|
|
1903
1705
|
function authzenTodoVectors() {
|
|
1904
1706
|
const rick = authzenTodoUsers[0];
|
|
@@ -2098,9 +1900,9 @@ function testApprovalStore(store, options = {}) {
|
|
|
2098
1900
|
});
|
|
2099
1901
|
const [first, second] = await Promise.all([store.consume("once-token"), store.consume("once-token")]);
|
|
2100
1902
|
expect([first, second].filter((item) => item !== null)).toHaveLength(1);
|
|
2101
|
-
expect((first ?? second)?.consumedAt).
|
|
1903
|
+
expect((first ?? second)?.consumedAt).toStrictEqual(expect.any(String));
|
|
2102
1904
|
expect(await store.consume("once-token")).toBeNull();
|
|
2103
|
-
expect((await store.get("once-token"))?.consumedAt).
|
|
1905
|
+
expect((await store.get("once-token"))?.consumedAt).toStrictEqual(expect.any(String));
|
|
2104
1906
|
});
|
|
2105
1907
|
it("refuses an approver from another tenant", async () => {
|
|
2106
1908
|
await store.create(tenantApproval("tenant-token", "o_1"));
|
|
@@ -2116,17 +1918,31 @@ function testApprovalStore(store, options = {}) {
|
|
|
2116
1918
|
status: "approved",
|
|
2117
1919
|
by: approver
|
|
2118
1920
|
});
|
|
1921
|
+
assert.isDefined(options.reopen);
|
|
2119
1922
|
const reopened = await options.reopen();
|
|
2120
1923
|
expect((await reopened.get("restart-token"))?.status).toBe("approved");
|
|
2121
1924
|
expect(await reopened.consume("restart-token")).not.toBeNull();
|
|
2122
1925
|
expect(await store.consume("restart-token")).toBeNull();
|
|
2123
1926
|
});
|
|
1927
|
+
it.skipIf(options.capabilities?.expire === void 0)("replaces an expired request on a new ask", async () => {
|
|
1928
|
+
await store.create(sampleApproval("renew-token"));
|
|
1929
|
+
assert.isDefined(options.capabilities?.expire);
|
|
1930
|
+
await options.capabilities.expire("renew-token");
|
|
1931
|
+
const renewed = {
|
|
1932
|
+
...sampleApproval("renew-token"),
|
|
1933
|
+
id: "apr_renewed"
|
|
1934
|
+
};
|
|
1935
|
+
await store.create(renewed);
|
|
1936
|
+
const loaded = await store.get("renew-token");
|
|
1937
|
+
expect(loaded?.id).toBe("apr_renewed");
|
|
1938
|
+
expect(loaded?.status).toBe("pending");
|
|
1939
|
+
});
|
|
2124
1940
|
it("creates, gets, lists, resolves and expires", async () => {
|
|
2125
1941
|
const request = sampleApproval("opaque-token");
|
|
2126
1942
|
await store.create(request);
|
|
2127
1943
|
const loaded = await store.get("opaque-token");
|
|
2128
|
-
expect(loaded).
|
|
2129
|
-
expect(JSON.parse(JSON.stringify(loaded))).
|
|
1944
|
+
expect(loaded).toStrictEqual(request);
|
|
1945
|
+
expect(JSON.parse(JSON.stringify(loaded))).toStrictEqual(request);
|
|
2130
1946
|
const listed = await store.list({ status: "pending" });
|
|
2131
1947
|
expect(listed.items.some((item) => item.token === "opaque-token")).toBe(true);
|
|
2132
1948
|
const resolved = await store.resolve("opaque-token", {
|
|
@@ -2137,7 +1953,7 @@ function testApprovalStore(store, options = {}) {
|
|
|
2137
1953
|
await expect(Promise.resolve().then(() => store.resolve("opaque-token", {
|
|
2138
1954
|
status: "rejected",
|
|
2139
1955
|
by: approver
|
|
2140
|
-
}))).rejects.toThrow(/not pending/);
|
|
1956
|
+
}))).rejects.toThrow(/not pending/u);
|
|
2141
1957
|
await store.create(sampleApproval("stale-token"));
|
|
2142
1958
|
const expired = await store.expire({ epochMilliseconds: nowMillis() + 72e5 });
|
|
2143
1959
|
expect(expired).toBeGreaterThanOrEqual(1);
|
|
@@ -2160,19 +1976,20 @@ function testApprovalStore(store, options = {}) {
|
|
|
2160
1976
|
principalId: "u_pager",
|
|
2161
1977
|
limit: 2
|
|
2162
1978
|
});
|
|
2163
|
-
expect(first.items.map((item) => item.token)).
|
|
2164
|
-
expect(
|
|
1979
|
+
expect(first.items.map((item) => item.token)).toStrictEqual(["page-a", "page-b"]);
|
|
1980
|
+
expect(first.next).toBeTypeOf("string");
|
|
1981
|
+
assert.isDefined(first.next);
|
|
2165
1982
|
const second = await store.list({
|
|
2166
1983
|
principalId: "u_pager",
|
|
2167
1984
|
limit: 2,
|
|
2168
1985
|
cursor: first.next
|
|
2169
1986
|
});
|
|
2170
|
-
expect(second.items.map((item) => item.token)).
|
|
1987
|
+
expect(second.items.map((item) => item.token)).toStrictEqual(["page-c"]);
|
|
2171
1988
|
expect(second.next).toBeUndefined();
|
|
2172
1989
|
expect((await store.list({
|
|
2173
1990
|
principalId: "u_pager",
|
|
2174
1991
|
cursor: "not a cursor"
|
|
2175
|
-
})).items).
|
|
1992
|
+
})).items).toStrictEqual([]);
|
|
2176
1993
|
});
|
|
2177
1994
|
it("refuses an approver who does not match request.approvers", async () => {
|
|
2178
1995
|
const request = {
|
|
@@ -2194,7 +2011,7 @@ function testApprovalStore(store, options = {}) {
|
|
|
2194
2011
|
await expect(Promise.resolve().then(() => store.resolve("gated-token", {
|
|
2195
2012
|
status: "approved",
|
|
2196
2013
|
by: member
|
|
2197
|
-
}))).rejects.toThrow(/eligible|not pending|not found/);
|
|
2014
|
+
}))).rejects.toThrow(/eligible|not pending|not found/u);
|
|
2198
2015
|
});
|
|
2199
2016
|
it("refuses the principal as approver unless the grant sets distinct: false", async () => {
|
|
2200
2017
|
const principal = {
|
|
@@ -2249,7 +2066,7 @@ function testApprovalStore(store, options = {}) {
|
|
|
2249
2066
|
by: approver
|
|
2250
2067
|
});
|
|
2251
2068
|
expect(first.status).toBe("pending");
|
|
2252
|
-
expect(first.approvals).
|
|
2069
|
+
expect(first.approvals).toStrictEqual([{
|
|
2253
2070
|
by: "u_9",
|
|
2254
2071
|
at: expect.any(String)
|
|
2255
2072
|
}]);
|
|
@@ -2263,7 +2080,7 @@ function testApprovalStore(store, options = {}) {
|
|
|
2263
2080
|
by: secondApprover
|
|
2264
2081
|
});
|
|
2265
2082
|
expect(second.status).toBe("approved");
|
|
2266
|
-
expect(second.approvals?.map((item) => item.by)).
|
|
2083
|
+
expect(second.approvals?.map((item) => item.by)).toStrictEqual(["u_9", "u_10"]);
|
|
2267
2084
|
expect(second.resolvedBy).toBe("u_10");
|
|
2268
2085
|
expect(await store.consume("quorum-token")).not.toBeNull();
|
|
2269
2086
|
});
|
|
@@ -2299,7 +2116,7 @@ function testApprovalStore(store, options = {}) {
|
|
|
2299
2116
|
});
|
|
2300
2117
|
expect(resolved.status).toBe("approved");
|
|
2301
2118
|
expect(resolved.vouched).toBe("app-rule");
|
|
2302
|
-
expect(resolved.approvals).
|
|
2119
|
+
expect(resolved.approvals).toStrictEqual([{
|
|
2303
2120
|
by: "u_outsider",
|
|
2304
2121
|
at: expect.any(String),
|
|
2305
2122
|
vouched: "app-rule"
|
|
@@ -2382,7 +2199,7 @@ function testApprovalStore(store, options = {}) {
|
|
|
2382
2199
|
by: secondApprover
|
|
2383
2200
|
});
|
|
2384
2201
|
expect(first.status).toBe("pending");
|
|
2385
|
-
expect(first.approvals).
|
|
2202
|
+
expect(first.approvals).toStrictEqual([{
|
|
2386
2203
|
by: "u_10",
|
|
2387
2204
|
at: expect.any(String),
|
|
2388
2205
|
stage: 0
|
|
@@ -2392,7 +2209,7 @@ function testApprovalStore(store, options = {}) {
|
|
|
2392
2209
|
by: approver
|
|
2393
2210
|
});
|
|
2394
2211
|
expect(second.status).toBe("approved");
|
|
2395
|
-
expect(second.approvals?.map((item) => item.stage)).
|
|
2212
|
+
expect(second.approvals?.map((item) => item.stage)).toStrictEqual([0, 1]);
|
|
2396
2213
|
});
|
|
2397
2214
|
it("matches a relation approver only through the verdict's relations", async () => {
|
|
2398
2215
|
const manager = {
|
|
@@ -2423,21 +2240,21 @@ function testApprovalStore(store, options = {}) {
|
|
|
2423
2240
|
}
|
|
2424
2241
|
/**
|
|
2425
2242
|
* Checks an `ApprovalPolicySource`: entries are plain JSON, belong to the
|
|
2426
|
-
* queried tenant or to none, and every entry loads against `policy` (an
|
|
2427
|
-
*
|
|
2243
|
+
* queried tenant or to none, and every entry loads against `policy` (an entry
|
|
2244
|
+
* that does not would deny every call).
|
|
2428
2245
|
*/
|
|
2429
2246
|
function testApprovalPolicySource(source, options) {
|
|
2430
2247
|
it("answers per tenant with plain entries that load against the policy", async () => {
|
|
2431
2248
|
const entries = await source.approvalPoliciesFor({ tenants: [options.tenant] });
|
|
2432
2249
|
expect(Array.isArray(entries)).toBe(true);
|
|
2433
2250
|
for (const entry of entries) {
|
|
2434
|
-
expect(JSON.parse(JSON.stringify(entry))).
|
|
2251
|
+
expect(JSON.parse(JSON.stringify(entry))).toStrictEqual(entry);
|
|
2435
2252
|
expect([void 0, options.tenant]).toContain(entry.tenant);
|
|
2436
2253
|
}
|
|
2437
2254
|
const loaded = await approvalPoliciesFor(options.policy, source, [options.tenant], []);
|
|
2438
2255
|
expect(loaded).not.toBe("failed");
|
|
2439
2256
|
const other = await source.approvalPoliciesFor({ tenants: [options.unknown ?? "__permdock_unknown__"] });
|
|
2440
|
-
expect(other.filter((entry) => entry.tenant !== void 0)).
|
|
2257
|
+
expect(other.filter((entry) => entry.tenant !== void 0)).toStrictEqual([]);
|
|
2441
2258
|
});
|
|
2442
2259
|
}
|
|
2443
2260
|
const reviewItem = {
|
|
@@ -2482,7 +2299,7 @@ function testAccessReviewStore(store) {
|
|
|
2482
2299
|
limit: 1
|
|
2483
2300
|
});
|
|
2484
2301
|
expect(page.items).toHaveLength(1);
|
|
2485
|
-
expect(page.next).
|
|
2302
|
+
expect(page.next).toStrictEqual(expect.any(String));
|
|
2486
2303
|
const kept = await store.decide("rev", reviewItem.id, {
|
|
2487
2304
|
status: "kept",
|
|
2488
2305
|
decidedBy: "u_9",
|
|
@@ -2523,7 +2340,7 @@ function testApprovalDelegationSource(source, options) {
|
|
|
2523
2340
|
expect(entries.some((entry) => entry.delegate === options.delegate && entry.delegator === options.delegator)).toBe(true);
|
|
2524
2341
|
const instant = Date.parse(at);
|
|
2525
2342
|
for (const entry of entries) {
|
|
2526
|
-
expect(JSON.parse(JSON.stringify(entry))).
|
|
2343
|
+
expect(JSON.parse(JSON.stringify(entry))).toStrictEqual(entry);
|
|
2527
2344
|
expect(entry.delegate).toBe(options.delegate);
|
|
2528
2345
|
expect(Date.parse(entry.from)).toBeLessThanOrEqual(instant);
|
|
2529
2346
|
expect(Date.parse(entry.until)).toBeGreaterThan(instant);
|
|
@@ -2532,13 +2349,13 @@ function testApprovalDelegationSource(source, options) {
|
|
|
2532
2349
|
delegate: "__permdock_unknown__",
|
|
2533
2350
|
...options.tenant === void 0 ? {} : { tenant: options.tenant },
|
|
2534
2351
|
at
|
|
2535
|
-
})).
|
|
2352
|
+
})).toStrictEqual([]);
|
|
2536
2353
|
});
|
|
2537
2354
|
}
|
|
2538
2355
|
/**
|
|
2539
|
-
* Checks an `IdempotencyStore`: a stored response comes back as written
|
|
2540
|
-
*
|
|
2541
|
-
*
|
|
2356
|
+
* Checks an `IdempotencyStore`: a stored response comes back as written until
|
|
2357
|
+
* it expires, keys never share an entry, and a later `set` replaces an earlier
|
|
2358
|
+
* one.
|
|
2542
2359
|
*/
|
|
2543
2360
|
function testIdempotencyStore(store) {
|
|
2544
2361
|
it("replays a stored response per key and keeps keys apart", async () => {
|
|
@@ -2553,14 +2370,14 @@ function testIdempotencyStore(store) {
|
|
|
2553
2370
|
const later = nowMillis() + 36e5;
|
|
2554
2371
|
expect(await store.get(key)).toBeUndefined();
|
|
2555
2372
|
await store.set(key, response, later);
|
|
2556
|
-
expect(await store.get(key)).
|
|
2373
|
+
expect(await store.get(key)).toStrictEqual(response);
|
|
2557
2374
|
expect(await store.get(other)).toBeUndefined();
|
|
2558
2375
|
const replaced = {
|
|
2559
2376
|
...response,
|
|
2560
2377
|
status: 201
|
|
2561
2378
|
};
|
|
2562
2379
|
await store.set(key, replaced, later);
|
|
2563
|
-
expect(await store.get(key)).
|
|
2380
|
+
expect(await store.get(key)).toStrictEqual(replaced);
|
|
2564
2381
|
});
|
|
2565
2382
|
}
|
|
2566
2383
|
//#endregion
|
|
@@ -2578,7 +2395,7 @@ function testLimitStore(store) {
|
|
|
2578
2395
|
expect(first.remaining).toBeGreaterThanOrEqual(0);
|
|
2579
2396
|
const peeked = store.remaining(input);
|
|
2580
2397
|
if (peeked !== void 0) {
|
|
2581
|
-
expect(
|
|
2398
|
+
expect(peeked.remaining).toBeTypeOf("number");
|
|
2582
2399
|
expect(peeked !== null && typeof peeked === "object" && "then" in peeked && typeof peeked.then === "function").toBe(false);
|
|
2583
2400
|
}
|
|
2584
2401
|
await store.consume(input);
|
|
@@ -2622,7 +2439,7 @@ function testPolicySource(source, options = {}) {
|
|
|
2622
2439
|
};
|
|
2623
2440
|
it("returns null or a v1 policy document synchronously", () => {
|
|
2624
2441
|
const document = readCurrent();
|
|
2625
|
-
if (document !== null) expect(parsePolicyDocument(document)).
|
|
2442
|
+
if (document !== null) expect(parsePolicyDocument(document)).toStrictEqual(document);
|
|
2626
2443
|
});
|
|
2627
2444
|
it("refreshes without rejecting and keeps current() synchronous", async () => {
|
|
2628
2445
|
await expect(Promise.resolve(source.refresh())).resolves.toBeUndefined();
|
|
@@ -2649,7 +2466,9 @@ const OAUTH_FORM$1 = "application/x-www-form-urlencoded";
|
|
|
2649
2466
|
const PLATFORM_SCOPES = {
|
|
2650
2467
|
"projects:read": "List and read the projects the person belongs to",
|
|
2651
2468
|
"policy:read": "Read a project's pushed catalog, manifest and policy",
|
|
2652
|
-
"catalog:write": "Push a catalog and policy with permdock cloud push"
|
|
2469
|
+
"catalog:write": "Push a catalog and policy with permdock cloud push",
|
|
2470
|
+
"evidence:read": "Read a project's decision log and exports, the evidence an access review uses",
|
|
2471
|
+
"approvals:read": "Read a project's approval requests and their verdicts"
|
|
2653
2472
|
};
|
|
2654
2473
|
const ref$1 = (name) => ({ $ref: `#/components/schemas/${name}` });
|
|
2655
2474
|
const RATE_LIMIT_HEADERS = {
|
|
@@ -2683,14 +2502,22 @@ const TOO_MANY = problem$1("Rate limited; retry after Retry-After seconds", {
|
|
|
2683
2502
|
schema: { type: "string" }
|
|
2684
2503
|
}
|
|
2685
2504
|
});
|
|
2505
|
+
/**
|
|
2506
|
+
* The RFC 6750 section 3 challenge a refused read carries, so a client tells a
|
|
2507
|
+
* dead token from a missing scope.
|
|
2508
|
+
*/
|
|
2509
|
+
const challenge = (example) => ({ "WWW-Authenticate": {
|
|
2510
|
+
description: `RFC 6750 Bearer challenge (\`${example}\`)`,
|
|
2511
|
+
schema: { type: "string" }
|
|
2512
|
+
} });
|
|
2686
2513
|
const READ_SECURITY = (scope) => [{ platform: [scope] }, { cloudToken: [] }];
|
|
2687
2514
|
const read = (scope, summary, responses) => ({ get: {
|
|
2688
2515
|
security: READ_SECURITY(scope),
|
|
2689
2516
|
summary,
|
|
2690
2517
|
responses: {
|
|
2691
2518
|
...responses,
|
|
2692
|
-
"401": problem$1("Missing, expired or revoked token"),
|
|
2693
|
-
"403": problem$1(
|
|
2519
|
+
"401": problem$1("Missing, expired or revoked token; a refused token carries `error=\"invalid_token\"`", challenge("Bearer realm=\"permdock\", error=\"invalid_token\"")),
|
|
2520
|
+
"403": problem$1(`The token lacks ${scope} (\`error="insufficient_scope"\` with \`scope\`), or may not read this project; a PERMDOCK_CLOUD_TOKEN reads only its own environment`, challenge(`Bearer realm="permdock", error="insufficient_scope", scope="${scope}"`)),
|
|
2694
2521
|
"429": TOO_MANY
|
|
2695
2522
|
}
|
|
2696
2523
|
} });
|
|
@@ -2776,6 +2603,10 @@ const PLATFORM_SCHEMAS = {
|
|
|
2776
2603
|
minLength: 1
|
|
2777
2604
|
},
|
|
2778
2605
|
name: { type: "string" },
|
|
2606
|
+
slug: {
|
|
2607
|
+
type: "string",
|
|
2608
|
+
description: "The project's URL-safe name, unique in its organization"
|
|
2609
|
+
},
|
|
2779
2610
|
environment: {
|
|
2780
2611
|
type: "string",
|
|
2781
2612
|
minLength: 1
|
|
@@ -2794,6 +2625,11 @@ const PLATFORM_SCHEMAS = {
|
|
|
2794
2625
|
},
|
|
2795
2626
|
fingerprint: { type: "string" }
|
|
2796
2627
|
}
|
|
2628
|
+
},
|
|
2629
|
+
updatedAt: {
|
|
2630
|
+
type: "string",
|
|
2631
|
+
format: "date-time",
|
|
2632
|
+
description: "When the project last changed, RFC 3339"
|
|
2797
2633
|
}
|
|
2798
2634
|
}
|
|
2799
2635
|
},
|
|
@@ -2842,13 +2678,24 @@ const PROJECT_ID = {
|
|
|
2842
2678
|
minLength: 1
|
|
2843
2679
|
}
|
|
2844
2680
|
};
|
|
2845
|
-
/**
|
|
2681
|
+
/**
|
|
2682
|
+
* Routes of the platform issuer and its read API; they sit at the API root,
|
|
2683
|
+
* outside /v1/environments/{env}.
|
|
2684
|
+
*/
|
|
2846
2685
|
const PLATFORM_PATHS = {
|
|
2847
2686
|
"/.well-known/oauth-authorization-server": { get: {
|
|
2848
2687
|
security: [],
|
|
2849
2688
|
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
2689
|
responses: { "200": oauthJson("The metadata", ref$1("AuthorizationServerMetadata")) }
|
|
2851
2690
|
} },
|
|
2691
|
+
"/openapi.json": { get: {
|
|
2692
|
+
security: [],
|
|
2693
|
+
summary: "This document, the contract of https://permdock.com/api. permdock/schemas/cloud-api-v1.json ships the same document in the npm package.",
|
|
2694
|
+
responses: { "200": oauthJson("The OpenAPI 3.1 document", {
|
|
2695
|
+
type: "object",
|
|
2696
|
+
required: ["openapi", "paths"]
|
|
2697
|
+
}) }
|
|
2698
|
+
} },
|
|
2852
2699
|
"/oauth/authorize": { get: {
|
|
2853
2700
|
security: [],
|
|
2854
2701
|
summary: "Authorization code with PKCE S256 for public clients, including the RFC 8252 loopback redirect http://127.0.0.1:{port}/callback",
|
|
@@ -2981,17 +2828,25 @@ const PLATFORM_PATHS = {
|
|
|
2981
2828
|
"400": oauthJson("invalid_redirect_uri or invalid_client_metadata", ref$1("OAuthError"))
|
|
2982
2829
|
}
|
|
2983
2830
|
} },
|
|
2984
|
-
"/v1/projects":
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2831
|
+
"/v1/projects": {
|
|
2832
|
+
parameters: [{
|
|
2833
|
+
name: "cursor",
|
|
2834
|
+
in: "query",
|
|
2835
|
+
description: "The `next` of the previous page",
|
|
2836
|
+
schema: { type: "string" }
|
|
2837
|
+
}],
|
|
2838
|
+
...read("projects:read", "The projects the token reads", { "200": limited("The projects", {
|
|
2839
|
+
type: "object",
|
|
2840
|
+
required: ["items"],
|
|
2841
|
+
properties: {
|
|
2842
|
+
items: {
|
|
2843
|
+
type: "array",
|
|
2844
|
+
items: ref$1("Project")
|
|
2845
|
+
},
|
|
2846
|
+
next: { type: "string" }
|
|
2847
|
+
}
|
|
2848
|
+
}) })
|
|
2849
|
+
},
|
|
2995
2850
|
"/v1/projects/{id}": {
|
|
2996
2851
|
parameters: [PROJECT_ID],
|
|
2997
2852
|
...read("projects:read", "One project", {
|
|
@@ -3027,7 +2882,10 @@ const PLATFORM_SECURITY_SCHEMES = {
|
|
|
3027
2882
|
//#endregion
|
|
3028
2883
|
//#region src/testing/cloud-api/platform-checks.ts
|
|
3029
2884
|
const ITEM = /^"[^"]+"(?:;[a-z]+=-?\d+)+$/u;
|
|
3030
|
-
/**
|
|
2885
|
+
/**
|
|
2886
|
+
* Whether a RateLimit or RateLimit-Policy field value lists items with the
|
|
2887
|
+
* parameters `keys` (draft-ietf-httpapi-ratelimit-headers-11).
|
|
2888
|
+
*/
|
|
3031
2889
|
function rateLimitField(value, keys) {
|
|
3032
2890
|
if (value === null || value.trim() === "") return false;
|
|
3033
2891
|
return value.split(",").every((raw) => {
|
|
@@ -3038,6 +2896,7 @@ function rateLimitField(value, keys) {
|
|
|
3038
2896
|
function rateLimited(response) {
|
|
3039
2897
|
return rateLimitField(response.headers.get("ratelimit"), ["r", "t"]) && rateLimitField(response.headers.get("ratelimit-policy"), ["q", "w"]);
|
|
3040
2898
|
}
|
|
2899
|
+
const challengeOf = (response) => bearerChallenge(response.headers.get("www-authenticate"));
|
|
3041
2900
|
function problemType(response, body) {
|
|
3042
2901
|
return (response.headers.get("content-type") ?? "").startsWith("application/problem+json") && isRecord(body) ? body["type"] : void 0;
|
|
3043
2902
|
}
|
|
@@ -3051,14 +2910,14 @@ function testCloudPlatform(url, environment, options, fetchFn) {
|
|
|
3051
2910
|
body: new URLSearchParams(fields).toString()
|
|
3052
2911
|
});
|
|
3053
2912
|
it("publishes RFC 8414 metadata for the platform issuer", async () => {
|
|
3054
|
-
const metadata = await discoverAuthorizationServer(url, { fetch: (input, init) => fetchFn(
|
|
2913
|
+
const metadata = await discoverAuthorizationServer(url, { fetch: (input, init) => fetchFn(input instanceof Request ? input.url : input.toString(), init) });
|
|
3055
2914
|
expect(metadata).not.toBeNull();
|
|
3056
2915
|
expect(metadata).toMatchObject({
|
|
3057
2916
|
issuer: url,
|
|
3058
2917
|
code_challenge_methods_supported: ["S256"]
|
|
3059
2918
|
});
|
|
3060
2919
|
const scopes = metadata?.["scopes_supported"];
|
|
3061
|
-
expect(scopes).
|
|
2920
|
+
expect(scopes).toStrictEqual(expect.arrayContaining(Object.keys(PLATFORM_SCOPES)));
|
|
3062
2921
|
});
|
|
3063
2922
|
it("lists and reads the project and its policy with RateLimit fields", async () => {
|
|
3064
2923
|
const list = await read("/v1/projects", options.token);
|
|
@@ -3106,6 +2965,38 @@ function testCloudPlatform(url, environment, options, fetchFn) {
|
|
|
3106
2965
|
expect(response.status).toBe(401);
|
|
3107
2966
|
expect(response.headers.get("content-type")).toMatch(/^application\/problem\+json/u);
|
|
3108
2967
|
});
|
|
2968
|
+
it("answers 401 with an error=\"invalid_token\" challenge to an unknown token", async () => {
|
|
2969
|
+
const response = await read("/v1/projects", `unknown-${globalThis.crypto.randomUUID()}`);
|
|
2970
|
+
expect(response.status).toBe(401);
|
|
2971
|
+
expect(challengeOf(response)).toMatchObject({
|
|
2972
|
+
scheme: "bearer",
|
|
2973
|
+
params: { error: "invalid_token" }
|
|
2974
|
+
});
|
|
2975
|
+
});
|
|
2976
|
+
it("answers 403 with an error=\"insufficient_scope\" challenge naming the scope", async (context) => {
|
|
2977
|
+
if (options.projectsOnly === void 0) {
|
|
2978
|
+
context.skip();
|
|
2979
|
+
return;
|
|
2980
|
+
}
|
|
2981
|
+
const response = await read(`/v1/projects/${encodeURIComponent(project)}/policy`, options.projectsOnly);
|
|
2982
|
+
expect(response.status).toBe(403);
|
|
2983
|
+
expect(challengeOf(response)).toMatchObject({
|
|
2984
|
+
scheme: "bearer",
|
|
2985
|
+
params: {
|
|
2986
|
+
error: "insufficient_scope",
|
|
2987
|
+
scope: "policy:read"
|
|
2988
|
+
}
|
|
2989
|
+
});
|
|
2990
|
+
});
|
|
2991
|
+
it("serves its OpenAPI document at /openapi.json", async () => {
|
|
2992
|
+
const response = await read("/openapi.json", null);
|
|
2993
|
+
expect(response.status).toBe(200);
|
|
2994
|
+
const body = await response.json();
|
|
2995
|
+
expect(body).toMatchObject({
|
|
2996
|
+
openapi: expect.stringMatching(/^3\.[1-9]\d*\./u),
|
|
2997
|
+
paths: expect.objectContaining({ "/v1/projects": expect.any(Object) })
|
|
2998
|
+
});
|
|
2999
|
+
});
|
|
3109
3000
|
it("refuses a catalog push with the read-only token", async () => {
|
|
3110
3001
|
const response = await fetchFn(`${url}/v1/environments/${encodeURIComponent(project)}/catalog`, {
|
|
3111
3002
|
method: "POST",
|
|
@@ -3145,436 +3036,13 @@ function testCloudPlatform(url, environment, options, fetchFn) {
|
|
|
3145
3036
|
const loopback = await register("http://127.0.0.1:33418/callback");
|
|
3146
3037
|
expect(loopback.status).toBe(201);
|
|
3147
3038
|
expect(await loopback.json()).toMatchObject({ client_id: expect.any(String) });
|
|
3148
|
-
|
|
3039
|
+
const { status } = await register("http://mcp.example.invalid/callback");
|
|
3040
|
+
expect(status).toBe(400);
|
|
3149
3041
|
});
|
|
3150
3042
|
}
|
|
3151
3043
|
//#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
3044
|
//#region src/testing/cloud-api/document.ts
|
|
3045
|
+
var import__2020 = require__2020();
|
|
3578
3046
|
const ENV = "/v1/environments/{env}";
|
|
3579
3047
|
const COMPACT_JWS = String.raw`^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+$`;
|
|
3580
3048
|
const APPROVAL_REQUEST = "https://permdock.com/schemas/approval-request-v1.json";
|
|
@@ -4073,6 +3541,31 @@ const APPROVALS = {
|
|
|
4073
3541
|
}) }
|
|
4074
3542
|
};
|
|
4075
3543
|
const RUNTIME = {
|
|
3544
|
+
"/v1/environment": { get: client({
|
|
3545
|
+
summary: "The environment the key belongs to; cloud() calls it when no environment is configured",
|
|
3546
|
+
responses: { "200": json("The key's environment", {
|
|
3547
|
+
type: "object",
|
|
3548
|
+
required: [
|
|
3549
|
+
"environment",
|
|
3550
|
+
"issuer",
|
|
3551
|
+
"jwks_uri"
|
|
3552
|
+
],
|
|
3553
|
+
properties: {
|
|
3554
|
+
environment: {
|
|
3555
|
+
type: "string",
|
|
3556
|
+
minLength: 1
|
|
3557
|
+
},
|
|
3558
|
+
issuer: {
|
|
3559
|
+
type: "string",
|
|
3560
|
+
format: "uri"
|
|
3561
|
+
},
|
|
3562
|
+
jwks_uri: {
|
|
3563
|
+
type: "string",
|
|
3564
|
+
format: "uri"
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
}) }
|
|
3568
|
+
}) },
|
|
4076
3569
|
[`${ENV}/decisions`]: { post: client({
|
|
4077
3570
|
summary: "sink.write and sink.flush",
|
|
4078
3571
|
requestBody: body({
|
|
@@ -4563,7 +4056,8 @@ const bearer = (description) => ({
|
|
|
4563
4056
|
});
|
|
4564
4057
|
/**
|
|
4565
4058
|
* `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
|
|
4059
|
+
* document. The route tables in `adapters/cloud.mdx` and `docs:drift` keep it
|
|
4060
|
+
* whole.
|
|
4567
4061
|
*/
|
|
4568
4062
|
const CLOUD_API_DOCUMENT = freezeDeep({
|
|
4569
4063
|
openapi: "3.1.1",
|
|
@@ -4666,7 +4160,10 @@ function routesOf(paths) {
|
|
|
4666
4160
|
function pointer(parts) {
|
|
4667
4161
|
return parts.map((part) => part.replaceAll("~", "~0").replaceAll("/", "~1")).join("/");
|
|
4668
4162
|
}
|
|
4669
|
-
/**
|
|
4163
|
+
/**
|
|
4164
|
+
* A checker for responses of the PermDock Cloud API, compiled once from
|
|
4165
|
+
* `CLOUD_API_DOCUMENT`.
|
|
4166
|
+
*/
|
|
4670
4167
|
function cloudApiValidator() {
|
|
4671
4168
|
const ajv = new import__2020.Ajv2020({
|
|
4672
4169
|
strict: false,
|
|
@@ -4776,28 +4273,26 @@ async function problemCode(response) {
|
|
|
4776
4273
|
* Runs the conformance runners and the HTTP contract checks against one
|
|
4777
4274
|
* PermDock Cloud environment, and validates every response body against
|
|
4778
4275
|
* `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
|
-
*
|
|
4276
|
+
* sends decision events and deliveries to `.invalid` addresses, and pushes the
|
|
4277
|
+
* catalog the environment already holds.
|
|
4781
4278
|
*/
|
|
4782
4279
|
function testCloudApi(baseUrl, keys, options) {
|
|
4783
4280
|
const fetchFn = options.fetch ?? globalThis.fetch.bind(globalThis);
|
|
4784
4281
|
const url = trimSlash(baseUrl);
|
|
4785
4282
|
const prefix = trimSlash(new URL(url).pathname);
|
|
4283
|
+
const environment = options.environment ?? "production";
|
|
4786
4284
|
const { issuer } = cloudEndpoints({
|
|
4787
4285
|
url,
|
|
4788
|
-
|
|
4286
|
+
environment
|
|
4789
4287
|
});
|
|
4790
4288
|
const check = cloudApiValidator();
|
|
4791
|
-
const problems = [];
|
|
4792
4289
|
const checks = [];
|
|
4793
4290
|
const recording = async (input, init) => {
|
|
4794
4291
|
const request = new Request(input, init);
|
|
4795
4292
|
const response = await fetchFn(request);
|
|
4796
4293
|
const { pathname } = new URL(request.url);
|
|
4797
4294
|
const path = pathname === `${METADATA}${prefix}` ? METADATA : pathname.slice(prefix.length);
|
|
4798
|
-
checks.push(check(request.method, path, response.clone())
|
|
4799
|
-
problems.push(...found);
|
|
4800
|
-
}));
|
|
4295
|
+
checks.push(check(request.method, path, response.clone()));
|
|
4801
4296
|
return response;
|
|
4802
4297
|
};
|
|
4803
4298
|
const client = cloud({
|
|
@@ -4805,7 +4300,7 @@ function testCloudApi(baseUrl, keys, options) {
|
|
|
4805
4300
|
key: keys.client,
|
|
4806
4301
|
fetch: recording,
|
|
4807
4302
|
approver: options.approver,
|
|
4808
|
-
|
|
4303
|
+
environment,
|
|
4809
4304
|
...options.verifier === void 0 ? {} : { verifier: options.verifier }
|
|
4810
4305
|
});
|
|
4811
4306
|
const call = (path, key, init = {}) => {
|
|
@@ -4851,21 +4346,33 @@ function testCloudApi(baseUrl, keys, options) {
|
|
|
4851
4346
|
origin: issuer,
|
|
4852
4347
|
...options.authzen.headers === void 0 ? {} : { headers: options.authzen.headers }
|
|
4853
4348
|
});
|
|
4854
|
-
if (options.platform !== void 0) testCloudPlatform(url,
|
|
4349
|
+
if (options.platform !== void 0) testCloudPlatform(url, environment, options.platform, recording);
|
|
4855
4350
|
it("answers 401 Problem Details without a key", async () => {
|
|
4856
4351
|
const response = await call("/approvals", null);
|
|
4857
4352
|
expect(response.status).toBe(401);
|
|
4858
4353
|
expect(response.headers.get("content-type")).toMatch(/^application\/problem\+json/u);
|
|
4859
4354
|
});
|
|
4355
|
+
it("names the client key's environment at GET /v1/environment", async () => {
|
|
4356
|
+
const response = await recording(`${url}/v1/environment`, { headers: { authorization: `Bearer ${keys.client}` } });
|
|
4357
|
+
expect(response.status).toBe(200);
|
|
4358
|
+
expect(await response.json()).toMatchObject({
|
|
4359
|
+
environment,
|
|
4360
|
+
issuer,
|
|
4361
|
+
jwks_uri: `${issuer}/.well-known/jwks.json`
|
|
4362
|
+
});
|
|
4363
|
+
const { status } = await recording(`${url}/v1/environment`);
|
|
4364
|
+
expect(status).toBe(401);
|
|
4365
|
+
});
|
|
4860
4366
|
it("answers 403 to a key of another kind", async () => {
|
|
4861
|
-
|
|
4367
|
+
const { status } = await call("/hosted-grants", keys.client);
|
|
4368
|
+
expect(status).toBe(403);
|
|
4862
4369
|
if (keys.export !== void 0) expect((await call("/approvals", keys.export)).status).toBe(403);
|
|
4863
4370
|
});
|
|
4864
4371
|
it("sends an ETag on the approval read and refuses an approver token with a stale one", async () => {
|
|
4865
4372
|
const token = tag();
|
|
4866
4373
|
await client.approvals.create(sample(token));
|
|
4867
4374
|
const { request, etag } = await current(token);
|
|
4868
|
-
expect(etag).
|
|
4375
|
+
expect(etag).toStrictEqual(expect.any(String));
|
|
4869
4376
|
const stale = await resolveRaw(token, {
|
|
4870
4377
|
status: "approved",
|
|
4871
4378
|
by: admin
|
|
@@ -4926,7 +4433,7 @@ function testCloudApi(baseUrl, keys, options) {
|
|
|
4926
4433
|
aud: issuer
|
|
4927
4434
|
})
|
|
4928
4435
|
});
|
|
4929
|
-
expect([typ, response.status]).
|
|
4436
|
+
expect([typ, response.status]).toStrictEqual([typ, 422]);
|
|
4930
4437
|
}
|
|
4931
4438
|
});
|
|
4932
4439
|
it("repeats a delivery for a repeated Idempotency-Key and requires one", async () => {
|
|
@@ -4954,7 +4461,8 @@ function testCloudApi(baseUrl, keys, options) {
|
|
|
4954
4461
|
expect(first.ok && second.ok).toBe(true);
|
|
4955
4462
|
const [a, b] = await Promise.all([first.json(), second.json()]);
|
|
4956
4463
|
expect(isRecord(a) && isRecord(b) && a["id"] === b["id"]).toBe(true);
|
|
4957
|
-
|
|
4464
|
+
const { status } = await send({});
|
|
4465
|
+
expect(status).toBe(400);
|
|
4958
4466
|
});
|
|
4959
4467
|
it("answers unchanged for the catalog it already holds", async (context) => {
|
|
4960
4468
|
const response = await call("/catalog", keys.client);
|
|
@@ -4991,12 +4499,12 @@ function testCloudApi(baseUrl, keys, options) {
|
|
|
4991
4499
|
const first = await get({ "permdock-subject": token });
|
|
4992
4500
|
expect(first.status).toBe(200);
|
|
4993
4501
|
expect(first.headers.get("content-type")).toMatch(/^application\/jwt/u);
|
|
4994
|
-
|
|
4502
|
+
const { status } = await get({ "permdock-subject": token });
|
|
4503
|
+
expect(status).toBe(401);
|
|
4995
4504
|
expect((await get({})).status).toBe(401);
|
|
4996
4505
|
});
|
|
4997
4506
|
it("answers every request in the shapes cloud-api-v1.json documents", async () => {
|
|
4998
|
-
await Promise.all(checks);
|
|
4999
|
-
expect(problems).toEqual([]);
|
|
4507
|
+
expect((await Promise.all(checks)).flat()).toStrictEqual([]);
|
|
5000
4508
|
});
|
|
5001
4509
|
}
|
|
5002
4510
|
//#endregion
|
|
@@ -5157,10 +4665,10 @@ const WHERE_SUBJECT = {
|
|
|
5157
4665
|
context: {}
|
|
5158
4666
|
};
|
|
5159
4667
|
/**
|
|
5160
|
-
* `matches` answers whether the compiled output selects `row`, for example
|
|
5161
|
-
*
|
|
5162
|
-
*
|
|
5163
|
-
*
|
|
4668
|
+
* `matches` answers whether the compiled output selects `row`, for example by
|
|
4669
|
+
* running it against a table holding only that row. With it, every case must
|
|
4670
|
+
* select the rows the in-memory evaluator keeps, including rows whose fields
|
|
4671
|
+
* are NULL.
|
|
5164
4672
|
*/
|
|
5165
4673
|
function testWhereCompiler(compiler, options) {
|
|
5166
4674
|
const { matches } = options;
|
|
@@ -5169,7 +4677,7 @@ function testWhereCompiler(compiler, options) {
|
|
|
5169
4677
|
const compiled = compiler(condition, options.target);
|
|
5170
4678
|
const selected = [];
|
|
5171
4679
|
for (const row of WHERE_ROWS) if (await matches(compiled, row)) selected.push(row["id"]);
|
|
5172
|
-
expect(selected).
|
|
4680
|
+
expect(selected).toStrictEqual(WHERE_ROWS.filter((row) => evaluateCondition(condition, row, WHERE_SUBJECT)).map((row) => row["id"]));
|
|
5173
4681
|
});
|
|
5174
4682
|
it("fails closed on an empty allow set", () => {
|
|
5175
4683
|
const compiled = compiler({
|
|
@@ -5237,7 +4745,7 @@ function testReplayStore(store) {
|
|
|
5237
4745
|
}
|
|
5238
4746
|
const expiresAt = nowSeconds() + 60;
|
|
5239
4747
|
const results = await Promise.all([store.claim("claim-key", expiresAt), store.claim("claim-key", expiresAt)]);
|
|
5240
|
-
expect(results.toSorted()).
|
|
4748
|
+
expect(results.toSorted()).toStrictEqual([false, true]);
|
|
5241
4749
|
expect(await store.seen("claim-key")).toBe(true);
|
|
5242
4750
|
await store.release("claim-key");
|
|
5243
4751
|
expect(await store.claim("claim-key", expiresAt)).toBe(true);
|
|
@@ -5248,18 +4756,18 @@ function testSetQueue(queue) {
|
|
|
5248
4756
|
await queue.enqueue("stream-a", "a1", "set-a1");
|
|
5249
4757
|
await queue.enqueue("stream-a", "a2", "set-a2");
|
|
5250
4758
|
await queue.enqueue("stream-b", "b1", "set-b1");
|
|
5251
|
-
expect(await queue.pending("stream-a", 10)).
|
|
4759
|
+
expect(await queue.pending("stream-a", 10)).toStrictEqual([{
|
|
5252
4760
|
jti: "a1",
|
|
5253
4761
|
set: "set-a1"
|
|
5254
4762
|
}, {
|
|
5255
4763
|
jti: "a2",
|
|
5256
4764
|
set: "set-a2"
|
|
5257
4765
|
}]);
|
|
5258
|
-
expect(await queue.pending("stream-a", 1)).
|
|
4766
|
+
expect(await queue.pending("stream-a", 1)).toStrictEqual([{
|
|
5259
4767
|
jti: "a1",
|
|
5260
4768
|
set: "set-a1"
|
|
5261
4769
|
}]);
|
|
5262
|
-
expect(await queue.pending("stream-b", 10)).
|
|
4770
|
+
expect(await queue.pending("stream-b", 10)).toStrictEqual([{
|
|
5263
4771
|
jti: "b1",
|
|
5264
4772
|
set: "set-b1"
|
|
5265
4773
|
}]);
|
|
@@ -5274,12 +4782,76 @@ function testSetQueue(queue) {
|
|
|
5274
4782
|
description: "unknown kid"
|
|
5275
4783
|
});
|
|
5276
4784
|
await queue.ack("stream-other", "c3");
|
|
5277
|
-
expect(await queue.pending("stream-c", 10)).
|
|
4785
|
+
expect(await queue.pending("stream-c", 10)).toStrictEqual([{
|
|
5278
4786
|
jti: "c3",
|
|
5279
4787
|
set: "set-c3"
|
|
5280
4788
|
}]);
|
|
5281
4789
|
});
|
|
5282
4790
|
}
|
|
4791
|
+
function streamConfiguration(id, description) {
|
|
4792
|
+
return {
|
|
4793
|
+
stream_id: id,
|
|
4794
|
+
iss: "https://transmitter.example",
|
|
4795
|
+
aud: "receiver",
|
|
4796
|
+
events_requested: [],
|
|
4797
|
+
delivery: { method: "urn:ietf:rfc:8936" },
|
|
4798
|
+
...description === void 0 ? {} : { description }
|
|
4799
|
+
};
|
|
4800
|
+
}
|
|
4801
|
+
function testSsfStreamStore(store) {
|
|
4802
|
+
const id = (suffix) => `stream-${suffix}-${crypto.randomUUID()}`;
|
|
4803
|
+
it("creates an enabled stream and reads it back by id and by owner", async () => {
|
|
4804
|
+
const first = id("a");
|
|
4805
|
+
const second = id("b");
|
|
4806
|
+
const owner = `owner-${crypto.randomUUID()}`;
|
|
4807
|
+
await store.create(owner, streamConfiguration(first));
|
|
4808
|
+
await store.create(owner, streamConfiguration(second));
|
|
4809
|
+
await store.create("someone-else", streamConfiguration(id("c")));
|
|
4810
|
+
expect(await store.get(first)).toStrictEqual({
|
|
4811
|
+
owner,
|
|
4812
|
+
configuration: streamConfiguration(first)
|
|
4813
|
+
});
|
|
4814
|
+
expect(await store.status(first)).toStrictEqual({
|
|
4815
|
+
stream_id: first,
|
|
4816
|
+
status: "enabled"
|
|
4817
|
+
});
|
|
4818
|
+
const listed = await store.list(owner);
|
|
4819
|
+
expect(listed.map((stream) => stream.configuration.stream_id)).toStrictEqual([first, second]);
|
|
4820
|
+
const all = await store.list();
|
|
4821
|
+
expect(all.map((stream) => stream.configuration.stream_id)).toStrictEqual(expect.arrayContaining([first, second]));
|
|
4822
|
+
});
|
|
4823
|
+
it("answers null for a stream that does not exist", async () => {
|
|
4824
|
+
expect(await store.get(id("missing"))).toBeNull();
|
|
4825
|
+
expect(await store.status(id("missing"))).toBeNull();
|
|
4826
|
+
});
|
|
4827
|
+
it("replaces the configuration and keeps the owner and status", async () => {
|
|
4828
|
+
const stream = id("update");
|
|
4829
|
+
await store.create("owner-update", streamConfiguration(stream));
|
|
4830
|
+
await store.setStatus({
|
|
4831
|
+
stream_id: stream,
|
|
4832
|
+
status: "paused",
|
|
4833
|
+
reason: "maintenance"
|
|
4834
|
+
});
|
|
4835
|
+
await store.update(streamConfiguration(stream, "renamed"));
|
|
4836
|
+
expect(await store.get(stream)).toStrictEqual({
|
|
4837
|
+
owner: "owner-update",
|
|
4838
|
+
configuration: streamConfiguration(stream, "renamed")
|
|
4839
|
+
});
|
|
4840
|
+
expect(await store.status(stream)).toStrictEqual({
|
|
4841
|
+
stream_id: stream,
|
|
4842
|
+
status: "paused",
|
|
4843
|
+
reason: "maintenance"
|
|
4844
|
+
});
|
|
4845
|
+
});
|
|
4846
|
+
it("forgets a deleted stream and its status", async () => {
|
|
4847
|
+
const stream = id("delete");
|
|
4848
|
+
await store.create("owner-delete", streamConfiguration(stream));
|
|
4849
|
+
await store.delete(stream);
|
|
4850
|
+
expect(await store.get(stream)).toBeNull();
|
|
4851
|
+
expect(await store.status(stream)).toBeNull();
|
|
4852
|
+
expect((await store.list("owner-delete")).some((entry) => entry.configuration.stream_id === stream)).toBe(false);
|
|
4853
|
+
});
|
|
4854
|
+
}
|
|
5283
4855
|
function testRevocationFeed(feed) {
|
|
5284
4856
|
it("delivers each event to every subscriber until it unsubscribes", async () => {
|
|
5285
4857
|
const first = [];
|
|
@@ -5297,8 +4869,8 @@ function testRevocationFeed(feed) {
|
|
|
5297
4869
|
};
|
|
5298
4870
|
await feed.revoke(revoked);
|
|
5299
4871
|
await vi.waitFor(() => {
|
|
5300
|
-
expect(first).
|
|
5301
|
-
expect(second).
|
|
4872
|
+
expect(first).toStrictEqual([revoked]);
|
|
4873
|
+
expect(second).toStrictEqual([revoked]);
|
|
5302
4874
|
});
|
|
5303
4875
|
stopFirst();
|
|
5304
4876
|
const changed = {
|
|
@@ -5308,9 +4880,9 @@ function testRevocationFeed(feed) {
|
|
|
5308
4880
|
};
|
|
5309
4881
|
await feed.revoke(changed);
|
|
5310
4882
|
await vi.waitFor(() => {
|
|
5311
|
-
expect(second).
|
|
4883
|
+
expect(second).toStrictEqual([revoked, changed]);
|
|
5312
4884
|
});
|
|
5313
|
-
expect(first).
|
|
4885
|
+
expect(first).toStrictEqual([revoked]);
|
|
5314
4886
|
stopSecond();
|
|
5315
4887
|
});
|
|
5316
4888
|
it("keeps delivering when a listener throws", async () => {
|
|
@@ -5327,7 +4899,7 @@ function testRevocationFeed(feed) {
|
|
|
5327
4899
|
};
|
|
5328
4900
|
await feed.revoke(event);
|
|
5329
4901
|
await vi.waitFor(() => {
|
|
5330
|
-
expect(seen).
|
|
4902
|
+
expect(seen).toStrictEqual([event]);
|
|
5331
4903
|
});
|
|
5332
4904
|
stopThrowing();
|
|
5333
4905
|
stop();
|
|
@@ -5346,7 +4918,7 @@ function testRevocationFeed(feed) {
|
|
|
5346
4918
|
}]) await expect((async () => {
|
|
5347
4919
|
await feed.revoke(event);
|
|
5348
4920
|
})()).rejects.toThrow(TypeError);
|
|
5349
|
-
expect(seen).
|
|
4921
|
+
expect(seen).toStrictEqual([]);
|
|
5350
4922
|
stop();
|
|
5351
4923
|
});
|
|
5352
4924
|
}
|
|
@@ -5421,8 +4993,8 @@ function flipLast(value) {
|
|
|
5421
4993
|
}
|
|
5422
4994
|
/**
|
|
5423
4995
|
* `key` is a live key the verifier knows, or a function issuing one (called
|
|
5424
|
-
* once). `revoke`, when given, revokes it; the runner then expects the key
|
|
5425
|
-
*
|
|
4996
|
+
* once). `revoke`, when given, revokes it; the runner then expects the key to
|
|
4997
|
+
* stop verifying.
|
|
5426
4998
|
*/
|
|
5427
4999
|
function testCredentialVerifier(verifier, options) {
|
|
5428
5000
|
let issued;
|
|
@@ -5438,7 +5010,7 @@ function testCredentialVerifier(verifier, options) {
|
|
|
5438
5010
|
const credential = parseCredential(await verifier.verify(key));
|
|
5439
5011
|
expect(credential).toBeDefined();
|
|
5440
5012
|
expect(credential?.id).toBe(parts?.id);
|
|
5441
|
-
expect(parseCredential(await verifier.verify(key))).
|
|
5013
|
+
expect(parseCredential(await verifier.verify(key))).toStrictEqual(credential);
|
|
5442
5014
|
});
|
|
5443
5015
|
it("never throws and answers null for every other key", async () => {
|
|
5444
5016
|
const key = await live();
|
|
@@ -5468,7 +5040,7 @@ function testCredentialVerifier(verifier, options) {
|
|
|
5468
5040
|
const id = parseApiKey(key)?.id ?? "";
|
|
5469
5041
|
await verifier.touch?.(id, { epochMilliseconds: nowMillis() });
|
|
5470
5042
|
await verifier.touch?.("unknown-id", { epochMilliseconds: nowMillis() });
|
|
5471
|
-
expect(parseCredential(await verifier.verify(key))).
|
|
5043
|
+
expect(parseCredential(await verifier.verify(key))).toStrictEqual(before);
|
|
5472
5044
|
});
|
|
5473
5045
|
if (options.revoke !== void 0) {
|
|
5474
5046
|
const revoke = options.revoke;
|
|
@@ -5489,7 +5061,7 @@ function testTokenSigner(signer, options) {
|
|
|
5489
5061
|
audience: "https://app.example.com"
|
|
5490
5062
|
});
|
|
5491
5063
|
const header = decodeHeader(token);
|
|
5492
|
-
expect(Object.keys(header).toSorted()).
|
|
5064
|
+
expect(Object.keys(header).toSorted()).toStrictEqual([
|
|
5493
5065
|
"alg",
|
|
5494
5066
|
"kid",
|
|
5495
5067
|
"typ"
|
|
@@ -5535,7 +5107,7 @@ function testDecisionProvider(provider, options) {
|
|
|
5535
5107
|
local: LOCAL
|
|
5536
5108
|
});
|
|
5537
5109
|
it("names itself and claims the permission synchronously", () => {
|
|
5538
|
-
expect(
|
|
5110
|
+
expect(provider.name).toBeTypeOf("string");
|
|
5539
5111
|
expect(provider.name.length).toBeGreaterThan(0);
|
|
5540
5112
|
expect(provider.handles(options.permission)).toBe(true);
|
|
5541
5113
|
});
|
|
@@ -5611,12 +5183,12 @@ function testRelationSource(source, options) {
|
|
|
5611
5183
|
});
|
|
5612
5184
|
expect(Array.isArray(full.ancestors)).toBe(true);
|
|
5613
5185
|
for (const ancestor of full.ancestors) {
|
|
5614
|
-
expect(
|
|
5186
|
+
expect(ancestor.id).toBeTypeOf("string");
|
|
5615
5187
|
expect(ancestor.id).not.toBe("");
|
|
5616
5188
|
}
|
|
5617
5189
|
expect(full.ancestors.length).toBeLessThanOrEqual(32);
|
|
5618
5190
|
const expected = options.expect?.ancestors?.[`${object.resource}:${object.id}`];
|
|
5619
|
-
if (expected !== void 0) expect(full.ancestors.map((ancestor) => ancestor.id)).
|
|
5191
|
+
if (expected !== void 0) expect(full.ancestors.map((ancestor) => ancestor.id)).toStrictEqual(expected);
|
|
5620
5192
|
if (full.ancestors.length > 1) {
|
|
5621
5193
|
const short = await source.ancestors({
|
|
5622
5194
|
resource: object.resource,
|
|
@@ -5624,7 +5196,7 @@ function testRelationSource(source, options) {
|
|
|
5624
5196
|
through: "parent",
|
|
5625
5197
|
depth: 1
|
|
5626
5198
|
});
|
|
5627
|
-
expect(short.ancestors.map((ancestor) => ancestor.id)).
|
|
5199
|
+
expect(short.ancestors.map((ancestor) => ancestor.id)).toStrictEqual([full.ancestors[0]?.id]);
|
|
5628
5200
|
expect(short.truncated).toBe(true);
|
|
5629
5201
|
}
|
|
5630
5202
|
const none = await source.ancestors({
|
|
@@ -5633,7 +5205,7 @@ function testRelationSource(source, options) {
|
|
|
5633
5205
|
through: "parent",
|
|
5634
5206
|
depth: 0
|
|
5635
5207
|
});
|
|
5636
|
-
expect(none.ancestors).
|
|
5208
|
+
expect(none.ancestors).toStrictEqual([]);
|
|
5637
5209
|
}
|
|
5638
5210
|
});
|
|
5639
5211
|
it("returns well-formed holders", async () => {
|
|
@@ -5651,14 +5223,14 @@ function testRelationSource(source, options) {
|
|
|
5651
5223
|
holder.group.id,
|
|
5652
5224
|
holder.group.relation
|
|
5653
5225
|
]) {
|
|
5654
|
-
expect(
|
|
5226
|
+
expect(part).toBeTypeOf("string");
|
|
5655
5227
|
expect(part).not.toBe("");
|
|
5656
5228
|
}
|
|
5657
|
-
else expect(
|
|
5229
|
+
else expect(holder.principal.id).toBeTypeOf("string");
|
|
5658
5230
|
for (const instant of [holder.startsAt, holder.expiresAt]) expect(instant === void 0 || typeof instant === "number" && Number.isFinite(instant)).toBe(true);
|
|
5659
5231
|
}
|
|
5660
5232
|
const expected = options.expect?.holders?.[`${object.resource}:${object.id}#${object.relation}`];
|
|
5661
|
-
if (expected !== void 0) expect(holders.map(holderKey).toSorted()).
|
|
5233
|
+
if (expected !== void 0) expect(holders.map(holderKey).toSorted()).toStrictEqual([...expected].toSorted());
|
|
5662
5234
|
}
|
|
5663
5235
|
});
|
|
5664
5236
|
it("follows each link to exactly one instance", async () => {
|
|
@@ -5672,7 +5244,7 @@ function testRelationSource(source, options) {
|
|
|
5672
5244
|
through,
|
|
5673
5245
|
depth: 1
|
|
5674
5246
|
});
|
|
5675
|
-
expect(linked.ancestors.map((ancestor) => ancestor.id)).
|
|
5247
|
+
expect(linked.ancestors.map((ancestor) => ancestor.id)).toStrictEqual([id]);
|
|
5676
5248
|
}
|
|
5677
5249
|
});
|
|
5678
5250
|
it("reads each object's row as an object, or null when there is none", async () => {
|
|
@@ -5702,11 +5274,11 @@ function testRelationSource(source, options) {
|
|
|
5702
5274
|
...missing,
|
|
5703
5275
|
through: "parent",
|
|
5704
5276
|
depth: 16
|
|
5705
|
-
})).ancestors).
|
|
5277
|
+
})).ancestors).toStrictEqual([]);
|
|
5706
5278
|
if (first.relation !== void 0) expect(await source.related({
|
|
5707
5279
|
...missing,
|
|
5708
5280
|
relation: first.relation
|
|
5709
|
-
})).
|
|
5281
|
+
})).toStrictEqual([]);
|
|
5710
5282
|
});
|
|
5711
5283
|
}
|
|
5712
5284
|
//#endregion
|
|
@@ -5718,8 +5290,8 @@ function isFrozenDeep(value) {
|
|
|
5718
5290
|
/**
|
|
5719
5291
|
* Checks a `PrincipalSource`: `principal` answers each listed id with the
|
|
5720
5292
|
* expected principal or `null`, never another id; `principalByIdentity`
|
|
5721
|
-
* resolves each listed identity to the expected principal id or `null`, and
|
|
5722
|
-
*
|
|
5293
|
+
* resolves each listed identity to the expected principal id or `null`, and an
|
|
5294
|
+
* email matches trimmed and in any case.
|
|
5723
5295
|
*/
|
|
5724
5296
|
function testPrincipalSource(source, options) {
|
|
5725
5297
|
it("returns the principal with the asked id, or null", async () => {
|
|
@@ -5731,8 +5303,8 @@ function testPrincipalSource(source, options) {
|
|
|
5731
5303
|
found = null;
|
|
5732
5304
|
}
|
|
5733
5305
|
expect(found === null || typeof found === "object").toBe(true);
|
|
5734
|
-
if (found !== null) expect(await loadPrincipal(source, entry.id, entry.tenant)).
|
|
5735
|
-
if (entry.expect !== void 0) expect(found).
|
|
5306
|
+
if (found !== null) expect(await loadPrincipal(source, entry.id, entry.tenant)).toStrictEqual(found);
|
|
5307
|
+
if (entry.expect !== void 0) expect(found).toStrictEqual(entry.expect);
|
|
5736
5308
|
}
|
|
5737
5309
|
});
|
|
5738
5310
|
it("returns principals a caller cannot change", async () => {
|
|
@@ -5775,10 +5347,10 @@ async function automationOf(source, id, tenant) {
|
|
|
5775
5347
|
}
|
|
5776
5348
|
}
|
|
5777
5349
|
/**
|
|
5778
|
-
* Checks an `AutomationSource`: `automation(id)` answers each listed id
|
|
5779
|
-
*
|
|
5780
|
-
*
|
|
5781
|
-
*
|
|
5350
|
+
* Checks an `AutomationSource`: `automation(id)` answers each listed id with a
|
|
5351
|
+
* valid Automation v1 record of that id or `null`, never another id; a record
|
|
5352
|
+
* held to one tenant is `null` when asked for another; an empty or unknown id
|
|
5353
|
+
* is `null`.
|
|
5782
5354
|
*/
|
|
5783
5355
|
function testAutomationSource(source, options) {
|
|
5784
5356
|
it("returns a valid v1 record with the asked id, or null", async () => {
|
|
@@ -5786,10 +5358,10 @@ function testAutomationSource(source, options) {
|
|
|
5786
5358
|
const found = await automationOf(source, entry.id, entry.tenant);
|
|
5787
5359
|
if (found !== null) {
|
|
5788
5360
|
const parsed = parseAutomation(found);
|
|
5789
|
-
expect(parsed).
|
|
5361
|
+
expect(parsed).toStrictEqual(found);
|
|
5790
5362
|
expect(parsed?.id).toBe(entry.id);
|
|
5791
5363
|
}
|
|
5792
|
-
if (entry.expect !== void 0) expect(found).
|
|
5364
|
+
if (entry.expect !== void 0) expect(found).toStrictEqual(entry.expect);
|
|
5793
5365
|
}
|
|
5794
5366
|
});
|
|
5795
5367
|
it("refuses a record held to another tenant", async () => {
|
|
@@ -5812,9 +5384,9 @@ async function sessionOf(source, id) {
|
|
|
5812
5384
|
}
|
|
5813
5385
|
}
|
|
5814
5386
|
/**
|
|
5815
|
-
* Checks a `SupportSessionSource`: `session(id)` answers each listed id
|
|
5816
|
-
*
|
|
5817
|
-
*
|
|
5387
|
+
* Checks a `SupportSessionSource`: `session(id)` answers each listed id with a
|
|
5388
|
+
* record of that id or `null`, never another id; `ended` is a boolean when
|
|
5389
|
+
* present; an empty or unknown id is `null`.
|
|
5818
5390
|
*/
|
|
5819
5391
|
function testSupportSessionSource(source, options) {
|
|
5820
5392
|
it("returns the session with the asked id, or null", async () => {
|
|
@@ -5839,7 +5411,10 @@ function testSupportSessionSource(source, options) {
|
|
|
5839
5411
|
});
|
|
5840
5412
|
}
|
|
5841
5413
|
const SETTLED = Symbol("settled");
|
|
5842
|
-
/**
|
|
5414
|
+
/**
|
|
5415
|
+
* Whether `promise` settles within `ms` milliseconds; a rejection counts as
|
|
5416
|
+
* settled.
|
|
5417
|
+
*/
|
|
5843
5418
|
async function settlesWithin(promise, ms) {
|
|
5844
5419
|
let timer;
|
|
5845
5420
|
const timeout = new Promise((resolve) => {
|
|
@@ -5854,12 +5429,12 @@ async function settlesWithin(promise, ms) {
|
|
|
5854
5429
|
}
|
|
5855
5430
|
}
|
|
5856
5431
|
/**
|
|
5857
|
-
* Checks an `ApprovalWaiter`: a wait ends when its token is woken and not
|
|
5858
|
-
*
|
|
5859
|
-
*
|
|
5860
|
-
* Pass `expiry: false` or `abort: false` for a waiter whose runner
|
|
5861
|
-
* timer or no cancellation, `pending` when the waiter reads a store,
|
|
5862
|
-
* `parked` when a wait parks asynchronously.
|
|
5432
|
+
* Checks an `ApprovalWaiter`: a wait ends when its token is woken and not when
|
|
5433
|
+
* another token is; waking a token nobody waits on resolves; a wait whose
|
|
5434
|
+
* `expiresAt` has passed ends; an aborted `signal` rejects the wait with its
|
|
5435
|
+
* reason. Pass `expiry: false` or `abort: false` for a waiter whose runner
|
|
5436
|
+
* offers no timer or no cancellation, `pending` when the waiter reads a store,
|
|
5437
|
+
* and `parked` when a wait parks asynchronously.
|
|
5863
5438
|
*/
|
|
5864
5439
|
function testApprovalWaiter(waiter, options = {}) {
|
|
5865
5440
|
const timeout = options.timeout ?? 1e3;
|
|
@@ -5921,9 +5496,9 @@ function key(suffix) {
|
|
|
5921
5496
|
}
|
|
5922
5497
|
/**
|
|
5923
5498
|
* 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
|
-
*
|
|
5926
|
-
*
|
|
5499
|
+
* idempotency key, keeps the key on a retry, refuses a target without a granted
|
|
5500
|
+
* decision before calling the provider, and maps provider errors to retryable
|
|
5501
|
+
* or final failures without rejecting.
|
|
5927
5502
|
*/
|
|
5928
5503
|
function testDeliveryTransport(name, options) {
|
|
5929
5504
|
const targetWith = async (decision) => ({
|
|
@@ -5937,14 +5512,14 @@ function testDeliveryTransport(name, options) {
|
|
|
5937
5512
|
it("names its channel and provider", () => {
|
|
5938
5513
|
const { transport } = options.create();
|
|
5939
5514
|
expect(transport.channel).toBe(options.channel);
|
|
5940
|
-
expect(
|
|
5515
|
+
expect(transport.name).toBeTypeOf("string");
|
|
5941
5516
|
expect(transport.name).not.toBe("");
|
|
5942
5517
|
});
|
|
5943
5518
|
it("sends a checked target with its idempotency key", async () => {
|
|
5944
5519
|
const harness = options.create();
|
|
5945
5520
|
const result = await send(harness, await targetWith(await grantedDecision()), "a");
|
|
5946
5521
|
expect(result.status).toBe("sent");
|
|
5947
|
-
expect(await harness.received()).
|
|
5522
|
+
expect(await harness.received()).toStrictEqual([{ idempotencyKey: "pdd_runner_a" }]);
|
|
5948
5523
|
});
|
|
5949
5524
|
it("passes the same key on a retry of the same delivery", async () => {
|
|
5950
5525
|
const harness = options.create();
|
|
@@ -5970,7 +5545,7 @@ function testDeliveryTransport(name, options) {
|
|
|
5970
5545
|
cause: "invalid-recipient",
|
|
5971
5546
|
retryable: false
|
|
5972
5547
|
});
|
|
5973
|
-
expect(await harness.received()).
|
|
5548
|
+
expect(await harness.received()).toStrictEqual([]);
|
|
5974
5549
|
});
|
|
5975
5550
|
it("maps a retryable provider error to a retryable failure", async () => {
|
|
5976
5551
|
const harness = options.create();
|
|
@@ -6001,7 +5576,7 @@ function testDeliveryTransport(name, options) {
|
|
|
6001
5576
|
status: "failed",
|
|
6002
5577
|
retryable: true
|
|
6003
5578
|
});
|
|
6004
|
-
expect(await harness.received()).
|
|
5579
|
+
expect(await harness.received()).toStrictEqual([]);
|
|
6005
5580
|
});
|
|
6006
5581
|
});
|
|
6007
5582
|
}
|
|
@@ -6039,7 +5614,7 @@ function testMembershipSource(source, options) {
|
|
|
6039
5614
|
if (options.policy !== void 0) expect(normalizeMemberships([membership], scopeList(options.policy.scopes))).toHaveLength(1);
|
|
6040
5615
|
}
|
|
6041
5616
|
const expected = options.expect?.[principal.id];
|
|
6042
|
-
if (expected !== void 0) expect(memberships).
|
|
5617
|
+
if (expected !== void 0) expect(memberships).toStrictEqual(expected);
|
|
6043
5618
|
}
|
|
6044
5619
|
});
|
|
6045
5620
|
if (source.list !== void 0) it("lists every member of an instance it returns a membership for", async () => {
|
|
@@ -6071,9 +5646,9 @@ function testEntitlementSource(source, options) {
|
|
|
6071
5646
|
it("returns plan names for a tenant and none without one", async () => {
|
|
6072
5647
|
const found = await source.entitlementsFor(options.principal, { tenant: options.tenant });
|
|
6073
5648
|
expect(Array.isArray(found)).toBe(true);
|
|
6074
|
-
for (const name of found) expect(
|
|
6075
|
-
if (options.expect !== void 0) expect([...found].toSorted()).
|
|
6076
|
-
expect(await source.entitlementsFor(options.principal, {})).
|
|
5649
|
+
for (const name of found) expect(name).toBeTypeOf("string");
|
|
5650
|
+
if (options.expect !== void 0) expect([...found].toSorted()).toStrictEqual([...options.expect].toSorted());
|
|
5651
|
+
expect(await source.entitlementsFor(options.principal, {})).toStrictEqual([]);
|
|
6077
5652
|
});
|
|
6078
5653
|
}
|
|
6079
5654
|
function testCustomRoleSource(source, options) {
|
|
@@ -6083,7 +5658,7 @@ function testCustomRoleSource(source, options) {
|
|
|
6083
5658
|
for (const role of roles) {
|
|
6084
5659
|
expect(role.tenant).toBe(options.tenant);
|
|
6085
5660
|
for (const included of role.includes ?? []) expect(declared).toContain(included);
|
|
6086
|
-
if (options.policy !== void 0) expect(validateCustomRole(options.policy, role).dropped).
|
|
5661
|
+
if (options.policy !== void 0) expect(validateCustomRole(options.policy, role).dropped).toStrictEqual([]);
|
|
6087
5662
|
}
|
|
6088
5663
|
if (source.assignable !== void 0) {
|
|
6089
5664
|
const assignable = await source.assignable(options.tenant);
|
|
@@ -6093,7 +5668,7 @@ function testCustomRoleSource(source, options) {
|
|
|
6093
5668
|
const every = options.every;
|
|
6094
5669
|
if (every !== void 0) it("returns every custom role of the tenant, held or not", async () => {
|
|
6095
5670
|
const roles = await source.rolesFor(options.tenant, { held: [] });
|
|
6096
|
-
expect([...new Set(roles.map((role) => role.name))].toSorted()).
|
|
5671
|
+
expect([...new Set(roles.map((role) => role.name))].toSorted()).toStrictEqual([...new Set(every)].toSorted());
|
|
6097
5672
|
});
|
|
6098
5673
|
}
|
|
6099
5674
|
function testDirectoryStore(store, options) {
|
|
@@ -6123,7 +5698,7 @@ function testDirectoryStore(store, options) {
|
|
|
6123
5698
|
created: "",
|
|
6124
5699
|
lastModified: ""
|
|
6125
5700
|
}
|
|
6126
|
-
})).rejects.toThrow(/userName/);
|
|
5701
|
+
})).rejects.toThrow(/userName/u);
|
|
6127
5702
|
const group = await store.putGroup(home, {
|
|
6128
5703
|
id: "g_editors",
|
|
6129
5704
|
displayName: "Editors",
|
|
@@ -6134,11 +5709,11 @@ function testDirectoryStore(store, options) {
|
|
|
6134
5709
|
lastModified: ""
|
|
6135
5710
|
}
|
|
6136
5711
|
});
|
|
6137
|
-
expect(await store.groupsFor(home, created.id)).
|
|
5712
|
+
expect(await store.groupsFor(home, created.id)).toStrictEqual([expect.objectContaining({
|
|
6138
5713
|
id: group.id,
|
|
6139
5714
|
displayName: "Editors"
|
|
6140
5715
|
})]);
|
|
6141
|
-
expect(await store.groupsFor(other, created.id)).
|
|
5716
|
+
expect(await store.groupsFor(other, created.id)).toStrictEqual([]);
|
|
6142
5717
|
const patched = await store.patchUser(home, created.id, [{
|
|
6143
5718
|
op: "replace",
|
|
6144
5719
|
path: "active",
|
|
@@ -6146,13 +5721,13 @@ function testDirectoryStore(store, options) {
|
|
|
6146
5721
|
}]);
|
|
6147
5722
|
expect(patched.active).toBe(false);
|
|
6148
5723
|
const source = directoryMembershipSource(store, { assignable: ["editor"] });
|
|
6149
|
-
expect(await source.membershipsFor({ id: "00u1" }, { tenant: home })).
|
|
5724
|
+
expect(await source.membershipsFor({ id: "00u1" }, { tenant: home })).toStrictEqual([]);
|
|
6150
5725
|
await store.patchUser(home, created.id, [{
|
|
6151
5726
|
op: "replace",
|
|
6152
5727
|
path: "active",
|
|
6153
5728
|
value: true
|
|
6154
5729
|
}]);
|
|
6155
|
-
expect(await source.membershipsFor({ id: "00u1" }, { tenant: home })).
|
|
5730
|
+
expect(await source.membershipsFor({ id: "00u1" }, { tenant: home })).toStrictEqual([{
|
|
6156
5731
|
tenant: home,
|
|
6157
5732
|
roles: ["editor"],
|
|
6158
5733
|
via: `group:${group.id}`,
|
|
@@ -6163,7 +5738,7 @@ function testDirectoryStore(store, options) {
|
|
|
6163
5738
|
path: "members",
|
|
6164
5739
|
value: [{ value: created.id }]
|
|
6165
5740
|
}]);
|
|
6166
|
-
expect(await store.groupsFor(home, created.id)).
|
|
5741
|
+
expect(await store.groupsFor(home, created.id)).toStrictEqual([]);
|
|
6167
5742
|
});
|
|
6168
5743
|
it("ends sessions when scimHandler deactivates a user in this store", async () => {
|
|
6169
5744
|
const feed = memoryRevocationFeed();
|
|
@@ -6216,7 +5791,7 @@ function testDirectoryStore(store, options) {
|
|
|
6216
5791
|
kind: "session-revoked"
|
|
6217
5792
|
});
|
|
6218
5793
|
expect(seen.every((event) => event.kind === "session-revoked")).toBe(true);
|
|
6219
|
-
expect(kinds).
|
|
5794
|
+
expect(kinds).toStrictEqual(["changed", "session-revoked"]);
|
|
6220
5795
|
});
|
|
6221
5796
|
}
|
|
6222
5797
|
/** Settings for `tenant`, or `undefined`; a thrown source fails the runner. */
|
|
@@ -6224,13 +5799,13 @@ function testSettingsSource(source, options) {
|
|
|
6224
5799
|
it("answers per tenant with plain settings and nothing for an unknown tenant", async () => {
|
|
6225
5800
|
const settings = await source.settingsFor(options.tenant);
|
|
6226
5801
|
if (settings !== void 0) {
|
|
6227
|
-
expect(
|
|
5802
|
+
expect(settings).toBeTypeOf("object");
|
|
6228
5803
|
const credentials = settings.credentials;
|
|
6229
5804
|
if (credentials !== void 0) {
|
|
6230
5805
|
expect(credentials.maxTtl === void 0 || Number.isFinite(credentials.maxTtl) && credentials.maxTtl >= 0).toBe(true);
|
|
6231
5806
|
expect(credentials.kinds === void 0 || credentials.kinds.every((kind) => kind === "user" || kind === "service" || kind === "feed")).toBe(true);
|
|
6232
5807
|
}
|
|
6233
|
-
expect(JSON.parse(JSON.stringify(settings))).
|
|
5808
|
+
expect(JSON.parse(JSON.stringify(settings))).toStrictEqual(settings);
|
|
6234
5809
|
}
|
|
6235
5810
|
expect(await source.settingsFor(options.unknown ?? "__permdock_unknown__")).toBeUndefined();
|
|
6236
5811
|
});
|
|
@@ -6665,8 +6240,8 @@ const supabaseClaimVectors = {
|
|
|
6665
6240
|
/**
|
|
6666
6241
|
* The `permdock supabase inspect --json` manifest for a policy with one
|
|
6667
6242
|
* `tenant` scope, the default `supabase.hook` and a `features` claim from
|
|
6668
|
-
* `public.feature_claims`. A package that reads the manifest tests its
|
|
6669
|
-
*
|
|
6243
|
+
* `public.feature_claims`. A package that reads the manifest tests its parser
|
|
6244
|
+
* against this value.
|
|
6670
6245
|
*/
|
|
6671
6246
|
const supabaseHookManifestFixture = {
|
|
6672
6247
|
$schema: "https://permdock.com/schemas/supabase-manifest-v1.json",
|
|
@@ -6957,4 +6532,4 @@ const PROBLEM_EXAMPLES = Object.freeze({
|
|
|
6957
6532
|
}
|
|
6958
6533
|
});
|
|
6959
6534
|
//#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 };
|
|
6535
|
+
export { AGENT_THROWING_ROW, AGENT_THROWING_USER, PROBLEM_EXAMPLES, authzenTodoData, authzenTodoPermissions, authzenTodoPolicy, authzenTodoUsers, authzenTodoVectors, describePolicy, expectTypeOf, jwtFixtureAudience, jwtFixtureIssuer, jwtFixtureJwks, jwtFixtureTokens, policyMatrix, seedRows, 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, testSsfStreamStore, testSubjectResolver, testSupportSessionSource, testTokenSigner, testTokenVerifier, testWhereCompiler };
|