diff --git a/src/sync/config.ts b/src/sync/config.ts index b0d4af5..7ba2952 100644 --- a/src/sync/config.ts +++ b/src/sync/config.ts @@ -185,10 +185,34 @@ export function applyOverridesToRuntimeConfig( overrides: Record ): void { const merged = deepMerge(config, overrides) as Record; + const resolved = resolveEnvPlaceholders(merged); for (const key of Object.keys(config)) { delete config[key]; } - Object.assign(config, merged); + Object.assign(config, resolved); +} + +export function resolveEnvPlaceholders(config: unknown): unknown { + if (typeof config === 'string') { + return config.replace(/\{env:([^}]+)\}/g, (match, envVar) => { + const value = process.env[envVar]; + return value !== undefined ? value : match; + }); + } + + if (Array.isArray(config)) { + return config.map((item) => resolveEnvPlaceholders(item)); + } + + if (isPlainObject(config)) { + const result: Record = {}; + for (const [key, value] of Object.entries(config)) { + result[key] = resolveEnvPlaceholders(value); + } + return result; + } + + return config; } export function deepMerge(base: T, override: unknown): T { diff --git a/src/sync/resolve.test.ts b/src/sync/resolve.test.ts new file mode 100644 index 0000000..462a15e --- /dev/null +++ b/src/sync/resolve.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from 'vitest'; +import { applyOverridesToRuntimeConfig } from './config.js'; + +describe('applyOverridesToRuntimeConfig environment resolution', () => { + it('resolves {env:VAR} placeholders from process.env', () => { + process.env.TEST_VAR = 'secret-value'; + process.env.OTHER_VAR = 'other-value'; + + const config: Record = { + mcp: { + github: { + headers: { + Authorization: 'Bearer {env:TEST_VAR}', + }, + other: '{env:OTHER_VAR}', + }, + }, + unrelated: 'keep-me', + }; + + const overrides = { + mcp: { + github: { + headers: { + Authorization: 'Bearer {env:TEST_VAR}', + }, + }, + }, + }; + + // We apply overrides (which might already contain placeholders) + applyOverridesToRuntimeConfig(config, overrides); + + const mcp = config.mcp as Record>>; + expect(mcp.github.headers.Authorization).toBe('Bearer secret-value'); + expect(mcp.github.other).toBe('other-value'); + expect(config.unrelated).toBe('keep-me'); + + delete process.env.TEST_VAR; + delete process.env.OTHER_VAR; + }); + + it('handles missing environment variables by leaving placeholder intact', () => { + process.env.PRESENT_VAR = 'present'; + delete process.env.ABSENT_VAR; + + const config: Record = { + val: '{env:PRESENT_VAR}', + missing: '{env:ABSENT_VAR}', + }; + + applyOverridesToRuntimeConfig(config, {}); + + expect(config.val).toBe('present'); + // If it's missing, we keep it as is to avoid breaking things silently or passing empty strings + // that might be harder to debug. + expect(config.missing).toBe('{env:ABSENT_VAR}'); + + delete process.env.PRESENT_VAR; + }); +});