{"id":32694202,"url":"https://github.com/izadoesdev/keypal","last_synced_at":"2026-07-09T16:01:30.970Z","repository":{"id":320374007,"uuid":"1081834395","full_name":"izadoesdev/keypal","owner":"izadoesdev","description":"A TypeScript library for secure API key management with cryptographic hashing, expiration, scopes, and pluggable storage","archived":false,"fork":false,"pushed_at":"2026-04-21T18:41:37.000Z","size":15489,"stargazers_count":203,"open_issues_count":11,"forks_count":14,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-07-07T13:29:17.347Z","etag":null,"topics":["api-keys","authentication","drizzle","hashing","keypal","permissions","redis","scopes","security","typescript"],"latest_commit_sha":null,"homepage":"https://keypal.vercel.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/izadoesdev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"ko_fi":"izadoesdev"}},"created_at":"2025-10-23T11:02:44.000Z","updated_at":"2026-07-01T09:46:42.000Z","dependencies_parsed_at":"2025-10-23T22:25:51.962Z","dependency_job_id":null,"html_url":"https://github.com/izadoesdev/keypal","commit_stats":null,"previous_names":["izadoesdev/better-api-keys","izadoesdev/keypal"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/izadoesdev/keypal","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izadoesdev%2Fkeypal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izadoesdev%2Fkeypal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izadoesdev%2Fkeypal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izadoesdev%2Fkeypal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/izadoesdev","download_url":"https://codeload.github.com/izadoesdev/keypal/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/izadoesdev%2Fkeypal/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35304875,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-09T02:00:07.329Z","response_time":57,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api-keys","authentication","drizzle","hashing","keypal","permissions","redis","scopes","security","typescript"],"created_at":"2025-11-01T17:02:15.725Z","updated_at":"2026-07-09T16:01:30.964Z","avatar_url":"https://github.com/izadoesdev.png","language":"TypeScript","funding_links":["https://ko-fi.com/izadoesdev"],"categories":["TypeScript"],"sub_categories":[],"readme":"# keypal\n\n[![Test](https://github.com/izadoesdev/keypal/actions/workflows/test.yml/badge.svg)](https://github.com/izadoesdev/keypal/actions/workflows/test.yml)\n[![Benchmark](https://github.com/izadoesdev/keypal/actions/workflows/benchmark.yml/badge.svg)](https://github.com/izadoesdev/keypal/actions/workflows/benchmark.yml)\n[![npm version](https://badge.fury.io/js/keypal.svg)](https://badge.fury.io/js/keypal)\n\nA TypeScript library for secure API key management with cryptographic hashing, expiration, scopes, and pluggable storage.\n\n## Features\n\n- **Secure by Default**: SHA-256/SHA-512 hashing with optional salt and timing-safe comparison\n- **Smart Key Detection**: Automatically extracts keys from `Authorization`, `x-api-key`, or custom headers\n- **Built-in Caching**: Optional in-memory or Redis caching for validated keys\n- **Flexible Storage**: Memory, Redis, Drizzle ORM, Prisma, and Kysely adapters included\n- **Scope-based Permissions**: Fine-grained access control with resource-specific scopes\n- **Tags**: Organize and find keys by tags\n- **Key Management**: Enable/disable, rotate, and soft-revoke keys with audit trails\n- **Audit Logging**: Track who did what, when, and why (opt-in)\n- **TypeScript**: Full type safety\n- **Zero Config**: Works out of the box with sensible defaults\n\n## Installation\n\n```bash\nnpm install keypal\n# or\nbun add keypal\n```\n\n## Quick Start\n\n```typescript\nimport { createKeys } from 'keypal'\n\nconst keys = createKeys({\n  prefix: 'sk_',\n  cache: true,\n})\n\n// Create a key\nconst { key, record } = await keys.create({\n  ownerId: 'user_123',\n  scopes: ['read', 'write'],\n})\n\n// Verify from headers\nconst result = await keys.verify(request.headers)\nif (result.valid) {\n  console.log('Authenticated:', result.record.metadata.ownerId)\n}\n```\n\n## Configuration\n\n```typescript\nimport Redis from 'ioredis'\n\nconst redis = new Redis()\n\nconst keys = createKeys({\n  // Key generation\n  prefix: 'sk_prod_',\n  length: 32,\n  alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',\n  \n  // Security\n  algorithm: 'sha256',  // or 'sha512'\n  salt: process.env.API_KEY_SALT,\n  \n  // Storage (memory by default)\n  storage: 'redis',  // or custom Storage instance\n  redis,             // required when storage/cache is 'redis'\n  \n  // Caching\n  cache: true,       // in-memory cache\n  // cache: 'redis', // Redis cache\n  cacheTtl: 60,\n  \n  // Revocation\n  revokedKeyTtl: 604800, // TTL for revoked keys in Redis (7 days), set to 0 to keep forever\n  \n  // Usage tracking\n  autoTrackUsage: true, // Automatically update lastUsedAt on verify\n  \n  // Audit logging (opt-in)\n  auditLogs: true,  // Enable audit logging\n  auditContext: {   // Default context for all audit logs (optional)\n    userId: 'system',\n    metadata: { service: 'api' }\n  },\n  \n  // Header detection\n  headerNames: ['x-api-key', 'authorization'],\n  extractBearer: true,\n})\n```\n\n## API\n\n### Creating \u0026 Managing Keys\n\n```typescript\n// Create with plain object\nconst { key, record } = await keys.create({\n  ownerId: 'user_123',\n  name: 'Production Key',\n  description: 'Key for production API access',\n  scopes: ['read', 'write'],\n  tags: ['production', 'api'],\n  resources: {\n    'project:123': ['read', 'write'],\n    'project:456': ['read']\n  },\n  expiresAt: '2025-12-31',\n  enabled: true, // optional, defaults to true\n})\n\n// Create with ResourceBuilder (fluent API)\nimport { ResourceBuilder, createResourceBuilder } from 'keypal'\n\nconst resources = new ResourceBuilder()\n  .add('website', 'site123', ['read', 'write'])\n  .add('project', 'proj456', ['deploy'])\n  .addMany('website', ['site1', 'site2', 'site3'], ['read']) // Same scopes for multiple resources\n  .build()\n\nconst { key: key2, record: record2 } = await keys.create({\n  ownerId: 'user_123',\n  scopes: ['admin'],\n  resources, // Use the built resources object\n})\n\n// List\nconst userKeys = await keys.list('user_123')\n\n// Find by tag\nconst taggedKeys = await keys.findByTag('production')\nconst multiTagKeys = await keys.findByTags(['production', 'api'])\n\n// Find by ID or hash\nconst keyRecord = await keys.findById(record.id)\nconst keyByHash = await keys.findByHash(record.keyHash)\n\n// Enable/Disable\nawait keys.enable(record.id)\nawait keys.disable(record.id)\n\n// Rotate (create new key, mark old as revoked)\nconst { key: newKey, record: newRecord, oldRecord } = await keys.rotate(record.id, {\n  name: 'Updated Key',\n  scopes: ['read', 'write', 'admin'],\n})\n\n// Revoke (soft delete - keeps record with revokedAt timestamp)\nawait keys.revoke(record.id)\nawait keys.revokeAll('user_123')\n\n// Update last used\nawait keys.updateLastUsed(record.id)\n```\n\n### Verifying Keys\n\n```typescript\n// From headers (automatic detection)\nconst result = await keys.verify(request.headers)\n\n// From string\nconst result = await keys.verify('sk_abc123')\nconst result = await keys.verify('Bearer sk_abc123')\n\n// With options\nconst result = await keys.verify(headers, {\n  headerNames: ['x-custom-key'],\n  skipCache: true,\n  skipTracking: true, // Skip updating lastUsedAt (useful when autoTrackUsage is enabled)\n})\n\n// Check result\nif (result.valid) {\n  console.log(result.record)\n} else {\n  console.log(result.error) // Human-readable error message\n  console.log(result.errorCode) // Error code for programmatic handling (see ApiKeyErrorCode)\n}\n```\n\n### Permission Checking\n\n```typescript\n// Global scope checks\nif (keys.hasScope(record, 'write')) { /* ... */ }\nif (keys.hasAnyScope(record, ['admin', 'moderator'])) { /* ... */ }\nif (keys.hasAllScopes(record, ['read', 'write'])) { /* ... */ }\nif (keys.isExpired(record)) { /* ... */ }\n\n// Resource-specific scope checks\n// Check if key has 'read' scope for a specific resource\nif (keys.checkResourceScope(record, 'website', 'site123', 'read')) { /* ... */ }\n\n// Check if key has any of the specified scopes for a resource\nif (keys.checkResourceAnyScope(record, 'website', 'site123', ['admin', 'write'])) { /* ... */ }\n\n// Check if key has all specified scopes for a resource (checks both global and resource scopes)\nif (keys.checkResourceAllScopes(record, 'website', 'site123', ['read', 'write'])) { /* ... */ }\n```\n\n### ResourceBuilder (Fluent API)\n\nBuild resource-specific scopes with a clean, chainable API:\n\n```typescript\nimport { ResourceBuilder, createResourceBuilder } from 'keypal'\n\n// Basic usage\nconst resources = new ResourceBuilder()\n  .add('website', 'site123', ['read', 'write'])\n  .add('project', 'proj456', ['deploy'])\n  .build()\n\n// Add scopes to multiple resources at once\nconst resources2 = new ResourceBuilder()\n  .addMany('website', ['site1', 'site2', 'site3'], ['read'])\n  .add('project', 'proj1', ['deploy', 'rollback'])\n  .build()\n\n// Add single scopes\nconst resources3 = new ResourceBuilder()\n  .addOne('website', 'site123', 'read')\n  .addOne('website', 'site123', 'write')\n  .build()\n\n// Modify existing resources\nconst builder = new ResourceBuilder()\n  .add('website', 'site123', ['read', 'write'])\n  .add('project', 'proj456', ['deploy'])\n\n// Check if resource exists\nif (builder.has('website', 'site123')) {\n  const scopes = builder.get('website', 'site123')\n  console.log(scopes) // ['read', 'write']\n}\n\n// Remove specific scopes\nbuilder.removeScopes('website', 'site123', ['write'])\n\n// Remove entire resource\nbuilder.remove('project', 'proj456')\n\n// Build final result\nconst finalResources = builder.build()\n\n// Start from existing resources (useful for updates)\nconst existingResources = {\n  'website:site123': ['read'],\n  'project:proj456': ['deploy']\n}\n\nconst updated = ResourceBuilder.from(existingResources)\n  .add('website', 'site123', ['write']) // Merges with existing\n  .add('team', 'team789', ['admin'])\n  .build()\n\n// Use with createKeys\nawait keys.create({\n  ownerId: 'user_123',\n  scopes: ['admin'],\n  resources: updated\n})\n```\n\n**ResourceBuilder Methods:**\n- `add(resourceType, resourceId, scopes)` - Add scopes to a resource (merges if exists)\n- `addOne(resourceType, resourceId, scope)` - Add a single scope\n- `addMany(resourceType, resourceIds, scopes)` - Add same scopes to multiple resources\n- `remove(resourceType, resourceId)` - Remove entire resource\n- `removeScopes(resourceType, resourceId, scopes)` - Remove specific scopes\n- `has(resourceType, resourceId)` - Check if resource exists\n- `get(resourceType, resourceId)` - Get scopes for a resource\n- `clear()` - Clear all resources\n- `build()` - Build and return the resources object\n- `ResourceBuilder.from(resources)` - Create from existing resources object\n\n### Usage Tracking\n\n```typescript\n// Enable automatic tracking in config\nconst keys = createKeys({\n  autoTrackUsage: true, // Automatically updates lastUsedAt on verify\n})\n\n// Manually update (always available)\nawait keys.updateLastUsed(record.id)\n\n// Skip tracking for specific requests\nconst result = await keys.verify(headers, { skipTracking: true })\n```\n\n### Audit Logging\n\nTrack all key operations with context about who performed each action:\n\n```typescript\n// Enable audit logging\nconst keys = createKeys({\n  auditLogs: true,\n  auditContext: {\n    // Default context merged into all logs\n    metadata: { environment: 'production' }\n  }\n})\n\n// Actions are automatically logged with optional context\nawait keys.create({\n  ownerId: 'user_123',\n  scopes: ['read']\n}, {\n  userId: 'admin_456',\n  ip: '192.168.1.1',\n  metadata: { reason: 'New customer onboarding' }\n})\n\nawait keys.revoke('key_123', {\n  userId: 'admin_789',\n  metadata: { reason: 'Security breach' }\n})\n\n// Query logs\nconst logs = await keys.getLogs({\n  keyId: 'key_123',\n  action: 'revoked',\n  startDate: '2025-01-01',\n  limit: 100\n})\n\n// Count logs\nconst count = await keys.countLogs({ action: 'created' })\n\n// Get statistics\nconst stats = await keys.getLogStats('user_123')\nconsole.log(stats.total)\nconsole.log(stats.byAction.created)\nconsole.log(stats.lastActivity)\n\n// Clean up old logs\nconst deleted = await keys.deleteLogs({\n  endDate: '2024-01-01'\n})\n\n// Clear logs for a specific key\nawait keys.clearLogs('key_123')\n```\n\n**Log Entry Structure:**\n\n```typescript\n{\n  id: 'log_xyz',\n  action: 'created' | 'revoked' | 'rotated' | 'enabled' | 'disabled',\n  keyId: 'key_123',\n  ownerId: 'user_456',\n  timestamp: '2025-10-25T12:00:00.000Z',\n  data: {\n    userId: 'admin_789',\n    ip: '192.168.1.1',\n    metadata: { reason: 'Security breach' }\n  }\n}\n```\n\n### Helper Methods\n\n```typescript\nkeys.hasKey(headers)              // boolean - check if headers contain an API key\nkeys.extractKey(headers)          // string | null - extract key from headers\nkeys.generateKey()                // string - generate a new key (without saving)\nkeys.hashKey(key)                 // string - hash a key (useful for custom storage)\nkeys.invalidateCache(keyHash)     // Promise\u003cvoid\u003e - manually invalidate cached key\n```\n\n### Standalone Utility Functions\n\nYou can also use these functions without a manager instance:\n\n```typescript\nimport { \n  isExpired, \n  getExpirationTime,\n  extractKeyFromHeaders,\n  hasApiKey,\n  hasScope,\n  hasAnyScope,\n  hasAllScopes,\n  ApiKeyErrorCode,\n  createApiKeyError\n} from 'keypal'\n\n// Check expiration\nconst expired = isExpired('2025-12-31T00:00:00.000Z')\nconst expirationDate = getExpirationTime('2025-12-31T00:00:00.000Z') // Date | null\n\n// Extract key from headers\nconst key = extractKeyFromHeaders(request.headers, {\n  headerNames: ['x-api-key'],\n  extractBearer: true\n})\n\n// Check if headers have API key\nif (hasApiKey(request.headers)) {\n  const key = extractKeyFromHeaders(request.headers)\n}\n\n// Check scopes (for plain scope arrays, not records)\nconst hasWrite = hasScope(['read', 'write'], 'write')\nconst hasAny = hasAnyScope(['read', 'write'], ['admin', 'write'])\nconst hasAll = hasAllScopes(['read', 'write'], ['read', 'write'])\n\n// Error handling\nif (!result.valid) {\n  switch (result.errorCode) {\n    case ApiKeyErrorCode.EXPIRED:\n      // Handle expired key\n      break\n    case ApiKeyErrorCode.REVOKED:\n      // Handle revoked key\n      break\n    case ApiKeyErrorCode.DISABLED:\n      // Handle disabled key\n      break\n    // ... other error codes\n  }\n}\n\n// Create custom errors\nconst error = createApiKeyError(ApiKeyErrorCode.INVALID_KEY, {\n  attemptedKey: 'sk_abc123'\n})\n```\n\n**Available Error Codes:**\n- `MISSING_KEY` - No API key provided\n- `INVALID_FORMAT` - API key format is invalid\n- `INVALID_KEY` - API key does not exist\n- `EXPIRED` - API key has expired\n- `REVOKED` - API key has been revoked\n- `DISABLED` - API key is disabled\n- `STORAGE_ERROR` - Storage operation failed\n- `CACHE_ERROR` - Cache operation failed\n- `ALREADY_REVOKED` - Key is already revoked\n- `ALREADY_ENABLED` - Key is already enabled\n- `ALREADY_DISABLED` - Key is already disabled\n- `CANNOT_MODIFY_REVOKED` - Cannot modify revoked key\n- `KEY_NOT_FOUND` - API key not found\n- `AUDIT_LOGGING_DISABLED` - Audit logging not enabled\n- `STORAGE_NOT_SUPPORTED` - Storage doesn't support operation\n\n## Storage Examples\n\n### Memory (Default)\n\n```typescript\nconst keys = createKeys({ prefix: 'sk_' })\n```\n\n### Redis\n\n```typescript\nimport Redis from 'ioredis'\n\nconst redis = new Redis()\n\nconst keys = createKeys({\n  prefix: 'sk_',\n  storage: 'redis',\n  cache: 'redis',\n  redis,\n})\n```\n\n### Drizzle ORM\n\n```typescript\nimport { drizzle } from 'drizzle-orm/node-postgres'\nimport { Pool } from 'pg'\nimport { DrizzleStore } from 'keypal/drizzle'\nimport { apikey } from 'keypal/drizzle/schema'\nimport { createKeys } from 'keypal'\n\nconst pool = new Pool({\n  connectionString: process.env.DATABASE_URL\n})\n\nconst db = drizzle(pool, { schema: { apikey } })\n\nconst keys = createKeys({\n  prefix: 'sk_prod_',\n  storage: new DrizzleStore({ db, table: apikey }),\n  cache: true,\n})\n```\n\n**Setup Database Schema:**\n\n```typescript\n// src/drizzle/schema.ts\nimport { index, jsonb, pgTable, text, unique } from 'drizzle-orm/pg-core'\n\nexport const apikey = pgTable(\n  'apikey',\n  {\n    id: text().primaryKey().notNull(),\n    keyHash: text('key_hash').notNull(),\n    metadata: jsonb('metadata').notNull(),\n  },\n  (table) =\u003e [\n    index('apikey_key_hash_idx').on(table.keyHash),\n    unique('apikey_key_hash_unique').on(table.keyHash),\n  ]\n)\n```\n\n**Generate migrations:**\n\n```bash\nbun run db:generate\nbun run db:push\n```\n\n**Use Drizzle Studio:**\n\n```bash\nbun run studio\n```\n\n### Prisma\n\n```typescript\nimport { PrismaClient } from '@prisma/client'\nimport { PrismaStore } from 'keypal/prisma'\nimport { createKeys } from 'keypal'\n\nconst prisma = new PrismaClient()\n\nconst keys = createKeys({\n  prefix: 'sk_prod_',\n  storage: new PrismaStore({ prisma, model: 'apiKey' }),\n  cache: true,\n})\n```\n\n**Setup Prisma Schema:**\n\n```prisma\nmodel ApiKey {\n  id       String @id @default(cuid())\n  keyHash  String @unique\n  metadata Json\n\n  @@index([keyHash])\n  @@map(\"api_keys\")\n}\n```\n\n### Kysely\n\n```typescript\nimport { Kysely, PostgresDialect } from 'kysely'\nimport { Pool } from 'pg'\nimport { KyselyStore } from 'keypal/kysely'\nimport { createKeys } from 'keypal'\n\nconst db = new Kysely({\n  dialect: new PostgresDialect({\n    pool: new Pool({\n      connectionString: process.env.DATABASE_URL\n    })\n  })\n})\n\nconst keys = createKeys({\n  prefix: 'sk_prod_',\n  storage: new KyselyStore({ db, tableName: 'api_keys' }),\n  cache: true,\n})\n```\n\n**Setup Database Schema (PostgreSQL):**\n\n```sql\nCREATE TABLE api_keys (\n  id TEXT PRIMARY KEY,\n  \"keyHash\" TEXT UNIQUE NOT NULL,\n  metadata JSONB NOT NULL\n);\n\nCREATE INDEX api_keys_key_hash_idx ON api_keys(\"keyHash\");\n```\n\n\u003e **Note**: The column uses camelCase (`keyHash`) by default. For MySQL, omit the quotes around `keyHash`.\n\n### Custom Storage\n\n```typescript\nimport { type Storage } from 'keypal'\n\nconst customStorage: Storage = {\n  save: async (record) =\u003e { /* ... */ },\n  findByHash: async (keyHash) =\u003e { /* ... */ },\n  findById: async (id) =\u003e { /* ... */ },\n  findByOwner: async (ownerId) =\u003e { /* ... */ },\n  findByTag: async (tag, ownerId) =\u003e { /* ... */ },\n  findByTags: async (tags, ownerId) =\u003e { /* ... */ },\n  updateMetadata: async (id, metadata) =\u003e { /* ... */ },\n  delete: async (id) =\u003e { /* ... */ },\n  deleteByOwner: async (ownerId) =\u003e { /* ... */ },\n}\n\nconst keys = createKeys({\n  storage: customStorage,\n})\n```\n\n## Error Handling Best Practices\n\n### Comprehensive Error Handling\n\n```typescript\nimport { createKeys, ApiKeyErrorCode } from 'keypal'\n\nconst keys = createKeys({\n  prefix: 'sk_',\n  storage: 'redis',\n  redis,\n})\n\n// Verify with comprehensive error handling\nconst result = await keys.verify(request.headers)\n\nif (!result.valid) {\n  switch (result.errorCode) {\n    case ApiKeyErrorCode.MISSING_KEY:\n      return { error: 'API key is required', statusCode: 401 }\n    \n    case ApiKeyErrorCode.INVALID_FORMAT:\n      return { error: 'Invalid API key format', statusCode: 401 }\n    \n    case ApiKeyErrorCode.INVALID_KEY:\n      return { error: 'Invalid API key', statusCode: 401 }\n    \n    case ApiKeyErrorCode.EXPIRED:\n      return { error: 'API key has expired', statusCode: 401 }\n    \n    case ApiKeyErrorCode.REVOKED:\n      return { error: 'API key has been revoked', statusCode: 401 }\n    \n    case ApiKeyErrorCode.DISABLED:\n      return { error: 'API key is disabled', statusCode: 403 }\n    \n    default:\n      return { error: 'Authentication failed', statusCode: 401 }\n  }\n}\n\n// Key is valid, proceed with request\nconsole.log('Authenticated user:', result.record.metadata.ownerId)\n```\n\n### Handling Storage Errors\n\n```typescript\nimport { createKeys, createApiKeyError, ApiKeyErrorCode } from 'keypal'\n\ntry {\n  // Create a key\n  const { key, record } = await keys.create({\n    ownerId: 'user_123',\n    scopes: ['read', 'write'],\n  })\n  \n  return { success: true, key, keyId: record.id }\n} catch (error) {\n  console.error('Failed to create API key:', error)\n  \n  // Handle specific error types\n  if (error instanceof Error) {\n    if (error.message.includes('duplicate')) {\n      return { success: false, error: 'Duplicate key detected' }\n    }\n    if (error.message.includes('connection')) {\n      return { success: false, error: 'Database connection failed' }\n    }\n  }\n  \n  return { success: false, error: 'Failed to create API key' }\n}\n```\n\n### Handling Key Operations\n\n```typescript\n// Revoke with error handling\ntry {\n  await keys.revoke(keyId, {\n    userId: 'admin_123',\n    metadata: { reason: 'User request' }\n  })\n} catch (error) {\n  if (error.code === ApiKeyErrorCode.KEY_NOT_FOUND) {\n    return { error: 'Key not found', statusCode: 404 }\n  }\n  if (error.code === ApiKeyErrorCode.ALREADY_REVOKED) {\n    return { error: 'Key is already revoked', statusCode: 400 }\n  }\n  throw error // Re-throw unexpected errors\n}\n\n// Enable/Disable with error handling\ntry {\n  await keys.enable(keyId)\n} catch (error) {\n  if (error.code === ApiKeyErrorCode.KEY_NOT_FOUND) {\n    return { error: 'Key not found', statusCode: 404 }\n  }\n  if (error.code === ApiKeyErrorCode.ALREADY_ENABLED) {\n    return { message: 'Key was already enabled', statusCode: 200 }\n  }\n  if (error.code === ApiKeyErrorCode.CANNOT_MODIFY_REVOKED) {\n    return { error: 'Cannot enable a revoked key', statusCode: 400 }\n  }\n  throw error\n}\n\n// Rotate with error handling\ntry {\n  const { key: newKey, record, oldRecord } = await keys.rotate(keyId, {\n    scopes: ['read', 'write', 'admin'],\n  })\n  return { success: true, key: newKey, keyId: record.id }\n} catch (error) {\n  if (error.code === ApiKeyErrorCode.KEY_NOT_FOUND) {\n    return { error: 'Key not found', statusCode: 404 }\n  }\n  if (error.code === ApiKeyErrorCode.CANNOT_MODIFY_REVOKED) {\n    return { error: 'Cannot rotate a revoked key', statusCode: 400 }\n  }\n  throw error\n}\n```\n\n### Drizzle Storage Error Handling\n\n```typescript\nimport { DrizzleStore } from 'keypal/drizzle'\nimport { drizzle } from 'drizzle-orm/node-postgres'\nimport { Pool } from 'pg'\nimport { apikey } from 'keypal/drizzle/schema'\n\n// Initialize with connection error handling\nlet pool: Pool\nlet store: DrizzleStore\n\ntry {\n  pool = new Pool({\n    connectionString: process.env.DATABASE_URL,\n    // Connection pool settings\n    max: 20,\n    idleTimeoutMillis: 30000,\n    connectionTimeoutMillis: 2000,\n  })\n\n  // Test connection\n  await pool.query('SELECT 1')\n  \n  const db = drizzle(pool, { schema: { apikey } })\n  store = new DrizzleStore({ db, table: apikey })\n  \n  console.log('Database connection established')\n} catch (error) {\n  console.error('Failed to connect to database:', error)\n  throw new Error('Database initialization failed')\n}\n\nconst keys = createKeys({\n  prefix: 'sk_',\n  storage: store,\n  cache: true,\n})\n\n// Update metadata with error handling\ntry {\n  await store.updateMetadata(keyId, {\n    name: 'Updated Key',\n    scopes: ['admin'],\n  })\n} catch (error) {\n  if (error.message.includes('not found')) {\n    return { error: 'Key not found', statusCode: 404 }\n  }\n  console.error('Failed to update key metadata:', error)\n  throw error\n}\n\n// Handle duplicate key errors\ntry {\n  await keys.create({\n    ownerId: 'user_123',\n    name: 'My Key',\n  })\n} catch (error) {\n  // PostgreSQL duplicate key error\n  if (error.code === '23505') {\n    return { error: 'Duplicate key detected', statusCode: 409 }\n  }\n  throw error\n}\n\n// Graceful shutdown\nprocess.on('SIGTERM', async () =\u003e {\n  await pool.end()\n  console.log('Database connection closed')\n})\n```\n\n## Framework Example (Hono)\n\n```typescript\nimport { Hono } from 'hono'\nimport { createKeys, ApiKeyErrorCode } from 'keypal'\nimport Redis from 'ioredis'\n\nconst redis = new Redis()\n\nconst keys = createKeys({\n  prefix: 'sk_',\n  storage: 'redis',\n  cache: 'redis',\n  redis,\n  auditLogs: true,\n})\n\nconst app = new Hono()\n\n// Authentication middleware with comprehensive error handling\napp.use('/api/*', async (c, next) =\u003e {\n  const result = await keys.verify(c.req.raw.headers)\n  \n  if (!result.valid) {\n    // Log failed authentication attempts\n    console.warn('Authentication failed:', {\n      error: result.error,\n      errorCode: result.errorCode,\n      path: c.req.path,\n      ip: c.req.header('x-forwarded-for'),\n    })\n    \n    // Return appropriate error response\n    const statusCode = result.errorCode === ApiKeyErrorCode.DISABLED ? 403 : 401\n    return c.json({ error: result.error, code: result.errorCode }, statusCode)\n  }\n\n  // Store record in context for downstream handlers\n  c.set('apiKey', result.record)\n  \n  // Track usage (fire and forget)\n  if (result.record) {\n    keys.updateLastUsed(result.record.id).catch((err) =\u003e {\n      console.error('Failed to update lastUsedAt:', err)\n    })\n  }\n  \n  await next()\n})\n\n// Protected route with scope check\napp.get('/api/data', async (c) =\u003e {\n  const record = c.get('apiKey')\n  \n  if (!keys.hasScope(record, 'read')) {\n    return c.json({ error: 'Insufficient permissions' }, 403)\n  }\n\n  return c.json({ data: 'sensitive data' })\n})\n\n// Resource-specific scope check\napp.get('/api/projects/:id', async (c) =\u003e {\n  const record = c.get('apiKey')\n  const projectId = c.req.param('id')\n  \n  // Check if key has read scope for this specific project\n  if (!keys.checkResourceScope(record, 'project', projectId, 'read')) {\n    return c.json({ error: 'No access to this project' }, 403)\n  }\n\n  return c.json({ project: { id: projectId } })\n})\n\n// Create API key endpoint\napp.post('/api/keys', async (c) =\u003e {\n  const record = c.get('apiKey')\n  \n  // Only admins can create keys\n  if (!keys.hasScope(record, 'admin')) {\n    return c.json({ error: 'Admin permission required' }, 403)\n  }\n  \n  try {\n    const body = await c.req.json()\n    \n    const { key, record: newRecord } = await keys.create({\n      ownerId: body.ownerId,\n      name: body.name,\n      scopes: body.scopes,\n      expiresAt: body.expiresAt,\n    }, {\n      userId: record.metadata.ownerId,\n      ip: c.req.header('x-forwarded-for'),\n      metadata: { action: 'api_create' },\n    })\n    \n    return c.json({ \n      success: true, \n      key,  // Only returned once!\n      keyId: newRecord.id \n    })\n  } catch (error) {\n    console.error('Failed to create key:', error)\n    return c.json({ error: 'Failed to create key' }, 500)\n  }\n})\n\n// Revoke API key endpoint\napp.delete('/api/keys/:id', async (c) =\u003e {\n  const record = c.get('apiKey')\n  const keyId = c.req.param('id')\n  \n  try {\n    // Verify ownership or admin permission\n    const keyToRevoke = await keys.findById(keyId)\n    \n    if (!keyToRevoke) {\n      return c.json({ error: 'Key not found' }, 404)\n    }\n    \n    const isOwner = keyToRevoke.metadata.ownerId === record.metadata.ownerId\n    const isAdmin = keys.hasScope(record, 'admin')\n    \n    if (!isOwner \u0026\u0026 !isAdmin) {\n      return c.json({ error: 'Not authorized' }, 403)\n    }\n    \n    await keys.revoke(keyId, {\n      userId: record.metadata.ownerId,\n      ip: c.req.header('x-forwarded-for'),\n      metadata: { via: 'api' },\n    })\n    \n    return c.json({ success: true })\n  } catch (error) {\n    if (error.code === ApiKeyErrorCode.KEY_NOT_FOUND) {\n      return c.json({ error: 'Key not found' }, 404)\n    }\n    if (error.code === ApiKeyErrorCode.ALREADY_REVOKED) {\n      return c.json({ error: 'Key is already revoked' }, 400)\n    }\n    \n    console.error('Failed to revoke key:', error)\n    return c.json({ error: 'Failed to revoke key' }, 500)\n  }\n})\n```\n\n## Security Best Practices\n\n1. **Use a salt in production**:\n   ```typescript\n   const keys = createKeys({\n     salt: process.env.API_KEY_SALT,\n     algorithm: 'sha512',\n   })\n   ```\n\n2. **Set expiration dates**: Don't create keys that never expire\n\n3. **Use scopes**: Implement least-privilege access\n\n4. **Enable caching**: Reduce database load in production\n\n5. **Use HTTPS**: Always use HTTPS to prevent key interception\n\n6. **Monitor usage**: Track `lastUsedAt` to identify unused keys\n\n7. **Rotate keys**: Implement regular key rotation policies\n   ```typescript\n   // Rotate keys periodically\n   const { key: newKey } = await keys.rotate(oldRecord.id)\n   ```\n\n8. **Use soft revocation**: Revoked keys are kept with `revokedAt` timestamp for audit trails (Redis TTL: 7 days, Drizzle: forever)\n\n9. **Enable/Disable rather than revoke**: Temporarily disable keys instead of revoking them\n\n## TypeScript Types\n\n```typescript\ninterface ApiKeyRecord {\n  id: string\n  keyHash: string\n  metadata: ApiKeyMetadata\n}\n\ninterface ApiKeyMetadata {\n  ownerId: string\n  name?: string\n  description?: string\n  scopes?: string[]\n  resources?: Record\u003cstring, string[]\u003e // Resource-specific scopes (e.g., { \"project:123\": [\"read\", \"write\"] })\n  tags?: string[]\n  expiresAt: string | null\n  createdAt?: string\n  lastUsedAt?: string\n  enabled?: boolean\n  revokedAt?: string | null\n  rotatedTo?: string | null\n}\n\ninterface VerifyResult {\n  valid: boolean\n  record?: ApiKeyRecord\n  error?: string\n  errorCode?: ApiKeyErrorCode\n}\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fizadoesdev%2Fkeypal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fizadoesdev%2Fkeypal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fizadoesdev%2Fkeypal/lists"}