{"_id":"@alexify/kerberos","_rev":"8-c50fb82ee5b53f427d10b906d2e53d8e","name":"@alexify/kerberos","dist-tags":{"latest":"4.2.0"},"versions":{"1.0.0":{"name":"@alexify/kerberos","version":"1.0.0","keywords":["kerberos","authorization","authentication","security","auth","policy","access control","cerbos","framework","schema","web"],"author":{"name":"Alex Dolid","email":"dolid.sasha@gmail.com"},"license":"MIT","_id":"@alexify/kerberos@1.0.0","maintainers":[{"name":"alex-dolid","email":"dolid.sasha@gmail.com"}],"homepage":"https://alexis.dev","bugs":{"url":"https://github.com/Alexis-Technologies/kerberos/issues"},"dist":{"shasum":"c242742d66e2c260d9b763f8f0f9fef142331968","tarball":"https://registry.npmjs.org/@alexify/kerberos/-/kerberos-1.0.0.tgz","fileCount":53,"integrity":"sha512-pM76MJahJrAML+asAZ6dnfl04mGLJUVKtIAuM9co6hHahA1aaB6+BEvxy5ZbK4oC0M0N9n3Yb35ovd9tYUT2pg==","signatures":[{"sig":"MEUCID7XVKEBXnvZ1gWISPWLbfRDPxmxqrvO+sqsfPCJxjvTAiEAv1nWme/QWTboH/Q4YMxmbucvbTte8PjwAn4wayviww4=","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}],"unpackedSize":91100},"main":"index.js","_from":"file:alexify-kerberos-1.0.0.tgz","types":"index.d.ts","scripts":{"lint":"eslint src -c eslint.config.js","test":"node --test test/*.test.js"},"_npmUser":{"name":"alex-dolid","email":"dolid.sasha@gmail.com"},"_resolved":"/private/var/folders/9z/hn3k1glj0wz37h2hr379hr600000gn/T/c137477cae3275bafe2efcbbcb0054f7/alexify-kerberos-1.0.0.tgz","_integrity":"sha512-pM76MJahJrAML+asAZ6dnfl04mGLJUVKtIAuM9co6hHahA1aaB6+BEvxy5ZbK4oC0M0N9n3Yb35ovd9tYUT2pg==","pre-commit":["lint","test"],"repository":{"url":"git+https://github.com/Alexis-Technologies/kerberos.git","type":"git"},"_npmVersion":"10.2.4","description":"Fast and low overhead authorization framework for JavaScript runtime","directories":{"test":"test"},"_nodeVersion":"18.19.1","dependencies":{"zod":"^3.23.8"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.14.0","globals":"^15.11.0","prettier":"^3.3.3","@eslint/js":"^9.13.0","neostandard":"^0.11.7","eslint-plugin-jest":"^28.8.3","eslint-plugin-node":"^11.1.0","eslint-plugin-import":"^2.31.0","eslint-plugin-promise":"^7.1.0","eslint-plugin-unicorn":"^56.0.0","eslint-config-prettier":"^9.1.0","eslint-plugin-prettier":"^5.2.1","@stylistic/eslint-plugin-js":"^2.10.1"},"_npmOperationalInternal":{"tmp":"tmp/kerberos_1.0.0_1733659162588_0.6076064304760316","host":"s3://npm-registry-packages"}},"2.0.0":{"name":"@alexify/kerberos","version":"2.0.0","keywords":["kerberos","authorization","authentication","security","auth","policy","access control","cerbos","framework","schema","web"],"author":"Alex Dolid <dolid.sasha@gmail.com>","license":"MIT","_id":"@alexify/kerberos@2.0.0","maintainers":[{"name":"alex-dolid","email":"dolid.sasha@gmail.com"}],"homepage":"https://alexis.dev","bugs":{"url":"https://github.com/Alexis-Technologies/kerberos/issues"},"dist":{"shasum":"2e98a45a75556018db6062a0547127e370f9215c","tarball":"https://registry.npmjs.org/@alexify/kerberos/-/kerberos-2.0.0.tgz","fileCount":63,"integrity":"sha512-BAASS+4qQRe4tlhhjN1wRAEVAGRx22+KNzZsvhIh/uYJc38nCzKMRN4lvhOBCREyOLYGVI4vdB8R+KDhPj6DzQ==","signatures":[{"sig":"MEUCIQDWLNouTO5jjsjsccUByNLNYK2xltJSA4PwYmb7PFOSTAIgKWFHGcmDKifJEYXoLSJ8SA20wJJ8Cup9pwqCLjqfwJE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":251946},"main":"index.js","types":"index.d.ts","exports":{".":{"types":"./index.d.ts","default":"./index.js"},"./tests":{"types":"./tests.d.ts","default":"./tests.js"}},"scripts":{"lint":"eslint src -c eslint.config.js","test":"node --test test/*.test.js"},"_npmUser":{"name":"alex-dolid","email":"dolid.sasha@gmail.com"},"pre-commit":["lint","test"],"repository":{"url":"git+https://github.com/Alexis-Technologies/kerberos.git","type":"git"},"description":"Fast and low overhead authorization framework for JavaScript runtime","directories":{"test":"test"},"_nodeVersion":"24.14.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"ajv":"^8.18.0","zod":"^4.1.1","jsep":"^1.4.0","keyv":"^5.6.0","pino":"^10.3.1","eslint":"^9.14.0","globals":"^15.11.0","prettier":"^3.3.3","@eslint/js":"^9.13.0","neostandard":"^0.11.7","@jsep-plugin/new":"^1.0.4","@sinclair/typebox":"^0.34.48","eslint-plugin-jest":"^28.8.3","eslint-plugin-node":"^11.1.0","@jsep-plugin/object":"^1.2.2","@jsep-plugin/ternary":"^1.1.4","eslint-plugin-import":"^2.31.0","eslint-plugin-promise":"^7.1.0","eslint-plugin-unicorn":"^56.0.0","eslint-config-prettier":"^9.1.0","eslint-plugin-prettier":"^5.2.1","@stylistic/eslint-plugin-js":"^2.10.1"},"_npmOperationalInternal":{"tmp":"tmp/kerberos_2.0.0_1780254346905_0.39574806469830537","host":"s3://npm-registry-packages-npm-production"}},"2.0.1":{"name":"@alexify/kerberos","version":"2.0.1","keywords":["kerberos","authorization","authentication","security","auth","policy","access control","cerbos","framework","schema","web"],"author":"Alex Dolid <dolid.sasha@gmail.com>","license":"MIT","_id":"@alexify/kerberos@2.0.1","maintainers":[{"name":"alex-dolid","email":"dolid.sasha@gmail.com"}],"homepage":"https://alexis.dev","bugs":{"url":"https://github.com/Alexis-Technologies/kerberos/issues"},"tsd":{"directory":"test"},"dist":{"shasum":"854395182e714eaede2b9ae26b14ea6d0507ab86","tarball":"https://registry.npmjs.org/@alexify/kerberos/-/kerberos-2.0.1.tgz","fileCount":63,"integrity":"sha512-W1Q1+r2XMhagFeCfXj50MV9sCtsug8gI9bibMciadSUmIHHc6MGTVeA9DI+SgeIeTYwMDzgoBtug8ZWFt4qooA==","signatures":[{"sig":"MEYCIQDYwmx9vSqn8f+U7UGPY/JmTfExCeR3IekPAPZ4iIZZAQIhAKy9ns0D5dUwBeFnemDJIaRw23ocoPEDZf+cyfqSCiSe","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":252722},"main":"index.js","types":"index.d.ts","exports":{".":{"types":"./index.d.ts","default":"./index.js"},"./tests":{"types":"./tests.d.ts","default":"./tests.js"}},"scripts":{"lint":"eslint src -c eslint.config.js","test":"node --test test/*.test.js","test:types":"tsd"},"_npmUser":{"name":"alex-dolid","email":"dolid.sasha@gmail.com"},"pre-commit":["lint","test"],"repository":{"url":"git+https://github.com/Alexis-Technologies/kerberos.git","type":"git"},"description":"Fast and low overhead authorization framework for JavaScript runtime","directories":{"test":"test"},"_nodeVersion":"24.14.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"ajv":"^8.18.0","tsd":"^0.31.2","zod":"^4.1.1","jsep":"^1.4.0","keyv":"^5.6.0","pino":"^10.3.1","eslint":"^9.14.0","globals":"^15.11.0","prettier":"^3.3.3","@eslint/js":"^9.13.0","typescript":"^5.8.3","neostandard":"^0.11.7","@jsep-plugin/new":"^1.0.4","@sinclair/typebox":"^0.34.48","eslint-plugin-jest":"^28.8.3","eslint-plugin-node":"^11.1.0","@jsep-plugin/object":"^1.2.2","@jsep-plugin/ternary":"^1.1.4","eslint-plugin-import":"^2.31.0","eslint-plugin-promise":"^7.1.0","eslint-plugin-unicorn":"^56.0.0","eslint-config-prettier":"^9.1.0","eslint-plugin-prettier":"^5.2.1","@stylistic/eslint-plugin-js":"^2.10.1"},"_npmOperationalInternal":{"tmp":"tmp/kerberos_2.0.1_1780342930740_0.47255865292139587","host":"s3://npm-registry-packages-npm-production"}},"3.0.0":{"name":"@alexify/kerberos","version":"3.0.0","keywords":["kerberos","authorization","security","auth","policy","access control","cerbos","framework","schema","web","abac","rbac","rebac","zanzibar","spicedb","openfga","fine-grained-authorization","permissions","policy-as-code","derived-roles","opentelemetry"],"author":{"name":"Alex Dolid","email":"dolid.sasha@gmail.com"},"license":"MIT","_id":"@alexify/kerberos@3.0.0","maintainers":[{"name":"alex-dolid","email":"dolid.sasha@gmail.com"}],"homepage":"https://alexis.dev","bugs":{"url":"https://github.com/Alexis-Technologies/kerberos/issues"},"tsd":{"directory":"test"},"dist":{"shasum":"52d961255fc933d54785867ce952bd3635e1a054","tarball":"https://registry.npmjs.org/@alexify/kerberos/-/kerberos-3.0.0.tgz","fileCount":77,"integrity":"sha512-xUMl3c6jGFJq0C3RIYMif5Otzj+1y1Ais4z7kHmoSd5uNGY2SM0XYN9UpfK6EqBLHMeJud16xan4hJmBSVovPA==","signatures":[{"sig":"MEUCIQCMDlcIM49mVg+6QjP2yoJYrnaCfH6KNOH/iji1OLV1owIgNzGfX30nbPsBRiFkratpnpri2CVp9AsMw3WbyCPoPcU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":440020},"main":"index.js","_from":"file:alexify-kerberos-3.0.0.tgz","types":"index.d.ts","browser":{"./index.js":"./browser.js","./src/runtime/node.js":"./src/runtime/browser.js"},"engines":{"node":">=18.0.0"},"exports":{".":{"types":"./index.d.ts","browser":"./browser.js","default":"./index.js"},"./tests":{"types":"./tests.d.ts","default":"./tests.js"},"./relations":{"types":"./relations.d.ts","default":"./relations.js"}},"scripts":{"lint":"oxlint src test","test":"node --test test/*.test.js","bench":"node bench/bench.js","format":"oxfmt src test","test:types":"tsd","format:check":"oxfmt src test --check","test:coverage":"c8 --all --include 'src/**' --check-coverage --lines 95 --statements 95 --branches 90 --functions 95 --reporter text --reporter lcov node --test test/*.test.js"},"_npmUser":{"name":"alex-dolid","email":"dolid.sasha@gmail.com"},"_resolved":"/private/var/folders/9z/hn3k1glj0wz37h2hr379hr600000gn/T/5790ea80e84f684a1aba7282209be43b/alexify-kerberos-3.0.0.tgz","_integrity":"sha512-xUMl3c6jGFJq0C3RIYMif5Otzj+1y1Ais4z7kHmoSd5uNGY2SM0XYN9UpfK6EqBLHMeJud16xan4hJmBSVovPA==","repository":{"url":"git+https://github.com/Alexis-Technologies/kerberos.git","type":"git"},"_npmVersion":"11.11.0","description":"Fast, zero-dependency in-process authorization engine for Node.js and browsers — ABAC, RBAC and ReBAC (Zanzibar-style relations) with Cerbos-like policies, caveats, OpenTelemetry and pluggable validation/caching","directories":{"test":"test"},"_nodeVersion":"24.14.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"c8":"^10.1.3","ajv":"^8.18.0","tsd":"^0.31.2","zod":"^4.1.1","jsep":"^1.4.0","keyv":"^5.6.0","pino":"^10.3.1","oxfmt":"^0.58.0","oxlint":"^1.73.0","typescript":"^5.8.3","@jsep-plugin/new":"^1.0.4","@sinclair/typebox":"^0.34.48","@opentelemetry/api":"^1.9.1","@jsep-plugin/object":"^1.2.2","@jsep-plugin/ternary":"^1.1.4","@opentelemetry/sdk-metrics":"^2.9.0","@opentelemetry/sdk-trace-base":"^2.9.0","@opentelemetry/context-async-hooks":"^2.9.0"},"_npmOperationalInternal":{"tmp":"tmp/kerberos_3.0.0_1784542730619_0.49308207926777725","host":"s3://npm-registry-packages-npm-production"}},"3.1.0":{"name":"@alexify/kerberos","version":"3.1.0","keywords":["kerberos","authorization","security","auth","policy","access control","cerbos","framework","schema","web","abac","rbac","rebac","zanzibar","spicedb","openfga","fine-grained-authorization","permissions","policy-as-code","derived-roles","opentelemetry"],"author":{"name":"Alex Dolid","email":"dolid.sasha@gmail.com"},"license":"MIT","_id":"@alexify/kerberos@3.1.0","maintainers":[{"name":"alex-dolid","email":"dolid.sasha@gmail.com"}],"homepage":"https://alexis.dev","bugs":{"url":"https://github.com/Alexis-Technologies/kerberos/issues"},"tsd":{"directory":"test"},"dist":{"shasum":"75238e99743ca285a51848da9177c3d5668a2a08","tarball":"https://registry.npmjs.org/@alexify/kerberos/-/kerberos-3.1.0.tgz","fileCount":81,"integrity":"sha512-oEW3AvSu3QoaXl+E9dwSeKVnv0udtJqDuGBnXwB3EqPzS5gSqH8o4uXZY/kdAoddWKdG/q4P9nNW7Gr6V+7U/w==","signatures":[{"sig":"MEYCIQCdoOX23ge770qugTBcO+kfwQKlTe2tuJTrYazuZDzQ8AIhAIRvYfvQsgluFJGd/uk9McWl5rjay2RsV2J8RO1Fsg/N","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":537820},"main":"index.js","_from":"file:alexify-kerberos-3.1.0.tgz","types":"index.d.ts","browser":{"./index.js":"./browser.js","./src/runtime/node.js":"./src/runtime/browser.js"},"engines":{"node":">=18.0.0"},"exports":{".":{"types":"./index.d.ts","browser":"./browser.js","default":"./index.js"},"./tests":{"types":"./tests.d.ts","default":"./tests.js"},"./relations":{"types":"./relations.d.ts","default":"./relations.js"}},"scripts":{"lint":"oxlint src test scripts bench","size":"node scripts/size.js","test":"node --test test/*.test.js","bench":"node bench/bench.js","format":"oxfmt src test scripts bench","test:types":"tsd","format:check":"oxfmt --check src test scripts bench","test:coverage":"c8 --all --include 'src/**' --check-coverage --lines 95 --statements 95 --branches 90 --functions 95 --reporter text --reporter lcov node --test test/*.test.js"},"_npmUser":{"name":"alex-dolid","email":"dolid.sasha@gmail.com"},"_resolved":"/private/var/folders/9z/hn3k1glj0wz37h2hr379hr600000gn/T/acd67e1dcbbd844477f5aef832696499/alexify-kerberos-3.1.0.tgz","_integrity":"sha512-oEW3AvSu3QoaXl+E9dwSeKVnv0udtJqDuGBnXwB3EqPzS5gSqH8o4uXZY/kdAoddWKdG/q4P9nNW7Gr6V+7U/w==","repository":{"url":"git+https://github.com/Alexis-Technologies/kerberos.git","type":"git"},"_npmVersion":"11.11.0","description":"Fast, zero-dependency in-process authorization engine for Node.js and browsers — ABAC, RBAC and ReBAC (Zanzibar-style relations) with Cerbos-like policies, caveats, OpenTelemetry and pluggable validation/caching","directories":{"test":"test"},"_nodeVersion":"24.14.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"c8":"^10.1.3","ajv":"^8.18.0","tsd":"^0.31.2","zod":"^4.1.1","jsep":"^1.4.0","keyv":"^5.6.0","pino":"^10.3.1","oxfmt":"^0.58.0","oxlint":"^1.73.0","esbuild":"^0.28.1","typescript":"^5.8.3","@jsep-plugin/new":"^1.0.4","@sinclair/typebox":"^0.34.48","@opentelemetry/api":"^1.9.1","@jsep-plugin/object":"^1.2.2","@jsep-plugin/ternary":"^1.1.4","@opentelemetry/sdk-metrics":"^2.9.0","@opentelemetry/sdk-trace-base":"^2.9.0","@opentelemetry/context-async-hooks":"^2.9.0"},"_npmOperationalInternal":{"tmp":"tmp/kerberos_3.1.0_1784581260075_0.45586368735380467","host":"s3://npm-registry-packages-npm-production"}},"4.0.0":{"name":"@alexify/kerberos","version":"4.0.0","keywords":["kerberos","authorization","security","auth","policy","access control","cerbos","framework","schema","web","abac","rbac","rebac","zanzibar","spicedb","openfga","fine-grained-authorization","permissions","policy-as-code","derived-roles","opentelemetry"],"author":{"name":"Alex Dolid","email":"dolid.sasha@gmail.com"},"license":"MIT","_id":"@alexify/kerberos@4.0.0","maintainers":[{"name":"alex-dolid","email":"dolid.sasha@gmail.com"}],"homepage":"https://kerberosjs.vercel.app/","bugs":{"url":"https://github.com/Alexis-Technologies/kerberos/issues"},"bin":{"kerberos":"bin/kerberos.js"},"tsd":{"directory":"test"},"dist":{"shasum":"f16e37d1cd55cd73cc7bb6a17f1feb4ccd2b59ac","tarball":"https://registry.npmjs.org/@alexify/kerberos/-/kerberos-4.0.0.tgz","fileCount":103,"integrity":"sha512-z5fDpI5mXYlTVAGhqlT7jKc08Y2LOcHdhBDziyoa7S5h1WJvUA5AsvnLUTxPhpCq2ntQLULy21as91Tyxn5+Lw==","signatures":[{"sig":"MEYCIQDIaAtfB5UEuY696O6gMXyKqrieiejDif+YaWS4QIy3AgIhAIqxg5/aDSIzKSTKWlc9DHYgH7nGMzHtbTLCNGCYQpYf","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":812624},"main":"index.js","_from":"file:alexify-kerberos-4.0.0.tgz","types":"index.d.ts","browser":{"./index.js":"./browser.js","./loader.js":"./src/loader/browser.js","./src/loader/index.js":"./src/loader/browser.js","./src/runtime/node.js":"./src/runtime/browser.js"},"engines":{"node":">=18.0.0"},"exports":{".":{"types":"./index.d.ts","browser":"./browser.js","default":"./index.js"},"./tests":{"types":"./tests.d.ts","default":"./tests.js"},"./cerbos":{"types":"./cerbos.d.ts","default":"./cerbos.js"},"./loader":{"types":"./loader.d.ts","browser":"./src/loader/browser.js","default":"./loader.js"},"./relations":{"types":"./relations.d.ts","default":"./relations.js"}},"scripts":{"lint":"oxlint src test scripts bench conformance bin","size":"node scripts/size.js","test":"node --test test/*.test.js","bench":"node bench/bench.js","format":"oxfmt src test scripts bench conformance bin","docs:dev":"vitepress dev docs","docs:build":"vitepress build docs","test:types":"tsd","docs:preview":"vitepress preview docs","format:check":"oxfmt --check src test scripts bench conformance bin","size:compare":"node scripts/size-compare.js","bench:compare":"node bench/compare.js","test:coverage":"c8 --all --include 'src/**' --check-coverage --lines 95 --statements 95 --branches 90 --functions 95 --reporter text --reporter lcov node --test test/*.test.js","test:conformance":"node --test conformance/*.test.js"},"_npmUser":{"name":"alex-dolid","email":"dolid.sasha@gmail.com"},"_resolved":"/private/var/folders/9z/hn3k1glj0wz37h2hr379hr600000gn/T/d1a3278361932705b7a2a5b6069e3c17/alexify-kerberos-4.0.0.tgz","_integrity":"sha512-z5fDpI5mXYlTVAGhqlT7jKc08Y2LOcHdhBDziyoa7S5h1WJvUA5AsvnLUTxPhpCq2ntQLULy21as91Tyxn5+Lw==","repository":{"url":"git+https://github.com/Alexis-Technologies/kerberos.git","type":"git"},"_npmVersion":"11.11.0","description":"Fast, zero-dependency in-process authorization engine for Node.js and browsers — ABAC, RBAC and ReBAC (Zanzibar-style relations) with Cerbos-like policies, caveats, OpenTelemetry and pluggable validation/caching","directories":{"test":"test"},"_nodeVersion":"24.14.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"c8":"^10.1.3","ajv":"^8.18.0","tsd":"^0.31.2","zod":"^4.1.1","jsep":"^1.4.0","keyv":"^5.6.0","pino":"^10.3.1","yaml":"^2.9.0","dayjs":"^1.11.21","debug":"^4.4.3","oxfmt":"^0.58.0","casbin":"^5.51.1","oxlint":"^1.73.0","esbuild":"^0.28.1","mermaid":"^11.16.0","cytoscape":"^3.34.0","vitepress":"^1.6.4","typescript":"^5.8.3","drizzle-orm":"^0.45.2","@casl/ability":"^7.0.1","@prisma/client":"^7.10.0","@jsep-plugin/new":"^1.0.4","@sinclair/typebox":"^0.34.48","@vercel/analytics":"^2.0.1","@cerbos/orm-prisma":"^4.1.0","@opentelemetry/api":"^1.9.1","@cerbos/orm-drizzle":"^0.1.0","@jsep-plugin/object":"^1.2.2","@jsep-plugin/ternary":"^1.1.4","@vercel/speed-insights":"^2.0.0","cytoscape-cose-bilkent":"^4.1.0","@braintree/sanitize-url":"^7.1.2","vitepress-plugin-mermaid":"^2.0.17","@opentelemetry/sdk-metrics":"^2.9.0","@opentelemetry/sdk-trace-base":"^2.9.0","@opentelemetry/context-async-hooks":"^2.9.0"},"_npmOperationalInternal":{"tmp":"tmp/kerberos_4.0.0_1788081872021_0.2554485766531889","host":"s3://npm-registry-packages-npm-production"}},"4.1.0":{"name":"@alexify/kerberos","version":"4.1.0","keywords":["kerberos","authorization","security","auth","policy","access control","cerbos","framework","schema","web","abac","rbac","rebac","zanzibar","spicedb","openfga","fine-grained-authorization","permissions","policy-as-code","derived-roles","opentelemetry"],"author":{"name":"Alex Dolid","email":"dolid.sasha@gmail.com"},"license":"MIT","_id":"@alexify/kerberos@4.1.0","maintainers":[{"name":"alex-dolid","email":"dolid.sasha@gmail.com"}],"homepage":"https://kerberosjs.vercel.app/","bugs":{"url":"https://github.com/Alexis-Technologies/kerberos/issues"},"bin":{"kerberos":"bin/kerberos.js"},"tsd":{"directory":"test"},"dist":{"shasum":"7047a9cf9ef6bcde983452a365f5e298cc8a9ebb","tarball":"https://registry.npmjs.org/@alexify/kerberos/-/kerberos-4.1.0.tgz","fileCount":106,"integrity":"sha512-MamBszpn6S0YSUDkLjkdy+6a3uRDSQP5RnI3yncaPyWCHxqAnnnwLzb2AWkTj+VGbK7s5Db0Mqh/9Fgptlgstw==","signatures":[{"sig":"MEUCIQCmbHPcYYdvanmJm4VVIlv+ziofV1AhOtkxIo8qm+TaoAIgIt8dL5Wbo41OBtAITl16/CCXI2apMdQX0CkTCTe3rSg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":905089},"main":"index.js","_from":"file:alexify-kerberos-4.1.0.tgz","types":"index.d.ts","browser":{"./index.js":"./browser.js","./loader.js":"./src/loader/browser.js","./src/loader/index.js":"./src/loader/browser.js","./src/runtime/node.js":"./src/runtime/browser.js"},"engines":{"node":">=18.0.0"},"exports":{".":{"types":"./index.d.ts","browser":"./browser.js","default":"./index.js"},"./tests":{"types":"./tests.d.ts","default":"./tests.js"},"./cerbos":{"types":"./cerbos.d.ts","default":"./cerbos.js"},"./loader":{"types":"./loader.d.ts","browser":"./src/loader/browser.js","default":"./loader.js"},"./relations":{"types":"./relations.d.ts","default":"./relations.js"}},"scripts":{"lint":"oxlint src test scripts bench conformance bin","size":"node scripts/size.js","test":"node --test test/*.test.js","bench":"node bench/bench.js","format":"oxfmt src test scripts bench conformance bin","docs:dev":"vitepress dev docs","docs:build":"vitepress build docs","test:types":"tsd","docs:preview":"vitepress preview docs","format:check":"oxfmt --check src test scripts bench conformance bin","size:compare":"node scripts/size-compare.js","bench:compare":"node bench/compare.js","test:coverage":"c8 --all --include 'src/**' --check-coverage --lines 95 --statements 95 --branches 90 --functions 95 --reporter text --reporter lcov node --test test/*.test.js","test:conformance":"node --test conformance/*.test.js"},"_npmUser":{"name":"alex-dolid","email":"dolid.sasha@gmail.com"},"_resolved":"/private/var/folders/9z/hn3k1glj0wz37h2hr379hr600000gn/T/2cad7a31e354e078eb401921c2b526bc/alexify-kerberos-4.1.0.tgz","_integrity":"sha512-MamBszpn6S0YSUDkLjkdy+6a3uRDSQP5RnI3yncaPyWCHxqAnnnwLzb2AWkTj+VGbK7s5Db0Mqh/9Fgptlgstw==","repository":{"url":"git+https://github.com/Alexis-Technologies/kerberos.git","type":"git"},"_npmVersion":"11.11.0","description":"Fast, zero-dependency in-process authorization engine for Node.js and browsers — ABAC, RBAC and ReBAC (Zanzibar-style relations) with Cerbos-like policies, caveats, OpenTelemetry and pluggable validation/caching","directories":{"test":"test"},"_nodeVersion":"24.14.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"c8":"^10.1.3","ajv":"^8.18.0","tsd":"^0.31.2","zod":"^4.1.1","jsep":"^1.4.0","keyv":"^5.6.0","pino":"^10.3.1","yaml":"^2.9.0","dayjs":"^1.11.21","debug":"^4.4.3","oxfmt":"^0.58.0","casbin":"^5.51.1","oxlint":"^1.73.0","esbuild":"^0.28.1","mermaid":"^11.16.0","cytoscape":"^3.34.0","vitepress":"^1.6.4","typescript":"^5.8.3","drizzle-orm":"^0.45.2","@casl/ability":"^7.0.1","@prisma/client":"^7.10.0","@jsep-plugin/new":"^1.0.4","@sinclair/typebox":"^0.34.48","@vercel/analytics":"^2.0.1","@cerbos/orm-prisma":"^4.1.0","@opentelemetry/api":"^1.9.1","@cerbos/orm-drizzle":"^0.1.0","@jsep-plugin/object":"^1.2.2","@jsep-plugin/ternary":"^1.1.4","@vercel/speed-insights":"^2.0.0","cytoscape-cose-bilkent":"^4.1.0","@braintree/sanitize-url":"^7.1.2","vitepress-plugin-mermaid":"^2.0.17","@opentelemetry/sdk-metrics":"^2.9.0","@opentelemetry/sdk-trace-base":"^2.9.0","@opentelemetry/context-async-hooks":"^2.9.0"},"_npmOperationalInternal":{"tmp":"tmp/kerberos_4.1.0_1788691610524_0.33251608778094766","host":"s3://npm-registry-packages-npm-production"}},"4.2.0":{"_id":"@alexify/kerberos@4.2.0","bin":{"kerberos":"bin/kerberos.js"},"tsd":{"directory":"test"},"bugs":{"url":"https://github.com/Alexis-Technologies/kerberos/issues"},"dist":{"shasum":"3f166feb88b41c2e6cd4e3e7237a72cb506d97b0","tarball":"https://registry.npmjs.org/@alexify/kerberos/-/kerberos-4.2.0.tgz","fileCount":106,"integrity":"sha512-+Bq/5QeZMCamlDi5WkEcIYHEk1x93Itb10Bho7fYNZsRmfTULr5B5oUv+vpA3C732F0SReJR2ojAMFfk9ujfLQ==","signatures":[{"sig":"MEUCIQCyZh7Y3Y4jv67RTnpvnDo7oDwPfHR48LxEYToOO8zfBAIgNkYzch31AbTpEAAschPlrLqngAnHwK7FL0uzGPEZDdU=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDWzqtSBAnKB70tiqS1u7/ZSnPrAqFX+J1IuOWfcmqSDAIgZPqS8yyKF4K6jiH75YfZL6J7tfxRXLDqhwCxN21M6yU="}],"unpackedSize":922086},"main":"index.js","name":"@alexify/kerberos","_from":"file:alexify-kerberos-4.2.0.tgz","types":"index.d.ts","author":{"name":"Alex Dolid","email":"dolid.sasha@gmail.com"},"browser":{"./index.js":"./browser.js","./loader.js":"./src/loader/browser.js","./src/loader/index.js":"./src/loader/browser.js","./src/runtime/node.js":"./src/runtime/browser.js"},"engines":{"node":">=18.0.0"},"exports":{".":{"types":"./index.d.ts","browser":"./browser.js","default":"./index.js"},"./tests":{"types":"./tests.d.ts","default":"./tests.js"},"./cerbos":{"types":"./cerbos.d.ts","default":"./cerbos.js"},"./loader":{"types":"./loader.d.ts","browser":"./src/loader/browser.js","default":"./loader.js"},"./relations":{"types":"./relations.d.ts","default":"./relations.js"}},"license":"MIT","scripts":{"lint":"oxlint src test scripts bench conformance bin","size":"node scripts/size.js","test":"node --test test/*.test.js","bench":"node bench/bench.js","format":"oxfmt src test scripts bench conformance bin","docs:dev":"vitepress dev docs","docs:build":"vitepress build docs","test:types":"tsd","docs:preview":"vitepress preview docs","format:check":"oxfmt --check src test scripts bench conformance bin","size:compare":"node scripts/size-compare.js","bench:compare":"node bench/compare.js","test:coverage":"c8 --all --include 'src/**' --check-coverage --lines 95 --statements 95 --branches 90 --functions 95 --reporter text --reporter lcov node --test test/*.test.js","test:conformance":"node --test conformance/*.test.js"},"version":"4.2.0","_npmUser":{"name":"alex-dolid","email":"dolid.sasha@gmail.com"},"homepage":"https://kerberosjs.vercel.app/","keywords":["kerberos","authorization","security","auth","policy","access control","cerbos","framework","schema","web","abac","rbac","rebac","zanzibar","spicedb","openfga","fine-grained-authorization","permissions","policy-as-code","derived-roles","opentelemetry"],"_resolved":"/private/var/folders/9z/hn3k1glj0wz37h2hr379hr600000gn/T/9b60eed8fc5fb70218ac2b39c1f2558d/alexify-kerberos-4.2.0.tgz","_integrity":"sha512-+Bq/5QeZMCamlDi5WkEcIYHEk1x93Itb10Bho7fYNZsRmfTULr5B5oUv+vpA3C732F0SReJR2ojAMFfk9ujfLQ==","repository":{"url":"git+https://github.com/Alexis-Technologies/kerberos.git","type":"git"},"_npmVersion":"11.11.0","description":"Fast, zero-dependency in-process authorization engine for Node.js and browsers — ABAC, RBAC and ReBAC (Zanzibar-style relations) with Cerbos-like policies, caveats, OpenTelemetry and pluggable validation/caching","directories":{"test":"test"},"maintainers":[{"name":"alex-dolid","email":"dolid.sasha@gmail.com"}],"_nodeVersion":"24.14.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"c8":"^10.1.3","ajv":"^8.18.0","tsd":"^0.31.2","zod":"^4.1.1","jsep":"^1.4.0","keyv":"^5.6.0","pino":"^10.3.1","yaml":"^2.9.0","dayjs":"^1.11.21","debug":"^4.4.3","oxfmt":"^0.58.0","casbin":"^5.51.1","oxlint":"^1.73.0","esbuild":"^0.28.1","mermaid":"^11.16.0","cytoscape":"^3.34.0","vitepress":"^1.6.4","typescript":"^5.8.3","drizzle-orm":"^0.45.2","@casl/ability":"^7.0.1","@prisma/client":"^7.10.0","@jsep-plugin/new":"^1.0.4","@sinclair/typebox":"^0.34.48","@vercel/analytics":"^2.0.1","@cerbos/orm-prisma":"^4.1.0","@opentelemetry/api":"^1.9.1","@cerbos/orm-drizzle":"^0.1.0","@jsep-plugin/object":"^1.2.2","@jsep-plugin/ternary":"^1.1.4","@vercel/speed-insights":"^2.0.0","cytoscape-cose-bilkent":"^4.1.0","@braintree/sanitize-url":"^7.1.2","vitepress-plugin-mermaid":"^2.0.17","@opentelemetry/sdk-metrics":"^2.9.0","@opentelemetry/sdk-trace-base":"^2.9.0","@opentelemetry/context-async-hooks":"^2.9.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/kerberos_4.2.0_1790101095723_0.8948636389735141"}}},"time":{"created":"2024-12-08T11:59:22.486Z","modified":"2026-09-22T18:18:16.011Z","1.0.0":"2024-12-08T11:59:22.747Z","2.0.0":"2026-05-31T19:05:47.081Z","2.0.1":"2026-06-01T19:42:10.876Z","3.0.0":"2026-07-20T10:18:50.776Z","3.1.0":"2026-07-20T21:01:00.216Z","4.0.0":"2026-08-30T09:24:32.227Z","4.1.0":"2026-09-06T10:46:50.673Z","4.2.0":"2026-09-22T18:18:15.828Z"},"bugs":{"url":"https://github.com/Alexis-Technologies/kerberos/issues"},"author":{"name":"Alex Dolid","email":"dolid.sasha@gmail.com"},"license":"MIT","homepage":"https://kerberosjs.vercel.app/","keywords":["kerberos","authorization","security","auth","policy","access control","cerbos","framework","schema","web","abac","rbac","rebac","zanzibar","spicedb","openfga","fine-grained-authorization","permissions","policy-as-code","derived-roles","opentelemetry"],"repository":{"url":"git+https://github.com/Alexis-Technologies/kerberos.git","type":"git"},"description":"Fast, zero-dependency in-process authorization engine for Node.js and browsers — ABAC, RBAC and ReBAC (Zanzibar-style relations) with Cerbos-like policies, caveats, OpenTelemetry and pluggable validation/caching","maintainers":[{"name":"alex-dolid","email":"dolid.sasha@gmail.com"}],"readme":"# Kerberos.js\n\n[![npm](https://img.shields.io/npm/v/%40alexify%2Fkerberos)](https://www.npmjs.com/package/@alexify/kerberos)\n[![CI](https://github.com/Alexis-Technologies/kerberos/actions/workflows/ci.yml/badge.svg)](https://github.com/Alexis-Technologies/kerberos/actions/workflows/ci.yml)\n[![node](https://img.shields.io/node/v/%40alexify%2Fkerberos)](#installation)\n[![dependencies](https://img.shields.io/badge/runtime_dependencies-0-brightgreen)](#bundle-size)\n[![license](https://img.shields.io/npm/l/%40alexify%2Fkerberos)](./LICENSE)\n\nAn **embedded, zero-dependency authorization engine** for Node.js and the browser: Cerbos-style policies (RBAC + ABAC), a SpiceDB-inspired \"Zanzibar-lite\" resolver (ReBAC) and Cerbos-compatible query plans — all in-process, no server to deploy, [~34 KB min+gzip](#bundle-size). The API deliberately stays as close to Cerbos as possible: if you know Cerbos, you already know Kerberos.js.\n\n```javascript\nimport { Kerberos, Effect } from '@alexify/kerberos';\n\nconst kerberos = new Kerberos([{\n  resourcePolicy: {\n    resource: 'expense',\n    version: 'default',\n    rules: [{ actions: ['view'], effect: Effect.Allow, roles: ['USER'],\n              condition: { match: ({ P, R }) => R.attr.ownerId === P.id } }],\n  },\n}], []);\n\nawait kerberos.isAllowed({\n  principal: { id: 'sally', roles: ['USER'] },\n  action: 'view',\n  resource: { id: 'expense1', kind: 'expense', attr: { ownerId: 'sally' } },\n}); // → true\n```\n\n### Why in-process?\n\n**Authorization as a library, not a service.** Cerbos and SpiceDB are excellent engines, but each runs as a separate Go server: another deployment, another network hop on every check, another thing that can be down. In a JavaScript stack, Kerberos.js gives you the same policy models with zero infrastructure — decisions are a function call, policies ship (and roll back) atomically with your code, and there is no PDP to keep in sync. A centralized service remains the right choice for polyglot stacks — see [When NOT to use it](#when-not-to-use-kerberosjs).\n\n**Policies are data plus the full power of JavaScript.** In-process policies use plain JS functions for conditions, variables and outputs — no expression-language ceiling. Policies stored in a cache/database use the same shapes with safe, eval-free [`$expr` expressions](#caching--storing-policies). Local testing needs no emulator: the [`/tests` subpath](#testing) runs Cerbos-style declarative test suites against the real engine.\n\n**One engine everywhere.** The [browser build](#browser-usage) contains zero Node builtins, so the same policies that guard your API also gate your UI (hide buttons, filter menus) — without maintaining a second source of truth. Serverless and edge runtimes get the same benefit: no cold-start dependency on an external PDP.\n\n### Positioning\n\n| | **Kerberos.js**                                                                       | **Cerbos**                         | **SpiceDB** |\n| --- |---------------------------------------------------------------------------------------|------------------------------------| --- |\n| Deployment | in-process library (JS)                                                               | PDP service (sidecar/central)      | central service |\n| Policy model | Cerbos-style RBAC+ABAC + ReBAC + query plans                                          | RBAC+ABAC (policies style)         | ReBAC (Zanzibar) |\n| Conditions | JS functions / safe `$expr`                                                           | CEL                                | caveats (CEL) |\n| Query plans | `planResources` (Cerbos-compatible shape)                                             | `PlanResources`                    | `LookupResources` |\n| Consistency | in-process state + your cache ([honest limitations](#consistency-honest-limitations)) | per-PDP policy sync                | Zanzibar consistency (zookies) |\n| Best when | JS/TS stack, zero-infra, browser/edge                                                 | polyglot stack, central governance | relationship graphs at scale, strict consistency |\n\n> [!NOTE]\n> Compatibility with Cerbos is checked in CI by a [conformance suite](./conformance/) that runs one corpus against both engines — every decision and every query plan is compared to a real Cerbos PDP. Features Kerberos deliberately does not implement (CEL, attribute schemas, `scopePermissions`, `auxData`) are catalogued in [DIVERGENCES.md](./conformance/DIVERGENCES.md).\n\n### When NOT to use Kerberos.js\n\n- **Polyglot backends** — if Go/Python/Java services need the same decisions, a central PDP (Cerbos) beats reimplementing policies per language.\n- **Zanzibar-grade consistency** — the built-in ReBAC resolver reads current in-memory/cache state and deliberately has no revision tokens; if the [New Enemy Problem](https://authzed.com/docs/spicedb/concepts/consistency) matters for your threat model, use SpiceDB.\n- **Non-engineering policy ownership** — policies here live in code/storage you control; if compliance teams need a managed policy workflow and UI, that is Cerbos Hub's territory.\n\n### Features\n\n| Area | What you get |\n| ---- | ------------ |\n| **Policy engine** | [Resource / principal / role policies](#policy-types) (with `parentRoles` inheritance), [derived roles](#quick-start), conditions, variables & constants, [outputs](#outputs), [scopes & policy versions](#scopes-and-policy-versions) |\n| **APIs** | [`isAllowed`](#kerberosisallowedargs--promiseboolean), [`checkResources`](#kerberoscheckresourcesargs-effectasboolean--false--promisecheckresourcesresponse), [`planResources`](#query-plans-planresources) (Cerbos-compatible query plans) |\n| **Dynamic policies** | [Cache-agnostic storage](#caching--storing-policies) with a safe, eval-free `$expr` codec (jsep AST allowlist) |\n| **ReBAC** | [Relation-backed derived roles](#rebac-relations) + a built-in Zanzibar-lite resolver (`@alexify/kerberos/relations`) |\n| **Observability** | [Audit logs](#options) (console / structured / Pino), [OpenTelemetry](#opentelemetry) traces + metrics, [decision metadata](#decision-metadata-includemeta) |\n| **DX** | [Pluggable validation](#schema-validation) (Zod / JSON Schema + Ajv / TypeBox), [testing DSL](#testing) (`/tests`), [typed authoring](#typescript) via an optional app schema, [browser build](#browser-usage), [live playground](https://kerberosjs.vercel.app/playground) |\n| **Compatibility** | A [conformance suite](./conformance/) runs one corpus — written in Cerbos's own policy and test formats — against both Kerberos and a real Cerbos PDP in CI; known gaps are listed in [DIVERGENCES.md](./conformance/DIVERGENCES.md) |\n\n> **Version 4.x** — see the [CHANGELOG](./CHANGELOG.md) for everything that changed since `3.1.0`: verified Cerbos compatibility (a conformance corpus replayed against a live PDP, plus the `/cerbos` YAML + CEL policy importer), attribute-schema enforcement, policy-as-code tooling (the `/loader` subpath and the `kerberos` CLI), typed policy authoring, and the code-review hardening waves — a `Conditions` fail-open fix, restored Node-ESM named exports, and a synchronous evaluation driver (~2.5× on simple `isAllowed`). `4.2.0` closes the parity gaps a differential campaign against a live PDP turned up — CEL-style strict comparisons and Cerbos's resource-kind sanitization — see its upgrade notes.\n\n## Table of Contents\n\n- [Installation](#installation)\n  - [Bundle size](#bundle-size) · [Browser usage](#browser-usage)\n- [Quick Start](#quick-start)\n- [Policy Types](#policy-types)\n  - [ResourcePolicy](#resourcepolicy) · [PrincipalPolicy](#principalpolicy) · [RolePolicy](#rolepolicy) · [Mixed Policy Evaluation](#mixed-policy-evaluation)\n- [Scopes and Policy Versions](#scopes-and-policy-versions)\n- [API Reference](#api-reference)\n  - [`new Kerberos(...)`](#new-kerberospolicies-derivedroles-options) · [`isAllowed`](#kerberosisallowedargs--promiseboolean) · [`checkResources`](#kerberoscheckresourcesargs-effectasboolean--false--promisecheckresourcesresponse) · [`planResources`](#kerberosplanresourcesargs--promiseplanresourcesresponse) · [Events](#events-kerberosonevent-listener--this) · [Errors](#errors) · [Exports](#exports)\n- [TypeScript](#typescript)\n  - [Declaring a schema](#declaring-a-schema) · [What it buys you](#what-it-buys-you) · [Schema helper types](#schema-helper-types)\n- [Configuration Options](#configuration-options)\n  - [Options](#options) · [Pino logging](#using-pino-for-production-logging) · [Call ID generation](#call-id-generation)\n- [Outputs](#outputs)\n- [Decision metadata (includeMeta)](#decision-metadata-includemeta)\n- [Caching / Storing Policies](#caching--storing-policies)\n  - [How it works](#how-it-works-fallback-layer) · [`codec` modes](#codec-option--three-modes) · [Dynamic policy format](#dynamic-policy-format) · [Safe builtins](#allowed-safe-builtins) · [Serialization mechanism](#serialization-mechanism-security--performance)\n- [Importing Cerbos Policies](#importing-cerbos-policies)\n  - [What is translated](#what-is-translated) · [CEL → `$expr`](#the-cel--expr-translation) · [How this is verified](#how-the-importer-is-verified)\n- [Loading Policies from Files](#loading-policies-from-files)\n- [ReBAC (Relations)](#rebac-relations)\n  - [Relation-backed derived roles](#relation-backed-derived-roles) · [Zanzibar-lite resolver](#the-built-in-zanzibar-lite-resolver) · [Dynamic tuples](#dynamic-tuples-cache-backed) · [Consistency](#consistency-honest-limitations)\n- [Query Plans (planResources)](#query-plans-planresources)\n  - [How a plan is composed](#how-a-plan-is-composed) · [Operators](#operators) · [Writing plannable policies](#writing-plannable-policies) · [ORM adapters](#using-the-official-cerbos-orm-adapters) · [Translating a plan](#translating-a-plan)\n- [Testing](#testing)\n  - [CLI](#policy-testing-from-the-command-line)\n- [Schema Validation](#schema-validation)\n  - [Zod](#using-zod) · [JSON Schema + Ajv](#using-json-schema--ajv) · [TypeBox + Ajv](#using-typebox--ajv) · [Explicit Builders](#using-explicit-builders) · [Attribute schemas](#attribute-schemas-cerbos-schemas)\n- [OpenTelemetry](#opentelemetry)\n- [Hooks & events](#hooks--events)\n  - [Hooks](#hooks) · [Events](#events) · [Hooks vs events](#hooks-vs-events)\n- [Benchmarks](#benchmarks)\n- [Changelog](#changelog) · [License](#license) · [Used by](#used-by)\n\n## Installation\n\n```bash\nnpm install @alexify/kerberos\n```\n\nRequires **Node.js ≥ 18** (or any modern browser through a bundler). The package is CommonJS; both `require('@alexify/kerberos')` and `import { Kerberos } from '@alexify/kerberos'` (via Node/bundler ESM interop) work — the examples below use `import`.\n\n### Bundle size\n\nZero runtime dependencies. Measured with `pnpm size` (esbuild browser bundle, fully minified with identifier mangling, then gzipped):\n\n| Entry | min | min+gzip |\n| ----- | ---:| --------:|\n| `@alexify/kerberos` (main entry, query planner included) | 127.4 KB | **35.8 KB** |\n| `@alexify/kerberos/relations` (opt-in ReBAC resolver) | 70.9 KB | 19.8 KB |\n| `@alexify/kerberos/cerbos` (opt-in [Cerbos importer](#importing-cerbos-policies)) | 34.0 KB | 10.9 KB |\n| `@alexify/kerberos/loader` (Node-only; browser bundlers get a throwing stub) | 1.0 KB | 0.5 KB |\n\nEvery subpath (`/relations`, `/cerbos`, `/loader`, `/tests`) is only bundled if you import it. Optional tooling (`jsep`, `zod`, `ajv`, `@sinclair/typebox`, `@opentelemetry/api`) is never included — you install what you use.\n\n### Browser usage\n\nThe package ships two entrypoints: a Node.js entry (`index.js`, uses `node:crypto` / `node:perf_hooks` directly) and a browser entry (`browser.js`) declared via the package.json `browser` field and the `browser` condition in `exports`. Browser bundlers pick the browser build automatically — **no configuration needed** for webpack 5, Vite, esbuild (`platform: 'browser'`), Parcel or Bun. Rollup users need [`@rollup/plugin-node-resolve`](https://github.com/rollup/plugins/tree/master/packages/node-resolve) with `browser: true`.\n\nThe browser build contains **zero Node.js builtins** — the only platform-specific code (`generateCallId`, `getNow`) is swapped to a browser implementation backed by `globalThis.crypto.randomUUID` and `globalThis.performance`.\n\nNotes:\n\n- In insecure contexts (plain HTTP), where `crypto.randomUUID` is unavailable, call IDs fall back to a `Math.random`-based pseudo UUID. Call IDs are **correlation identifiers, not security tokens**, so this is safe.\n- The package is CommonJS, so browser usage requires a bundler (no bare `<script>` tag).\n- Node.js itself ignores the `browser` field entirely — server-side usage (with or without a bundler) always resolves the Node entry.\n\n## Quick Start\n\nA resource policy with a **derived role** (a role computed per request — here, \"the owner of this expense\"), checked through both public APIs:\n\n```javascript\nimport { Kerberos, Effect } from '@alexify/kerberos';\n\nconst expensePolicy = {\n  resourcePolicy: {\n    resource: 'expense', // applies to resources of kind 'expense'\n    version: 'default',\n    importDerivedRoles: ['common_roles'],\n    rules: [\n      { actions: ['*'], effect: Effect.Allow, roles: ['ADMIN'] },\n      { actions: ['view', 'delete'], effect: Effect.Allow, derivedRoles: ['OWNER'] },\n      {\n        actions: ['view'],\n        effect: Effect.Allow,\n        roles: ['USER'],\n        condition: { match: ({ R }) => R.attr.status === 'OPEN' },\n      },\n    ],\n  },\n};\n\nconst commonRoles = {\n  name: 'common_roles',\n  definitions: [\n    { name: 'OWNER', parentRoles: ['USER'], condition: { match: ({ P, R }) => R.attr.ownerId === P.id } },\n  ],\n};\n\nconst kerberos = new Kerberos([expensePolicy], [commonRoles]);\n\n// Single decision:\nawait kerberos.isAllowed({\n  principal: { id: 'sally', roles: ['USER'] },\n  action: 'delete',\n  resource: { id: 'expense1', kind: 'expense', attr: { ownerId: 'sally', status: 'OPEN' } },\n}); // → true (OWNER derived role)\n\n// Batch decisions:\nconst response = await kerberos.checkResources({\n  principal: { id: 'frank', roles: ['USER'] },\n  resources: [\n    { resource: { id: 'expense1', kind: 'expense', attr: { ownerId: 'sally', status: 'OPEN' } }, actions: ['view', 'delete'] },\n  ],\n});\n// {\n//   kerberosCallId: 'b9c4362d-…', // generated UUID for audit correlation\n//   results: [{\n//     resource: { id: 'expense1', kind: 'expense' },\n//     actions: { view: 'EFFECT_ALLOW', delete: 'EFFECT_DENY' },\n//     outputs: [],\n//   }],\n// }\n```\n\nFrom here: [principal and role policies](#policy-types) for overrides and allowlists, [`planResources`](#query-plans-planresources) for \"which resources can this principal access\" filters, [dynamic policies](#caching--storing-policies) for cache-stored rules, and [ReBAC](#rebac-relations) for relationship-based access.\n\n## Policy Types\n\nKerberos.js supports three policy types:\n\n- **`resourcePolicy`**: selected by `resource.kind`, `resource.policyVersion`, and `resource.scope`\n- **`principalPolicy`**: selected by `principal.id`, `principal.policyVersion`, and `principal.scope`\n- **`rolePolicy`**: selected by each `principal.roles[]`, and — like resource policies — `resource.policyVersion` and `resource.scope`\n\nYou can pass either type on its own or mix them in the same constructor call:\n\n```javascript\nconst kerberos = new Kerberos(\n  [\n    expenseResourcePolicy,\n    sallyPrincipalPolicy,\n    userRolePolicy,\n  ],\n  [commonRoles]\n);\n```\n\n### ResourcePolicy\n\n`ResourcePolicy` is the workhorse policy type, selected by `resource.kind`. Rules are matched by action, then by `roles` or `derivedRoles`, and may also use `conditions`, `variables`, `constants`, `outputs`, versions, and scopes — see the [Quick Start](#quick-start) for a complete example.\n\n#### Conflict resolution\n\nConflicts are resolved **per principal role**, matching Cerbos: `EFFECT_DENY` overrides `EFFECT_ALLOW` **within** a role, and an `EFFECT_ALLOW` from **any** role wins across roles. Rule order never decides the outcome.\n\nThis is deliberate anti-lockout behaviour — picking up an extra, less privileged role can never take away access another role grants:\n\n```javascript\nrules: [\n  { actions: ['close'], effect: Effect.Allow, roles: ['SUPPORT'] },\n  { actions: ['close'], effect: Effect.Deny, roles: ['AUDITOR'] },\n];\n// principal roles ['SUPPORT', 'AUDITOR'] -> EFFECT_ALLOW\n```\n\nA deny that is meant to hold regardless has to cover the role carrying the allow — either with the `'*'` wildcard or by naming it:\n\n```javascript\n{ actions: ['close'], effect: Effect.Deny, roles: ['*'] }                 // always denies\n{ actions: ['close'], effect: Effect.Deny, roles: ['SUPPORT', 'AUDITOR'] } // denies both roles\n```\n\nDerived roles do not form a dimension of their own: a rule reached through `derivedRoles` counts for the principal roles listed in that definition's `parentRoles`.\n\n### PrincipalPolicy\n\n`PrincipalPolicy` follows the Cerbos-style model for principal-specific overrides. It is bound to a single principal and targets `resource + action` directly instead of `roles` / `derivedRoles`.\n\n```javascript\nconst sallyPrincipalPolicy = {\n  principalPolicy: {\n    principal: 'sally',\n    version: 'default',\n    scope: 'acme.corp',\n    constants: {\n      restrictedVendor: 'Flux Water Gear',\n    },\n    variables: {\n      isRestrictedVendor: ({ R, C }) => R.attr.vendor === C.restrictedVendor,\n    },\n    rules: [\n      {\n        resource: 'expense',\n        actions: [\n          {\n            name: 'deny-restricted-vendor-view',\n            action: 'view',\n            effect: Effect.Deny,\n            condition: {\n              match: ({ V }) => V.isRestrictedVendor,\n            },\n          },\n          {\n            name: 'allow-delete-override',\n            action: 'delete',\n            effect: Effect.Allow,\n          },\n        ],\n      },\n    ],\n  },\n};\n```\n\n### RolePolicy\n\n`RolePolicy` follows the Cerbos-style role-centric model. It is bound to a single role, targets `resource + allowActions`, and acts as a **narrowing filter over the [`ResourcePolicy`](#resourcepolicy)** — it never grants on its own. Three consequences worth internalising:\n\n- **A role policy cannot allow what the resource policy withholds.** The resource policy is always what grants; a role policy only takes away. With no matching `ResourcePolicy` at all, nothing is allowed.\n- **Multiple role policies union.** A principal may do what **any** of its roles permits. Holding an extra role can widen access, never narrow it.\n- **A role with no applicable role policy is unrestricted.** If any of the principal's roles has no role policy targeting this resource kind, the filter does not apply at all.\n\n```javascript\n// resourcePolicy `report` allows view + edit + delete for roles: ['*']\nrolePolicy READER: allowActions: ['view']\nrolePolicy WRITER: allowActions: ['edit']\n\nroles: ['READER']            -> view                    (filtered to the allowlist)\nroles: ['READER', 'WRITER']  -> view, edit              (union, not intersection)\nroles: ['READER', 'PLAIN']   -> view, edit, delete      (PLAIN is unconstrained)\n```\n\n```javascript\nconst userRolePolicy = {\n  rolePolicy: {\n    role: 'USER',\n    version: 'default',\n    scope: 'acme.corp',\n    constants: {\n      restrictedVendor: 'Flux Water Gear',\n    },\n    variables: {\n      isRestrictedVendor: ({ R, C }) => R.attr.vendor === C.restrictedVendor,\n    },\n    rules: [\n      {\n        resource: 'expense',\n        allowActions: ['create'],\n      },\n      {\n        resource: 'expense',\n        allowActions: ['view'],\n        condition: {\n          match: ({ V }) => V.isRestrictedVendor === false,\n        },\n      },\n    ],\n  },\n};\n```\n\n`RolePolicy` also supports `parentRoles`. Within a single role, the child keeps only actions that are **also** allowed by each locally defined parent role policy (intersection along the inheritance chain — distinct from the union *across* the principal's roles). Missing parent role policies are treated as external IdP roles and do not impose extra constraints inside Kerberos.\n\n### Mixed Policy Evaluation\n\nWhen mixed policy types are present, Kerberos resolves each action in this order:\n\n1. Find the matching `PrincipalPolicy` for the request principal.\n2. If it returns an explicit `EFFECT_ALLOW` or `EFFECT_DENY`, use that result — role policies do not narrow a principal-policy override.\n3. Otherwise, evaluate the matching `ResourcePolicy`. Before its rules are matched, the imported **derived roles are resolved**: condition-backed definitions evaluate synchronously, and relation-backed definitions (the `relation:` field) resolve through the configured [`relations` resolver](#rebac-relations) (ReBAC) — `list`-first with parallel `check` fallback, one shared memo per request. The resulting `effectiveDerivedRoles` then participate in rule matching alongside plain `roles`. Conflicts resolve **per principal role**: `EFFECT_DENY` overrides `EFFECT_ALLOW` within a role, an `EFFECT_ALLOW` from any role wins across roles.\n4. Apply the `RolePolicy` layer as a **filter** on that result: if every principal role is constrained by an applicable role policy, an `EFFECT_ALLOW` survives only when at least one of those roles allowlists the action (union across roles, `parentRoles` intersection within a role).\n5. If nothing matches, return `EFFECT_DENY`.\n\nThe decision is computed **per action** — different actions in the same request may be resolved by different policy layers. Each lookup (principal / role / resource) walks the [scope search chain](#scopes-and-policy-versions) and `policyVersion`, and checks in-memory policies first, then the optional `cache`.\n\n```mermaid\nflowchart TD\n    A([Request: principal · resource · action]) --> P{{\"PrincipalPolicy<br/>(by principal.id)\"}}\n    P -->|\"EFFECT_ALLOW / EFFECT_DENY\"| DONE([Action effect resolved])\n    P -->|no matching rule| DR\n\n    subgraph DR [\"Derived-roles resolution (importDerivedRoles)\"]\n        direction TB\n        SYNC[\"Condition-backed definitions<br/>(sync: parentRoles + condition)\"] --> EDR([effectiveDerivedRoles])\n        REL[\"Relation-backed definitions (relation: field)<br/>async via the relations resolver (ReBAC):<br/>list-first, parallel check fallback, shared memo\"] --> EDR\n    end\n\n    EDR --> RES{{\"ResourcePolicy<br/>(by resource.kind — rules match roles / derivedRoles)\"}}\n\n    RES -->|\"EFFECT_ALLOW (per-role conflict resolution)\"| RP{{\"RolePolicy filter<br/>(union across principal.roles[])\"}}\n    RES -->|\"EFFECT_DENY\"| DONE\n    RES -->|no rule matched| DEF([Default: EFFECT_DENY])\n    DEF --> DONE\n\n    RP -->|\"allowlisted by some role, or a role is unconstrained\"| DONE\n    RP -->|\"every role constrained and none allowlists it\"| DEF\n```\n\n> **Within the role layer:** the principal may do what **any** of its roles allowlists (union). A role with no applicable role policy is unrestricted, which disables the filter entirely. When a role declares `parentRoles`, the child keeps only the actions that are **also** allowed by each locally defined parent role policy (intersection along the chain).\n\nThis keeps Kerberos.js aligned with the Cerbos-style principal override model described in the [Cerbos principal policies documentation](https://docs.cerbos.dev/cerbos/latest/policies/principal_policies) while extending the runtime with role-centric policy evaluation similar to [Cerbos role policies](https://docs.cerbos.dev/cerbos/latest/policies/role_policies).\n\n## Scopes and Policy Versions\n\nKerberos.js supports scoped policies and policy versions, allowing you to organize policies for different environments or versions.\n\nPolicy selection depends on the policy type:\n\n- **`ResourcePolicy`**\n  - `resource.kind`\n  - `resource.policyVersion` (defaults to `'default'` when omitted)\n  - `resource.scope`\n- **`PrincipalPolicy`**\n  - `principal.id`\n  - `principal.policyVersion` (defaults to `'default'` when omitted)\n  - `principal.scope`\n- **`RolePolicy`**\n  - each `principal.roles[]` entry (plus every `parentRoles` ancestor)\n  - `resource.policyVersion` (defaults to `'default'` when omitted)\n  - `resource.scope`\n\nScope behavior follows the Cerbos-style model:\n\n- If `scope` is **not** provided for the relevant side of the lookup, Kerberos.js evaluates only the base policy without a scope.\n- If `scope` **is** provided, Kerberos.js searches from the most specific scope to the least specific scope, and finally falls back to the base policy.\n- Example search chain for `scope: 'acme.corp'`: `acme.corp -> acme -> ''`\n\nWhen both policy types are loaded, Kerberos first resolves principal overrides using the principal's scope/version chain, and falls back to the resource layer — resource policies and role policies, both keyed by the resource's version and scope — when no principal policy decides the action.\n\n### Which version applies to what\n\n| Policy            | Looked up by                                          | Version                   | Scope chain       |\n| ----------------- | ----------------------------------------------------- | ------------------------- | ----------------- |\n| `resourcePolicy`  | `resource.kind`                                       | `resource.policyVersion`  | `resource.scope`  |\n| `principalPolicy` | `principal.id`                                        | `principal.policyVersion` | `principal.scope` |\n| `rolePolicy`      | each `principal.roles[]` (+ `parentRoles` ancestors)  | `resource.policyVersion`  | `resource.scope`  |\n| `derivedRoles`    | name, from the resource policy's `importDerivedRoles` | — (unversioned)           | —                 |\n\nThree consequences:\n\n- The version is **fixed along the whole scope chain** — the walk never crosses versions — and there is **no fallback**: asking for a version no policy carries resolves to nothing rather than to `'default'`.\n- A request may mix versions. `principal.policyVersion: 'v2'` with an unversioned resource evaluates the principal policy at `v2` and the resource/role layer at `'default'`.\n- In `checkResources` the principal chain is resolved once per batch while every resource uses its own `policyVersion`; `planResources` echoes the resource's version back as `policyVersion`.\n\nCerbos 0.41+ differs here: it selects principal policies by the **resource's** version, a regression against its own documentation and its pre-0.41 engine. When the same policies are served by both engines, send the same value in both fields. See [DIVERGENCES.md](https://github.com/Alexis-Technologies/kerberos/blob/main/conformance/DIVERGENCES.md).\n\n### How the scope chain is evaluated\n\nMatching Cerbos's `SCOPE_PERMISSIONS_OVERRIDE_PARENT` (its default), the chain is not a lookup for one policy — every policy found along it participates, and evaluation is **per action, per principal role**:\n\n- The first scope whose policy produces a decision (allow or deny) for an action and a role **seals** it; policies further up cannot change it.\n- A rule whose condition fails decides nothing — the walk **falls through** to the parent scope for that action.\n- The walk runs per principal role, so a deny sealing one role at a specific scope does not stop another role from winning an allow at the base scope (allow from any role wins across roles).\n- A scope with no policy at all is simply skipped (Cerbos's `lenientScopeSearch`; Kerberos has no strict mode).\n\nWhich scope drives which policy type: **resource policies and role policies** walk the *resource's* scope chain; **principal policies** walk the *principal's*. (Cerbos's docs describe role-policy scope as the principal's, but its engine — and a live PDP — match it against the resource's; see [DIVERGENCES.md](./conformance/DIVERGENCES.md).)\n\n### Wildcards\n\nName fields glob, exactly as in Cerbos: a bare `*` matches anything; in any other pattern `*` matches within a single `:`-delimited segment (`view:*` matches `view:public` but neither the bare `view` nor `view:a:b`), and `**` crosses segments. Globs work in resource-policy `actions` and `roles`, principal-policy `resource` and `action`, role-policy `resource` and `allowActions`, and derived-role `parentRoles`. `rules[].derivedRoles` references are exact names — Cerbos's schema rejects globs there too.\n\nResource **kind** names are compared after Cerbos's own sanitization (`namer.SanitizedResource`): for a name shaped like `foo`, `foo:bar`, `foo-bar` or `foo/bar`, every run of characters outside `[A-Za-z0-9_.]` becomes `_` — on the policy field and on `resource.kind` alike. So `gk*` does match the kind `gka:b` (which is `gka_b` at match time), `doc:*` matches no kind at all (a pattern is never sanitized, and no sanitized kind keeps a `:`), and `ka-b`, `ka_b` and `ka/b` are one and the same resource — declaring policies for two of those spellings throws `Duplicate resource policy`. Principal ids and role names are not sanitized.\n\n\nExample:\n\n```javascript\nconst results = await kerberos.checkResources({\n  reqId: 'test-request',\n  principal: {\n    id: 'alice',\n    policyVersion: '20210210',  // Optional: selects the principal policy version\n    scope: 'acme.corp',         // Optional: principal policy scope chain\n    roles: ['employee'],\n    attr: {\n      department: 'accounting',\n      geography: 'GB'\n    }\n  },\n  resources: [\n    {\n      resource: {\n        id: 'XX125',\n        kind: 'leave_request',\n        policyVersion: '20210210', // Optional: specify resource policy version\n        scope: 'acme.corp',        // Optional: specify resource scope\n        attr: {\n          department: 'accounting',\n          owner: 'john'\n        }\n      },\n      actions: ['view:public', 'approve', 'create']\n    }\n  ],\n  includeMeta: true  // Optional: include metadata in response\n});\n```\n\n## API Reference\n\n### `new Kerberos(policies, derivedRoles?, options?)`\n\n| Parameter | Type | Description |\n| --------- | ---- | ----------- |\n| `policies` | `Array<ResourcePolicy \\| PrincipalPolicy \\| RolePolicy \\| object>` | Static policies loaded into memory. Plain objects are auto-detected by their `resourcePolicy` / `principalPolicy` / `rolePolicy` key. May be empty when policies are resolved from a `cache`. |\n| `derivedRoles` | `Array<DerivedRoles \\| object>` | Optional derived-role definition sets. |\n| `options` | `object` | Optional configuration — see [Configuration Options](#configuration-options). |\n\n### `kerberos.isAllowed(args) => Promise<boolean>`\n\nEvaluates a **single** action against a single resource and returns a boolean.\n\n- `args.principal` — the principal (`id`, `roles`, optional `policyVersion`, `scope`, `attr`).\n- `args.action` — the action to check.\n- `args.resource` — the resource (`id`, `kind`, optional `policyVersion`, `scope`, `attr`).\n- `args.reqId` — optional correlation id echoed in logs.\n- `args.includeMeta` — when `true`, enables decision tracing (visible in audit logs).\n\n```javascript\nconst allowed = await kerberos.isAllowed({\n  principal: { id: 'user1', roles: ['USER'], policyVersion: 'default', scope: 'acme.corp' },\n  action: 'view',\n  resource: { id: 'expense1', kind: 'expense', attr: { amount: 5000, status: 'OPEN' } },\n  reqId: 'optional-correlation-id', // optional\n});\n```\n\n### `kerberos.checkResources(args, effectAsBoolean = false) => Promise<CheckResourcesResponse>`\n\nEvaluates **multiple resources and actions** in a single request.\n\n- `args.principal` — the principal (`id`, `roles`, optional `policyVersion`, `scope`, `attr`).\n- `args.resources` — array of `{ resource, actions }` entries.\n- `args.reqId` — optional correlation id echoed in the response and logs.\n- `args.includeMeta` — when `true`, includes evaluation [metadata](#decision-metadata-includemeta).\n- `effectAsBoolean` — when `true`, action results are `true`/`false` instead of `EFFECT_ALLOW`/`EFFECT_DENY`.\n\n```javascript\nconst response = await kerberos.checkResources({\n  principal: { id: 'user1', roles: ['USER'] },\n  resources: [{ resource: { id: 'expense1', kind: 'expense' }, actions: ['view', 'create'] }],\n});\n// {\n//   kerberosCallId: 'b9c4362d-…',          // always present, for audit correlation\n//   reqId: '…',                            // present only if provided in the request\n//   results: [{ resource, actions, outputs, meta? }],\n// }\n```\n\n### `kerberos.planResources(args) => Promise<PlanResourcesResponse>`\n\nBuilds a **resources query plan**: instead of a yes/no decision for one resource, it returns a *filter* describing **which** resources of a kind the principal may act on — ready to translate into a database query. See [Query Plans](#query-plans-planresources).\n\n- `args.principal` — the principal (`id`, `roles`, optional `policyVersion`, `scope`, `attr`).\n- `args.resource` — the resource **kind** (`kind`, optional `policyVersion`, `scope`, `attr`). No `id`: `attr` carries only the *known* attributes; everything else stays unknown and surfaces in the filter.\n- `args.action` **or** `args.actions` — exactly one of them; multiple actions plan the conjunction (Cerbos AND semantics). The wildcard `'*'` cannot be planned.\n- `args.reqId` / `args.includeMeta` — as in `checkResources`; `includeMeta` adds `filterDebug`, `matchedScopes` and the `resolution` trace.\n\n```javascript\nconst plan = await kerberos.planResources({\n  principal: { id: 'user1', roles: ['USER'] },\n  resource: { kind: 'expense' },\n  action: 'view',\n});\n// {\n//   kerberosCallId: '…', action: 'view', resourceKind: 'expense', policyVersion: 'default',\n//   filter: { kind: 'KIND_ALWAYS_ALLOWED' | 'KIND_ALWAYS_DENIED' | 'KIND_CONDITIONAL', condition? },\n// }\n```\n\n### Events: `kerberos.on(event, listener) => this`\n\n`on`, `once`, `off` and `removeAllListeners(event?)` (all chainable) plus `listenerCount(event)` subscribe to the engine's lifecycle events — `request:start` / `request:end` / `request:error`, `decision`, `plan`, `relations:resolved`, `cache:hit` / `cache:miss` / `cache:error`. Listeners are contained (a throwing listener never affects a decision) and there is no public `emit`. Lifecycle **hooks** (awaited, may veto) are configured through the `hooks` option instead. See [Hooks & events](#events).\n\n```javascript\nkerberos.on('decision', ({ callId, resource, actions }) => metrics.record(callId, resource.kind, actions));\n```\n\n### Errors\n\nAll error classes are exported from the main entry. Evaluation-phase errors follow the [`onError`](#options) option; `KerberosValidationError` always throws.\n\n| Class | Thrown when |\n| ----- | ----------- |\n| `KerberosValidationError` | Malformed method arguments or request shapes (always propagates — a programming error, not a deny). |\n| `KerberosCacheError` | A transient `cache.get` failure persists after the [`cacheRetry`](#options) attempts. |\n| `KerberosCodecError` | A cached policy/tuple document is corrupt or fails to deserialize (for policies it is logged and counts as a miss; for ReBAC tuple documents it throws — see [Dynamic tuples](#dynamic-tuples-cache-backed)). |\n| `KerberosExprError` | A `{ $expr }` string uses a construct outside the [safe allowlist](#allowed-safe-builtins), exceeds codec limits, or fails to parse. |\n| `KerberosRelationsError` | The built-in ReBAC resolver hits `maxDepth`, a throwing caveat, or invalid relation data. |\n| `KerberosHookError` | A lifecycle hook threw, exceeded `hooksTimeoutMs` (`timedOut: true`) or returned an invalid replacement — `hook` names it, `cause` is the original error. Follows `onError` in the engine, always propagates from the resolver; see [Hooks & events](#error-contract). |\n\n### Exports\n\n| Export | Purpose |\n| ------ | ------- |\n| `Kerberos` | Main authorization engine. |\n| `Effect` | `{ Allow: 'EFFECT_ALLOW', Deny: 'EFFECT_DENY' }` — a frozen const object, [not an `enum`](#typescript). |\n| `ResourcePolicy`, `PrincipalPolicy`, `RolePolicy`, `DerivedRoles` | Policy classes (rarely constructed directly). |\n| `Conditions`, `Variables`, `Constants`, `Outputs` | DSL building blocks. |\n| `createSafeExprCodec`, `serializePolicy`, `deserializePolicy` | Safe AST codec for [dynamic/stored policies](#caching--storing-policies). |\n| `PlanKind` | `{ AlwaysAllowed, AlwaysDenied, Conditional }` — [query plan](#query-plans-planresources) filter kinds. |\n| `expandRelationOperands` | Materializes ReBAC `relation` operands of a [query plan](#query-plans-planresources) into id filters. |\n| `toCerbosQueryPlan` | Converts a plan to the `@cerbos/core` SDK shape for the [official Cerbos ORM adapters](#using-the-official-cerbos-orm-adapters). |\n| `KerberosValidationError`, `KerberosCacheError`, `KerberosCodecError`, `KerberosExprError`, `KerberosRelationsError`, `KerberosHookError` | Typed [error classes](#errors). |\n| `registerAjvKeywords`, `createAjvAdapter` | [Validation](#schema-validation) helpers. |\n| `resolveValidationAdapter`, `toValidationAdapter`, `parseWithValidation` | Backend dispatch used by every DSL module — pick an adapter (explicit → Zod → TypeBox+Ajv → JSON Schema+Ajv → passthrough) and parse with it. |\n| `createCacheReader` | Wraps any `get(key)` store as the engine's read-only [policy fallback layer](#caching--storing-policies). |\n| `JsonSchemas`, `TypeBoxSchemas`, `ZodSchemas`, `KerberosJsonSchemas`, `ResourcePolicyJsonSchemas`, `PrincipalPolicyJsonSchemas`, `RolePolicyJsonSchemas`, … | Schema builders for the three backends. |\n| `ALL_ACTIONS`, `ALL_ROLES`, `ALL_RESOURCES`, `DEFAULT_VERSION`, `BASE_SCOPE` | Wildcard/default tokens (`'*'`, `'default'`, `''`). |\n\nSubpath **`@alexify/kerberos/relations`** (opt-in ReBAC — kept out of the main entry so non-ReBAC bundles do not grow):\n\n| Export | Purpose |\n| ------ | ------- |\n| `RelationResolver` | The built-in [Zanzibar-lite resolver](#the-built-in-zanzibar-lite-resolver) (check / list / lookupSubjects / lookupResources). |\n| `RelationSchema` | Compiles the relation-schema DSL standalone (validated schemas reusable across resolvers). |\n| `Relations*Schemas` | Schema builders for the resolver's shapes (three validation backends). |\n| `parseRelationSchemaShape`, `parseObjectRef`, `parseSubjectRef`, `parseTuple` | Standalone parsers/validators for schema documents, `type:id` refs and tuples. |\n| `buildAdmissionKey` | Builds the `type` + `relation` + `subjectType` admission key the compiled schema indexes by. |\n\nSubpath **`@alexify/kerberos/tests`** (dev/test only — not loaded by the main entry):\n\n| Export | Purpose |\n| ------ | ------- |\n| `KerberosTest`, `KerberosTests` | Cerbos-style declarative test runner. |\n| `PrincipalMock`, `PrincipalsMock`, `ResourceMock`, `ResourcesMock` | Named fixtures for test suites. |\n| `*ZodSchemas`, `*JsonSchemas`, `*TypeBoxSchemas` | Schema builders for the test harness. |\n\nSubpath **`@alexify/kerberos/loader`** (Node-only [file/directory loader + versioned bundles](#loading-policies-from-files); browser bundlers substitute throwing stubs):\n\n| Export | Purpose |\n| ------ | ------- |\n| `loadPolicyDirectory`, `loadPolicyFile` | Read Kerberos JSON / Cerbos YAML+JSON policy files (+ `_schemas/`) into constructor inputs. |\n| `createPolicyBundle`, `writePolicyBundle`, `loadPolicyBundle` | Hash-stamped (SHA-256, content-addressed) policy bundles with load-time integrity verification. |\n| `promises` | The [asynchronous driver](#loading-policies-from-files) — the same four functions returning promises, reading files concurrently (`concurrency`, default 64). |\n| `KerberosLoaderError` | Typed error for I/O, format and bundle-integrity failures (carries `file`). |\n\nSubpath **`@alexify/kerberos/cerbos`** (the [Cerbos policy importer](#importing-cerbos-policies) — kept out of the main entry):\n\n| Export | Purpose |\n| ------ | ------- |\n| `importCerbosPolicies` | Cerbos YAML/JSON documents → `{ policies, derivedRoles }` serialized Kerberos documents. |\n| `celToExpr` | Translates one CEL expression into a `$expr`-compatible JavaScript expression string. |\n| `parseYamlDocuments` | The zero-dependency YAML-subset parser, standalone. |\n| `KerberosImportError` | Typed error for unsupported constructs (carries `line` for YAML errors). |\n\n## TypeScript\n\nKerberos.js ships hand-maintained types. By default every position is open — `kind` and `action` are `string`, `attr` is `Record<string, unknown>` — which is what you want for policies loaded from a store at runtime.\n\nWhen your resource kinds are known at compile time, declare them once and the whole surface narrows to them.\n\n### Declaring a schema\n\n```typescript\nimport { Kerberos, Effect, type KerberosPolicy } from '@alexify/kerberos';\n\ntype AppSchema = {\n  principal: {\n    roles: 'admin' | 'user';\n    attr: { department: string; clearance: number };\n  };\n  resources: {\n    document: { actions: 'view' | 'edit' | 'delete'; attr: { ownerId: string; status: 'draft' | 'published' } };\n    invoice: { actions: 'view' | 'approve'; attr: { amount: number } };\n  };\n};\n\nconst kerberos = new Kerberos<AppSchema>(policies, derivedRoles);\n```\n\nBoth keys are optional — declare only `resources` if you do not want to enumerate roles.\n\n### What it buys you\n\nThe resource kind drives everything else. `action`, `attr`, and the condition callbacks all narrow to the kind you named:\n\n```typescript\nawait kerberos.isAllowed({\n  principal: { id: 'u1', roles: ['admin'], attr: { department: 'eng', clearance: 3 } },\n  resource: { kind: 'document', id: 'd1', attr: { ownerId: 'u1', status: 'draft' } },\n  action: 'edit', // ✅ autocompleted from `document`'s actions\n});\n\nawait kerberos.isAllowed({\n  principal: { id: 'u1', roles: ['admin'] },\n  resource: { kind: 'document', id: 'd1' },\n  action: 'approve', // ❌ 'approve' belongs to `invoice`, not `document`\n});\n```\n\nPolicy documents are checked the same way — `resource:` discriminates the rules, so a typo in an action or a role is a compile error rather than a silent `EFFECT_DENY` at 3am:\n\n```typescript\nconst policy: KerberosPolicy<AppSchema> = {\n  resourcePolicy: {\n    version: 'default',\n    resource: 'document',\n    rules: [\n      { actions: ['view', 'edit'], effect: Effect.Allow, roles: ['admin'] },\n      {\n        actions: ['edit'],\n        effect: Effect.Allow,\n        roles: ['user'],\n        // R.attr is { ownerId: string; status: 'draft' | 'published' }\n        condition: { match: ({ R, P }) => R.attr?.ownerId === P.id && R.attr?.status === 'draft' },\n      },\n    ],\n  },\n};\n```\n\n`checkResources` keeps each batch entry typed independently, so a mixed batch still catches a wrong action per kind:\n\n```typescript\nconst { results } = await kerberos.checkResources({\n  principal: { id: 'u1', roles: ['user'] },\n  resources: [\n    { resource: { kind: 'document', id: 'd1' }, actions: ['view', 'edit'] },\n    { resource: { kind: 'invoice', id: 'i1' }, actions: ['approve'] },\n  ],\n});\n```\n\nThe second argument now selects the effect representation through overloads: `checkResources(args)` resolves `results[].actions` to `Effect`, and `checkResources(args, true)` to `boolean` — previously both were typed as the `Effect | boolean` union.\n\n### Schema helper types\n\nExported so you can build your own typed wrappers (an Express middleware, a React hook) over the same schema:\n\n| Type | Resolves to |\n| ---- | ----------- |\n| `ResourceKindOf<S>` | Union of declared resource kinds. |\n| `ActionOf<S, K>` | Actions for kind `K`; every action across all kinds when `K` is omitted. |\n| `ResourceAttrOf<S, K>` | Attribute bag of kind `K`. |\n| `PrincipalRoleOf<S>` / `PrincipalAttrOf<S>` | Declared principal roles / attributes. |\n| `RequestPrincipal<S>`, `RequestResource<S, K>`, `BaseRequest<S, K>` | Request shapes. |\n| `PolicyEvalRequest<S, K>` | The `{ P, R, V, C }` envelope a condition/variable/output callback receives. |\n| `CheckResourcesArgs<S>`, `CheckResourcesResponse<S, E>`, `PlanResourcesArgs<S, K>`, `PlanResourcesResponse<S>` | Method arguments and responses. |\n| `AnySchema` | The permissive default used when no schema is supplied. |\n\n> [!NOTE]\n> Typing is **compile-time only** — there is no runtime cost and no runtime enforcement. A schema constrains the policies and requests you write in TypeScript; it does not validate policies loaded from a cache at runtime. For that, use [schema validation](#schema-validation).\n\n`Effect` and `PlanKind` are const objects rather than TypeScript `enum`s, so the raw wire strings that a stored policy or a serialized plan actually carries stay assignable:\n\n```typescript\nconst rule = { actions: ['view'], effect: 'EFFECT_ALLOW', roles: ['user'] }; // ✅ no `Effect.Allow` needed\n```\n\n## Configuration Options\n\nThe Kerberos constructor accepts an optional third parameter with configuration options:\n\n```javascript\nconst kerberos = new Kerberos(policies, derivedRoles, {\n  logger: true, // Legacy console audit logging with summary + table + debug(json)\n  onError: 'deny', // 'throw' (default) or 'deny' — fail-closed evaluation errors\n  telemetry, // Optional: OpenTelemetry traces + metrics ({ api } or { tracer, meter })\n  cache, // Optional: any cache solution exposing get(key) (keyv, cacheable, ...)\n  cacheRetry: { attempts: 3 }, // Optional: retry policy for transient cache.get failures\n  codec, // Optional: (de)serialization codec for dynamic policies ({ jsep } or { deserialize })\n  relations, // Optional: ReBAC resolver for relation-backed derived roles\n  hooks: { beforeRequest, afterRequest }, // Optional: lifecycle hooks (awaited; throw to veto, return args to enrich)\n  hooksTimeoutMs: 500, // Optional: a hook that never settles fails as KerberosHookError instead of hanging\n  maxListeners: 10, // Optional: listener-leak warning threshold per event name (0 disables)\n  z, // Optional: validate with Zod\n  ajv, // Optional: validate with Ajv\n  typebox: Type, // Optional: switch Ajv validation to TypeBox builders\n  getCallId: () => `custom-${Date.now()}`, // Custom call ID generator (optional)\n});\n```\n\n### Options\n\n- **`logger`** (boolean | KerberosLogger): Enable audit logging.\n  - `true` keeps the legacy console behavior with `group + summary + table + debug(json)`\n  - `false` or omitted disables logging\n  - a custom `console`-like logger keeps the legacy table/json flow\n  - a structured logger such as `Pino` receives one structured audit entry per evaluated action\n  - Logging is pure observability: it never changes decisions or error behavior (that is [`onError`](#options)'s job), and a throwing logger is swallowed — it can never affect authorization.\n- **`onError`** (`'throw' | 'deny'`, default `'throw'`): What happens when policy **evaluation** fails at runtime (a throwing condition function, a failing cache backend, a ReBAC resolver error).\n  - `'throw'` propagates the error to the caller;\n  - `'deny'` fails closed: `isAllowed` resolves to `false`, `checkResources` to one all-DENY result per requested resource (positional parity with the request, like the per-resource fail-closed path — entries that cannot be echoed back from malformed arguments are skipped), `planResources` to a `KIND_ALWAYS_DENIED` filter.\n  - Malformed **arguments** are programming errors and always throw `KerberosValidationError`, regardless of this option.\n\n  ```javascript\n  // Fail-closed setup: evaluation errors deny instead of throwing.\n  const kerberos = new Kerberos(policies, derivedRoles, { onError: 'deny' });\n  ```\n\n- **`telemetry`** (KerberosTelemetryOptions): Enable OpenTelemetry traces and metrics. Pass `{ api }` (the `@opentelemetry/api` module) or `{ tracer, meter }` instances — see [OpenTelemetry](#opentelemetry).\n- **`cache`** (CacheLike): An optional cache used as a fallback source for dynamic/stored policies. Any object exposing a `get(key)` method is accepted (keyv, cacheable, cache-manager, ...). See [Caching / Storing policies](#caching--storing-policies).\n- **`cacheRetry`** (`{ attempts?, delayMs?, jitter?, timeoutMs?, onExhausted? }`, default `{ attempts: 3, delayMs: 25, jitter: true }`): Retry policy for `cache.get` failures. Attempts are spaced by full-jitter exponential backoff (`delayMs` base, doubling per attempt; `delayMs: 0` restores immediate retries); deterministic adapter errors (`TypeError`/`SyntaxError`) are never retried. `timeoutMs` (off by default) bounds each read attempt so a *hung* backend fails instead of hanging authorization. After the attempts are exhausted the failure surfaces as `KerberosCacheError` (and then follows `onError`) — unless `onExhausted: 'miss'` opts into **degraded mode**: the read counts as a cache miss and evaluation falls through to the remaining static sources, so a cache outage no longer disables statically-resolvable decisions (the degradation stays visible via the `kerberos.cache.requests` `error` metric and a guarded error log entry). `attempts: 1` disables retrying.\n- **`cacheKeyPrefix`** (`string`, default `''`): Prefix prepended to **every** cache key (policies *and* derived roles). Use it to namespace tenants or environments sharing one store — derived-roles documents are otherwise a single global `derivedRoles:<name>` namespace, so two tenants publishing the same definition name on a shared store would silently overwrite each other.\n- **`relationsTimeoutMs`** (`number`, off by default): Bounds each `relations.check` / `relations.list` call; a resolver that neither resolves nor rejects fails as `KerberosRelationsError` (following `onError`) instead of hanging the request.\n- **`audit`** (`{ includeMeta?: boolean }`): Engine-level audit enrichment. With `{ includeMeta: true }` and a logger attached, decision tracing runs for **every** request, so audit entries always carry `meta.resolution` and the `policy-miss` reason — audit completeness stops depending on each call site remembering the per-request `includeMeta` flag. The response stays gated on the request flag.\n- **`maxConcurrency`** (`number`, unbounded by default): Caps how many resources of a `checkResources` batch evaluate at once. Without it a 10k-resource batch launches 10k concurrent evaluation chains (each issuing its own cache reads) — memory spikes, event-loop saturation and a thundering herd on the cache backend. The built-in `RelationResolver` accepts the same option for its `lookupResources` candidate-verification fan-out.\n- **`codec`** (PolicyCodec): How cached policy documents are transformed before construction: `{ jsep }` enables the built-in safe `$expr` evaluator, `{ deserialize }` plugs in your own logic, and when omitted cached values are passed to policy constructors **as-is** — see [`codec` option — three modes](#codec-option--three-modes).\n- **`schemas`** (`{ enforcement?, definitions? }`): **Attribute schema enforcement** — Cerbos [`schemas`](https://docs.cerbos.dev/cerbos/latest/policies/schemas) parity. Resource policies declare `schemas.principalSchema` / `resourceSchema` refs (with optional `ignoreWhen.actions` globs); this option maps the refs to validators and picks the level: `'reject'` (default when set) denies requests whose attributes fail validation, `'warn'` reports without changing decisions, `'none'` disables (the Cerbos default when unconfigured). Failures are returned as Cerbos-shaped `validationErrors` (`{ path, message, source }`) on `checkResources` results — regardless of `includeMeta` — and reach the audit log. A definition may be a JSON Schema object (compiled with the `ajv` option), a Zod schema, or a validator function. See [Attribute schemas](#attribute-schemas-cerbos-schemas).\n- **`relations`** (KerberosRelationsResolver): ReBAC resolver used by relation-backed derived roles — any object with a `check(args, opts)` method (and an optional batched `list`). See [ReBAC (Relations)](#rebac-relations).\n- **`hooks`** (KerberosHooks): Lifecycle hooks — `beforeRequest`, `afterRequest`, `beforeResource`, `afterResource`, `onError` — awaited inside the request flow. A throwing hook vetoes the request as `KerberosHookError` (following `onError`); `beforeRequest` may return replacement arguments to **enrich** the request (re-validated, marked `enriched` on audit entries, spans and events); `onError`, a failed request's `afterRequest` and a failed resource's `afterResource` are swallowed and never mask the original error. Unknown names / non-functions throw at construction. See [Hooks & events](#hooks--events).\n- **`hooksTimeoutMs`** (`number`, off by default): Bounds every awaited hook invocation; a hook that neither resolves nor rejects fails as `KerberosHookError` (`timedOut: true`, then following that hook's throwing/swallowing rule) instead of hanging authorization — the hook counterpart of `relationsTimeoutMs`.\n- **`maxListeners`** (`number`, default `10`): Listener-leak detection for the events façade — the first subscription past this count on one event name logs a `console.warn` (subscribe once at startup, not per request). Never a limit; `0` disables the warning. The built-in `RelationResolver` accepts the same option.\n- **`z`**: Enables validation using the built-in Zod schema builders.\n- **`ajv`**: Enables validation using the built-in JSON Schema builders compiled with Ajv.\n- **`typebox`**: When used together with `ajv`, switches validation to the built-in TypeBox builders.\n- **`getCallId`** (function): Custom function to generate call IDs for audit tracking. \n  - **Default behavior**: Uses `crypto.randomUUID()` in Node.js, `window.crypto.randomUUID()` in browsers, or falls back to a pseudo UUID generator\n  - **Custom example**: `() => \\`req-\\${Date.now()}-\\${Math.random()}\\``\n\n### Using Pino for Production Logging\n\nIf you want machine-readable audit logs in production, pass a `Pino` instance as the `logger` option:\n\n```javascript\nimport pino from 'pino';\nimport { Kerberos } from '@alexify/kerberos';\n\nconst logger = pino({ level: 'info' });\n\nconst kerberos = new Kerberos(policies, derivedRoles, {\n  logger,\n});\n```\n\nWith `Pino`, Kerberos emits structured audit entries that include `callId`, `reqId`, `reqKind`, `principalId`, `principalRoles` (the role set the decision was based on — roles change over time, so past entries stay explainable), `resourceId`, `action`, `effect`, `outputs`, and `meta`. Fail-closed denials are part of the stream too: a resource whose evaluation failed inside a `checkResources` batch (and the `onError: 'deny'` fallback of `isAllowed`) logs its DENY decisions marked `reason: 'evaluation-error'`, and `planResources` results (`PlanResources.result`, with the filter kind) go out at **info** level like other decision entries — only lifecycle `*.start`/`*.finish` events sit at debug. This mode is better suited for production ingestion than the default console table output.\n\nIt also emits lifecycle logs such as `IsAllowed.start`, `IsAllowed.error`, `IsAllowed.finish`, `CheckResources.start`, `CheckResources.finish` and `PlanResources.*`. Errors are always logged, but whether they are rethrown or converted into a fail-closed response is decided solely by the [`onError`](#options) option — never by the logger.\n\n### Call ID Generation\n\nEvery request (`isAllowed` / `checkResources` / `planResources`) automatically generates a unique `kerberosCallId` for audit tracking:\n\n- **Node.js**: Uses `crypto.randomUUID()` \n- **Browser**: Uses `window.crypto.randomUUID()`\n- **Fallback**: Pseudo UUID v4 generator if crypto APIs are unavailable\n- **Custom**: Provide your own `getCallId` function for custom ID formats\n\nThis ID is included in both the response and audit logs for correlation.\n\n## Outputs\n\nKerberos.js supports outputs functionality similar to Cerbos. You can define output expressions that are evaluated when policy rules are activated or when conditions are not met. These outputs are included in the API response and can be used to provide detailed information about policy decisions.\n\n### Defining Outputs\n\nYou can add output functions to your policy rules:\n\n```javascript\nconst policyWithOutputs = {\n  resourcePolicy: {\n    version: 'default',\n    resource: 'system_access',\n    rules: [\n      {\n        name: 'working-hours-only',\n        actions: ['*'],\n        effect: Effect.Deny,\n        roles: ['*'],\n        condition: {\n          match: () => {\n            const now = new Date();\n            return now.getHours() > 18 || now.getHours() < 8;\n          }\n        },\n        output: {\n          when: {\n            ruleActivated: ({ P, R }) => ({\n              principal: P.id,\n              resource: R.id,\n              timestamp: new Date().toISOString(),\n              message: \"System can only be accessed between 0800 and 1800\"\n            }),\n            conditionNotMet: ({ P, R }) => ({\n              principal: P.id,\n              resource: R.id,\n              timestamp: new Date().toISOString(),\n              message: \"System can be accessed at this time\"\n            })\n          }\n        }\n      },\n      {\n        name: 'admin-access',\n        actions: ['*'],\n        effect: Effect.Allow,\n        roles: ['admin'],\n        output: {\n          when: {\n            ruleActivated: ({ P }) => ({\n              message: \"Admin access granted\",\n              admin: P.id\n            })\n          }\n        }\n      }\n    ]\n  }\n};\n```\n\n### Using checkResources with Outputs\n\nThe `checkResources` method returns outputs in the response:\n\n```javascript\nconst results = await kerberos.checkResources({\n  principal: {\n    id: 'john',\n    roles: ['user']\n  },\n  resources: [\n    {\n      resource: {\n        id: 'bastion_002',\n        kind: 'system_access'\n      },\n      actions: ['login']\n    }\n  ]\n});\n\nconsole.log(results);\n// {\n//   results: [\n//     {\n//       resource: { id: 'bastion_002', kind: 'system_access' },\n//       actions: { login: 'EFFECT_DENY' },\n//       outputs: [\n//         {\n//           src: 'resource.system_access.vdefault#working-hours-only',\n//           val: {\n//             principal: 'john',\n//             resource: 'bastion_002',\n//             timestamp: '2023-06-02T20:53:58.319Z',\n//             message: 'System can only be accessed between 0800 and 1800'\n//           }\n//         }\n//       ]\n//     }\n//   ]\n// }\n```\n\n### Output Function Syntax\n\nOutput functions are JavaScript functions that receive the request context and return any value:\n\n```javascript\n// Basic function syntax\n({ P, R, V, C }) => {\n  // Your logic here\n  return {\n    principal: P.id,\n    resource: R.kind,\n    timestamp: new Date().toISOString()\n  };\n}\n```\n\nAvailable context parameters:\n\n- **P**: Principal object with `id`, `roles`, and `attr`\n- **R**: Resource object with `id` and `kind`\n- **V**: Variables (computed values)\n- **C**: Constants (static values)\n\nOutput functions are called when:\n\n- **ruleActivated**: The rule matches and its condition is satisfied\n- **conditionNotMet**: The rule matches but its condition is not satisfied\n\nThe output `src` field reflects the policy type that produced it:\n\n- Resource policy example: `resource.expense.vdefault#rule-name`\n- Principal policy example: `principal.sally.vdefault#rule-name`\n\n## Decision metadata (includeMeta)\n\nWhen `includeMeta: true` is set, the response includes additional metadata about policy evaluation:\n\n```javascript\nconst results = await kerberos.checkResources({\n  principal: { \n    id: 'alice', \n    scope: 'acme.corp',\n    roles: ['employee'] \n  },\n  resources: [\n    {\n      resource: { \n        id: 'XX125', \n        kind: 'leave_request',\n        policyVersion: '20210210',\n        scope: 'acme.corp' \n      },\n      actions: ['view:public', 'approve']\n    }\n  ],\n  includeMeta: true\n});\n\nconsole.log(results);\n// {\n//   reqId: 'test-request',\n//   kerberosCallId: 'b9c4362d-b92a-4c2b-9d49-845f00d7a372',\n//   results: [\n//     {\n//       resource: {\n//         id: 'XX125',\n//         kind: 'leave_request',\n//         policyVersion: '20210210',\n//         scope: 'acme.corp'\n//       },\n//       actions: {\n//         'view:public': 'EFFECT_ALLOW',\n//         'approve': 'EFFECT_DENY'\n//       },\n//       outputs: [\n//         {\n//           src: 'resource.leave_request.v20210210/acme.corp#rule-001',\n//           val: 'create_allowed:john'\n//         }\n//       ],\n//       meta: {\n//         actions: {\n//           'view:public': {\n//             matchedPolicy: 'resource.leave_request.v20210210/acme.corp',\n//             matchedRule: 'resource.leave_request.v20210210/acme.corp#rule-001',\n//             matchedScope: 'acme.corp'\n//           },\n//           'approve': {\n//             matchedPolicy: 'resource.leave_request.v20210210/acme.corp',\n//             reason: 'condition-not-met'\n//           }\n//         },\n//         effectiveDerivedRoles: [\n//           'employee_that_owns_the_record',\n//           'any_employee'\n//         ],\n//         resolution: [\n//           { source: 'principal', id: 'alice', version: 'default',\n//             scopesSearched: ['acme.corp', 'acme', ''], matchedScope: null },\n//           { source: 'resource', id: 'leave_request', version: '20210210',\n//             scopesSearched: ['acme.corp', 'acme', ''], matchedScope: 'acme.corp' }\n//         ]\n//       }\n//     }\n//   ]\n// }\n```\n\nPer action, `meta.actions[action]` includes:\n\n- **matchedPolicy**: The policy source that produced the decision — a resource source such as `resource.expense.vdefault/acme.corp`, a principal source such as `principal.sally.vdefault/acme.corp`, or a role source such as `role.USER.vdefault`\n- **matchedRule**: The exact rule that produced the decision\n- **matchedScope**: The scope of the matched policy (present for scoped policies)\n- **reason** (denied actions only): why nothing allowed the action — `'rule-miss'` (no rule targeted the action / matched the principal's roles), `'condition-not-met'` (a rule targeted it but its condition failed), `'policy-miss'` (no applicable policy existed at all) or `'evaluation-error'` (the resource's evaluation rejected inside a `checkResources` batch and failed closed — paired with `errorName` so an outage is distinguishable from a policy DENY)\n\nAt the result level:\n\n- **effectiveDerivedRoles**: derived roles that activated for this resource\n- **resolution** (decision trace): every policy lookup that was attempted — `{ source, id, version, scopesSearched, matchedScope, origin? }` entries (with `origin: 'cache'` for cache-resolved policies), `{ source: 'derivedRoles', name, matched, origin? }` entries for every imported derived-roles set (`matched: false` = the import resolved nowhere — e.g. an evicted or corrupt cache document silently stopping rules from matching), plus `{ source: 'relations', name, relation, matched, reason? }` entries for [relation-backed derived roles](#rebac-relations). The same trace appears in [`planResources` meta](#query-plans-planresources).\n\n## Caching / Storing policies\n\nKerberos.js can resolve policies dynamically from a remote store (Redis, MongoDB, PostgreSQL, in-memory, ...) instead of loading every policy up front. Following the same delegating philosophy as the `logger` option, Kerberos stays **agnostic**: it does not implement caching, TTL or invalidation logic itself. You pass a `cache`, and Kerberos simply calls `cache.get(key)` when it needs a policy. Everything else — storage, layering, expiry, and multi-host invalidation — is delegated to dedicated solutions such as [`keyv`](https://keyv.org), [`cacheable`](https://cacheable.org) (`CacheSync`) and [`qified`](https://qified.org).\n\n### How it works (fallback layer)\n\nStatic policies passed to the constructor stay in memory; the `cache` is a fallback source. Resolution collects the **whole policy chain** along the scope search chain, with per-scope precedence:\n\n1. For each scope in the chain (most specific → base), look the policy up in memory first, then — only on a miss at that scope, and only if a `cache` is configured — call `await cache.get(key)`.\n2. On a hit, the JSON document is handled according to the `codec` option (see below).\n3. Every policy found participates in [per-action scope evaluation](#scopes-and-policy-versions) — a more specific policy decides first, and actions it does not decide fall through to less specific ones.\n4. If nothing matches, the action falls back to `EFFECT_DENY` (unchanged behavior).\n\n> [!NOTE]\n> Precedence is **per scope**: an in-memory policy wins at its own scope, but no longer shadows a *more specific* cached policy at a deeper scope. Hybrid deployments (static org-wide defaults in code + per-tenant overrides in the store) resolve the way scope specificity implies.\n\nCache keys follow this layout:\n\n| Policy type     | Key format                                |\n| --------------- | ----------------------------------------- |\n| Resource policy | `resource:<kind>:<version>:<scope>`       |\n| Principal policy| `principal:<id>:<version>:<scope>`        |\n| Role policy     | `role:<role>:<version>:<scope>`           |\n| Derived roles   | `derivedRoles:<name>`                     |\n\n`<version>` defaults to `default`, and `<scope>` is empty for unscoped policies (e.g. `resource:expense:default:`).\n\n### `CacheLike`\n\nThe only requirement is a single `get` method, so any cache backend works:\n\n```typescript\ntype CacheLike = {\n  get(key: string): unknown | Promise<unknown>;\n};\n```\n\n### `codec` option — three modes\n\nThe `codec` option controls how a value returned from the cache is transformed before being passed to the policy constructor:\n\n| Provided option | Behaviour |\n| --------------- | --------- |\n| `codec: { jsep }` | Kerberos uses the **built-in AST allowlist evaluator** with the pre-configured `jsep` instance you supply. `{ $expr: \"...\" }` descriptors are resolved into runtime evaluator functions. |\n| `codec: { deserialize }` | Your own **custom deserialization** function is called on the raw cached value. |\n| *(omit `codec`)* | The cached value is **passed as-is** to the policy constructor — no `{ $expr }` transformation. Use t","readmeFilename":"README.md"}