{"_id":"@access-tokens/express","_rev":"4-7569f7e4ce55df3989ee34031449c16a","name":"@access-tokens/express","dist-tags":{"latest":"1.1.0"},"versions":{"1.0.0":{"name":"@access-tokens/express","version":"1.0.0","keywords":["dynamodb","personal-access-token","pat","authentication","express","middleware","jwt"],"author":{"name":"Trevor Robinson"},"license":"ISC","_id":"@access-tokens/express@1.0.0","maintainers":[{"name":"trevorr","email":"trevor@scurrilous.com"}],"homepage":"https://github.com/loancrate/access-tokens#readme","bugs":{"url":"https://github.com/loancrate/access-tokens/issues"},"dist":{"shasum":"b0f9b22d6695fbe3e196f600c0c0f84708e68010","tarball":"https://registry.npmjs.org/@access-tokens/express/-/express-1.0.0.tgz","fileCount":43,"integrity":"sha512-ZGe+DvOZoPigv2KOggHIC8bVRfk8l7bdj2YCj09FAxO8c7EcSehE25tlpA8FxPi3Pu9NrCWHtl+CAvgG9yJGtQ==","signatures":[{"sig":"MEUCIQD1uaeZ2BzoCUwHRU2l0RTMNIG9zXCBGqEEwXxKzrYDlQIgJKatVFuHt8KPCOqHIJgyt2rHay+inTmiP4/gVkydVEg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@access-tokens%2fexpress@1.0.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":65134},"main":"./dist/index.js","_from":"file:access-tokens-express-1.0.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=20.0.0"},"scripts":{"lint":"eslint src","test":"node --experimental-vm-modules node_modules/jest/bin/jest.js","build":"tsc --build tsconfig.build.json","clean":"rm -rf dist *.tsbuildinfo coverage *-junit.xml","genkey":"tsx src/tools/genkey.ts","typecheck":"tsc --noEmit"},"_npmUser":{"name":"trevorr","email":"trevor@scurrilous.com"},"_resolved":"/tmp/7a5d488c48341945898cde601c9cbe4c/access-tokens-express-1.0.0.tgz","_integrity":"sha512-ZGe+DvOZoPigv2KOggHIC8bVRfk8l7bdj2YCj09FAxO8c7EcSehE25tlpA8FxPi3Pu9NrCWHtl+CAvgG9yJGtQ==","repository":{"url":"git+https://github.com/loancrate/access-tokens.git","type":"git","directory":"packages/express"},"_npmVersion":"11.6.1","description":"Express routes and middleware for personal access token authentication","directories":{},"_nodeVersion":"24.11.0","dependencies":{"zod":"^4.1.12","id62":"^2.0.0","jose":"^6.1.0","pino":"^10.1.0","http-errors":"^2.0.0","catch-unknown":"^2.0.0","@access-tokens/core":"1.0.0","express-async-handler":"^1.2.0"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.20.6","jest":"^30.2.0","eslint":"^9.38.0","express":"^5.1.0","ts-jest":"^29.4.5","supertest":"^7.1.4","jest-junit":"^16.0.0","typescript":"^5.9.3","@types/jest":"^30.0.0","@types/node":"^24.9.2","@jest/globals":"^30.2.0","@types/express":"^5.0.5","@types/supertest":"^6.0.3","@types/http-errors":"^2.0.5","@access-tokens/jest-config":"1.0.0"},"peerDependencies":{"express":"^4.18.0 || ^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/express_1.0.0_1762480580902_0.7282597625842973","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@access-tokens/express","version":"1.0.1","keywords":["dynamodb","personal-access-token","pat","authentication","express","middleware","jwt"],"author":{"name":"Trevor Robinson"},"license":"ISC","_id":"@access-tokens/express@1.0.1","maintainers":[{"name":"trevorr","email":"trevor@scurrilous.com"},{"name":"andrew-loancrate","email":"andrew@loancrate.com"}],"homepage":"https://github.com/loancrate/access-tokens#readme","bugs":{"url":"https://github.com/loancrate/access-tokens/issues"},"dist":{"shasum":"8640021c6661486c855974b4e67056d1d26140f6","tarball":"https://registry.npmjs.org/@access-tokens/express/-/express-1.0.1.tgz","fileCount":25,"integrity":"sha512-7oBXh1kIcOPxXU0iMoSZjJX9wGbdmDV9HPm3ngDD2xUPrLc+IFEHUPI/9BfHuCIj4Efrc3buM6LowXxgmvLJQw==","signatures":[{"sig":"MEYCIQCSuaRZFy70KxjQ2CQCe8Ys39/fWcOMNz8HJO+v+S6frwIhAMRwr2qaietqHEA72W/FtfCu/ML7bCj/kdDw/njW/d2j","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@access-tokens%2fexpress@1.0.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":235127},"main":"./dist/index.js","_from":"file:access-tokens-express-1.0.1.tgz","types":"./dist/index.d.ts","engines":{"node":">=20.0.0"},"scripts":{"lint":"eslint src","test":"node --experimental-vm-modules node_modules/jest/bin/jest.js","build":"node esbuild.config.mjs && tsc --project tsconfig.build.json --emitDeclarationOnly","clean":"rm -rf dist *.tsbuildinfo coverage *-junit.xml","genkey":"tsx src/tools/genkey.ts","typecheck":"tsc --noEmit","test-smoke":"jest --config jest-smoke.config.js"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:88addce9-4f44-4c5b-86cf-319b01f64ff0"}},"_resolved":"/tmp/28cb9b4e8b8d861b82856134a97a674a/access-tokens-express-1.0.1.tgz","_integrity":"sha512-7oBXh1kIcOPxXU0iMoSZjJX9wGbdmDV9HPm3ngDD2xUPrLc+IFEHUPI/9BfHuCIj4Efrc3buM6LowXxgmvLJQw==","repository":{"url":"git+https://github.com/loancrate/access-tokens.git","type":"git","directory":"packages/express"},"_npmVersion":"11.6.1","description":"Express routes and middleware for personal access token authentication","directories":{},"_nodeVersion":"24.11.0","dependencies":{"zod":"^4.1.12","id62":"^2.0.0","jose":"^6.1.0","pino":"^10.1.0","http-errors":"^2.0.0","catch-unknown":"^2.0.0","@access-tokens/core":"1.0.0","express-async-handler":"^1.2.0"},"_hasShrinkwrap":false,"devDependencies":{"tsx":"^4.20.6","jest":"^30.2.0","eslint":"^9.38.0","esbuild":"^0.25.12","express":"^5.1.0","ts-jest":"^29.4.5","supertest":"^7.1.4","jest-junit":"^16.0.0","typescript":"^5.9.3","@types/jest":"^30.0.0","@types/node":"^24.9.2","@jest/globals":"^30.2.0","@types/express":"^5.0.5","@types/supertest":"^6.0.3","@types/http-errors":"^2.0.5","@access-tokens/jest-config":"1.0.1"},"peerDependencies":{"express":"^4.18.0 || ^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/express_1.0.1_1762583068508_0.6115640915734619","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@access-tokens/express","version":"1.1.0","description":"Express routes and middleware for personal access token authentication","keywords":["dynamodb","personal-access-token","pat","authentication","express","middleware","jwt"],"repository":{"type":"git","url":"git+https://github.com/loancrate/access-tokens.git","directory":"packages/express"},"license":"ISC","author":{"name":"Trevor Robinson"},"main":"./dist/index.js","types":"./dist/index.d.ts","dependencies":{"catch-unknown":"^2.0.0","express-async-handler":"^1.2.0","http-errors":"^2.0.0","id62":"^2.0.0","jose":"^6.1.0","pino":"^10.1.0","zod":"^4.1.12","@access-tokens/core":"1.1.0"},"devDependencies":{"@jest/globals":"^30.2.0","@types/express":"^5.0.5","@types/http-errors":"^2.0.5","@types/jest":"^30.0.0","@types/node":"^24.9.2","@types/supertest":"^6.0.3","esbuild":"^0.25.12","eslint":"^9.38.0","express":"^5.1.0","jest":"^30.2.0","jest-junit":"^16.0.0","supertest":"^7.1.4","ts-jest":"^29.4.5","tsx":"^4.20.6","typescript":"^5.9.3","@access-tokens/jest-config":"1.0.1"},"peerDependencies":{"express":"^4.18.0 || ^5.0.0"},"engines":{"node":">=20.0.0"},"scripts":{"build":"node esbuild.config.mjs && tsc --project tsconfig.build.json --emitDeclarationOnly","clean":"rm -rf dist *.tsbuildinfo coverage *-junit.xml","genkey":"tsx src/tools/genkey.ts","lint":"eslint src","test":"node --experimental-vm-modules node_modules/jest/bin/jest.js","test-smoke":"jest --config jest-smoke.config.js","typecheck":"tsc --noEmit"},"_id":"@access-tokens/express@1.1.0","bugs":{"url":"https://github.com/loancrate/access-tokens/issues"},"homepage":"https://github.com/loancrate/access-tokens#readme","_integrity":"sha512-HWE4fXA41TJl1iAz80vCcE50FibSowo1DvGkPGMAWVBGXNeVS26HKrpwXIIlOlC2aSNxwyFLRU8ygccFRWdLSg==","_resolved":"/tmp/7092f24dc684ec0f7f2027051a5b1dbf/access-tokens-express-1.1.0.tgz","_from":"file:access-tokens-express-1.1.0.tgz","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-HWE4fXA41TJl1iAz80vCcE50FibSowo1DvGkPGMAWVBGXNeVS26HKrpwXIIlOlC2aSNxwyFLRU8ygccFRWdLSg==","shasum":"d68cbca10cb04f10384fc2985b6cff3c3728989f","tarball":"https://registry.npmjs.org/@access-tokens/express/-/express-1.1.0.tgz","fileCount":27,"unpackedSize":241412,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@access-tokens%2fexpress@1.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQD1K4Sz768NO1IwW0AJwtDuXH9iHq1Dp5n2X61m1+GBZwIgWc88gs/SAHOxvt9H/3ap4iTY5bmqsSBfjy0mexDD3cA="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:88addce9-4f44-4c5b-86cf-319b01f64ff0"}},"directories":{},"maintainers":[{"name":"trevorr","email":"trevor@scurrilous.com"},{"name":"andrew-loancrate","email":"andrew@loancrate.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/express_1.1.0_1767127271031_0.24304814293547317"},"_hasShrinkwrap":false}},"time":{"created":"2025-11-07T01:56:20.803Z","modified":"2025-12-30T20:41:11.594Z","1.0.0":"2025-11-07T01:56:21.143Z","1.0.1":"2025-11-08T06:24:28.717Z","1.1.0":"2025-12-30T20:41:11.187Z"},"bugs":{"url":"https://github.com/loancrate/access-tokens/issues"},"author":{"name":"Trevor Robinson"},"license":"ISC","homepage":"https://github.com/loancrate/access-tokens#readme","keywords":["dynamodb","personal-access-token","pat","authentication","express","middleware","jwt"],"repository":{"type":"git","url":"git+https://github.com/loancrate/access-tokens.git","directory":"packages/express"},"description":"Express routes and middleware for personal access token authentication","maintainers":[{"name":"trevorr","email":"trevor@scurrilous.com"},{"name":"andrew-loancrate","email":"andrew@loancrate.com"}],"readme":"# @access-tokens/express\n\n[![npm](https://img.shields.io/npm/v/@access-tokens/express)](https://www.npmjs.com/package/@access-tokens/express)\n[![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)\n\nExpress routes and middleware for Personal Access Token (PAT) authentication with OAuth 2.0-compatible JWT token exchange.\n\n## Features\n\n- **Ready-to-Use Routes**: Pre-built authentication and admin token management endpoints\n- **JWT Token Exchange**: OAuth 2.0-compatible token endpoint for PAT-to-JWT exchange\n- **Express Middleware**: `requireJwt`, `requireAdmin`, and `requireRole` middleware for route protection\n- **JOSE Integration**: Industry-standard JWT signing and verification\n- **TypeScript**: Full type safety with Express request augmentation\n- **Flexible Configuration**: Customizable paths, token lifetime, and key management\n\n## Installation\n\n```bash\nnpm install @access-tokens/express @access-tokens/core\n```\n\n## Quick Start\n\n```typescript\nimport express from \"express\";\nimport { DynamoDBPat } from \"@access-tokens/core\";\nimport {\n  createAuthRouter,\n  createAdminTokensRouter,\n  createRequireJwt,\n  createRequireAdmin,\n  createRequireRole,\n  buildSignerVerifier,\n  generateKeySet,\n} from \"@access-tokens/express\";\nimport { DynamoDBDocumentClient } from \"@aws-sdk/lib-dynamodb\";\nimport { DynamoDBClient } from \"@aws-sdk/client-dynamodb\";\n\nconst app = express();\napp.use(express.json());\n\n// Initialize DynamoDB\nconst dynamoClient = new DynamoDBClient({ region: \"us-east-1\" });\nconst docClient = DynamoDBDocumentClient.from(dynamoClient);\nconst pat = new DynamoDBPat({ tableName: \"tokens\", docClient });\n\n// Generate JWT signing keys\nconst keySet = await generateKeySet(\"my-key-id-1\");\nconst signerVerifier = await buildSignerVerifier({\n  keySet,\n  issuer: \"my-app\",\n  ttl: \"1h\",\n});\n\n// Add authentication and token admin routes\napp.use(\"/auth\", createAuthRouter({ pat, signerVerifier }));\napp.use(\"/admin\", createAdminTokensRouter({ pat, signerVerifier }));\n\nconst requireJwt = createRequireJwt({ signerVerifier });\nconst requireAdmin = createRequireAdmin();\nconst requireEditor = createRequireRole({ role: \"editor\" });\n\n// Your protected routes\napp.get(\"/api/data\", requireJwt, (req, res) => {\n  res.json({\n    message: \"User data\",\n    user: req.user, // { sub, owner, admin, roles }\n  });\n});\n\napp.get(\"/api/admin/data\", requireJwt, requireAdmin, (req, res) => {\n  res.json({\n    message: \"Admin data\",\n    user: req.user, // { sub, owner, admin, roles }\n  });\n});\n\napp.put(\"/api/content\", requireJwt, requireEditor, (req, res) => {\n  res.json({\n    message: \"Content updated\",\n    user: req.user, // { sub, owner, admin, roles }\n  });\n});\n\napp.listen(3000, () => console.log(\"Server running on port 3000\"));\n```\n\n## API Reference\n\n### Routes\n\n#### `createAuthRouter(options)`\n\nCreates an Express router with authentication endpoints.\n\n**Options:**\n\n```typescript\n{\n  pat: DynamoDBPat;                    // DynamoDBPat instance\n  signerVerifier: JwtSignerVerifier;   // JWT signer/verifier from buildSignerVerifier()\n  logger?: pino.Logger;                // Optional logger\n}\n```\n\n**Note:** JWT lifetime (TTL) is configured in `buildSignerVerifier()`.\n\n**Endpoints:**\n\n- `POST /token` - Exchange PAT for JWT (OAuth 2.0 token endpoint)\n\n  **Request Body:**\n\n  ```typescript\n  {\n    grant_type?: \"client_credentials\";  // Optional, must be \"client_credentials\" if provided\n    client_secret?: string;             // PAT (for OAuth 2.0 client_secret_post method)\n    client_id?: string;                 // Optional, accepted but not used\n    state?: string;                     // Optional, echoed back in response\n  }\n  ```\n\n  **Authentication Methods** (checked in this order):\n  1. **Body parameter** (OAuth 2.0 `client_secret_post`): Include `client_secret` in request body\n  2. **Basic authentication** (OAuth 2.0 `client_secret_basic`): Use `Authorization: Basic <base64>` header (format: `Basic base64(\":<token>\")`)\n  3. **Bearer token**: Use `Authorization: Bearer <token>` header\n\n  **Response:**\n\n  ```json\n  {\n    \"access_token\": \"eyJ...\", // The signed JWT\n    \"token_type\": \"Bearer\", // Always \"Bearer\"\n    \"expires_in\": 3600, // JWT lifetime in seconds\n    \"state\": \"...\" // Optional, echoed from request\n  }\n  ```\n\n**Examples:**\n\n```bash\n# Method 1: Body parameter (client_secret_post)\ncurl -X POST http://localhost:3000/auth/token \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"grant_type\":\"client_credentials\",\"client_secret\":\"pat_abc123...\"}'\n\n# Method 2: Basic authentication (client_secret_basic)\ncurl -X POST http://localhost:3000/auth/token \\\n  -H \"Authorization: Basic $(echo -n \":pat_abc123...\" | base64)\"\n\n# Method 3: Bearer token (non-OAuth)\ncurl -X POST http://localhost:3000/auth/token \\\n  -H \"Authorization: Bearer pat_abc123...\"\n```\n\n#### `createAdminTokensRouter(options)`\n\nCreates an Express router with admin token management endpoints. Requires JWT authentication and admin privileges.\n\n**Options:**\n\n```typescript\n{\n  pat: DynamoDBPat;                              // DynamoDBPat instance\n  signerVerifier: JwtSignerVerifier;             // JWT signer/verifier\n  logger?: pino.Logger;                          // Optional logger\n}\n```\n\n**Endpoints:**\n\n- `GET /tokens` - List tokens\n  - **Query Params:** `afterTokenId`, `limit`, `includeRevoked`, `includeExpired`, `includeSecretPhc`, `hasRole`\n  - **Response:** `{ \"records\": [...] }`\n\n- `POST /tokens` - Issue a new token\n  - **Request Body:** `{ \"owner\": \"user@example.com\", \"isAdmin\"?: false, \"roles\"?: [\"reader\"], \"tokenId\"?: \"...\", \"expiresAt\"?: 1234567890 }`\n  - **Response:** `{ \"token\": \"pat_...\", \"record\": {...} }`\n\n- `PUT /tokens/:tokenId` - Register pre-generated token\n  - **Request Body:** `{ \"secretPhc\": \"...\", \"owner\": \"...\", \"isAdmin\"?: false, \"roles\"?: [\"reader\"], \"expiresAt\"?: 1234567890 }`\n  - **Response:** `{ \"record\": {...} }`\n\n- `PATCH /tokens/:tokenId` - Update token\n  - **Request Body:** `{ \"owner\"?: \"...\", \"isAdmin\"?: true, \"secretPhc\"?: \"...\", \"roles\"?: ..., \"expiresAt\"?: 1234567890 }`\n  - **Roles Update Syntax:**\n    ```json\n    { \"roles\": [\"role1\", \"role2\"] }     // Replace all roles\n    { \"roles\": { \"add\": [\"admin\"] } }   // Atomic add (cannot combine with remove)\n    { \"roles\": { \"remove\": [\"guest\"] } } // Atomic remove (cannot combine with add)\n    ```\n  - **Response:** 204 No Content\n\n- `PUT /tokens/:tokenId/revoke` - Revoke token\n  - **Request Body:** `{ \"expiresAt\"?: 1234567890 }` (optional)\n  - **Response:** 204 No Content\n\n- `PUT /tokens/:tokenId/restore` - Restore revoked token\n  - **Response:** 204 No Content\n\n- `POST /tokens/batch` - Batch retrieve tokens\n  - **Request Body:** `{ \"tokenIds\": [\"id1\", \"id2\"], \"includeSecretPhc\"?: false }`\n  - **Response:** `{ \"found\": [...], \"missing\": [...] }`\n\n**Note:** All endpoints require JWT authentication and admin privileges. They\nuse `requireJwt` and `requireAdmin` middleware internally.\n\n### Middleware\n\n#### `createRequireJwt(options)`\n\nCreates middleware that validates JWT tokens and populates `req.user`.\n\n**Options:**\n\n```typescript\n{\n  signerVerifier: JwtSignerVerifier;   // JWT signer/verifier from buildSignerVerifier()\n  logger?: pino.Logger;                // Optional logger\n}\n```\n\n**Request Extension:**\n\n```typescript\nreq.user = {\n  sub: string;      // Token ID\n  owner: string;    // Token owner\n  admin: boolean;   // Admin status\n  roles: string[];  // Array of role strings\n};\n```\n\n**Note:** The `roles` array comes from the JWT payload and reflects the roles assigned to the token at the time the JWT was issued.\n\n**Usage:**\n\n```typescript\nconst requireJwt = createRequireJwt({ signerVerifier });\n\napp.get(\"/protected\", requireJwt, (req, res) => {\n  console.log(\"User:\", req.user?.owner);\n  res.json({ data: \"secret\" });\n});\n```\n\n#### `createRequireAdmin(options?)`\n\nCreates middleware that requires `req.user.admin` to be `true`. Must be used after `requireJwt`.\n\n**Options:**\n\n```typescript\n{\n  logger?: pino.Logger;  // Optional logger\n}\n```\n\n**Usage:**\n\n```typescript\nconst requireAdmin = createRequireAdmin();\n\napp.delete(\"/users/:id\", requireJwt, requireAdmin, (req, res) => {\n  // Only admin users can access this\n  res.json({ success: true });\n});\n```\n\n#### `createRequireRole(options)`\n\nCreates middleware that requires `req.user.roles` to include a specific role. Must be used after `requireJwt`.\n\n**Options:**\n\n```typescript\n{\n  role: string;            // Required role name\n  logger?: pino.Logger;    // Optional logger\n}\n```\n\n**Usage:**\n\n```typescript\nconst requireEditor = createRequireRole({ role: \"editor\" });\n\napp.put(\"/content/:id\", requireJwt, requireEditor, (req, res) => {\n  // Only users with \"editor\" role can access this\n  res.json({ success: true });\n});\n```\n\n### JWT Utilities\n\n#### `generateKeySet(kid: string, algorithm?: \"EdDSA\" | \"RS256\")`\n\nGenerates a new asymmetric key set for JWT signing.\n\n**Parameters:**\n\n- `kid: string` - Key ID (required) - unique identifier for this key set\n- `algorithm?: \"EdDSA\" | \"RS256\"` - Signing algorithm (default: \"EdDSA\")\n\n**Returns:**\n\n```typescript\n{\n  active_kid: string;    // The active key ID\n  private_keys: JWK[];   // Array of private keys in JWK format\n  public_keys: JWK[];    // Array of public keys in JWK format\n}\n```\n\n**Example:**\n\n```typescript\nconst keySet = await generateKeySet(\"my-key-id-1\");\n// or with specific algorithm\nconst rsaKeySet = await generateKeySet(\"rsa-key-1\", \"RS256\");\n```\n\n**Note:** Store keys securely (e.g., AWS Secrets Manager, environment variables). Generate once and reuse.\n\n#### `buildSignerVerifier(config)`\n\nCreates JWT signer and verifier from a key set.\n\n**Config:**\n\n```typescript\n{\n  keySet: KeySet; // From generateKeySet()\n  issuer: string; // JWT issuer claim\n  ttl: string; // Token time-to-live (e.g., \"1h\", \"30m\")\n}\n```\n\n**Returns:**\n\n```typescript\nJwtSignerVerifier {\n  sign: (claims) => Promise<string>;\n  verify: (jws: string) => Promise<JWTVerifyResult>;\n  jwks: { keys: readonly JWK[] };\n}\n```\n\n**Example:**\n\n```typescript\nconst keySet = await generateKeySet(\"my-key-id\");\nconst signerVerifier = await buildSignerVerifier({\n  keySet,\n  issuer: \"my-app\",\n  ttl: \"1h\",\n});\n```\n\n## OAuth 2.0 Flow\n\nThis library implements a simplified OAuth 2.0 client credentials flow:\n\n1. **Client authenticates** with PAT to `POST /auth/token`\n2. **Server validates** PAT and issues short-lived JWT (default: 1 hour)\n3. **Client uses JWT** for subsequent API requests via `Authorization: Bearer <jwt>`\n4. **Server validates JWT** using `requireJwt` middleware\n\n### Why JWT Exchange?\n\n- **Performance**: Avoid DynamoDB lookup and scrypt on every request\n- **Scalability**: Stateless JWT verification\n- **Short-lived**: Reduced risk if JWT is compromised\n- **Standard**: OAuth 2.0 compatible\n\n## Security Best Practices\n\n1. **Use HTTPS** - Always use TLS in production\n2. **Secure Key Storage** - Store private keys in secure vaults (AWS Secrets Manager, etc.)\n3. **Short JWT Lifetime** - Default 1 hour is recommended\n4. **Rotate Keys** - Implement key rotation for long-running services\n5. **Validate Issuer/Audience** - Configure these in production\n6. **Rate Limiting** - Add rate limiting to `/auth/token` endpoint\n\n## Key Generation\n\nGenerate keys using the included tool:\n\n```bash\npnpm --filter @access-tokens/express genkey\n```\n\nOr programmatically:\n\n```typescript\nimport { generateKeySet } from \"@access-tokens/express\";\n\nconst keys = await generateKeySet(\"my-key-id-1\");\nconsole.log(\"Public Key:\", keys.public_keys);\nconsole.log(\"Private Key:\", keys.private_keys);\n```\n\nStore these keys securely and pass them to your application via environment variables.\n\n## Error Handling\n\nAll endpoints return standard HTTP error codes:\n\n- `400 Bad Request` - Invalid request body or parameters\n- `401 Unauthorized` - Invalid or missing token\n- `403 Forbidden` - Insufficient permissions (not admin)\n- `404 Not Found` - Token not found\n- `500 Internal Server Error` - Server error\n\nExample error response:\n\n```json\n{\n  \"error\": \"Invalid token\",\n  \"details\": \"Token has been revoked\"\n}\n```\n\n## TypeScript Types\n\nThe package extends Express types:\n\n```typescript\ndeclare global {\n  namespace Express {\n    interface Request {\n      user?: {\n        sub: string; // Token ID\n        owner: string; // Token owner\n        admin: boolean; // Admin status\n        roles: string[]; // Array of role strings\n      };\n      logger?: Logger; // Optional Pino logger\n      clientIp?: string; // Optional client IP (from request-ip)\n    }\n  }\n}\n```\n\n## Requirements\n\n- Node.js 20+\n- Express 4.18+ or 5.0+\n- @access-tokens/core\n\n## Related Packages\n\n- [@access-tokens/core](https://www.npmjs.com/package/@access-tokens/core) - Core token management library\n- [@access-tokens/client](https://www.npmjs.com/package/@access-tokens/client) - HTTP client for PAT API\n- [@access-tokens/cli](https://www.npmjs.com/package/@access-tokens/cli) - Command-line token management\n\n## License\n\n[ISC](https://opensource.org/licenses/ISC) © 2025 Loan Crate, Inc.\n\n## Links\n\n- [GitHub Repository](https://github.com/loancrate/access-tokens)\n- [npm Package](https://www.npmjs.com/package/@access-tokens/express)\n- [Documentation](https://github.com/loancrate/access-tokens#readme)\n","readmeFilename":"README.md"}