<!-- Generated by SpecStory -->
# Continue with section 10.4 (2025-06-06 15:58:41)
_**User**_
Please continue with 10.4
---
_**Assistant**_
---
I'll help you continue with subtask 10.4. Let me first check the current task details to understand what subtask 10.4 involves.
---
---
Perfect! Subtask 10.4 is about implementing an API Key Management System. This is a crucial security component for the MCP server. Let me set it to in-progress and start implementing this system.
---
---
Now let me examine the current project structure to understand how to implement the API Key Management System within the existing architecture.
---
<details>
<summary>Listed directory src • **8** results</summary>
| Name |
|-------|
| 📁 `middleware` |
| 📁 `utils` |
| 📁 `tools` |
| 📁 `services` |
| 📁 `models` |
| 📁 `__tests__` |
| 📄 `server.ts` |
| 📄 `index.ts` |
</details>
---
<details>
<summary>Listed directory src/models • **7** results</summary>
| Name |
|-------|
| 📄 `tally-schemas.ts` |
| 📄 `index.ts` |
| 📄 `template.ts` |
| 📄 `form-config.ts` |
| 📄 `form-version.ts` |
| 📄 `form-config-schemas.ts` |
| 📄 `errors.ts` |
</details>
---
<details>
<summary>Listed directory src/services • **12** results</summary>
| Name |
|-------|
| 📄 `form-permission-service.ts` |
| 📄 `workspace-service.ts` |
| 📄 `TallyApiClient.ts` |
| 📁 `__tests__` |
| 📄 `index.ts` |
| 📄 `submission-service.ts` |
| 📄 `nlp-service.ts` |
| 📄 `template-service.ts` |
| 📄 `form-modification-operations.ts` |
| 📄 `tally-api-service.ts` |
| 📄 `form-modification-parser.ts` |
| 📄 `TokenManager.ts` |
</details>
---
<details>
<summary>Listed directory src/middleware • **7** results</summary>
| Name |
|-------|
| 📄 `rateLimiter.example.ts` |
| 📄 `rateLimiter.ts` |
| 📁 `__tests__` |
| 📄 `index.ts` |
| 📄 `validation.ts` |
| 📄 `sanitization.ts` |
| 📄 `error-handler.ts` |
</details>
---
Now let me examine the package.json to understand the available dependencies for implementing encryption and storage.
---
Read file: package.json
---
Perfect! I can see that we have the necessary dependencies. Now I'll implement the API Key Management System. This will include:
1. A model/schema for API key storage
2. A service for managing API keys (creation, validation, rotation, revocation)
3. Middleware for API key authentication
4. Encryption utilities for secure storage
Let me start by implementing the API key model and schemas:
---
```diff
-
+ import { z } from 'zod';
+
+ /**
+ * API Key Status enum
+ */
+ export enum ApiKeyStatus {
+ ACTIVE = 'active',
+ INACTIVE = 'inactive',
+ REVOKED = 'revoked',
+ EXPIRED = 'expired'
+ }
+
+ /**
+ * API Key Permission Scopes
+ */
+ export enum ApiKeyScope {
+ READ = 'read',
+ WRITE = 'write',
+ ADMIN = 'admin',
+ FORMS_READ = 'forms:read',
+ FORMS_WRITE = 'forms:write',
+ SUBMISSIONS_READ = 'submissions:read',
+ SUBMISSIONS_WRITE = 'submissions:write',
+ WORKSPACES_READ = 'workspaces:read',
+ WORKSPACES_WRITE = 'workspaces:write'
+ }
+
+ /**
+ * Schema for creating a new API key
+ */
+ export const CreateApiKeySchema = z.object({
+ name: z.string().min(1, 'API key name is required').max(100, 'Name too long'),
+ description: z.string().optional(),
+ scopes: z.array(z.nativeEnum(ApiKeyScope)).min(1, 'At least one scope is required'),
+ expiresAt: z.date().optional(),
+ maxUsage: z.number().positive().optional(),
+ ipWhitelist: z.array(z.string().ip()).optional(),
+ metadata: z.record(z.string()).optional()
+ });
+
+ /**
+ * Schema for updating an API key
+ */
+ export const UpdateApiKeySchema = z.object({
+ name: z.string().min(1).max(100).optional(),
+ description: z.string().optional(),
+ scopes: z.array(z.nativeEnum(ApiKeyScope)).optional(),
+ status: z.nativeEnum(ApiKeyStatus).optional(),
+ expiresAt: z.date().optional(),
+ maxUsage: z.number().positive().optional(),
+ ipWhitelist: z.array(z.string().ip()).optional(),
+ metadata: z.record(z.string()).optional()
+ });
+
+ /**
+ * Schema for API key validation
+ */
+ export const ValidateApiKeySchema = z.object({
+ key: z.string().min(1, 'API key is required'),
+ ipAddress: z.string().ip().optional(),
+ userAgent: z.string().optional(),
+ endpoint: z.string().optional()
+ });
+
+ /**
+ * API Key interface
+ */
+ export interface ApiKey {
+ id: string;
+ keyHash: string; // Hashed version of the key for storage
+ name: string;
+ description?: string;
+ scopes: ApiKeyScope[];
+ status: ApiKeyStatus;
+ createdAt: Date;
+ updatedAt: Date;
+ expiresAt?: Date;
+ lastUsedAt?: Date;
+ usageCount: number;
+ maxUsage?: number;
+ ipWhitelist?: string[];
+ metadata?: Record<string, string>;
+ createdBy?: string;
+ }
+
+ /**
+ * API Key with plain text key (only returned on creation)
+ */
+ export interface ApiKeyWithSecret extends Omit<ApiKey, 'keyHash'> {
+ key: string; // Plain text key, only shown once
+ keyHash: string;
+ }
+
+ /**
+ * API Key usage log entry
+ */
+ export interface ApiKeyUsageLog {
+ id: string;
+ keyId: string;
+ timestamp: Date;
+ ipAddress?: string;
+ userAgent?: string;
+ endpoint?: string;
+ success: boolean;
+ errorReason?: string;
+ responseTime?: number;
+ }
+
+ /**
+ * Schema for API key usage logging
+ */
+ export const ApiKeyUsageLogSchema = z.object({
+ keyId: z.string().uuid(),
+ ipAddress: z.string().ip().optional(),
+ userAgent: z.string().optional(),
+ endpoint: z.string().optional(),
+ success: z.boolean(),
+ errorReason: z.string().optional(),
+ responseTime: z.number().positive().optional()
+ });
+
+ /**
+ * API Key rotation configuration
+ */
+ export interface ApiKeyRotationConfig {
+ id: string;
+ keyId: string;
+ rotationIntervalDays: number;
+ notifyDaysBefore: number;
+ autoRotate: boolean;
+ lastRotatedAt?: Date;
+ nextRotationAt: Date;
+ isActive: boolean;
+ }
+
+ /**
+ * Schema for API key rotation configuration
+ */
+ export const ApiKeyRotationConfigSchema = z.object({
+ keyId: z.string().uuid(),
+ rotationIntervalDays: z.number().min(1).max(365),
+ notifyDaysBefore: z.number().min(0).max(30),
+ autoRotate: z.boolean(),
+ isActive: z.boolean().default(true)
+ });
+
+ /**
+ * Type definitions for API key operations
+ */
+ export type CreateApiKeyInput = z.infer<typeof CreateApiKeySchema>;
+ export type UpdateApiKeyInput = z.infer<typeof UpdateApiKeySchema>;
+ export type ValidateApiKeyInput = z.infer<typeof ValidateApiKeySchema>;
+ export type ApiKeyUsageLogInput = z.infer<typeof ApiKeyUsageLogSchema>;
+ export type ApiKeyRotationConfigInput = z.infer<typeof ApiKeyRotationConfigSchema>;
+
+ /**
+ * API Key validation result
+ */
+ export interface ApiKeyValidationResult {
+ isValid: boolean;
+ apiKey?: ApiKey;
+ errorReason?: string;
+ remainingUsage?: number;
+ expiresIn?: number; // seconds until expiration
+ }
+
+ /**
+ * API Key generation options
+ */
+ export interface ApiKeyGenerationOptions {
+ length?: number;
+ prefix?: string;
+ includeChecksum?: boolean;
+ }
+
+ /**
+ * Constants for API key management
+ */
+ export const API_KEY_CONSTANTS = {
+ DEFAULT_KEY_LENGTH: 32,
+ DEFAULT_PREFIX: 'tally_',
+ MIN_KEY_LENGTH: 16,
+ MAX_KEY_LENGTH: 64,
+ HASH_ALGORITHM: 'sha256',
+ ENCRYPTION_ALGORITHM: 'aes-256-gcm',
+ DEFAULT_EXPIRY_DAYS: 90,
+ MAX_USAGE_LOGS_PER_KEY: 1000,
+ RATE_LIMIT_WINDOW_MS: 60000, // 1 minute
+ RATE_LIMIT_MAX_REQUESTS: 100
+ } as const;
```
---
Now I'll create a utility module for encryption and key management operations:
---
```diff
-
+ import crypto from 'crypto';
+ import { API_KEY_CONSTANTS, ApiKeyGenerationOptions } from '../models/api-key';
+
+ /**
+ * Encryption result interface
+ */
+ export interface EncryptionResult {
+ encrypted: string;
+ iv: string;
+ tag: string;
+ }
+
+ /**
+ * Decryption input interface
+ */
+ export interface DecryptionInput {
+ encrypted: string;
+ iv: string;
+ tag: string;
+ }
+
+ /**
+ * Crypto utilities for API key management
+ */
+ export class CryptoUtils {
+ private static readonly algorithm = API_KEY_CONSTANTS.ENCRYPTION_ALGORITHM;
+ private static readonly hashAlgorithm = API_KEY_CONSTANTS.HASH_ALGORITHM;
+
+ /**
+ * Get encryption key from environment variable
+ */
+ private static getEncryptionKey(): Buffer {
+ const key = process.env.API_KEY_ENCRYPTION_KEY;
+ if (!key) {
+ throw new Error('API_KEY_ENCRYPTION_KEY environment variable is required');
+ }
+
+ // If key is shorter than 32 bytes, pad it with zeros
+ const keyBuffer = Buffer.from(key, 'utf8');
+ if (keyBuffer.length < 32) {
+ const paddedKey = Buffer.alloc(32);
+ keyBuffer.copy(paddedKey);
+ return paddedKey;
+ }
+
+ // If key is longer than 32 bytes, truncate it
+ return keyBuffer.slice(0, 32);
+ }
+
+ /**
+ * Generate a cryptographically secure API key
+ */
+ public static generateApiKey(options: ApiKeyGenerationOptions = {}): string {
+ const {
+ length = API_KEY_CONSTANTS.DEFAULT_KEY_LENGTH,
+ prefix = API_KEY_CONSTANTS.DEFAULT_PREFIX,
+ includeChecksum = true
+ } = options;
+
+ // Validate length
+ if (length < API_KEY_CONSTANTS.MIN_KEY_LENGTH || length > API_KEY_CONSTANTS.MAX_KEY_LENGTH) {
+ throw new Error(`Key length must be between ${API_KEY_CONSTANTS.MIN_KEY_LENGTH} and ${API_KEY_CONSTANTS.MAX_KEY_LENGTH}`);
+ }
+
+ // Generate random bytes
+ const randomBytes = crypto.randomBytes(length);
+ const keyBody = randomBytes.toString('base64url');
+
+ // Add checksum if requested
+ let key = prefix + keyBody;
+ if (includeChecksum) {
+ const checksum = this.calculateChecksum(key);
+ key += '_' + checksum;
+ }
+
+ return key;
+ }
+
+ /**
+ * Calculate checksum for API key validation
+ */
+ private static calculateChecksum(data: string): string {
+ return crypto
+ .createHash('sha256')
+ .update(data)
+ .digest('base64url')
+ .slice(0, 8);
+ }
+
+ /**
+ * Validate API key format and checksum
+ */
+ public static validateKeyFormat(key: string): boolean {
+ // Check if key has the expected prefix
+ if (!key.startsWith(API_KEY_CONSTANTS.DEFAULT_PREFIX)) {
+ return false;
+ }
+
+ // Check if key has checksum
+ const parts = key.split('_');
+ if (parts.length < 2) {
+ return false;
+ }
+
+ const checksumPart = parts.pop();
+ const keyWithoutChecksum = parts.join('_');
+
+ // Validate checksum
+ const expectedChecksum = this.calculateChecksum(keyWithoutChecksum);
+ return expectedChecksum === checksumPart;
+ }
+
+ /**
+ * Hash an API key for secure storage
+ */
+ public static hashApiKey(key: string): string {
+ return crypto
+ .createHash(this.hashAlgorithm)
+ .update(key)
+ .digest('hex');
+ }
+
+ /**
+ * Encrypt sensitive data
+ */
+ public static encrypt(plaintext: string): EncryptionResult {
+ const key = this.getEncryptionKey();
+ const iv = crypto.randomBytes(16);
+
+ const cipher = crypto.createCipherGCM(this.algorithm, key, iv);
+
+ let encrypted = cipher.update(plaintext, 'utf8', 'hex');
+ encrypted += cipher.final('hex');
+
+ const tag = cipher.getAuthTag();
+
+ return {
+ encrypted,
+ iv: iv.toString('hex'),
+ tag: tag.toString('hex')
+ };
+ }
+
+ /**
+ * Decrypt sensitive data
+ */
+ public static decrypt(data: DecryptionInput): string {
+ const key = this.getEncryptionKey();
+ const iv = Buffer.from(data.iv, 'hex');
+ const tag = Buffer.from(data.tag, 'hex');
+
+ const decipher = crypto.createDecipherGCM(this.algorithm, key, iv);
+ decipher.setAuthTag(tag);
+
+ let decrypted = decipher.update(data.encrypted, 'hex', 'utf8');
+ decrypted += decipher.final('utf8');
+
+ return decrypted;
+ }
+
+ /**
+ * Generate a secure random salt
+ */
+ public static generateSalt(length: number = 16): string {
+ return crypto.randomBytes(length).toString('hex');
+ }
+
+ /**
+ * Create HMAC signature
+ */
+ public static createHmac(data: string, secret: string): string {
+ return crypto
+ .createHmac('sha256', secret)
+ .update(data)
+ .digest('hex');
+ }
+
+ /**
+ * Verify HMAC signature
+ */
+ public static verifyHmac(data: string, signature: string, secret: string): boolean {
+ const expectedSignature = this.createHmac(data, secret);
+ return crypto.timingSafeEqual(
+ Buffer.from(signature, 'hex'),
+ Buffer.from(expectedSignature, 'hex')
+ );
+ }
+
+ /**
+ * Generate a unique identifier
+ */
+ public static generateId(): string {
+ return crypto.randomUUID();
+ }
+
+ /**
+ * Create a secure hash with salt
+ */
+ public static hashWithSalt(data: string, salt: string): string {
+ return crypto
+ .createHash('sha256')
+ .update(data + salt)
+ .digest('hex');
+ }
+
+ /**
+ * Generate a JWT-like token for temporary authentication
+ */
+ public static generateTempToken(payload: Record<string, any>, expiresInSeconds: number = 3600): string {
+ const header = {
+ typ: 'JWT',
+ alg: 'HS256'
+ };
+
+ const now = Math.floor(Date.now() / 1000);
+ const tokenPayload = {
+ ...payload,
+ iat: now,
+ exp: now + expiresInSeconds
+ };
+
+ const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
+ const encodedPayload = Buffer.from(JSON.stringify(tokenPayload)).toString('base64url');
+
+ const secret = process.env.JWT_SECRET || 'fallback-secret';
+ const signature = this.createHmac(`${encodedHeader}.${encodedPayload}`, secret);
+ const encodedSignature = Buffer.from(signature, 'hex').toString('base64url');
+
+ return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
+ }
+
+ /**
+ * Verify and decode a temporary token
+ */
+ public static verifyTempToken(token: string): { valid: boolean; payload?: any; error?: string } {
+ try {
+ const [encodedHeader, encodedPayload, encodedSignature] = token.split('.');
+
+ if (!encodedHeader || !encodedPayload || !encodedSignature) {
+ return { valid: false, error: 'Invalid token format' };
+ }
+
+ const secret = process.env.JWT_SECRET || 'fallback-secret';
+ const expectedSignature = this.createHmac(`${encodedHeader}.${encodedPayload}`, secret);
+ const expectedEncodedSignature = Buffer.from(expectedSignature, 'hex').toString('base64url');
+
+ if (encodedSignature !== expectedEncodedSignature) {
+ return { valid: false, error: 'Invalid signature' };
+ }
+
+ const payload = JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8'));
+ const now = Math.floor(Date.now() / 1000);
+
+ if (payload.exp && payload.exp < now) {
+ return { valid: false, error: 'Token expired' };
+ }
+
+ return { valid: true, payload };
+ } catch (error) {
+ return { valid: false, error: 'Token parsing failed' };
+ }
+ }
+
+ /**
+ * Mask sensitive data for logging
+ */
+ public static maskSensitiveData(data: string, visibleChars: number = 4): string {
+ if (data.length <= visibleChars * 2) {
+ return '*'.repeat(data.length);
+ }
+
+ const start = data.slice(0, visibleChars);
+ const end = data.slice(-visibleChars);
+ const middle = '*'.repeat(data.length - (visibleChars * 2));
+
+ return `${start}${middle}${end}`;
+ }
+ }
```
---
I need to fix the linter errors by using the correct Node.js crypto API. Let me fix the encryption methods:
---
---
Let me fix both methods to use the correct Node.js crypto API:
---
---
Great! Now let me create the API Key Service that handles all the CRUD operations and business logic:
---
```diff
-
+ import {
+ ApiKey,
+ ApiKeyWithSecret,
+ ApiKeyStatus,
+ ApiKeyScope,
+ ApiKeyUsageLog,
+ ApiKeyRotationConfig,
+ ApiKeyValidationResult,
+ CreateApiKeyInput,
+ UpdateApiKeyInput,
+ ValidateApiKeyInput,
+ ApiKeyUsageLogInput,
+ ApiKeyRotationConfigInput,
+ CreateApiKeySchema,
+ UpdateApiKeySchema,
+ ValidateApiKeySchema,
+ ApiKeyUsageLogSchema,
+ ApiKeyRotationConfigSchema,
+ API_KEY_CONSTANTS
+ } from '../models/api-key';
+ import { CryptoUtils } from '../utils/crypto';
+ import { z } from 'zod';
+
+ /**
+ * In-memory storage for API keys (in production, this would be a database)
+ */
+ class ApiKeyStorage {
+ private apiKeys: Map<string, ApiKey> = new Map();
+ private usageLogs: Map<string, ApiKeyUsageLog[]> = new Map();
+ private rotationConfigs: Map<string, ApiKeyRotationConfig> = new Map();
+
+ // API Key operations
+ async createApiKey(apiKey: ApiKey): Promise<void> {
+ this.apiKeys.set(apiKey.id, { ...apiKey });
+ }
+
+ async getApiKey(id: string): Promise<ApiKey | null> {
+ return this.apiKeys.get(id) || null;
+ }
+
+ async getApiKeyByHash(keyHash: string): Promise<ApiKey | null> {
+ for (const apiKey of this.apiKeys.values()) {
+ if (apiKey.keyHash === keyHash) {
+ return { ...apiKey };
+ }
+ }
+ return null;
+ }
+
+ async getAllApiKeys(): Promise<ApiKey[]> {
+ return Array.from(this.apiKeys.values()).map(key => ({ ...key }));
+ }
+
+ async updateApiKey(id: string, updates: Partial<ApiKey>): Promise<boolean> {
+ const existing = this.apiKeys.get(id);
+ if (!existing) return false;
+
+ this.apiKeys.set(id, { ...existing, ...updates, updatedAt: new Date() });
+ return true;
+ }
+
+ async deleteApiKey(id: string): Promise<boolean> {
+ const deleted = this.apiKeys.delete(id);
+ if (deleted) {
+ this.usageLogs.delete(id);
+ this.rotationConfigs.delete(id);
+ }
+ return deleted;
+ }
+
+ // Usage log operations
+ async addUsageLog(log: ApiKeyUsageLog): Promise<void> {
+ const logs = this.usageLogs.get(log.keyId) || [];
+ logs.push({ ...log });
+
+ // Keep only the last N logs per key
+ if (logs.length > API_KEY_CONSTANTS.MAX_USAGE_LOGS_PER_KEY) {
+ logs.splice(0, logs.length - API_KEY_CONSTANTS.MAX_USAGE_LOGS_PER_KEY);
+ }
+
+ this.usageLogs.set(log.keyId, logs);
+ }
+
+ async getUsageLogs(keyId: string, limit?: number): Promise<ApiKeyUsageLog[]> {
+ const logs = this.usageLogs.get(keyId) || [];
+ return limit ? logs.slice(-limit) : [...logs];
+ }
+
+ // Rotation config operations
+ async createRotationConfig(config: ApiKeyRotationConfig): Promise<void> {
+ this.rotationConfigs.set(config.keyId, { ...config });
+ }
+
+ async getRotationConfig(keyId: string): Promise<ApiKeyRotationConfig | null> {
+ return this.rotationConfigs.get(keyId) || null;
+ }
+
+ async updateRotationConfig(keyId: string, updates: Partial<ApiKeyRotationConfig>): Promise<boolean> {
+ const existing = this.rotationConfigs.get(keyId);
+ if (!existing) return false;
+
+ this.rotationConfigs.set(keyId, { ...existing, ...updates });
+ return true;
+ }
+
+ async deleteRotationConfig(keyId: string): Promise<boolean> {
+ return this.rotationConfigs.delete(keyId);
+ }
+
+ async getKeysNeedingRotation(): Promise<ApiKeyRotationConfig[]> {
+ const now = new Date();
+ return Array.from(this.rotationConfigs.values())
+ .filter(config => config.isActive && config.nextRotationAt <= now);
+ }
+ }
+
+ /**
+ * API Key Management Service
+ */
+ export class ApiKeyService {
+ private storage: ApiKeyStorage;
+
+ constructor() {
+ this.storage = new ApiKeyStorage();
+ this.initializeBootstrapKeys();
+ }
+
+ /**
+ * Initialize bootstrap API keys from environment variables
+ */
+ private async initializeBootstrapKeys(): Promise<void> {
+ const bootstrapKey = process.env.BOOTSTRAP_API_KEY;
+ if (bootstrapKey && CryptoUtils.validateKeyFormat(bootstrapKey)) {
+ const keyHash = CryptoUtils.hashApiKey(bootstrapKey);
+ const existing = await this.storage.getApiKeyByHash(keyHash);
+
+ if (!existing) {
+ const apiKey: ApiKey = {
+ id: CryptoUtils.generateId(),
+ keyHash,
+ name: 'Bootstrap Key',
+ description: 'Initial bootstrap API key from environment',
+ scopes: [ApiKeyScope.ADMIN],
+ status: ApiKeyStatus.ACTIVE,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ usageCount: 0,
+ createdBy: 'system'
+ };
+
+ await this.storage.createApiKey(apiKey);
+ console.log('Bootstrap API key initialized');
+ }
+ }
+ }
+
+ /**
+ * Create a new API key
+ */
+ async createApiKey(input: CreateApiKeyInput, createdBy?: string): Promise<ApiKeyWithSecret> {
+ // Validate input
+ const validatedInput = CreateApiKeySchema.parse(input);
+
+ // Generate the API key
+ const plainKey = CryptoUtils.generateApiKey();
+ const keyHash = CryptoUtils.hashApiKey(plainKey);
+
+ // Create the API key object
+ const apiKey: ApiKey = {
+ id: CryptoUtils.generateId(),
+ keyHash,
+ name: validatedInput.name,
+ description: validatedInput.description,
+ scopes: validatedInput.scopes,
+ status: ApiKeyStatus.ACTIVE,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ expiresAt: validatedInput.expiresAt,
+ usageCount: 0,
+ maxUsage: validatedInput.maxUsage,
+ ipWhitelist: validatedInput.ipWhitelist,
+ metadata: validatedInput.metadata,
+ createdBy
+ };
+
+ // Store the API key
+ await this.storage.createApiKey(apiKey);
+
+ return {
+ ...apiKey,
+ key: plainKey
+ };
+ }
+
+ /**
+ * Validate an API key
+ */
+ async validateApiKey(input: ValidateApiKeyInput): Promise<ApiKeyValidationResult> {
+ try {
+ const validatedInput = ValidateApiKeySchema.parse(input);
+
+ // Check key format first
+ if (!CryptoUtils.validateKeyFormat(validatedInput.key)) {
+ return {
+ isValid: false,
+ errorReason: 'Invalid API key format'
+ };
+ }
+
+ // Hash the key and look it up
+ const keyHash = CryptoUtils.hashApiKey(validatedInput.key);
+ const apiKey = await this.storage.getApiKeyByHash(keyHash);
+
+ if (!apiKey) {
+ await this.logUsage(validatedInput.key, {
+ success: false,
+ errorReason: 'API key not found',
+ ipAddress: validatedInput.ipAddress,
+ userAgent: validatedInput.userAgent,
+ endpoint: validatedInput.endpoint
+ });
+
+ return {
+ isValid: false,
+ errorReason: 'API key not found'
+ };
+ }
+
+ // Check if key is active
+ if (apiKey.status !== ApiKeyStatus.ACTIVE) {
+ await this.logUsage(validatedInput.key, {
+ success: false,
+ errorReason: `API key is ${apiKey.status}`,
+ ipAddress: validatedInput.ipAddress,
+ userAgent: validatedInput.userAgent,
+ endpoint: validatedInput.endpoint
+ });
+
+ return {
+ isValid: false,
+ errorReason: `API key is ${apiKey.status}`,
+ apiKey
+ };
+ }
+
+ // Check expiration
+ if (apiKey.expiresAt && apiKey.expiresAt <= new Date()) {
+ // Auto-expire the key
+ await this.storage.updateApiKey(apiKey.id, { status: ApiKeyStatus.EXPIRED });
+
+ await this.logUsage(validatedInput.key, {
+ success: false,
+ errorReason: 'API key expired',
+ ipAddress: validatedInput.ipAddress,
+ userAgent: validatedInput.userAgent,
+ endpoint: validatedInput.endpoint
+ });
+
+ return {
+ isValid: false,
+ errorReason: 'API key expired',
+ apiKey
+ };
+ }
+
+ // Check usage limits
+ if (apiKey.maxUsage && apiKey.usageCount >= apiKey.maxUsage) {
+ await this.logUsage(validatedInput.key, {
+ success: false,
+ errorReason: 'Usage limit exceeded',
+ ipAddress: validatedInput.ipAddress,
+ userAgent: validatedInput.userAgent,
+ endpoint: validatedInput.endpoint
+ });
+
+ return {
+ isValid: false,
+ errorReason: 'Usage limit exceeded',
+ apiKey,
+ remainingUsage: 0
+ };
+ }
+
+ // Check IP whitelist
+ if (apiKey.ipWhitelist && validatedInput.ipAddress) {
+ const isIpAllowed = apiKey.ipWhitelist.includes(validatedInput.ipAddress);
+ if (!isIpAllowed) {
+ await this.logUsage(validatedInput.key, {
+ success: false,
+ errorReason: 'IP address not whitelisted',
+ ipAddress: validatedInput.ipAddress,
+ userAgent: validatedInput.userAgent,
+ endpoint: validatedInput.endpoint
+ });
+
+ return {
+ isValid: false,
+ errorReason: 'IP address not whitelisted',
+ apiKey
+ };
+ }
+ }
+
+ // Update usage statistics
+ await this.storage.updateApiKey(apiKey.id, {
+ usageCount: apiKey.usageCount + 1,
+ lastUsedAt: new Date()
+ });
+
+ // Log successful usage
+ await this.logUsage(validatedInput.key, {
+ success: true,
+ ipAddress: validatedInput.ipAddress,
+ userAgent: validatedInput.userAgent,
+ endpoint: validatedInput.endpoint
+ });
+
+ const remainingUsage = apiKey.maxUsage ? apiKey.maxUsage - apiKey.usageCount - 1 : undefined;
+ const expiresIn = apiKey.expiresAt ? Math.floor((apiKey.expiresAt.getTime() - Date.now()) / 1000) : undefined;
+
+ return {
+ isValid: true,
+ apiKey: { ...apiKey, usageCount: apiKey.usageCount + 1, lastUsedAt: new Date() },
+ remainingUsage,
+ expiresIn
+ };
+
+ } catch (error) {
+ if (error instanceof z.ZodError) {
+ return {
+ isValid: false,
+ errorReason: 'Invalid validation input: ' + error.errors.map(e => e.message).join(', ')
+ };
+ }
+
+ return {
+ isValid: false,
+ errorReason: 'Validation error: ' + (error as Error).message
+ };
+ }
+ }
+
+ /**
+ * Get API key by ID
+ */
+ async getApiKey(id: string): Promise<ApiKey | null> {
+ return this.storage.getApiKey(id);
+ }
+
+ /**
+ * List all API keys
+ */
+ async listApiKeys(status?: ApiKeyStatus): Promise<ApiKey[]> {
+ const allKeys = await this.storage.getAllApiKeys();
+ return status ? allKeys.filter(key => key.status === status) : allKeys;
+ }
+
+ /**
+ * Update an API key
+ */
+ async updateApiKey(id: string, input: UpdateApiKeyInput): Promise<ApiKey | null> {
+ const validatedInput = UpdateApiKeySchema.parse(input);
+
+ const updated = await this.storage.updateApiKey(id, validatedInput);
+ return updated ? this.storage.getApiKey(id) : null;
+ }
+
+ /**
+ * Revoke an API key
+ */
+ async revokeApiKey(id: string): Promise<boolean> {
+ return this.storage.updateApiKey(id, { status: ApiKeyStatus.REVOKED });
+ }
+
+ /**
+ * Delete an API key permanently
+ */
+ async deleteApiKey(id: string): Promise<boolean> {
+ return this.storage.deleteApiKey(id);
+ }
+
+ /**
+ * Log API key usage
+ */
+ private async logUsage(key: string, details: Omit<ApiKeyUsageLogInput, 'keyId'>): Promise<void> {
+ try {
+ // Find the key ID by hash
+ const keyHash = CryptoUtils.hashApiKey(key);
+ const apiKey = await this.storage.getApiKeyByHash(keyHash);
+
+ if (apiKey) {
+ const log: ApiKeyUsageLog = {
+ id: CryptoUtils.generateId(),
+ keyId: apiKey.id,
+ timestamp: new Date(),
+ ...details
+ };
+
+ await this.storage.addUsageLog(log);
+ }
+ } catch (error) {
+ console.error('Failed to log API key usage:', error);
+ }
+ }
+
+ /**
+ * Get usage logs for an API key
+ */
+ async getUsageLogs(keyId: string, limit?: number): Promise<ApiKeyUsageLog[]> {
+ return this.storage.getUsageLogs(keyId, limit);
+ }
+
+ /**
+ * Rotate an API key
+ */
+ async rotateApiKey(id: string): Promise<ApiKeyWithSecret | null> {
+ const existingKey = await this.storage.getApiKey(id);
+ if (!existingKey) {
+ return null;
+ }
+
+ // Generate new key
+ const newPlainKey = CryptoUtils.generateApiKey();
+ const newKeyHash = CryptoUtils.hashApiKey(newPlainKey);
+
+ // Update the existing key
+ const updated = await this.storage.updateApiKey(id, {
+ keyHash: newKeyHash,
+ updatedAt: new Date(),
+ usageCount: 0, // Reset usage count after rotation
+ lastUsedAt: undefined
+ });
+
+ if (!updated) {
+ return null;
+ }
+
+ const updatedKey = await this.storage.getApiKey(id);
+ if (!updatedKey) {
+ return null;
+ }
+
+ // Update rotation config if it exists
+ const rotationConfig = await this.storage.getRotationConfig(id);
+ if (rotationConfig) {
+ const nextRotationAt = new Date();
+ nextRotationAt.setDate(nextRotationAt.getDate() + rotationConfig.rotationIntervalDays);
+
+ await this.storage.updateRotationConfig(id, {
+ lastRotatedAt: new Date(),
+ nextRotationAt
+ });
+ }
+
+ return {
+ ...updatedKey,
+ key: newPlainKey
+ };
+ }
+
+ /**
+ * Setup automatic rotation for an API key
+ */
+ async setupRotation(keyId: string, config: ApiKeyRotationConfigInput): Promise<ApiKeyRotationConfig | null> {
+ const validatedConfig = ApiKeyRotationConfigSchema.parse(config);
+
+ // Verify the key exists
+ const apiKey = await this.storage.getApiKey(keyId);
+ if (!apiKey) {
+ return null;
+ }
+
+ const nextRotationAt = new Date();
+ nextRotationAt.setDate(nextRotationAt.getDate() + validatedConfig.rotationIntervalDays);
+
+ const rotationConfig: ApiKeyRotationConfig = {
+ id: CryptoUtils.generateId(),
+ keyId,
+ rotationIntervalDays: validatedConfig.rotationIntervalDays,
+ notifyDaysBefore: validatedConfig.notifyDaysBefore,
+ autoRotate: validatedConfig.autoRotate,
+ nextRotationAt,
+ isActive: validatedConfig.isActive
+ };
+
+ await this.storage.createRotationConfig(rotationConfig);
+ return rotationConfig;
+ }
+
+ /**
+ * Get rotation configuration for an API key
+ */
+ async getRotationConfig(keyId: string): Promise<ApiKeyRotationConfig | null> {
+ return this.storage.getRotationConfig(keyId);
+ }
+
+ /**
+ * Process automatic key rotations
+ */
+ async processAutomaticRotations(): Promise<string[]> {
+ const keysNeedingRotation = await this.storage.getKeysNeedingRotation();
+ const rotatedKeys: string[] = [];
+
+ for (const config of keysNeedingRotation) {
+ if (config.autoRotate) {
+ try {
+ const rotated = await this.rotateApiKey(config.keyId);
+ if (rotated) {
+ rotatedKeys.push(config.keyId);
+ console.log(`Automatically rotated API key: ${CryptoUtils.maskSensitiveData(rotated.name)}`);
+ }
+ } catch (error) {
+ console.error(`Failed to auto-rotate key ${config.keyId}:`, error);
+ }
+ }
+ }
+
+ return rotatedKeys;
+ }
+
+ /**
+ * Check if an API key has required scopes
+ */
+ hasRequiredScopes(apiKey: ApiKey, requiredScopes: ApiKeyScope[]): boolean {
+ // Admin scope grants all permissions
+ if (apiKey.scopes.includes(ApiKeyScope.ADMIN)) {
+ return true;
+ }
+
+ // Check if all required scopes are present
+ return requiredScopes.every(scope => apiKey.scopes.includes(scope));
+ }
+
+ /**
+ * Get API key statistics
+ */
+ async getApiKeyStats(): Promise<{
+ total: number;
+ active: number;
+ expired: number;
+ revoked: number;
+ totalUsage: number;
+ }> {
+ const allKeys = await this.storage.getAllApiKeys();
+
+ return {
+ total: allKeys.length,
+ active: allKeys.filter(k => k.status === ApiKeyStatus.ACTIVE).length,
+ expired: allKeys.filter(k => k.status === ApiKeyStatus.EXPIRED).length,
+ revoked: allKeys.filter(k => k.status === ApiKeyStatus.REVOKED).length,
+ totalUsage: allKeys.reduce((sum, key) => sum + key.usageCount, 0)
+ };
+ }
+ }
+
+ // Export a singleton instance
+ export const apiKeyService = new ApiKeyService();
```
---
I need to fix the TypeScript strict optional properties issues. Let me update the interfaces to properly handle optional properties:
---
---
---
Let me fix the remaining TypeScript issues by being more explicit about optional properties:
---
---
Let me fix the remaining two issues by handling the optional properties correctly:
---
---
Let me fix the final issue with the lastUsedAt property:
---
---
Great! Now let me create the API key authentication middleware:
---
```diff
-
+ import { Request, Response, NextFunction } from 'express';
+ import { apiKeyService } from '../services/api-key-service';
+ import { ApiKeyScope, ApiKeyValidationResult } from '../models/api-key';
+ import { CryptoUtils } from '../utils/crypto';
+
+ /**
+ * Extended Request interface to include API key information
+ */
+ export interface AuthenticatedRequest extends Request {
+ apiKey?: {
+ id: string;
+ name: string;
+ scopes: ApiKeyScope[];
+ usageCount: number;
+ remainingUsage?: number;
+ expiresIn?: number;
+ };
+ }
+
+ /**
+ * Options for API key authentication middleware
+ */
+ export interface ApiKeyAuthOptions {
+ /**
+ * Required scopes for the endpoint
+ */
+ requiredScopes?: ApiKeyScope[];
+
+ /**
+ * Whether to allow requests without API keys (optional authentication)
+ */
+ optional?: boolean;
+
+ /**
+ * Custom error messages
+ */
+ errorMessages?: {
+ missing?: string;
+ invalid?: string;
+ insufficientScopes?: string;
+ expired?: string;
+ revoked?: string;
+ usageLimitExceeded?: string;
+ ipNotWhitelisted?: string;
+ };
+
+ /**
+ * Header name for API key (default: 'x-api-key')
+ */
+ headerName?: string;
+
+ /**
+ * Whether to log authentication attempts
+ */
+ logAttempts?: boolean;
+ }
+
+ /**
+ * Default options for API key authentication
+ */
+ const defaultOptions: Required<ApiKeyAuthOptions> = {
+ requiredScopes: [],
+ optional: false,
+ errorMessages: {
+ missing: 'API key is required',
+ invalid: 'Invalid API key',
+ insufficientScopes: 'Insufficient permissions',
+ expired: 'API key has expired',
+ revoked: 'API key has been revoked',
+ usageLimitExceeded: 'API key usage limit exceeded',
+ ipNotWhitelisted: 'IP address not authorized for this API key'
+ },
+ headerName: 'x-api-key',
+ logAttempts: true
+ };
+
+ /**
+ * Extract API key from request headers
+ */
+ function extractApiKey(req: Request, headerName: string): string | null {
+ // Try the configured header first
+ let apiKey = req.headers[headerName] as string;
+
+ // Try common variations
+ if (!apiKey) {
+ apiKey = req.headers['authorization'] as string;
+ if (apiKey && apiKey.startsWith('Bearer ')) {
+ apiKey = apiKey.slice(7);
+ } else if (apiKey && apiKey.startsWith('ApiKey ')) {
+ apiKey = apiKey.slice(7);
+ }
+ }
+
+ // Try query parameter as fallback (not recommended for production)
+ if (!apiKey && process.env.NODE_ENV !== 'production') {
+ apiKey = req.query.apiKey as string || req.query.api_key as string;
+ }
+
+ return apiKey ? apiKey.trim() : null;
+ }
+
+ /**
+ * Get client IP address
+ */
+ function getClientIp(req: Request): string {
+ const xForwardedFor = req.headers['x-forwarded-for'] as string;
+ const xRealIp = req.headers['x-real-ip'] as string;
+ const cfConnectingIp = req.headers['cf-connecting-ip'] as string;
+
+ // Try various headers
+ if (xForwardedFor) {
+ return xForwardedFor.split(',')[0].trim();
+ }
+
+ if (xRealIp) {
+ return xRealIp;
+ }
+
+ if (cfConnectingIp) {
+ return cfConnectingIp;
+ }
+
+ return req.socket.remoteAddress || req.ip || 'unknown';
+ }
+
+ /**
+ * API Key Authentication Middleware
+ */
+ export function apiKeyAuth(options: ApiKeyAuthOptions = {}): (req: Request, res: Response, next: NextFunction) => Promise<void> {
+ const config = { ...defaultOptions, ...options };
+
+ return async (req: Request, res: Response, next: NextFunction): Promise<void> => {
+ const startTime = Date.now();
+
+ try {
+ // Extract API key from request
+ const apiKey = extractApiKey(req, config.headerName);
+
+ // Handle missing API key
+ if (!apiKey) {
+ if (config.optional) {
+ return next();
+ }
+
+ if (config.logAttempts) {
+ console.log(`API key authentication failed - Missing key for ${req.method} ${req.path} from ${getClientIp(req)}`);
+ }
+
+ return res.status(401).json({
+ error: 'Authentication failed',
+ message: config.errorMessages.missing,
+ code: 'MISSING_API_KEY'
+ });
+ }
+
+ // Validate API key
+ const validationResult: ApiKeyValidationResult = await apiKeyService.validateApiKey({
+ key: apiKey,
+ ipAddress: getClientIp(req),
+ userAgent: req.headers['user-agent'],
+ endpoint: `${req.method} ${req.path}`
+ });
+
+ // Handle validation failure
+ if (!validationResult.isValid) {
+ const responseTime = Date.now() - startTime;
+
+ if (config.logAttempts) {
+ console.log(`API key authentication failed - ${validationResult.errorReason} for ${req.method} ${req.path} from ${getClientIp(req)} (${responseTime}ms)`);
+ }
+
+ let statusCode = 401;
+ let errorCode = 'INVALID_API_KEY';
+ let message = config.errorMessages.invalid;
+
+ // Customize error response based on failure reason
+ if (validationResult.errorReason?.includes('expired')) {
+ statusCode = 401;
+ errorCode = 'EXPIRED_API_KEY';
+ message = config.errorMessages.expired;
+ } else if (validationResult.errorReason?.includes('revoked')) {
+ statusCode = 403;
+ errorCode = 'REVOKED_API_KEY';
+ message = config.errorMessages.revoked;
+ } else if (validationResult.errorReason?.includes('usage limit')) {
+ statusCode = 429;
+ errorCode = 'USAGE_LIMIT_EXCEEDED';
+ message = config.errorMessages.usageLimitExceeded;
+ } else if (validationResult.errorReason?.includes('IP')) {
+ statusCode = 403;
+ errorCode = 'IP_NOT_WHITELISTED';
+ message = config.errorMessages.ipNotWhitelisted;
+ }
+
+ return res.status(statusCode).json({
+ error: 'Authentication failed',
+ message,
+ code: errorCode,
+ ...(validationResult.remainingUsage !== undefined && { remainingUsage: validationResult.remainingUsage }),
+ ...(validationResult.expiresIn !== undefined && { expiresIn: validationResult.expiresIn })
+ });
+ }
+
+ // Check required scopes
+ if (config.requiredScopes.length > 0 && validationResult.apiKey) {
+ const hasRequiredScopes = apiKeyService.hasRequiredScopes(validationResult.apiKey, config.requiredScopes);
+
+ if (!hasRequiredScopes) {
+ if (config.logAttempts) {
+ console.log(`API key authorization failed - Insufficient scopes for ${req.method} ${req.path}. Required: ${config.requiredScopes.join(', ')}, Has: ${validationResult.apiKey.scopes.join(', ')}`);
+ }
+
+ return res.status(403).json({
+ error: 'Authorization failed',
+ message: config.errorMessages.insufficientScopes,
+ code: 'INSUFFICIENT_SCOPES',
+ requiredScopes: config.requiredScopes,
+ availableScopes: validationResult.apiKey.scopes
+ });
+ }
+ }
+
+ // Attach API key info to request
+ if (validationResult.apiKey) {
+ (req as AuthenticatedRequest).apiKey = {
+ id: validationResult.apiKey.id,
+ name: validationResult.apiKey.name,
+ scopes: validationResult.apiKey.scopes,
+ usageCount: validationResult.apiKey.usageCount,
+ remainingUsage: validationResult.remainingUsage,
+ expiresIn: validationResult.expiresIn
+ };
+ }
+
+ // Add security headers
+ res.setHeader('X-API-Key-ID', CryptoUtils.maskSensitiveData(validationResult.apiKey?.id || 'unknown'));
+ res.setHeader('X-Rate-Limit-Remaining', validationResult.remainingUsage?.toString() || 'unlimited');
+
+ if (config.logAttempts) {
+ const responseTime = Date.now() - startTime;
+ console.log(`API key authentication successful for ${validationResult.apiKey?.name} - ${req.method} ${req.path} (${responseTime}ms)`);
+ }
+
+ next();
+
+ } catch (error) {
+ console.error('API key authentication error:', error);
+
+ return res.status(500).json({
+ error: 'Authentication error',
+ message: 'Internal server error during authentication',
+ code: 'INTERNAL_ERROR'
+ });
+ }
+ };
+ }
+
+ /**
+ * Middleware factory for specific scope requirements
+ */
+ export const requireScopes = (scopes: ApiKeyScope[]) => apiKeyAuth({ requiredScopes: scopes });
+
+ /**
+ * Middleware for read-only access
+ */
+ export const requireReadAccess = () => apiKeyAuth({
+ requiredScopes: [ApiKeyScope.READ]
+ });
+
+ /**
+ * Middleware for write access
+ */
+ export const requireWriteAccess = () => apiKeyAuth({
+ requiredScopes: [ApiKeyScope.WRITE]
+ });
+
+ /**
+ * Middleware for admin access
+ */
+ export const requireAdminAccess = () => apiKeyAuth({
+ requiredScopes: [ApiKeyScope.ADMIN]
+ });
+
+ /**
+ * Middleware for forms read access
+ */
+ export const requireFormsReadAccess = () => apiKeyAuth({
+ requiredScopes: [ApiKeyScope.FORMS_READ]
+ });
+
+ /**
+ * Middleware for forms write access
+ */
+ export const requireFormsWriteAccess = () => apiKeyAuth({
+ requiredScopes: [ApiKeyScope.FORMS_WRITE]
+ });
+
+ /**
+ * Middleware for submissions read access
+ */
+ export const requireSubmissionsReadAccess = () => apiKeyAuth({
+ requiredScopes: [ApiKeyScope.SUBMISSIONS_READ]
+ });
+
+ /**
+ * Middleware for submissions write access
+ */
+ export const requireSubmissionsWriteAccess = () => apiKeyAuth({
+ requiredScopes: [ApiKeyScope.SUBMISSIONS_WRITE]
+ });
+
+ /**
+ * Optional authentication middleware (doesn't fail if no API key)
+ */
+ export const optionalApiKeyAuth = () => apiKeyAuth({ optional: true });
+
+ /**
+ * Helper function to check if request is authenticated
+ */
+ export function isAuthenticated(req: Request): boolean {
+ return !!(req as AuthenticatedRequest).apiKey;
+ }
+
+ /**
+ * Helper function to get API key info from request
+ */
+ export function getApiKeyInfo(req: Request): AuthenticatedRequest['apiKey'] | undefined {
+ return (req as AuthenticatedRequest).apiKey;
+ }
```
---
I've hit the 3-iteration limit for fixing linter errors, but let me create tests for the API Key Management System to demonstrate its functionality:
---
Cancelled
---
Let me update the models index to export the new API key models:
---
---
Read file: src/models/index.ts
---
---
---