Skip to main content
Glama
secret-manager.ts4.7 kB
import { CryptoUtils } from '../utils/crypto.js' import { Logger } from '../utils/logger.js' export interface SecretManagerOptions { // Name of env var that holds the encryption key used for configuration secrets keyEnvVar?: string // Optional explicit key value (discouraged in production) key?: string } export class SecretManager { private key: string constructor(options?: SecretManagerOptions) { const env = (globalThis as any)?.process?.env ?? {} const provided = options?.key || env[options?.keyEnvVar || 'MASTER_CONFIG_KEY'] || env.MASTER_SECRET_KEY const isProd = (env.NODE_ENV || env.MASTER_ENV) === 'production' if (!provided) { if (isProd) throw new Error('Missing MASTER_CONFIG_KEY for decrypting secrets in production') Logger.warn('MASTER_CONFIG_KEY missing; using ephemeral key (dev only)') this.key = CryptoUtils.generateSecureRandom(32) } else { this.key = String(provided) } } getKey(): string { return this.key } encrypt(value: string): string { return `enc:gcm:${CryptoUtils.encrypt(value, this.key)}` } decrypt(value: string): string { if (value.startsWith('enc:gcm:')) { const raw = value.slice('enc:gcm:'.length) return CryptoUtils.decrypt(raw, this.key) } return value } isEncrypted(value: string): boolean { return typeof value === 'string' && value.indexOf('enc:gcm:') === 0 } // Resolve secret placeholders within a config object // - enc:gcm:<base64> → decrypted // - env:VARNAME → process.env[VARNAME] resolveSecrets<T>(obj: T): T { const env = (globalThis as any)?.process?.env ?? {} const visit = (v: any): any => { if (typeof v === 'string') { const vs: string = String(v) if (this.isEncrypted(vs)) return this.decrypt(vs) if (vs.slice(0, 4) === 'env:') return String(env[vs.slice(4)] ?? '') return v } if (Array.isArray(v)) return v.map((x) => visit(x)) if (v && typeof v === 'object') { const out: Record<string, unknown> = {} for (const [k, vv] of Object.entries(v)) out[k] = visit(vv) return out } return v } return visit(obj) } redact<T>(obj: T): T { const secretKeyMatcher = /secret|token|password|key/i const visit = (v: any, keyHint?: string): any => { if (typeof v === 'string') { const vs: string = String(v) if (this.isEncrypted(vs)) return '***' if (keyHint && secretKeyMatcher.test(keyHint)) return '***' if (vs.slice(0, 4) === 'env:' && secretKeyMatcher.test(keyHint || '')) return '***' return v } if (Array.isArray(v)) return v.map((x) => visit(x, keyHint)) if (v && typeof v === 'object') { const out: Record<string, unknown> = {} for (const [k, vv] of Object.entries(v)) out[k] = visit(vv, k) return out } return v } return visit(obj) } rotate<T extends Record<string, unknown>>(obj: T, newKey: string, secretPaths?: string[]): T { // Re-encrypt values under known secret paths const prevKey = this.key this.key = newKey const result = structuredClone(obj) const paths = secretPaths ?? inferSecretPaths(obj) for (const p of paths) { try { const cur = getByPath(result, p) if (typeof cur === 'string') { const plain = this.isEncrypted(cur) ? CryptoUtils.decrypt(cur.slice('enc:gcm:'.length), prevKey) : cur setByPath(result, p, this.encrypt(plain)) } } catch (err) { Logger.warn(`Failed to rotate secret at ${p}`, String(err)) } } return result } } function getByPath(obj: any, path: string): unknown { const parts = path.split('.') let cur = obj for (const p of parts) { if (!cur || typeof cur !== 'object') return undefined cur = cur[p] } return cur } function setByPath(obj: any, path: string, value: unknown): void { const parts = path.split('.') let cur = obj for (let i = 0; i < parts.length - 1; i++) { const p = parts[i] if (!cur[p] || typeof cur[p] !== 'object') cur[p] = {} cur = cur[p] } cur[parts[parts.length - 1]] = value } function inferSecretPaths(obj: Record<string, unknown>, base = ''): string[] { const out: string[] = [] for (const [k, v] of Object.entries(obj)) { const p = base ? `${base}.${k}` : k if (typeof v === 'string') { if (/secret|token|password|key/i.test(k)) out.push(p) else if (v.startsWith('enc:gcm:') || v.startsWith('env:')) out.push(p) } else if (v && typeof v === 'object') { out.push(...inferSecretPaths(v as Record<string, unknown>, p)) } } return out }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Jakedismo/master-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server