diff --git a/_artifacts/domain_map.yaml b/_artifacts/domain_map.yaml index 318aa7c9..df41d08c 100644 --- a/_artifacts/domain_map.yaml +++ b/_artifacts/domain_map.yaml @@ -39,7 +39,7 @@ domains: - name: 'SSR and hydration' slug: ssr description: > - dehydrate/hydrate snapshots, PermixHydrate, Next.js and TanStack Start wiring. + dehydrate/hydrate snapshots, PermixHydrate, Next.js, TanStack Start, and Nuxt wiring. skills: - name: 'Getting started' @@ -77,7 +77,7 @@ skills: Everything past initial setup: dot-path check, callback combinators, ~all/~any, entity-aware ReBAC rules, isReady/isReadyAsync; PermixProvider/usePermix/createComponents and SSR dehydrate/hydrate for - React, Vue, Solid, Svelte, Next.js, TanStack Start; setupMiddleware and + React, Vue, Solid, Svelte, Next.js, TanStack Start, Nuxt; setupMiddleware and checkMiddleware for Express, Hono, Fastify, tRPC, oRPC, Node, Elysia. Single skill with a thin SKILL.md router and three reference files loaded on demand. @@ -91,6 +91,7 @@ skills: - svelte - next - tanstack-start + - nuxt - express - hono - fastify @@ -119,7 +120,7 @@ skills: - 'Wrap the app tree and hide actions based on permissions' - 'Integrate permix/react, permix/vue, permix/solid, or permix/svelte' - 'Pass permission booleans from server render to client' - - 'Wire permix/next or permix/tanstack-start' + - 'Wire permix/next, permix/tanstack-start, or permix/nuxt' - 'Protect HTTP or RPC routes on the server' - 'Derive rules from authenticated request context' references: @@ -137,6 +138,7 @@ skills: - 'letstri/permix:docs/content/docs/integrations/svelte.mdx' - 'letstri/permix:docs/content/docs/integrations/next.mdx' - 'letstri/permix:docs/content/docs/integrations/tanstack-start.mdx' + - 'letstri/permix:docs/content/docs/integrations/nuxt.mdx' - 'letstri/permix:docs/content/docs/integrations/express.mdx' - 'letstri/permix:docs/content/docs/integrations/hono.mdx' - 'letstri/permix:docs/content/docs/integrations/fastify.mdx' @@ -153,6 +155,7 @@ coverage: - examples/* - next - tanstack-start + - nuxt failure_modes: - mistake: 'Using v3 { action, dataType } schema shape' diff --git a/_artifacts/skill_tree.yaml b/_artifacts/skill_tree.yaml index 70b42866..69efa7db 100644 --- a/_artifacts/skill_tree.yaml +++ b/_artifacts/skill_tree.yaml @@ -39,7 +39,7 @@ skills: check() dot paths, callbacks, ~all/~any, entity-aware ReBAC rules, isReady/isReadyAsync; PermixProvider/usePermix/createComponents and SSR dehydrate/hydrate for React, Vue, Solid, Svelte, Next.js, TanStack - Start; setupMiddleware/checkMiddleware for Express, Hono, Fastify, + Start, Nuxt; setupMiddleware/checkMiddleware for Express, Hono, Fastify, tRPC, oRPC, Node, Elysia. Thin router with references loaded on demand. requires: - permix-getting-started @@ -50,6 +50,7 @@ skills: - svelte - next - tanstack-start + - nuxt - express - hono - fastify @@ -72,6 +73,7 @@ skills: - 'letstri/permix:docs/content/docs/integrations/svelte.mdx' - 'letstri/permix:docs/content/docs/integrations/next.mdx' - 'letstri/permix:docs/content/docs/integrations/tanstack-start.mdx' + - 'letstri/permix:docs/content/docs/integrations/nuxt.mdx' - 'letstri/permix:docs/content/docs/integrations/express.mdx' - 'letstri/permix:docs/content/docs/integrations/hono.mdx' - 'letstri/permix:docs/content/docs/integrations/fastify.mdx' @@ -88,3 +90,4 @@ coverage: - examples/* - next - tanstack-start + - nuxt diff --git a/docs/content/docs/comparison.mdx b/docs/content/docs/comparison.mdx index d0d9ea67..78198c77 100644 --- a/docs/content/docs/comparison.mdx +++ b/docs/content/docs/comparison.mdx @@ -19,7 +19,7 @@ Permix is a library that provides a way to manage permissions in your applicatio | Events | ✅ | ❌ | | Simple DX | ✅ Create instance, use built-in integrations | ❌ In CASL you need to manage a lot of stuff manually (type-safe, hydration, etc.) | | Modernity | ✅ Uses modern updates and features of each lib and framework | ❌ CASL was created a long time ago and hasn't updated the core | -| Size | **2.64 kB** gzip (core) [react 0.86 kB, vue 0.87 kB, solid 0.82 kB, next 1.00 kB, tanstack-start 2.05 kB, svelte ~2.17 kB, …] | **6.17 kB** min+gzip (core) [@casl/react 0.62 kB] | +| Size | **2.64 kB** gzip (core) [react 0.86 kB, vue 0.87 kB, solid 0.82 kB, next 1.00 kB, nuxt 0.99 kB, tanstack-start 2.05 kB, svelte ~2.17 kB, …] | **6.17 kB** min+gzip (core) [@casl/react 0.62 kB] | Sizes are hard numbers from published builds — see [Bundle size](#bundle-size). Bracketed values are integration adapters imported on top of core. @@ -39,6 +39,7 @@ Built with `pnpm run build` in the `permix` package (`tsdown` for all entries ex | `permix/solid` | 0.82 kB | adapter | | `permix/svelte` | ~2.17 kB | adapter (`dist/svelte/`, `svelte-package` build) | | `permix/next` | 1.00 kB | adapter | +| `permix/nuxt` | 0.99 kB | adapter | | `permix/tanstack-start` | 2.05 kB | adapter | | `permix/node` | 0.95 kB | adapter | | `permix/server` | 1.10 kB | adapter | diff --git a/docs/content/docs/integrations/next.mdx b/docs/content/docs/integrations/next.mdx index 5f1f1204..9b2d5e5e 100644 --- a/docs/content/docs/integrations/next.mdx +++ b/docs/content/docs/integrations/next.mdx @@ -322,7 +322,7 @@ Returns an object with the following methods: ## TanStack Start and other frameworks -The client layer (`permix/react`) is framework-agnostic. If you're using TanStack Start, Remix, or a custom React SSR setup, you can still use `permix/react` on the client. For the server side, either: +The client layer (`permix/react`) is framework-agnostic. If you're using TanStack Start, Remix, or a custom React SSR setup, you can still use `permix/react` on the client. For Nuxt, use [`permix/nuxt`](/docs/integrations/nuxt) on the server and `permix/vue` on the client. For the server side, either: - Use the dedicated [`permix/tanstack-start`](/docs/integrations/tanstack-start) integration, which follows the same shape as `permix/next`, or - Create a core Permix instance per request manually (see [Hydration guide](/docs/guide/hydration)), or diff --git a/docs/content/docs/integrations/nuxt.mdx b/docs/content/docs/integrations/nuxt.mdx new file mode 100644 index 00000000..620d2e32 --- /dev/null +++ b/docs/content/docs/integrations/nuxt.mdx @@ -0,0 +1,258 @@ +--- +title: Nuxt +description: Learn how to use Permix with Nuxt +--- + +## Overview + +Permix provides middleware for [Nuxt](https://nuxt.com/) server routes through `permix/nuxt`. It follows the same shape as the other server integrations: `setupMiddleware` creates a **per-request** Permix instance on the Nitro `event.context`, and `checkMiddleware` guards a route. Concurrent requests never share state. + +The client side reuses the Vue integration (`permix/vue`): the server dehydrates its state and the client hydrates it with `PermixProvider` + `PermixHydrate`. + + + Before getting started with the Nuxt integration, make sure you've completed + the initial setup steps in the [Quick Start](/docs/quick-start) guide. + Familiarity with the [Hydration guide](/docs/guide/hydration) and the [Vue + integration](/docs/integrations/vue) helps too. + + + + + + +## Define your permissions + +Create the factory once in a server utility so every server file can import it: + +```ts title="server/utils/permix.ts" +import { createPermix } from 'permix/nuxt' + +interface Post { + id: string + authorId: string +} + +export const permix = createPermix<{ + post: [ + { name: 'create'; type: Post }, + { name: 'read'; type: Post }, + { name: 'update'; type: Post }, + { name: 'delete'; type: Post }, + ] +}>() +``` + +The factory holds no permission state at module scope — every request gets its own instance on `event.context`. + + + + + +## Setup per request + +Register `setupMiddleware` as a Nitro server middleware so it runs before every route (including SSR page renders). The callback receives the h3 `event`, so you can derive rules from the session, headers, or a database lookup: + +```ts title="server/middleware/permix.ts" +export default defineEventHandler( + permix.setupMiddleware(async ({ event }) => { + const user = event.context.user + + return { + post: { + create: !!user, + read: true, + update: (post) => post?.authorId === user?.id, + delete: user?.role === 'admin', + }, + } + }) +) +``` + + + Nitro runs `server/middleware` files in alphabetical order. Name the file so + it runs after the middleware that populates `event.context.user`. + + + + + + +## Check on the server + +Guard a route with `checkMiddleware` in the handler's `onRequest` hooks: + +```ts title="server/api/posts/index.post.ts" +export default defineEventHandler({ + onRequest: [permix.checkMiddleware('post.create')], + handler: async (event) => { + // create the post... + return { ok: true } + }, +}) +``` + +Or check manually anywhere the request `event` is available, including Vue pages during SSR via `useRequestEvent()`: + +```ts title="server/api/posts/[id].put.ts" +export default defineEventHandler(async (event) => { + const post = await getPost(getRouterParam(event, 'id')!) + + if (!permix.getOrThrow(event).check('post.update', post)) { + throw createError({ statusCode: 403, statusMessage: 'Forbidden' }) + } + + // update the post... + return { ok: true } +}) +``` + +`checkMiddleware` accepts the same arguments as core `check()`: a dot path with optional data, `'~all'` / `'~any'`, or a callback such as `(c) => c('post.read') && c('post.update')`. + + + + + +## Send permissions to the client + +`permix/nuxt` is server-only. Create a client singleton with core `permix`, dehydrate the request's rules in a server plugin, and hydrate them with `PermixProvider` + `PermixHydrate` from `permix/vue`: + +```ts title="app/utils/permix.ts" +import { createPermix } from 'permix' + +// Same definition as on the server. +export const permix = createPermix<{ + post: [ + { name: 'create'; type: { id: string; authorId: string } }, + { name: 'read'; type: { id: string; authorId: string } }, + { name: 'update'; type: { id: string; authorId: string } }, + { name: 'delete'; type: { id: string; authorId: string } }, + ] +}>() +``` + +```ts title="app/plugins/permix.server.ts" +import { permix } from '~~/server/utils/permix' + +export default defineNuxtPlugin(() => { + const event = useRequestEvent() + useState('permix', () => + event ? permix.getOrThrow(event).dehydrate() : null + ) +}) +``` + +```vue title="app/app.vue" + + + +``` + + + `hydrate()` restores the boolean state but does not flip `isReady` — + function-based rules are lost during serialization. Call `permix.setup(...)` + on the client too if you need function rules or `isReady` there. See the + [Hydration guide](/docs/guide/hydration). + + +From any component, use `usePermix` from `permix/vue`: + +```vue title="app/components/EditButton.vue" + + + +``` + + + + + +## Using Templates + +`template()` works the same as in the core API: + +```ts title="server/utils/permix.ts" +export const adminTemplate = permix.template({ + post: { create: true, read: true, update: true, delete: true }, +}) + +export const guestTemplate = permix.template({ + post: { create: false, read: true, update: false, delete: false }, +}) +``` + +```ts title="server/middleware/permix.ts" +export default defineEventHandler( + permix.setupMiddleware(({ event }) => + event.context.user?.role === 'admin' ? adminTemplate() : guestTemplate() + ) +) +``` + + + + + +## Example + +You can find a runnable example of the Nuxt integration [here](https://github.com/letstri/permix/tree/main/examples/nuxt). It uses a plain h3 app, which is what Nitro runs under the hood. + + + + + +## Custom Error Handling + +By default a denied `checkMiddleware` throws an error with `statusCode: 403`, which h3 turns into a 403 response. Provide `onForbidden` to customise it: + +```ts +const permix = createPermix({ + onForbidden: ({ path }) => { + throw createError({ + statusCode: 403, + statusMessage: `You don't have permission for ${path}`, + }) + }, +}) +``` + +The `onForbidden` handler receives: + +- `event`: the h3 event +- `path`: the permission path that was checked (or `null` for callback checks) +- `data`: optional entity data passed to the check + +## API + +### `createPermix(options?)` + +| Method | Description | +| --- | --- | +| `setupMiddleware(rules \| callback)` | h3 handler that creates a per-request instance on `event.context`. Wrap with `defineEventHandler`. | +| `checkMiddleware(...args)` | h3 handler that throws via `onForbidden` when denied. Use in `onRequest`. | +| `get(event)` | Return the instance, or `null`. | +| `getOrThrow(event)` | Return the instance, or throw `PermixNotFoundError`. | +| `getRules(event)` | Return the current rules, or `null`. | +| `template(rules)` | Create a reusable rule set. | +| `hook` / `hookOnce` | Factory-level hooks, e.g. `permix.hook('check', ...)` across all requests. | +| `contextKey(key)` | Use a custom `event.context` key instead of the default `Symbol('permix')`. | diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json index dbd79bfc..cb8716a4 100644 --- a/docs/content/docs/meta.json +++ b/docs/content/docs/meta.json @@ -18,6 +18,7 @@ "integrations/react", "integrations/next", "integrations/tanstack-start", + "integrations/nuxt", "integrations/vue", "integrations/solid", "integrations/svelte", diff --git a/docs/content/docs/migration-v3-to-v4.mdx b/docs/content/docs/migration-v3-to-v4.mdx index 20be84ca..effc20d4 100644 --- a/docs/content/docs/migration-v3-to-v4.mdx +++ b/docs/content/docs/migration-v3-to-v4.mdx @@ -340,6 +340,7 @@ These are additive — migrate the core API first, then adopt what you need: | ----------------------- | ------------------------------------------- | | `permix/next` | Next.js App Router, request-scoped instance | | `permix/tanstack-start` | TanStack Start middleware and SSR | +| `permix/nuxt` | Nuxt / Nitro, request-scoped instance | | `permix/server` | Framework-agnostic fetch middleware | | `permix/svelte` | Svelte 5 runes | | `permix/drizzle` | Rules from Drizzle v1 schema | diff --git a/docs/content/docs/quick-start.mdx b/docs/content/docs/quick-start.mdx index c6bc2196..83290357 100644 --- a/docs/content/docs/quick-start.mdx +++ b/docs/content/docs/quick-start.mdx @@ -170,6 +170,10 @@ Continuing from the quick start, you can now explore how Permix integrates with Integration with TanStack Start. + + Integration with Nuxt via Nitro request isolation. + + Integration with Solid via provider and hook. diff --git a/examples/nuxt/main.ts b/examples/nuxt/main.ts new file mode 100644 index 00000000..76d4ba09 --- /dev/null +++ b/examples/nuxt/main.ts @@ -0,0 +1,57 @@ +import { createServer } from 'node:http' + +import { createApp, createRouter, defineEventHandler, toNodeListener } from 'h3' +import type { ValidateDefinition } from 'permix' +import { createPermix } from 'permix/nuxt' + +type PermissionsDefinition = ValidateDefinition<{ + user: ['read', 'write'] +}> + +const permix = createPermix() + +// In Nuxt this is `server/middleware/permix.ts`. +const app = createApp() + +app.use( + defineEventHandler( + permix.setupMiddleware(() => ({ + user: { + read: true, + write: false, + }, + })) + ) +) + +// In Nuxt these are `server/api/*.ts` handlers. +const router = createRouter() + +router.get( + '/', + defineEventHandler({ + onRequest: [permix.checkMiddleware('user.read')], + handler: () => 'Hello World', + }) +) + +router.get( + '/write', + defineEventHandler({ + onRequest: [permix.checkMiddleware('user.write')], + handler: () => 'Hello World', + }) +) + +router.get( + '/permix', + defineEventHandler((event) => ({ + canRead: permix.getOrThrow(event).check('user.read'), + })) +) + +app.use(router) + +createServer(toNodeListener(app)).listen(3000, () => { + console.log('Server is running on port 3000') +}) diff --git a/examples/nuxt/package.json b/examples/nuxt/package.json new file mode 100644 index 00000000..d655f2bc --- /dev/null +++ b/examples/nuxt/package.json @@ -0,0 +1,17 @@ +{ + "name": "nuxt", + "private": true, + "type": "module", + "scripts": { + "check-types": "tsc --noEmit", + "start": "tsx main.ts" + }, + "dependencies": { + "h3": "^1.15.4", + "permix": "workspace:*" + }, + "devDependencies": { + "@types/node": "^25.9.1", + "tsx": "^4.22.4" + } +} diff --git a/examples/nuxt/tsconfig.json b/examples/nuxt/tsconfig.json new file mode 100644 index 00000000..48ce3c61 --- /dev/null +++ b/examples/nuxt/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "types": ["node"], + "esModuleInterop": true, + "skipLibCheck": true + } +} diff --git a/ignores.ts b/ignores.ts index 9e77439e..65349182 100644 --- a/ignores.ts +++ b/ignores.ts @@ -3,6 +3,8 @@ export const ignorePatterns = [ '**/.next/**', '**/.turbo/**', '**/.vercel/**', + '**/.agents/**', + '**/.claude/**', '**/dist/**', '**/build/**', '**/coverage/**', diff --git a/permix/package.json b/permix/package.json index dfbefd33..abb7214b 100644 --- a/permix/package.json +++ b/permix/package.json @@ -11,6 +11,7 @@ "frontend", "javascript", "nextjs", + "nuxt", "permissions", "permissions-management", "rbac", @@ -113,6 +114,10 @@ "types": "./dist/next/index.d.mts", "import": "./dist/next/index.mjs" }, + "./nuxt": { + "types": "./dist/nuxt/index.d.mts", + "import": "./dist/nuxt/index.mjs" + }, "./tanstack-start": { "types": "./dist/tanstack-start/index.d.mts", "import": "./dist/tanstack-start/index.mjs" @@ -146,6 +151,7 @@ "@vue/test-utils": "^2.4.10", "drizzle-orm": "1.0.0-rc.3", "effect": "^3.21.2", + "h3": "^1.15.11", "happy-dom": "^20.9.0", "react-dom": "^19.2.6", "supertest": "^7.2.2", diff --git a/permix/skills/permix/SKILL.md b/permix/skills/permix/SKILL.md index b3ebc1b5..035aba3f 100644 --- a/permix/skills/permix/SKILL.md +++ b/permix/skills/permix/SKILL.md @@ -1,7 +1,7 @@ --- name: permix description: >- - Applies Permix authorization once a schema exists: permix.check() paths and ReBAC callbacks, frontend bindings (permix/react, permix/vue, permix/solid, permix/svelte) with SSR dehydrate/hydrate for Next.js and TanStack Start, and server middleware (permix/express, hono, fastify, trpc, orpc, node, elysia). Use for anything past initial setup — checking permissions, gating UI, or protecting routes. For creating the schema and first `permix.setup()`, use permix-getting-started first. + Applies Permix authorization once a schema exists: permix.check() paths and ReBAC callbacks, frontend bindings (permix/react, permix/vue, permix/solid, permix/svelte) with SSR dehydrate/hydrate for Next.js, TanStack Start, and Nuxt, and server middleware (permix/express, hono, fastify, trpc, orpc, node, elysia). Use for anything past initial setup — checking permissions, gating UI, or protecting routes. For creating the schema and first `permix.setup()`, use permix-getting-started first. metadata: type: core library: permix @@ -19,6 +19,7 @@ sources: - 'letstri/permix:docs/content/docs/integrations/svelte.mdx' - 'letstri/permix:docs/content/docs/integrations/next.mdx' - 'letstri/permix:docs/content/docs/integrations/tanstack-start.mdx' + - 'letstri/permix:docs/content/docs/integrations/nuxt.mdx' - 'letstri/permix:docs/content/docs/integrations/express.mdx' - 'letstri/permix:docs/content/docs/integrations/hono.mdx' - 'letstri/permix:docs/content/docs/integrations/fastify.mdx' @@ -37,7 +38,7 @@ Assumes a `permix` instance already exists (see **permix-getting-started**). Loa | Task | Reference | | --- | --- | | `permix.check()` paths, callbacks, `~all`/`~any`, ReBAC/ABAC with entity data, `isReady` | [references/check.md](references/check.md) | -| React, Vue, Solid, or Svelte UI — `PermixProvider`, `usePermix`, `createComponents`, SSR `dehydrate`/`hydrate` for Next.js / TanStack Start | [references/frontend.md](references/frontend.md) | +| React, Vue, Solid, or Svelte UI — `PermixProvider`, `usePermix`, `createComponents`, SSR `dehydrate`/`hydrate` for Next.js / TanStack Start / Nuxt | [references/frontend.md](references/frontend.md) | | Protecting Express, Hono, Fastify, tRPC, oRPC, Node, or Elysia routes — `setupMiddleware`, `checkMiddleware` | [references/server.md](references/server.md) | ## Rules that apply everywhere diff --git a/permix/skills/permix/references/frontend.md b/permix/skills/permix/references/frontend.md index 510f619c..64c48da0 100644 --- a/permix/skills/permix/references/frontend.md +++ b/permix/skills/permix/references/frontend.md @@ -188,9 +188,9 @@ function App({ Run client `permix.setup(...)` where you restore the session (e.g. after `PermixHydrate` mounts or in the same auth effect). -### Next.js / TanStack Start +### Next.js / TanStack Start / Nuxt -Use framework helpers from `permix/next` or `permix/tanstack-start` when available — they wire dehydrate/hydrate into the framework data flow. +Use framework helpers from `permix/next` or `permix/tanstack-start` when available — they wire dehydrate/hydrate into the framework data flow. In Nuxt, `permix/nuxt` is server middleware (`setupMiddleware` / `checkMiddleware` on the h3 event); dehydrate with `permix.getOrThrow(event).dehydrate()` and hydrate the client with `PermixProvider` / `PermixHydrate` from `permix/vue`. In TanStack Start, `permix.get(context)` only works in server functions and server routes. To check inside `beforeLoad`/`loader`, put a core instance on the **router context** in `getRouter()` (`context: { permix }`), type it with `createRootRouteWithContext`, hydrate it in the root route's `beforeLoad`, then call `context.permix.check(...)` in any child route. Passing only the context type without the runtime value leaves `context.permix` undefined. @@ -200,6 +200,7 @@ Docs: - https://permix.letstri.dev/docs/integrations/next - https://permix.letstri.dev/docs/integrations/tanstack-start +- https://permix.letstri.dev/docs/integrations/nuxt ### Flow diagram @@ -225,5 +226,6 @@ For static-only permissions (all booleans), dehydrate + hydrate + `setup` with t - Solid: https://github.com/letstri/permix/tree/main/examples/solid - Svelte: https://github.com/letstri/permix/tree/main/examples/svelte - Next.js (SSR): https://github.com/letstri/permix/tree/main/examples/next +- Nuxt (SSR): https://github.com/letstri/permix/tree/main/examples/nuxt - Role templates: https://github.com/letstri/permix/tree/main/examples/role-based - ReBAC: https://github.com/letstri/permix/tree/main/examples/rebac diff --git a/permix/skills/permix/references/server.md b/permix/skills/permix/references/server.md index 8b8c79d5..fc3bf3d9 100644 --- a/permix/skills/permix/references/server.md +++ b/permix/skills/permix/references/server.md @@ -79,6 +79,7 @@ app.get('/posts/:id', (req, res) => { | Fastify | `permix/fastify` | | tRPC | `permix/trpc` | | oRPC | `permix/orpc` | +| Nuxt / Nitro (h3) | `permix/nuxt` | | Generic HTTP | `permix/node` or `permix/server` | | Elysia | `permix/elysia` | | Effect | `permix/effect` — see integration docs | diff --git a/permix/src/nuxt/index.ts b/permix/src/nuxt/index.ts new file mode 100644 index 00000000..60dafabb --- /dev/null +++ b/permix/src/nuxt/index.ts @@ -0,0 +1 @@ +export * from './permix' diff --git a/permix/src/nuxt/permix.test.ts b/permix/src/nuxt/permix.test.ts new file mode 100644 index 00000000..05affc32 --- /dev/null +++ b/permix/src/nuxt/permix.test.ts @@ -0,0 +1,300 @@ +import type { App } from 'h3' +import { + createApp, + createError, + createRouter, + defineEventHandler, + toWebHandler, +} from 'h3' +import { describe, expect, it } from 'vitest' + +import type { ValidateDefinition } from '../core' +import { PermixNotFoundError } from '../core' +import { createPermix } from './permix' + +interface Post { + id: string + authorId: string +} + +type PermissionsDefinition = ValidateDefinition<{ + post: ['create', 'read', 'update'] + user: ['delete'] +}> + +type PostWithData = ValidateDefinition<{ + post: [{ name: 'create'; type: Post }] +}> + +function request(app: App, path: string, init?: RequestInit) { + return toWebHandler(app)(new Request(`http://localhost${path}`, init)) +} + +describe(createPermix, () => { + const permix = createPermix() + + it('should throw ts error', () => { + // @ts-expect-error path does not exist + permix.checkMiddleware('post.delete') + }) + + it('should allow access when permission is granted', async () => { + const app = createApp() + + app.use( + defineEventHandler( + permix.setupMiddleware({ + post: { create: true, read: false, update: false }, + user: { delete: false }, + }) + ) + ) + + const router = createRouter() + router.post( + '/posts', + defineEventHandler({ + onRequest: [permix.checkMiddleware('post.create')], + handler: () => ({ success: true }), + }) + ) + app.use(router) + + const res = await request(app, '/posts', { method: 'POST' }) + expect(res.status).toBe(200) + await expect(res.json()).resolves.toStrictEqual({ success: true }) + }) + + it('should deny access when permission is not granted', async () => { + const app = createApp() + + app.use( + defineEventHandler( + permix.setupMiddleware(() => ({ + post: { create: false, read: false, update: false }, + user: { delete: false }, + })) + ) + ) + + const router = createRouter() + router.post( + '/posts', + defineEventHandler({ + onRequest: [permix.checkMiddleware('post.create')], + handler: () => ({ success: true }), + }) + ) + app.use(router) + + const res = await request(app, '/posts', { method: 'POST' }) + expect(res.status).toBe(403) + await expect(res.json()).resolves.toMatchObject({ + statusCode: 403, + data: { error: 'Forbidden' }, + }) + }) + + it('should work with custom error handler', async () => { + const permix = createPermix({ + onForbidden: ({ path }) => { + throw createError({ + statusCode: 403, + data: { error: `Custom error: ${path}` }, + }) + }, + }) + + const app = createApp() + + app.use( + defineEventHandler( + permix.setupMiddleware(() => ({ + post: { create: false, read: false, update: false }, + user: { delete: false }, + })) + ) + ) + app.use( + '/posts', + defineEventHandler({ + onRequest: [permix.checkMiddleware('post.create')], + handler: () => ({ success: true }), + }) + ) + + const res = await request(app, '/posts', { method: 'POST' }) + expect(res.status).toBe(403) + await expect(res.json()).resolves.toMatchObject({ + data: { error: 'Custom error: post.create' }, + }) + }) + + it('should pass data through to a rule callback', async () => { + const permix = createPermix() + const app = createApp() + + app.use( + defineEventHandler( + permix.setupMiddleware({ + post: { create: (post) => post?.authorId === '1' }, + }) + ) + ) + app.use( + '/allowed', + defineEventHandler({ + onRequest: [ + permix.checkMiddleware('post.create', { id: '1', authorId: '1' }), + ], + handler: () => ({ success: true }), + }) + ) + app.use( + '/denied', + defineEventHandler({ + onRequest: [ + permix.checkMiddleware('post.create', { id: '1', authorId: '2' }), + ], + handler: () => ({ success: true }), + }) + ) + + const allowed = await request(app, '/allowed') + const denied = await request(app, '/denied') + expect(allowed.status).toBe(200) + expect(denied.status).toBe(403) + }) + + it('should work with checker callback form', async () => { + const app = createApp() + + app.use( + defineEventHandler( + permix.setupMiddleware({ + post: { create: true, read: true, update: false }, + user: { delete: false }, + }) + ) + ) + app.use( + '/posts', + defineEventHandler({ + onRequest: [ + permix.checkMiddleware((c) => c('post.create') && c('post.read')), + ], + handler: () => ({ success: true }), + }) + ) + + const res = await request(app, '/posts') + expect(res.status).toBe(200) + }) + + it('should work with template', async () => { + const app = createApp() + const admin = permix.template({ + post: { create: true, read: true, update: true }, + user: { delete: true }, + }) + + app.use(defineEventHandler(permix.setupMiddleware(admin()))) + app.use( + '/posts', + defineEventHandler({ + onRequest: [permix.checkMiddleware('post.~all')], + handler: () => ({ success: true }), + }) + ) + + const res = await request(app, '/posts') + expect(res.status).toBe(200) + }) + + it('should isolate instances between concurrent requests', async () => { + const app = createApp() + + app.use( + defineEventHandler( + permix.setupMiddleware(({ event }) => ({ + post: { + create: (event.context as any).admin === true, + read: true, + update: false, + }, + user: { delete: false }, + })) + ) + ) + app.use( + '/posts', + defineEventHandler((event) => ({ + canCreate: permix.getOrThrow(event).check('post.create'), + })) + ) + + const admin = createApp() + admin.use( + defineEventHandler((event) => { + ;(event.context as any).admin = true + }) + ) + admin.use(app) + + const [adminRes, guestRes] = await Promise.all([ + request(admin, '/posts'), + request(app, '/posts'), + ]) + await expect(adminRes.json()).resolves.toStrictEqual({ canCreate: true }) + await expect(guestRes.json()).resolves.toStrictEqual({ canCreate: false }) + }) + + it('should throw PermixNotFoundError when setupMiddleware has not run', async () => { + const event = { context: {} } + + expect(permix.get(event)).toBeNull() + expect(permix.getRules(event)).toBeNull() + expect(() => permix.getOrThrow(event)).toThrow(PermixNotFoundError) + await expect(permix.checkMiddleware('post.read')(event)).rejects.toThrow( + PermixNotFoundError + ) + }) + + it('should let two factories coexist on the same event', async () => { + const a = createPermix() + const b = createPermix().contextKey('permix-b') + const event = { context: {} as Record } + + await a.setupMiddleware({ + post: { create: true, read: true, update: true }, + user: { delete: true }, + })(event) + await b.setupMiddleware({ + post: { create: false, read: false, update: false }, + user: { delete: false }, + })(event) + + expect(a.getOrThrow(event).check('post.create')).toBe(true) + expect(b.getOrThrow(event).check('post.create')).toBe(false) + expect(event.context['permix-b']).toBe(b.get(event)) + expect(a.key).toBeTypeOf('symbol') + expect(b.key).toBe('permix-b') + }) + + it('should fire factory-level check hooks', async () => { + const permix = createPermix() + const event = { context: {} } + const paths: unknown[] = [] + permix.hook('check', ({ path }) => { + paths.push(path) + }) + + await permix.setupMiddleware({ + post: { create: true, read: true, update: true }, + user: { delete: true }, + })(event) + await permix.checkMiddleware('post.read')(event) + + expect(paths).toStrictEqual(['post.read']) + }) +}) diff --git a/permix/src/nuxt/permix.ts b/permix/src/nuxt/permix.ts new file mode 100644 index 00000000..1ad1f4a3 --- /dev/null +++ b/permix/src/nuxt/permix.ts @@ -0,0 +1,180 @@ +import type { Permix as PermixCore } from '../core' +import { + createCheckContext, + createHooks, + createPermix as createPermixCore, + createTemplate, + PermixNotFoundError, +} from '../core' +import type { CheckArgs, CheckContext } from '../core/check' +import type { Definition } from '../core/definitions' +import type { PermixHooks, Rules, RulesPaths } from '../core/permix' +import type { MaybePromise } from '../utils' + +/** + * Minimal h3 / Nitro event shape. Compatible with `H3Event` from `h3` and the + * value returned by Nuxt's `useRequestEvent()`, so this entry has no runtime + * dependency on `h3`. + */ +export interface NuxtEvent { + context: object +} + +/** + * Plain h3 event handler. Wrap it with `defineEventHandler` (or pass it to + * `onRequest`) in Nuxt server middleware and routes. + */ +export type EventHandler = (event: NuxtEvent) => Promise + +export interface MiddlewareContext { + event: NuxtEvent +} + +export interface PermixOptions { + /** + * Called when a `checkMiddleware` denies the request. Defaults to throwing an + * error with `statusCode: 403`, which h3 turns into a 403 response. Throw + * `createError(...)` here to customise the response. + */ + onForbidden?: ( + params: CheckContext & MiddlewareContext + ) => MaybePromise +} + +function buildPermix( + resolveKey: () => string | symbol, + options: PermixOptions = {} +) { + const onForbidden = + options.onForbidden ?? + (() => { + // h3 reads statusCode/statusMessage/data off thrown errors. + throw Object.assign(new Error('Forbidden'), { + statusCode: 403, + statusMessage: 'Forbidden', + data: { error: 'Forbidden' }, + }) + }) + + const hooks = createHooks>() + + function get(event: NuxtEvent): PermixCore | null { + const instance = (event.context as any)[resolveKey()] as + | PermixCore + | undefined + return instance ?? null + } + + function getOrThrow(event: NuxtEvent): PermixCore { + const instance = get(event) + if (!instance) { + throw new PermixNotFoundError(resolveKey()) + } + return instance + } + + function setupMiddleware( + callbackOrRules: + | ((context: MiddlewareContext) => MaybePromise>) + | Rules + ): EventHandler { + return async (event) => { + const rules = + typeof callbackOrRules === 'function' + ? await callbackOrRules({ event }) + : callbackOrRules + const instance = createPermixCore(rules) + instance.hook('check', (context) => { + hooks.callHook('check', context) + }) + ;(event.context as any)[resolveKey()] = instance + } + } + + const checkMiddleware: (...args: CheckArgs) => EventHandler = + (...args) => + async (event) => { + const permix = getOrThrow(event) + + if (!permix.check(...args)) { + await onForbidden({ event, ...createCheckContext(...args) }) + } + } + + function getRules(event: NuxtEvent): Rules | null { + return get(event)?.getRules() ?? null + } + + function template(rules: Rules | ((param: T) => Rules)) { + return createTemplate(rules) + } + + return { + setupMiddleware, + checkMiddleware, + template, + get, + getOrThrow, + getRules, + hook: hooks.hook, + hookOnce: hooks.hookOnce, + get key() { + return resolveKey() + }, + $inferDefinition: undefined as unknown as D, + $inferPath: undefined as unknown as RulesPaths, + } +} + +/** + * Create a middleware factory that wires Permix into Nuxt / Nitro (h3) routes. + * + * The per-request instance lives on `event.context`, so server middleware, + * API routes, and SSR rendering of the same request share one instance while + * concurrent requests stay isolated. + * + * Use `.contextKey('name')` to set a custom context key (defaults to a unique + * `Symbol('permix')`). + * + * @example + * ```ts + * // server/utils/permix.ts + * import { createPermix } from 'permix/nuxt' + * + * export const permix = createPermix<{ + * post: ['create', 'read'] + * }>() + * + * // server/middleware/permix.ts + * export default defineEventHandler( + * permix.setupMiddleware(({ event }) => ({ + * post: { create: !!event.context.user, read: true }, + * })), + * ) + * + * // server/api/posts.post.ts + * export default defineEventHandler({ + * onRequest: [permix.checkMiddleware('post.create')], + * handler: () => ({ ok: true }), + * }) + * ``` + * + * @link https://permix.letstri.dev/docs/integrations/nuxt + */ +export function createPermix( + options: PermixOptions = {} +) { + let key: string | symbol = Symbol('permix') + const permix = buildPermix(() => key, options) + + return Object.assign(permix, { + contextKey(newKey: string | symbol) { + key = newKey + return permix + }, + }) +} + +export type NuxtPermix = ReturnType< + typeof createPermix +> diff --git a/permix/tsconfig.base.tsbuildinfo b/permix/tsconfig.base.tsbuildinfo index 2c6c0e5c..35f233e9 100644 --- a/permix/tsconfig.base.tsbuildinfo +++ b/permix/tsconfig.base.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/utils.test.ts","./src/utils.ts","./src/core/check.ts","./src/core/definitions.ts","./src/core/errors.ts","./src/core/hooks.test.ts","./src/core/hooks.ts","./src/core/index.ts","./src/core/merge.test.ts","./src/core/merge.ts","./src/core/permix.test.ts","./src/core/permix.ts","./src/core/rules.ts","./src/core/template.ts","./src/drizzle/errors.ts","./src/drizzle/index.ts","./src/drizzle/permix.test.ts","./src/drizzle/permix.ts","./src/drizzle/legacy/index.ts","./src/drizzle/legacy/permix.test.ts","./src/drizzle/legacy/permix.ts","./src/effect/index.ts","./src/effect/permix.test.ts","./src/effect/permix.ts","./src/elysia/index.ts","./src/elysia/permix.test.ts","./src/elysia/permix.ts","./src/express/index.ts","./src/express/permix.test.ts","./src/express/permix.ts","./src/fastify/index.ts","./src/fastify/permix.test.ts","./src/fastify/permix.ts","./src/hono/index.ts","./src/hono/permix.test.ts","./src/hono/permix.ts","./src/node/index.ts","./src/node/permix.test.ts","./src/node/permix.ts","./src/orpc/index.ts","./src/orpc/permix.test.ts","./src/orpc/permix.ts","./src/server/index.ts","./src/server/permix.test.ts","./src/server/permix.ts","./src/trpc/index.ts","./src/trpc/permix.test.ts","./src/trpc/permix.ts","./src/vue/components.test.ts","./src/vue/components.ts","./src/vue/composables.test.ts","./src/vue/composables.ts","./src/vue/context.ts","./src/vue/index.ts","./src/vue/provider.test.ts","./src/vue/test-utils.ts","./tsdown.config.ts","./vitest.config.ts"],"version":"6.0.3"} \ No newline at end of file +{"root":["./src/utils.test.ts","./src/utils.ts","./src/core/check.ts","./src/core/definitions.ts","./src/core/errors.ts","./src/core/hooks.test.ts","./src/core/hooks.ts","./src/core/index.ts","./src/core/merge.test.ts","./src/core/merge.ts","./src/core/permix.test.ts","./src/core/permix.ts","./src/core/rules.ts","./src/core/template.ts","./src/drizzle/errors.ts","./src/drizzle/index.ts","./src/drizzle/permix.test.ts","./src/drizzle/permix.ts","./src/drizzle/legacy/index.ts","./src/drizzle/legacy/permix.test.ts","./src/drizzle/legacy/permix.ts","./src/effect/index.ts","./src/effect/permix.test.ts","./src/effect/permix.ts","./src/elysia/index.ts","./src/elysia/permix.test.ts","./src/elysia/permix.ts","./src/express/index.ts","./src/express/permix.test.ts","./src/express/permix.ts","./src/fastify/index.ts","./src/fastify/permix.test.ts","./src/fastify/permix.ts","./src/hono/index.ts","./src/hono/permix.test.ts","./src/hono/permix.ts","./src/node/index.ts","./src/node/permix.test.ts","./src/node/permix.ts","./src/nuxt/index.ts","./src/nuxt/permix.test.ts","./src/nuxt/permix.ts","./src/orpc/index.ts","./src/orpc/permix.test.ts","./src/orpc/permix.ts","./src/server/index.ts","./src/server/permix.test.ts","./src/server/permix.ts","./src/trpc/index.ts","./src/trpc/permix.test.ts","./src/trpc/permix.ts","./src/vue/components.test.ts","./src/vue/components.ts","./src/vue/composables.test.ts","./src/vue/composables.ts","./src/vue/context.ts","./src/vue/index.ts","./src/vue/provider.test.ts","./src/vue/test-utils.ts","./tsdown.config.ts","./vitest.config.ts"],"version":"6.0.3"} \ No newline at end of file diff --git a/permix/tsdown.config.ts b/permix/tsdown.config.ts index 00e9a0a3..8fa2de2c 100644 --- a/permix/tsdown.config.ts +++ b/permix/tsdown.config.ts @@ -21,6 +21,7 @@ export default defineConfig({ './src/drizzle/index.ts', './src/drizzle/legacy/index.ts', './src/next/index.ts', + './src/nuxt/index.ts', './src/tanstack-start/index.ts', ], dts: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 761d130a..d410adbe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -297,6 +297,22 @@ importers: specifier: ^6.0.3 version: 6.0.3 + examples/nuxt: + dependencies: + h3: + specifier: ^1.15.4 + version: 1.15.11 + permix: + specifier: workspace:* + version: link:../../permix + devDependencies: + '@types/node': + specifier: ^25.9.1 + version: 25.9.1 + tsx: + specifier: ^4.22.4 + version: 4.22.4 + examples/react: dependencies: permix: @@ -435,7 +451,7 @@ importers: version: 1.167.1(@tanstack/react-router@1.170.32(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.27)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@tanstack/react-router-ssr-query': specifier: latest - version: 1.167.1(@tanstack/query-core@5.101.0)(@tanstack/react-query@5.101.0(react@19.2.6))(@tanstack/react-router@1.170.32(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.27)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 1.167.2(@tanstack/query-core@5.101.0)(@tanstack/react-query@5.101.0(react@19.2.6))(@tanstack/react-router@1.170.32(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.27)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@tanstack/react-start': specifier: latest version: 1.168.49(crossws@0.4.5(srvx@0.11.16))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(vite-plugin-solid@2.11.12(@testing-library/jest-dom@6.9.1)(solid-js@1.9.13)(vite@8.0.16(@types/node@22.19.20)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)))(vite@8.0.16(@types/node@22.19.20)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) @@ -548,7 +564,7 @@ importers: version: 4.12.23 next: specifier: '>=14' - version: 16.2.6(@babel/core@7.29.7)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + version: 16.2.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: specifier: '>=18' version: 19.2.6 @@ -607,6 +623,9 @@ importers: effect: specifier: ^3.21.2 version: 3.21.2 + h3: + specifier: ^1.15.11 + version: 1.15.11 happy-dom: specifier: ^20.9.0 version: 20.9.0 @@ -2634,12 +2653,12 @@ packages: '@tanstack/router-core': optional: true - '@tanstack/react-router-ssr-query@1.167.1': - resolution: {integrity: sha512-W9j5JPnBikyafvuUfykFfHIWod58OAbAAa5leNkXBcoDoocghMmu6w9uZOmUZvAWT7CSvgj5tBUtF7CM2OoHXQ==} + '@tanstack/react-router-ssr-query@1.167.2': + resolution: {integrity: sha512-yRvy0VJ00R8huPuquk+qyYQGMpYRc/G33mc6/yZ4f7LaBZz9h/VbACjmLzhm/+6u5WSmUt6ouJJZnI1/5Npfag==} engines: {node: '>=20.19'} peerDependencies: - '@tanstack/query-core': '>=5.90.0' - '@tanstack/react-query': '>=5.90.0' + '@tanstack/query-core': '>=5.102.0' + '@tanstack/react-query': '>=5.102.0' '@tanstack/react-router': '>=1.127.0' react: '>=18.0.0 || >=19.0.0' react-dom: '>=18.0.0 || >=19.0.0' @@ -2927,11 +2946,11 @@ packages: webpack: optional: true - '@tanstack/router-ssr-query-core@1.169.1': - resolution: {integrity: sha512-rngux8s/3mPQzcjLYDLkNU31coYVyCgrVTfpdwqUdY5jIEHqGTXrO73DTkPR1PppwYUeVhmNCgl8TctRcnupjg==} + '@tanstack/router-ssr-query-core@1.169.2': + resolution: {integrity: sha512-7pO65Aiq/1+aS3Mb6vSGtIjzQ/YGv9JTfBbn2EYlHcZnJ4s4ZGCeTPI847geB1RfJqVhfwxM8bZgGM51mYkolg==} engines: {node: '>=20.19'} peerDependencies: - '@tanstack/query-core': '>=5.90.0' + '@tanstack/query-core': '>=5.102.0' '@tanstack/router-core': '>=1.127.0' '@tanstack/router-utils@1.162.1': @@ -3811,6 +3830,9 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-es@1.2.3: + resolution: {integrity: sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==} + cookie-es@3.1.1: resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} @@ -3847,6 +3869,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + crossws@0.4.5: resolution: {integrity: sha512-wUR89x/Rw7/8t+vn0CmGDYM9TD6VtARGb0LD5jq2wjtMy1vCP4M+sm6N6TigWeTYvnA8MoW29NqqXD0ep0rfBA==} peerDependencies: @@ -4760,6 +4785,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + h3@1.15.11: + resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} + h3@2.0.1-rc.20: resolution: {integrity: sha512-28ljodXuUp0fZovdiSRq4G9OgrxCztrJe5VdYzXAB7ueRvI7pIUqLU14Xi3XqdYJ/khXjfpUOOD2EQa6CmBgsg==} engines: {node: '>=20.11.1'} @@ -4920,6 +4948,9 @@ packages: resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} engines: {node: '>= 10'} + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + is-alphabetical@2.0.1: resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} @@ -5557,6 +5588,9 @@ packages: node-fetch-native@1.6.7: resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-mock-http@1.0.5: + resolution: {integrity: sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==} + node-releases@2.0.46: resolution: {integrity: sha512-GYVXHE2KnrzAfsAjl4uP++evGFCrAU1jta4ubEjIG7YWt/64Gqv66a30yKwWczVjA6j3bM4nBwH7Pk1JmDHaxQ==} engines: {node: '>=18'} @@ -5846,6 +5880,9 @@ packages: resolution: {integrity: sha512-h36JMxKRqrAxVD8201FrCpyeNuUY9Y5zZwujr20fFO77tpUtGa6EZzfKw/3WaiBX95fq7+MpsuMLNdSnORAwSA==} engines: {node: '>=14.18.0'} + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -6507,6 +6544,9 @@ packages: unconfig@7.5.0: resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} @@ -8665,12 +8705,12 @@ snapshots: transitivePeerDependencies: - csstype - '@tanstack/react-router-ssr-query@1.167.1(@tanstack/query-core@5.101.0)(@tanstack/react-query@5.101.0(react@19.2.6))(@tanstack/react-router@1.170.32(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.27)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@tanstack/react-router-ssr-query@1.167.2(@tanstack/query-core@5.101.0)(@tanstack/react-query@5.101.0(react@19.2.6))(@tanstack/react-router@1.170.32(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.27)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@tanstack/query-core': 5.101.0 '@tanstack/react-query': 5.101.0(react@19.2.6) '@tanstack/react-router': 1.170.32(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@tanstack/router-ssr-query-core': 1.169.1(@tanstack/query-core@5.101.0)(@tanstack/router-core@1.171.27) + '@tanstack/router-ssr-query-core': 1.169.2(@tanstack/query-core@5.101.0)(@tanstack/router-core@1.171.27) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) transitivePeerDependencies: @@ -9062,7 +9102,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/router-ssr-query-core@1.169.1(@tanstack/query-core@5.101.0)(@tanstack/router-core@1.171.27)': + '@tanstack/router-ssr-query-core@1.169.2(@tanstack/query-core@5.101.0)(@tanstack/router-core@1.171.27)': dependencies: '@tanstack/query-core': 5.101.0 '@tanstack/router-core': 1.171.27 @@ -9673,7 +9713,7 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.8(@types/node@25.9.1)(@vitest/coverage-v8@4.1.8)(happy-dom@20.9.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + vitest: 4.1.8(@types/node@22.19.20)(@vitest/coverage-v8@4.1.8)(happy-dom@20.9.0)(jsdom@28.1.0(@noble/hashes@1.8.0))(vite@8.0.16(@types/node@22.19.20)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) '@vitest/expect@4.1.8': dependencies: @@ -10080,6 +10120,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie-es@1.2.3: {} + cookie-es@3.1.1: {} cookie-signature@1.2.2: {} @@ -10111,6 +10153,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + crossws@0.4.5(srvx@0.11.16): optionalDependencies: srvx: 0.11.16 @@ -10964,6 +11010,18 @@ snapshots: graceful-fs@4.2.11: {} + h3@1.15.11: + dependencies: + cookie-es: 1.2.3 + crossws: 0.3.5 + defu: 6.1.7 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.5 + radix3: 1.1.2 + ufo: 1.6.4 + uncrypto: 0.1.3 + h3@2.0.1-rc.20(crossws@0.4.5(srvx@0.11.16)): dependencies: rou3: 0.8.1 @@ -11186,6 +11244,8 @@ snapshots: ipaddr.js@2.4.0: {} + iron-webcrypto@1.2.1: {} + is-alphabetical@2.0.1: {} is-alphanumerical@2.0.1: @@ -11982,30 +12042,6 @@ snapshots: react: 19.2.6 react-dom: 19.2.6(react@19.2.6) - next@16.2.6(@babel/core@7.29.7)(react-dom@19.2.6(react@19.2.6))(react@19.2.6): - dependencies: - '@next/env': 16.2.6 - '@swc/helpers': 0.5.15 - baseline-browser-mapping: 2.10.33 - caniuse-lite: 1.0.30001793 - postcss: 8.4.31 - react: 19.2.6 - react-dom: 19.2.6(react@19.2.6) - styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.6) - optionalDependencies: - '@next/swc-darwin-arm64': 16.2.6 - '@next/swc-darwin-x64': 16.2.6 - '@next/swc-linux-arm64-gnu': 16.2.6 - '@next/swc-linux-arm64-musl': 16.2.6 - '@next/swc-linux-x64-gnu': 16.2.6 - '@next/swc-linux-x64-musl': 16.2.6 - '@next/swc-win32-arm64-msvc': 16.2.6 - '@next/swc-win32-x64-msvc': 16.2.6 - sharp: 0.34.5 - transitivePeerDependencies: - - '@babel/core' - - babel-plugin-macros - next@16.2.6(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@next/env': 16.2.6 @@ -12039,7 +12075,7 @@ snapshots: postcss: 8.4.31 react: 19.2.6 react-dom: 19.2.6(react@19.2.6) - styled-jsx: 5.1.6(@babel/core@7.29.7)(react@19.2.6) + styled-jsx: 5.1.6(react@19.2.6) optionalDependencies: '@next/swc-darwin-arm64': 16.2.6 '@next/swc-darwin-x64': 16.2.6 @@ -12053,7 +12089,6 @@ snapshots: transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - optional: true nf3@0.3.17: {} @@ -12109,6 +12144,8 @@ snapshots: node-fetch-native@1.6.7: {} + node-mock-http@1.0.5: {} + node-releases@2.0.46: {} nopt@7.2.1: @@ -12438,6 +12475,8 @@ snapshots: radash@12.1.1: {} + radix3@1.1.2: {} + range-parser@1.2.1: {} raw-body@3.0.2: @@ -12946,17 +12985,15 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - styled-jsx@5.1.6(@babel/core@7.29.7)(react@19.2.6): + styled-jsx@5.1.6(react@19.2.4): dependencies: client-only: 0.0.1 - react: 19.2.6 - optionalDependencies: - '@babel/core': 7.29.7 + react: 19.2.4 - styled-jsx@5.1.6(react@19.2.4): + styled-jsx@5.1.6(react@19.2.6): dependencies: client-only: 0.0.1 - react: 19.2.4 + react: 19.2.6 stylis@4.4.0: {} @@ -13216,6 +13253,8 @@ snapshots: quansync: 1.0.0 unconfig-core: 7.5.0 + uncrypto@0.1.3: {} + undici-types@6.21.0: {} undici-types@7.16.0: {}