UNPKG

keyvault-api-sdk

Version:

Official SDK for KeyVault - Secure API key management with user-specific encryption and team support

631 lines (630 loc) 25.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.getGitHubToken = exports.getAWSKey = exports.getOpenAIKey = exports.getStripeKey = exports.getAPIKeyByService = exports.getAPIKey = exports.createKeyVaultFromConfig = exports.createKeyVaultFromEnv = exports.KeyVaultSDK = void 0; const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const os_1 = __importDefault(require("os")); const crypto_1 = __importDefault(require("crypto")); class KeyVaultSDK { constructor(config) { this.cache = new Map(); this.defaultTTL = 5 * 60 * 1000; // 5 minutes this.apiUrl = config?.apiUrl || 'https://1pass.vercel.app'; this.apiUrl = this.apiUrl.replace(/\/$/, ''); this.apiToken = config?.apiToken; if (config?.ttl) { this.defaultTTL = config.ttl; } } /** * Load configuration from ~/.keyvault/config.json (like CLI does) * Sets up user private key and authentication automatically */ async configfile() { const configPath = path_1.default.join(os_1.default.homedir(), '.keyvault', 'config.json'); try { const configData = await fs_1.default.promises.readFile(configPath, 'utf8'); this.config = JSON.parse(configData); if (this.config) { this.apiUrl = this.config.apiUrl || this.apiUrl; this.apiToken = this.config.token; this.currentTeam = this.config.currentTeam; // Get or generate user private key this.userPrivateKey = await this.getUserPrivateKey(); } } catch (error) { throw new Error('Failed to read KeyVault config file. Please run CLI login first or ensure ~/.keyvault/config.json exists.'); } } /** * Get or generate user's private key for client-side encryption */ async getUserPrivateKey() { if (!this.config) { throw new Error('Config not loaded. Call configfile() first.'); } // If private key exists in config, use it if (this.config.privateKey) { return this.config.privateKey; } // Generate new private key const privateKey = crypto_1.default.randomBytes(32).toString('hex'); // Save to config this.config.privateKey = privateKey; await this.saveConfig(); return privateKey; } /** * Save current config back to file */ async saveConfig() { if (!this.config) return; const configPath = path_1.default.join(os_1.default.homedir(), '.keyvault', 'config.json'); const configDir = path_1.default.dirname(configPath); // Ensure directory exists await fs_1.default.promises.mkdir(configDir, { recursive: true }); // Save config await fs_1.default.promises.writeFile(configPath, JSON.stringify(this.config, null, 2)); // Set secure permissions (owner read/write only) await fs_1.default.promises.chmod(configPath, 0o600); } /** * Client-side encryption using user's private key */ encryptValue(value) { if (!this.userPrivateKey) { throw new Error('User private key not available. Call configfile() first.'); } const cipher = crypto_1.default.createCipher('aes-256-cbc', this.userPrivateKey); let encrypted = cipher.update(value, 'utf8', 'hex'); encrypted += cipher.final('hex'); return encrypted; } /** * Client-side decryption using user's private key */ decryptValue(encryptedValue) { if (!this.userPrivateKey) { throw new Error('User private key not available. Call configfile() first.'); } try { const decipher = crypto_1.default.createDecipher('aes-256-cbc', this.userPrivateKey); let decrypted = decipher.update(encryptedValue, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { throw new Error('Failed to decrypt value. Key may have been encrypted with different private key.'); } } /** * Switch team context for subsequent operations */ team(teamName) { // Store team name as current context this.currentTeam = teamName; // Return team context object with fluent interface return { service: (serviceName) => { return { key: async (keyName) => { return this.getTeamServiceKey(teamName, serviceName, keyName); } }; }, endpoint: async (endpointName) => { return this.getTeamEndpoint(teamName, endpointName); } }; } /** * Direct service access (uses current team context if set) */ service(serviceName) { return { key: async (keyName) => { if (this.currentTeam) { return this.getTeamServiceKey(this.currentTeam, serviceName, keyName); } else { return this.getKeyByService(serviceName, keyName); } } }; } /** * Get key from team context by service and key name */ async getTeamServiceKey(teamName, serviceName, keyName) { if (!this.apiToken) { throw new Error('API token not available. Call configfile() first or provide apiToken in constructor.'); } const cacheKey = `team_${teamName}_${serviceName}_${keyName}`; // Check cache first if (this.cache.has(cacheKey)) { const cached = this.cache.get(cacheKey); if (cached.expires > Date.now()) { return cached.value; } this.cache.delete(cacheKey); } try { // First get team ID by name const teamsResponse = await fetch(`${this.apiUrl}/api/teams`, { headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' } }); if (!teamsResponse.ok) { throw new Error(`Failed to get teams: HTTP ${teamsResponse.status}`); } const teamsData = await teamsResponse.json(); const team = teamsData.teams.find((t) => t.name === teamName); if (!team) { throw new Error(`Team '${teamName}' not found`); } // Get team keys const keysResponse = await fetch(`${this.apiUrl}/api/teams/${team.id}/keys`, { headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' } }); if (!keysResponse.ok) { throw new Error(`Failed to get team keys: HTTP ${keysResponse.status}`); } const keysData = await keysResponse.json(); // Find the specific key const key = keysData.keys.find((k) => k.service === serviceName && k.name === keyName); if (!key) { throw new Error(`Key '${keyName}' for service '${serviceName}' not found in team '${teamName}'`); } // Get the actual encrypted value const keyResponse = await fetch(`${this.apiUrl}/api/keys/${key.id}`, { headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' } }); if (!keyResponse.ok) { throw new Error(`Failed to retrieve key value: HTTP ${keyResponse.status}`); } const keyData = await keyResponse.json(); // Decrypt the value using user's private key const decryptedValue = this.decryptValue(keyData.value); // Cache the result this.cache.set(cacheKey, { value: decryptedValue, expires: Date.now() + this.defaultTTL }); return decryptedValue; } catch (error) { throw new Error(`Failed to retrieve team key: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Get team endpoint by name */ async getTeamEndpoint(teamName, endpointName) { if (!this.apiToken) { throw new Error('API token not available. Call configfile() first or provide apiToken in constructor.'); } const cacheKey = `team_endpoint_${teamName}_${endpointName}`; // Check cache first if (this.cache.has(cacheKey)) { const cached = this.cache.get(cacheKey); if (cached.expires > Date.now()) { return cached.value; } this.cache.delete(cacheKey); } try { // First get team ID by name const teamsResponse = await fetch(`${this.apiUrl}/api/teams`, { headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' } }); if (!teamsResponse.ok) { throw new Error(`Failed to get teams: HTTP ${teamsResponse.status}`); } const teamsData = await teamsResponse.json(); const team = teamsData.teams.find((t) => t.name === teamName); if (!team) { throw new Error(`Team '${teamName}' not found`); } // Get team endpoints const endpointsResponse = await fetch(`${this.apiUrl}/api/teams/${team.id}/endpoints`, { headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' } }); if (!endpointsResponse.ok) { throw new Error(`Failed to get team endpoints: HTTP ${endpointsResponse.status}`); } const endpointsData = await endpointsResponse.json(); // Find the specific endpoint const endpoint = endpointsData.endpoints.find((e) => e.name === endpointName); if (!endpoint) { throw new Error(`Endpoint '${endpointName}' not found in team '${teamName}'`); } // Cache the result this.cache.set(cacheKey, { value: endpoint.url, expires: Date.now() + this.defaultTTL }); return endpoint.url; } catch (error) { throw new Error(`Failed to retrieve team endpoint: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getKey(keyId, useCache = true) { if (!this.apiToken) { throw new Error('API token not available. Call configfile() first or provide apiToken in constructor.'); } const cacheKey = `key_${keyId}`; if (useCache && this.cache.has(cacheKey)) { const cached = this.cache.get(cacheKey); if (cached.expires > Date.now()) { return cached.value; } this.cache.delete(cacheKey); } try { const response = await fetch(`${this.apiUrl}/api/keys/${keyId}`, { headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' } }); if (!response.ok) { if (response.status === 401) { throw new Error('Authentication failed. Please check your API token.'); } if (response.status === 404) { throw new Error(`API key with ID '${keyId}' not found.`); } throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); // Try to decrypt with user's private key if available, otherwise return as-is let decryptedValue; if (this.userPrivateKey) { try { decryptedValue = this.decryptValue(data.value); } catch (decryptError) { // If decryption fails, might be old server-encrypted key decryptedValue = data.value; } } else { decryptedValue = data.value; } if (useCache) { this.cache.set(cacheKey, { value: decryptedValue, expires: Date.now() + this.defaultTTL }); } return decryptedValue; } catch (error) { throw new Error(`Failed to retrieve API key: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getKeyByService(service, name, useCache = true) { if (!this.apiToken) { throw new Error('API token not available. Call configfile() first or provide apiToken in constructor.'); } const cacheKey = `service_${service}_${name || 'default'}`; if (useCache && this.cache.has(cacheKey)) { const cached = this.cache.get(cacheKey); if (cached.expires > Date.now()) { return cached.value; } this.cache.delete(cacheKey); } try { const params = new URLSearchParams({ service }); if (name) params.append('name', name); const response = await fetch(`${this.apiUrl}/api/keys?${params}`, { headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); if (!data.keys || data.keys.length === 0) { throw new Error(`No API key found for service '${service}'${name ? ` with name '${name}'` : ''}`); } // Find exact match or first key for the service const key = name ? data.keys.find((k) => k.name === name && k.service === service) : data.keys.find((k) => k.service === service); if (!key) { throw new Error(`No matching API key found for service '${service}'${name ? ` with name '${name}'` : ''}`); } // Get the actual key value const keyResponse = await fetch(`${this.apiUrl}/api/keys/${key.id}`, { headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' } }); if (!keyResponse.ok) { throw new Error(`Failed to retrieve key value: HTTP ${keyResponse.status}`); } const keyData = await keyResponse.json(); // Try to decrypt with user's private key if available, otherwise return as-is let decryptedValue; if (this.userPrivateKey) { try { decryptedValue = this.decryptValue(keyData.value); } catch (decryptError) { // If decryption fails, might be old server-encrypted key decryptedValue = keyData.value; } } else { decryptedValue = keyData.value; } if (useCache) { this.cache.set(cacheKey, { value: decryptedValue, expires: Date.now() + this.defaultTTL }); } return decryptedValue; } catch (error) { throw new Error(`Failed to retrieve API key for service '${service}': ${error instanceof Error ? error.message : 'Unknown error'}`); } } async createKey(keyData) { if (!this.apiToken) { throw new Error('API token not available. Call configfile() first or provide apiToken in constructor.'); } try { // Encrypt the value client-side if user private key is available let encryptedValue; if (this.userPrivateKey) { encryptedValue = this.encryptValue(keyData.value); } else { // For backward compatibility, if no private key, send as-is (server will encrypt) encryptedValue = keyData.value; } const requestBody = { ...keyData, encryptedValue, }; // Remove the plain value from request delete requestBody.value; const response = await fetch(`${this.apiUrl}/api/keys`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return data.key; } catch (error) { throw new Error(`Failed to create API key: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async updateKey(keyId, updates) { if (!this.apiToken) { throw new Error('API token not available. Call configfile() first or provide apiToken in constructor.'); } try { const requestBody = { ...updates }; // If updating value, encrypt it client-side if (updates.value && this.userPrivateKey) { requestBody.encryptedValue = this.encryptValue(updates.value); delete requestBody.value; } const response = await fetch(`${this.apiUrl}/api/keys/${keyId}`, { method: 'PUT', headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(requestBody) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } // Clear cache for this key this.cache.delete(`key_${keyId}`); } catch (error) { throw new Error(`Failed to update API key: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async deleteKey(keyId) { if (!this.apiToken) { throw new Error('API token not available. Call configfile() first or provide apiToken in constructor.'); } try { const response = await fetch(`${this.apiUrl}/api/keys/${keyId}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } // Clear cache for this key this.cache.delete(`key_${keyId}`); } catch (error) { throw new Error(`Failed to delete API key: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async listKeys() { if (!this.apiToken) { throw new Error('API token not available. Call configfile() first or provide apiToken in constructor.'); } try { const response = await fetch(`${this.apiUrl}/api/keys`, { headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const data = await response.json(); return data.keys; } catch (error) { throw new Error(`Failed to list API keys: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Get endpoint URL (supports current team context) */ async endpoint(endpointName) { if (this.currentTeam) { return this.getTeamEndpoint(this.currentTeam, endpointName); } else { return this.getPersonalEndpoint(endpointName); } } /** * Get personal endpoint by name */ async getPersonalEndpoint(endpointName) { if (!this.apiToken) { throw new Error('API token not available. Call configfile() first or provide apiToken in constructor.'); } const cacheKey = `personal_endpoint_${endpointName}`; // Check cache first if (this.cache.has(cacheKey)) { const cached = this.cache.get(cacheKey); if (cached.expires > Date.now()) { return cached.value; } this.cache.delete(cacheKey); } try { const response = await fetch(`${this.apiUrl}/api/endpoints`, { headers: { 'Authorization': `Bearer ${this.apiToken}`, 'Content-Type': 'application/json' } }); if (!response.ok) { throw new Error(`Failed to get endpoints: HTTP ${response.status}`); } const data = await response.json(); // Find the specific endpoint const endpoint = data.endpoints.find((e) => e.name === endpointName); if (!endpoint) { throw new Error(`Endpoint '${endpointName}' not found`); } // Cache the result this.cache.set(cacheKey, { value: endpoint.url, expires: Date.now() + this.defaultTTL }); return endpoint.url; } catch (error) { throw new Error(`Failed to retrieve endpoint: ${error instanceof Error ? error.message : 'Unknown error'}`); } } clearCache() { this.cache.clear(); } getCacheSize() { return this.cache.size; } } exports.KeyVaultSDK = KeyVaultSDK; // Environment variable helper const createKeyVaultFromEnv = () => { const apiUrl = process.env.KEYVAULT_API_URL || 'https://1pass.vercel.app'; const apiToken = process.env.KEYVAULT_API_TOKEN; if (!apiToken) { throw new Error('KEYVAULT_API_TOKEN environment variable is required'); } return new KeyVaultSDK({ apiUrl, apiToken }); }; exports.createKeyVaultFromEnv = createKeyVaultFromEnv; // Config file helper - creates SDK and loads config automatically const createKeyVaultFromConfig = async () => { const sdk = new KeyVaultSDK(); await sdk.configfile(); return sdk; }; exports.createKeyVaultFromConfig = createKeyVaultFromConfig; // Convenience functions - now support both env and config const getAPIKey = async (keyId) => { // Try config first, fallback to env try { const vault = await (0, exports.createKeyVaultFromConfig)(); return vault.getKey(keyId); } catch (configError) { const vault = (0, exports.createKeyVaultFromEnv)(); return vault.getKey(keyId); } }; exports.getAPIKey = getAPIKey; const getAPIKeyByService = async (service, name) => { // Try config first, fallback to env try { const vault = await (0, exports.createKeyVaultFromConfig)(); return vault.getKeyByService(service, name); } catch (configError) { const vault = (0, exports.createKeyVaultFromEnv)(); return vault.getKeyByService(service, name); } }; exports.getAPIKeyByService = getAPIKeyByService; // Service-specific helpers - updated to support new SDK features const getStripeKey = async (type = 'live') => { return (0, exports.getAPIKeyByService)('stripe', type); }; exports.getStripeKey = getStripeKey; const getOpenAIKey = async () => { return (0, exports.getAPIKeyByService)('openai'); }; exports.getOpenAIKey = getOpenAIKey; const getAWSKey = async (type = 'access') => { return (0, exports.getAPIKeyByService)('aws', type); }; exports.getAWSKey = getAWSKey; const getGitHubToken = async () => { return (0, exports.getAPIKeyByService)('github'); }; exports.getGitHubToken = getGitHubToken; exports.default = KeyVaultSDK;