{"_id":"@abolfazl-khalaj/permission-react","_rev":"4-a78dff97de676c0435f5b739f3b47acf","name":"@abolfazl-khalaj/permission-react","dist-tags":{"latest":"0.2.1"},"versions":{"0.1.0":{"name":"@abolfazl-khalaj/permission-react","version":"0.1.0","license":"MIT","_id":"@abolfazl-khalaj/permission-react@0.1.0","maintainers":[{"name":"abolfazl-khalaj","email":"abolfazlkhalaj.dev@gmail.com"}],"dist":{"shasum":"10ef380d9be19d9e5f659b1e3f0b431db69f6d43","tarball":"https://registry.npmjs.org/@abolfazl-khalaj/permission-react/-/permission-react-0.1.0.tgz","fileCount":52,"integrity":"sha512-JuPfA11Qx8hr0QxRNyE0xKNhlBor9BxfJHpP7zB+MRbnLpiyJsTgfhpBymEM3dyQ2mcS3CCsBR2RxaBYSpfXdw==","signatures":[{"sig":"MEUCIQDL82LqtN3bKdDtVoeygU03CuuV1Vwdp4lgjz5wFyfqWQIgHIwTRhbvYm+5yKkClrb/VISaNyrX7bQahvsCnq26a68=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":50514},"type":"module","engines":{"node":">=20"},"private":false,"scripts":{"dev":"pnpm --filter @abolfazl-khalaj/permission-react dev","docs":"pnpm --filter @abolfazl-khalaj/permission-react docs","lint":"eslint .","test":"pnpm --filter @abolfazl-khalaj/permission-react test","build":"pnpm --filter @abolfazl-khalaj/permission-react build","clean":"pnpm --filter @abolfazl-khalaj/permission-react clean","format":"prettier --write .","prepare":"husky","release":"pnpm build && changeset publish","lint:fix":"eslint . --fix","changeset":"changeset","typecheck":"pnpm --filter @abolfazl-khalaj/permission-react typecheck","test:watch":"pnpm --filter @abolfazl-khalaj/permission-react test:watch","format:check":"prettier --check .","version-packages":"changeset version"},"_npmUser":{"name":"abolfazl-khalaj","email":"abolfazlkhalaj.dev@gmail.com"},"_npmVersion":"11.1.0","description":"Monorepo for the package-permission ecosystem","directories":{},"lint-staged":{"*.{ts,tsx}":["eslint --fix","prettier --write"],"*.{json,md,yml,yaml}":["prettier --write"]},"_nodeVersion":"24.13.0","_hasShrinkwrap":false,"packageManager":"pnpm@9.15.0","devDependencies":{"husky":"^9.1.7","eslint":"^9.17.0","globals":"^15.14.0","prettier":"^3.4.2","@eslint/js":"^9.17.0","typescript":"^5.7.2","lint-staged":"^15.2.11","@changesets/cli":"^2.27.11","typescript-eslint":"^8.18.2","eslint-config-prettier":"^9.1.0","eslint-plugin-react-hooks":"^5.1.0"},"_npmOperationalInternal":{"tmp":"tmp/permission-react_0.1.0_1786008362173_0.4258725496928206","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@abolfazl-khalaj/permission-react","version":"0.1.1","keywords":["react","permissions","authorization","access-control","rbac","acl","hooks","typescript"],"author":{"name":"abolfazl-khalaj"},"license":"MIT","_id":"@abolfazl-khalaj/permission-react@0.1.1","maintainers":[{"name":"abolfazl-khalaj","email":"abolfazlkhalaj.dev@gmail.com"}],"dist":{"shasum":"10a45aa1b98f2e6d0015f0467790dfa0931b8db4","tarball":"https://registry.npmjs.org/@abolfazl-khalaj/permission-react/-/permission-react-0.1.1.tgz","fileCount":9,"integrity":"sha512-FjJV4J73MvNISWxiPxTX4DO4AKmZxXF4znti7EFWkw8rt7KwVi9+/mEo8Gg52VV4Tsl5URSp0hb+4X+r9lgAwg==","signatures":[{"sig":"MEUCIG/a6dDCaQ3lVylhmbCpPk1EXVPe86TCyPco1h9PLFNCAiEAnfD/YU8XhYmVwrxyKFBrV1lreIUtCoStcrZmkI1+byk=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":66804},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"scripts":{"dev":"tsup --watch","docs":"typedoc","test":"vitest run","build":"tsup","clean":"rimraf dist coverage docs/api","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"abolfazl-khalaj","email":"abolfazlkhalaj.dev@gmail.com"},"_npmVersion":"11.1.0","description":"React permission primitives for declarative access control","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vite":"^6.0.6","jsdom":"^25.0.1","react":"^19.0.0","rimraf":"^6.0.1","vitest":"^3.0.5","typedoc":"^0.28.5","react-dom":"^19.0.0","typescript":"^5.7.2","@types/react":"^19.0.2","@types/react-dom":"^19.0.2","@vitejs/plugin-react":"^4.3.4","@testing-library/react":"^16.1.0","@testing-library/jest-dom":"^6.6.3"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0"},"peerDependenciesMeta":{"react-dom":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/permission-react_0.1.1_1786008714018_0.33940954395356737","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@abolfazl-khalaj/permission-react","version":"0.2.0","keywords":["react","permissions","authorization","access-control","rbac","acl","hooks","typescript"],"author":{"name":"abolfazl-khalaj"},"license":"MIT","_id":"@abolfazl-khalaj/permission-react@0.2.0","maintainers":[{"name":"abolfazl-khalaj","email":"abolfazlkhalaj.dev@gmail.com"}],"dist":{"shasum":"6f4947cef9cdda8fc398bd0e72a2d741e4b3a3af","tarball":"https://registry.npmjs.org/@abolfazl-khalaj/permission-react/-/permission-react-0.2.0.tgz","fileCount":7,"integrity":"sha512-DLQ2xvMdiMDP5jw2ussezrHqTXkF0jH35KuB8VQ60rBYubVdijpNMjol93fXeph+q2u11u8x3okLBH6DtI6AgA==","signatures":[{"sig":"MEUCIQCFgVZa2pchd1dWHLtI8VkP0PmMVQFpLQLPrYOFtjTH1AIgHGmZhtObwzk5Kj7D6N5M/24fUVc2R12LJ5ziWTPHWLE=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":74932},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"scripts":{"dev":"tsup --watch","docs":"typedoc","test":"vitest run","build":"tsup","clean":"rimraf dist coverage docs/api","typecheck":"tsc --noEmit","test:watch":"vitest","prepublishOnly":"pnpm run build"},"_npmUser":{"name":"abolfazl-khalaj","email":"abolfazlkhalaj.dev@gmail.com"},"_npmVersion":"11.1.0","description":"React permission primitives for declarative access control","directories":{},"sideEffects":false,"_nodeVersion":"24.13.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","vite":"^6.0.6","jsdom":"^25.0.1","react":"^19.0.0","rimraf":"^6.0.1","vitest":"^3.0.5","typedoc":"^0.28.5","react-dom":"^19.0.0","typescript":"^5.7.2","@types/react":"^19.0.2","@types/react-dom":"^19.0.2","@vitejs/plugin-react":"^4.3.4","@testing-library/react":"^16.1.0","@testing-library/jest-dom":"^6.6.3"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0"},"peerDependenciesMeta":{"react-dom":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/permission-react_0.2.0_1786167963302_0.7865680398341246","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@abolfazl-khalaj/permission-react","version":"0.2.1","description":"TypeScript-first React authorization primitives for permission-based UI access control","license":"MIT","author":{"name":"abolfazl-khalaj"},"homepage":"https://www.npmjs.com/package/@abolfazl-khalaj/permission-react","bugs":{"url":"https://github.com/abolfazl-khalaj/package-permission/issues"},"repository":{"type":"git","url":"git+https://github.com/abolfazl-khalaj/package-permission.git","directory":"packages/react"},"keywords":["react","permissions","authorization","access-control","rbac","acl","hooks","typescript"],"sideEffects":false,"type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"publishConfig":{"access":"public"},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","typecheck":"tsc --noEmit","docs":"typedoc","clean":"rimraf dist coverage docs/api","prepublishOnly":"pnpm run build"},"peerDependencies":{"react":"^18.0.0 || ^19.0.0","react-dom":"^18.0.0 || ^19.0.0"},"peerDependenciesMeta":{"react-dom":{"optional":true}},"devDependencies":{"@testing-library/jest-dom":"^6.6.3","@testing-library/react":"^16.1.0","@types/react":"^19.0.2","@types/react-dom":"^19.0.2","@vitejs/plugin-react":"^4.3.4","jsdom":"^25.0.1","react":"^19.0.0","react-dom":"^19.0.0","rimraf":"^6.0.1","tsup":"^8.3.5","typedoc":"^0.28.5","typescript":"^5.7.2","vite":"^6.0.6","vitest":"^3.0.5"},"_id":"@abolfazl-khalaj/permission-react@0.2.1","_nodeVersion":"24.13.0","_npmVersion":"11.1.0","dist":{"integrity":"sha512-go75o75zsF2ey//XITgnke25kf4yZu4CQpZYuV5O+EHRm6FItR8K68KkA6vkndKcjv1rMOOH48nY/PdR2qjKyg==","shasum":"09ad05b8ebcd90a46bc936215dfbdf70a983bd97","tarball":"https://registry.npmjs.org/@abolfazl-khalaj/permission-react/-/permission-react-0.2.1.tgz","fileCount":7,"unpackedSize":78009,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIEaqTA3bhnOUFnXkSDQv27jOp2/rKkT/vFSKgxe5dKyRAiAdt0E0SOgvpltrcvZK+H8BOCbbCU1fBOcPBcUFmbFQ+w=="}]},"_npmUser":{"name":"abolfazl-khalaj","email":"abolfazlkhalaj.dev@gmail.com"},"directories":{},"maintainers":[{"name":"abolfazl-khalaj","email":"abolfazlkhalaj.dev@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/permission-react_0.2.1_1786170449511_0.9227303044665016"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-06T09:26:01.837Z","modified":"2026-08-08T06:27:29.850Z","0.1.0":"2026-08-06T09:26:02.310Z","0.1.1":"2026-08-06T09:31:54.158Z","0.2.0":"2026-08-08T05:46:03.472Z","0.2.1":"2026-08-08T06:27:29.706Z"},"author":{"name":"abolfazl-khalaj"},"license":"MIT","keywords":["react","permissions","authorization","access-control","rbac","acl","hooks","typescript"],"description":"TypeScript-first React authorization primitives for permission-based UI access control","maintainers":[{"name":"abolfazl-khalaj","email":"abolfazlkhalaj.dev@gmail.com"}],"readme":"# @abolfazl-khalaj/permission-react\n\n**Enterprise permission & authorization primitives for React applications.**\n\nTypeScript-first access control for buttons, menus, pages, and features — without scattering `user.permissions.includes(...)` across your codebase.\n\n```bash\nnpm install @abolfazl-khalaj/permission-react\n```\n\n<p align=\"center\">\n  <a href=\"https://www.npmjs.com/package/@abolfazl-khalaj/permission-react\"><img src=\"https://img.shields.io/npm/v/@abolfazl-khalaj/permission-react.svg?style=flat-square\" alt=\"npm version\" /></a>\n  <a href=\"https://www.npmjs.com/package/@abolfazl-khalaj/permission-react\"><img src=\"https://img.shields.io/npm/dm/@abolfazl-khalaj/permission-react.svg?style=flat-square\" alt=\"npm downloads\" /></a>\n  <a href=\"https://github.com/abolfazl-khalaj/package-permission/blob/main/LICENSE\"><img src=\"https://img.shields.io/npm/l/@abolfazl-khalaj/permission-react.svg?style=flat-square\" alt=\"license\" /></a>\n  <img src=\"https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white\" alt=\"TypeScript\" />\n  <img src=\"https://img.shields.io/badge/React-18%20%7C%2019-61DAFB?style=flat-square&logo=react&logoColor=black\" alt=\"React 18 and 19\" />\n  <img src=\"https://img.shields.io/badge/bundle-ESM%20%2B%20CJS-success?style=flat-square\" alt=\"ESM and CJS\" />\n</p>\n\n| | |\n| --- | --- |\n| **Package** | `@abolfazl-khalaj/permission-react` |\n| **Peers** | React `^18 \\|\\| ^19` |\n| **License** | MIT |\n| **Output** | Tree-shakeable ESM + CJS (`sideEffects: false`) |\n\n---\n\n## Contents\n\n- [Introduction](#1-introduction)\n- [Features](#2-features)\n- [Installation](#3-installation)\n- [Quick Start](#4-quick-start)\n- [PermissionProvider](#5-permissionprovider)\n- [Subject Model](#6-subject-model)\n- [usePermission](#7-usepermission)\n- [Can Component](#8-can-component)\n- [Route Protection](#9-route-protection)\n- [Enterprise Example](#10-real-enterprise-example)\n- [Architecture](#11-architecture)\n- [Best Practices](#12-best-practices)\n- [Security & Limitations](#13-security--limitations)\n- [Migration Guide](#14-migration-guide)\n- [Troubleshooting](#15-troubleshooting)\n- [API Reference](#16-api-reference)\n- [Roadmap](#17-roadmap)\n\n---\n\n## 1. Introduction\n\n### What is this package?\n\n`@abolfazl-khalaj/permission-react` is a React adapter around a framework-agnostic permission **engine** (living under `src/core`).\n\nYou provide a **subject** (the current user: id, roles, permissions) and get:\n\n| Capability | API |\n| --- | --- |\n| Imperative checks | `can`, `hasRole`, … |\n| Declarative UI gates | `<Can />` |\n| Route-level gates | `<PermissionRoute />` |\n| Async backend loading | `permissionLoader`, `resolver` |\n| Structured debugging | `explain`, `debug` |\n\n### Who is it for?\n\n- Frontend teams building dashboards, admin panels, and SaaS products\n- Apps where authorization rules come from a backend (JWT, session, API)\n- Codebases that need typed, testable, centralized permission logic\n\n### What problem does it solve?\n\nWithout a library, authorization usually becomes:\n\n```ts\nif (user.permissions.includes('user.delete')) {\n  // show delete button\n}\n```\n\nThat pattern does not scale:\n\n- duplicated checks across components\n- no shared wildcard / role inheritance rules\n- hard to show loading / denied / disabled UI consistently\n- easy to flash unauthorized UI before permissions load\n- difficult to explain *why* a check failed\n\nThis package centralizes those decisions behind one provider and one engine.\n\n### Authentication vs authorization\n\n| Concept | Question | Typical source |\n| --- | --- | --- |\n| **Authentication** | Who is this user? | Login, JWT, session cookie |\n| **Authorization** | What may this user do? | Roles, permissions, policies |\n\n> This library is an **authorization** layer. It assumes you already know who the user is.\n\n### RBAC, permissions, and ABAC\n\n| Model | Idea | Status in this package |\n| --- | --- | --- |\n| **RBAC** | Access via roles (`admin`, `editor`) | Supported (`hasRole`, role inheritance) |\n| **Permission-based** | Access via fine-grained strings (`invoice.approve`) | Primary model (`can`, wildcards) |\n| **ABAC** | Access via attributes/conditions (`ownerId === user.id`) | **Types prepared**; evaluation not implemented yet |\n\nMost production UIs start with permissions + roles. Attribute-based rules are a later evolution — the public types (`PermissionRule`, `PermissionCondition`, `PermissionPolicy`, `PermissionQuery`) exist so that path does not require an API rewrite.\n\n---\n\n## 2. Features\n\n### Implemented\n\n| Feature | Description |\n| --- | --- |\n| **PermissionProvider** | Central authorization context and engine lifecycle |\n| **Subject model** | `id`, `roles`, `permissions`, `attributes` |\n| **Permission engine** | Framework-agnostic evaluation in `src/core` |\n| **`can` / `cannot` / `not`** | Sync permission checks |\n| **`canAny` / `canAll` / `hasNone`** | Multi-permission operators |\n| **Role helpers** | `hasRole`, `hasAnyRole`, `hasAllRoles`, `hasInheritedRole` |\n| **`check` / `explain`** | Structured results and decision reasons |\n| **Async API** | `canAsync`, `status`, `loading`, `error`, reload / cache clear |\n| **`<Can />`** | Declarative UI with `fallback`, `loading`, `errorFallback` |\n| **Modes** | `hidden` \\| `disable` \\| `readonly` |\n| **`<PermissionRoute />`** | Page gates with loading UI and optional redirect |\n| **Wildcards** | e.g. `users.*` |\n| **Role hierarchy** | Via `roleDefinitions` |\n| **Custom matcher** | `permissionMatcher` override |\n| **Loaders & resolvers** | `permissionLoader`, remote `resolver`, `cacheTime` |\n| **Debug** | Provider `debug` logging |\n| **TypeScript** | Strict public types |\n| **React 18 / 19** | Peer support |\n| **Tree-shaking** | ESM + CJS, `sideEffects: false` |\n\n### Roadmap\n\n| Item | Notes |\n| --- | --- |\n| Full ABAC **evaluation** | Types exist today |\n| Policy / condition runtime | Planned |\n| `@abolfazl-khalaj/permission-core` | Extract `src/core` for publish |\n| Next.js / Vue / Angular adapters | Planned |\n| SSR-safe navigation adapters | Beyond `window.location` |\n\n---\n\n## 3. Installation\n\nChoose your package manager:\n\n```bash\nnpm install @abolfazl-khalaj/permission-react\n```\n\n```bash\npnpm add @abolfazl-khalaj/permission-react\n```\n\n```bash\nyarn add @abolfazl-khalaj/permission-react\n```\n\n> **Requirement:** React `^18 || ^19`.\n\n---\n\n## 4. Quick Start\n\nThree steps to wire authorization into an app.\n\n### Step 1 — Create a subject (your user)\n\n```ts\nconst user = {\n  id: '1',\n  roles: ['admin'],\n  permissions: ['user.create', 'user.delete'],\n};\n```\n\nThis object is the authorization **subject**: the identity the engine evaluates against.\n\n### Step 2 — Wrap your app\n\n```tsx\nimport { PermissionProvider } from '@abolfazl-khalaj/permission-react';\n\nexport function Root() {\n  return (\n    <PermissionProvider subject={user}>\n      <App />\n    </PermissionProvider>\n  );\n}\n```\n\n`PermissionProvider` normalizes the subject, builds the engine/store, and exposes it through React context.\n\n### Step 3 — Check a permission\n\n```tsx\nimport { usePermission } from '@abolfazl-khalaj/permission-react';\n\nfunction CreateUserButton() {\n  const { can } = usePermission();\n\n  if (!can('user.create')) {\n    return null;\n  }\n\n  return <button type=\"button\">Create user</button>;\n}\n```\n\n| Line | Behavior |\n| --- | --- |\n| `usePermission()` | Reads the nearest provider context (throws if missing) |\n| `can('user.create')` | Asks the engine whether the subject holds that permission |\n| Conditional render | You control UI from the boolean result |\n\n**Declarative alternative:**\n\n```tsx\nimport { Can } from '@abolfazl-khalaj/permission-react';\n\n<Can permission=\"user.create\">\n  <button type=\"button\">Create user</button>\n</Can>\n```\n\n> **Expected:** If the subject lacks `user.create` and mode is `hidden` (default), the button is not rendered.\n\n---\n\n## 5. PermissionProvider\n\n### Purpose\n\n`PermissionProvider` is the root of the authorization tree.\n\n1. Accepts a subject (and optional async sources)\n2. Creates a framework-agnostic permission **store/engine**\n3. Publishes a stable API through context\n4. Keeps React components thin (no business rules in UI)\n\n### Props\n\n| Prop | Type | Default | Description |\n| --- | --- | --- | --- |\n| `subject` | `Subject` | `{}` | Current user snapshot |\n| `permissions` | `string[]` | — | Legacy shortcut; merged into subject permissions |\n| `defaultMode` | `'hidden' \\| 'disable' \\| 'readonly'` | `'hidden'` | Default denied behavior for `<Can />` |\n| `roleDefinitions` | `RoleDefinition[]` | — | Role inheritance + role-granted permissions |\n| `permissionMatcher` | `(required, available) => boolean` | wildcard matcher | Custom matching strategy |\n| `permissionLoader` | `(subject) => Promise<PermissionResponse>` | — | Load permissions/roles from backend |\n| `resolver` | `(permission, subject) => Promise<boolean>` | — | Per-permission remote check |\n| `cacheTime` | `number` (ms) | `0` | Cache TTL for loader/resolver results |\n| `debug` | `boolean` | `false` | Log permission decisions to the console |\n| `children` | `ReactNode` | — | App tree |\n\n### Static example\n\n```tsx\n<PermissionProvider\n  subject={{\n    id: '42',\n    roles: ['editor'],\n    permissions: ['posts.read', 'posts.write'],\n  }}\n  defaultMode=\"hidden\"\n>\n  <App />\n</PermissionProvider>\n```\n\n### Legacy `permissions` prop\n\nStill supported for simple apps:\n\n```tsx\n<PermissionProvider permissions={['posts.read', 'posts.write']}>\n  <App />\n</PermissionProvider>\n```\n\nPrefer `subject` for production systems — it carries `id`, `roles`, and `attributes` needed for caching and future ABAC.\n\n### Async backend loading\n\n```tsx\n<PermissionProvider\n  subject={{ id: currentUser.id }}\n  permissionLoader={async (subject) => {\n    const data = await api.getPermissions(subject.id);\n    return {\n      permissions: data.permissions,\n      roles: data.roles,\n    };\n  }}\n  cacheTime={5 * 60 * 1000}\n>\n  <App />\n</PermissionProvider>\n```\n\n> While loading, `usePermission().status` is `'loading'` (or `'idle'` before the first load starts). Use `<Can loading={...} />` to avoid unauthorized flashes.\n\n### Remote resolver\n\nUse when some checks must hit the server even if local grants miss:\n\n```tsx\n<PermissionProvider\n  subject={user}\n  permissions={user.permissions}\n  resolver={async (permission, subject) =>\n    api.checkPermission(subject.id, permission)\n  }\n  cacheTime={60_000}\n>\n  <App />\n</PermissionProvider>\n```\n\n**Resolution order for `canAsync(permission)`:**\n\n1. Local engine allow (if already resolved)\n2. Cached resolver result\n3. In-flight dedupe\n4. Call `resolver`\n\n---\n\n## 6. Subject Model\n\n### Shape\n\n```ts\ntype Subject = {\n  id?: string | number;\n  roles?: readonly string[];\n  permissions?: readonly string[];\n  attributes?: Readonly<Record<string, unknown>>;\n};\n```\n\n### Why a subject exists\n\nAuthorization is always evaluated **against someone**.\n\nBundling identity fields into one object:\n\n- matches how backends usually return users\n- gives the cache a stable key (`id`)\n- keeps roles and permissions together\n- leaves room for attributes (department, tenant, plan) without changing the React API\n\n### Why this is future-proof\n\nToday the engine primarily uses `permissions` and `roles`.  \n`attributes` is already on the subject so ABAC conditions can attach later without renaming the model.\n\nThe core layer (`src/core`) does not import React. The same subject/engine design is intended to move into `@abolfazl-khalaj/permission-core` and be reused by Vue/Next adapters.\n\n### Mapping a backend user to Subject\n\n**Backend response:**\n\n```json\n{\n  \"id\": 10,\n  \"role\": \"admin\",\n  \"permissions\": [\"users.*\", \"billing.view\"],\n  \"meta\": { \"department\": \"finance\" }\n}\n```\n\n**Transform:**\n\n```ts\nimport type { Subject } from '@abolfazl-khalaj/permission-react';\n\nconst subject: Subject = {\n  id: response.id,\n  roles: [response.role], // normalize singular → array\n  permissions: response.permissions,\n  attributes: response.meta,\n};\n```\n\n### Role definitions (inheritance)\n\n```tsx\n<PermissionProvider\n  subject={{ id: '1', roles: ['admin'], permissions: [] }}\n  roleDefinitions={[\n    { name: 'manager', permissions: ['reports.view'] },\n    {\n      name: 'admin',\n      inherits: ['manager'],\n      permissions: ['users.*'],\n    },\n  ]}\n>\n  <App />\n</PermissionProvider>\n```\n\n| Check | Result | Why |\n| --- | --- | --- |\n| `hasRole('admin')` | `true` | Direct |\n| `hasRole('manager')` | `true` | Inherited |\n| `hasInheritedRole('manager')` | `true` | Via inheritance only |\n| `can('reports.view')` | `true` | From manager role permissions |\n| `can('users.create')` | `true` | Wildcard from admin |\n\n---\n\n## 7. usePermission\n\nImperative authorization API. Must be called under `PermissionProvider`.\n\n```ts\nconst {\n  // subject snapshot\n  subject,\n  permissions,\n  roles,\n\n  // sync checks\n  can,\n  cannot,\n  not,\n  canAny,\n  canAll,\n  hasNone,\n  check,\n  explain,\n\n  // roles\n  hasRole,\n  hasAnyRole,\n  hasAllRoles,\n  hasInheritedRole,\n\n  // async / lifecycle\n  canAsync,\n  status,\n  loading,\n  error,\n  reloadPermissions,\n  clearPermissionCache,\n} = usePermission();\n```\n\n---\n\n### `can(permission, strategy?)`\n\nReturns whether the subject is allowed the permission(s).\n\n```ts\ncan('user.create');\ncan(['user.create', 'user.delete'], 'all'); // every permission\ncan(['user.create', 'user.delete'], 'any'); // at least one\n```\n\n| Parameter | Type | Default | Meaning |\n| --- | --- | --- | --- |\n| `permission` | `string \\| string[]` | — | Required permission(s) |\n| `strategy` | `'all' \\| 'any'` | `'all'` | How arrays are evaluated |\n\n**Returns:** `boolean`  \n**Use case:** Show/hide a feature in imperative code.\n\nWildcards are supported by default:\n\n```ts\n// subject.permissions = ['users.*']\ncan('users.create'); // → true\ncan('users.delete'); // → true\ncan('billing.edit'); // → false\n```\n\n---\n\n### `cannot` / `not`\n\nInverse of `can`.\n\n```ts\nif (cannot('billing.edit')) {\n  showUpgradeBanner();\n}\n\nif (not('billing.edit')) {\n  showUpgradeBanner();\n}\n```\n\n`not` is an engine-oriented alias of `cannot`.\n\n---\n\n### `canAny` / `canAll` / `hasNone`\n\n```ts\ncanAny(['user.create', 'user.update']); // at least one\ncanAll(['user.create', 'user.delete']); // every\nhasNone(['billing.edit', 'billing.delete']); // none granted\n```\n\n---\n\n### `check(permission, strategy?)`\n\nReturns a structured sync result:\n\n```ts\nconst result = check(['user.create', 'user.delete']);\n// {\n//   allowed: false,\n//   missing: ['user.delete']\n// }\n```\n\n---\n\n### `explain(permission)`\n\nReturns a **decision object** (useful for debugging):\n\n```ts\nexplain('users.create');\n// {\n//   allowed: true,\n//   permission: 'users.create',\n//   reason: 'permission_match',\n//   source: 'users.*'\n// }\n\nexplain('billing.edit');\n// {\n//   allowed: false,\n//   permission: 'billing.edit',\n//   reason: 'permission_missing'\n// }\n```\n\nReasons include: `permission_match`, `permission_missing`, `role_missing`, `condition_failed`, `denied`.\n\nEnable console logging with `debug` on the provider.\n\n---\n\n### Role helpers\n\n```ts\nhasRole('admin');\nhasAnyRole(['admin', 'editor']);\nhasAllRoles(['editor', 'member']);\nhasInheritedRole('manager'); // true only via inheritance, not direct assignment\n```\n\n---\n\n### Async lifecycle\n\n```ts\nstatus;  // 'idle' | 'loading' | 'resolved' | 'error'\nloading; // boolean shortcut for status === 'loading'\nerror;   // Error | null\n\nawait canAsync('invoice.delete');\nawait reloadPermissions();\nclearPermissionCache();\n```\n\n| API | Behavior |\n| --- | --- |\n| `canAsync` | Local allow first; otherwise resolver/loader path with cache |\n| `reloadPermissions` | Invalidate cache for subject and reload via `permissionLoader` |\n| `clearPermissionCache` | Drop cached loader/resolver entries for current subject |\n\n---\n\n## 8. Can Component\n\nDeclarative authorization keeps JSX readable and avoids repeating hooks in every leaf component.\n\n### Basic usage\n\n```tsx\n<Can permission=\"user.delete\">\n  <button type=\"button\">Delete</button>\n</Can>\n```\n\n> **Expected:** If denied and mode is `hidden` (default), children are not rendered.\n\n### Why a component?\n\n- Colocates authorization with UI\n- Standardizes denied / loading / error presentation\n- Works well in design systems (shared fallbacks)\n- Supports render props for advanced UI states\n\n### `fallback`\n\n```tsx\n<Can permission=\"user.delete\" fallback={<NoAccess />}>\n  <DeleteButton />\n</Can>\n```\n\nRendered when the check fails and mode is `hidden`.\n\n### `loading` / `errorFallback`\n\n```tsx\n<Can\n  permission=\"user.delete\"\n  loading={<Skeleton />}\n  errorFallback={<PermissionError />}\n  fallback={<NoAccess />}\n>\n  <DeleteButton />\n</Can>\n```\n\n| State | Rendered |\n| --- | --- |\n| provider `loading` / `idle` | `loading` (or `null` if omitted — avoids unauthorized flash) |\n| provider `error` | `errorFallback` (falls back to `fallback`) |\n| allowed | `children` |\n| denied + `hidden` | `fallback` |\n| denied + `disable` / `readonly` | hardened children |\n\n### `mode`\n\n```tsx\n{/* Hidden (default): remove from UI */}\n<Can permission=\"invoice.edit\" mode=\"hidden\" fallback={<Locked />}>\n  <EditInvoice />\n</Can>\n\n{/* Disable: visible but non-interactive */}\n<Can permission=\"invoice.edit\" mode=\"disable\">\n  <button type=\"button\">Edit</button>\n</Can>\n\n{/* Readonly: visible, read-only semantics (aria + blocked submit/click) */}\n<Can permission=\"invoice.edit\" mode=\"readonly\">\n  <InvoiceForm />\n</Can>\n```\n\n| Mode | User experience |\n| --- | --- |\n| `hidden` | Element is not shown |\n| `disable` | Element is shown but interaction is blocked (`disabled` / `aria-disabled`) |\n| `readonly` | Element is shown as read-only (`aria-readonly`, blocked submit/click) |\n\n`defaultMode` on the provider sets the default when `mode` is omitted.\n\n### Render functions\n\n```tsx\n<Can\n  permission=\"user.create\"\n  fallback={({ allowed, mode }) => <NoAccess mode={mode} />}\n>\n  {({ allowed }) => (allowed ? <CreateButton /> : null)}\n</Can>\n```\n\n### Strategy for multiple permissions\n\n```tsx\n<Can permission={['user.create', 'user.invite']} strategy=\"any\">\n  <InvitePanel />\n</Can>\n```\n\n---\n\n## 9. Route Protection\n\n`PermissionRoute` gates page-level content. It is **router-agnostic** (works with React Router, Next.js pages, or plain composition).\n\n```tsx\n<PermissionRoute\n  permission=\"dashboard.view\"\n  loading={<PageLoader />}\n  fallback={<Forbidden />}\n  redirect=\"/403\"\n>\n  <DashboardPage />\n</PermissionRoute>\n```\n\n### Props\n\n| Prop | Description |\n| --- | --- |\n| `permission` | Required permission(s) |\n| `strategy` | `'all'` (default) or `'any'` for arrays |\n| `loading` | Shown while provider/async check is pending |\n| `fallback` | Shown when unauthorized |\n| `redirect` / `redirectTo` | Optional browser redirect target when denied |\n\n### Behavior\n\n1. While permissions are loading → render `loading`\n2. If sync `can(...)` allows → render children\n3. Otherwise try `canAsync(...)` (resolver / delayed grants)\n4. If still denied → render `fallback`, and if `redirect`/`redirectTo` is set, navigate via `window.location.assign`\n\n**Admin section example:**\n\n```tsx\n<PermissionRoute permission=\"admin.access\" redirect=\"/unauthorized\">\n  <AdminLayout />\n</PermissionRoute>\n```\n\n---\n\n## 10. Real Enterprise Example\n\nImagine a SaaS dashboard with permissions:\n\n| Permission | Typical use |\n| --- | --- |\n| `dashboard.view` | Open the main dashboard |\n| `user.create` | Create users / users nav |\n| `user.delete` | Destructive user actions |\n| `invoice.approve` | Finance workflow |\n\n```tsx\nimport {\n  PermissionProvider,\n  PermissionRoute,\n  Can,\n  usePermission,\n} from '@abolfazl-khalaj/permission-react';\n\nconst subject = {\n  id: session.user.id,\n  roles: session.user.roles,\n  permissions: session.user.permissions,\n};\n\nexport function AppShell() {\n  return (\n    <PermissionProvider\n      subject={subject}\n      roleDefinitions={ROLE_CATALOG}\n      defaultMode=\"hidden\"\n      debug={import.meta.env.DEV}\n    >\n      <Shell />\n    </PermissionProvider>\n  );\n}\n\nfunction Shell() {\n  const { can, hasRole } = usePermission();\n\n  return (\n    <div className=\"layout\">\n      <aside>\n        {/* Sidebar visibility */}\n        <Can permission=\"dashboard.view\">\n          <NavLink to=\"/dashboard\">Dashboard</NavLink>\n        </Can>\n\n        <Can permission=\"user.create\">\n          <NavLink to=\"/users\">Users</NavLink>\n        </Can>\n\n        {hasRole('finance') ? (\n          <NavLink to=\"/invoices\">Invoices</NavLink>\n        ) : null}\n      </aside>\n\n      <main>\n        {/* Route protection */}\n        <PermissionRoute permission=\"dashboard.view\" fallback={<NoAccess />}>\n          <DashboardPage />\n        </PermissionRoute>\n      </main>\n    </div>\n  );\n}\n\nfunction UsersToolbar() {\n  return (\n    <div>\n      {/* Button protection */}\n      <Can permission=\"user.create\">\n        <button type=\"button\">Create user</button>\n      </Can>\n\n      <Can permission=\"user.delete\" mode=\"disable\" fallback={null}>\n        <button type=\"button\">Delete user</button>\n      </Can>\n    </div>\n  );\n}\n\nfunction InvoiceActions({ invoice }: { invoice: { id: string } }) {\n  const { can } = usePermission();\n\n  return (\n    <Can permission=\"invoice.approve\" fallback={<span>Awaiting permission</span>}>\n      <button type=\"button\" onClick={() => approve(invoice.id)}>\n        Approve\n      </button>\n      {/* Feature visibility */}\n      {can('invoice.approve') ? <AuditHint /> : null}\n    </Can>\n  );\n}\n```\n\nThis pattern keeps:\n\n| Concern | Approach |\n| --- | --- |\n| Sidebar | `<Can />` / `hasRole` |\n| Buttons | Declarative `<Can />` |\n| Routes | `<PermissionRoute />` |\n| Business checks | Out of random utilities |\n\n---\n\n## 11. Architecture\n\nHigh-level data flow:\n\n```text\n┌─────────────────────────────────────────────┐\n│              Application UI                 │\n│         (pages, buttons, menus)             │\n└─────────────────────┬───────────────────────┘\n                      │\n                      ▼\n┌─────────────────────────────────────────────┐\n│           PermissionProvider                │\n│     React adapter · context · lifecycle     │\n└─────────────────────┬───────────────────────┘\n                      │\n                      ▼\n┌─────────────────────────────────────────────┐\n│      Permission Store / Engine              │\n│           (src/core · no React)             │\n│                                             │\n│   ┌───────────┐  ┌──────────┐  ┌─────────┐  │\n│   │  Matchers │  │  Roles   │  │ Decisions│ │\n│   │ exact + * │  │ inherit  │  │ explain │  │\n│   └───────────┘  └──────────┘  └─────────┘  │\n│                                             │\n│   ┌─────────────────────────────────────┐   │\n│   │ Async: loader · resolver · cache    │   │\n│   └─────────────────────────────────────┘   │\n└─────────────────────┬───────────────────────┘\n                      │\n                      ▼\n┌─────────────────────────────────────────────┐\n│           Permission Decision               │\n│   can() · <Can /> · <PermissionRoute />     │\n└─────────────────────────────────────────────┘\n```\n\n### Why the engine is separated\n\n- UI libraries should not own authorization rules\n- The same core can power Vue / Next / Angular adapters later\n- Unit tests can target pure functions without rendering React\n- Bundle stays focused: React layer is a thin wrapper\n\n### Multi-framework direction\n\n```text\n@abolfazl-khalaj/permission-core   ← planned extraction of src/core\n         ▲\n         │\n@abolfazl-khalaj/permission-react  ← this package (ships today)\n@abolfazl-khalaj/permission-vue    ← planned\n@abolfazl-khalaj/permission-next   ← planned\n```\n\nToday, core code ships inside the React package but lives in an isolated `src/core` tree with **zero React imports**.\n\n---\n\n## 12. Best Practices\n\n### Do\n\n| Practice | Why |\n| --- | --- |\n| Load permissions from backend / token into a `Subject` | Single source of truth |\n| Use namespaced strings (`invoice.approve`, `users.*`) | Readable, scalable catalogs |\n| Put `PermissionProvider` near the top of the authenticated tree | One context for the app |\n| Prefer `<Can />` for UI, `can()` for logic | Clear separation |\n| Use `loading` UI with `permissionLoader` | Prevents unauthorized flashes |\n| Use `explain` / `debug` while developing rules | Faster diagnosis |\n| Version `roleDefinitions` with your backend | Avoid drift |\n\n### Don’t\n\n| Anti-pattern | Why |\n| --- | --- |\n| Mix auth redirects with authz in random components | Hard to reason about |\n| Hardcode the same permission in 20 files | Drift and typos |\n| Re-implement matching outside the engine | Divergent rules |\n| Assume permissions are ready before `status === 'resolved'` | Race conditions |\n| Depend on internal `src/core` paths | Not a public contract |\n\n**Suggested constants module:**\n\n```ts\nexport const Permissions = {\n  UserCreate: 'user.create',\n  UserDelete: 'user.delete',\n  InvoiceApprove: 'invoice.approve',\n} as const;\n```\n\n---\n\n## 13. Security & Limitations\n\n> **This package is for client-side / UI authorization only.**  \n> It does **not** replace backend authorization, API authorization, or server-side policy enforcement.\n\nTreat every permission check in the browser as a **UX and presentation control**:\n\n| Layer | Responsibility |\n| --- | --- |\n| This library | Hide, disable, or gate UI based on known grants |\n| Your backend | Enforce the same rules on every sensitive operation |\n\n### Security principles baked into the engine\n\n| Principle | Behavior |\n| --- | --- |\n| **Fail closed** | Unknown / missing permission → deny |\n| **Empty requirements** | `\"\"` never authorizes |\n| **Loading** | While `status` is `idle` / `loading`, `<Can />` does not render privileged children |\n| **Subject input** | Caller mutations of role/permission arrays do not affect a normalized snapshot |\n\n### What this library will not do\n\n- Encrypt or validate tokens\n- Prevent forged client permission arrays\n- Guarantee that a user cannot call your API without a grant\n- Replace CSRF, authn, rate limiting, or tenancy checks\n\nAlways re-check authorization on the server.\n\n---\n\n## 14. Migration Guide\n\n### Before\n\n```tsx\nfunction DeleteButton({ user }: { user: { permissions: string[] } }) {\n  if (!user.permissions.includes('user.delete')) {\n    return null;\n  }\n  return <button type=\"button\">Delete</button>;\n}\n```\n\nProblems: prop drilling, duplicated strings, no loading/disabled modes, no wildcards/roles.\n\n### After\n\n```tsx\n// root\n<PermissionProvider subject={mapUserToSubject(user)}>\n  <App />\n</PermissionProvider>\n\n// leaf\n<Can permission=\"user.delete\">\n  <button type=\"button\">Delete</button>\n</Can>\n```\n\nOr imperative:\n\n```tsx\nconst { can } = usePermission();\nreturn can('user.delete') ? <DeleteButton /> : null;\n```\n\n**Migration steps:**\n\n1. Map your auth user → `Subject`\n2. Wrap the authenticated tree with `PermissionProvider`\n3. Replace `includes` checks with `can` / `<Can />`\n4. Optionally introduce `permissionLoader` if permissions arrive asynchronously\n\n---\n\n## 15. Troubleshooting\n\n### `usePermission must be used within a PermissionProvider`\n\n**Cause:** Hook/component rendered outside the provider.\n\n**Fix:** Wrap the authenticated subtree (or the whole app after login) with `PermissionProvider`.\n\n---\n\n### Permission always denied\n\nChecklist:\n\n- Is `status` still `'loading'`? Wait or show `loading` UI.\n- Does the subject actually include the permission (or a matching wildcard)?\n- Are permissions coming from `permissionLoader` but the loader failed? Check `error`.\n- Did you pass `permissions={...}` *and* an empty `subject.permissions` unexpectedly?\n- For roles: did you configure `roleDefinitions` if you rely on inheritance?\n\n```ts\nconsole.log(usePermission().subject);\nconsole.log(usePermission().explain('user.delete'));\n```\n\n---\n\n### Wrong subject shape\n\n`roles` and `permissions` must be **arrays**. If the backend sends `role: \"admin\"`, convert it:\n\n```ts\nroles: Array.isArray(user.roles) ? user.roles : [user.role]\n```\n\n---\n\n### TypeScript: permission strings\n\nPermission type is `string`. For stricter apps, wrap your own union:\n\n```ts\ntype AppPermission = 'user.create' | 'user.delete' | 'invoice.approve';\ncan('user.create' as AppPermission);\n```\n\n---\n\n### Redirect does nothing in tests / SSR\n\n`PermissionRoute` uses `window.location.assign` when `redirect` / `redirectTo` is set. In non-browser environments, provide `fallback` UI instead of relying on navigation.\n\n---\n\n## 16. API Reference\n\n### Runtime exports\n\n| API | Kind | Purpose | Example |\n| --- | --- | --- | --- |\n| `PermissionProvider` | Component | Provide subject + engine/store | `<PermissionProvider subject={user}>` |\n| `usePermission` | Hook | Imperative authorization API | `const { can } = usePermission()` |\n| `Can` | Component | Declarative UI gate | `<Can permission=\"user.delete\">` |\n| `PermissionRoute` | Component | Page/section gate | `<PermissionRoute permission=\"admin.access\">` |\n\n### Common types\n\n| Type | Purpose |\n| --- | --- |\n| `Subject` | User snapshot for evaluation |\n| `Permission` / `PermissionList` / `PermissionInput` | Permission identifiers |\n| `PermissionMode` | `hidden` \\| `disable` \\| `readonly` |\n| `PermissionStatus` | `idle` \\| `loading` \\| `resolved` \\| `error` |\n| `RoleDefinition` | Role inheritance + role permissions |\n| `PermissionDecision` | Result of `explain()` |\n| `PermissionLoader` / `PermissionResolver` / `PermissionResponse` | Async integration |\n| `PermissionRuntimeApi` | Return type of `usePermission()` |\n| `CanProps` / `PermissionProviderProps` / `PermissionRouteProps` | Component props |\n| `PermissionRule` / `PermissionCondition` / `PermissionPolicy` / `PermissionQuery` | Future ABAC surface (types only) |\n\n> Internal modules under `src/core` are **not** public API.\n\n---\n\n## 17. Roadmap\n\n| Phase | Focus | Status |\n| --- | --- | --- |\n| **1** | React foundation: Provider, hooks, Can, routes, subject, engine | Done |\n| **2** | Advanced rules: wildcards, role hierarchy, explain/debug, operators | Done |\n| **3** | Async system: loader, resolver, cache, loading/error UI | Done |\n| **4** | ABAC evaluation + policy engine | Planned |\n| **5** | Extract `@abolfazl-khalaj/permission-core` | Planned |\n| **6** | Framework adapters (Next / Vue / Angular) | Planned |\n\n---\n\n## License\n\nMIT © [abolfazl-khalaj](https://www.npmjs.com/~abolfazl-khalaj)\n","readmeFilename":"README.md","homepage":"https://www.npmjs.com/package/@abolfazl-khalaj/permission-react","repository":{"type":"git","url":"git+https://github.com/abolfazl-khalaj/package-permission.git","directory":"packages/react"},"bugs":{"url":"https://github.com/abolfazl-khalaj/package-permission/issues"}}