permdock 0.3.1 → 0.4.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/dist/a2a/index.d.ts +7 -5
- package/dist/a2a/index.js +14 -21
- package/dist/ai-sdk/index.d.ts +18 -6
- package/dist/ai-sdk/index.js +8 -5
- package/dist/app-data-CV8pqqMi.js +59 -0
- package/dist/approvals/index.d.ts +2 -2
- package/dist/approvals/index.js +3 -3
- package/dist/{approvals-DbYAuNkG.js → approvals-DZL76lKW.js} +35 -19
- package/dist/{approvers-DT4AVyIg.js → approvers-CqPOLD-o.js} +1 -1
- package/dist/{arazzo-BAgw9ap6.js → arazzo-C4BcskbX.js} +6 -5
- package/dist/{arazzo-C9chp3ST.js → arazzo-CD8d0WLb.js} +2 -2
- package/dist/authzen/index.d.ts +3 -3
- package/dist/authzen/index.js +161 -23
- package/dist/better-auth/index.d.ts +2 -2
- package/dist/better-auth/index.js +6 -19
- package/dist/better-supabase/index.d.ts +112 -87
- package/dist/better-supabase/index.js +257 -156
- package/dist/{bind-C_ob3LKa.d.ts → bind-BzqG8SMb.d.ts} +16 -5
- package/dist/{bind-CuzGytRf.js → bind-kk-uyCwH.js} +14 -6
- package/dist/build-instance-DIIV9DbC.js +51 -0
- package/dist/{canonical-json-DemiK4Vp.js → canonical-json-CFKyFOke.js} +1 -3
- package/dist/{capability-2kYTMWDv.d.ts → capability-CksyCWDH.d.ts} +2 -2
- package/dist/{capability-4qPdO9gk.js → capability-txATtKV4.js} +3 -2
- package/dist/catalog/index.d.ts +1 -1
- package/dist/catalog/index.js +1 -1
- package/dist/catalog-6yWAX6Ps.js +2 -0
- package/dist/{catalog-CYj-mX2O.js → catalog-WHG4kBq7.js} +2 -2
- package/dist/{catalog-doc-DdGm82XT.js → catalog-doc-DWtg0gGR.js} +17 -10
- package/dist/{catalog-DWF5cYqB.js → catalog-tZUyOfOv.js} +4 -4
- package/dist/claude-agent/index.d.ts +4 -4
- package/dist/claude-agent/index.js +4 -5
- package/dist/clerk/index.d.ts +3 -3
- package/dist/clerk/index.js +8 -33
- package/dist/cli/bin.js +1 -1
- package/dist/cli/index.d.ts +3 -3
- package/dist/cli/index.js +3 -3
- package/dist/cloud/index.d.ts +4 -4
- package/dist/cloud/index.js +9 -15
- package/dist/{cloud-CfwZ39Rl.js → cloud-C7bSDgX4.js} +2 -2
- package/dist/{cloud-DWLXO2af.js → cloud-PeBN3IEn.js} +5 -5
- package/dist/{collect-DSTE3ml9.js → collect-2SrB7lfk.js} +6 -8
- package/dist/collect-Bw2Jdq1q.js +2 -0
- package/dist/{collect-CHB22Sx4.js → collect-CQx4hX4N.js} +3 -3
- package/dist/{commands-BeaGNj_o.js → commands-Cpip4ULx.js} +13 -13
- package/dist/{compact-y6vfu2_f.js → compact-CAf557ZB.js} +11 -1
- package/dist/compile/index.d.ts +1 -1
- package/dist/compile/index.js +1 -1
- package/dist/{compile-Ba4blROR.js → compile-CcEu2ZQ1.js} +6 -5
- package/dist/{compile-BkRyBDBw.d.ts → compile-CtTP7eur.d.ts} +2 -2
- package/dist/{config-BJiM-xAm.js → config-BIjREpyE.js} +4 -4
- package/dist/{config-ir40y8m0.js → config-DLt6BvV7.js} +2 -2
- package/dist/{config-report-CdCgbRnV.js → config-report--Xb9APJS.js} +5 -6
- package/dist/{connection-CU8MQxRi.js → connection-DUsczpB5.js} +3 -2
- package/dist/constants-DcJABbLW.js +20 -0
- package/dist/{context-Bcy85rGi.js → context-DieZVJXu.js} +16 -5
- package/dist/convex/index.d.ts +4 -4
- package/dist/convex/index.js +14 -16
- package/dist/create-BBO24vj6.js +324 -0
- package/dist/{credential-8P81ff4u.js → credential-kue0qpbm.js} +4 -4
- package/dist/deciding-columns-Sq12mEA6.js +123 -0
- package/dist/delegation-input-FTxMK3ji.js +19 -0
- package/dist/denied-text-DfRk15uw.d.ts +19 -0
- package/dist/{describe-DMYiCjJV.d.ts → describe-C3ldA9os.d.ts} +11 -2
- package/dist/{describe-B_gVZEVi.js → describe-Cgkp7eP0.js} +5 -4
- package/dist/{diff-C9RDjjhs.js → diff-Cnghh_Ip.js} +2 -2
- package/dist/{diff-2VaeChzX.js → diff-rpqoe7Ag.js} +9 -9
- package/dist/{doctor-Djh6JDEk.js → doctor-CuVJvhV_.js} +2 -2
- package/dist/doctor-owlv6ddh.js +2166 -0
- package/dist/drizzle/index.d.ts +2 -2
- package/dist/drizzle/index.js +6 -14
- package/dist/elysia/index.d.ts +4 -4
- package/dist/elysia/index.js +7 -7
- package/dist/{errors-DPdqI50v.d.ts → errors-OD_4c__H.d.ts} +2 -2
- package/dist/{errors-Cz5zn0mW.js → errors-drnfxYtm.js} +3 -3
- package/dist/{evaluations-tz6D9A2G.js → evaluations-C9YJsv3H.js} +45 -7
- package/dist/eve/index.d.ts +5 -5
- package/dist/eve/index.js +5 -5
- package/dist/express/index.d.ts +4 -4
- package/dist/express/index.js +8 -7
- package/dist/fastify/index.d.ts +15 -5
- package/dist/fastify/index.js +9 -29
- package/dist/fields-fwfXN01n.js +1016 -0
- package/dist/{files-DWUN9PzI.js → files-D_JIzUrP.js} +12 -3
- package/dist/{fixtures-ht28Cplr.js → fixtures-h2eG2OZV.js} +5 -6
- package/dist/{from-snapshot-CTLijvdm.js → from-snapshot-7REQZe0H.js} +32 -22
- package/dist/{generate-CH7ThJ14.js → generate-CNU1pIOo.js} +1 -3
- package/dist/grantee-CXY-0C87.js +254 -0
- package/dist/grantee-conditions-MQs5NIuX.js +270 -0
- package/dist/grantee-match-fxYp9Nmt.js +141 -0
- package/dist/{helpers-H_KARDVC.js → helpers-C9YmyBIa.js} +7 -6
- package/dist/hono/index.d.ts +4 -4
- package/dist/hono/index.js +8 -8
- package/dist/hosted-CBHgkzDl.js +655 -0
- package/dist/{http-SO7qGttf.js → http-BBoKefeS.js} +35 -2
- package/dist/{http-BIhmLfCc.js → http-BEbdMZ05.js} +2 -24
- package/dist/{rls-import-4lLW_pbb.js → import-BFIlhetD.js} +86 -80
- package/dist/{openapi-import-cmwetbzw.js → import-UHk1N7vH.js} +6 -7
- package/dist/{index-C8z4RjQV.d.ts → index-BwMe94Xq.d.ts} +1 -1
- package/dist/{index-0nNqHspG.d.ts → index-DgM7w96K.d.ts} +2 -2
- package/dist/{index-C7Tw7MaW.d.ts → index-Dz7didpC.d.ts} +3 -3
- package/dist/{index-BNc7Wer_.d.ts → index-UdVQgq48.d.ts} +105 -38
- package/dist/index.d.ts +15 -13
- package/dist/index.js +22 -22
- package/dist/{instance-options-jARdwfPN.d.ts → instance-options-plkrsAog.d.ts} +1 -1
- package/dist/{json-schema-BsAflUGW.js → json-schema-DBkdUuCO.js} +1 -3
- package/dist/jwt/index.d.ts +4 -4
- package/dist/jwt/index.js +13 -13
- package/dist/{kernel-_RRHmnMY.js → kernel-CB3LrIJM.js} +81 -32
- package/dist/kysely/index.d.ts +2 -2
- package/dist/kysely/index.js +6 -14
- package/dist/{live-options-H2eG8dfJ.js → live-options-BaAFCIiu.js} +16 -3
- package/dist/load-B_shHrmd.js +2 -0
- package/dist/{load-DhWEB9P1.js → load-C3_oTHyO.js} +11 -3
- package/dist/{manifest-iaoBXHKc.js → manifest-BngWKryL.js} +5 -18
- package/dist/{map-claims-Z4QZOiFF.js → map-claims-AVlBaJoa.js} +7 -16
- package/dist/{map-error-pgUWNNtG.js → map-error-BE27PYkE.js} +5 -5
- package/dist/{markers-B0hYLvMD.js → markers-CiwAOZtO.js} +6 -2
- package/dist/{may-use-DndQ5tQ-.js → may-use-BOn_dR_w.js} +4 -3
- package/dist/mcp/index.d.ts +26 -9
- package/dist/mcp/index.js +456 -446
- package/dist/{rls-migrate-CN4TlJTO.js → migrate-BwlB5-Wk.js} +75 -15
- package/dist/nest/index.d.ts +4 -4
- package/dist/nest/index.js +11 -12
- package/dist/next/index.d.ts +31 -8
- package/dist/next/index.js +29 -33
- package/dist/next/plugin.d.ts +1 -1
- package/dist/next/plugin.js +1 -1
- package/dist/node/index.d.ts +4 -4
- package/dist/node/index.js +4 -4
- package/dist/ocsf-uYFXGVw2.js +115 -0
- package/dist/openai/index.d.ts +5 -5
- package/dist/openai/index.js +4 -4
- package/dist/openapi/index.d.ts +3 -3
- package/dist/openapi/index.js +1 -1
- package/dist/{openapi-CqqEbHsL.js → openapi-BJdBmYDG.js} +19 -23
- package/dist/{openapi-BqRBvAng.js → openapi-DBzHQbov.js} +2 -5
- package/dist/{openapi-CQTsgKoB.js → openapi-DcQ1Kg40.js} +3 -3
- package/dist/orpc/index.d.ts +5 -5
- package/dist/orpc/index.js +9 -11
- package/dist/otel/index.d.ts +2 -2
- package/dist/otel/index.js +26 -23
- package/dist/output-files-CgIsvdSa.js +140 -0
- package/dist/{rls-ownership-DcYlZ09B.js → ownership-BgnbOJFT.js} +387 -379
- package/dist/{package-root-zGkxO3oN.js → package-root-DtNnpmUd.js} +3 -2
- package/dist/{parse-BuyjxhZV.js → parse-CVFQHk6c.js} +3 -3
- package/dist/{parse-local-manifest-BrMozJCo.js → parse-local-manifest-C-q02Pal.js} +2 -4
- package/dist/{parse-local-manifest-JLnaBH0S.d.ts → parse-local-manifest-DvYUR0QL.d.ts} +1 -1
- package/dist/pdp/index.d.ts +2 -2
- package/dist/pdp/index.js +8 -9
- package/dist/{permdock-CbgvLnuo.js → permdock-L26udHSk.js} +1387 -1220
- package/dist/{rls-fields-CZOMlwHl.js → permission-keys-B26jeyUv.js} +1268 -2053
- package/dist/permission-registry-BsblpB-z.js +385 -0
- package/dist/{permissions-DttnqqxC.d.ts → permission-types-Dg39z9Kv.d.ts} +59 -31
- package/dist/{permissions-RJrovw98.js → permissions-CCJ6lmxB.js} +3 -3
- package/dist/{permissions-CDgvKG5L.d.ts → permissions-COH-KRgT.d.ts} +2 -2
- package/dist/{policy-IB6yxpC3.d.ts → policy-types-sp9P1iIV.d.ts} +661 -582
- package/dist/{policy-D2W7uv19.d.ts → policy-ur0BBK_e.d.ts} +2 -2
- package/dist/{powersync-DE19b9dT.js → powersync-BFSbSElQ.js} +2 -2
- package/dist/powersync-DCDgZgfZ.js +2 -0
- package/dist/{powersync-Dv1_aWF7.js → powersync-cZe4kzhL.js} +26 -27
- package/dist/prisma/index.d.ts +1 -1
- package/dist/prisma/index.js +6 -12
- package/dist/{problem-B43ztKIm.js → problem-CnWraVhC.js} +8 -7
- package/dist/{problem-details-_AMOYz-w.js → problem-details-DECUnXAn.js} +2 -2
- package/dist/{problem-details-DX075l9V.d.ts → problem-details-rIIo8B2C.d.ts} +1 -1
- package/dist/{protected-BdnIEFkY.d.ts → protected-B3_P7xuw.d.ts} +9 -6
- package/dist/{protected-utU-SLuy.js → protected-BN7BUdY1.js} +20 -8
- package/dist/{provider-CSQd7m92.js → provider-Bi-0HNL3.js} +23 -10
- package/dist/{provider-DZY281jO.d.ts → provider-kwHC9CZD.d.ts} +1 -1
- package/dist/{provider-store-Cfbvo4DE.js → provider-store-Ctt-81Ni.js} +6 -3
- package/dist/react/index.d.ts +7 -7
- package/dist/react/index.js +5 -5
- package/dist/react/provider-client.d.ts +1 -1
- package/dist/react/provider-client.js +1 -1
- package/dist/react-native/index.d.ts +20 -11
- package/dist/react-native/index.js +33 -17
- package/dist/{refs-KYb4ntaz.js → refs-f3imMjNW.js} +1 -1
- package/dist/relation-walk-BXnlLtg2.js +186 -0
- package/dist/relations-CvmSpG67.js +163 -0
- package/dist/{replay-CYyiLiwO.d.ts → replay-COpmZUfx.d.ts} +1 -1
- package/dist/request-context-BVM7aWNs.js +71 -0
- package/dist/request-context-C2_1lchh.d.ts +5 -0
- package/dist/resource-relations-D1Un3pcz.js +243 -0
- package/dist/retire-BFcxL6un.js +240 -0
- package/dist/{rls-P-BeBx0n.js → rls-55DQTuf5.js} +14 -2
- package/dist/{rls-DiinlRML.js → rls-BEktUaqg.js} +884 -484
- package/dist/{policy-xyWg7QdR.js → role-builder-B4GbKM-i.js} +235 -541
- package/dist/{rpc-DL9Sqvb1.js → rpc-nYC3cR_u.js} +15 -5
- package/dist/{run-BWc9qqvE.js → run-DMcNW_8T.js} +3 -3
- package/dist/{runtime-CB8gQFvS.js → runtime-D_tMXnsq.js} +1 -1
- package/dist/{saas-DhpJjnBA.js → saas-DtaA4JuW.js} +9 -8
- package/dist/{openapi-schema--ARjyfQr.js → schema-Bol6gqXA.js} +3 -5
- package/dist/{schema-D3MnDMxa.js → schema-DjQBcAP9.js} +4 -2
- package/dist/scim/index.d.ts +1 -1
- package/dist/scim/index.js +1 -1
- package/dist/{scim-BFGBBAqR.js → scim-vFR1O5oG.js} +6 -12
- package/dist/{scopes-CVdPrWN-.js → scopes-tvpa34ep.js} +10 -2
- package/dist/server/index.d.ts +18 -8
- package/dist/server/index.js +8 -8
- package/dist/{server-DlZ28Oz5.js → server-ZYEHtNHS.js} +4 -4
- package/dist/{create-B1d519XK.d.ts → server-types-DdqBwZN_.d.ts} +69 -33
- package/dist/shared-1uhI-e-b.js +294 -0
- package/dist/{shared-DRu8VFVM.js → shared-JhMeOsDF.js} +1 -1
- package/dist/{sink-Bd9PSyfq.js → sink-BTtmfPnE.js} +1 -1
- package/dist/{sink-B1fZzIMf.d.ts → sink-CPN4zLfs.d.ts} +1 -1
- package/dist/{skills-Biq5uup3.js → skills-BZdRQkxs.js} +4 -3
- package/dist/{skills-C5CbhhpG.js → skills-Dle4ZoLF.js} +2 -2
- package/dist/{snapshot-Enxd0n-s.js → snapshot-Dc9CHPME.js} +116 -13
- package/dist/{snapshot-cache-D6jLelln.js → snapshot-cache-Cod40D-X.js} +1 -1
- package/dist/{snapshot-cache-B-LxUahj.d.ts → snapshot-cache-J5T7t_vD.d.ts} +1 -1
- package/dist/solid/index.d.ts +22 -22
- package/dist/solid/index.js +40 -10
- package/dist/{sources-4p0-4xQm.js → sources-B1-b-R0t.js} +25 -26
- package/dist/{sources-BdOYLw9j.d.ts → sources-Dljy06zL.d.ts} +6 -2
- package/dist/{sql-xdg5f4Fi.js → sql-t8teWOfo.js} +19 -3
- package/dist/{src-BDwEZWrZ.js → src-Bb7no-hS.js} +194 -123
- package/dist/ssf/index.d.ts +3 -3
- package/dist/ssf/index.js +58 -61
- package/dist/store-9KO4BlkB.d.ts +147 -0
- package/dist/{store-DMAWwAJV.js → store-ILkLUj_o.js} +4 -4
- package/dist/{store-options-ByhZq1jV.js → store-options-CbUHDoSe.js} +16 -7
- package/dist/{stores-BT2pSCub.js → stores-BHXdSbw9.js} +7 -3
- package/dist/stores-Cxz_80g-.d.ts +31 -0
- package/dist/{stream-DDpvynSB.js → stream-BMui98KK.js} +2 -1
- package/dist/{stream-D1RXubw2.d.ts → stream-Dwsg6WwE.d.ts} +2 -2
- package/dist/{subject-settings-BwBmCdTR.js → subject-settings-CkEwrw3W.js} +23 -5
- package/dist/supabase/index.d.ts +4 -4
- package/dist/supabase/index.js +3 -3
- package/dist/supabase/middleware.d.ts +11 -5
- package/dist/supabase/middleware.js +10 -7
- package/dist/{supabase-C0qESOa-.js → supabase-C1TWlnYt.js} +36 -52
- package/dist/{supabase-s4o9s1O1.js → supabase-Dbqr141s.js} +2 -2
- package/dist/supabase-hook-BULg7Ut_.js +2 -0
- package/dist/{supabase-hook-ntETYCQu.js → supabase-hook-Cva3ikXz.js} +730 -781
- package/dist/{supautils-u8jXTGrt.js → supautils-B6HYL9eL.js} +2 -2
- package/dist/svelte/Protected.svelte +9 -2
- package/dist/svelte/index.d.ts +6 -6
- package/dist/svelte/index.js +25 -11
- package/dist/svelte/runtime.d.ts +10 -3
- package/dist/svelte/runtime.js +4 -3
- package/dist/svelte/source.d.ts +6 -6
- package/dist/svelte/source.js +4 -4
- package/dist/{validity-uOp8UpYn.js → tenancy-DshjqY0n.js} +3 -39
- package/dist/terminal/index.d.ts +5 -5
- package/dist/terminal/index.js +18 -34
- package/dist/testing/index.d.ts +89 -77
- package/dist/testing/index.js +1276 -1262
- package/dist/testing/saas/index.d.ts +4 -4
- 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/thenable-QSmvt-vZ.js +65 -0
- package/dist/{token-CIWxJOCh.js → token-Bx9qy30p.js} +1 -1
- package/dist/tool-refusal-CQ7OEia9.js +41 -0
- package/dist/trpc/index.d.ts +5 -5
- package/dist/trpc/index.js +7 -8
- package/dist/{types-DbjRN55U.d.ts → types-7rJVLWe5.d.ts} +82 -4
- package/dist/{types-aqOHTHcp.d.ts → types-Br9IXUBG.d.ts} +1 -1
- package/dist/types-ByUo56un.d.ts +88 -0
- package/dist/{types-Cq_H8N_X.d.ts → types-C0dee0Qf.d.ts} +2 -2
- package/dist/{types-DxiCGmmd.d.ts → types-CIlTojhq.d.ts} +2 -2
- package/dist/{types-QCuYJh4q.d.ts → types-CUcFZfpU.d.ts} +1 -1
- package/dist/{types-C97IskBw.d.ts → types-CqrjB9o6.d.ts} +1 -1
- package/dist/{types-BnIEz1sU.d.ts → types-DHrmBJLH.d.ts} +24 -6
- package/dist/{types-Dnemwd77.d.ts → types-DpWfQdSh.d.ts} +5 -1
- package/dist/{types-Bvc4oWT6.d.ts → types-DvupZE0w.d.ts} +1 -1
- package/dist/unplugin/index.d.ts +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/{usage-B0YuTQRO.js → usage-4G6t643F.js} +2 -2
- package/dist/{usage-ItS7nLcl.js → usage-CG9nOYZw.js} +7 -7
- package/dist/usage-iFLJ0-rr.js +2 -0
- package/dist/{validation-error-Ba264OaJ.d.ts → validation-error-BSSw0l_8.d.ts} +1 -1
- package/dist/{validation-error-CnjZ4t92.js → validation-error-ZfUHbKvf.js} +1 -1
- package/dist/validity-dVrUEPJt.js +62 -0
- package/dist/{verifier-BxBo6I44.js → verifier-DMr_Zmb_.js} +51 -15
- package/dist/{rls-verify-CMv9H3wV.js → verify-BztUB3GX.js} +668 -239
- package/dist/{version-BvacVTZR.js → version-BEogC-rB.js} +1 -1
- package/dist/{views-Soe7bGSC.js → views-CJsrBS1f.js} +23 -1
- package/dist/{vocabulary-DA8a8zif.d.ts → vocabulary-hhEFLFoL.d.ts} +15 -6
- package/dist/vue/index.d.ts +24 -19
- package/dist/vue/index.js +24 -8
- package/dist/watch-C8TpsAl4.js +2 -0
- package/dist/{watch-DVBJ9cBq.js → watch-zeucB7eF.js} +6 -6
- package/dist/webmcp/index.d.ts +2 -2
- package/dist/webmcp/index.js +13 -37
- package/dist/wrap-Cr93qqdP.d.ts +14 -0
- package/dist/wrap-DWsU_5Bd.js +26 -0
- package/package.json +8 -8
- package/schemas/catalog-v1.json +6 -0
- package/skills/permdock-credentials/SKILL.md +1 -1
- package/skills/permdock-credentials/references/tokens-keys-links.md +7 -2
- package/skills/permdock-data/references/queries-and-rls.md +4 -0
- package/skills/permdock-tenancy/references/ownership-and-custom-roles.md +1 -0
- package/skills/permdock-wire/SKILL.md +2 -2
- package/skills/permdock-wire/references/adapters.md +9 -1
- package/skills/permdock-wire/references/policy-options.md +10 -1
- package/dist/catalog-CRW5Mx_B.js +0 -2
- package/dist/collect-DSWOk19R.js +0 -2
- package/dist/create-CMwIl-w2.js +0 -253
- package/dist/delegation-D8pUEuce.js +0 -103
- package/dist/doctor-DvH0IQff.js +0 -1194
- package/dist/doctor-collect-CjdO0wQX.js +0 -665
- package/dist/evaluation-items-CfCCejQd.js +0 -40
- package/dist/fields-BMl1RxoX.js +0 -26
- package/dist/grantee-Cv5Sogfv.js +0 -602
- package/dist/hosted-DJQw46hn.js +0 -266
- package/dist/instance-options-Dhhcnm3y.js +0 -22
- package/dist/load-DwJUafYN.js +0 -2
- package/dist/map-AcxK9o_D.js +0 -147
- package/dist/permissions-DVoF6dMt.js +0 -662
- package/dist/powersync-CrBX8hY6.js +0 -2
- package/dist/protected-xAr24FEC.d.ts +0 -40
- package/dist/relations-cjOL01hZ.js +0 -505
- package/dist/rls-graph-CHwx0V8q.js +0 -435
- package/dist/rls-shared-Cewd_-zI.js +0 -495
- package/dist/store-jOBYpiXC.d.ts +0 -36
- package/dist/stores-C0IMQrGI.d.ts +0 -52
- package/dist/supabase-config-Bj3iDH2I.js +0 -472
- package/dist/supabase-hook-gd3s8bxB.js +0 -2
- package/dist/thenable-okc2xDsj.js +0 -10
- package/dist/types-CKNSyIf_.d.ts +0 -52
- package/dist/usage-DorAJiU_.js +0 -2
- package/dist/watch-BwxLTYMZ.js +0 -2
package/dist/testing/index.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as
|
|
3
|
-
import {
|
|
4
|
-
import { _ as memoryRoleSource, u as snapshotFor } from "../src-BDwEZWrZ.js";
|
|
1
|
+
import { r as principal } from "../refs-f3imMjNW.js";
|
|
2
|
+
import { n as role, r as allow, u as memoryLimitStore } from "../role-builder-B4GbKM-i.js";
|
|
3
|
+
import { c as snapshotFor, g as memoryRoleSource, s as definePermissions } from "../src-Bb7no-hS.js";
|
|
5
4
|
import { t as memoryRevocationFeed } from "../revocations-Bhr0aMIC.js";
|
|
6
|
-
import { a as normalizeMemberships, d as scopeList } from "../scopes-
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
5
|
+
import { a as normalizeMemberships, d as scopeList } from "../scopes-tvpa34ep.js";
|
|
6
|
+
import { _ as evaluateCondition, n as parseSnapshot } from "../snapshot-Dc9CHPME.js";
|
|
7
|
+
import { _ as resource, c as listPermissions } from "../permission-registry-BsblpB-z.js";
|
|
8
|
+
import { i as parsePolicyDocument, o as definePolicy, r as mergeHostedGrants } from "../hosted-CBHgkzDl.js";
|
|
9
|
+
import { E as approvalPoliciesFor, b as validateCustomRole, t as createPermDock, v as membershipsClaim } from "../permdock-L26udHSk.js";
|
|
10
10
|
import { t as byCodePoint } from "../compare-pfyVnBjE.js";
|
|
11
|
-
import { n as fromSnapshot } from "../from-snapshot-
|
|
12
|
-
import { o as parseCredential } from "../credential-
|
|
13
|
-
import {
|
|
14
|
-
import { l as APPROVAL_HEADER, r as memoryApprovalStore } from "../store-
|
|
15
|
-
import { o as resolveApproval } from "../helpers-
|
|
16
|
-
import { a as parseApiKey } from "../server-
|
|
17
|
-
import { S as sha256Hex, n as directoryMembershipSource, r as scimHandler } from "../scim-
|
|
11
|
+
import { n as fromSnapshot } from "../from-snapshot-7REQZe0H.js";
|
|
12
|
+
import { o as parseCredential } from "../credential-kue0qpbm.js";
|
|
13
|
+
import { s as problemFromDecision } from "../problem-CnWraVhC.js";
|
|
14
|
+
import { l as APPROVAL_HEADER, r as memoryApprovalStore } from "../store-ILkLUj_o.js";
|
|
15
|
+
import { o as resolveApproval } from "../helpers-C9YmyBIa.js";
|
|
16
|
+
import { a as parseApiKey } from "../server-ZYEHtNHS.js";
|
|
17
|
+
import { S as sha256Hex, n as directoryMembershipSource, r as scimHandler } from "../scim-vFR1O5oG.js";
|
|
18
18
|
import { t as supabaseMembershipsBudget } from "../budget-NbEtbJC_.js";
|
|
19
|
-
import { a as
|
|
20
|
-
import { i as saasPermissions } from "../permissions-
|
|
21
|
-
import { C as saasOrg, E as saasSeed, S as saasMemberships, a as saasProject, g as verifySaasSession, h as signSaasToken, k as saasPolicy, t as saasDoc, w as saasPrincipal, x as saasCustomRoles } from "../saas-
|
|
19
|
+
import { a as quoteSqlIdent, s as quoteSqlTable, t as SQL_IDENT } from "../sql-t8teWOfo.js";
|
|
20
|
+
import { i as saasPermissions } from "../permissions-CCJ6lmxB.js";
|
|
21
|
+
import { C as saasOrg, E as saasSeed, S as saasMemberships, a as saasProject, g as verifySaasSession, h as signSaasToken, k as saasPolicy, t as saasDoc, w as saasPrincipal, x as saasCustomRoles } from "../saas-DtaA4JuW.js";
|
|
22
22
|
import { afterAll, beforeAll, describe, expect, expectTypeOf, it, vi } from "vitest";
|
|
23
23
|
//#region src/testing/describe-policy.ts
|
|
24
24
|
function isOutcomeCell(value) {
|
|
@@ -707,7 +707,7 @@ async function snapshotFixture(policy, subject, options = {}) {
|
|
|
707
707
|
return snapshot;
|
|
708
708
|
}
|
|
709
709
|
//#endregion
|
|
710
|
-
//#region src/testing/http-adapter.ts
|
|
710
|
+
//#region src/testing/http-adapter/domain.ts
|
|
711
711
|
function project$1(id) {
|
|
712
712
|
const row = saasSeed.projects.find((candidate) => candidate.id === id);
|
|
713
713
|
if (row === void 0) throw new Error(`no seed project ${id}`);
|
|
@@ -783,6 +783,11 @@ function createDomain$1() {
|
|
|
783
783
|
project: (id) => id === void 0 ? null : saasSeed.projects.find((row) => row.id === id) ?? null
|
|
784
784
|
};
|
|
785
785
|
}
|
|
786
|
+
async function approverSubject(user, tenant) {
|
|
787
|
+
return (await createPermDock(saasPolicy, saasPrincipal(user, tenant), { tenant })).subject;
|
|
788
|
+
}
|
|
789
|
+
//#endregion
|
|
790
|
+
//#region src/testing/http-adapter/wire.ts
|
|
786
791
|
function restRequest(base, call) {
|
|
787
792
|
const headers = new Headers();
|
|
788
793
|
if (call.authorization !== null) headers.set("authorization", call.authorization);
|
|
@@ -847,9 +852,6 @@ function reasonsOf(body) {
|
|
|
847
852
|
const denials = field(body, "denials");
|
|
848
853
|
return Array.isArray(denials) ? denials.map((denial) => field(denial, "reason")) : [];
|
|
849
854
|
}
|
|
850
|
-
async function approverSubject(user, tenant) {
|
|
851
|
-
return (await createPermDock(saasPolicy, saasPrincipal(user, tenant), { tenant })).subject;
|
|
852
|
-
}
|
|
853
855
|
const PARALLEL = [
|
|
854
856
|
{
|
|
855
857
|
user: "alice",
|
|
@@ -976,6 +978,8 @@ async function untilEnd(stream) {
|
|
|
976
978
|
};
|
|
977
979
|
}
|
|
978
980
|
}
|
|
981
|
+
//#endregion
|
|
982
|
+
//#region src/testing/http-adapter/scenarios.ts
|
|
979
983
|
/**
|
|
980
984
|
* Real-life HTTP scenarios over the shared SaaS domain. `mount` builds the
|
|
981
985
|
* application once with the adapter under test; every expectation below is
|
|
@@ -1768,1329 +1772,1339 @@ function authzenTodoVectors() {
|
|
|
1768
1772
|
};
|
|
1769
1773
|
}
|
|
1770
1774
|
//#endregion
|
|
1771
|
-
//#region src/testing/
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
approval: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6InBlcm1kb2NrLWFwcHJvdmFsK2p3dCJ9.eyJhcHByb3ZhbCI6eyJ0b2tlbiI6InBkMS5hYmMiLCJwZXJtaXNzaW9uIjoicG9zdC5kZWxldGUiLCJyZXNvdXJjZSI6eyJ0eXBlIjoicG9zdCIsImlkIjoiNDIifSwic3RhdHVzIjoiYXBwcm92ZWQifSwic3ViIjoidV8xIiwiaXNzIjoiaHR0cHM6Ly9hcHAuZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwcC5leGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoyMDAwMDAwMDAwLCJqdGkiOiJhcHJfZml4dHVyZSJ9.UHsoSzQCo68G7ee-EFqdzMLvmqaKlFIMdQKFuqZEM4yUQnJsJJTXbld_aCXcyfN-IdjF9cj2F5EHzsV4nLKbBg",
|
|
1791
|
-
decisions: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6InBlcm1kb2NrLWRlY2lzaW9ucytqd3QifQ.eyJldmVudHMiOltdLCJpc3MiOiJodHRwczovL2FwcC5leGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoyMDAwMDAwMDAwLCJqdGkiOiJkZWNfZml4dHVyZSJ9.UZhJd72yFGxHOdjYbzcGYUPgYjvLwOPCtHKxEbLu56SdKSKUDCREXmEBI9WaCPKWjlTBnnIKMYwjG8I8RATJCg"
|
|
1792
|
-
};
|
|
1793
|
-
//#endregion
|
|
1794
|
-
//#region src/testing/conformance.ts
|
|
1795
|
-
function testSubjectResolver(resolver, options) {
|
|
1796
|
-
it("never throws and fails closed to anonymous", async () => {
|
|
1797
|
-
let result;
|
|
1798
|
-
try {
|
|
1799
|
-
result = await resolver(options.invalid);
|
|
1800
|
-
} catch {
|
|
1801
|
-
throw new Error("SubjectResolver must not throw");
|
|
1802
|
-
}
|
|
1803
|
-
expect(result.principal).toBeNull();
|
|
1804
|
-
});
|
|
1805
|
-
}
|
|
1806
|
-
function testMembershipSource(source, options) {
|
|
1807
|
-
const query = options.tenant === void 0 ? {} : { tenant: options.tenant };
|
|
1808
|
-
it("returns well-formed memberships and fails closed on throw", async () => {
|
|
1809
|
-
for (const principal of options.principals) {
|
|
1810
|
-
let memberships = [];
|
|
1811
|
-
try {
|
|
1812
|
-
memberships = await source.membershipsFor(principal, query);
|
|
1813
|
-
} catch {
|
|
1814
|
-
memberships = [];
|
|
1815
|
-
}
|
|
1816
|
-
for (const membership of memberships) {
|
|
1817
|
-
const shapes = [
|
|
1818
|
-
membership.scope !== void 0,
|
|
1819
|
-
membership.tenant !== void 0 || membership.team !== void 0,
|
|
1820
|
-
membership.on !== void 0
|
|
1821
|
-
].filter(Boolean);
|
|
1822
|
-
expect(shapes.length).toBe(1);
|
|
1823
|
-
expect(Array.isArray(membership.roles)).toBe(true);
|
|
1824
|
-
if (options.policy !== void 0) expect(normalizeMemberships([membership], scopeList(options.policy.scopes))).toHaveLength(1);
|
|
1825
|
-
}
|
|
1826
|
-
const expected = options.expect?.[principal.id];
|
|
1827
|
-
if (expected !== void 0) expect(memberships).toEqual(expected);
|
|
1828
|
-
}
|
|
1829
|
-
});
|
|
1830
|
-
if (source.list !== void 0) it("lists every member of an instance it returns a membership for", async () => {
|
|
1831
|
-
for (const principal of options.principals) {
|
|
1832
|
-
let memberships = [];
|
|
1833
|
-
try {
|
|
1834
|
-
memberships = await source.membershipsFor(principal, query);
|
|
1835
|
-
} catch {
|
|
1836
|
-
continue;
|
|
1775
|
+
//#region src/testing/conformance/approvals.ts
|
|
1776
|
+
function sampleApproval(token) {
|
|
1777
|
+
return {
|
|
1778
|
+
v: 1,
|
|
1779
|
+
token,
|
|
1780
|
+
permission: "post.delete",
|
|
1781
|
+
scope: "post:delete",
|
|
1782
|
+
resource: {
|
|
1783
|
+
type: "post",
|
|
1784
|
+
id: "42"
|
|
1785
|
+
},
|
|
1786
|
+
subject: {
|
|
1787
|
+
principal: {
|
|
1788
|
+
id: "u_1",
|
|
1789
|
+
roles: ["member"]
|
|
1790
|
+
},
|
|
1791
|
+
actor: {
|
|
1792
|
+
id: "agent-1",
|
|
1793
|
+
kind: "eve"
|
|
1837
1794
|
}
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1795
|
+
},
|
|
1796
|
+
detail: "post.delete requires human approval.",
|
|
1797
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1798
|
+
expiresAt: new Date(Date.now() + 36e5).toISOString(),
|
|
1799
|
+
status: "pending"
|
|
1800
|
+
};
|
|
1801
|
+
}
|
|
1802
|
+
const approver = {
|
|
1803
|
+
principal: {
|
|
1804
|
+
id: "u_9",
|
|
1805
|
+
roles: ["admin"]
|
|
1806
|
+
},
|
|
1807
|
+
context: {}
|
|
1808
|
+
};
|
|
1809
|
+
const secondApprover = {
|
|
1810
|
+
principal: {
|
|
1811
|
+
id: "u_10",
|
|
1812
|
+
roles: ["admin"]
|
|
1813
|
+
},
|
|
1814
|
+
context: {}
|
|
1815
|
+
};
|
|
1816
|
+
const admin = {
|
|
1817
|
+
kind: "role",
|
|
1818
|
+
role: "admin",
|
|
1819
|
+
scope: "global"
|
|
1820
|
+
};
|
|
1821
|
+
function tenantApproval(token, tenant) {
|
|
1822
|
+
const base = sampleApproval(token);
|
|
1823
|
+
return {
|
|
1824
|
+
...base,
|
|
1825
|
+
subject: {
|
|
1826
|
+
...base.subject,
|
|
1827
|
+
principal: {
|
|
1828
|
+
id: "u_1",
|
|
1829
|
+
roles: ["member"],
|
|
1830
|
+
tenant
|
|
1845
1831
|
}
|
|
1846
1832
|
}
|
|
1847
|
-
}
|
|
1848
|
-
if (source.version !== void 0) it("reports a finite version or none", async () => {
|
|
1849
|
-
for (const principal of options.principals) {
|
|
1850
|
-
const version = await source.version?.({ id: principal.id });
|
|
1851
|
-
expect(version === void 0 || typeof version === "number" && Number.isFinite(version)).toBe(true);
|
|
1852
|
-
}
|
|
1853
|
-
});
|
|
1833
|
+
};
|
|
1854
1834
|
}
|
|
1855
|
-
function
|
|
1856
|
-
return
|
|
1835
|
+
function tenantApprover(tenant) {
|
|
1836
|
+
return {
|
|
1837
|
+
principal: {
|
|
1838
|
+
id: "u_9",
|
|
1839
|
+
roles: [],
|
|
1840
|
+
memberships: [{
|
|
1841
|
+
tenant,
|
|
1842
|
+
roles: ["admin"]
|
|
1843
|
+
}]
|
|
1844
|
+
},
|
|
1845
|
+
context: {}
|
|
1846
|
+
};
|
|
1857
1847
|
}
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
function testRelationSource(source, options) {
|
|
1868
|
-
it("returns chains nearest first, within depth, and truncated only when more exist", async () => {
|
|
1869
|
-
for (const object of options.objects) {
|
|
1870
|
-
const full = await source.ancestors({
|
|
1871
|
-
resource: object.resource,
|
|
1872
|
-
id: object.id,
|
|
1873
|
-
through: "parent",
|
|
1874
|
-
depth: 32
|
|
1875
|
-
});
|
|
1876
|
-
expect(Array.isArray(full.ancestors)).toBe(true);
|
|
1877
|
-
for (const ancestor of full.ancestors) {
|
|
1878
|
-
expect(typeof ancestor.id).toBe("string");
|
|
1879
|
-
expect(ancestor.id).not.toBe("");
|
|
1880
|
-
}
|
|
1881
|
-
expect(full.ancestors.length).toBeLessThanOrEqual(32);
|
|
1882
|
-
const expected = options.expect?.ancestors?.[`${object.resource}:${object.id}`];
|
|
1883
|
-
if (expected !== void 0) expect(full.ancestors.map((ancestor) => ancestor.id)).toEqual(expected);
|
|
1884
|
-
if (full.ancestors.length > 1) {
|
|
1885
|
-
const short = await source.ancestors({
|
|
1886
|
-
resource: object.resource,
|
|
1887
|
-
id: object.id,
|
|
1888
|
-
through: "parent",
|
|
1889
|
-
depth: 1
|
|
1890
|
-
});
|
|
1891
|
-
expect(short.ancestors.map((ancestor) => ancestor.id)).toEqual([full.ancestors[0]?.id]);
|
|
1892
|
-
expect(short.truncated).toBe(true);
|
|
1893
|
-
}
|
|
1894
|
-
const none = await source.ancestors({
|
|
1895
|
-
resource: object.resource,
|
|
1896
|
-
id: object.id,
|
|
1897
|
-
through: "parent",
|
|
1898
|
-
depth: 0
|
|
1899
|
-
});
|
|
1900
|
-
expect(none.ancestors).toEqual([]);
|
|
1901
|
-
}
|
|
1848
|
+
function testApprovalStore(store, options = {}) {
|
|
1849
|
+
it("keeps the existing record when the same call asks again", async () => {
|
|
1850
|
+
await store.create(sampleApproval("dup-token"));
|
|
1851
|
+
await store.resolve("dup-token", {
|
|
1852
|
+
status: "approved",
|
|
1853
|
+
by: approver
|
|
1854
|
+
});
|
|
1855
|
+
await store.create(sampleApproval("dup-token"));
|
|
1856
|
+
expect((await store.get("dup-token"))?.status).toBe("approved");
|
|
1902
1857
|
});
|
|
1903
|
-
it("
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
holder.group.id,
|
|
1916
|
-
holder.group.relation
|
|
1917
|
-
]) {
|
|
1918
|
-
expect(typeof part).toBe("string");
|
|
1919
|
-
expect(part).not.toBe("");
|
|
1920
|
-
}
|
|
1921
|
-
else expect(typeof holder.principal.id).toBe("string");
|
|
1922
|
-
for (const instant of [holder.startsAt, holder.expiresAt]) expect(instant === void 0 || typeof instant === "number" && Number.isFinite(instant)).toBe(true);
|
|
1923
|
-
}
|
|
1924
|
-
const expected = options.expect?.holders?.[`${object.resource}:${object.id}#${object.relation}`];
|
|
1925
|
-
if (expected !== void 0) expect(holders.map(holderKey).toSorted()).toEqual([...expected].toSorted());
|
|
1926
|
-
}
|
|
1858
|
+
it("consumes an approved request exactly once", async () => {
|
|
1859
|
+
await store.create(sampleApproval("once-token"));
|
|
1860
|
+
expect(await store.consume("once-token")).toBeNull();
|
|
1861
|
+
await store.resolve("once-token", {
|
|
1862
|
+
status: "approved",
|
|
1863
|
+
by: approver
|
|
1864
|
+
});
|
|
1865
|
+
const [first, second] = await Promise.all([store.consume("once-token"), store.consume("once-token")]);
|
|
1866
|
+
expect([first, second].filter((item) => item !== null)).toHaveLength(1);
|
|
1867
|
+
expect((first ?? second)?.consumedAt).toEqual(expect.any(String));
|
|
1868
|
+
expect(await store.consume("once-token")).toBeNull();
|
|
1869
|
+
expect((await store.get("once-token"))?.consumedAt).toEqual(expect.any(String));
|
|
1927
1870
|
});
|
|
1928
|
-
it("
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
id: objectId,
|
|
1936
|
-
through,
|
|
1937
|
-
depth: 1
|
|
1938
|
-
});
|
|
1939
|
-
expect(linked.ancestors.map((ancestor) => ancestor.id)).toEqual([id]);
|
|
1940
|
-
}
|
|
1871
|
+
it("refuses an approver from another tenant", async () => {
|
|
1872
|
+
await store.create(tenantApproval("tenant-token", "o_1"));
|
|
1873
|
+
await expect(Promise.resolve().then(() => store.resolve("tenant-token", {
|
|
1874
|
+
status: "approved",
|
|
1875
|
+
by: tenantApprover("o_2")
|
|
1876
|
+
}))).rejects.toThrow(/tenant|eligible/u);
|
|
1877
|
+
expect((await store.get("tenant-token"))?.status).toBe("pending");
|
|
1941
1878
|
});
|
|
1942
|
-
it(
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
resource: object.resource,
|
|
1948
|
-
id: object.id
|
|
1949
|
-
});
|
|
1950
|
-
expect(row === null || row === void 0 || typeof row === "object" && !Array.isArray(row)).toBe(true);
|
|
1951
|
-
}
|
|
1952
|
-
const missing = await read({
|
|
1953
|
-
resource: options.objects[0]?.resource ?? "",
|
|
1954
|
-
id: "permdock-missing-object"
|
|
1879
|
+
it.skipIf(options.reopen === void 0)("resumes an approval after a restart", async () => {
|
|
1880
|
+
await store.create(sampleApproval("restart-token"));
|
|
1881
|
+
await store.resolve("restart-token", {
|
|
1882
|
+
status: "approved",
|
|
1883
|
+
by: approver
|
|
1955
1884
|
});
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
if (first === void 0) return;
|
|
1961
|
-
const missing = {
|
|
1962
|
-
resource: first.resource,
|
|
1963
|
-
id: "permdock-missing-object"
|
|
1964
|
-
};
|
|
1965
|
-
expect((await source.ancestors({
|
|
1966
|
-
...missing,
|
|
1967
|
-
through: "parent",
|
|
1968
|
-
depth: 16
|
|
1969
|
-
})).ancestors).toEqual([]);
|
|
1970
|
-
if (first.relation !== void 0) expect(await source.related({
|
|
1971
|
-
...missing,
|
|
1972
|
-
relation: first.relation
|
|
1973
|
-
})).toEqual([]);
|
|
1885
|
+
const reopened = await options.reopen();
|
|
1886
|
+
expect((await reopened.get("restart-token"))?.status).toBe("approved");
|
|
1887
|
+
expect(await reopened.consume("restart-token")).not.toBeNull();
|
|
1888
|
+
expect(await store.consume("restart-token")).toBeNull();
|
|
1974
1889
|
});
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
const
|
|
1979
|
-
expect(
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
expect(
|
|
1890
|
+
it("creates, gets, lists, resolves and expires", async () => {
|
|
1891
|
+
const request = sampleApproval("opaque-token");
|
|
1892
|
+
await store.create(request);
|
|
1893
|
+
const loaded = await store.get("opaque-token");
|
|
1894
|
+
expect(loaded).toEqual(request);
|
|
1895
|
+
expect(JSON.parse(JSON.stringify(loaded))).toEqual(request);
|
|
1896
|
+
const listed = await store.list({ status: "pending" });
|
|
1897
|
+
expect(listed.items.some((item) => item.token === "opaque-token")).toBe(true);
|
|
1898
|
+
const resolved = await store.resolve("opaque-token", {
|
|
1899
|
+
status: "approved",
|
|
1900
|
+
by: approver
|
|
1901
|
+
});
|
|
1902
|
+
expect(resolved.status).toBe("approved");
|
|
1903
|
+
await expect(Promise.resolve().then(() => store.resolve("opaque-token", {
|
|
1904
|
+
status: "rejected",
|
|
1905
|
+
by: approver
|
|
1906
|
+
}))).rejects.toThrow(/not pending/);
|
|
1907
|
+
await store.create(sampleApproval("stale-token"));
|
|
1908
|
+
const expired = await store.expire(new Date(Date.now() + 72e5));
|
|
1909
|
+
expect(expired).toBeGreaterThanOrEqual(1);
|
|
1983
1910
|
});
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
}
|
|
1911
|
+
it("pages list results with limit and an opaque cursor", async () => {
|
|
1912
|
+
const base = Date.parse("2026-01-01T00:00:00.000Z");
|
|
1913
|
+
await Promise.all([
|
|
1914
|
+
"page-a",
|
|
1915
|
+
"page-b",
|
|
1916
|
+
"page-c"
|
|
1917
|
+
].map(async (token, index) => store.create({
|
|
1918
|
+
...sampleApproval(token),
|
|
1919
|
+
subject: { principal: {
|
|
1920
|
+
id: "u_pager",
|
|
1921
|
+
roles: ["member"]
|
|
1922
|
+
} },
|
|
1923
|
+
createdAt: new Date(base + index * 1e3).toISOString()
|
|
1924
|
+
})));
|
|
1925
|
+
const first = await store.list({
|
|
1926
|
+
principalId: "u_pager",
|
|
1927
|
+
limit: 2
|
|
1928
|
+
});
|
|
1929
|
+
expect(first.items.map((item) => item.token)).toEqual(["page-a", "page-b"]);
|
|
1930
|
+
expect(typeof first.next).toBe("string");
|
|
1931
|
+
const second = await store.list({
|
|
1932
|
+
principalId: "u_pager",
|
|
1933
|
+
limit: 2,
|
|
1934
|
+
cursor: first.next
|
|
1935
|
+
});
|
|
1936
|
+
expect(second.items.map((item) => item.token)).toEqual(["page-c"]);
|
|
1937
|
+
expect(second.next).toBeUndefined();
|
|
1938
|
+
expect((await store.list({
|
|
1939
|
+
principalId: "u_pager",
|
|
1940
|
+
cursor: "not a cursor"
|
|
1941
|
+
})).items).toEqual([]);
|
|
1998
1942
|
});
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
1943
|
+
it("refuses an approver who does not match request.approvers", async () => {
|
|
1944
|
+
const request = {
|
|
1945
|
+
...sampleApproval("gated-token"),
|
|
1946
|
+
approvers: { by: {
|
|
1947
|
+
kind: "role",
|
|
1948
|
+
role: "admin",
|
|
1949
|
+
scope: "global"
|
|
1950
|
+
} }
|
|
1951
|
+
};
|
|
1952
|
+
await store.create(request);
|
|
1953
|
+
const member = {
|
|
1954
|
+
principal: {
|
|
1955
|
+
id: "u_2",
|
|
1956
|
+
roles: ["member"]
|
|
1957
|
+
},
|
|
1958
|
+
context: {}
|
|
1959
|
+
};
|
|
1960
|
+
await expect(Promise.resolve().then(() => store.resolve("gated-token", {
|
|
1961
|
+
status: "approved",
|
|
1962
|
+
by: member
|
|
1963
|
+
}))).rejects.toThrow(/eligible|not pending|not found/);
|
|
2003
1964
|
});
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
per: "hour",
|
|
2012
|
-
now: 17e8
|
|
1965
|
+
it("refuses the principal as approver unless the grant sets distinct: false", async () => {
|
|
1966
|
+
const principal = {
|
|
1967
|
+
principal: {
|
|
1968
|
+
id: "u_1",
|
|
1969
|
+
roles: ["admin"]
|
|
1970
|
+
},
|
|
1971
|
+
context: {}
|
|
2013
1972
|
};
|
|
2014
|
-
const
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
1973
|
+
const shapes = [
|
|
1974
|
+
["self-human", void 0],
|
|
1975
|
+
["self-by", { by: admin }],
|
|
1976
|
+
["self-distinct", {
|
|
1977
|
+
by: admin,
|
|
1978
|
+
distinct: true
|
|
1979
|
+
}]
|
|
1980
|
+
];
|
|
1981
|
+
for (const [token, approvers] of shapes) {
|
|
1982
|
+
await store.create(approvers === void 0 ? sampleApproval(token) : {
|
|
1983
|
+
...sampleApproval(token),
|
|
1984
|
+
approvers
|
|
1985
|
+
});
|
|
1986
|
+
await expect(Promise.resolve().then(() => store.resolve(token, {
|
|
1987
|
+
status: "approved",
|
|
1988
|
+
by: principal
|
|
1989
|
+
}))).rejects.toThrow(/principal/u);
|
|
1990
|
+
expect((await store.get(token))?.status).toBe("pending");
|
|
2020
1991
|
}
|
|
2021
|
-
await store.
|
|
2022
|
-
|
|
2023
|
-
|
|
1992
|
+
await store.create({
|
|
1993
|
+
...sampleApproval("self-optout"),
|
|
1994
|
+
approvers: {
|
|
1995
|
+
by: admin,
|
|
1996
|
+
distinct: false
|
|
1997
|
+
}
|
|
1998
|
+
});
|
|
1999
|
+
const resolved = await store.resolve("self-optout", {
|
|
2000
|
+
status: "approved",
|
|
2001
|
+
by: principal
|
|
2002
|
+
});
|
|
2003
|
+
expect(resolved.resolvedBy).toBe("u_1");
|
|
2024
2004
|
});
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
source: "app",
|
|
2032
|
-
operation: "changed",
|
|
2033
|
-
principal: { id: "u_1" },
|
|
2034
|
-
roles: {
|
|
2035
|
-
added: ["admin"],
|
|
2036
|
-
removed: ["member"]
|
|
2005
|
+
it("stays pending until the quorum is met and counts an approver once", async () => {
|
|
2006
|
+
await store.create({
|
|
2007
|
+
...sampleApproval("quorum-token"),
|
|
2008
|
+
approvers: {
|
|
2009
|
+
by: admin,
|
|
2010
|
+
quorum: 2
|
|
2037
2011
|
}
|
|
2038
|
-
}
|
|
2039
|
-
await
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2012
|
+
});
|
|
2013
|
+
const first = await store.resolve("quorum-token", {
|
|
2014
|
+
status: "approved",
|
|
2015
|
+
by: approver
|
|
2016
|
+
});
|
|
2017
|
+
expect(first.status).toBe("pending");
|
|
2018
|
+
expect(first.approvals).toEqual([{
|
|
2019
|
+
by: "u_9",
|
|
2020
|
+
at: expect.any(String)
|
|
2021
|
+
}]);
|
|
2022
|
+
expect(await store.consume("quorum-token")).toBeNull();
|
|
2023
|
+
await expect(Promise.resolve().then(() => store.resolve("quorum-token", {
|
|
2024
|
+
status: "approved",
|
|
2025
|
+
by: approver
|
|
2026
|
+
}))).rejects.toThrow(/already approved/u);
|
|
2027
|
+
const second = await store.resolve("quorum-token", {
|
|
2028
|
+
status: "approved",
|
|
2029
|
+
by: secondApprover
|
|
2030
|
+
});
|
|
2031
|
+
expect(second.status).toBe("approved");
|
|
2032
|
+
expect(second.approvals?.map((item) => item.by)).toEqual(["u_9", "u_10"]);
|
|
2033
|
+
expect(second.resolvedBy).toBe("u_10");
|
|
2034
|
+
expect(await store.consume("quorum-token")).not.toBeNull();
|
|
2044
2035
|
});
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
});
|
|
2053
|
-
}
|
|
2054
|
-
function testPolicySource(source, options = {}) {
|
|
2055
|
-
const readCurrent = () => {
|
|
2056
|
-
const document = source.current();
|
|
2057
|
-
expect(document !== null && typeof document === "object" && "then" in document && typeof document.then === "function").toBe(false);
|
|
2058
|
-
return document;
|
|
2059
|
-
};
|
|
2060
|
-
it("returns null or a v1 policy document synchronously", () => {
|
|
2061
|
-
const document = readCurrent();
|
|
2062
|
-
if (document !== null) expect(parsePolicyDocument(document)).toEqual(document);
|
|
2063
|
-
});
|
|
2064
|
-
it("refreshes without rejecting and keeps current() synchronous", async () => {
|
|
2065
|
-
await expect(Promise.resolve(source.refresh())).resolves.toBeUndefined();
|
|
2066
|
-
const document = readCurrent();
|
|
2067
|
-
if (document !== null) expect(document.v).toBe(1);
|
|
2068
|
-
});
|
|
2069
|
-
if (options.policy !== void 0) {
|
|
2070
|
-
const policy = options.policy;
|
|
2071
|
-
it("merges only grants on hostable permissions", () => {
|
|
2072
|
-
const document = readCurrent();
|
|
2073
|
-
const merged = mergeHostedGrants(policy, document);
|
|
2074
|
-
for (const grant of merged.policy.grants) if (grant.hosted !== void 0) expect(policy.hostable).toContain(grant.permission.key);
|
|
2075
|
-
for (const dropped of merged.dropped) expect(dropped.kind).toBe("hosted-grant-dropped");
|
|
2036
|
+
it("resolves a vouched verdict at once, records its rule, and still refuses the requester", async () => {
|
|
2037
|
+
await store.create({
|
|
2038
|
+
...sampleApproval("vouched-token"),
|
|
2039
|
+
approvers: {
|
|
2040
|
+
by: admin,
|
|
2041
|
+
quorum: 2
|
|
2042
|
+
}
|
|
2076
2043
|
});
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
resource: {
|
|
2086
|
-
type: "post",
|
|
2087
|
-
id: "42"
|
|
2088
|
-
},
|
|
2089
|
-
subject: {
|
|
2090
|
-
principal: {
|
|
2091
|
-
id: "u_1",
|
|
2092
|
-
roles: ["member"]
|
|
2044
|
+
await expect(Promise.resolve().then(() => store.resolve("vouched-token", {
|
|
2045
|
+
status: "approved",
|
|
2046
|
+
by: {
|
|
2047
|
+
principal: {
|
|
2048
|
+
id: "u_1",
|
|
2049
|
+
roles: []
|
|
2050
|
+
},
|
|
2051
|
+
context: {}
|
|
2093
2052
|
},
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
}
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
id: "u_10",
|
|
2115
|
-
roles: ["admin"]
|
|
2116
|
-
},
|
|
2117
|
-
context: {}
|
|
2118
|
-
};
|
|
2119
|
-
const admin = {
|
|
2120
|
-
kind: "role",
|
|
2121
|
-
role: "admin",
|
|
2122
|
-
scope: "global"
|
|
2123
|
-
};
|
|
2124
|
-
function testReplayStore(store) {
|
|
2125
|
-
it("records a jti after remember and reports it as seen", async () => {
|
|
2126
|
-
const jti = "jti-1";
|
|
2127
|
-
expect(await store.seen(jti)).toBe(false);
|
|
2128
|
-
await store.remember(jti);
|
|
2129
|
-
expect(await store.seen(jti)).toBe(true);
|
|
2130
|
-
expect(await store.seen("jti-2")).toBe(false);
|
|
2131
|
-
});
|
|
2132
|
-
it("forgets a jti whose expiresAt has passed", async () => {
|
|
2133
|
-
await store.remember("ttl-jti", Math.floor(Date.now() / 1e3) - 1);
|
|
2134
|
-
expect(await store.seen("ttl-jti")).toBe(false);
|
|
2135
|
-
});
|
|
2136
|
-
it("claims a key once under concurrency and releases it for a retry", async (context) => {
|
|
2137
|
-
if (store.claim === void 0 || store.release === void 0) {
|
|
2138
|
-
context.skip();
|
|
2139
|
-
return;
|
|
2140
|
-
}
|
|
2141
|
-
const expiresAt = Math.floor(Date.now() / 1e3) + 60;
|
|
2142
|
-
const results = await Promise.all([store.claim("claim-key", expiresAt), store.claim("claim-key", expiresAt)]);
|
|
2143
|
-
expect(results.toSorted()).toEqual([false, true]);
|
|
2144
|
-
expect(await store.seen("claim-key")).toBe(true);
|
|
2145
|
-
await store.release("claim-key");
|
|
2146
|
-
expect(await store.claim("claim-key", expiresAt)).toBe(true);
|
|
2053
|
+
vouched: "app-rule"
|
|
2054
|
+
}))).rejects.toThrow(/principal/u);
|
|
2055
|
+
const resolved = await store.resolve("vouched-token", {
|
|
2056
|
+
status: "approved",
|
|
2057
|
+
by: {
|
|
2058
|
+
principal: {
|
|
2059
|
+
id: "u_outsider",
|
|
2060
|
+
roles: []
|
|
2061
|
+
},
|
|
2062
|
+
context: {}
|
|
2063
|
+
},
|
|
2064
|
+
vouched: "app-rule"
|
|
2065
|
+
});
|
|
2066
|
+
expect(resolved.status).toBe("approved");
|
|
2067
|
+
expect(resolved.vouched).toBe("app-rule");
|
|
2068
|
+
expect(resolved.approvals).toEqual([{
|
|
2069
|
+
by: "u_outsider",
|
|
2070
|
+
at: expect.any(String),
|
|
2071
|
+
vouched: "app-rule"
|
|
2072
|
+
}]);
|
|
2147
2073
|
});
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2074
|
+
it("lets one rejection end a quorum request", async () => {
|
|
2075
|
+
await store.create({
|
|
2076
|
+
...sampleApproval("veto-token"),
|
|
2077
|
+
approvers: {
|
|
2078
|
+
by: admin,
|
|
2079
|
+
quorum: 2
|
|
2080
|
+
}
|
|
2155
2081
|
});
|
|
2156
|
-
const
|
|
2157
|
-
|
|
2082
|
+
const rejected = await store.resolve("veto-token", {
|
|
2083
|
+
status: "rejected",
|
|
2084
|
+
by: approver
|
|
2158
2085
|
});
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2086
|
+
expect(rejected.status).toBe("rejected");
|
|
2087
|
+
expect(rejected.approvals).toBeUndefined();
|
|
2088
|
+
});
|
|
2089
|
+
it("admits the escalation grantee only once the request has waited", async () => {
|
|
2090
|
+
const auditor = {
|
|
2091
|
+
principal: {
|
|
2092
|
+
id: "u_11",
|
|
2093
|
+
roles: ["auditor"]
|
|
2094
|
+
},
|
|
2095
|
+
context: {}
|
|
2163
2096
|
};
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
principal: "u-feed",
|
|
2172
|
-
tenant: "t-feed",
|
|
2173
|
-
kind: "changed"
|
|
2097
|
+
const escalation = {
|
|
2098
|
+
after: "1h",
|
|
2099
|
+
to: {
|
|
2100
|
+
kind: "role",
|
|
2101
|
+
role: "auditor",
|
|
2102
|
+
scope: "global"
|
|
2103
|
+
}
|
|
2174
2104
|
};
|
|
2175
|
-
await
|
|
2176
|
-
|
|
2177
|
-
|
|
2105
|
+
await store.create({
|
|
2106
|
+
...sampleApproval("fresh-escalation"),
|
|
2107
|
+
approvers: {
|
|
2108
|
+
by: admin,
|
|
2109
|
+
escalation
|
|
2110
|
+
}
|
|
2178
2111
|
});
|
|
2179
|
-
expect(
|
|
2180
|
-
|
|
2112
|
+
await expect(Promise.resolve().then(() => store.resolve("fresh-escalation", {
|
|
2113
|
+
status: "approved",
|
|
2114
|
+
by: auditor
|
|
2115
|
+
}))).rejects.toThrow(/eligible/u);
|
|
2116
|
+
await store.create({
|
|
2117
|
+
...sampleApproval("waited-escalation"),
|
|
2118
|
+
createdAt: (/* @__PURE__ */ new Date(Date.now() - 72e5)).toISOString(),
|
|
2119
|
+
approvers: {
|
|
2120
|
+
by: admin,
|
|
2121
|
+
escalation
|
|
2122
|
+
}
|
|
2123
|
+
});
|
|
2124
|
+
const resolved = await store.resolve("waited-escalation", {
|
|
2125
|
+
status: "approved",
|
|
2126
|
+
by: auditor
|
|
2127
|
+
});
|
|
2128
|
+
expect(resolved.status).toBe("approved");
|
|
2129
|
+
expect(resolved.resolvedBy).toBe("u_11");
|
|
2181
2130
|
});
|
|
2182
|
-
it("
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2131
|
+
it("completes sequential stages in order and records each stage", async () => {
|
|
2132
|
+
await store.create({
|
|
2133
|
+
...sampleApproval("stages-token"),
|
|
2134
|
+
approvers: {
|
|
2135
|
+
mode: "sequential",
|
|
2136
|
+
stages: [{ by: {
|
|
2137
|
+
kind: "user",
|
|
2138
|
+
id: "u_10"
|
|
2139
|
+
} }, { by: admin }]
|
|
2140
|
+
}
|
|
2186
2141
|
});
|
|
2187
|
-
|
|
2188
|
-
|
|
2142
|
+
await expect(Promise.resolve().then(() => store.resolve("stages-token", {
|
|
2143
|
+
status: "approved",
|
|
2144
|
+
by: approver
|
|
2145
|
+
}))).rejects.toThrow(/eligible/u);
|
|
2146
|
+
const first = await store.resolve("stages-token", {
|
|
2147
|
+
status: "approved",
|
|
2148
|
+
by: secondApprover
|
|
2189
2149
|
});
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2150
|
+
expect(first.status).toBe("pending");
|
|
2151
|
+
expect(first.approvals).toEqual([{
|
|
2152
|
+
by: "u_10",
|
|
2153
|
+
at: expect.any(String),
|
|
2154
|
+
stage: 0
|
|
2155
|
+
}]);
|
|
2156
|
+
const second = await store.resolve("stages-token", {
|
|
2157
|
+
status: "approved",
|
|
2158
|
+
by: approver
|
|
2197
2159
|
});
|
|
2198
|
-
|
|
2199
|
-
|
|
2160
|
+
expect(second.status).toBe("approved");
|
|
2161
|
+
expect(second.approvals?.map((item) => item.stage)).toEqual([0, 1]);
|
|
2200
2162
|
});
|
|
2201
|
-
it("
|
|
2202
|
-
const
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
principal: "u-bad",
|
|
2211
|
-
kind: "granted"
|
|
2212
|
-
}]) await expect((async () => {
|
|
2213
|
-
await feed.revoke(event);
|
|
2214
|
-
})()).rejects.toThrow(TypeError);
|
|
2215
|
-
expect(seen).toEqual([]);
|
|
2216
|
-
stop();
|
|
2217
|
-
});
|
|
2218
|
-
}
|
|
2219
|
-
function testDirectoryStore(store, options) {
|
|
2220
|
-
const [home, other] = options.tenants;
|
|
2221
|
-
it("round-trips users and groups, isolates tenants, and drops inactive memberships", async () => {
|
|
2222
|
-
const created = await store.putUser(home, {
|
|
2223
|
-
id: "",
|
|
2224
|
-
userName: "ada",
|
|
2225
|
-
externalId: "00u1",
|
|
2226
|
-
active: true,
|
|
2227
|
-
meta: {
|
|
2228
|
-
created: "",
|
|
2229
|
-
lastModified: ""
|
|
2230
|
-
}
|
|
2231
|
-
});
|
|
2232
|
-
expect(created.id).not.toBe("");
|
|
2233
|
-
expect(await store.getUser(home, created.id)).toMatchObject({
|
|
2234
|
-
userName: "ada",
|
|
2235
|
-
externalId: "00u1"
|
|
2236
|
-
});
|
|
2237
|
-
expect(await store.getUser(other, created.id)).toBeNull();
|
|
2238
|
-
await expect(store.putUser(home, {
|
|
2239
|
-
id: "",
|
|
2240
|
-
userName: "ada",
|
|
2241
|
-
active: true,
|
|
2242
|
-
meta: {
|
|
2243
|
-
created: "",
|
|
2244
|
-
lastModified: ""
|
|
2245
|
-
}
|
|
2246
|
-
})).rejects.toThrow(/userName/);
|
|
2247
|
-
const group = await store.putGroup(home, {
|
|
2248
|
-
id: "g_editors",
|
|
2249
|
-
displayName: "Editors",
|
|
2250
|
-
members: [{ value: created.id }],
|
|
2251
|
-
roles: ["editor"],
|
|
2252
|
-
meta: {
|
|
2253
|
-
created: "",
|
|
2254
|
-
lastModified: ""
|
|
2255
|
-
}
|
|
2256
|
-
});
|
|
2257
|
-
expect(await store.groupsFor(home, created.id)).toEqual([expect.objectContaining({
|
|
2258
|
-
id: group.id,
|
|
2259
|
-
displayName: "Editors"
|
|
2260
|
-
})]);
|
|
2261
|
-
expect(await store.groupsFor(other, created.id)).toEqual([]);
|
|
2262
|
-
const patched = await store.patchUser(home, created.id, [{
|
|
2263
|
-
op: "replace",
|
|
2264
|
-
path: "active",
|
|
2265
|
-
value: false
|
|
2266
|
-
}]);
|
|
2267
|
-
expect(patched.active).toBe(false);
|
|
2268
|
-
const source = directoryMembershipSource(store, { assignable: ["editor"] });
|
|
2269
|
-
expect(await source.membershipsFor({ id: "00u1" }, { tenant: home })).toEqual([]);
|
|
2270
|
-
await store.patchUser(home, created.id, [{
|
|
2271
|
-
op: "replace",
|
|
2272
|
-
path: "active",
|
|
2273
|
-
value: true
|
|
2274
|
-
}]);
|
|
2275
|
-
expect(await source.membershipsFor({ id: "00u1" }, { tenant: home })).toEqual([{
|
|
2276
|
-
tenant: home,
|
|
2277
|
-
roles: ["editor"],
|
|
2278
|
-
via: `group:${group.id}`,
|
|
2279
|
-
managedBy: "idp"
|
|
2280
|
-
}]);
|
|
2281
|
-
await store.patchGroup(home, group.id, [{
|
|
2282
|
-
op: "remove",
|
|
2283
|
-
path: "members",
|
|
2284
|
-
value: [{ value: created.id }]
|
|
2285
|
-
}]);
|
|
2286
|
-
expect(await store.groupsFor(home, created.id)).toEqual([]);
|
|
2287
|
-
});
|
|
2288
|
-
it("ends sessions when scimHandler deactivates a user in this store", async () => {
|
|
2289
|
-
const feed = memoryRevocationFeed();
|
|
2290
|
-
const seen = [];
|
|
2291
|
-
feed.subscribe((event) => {
|
|
2292
|
-
seen.push(event);
|
|
2293
|
-
});
|
|
2294
|
-
const kinds = [];
|
|
2295
|
-
const token = "conformance-scim-token";
|
|
2296
|
-
const handle = scimHandler({
|
|
2297
|
-
store,
|
|
2298
|
-
tenant: home,
|
|
2299
|
-
token: {
|
|
2300
|
-
hash: "sha256",
|
|
2301
|
-
lookup: () => sha256Hex(token)
|
|
2302
|
-
},
|
|
2303
|
-
revocations: feed,
|
|
2304
|
-
onChange: (change) => {
|
|
2305
|
-
kinds.push(change.kind);
|
|
2306
|
-
}
|
|
2307
|
-
});
|
|
2308
|
-
const call = (path, method, body) => handle(new Request(`https://app.example.com/scim/v2${path}`, {
|
|
2309
|
-
method,
|
|
2310
|
-
headers: {
|
|
2311
|
-
authorization: `Bearer ${token}`,
|
|
2312
|
-
"content-type": "application/scim+json"
|
|
2313
|
-
},
|
|
2314
|
-
body: JSON.stringify(body)
|
|
2315
|
-
}));
|
|
2316
|
-
const created = await call("/Users", "POST", {
|
|
2317
|
-
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
|
2318
|
-
userName: "grace",
|
|
2319
|
-
active: true
|
|
2320
|
-
});
|
|
2321
|
-
expect(created.status).toBe(201);
|
|
2322
|
-
const id = String((await created.json()).id);
|
|
2323
|
-
seen.length = 0;
|
|
2324
|
-
const patched = await call(`/Users/${id}`, "PATCH", {
|
|
2325
|
-
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
2326
|
-
Operations: [{
|
|
2327
|
-
op: "replace",
|
|
2328
|
-
path: "active",
|
|
2329
|
-
value: false
|
|
2330
|
-
}]
|
|
2331
|
-
});
|
|
2332
|
-
expect(patched.status).toBe(200);
|
|
2333
|
-
expect(seen).toContainEqual({
|
|
2334
|
-
principal: "grace",
|
|
2335
|
-
tenant: home,
|
|
2336
|
-
kind: "session-revoked"
|
|
2337
|
-
});
|
|
2338
|
-
expect(seen.every((event) => event.kind === "session-revoked")).toBe(true);
|
|
2339
|
-
expect(kinds).toEqual(["changed", "session-revoked"]);
|
|
2340
|
-
});
|
|
2341
|
-
}
|
|
2342
|
-
function tenantApproval(token, tenant) {
|
|
2343
|
-
const base = sampleApproval(token);
|
|
2344
|
-
return {
|
|
2345
|
-
...base,
|
|
2346
|
-
subject: {
|
|
2347
|
-
...base.subject,
|
|
2348
|
-
principal: {
|
|
2349
|
-
id: "u_1",
|
|
2350
|
-
roles: ["member"],
|
|
2351
|
-
tenant
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
};
|
|
2355
|
-
}
|
|
2356
|
-
function tenantApprover(tenant) {
|
|
2357
|
-
return {
|
|
2358
|
-
principal: {
|
|
2359
|
-
id: "u_9",
|
|
2360
|
-
roles: [],
|
|
2361
|
-
memberships: [{
|
|
2362
|
-
tenant,
|
|
2363
|
-
roles: ["admin"]
|
|
2364
|
-
}]
|
|
2365
|
-
},
|
|
2366
|
-
context: {}
|
|
2367
|
-
};
|
|
2368
|
-
}
|
|
2369
|
-
function testApprovalStore(store, options = {}) {
|
|
2370
|
-
it("keeps the existing record when the same call asks again", async () => {
|
|
2371
|
-
await store.create(sampleApproval("dup-token"));
|
|
2372
|
-
await store.resolve("dup-token", {
|
|
2373
|
-
status: "approved",
|
|
2374
|
-
by: approver
|
|
2375
|
-
});
|
|
2376
|
-
await store.create(sampleApproval("dup-token"));
|
|
2377
|
-
expect((await store.get("dup-token"))?.status).toBe("approved");
|
|
2378
|
-
});
|
|
2379
|
-
it("consumes an approved request exactly once", async () => {
|
|
2380
|
-
await store.create(sampleApproval("once-token"));
|
|
2381
|
-
expect(await store.consume("once-token")).toBeNull();
|
|
2382
|
-
await store.resolve("once-token", {
|
|
2383
|
-
status: "approved",
|
|
2384
|
-
by: approver
|
|
2163
|
+
it("matches a relation approver only through the verdict's relations", async () => {
|
|
2164
|
+
const manager = {
|
|
2165
|
+
kind: "relation",
|
|
2166
|
+
resource: "post",
|
|
2167
|
+
relation: "manager"
|
|
2168
|
+
};
|
|
2169
|
+
await store.create({
|
|
2170
|
+
...sampleApproval("relation-token"),
|
|
2171
|
+
approvers: { by: manager }
|
|
2385
2172
|
});
|
|
2386
|
-
|
|
2387
|
-
expect([first, second].filter((item) => item !== null)).toHaveLength(1);
|
|
2388
|
-
expect((first ?? second)?.consumedAt).toEqual(expect.any(String));
|
|
2389
|
-
expect(await store.consume("once-token")).toBeNull();
|
|
2390
|
-
expect((await store.get("once-token"))?.consumedAt).toEqual(expect.any(String));
|
|
2391
|
-
});
|
|
2392
|
-
it("refuses an approver from another tenant", async () => {
|
|
2393
|
-
await store.create(tenantApproval("tenant-token", "o_1"));
|
|
2394
|
-
await expect(Promise.resolve().then(() => store.resolve("tenant-token", {
|
|
2395
|
-
status: "approved",
|
|
2396
|
-
by: tenantApprover("o_2")
|
|
2397
|
-
}))).rejects.toThrow(/tenant|eligible/u);
|
|
2398
|
-
expect((await store.get("tenant-token"))?.status).toBe("pending");
|
|
2399
|
-
});
|
|
2400
|
-
it.skipIf(options.reopen === void 0)("resumes an approval after a restart", async () => {
|
|
2401
|
-
await store.create(sampleApproval("restart-token"));
|
|
2402
|
-
await store.resolve("restart-token", {
|
|
2173
|
+
await expect(Promise.resolve().then(() => store.resolve("relation-token", {
|
|
2403
2174
|
status: "approved",
|
|
2404
2175
|
by: approver
|
|
2405
|
-
});
|
|
2406
|
-
const
|
|
2407
|
-
expect((await reopened.get("restart-token"))?.status).toBe("approved");
|
|
2408
|
-
expect(await reopened.consume("restart-token")).not.toBeNull();
|
|
2409
|
-
expect(await store.consume("restart-token")).toBeNull();
|
|
2410
|
-
});
|
|
2411
|
-
it("creates, gets, lists, resolves and expires", async () => {
|
|
2412
|
-
const request = sampleApproval("opaque-token");
|
|
2413
|
-
await store.create(request);
|
|
2414
|
-
const loaded = await store.get("opaque-token");
|
|
2415
|
-
expect(loaded).toEqual(request);
|
|
2416
|
-
expect(JSON.parse(JSON.stringify(loaded))).toEqual(request);
|
|
2417
|
-
const listed = await store.list({ status: "pending" });
|
|
2418
|
-
expect(listed.items.some((item) => item.token === "opaque-token")).toBe(true);
|
|
2419
|
-
const resolved = await store.resolve("opaque-token", {
|
|
2176
|
+
}))).rejects.toThrow(/eligible/u);
|
|
2177
|
+
const resolved = await store.resolve("relation-token", {
|
|
2420
2178
|
status: "approved",
|
|
2421
|
-
by: approver
|
|
2179
|
+
by: approver,
|
|
2180
|
+
relations: [JSON.stringify([
|
|
2181
|
+
"post",
|
|
2182
|
+
"manager",
|
|
2183
|
+
null,
|
|
2184
|
+
null
|
|
2185
|
+
])]
|
|
2422
2186
|
});
|
|
2423
2187
|
expect(resolved.status).toBe("approved");
|
|
2424
|
-
await expect(Promise.resolve().then(() => store.resolve("opaque-token", {
|
|
2425
|
-
status: "rejected",
|
|
2426
|
-
by: approver
|
|
2427
|
-
}))).rejects.toThrow(/not pending/);
|
|
2428
|
-
await store.create(sampleApproval("stale-token"));
|
|
2429
|
-
const expired = await store.expire(new Date(Date.now() + 72e5));
|
|
2430
|
-
expect(expired).toBeGreaterThanOrEqual(1);
|
|
2431
2188
|
});
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
}
|
|
2446
|
-
const
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2189
|
+
}
|
|
2190
|
+
/**
|
|
2191
|
+
* Checks an `ApprovalPolicySource`: entries are plain JSON, belong to the
|
|
2192
|
+
* queried tenant or to none, and every entry loads against `policy` (an
|
|
2193
|
+
* entry that does not would deny every call).
|
|
2194
|
+
*/
|
|
2195
|
+
function testApprovalPolicySource(source, options) {
|
|
2196
|
+
it("answers per tenant with plain entries that load against the policy", async () => {
|
|
2197
|
+
const entries = await source.approvalPoliciesFor({ tenants: [options.tenant] });
|
|
2198
|
+
expect(Array.isArray(entries)).toBe(true);
|
|
2199
|
+
for (const entry of entries) {
|
|
2200
|
+
expect(JSON.parse(JSON.stringify(entry))).toEqual(entry);
|
|
2201
|
+
expect([void 0, options.tenant]).toContain(entry.tenant);
|
|
2202
|
+
}
|
|
2203
|
+
const loaded = await approvalPoliciesFor(options.policy, source, [options.tenant], []);
|
|
2204
|
+
expect(loaded).not.toBe("failed");
|
|
2205
|
+
const other = await source.approvalPoliciesFor({ tenants: [options.unknown ?? "__permdock_unknown__"] });
|
|
2206
|
+
expect(other.filter((entry) => entry.tenant !== void 0)).toEqual([]);
|
|
2207
|
+
});
|
|
2208
|
+
}
|
|
2209
|
+
//#endregion
|
|
2210
|
+
//#region src/testing/conformance/where.ts
|
|
2211
|
+
const WHERE_ROWS = [
|
|
2212
|
+
{
|
|
2213
|
+
id: "r1",
|
|
2214
|
+
authorId: "u1",
|
|
2215
|
+
score: 5,
|
|
2216
|
+
title: "Plan 100%",
|
|
2217
|
+
archived: false,
|
|
2218
|
+
note: null
|
|
2219
|
+
},
|
|
2220
|
+
{
|
|
2221
|
+
id: "r2",
|
|
2222
|
+
authorId: "u2",
|
|
2223
|
+
score: 10,
|
|
2224
|
+
title: "plan_b",
|
|
2225
|
+
archived: true,
|
|
2226
|
+
note: "kept"
|
|
2227
|
+
},
|
|
2228
|
+
{
|
|
2229
|
+
id: "r3",
|
|
2230
|
+
authorId: null,
|
|
2231
|
+
score: null,
|
|
2232
|
+
title: null,
|
|
2233
|
+
archived: null,
|
|
2234
|
+
note: null
|
|
2235
|
+
}
|
|
2236
|
+
];
|
|
2237
|
+
const WHERE_CASES = [
|
|
2238
|
+
["eq", {
|
|
2239
|
+
op: "eq",
|
|
2240
|
+
field: "authorId",
|
|
2241
|
+
value: "u1"
|
|
2242
|
+
}],
|
|
2243
|
+
["ne", {
|
|
2244
|
+
op: "ne",
|
|
2245
|
+
field: "authorId",
|
|
2246
|
+
value: "u1"
|
|
2247
|
+
}],
|
|
2248
|
+
["eq on a boolean", {
|
|
2249
|
+
op: "eq",
|
|
2250
|
+
field: "archived",
|
|
2251
|
+
value: false
|
|
2252
|
+
}],
|
|
2253
|
+
["gt", {
|
|
2254
|
+
op: "gt",
|
|
2255
|
+
field: "score",
|
|
2256
|
+
value: 5
|
|
2257
|
+
}],
|
|
2258
|
+
["gte", {
|
|
2259
|
+
op: "gte",
|
|
2260
|
+
field: "score",
|
|
2261
|
+
value: 5
|
|
2262
|
+
}],
|
|
2263
|
+
["lt", {
|
|
2264
|
+
op: "lt",
|
|
2265
|
+
field: "score",
|
|
2266
|
+
value: 10
|
|
2267
|
+
}],
|
|
2268
|
+
["lte", {
|
|
2269
|
+
op: "lte",
|
|
2270
|
+
field: "score",
|
|
2271
|
+
value: 10
|
|
2272
|
+
}],
|
|
2273
|
+
["contains a literal %", {
|
|
2274
|
+
op: "contains",
|
|
2275
|
+
field: "title",
|
|
2276
|
+
value: "100%"
|
|
2277
|
+
}],
|
|
2278
|
+
["contains a literal _", {
|
|
2279
|
+
op: "contains",
|
|
2280
|
+
field: "title",
|
|
2281
|
+
value: "n_b"
|
|
2282
|
+
}],
|
|
2283
|
+
["in", {
|
|
2284
|
+
op: "in",
|
|
2285
|
+
field: "authorId",
|
|
2286
|
+
value: ["u1", "u2"]
|
|
2287
|
+
}],
|
|
2288
|
+
["notIn", {
|
|
2289
|
+
op: "notIn",
|
|
2290
|
+
field: "authorId",
|
|
2291
|
+
value: ["u1"]
|
|
2292
|
+
}],
|
|
2293
|
+
["isNull", {
|
|
2294
|
+
op: "isNull",
|
|
2295
|
+
field: "note",
|
|
2296
|
+
value: true
|
|
2297
|
+
}],
|
|
2298
|
+
["is not null", {
|
|
2299
|
+
op: "isNull",
|
|
2300
|
+
field: "note",
|
|
2301
|
+
value: false
|
|
2302
|
+
}],
|
|
2303
|
+
["and", {
|
|
2304
|
+
op: "and",
|
|
2305
|
+
conditions: [{
|
|
2306
|
+
op: "eq",
|
|
2307
|
+
field: "authorId",
|
|
2308
|
+
value: "u1"
|
|
2309
|
+
}, {
|
|
2310
|
+
op: "lt",
|
|
2311
|
+
field: "score",
|
|
2312
|
+
value: 10
|
|
2313
|
+
}]
|
|
2314
|
+
}],
|
|
2315
|
+
["or", {
|
|
2316
|
+
op: "or",
|
|
2317
|
+
conditions: [{
|
|
2318
|
+
op: "eq",
|
|
2319
|
+
field: "authorId",
|
|
2320
|
+
value: "u1"
|
|
2321
|
+
}, {
|
|
2322
|
+
op: "eq",
|
|
2323
|
+
field: "archived",
|
|
2324
|
+
value: true
|
|
2325
|
+
}]
|
|
2326
|
+
}],
|
|
2327
|
+
["not over a NULL field", {
|
|
2328
|
+
op: "not",
|
|
2329
|
+
condition: {
|
|
2330
|
+
op: "eq",
|
|
2331
|
+
field: "authorId",
|
|
2332
|
+
value: "u1"
|
|
2333
|
+
}
|
|
2334
|
+
}],
|
|
2335
|
+
["not over and", {
|
|
2336
|
+
op: "not",
|
|
2337
|
+
condition: {
|
|
2338
|
+
op: "and",
|
|
2339
|
+
conditions: [{
|
|
2340
|
+
op: "eq",
|
|
2341
|
+
field: "authorId",
|
|
2342
|
+
value: "u2"
|
|
2343
|
+
}, {
|
|
2344
|
+
op: "gt",
|
|
2345
|
+
field: "score",
|
|
2346
|
+
value: 5
|
|
2347
|
+
}]
|
|
2348
|
+
}
|
|
2349
|
+
}],
|
|
2350
|
+
["and of nothing", {
|
|
2351
|
+
op: "and",
|
|
2352
|
+
conditions: []
|
|
2353
|
+
}],
|
|
2354
|
+
["not of the empty allow set", {
|
|
2355
|
+
op: "not",
|
|
2356
|
+
condition: {
|
|
2357
|
+
op: "or",
|
|
2358
|
+
conditions: []
|
|
2359
|
+
}
|
|
2360
|
+
}]
|
|
2361
|
+
];
|
|
2362
|
+
const WHERE_SUBJECT = {
|
|
2363
|
+
principal: null,
|
|
2364
|
+
context: {}
|
|
2365
|
+
};
|
|
2366
|
+
/**
|
|
2367
|
+
* `matches` answers whether the compiled output selects `row`, for example
|
|
2368
|
+
* by running it against a table holding only that row. With it, every case
|
|
2369
|
+
* must select the rows the in-memory evaluator keeps, including rows whose
|
|
2370
|
+
* fields are NULL.
|
|
2371
|
+
*/
|
|
2372
|
+
function testWhereCompiler(compiler, options) {
|
|
2373
|
+
const { matches } = options;
|
|
2374
|
+
it.skipIf(matches === void 0).each(WHERE_CASES)("selects the rows the evaluator keeps: %s", async (_name, condition) => {
|
|
2375
|
+
if (matches === void 0) return;
|
|
2376
|
+
const compiled = compiler(condition, options.target);
|
|
2377
|
+
const selected = [];
|
|
2378
|
+
for (const row of WHERE_ROWS) if (await matches(compiled, row)) selected.push(row["id"]);
|
|
2379
|
+
expect(selected).toEqual(WHERE_ROWS.filter((row) => evaluateCondition(condition, row, WHERE_SUBJECT)).map((row) => row["id"]));
|
|
2380
|
+
});
|
|
2381
|
+
it("fails closed on an empty allow set", () => {
|
|
2382
|
+
const compiled = compiler({
|
|
2383
|
+
op: "or",
|
|
2384
|
+
conditions: []
|
|
2385
|
+
}, options.target);
|
|
2386
|
+
const closed = options.isFailClosed === void 0 ? compiled === false || compiled === void 0 || compiled === null : options.isFailClosed(compiled);
|
|
2387
|
+
expect(closed).toBe(true);
|
|
2388
|
+
});
|
|
2389
|
+
it("compiles a sqlFunction through its twin", () => {
|
|
2390
|
+
const twin = {
|
|
2391
|
+
op: "eq",
|
|
2392
|
+
field: "authorId",
|
|
2393
|
+
value: "u1"
|
|
2394
|
+
};
|
|
2395
|
+
expect(() => compiler({
|
|
2396
|
+
op: "sqlFunction",
|
|
2397
|
+
name: "job_permitted",
|
|
2398
|
+
args: [{ field: "id" }],
|
|
2399
|
+
twin
|
|
2400
|
+
}, options.target)).not.toThrow();
|
|
2401
|
+
});
|
|
2402
|
+
}
|
|
2403
|
+
//#endregion
|
|
2404
|
+
//#region src/testing/jwt-fixtures.ts
|
|
2405
|
+
const jwtFixtureJwks = { keys: [{
|
|
2406
|
+
crv: "Ed25519",
|
|
2407
|
+
x: "79ab4WR6Eb9LkefWpmh5ZlvjXg7wqVGNMwIEHQqduIQ",
|
|
2408
|
+
kty: "OKP",
|
|
2409
|
+
kid: "2026-09",
|
|
2410
|
+
alg: "Ed25519",
|
|
2411
|
+
use: "sig"
|
|
2412
|
+
}] };
|
|
2413
|
+
const jwtFixtureIssuer = "https://login.example.com";
|
|
2414
|
+
const jwtFixtureAudience = "https://api.example.com";
|
|
2415
|
+
const jwtFixtureTokens = {
|
|
2416
|
+
valid: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6ImF0K2p3dCJ9.eyJzdWIiOiJ1XzEiLCJjbGllbnRfaWQiOiJhcHAiLCJyb2xlcyI6WyJtZW1iZXIiXSwianRpIjoianRpLXZhbGlkIiwiaXNzIjoiaHR0cHM6Ly9sb2dpbi5leGFtcGxlLmNvbSIsImF1ZCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjIwMDAwMDAwMDB9.ENFMmLSyVde-2s4Gdz4iP7srZvU0Xuy_ZLz-OJGHD_-W1lEayEBeSPTnab0L_TBdj8p5BmkFrT-7VE7o20GbAw",
|
|
2417
|
+
expired: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6ImF0K2p3dCJ9.eyJzdWIiOiJ1XzEiLCJpc3MiOiJodHRwczovL2xvZ2luLmV4YW1wbGUuY29tIiwiYXVkIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20iLCJpYXQiOjEwMDAwMDAwMDAsImV4cCI6MTEwMDAwMDAwMH0.9X3d6kH3_2nmrbqMxuoxzFW-wsWJVCe-lR-aQ-MmsGGXlEUBMgQ-4MtAq1UR9lLE0h2b5JrE_GfjzuC04h5xCg",
|
|
2418
|
+
wrongAud: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6ImF0K2p3dCJ9.eyJzdWIiOiJ1XzEiLCJpc3MiOiJodHRwczovL2xvZ2luLmV4YW1wbGUuY29tIiwiYXVkIjoiaHR0cHM6Ly9vdGhlci5leGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoyMDAwMDAwMDAwfQ.PgWdzi56J31OwoMqH4ysHzVKSKUDhi-AtHFUp1ewFzi-qGE-tknzrNKnKhDOncVzj0okb_flyy3vQgfzJwG5AQ",
|
|
2419
|
+
wrongIss: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6ImF0K2p3dCJ9.eyJzdWIiOiJ1XzEiLCJpc3MiOiJodHRwczovL2V2aWwuZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoyMDAwMDAwMDAwfQ.tIcLHLa9JLtKz1gqjRGdr5DFbUzRDs5KsoPsJCxspvbBcsFfRFG2G0XJnd1h4QHyZ_5B4OhY3W_jIIsnOPD9Bg",
|
|
2420
|
+
unknownKid: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoibm9wZSIsInR5cCI6ImF0K2p3dCJ9.eyJzdWIiOiJ1XzEiLCJpc3MiOiJodHRwczovL2xvZ2luLmV4YW1wbGUuY29tIiwiYXVkIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20iLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MjAwMDAwMDAwMH0.walgG5GvpCVBnkMtkdw-epi5c93JqfvAM_N4EWR9Tqhiw_cnzITnlQyoVfihECmV1fqtFrhwM1RCIRJvd38zAQ",
|
|
2421
|
+
none: "eyJhbGciOiJub25lIn0.eyJzdWIiOiJ1XzEiLCJpc3MiOiJodHRwczovL2xvZ2luLmV4YW1wbGUuY29tIiwiYXVkIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20iLCJleHAiOjIwMDAwMDAwMDAsImlhdCI6MTcwMDAwMDAwMH0.",
|
|
2422
|
+
snapshot: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6InBlcm1kb2NrLXNuYXBzaG90K2p3dCJ9.eyJzbmFwc2hvdCI6eyJ2IjoyLCJpc3N1ZWRBdCI6MTcwMDAwMDAwMCwic3ViamVjdCI6eyJwcmluY2lwYWwiOnsiaWQiOiJ1XzEiLCJyb2xlcyI6WyJtZW1iZXIiXX0sImNvbnRleHQiOnt9fSwicm9sZXMiOlsibWVtYmVyIl0sImdyYW50cyI6W10sInRlbmFudHMiOltdfSwic3ViIjoidV8xIiwiaXNzIjoiaHR0cHM6Ly9hcHAuZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwcC5leGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoyMDAwMDAwMDAwLCJqdGkiOiJzbmFwX2ZpeHR1cmUifQ.4itaYctluoCwU-syytUxArtS_FRyNAti2SZxuSH-nKIMr7-Sb3h1Q3xugn_vPR1eupV4fVNIe6dH27_EiyDvCA",
|
|
2423
|
+
approval: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6InBlcm1kb2NrLWFwcHJvdmFsK2p3dCJ9.eyJhcHByb3ZhbCI6eyJ0b2tlbiI6InBkMS5hYmMiLCJwZXJtaXNzaW9uIjoicG9zdC5kZWxldGUiLCJyZXNvdXJjZSI6eyJ0eXBlIjoicG9zdCIsImlkIjoiNDIifSwic3RhdHVzIjoiYXBwcm92ZWQifSwic3ViIjoidV8xIiwiaXNzIjoiaHR0cHM6Ly9hcHAuZXhhbXBsZS5jb20iLCJhdWQiOiJodHRwczovL2FwcC5leGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoyMDAwMDAwMDAwLCJqdGkiOiJhcHJfZml4dHVyZSJ9.UHsoSzQCo68G7ee-EFqdzMLvmqaKlFIMdQKFuqZEM4yUQnJsJJTXbld_aCXcyfN-IdjF9cj2F5EHzsV4nLKbBg",
|
|
2424
|
+
decisions: "eyJhbGciOiJFZDI1NTE5Iiwia2lkIjoiMjAyNi0wOSIsInR5cCI6InBlcm1kb2NrLWRlY2lzaW9ucytqd3QifQ.eyJldmVudHMiOltdLCJpc3MiOiJodHRwczovL2FwcC5leGFtcGxlLmNvbSIsImlhdCI6MTcwMDAwMDAwMCwiZXhwIjoyMDAwMDAwMDAwLCJqdGkiOiJkZWNfZml4dHVyZSJ9.UZhJd72yFGxHOdjYbzcGYUPgYjvLwOPCtHKxEbLu56SdKSKUDCREXmEBI9WaCPKWjlTBnnIKMYwjG8I8RATJCg"
|
|
2425
|
+
};
|
|
2426
|
+
//#endregion
|
|
2427
|
+
//#region src/testing/conformance/credentials.ts
|
|
2428
|
+
function testReplayStore(store) {
|
|
2429
|
+
it("records a jti after remember and reports it as seen", async () => {
|
|
2430
|
+
const jti = "jti-1";
|
|
2431
|
+
expect(await store.seen(jti)).toBe(false);
|
|
2432
|
+
await store.remember(jti);
|
|
2433
|
+
expect(await store.seen(jti)).toBe(true);
|
|
2434
|
+
expect(await store.seen("jti-2")).toBe(false);
|
|
2435
|
+
});
|
|
2436
|
+
it("forgets a jti whose expiresAt has passed", async () => {
|
|
2437
|
+
await store.remember("ttl-jti", Math.floor(Date.now() / 1e3) - 1);
|
|
2438
|
+
expect(await store.seen("ttl-jti")).toBe(false);
|
|
2439
|
+
});
|
|
2440
|
+
it("claims a key once under concurrency and releases it for a retry", async (context) => {
|
|
2441
|
+
if (store.claim === void 0 || store.release === void 0) {
|
|
2442
|
+
context.skip();
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
const expiresAt = Math.floor(Date.now() / 1e3) + 60;
|
|
2446
|
+
const results = await Promise.all([store.claim("claim-key", expiresAt), store.claim("claim-key", expiresAt)]);
|
|
2447
|
+
expect(results.toSorted()).toEqual([false, true]);
|
|
2448
|
+
expect(await store.seen("claim-key")).toBe(true);
|
|
2449
|
+
await store.release("claim-key");
|
|
2450
|
+
expect(await store.claim("claim-key", expiresAt)).toBe(true);
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
function testRevocationFeed(feed) {
|
|
2454
|
+
it("delivers each event to every subscriber until it unsubscribes", async () => {
|
|
2455
|
+
const first = [];
|
|
2456
|
+
const second = [];
|
|
2457
|
+
const stopFirst = feed.subscribe((event) => {
|
|
2458
|
+
first.push(event);
|
|
2456
2459
|
});
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2460
|
+
const stopSecond = feed.subscribe((event) => {
|
|
2461
|
+
second.push(event);
|
|
2462
|
+
});
|
|
2463
|
+
const revoked = {
|
|
2464
|
+
principal: "u-feed",
|
|
2465
|
+
session: "s-feed",
|
|
2466
|
+
kind: "session-revoked"
|
|
2467
|
+
};
|
|
2468
|
+
await feed.revoke(revoked);
|
|
2469
|
+
await vi.waitFor(() => {
|
|
2470
|
+
expect(first).toEqual([revoked]);
|
|
2471
|
+
expect(second).toEqual([revoked]);
|
|
2472
|
+
});
|
|
2473
|
+
stopFirst();
|
|
2474
|
+
const changed = {
|
|
2475
|
+
principal: "u-feed",
|
|
2476
|
+
tenant: "t-feed",
|
|
2477
|
+
kind: "changed"
|
|
2478
|
+
};
|
|
2479
|
+
await feed.revoke(changed);
|
|
2480
|
+
await vi.waitFor(() => {
|
|
2481
|
+
expect(second).toEqual([revoked, changed]);
|
|
2482
|
+
});
|
|
2483
|
+
expect(first).toEqual([revoked]);
|
|
2484
|
+
stopSecond();
|
|
2463
2485
|
});
|
|
2464
|
-
it("
|
|
2465
|
-
const
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2486
|
+
it("keeps delivering when a listener throws", async () => {
|
|
2487
|
+
const seen = [];
|
|
2488
|
+
const stopThrowing = feed.subscribe(() => {
|
|
2489
|
+
throw new Error("listener failed");
|
|
2490
|
+
});
|
|
2491
|
+
const stop = feed.subscribe((event) => {
|
|
2492
|
+
seen.push(event);
|
|
2493
|
+
});
|
|
2494
|
+
const event = {
|
|
2495
|
+
principal: "u-throw",
|
|
2496
|
+
kind: "changed"
|
|
2472
2497
|
};
|
|
2473
|
-
await
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2498
|
+
await feed.revoke(event);
|
|
2499
|
+
await vi.waitFor(() => {
|
|
2500
|
+
expect(seen).toEqual([event]);
|
|
2501
|
+
});
|
|
2502
|
+
stopThrowing();
|
|
2503
|
+
stop();
|
|
2504
|
+
});
|
|
2505
|
+
it("rejects an event without a principal or with an unknown kind", async () => {
|
|
2506
|
+
const seen = [];
|
|
2507
|
+
const stop = feed.subscribe((event) => {
|
|
2508
|
+
seen.push(event);
|
|
2509
|
+
});
|
|
2510
|
+
for (const event of [{
|
|
2511
|
+
principal: "",
|
|
2512
|
+
kind: "changed"
|
|
2513
|
+
}, {
|
|
2514
|
+
principal: "u-bad",
|
|
2515
|
+
kind: "granted"
|
|
2516
|
+
}]) await expect((async () => {
|
|
2517
|
+
await feed.revoke(event);
|
|
2518
|
+
})()).rejects.toThrow(TypeError);
|
|
2519
|
+
expect(seen).toEqual([]);
|
|
2520
|
+
stop();
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
function decodeHeader(token) {
|
|
2524
|
+
const [encoded] = token.split(".");
|
|
2525
|
+
if (encoded === void 0) return {};
|
|
2526
|
+
const padded = encoded.replaceAll("-", "+").replaceAll("_", "/");
|
|
2527
|
+
const pad = padded.length % 4 === 0 ? "" : "=".repeat(4 - padded.length % 4);
|
|
2528
|
+
return JSON.parse(atob(`${padded}${pad}`));
|
|
2529
|
+
}
|
|
2530
|
+
function testTokenVerifier(verifier, options) {
|
|
2531
|
+
const audience = options?.audience ?? "https://api.example.com";
|
|
2532
|
+
const issuer = options?.issuer ?? "https://login.example.com";
|
|
2533
|
+
it("never throws and maps the JWT behaviour table", async () => {
|
|
2534
|
+
const valid = await verifier.verify(jwtFixtureTokens.valid, {
|
|
2535
|
+
audience,
|
|
2536
|
+
issuer
|
|
2537
|
+
});
|
|
2538
|
+
expect(valid.ok).toBe(true);
|
|
2539
|
+
if (valid.ok) {
|
|
2540
|
+
expect(valid.claims.sub).toBe("u_1");
|
|
2541
|
+
expect(valid.header.alg).toBe("Ed25519");
|
|
2542
|
+
}
|
|
2543
|
+
const rows = [
|
|
2544
|
+
{
|
|
2545
|
+
token: jwtFixtureTokens.none,
|
|
2546
|
+
cause: "alg-none"
|
|
2478
2547
|
},
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2548
|
+
{
|
|
2549
|
+
token: jwtFixtureTokens.expired,
|
|
2550
|
+
cause: "expired"
|
|
2551
|
+
},
|
|
2552
|
+
{
|
|
2553
|
+
token: jwtFixtureTokens.wrongAud,
|
|
2554
|
+
cause: "wrong-audience"
|
|
2555
|
+
},
|
|
2556
|
+
{
|
|
2557
|
+
token: jwtFixtureTokens.wrongIss,
|
|
2558
|
+
cause: "wrong-issuer"
|
|
2559
|
+
},
|
|
2560
|
+
{
|
|
2561
|
+
token: jwtFixtureTokens.unknownKid,
|
|
2562
|
+
cause: "unknown-kid"
|
|
2563
|
+
},
|
|
2564
|
+
{
|
|
2565
|
+
token: "not-a-jwt",
|
|
2566
|
+
cause: "malformed"
|
|
2567
|
+
},
|
|
2568
|
+
{
|
|
2569
|
+
token: "a.b.c.d.e",
|
|
2570
|
+
cause: "encrypted-token"
|
|
2571
|
+
}
|
|
2572
|
+
];
|
|
2573
|
+
for (const row of rows) {
|
|
2574
|
+
let result;
|
|
2575
|
+
try {
|
|
2576
|
+
result = await verifier.verify(row.token, {
|
|
2577
|
+
audience,
|
|
2578
|
+
issuer
|
|
2579
|
+
});
|
|
2580
|
+
} catch {
|
|
2581
|
+
throw new Error("TokenVerifier must not throw");
|
|
2582
|
+
}
|
|
2583
|
+
expect(result.ok).toBe(false);
|
|
2584
|
+
if (!result.ok) expect(result.cause).toBe(row.cause);
|
|
2585
|
+
}
|
|
2586
|
+
});
|
|
2587
|
+
}
|
|
2588
|
+
function flipLast(value) {
|
|
2589
|
+
const last = value.at(-1);
|
|
2590
|
+
return `${value.slice(0, -1)}${last === "A" ? "B" : "A"}`;
|
|
2591
|
+
}
|
|
2592
|
+
/**
|
|
2593
|
+
* `key` is a live key the verifier knows, or a function issuing one (called
|
|
2594
|
+
* once). `revoke`, when given, revokes it; the runner then expects the key
|
|
2595
|
+
* to stop verifying.
|
|
2596
|
+
*/
|
|
2597
|
+
function testCredentialVerifier(verifier, options) {
|
|
2598
|
+
let issued;
|
|
2599
|
+
const live = () => {
|
|
2600
|
+
const source = options.key;
|
|
2601
|
+
issued ??= Promise.resolve(typeof source === "string" ? source : source());
|
|
2602
|
+
return issued;
|
|
2603
|
+
};
|
|
2604
|
+
it("verifies the live key to a v1 credential with the key id", async () => {
|
|
2605
|
+
const key = await live();
|
|
2606
|
+
const parts = parseApiKey(key);
|
|
2607
|
+
expect(parts).toBeDefined();
|
|
2608
|
+
const credential = parseCredential(await verifier.verify(key));
|
|
2609
|
+
expect(credential).toBeDefined();
|
|
2610
|
+
expect(credential?.id).toBe(parts?.id);
|
|
2611
|
+
expect(parseCredential(await verifier.verify(key))).toEqual(credential);
|
|
2485
2612
|
});
|
|
2486
|
-
it("
|
|
2487
|
-
const
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
["self-distinct", {
|
|
2498
|
-
by: admin,
|
|
2499
|
-
distinct: true
|
|
2500
|
-
}]
|
|
2613
|
+
it("never throws and answers null for every other key", async () => {
|
|
2614
|
+
const key = await live();
|
|
2615
|
+
const parts = parseApiKey(key);
|
|
2616
|
+
const others = [
|
|
2617
|
+
"",
|
|
2618
|
+
"garbage",
|
|
2619
|
+
"pdk_",
|
|
2620
|
+
`${key}x`,
|
|
2621
|
+
flipLast(key),
|
|
2622
|
+
`pdk_${parts?.id ?? "x"}-other_${parts?.secret ?? ""}`,
|
|
2623
|
+
key.replace(/^pdk_/u, "sk_")
|
|
2501
2624
|
];
|
|
2502
|
-
for (const
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
status: "approved",
|
|
2509
|
-
by: principal
|
|
2510
|
-
}))).rejects.toThrow(/principal/u);
|
|
2511
|
-
expect((await store.get(token))?.status).toBe("pending");
|
|
2512
|
-
}
|
|
2513
|
-
await store.create({
|
|
2514
|
-
...sampleApproval("self-optout"),
|
|
2515
|
-
approvers: {
|
|
2516
|
-
by: admin,
|
|
2517
|
-
distinct: false
|
|
2625
|
+
for (const other of others) {
|
|
2626
|
+
let result;
|
|
2627
|
+
try {
|
|
2628
|
+
result = await verifier.verify(other);
|
|
2629
|
+
} catch {
|
|
2630
|
+
throw new Error("CredentialVerifier must not throw");
|
|
2518
2631
|
}
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
status: "approved",
|
|
2522
|
-
by: principal
|
|
2523
|
-
});
|
|
2524
|
-
expect(resolved.resolvedBy).toBe("u_1");
|
|
2632
|
+
expect(result).toBeNull();
|
|
2633
|
+
}
|
|
2525
2634
|
});
|
|
2526
|
-
it("
|
|
2527
|
-
await
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
});
|
|
2534
|
-
const first = await store.resolve("quorum-token", {
|
|
2535
|
-
status: "approved",
|
|
2536
|
-
by: approver
|
|
2537
|
-
});
|
|
2538
|
-
expect(first.status).toBe("pending");
|
|
2539
|
-
expect(first.approvals).toEqual([{
|
|
2540
|
-
by: "u_9",
|
|
2541
|
-
at: expect.any(String)
|
|
2542
|
-
}]);
|
|
2543
|
-
expect(await store.consume("quorum-token")).toBeNull();
|
|
2544
|
-
await expect(Promise.resolve().then(() => store.resolve("quorum-token", {
|
|
2545
|
-
status: "approved",
|
|
2546
|
-
by: approver
|
|
2547
|
-
}))).rejects.toThrow(/already approved/u);
|
|
2548
|
-
const second = await store.resolve("quorum-token", {
|
|
2549
|
-
status: "approved",
|
|
2550
|
-
by: secondApprover
|
|
2551
|
-
});
|
|
2552
|
-
expect(second.status).toBe("approved");
|
|
2553
|
-
expect(second.approvals?.map((item) => item.by)).toEqual(["u_9", "u_10"]);
|
|
2554
|
-
expect(second.resolvedBy).toBe("u_10");
|
|
2555
|
-
expect(await store.consume("quorum-token")).not.toBeNull();
|
|
2635
|
+
if (verifier.touch !== void 0) it("accepts a touch without changing what the key verifies to", async () => {
|
|
2636
|
+
const key = await live();
|
|
2637
|
+
const before = parseCredential(await verifier.verify(key));
|
|
2638
|
+
const id = parseApiKey(key)?.id ?? "";
|
|
2639
|
+
await verifier.touch?.(id, Math.floor(Date.now() / 1e3));
|
|
2640
|
+
await verifier.touch?.("unknown-id", Math.floor(Date.now() / 1e3));
|
|
2641
|
+
expect(parseCredential(await verifier.verify(key))).toEqual(before);
|
|
2556
2642
|
});
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
}
|
|
2564
|
-
});
|
|
2565
|
-
await expect(Promise.resolve().then(() => store.resolve("vouched-token", {
|
|
2566
|
-
status: "approved",
|
|
2567
|
-
by: {
|
|
2568
|
-
principal: {
|
|
2569
|
-
id: "u_1",
|
|
2570
|
-
roles: []
|
|
2571
|
-
},
|
|
2572
|
-
context: {}
|
|
2573
|
-
},
|
|
2574
|
-
vouched: "app-rule"
|
|
2575
|
-
}))).rejects.toThrow(/principal/u);
|
|
2576
|
-
const resolved = await store.resolve("vouched-token", {
|
|
2577
|
-
status: "approved",
|
|
2578
|
-
by: {
|
|
2579
|
-
principal: {
|
|
2580
|
-
id: "u_outsider",
|
|
2581
|
-
roles: []
|
|
2582
|
-
},
|
|
2583
|
-
context: {}
|
|
2584
|
-
},
|
|
2585
|
-
vouched: "app-rule"
|
|
2643
|
+
if (options.revoke !== void 0) {
|
|
2644
|
+
const revoke = options.revoke;
|
|
2645
|
+
it("stops verifying a revoked key", async () => {
|
|
2646
|
+
const key = await live();
|
|
2647
|
+
await revoke();
|
|
2648
|
+
expect(await verifier.verify(key)).toBeNull();
|
|
2586
2649
|
});
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
...sampleApproval("veto-token"),
|
|
2598
|
-
approvers: {
|
|
2599
|
-
by: admin,
|
|
2600
|
-
quorum: 2
|
|
2601
|
-
}
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
function testTokenSigner(signer, options) {
|
|
2653
|
+
it("emits compact JWS with only alg, kid and typ", async () => {
|
|
2654
|
+
const token = await signer.sign({
|
|
2655
|
+
snapshot: { v: 1 },
|
|
2656
|
+
sub: "u_1"
|
|
2657
|
+
}, {
|
|
2658
|
+
typ: "permdock-snapshot+jwt",
|
|
2659
|
+
audience: "https://app.example.com"
|
|
2602
2660
|
});
|
|
2603
|
-
const
|
|
2604
|
-
|
|
2605
|
-
|
|
2661
|
+
const header = decodeHeader(token);
|
|
2662
|
+
expect(Object.keys(header).toSorted()).toEqual([
|
|
2663
|
+
"alg",
|
|
2664
|
+
"kid",
|
|
2665
|
+
"typ"
|
|
2666
|
+
]);
|
|
2667
|
+
expect(header["typ"]).toBe("permdock-snapshot+jwt");
|
|
2668
|
+
expect(header["alg"]).not.toBe("none");
|
|
2669
|
+
const verified = await options.verifier.verify(token, {
|
|
2670
|
+
typ: "permdock-snapshot+jwt",
|
|
2671
|
+
audience: "https://app.example.com"
|
|
2606
2672
|
});
|
|
2607
|
-
expect(
|
|
2608
|
-
|
|
2673
|
+
expect(verified.ok).toBe(true);
|
|
2674
|
+
if (signer.jwks !== void 0) {
|
|
2675
|
+
const jwks = await signer.jwks();
|
|
2676
|
+
expect(jwks.keys.length).toBeGreaterThan(0);
|
|
2677
|
+
expect(jwks.keys[0]).not.toHaveProperty("d");
|
|
2678
|
+
}
|
|
2609
2679
|
});
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
kind: "role",
|
|
2622
|
-
role: "auditor",
|
|
2623
|
-
scope: "global"
|
|
2624
|
-
}
|
|
2680
|
+
}
|
|
2681
|
+
//#endregion
|
|
2682
|
+
//#region src/testing/conformance/runtime-stores.ts
|
|
2683
|
+
function testLimitStore(store) {
|
|
2684
|
+
it("counts down synchronously and fails closed when exhausted", async () => {
|
|
2685
|
+
const input = {
|
|
2686
|
+
key: "report.export",
|
|
2687
|
+
subjectId: "u_1",
|
|
2688
|
+
count: 2,
|
|
2689
|
+
per: "hour",
|
|
2690
|
+
now: 17e8
|
|
2625
2691
|
};
|
|
2626
|
-
await store.
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2692
|
+
const first = await store.consume(input);
|
|
2693
|
+
expect(first.remaining).toBeGreaterThanOrEqual(0);
|
|
2694
|
+
const peeked = store.remaining(input);
|
|
2695
|
+
if (peeked !== void 0) {
|
|
2696
|
+
expect(typeof peeked.remaining).toBe("number");
|
|
2697
|
+
expect(peeked !== null && typeof peeked === "object" && "then" in peeked && typeof peeked.then === "function").toBe(false);
|
|
2698
|
+
}
|
|
2699
|
+
await store.consume(input);
|
|
2700
|
+
const exhausted = await store.consume(input);
|
|
2701
|
+
expect(exhausted.remaining).toBeLessThan(0);
|
|
2702
|
+
});
|
|
2703
|
+
}
|
|
2704
|
+
function testDecisionSink(sink) {
|
|
2705
|
+
it("accepts batches and never propagates write errors", async () => {
|
|
2706
|
+
await expect(Promise.resolve(sink.write([{
|
|
2707
|
+
type: "membership",
|
|
2708
|
+
at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2709
|
+
source: "app",
|
|
2710
|
+
operation: "changed",
|
|
2711
|
+
principal: { id: "u_1" },
|
|
2712
|
+
roles: {
|
|
2713
|
+
added: ["admin"],
|
|
2714
|
+
removed: ["member"]
|
|
2631
2715
|
}
|
|
2716
|
+
}]))).resolves.toBeUndefined();
|
|
2717
|
+
await expect(Promise.resolve(sink.write([]))).resolves.toBeUndefined();
|
|
2718
|
+
if (sink.flush !== void 0) {
|
|
2719
|
+
await expect(Promise.resolve(sink.flush())).resolves.toBeUndefined();
|
|
2720
|
+
await expect(Promise.resolve(sink.flush())).resolves.toBeUndefined();
|
|
2721
|
+
}
|
|
2722
|
+
});
|
|
2723
|
+
}
|
|
2724
|
+
function testSnapshotSource(source) {
|
|
2725
|
+
it("round-trips a snapshot", async () => {
|
|
2726
|
+
const snapshot = await source.get();
|
|
2727
|
+
expect(snapshot === null || snapshot === void 0).toBe(false);
|
|
2728
|
+
if (typeof snapshot === "object" && snapshot !== null && "v" in snapshot) expect(snapshot.v).toBe(1);
|
|
2729
|
+
if (source.subscribe !== void 0) source.subscribe(() => void 0)();
|
|
2730
|
+
});
|
|
2731
|
+
}
|
|
2732
|
+
function testPolicySource(source, options = {}) {
|
|
2733
|
+
const readCurrent = () => {
|
|
2734
|
+
const document = source.current();
|
|
2735
|
+
expect(document !== null && typeof document === "object" && "then" in document && typeof document.then === "function").toBe(false);
|
|
2736
|
+
return document;
|
|
2737
|
+
};
|
|
2738
|
+
it("returns null or a v1 policy document synchronously", () => {
|
|
2739
|
+
const document = readCurrent();
|
|
2740
|
+
if (document !== null) expect(parsePolicyDocument(document)).toEqual(document);
|
|
2741
|
+
});
|
|
2742
|
+
it("refreshes without rejecting and keeps current() synchronous", async () => {
|
|
2743
|
+
await expect(Promise.resolve(source.refresh())).resolves.toBeUndefined();
|
|
2744
|
+
const document = readCurrent();
|
|
2745
|
+
if (document !== null) expect(document.v).toBe(1);
|
|
2746
|
+
});
|
|
2747
|
+
if (options.policy !== void 0) {
|
|
2748
|
+
const policy = options.policy;
|
|
2749
|
+
it("merges only grants on hostable permissions", () => {
|
|
2750
|
+
const document = readCurrent();
|
|
2751
|
+
const merged = mergeHostedGrants(policy, document);
|
|
2752
|
+
for (const grant of merged.policy.grants) if (grant.hosted !== void 0) expect(policy.hostable).toContain(grant.permission.key);
|
|
2753
|
+
for (const dropped of merged.dropped) expect(dropped.kind).toBe("hosted-grant-dropped");
|
|
2632
2754
|
});
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
//#endregion
|
|
2758
|
+
//#region src/testing/conformance/relations.ts
|
|
2759
|
+
function holderKey(holder) {
|
|
2760
|
+
return "group" in holder ? `${holder.group.resource}:${holder.group.id}#${holder.group.relation}` : holder.principal.id;
|
|
2761
|
+
}
|
|
2762
|
+
/**
|
|
2763
|
+
* A `RelationSource` answers facts only: well-formed chains nearest first and
|
|
2764
|
+
* no longer than `depth`, holders with principal ids or group references and
|
|
2765
|
+
* numeric instants, a `truncated` chain only when more ancestors exist, and
|
|
2766
|
+
* empty answers (never a throw) for an object it does not know. `expect` pins
|
|
2767
|
+
* the chains, holders and links for `objects`, keyed `resource:id`,
|
|
2768
|
+
* `resource:id#relation` and `resource:id>link`; a group holder is written
|
|
2769
|
+
* `resource:id#relation`.
|
|
2770
|
+
*/
|
|
2771
|
+
function testRelationSource(source, options) {
|
|
2772
|
+
it("returns chains nearest first, within depth, and truncated only when more exist", async () => {
|
|
2773
|
+
for (const object of options.objects) {
|
|
2774
|
+
const full = await source.ancestors({
|
|
2775
|
+
resource: object.resource,
|
|
2776
|
+
id: object.id,
|
|
2777
|
+
through: "parent",
|
|
2778
|
+
depth: 32
|
|
2779
|
+
});
|
|
2780
|
+
expect(Array.isArray(full.ancestors)).toBe(true);
|
|
2781
|
+
for (const ancestor of full.ancestors) {
|
|
2782
|
+
expect(typeof ancestor.id).toBe("string");
|
|
2783
|
+
expect(ancestor.id).not.toBe("");
|
|
2643
2784
|
}
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2785
|
+
expect(full.ancestors.length).toBeLessThanOrEqual(32);
|
|
2786
|
+
const expected = options.expect?.ancestors?.[`${object.resource}:${object.id}`];
|
|
2787
|
+
if (expected !== void 0) expect(full.ancestors.map((ancestor) => ancestor.id)).toEqual(expected);
|
|
2788
|
+
if (full.ancestors.length > 1) {
|
|
2789
|
+
const short = await source.ancestors({
|
|
2790
|
+
resource: object.resource,
|
|
2791
|
+
id: object.id,
|
|
2792
|
+
through: "parent",
|
|
2793
|
+
depth: 1
|
|
2794
|
+
});
|
|
2795
|
+
expect(short.ancestors.map((ancestor) => ancestor.id)).toEqual([full.ancestors[0]?.id]);
|
|
2796
|
+
expect(short.truncated).toBe(true);
|
|
2797
|
+
}
|
|
2798
|
+
const none = await source.ancestors({
|
|
2799
|
+
resource: object.resource,
|
|
2800
|
+
id: object.id,
|
|
2801
|
+
through: "parent",
|
|
2802
|
+
depth: 0
|
|
2803
|
+
});
|
|
2804
|
+
expect(none.ancestors).toEqual([]);
|
|
2805
|
+
}
|
|
2651
2806
|
});
|
|
2652
|
-
it("
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2807
|
+
it("returns well-formed holders", async () => {
|
|
2808
|
+
for (const object of options.objects) {
|
|
2809
|
+
if (object.relation === void 0) continue;
|
|
2810
|
+
const holders = await source.related({
|
|
2811
|
+
resource: object.resource,
|
|
2812
|
+
id: object.id,
|
|
2813
|
+
relation: object.relation
|
|
2814
|
+
});
|
|
2815
|
+
expect(Array.isArray(holders)).toBe(true);
|
|
2816
|
+
for (const holder of holders) {
|
|
2817
|
+
if ("group" in holder) for (const part of [
|
|
2818
|
+
holder.group.resource,
|
|
2819
|
+
holder.group.id,
|
|
2820
|
+
holder.group.relation
|
|
2821
|
+
]) {
|
|
2822
|
+
expect(typeof part).toBe("string");
|
|
2823
|
+
expect(part).not.toBe("");
|
|
2824
|
+
}
|
|
2825
|
+
else expect(typeof holder.principal.id).toBe("string");
|
|
2826
|
+
for (const instant of [holder.startsAt, holder.expiresAt]) expect(instant === void 0 || typeof instant === "number" && Number.isFinite(instant)).toBe(true);
|
|
2661
2827
|
}
|
|
2828
|
+
const expected = options.expect?.holders?.[`${object.resource}:${object.id}#${object.relation}`];
|
|
2829
|
+
if (expected !== void 0) expect(holders.map(holderKey).toSorted()).toEqual([...expected].toSorted());
|
|
2830
|
+
}
|
|
2831
|
+
});
|
|
2832
|
+
it("follows each link to exactly one instance", async () => {
|
|
2833
|
+
for (const [key, id] of Object.entries(options.expect?.links ?? {})) {
|
|
2834
|
+
const [object, through] = key.split(">");
|
|
2835
|
+
const [resource, objectId] = (object ?? "").split(":");
|
|
2836
|
+
if (resource === void 0 || objectId === void 0 || through === void 0) throw new Error(`expect.links key "${key}" is not resource:id>link.`);
|
|
2837
|
+
const linked = await source.ancestors({
|
|
2838
|
+
resource,
|
|
2839
|
+
id: objectId,
|
|
2840
|
+
through,
|
|
2841
|
+
depth: 1
|
|
2842
|
+
});
|
|
2843
|
+
expect(linked.ancestors.map((ancestor) => ancestor.id)).toEqual([id]);
|
|
2844
|
+
}
|
|
2845
|
+
});
|
|
2846
|
+
it("reads each object's row as an object, or null when there is none", async () => {
|
|
2847
|
+
if (source.row === void 0) return;
|
|
2848
|
+
const read = (query) => source.row?.(query);
|
|
2849
|
+
for (const object of options.objects) {
|
|
2850
|
+
const row = await read({
|
|
2851
|
+
resource: object.resource,
|
|
2852
|
+
id: object.id
|
|
2853
|
+
});
|
|
2854
|
+
expect(row === null || row === void 0 || typeof row === "object" && !Array.isArray(row)).toBe(true);
|
|
2855
|
+
}
|
|
2856
|
+
const missing = await read({
|
|
2857
|
+
resource: options.objects[0]?.resource ?? "",
|
|
2858
|
+
id: "permdock-missing-object"
|
|
2662
2859
|
});
|
|
2663
|
-
|
|
2664
|
-
status: "approved",
|
|
2665
|
-
by: approver
|
|
2666
|
-
}))).rejects.toThrow(/eligible/u);
|
|
2667
|
-
const first = await store.resolve("stages-token", {
|
|
2668
|
-
status: "approved",
|
|
2669
|
-
by: secondApprover
|
|
2670
|
-
});
|
|
2671
|
-
expect(first.status).toBe("pending");
|
|
2672
|
-
expect(first.approvals).toEqual([{
|
|
2673
|
-
by: "u_10",
|
|
2674
|
-
at: expect.any(String),
|
|
2675
|
-
stage: 0
|
|
2676
|
-
}]);
|
|
2677
|
-
const second = await store.resolve("stages-token", {
|
|
2678
|
-
status: "approved",
|
|
2679
|
-
by: approver
|
|
2680
|
-
});
|
|
2681
|
-
expect(second.status).toBe("approved");
|
|
2682
|
-
expect(second.approvals?.map((item) => item.stage)).toEqual([0, 1]);
|
|
2860
|
+
expect(missing ?? null).toBeNull();
|
|
2683
2861
|
});
|
|
2684
|
-
it("
|
|
2685
|
-
const
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2862
|
+
it("answers an unknown object with nothing instead of throwing", async () => {
|
|
2863
|
+
const first = options.objects[0];
|
|
2864
|
+
if (first === void 0) return;
|
|
2865
|
+
const missing = {
|
|
2866
|
+
resource: first.resource,
|
|
2867
|
+
id: "permdock-missing-object"
|
|
2689
2868
|
};
|
|
2690
|
-
await
|
|
2691
|
-
...
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
status: "approved",
|
|
2700
|
-
by: approver,
|
|
2701
|
-
relations: [JSON.stringify([
|
|
2702
|
-
"post",
|
|
2703
|
-
"manager",
|
|
2704
|
-
null,
|
|
2705
|
-
null
|
|
2706
|
-
])]
|
|
2707
|
-
});
|
|
2708
|
-
expect(resolved.status).toBe("approved");
|
|
2869
|
+
expect((await source.ancestors({
|
|
2870
|
+
...missing,
|
|
2871
|
+
through: "parent",
|
|
2872
|
+
depth: 16
|
|
2873
|
+
})).ancestors).toEqual([]);
|
|
2874
|
+
if (first.relation !== void 0) expect(await source.related({
|
|
2875
|
+
...missing,
|
|
2876
|
+
relation: first.relation
|
|
2877
|
+
})).toEqual([]);
|
|
2709
2878
|
});
|
|
2710
2879
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
expect(valid.header.alg).toBe("Ed25519");
|
|
2730
|
-
}
|
|
2731
|
-
const rows = [
|
|
2732
|
-
{
|
|
2733
|
-
token: jwtFixtureTokens.none,
|
|
2734
|
-
cause: "alg-none"
|
|
2735
|
-
},
|
|
2736
|
-
{
|
|
2737
|
-
token: jwtFixtureTokens.expired,
|
|
2738
|
-
cause: "expired"
|
|
2739
|
-
},
|
|
2740
|
-
{
|
|
2741
|
-
token: jwtFixtureTokens.wrongAud,
|
|
2742
|
-
cause: "wrong-audience"
|
|
2743
|
-
},
|
|
2744
|
-
{
|
|
2745
|
-
token: jwtFixtureTokens.wrongIss,
|
|
2746
|
-
cause: "wrong-issuer"
|
|
2747
|
-
},
|
|
2748
|
-
{
|
|
2749
|
-
token: jwtFixtureTokens.unknownKid,
|
|
2750
|
-
cause: "unknown-kid"
|
|
2751
|
-
},
|
|
2752
|
-
{
|
|
2753
|
-
token: "not-a-jwt",
|
|
2754
|
-
cause: "malformed"
|
|
2755
|
-
},
|
|
2756
|
-
{
|
|
2757
|
-
token: "a.b.c.d.e",
|
|
2758
|
-
cause: "encrypted-token"
|
|
2759
|
-
}
|
|
2760
|
-
];
|
|
2761
|
-
for (const row of rows) {
|
|
2762
|
-
let result;
|
|
2880
|
+
//#endregion
|
|
2881
|
+
//#region src/testing/conformance/identity.ts
|
|
2882
|
+
function testSubjectResolver(resolver, options) {
|
|
2883
|
+
it("never throws and fails closed to anonymous", async () => {
|
|
2884
|
+
let result;
|
|
2885
|
+
try {
|
|
2886
|
+
result = await resolver(options.invalid);
|
|
2887
|
+
} catch {
|
|
2888
|
+
throw new Error("SubjectResolver must not throw");
|
|
2889
|
+
}
|
|
2890
|
+
expect(result.principal).toBeNull();
|
|
2891
|
+
});
|
|
2892
|
+
}
|
|
2893
|
+
function testMembershipSource(source, options) {
|
|
2894
|
+
const query = options.tenant === void 0 ? {} : { tenant: options.tenant };
|
|
2895
|
+
it("returns well-formed memberships and fails closed on throw", async () => {
|
|
2896
|
+
for (const principal of options.principals) {
|
|
2897
|
+
let memberships = [];
|
|
2763
2898
|
try {
|
|
2764
|
-
|
|
2765
|
-
audience,
|
|
2766
|
-
issuer
|
|
2767
|
-
});
|
|
2899
|
+
memberships = await source.membershipsFor(principal, query);
|
|
2768
2900
|
} catch {
|
|
2769
|
-
|
|
2901
|
+
memberships = [];
|
|
2770
2902
|
}
|
|
2771
|
-
|
|
2772
|
-
|
|
2903
|
+
for (const membership of memberships) {
|
|
2904
|
+
const shapes = [
|
|
2905
|
+
membership.scope !== void 0,
|
|
2906
|
+
membership.tenant !== void 0 || membership.team !== void 0,
|
|
2907
|
+
membership.on !== void 0
|
|
2908
|
+
].filter(Boolean);
|
|
2909
|
+
expect(shapes.length).toBe(1);
|
|
2910
|
+
expect(Array.isArray(membership.roles)).toBe(true);
|
|
2911
|
+
if (options.policy !== void 0) expect(normalizeMemberships([membership], scopeList(options.policy.scopes))).toHaveLength(1);
|
|
2912
|
+
}
|
|
2913
|
+
const expected = options.expect?.[principal.id];
|
|
2914
|
+
if (expected !== void 0) expect(memberships).toEqual(expected);
|
|
2773
2915
|
}
|
|
2774
2916
|
});
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2917
|
+
if (source.list !== void 0) it("lists every member of an instance it returns a membership for", async () => {
|
|
2918
|
+
for (const principal of options.principals) {
|
|
2919
|
+
let memberships = [];
|
|
2920
|
+
try {
|
|
2921
|
+
memberships = await source.membershipsFor(principal, query);
|
|
2922
|
+
} catch {
|
|
2923
|
+
continue;
|
|
2924
|
+
}
|
|
2925
|
+
for (const membership of memberships) {
|
|
2926
|
+
if (membership.scope === void 0 || membership.id === void 0) continue;
|
|
2927
|
+
const members = await source.list?.({
|
|
2928
|
+
scope: membership.scope,
|
|
2929
|
+
id: membership.id
|
|
2930
|
+
}) ?? [];
|
|
2931
|
+
expect(members.some((entry) => entry.principal.id === principal.id && entry.membership.scope === membership.scope && entry.membership.id === membership.id)).toBe(true);
|
|
2786
2932
|
}
|
|
2787
|
-
expect(JSON.parse(JSON.stringify(settings))).toEqual(settings);
|
|
2788
2933
|
}
|
|
2789
|
-
expect(await source.settingsFor(options.unknown ?? "__permdock_unknown__")).toBeUndefined();
|
|
2790
2934
|
});
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
* entry that does not would deny every call).
|
|
2796
|
-
*/
|
|
2797
|
-
function testApprovalPolicySource(source, options) {
|
|
2798
|
-
it("answers per tenant with plain entries that load against the policy", async () => {
|
|
2799
|
-
const entries = await source.approvalPoliciesFor({ tenants: [options.tenant] });
|
|
2800
|
-
expect(Array.isArray(entries)).toBe(true);
|
|
2801
|
-
for (const entry of entries) {
|
|
2802
|
-
expect(JSON.parse(JSON.stringify(entry))).toEqual(entry);
|
|
2803
|
-
expect([void 0, options.tenant]).toContain(entry.tenant);
|
|
2935
|
+
if (source.version !== void 0) it("reports a finite version or none", async () => {
|
|
2936
|
+
for (const principal of options.principals) {
|
|
2937
|
+
const version = await source.version?.({ id: principal.id });
|
|
2938
|
+
expect(version === void 0 || typeof version === "number" && Number.isFinite(version)).toBe(true);
|
|
2804
2939
|
}
|
|
2805
|
-
const loaded = await approvalPoliciesFor(options.policy, source, [options.tenant], []);
|
|
2806
|
-
expect(loaded).not.toBe("failed");
|
|
2807
|
-
const other = await source.approvalPoliciesFor({ tenants: [options.unknown ?? "__permdock_unknown__"] });
|
|
2808
|
-
expect(other.filter((entry) => entry.tenant !== void 0)).toEqual([]);
|
|
2809
2940
|
});
|
|
2810
2941
|
}
|
|
2811
|
-
function
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
* to stop verifying.
|
|
2819
|
-
*/
|
|
2820
|
-
function testCredentialVerifier(verifier, options) {
|
|
2821
|
-
let issued;
|
|
2822
|
-
const live = () => {
|
|
2823
|
-
const source = options.key;
|
|
2824
|
-
issued ??= Promise.resolve(typeof source === "string" ? source : source());
|
|
2825
|
-
return issued;
|
|
2826
|
-
};
|
|
2827
|
-
it("verifies the live key to a v1 credential with the key id", async () => {
|
|
2828
|
-
const key = await live();
|
|
2829
|
-
const parts = parseApiKey(key);
|
|
2830
|
-
expect(parts).toBeDefined();
|
|
2831
|
-
const credential = parseCredential(await verifier.verify(key));
|
|
2832
|
-
expect(credential).toBeDefined();
|
|
2833
|
-
expect(credential?.id).toBe(parts?.id);
|
|
2834
|
-
expect(parseCredential(await verifier.verify(key))).toEqual(credential);
|
|
2942
|
+
function testEntitlementSource(source, options) {
|
|
2943
|
+
it("returns plan names for a tenant and none without one", async () => {
|
|
2944
|
+
const found = await source.entitlementsFor(options.principal, { tenant: options.tenant });
|
|
2945
|
+
expect(Array.isArray(found)).toBe(true);
|
|
2946
|
+
for (const name of found) expect(typeof name).toBe("string");
|
|
2947
|
+
if (options.expect !== void 0) expect([...found].toSorted()).toEqual([...options.expect].toSorted());
|
|
2948
|
+
expect(await source.entitlementsFor(options.principal, {})).toEqual([]);
|
|
2835
2949
|
});
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
const
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
let result;
|
|
2850
|
-
try {
|
|
2851
|
-
result = await verifier.verify(other);
|
|
2852
|
-
} catch {
|
|
2853
|
-
throw new Error("CredentialVerifier must not throw");
|
|
2854
|
-
}
|
|
2855
|
-
expect(result).toBeNull();
|
|
2950
|
+
}
|
|
2951
|
+
function testRoleSource(source, options) {
|
|
2952
|
+
it("only resolves declared role names", async () => {
|
|
2953
|
+
const declared = options.declared.map((item) => typeof item === "string" ? item : item.key);
|
|
2954
|
+
const roles = await source.rolesFor(options.tenant);
|
|
2955
|
+
for (const role of roles) {
|
|
2956
|
+
expect(role.tenant).toBe(options.tenant);
|
|
2957
|
+
for (const included of role.includes ?? []) expect(declared).toContain(included);
|
|
2958
|
+
if (options.policy !== void 0) expect(validateCustomRole(options.policy, role).dropped).toEqual([]);
|
|
2959
|
+
}
|
|
2960
|
+
if (source.assignable !== void 0) {
|
|
2961
|
+
const assignable = await source.assignable(options.tenant);
|
|
2962
|
+
for (const name of assignable) expect(declared).toContain(name);
|
|
2856
2963
|
}
|
|
2857
2964
|
});
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
const
|
|
2861
|
-
|
|
2862
|
-
await verifier.touch?.(id, Math.floor(Date.now() / 1e3));
|
|
2863
|
-
await verifier.touch?.("unknown-id", Math.floor(Date.now() / 1e3));
|
|
2864
|
-
expect(parseCredential(await verifier.verify(key))).toEqual(before);
|
|
2965
|
+
const every = options.every;
|
|
2966
|
+
if (every !== void 0) it("returns every custom role of the tenant, held or not", async () => {
|
|
2967
|
+
const roles = await source.rolesFor(options.tenant, { held: [] });
|
|
2968
|
+
expect([...new Set(roles.map((role) => role.name))].toSorted()).toEqual([...new Set(every)].toSorted());
|
|
2865
2969
|
});
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2970
|
+
}
|
|
2971
|
+
function testDirectoryStore(store, options) {
|
|
2972
|
+
const [home, other] = options.tenants;
|
|
2973
|
+
it("round-trips users and groups, isolates tenants, and drops inactive memberships", async () => {
|
|
2974
|
+
const created = await store.putUser(home, {
|
|
2975
|
+
id: "",
|
|
2976
|
+
userName: "ada",
|
|
2977
|
+
externalId: "00u1",
|
|
2978
|
+
active: true,
|
|
2979
|
+
meta: {
|
|
2980
|
+
created: "",
|
|
2981
|
+
lastModified: ""
|
|
2982
|
+
}
|
|
2983
|
+
});
|
|
2984
|
+
expect(created.id).not.toBe("");
|
|
2985
|
+
expect(await store.getUser(home, created.id)).toMatchObject({
|
|
2986
|
+
userName: "ada",
|
|
2987
|
+
externalId: "00u1"
|
|
2988
|
+
});
|
|
2989
|
+
expect(await store.getUser(other, created.id)).toBeNull();
|
|
2990
|
+
await expect(store.putUser(home, {
|
|
2991
|
+
id: "",
|
|
2992
|
+
userName: "ada",
|
|
2993
|
+
active: true,
|
|
2994
|
+
meta: {
|
|
2995
|
+
created: "",
|
|
2996
|
+
lastModified: ""
|
|
2997
|
+
}
|
|
2998
|
+
})).rejects.toThrow(/userName/);
|
|
2999
|
+
const group = await store.putGroup(home, {
|
|
3000
|
+
id: "g_editors",
|
|
3001
|
+
displayName: "Editors",
|
|
3002
|
+
members: [{ value: created.id }],
|
|
3003
|
+
roles: ["editor"],
|
|
3004
|
+
meta: {
|
|
3005
|
+
created: "",
|
|
3006
|
+
lastModified: ""
|
|
3007
|
+
}
|
|
3008
|
+
});
|
|
3009
|
+
expect(await store.groupsFor(home, created.id)).toEqual([expect.objectContaining({
|
|
3010
|
+
id: group.id,
|
|
3011
|
+
displayName: "Editors"
|
|
3012
|
+
})]);
|
|
3013
|
+
expect(await store.groupsFor(other, created.id)).toEqual([]);
|
|
3014
|
+
const patched = await store.patchUser(home, created.id, [{
|
|
3015
|
+
op: "replace",
|
|
3016
|
+
path: "active",
|
|
3017
|
+
value: false
|
|
3018
|
+
}]);
|
|
3019
|
+
expect(patched.active).toBe(false);
|
|
3020
|
+
const source = directoryMembershipSource(store, { assignable: ["editor"] });
|
|
3021
|
+
expect(await source.membershipsFor({ id: "00u1" }, { tenant: home })).toEqual([]);
|
|
3022
|
+
await store.patchUser(home, created.id, [{
|
|
3023
|
+
op: "replace",
|
|
3024
|
+
path: "active",
|
|
3025
|
+
value: true
|
|
3026
|
+
}]);
|
|
3027
|
+
expect(await source.membershipsFor({ id: "00u1" }, { tenant: home })).toEqual([{
|
|
3028
|
+
tenant: home,
|
|
3029
|
+
roles: ["editor"],
|
|
3030
|
+
via: `group:${group.id}`,
|
|
3031
|
+
managedBy: "idp"
|
|
3032
|
+
}]);
|
|
3033
|
+
await store.patchGroup(home, group.id, [{
|
|
3034
|
+
op: "remove",
|
|
3035
|
+
path: "members",
|
|
3036
|
+
value: [{ value: created.id }]
|
|
3037
|
+
}]);
|
|
3038
|
+
expect(await store.groupsFor(home, created.id)).toEqual([]);
|
|
3039
|
+
});
|
|
3040
|
+
it("ends sessions when scimHandler deactivates a user in this store", async () => {
|
|
3041
|
+
const feed = memoryRevocationFeed();
|
|
3042
|
+
const seen = [];
|
|
3043
|
+
feed.subscribe((event) => {
|
|
3044
|
+
seen.push(event);
|
|
3045
|
+
});
|
|
3046
|
+
const kinds = [];
|
|
3047
|
+
const token = "conformance-scim-token";
|
|
3048
|
+
const handle = scimHandler({
|
|
3049
|
+
store,
|
|
3050
|
+
tenant: home,
|
|
3051
|
+
token: {
|
|
3052
|
+
hash: "sha256",
|
|
3053
|
+
lookup: () => sha256Hex(token)
|
|
3054
|
+
},
|
|
3055
|
+
revocations: feed,
|
|
3056
|
+
onChange: (change) => {
|
|
3057
|
+
kinds.push(change.kind);
|
|
3058
|
+
}
|
|
3059
|
+
});
|
|
3060
|
+
const call = (path, method, body) => handle(new Request(`https://app.example.com/scim/v2${path}`, {
|
|
3061
|
+
method,
|
|
3062
|
+
headers: {
|
|
3063
|
+
authorization: `Bearer ${token}`,
|
|
3064
|
+
"content-type": "application/scim+json"
|
|
3065
|
+
},
|
|
3066
|
+
body: JSON.stringify(body)
|
|
3067
|
+
}));
|
|
3068
|
+
const created = await call("/Users", "POST", {
|
|
3069
|
+
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
|
|
3070
|
+
userName: "grace",
|
|
3071
|
+
active: true
|
|
2872
3072
|
});
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
3073
|
+
expect(created.status).toBe(201);
|
|
3074
|
+
const id = String((await created.json()).id);
|
|
3075
|
+
seen.length = 0;
|
|
3076
|
+
const patched = await call(`/Users/${id}`, "PATCH", {
|
|
3077
|
+
schemas: ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
|
|
3078
|
+
Operations: [{
|
|
3079
|
+
op: "replace",
|
|
3080
|
+
path: "active",
|
|
3081
|
+
value: false
|
|
3082
|
+
}]
|
|
2883
3083
|
});
|
|
2884
|
-
|
|
2885
|
-
expect(
|
|
2886
|
-
"
|
|
2887
|
-
|
|
2888
|
-
"
|
|
2889
|
-
]);
|
|
2890
|
-
expect(header["typ"]).toBe("permdock-snapshot+jwt");
|
|
2891
|
-
expect(header["alg"]).not.toBe("none");
|
|
2892
|
-
const verified = await options.verifier.verify(token, {
|
|
2893
|
-
typ: "permdock-snapshot+jwt",
|
|
2894
|
-
audience: "https://app.example.com"
|
|
3084
|
+
expect(patched.status).toBe(200);
|
|
3085
|
+
expect(seen).toContainEqual({
|
|
3086
|
+
principal: "grace",
|
|
3087
|
+
tenant: home,
|
|
3088
|
+
kind: "session-revoked"
|
|
2895
3089
|
});
|
|
2896
|
-
expect(
|
|
2897
|
-
|
|
2898
|
-
const jwks = await signer.jwks();
|
|
2899
|
-
expect(jwks.keys.length).toBeGreaterThan(0);
|
|
2900
|
-
expect(jwks.keys[0]).not.toHaveProperty("d");
|
|
2901
|
-
}
|
|
3090
|
+
expect(seen.every((event) => event.kind === "session-revoked")).toBe(true);
|
|
3091
|
+
expect(kinds).toEqual(["changed", "session-revoked"]);
|
|
2902
3092
|
});
|
|
2903
3093
|
}
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
score: 10,
|
|
2917
|
-
title: "plan_b",
|
|
2918
|
-
archived: true,
|
|
2919
|
-
note: "kept"
|
|
2920
|
-
},
|
|
2921
|
-
{
|
|
2922
|
-
id: "r3",
|
|
2923
|
-
authorId: null,
|
|
2924
|
-
score: null,
|
|
2925
|
-
title: null,
|
|
2926
|
-
archived: null,
|
|
2927
|
-
note: null
|
|
2928
|
-
}
|
|
2929
|
-
];
|
|
2930
|
-
const WHERE_CASES = [
|
|
2931
|
-
["eq", {
|
|
2932
|
-
op: "eq",
|
|
2933
|
-
field: "authorId",
|
|
2934
|
-
value: "u1"
|
|
2935
|
-
}],
|
|
2936
|
-
["ne", {
|
|
2937
|
-
op: "ne",
|
|
2938
|
-
field: "authorId",
|
|
2939
|
-
value: "u1"
|
|
2940
|
-
}],
|
|
2941
|
-
["eq on a boolean", {
|
|
2942
|
-
op: "eq",
|
|
2943
|
-
field: "archived",
|
|
2944
|
-
value: false
|
|
2945
|
-
}],
|
|
2946
|
-
["gt", {
|
|
2947
|
-
op: "gt",
|
|
2948
|
-
field: "score",
|
|
2949
|
-
value: 5
|
|
2950
|
-
}],
|
|
2951
|
-
["gte", {
|
|
2952
|
-
op: "gte",
|
|
2953
|
-
field: "score",
|
|
2954
|
-
value: 5
|
|
2955
|
-
}],
|
|
2956
|
-
["lt", {
|
|
2957
|
-
op: "lt",
|
|
2958
|
-
field: "score",
|
|
2959
|
-
value: 10
|
|
2960
|
-
}],
|
|
2961
|
-
["lte", {
|
|
2962
|
-
op: "lte",
|
|
2963
|
-
field: "score",
|
|
2964
|
-
value: 10
|
|
2965
|
-
}],
|
|
2966
|
-
["contains a literal %", {
|
|
2967
|
-
op: "contains",
|
|
2968
|
-
field: "title",
|
|
2969
|
-
value: "100%"
|
|
2970
|
-
}],
|
|
2971
|
-
["contains a literal _", {
|
|
2972
|
-
op: "contains",
|
|
2973
|
-
field: "title",
|
|
2974
|
-
value: "n_b"
|
|
2975
|
-
}],
|
|
2976
|
-
["in", {
|
|
2977
|
-
op: "in",
|
|
2978
|
-
field: "authorId",
|
|
2979
|
-
value: ["u1", "u2"]
|
|
2980
|
-
}],
|
|
2981
|
-
["notIn", {
|
|
2982
|
-
op: "notIn",
|
|
2983
|
-
field: "authorId",
|
|
2984
|
-
value: ["u1"]
|
|
2985
|
-
}],
|
|
2986
|
-
["isNull", {
|
|
2987
|
-
op: "isNull",
|
|
2988
|
-
field: "note",
|
|
2989
|
-
value: true
|
|
2990
|
-
}],
|
|
2991
|
-
["is not null", {
|
|
2992
|
-
op: "isNull",
|
|
2993
|
-
field: "note",
|
|
2994
|
-
value: false
|
|
2995
|
-
}],
|
|
2996
|
-
["and", {
|
|
2997
|
-
op: "and",
|
|
2998
|
-
conditions: [{
|
|
2999
|
-
op: "eq",
|
|
3000
|
-
field: "authorId",
|
|
3001
|
-
value: "u1"
|
|
3002
|
-
}, {
|
|
3003
|
-
op: "lt",
|
|
3004
|
-
field: "score",
|
|
3005
|
-
value: 10
|
|
3006
|
-
}]
|
|
3007
|
-
}],
|
|
3008
|
-
["or", {
|
|
3009
|
-
op: "or",
|
|
3010
|
-
conditions: [{
|
|
3011
|
-
op: "eq",
|
|
3012
|
-
field: "authorId",
|
|
3013
|
-
value: "u1"
|
|
3014
|
-
}, {
|
|
3015
|
-
op: "eq",
|
|
3016
|
-
field: "archived",
|
|
3017
|
-
value: true
|
|
3018
|
-
}]
|
|
3019
|
-
}],
|
|
3020
|
-
["not over a NULL field", {
|
|
3021
|
-
op: "not",
|
|
3022
|
-
condition: {
|
|
3023
|
-
op: "eq",
|
|
3024
|
-
field: "authorId",
|
|
3025
|
-
value: "u1"
|
|
3026
|
-
}
|
|
3027
|
-
}],
|
|
3028
|
-
["not over and", {
|
|
3029
|
-
op: "not",
|
|
3030
|
-
condition: {
|
|
3031
|
-
op: "and",
|
|
3032
|
-
conditions: [{
|
|
3033
|
-
op: "eq",
|
|
3034
|
-
field: "authorId",
|
|
3035
|
-
value: "u2"
|
|
3036
|
-
}, {
|
|
3037
|
-
op: "gt",
|
|
3038
|
-
field: "score",
|
|
3039
|
-
value: 5
|
|
3040
|
-
}]
|
|
3041
|
-
}
|
|
3042
|
-
}],
|
|
3043
|
-
["and of nothing", {
|
|
3044
|
-
op: "and",
|
|
3045
|
-
conditions: []
|
|
3046
|
-
}],
|
|
3047
|
-
["not of the empty allow set", {
|
|
3048
|
-
op: "not",
|
|
3049
|
-
condition: {
|
|
3050
|
-
op: "or",
|
|
3051
|
-
conditions: []
|
|
3094
|
+
/** Settings for `tenant`, or `undefined`; a thrown source fails the runner. */
|
|
3095
|
+
function testSettingsSource(source, options) {
|
|
3096
|
+
it("answers per tenant with plain settings and nothing for an unknown tenant", async () => {
|
|
3097
|
+
const settings = await source.settingsFor(options.tenant);
|
|
3098
|
+
if (settings !== void 0) {
|
|
3099
|
+
expect(typeof settings).toBe("object");
|
|
3100
|
+
const credentials = settings.credentials;
|
|
3101
|
+
if (credentials !== void 0) {
|
|
3102
|
+
expect(credentials.maxTtl === void 0 || Number.isFinite(credentials.maxTtl) && credentials.maxTtl >= 0).toBe(true);
|
|
3103
|
+
expect(credentials.kinds === void 0 || credentials.kinds.every((kind) => kind === "user" || kind === "service")).toBe(true);
|
|
3104
|
+
}
|
|
3105
|
+
expect(JSON.parse(JSON.stringify(settings))).toEqual(settings);
|
|
3052
3106
|
}
|
|
3053
|
-
|
|
3054
|
-
];
|
|
3055
|
-
const WHERE_SUBJECT = {
|
|
3056
|
-
principal: null,
|
|
3057
|
-
context: {}
|
|
3058
|
-
};
|
|
3059
|
-
/**
|
|
3060
|
-
* `matches` answers whether the compiled output selects `row`, for example
|
|
3061
|
-
* by running it against a table holding only that row. With it, every case
|
|
3062
|
-
* must select the rows the in-memory evaluator keeps, including rows whose
|
|
3063
|
-
* fields are NULL.
|
|
3064
|
-
*/
|
|
3065
|
-
function testWhereCompiler(compiler, options) {
|
|
3066
|
-
const { matches } = options;
|
|
3067
|
-
it.skipIf(matches === void 0).each(WHERE_CASES)("selects the rows the evaluator keeps: %s", async (_name, condition) => {
|
|
3068
|
-
if (matches === void 0) return;
|
|
3069
|
-
const compiled = compiler(condition, options.target);
|
|
3070
|
-
const selected = [];
|
|
3071
|
-
for (const row of WHERE_ROWS) if (await matches(compiled, row)) selected.push(row["id"]);
|
|
3072
|
-
expect(selected).toEqual(WHERE_ROWS.filter((row) => evaluateCondition(condition, row, WHERE_SUBJECT)).map((row) => row["id"]));
|
|
3073
|
-
});
|
|
3074
|
-
it("fails closed on an empty allow set", () => {
|
|
3075
|
-
const compiled = compiler({
|
|
3076
|
-
op: "or",
|
|
3077
|
-
conditions: []
|
|
3078
|
-
}, options.target);
|
|
3079
|
-
const closed = options.isFailClosed === void 0 ? compiled === false || compiled === void 0 || compiled === null : options.isFailClosed(compiled);
|
|
3080
|
-
expect(closed).toBe(true);
|
|
3081
|
-
});
|
|
3082
|
-
it("compiles a sqlFunction through its twin", () => {
|
|
3083
|
-
const twin = {
|
|
3084
|
-
op: "eq",
|
|
3085
|
-
field: "authorId",
|
|
3086
|
-
value: "u1"
|
|
3087
|
-
};
|
|
3088
|
-
expect(() => compiler({
|
|
3089
|
-
op: "sqlFunction",
|
|
3090
|
-
name: "job_permitted",
|
|
3091
|
-
args: [{ field: "id" }],
|
|
3092
|
-
twin
|
|
3093
|
-
}, options.target)).not.toThrow();
|
|
3107
|
+
expect(await source.settingsFor(options.unknown ?? "__permdock_unknown__")).toBeUndefined();
|
|
3094
3108
|
});
|
|
3095
3109
|
}
|
|
3096
3110
|
//#endregion
|