UNPKG

@withkeystone/cli

Version:

Keystone CLI - Test automation for modern web apps

168 lines 5.9 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.TokenStorage = void 0; const crypto_1 = __importDefault(require("crypto")); const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const os_1 = __importDefault(require("os")); let keytar; try { keytar = require('keytar'); } catch (e) { // Keytar not available - will use file-based storage keytar = null; } class TokenStorage { service = 'keystone-cli'; account = 'default'; configDir = path_1.default.join(os_1.default.homedir(), '.keystone'); tokenFile = path_1.default.join(this.configDir, 'credentials'); apiUrl; constructor(apiUrl) { this.apiUrl = apiUrl; } async saveTokens(tokens) { const data = { ...tokens, expires_at: new Date(Date.now() + tokens.expires_in * 1000).toISOString() }; const jsonData = JSON.stringify(data); if (keytar) { try { // Try OS keychain first await keytar.setPassword(this.service, this.account, jsonData); return; } catch { // Fallback to encrypted file } } // Fallback to encrypted file await this.saveToEncryptedFile(jsonData); } async getTokens() { let data = null; if (keytar) { try { // Try OS keychain first data = await keytar.getPassword(this.service, this.account); } catch { // Fallback to encrypted file } } if (!data) { // Fallback to encrypted file data = await this.readFromEncryptedFile(); } if (!data) return null; try { const parsed = JSON.parse(data); // Check if token needs refresh const expiresAt = new Date(parsed.expires_at); const now = new Date(); const bufferMs = 5 * 60 * 1000; // 5 minute buffer if (expiresAt.getTime() - now.getTime() < bufferMs) { // Token expired or about to expire, try to refresh try { const refreshed = await this.refreshTokens(parsed.refresh_token); await this.saveTokens(refreshed); return refreshed; } catch { // Refresh failed, return null to trigger re-authentication return null; } } return { access_token: parsed.access_token, refresh_token: parsed.refresh_token, expires_in: parsed.expires_in }; } catch { // Invalid data return null; } } async clear() { if (keytar) { try { // Clear from keychain await keytar.deletePassword(this.service, this.account); } catch { // Ignore keychain errors } } try { // Clear encrypted file await promises_1.default.unlink(this.tokenFile); } catch { // Ignore file errors } } async refreshTokens(refreshToken) { const response = await fetch(`${this.apiUrl}/api/v1/cli/token/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: refreshToken, grant_type: 'refresh_token' }) }); if (!response.ok) { throw new Error('Failed to refresh token'); } const data = await response.json(); return { access_token: data.access_token, refresh_token: data.refresh_token, expires_in: data.expires_in }; } async saveToEncryptedFile(data) { await promises_1.default.mkdir(this.configDir, { recursive: true }); const key = this.deriveKey(); const iv = crypto_1.default.randomBytes(16); const cipher = crypto_1.default.createCipheriv('aes-256-gcm', key, iv); let encrypted = cipher.update(data, 'utf8'); encrypted = Buffer.concat([encrypted, cipher.final()]); const authTag = cipher.getAuthTag(); const combined = Buffer.concat([iv, authTag, encrypted]); await promises_1.default.writeFile(this.tokenFile, combined, { mode: 0o600 }); } async readFromEncryptedFile() { try { const combined = await promises_1.default.readFile(this.tokenFile); if (combined.length < 32) { return null; // Invalid file } const iv = combined.subarray(0, 16); const authTag = combined.subarray(16, 32); const encrypted = combined.subarray(32); const key = this.deriveKey(); const decipher = crypto_1.default.createDecipheriv('aes-256-gcm', key, iv); decipher.setAuthTag(authTag); let decrypted = decipher.update(encrypted); decrypted = Buffer.concat([decrypted, decipher.final()]); return decrypted.toString('utf8'); } catch { return null; } } deriveKey() { // Derive key from machine-specific data const machineId = os_1.default.hostname() + os_1.default.platform() + os_1.default.arch(); return crypto_1.default.scryptSync(machineId, 'keystone-cli-salt', 32); } } exports.TokenStorage = TokenStorage; //# sourceMappingURL=TokenStorage.js.map