{"_id":"@econneq/rbac-layout-engine","name":"@econneq/rbac-layout-engine","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.1":{"name":"@econneq/rbac-layout-engine","version":"1.0.1","description":"Headless, fully-configurable RBAC + app-shell layout engine — Sidebar, Header, AuthGuard, RbacGuard, NotAuthorized & SessionExpired pages. Plug a config, get a production app shell.","author":{"name":"Econneq"},"license":"MIT","keywords":["rbac","layout","sidebar","header","auth","guard","headless","react","econneq"],"main":"./dist/index.js","module":"./dist/index.mjs","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"}},"sideEffects":false,"dependencies":{},"peerDependencies":{"react":">=18.0.0","react-dom":">=18.0.0"},"peerDependenciesMeta":{"@econneq/auth-core":{"optional":true},"@econneq/auth-react":{"optional":true}},"devDependencies":{"@types/react":"^18.3.0","@types/react-dom":"^18.3.0","react":"^18.3.0","react-dom":"^18.3.0","tsup":"^8.5.1","typescript":"^5.4.0","rimraf":"^5.0.0"},"scripts":{"build":"tsup","dev":"tsup --watch","typecheck":"tsc --noEmit","clean":"rimraf dist"},"_id":"@econneq/rbac-layout-engine@1.0.1","_integrity":"sha512-4CqHn2f3o/jY9wUGzbOID55HRIWv97IHFxwtD2Qe+ystfXK0NRqlzCV6PZsoKgwgHFWPOeu7Qm65NVllD8zkGA==","_resolved":"C:\\Users\\e-con\\AppData\\Local\\Temp\\15ad84f3a2e5b0ec77e0097b4d84bfd2\\econneq-rbac-layout-engine-1.0.1.tgz","_from":"file:econneq-rbac-layout-engine-1.0.1.tgz","_nodeVersion":"24.13.0","_npmVersion":"11.8.0","dist":{"integrity":"sha512-4CqHn2f3o/jY9wUGzbOID55HRIWv97IHFxwtD2Qe+ystfXK0NRqlzCV6PZsoKgwgHFWPOeu7Qm65NVllD8zkGA==","shasum":"0357fab0ad1fa5a7ea5bb119097b8ec0d59875ff","tarball":"https://registry.npmjs.org/@econneq/rbac-layout-engine/-/rbac-layout-engine-1.0.1.tgz","fileCount":9,"unpackedSize":552081,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDdmfg0paHOj4RI1/mT9UMqSExO7QKsVbAgu18P2AxzgwIgZ9+wyxDs/MTyTXkI0TDF5AfPbOWk1Qv+IYzxulVt3oI="}]},"_npmUser":{"name":"e-conneq","email":"wemoov13@gmail.com"},"directories":{},"maintainers":[{"name":"e-conneq","email":"wemoov13@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/rbac-layout-engine_1.0.1_1779159267479_0.630911806091061"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-19T02:54:27.420Z","1.0.1":"2026-05-19T02:54:27.721Z","modified":"2026-05-19T02:54:27.894Z"},"maintainers":[{"name":"e-conneq","email":"wemoov13@gmail.com"}],"description":"Headless, fully-configurable RBAC + app-shell layout engine — Sidebar, Header, AuthGuard, RbacGuard, NotAuthorized & SessionExpired pages. Plug a config, get a production app shell.","keywords":["rbac","layout","sidebar","header","auth","guard","headless","react","econneq"],"author":{"name":"Econneq"},"license":"MIT","readme":"# @econneq/rbac-layout-engine\n\n> Headless, fully-configurable RBAC + app-shell layout engine for React 18+.\n> Plug a config, get a production-grade app shell — sidebar, header, drawer,\n> route guards, and a 403/session-expired flow that all honour your role &\n> permission graph.\n\n```\n┌─────────────────────────────────────────────────────────┐\n│   <RbacLayoutProvider config={...}>                     │\n│      <AppLayout>                                        │\n│        <Sidebar>  ←  modules can contribute items       │\n│        <Header>   ←  modules can contribute actions     │\n│        <main>     ←  your routes (guarded)              │\n│      </AppLayout>                                       │\n│   </RbacLayoutProvider>                                 │\n└─────────────────────────────────────────────────────────┘\n```\n\nThe engine is **truly headless**: it never imports your auth library directly.\nYou pass in a `useAuthState()` hook (one-liner against `@econneq/auth-react`,\nNextAuth, Clerk, Auth.js, your own Redux slice — anything) and the engine\ntakes care of access decisions, token-expiry detection, redirects, sidebar\nfiltering, and 403 pages.\n\n---\n\n## Features\n\n- **Headless** — zero hard dependencies on any auth library. Bring your own.\n- **Config-driven** — one `defineRbacLayoutConfig({...})` call and you're done.\n- **Strict JWT validation** — `exp`/`nbf` checked, configurable refresh\n  threshold, auto-redirect to a session-expired page when the token lapses.\n- **Env-var control** — toggle the auth and RBAC subsystems via\n  `NEXT_PUBLIC_RBAC_ENABLED` / `NEXT_PUBLIC_AUTH_ENABLED` (configurable\n  names). Useful for staging environments and feature gates.\n- **Module registry** — feature modules can register sidebar items and\n  header actions, statically in config or dynamically via hooks at runtime.\n- **Three denial strategies** per item — `hide` (default), `disable`, or\n  `show` (block on click). Configurable globally.\n- **Permission wildcards** — `\"*\"` (super-admin), `\"billing:*\"` (namespace).\n- **Theme system** — CSS custom properties scoped under `[data-rle-root]`,\n  defaults to a polished dark-blue palette. Light, dark, and auto modes.\n- **Responsive** — desktop (sidebar + header), tablet (auto-collapsed),\n  mobile (drawer overlay). All breakpoints configurable.\n- **Zero UI dependencies** — pure inline styles. No CSS-in-JS runtime,\n  no Tailwind requirement, no Radix, no styled-components.\n\n---\n\n## Install\n\n```bash\nnpm install @econneq/rbac-layout-engine\n# or\npnpm add @econneq/rbac-layout-engine\n# or\nyarn add @econneq/rbac-layout-engine\n```\n\nPeer dependencies (required):\n\n```bash\nnpm install react react-dom\n```\n\nOptional peer dependencies — wire to these if you're using the Econneq\nauth stack:\n\n```bash\nnpm install @econneq/auth-core @econneq/auth-react\n```\n\nThe package works without them — you only need them if you want\n`useAuthState` to be a one-liner over Econneq's auth.\n\n---\n\n## Quick start\n\n```tsx\n// app/layout.tsx (Next.js App Router) — or your top-level component\n'use client'\n\nimport {\n  RbacLayoutProvider,\n  AppLayout,\n  defineRbacLayoutConfig,\n} from '@econneq/rbac-layout-engine'\nimport { useAuth } from '@econneq/auth-react' // or your own auth hook\nimport { usePathname } from 'next/navigation'\n\nconst config = defineRbacLayoutConfig({\n  appName: 'Acme Console',\n  usePathname,\n\n  auth: {\n    enabled: 'env', // respect NEXT_PUBLIC_AUTH_ENABLED, default true\n    useAuthState: () => {\n      const a = useAuth()\n      return {\n        isAuthenticated: a.isAuthenticated,\n        loading: a.loading,\n        user: a.user,\n        token: a.token,\n        roles: a.user?.roles ?? [],\n        permissions: a.user?.permissions ?? [],\n      }\n    },\n    loginUrl: '/auth/login',\n    sessionExpiredUrl: '/auth/session-expired',\n    refreshThresholdSec: 30,\n  },\n\n  rbac: {\n    enabled: 'env', // respect NEXT_PUBLIC_RBAC_ENABLED\n    superRoles: ['superadmin'],\n    itemDeniedStrategy: 'hide',\n  },\n\n  theme: {\n    mode: 'dark', // 'dark' | 'light' | 'auto'\n  },\n\n  layout: {\n    initialMenu: [\n      { id: 'home',     label: 'Dashboard', href: '/' },\n      { id: 'billing',  label: 'Billing',   href: '/billing',\n        access: { permissions: ['billing:read'] } },\n      { id: 'admin',    label: 'Admin',     href: '/admin',\n        access: { roles: ['admin'] } },\n    ],\n  },\n})\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html>\n      <body>\n        <RbacLayoutProvider config={config}>\n          <AppLayout>{children}</AppLayout>\n        </RbacLayoutProvider>\n      </body>\n    </html>\n  )\n}\n```\n\nThat's it. You now have:\n\n- A dark-themed app shell with a sidebar, header, and content area.\n- An auto-collapsed sidebar on tablet, drawer on mobile.\n- Sidebar items filtered by the current user's roles/permissions.\n- Automatic redirect to `/auth/login?next=/...` for unauthenticated visitors.\n- Automatic redirect to `/auth/session-expired` when the JWT expires.\n- A 403 page when an RBAC rule denies access.\n\n---\n\n## RBAC rules\n\nEvery gate-able thing (route, sidebar item, header action) accepts an\n`AccessRule`:\n\n```ts\ninterface AccessRule {\n  roles?: string[]\n  permissions?: string[]\n  mode?: 'all' | 'any' // default: 'all' (configurable globally)\n  check?: (state: AuthState) => boolean  // custom predicate\n}\n```\n\n```tsx\n// Need ALL of these\n{ roles: ['admin'], permissions: ['users:read'] }\n\n// Need ANY of these\n{ permissions: ['billing:read', 'billing:write'], mode: 'any' }\n\n// Wildcards — namespace match\n{ permissions: ['reports:*'] } // matches reports:read, reports:write, ...\n\n// Custom logic\n{ check: (auth) => auth.user?.tenantId === 'acme' }\n```\n\n**Super-admin bypass** — anyone with a role in `superRoles` (default\n`['superadmin']`) or a permission in `superPermissions` (default `['*']`)\npasses every rule. Use this for a single \"god-mode\" account during ops.\n\n---\n\n## Guards\n\nFour guards, each composable:\n\n```tsx\nimport {\n  AuthGuard,\n  RbacGuard,\n  RouteGuard,\n  Protected,\n} from '@econneq/rbac-layout-engine'\n\n// Whole-page: requires login.\n<AuthGuard>\n  <AccountSettings />\n</AuthGuard>\n\n// Whole-page: requires login + rule.\n<RouteGuard rule={{ roles: ['admin'] }}>\n  <AdminPage />\n</RouteGuard>\n\n// Just the rule (skip the auth check).\n<RbacGuard rule={{ permissions: ['billing:read'] }}>\n  <BillingTable />\n</RbacGuard>\n\n// Inline — render nothing if denied (no 403 page).\n<Protected rule={{ permissions: ['users:delete'] }}>\n  <button onClick={onDelete}>Delete user</button>\n</Protected>\n```\n\nProgrammatic version:\n\n```tsx\nimport { useRbac, useAuthGuard } from '@econneq/rbac-layout-engine'\n\nfunction DeleteButton() {\n  const { can } = useRbac()\n  if (!can({ permissions: ['users:delete'] })) return null\n  return <button>Delete</button>\n}\n\nfunction MyPage() {\n  const { allowed, loading, reason } = useAuthGuard({ roles: ['admin'] })\n  if (loading) return <Spinner />\n  if (!allowed && reason === 'unauthenticated') return <Redirect to=\"/login\" />\n  if (!allowed) return <Forbidden />\n  return <AdminUI />\n}\n```\n\n---\n\n## Sidebar items\n\nFour kinds: `link` (default), `group`, `divider`, `section`.\n\n```ts\n{\n  layout: {\n    initialMenu: [\n      // Section header (uppercase label)\n      { id: 'sec-main', kind: 'section', label: 'Main' },\n\n      // Plain link\n      { id: 'home', label: 'Dashboard', href: '/', icon: <HomeIcon/> },\n\n      // Group with collapsible children\n      { id: 'reports', kind: 'group', label: 'Reports', icon: <ChartIcon/>,\n        children: [\n          { id: 'reports-sales', label: 'Sales', href: '/reports/sales' },\n          { id: 'reports-churn', label: 'Churn', href: '/reports/churn',\n            access: { permissions: ['reports:churn'] } },\n        ] },\n\n      // Visual separator\n      { id: 'div-1', kind: 'divider' },\n\n      // External link\n      { id: 'docs', label: 'Docs', href: 'https://docs.acme.io', external: true },\n\n      // Item with a badge\n      { id: 'inbox', label: 'Inbox', href: '/inbox', badge: 12 },\n    ],\n  },\n}\n```\n\nActive-link detection is automatic (compares `pathname` to `href`). Override\nwith `match: (pathname, item) => boolean` on any link.\n\n---\n\n## Module registration\n\nFeature modules can contribute menu items and header actions without\ntouching the root config:\n\n```tsx\n// Static — declared up-front\ndefineRbacLayoutConfig({\n  modules: [\n    {\n      id: 'billing-module',\n      menuItems: [\n        { id: 'billing', label: 'Billing', href: '/billing' },\n      ],\n      headerActions: [\n        { id: 'cart', label: 'Cart', icon: <CartIcon/>, href: '/cart', badge: 3 },\n      ],\n    },\n  ],\n})\n```\n\n```tsx\n// Dynamic — at runtime, from inside a feature provider\nimport { useRegisterMenuItems } from '@econneq/rbac-layout-engine'\n\nfunction BillingProvider({ children }) {\n  useRegisterMenuItems('billing-module', [\n    { id: 'invoices', label: 'Invoices', href: '/billing/invoices' },\n  ])\n  return children\n}\n```\n\nItems unmount cleanly when their owning component unmounts.\n\n---\n\n## Theming\n\nDefaults to a polished dark-blue palette. Override per-mode or globally:\n\n```ts\ndefineRbacLayoutConfig({\n  theme: {\n    mode: 'dark',           // 'dark' | 'light' | 'auto'\n    tokens: {               // global override (wins over per-mode)\n      colors: {\n        primary: '#22d3ee',\n        primaryForeground: '#0b1220',\n      },\n      radii: { md: '10px' },\n      layout: { sidebarWidth: 280 },\n    },\n    dark: {                 // dark-mode-only overrides\n      colors: { sidebarBackground: '#000' },\n    },\n    light: {                // light-mode-only overrides\n      colors: { background: '#fafafa' },\n    },\n    // Optional — drive `mode` from your own theme switcher hook\n    useThemeMode: () => useMyThemeStore((s) => s.mode),\n  },\n})\n```\n\nAll tokens are emitted as CSS custom properties under `[data-rle-root]`:\n\n```css\n[data-rle-root] {\n  --rle-color-primary: #3b82f6;\n  --rle-color-background: #0b1220;\n  --rle-color-sidebar-background: #0d1424;\n  --rle-radius-md: 8px;\n  --rle-sidebar-width: 260px;\n  /* ... */\n}\n```\n\nYou can override these from your own stylesheet, scoped or globally — no\nneed to go through the config object for ad-hoc tweaks.\n\n---\n\n## Environment-variable control\n\nBoth subsystems can be toggled by environment variable:\n\n```bash\n# .env\nNEXT_PUBLIC_AUTH_ENABLED=true\nNEXT_PUBLIC_RBAC_ENABLED=true\n```\n\nSet the `enabled` field to `'env'` (the default) to consult the variable.\nThe engine probes, in order: `process.env`, `import.meta.env`, and\n`globalThis.__ENV__`. Default variable names:\n\n- Auth: `NEXT_PUBLIC_AUTH_ENABLED`, `VITE_AUTH_ENABLED`, `AUTH_ENABLED`\n- RBAC: `NEXT_PUBLIC_RBAC_ENABLED`, `VITE_RBAC_ENABLED`, `RBAC_ENABLED`\n\nOverride the list per-app via `envVarNames`:\n\n```ts\ndefineRbacLayoutConfig({\n  envVarNames: {\n    auth: ['MY_AUTH_FLAG'],\n    rbac: ['MY_RBAC_FLAG'],\n  },\n})\n```\n\nWhen the variable is unset, the default is **enabled**. Use explicit\n`auth.enabled: false` for \"public-by-default\" apps.\n\n---\n\n## Token expiry\n\nFor any `state.token` that looks like a JWT, the engine decodes the `exp`\nclaim and:\n\n1. Compares it immediately on every auth-state change.\n2. Sets a `setTimeout` alarm at `exp - refreshThresholdSec`.\n3. Redirects to `auth.sessionExpiredUrl` when the timer fires.\n\nIf the token isn't a JWT, supply `auth.expiresAt` directly as a Unix-ms\ntimestamp, or provide your own `auth.validateToken: (token) => result`.\n\nThe redirect is **loop-safe** — already on the session-expired or login\nURL, it stays put.\n\n---\n\n## Customising the default pages\n\n```tsx\ndefineRbacLayoutConfig({\n  pages: {\n    notAuthorized: MyForbiddenPage,        // 403\n    sessionExpired: MySessionExpiredPage,  // /auth/session-expired\n    loading: MyLoadingPage,                // shown while auth.loading\n  },\n})\n```\n\nThe built-in pages are themed by CSS variables. Drop them into your own\nchrome by reusing `NotAuthorizedPage`, `SessionExpiredPage`, `LoadingPage`\nfrom this package.\n\n---\n\n## Responsive behaviour\n\n| Breakpoint | Default range | Sidebar behaviour |\n| --- | --- | --- |\n| Mobile  | `≤ 767px`            | Hidden; replaced by drawer overlay (hamburger toggle). Escape & backdrop close. |\n| Tablet  | `768px – 1023px`     | Auto-collapses to icon rail unless persisted. |\n| Desktop | `≥ 1024px`           | Full sidebar; user can collapse via the toggle. |\n\nBreakpoints are configurable via `theme.tokens.layout.mobileBreakpoint` and\n`tabletBreakpoint`.\n\n---\n\n## Integration recipes\n\n### Without `@econneq/auth-react` (e.g. NextAuth)\n\n```tsx\nimport { useSession, signIn, signOut } from 'next-auth/react'\n\nconst config = defineRbacLayoutConfig({\n  auth: {\n    useAuthState: () => {\n      const { data, status } = useSession()\n      return {\n        isAuthenticated: status === 'authenticated',\n        loading: status === 'loading',\n        user: data?.user,\n        token: (data as any)?.accessToken ?? null,\n        roles: (data?.user as any)?.roles ?? [],\n        permissions: (data?.user as any)?.permissions ?? [],\n      }\n    },\n    redirect: (url) => signIn(undefined, { callbackUrl: url }),\n  },\n})\n```\n\n### Public-by-default with selective gating\n\n```tsx\nconst config = defineRbacLayoutConfig({\n  auth: { enabled: false },\n  rbac: { enabled: false },\n})\n\n// Then use RouteGuard / Protected only on the pages that need it.\n<RouteGuard rule={{ roles: ['admin'] }}>\n  <AdminPanel />\n</RouteGuard>\n```\n\n### Custom user menu\n\n```tsx\ndefineRbacLayoutConfig({\n  layout: {\n    renderUserMenu: ({ user, logout }) => (\n      <DropdownMenu>\n        <DropdownMenuTrigger>{user?.name ?? 'Account'}</DropdownMenuTrigger>\n        <DropdownMenuContent>\n          <DropdownMenuItem href=\"/profile\">Profile</DropdownMenuItem>\n          <DropdownMenuItem onSelect={logout}>Sign out</DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    ),\n  },\n})\n```\n\n### Custom brand block\n\n```tsx\ndefineRbacLayoutConfig({\n  layout: {\n    renderBrand: () => (\n      <a href=\"/\" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>\n        <img src=\"/logo.svg\" width={28} height={28} />\n        <strong>Acme</strong>\n      </a>\n    ),\n  },\n})\n```\n\n### Persist sidebar collapsed state\n\n```ts\ndefineRbacLayoutConfig({\n  layout: {\n    sidebar: {\n      persistKey: 'acme.sidebar.open', // → localStorage\n    },\n  },\n})\n```\n\n---\n\n## API summary\n\n```ts\n// Provider\n<RbacLayoutProvider config={...}>...</RbacLayoutProvider>\n\n// Layout\n<AppLayout>{children}</AppLayout>\n<Sidebar />          // standalone, advanced\n<Header />           // standalone, advanced\n<MobileDrawer />     // standalone, advanced\n\n// Guards\n<AuthGuard fallback={...}>...</AuthGuard>\n<RbacGuard rule={...} fallback={...}>...</RbacGuard>\n<RouteGuard rule={...}>...</RouteGuard>\n<Protected rule={...}>...</Protected>\n\n// Hooks\nuseRbacLayout()        // full context\nuseRbac()              // { can, hasRole, hasPermission, ... }\nuseAuthState()         // raw auth state\nuseAuthGuard(rule)     // { allowed, loading, reason }\nuseSidebar()           // open/toggle/drawer controls\nuseLayoutConfig()      // resolved config\nusePathname()          // current path\n\nuseRegisterModule({ id, menuItems, headerActions })\nuseRegisterMenuItems(id, items)\nuseRegisterHeaderActions(id, actions)\n\n// Config helpers\ndefineRbacLayoutConfig({...})\nresolveConfig({...})\n\n// Token utilities\nvalidateJwt(token, { refreshThresholdSec })\ndecodeJwtPayload(token)\ngetJwtExpiresAt(token)\nisExpired(timestamp)\nresolveExpiresAt(explicit, token)\n```\n\n---\n\n## License\n\nMIT — © Econneq\n","readmeFilename":"README.md","_rev":"1-332ca08b3a5b069cb8f15be0f9c3816e"}