fix(config): preserve hidden auth secrets and block scalars

This commit is contained in:
Tam Nhu Tran
2026-03-24 15:25:49 -04:00
parent e983bed1da
commit 6ccf53ec7d
2 changed files with 107 additions and 8 deletions
+33 -2
View File
@@ -201,7 +201,38 @@ function redactYamlScalarLine(line: string): string {
} }
function isYamlBlockScalarIndicator(value: string): boolean { function isYamlBlockScalarIndicator(value: string): boolean {
return /^[>|](?:[1-9][+-]?|[+-]?[1-9])?$/.test(value); const trimmedValue = value.trim();
if (!(trimmedValue.startsWith('|') || trimmedValue.startsWith('>'))) {
return false;
}
const indicators = trimmedValue.slice(1);
if (indicators.length === 0) {
return true;
}
if (indicators.length > 2) {
return false;
}
let sawChompingIndicator = false;
let sawIndentIndicator = false;
for (const indicator of indicators) {
if ((indicator === '+' || indicator === '-') && !sawChompingIndicator) {
sawChompingIndicator = true;
continue;
}
if (/[1-9]/.test(indicator) && !sawIndentIndicator) {
sawIndentIndicator = true;
continue;
}
return false;
}
return true;
} }
function shouldRedactRawYamlPath(pathKeys: string[]): boolean { function shouldRedactRawYamlPath(pathKeys: string[]): boolean {
@@ -404,7 +435,7 @@ function mergeCliproxyAuthConfig(
nextAuth: UnifiedConfig['cliproxy']['auth'] nextAuth: UnifiedConfig['cliproxy']['auth']
): UnifiedConfig['cliproxy']['auth'] { ): UnifiedConfig['cliproxy']['auth'] {
if (!nextAuth) { if (!nextAuth) {
return undefined; return currentAuth;
} }
const mergedAuth = { ...nextAuth }; const mergedAuth = { ...nextAuth };
@@ -412,6 +412,74 @@ describe('web-server config-routes account context validation', () => {
); );
}); });
it('preserves cliproxy auth secrets when hidden auth subtrees are omitted on save', async () => {
const config = createEmptyUnifiedConfig();
config.cliproxy.auth = {
api_key: 'cliproxy-global-api-key',
management_secret: 'cliproxy-global-management-secret',
};
config.cliproxy.variants.gemini = {
provider: 'gemini',
auth: {
api_key: 'variant-api-key',
management_secret: 'variant-management-secret',
},
};
saveUnifiedConfig(config);
const getResponse = await fetch(`${baseUrl}/api/config`);
const sanitized = (await getResponse.json()) as typeof config;
delete sanitized.cliproxy.auth;
if (sanitized.cliproxy.variants.gemini) {
delete sanitized.cliproxy.variants.gemini.auth;
}
sanitized.cliproxy.kiro_no_incognito = true;
const putResponse = await putJson(baseUrl, '/api/config', sanitized);
expect(putResponse.status).toBe(200);
const savedConfig = loadUnifiedConfig();
expect(savedConfig?.cliproxy.kiro_no_incognito).toBe(true);
expect(savedConfig?.cliproxy.auth?.api_key).toBe('cliproxy-global-api-key');
expect(savedConfig?.cliproxy.auth?.management_secret).toBe(
'cliproxy-global-management-secret'
);
expect(savedConfig?.cliproxy.variants.gemini?.auth?.api_key).toBe('variant-api-key');
expect(savedConfig?.cliproxy.variants.gemini?.auth?.management_secret).toBe(
'variant-management-secret'
);
});
it('clears cliproxy auth secrets when empty auth objects are saved explicitly', async () => {
const config = createEmptyUnifiedConfig();
config.cliproxy.auth = {
api_key: 'cliproxy-global-api-key',
management_secret: 'cliproxy-global-management-secret',
};
config.cliproxy.variants.gemini = {
provider: 'gemini',
auth: {
api_key: 'variant-api-key',
management_secret: 'variant-management-secret',
},
};
saveUnifiedConfig(config);
const getResponse = await fetch(`${baseUrl}/api/config`);
const sanitized = (await getResponse.json()) as typeof config;
sanitized.cliproxy.auth = {};
if (sanitized.cliproxy.variants.gemini) {
sanitized.cliproxy.variants.gemini.auth = {};
}
const putResponse = await putJson(baseUrl, '/api/config', sanitized);
expect(putResponse.status).toBe(200);
const savedConfig = loadUnifiedConfig();
expect(savedConfig?.cliproxy.auth).toBeUndefined();
expect(savedConfig?.cliproxy.variants.gemini?.auth).toBeUndefined();
});
it('redacts block-scalar secrets from raw YAML responses', async () => { it('redacts block-scalar secrets from raw YAML responses', async () => {
const yamlPath = path.join(tempHome, '.ccs', 'config.yaml'); const yamlPath = path.join(tempHome, '.ccs', 'config.yaml');
fs.writeFileSync( fs.writeFileSync(
@@ -428,18 +496,18 @@ describe('web-server config-routes account context validation', () => {
' gemini:', ' gemini:',
' provider: gemini', ' provider: gemini',
' auth:', ' auth:',
' api_key: |', ' api_key: |-',
' variant-api-secret-line-1', ' variant-api-secret-line-1',
' variant-api-secret-line-2', ' variant-api-secret-line-2',
' auth:', ' auth:',
' api_key: >', ' api_key: >+',
' cliproxy-global-api-secret', ' cliproxy-global-api-secret',
' management_secret: |', ' management_secret: |+',
' cliproxy-global-management-secret', ' cliproxy-global-management-secret',
'global_env:', 'global_env:',
' enabled: true', ' enabled: true',
' env:', ' env:',
' GITHUB_TOKEN: |', ' GITHUB_TOKEN: |-',
' github-token-secret-line-1', ' github-token-secret-line-1',
' github-token-secret-line-2', ' github-token-secret-line-2',
'cliproxy_server:', 'cliproxy_server:',
@@ -447,12 +515,12 @@ describe('web-server config-routes account context validation', () => {
' enabled: true', ' enabled: true',
' host: proxy.example.com', ' host: proxy.example.com',
' protocol: https', ' protocol: https',
' auth_token: >', ' auth_token: >-',
' remote-auth-secret', ' remote-auth-secret',
'dashboard_auth:', 'dashboard_auth:',
' enabled: true', ' enabled: true',
' username: admin', ' username: admin',
' password_hash: |', ' password_hash: >+',
' dashboard-password-secret', ' dashboard-password-secret',
'', '',
].join('\n') ].join('\n')