{"_id":"@bynalab/ui-resource-access","_rev":"4-f970d15204594807f3fcdc36a149c908","name":"@bynalab/ui-resource-access","dist-tags":{"latest":"0.3.0"},"versions":{"0.1.0":{"name":"@bynalab/ui-resource-access","version":"0.1.0","_id":"@bynalab/ui-resource-access@0.1.0","maintainers":[{"name":"bynalab","email":"bynalabs@gmail.com"}],"homepage":"https://github.com/bynalab/hourbackrbac#readme","bugs":{"url":"https://github.com/bynalab/hourbackrbac/issues"},"dist":{"shasum":"f4ceddf3606434675ad9cad5e76e6563e59eff41","tarball":"https://registry.npmjs.org/@bynalab/ui-resource-access/-/ui-resource-access-0.1.0.tgz","fileCount":40,"integrity":"sha512-zMCJmZETFQHMoaBWSBjKEVhgb+vZY/CO66aA+w7B+EAeOe8YYMrx9tbzyokDP9STEHEmoC2RLtadZpG0GSMkJQ==","signatures":[{"sig":"MEUCIQC+EDLPcnqiogPKAI4ZkXNR7crNnOwmoPAS59wJxekKQAIgfRmC6Wamvgory0cFZknBJA14CHwtLZDgxu65P3Mhgyg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":65557},"main":"./dist/nestjs/index.js","types":"./dist/nestjs/index.d.ts","exports":{".":"./dist/nestjs/index.js","./core":"./dist/core/index.js","./react":"./src/react/index.ts","./nestjs":"./dist/nestjs/index.js"},"gitHead":"4d2664d9dc6af4d6a8dca68f97261116c614c07e","private":false,"scripts":{"test":"jest --config jest.config.js","build":"tsc -p tsconfig.build.json"},"_npmUser":{"name":"bynalab","email":"bynalabs@gmail.com"},"repository":{"url":"git+https://github.com/bynalab/hourbackrbac.git","type":"git"},"_npmVersion":"10.8.2","description":"Portable RBAC v2 UI resource access — catalogue tree, overrides, guards","directories":{},"_nodeVersion":"20.18.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.0.0","react":"^19.2.0","ts-jest":"^29.4.6","typeorm":"^0.3.27","typescript":"^5.9.3","@types/jest":"^30.0.0","@nestjs/core":"^11.1.8","@types/react":"^19.0.0","@nestjs/common":"^11.1.8","@nestjs/swagger":"^11.2.1","@nestjs/testing":"^11.1.9","@nestjs/typeorm":"^11.0.0","class-validator":"^0.14.2","class-transformer":"^0.5.1"},"peerDependencies":{"react":">=18","typeorm":"^0.3.0","@nestjs/core":"^11.0.0","@nestjs/common":"^11.0.0","@nestjs/swagger":"^11.0.0","@nestjs/typeorm":"^11.0.0","class-validator":"^0.14.0","class-transformer":"^0.5.0"},"_npmOperationalInternal":{"tmp":"tmp/ui-resource-access_0.1.0_1781432653996_0.8701279393538732","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@bynalab/ui-resource-access","version":"0.1.1","_id":"@bynalab/ui-resource-access@0.1.1","maintainers":[{"name":"bynalab","email":"bynalabs@gmail.com"}],"homepage":"https://github.com/bynalab/hourbackrbac#readme","bugs":{"url":"https://github.com/bynalab/hourbackrbac/issues"},"dist":{"shasum":"9ae5475ac7d917bb8ccf29920feaffc3c2e603d4","tarball":"https://registry.npmjs.org/@bynalab/ui-resource-access/-/ui-resource-access-0.1.1.tgz","fileCount":45,"integrity":"sha512-k9XpNpWQvnYwlqFiXP6mPy9Tcyx4dEWfB8pkzwDmPA8g+zlPjoB8LwxRu2AoPXiTcTjL0Y5V3EEEOfPV372DFA==","signatures":[{"sig":"MEUCIBad2wU+gouF0h5OVwlWS0v2YZDFl7ukBzkRYiavnG4cAiEAoeK9cMEKRXGqpDODr6T14FcNJWd3rpqjIYChzJ2vWhM=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":70356},"main":"./dist/nestjs/index.js","types":"./dist/nestjs/index.d.ts","exports":{".":{"types":"./dist/nestjs/index.d.ts","default":"./dist/nestjs/index.js"},"./core":{"types":"./dist/core/index.d.ts","default":"./dist/core/index.js"},"./react":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"./nestjs":{"types":"./dist/nestjs/index.d.ts","default":"./dist/nestjs/index.js"}},"gitHead":"4d2664d9dc6af4d6a8dca68f97261116c614c07e","private":false,"scripts":{"test":"jest --config jest.config.js","build":"tsc -p tsconfig.build.json"},"_npmUser":{"name":"bynalab","email":"bynalabs@gmail.com"},"repository":{"url":"git+https://github.com/bynalab/hourbackrbac.git","type":"git"},"_npmVersion":"10.8.2","description":"Portable RBAC v2 UI resource access — catalogue tree, overrides, guards","directories":{},"_nodeVersion":"20.18.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"jest":"^30.0.0","react":"^19.2.0","ts-jest":"^29.4.6","typeorm":"^0.3.27","typescript":"^5.9.3","@types/jest":"^30.0.0","@nestjs/core":"^11.1.8","@types/react":"^19.0.0","@nestjs/common":"^11.1.8","@nestjs/swagger":"^11.2.1","@nestjs/testing":"^11.1.9","@nestjs/typeorm":"^11.0.0","class-validator":"^0.14.2","class-transformer":"^0.5.1"},"peerDependencies":{"react":">=18","typeorm":"^0.3.0","@nestjs/core":"^11.0.0","@nestjs/common":"^11.0.0","@nestjs/swagger":"^11.0.0","@nestjs/typeorm":"^11.0.0","class-validator":"^0.14.0","class-transformer":"^0.5.0"},"_npmOperationalInternal":{"tmp":"tmp/ui-resource-access_0.1.1_1781434856879_0.23331167073512193","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@bynalab/ui-resource-access","version":"0.2.0","_id":"@bynalab/ui-resource-access@0.2.0","maintainers":[{"name":"bynalab","email":"bynalabs@gmail.com"}],"homepage":"https://github.com/bynalab/hourbackrbac#readme","bugs":{"url":"https://github.com/bynalab/hourbackrbac/issues"},"dist":{"shasum":"c3fcdf7f3cb098c35b990f06eb256ef30240fb4b","tarball":"https://registry.npmjs.org/@bynalab/ui-resource-access/-/ui-resource-access-0.2.0.tgz","fileCount":73,"integrity":"sha512-FYGxDt37neHKoobZ6n+VPgdpB0Hk4DkV8XBilkUW2YsX5DqqTv//j35hW0zM4MaEyKdhWpzNkmxcbVGJ6WQ1KQ==","signatures":[{"sig":"MEQCIHYib7i+pTzCI4LQEdrCDyAMdi7GCUZmT/Bs8rUUSyntAiBry/oybZ8krT6B8EtIVVYeDnKpq8i/8fLsHipwVeh4Lg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":94441},"main":"./dist/nestjs/index.js","types":"./dist/nestjs/index.d.ts","exports":{".":{"types":"./dist/nestjs/index.d.ts","default":"./dist/nestjs/index.js"},"./core":{"types":"./dist/core/index.d.ts","default":"./dist/core/index.js"},"./react":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"./nestjs":{"types":"./dist/nestjs/index.d.ts","default":"./dist/nestjs/index.js"},"./server":{"types":"./dist/server/index.d.ts","default":"./dist/server/index.js"},"./fastify":{"types":"./dist/fastify/index.d.ts","default":"./dist/fastify/index.js"}},"gitHead":"4d2664d9dc6af4d6a8dca68f97261116c614c07e","private":false,"scripts":{"test":"jest --config jest.config.js","build":"tsc -p tsconfig.build.json"},"_npmUser":{"name":"bynalab","email":"bynalabs@gmail.com"},"repository":{"url":"git+https://github.com/bynalab/hourbackrbac.git","type":"git"},"_npmVersion":"10.8.2","description":"Portable RBAC v2 UI resource access — catalogue tree, overrides, guards","directories":{},"_nodeVersion":"20.18.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"pg":"^8.16.0","jest":"^30.0.0","react":"^19.2.0","fastify":"^5.6.0","ts-jest":"^29.4.6","typeorm":"^0.3.27","@types/pg":"^8.11.0","typescript":"^5.9.3","@types/jest":"^30.0.0","@nestjs/core":"^11.1.8","@types/react":"^19.0.0","@nestjs/common":"^11.1.8","@nestjs/swagger":"^11.2.1","@nestjs/testing":"^11.1.9","@nestjs/typeorm":"^11.0.0","class-validator":"^0.14.2","class-transformer":"^0.5.1"},"peerDependencies":{"pg":"^8.0.0","react":">=18","fastify":"^4.28.0 || ^5.0.0","typeorm":"^0.3.0","@nestjs/core":"^11.0.0","@nestjs/common":"^11.0.0","@nestjs/swagger":"^11.0.0","@nestjs/typeorm":"^11.0.0","class-validator":"^0.14.0","class-transformer":"^0.5.0"},"peerDependenciesMeta":{"pg":{"optional":true},"fastify":{"optional":true},"typeorm":{"optional":true},"@nestjs/core":{"optional":true},"@nestjs/common":{"optional":true},"@nestjs/swagger":{"optional":true},"@nestjs/typeorm":{"optional":true},"class-validator":{"optional":true},"class-transformer":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/ui-resource-access_0.2.0_1781440472266_0.5448198604829608","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@bynalab/ui-resource-access","version":"0.3.0","description":"Portable RBAC v2 UI resource access — catalogue tree, overrides, guards","private":false,"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/bynalab/hourbackrbac.git"},"main":"./dist/nestjs/index.js","types":"./dist/nestjs/index.d.ts","exports":{".":{"types":"./dist/nestjs/index.d.ts","default":"./dist/nestjs/index.js"},"./core":{"types":"./dist/core/index.d.ts","default":"./dist/core/index.js"},"./nestjs":{"types":"./dist/nestjs/index.d.ts","default":"./dist/nestjs/index.js"},"./react":{"types":"./dist/react/index.d.ts","default":"./dist/react/index.js"},"./fastify":{"types":"./dist/fastify/index.d.ts","default":"./dist/fastify/index.js"},"./server":{"types":"./dist/server/index.d.ts","default":"./dist/server/index.js"}},"scripts":{"build":"tsc -p tsconfig.build.json","test":"jest --config jest.config.js"},"peerDependencies":{"@nestjs/common":"^11.0.0","@nestjs/core":"^11.0.0","@nestjs/swagger":"^11.0.0","@nestjs/typeorm":"^11.0.0","class-transformer":"^0.5.0","class-validator":"^0.14.0","fastify":"^4.28.0 || ^5.0.0","pg":"^8.0.0","react":">=18","typeorm":"^0.3.0"},"peerDependenciesMeta":{"@nestjs/common":{"optional":true},"@nestjs/core":{"optional":true},"@nestjs/swagger":{"optional":true},"@nestjs/typeorm":{"optional":true},"class-transformer":{"optional":true},"class-validator":{"optional":true},"fastify":{"optional":true},"pg":{"optional":true},"typeorm":{"optional":true}},"devDependencies":{"@nestjs/common":"^11.1.8","@nestjs/core":"^11.1.8","@nestjs/swagger":"^11.2.1","@nestjs/testing":"^11.1.9","@nestjs/typeorm":"^11.0.0","@types/jest":"^30.0.0","@types/pg":"^8.11.0","@types/react":"^19.0.0","class-transformer":"^0.5.1","class-validator":"^0.14.2","fastify":"^5.6.0","jest":"^30.0.0","pg":"^8.16.0","react":"^19.2.0","ts-jest":"^29.4.6","typeorm":"^0.3.27","typescript":"^5.9.3"},"_id":"@bynalab/ui-resource-access@0.3.0","gitHead":"4d2664d9dc6af4d6a8dca68f97261116c614c07e","bugs":{"url":"https://github.com/bynalab/hourbackrbac/issues"},"homepage":"https://github.com/bynalab/hourbackrbac#readme","_nodeVersion":"20.18.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-nx4JzGIXJ1LfP5ldSlXTWGyU5dc1fhA1i88xY0gQbfjH3BSpYVk0f2cQmwSV2ibGGtq8k5gJ2ddZgqRbXqTwRQ==","shasum":"bd5d0e879f827cb7ee0a430983db0f570bf0b2f4","tarball":"https://registry.npmjs.org/@bynalab/ui-resource-access/-/ui-resource-access-0.3.0.tgz","fileCount":78,"unpackedSize":122442,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDoS5o7meKZtm2gC2Fm1eO6WsU1wTwLfKaVMQ5QKos4twIgG+jEvet6+6oWPXeh8cD79FhpptovCa6Fkdw+pU6tRCg="}]},"_npmUser":{"name":"bynalab","email":"bynalabs@gmail.com"},"directories":{},"maintainers":[{"name":"bynalab","email":"bynalabs@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ui-resource-access_0.3.0_1781447146902_0.9645307441145694"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-14T10:24:13.837Z","modified":"2026-06-14T14:25:47.165Z","0.1.0":"2026-06-14T10:24:14.150Z","0.1.1":"2026-06-14T11:00:57.018Z","0.2.0":"2026-06-14T12:34:32.404Z","0.3.0":"2026-06-14T14:25:47.049Z"},"bugs":{"url":"https://github.com/bynalab/hourbackrbac/issues"},"homepage":"https://github.com/bynalab/hourbackrbac#readme","repository":{"type":"git","url":"git+https://github.com/bynalab/hourbackrbac.git"},"description":"Portable RBAC v2 UI resource access — catalogue tree, overrides, guards","maintainers":[{"name":"bynalab","email":"bynalabs@gmail.com"}],"readme":"# @bynalab/ui-resource-access\n\nControl **what users see and can open** in your app — sidebar links, routes, buttons, and API endpoints — from one shared resource catalogue.\n\n- Hierarchical UI resource tree (domain → page → tile → block)\n- Role defaults + per-user allow/deny overrides\n- Backend route guards and admin CRUD API\n- React provider, hooks, and `<CanAccess>` components\n\nNo CASL or Casbin required. You bring auth (JWT, session, etc.) and define the catalogue in your app.\n\n```bash\nnpm install @bynalab/ui-resource-access@^0.3.0\n```\n\n---\n\n## How it works\n\n```\nCatalogue (you define)     Role defaults        User overrides (DB)\n        │                        │                        │\n        └──────────► effective access ◄───────────────────┘\n                              │\n              ┌───────────────┼───────────────┐\n              ▼               ▼               ▼\n         Sidebar filter   Route guards    API @RequireResource\n         <CanAccess>       redirect        403 if denied\n```\n\n**Resource types**\n\n| Type | Example ID | Use for |\n|------|------------|---------|\n| `domain` | `admin` | Top-level app area |\n| `page` | `admin.users` | Nav grouping |\n| `tile` | `admin.users.list` | Sidebar link / route (`path` required) |\n| `block` | `admin.users.list.export` | Buttons, cards, table actions |\n\nUse the **same resource IDs** in your backend guards and frontend `<CanAccess>`.\n\n---\n\n## 1. Database\n\nRun the migration from the package (adjust the `users` FK if your schema differs):\n\n- **Single-role / single-tenant:** `migration/user_resource_overrides.sql`\n- **Multi-role + tenant-scoped overrides (v0.3+):** `migration/user_resource_overrides_multi_tenant.sql`\n\nRegister the TypeORM entity (NestJS only):\n\n```typescript\nimport { UserResourceOverride } from '@bynalab/ui-resource-access';\n// Add UserResourceOverride to your TypeORM entities / forFeature imports\n```\n\n---\n\n## 2. Define your catalogue\n\nKeep this in **your app**, not inside the package. Mirror it on backend and frontend.\n\n```typescript\n// config/resource-catalogue.ts\nimport type { ResourceCatalogueConfig } from '@bynalab/ui-resource-access/core';\n\nexport const APP_CATALOGUE: ResourceCatalogueConfig = {\n  tree: [\n    {\n      id: 'admin',\n      type: 'domain',\n      label: 'Admin',\n      children: [\n        {\n          id: 'admin.users',\n          type: 'page',\n          label: 'Users',\n          children: [\n            {\n              id: 'admin.users.list',\n              type: 'tile',\n              label: 'User list',\n              path: '/admin/users',\n              children: [\n                { id: 'admin.users.list.export', type: 'block', label: 'Export' },\n              ],\n            },\n          ],\n        },\n      ],\n    },\n    {\n      id: 'reports',\n      type: 'domain',\n      label: 'Reports',\n      children: [\n        {\n          id: 'reports.main',\n          type: 'tile',\n          label: 'Reports',\n          path: '/reports',\n        },\n      ],\n    },\n  ],\n  role_domain_grants: {\n    super_admin: ['*'],\n    admin: ['admin'],\n    viewer: ['reports'],\n  },\n  sidebar_resource_by_path: {\n    '/admin/users': 'admin.users.list',\n    '/reports': 'reports.main',\n  },\n};\n```\n\n---\n\n## 3. Backend\n\nPick **NestJS** or **Fastify**. Both expose the same HTTP API and work with the React client.\n\n### NestJS + TypeORM\n\n```typescript\nimport { Module } from '@nestjs/common';\nimport { UiResourceAccessModule } from '@bynalab/ui-resource-access';\nimport { User } from './user.entity';\nimport { JwtAuthGuard } from './auth/jwt-auth.guard';\nimport { APP_CATALOGUE } from './config/resource-catalogue';\n\n@Module({\n  imports: [\n    UiResourceAccessModule.forRoot({\n      catalogue: APP_CATALOGUE,\n      userEntity: User,              // id, email, role, is_deleted?\n      godModeRoles: ['super_admin'],\n      adminRoles: ['super_admin'],\n      authGuards: [JwtAuthGuard],\n      adminGuards: [JwtAuthGuard],\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\nProtect a controller action:\n\n```typescript\nimport { Controller, Post, UseGuards } from '@nestjs/common';\nimport { ResourceAccessGuard, RequireResource } from '@bynalab/ui-resource-access';\nimport { JwtAuthGuard } from './auth/jwt-auth.guard';\n\n@Controller('admin/users')\nexport class UsersController {\n  @Post('export')\n  @UseGuards(JwtAuthGuard, ResourceAccessGuard)\n  @RequireResource('admin.users.list.export')\n  exportUsers() {\n    // ...\n  }\n}\n```\n\n**Peers:** `@nestjs/common`, `@nestjs/core`, `@nestjs/typeorm`, `@nestjs/swagger`, `typeorm`, `class-validator`, `class-transformer`\n\n### Fastify + pg\n\n```typescript\nimport Fastify from 'fastify';\nimport { createUiResourceAccess } from '@bynalab/ui-resource-access/fastify';\nimport { pool } from './db';\nimport { authenticate } from './middleware/authenticate';\nimport { APP_CATALOGUE } from './config/resource-catalogue';\n\nconst fastify = Fastify();\n\nconst resourceAccess = createUiResourceAccess({\n  pool,\n  catalogue: APP_CATALOGUE,\n  godModeRoles: ['super_admin'],\n  adminRoles: ['super_admin'],\n  preHandlers: [authenticate],\n  getContext: (req) => ({\n    userId: req.user.id,\n    tenantId: req.organizationId, // optional\n    roles: req.userRoles,         // optional — storage can load roles\n  }),\n});\n\nresourceAccess.registerRoutes(fastify);\n\nfastify.post('/admin/users/export', {\n  preHandler: [\n    authenticate,\n    resourceAccess.requireResource('admin.users.list.export'),\n  ],\n}, async () => {\n  // ...\n});\n\nawait fastify.listen({ port: 3000 });\n```\n\n**Peers:** `fastify`, `pg`\n\nOptional: `adminControllerPath: 'admin/rbac'` if you want a custom admin URL prefix.\n\n---\n\n## 4. React / Next.js\n\nConfigure once, then wrap your layout.\n\n```typescript\n// lib/rbac/setup.ts\nimport { configureResourceAccess } from '@bynalab/ui-resource-access/react';\nimport { APP_CATALOGUE } from '../config/resource-catalogue';\n\nconfigureResourceAccess({\n  getApiBaseUrl: () => process.env.NEXT_PUBLIC_API_URL!,\n  credentials: 'include', // cookie/session apps — omit getAuthHeaders\n  sidebarResourceByPath: APP_CATALOGUE.sidebar_resource_by_path ?? {},\n});\n\n// Bearer token apps:\n// getAuthHeaders: () => ({ Authorization: `Bearer ${getToken()}` }),\n```\n\n```tsx\n// app/(portal)/layout.tsx\n'use client';\n\nimport {\n  ResourceAccessProvider,\n  CanAccess,\n  useResourceAccess,\n  useMemoizedSidebarFilter,\n} from '@bynalab/ui-resource-access/react';\nimport { ensureResourceAccessConfigured } from '@/lib/rbac/setup';\n\nensureResourceAccessConfigured();\n\nexport default function PortalLayout({ children }) {\n  const { canAccessPath } = useResourceAccess();\n  const items = useMemoizedSidebarFilter(rawSidebarItems, canAccessPath);\n\n  return (\n    <ResourceAccessProvider>\n      <Sidebar items={items} />\n      {children}\n    </ResourceAccessProvider>\n  );\n}\n```\n\nHide a button:\n\n```tsx\n<CanAccess resourceId=\"admin.users.list.export\">\n  <ExportButton />\n</CanAccess>\n```\n\n**Peer:** `react` ≥ 18\n\n---\n\n## HTTP API\n\nDefault admin prefix: `admin/resource-access`. Override with `adminControllerPath`.\n\n| Method | Path | Who | Purpose |\n|--------|------|-----|---------|\n| GET | `/resource-access/me` | User | Allowed resource IDs (+ optional `roles`, `isSuperAdmin`) |\n| GET | `/admin/resource-access/resource-catalogue` | Admin | Full tree for editor |\n| GET | `/admin/resource-access/users/:id/resource-access` | Admin | User override state |\n| PUT | `/admin/resource-access/users/:id/resource-access/batch` | Admin | Batch save overrides |\n| PATCH | `/admin/resource-access/users/:id/resource-access/toggle` | Admin | Toggle one node |\n\nIf you set `adminControllerPath: 'admin/rbac'`, paths become `/admin/rbac/...`.\n\n---\n\n## Package exports\n\n| Import | When to use |\n|--------|-------------|\n| `@bynalab/ui-resource-access` | NestJS (main entry) |\n| `@bynalab/ui-resource-access/nestjs` | Same as main |\n| `@bynalab/ui-resource-access/fastify` | Fastify + pg |\n| `@bynalab/ui-resource-access/react` | Frontend provider, hooks, guards |\n| `@bynalab/ui-resource-access/core` | Types and catalogue helpers only |\n| `@bynalab/ui-resource-access/server` | Custom storage adapter / advanced use |\n\nUse **`^0.3.0`** for multi-role/tenant storage, `getContext`, and cookie-session React auth.\n\n---\n\n## Multi-role & multi-tenant integration (v0.3+)\n\nv0.2 apps keep working unchanged (single `users.role`, overrides keyed by `user_id` only). Opt in when you need multiple roles per user or tenant-scoped overrides.\n\n### Storage contract\n\nImplement or use a reference adapter from `@bynalab/ui-resource-access/server`:\n\n```typescript\ninterface ResourceAccessContext {\n  userId: string;\n  tenantId?: string;\n  roles?: string[]; // optional hint; storage loads roles when omitted\n}\n\ninterface ResourceAccessStorage {\n  findUser(ctx: ResourceAccessContext): Promise<ResourceAccessUser | null>;\n  getUserRoles(ctx: ResourceAccessContext): Promise<string[]>;\n  loadOverrideMap(ctx: ResourceAccessContext): Promise<Map<string, ResourceOverrideEffect>>;\n  upsertOverride(ctx, resourceId, effect): Promise<void>;\n  deleteOverride(ctx, resourceId): Promise<void>;\n}\n```\n\n**Reference adapters (Fastify):**\n\n| Adapter | Use |\n|---------|-----|\n| `createPgResourceAccessStorage(pool)` | v0.2 behaviour — single role from `users.role` |\n| `createPgMultiRoleStorage(pool)` | Roles from `user_roles` join table + `tenant_id` on overrides |\n\nPass custom storage to the factory:\n\n```typescript\nimport {\n  createUiResourceAccess,\n  createPgMultiRoleStorage,\n} from '@bynalab/ui-resource-access/fastify';\n\nconst resourceAccess = createUiResourceAccess({\n  catalogue: APP_CATALOGUE,\n  storage: createPgMultiRoleStorage(pool),\n  preHandlers: [authenticate, scopeToTenant],\n  getContext: (req) => ({ userId: req.user.id, tenantId: req.tenantId }),\n});\n```\n\nNestJS: implement `ResourceAccessStorage` or use TypeORM adapter (single-role). Set `getContextFromRequest` on the module for tenant scope.\n\n### Resolution rules\n\n| Case | Result |\n|------|--------|\n| Any role grants the domain (union) | Allowed by role default |\n| Any role in `godModeRoles` | Allowed |\n| Ancestor **deny** override | Descendants denied |\n| Ancestor **allow** override | Descendants allowed unless explicit child deny |\n| `/me` vs `requireResource()` | Same resolver path — IDs always agree |\n\nMulti-role helpers exported from `/core`:\n\n- `buildNodeStateMultiRole`\n- `computeAllowedResourceIdsMultiRole`\n- `canAccessResourceMultiRole`\n\nSingle-role helpers (`buildNodeState`, `canAccessResource`, …) remain and delegate internally.\n\n### Toggle inheritance\n\nWhen an admin **enables** a parent node, descendant overrides are cleared so inheritance applies cleanly. **Disabling** a parent sets a deny override on that node; descendants inherit the deny.\n\n### Migrating from a local fork\n\nIf you copied resolver or `/me` logic locally:\n\n1. Remove forked resolution — use `@bynalab/ui-resource-access/core` multi-role helpers or the built-in service.\n2. Replace direct SQL on `user_resource_overrides` with a `ResourceAccessStorage` adapter (or the PG reference adapters).\n3. Wire `getContext` / `getContextFromRequest` in your auth middleware layer (tenant id stays host-owned).\n4. Point React at `/resource-access/me` with `credentials: 'include'` when using sessions.\n\n---\n\n## What this does not replace\n\nThis package gates **UI resources** (navigation and catalogue-aligned endpoints). Use CASL, Casbin, or row-level checks separately for entity-level authorization (e.g. “can edit this specific record”). The two layers complement each other.\n\n---\n\n## Troubleshooting\n\n| Problem | Fix |\n|---------|-----|\n| UI shows items user shouldn't see | Check `sidebar_resource_by_path` and that `ResourceAccessProvider` wraps the layout |\n| API returns 403 but UI allows access | Add `@RequireResource` / `requireResource()` on the backend route |\n| Admin editor empty or 404 | Match `adminControllerPath` with React `adminEndpoints` config |\n| `Can't resolve '../core/...'` in React | Upgrade to `^0.1.1` or later (compiled `dist/react`) |\n","readmeFilename":"README.md"}