
Security News
The Hidden Blast Radius of the Axios Compromise
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.
better-api-keys
Advanced tools
[](https://github.com/izadoesdev/better-api-keys/actions/workflows/test.yml) [
// 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)
}
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
// Header detection
headerNames: ['x-api-key', 'authorization'],
extractBearer: true,
})
// Create
const { key, record } = await keys.create({
ownerId: 'user_123',
name: 'Production Key',
scopes: ['read', 'write'],
expiresAt: '2025-12-31',
enabled: true, // optional, defaults to true
})
// List
const userKeys = await keys.list('user_123')
// 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)
// 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) // 'Missing API key' | 'Invalid API key' | 'API key has expired' | 'API key is disabled' | 'API key has been revoked'
}
if (keys.hasScope(record, 'write')) { /* ... */ }
if (keys.hasAnyScope(record, ['admin', 'moderator'])) { /* ... */ }
if (keys.hasAllScopes(record, ['read', 'write'])) { /* ... */ }
if (keys.isExpired(record)) { /* ... */ }
// 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 })
keys.hasKey(headers) // boolean
keys.extractKey(headers) // string | null
keys.generateKey() // string
keys.hashKey(key) // string
const keys = createKeys({ prefix: 'sk_' })
import Redis from 'ioredis'
const redis = new Redis()
const keys = createKeys({
prefix: 'sk_',
storage: 'redis',
cache: 'redis',
redis,
})
import { DrizzleStore } from 'better-api-keys/storage/drizzle'
const keys = createKeys({
prefix: 'sk_',
storage: new DrizzleStore({
db,
table: apiKeys,
columns: {
keyHash: 'key_hash',
ownerId: 'user_id',
}
})
})
import { type Storage } from 'better-api-keys'
const customStorage: Storage = {
save: async (record) => { /* ... */ },
findByHash: async (keyHash) => { /* ... */ },
findById: async (id) => { /* ... */ },
findByOwner: async (ownerId) => { /* ... */ },
updateMetadata: async (id, metadata) => { /* ... */ },
delete: async (id) => { /* ... */ },
deleteByOwner: async (ownerId) => { /* ... */ },
}
const keys = createKeys({
storage: customStorage,
})
import { Hono } from 'hono'
import { createKeys } from 'better-api-keys'
import Redis from 'ioredis'
const redis = new Redis()
const keys = createKeys({
prefix: 'sk_',
storage: 'redis',
cache: 'redis',
redis,
})
const app = new Hono()
// Authentication middleware
app.use('/api/*', async (c, next) => {
const result = await keys.verify(c.req.raw.headers)
if (!result.valid) {
return c.json({ error: result.error }, 401)
}
c.set('apiKey', result.record)
keys.updateLastUsed(result.record.id).catch(console.error)
await next()
})
// Protected route with scope check
app.get('/api/data', async (c) => {
const record = c.get('apiKey')
if (!keys.hasScope(record, 'read')) {
return c.json({ error: 'Insufficient permissions' }, 403)
}
return c.json({ data: 'sensitive data' })
})
Use a salt in production:
const keys = createKeys({
salt: process.env.API_KEY_SALT,
algorithm: 'sha512',
})
Set expiration dates: Don't create keys that never expire
Use scopes: Implement least-privilege access
Enable caching: Reduce database load in production
Use HTTPS: Always use HTTPS to prevent key interception
Monitor usage: Track lastUsedAt to identify unused keys
Rotate keys: Implement regular key rotation policies
// Rotate keys periodically
const { key: newKey } = await keys.rotate(oldRecord.id)
Use soft revocation: Revoked keys are kept with revokedAt timestamp for audit trails (Redis TTL: 7 days, Drizzle: forever)
Enable/Disable rather than revoke: Temporarily disable keys instead of revoking them
interface ApiKeyRecord {
id: string
keyHash: string
metadata: ApiKeyMetadata
}
interface ApiKeyMetadata {
ownerId: string
name?: string
description?: string
scopes?: string[]
expiresAt: string | null
createdAt?: string
lastUsedAt?: string
enabled?: boolean
revokedAt?: string | null
rotatedTo?: string | null
}
interface VerifyResult {
valid: boolean
record?: ApiKeyRecord
error?: string
}
MIT
FAQs
[](https://github.com/izadoesdev/better-api-keys/actions/workflows/test.yml) [
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.