{"_id":"@api-policy/core","_rev":"3-566349f708398f63e8f725cde90a75c9","name":"@api-policy/core","dist-tags":{"latest":"1.0.2"},"versions":{"1.0.0":{"name":"@api-policy/core","version":"1.0.0","keywords":["policy","authorization","access-control","rbac","engine","ast","dsl","permissions"],"author":{"name":"minhtaimc"},"license":"MIT","_id":"@api-policy/core@1.0.0","maintainers":[{"name":"minhtaimc","email":"minhtaimc@gmail.com"}],"dist":{"shasum":"eba2a24b6ebba37adf2c9f84d4bc5bbae7325f6e","tarball":"https://registry.npmjs.org/@api-policy/core/-/core-1.0.0.tgz","fileCount":38,"integrity":"sha512-tWHCoMM5cTt+K8DgOdkv2HryiYjNfUcp8jOpx+oCVXMKcuWzLAKKQa6U9htlL0r+esPfIflbzlTjCK99EJ1N4Q==","signatures":[{"sig":"MEUCIQDogAo26GXFORkMheYHEbBge0hhnMjWX5vmxUBxDqXsSwIgYApE1R721y+vWoMr7bdbY1WFkgsX1iqX52DKZ8llH3w=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":106447},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"c7f2fd5117e9eddda25427c587be46fa837a4dae","scripts":{"test":"vitest run","build":"tsc","clean":"rm -rf dist","test:watch":"vitest watch","type-check":"tsc --noEmit"},"_npmUser":{"name":"minhtaimc","email":"minhtaimc@gmail.com"},"_npmVersion":"11.6.2","description":"Universal policy engine for authorization. Framework-agnostic, 4KB gzipped, zero dependencies.","directories":{},"_nodeVersion":"24.11.1","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/core_1.0.0_1773067308337_0.22180468449669166","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@api-policy/core","version":"1.0.1","keywords":["policy","authorization","access-control","rbac","engine","ast","dsl","permissions"],"author":{"name":"minhtaimc"},"license":"MIT","_id":"@api-policy/core@1.0.1","maintainers":[{"name":"minhtaimc","email":"minhtaimc@gmail.com"}],"dist":{"shasum":"6d5452d69d7eb184a21e75df31cc98de260f3c1e","tarball":"https://registry.npmjs.org/@api-policy/core/-/core-1.0.1.tgz","fileCount":38,"integrity":"sha512-vnk5GAjpAy23BgD0tXIOOu0QOdn6TdYFoenaJwkHgNQbDT70qVz8iwNXD1uahw2uuUwFSG900m358qfAMcS7CA==","signatures":[{"sig":"MEYCIQC+u+S7zd/+cQsD2sFzOSBbbr9fXdW3/RaEKyngHztrwQIhAPGNh5PEkL/5mMQxkQbYiZig/j5EMOO8nDBHU5chfgLd","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":106531},"main":"./dist/index.js","type":"module","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"gitHead":"1dfb68d711990705804119c4cdce30bcd56a3656","scripts":{"test":"vitest run","build":"tsc","clean":"rm -rf dist","test:watch":"vitest watch","type-check":"tsc --noEmit"},"_npmUser":{"name":"minhtaimc","email":"minhtaimc@gmail.com"},"_npmVersion":"11.6.2","description":"Universal policy engine for authorization. Framework-agnostic, 4KB gzipped, zero dependencies.","directories":{},"_nodeVersion":"24.11.1","_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/core_1.0.1_1773106937607_0.07205197253648477","host":"s3://npm-registry-packages-npm-production"}},"1.0.2":{"name":"@api-policy/core","version":"1.0.2","description":"Universal policy engine for authorization. Framework-agnostic, 4KB gzipped, zero dependencies.","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"build":"tsc","test":"vitest run","test:watch":"vitest watch","type-check":"tsc --noEmit","clean":"rm -rf dist"},"keywords":["policy","authorization","access-control","rbac","engine","ast","dsl","permissions"],"author":{"name":"minhtaimc"},"license":"MIT","gitHead":"16ba5dc244324cebdcfc88b4c02266c9b22f4121","_id":"@api-policy/core@1.0.2","_nodeVersion":"24.15.0","_npmVersion":"11.12.1","dist":{"integrity":"sha512-4oeJMxFzK6gY2EOsGmujnDBWwt7xzC5lM2MtfP0hNIMfW6ExF4p1YEWuaDmJbfmDUgAhs9RN5pAuyQwDxqybeA==","shasum":"361542b741410c6fbd19f71128c540afe0a1be35","tarball":"https://registry.npmjs.org/@api-policy/core/-/core-1.0.2.tgz","fileCount":38,"unpackedSize":106636,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIER0RHrPy6hj9iuCt0ZenxMEhf53S76+iiva2NN9AyccAiBSDECZzUhnyY3J41yNX96wGi4iCQUiavQxxkp3HHEyIg=="}]},"_npmUser":{"name":"minhtaimc","email":"minhtaimc@gmail.com"},"directories":{},"maintainers":[{"name":"minhtaimc","email":"minhtaimc@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/core_1.0.2_1781191455255_0.7834983707189156"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-09T14:41:48.261Z","modified":"2026-06-11T15:24:15.565Z","1.0.0":"2026-03-09T14:41:48.495Z","1.0.1":"2026-03-10T01:42:17.759Z","1.0.2":"2026-06-11T15:24:15.420Z"},"author":{"name":"minhtaimc"},"license":"MIT","keywords":["policy","authorization","access-control","rbac","engine","ast","dsl","permissions"],"description":"Universal policy engine for authorization. Framework-agnostic, 4KB gzipped, zero dependencies.","maintainers":[{"name":"minhtaimc","email":"minhtaimc@gmail.com"}],"readme":"# @api-policy/core\n\nUniversal policy engine for Node.js authorization. Framework-agnostic, 4KB gzipped, zero dependencies.\n\n```bash\nnpm install @api-policy/core\n```\n\n## Overview\n\nBuild authorization policies as composable AST expressions, then evaluate them against a subject (user) and resource.\n\n```ts\nimport { and, or, role, perm, owner, evaluate } from '@api-policy/core'\n\nconst WRITE = 1 << 1\n\n// admin OR (has WRITE permission AND owns the resource)\nconst policy = or(\n  role('admin'),\n  and(perm(WRITE), owner('authorId'))\n)\n\nconst allowed = evaluate(policy, {\n  subject: { id: 'user-1', role: 'editor', permissions: WRITE },\n  resource: { authorId: 'user-1' },\n})\n// → true\n```\n\n## Builders\n\nBuild policies by composing these primitives:\n\n| Builder | Description |\n|---------|-------------|\n| `role(name)` | `subject.role === name` |\n| `perm(mask)` | `(subject.permissions & mask) === mask` — ALL bits must match |\n| `owner(field?)` | `resource[field] === subject.id` — field defaults to `'ownerId'` |\n| `sameTenant()` | `subject.tenantId === resource.tenantId` |\n| `inTenant(id)` | `resource.tenantId === id` |\n| `custom(fn)` | Arbitrary predicate `(ctx: PolicyContext) => boolean` |\n| `and(...nodes)` | All must pass |\n| `or(...nodes)` | At least one must pass |\n| `not(node)` | Negation |\n\n## API\n\n### `evaluate(policy, ctx)`\n\nEvaluates a policy against a context. Returns `boolean`.\n\n```ts\nimport { evaluate } from '@api-policy/core'\n\nconst allowed = evaluate(policy, {\n  subject: {\n    id: 'user-123',\n    role: 'editor',\n    permissions: 0b0011,   // READ | WRITE\n    tenantId: 'tenant-abc',\n  },\n  resource: {\n    authorId: 'user-123',\n    tenantId: 'tenant-abc',\n  },\n})\n```\n\n**`PolicyContext` shape:**\n\n```ts\ninterface PolicyContext {\n  subject: {\n    id: string\n    role?: string\n    permissions?: number\n    tenantId?: string\n    [key: string]: unknown\n  }\n  resource?: {\n    [key: string]: unknown\n  }\n}\n```\n\n### `explain(policy, ctx)`\n\nSame as `evaluate`, but returns a detailed trace — useful for debugging why a policy allowed or denied.\n\n```ts\nimport { explain } from '@api-policy/core'\n\nconst result = explain(policy, { subject, resource })\n\nconsole.log(result)\n// {\n//   allowed: true,\n//   policyString: \"or(role('admin'), and(owner('authorId'), perm(2)))\",\n//   steps: [\n//     { description: \"or(...) [2 children]\", result: true },\n//     { description: \"role('admin')\", result: false, details: \"user.role = editor\" },\n//     { description: \"and(...) [2 children]\", result: true },\n//     { description: \"owner('authorId')\", result: true, details: \"resource.authorId = user-1, user.id = user-1\" },\n//     { description: \"perm(2)\", result: true, details: \"user.permissions = 3, required = 2\" },\n//   ],\n//   evaluationTime: 0.021\n// }\n```\n\n### `compileToBranches(policy)` + `checkCompiled(compiled, ctx)`\n\nCompile a policy to DNF (Disjunctive Normal Form) for repeated evaluation. Useful when the same policy is checked many times.\n\n```ts\nimport { compileToBranches, checkCompiled } from '@api-policy/core'\n\nconst compiled = compileToBranches(policy)  // compile once\n\n// check many times\nconst allowed = checkCompiled(compiled, ctx)\n```\n\nCompilation is capped at 64 branches. If the policy would exceed that, `compiled.isFallback = true` and evaluation falls back to `evaluate()`.\n\n### `normalizePolicy(policy)`\n\nReduce a policy to canonical form: flattens nested `and`/`or`, applies De Morgan's laws to `not`, and sorts children deterministically.\n\n```ts\nimport { normalizePolicy } from '@api-policy/core'\n\n// not(and(A, B)) → or(not(A), not(B))\nconst normalized = normalizePolicy(policy)\n```\n\n### `policyToString(policy)` / `policiesEqual(a, b)`\n\nCanonical string representation and structural equality check.\n\n```ts\nimport { policyToString, policiesEqual } from '@api-policy/core'\n\npolicyToString(or(role('admin'), perm(2)))\n// → \"or(perm(2), role('admin'))\"\n\npoliciesEqual(and(role('admin'), perm(1)), and(perm(1), role('admin')))\n// → true  (order-independent)\n```\n\n## Examples\n\n### RBAC\n\n```ts\nconst adminOnly = role('admin')\nconst editorOrAdmin = or(role('editor'), role('admin'))\n```\n\n### Permission bitmask\n\n```ts\nconst READ   = 1 << 0  // 1\nconst WRITE  = 1 << 1  // 2\nconst DELETE = 1 << 2  // 4\n\nconst canEdit   = perm(WRITE)\nconst canDelete = perm(DELETE)\nconst canReadAndWrite = perm(READ | WRITE)  // both bits must be set\n```\n\n### Owner-only access\n\n```ts\n// User can only access their own resources\nconst ownResourceOnly = owner('ownerId')\n\n// Custom owner field\nconst ownPost = owner('authorId')\n```\n\n### Multi-tenant isolation\n\n```ts\n// Must be in the same tenant as the resource\nconst sameTenantPolicy = and(perm(READ), sameTenant())\n```\n\n### Complex policies\n\n```ts\n// admin can do anything; editors can write if they own the resource\nconst editPolicy = or(\n  role('admin'),\n  and(role('editor'), perm(WRITE), owner('authorId'))\n)\n\n// Public read, owner or admin can write\nconst resourcePolicy = or(\n  perm(READ),\n  role('admin'),\n  and(perm(WRITE), owner())\n)\n```\n\n### Custom predicates\n\n```ts\nconst approvedOnly = and(\n  perm(READ),\n  custom(ctx => ctx.resource?.status === 'approved')\n)\n```\n\n## Permission bitmask convention\n\nPermissions are checked with exact-match bitwise AND: `(subject.permissions & mask) === mask`.\n\nThis means **all bits in the mask must be set**. A user with `permissions = READ | WRITE` passes `perm(READ)`, `perm(WRITE)`, and `perm(READ | WRITE)` — but not `perm(DELETE)`.\n\nRecommended constants:\n\n```ts\nexport const PERM = Object.freeze({\n  READ:    1 << 0,  // 1\n  WRITE:   1 << 1,  // 2\n  DELETE:  1 << 2,  // 4\n  APPROVE: 1 << 3,  // 8\n  EXECUTE: 1 << 4,  // 16\n})\n```\n\n## Use with @api-policy/server\n\n`@api-policy/core` uses `subject` (single role, flat permissions). `@api-policy/server` uses `UserContext` (multiple roles, per-resource permission map). Bridge them with `toSubject()`:\n\n```ts\nimport { evaluate, or, role, owner } from '@api-policy/core'\nimport { toSubject } from '@api-policy/server'\n\nconst allowed = evaluate(\n  or(role('admin'), owner('authorId')),\n  {\n    subject: toSubject(ctx.user, 'post'),  // picks user.perms['post']\n    resource: post,\n  }\n)\n```\n\n## License\n\nMIT\n","readmeFilename":"README.md"}