fix(config): prevent arrays from being treated as records in preference normalization
Update preference normalization logic to explicitly reject arrays when validating record types, ensuring only plain objects are accepted. Adjust ConfigProvider effect conditions for clarity and correctness. Add unit tests verifying that arrays are not accepted as valid preference records or saved voices.
This commit is contained in:
parent
f8182c97df
commit
23b9417567
3 changed files with 49 additions and 40 deletions
|
|
@ -209,8 +209,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
}, [appConfig]);
|
}, [appConfig]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ttsProvidersTabDisabled || !isDBReady || !appConfig || sharedProvidersLoading) return;
|
if (ttsProvidersTabDisabled && isDBReady && appConfig && !sharedProvidersLoading) {
|
||||||
|
|
||||||
const resetPatch: Partial<AppConfigRow> = {};
|
const resetPatch: Partial<AppConfigRow> = {};
|
||||||
|
|
||||||
// When the provider tab is hidden, clear any user-set provider config back to
|
// When the provider tab is hidden, clear any user-set provider config back to
|
||||||
|
|
@ -231,11 +230,11 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
console.warn('Failed to clear hidden TTS provider settings:', error);
|
console.warn('Failed to clear hidden TTS provider settings:', error);
|
||||||
});
|
});
|
||||||
queueSyncedPreferencePatch(resetPatch);
|
queueSyncedPreferencePatch(resetPatch);
|
||||||
|
}
|
||||||
}, [ttsProvidersTabDisabled, isDBReady, appConfig, queueSyncedPreferencePatch, sharedProvidersLoading]);
|
}, [ttsProvidersTabDisabled, isDBReady, appConfig, queueSyncedPreferencePatch, sharedProvidersLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!restrictUserApiKeys || !isDBReady || !appConfig || sharedProvidersLoading) return;
|
if (restrictUserApiKeys && isDBReady && appConfig && !sharedProvidersLoading) {
|
||||||
|
|
||||||
const resetPatch: Partial<AppConfigRow> = {};
|
const resetPatch: Partial<AppConfigRow> = {};
|
||||||
|
|
||||||
if (appConfig.apiKey !== '') resetPatch.apiKey = '';
|
if (appConfig.apiKey !== '') resetPatch.apiKey = '';
|
||||||
|
|
@ -257,6 +256,7 @@ export function ConfigProvider({ children }: { children: ReactNode }) {
|
||||||
console.warn('Failed to enforce restricted user API key mode:', error);
|
console.warn('Failed to enforce restricted user API key mode:', error);
|
||||||
});
|
});
|
||||||
queueSyncedPreferencePatch(resetPatch);
|
queueSyncedPreferencePatch(resetPatch);
|
||||||
|
}
|
||||||
}, [restrictUserApiKeys, isDBReady, appConfig, queueSyncedPreferencePatch, sharedProvidersLoading]);
|
}, [restrictUserApiKeys, isDBReady, appConfig, queueSyncedPreferencePatch, sharedProvidersLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export interface PreferenceNormalizationContext {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
return typeof value === 'object' && value !== null;
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeSavedVoices(value: unknown): Record<string, string> {
|
export function sanitizeSavedVoices(value: unknown): Record<string, string> {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
sanitizePreferencesPatch,
|
sanitizePreferencesPatch,
|
||||||
|
sanitizeSavedVoices,
|
||||||
type PreferenceNormalizationContext,
|
type PreferenceNormalizationContext,
|
||||||
} from '../../src/lib/server/user/preferences-normalize';
|
} from '../../src/lib/server/user/preferences-normalize';
|
||||||
|
|
||||||
|
|
@ -96,4 +97,12 @@ describe('sanitizePreferencesPatch — inherit-by-default provider model', () =>
|
||||||
expect('providerType' in patch).toBe(false);
|
expect('providerType' in patch).toBe(false);
|
||||||
expect('ttsModel' in patch).toBe(false);
|
expect('ttsModel' in patch).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('rejects arrays where preference records are expected', () => {
|
||||||
|
expect(sanitizePreferencesPatch(['voice'], makeContext(), { fillMissingProvider: false })).toEqual({
|
||||||
|
patch: {},
|
||||||
|
migrated: false,
|
||||||
|
});
|
||||||
|
expect(sanitizeSavedVoices(['af_sarah'])).toEqual({});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue