From 757ca497d74363137850235f851ee3de94595853 Mon Sep 17 00:00:00 2001 From: nafees87n Date: Mon, 22 Dec 2025 16:33:05 +0530 Subject: [PATCH 01/11] init files --- .../actions/externalSecrets/cacheService.ts | 206 +++++++++++++++++ .../encryptedStorageService.ts | 134 ++++++++++++ .../externalSecrets/externalSecretsManager.ts | 7 + .../awsSecretManagerProvider.ts | 111 ++++++++++ .../providerService/providerService.ts | 207 ++++++++++++++++++ .../externalSecrets/providerService/types.ts | 31 +++ 6 files changed, 696 insertions(+) create mode 100644 src/main/actions/externalSecrets/cacheService.ts create mode 100644 src/main/actions/externalSecrets/encryptedStorageService.ts create mode 100644 src/main/actions/externalSecrets/externalSecretsManager.ts create mode 100644 src/main/actions/externalSecrets/providerService/awsSecretManagerProvider.ts create mode 100644 src/main/actions/externalSecrets/providerService/providerService.ts create mode 100644 src/main/actions/externalSecrets/providerService/types.ts diff --git a/src/main/actions/externalSecrets/cacheService.ts b/src/main/actions/externalSecrets/cacheService.ts new file mode 100644 index 0000000..db87ec9 --- /dev/null +++ b/src/main/actions/externalSecrets/cacheService.ts @@ -0,0 +1,206 @@ +import { SecretProviderType } from "./providerService/types"; + +/** + * Cached secret entry + */ +interface CachedSecret { + value: string; + providerId: string; + providerType: SecretProviderType; + secretName: string; + fetchedAt: number; + expiresAt: number; +} + +/** + * TTL configuration per provider type + */ +const DEFAULT_TTL: Record = { + [SecretProviderType.AWS_SECRETS_MANGER]: 60 * 60 * 1000, // 60 minutes +}; + +// Only 1 provider active at a time, so no need for complex strategies +// name to value is enough + +/** + * In-memory cache for secrets + * - Session-scoped (cleared on app restart) + * - TTL-based expiration + * - Provider-aware invalidation + */ +export class CacheService { + private cache: Map = new Map(); + + get(secretName: string): string | null { + const entry = this.cache.get(secretName); + + if (!entry) { + // provider.fetchSecret(secretName); + // + + } + + // Check if expired + if (Date.now() > entry.expiresAt) { + this.cache.delete(secretName); + return null; + } + + return entry.value; + } + + /** + * Set a cached secret + */ + set( + providerId: string, + providerType: SecretProviderType, + secretName: string, + value: string, + customTTL?: number + ): void { + const key = this.getCacheKey(providerId, secretName); + const ttl = customTTL ?? DEFAULT_TTL[providerType]; + const now = Date.now(); + + this.cache.set(key, { + value, + providerId, + providerType, + secretName, + fetchedAt: now, + expiresAt: ttl === Infinity ? Infinity : now + ttl, + }); + } + + /** + * Invalidate a specific secret + */ + invalidate(providerId: string, secretName: string): boolean { + const key = this.getCacheKey(providerId, secretName); + return this.cache.delete(key); + } + + /** + * Invalidate all secrets from a specific provider + */ + invalidateByProvider(providerId: string): number { + let count = 0; + + for (const [key, entry] of this.cache.entries()) { + if (entry.providerId === providerId) { + this.cache.delete(key); + count++; + } + } + + return count; + } + + /** + * Invalidate all secrets of a specific provider type + */ + invalidateByProviderType(providerType: SecretProviderType): number { + let count = 0; + + for (const [key, entry] of this.cache.entries()) { + if (entry.providerType === providerType) { + this.cache.delete(key); + count++; + } + } + + return count; + } + + /** + * Clear all cached secrets + */ + clear(): void { + this.cache.clear(); + this.stats.hits = 0; + this.stats.misses = 0; + } + + /** + * Get cache statistics + */ + getStats(): CacheStats { + const total = this.stats.hits + this.stats.misses; + + return { + size: this.cache.size, + hits: this.stats.hits, + misses: this.stats.misses, + hitRate: total > 0 ? this.stats.hits / total : 0, + }; + } + + /** + * Clean up expired entries + * Should be called periodically + */ + cleanExpired(): number { + let count = 0; + const now = Date.now(); + + for (const [key, entry] of this.cache.entries()) { + if (entry.expiresAt !== Infinity && now > entry.expiresAt) { + this.cache.delete(key); + count++; + } + } + + return count; + } + + /** + * Get all cached entries (for debugging) + */ + getAllEntries(): Array<{ key: string; entry: CachedSecret }> { + return Array.from(this.cache.entries()).map(([key, entry]) => ({ + key, + entry, + })); + } + + /** + * Check if a specific secret is cached and valid + */ + has(providerId: string, secretName: string): boolean { + const key = this.getCacheKey(providerId, secretName); + const entry = this.cache.get(key); + + if (!entry) { + return false; + } + + // Check if expired + if (entry.expiresAt !== Infinity && Date.now() > entry.expiresAt) { + this.cache.delete(key); + return false; + } + + return true; + } + + /** + * Get time until a cached secret expires (in ms) + * Returns null if not cached or already expired + */ + getTimeToExpiry(providerId: string, secretName: string): number | null { + const key = this.getCacheKey(providerId, secretName); + const entry = this.cache.get(key); + + if (!entry) { + return null; + } + + if (entry.expiresAt === Infinity) { + return Infinity; + } + + const remaining = entry.expiresAt - Date.now(); + return remaining > 0 ? remaining : null; + } +} diff --git a/src/main/actions/externalSecrets/encryptedStorageService.ts b/src/main/actions/externalSecrets/encryptedStorageService.ts new file mode 100644 index 0000000..8c2bced --- /dev/null +++ b/src/main/actions/externalSecrets/encryptedStorageService.ts @@ -0,0 +1,134 @@ +// main/services/vault/EncryptedStorage.ts + +import { safeStorage } from "electron"; +import * as fs from "fs/promises"; +import * as path from "path"; +import { + SecretProviderConfig, + SecretProviderType, +} from "./providerService/types"; +import { GLOBAL_CONFIG_FOLDER_PATH } from "renderer/actions/local-sync/constants"; + +/** + * Stored provider configuration + */ +interface StoredProviderConfig { + type: SecretProviderType; + name: string; + config: SecretProviderConfig; + createdAt: number; + updatedAt: number; +} + +/** + * EncryptedStorage using Electron's safeStorage API + * + * All encryption is handled by safeStorage (OS-level): + * - macOS: Keychain + * - Windows: DPAPI + * - Linux: libsecret + * + * No need for separate vault key or CryptoService! + */ +export class EncryptedStorage { + private readonly basePath: string; + + private readonly providersPath: string; + + constructor(baseFolder: string) { + this.basePath = path.join(GLOBAL_CONFIG_FOLDER_PATH); + this.providersPath = path.join(this.basePath, baseFolder); + } + + async initialize(): Promise { + if (!safeStorage.isEncryptionAvailable()) { + throw new Error( + "Encryption is not available on this system. " + + "This may happen on Linux if libsecret is not installed." + ); + } + + await fs.mkdir(this.providersPath, { recursive: true }); + } + + async save( + data: Record, + fileName: string, + keysToEncrypt: string[] = [] + ): Promise { + const stored: any = { + ...data, + updatedAt: Date.now(), + }; + + // 2. Encrypt sensitive keys within the config + for (const key of keysToEncrypt) { + if (stored[key] !== undefined) { + const sensitiveValue = JSON.stringify(stored[key]); + const encrypted = safeStorage.encryptString(sensitiveValue); + stored[key] = encrypted.toString("base64"); + } + } + + const json = JSON.stringify(stored); + + const filePath = this.getProviderFilePath(fileName); + const tempFilePath = `${filePath}.tmp`; + await fs.writeFile(tempFilePath, json); + await fs.rename(tempFilePath, filePath); + } + + async load( + fileName: string, + keysToDecrypt: string[] = [] + ): Promise { + const filePath = this.getProviderFilePath(fileName); + + try { + const encrypted = await fs.readFile(filePath); + + const stored: any = JSON.parse(encrypted.toString()); + + // Decrypt sensitive keys within the config + for (const key of keysToDecrypt) { + if (stored[key] !== undefined) { + const encryptedBuffer = Buffer.from(stored[key], "base64"); + const decrypted = safeStorage.decryptString(encryptedBuffer); + stored[key] = JSON.parse(decrypted); + } + } + + return stored as StoredProviderConfig; + } catch (error: any) { + // TODO: check error handling + if (error.code === "ENOENT") { + return null; // File doesn't exist + } + throw error; + } + } + + /** + * Delete provider configuration + */ + async delete(fileName: string): Promise { + const filePath = this.getProviderFilePath(fileName); + + try { + await fs.unlink(filePath); + } catch (error) { + // TODO: check error handling + if (error.code !== "ENOENT") { + throw error; + } + // Ignore if file doesn't exist + } + } + + /** + * Get file path for a provider + */ + private getProviderFilePath(fileName: string): string { + return path.join(this.providersPath, `${fileName}.enc`); + } +} diff --git a/src/main/actions/externalSecrets/externalSecretsManager.ts b/src/main/actions/externalSecrets/externalSecretsManager.ts new file mode 100644 index 0000000..2818f25 --- /dev/null +++ b/src/main/actions/externalSecrets/externalSecretsManager.ts @@ -0,0 +1,7 @@ +class ExternalSecretsManager { + private isItialized=false; + + resolveSecret(variableName: string): string | null { + + } +} \ No newline at end of file diff --git a/src/main/actions/externalSecrets/providerService/awsSecretManagerProvider.ts b/src/main/actions/externalSecrets/providerService/awsSecretManagerProvider.ts new file mode 100644 index 0000000..61ef5e7 --- /dev/null +++ b/src/main/actions/externalSecrets/providerService/awsSecretManagerProvider.ts @@ -0,0 +1,111 @@ +import { + SecretsManagerClient, + GetSecretValueCommand, + ListSecretsCommand, +} from "@aws-sdk/client-secrets-manager"; +import { + AWSSecretsManagerConfig, + ISecretProvider, + SecretProviderConfig, + SecretProviderMetadata, + SecretProviderType, +} from "./types"; + +export class AWSSecretsManagerProvider implements ISecretProvider { + readonly type = SecretProviderType.AWS_SECRETS_MANGER; + + // Provider instance holds its own identity + constructor(public readonly id: string) {} + + private config: AWSSecretsManagerConfig | null = null; + private client: SecretsManagerClient | null = null; + + async configure(config: AWSSecretsManagerConfig): Promise { + // Validate + this.validateConfig(config); + + // Create AWS client + this.client = new SecretsManagerClient({ + region: config.region, + credentials: { + accessKeyId: config.accessKeyId, + secretAccessKey: config.secretAccessKey, + }, + }); + + // Test connection ?? IS this needed + // await this.testConnection(); + + // Store config + this.config = config; + } + + /** + * Get non-sensitive metadata + */ + getConfig(): Promise { + // Fetch and return metadata + } + + /** + * Test connection + */ + async testConnection(): Promise { + if (!this.client) { + throw new Error("Client not initialized"); + } + + try { + await this.client.send(new ListSecretsCommand({ MaxResults: 1 })); + return true; + } catch (error: any) { + throw new Error(`Connection test failed: ${error.message}`); + } + } + + /** + * Fetch secret + */ + async fetchSecret(secretName: string): Promise { + if (!this.client) { + throw new Error("Provider not configured"); + } + + const result = await this.client.send( + new GetSecretValueCommand({ SecretId: secretName }) + ); + + return ( + result.SecretString || Buffer.from(result.SecretBinary!).toString("utf-8") + ); + } + + /** + * List secrets (for autocomplete) + */ + async listSecrets(prefix?: string): Promise { + if (!this.client) { + throw new Error("Provider not configured"); + } + + // TODO: learn about prefix + + const result = await this.client.send( + new ListSecretsCommand({ + Filters: prefix ? [{ Key: "name", Values: [prefix] }] : undefined, + }) + ); + + return result.SecretList?.map((s) => s.Name!) || []; + } + async disconnect(): Promise { + this.client?.destroy(); + this.client = null; + } + + private validateConfig(config: AWSSecretsManagerConfig): void { + if (!config.accessKeyId || !config.secretAccessKey || !config.region) { + throw new Error("Missing required fields"); + } + } +} diff --git a/src/main/actions/externalSecrets/providerService/providerService.ts b/src/main/actions/externalSecrets/providerService/providerService.ts new file mode 100644 index 0000000..861ecd9 --- /dev/null +++ b/src/main/actions/externalSecrets/providerService/providerService.ts @@ -0,0 +1,207 @@ +import { ISecretProvider } from "./types"; + +export class ProviderService { + private providers: Map = new Map(); + + private encryptedStorage: EncryptedStorage; + private cache: SecretCache; + + constructor() { + this.encryptedStorage = new EncryptedStorage(); + this.cache = new SecretCache(); + } + + /** + * Initialize vault (load key, load existing providers) + */ + async initialize(): Promise { + // Load vault key from safeStorage + const keyManager = new KeyManager(); + + if (await keyManager.hasKey()) { + this.vaultKey = await keyManager.retrieveKey(); + this.isLocked = false; + + // Load all existing provider configs + await this.loadProviders(); + } + + this.isInitialized = true; + } + + /** + * Configure a provider (create new or update existing) + */ + async configureProvider( + id: string, + type: SecretProviderType, + name: string, + config: SecretProviderConfig + ): Promise { + if (!this.vaultKey) { + throw new Error("Vault is locked"); + } + + // 1. Create or get existing provider instance + let provider = this.providers.get(id); + + if (!provider) { + // Create new provider instance + provider = this.createProvider(id, type, name); + } + + // 2. Configure the provider (validates & tests connection) + await provider.configure(config); + + // 3. Save encrypted config to disk + await this.encryptedStorage.saveProviderConfig( + id, + { + type, + name, + config, + createdAt: Date.now(), + updatedAt: Date.now(), + }, + this.vaultKey + ); + + // 4. Store in memory + this.providers.set(id, provider); + + // 5. Clear cache for this provider + this.cache.invalidateByProvider(id); + + return provider.getMetadata(); + } + + /** + * Remove a provider + */ + async removeProvider(id: string): Promise { + const provider = this.providers.get(id); + if (!provider) { + throw new Error(`Provider not found: ${id}`); + } + + // 1. Disconnect + await provider.disconnect(); + + // 2. Remove from memory + this.providers.delete(id); + + // 3. Delete encrypted config + await this.encryptedStorage.deleteProviderConfig(id); + + // 4. Clear cache + this.cache.invalidateByProvider(id); + } + + /** + * List all configured providers + */ + listProviders(): SecretProviderMetadata[] { + return Array.from(this.providers.values()).map((p) => p.getMetadata()); + } + + /** + * Get a specific provider + */ + getProvider(id: string): ISecretProvider | undefined { + return this.providers.get(id); + } + + /** + * Resolve a secret + */ + async resolveSecret(providerId: string, secretName: string): Promise { + // Check cache + const cacheKey = `${providerId}/${secretName}`; + const cached = this.cache.get(cacheKey); + if (cached) { + return cached.value; + } + + // Get provider + const provider = this.providers.get(providerId); + if (!provider) { + throw new Error(`Provider not found: ${providerId}`); + } + + if (!provider.isConfigured) { + throw new Error(`Provider not configured: ${providerId}`); + } + + // Fetch from provider + const value = await provider.fetchSecret(secretName); + + // Cache it + this.cache.set(cacheKey, { + value, + provider: providerId, + expiresAt: Date.now() + this.getTTL(provider.type), + }); + + return value; + } + + /** + * Load providers from disk on startup + */ + private async loadProviders(): Promise { + const providerIds = await this.encryptedStorage.listProviders(); + + for (const id of providerIds) { + try { + const saved = await this.encryptedStorage.loadProviderConfig( + id, + this.vaultKey! + ); + + // Create provider instance + const provider = this.createProvider(id, saved.type, saved.name); + + // Configure it with saved config + await provider.configure(saved.config); + + // Store in memory + this.providers.set(id, provider); + } catch (error) { + console.error(`Failed to load provider ${id}:`, error); + } + } + } + + /** + * Factory method to create provider instances + */ + private createProvider( + id: string, + type: SecretProviderType, + name: string + ): ISecretProvider { + switch (type) { + case SecretProviderType.AWS_SECRETS_MANAGER: + return new AWSSecretsManagerProvider(id, name); + + case SecretProviderType.HASHICORP_VAULT: + return new HashiCorpVaultProvider(id, name); + + case SecretProviderType.AZURE_KEY_VAULT: + return new AzureKeyVaultProvider(id, name); + + default: + throw new Error(`Unknown provider type: ${type}`); + } + } + +// private getTTL(type: SecretProviderType): number { +// const TTL_CONFIG = { +// [SecretProviderType.AWS_SECRETS_MANAGER]: 5 * 60 * 1000, // 5 min +// [SecretProviderType.HASHICORP_VAULT]: 1 * 60 * 1000, // 1 min +// [SecretProviderType.AZURE_KEY_VAULT]: 5 * 60 * 1000, // 5 min +// [SecretProviderType.LOCAL_VAULT]: Infinity, // Never expire +// }; +// return TTL_CONFIG[type]; +// } +// } diff --git a/src/main/actions/externalSecrets/providerService/types.ts b/src/main/actions/externalSecrets/providerService/types.ts new file mode 100644 index 0000000..a432972 --- /dev/null +++ b/src/main/actions/externalSecrets/providerService/types.ts @@ -0,0 +1,31 @@ +export enum SecretProviderType { + AWS_SECRETS_MANGER = "aws", +} + +export interface AWSSecretsManagerConfig { + accessKeyId: string; + secretAccessKey: string; + region: string; +} + +export interface SecretProviderMetadata { + id: string; + type: SecretProviderType; + name: string; + isConfigured: boolean; + createdAt: number; + updatedAt: number; +} + +export type SecretProviderConfig = AWSSecretsManagerConfig; + +export interface ISecretProvider { + type: SecretProviderType; + id: string; + + configure(config: SecretProviderConfig): Promise; + getConfig(id: string): Promise; // should return all or any non-sentive metadata? sensitive data is lazily fetched + + testConnection(): Promise; + fetchSecret(secretName: string): Promise; +} From 0f8cb7a21b5be21f86509b42a4c1e71f2af57b4a Mon Sep 17 00:00:00 2001 From: nafees87n Date: Mon, 22 Dec 2025 18:57:46 +0530 Subject: [PATCH 02/11] added external secrets manager interfaces --- .../actions/externalSecrets/cacheService.ts | 206 ----------------- .../encryptedFsStorageService.ts | 26 +++ .../encryptedStorageService.ts | 134 ------------ .../externalSecrets/externalSecretsManager.ts | 67 +++++- .../awsSecretManagerProvider.ts | 111 ---------- .../providerService/providerService.ts | 207 ------------------ .../externalSecrets/providerService/types.ts | 10 +- 7 files changed, 93 insertions(+), 668 deletions(-) create mode 100644 src/main/actions/externalSecrets/encryptedFsStorageService.ts delete mode 100644 src/main/actions/externalSecrets/encryptedStorageService.ts delete mode 100644 src/main/actions/externalSecrets/providerService/providerService.ts diff --git a/src/main/actions/externalSecrets/cacheService.ts b/src/main/actions/externalSecrets/cacheService.ts index db87ec9..e69de29 100644 --- a/src/main/actions/externalSecrets/cacheService.ts +++ b/src/main/actions/externalSecrets/cacheService.ts @@ -1,206 +0,0 @@ -import { SecretProviderType } from "./providerService/types"; - -/** - * Cached secret entry - */ -interface CachedSecret { - value: string; - providerId: string; - providerType: SecretProviderType; - secretName: string; - fetchedAt: number; - expiresAt: number; -} - -/** - * TTL configuration per provider type - */ -const DEFAULT_TTL: Record = { - [SecretProviderType.AWS_SECRETS_MANGER]: 60 * 60 * 1000, // 60 minutes -}; - -// Only 1 provider active at a time, so no need for complex strategies -// name to value is enough - -/** - * In-memory cache for secrets - * - Session-scoped (cleared on app restart) - * - TTL-based expiration - * - Provider-aware invalidation - */ -export class CacheService { - private cache: Map = new Map(); - - get(secretName: string): string | null { - const entry = this.cache.get(secretName); - - if (!entry) { - // provider.fetchSecret(secretName); - // - - } - - // Check if expired - if (Date.now() > entry.expiresAt) { - this.cache.delete(secretName); - return null; - } - - return entry.value; - } - - /** - * Set a cached secret - */ - set( - providerId: string, - providerType: SecretProviderType, - secretName: string, - value: string, - customTTL?: number - ): void { - const key = this.getCacheKey(providerId, secretName); - const ttl = customTTL ?? DEFAULT_TTL[providerType]; - const now = Date.now(); - - this.cache.set(key, { - value, - providerId, - providerType, - secretName, - fetchedAt: now, - expiresAt: ttl === Infinity ? Infinity : now + ttl, - }); - } - - /** - * Invalidate a specific secret - */ - invalidate(providerId: string, secretName: string): boolean { - const key = this.getCacheKey(providerId, secretName); - return this.cache.delete(key); - } - - /** - * Invalidate all secrets from a specific provider - */ - invalidateByProvider(providerId: string): number { - let count = 0; - - for (const [key, entry] of this.cache.entries()) { - if (entry.providerId === providerId) { - this.cache.delete(key); - count++; - } - } - - return count; - } - - /** - * Invalidate all secrets of a specific provider type - */ - invalidateByProviderType(providerType: SecretProviderType): number { - let count = 0; - - for (const [key, entry] of this.cache.entries()) { - if (entry.providerType === providerType) { - this.cache.delete(key); - count++; - } - } - - return count; - } - - /** - * Clear all cached secrets - */ - clear(): void { - this.cache.clear(); - this.stats.hits = 0; - this.stats.misses = 0; - } - - /** - * Get cache statistics - */ - getStats(): CacheStats { - const total = this.stats.hits + this.stats.misses; - - return { - size: this.cache.size, - hits: this.stats.hits, - misses: this.stats.misses, - hitRate: total > 0 ? this.stats.hits / total : 0, - }; - } - - /** - * Clean up expired entries - * Should be called periodically - */ - cleanExpired(): number { - let count = 0; - const now = Date.now(); - - for (const [key, entry] of this.cache.entries()) { - if (entry.expiresAt !== Infinity && now > entry.expiresAt) { - this.cache.delete(key); - count++; - } - } - - return count; - } - - /** - * Get all cached entries (for debugging) - */ - getAllEntries(): Array<{ key: string; entry: CachedSecret }> { - return Array.from(this.cache.entries()).map(([key, entry]) => ({ - key, - entry, - })); - } - - /** - * Check if a specific secret is cached and valid - */ - has(providerId: string, secretName: string): boolean { - const key = this.getCacheKey(providerId, secretName); - const entry = this.cache.get(key); - - if (!entry) { - return false; - } - - // Check if expired - if (entry.expiresAt !== Infinity && Date.now() > entry.expiresAt) { - this.cache.delete(key); - return false; - } - - return true; - } - - /** - * Get time until a cached secret expires (in ms) - * Returns null if not cached or already expired - */ - getTimeToExpiry(providerId: string, secretName: string): number | null { - const key = this.getCacheKey(providerId, secretName); - const entry = this.cache.get(key); - - if (!entry) { - return null; - } - - if (entry.expiresAt === Infinity) { - return Infinity; - } - - const remaining = entry.expiresAt - Date.now(); - return remaining > 0 ? remaining : null; - } -} diff --git a/src/main/actions/externalSecrets/encryptedFsStorageService.ts b/src/main/actions/externalSecrets/encryptedFsStorageService.ts new file mode 100644 index 0000000..e0eb78b --- /dev/null +++ b/src/main/actions/externalSecrets/encryptedFsStorageService.ts @@ -0,0 +1,26 @@ +import { safeStorage } from "electron"; + +export class EncryptedFsStorageService { + constructor(private readonly baseFolderPath: string) {} + + async initialize(): Promise { + if (!safeStorage.isEncryptionAvailable()) { + // Show trouble shooting steps to user + throw new Error("Encryption is not available on this system. "); + } + + // initialize directories + } + + async save( + data: Record, + path: string, + keysToEncrypt: string[] = [] + ): Promise { + // encrypted + } + + async load(path: string, keysToDecrypt: string[] = []): Promise<> {} + + async delete(path: string): Promise {} +} diff --git a/src/main/actions/externalSecrets/encryptedStorageService.ts b/src/main/actions/externalSecrets/encryptedStorageService.ts deleted file mode 100644 index 8c2bced..0000000 --- a/src/main/actions/externalSecrets/encryptedStorageService.ts +++ /dev/null @@ -1,134 +0,0 @@ -// main/services/vault/EncryptedStorage.ts - -import { safeStorage } from "electron"; -import * as fs from "fs/promises"; -import * as path from "path"; -import { - SecretProviderConfig, - SecretProviderType, -} from "./providerService/types"; -import { GLOBAL_CONFIG_FOLDER_PATH } from "renderer/actions/local-sync/constants"; - -/** - * Stored provider configuration - */ -interface StoredProviderConfig { - type: SecretProviderType; - name: string; - config: SecretProviderConfig; - createdAt: number; - updatedAt: number; -} - -/** - * EncryptedStorage using Electron's safeStorage API - * - * All encryption is handled by safeStorage (OS-level): - * - macOS: Keychain - * - Windows: DPAPI - * - Linux: libsecret - * - * No need for separate vault key or CryptoService! - */ -export class EncryptedStorage { - private readonly basePath: string; - - private readonly providersPath: string; - - constructor(baseFolder: string) { - this.basePath = path.join(GLOBAL_CONFIG_FOLDER_PATH); - this.providersPath = path.join(this.basePath, baseFolder); - } - - async initialize(): Promise { - if (!safeStorage.isEncryptionAvailable()) { - throw new Error( - "Encryption is not available on this system. " + - "This may happen on Linux if libsecret is not installed." - ); - } - - await fs.mkdir(this.providersPath, { recursive: true }); - } - - async save( - data: Record, - fileName: string, - keysToEncrypt: string[] = [] - ): Promise { - const stored: any = { - ...data, - updatedAt: Date.now(), - }; - - // 2. Encrypt sensitive keys within the config - for (const key of keysToEncrypt) { - if (stored[key] !== undefined) { - const sensitiveValue = JSON.stringify(stored[key]); - const encrypted = safeStorage.encryptString(sensitiveValue); - stored[key] = encrypted.toString("base64"); - } - } - - const json = JSON.stringify(stored); - - const filePath = this.getProviderFilePath(fileName); - const tempFilePath = `${filePath}.tmp`; - await fs.writeFile(tempFilePath, json); - await fs.rename(tempFilePath, filePath); - } - - async load( - fileName: string, - keysToDecrypt: string[] = [] - ): Promise { - const filePath = this.getProviderFilePath(fileName); - - try { - const encrypted = await fs.readFile(filePath); - - const stored: any = JSON.parse(encrypted.toString()); - - // Decrypt sensitive keys within the config - for (const key of keysToDecrypt) { - if (stored[key] !== undefined) { - const encryptedBuffer = Buffer.from(stored[key], "base64"); - const decrypted = safeStorage.decryptString(encryptedBuffer); - stored[key] = JSON.parse(decrypted); - } - } - - return stored as StoredProviderConfig; - } catch (error: any) { - // TODO: check error handling - if (error.code === "ENOENT") { - return null; // File doesn't exist - } - throw error; - } - } - - /** - * Delete provider configuration - */ - async delete(fileName: string): Promise { - const filePath = this.getProviderFilePath(fileName); - - try { - await fs.unlink(filePath); - } catch (error) { - // TODO: check error handling - if (error.code !== "ENOENT") { - throw error; - } - // Ignore if file doesn't exist - } - } - - /** - * Get file path for a provider - */ - private getProviderFilePath(fileName: string): string { - return path.join(this.providersPath, `${fileName}.enc`); - } -} diff --git a/src/main/actions/externalSecrets/externalSecretsManager.ts b/src/main/actions/externalSecrets/externalSecretsManager.ts index 2818f25..497092d 100644 --- a/src/main/actions/externalSecrets/externalSecretsManager.ts +++ b/src/main/actions/externalSecrets/externalSecretsManager.ts @@ -1,7 +1,66 @@ -class ExternalSecretsManager { - private isItialized=false; +import { EncryptedFsStorageService } from "./encryptedFsStorageService"; +import { ISecretProvider, SecretProviderConfig } from "./providerService/types"; - resolveSecret(variableName: string): string | null { +export class ExternalSecretsManager { + private providers: Map = new Map(); + constructor(private encryptedStorage: EncryptedFsStorageService) {} + + async initialize(): Promise { + this.encryptedStorage.initialize(); + this.listProviderConfigs().then((configs) => { + configs.forEach((config) => { + const providerInstance = this.createProviderInstance(config); + this.registerProviderInstance(providerInstance); + }); + }); + } + + async configureProvider(config: SecretProviderConfig) { + // process the config + // validate the config + + this.encryptedStorage.save(config, `providers/${config.id}`, [ + "config.accessKeyId", + "config.secretAccessKey", + "config.sessionToken", + ]); + } + + async removeProviderConfig(id: string) { + this.encryptedStorage.delete(`providers/${id}`); + this.unregisterProviderInstance(id); + } + + async getProviderConfig(id: string): Promise { + return this.encryptedStorage.load(`providers/${id}`, [ + "config.accessKeyId", + "config.secretAccessKey", + "config.sessionToken", + ]); + } + + private registerProviderInstance(provider: ISecretProvider) { + this.providers.set(provider.id, provider); } -} \ No newline at end of file + + private getProviderInstance(id: string): ISecretProvider | undefined { + return this.providers.get(id); + } + + private listProviderConfigs(): Promise { + return []; + } + + private hasProvider(id: string): boolean { + return this.providers.has(id); + } + + private unregisterProviderInstance(id: string): boolean { + return this.providers.delete(id); + } + + private createProviderInstance( + config: SecretProviderConfig + ): ISecretProvider {} +} diff --git a/src/main/actions/externalSecrets/providerService/awsSecretManagerProvider.ts b/src/main/actions/externalSecrets/providerService/awsSecretManagerProvider.ts index 61ef5e7..e69de29 100644 --- a/src/main/actions/externalSecrets/providerService/awsSecretManagerProvider.ts +++ b/src/main/actions/externalSecrets/providerService/awsSecretManagerProvider.ts @@ -1,111 +0,0 @@ -import { - SecretsManagerClient, - GetSecretValueCommand, - ListSecretsCommand, -} from "@aws-sdk/client-secrets-manager"; -import { - AWSSecretsManagerConfig, - ISecretProvider, - SecretProviderConfig, - SecretProviderMetadata, - SecretProviderType, -} from "./types"; - -export class AWSSecretsManagerProvider implements ISecretProvider { - readonly type = SecretProviderType.AWS_SECRETS_MANGER; - - // Provider instance holds its own identity - constructor(public readonly id: string) {} - - private config: AWSSecretsManagerConfig | null = null; - private client: SecretsManagerClient | null = null; - - async configure(config: AWSSecretsManagerConfig): Promise { - // Validate - this.validateConfig(config); - - // Create AWS client - this.client = new SecretsManagerClient({ - region: config.region, - credentials: { - accessKeyId: config.accessKeyId, - secretAccessKey: config.secretAccessKey, - }, - }); - - // Test connection ?? IS this needed - // await this.testConnection(); - - // Store config - this.config = config; - } - - /** - * Get non-sensitive metadata - */ - getConfig(): Promise { - // Fetch and return metadata - } - - /** - * Test connection - */ - async testConnection(): Promise { - if (!this.client) { - throw new Error("Client not initialized"); - } - - try { - await this.client.send(new ListSecretsCommand({ MaxResults: 1 })); - return true; - } catch (error: any) { - throw new Error(`Connection test failed: ${error.message}`); - } - } - - /** - * Fetch secret - */ - async fetchSecret(secretName: string): Promise { - if (!this.client) { - throw new Error("Provider not configured"); - } - - const result = await this.client.send( - new GetSecretValueCommand({ SecretId: secretName }) - ); - - return ( - result.SecretString || Buffer.from(result.SecretBinary!).toString("utf-8") - ); - } - - /** - * List secrets (for autocomplete) - */ - async listSecrets(prefix?: string): Promise { - if (!this.client) { - throw new Error("Provider not configured"); - } - - // TODO: learn about prefix - - const result = await this.client.send( - new ListSecretsCommand({ - Filters: prefix ? [{ Key: "name", Values: [prefix] }] : undefined, - }) - ); - - return result.SecretList?.map((s) => s.Name!) || []; - } - async disconnect(): Promise { - this.client?.destroy(); - this.client = null; - } - - private validateConfig(config: AWSSecretsManagerConfig): void { - if (!config.accessKeyId || !config.secretAccessKey || !config.region) { - throw new Error("Missing required fields"); - } - } -} diff --git a/src/main/actions/externalSecrets/providerService/providerService.ts b/src/main/actions/externalSecrets/providerService/providerService.ts deleted file mode 100644 index 861ecd9..0000000 --- a/src/main/actions/externalSecrets/providerService/providerService.ts +++ /dev/null @@ -1,207 +0,0 @@ -import { ISecretProvider } from "./types"; - -export class ProviderService { - private providers: Map = new Map(); - - private encryptedStorage: EncryptedStorage; - private cache: SecretCache; - - constructor() { - this.encryptedStorage = new EncryptedStorage(); - this.cache = new SecretCache(); - } - - /** - * Initialize vault (load key, load existing providers) - */ - async initialize(): Promise { - // Load vault key from safeStorage - const keyManager = new KeyManager(); - - if (await keyManager.hasKey()) { - this.vaultKey = await keyManager.retrieveKey(); - this.isLocked = false; - - // Load all existing provider configs - await this.loadProviders(); - } - - this.isInitialized = true; - } - - /** - * Configure a provider (create new or update existing) - */ - async configureProvider( - id: string, - type: SecretProviderType, - name: string, - config: SecretProviderConfig - ): Promise { - if (!this.vaultKey) { - throw new Error("Vault is locked"); - } - - // 1. Create or get existing provider instance - let provider = this.providers.get(id); - - if (!provider) { - // Create new provider instance - provider = this.createProvider(id, type, name); - } - - // 2. Configure the provider (validates & tests connection) - await provider.configure(config); - - // 3. Save encrypted config to disk - await this.encryptedStorage.saveProviderConfig( - id, - { - type, - name, - config, - createdAt: Date.now(), - updatedAt: Date.now(), - }, - this.vaultKey - ); - - // 4. Store in memory - this.providers.set(id, provider); - - // 5. Clear cache for this provider - this.cache.invalidateByProvider(id); - - return provider.getMetadata(); - } - - /** - * Remove a provider - */ - async removeProvider(id: string): Promise { - const provider = this.providers.get(id); - if (!provider) { - throw new Error(`Provider not found: ${id}`); - } - - // 1. Disconnect - await provider.disconnect(); - - // 2. Remove from memory - this.providers.delete(id); - - // 3. Delete encrypted config - await this.encryptedStorage.deleteProviderConfig(id); - - // 4. Clear cache - this.cache.invalidateByProvider(id); - } - - /** - * List all configured providers - */ - listProviders(): SecretProviderMetadata[] { - return Array.from(this.providers.values()).map((p) => p.getMetadata()); - } - - /** - * Get a specific provider - */ - getProvider(id: string): ISecretProvider | undefined { - return this.providers.get(id); - } - - /** - * Resolve a secret - */ - async resolveSecret(providerId: string, secretName: string): Promise { - // Check cache - const cacheKey = `${providerId}/${secretName}`; - const cached = this.cache.get(cacheKey); - if (cached) { - return cached.value; - } - - // Get provider - const provider = this.providers.get(providerId); - if (!provider) { - throw new Error(`Provider not found: ${providerId}`); - } - - if (!provider.isConfigured) { - throw new Error(`Provider not configured: ${providerId}`); - } - - // Fetch from provider - const value = await provider.fetchSecret(secretName); - - // Cache it - this.cache.set(cacheKey, { - value, - provider: providerId, - expiresAt: Date.now() + this.getTTL(provider.type), - }); - - return value; - } - - /** - * Load providers from disk on startup - */ - private async loadProviders(): Promise { - const providerIds = await this.encryptedStorage.listProviders(); - - for (const id of providerIds) { - try { - const saved = await this.encryptedStorage.loadProviderConfig( - id, - this.vaultKey! - ); - - // Create provider instance - const provider = this.createProvider(id, saved.type, saved.name); - - // Configure it with saved config - await provider.configure(saved.config); - - // Store in memory - this.providers.set(id, provider); - } catch (error) { - console.error(`Failed to load provider ${id}:`, error); - } - } - } - - /** - * Factory method to create provider instances - */ - private createProvider( - id: string, - type: SecretProviderType, - name: string - ): ISecretProvider { - switch (type) { - case SecretProviderType.AWS_SECRETS_MANAGER: - return new AWSSecretsManagerProvider(id, name); - - case SecretProviderType.HASHICORP_VAULT: - return new HashiCorpVaultProvider(id, name); - - case SecretProviderType.AZURE_KEY_VAULT: - return new AzureKeyVaultProvider(id, name); - - default: - throw new Error(`Unknown provider type: ${type}`); - } - } - -// private getTTL(type: SecretProviderType): number { -// const TTL_CONFIG = { -// [SecretProviderType.AWS_SECRETS_MANAGER]: 5 * 60 * 1000, // 5 min -// [SecretProviderType.HASHICORP_VAULT]: 1 * 60 * 1000, // 1 min -// [SecretProviderType.AZURE_KEY_VAULT]: 5 * 60 * 1000, // 5 min -// [SecretProviderType.LOCAL_VAULT]: Infinity, // Never expire -// }; -// return TTL_CONFIG[type]; -// } -// } diff --git a/src/main/actions/externalSecrets/providerService/types.ts b/src/main/actions/externalSecrets/providerService/types.ts index a432972..c8d8883 100644 --- a/src/main/actions/externalSecrets/providerService/types.ts +++ b/src/main/actions/externalSecrets/providerService/types.ts @@ -6,26 +6,24 @@ export interface AWSSecretsManagerConfig { accessKeyId: string; secretAccessKey: string; region: string; + sessionToken?: string; } -export interface SecretProviderMetadata { +export interface SecretProviderConfig { id: string; type: SecretProviderType; name: string; isConfigured: boolean; createdAt: number; updatedAt: number; + config: AWSSecretsManagerConfig; } -export type SecretProviderConfig = AWSSecretsManagerConfig; - export interface ISecretProvider { type: SecretProviderType; id: string; - configure(config: SecretProviderConfig): Promise; - getConfig(id: string): Promise; // should return all or any non-sentive metadata? sensitive data is lazily fetched - testConnection(): Promise; fetchSecret(secretName: string): Promise; + listSecrets(): Promise; } From 7b4d8696e49da5a58b4bfab2fb4953f7b51e1368 Mon Sep 17 00:00:00 2001 From: nafees87n Date: Tue, 23 Dec 2025 14:29:02 +0530 Subject: [PATCH 03/11] fix: config type --- src/main/actions/externalSecrets/providerService/types.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/actions/externalSecrets/providerService/types.ts b/src/main/actions/externalSecrets/providerService/types.ts index c8d8883..1ca3bdc 100644 --- a/src/main/actions/externalSecrets/providerService/types.ts +++ b/src/main/actions/externalSecrets/providerService/types.ts @@ -13,7 +13,6 @@ export interface SecretProviderConfig { id: string; type: SecretProviderType; name: string; - isConfigured: boolean; createdAt: number; updatedAt: number; config: AWSSecretsManagerConfig; From fa080d0c206a6a2a74c388d66f6bb0473cda2135 Mon Sep 17 00:00:00 2001 From: nafees87n Date: Tue, 23 Dec 2025 14:38:34 +0530 Subject: [PATCH 04/11] added method --- .../actions/externalSecrets/externalSecretsManager.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/actions/externalSecrets/externalSecretsManager.ts b/src/main/actions/externalSecrets/externalSecretsManager.ts index 497092d..2809b63 100644 --- a/src/main/actions/externalSecrets/externalSecretsManager.ts +++ b/src/main/actions/externalSecrets/externalSecretsManager.ts @@ -19,6 +19,7 @@ export class ExternalSecretsManager { async configureProvider(config: SecretProviderConfig) { // process the config // validate the config + this.registerProviderInstance(this.createProviderInstance(config)); this.encryptedStorage.save(config, `providers/${config.id}`, [ "config.accessKeyId", @@ -28,8 +29,8 @@ export class ExternalSecretsManager { } async removeProviderConfig(id: string) { - this.encryptedStorage.delete(`providers/${id}`); this.unregisterProviderInstance(id); + this.encryptedStorage.delete(`providers/${id}`); } async getProviderConfig(id: string): Promise { @@ -40,6 +41,11 @@ export class ExternalSecretsManager { ]); } + async testProviderConnection(id: string): Promise { + const provider = this.getProviderInstance(id); + return provider?.testConnection(); + } + private registerProviderInstance(provider: ISecretProvider) { this.providers.set(provider.id, provider); } From 0caccc012af8eb71cf29f62a254599d90ffbdb71 Mon Sep 17 00:00:00 2001 From: nafees87n Date: Wed, 24 Dec 2025 12:28:12 +0530 Subject: [PATCH 05/11] refactor --- .../externalSecrets => lib/secrets}/cacheService.ts | 0 .../secrets}/encryptedFsStorageService.ts | 0 .../secrets}/providerService/awsSecretManagerProvider.ts | 0 .../secrets}/providerService/types.ts | 0 .../secrets/secretsManager.ts} | 7 ++++++- 5 files changed, 6 insertions(+), 1 deletion(-) rename src/{main/actions/externalSecrets => lib/secrets}/cacheService.ts (100%) rename src/{main/actions/externalSecrets => lib/secrets}/encryptedFsStorageService.ts (100%) rename src/{main/actions/externalSecrets => lib/secrets}/providerService/awsSecretManagerProvider.ts (100%) rename src/{main/actions/externalSecrets => lib/secrets}/providerService/types.ts (100%) rename src/{main/actions/externalSecrets/externalSecretsManager.ts => lib/secrets/secretsManager.ts} (92%) diff --git a/src/main/actions/externalSecrets/cacheService.ts b/src/lib/secrets/cacheService.ts similarity index 100% rename from src/main/actions/externalSecrets/cacheService.ts rename to src/lib/secrets/cacheService.ts diff --git a/src/main/actions/externalSecrets/encryptedFsStorageService.ts b/src/lib/secrets/encryptedFsStorageService.ts similarity index 100% rename from src/main/actions/externalSecrets/encryptedFsStorageService.ts rename to src/lib/secrets/encryptedFsStorageService.ts diff --git a/src/main/actions/externalSecrets/providerService/awsSecretManagerProvider.ts b/src/lib/secrets/providerService/awsSecretManagerProvider.ts similarity index 100% rename from src/main/actions/externalSecrets/providerService/awsSecretManagerProvider.ts rename to src/lib/secrets/providerService/awsSecretManagerProvider.ts diff --git a/src/main/actions/externalSecrets/providerService/types.ts b/src/lib/secrets/providerService/types.ts similarity index 100% rename from src/main/actions/externalSecrets/providerService/types.ts rename to src/lib/secrets/providerService/types.ts diff --git a/src/main/actions/externalSecrets/externalSecretsManager.ts b/src/lib/secrets/secretsManager.ts similarity index 92% rename from src/main/actions/externalSecrets/externalSecretsManager.ts rename to src/lib/secrets/secretsManager.ts index 2809b63..dd5e479 100644 --- a/src/main/actions/externalSecrets/externalSecretsManager.ts +++ b/src/lib/secrets/secretsManager.ts @@ -1,7 +1,7 @@ import { EncryptedFsStorageService } from "./encryptedFsStorageService"; import { ISecretProvider, SecretProviderConfig } from "./providerService/types"; -export class ExternalSecretsManager { +export class SecretsManager { private providers: Map = new Map(); constructor(private encryptedStorage: EncryptedFsStorageService) {} @@ -41,6 +41,11 @@ export class ExternalSecretsManager { ]); } + private validateProviderConfig(config: SecretProviderConfig): boolean { + // implement validation logic + return true; + } + async testProviderConnection(id: string): Promise { const provider = this.getProviderInstance(id); return provider?.testConnection(); From a96565024f7080df6a303c152cb1139ce51d4c2f Mon Sep 17 00:00:00 2001 From: nafees87n Date: Thu, 25 Dec 2025 18:10:24 +0530 Subject: [PATCH 06/11] refactor folder name --- .../cacheService.ts | 0 .../encryptedFsStorageService.ts | 19 ++++++++--- .../providerService/ISecretProvider.ts | 10 ++++++ .../awsSecretManagerProvider.ts | 0 .../secretsManager.ts | 33 ++++++++++--------- .../types.ts | 9 ----- 6 files changed, 42 insertions(+), 29 deletions(-) rename src/lib/{secrets => secretsManager}/cacheService.ts (100%) rename src/lib/{secrets => secretsManager}/encryptedFsStorageService.ts (57%) create mode 100644 src/lib/secretsManager/providerService/ISecretProvider.ts rename src/lib/{secrets => secretsManager}/providerService/awsSecretManagerProvider.ts (100%) rename src/lib/{secrets => secretsManager}/secretsManager.ts (82%) rename src/lib/{secrets/providerService => secretsManager}/types.ts (64%) diff --git a/src/lib/secrets/cacheService.ts b/src/lib/secretsManager/cacheService.ts similarity index 100% rename from src/lib/secrets/cacheService.ts rename to src/lib/secretsManager/cacheService.ts diff --git a/src/lib/secrets/encryptedFsStorageService.ts b/src/lib/secretsManager/encryptedFsStorageService.ts similarity index 57% rename from src/lib/secrets/encryptedFsStorageService.ts rename to src/lib/secretsManager/encryptedFsStorageService.ts index e0eb78b..fc437f3 100644 --- a/src/lib/secrets/encryptedFsStorageService.ts +++ b/src/lib/secretsManager/encryptedFsStorageService.ts @@ -1,5 +1,13 @@ import { safeStorage } from "electron"; +interface EncryptionConfig { + keysToEncrypt?: (keyof T)[]; +} + +interface DecryptionConfig { + keysToDecrypt?: (keyof T)[]; +} + export class EncryptedFsStorageService { constructor(private readonly baseFolderPath: string) {} @@ -12,15 +20,18 @@ export class EncryptedFsStorageService { // initialize directories } - async save( - data: Record, + async save>( + data: T, path: string, - keysToEncrypt: string[] = [] + encryptConfig: EncryptionConfig ): Promise { // encrypted } - async load(path: string, keysToDecrypt: string[] = []): Promise<> {} + async load>( + path: string, + decryptConfig: DecryptionConfig + ): Promise {} async delete(path: string): Promise {} } diff --git a/src/lib/secretsManager/providerService/ISecretProvider.ts b/src/lib/secretsManager/providerService/ISecretProvider.ts new file mode 100644 index 0000000..afef809 --- /dev/null +++ b/src/lib/secretsManager/providerService/ISecretProvider.ts @@ -0,0 +1,10 @@ +import { SecretProviderType } from "../types"; + +export interface ISecretProvider { + type: SecretProviderType; + id: string; + + testConnection(): Promise; + fetchSecret(secretName: string): Promise; + listSecrets(): Promise; +} diff --git a/src/lib/secrets/providerService/awsSecretManagerProvider.ts b/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts similarity index 100% rename from src/lib/secrets/providerService/awsSecretManagerProvider.ts rename to src/lib/secretsManager/providerService/awsSecretManagerProvider.ts diff --git a/src/lib/secrets/secretsManager.ts b/src/lib/secretsManager/secretsManager.ts similarity index 82% rename from src/lib/secrets/secretsManager.ts rename to src/lib/secretsManager/secretsManager.ts index dd5e479..985ab37 100644 --- a/src/lib/secrets/secretsManager.ts +++ b/src/lib/secretsManager/secretsManager.ts @@ -1,5 +1,6 @@ import { EncryptedFsStorageService } from "./encryptedFsStorageService"; -import { ISecretProvider, SecretProviderConfig } from "./providerService/types"; +import { ISecretProvider } from "./providerService/ISecretProvider"; +import { SecretProviderConfig } from "./types"; export class SecretsManager { private providers: Map = new Map(); @@ -21,11 +22,13 @@ export class SecretsManager { // validate the config this.registerProviderInstance(this.createProviderInstance(config)); - this.encryptedStorage.save(config, `providers/${config.id}`, [ - "config.accessKeyId", - "config.secretAccessKey", - "config.sessionToken", - ]); + this.encryptedStorage.save( + config, + `providers/${config.id}`, + { + keysToEncrypt: ["config"], + } + ); } async removeProviderConfig(id: string) { @@ -34,16 +37,9 @@ export class SecretsManager { } async getProviderConfig(id: string): Promise { - return this.encryptedStorage.load(`providers/${id}`, [ - "config.accessKeyId", - "config.secretAccessKey", - "config.sessionToken", - ]); - } - - private validateProviderConfig(config: SecretProviderConfig): boolean { - // implement validation logic - return true; + return this.encryptedStorage.load(`providers/${id}`, { + keysToDecrypt: ["config"], + }); } async testProviderConnection(id: string): Promise { @@ -51,6 +47,11 @@ export class SecretsManager { return provider?.testConnection(); } + private validateProviderConfig(config: SecretProviderConfig): boolean { + // implement validation logic + return true; + } + private registerProviderInstance(provider: ISecretProvider) { this.providers.set(provider.id, provider); } diff --git a/src/lib/secrets/providerService/types.ts b/src/lib/secretsManager/types.ts similarity index 64% rename from src/lib/secrets/providerService/types.ts rename to src/lib/secretsManager/types.ts index 1ca3bdc..6440b39 100644 --- a/src/lib/secrets/providerService/types.ts +++ b/src/lib/secretsManager/types.ts @@ -17,12 +17,3 @@ export interface SecretProviderConfig { updatedAt: number; config: AWSSecretsManagerConfig; } - -export interface ISecretProvider { - type: SecretProviderType; - id: string; - - testConnection(): Promise; - fetchSecret(secretName: string): Promise; - listSecrets(): Promise; -} From 2f3859782f85bc9cb55465ca9efd53c21bd60f00 Mon Sep 17 00:00:00 2001 From: nafees87n Date: Sun, 28 Dec 2025 00:53:00 +0530 Subject: [PATCH 07/11] fix: interface reusability --- .../encryptedStorage/IEncryptedStorage.ts | 6 ++ .../encryptedFsStorageService.ts | 23 ++---- .../providerRegistry/FsProviderRegistry.ts | 80 +++++++++++++++++++ .../providerRegistry/IProviderRegistry.ts | 10 +++ .../providerService/ISecretProvider.ts | 2 + src/lib/secretsManager/secretsManager.ts | 70 +++++----------- 6 files changed, 122 insertions(+), 69 deletions(-) create mode 100644 src/lib/secretsManager/encryptedStorage/IEncryptedStorage.ts rename src/lib/secretsManager/{ => encryptedStorage}/encryptedFsStorageService.ts (50%) create mode 100644 src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts create mode 100644 src/lib/secretsManager/providerRegistry/IProviderRegistry.ts diff --git a/src/lib/secretsManager/encryptedStorage/IEncryptedStorage.ts b/src/lib/secretsManager/encryptedStorage/IEncryptedStorage.ts new file mode 100644 index 0000000..7e364ba --- /dev/null +++ b/src/lib/secretsManager/encryptedStorage/IEncryptedStorage.ts @@ -0,0 +1,6 @@ +export interface IEncryptedStorage { + initialize(): Promise; + save>(key: string, data: T): Promise; + load>(key: string): Promise; + delete(key: string): Promise; +} diff --git a/src/lib/secretsManager/encryptedFsStorageService.ts b/src/lib/secretsManager/encryptedStorage/encryptedFsStorageService.ts similarity index 50% rename from src/lib/secretsManager/encryptedFsStorageService.ts rename to src/lib/secretsManager/encryptedStorage/encryptedFsStorageService.ts index fc437f3..ad54941 100644 --- a/src/lib/secretsManager/encryptedFsStorageService.ts +++ b/src/lib/secretsManager/encryptedStorage/encryptedFsStorageService.ts @@ -1,14 +1,7 @@ import { safeStorage } from "electron"; +import { IEncryptedStorage } from "./IEncryptedStorage"; -interface EncryptionConfig { - keysToEncrypt?: (keyof T)[]; -} - -interface DecryptionConfig { - keysToDecrypt?: (keyof T)[]; -} - -export class EncryptedFsStorageService { +export class EncryptedFsStorageService implements IEncryptedStorage { constructor(private readonly baseFolderPath: string) {} async initialize(): Promise { @@ -21,17 +14,13 @@ export class EncryptedFsStorageService { } async save>( - data: T, - path: string, - encryptConfig: EncryptionConfig + key: string, + data: T ): Promise { // encrypted } - async load>( - path: string, - decryptConfig: DecryptionConfig - ): Promise {} + async load>(key: string): Promise {} - async delete(path: string): Promise {} + async delete(key: string): Promise {} } diff --git a/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts new file mode 100644 index 0000000..554e98a --- /dev/null +++ b/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts @@ -0,0 +1,80 @@ +import * as fs from "fs/promises"; +import * as path from "path"; +import { IProviderRegistry } from "./IProviderRegistry"; +import { SecretProviderConfig, SecretProviderType } from "../types"; +import { IEncryptedStorage } from "../encryptedStorage/IEncryptedStorage"; + +const MANIFEST_FILENAME = "providers.json"; + +export interface ProvidersManifest { + version: string; + providers: { + id: string; + storagePath: string; + type: SecretProviderType; + }[]; +} + +export class FileBasedProviderRegistry implements IProviderRegistry { + private manifestPath: string; + + constructor( + private readonly encryptedStorage: IEncryptedStorage, + private readonly configDir: string + ) { + this.manifestPath = path.join(configDir, MANIFEST_FILENAME); + } + + async initialize(): Promise { + await this.ensureConfigDir(); + } + + async listProviders(): Promise { + const manifest = await this.loadManifest(); + return manifest.providers.map((p) => p.id); + } + + async loadAllProviderConfigs(): Promise {} + + async saveProviderConfig(config: SecretProviderConfig): Promise { + const manifest = await this.loadManifest(); + const storageKey = config.id; + + await this.encryptedStorage.save(storageKey, config); + + // Update manifest + + await this.saveManifest(manifest); + } + + async deleteProviderConfig(id: string): Promise { + const manifest = await this.loadManifest(); + const entry = manifest.providers.find((p) => p.id === id); + if (!entry) return; + + await this.encryptedStorage.delete(id); + + manifest.providers = manifest.providers.filter((p) => p.id !== id); + await this.saveManifest(manifest); + } + + async getProviderConfig(id: string): Promise { + const manifest = await this.loadManifest(); + const entry = manifest.providers.find((p) => p.id === id); + if (!entry) return null; + + return this.encryptedStorage.load(id); + } + + private async ensureConfigDir(): Promise { + try { + await fs.mkdir(this.configDir, { recursive: true }); + } catch (error) { + console.error("Failed to create config directory:", error); + } + } + + private async loadManifest(): Promise {} + + private async saveManifest(manifest: ProvidersManifest): Promise {} +} diff --git a/src/lib/secretsManager/providerRegistry/IProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/IProviderRegistry.ts new file mode 100644 index 0000000..e9fda3b --- /dev/null +++ b/src/lib/secretsManager/providerRegistry/IProviderRegistry.ts @@ -0,0 +1,10 @@ +import { SecretProviderConfig } from "../types"; + +export interface IProviderRegistry { + initialize(): Promise; + listProviders(): Promise; + loadAllProviderConfigs(): Promise; + saveProviderConfig(config: SecretProviderConfig): Promise; + deleteProviderConfig(id: string): Promise; + getProviderConfig(id: string): Promise; +} diff --git a/src/lib/secretsManager/providerService/ISecretProvider.ts b/src/lib/secretsManager/providerService/ISecretProvider.ts index afef809..353f8a7 100644 --- a/src/lib/secretsManager/providerService/ISecretProvider.ts +++ b/src/lib/secretsManager/providerService/ISecretProvider.ts @@ -7,4 +7,6 @@ export interface ISecretProvider { testConnection(): Promise; fetchSecret(secretName: string): Promise; listSecrets(): Promise; + + validateConfig(): boolean; } diff --git a/src/lib/secretsManager/secretsManager.ts b/src/lib/secretsManager/secretsManager.ts index 985ab37..fde69dc 100644 --- a/src/lib/secretsManager/secretsManager.ts +++ b/src/lib/secretsManager/secretsManager.ts @@ -1,77 +1,43 @@ -import { EncryptedFsStorageService } from "./encryptedFsStorageService"; +import { IProviderRegistry } from "./providerRegistry/IProviderRegistry"; import { ISecretProvider } from "./providerService/ISecretProvider"; import { SecretProviderConfig } from "./types"; export class SecretsManager { private providers: Map = new Map(); - constructor(private encryptedStorage: EncryptedFsStorageService) {} + constructor(private registry: IProviderRegistry) {} async initialize(): Promise { - this.encryptedStorage.initialize(); - this.listProviderConfigs().then((configs) => { - configs.forEach((config) => { - const providerInstance = this.createProviderInstance(config); - this.registerProviderInstance(providerInstance); - }); - }); + this.registry.initialize(); + this.initProvidersFromManifest(); } - async configureProvider(config: SecretProviderConfig) { - // process the config - // validate the config - this.registerProviderInstance(this.createProviderInstance(config)); + private async initProvidersFromManifest() { + const configs = await this.registry.loadAllProviderConfigs(); + configs.forEach((config) => { + this.providers.set(config.id, this.createProviderInstance(config)); + }); + } - this.encryptedStorage.save( - config, - `providers/${config.id}`, - { - keysToEncrypt: ["config"], - } - ); + async addProviderConfig(config: SecretProviderConfig) { + this.providers.set(config.id, this.createProviderInstance(config)); + this.registry.saveProviderConfig(config); } async removeProviderConfig(id: string) { - this.unregisterProviderInstance(id); - this.encryptedStorage.delete(`providers/${id}`); + this.providers.delete(id); + this.registry.deleteProviderConfig(id); } - async getProviderConfig(id: string): Promise { - return this.encryptedStorage.load(`providers/${id}`, { - keysToDecrypt: ["config"], - }); + async getProviderConfig(id: string): Promise { + return this.registry.getProviderConfig(id); } async testProviderConnection(id: string): Promise { - const provider = this.getProviderInstance(id); + const provider = this.providers.get(id); return provider?.testConnection(); } - private validateProviderConfig(config: SecretProviderConfig): boolean { - // implement validation logic - return true; - } - - private registerProviderInstance(provider: ISecretProvider) { - this.providers.set(provider.id, provider); - } - - private getProviderInstance(id: string): ISecretProvider | undefined { - return this.providers.get(id); - } - - private listProviderConfigs(): Promise { - return []; - } - - private hasProvider(id: string): boolean { - return this.providers.has(id); - } - - private unregisterProviderInstance(id: string): boolean { - return this.providers.delete(id); - } - private createProviderInstance( config: SecretProviderConfig ): ISecretProvider {} From bbefd3ff1326184c68809b9f0c2596846861def6 Mon Sep 17 00:00:00 2001 From: nafees87n Date: Tue, 30 Dec 2025 15:39:55 +0530 Subject: [PATCH 08/11] fix --- src/lib/secretsManager/secretsManager.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/secretsManager/secretsManager.ts b/src/lib/secretsManager/secretsManager.ts index fde69dc..d263e9f 100644 --- a/src/lib/secretsManager/secretsManager.ts +++ b/src/lib/secretsManager/secretsManager.ts @@ -3,9 +3,13 @@ import { ISecretProvider } from "./providerService/ISecretProvider"; import { SecretProviderConfig } from "./types"; export class SecretsManager { + private registry: IProviderRegistry; + private providers: Map = new Map(); - constructor(private registry: IProviderRegistry) {} + constructor(registry: IProviderRegistry) { + this.registry = registry; + } async initialize(): Promise { this.registry.initialize(); From a808ba9397cb9bdd659d4ac8cfd8017406322351 Mon Sep 17 00:00:00 2001 From: nafees87n Date: Fri, 9 Jan 2026 18:06:06 +0530 Subject: [PATCH 09/11] added caching service --- src/lib/secretsManager/cacheService.ts | 79 +++++++++++ .../cacheStorage/ICacheStorage.ts | 56 ++++++++ .../cacheStorage/InMemoryCacheStorage.ts | 66 +++++++++ .../encryptedStorage/IEncryptedStorage.ts | 25 +++- .../providerRegistry/FsProviderRegistry.ts | 20 ++- .../providerRegistry/IProviderRegistry.ts | 22 +++ .../providerService/ISecretProvider.ts | 25 +++- .../awsSecretManagerProvider.ts | 45 ++++++ .../providerService/providerFactory.ts | 13 ++ src/lib/secretsManager/secretsManager.ts | 129 +++++++++++++++++- src/lib/secretsManager/types.ts | 31 ++++- 11 files changed, 491 insertions(+), 20 deletions(-) create mode 100644 src/lib/secretsManager/cacheStorage/ICacheStorage.ts create mode 100644 src/lib/secretsManager/cacheStorage/InMemoryCacheStorage.ts create mode 100644 src/lib/secretsManager/providerService/providerFactory.ts diff --git a/src/lib/secretsManager/cacheService.ts b/src/lib/secretsManager/cacheService.ts index e69de29..e0b47ff 100644 --- a/src/lib/secretsManager/cacheService.ts +++ b/src/lib/secretsManager/cacheService.ts @@ -0,0 +1,79 @@ +import { ICacheStorage } from "./cacheStorage/ICacheStorage"; +import { CachedSecret, SecretProviderType } from "./types"; + +// Function +// 1. get/set/invalidate cached secrets +// 2. cache storage is pluggable (in-memory, file-based, redis, etc.) + +export class SecretsCacheService { + private storage: ICacheStorage; + + private defaultTTL: number; + + constructor(storage: ICacheStorage) { + this.storage = storage; + } + + async get( + providerId: string, + identifier: string, + version?: string + ): Promise { + const secret = await this.storage.findByIdentifier( + providerId, + identifier, + version + ); + return secret?.value ?? null; + } + + async set( + providerId: string, + providerType: SecretProviderType, + identifier: string, + value: string, + version?: string, + ttl?: number + ): Promise { + const now = Date.now(); + const secret: CachedSecret = { + id: this.generateId(), + identifier, + value, + providerId, + providerType, + fetchedAt: now, + expiresAt: now + (ttl ?? this.defaultTTL), + version, + }; + await this.storage.set(secret); + } + + async invalidate( + providerId: string, + key: string, + version?: string + ): Promise { + const secret = await this.storage.findByIdentifier( + providerId, + key, + version + ); + if (secret) { + await this.storage.delete(secret.id); + } + } + + async invalidateByProvider(providerId: string): Promise { + await this.storage.deleteByProvider(providerId); + } + + async clear(): Promise { + await this.storage.clear(); + } + + // eslint-disable-next-line class-methods-use-this + private generateId(): string { + return uuidv4(); + } +} diff --git a/src/lib/secretsManager/cacheStorage/ICacheStorage.ts b/src/lib/secretsManager/cacheStorage/ICacheStorage.ts new file mode 100644 index 0000000..3891dd7 --- /dev/null +++ b/src/lib/secretsManager/cacheStorage/ICacheStorage.ts @@ -0,0 +1,56 @@ +import { CachedSecret } from "../types"; +import { ISecretsStorage } from "../encryptedStorage/IEncryptedStorage"; + +// export interface ICacheStorage { +/** + * Get a cached secret by its unique ID + */ +// get(id: string): Promise; + +// /** +// * Store a cached secret +// */ +// set(secret: CachedSecret): Promise; + +// /** +// * Delete a cached secret by its ID +// */ +// delete(id: string): Promise; + +// /** +// * Find a cached secret by provider ID, key, and optional version +// */ +// findByIdentifier( +// providerId: string, +// identifier: string, +// version?: string +// ): Promise; + +// /** +// * Delete all cached secrets for a specific provider +// */ +// deleteByProvider(providerId: string): Promise; + +// /** +// * Clear all cached secrets +// */ +// clear(): Promise; +// } + +export abstract class AbstractCacheStorage implements ISecretsStorage { + abstract initialize(): Promise; + + abstract save(key: string, data: CachedSecret): Promise; + + abstract load(key: string): Promise; + + abstract delete(key: string): Promise; + + abstract findByIdentifier( + providerId: string, + identifier: string, + version?: string + ): Promise; + + abstract clear(): Promise; +} diff --git a/src/lib/secretsManager/cacheStorage/InMemoryCacheStorage.ts b/src/lib/secretsManager/cacheStorage/InMemoryCacheStorage.ts new file mode 100644 index 0000000..04fd97a --- /dev/null +++ b/src/lib/secretsManager/cacheStorage/InMemoryCacheStorage.ts @@ -0,0 +1,66 @@ +import { CachedSecret } from "../types"; +import { AbstractCacheStorage } from "./ICacheStorage"; + +export class InMemoryCacheStorage implements AbstractCacheStorage { + private cache: Map = new Map(); + + async load(key: string): Promise { + const secret = this.cache.get(key); + if (!secret) { + return null; + } + + if (Date.now() > secret.expiresAt) { + this.cache.delete(key); + return null; + } + return secret; + } + + async save(key: string, data: CachedSecret): Promise { + this.cache.set(key, data); + } + + async delete(id: string): Promise { + this.cache.delete(id); + } + + async findByIdentifier( + key: string, + identifier: string, + version?: string // Store multiple versions of the same secret or not? + ): Promise { + const now = Date.now(); + let found: CachedSecret | null = null; + + for (const secret of this.cache.values()) { + // Skip expired entries + if (now > secret.expiresAt) { + continue; + } + + if ( + secret.providerId === key && + secret.identifier === identifier && + secret.version === version + ) { + found = secret; + break; + } + } + + return found; + } + + async deleteByProvider(providerId: string): Promise { + for (const [id, secret] of this.cache.entries()) { + if (secret.providerId === providerId) { + this.cache.delete(id); + } + } + } + + async clear(): Promise { + this.cache.clear(); + } +} diff --git a/src/lib/secretsManager/encryptedStorage/IEncryptedStorage.ts b/src/lib/secretsManager/encryptedStorage/IEncryptedStorage.ts index 7e364ba..453ab7c 100644 --- a/src/lib/secretsManager/encryptedStorage/IEncryptedStorage.ts +++ b/src/lib/secretsManager/encryptedStorage/IEncryptedStorage.ts @@ -1,6 +1,27 @@ -export interface IEncryptedStorage { +// Can this be storage and not encrypted storage? +// export interface IEncryptedStorage { +// initialize(): Promise; +// save>(key: string, data: T): Promise; +// load>(key: string): Promise; +// delete(key: string): Promise; +// } + +export interface ISecretsStorage { initialize(): Promise; save>(key: string, data: T): Promise; - load>(key: string): Promise; + load>(key: string): Promise; delete(key: string): Promise; } + +export abstract class AbstractEncryptedStorage implements ISecretsStorage { + abstract initialize(): Promise; + + abstract save>( + key: string, + data: T + ): Promise; + + abstract load>(key: string): Promise; + + abstract delete(key: string): Promise; +} diff --git a/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts index 554e98a..eafd655 100644 --- a/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts +++ b/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts @@ -1,6 +1,8 @@ import * as fs from "fs/promises"; import * as path from "path"; -import { IProviderRegistry } from "./IProviderRegistry"; +import { + AbstractProviderRegistry, +} from "./IProviderRegistry"; import { SecretProviderConfig, SecretProviderType } from "../types"; import { IEncryptedStorage } from "../encryptedStorage/IEncryptedStorage"; @@ -15,13 +17,19 @@ export interface ProvidersManifest { }[]; } -export class FileBasedProviderRegistry implements IProviderRegistry { +// Functions +// 1. initialize registry (create config dir if not exists) +// 2. list providers +// 3. + +export class FileBasedProviderRegistry extends AbstractProviderRegistry { private manifestPath: string; - constructor( - private readonly encryptedStorage: IEncryptedStorage, - private readonly configDir: string - ) { + private configDir: string; + + constructor(encryptedStorage: IEncryptedStorage, configDir: string) { + super(encryptedStorage); + this.configDir = configDir; this.manifestPath = path.join(configDir, MANIFEST_FILENAME); } diff --git a/src/lib/secretsManager/providerRegistry/IProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/IProviderRegistry.ts index e9fda3b..16d1a70 100644 --- a/src/lib/secretsManager/providerRegistry/IProviderRegistry.ts +++ b/src/lib/secretsManager/providerRegistry/IProviderRegistry.ts @@ -1,4 +1,6 @@ +// filepath: /Users/nafeesn/requestly/requestly-desktop-app/src/lib/secretsManager/providerRegistry/IProviderRegistry.ts import { SecretProviderConfig } from "../types"; +import { IEncryptedStorage } from "../encryptedStorage/IEncryptedStorage"; export interface IProviderRegistry { initialize(): Promise; @@ -8,3 +10,23 @@ export interface IProviderRegistry { deleteProviderConfig(id: string): Promise; getProviderConfig(id: string): Promise; } + +export abstract class AbstractProviderRegistry implements IProviderRegistry { + protected encryptedStorage: IEncryptedStorage + + constructor(encryptedStorage: IEncryptedStorage) { + this.encryptedStorage = encryptedStorage; + } + + abstract initialize(): Promise; + + abstract listProviders(): Promise; + + abstract loadAllProviderConfigs(): Promise; + + abstract saveProviderConfig(config: SecretProviderConfig): Promise; + + abstract deleteProviderConfig(id: string): Promise; + + abstract getProviderConfig(id: string): Promise; +} diff --git a/src/lib/secretsManager/providerService/ISecretProvider.ts b/src/lib/secretsManager/providerService/ISecretProvider.ts index 353f8a7..f060570 100644 --- a/src/lib/secretsManager/providerService/ISecretProvider.ts +++ b/src/lib/secretsManager/providerService/ISecretProvider.ts @@ -1,12 +1,29 @@ -import { SecretProviderType } from "../types"; +import { SecretProviderType, SecretReference } from "../types"; -export interface ISecretProvider { +// evaluate if its bettter to have abstract class than interface +interface ISecretProvider { type: SecretProviderType; id: string; testConnection(): Promise; - fetchSecret(secretName: string): Promise; + fetchSecret(ref: SecretReference): Promise; listSecrets(): Promise; +} + +export abstract class AbstractSecretProvider implements ISecretProvider { + abstract readonly type: SecretProviderType; + + abstract readonly id: string; + + protected config: any; + + abstract testConnection(): Promise; + + abstract fetchSecret(ref: SecretReference): Promise; + + abstract listSecrets(): Promise; - validateConfig(): boolean; + static validateConfig(config: any): boolean { + throw new Error("Not implemented"); + } } diff --git a/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts b/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts index e69de29..2a0a394 100644 --- a/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts +++ b/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts @@ -0,0 +1,45 @@ +import { AbstractSecretProvider } from "./ISecretProvider"; +import { + AWSSecretsManagerConfig, + SecretProviderConfig, + SecretProviderType, + SecretReference, +} from "../types"; + +// Functions +// 1. validate config +// 2. test connection +// 3. fetch secret +// 4. list secrets + +export class AWSSecretsManagerProvider extends AbstractSecretProvider { + readonly type = SecretProviderType.AWS_SECRETS_MANAGER; + + readonly id: string; + + protected config: AWSSecretsManagerConfig; + + constructor(providerConfig: SecretProviderConfig) { + super(); + this.id = providerConfig.id; + this.config = providerConfig.config as AWSSecretsManagerConfig; + } + + static validateConfig(config: AWSSecretsManagerConfig): boolean { + return Boolean( + config.accessKeyId && config.secretAccessKey && config.region + ); + } + + async testConnection(): Promise { + if (!AWSSecretsManagerProvider.validateConfig(this.config)) { + return false; + } + + return true; + } + + async fetchSecret(ref: SecretReference): Promise {} + + async listSecrets(): Promise {} +} diff --git a/src/lib/secretsManager/providerService/providerFactory.ts b/src/lib/secretsManager/providerService/providerFactory.ts new file mode 100644 index 0000000..70a68c8 --- /dev/null +++ b/src/lib/secretsManager/providerService/providerFactory.ts @@ -0,0 +1,13 @@ +import { SecretProviderConfig, SecretProviderType } from "../types"; +import { ISecretProvider } from "./ISecretProvider"; +import { AWSSecretsManagerProvider } from "./awsSecretManagerProvider"; + +export function createProvider(config: SecretProviderConfig): ISecretProvider { + switch (config.type) { + case SecretProviderType.AWS_SECRETS_MANAGER: + return new AWSSecretsManagerProvider(config); + default: + throw new Error(`Unknown provider type: ${config.type}`); + } +} + diff --git a/src/lib/secretsManager/secretsManager.ts b/src/lib/secretsManager/secretsManager.ts index d263e9f..a5d2008 100644 --- a/src/lib/secretsManager/secretsManager.ts +++ b/src/lib/secretsManager/secretsManager.ts @@ -1,14 +1,40 @@ +import { SecretsCacheService } from "./cacheService"; import { IProviderRegistry } from "./providerRegistry/IProviderRegistry"; -import { ISecretProvider } from "./providerService/ISecretProvider"; -import { SecretProviderConfig } from "./types"; +import { createProvider } from "./providerService/providerFactory"; +import { SecretProviderConfig, SecretReference } from "./types"; +import { AbstractSecretProvider } from "./providerService/ISecretProvider"; + +// Questions +// 1. store multiple versions of the same secret or not? +// 2. use secretReference or do it like mp with union types? +// 3. cache invalidation strategy? +// 4. Need a new method for refreshing all the cached secrets +// --5. reuse the storage interface for encrypted storage and cache storage? + +// Functions +// 1. initialize registry and cache service +// 2. add/remove provider configs +// 3. fetch secret (with caching) +// 4. list secrets +// 5. invalidate cache + +// FLows +// 1. INIT - load all provider configs from the registry and create provider instances. +// 2. ADD/REMOVE PROVIDER CONFIG - update the registry and provider instances map. +// 3. FETCH SECRET - check cache first, if not found or expired, fetch from provider, store in cache, return secret. +// 4. Refresh Secrets - bulk fetch and update all secrets from their providers and update the cache +// export class SecretsManager { private registry: IProviderRegistry; - private providers: Map = new Map(); + private cacheService: SecretsCacheService; + + private providers: Map = new Map(); - constructor(registry: IProviderRegistry) { + constructor(registry: IProviderRegistry, cacheService: SecretsCacheService) { this.registry = registry; + this.cacheService = cacheService; } async initialize(): Promise { @@ -39,10 +65,101 @@ export class SecretsManager { async testProviderConnection(id: string): Promise { const provider = this.providers.get(id); - return provider?.testConnection(); + return provider?.testConnection() ?? false; + } + + async fetchSecret(providerId: string, ref: SecretReference): Promise { + const cached = await this.cacheService.get( + providerId, + ref.identifier, + ref.version + ); + + if (cached) { + return cached; + } + + const provider = this.providers.get(providerId); + if (!provider) { + throw new Error(`Provider not found: ${providerId}`); + } + + const secret = await provider.fetchSecret(ref); + + await this.cacheService.set( + providerId, + provider.type, + ref.identifier, + secret, + ref.version + ); + + return secret; } + async refreshSecrets( + secrets: Array<{ providerId: string; ref: SecretReference }> + ): Promise { + for (const s of secrets) { + // Invalidate cache + // Fetch fresh secret and update cache + } + } + + async fetchSecrets( + secrets: Array<{ providerId: string; ref: SecretReference }> + ): Promise> { + for (const s of secrets) { + const secret = await this.fetchSecret(s.providerId, s.ref); + } + } + + // Do we need this method? + async fetchSecretFresh( + providerId: string, + ref: SecretReference + ): Promise { + await this.cacheService.invalidate(providerId, ref.identifier, ref.version); + + const provider = this.providers.get(providerId); + if (!provider) { + throw new Error(`Provider not found: ${providerId}`); + } + + const secret = await provider.fetchSecret(ref); + + await this.cacheService.set( + providerId, + provider.type, + ref.identifier, + secret, + ref.version + ); + + return secret; + } + + async listSecrets(providerId: string): Promise { + const provider = this.providers.get(providerId); + if (!provider) { + throw new Error(`Provider not found: ${providerId}`); + } + + return provider.listSecrets(); + } + + async invalidateCache(providerId?: string): Promise { + if (providerId) { + await this.cacheService.invalidateByProvider(providerId); + } else { + await this.cacheService.clear(); + } + } + + // eslint-disable-next-line class-methods-use-this private createProviderInstance( config: SecretProviderConfig - ): ISecretProvider {} + ): AbstractSecretProvider { + return createProvider(config); + } } diff --git a/src/lib/secretsManager/types.ts b/src/lib/secretsManager/types.ts index 6440b39..1a1a962 100644 --- a/src/lib/secretsManager/types.ts +++ b/src/lib/secretsManager/types.ts @@ -1,5 +1,5 @@ export enum SecretProviderType { - AWS_SECRETS_MANGER = "aws", + AWS_SECRETS_MANAGER = "aws", } export interface AWSSecretsManagerConfig { @@ -9,11 +9,38 @@ export interface AWSSecretsManagerConfig { sessionToken?: string; } +export type ProviderSpecificConfig = AWSSecretsManagerConfig; // | HashicorpVaultConfig | OtherProviderConfig; + export interface SecretProviderConfig { id: string; type: SecretProviderType; name: string; createdAt: number; updatedAt: number; - config: AWSSecretsManagerConfig; + config: ProviderSpecificConfig; +} + +export interface SecretReference { + identifier: string; + version?: string; + key?: string; +} + +/** + * + * type SecretReference = + * | { type: "aws"; nameOrArn: string; versionId?: string; versionStage?: string } + * | { type: "vault"; path: string; version?: number; key?: string }; + */ + + +export interface CachedSecret { + id: string; // Unique identifier + identifier: string; // Secret identifier (name, ARN, or path) + value: string; // The actual secret value + providerId: string; + providerType: SecretProviderType; + fetchedAt: number; + expiresAt: number; + version?: string; } From aa26c497dc4447740148fee42532317826c497ce Mon Sep 17 00:00:00 2001 From: nafees87n Date: Mon, 12 Jan 2026 18:29:55 +0530 Subject: [PATCH 10/11] refactored providerRegistry --- src/lib/secretsManager/cacheService.ts | 79 ----------- .../cacheStorage/ICacheStorage.ts | 56 -------- .../cacheStorage/InMemoryCacheStorage.ts | 66 --------- .../AbstractEncryptedStorage.ts | 12 ++ .../encryptedStorage/IEncryptedStorage.ts | 27 ---- .../AbstractProviderRegistry.ts | 42 ++++++ .../providerRegistry/FsProviderRegistry.ts | 65 ++++++--- .../providerRegistry/IProviderRegistry.ts | 32 ----- .../providerService/AbstractSecretProvider.ts | 27 ++++ .../providerService/ISecretProvider.ts | 29 ---- .../awsSecretManagerProvider.ts | 15 ++- src/lib/secretsManager/secretsManager.ts | 125 ++++++------------ src/lib/secretsManager/types.ts | 18 +++ 13 files changed, 195 insertions(+), 398 deletions(-) delete mode 100644 src/lib/secretsManager/cacheService.ts delete mode 100644 src/lib/secretsManager/cacheStorage/ICacheStorage.ts delete mode 100644 src/lib/secretsManager/cacheStorage/InMemoryCacheStorage.ts create mode 100644 src/lib/secretsManager/encryptedStorage/AbstractEncryptedStorage.ts delete mode 100644 src/lib/secretsManager/encryptedStorage/IEncryptedStorage.ts create mode 100644 src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts delete mode 100644 src/lib/secretsManager/providerRegistry/IProviderRegistry.ts create mode 100644 src/lib/secretsManager/providerService/AbstractSecretProvider.ts delete mode 100644 src/lib/secretsManager/providerService/ISecretProvider.ts diff --git a/src/lib/secretsManager/cacheService.ts b/src/lib/secretsManager/cacheService.ts deleted file mode 100644 index e0b47ff..0000000 --- a/src/lib/secretsManager/cacheService.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { ICacheStorage } from "./cacheStorage/ICacheStorage"; -import { CachedSecret, SecretProviderType } from "./types"; - -// Function -// 1. get/set/invalidate cached secrets -// 2. cache storage is pluggable (in-memory, file-based, redis, etc.) - -export class SecretsCacheService { - private storage: ICacheStorage; - - private defaultTTL: number; - - constructor(storage: ICacheStorage) { - this.storage = storage; - } - - async get( - providerId: string, - identifier: string, - version?: string - ): Promise { - const secret = await this.storage.findByIdentifier( - providerId, - identifier, - version - ); - return secret?.value ?? null; - } - - async set( - providerId: string, - providerType: SecretProviderType, - identifier: string, - value: string, - version?: string, - ttl?: number - ): Promise { - const now = Date.now(); - const secret: CachedSecret = { - id: this.generateId(), - identifier, - value, - providerId, - providerType, - fetchedAt: now, - expiresAt: now + (ttl ?? this.defaultTTL), - version, - }; - await this.storage.set(secret); - } - - async invalidate( - providerId: string, - key: string, - version?: string - ): Promise { - const secret = await this.storage.findByIdentifier( - providerId, - key, - version - ); - if (secret) { - await this.storage.delete(secret.id); - } - } - - async invalidateByProvider(providerId: string): Promise { - await this.storage.deleteByProvider(providerId); - } - - async clear(): Promise { - await this.storage.clear(); - } - - // eslint-disable-next-line class-methods-use-this - private generateId(): string { - return uuidv4(); - } -} diff --git a/src/lib/secretsManager/cacheStorage/ICacheStorage.ts b/src/lib/secretsManager/cacheStorage/ICacheStorage.ts deleted file mode 100644 index 3891dd7..0000000 --- a/src/lib/secretsManager/cacheStorage/ICacheStorage.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { CachedSecret } from "../types"; -import { ISecretsStorage } from "../encryptedStorage/IEncryptedStorage"; - -// export interface ICacheStorage { -/** - * Get a cached secret by its unique ID - */ -// get(id: string): Promise; - -// /** -// * Store a cached secret -// */ -// set(secret: CachedSecret): Promise; - -// /** -// * Delete a cached secret by its ID -// */ -// delete(id: string): Promise; - -// /** -// * Find a cached secret by provider ID, key, and optional version -// */ -// findByIdentifier( -// providerId: string, -// identifier: string, -// version?: string -// ): Promise; - -// /** -// * Delete all cached secrets for a specific provider -// */ -// deleteByProvider(providerId: string): Promise; - -// /** -// * Clear all cached secrets -// */ -// clear(): Promise; -// } - -export abstract class AbstractCacheStorage implements ISecretsStorage { - abstract initialize(): Promise; - - abstract save(key: string, data: CachedSecret): Promise; - - abstract load(key: string): Promise; - - abstract delete(key: string): Promise; - - abstract findByIdentifier( - providerId: string, - identifier: string, - version?: string - ): Promise; - - abstract clear(): Promise; -} diff --git a/src/lib/secretsManager/cacheStorage/InMemoryCacheStorage.ts b/src/lib/secretsManager/cacheStorage/InMemoryCacheStorage.ts deleted file mode 100644 index 04fd97a..0000000 --- a/src/lib/secretsManager/cacheStorage/InMemoryCacheStorage.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { CachedSecret } from "../types"; -import { AbstractCacheStorage } from "./ICacheStorage"; - -export class InMemoryCacheStorage implements AbstractCacheStorage { - private cache: Map = new Map(); - - async load(key: string): Promise { - const secret = this.cache.get(key); - if (!secret) { - return null; - } - - if (Date.now() > secret.expiresAt) { - this.cache.delete(key); - return null; - } - return secret; - } - - async save(key: string, data: CachedSecret): Promise { - this.cache.set(key, data); - } - - async delete(id: string): Promise { - this.cache.delete(id); - } - - async findByIdentifier( - key: string, - identifier: string, - version?: string // Store multiple versions of the same secret or not? - ): Promise { - const now = Date.now(); - let found: CachedSecret | null = null; - - for (const secret of this.cache.values()) { - // Skip expired entries - if (now > secret.expiresAt) { - continue; - } - - if ( - secret.providerId === key && - secret.identifier === identifier && - secret.version === version - ) { - found = secret; - break; - } - } - - return found; - } - - async deleteByProvider(providerId: string): Promise { - for (const [id, secret] of this.cache.entries()) { - if (secret.providerId === providerId) { - this.cache.delete(id); - } - } - } - - async clear(): Promise { - this.cache.clear(); - } -} diff --git a/src/lib/secretsManager/encryptedStorage/AbstractEncryptedStorage.ts b/src/lib/secretsManager/encryptedStorage/AbstractEncryptedStorage.ts new file mode 100644 index 0000000..d3a4308 --- /dev/null +++ b/src/lib/secretsManager/encryptedStorage/AbstractEncryptedStorage.ts @@ -0,0 +1,12 @@ +export abstract class AbstractEncryptedStorage { + abstract initialize(): Promise; + + abstract save>( + key: string, + data: T + ): Promise; + + abstract load>(key: string): Promise; + + abstract delete(key: string): Promise; +} diff --git a/src/lib/secretsManager/encryptedStorage/IEncryptedStorage.ts b/src/lib/secretsManager/encryptedStorage/IEncryptedStorage.ts deleted file mode 100644 index 453ab7c..0000000 --- a/src/lib/secretsManager/encryptedStorage/IEncryptedStorage.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Can this be storage and not encrypted storage? -// export interface IEncryptedStorage { -// initialize(): Promise; -// save>(key: string, data: T): Promise; -// load>(key: string): Promise; -// delete(key: string): Promise; -// } - -export interface ISecretsStorage { - initialize(): Promise; - save>(key: string, data: T): Promise; - load>(key: string): Promise; - delete(key: string): Promise; -} - -export abstract class AbstractEncryptedStorage implements ISecretsStorage { - abstract initialize(): Promise; - - abstract save>( - key: string, - data: T - ): Promise; - - abstract load>(key: string): Promise; - - abstract delete(key: string): Promise; -} diff --git a/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts new file mode 100644 index 0000000..8b5fbcf --- /dev/null +++ b/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts @@ -0,0 +1,42 @@ +import { SecretProviderConfig, SecretProviderType } from "../types"; +import { AbstractEncryptedStorage } from "../encryptedStorage/AbstractEncryptedStorage"; +import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider"; + +export interface ProvidersManifest { + version: string; + providers: { + id: string; + storagePath: string; + type: SecretProviderType; + }[]; +} + +export abstract class AbstractProviderRegistry { + protected encryptedStorage: AbstractEncryptedStorage; + + protected providers: Map = new Map(); + + constructor(encryptedStorage: AbstractEncryptedStorage) { + this.encryptedStorage = encryptedStorage; + } + + abstract initialize(): Promise; + + protected abstract createProviderInstance( + config: SecretProviderConfig + ): AbstractSecretProvider; + + protected abstract loadManifest(): Promise; + + protected abstract saveManifest(manifest: ProvidersManifest): Promise; + + abstract getAllProviderConfigs(): Promise; + + abstract getProviderConfig(id: string): Promise; + + abstract setProviderConfig(config: SecretProviderConfig): Promise; + + abstract deleteProviderConfig(id: string): Promise; + + abstract getProvider(providerId: string): AbstractSecretProvider | null; +} diff --git a/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts index eafd655..d8428d8 100644 --- a/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts +++ b/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts @@ -1,21 +1,14 @@ -import * as fs from "fs/promises"; +import * as fs from "fs"; import * as path from "path"; -import { - AbstractProviderRegistry, -} from "./IProviderRegistry"; import { SecretProviderConfig, SecretProviderType } from "../types"; -import { IEncryptedStorage } from "../encryptedStorage/IEncryptedStorage"; +import { createProvider } from "../providerService/providerFactory"; +import { AbstractProviderRegistry } from "./AbstractProviderRegistry"; +import { AbstractEncryptedStorage } from "../encryptedStorage/AbstractEncryptedStorage"; +import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider"; const MANIFEST_FILENAME = "providers.json"; -export interface ProvidersManifest { - version: string; - providers: { - id: string; - storagePath: string; - type: SecretProviderType; - }[]; -} + // Functions // 1. initialize registry (create config dir if not exists) @@ -27,24 +20,45 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { private configDir: string; - constructor(encryptedStorage: IEncryptedStorage, configDir: string) { + protected providers: Map = new Map(); + + constructor(encryptedStorage: AbstractEncryptedStorage, configDir: string) { super(encryptedStorage); this.configDir = configDir; this.manifestPath = path.join(configDir, MANIFEST_FILENAME); } + getProvider(providerId: string): AbstractSecretProvider | null { + return this.providers.get(providerId) || null; + } + async initialize(): Promise { await this.ensureConfigDir(); + this.initProvidersFromManifest(); } - async listProviders(): Promise { - const manifest = await this.loadManifest(); - return manifest.providers.map((p) => p.id); + private async initProvidersFromManifest() { + const configs = await this.getAllProviderConfigs(); + configs.forEach((config) => { + this.providers.set(config.id, this.createProviderInstance(config)); + }); } - async loadAllProviderConfigs(): Promise {} + async getAllProviderConfigs(): Promise { + const manifest = await this.loadManifest(); + const configs: SecretProviderConfig[] = []; - async saveProviderConfig(config: SecretProviderConfig): Promise { + for (const entry of manifest.providers) { + const config = await this.encryptedStorage.load( + entry.id + ); + configs.push(config); + } + + return configs; + } + + async setProviderConfig(config: SecretProviderConfig): Promise { const manifest = await this.loadManifest(); const storageKey = config.id; @@ -53,6 +67,7 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { // Update manifest await this.saveManifest(manifest); + this.providers.set(config.id, this.createProviderInstance(config)); } async deleteProviderConfig(id: string): Promise { @@ -64,6 +79,7 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { manifest.providers = manifest.providers.filter((p) => p.id !== id); await this.saveManifest(manifest); + this.providers.delete(id); } async getProviderConfig(id: string): Promise { @@ -82,7 +98,14 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { } } - private async loadManifest(): Promise {} + protected async loadManifest(): Promise {} + + protected async saveManifest(manifest: ProvidersManifest): Promise {} - private async saveManifest(manifest: ProvidersManifest): Promise {} + // eslint-disable-next-line class-methods-use-this + protected createProviderInstance( + config: SecretProviderConfig + ): AbstractSecretProvider { + return createProvider(config); + } } diff --git a/src/lib/secretsManager/providerRegistry/IProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/IProviderRegistry.ts deleted file mode 100644 index 16d1a70..0000000 --- a/src/lib/secretsManager/providerRegistry/IProviderRegistry.ts +++ /dev/null @@ -1,32 +0,0 @@ -// filepath: /Users/nafeesn/requestly/requestly-desktop-app/src/lib/secretsManager/providerRegistry/IProviderRegistry.ts -import { SecretProviderConfig } from "../types"; -import { IEncryptedStorage } from "../encryptedStorage/IEncryptedStorage"; - -export interface IProviderRegistry { - initialize(): Promise; - listProviders(): Promise; - loadAllProviderConfigs(): Promise; - saveProviderConfig(config: SecretProviderConfig): Promise; - deleteProviderConfig(id: string): Promise; - getProviderConfig(id: string): Promise; -} - -export abstract class AbstractProviderRegistry implements IProviderRegistry { - protected encryptedStorage: IEncryptedStorage - - constructor(encryptedStorage: IEncryptedStorage) { - this.encryptedStorage = encryptedStorage; - } - - abstract initialize(): Promise; - - abstract listProviders(): Promise; - - abstract loadAllProviderConfigs(): Promise; - - abstract saveProviderConfig(config: SecretProviderConfig): Promise; - - abstract deleteProviderConfig(id: string): Promise; - - abstract getProviderConfig(id: string): Promise; -} diff --git a/src/lib/secretsManager/providerService/AbstractSecretProvider.ts b/src/lib/secretsManager/providerService/AbstractSecretProvider.ts new file mode 100644 index 0000000..c0628d7 --- /dev/null +++ b/src/lib/secretsManager/providerService/AbstractSecretProvider.ts @@ -0,0 +1,27 @@ +import { CachedSecret, SecretProviderType, SecretReference } from "../types"; + +export abstract class AbstractSecretProvider { + protected cache: Map = new Map(); + + abstract getSecretIdentfier(ref: SecretReference): string; + + abstract readonly type: SecretProviderType; + + abstract readonly id: string; + + protected config: any; + + abstract testConnection(): Promise; + + abstract getSecret(ref: SecretReference): Promise; + + abstract getSecrets(): Promise; + + abstract setSecret(): Promise; + + abstract setSecrets(): Promise; + + static validateConfig(config: any): boolean { + throw new Error("Not implemented"); + } +} diff --git a/src/lib/secretsManager/providerService/ISecretProvider.ts b/src/lib/secretsManager/providerService/ISecretProvider.ts deleted file mode 100644 index f060570..0000000 --- a/src/lib/secretsManager/providerService/ISecretProvider.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { SecretProviderType, SecretReference } from "../types"; - -// evaluate if its bettter to have abstract class than interface -interface ISecretProvider { - type: SecretProviderType; - id: string; - - testConnection(): Promise; - fetchSecret(ref: SecretReference): Promise; - listSecrets(): Promise; -} - -export abstract class AbstractSecretProvider implements ISecretProvider { - abstract readonly type: SecretProviderType; - - abstract readonly id: string; - - protected config: any; - - abstract testConnection(): Promise; - - abstract fetchSecret(ref: SecretReference): Promise; - - abstract listSecrets(): Promise; - - static validateConfig(config: any): boolean { - throw new Error("Not implemented"); - } -} diff --git a/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts b/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts index 2a0a394..7b6152d 100644 --- a/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts +++ b/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts @@ -5,6 +5,7 @@ import { SecretProviderType, SecretReference, } from "../types"; +import { SecretsCacheService } from "../cacheService"; // Functions // 1. validate config @@ -17,12 +18,24 @@ export class AWSSecretsManagerProvider extends AbstractSecretProvider { readonly id: string; + protected config: AWSSecretsManagerConfig; - constructor(providerConfig: SecretProviderConfig) { + protected cacheService: SecretsCacheService; + + // { type: "aws"; nameOrArn: string; versionId?: string; versionStage?: string } + protected getSecretIdentfier(ref: AwsReference): string { + return `name=${this.nameOrArn};version:${ref.version}`; + } + + constructor( + providerConfig: SecretProviderConfig, + cacheService: SecretsCacheService + ) { super(); this.id = providerConfig.id; this.config = providerConfig.config as AWSSecretsManagerConfig; + this.cacheService = cacheService; } static validateConfig(config: AWSSecretsManagerConfig): boolean { diff --git a/src/lib/secretsManager/secretsManager.ts b/src/lib/secretsManager/secretsManager.ts index a5d2008..584c4e6 100644 --- a/src/lib/secretsManager/secretsManager.ts +++ b/src/lib/secretsManager/secretsManager.ts @@ -1,15 +1,16 @@ import { SecretsCacheService } from "./cacheService"; -import { IProviderRegistry } from "./providerRegistry/IProviderRegistry"; -import { createProvider } from "./providerService/providerFactory"; import { SecretProviderConfig, SecretReference } from "./types"; -import { AbstractSecretProvider } from "./providerService/ISecretProvider"; +import { EncryptedFsStorageService } from "./encryptedStorage/encryptedFsStorageService"; +import { FileBasedProviderRegistry } from "./providerRegistry/FsProviderRegistry"; +import { AbstractProviderRegistry } from "./providerRegistry/AbstractProviderRegistry"; // Questions -// 1. store multiple versions of the same secret or not? +// 1. store multiple versions of the same secret or not?@ // 2. use secretReference or do it like mp with union types? -// 3. cache invalidation strategy? +// 3. cache invalidation strategy?@ // 4. Need a new method for refreshing all the cached secrets // --5. reuse the storage interface for encrypted storage and cache storage? +// 6. // providerId in fetchByIdentifier ?? would be confusing and then I can name it key but without using "key" word. How can I make it generic? AI suggested using composite key. // Functions // 1. initialize registry and cache service @@ -25,37 +26,44 @@ import { AbstractSecretProvider } from "./providerService/ISecretProvider"; // 4. Refresh Secrets - bulk fetch and update all secrets from their providers and update the cache // -export class SecretsManager { - private registry: IProviderRegistry; +// Merging secretManager and providerRegistry +// Methods like createProviderInstance need to be moved to the providerRegistry + +const encryptedStorage = new EncryptedFsStorageService(""); +const providerRegistry = new FileBasedProviderRegistry(encryptedStorage, ""); + +const secretsManager = new SecretsManager(providerRegistry); + +// createProviderInstance should have cache Storage as dependency. +// providerRegistry cannot be exposed because it would have storage specific code like fs methods etc. - private cacheService: SecretsCacheService; +// Why need to change cache storage at provider lever? +// Different providers may have different cache storage requirements but why? +// Different providers can have different source of truth but caching should be common. WHy not? +// But agreed provider should be the one interacting with cache storage and repo layer and not secretmanager - private providers: Map = new Map(); +// Changes +// 1. cacheService associated with provider instance instead of secretManager. +// 2. registry manages the providers map +// 3. All the methods from secretManager delegated to registry and provider instances. +// 4. provider instance creation is moved to registry. - constructor(registry: IProviderRegistry, cacheService: SecretsCacheService) { +export class SecretsManager { + private registry: AbstractProviderRegistry; + + constructor(registry: AbstractProviderRegistry) { this.registry = registry; - this.cacheService = cacheService; } async initialize(): Promise { this.registry.initialize(); - this.initProvidersFromManifest(); - } - - private async initProvidersFromManifest() { - const configs = await this.registry.loadAllProviderConfigs(); - configs.forEach((config) => { - this.providers.set(config.id, this.createProviderInstance(config)); - }); } async addProviderConfig(config: SecretProviderConfig) { - this.providers.set(config.id, this.createProviderInstance(config)); - this.registry.saveProviderConfig(config); + this.registry.setProviderConfig(config); } async removeProviderConfig(id: string) { - this.providers.delete(id); this.registry.deleteProviderConfig(id); } @@ -64,37 +72,12 @@ export class SecretsManager { } async testProviderConnection(id: string): Promise { - const provider = this.providers.get(id); + const provider = this.registry.getProvider(id); return provider?.testConnection() ?? false; } async fetchSecret(providerId: string, ref: SecretReference): Promise { - const cached = await this.cacheService.get( - providerId, - ref.identifier, - ref.version - ); - - if (cached) { - return cached; - } - - const provider = this.providers.get(providerId); - if (!provider) { - throw new Error(`Provider not found: ${providerId}`); - } - - const secret = await provider.fetchSecret(ref); - - await this.cacheService.set( - providerId, - provider.type, - ref.identifier, - secret, - ref.version - ); - - return secret; + this.registry.getProvider(providerId)?.fetchSecret(ref); } async refreshSecrets( @@ -114,33 +97,8 @@ export class SecretsManager { } } - // Do we need this method? - async fetchSecretFresh( - providerId: string, - ref: SecretReference - ): Promise { - await this.cacheService.invalidate(providerId, ref.identifier, ref.version); - - const provider = this.providers.get(providerId); - if (!provider) { - throw new Error(`Provider not found: ${providerId}`); - } - - const secret = await provider.fetchSecret(ref); - - await this.cacheService.set( - providerId, - provider.type, - ref.identifier, - secret, - ref.version - ); - - return secret; - } - async listSecrets(providerId: string): Promise { - const provider = this.providers.get(providerId); + const provider = this.registry.getProvider(providerId); if (!provider) { throw new Error(`Provider not found: ${providerId}`); } @@ -148,18 +106,11 @@ export class SecretsManager { return provider.listSecrets(); } - async invalidateCache(providerId?: string): Promise { - if (providerId) { - await this.cacheService.invalidateByProvider(providerId); - } else { - await this.cacheService.clear(); + async invalidateCache(providerId: string): Promise { + const provider = this.registry.getProvider(providerId); + if (!provider) { + throw new Error(`Provider not found: ${providerId}`); } - } - - // eslint-disable-next-line class-methods-use-this - private createProviderInstance( - config: SecretProviderConfig - ): AbstractSecretProvider { - return createProvider(config); + await provider.invalidateCache(); } } diff --git a/src/lib/secretsManager/types.ts b/src/lib/secretsManager/types.ts index 1a1a962..6b0a23f 100644 --- a/src/lib/secretsManager/types.ts +++ b/src/lib/secretsManager/types.ts @@ -26,6 +26,8 @@ export interface SecretReference { key?: string; } + + /** * * type SecretReference = @@ -33,6 +35,22 @@ export interface SecretReference { * | { type: "vault"; path: string; version?: number; key?: string }; */ +ProviderCache: { + [key: string]: { + value: string; + ttl: number; + }[] +} = { + "foo": { + value: "bar", + ttl: 1000, + } +} + + + + + export interface CachedSecret { id: string; // Unique identifier From 13f5131de6dc6ea79aae1eb7f63b79bbdb76e0fb Mon Sep 17 00:00:00 2001 From: nafees87n Date: Mon, 12 Jan 2026 23:47:53 +0530 Subject: [PATCH 11/11] fix: types --- .../providerRegistry/FsProviderRegistry.ts | 1 + .../providerService/AbstractSecretProvider.ts | 4 +- .../awsSecretManagerProvider.ts | 64 +++++++++++++------ src/lib/secretsManager/secretsManager.ts | 28 +------- src/lib/secretsManager/types.ts | 34 ++-------- 5 files changed, 53 insertions(+), 78 deletions(-) diff --git a/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts index d8428d8..7631509 100644 --- a/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts +++ b/src/lib/secretsManager/providerRegistry/FsProviderRegistry.ts @@ -62,6 +62,7 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { const manifest = await this.loadManifest(); const storageKey = config.id; + // do atomic write await this.encryptedStorage.save(storageKey, config); // Update manifest diff --git a/src/lib/secretsManager/providerService/AbstractSecretProvider.ts b/src/lib/secretsManager/providerService/AbstractSecretProvider.ts index c0628d7..6902918 100644 --- a/src/lib/secretsManager/providerService/AbstractSecretProvider.ts +++ b/src/lib/secretsManager/providerService/AbstractSecretProvider.ts @@ -3,14 +3,14 @@ import { CachedSecret, SecretProviderType, SecretReference } from "../types"; export abstract class AbstractSecretProvider { protected cache: Map = new Map(); - abstract getSecretIdentfier(ref: SecretReference): string; - abstract readonly type: SecretProviderType; abstract readonly id: string; protected config: any; + protected abstract getSecretIdentfier(ref: SecretReference): string; + abstract testConnection(): Promise; abstract getSecret(ref: SecretReference): Promise; diff --git a/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts b/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts index 7b6152d..6858740 100644 --- a/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts +++ b/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts @@ -1,11 +1,13 @@ -import { AbstractSecretProvider } from "./ISecretProvider"; +/* eslint-disable class-methods-use-this */ import { + AwsSecretReference, AWSSecretsManagerConfig, + CachedSecret, SecretProviderConfig, SecretProviderType, SecretReference, } from "../types"; -import { SecretsCacheService } from "../cacheService"; +import { AbstractSecretProvider } from "./AbstractSecretProvider"; // Functions // 1. validate config @@ -13,35 +15,25 @@ import { SecretsCacheService } from "../cacheService"; // 3. fetch secret // 4. list secrets +const DEFAULT_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes + export class AWSSecretsManagerProvider extends AbstractSecretProvider { readonly type = SecretProviderType.AWS_SECRETS_MANAGER; readonly id: string; - protected config: AWSSecretsManagerConfig; - protected cacheService: SecretsCacheService; + protected cache: Map = new Map(); - // { type: "aws"; nameOrArn: string; versionId?: string; versionStage?: string } - protected getSecretIdentfier(ref: AwsReference): string { - return `name=${this.nameOrArn};version:${ref.version}`; + protected getSecretIdentfier(ref: AwsSecretReference): string { + return `name=${ref.nameOrArn};version:${ref.version}`; } - constructor( - providerConfig: SecretProviderConfig, - cacheService: SecretsCacheService - ) { + constructor(providerConfig: SecretProviderConfig) { super(); this.id = providerConfig.id; this.config = providerConfig.config as AWSSecretsManagerConfig; - this.cacheService = cacheService; - } - - static validateConfig(config: AWSSecretsManagerConfig): boolean { - return Boolean( - config.accessKeyId && config.secretAccessKey && config.region - ); } async testConnection(): Promise { @@ -52,7 +44,39 @@ export class AWSSecretsManagerProvider extends AbstractSecretProvider { return true; } - async fetchSecret(ref: SecretReference): Promise {} + async getSecret(ref: AwsSecretReference): Promise { + const secretKey = this.getSecretIdentfier(ref); + const cachedSecret = this.cache.get(secretKey); + const now = Date.now(); + + if (cachedSecret && cachedSecret.expiry > now) { + return cachedSecret.value; + } + + // Fetch from AWS Secrets Manager + const secretValue = "fetched-secret-value"; // Placeholder for actual fetch logic + + this.cache.set(secretKey, { + value: secretValue, + expiry: now + DEFAULT_CACHE_TTL_MS, + }); + + return secretValue; + } + + async getSecrets(): Promise {} + + async setSecret(): Promise { + throw new Error("Method not implemented."); + } + + async setSecrets(): Promise { + throw new Error("Method not implemented."); + } - async listSecrets(): Promise {} + static validateConfig(config: AWSSecretsManagerConfig): boolean { + return Boolean( + config.accessKeyId && config.secretAccessKey && config.region + ); + } } diff --git a/src/lib/secretsManager/secretsManager.ts b/src/lib/secretsManager/secretsManager.ts index 584c4e6..5a299e9 100644 --- a/src/lib/secretsManager/secretsManager.ts +++ b/src/lib/secretsManager/secretsManager.ts @@ -1,15 +1,9 @@ -import { SecretsCacheService } from "./cacheService"; import { SecretProviderConfig, SecretReference } from "./types"; import { EncryptedFsStorageService } from "./encryptedStorage/encryptedFsStorageService"; import { FileBasedProviderRegistry } from "./providerRegistry/FsProviderRegistry"; import { AbstractProviderRegistry } from "./providerRegistry/AbstractProviderRegistry"; // Questions -// 1. store multiple versions of the same secret or not?@ -// 2. use secretReference or do it like mp with union types? -// 3. cache invalidation strategy?@ -// 4. Need a new method for refreshing all the cached secrets -// --5. reuse the storage interface for encrypted storage and cache storage? // 6. // providerId in fetchByIdentifier ?? would be confusing and then I can name it key but without using "key" word. How can I make it generic? AI suggested using composite key. // Functions @@ -26,9 +20,6 @@ import { AbstractProviderRegistry } from "./providerRegistry/AbstractProviderReg // 4. Refresh Secrets - bulk fetch and update all secrets from their providers and update the cache // -// Merging secretManager and providerRegistry -// Methods like createProviderInstance need to be moved to the providerRegistry - const encryptedStorage = new EncryptedFsStorageService(""); const providerRegistry = new FileBasedProviderRegistry(encryptedStorage, ""); @@ -77,7 +68,7 @@ export class SecretsManager { } async fetchSecret(providerId: string, ref: SecretReference): Promise { - this.registry.getProvider(providerId)?.fetchSecret(ref); + this.registry.getProvider(providerId)?.getSecret(ref); } async refreshSecrets( @@ -96,21 +87,4 @@ export class SecretsManager { const secret = await this.fetchSecret(s.providerId, s.ref); } } - - async listSecrets(providerId: string): Promise { - const provider = this.registry.getProvider(providerId); - if (!provider) { - throw new Error(`Provider not found: ${providerId}`); - } - - return provider.listSecrets(); - } - - async invalidateCache(providerId: string): Promise { - const provider = this.registry.getProvider(providerId); - if (!provider) { - throw new Error(`Provider not found: ${providerId}`); - } - await provider.invalidateCache(); - } } diff --git a/src/lib/secretsManager/types.ts b/src/lib/secretsManager/types.ts index 6b0a23f..de87cae 100644 --- a/src/lib/secretsManager/types.ts +++ b/src/lib/secretsManager/types.ts @@ -20,37 +20,13 @@ export interface SecretProviderConfig { config: ProviderSpecificConfig; } -export interface SecretReference { - identifier: string; +export type AwsSecretReference = { + type: SecretProviderType.AWS_SECRETS_MANAGER; + nameOrArn: string; version?: string; - key?: string; -} - - - -/** - * - * type SecretReference = - * | { type: "aws"; nameOrArn: string; versionId?: string; versionStage?: string } - * | { type: "vault"; path: string; version?: number; key?: string }; - */ - -ProviderCache: { - [key: string]: { - value: string; - ttl: number; - }[] -} = { - "foo": { - value: "bar", - ttl: 1000, - } -} - - - - +}; +export type SecretReference = AwsSecretReference; // | VaultSecretReference; // | OtherProviderSecretReference; export interface CachedSecret { id: string; // Unique identifier