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
JavaScript
"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;