mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 13:22:11 +03:00
Integrates two community contributions into release/v3.8.8 with security hardening and conflict resolution. - **Plugins framework** (#2913 — thanks @oyi77): hooks + registry unification, plugin SDK (`definePlugin`), worker-thread sandbox, per-plugin hook rate limiting, SHA-256 integrity verification, semver-gated upgrade, and execution analytics. Plugin routes are loopback-only (`isLocalOnlyPath`); `child_process` exec is opt-in via `OMNIROUTE_PLUGINS_ALLOW_EXEC` (default off). - **API key option: disable non-published models** (#3017 — thanks @androw): a per-key flag restricting the key to discovered public models (combos / `auto/*` / `qtSd/*` routing still allowed). Hardening applied during integration: migration renumber (089/090/091), `/api/plugins` LOCAL_ONLY route-guard classification (closes the plugin-RCE vector), atomic install/upgrade with path containment, `O_EXCL` tmp-file creation (TOCTOU), rate-limit-map eviction, `validatePluginConfig` on configure, `buildErrorBody` on all plugin error paths. 246/246 tests; typecheck / cycles / docs-sync clean. Co-authored-by: oyi77 <14921983+oyi77@users.noreply.github.com> Co-authored-by: Nicolas Lorin <androw95220@gmail.com>
139 lines
4.3 KiB
TypeScript
139 lines
4.3 KiB
TypeScript
/**
|
|
* Plugin doctor — diagnostic tool for plugin health checks.
|
|
*
|
|
* Runs 5 checks: directory exists, manifest valid, entry point exists,
|
|
* can spawn, DB status matches filesystem.
|
|
*
|
|
* @module plugins/doctor
|
|
*/
|
|
|
|
import { stat } from "fs/promises";
|
|
import { join } from "path";
|
|
import { safeValidateManifest } from "./manifest";
|
|
import { getPluginByName } from "../db/plugins";
|
|
import { readFile } from "fs/promises";
|
|
import { logger } from "../../../open-sse/utils/logger.ts";
|
|
|
|
const log = logger("PLUGIN_DOCTOR");
|
|
|
|
export interface DoctorCheck {
|
|
name: string;
|
|
status: "pass" | "fail" | "warn";
|
|
message?: string;
|
|
}
|
|
|
|
export interface DoctorResult {
|
|
pluginName: string;
|
|
checks: DoctorCheck[];
|
|
overall: "healthy" | "degraded" | "unhealthy";
|
|
}
|
|
|
|
/**
|
|
* Run diagnostic checks on a plugin.
|
|
*/
|
|
export async function runPluginDoctor(pluginDir: string, pluginName: string): Promise<DoctorResult> {
|
|
const checks: DoctorCheck[] = [];
|
|
|
|
// Check 1: directory_exists
|
|
try {
|
|
const dirStat = await stat(pluginDir);
|
|
checks.push({
|
|
name: "directory_exists",
|
|
status: dirStat.isDirectory() ? "pass" : "fail",
|
|
message: dirStat.isDirectory() ? undefined : "Path is not a directory",
|
|
});
|
|
} catch {
|
|
checks.push({ name: "directory_exists", status: "fail", message: "Directory not found" });
|
|
}
|
|
|
|
// Check 2: manifest_valid
|
|
const manifestPath = join(pluginDir, "plugin.json");
|
|
try {
|
|
const raw = await readFile(manifestPath, "utf-8");
|
|
const parsed = JSON.parse(raw);
|
|
const result = safeValidateManifest(parsed);
|
|
checks.push({
|
|
name: "manifest_valid",
|
|
status: result.success ? "pass" : "fail",
|
|
message: result.success ? undefined : (result as { success: false; errors: string[] }).errors.join("; "),
|
|
});
|
|
} catch (err: unknown) {
|
|
checks.push({
|
|
name: "manifest_valid",
|
|
status: "fail",
|
|
message: `Cannot read manifest: ${err instanceof Error ? err.message : String(err)}`,
|
|
});
|
|
}
|
|
|
|
// Check 3: entry_point_exists
|
|
try {
|
|
const raw = await readFile(manifestPath, "utf-8");
|
|
const parsed = JSON.parse(raw);
|
|
const result = safeValidateManifest(parsed);
|
|
if (result.success) {
|
|
const entryPoint = join(pluginDir, result.data.main);
|
|
try {
|
|
await stat(entryPoint);
|
|
checks.push({ name: "entry_point_exists", status: "pass" });
|
|
} catch {
|
|
checks.push({ name: "entry_point_exists", status: "fail", message: `Entry point not found: ${result.data.main}` });
|
|
}
|
|
} else {
|
|
checks.push({ name: "entry_point_exists", status: "warn", message: "Skipped — manifest invalid" });
|
|
}
|
|
} catch {
|
|
checks.push({ name: "entry_point_exists", status: "warn", message: "Skipped — manifest unreadable" });
|
|
}
|
|
|
|
// Check 4: can_spawn (simplified — check entry point is .js/.mjs)
|
|
try {
|
|
const raw = await readFile(manifestPath, "utf-8");
|
|
const parsed = JSON.parse(raw);
|
|
const result = safeValidateManifest(parsed);
|
|
if (result.success) {
|
|
const main = result.data.main;
|
|
const ext = main.split(".").pop();
|
|
checks.push({
|
|
name: "can_spawn",
|
|
status: ext === "js" || ext === "mjs" ? "pass" : "warn",
|
|
message: ext === "js" || ext === "mjs" ? undefined : `Unexpected extension: .${ext}`,
|
|
});
|
|
} else {
|
|
checks.push({ name: "can_spawn", status: "warn", message: "Skipped — manifest invalid" });
|
|
}
|
|
} catch {
|
|
checks.push({ name: "can_spawn", status: "warn", message: "Skipped — manifest unreadable" });
|
|
}
|
|
|
|
// Check 5: db_status_correct
|
|
const dbRow = getPluginByName(pluginName);
|
|
if (dbRow) {
|
|
checks.push({
|
|
name: "db_status_correct",
|
|
status: "pass",
|
|
message: `Status: ${dbRow.status}`,
|
|
});
|
|
} else {
|
|
checks.push({
|
|
name: "db_status_correct",
|
|
status: "warn",
|
|
message: "Plugin not in database",
|
|
});
|
|
}
|
|
|
|
// Overall
|
|
const failCount = checks.filter((c) => c.status === "fail").length;
|
|
const warnCount = checks.filter((c) => c.status === "warn").length;
|
|
let overall: DoctorResult["overall"];
|
|
if (failCount === 0 && warnCount === 0) {
|
|
overall = "healthy";
|
|
} else if (failCount === 0) {
|
|
overall = "degraded";
|
|
} else {
|
|
overall = "unhealthy";
|
|
}
|
|
|
|
log.info("doctor.result", { pluginName, overall, failCount, warnCount });
|
|
return { pluginName, checks, overall };
|
|
}
|