shim plugin related APIs, proxy web requests.

This commit is contained in:
Nystik
2026-03-11 23:03:14 +01:00
parent 9789be6d70
commit ac41ac3c4e
10 changed files with 452 additions and 0 deletions

View File

@@ -30,9 +30,11 @@ app.use((req, res, next) => {
const fsRoutes = require("./routes/fs");
const vaultRoutes = require("./routes/vault");
const proxyRoutes = require("./routes/proxy");
app.use("/api/fs", fsRoutes);
app.use("/api/vault", vaultRoutes);
app.use("/api/proxy", proxyRoutes);
// Serve vault files for resource URLs (images, attachments, etc.)
// Vault ID is the first path segment: /vault-files/<vault-id>/path/to/file

45
server/routes/proxy.js Normal file
View File

@@ -0,0 +1,45 @@
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.
router.post("/", async (req, res) => {
const { url, method, headers, body, binary } = req.body;
if (!url) {
return res.status(400).json({ error: "Missing url" });
}
try {
const fetchOpts = {
method: method || "GET",
headers: headers || {},
};
if (body && method !== "GET" && method !== "HEAD") {
if (binary && typeof body === "string") {
fetchOpts.body = Buffer.from(body, "base64");
} else {
fetchOpts.body = body;
}
}
const upstream = await fetch(url, fetchOpts);
const respBody = Buffer.from(await upstream.arrayBuffer());
// Forward response headers
const respHeaders = {};
upstream.headers.forEach((val, key) => {
respHeaders[key] = val;
});
res.json({
status: upstream.status,
headers: respHeaders,
body: respBody.toString("base64"),
});
} catch (e) {
res.status(502).json({ error: e.message });
}
});
module.exports = router;

View File

@@ -76,6 +76,68 @@ const syncHandlers = {
resources: () => "",
};
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;
}
async function handleRequestUrl(requestId, request) {
try {
let body = request.body;
let binary = false;
if (body instanceof ArrayBuffer) {
body = arrayBufferToBase64(body);
binary = true;
}
const res = await fetch("/api/proxy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: request.url,
method: request.method || "GET",
headers: request.headers || {},
contentType: request.contentType,
body,
binary,
}),
});
const proxyResult = await res.json();
if (!res.ok) {
ipcRenderer._emit(requestId, {
error: proxyResult.error || "Proxy request failed",
});
return;
}
// Electron's e.reply(requestId, data) sends on the requestId channel
ipcRenderer._emit(requestId, {
status: proxyResult.status,
headers: proxyResult.headers,
body: base64ToArrayBuffer(proxyResult.body),
});
} catch (e) {
ipcRenderer._emit(requestId, {
error: e.message,
});
}
}
export const ipcRenderer = {
send(channel, ...args) {
console.log("[shim:ipcRenderer] send:", channel, args);
@@ -89,6 +151,12 @@ export const ipcRenderer = {
);
return;
}
if (channel === "request-url") {
const [requestId, request] = args;
handleRequestUrl(requestId, request);
return;
}
},
sendSync(channel, ...args) {

View File

@@ -5,6 +5,12 @@ import { pathShim } from "./path.js";
import { urlShim } from "./url.js";
import { cryptoShim } from "./crypto/index.js";
import { processShim } from "./process.js";
import { installRequestUrlShim } from "./request-url.js";
import * as childProcessShim from "./node/child_process.js";
import * as eventsShim from "./node/events.js";
import * as osShim from "./node/os.js";
import * as netShim from "./node/net.js";
import * as httpShim from "./node/http.js";
const DEBUG = true;
const _accessLog = new Map(); // "module.property" -> count
@@ -53,6 +59,12 @@ const rawRegistry = {
path: pathShim,
url: urlShim,
crypto: cryptoShim,
child_process: childProcessShim,
events: eventsShim,
os: osShim,
net: netShim,
http: httpShim,
https: httpShim,
};
const shimRegistry = {};
@@ -182,4 +194,6 @@ window.__currentVaultId = _urlParams.get("vault") || "";
}
})();
installRequestUrlShim();
console.log("[obsidian-bridge] Shim loader initialized");

View File

@@ -0,0 +1,15 @@
function notAvailable(name) {
return function () {
throw new Error(
`child_process.${name}() is not available in the web version.`,
);
};
}
export const exec = notAvailable("exec");
export const execSync = notAvailable("execSync");
export const spawn = notAvailable("spawn");
export const fork = notAvailable("fork");
export const execFile = notAvailable("execFile");
export const execFileSync = notAvailable("execFileSync");
export const spawnSync = notAvailable("spawnSync");

82
shims/node/events.js Normal file
View File

@@ -0,0 +1,82 @@
export class EventEmitter {
constructor() {
this._events = {};
}
on(event, listener) {
if (!this._events[event]) this._events[event] = [];
this._events[event].push(listener);
return this;
}
once(event, listener) {
const wrapped = (...args) => {
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;
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);
return this;
}
off(event, listener) {
return this.removeListener(event, listener);
}
removeAllListeners(event) {
if (event) {
delete this._events[event];
} else {
this._events = {};
}
return this;
}
listeners(event) {
return (this._events[event] || []).slice();
}
listenerCount(event) {
return (this._events[event] || []).length;
}
addListener(event, listener) {
return this.on(event, listener);
}
prependListener(event, listener) {
if (!this._events[event]) this._events[event] = [];
this._events[event].unshift(listener);
return this;
}
eventNames() {
return Object.keys(this._events);
}
setMaxListeners() {
return this;
}
getMaxListeners() {
return 10;
}
}
export default EventEmitter;

47
shims/node/http.js Normal file
View File

@@ -0,0 +1,47 @@
// Minimal http/https stub. Plugins needing full http.request won't work,
// but this prevents crashes for plugins that just import the module.
import { EventEmitter } from "./events.js";
export class IncomingMessage extends EventEmitter {
constructor() {
super();
this.headers = {};
this.statusCode = 0;
}
}
export class ClientRequest extends EventEmitter {
constructor() {
super();
}
end() {}
write() {}
abort() {}
destroy() {}
}
export function request(options, callback) {
const req = new ClientRequest();
if (callback) {
req.once("response", callback);
}
// Immediately error - real HTTP requests need fetch or the proxy
setTimeout(() => {
req.emit("error", new Error("http.request is not available in the web version. Use requestUrl() instead."));
}, 0);
return req;
}
export function get(options, callback) {
const req = request(options, callback);
req.end();
return req;
}
export function createServer() {
throw new Error("http.createServer is not available in the web version.");
}
export const Agent = class {};
export const globalAgent = new Agent();

19
shims/node/net.js Normal file
View File

@@ -0,0 +1,19 @@
function notAvailable(name) {
return function () {
throw new Error(`net.${name}() is not available in the web version.`);
};
}
export const createServer = notAvailable("createServer");
export const createConnection = notAvailable("createConnection");
export const connect = notAvailable("connect");
export class Socket {
constructor() {
throw new Error("net.Socket is not available in the web version.");
}
}
export class Server {
constructor() {
throw new Error("net.Server is not available in the web version.");
}
}

49
shims/node/os.js Normal file
View File

@@ -0,0 +1,49 @@
export function platform() {
return "linux";
}
export function arch() {
return "x64";
}
export function homedir() {
return "/";
}
export function tmpdir() {
return "/tmp";
}
export function hostname() {
return "localhost";
}
export function type() {
return "Linux";
}
export function release() {
return "0.0.0";
}
export function cpus() {
return [{ model: "browser", speed: 0 }];
}
export function totalmem() {
return 0;
}
export function freemem() {
return 0;
}
export function networkInterfaces() {
return {};
}
export function endianness() {
return "LE";
}
export const EOL = "\n";

111
shims/request-url.js Normal file
View File

@@ -0,0 +1,111 @@
// 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.
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;
}
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);
}
async function proxyRequestUrl(request) {
if (typeof request === "string") {
request = { url: request };
}
const isSameOrigin =
request.url.startsWith(window.location.origin) ||
request.url.startsWith("/");
// Same-origin requests don't need the proxy
if (isSameOrigin) {
const res = await fetch(request.url, {
method: request.method || "GET",
headers: request.headers || {},
body: request.body,
});
const arrayBuf = await res.arrayBuffer();
return makeResponse(
request,
res.status,
Object.fromEntries(res.headers),
arrayBuf,
);
}
// Cross-origin: route through server proxy
let body = request.body;
let binary = false;
if (body instanceof ArrayBuffer) {
body = arrayBufferToBase64(body);
binary = true;
}
const res = await fetch("/api/proxy", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
url: request.url,
method: request.method || "GET",
headers: request.headers || {},
body,
binary,
}),
});
if (!res.ok) {
const err = await res
.json()
.catch(() => ({ error: "Proxy request failed" }));
throw new Error(err.error);
}
const proxyResult = await res.json();
const arrayBuf = base64ToArrayBuffer(proxyResult.body);
return makeResponse(
request,
proxyResult.status,
proxyResult.headers,
arrayBuf,
);
}
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 };
}
export function installRequestUrlShim() {
// Obsidian sets window.requestUrl in app.js. We override it once the page loads.
// Use a getter so it intercepts even if app.js sets it later.
let _original = null;
Object.defineProperty(window, "requestUrl", {
get() {
return proxyRequestUrl;
},
set(val) {
_original = val;
},
configurable: true,
});
}