some styling cleanup

This commit is contained in:
Nystik 2026-03-17 12:38:30 +01:00
parent c70b9e9d0f
commit 0738c47ac5
27 changed files with 479 additions and 105 deletions

View file

@ -3,7 +3,7 @@ const fs = require("fs");
// VAULT_ROOT: a directory that contains vault folders. // VAULT_ROOT: a directory that contains vault folders.
// Each subdirectory is a vault. New vaults are created as new subdirs. // 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 = const vaultRoot =
process.env.VAULT_ROOT || process.env.VAULT_ROOT ||
(process.env.VAULT_PATH (process.env.VAULT_PATH
@ -19,8 +19,10 @@ try {
function discoverVaults() { function discoverVaults() {
const vaults = {}; const vaults = {};
try { try {
const entries = fs.readdirSync(vaultRoot, { withFileTypes: true }); const entries = fs.readdirSync(vaultRoot, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith(".")) { if (entry.isDirectory() && !entry.name.startsWith(".")) {
vaults[entry.name] = path.join(vaultRoot, entry.name); vaults[entry.name] = path.join(vaultRoot, entry.name);
@ -29,12 +31,15 @@ function discoverVaults() {
} catch (e) { } catch (e) {
console.error("[config] Failed to read VAULT_ROOT:", vaultRoot, e.message); console.error("[config] Failed to read VAULT_ROOT:", vaultRoot, e.message);
} }
// Create a default vault if none exist // Create a default vault if none exist
if (Object.keys(vaults).length === 0) { if (Object.keys(vaults).length === 0) {
const defaultPath = path.join(vaultRoot, "My Vault"); const defaultPath = path.join(vaultRoot, "My Vault");
try { try {
fs.mkdirSync(path.join(defaultPath, ".obsidian"), { recursive: true }); fs.mkdirSync(path.join(defaultPath, ".obsidian"), { recursive: true });
vaults["My Vault"] = defaultPath; vaults["My Vault"] = defaultPath;
console.log("[config] Created default vault: My Vault"); console.log("[config] Created default vault: My Vault");
} catch (e) { } catch (e) {
console.error("[config] Failed to create default vault:", e.message); console.error("[config] Failed to create default vault:", e.message);

View file

@ -3,28 +3,39 @@ const path = require("path");
const config = require("./config"); const config = require("./config");
const { setupWebSocket } = require("./ws"); 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(); const app = express();
app.use(express.json({ limit: "50mb" })); app.use(express.json({ limit: "50mb" }));
// logger middleware
app.use((req, res, next) => { app.use((req, res, next) => {
const start = Date.now(); const start = Date.now();
const origEnd = res.end; const origEnd = res.end;
res.end = function (...args) { res.end = function (...args) {
const duration = Date.now() - start; const duration = Date.now() - start;
const status = res.statusCode; const status = res.statusCode;
const color = const color =
status >= 500 ? "\x1b[31m" : status >= 400 ? "\x1b[33m" : "\x1b[32m"; status >= 500 ? ANSI_RED : status >= 400 ? ANSI_YELLOW : ANSI_GREEN;
const reset = "\x1b[0m";
const path = const path =
req.originalUrl.length > 80 req.originalUrl.length > 80
? req.originalUrl.slice(0, 80) + "..." ? req.originalUrl.slice(0, 80) + "..."
: req.originalUrl; : req.originalUrl;
console.log( console.log(
`${color}${req.method} ${status}${reset} ${path} (${duration}ms)`, `${color}${req.method} ${status}${ANSI_RESET} ${path} (${duration}ms)`,
); );
origEnd.apply(this, args); origEnd.apply(this, args);
}; };
next(); next();
}); });
@ -41,11 +52,18 @@ app.use("/api/proxy", proxyRoutes);
app.use("/vault-files", (req, res, next) => { app.use("/vault-files", (req, res, next) => {
// Extract vault ID from the first path segment // Extract vault ID from the first path segment
const parts = req.path.split("/").filter(Boolean); const parts = req.path.split("/").filter(Boolean);
if (parts.length === 0)
if (parts.length === 0) {
return res.status(400).json({ error: "Missing vault ID" }); return res.status(400).json({ error: "Missing vault ID" });
}
const vaultId = decodeURIComponent(parts[0]); const vaultId = decodeURIComponent(parts[0]);
const vaultPath = config.getVaultPath(vaultId); 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 // Rewrite req.url to strip the vault ID prefix, then serve statically
req.url = "/" + parts.slice(1).join("/"); req.url = "/" + parts.slice(1).join("/");
express.static(vaultPath)(req, res, next); express.static(vaultPath)(req, res, next);

View file

@ -9,6 +9,7 @@ const router = express.Router();
function getVaultRoot(req, res) { function getVaultRoot(req, res) {
const vaultId = req.query.vault || req.body?.vault || config.defaultVaultId; const vaultId = req.query.vault || req.body?.vault || config.defaultVaultId;
const vaultPath = config.getVaultPath(vaultId); const vaultPath = config.getVaultPath(vaultId);
if (!vaultPath) { if (!vaultPath) {
res.status(404).json({ error: "Vault not found", id: vaultId }); res.status(404).json({ error: "Vault not found", id: vaultId });
return null; return null;
@ -21,39 +22,35 @@ function getVaultRoot(req, res) {
function resolveVaultPath(vaultRoot, relativePath) { function resolveVaultPath(vaultRoot, relativePath) {
const cleaned = (relativePath || "").replace(/^\/+/, ""); const cleaned = (relativePath || "").replace(/^\/+/, "");
const resolved = path.resolve(vaultRoot, cleaned); const resolved = path.resolve(vaultRoot, cleaned);
if (!resolved.startsWith(path.resolve(vaultRoot))) { if (!resolved.startsWith(path.resolve(vaultRoot))) {
return null; return null;
} }
return resolved; return resolved;
} }
function guardPath(req, res) { function guardPath(req, res, source = "query") {
const vaultRoot = getVaultRoot(req, res); 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) { if (p === undefined || p === null) {
res.status(400).json({ error: "Missing path parameter" }); res.status(400).json({ error: "Missing path parameter" });
return null; return null;
} }
// Empty string = vault root, which is valid // Empty string = vault root, which is valid
const resolved = resolveVaultPath(vaultRoot, p); const resolved = resolveVaultPath(vaultRoot, p);
if (!resolved) { if (!resolved) {
res.status(403).json({ error: "Path traversal rejected" }); res.status(403).json({ error: "Path traversal rejected" });
return null; 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; req._vaultRoot = vaultRoot;
return resolved; return resolved;
} }
@ -61,9 +58,14 @@ function guardBodyPath(req, res) {
// GET /api/fs/stat?path=... // GET /api/fs/stat?path=...
router.get("/stat", async (req, res) => { router.get("/stat", async (req, res) => {
const resolved = guardPath(req, res); const resolved = guardPath(req, res);
if (!resolved) return;
if (!resolved) {
return;
}
try { try {
const stat = await fs.promises.stat(resolved); const stat = await fs.promises.stat(resolved);
res.json({ res.json({
type: stat.isDirectory() ? "directory" : "file", type: stat.isDirectory() ? "directory" : "file",
size: stat.size, size: stat.size,
@ -80,18 +82,25 @@ router.get("/stat", async (req, res) => {
// GET /api/fs/readdir?path=... // GET /api/fs/readdir?path=...
router.get("/readdir", async (req, res) => { router.get("/readdir", async (req, res) => {
const resolved = guardPath(req, res); const resolved = guardPath(req, res);
if (!resolved) return;
if (!resolved) {
return;
}
try { try {
// Check if path is a file. return ENOTDIR instead of crashing // Check if path is a file. return ENOTDIR instead of crashing
const stat = await fs.promises.stat(resolved); const stat = await fs.promises.stat(resolved);
if (!stat.isDirectory()) { if (!stat.isDirectory()) {
return res return res
.status(400) .status(400)
.json({ error: "ENOTDIR: not a directory", code: "ENOTDIR" }); .json({ error: "ENOTDIR: not a directory", code: "ENOTDIR" });
} }
const entries = await fs.promises.readdir(resolved, { const entries = await fs.promises.readdir(resolved, {
withFileTypes: true, withFileTypes: true,
}); });
res.json( res.json(
entries.map((e) => ({ entries.map((e) => ({
name: e.name, name: e.name,
@ -108,22 +117,30 @@ router.get("/readdir", async (req, res) => {
// GET /api/fs/readFile?path=...&encoding=... // GET /api/fs/readFile?path=...&encoding=...
router.get("/readFile", async (req, res) => { router.get("/readFile", async (req, res) => {
const resolved = guardPath(req, res); const resolved = guardPath(req, res);
if (!resolved) return;
if (!resolved) {
return;
}
try { try {
// Check if path is a directory
const stat = await fs.promises.stat(resolved); const stat = await fs.promises.stat(resolved);
if (stat.isDirectory()) { if (stat.isDirectory()) {
return res.status(400).json({ return res.status(400).json({
error: "EISDIR: illegal operation on a directory", error: "EISDIR: illegal operation on a directory",
code: "EISDIR", code: "EISDIR",
}); });
} }
const encoding = req.query.encoding; const encoding = req.query.encoding;
if (encoding === "utf8" || encoding === "utf-8") { if (encoding === "utf8" || encoding === "utf-8") {
const data = await fs.promises.readFile(resolved, "utf-8"); const data = await fs.promises.readFile(resolved, "utf-8");
res.type("text/plain").send(data); res.type("text/plain").send(data);
} else { } else {
const data = await fs.promises.readFile(resolved); const data = await fs.promises.readFile(resolved);
res.type("application/octet-stream").send(data); res.type("application/octet-stream").send(data);
} }
} catch (e) { } catch (e) {
@ -135,8 +152,12 @@ router.get("/readFile", async (req, res) => {
// POST /api/fs/writeFile { path, content, encoding?, vault? } // POST /api/fs/writeFile { path, content, encoding?, vault? }
router.post("/writeFile", async (req, res) => { router.post("/writeFile", async (req, res) => {
const resolved = guardBodyPath(req, res); const resolved = guardPath(req, res, "body");
if (!resolved) return;
if (!resolved) {
return;
}
try { try {
// Ensure parent directory exists // Ensure parent directory exists
const dir = path.dirname(resolved); const dir = path.dirname(resolved);
@ -144,15 +165,19 @@ router.post("/writeFile", async (req, res) => {
const encoding = req.body.encoding || "utf-8"; const encoding = req.body.encoding || "utf-8";
let data = req.body.content; let data = req.body.content;
if (req.body.base64) { if (req.body.base64) {
data = Buffer.from(req.body.content, "base64"); data = Buffer.from(req.body.content, "base64");
} }
await fs.promises.writeFile( await fs.promises.writeFile(
resolved, resolved,
data, data,
encoding === "binary" ? undefined : encoding, encoding === "binary" ? undefined : encoding,
); );
const stat = await fs.promises.stat(resolved); const stat = await fs.promises.stat(resolved);
res.json({ ok: true, mtime: stat.mtimeMs, size: stat.size }); res.json({ ok: true, mtime: stat.mtimeMs, size: stat.size });
} catch (e) { } catch (e) {
res.status(500).json({ error: e.message, code: e.code }); 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? } // POST /api/fs/appendFile { path, content, vault? }
router.post("/appendFile", async (req, res) => { router.post("/appendFile", async (req, res) => {
const resolved = guardBodyPath(req, res); const resolved = guardPath(req, res, "body");
if (!resolved) return;
if (!resolved) {
return;
}
try { try {
await fs.promises.appendFile(resolved, req.body.content, "utf-8"); await fs.promises.appendFile(resolved, req.body.content, "utf-8");
res.json({ ok: true }); res.json({ ok: true });
} catch (e) { } catch (e) {
res.status(500).json({ error: e.message, code: e.code }); 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? } // POST /api/fs/mkdir { path, recursive?, vault? }
router.post("/mkdir", async (req, res) => { router.post("/mkdir", async (req, res) => {
const resolved = guardBodyPath(req, res); const resolved = guardPath(req, res, "body");
if (!resolved) return;
if (!resolved) {
return;
}
try { try {
await fs.promises.mkdir(resolved, { recursive: !!req.body.recursive }); await fs.promises.mkdir(resolved, { recursive: !!req.body.recursive });
res.json({ ok: true }); res.json({ ok: true });
} catch (e) { } catch (e) {
res.status(500).json({ error: e.message, code: e.code }); 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? } // POST /api/fs/rename { oldPath, newPath, vault? }
router.post("/rename", async (req, res) => { router.post("/rename", async (req, res) => {
const vaultRoot = getVaultRoot(req, res); const vaultRoot = getVaultRoot(req, res);
if (!vaultRoot) return;
if (!vaultRoot) {
return;
}
const oldResolved = resolveVaultPath(vaultRoot, req.body?.oldPath); const oldResolved = resolveVaultPath(vaultRoot, req.body?.oldPath);
const newResolved = resolveVaultPath(vaultRoot, req.body?.newPath); const newResolved = resolveVaultPath(vaultRoot, req.body?.newPath);
if (!oldResolved || !newResolved)
if (!oldResolved || !newResolved) {
return res.status(403).json({ error: "Invalid path" }); return res.status(403).json({ error: "Invalid path" });
}
try { try {
await fs.promises.rename(oldResolved, newResolved); await fs.promises.rename(oldResolved, newResolved);
res.json({ ok: true }); res.json({ ok: true });
} catch (e) { } catch (e) {
res.status(500).json({ error: e.message, code: e.code }); 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? } // POST /api/fs/copyFile { src, dest, vault? }
router.post("/copyFile", async (req, res) => { router.post("/copyFile", async (req, res) => {
const vaultRoot = getVaultRoot(req, res); const vaultRoot = getVaultRoot(req, res);
if (!vaultRoot) return;
if (!vaultRoot) {
return;
}
const srcResolved = resolveVaultPath(vaultRoot, req.body?.src); const srcResolved = resolveVaultPath(vaultRoot, req.body?.src);
const destResolved = resolveVaultPath(vaultRoot, req.body?.dest); const destResolved = resolveVaultPath(vaultRoot, req.body?.dest);
if (!srcResolved || !destResolved)
if (!srcResolved || !destResolved) {
return res.status(403).json({ error: "Invalid path" }); return res.status(403).json({ error: "Invalid path" });
}
try { try {
await fs.promises.copyFile(srcResolved, destResolved); await fs.promises.copyFile(srcResolved, destResolved);
res.json({ ok: true }); res.json({ ok: true });
} catch (e) { } catch (e) {
res.status(500).json({ error: e.message, code: e.code }); 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=... // DELETE /api/fs/unlink?path=...
router.delete("/unlink", async (req, res) => { router.delete("/unlink", async (req, res) => {
const resolved = guardPath(req, res); const resolved = guardPath(req, res);
if (!resolved) return;
if (!resolved) {
return;
}
try { try {
await fs.promises.unlink(resolved); await fs.promises.unlink(resolved);
res.json({ ok: true }); res.json({ ok: true });
} catch (e) { } catch (e) {
const status = e.code === "ENOENT" ? 404 : 500; const status = e.code === "ENOENT" ? 404 : 500;
res.status(status).json({ error: e.message, code: e.code }); 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=... // DELETE /api/fs/rmdir?path=...
router.delete("/rmdir", async (req, res) => { router.delete("/rmdir", async (req, res) => {
const resolved = guardPath(req, res); const resolved = guardPath(req, res);
if (!resolved) return;
if (!resolved) {
return;
}
try { try {
await fs.promises.rmdir(resolved); await fs.promises.rmdir(resolved);
res.json({ ok: true }); res.json({ ok: true });
} catch (e) { } catch (e) {
res.status(500).json({ error: e.message, code: e.code }); 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 // DELETE /api/fs/rm?path=...&recursive=true
router.delete("/rm", async (req, res) => { router.delete("/rm", async (req, res) => {
const resolved = guardPath(req, res); const resolved = guardPath(req, res);
if (!resolved) return;
if (!resolved) {
return;
}
try { try {
await fs.promises.rm(resolved, { await fs.promises.rm(resolved, {
recursive: req.query.recursive === "true", 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) => { router.get("/access", async (req, res) => {
const resolved = guardPath(req, res); const resolved = guardPath(req, res);
if (!resolved) return;
if (!resolved) {
return;
}
try { try {
await fs.promises.access(resolved); await fs.promises.access(resolved);
res.json({ ok: true }); res.json({ ok: true });
} catch (e) { } catch (e) {
res res
@ -268,13 +338,16 @@ router.get("/access", async (req, res) => {
} }
}); });
// GET /api/fs/realpath?path=...
router.get("/realpath", async (req, res) => { router.get("/realpath", async (req, res) => {
const resolved = guardPath(req, res); const resolved = guardPath(req, res);
if (!resolved) return;
if (!resolved) {
return;
}
try { try {
const real = await fs.promises.realpath(resolved); const real = await fs.promises.realpath(resolved);
// Return path relative to vault root
res.json({ path: path.relative(req._vaultRoot, real) }); res.json({ path: path.relative(req._vaultRoot, real) });
} catch (e) { } catch (e) {
res.status(500).json({ error: e.message, code: e.code }); 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? } // POST /api/fs/utimes { path, atime, mtime, vault? }
router.post("/utimes", async (req, res) => { router.post("/utimes", async (req, res) => {
const resolved = guardBodyPath(req, res); const resolved = guardPath(req, res, "body");
if (!resolved) return;
if (!resolved) {
return;
}
try { try {
await fs.promises.utimes( await fs.promises.utimes(
resolved, resolved,
@ -300,23 +377,36 @@ router.post("/utimes", async (req, res) => {
// GET /api/fs/tree?path=...&vault=... returns full recursive file tree with metadata // GET /api/fs/tree?path=...&vault=... returns full recursive file tree with metadata
router.get("/tree", async (req, res) => { router.get("/tree", async (req, res) => {
const vaultRoot = getVaultRoot(req, res); const vaultRoot = getVaultRoot(req, res);
if (!vaultRoot) return;
if (!vaultRoot) {
return;
}
const rootPath = req.query.path const rootPath = req.query.path
? resolveVaultPath(vaultRoot, req.query.path) ? resolveVaultPath(vaultRoot, req.query.path)
: vaultRoot; : vaultRoot;
if (!rootPath) return res.status(403).json({ error: "Invalid path" });
if (!rootPath) {
return res.status(403).json({ error: "Invalid path" });
}
try { try {
const tree = {}; const tree = {};
async function walk(dir, prefix) { async function walk(dir, prefix) {
const entries = await fs.promises.readdir(dir, { withFileTypes: true }); const entries = await fs.promises.readdir(dir, { withFileTypes: true });
for (const entry of entries) { for (const entry of entries) {
const rel = prefix ? prefix + "/" + entry.name : entry.name; const rel = prefix ? prefix + "/" + entry.name : entry.name;
const full = path.join(dir, entry.name); const full = path.join(dir, entry.name);
if (entry.isDirectory()) { if (entry.isDirectory()) {
tree[rel] = { type: "directory" }; tree[rel] = { type: "directory" };
await walk(full, rel); await walk(full, rel);
} else { } else {
const stat = await fs.promises.stat(full); const stat = await fs.promises.stat(full);
tree[rel] = { tree[rel] = {
type: "file", type: "file",
size: stat.size, size: stat.size,
@ -326,7 +416,9 @@ router.get("/tree", async (req, res) => {
} }
} }
} }
await walk(rootPath, ""); await walk(rootPath, "");
res.json(tree); res.json(tree);
} catch (e) { } catch (e) {
res.status(500).json({ error: e.message, code: e.code }); res.status(500).json({ error: e.message, code: e.code });

View file

@ -2,10 +2,11 @@ const express = require("express");
const router = express.Router(); const router = express.Router();
// POST /api/proxy - forward a request to an external URL (bypasses browser CORS) // POST /api/proxy - forward a request to an external URL to bypass CORS
// Used by the requestUrl shim for plugin installation, update checks, etc. // Used by the requestUrl shim for plugin installation, etc.
router.post("/", async (req, res) => { router.post("/", async (req, res) => {
const { url, method, headers, body, binary } = req.body; const { url, method, headers, body, binary } = req.body;
if (!url) { if (!url) {
return res.status(400).json({ error: "Missing url" }); return res.status(400).json({ error: "Missing url" });
} }
@ -15,6 +16,7 @@ router.post("/", async (req, res) => {
method: method || "GET", method: method || "GET",
headers: headers || {}, headers: headers || {},
}; };
if (body && method !== "GET" && method !== "HEAD") { if (body && method !== "GET" && method !== "HEAD") {
if (binary && typeof body === "string") { if (binary && typeof body === "string") {
fetchOpts.body = Buffer.from(body, "base64"); fetchOpts.body = Buffer.from(body, "base64");

View file

@ -8,11 +8,13 @@ const router = express.Router();
// GET /api/vault/list - returns all discovered vaults (re-scans on each call) // GET /api/vault/list - returns all discovered vaults (re-scans on each call)
router.get("/list", (req, res) => { router.get("/list", (req, res) => {
config.refreshVaults(); config.refreshVaults();
const list = Object.entries(config.vaults).map(([id, vaultPath]) => ({ const list = Object.entries(config.vaults).map(([id, vaultPath]) => ({
id, id,
name: id, name: id,
path: vaultPath, path: vaultPath,
})); }));
res.json(list); res.json(list);
}); });
@ -20,9 +22,11 @@ router.get("/list", (req, res) => {
router.get("/info", (req, res) => { router.get("/info", (req, res) => {
const vaultId = req.query.vault || config.defaultVaultId; const vaultId = req.query.vault || config.defaultVaultId;
const vaultPath = config.getVaultPath(vaultId); const vaultPath = config.getVaultPath(vaultId);
if (!vaultPath) { if (!vaultPath) {
return res.status(404).json({ error: "Vault not found", id: vaultId }); return res.status(404).json({ error: "Vault not found", id: vaultId });
} }
res.json({ res.json({
id: vaultId, id: vaultId,
name: vaultId, name: vaultId,
@ -35,21 +39,27 @@ router.get("/info", (req, res) => {
// POST /api/vault/create { name } - create a new vault in VAULT_ROOT // POST /api/vault/create { name } - create a new vault in VAULT_ROOT
router.post("/create", async (req, res) => { router.post("/create", async (req, res) => {
const name = req.body?.name; const name = req.body?.name;
if (!name || /[\/\\:*?"<>|]/.test(name)) { if (!name || /[\/\\:*?"<>|]/.test(name)) {
return res.status(400).json({ error: "Invalid vault name" }); return res.status(400).json({ error: "Invalid vault name" });
} }
const vaultPath = path.join(config.vaultRoot, name); const vaultPath = path.join(config.vaultRoot, name);
try { try {
await fs.promises.mkdir(vaultPath, { recursive: false }); await fs.promises.mkdir(vaultPath, { recursive: false });
await fs.promises.mkdir(path.join(vaultPath, ".obsidian"), { await fs.promises.mkdir(path.join(vaultPath, ".obsidian"), {
recursive: false, recursive: false,
}); });
config.refreshVaults(); config.refreshVaults();
res.json({ ok: true, id: name, path: vaultPath }); res.json({ ok: true, id: name, path: vaultPath });
} catch (e) { } catch (e) {
if (e.code === "EEXIST") { if (e.code === "EEXIST") {
return res.status(409).json({ error: "Vault already exists" }); return res.status(409).json({ error: "Vault already exists" });
} }
res.status(500).json({ error: e.message, code: e.code }); 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) => { router.delete("/remove", async (req, res) => {
const vaultId = req.query.vault; const vaultId = req.query.vault;
const vaultPath = config.getVaultPath(vaultId); const vaultPath = config.getVaultPath(vaultId);
if (!vaultPath) { if (!vaultPath) {
return res.status(404).json({ error: "Vault not found" }); return res.status(404).json({ error: "Vault not found" });
} }
try { try {
await fs.promises.rm(vaultPath, { recursive: true }); await fs.promises.rm(vaultPath, { recursive: true });
config.refreshVaults(); config.refreshVaults();
res.json({ ok: true }); res.json({ ok: true });
} catch (e) { } catch (e) {
res.status(500).json({ error: e.message, code: e.code }); res.status(500).json({ error: e.message, code: e.code });

View file

@ -1,24 +1,24 @@
const { WebSocketServer } = require('ws'); const { WebSocketServer } = require("ws");
//currently unused
function setupWebSocket(server) { function setupWebSocket(server) {
const wss = new WebSocketServer({ server, path: '/ws' }); const wss = new WebSocketServer({ server, path: "/ws" });
wss.on('connection', (ws) => { wss.on("connection", (ws) => {
console.log('[ws] Client connected'); console.log("[ws] Client connected");
ws.on('message', (data) => { ws.on("message", (data) => {
// TODO: handle watch/unwatch subscriptions from client // TODO: handle watch/unwatch subscriptions from client
const msg = JSON.parse(data); const msg = JSON.parse(data);
console.log('[ws] Received:', msg); console.log("[ws] Received:", msg);
}); });
ws.on('close', () => { ws.on("close", () => {
console.log('[ws] Client disconnected'); console.log("[ws] Client disconnected");
}); });
}); });
// TODO: integrate chokidar file watching and broadcast changes // TODO: maybe integrate chokidar file watching and broadcast changes
// This will be implemented once the sync strategy is finalized.
return wss; return wss;
} }

View file

@ -1,5 +1,6 @@
export function createHash(algorithm) { export function createHash(algorithm) {
const alg = algorithm.toUpperCase().replace("-", ""); const alg = algorithm.toUpperCase().replace("-", "");
const subtleAlg = const subtleAlg =
alg === "SHA256" alg === "SHA256"
? "SHA-256" ? "SHA-256"
@ -16,32 +17,47 @@ export function createHash(algorithm) {
if (typeof data === "string") { if (typeof data === "string") {
data = new TextEncoder().encode(data); data = new TextEncoder().encode(data);
} }
const merged = new Uint8Array(inputData.length + data.length); const merged = new Uint8Array(inputData.length + data.length);
merged.set(inputData); merged.set(inputData);
merged.set(data, inputData.length); merged.set(data, inputData.length);
inputData = merged; inputData = merged;
return this; return this;
}, },
digest(encoding) { digest(encoding) {
console.warn("[shim:crypto] createHash.digest - using placeholder"); console.warn("[shim:crypto] createHash.digest - using placeholder");
const hash = simpleHash(inputData); 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; return hash;
}, },
async digestAsync(encoding) { async digestAsync(encoding) {
const hashBuffer = await crypto.subtle.digest(subtleAlg, inputData); const hashBuffer = await crypto.subtle.digest(subtleAlg, inputData);
const hashArray = new Uint8Array(hashBuffer); const hashArray = new Uint8Array(hashBuffer);
if (encoding === "hex") { if (encoding === "hex") {
return Array.from(hashArray) return Array.from(hashArray)
.map((b) => b.toString(16).padStart(2, "0")) .map((b) => b.toString(16).padStart(2, "0"))
.join(""); .join("");
} }
if (encoding === "base64") { if (encoding === "base64") {
return btoa(String.fromCharCode(...hashArray)); return btoa(String.fromCharCode(...hashArray));
} }
return hashArray; return hashArray;
}, },
}; };
@ -49,8 +65,10 @@ export function createHash(algorithm) {
function simpleHash(data) { function simpleHash(data) {
let hash = 0; let hash = 0;
for (let i = 0; i < data.length; i++) { for (let i = 0; i < data.length; i++) {
hash = ((hash << 5) - hash + data[i]) | 0; hash = ((hash << 5) - hash + data[i]) | 0;
} }
return Math.abs(hash).toString(16).padStart(8, "0"); return Math.abs(hash).toString(16).padStart(8, "0");
} }

View file

@ -8,9 +8,11 @@ export function randomBytes(size) {
.map((b) => b.toString(16).padStart(2, "0")) .map((b) => b.toString(16).padStart(2, "0"))
.join(""); .join("");
} }
if (encoding === "base64") { if (encoding === "base64") {
return btoa(String.fromCharCode(...this)); return btoa(String.fromCharCode(...this));
} }
return new TextDecoder().decode(this); return new TextDecoder().decode(this);
}; };

View file

@ -6,31 +6,40 @@ const syncHandlers = {
vault: () => window.__vaultConfig || { id: "default-vault", path: "/" }, vault: () => window.__vaultConfig || { id: "default-vault", path: "/" },
version: () => window.__obsidianVersion || "0.0.0", version: () => window.__obsidianVersion || "0.0.0",
"is-dev": () => false, "is-dev": () => false,
"file-url": () => "file-url": () =>
"/vault-files/" + encodeURIComponent(window.__currentVaultId || "") + "/", "/vault-files/" + encodeURIComponent(window.__currentVaultId || "") + "/",
"disable-update": () => true, "disable-update": () => true,
update: () => "", update: () => "",
"disable-gpu": () => false, "disable-gpu": () => false,
frame: () => null, frame: () => null,
"set-icon": () => null, "set-icon": () => null,
"get-icon": () => null, "get-icon": () => null,
relaunch: () => { relaunch: () => {
window.location.reload(); window.location.reload();
return null; return null;
}, },
starter: () => { starter: () => {
showVaultManager(); showVaultManager();
return null; return null;
}, },
help: () => { help: () => {
window.open("https://help.obsidian.md/", "_blank"); window.open("https://help.obsidian.md/", "_blank");
return null; return null;
}, },
sandbox: () => null, sandbox: () => null,
"copy-asar": () => false, "copy-asar": () => false,
"check-update": () => null, "check-update": () => null,
"vault-list": () => { "vault-list": () => {
const result = {}; const result = {};
for (const v of window.__vaultList || []) { for (const v of window.__vaultList || []) {
result[v.id] = { result[v.id] = {
path: "/" + v.id, path: "/" + v.id,
@ -38,36 +47,51 @@ const syncHandlers = {
open: v.id === (window.__currentVaultId || ""), open: v.id === (window.__currentVaultId || ""),
}; };
} }
return result; return result;
}, },
"vault-open": (vaultPath, newWindow) => { "vault-open": (vaultPath, newWindow) => {
const id = (vaultPath || "").replace(/^\/+/, ""); const id = (vaultPath || "").replace(/^\/+/, "");
const vault = (window.__vaultList || []).find((v) => v.id === id); const vault = (window.__vaultList || []).find((v) => v.id === id);
if (!vault && id) { if (!vault && id) {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open("POST", "/api/vault/create", false); xhr.open("POST", "/api/vault/create", false);
xhr.setRequestHeader("Content-Type", "application/json"); xhr.setRequestHeader("Content-Type", "application/json");
xhr.send(JSON.stringify({ name: id })); 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; const target = window.parent !== window ? window.parent : window;
target.location.href = "/?vault=" + encodeURIComponent(id); target.location.href = "/?vault=" + encodeURIComponent(id);
return true; return true;
}, },
"vault-remove": (vaultPath) => { "vault-remove": (vaultPath) => {
const id = (vaultPath || "").replace(/^\/+/, ""); const id = (vaultPath || "").replace(/^\/+/, "");
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open( xhr.open(
"DELETE", "DELETE",
"/api/vault/remove?vault=" + encodeURIComponent(id), "/api/vault/remove?vault=" + encodeURIComponent(id),
false, false,
); );
xhr.send(); xhr.send();
return xhr.status < 400; return xhr.status < 400;
}, },
"vault-move": (oldPath, newPath) => { "vault-move": (oldPath, newPath) => {
return "Moving vaults is not supported in the web version"; return "Moving vaults is not supported in the web version";
}, },
"vault-message": () => null, "vault-message": () => null,
"get-default-vault-path": () => "/My Vault", "get-default-vault-path": () => "/My Vault",
"get-documents-path": () => "/", "get-documents-path": () => "/",
@ -80,18 +104,22 @@ function arrayBufferToBase64(buf) {
const bytes = new Uint8Array(buf); const bytes = new Uint8Array(buf);
let binary = ""; let binary = "";
const chunk = 8192; const chunk = 8192;
for (let i = 0; i < bytes.length; i += chunk) { for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk)); binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
} }
return btoa(binary); return btoa(binary);
} }
function base64ToArrayBuffer(base64) { function base64ToArrayBuffer(base64) {
const binary = atob(base64); const binary = atob(base64);
const bytes = new Uint8Array(binary.length); const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) { for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i); bytes[i] = binary.charCodeAt(i);
} }
return bytes.buffer; return bytes.buffer;
} }
@ -99,6 +127,7 @@ async function handleRequestUrl(requestId, request) {
try { try {
let body = request.body; let body = request.body;
let binary = false; let binary = false;
if (body instanceof ArrayBuffer) { if (body instanceof ArrayBuffer) {
body = arrayBufferToBase64(body); body = arrayBufferToBase64(body);
binary = true; binary = true;
@ -118,6 +147,7 @@ async function handleRequestUrl(requestId, request) {
}); });
const proxyResult = await res.json(); const proxyResult = await res.json();
if (!res.ok) { if (!res.ok) {
ipcRenderer._emit(requestId, { ipcRenderer._emit(requestId, {
error: proxyResult.error || "Proxy request failed", error: proxyResult.error || "Proxy request failed",
@ -160,6 +190,7 @@ export const ipcRenderer = {
if (channel === "print-to-pdf") { if (channel === "print-to-pdf") {
const iframe = window.__popupIframe; const iframe = window.__popupIframe;
if (iframe) { if (iframe) {
setTimeout(() => { setTimeout(() => {
iframe.contentWindow.print(); iframe.contentWindow.print();
@ -170,6 +201,7 @@ export const ipcRenderer = {
}, 200); }, 200);
} else { } else {
window.print(); window.print();
queueMicrotask(() => { queueMicrotask(() => {
ipcRenderer._emit("print-to-pdf", { success: true }); ipcRenderer._emit("print-to-pdf", { success: true });
}); });
@ -180,9 +212,11 @@ export const ipcRenderer = {
sendSync(channel, ...args) { sendSync(channel, ...args) {
console.log("[shim:ipcRenderer] sendSync:", channel, args); console.log("[shim:ipcRenderer] sendSync:", channel, args);
if (syncHandlers[channel]) { if (syncHandlers[channel]) {
return syncHandlers[channel](...args); return syncHandlers[channel](...args);
} }
console.warn("[shim:ipcRenderer] Unhandled sendSync channel:", channel); console.warn("[shim:ipcRenderer] Unhandled sendSync channel:", channel);
return null; return null;
}, },
@ -191,7 +225,9 @@ export const ipcRenderer = {
if (!listeners.has(channel)) { if (!listeners.has(channel)) {
listeners.set(channel, []); listeners.set(channel, []);
} }
listeners.get(channel).push(listener); listeners.get(channel).push(listener);
return ipcRenderer; return ipcRenderer;
}, },
@ -200,6 +236,7 @@ export const ipcRenderer = {
ipcRenderer.removeListener(channel, wrapped); ipcRenderer.removeListener(channel, wrapped);
listener(...args); listener(...args);
}; };
return ipcRenderer.on(channel, wrapped); return ipcRenderer.on(channel, wrapped);
}, },
@ -207,8 +244,12 @@ export const ipcRenderer = {
const arr = listeners.get(channel); const arr = listeners.get(channel);
if (arr) { if (arr) {
const idx = arr.indexOf(listener); const idx = arr.indexOf(listener);
if (idx >= 0) arr.splice(idx, 1);
if (idx >= 0) {
arr.splice(idx, 1);
} }
}
return ipcRenderer; return ipcRenderer;
}, },
@ -218,11 +259,13 @@ export const ipcRenderer = {
} else { } else {
listeners.clear(); listeners.clear();
} }
return ipcRenderer; return ipcRenderer;
}, },
_emit(channel, ...args) { _emit(channel, ...args) {
const arr = listeners.get(channel); const arr = listeners.get(channel);
if (arr) { if (arr) {
for (const fn of arr) { for (const fn of arr) {
fn({}, ...args); fn({}, ...args);

View file

@ -1,6 +1,6 @@
// stub
export const clipboardShim = { export const clipboardShim = {
readText() { readText() {
// TODO: maintain a local mirror updated via async reads
return ""; return "";
}, },

View file

@ -19,10 +19,12 @@ export const dialogShim = {
return { canceled: false, filePath: "/downloads/" + name }; return { canceled: false, filePath: "/downloads/" + name };
}, },
// TODO: replace alert() with a styled modal (matching vault manager style)
async showMessageBox(browserWindow, options) { async showMessageBox(browserWindow, options) {
if (typeof browserWindow === "object" && !options) { if (typeof browserWindow === "object" && !options) {
options = browserWindow; options = browserWindow;
} }
console.log("[shim:dialog] showMessageBox:", options); console.log("[shim:dialog] showMessageBox:", options);
const message = options.message || ""; const message = options.message || "";

View file

@ -25,6 +25,7 @@ export const remoteShim = {
screen: screenShim, screen: screenShim,
nativeImage: nativeImageShim, nativeImage: nativeImageShim,
Notification: notificationShim, Notification: notificationShim,
safeStorage: { safeStorage: {
isEncryptionAvailable() { isEncryptionAvailable() {
return false; return false;

View file

@ -6,6 +6,7 @@ export class menuShim {
static buildFromTemplate(template) { static buildFromTemplate(template) {
const menu = new menuShim(); const menu = new menuShim();
menu.items = (template || []).map((item) => new menuItemShim(item)); menu.items = (template || []).map((item) => new menuItemShim(item));
return menu; return menu;
} }
@ -18,7 +19,6 @@ export class menuShim {
} }
popup(options) { popup(options) {
// TODO: render custom HTML context menu at mouse position
console.log("[shim:Menu] popup (stub)", options); console.log("[shim:Menu] popup (stub)", options);
} }
@ -30,9 +30,7 @@ export class menuShim {
this.items.splice(pos, 0, menuItem); this.items.splice(pos, 0, menuItem);
} }
closePopup() { closePopup() {}
// TODO: hide custom context menu
}
} }
export class menuItemShim { export class menuItemShim {

View file

@ -37,7 +37,10 @@ export const themeShim = {
if (event === "updated") { if (event === "updated") {
const wrapped = () => { const wrapped = () => {
const idx = listeners.indexOf(wrapped); const idx = listeners.indexOf(wrapped);
if (idx >= 0) listeners.splice(idx, 1); if (idx >= 0) {
listeners.splice(idx, 1);
}
callback(); callback();
}; };
listeners.push(wrapped); listeners.push(wrapped);
@ -47,7 +50,10 @@ export const themeShim = {
removeListener(event, callback) { removeListener(event, callback) {
const idx = listeners.indexOf(callback); const idx = listeners.indexOf(callback);
if (idx >= 0) listeners.splice(idx, 1); if (idx >= 0) {
listeners.splice(idx, 1);
}
return themeShim; return themeShim;
}, },

View file

@ -107,15 +107,21 @@ const currentWindow = {
}, },
on(event, handler) { on(event, handler) {
if (event === "focus") window.addEventListener("focus", handler); if (event === "focus") {
else if (event === "blur") window.addEventListener("blur", handler); window.addEventListener("focus", handler);
else if (event === "resize") window.addEventListener("resize", handler); } else if (event === "blur") {
window.addEventListener("blur", handler);
} else if (event === "resize") {
window.addEventListener("resize", handler);
}
return currentWindow; return currentWindow;
}, },
once(event, handler) { once(event, handler) {
if (event === "focus") if (event === "focus") {
window.addEventListener("focus", handler, { once: true }); window.addEventListener("focus", handler, { once: true });
}
return currentWindow; return currentWindow;
}, },
@ -309,19 +315,34 @@ export const windowShim = {
getAllWindows() { getAllWindows() {
const wins = [currentWindow]; const wins = [currentWindow];
if (_popupWindow) wins.push(_popupWindow); if (_popupWindow) {
wins.push(_popupWindow);
}
return wins; return wins;
}, },
fromId(id) { fromId(id) {
if (id === currentWindow.id) return currentWindow; if (id === currentWindow.id) {
if (_popupWindow && id === _popupWindow.id) return _popupWindow; return currentWindow;
}
if (_popupWindow && id === _popupWindow.id) {
return _popupWindow;
}
return null; return null;
}, },
fromWebContents(wc) { fromWebContents(wc) {
if (wc === currentWebContents) return currentWindow; if (wc === currentWebContents) {
if (_popupWebContents && wc === _popupWebContents) return _popupWindow; return currentWindow;
}
if (_popupWebContents && wc === _popupWebContents) {
return _popupWindow;
}
return null; return null;
}, },
}; };
@ -329,14 +350,22 @@ export const windowShim = {
export const webContentsShim = { export const webContentsShim = {
_current: () => currentWebContents, _current: () => currentWebContents,
fromId(id) { fromId(id) {
if (id === currentWebContents.id) return currentWebContents; if (id === currentWebContents.id) {
if (_popupWebContents && id === _popupWebContents.id) return currentWebContents;
}
if (_popupWebContents && id === _popupWebContents.id) {
return _popupWebContents; return _popupWebContents;
}
return null; return null;
}, },
getAllWebContents() { getAllWebContents() {
const wcs = [currentWebContents]; const wcs = [currentWebContents];
if (_popupWebContents) wcs.push(_popupWebContents); if (_popupWebContents) {
wcs.push(_popupWebContents);
}
return wcs; return wcs;
}, },
}; };

View file

@ -20,6 +20,7 @@ export class ContentCache {
entry.accessedAt = Date.now(); entry.accessedAt = Date.now();
return entry.data; return entry.data;
} }
return null; return null;
} }
@ -44,6 +45,7 @@ export class ContentCache {
delete(path) { delete(path) {
const norm = this._normalize(path); const norm = this._normalize(path);
const entry = this._cache.get(norm); const entry = this._cache.get(norm);
if (entry) { if (entry) {
this._currentSize -= entry.size; this._currentSize -= entry.size;
this._cache.delete(norm); this._cache.delete(norm);
@ -71,12 +73,14 @@ export class ContentCache {
_evictOne() { _evictOne() {
let oldest = null; let oldest = null;
let oldestTime = Infinity; let oldestTime = Infinity;
for (const [key, entry] of this._cache) { for (const [key, entry] of this._cache) {
if (entry.accessedAt < oldestTime) { if (entry.accessedAt < oldestTime) {
oldest = key; oldest = key;
oldestTime = entry.accessedAt; oldestTime = entry.accessedAt;
} }
} }
if (oldest) { if (oldest) {
this.delete(oldest); this.delete(oldest);
} }

View file

@ -1,6 +1,6 @@
// In-memory metadata cache // In-memory metadata cache
// Populated from /api/fs/tree on startup, kept in sync via transport events. // 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 { export class MetadataCache {
constructor() { constructor() {
@ -38,10 +38,12 @@ export class MetadataCache {
const oldNorm = this._normalize(oldPath); const oldNorm = this._normalize(oldPath);
const newNorm = this._normalize(newPath); const newNorm = this._normalize(newPath);
const meta = this._entries.get(oldNorm); const meta = this._entries.get(oldNorm);
if (meta) { if (meta) {
this._entries.delete(oldNorm); this._entries.delete(oldNorm);
this._entries.set(newNorm, meta); this._entries.set(newNorm, meta);
} }
// Move children // Move children
const prefix = oldNorm + "/"; const prefix = oldNorm + "/";
for (const [key, val] of this._entries) { for (const [key, val] of this._entries) {
@ -65,9 +67,11 @@ export class MetadataCache {
const rest = key.slice(prefix.length); const rest = key.slice(prefix.length);
const slashIdx = rest.indexOf("/"); const slashIdx = rest.indexOf("/");
const childName = slashIdx >= 0 ? rest.slice(0, slashIdx) : rest; const childName = slashIdx >= 0 ? rest.slice(0, slashIdx) : rest;
if (childName && !seen.has(childName)) { if (childName && !seen.has(childName)) {
seen.add(childName); seen.add(childName);
const childMeta = this._entries.get(prefix + childName); const childMeta = this._entries.get(prefix + childName);
results.push({ results.push({
name: childName, name: childName,
type: childMeta?.type || (slashIdx >= 0 ? "directory" : "file"), type: childMeta?.type || (slashIdx >= 0 ? "directory" : "file"),
@ -82,10 +86,13 @@ export class MetadataCache {
return this._entries.size; return this._entries.size;
} }
// Build a stat-like object from metadata
toStat(path) { toStat(path) {
const meta = this.get(path); const meta = this.get(path);
if (!meta) return null;
if (!meta) {
return null;
}
return { return {
size: meta.size || 0, size: meta.size || 0,
mtimeMs: meta.mtime || 0, mtimeMs: meta.mtime || 0,

View file

@ -2,7 +2,10 @@ export function createFsPromises(metadataCache, contentCache, transport) {
return { return {
async stat(path) { async stat(path) {
const cached = metadataCache.toStat(path); const cached = metadataCache.toStat(path);
if (cached) return cached;
if (cached) {
return cached;
}
const meta = await transport.stat(path); const meta = await transport.stat(path);
metadataCache.set(path, meta); metadataCache.set(path, meta);
@ -16,13 +19,16 @@ export function createFsPromises(metadataCache, contentCache, transport) {
async readdir(path) { async readdir(path) {
const meta = metadataCache.get(path); const meta = metadataCache.get(path);
if (meta && meta.type === "file") { if (meta && meta.type === "file") {
return []; return [];
} }
if (!meta && path && path !== "/" && path !== ".") { if (!meta && path && path !== "/" && path !== ".") {
const e = new Error( const e = new Error(
`ENOENT: no such file or directory, scandir '${path}'`, `ENOENT: no such file or directory, scandir '${path}'`,
); );
e.code = "ENOENT"; e.code = "ENOENT";
throw e; throw e;
} }
@ -31,7 +37,10 @@ export function createFsPromises(metadataCache, contentCache, transport) {
}, },
async readFile(path, encoding) { 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 wantText = encoding === "utf8" || encoding === "utf-8";
const meta = metadataCache.get(path); const meta = metadataCache.get(path);
@ -40,6 +49,7 @@ export function createFsPromises(metadataCache, contentCache, transport) {
e.code = "EISDIR"; e.code = "EISDIR";
throw e; throw e;
} }
if (!meta && path) { if (!meta && path) {
const e = new Error( const e = new Error(
`ENOENT: no such file or directory, open '${path}'`, `ENOENT: no such file or directory, open '${path}'`,
@ -49,16 +59,19 @@ export function createFsPromises(metadataCache, contentCache, transport) {
} }
const cached = contentCache.get(path); const cached = contentCache.get(path);
if (cached !== null) { if (cached !== null) {
if (wantText) { if (wantText) {
return typeof cached === "string" return typeof cached === "string"
? cached ? cached
: new TextDecoder().decode(cached); : new TextDecoder().decode(cached);
} }
// binary. ensure we return a proper Uint8Array with .buffer // binary. ensure we return a proper Uint8Array with .buffer
if (typeof cached === "string") { if (typeof cached === "string") {
return new TextEncoder().encode(cached); return new TextEncoder().encode(cached);
} }
return cached; return cached;
} }
@ -68,11 +81,15 @@ export function createFsPromises(metadataCache, contentCache, transport) {
}, },
async writeFile(path, data, encoding) { async writeFile(path, data, encoding) {
if (typeof encoding === "object") encoding = encoding?.encoding; if (typeof encoding === "object") {
encoding = encoding?.encoding;
}
contentCache.set(path, data); contentCache.set(path, data);
const size = const size =
typeof data === "string" ? data.length : data.byteLength || 0; typeof data === "string" ? data.length : data.byteLength || 0;
metadataCache.set(path, { metadataCache.set(path, {
type: "file", type: "file",
size, size,
@ -81,6 +98,7 @@ export function createFsPromises(metadataCache, contentCache, transport) {
}); });
const result = await transport.writeFile(path, data, encoding); const result = await transport.writeFile(path, data, encoding);
if (result.mtime) { if (result.mtime) {
metadataCache.set(path, { metadataCache.set(path, {
type: "file", type: "file",
@ -93,6 +111,7 @@ export function createFsPromises(metadataCache, contentCache, transport) {
async appendFile(path, data, encoding) { async appendFile(path, data, encoding) {
contentCache.invalidate(path); contentCache.invalidate(path);
await transport.appendFile(path, data); await transport.appendFile(path, data);
const meta = await transport.stat(path); const meta = await transport.stat(path);
@ -102,15 +121,18 @@ export function createFsPromises(metadataCache, contentCache, transport) {
async unlink(path) { async unlink(path) {
contentCache.delete(path); contentCache.delete(path);
metadataCache.delete(path); metadataCache.delete(path);
await transport.unlink(path); await transport.unlink(path);
}, },
async rename(oldPath, newPath) { async rename(oldPath, newPath) {
const content = contentCache.get(oldPath); const content = contentCache.get(oldPath);
if (content !== null) { if (content !== null) {
contentCache.set(newPath, content); contentCache.set(newPath, content);
contentCache.delete(oldPath); contentCache.delete(oldPath);
} }
metadataCache.rename(oldPath, newPath); metadataCache.rename(oldPath, newPath);
await transport.rename(oldPath, newPath); await transport.rename(oldPath, newPath);
@ -119,7 +141,9 @@ export function createFsPromises(metadataCache, contentCache, transport) {
async mkdir(path, options) { async mkdir(path, options) {
const recursive = const recursive =
typeof options === "object" ? !!options.recursive : !!options; typeof options === "object" ? !!options.recursive : !!options;
metadataCache.set(path, { type: "directory" }); metadataCache.set(path, { type: "directory" });
await transport.mkdir(path, recursive); await transport.mkdir(path, recursive);
}, },
@ -131,19 +155,25 @@ export function createFsPromises(metadataCache, contentCache, transport) {
async rm(path, options) { async rm(path, options) {
const recursive = const recursive =
typeof options === "object" ? !!options.recursive : false; typeof options === "object" ? !!options.recursive : false;
metadataCache.delete(path); metadataCache.delete(path);
contentCache.delete(path); contentCache.delete(path);
await transport.rm(path, recursive); await transport.rm(path, recursive);
}, },
async copyFile(src, dest) { async copyFile(src, dest) {
await transport.copyFile(src, dest); await transport.copyFile(src, dest);
const meta = await transport.stat(dest); const meta = await transport.stat(dest);
metadataCache.set(dest, meta); metadataCache.set(dest, meta);
}, },
async access(path) { async access(path) {
if (metadataCache.has(path)) return; if (metadataCache.has(path)) {
return;
}
const e = new Error( const e = new Error(
`ENOENT: no such file or directory, access '${path}'`, `ENOENT: no such file or directory, access '${path}'`,
); );
@ -152,7 +182,10 @@ export function createFsPromises(metadataCache, contentCache, transport) {
}, },
async realpath(path) { async realpath(path) {
if (!path || path === "/" || path === ".") return "/"; if (!path || path === "/" || path === ".") {
return "/";
}
return transport.realpath(path); return transport.realpath(path);
}, },

View file

@ -6,6 +6,7 @@ export function createFsSync(metadataCache, contentCache, transport) {
statSync(path) { statSync(path) {
const stat = metadataCache.toStat(path); const stat = metadataCache.toStat(path);
if (!stat) { if (!stat) {
const err = new Error( const err = new Error(
`ENOENT: no such file or directory, stat '${path}'`, `ENOENT: no such file or directory, stat '${path}'`,
@ -13,6 +14,7 @@ export function createFsSync(metadataCache, contentCache, transport) {
err.code = "ENOENT"; err.code = "ENOENT";
throw err; throw err;
} }
return stat; return stat;
}, },
@ -27,7 +29,9 @@ export function createFsSync(metadataCache, contentCache, transport) {
}, },
readFileSync(path, encoding) { readFileSync(path, encoding) {
if (typeof encoding === "object") encoding = encoding?.encoding; if (typeof encoding === "object") {
encoding = encoding?.encoding;
}
const meta = metadataCache.get(path); const meta = metadataCache.get(path);
if (meta && meta.type === "directory") { if (meta && meta.type === "directory") {
@ -43,21 +47,28 @@ export function createFsSync(metadataCache, contentCache, transport) {
? cached ? cached
: new TextDecoder().decode(cached); : new TextDecoder().decode(cached);
} }
return cached; return cached;
} }
console.warn("[shim:fs] readFileSync cache miss, using sync XHR:", path); console.warn("[shim:fs] readFileSync cache miss, using sync XHR:", path);
const data = transport.readFileSync(path, encoding); const data = transport.readFileSync(path, encoding);
contentCache.set(path, data); contentCache.set(path, data);
return data; return data;
}, },
writeFileSync(path, data, encoding) { writeFileSync(path, data, encoding) {
if (typeof encoding === "object") encoding = encoding?.encoding; if (typeof encoding === "object") {
encoding = encoding?.encoding;
}
contentCache.set(path, data); contentCache.set(path, data);
const size = const size =
typeof data === "string" ? data.length : data.byteLength || 0; typeof data === "string" ? data.length : data.byteLength || 0;
metadataCache.set(path, { metadataCache.set(path, {
type: "file", type: "file",
size, size,

View file

@ -7,9 +7,11 @@ function normPath(p) {
function uint8ToBase64(bytes) { function uint8ToBase64(bytes) {
let binary = ""; let binary = "";
const chunk = 8192; const chunk = 8192;
for (let i = 0; i < bytes.length; i += chunk) { for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk)); binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
} }
return btoa(binary); return btoa(binary);
} }
@ -23,7 +25,10 @@ async function request(method, endpoint, params = {}) {
const options = { method }; const options = { method };
if (method === "GET" || method === "DELETE") { 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)) { for (const [key, val] of Object.entries(params)) {
url.searchParams.set(key, val); url.searchParams.set(key, val);
} }
@ -41,6 +46,7 @@ async function request(method, endpoint, params = {}) {
e.code = err.code || "UNKNOWN"; e.code = err.code || "UNKNOWN";
throw e; throw e;
} }
return res; return res;
} }
@ -53,7 +59,10 @@ function requestSync(method, endpoint, params = {}) {
const url = new URL(API_BASE + endpoint, window.location.origin); const url = new URL(API_BASE + endpoint, window.location.origin);
if (method === "GET" || method === "DELETE") { 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)) { for (const [key, val] of Object.entries(params)) {
url.searchParams.set(key, val); url.searchParams.set(key, val);
} }
@ -71,6 +80,7 @@ function requestSync(method, endpoint, params = {}) {
if (xhr.status >= 400) { if (xhr.status >= 400) {
let err; let err;
try { try {
const body = JSON.parse(xhr.responseText); const body = JSON.parse(xhr.responseText);
err = new Error(body.error || "Request failed"); err = new Error(body.error || "Request failed");
@ -79,6 +89,7 @@ function requestSync(method, endpoint, params = {}) {
err = new Error("Request failed: " + xhr.status); err = new Error("Request failed: " + xhr.status);
err.code = "UNKNOWN"; err.code = "UNKNOWN";
} }
throw err; throw err;
} }
@ -103,9 +114,11 @@ export const transport = {
path: normPath(path), path: normPath(path),
encoding: encoding || "", encoding: encoding || "",
}); });
if (encoding === "utf8" || encoding === "utf-8") { if (encoding === "utf8" || encoding === "utf-8") {
return res.text(); return res.text();
} }
const buf = await res.arrayBuffer(); const buf = await res.arrayBuffer();
return new Uint8Array(buf); return new Uint8Array(buf);
}, },
@ -184,14 +197,18 @@ export const transport = {
path: normPath(path), path: normPath(path),
encoding: encoding || "", encoding: encoding || "",
}); });
if (encoding === "utf8" || encoding === "utf-8") { if (encoding === "utf8" || encoding === "utf-8") {
return xhr.responseText; return xhr.responseText;
} }
const binary = xhr.responseText; const binary = xhr.responseText;
const bytes = new Uint8Array(binary.length); const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) { for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i); bytes[i] = binary.charCodeAt(i);
} }
return bytes; return bytes;
}, },

View file

@ -20,7 +20,10 @@ const DEBUG = true;
const _accessLog = new Map(); // "module.property" -> count const _accessLog = new Map(); // "module.property" -> count
function wrapWithProxy(obj, name) { function wrapWithProxy(obj, name) {
if (!DEBUG || !obj || typeof obj !== "object") return obj; if (!DEBUG || !obj || typeof obj !== "object") {
return obj;
}
return new Proxy(obj, { return new Proxy(obj, {
get(target, prop) { get(target, prop) {
if ( if (
@ -31,10 +34,12 @@ function wrapWithProxy(obj, name) {
) { ) {
const key = `${name}.${prop}`; const key = `${name}.${prop}`;
_accessLog.set(key, (_accessLog.get(key) || 0) + 1); _accessLog.set(key, (_accessLog.get(key) || 0) + 1);
if (!(prop in target)) { if (!(prop in target)) {
console.warn(`[shim:MISS] ${key} - property not found on shim`); console.warn(`[shim:MISS] ${key} - property not found on shim`);
} }
} }
return target[prop]; return target[prop];
}, },
}); });
@ -44,6 +49,7 @@ window.__shimLog = function () {
const sorted = [..._accessLog.entries()].sort((a, b) => b[1] - a[1]); const sorted = [..._accessLog.entries()].sort((a, b) => b[1] - a[1]);
console.table(sorted.map(([k, v]) => ({ api: k, calls: v }))); console.table(sorted.map(([k, v]) => ({ api: k, calls: v })));
}; };
window.__shimMisses = function () { window.__shimMisses = function () {
const sorted = [..._accessLog.entries()] const sorted = [..._accessLog.entries()]
.filter(([k]) => { .filter(([k]) => {
@ -52,6 +58,7 @@ window.__shimMisses = function () {
return shim && !(prop in shim); return shim && !(prop in shim);
}) })
.sort((a, b) => b[1] - a[1]); .sort((a, b) => b[1] - a[1]);
console.table(sorted.map(([k, v]) => ({ api: k, calls: v }))); console.table(sorted.map(([k, v]) => ({ api: k, calls: v })));
}; };
@ -82,9 +89,11 @@ window.require = function (moduleName) {
if (throwOnRequire.has(moduleName)) { if (throwOnRequire.has(moduleName)) {
throw new Error(`Cannot find module '${moduleName}'`); throw new Error(`Cannot find module '${moduleName}'`);
} }
if (shimRegistry[moduleName]) { if (shimRegistry[moduleName]) {
return shimRegistry[moduleName]; return shimRegistry[moduleName];
} }
console.warn("[ignis] Unshimmed require:", moduleName); console.warn("[ignis] Unshimmed require:", moduleName);
return wrapWithProxy({}, `UNKNOWN(${moduleName})`); return wrapWithProxy({}, `UNKNOWN(${moduleName})`);
}; };
@ -97,19 +106,23 @@ if (typeof window.Buffer === "undefined") {
if (typeof data === "string") { if (typeof data === "string") {
return new TextEncoder().encode(data); return new TextEncoder().encode(data);
} }
if (data instanceof ArrayBuffer) { if (data instanceof ArrayBuffer) {
return new Uint8Array(data); return new Uint8Array(data);
} }
return new Uint8Array(data); return new Uint8Array(data);
}, },
concat: function (arrays) { concat: function (arrays) {
const total = arrays.reduce((sum, a) => sum + a.length, 0); const total = arrays.reduce((sum, a) => sum + a.length, 0);
const result = new Uint8Array(total); const result = new Uint8Array(total);
let offset = 0; let offset = 0;
for (const arr of arrays) { for (const arr of arrays) {
result.set(arr, offset); result.set(arr, offset);
offset += arr.length; offset += arr.length;
} }
return result; return result;
}, },
isBuffer: function (obj) { isBuffer: function (obj) {
@ -127,29 +140,37 @@ const _originalOpen = window.open;
window.open = function (url, target, features) { window.open = function (url, target, features) {
if (url === "about:blank" || (features && features.includes("popup"))) { if (url === "about:blank" || (features && features.includes("popup"))) {
console.log("[ignis] intercepted popup:", url, features); console.log("[ignis] intercepted popup:", url, features);
registerPopupWindow(); registerPopupWindow();
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
iframe.style.cssText = iframe.style.cssText =
"position:fixed;left:-9999px;width:0;height:0;border:none;"; "position:fixed;left:-9999px;width:0;height:0;border:none;";
document.body.appendChild(iframe); document.body.appendChild(iframe);
window.__popupIframe = iframe; window.__popupIframe = iframe;
const iframeWin = iframe.contentWindow; const iframeWin = iframe.contentWindow;
iframeWin.require = window.require; iframeWin.require = window.require;
iframeWin.module = window.module; iframeWin.module = window.module;
iframeWin.Buffer = window.Buffer; iframeWin.Buffer = window.Buffer;
iframeWin.process = window.process; iframeWin.process = window.process;
iframeWin.global = iframeWin; iframeWin.global = iframeWin;
iframeWin.globalEnhance = window.globalEnhance; iframeWin.globalEnhance = window.globalEnhance;
iframeWin.close = function () { iframeWin.close = function () {
unregisterPopupWindow(); unregisterPopupWindow();
iframe.remove(); iframe.remove();
window.__popupIframe = null; window.__popupIframe = null;
}; };
return iframeWin; return iframeWin;
} }
return _originalOpen.call(window, url, target, features); return _originalOpen.call(window, url, target, features);
}; };
// hacky fix to prevent browser from showing context menu while allowing obsidian context menu
window.addEventListener( window.addEventListener(
"contextmenu", "contextmenu",
(e) => { (e) => {
@ -167,17 +188,23 @@ window.__currentVaultId = _urlParams.get("vault") || "";
const vaultParam = window.__currentVaultId const vaultParam = window.__currentVaultId
? "?vault=" + encodeURIComponent(window.__currentVaultId) ? "?vault=" + encodeURIComponent(window.__currentVaultId)
: ""; : "";
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/vault/info" + vaultParam, false); xhr.open("GET", "/api/vault/info" + vaultParam, false);
xhr.send(); xhr.send();
if (xhr.status === 200) { if (xhr.status === 200) {
const info = JSON.parse(xhr.responseText); const info = JSON.parse(xhr.responseText);
window.__currentVaultId = info.id; window.__currentVaultId = info.id;
window.__obsidianVersion = info.version || "0.0.0"; window.__obsidianVersion = info.version || "0.0.0";
window.__vaultConfig = { window.__vaultConfig = {
id: info.id, id: info.id,
path: "/", path: "/",
}; };
console.log("[ignis] Vault:", window.__vaultConfig); console.log("[ignis] Vault:", window.__vaultConfig);
console.log("[ignis] Obsidian version:", window.__obsidianVersion); console.log("[ignis] Obsidian version:", window.__obsidianVersion);
} else { } else {
@ -191,8 +218,10 @@ window.__currentVaultId = _urlParams.get("vault") || "";
(function initVaultList() { (function initVaultList() {
try { try {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/vault/list", false); xhr.open("GET", "/api/vault/list", false);
xhr.send(); xhr.send();
if (xhr.status === 200) { if (xhr.status === 200) {
window.__vaultList = JSON.parse(xhr.responseText); window.__vaultList = JSON.parse(xhr.responseText);
} }
@ -206,14 +235,19 @@ window.__currentVaultId = _urlParams.get("vault") || "";
const vaultParam = window.__currentVaultId const vaultParam = window.__currentVaultId
? "?vault=" + encodeURIComponent(window.__currentVaultId) ? "?vault=" + encodeURIComponent(window.__currentVaultId)
: ""; : "";
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open("GET", "/api/fs/tree" + vaultParam, false); xhr.open("GET", "/api/fs/tree" + vaultParam, false);
xhr.send(); xhr.send();
if (xhr.status === 200) { if (xhr.status === 200) {
const tree = JSON.parse(xhr.responseText); const tree = JSON.parse(xhr.responseText);
fsShim._metadataCache.populate(tree); fsShim._metadataCache.populate(tree);
fsShim._metadataCache.set("", { type: "directory" }); fsShim._metadataCache.set("", { type: "directory" });
fsShim._metadataCache.set("/", { type: "directory" }); fsShim._metadataCache.set("/", { type: "directory" });
console.log( console.log(
"[ignis] Metadata cache populated:", "[ignis] Metadata cache populated:",
fsShim._metadataCache.size, fsShim._metadataCache.size,

View file

@ -4,8 +4,12 @@ export class EventEmitter {
} }
on(event, listener) { on(event, listener) {
if (!this._events[event]) this._events[event] = []; if (!this._events[event]) {
this._events[event] = [];
}
this._events[event].push(listener); this._events[event].push(listener);
return this; return this;
} }
@ -14,24 +18,39 @@ export class EventEmitter {
this.removeListener(event, wrapped); this.removeListener(event, wrapped);
listener.apply(this, args); listener.apply(this, args);
}; };
wrapped._original = listener; wrapped._original = listener;
return this.on(event, wrapped); return this.on(event, wrapped);
} }
emit(event, ...args) { emit(event, ...args) {
const listeners = this._events[event]; const listeners = this._events[event];
if (!listeners || listeners.length === 0) return false;
if (!listeners || listeners.length === 0) {
return false;
}
for (const fn of [...listeners]) { for (const fn of [...listeners]) {
fn.apply(this, args); fn.apply(this, args);
} }
return true; return true;
} }
removeListener(event, listener) { removeListener(event, listener) {
const arr = this._events[event]; const arr = this._events[event];
if (!arr) return this; if (!arr) {
const idx = arr.findIndex((fn) => fn === listener || fn._original === listener); return this;
if (idx >= 0) arr.splice(idx, 1); }
const idx = arr.findIndex(
(fn) => fn === listener || fn._original === listener,
);
if (idx >= 0) {
arr.splice(idx, 1);
}
return this; return this;
} }
@ -45,6 +64,7 @@ export class EventEmitter {
} else { } else {
this._events = {}; this._events = {};
} }
return this; return this;
} }
@ -61,8 +81,12 @@ export class EventEmitter {
} }
prependListener(event, listener) { prependListener(event, listener) {
if (!this._events[event]) this._events[event] = []; if (!this._events[event]) {
this._events[event] = [];
}
this._events[event].unshift(listener); this._events[event].unshift(listener);
return this; return this;
} }

View file

@ -15,6 +15,7 @@ export class ClientRequest extends EventEmitter {
constructor() { constructor() {
super(); super();
} }
end() {} end() {}
write() {} write() {}
abort() {} abort() {}
@ -26,6 +27,7 @@ export function request(options, callback) {
if (callback) { if (callback) {
req.once("response", callback); req.once("response", callback);
} }
// Immediately error. real HTTP requests need fetch or the proxy // Immediately error. real HTTP requests need fetch or the proxy
setTimeout(() => { setTimeout(() => {
req.emit( req.emit(

View file

@ -12,6 +12,7 @@ export const pathShim = {
if (p === "/" && window.__currentVaultId) { if (p === "/" && window.__currentVaultId) {
return window.__currentVaultId; return window.__currentVaultId;
} }
return _origBasename(p, ext); return _origBasename(p, ext);
}, },
}; };

View file

@ -1,13 +1,14 @@
// Override window.requestUrl to proxy external requests through our server, // Override window.requestUrl to proxy external requests through our server, bypassing CORS.
// bypassing browser CORS restrictions. Obsidian sets window.requestUrl = UA // Obsidian sets window.requestUrl in app.js, so we override it after app.js loads.
// in app.js, so we override it after app.js loads.
function base64ToArrayBuffer(base64) { function base64ToArrayBuffer(base64) {
const binary = atob(base64); const binary = atob(base64);
const bytes = new Uint8Array(binary.length); const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) { for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i); bytes[i] = binary.charCodeAt(i);
} }
return bytes.buffer; return bytes.buffer;
} }
@ -15,9 +16,11 @@ function arrayBufferToBase64(buf) {
const bytes = new Uint8Array(buf); const bytes = new Uint8Array(buf);
let binary = ""; let binary = "";
const chunk = 8192; const chunk = 8192;
for (let i = 0; i < bytes.length; i += chunk) { for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk)); binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
} }
return btoa(binary); return btoa(binary);
} }
@ -37,7 +40,9 @@ async function proxyRequestUrl(request) {
headers: request.headers || {}, headers: request.headers || {},
body: request.body, body: request.body,
}); });
const arrayBuf = await res.arrayBuffer(); const arrayBuf = await res.arrayBuffer();
return makeResponse( return makeResponse(
request, request,
res.status, res.status,
@ -49,6 +54,7 @@ async function proxyRequestUrl(request) {
// Cross-origin: route through server proxy // Cross-origin: route through server proxy
let body = request.body; let body = request.body;
let binary = false; let binary = false;
if (body instanceof ArrayBuffer) { if (body instanceof ArrayBuffer) {
body = arrayBufferToBase64(body); body = arrayBufferToBase64(body);
binary = true; binary = true;
@ -75,6 +81,7 @@ async function proxyRequestUrl(request) {
const proxyResult = await res.json(); const proxyResult = await res.json();
const arrayBuf = base64ToArrayBuffer(proxyResult.body); const arrayBuf = base64ToArrayBuffer(proxyResult.body);
return makeResponse( return makeResponse(
request, request,
proxyResult.status, proxyResult.status,
@ -86,11 +93,13 @@ async function proxyRequestUrl(request) {
function makeResponse(request, status, headers, arrayBuf) { function makeResponse(request, status, headers, arrayBuf) {
const text = new TextDecoder().decode(arrayBuf); const text = new TextDecoder().decode(arrayBuf);
let json; let json;
try { try {
json = JSON.parse(text); json = JSON.parse(text);
} catch { } catch {
json = null; json = null;
} }
return { status, headers, arrayBuffer: arrayBuf, text, json }; return { status, headers, arrayBuffer: arrayBuf, text, json };
} }

View file

@ -1,5 +1,4 @@
// Custom vault manager modal. will migrate to Svelte later // Custom vault manager modal. will migrate to Svelte later
// Shows list of vaults, create new, delete, switch.
export function showVaultManager() { export function showVaultManager() {
if (!document.querySelector(".workspace")) return; if (!document.querySelector(".workspace")) return;

View file

@ -6,16 +6,19 @@ export const urlShim = {
// Return an object with .href matching Node's url.pathToFileURL behavior // Return an object with .href matching Node's url.pathToFileURL behavior
const encoded = encodeURI(p.replace(/\\/g, "/")); const encoded = encodeURI(p.replace(/\\/g, "/"));
const href = "file:///" + encoded.replace(/^\/+/, ""); const href = "file:///" + encoded.replace(/^\/+/, "");
return { href, toString: () => href }; return { href, toString: () => href };
}, },
fileURLToPath(url) { fileURLToPath(url) {
let str = typeof url === "string" ? url : url.href || url.toString(); let str = typeof url === "string" ? url : url.href || url.toString();
if (str.startsWith("file:///")) { if (str.startsWith("file:///")) {
str = str.slice(8); str = str.slice(8);
} else if (str.startsWith("file://")) { } else if (str.startsWith("file://")) {
str = str.slice(7); str = str.slice(7);
} }
return decodeURI(str); return decodeURI(str);
}, },
}; };