Skip to content
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
757ca49
init files
nafees87n Dec 22, 2025
0f8cb7a
added external secrets manager interfaces
nafees87n Dec 22, 2025
7b4d869
fix: config type
nafees87n Dec 23, 2025
fa080d0
added method
nafees87n Dec 23, 2025
0caccc0
refactor
nafees87n Dec 24, 2025
a965650
refactor folder name
nafees87n Dec 25, 2025
e68e69f
Merge branch 'master' of github.com:requestly/requestly-desktop-app i…
nafees87n Dec 25, 2025
2f38597
fix: interface reusability
nafees87n Dec 27, 2025
bbefd3f
fix
nafees87n Dec 30, 2025
a808ba9
added caching service
nafees87n Jan 9, 2026
aa26c49
refactored providerRegistry
nafees87n Jan 12, 2026
13f5131
fix: types
nafees87n Jan 12, 2026
2bb53a6
fix
nafees87n Jan 13, 2026
36e1b45
poc code
nafees87n Jan 14, 2026
3b34f7d
added initilization flow
nafees87n Jan 14, 2026
bdb67fb
remove unused code
nafees87n Jan 15, 2026
b597f38
Merge branch 'master' of github.com:requestly/requestly-desktop-app i…
nafees87n Jan 15, 2026
6968919
fix
nafees87n Jan 16, 2026
1756a21
fix: encrypted storage init
nafees87n Jan 20, 2026
e603899
fix: error
nafees87n Jan 20, 2026
4375589
fix: type
nafees87n Jan 20, 2026
0468284
fix: schemas
nafees87n Jan 20, 2026
c0b5a93
fix: config read/write
nafees87n Jan 20, 2026
3faf236
fix: provider reading
nafees87n Jan 20, 2026
ae99164
added key sanitization
nafees87n Jan 20, 2026
d8b7462
fix: deletion result
nafees87n Jan 20, 2026
9ae079b
fix: orphaned indexes in manifest
nafees87n Jan 20, 2026
8de1756
use electron-store
nafees87n Jan 21, 2026
56c2b4b
fix: initialization
nafees87n Jan 21, 2026
0f93b01
fix: file name
nafees87n Jan 21, 2026
11f1ede
fix: encryption code
nafees87n Jan 21, 2026
8ff48a7
cleanup unncessary changes
nafees87n Jan 21, 2026
2a4ca6d
Merge branch 'master' of github.com:requestly/requestly-desktop-app i…
nafees87n Jan 21, 2026
04f496b
wrap in trycatch
nafees87n Jan 21, 2026
cbec03c
removed linting changes
nafees87n Jan 28, 2026
555f2f0
Merge branch 'master' of github.com:requestly/requestly-desktop-app i…
nafees87n Jan 28, 2026
63a10fc
fix: new line
nafees87n Jan 28, 2026
88fb8fb
[DB-21] added variable fetching flow and awsSecretsManagerProvider (#…
nafees87n Jan 29, 2026
e95012d
convert into generic types
wrongsahil Jan 30, 2026
caaf624
fix: types
nafees87n Jan 30, 2026
7bce339
readded vault types
nafees87n Jan 30, 2026
20c9454
Merge branch 'master' of github.com:requestly/requestly-desktop-app i…
nafees87n Feb 25, 2026
75cd56c
adds support for listener in secretsManager (#273)
nafees87n Mar 9, 2026
acf3121
[DB-29] added IPC methods for secretsManager (#277)
nafees87n Mar 9, 2026
312b1f6
Merge branch 'master' of github.com:requestly/requestly-desktop-app i…
nafees87n Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { SecretProviderConfig } from "../types";

export abstract class AbstractSecretsManagerStorage {
abstract set(_key: string, _data: SecretProviderConfig): Promise<void>;

abstract get(_key: string): Promise<SecretProviderConfig | null>;

abstract getAll(): Promise<SecretProviderConfig[]>;

abstract delete(_key: string): Promise<void>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { AbstractSecretsManagerStorage } from "./AbstractSecretsManagerStorage";
import { EncryptedElectronStore } from "../../storage/EncryptedElectronStore";
import { SecretProviderConfig } from "../types";

export class SecretsManagerEncryptedStorage extends AbstractSecretsManagerStorage {
private encryptedStore: EncryptedElectronStore;

constructor(storeName: string) {
super();
this.encryptedStore = new EncryptedElectronStore(storeName);
}

async set(key: string, data: SecretProviderConfig): Promise<void> {
return this.encryptedStore.set<SecretProviderConfig>(key, data);
}

async get(key: string): Promise<SecretProviderConfig | null> {
return this.encryptedStore.get<SecretProviderConfig>(key);
}

async getAll(): Promise<SecretProviderConfig[]> {
const allData = this.encryptedStore.getAll<SecretProviderConfig>();
return Object.values(allData);
}

async delete(key: string): Promise<void> {
return this.encryptedStore.delete(key);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { SecretProviderConfig } from "../types";
import { AbstractSecretsManagerStorage } from "../encryptedStorage/AbstractSecretsManagerStorage";
import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider";

export abstract class AbstractProviderRegistry {
protected store: AbstractSecretsManagerStorage;

protected providers: Map<string, AbstractSecretProvider> = new Map();

constructor(store: AbstractSecretsManagerStorage) {
this.store = store;
}

abstract initialize(): Promise<void>;

abstract getAllProviderConfigs(): Promise<SecretProviderConfig[]>;

abstract getProviderConfig(_id: string): Promise<SecretProviderConfig | null>;

abstract setProviderConfig(_config: SecretProviderConfig): Promise<void>;

abstract deleteProviderConfig(_id: string): Promise<void>;

abstract getProvider(_providerId: string): AbstractSecretProvider | null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { SecretProviderConfig } from "../types";
import { createProviderInstance } from "../providerService/providerFactory";
import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider";
import { AbstractProviderRegistry } from "./AbstractProviderRegistry";

export class FileBasedProviderRegistry extends AbstractProviderRegistry {
async initialize(): Promise<void> {
await this.initProvidersFromStorage();
}
Comment thread
nafees87n marked this conversation as resolved.

private async initProvidersFromStorage(): Promise<void> {
const configs = await this.getAllProviderConfigs();
configs.forEach((config) => {
this.providers.set(config.id, createProviderInstance(config));
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

async getAllProviderConfigs(): Promise<SecretProviderConfig[]> {
const allConfigs = this.store.getAll();
return allConfigs;
}

async getProviderConfig(id: string): Promise<SecretProviderConfig | null> {
try {
return await this.store.get(id);
} catch (error) {
console.error(`Failed to load provider config for id: ${id}`, error);
return null;
}
}

async setProviderConfig(config: SecretProviderConfig): Promise<void> {
await this.store.set(config.id, config);
this.providers.set(config.id, createProviderInstance(config));
}

async deleteProviderConfig(id: string): Promise<void> {
await this.store.delete(id);
this.providers.delete(id);
}

getProvider(providerId: string): AbstractSecretProvider | null {
return this.providers.get(providerId) ?? null;
}
}
27 changes: 27 additions & 0 deletions src/lib/secretsManager/providerService/AbstractSecretProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { CachedSecret, ProviderSpecificConfig, SecretProviderType, SecretReference } from "../types";

export abstract class AbstractSecretProvider {
protected cache: Map<string, CachedSecret> = new Map();

abstract readonly type: SecretProviderType;

abstract readonly id: string;

protected config: ProviderSpecificConfig;

protected abstract getSecretIdentfier(ref: SecretReference): string;
Comment thread
nafees87n marked this conversation as resolved.
Outdated

abstract testConnection(): Promise<boolean>;

abstract getSecret(ref: SecretReference): Promise<string>;

abstract getSecrets(): Promise<string[]>;

abstract setSecret(): Promise<void>;

abstract setSecrets(): Promise<void>;
Comment thread
nafees87n marked this conversation as resolved.
Outdated

static validateConfig(config: any): boolean {
throw new Error("Not implemented");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/* eslint-disable class-methods-use-this */
import { AbstractSecretProvider } from "./AbstractSecretProvider";

export class AWSSecretsManagerProvider extends AbstractSecretProvider {}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
14 changes: 14 additions & 0 deletions src/lib/secretsManager/providerService/providerFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { SecretProviderConfig, SecretProviderType } from "../types";
import { AWSSecretsManagerProvider } from "./awsSecretManagerProvider";
import { AbstractSecretProvider } from "./AbstractSecretProvider";

export function createProviderInstance(
config: SecretProviderConfig
): AbstractSecretProvider {
switch (config.type) {
case SecretProviderType.AWS_SECRETS_MANAGER:
return new AWSSecretsManagerProvider(config);
default:
throw new Error(`Unknown provider type: ${config.type}`);
}
}
27 changes: 27 additions & 0 deletions src/lib/secretsManager/secretsManager.ts
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initialization of class' singleton instance has been done in PR: #266

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { SecretProviderConfig } from "./types";
import { AbstractProviderRegistry } from "./providerRegistry/AbstractProviderRegistry";

export class SecretsManager {
private registry: AbstractProviderRegistry;

constructor(registry: AbstractProviderRegistry) {
this.registry = registry;
}

async initialize(): Promise<void> {
await this.registry.initialize();
}

async addProviderConfig(config: SecretProviderConfig) {
console.log("!!!debug", "addconfig", config);
await this.registry.setProviderConfig(config);
}

async removeProviderConfig(id: string) {
await this.registry.deleteProviderConfig(id);
}

async getProviderConfig(id: string): Promise<SecretProviderConfig | null> {
return this.registry.getProviderConfig(id);
}
}
40 changes: 40 additions & 0 deletions src/lib/secretsManager/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
export enum SecretProviderType {
AWS_SECRETS_MANAGER = "aws",
}

export interface AWSSecretsManagerConfig {
accessKeyId: string;
secretAccessKey: string;
region: string;
sessionToken?: string;
}
Comment thread
nafees87n marked this conversation as resolved.
Outdated
Comment thread
nafees87n marked this conversation as resolved.
Outdated

export type ProviderSpecificConfig = AWSSecretsManagerConfig; // | HashicorpVaultConfig | OtherProviderConfig;

export interface SecretProviderConfig {
id: string;
type: SecretProviderType;
name: string;
createdAt: number;
updatedAt: number;
config: ProviderSpecificConfig;
}

export type AwsSecretReference = {
type: SecretProviderType.AWS_SECRETS_MANAGER;
nameOrArn: string;
version?: string;
};

export type SecretReference = AwsSecretReference; // | VaultSecretReference; // | OtherProviderSecretReference;

export interface CachedSecret {
id: string; // Unique identifier
identifier: string; // Secret identifier (name, ARN, or path)
value: string; // The actual secret value
providerId: string;
providerType: SecretProviderType;
fetchedAt: number;
expiresAt: number;
version?: string;
}
103 changes: 103 additions & 0 deletions src/lib/storage/EncryptedElectronStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import Store from "electron-store";
import { safeStorage } from "electron";

interface EncryptedStoreSchema {
version: number;
data: Record<string, any>;
}

const STORE_VERSION = 1;

/**
* Generic encrypted key-value storage using electron-store + Electron's safeStorage.
* - OS-level encryption via safeStorage (Keychain/DPAPI/libsecret)
*
* Storage location: <userData>/storage/<storeName>.txt
*
*/
export class EncryptedElectronStore {
private store: Store<EncryptedStoreSchema>;

constructor(storeName: string) {
if (!safeStorage.isEncryptionAvailable()) {
throw new Error(
"Encryption is not available on this system. Please ensure your operating system's secure storage is properly configured."
);
}
Comment thread
nafees87n marked this conversation as resolved.

const storeOptions: Store.Options<EncryptedStoreSchema> = {
name: storeName,
cwd: "storage",
watch: true,
fileExtension: "txt",
serialize: (data) => {
const jsonString = JSON.stringify(data);
const encrypted = safeStorage.encryptString(jsonString);
const base64 = encrypted.toString("base64");
return base64;
},
deserialize: (data) => {
const encryptedBuffer = Buffer.from(data, "base64");
const decrypted = safeStorage.decryptString(encryptedBuffer);
return JSON.parse(decrypted) as EncryptedStoreSchema;
},
schema: {
version: { type: "number" },
data: { type: "object", additionalProperties: true },
},
defaults: {
version: STORE_VERSION,
data: {},
},
};

this.store = new Store<EncryptedStoreSchema>(storeOptions);
}

set<T>(key: string, data: T) {
this.store.set(`data.${key}`, data);
}

get<T>(key: string): T | null {
const data = this.store.get(`data.${key}`) as T;
return data ?? null;
}

getAll<T>(): T {
return this.store.get("data") as T;
}

delete(key: string): void {
this.store.delete(`data.${key}` as keyof EncryptedStoreSchema);
}

has(key: string): boolean {
return this.store.has(`data.${key}` as keyof EncryptedStoreSchema);
}

keys(): string[] {
return Object.keys(this.store.get("data"));
}

clear(): void {
this.store.set("data", {});
}

/**
* Registers a callback for when storage changes.
*
* @param callback - Function to call when data changes
* @returns Unsubscribe function
*/
onChange(callback: (_data: Record<string, string>) => void): () => void {
return this.store.onDidChange("data", (newValue) => {
if (newValue) {
callback(newValue);
}
});
}

getStore(): Store<EncryptedStoreSchema> {
return this.store;
}
}
30 changes: 30 additions & 0 deletions src/main/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import { createOrUpdateAxiosInstance } from "./actions/getProxiedAxios";
// and then build these utilites elsewhere
// eslint-disable-next-line import/no-cycle
import createTrayMenu from "./main";
import { SecretsManager } from "../lib/secretsManager/secretsManager";
import { FileBasedProviderRegistry } from "../lib/secretsManager/providerRegistry/FileBasedProviderRegistry";
import { SecretsManagerEncryptedStorage } from "lib/secretsManager/encryptedStorage/SecretsManagerEncryptedStorage";
Comment thread
nafees87n marked this conversation as resolved.
Outdated

const getFileCategory = (fileExtension) => {
switch (fileExtension) {
Expand Down Expand Up @@ -269,6 +272,33 @@ export const registerMainProcessEventsForWebAppWindow = (webAppWindow) => {
ipcMain.handle("helper-server-hit", () => {
webAppWindow?.send("helper-server-hit");
});

let secretsManager = null;

ipcMain.handle("init-secretsManager", () => {
const secretsStore = new SecretsManagerEncryptedStorage("providers");
const registry = new FileBasedProviderRegistry(secretsStore);
secretsManager = new SecretsManager(registry);

try {
secretsManager.initialize();
} catch (err) {
console.error("Error initializing Secrets Manager", err);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
});

ipcMain.handle("secretsManager:addProviderConfig", async (event, config) => {
await secretsManager.addProviderConfig(config);
});

ipcMain.handle("secretsManager:getProviderConfig", async (event, id) => {
const providerConfig = await secretsManager.getProviderConfig(id);
console.log("!!!debug", "getConfig", providerConfig);
});
Comment thread
nafees87n marked this conversation as resolved.
Outdated

ipcMain.handle("secretsManager:removeProviderConfig", async (event, id) => {
await secretsManager.removeProviderConfig(id);
});
};

export const registerMainProcessCommonEvents = () => {
Expand Down
Loading