refactor + warning for failed decryptions

This commit is contained in:
nicwands
2026-04-02 11:45:46 -04:00
parent a0a5cde33a
commit ad961fd31a
16 changed files with 505 additions and 187 deletions

View File

@@ -1,3 +1,7 @@
import Ajv from 'ajv'
const ajv = new Ajv({ allErrors: true, strict: false })
const getDefaultConfig = () => {
return {
activeAdapter: 'browser',
@@ -5,7 +9,65 @@ const getDefaultConfig = () => {
}
}
export const createConfigManager = (storage) => {
const CONFIG_SCHEMA = {
type: 'object',
properties: {
activeAdapter: { type: 'string' },
theme: { type: 'string', enum: ['dark', 'light'] },
encryptionKey: { type: 'string' },
adapters: { type: 'object' },
},
required: ['activeAdapter'],
additionalProperties: true,
}
const validateConfig = ajv.compile(CONFIG_SCHEMA)
const convertSchemaToJson = (schemaArray) => {
const properties = {}
const required = []
for (const field of schemaArray) {
properties[field.key] = { type: 'string' }
if (field.required) {
required.push(field.key)
}
}
return {
type: 'object',
properties,
required,
additionalProperties: false,
}
}
const validateAdapterConfigs = (adapters, pluginManager) => {
if (!adapters || !pluginManager) return { valid: true, errors: [] }
const errors = []
for (const [adapterId, adapterConfig] of Object.entries(adapters)) {
const plugin = pluginManager.getPlugin(adapterId)
if (!plugin?.configSchema) continue
const schema = convertSchemaToJson(plugin.configSchema)
const validate = ajv.compile(schema)
const valid = validate(adapterConfig)
if (!valid) {
const adapterErrors = validate.errors.map(
(e) => `${adapterId}${e.instancePath}: ${e.message}`,
)
errors.push(...adapterErrors)
}
}
return { valid: errors.length === 0, errors }
}
export const createConfigManager = (storage, pluginManager) => {
let config = null
return {
@@ -27,6 +89,24 @@ export const createConfigManager = (storage) => {
},
async setConfig(newConfig) {
const valid = validateConfig(newConfig)
if (!valid) {
const errors = validateConfig.errors
.map((e) => `${e.instancePath || 'root'}: ${e.message}`)
.join('; ')
throw new Error(`Config validation failed: ${errors}`)
}
const adapterValidation = validateAdapterConfigs(
newConfig.adapters,
pluginManager,
)
if (!adapterValidation.valid) {
throw new Error(
`Adapter config validation failed: ${adapterValidation.errors.join('; ')}`,
)
}
config = newConfig
await storage.save(newConfig)
},

View File

@@ -10,8 +10,10 @@ export default class NotesAPI {
this.adapter = adapter
this.notesCache = new Map()
this.categories = new Set()
this.encryptionKey = encryptionKey
this._sodiumReady = false
this._decryptionFailures = []
this.index = new Index({
tokenize: 'forward',
@@ -26,11 +28,9 @@ export default class NotesAPI {
}
_hexToUint8Array(hex) {
const bytes = new Uint8Array(hex.length / 2)
for (let i = 0; i < bytes.length; i++) {
bytes[i] = parseInt(hex.substr(i * 2, 2), 16)
}
return bytes
return Uint8Array.from(hex.match(/.{1,2}/g), (byte) =>
parseInt(byte, 16),
)
}
_concatUint8Arrays(a, b) {
@@ -41,20 +41,12 @@ export default class NotesAPI {
}
_uint8ArrayToBase64(bytes) {
let binary = ''
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i])
}
return btoa(binary)
return btoa(String.fromCharCode(...bytes))
}
_base64ToUint8Array(base64) {
const binary = atob(base64)
const bytes = new Uint8Array(binary.length)
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i)
}
return bytes
return Uint8Array.from(binary, (c) => c.charCodeAt(0))
}
_encrypt(note) {
@@ -142,23 +134,34 @@ export default class NotesAPI {
await this._initSodium()
await this.adapter.init()
this.notesCache.clear()
this.categories.clear()
this._decryptionFailures = []
const encryptedNotes = await this.adapter.getAll()
for (const encryptedNote of encryptedNotes) {
const noteId = encryptedNote.id || 'unknown'
try {
const note = this._decrypt(encryptedNote.data || encryptedNote)
this.notesCache.set(note.id, note)
if (note.category) {
this.categories.add(note.category)
}
const searchText =
note.plainText || this._extractPlainText(note.content)
this.index.add(note.id, note.title + '\n' + searchText)
} catch (error) {
console.error('Failed to decrypt note:', error)
this._decryptionFailures.push(noteId)
}
}
}
getDecryptionFailures() {
return [...this._decryptionFailures]
}
_extractPlainText(content) {
if (!content) return ''
if (typeof content === 'string') return content
@@ -173,15 +176,7 @@ export default class NotesAPI {
}
getCategories() {
const categories = new Set()
for (const note of this.notesCache.values()) {
if (note.category) {
categories.add(note.category)
}
}
return Array.from(categories).sort()
return Array.from(this.categories).sort()
}
getCategoryNotes(categoryName = null) {
@@ -193,6 +188,7 @@ export default class NotesAPI {
getNote(id) {
const note = this.notesCache.get(id)
return note ? { ...note } : null
}
@@ -216,6 +212,9 @@ export default class NotesAPI {
}
this.notesCache.set(id, note)
if (note.category) {
this.categories.add(note.category)
}
this.index.add(id, note.title + '\n' + plainText)
await this.adapter.create(encryptedNote)
@@ -224,10 +223,30 @@ export default class NotesAPI {
}
async deleteNote(id) {
const note = this.notesCache.get(id)
const category = note?.category
await this.adapter.delete(id)
this.notesCache.delete(id)
this.index.remove(id)
// Delete category if needed
if (category) {
const notesWithCategory = Array.from(
this.notesCache.values(),
).filter((n) => n.category === category)
if (notesWithCategory.length === 0) {
this.categories.delete(category)
}
}
// Update decryption failures
if (this._decryptionFailures.includes(id)) {
this._decryptionFailures = this._decryptionFailures.filter(
(id) => id !== id,
)
}
}
async updateNote(id, updates = {}) {
@@ -242,6 +261,8 @@ export default class NotesAPI {
}
}
const oldCategory = note.category
const updatedNote = {
...note,
...updates,
@@ -255,6 +276,20 @@ export default class NotesAPI {
this.notesCache.set(id, updatedNote)
if (updates.category !== undefined) {
if (oldCategory) {
const notesWithOldCategory = Array.from(
this.notesCache.values(),
).filter((n) => n.category === oldCategory && n.id !== id)
if (notesWithOldCategory.length === 0) {
this.categories.delete(oldCategory)
}
}
if (updatedNote.category) {
this.categories.add(updatedNote.category)
}
}
const searchText =
updatedNote.plainText || this._extractPlainText(updatedNote.content)

View File

@@ -18,7 +18,7 @@ const generateEncryptionKey = () => {
.join('')
}
const initPluginManager = (runtime, plugins, config) => {
const initPluginManager = (runtime, plugins) => {
if (runtime === 'electron-renderer') return createPluginManagerClient()
const registry = new PluginRegistry()
@@ -27,14 +27,10 @@ const initPluginManager = (runtime, plugins, config) => {
registry.register(plugin)
}
const manager = createPluginManager(registry)
const activeConfig = config.adapters?.[config.activeAdapter] || {}
manager.setActivePlugin(config.activeAdapter, activeConfig)
return manager
return createPluginManager(registry)
}
const initConfigManager = async (runtime) => {
const initConfigManager = async (runtime, pluginManager) => {
if (runtime === 'electron-renderer') return createConfigManagerClient()
let storage
@@ -48,35 +44,41 @@ const initConfigManager = async (runtime) => {
storage = createWebStorage()
}
return createConfigManager(storage)
return createConfigManager(storage, pluginManager)
}
export const initializeCore = async (runtime, { plugins }) => {
const configManager = await initConfigManager(runtime)
const pluginManager = initPluginManager(runtime, plugins)
const configManager = await initConfigManager(runtime, pluginManager)
const config = await configManager.loadConfig()
const pluginManager = initPluginManager(runtime, plugins, config)
// NotesAPI bootstrap
// Set active plugin
const activeConfig = config.adapters?.[config.activeAdapter] || {}
pluginManager.setActivePlugin(config.activeAdapter, activeConfig)
// Create API instance
let notesAPI = null
let initPromise = null
const getNotesAPI = async () => {
if (notesAPI) return notesAPI
if (!initPromise) {
initPromise = (async () => {
let encryptionKey = config?.encryptionKey
// Get fresh config to ensure adapters are populated from main process
const latestConfig = await configManager.loadConfig()
let encryptionKey = latestConfig?.encryptionKey
if (!encryptionKey) {
encryptionKey = generateEncryptionKey()
await configManager.setConfig({
...config,
...latestConfig,
encryptionKey,
})
}
const pluginId = config?.activeAdapter || 'filesystem'
const adapterConfig = config?.adapters?.[pluginId] || {}
const pluginId = latestConfig?.activeAdapter || 'filesystem'
const adapterConfig = latestConfig?.adapters?.[pluginId] || {}
const adapter = pluginManager.getAdapter(
pluginId,