tpmjs/apps/web/src/lib/api-keys/index.ts
Ajax Davis a52d32c367 feat: add resend tools, MCP collection endpoint, and UI refinements
- Add @tpmjs/tools-resend with email API tools and blocks.yml entries
- Add MCP route for collection skill discovery
- Add InstallationSection component for collections
- Refactor collection pages to use shared components and simplify layouts
- Update skills questions API, rate limiting, and API key handling
- Add tpmjs-tool-creator skill for Claude
- Update video feature scenes and fix lint issues
- Update .gitignore with IDE and temp file exclusions
2026-02-07 00:49:40 +10:00

121 lines
3.3 KiB
TypeScript

import { createHash, randomBytes } from 'node:crypto';
/**
* API Key Utilities for TPMJS
*
* Keys are prefixed with 'tpmjs_sk_' for easy identification in logs/configs.
* We only store SHA-256 hashes - the raw key is shown once at creation.
*/
const API_KEY_PREFIX = 'tpmjs_sk_';
const RANDOM_BYTES_LENGTH = 32;
export interface GeneratedApiKey {
/** The raw API key (only shown once at creation) */
rawKey: string;
/** SHA-256 hash for storage and lookup */
keyHash: string;
/** First 16 characters for display (e.g., "tpmjs_sk_abc123...") */
keyPrefix: string;
}
/**
* Generates a new TPMJS API key
*
* @returns Object containing rawKey (show once), keyHash (for storage), keyPrefix (for display)
*
* @example
* const { rawKey, keyHash, keyPrefix } = generateApiKey();
* // rawKey: "tpmjs_sk_abc123..." (show to user once)
* // keyHash: "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae"
* // keyPrefix: "tpmjs_sk_abc123..."
*/
export function generateApiKey(): GeneratedApiKey {
const randomPart = randomBytes(RANDOM_BYTES_LENGTH).toString('base64url');
const rawKey = `${API_KEY_PREFIX}${randomPart}`;
const keyHash = hashApiKey(rawKey);
const keyPrefix = rawKey.substring(0, 16);
return { rawKey, keyHash, keyPrefix };
}
/**
* Hashes an API key using SHA-256 for storage and lookup
*
* We use hashing instead of encryption because:
* 1. We never need to recover the original key
* 2. Users can generate new keys if lost
* 3. Simpler and more secure (no encryption key to manage)
*
* @param rawKey - The raw API key to hash
* @returns SHA-256 hash as hex string (64 characters)
*/
export function hashApiKey(rawKey: string): string {
return createHash('sha256').update(rawKey).digest('hex');
}
/**
* Validates API key format
*
* @param key - The key to validate
* @returns True if the key has valid format
*/
export function isValidApiKeyFormat(key: string): boolean {
// Must start with prefix and be at least 40 chars (prefix + some random bytes)
return key.startsWith(API_KEY_PREFIX) && key.length >= 40;
}
/**
* Masks an API key for safe display
*
* @param keyPrefix - The key prefix (first 16 chars)
* @returns Masked string like "tpmjs_sk_abc1..."
*/
export function maskApiKey(keyPrefix: string): string {
return `${keyPrefix}...`;
}
/**
* API key scopes for granular permissions
*/
export const API_KEY_SCOPES = {
/** Execute MCP tools */
MCP_EXECUTE: 'mcp:execute',
/** Chat with agents */
AGENT_CHAT: 'agent:chat',
/** Connect via bridge */
BRIDGE_CONNECT: 'bridge:connect',
/** Read usage data */
USAGE_READ: 'usage:read',
/** Read collection data */
COLLECTION_READ: 'collection:read',
} as const;
export type ApiKeyScope = (typeof API_KEY_SCOPES)[keyof typeof API_KEY_SCOPES];
/**
* Default scopes for new API keys
*/
export const DEFAULT_API_KEY_SCOPES: ApiKeyScope[] = [
API_KEY_SCOPES.MCP_EXECUTE,
API_KEY_SCOPES.AGENT_CHAT,
API_KEY_SCOPES.BRIDGE_CONNECT,
API_KEY_SCOPES.USAGE_READ,
API_KEY_SCOPES.COLLECTION_READ,
];
/**
* Rate limits by user tier (requests per hour)
*/
export const RATE_LIMITS_BY_TIER = {
FREE: 1000,
PRO: 10000,
ENTERPRISE: 100000,
} as const;
/**
* Gets the rate limit for a user tier
*/
export function getRateLimitForTier(tier: keyof typeof RATE_LIMITS_BY_TIER): number {
return RATE_LIMITS_BY_TIER[tier];
}