@gati-framework/runtime
Version:
Gati runtime execution engine for running handler-based applications
253 lines • 7.57 kB
JavaScript
/**
* @module runtime/secrets-manager
* @description Secure secrets management with caching and audit logging
*/
import { SecretsManagerError } from './types/secrets-manager.js';
/**
* Default configuration values
*/
const DEFAULT_CONFIG = {
defaultTtl: 5 * 60 * 1000, // 5 minutes
maxTtl: 15 * 60 * 1000, // 15 minutes
minTtl: 60 * 1000, // 1 minute
auditLogging: true,
keyPrefix: '',
autoRefresh: false,
refreshBuffer: 30 * 1000, // 30 seconds
};
/**
* Secrets manager implementation
*/
export class SecretsManager {
config;
cache;
auditLog;
stats;
constructor(config) {
if (!config.providers || config.providers.length === 0) {
throw new Error('At least one secret provider is required');
}
this.config = {
...DEFAULT_CONFIG,
...config,
providers: config.providers,
};
this.cache = new Map();
this.auditLog = [];
this.stats = {
size: 0,
hits: 0,
misses: 0,
evictions: 0,
};
}
/**
* Get a single secret
*/
async getSecret(request) {
const key = this.config.keyPrefix + request.key;
const now = Date.now();
// Check cache first (unless force refresh)
if (!request.forceRefresh) {
const cached = this.cache.get(key);
if (cached && cached.expiresAt > now) {
this.stats.hits++;
this.logAccess({
timestamp: now,
key: request.key,
requestId: request.requestId,
handlerId: request.handlerId,
success: true,
provider: 'cache',
fromCache: true,
});
return {
value: cached.secret.value,
fromCache: true,
provider: 'cache',
version: cached.secret.version,
expiresAt: cached.expiresAt,
};
}
// Remove expired entry
if (cached) {
this.cache.delete(key);
this.stats.evictions++;
}
}
// Cache miss - fetch from providers
this.stats.misses++;
try {
const { value, provider, version } = await this.fetchFromProviders(key);
// Calculate TTL
const ttl = this.calculateTtl(request.ttl);
const expiresAt = now + ttl;
// Cache the secret
const secret = {
key: request.key,
value,
version,
retrievedAt: now,
expiresAt,
};
this.cache.set(key, { secret, expiresAt });
this.stats.size = this.cache.size;
this.logAccess({
timestamp: now,
key: request.key,
requestId: request.requestId,
handlerId: request.handlerId,
success: true,
provider,
fromCache: false,
});
return {
value,
fromCache: false,
provider,
version,
expiresAt,
};
}
catch (error) {
this.logAccess({
timestamp: now,
key: request.key,
requestId: request.requestId,
handlerId: request.handlerId,
success: false,
fromCache: false,
error: error instanceof Error ? error.message : String(error),
});
throw new SecretsManagerError(`Failed to retrieve secret: ${request.key}`, request.key, error instanceof Error ? error : undefined);
}
}
/**
* Get multiple secrets
*/
async getSecrets(keys, context) {
const results = new Map();
// Fetch secrets in parallel
await Promise.all(keys.map(async (key) => {
try {
const result = await this.getSecret({
key,
requestId: context?.requestId,
handlerId: context?.handlerId,
});
results.set(key, result);
}
catch (error) {
// Individual failures don't stop the batch
// Error is already logged in getSecret
}
}));
return results;
}
/**
* Check if a secret exists
*/
async hasSecret(key) {
const fullKey = this.config.keyPrefix + key;
// Check cache first
const cached = this.cache.get(fullKey);
if (cached && cached.expiresAt > Date.now()) {
return true;
}
// Try to fetch from providers
try {
await this.fetchFromProviders(fullKey);
return true;
}
catch {
return false;
}
}
/**
* Invalidate cached secret
*/
invalidate(key) {
const fullKey = this.config.keyPrefix + key;
if (this.cache.delete(fullKey)) {
this.stats.evictions++;
this.stats.size = this.cache.size;
}
}
/**
* Clear all cached secrets
*/
clearCache() {
const size = this.cache.size;
this.cache.clear();
this.stats.evictions += size;
this.stats.size = 0;
}
/**
* Get cache statistics
*/
getCacheStats() {
return { ...this.stats };
}
/**
* Get audit log entries
*/
getAuditLog() {
return [...this.auditLog];
}
/**
* Fetch secret from providers (fallback chain)
*/
async fetchFromProviders(key) {
const errors = [];
for (const provider of this.config.providers) {
try {
// Check if provider is available
const available = await provider.isAvailable();
if (!available) {
continue;
}
// Try to get secret
const value = await provider.getSecret(key);
return {
value,
provider: provider.name,
version: undefined, // Version support depends on provider
};
}
catch (error) {
errors.push(error instanceof Error ? error : new Error(String(error)));
// Continue to next provider
}
}
// All providers failed
throw new Error(`All providers failed for key: ${key}. Errors: ${errors.map((e) => e.message).join(', ')}`);
}
/**
* Calculate TTL with bounds checking
*/
calculateTtl(requestTtl) {
const ttl = requestTtl ?? this.config.defaultTtl;
// Enforce bounds
if (ttl < this.config.minTtl) {
return this.config.minTtl;
}
if (ttl > this.config.maxTtl) {
return this.config.maxTtl;
}
return ttl;
}
/**
* Log secret access for audit trail
*/
logAccess(entry) {
if (!this.config.auditLogging) {
return;
}
this.auditLog.push(entry);
// Limit audit log size (keep last 1000 entries)
if (this.auditLog.length > 1000) {
this.auditLog.shift();
}
}
}
//# sourceMappingURL=secrets-manager.js.map