Published source
Download source ZIP
CH-J Server Manager
Browse directories and files for a specific application release.
Source is provided under the CH-J Proprietary Software License 1.14. Its availability does not change the license terms or grant additional rights.
1
"use strict";3
const test = require("node:test");4
const assert = require("node:assert/strict");5
const { EventEmitter } = require("node:events");6
const { PluginRuntime } = require("../src/main/plugins/pluginRuntime");8
class FakeWindow extends EventEmitter {9
constructor() {10
super();11
let protocolHandler = null;12
this.protocolRegistrations = 0;13
this.webContents = new EventEmitter();14
this.webContents.id = 42;15
this.webContents.session = {16
protocol: {17
isProtocolHandled: () => Boolean(protocolHandler),18
handle: (scheme, handler) => {19
assert.equal(scheme, "chj-plugin");20
protocolHandler = handler;21
this.protocolRegistrations += 1;22
}23
},24
setPermissionRequestHandler: () => {},25
setPermissionCheckHandler: () => {}26
};27
this.webContents.setWindowOpenHandler = () => {};28
this.destroyed = false;29
this.visible = false;30
this.loadedUrl = null;31
}33
isDestroyed() { return this.destroyed; }34
isMinimized() { return false; }35
show() { this.visible = true; }36
hide() { this.visible = false; }37
focus() { this.focused = true; }38
loadURL(url) { this.loadedUrl = url; return Promise.resolve(); }39
close() { this.destroyed = true; this.emit("closed"); }40
}42
test("Core chrome CSS is available to strict self-only plugin CSP without exposing other Core files", async () => {43
const runtime = new PluginRuntime({ registry: { resolveEntry: () => ({ manifest: { entry: "ui/index.html" }, root: "/missing-plugin" }) } });44
const response = await runtime.handleRequest({ url: "chj-plugin://chj.key-generator/__chj_core__/window-chrome.css" });45
assert.equal(response.status, 200);46
assert.equal(response.headers.get("Content-Type"), "text/css; charset=utf-8");47
assert.match(await response.text(), /#chj-chrome-collapse/);48
const denied = await runtime.handleRequest({ url: "chj-plugin://chj.key-generator/__chj_core__/windowChrome.js" });49
assert.equal(denied.status, 404);50
});52
test("plugin runtime registers the custom protocol in the isolated plugin session", () => {53
let pluginWindow;54
let pluginWindowOptions;55
const windowStates = [];56
const managerWindow = Object.assign(new EventEmitter(), { destroyed: false, minimized: false, shown: false, focused: false, isDestroyed() { return this.destroyed; }, isMinimized() { return this.minimized; }, restore() { this.minimized = false; }, show() { this.shown = true; }, focus() { this.focused = true; } });57
const manifest = {58
id: "chj.system-monitor",59
name: "System Monitor",60
version: "0.0.1",61
entry: "ui/index.html",62
permissions: ["session.read", "system.metrics.read"]63
};64
const runtime = new PluginRuntime({65
BrowserWindow: class extends FakeWindow { constructor(options) { super(); pluginWindow = this; pluginWindowOptions = options; } },66
registry: { resolveEntry: () => ({ manifest, root: "/plugin", entryPath: "/plugin/ui/index.html" }) },67
sessionManager: {},68
getMainWindow: () => managerWindow,69
onWindowState: (windows) => windowStates.push(windows),70
isVaultUnlocked: () => true,71
preload: "/pluginPreload.js"72
});74
runtime.open(manifest.id);75
assert.equal(pluginWindow.protocolRegistrations, 1);76
assert.equal(pluginWindow.loadedUrl, "chj-plugin://chj.system-monitor/ui/index.html");77
assert.equal(pluginWindowOptions.parent, managerWindow);78
assert.equal(pluginWindowOptions.modal, false);79
pluginWindow.emit("ready-to-show");80
pluginWindow.emit("minimize");81
assert.equal(pluginWindow.visible, false);82
assert.equal(managerWindow.focused, true);83
assert.equal(windowStates.at(-1)[0].minimized, true);84
runtime.open(manifest.id);85
assert.equal(pluginWindow.protocolRegistrations, 1);86
assert.equal(pluginWindow.visible, true);87
assert.equal(runtime.listWindows()[0].minimized, false);88
managerWindow.minimized = true;89
managerWindow.focused = false;90
managerWindow.emit("minimize");91
assert.equal(pluginWindow.visible, false);92
assert.equal(managerWindow.minimized, true);93
assert.equal(managerWindow.focused, false);94
assert.equal(runtime.listWindows()[0].minimized, true);95
pluginWindow.close();96
assert.equal(managerWindow.listenerCount("minimize"), 0);97
});99
test("plugin collapse IPC docks only the sender's window and restores it on reopen", async () => {100
const handlers = new Map();101
let pluginWindow;102
const manifest = { id: "chj.hash-checksum", name: "Hash & Checksum", entry: "ui/index.html", permissions: [] };103
const runtime = new PluginRuntime({104
BrowserWindow: class extends FakeWindow { constructor() { super(); pluginWindow = this; } },105
registry: { resolveEntry: () => ({ manifest, root: "/plugin" }) },106
isVaultUnlocked: () => true107
});108
runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });109
runtime.open(manifest.id);110
pluginWindow.emit("ready-to-show");111
const collapse = handlers.get("plugin:window:minimize");112
await assert.rejects(() => collapse({ sender: { id: 999 } }), { code: "UNTRUSTED_PLUGIN_SENDER" });113
await collapse({ sender: { id: pluginWindow.webContents.id } });114
assert.equal(pluginWindow.visible, false);115
assert.equal(runtime.listWindows()[0].minimized, true);116
runtime.open(manifest.id);117
assert.equal(pluginWindow.visible, true);118
assert.equal(runtime.listWindows()[0].minimized, false);119
});121
test("plugin runtime routes user capabilities by sender identity and manifest permission", async () => {122
const handlers = new Map();123
const calls = [];124
const runtime = new PluginRuntime({125
BrowserWindow: FakeWindow,126
registry: {},127
sessionManager: {128
readUsers: async (sessionId) => { calls.push(["read", sessionId]); return [{ username: "test" }]; },129
manageUser: async (sessionId, payload) => { calls.push(["manage", sessionId, payload.action]); return { action: payload.action }; }130
},131
logService: { readTail: async () => ({ text: "forbidden" }) },132
keyGeneratorService: {},133
isVaultUnlocked: () => true134
});135
runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });136
const window = { isDestroyed: () => false, close: () => {} };137
runtime.contexts.set(77, { manifest: { permissions: ["users.read", "users.manage"] }, window });138
const event = { sender: { id: 77 } };139
assert.deepEqual(await handlers.get("plugin:users:list")(event, { sessionId: "terminal-users" }), [{ username: "test" }]);140
assert.deepEqual(await handlers.get("plugin:users:manage")(event, { sessionId: "terminal-users", action: "lock", username: "bob" }), { action: "lock" });141
assert.deepEqual(calls, [["read", "terminal-users"], ["manage", "terminal-users", "lock"]]);142
await assert.rejects(() => handlers.get("plugin:logs:read")(event, {}), { code: "PLUGIN_PERMISSION_DENIED" });143
});145
test("plugin runtime routes file capabilities and keeps permissions separate", async () => {146
const handlers = new Map();147
const calls = [];148
const remoteFileService = {149
list: async (sessionId, path) => { calls.push(["list", sessionId, path]); return [{ name: "a.txt" }]; },150
readText: async (sessionId, path) => { calls.push(["readText", sessionId, path]); return { text: "ahoj" }; },151
writeText: async (sessionId, path, text) => { calls.push(["writeText", sessionId, path, text]); return { size: 4 }; },152
createFile: async () => ({}), mkdir: async () => ({}), rename: async () => ({}), remove: async () => ({}), removeMany: async () => ({}),153
upload: async () => ({}), download: async () => ({}), downloadMany: async () => ({}), downloadArchive: async () => ({})154
};155
const runtime = new PluginRuntime({ BrowserWindow: FakeWindow, registry: {}, sessionManager: {}, remoteFileService, isVaultUnlocked: () => true });156
runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });157
const window = { isDestroyed: () => false, close: () => {} };158
runtime.contexts.set(88, { manifest: { permissions: ["files.read"] }, window });159
const event = { sender: { id: 88 } };161
assert.deepEqual(await handlers.get("plugin:files:list")(event, { sessionId: "sftp-1", path: "/home/test" }), [{ name: "a.txt" }]);162
assert.deepEqual(await handlers.get("plugin:files:readText")(event, { sessionId: "sftp-1", path: "/home/test/a.txt" }), { text: "ahoj" });163
await assert.rejects(() => handlers.get("plugin:files:writeText")(event, { sessionId: "sftp-1", path: "/home/test/a.txt", text: "nově" }), { code: "PLUGIN_PERMISSION_DENIED" });164
await assert.rejects(() => handlers.get("plugin:files:download")(event, { sessionId: "sftp-1", path: "/home/test/a.txt" }), { code: "PLUGIN_PERMISSION_DENIED" });165
for (const capability of ["saveText", "cleanupRecovery"]) {166
await assert.rejects(() => handlers.get(`plugin:files:${capability}`)(event, { sessionId: "sftp-1" }), { code: "PLUGIN_PERMISSION_DENIED" });167
}168
assert.deepEqual(calls, [["list", "sftp-1", "/home/test"], ["readText", "sftp-1", "/home/test/a.txt"]]);169
});171
test("editor IPC forwards structured options, owns document handles and protects recovery reads", async () => {172
const handlers = new Map(), calls = [];173
const remoteFileService = {};174
for (const method of ["readText", "saveText", "listRecovery", "readRecovery", "cleanupRecovery", "closeText"]) {175
remoteFileService[method] = (...args) => { calls.push([method, ...args]); return { ok: true }; };176
}177
const runtime = new PluginRuntime({ BrowserWindow: FakeWindow, registry: {}, remoteFileService, isVaultUnlocked: () => true });178
runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });179
const window = { isDestroyed: () => false };180
runtime.contexts.set(91, { manifest: { id: "editor", permissions: ["files.read", "files.write"] }, window });181
runtime.contexts.set(92, { manifest: { id: "other", permissions: [] }, window });182
const options = { editId: "opaque", sudo: true, sudoPassword: "secret" };183
await handlers.get("plugin:files:saveText")({ sender: { id: 91 } }, { sessionId: "s", path: "/etc/config", text: "content", options });184
assert.deepEqual(calls[0], ["saveText", "s", "/etc/config", "content", options, "editor"]);185
await handlers.get("plugin:files:closeText")({ sender: { id: 91 } }, { editId: "opaque" });186
assert.deepEqual(calls[1], ["closeText", "opaque", "editor"]);187
for (const method of ["listRecovery", "readRecovery", "closeText"]) {188
await assert.rejects(() => handlers.get(`plugin:files:${method}`)({ sender: { id: 92 } }), { code: "PLUGIN_PERMISSION_DENIED" });189
}190
});192
test("plugin runtime separates NGINX read and manage capabilities", async () => {193
const handlers = new Map();194
const calls = [];195
const sessionManager = {196
inspectNginx: async (sessionId) => { calls.push(["inspect", sessionId]); return { installed: true }; },197
readNginxConfig: async () => ({}), dumpNginxConfig: async () => ({}), testNginxConfig: async () => ({ ok: true }),198
saveNginxConfig: async (sessionId, payload) => { calls.push(["save", sessionId, payload.path]); return { path: payload.path }; },199
reloadNginx: async () => ({ ok: true })200
};201
const runtime = new PluginRuntime({ BrowserWindow: FakeWindow, registry: {}, sessionManager, isVaultUnlocked: () => true });202
runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });203
const window = { isDestroyed: () => false, close: () => {} };204
const event = { sender: { id: 99 } };205
runtime.contexts.set(99, { manifest: { permissions: ["nginx.read"] }, window });206
assert.deepEqual(await handlers.get("plugin:nginx:inspect")(event, { sessionId: "nginx-1" }), { installed: true });207
await assert.rejects(() => handlers.get("plugin:nginx:saveConfig")(event, { sessionId: "nginx-1", path: "/etc/nginx/nginx.conf" }), { code: "PLUGIN_PERMISSION_DENIED" });208
runtime.contexts.set(99, { manifest: { permissions: ["nginx.read", "nginx.manage"] }, window });209
assert.deepEqual(await handlers.get("plugin:nginx:saveConfig")(event, { sessionId: "nginx-1", path: "/etc/nginx/nginx.conf" }), { path: "/etc/nginx/nginx.conf" });210
assert.deepEqual(calls, [["inspect", "nginx-1"], ["save", "nginx-1", "/etc/nginx/nginx.conf"]]);211
});213
test("closing a destroyed plugin window releases its context without reading webContents", () => {214
let pluginWindow;215
const cleanup = [];216
const manifest = { id: "chj.hash-checksum", name: "Hash & Checksum", version: "0.0.1", entry: "ui/index.html", permissions: ["local.hash"] };217
const runtime = new PluginRuntime({218
BrowserWindow: class extends FakeWindow { constructor() { super(); pluginWindow = this; } },219
registry: { resolveEntry: () => ({ manifest, root: "/plugin" }) },220
localHashService: { on: () => {}, cleanupPlugin: (id) => cleanup.push(id) },221
isVaultUnlocked: () => true222
});223
runtime.open(manifest.id);224
const senderId = pluginWindow.webContents.id;225
Object.defineProperty(pluginWindow, "webContents", { get() { throw new Error("Object has been destroyed"); } });226
pluginWindow.destroyed = true;227
assert.doesNotThrow(() => pluginWindow.emit("closed"));228
assert.equal(runtime.contexts.has(senderId), false);229
assert.equal(runtime.windows.has(manifest.id), false);230
assert.deepEqual(cleanup, [manifest.id]);231
});SHA-256: 0575cb8e60be5ac2c60e5024716b1a0829a68c877c1a46c2abaff2ff74ff2710
Archive SHA-256: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0