Julkaistu lähde
Lataa lähde ZIP
CH-J Tools
Selaile hakemistoja ja tiedostoja tietyn sovelluksen julkaisua varten.
CH-J / Server Manager
406 tiedostot
core-20261024T135205Z-7c487d719de2
Lisätty: · Rakennettu:
1
"use strict";3
const fs = require("node:fs");4
const path = require("node:path");5
const { compareVersions, parseVersion } = require("../../shared/version");7
const PLUGIN_ID_PATTERN = /^[a-z0-9][a-z0-9.-]{2,63}$/;8
const PLUGIN_API_VERSION = "1.7.0";9
const ALLOWED_PERMISSIONS = new Set([10
"session.read",11
"system.metrics.read",12
"system.memory.manage",13
"disks.read",14
"disks.manage",15
"keys.generate",16
"files.read",17
"files.write",18
"files.transfer",19
"users.read",20
"users.manage",21
"nginx.read",22
"nginx.manage",23
"certbot.manage",24
"mail.read",25
"mail.manage",26
"logs.read",27
"remote.exec",28
"local.hash"29
]);31
function validateManifest(input) {32
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Plugin manifest must be an object.");33
const id = String(input.id || "").trim();34
if (!PLUGIN_ID_PATTERN.test(id)) throw new Error("Invalid plugin ID.");35
const version = String(input.version || "").trim();36
parseVersion(version);37
const entry = String(input.entry || "").replace(/\\/g, "/");38
if (!entry || entry.startsWith("/") || entry.split("/").includes("..")) throw new Error("Invalid plugin entry path.");39
const permissions = Array.isArray(input.permissions) ? [...new Set(input.permissions.map(String))] : [];40
for (const permission of permissions) {41
if (!ALLOWED_PERMISSIONS.has(permission)) throw new Error(`Unsupported plugin permission: ${permission}`);42
}43
const schemaVersion = Number(input.schemaVersion || 1);44
if (schemaVersion !== 1) throw new Error(`Unsupported plugin manifest schema: ${schemaVersion}`);45
const minAppVersion = String(input.minAppVersion || "0.0.1");46
parseVersion(minAppVersion);47
const pluginApi = String(input.pluginApi || "^1.0.0");48
if (!/^\^1\.\d+\.\d+$/.test(pluginApi)) throw new Error(`Unsupported plugin API: ${pluginApi}`);49
return {50
schemaVersion,51
id,52
name: String(input.name || id).trim().slice(0, 100),53
description: String(input.description || "").trim().slice(0, 500),54
version,55
publisher: String(input.publisher || "Unknown").trim().slice(0, 100),56
entry,57
pluginApi,58
minAppVersion,59
permissions60
};61
}63
function supportsPluginApi(range, supportedVersion = PLUGIN_API_VERSION) {64
const required = String(range || "").match(/^\^1\.(\d+)\.(\d+)$/);65
const supported = String(supportedVersion || "").match(/^1\.(\d+)\.(\d+)$/);66
return Boolean(required && supported && (Number(required[1]) < Number(supported[1]) || (Number(required[1]) === Number(supported[1]) && Number(required[2]) <= Number(supported[2]))));67
}69
class PluginRegistry {70
constructor(rootDir, logger, options = {}) {71
this.rootDir = path.join(rootDir, "plugins");72
this.bundledRoot = options.bundledRoot || null;73
this.logger = logger;74
this.locations = new Map();75
}77
listInstalled() {78
fs.mkdirSync(this.rootDir, { recursive: true });79
this.locations.clear();80
const plugins = [];81
const roots = [{ root: this.rootDir, bundled: false }];82
if (this.bundledRoot && fs.statSync(this.bundledRoot, { throwIfNoEntry: false })?.isDirectory()) roots.push({ root: this.bundledRoot, bundled: true });83
const candidatesById = new Map();84
for (const source of roots) for (const idEntry of fs.readdirSync(source.root, { withFileTypes: true })) {85
if (!idEntry.isDirectory() || !PLUGIN_ID_PATTERN.test(idEntry.name)) continue;86
const idRoot = path.join(source.root, idEntry.name);87
const candidates = [];88
for (const versionEntry of fs.readdirSync(idRoot, { withFileTypes: true })) {89
if (!versionEntry.isDirectory() || versionEntry.name.startsWith(".")) continue;90
const manifestPath = path.join(idRoot, versionEntry.name, "manifest.json");91
try {92
const manifest = validateManifest(JSON.parse(fs.readFileSync(manifestPath, "utf8")));93
if (manifest.id !== idEntry.name || manifest.version !== versionEntry.name) {94
throw new Error("Plugin directory does not match its manifest.");95
}96
const entryPath = path.join(idRoot, versionEntry.name, manifest.entry);97
if (!fs.statSync(entryPath).isFile()) throw new Error("Plugin entry file is missing.");98
let installation = {};99
try { installation = JSON.parse(fs.readFileSync(path.join(idRoot, versionEntry.name, ".installation.json"), "utf8")); } catch {}100
candidates.push({101
...manifest,102
bundled: source.bundled,103
installedReleaseId: typeof installation.releaseId === "string" ? installation.releaseId : null,104
installedSha512: /^[a-f0-9]{128}$/i.test(String(installation.sha512 || "")) ? String(installation.sha512).toLowerCase() : null,105
installedAt: typeof installation.installedAt === "string" ? installation.installedAt : null,106
installedPublishedAt: typeof installation.publishedAt === "string" ? installation.publishedAt : null,107
_root: path.join(idRoot, versionEntry.name)108
});109
} catch (error) {110
this.logger?.warn("Ignoring invalid installed plugin.", {111
pluginId: idEntry.name,112
version: versionEntry.name,113
message: error?.message || String(error)114
});115
}116
}117
if (!candidatesById.has(idEntry.name)) candidatesById.set(idEntry.name, []);118
candidatesById.get(idEntry.name).push(...candidates);119
}120
for (const candidates of candidatesById.values()) {121
candidates.sort((left, right) => compareVersions(right.version, left.version) || Number(right.bundled) - Number(left.bundled));122
if (!candidates[0]) continue;123
const selected = candidates[0];124
this.locations.set(`${selected.id}@${selected.version}`, selected._root);125
const { _root, ...publicManifest } = selected;126
plugins.push(publicManifest);127
}128
return plugins.sort((left, right) => left.name.localeCompare(right.name));129
}131
getInstalled(pluginId) {132
const id = String(pluginId || "");133
if (!PLUGIN_ID_PATTERN.test(id)) throw new Error("Invalid plugin ID.");134
return this.listInstalled().find((plugin) => plugin.id === id) || null;135
}137
resolveEntry(pluginId) {138
const manifest = this.getInstalled(pluginId);139
if (!manifest) throw new Error("Plugin is not installed.");140
const root = this.locations.get(`${manifest.id}@${manifest.version}`) || path.join(this.rootDir, manifest.id, manifest.version);141
return { manifest, root, entryPath: path.join(root, ...manifest.entry.split("/")) };142
}144
removeInstalled(pluginId) {145
const id = String(pluginId || "");146
if (!PLUGIN_ID_PATTERN.test(id)) throw new Error("Invalid plugin ID.");147
const directory = path.join(this.rootDir, id);148
if (!fs.existsSync(directory)) return { removed: false, pluginId: id };149
fs.rmSync(directory, { recursive: true, force: true });150
this.logger?.info("Plugin removed.", { pluginId: id });151
return { removed: true, pluginId: id };152
}153
}155
module.exports = {156
ALLOWED_PERMISSIONS,157
PLUGIN_API_VERSION,158
PLUGIN_ID_PATTERN,159
PluginRegistry,160
supportsPluginApi,161
validateManifest162
};SHA-256
39c95e8487b2c5c75be9cc001e0ec5d40f56989a676f363933ebe5d63aec2812CH-J Proprietary Software License 1.14 · Lähde ilmoitetaan CH-J Proprietary Software License 1.14-koodilla. Sen saatavuus ei muuta lisenssiehtoja eikä myönnä lisäoikeuksia.
Arkisto SHA-256
476ed8aef0f1e96a5acdf223127b6692acdf4492eeaf5d0816b6e2a634fbbda6