Keypal

repository·main·Indexed 42 days ago

https://github.com/izadoesdev/keypal

A TypeScript library for secure API key management featuring cryptographic hashing, expiration, scope-based permissions, and pluggable storage backends including Redis, Drizzle, Prisma, and Kysely. Version 0.2.0 provides built-in caching, audit logging, and a ResourceBuilder for fluent resource-specific permission management.

Tokens
50.6K
Snippets
184
Records
228
Agent score
51%

What's inside Keypal

  1. Keypal core features

    main

    keypal provides a secure, type-safe framework for managing API keys with the following capabilities:

    • Security: Uses SHA-256/SHA-512 hashing with optional salt and timing-safe comparisons.
    • Automatic Detection: Extracts keys from Authorization, x-api-key, or custom headers.
    • Caching: Supports optional in-memory or Redis caching for validated keys.
    • Pluggable Storage: Includes adapters for Memory, Redis, Drizzle ORM, Prisma, Kysely, and Convex.
    • Access Control: Fine-grained, scope-based permissions and resource-specific scopes.
    • Management: Features for enabling/disabling, rotating, and soft-revoking keys, plus organization via tags.
    • Observability: Opt-in audit logging to track key usage and management actions.
  2. What are Resource Scopes in Keypal

    main

    Resource scopes enable fine-grained permission management by allowing you to grant specific permissions (e.g., read, write, deploy) to specific resources (e.g., a project, website, or team) rather than just applying global permissions.

    Resources are identified using a string format: {resourceType}:{resourceId} (for example, project:123 or website:site-abc).

    When creating a key, you can define:

    1. Global Scopes: Permissions that apply to everything (e.g., scopes: ['read']).
    2. Resource Scopes: Permissions tied to specific resource identifiers (e.g., 'project:123': ['read', 'write']).
    const { key, record } = await keys.create({
      ownerId: 'user_123',
      scopes: ['read'], // Global scope
      resources: {
        'project:123': ['read', 'write'],
        'project:456': ['read'],
        'website:789': ['deploy', 'rollback'],
      },
    })
  3. What are Scopes and how to use them

    main

    Scopes are string arrays that define the specific actions an API key is authorized to perform. They act as the primary mechanism for access control within Keypal.

    Global Scopes

    Global scopes apply to all resources within your application. You define these when creating a new key using the create method.

    const { key, record } = await keys.create({
      ownerId: 'user_123',
      scopes: ['read', 'write', 'admin'],
    })
  4. How Memory Storage works internally

    main

    The memory storage adapter uses an in-memory Map structure with optimized indexing to ensure high performance:

    • O(1) Hash Lookups: A hashIndex maps key hashes to IDs for instant verification.
    • O(1) Owner Lookups: An ownerIndex maps owner IDs to their key sets for fast listing.
    • Hash Collision Detection: Automatically detects and rejects duplicate key hashes.
    • Zero Latency: Operations run in-process with no network calls or external dependencies.

    Internal Data Structure

    // Primary storage: id -> ApiKeyRecord
    keys: Map<string, ApiKeyRecord>
    
    // Hash index for O(1) verification: keyHash -> id
    hashIndex: Map<string, string>
    
    // Owner index for O(1) listing: ownerId -> Set<id>
    ownerIndex: Map<string, Set<string>>
  5. Understand the API Key lifecycle

    main

    The lifecycle of a key in keypal follows these stages:

    1. Creation: Keys are generated and immediately hashed before being saved.
    2. Storage: For security, only the cryptographic hash is stored in the database; the plain-text key is never persisted.
    3. Verification: When a key is used, it is hashed and compared against the stored hash.
    4. Revocation: Keys can be soft-deleted to maintain audit trails while preventing further use.
    5. Rotation: Existing keys can be replaced with new ones to maintain security hygiene.
  6. Track API key usage

    main

    Keypal supports tracking when a key was last used.

    1. Automatic Tracking: Enable autoTrackUsage: true in the createKeys configuration to automatically update lastUsedAt during every verify call.
    2. Manual Tracking: Call await keys.updateLastUsed(record.id) manually.
    3. Skipping Tracking: Use { skipTracking: true } in the verify options to prevent an update for a specific request.
    const keys = createKeys({
      autoTrackUsage: true,
    })
    
    // Or skip for specific requests
    const result = await keys.verify(headers, { skipTracking: true })
  7. How caching works in keypal

    main

    The caching lifecycle follows these steps:

    1. On Verify: After a successful verification, the key is added to the cache.
    2. Cache Lookup: Subsequent verification requests check the cache before querying storage.
    3. Cache Miss: If the key is not found in the cache, the system queries the storage and then caches the result.
    4. TTL: Entries expire after the configured cacheTtl (in seconds).
    5. Validation: Data is validated upon retrieval to ensure no corruption has occurred.
  8. Check permissions and resource-specific scopes

    main

    Keypal provides two ways to check permissions:

    1. General Scopes: Use keys.hasScope(record, scope) to check if a key has a broad permission (e.g., 'read', 'admin').
    2. Resource-Specific Scopes: Use keys.checkResourceScope(record, resourceType, resourceId, scope) to verify if a key has access to a specific instance of a resource (e.g., checking if a key can 'read' a specific 'project' ID).

    Resource-specific scopes are stored in the ApiKeyMetadata.resources object as a mapping of resource strings to arrays of allowed actions (e.g., { "project:123": ["read", "write"] }).

    // General scope check
    if (!keys.hasScope(record, 'read')) {
      return c.json({ error: 'Insufficient permissions' }, 403)
    }
    
    // Resource-specific scope check
    if (!keys.checkResourceScope(record, 'project', projectId, 'read')) {
      return c.json({ error: 'No access to this project' }, 403)
    }
  9. Choose a Storage Adapter for keypal

    main

    keypal supports multiple storage backends for persisting API keys. Select an adapter based on your infrastructure requirements:

    • Memory: Use for prototypes, demos, local testing, or when persistence is not required. This is the default adapter.
    • Redis: Use for distributed applications requiring high performance or if Redis is already part of your stack.
    • Drizzle / Prisma / Kysely: Use when you need persistent storage in an existing SQL database (PostgreSQL, MySQL, SQLite) and require complex queries.
    • Convex: Use if you are building with the Convex backend and want real-time capabilities.

    If none of these fit your needs, you can implement a custom adapter by satisfying the Storage interface.