import "dotenv/config"; import { electronApp, optimizer, is } from "@electron-toolkit/utils"; import { app, ipcMain, BrowserWindow, shell } from "electron"; import filesystemPlugin from "@takerofnotes/plugin-filesystem"; import supabasePlugin from "@takerofnotes/plugin-supabase"; import s3Plugin from "@takerofnotes/plugin-s3"; import postgresPlugin from "@takerofnotes/plugin-postgre-sql"; import Ajv from "ajv"; import sodium from "libsodium-wrappers"; import { v4 } from "uuid"; import { Index } from "flexsearch"; import { join } from "path"; import __cjs_mod__ from "node:module"; const __filename = import.meta.filename; const __dirname = import.meta.dirname; const require2 = __cjs_mod__.createRequire(import.meta.url); const createPluginManager = (registry) => { let activePluginId = null; let adapter = null; return { listPlugins() { return registry.list(); }, getPlugin(pluginId) { return registry.get(pluginId); }, getAdapter(pluginId, adapterConfig = {}) { const plugin = registry.get(pluginId); if (!plugin) { throw new Error(`Plugin not found: ${pluginId}`); } return plugin.createAdapter(adapterConfig); }, setActivePlugin(pluginId, adapterConfig = {}) { activePluginId = pluginId; adapter = this.getAdapter(pluginId, adapterConfig); return adapter; }, async testPlugin(pluginId, adapterConfig = {}) { const plugin = registry.get(pluginId); if (!plugin) { throw new Error(`Plugin not found: ${pluginId}`); } const adapter2 = this.getAdapter(pluginId, adapterConfig); await adapter2.init(); return await adapter2.testConnection(); }, getActiveAdapter() { return adapter; }, getActivePluginId() { return activePluginId; } }; }; class PluginRegistry { constructor() { this.plugins = /* @__PURE__ */ new Map(); } register(plugin) { if (!plugin.id) { throw new Error("Plugin must have an id"); } this.plugins.set(plugin.id, plugin); } get(id) { return this.plugins.get(id); } list() { return Array.from(this.plugins.values()).map((plugin) => ({ id: plugin.id, name: plugin.name, description: plugin.description, configSchema: plugin.configSchema })); } } const ajv = new Ajv({ allErrors: true, strict: false }); const getDefaultConfig = () => { return { activeAdapter: "browser", theme: "dark" }; }; 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 }; }; const createConfigManager = (storage, pluginManager) => { let config = null; return { async loadConfig() { if (config) return config; const stored = await storage.load(); config = stored || getDefaultConfig(); if (!stored) { await storage.save(config); } return config; }, getConfig() { return config; }, 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); }, async refreshConfig() { config = await storage.load(); } }; }; class NotesAPI { constructor(adapter, encryptionKey = null) { if (!adapter) { throw new Error("NotesAPI requires a storage adapter"); } this.adapter = adapter; this.notesCache = /* @__PURE__ */ new Map(); this.categories = /* @__PURE__ */ new Set(); this.encryptionKey = encryptionKey; this._sodiumReady = false; this._decryptionFailures = []; this.index = new Index({ tokenize: "forward" }); } async _initSodium() { if (!this._sodiumReady) { await sodium.ready; this._sodiumReady = true; } } _hexToUint8Array(hex) { return Uint8Array.from( hex.match(/.{1,2}/g), (byte) => parseInt(byte, 16) ); } _concatUint8Arrays(a, b) { const result = new Uint8Array(a.length + b.length); result.set(a, 0); result.set(b, a.length); return result; } _uint8ArrayToBase64(bytes) { return btoa(String.fromCharCode(...bytes)); } _base64ToUint8Array(base64) { const binary = atob(base64); return Uint8Array.from(binary, (c) => c.charCodeAt(0)); } _encrypt(note) { if (!this.encryptionKey) { throw new Error("Encryption key not set"); } const key = this._hexToUint8Array(this.encryptionKey); if (key.length !== 32) { throw new Error( "Encryption key must be 64 hex characters (32 bytes)" ); } const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES); const message = JSON.stringify(note); const ciphertext = sodium.crypto_secretbox_easy( new TextEncoder().encode(message), nonce, key ); const combined = this._concatUint8Arrays(nonce, ciphertext); return this._uint8ArrayToBase64(combined); } _decrypt(encryptedData) { if (!this.encryptionKey) { throw new Error("Encryption key not set"); } const key = this._hexToUint8Array(this.encryptionKey); if (key.length !== 32) { throw new Error( "Encryption key must be 64 hex characters (32 bytes)" ); } let combined; try { combined = this._base64ToUint8Array(encryptedData); } catch (e) { throw new Error("Invalid encrypted data: not valid base64"); } if (combined.length < sodium.crypto_secretbox_NONCEBYTES + sodium.crypto_secretbox_MACBYTES) { throw new Error("Invalid encrypted data: too short"); } const nonce = combined.slice(0, sodium.crypto_secretbox_NONCEBYTES); const ciphertext = combined.slice(sodium.crypto_secretbox_NONCEBYTES); let decrypted; try { decrypted = sodium.crypto_secretbox_open_easy( ciphertext, nonce, key ); } catch (e) { throw new Error("Decryption failed: wrong key or corrupted data"); } if (!decrypted) { throw new Error("Decryption failed: no data returned"); } const decryptedStr = new TextDecoder().decode(decrypted); try { return JSON.parse(decryptedStr); } catch (e) { throw new Error( `Decryption succeeded but invalid JSON: ${decryptedStr}` ); } } async init() { 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; const extractText = (node) => { if (typeof node === "string") return node; if (!node || !node.content) return ""; return node.content.map(extractText).join(" "); }; return extractText(content); } getCategories() { return Array.from(this.categories).sort(); } getCategoryNotes(categoryName = null) { return Array.from(this.notesCache.values()).filter((n) => n.category === categoryName).sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt)).map((n) => ({ ...n })); } getNote(id) { const note = this.notesCache.get(id); console.log(id, this.notesCache); return note ? { ...note } : null; } async createNote(metadata = {}, content = "", plainText = "") { const id = v4(); const now = (/* @__PURE__ */ new Date()).toISOString(); const note = { id, title: metadata.title || "Untitled", category: metadata.category || null, createdAt: now, updatedAt: now, content, plainText }; const encryptedNote = { id: note.id, data: this._encrypt(note) }; 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); return note; } 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); if (category) { const notesWithCategory = Array.from( this.notesCache.values() ).filter((n) => n.category === category); if (notesWithCategory.length === 0) { this.categories.delete(category); } } if (this._decryptionFailures.includes(id)) { this._decryptionFailures = this._decryptionFailures.filter( (id2) => id2 !== id2 ); } } async updateNote(id, updates = {}) { const note = this.notesCache.get(id); if (!note) throw new Error("Note not found"); const allowedFields = ["title", "category", "content", "plainText"]; for (const key of Object.keys(updates)) { if (!allowedFields.includes(key)) { throw new Error(`Invalid update field: ${key}`); } } const oldCategory = note.category; const updatedNote = { ...note, ...updates, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }; const encryptedNote = { id: updatedNote.id, data: this._encrypt(updatedNote) }; this.notesCache.set(id, updatedNote); if (updates.category !== void 0) { 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); this.index.update(id, updatedNote.title + "\n" + searchText); await this.adapter.update(encryptedNote); return updatedNote; } search(query) { const ids = this.index.search(query, { limit: 50, suggest: true }); return ids.map((id) => this.notesCache.get(id)); } } const generateEncryptionKey = () => { const array = new Uint8Array(32); crypto.getRandomValues(array); return Array.from(array).map((b) => b.toString(16).padStart(2, "0")).join(""); }; const initPluginManager = (runtime, plugins) => { const registry = new PluginRegistry(); for (const plugin of plugins) { registry.register(plugin); } return createPluginManager(registry); }; const initConfigManager = async (runtime, pluginManager) => { let storage; { const { createNodeStorage } = await import("./NodeStorage-B8VFtrTS.js"); const filesystemPlugin2 = (await import("@takerofnotes/plugin-filesystem")).default; storage = createNodeStorage(filesystemPlugin2); } return createConfigManager(storage, pluginManager); }; const initializeCore = async (runtime, { plugins }) => { const pluginManager = initPluginManager(runtime, plugins); const configManager = await initConfigManager(runtime, pluginManager); const config = await configManager.loadConfig(); const activeConfig = config.adapters?.[config.activeAdapter] || {}; pluginManager.setActivePlugin(config.activeAdapter, activeConfig); let notesAPI = null; let initPromise = null; const getNotesAPI = async () => { if (notesAPI) return notesAPI; if (!initPromise) { initPromise = (async () => { const latestConfig = await configManager.loadConfig(); let encryptionKey = latestConfig?.encryptionKey; if (!encryptionKey) { encryptionKey = generateEncryptionKey(); await configManager.setConfig({ ...latestConfig, encryptionKey }); } const pluginId = latestConfig?.activeAdapter || "filesystem"; const adapterConfig = latestConfig?.adapters?.[pluginId] || {}; const adapter = pluginManager.getAdapter( pluginId, adapterConfig ); notesAPI = new NotesAPI(adapter, encryptionKey); await notesAPI.init(); return notesAPI; })(); } return initPromise; }; return { runtime, pluginManager, configManager, getNotesAPI }; }; const DEFAULT_WINDOW_SIZE = { width: 354, height: 549 }; const DEFAULT_MOVE_WINDOW_SIZE = { width: 708, height: 549 }; const preloadPath = join(__dirname, "../preload/index.mjs"); const rendererPath = join(__dirname, "../renderer/index.html"); function createWindow() { const mainWindow = new BrowserWindow({ width: DEFAULT_WINDOW_SIZE.width, height: DEFAULT_WINDOW_SIZE.height, show: false, autoHideMenuBar: true, webPreferences: { preload: preloadPath, sandbox: false } }); mainWindow.on("ready-to-show", () => { mainWindow.show(); }); mainWindow.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url); return { action: "deny" }; }); if (is.dev && process.env["ELECTRON_RENDERER_URL"]) { mainWindow.loadURL(process.env["ELECTRON_RENDERER_URL"]); } else { mainWindow.loadFile(rendererPath); } } function createNoteWindow(noteId) { const noteWindow = new BrowserWindow({ width: DEFAULT_WINDOW_SIZE.width, height: DEFAULT_WINDOW_SIZE.height, autoHideMenuBar: true, webPreferences: { preload: preloadPath, contextIsolation: true, nodeIntegration: false, sandbox: false } }); if (is.dev && process.env["ELECTRON_RENDERER_URL"]) { noteWindow.loadURL( `${process.env["ELECTRON_RENDERER_URL"]}/#/note/${noteId}` ); } else { noteWindow.loadFile(rendererPath, { hash: `/note/${noteId}` }); } } app.whenReady().then(async () => { ipcMain.on("open-note-window", (_, noteId) => { createNoteWindow(noteId); }); const broadcastNoteChange = (event, data) => { BrowserWindow.getAllWindows().forEach((win) => { win.webContents.send(event, data); }); }; const { pluginManager, configManager } = await initializeCore( "electron-main", { plugins: [ filesystemPlugin, s3Plugin, postgresPlugin, supabasePlugin ] } ); ipcMain.handle("pluginManager:call", async (_, method, ...args) => { const methodCall = await pluginManager[method](...args); if (method === "setActivePlugin") { broadcastNoteChange("plugin-changed"); } return methodCall; }); ipcMain.handle("configManager:call", async (_, method, ...args) => { return await configManager[method](...args); }); ipcMain.handle("adapter:call", async (_, method, ...args) => { const adapter = pluginManager.getActiveAdapter(); if (!adapter[method]) { throw new Error(`Invalid adapter method: ${method}`); } return await adapter[method](...args); }); ipcMain.on("note-changed", (_, event, data) => { broadcastNoteChange(event, data); }); ipcMain.handle("move-opened", (_) => { const activeWindow = BrowserWindow.getFocusedWindow(); const windowSize = activeWindow.getSize(); if (windowSize[0] < DEFAULT_MOVE_WINDOW_SIZE.width) { activeWindow.setSize( DEFAULT_MOVE_WINDOW_SIZE.width, DEFAULT_MOVE_WINDOW_SIZE.height ); } }); ipcMain.handle("move-closed", (_) => { const activeWindow = BrowserWindow.getFocusedWindow(); const windowSize = activeWindow.getSize(); if (windowSize[0] === 708) { activeWindow.setSize( DEFAULT_WINDOW_SIZE.width, DEFAULT_WINDOW_SIZE.height ); } }); electronApp.setAppUserModelId("com.electron"); app.on("browser-window-created", (_, window) => { optimizer.watchWindowShortcuts(window); }); createWindow(); app.on("activate", function() { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on("window-all-closed", () => { if (process.platform !== "darwin") { app.quit(); } });