diff --git a/server/config.js b/server/config.js index 3dc16c4..16b4ba0 100644 --- a/server/config.js +++ b/server/config.js @@ -3,7 +3,7 @@ const fs = require("fs"); // VAULT_ROOT: a directory that contains vault folders. // Each subdirectory is a vault. New vaults are created as new subdirs. -// Falls back to parent of VAULT_PATH (single-vault compat) or ./vaults. +// Falls back to parent of VAULT_PATH (single-vault compatibility) or ./vaults. const vaultRoot = process.env.VAULT_ROOT || (process.env.VAULT_PATH @@ -19,8 +19,10 @@ try { function discoverVaults() { const vaults = {}; + try { const entries = fs.readdirSync(vaultRoot, { withFileTypes: true }); + for (const entry of entries) { if (entry.isDirectory() && !entry.name.startsWith(".")) { vaults[entry.name] = path.join(vaultRoot, entry.name); @@ -29,12 +31,15 @@ function discoverVaults() { } catch (e) { console.error("[config] Failed to read VAULT_ROOT:", vaultRoot, e.message); } + // Create a default vault if none exist if (Object.keys(vaults).length === 0) { const defaultPath = path.join(vaultRoot, "My Vault"); + try { fs.mkdirSync(path.join(defaultPath, ".obsidian"), { recursive: true }); vaults["My Vault"] = defaultPath; + console.log("[config] Created default vault: My Vault"); } catch (e) { console.error("[config] Failed to create default vault:", e.message); diff --git a/server/index.js b/server/index.js index fe9baa9..0d3fab0 100644 --- a/server/index.js +++ b/server/index.js @@ -3,28 +3,39 @@ const path = require("path"); const config = require("./config"); const { setupWebSocket } = require("./ws"); +const ANSI_RED = "\x1b[31m"; +const ANSI_YELLOW = "\x1b[33m"; +const ANSI_GREEN = "\x1b[32m"; +const ANSI_RESET = "\x1b[0m"; + const app = express(); app.use(express.json({ limit: "50mb" })); +// logger middleware app.use((req, res, next) => { const start = Date.now(); const origEnd = res.end; + res.end = function (...args) { const duration = Date.now() - start; const status = res.statusCode; + const color = - status >= 500 ? "\x1b[31m" : status >= 400 ? "\x1b[33m" : "\x1b[32m"; - const reset = "\x1b[0m"; + status >= 500 ? ANSI_RED : status >= 400 ? ANSI_YELLOW : ANSI_GREEN; + const path = req.originalUrl.length > 80 ? req.originalUrl.slice(0, 80) + "..." : req.originalUrl; + console.log( - `${color}${req.method} ${status}${reset} ${path} (${duration}ms)`, + `${color}${req.method} ${status}${ANSI_RESET} ${path} (${duration}ms)`, ); + origEnd.apply(this, args); }; + next(); }); @@ -41,11 +52,18 @@ app.use("/api/proxy", proxyRoutes); app.use("/vault-files", (req, res, next) => { // Extract vault ID from the first path segment const parts = req.path.split("/").filter(Boolean); - if (parts.length === 0) + + if (parts.length === 0) { return res.status(400).json({ error: "Missing vault ID" }); + } + const vaultId = decodeURIComponent(parts[0]); const vaultPath = config.getVaultPath(vaultId); - if (!vaultPath) return res.status(404).json({ error: "Vault not found" }); + + if (!vaultPath) { + return res.status(404).json({ error: "Vault not found" }); + } + // Rewrite req.url to strip the vault ID prefix, then serve statically req.url = "/" + parts.slice(1).join("/"); express.static(vaultPath)(req, res, next); diff --git a/server/routes/fs.js b/server/routes/fs.js index 6602707..dc2206e 100644 --- a/server/routes/fs.js +++ b/server/routes/fs.js @@ -9,6 +9,7 @@ const router = express.Router(); function getVaultRoot(req, res) { const vaultId = req.query.vault || req.body?.vault || config.defaultVaultId; const vaultPath = config.getVaultPath(vaultId); + if (!vaultPath) { res.status(404).json({ error: "Vault not found", id: vaultId }); return null; @@ -21,39 +22,35 @@ function getVaultRoot(req, res) { function resolveVaultPath(vaultRoot, relativePath) { const cleaned = (relativePath || "").replace(/^\/+/, ""); const resolved = path.resolve(vaultRoot, cleaned); + if (!resolved.startsWith(path.resolve(vaultRoot))) { return null; } return resolved; } -function guardPath(req, res) { +function guardPath(req, res, source = "query") { const vaultRoot = getVaultRoot(req, res); - if (!vaultRoot) return null; - const p = req.query.path ?? req.body?.path; + + if (!vaultRoot) { + return null; + } + + const p = source === "body" ? req.body?.path : req.query.path; + if (p === undefined || p === null) { res.status(400).json({ error: "Missing path parameter" }); return null; } + // Empty string = vault root, which is valid const resolved = resolveVaultPath(vaultRoot, p); + if (!resolved) { res.status(403).json({ error: "Path traversal rejected" }); return null; } - req._vaultRoot = vaultRoot; - return resolved; -} -// Same as guardPath but reads path from req.body (POST routes) -function guardBodyPath(req, res) { - const vaultRoot = getVaultRoot(req, res); - if (!vaultRoot) return null; - const resolved = resolveVaultPath(vaultRoot, req.body?.path); - if (!resolved) { - res.status(403).json({ error: "Invalid path" }); - return null; - } req._vaultRoot = vaultRoot; return resolved; } @@ -61,9 +58,14 @@ function guardBodyPath(req, res) { // GET /api/fs/stat?path=... router.get("/stat", async (req, res) => { const resolved = guardPath(req, res); - if (!resolved) return; + + if (!resolved) { + return; + } + try { const stat = await fs.promises.stat(resolved); + res.json({ type: stat.isDirectory() ? "directory" : "file", size: stat.size, @@ -80,18 +82,25 @@ router.get("/stat", async (req, res) => { // GET /api/fs/readdir?path=... router.get("/readdir", async (req, res) => { const resolved = guardPath(req, res); - if (!resolved) return; + + if (!resolved) { + return; + } + try { // Check if path is a file. return ENOTDIR instead of crashing const stat = await fs.promises.stat(resolved); + if (!stat.isDirectory()) { return res .status(400) .json({ error: "ENOTDIR: not a directory", code: "ENOTDIR" }); } + const entries = await fs.promises.readdir(resolved, { withFileTypes: true, }); + res.json( entries.map((e) => ({ name: e.name, @@ -108,22 +117,30 @@ router.get("/readdir", async (req, res) => { // GET /api/fs/readFile?path=...&encoding=... router.get("/readFile", async (req, res) => { const resolved = guardPath(req, res); - if (!resolved) return; + + if (!resolved) { + return; + } + try { - // Check if path is a directory const stat = await fs.promises.stat(resolved); + if (stat.isDirectory()) { return res.status(400).json({ error: "EISDIR: illegal operation on a directory", code: "EISDIR", }); } + const encoding = req.query.encoding; + if (encoding === "utf8" || encoding === "utf-8") { const data = await fs.promises.readFile(resolved, "utf-8"); + res.type("text/plain").send(data); } else { const data = await fs.promises.readFile(resolved); + res.type("application/octet-stream").send(data); } } catch (e) { @@ -135,8 +152,12 @@ router.get("/readFile", async (req, res) => { // POST /api/fs/writeFile { path, content, encoding?, vault? } router.post("/writeFile", async (req, res) => { - const resolved = guardBodyPath(req, res); - if (!resolved) return; + const resolved = guardPath(req, res, "body"); + + if (!resolved) { + return; + } + try { // Ensure parent directory exists const dir = path.dirname(resolved); @@ -144,15 +165,19 @@ router.post("/writeFile", async (req, res) => { const encoding = req.body.encoding || "utf-8"; let data = req.body.content; + if (req.body.base64) { data = Buffer.from(req.body.content, "base64"); } + await fs.promises.writeFile( resolved, data, encoding === "binary" ? undefined : encoding, ); + const stat = await fs.promises.stat(resolved); + res.json({ ok: true, mtime: stat.mtimeMs, size: stat.size }); } catch (e) { res.status(500).json({ error: e.message, code: e.code }); @@ -161,10 +186,15 @@ router.post("/writeFile", async (req, res) => { // POST /api/fs/appendFile { path, content, vault? } router.post("/appendFile", async (req, res) => { - const resolved = guardBodyPath(req, res); - if (!resolved) return; + const resolved = guardPath(req, res, "body"); + + if (!resolved) { + return; + } + try { await fs.promises.appendFile(resolved, req.body.content, "utf-8"); + res.json({ ok: true }); } catch (e) { res.status(500).json({ error: e.message, code: e.code }); @@ -173,10 +203,15 @@ router.post("/appendFile", async (req, res) => { // POST /api/fs/mkdir { path, recursive?, vault? } router.post("/mkdir", async (req, res) => { - const resolved = guardBodyPath(req, res); - if (!resolved) return; + const resolved = guardPath(req, res, "body"); + + if (!resolved) { + return; + } + try { await fs.promises.mkdir(resolved, { recursive: !!req.body.recursive }); + res.json({ ok: true }); } catch (e) { res.status(500).json({ error: e.message, code: e.code }); @@ -186,13 +221,21 @@ router.post("/mkdir", async (req, res) => { // POST /api/fs/rename { oldPath, newPath, vault? } router.post("/rename", async (req, res) => { const vaultRoot = getVaultRoot(req, res); - if (!vaultRoot) return; + + if (!vaultRoot) { + return; + } + const oldResolved = resolveVaultPath(vaultRoot, req.body?.oldPath); const newResolved = resolveVaultPath(vaultRoot, req.body?.newPath); - if (!oldResolved || !newResolved) + + if (!oldResolved || !newResolved) { return res.status(403).json({ error: "Invalid path" }); + } + try { await fs.promises.rename(oldResolved, newResolved); + res.json({ ok: true }); } catch (e) { res.status(500).json({ error: e.message, code: e.code }); @@ -202,13 +245,21 @@ router.post("/rename", async (req, res) => { // POST /api/fs/copyFile { src, dest, vault? } router.post("/copyFile", async (req, res) => { const vaultRoot = getVaultRoot(req, res); - if (!vaultRoot) return; + + if (!vaultRoot) { + return; + } + const srcResolved = resolveVaultPath(vaultRoot, req.body?.src); const destResolved = resolveVaultPath(vaultRoot, req.body?.dest); - if (!srcResolved || !destResolved) + + if (!srcResolved || !destResolved) { return res.status(403).json({ error: "Invalid path" }); + } + try { await fs.promises.copyFile(srcResolved, destResolved); + res.json({ ok: true }); } catch (e) { res.status(500).json({ error: e.message, code: e.code }); @@ -218,12 +269,18 @@ router.post("/copyFile", async (req, res) => { // DELETE /api/fs/unlink?path=... router.delete("/unlink", async (req, res) => { const resolved = guardPath(req, res); - if (!resolved) return; + + if (!resolved) { + return; + } + try { await fs.promises.unlink(resolved); + res.json({ ok: true }); } catch (e) { const status = e.code === "ENOENT" ? 404 : 500; + res.status(status).json({ error: e.message, code: e.code }); } }); @@ -231,9 +288,14 @@ router.delete("/unlink", async (req, res) => { // DELETE /api/fs/rmdir?path=... router.delete("/rmdir", async (req, res) => { const resolved = guardPath(req, res); - if (!resolved) return; + + if (!resolved) { + return; + } + try { await fs.promises.rmdir(resolved); + res.json({ ok: true }); } catch (e) { res.status(500).json({ error: e.message, code: e.code }); @@ -243,7 +305,11 @@ router.delete("/rmdir", async (req, res) => { // DELETE /api/fs/rm?path=...&recursive=true router.delete("/rm", async (req, res) => { const resolved = guardPath(req, res); - if (!resolved) return; + + if (!resolved) { + return; + } + try { await fs.promises.rm(resolved, { recursive: req.query.recursive === "true", @@ -254,12 +320,16 @@ router.delete("/rm", async (req, res) => { } }); -// GET /api/fs/access?path=... router.get("/access", async (req, res) => { const resolved = guardPath(req, res); - if (!resolved) return; + + if (!resolved) { + return; + } + try { await fs.promises.access(resolved); + res.json({ ok: true }); } catch (e) { res @@ -268,13 +338,16 @@ router.get("/access", async (req, res) => { } }); -// GET /api/fs/realpath?path=... router.get("/realpath", async (req, res) => { const resolved = guardPath(req, res); - if (!resolved) return; + + if (!resolved) { + return; + } + try { const real = await fs.promises.realpath(resolved); - // Return path relative to vault root + res.json({ path: path.relative(req._vaultRoot, real) }); } catch (e) { res.status(500).json({ error: e.message, code: e.code }); @@ -283,8 +356,12 @@ router.get("/realpath", async (req, res) => { // POST /api/fs/utimes { path, atime, mtime, vault? } router.post("/utimes", async (req, res) => { - const resolved = guardBodyPath(req, res); - if (!resolved) return; + const resolved = guardPath(req, res, "body"); + + if (!resolved) { + return; + } + try { await fs.promises.utimes( resolved, @@ -300,23 +377,36 @@ router.post("/utimes", async (req, res) => { // GET /api/fs/tree?path=...&vault=... returns full recursive file tree with metadata router.get("/tree", async (req, res) => { const vaultRoot = getVaultRoot(req, res); - if (!vaultRoot) return; + + if (!vaultRoot) { + return; + } + const rootPath = req.query.path ? resolveVaultPath(vaultRoot, req.query.path) : vaultRoot; - if (!rootPath) return res.status(403).json({ error: "Invalid path" }); + + if (!rootPath) { + return res.status(403).json({ error: "Invalid path" }); + } + try { const tree = {}; + async function walk(dir, prefix) { const entries = await fs.promises.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { const rel = prefix ? prefix + "/" + entry.name : entry.name; const full = path.join(dir, entry.name); + if (entry.isDirectory()) { tree[rel] = { type: "directory" }; + await walk(full, rel); } else { const stat = await fs.promises.stat(full); + tree[rel] = { type: "file", size: stat.size, @@ -326,7 +416,9 @@ router.get("/tree", async (req, res) => { } } } + await walk(rootPath, ""); + res.json(tree); } catch (e) { res.status(500).json({ error: e.message, code: e.code }); diff --git a/server/routes/proxy.js b/server/routes/proxy.js index 8f5d4df..d976c19 100644 --- a/server/routes/proxy.js +++ b/server/routes/proxy.js @@ -2,10 +2,11 @@ const express = require("express"); const router = express.Router(); -// POST /api/proxy - forward a request to an external URL (bypasses browser CORS) -// Used by the requestUrl shim for plugin installation, update checks, etc. +// POST /api/proxy - forward a request to an external URL to bypass CORS +// Used by the requestUrl shim for plugin installation, etc. router.post("/", async (req, res) => { const { url, method, headers, body, binary } = req.body; + if (!url) { return res.status(400).json({ error: "Missing url" }); } @@ -15,6 +16,7 @@ router.post("/", async (req, res) => { method: method || "GET", headers: headers || {}, }; + if (body && method !== "GET" && method !== "HEAD") { if (binary && typeof body === "string") { fetchOpts.body = Buffer.from(body, "base64"); diff --git a/server/routes/vault.js b/server/routes/vault.js index 9bb3d80..8fa93d7 100644 --- a/server/routes/vault.js +++ b/server/routes/vault.js @@ -8,11 +8,13 @@ const router = express.Router(); // GET /api/vault/list - returns all discovered vaults (re-scans on each call) router.get("/list", (req, res) => { config.refreshVaults(); + const list = Object.entries(config.vaults).map(([id, vaultPath]) => ({ id, name: id, path: vaultPath, })); + res.json(list); }); @@ -20,9 +22,11 @@ router.get("/list", (req, res) => { router.get("/info", (req, res) => { const vaultId = req.query.vault || config.defaultVaultId; const vaultPath = config.getVaultPath(vaultId); + if (!vaultPath) { return res.status(404).json({ error: "Vault not found", id: vaultId }); } + res.json({ id: vaultId, name: vaultId, @@ -35,21 +39,27 @@ router.get("/info", (req, res) => { // POST /api/vault/create { name } - create a new vault in VAULT_ROOT router.post("/create", async (req, res) => { const name = req.body?.name; + if (!name || /[\/\\:*?"<>|]/.test(name)) { return res.status(400).json({ error: "Invalid vault name" }); } + const vaultPath = path.join(config.vaultRoot, name); + try { await fs.promises.mkdir(vaultPath, { recursive: false }); await fs.promises.mkdir(path.join(vaultPath, ".obsidian"), { recursive: false, }); + config.refreshVaults(); + res.json({ ok: true, id: name, path: vaultPath }); } catch (e) { if (e.code === "EEXIST") { return res.status(409).json({ error: "Vault already exists" }); } + res.status(500).json({ error: e.message, code: e.code }); } }); @@ -58,12 +68,16 @@ router.post("/create", async (req, res) => { router.delete("/remove", async (req, res) => { const vaultId = req.query.vault; const vaultPath = config.getVaultPath(vaultId); + if (!vaultPath) { return res.status(404).json({ error: "Vault not found" }); } + try { await fs.promises.rm(vaultPath, { recursive: true }); + config.refreshVaults(); + res.json({ ok: true }); } catch (e) { res.status(500).json({ error: e.message, code: e.code }); diff --git a/server/ws.js b/server/ws.js index fe77716..6e52cb4 100644 --- a/server/ws.js +++ b/server/ws.js @@ -1,24 +1,24 @@ -const { WebSocketServer } = require('ws'); +const { WebSocketServer } = require("ws"); +//currently unused function setupWebSocket(server) { - const wss = new WebSocketServer({ server, path: '/ws' }); + const wss = new WebSocketServer({ server, path: "/ws" }); - wss.on('connection', (ws) => { - console.log('[ws] Client connected'); + wss.on("connection", (ws) => { + console.log("[ws] Client connected"); - ws.on('message', (data) => { + ws.on("message", (data) => { // TODO: handle watch/unwatch subscriptions from client const msg = JSON.parse(data); - console.log('[ws] Received:', msg); + console.log("[ws] Received:", msg); }); - ws.on('close', () => { - console.log('[ws] Client disconnected'); + ws.on("close", () => { + console.log("[ws] Client disconnected"); }); }); - // TODO: integrate chokidar file watching and broadcast changes - // This will be implemented once the sync strategy is finalized. + // TODO: maybe integrate chokidar file watching and broadcast changes return wss; } diff --git a/shims/crypto/create-hash.js b/shims/crypto/create-hash.js index 808ac31..9d73c44 100644 --- a/shims/crypto/create-hash.js +++ b/shims/crypto/create-hash.js @@ -1,5 +1,6 @@ export function createHash(algorithm) { const alg = algorithm.toUpperCase().replace("-", ""); + const subtleAlg = alg === "SHA256" ? "SHA-256" @@ -16,32 +17,47 @@ export function createHash(algorithm) { if (typeof data === "string") { data = new TextEncoder().encode(data); } + const merged = new Uint8Array(inputData.length + data.length); + merged.set(inputData); merged.set(data, inputData.length); + inputData = merged; + return this; }, digest(encoding) { console.warn("[shim:crypto] createHash.digest - using placeholder"); + const hash = simpleHash(inputData); - if (encoding === "hex") return hash; - if (encoding === "base64") return btoa(hash); + + if (encoding === "hex") { + return hash; + } + + if (encoding === "base64") { + return btoa(hash); + } + return hash; }, async digestAsync(encoding) { const hashBuffer = await crypto.subtle.digest(subtleAlg, inputData); const hashArray = new Uint8Array(hashBuffer); + if (encoding === "hex") { return Array.from(hashArray) .map((b) => b.toString(16).padStart(2, "0")) .join(""); } + if (encoding === "base64") { return btoa(String.fromCharCode(...hashArray)); } + return hashArray; }, }; @@ -49,8 +65,10 @@ export function createHash(algorithm) { function simpleHash(data) { let hash = 0; + for (let i = 0; i < data.length; i++) { hash = ((hash << 5) - hash + data[i]) | 0; } + return Math.abs(hash).toString(16).padStart(8, "0"); } diff --git a/shims/crypto/random-bytes.js b/shims/crypto/random-bytes.js index 5d4aded..3911dc1 100644 --- a/shims/crypto/random-bytes.js +++ b/shims/crypto/random-bytes.js @@ -8,9 +8,11 @@ export function randomBytes(size) { .map((b) => b.toString(16).padStart(2, "0")) .join(""); } + if (encoding === "base64") { return btoa(String.fromCharCode(...this)); } + return new TextDecoder().decode(this); }; diff --git a/shims/electron/ipc-renderer.js b/shims/electron/ipc-renderer.js index 60a62d3..c979727 100644 --- a/shims/electron/ipc-renderer.js +++ b/shims/electron/ipc-renderer.js @@ -6,31 +6,40 @@ const syncHandlers = { vault: () => window.__vaultConfig || { id: "default-vault", path: "/" }, version: () => window.__obsidianVersion || "0.0.0", "is-dev": () => false, + "file-url": () => "/vault-files/" + encodeURIComponent(window.__currentVaultId || "") + "/", + "disable-update": () => true, update: () => "", "disable-gpu": () => false, frame: () => null, "set-icon": () => null, "get-icon": () => null, + relaunch: () => { window.location.reload(); return null; }, + starter: () => { showVaultManager(); return null; }, + help: () => { window.open("https://help.obsidian.md/", "_blank"); return null; }, + sandbox: () => null, + "copy-asar": () => false, "check-update": () => null, + "vault-list": () => { const result = {}; + for (const v of window.__vaultList || []) { result[v.id] = { path: "/" + v.id, @@ -38,36 +47,51 @@ const syncHandlers = { open: v.id === (window.__currentVaultId || ""), }; } + return result; }, + "vault-open": (vaultPath, newWindow) => { const id = (vaultPath || "").replace(/^\/+/, ""); const vault = (window.__vaultList || []).find((v) => v.id === id); + if (!vault && id) { const xhr = new XMLHttpRequest(); + xhr.open("POST", "/api/vault/create", false); xhr.setRequestHeader("Content-Type", "application/json"); xhr.send(JSON.stringify({ name: id })); - if (xhr.status >= 400) return "Failed to create vault"; + + if (xhr.status >= 400) { + return "Failed to create vault"; + } } + const target = window.parent !== window ? window.parent : window; target.location.href = "/?vault=" + encodeURIComponent(id); + return true; }, + "vault-remove": (vaultPath) => { const id = (vaultPath || "").replace(/^\/+/, ""); const xhr = new XMLHttpRequest(); + xhr.open( "DELETE", "/api/vault/remove?vault=" + encodeURIComponent(id), false, ); + xhr.send(); + return xhr.status < 400; }, + "vault-move": (oldPath, newPath) => { return "Moving vaults is not supported in the web version"; }, + "vault-message": () => null, "get-default-vault-path": () => "/My Vault", "get-documents-path": () => "/", @@ -80,18 +104,22 @@ function arrayBufferToBase64(buf) { const bytes = new Uint8Array(buf); let binary = ""; const chunk = 8192; + for (let i = 0; i < bytes.length; i += chunk) { binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk)); } + return btoa(binary); } function base64ToArrayBuffer(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.buffer; } @@ -99,6 +127,7 @@ async function handleRequestUrl(requestId, request) { try { let body = request.body; let binary = false; + if (body instanceof ArrayBuffer) { body = arrayBufferToBase64(body); binary = true; @@ -118,6 +147,7 @@ async function handleRequestUrl(requestId, request) { }); const proxyResult = await res.json(); + if (!res.ok) { ipcRenderer._emit(requestId, { error: proxyResult.error || "Proxy request failed", @@ -160,6 +190,7 @@ export const ipcRenderer = { if (channel === "print-to-pdf") { const iframe = window.__popupIframe; + if (iframe) { setTimeout(() => { iframe.contentWindow.print(); @@ -170,6 +201,7 @@ export const ipcRenderer = { }, 200); } else { window.print(); + queueMicrotask(() => { ipcRenderer._emit("print-to-pdf", { success: true }); }); @@ -180,9 +212,11 @@ export const ipcRenderer = { sendSync(channel, ...args) { console.log("[shim:ipcRenderer] sendSync:", channel, args); + if (syncHandlers[channel]) { return syncHandlers[channel](...args); } + console.warn("[shim:ipcRenderer] Unhandled sendSync channel:", channel); return null; }, @@ -191,7 +225,9 @@ export const ipcRenderer = { if (!listeners.has(channel)) { listeners.set(channel, []); } + listeners.get(channel).push(listener); + return ipcRenderer; }, @@ -200,6 +236,7 @@ export const ipcRenderer = { ipcRenderer.removeListener(channel, wrapped); listener(...args); }; + return ipcRenderer.on(channel, wrapped); }, @@ -207,8 +244,12 @@ export const ipcRenderer = { const arr = listeners.get(channel); if (arr) { const idx = arr.indexOf(listener); - if (idx >= 0) arr.splice(idx, 1); + + if (idx >= 0) { + arr.splice(idx, 1); + } } + return ipcRenderer; }, @@ -218,11 +259,13 @@ export const ipcRenderer = { } else { listeners.clear(); } + return ipcRenderer; }, _emit(channel, ...args) { const arr = listeners.get(channel); + if (arr) { for (const fn of arr) { fn({}, ...args); diff --git a/shims/electron/remote/clipboard.js b/shims/electron/remote/clipboard.js index a20b0af..490ff45 100644 --- a/shims/electron/remote/clipboard.js +++ b/shims/electron/remote/clipboard.js @@ -1,6 +1,6 @@ +// stub export const clipboardShim = { readText() { - // TODO: maintain a local mirror updated via async reads return ""; }, diff --git a/shims/electron/remote/dialog.js b/shims/electron/remote/dialog.js index 73eccb6..bb9da0f 100644 --- a/shims/electron/remote/dialog.js +++ b/shims/electron/remote/dialog.js @@ -19,10 +19,12 @@ export const dialogShim = { return { canceled: false, filePath: "/downloads/" + name }; }, + // TODO: replace alert() with a styled modal (matching vault manager style) async showMessageBox(browserWindow, options) { if (typeof browserWindow === "object" && !options) { options = browserWindow; } + console.log("[shim:dialog] showMessageBox:", options); const message = options.message || ""; diff --git a/shims/electron/remote/index.js b/shims/electron/remote/index.js index 8707c37..a573e62 100644 --- a/shims/electron/remote/index.js +++ b/shims/electron/remote/index.js @@ -25,6 +25,7 @@ export const remoteShim = { screen: screenShim, nativeImage: nativeImageShim, Notification: notificationShim, + safeStorage: { isEncryptionAvailable() { return false; diff --git a/shims/electron/remote/menu.js b/shims/electron/remote/menu.js index 306128f..45461e0 100644 --- a/shims/electron/remote/menu.js +++ b/shims/electron/remote/menu.js @@ -6,6 +6,7 @@ export class menuShim { static buildFromTemplate(template) { const menu = new menuShim(); menu.items = (template || []).map((item) => new menuItemShim(item)); + return menu; } @@ -18,7 +19,6 @@ export class menuShim { } popup(options) { - // TODO: render custom HTML context menu at mouse position console.log("[shim:Menu] popup (stub)", options); } @@ -30,9 +30,7 @@ export class menuShim { this.items.splice(pos, 0, menuItem); } - closePopup() { - // TODO: hide custom context menu - } + closePopup() {} } export class menuItemShim { diff --git a/shims/electron/remote/theme.js b/shims/electron/remote/theme.js index 4c6c031..daf0e95 100644 --- a/shims/electron/remote/theme.js +++ b/shims/electron/remote/theme.js @@ -37,7 +37,10 @@ export const themeShim = { if (event === "updated") { const wrapped = () => { const idx = listeners.indexOf(wrapped); - if (idx >= 0) listeners.splice(idx, 1); + if (idx >= 0) { + listeners.splice(idx, 1); + } + callback(); }; listeners.push(wrapped); @@ -47,7 +50,10 @@ export const themeShim = { removeListener(event, callback) { const idx = listeners.indexOf(callback); - if (idx >= 0) listeners.splice(idx, 1); + if (idx >= 0) { + listeners.splice(idx, 1); + } + return themeShim; }, diff --git a/shims/electron/remote/window.js b/shims/electron/remote/window.js index 78f8739..3ea79b8 100644 --- a/shims/electron/remote/window.js +++ b/shims/electron/remote/window.js @@ -107,15 +107,21 @@ const currentWindow = { }, on(event, handler) { - if (event === "focus") window.addEventListener("focus", handler); - else if (event === "blur") window.addEventListener("blur", handler); - else if (event === "resize") window.addEventListener("resize", handler); + if (event === "focus") { + window.addEventListener("focus", handler); + } else if (event === "blur") { + window.addEventListener("blur", handler); + } else if (event === "resize") { + window.addEventListener("resize", handler); + } + return currentWindow; }, once(event, handler) { - if (event === "focus") + if (event === "focus") { window.addEventListener("focus", handler, { once: true }); + } return currentWindow; }, @@ -309,19 +315,34 @@ export const windowShim = { getAllWindows() { const wins = [currentWindow]; - if (_popupWindow) wins.push(_popupWindow); + if (_popupWindow) { + wins.push(_popupWindow); + } + return wins; }, fromId(id) { - if (id === currentWindow.id) return currentWindow; - if (_popupWindow && id === _popupWindow.id) return _popupWindow; + if (id === currentWindow.id) { + return currentWindow; + } + + if (_popupWindow && id === _popupWindow.id) { + return _popupWindow; + } + return null; }, fromWebContents(wc) { - if (wc === currentWebContents) return currentWindow; - if (_popupWebContents && wc === _popupWebContents) return _popupWindow; + if (wc === currentWebContents) { + return currentWindow; + } + + if (_popupWebContents && wc === _popupWebContents) { + return _popupWindow; + } + return null; }, }; @@ -329,14 +350,22 @@ export const windowShim = { export const webContentsShim = { _current: () => currentWebContents, fromId(id) { - if (id === currentWebContents.id) return currentWebContents; - if (_popupWebContents && id === _popupWebContents.id) + if (id === currentWebContents.id) { + return currentWebContents; + } + + if (_popupWebContents && id === _popupWebContents.id) { return _popupWebContents; + } + return null; }, getAllWebContents() { const wcs = [currentWebContents]; - if (_popupWebContents) wcs.push(_popupWebContents); + if (_popupWebContents) { + wcs.push(_popupWebContents); + } + return wcs; }, }; diff --git a/shims/fs/content-cache.js b/shims/fs/content-cache.js index 86a883b..f329be2 100644 --- a/shims/fs/content-cache.js +++ b/shims/fs/content-cache.js @@ -20,6 +20,7 @@ export class ContentCache { entry.accessedAt = Date.now(); return entry.data; } + return null; } @@ -44,6 +45,7 @@ export class ContentCache { delete(path) { const norm = this._normalize(path); const entry = this._cache.get(norm); + if (entry) { this._currentSize -= entry.size; this._cache.delete(norm); @@ -71,12 +73,14 @@ export class ContentCache { _evictOne() { let oldest = null; let oldestTime = Infinity; + for (const [key, entry] of this._cache) { if (entry.accessedAt < oldestTime) { oldest = key; oldestTime = entry.accessedAt; } } + if (oldest) { this.delete(oldest); } diff --git a/shims/fs/metadata-cache.js b/shims/fs/metadata-cache.js index b421972..9514591 100644 --- a/shims/fs/metadata-cache.js +++ b/shims/fs/metadata-cache.js @@ -1,6 +1,6 @@ // In-memory metadata cache // Populated from /api/fs/tree on startup, kept in sync via transport events. -// All stat/exists/readdir calls are served from this cache (zero latency). +// All stat/exists/readdir calls are served from this cache. export class MetadataCache { constructor() { @@ -38,10 +38,12 @@ export class MetadataCache { const oldNorm = this._normalize(oldPath); const newNorm = this._normalize(newPath); const meta = this._entries.get(oldNorm); + if (meta) { this._entries.delete(oldNorm); this._entries.set(newNorm, meta); } + // Move children const prefix = oldNorm + "/"; for (const [key, val] of this._entries) { @@ -65,9 +67,11 @@ export class MetadataCache { const rest = key.slice(prefix.length); const slashIdx = rest.indexOf("/"); const childName = slashIdx >= 0 ? rest.slice(0, slashIdx) : rest; + if (childName && !seen.has(childName)) { seen.add(childName); const childMeta = this._entries.get(prefix + childName); + results.push({ name: childName, type: childMeta?.type || (slashIdx >= 0 ? "directory" : "file"), @@ -82,10 +86,13 @@ export class MetadataCache { return this._entries.size; } - // Build a stat-like object from metadata toStat(path) { const meta = this.get(path); - if (!meta) return null; + + if (!meta) { + return null; + } + return { size: meta.size || 0, mtimeMs: meta.mtime || 0, diff --git a/shims/fs/promises.js b/shims/fs/promises.js index 516c247..de08f6d 100644 --- a/shims/fs/promises.js +++ b/shims/fs/promises.js @@ -2,7 +2,10 @@ export function createFsPromises(metadataCache, contentCache, transport) { return { async stat(path) { const cached = metadataCache.toStat(path); - if (cached) return cached; + + if (cached) { + return cached; + } const meta = await transport.stat(path); metadataCache.set(path, meta); @@ -16,13 +19,16 @@ export function createFsPromises(metadataCache, contentCache, transport) { async readdir(path) { const meta = metadataCache.get(path); + if (meta && meta.type === "file") { return []; } + if (!meta && path && path !== "/" && path !== ".") { const e = new Error( `ENOENT: no such file or directory, scandir '${path}'`, ); + e.code = "ENOENT"; throw e; } @@ -31,7 +37,10 @@ export function createFsPromises(metadataCache, contentCache, transport) { }, async readFile(path, encoding) { - if (typeof encoding === "object") encoding = encoding?.encoding; + if (typeof encoding === "object") { + encoding = encoding?.encoding; + } + const wantText = encoding === "utf8" || encoding === "utf-8"; const meta = metadataCache.get(path); @@ -40,6 +49,7 @@ export function createFsPromises(metadataCache, contentCache, transport) { e.code = "EISDIR"; throw e; } + if (!meta && path) { const e = new Error( `ENOENT: no such file or directory, open '${path}'`, @@ -49,16 +59,19 @@ export function createFsPromises(metadataCache, contentCache, transport) { } const cached = contentCache.get(path); + if (cached !== null) { if (wantText) { return typeof cached === "string" ? cached : new TextDecoder().decode(cached); } + // binary. ensure we return a proper Uint8Array with .buffer if (typeof cached === "string") { return new TextEncoder().encode(cached); } + return cached; } @@ -68,11 +81,15 @@ export function createFsPromises(metadataCache, contentCache, transport) { }, async writeFile(path, data, encoding) { - if (typeof encoding === "object") encoding = encoding?.encoding; + if (typeof encoding === "object") { + encoding = encoding?.encoding; + } contentCache.set(path, data); + const size = typeof data === "string" ? data.length : data.byteLength || 0; + metadataCache.set(path, { type: "file", size, @@ -81,6 +98,7 @@ export function createFsPromises(metadataCache, contentCache, transport) { }); const result = await transport.writeFile(path, data, encoding); + if (result.mtime) { metadataCache.set(path, { type: "file", @@ -93,6 +111,7 @@ export function createFsPromises(metadataCache, contentCache, transport) { async appendFile(path, data, encoding) { contentCache.invalidate(path); + await transport.appendFile(path, data); const meta = await transport.stat(path); @@ -102,15 +121,18 @@ export function createFsPromises(metadataCache, contentCache, transport) { async unlink(path) { contentCache.delete(path); metadataCache.delete(path); + await transport.unlink(path); }, async rename(oldPath, newPath) { const content = contentCache.get(oldPath); + if (content !== null) { contentCache.set(newPath, content); contentCache.delete(oldPath); } + metadataCache.rename(oldPath, newPath); await transport.rename(oldPath, newPath); @@ -119,7 +141,9 @@ export function createFsPromises(metadataCache, contentCache, transport) { async mkdir(path, options) { const recursive = typeof options === "object" ? !!options.recursive : !!options; + metadataCache.set(path, { type: "directory" }); + await transport.mkdir(path, recursive); }, @@ -131,19 +155,25 @@ export function createFsPromises(metadataCache, contentCache, transport) { async rm(path, options) { const recursive = typeof options === "object" ? !!options.recursive : false; + metadataCache.delete(path); contentCache.delete(path); + await transport.rm(path, recursive); }, async copyFile(src, dest) { await transport.copyFile(src, dest); + const meta = await transport.stat(dest); metadataCache.set(dest, meta); }, async access(path) { - if (metadataCache.has(path)) return; + if (metadataCache.has(path)) { + return; + } + const e = new Error( `ENOENT: no such file or directory, access '${path}'`, ); @@ -152,7 +182,10 @@ export function createFsPromises(metadataCache, contentCache, transport) { }, async realpath(path) { - if (!path || path === "/" || path === ".") return "/"; + if (!path || path === "/" || path === ".") { + return "/"; + } + return transport.realpath(path); }, diff --git a/shims/fs/sync.js b/shims/fs/sync.js index 953dda5..1006ee9 100644 --- a/shims/fs/sync.js +++ b/shims/fs/sync.js @@ -6,6 +6,7 @@ export function createFsSync(metadataCache, contentCache, transport) { statSync(path) { const stat = metadataCache.toStat(path); + if (!stat) { const err = new Error( `ENOENT: no such file or directory, stat '${path}'`, @@ -13,6 +14,7 @@ export function createFsSync(metadataCache, contentCache, transport) { err.code = "ENOENT"; throw err; } + return stat; }, @@ -27,7 +29,9 @@ export function createFsSync(metadataCache, contentCache, transport) { }, readFileSync(path, encoding) { - if (typeof encoding === "object") encoding = encoding?.encoding; + if (typeof encoding === "object") { + encoding = encoding?.encoding; + } const meta = metadataCache.get(path); if (meta && meta.type === "directory") { @@ -43,21 +47,28 @@ export function createFsSync(metadataCache, contentCache, transport) { ? cached : new TextDecoder().decode(cached); } + return cached; } console.warn("[shim:fs] readFileSync cache miss, using sync XHR:", path); + const data = transport.readFileSync(path, encoding); contentCache.set(path, data); + return data; }, writeFileSync(path, data, encoding) { - if (typeof encoding === "object") encoding = encoding?.encoding; + if (typeof encoding === "object") { + encoding = encoding?.encoding; + } contentCache.set(path, data); + const size = typeof data === "string" ? data.length : data.byteLength || 0; + metadataCache.set(path, { type: "file", size, diff --git a/shims/fs/transport.js b/shims/fs/transport.js index 1babd33..1298720 100644 --- a/shims/fs/transport.js +++ b/shims/fs/transport.js @@ -7,9 +7,11 @@ function normPath(p) { function uint8ToBase64(bytes) { let binary = ""; const chunk = 8192; + for (let i = 0; i < bytes.length; i += chunk) { binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk)); } + return btoa(binary); } @@ -23,7 +25,10 @@ async function request(method, endpoint, params = {}) { const options = { method }; if (method === "GET" || method === "DELETE") { - if (vaultId()) url.searchParams.set("vault", vaultId()); + if (vaultId()) { + url.searchParams.set("vault", vaultId()); + } + for (const [key, val] of Object.entries(params)) { url.searchParams.set(key, val); } @@ -41,6 +46,7 @@ async function request(method, endpoint, params = {}) { e.code = err.code || "UNKNOWN"; throw e; } + return res; } @@ -53,7 +59,10 @@ function requestSync(method, endpoint, params = {}) { const url = new URL(API_BASE + endpoint, window.location.origin); if (method === "GET" || method === "DELETE") { - if (vaultId()) url.searchParams.set("vault", vaultId()); + if (vaultId()) { + url.searchParams.set("vault", vaultId()); + } + for (const [key, val] of Object.entries(params)) { url.searchParams.set(key, val); } @@ -71,6 +80,7 @@ function requestSync(method, endpoint, params = {}) { if (xhr.status >= 400) { let err; + try { const body = JSON.parse(xhr.responseText); err = new Error(body.error || "Request failed"); @@ -79,6 +89,7 @@ function requestSync(method, endpoint, params = {}) { err = new Error("Request failed: " + xhr.status); err.code = "UNKNOWN"; } + throw err; } @@ -103,9 +114,11 @@ export const transport = { path: normPath(path), encoding: encoding || "", }); + if (encoding === "utf8" || encoding === "utf-8") { return res.text(); } + const buf = await res.arrayBuffer(); return new Uint8Array(buf); }, @@ -184,14 +197,18 @@ export const transport = { path: normPath(path), encoding: encoding || "", }); + if (encoding === "utf8" || encoding === "utf-8") { return xhr.responseText; } + const binary = xhr.responseText; const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { bytes[i] = binary.charCodeAt(i); } + return bytes; }, diff --git a/shims/loader.js b/shims/loader.js index 4f36d78..09cd108 100644 --- a/shims/loader.js +++ b/shims/loader.js @@ -20,7 +20,10 @@ const DEBUG = true; const _accessLog = new Map(); // "module.property" -> count function wrapWithProxy(obj, name) { - if (!DEBUG || !obj || typeof obj !== "object") return obj; + if (!DEBUG || !obj || typeof obj !== "object") { + return obj; + } + return new Proxy(obj, { get(target, prop) { if ( @@ -31,10 +34,12 @@ function wrapWithProxy(obj, name) { ) { const key = `${name}.${prop}`; _accessLog.set(key, (_accessLog.get(key) || 0) + 1); + if (!(prop in target)) { console.warn(`[shim:MISS] ${key} - property not found on shim`); } } + return target[prop]; }, }); @@ -44,6 +49,7 @@ window.__shimLog = function () { const sorted = [..._accessLog.entries()].sort((a, b) => b[1] - a[1]); console.table(sorted.map(([k, v]) => ({ api: k, calls: v }))); }; + window.__shimMisses = function () { const sorted = [..._accessLog.entries()] .filter(([k]) => { @@ -52,6 +58,7 @@ window.__shimMisses = function () { return shim && !(prop in shim); }) .sort((a, b) => b[1] - a[1]); + console.table(sorted.map(([k, v]) => ({ api: k, calls: v }))); }; @@ -82,9 +89,11 @@ window.require = function (moduleName) { if (throwOnRequire.has(moduleName)) { throw new Error(`Cannot find module '${moduleName}'`); } + if (shimRegistry[moduleName]) { return shimRegistry[moduleName]; } + console.warn("[ignis] Unshimmed require:", moduleName); return wrapWithProxy({}, `UNKNOWN(${moduleName})`); }; @@ -97,19 +106,23 @@ if (typeof window.Buffer === "undefined") { if (typeof data === "string") { return new TextEncoder().encode(data); } + if (data instanceof ArrayBuffer) { return new Uint8Array(data); } + return new Uint8Array(data); }, concat: function (arrays) { const total = arrays.reduce((sum, a) => sum + a.length, 0); const result = new Uint8Array(total); let offset = 0; + for (const arr of arrays) { result.set(arr, offset); offset += arr.length; } + return result; }, isBuffer: function (obj) { @@ -127,29 +140,37 @@ const _originalOpen = window.open; window.open = function (url, target, features) { if (url === "about:blank" || (features && features.includes("popup"))) { console.log("[ignis] intercepted popup:", url, features); + registerPopupWindow(); + const iframe = document.createElement("iframe"); iframe.style.cssText = "position:fixed;left:-9999px;width:0;height:0;border:none;"; + document.body.appendChild(iframe); window.__popupIframe = iframe; + const iframeWin = iframe.contentWindow; + iframeWin.require = window.require; iframeWin.module = window.module; iframeWin.Buffer = window.Buffer; iframeWin.process = window.process; iframeWin.global = iframeWin; iframeWin.globalEnhance = window.globalEnhance; + iframeWin.close = function () { unregisterPopupWindow(); iframe.remove(); window.__popupIframe = null; }; + return iframeWin; } return _originalOpen.call(window, url, target, features); }; +// hacky fix to prevent browser from showing context menu while allowing obsidian context menu window.addEventListener( "contextmenu", (e) => { @@ -167,17 +188,23 @@ window.__currentVaultId = _urlParams.get("vault") || ""; const vaultParam = window.__currentVaultId ? "?vault=" + encodeURIComponent(window.__currentVaultId) : ""; + const xhr = new XMLHttpRequest(); + xhr.open("GET", "/api/vault/info" + vaultParam, false); xhr.send(); + if (xhr.status === 200) { const info = JSON.parse(xhr.responseText); + window.__currentVaultId = info.id; window.__obsidianVersion = info.version || "0.0.0"; + window.__vaultConfig = { id: info.id, path: "/", }; + console.log("[ignis] Vault:", window.__vaultConfig); console.log("[ignis] Obsidian version:", window.__obsidianVersion); } else { @@ -191,8 +218,10 @@ window.__currentVaultId = _urlParams.get("vault") || ""; (function initVaultList() { try { const xhr = new XMLHttpRequest(); + xhr.open("GET", "/api/vault/list", false); xhr.send(); + if (xhr.status === 200) { window.__vaultList = JSON.parse(xhr.responseText); } @@ -206,14 +235,19 @@ window.__currentVaultId = _urlParams.get("vault") || ""; const vaultParam = window.__currentVaultId ? "?vault=" + encodeURIComponent(window.__currentVaultId) : ""; + const xhr = new XMLHttpRequest(); + xhr.open("GET", "/api/fs/tree" + vaultParam, false); xhr.send(); + if (xhr.status === 200) { const tree = JSON.parse(xhr.responseText); + fsShim._metadataCache.populate(tree); fsShim._metadataCache.set("", { type: "directory" }); fsShim._metadataCache.set("/", { type: "directory" }); + console.log( "[ignis] Metadata cache populated:", fsShim._metadataCache.size, diff --git a/shims/node/events.js b/shims/node/events.js index 6f12da2..69942d7 100644 --- a/shims/node/events.js +++ b/shims/node/events.js @@ -4,8 +4,12 @@ export class EventEmitter { } on(event, listener) { - if (!this._events[event]) this._events[event] = []; + if (!this._events[event]) { + this._events[event] = []; + } + this._events[event].push(listener); + return this; } @@ -14,24 +18,39 @@ export class EventEmitter { this.removeListener(event, wrapped); listener.apply(this, args); }; + wrapped._original = listener; return this.on(event, wrapped); } emit(event, ...args) { const listeners = this._events[event]; - if (!listeners || listeners.length === 0) return false; + + if (!listeners || listeners.length === 0) { + return false; + } + for (const fn of [...listeners]) { fn.apply(this, args); } + return true; } removeListener(event, listener) { const arr = this._events[event]; - if (!arr) return this; - const idx = arr.findIndex((fn) => fn === listener || fn._original === listener); - if (idx >= 0) arr.splice(idx, 1); + if (!arr) { + return this; + } + + const idx = arr.findIndex( + (fn) => fn === listener || fn._original === listener, + ); + + if (idx >= 0) { + arr.splice(idx, 1); + } + return this; } @@ -45,6 +64,7 @@ export class EventEmitter { } else { this._events = {}; } + return this; } @@ -61,8 +81,12 @@ export class EventEmitter { } prependListener(event, listener) { - if (!this._events[event]) this._events[event] = []; + if (!this._events[event]) { + this._events[event] = []; + } + this._events[event].unshift(listener); + return this; } diff --git a/shims/node/http.js b/shims/node/http.js index c11e85c..85e6f42 100644 --- a/shims/node/http.js +++ b/shims/node/http.js @@ -15,6 +15,7 @@ export class ClientRequest extends EventEmitter { constructor() { super(); } + end() {} write() {} abort() {} @@ -26,6 +27,7 @@ export function request(options, callback) { if (callback) { req.once("response", callback); } + // Immediately error. real HTTP requests need fetch or the proxy setTimeout(() => { req.emit( diff --git a/shims/path.js b/shims/path.js index 776c32d..12788ba 100644 --- a/shims/path.js +++ b/shims/path.js @@ -12,6 +12,7 @@ export const pathShim = { if (p === "/" && window.__currentVaultId) { return window.__currentVaultId; } + return _origBasename(p, ext); }, }; diff --git a/shims/request-url.js b/shims/request-url.js index 57ebedd..b67b025 100644 --- a/shims/request-url.js +++ b/shims/request-url.js @@ -1,13 +1,14 @@ -// Override window.requestUrl to proxy external requests through our server, -// bypassing browser CORS restrictions. Obsidian sets window.requestUrl = UA -// in app.js, so we override it after app.js loads. +// Override window.requestUrl to proxy external requests through our server, bypassing CORS. +// Obsidian sets window.requestUrl in app.js, so we override it after app.js loads. function base64ToArrayBuffer(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.buffer; } @@ -15,9 +16,11 @@ function arrayBufferToBase64(buf) { const bytes = new Uint8Array(buf); let binary = ""; const chunk = 8192; + for (let i = 0; i < bytes.length; i += chunk) { binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk)); } + return btoa(binary); } @@ -37,7 +40,9 @@ async function proxyRequestUrl(request) { headers: request.headers || {}, body: request.body, }); + const arrayBuf = await res.arrayBuffer(); + return makeResponse( request, res.status, @@ -49,6 +54,7 @@ async function proxyRequestUrl(request) { // Cross-origin: route through server proxy let body = request.body; let binary = false; + if (body instanceof ArrayBuffer) { body = arrayBufferToBase64(body); binary = true; @@ -75,6 +81,7 @@ async function proxyRequestUrl(request) { const proxyResult = await res.json(); const arrayBuf = base64ToArrayBuffer(proxyResult.body); + return makeResponse( request, proxyResult.status, @@ -86,11 +93,13 @@ async function proxyRequestUrl(request) { function makeResponse(request, status, headers, arrayBuf) { const text = new TextDecoder().decode(arrayBuf); let json; + try { json = JSON.parse(text); } catch { json = null; } + return { status, headers, arrayBuffer: arrayBuf, text, json }; } diff --git a/shims/ui/vault-manager.js b/shims/ui/vault-manager.js index 132e697..f6799c4 100644 --- a/shims/ui/vault-manager.js +++ b/shims/ui/vault-manager.js @@ -1,5 +1,4 @@ // Custom vault manager modal. will migrate to Svelte later -// Shows list of vaults, create new, delete, switch. export function showVaultManager() { if (!document.querySelector(".workspace")) return; diff --git a/shims/url.js b/shims/url.js index 272e307..117907b 100644 --- a/shims/url.js +++ b/shims/url.js @@ -6,16 +6,19 @@ export const urlShim = { // Return an object with .href matching Node's url.pathToFileURL behavior const encoded = encodeURI(p.replace(/\\/g, "/")); const href = "file:///" + encoded.replace(/^\/+/, ""); + return { href, toString: () => href }; }, fileURLToPath(url) { let str = typeof url === "string" ? url : url.href || url.toString(); + if (str.startsWith("file:///")) { str = str.slice(8); } else if (str.startsWith("file://")) { str = str.slice(7); } + return decodeURI(str); }, };