CH-J Server Managerserver management over SSH
Menu
Published source

CH-J Server Manager

Browse directories and files for a specific application release.

Download source ZIP
CH-J Proprietary Software License 1.14

Source is provided under the CH-J Proprietary Software License 1.14. Its availability does not change the license terms or grant additional rights.

6,8 KB · 80 linesDownload file
1"use strict";
3const test = require("node:test");
4const assert = require("node:assert/strict");
5const fs = require("node:fs");
6const os = require("node:os");
7const path = require("node:path");
8const { EventEmitter } = require("node:events");
9const { PluginRegistry, PLUGIN_API_VERSION, supportsPluginApi, validateManifest } = require("../src/main/plugins/pluginRegistry");
10const { PLUGIN_CSP, PluginRuntime } = require("../src/main/plugins/pluginRuntime");
12const pluginRoot = path.join(__dirname, "..", "src", "main", "firstPartyPlugins");
13const manifestPath = path.join(pluginRoot, "chj.hash-checksum", "0.0.1", "manifest.json");
15test("Hash & Checksum is a bundled first-party plugin on Plugin API 1.2", (t) => {
16 const storage = fs.mkdtempSync(path.join(os.tmpdir(), "chj-hash-registry-")); t.after(() => fs.rmSync(storage, { recursive: true, force: true }));
17 const manifest = validateManifest(JSON.parse(fs.readFileSync(manifestPath, "utf8")));
18 assert.equal(PLUGIN_API_VERSION, "1.2.0"); assert.equal(manifest.pluginApi, "^1.2.0"); assert.deepEqual(manifest.permissions, ["local.hash"]);
19 assert.equal(supportsPluginApi("^1.0.0"), true); assert.equal(supportsPluginApi("^1.1.0"), true); assert.equal(supportsPluginApi("^1.2.0"), true);
20 const registry = new PluginRegistry(storage, null, { bundledRoot: pluginRoot }); const installed = registry.getInstalled("chj.hash-checksum");
21 assert.equal(installed.bundled, true); assert.equal(registry.resolveEntry(installed.id).entryPath.endsWith(path.join("ui", "index.html")), true);
22});
24test("plugin UI is network-isolated and contains no Node, remote-session, or path-based access", () => {
25 const preload = fs.readFileSync(path.join(__dirname, "..", "src", "preload", "pluginPreload.js"), "utf8");
26 const ui = fs.readFileSync(path.join(pluginRoot, "chj.hash-checksum", "0.0.1", "ui", "index.js"), "utf8");
27 assert.match(PLUGIN_CSP, /connect-src 'none'/); assert.match(preload, /plugin:hashing:selectFiles/); assert.doesNotMatch(preload, /require\(["']node:fs/);
28 assert.doesNotMatch(ui, /\brequire\s*\(/); assert.doesNotMatch(ui, /\b(?:ssh|sftp|scp|remote\.exec)\b/i); assert.doesNotMatch(ui, /filePath|localPath|absolutePath/);
29});
31test("hash IPC enforces local.hash, scopes calls and progress to the owning plugin, and cleans up on close", async () => {
32 const handlers = new Map(); const calls = []; const events = []; const hashing = new EventEmitter();
33 hashing.getAlgorithms = () => [{ id: "sha256" }];
34 hashing.start = (pluginId, payload) => { calls.push(["start", pluginId, payload.selectionId]); return { jobId: "job_owner" }; };
35 hashing.status = (pluginId, jobId) => { calls.push(["status", pluginId, jobId]); if (pluginId !== "owner") throw Object.assign(new Error("Unknown job"), { code: "HASH_INVALID_JOB" }); return { jobId }; };
36 hashing.cancel = async (pluginId, jobId) => { calls.push(["cancel", pluginId, jobId]); return { state: "cancelled" }; };
37 hashing.selectFiles = async () => ({}); hashing.selectDirectory = async () => ({}); hashing.selectManifest = async () => ({}); hashing.selectManifestDestination = async () => ({});
38 hashing.verify = () => ({}); hashing.compare = () => ({}); hashing.generateManifest = () => ({}); hashing.verifyManifest = () => ({}); hashing.exportResults = () => ({}); hashing.copyResult = () => ({});
39 hashing.cleanupPlugin = (pluginId) => calls.push(["cleanup", pluginId]);
40 const runtime = new PluginRuntime({ BrowserWindow: class {}, registry: {}, sessionManager: {}, localHashService: hashing, isVaultUnlocked: () => true });
41 runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });
42 const makeWindow = (id) => ({ isDestroyed: () => false, close: () => {}, webContents: { id, send: (channel, payload) => events.push([id, channel, payload.jobId]) } });
43 const ownerWindow = makeWindow(10); const deniedWindow = makeWindow(11); const otherWindow = makeWindow(12);
44 runtime.contexts.set(10, { manifest: { id: "owner", permissions: ["local.hash"] }, window: ownerWindow });
45 runtime.contexts.set(11, { manifest: { id: "denied", permissions: [] }, window: deniedWindow });
46 runtime.contexts.set(12, { manifest: { id: "other", permissions: ["local.hash"] }, window: otherWindow });
47 runtime.windows.set("owner", ownerWindow); runtime.windows.set("other", otherWindow);
48 assert.deepEqual(await handlers.get("plugin:hashing:start")({ sender: { id: 10 } }, { selectionId: "opaque" }), { jobId: "job_owner" });
49 await assert.rejects(() => handlers.get("plugin:hashing:start")({ sender: { id: 11 } }, { selectionId: "opaque" }), { code: "PLUGIN_PERMISSION_DENIED" });
50 await assert.rejects(() => handlers.get("plugin:hashing:status")({ sender: { id: 12 } }, { jobId: "job_owner" }), { code: "HASH_INVALID_JOB" });
51 hashing.emit("progress", { pluginId: "owner", job: { jobId: "job_owner" } }); assert.deepEqual(events, [[10, "plugin:hashing:progress", "job_owner"]]);
52 ownerWindow.webContents = { id: 10, send: () => {} }; runtime.contexts.set(10, { manifest: { id: "owner", permissions: ["local.hash"] }, window: ownerWindow });
53 hashing.cleanupPlugin("owner"); assert.ok(calls.some((call) => call[0] === "cleanup" && call[1] === "owner"));
54});
56test("plugin language IPC uses the app language and broadcasts updates to open windows", async () => {
57 const handlers = new Map();
58 const events = [];
59 const runtime = new PluginRuntime({ BrowserWindow: class {}, registry: {}, getLanguage: () => "cs", isVaultUnlocked: () => true });
60 runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });
61 const window = { isDestroyed: () => false, webContents: { send: (channel, payload) => events.push([channel, payload]) } };
62 runtime.contexts.set(21, { manifest: { id: "chj.hash-checksum", permissions: ["local.hash"] }, window });
63 runtime.windows.set("chj.hash-checksum", window);
64 runtime.windows.set("closed", { isDestroyed: () => true });
65 assert.equal(await handlers.get("plugin:ui:getLanguage")({ sender: { id: 21 } }), "cs");
66 await assert.rejects(() => handlers.get("plugin:ui:getLanguage")({ sender: { id: 999 } }));
67 runtime.notifyLanguageChanged("de");
68 assert.deepEqual(events, [["plugin:ui:languageChanged", "de"]]);
69});
71test("hash errors retain their code in the IPC message for localized renderer errors", async () => {
72 const handlers = new Map();
73 const runtime = new PluginRuntime({ BrowserWindow: class {}, registry: {}, isVaultUnlocked: () => true, localHashService: {
74 on: () => {},
75 start: () => { throw Object.assign(new Error("Invalid key"), { code: "HASH_INVALID_KEY" }); }
76 } });
77 runtime.registerIpc({ handle: (channel, handler) => handlers.set(channel, handler) });
78 runtime.contexts.set(22, { manifest: { id: "chj.hash-checksum", permissions: ["local.hash"] }, window: { isDestroyed: () => false } });
79 await assert.rejects(() => handlers.get("plugin:hashing:start")({ sender: { id: 22 } }), { code: "HASH_INVALID_KEY", message: "HASH_INVALID_KEY: Invalid key" });
80});

SHA-256: 9d363b9e71fe399a4d6e3030e39c559342ee6a90660e61665dc4f9a8ded5cee9

Archive SHA-256: 5ac91caf4fa32a6fdb114f2430deed486fbe7489d5eea343d1f034169fafb5e0