@devx-retailos/rbac
v0.0.2
Published
Generic RBAC Medusa v2 module for retailOS: organizations, stores, roles, permissions, and scoped user-role assignments.
Downloads
578
Keywords
Readme
@devx-retailos/rbac
Generic RBAC Medusa v2 module: organizations, stores, roles, permissions, and scoped user-role assignments. Subject-agnostic — roles are assigned to any { id, type } subject, not just Medusa users.
Part of retailOS, a Medusa v2 SDK for offline-store POS systems. Packages are installed independently and composed in a brand's Medusa backend; this module is the permission backbone the other modules register their permission keys into.
Installation
npm install @devx-retailos/rbacRequires a Medusa v2 project (peer dependencies: @medusajs/framework and @medusajs/medusa ^2.15.0). Depends on @devx-retailos/core for the shared Subject / PermissionScope / error types.
Setup
// medusa-config.ts
export default defineConfig({
// ...
plugins: [
{
resolve: "@devx-retailos/rbac",
options: {},
},
],
})Then run migrations (npx medusa db:migrate) to create the retailos_* tables.
Data model
| Entity | Purpose |
| --- | --- |
| Organization | Top-level tenant (name, unique slug). |
| Store | Physical store within an organization (name, code, address). |
| Permission | Registered permission key (key, description, registered_by). |
| Role | Named role, optionally org-scoped, hierarchical via parent_id (child roles inherit the parent chain's grants). |
| RolePermission | Grant of a permission to a role (granted, optional conditions). |
| UserRole | Assignment of a role to a subject (subject_id, subject_type), scoped by organization_id and optional store_id, with optional expires_at. |
Usage
Resolve the service from the container with the RBAC_MODULE key ("retailos_rbac"):
import { RBAC_MODULE, type RBACModuleService } from "@devx-retailos/rbac"
const rbac: RBACModuleService = req.scope.resolve(RBAC_MODULE)
// Assign a role to any subject
await rbac.assignRole({
subject_id: "user_123",
subject_type: "medusa_user",
role_id: "role_01",
organization_id: "org_01",
store_id: "store_01", // optional
})
// Check a permission
const allowed = await rbac.can(
{ id: "user_123", type: "medusa_user" },
"employee.read",
{ organization_id: "org_01", store_id: "store_01" }
)Service surface beyond the standard Medusa-generated CRUD (createRoles, listOrganizations, …):
can(subject, permissionKey, scope?)— boolean permission check.listPermissionKeysForSubject(subject, scope?)— full effective permission set.listRolesForSubject(subject, scope?)— active, unexpired role assignments (store-scoped or org-wide).getEffectivePermissionKeysForRole(role_id)— grants including the inherited parent-role chain.assignRole(input)/revokeRole(user_role_id).grantPermissionToRole(role_id, permission_id, options?)/revokePermissionFromRole(role_id, permission_id).syncPermissionsFromRegistry(registry)— upserts keys from a@devx-retailos/corePermissionRegistry(this is how sibling modules register their permission keys at boot).
Route middleware
requirePermission guards Medusa API routes. It resolves the caller's identity, runs can(), responds 401 when identity cannot be resolved and 403 (RETAILOS_PERMISSION_DENIED) when the check fails, and attaches req.retailos.identity / req.retailos.scope on success.
// src/api/middlewares.ts
import { defineMiddlewares } from "@medusajs/framework/http"
import { requirePermission } from "@devx-retailos/rbac/middlewares"
export default defineMiddlewares({
routes: [
{
matcher: "/admin/retailos/employees*",
middlewares: [requirePermission("employee.read")],
},
],
})By default it uses medusaAdminIdentityResolver (exported from @devx-retailos/rbac/identity), which maps the authenticated Medusa admin user to a subject of type "medusa_user" (DEFAULT_SUBJECT_TYPE_MEDUSA_USER) and reads scope from the x-retailos-organization-id / x-retailos-store-id request headers.
Extension points
Both are options on requirePermission(permissionKey, options):
identityResolver— anyIdentityResolver<MedusaRequest>from@devx-retailos/core. Swap inemployeeIdentityResolverfrom@devx-retailos/employee/identity, or your own (PIN, SSO, badge), without touching the schema —UserRole.subject_typeis just a string.getScope(req, ctx)— derive thePermissionScopeper request instead of using the resolver's default.
Related packages
@devx-retailos/core— sharedSubject,PermissionScope,IdentityResolver, errors, registries.@devx-retailos/employee— employee entity whose identity resolver plugs into this module.@devx-retailos/sdk-client— frontend hooks (useCan,<PermissionGate>) over the same permission keys.
License
MIT
