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 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 getDefaultConfig = () => { return { activeAdapter: "supabase", theme: "dark" }; }; const createConfigManager = (storage) => { 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) { 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.encryptionKey = encryptionKey; this._sodiumReady = false; this.index = new Index({ tokenize: "forward" }); } async _initSodium() { if (!this._sodiumReady) { await sodium.ready; this._sodiumReady = true; } } _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; } _concatUint8Arrays(a, b) { const result = new Uint8Array(a.length + b.length); result.set(a, 0); result.set(b, a.length); return result; } _uint8ArrayToBase64(bytes) { let binary = ""; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i]); } return btoa(binary); } _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; } _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(); const encryptedNotes = await this.adapter.getAll(); for (const encryptedNote of encryptedNotes) { try { const note = this._decrypt(encryptedNote.data || encryptedNote); this.notesCache.set(note.id, note); 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); } } } _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() { const categories = /* @__PURE__ */ new Set(); for (const note of this.notesCache.values()) { if (note.category) { categories.add(note.category); } } return Array.from(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); 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); this.index.add(id, note.title + "\n" + plainText); await this.adapter.create(encryptedNote); return note; } async deleteNote(id) { await this.adapter.delete(id); this.notesCache.delete(id); this.index.remove(id); } 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 updatedNote = { ...note, ...updates, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }; const encryptedNote = { id: updatedNote.id, data: this._encrypt(updatedNote) }; this.notesCache.set(id, updatedNote); 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, config) => { const registry = new PluginRegistry(); for (const plugin of plugins) { registry.register(plugin); } const manager = createPluginManager(registry); manager.setActivePlugin( config.activeAdapter, config.adapters[config.activeAdapter] ); return manager; }; const initConfigManager = async (runtime) => { let storage; { const { createNodeStorage } = await import("./NodeStorage-DrLmsh6l.js"); storage = createNodeStorage(filesystemPlugin); } return createConfigManager(storage); }; const initializeCore = async (runtime, { plugins }) => { const configManager = await initConfigManager(); const config = await configManager.loadConfig(); const pluginManager = initPluginManager(runtime, plugins, config); let notesAPI = null; let initPromise = null; const getNotesAPI = async () => { if (notesAPI) return notesAPI; if (!initPromise) { initPromise = (async () => { const config2 = await configManager.loadConfig(); let encryptionKey = config2?.encryptionKey; if (!encryptionKey) { encryptionKey = generateEncryptionKey(); await configManager.setConfig({ ...config2, encryptionKey }); } const pluginId = config2?.activeAdapter || "filesystem"; const adapterConfig = config2?.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, 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(); } });