Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
116 lines
4.5 KiB
JavaScript
116 lines
4.5 KiB
JavaScript
"use strict";
|
|
|
|
const { randomUUID } = require("node:crypto");
|
|
|
|
const DEFAULT_RASTER_TIMEOUT_MS = 5_000;
|
|
|
|
function withTimeout(promise, timeoutMs, message) {
|
|
let timer;
|
|
return Promise.race([
|
|
promise,
|
|
new Promise((_, reject) => {
|
|
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
}),
|
|
]).finally(() => clearTimeout(timer));
|
|
}
|
|
|
|
function browserRasterizeExpression(payload) {
|
|
return `(async (payload) => {
|
|
const image = new Image();
|
|
image.decoding = "sync";
|
|
if (payload.mimeType === "image/svg+xml") {
|
|
const bytes = Uint8Array.from(atob(payload.source), (character) => character.charCodeAt(0));
|
|
const source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
const document = new DOMParser().parseFromString(source, "image/svg+xml");
|
|
const root = document.documentElement;
|
|
if (root.localName !== "svg" || document.querySelector("parsererror")) {
|
|
throw new Error("Plugin contribution SVG icon is invalid");
|
|
}
|
|
root.setAttribute("width", String(payload.width));
|
|
root.setAttribute("height", String(payload.height));
|
|
const normalized = new XMLSerializer().serializeToString(root);
|
|
image.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(normalized);
|
|
} else {
|
|
image.src = "data:" + payload.mimeType + ";base64," + payload.source;
|
|
}
|
|
await image.decode();
|
|
if (image.naturalWidth !== payload.width || image.naturalHeight !== payload.height) {
|
|
throw new Error("Plugin contribution icon dimensions changed during decoding");
|
|
}
|
|
const scale = Math.min(1, payload.maxEdge / image.naturalWidth, payload.maxEdge / image.naturalHeight);
|
|
const width = Math.max(1, Math.round(image.naturalWidth * scale));
|
|
const height = Math.max(1, Math.round(image.naturalHeight * scale));
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = width;
|
|
canvas.height = height;
|
|
const context = canvas.getContext("2d", { alpha: true, desynchronized: false });
|
|
if (!context) throw new Error("Plugin contribution icon canvas is unavailable");
|
|
context.clearRect(0, 0, width, height);
|
|
context.drawImage(image, 0, 0, width, height);
|
|
return canvas.toDataURL("image/png");
|
|
})(${JSON.stringify(payload)})`;
|
|
}
|
|
|
|
function createIsolatedContributionIconRasterizer(options) {
|
|
const BrowserWindow = options?.BrowserWindow;
|
|
const randomUUIDFn = options?.randomUUID ?? randomUUID;
|
|
const timeoutMs = options?.timeoutMs ?? DEFAULT_RASTER_TIMEOUT_MS;
|
|
return async ({ body, mimeType, width, height, maxEdge }) => {
|
|
if (typeof BrowserWindow !== "function") {
|
|
throw new Error("Isolated plugin contribution icon rasterizer is unavailable");
|
|
}
|
|
const partition = `plugin-icon-raster-${randomUUIDFn()}`;
|
|
const win = new BrowserWindow({
|
|
show: false,
|
|
width: maxEdge,
|
|
height: maxEdge,
|
|
webPreferences: {
|
|
partition,
|
|
sandbox: true,
|
|
spellcheck: false,
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
webSecurity: true,
|
|
allowRunningInsecureContent: false,
|
|
backgroundThrottling: false,
|
|
},
|
|
});
|
|
try {
|
|
win.removeMenu?.();
|
|
win.webContents?.session?.webRequest?.onBeforeRequest?.(
|
|
{ urls: ["http://*/*", "https://*/*", "file://*/*", "ftp://*/*"] },
|
|
(_details, callback) => callback({ cancel: true }),
|
|
);
|
|
await withTimeout(
|
|
Promise.resolve(win.loadURL("data:text/html;charset=utf-8,%3Cmeta%20http-equiv%3D%22Content-Security-Policy%22%20content%3D%22default-src%20'none'%3B%20img-src%20data%3A%3B%22%3E")),
|
|
timeoutMs,
|
|
"Plugin contribution icon rasterizer did not load in time",
|
|
);
|
|
const result = await withTimeout(
|
|
Promise.resolve(win.webContents.executeJavaScript(browserRasterizeExpression({
|
|
source: body.toString("base64"),
|
|
mimeType,
|
|
width,
|
|
height,
|
|
maxEdge,
|
|
}), true)),
|
|
timeoutMs,
|
|
"Plugin contribution icon rasterization timed out",
|
|
);
|
|
const prefix = "data:image/png;base64,";
|
|
if (typeof result !== "string" || !result.startsWith(prefix)) {
|
|
throw new Error("Plugin contribution icon rasterizer returned an invalid result");
|
|
}
|
|
return Buffer.from(result.slice(prefix.length), "base64");
|
|
} finally {
|
|
try { win.destroy(); } catch {}
|
|
}
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
DEFAULT_RASTER_TIMEOUT_MS,
|
|
browserRasterizeExpression,
|
|
createIsolatedContributionIconRasterizer,
|
|
};
|