UNPKG

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
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 };