{"_id":"@chaeco/jwt-permission","_rev":"4-a34c6cae64cee153c66788375ce6182f","name":"@chaeco/jwt-permission","dist-tags":{"latest":"1.1.2"},"versions":{"1.0.2":{"name":"@chaeco/jwt-permission","version":"1.0.2","keywords":["hoa","jwt","permission","middleware","authentication"],"author":{"name":"iptodays"},"license":"MIT","_id":"@chaeco/jwt-permission@1.0.2","maintainers":[{"name":"iptodays","email":"kingiswinter@outlook.com"}],"homepage":"https://github.com/chaeco/jwt-permission#readme","bugs":{"url":"https://github.com/chaeco/jwt-permission/issues"},"bin":{"jwt-permission-init-skills":"scripts/init-skills.js"},"dist":{"shasum":"55d37c89246f47056ab03f20928b85ebd27e6ad0","tarball":"https://registry.npmjs.org/@chaeco/jwt-permission/-/jwt-permission-1.0.2.tgz","fileCount":13,"integrity":"sha512-nbakaI/x1zaYZ8mizO1PV+bA7k5Er2085m/08/ruIk8E2dgm2TXpJAfQN3Jm80c28VC5ZVgTTXQkh1r3hhhjag==","signatures":[{"sig":"MEQCIEX/GeD2pF6nPghsumSzjUTJT+GEXVPk5cFxktIN35ofAiAfUbrk0bg6sh1QK5PzqHOEa04a0+oigqsLynsu7unb4w==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":62045},"main":"dist/cjs/index.js","types":"dist/types/index.d.ts","module":"dist/esm/index.js","exports":{".":{"types":"./dist/types/index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"}},"gitHead":"4ec9982e1f4c500243f4e56f3503fd1dfd9d8de7","scripts":{"dev":"tsc --watch","test":"vitest run","build":"npm run build:esm && npm run build:cjs","coverage":"vitest run --coverage","build:cjs":"tsc --module commonjs --outDir dist/cjs --declaration false && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json && tsc --emitDeclarationOnly --declaration --declarationDir dist/types","build:esm":"tsc --module esnext --outDir dist/esm --declaration false && echo '{\"type\":\"module\"}' > dist/esm/package.json","test:watch":"vitest"},"_npmUser":{"name":"iptodays","email":"kingiswinter@outlook.com"},"repository":{"url":"git+https://github.com/chaeco/jwt-permission.git","type":"git"},"_npmVersion":"10.8.2","description":"Framework-agnostic JWT permission middleware for Node.js (Hoa, Koa, Express, etc.)","directories":{},"_nodeVersion":"20.20.2","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.18","typescript":"^5.9.3","@vitest/coverage-v8":"^4.0.18"},"_npmOperationalInternal":{"tmp":"tmp/jwt-permission_1.0.2_1780970947613_0.2733545269154465","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@chaeco/jwt-permission","version":"1.1.0","keywords":["hoa","jwt","permission","middleware","authentication"],"author":{"name":"iptodays"},"license":"MIT","_id":"@chaeco/jwt-permission@1.1.0","maintainers":[{"name":"iptodays","email":"kingiswinter@outlook.com"}],"homepage":"https://github.com/chaeco/jwt-permission#readme","bugs":{"url":"https://github.com/chaeco/jwt-permission/issues"},"bin":{"jwt-permission-init-skills":"scripts/init-skills.mjs"},"dist":{"shasum":"611008d0a8b89f98e31a46b2beba994ac2acc13e","tarball":"https://registry.npmjs.org/@chaeco/jwt-permission/-/jwt-permission-1.1.0.tgz","fileCount":13,"integrity":"sha512-2ZUFxERwlw4J/TaeX2tuNOJjqOGGpIV9cgFoiZibU6P/mnJP4du5LbBJah/4L2z1qTtK6Jl+BqYiCAdHRgoduw==","signatures":[{"sig":"MEQCIEFxpuJcn47fas+jo6445NtLaGKv08Plc3UKAankiyvJAiB55n9ZdThvRrFgAoGz94fjkXy0bAEkW7W57fgQk9OnKQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":66781},"main":"dist/cjs/index.js","types":"dist/types/index.d.ts","module":"dist/esm/index.js","exports":{".":{"types":"./dist/types/index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"}},"gitHead":"64cd8768de48504643461bd9ea54f1fab9ac814f","scripts":{"dev":"tsc --watch","test":"vitest run","build":"npm run build:esm && npm run build:cjs","coverage":"vitest run --coverage","build:cjs":"tsc --module commonjs --outDir dist/cjs --declaration false && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json && tsc --emitDeclarationOnly --declaration --declarationDir dist/types","build:esm":"tsc --module esnext --outDir dist/esm --declaration false && echo '{\"type\":\"module\"}' > dist/esm/package.json","test:watch":"vitest"},"_npmUser":{"name":"iptodays","email":"kingiswinter@outlook.com"},"repository":{"url":"git+https://github.com/chaeco/jwt-permission.git","type":"git"},"_npmVersion":"10.8.2","description":"Framework-agnostic JWT permission middleware for Node.js (Hoa, Koa, Express, etc.)","directories":{},"_nodeVersion":"20.20.2","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.18","typescript":"^5.9.3","@vitest/coverage-v8":"^4.0.18"},"_npmOperationalInternal":{"tmp":"tmp/jwt-permission_1.1.0_1785604469880_0.6897563151955761","host":"s3://npm-registry-packages-npm-production"}},"1.1.1":{"name":"@chaeco/jwt-permission","version":"1.1.1","keywords":["hoa","jwt","permission","middleware","authentication"],"author":{"name":"iptodays"},"license":"MIT","_id":"@chaeco/jwt-permission@1.1.1","maintainers":[{"name":"iptodays","email":"kingiswinter@outlook.com"}],"homepage":"https://github.com/chaeco/jwt-permission#readme","bugs":{"url":"https://github.com/chaeco/jwt-permission/issues"},"bin":{"jwt-permission-init-skills":"scripts/init-skills.mjs"},"dist":{"shasum":"3996e10bfb65af5cf349bb101c9c9b686858bd0a","tarball":"https://registry.npmjs.org/@chaeco/jwt-permission/-/jwt-permission-1.1.1.tgz","fileCount":13,"integrity":"sha512-5O/MOFyIkJtc05HwytTZ/i2CTQgyVPQ8ciWd1WdoFlWHg25BpJq8p9dhMRBfqSshxZHjYoc6vxWrDgaGT1qDKg==","signatures":[{"sig":"MEYCIQDx2xpq4M2mByJ7BuKctyYOIZhoqwRzPdXbPnLAw2D8fAIhAJ7S0NfSwHQgDscXZkmbj/znPC7cOomPQKrq9tDrF62m","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":68296},"main":"dist/cjs/index.js","types":"dist/types/index.d.ts","module":"dist/esm/index.js","exports":{".":{"types":"./dist/types/index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"}},"gitHead":"56b10c714a3dedfe5a71e248977a528362c16ceb","scripts":{"dev":"tsc --watch","test":"vitest run","build":"npm run build:esm && npm run build:cjs","coverage":"vitest run --coverage","build:cjs":"tsc --module commonjs --outDir dist/cjs --declaration false && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json && tsc --emitDeclarationOnly --declaration --declarationDir dist/types","build:esm":"tsc --module esnext --outDir dist/esm --declaration false && echo '{\"type\":\"module\"}' > dist/esm/package.json","test:watch":"vitest"},"_npmUser":{"name":"iptodays","email":"kingiswinter@outlook.com"},"repository":{"url":"git+https://github.com/chaeco/jwt-permission.git","type":"git"},"_npmVersion":"10.8.2","description":"Framework-agnostic JWT permission middleware for Node.js (Hoa, Koa, Express, etc.)","directories":{},"_nodeVersion":"20.20.2","_hasShrinkwrap":false,"devDependencies":{"vitest":"^4.0.18","typescript":"^5.9.3","@vitest/coverage-v8":"^4.0.18"},"_npmOperationalInternal":{"tmp":"tmp/jwt-permission_1.1.1_1786708377950_0.2653675239286888","host":"s3://npm-registry-packages-npm-production"}},"1.1.2":{"name":"@chaeco/jwt-permission","version":"1.1.2","description":"Framework-agnostic JWT permission middleware for Node.js (Hoa, Koa, Express, etc.)","main":"dist/cjs/index.js","module":"dist/esm/index.js","types":"dist/types/index.d.ts","bin":{"jwt-permission-init-skills":"scripts/init-skills.mjs"},"exports":{".":{"types":"./dist/types/index.d.ts","import":"./dist/esm/index.js","require":"./dist/cjs/index.js"}},"scripts":{"build":"rm -rf dist && rollup -c rollup.config.mjs && echo '{\"type\":\"module\"}' > dist/esm/package.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json","dev":"tsc --watch","test":"vitest run","test:watch":"vitest","coverage":"vitest run --coverage"},"keywords":["hoa","jwt","permission","middleware","authentication"],"author":{"name":"iptodays"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/chaeco/jwt-permission.git"},"devDependencies":{"@rollup/plugin-commonjs":"^29.0.3","@rollup/plugin-node-resolve":"^16.0.3","@rollup/plugin-typescript":"^12.3.0","@vitest/coverage-v8":"^4.0.18","rollup":"^4.62.4","rollup-plugin-dts":"^6.5.1","tslib":"^2.8.1","typescript":"^5.9.3","vitest":"^4.0.18"},"_id":"@chaeco/jwt-permission@1.1.2","gitHead":"bb5df36c87be198169293550a52bc9fec67a2b05","bugs":{"url":"https://github.com/chaeco/jwt-permission/issues"},"homepage":"https://github.com/chaeco/jwt-permission#readme","_nodeVersion":"20.20.2","_npmVersion":"10.8.2","dist":{"integrity":"sha512-/ei4c080vYACjo0zQI7yVHg2c1S6b1WUER+oKrzLvWI0Kjd5oZCaTkAHrQg6D2QZuRAv1MvBputbjUmF62IvAg==","shasum":"383d8afc68a5af56ea34c12e735a5673eca6fa9f","tarball":"https://registry.npmjs.org/@chaeco/jwt-permission/-/jwt-permission-1.1.2.tgz","fileCount":15,"unpackedSize":101571,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDerT1Pm7D0UX9X/w3rkTqEhYWOw074mll8plYbwbrFMwIgIIfZKXWohRvDmfigaMY/4KNEaxabSRJH3bk3Kme569M="}]},"_npmUser":{"name":"iptodays","email":"kingiswinter@outlook.com"},"directories":{},"maintainers":[{"name":"iptodays","email":"kingiswinter@outlook.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/jwt-permission_1.1.2_1787144114232_0.043676459197824924"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-09T02:09:07.429Z","modified":"2026-08-19T12:55:14.550Z","1.0.2":"2026-06-09T02:09:07.765Z","1.1.0":"2026-08-01T17:14:30.025Z","1.1.1":"2026-08-14T11:52:58.101Z","1.1.2":"2026-08-19T12:55:14.386Z"},"bugs":{"url":"https://github.com/chaeco/jwt-permission/issues"},"author":{"name":"iptodays"},"license":"MIT","homepage":"https://github.com/chaeco/jwt-permission#readme","keywords":["hoa","jwt","permission","middleware","authentication"],"repository":{"type":"git","url":"git+https://github.com/chaeco/jwt-permission.git"},"description":"Framework-agnostic JWT permission middleware for Node.js (Hoa, Koa, Express, etc.)","maintainers":[{"name":"iptodays","email":"kingiswinter@outlook.com"}],"readme":"# @chaeco/jwt-permission\n\nEnglish | [中文](./README-zh.md)\n\n[![version](https://img.shields.io/badge/version-1.1.1-blue.svg)](./CHANGELOG.md)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](./LICENSE)\n[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-blue.svg)](https://www.typescriptlang.org/)\n[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](./coverage)\n[![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](./package.json)\n\nFramework-agnostic JWT route permission middleware for Node.js. Compatible with Hoa, Koa, Express, and any framework that uses a `ctx.state.user` convention.\n\n## Features\n\n- ✅ **Framework-agnostic** — works with Hoa, Koa, Express, and more\n- ✅ Route-based permission control\n- ✅ Auto-discovery via `@chaeco/auto-router` — no manual route lists needed\n- ✅ Public and protected route support\n- ✅ Path parameter matching (e.g. `/api/users/:userId`)\n- ✅ URL-decode safety — prevents `%xx` encoding bypass attacks\n- ✅ Custom route matching logic\n- ✅ Custom unauthorized response handler\n- ✅ Full TypeScript generics support\n\n## Installation\n\n```bash\nnpm install @chaeco/jwt-permission\n```\n\nOr pin to a specific version:\n\n```bash\nnpm install @chaeco/jwt-permission@1.1.2\n```\n\n## Quick Start\n\n### Option 1: Auto-discovery (Recommended)\n\nUse with `@chaeco/auto-router` to automatically read route permission metadata:\n\n```typescript\nimport { jwtAuth } from '@chaeco/jwt-permission'\nimport { autoRouter } from '@chaeco/auto-router'\n\n// autoRouter scans controllers/ and extracts permission metadata\napp.extend(\n  autoRouter({\n    defaultRequiresAuth: false,\n  })\n)\n\n// jwtAuth reads permission config automatically from autoRouter\napp.use(\n  jwtAuth({\n    autoDiscovery: true,\n  })\n)\n```\n\n### Option 2: Manual configuration\n\n```typescript\nimport { jwtAuth } from '@chaeco/jwt-permission'\n\napp.use(\n  jwtAuth({\n    publicRoutes: [\n      { method: 'POST', path: '/api/auth/login' },\n      { method: 'POST', path: '/api/users/register' },\n    ],\n    protectedRoutes: [\n      { method: 'GET', path: '/api/users/info' },\n      { method: 'DELETE', path: '/api/users/:id' },\n    ],\n  })\n)\n```\n\n### Using in a controller\n\n```typescript\nimport { getCurrentUser } from '@chaeco/jwt-permission'\n\nasync function getInfoHandler(ctx) {\n  const user = getCurrentUser(ctx)\n  ctx.res.body = { success: true, data: user }\n}\n```\n\n## API\n\n### `jwtAuth(options)`\n\nCreates a JWT permission middleware (shorthand alias for `createJwtPermission`).\n\n**Options**:\n\n| Option | Type | Default | Description |\n|--------|------|---------|-------------|\n| `autoDiscovery` | `boolean` | `true` | Enable auto-discovery from `app.$routes` |\n| `publicRoutes` | `RouteRule[]` | — | Routes that skip JWT verification |\n| `protectedRoutes` | `RouteRule[]` | — | Routes that require JWT verification |\n| `unauthorizedResponse` | `(ctx) => void` | built-in | Custom 401 response handler |\n| `isPublicRoute` | `(method, path) => boolean` | — | Custom public route matcher (overrides built-in) |\n| `isProtectedRoute` | `(method, path) => boolean` | — | Custom protected route matcher (overrides built-in) |\n| `defaultDeny` | `boolean` | `false` | Reject unknown routes with 401 (recommended for production) |\n| `onUnauthorized` | `(ctx, reason) => void` | — | Callback when request is rejected: `'no_user'` or `'default_deny'` |\n\n**Returns**: `PermissionMiddleware<TContext>`\n\n> **Built-in `unauthorizedResponse`** auto-detects the framework style:\n> - Hoa style: writes to `ctx.res.status` / `ctx.res.body`\n> - Koa style: writes to `ctx.status` / `ctx.body`\n> - Other frameworks (e.g. Express): **must** provide a custom `unauthorizedResponse`\n\n### `createJwtPermission<TContext>(options)`\n\nSame as `jwtAuth()`, with explicit generic support for custom context types:\n\n```typescript\nimport { createJwtPermission } from '@chaeco/jwt-permission'\nimport type { MyAppContext } from './types'\n\nconst middleware = createJwtPermission<MyAppContext>({ ... })\n```\n\n### `getCurrentUser(ctx)`\n\nReturns the authenticated user stored in `ctx.state.user`, or `null` if not authenticated.\n\n```typescript\nconst user = getCurrentUser(ctx)\n// { id: 1, username: 'alice', ... } | null\n```\n\n### `isAuthenticated(ctx)`\n\nReturns `true` if `ctx.state.user` is present.\n\n```typescript\nif (isAuthenticated(ctx)) {\n  // request is authenticated\n}\n```\n\n## Integration with `@chaeco/auto-router`\n\n### Marking permissions in controllers\n\n```typescript\n// controllers/users/get-info.ts\nimport { createHandler } from '@chaeco/auto-router'\n\nexport default createHandler(\n  async ctx => {\n    ctx.res.body = { success: true, data: ctx.state.user }\n  },\n  { requiresAuth: true },\n)\n\n// controllers/auth/post-login.ts\nexport default createHandler(\n  async ctx => {\n    // login logic\n  },\n  { requiresAuth: false },\n)\n```\n\n### How it works\n\n1. `autoRouter` scans the `controllers/` directory\n2. Extracts `requiresAuth` metadata from `createHandler()` calls\n3. Stores route info in `app.$routes`\n4. `jwtAuth` reads and caches `app.$routes` **on the first request**\n5. **No duplicated route lists needed!**\n\n## Route Rules\n\n### HTTP methods\n\n`GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS` — case-insensitive.\n\n### Path format\n\n- Must start with `/`\n- Trailing slash is significant (`/api/users` ≠ `/api/users/`)\n- Supports `:paramName` syntax for dynamic segments\n- Wildcards (`*`) are **not** supported — use `isPublicRoute` / `isProtectedRoute` instead\n\n### Path parameter matching\n\n```typescript\n{ method: 'GET', path: '/api/users/:userId/posts/:postId' }\n// matches: /api/users/123/posts/456\n```\n\n## Custom Route Matching\n\n```typescript\nimport { createJwtPermission } from '@chaeco/jwt-permission'\n\nconst middleware = createJwtPermission({\n  isPublicRoute: (method, path) => path.startsWith('/api/public'),\n  isProtectedRoute: (method, path) => path.startsWith('/api/admin'),\n})\n```\n\n> `method` is always uppercase (e.g. `'GET'`). `path` is URL-decoded and query-string-free.\n\n## Custom Unauthorized Response\n\n```typescript\nimport { jwtAuth } from '@chaeco/jwt-permission'\n\nconst middleware = jwtAuth({\n  unauthorizedResponse: ctx => {\n    ctx.res.status = 401\n    ctx.res.body = {\n      success: false,\n      message: 'Authentication required',\n      code: 'UNAUTHORIZED',\n    }\n  },\n})\n```\n\n## Middleware Order\n\nJWT token parsing **must run before** `jwtAuth`. The upstream JWT middleware is responsible for verifying the token and writing the decoded payload to `ctx.state.user`.\n\n```typescript\n// Hoa\nimport { jwt } from '@hoajs/jwt'\napp.use(jwt({ secret: process.env.JWT_SECRET!, algorithms: ['HS256'] }))\napp.use(jwtAuth({ autoDiscovery: true }))\n\n// Koa\nimport koaJwt from 'koa-jwt'\napp.use(koaJwt({ secret: process.env.JWT_SECRET! }))\napp.use(jwtAuth({ autoDiscovery: true }))\n```\n\n## Request Flow\n\n```text\nIncoming request\n  ↓\n[1] JWT parsing middleware\n  ├─ Verify token signature & expiry\n  ├─ Valid   → write decoded payload to ctx.state.user\n  └─ Invalid → return 401\n  ↓\n[2] autoRouter (route registration)\n  ├─ Scan controllers/\n  ├─ Collect requiresAuth metadata\n  └─ Store in app.$routes\n  ↓\n[3] jwtAuth (permission check)\n  ├─ Public route?    → pass through\n  ├─ Protected route?\n  │  ├─ ctx.state.user exists → pass through\n  │  └─ ctx.state.user absent → return 401\n  └─ Unknown route\n     ├─ defaultDeny: true  → return 401\n     └─ defaultDeny: false → pass through (default)\n  ↓\nRoute handler (business logic)\n```\n\n## Best Practices\n\n✅ **Do**:\n\n- Use `autoDiscovery: true` together with `@chaeco/auto-router`\n- Explicitly mark route permissions with `createHandler()` in controllers\n- Use `getCurrentUser()` in handlers after the middleware chain\n- Regularly audit route permission configuration\n- Use strong JWT secrets\n\n❌ **Don't**:\n\n- Forget to place JWT token-parsing middleware before `jwtAuth`\n- Use wildcards (`*`) in route rules — use `isPublicRoute` / `isProtectedRoute` instead\n- Hardcode permissions for sensitive endpoints — declare them with `createHandler` in controllers\n\n## Examples\n\n### Hoa + autoRouter (recommended)\n\n```typescript\nimport { Hoa } from 'hoa'\nimport { jwt } from '@hoajs/jwt'\nimport { jwtAuth, getCurrentUser } from '@chaeco/jwt-permission'\nimport { autoRouter } from '@chaeco/auto-router'\n\nconst app = new Hoa()\n\napp.use(jwt({ secret: process.env.JWT_SECRET!, algorithms: ['HS256'] }))\napp.use(jwtAuth({ autoDiscovery: true }))\napp.extend(autoRouter({ defaultRequiresAuth: false }))\n\napp.get('/api/users/info', async ctx => {\n  ctx.res.body = { success: true, data: getCurrentUser(ctx) }\n})\n\napp.listen(3000)\n```\n\n### Koa\n\n```typescript\nimport Koa from 'koa'\nimport koaJwt from 'koa-jwt'\nimport { jwtAuth } from '@chaeco/jwt-permission'\n\nconst app = new Koa()\n\napp.use(koaJwt({ secret: process.env.JWT_SECRET! }))\napp.use(\n  jwtAuth({\n    publicRoutes: [{ method: 'POST', path: '/api/auth/login' }],\n    protectedRoutes: [{ method: 'GET', path: '/api/profile' }],\n  })\n)\n\napp.listen(3000)\n```\n\n### Express\n\nExpress uses a different `req`/`res` structure. Bridge `req.auth` to `ctx.state.user` and provide a custom `unauthorizedResponse`:\n\n```typescript\nimport express from 'express'\nimport { expressjwt } from 'express-jwt'\nimport { jwtAuth } from '@chaeco/jwt-permission'\n\nconst app = express()\n\napp.use((req, res, next) => {\n  expressjwt({ secret: process.env.JWT_SECRET!, algorithms: ['HS256'] })(req, res, () => {\n    ;(req as any).state = { user: (req as any).auth }\n    next()\n  })\n})\n\nconst permission = jwtAuth({\n  publicRoutes: [{ method: 'POST', path: '/api/auth/login' }],\n  protectedRoutes: [{ method: 'GET', path: '/api/profile' }],\n  unauthorizedResponse: ctx => {\n    ;(ctx as any).res.status(401).json({ success: false, code: 'UNAUTHORIZED' })\n  },\n})\n\napp.use((req, res, next) => {\n  permission({ req, res, state: (req as any).state ?? {} } as any, next as any)\n})\n\napp.listen(3000)\n```\n\n## TypeScript\n\nAll types are exported directly from the package:\n\n```typescript\nimport type {\n  HttpMethod,\n  PermissionContext,\n  PermissionMiddleware,\n  RouteRule,\n  JwtPermissionOptions,\n} from '@chaeco/jwt-permission'\n```\n\n## FAQ\n\n**Q: Can I mix auto-discovery and manual configuration?**\n\nA: Yes. `autoDiscovery` only fills in the side that is not manually provided (`publicRoutes` or `protectedRoutes`). Both sides are independent.\n\n**Q: How do I make a route always public?**\n\n```typescript\njwtAuth({\n  publicRoutes: [\n    { method: 'POST', path: '/api/auth/login' },\n    { method: 'POST', path: '/api/users/register' },\n    { method: 'GET', path: '/api/health' },\n  ],\n})\n```\n\n**Q: How do I handle token refresh?**\n\nMark the refresh endpoint as a public route:\n\n```typescript\n{ method: 'POST', path: '/api/auth/refresh' }\n```\n\n**Q: What happens to routes not in either list?**\n\nThey are allowed through by default (pass-through behavior). To reject unknown routes in production, use `defaultDeny: true`:\n\n```typescript\njwtAuth({\n  defaultDeny: true,\n  // unknown routes → 401\n})\n```\n\n## Performance\n\n- ✅ Route regexes are compiled once and cached at module level\n- ✅ Auto-discovered routes are read and cached on the first request only\n- ✅ When both sides are covered by custom functions, all route list parsing is skipped\n- ✅ Token verification is handled by the upstream JWT middleware — this middleware only checks whether `ctx.state.user` exists\n\n## AI Tool Skills\n\nThis package includes AI agent skills for Claude Code and OpenAI Codex.\n\nAfter installation, run **one command** to copy the skills into your project:\n\n```bash\nnpx jwt-permission-init-skills\n```\n\nThis places skill files into `.claude/skills/jwt-permission/` and `.codex/skills/jwt-permission/`.\nAI tools will then enforce correct middleware order, framework-specific setup (Hoa / Koa / Express),\nroute rule format, and auth best practices when adding JWT permission checks.\n\n## Changelog\n\nSee [CHANGELOG.md](./CHANGELOG.md).\n\n## License\n\nMIT\n","readmeFilename":"README.md"}