permix
Version:
Permix is a lightweight, framework-agnostic, type-safe permissions management library for JavaScript applications on the client and server sides.
183 lines (182 loc) • 5.75 kB
JavaScript
import { PermixForbiddenError, PermixNotFoundError, createCheckContext, createHooks, createPermix as createPermix$1, createTemplate } from "../core/index.mjs";
import { createMiddleware } from "@tanstack/react-start";
//#region src/tanstack-start/permix.ts
function buildPermix(resolveKey, options = {}) {
const onForbidden = options.onForbidden ?? (() => {
throw new PermixForbiddenError();
});
const hooks = createHooks();
/**
* Returns the request-scoped Permix instance from a TanStack Start context
* object, or `null` when not set up yet.
*/
function get(context) {
return context?.[resolveKey()] ?? null;
}
/**
* Like {@link get}, but throws {@link PermixNotFoundError} when the instance
* is missing.
*/
function getOrThrow(context) {
const instance = get(context);
if (!instance) throw new PermixNotFoundError(resolveKey());
return instance;
}
/**
* Build the server handler used by {@link setupMiddleware}, for passing to
* your own `createMiddleware().server(...)` call.
*
* Prefer `setupMiddleware()` unless the callback reaches for server-only
* imports (a database client, an auth library, `node:` builtins). TanStack
* Start strips `.server()` bodies from the client bundle by matching
* `createMiddleware().server(...)` **in your own source**. It cannot see the
* `.server()` call hidden inside `setupMiddleware()`, so those imports stay
* in the client graph and surface as `Buffer is not defined` or externalized
* `node:` module warnings in the browser.
*
* Writing the `.server()` boundary yourself puts the callback where the
* compiler can strip it.
*
* @example
* ```ts
* import { createMiddleware } from '@tanstack/react-start'
* import { auth } from './lib/auth'
* import { permix } from './lib/permix'
*
* export const permixMiddleware = createMiddleware().server(
* permix.createSetupHandler(async ({ request }) => {
* const session = await auth.api.getSession({ headers: request.headers })
* return { post: { create: !!session, read: true } }
* }),
* )
* ```
*
* @link https://permix.letstri.dev/docs/integrations/tanstack-start#server-only-imports-in-the-setup-callback
*/
function createSetupHandler(callbackOrRules) {
return async ({ next, request }) => {
const rules = typeof callbackOrRules === "function" ? await callbackOrRules({ request }) : callbackOrRules;
const instance = createPermix$1(rules);
instance.hook("check", (context) => {
hooks.callHook("check", context);
});
return next({ context: { [resolveKey()]: instance } });
};
}
/**
* TanStack Start middleware that creates a request-scoped Permix instance,
* calls `setup()` with the resolved rules, and stores it in the server context.
*
* Register it globally via `createStart({ requestMiddleware: [...] })` so it
* runs for every request, or attach it to specific server routes.
*
* If the callback imports server-only code, use {@link createSetupHandler}
* instead so TanStack Start can strip it from the client bundle.
*/
function setupMiddleware(callbackOrRules) {
return createMiddleware().server(createSetupHandler(callbackOrRules));
}
/**
* TanStack Start middleware that enforces a permission check before the
* server function handler runs.
*
* @example
* ```ts
* export const createPost = createServerFn({ method: 'POST' })
* .middleware([permix.checkMiddleware('post.create')])
* .handler(() => { ... })
* ```
*/
const checkMiddleware = (...args) => createMiddleware({ type: "function" }).server(async ({ next, context }) => {
if (getOrThrow(context).check(...args)) return await next();
else return await onForbidden({
next,
...createCheckContext(...args)
});
});
/**
* Serialize the request's permission state for client hydration.
*/
function dehydrate(context) {
return getOrThrow(context).dehydrate();
}
function getRules(context) {
return get(context)?.getRules() ?? null;
}
function template(rules) {
return createTemplate(rules);
}
return {
setupMiddleware,
createSetupHandler,
checkMiddleware,
get,
getOrThrow,
dehydrate,
getRules,
template,
hook: hooks.hook,
hookOnce: hooks.hookOnce,
get key() {
return resolveKey();
},
$inferDefinition: void 0,
$inferPath: void 0
};
}
/**
* Create a per-request Permix helper for TanStack Start.
*
* Uses TanStack Start's per-request server context to share a single Permix
* instance across global middleware, server routes, server functions, and the
* router for the lifetime of each request.
*
* Use `.contextKey('name')` to set a custom context key (defaults to
* `'__permix'`).
*
* @example
* ```ts
* // lib/permix.ts
* import { createPermix } from 'permix/tanstack-start'
*
* export const permix = createPermix<{
* post: ['create', 'read', 'update', 'delete']
* }>()
* ```
*
* ```ts
* // src/start.ts
* import { createStart } from '@tanstack/react-start'
* import { getSession } from './lib/auth'
* import { permix } from './lib/permix'
*
* export const startInstance = createStart(() => ({
* requestMiddleware: [
* permix.setupMiddleware(async ({ request }) => {
* const session = await getSession(request)
* return {
* post: {
* create: !!session,
* read: true,
* update: session?.role === 'admin',
* delete: session?.role === 'admin',
* },
* }
* }),
* ],
* }))
* ```
*
* @link https://permix.letstri.dev/docs/integrations/tanstack-start
*/
function createPermix(options = {}) {
let key = "__permix";
const permix = buildPermix(() => key, options);
const instance = Object.assign(permix, { contextKey(newKey) {
key = newKey;
return instance;
} });
return instance;
}
//#endregion
export { createPermix };