Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

223 lines
6.7 KiB
TypeScript

/**
* Tests for API key authentication utilities.
*
* Tests cover:
* - API key format detection (legacy vs encrypted)
* - Authentication against stored keys
* - Key encryption and decryption
* - Display formatting
* - Edge cases
*/
import { randomBytes } from 'crypto'
import { createEncryptedApiKey, createLegacyApiKey } from '@sim/testing'
import { describe, expect, it, vi } from 'vitest'
const cryptoMock = vi.hoisted(() => ({
isEncryptedApiKeyFormat: (key: string) => key.startsWith('sk-sim-'),
isLegacyApiKeyFormat: (key: string) => key.startsWith('sim_') && !key.startsWith('sk-sim-'),
generateApiKey: () => `sim_${randomBytes(24).toString('base64url')}`,
generateEncryptedApiKey: () => `sk-sim-${randomBytes(24).toString('base64url')}`,
encryptApiKey: async (apiKey: string) => ({
encrypted: `mock-iv:${Buffer.from(apiKey).toString('hex')}:mock-tag`,
iv: 'mock-iv',
}),
decryptApiKey: async (encryptedValue: string) => {
if (!encryptedValue.includes(':') || encryptedValue.split(':').length !== 3) {
return { decrypted: encryptedValue }
}
const parts = encryptedValue.split(':')
const hexPart = parts[1]
return { decrypted: Buffer.from(hexPart, 'hex').toString('utf8') }
},
}))
vi.mock('@/lib/api-key/crypto', () => cryptoMock)
import {
formatApiKeyForDisplay,
getApiKeyLast4,
isEncryptedKey,
isValidApiKeyFormat,
} from '@/lib/api-key/auth'
const { generateApiKey, generateEncryptedApiKey, isEncryptedApiKeyFormat, isLegacyApiKeyFormat } =
cryptoMock
describe('isEncryptedKey', () => {
it('should detect encrypted storage format (iv:encrypted:authTag)', () => {
const encryptedStorage = 'abc123:encrypted-data:tag456'
expect(isEncryptedKey(encryptedStorage)).toBe(true)
})
it('should detect plain text storage (no colons)', () => {
const plainKey = 'sim_abcdef123456'
expect(isEncryptedKey(plainKey)).toBe(false)
})
it('should detect plain text with single colon', () => {
const singleColon = 'part1:part2'
expect(isEncryptedKey(singleColon)).toBe(false)
})
it('should detect encrypted format with exactly 3 parts', () => {
const threeParts = 'iv:data:tag'
expect(isEncryptedKey(threeParts)).toBe(true)
})
it('should reject format with more than 3 parts', () => {
const fourParts = 'a:b:c:d'
expect(isEncryptedKey(fourParts)).toBe(false)
})
it('should reject empty string', () => {
expect(isEncryptedKey('')).toBe(false)
})
})
describe('isEncryptedApiKeyFormat (key prefix)', () => {
it('should detect sk-sim- prefix as encrypted format', () => {
const { key } = createEncryptedApiKey()
expect(isEncryptedApiKeyFormat(key)).toBe(true)
})
it('should not detect sim_ prefix as encrypted format', () => {
const { key } = createLegacyApiKey()
expect(isEncryptedApiKeyFormat(key)).toBe(false)
})
it('should not detect random string as encrypted format', () => {
expect(isEncryptedApiKeyFormat('random-string')).toBe(false)
})
})
describe('isLegacyApiKeyFormat', () => {
it('should detect sim_ prefix as legacy format', () => {
const { key } = createLegacyApiKey()
expect(isLegacyApiKeyFormat(key)).toBe(true)
})
it('should not detect sk-sim- prefix as legacy format', () => {
const { key } = createEncryptedApiKey()
expect(isLegacyApiKeyFormat(key)).toBe(false)
})
it('should not detect random string as legacy format', () => {
expect(isLegacyApiKeyFormat('random-string')).toBe(false)
})
})
describe('isValidApiKeyFormat', () => {
it('should accept valid length keys', () => {
expect(isValidApiKeyFormat(`sim_${'a'.repeat(20)}`)).toBe(true)
})
it('should reject too short keys', () => {
expect(isValidApiKeyFormat('short')).toBe(false)
})
it('should reject too long keys (>200 chars)', () => {
expect(isValidApiKeyFormat('a'.repeat(201))).toBe(false)
})
it('should accept keys at boundary (11 chars)', () => {
expect(isValidApiKeyFormat('a'.repeat(11))).toBe(true)
})
it('should reject keys at boundary (10 chars)', () => {
expect(isValidApiKeyFormat('a'.repeat(10))).toBe(false)
})
it('should reject non-string input', () => {
expect(isValidApiKeyFormat(null as any)).toBe(false)
expect(isValidApiKeyFormat(undefined as any)).toBe(false)
expect(isValidApiKeyFormat(123 as any)).toBe(false)
})
it('should reject empty string', () => {
expect(isValidApiKeyFormat('')).toBe(false)
})
})
describe('getApiKeyLast4', () => {
it('should return last 4 characters of key', () => {
expect(getApiKeyLast4('sim_abcdefghijklmnop')).toBe('mnop')
})
it('should return last 4 characters of encrypted format key', () => {
expect(getApiKeyLast4('sk-sim-abcdefghijkl')).toBe('ijkl')
})
it('should return entire key if less than 4 chars', () => {
expect(getApiKeyLast4('abc')).toBe('abc')
})
it('should handle exactly 4 chars', () => {
expect(getApiKeyLast4('abcd')).toBe('abcd')
})
})
describe('formatApiKeyForDisplay', () => {
it('should format encrypted format key with sk-sim- prefix', () => {
const key = 'sk-sim-abcdefghijklmnopqrstuvwx'
const formatted = formatApiKeyForDisplay(key)
expect(formatted).toBe('sk-sim-...uvwx')
})
it('should format legacy key with sim_ prefix', () => {
const key = 'sim_abcdefghijklmnopqrstuvwx'
const formatted = formatApiKeyForDisplay(key)
expect(formatted).toBe('sim_...uvwx')
})
it('should format unknown format key with just ellipsis', () => {
const key = 'custom-key-format-abcd'
const formatted = formatApiKeyForDisplay(key)
expect(formatted).toBe('...abcd')
})
it('should show last 4 characters correctly', () => {
const key = 'sk-sim-xxxxxxxxxxxxxxxxr6AA'
const formatted = formatApiKeyForDisplay(key)
expect(formatted).toContain('r6AA')
})
})
describe('generateApiKey', () => {
it('should generate key with sim_ prefix', () => {
const key = generateApiKey()
expect(key).toMatch(/^sim_/)
})
it('should generate unique keys', () => {
const key1 = generateApiKey()
const key2 = generateApiKey()
expect(key1).not.toBe(key2)
})
it('should generate key of valid length', () => {
const key = generateApiKey()
expect(key.length).toBeGreaterThan(10)
expect(key.length).toBeLessThan(100)
})
})
describe('generateEncryptedApiKey', () => {
it('should generate key with sk-sim- prefix', () => {
const key = generateEncryptedApiKey()
expect(key).toMatch(/^sk-sim-/)
})
it('should generate unique keys', () => {
const key1 = generateEncryptedApiKey()
const key2 = generateEncryptedApiKey()
expect(key1).not.toBe(key2)
})
it('should generate key of valid length', () => {
const key = generateEncryptedApiKey()
expect(key.length).toBeGreaterThan(10)
expect(key.length).toBeLessThan(100)
})
})