refactor + warning for failed decryptions
This commit is contained in:
@@ -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)
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user