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: {}