diff --git a/src/lib/secretsManager/encryptedStorage/AbstractSecretsManagerStorage.ts b/src/lib/secretsManager/encryptedStorage/AbstractSecretsManagerStorage.ts index fb601918..67ba72ba 100644 --- a/src/lib/secretsManager/encryptedStorage/AbstractSecretsManagerStorage.ts +++ b/src/lib/secretsManager/encryptedStorage/AbstractSecretsManagerStorage.ts @@ -1,5 +1,9 @@ import { SecretProviderConfig } from "../types"; +export type StorageChangeCallback = ( + data: Record +) => void; + export abstract class AbstractSecretsManagerStorage { abstract set(_key: string, _data: SecretProviderConfig): Promise; @@ -8,4 +12,6 @@ export abstract class AbstractSecretsManagerStorage { abstract getAll(): Promise; abstract delete(_key: string): Promise; + + abstract onStorageChange(callback: StorageChangeCallback): () => void; } diff --git a/src/lib/secretsManager/encryptedStorage/SecretsManagerEncryptedStorage.ts b/src/lib/secretsManager/encryptedStorage/SecretsManagerEncryptedStorage.ts index 1e134633..d76e1db2 100644 --- a/src/lib/secretsManager/encryptedStorage/SecretsManagerEncryptedStorage.ts +++ b/src/lib/secretsManager/encryptedStorage/SecretsManagerEncryptedStorage.ts @@ -1,4 +1,7 @@ -import { AbstractSecretsManagerStorage } from "./AbstractSecretsManagerStorage"; +import { + AbstractSecretsManagerStorage, + StorageChangeCallback, +} from "./AbstractSecretsManagerStorage"; import { EncryptedElectronStore } from "../../storage/EncryptedElectronStore"; import { SecretProviderConfig } from "../types"; @@ -26,4 +29,8 @@ export class SecretsManagerEncryptedStorage extends AbstractSecretsManagerStorag async delete(key: string): Promise { return this.encryptedStore.delete(key); } + + onStorageChange(callback: StorageChangeCallback): () => void { + return this.encryptedStore.onChange(callback); + } } diff --git a/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts index c493ef11..0d8bb10f 100644 --- a/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts +++ b/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts @@ -2,17 +2,15 @@ import { SecretProviderConfig, SecretProviderType } from "../types"; import { AbstractSecretsManagerStorage } from "../encryptedStorage/AbstractSecretsManagerStorage"; import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider"; -/** - * Abstract registry for managing secret providers. - * - * The registry stores provider configurations and maintains instances of providers. - * Providers are stored with type erasure but maintain their full type safety - * when accessed through type-aware methods. - */ +export type ProviderChangeCallback = ( + configs: Omit[] +) => void; + export abstract class AbstractProviderRegistry { protected store: AbstractSecretsManagerStorage; - protected providers: Map> = new Map(); + protected providers: Map> = + new Map(); constructor(store: AbstractSecretsManagerStorage) { this.store = store; @@ -28,7 +26,11 @@ export abstract class AbstractProviderRegistry { abstract deleteProviderConfig(_id: string): Promise; - abstract getProvider(_providerId: string): AbstractSecretProvider | null; + abstract onProvidersChange(callback: ProviderChangeCallback): () => void; + + abstract getProvider( + _providerId: string + ): AbstractSecretProvider | null; /** * Type-safe method to get a provider with a specific type. diff --git a/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts index 7c23159b..703582e3 100644 --- a/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts +++ b/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts @@ -1,11 +1,17 @@ import { SecretProviderConfig, SecretProviderType } from "../types"; import { createProviderInstance } from "../providerService/providerFactory"; import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider"; -import { AbstractProviderRegistry } from "./AbstractProviderRegistry"; +import { + AbstractProviderRegistry, + ProviderChangeCallback, +} from "./AbstractProviderRegistry"; export class FileBasedProviderRegistry extends AbstractProviderRegistry { + private changeCallbacks: Set = new Set(); + async initialize(): Promise { await this.initProvidersFromStorage(); + this.setupStorageListener(); } private async initProvidersFromStorage(): Promise { @@ -52,4 +58,58 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { getProvider(providerId: string): AbstractSecretProvider | null { return this.providers.get(providerId) ?? null; } + + onProvidersChange(callback: ProviderChangeCallback): () => void { + this.changeCallbacks.add(callback); + + return () => { + this.changeCallbacks.delete(callback); + }; + } + + private setupStorageListener(): void { + this.store.onStorageChange((data) => { + this.syncProvidersFromStorageData(data); + this.notifyChangeCallbacks(data); + }); + } + + private syncProvidersFromStorageData( + data: Record + ): void { + const newConfigIds = new Set(Object.keys(data)); + const existingProviderIds = new Set(this.providers.keys()); + + // Remove providers that no longer exist + for (const existingId of existingProviderIds) { + if (!newConfigIds.has(existingId)) { + this.providers.delete(existingId); + } + } + + for (const [id, config] of Object.entries(data)) { + try { + // recreate provider instance + this.providers.set(id, createProviderInstance(config)); + } catch (error) { + console.log( + "!!!debug", + `Failed to sync provider for config id: ${id}`, + error + ); + } + } + } + + private notifyChangeCallbacks( + data: Record + ): void { + this.changeCallbacks.forEach((callback) => { + const configsMetadata = Object.values(data).map((config) => { + const { config: _, ...metadata } = config; + return metadata; + }); + callback(configsMetadata); + }); + } } diff --git a/src/lib/secretsManager/providerService/AbstractSecretProvider.ts b/src/lib/secretsManager/providerService/AbstractSecretProvider.ts index ecc7b827..ae5149ff 100644 --- a/src/lib/secretsManager/providerService/AbstractSecretProvider.ts +++ b/src/lib/secretsManager/providerService/AbstractSecretProvider.ts @@ -30,7 +30,9 @@ export abstract class AbstractSecretProvider { abstract testConnection(): Promise; - abstract getSecret(_ref: ReferenceForProvider): Promise | null>; + abstract getSecret( + _ref: ReferenceForProvider + ): Promise | null>; abstract getSecrets( _refs: ReferenceForProvider[] @@ -42,13 +44,28 @@ export abstract class AbstractSecretProvider { ): Promise; abstract setSecrets( - _entries: Array<{ ref: ReferenceForProvider; value: string | Record }> + _entries: Array<{ + ref: ReferenceForProvider; + value: string | Record; + }> ): Promise; abstract removeSecret(_ref: ReferenceForProvider): Promise; abstract removeSecrets(_refs: ReferenceForProvider[]): Promise; + abstract refreshSecrets(): Promise<(ValueForProvider | null)[]>; + + static validateConfig(config: any): boolean { + // Base implementation rejects all configs as a fail-safe. + // Provider implementations must override with specific validation. + if (!config) { + return false; + } + + return false; + } + protected invalidateCache(): void { this.cache.clear(); } @@ -91,16 +108,4 @@ export abstract class AbstractSecretProvider { keysToDelete.forEach((key) => this.cache.delete(key)); } - - abstract refreshSecrets(): Promise<(ValueForProvider | null)[]>; - - static validateConfig(config: any): boolean { - // Base implementation rejects all configs as a fail-safe. - // Provider implementations must override with specific validation. - if (!config) { - return false; - } - - return false; - } } diff --git a/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts b/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts index f6d5af74..0087af99 100644 --- a/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts +++ b/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts @@ -146,24 +146,19 @@ export class AWSSecretsManagerProvider extends AbstractSecretProvider this.getSecret(ref))); } - async setSecret( - _ref: AwsSecretReference, - _value: string | Record - ): Promise { + async setSecret(): Promise { throw new Error("Method not implemented."); } - async setSecrets( - _entries: Array<{ ref: AwsSecretReference; value: string | Record }> - ): Promise { + async setSecrets(): Promise { throw new Error("Method not implemented."); } - async removeSecret(_ref: AwsSecretReference): Promise { + async removeSecret(): Promise { throw new Error("Method not implemented."); } - async removeSecrets(_refs: AwsSecretReference[]): Promise { + async removeSecrets(): Promise { throw new Error("Method not implemented."); } diff --git a/src/lib/secretsManager/secretsManager.ts b/src/lib/secretsManager/secretsManager.ts index 93706efc..e2f83ea7 100644 --- a/src/lib/secretsManager/secretsManager.ts +++ b/src/lib/secretsManager/secretsManager.ts @@ -138,6 +138,20 @@ export class SecretsManager { return provider.refreshSecrets(); } + + async listProviders(): Promise[]> { + const configs = await this.registry.getAllProviderConfigs(); + + const configMetadata: Omit[] = configs.map( + ({ config: _, ...rest }) => rest + ); + + return configMetadata; + } + + onProvidersChange(callback: ProviderChangeCallback): () => void { + return this.registry.onProvidersChange(callback); + } } /** diff --git a/src/lib/storage/EncryptedElectronStore.ts b/src/lib/storage/EncryptedElectronStore.ts index 2a323c67..5922c0be 100644 --- a/src/lib/storage/EncryptedElectronStore.ts +++ b/src/lib/storage/EncryptedElectronStore.ts @@ -89,7 +89,7 @@ export class EncryptedElectronStore { * @param callback - Function to call when data changes * @returns Unsubscribe function */ - onChange(callback: (_data: Record) => void): () => void { + onChange(callback: (_data: Record) => void): () => void { return this.store.onDidChange("data", (newValue) => { if (newValue) { callback(newValue);