Files
simstudioai--sim/apps/sim/lib/api-key/auth.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (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 / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

213 lines
6.4 KiB
TypeScript

import { db } from '@sim/db'
import { apiKey } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateShortId } from '@sim/utils/id'
import { and, eq } from 'drizzle-orm'
import {
decryptApiKey,
encryptApiKey,
generateApiKey,
generateEncryptedApiKey,
hashApiKey,
isEncryptedApiKeyFormat,
isLegacyApiKeyFormat,
} from '@/lib/api-key/crypto'
import { env } from '@/lib/core/config/env'
const logger = createLogger('ApiKeyAuth')
/**
* API key authentication utilities supporting both legacy plain text keys
* and modern encrypted keys for gradual migration without breaking existing keys
*/
/**
* Checks if a stored key is in the new encrypted format
* @param storedKey - The key stored in the database
* @returns true if the key is encrypted, false if it's plain text
*/
export function isEncryptedKey(storedKey: string): boolean {
// Check if it follows the encrypted format: iv:encrypted:authTag
return storedKey.includes(':') && storedKey.split(':').length === 3
}
/**
* Encrypts an API key for secure storage
* @param apiKey - The plain text API key to encrypt
* @returns Promise<string> - The encrypted key
*/
async function encryptApiKeyForStorage(apiKey: string): Promise<string> {
try {
const { encrypted } = await encryptApiKey(apiKey)
return encrypted
} catch (error) {
logger.error('API key encryption error:', { error })
throw new Error('Failed to encrypt API key')
}
}
/**
* Creates a new API key
* @param useStorage - Whether to encrypt the key before storage (default: true)
* @returns Promise<{key: string, encryptedKey?: string}> - The plain key and optionally encrypted version
*/
export async function createApiKey(useStorage = true): Promise<{
key: string
encryptedKey?: string
}> {
try {
const hasEncryptionKey = env.API_ENCRYPTION_KEY !== undefined
const plainKey = hasEncryptionKey ? generateEncryptedApiKey() : generateApiKey()
if (useStorage) {
const encryptedKey = await encryptApiKeyForStorage(plainKey)
return { key: plainKey, encryptedKey }
}
return { key: plainKey }
} catch (error) {
logger.error('API key creation error:', { error })
throw new Error('Failed to create API key')
}
}
/**
* Decrypts an API key from storage for display purposes
* @param encryptedKey - The encrypted API key from the database
* @returns Promise<string> - The decrypted API key
*/
async function decryptApiKeyFromStorage(encryptedKey: string): Promise<string> {
try {
const { decrypted } = await decryptApiKey(encryptedKey)
return decrypted
} catch (error) {
logger.error('API key decryption error:', { error })
throw new Error('Failed to decrypt API key')
}
}
/**
* Gets the last 4 characters of an API key for display purposes
* @param apiKey - The API key (plain text)
* @returns string - The last 4 characters
*/
export function getApiKeyLast4(apiKey: string): string {
return apiKey.slice(-4)
}
/**
* Gets the display format for an API key showing prefix and last 4 characters
* @param encryptedKey - The encrypted API key from the database
* @returns Promise<string> - The display format like "sk-sim-...r6AA"
*/
export async function getApiKeyDisplayFormat(encryptedKey: string): Promise<string> {
try {
if (isEncryptedKey(encryptedKey)) {
const decryptedKey = await decryptApiKeyFromStorage(encryptedKey)
return formatApiKeyForDisplay(decryptedKey)
}
// For plain text keys (legacy), format directly
return formatApiKeyForDisplay(encryptedKey)
} catch (error) {
logger.error('Failed to format API key for display:', { error })
return '****'
}
}
/**
* Formats an API key for display showing prefix and last 4 characters
* @param apiKey - The API key (plain text)
* @returns string - The display format like "sk-sim-...r6AA" or "sim_...r6AA"
*/
export function formatApiKeyForDisplay(apiKey: string): string {
if (isEncryptedApiKeyFormat(apiKey)) {
// For sk-sim- format: "sk-sim-...r6AA"
const last4 = getApiKeyLast4(apiKey)
return `sk-sim-...${last4}`
}
if (isLegacyApiKeyFormat(apiKey)) {
// For sim_ format: "sim_...r6AA"
const last4 = getApiKeyLast4(apiKey)
return `sim_...${last4}`
}
// Unknown format, just show last 4
const last4 = getApiKeyLast4(apiKey)
return `...${last4}`
}
/**
* Gets the last 4 characters of an encrypted API key by decrypting it first
* @param encryptedKey - The encrypted API key from the database
* @returns Promise<string> - The last 4 characters
*/
async function getEncryptedApiKeyLast4(encryptedKey: string): Promise<string> {
try {
if (isEncryptedKey(encryptedKey)) {
const decryptedKey = await decryptApiKeyFromStorage(encryptedKey)
return getApiKeyLast4(decryptedKey)
}
// For plain text keys (legacy), return last 4 directly
return getApiKeyLast4(encryptedKey)
} catch (error) {
logger.error('Failed to get last 4 characters of API key:', { error })
return '****'
}
}
/**
* Validates API key format (basic validation)
* @param apiKey - The API key to validate
* @returns boolean - true if the format appears valid
*/
export function isValidApiKeyFormat(apiKeyValue: string): boolean {
return typeof apiKeyValue === 'string' && apiKeyValue.length > 10 && apiKeyValue.length < 200
}
export async function createWorkspaceApiKey(params: {
workspaceId: string
userId: string
name: string
}) {
const existingKey = await db
.select({ id: apiKey.id })
.from(apiKey)
.where(
and(
eq(apiKey.workspaceId, params.workspaceId),
eq(apiKey.name, params.name),
eq(apiKey.type, 'workspace')
)
)
.limit(1)
if (existingKey.length > 0) {
throw new Error(
`A workspace API key named "${params.name}" already exists. Choose a different name.`
)
}
const { key: plainKey, encryptedKey } = await createApiKey(true)
if (!encryptedKey) {
throw new Error('Failed to encrypt API key for storage')
}
const [newKey] = await db
.insert(apiKey)
.values({
id: generateShortId(),
workspaceId: params.workspaceId,
userId: params.userId,
createdBy: params.userId,
name: params.name,
key: encryptedKey,
keyHash: hashApiKey(plainKey),
type: 'workspace',
createdAt: new Date(),
updatedAt: new Date(),
})
.returning({ id: apiKey.id, name: apiKey.name, createdAt: apiKey.createdAt })
return { id: newKey.id, name: newKey.name, key: plainKey, createdAt: newKey.createdAt }
}