// Default worker URL for image proxy const DEFAULT_IMAGE_PROXY_URL = 'https://image-proxy.opennotas.io'; function getDefaultSettings() { return { general: { lang: 'en', theme: 'system', defaultEditor: 'Crepe', editorView: 'compact', fontFamily: 'System', }, sync: { frequency: '5', adapter: 'LocalForage', configuration: '{}', }, imageSync: { enabled: false, workerUrl: '', s3Endpoint: '', s3AccessKey: '', s3SecretKey: '', s3Bucket: '', }, } } function sanitizeSettingsForExport(settings: any) { const clone = JSON.parse(JSON.stringify(settings)); if (clone.sync) delete clone.sync.configuration; if (clone.imageSync) { delete clone.imageSync.s3AccessKey; delete clone.imageSync.s3SecretKey; } return clone; } // Settings fields that hold secrets (S3 credentials, sync connection strings). When an export // includes them, they're individually AES-encrypted rather than shipped as plaintext in the // downloaded JSON file — encryptedFields records which ones so import can reverse it. const SENSITIVE_SETTINGS_FIELDS: [string, string][] = [ ['sync', 'configuration'], ['imageSync', 's3AccessKey'], ['imageSync', 's3SecretKey'], ]; async function encryptSensitiveSettings(settings: any, password: string) { const clone = JSON.parse(JSON.stringify(settings)); const encryptedFields: string[] = []; for (const [group, field] of SENSITIVE_SETTINGS_FIELDS) { const value = clone[group]?.[field]; if (value) { clone[group][field] = await encryptData(value, password); encryptedFields.push(`${group}.${field}`); } } return { settings: clone, encryptedFields }; } // Decrypts each flagged field independently — an older/foreign client can leave a stale // `encryptedFields` marker next to a value it wrote in plaintext (it doesn't know the marker // exists). Recover that value as-is instead of trying (and failing) to decrypt it: real // ciphertext has a distinctive shape (see looksLikeCiphertext), so a value that doesn't match // it was never encrypted to begin with. Only genuinely undecryptable ciphertext (wrong/lost // key, corruption) falls through to being dropped — that's the one case that can't be // recovered client-side, so it must not take down the rest of the settings object either. async function decryptSensitiveSettings(settings: any, encryptedFields: string[], password: string) { const clone = JSON.parse(JSON.stringify(settings)); for (const path of encryptedFields || []) { const [group, field] = path.split('.'); const value = clone[group]?.[field]; if (!value) continue; if (!looksLikeCiphertext(value)) { console.warn(`Setting "${path}" is flagged encrypted but isn't valid ciphertext — treating it as plaintext`); continue; } try { clone[group][field] = await decryptData(value, password); } catch (err) { console.warn(`Failed to decrypt setting "${path}", dropping it`, err); delete clone[group][field]; } } return clone; } // Drops sensitive fields that can't be decrypted (no local password to unlock them with) // instead of importing them as raw ciphertext. function stripEncryptedFields(settings: any, encryptedFields: string[]) { const clone = JSON.parse(JSON.stringify(settings)); for (const path of encryptedFields || []) { const [group, field] = path.split('.'); if (clone[group]) delete clone[group][field]; } return clone; } export { getDefaultSettings, DEFAULT_IMAGE_PROXY_URL, sanitizeSettingsForExport, encryptSensitiveSettings, decryptSensitiveSettings, stripEncryptedFields, };