SkillAgentSearch skills...

Keypal

A TypeScript library for secure API key management with cryptographic hashing, expiration, scopes, and pluggable storage

Install / Use

/learn @izadoesdev/Keypal
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

keypal

Test Benchmark npm version

A TypeScript library for secure API key management with cryptographic hashing, expiration, scopes, and pluggable storage.

Features

  • Secure by Default: SHA-256/SHA-512 hashing with optional salt and timing-safe comparison
  • Smart Key Detection: Automatically extracts keys from Authorization, x-api-key, or custom headers
  • Built-in Caching: Optional in-memory or Redis caching for validated keys
  • Flexible Storage: Memory, Redis, Drizzle ORM, Prisma, and Kysely adapters included
  • Scope-based Permissions: Fine-grained access control with resource-specific scopes
  • Tags: Organize and find keys by tags
  • Key Management: Enable/disable, rotate, and soft-revoke keys with audit trails
  • Audit Logging: Track who did what, when, and why (opt-in)
  • TypeScript: Full type safety
  • Zero Config: Works out of the box with sensible defaults

Installation

npm install keypal
# or
bun add keypal

Quick Start

import { createKeys } from 'keypal'

const keys = createKeys({
  prefix: 'sk_',
  cache: true,
})

// Create a key
const { key, record } = await keys.create({
  ownerId: 'user_123',
  scopes: ['read', 'write'],
})

// Verify from headers
const result = await keys.verify(request.headers)
if (result.valid) {
  console.log('Authenticated:', result.record.metadata.ownerId)
}

Configuration

import Redis from 'ioredis'

const redis = new Redis()

const keys = createKeys({
  // Key generation
  prefix: 'sk_prod_',
  length: 32,
  alphabet: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
  
  // Security
  algorithm: 'sha256',  // or 'sha512'
  salt: process.env.API_KEY_SALT,
  
  // Storage (memory by default)
  storage: 'redis',  // or custom Storage instance
  redis,             // required when storage/cache is 'redis'
  
  // Caching
  cache: true,       // in-memory cache
  // cache: 'redis', // Redis cache
  cacheTtl: 60,
  
  // Revocation
  revokedKeyTtl: 604800, // TTL for revoked keys in Redis (7 days), set to 0 to keep forever
  
  // Usage tracking
  autoTrackUsage: true, // Automatically update lastUsedAt on verify
  
  // Audit logging (opt-in)
  auditLogs: true,  // Enable audit logging
  auditContext: {   // Default context for all audit logs (optional)
    userId: 'system',
    metadata: { service: 'api' }
  },
  
  // Header detection
  headerNames: ['x-api-key', 'authorization'],
  extractBearer: true,
})

API

Creating & Managing Keys

// Create with plain object
const { key, record } = await keys.create({
  ownerId: 'user_123',
  name: 'Production Key',
  description: 'Key for production API access',
  scopes: ['read', 'write'],
  tags: ['production', 'api'],
  resources: {
    'project:123': ['read', 'write'],
    'project:456': ['read']
  },
  expiresAt: '2025-12-31',
  enabled: true, // optional, defaults to true
})

// Create with ResourceBuilder (fluent API)
import { ResourceBuilder, createResourceBuilder } from 'keypal'

const resources = new ResourceBuilder()
  .add('website', 'site123', ['read', 'write'])
  .add('project', 'proj456', ['deploy'])
  .addMany('website', ['site1', 'site2', 'site3'], ['read']) // Same scopes for multiple resources
  .build()

const { key: key2, record: record2 } = await keys.create({
  ownerId: 'user_123',
  scopes: ['admin'],
  resources, // Use the built resources object
})

// List
const userKeys = await keys.list('user_123')

// Find by tag
const taggedKeys = await keys.findByTag('production')
const multiTagKeys = await keys.findByTags(['production', 'api'])

// Find by ID or hash
const keyRecord = await keys.findById(record.id)
const keyByHash = await keys.findByHash(record.keyHash)

// Enable/Disable
await keys.enable(record.id)
await keys.disable(record.id)

// Rotate (create new key, mark old as revoked)
const { key: newKey, record: newRecord, oldRecord } = await keys.rotate(record.id, {
  name: 'Updated Key',
  scopes: ['read', 'write', 'admin'],
})

// Revoke (soft delete - keeps record with revokedAt timestamp)
await keys.revoke(record.id)
await keys.revokeAll('user_123')

// Update last used
await keys.updateLastUsed(record.id)

Verifying Keys

// From headers (automatic detection)
const result = await keys.verify(request.headers)

// From string
const result = await keys.verify('sk_abc123')
const result = await keys.verify('Bearer sk_abc123')

// With options
const result = await keys.verify(headers, {
  headerNames: ['x-custom-key'],
  skipCache: true,
  skipTracking: true, // Skip updating lastUsedAt (useful when autoTrackUsage is enabled)
})

// Check result
if (result.valid) {
  console.log(result.record)
} else {
  console.log(result.error) // Human-readable error message
  console.log(result.errorCode) // Error code for programmatic handling (see ApiKeyErrorCode)
}

Permission Checking

// Global scope checks
if (keys.hasScope(record, 'write')) { /* ... */ }
if (keys.hasAnyScope(record, ['admin', 'moderator'])) { /* ... */ }
if (keys.hasAllScopes(record, ['read', 'write'])) { /* ... */ }
if (keys.isExpired(record)) { /* ... */ }

// Resource-specific scope checks
// Check if key has 'read' scope for a specific resource
if (keys.checkResourceScope(record, 'website', 'site123', 'read')) { /* ... */ }

// Check if key has any of the specified scopes for a resource
if (keys.checkResourceAnyScope(record, 'website', 'site123', ['admin', 'write'])) { /* ... */ }

// Check if key has all specified scopes for a resource (checks both global and resource scopes)
if (keys.checkResourceAllScopes(record, 'website', 'site123', ['read', 'write'])) { /* ... */ }

ResourceBuilder (Fluent API)

Build resource-specific scopes with a clean, chainable API:

import { ResourceBuilder, createResourceBuilder } from 'keypal'

// Basic usage
const resources = new ResourceBuilder()
  .add('website', 'site123', ['read', 'write'])
  .add('project', 'proj456', ['deploy'])
  .build()

// Add scopes to multiple resources at once
const resources2 = new ResourceBuilder()
  .addMany('website', ['site1', 'site2', 'site3'], ['read'])
  .add('project', 'proj1', ['deploy', 'rollback'])
  .build()

// Add single scopes
const resources3 = new ResourceBuilder()
  .addOne('website', 'site123', 'read')
  .addOne('website', 'site123', 'write')
  .build()

// Modify existing resources
const builder = new ResourceBuilder()
  .add('website', 'site123', ['read', 'write'])
  .add('project', 'proj456', ['deploy'])

// Check if resource exists
if (builder.has('website', 'site123')) {
  const scopes = builder.get('website', 'site123')
  console.log(scopes) // ['read', 'write']
}

// Remove specific scopes
builder.removeScopes('website', 'site123', ['write'])

// Remove entire resource
builder.remove('project', 'proj456')

// Build final result
const finalResources = builder.build()

// Start from existing resources (useful for updates)
const existingResources = {
  'website:site123': ['read'],
  'project:proj456': ['deploy']
}

const updated = ResourceBuilder.from(existingResources)
  .add('website', 'site123', ['write']) // Merges with existing
  .add('team', 'team789', ['admin'])
  .build()

// Use with createKeys
await keys.create({
  ownerId: 'user_123',
  scopes: ['admin'],
  resources: updated
})

ResourceBuilder Methods:

  • add(resourceType, resourceId, scopes) - Add scopes to a resource (merges if exists)
  • addOne(resourceType, resourceId, scope) - Add a single scope
  • addMany(resourceType, resourceIds, scopes) - Add same scopes to multiple resources
  • remove(resourceType, resourceId) - Remove entire resource
  • removeScopes(resourceType, resourceId, scopes) - Remove specific scopes
  • has(resourceType, resourceId) - Check if resource exists
  • get(resourceType, resourceId) - Get scopes for a resource
  • clear() - Clear all resources
  • build() - Build and return the resources object
  • ResourceBuilder.from(resources) - Create from existing resources object

Usage Tracking

// Enable automatic tracking in config
const keys = createKeys({
  autoTrackUsage: true, // Automatically updates lastUsedAt on verify
})

// Manually update (always available)
await keys.updateLastUsed(record.id)

// Skip tracking for specific requests
const result = await keys.verify(headers, { skipTracking: true })

Audit Logging

Track all key operations with context about who performed each action:

// Enable audit logging
const keys = createKeys({
  auditLogs: true,
  auditContext: {
    // Default context merged into all logs
    metadata: { environment: 'production' }
  }
})

// Actions are automatically logged with optional context
await keys.create({
  ownerId: 'user_123',
  scopes: ['read']
}, {
  userId: 'admin_456',
  ip: '192.168.1.1',
  metadata: { reason: 'New customer onboarding' }
})

await keys.revoke('key_123', {
  userId: 'admin_789',
  metadata: { reason: 'Security breach' }
})

// Query logs
const logs = await keys.getLogs({
  keyId: 'key_123',
  action: 'revoked',
  startDate: '2025-01-01',
  limit: 100
})

// Count logs
const count = await keys.countLogs({ action: 'created' })

// Get statistics
const stats = await keys.getLogStats('user_123')
console.log(stats.total)
console.log(stats.byAction.created)
console.log(stats.lastActivity)

// Clean up old logs
const deleted = await keys.deleteLogs({
  endDate: '2024-01-01'
})

// Clear logs for a specific key
await keys.clearLogs('key_123')

Log Entry Structure:

{

Related Skills

View on GitHub
GitHub Stars195
CategoryDevelopment
Updated1d ago
Forks14

Languages

TypeScript

Security Score

100/100

Audited on Mar 29, 2026

No findings