Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 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,12 @@
export abstract class AbstractEncryptedStorage {
abstract initialize(): Promise<void>;

abstract save<T extends Record<string, any>>(
key: string,
data: T
): Promise<void>;

abstract load<T extends Record<string, any>>(key: string): Promise<T>;

abstract delete(key: string): Promise<void>;
}
96 changes: 96 additions & 0 deletions src/lib/secretsManager/encryptedStorage/encryptedFsStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { safeStorage } from "electron";
import { AbstractEncryptedStorage } from "./AbstractEncryptedStorage";
import {
appendPath,
createFsResource,
} from "../../../renderer/actions/local-sync/common-utils";
import {
createFolder,
deleteFsResource,
getIfFolderExists,
parseFileRaw,
writeContentRaw,
} from "../../../renderer/actions/local-sync/fs-utils";

export class EncryptedFsStorage extends AbstractEncryptedStorage {
private readonly baseFolderPath: string;

constructor(baseFolderPath: string) {
super();
this.baseFolderPath = baseFolderPath;
}

async initialize(): Promise<void> {
if (!safeStorage.isEncryptionAvailable()) {
// Show trouble shooting steps to user
throw new Error("Encryption is not available on this system. ");
}

if (!this.baseFolderPath) {
throw new Error("Base folder path is not set for EncryptedFsStorage.");
}
}

async save<T extends Record<string, any>>(
key: string,
data: T
): Promise<void> {
const stringifiedData = JSON.stringify(data);
const encryptedData = safeStorage.encryptString(stringifiedData);

const fsFolderResource = createFsResource({
rootPath: this.baseFolderPath,
path: this.baseFolderPath,
type: "folder",
});

const providerFolderExists = await getIfFolderExists(fsFolderResource);

if (!providerFolderExists) {
await createFolder(fsFolderResource);
}

const fsResource = createFsResource({
rootPath: this.baseFolderPath,
path: appendPath(this.baseFolderPath, key),
type: "file",
});

try {
await writeContentRaw(fsResource, encryptedData.toString("base64"));
} catch (err) {
console.error("!!!debug", "Error writing encrypted data", err);
}
}

async load<T extends Record<string, any>>(key: string): Promise<T> {
const fsResource = createFsResource({
rootPath: this.baseFolderPath,
path: appendPath(this.baseFolderPath, key),
type: "file",
});
const fileContent = await parseFileRaw({
resource: fsResource,
});

if (fileContent.type === "error") {
throw new Error(
`Failed to load encrypted data for key: ${key}, error: ${fileContent.error.message}`
);
}

const encryptedBuffer = Buffer.from(fileContent.content, "base64");
const decryptedString = safeStorage.decryptString(encryptedBuffer);
return JSON.parse(decryptedString) as T;
}

async delete(key: string): Promise<void> {
const fsResource = createFsResource({
rootPath: this.baseFolderPath,
path: appendPath(this.baseFolderPath, key),
type: "file",
});

await deleteFsResource(fsResource);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { SecretProviderConfig, SecretProviderType } from "../types";
import { AbstractEncryptedStorage } from "../encryptedStorage/AbstractEncryptedStorage";
import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider";

type ProviderManifestItem = {
id: string;
type: SecretProviderType;
};

export type ProviderManifest = ProviderManifestItem[];

export abstract class AbstractProviderRegistry {
protected encryptedStorage: AbstractEncryptedStorage;

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

constructor(encryptedStorage: AbstractEncryptedStorage) {
this.encryptedStorage = encryptedStorage;
}

abstract initialize(): Promise<void>;

protected abstract loadManifest(): Promise<ProviderManifest>;

protected abstract saveManifest(manifest: ProviderManifest): 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;
}
223 changes: 223 additions & 0 deletions src/lib/secretsManager/providerRegistry/FileBasedProviderRegistry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import * as path from "path";
import { SecretProviderConfig } from "../types";
import { createProviderInstance } from "../providerService/providerFactory";
import {
AbstractProviderRegistry,
ProviderManifest,
} from "./AbstractProviderRegistry";
import { AbstractEncryptedStorage } from "../encryptedStorage/AbstractEncryptedStorage";
import { AbstractSecretProvider } from "../providerService/AbstractSecretProvider";
import { createFsResource } from "../../../renderer/actions/local-sync/common-utils";
import {
createGlobalConfigFolder,
getIfFileExists,
getIfFolderExists,
parseFile,
writeToGlobalConfig,
} from "../../../renderer/actions/local-sync/fs-utils";
import {
CORE_CONFIG_FILE_VERSION,
GLOBAL_CONFIG_FILE_NAME,
} from "../../../renderer/actions/local-sync/constants";
import { GlobalConfigRecordFileType } from "../../../renderer/actions/local-sync/file-types/file-types";
import { Static } from "@sinclair/typebox";
import { GlobalConfig } from "../../../renderer/actions/local-sync/schemas";

// TODO:@nafees check version of config.json
const MANIFEST_FILENAME = GLOBAL_CONFIG_FILE_NAME;
export class FileBasedProviderRegistry extends AbstractProviderRegistry {
private manifestPath: string;

private configDir: string;

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

constructor(encryptedStorage: AbstractEncryptedStorage, configDir: string) {
super(encryptedStorage);
this.configDir = configDir;
this.manifestPath = path.join(configDir, MANIFEST_FILENAME);
}

getProvider(providerId: string): AbstractSecretProvider | null {
return this.providers.get(providerId) || null;
}

async initialize(): Promise<void> {
await this.ensureConfigDir();
await this.ensureConfigFile();
await this.initProvidersFromManifest();
}

private async initProvidersFromManifest() {
const configs = await this.getAllProviderConfigs();
configs.forEach((config) => {
this.providers.set(config.id, createProviderInstance(config));
});
}

async getAllProviderConfigs(): Promise<SecretProviderConfig[]> {
const providerManifest = await this.loadManifest();

console.log("!!!debug", "manifest loaded", providerManifest);
const configs: SecretProviderConfig[] = [];

for (const entry of providerManifest) {
const config = await this.encryptedStorage.load<SecretProviderConfig>(
entry.id
);

if (config) {
configs.push(config);
} else {
// Should we throw error for this case?
console.log("!!!debug", "Config not found for entry", entry);
}
}

console.log("!!!debug", "all configs", configs);
return configs;
}

async setProviderConfig(config: SecretProviderConfig): Promise<void> {
const storageKey = config.id;

await this.encryptedStorage.save(storageKey, config);

const manifest = await this.loadManifest();
const existingEntryIndex = manifest.findIndex((p) => p.id === config.id);
if (existingEntryIndex !== -1) {
manifest[existingEntryIndex] = { id: config.id, type: config.type };
} else {
manifest.push({ id: config.id, type: config.type });
}

await this.saveManifest(manifest);
this.providers.set(config.id, createProviderInstance(config));
}

async deleteProviderConfig(id: string): Promise<void> {
const providerManifest = await this.loadManifest();
const entry = providerManifest.find((p) => p.id === id);
if (!entry) return;

await this.encryptedStorage.delete(id);

providerManifest.splice(providerManifest.indexOf(entry), 1);

await this.saveManifest(providerManifest);
this.providers.delete(id);
}

async getProviderConfig(id: string): Promise<SecretProviderConfig | null> {
const providerManifest = await this.loadManifest();
const entry = providerManifest.find((p) => p.id === id);
if (!entry) return null;

return this.encryptedStorage.load<SecretProviderConfig>(id);
}

private async ensureConfigDir(): Promise<void> {
try {
const globalConfigFolderResource = createFsResource({
rootPath: this.configDir,
path: this.configDir,
type: "folder",
});
const globalConfigFolderExists = await getIfFolderExists(
globalConfigFolderResource
);

if (!globalConfigFolderExists) {
await createGlobalConfigFolder();
}
} catch (error) {
console.error("Failed to create config directory:", error);
throw new Error("Failed to create config directory.");
}
}

private async ensureConfigFile(): Promise<void> {
const globalConfigFileResource = createFsResource({
rootPath: this.configDir,
path: this.manifestPath,
type: "file",
});

const globalConfigFileExists = await getIfFileExists(
globalConfigFileResource
);

if (!globalConfigFileExists) {
const config: Static<typeof GlobalConfig> = {
version: CORE_CONFIG_FILE_VERSION,
workspaces: [],
providers: [],
};
const writeResult = await writeToGlobalConfig(config);

if (writeResult.type === "error") {
throw new Error("Failed to create manifest file.");
}
}
}

protected async loadManifest(): Promise<ProviderManifest> {
const globalConfigFileResource = createFsResource({
rootPath: this.configDir,
path: this.manifestPath,
type: "file",
});

const globalConfigFileExists = await getIfFileExists(
globalConfigFileResource
);

if (!globalConfigFileExists) {
return [];
}

const readResult = await parseFile({
resource: globalConfigFileResource,
fileType: new GlobalConfigRecordFileType(),
});

if (readResult.type === "error") {
throw new Error("Failed to parse manifest file.");
}

console.log("!!!debug", "readResult", readResult);
// TODO:@nafees handle versioning and schema in schema.ts
return (readResult.content.providers ?? []) as ProviderManifest;
}

protected async saveManifest(
providerManifest: ProviderManifest
): Promise<void> {
const globalConfigFileResource = createFsResource({
rootPath: this.configDir,
path: this.manifestPath,
type: "file",
});

const readResult = await parseFile({
resource: globalConfigFileResource,
fileType: new GlobalConfigRecordFileType(),
});

if (readResult.type === "error") {
throw new Error("Failed to parse manifest file.");
}

const updatedConfig: Static<typeof GlobalConfig> = {
version: readResult.content.version || CORE_CONFIG_FILE_VERSION,
workspaces: readResult.content.workspaces || [],
providers: providerManifest,
};

const writeResult = await writeToGlobalConfig(updatedConfig);

if (writeResult.type === "error") {
throw new Error("Failed to write manifest file.");
}
}
}
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, 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: any;

protected abstract getSecretIdentfier(ref: SecretReference): string;

abstract testConnection(): Promise<boolean>;

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

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

abstract setSecret(): Promise<void>;

abstract setSecrets(): Promise<void>;

static validateConfig(config: any): boolean {
throw new Error("Not implemented");
}
}
Loading