From f4d6a8349c73ef8038273d55f50e616a664d0acc Mon Sep 17 00:00:00 2001 From: Martijn van Dijk Date: Fri, 28 Aug 2026 14:45:08 +0200 Subject: [PATCH 1/2] feat: add NestJS adapter with guard and Check decorator Wire Permix into Nest via APP_GUARD so each request gets its own instance, then enforce @Check on handlers without tying the API to Express middleware. Closes #11. Co-authored-by: Cursor --- _artifacts/domain_map.yaml | 6 +- _artifacts/skill_tree.yaml | 4 +- docs/content/docs/comparison.mdx | 1 + docs/content/docs/integrations/nest.mdx | 261 +++++++++ docs/content/docs/meta.json | 1 + examples/nest/main.ts | 67 +++ examples/nest/package.json | 21 + examples/nest/tsconfig.json | 12 + oxlint.config.ts | 7 + permix/package.json | 19 + permix/skills/README.md | 2 +- permix/skills/permix/SKILL.md | 5 +- permix/skills/permix/references/server.md | 31 +- permix/src/nest/index.ts | 1 + permix/src/nest/permix.test.ts | 643 ++++++++++++++++++++++ permix/src/nest/permix.ts | 214 +++++++ permix/tsconfig.base.json | 3 +- permix/tsdown.config.ts | 1 + pnpm-lock.yaml | 324 ++++++++++- 19 files changed, 1601 insertions(+), 22 deletions(-) create mode 100644 docs/content/docs/integrations/nest.mdx create mode 100644 examples/nest/main.ts create mode 100644 examples/nest/package.json create mode 100644 examples/nest/tsconfig.json create mode 100644 permix/src/nest/index.ts create mode 100644 permix/src/nest/permix.test.ts create mode 100644 permix/src/nest/permix.ts diff --git a/_artifacts/domain_map.yaml b/_artifacts/domain_map.yaml index 318aa7c9..8cfb9fd0 100644 --- a/_artifacts/domain_map.yaml +++ b/_artifacts/domain_map.yaml @@ -34,7 +34,7 @@ domains: slug: server description: > Per-request setupMiddleware and checkMiddleware for Express, Hono, Fastify, - tRPC, oRPC, Node, and Elysia. + NestJS, tRPC, oRPC, Node, and Elysia. - name: 'SSR and hydration' slug: ssr @@ -78,7 +78,7 @@ skills: ~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 - checkMiddleware for Express, Hono, Fastify, tRPC, oRPC, Node, Elysia. + checkMiddleware for Express, Hono, Fastify, NestJS, tRPC, oRPC, Node, Elysia. Single skill with a thin SKILL.md router and three reference files loaded on demand. type: core @@ -94,6 +94,7 @@ skills: - express - hono - fastify + - nest - trpc - orpc - node @@ -140,6 +141,7 @@ skills: - 'letstri/permix:docs/content/docs/integrations/express.mdx' - 'letstri/permix:docs/content/docs/integrations/hono.mdx' - 'letstri/permix:docs/content/docs/integrations/fastify.mdx' + - 'letstri/permix:docs/content/docs/integrations/nest.mdx' - 'letstri/permix:docs/content/docs/integrations/trpc.mdx' - 'letstri/permix:docs/content/docs/integrations/orpc.mdx' - 'letstri/permix:docs/content/docs/integrations/node.mdx' diff --git a/_artifacts/skill_tree.yaml b/_artifacts/skill_tree.yaml index 70b42866..011bfb17 100644 --- a/_artifacts/skill_tree.yaml +++ b/_artifacts/skill_tree.yaml @@ -40,7 +40,7 @@ skills: isReady/isReadyAsync; PermixProvider/usePermix/createComponents and SSR dehydrate/hydrate for React, Vue, Solid, Svelte, Next.js, TanStack Start; setupMiddleware/checkMiddleware for Express, Hono, Fastify, - tRPC, oRPC, Node, Elysia. Thin router with references loaded on demand. + NestJS, tRPC, oRPC, Node, Elysia. Thin router with references loaded on demand. requires: - permix-getting-started subsystems: @@ -53,6 +53,7 @@ skills: - express - hono - fastify + - nest - trpc - orpc - node @@ -75,6 +76,7 @@ skills: - 'letstri/permix:docs/content/docs/integrations/express.mdx' - 'letstri/permix:docs/content/docs/integrations/hono.mdx' - 'letstri/permix:docs/content/docs/integrations/fastify.mdx' + - 'letstri/permix:docs/content/docs/integrations/nest.mdx' - 'letstri/permix:docs/content/docs/integrations/trpc.mdx' - 'letstri/permix:docs/content/docs/integrations/orpc.mdx' - 'letstri/permix:docs/content/docs/integrations/node.mdx' diff --git a/docs/content/docs/comparison.mdx b/docs/content/docs/comparison.mdx index d0d9ea67..64899ff7 100644 --- a/docs/content/docs/comparison.mdx +++ b/docs/content/docs/comparison.mdx @@ -46,6 +46,7 @@ Built with `pnpm run build` in the `permix` package (`tsdown` for all entries ex | `permix/hono` | 0.88 kB | adapter | | `permix/fastify` | 1.04 kB | adapter | | `permix/elysia` | 0.88 kB | adapter | +| `permix/nest` | 1.39 kB | adapter | | `permix/trpc` | 0.96 kB | adapter | | `permix/orpc` | 0.92 kB | adapter | | `permix/effect` | 1.28 kB | adapter | diff --git a/docs/content/docs/integrations/nest.mdx b/docs/content/docs/integrations/nest.mdx new file mode 100644 index 00000000..4a1ebe3a --- /dev/null +++ b/docs/content/docs/integrations/nest.mdx @@ -0,0 +1,261 @@ +--- +title: NestJS +description: Learn how to use Permix with NestJS +--- + +## Overview + +Permix provides a NestJS integration that sets up permissions per request and enforces them with a guard plus a `@Check` decorator. The factory is created using `createPermix` from `permix/nest`. + + + Before getting started with the NestJS integration, make sure you've completed + the initial setup steps in the [Quick Start](/docs/quick-start) guide. + + + + + + +## Setup + +Create a Permix factory and register its guard as a global `APP_GUARD`. The guard always attaches a per-request instance; it only enforces a permission when `@Check` is present. + +```ts +import { Module } from '@nestjs/common' +import { APP_GUARD } from '@nestjs/core' +import { createPermix } from 'permix/nest' + +interface Post { + id: string + authorId: string + title: string + content: string +} + +export const permix = createPermix<{ + post: [ + { name: 'create'; type: Post }, + { name: 'read'; type: Post }, + { name: 'update'; type: Post }, + ] +}>() + +@Module({ + providers: [ + { + provide: APP_GUARD, + useValue: permix.guard(({ req }) => { + // You can access req.user or other properties to determine permissions + return { + post: { + create: true, + read: true, + update: false, + }, + } + }), + }, + ], +}) +export class AppModule {} +``` + + + The guard works with both the Express and Fastify Nest HTTP adapters. It + preserves full type safety from your Permix definition. + + + + + + +## Checking Permissions + +Use the `Check` decorator on a handler or controller: + +```ts +import { Controller, Delete, Get, Post, Put } from '@nestjs/common' +import { permix } from './permix' + +@Controller('posts') +export class PostsController { + @Post() + @permix.Check('post.create') + create() { + return { success: true } + } + + @Put(':id') + @permix.Check((c) => c('post.read') && c('post.update')) + update() { + return { success: true } + } + + @Delete(':id') + @permix.Check('post.~all') + remove() { + return { success: true } + } + + @Get() + @permix.Check('post.~any') + findAll() { + return { posts: getAllPosts() } + } +} +``` + + + + + +## Accessing Permix Directly + +You can access the Permix instance directly in your route handlers using the `get` function: + +```ts +@Get() +findAll(@Req() req: Request) { + const { check } = permix.getOrThrow(req) + + if (check('post.read')) { + return { posts: getAllPosts() } + } + + throw new ForbiddenException({ + error: 'You do not have permission to read posts', + }) +} +``` + +Entity-based (ReBAC) checks usually run in the handler after the resource is loaded: + +```ts +@Put(':id') +async update(@Param('id') id: string, @Req() req: Request) { + const post = await getPostById(id) + const { check } = permix.getOrThrow(req) + + if (!check('post.update', post)) { + throw new ForbiddenException({ error: 'You cannot update this post' }) + } + + return { success: true } +} +``` + + + + + +## Using Templates + +Permix provides a template helper to create reusable permission rule sets: + +```ts +const adminTemplate = permix.template({ + post: { + create: true, + read: true, + update: true, + }, +}) + +{ + provide: APP_GUARD, + useValue: permix.guard(({ req }) => { + if (req.user?.role === 'admin') { + return adminTemplate() + } + + return { + post: { + create: false, + read: true, + update: false, + }, + } + }), +} +``` + + + + + +## Custom Error Handling + +By default, a denied `@Check` throws a Nest `ForbiddenException` with `{ error: 'Forbidden' }`. You can customize this by providing an `onForbidden` handler: + +### Basic Error Handler + +```ts +const permix = createPermix({ + onForbidden: () => { + throw new ForbiddenException({ + error: 'Custom forbidden message', + }) + }, +}) +``` + +### Dynamic Error Handler + +You can also throw different responses based on the checked path: + +```ts +const permix = createPermix({ + onForbidden: ({ path }) => { + if (path === 'post.create') { + throw new ForbiddenException({ + error: `You don't have permission for ${path}`, + }) + } + + throw new ForbiddenException({ + error: 'You do not have permission to perform this action', + }) + }, +}) +``` + +The `onForbidden` handler receives: + +- `req`: The HTTP request object (Express or Fastify) +- `context`: Nest `ExecutionContext` +- `path`: The permission path that was checked (or `null` for callback checks) +- `data`: Optional entity data passed to the check + +## Advanced Usage + +### Async Permission Rules + +You can use async functions in your guard setup: + +```ts +permix.guard(async ({ req }) => { + const userPermissions = await getUserPermissions(req.user.id) + + return { + post: { + create: userPermissions.canCreatePosts, + read: userPermissions.canReadPosts, + update: userPermissions.canUpdatePosts, + }, + } +}) +``` + +### Hooks + +You can register hooks at the factory level to listen for events across all requests: + +```ts +permix.hook('check', ({ path, data }) => { + console.log(`Permission checked: ${path}`, data) +}) +``` + +### Example + +You can find the example of the NestJS integration [here](https://github.com/letstri/permix/tree/main/examples/nest). diff --git a/docs/content/docs/meta.json b/docs/content/docs/meta.json index dbd79bfc..36552fe8 100644 --- a/docs/content/docs/meta.json +++ b/docs/content/docs/meta.json @@ -29,6 +29,7 @@ "integrations/hono", "integrations/elysia", "integrations/fastify", + "integrations/nest", "integrations/effect", "integrations/drizzle", "---", diff --git a/examples/nest/main.ts b/examples/nest/main.ts new file mode 100644 index 00000000..291b4b7a --- /dev/null +++ b/examples/nest/main.ts @@ -0,0 +1,67 @@ +import 'reflect-metadata' +import { + Controller, + ForbiddenException, + Get, + Module, + Req, +} from '@nestjs/common' +import { APP_GUARD, NestFactory } from '@nestjs/core' +import type { ValidateDefinition } from 'permix' +import { createPermix } from 'permix/nest' + +type PermissionsDefinition = ValidateDefinition<{ + user: ['read', 'write'] +}> + +const permix = createPermix({ + onForbidden: () => { + throw new ForbiddenException({ + error: 'You do not have permission to access this resource', + }) + }, +}) + +@Controller() +class AppController { + @Get() + @permix.Check('user.read') + read() { + return 'Hello World' + } + + @Get('write') + @permix.Check('user.write') + write() { + return 'Hello World' + } + + @Get('permix') + inspect(@Req() req: { [key: PropertyKey]: unknown }) { + return { canRead: permix.getOrThrow(req).check('user.read') } + } +} + +@Module({ + controllers: [AppController], + providers: [ + { + provide: APP_GUARD, + useValue: permix.guard(() => ({ + user: { + read: true, + write: false, + }, + })), + }, + ], +}) +class AppModule {} + +async function bootstrap() { + const app = await NestFactory.create(AppModule) + await app.listen(3000) + console.log('Server is running on port 3000') +} + +bootstrap() diff --git a/examples/nest/package.json b/examples/nest/package.json new file mode 100644 index 00000000..425acf92 --- /dev/null +++ b/examples/nest/package.json @@ -0,0 +1,21 @@ +{ + "name": "nest", + "private": true, + "type": "module", + "scripts": { + "check-types": "tsc --noEmit", + "start": "tsx main.ts" + }, + "dependencies": { + "@nestjs/common": "^11.1.6", + "@nestjs/core": "^11.1.6", + "@nestjs/platform-express": "^11.1.6", + "permix": "workspace:*", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2" + }, + "devDependencies": { + "tsx": "^4.22.4", + "typescript": "^6.0.3" + } +} diff --git a/examples/nest/tsconfig.json b/examples/nest/tsconfig.json new file mode 100644 index 00000000..c123cb28 --- /dev/null +++ b/examples/nest/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true + } +} diff --git a/oxlint.config.ts b/oxlint.config.ts index e29a4dc3..5b3df5ee 100644 --- a/oxlint.config.ts +++ b/oxlint.config.ts @@ -125,5 +125,12 @@ export default defineConfig({ 'react/jsx-key': 'off', }, }, + { + files: ['permix/src/nest/**', 'examples/nest/**'], + rules: { + 'class-methods-use-this': 'off', + 'typescript/no-extraneous-class': 'off', + }, + }, ], }) diff --git a/permix/package.json b/permix/package.json index dfbefd33..f2f1958f 100644 --- a/permix/package.json +++ b/permix/package.json @@ -10,6 +10,7 @@ "authorization", "frontend", "javascript", + "nestjs", "nextjs", "permissions", "permissions-management", @@ -116,6 +117,10 @@ "./tanstack-start": { "types": "./dist/tanstack-start/index.d.mts", "import": "./dist/tanstack-start/index.mjs" + }, + "./nest": { + "types": "./dist/nest/index.d.mts", + "import": "./dist/nest/index.mjs" } }, "scripts": { @@ -129,6 +134,10 @@ "test": "vitest run" }, "devDependencies": { + "@nestjs/common": "^11.2.3", + "@nestjs/core": "^11.2.3", + "@nestjs/platform-express": "^11.2.3", + "@nestjs/testing": "^11.2.3", "@solidjs/testing-library": "^0.8.10", "@sveltejs/package": "^2.5.7", "@sveltejs/vite-plugin-svelte": "^7.1.2", @@ -148,6 +157,8 @@ "effect": "^3.21.2", "happy-dom": "^20.9.0", "react-dom": "^19.2.6", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2", "supertest": "^7.2.2", "svelte": "^5.56.0", "svelte-check": "^4.5.0", @@ -159,6 +170,8 @@ "zod": "^4.4.3" }, "peerDependencies": { + "@nestjs/common": ">=10", + "@nestjs/core": ">=10", "@orpc/server": ">=1", "@tanstack/react-start": ">=1", "@trpc/server": ">=11", @@ -177,6 +190,12 @@ "vue": ">=3" }, "peerDependenciesMeta": { + "@nestjs/common": { + "optional": true + }, + "@nestjs/core": { + "optional": true + }, "@orpc/server": { "optional": true }, diff --git a/permix/skills/README.md b/permix/skills/README.md index f2616977..b78dbd84 100644 --- a/permix/skills/README.md +++ b/permix/skills/README.md @@ -42,7 +42,7 @@ Restart Cursor or start a new agent chat so skills are picked up. | Skill | Intent id | When to use | | --- | --- | --- | | [permix-getting-started](./permix-getting-started/SKILL.md) | `permix#permix-getting-started` | New project, schema, `setup`, roles/templates | -| [permix](./permix/SKILL.md) | `permix#permix` | Everything past setup: `check`/ReBAC (`references/check.md`), React/Vue/Solid/Svelte + SSR (`references/frontend.md`), Express/Hono/Fastify/tRPC/oRPC middleware (`references/server.md`) | +| [permix](./permix/SKILL.md) | `permix#permix` | Everything past setup: `check`/ReBAC (`references/check.md`), React/Vue/Solid/Svelte + SSR (`references/frontend.md`), Express/Hono/Fastify/NestJS/tRPC/oRPC middleware (`references/server.md`) | ## Registry and version history diff --git a/permix/skills/permix/SKILL.md b/permix/skills/permix/SKILL.md index b3ebc1b5..45f2b62a 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 and TanStack Start, and server middleware (permix/express, hono, fastify, nest, 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 @@ -22,6 +22,7 @@ sources: - 'letstri/permix:docs/content/docs/integrations/express.mdx' - 'letstri/permix:docs/content/docs/integrations/hono.mdx' - 'letstri/permix:docs/content/docs/integrations/fastify.mdx' + - 'letstri/permix:docs/content/docs/integrations/nest.mdx' - 'letstri/permix:docs/content/docs/integrations/trpc.mdx' - 'letstri/permix:docs/content/docs/integrations/orpc.mdx' - 'letstri/permix:docs/content/docs/integrations/node.mdx' @@ -38,7 +39,7 @@ Assumes a `permix` instance already exists (see **permix-getting-started**). Loa | --- | --- | | `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) | -| Protecting Express, Hono, Fastify, tRPC, oRPC, Node, or Elysia routes — `setupMiddleware`, `checkMiddleware` | [references/server.md](references/server.md) | +| Protecting Express, Hono, Fastify, NestJS, tRPC, oRPC, Node, or Elysia routes — `setupMiddleware`, `checkMiddleware`, or Nest `guard` / `@Check` | [references/server.md](references/server.md) | ## Rules that apply everywhere diff --git a/permix/skills/permix/references/server.md b/permix/skills/permix/references/server.md index 8b8c79d5..0306544f 100644 --- a/permix/skills/permix/references/server.md +++ b/permix/skills/permix/references/server.md @@ -4,7 +4,7 @@ Authorization must run on the server. Client checks are UX only. Docs: https://permix.letstri.dev/docs/integrations/express -## Pattern (Express-style; similar for Hono, Fastify, Node) +## Pattern (Express-style; similar for Hono, Fastify, Node, Nest) Import from the framework subpath, not bare `permix`: @@ -59,6 +59,32 @@ app.delete( Denied requests default to `403` with `{ error: 'Forbidden' }`. Customize with `onForbidden` in `createPermix` options. +### NestJS (`permix/nest`) + +Use a global `APP_GUARD` plus `@Check`. The guard always sets up the per-request instance and only enforces a path when the decorator is present: + +```ts +import { APP_GUARD } from '@nestjs/core' +import { createPermix } from 'permix/nest' + +const permix = createPermix<{ + post: ['create', 'read'] +}>() + +{ + provide: APP_GUARD, + useValue: permix.guard(({ req }) => ({ + post: { create: !!req.user, read: true }, + })), +} + +@Get() +@permix.Check('post.read') +findAll() {} +``` + +Entity checks run in the handler after the resource is loaded: `permix.getOrThrow(req).check('post.update', post)`. + ### Access instance in handlers ```ts @@ -77,6 +103,7 @@ app.get('/posts/:id', (req, res) => { | Express | `permix/express` | | Hono | `permix/hono` | | Fastify | `permix/fastify` | +| NestJS | `permix/nest` | | tRPC | `permix/trpc` | | oRPC | `permix/orpc` | | Generic HTTP | `permix/node` or `permix/server` | @@ -101,7 +128,7 @@ app.use(permix.setupMiddleware(rules)) ## Checklist -- [ ] `setupMiddleware` runs **before** `checkMiddleware` on protected routes +- [ ] `setupMiddleware` runs **before** `checkMiddleware` on protected routes (Nest: register `permix.guard(...)` as `APP_GUARD` before `@Check`) - [ ] Rules derived from authenticated `req.user` (or RPC context), not client headers alone - [ ] Entity checks pass resource data when the action has `type` / `required: true` - [ ] Same paths as frontend (`post.update`, not ad-hoc strings) diff --git a/permix/src/nest/index.ts b/permix/src/nest/index.ts new file mode 100644 index 00000000..60dafabb --- /dev/null +++ b/permix/src/nest/index.ts @@ -0,0 +1 @@ +export * from './permix' diff --git a/permix/src/nest/permix.test.ts b/permix/src/nest/permix.test.ts new file mode 100644 index 00000000..27fa3992 --- /dev/null +++ b/permix/src/nest/permix.test.ts @@ -0,0 +1,643 @@ +import 'reflect-metadata' +import type { INestApplication, Type } from '@nestjs/common' +import { + Controller, + ForbiddenException, + Get, + Module, + Post, + Req, +} from '@nestjs/common' +import { APP_GUARD } from '@nestjs/core' +import { Test } from '@nestjs/testing' +import request from 'supertest' +import { afterEach, describe, expect, it } from 'vitest' + +import type { ValidateDefinition } from '../core' +import { createPermix } from './permix' + +interface PostEntity { + id: string + authorId: string +} + +type PermissionsDefinition = ValidateDefinition<{ + post: ['create', 'read', 'update'] + user: ['delete'] +}> + +type PostWithData = ValidateDefinition<{ + post: [{ name: 'create'; type: PostEntity }] +}> + +const denied = { + post: { create: false, read: false, update: false }, + user: { delete: false }, +} + +describe('permix/nest', () => { + let app: INestApplication | undefined + + afterEach(async () => { + if (app) { + await app.close() + app = undefined + } + }) + + async function createApp(module: Type): Promise { + const moduleRef = await Test.createTestingModule({ + imports: [module], + }).compile() + app = moduleRef.createNestApplication({ logger: false }) + await app.init() + return app + } + + describe(createPermix, () => { + const permix = createPermix() + + it('should throw ts error', () => { + // @ts-expect-error path does not exist + permix.Check('post.delete') + }) + + it('should allow access when permission is granted', async () => { + @Controller() + class PostsController { + @Post('posts') + @permix.Check('post.create') + create() { + return { success: true } + } + } + + @Module({ + controllers: [PostsController], + providers: [ + { + provide: APP_GUARD, + useValue: permix.guard({ + post: { create: true, read: false, update: false }, + user: { delete: false }, + }), + }, + ], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()) + .post('/posts') + .send({ title: 'Test Post' }) + + expect(response.status).toBe(201) + expect(response.body).toStrictEqual({ success: true }) + }) + + it('should deny access when permission is not granted', async () => { + @Controller() + class PostsController { + @Post('posts') + @permix.Check('post.create') + create() { + return { success: true } + } + } + + @Module({ + controllers: [PostsController], + providers: [{ provide: APP_GUARD, useValue: permix.guard(denied) }], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()) + .post('/posts') + .send({ title: 'Test Post' }) + + expect(response.status).toBe(403) + expect(response.body).toStrictEqual({ error: 'Forbidden' }) + }) + + it('should work with custom error handler', async () => { + const permix = createPermix({ + onForbidden: () => { + throw new ForbiddenException({ error: 'Custom error' }) + }, + }) + + @Controller() + class PostsController { + @Post('posts') + @permix.Check('post.create') + create() { + return { success: true } + } + } + + @Module({ + controllers: [PostsController], + providers: [{ provide: APP_GUARD, useValue: permix.guard(denied) }], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()) + .post('/posts') + .send({ title: 'Test Post' }) + + expect(response.status).toBe(403) + expect(response.body).toStrictEqual({ error: 'Custom error' }) + }) + + it('should work with custom error and params', async () => { + const permix = createPermix({ + onForbidden: ({ path }) => { + throw new ForbiddenException({ + error: `You do not have permission for ${path}`, + }) + }, + }) + + @Controller() + class PostsController { + @Post('posts') + @permix.Check('post.create') + create() { + return { success: true } + } + } + + @Module({ + controllers: [PostsController], + providers: [{ provide: APP_GUARD, useValue: permix.guard(denied) }], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()) + .post('/posts') + .send({ title: 'Test Post' }) + + expect(response.status).toBe(403) + expect(response.body).toStrictEqual({ + error: 'You do not have permission for post.create', + }) + }) + + it('should pass data through to a rule callback', async () => { + const permix = createPermix() + + @Controller() + class PostsController { + @Post('posts') + @permix.Check('post.create', { id: 'a', authorId: '1' }) + create() { + return { success: true } + } + } + + @Module({ + controllers: [PostsController], + providers: [ + { + provide: APP_GUARD, + useValue: permix.guard({ + post: { + create: (post) => post?.authorId === '1', + }, + }), + }, + ], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()) + .post('/posts') + .send({ title: 'Test Post' }) + + expect(response.status).toBe(201) + expect(response.body).toStrictEqual({ success: true }) + }) + + it('should work with checker callback form', async () => { + const permix = createPermix() + + @Controller() + class PostsController { + @Post('posts') + @permix.Check((c) => c('post.create') && c('user.delete')) + create() { + return { success: true } + } + } + + @Module({ + controllers: [PostsController], + providers: [ + { + provide: APP_GUARD, + useValue: permix.guard({ + post: { create: true, read: true, update: false }, + user: { delete: true }, + }), + }, + ], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()) + .post('/posts') + .send({ title: 'Test Post' }) + + expect(response.status).toBe(201) + expect(response.body).toStrictEqual({ success: true }) + }) + + it('should work with template', async () => { + const template = permix.template({ + post: { create: true, read: true, update: true }, + user: { delete: true }, + }) + + @Controller() + class PostsController { + @Post('posts') + @permix.Check('post.create') + create() { + return { success: true } + } + } + + @Module({ + controllers: [PostsController], + providers: [ + { provide: APP_GUARD, useValue: permix.guard(() => template()) }, + ], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()) + .post('/posts') + .send({ title: 'Test Post' }) + + expect(response.status).toBe(201) + expect(response.body).toStrictEqual({ success: true }) + }) + + it('should dehydrate permissions', async () => { + const template = permix.template({ + post: { create: true, read: false, update: true }, + user: { delete: false }, + }) + + @Controller() + class DehydrateController { + @Get('dehydrate') + dehydrate(@Req() req: { [key: PropertyKey]: unknown }) { + return permix.getOrThrow(req).dehydrate() + } + } + + @Module({ + controllers: [DehydrateController], + providers: [ + { provide: APP_GUARD, useValue: permix.guard(() => template()) }, + ], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()).get('/dehydrate') + + expect(response.status).toBe(200) + expect(response.body).toStrictEqual({ + post: { create: true, read: false, update: true }, + user: { delete: false }, + }) + }) + + it('should allow a handler without Check after setup', async () => { + @Controller() + class OpenController { + @Get('open') + open() { + return { ok: true } + } + } + + @Module({ + controllers: [OpenController], + providers: [{ provide: APP_GUARD, useValue: permix.guard(denied) }], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()).get('/open') + expect(response.status).toBe(200) + expect(response.body).toStrictEqual({ ok: true }) + }) + + it('should let two factories with different keys coexist on the same request', async () => { + const admin = createPermix().contextKey('admin') + const guest = createPermix().contextKey('guest') + + @Controller() + class DualController { + @Post('admin') + @admin.Check('post.create') + adminRoute() { + return { scope: 'admin' } + } + + @Post('guest') + @guest.Check('post.create') + guestRoute() { + return { scope: 'guest' } + } + } + + @Module({ + controllers: [DualController], + providers: [ + { + provide: APP_GUARD, + useValue: admin.guard({ + post: { create: true, read: true, update: true }, + user: { delete: true }, + }), + }, + { + provide: APP_GUARD, + useValue: guest.guard({ + post: { create: false, read: true, update: false }, + user: { delete: false }, + }), + }, + ], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + + const adminResponse = await request(nestApp.getHttpServer()).post( + '/admin' + ) + expect(adminResponse.status).toBe(201) + expect(adminResponse.body).toStrictEqual({ scope: 'admin' }) + + const guestResponse = await request(nestApp.getHttpServer()).post( + '/guest' + ) + expect(guestResponse.status).toBe(403) + expect(guestResponse.body).toStrictEqual({ error: 'Forbidden' }) + }) + + it('should default to a per-instance symbol so two factories without a key do not collide', async () => { + const first = createPermix() + const second = createPermix() + + @Controller() + class DualController { + @Post('first') + @first.Check('post.create') + firstRoute() { + return { ok: true } + } + + @Post('second') + @second.Check('post.create') + secondRoute() { + return { ok: true } + } + } + + @Module({ + controllers: [DualController], + providers: [ + { + provide: APP_GUARD, + useValue: first.guard({ + post: { create: true, read: true, update: true }, + user: { delete: true }, + }), + }, + { + provide: APP_GUARD, + useValue: second.guard({ + post: { create: false, read: false, update: false }, + user: { delete: false }, + }), + }, + ], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + + const firstResponse = await request(nestApp.getHttpServer()).post( + '/first' + ) + expect(firstResponse.status).toBe(201) + + const secondResponse = await request(nestApp.getHttpServer()).post( + '/second' + ) + expect(secondResponse.status).toBe(403) + }) + + it('should accept an explicit symbol key', async () => { + const key = Symbol('my-permix') + const permix = createPermix().contextKey(key) + + @Controller() + class ProbeController { + @Get('probe') + probe(@Req() req: { [key: PropertyKey]: unknown }) { + return { attached: Boolean(req[key]) } + } + } + + @Module({ + controllers: [ProbeController], + providers: [ + { + provide: APP_GUARD, + useValue: permix.guard({ + post: { create: true, read: true, update: true }, + user: { delete: true }, + }), + }, + ], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()).get('/probe') + expect(response.status).toBe(200) + expect(response.body).toStrictEqual({ attached: true }) + }) + + it('should honor a class-level Check decorator', async () => { + @Controller('posts') + @permix.Check('post.create') + class PostsController { + @Post() + create() { + return { success: true } + } + } + + @Module({ + controllers: [PostsController], + providers: [{ provide: APP_GUARD, useValue: permix.guard(denied) }], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()).post('/posts') + expect(response.status).toBe(403) + expect(response.body).toStrictEqual({ error: 'Forbidden' }) + }) + }) + + describe('get / getOrThrow', () => { + const permix = createPermix() + + it('should return null when the guard has not run', async () => { + @Controller() + class RootController { + @Get() + root(@Req() req: { [key: PropertyKey]: unknown }) { + return { result: permix.get(req) } + } + } + + @Module({ controllers: [RootController] }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()).get('/') + expect(response.status).toBe(200) + expect(response.body).toStrictEqual({ result: null }) + }) + + it('should return the instance when the guard has run', async () => { + @Controller() + class RootController { + @Get() + root(@Req() req: { [key: PropertyKey]: unknown }) { + const p = permix.getOrThrow(req) + return { hasCheck: typeof p.check === 'function' } + } + } + + @Module({ + controllers: [RootController], + providers: [ + { + provide: APP_GUARD, + useValue: permix.guard({ + post: { create: true, read: true, update: true }, + user: { delete: true }, + }), + }, + ], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()).get('/') + expect(response.status).toBe(200) + expect(response.body).toStrictEqual({ hasCheck: true }) + }) + + it('getOrThrow should throw PermixNotFoundError when missing', async () => { + @Controller() + class RootController { + @Get() + root(@Req() req: { [key: PropertyKey]: unknown }) { + permix.getOrThrow(req) + return { ok: true } + } + } + + @Module({ controllers: [RootController] }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()).get('/') + expect(response.status).toBe(500) + expect(response.body.statusCode).toBe(500) + }) + + it('getRules should return null when the guard has not run', async () => { + @Controller() + class RootController { + @Get() + root(@Req() req: { [key: PropertyKey]: unknown }) { + return { rules: permix.getRules(req) } + } + } + + @Module({ controllers: [RootController] }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()).get('/') + expect(response.status).toBe(200) + expect(response.body).toStrictEqual({ rules: null }) + }) + + it('getRules should return the current rules when the guard has run', async () => { + @Controller() + class RootController { + @Get() + root(@Req() req: { [key: PropertyKey]: unknown }) { + return { rules: permix.getRules(req) } + } + } + + @Module({ + controllers: [RootController], + providers: [ + { + provide: APP_GUARD, + useValue: permix.guard({ + post: { create: true, read: false, update: false }, + user: { delete: true }, + }), + }, + ], + }) + class AppModule {} + + const nestApp = await createApp(AppModule) + const response = await request(nestApp.getHttpServer()).get('/') + expect(response.status).toBe(200) + expect(response.body).toStrictEqual({ + rules: { + post: { create: true, read: false, update: false }, + user: { delete: true }, + }, + }) + }) + }) + + describe('key exposure', () => { + it('should expose the key on the factory return', () => { + const permix = + createPermix().contextKey('custom-key') + expect(permix.key).toBe('custom-key') + }) + + it('should expose a symbol key when using default', () => { + const permix = createPermix() + expect(permix.key).toBeTypeOf('symbol') + }) + }) +}) diff --git a/permix/src/nest/permix.ts b/permix/src/nest/permix.ts new file mode 100644 index 00000000..7d1f8bc1 --- /dev/null +++ b/permix/src/nest/permix.ts @@ -0,0 +1,214 @@ +import type { + CanActivate, + CustomDecorator, + ExecutionContext, +} from '@nestjs/common' +import { ForbiddenException, SetMetadata } from '@nestjs/common' + +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' + +/** + * HTTP request object from `ExecutionContext.switchToHttp().getRequest()`. + * Compatible with both the Express and Fastify Nest adapters. + */ +export type NestHttpRequest = Record + +export interface GuardContext { + req: NestHttpRequest + context: ExecutionContext +} + +export interface PermixOptions { + /** + * Called when a `@Check` decorator denies the request. Defaults to throwing + * a Nest `ForbiddenException` with `{ error: 'Forbidden' }`. + */ + onForbidden?: (params: CheckContext & GuardContext) => MaybePromise +} + +function getRequest(context: ExecutionContext): NestHttpRequest { + return context.switchToHttp().getRequest() +} + +function readCheckArgs( + metadataKey: string | symbol, + context: ExecutionContext +): CheckArgs | undefined { + const handler = context.getHandler() + const classRef = context.getClass() + const fromHandler = Reflect.getMetadata(metadataKey, handler) as + | CheckArgs + | undefined + if (fromHandler) { + return fromHandler + } + return Reflect.getMetadata(metadataKey, classRef) as CheckArgs | undefined +} + +function buildPermix( + resolveKey: () => string | symbol, + options: PermixOptions = {} +) { + const checkMetadataKey = Symbol('permix:check') + const onForbidden = + options.onForbidden ?? + (() => { + throw new ForbiddenException({ error: 'Forbidden' }) + }) + + const hooks = createHooks>() + + function get(req: NestHttpRequest): PermixCore | null { + const instance = req[resolveKey()] as PermixCore | undefined + return instance ?? null + } + + function getOrThrow(req: NestHttpRequest): PermixCore { + const instance = get(req) + if (!instance) { + throw new PermixNotFoundError(resolveKey()) + } + return instance + } + + function attach(req: NestHttpRequest, rules: Rules): PermixCore { + const instance = createPermixCore(rules) + instance.hook('check', (context) => { + hooks.callHook('check', context) + }) + req[resolveKey()] = instance + return instance + } + + /** + * Nest guard that always sets up a per-request Permix instance, then enforces + * `@Check(...)` when that decorator is present on the handler or controller. + * + * Register globally with `APP_GUARD`, or per-controller / per-route with + * `@UseGuards`. + */ + function guard( + callbackOrRules: + | ((context: GuardContext) => MaybePromise>) + | Rules + ): CanActivate { + return { + async canActivate(context) { + const req = getRequest(context) + const rules = + typeof callbackOrRules === 'function' + ? await callbackOrRules({ req, context }) + : callbackOrRules + const instance = attach(req, rules) + + const args = readCheckArgs(checkMetadataKey, context) + if (!args) { + return true + } + + const allowed = instance.check(...args) + if (allowed) { + return true + } + + await onForbidden({ + req, + context, + ...createCheckContext(...args), + }) + return false + }, + } + } + + /** + * Method or class decorator that records the permission check for `guard()`. + */ + const Check: (...args: CheckArgs) => CustomDecorator = ( + ...args + ) => SetMetadata(checkMetadataKey, args) + + function getRules(req: NestHttpRequest): Rules | null { + return get(req)?.getRules() ?? null + } + + function template(rules: Rules | ((param: T) => Rules)) { + return createTemplate(rules) + } + + return { + guard, + Check, + 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 guard factory that wires Permix into NestJS routes. + * + * Use `.contextKey('name')` to set a custom request key (defaults to a unique + * `Symbol('permix')`). + * + * @example + * ```ts + * import { APP_GUARD } from '@nestjs/core' + * import { createPermix } from 'permix/nest' + * + * const permix = createPermix<{ + * post: ['create', 'read'] + * }>() + * + * @Get() + * @permix.Check('post.read') + * findAll() {} + * + * // app.module.ts + * { + * provide: APP_GUARD, + * useValue: permix.guard(({ req }) => ({ + * post: { create: !!req.user, read: true }, + * })), + * } + * ``` + * + * @link https://permix.letstri.dev/docs/integrations/nest + */ +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 + }, + }) +} + +/** Return type of {@link createPermix}. */ +export type NestPermix = ReturnType< + typeof createPermix +> diff --git a/permix/tsconfig.base.json b/permix/tsconfig.base.json index cf0229e7..d24217e3 100644 --- a/permix/tsconfig.base.json +++ b/permix/tsconfig.base.json @@ -7,7 +7,8 @@ "strict": true, "noEmit": true, "forceConsistentCasingInFileNames": true, - "skipLibCheck": true + "skipLibCheck": true, + "experimentalDecorators": true }, "include": ["src/**/*.ts", "src/**/*.tsx", "*.ts"], "exclude": [ diff --git a/permix/tsdown.config.ts b/permix/tsdown.config.ts index 00e9a0a3..1a2cdb10 100644 --- a/permix/tsdown.config.ts +++ b/permix/tsdown.config.ts @@ -22,6 +22,7 @@ export default defineConfig({ './src/drizzle/legacy/index.ts', './src/next/index.ts', './src/tanstack-start/index.ts', + './src/nest/index.ts', ], dts: { build: true, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 761d130a..3b37f901 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -263,6 +263,34 @@ importers: specifier: ^8.0.16 version: 8.0.16(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + examples/nest: + dependencies: + '@nestjs/common': + specifier: ^11.1.6 + version: 11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': + specifier: ^11.1.6 + version: 11.2.3(@nestjs/common@11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.2.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': + specifier: ^11.1.6 + version: 11.2.3(@nestjs/common@11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3) + permix: + specifier: workspace:* + version: link:../../permix + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.2 + version: 7.8.2 + devDependencies: + tsx: + specifier: ^4.22.4 + version: 4.22.4 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + examples/next: dependencies: next: @@ -435,7 +463,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)) @@ -556,6 +584,18 @@ importers: specifier: '>=1' version: 1.9.13 devDependencies: + '@nestjs/common': + specifier: ^11.2.3 + version: 11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': + specifier: ^11.2.3 + version: 11.2.3(@nestjs/common@11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.2.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/platform-express': + specifier: ^11.2.3 + version: 11.2.3(@nestjs/common@11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3) + '@nestjs/testing': + specifier: ^11.2.3 + version: 11.2.3(@nestjs/common@11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3)(@nestjs/platform-express@11.2.3) '@solidjs/testing-library': specifier: ^0.8.10 version: 0.8.10(solid-js@1.9.13) @@ -613,6 +653,12 @@ importers: react-dom: specifier: ^19.2.6 version: 19.2.6(react@19.2.6) + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.2 + version: 7.8.2 supertest: specifier: ^7.2.2 version: 7.2.2 @@ -621,7 +667,7 @@ importers: version: 5.56.0 svelte-check: specifier: ^4.5.0 - version: 4.5.0(picomatch@4.0.7)(svelte@5.56.0)(typescript@6.0.3) + version: 4.5.0(picomatch@4.0.4)(svelte@5.56.0)(typescript@6.0.3) tsdown: specifier: ^0.22.1 version: 0.22.1(tsx@4.22.4)(typescript@6.0.3) @@ -1252,6 +1298,10 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + '@mdx-js/mdx@3.1.1': resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} @@ -1273,6 +1323,56 @@ packages: '@neodrag/core': 3.0.0-next.11 solid-js: ^1.0.0 + '@nestjs/common@11.2.3': + resolution: {integrity: sha512-obdauJXHfthhepbV+LpGe88OeBlR/Kw9lwjLo0Utzc//agoLXYb9DUGhPQWtm81IpBWMv+19eiwcve9MsBZwXA==} + peerDependencies: + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/core@11.2.3': + resolution: {integrity: sha512-vkA9/Ja0Z3hvqXErSa+HaxrfF+cNXthNFi8VPNEKVli4rMd009yExAl0gLmko/Kf8peDXr72u1RN+j9Da2ukHg==} + engines: {node: '>= 20'} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + + '@nestjs/platform-express@11.2.3': + resolution: {integrity: sha512-YFQvRXT2de1qNL9LJPUBQ31+RsfI4cJ+sbpU9ENM/hDCgoHSEhm7oxUuGGKmhTZBNZEYm8mDYdfoTFmAH1LIJg==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + + '@nestjs/testing@11.2.3': + resolution: {integrity: sha512-7ANDWlkm8Xw4CYIhCNZhtBzANsQUKqjteA2yx/6sjqGyWhekeBKz8wgCJykm0vo+ltrg6U34dZlm2NgiRcNHPQ==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@next/env@16.2.6': resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==} @@ -2634,12 +2734,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 +3027,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': @@ -3568,6 +3668,9 @@ packages: resolution: {integrity: sha512-44mvgtPvohuU/70DdY5Oz2AIrLJ9k6/5x4KmoSvPwO+5Moijo0+N9D0fKbbYZQWP1hNm5CpOf+E01jhxG/r8xg==} engines: {node: '>=14'} + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -3668,6 +3771,13 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -3786,6 +3896,10 @@ packages: compute-scroll-into-view@3.1.1: resolution: {integrity: sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==} + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} @@ -4514,6 +4628,10 @@ packages: resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} engines: {node: '>=18'} + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} + file-type@22.0.1: resolution: {integrity: sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==} engines: {node: '>=22'} @@ -5000,6 +5118,10 @@ packages: resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} engines: {node: '>=8'} + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} @@ -5154,6 +5276,10 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} + engines: {node: '>=13.2.0'} + locate-character@3.0.0: resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} @@ -5271,6 +5397,10 @@ packages: mdn-data@2.27.1: resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + media-typer@1.1.0: resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} engines: {node: '>= 0.8'} @@ -5484,6 +5614,10 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + multer@2.2.0: + resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} + engines: {node: '>= 10.16.0'} + nanoid@3.3.12: resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -5909,6 +6043,10 @@ packages: resolution: {integrity: sha512-PNaGjoCnw9DBA2Kl8D+8po957z778q/HOPuY2u3Bkw/JO3eC8MDx7jn/PgMtSgpcBbs+6UOjDbwReGpXmRvs0g==} engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -5942,6 +6080,9 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + regex-recursion@6.0.2: resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} @@ -6048,10 +6189,16 @@ packages: rw@1.3.3: resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-regex2@5.1.1: resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} hasBin: true @@ -6220,6 +6367,10 @@ packages: std-env@4.1.0: resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -6232,6 +6383,9 @@ packages: resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} engines: {node: '>=20'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} @@ -6473,10 +6627,17 @@ packages: resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==} engines: {node: '>=20'} + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + type-is@2.1.0: resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} engines: {node: '>= 18'} + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + typescript@6.0.3: resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} @@ -6485,6 +6646,10 @@ packages: ufo@1.6.4: resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + uint8array-extras@1.5.0: resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} engines: {node: '>=18'} @@ -7449,6 +7614,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lukeed/csprng@1.1.0': {} + '@mdx-js/mdx@3.1.1': dependencies: '@types/estree': 1.0.9 @@ -7497,6 +7664,51 @@ snapshots: '@neodrag/core': 3.0.0-next.11 solid-js: 1.9.13 + '@nestjs/common@11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + file-type: 21.3.4 + iterare: 1.2.1 + load-esm: 1.0.3 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + transitivePeerDependencies: + - supports-color + + '@nestjs/core@11.2.3(@nestjs/common@11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.2.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2) + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + '@nestjs/platform-express': 11.2.3(@nestjs/common@11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3) + + '@nestjs/platform-express@11.2.3(@nestjs/common@11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3)': + dependencies: + '@nestjs/common': 11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.2.3(@nestjs/common@11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.2.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 + express: 5.2.1 + multer: 2.2.0 + path-to-regexp: 8.4.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@nestjs/testing@11.2.3(@nestjs/common@11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3)(@nestjs/platform-express@11.2.3)': + dependencies: + '@nestjs/common': 11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.2.3(@nestjs/common@11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.2.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 + optionalDependencies: + '@nestjs/platform-express': 11.2.3(@nestjs/common@11.2.3(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3) + '@next/env@16.2.6': {} '@next/swc-darwin-arm64@16.2.6': @@ -8665,12 +8877,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 +9274,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 @@ -9862,6 +10074,8 @@ snapshots: ansis@4.3.0: {} + append-field@1.0.0: {} + argparse@2.0.1: {} aria-hidden@1.2.6: @@ -9972,6 +10186,12 @@ snapshots: node-releases: 2.0.46 update-browserslist-db: 1.2.3(browserslist@4.28.2) + buffer-from@1.1.2: {} + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + bytes@3.1.2: {} cac@6.7.14: {} @@ -10063,6 +10283,13 @@ snapshots: compute-scroll-into-view@3.1.1: {} + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + confbox@0.1.8: {} config-chain@1.1.13: @@ -10708,6 +10935,15 @@ snapshots: dependencies: is-unicode-supported: 2.1.0 + file-type@21.3.4: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + file-type@22.0.1: dependencies: '@tokenizer/inflate': 0.4.1 @@ -11246,6 +11482,8 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + iterare@1.2.1: {} + jackspeak@3.4.3: dependencies: '@isaacs/cliui': 8.0.2 @@ -11385,6 +11623,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + load-esm@1.0.3: {} + locate-character@3.0.0: {} lodash-es@4.18.1: {} @@ -11611,6 +11851,8 @@ snapshots: mdn-data@2.27.1: {} + media-typer@0.3.0: {} + media-typer@1.1.0: {} memoirist@0.4.0: {} @@ -11973,6 +12215,13 @@ snapshots: muggle-string@0.4.1: {} + multer@2.2.0: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + nanoid@3.3.12: {} negotiator@1.0.0: {} @@ -12039,7 +12288,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 @@ -12495,6 +12744,12 @@ snapshots: json-parse-even-better-errors: 6.0.0 npm-normalize-package-bin: 6.0.0 + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + readdirp@4.1.2: {} readdirp@5.0.0: {} @@ -12537,6 +12792,8 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + reflect-metadata@0.2.2: {} + regex-recursion@6.0.2: dependencies: regex-utilities: 2.3.0 @@ -12694,10 +12951,16 @@ snapshots: rw@1.3.3: {} + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + sade@1.8.1: dependencies: mri: 1.2.0 + safe-buffer@5.2.1: {} + safe-regex2@5.1.1: dependencies: ret: 0.5.0 @@ -12898,6 +13161,8 @@ snapshots: std-env@4.1.0: {} + streamsearch@1.1.0: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -12915,6 +13180,10 @@ snapshots: get-east-asian-width: 1.6.0 strip-ansi: 7.2.0 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 @@ -12958,6 +13227,12 @@ snapshots: client-only: 0.0.1 react: 19.2.4 + styled-jsx@5.1.6(react@19.2.6): + dependencies: + client-only: 0.0.1 + react: 19.2.6 + optional: true + stylis@4.4.0: {} superagent@10.3.0: @@ -12986,6 +13261,18 @@ snapshots: dependencies: has-flag: 4.0.0 + svelte-check@4.5.0(picomatch@4.0.4)(svelte@5.56.0)(typescript@6.0.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.56.0 + typescript: 6.0.3 + transitivePeerDependencies: + - picomatch + svelte-check@4.5.0(picomatch@4.0.7)(svelte@5.56.0)(typescript@6.0.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -13169,16 +13456,27 @@ snapshots: dependencies: tagged-tag: 1.0.0 + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + type-is@2.1.0: dependencies: content-type: 2.0.0 media-typer: 1.1.0 mime-types: 3.0.2 + typedarray@0.0.6: {} + typescript@6.0.3: {} ufo@1.6.4: {} + uid@2.0.2: + dependencies: + '@lukeed/csprng': 1.1.0 + uint8array-extras@1.5.0: {} ultracite@7.10.6(oxfmt@0.64.0(svelte@5.56.0))(oxlint@1.80.0(oxlint-tsgolint@7.0.2001)): From c98abe195260cf73bbfa1a63a2ead787e77748ac Mon Sep 17 00:00:00 2001 From: Martijn van Dijk Date: Fri, 28 Aug 2026 20:59:08 +0200 Subject: [PATCH 2/2] fix: skip vendored agent skills in Oxfmt and Oxlint Toolchain skills from skills.sh fail format check and are not project source. --- ignores.ts | 2 ++ 1 file changed, 2 insertions(+) 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/**',