mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 11:30:43 +00:00
95 lines
2.9 KiB
TypeScript
95 lines
2.9 KiB
TypeScript
import path from "node:path";
|
|
import {
|
|
resolveSafeInstallDir,
|
|
safeDirName,
|
|
safePathSegmentHashed,
|
|
unscopedPackageName,
|
|
} from "../infra/install-safe-path.js";
|
|
import { CONFIG_DIR, resolveUserPath } from "../utils.js";
|
|
|
|
export function safePluginInstallFileName(input: string): string {
|
|
return safeDirName(input);
|
|
}
|
|
|
|
export function encodePluginInstallDirName(pluginId: string): string {
|
|
const trimmed = pluginId.trim();
|
|
if (!trimmed.includes("/")) {
|
|
return safeDirName(trimmed);
|
|
}
|
|
// Scoped plugin ids need a reserved on-disk namespace so they cannot collide
|
|
// with valid unscoped ids that happen to match the hashed slug.
|
|
return `@${safePathSegmentHashed(trimmed)}`;
|
|
}
|
|
|
|
export function validatePluginId(pluginId: string): string | null {
|
|
const trimmed = pluginId.trim();
|
|
if (!trimmed) {
|
|
return "invalid plugin name: missing";
|
|
}
|
|
if (trimmed.includes("\\")) {
|
|
return "invalid plugin name: path separators not allowed";
|
|
}
|
|
const segments = trimmed.split("/");
|
|
if (segments.some((segment) => !segment)) {
|
|
return "invalid plugin name: malformed scope";
|
|
}
|
|
if (segments.some((segment) => segment === "." || segment === "..")) {
|
|
return "invalid plugin name: reserved path segment";
|
|
}
|
|
if (segments.length === 1) {
|
|
if (trimmed.startsWith("@")) {
|
|
return "invalid plugin name: scoped ids must use @scope/name format";
|
|
}
|
|
return null;
|
|
}
|
|
if (segments.length !== 2) {
|
|
return "invalid plugin name: path separators not allowed";
|
|
}
|
|
if (!segments[0]?.startsWith("@") || segments[0].length < 2) {
|
|
return "invalid plugin name: scoped ids must use @scope/name format";
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function matchesExpectedPluginId(params: {
|
|
expectedPluginId?: string;
|
|
pluginId: string;
|
|
manifestPluginId?: string;
|
|
npmPluginId: string;
|
|
}): boolean {
|
|
if (!params.expectedPluginId) {
|
|
return true;
|
|
}
|
|
if (params.expectedPluginId === params.pluginId) {
|
|
return true;
|
|
}
|
|
// Backward compatibility: older install records keyed scoped npm packages by
|
|
// their unscoped package name. Preserve update-in-place for those records
|
|
// unless the package declares an explicit manifest id override.
|
|
return (
|
|
!params.manifestPluginId &&
|
|
params.pluginId === params.npmPluginId &&
|
|
params.expectedPluginId === unscopedPackageName(params.npmPluginId)
|
|
);
|
|
}
|
|
|
|
export function resolvePluginInstallDir(pluginId: string, extensionsDir?: string): string {
|
|
const extensionsBase = extensionsDir
|
|
? resolveUserPath(extensionsDir)
|
|
: path.join(CONFIG_DIR, "extensions");
|
|
const pluginIdError = validatePluginId(pluginId);
|
|
if (pluginIdError) {
|
|
throw new Error(pluginIdError);
|
|
}
|
|
const targetDirResult = resolveSafeInstallDir({
|
|
baseDir: extensionsBase,
|
|
id: pluginId,
|
|
invalidNameMessage: "invalid plugin name: path traversal detected",
|
|
nameEncoder: encodePluginInstallDirName,
|
|
});
|
|
if (!targetDirResult.ok) {
|
|
throw new Error(targetDirResult.error);
|
|
}
|
|
return targetDirResult.path;
|
|
}
|