@withkeystone/cli
Version:
Keystone CLI - Test automation for modern web apps
168 lines • 5.9 kB
JavaScript
;
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