{"_id":"@cogitx-ai/experience","name":"@cogitx-ai/experience","dist-tags":{"latest":"0.2.0"},"versions":{"0.2.0":{"name":"@cogitx-ai/experience","version":"0.2.0","description":"Config-driven RBAC and multitenancy for CogitX experiences — roles, permissions, wildcards, ABAC conditions, and tenant isolation over the ExperienceUser identity, with optional React bindings.","license":"UNLICENSED","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"},"./react":{"types":"./dist/react/index.d.ts","import":"./dist/react/index.js","require":"./dist/react/index.cjs"},"./package.json":"./package.json"},"sideEffects":false,"publishConfig":{"registry":"https://registry.npmjs.org","access":"public"},"repository":{"type":"git","url":"git+https://github.com/CogitX/cogitx-sdk.git","directory":"packages/experience"},"peerDependencies":{"react":">=18"},"peerDependenciesMeta":{"react":{"optional":true}},"dependencies":{"@cogitx-ai/sdk":"0.2.0"},"devDependencies":{"@types/react":"^18.3.0","@types/react-dom":"^18","react":"^18.3.0","react-dom":"^18","tsup":"^8.3.0","typescript":"^5.6.0","vitest":"^2.1.0"},"scripts":{"build":"tsup","test":"vitest run","typecheck":"tsc --noEmit"},"_id":"@cogitx-ai/experience@0.2.0","bugs":{"url":"https://github.com/CogitX/cogitx-sdk/issues"},"homepage":"https://github.com/CogitX/cogitx-sdk#readme","_integrity":"sha512-kC/28qgbJ3spbKZTtVXPiv+gs9mQpV3ZX9D/zj/avhYQXvtwYuFPEAzA+yj7mjjqpgn7QQrcZOaFhgnDaivzpg==","_resolved":"/private/var/folders/83/flmpk1gx741bdpk_n9yh8vlh0000gn/T/dacb48f5322ef6fecdc260290166f0ba/cogitx-ai-experience-0.2.0.tgz","_from":"file:cogitx-ai-experience-0.2.0.tgz","_nodeVersion":"22.22.3","_npmVersion":"10.9.8","dist":{"integrity":"sha512-kC/28qgbJ3spbKZTtVXPiv+gs9mQpV3ZX9D/zj/avhYQXvtwYuFPEAzA+yj7mjjqpgn7QQrcZOaFhgnDaivzpg==","shasum":"df4bd64f2fbaefca30176598fa29a14e9b5c8e84","tarball":"https://registry.npmjs.org/@cogitx-ai/experience/-/experience-0.2.0.tgz","fileCount":16,"unpackedSize":169057,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCNncuqLzRbz8XdlOZi+4JcbANOSvjo5f0HIwKAj+TW5QIhAIJapDtXCjzK1bJRnrE525NZcRDKTxyhwyv4gn6mdbmT"}]},"_npmUser":{"name":"adarsh-cogitx","email":"adarsh@cogitx.ai"},"directories":{},"maintainers":[{"name":"adarsh-cogitx","email":"adarsh@cogitx.ai"},{"name":"kushan-cogitx","email":"kushan@cogitx.ai"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/experience_0.2.0_1783913479328_0.029615628399347615"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-13T03:31:19.200Z","0.2.0":"2026-07-13T03:31:19.514Z","modified":"2026-07-13T03:31:19.739Z"},"maintainers":[{"name":"adarsh-cogitx","email":"adarsh@cogitx.ai"},{"name":"kushan-cogitx","email":"kushan@cogitx.ai"}],"description":"Config-driven RBAC and multitenancy for CogitX experiences — roles, permissions, wildcards, ABAC conditions, and tenant isolation over the ExperienceUser identity, with optional React bindings.","homepage":"https://github.com/CogitX/cogitx-sdk#readme","repository":{"type":"git","url":"git+https://github.com/CogitX/cogitx-sdk.git","directory":"packages/experience"},"bugs":{"url":"https://github.com/CogitX/cogitx-sdk/issues"},"license":"UNLICENSED","readme":"# @cogitx-ai/experience\n\nConfig-driven **RBAC** and **multitenancy** for CogitX experiences. Declare the\nroles, permissions, and tenancy model of an experience once, then gate every\naction — on the server and in React — against a `Principal` built straight from\nan `ExperienceUser` or session JWT.\n\nZero runtime dependencies (beyond `@cogitx-ai/sdk` types). Works in Node 18+ and\nthe browser. React bindings ship from the `/react` subpath (React is an\n**optional** peer — the core is fully usable without it).\n\n```bash\npnpm add @cogitx-ai/experience @cogitx-ai/sdk\n```\n\n---\n\n## 1. Define the experience\n\nEverything flows from a single declarative config. Roles grant permissions\n(`resource:action`), may inherit other roles, and — with `tenancy` — are subject\nto tenant isolation.\n\n```ts\nimport { defineExperience } from '@cogitx-ai/experience';\n\nexport const experience = defineExperience({\n  id: 'support-copilot',\n\n  // Roles → permissions. `resource:action`, with `*` wildcards per segment.\n  roles: {\n    viewer: ['tickets:read', 'kb:read'],\n    agent: { inherits: ['viewer'], permissions: ['tickets:write', 'tickets:comment'] },\n    admin: { inherits: ['agent'], permissions: ['*', 'tenant:cross-access'] },\n  },\n\n  // Turn on tenant isolation: a principal can only touch resources in its tenant\n  // (unless it holds the cross-tenant permission — `admin` above does).\n  tenancy: { enabled: true },\n\n  // How to read roles / tenant off the ExperienceUser + its decoded JWT claims.\n  claims: {\n    roles: (_user, raw) => (raw.roles as string[]) ?? [],\n    tenantId: (_user, raw) => (raw.tenantId as string) ?? null,\n  },\n});\n```\n\n## 2. Build a principal\n\nA `Principal` is the resolved caller. Build one from the identity you already\nhave from the SDK's EAT flow:\n\n```ts\n// From an ExperienceUser + decoded claims:\nconst principal = experience.principalFromUser(user, decodedClaims);\n\n// …or straight from an ExperienceSession (decodes the JWT payload for you):\nconst principal = experience.principalFromSession(session);\n\n// …or explicitly (tests, server-minted identities):\nconst principal = experience.principal({ userId: 'u1', roles: ['agent'], tenantId: 't1' });\n```\n\n> `principalFromSession` decodes the JWT **without verifying the signature** —\n> trust it only for a token your server already validated. Never authorize an\n> untrusted token from its decoded claims.\n\n## 3. Check access\n\n```ts\nexperience.can(principal, 'tickets:write');                  // boolean\nexperience.assert(principal, 'tickets:write', { resource: ticket }); // throws on deny\n\nexperience.canAny(principal, ['tickets:write', 'tickets:comment']);\nexperience.canAll(principal, ['tickets:read', 'kb:read']);\nexperience.hasRole(principal, 'viewer');                     // follows inheritance\nexperience.explain(principal, 'tickets:delete', { resource: ticket }); // why (not)\n```\n\n`assert` throws an **`ExperienceAccessError`** (a `403`). It extends the SDK's\n`CogitxAuthError`, so existing `catch (err instanceof CogitxAuthError)` handling\npicks it up. Tenant violations throw the more specific `TenantIsolationError`.\n\n### ABAC conditions (ownership etc.)\n\nAttach a `when` predicate to any grant for attribute-based checks:\n\n```ts\ndefineExperience({\n  roles: {\n    author: {\n      permissions: [\n        { permission: 'tickets:delete', when: (ctx) => ctx.resource?.ownerId === ctx.principal.userId },\n      ],\n    },\n  },\n});\n```\n\n### Multitenancy\n\nWhen `tenancy.enabled`, passing a `resource` makes `can()`/`assert()` enforce\nthat the resource's tenant matches the principal's — layered **on top of** the\nRBAC grant:\n\n```ts\nexperience.can(agent, 'tickets:write', { resource: { tenantId: 't1' } }); // ✅ same tenant\nexperience.can(agent, 'tickets:write', { resource: { tenantId: 't2' } }); // ❌ isolated\nexperience.can(admin, 'tickets:write', { resource: { tenantId: 't2' } }); // ✅ tenant:cross-access\nexperience.assertTenant(agent, resource);                                 // isolation only, no RBAC\n```\n\nThe resource tenant defaults to a `tenantId` / `tenant` property; override with\n`tenancy.resourceTenant`. Configure the escape hatch with\n`tenancy.crossTenantPermission` (default `tenant:cross-access`).\n\n---\n\n## React\n\n```tsx\nimport {\n  ExperienceProvider,\n  Can,\n  RequireRole,\n  useCan,\n  usePrincipal,\n} from '@cogitx-ai/experience/react';\n\nfunction App({ principal }) {\n  return (\n    <ExperienceProvider experience={experience} principal={principal}>\n      <Tickets />\n    </ExperienceProvider>\n  );\n}\n\nfunction Tickets() {\n  const canCreate = useCan('tickets:write');\n\n  return (\n    <>\n      {canCreate && <NewTicketButton />}\n\n      {/* Declarative gating, tenant-aware when a resource is passed */}\n      <Can permission=\"tickets:delete\" resource={ticket} fallback={<Locked />}>\n        <DeleteButton />\n      </Can>\n\n      <RequireRole role=\"admin\">\n        <AdminPanel />\n      </RequireRole>\n    </>\n  );\n}\n```\n\n`<Can>` accepts `permission`, `resource`, `skipTenantCheck`, `not` (invert), and\n`fallback`. `useCan(permission, { resource })` is the hook form; `useAccess()`\nreturns a bound `can(...)` plus the current principal.\n\n> **Security:** client-side checks are for UX only. Always re-check on the\n> server with `experience.assert(...)` before performing the action.\n\n---\n\n## Examples\n\n- [`examples/node-experience`](../../examples/node-experience) — a runnable,\n  dependency-free script that prints every RBAC and tenant-isolation decision.\n  No credentials needed: `pnpm --filter @cogitx-ai/example-node-experience start`.\n- [`examples/next-experience-rbac`](../../examples/next-experience-rbac) — a\n  Next.js app showing the full integration: `<Can>` / `useCan` / `<RequireRole>`\n  UI gating on the client **and** `experience.assert(...)` enforcement in a\n  route handler (the real gate), both sharing one `lib/experience.ts` config.\n\n## API surface\n\n| Export | What it is |\n| --- | --- |\n| `defineExperience(config)` → `Experience` | Compile the config into the engine |\n| `Experience.can / cannot / assert / canAny / canAll` | Access checks |\n| `Experience.hasRole / permissionsOf / explain` | Introspection |\n| `Experience.principal / principalFromUser / principalFromSession` | Build a `Principal` |\n| `Experience.assertTenant` | Tenant isolation on its own |\n| `permissionMatches`, `resolveGrants`, `expandRoles` | RBAC primitives (pure) |\n| `decodeJwtPayload`, `decodeSessionClaims` | Unverified JWT claim readers |\n| `ExperienceAccessError`, `TenantIsolationError`, `ExperienceConfigError` | Typed errors |\n| `@cogitx-ai/experience/react` | `ExperienceProvider`, `Can`, `RequireRole`, `useCan`, `useAccess`, `usePrincipal`, `useExperience` |\n","readmeFilename":"README.md","_rev":"1-1a1891c4c5854acd6474fa1ef55c2e87"}