mirror of
https://github.com/Nystik-gh/ignis.git
synced 2026-06-17 04:35:53 +00:00
10
CHANGELOG.md
10
CHANGELOG.md
@@ -2,6 +2,16 @@
|
|||||||
|
|
||||||
All notable changes to this project will be documented in this file.
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [0.8.4] - Karm (2026-06-03)
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Codeblocks calling clipboard APIs no longer causes reccursion error.
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
- Hardened same-origin checks, virtual-plugin URL validation, token file permissions, and log line bounds.
|
||||||
|
|
||||||
## [0.8.3] - Karm (2026-06-01)
|
## [0.8.3] - Karm (2026-06-01)
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -83,15 +83,23 @@ function isAuthenticated(dataDir) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeSecret(file, contents) {
|
||||||
|
fs.writeFileSync(file, contents, { encoding: "utf-8", mode: 0o600 });
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.chmodSync(file, 0o600);
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
function saveInternal(dataDir, tokenData) {
|
function saveInternal(dataDir, tokenData) {
|
||||||
const internalFile = getInternalTokenFile(dataDir);
|
const internalFile = getInternalTokenFile(dataDir);
|
||||||
const dir = path.dirname(internalFile);
|
const dir = path.dirname(internalFile);
|
||||||
|
|
||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(dir)) {
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(internalFile, JSON.stringify(tokenData, null, 2), "utf-8");
|
writeSecret(internalFile, JSON.stringify(tokenData, null, 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncToObCli(dataDir, token) {
|
function syncToObCli(dataDir, token) {
|
||||||
@@ -101,10 +109,10 @@ function syncToObCli(dataDir, token) {
|
|||||||
const dir = path.dirname(obAuthFile);
|
const dir = path.dirname(obAuthFile);
|
||||||
|
|
||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(dir)) {
|
||||||
fs.mkdirSync(dir, { recursive: true });
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFileSync(obAuthFile, token, "utf-8");
|
writeSecret(obAuthFile, token);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const { spawn } = require("child_process");
|
|||||||
const { spawnOb, runCommand } = require("./ob-cli");
|
const { spawnOb, runCommand } = require("./ob-cli");
|
||||||
|
|
||||||
const MAX_LOG_ENTRIES = 200;
|
const MAX_LOG_ENTRIES = 200;
|
||||||
|
const MAX_LOG_LINE = 4096;
|
||||||
|
|
||||||
function killProcess(proc) {
|
function killProcess(proc) {
|
||||||
if (!proc) {
|
if (!proc) {
|
||||||
@@ -151,10 +152,13 @@ class SyncManager {
|
|||||||
const lines = data.toString().split("\n");
|
const lines = data.toString().split("\n");
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.trim()) {
|
const trimmed = line.trim();
|
||||||
this.addLog(state, line.trim());
|
|
||||||
|
if (trimmed) {
|
||||||
|
const capped = trimmed.slice(0, MAX_LOG_LINE);
|
||||||
|
this.addLog(state, capped);
|
||||||
state.lastActivity = new Date().toISOString();
|
state.lastActivity = new Date().toISOString();
|
||||||
this.broadcaster.broadcastLog(vaultId, line.trim());
|
this.broadcaster.broadcastLog(vaultId, capped);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -302,7 +306,7 @@ class SyncManager {
|
|||||||
addLog(state, line) {
|
addLog(state, line) {
|
||||||
state.logs.push({
|
state.logs.push({
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
line,
|
line: line.slice(0, MAX_LOG_LINE),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (state.logs.length > MAX_LOG_ENTRIES) {
|
if (state.logs.length > MAX_LOG_ENTRIES) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "ignis-monorepo",
|
"name": "ignis-monorepo",
|
||||||
"version": "0.8.3",
|
"version": "0.8.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Monorepo for Ignis: a browser-based Obsidian client. Self-hosted server in apps/ignis-server; shim, UI, and shared libraries in packages/.",
|
"description": "Monorepo for Ignis: a browser-based Obsidian client. Self-hosted server in apps/ignis-server; shim, UI, and shared libraries in packages/.",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { showVaultManager } from "../ui-registry.js";
|
import { showVaultManager } from "../ui-registry.js";
|
||||||
import { vaultService } from "@ignis/services";
|
import { vaultService } from "@ignis/services";
|
||||||
|
import { arrayBufferToBase64, base64ToArrayBuffer } from "../util/base64.js";
|
||||||
|
|
||||||
const listeners = new Map();
|
const listeners = new Map();
|
||||||
|
|
||||||
@@ -85,29 +86,6 @@ const syncHandlers = {
|
|||||||
resources: () => "",
|
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) {
|
async function handleRequestUrl(requestId, request) {
|
||||||
try {
|
try {
|
||||||
let body = request.body;
|
let body = request.body;
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
|
import { getClipboard } from "./native-clipboard.js";
|
||||||
|
|
||||||
export const clipboardShim = {
|
export const clipboardShim = {
|
||||||
readText() {
|
readText() {
|
||||||
return "";
|
return "";
|
||||||
},
|
},
|
||||||
|
|
||||||
writeText(text) {
|
writeText(text) {
|
||||||
navigator.clipboard.writeText(text).catch((e) => {
|
const clip = getClipboard();
|
||||||
|
|
||||||
|
if (!clip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clip.writeText(text).catch((e) => {
|
||||||
console.warn("[shim:clipboard] writeText failed:", e);
|
console.warn("[shim:clipboard] writeText failed:", e);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -14,7 +22,13 @@ export const clipboardShim = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
writeHTML(html) {
|
writeHTML(html) {
|
||||||
navigator.clipboard
|
const clip = getClipboard();
|
||||||
|
|
||||||
|
if (!clip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clip
|
||||||
.write([
|
.write([
|
||||||
new ClipboardItem({
|
new ClipboardItem({
|
||||||
"text/html": new Blob([html], { type: "text/html" }),
|
"text/html": new Blob([html], { type: "text/html" }),
|
||||||
@@ -35,6 +49,12 @@ export const clipboardShim = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clip = getClipboard();
|
||||||
|
|
||||||
|
if (!clip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const pngData = image.toPNG();
|
const pngData = image.toPNG();
|
||||||
|
|
||||||
if (!pngData || pngData.length === 0) {
|
if (!pngData || pngData.length === 0) {
|
||||||
@@ -43,11 +63,9 @@ export const clipboardShim = {
|
|||||||
|
|
||||||
const blob = new Blob([pngData], { type: "image/png" });
|
const blob = new Blob([pngData], { type: "image/png" });
|
||||||
|
|
||||||
navigator.clipboard
|
clip.write([new ClipboardItem({ "image/png": blob })]).catch((e) => {
|
||||||
.write([new ClipboardItem({ "image/png": blob })])
|
console.warn("[shim:clipboard] writeImage failed:", e);
|
||||||
.catch((e) => {
|
});
|
||||||
console.warn("[shim:clipboard] writeImage failed:", e);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
has(format) {
|
has(format) {
|
||||||
@@ -59,6 +77,12 @@ export const clipboardShim = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
navigator.clipboard.writeText("").catch(() => {});
|
const clip = getClipboard();
|
||||||
|
|
||||||
|
if (!clip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clip.writeText("").catch(() => {});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
22
packages/shim/src/electron/remote/native-clipboard.js
Normal file
22
packages/shim/src/electron/remote/native-clipboard.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
// Obsidian points navigator.clipboard.writeText at electron.clipboard, which already points at this shim.
|
||||||
|
// To avoid recursion, use the untouched native prototype methods.
|
||||||
|
const proto = typeof Clipboard !== "undefined" ? Clipboard.prototype : null;
|
||||||
|
|
||||||
|
// Returns a native-backed clipboard facade, or null in insecure (non-localhost http) contexts.
|
||||||
|
export function getClipboard() {
|
||||||
|
const clip =
|
||||||
|
typeof navigator !== "undefined" ? navigator.clipboard : undefined;
|
||||||
|
|
||||||
|
if (!proto || !clip) {
|
||||||
|
console.warn(
|
||||||
|
"[shim:clipboard] clipboard API unavailable (insecure context?)",
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
writeText: (text) => proto.writeText.call(clip, text),
|
||||||
|
write: (items) => proto.write.call(clip, items),
|
||||||
|
read: () => proto.read.call(clip),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { getClipboard } from "./native-clipboard.js";
|
||||||
|
|
||||||
const currentWindowState = {
|
const currentWindowState = {
|
||||||
title: "Obsidian",
|
title: "Obsidian",
|
||||||
isMaximized: false,
|
isMaximized: false,
|
||||||
@@ -196,7 +198,13 @@ const currentWebContents = {
|
|||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
},
|
},
|
||||||
paste() {
|
paste() {
|
||||||
navigator.clipboard
|
const clip = getClipboard();
|
||||||
|
|
||||||
|
if (!clip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clip
|
||||||
.read()
|
.read()
|
||||||
.then(async (items) => {
|
.then(async (items) => {
|
||||||
const dt = new DataTransfer();
|
const dt = new DataTransfer();
|
||||||
@@ -233,7 +241,13 @@ const currentWebContents = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
pasteAndMatchStyle() {
|
pasteAndMatchStyle() {
|
||||||
navigator.clipboard
|
const clip = getClipboard();
|
||||||
|
|
||||||
|
if (!clip) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clip
|
||||||
.read()
|
.read()
|
||||||
.then(async (items) => {
|
.then(async (items) => {
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
// Shared echo suppression for file watcher.
|
// Shared echo suppression for file watcher.
|
||||||
// fs operations mark paths as "locally modified" so the watcher client
|
// fs operations mark paths as "locally modified" so the watcher client can skip events that originated from this client.
|
||||||
// can skip events that originated from this client.
|
import { normalize } from "../util/path.js";
|
||||||
|
|
||||||
const ECHO_SUPPRESS_MS = 1500;
|
const ECHO_SUPPRESS_MS = 1500;
|
||||||
const recentOps = new Map(); // normalized path -> timestamp
|
const recentOps = new Map(); // normalized path -> timestamp
|
||||||
|
|
||||||
function normalize(p) {
|
|
||||||
return (p || "")
|
|
||||||
.replace(/\\/g, "/")
|
|
||||||
.replace(/^\/+/, "")
|
|
||||||
.replace(/\/+$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function markLocalOp(path) {
|
export function markLocalOp(path) {
|
||||||
recentOps.set(normalize(path), Date.now());
|
recentOps.set(normalize(path), Date.now());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,19 +5,14 @@
|
|||||||
// - 5-minute TTL per entry
|
// - 5-minute TTL per entry
|
||||||
// - Entries kept until TTL expires (plugins may read the same file multiple times)
|
// - Entries kept until TTL expires (plugins may read the same file multiple times)
|
||||||
|
|
||||||
|
import { normalize } from "../util/path.js";
|
||||||
|
|
||||||
const MAX_SIZE = 200 * 1024 * 1024;
|
const MAX_SIZE = 200 * 1024 * 1024;
|
||||||
const TTL_MS = 5 * 60 * 1000;
|
const TTL_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
const cache = new Map(); // path -> { data, size, createdAt }
|
const cache = new Map(); // path -> { data, size, createdAt }
|
||||||
let currentSize = 0;
|
let currentSize = 0;
|
||||||
|
|
||||||
function normalize(p) {
|
|
||||||
return (p || "")
|
|
||||||
.replace(/\\/g, "/")
|
|
||||||
.replace(/^\/+/, "")
|
|
||||||
.replace(/\/+$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function evictExpired() {
|
function evictExpired() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,7 @@
|
|||||||
// Path resolvers map logical paths to physical paths; read transforms post-process bytes after a read; write transforms pre-process bytes before a write.
|
// Path resolvers map logical paths to physical paths; read transforms post-process bytes after a read; write transforms pre-process bytes before a write.
|
||||||
// All hooks run at the shim's public surface, so caches and transport see only physical paths and as-stored bytes.
|
// All hooks run at the shim's public surface, so caches and transport see only physical paths and as-stored bytes.
|
||||||
|
|
||||||
function normalize(p) {
|
import { normalize } from "../util/path.js";
|
||||||
return (p || "").replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Path resolvers ---
|
// --- Path resolvers ---
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
// Virtual plugin source served from memory; the fs shim's read path checks here before disk.
|
// Virtual plugin source served from memory; the fs shim's read path checks here before disk.
|
||||||
|
|
||||||
function normalize(p) {
|
import { normalize } from "../util/path.js";
|
||||||
return (p || "").replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
|
|
||||||
}
|
|
||||||
|
|
||||||
const virtualFiles = new Map();
|
const virtualFiles = new Map();
|
||||||
|
|
||||||
export function setVirtualFile(path, content) {
|
export function setVirtualFile(path, content) {
|
||||||
virtualFiles.set(normalize(path), content);
|
const normalized = normalize(path);
|
||||||
|
|
||||||
|
if (normalized.split("/").includes("..")) {
|
||||||
|
throw new Error(`virtual file path may not contain '..': ${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
virtualFiles.set(normalized, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeVirtualFile(path) {
|
export function removeVirtualFile(path) {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {
|
|||||||
unregisterPopupWindow,
|
unregisterPopupWindow,
|
||||||
} from "./electron/remote/window.js";
|
} from "./electron/remote/window.js";
|
||||||
import { showVaultManager } from "./ui-registry.js";
|
import { showVaultManager } from "./ui-registry.js";
|
||||||
|
import { arrayBufferToBase64, base64ToArrayBuffer } from "./util/base64.js";
|
||||||
|
import { isSameOrigin } from "./util/url.js";
|
||||||
|
|
||||||
function installProcess() {
|
function installProcess() {
|
||||||
window.process = processShim;
|
window.process = processShim;
|
||||||
@@ -115,51 +117,6 @@ function installWindowOpen() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSameOrigin(url) {
|
|
||||||
if (
|
|
||||||
!url ||
|
|
||||||
url.startsWith("/") ||
|
|
||||||
url.startsWith("./") ||
|
|
||||||
url.startsWith("../")
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (url.startsWith("data:") || url.startsWith("blob:")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const parsed = new URL(url, window.location.origin);
|
|
||||||
return parsed.origin === window.location.origin;
|
|
||||||
} catch {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function installFetchShim() {
|
function installFetchShim() {
|
||||||
const originalFetch = window.fetch.bind(window);
|
const originalFetch = window.fetch.bind(window);
|
||||||
window.__originalFetch = originalFetch;
|
window.__originalFetch = originalFetch;
|
||||||
|
|||||||
@@ -1,40 +1,16 @@
|
|||||||
// Override window.requestUrl to proxy external requests through our server, bypassing CORS.
|
// 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.
|
// Obsidian sets window.requestUrl in app.js, so we override it after app.js loads.
|
||||||
|
|
||||||
function base64ToArrayBuffer(base64) {
|
import { isSameOrigin } from "./util/url.js";
|
||||||
const binary = atob(base64);
|
import { arrayBufferToBase64, base64ToArrayBuffer } from "./util/base64.js";
|
||||||
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) {
|
async function proxyRequestUrl(request) {
|
||||||
if (typeof request === "string") {
|
if (typeof request === "string") {
|
||||||
request = { url: request };
|
request = { url: request };
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSameOrigin =
|
// Same-origin requests don't need the proxy.
|
||||||
request.url.startsWith(window.location.origin) ||
|
if (isSameOrigin(request.url)) {
|
||||||
request.url.startsWith("/");
|
|
||||||
|
|
||||||
// Same-origin requests don't need the proxy
|
|
||||||
if (isSameOrigin) {
|
|
||||||
const res = await fetch(request.url, {
|
const res = await fetch(request.url, {
|
||||||
method: request.method || "GET",
|
method: request.method || "GET",
|
||||||
headers: request.headers || {},
|
headers: request.headers || {},
|
||||||
|
|||||||
26
packages/shim/src/util/base64.js
Normal file
26
packages/shim/src/util/base64.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
// Base64 codec for the binary bodies exchanged with the server proxy.
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { arrayBufferToBase64, base64ToArrayBuffer };
|
||||||
7
packages/shim/src/util/path.js
Normal file
7
packages/shim/src/util/path.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
// Canonical key form for fs paths: backslashes to forward slashes, no leading or trailing slash.
|
||||||
|
// Used by caches and registries that key on path.
|
||||||
|
function normalize(p) {
|
||||||
|
return (p || "").replace(/\\/g, "/").replace(/^\/+/, "").replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export { normalize };
|
||||||
24
packages/shim/src/util/url.js
Normal file
24
packages/shim/src/util/url.js
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// True when a request URL targets the page's own origin (so it can skip the cross-origin proxy).
|
||||||
|
function isSameOrigin(url) {
|
||||||
|
if (
|
||||||
|
!url ||
|
||||||
|
url.startsWith("/") ||
|
||||||
|
url.startsWith("./") ||
|
||||||
|
url.startsWith("../")
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.startsWith("data:") || url.startsWith("blob:")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url, window.location.origin);
|
||||||
|
return parsed.origin === window.location.origin;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { isSameOrigin };
|
||||||
@@ -104,6 +104,12 @@ export async function extractObsidianModule() {
|
|||||||
return captured;
|
return captured;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function assertSameOrigin(url) {
|
||||||
|
if (new URL(url, location.origin).origin !== location.origin) {
|
||||||
|
throw new Error(`refusing cross-origin plugin URL: ${url}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Serialize per-id load/unload so rapid toggles can't race.
|
// Serialize per-id load/unload so rapid toggles can't race.
|
||||||
const inFlight = new Map();
|
const inFlight = new Map();
|
||||||
|
|
||||||
@@ -128,7 +134,11 @@ export function loadVirtualPlugin(entry) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertSameOrigin(entry.scriptUrl);
|
||||||
|
|
||||||
if (entry.cssUrl) {
|
if (entry.cssUrl) {
|
||||||
|
assertSameOrigin(entry.cssUrl);
|
||||||
|
|
||||||
const link = document.createElement("link");
|
const link = document.createElement("link");
|
||||||
link.rel = "stylesheet";
|
link.rel = "stylesheet";
|
||||||
link.href = entry.cssUrl;
|
link.href = entry.cssUrl;
|
||||||
|
|||||||
Reference in New Issue
Block a user