diff --git a/Overlord-Server/src/server/plugin-state-bundle.test.ts b/Overlord-Server/src/server/plugin-state-bundle.test.ts
index e844048..3c91513 100644
--- a/Overlord-Server/src/server/plugin-state-bundle.test.ts
+++ b/Overlord-Server/src/server/plugin-state-bundle.test.ts
@@ -6,6 +6,7 @@ import {
arePluginNeedsApproved,
compilePluginTypeScript,
computePluginNeedsHash,
+ ensurePluginExtracted,
loadPluginBundle,
type PluginState,
} from "./plugin-state-bundle";
@@ -70,6 +71,31 @@ describe("loadPluginBundle", () => {
expect(bundle.size).toBe(4);
});
+ test("keeps helper DLLs out of the native platform binary map", async () => {
+ const root = await createTempRoot();
+ const AdmZip = require("adm-zip");
+ const zip = new AdmZip();
+ zip.addFile("config.json", Buffer.from(JSON.stringify({
+ name: "Plink",
+ apiVersion: 2,
+ runtime: "native",
+ uiEntry: "src/ui.ts",
+ })));
+ zip.addFile("plink.html", Buffer.from("
"));
+ zip.addFile("plink.css", Buffer.from("#plink-root { display: block; }"));
+ zip.addFile("src/ui.ts", Buffer.from("document.body.dataset.plugin = 'plink';"));
+ zip.addFile("plink-windows-amd64.dll", Buffer.from("entry-dll"));
+ zip.addFile("plink-payload.dll", Buffer.from("helper-dll"));
+ zip.writeZip(join(root, "plink.zip"));
+
+ await ensurePluginExtracted(root, "plink", (name) => name);
+ const manifest = JSON.parse(await readFile(join(root, "plink", "manifest.json"), "utf-8"));
+ expect(manifest.binaries["windows-amd64"]).toBe("plink-windows-amd64.dll");
+
+ const bundle = await loadPluginBundle(root, "plink", async () => {}, "Windows 11", "amd64");
+ expect(bundle.binaryPath).toBe(join(root, "plink", "plink-windows-amd64.dll"));
+ });
+
test("computes stable need hashes and invalidates changed approvals", () => {
const state: PluginState = {
enabled: {},
diff --git a/Overlord-Server/src/server/plugin-state-bundle.ts b/Overlord-Server/src/server/plugin-state-bundle.ts
index e3cf025..facf4ea 100644
--- a/Overlord-Server/src/server/plugin-state-bundle.ts
+++ b/Overlord-Server/src/server/plugin-state-bundle.ts
@@ -149,11 +149,17 @@ export async function ensurePluginExtracted(
throw new Error(`Invalid plugin bundle: ${safeId} (missing .html, .css, and either .js or src/ui.ts)`);
}
- const binariesMap: Record = {};
for (const [filename, data] of nativeBinaries) {
await fs.writeFile(path.join(pluginDir, filename), data);
+ }
+
+ const binariesMap: Record = {};
+ applyConfiguredNativeBinaries(binariesMap, extraConfig, nativeBinaries);
+ const nativeBinaryNames = Array.from(nativeBinaries.keys())
+ .sort((a, b) => nativeBinaryPriority(a, safeId) - nativeBinaryPriority(b, safeId) || a.localeCompare(b));
+ for (const filename of nativeBinaryNames) {
const platformKey = derivePlatformKey(filename);
- if (platformKey) {
+ if (platformKey && !binariesMap[platformKey]) {
binariesMap[platformKey] = filename;
}
}
@@ -203,6 +209,7 @@ export async function ensurePluginExtracted(
runtime,
version: extraConfig.version || "1.0.0",
description: extraConfig.description,
+ ...(typeof extraConfig.binary === "string" && extraConfig.binary && { binary: extraConfig.binary }),
...(runtime === "wasm" && { wasm: String(extraConfig.wasm || wasmEntry?.filename || binariesMap["wasm32-wasi"] || "") }),
...(needs && { needs }),
binaries: binariesMap,
@@ -345,7 +352,7 @@ export async function loadPluginBundle(
const files = await fs.readdir(dir);
const archRegex = /-(linux|darwin|windows|freebsd)-(amd64|arm64|arm|386)\.(so|dll|dylib)$/i;
- const found = files.find((f) => {
+ const found = files.find((f: string) => {
const m = f.match(archRegex);
if (m) {
return (
@@ -733,3 +740,38 @@ function derivePlatformKey(filename: string): string {
if (lower.endsWith(".so")) return "linux-amd64";
return "";
}
+
+function applyConfiguredNativeBinaries(
+ binariesMap: Record,
+ config: any,
+ nativeBinaries: Map,
+): void {
+ if (config?.binaries && typeof config.binaries === "object") {
+ for (const [platformKey, filename] of Object.entries(config.binaries)) {
+ if (typeof platformKey !== "string" || typeof filename !== "string") continue;
+ const base = path.basename(filename);
+ if (!nativeBinaries.has(base)) continue;
+ binariesMap[platformKey.toLowerCase()] = base;
+ }
+ }
+
+ if (typeof config?.binary === "string" && config.binary) {
+ const base = path.basename(config.binary);
+ if (!nativeBinaries.has(base)) return;
+ const platformKey = derivePlatformKey(base);
+ if (platformKey) {
+ binariesMap[platformKey] = base;
+ }
+ }
+}
+
+function nativeBinaryPriority(filename: string, pluginId: string): number {
+ const escapedId = pluginId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ if (new RegExp(`^${escapedId}-(linux|darwin|windows|freebsd)-(amd64|arm64|arm|386)\\.(so|dll|dylib)$`, "i").test(filename)) {
+ return 0;
+ }
+ if (/-(linux|darwin|windows|freebsd)-(amd64|arm64|arm|386)\.(so|dll|dylib)$/i.test(filename)) {
+ return 1;
+ }
+ return 2;
+}