import { createHash } from "node:crypto"; import fs from "node:fs/promises"; import JSZip from "jszip"; import {
DEFAULT_MAX_ARCHIVE_BYTES_ZIP,
DEFAULT_MAX_ENTRIES,
DEFAULT_MAX_EXTRACTED_BYTES,
DEFAULT_MAX_ENTRY_BYTES,
} from "../infra/archive.js"; import {
ClawHubRequestError,
downloadClawHubPackageArchive,
fetchClawHubPackageDetail,
fetchClawHubPackageVersion,
normalizeClawHubSha256Integrity,
normalizeClawHubSha256Hex,
parseClawHubPluginSpec,
resolveLatestVersionFromPackage,
satisfiesGatewayMinimum,
satisfiesPluginApiRange,
type ClawHubPackageChannel,
type ClawHubPackageCompatibility,
type ClawHubPackageDetail,
type ClawHubPackageFamily,
type ClawHubPackageVersion,
} from "../infra/clawhub.js"; import { formatErrorMessage } from "../infra/errors.js"; import { normalizeOptionalString } from "../shared/string-coerce.js"; import { resolveCompatibilityHostVersion } from "../version.js"; import type { InstallSafetyOverrides } from "./install-security-scan.js"; import { installPluginFromArchive, type InstallPluginResult } from "./install.js";
type JSZipObjectWithSize = JSZip.JSZipObject & { // Internal JSZip field from loadAsync() metadata. Use it only as a best-effort // size hint; the streaming byte checks below are the authoritative guard.
_data?: {
uncompressedSize?: number;
};
};
function isClawHubInstallFailure(value: unknown): value is ClawHubInstallFailure { returnBoolean(
value && typeof value === "object" && "ok" in value &&
(value as { ok?: unknown }).ok === false && "error" in value,
);
}
function mapClawHubRequestError(
error: unknown,
context: { stage: "package" | "version"; name: string; version?: string },
): ClawHubInstallFailure { if (error instanceof ClawHubRequestError && error.status === 404) { if (context.stage === "package") { return buildClawHubInstallFailure( "Package not found on ClawHub.",
CLAWHUB_INSTALL_ERROR_CODE.PACKAGE_NOT_FOUND,
);
} return buildClawHubInstallFailure(
`Version not found on ClawHub: ${context.name}@${context.version ?? "unknown"}.`,
CLAWHUB_INSTALL_ERROR_CODE.VERSION_NOT_FOUND,
);
} return buildClawHubInstallFailure(formatErrorMessage(error));
}
function normalizeClawHubRelativePath(value: unknown): string | null { if (typeof value !== "string" || value.length === 0) { returnnull;
} if (value.trim() !== value || value.includes("\\")) { returnnull;
} if (value.startsWith("/")) { returnnull;
} const segments = value.split("/"); if (segments.some((segment) => segment.length === 0 || segment === "." || segment === "..")) { returnnull;
} return value;
}
function describeInvalidClawHubRelativePath(value: unknown): string { if (typeof value !== "string") { return `non-string value of type ${typeof value}`;
} if (value.length === 0) { return"empty string";
} if (value.trim() !== value) { return `path "${value}" has leading or trailing whitespace`;
} if (value.includes("\\")) { return `path "${value}" contains backslashes`;
} if (value.startsWith("/")) { return `path "${value}" is absolute`;
} const segments = value.split("/"); if (segments.some((segment) => segment.length === 0)) { return `path "${value}" contains an empty segment`;
} if (segments.some((segment) => segment === "." || segment === "..")) { return `path "${value}" contains dot segments`;
} return `path "${value}" failed validation for an unknown reason`;
}
function describeInvalidClawHubSha256(value: unknown): string { if (typeof value !== "string") { return `non-string value of type ${typeof value}`;
} if (value.length === 0) { return"empty string";
} if (value.trim().length === 0) { return"whitespace-only string";
} return `value "${value}" is not a 64-character hexadecimal SHA-256 digest`;
}
function resolveClawHubArchiveVerification(
versionDetail: ClawHubPackageVersion,
packageName: string,
version: string,
): ClawHubArchiveVerificationResolution { const sha256hashValue = versionDetail.version?.sha256hash; const sha256hash = readTrimmedString(sha256hashValue); const integrity = sha256hash ? normalizeClawHubSha256Integrity(sha256hash) : null; if (integrity) { return {
ok: true,
verification: {
kind: "archive-integrity",
integrity,
},
};
} if (sha256hashValue !== undefined && sha256hashValue !== null) { const detail = typeof sha256hashValue === "string" && sha256hashValue.trim().length === 0
? "empty string"
: typeof sha256hashValue === "string"
? `unrecognized value "${sha256hashValue.trim()}"`
: `non-string value of type ${typeof sha256hashValue}`; return buildClawHubInstallFailure(
`ClawHub version metadata for"${packageName}@${version}" has an invalid sha256hash (${detail}).`,
CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY,
);
} const files = versionDetail.version?.files; if (!Array.isArray(files) || files.length === 0) { return {
ok: true,
verification: null,
};
} const normalizedFiles: ClawHubFileVerificationEntry[] = []; const seenPaths = new Set<string>(); for (const [index, file] of files.entries()) { if (!file || typeof file !== "object") { return buildClawHubInstallFailure(
`ClawHub version metadata for"${packageName}@${version}" has an invalid files[${index}] entry (expected an object, got ${file === null ? "null" : typeof file}).`,
CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY,
);
} const fileRecord = file as ClawHubFileEntryLike; const filePath = normalizeClawHubRelativePath(fileRecord.path); const sha256Value = readTrimmedString(fileRecord.sha256); const sha256 = sha256Value ? normalizeClawHubSha256Hex(sha256Value) : null; if (!filePath) { return buildClawHubInstallFailure(
`ClawHub version metadata for"${packageName}@${version}" has an invalid files[${index}].path (${describeInvalidClawHubRelativePath(fileRecord.path)}).`,
CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY,
);
} if (filePath === CLAWHUB_GENERATED_ARCHIVE_METADATA_FILE) { return buildClawHubInstallFailure(
`ClawHub version metadata for"${packageName}@${version}" must not include generated file "${filePath}" in files[].`,
CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY,
);
} if (!sha256) { return buildClawHubInstallFailure(
`ClawHub version metadata for"${packageName}@${version}" has an invalid files[${index}].sha256 (${describeInvalidClawHubSha256(fileRecord.sha256)}).`,
CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY,
);
} if (seenPaths.has(filePath)) { return buildClawHubInstallFailure(
`ClawHub version metadata for"${packageName}@${version}" has duplicate files[] path "${filePath}".`,
CLAWHUB_INSTALL_ERROR_CODE.MISSING_ARCHIVE_INTEGRITY,
);
}
seenPaths.add(filePath);
normalizedFiles.push({ path: filePath, sha256 });
} return {
ok: true,
verification: {
kind: "file-list",
files: normalizedFiles,
},
};
}
function validateClawHubArchiveMetaJson(params: {
packageName: string;
version: string;
bytes: Buffer;
}): ClawHubInstallFailure | null {
let parsed: unknown; try {
parsed = JSON.parse(params.bytes.toString("utf8"));
} catch { return buildClawHubInstallFailure(
`ClawHub archive contents do not match files[] metadata for"${params.packageName}@${params.version}": _meta.json is not valid JSON.`,
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
} if (!parsed || typeof parsed !== "object") { return buildClawHubInstallFailure(
`ClawHub archive contents do not match files[] metadata for"${params.packageName}@${params.version}": _meta.json is not a JSON object.`,
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
} const record = parsed as { slug?: unknown; version?: unknown }; if (record.slug !== params.packageName) { return buildClawHubInstallFailure(
`ClawHub archive contents do not match files[] metadata for"${params.packageName}@${params.version}": _meta.json slug does not match the package name.`,
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
} if (record.version !== params.version) { return buildClawHubInstallFailure(
`ClawHub archive contents do not match files[] metadata for"${params.packageName}@${params.version}": _meta.json version does not match the package version.`,
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
} returnnull;
}
async function verifyClawHubArchiveFiles(params: {
archivePath: string;
packageName: string;
packageVersion: string;
files: ClawHubFileVerificationEntry[];
}): Promise<ClawHubArchiveFileVerificationResult> { try { const archiveStat = await fs.stat(params.archivePath); if (archiveStat.size > DEFAULT_MAX_ARCHIVE_BYTES_ZIP) { return buildClawHubInstallFailure( "ClawHub archive fallback verification rejected the downloaded archive because it exceeds the ZIP archive size limit.",
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
} const archiveBytes = await fs.readFile(params.archivePath); const zip = await JSZip.loadAsync(archiveBytes); const actualFiles = new Map<string, string>(); const validatedGeneratedPaths = new Set<string>();
let entryCount = 0;
let extractedBytes = 0; const addArchiveBytes = (bytes: number): boolean => {
extractedBytes += bytes; return extractedBytes <= DEFAULT_MAX_EXTRACTED_BYTES;
}; for (const entry of Object.values(zip.files)) {
entryCount += 1; if (entryCount > DEFAULT_MAX_ENTRIES) { return buildClawHubInstallFailure( "ClawHub archive fallback verification exceeded the archive entry limit.",
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
} if (entry.dir) { continue;
} const relativePath = normalizeClawHubRelativePath(entry.name); if (!relativePath) { return buildClawHubInstallFailure(
`ClawHub archive contents do not match files[] metadata for"${params.packageName}@${params.packageVersion}": invalid package file path "${entry.name}" (${describeInvalidClawHubRelativePath(entry.name)}).`,
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
} if (relativePath === CLAWHUB_GENERATED_ARCHIVE_METADATA_FILE) { const metaResult = await readClawHubArchiveEntryBuffer(entry, {
maxEntryBytes: DEFAULT_MAX_ENTRY_BYTES,
addArchiveBytes,
}); if (isClawHubInstallFailure(metaResult)) { return metaResult;
} const metaFailure = validateClawHubArchiveMetaJson({
packageName: params.packageName,
version: params.packageVersion,
bytes: metaResult,
}); if (metaFailure) { return metaFailure;
}
validatedGeneratedPaths.add(relativePath); continue;
} const sha256 = await hashClawHubArchiveEntry(entry, {
maxEntryBytes: DEFAULT_MAX_ENTRY_BYTES,
addArchiveBytes,
}); if (typeof sha256 !== "string") { return sha256;
}
actualFiles.set(relativePath, sha256);
} for (const file of params.files) { const actualSha256 = actualFiles.get(file.path); if (!actualSha256) { return buildClawHubInstallFailure(
`ClawHub archive contents do not match files[] metadata for"${params.packageName}@${params.packageVersion}": missing "${file.path}".`,
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
} if (actualSha256 !== file.sha256) { return buildClawHubInstallFailure(
`ClawHub archive contents do not match files[] metadata for"${params.packageName}@${params.packageVersion}": expected ${file.path} to hash to ${file.sha256}, got ${actualSha256}.`,
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
}
actualFiles.delete(file.path);
} const unexpectedFile = [...actualFiles.keys()].toSorted()[0]; if (unexpectedFile) { return buildClawHubInstallFailure(
`ClawHub archive contents do not match files[] metadata for"${params.packageName}@${params.packageVersion}": unexpected file "${unexpectedFile}".`,
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
} return {
ok: true,
validatedGeneratedPaths: [...validatedGeneratedPaths].toSorted(),
};
} catch { return buildClawHubInstallFailure( "ClawHub archive fallback verification failed while reading the downloaded archive.",
CLAWHUB_INSTALL_ERROR_CODE.ARCHIVE_INTEGRITY_MISMATCH,
);
}
}
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.