From 95b60b670de4c53b53e8d4c42490afa34179b192 Mon Sep 17 00:00:00 2001 From: nafees87n Date: Thu, 22 Jan 2026 18:29:28 +0530 Subject: [PATCH 1/9] added ipc methods --- src/lib/secretsManager/index.ts | 49 +++++++++++++++++ src/lib/secretsManager/secretsManager.ts | 14 ----- src/main/events.js | 68 ++++++++++-------------- 3 files changed, 77 insertions(+), 54 deletions(-) create mode 100644 src/lib/secretsManager/index.ts diff --git a/src/lib/secretsManager/index.ts b/src/lib/secretsManager/index.ts new file mode 100644 index 00000000..98da7fa1 --- /dev/null +++ b/src/lib/secretsManager/index.ts @@ -0,0 +1,49 @@ +import { SecretsManagerEncryptedStorage } from "./encryptedStorage/SecretsManagerEncryptedStorage"; +import { FileBasedProviderRegistry } from "./providerRegistry/FileBasedProviderRegistry"; +import { SecretsManager } from "./secretsManager"; +import { SecretProviderConfig, SecretReference, SecretValue } from "./types"; + +const secretsManager = SecretsManager.getInstance(); +const PROVIDERS_DIRECTORY = "providers"; + +export const initSecretsManager = async () => { + const secretsStorage = new SecretsManagerEncryptedStorage( + PROVIDERS_DIRECTORY + ); + const registry = new FileBasedProviderRegistry(secretsStorage); + + await SecretsManager.initialize(registry); +}; + +export const setSecretProviderConfig = async (config: SecretProviderConfig) => { + return secretsManager.setProviderConfig(config); +}; + +export const removeSecretProviderConfig = async (providerId: string) => { + return secretsManager.removeProviderConfig(providerId); +}; + +export const getSecretProviderConfig = async ( + providerId: string +): Promise => { + return secretsManager.getProviderConfig(providerId); +}; + +export const testSecretProviderConnection = async ( + providerId: string +): Promise => { + return secretsManager.testProviderConnection(providerId); +}; + +export const getSecretValue = async ( + providerId: string, + ref: SecretReference +): Promise => { + return secretsManager.getSecret(providerId, ref); +}; + +export const refreshSecrets = async ( + providerId: string +): Promise<(SecretValue | null)[]> => { + return secretsManager.refreshSecrets(providerId); +}; diff --git a/src/lib/secretsManager/secretsManager.ts b/src/lib/secretsManager/secretsManager.ts index 93706efc..ce6b58f8 100644 --- a/src/lib/secretsManager/secretsManager.ts +++ b/src/lib/secretsManager/secretsManager.ts @@ -2,7 +2,6 @@ import { SecretProviderConfig, SecretReference, SecretValue } from "./types"; import { AbstractProviderRegistry } from "./providerRegistry/AbstractProviderRegistry"; export class SecretsManager { - // eslint-disable-next-line no-use-before-define private static instance: SecretsManager | null = null; private static initPromise: Promise | null = null; @@ -139,16 +138,3 @@ export class SecretsManager { return provider.refreshSecrets(); } } - -/** - * // At app startup (once): - * await SecretsManager.initialize(registry); - * - * // Everywhere else: - * import { getSecretsManager } from "./secretsManager"; - * const secretsManager = getSecretsManager(); - * await secretsManager.getSecret(...); - */ -export function getSecretsManager(): SecretsManager { - return SecretsManager.getInstance(); -} diff --git a/src/main/events.js b/src/main/events.js index 4b7ea513..f7458dbb 100644 --- a/src/main/events.js +++ b/src/main/events.js @@ -23,12 +23,15 @@ import { createOrUpdateAxiosInstance } from "./actions/getProxiedAxios"; // and then build these utilites elsewhere // eslint-disable-next-line import/no-cycle import createTrayMenu from "./main"; -import { SecretsManagerEncryptedStorage } from "../lib/secretsManager/encryptedStorage/SecretsManagerEncryptedStorage"; -import { FileBasedProviderRegistry } from "../lib/secretsManager/providerRegistry/FileBasedProviderRegistry"; import { - getSecretsManager, - SecretsManager, -} from "../lib/secretsManager/secretsManager"; + getSecretProviderConfig, + getSecretValue, + initSecretsManager, + refreshSecrets, + removeSecretProviderConfig, + setSecretProviderConfig, + testSecretProviderConnection, +} from "../lib/secretsManager"; const getFileCategory = (fileExtension) => { switch (fileExtension) { @@ -276,60 +279,45 @@ export const registerMainProcessEventsForWebAppWindow = (webAppWindow) => { webAppWindow?.send("helper-server-hit"); }); - let secretsManager = null; - - ipcMain.handle("init-secretsManager", async () => { - const secretsStorage = new SecretsManagerEncryptedStorage("providers"); - const registry = new FileBasedProviderRegistry(secretsStorage); - - await SecretsManager.initialize(registry); - secretsManager = getSecretsManager(); - return true; + ipcMain.handle("secretsManager:init", () => { + return initSecretsManager(); }); ipcMain.handle( - "secretsManager:addProviderConfig", - async (event, { config }) => { - await secretsManager.setProviderConfig(config); + "secretsManager:setSecretProviderConfig", + (event, { config }) => { + return setSecretProviderConfig(config); } ); - ipcMain.handle("secretsManager:getProviderConfig", async (event, { id }) => { - const providerConfig = await secretsManager.getProviderConfig(id); - console.log("!!!debug", "getConfig", providerConfig); - return providerConfig; + ipcMain.handle("secretsManager:getSecretProviderConfig", (event, { id }) => { + return getSecretProviderConfig(id); }); ipcMain.handle( - "secretsManager:removeProviderConfig", - async (event, { id }) => { - await secretsManager.removeProviderConfig(id); + "secretsManager:removeSecretProviderConfig", + (event, { id }) => { + return removeSecretProviderConfig(id); } ); ipcMain.handle( - "secretsManager:testConnection", - async (event, { providerId }) => { - const providerConnection = await secretsManager.testProviderConnection( - providerId - ); - - return providerConnection; + "secretsManager:testSecretProviderConnection", + (event, { id }) => { + return testSecretProviderConnection(id); } ); ipcMain.handle( - "secretsManager:resolveSecret", - async (event, { providerId, ref }) => { - console.log("!!!debug", "resolve", { - providerId, - ref, - }); - const secretValue = await secretsManager.getSecret(providerId, ref); - console.log("!!!debug", "resolveSecret value", secretValue); - return secretValue; + "secretsManager:getSecretValue", + (event, { providerId, secretReference }) => { + return getSecretValue(providerId, secretReference); } ); + + ipcMain.handle("secretsManager:refreshSecrets", (event, { providerId }) => { + return refreshSecrets(providerId); + }); }; export const registerMainProcessCommonEvents = () => { From 98627e5c0f19f1e13b138812b49008df7bd44d65 Mon Sep 17 00:00:00 2001 From: nafees87n Date: Thu, 22 Jan 2026 20:54:06 +0530 Subject: [PATCH 2/9] added listProviders --- src/lib/secretsManager/index.ts | 6 ++++++ src/lib/secretsManager/secretsManager.ts | 5 +++++ src/main/events.js | 5 +++++ 3 files changed, 16 insertions(+) diff --git a/src/lib/secretsManager/index.ts b/src/lib/secretsManager/index.ts index 98da7fa1..ca4ca158 100644 --- a/src/lib/secretsManager/index.ts +++ b/src/lib/secretsManager/index.ts @@ -47,3 +47,9 @@ export const refreshSecrets = async ( ): Promise<(SecretValue | null)[]> => { return secretsManager.refreshSecrets(providerId); }; + +export const listSecretProviders = async (): Promise< + Omit[] +> => { + return secretsManager.listProviders(); +}; diff --git a/src/lib/secretsManager/secretsManager.ts b/src/lib/secretsManager/secretsManager.ts index ce6b58f8..ebca7d56 100644 --- a/src/lib/secretsManager/secretsManager.ts +++ b/src/lib/secretsManager/secretsManager.ts @@ -137,4 +137,9 @@ export class SecretsManager { return provider.refreshSecrets(); } + + async listProviders(): Promise[]> { + const configs = await this.registry.getAllProviderConfigs(); + return configs.map(({ config: _, ...rest }) => rest); + } } diff --git a/src/main/events.js b/src/main/events.js index f7458dbb..9027ef34 100644 --- a/src/main/events.js +++ b/src/main/events.js @@ -27,6 +27,7 @@ import { getSecretProviderConfig, getSecretValue, initSecretsManager, + listSecretProviders, refreshSecrets, removeSecretProviderConfig, setSecretProviderConfig, @@ -318,6 +319,10 @@ export const registerMainProcessEventsForWebAppWindow = (webAppWindow) => { ipcMain.handle("secretsManager:refreshSecrets", (event, { providerId }) => { return refreshSecrets(providerId); }); + + ipcMain.handle("secretsManager:listSecretProviders", () => { + return listSecretProviders(); + }); }; export const registerMainProcessCommonEvents = () => { From 16a4a1469949ffc9b8eb4275ccfaaea8c9176ce9 Mon Sep 17 00:00:00 2001 From: nafees87n Date: Fri, 23 Jan 2026 13:47:24 +0530 Subject: [PATCH 3/9] fix: initialization --- src/lib/secretsManager/index.ts | 25 +++++++++++++++--------- src/lib/secretsManager/secretsManager.ts | 4 ++-- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/lib/secretsManager/index.ts b/src/lib/secretsManager/index.ts index ca4ca158..6b3a0e49 100644 --- a/src/lib/secretsManager/index.ts +++ b/src/lib/secretsManager/index.ts @@ -3,7 +3,13 @@ import { FileBasedProviderRegistry } from "./providerRegistry/FileBasedProviderR import { SecretsManager } from "./secretsManager"; import { SecretProviderConfig, SecretReference, SecretValue } from "./types"; -const secretsManager = SecretsManager.getInstance(); +const getSecretsManager = (): SecretsManager => { + if (!SecretsManager.isInitialized()) { + return null as any; + } + return SecretsManager.getInstance(); +}; + const PROVIDERS_DIRECTORY = "providers"; export const initSecretsManager = async () => { @@ -13,43 +19,44 @@ export const initSecretsManager = async () => { const registry = new FileBasedProviderRegistry(secretsStorage); await SecretsManager.initialize(registry); + console.log("!!!debug", "secretsManager initialized"); }; export const setSecretProviderConfig = async (config: SecretProviderConfig) => { - return secretsManager.setProviderConfig(config); + return getSecretsManager().setProviderConfig(config); }; export const removeSecretProviderConfig = async (providerId: string) => { - return secretsManager.removeProviderConfig(providerId); + return getSecretsManager().removeProviderConfig(providerId); }; export const getSecretProviderConfig = async ( providerId: string ): Promise => { - return secretsManager.getProviderConfig(providerId); + return getSecretsManager().getProviderConfig(providerId); }; export const testSecretProviderConnection = async ( providerId: string ): Promise => { - return secretsManager.testProviderConnection(providerId); + return getSecretsManager().testProviderConnection(providerId); }; export const getSecretValue = async ( providerId: string, ref: SecretReference ): Promise => { - return secretsManager.getSecret(providerId, ref); + return getSecretsManager().getSecret(providerId, ref); }; export const refreshSecrets = async ( providerId: string ): Promise<(SecretValue | null)[]> => { - return secretsManager.refreshSecrets(providerId); + return getSecretsManager().refreshSecrets(providerId); }; export const listSecretProviders = async (): Promise< - Omit[] + SecretProviderConfig[] > => { - return secretsManager.listProviders(); + return getSecretsManager().listProviders(); }; diff --git a/src/lib/secretsManager/secretsManager.ts b/src/lib/secretsManager/secretsManager.ts index ebca7d56..bf2fc0cd 100644 --- a/src/lib/secretsManager/secretsManager.ts +++ b/src/lib/secretsManager/secretsManager.ts @@ -138,8 +138,8 @@ export class SecretsManager { return provider.refreshSecrets(); } - async listProviders(): Promise[]> { + async listProviders(): Promise { const configs = await this.registry.getAllProviderConfigs(); - return configs.map(({ config: _, ...rest }) => rest); + return configs; } } From ecd380a051033ff115eef0f1dc872c4b2eb8f7d3 Mon Sep 17 00:00:00 2001 From: nafees87n Date: Tue, 27 Jan 2026 13:36:46 +0530 Subject: [PATCH 4/9] exposed getSecrets --- src/lib/secretsManager/index.ts | 6 ++++++ src/main/events.js | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/src/lib/secretsManager/index.ts b/src/lib/secretsManager/index.ts index 6b3a0e49..b294719d 100644 --- a/src/lib/secretsManager/index.ts +++ b/src/lib/secretsManager/index.ts @@ -49,6 +49,12 @@ export const getSecretValue = async ( return getSecretsManager().getSecret(providerId, ref); }; +export const getSecretValues = async ( + secrets: Array<{ providerId: string; ref: SecretReference }> +): Promise<(SecretValue | null)[]> => { + return getSecretsManager().getSecrets(secrets); +}; + export const refreshSecrets = async ( providerId: string ): Promise<(SecretValue | null)[]> => { diff --git a/src/main/events.js b/src/main/events.js index 9027ef34..27746df9 100644 --- a/src/main/events.js +++ b/src/main/events.js @@ -26,6 +26,7 @@ import createTrayMenu from "./main"; import { getSecretProviderConfig, getSecretValue, + getSecretValues, initSecretsManager, listSecretProviders, refreshSecrets, @@ -316,6 +317,10 @@ export const registerMainProcessEventsForWebAppWindow = (webAppWindow) => { } ); + ipcMain.handle("secretsManager:getSecretValues", (event, { secrets }) => { + return getSecretValues(secrets); + }); + ipcMain.handle("secretsManager:refreshSecrets", (event, { providerId }) => { return refreshSecrets(providerId); }); From 9898462c5d3efd55eff32c5a9d6d9d5011de6c6f Mon Sep 17 00:00:00 2001 From: nafees87n Date: Tue, 27 Jan 2026 17:17:41 +0530 Subject: [PATCH 5/9] fix: event name --- src/main/events.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/events.js b/src/main/events.js index 27746df9..7fdcfcc3 100644 --- a/src/main/events.js +++ b/src/main/events.js @@ -292,21 +292,21 @@ export const registerMainProcessEventsForWebAppWindow = (webAppWindow) => { } ); - ipcMain.handle("secretsManager:getSecretProviderConfig", (event, { id }) => { - return getSecretProviderConfig(id); + ipcMain.handle("secretsManager:getSecretProviderConfig", (event, { providerId }) => { + return getSecretProviderConfig(providerId); }); ipcMain.handle( "secretsManager:removeSecretProviderConfig", - (event, { id }) => { - return removeSecretProviderConfig(id); + (event, { providerId }) => { + return removeSecretProviderConfig(providerId); } ); ipcMain.handle( - "secretsManager:testSecretProviderConnection", - (event, { id }) => { - return testSecretProviderConnection(id); + "secretsManager:testProviderConnection", + (event, { providerId }) => { + return testSecretProviderConnection(providerId); } ); From 6c67bcccfb0e0e95228e15f93053c52c5e112f07 Mon Sep 17 00:00:00 2001 From: nafees87n Date: Tue, 27 Jan 2026 18:56:30 +0530 Subject: [PATCH 6/9] only return metadata for listing --- src/lib/secretsManager/index.ts | 2 +- src/lib/secretsManager/secretsManager.ts | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/lib/secretsManager/index.ts b/src/lib/secretsManager/index.ts index b294719d..4c68152c 100644 --- a/src/lib/secretsManager/index.ts +++ b/src/lib/secretsManager/index.ts @@ -62,7 +62,7 @@ export const refreshSecrets = async ( }; export const listSecretProviders = async (): Promise< - SecretProviderConfig[] + Omit[] > => { return getSecretsManager().listProviders(); }; diff --git a/src/lib/secretsManager/secretsManager.ts b/src/lib/secretsManager/secretsManager.ts index bf2fc0cd..d17e6d4e 100644 --- a/src/lib/secretsManager/secretsManager.ts +++ b/src/lib/secretsManager/secretsManager.ts @@ -138,8 +138,13 @@ export class SecretsManager { return provider.refreshSecrets(); } - async listProviders(): Promise { + async listProviders(): Promise[]> { const configs = await this.registry.getAllProviderConfigs(); - return configs; + + const configMetadata: Omit[] = configs.map( + ({ config: _, ...rest }) => rest + ); + + return configMetadata; } } From 8237627de754a6bdd8bb286d89cc9d1cda6bdb3d Mon Sep 17 00:00:00 2001 From: nafees87n Date: Tue, 27 Jan 2026 22:01:50 +0530 Subject: [PATCH 7/9] added providers onCHange listener --- .../AbstractSecretsManagerStorage.ts | 6 ++ .../SecretsManagerEncryptedStorage.ts | 14 +++- src/lib/secretsManager/index.ts | 7 ++ .../AbstractProviderRegistry.ts | 6 ++ .../FileBasedProviderRegistry.ts | 77 ++++++++++++++++++- src/lib/secretsManager/secretsManager.ts | 9 ++- src/lib/storage/EncryptedElectronStore.ts | 2 +- src/main/events.js | 19 ++++- 8 files changed, 132 insertions(+), 8 deletions(-) diff --git a/src/lib/secretsManager/encryptedStorage/AbstractSecretsManagerStorage.ts b/src/lib/secretsManager/encryptedStorage/AbstractSecretsManagerStorage.ts index fb601918..692ed492 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 onChange(_callback: StorageChangeCallback): () => void; } diff --git a/src/lib/secretsManager/encryptedStorage/SecretsManagerEncryptedStorage.ts b/src/lib/secretsManager/encryptedStorage/SecretsManagerEncryptedStorage.ts index 1e134633..ef840f08 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"; @@ -19,11 +22,18 @@ export class SecretsManagerEncryptedStorage extends AbstractSecretsManagerStorag } async getAll(): Promise { - const allData = this.encryptedStore.getAll(); + const allData = + this.encryptedStore.getAll>(); return Object.values(allData); } async delete(key: string): Promise { return this.encryptedStore.delete(key); } + + onChange(callback: StorageChangeCallback): () => void { + return this.encryptedStore.onChange((data) => { + callback(data); + }); + } } diff --git a/src/lib/secretsManager/index.ts b/src/lib/secretsManager/index.ts index 4c68152c..efd59f95 100644 --- a/src/lib/secretsManager/index.ts +++ b/src/lib/secretsManager/index.ts @@ -1,5 +1,6 @@ import { SecretsManagerEncryptedStorage } from "./encryptedStorage/SecretsManagerEncryptedStorage"; import { FileBasedProviderRegistry } from "./providerRegistry/FileBasedProviderRegistry"; +import { ProviderChangeCallback } from "./providerRegistry/AbstractProviderRegistry"; import { SecretsManager } from "./secretsManager"; import { SecretProviderConfig, SecretReference, SecretValue } from "./types"; @@ -22,6 +23,12 @@ export const initSecretsManager = async () => { console.log("!!!debug", "secretsManager initialized"); }; +export const subscribeToProvidersChange = ( + callback: ProviderChangeCallback +): (() => void) => { + return getSecretsManager().onProvidersChange(callback); +}; + export const setSecretProviderConfig = async (config: SecretProviderConfig) => { return getSecretsManager().setProviderConfig(config); }; diff --git a/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts index 3d7850df..63a0965e 100644 --- a/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts +++ b/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts @@ -2,6 +2,10 @@ import { SecretProviderConfig } from "../types"; import { AbstractSecretsManagerStorage } from "../encryptedStorage/AbstractSecretsManagerStorage"; import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider"; +export type ProviderChangeCallback = ( + configs: Omit[] +) => void; + export abstract class AbstractProviderRegistry { protected store: AbstractSecretsManagerStorage; @@ -22,4 +26,6 @@ export abstract class AbstractProviderRegistry { abstract deleteProviderConfig(_id: string): Promise; abstract getProvider(_providerId: string): AbstractSecretProvider | null; + + abstract onProvidersChange(_callback: ProviderChangeCallback): () => void; } diff --git a/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts index 31a41a6d..11634722 100644 --- a/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts +++ b/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts @@ -1,11 +1,19 @@ import { SecretProviderConfig } 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(); + + private unsubscribeFromStorage: (() => void) | null = null; + async initialize(): Promise { await this.initProvidersFromStorage(); + this.setupStorageListener(); } private async initProvidersFromStorage(): Promise { @@ -24,6 +32,53 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { }); } + private setupStorageListener(): void { + this.unsubscribeFromStorage = this.store.onChange((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 in storage + for (const existingId of existingProviderIds) { + if (!newConfigIds.has(existingId)) { + this.providers.delete(existingId); + } + } + + // Add or update providers from storage + for (const [id, config] of Object.entries(data)) { + try { + // Always recreate provider instance to ensure config is up-to-date + 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); + }); + } + async getAllProviderConfigs(): Promise { const allConfigs = this.store.getAll(); return allConfigs; @@ -52,4 +107,24 @@ 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); + }; + } + + /** + * Cleanup method to unsubscribe from storage changes. + * Should be called when the registry is being destroyed. + */ + destroy(): void { + if (this.unsubscribeFromStorage) { + this.unsubscribeFromStorage(); + this.unsubscribeFromStorage = null; + } + this.changeCallbacks.clear(); + } } diff --git a/src/lib/secretsManager/secretsManager.ts b/src/lib/secretsManager/secretsManager.ts index d17e6d4e..3fb36d9b 100644 --- a/src/lib/secretsManager/secretsManager.ts +++ b/src/lib/secretsManager/secretsManager.ts @@ -1,5 +1,8 @@ import { SecretProviderConfig, SecretReference, SecretValue } from "./types"; -import { AbstractProviderRegistry } from "./providerRegistry/AbstractProviderRegistry"; +import { + AbstractProviderRegistry, + ProviderChangeCallback, +} from "./providerRegistry/AbstractProviderRegistry"; export class SecretsManager { private static instance: SecretsManager | null = null; @@ -147,4 +150,8 @@ export class SecretsManager { 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); diff --git a/src/main/events.js b/src/main/events.js index 7fdcfcc3..d8c6395e 100644 --- a/src/main/events.js +++ b/src/main/events.js @@ -32,6 +32,7 @@ import { refreshSecrets, removeSecretProviderConfig, setSecretProviderConfig, + subscribeToProvidersChange, testSecretProviderConnection, } from "../lib/secretsManager"; @@ -285,6 +286,15 @@ export const registerMainProcessEventsForWebAppWindow = (webAppWindow) => { return initSecretsManager(); }); + ipcMain.handle("secretsManager:subscribeToProvidersChange", () => { + subscribeToProvidersChange((providers) => { + webAppWindow?.webContents.send( + "secretsManager:providersChanged", + providers + ); + }); + }); + ipcMain.handle( "secretsManager:setSecretProviderConfig", (event, { config }) => { @@ -292,9 +302,12 @@ export const registerMainProcessEventsForWebAppWindow = (webAppWindow) => { } ); - ipcMain.handle("secretsManager:getSecretProviderConfig", (event, { providerId }) => { - return getSecretProviderConfig(providerId); - }); + ipcMain.handle( + "secretsManager:getSecretProviderConfig", + (event, { providerId }) => { + return getSecretProviderConfig(providerId); + } + ); ipcMain.handle( "secretsManager:removeSecretProviderConfig", From 44bdcac7818c7af30b0fbd3ab585bcef3253e674 Mon Sep 17 00:00:00 2001 From: Nafees Nehar Date: Fri, 30 Jan 2026 13:02:14 +0530 Subject: [PATCH 8/9] [DB-28] error handling in secretsManager (#278) * handled secret manager errors * fix: return types * added initialization error handling * fix: test return * [DB-21] added variable fetching flow and awsSecretsManagerProvider (#266) * convert into generic types * fix: types * fix: working types --------- Co-authored-by: Sahil Gupta --- src/lib/secretsManager/USAGE_EXAMPLES.txt | 366 ++++++++++++++++++ src/lib/secretsManager/baseTypes.ts | 28 ++ src/lib/secretsManager/errors.ts | 54 +++ src/lib/secretsManager/index.ts | 71 +++- .../AbstractProviderRegistry.ts | 32 +- .../FileBasedProviderRegistry.ts | 38 +- .../providerService/AbstractSecretProvider.ts | 77 ++-- .../awsSecretManagerProvider.ts | 88 +++-- .../providerService/providerFactory.ts | 13 +- src/lib/secretsManager/secretsManager.ts | 204 +++++++--- src/lib/secretsManager/types.ts | 80 ++-- src/lib/storage/EncryptedElectronStore.ts | 4 +- 12 files changed, 844 insertions(+), 211 deletions(-) create mode 100644 src/lib/secretsManager/USAGE_EXAMPLES.txt create mode 100644 src/lib/secretsManager/baseTypes.ts create mode 100644 src/lib/secretsManager/errors.ts diff --git a/src/lib/secretsManager/USAGE_EXAMPLES.txt b/src/lib/secretsManager/USAGE_EXAMPLES.txt new file mode 100644 index 00000000..c672669b --- /dev/null +++ b/src/lib/secretsManager/USAGE_EXAMPLES.txt @@ -0,0 +1,366 @@ +/** + * Usage Examples for Type-Safe Secrets Manager + * + * This file demonstrates how TypeScript automatically infers types + * throughout the secrets manager system. + */ + +import { + SecretProviderType, + AWSSecretProviderConfig, + HashicorpVaultProviderConfig, + AwsSecretReference, + VaultSecretReference, +} from "./types"; +import { createProviderInstance } from "./providerService/providerFactory"; +import { AWSSecretsManagerProvider } from "./providerService/awsSecretManagerProvider"; +// import { HashicorpVaultProvider } from "./providerService/hashicorpVaultProvider"; + +// ============================================================================ +// Example 1: Creating Provider Configurations (Type-Safe) +// ============================================================================ + +// AWS Provider Config - TypeScript enforces correct config structure +const awsConfig: AWSSecretProviderConfig = { + id: "aws-prod", + type: SecretProviderType.AWS_SECRETS_MANAGER, + name: "AWS Production", + createdAt: Date.now(), + updatedAt: Date.now(), + credentials: { + accessKeyId: "AKIA...", + secretAccessKey: "...", + region: "us-east-1", + sessionToken: "...", // optional + }, +}; + +// HashiCorp Vault Config - TypeScript enforces correct config structure +const vaultConfig: HashicorpVaultProviderConfig = { + id: "vault-dev", + type: SecretProviderType.HASHICORP_VAULT, + name: "Vault Development", + createdAt: Date.now(), + updatedAt: Date.now(), + credentials: { + address: "https://vault.example.com", + token: "s.xyz...", + namespace: "admin", // optional + }, +}; + +// ❌ This will cause a TypeScript error - wrong config type for provider type +// const invalidConfig: AWSSecretProviderConfig = { +// id: "invalid", +// type: SecretProviderType.HASHICORP_VAULT, // ❌ Error: type mismatch +// ... +// }; + +// ============================================================================ +// Example 2: Creating Provider Instances (Type-Safe Factory) +// ============================================================================ + +async function example2() { + // Generic factory - returns AbstractSecretProvider + const awsProvider = createProviderInstance(awsConfig); + const vaultProvider = createProviderInstance(vaultConfig); + + // TypeScript knows the provider type from the instance + console.log(awsProvider.type); // SecretProviderType.AWS_SECRETS_MANAGER + console.log(vaultProvider.type); // SecretProviderType.HASHICORP_VAULT + + // Strongly-typed factory - returns specific provider type + const typedAwsProvider = createTypedProviderInstance(awsConfig); + // typedAwsProvider is AbstractSecretProvider + + const typedVaultProvider = createTypedProviderInstance(vaultConfig); + // typedVaultProvider is AbstractSecretProvider +} + +// ============================================================================ +// Example 3: Working with Secret References (Type-Safe) +// ============================================================================ + +async function example3() { + const awsProvider = new AWSSecretsManagerProvider(awsConfig); + const vaultProvider = new HashicorpVaultProvider(vaultConfig); + + // AWS Secret Reference - TypeScript enforces correct structure + const awsRef: AwsSecretReference = { + type: SecretProviderType.AWS_SECRETS_MANAGER, + identifier: "arn:aws:secretsmanager:us-east-1:123456789:secret:myapp/config", + version: "AWSCURRENT", // optional + }; + + // Vault Secret Reference - TypeScript enforces correct structure + const vaultRef: VaultSecretReference = { + type: SecretProviderType.HASHICORP_VAULT, + path: "secret/data/myapp/config", + version: 2, // optional - KV v2 version number + }; + + // TypeScript ensures you pass the correct reference type to each provider + const awsSecret = await awsProvider.getSecret(awsRef); // ✅ Correct + const vaultSecret = await vaultProvider.getSecret(vaultRef); // ✅ Correct + + // ❌ These would cause TypeScript errors: + // await awsProvider.getSecret(vaultRef); // ❌ Error: wrong reference type + // await vaultProvider.getSecret(awsRef); // ❌ Error: wrong reference type + + // TypeScript knows the exact return types + if (awsSecret) { + console.log(awsSecret.ARN); // ✅ ARN exists on AwsSecretValue + console.log(awsSecret.value); // ✅ value is string | undefined + // console.log(awsSecret.data); // ❌ Error: data doesn't exist on AwsSecretValue + } + + if (vaultSecret) { + console.log(vaultSecret.data); // ✅ data exists on VaultSecretValue + console.log(vaultSecret.metadata?.version); // ✅ metadata is optional + // console.log(vaultSecret.ARN); // ❌ Error: ARN doesn't exist on VaultSecretValue + } +} + +// ============================================================================ +// Example 4: Batch Operations (Type-Safe) +// ============================================================================ + +async function example4() { + const awsProvider = new AWSSecretsManagerProvider(awsConfig); + const vaultProvider = new HashicorpVaultProvider(vaultConfig); + + // Get multiple secrets - types are preserved + const awsRefs: AwsSecretReference[] = [ + { + type: SecretProviderType.AWS_SECRETS_MANAGER, + identifier: "secret-1", + }, + { + type: SecretProviderType.AWS_SECRETS_MANAGER, + identifier: "secret-2", + }, + ]; + + const vaultRefs: VaultSecretReference[] = [ + { + type: SecretProviderType.HASHICORP_VAULT, + path: "secret/data/app1", + }, + { + type: SecretProviderType.HASHICORP_VAULT, + path: "secret/data/app2", + }, + ]; + + const awsSecrets = await awsProvider.getSecrets(awsRefs); + // TypeScript knows: awsSecrets is (AwsSecretValue | null)[] + + const vaultSecrets = await vaultProvider.getSecrets(vaultRefs); + // TypeScript knows: vaultSecrets is (VaultSecretValue | null)[] + + // Type-safe iteration + awsSecrets.forEach((secret) => { + if (secret) { + console.log(secret.ARN); // ✅ ARN exists + console.log(secret.versionId); // ✅ versionId exists + } + }); + + vaultSecrets.forEach((secret) => { + if (secret) { + console.log(secret.path); // ✅ path exists + console.log(secret.data); // ✅ data exists + } + }); +} + +// ============================================================================ +// Example 5: Setting Secrets (Type-Safe) +// ============================================================================ + +async function example5() { + const awsProvider = new AWSSecretsManagerProvider(awsConfig); + const vaultProvider = new HashicorpVaultProvider(vaultConfig); + + const awsRef: AwsSecretReference = { + type: SecretProviderType.AWS_SECRETS_MANAGER, + identifier: "my-secret", + }; + + const vaultRef: VaultSecretReference = { + type: SecretProviderType.HASHICORP_VAULT, + path: "secret/data/myapp/config", + }; + + // Set a single secret - both string and object values are supported + await awsProvider.setSecret(awsRef, "my-secret-value"); + await vaultProvider.setSecret(vaultRef, { + database: "postgres://...", + apiKey: "xyz...", + }); + + // Batch set + await awsProvider.setSecrets([ + { ref: awsRef, value: "value1" }, + ]); + + await vaultProvider.setSecrets([ + { + ref: { type: SecretProviderType.HASHICORP_VAULT, path: "secret/data/app1" }, + value: { key1: "value1" }, + }, + { + ref: { type: SecretProviderType.HASHICORP_VAULT, path: "secret/data/app2" }, + value: { key2: "value2" }, + }, + ]); +} + +// ============================================================================ +// Example 6: Using the Registry (Type-Safe) +// ============================================================================ + +async function example6() { + // Assume we have a registry instance + const registry: any = null; // FileBasedProviderRegistry instance + + // Get a provider without knowing its type + const provider = registry.getProvider("aws-prod"); + if (provider) { + // provider is AbstractSecretProvider + console.log(provider.type); + } + + // Get a provider with a specific type (type-safe) + const awsProvider = registry.getTypedProvider( + "aws-prod", + SecretProviderType.AWS_SECRETS_MANAGER + ); + + if (awsProvider) { + // TypeScript knows: awsProvider is AbstractSecretProvider + const ref: AwsSecretReference = { + type: SecretProviderType.AWS_SECRETS_MANAGER, + identifier: "my-secret", + }; + + const secret = await awsProvider.getSecret(ref); + // TypeScript knows: secret is AwsSecretValue | null + + if (secret) { + console.log(secret.ARN); // ✅ Type-safe access to AWS-specific fields + } + } +} + +// ============================================================================ +// Example 7: Type Guards and Runtime Checks +// ============================================================================ + +async function example7() { + const registry: any = null; // FileBasedProviderRegistry instance + + // Get a provider and use type guards + const provider = registry.getProvider("some-provider-id"); + + if (provider) { + // Runtime check with type narrowing + if (provider.type === SecretProviderType.AWS_SECRETS_MANAGER) { + // TypeScript narrows the type here + const awsProvider = provider as AWSSecretsManagerProvider; + // Now you have full access to AWS-specific methods if any + } else if (provider.type === SecretProviderType.HASHICORP_VAULT) { + const vaultProvider = provider as HashicorpVaultProvider; + // Now you have full access to Vault-specific methods if any + } + } +} + +// ============================================================================ +// Example 8: Adding a New Provider (Easy Extension) +// ============================================================================ + +/** + * To add a new provider (e.g., Azure Key Vault): + * + * 1. Add the provider type to the enum in types.ts: + * ``` + * export enum SecretProviderType { + * AWS_SECRETS_MANAGER = "aws", + * HASHICORP_VAULT = "vault", + * AZURE_KEY_VAULT = "azure", // ← Add this + * } + * ``` + * + * 2. Add the config interface in types.ts: + * ``` + * export interface AzureKeyVaultConfig { + * vaultUrl: string; + * tenantId: string; + * clientId: string; + * clientSecret: string; + * } + * ``` + * + * 3. Add to the discriminated unions in types.ts: + * ``` + * export type AzureKeyVaultProviderConfig = ProviderConfig< + * SecretProviderType.AZURE_KEY_VAULT, + * AzureKeyVaultConfig + * >; + * + * export type SecretProviderConfig = + * | AWSSecretProviderConfig + * | HashicorpVaultProviderConfig + * | AzureKeyVaultProviderConfig; // ← Add this + * ``` + * + * 4. Add reference and value types in types.ts: + * ``` + * export interface AzureSecretReference extends BaseSecretReference<...> { + * name: string; + * version?: string; + * } + * + * export interface AzureSecretValue extends BaseSecretValue<...> { + * value: string; + * id: string; + * // ... other Azure-specific fields + * } + * ``` + * + * 5. Add to the type map in types.ts: + * ``` + * export interface ProviderTypeMap { + * [SecretProviderType.AWS_SECRETS_MANAGER]: { ... }; + * [SecretProviderType.HASHICORP_VAULT]: { ... }; + * [SecretProviderType.AZURE_KEY_VAULT]: { // ← Add this + * config: AzureKeyVaultConfig; + * providerConfig: AzureKeyVaultProviderConfig; + * reference: AzureSecretReference; + * value: AzureSecretValue; + * }; + * } + * ``` + * + * 6. Create the provider class (azureKeyVaultProvider.ts): + * ``` + * export class AzureKeyVaultProvider extends AbstractSecretProvider< + * SecretProviderType.AZURE_KEY_VAULT + * > { + * readonly type = SecretProviderType.AZURE_KEY_VAULT as const; + * // ... implement abstract methods + * } + * ``` + * + * 7. Add to the factory in providerFactory.ts: + * ``` + * case SecretProviderType.AZURE_KEY_VAULT: + * return new AzureKeyVaultProvider(config as AzureKeyVaultProviderConfig); + * ``` + * + * That's it! TypeScript will now enforce type safety for your new provider + * throughout the entire system. + */ + +export {}; diff --git a/src/lib/secretsManager/baseTypes.ts b/src/lib/secretsManager/baseTypes.ts new file mode 100644 index 00000000..8d7368b9 --- /dev/null +++ b/src/lib/secretsManager/baseTypes.ts @@ -0,0 +1,28 @@ +export enum SecretProviderType { + AWS_SECRETS_MANAGER = "aws", +} + +/** + * Generic provider configuration wrapper that adds metadata to credentials. + * + * @template T - The provider type + * @template C - The provider-specific credentials type + */ +export interface ProviderConfig { + id: string; + type: T; + name: string; + createdAt: number; + updatedAt: number; + credentials: C; +} + +/** + * Base secret reference interface. + * Provider-specific implementations extend this with additional fields. + * + * @template T - The provider type + */ +export interface SecretReference { + type: T; +} diff --git a/src/lib/secretsManager/errors.ts b/src/lib/secretsManager/errors.ts new file mode 100644 index 00000000..62dec904 --- /dev/null +++ b/src/lib/secretsManager/errors.ts @@ -0,0 +1,54 @@ +import { SecretReference } from "./types"; + +export enum SecretsErrorCode { + SAFE_STORAGE_ENCRYPTION_NOT_AVAILABLE = "safe_storage_encryption_not_available", + + PROVIDER_NOT_FOUND = "provider_not_found", + + AUTH_FAILED = "auth_failed", + PERMISSION_DENIED = "permission_denied", + + SECRET_NOT_FOUND = "secret_not_found", + SECRET_FETCH_FAILED = "secret_fetch_failed", + + STORAGE_READ_FAILED = "storage_read_failed", + STORAGE_WRITE_FAILED = "storage_write_failed", + + UNKNOWN = "unknown", +} + +export interface SecretsError { + code: SecretsErrorCode; + message: string; + providerId?: string; + secretRef?: SecretReference; + cause?: Error; // Original error +} + +export type SecretsManagerError = { + type: "error"; + error: SecretsError; +}; + +export type SecretsSuccess = T extends void + ? { type: "success" } + : { type: "success"; data: T }; + +export type SecretsResult = SecretsSuccess | SecretsManagerError; + +export type SecretsResultPromise = Promise>; + +export function createSecretsError( + code: SecretsErrorCode, + message: string, + context?: Omit +): SecretsManagerError { + return { + type: "error", + error: { + code, + message, + ...context, + }, + }; +} diff --git a/src/lib/secretsManager/index.ts b/src/lib/secretsManager/index.ts index efd59f95..fc17a466 100644 --- a/src/lib/secretsManager/index.ts +++ b/src/lib/secretsManager/index.ts @@ -2,7 +2,17 @@ import { SecretsManagerEncryptedStorage } from "./encryptedStorage/SecretsManage import { FileBasedProviderRegistry } from "./providerRegistry/FileBasedProviderRegistry"; import { ProviderChangeCallback } from "./providerRegistry/AbstractProviderRegistry"; import { SecretsManager } from "./secretsManager"; -import { SecretProviderConfig, SecretReference, SecretValue } from "./types"; +import { + SecretProviderConfig, + SecretProviderMetadata, + SecretReference, + SecretValue, +} from "./types"; +import { + createSecretsError, + SecretsErrorCode, + SecretsResultPromise, +} from "./errors"; const getSecretsManager = (): SecretsManager => { if (!SecretsManager.isInitialized()) { @@ -13,14 +23,37 @@ const getSecretsManager = (): SecretsManager => { const PROVIDERS_DIRECTORY = "providers"; -export const initSecretsManager = async () => { - const secretsStorage = new SecretsManagerEncryptedStorage( - PROVIDERS_DIRECTORY - ); - const registry = new FileBasedProviderRegistry(secretsStorage); +export const initSecretsManager = async (): SecretsResultPromise => { + try { + const secretsStorage = new SecretsManagerEncryptedStorage( + PROVIDERS_DIRECTORY + ); + const registry = new FileBasedProviderRegistry(secretsStorage); - await SecretsManager.initialize(registry); - console.log("!!!debug", "secretsManager initialized"); + await SecretsManager.initialize(registry); + + return { + type: "success", + }; + } catch (error) { + if ((error as Error).name === "SafeStorageEncryptionNotAvailable") { + return createSecretsError( + SecretsErrorCode.SAFE_STORAGE_ENCRYPTION_NOT_AVAILABLE, + "Safe storage encryption is not available.", // UI to show OS specific message here + { + cause: error as Error, + } + ); + } + + return createSecretsError( + SecretsErrorCode.UNKNOWN, + "Failed to initialize SecretsManager.", + { + cause: error as Error, + } + ); + } }; export const subscribeToProvidersChange = ( @@ -29,47 +62,51 @@ export const subscribeToProvidersChange = ( return getSecretsManager().onProvidersChange(callback); }; -export const setSecretProviderConfig = async (config: SecretProviderConfig) => { +export const setSecretProviderConfig = async ( + config: SecretProviderConfig +): SecretsResultPromise => { return getSecretsManager().setProviderConfig(config); }; -export const removeSecretProviderConfig = async (providerId: string) => { +export const removeSecretProviderConfig = async ( + providerId: string +): SecretsResultPromise => { return getSecretsManager().removeProviderConfig(providerId); }; export const getSecretProviderConfig = async ( providerId: string -): Promise => { +): SecretsResultPromise => { return getSecretsManager().getProviderConfig(providerId); }; export const testSecretProviderConnection = async ( providerId: string -): Promise => { +): SecretsResultPromise => { return getSecretsManager().testProviderConnection(providerId); }; export const getSecretValue = async ( providerId: string, ref: SecretReference -): Promise => { +): SecretsResultPromise => { return getSecretsManager().getSecret(providerId, ref); }; export const getSecretValues = async ( secrets: Array<{ providerId: string; ref: SecretReference }> -): Promise<(SecretValue | null)[]> => { +): SecretsResultPromise => { return getSecretsManager().getSecrets(secrets); }; export const refreshSecrets = async ( providerId: string -): Promise<(SecretValue | null)[]> => { +): SecretsResultPromise<(SecretValue | null)[]> => { return getSecretsManager().refreshSecrets(providerId); }; -export const listSecretProviders = async (): Promise< - Omit[] +export const listSecretProviders = async (): SecretsResultPromise< + SecretProviderMetadata[] > => { return getSecretsManager().listProviders(); }; diff --git a/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts index 965843c1..8525d7b5 100644 --- a/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts +++ b/src/lib/secretsManager/providerRegistry/AbstractProviderRegistry.ts @@ -1,15 +1,20 @@ -import { SecretProviderConfig } from "../types"; +import { + SecretProviderConfig, + SecretProviderMetadata, + SecretProviderType, +} from "../types"; import { AbstractSecretsManagerStorage } from "../encryptedStorage/AbstractSecretsManagerStorage"; import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider"; export type ProviderChangeCallback = ( - configs: Omit[] + configs: SecretProviderMetadata[] ) => void; export abstract class AbstractProviderRegistry { protected store: AbstractSecretsManagerStorage; - protected providers: Map = new Map(); + protected providers: Map> = + new Map(); constructor(store: AbstractSecretsManagerStorage) { this.store = store; @@ -25,7 +30,24 @@ 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. + * Returns the provider cast to the correct generic type. + */ + getTypedProvider( + providerId: string, + expectedType: T + ): AbstractSecretProvider | null { + const provider = this.getProvider(providerId); + if (provider && provider.type === expectedType) { + return provider as AbstractSecretProvider; + } + return null; + } } diff --git a/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts b/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts index a464e9fa..d330bff5 100644 --- a/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts +++ b/src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts @@ -1,4 +1,4 @@ -import { SecretProviderConfig } from "../types"; +import { SecretProviderConfig, SecretProviderType } from "../types"; import { createProviderInstance } from "../providerService/providerFactory"; import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider"; import { @@ -17,16 +17,7 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { private async initProvidersFromStorage(): Promise { const configs = await this.getAllProviderConfigs(); configs.forEach((config) => { - try { - this.providers.set(config.id, createProviderInstance(config)); - } catch (error) { - // TODO error to be propagated - console.log( - "!!!debug", - `Failed to initialize provider for config id: ${config.id}`, - error - ); - } + this.providers.set(config.id, createProviderInstance(config)); // TODO: check if this needs error handling }); } @@ -36,12 +27,7 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { } async getProviderConfig(id: string): Promise { - try { - return await this.store.get(id); - } catch (error) { - console.error(`Failed to load provider config for id: ${id}`, error); - return null; - } + return this.store.get(id); } async setProviderConfig(config: SecretProviderConfig): Promise { @@ -55,7 +41,9 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { this.providers.delete(id); } - getProvider(providerId: string): AbstractSecretProvider | null { + getProvider( + providerId: string + ): AbstractSecretProvider | null { return this.providers.get(providerId) ?? null; } @@ -88,16 +76,8 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { } 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 - ); - } + // recreate provider instance + this.providers.set(id, createProviderInstance(config)); } } @@ -106,7 +86,7 @@ export class FileBasedProviderRegistry extends AbstractProviderRegistry { ): void { this.changeCallbacks.forEach((callback) => { const configsMetadata = Object.values(data).map((config) => { - const { config: _, ...metadata } = config; + const { credentials: _, ...metadata } = config; return metadata; }); callback(configsMetadata); diff --git a/src/lib/secretsManager/providerService/AbstractSecretProvider.ts b/src/lib/secretsManager/providerService/AbstractSecretProvider.ts index 6ee59c58..ae5149ff 100644 --- a/src/lib/secretsManager/providerService/AbstractSecretProvider.ts +++ b/src/lib/secretsManager/providerService/AbstractSecretProvider.ts @@ -1,51 +1,76 @@ +import { SecretProviderType } from "../baseTypes"; import { - ProviderSpecificConfig, - SecretProviderType, - SecretReference, - SecretValue, + CredentialsForProvider, + ReferenceForProvider, + ValueForProvider, } from "../types"; const DEFAULT_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour const DEFAULT_MAX_CACHE_SIZE = 100; -export abstract class AbstractSecretProvider { - protected cache: Map = new Map(); +/** + * Generic abstract base class for secret providers. + * + * @template T - The provider type + */ +export abstract class AbstractSecretProvider { + protected cache: Map> = new Map(); - /** Cache TTL in milliseconds. Subclasses can override. */ protected cacheTtlMs: number = DEFAULT_CACHE_TTL_MS; - /** Maximum cache size (Size of the map). Subclasses can override. */ protected maxCacheSize: number = DEFAULT_MAX_CACHE_SIZE; - abstract readonly type: SecretProviderType; + abstract readonly type: T; abstract readonly id: string; - protected abstract config: ProviderSpecificConfig; + protected abstract config: CredentialsForProvider; - protected abstract getCacheKey(_ref: SecretReference): string; + protected abstract getCacheKey(_ref: ReferenceForProvider): string; abstract testConnection(): Promise; - abstract getSecret(_ref: SecretReference): Promise; + abstract getSecret( + _ref: ReferenceForProvider + ): Promise | null>; abstract getSecrets( - _refs: SecretReference[] - ): Promise<(SecretValue | null)[]>; + _refs: ReferenceForProvider[] + ): Promise<(ValueForProvider | null)[]>; - abstract setSecret(): Promise; + abstract setSecret( + _ref: ReferenceForProvider, + _value: string | Record + ): Promise; - abstract setSecrets(): Promise; + abstract setSecrets( + _entries: Array<{ + ref: ReferenceForProvider; + value: string | Record; + }> + ): Promise; - abstract removeSecret(): Promise; + abstract removeSecret(_ref: ReferenceForProvider): Promise; - abstract removeSecrets(): 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(); } - protected getCachedSecret(key: string): SecretValue | null { + protected getCachedSecret(key: string): ValueForProvider | null { const cached = this.cache.get(key); if (cached && cached.fetchedAt + this.cacheTtlMs > Date.now()) { return cached; @@ -53,7 +78,7 @@ export abstract class AbstractSecretProvider { return null; } - protected setCacheEntry(key: string, value: SecretValue): void { + protected setCacheEntry(key: string, value: ValueForProvider): void { if (this.maxCacheSize <= 0) { return; } @@ -83,16 +108,4 @@ export abstract class AbstractSecretProvider { keysToDelete.forEach((key) => this.cache.delete(key)); } - - abstract refreshSecrets(): Promise<(SecretValue | 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 32a2f9f2..55e3d651 100644 --- a/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts +++ b/src/lib/secretsManager/providerService/awsSecretManagerProvider.ts @@ -1,30 +1,53 @@ -import { - AwsSecretReference, - AWSSecretsManagerConfig, - AwsSecretValue, - SecretProviderConfig, - SecretProviderType, -} from "../types"; +import { SecretProviderType, ProviderConfig, SecretReference } from "../baseTypes"; import { AbstractSecretProvider } from "./AbstractSecretProvider"; import { GetSecretValueCommand, + GetSecretValueCommandOutput, ListSecretsCommand, SecretsManagerClient, } from "@aws-sdk/client-secrets-manager"; -export class AWSSecretsManagerProvider extends AbstractSecretProvider { - readonly type = SecretProviderType.AWS_SECRETS_MANAGER; +export interface AWSSecretsManagerCredentials { + accessKeyId: string; + secretAccessKey: string; + region: string; + sessionToken?: string; +} + +export type AWSSecretProviderConfig = ProviderConfig< + SecretProviderType.AWS_SECRETS_MANAGER, + AWSSecretsManagerCredentials +>; + +export interface AwsSecretReference extends SecretReference { + identifier: string; + version?: string; +} + +export interface AwsSecretValue { + type: SecretProviderType.AWS_SECRETS_MANAGER; + providerId: string; + secretReference: AwsSecretReference; + fetchedAt: number; + name: GetSecretValueCommandOutput["Name"]; + value: GetSecretValueCommandOutput["SecretString"]; + ARN: GetSecretValueCommandOutput["ARN"]; + versionId: GetSecretValueCommandOutput["VersionId"]; +} + +export class AWSSecretsManagerProvider extends AbstractSecretProvider { + readonly type = SecretProviderType.AWS_SECRETS_MANAGER as const; readonly id: string; - protected config: AWSSecretsManagerConfig; + protected config: AWSSecretsManagerCredentials; private client: SecretsManagerClient; - constructor(providerConfig: SecretProviderConfig) { + constructor(providerConfig: AWSSecretProviderConfig) { super(); this.id = providerConfig.id; - this.config = providerConfig.config as AWSSecretsManagerConfig; + this.config = providerConfig.credentials; this.client = new SecretsManagerClient({ region: this.config.region, credentials: { @@ -44,24 +67,16 @@ export class AWSSecretsManagerProvider extends AbstractSecretProvider { return false; } - try { - const listSecretsCommand = new ListSecretsCommand({ MaxResults: 1 }); - const res = await this.client.send(listSecretsCommand); - console.log("!!!debug", "aws result", res); - - if (res.$metadata.httpStatusCode !== 200) { - return false; - } - - return true; - } catch (err) { - console.error( - "!!!debug", - "aws secrets manager test connection error", - err - ); + + const listSecretsCommand = new ListSecretsCommand({ MaxResults: 1 }); + const res = await this.client.send(listSecretsCommand); + console.log("!!!debug", "aws result", res); + + if (res.$metadata.httpStatusCode !== 200) { return false; } + + return true; } async getSecret(ref: AwsSecretReference): Promise { @@ -70,7 +85,9 @@ export class AWSSecretsManagerProvider extends AbstractSecretProvider { } const cacheKey = this.getCacheKey(ref); - const cachedSecret = this.getCachedSecret(cacheKey) as AwsSecretValue | null; + const cachedSecret = this.getCachedSecret( + cacheKey + ) as AwsSecretValue | null; if (cachedSecret) { console.log("!!!debug", "returning from cache", cachedSecret); @@ -85,16 +102,11 @@ export class AWSSecretsManagerProvider extends AbstractSecretProvider { const secretResponse = await this.client.send(getSecretCommand); if (secretResponse.$metadata.httpStatusCode !== 200) { - console.error("!!!debug", "Failed to fetch secret", secretResponse); - return null; - } - - if (!secretResponse.SecretString) { - console.error("!!!debug", "SecretString is empty", secretResponse); - return null; + throw new Error("Failed to fetch secret from AWS Secrets Manager."); } const awsSecret: AwsSecretValue = { + type: SecretProviderType.AWS_SECRETS_MANAGER, providerId: this.id, secretReference: ref, fetchedAt: Date.now(), @@ -104,8 +116,6 @@ export class AWSSecretsManagerProvider extends AbstractSecretProvider { versionId: secretResponse.VersionId, }; - console.log("!!!debug", "returning after fetching", awsSecret); - this.setCacheEntry(cacheKey, awsSecret); return awsSecret; @@ -148,7 +158,7 @@ export class AWSSecretsManagerProvider extends AbstractSecretProvider { return this.getSecrets(allSecretRefs); } - static validateConfig(config: AWSSecretsManagerConfig): boolean { + static validateConfig(config: AWSSecretsManagerCredentials): boolean { return Boolean( config.accessKeyId && config.secretAccessKey && config.region ); diff --git a/src/lib/secretsManager/providerService/providerFactory.ts b/src/lib/secretsManager/providerService/providerFactory.ts index 00e108ed..794c44fb 100644 --- a/src/lib/secretsManager/providerService/providerFactory.ts +++ b/src/lib/secretsManager/providerService/providerFactory.ts @@ -4,11 +4,16 @@ import { AbstractSecretProvider } from "./AbstractSecretProvider"; export function createProviderInstance( config: SecretProviderConfig -): AbstractSecretProvider { +): AbstractSecretProvider { switch (config.type) { - case SecretProviderType.AWS_SECRETS_MANAGER: + case SecretProviderType.AWS_SECRETS_MANAGER: { + // TypeScript knows config is AWSSecretProviderConfig here return new AWSSecretsManagerProvider(config); - default: - throw new Error(`Unknown provider type: ${config.type}`); + } + + default: { + // Exhaustiveness check - TypeScript will error if we miss a case + throw new Error(`Unknown provider type: ${(config as any).type}`); + } } } diff --git a/src/lib/secretsManager/secretsManager.ts b/src/lib/secretsManager/secretsManager.ts index 3fb36d9b..c866f3f9 100644 --- a/src/lib/secretsManager/secretsManager.ts +++ b/src/lib/secretsManager/secretsManager.ts @@ -1,10 +1,16 @@ -import { SecretProviderConfig, SecretReference, SecretValue } from "./types"; +import { SecretProviderConfig, SecretProviderMetadata, SecretReference, SecretValue } from "./types"; import { AbstractProviderRegistry, ProviderChangeCallback, } from "./providerRegistry/AbstractProviderRegistry"; +import { + SecretsResultPromise, + createSecretsError, + SecretsErrorCode, +} from "./errors"; export class SecretsManager { + // eslint-disable-next-line no-use-before-define private static instance: SecretsManager | null = null; private static initPromise: Promise | null = null; @@ -61,48 +67,112 @@ export class SecretsManager { this.initPromise = null; } - async setProviderConfig(config: SecretProviderConfig) { - console.log("!!!debug", "addconfig", config); - await this.registry.setProviderConfig(config); + async setProviderConfig( + config: SecretProviderConfig + ): SecretsResultPromise { + try { + await this.registry.setProviderConfig(config); + return { type: "success" }; + } catch (error) { + return createSecretsError( + SecretsErrorCode.STORAGE_WRITE_FAILED, + error instanceof Error ? error.message : String(error), + { + providerId: config.id, + cause: error as Error, + } + ); + } } - async removeProviderConfig(id: string) { - await this.registry.deleteProviderConfig(id); + async removeProviderConfig(id: string): SecretsResultPromise { + try { + await this.registry.deleteProviderConfig(id); + return { type: "success" }; + } catch (error) { + return createSecretsError( + SecretsErrorCode.STORAGE_WRITE_FAILED, + error instanceof Error ? error.message : String(error), + { + providerId: id, + cause: error as Error, + } + ); + } } - async getProviderConfig(id: string): Promise { - return this.registry.getProviderConfig(id); + async getProviderConfig( + id: string + ): SecretsResultPromise { + try { + const config = await this.registry.getProviderConfig(id); + return { type: "success", data: config }; + } catch (error) { + return createSecretsError( + SecretsErrorCode.STORAGE_READ_FAILED, + error instanceof Error ? error.message : String(error), + { + providerId: id, + cause: error as Error, + } + ); + } } - async testProviderConnection(id: string): Promise { - const provider = this.registry.getProvider(id); - - if (!provider) { - throw new Error(`Provider with id ${id} not found`); - } + async testProviderConnection(id: string): SecretsResultPromise { + try { + const provider = this.registry.getProvider(id); - const isConnected = await provider.testConnection(); + if (!provider) { + return createSecretsError( + SecretsErrorCode.PROVIDER_NOT_FOUND, + `Provider with id ${id} not found`, + { providerId: id } + ); + } - return isConnected ?? false; + const isConnected = await provider.testConnection(); + return { type: "success", data: isConnected ?? false }; + } catch (error) { + return createSecretsError( + SecretsErrorCode.AUTH_FAILED, + error instanceof Error + ? error.message + : `Failed to test connection for provider ${id}`, + { providerId: id, cause: error as Error } + ); + } } async getSecret( providerId: string, ref: SecretReference - ): Promise { - const provider = this.registry.getProvider(providerId); - if (!provider) { - throw new Error(`Provider with id ${providerId} not found`); + ): SecretsResultPromise { + try { + const provider = this.registry.getProvider(providerId); + if (!provider) { + return createSecretsError( + SecretsErrorCode.PROVIDER_NOT_FOUND, + `Provider with id ${providerId} not found`, + { providerId } + ); + } + const secretValue = await provider.getSecret(ref); + return { type: "success", data: secretValue }; + } catch (error) { + return createSecretsError( + SecretsErrorCode.SECRET_FETCH_FAILED, + error instanceof Error + ? error.message + : `Failed to fetch secret from provider`, + { providerId, secretRef: ref, cause: error as Error } + ); } - - const secretValue = await provider.getSecret(ref); - - return secretValue; } async getSecrets( secrets: Array<{ providerId: string; ref: SecretReference }> - ): Promise { + ): SecretsResultPromise { const providerMap: Map = new Map(); for (const s of secrets) { @@ -114,41 +184,79 @@ export class SecretsManager { const results: SecretValue[] = []; + // Handle partial failures appropriately for (const [providerId, refs] of providerMap.entries()) { + try { + const provider = this.registry.getProvider(providerId); + if (!provider) { + return createSecretsError( + SecretsErrorCode.PROVIDER_NOT_FOUND, + `Provider with id ${providerId} not found`, + { providerId } + ); + } + + const secretValues = await provider.getSecrets(refs); + results.push( + ...secretValues.filter((sv): sv is SecretValue => sv !== null) + ); + } catch (error) { + return createSecretsError( + SecretsErrorCode.SECRET_FETCH_FAILED, + error instanceof Error + ? error.message + : `Failed to fetch secrets from provider ${providerId}`, + { providerId, cause: error as Error } + ); + } + } + + return { type: "success", data: results }; + } + + async refreshSecrets( + providerId: string + ): SecretsResultPromise<(SecretValue | null)[]> { + try { const provider = this.registry.getProvider(providerId); if (!provider) { - // TODO: Error to be handled properly - continue; + return createSecretsError( + SecretsErrorCode.PROVIDER_NOT_FOUND, + `Provider with id ${providerId} not found`, + { providerId } + ); } - const secretValues = await provider.getSecrets(refs); + const secrets = await provider.refreshSecrets(); - results.push( - ...secretValues.filter((sv): sv is SecretValue => sv !== null) + return { type: "success", data: secrets }; + } catch (error) { + return createSecretsError( + SecretsErrorCode.SECRET_FETCH_FAILED, + error instanceof Error + ? error.message + : `Failed to refresh secrets for provider ${providerId}`, + { providerId, cause: error as Error } ); } - - return results; } - async refreshSecrets(providerId: string): Promise<(SecretValue | null)[]> { - const provider = this.registry.getProvider(providerId); - if (!provider) { - throw new Error(`Provider with id ${providerId} not found`); + async listProviders(): SecretsResultPromise< + SecretProviderMetadata[] + > { + try { + const configs = await this.registry.getAllProviderConfigs(); + const configMetadata: SecretProviderMetadata[] = + configs.map(({ credentials: _, ...rest }) => rest); + return { type: "success", data: configMetadata }; + } catch (error) { + return createSecretsError( + SecretsErrorCode.STORAGE_READ_FAILED, + error instanceof Error ? error.message : "Failed to list providers", + { cause: error as Error } + ); } - - 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 { diff --git a/src/lib/secretsManager/types.ts b/src/lib/secretsManager/types.ts index ee012e86..bcf9dac5 100644 --- a/src/lib/secretsManager/types.ts +++ b/src/lib/secretsManager/types.ts @@ -1,46 +1,54 @@ -import { GetSecretValueCommandOutput } from "@aws-sdk/client-secrets-manager"; +import { SecretProviderType } from "./baseTypes"; +import type { + AWSSecretsManagerCredentials, + AWSSecretProviderConfig, + AwsSecretReference, + AwsSecretValue, +} from "./providerService/awsSecretManagerProvider"; -export enum SecretProviderType { - AWS_SECRETS_MANAGER = "aws", -} +export { + SecretProviderType, + ProviderConfig, + SecretReference as BaseSecretReference, +} from "./baseTypes"; -export interface AWSSecretsManagerConfig { - accessKeyId: string; - secretAccessKey: string; - region: string; - sessionToken?: string; -} +export type ProviderCredentials = AWSSecretsManagerCredentials; +// | HashicorpVaultCredentials; -export type ProviderSpecificConfig = AWSSecretsManagerConfig; // | HashicorpVaultConfig | OtherProviderConfig; +export type SecretProviderConfig = AWSSecretProviderConfig; +// | HashicorpVaultProviderConfig; -export interface SecretProviderConfig { - id: string; - type: SecretProviderType; - name: string; - createdAt: number; - updatedAt: number; - config: ProviderSpecificConfig; -} +export type SecretProviderMetadata = Omit; -export type AwsSecretReference = { - type: SecretProviderType.AWS_SECRETS_MANAGER; - identifier: string; // ARN or Name - version?: string; -}; +export type SecretReference = AwsSecretReference; // | VaultSecretReference; -export type SecretReference = AwsSecretReference; // | VaultSecretReference; // | OtherProviderSecretReference; +export type SecretValue = AwsSecretValue; // | VaultSecretValue; -interface BaseSecretValue { - providerId: string; - secretReference: SecretReference; - fetchedAt: number; -} +export type { + AWSSecretsManagerCredentials, + AWSSecretProviderConfig, + AwsSecretReference, + AwsSecretValue, +}; -export interface AwsSecretValue extends BaseSecretValue { - name: GetSecretValueCommandOutput["Name"]; - value: GetSecretValueCommandOutput["SecretString"]; - ARN: GetSecretValueCommandOutput["ARN"]; - versionId: GetSecretValueCommandOutput["VersionId"]; +/** + * Type map for compile-time type lookup. + * Enables AbstractSecretProvider to infer correct types automatically. + */ +export interface ProviderTypeMap { + [SecretProviderType.AWS_SECRETS_MANAGER]: { + credentials: AWSSecretsManagerCredentials; + providerConfig: AWSSecretProviderConfig; + reference: AwsSecretReference; + value: AwsSecretValue; + }; } -export type SecretValue = AwsSecretValue; // | VaultSecretValue; // | OtherProviderSecretValue; +export type CredentialsForProvider = + ProviderTypeMap[T]["credentials"]; +export type ProviderConfigForProvider = + ProviderTypeMap[T]["providerConfig"]; +export type ReferenceForProvider = + ProviderTypeMap[T]["reference"]; +export type ValueForProvider = + ProviderTypeMap[T]["value"]; diff --git a/src/lib/storage/EncryptedElectronStore.ts b/src/lib/storage/EncryptedElectronStore.ts index 5922c0be..2c6aff87 100644 --- a/src/lib/storage/EncryptedElectronStore.ts +++ b/src/lib/storage/EncryptedElectronStore.ts @@ -20,9 +20,11 @@ export class EncryptedElectronStore { constructor(storeName: string) { if (!safeStorage.isEncryptionAvailable()) { - throw new Error( + const error = new Error( "Encryption is not available on this system. Please ensure your operating system's secure storage is properly configured." ); + error.name = "SafeStorageEncryptionNotAvailable"; + throw error; } const storeOptions: Store.Options = { From 63ff56ac9cadb5eb7c7df98af834a0f823b2b8c3 Mon Sep 17 00:00:00 2001 From: nafees87n Date: Fri, 30 Jan 2026 13:28:37 +0530 Subject: [PATCH 9/9] remove examples.ts --- src/lib/secretsManager/USAGE_EXAMPLES.ts | 366 ----------------------- 1 file changed, 366 deletions(-) delete mode 100644 src/lib/secretsManager/USAGE_EXAMPLES.ts diff --git a/src/lib/secretsManager/USAGE_EXAMPLES.ts b/src/lib/secretsManager/USAGE_EXAMPLES.ts deleted file mode 100644 index c672669b..00000000 --- a/src/lib/secretsManager/USAGE_EXAMPLES.ts +++ /dev/null @@ -1,366 +0,0 @@ -/** - * Usage Examples for Type-Safe Secrets Manager - * - * This file demonstrates how TypeScript automatically infers types - * throughout the secrets manager system. - */ - -import { - SecretProviderType, - AWSSecretProviderConfig, - HashicorpVaultProviderConfig, - AwsSecretReference, - VaultSecretReference, -} from "./types"; -import { createProviderInstance } from "./providerService/providerFactory"; -import { AWSSecretsManagerProvider } from "./providerService/awsSecretManagerProvider"; -// import { HashicorpVaultProvider } from "./providerService/hashicorpVaultProvider"; - -// ============================================================================ -// Example 1: Creating Provider Configurations (Type-Safe) -// ============================================================================ - -// AWS Provider Config - TypeScript enforces correct config structure -const awsConfig: AWSSecretProviderConfig = { - id: "aws-prod", - type: SecretProviderType.AWS_SECRETS_MANAGER, - name: "AWS Production", - createdAt: Date.now(), - updatedAt: Date.now(), - credentials: { - accessKeyId: "AKIA...", - secretAccessKey: "...", - region: "us-east-1", - sessionToken: "...", // optional - }, -}; - -// HashiCorp Vault Config - TypeScript enforces correct config structure -const vaultConfig: HashicorpVaultProviderConfig = { - id: "vault-dev", - type: SecretProviderType.HASHICORP_VAULT, - name: "Vault Development", - createdAt: Date.now(), - updatedAt: Date.now(), - credentials: { - address: "https://vault.example.com", - token: "s.xyz...", - namespace: "admin", // optional - }, -}; - -// ❌ This will cause a TypeScript error - wrong config type for provider type -// const invalidConfig: AWSSecretProviderConfig = { -// id: "invalid", -// type: SecretProviderType.HASHICORP_VAULT, // ❌ Error: type mismatch -// ... -// }; - -// ============================================================================ -// Example 2: Creating Provider Instances (Type-Safe Factory) -// ============================================================================ - -async function example2() { - // Generic factory - returns AbstractSecretProvider - const awsProvider = createProviderInstance(awsConfig); - const vaultProvider = createProviderInstance(vaultConfig); - - // TypeScript knows the provider type from the instance - console.log(awsProvider.type); // SecretProviderType.AWS_SECRETS_MANAGER - console.log(vaultProvider.type); // SecretProviderType.HASHICORP_VAULT - - // Strongly-typed factory - returns specific provider type - const typedAwsProvider = createTypedProviderInstance(awsConfig); - // typedAwsProvider is AbstractSecretProvider - - const typedVaultProvider = createTypedProviderInstance(vaultConfig); - // typedVaultProvider is AbstractSecretProvider -} - -// ============================================================================ -// Example 3: Working with Secret References (Type-Safe) -// ============================================================================ - -async function example3() { - const awsProvider = new AWSSecretsManagerProvider(awsConfig); - const vaultProvider = new HashicorpVaultProvider(vaultConfig); - - // AWS Secret Reference - TypeScript enforces correct structure - const awsRef: AwsSecretReference = { - type: SecretProviderType.AWS_SECRETS_MANAGER, - identifier: "arn:aws:secretsmanager:us-east-1:123456789:secret:myapp/config", - version: "AWSCURRENT", // optional - }; - - // Vault Secret Reference - TypeScript enforces correct structure - const vaultRef: VaultSecretReference = { - type: SecretProviderType.HASHICORP_VAULT, - path: "secret/data/myapp/config", - version: 2, // optional - KV v2 version number - }; - - // TypeScript ensures you pass the correct reference type to each provider - const awsSecret = await awsProvider.getSecret(awsRef); // ✅ Correct - const vaultSecret = await vaultProvider.getSecret(vaultRef); // ✅ Correct - - // ❌ These would cause TypeScript errors: - // await awsProvider.getSecret(vaultRef); // ❌ Error: wrong reference type - // await vaultProvider.getSecret(awsRef); // ❌ Error: wrong reference type - - // TypeScript knows the exact return types - if (awsSecret) { - console.log(awsSecret.ARN); // ✅ ARN exists on AwsSecretValue - console.log(awsSecret.value); // ✅ value is string | undefined - // console.log(awsSecret.data); // ❌ Error: data doesn't exist on AwsSecretValue - } - - if (vaultSecret) { - console.log(vaultSecret.data); // ✅ data exists on VaultSecretValue - console.log(vaultSecret.metadata?.version); // ✅ metadata is optional - // console.log(vaultSecret.ARN); // ❌ Error: ARN doesn't exist on VaultSecretValue - } -} - -// ============================================================================ -// Example 4: Batch Operations (Type-Safe) -// ============================================================================ - -async function example4() { - const awsProvider = new AWSSecretsManagerProvider(awsConfig); - const vaultProvider = new HashicorpVaultProvider(vaultConfig); - - // Get multiple secrets - types are preserved - const awsRefs: AwsSecretReference[] = [ - { - type: SecretProviderType.AWS_SECRETS_MANAGER, - identifier: "secret-1", - }, - { - type: SecretProviderType.AWS_SECRETS_MANAGER, - identifier: "secret-2", - }, - ]; - - const vaultRefs: VaultSecretReference[] = [ - { - type: SecretProviderType.HASHICORP_VAULT, - path: "secret/data/app1", - }, - { - type: SecretProviderType.HASHICORP_VAULT, - path: "secret/data/app2", - }, - ]; - - const awsSecrets = await awsProvider.getSecrets(awsRefs); - // TypeScript knows: awsSecrets is (AwsSecretValue | null)[] - - const vaultSecrets = await vaultProvider.getSecrets(vaultRefs); - // TypeScript knows: vaultSecrets is (VaultSecretValue | null)[] - - // Type-safe iteration - awsSecrets.forEach((secret) => { - if (secret) { - console.log(secret.ARN); // ✅ ARN exists - console.log(secret.versionId); // ✅ versionId exists - } - }); - - vaultSecrets.forEach((secret) => { - if (secret) { - console.log(secret.path); // ✅ path exists - console.log(secret.data); // ✅ data exists - } - }); -} - -// ============================================================================ -// Example 5: Setting Secrets (Type-Safe) -// ============================================================================ - -async function example5() { - const awsProvider = new AWSSecretsManagerProvider(awsConfig); - const vaultProvider = new HashicorpVaultProvider(vaultConfig); - - const awsRef: AwsSecretReference = { - type: SecretProviderType.AWS_SECRETS_MANAGER, - identifier: "my-secret", - }; - - const vaultRef: VaultSecretReference = { - type: SecretProviderType.HASHICORP_VAULT, - path: "secret/data/myapp/config", - }; - - // Set a single secret - both string and object values are supported - await awsProvider.setSecret(awsRef, "my-secret-value"); - await vaultProvider.setSecret(vaultRef, { - database: "postgres://...", - apiKey: "xyz...", - }); - - // Batch set - await awsProvider.setSecrets([ - { ref: awsRef, value: "value1" }, - ]); - - await vaultProvider.setSecrets([ - { - ref: { type: SecretProviderType.HASHICORP_VAULT, path: "secret/data/app1" }, - value: { key1: "value1" }, - }, - { - ref: { type: SecretProviderType.HASHICORP_VAULT, path: "secret/data/app2" }, - value: { key2: "value2" }, - }, - ]); -} - -// ============================================================================ -// Example 6: Using the Registry (Type-Safe) -// ============================================================================ - -async function example6() { - // Assume we have a registry instance - const registry: any = null; // FileBasedProviderRegistry instance - - // Get a provider without knowing its type - const provider = registry.getProvider("aws-prod"); - if (provider) { - // provider is AbstractSecretProvider - console.log(provider.type); - } - - // Get a provider with a specific type (type-safe) - const awsProvider = registry.getTypedProvider( - "aws-prod", - SecretProviderType.AWS_SECRETS_MANAGER - ); - - if (awsProvider) { - // TypeScript knows: awsProvider is AbstractSecretProvider - const ref: AwsSecretReference = { - type: SecretProviderType.AWS_SECRETS_MANAGER, - identifier: "my-secret", - }; - - const secret = await awsProvider.getSecret(ref); - // TypeScript knows: secret is AwsSecretValue | null - - if (secret) { - console.log(secret.ARN); // ✅ Type-safe access to AWS-specific fields - } - } -} - -// ============================================================================ -// Example 7: Type Guards and Runtime Checks -// ============================================================================ - -async function example7() { - const registry: any = null; // FileBasedProviderRegistry instance - - // Get a provider and use type guards - const provider = registry.getProvider("some-provider-id"); - - if (provider) { - // Runtime check with type narrowing - if (provider.type === SecretProviderType.AWS_SECRETS_MANAGER) { - // TypeScript narrows the type here - const awsProvider = provider as AWSSecretsManagerProvider; - // Now you have full access to AWS-specific methods if any - } else if (provider.type === SecretProviderType.HASHICORP_VAULT) { - const vaultProvider = provider as HashicorpVaultProvider; - // Now you have full access to Vault-specific methods if any - } - } -} - -// ============================================================================ -// Example 8: Adding a New Provider (Easy Extension) -// ============================================================================ - -/** - * To add a new provider (e.g., Azure Key Vault): - * - * 1. Add the provider type to the enum in types.ts: - * ``` - * export enum SecretProviderType { - * AWS_SECRETS_MANAGER = "aws", - * HASHICORP_VAULT = "vault", - * AZURE_KEY_VAULT = "azure", // ← Add this - * } - * ``` - * - * 2. Add the config interface in types.ts: - * ``` - * export interface AzureKeyVaultConfig { - * vaultUrl: string; - * tenantId: string; - * clientId: string; - * clientSecret: string; - * } - * ``` - * - * 3. Add to the discriminated unions in types.ts: - * ``` - * export type AzureKeyVaultProviderConfig = ProviderConfig< - * SecretProviderType.AZURE_KEY_VAULT, - * AzureKeyVaultConfig - * >; - * - * export type SecretProviderConfig = - * | AWSSecretProviderConfig - * | HashicorpVaultProviderConfig - * | AzureKeyVaultProviderConfig; // ← Add this - * ``` - * - * 4. Add reference and value types in types.ts: - * ``` - * export interface AzureSecretReference extends BaseSecretReference<...> { - * name: string; - * version?: string; - * } - * - * export interface AzureSecretValue extends BaseSecretValue<...> { - * value: string; - * id: string; - * // ... other Azure-specific fields - * } - * ``` - * - * 5. Add to the type map in types.ts: - * ``` - * export interface ProviderTypeMap { - * [SecretProviderType.AWS_SECRETS_MANAGER]: { ... }; - * [SecretProviderType.HASHICORP_VAULT]: { ... }; - * [SecretProviderType.AZURE_KEY_VAULT]: { // ← Add this - * config: AzureKeyVaultConfig; - * providerConfig: AzureKeyVaultProviderConfig; - * reference: AzureSecretReference; - * value: AzureSecretValue; - * }; - * } - * ``` - * - * 6. Create the provider class (azureKeyVaultProvider.ts): - * ``` - * export class AzureKeyVaultProvider extends AbstractSecretProvider< - * SecretProviderType.AZURE_KEY_VAULT - * > { - * readonly type = SecretProviderType.AZURE_KEY_VAULT as const; - * // ... implement abstract methods - * } - * ``` - * - * 7. Add to the factory in providerFactory.ts: - * ``` - * case SecretProviderType.AZURE_KEY_VAULT: - * return new AzureKeyVaultProvider(config as AzureKeyVaultProviderConfig); - * ``` - * - * That's it! TypeScript will now enforce type safety for your new provider - * throughout the entire system. - */ - -export {};