diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 34aef1e..0000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: Build Electron App - -on: - push: - tags: - - 'v*' - -jobs: - build: - strategy: - fail-fast: false - matrix: - include: - - os: windows-latest - script: build:win - - os: macos-latest - script: build:mac - - os: ubuntu-latest - script: build:linux - - runs-on: ${{ matrix.os }} - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: npm - - - name: Install dependencies - run: npm ci - - - name: Run platform build - run: npm run ${{ matrix.script }} - env: - CSC_LINK: ${{ secrets.CSC_LINK }} - CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }} - - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.os }}-build - path: | - dist/** diff --git a/out/main/index.js b/out/main/index.js index a6b22de..2969fc2 100644 --- a/out/main/index.js +++ b/out/main/index.js @@ -3,28 +3,61 @@ 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 "libsodium-wrappers"; -import "uuid"; -import "flexsearch"; -import fs from "fs/promises"; -import path, { join } from "path"; +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(environment = "web") { + constructor() { this.plugins = /* @__PURE__ */ new Map(); - this.environment = environment; } register(plugin) { if (!plugin.id) { throw new Error("Plugin must have an id"); } - const environments = plugin.environments || ["electron", "web"]; - if (!environments.includes(this.environment)) { - return; - } this.plugins.set(plugin.id, plugin); } get(id) { @@ -39,146 +72,314 @@ class PluginRegistry { })); } } -function createIpcStorage() { - return { - async load() { - return await window.api.getConfig(); - }, - async save(data) { - await window.api.setConfig(data); - } - }; -} -function getDefaultConfig() { +const getDefaultConfig = () => { return { activeAdapter: "supabase", - adapters: { - supabase: { - supabaseUrl: "https://example.supabase.co", - supabaseKey: "", - bucket: "notes" - } - }, - theme: "light" - }; -} -let config = null; -let configResolve = null; -const configPromise = new Promise((resolve) => { - configResolve = resolve; -}); -function createConfigManager(environment, customStorage = null) { - let storage; - if (customStorage) { - storage = customStorage; - } else { - storage = createIpcStorage(); - } - const loadConfig = async () => { - if (config !== null) { - return config; - } - const stored = await storage.load(); - config = stored || getDefaultConfig(); - if (!stored) { - await storage.save(config); - } - configResolve(config); - return config; - }; - const writeConfig = async (newConfig) => { - await storage.save(newConfig); - config = newConfig; - }; - const getConfig = () => { - if (config !== null) { - return config; - } - return configPromise; - }; - const setConfig = async (newConfig) => { - config = newConfig; - await writeConfig(newConfig); - }; - const refreshConfig = async () => { - config = await storage.load(); - configResolve(config); + theme: "dark" }; +}; +const createConfigManager = (storage) => { + let config = null; return { - loadConfig, - getConfig, - setConfig, - refreshConfig - }; -} -const USER_DATA_STRING = "__DEFAULT_USER_DATA__"; -function createNodeStorage(defaultPlugin = null) { - const configPath = path.join(app.getPath("userData"), "config.json"); - function resolveDefaults(obj) { - if (Array.isArray(obj)) { - return obj.map(resolveDefaults); - } else if (obj && typeof obj === "object") { - const resolved = {}; - for (const [key, value] of Object.entries(obj)) { - resolved[key] = resolveDefaults(value); + async loadConfig() { + if (config) return config; + const stored = await storage.load(); + config = stored || getDefaultConfig(); + if (!stored) { + await storage.save(config); } - return resolved; - } else if (typeof obj === "string" && obj.includes(USER_DATA_STRING)) { - return obj.replace(USER_DATA_STRING, app.getPath("userData")); - } - return obj; - } - return { - async load() { - let parsed; - try { - const raw = await fs.readFile(configPath, "utf8"); - parsed = JSON.parse(raw); - } catch { - parsed = null; - } - if (!parsed || !parsed.activeAdapter) { - const defaultConfig = {}; - if (defaultPlugin) { - for (const field of defaultPlugin.configSchema) { - defaultConfig[field.key] = field.default ?? null; - } - } - parsed = { - ...parsed ? parsed : {}, - activeAdapter: defaultPlugin?.id || "supabase", - adapters: {}, - theme: "dark" - }; - if (defaultPlugin) { - parsed.adapters[defaultPlugin.id] = defaultConfig; - } - await this.save(parsed); - } else { - parsed.adapters = resolveDefaults(parsed.adapters); - } - return parsed; + return config; }, - async save(configObject) { - const dir = path.dirname(configPath); - await fs.mkdir(dir, { recursive: true }); - const resolved = { - ...configObject, - adapters: resolveDefaults(configObject.adapters) - }; - await fs.writeFile( - configPath, - JSON.stringify(resolved, null, 2), - "utf8" + 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"); -let activeAdapter = null; function createWindow() { const mainWindow = new BrowserWindow({ width: DEFAULT_WINDOW_SIZE.width, @@ -234,44 +435,28 @@ app.whenReady().then(async () => { win.webContents.send(event, data); }); }; - const registry = new PluginRegistry("electron"); - registry.register(filesystemPlugin); - registry.register(supabasePlugin); - const nodeStorage = createNodeStorage(filesystemPlugin); - const configManager = createConfigManager("electron", nodeStorage); - const initialConfig = await configManager.loadConfig(); - const setActivePlugin = async (pluginId) => { - const currentConfig = await configManager.loadConfig(); - await configManager.setConfig({ - ...currentConfig, - activeAdapter: pluginId - }); - const plugin = registry.get(pluginId); - const adapterConfig = currentConfig.adapters[pluginId] || {}; - activeAdapter = plugin.createAdapter(adapterConfig); - await activeAdapter.init(); - ipcMain.removeHandler("adapter:call"); - ipcMain.handle("adapter:call", async (_, method, args) => { - if (!activeAdapter[method]) { - throw new Error(`Invalid adapter method: ${method}`); - } - return await activeAdapter[method](...args); - }); - broadcastNoteChange("plugin-changed", pluginId); - return true; - }; - await setActivePlugin(initialConfig.activeAdapter); - ipcMain.handle("getConfig", async () => { - return await configManager.loadConfig(); + 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("setConfig", async (_, newConfig) => { - await configManager.setConfig(newConfig); + ipcMain.handle("configManager:call", async (_, method, ...args) => { + return await configManager[method](...args); }); - ipcMain.handle("listPlugins", async () => { - return registry.list(); - }); - ipcMain.handle("setActivePlugin", async (_, pluginId) => { - return await setActivePlugin(pluginId); + 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); @@ -297,8 +482,8 @@ app.whenReady().then(async () => { } }); electronApp.setAppUserModelId("com.electron"); - app.on("browser-window-created", (_, window2) => { - optimizer.watchWindowShortcuts(window2); + app.on("browser-window-created", (_, window) => { + optimizer.watchWindowShortcuts(window); }); createWindow(); app.on("activate", function() { diff --git a/out/preload/index.mjs b/out/preload/index.mjs index fe8debb..b4f58bc 100644 --- a/out/preload/index.mjs +++ b/out/preload/index.mjs @@ -1,9 +1,20 @@ import { contextBridge, ipcRenderer } from "electron"; const api = { - getConfig: () => ipcRenderer.invoke("getConfig"), - setConfig: (config) => ipcRenderer.invoke("setConfig", config), - listPlugins: () => ipcRenderer.invoke("listPlugins"), - setActivePlugin: (pluginId) => ipcRenderer.invoke("setActivePlugin", pluginId), + pluginManagerCall: (method, ...args) => ipcRenderer.invoke( + "pluginManager:call", + method, + ...args.length ? args : [] + ), + configManagerCall: (method, ...args) => ipcRenderer.invoke( + "configManager:call", + method, + ...args.length ? args : [] + ), + adapterCall: (method, ...args) => ipcRenderer.invoke( + "adapter:call", + method, + ...args.length ? args : [] + ), openNoteWindow: (noteId) => { ipcRenderer.send("open-note-window", noteId); }, @@ -29,17 +40,12 @@ const api = { ipcRenderer.invoke("move-closed"); } }; -const adapter = { - call: (method, ...args) => ipcRenderer.invoke("adapter:call", method, args) -}; if (process.contextIsolated) { try { contextBridge.exposeInMainWorld("api", api); - contextBridge.exposeInMainWorld("adapter", adapter); } catch (error) { console.error(error); } } else { window.api = api; - window.adapter = adapter; } diff --git a/out/renderer/assets/__vite-browser-external-2Ng8QIWW.js b/out/renderer/assets/__vite-browser-external-2Ng8QIWW.js deleted file mode 100644 index f2316bd..0000000 --- a/out/renderer/assets/__vite-browser-external-2Ng8QIWW.js +++ /dev/null @@ -1,4 +0,0 @@ -const __viteBrowserExternal = {}; -export { - __viteBrowserExternal as default -}; diff --git a/out/renderer/assets/geist-mono-BzrJhchg.woff2 b/out/renderer/assets/geist-mono-BzrJhchg.woff2 deleted file mode 100644 index acf07fc..0000000 Binary files a/out/renderer/assets/geist-mono-BzrJhchg.woff2 and /dev/null differ diff --git a/out/renderer/assets/geist-mono-OFKGen7b.woff b/out/renderer/assets/geist-mono-OFKGen7b.woff deleted file mode 100644 index 5d71b23..0000000 Binary files a/out/renderer/assets/geist-mono-OFKGen7b.woff and /dev/null differ diff --git a/out/renderer/assets/geist-mono-bold-Bz_UliG4.woff b/out/renderer/assets/geist-mono-bold-Bz_UliG4.woff deleted file mode 100644 index ecaceab..0000000 Binary files a/out/renderer/assets/geist-mono-bold-Bz_UliG4.woff and /dev/null differ diff --git a/out/renderer/assets/geist-mono-bold-CTLtpKvJ.woff2 b/out/renderer/assets/geist-mono-bold-CTLtpKvJ.woff2 deleted file mode 100644 index c44b8fd..0000000 Binary files a/out/renderer/assets/geist-mono-bold-CTLtpKvJ.woff2 and /dev/null differ diff --git a/out/renderer/assets/neuefraktur-A4S1ACH2.woff2 b/out/renderer/assets/neuefraktur-A4S1ACH2.woff2 deleted file mode 100644 index 5c41f61..0000000 Binary files a/out/renderer/assets/neuefraktur-A4S1ACH2.woff2 and /dev/null differ diff --git a/out/renderer/assets/neuefraktur-CwjUIZ0G.woff b/out/renderer/assets/neuefraktur-CwjUIZ0G.woff deleted file mode 100644 index 359f29f..0000000 Binary files a/out/renderer/assets/neuefraktur-CwjUIZ0G.woff and /dev/null differ diff --git a/out/renderer/index.html b/out/renderer/index.html deleted file mode 100644 index c1b7715..0000000 --- a/out/renderer/index.html +++ /dev/null @@ -1,18 +0,0 @@ - - -
- -