공개된 소스
소스 ZIP 다운로드
CH-J Server Manager
특정 애플리케이션 릴리스의 디렉터리와 파일을 살펴보세요.
CH-J / Server Manager
377 개 파일
core-20261019T111616Z-160a00bfcddc
추가됨: · 빌드됨:
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.5.0";9
const ALLOWED_PERMISSIONS = new Set([10
"session.read",11
"system.metrics.read",12
"system.memory.manage",13
"keys.generate",14
"files.read",15
"files.write",16
"files.transfer",17
"users.read",18
"users.manage",19
"nginx.read",20
"nginx.manage",21
"certbot.manage",22
"mail.read",23
"mail.manage",24
"logs.read",25
"remote.exec",26
"local.hash"27
]);29
function validateManifest(input) {30
if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("Plugin manifest must be an object.");31
const id = String(input.id || "").trim();32
if (!PLUGIN_ID_PATTERN.test(id)) throw new Error("Invalid plugin ID.");33
const version = String(input.version || "").trim();34
parseVersion(version);35
const entry = String(input.entry || "").replace(/\\/g, "/");36
if (!entry || entry.startsWith("/") || entry.split("/").includes("..")) throw new Error("Invalid plugin entry path.");37
const permissions = Array.isArray(input.permissions) ? [...new Set(input.permissions.map(String))] : [];38
for (const permission of permissions) {39
if (!ALLOWED_PERMISSIONS.has(permission)) throw new Error(`Unsupported plugin permission: ${permission}`);40
}41
const schemaVersion = Number(input.schemaVersion || 1);42
if (schemaVersion !== 1) throw new Error(`Unsupported plugin manifest schema: ${schemaVersion}`);43
const minAppVersion = String(input.minAppVersion || "0.0.1");44
parseVersion(minAppVersion);45
const pluginApi = String(input.pluginApi || "^1.0.0");46
if (!/^\^1\.\d+\.\d+$/.test(pluginApi)) throw new Error(`Unsupported plugin API: ${pluginApi}`);47
return {48
schemaVersion,49
id,50
name: String(input.name || id).trim().slice(0, 100),51
description: String(input.description || "").trim().slice(0, 500),52
version,53
publisher: String(input.publisher || "Unknown").trim().slice(0, 100),54
entry,55
pluginApi,56
minAppVersion,57
permissions58
};59
}61
function supportsPluginApi(range, supportedVersion = PLUGIN_API_VERSION) {62
const required = String(range || "").match(/^\^1\.(\d+)\.(\d+)$/);63
const supported = String(supportedVersion || "").match(/^1\.(\d+)\.(\d+)$/);64
return Boolean(required && supported && (Number(required[1]) < Number(supported[1]) || (Number(required[1]) === Number(supported[1]) && Number(required[2]) <= Number(supported[2]))));65
}67
class PluginRegistry {68
constructor(rootDir, logger, options = {}) {69
this.rootDir = path.join(rootDir, "plugins");70
this.bundledRoot = options.bundledRoot || null;71
this.logger = logger;72
this.locations = new Map();73
}75
listInstalled() {76
fs.mkdirSync(this.rootDir, { recursive: true });77
this.locations.clear();78
const plugins = [];79
const roots = [{ root: this.rootDir, bundled: false }];80
if (this.bundledRoot && fs.statSync(this.bundledRoot, { throwIfNoEntry: false })?.isDirectory()) roots.push({ root: this.bundledRoot, bundled: true });81
const candidatesById = new Map();82
for (const source of roots) for (const idEntry of fs.readdirSync(source.root, { withFileTypes: true })) {83
if (!idEntry.isDirectory() || !PLUGIN_ID_PATTERN.test(idEntry.name)) continue;84
const idRoot = path.join(source.root, idEntry.name);85
const candidates = [];86
for (const versionEntry of fs.readdirSync(idRoot, { withFileTypes: true })) {87
if (!versionEntry.isDirectory() || versionEntry.name.startsWith(".")) continue;88
const manifestPath = path.join(idRoot, versionEntry.name, "manifest.json");89
try {90
const manifest = validateManifest(JSON.parse(fs.readFileSync(manifestPath, "utf8")));91
if (manifest.id !== idEntry.name || manifest.version !== versionEntry.name) {92
throw new Error("Plugin directory does not match its manifest.");93
}94
const entryPath = path.join(idRoot, versionEntry.name, manifest.entry);95
if (!fs.statSync(entryPath).isFile()) throw new Error("Plugin entry file is missing.");96
let installation = {};97
try { installation = JSON.parse(fs.readFileSync(path.join(idRoot, versionEntry.name, ".installation.json"), "utf8")); } catch {}98
candidates.push({99
...manifest,100
bundled: source.bundled,101
installedReleaseId: typeof installation.releaseId === "string" ? installation.releaseId : null,102
installedSha512: /^[a-f0-9]{128}$/i.test(String(installation.sha512 || "")) ? String(installation.sha512).toLowerCase() : null,103
installedAt: typeof installation.installedAt === "string" ? installation.installedAt : null,104
installedPublishedAt: typeof installation.publishedAt === "string" ? installation.publishedAt : null,105
_root: path.join(idRoot, versionEntry.name)106
});107
} catch (error) {108
this.logger?.warn("Ignoring invalid installed plugin.", {109
pluginId: idEntry.name,110
version: versionEntry.name,111
message: error?.message || String(error)112
});113
}114
}115
if (!candidatesById.has(idEntry.name)) candidatesById.set(idEntry.name, []);116
candidatesById.get(idEntry.name).push(...candidates);117
}118
for (const candidates of candidatesById.values()) {119
candidates.sort((left, right) => compareVersions(right.version, left.version) || Number(right.bundled) - Number(left.bundled));120
if (!candidates[0]) continue;121
const selected = candidates[0];122
this.locations.set(`${selected.id}@${selected.version}`, selected._root);123
const { _root, ...publicManifest } = selected;124
plugins.push(publicManifest);125
}126
return plugins.sort((left, right) => left.name.localeCompare(right.name));127
}129
getInstalled(pluginId) {130
const id = String(pluginId || "");131
if (!PLUGIN_ID_PATTERN.test(id)) throw new Error("Invalid plugin ID.");132
return this.listInstalled().find((plugin) => plugin.id === id) || null;133
}135
resolveEntry(pluginId) {136
const manifest = this.getInstalled(pluginId);137
if (!manifest) throw new Error("Plugin is not installed.");138
const root = this.locations.get(`${manifest.id}@${manifest.version}`) || path.join(this.rootDir, manifest.id, manifest.version);139
return { manifest, root, entryPath: path.join(root, ...manifest.entry.split("/")) };140
}142
removeInstalled(pluginId) {143
const id = String(pluginId || "");144
if (!PLUGIN_ID_PATTERN.test(id)) throw new Error("Invalid plugin ID.");145
const directory = path.join(this.rootDir, id);146
if (!fs.existsSync(directory)) return { removed: false, pluginId: id };147
fs.rmSync(directory, { recursive: true, force: true });148
this.logger?.info("Plugin removed.", { pluginId: id });149
return { removed: true, pluginId: id };150
}151
}153
module.exports = {154
ALLOWED_PERMISSIONS,155
PLUGIN_API_VERSION,156
PLUGIN_ID_PATTERN,157
PluginRegistry,158
supportsPluginApi,159
validateManifest160
};SHA-256
483eff27a6b0f0d74f8d1db56d2691343b4a32e8d084011eb114e45b2df1e5a5CH-J Proprietary Software License 1.14 · 소스는 CH-J Proprietary Software License 1.14에 따라 제공됩니다. 소스를 공개한다고 해서 라이선스 조항이 바뀌거나 추가 권리가 부여되지는 않습니다.
압축 파일 SHA-256
a9605c0870a223b6a8542bb0705edd51d8b04f3069582653417a8397fabcc09a