├── .node-version ├── permix ├── src │ ├── better-auth │ │ └── index.ts │ ├── elysia │ │ ├── index.ts │ │ └── create-permix.ts │ ├── orpc │ │ ├── index.ts │ │ └── create-permix.ts │ ├── trpc │ │ ├── index.ts │ │ └── create-permix.ts │ ├── express │ │ ├── index.ts │ │ └── create-permix.ts │ ├── fastify │ │ ├── index.ts │ │ └── create-permix.ts │ ├── hono │ │ ├── index.ts │ │ └── create-permix.ts │ ├── node │ │ ├── index.ts │ │ └── create-permix.ts │ ├── vue │ │ ├── index.ts │ │ ├── plugin.test.ts │ │ ├── composables.ts │ │ ├── plugin.ts │ │ └── components.ts │ ├── react │ │ ├── index.ts │ │ ├── hooks.ts │ │ ├── components.tsx │ │ └── hooks.test.tsx │ ├── solid │ │ ├── index.ts │ │ ├── hooks.ts │ │ ├── components.tsx │ │ └── hooks.test.tsx │ ├── core │ │ ├── index.ts │ │ ├── template.ts │ │ ├── utils.ts │ │ ├── hydration.ts │ │ ├── utils.test.ts │ │ ├── params.ts │ │ ├── hooks.ts │ │ ├── template.test.ts │ │ └── hooks.test.ts │ ├── utils.ts │ └── utils.test.ts ├── .gitignore ├── tsconfig.json ├── scripts │ └── copy-readme.ts ├── vitest.config.ts ├── tsconfig.react.json ├── tsconfig.solid.json ├── tsconfig.base.json ├── build.config.ts └── package.json ├── .github ├── FUNDING.yml └── workflows │ └── npm-publish.yml ├── .gitignore ├── examples ├── express-trpc-react │ ├── .gitignore │ ├── client │ │ ├── src │ │ │ ├── vite-env.d.ts │ │ │ ├── components │ │ │ │ └── permix.ts │ │ │ ├── hooks │ │ │ │ └── use-permissions.ts │ │ │ ├── permix.ts │ │ │ ├── trpc.ts │ │ │ ├── main.tsx │ │ │ └── App.tsx │ │ └── index.html │ ├── vite.config.ts │ ├── shared │ │ ├── trpc.ts │ │ └── permix.ts │ ├── tsconfig.json │ ├── package.json │ └── server │ │ └── main.ts ├── vue │ ├── src │ │ ├── vite-env.d.ts │ │ ├── composables │ │ │ ├── permissions.ts │ │ │ ├── posts.ts │ │ │ └── user.ts │ │ ├── main.ts │ │ ├── lib │ │ │ └── permix.ts │ │ ├── App.vue │ │ └── style.css │ ├── .vscode │ │ └── extensions.json │ ├── tsconfig.json │ ├── vite.config.ts │ ├── .gitignore │ ├── index.html │ ├── README.md │ ├── tsconfig.app.json │ ├── package.json │ ├── tsconfig.node.json │ └── public │ │ └── vite.svg ├── react │ ├── src │ │ ├── vite-env.d.ts │ │ ├── hooks │ │ │ ├── permissions.ts │ │ │ ├── posts.ts │ │ │ └── user.ts │ │ ├── main.tsx │ │ ├── lib │ │ │ └── permix.ts │ │ ├── App.css │ │ ├── index.css │ │ ├── App.tsx │ │ └── assets │ │ │ └── react.svg │ ├── tsconfig.json │ ├── vite.config.ts │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── tsconfig.node.json │ ├── tsconfig.app.json │ ├── public │ │ └── vite.svg │ └── README.md ├── solid │ ├── src │ │ ├── vite-env.d.ts │ │ ├── hooks │ │ │ ├── permissions.ts │ │ │ ├── posts.ts │ │ │ └── user.ts │ │ ├── index.tsx │ │ ├── App.css │ │ ├── lib │ │ │ └── permix.ts │ │ ├── index.css │ │ ├── App.tsx │ │ └── assets │ │ │ └── solid.svg │ ├── tsconfig.json │ ├── vite.config.ts │ ├── .gitignore │ ├── index.html │ ├── package.json │ ├── tsconfig.node.json │ ├── tsconfig.app.json │ ├── README.md │ └── public │ │ └── vite.svg ├── enum-based │ ├── src │ │ ├── vite-env.d.ts │ │ ├── hooks │ │ │ └── use-permissions.ts │ │ ├── lib │ │ │ ├── user.ts │ │ │ ├── permissions.ts │ │ │ └── permix.ts │ │ ├── main.tsx │ │ └── App.tsx │ ├── tsconfig.json │ ├── vite.config.ts │ ├── .gitignore │ ├── index.html │ ├── tsconfig.node.json │ ├── tsconfig.app.json │ └── package.json ├── feature-flags │ ├── src │ │ ├── vite-env.d.ts │ │ ├── hooks │ │ │ └── use-permissions.ts │ │ ├── lib │ │ │ ├── user.ts │ │ │ └── permix.ts │ │ ├── main.tsx │ │ └── App.tsx │ ├── tsconfig.json │ ├── vite.config.ts │ ├── .gitignore │ ├── index.html │ ├── tsconfig.node.json │ ├── package.json │ └── tsconfig.app.json ├── role-based │ ├── src │ │ ├── vite-env.d.ts │ │ ├── hooks │ │ │ └── use-permissions.ts │ │ ├── lib │ │ │ ├── user.ts │ │ │ └── permix.ts │ │ ├── main.tsx │ │ └── App.tsx │ ├── tsconfig.json │ ├── vite.config.ts │ ├── .gitignore │ ├── index.html │ ├── tsconfig.node.json │ ├── tsconfig.app.json │ └── package.json └── express │ ├── package.json │ └── main.ts ├── .husky ├── commit-msg └── pre-commit ├── docs ├── lib │ ├── cn.ts │ ├── llm.ts │ └── source.ts ├── postcss.config.js ├── app │ ├── api │ │ └── search │ │ │ └── route.ts │ ├── (home) │ │ ├── code │ │ │ ├── usage.mdx │ │ │ ├── init.mdx │ │ │ └── setup.mdx │ │ ├── layout.tsx │ │ └── page.tsx │ ├── llms-full.txt │ │ └── route.ts │ ├── docs │ │ ├── layout.tsx │ │ └── [[...slug]] │ │ │ └── page.tsx │ ├── global.css │ ├── layout.shared.tsx │ ├── mdx-components.tsx │ ├── llms.txt │ │ └── route.ts │ └── layout.tsx ├── .gitignore ├── next.config.ts ├── README.md ├── source.config.ts ├── tsconfig.json ├── content │ └── docs │ │ ├── guide │ │ ├── events.mdx │ │ ├── ready.mdx │ │ ├── check.mdx │ │ ├── hydration.mdx │ │ └── template.mdx │ │ ├── comparison.mdx │ │ ├── meta.json │ │ ├── quick-start.mdx │ │ ├── integrations │ │ ├── react.mdx │ │ ├── vue.mdx │ │ └── solid.mdx │ │ └── index.mdx └── package.json ├── pnpm-workspace.yaml ├── commitlint.config.ts ├── package.json ├── README.md ├── LICENSE ├── eslint.config.ts └── .vscode └── settings.json /.node-version: -------------------------------------------------------------------------------- 1 | 22.18.0 2 | -------------------------------------------------------------------------------- /permix/src/better-auth/index.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: letstri 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /permix/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | README.md 3 | -------------------------------------------------------------------------------- /examples/express-trpc-react/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | pnpm dlx commitlint --edit $1 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | run-p test lint check-types build 2 | -------------------------------------------------------------------------------- /docs/lib/cn.ts: -------------------------------------------------------------------------------- 1 | export { twMerge as cn } from 'tailwind-merge' 2 | -------------------------------------------------------------------------------- /examples/vue/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/solid/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/enum-based/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/feature-flags/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/role-based/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - permix 3 | - docs 4 | - examples/* 5 | -------------------------------------------------------------------------------- /examples/vue/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/express-trpc-react/client/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /permix/src/elysia/index.ts: -------------------------------------------------------------------------------- 1 | export { createPermix, type PermixOptions } from './create-permix' 2 | -------------------------------------------------------------------------------- /permix/src/orpc/index.ts: -------------------------------------------------------------------------------- 1 | export { createPermix, type PermixOptions } from './create-permix' 2 | -------------------------------------------------------------------------------- /permix/src/trpc/index.ts: -------------------------------------------------------------------------------- 1 | export { createPermix, type PermixOptions } from './create-permix' 2 | -------------------------------------------------------------------------------- /commitlint.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | extends: ['@commitlint/config-conventional'], 3 | } 4 | -------------------------------------------------------------------------------- /docs/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | '@tailwindcss/postcss': {}, 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /permix/src/express/index.ts: -------------------------------------------------------------------------------- 1 | export { createPermix, type MiddlewareContext, type PermixOptions } from './create-permix' 2 | -------------------------------------------------------------------------------- /permix/src/fastify/index.ts: -------------------------------------------------------------------------------- 1 | export { createPermix, type MiddlewareContext, type PermixOptions } from './create-permix' 2 | -------------------------------------------------------------------------------- /permix/src/hono/index.ts: -------------------------------------------------------------------------------- 1 | export { createPermix, type MiddlewareContext, type PermixOptions } from './create-permix' 2 | -------------------------------------------------------------------------------- /permix/src/node/index.ts: -------------------------------------------------------------------------------- 1 | export { createPermix, type MiddlewareContext, type PermixOptions } from './create-permix' 2 | -------------------------------------------------------------------------------- /examples/vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.app.json" }, 4 | { "path": "./tsconfig.node.json" } 5 | ], 6 | "files": [] 7 | } 8 | -------------------------------------------------------------------------------- /examples/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.app.json" }, 4 | { "path": "./tsconfig.node.json" } 5 | ], 6 | "files": [] 7 | } 8 | -------------------------------------------------------------------------------- /examples/solid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.app.json" }, 4 | { "path": "./tsconfig.node.json" } 5 | ], 6 | "files": [] 7 | } 8 | -------------------------------------------------------------------------------- /examples/enum-based/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.app.json" }, 4 | { "path": "./tsconfig.node.json" } 5 | ], 6 | "files": [] 7 | } 8 | -------------------------------------------------------------------------------- /examples/feature-flags/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.app.json" }, 4 | { "path": "./tsconfig.node.json" } 5 | ], 6 | "files": [] 7 | } 8 | -------------------------------------------------------------------------------- /examples/role-based/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.app.json" }, 4 | { "path": "./tsconfig.node.json" } 5 | ], 6 | "files": [] 7 | } 8 | -------------------------------------------------------------------------------- /examples/solid/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import solid from 'vite-plugin-solid' 3 | 4 | export default defineConfig({ 5 | plugins: [solid()], 6 | }) 7 | -------------------------------------------------------------------------------- /examples/react/src/hooks/permissions.ts: -------------------------------------------------------------------------------- 1 | import { usePermix } from 'permix/react' 2 | import { permix } from '../lib/permix' 3 | 4 | export function usePermissions() { 5 | return usePermix(permix) 6 | } 7 | -------------------------------------------------------------------------------- /examples/solid/src/hooks/permissions.ts: -------------------------------------------------------------------------------- 1 | import { usePermix } from 'permix/solid' 2 | import { permix } from '../lib/permix' 3 | 4 | export function usePermissions() { 5 | return usePermix(permix) 6 | } 7 | -------------------------------------------------------------------------------- /examples/express-trpc-react/client/src/components/permix.ts: -------------------------------------------------------------------------------- 1 | import { createComponents } from 'permix/react' 2 | import { permix } from '../permix' 3 | 4 | export const { Check } = createComponents(permix) 5 | -------------------------------------------------------------------------------- /examples/vue/src/composables/permissions.ts: -------------------------------------------------------------------------------- 1 | import { usePermix } from 'permix/vue' 2 | import { permix } from '../lib/permix' 3 | 4 | export function usePermissions() { 5 | return usePermix(permix) 6 | } 7 | -------------------------------------------------------------------------------- /examples/vue/vite.config.ts: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [vue()], 7 | }) 8 | -------------------------------------------------------------------------------- /examples/enum-based/src/hooks/use-permissions.ts: -------------------------------------------------------------------------------- 1 | import { usePermix } from 'permix/react' 2 | import { permix } from '../lib/permix' 3 | 4 | export function usePermissions() { 5 | return usePermix(permix) 6 | } 7 | -------------------------------------------------------------------------------- /examples/react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /examples/role-based/src/hooks/use-permissions.ts: -------------------------------------------------------------------------------- 1 | import { usePermix } from 'permix/react' 2 | import { permix } from '../lib/permix' 3 | 4 | export function usePermissions() { 5 | return usePermix(permix) 6 | } 7 | -------------------------------------------------------------------------------- /permix/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { "path": "./tsconfig.base.json" }, 4 | { "path": "./tsconfig.react.json" }, 5 | { "path": "./tsconfig.solid.json" } 6 | ], 7 | "files": [] 8 | } 9 | -------------------------------------------------------------------------------- /examples/enum-based/src/lib/user.ts: -------------------------------------------------------------------------------- 1 | export async function getUser() { 2 | // It can be any async function such as fetching from database 3 | return { 4 | id: '1', 5 | role: 'admin' as const, 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/enum-based/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /examples/feature-flags/src/hooks/use-permissions.ts: -------------------------------------------------------------------------------- 1 | import { usePermix } from 'permix/react' 2 | import { permix } from '../lib/permix' 3 | 4 | export function usePermissions() { 5 | return usePermix(permix) 6 | } 7 | -------------------------------------------------------------------------------- /examples/role-based/src/lib/user.ts: -------------------------------------------------------------------------------- 1 | export async function getUser() { 2 | // It can be any async function such as fetching from database 3 | return { 4 | id: '1', 5 | role: 'admin' as const, 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/role-based/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /examples/express-trpc-react/client/src/hooks/use-permissions.ts: -------------------------------------------------------------------------------- 1 | import { usePermix } from 'permix/react' 2 | import { permix } from '../permix' 3 | 4 | export function usePermissions() { 5 | return usePermix(permix) 6 | } 7 | -------------------------------------------------------------------------------- /examples/express-trpc-react/client/src/permix.ts: -------------------------------------------------------------------------------- 1 | import type { PermissionsDefinition } from '@/shared/permix' 2 | import { createPermix } from 'permix' 3 | 4 | export const permix = createPermix() 5 | -------------------------------------------------------------------------------- /examples/feature-flags/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig } from 'vite' 3 | 4 | // https://vite.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }) 8 | -------------------------------------------------------------------------------- /docs/app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { createFromSource } from 'fumadocs-core/search/server' 2 | import { source } from '@/lib/source' 3 | 4 | export const { GET } = createFromSource(source, { 5 | language: 'english', 6 | }) 7 | -------------------------------------------------------------------------------- /permix/src/vue/index.ts: -------------------------------------------------------------------------------- 1 | export { createComponents } from './components' 2 | export type { CheckProps, PermixComponents } from './components' 3 | export { usePermix } from './composables' 4 | export { permixPlugin } from './plugin' 5 | -------------------------------------------------------------------------------- /examples/feature-flags/src/lib/user.ts: -------------------------------------------------------------------------------- 1 | export async function getUser() { 2 | // It can be any async function such as fetching from database 3 | return { 4 | id: '1', 5 | role: 'admin' as const, 6 | isBetaUser: true, 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /permix/src/react/index.ts: -------------------------------------------------------------------------------- 1 | export { createComponents, PermixHydrate, PermixProvider } from './components' 2 | export type { CheckProps, PermixComponents } from './components' 3 | export { usePermix } from './hooks' 4 | export type { PermixContext } from './hooks' 5 | -------------------------------------------------------------------------------- /permix/src/solid/index.ts: -------------------------------------------------------------------------------- 1 | export { createComponents, PermixHydrate, PermixProvider } from './components' 2 | export type { CheckProps, PermixComponents } from './components' 3 | export { usePermix } from './hooks' 4 | export type { PermixContext } from './hooks' 5 | -------------------------------------------------------------------------------- /examples/vue/src/main.ts: -------------------------------------------------------------------------------- 1 | import { permixPlugin } from 'permix/vue' 2 | import { createApp } from 'vue' 3 | import App from './App.vue' 4 | import { permix } from './lib/permix' 5 | import './style.css' 6 | 7 | createApp(App).use(permixPlugin, { permix }).mount('#app') 8 | -------------------------------------------------------------------------------- /docs/app/(home)/code/usage.mdx: -------------------------------------------------------------------------------- 1 | ```tsx 2 | import { permix } from './permix' 3 | 4 | // Can I delete any post? 5 | permix.check('post', 'delete') 6 | 7 | const post = await fetchPost() 8 | 9 | // Can I edit this post? 10 | permix.check('post', 'edit', post) 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/react/src/hooks/posts.ts: -------------------------------------------------------------------------------- 1 | export interface Post { 2 | id: string 3 | authorId: string 4 | } 5 | 6 | const posts = [ 7 | { id: '1', authorId: '1' }, 8 | { id: '2', authorId: '2' }, 9 | ] 10 | 11 | export function usePosts() { 12 | return posts 13 | } 14 | -------------------------------------------------------------------------------- /docs/app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { HomeLayout } from 'fumadocs-ui/layouts/home' 2 | import { baseOptions } from '@/app/layout.shared' 3 | 4 | export default function Layout({ children }: LayoutProps<'/'>) { 5 | return {children} 6 | } 7 | -------------------------------------------------------------------------------- /examples/vue/src/composables/posts.ts: -------------------------------------------------------------------------------- 1 | export interface Post { 2 | id: string 3 | authorId: string 4 | } 5 | 6 | const posts = [ 7 | { id: '1', authorId: '1' }, 8 | { id: '2', authorId: '2' }, 9 | ] 10 | 11 | export function usePosts() { 12 | return posts 13 | } 14 | -------------------------------------------------------------------------------- /examples/express-trpc-react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import { defineConfig } from 'vite' 3 | import tsconfigPaths from 'vite-tsconfig-paths' 4 | 5 | export default defineConfig({ 6 | plugins: [tsconfigPaths(), react()], 7 | root: './client', 8 | }) 9 | -------------------------------------------------------------------------------- /examples/express-trpc-react/client/src/trpc.ts: -------------------------------------------------------------------------------- 1 | import type { AppRouter } from '@/server/main' 2 | import { createTRPCProxyClient, httpBatchLink } from '@trpc/client' 3 | 4 | export const trpc = createTRPCProxyClient({ 5 | links: [httpBatchLink({ url: 'http://localhost:3000/trpc' })], 6 | }) 7 | -------------------------------------------------------------------------------- /examples/enum-based/src/lib/permissions.ts: -------------------------------------------------------------------------------- 1 | export enum PostPermission { 2 | Create = 'create', 3 | Read = 'read', 4 | Update = 'update', 5 | Delete = 'delete', 6 | } 7 | 8 | export enum UserPermission { 9 | Create = 'create', 10 | Read = 'read', 11 | Update = 'update', 12 | Delete = 'delete', 13 | } 14 | -------------------------------------------------------------------------------- /examples/express-trpc-react/shared/trpc.ts: -------------------------------------------------------------------------------- 1 | import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server' 2 | import type { AppRouter } from '@/server/main' 3 | 4 | export type { AppRouter } from '@/server/main' 5 | 6 | export type RouterInput = inferRouterInputs 7 | export type RouterOutput = inferRouterOutputs 8 | -------------------------------------------------------------------------------- /examples/solid/src/hooks/posts.ts: -------------------------------------------------------------------------------- 1 | import { createMemo } from 'solid-js' 2 | 3 | export interface Post { 4 | id: string 5 | authorId: string 6 | } 7 | 8 | const posts = [ 9 | { id: '1', authorId: '1' }, 10 | { id: '2', authorId: '2' }, 11 | ] 12 | 13 | export function usePosts() { 14 | return createMemo(() => posts) 15 | } 16 | -------------------------------------------------------------------------------- /docs/app/(home)/code/init.mdx: -------------------------------------------------------------------------------- 1 | ```tsx 2 | import { createPermix } from 'permix' 3 | 4 | interface Post { 5 | id: string 6 | published: boolean 7 | } 8 | 9 | // Create the permix instance 10 | export const permix = createPermix<{ 11 | post: { 12 | dataType: Post 13 | action: 'create' | 'update' | 'delete' 14 | } 15 | }>() 16 | ``` 17 | -------------------------------------------------------------------------------- /docs/app/llms-full.txt/route.ts: -------------------------------------------------------------------------------- 1 | import { getLLMText } from '@/lib/llm' 2 | import { source } from '@/lib/source' 3 | 4 | export const revalidate = false 5 | 6 | export async function GET() { 7 | const scan = source.getPages().map(getLLMText) 8 | const scanned = await Promise.all(scan) 9 | 10 | return new Response(scanned.join('\n\n')) 11 | } 12 | -------------------------------------------------------------------------------- /examples/express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "start": "tsx main.ts" 7 | }, 8 | "dependencies": { 9 | "express": ">=4", 10 | "permix": "workspace:*" 11 | }, 12 | "devDependencies": { 13 | "@types/express": "^5.0.5", 14 | "tsx": "^4.20.6" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /docs/app/(home)/code/setup.mdx: -------------------------------------------------------------------------------- 1 | ```tsx 2 | import { permix } from './permix' 3 | 4 | // Fetch the user 5 | const user = await fetchUser() 6 | 7 | // Setup the permissions 8 | permix.setup({ 9 | post: { 10 | create: true, 11 | edit: post => post ? !post.published : user.role === 'admin', 12 | delete: user.role === 'admin', 13 | }, 14 | }) 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/vue/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /docs/app/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import { DocsLayout } from 'fumadocs-ui/layouts/docs' 2 | import { baseOptions } from '@/app/layout.shared' 3 | import { source } from '@/lib/source' 4 | 5 | export default function Layout({ children }: LayoutProps<'/docs'>) { 6 | return ( 7 | 8 | {children} 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /examples/react/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/solid/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/enum-based/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/feature-flags/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /examples/role-based/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /permix/scripts/copy-readme.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import path from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | 5 | const currentDir = path.dirname(fileURLToPath(import.meta.url)) 6 | const readmePath = path.join(currentDir, '../..', 'README.md') 7 | const distReadmePath = path.join(currentDir, '..', 'README.md') 8 | 9 | fs.copyFileSync(readmePath, distReadmePath) 10 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # deps 2 | /node_modules 3 | 4 | # generated content 5 | .contentlayer 6 | .content-collections 7 | .source 8 | 9 | # test & build 10 | /coverage 11 | /.next/ 12 | /out/ 13 | /build 14 | *.tsbuildinfo 15 | 16 | # misc 17 | .DS_Store 18 | *.pem 19 | /.pnp 20 | .pnp.js 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # others 26 | .env*.local 27 | .vercel 28 | next-env.d.ts -------------------------------------------------------------------------------- /docs/lib/llm.ts: -------------------------------------------------------------------------------- 1 | import type { InferPageType } from 'fumadocs-core/source' 2 | import type { source } from '@/lib/source' 3 | 4 | export async function getLLMText(page: InferPageType) { 5 | const processed = await page.data.getText('processed') 6 | 7 | return `# ${page.data.title} 8 | URL: https://permix.letstri.dev${page.url} 9 | 10 | ${page.data.description} 11 | 12 | ${processed}` 13 | } 14 | -------------------------------------------------------------------------------- /examples/solid/src/index.tsx: -------------------------------------------------------------------------------- 1 | import { PermixProvider } from 'permix/solid' 2 | import { render } from 'solid-js/web' 3 | /* @refresh reload */ 4 | import App from './App.tsx' 5 | import { permix } from './lib/permix' 6 | import './index.css' 7 | 8 | const root = document.getElementById('root') 9 | 10 | render(() => ( 11 | 12 | 13 | 14 | ), root!) 15 | -------------------------------------------------------------------------------- /permix/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | checkWithRules, 3 | createPermix, 4 | getRules, 5 | type Permix, 6 | type PermixDefinition, 7 | type PermixRules, 8 | } from './create-permix' 9 | export { dehydrate, type DehydratedState, hydrate } from './hydration' 10 | export type { CheckContext, CheckFunctionObject, CheckFunctionParams } from './params' 11 | export { isRulesValid, type MaybePromise } from './utils' 12 | -------------------------------------------------------------------------------- /docs/app/global.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import 'fumadocs-ui/css/neutral.css'; 3 | @import 'fumadocs-ui/css/preset.css'; 4 | @import 'fumadocs-twoslash/twoslash.css'; 5 | 6 | @source '../node_modules/fumadocs-ui/dist/**/*.js'; 7 | @source '../node_modules/fumadocs-openapi/dist/**/*.js'; 8 | 9 | .prose { 10 | --tw-prose-body: hsl(var(--foreground) / 0.8); 11 | } 12 | 13 | :root { 14 | --fd-layout-width: 1400px; 15 | } 16 | -------------------------------------------------------------------------------- /examples/enum-based/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { PermixProvider } from 'permix/react' 2 | import { StrictMode } from 'react' 3 | import { createRoot } from 'react-dom/client' 4 | import App from './App' 5 | import { permix } from './lib/permix' 6 | 7 | createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | 11 | 12 | , 13 | ) 14 | -------------------------------------------------------------------------------- /examples/role-based/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { PermixProvider } from 'permix/react' 2 | import { StrictMode } from 'react' 3 | import { createRoot } from 'react-dom/client' 4 | import App from './App' 5 | import { permix } from './lib/permix' 6 | 7 | createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | 11 | 12 | , 13 | ) 14 | -------------------------------------------------------------------------------- /examples/feature-flags/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { PermixProvider } from 'permix/react' 2 | import { StrictMode } from 'react' 3 | import { createRoot } from 'react-dom/client' 4 | import App from './App' 5 | import { permix } from './lib/permix' 6 | 7 | createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | 11 | 12 | , 13 | ) 14 | -------------------------------------------------------------------------------- /examples/vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + Vue + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docs/app/layout.shared.tsx: -------------------------------------------------------------------------------- 1 | import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared' 2 | 3 | export function baseOptions(): BaseLayoutProps { 4 | return { 5 | githubUrl: 'https://github.com/letstri/permix', 6 | nav: { 7 | title: 'Permix', 8 | }, 9 | links: [ 10 | { 11 | text: 'Documentation', 12 | url: '/docs', 13 | active: 'nested-url', 14 | }, 15 | ], 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/solid/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + Solid + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /permix/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react' 2 | import solid from 'vite-plugin-solid' 3 | import { defineConfig } from 'vitest/config' 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | react({ 8 | include: ['**/src/react/*.ts?(x)'], 9 | }), 10 | solid({ 11 | include: ['**/src/solid/*.ts?(x)'], 12 | }), 13 | ], 14 | test: { 15 | environment: 'happy-dom', 16 | }, 17 | }) 18 | -------------------------------------------------------------------------------- /examples/enum-based/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/role-based/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /permix/src/utils.ts: -------------------------------------------------------------------------------- 1 | export function pick(obj: T, keys: K[]): Pick { 2 | return keys.reduce((acc, key) => { 3 | acc[key] = obj[key] 4 | return acc 5 | }, {} as Pick) 6 | } 7 | 8 | export function omit(obj: T, keys: K[]): Omit { 9 | return Object.fromEntries(Object.entries(obj).filter(([key]) => !keys.includes(key as K))) as Omit 10 | } 11 | -------------------------------------------------------------------------------- /examples/feature-flags/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/express-trpc-react/client/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { PermixProvider } from 'permix/react' 2 | import { StrictMode } from 'react' 3 | import { createRoot } from 'react-dom/client' 4 | import { permix } from '@/shared/permix' 5 | import App from './App.tsx' 6 | 7 | createRoot(document.getElementById('root')!).render( 8 | 9 | 10 | 11 | 12 | , 13 | ) 14 | -------------------------------------------------------------------------------- /examples/express-trpc-react/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { PermixProvider } from 'permix/react' 2 | import { StrictMode } from 'react' 3 | import { createRoot } from 'react-dom/client' 4 | import App from './App.tsx' 5 | import { permix } from './lib/permix.ts' 6 | import './index.css' 7 | 8 | createRoot(document.getElementById('root')!).render( 9 | 10 | 11 | 12 | 13 | , 14 | ) 15 | -------------------------------------------------------------------------------- /examples/vue/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 18 | 19 | 44 | -------------------------------------------------------------------------------- /permix/src/solid/hooks.ts: -------------------------------------------------------------------------------- 1 | import type { Permix, PermixDefinition, PermixRules } from '../core/create-permix' 2 | import { createContext, useContext } from 'solid-js' 3 | import { checkWithRules, getRules, validatePermix } from '../core/create-permix' 4 | 5 | export interface PermixContext { 6 | permix: Permix 7 | isReady: boolean 8 | rules?: PermixRules 9 | } 10 | 11 | export const Context = createContext>(null!) 12 | 13 | export function usePermixContext() { 14 | const context = useContext(Context) 15 | 16 | if (!context) { 17 | throw new Error('[Permix]: Looks like you forgot to wrap your app with ') 18 | } 19 | 20 | return context 21 | } 22 | 23 | /** 24 | * Hook that provides the Permix reactive methods to your Solid components. 25 | * 26 | * @link https://permix.letstri.dev/docs/integrations/solid 27 | */ 28 | 29 | export function usePermix(permix: Permix) { 30 | validatePermix(permix) 31 | 32 | const context = usePermixContext() 33 | 34 | validatePermix(context.permix) 35 | 36 | const check: typeof permix.check = (...args) => { 37 | return checkWithRules(context.rules ?? getRules(context.permix), ...args) 38 | } 39 | 40 | return { check, isReady: () => context.isReady } 41 | } 42 | -------------------------------------------------------------------------------- /docs/content/docs/guide/events.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Events 3 | description: Learn how to handle permission updates in your application 4 | --- 5 | 6 | ## Overview 7 | 8 | Permix provides an event system that allows you to react to permission changes in your application. Each event provides type-safe data and hooks to register handlers. 9 | 10 | ## Usage 11 | 12 | You can register event handlers using the `hook` and `hookOnce` methods: 13 | 14 | ```ts 15 | const permix = createPermix<{ 16 | post: { 17 | action: 'create' | 'read' 18 | } 19 | }>() 20 | 21 | // The handler will be called every time setup is executed 22 | permix.hook('setup', () => { 23 | console.log('Permissions were updated') 24 | }) 25 | 26 | // The handler will be called only once 27 | permix.hookOnce('setup', () => { 28 | console.log('Permissions were updated once') 29 | }) 30 | 31 | // Calling `setup` method triggers the `setup` event 32 | // and `ready` event if called on the client side 33 | permix.setup({ 34 | post: { 35 | create: true, 36 | read: true 37 | } 38 | }) 39 | ``` 40 | 41 | Available events: 42 | 43 | - `setup` - Triggered when permissions are updated through the `setup` method. 44 | - `ready` - Triggered when the permissions are ready to be used. 45 | - `hydrate` - Triggered when the permissions are hydrated from the server. 46 | -------------------------------------------------------------------------------- /permix/src/core/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { isRulesValid } from './utils' 3 | 4 | describe('utils', () => { 5 | it('should return true for valid permissions object', () => { 6 | const permissions = { 7 | post: { 8 | create: true, 9 | read: false, 10 | }, 11 | } 12 | 13 | expect(isRulesValid(permissions)).toBe(true) 14 | }) 15 | 16 | it('should return true for permissions with function values', () => { 17 | const permissions = { 18 | post: { 19 | create: () => true, 20 | read: (data: any) => data.id === '1', 21 | }, 22 | } 23 | 24 | expect(isRulesValid(permissions)).toBe(true) 25 | }) 26 | 27 | it('should return false for invalid permissions object', () => { 28 | const permissions = { 29 | post: { 30 | create: true, 31 | read: 'string', 32 | }, 33 | } 34 | 35 | expect(isRulesValid(permissions)).toBe(false) 36 | }) 37 | 38 | it('should return false for null value', () => { 39 | expect(isRulesValid(null)).toBe(false) 40 | }) 41 | 42 | it('should return false for non-object value', () => { 43 | expect(isRulesValid('string')).toBe(false) 44 | expect(isRulesValid(123)).toBe(false) 45 | expect(isRulesValid(true)).toBe(false) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /permix/src/react/hooks.ts: -------------------------------------------------------------------------------- 1 | import type { Permix, PermixDefinition, PermixRules } from '../core/create-permix' 2 | import * as React from 'react' 3 | import { checkWithRules, getRules, validatePermix } from '../core/create-permix' 4 | 5 | export interface PermixContext { 6 | permix: Permix 7 | isReady: boolean 8 | rules?: PermixRules 9 | } 10 | 11 | export const Context = React.createContext>(null!) 12 | 13 | export function usePermixContext() { 14 | const context = React.useContext(Context) 15 | 16 | if (!context) { 17 | throw new Error('[Permix]: Looks like you forgot to wrap your app with ') 18 | } 19 | 20 | return context 21 | } 22 | 23 | /** 24 | * Hook that provides the Permix reactive methods to your React components. 25 | * 26 | * @link https://permix.letstri.dev/docs/integrations/react 27 | */ 28 | 29 | export function usePermix(permix: Permix) { 30 | validatePermix(permix) 31 | 32 | const { permix: permixContext, isReady, rules } = usePermixContext() 33 | 34 | validatePermix(permixContext) 35 | 36 | const check: typeof permix.check = React.useCallback((...args) => { 37 | return checkWithRules(rules ?? getRules(permixContext), ...args) 38 | }, [rules, permixContext]) 39 | 40 | return { check, isReady } 41 | } 42 | -------------------------------------------------------------------------------- /docs/content/docs/comparison.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Comparison 3 | description: Comparison with other libraries 4 | icon: RiArrowLeftRightFill 5 | --- 6 | 7 | ## Overview 8 | 9 | Permix is a library that provides a way to manage permissions in your application. It is designed to be used with React, Vue, etc. But not only Permix can manage permissions, there are other libraries that can do the same thing. 10 | 11 | ## Comparison 12 | 13 | | Feature | Permix | CASL | 14 | | --- | --- | --- | 15 | | Type-safe | ✅ Native | ✅ Via [external type](https://casl.js.org/v6/en/advanced/typescript#permissions-inference) | 16 | | Saving rules | ✅ Via [template](/docs/guide/template) | ✅ Via [AppAbility](https://casl.js.org/v6/en/cookbook/cache-rules#the-issue) | 17 | | Hydration | ✅ Native | ⚠️ Via custom implementation | 18 | | Entity | ✅ Depends on props of a [passed object](/docs/guide/setup#type-based) | ⚠️ Class-based yes, but object-based via [external function](https://casl.js.org/v6/en/guide/subject-type-detection) | 19 | | Events | ✅ | ❌ | 20 | | Simple DX | ✅ Create instance, use built-in integrations | ❌ In CASL you need to manage a lot of stuff manually (type-safe, hydration, etc.) | 21 | | Modernity | ✅ Uses modern updates and features of each lib and framework | ❌ CASL was created a long time ago and hasn't updated the core | 22 | | Size | ~2.5kb | ~21kb (ability) + ~2.5kb (framework adapter) | 23 | -------------------------------------------------------------------------------- /examples/role-based/src/lib/permix.ts: -------------------------------------------------------------------------------- 1 | import { createPermix } from 'permix' 2 | import { createComponents } from 'permix/react' 3 | import { getUser } from './user' 4 | 5 | // Define permix instance 6 | export const permix = createPermix<{ 7 | post: { 8 | action: 'create' | 'read' | 'update' | 'delete' 9 | } 10 | user: { 11 | action: 'create' | 'read' | 'update' | 'delete' 12 | } 13 | }>() 14 | 15 | // Not necessary, but you can use components to check permissions 16 | export const { Check } = createComponents(permix) 17 | 18 | // Define the permissions for each role 19 | export const adminPermissions = permix.template({ 20 | post: { 21 | create: true, 22 | read: true, 23 | update: true, 24 | delete: true, 25 | }, 26 | user: { 27 | create: true, 28 | read: true, 29 | update: true, 30 | delete: true, 31 | }, 32 | }) 33 | 34 | export const userPermissions = permix.template({ 35 | post: { 36 | read: true, 37 | update: true, 38 | delete: false, 39 | create: false, 40 | }, 41 | user: { 42 | read: true, 43 | update: true, 44 | delete: false, 45 | create: false, 46 | }, 47 | }) 48 | 49 | export async function setupPermissions() { 50 | const user = await getUser() 51 | 52 | const rolesMap = { 53 | admin: () => adminPermissions(), 54 | user: () => userPermissions(), 55 | } 56 | 57 | permix.setup(rolesMap[user.role]()) 58 | } 59 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@permix/docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "prebuild": "cd ../permix && pnpm run build", 7 | "build": "next build", 8 | "predeploy": "cd ../permix && pnpm run build", 9 | "dev": "next dev --turbopack", 10 | "start": "next start", 11 | "postinstall": "fumadocs-mdx" 12 | }, 13 | "dependencies": { 14 | "@remixicon/react": "^4.7.0", 15 | "@tailwindcss/postcss": "^4.1.16", 16 | "@vercel/analytics": "^1.5.0", 17 | "class-variance-authority": "^0.7.1", 18 | "fumadocs-core": "16.0.7", 19 | "fumadocs-mdx": "13.0.5", 20 | "fumadocs-twoslash": "^3.1.9", 21 | "fumadocs-ui": "16.0.7", 22 | "lucide-react": "^0.552.0", 23 | "next": "16.0.1", 24 | "permix": "workspace:*", 25 | "react": "^19.2.0", 26 | "react-dom": "^19.2.0", 27 | "tailwind-merge": "^3.3.1", 28 | "twoslash": "^0.3.4" 29 | }, 30 | "devDependencies": { 31 | "@orpc/server": ">=1.10.3", 32 | "@trpc/server": ">=11.7.1", 33 | "@types/express": "^5.0.5", 34 | "@types/mdx": "^2.0.13", 35 | "@types/node": "24.10.0", 36 | "@types/react": "^19.2.2", 37 | "@types/react-dom": "^19.2.2", 38 | "autoprefixer": "^10.4.21", 39 | "elysia": ">=1.4.15", 40 | "express": ">=5", 41 | "fastify": ">=5.6.1", 42 | "hono": ">=4.10.4", 43 | "postcss": "^8.5.6", 44 | "tailwindcss": "^4.1.16", 45 | "typescript": "^5.9.3" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/solid/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | @media (prefers-color-scheme: light) { 58 | :root { 59 | color: #213547; 60 | background-color: #ffffff; 61 | } 62 | a:hover { 63 | color: #747bff; 64 | } 65 | button { 66 | background-color: #f9f9f9; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/react/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/solid/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vue/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/react/src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | @media (prefers-color-scheme: light) { 58 | :root { 59 | color: #213547; 60 | background-color: #ffffff; 61 | } 62 | a:hover { 63 | color: #747bff; 64 | } 65 | button { 66 | background-color: #f9f9f9; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /docs/content/docs/meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "---Getting Started---", 4 | "index", 5 | "quick-start", 6 | "comparison", 7 | "---Guide---", 8 | "guide/instance", 9 | "guide/setup", 10 | "guide/check", 11 | "guide/template", 12 | "guide/events", 13 | "guide/hydration", 14 | "guide/ready", 15 | "---Integrations---", 16 | "integrations/react", 17 | "integrations/vue", 18 | "integrations/solid", 19 | "integrations/node", 20 | "integrations/trpc", 21 | "integrations/orpc", 22 | "integrations/express", 23 | "integrations/hono", 24 | "integrations/elysia", 25 | "integrations/fastify", 26 | "---Examples---", 27 | "[role-based](https://github.com/letstri/permix/tree/main/examples/role-based)", 28 | "[enum-based](https://github.com/letstri/permix/tree/main/examples/enum-based)", 29 | "[feature-flags](https://github.com/letstri/permix/tree/main/examples/feature-flags)", 30 | "[react](https://github.com/letstri/permix/tree/main/examples/react)", 31 | "[vue](https://github.com/letstri/permix/tree/main/examples/vue)", 32 | "[solid](https://github.com/letstri/permix/tree/main/examples/solid)", 33 | "[express](https://github.com/letstri/permix/tree/main/examples/express)", 34 | "[express-trpc-react](https://github.com/letstri/permix/tree/main/examples/express-trpc-react)", 35 | "---LLMs---", 36 | "[llms.txt](/llms.txt)", 37 | "[llms-full.txt](/llms-full.txt)" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /docs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import type { ReactNode } from 'react' 3 | import { Analytics } from '@vercel/analytics/react' 4 | import { RootProvider } from 'fumadocs-ui/provider/next' 5 | import { Inter } from 'next/font/google' 6 | import './global.css' 7 | 8 | const inter = Inter({ 9 | subsets: ['latin'], 10 | }) 11 | 12 | export const metadata: Metadata = { 13 | title: 'Permix - Type-safe permissions management for JavaScript', 14 | description: 'A lightweight, framework-agnostic, type-safe permissions management library for client-side and server-side JavaScript applications.', 15 | keywords: ['permissions', 'authorization', 'acl', 'access-control', 'typescript', 'react', 'vue', 'type-safe', 'rbac', 'security', 'permissions-management', 'frontend', 'javascript'], 16 | openGraph: { 17 | title: 'Permix - Type-safe permissions management for TypeScript', 18 | description: 'A lightweight, framework-agnostic, type-safe permissions management library for client-side and server-side TypeScript applications.', 19 | url: 'https://permix.letstri.dev', 20 | siteName: 'Permix', 21 | }, 22 | } 23 | 24 | export default function Layout({ children }: { children: ReactNode }) { 25 | return ( 26 | 27 | 28 | 29 | {children} 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /examples/react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react' 2 | import { usePermissions } from './hooks/permissions' 3 | import { usePosts } from './hooks/posts' 4 | import { useUser } from './hooks/user' 5 | import { Check, setupPermix } from './lib/permix' 6 | import './App.css' 7 | 8 | function App() { 9 | const user = useUser() 10 | const { check, isReady } = usePermissions() 11 | const posts = usePosts() 12 | 13 | useEffect(() => { 14 | if (user) { 15 | setupPermix(user) 16 | } 17 | }, [user]) 18 | 19 | return ( 20 | <> 21 | Is Permix ready? 22 | {' '} 23 | {isReady ? 'Yes' : 'No'} 24 |
25 | My user is 26 | {' '} 27 | {user?.id ?? '...'} 28 |
29 | {posts.map(post => ( 30 |
31 |

32 | Post 33 | {' '} 34 | {post.id} 35 |

36 | Can I edit the post where authorId is 37 | {post.authorId} 38 | ? 39 |
40 | {check('post', 'edit', post) ? 'Yes' : 'No'} 41 |
42 | 48 | I can edit a post inside the Check component 49 | 50 |
51 |
52 | ))} 53 | 54 | ) 55 | } 56 | 57 | export default App 58 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and publish 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build-and-publish: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | contents: read 11 | id-token: write 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: pnpm/action-setup@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version-file: .node-version 18 | cache: pnpm 19 | registry-url: 'https://registry.npmjs.org' 20 | - run: pnpm i --frozen-lockfile 21 | - name: Run linter 22 | run: pnpm run lint 23 | - name: Check version and publish if needed 24 | run: | 25 | cd permix 26 | PACKAGE_VERSION=$(node -p "require('./package.json').version") 27 | NPM_VERSION=$(npm view permix version 2>/dev/null || echo "0.0.0") 28 | if [ "$PACKAGE_VERSION" != "$NPM_VERSION" ]; then 29 | npm config set sign-git-tag true 30 | if [[ "$PACKAGE_VERSION" == *"beta"* ]]; then 31 | pnpm publish --no-git-checks --tag beta 32 | elif [[ "$PACKAGE_VERSION" == *"rc"* ]]; then 33 | pnpm publish --no-git-checks --tag rc 34 | else 35 | pnpm publish --no-git-checks 36 | fi 37 | else 38 | echo "Package version $PACKAGE_VERSION is already published" 39 | fi 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | NPM_CONFIG_PROVENANCE: true 43 | -------------------------------------------------------------------------------- /examples/solid/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { createEffect } from 'solid-js' 2 | import { usePermissions } from './hooks/permissions' 3 | import { usePosts } from './hooks/posts' 4 | import { useUser } from './hooks/user' 5 | import { Check, setupPermix } from './lib/permix' 6 | import './App.css' 7 | 8 | function App() { 9 | const user = useUser() 10 | const { check, isReady } = usePermissions() 11 | const posts = usePosts() 12 | 13 | createEffect(() => { 14 | const value = user() 15 | if (value) { 16 | setupPermix(value) 17 | } 18 | }) 19 | 20 | return ( 21 | <> 22 | Is Permix ready? 23 | {' '} 24 | {isReady() ? 'Yes' : 'No'} 25 |
26 | My user is 27 | {' '} 28 | {user()?.id ?? '...'} 29 |
30 | {posts().map(post => ( 31 |
32 |

33 | Post 34 | {' '} 35 | {post.id} 36 |

37 | Can I edit the post where authorId is 38 | {post.authorId} 39 | ? 40 |
41 | {check('post', 'edit', post) ? 'Yes' : 'No'} 42 |
43 | 49 | I can edit a post inside the Check component 50 | 51 |
52 |
53 | ))} 54 | 55 | ) 56 | } 57 | 58 | export default App 59 | -------------------------------------------------------------------------------- /permix/src/core/params.ts: -------------------------------------------------------------------------------- 1 | import type { PermixDefinition } from './create-permix' 2 | 3 | export type CheckFunctionParams = Definition[K]['dataRequired'] extends true 4 | ? [ 5 | entity: K, 6 | action: 'all' | 'any' | Definition[K]['action'] | Definition[K]['action'][], 7 | data: Definition[K]['dataType'], 8 | ] : [ 9 | entity: K, 10 | action: 'all' | 'any' | Definition[K]['action'] | Definition[K]['action'][], 11 | data?: Definition[K]['dataType'], 12 | ] 13 | 14 | export type CheckFunctionObject = Definition[K]['dataRequired'] extends true 15 | ? { 16 | entity: K 17 | action: 'all' | 'any' | Definition[K]['action'] | Definition[K]['action'][] 18 | data: Definition[K]['dataType'] 19 | } 20 | : { 21 | entity: K 22 | action: 'all' | 'any' | Definition[K]['action'] | Definition[K]['action'][] 23 | data?: Definition[K]['dataType'] 24 | } 25 | 26 | export interface CheckContext { 27 | entity: keyof Definition 28 | actions: Definition[keyof Definition]['action'][] 29 | } 30 | 31 | export function createCheckContext( 32 | ...params: CheckFunctionParams 33 | ): CheckContext { 34 | const [entity, action] = params 35 | 36 | return { 37 | entity, 38 | actions: Array.isArray(action) ? action : [action], 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /examples/solid/src/assets/solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // Disable the default formatter, use eslint instead 3 | "prettier.enable": false, 4 | "editor.formatOnSave": false, 5 | 6 | // Auto fix 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll.eslint": "explicit", 9 | "source.organizeImports": "never" 10 | }, 11 | 12 | // Silent the stylistic rules in you IDE, but still auto fix them 13 | "eslint.rules.customizations": [ 14 | { "rule": "style/*", "severity": "off", "fixable": true }, 15 | { "rule": "format/*", "severity": "off", "fixable": true }, 16 | { "rule": "*-indent", "severity": "off", "fixable": true }, 17 | { "rule": "*-spacing", "severity": "off", "fixable": true }, 18 | { "rule": "*-spaces", "severity": "off", "fixable": true }, 19 | { "rule": "*-order", "severity": "off", "fixable": true }, 20 | { "rule": "*-dangle", "severity": "off", "fixable": true }, 21 | { "rule": "*-newline", "severity": "off", "fixable": true }, 22 | { "rule": "*quotes", "severity": "off", "fixable": true }, 23 | { "rule": "*semi", "severity": "off", "fixable": true } 24 | ], 25 | 26 | // Enable eslint for all supported languages 27 | "eslint.validate": [ 28 | "javascript", 29 | "javascriptreact", 30 | "typescript", 31 | "typescriptreact", 32 | "vue", 33 | "html", 34 | "markdown", 35 | "json", 36 | "json5", 37 | "jsonc", 38 | "yaml", 39 | "toml", 40 | "xml", 41 | "gql", 42 | "graphql", 43 | "astro", 44 | "svelte", 45 | "css", 46 | "less", 47 | "scss", 48 | "pcss", 49 | "postcss" 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /permix/src/core/hooks.ts: -------------------------------------------------------------------------------- 1 | type HookHandler = (...args: T[]) => void 2 | 3 | export function createHooks>() { 4 | let hooks: Record = {} 5 | 6 | const hook = (name: K, fn: T[K]) => { 7 | if (!hooks[name as string]) { 8 | hooks[name as string] = [] 9 | } 10 | hooks[name as string].push(fn) 11 | 12 | return () => { 13 | const index = hooks[name as string].indexOf(fn) 14 | if (index !== -1) { 15 | hooks[name as string].splice(index, 1) 16 | } 17 | } 18 | } 19 | 20 | const hookOnce = (name: K, fn: T[K]) => { 21 | const remove = hook(name, ((...args) => { 22 | remove() 23 | fn(...args) 24 | }) as T[K]) 25 | } 26 | 27 | const removeHook = (name: K, fn: T[K]) => { 28 | if (hooks[name as string]) { 29 | const index = hooks[name as string].indexOf(fn) 30 | if (index !== -1) { 31 | hooks[name as string].splice(index, 1) 32 | } 33 | } 34 | } 35 | 36 | const callHook = (name: K, ...args: Parameters) => { 37 | if (hooks[name as string]) { 38 | for (const fn of hooks[name as string]) { 39 | fn(...args) 40 | } 41 | } 42 | } 43 | 44 | const clearHook = (name: K) => { 45 | delete hooks[name as string] 46 | } 47 | 48 | const clearAllHooks = () => { 49 | hooks = {} 50 | } 51 | 52 | return { 53 | hook, 54 | hookOnce, 55 | removeHook, 56 | callHook, 57 | clearHook, 58 | clearAllHooks, 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/vue/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | .card { 58 | padding: 2em; 59 | } 60 | 61 | #app { 62 | max-width: 1280px; 63 | margin: 0 auto; 64 | padding: 2rem; 65 | text-align: center; 66 | } 67 | 68 | @media (prefers-color-scheme: light) { 69 | :root { 70 | color: #213547; 71 | background-color: #ffffff; 72 | } 73 | a:hover { 74 | color: #747bff; 75 | } 76 | button { 77 | background-color: #f9f9f9; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /examples/enum-based/src/lib/permix.ts: -------------------------------------------------------------------------------- 1 | import { createPermix } from 'permix' 2 | import { createComponents } from 'permix/react' 3 | import { PostPermission, UserPermission } from './permissions' 4 | import { getUser } from './user' 5 | 6 | // Define permix instance 7 | export const permix = createPermix<{ 8 | post: { 9 | action: PostPermission 10 | } 11 | user: { 12 | action: UserPermission 13 | } 14 | }>() 15 | 16 | // Not necessary, but you can use components to check permissions 17 | export const { Check } = createComponents(permix) 18 | 19 | // Define the permissions for each role 20 | export const adminPermissions = permix.template({ 21 | post: { 22 | [PostPermission.Create]: true, 23 | [PostPermission.Read]: true, 24 | [PostPermission.Update]: true, 25 | [PostPermission.Delete]: true, 26 | }, 27 | user: { 28 | [UserPermission.Create]: true, 29 | [UserPermission.Read]: true, 30 | [UserPermission.Update]: true, 31 | [UserPermission.Delete]: true, 32 | }, 33 | }) 34 | 35 | // You can also use string literals - TypeScript will infer the enum type 36 | export const userPermissions = permix.template({ 37 | post: { 38 | create: false, 39 | read: true, 40 | update: true, 41 | delete: false, 42 | }, 43 | user: { 44 | create: false, 45 | read: true, 46 | update: true, 47 | delete: false, 48 | }, 49 | }) 50 | 51 | export async function setupPermissions() { 52 | const user = await getUser() 53 | 54 | const rolesMap = { 55 | admin: () => adminPermissions(), 56 | user: () => userPermissions(), 57 | } 58 | 59 | permix.setup(rolesMap[user.role]()) 60 | } 61 | -------------------------------------------------------------------------------- /permix/src/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { omit, pick } from './utils' 3 | 4 | describe('utils', () => { 5 | describe('pick', () => { 6 | it('should pick specified keys from an object', () => { 7 | const obj = { a: 1, b: 2, c: 3 } 8 | const result = pick(obj, ['a', 'c']) 9 | 10 | expect(result).toEqual({ a: 1, c: 3 }) 11 | expect(result).not.toHaveProperty('b') 12 | }) 13 | 14 | it('should return empty object when no keys are provided', () => { 15 | const obj = { a: 1, b: 2, c: 3 } 16 | const result = pick(obj, []) 17 | 18 | expect(result).toEqual({}) 19 | }) 20 | 21 | it('should handle nested objects', () => { 22 | const obj = { a: 1, b: { x: 10, y: 20 }, c: 3 } 23 | const result = pick(obj, ['a', 'b']) 24 | 25 | expect(result).toEqual({ a: 1, b: { x: 10, y: 20 } }) 26 | }) 27 | }) 28 | 29 | describe('omit', () => { 30 | it('should omit specified keys from an object', () => { 31 | const obj = { a: 1, b: 2, c: 3 } 32 | const result = omit(obj, ['a', 'c']) 33 | 34 | expect(result).toEqual({ b: 2 }) 35 | expect(result).not.toHaveProperty('a') 36 | expect(result).not.toHaveProperty('c') 37 | }) 38 | 39 | it('should return the original object when no keys are provided', () => { 40 | const obj = { a: 1, b: 2, c: 3 } 41 | const result = omit(obj, []) 42 | 43 | expect(result).toEqual(obj) 44 | }) 45 | 46 | it('should handle nested objects', () => { 47 | const obj = { a: 1, b: { x: 10, y: 20 }, c: 3 } 48 | const result = omit(obj, ['a', 'c']) 49 | 50 | expect(result).toEqual({ b: { x: 10, y: 20 } }) 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /examples/react/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | export default tseslint.config({ 18 | languageOptions: { 19 | // other options... 20 | parserOptions: { 21 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 22 | tsconfigRootDir: import.meta.dirname, 23 | }, 24 | }, 25 | }) 26 | ``` 27 | 28 | - Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked` 29 | - Optionally add `...tseslint.configs.stylisticTypeChecked` 30 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config: 31 | 32 | ```js 33 | // eslint.config.js 34 | import react from 'eslint-plugin-react' 35 | 36 | export default tseslint.config({ 37 | // Set the react version 38 | settings: { react: { version: '18.3' } }, 39 | plugins: { 40 | // Add the react plugin 41 | react, 42 | }, 43 | rules: { 44 | // other rules... 45 | // Enable its recommended rules 46 | ...react.configs.recommended.rules, 47 | ...react.configs['jsx-runtime'].rules, 48 | }, 49 | }) 50 | ``` 51 | -------------------------------------------------------------------------------- /permix/src/vue/components.ts: -------------------------------------------------------------------------------- 1 | import type { SetupContext, SlotsType, VNode } from 'vue' 2 | import type { Permix, PermixDefinition } from '../core/create-permix' 3 | import type { CheckFunctionObject } from '../core/params' 4 | import { usePermix } from './composables' 5 | 6 | export type CheckProps = CheckFunctionObject & { 7 | reverse?: boolean 8 | } 9 | 10 | type CheckContext = SetupContext> 14 | 15 | export interface PermixComponents { 16 | Check: ( 17 | props: CheckProps, 18 | context: CheckContext, 19 | ) => VNode | VNode[] | undefined 20 | } 21 | 22 | export function createComponents(permix: Permix): PermixComponents { 23 | function Check( 24 | props: CheckProps, 25 | context: CheckContext, 26 | ) { 27 | const { check } = usePermix(permix) 28 | 29 | const hasPermission = check(props.entity, props.action, props.data) 30 | return props.reverse 31 | ? (hasPermission ? context.slots.otherwise?.() : context.slots.default?.()) 32 | : (hasPermission ? context.slots.default?.() : context.slots.otherwise?.()) 33 | } 34 | 35 | Check.inheritAttrs = false 36 | Check.props = { 37 | entity: { 38 | type: String, 39 | required: true, 40 | }, 41 | action: { 42 | type: [String, Array], 43 | required: true, 44 | }, 45 | data: { 46 | type: Object, 47 | required: false, 48 | }, 49 | reverse: { 50 | type: Boolean, 51 | required: false, 52 | default: false, 53 | }, 54 | } 55 | 56 | return { 57 | Check, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/express-trpc-react/client/src/App.tsx: -------------------------------------------------------------------------------- 1 | import type { RouterOutput } from '@/shared/trpc' 2 | import { useEffect, useState } from 'react' 3 | import { getRules } from '@/shared/permix' 4 | import { Check } from './components/permix' 5 | import { usePermissions } from './hooks/use-permissions' 6 | import { permix } from './permix' 7 | import { trpc } from './trpc' 8 | 9 | export default function App() { 10 | const { check, isReady } = usePermissions() 11 | const [users, setUsers] = useState([]) 12 | 13 | const canReadUser = check('user', 'read') 14 | 15 | useEffect(() => { 16 | // Imagine this is a request from the server that gets the user from the request 17 | const user = { 18 | role: 'user' as const, 19 | } 20 | 21 | permix.setup(getRules(user.role)) 22 | }, []) 23 | 24 | useEffect(() => { 25 | if (canReadUser) { 26 | trpc.userList.query().then((users) => { 27 | setUsers(users) 28 | }) 29 | } 30 | }, [canReadUser]) 31 | 32 | return ( 33 | <> 34 | Is Permix ready? 35 | {' '} 36 | {isReady ? 'Yes' : 'No'} 37 |
38 | Can I read a user? 39 | {' '} 40 | {check('user', 'read') ? 'Yes' : 'No'} 41 |
42 | You don't have permission to read a user}> 43 | Can I read a user inside the Check component? 44 | 45 |
46 | {users.map(user => ( 47 |
{user.name}
48 | ))} 49 |
50 | You don't have permission to create a user}> 51 | 54 | 55 | 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /permix/build.config.ts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'node:fs/promises' 2 | import babel from '@rollup/plugin-babel' 3 | import { defineBuildConfig } from 'unbuild' 4 | 5 | export default defineBuildConfig({ 6 | name: 'permix', 7 | entries: [ 8 | './src/core/index.ts', 9 | './src/react/index.ts', 10 | './src/vue/index.ts', 11 | './src/trpc/index.ts', 12 | './src/orpc/index.ts', 13 | './src/express/index.ts', 14 | './src/hono/index.ts', 15 | './src/node/index.ts', 16 | './src/elysia/index.ts', 17 | './src/fastify/index.ts', 18 | './src/solid/index.ts', 19 | ], 20 | declaration: true, 21 | clean: true, 22 | rollup: { 23 | // Disable esbuild in favor of Babel for SolidJS support 24 | esbuild: false, 25 | }, 26 | hooks: { 27 | 'rollup:options': async (config, options) => { 28 | options.plugins.unshift( 29 | babel({ 30 | babelHelpers: 'bundled', 31 | include: ['src/**'], 32 | exclude: ['src/solid/**', 'src/react/**'], 33 | presets: ['@babel/preset-typescript'], 34 | extensions: ['.ts', '.js'], 35 | }), 36 | ) 37 | options.plugins.unshift( 38 | babel({ 39 | babelHelpers: 'bundled', 40 | include: ['src/solid/**'], 41 | presets: ['@babel/preset-typescript', 'solid'], 42 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 43 | }), 44 | ) 45 | options.plugins.unshift( 46 | babel({ 47 | babelHelpers: 'bundled', 48 | include: ['src/react/**'], 49 | presets: ['@babel/preset-typescript', '@babel/preset-react'], 50 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 51 | }), 52 | ) 53 | }, 54 | 'build:done': async () => { 55 | const file = await readFile('./dist/react/index.mjs', 'utf-8') 56 | await writeFile('./dist/react/index.mjs', `'use client';\n\n${file}`) 57 | }, 58 | }, 59 | }) 60 | -------------------------------------------------------------------------------- /docs/app/docs/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import { createRelativeLink } from 'fumadocs-ui/mdx' 3 | import { 4 | DocsBody, 5 | DocsDescription, 6 | DocsPage, 7 | DocsTitle, 8 | } from 'fumadocs-ui/page' 9 | import { notFound } from 'next/navigation' 10 | import { getMDXComponents } from '@/app/mdx-components' 11 | import { LLMCopyButton, ViewOptions } from '@/components/ai/page-actions' 12 | import { source } from '@/lib/source' 13 | 14 | export default async function Page(props: PageProps<'/docs/[[...slug]]'>) { 15 | const params = await props.params 16 | const page = source.getPage(params.slug) 17 | 18 | if (!page) 19 | notFound() 20 | 21 | const MDX = page.data.body 22 | const markdownUrl = page.url === '/docs' ? '/docs/index.mdx' : `${page.url}.mdx` 23 | 24 | return ( 25 | 26 | {page.data.title} 27 | {page.data.description} 28 |
29 | 30 | 34 |
35 | 36 | 41 | 42 |
43 | ) 44 | } 45 | 46 | export async function generateStaticParams() { 47 | return source.generateParams() 48 | } 49 | 50 | export async function generateMetadata( 51 | props: PageProps<'/docs/[[...slug]]'>, 52 | ): Promise { 53 | const params = await props.params 54 | const page = source.getPage(params.slug) 55 | 56 | if (!page) 57 | notFound() 58 | 59 | return { 60 | title: page.data.title, 61 | description: page.data.description, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /permix/src/elysia/create-permix.ts: -------------------------------------------------------------------------------- 1 | import type { Context } from 'elysia' 2 | import type { Permix, PermixDefinition, PermixRules } from '../core/create-permix' 3 | import type { CheckContext, CheckFunctionParams } from '../core/params' 4 | import type { MaybePromise } from '../core/utils' 5 | import { createPermix as createPermixCore } from '../core/create-permix' 6 | import { createCheckContext } from '../core/params' 7 | import { createTemplate } from '../core/template' 8 | import { pick } from '../utils' 9 | 10 | export interface ElysiaContext { 11 | context: Context 12 | } 13 | 14 | export interface PermixOptions { 15 | /** 16 | * Custom error handler 17 | */ 18 | onForbidden?: (params: CheckContext & ElysiaContext) => MaybePromise 19 | } 20 | 21 | /** 22 | * Create a middleware function that checks permissions for Elysia routes. 23 | * 24 | * @link https://permix.letstri.dev/docs/integrations/elysia 25 | */ 26 | export function createPermix( 27 | { 28 | onForbidden = ({ context }) => { 29 | context.set.status = 403 30 | return { error: 'Forbidden' } 31 | }, 32 | }: PermixOptions = {}, 33 | ) { 34 | const derive = (rules: PermixRules) => ({ 35 | permix: pick(createPermixCore(rules), ['check', 'dehydrate']), 36 | }) 37 | 38 | const checkHandler = (...params: CheckFunctionParams) => { 39 | return async (context: Context & { permix: Pick, 'check' | 'dehydrate'> }) => { 40 | if (!context.permix) { 41 | throw new Error('[Permix]: Instance not found. Please use the `setupMiddleware` function.') 42 | } 43 | 44 | const hasPermission = context.permix.check(...params) 45 | 46 | if (!hasPermission) { 47 | return onForbidden({ 48 | context, 49 | ...createCheckContext(...params), 50 | }) 51 | } 52 | } 53 | } 54 | 55 | function template(...params: Parameters>) { 56 | return createTemplate(...params) 57 | } 58 | 59 | return { 60 | derive, 61 | checkHandler, 62 | template, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/express-trpc-react/server/main.ts: -------------------------------------------------------------------------------- 1 | import type { PermissionsDefinition } from '@/shared/permix' 2 | import { initTRPC, TRPCError } from '@trpc/server' 3 | import * as trpcExpress from '@trpc/server/adapters/express' 4 | import cors from 'cors' 5 | import express from 'express' 6 | import { createPermix } from 'permix/trpc' 7 | import { z } from 'zod' 8 | import { getRules } from '@/shared/permix' 9 | 10 | const app = express() 11 | 12 | app.use(cors()) 13 | 14 | const t = initTRPC.context<{ extraInfo: string }>().create() 15 | 16 | export const permix = createPermix({ 17 | forbiddenError: () => new TRPCError({ 18 | code: 'FORBIDDEN', 19 | message: 'You do not have permission to access this resource', 20 | }), 21 | }) 22 | 23 | export const router = t.router 24 | export const publicProcedure = t.procedure.use(({ next }) => { 25 | // Imagine this is a middleware that gets the user from the request 26 | const user = { 27 | role: 'admin' as const, 28 | } 29 | 30 | return next({ 31 | ctx: { 32 | permix: permix.setup(getRules(user.role)), 33 | }, 34 | }) 35 | }) 36 | 37 | export const appRouter = router({ 38 | userList: publicProcedure 39 | .use(permix.checkMiddleware('user', 'read')) 40 | // Imagine this is a database query 41 | .query(() => [ 42 | { 43 | id: '1', 44 | name: 'John Doe', 45 | email: 'john.doe@example.com', 46 | }, 47 | { 48 | id: '2', 49 | name: 'Jane Doe 2', 50 | email: 'jane.doe2@example.com', 51 | }, 52 | ]), 53 | userWrite: publicProcedure 54 | .use(permix.checkMiddleware('user', 'create')) 55 | .input(z.object({ 56 | name: z.string(), 57 | email: z.email(), 58 | })) 59 | .mutation(() => { 60 | // Imagine this is a database mutation 61 | return { id: '1', name: 'John Doe', email: 'john.doe@example.com' } 62 | }), 63 | }) 64 | 65 | export type AppRouter = typeof appRouter 66 | 67 | app.use( 68 | '/trpc', 69 | trpcExpress.createExpressMiddleware({ 70 | router: appRouter, 71 | createContext: () => ({ 72 | extraInfo: 'some extra info', 73 | }), 74 | }), 75 | ) 76 | app.listen(3000, () => { 77 | // eslint-disable-next-line no-console 78 | console.log('Server is running on port 3000') 79 | }) 80 | -------------------------------------------------------------------------------- /docs/content/docs/guide/ready.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Ready State 3 | description: Learn how to use the `isReady()` method to check if permissions are ready to use. 4 | --- 5 | 6 | ## Overview 7 | 8 | Sometimes you need to know when permissions are ready to use. For example, you might want to wait for permissions to be ready before rendering a component. That's where the `isReady()` and `isReadyAsync()` methods come in. 9 | 10 | 11 | Note that `isReady()` and `isReadyAsync()` will always return `false` on server-side. It only becomes `true` after the first successful call to `setup()` on the client. 12 | 13 | 14 | ## Usage 15 | 16 | ### Basic 17 | 18 | Permix provides an `isReady()` method to check if permissions have been properly initialized on the client side: 19 | 20 | ```ts twoslash 21 | import { createPermix } from 'permix' 22 | 23 | const permix = createPermix() 24 | 25 | console.log(permix.isReady()) // false 26 | 27 | // After setup completes 28 | permix.setup({ 29 | post: { 30 | create: true, 31 | read: true 32 | } 33 | }) 34 | 35 | console.log(permix.isReady()) // true 36 | ``` 37 | 38 | ### Async 39 | 40 | If you need to wait for permissions to be ready in an async context, you can use the `isReadyAsync()` method. This returns a promise that resolves when permissions are ready: 41 | 42 | ```ts 43 | import { createPermix } from 'permix' 44 | 45 | const permix = createPermix() 46 | 47 | async function init() { 48 | await permix.isReadyAsync() 49 | // Permissions are now ready to use 50 | const canCreate = permix.check('post', 'create') 51 | } 52 | ``` 53 | 54 | ### SSR 55 | 56 | This is particularly useful in SSR applications when using function-based permissions, since the dehydration process converts all function permissions to `false` until they are properly rehydrated on the client. 57 | 58 | 59 | Read more about [hydration](/guide/hydration) to learn how to transfer permissions from the server to the client. 60 | 61 | 62 | ```ts 63 | import { dehydrate, hydrate, createPermix } from 'permix' 64 | import { permix } from './permix' 65 | 66 | permix.setup({ 67 | post: { 68 | create: true, 69 | read: post => post.isPublic 70 | } 71 | }) 72 | 73 | // Dehydrate permissions on the server 74 | const state = dehydrate(permix) 75 | // { post: { create: true, read: false } } 76 | 77 | // Rehydrate permissions on the client 78 | hydrate(permix, state) 79 | 80 | const canRead = permix.check('post', 'read', { isPublic: true }) // false 81 | 82 | permix.hook('ready', (state) => { 83 | const canRead = permix.check('post', 'read', { isPublic: true }) 84 | console.log(canRead) // true 85 | }) 86 | ``` 87 | -------------------------------------------------------------------------------- /permix/src/orpc/create-permix.ts: -------------------------------------------------------------------------------- 1 | import type { Permix, PermixDefinition, PermixRules } from '../core/create-permix' 2 | import type { CheckContext, CheckFunctionParams } from '../core/params' 3 | import { ORPCError, os } from '@orpc/server' 4 | import { createPermix as createPermixCore } from '../core/create-permix' 5 | import { createCheckContext } from '../core/params' 6 | import { createTemplate } from '../core/template' 7 | import { pick } from '../utils' 8 | 9 | export interface PermixOptions { 10 | /** 11 | * Custom error to throw when permission is denied 12 | */ 13 | forbiddenError?: (params: CheckContext & { context: C }) => ORPCError 14 | } 15 | 16 | /** 17 | * Create a middleware function that checks permissions for ORPC routes. 18 | * 19 | * @link https://permix.letstri.dev/docs/integrations/orpc 20 | */ 21 | export function createPermix( 22 | { 23 | forbiddenError = () => new ORPCError('FORBIDDEN', { 24 | message: 'You do not have permission to perform this action', 25 | }), 26 | }: PermixOptions = {}, 27 | ) { 28 | const plugin = os.$context<{ permix: Pick, 'check' | 'dehydrate'> }>() 29 | 30 | function setup(rules: PermixRules) { 31 | return pick(createPermixCore(rules), ['check', 'dehydrate']) 32 | } 33 | 34 | function checkMiddleware(...params: CheckFunctionParams) { 35 | return plugin.middleware(async ({ context, next }) => { 36 | if (!context.permix) { 37 | throw new Error('[Permix] Instance not found. Please use the `setupMiddleware` function.') 38 | } 39 | 40 | const hasPermission = context.permix.check(...params) 41 | 42 | if (!hasPermission) { 43 | const error = typeof forbiddenError === 'function' 44 | ? forbiddenError({ 45 | ...createCheckContext(...params), 46 | context, 47 | }) 48 | : forbiddenError 49 | 50 | if (!(error instanceof ORPCError)) { 51 | console.error('[Permix]: forbiddenError is not ORPCError') 52 | 53 | throw new ORPCError('FORBIDDEN', { 54 | message: 'You do not have permission to perform this action', 55 | }) 56 | } 57 | 58 | throw error 59 | } 60 | 61 | return next() 62 | }) 63 | } 64 | 65 | function template(...params: Parameters>) { 66 | return createTemplate(...params) 67 | } 68 | 69 | return { 70 | setup, 71 | checkMiddleware, 72 | template, 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /permix/src/trpc/create-permix.ts: -------------------------------------------------------------------------------- 1 | import type { Permix, PermixDefinition, PermixRules } from '../core/create-permix' 2 | import type { CheckContext, CheckFunctionParams } from '../core/params' 3 | import { initTRPC, TRPCError } from '@trpc/server' 4 | import { createPermix as createPermixCore } from '../core/create-permix' 5 | import { createCheckContext } from '../core/params' 6 | import { createTemplate } from '../core/template' 7 | import { pick } from '../utils' 8 | 9 | export interface PermixOptions { 10 | /** 11 | * Custom error to throw when permission is denied 12 | */ 13 | forbiddenError?: (params: CheckContext & { ctx: C }) => TRPCError 14 | } 15 | 16 | /** 17 | * Create a middleware function that checks permissions for TRPC routes. 18 | * 19 | * @link https://permix.letstri.dev/docs/integrations/trpc 20 | */ 21 | export function createPermix( 22 | { 23 | forbiddenError = () => new TRPCError({ 24 | code: 'FORBIDDEN', 25 | message: 'You do not have permission to perform this action', 26 | }), 27 | }: PermixOptions = {}, 28 | ) { 29 | const plugin = initTRPC.context<{ permix: Pick, 'check' | 'dehydrate'> }>().create() 30 | 31 | function setup(rules: PermixRules) { 32 | return pick(createPermixCore(rules), ['check', 'dehydrate']) 33 | } 34 | 35 | function checkMiddleware(...params: CheckFunctionParams) { 36 | return plugin.middleware(async ({ ctx, next }) => { 37 | if (!ctx.permix) { 38 | throw new TRPCError({ 39 | code: 'INTERNAL_SERVER_ERROR', 40 | message: '[Permix] Instance not found. Please use the `setup` function.', 41 | }) 42 | } 43 | 44 | const hasPermission = ctx.permix.check(...params) 45 | 46 | if (!hasPermission) { 47 | const error = typeof forbiddenError === 'function' 48 | ? forbiddenError({ 49 | ...createCheckContext(...params), 50 | ctx, 51 | }) 52 | : forbiddenError 53 | 54 | if (!(error instanceof TRPCError)) { 55 | console.error('[Permix]: forbiddenError is not TRPCError') 56 | 57 | throw new TRPCError({ 58 | code: 'FORBIDDEN', 59 | message: 'You do not have permission to perform this action', 60 | }) 61 | } 62 | 63 | throw error 64 | } 65 | 66 | return next() 67 | }) 68 | } 69 | 70 | function template(...params: Parameters>) { 71 | return createTemplate(...params) 72 | } 73 | 74 | return { 75 | setup, 76 | checkMiddleware, 77 | template, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /permix/src/solid/components.tsx: -------------------------------------------------------------------------------- 1 | import type { JSX } from 'solid-js' 2 | import type { Permix, PermixDefinition } from '../core' 3 | import type { PermixStateJSON } from '../core/create-permix' 4 | import type { CheckFunctionObject } from '../core/params' 5 | import type { PermixContext } from './hooks' 6 | import { createEffect, createMemo, onCleanup } from 'solid-js' 7 | import { createStore } from 'solid-js/store' 8 | import { getRules, validatePermix } from '../core/create-permix' 9 | import { Context, usePermix, usePermixContext } from './hooks' 10 | 11 | /** 12 | * Provider that provides the Permix context to your Solid components. 13 | * 14 | * @link https://permix.letstri.dev/docs/integrations/solid 15 | */ 16 | export function PermixProvider(props: { 17 | children: JSX.Element 18 | permix: Permix 19 | }): JSX.Element { 20 | validatePermix(props.permix) 21 | 22 | const [context, setContext] = createStore>({ 23 | permix: props.permix, 24 | isReady: props.permix.isReady(), 25 | rules: getRules(props.permix), 26 | }) 27 | 28 | createEffect(() => { 29 | const setup = props.permix.hook('setup', () => setContext('rules', getRules(props.permix))) 30 | const ready = props.permix.hook('ready', () => setContext('isReady', props.permix.isReady())) 31 | 32 | onCleanup(() => { 33 | setup() 34 | ready() 35 | }) 36 | }) 37 | 38 | return ( 39 | 40 | {props.children} 41 | 42 | ) 43 | } 44 | 45 | export function PermixHydrate(props: { children: JSX.Element, state: PermixStateJSON }) { 46 | const context = usePermixContext() 47 | 48 | validatePermix(context.permix) 49 | 50 | context.permix.hydrate(props.state) 51 | 52 | return props.children 53 | } 54 | 55 | export type CheckProps = CheckFunctionObject & { 56 | children: JSX.Element 57 | otherwise?: JSX.Element 58 | reverse?: boolean 59 | } 60 | 61 | export interface PermixComponents { 62 | Check: (props: CheckProps) => JSX.Element 63 | } 64 | 65 | export function createComponents(permix: Permix): PermixComponents { 66 | function Check(props: CheckProps): JSX.Element { 67 | const context = usePermix(permix) 68 | const hasPermission = createMemo(() => context.check(props.entity, props.action, props.data)) 69 | 70 | return ( 71 | <> 72 | {props.reverse 73 | ? hasPermission() ? (props.otherwise || null) : props.children 74 | : hasPermission() ? props.children : (props.otherwise || null)} 75 | 76 | ) 77 | } 78 | 79 | return { 80 | Check, 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /permix/src/express/create-permix.ts: -------------------------------------------------------------------------------- 1 | import type { Handler, Request, Response } from 'express' 2 | import type { Permix, PermixDefinition, PermixRules } from '../core/create-permix' 3 | import type { CheckContext, CheckFunctionParams } from '../core/params' 4 | import type { MaybePromise } from '../core/utils' 5 | import { createPermix as createPermixCore } from '../core/create-permix' 6 | import { createCheckContext } from '../core/params' 7 | import { createTemplate } from '../core/template' 8 | import { pick } from '../utils' 9 | 10 | const permixSymbol = Symbol('permix') 11 | 12 | export interface MiddlewareContext { 13 | req: Request 14 | res: Response 15 | } 16 | 17 | export interface PermixOptions { 18 | /** 19 | * Custom error handler 20 | */ 21 | onForbidden?: (params: CheckContext & MiddlewareContext) => MaybePromise 22 | } 23 | 24 | /** 25 | * Create a middleware function that checks permissions for Express routes. 26 | * 27 | * @link https://permix.letstri.dev/docs/integrations/express 28 | */ 29 | export function createPermix( 30 | { 31 | onForbidden = ({ res }) => { 32 | res.status(403).json({ error: 'Forbidden' }) 33 | }, 34 | }: PermixOptions = {}, 35 | ) { 36 | function getPermix(req: Request, res: Response) { 37 | try { 38 | const permix = (req as any)[permixSymbol] as Permix | undefined 39 | 40 | if (!permix) { 41 | throw new Error('Not found') 42 | } 43 | 44 | return pick(permix, ['check', 'dehydrate']) 45 | } 46 | catch { 47 | res.status(500).json({ error: '[Permix]: Instance not found. Please use the `setupMiddleware` function.' }) 48 | return null! 49 | } 50 | } 51 | 52 | function setupMiddleware(callback: (context: MiddlewareContext) => MaybePromise>): Handler { 53 | return async (req, res, next) => { 54 | (req as any)[permixSymbol] = createPermixCore(await callback({ req, res })) 55 | 56 | return next() 57 | } 58 | } 59 | 60 | function checkMiddleware(...params: CheckFunctionParams): Handler { 61 | return async (req, res, next) => { 62 | const permix = getPermix(req, res) 63 | 64 | if (!permix) 65 | return 66 | 67 | const hasPermission = permix.check(...params) 68 | 69 | if (!hasPermission) { 70 | await onForbidden({ 71 | req, 72 | res, 73 | ...createCheckContext(...params), 74 | }) 75 | return 76 | } 77 | 78 | return next() 79 | } 80 | } 81 | 82 | function template(...params: Parameters>) { 83 | return createTemplate(...params) 84 | } 85 | 86 | return { 87 | setupMiddleware, 88 | checkMiddleware, 89 | template, 90 | get: getPermix, 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /permix/src/core/template.test.ts: -------------------------------------------------------------------------------- 1 | import type { PermixDefinition } from './create-permix' 2 | import { describe, expect, it } from 'vitest' 3 | import { createPermix } from './create-permix' 4 | 5 | interface Post { 6 | id: string 7 | title: string 8 | authorId: string 9 | } 10 | 11 | interface Comment { 12 | id: string 13 | content: string 14 | postId: string 15 | } 16 | 17 | type Definition = PermixDefinition<{ 18 | post: { 19 | dataType: Post 20 | action: 'create' | 'read' 21 | } 22 | comment: { 23 | dataType: Comment 24 | action: 'create' | 'read' | 'update' 25 | } 26 | }> 27 | 28 | describe('createTemplate', () => { 29 | it('should define permissions with template', () => { 30 | const permix = createPermix() 31 | 32 | const permissions = permix.template({ 33 | post: { 34 | create: true, 35 | read: true, 36 | }, 37 | comment: { 38 | create: true, 39 | read: true, 40 | update: true, 41 | }, 42 | }) 43 | 44 | expect(permissions()).toEqual({ 45 | post: { 46 | create: true, 47 | read: true, 48 | }, 49 | comment: { 50 | create: true, 51 | read: true, 52 | update: true, 53 | }, 54 | }) 55 | 56 | permix.setup(permissions()) 57 | 58 | expect(permix.check('post', 'create')).toBe(true) 59 | }) 60 | 61 | it('should throw an error if permissions are not valid', () => { 62 | const permix = createPermix() 63 | 64 | expect(() => permix.template({ 65 | // @ts-expect-error create isn't valid 66 | post: { create: 1 }, 67 | })).toThrow() 68 | expect(() => permix.template({ 69 | // @ts-expect-error create isn't valid 70 | post: { create: 'string' }, 71 | })).toThrow() 72 | expect(() => permix.template({ 73 | // @ts-expect-error create isn't valid 74 | post: { create: [] }, 75 | })).toThrow() 76 | expect(() => permix.template({ 77 | // @ts-expect-error create isn't valid 78 | post: { create: {} }, 79 | })).toThrow() 80 | expect(() => permix.template({ 81 | // @ts-expect-error create isn't valid 82 | post: { create: null }, 83 | })).toThrow() 84 | }) 85 | 86 | it('should work with template function with param', () => { 87 | const permix = createPermix() 88 | 89 | const rules1 = permix.template(({ user }: { user: { role: string } }) => ({ 90 | post: { 91 | create: user.role !== 'admin', 92 | read: true, 93 | update: true, 94 | }, 95 | comment: { 96 | create: user.role !== 'admin', 97 | read: true, 98 | update: true, 99 | }, 100 | })) 101 | 102 | permix.setup(rules1({ user: { role: 'admin' } })) 103 | 104 | expect(permix.check('post', 'create')).toBe(false) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /permix/src/react/components.tsx: -------------------------------------------------------------------------------- 1 | import type { Permix, PermixDefinition } from '../core' 2 | import type { PermixStateJSON } from '../core/create-permix' 3 | import type { CheckFunctionObject } from '../core/params' 4 | import type { PermixContext } from './hooks' 5 | import * as React from 'react' 6 | import { getRules, validatePermix } from '../core/create-permix' 7 | import { Context, usePermix, usePermixContext } from './hooks' 8 | 9 | /** 10 | * Provider that provides the Permix context to your React components. 11 | * 12 | * @link https://permix.letstri.dev/docs/integrations/react 13 | */ 14 | export function PermixProvider({ 15 | children, 16 | permix, 17 | }: { children: React.ReactNode, permix: Permix }) { 18 | validatePermix(permix) 19 | 20 | const [context, setContext] = React.useState>(() => ({ 21 | permix, 22 | isReady: permix.isReady(), 23 | rules: getRules(permix), 24 | })) 25 | 26 | React.useEffect(() => { 27 | const setup = permix.hook('setup', () => setContext(c => ({ ...c, rules: getRules(permix) }))) 28 | const ready = permix.hook('ready', () => setContext(c => ({ ...c, isReady: permix.isReady() }))) 29 | 30 | return () => { 31 | setup() 32 | ready() 33 | } 34 | }, [permix]) 35 | 36 | return ( 37 | // eslint-disable-next-line react/no-context-provider 38 | 39 | {children} 40 | 41 | ) 42 | } 43 | 44 | export function PermixHydrate({ children, state }: { children: React.ReactNode, state: PermixStateJSON }) { 45 | const { permix } = usePermixContext() 46 | 47 | validatePermix(permix) 48 | 49 | React.useMemo(() => permix.hydrate(state), [permix, state]) 50 | 51 | return children 52 | } 53 | 54 | export type CheckProps = CheckFunctionObject & { 55 | children: React.ReactNode 56 | otherwise?: React.ReactNode 57 | reverse?: boolean 58 | } 59 | 60 | export interface PermixComponents { 61 | Check: (props: CheckProps) => React.ReactNode 62 | } 63 | 64 | // eslint-disable-next-line react-refresh/only-export-components 65 | export function createComponents(permix: Permix): PermixComponents { 66 | function Check({ 67 | children, 68 | entity, 69 | action, 70 | data, 71 | otherwise = null, 72 | reverse = false, 73 | }: CheckProps) { 74 | const { check } = usePermix(permix) 75 | 76 | const hasPermission = check(entity, action, data) 77 | return reverse 78 | ? hasPermission ? otherwise : children 79 | : hasPermission ? children : otherwise 80 | } 81 | 82 | Check.displayName = 'Check' 83 | 84 | return { 85 | Check, 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /permix/src/hono/create-permix.ts: -------------------------------------------------------------------------------- 1 | import type { Context, MiddlewareHandler } from 'hono' 2 | import type { Permix, PermixDefinition, PermixRules } from '../core/create-permix' 3 | import type { CheckContext, CheckFunctionParams } from '../core/params' 4 | import type { MaybePromise } from '../core/utils' 5 | import { createMiddleware } from 'hono/factory' 6 | import { HTTPException } from 'hono/http-exception' 7 | import { createPermix as createPermixCore } from '../core/create-permix' 8 | import { createCheckContext } from '../core/params' 9 | import { createTemplate } from '../core/template' 10 | import { pick } from '../utils' 11 | 12 | const permixSymbol = Symbol('permix') as unknown as string 13 | 14 | export interface MiddlewareContext { 15 | c: Context 16 | } 17 | 18 | export interface PermixOptions { 19 | /** 20 | * Custom error handler 21 | */ 22 | onForbidden?: (params: CheckContext & { c: Context }) => MaybePromise 23 | } 24 | 25 | /** 26 | * Create a middleware function that checks permissions for Hono routes. 27 | * 28 | * @link https://permix.letstri.dev/docs/integrations/hono 29 | */ 30 | export function createPermix( 31 | { 32 | onForbidden = ({ c }) => c.json({ error: 'Forbidden' }, 403), 33 | }: PermixOptions = {}, 34 | ) { 35 | function getPermix(c: Context) { 36 | try { 37 | const permix = c.get(permixSymbol) as Permix | undefined 38 | 39 | if (!permix) { 40 | throw new Error('Not found') 41 | } 42 | 43 | return pick(permix, ['check', 'dehydrate']) 44 | } 45 | catch { 46 | throw new HTTPException(500, { 47 | message: '[Permix] Instance not found. Please use the `setupMiddleware` function.', 48 | }) 49 | } 50 | } 51 | 52 | function setupMiddleware(callback: (context: { c: Context }) => PermixRules | Promise>): MiddlewareHandler { 53 | return createMiddleware(async (c, next) => { 54 | c.set(permixSymbol, createPermixCore(await callback({ c }))) 55 | 56 | await next() 57 | }) 58 | } 59 | 60 | function checkMiddleware(...params: CheckFunctionParams): MiddlewareHandler { 61 | return createMiddleware(async (c, next) => { 62 | try { 63 | const permix = getPermix(c) 64 | 65 | const hasPermission = permix.check(...params) 66 | 67 | if (!hasPermission) { 68 | return await onForbidden({ c, ...createCheckContext(...params) }) 69 | } 70 | 71 | await next() 72 | } 73 | catch { 74 | return await onForbidden({ c, ...createCheckContext(...params) }) 75 | } 76 | }) 77 | } 78 | 79 | function template(...params: Parameters>) { 80 | return createTemplate(...params) 81 | } 82 | 83 | return { 84 | setupMiddleware, 85 | checkMiddleware, 86 | template, 87 | get: getPermix, 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /permix/src/node/create-permix.ts: -------------------------------------------------------------------------------- 1 | import type { IncomingMessage, ServerResponse } from 'node:http' 2 | import type { Permix, PermixDefinition, PermixRules } from '../core/create-permix' 3 | import type { CheckContext, CheckFunctionParams } from '../core/params' 4 | import { createPermix as createPermixCore } from '../core/create-permix' 5 | import { createCheckContext } from '../core/params' 6 | import { createTemplate } from '../core/template' 7 | import { pick } from '../utils' 8 | 9 | const permixSymbol = Symbol('permix') 10 | 11 | export interface MiddlewareContext { 12 | req: IncomingMessage 13 | res: ServerResponse 14 | } 15 | 16 | export interface PermixOptions { 17 | /** 18 | * Custom error handler 19 | */ 20 | onForbidden?: (params: CheckContext & MiddlewareContext) => void 21 | } 22 | 23 | /** 24 | * Create a middleware function that checks permissions for Node.js HTTP servers. 25 | * Compatible with raw Node.js HTTP servers. 26 | * 27 | * @link https://permix.letstri.dev/docs/integrations/node 28 | */ 29 | export function createPermix( 30 | { 31 | onForbidden = ({ res }) => { 32 | res.statusCode = 403 33 | res.setHeader('Content-Type', 'application/json') 34 | res.end(JSON.stringify({ error: 'Forbidden' })) 35 | }, 36 | }: PermixOptions = {}, 37 | ) { 38 | function getPermix(req: IncomingMessage, res: ServerResponse) { 39 | try { 40 | const permix = (req as any)[permixSymbol] as Permix | undefined 41 | 42 | if (!permix) { 43 | throw new Error('Not found') 44 | } 45 | 46 | return pick(permix, ['check', 'dehydrate']) 47 | } 48 | catch { 49 | res.statusCode = 500 50 | res.setHeader('Content-Type', 'application/json') 51 | res.end(JSON.stringify({ error: '[Permix]: Instance not found. Please use the `setupMiddleware` function.' })) 52 | return null! 53 | } 54 | } 55 | 56 | function setupMiddleware(callback: (context: MiddlewareContext) => PermixRules | Promise>) { 57 | return async (context: MiddlewareContext) => { 58 | (context.req as any)[permixSymbol] = createPermixCore(await callback(context)) 59 | } 60 | } 61 | 62 | function checkMiddleware(...params: CheckFunctionParams) { 63 | return async (context: MiddlewareContext) => { 64 | const permix = getPermix(context.req, context.res) 65 | 66 | if (!permix) 67 | return 68 | 69 | const hasPermission = permix.check(...params) 70 | 71 | if (!hasPermission) { 72 | return onForbidden({ 73 | ...createCheckContext(...params), 74 | req: context.req, 75 | res: context.res, 76 | }) 77 | } 78 | } 79 | } 80 | 81 | function template(...params: Parameters>) { 82 | return createTemplate(...params) 83 | } 84 | 85 | return { 86 | setupMiddleware, 87 | checkMiddleware, 88 | template, 89 | get: getPermix, 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /permix/src/fastify/create-permix.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyPluginAsync, FastifyReply, FastifyRequest, RouteHandler } from 'fastify' 2 | import type { Permix, PermixDefinition, PermixRules } from '../core/create-permix' 3 | import type { CheckContext, CheckFunctionParams } from '../core/params' 4 | import type { MaybePromise } from '../core/utils' 5 | import fp from 'fastify-plugin' 6 | import { createPermix as createPermixCore } from '../core/create-permix' 7 | import { createCheckContext } from '../core/params' 8 | import { createTemplate } from '../core/template' 9 | import { pick } from '../utils' 10 | 11 | const permixSymbol = Symbol('permix') 12 | 13 | export interface MiddlewareContext { 14 | request: FastifyRequest 15 | reply: FastifyReply 16 | } 17 | 18 | export interface PermixOptions { 19 | /** 20 | * Custom error handler 21 | */ 22 | onForbidden?: (params: CheckContext & MiddlewareContext) => MaybePromise 23 | } 24 | 25 | /** 26 | * Create a middleware function that checks permissions for Fastify routes. 27 | * 28 | * @link https://permix.letstri.dev/docs/integrations/fastify 29 | */ 30 | export function createPermix( 31 | { 32 | onForbidden = ({ reply }) => { 33 | reply.status(403).send({ error: 'Forbidden' }) 34 | }, 35 | }: PermixOptions = {}, 36 | ) { 37 | function getPermix(request: FastifyRequest, reply: FastifyReply) { 38 | try { 39 | const permix = request.getDecorator(permixSymbol) as Permix | undefined 40 | 41 | if (!permix) { 42 | throw new Error('Not found') 43 | } 44 | 45 | return pick(permix, ['check', 'dehydrate']) 46 | } 47 | catch { 48 | reply.status(500).send({ error: '[Permix]: Instance not found. Please register the `plugin` function.' }) 49 | return null! 50 | } 51 | } 52 | 53 | function plugin(callback: (context: MiddlewareContext) => MaybePromise>): FastifyPluginAsync { 54 | return fp(async (fastify) => { 55 | fastify.decorateRequest(permixSymbol, null) 56 | 57 | fastify.addHook('onRequest', async (request, reply) => { 58 | const permix = createPermixCore(await callback({ request, reply })) 59 | request.setDecorator(permixSymbol, permix) 60 | }) 61 | }, { 62 | fastify: '5.x', 63 | name: 'permix', 64 | }) 65 | } 66 | 67 | function checkHandler(...params: CheckFunctionParams): RouteHandler { 68 | return async (request, reply) => { 69 | const permix = getPermix(request, reply) 70 | 71 | const hasPermission = permix.check(...params) 72 | 73 | if (!hasPermission) { 74 | await onForbidden({ 75 | request, 76 | reply, 77 | ...createCheckContext(...params), 78 | }) 79 | } 80 | } 81 | } 82 | 83 | function template(...params: Parameters>) { 84 | return createTemplate(...params) 85 | } 86 | 87 | return { 88 | plugin, 89 | checkHandler, 90 | template, 91 | get: getPermix, 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /docs/content/docs/guide/check.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Check 3 | description: Learn how to check permissions in your application 4 | --- 5 | 6 | ## Overview 7 | 8 | Permix provides two methods for checking permissions: `check` and `checkAsync`. Both methods return a boolean indicating whether the action is allowed. 9 | 10 | ## `check` 11 | 12 | The `check` method allows you to verify if certain actions are permitted. It returns a boolean indicating whether the action is allowed: 13 | 14 | ```ts 15 | permix.check('post', 'create') // returns true/false 16 | ``` 17 | 18 | ## Array 19 | 20 | You can check multiple actions at once by passing an array of actions. All actions must be permitted for the check to return true: 21 | 22 | ```ts 23 | // Check if both create and read are allowed 24 | permix.check('post', ['create', 'read']) // returns true if both are allowed 25 | ``` 26 | 27 | ## All 28 | 29 | Use the special 'all' keyword to verify if all possible actions for an entity are permitted: 30 | 31 | ```ts 32 | // Check if all actions are allowed for posts 33 | permix.check('post', 'all') // returns true only if all actions are permitted 34 | ``` 35 | 36 | ## Any 37 | 38 | Use the special 'any' keyword to verify if any of the actions for an entity are permitted: 39 | 40 | ```ts 41 | // Check if any action is allowed for posts 42 | permix.check('post', 'any') // returns true if any action is permitted 43 | ``` 44 | 45 | ## `checkAsync` 46 | 47 | When you need to ensure permissions are ready before checking, use `checkAsync`. This is useful when permissions might be set up asynchronously: 48 | 49 | ```ts 50 | setTimeout(() => { 51 | permix.setup({ 52 | post: { create: true } 53 | }) 54 | }, 1000) 55 | 56 | await permix.checkAsync('post', 'create') // waits for setup 57 | ``` 58 | 59 | 60 | In most cases you should use `check` instead of `checkAsync`. `checkAsync` is useful when you need to ensure permissions are ready before checking, for example in route middleware. 61 | 62 | 63 | ## Data-Based 64 | 65 | You can define permissions that depend on the data being accessed: 66 | 67 | ```ts 68 | permix.setup({ 69 | post: { 70 | // Only allow updates if user is the author 71 | update: post => post.authorId === currentUserId, 72 | // Static permission 73 | read: true 74 | } 75 | }) 76 | 77 | // Check with data 78 | const post = { id: '1', authorId: 'user1' } 79 | 80 | permix.check('post', 'update', post) // true if currentUserId === 'user1' 81 | ``` 82 | 83 | 84 | You still can check permissions without providing the data, but it will return `false` in this case. 85 | 86 | 87 | ## Type Safety 88 | 89 | Permix provides full type safety for your permissions: 90 | 91 | ```ts twoslash 92 | import { createPermix } from 'permix' 93 | 94 | const permix = createPermix<{ 95 | post: { 96 | action: 'create' | 'update' 97 | } 98 | }>() 99 | 100 | // @errors: 2345 101 | // This will cause a TypeScript error but will return false 102 | permix.check('post', 'invalid-action') 103 | 104 | permix.check('invalid-entity', 'create') 105 | ``` 106 | -------------------------------------------------------------------------------- /permix/src/core/hooks.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from 'vitest' 2 | import { createHooks } from './hooks' 3 | 4 | describe('createHooks', () => { 5 | it('should register and call hooks', () => { 6 | const { hook, callHook } = createHooks() 7 | const mockFn = vi.fn() 8 | 9 | hook('test', mockFn) 10 | callHook('test', 'arg1', 'arg2') 11 | 12 | expect(mockFn).toHaveBeenCalledWith('arg1', 'arg2') 13 | }) 14 | 15 | it('should allow removing hooks', () => { 16 | const { hook, callHook } = createHooks() 17 | const mockFn = vi.fn() 18 | 19 | const remove = hook('test', mockFn) 20 | remove() 21 | callHook('test', 'arg') 22 | 23 | expect(mockFn).not.toHaveBeenCalled() 24 | }) 25 | 26 | it('should handle hookOnce correctly', () => { 27 | const { hookOnce, callHook } = createHooks() 28 | const mockFn = vi.fn() 29 | 30 | hookOnce('test', mockFn) 31 | callHook('test', 'arg1') 32 | callHook('test', 'arg2') 33 | 34 | expect(mockFn).toHaveBeenCalledTimes(1) 35 | expect(mockFn).toHaveBeenCalledWith('arg1') 36 | }) 37 | 38 | it('should remove specific hooks with removeHook', () => { 39 | const { hook, removeHook, callHook } = createHooks() 40 | const mockFn1 = vi.fn() 41 | const mockFn2 = vi.fn() 42 | 43 | hook('test', mockFn1) 44 | hook('test', mockFn2) 45 | removeHook('test', mockFn1) 46 | callHook('test', 'arg') 47 | 48 | expect(mockFn1).not.toHaveBeenCalled() 49 | expect(mockFn2).toHaveBeenCalledWith('arg') 50 | }) 51 | 52 | it('should clear specific hooks with clearHook', () => { 53 | const { hook, clearHook, callHook } = createHooks() 54 | const mockFn = vi.fn() 55 | 56 | hook('test', mockFn) 57 | clearHook('test') 58 | callHook('test', 'arg') 59 | 60 | expect(mockFn).not.toHaveBeenCalled() 61 | }) 62 | 63 | it('should clear all hooks with clearAllHooks', () => { 64 | const { hook, clearAllHooks, callHook } = createHooks() 65 | const mockFn1 = vi.fn() 66 | const mockFn2 = vi.fn() 67 | 68 | hook('test1', mockFn1) 69 | hook('test2', mockFn2) 70 | clearAllHooks() 71 | callHook('test1', 'arg') 72 | callHook('test2', 'arg') 73 | 74 | expect(mockFn1).not.toHaveBeenCalled() 75 | expect(mockFn2).not.toHaveBeenCalled() 76 | }) 77 | 78 | it('should handle multiple hooks for the same event', () => { 79 | const { hook, callHook } = createHooks() 80 | const mockFn1 = vi.fn() 81 | const mockFn2 = vi.fn() 82 | 83 | hook('test', mockFn1) 84 | hook('test', mockFn2) 85 | callHook('test', 'arg') 86 | 87 | expect(mockFn1).toHaveBeenCalledWith('arg') 88 | expect(mockFn2).toHaveBeenCalledWith('arg') 89 | }) 90 | 91 | it('should safely handle calling non-existent hooks', () => { 92 | const { callHook } = createHooks() 93 | expect(() => callHook('nonexistent', 'arg')).not.toThrow() 94 | }) 95 | 96 | it('should call hooks with generic', () => { 97 | const { hook, callHook } = createHooks<{ 98 | test: (arg: string) => void 99 | }>() 100 | 101 | const mockFn = vi.fn() 102 | 103 | hook('test', mockFn) 104 | callHook('test', 'arg') 105 | 106 | expect(mockFn).toHaveBeenCalledWith('arg') 107 | }) 108 | }) 109 | -------------------------------------------------------------------------------- /docs/content/docs/guide/hydration.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Hydration (SSR) 3 | description: Learn how to hydrate and dehydrate permissions in your application 4 | --- 5 | 6 | ## Overview 7 | 8 | Hydration is the process of converting server-side state into client-side state. In Permix, hydration allows you to serialize permissions on the server and restore them on the client. 9 | 10 | 11 | Note that function-based permissions will be converted to `false` during dehydration since functions cannot be serialized to JSON. You should call `setup` method on the client side after hydration to fully restore function-based permissions. 12 | 13 | 14 | ## Usage 15 | 16 | Permix provides two instance methods for handling hydration: 17 | 18 | - `dehydrate()` - Converts the current permissions state into a JSON-serializable format 19 | - `hydrate(state)` - Restores permissions from a previously dehydrated state 20 | 21 | ```ts twoslash 22 | import { createPermix } from 'permix' 23 | 24 | const permix = createPermix<{ 25 | post: { 26 | dataType: { isPublic: boolean } 27 | action: 'create' | 'read' 28 | } 29 | }>() 30 | 31 | // Set up initial permissions 32 | permix.setup({ 33 | post: { 34 | create: true, 35 | read: post => !!post?.isPublic 36 | } 37 | }) 38 | 39 | // Dehydrate permissions to JSON 40 | const state = permix.dehydrate() 41 | // Result: { post: { create: true, read: false } } 42 | 43 | // Later, hydrate permissions from the state 44 | permix.hydrate(state) 45 | ``` 46 | 47 | ## Server-Side Rendering 48 | 49 | Hydration is particularly useful in server-side rendering scenarios where you want to transfer permissions from the server to the client: 50 | 51 | ```ts twoslash 52 | // Express server 53 | import express from 'express' 54 | import { createPermix } from 'permix' 55 | 56 | const app = express() 57 | const permix = createPermix<{ 58 | post: { 59 | action: 'create' | 'read' 60 | } 61 | }>() 62 | 63 | app.get('/', (req, res) => { 64 | // Setup permissions on the server 65 | permix.setup({ 66 | post: { 67 | create: true, 68 | read: true 69 | } 70 | }) 71 | 72 | // Dehydrate permissions for client 73 | const dehydratedState = permix.dehydrate() 74 | 75 | // Send HTML with embedded permissions data 76 | res.send(` 77 | 78 | 79 | 80 | 83 | 84 | 85 |
86 | 104 | 105 | 106 | `) 107 | }) 108 | ``` 109 | -------------------------------------------------------------------------------- /docs/content/docs/quick-start.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quick Start 3 | description: A quick start guide to start using Permix and validating your permissions 4 | icon: RiPlayLargeLine 5 | --- 6 | 7 | ## Try Permix 8 | 9 | Want to explore Permix before installing? Try our interactive sandbox environment where you can experiment with type-safe permissions management right in your browser. 10 | 11 | [Try Permix Sandbox](https://stackblitz.com/edit/permix-sandbox?file=src%2Fmain.ts&terminal=dev) 12 | 13 | ## Installation 14 | 15 | 16 | 17 | 18 | 19 | ### Install Permix 20 | 21 | Typically you'll need to install Permix using your package manager: 22 | 23 | ```package-install 24 | permix 25 | ``` 26 | 27 | 28 | 29 | 30 | 31 | ### Create an instance 32 | 33 | To create a base instance, you need to provide a schema as a generic type to `createPermix` function that defines your permissions: 34 | 35 | ```ts title="/lib/permix.ts" 36 | import { createPermix } from 'permix' 37 | 38 | export const permix = createPermix<{ 39 | post: { 40 | action: 'create' | 'read' | 'update' | 'delete' 41 | } 42 | }>() 43 | 44 | // ... 45 | ``` 46 | 47 | Learn more about features and configuration of instances in the [instance guide](/docs/guide/instance). 48 | 49 | 50 | 51 | 52 | 53 | ### Setup your permissions 54 | 55 | You can setup your permissions by calling `setup` method on your instance in any place you want: 56 | 57 | ```ts title="/lib/permix.ts" 58 | // ... 59 | 60 | // Call setupPermissions in your application 61 | export function setupPermissions() { 62 | permix.setup({ 63 | post: { 64 | create: true, 65 | read: true, 66 | update: true, 67 | delete: false, 68 | }, 69 | }) 70 | } 71 | ``` 72 | 73 | 74 | 75 | 76 | 77 | ### Check permissions 78 | 79 | After setup, you can use `check` method to check available permissions: 80 | 81 | ```ts 82 | permix.check('post', 'create') // true 83 | ``` 84 | 85 | 86 | 87 | 88 | 89 | ### Finish 90 | 91 | That's it! 🎉 92 | 93 | You've now got a basic setup of Permix. The next step is to learn more about 3 core concepts of Permix: 94 | 95 | - [Instance](/docs/guide/instance) 96 | - [Setup](/docs/guide/setup) 97 | - [Check](/docs/guide/check) 98 | - Of course, do not hesitate to read other guide pages. 99 | 100 | 101 | 102 | 103 | 104 | ## Integrations 105 | 106 | Continuing from the quick start, you can now explore how Permix integrates with other libraries and frameworks. 107 | 108 | 109 | 110 | 111 | Integration with React via provider and hook. 112 | 113 | 114 | 115 | Integration with Vue via plugin and composable. 116 | 117 | 118 | 119 | Integration with Node.js via middleware. 120 | 121 | 122 | 123 | Integration with Hono via middleware. 124 | 125 | 126 | 127 | Integration with Express via middleware. 128 | 129 | 130 | 131 | Integration with tRPC via middleware. 132 | 133 | 134 | 135 | -------------------------------------------------------------------------------- /docs/app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import { CodeBlock, Pre } from 'fumadocs-ui/components/codeblock' 2 | import { Tab, Tabs } from 'fumadocs-ui/components/tabs' 3 | import InitCode from './code/init.mdx' 4 | import SetupCode from './code/setup.mdx' 5 | import UsageCode from './code/usage.mdx' 6 | 7 | export default function Page() { 8 | return ( 9 |
10 |
11 |
12 |

13 | Permix 14 |

15 |

16 | A lightweight, framework-agnostic, type-safe permissions management library for client-side and server-side JavaScript applications. 17 |

18 |
19 |
20 | 21 | 22 | 23 |
24 |                   
25 |                 
26 |
27 |
28 | 29 | 30 |
31 |                   
32 |                 
33 |
34 |
35 | 36 | 37 |
38 |                   
39 |                 
40 |
41 |
42 |
43 |
44 |
45 |
46 | {[ 47 | { 48 | emoji: '🔒', 49 | title: 'Type-safe', 50 | description: 51 | 'Permix is built with TypeScript in mind, providing full type safety and autocompletion for your permissions system.', 52 | }, 53 | { 54 | emoji: '🌐', 55 | title: 'Framework Agnostic', 56 | description: 57 | 'Use Permix with any JavaScript framework or runtime - it works everywhere from React to Node.js.', 58 | }, 59 | { 60 | emoji: '🛠️', 61 | title: 'Simple DX', 62 | description: 63 | 'Permix provides an intuitive API that makes managing permissions straightforward and easy to understand.', 64 | }, 65 | ].map((item) => { 66 | return ( 67 |
71 |
72 | {item.emoji} 73 |
74 |

{item.title}

75 |

80 |

81 | ) 82 | })} 83 |
84 |
85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /docs/content/docs/integrations/react.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: React 3 | description: Learn how to use Permix with React applications 4 | --- 5 | 6 | ## Overview 7 | 8 | Permix provides official React integration through the `PermixProvider` component and `usePermix` hook. This allows you to manage permissions reactively in your React app. 9 | 10 | 11 | Before getting started with React integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. 12 | 13 | 14 | 15 | 16 | 17 | 18 | ## Setup 19 | 20 | First, wrap your application with the `PermixProvider`: 21 | 22 | ```tsx title="App.tsx" 23 | import { PermixProvider } from 'permix/react' 24 | import { permix } from './lib/permix' 25 | 26 | function App() { 27 | return ( 28 | 29 | 30 | 31 | ) 32 | } 33 | ``` 34 | 35 | 36 | Remember to always pass the same Permix instance to both the `PermixProvider` and `usePermix` hook to maintain type safety. 37 | 38 | 39 | 40 | 41 | 42 | 43 | ## Hook 44 | 45 | For checking permissions in your components, you can use the `usePermix` hook. And to avoid importing the hook and Permix instance in every component, you can create a custom hook: 46 | 47 | ```tsx title="hooks/use-permissions.ts" 48 | import { usePermix } from 'permix/react' 49 | import { permix } from '../lib/permix' 50 | 51 | export function usePermissions() { 52 | return usePermix(permix) 53 | } 54 | ``` 55 | 56 | 57 | 58 | 59 | 60 | ## Components 61 | 62 | If you prefer using components, you can import the `createComponents` function from `permix/react` and create checking components: 63 | 64 | ```ts title="lib/permix.ts" 65 | import { createComponents } from 'permix/react' 66 | 67 | // ... 68 | 69 | export const { Check } = createComponents(permix) 70 | ``` 71 | 72 | And then you can use the `Check` component in your components: 73 | 74 | ```tsx title="page.tsx" 75 | export default function Page() { 76 | return ( 77 | Will show this if a user doesn't have permission

} // Will show this if a user doesn't have permission 82 | reverse // Will flip the logic of the permission check 83 | > 84 | Will show this if a user has permission 85 |
86 | ) 87 | } 88 | ``` 89 | 90 |
91 | 92 | 93 | 94 | ## Usage 95 | 96 | Use the `usePermix` hook and checking components in your components: 97 | 98 | ```tsx title="page.tsx" 99 | import { usePermix } from 'permix/react' 100 | import { permix } from './lib/permix' 101 | import { Check } from './lib/permix-components' 102 | 103 | export default function Page() { 104 | const post = usePost() 105 | const { check, isReady } = usePermix(permix) 106 | 107 | if (!isReady) { 108 | return
Loading permissions...
109 | } 110 | 111 | const canEdit = check('post', 'edit', post) 112 | 113 | return ( 114 |
115 | {canEdit ? ( 116 | 117 | ) : ( 118 |

You don't have permission to edit this post

119 | )} 120 | 121 | Can I create a post inside the Check component? 122 | 123 |
124 | ) 125 | } 126 | ``` 127 | 128 |
129 | 130 | 131 | 132 | ## Example 133 | 134 | You can find the example of the React integration [here](https://github.com/letstri/permix/tree/main/examples/react). 135 | 136 | 137 | 138 |
139 | -------------------------------------------------------------------------------- /docs/content/docs/integrations/vue.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Vue 3 | description: Learn how to use Permix with Vue applications 4 | --- 5 | 6 | ## Overview 7 | 8 | Permix provides official Vue integration through the `permixPlugin` and `usePermix` composable. This allows you to manage permissions reactively in your Vue app. 9 | 10 | 11 | Before getting started with Vue integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. 12 | 13 | 14 | 15 | 16 | 17 | 18 | ## Setup 19 | 20 | First, install the Vue plugin in your application: 21 | 22 | ```ts title="main.ts" 23 | import { createApp } from 'vue' 24 | import { permixPlugin } from 'permix/vue' 25 | import { permix } from './lib/permix' 26 | import App from './App.vue' 27 | 28 | const app = createApp(App) 29 | 30 | app.use(permixPlugin, { permix }) 31 | app.mount('#app') 32 | ``` 33 | 34 | 35 | Remember to always use the same Permix instance here and in the `usePermix` composable to maintain type safety. 36 | 37 | 38 | 39 | 40 | 41 | 42 | ## Composable 43 | 44 | For checking permissions in your components, you can use the `usePermix` composable. And to avoid importing the composable and Permix instance in every component, you can create a custom composable: 45 | 46 | ```ts title="composables/use-permissions.ts" 47 | import { usePermix } from 'permix/vue' 48 | import { permix } from './lib/permix' 49 | 50 | export function usePermissions() { 51 | return usePermix(permix) 52 | } 53 | ``` 54 | 55 | 56 | 57 | 58 | 59 | ## Components 60 | 61 | If you prefer using components, you can import the `createComponents` function from `permix/vue` and create checking components: 62 | 63 | ```ts title="lib/permix.ts" 64 | import { createComponents } from 'permix/vue' 65 | 66 | // ... 67 | 68 | export const { Check } = createComponents(permix) 69 | ``` 70 | 71 | And then you can use the `Check` component in your templates: 72 | 73 | ```vue title="page.vue" 74 | 86 | ``` 87 | 88 | 89 | 90 | 91 | 92 | ## Usage 93 | 94 | Use the `usePermix` composable in your components to check permissions: 95 | 96 | ```vue title="page.vue" 97 | 107 | 108 | 123 | ``` 124 | 125 | 126 | 127 | 128 | 129 | ## Example 130 | 131 | You can find the example of the Vue integration [here](https://github.com/letstri/permix/tree/main/examples/vue). 132 | 133 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /docs/content/docs/integrations/solid.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Solid 3 | description: Learn how to use Permix with Solid applications 4 | --- 5 | 6 | ## Overview 7 | 8 | Permix provides official Solid integration through the `PermixProvider` component and `usePermix` hook. This allows you to manage permissions reactively in your Solid app. 9 | 10 | 11 | Before getting started with Solid integration, make sure you've completed the initial setup steps in the [Quick Start](/docs/quick-start) guide. 12 | 13 | 14 | 15 | 16 | 17 | 18 | ## Setup 19 | 20 | First, wrap your application with the `PermixProvider`: 21 | 22 | ```tsx title="App.tsx" 23 | import { PermixProvider } from 'permix/solid' 24 | import { permix } from './lib/permix' 25 | 26 | function App() { 27 | return ( 28 | 29 | 30 | 31 | ) 32 | } 33 | ``` 34 | 35 | 36 | Remember to always pass the same Permix instance to both the `PermixProvider` and `usePermix` hook to maintain type safety. 37 | 38 | 39 | 40 | 41 | 42 | 43 | ## Hook 44 | 45 | For checking permissions in your components, you can use the `usePermix` hook. And to avoid importing the hook and Permix instance in every component, you can create a custom utility: 46 | 47 | ```tsx title="hooks/use-permissions.ts" 48 | import { usePermix } from 'permix/solid' 49 | import { permix } from '../lib/permix' 50 | 51 | export function usePermissions() { 52 | return usePermix(permix) 53 | } 54 | ``` 55 | 56 | 57 | 58 | 59 | 60 | ## Components 61 | 62 | If you prefer using components, you can import the `createComponents` function from `permix/solid` and create checking components: 63 | 64 | ```ts title="lib/permix.ts" 65 | import { createComponents } from 'permix/solid' 66 | 67 | // ... 68 | 69 | export const { Check } = createComponents(permix) 70 | ``` 71 | 72 | And then you can use the `Check` component in your components: 73 | 74 | ```tsx title="page.tsx" 75 | export default function Page() { 76 | return ( 77 | Will show this if a user doesn't have permission

} // Will show this if a user doesn't have permission 82 | reverse // Will flip the logic of the permission check 83 | > 84 | Will show this if a user has permission 85 |
86 | ) 87 | } 88 | ``` 89 | 90 |
91 | 92 | 93 | 94 | ## Usage 95 | 96 | Use the `usePermix` hook and checking components in your components: 97 | 98 | ```tsx title="page.tsx" 99 | import { usePermix } from 'permix/solid' 100 | import { permix } from './lib/permix' 101 | import { Check } from './lib/permix-components' 102 | 103 | export default function Page() { 104 | const post = usePost() 105 | const { check, isReady } = usePermix(permix) 106 | 107 | const canEdit = () => check('post', 'edit', post) 108 | 109 | return ( 110 | <> 111 | {!isReady() 112 | ?
Loading permissions...
113 | : ( 114 |
115 | {canEdit() ? ( 116 | 117 | ) : ( 118 |

You don't have permission to edit this post

119 | )} 120 | 121 | Can I create a post inside the Check component? 122 | 123 |
124 | ) 125 | } 126 | 127 | ) 128 | } 129 | ``` 130 | 131 |
132 | 133 | 134 | 135 | ## Example 136 | 137 | You can find the example of the Solid integration [here](https://github.com/letstri/permix/tree/main/examples/solid). 138 | 139 | 140 | 141 |
142 | -------------------------------------------------------------------------------- /permix/src/react/hooks.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, renderHook, waitFor } from '@testing-library/react' 2 | import React from 'react' 3 | import { describe, expect, it } from 'vitest' 4 | import { createPermix } from '../core' 5 | import { PermixProvider, usePermix } from './index' 6 | import '@testing-library/jest-dom/vitest' 7 | 8 | describe('permix react', () => { 9 | it('should work with custom hook', () => { 10 | const permix = createPermix<{ 11 | post: { 12 | dataType: { id: string } 13 | action: 'create' | 'read' 14 | } 15 | }>() 16 | 17 | permix.setup({ 18 | post: { 19 | create: true, 20 | read: false, 21 | }, 22 | }) 23 | 24 | const usePermissions = () => usePermix(permix) 25 | 26 | const { result } = renderHook(() => usePermissions(), { 27 | wrapper: ({ children }) => ( 28 | {children} 29 | ), 30 | }) 31 | 32 | expect(result.current.check('post', 'create')).toBe(true) 33 | expect(result.current.check('post', 'read')).toBe(false) 34 | }) 35 | 36 | it('should work with DOM rerender', async () => { 37 | const permix = createPermix<{ 38 | post: { 39 | dataType: { id: string } 40 | action: 'create' | 'read' 41 | } 42 | }>() 43 | 44 | permix.setup({ 45 | post: { 46 | create: post => post?.id === '1', 47 | read: false, 48 | }, 49 | }) 50 | 51 | const TestComponent = () => { 52 | const { check } = usePermix(permix) 53 | 54 | const post = { id: '1' } 55 | 56 | return ( 57 |
58 | {check('post', 'create', post).toString()} 59 | {check('post', 'read').toString()} 60 |
61 | ) 62 | } 63 | 64 | const { getByTestId } = render( 65 | 66 | 67 | , 68 | ) 69 | 70 | expect(getByTestId('create')).toHaveTextContent('true') 71 | expect(getByTestId('read')).toHaveTextContent('false') 72 | 73 | permix.setup({ 74 | post: { 75 | create: post => post?.id === '2', 76 | read: true, 77 | }, 78 | }) 79 | 80 | await waitFor(() => { 81 | expect(getByTestId('create')).toHaveTextContent('false') 82 | expect(getByTestId('read')).toHaveTextContent('true') 83 | }) 84 | }) 85 | 86 | it('should check isReady', async () => { 87 | const permix = createPermix<{ 88 | post: { 89 | dataType: { id: string } 90 | action: 'create' | 'read' 91 | } 92 | }>() 93 | 94 | const TestComponent = () => { 95 | const { isReady } = usePermix(permix) 96 | return
{isReady.toString()}
97 | } 98 | 99 | const { container } = render( 100 | 101 | 102 | , 103 | ) 104 | 105 | expect(container.firstChild).toHaveTextContent('false') 106 | 107 | permix.setup({ 108 | post: { 109 | create: true, 110 | read: false, 111 | }, 112 | }) 113 | 114 | await waitFor(() => { 115 | expect(container.firstChild).toHaveTextContent('true') 116 | }) 117 | }) 118 | 119 | it('should throw error when PermixProvider is missing', () => { 120 | const permix = createPermix<{ 121 | post: { 122 | dataType: { id: string } 123 | action: 'create' | 'read' 124 | } 125 | }>() 126 | 127 | const TestComponent = () => { 128 | const { check } = usePermix(permix) 129 | return
{check('post', 'create').toString()}
130 | } 131 | 132 | expect(() => render()).toThrow() 133 | }) 134 | }) 135 | -------------------------------------------------------------------------------- /permix/src/solid/hooks.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, renderHook, waitFor } from '@solidjs/testing-library' 2 | import { describe, expect, it } from 'vitest' 3 | import { createPermix } from '../core' 4 | import { PermixProvider, usePermix } from './index' 5 | import '@testing-library/jest-dom/vitest' 6 | 7 | describe('permix solid', () => { 8 | it('should work with custom hook', () => { 9 | const permix = createPermix<{ 10 | post: { 11 | dataType: { id: string } 12 | action: 'create' | 'read' 13 | } 14 | }>() 15 | 16 | permix.setup({ 17 | post: { 18 | create: true, 19 | read: false, 20 | }, 21 | }) 22 | 23 | const usePermissions = () => usePermix(permix) 24 | 25 | const { result } = renderHook(() => usePermissions(), { 26 | wrapper: props => ( 27 | {props.children} 28 | ), 29 | }) 30 | 31 | expect(result.check('post', 'create')).toBe(true) 32 | expect(result.check('post', 'read')).toBe(false) 33 | }) 34 | 35 | it('should work with DOM rerender', async () => { 36 | const permix = createPermix<{ 37 | post: { 38 | dataType: { id: string } 39 | action: 'create' | 'read' 40 | } 41 | }>() 42 | 43 | permix.setup({ 44 | post: { 45 | create: post => post?.id === '1', 46 | read: false, 47 | }, 48 | }) 49 | 50 | const TestComponent = () => { 51 | const { check } = usePermix(permix) 52 | 53 | const post = { id: '1' } 54 | 55 | return ( 56 |
57 | {check('post', 'create', post).toString()} 58 | {check('post', 'read').toString()} 59 |
60 | ) 61 | } 62 | 63 | const { getByTestId } = render(() => , { 64 | wrapper: props => ( 65 | {props.children} 66 | ), 67 | }) 68 | 69 | expect(getByTestId('create')).toHaveTextContent('true') 70 | expect(getByTestId('read')).toHaveTextContent('false') 71 | 72 | permix.setup({ 73 | post: { 74 | create: post => post?.id === '2', 75 | read: true, 76 | }, 77 | }) 78 | 79 | await waitFor(() => { 80 | expect(getByTestId('create')).toHaveTextContent('false') 81 | expect(getByTestId('read')).toHaveTextContent('true') 82 | }) 83 | }) 84 | 85 | it('should check isReady', async () => { 86 | const permix = createPermix<{ 87 | post: { 88 | dataType: { id: string } 89 | action: 'create' | 'read' 90 | } 91 | }>() 92 | 93 | const TestComponent = () => { 94 | const { isReady } = usePermix(permix) 95 | return
{isReady().toString()}
96 | } 97 | 98 | const { container } = render(() => , { 99 | wrapper: props => ( 100 | {props.children} 101 | ), 102 | }) 103 | 104 | expect(container.firstChild).toHaveTextContent('false') 105 | 106 | permix.setup({ 107 | post: { 108 | create: true, 109 | read: false, 110 | }, 111 | }) 112 | 113 | await waitFor(() => { 114 | expect(container.firstChild).toHaveTextContent('true') 115 | }) 116 | }) 117 | 118 | it('should throw error when PermixProvider is missing', () => { 119 | const permix = createPermix<{ 120 | post: { 121 | dataType: { id: string } 122 | action: 'create' | 'read' 123 | } 124 | }>() 125 | 126 | const TestComponent = () => { 127 | const { check } = usePermix(permix) 128 | return
{check('post', 'create').toString()}
129 | } 130 | 131 | expect(() => render(() => )).toThrow() 132 | }) 133 | }) 134 | -------------------------------------------------------------------------------- /examples/react/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/content/docs/guide/template.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Template 3 | description: Learn how to define permissions using templates 4 | --- 5 | 6 | ## Overview 7 | 8 | Permix provides a `template` method that allows you to define permissions in a separate location from where they are set up. This is useful for organizing permission definitions and reusing them across different parts of your application. 9 | 10 | 11 | Templates are validated when they are created, ensuring your permission definitions are correct before runtime. 12 | 13 | 14 | ## Basic Usage 15 | 16 | The simplest way to use templates is to define static permissions: 17 | 18 | ```ts 19 | const adminPermissions = permix.template({ 20 | post: { 21 | create: true, 22 | read: true 23 | } 24 | }) 25 | 26 | // Later, use the template to setup permissions 27 | permix.setup(adminPermissions()) 28 | ``` 29 | 30 | ## Dynamic Templates 31 | 32 | Templates can accept parameters to create dynamic permissions based on runtime values: 33 | 34 | ```ts 35 | interface User { 36 | id: string 37 | role: string 38 | } 39 | 40 | const userPermissions = permix.template(({ id: userId }: User) => ({ 41 | post: { 42 | create: true, 43 | read: true, 44 | update: post => post?.authorId === userId 45 | } 46 | })) 47 | 48 | // Use with specific user data 49 | const user = await getUser() 50 | permix.setup(userPermissions(user)) 51 | ``` 52 | 53 | ## Type Safety 54 | 55 | Templates maintain full type safety from your Permix instance definition: 56 | 57 | ```ts twoslash 58 | import { createPermix } from 'permix' 59 | 60 | const permix = createPermix<{ 61 | post: { 62 | action: 'create' 63 | } 64 | }>() 65 | 66 | // @errors: 2353 67 | // This will throw an error 68 | const invalidTemplate = permix.template({ 69 | post: { 70 | edit: true 71 | } 72 | }) 73 | ``` 74 | 75 | ## Role-Based Example 76 | 77 | Templates are particularly useful for role-based permission systems: 78 | 79 | ```ts 80 | const editorPermissions = permix.template({ 81 | post: { 82 | create: true, 83 | read: true, 84 | update: post => !post?.published, 85 | delete: post => !post?.published 86 | } 87 | }) 88 | 89 | const userPermissions = permix.template(({ id: userId }: User) => ({ 90 | post: { 91 | create: false, 92 | read: true, 93 | update: post => post?.authorId === userId, 94 | delete: false 95 | } 96 | })) 97 | 98 | // Setup based on user role 99 | function setupPermissions() { 100 | const user = await getUser() 101 | const permissionsMap = { 102 | editor: () => editorPermissions(), 103 | user: () => userPermissions(user) 104 | } 105 | 106 | return permix.setup(permissionsMap[user.role]()) 107 | } 108 | ``` 109 | 110 | ## Standalone Templates 111 | 112 | You can define permission templates outside of the Permix instance using the `PermixRules` type. This is useful when you want to organize your permission logic in separate files: 113 | 114 | ```ts twoslash 115 | import type { PermixRules, PermixDefinition } from 'permix' 116 | import { createPermix } from 'permix' 117 | 118 | // Define your Permix definition type 119 | type Definition = PermixDefinition<{ 120 | post: { 121 | dataType: { id: string; authorId: string } 122 | action: 'create' | 'read' | 'update' | 'delete' 123 | } 124 | }> 125 | 126 | // It can be in separate file and imported here 127 | const permix = createPermix() 128 | 129 | // Create a standalone template function 130 | function userPermissions(userId: string, role: 'admin' | 'user'): PermixRules { 131 | return { 132 | post: { 133 | create: role === 'admin', 134 | read: true, 135 | update: role === 'admin' ? true : (post) => post?.authorId === userId, 136 | delete: role === 'admin' 137 | } 138 | } 139 | } 140 | 141 | // Later, use it with your Permix instance 142 | const permissions = userPermissions('1', 'admin') 143 | 144 | permix.setup(permissions) 145 | ``` 146 | 147 | This approach allows you to: 148 | - Keep permission logic separate from your Permix instance 149 | - Reuse permission templates across different parts of your application 150 | - Maintain full type safety with your Permix definition 151 | -------------------------------------------------------------------------------- /permix/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "permix", 3 | "displayName": "Permix", 4 | "type": "module", 5 | "version": "3.6.0", 6 | "private": false, 7 | "packageManager": "pnpm@10.20.0", 8 | "description": "Permix is a lightweight, framework-agnostic, type-safe permissions management library for JavaScript applications on the client and server sides.", 9 | "author": "Valerii Strilets", 10 | "license": "MIT", 11 | "funding": "https://github.com/sponsors/letstri", 12 | "homepage": "https://permix.letstri.dev", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/letstri/permix.git", 16 | "directory": "permix" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/letstri/permix/issues" 20 | }, 21 | "keywords": [ 22 | "permissions", 23 | "authorization", 24 | "acl", 25 | "access-control", 26 | "typescript", 27 | "react", 28 | "vue", 29 | "type-safe", 30 | "rbac", 31 | "security", 32 | "permissions-management", 33 | "frontend", 34 | "javascript" 35 | ], 36 | "exports": { 37 | ".": { 38 | "types": "./dist/core/index.d.ts", 39 | "import": "./dist/core/index.mjs" 40 | }, 41 | "./react": { 42 | "types": "./dist/react/index.d.ts", 43 | "import": "./dist/react/index.mjs" 44 | }, 45 | "./vue": { 46 | "types": "./dist/vue/index.d.ts", 47 | "import": "./dist/vue/index.mjs" 48 | }, 49 | "./trpc": { 50 | "types": "./dist/trpc/index.d.ts", 51 | "import": "./dist/trpc/index.mjs" 52 | }, 53 | "./orpc": { 54 | "types": "./dist/orpc/index.d.ts", 55 | "import": "./dist/orpc/index.mjs" 56 | }, 57 | "./express": { 58 | "types": "./dist/express/index.d.ts", 59 | "import": "./dist/express/index.mjs" 60 | }, 61 | "./hono": { 62 | "types": "./dist/hono/index.d.ts", 63 | "import": "./dist/hono/index.mjs" 64 | }, 65 | "./node": { 66 | "types": "./dist/node/index.d.ts", 67 | "import": "./dist/node/index.mjs" 68 | }, 69 | "./elysia": { 70 | "types": "./dist/elysia/index.d.ts", 71 | "import": "./dist/elysia/index.mjs" 72 | }, 73 | "./fastify": { 74 | "types": "./dist/fastify/index.d.ts", 75 | "import": "./dist/fastify/index.mjs" 76 | }, 77 | "./solid": { 78 | "types": "./dist/solid/index.d.ts", 79 | "import": "./dist/solid/index.mjs" 80 | } 81 | }, 82 | "main": "./dist/core/index.mjs", 83 | "types": "./dist/core/index.d.ts", 84 | "files": [ 85 | "dist" 86 | ], 87 | "engines": { 88 | "node": ">=18" 89 | }, 90 | "scripts": { 91 | "prepublishOnly": "run-s check-types test build scripts:copy-readme", 92 | "build": "unbuild", 93 | "test": "vitest run", 94 | "check-types": "tsc --build", 95 | "scripts:copy-readme": "node ./scripts/copy-readme.ts" 96 | }, 97 | "peerDependencies": { 98 | "@orpc/server": ">=1", 99 | "@trpc/server": ">=11", 100 | "elysia": ">=1", 101 | "express": ">=4", 102 | "fastify": ">=5", 103 | "fastify-plugin": ">=5", 104 | "hono": ">=4", 105 | "react": ">=18", 106 | "react-dom": ">=18", 107 | "solid-js": ">=1", 108 | "vue": ">=3" 109 | }, 110 | "peerDependenciesMeta": { 111 | "@orpc/server": { 112 | "optional": true 113 | }, 114 | "@trpc/server": { 115 | "optional": true 116 | }, 117 | "elysia": { 118 | "optional": true 119 | }, 120 | "express": { 121 | "optional": true 122 | }, 123 | "fastify": { 124 | "optional": true 125 | }, 126 | "fastify-plugin": { 127 | "optional": true 128 | }, 129 | "hono": { 130 | "optional": true 131 | }, 132 | "react": { 133 | "optional": true 134 | }, 135 | "react-dom": { 136 | "optional": true 137 | }, 138 | "solid-js": { 139 | "optional": true 140 | }, 141 | "vue": { 142 | "optional": true 143 | } 144 | }, 145 | "devDependencies": { 146 | "@babel/preset-react": "^7.28.5", 147 | "@babel/preset-typescript": "^7.28.5", 148 | "@rollup/plugin-babel": "^6.1.0", 149 | "@solidjs/testing-library": "^0.8.10", 150 | "@testing-library/jest-dom": "^6.9.1", 151 | "@testing-library/react": "^16.3.0", 152 | "@types/express": "^5.0.5", 153 | "@types/node": "^24.10.0", 154 | "@types/react": "^19.2.2", 155 | "@types/supertest": "^6.0.3", 156 | "@vitejs/plugin-react": "^5.1.0", 157 | "@vitest/coverage-v8": "^4.0.6", 158 | "@vue/test-utils": "^2.4.6", 159 | "babel-preset-solid": "^1.9.10", 160 | "happy-dom": "^20.0.10", 161 | "react-dom": "^19.2.0", 162 | "supertest": "^7.1.4", 163 | "typescript": "^5.9.3", 164 | "unbuild": "^3.6.1", 165 | "vite-plugin-solid": "^2.11.10", 166 | "vitest": "^4.0.6", 167 | "vue": "^3.5.17", 168 | "zod": "^4.1.12" 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /docs/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: The type-safe permission management you've always needed 4 | icon: RiInboxLine 5 | --- 6 | 7 | ## Idea 8 | 9 | In my many years of experience, I have worked extensively with permissions management, and early in my career I wrote solutions that looked like this: 10 | 11 | ```ts 12 | if (user.role === 'admin') { 13 | // do something 14 | } 15 | ``` 16 | 17 | Later, I started using [CASL](https://casl.js.org) for permissions management in a [Vue](https://vuejs.org/) application. 18 | 19 | ```ts 20 | can('read', ['Post', 'Comment']); 21 | can('manage', 'Post', { author: 'me' }); 22 | can('create', 'Comment'); 23 | ``` 24 | 25 | But time goes on, CASL becomes older, and developers' needs grow, especially for type-safe libraries. Unfortunately, CASL couldn't satisfy my type validation needs and so I started thinking again about writing my own validation solution. But this time I wanted to make it as a library, as I already had experience with open-source. 26 | 27 | ## Implementation 28 | 29 | I started to create my own solution. However, nothing occurred to me until I watched a Web Dev Simplified [video](https://www.youtube.com/watch?v=5GG-VUvruzE) where he demonstrated an example of implementing permission management as he envisioned it. I really liked his approach because it was based on type-safety, which is exactly what I needed. 30 | 31 | So I'm ready to present to you my permission management solution called Permix! 32 | 33 | ## DX 34 | 35 | When creating Permix, the goal was to simplify DX as much as possible without losing type-safety and provide the necessary functionality. 36 | 37 | That is why you only need to write the following code to get started: 38 | 39 | ```ts twoslash 40 | import { createPermix } from 'permix' 41 | 42 | const permix = createPermix<{ 43 | post: { 44 | action: 'read' 45 | } 46 | }>() 47 | 48 | permix.setup({ 49 | post: { 50 | read: true, 51 | } 52 | }) 53 | 54 | const canReadPost = permix.check('post', 'read') // true 55 | ``` 56 | 57 | It looks too simple, so here's a more interesting example: 58 | 59 | ```ts twoslash 60 | import type { PermixDefinition } from 'permix' 61 | import { createPermix } from 'permix' 62 | 63 | // You can take types from your database 64 | interface User { 65 | id: string 66 | role: 'editor' | 'user' 67 | } 68 | 69 | interface Post { 70 | id: string 71 | title: string 72 | authorId: string 73 | published: boolean 74 | } 75 | 76 | interface Comment { 77 | id: string 78 | content: string 79 | authorId: string 80 | } 81 | 82 | // Create definition to describe your permissions 83 | type PermissionsDefinition = PermixDefinition<{ 84 | post: { 85 | dataType: Post 86 | action: 'create' | 'read' | 'update' | 'delete' 87 | } 88 | comment: { 89 | dataType: Comment 90 | action: 'create' | 'read' | 'update' 91 | } 92 | }> 93 | 94 | const permix = createPermix() 95 | 96 | // Define permissions for different users 97 | const editorPermissions = permix.template({ 98 | post: { 99 | create: true, 100 | read: true, 101 | update: post => !post?.published, 102 | delete: post => !post?.published, 103 | }, 104 | comment: { 105 | create: false, 106 | read: true, 107 | update: false, 108 | }, 109 | }) 110 | 111 | const userPermissions = permix.template(({ id: userId }: User) => ({ 112 | post: { 113 | create: false, 114 | read: true, 115 | update: false, 116 | delete: false, 117 | }, 118 | comment: { 119 | create: true, 120 | read: true, 121 | update: comment => comment?.authorId === userId, 122 | }, 123 | })) 124 | 125 | async function getUser() { 126 | // Imagine that this function is fetching user from database 127 | return { 128 | id: '1', 129 | role: 'editor' as const, 130 | } 131 | } 132 | 133 | // Setup permissions for signed in user 134 | async function setupPermix() { 135 | const user = await getUser() 136 | const permissionsMap = { 137 | editor: () => editorPermissions(), 138 | user: () => userPermissions(user), 139 | } 140 | 141 | permix.setup(permissionsMap[user.role]()) 142 | } 143 | 144 | // Call setupPermix where you need to setup permissions 145 | setupPermix() 146 | 147 | // Check if a user has permission to do something 148 | const canCreatePost = permix.check('post', 'create') 149 | 150 | async function getComment() { 151 | // Imagine that this function is fetching comment from database 152 | return { 153 | id: '1', 154 | content: 'Hello, world!', 155 | authorId: '1', 156 | } 157 | } 158 | 159 | const comment = await getComment() 160 | 161 | const canUpdateComment = permix.check('comment', 'update', comment) 162 | ``` 163 | 164 | ## Benefits 165 | 166 | What are the benefits of using Permix? 167 | 168 | - 100% type-safe without writing TypeScript (except for initialization) 169 | - Single source of truth for your entire app 170 | - Perfect match for TypeScript monorepos 171 | - Zero dependencies 172 | - Useful methods for specific cases 173 | - Large number of integrations for different frameworks, such as [React](/docs/integrations/react), [Vue](/docs/integrations/vue), [Express](/docs/integrations/express), and more. 174 | 175 | ## Ready? 176 | 177 | Ready to take Permix to your project? Let's go to the [Quick Start](/docs/quick-start) page. 178 | --------------------------------------------------------------------------------