119 lines
4.4 KiB
JavaScript
119 lines
4.4 KiB
JavaScript
"use strict";
|
|
|
|
const path = require("node:path");
|
|
|
|
const CATALOG_PACKAGE = "agentic-awesome-skills";
|
|
const CATALOG_IDENTITY_FILE = ".aas-catalog-identity.json";
|
|
const RUNTIME_IDENTITY_FILE = ".aas-runtime-identity.json";
|
|
const DIGEST_VERSION = 1;
|
|
const SRI_LENGTHS = Object.freeze({ sha256: 32, sha384: 48, sha512: 64 });
|
|
|
|
function cacheError(code, message) {
|
|
const error = new Error(message);
|
|
error.code = code;
|
|
return error;
|
|
}
|
|
|
|
function parseNpmIntegrity(integrity) {
|
|
if (typeof integrity !== "string" || integrity.trim() !== integrity || /\s/.test(integrity)) {
|
|
throw cacheError("AAS_CACHE_INTEGRITY_INVALID", "npm integrity must be one canonical SRI token");
|
|
}
|
|
const match = /^(sha256|sha384|sha512)-([A-Za-z0-9+/]+={0,2})$/.exec(integrity);
|
|
if (!match) throw cacheError("AAS_CACHE_INTEGRITY_INVALID", "npm integrity is not a supported SRI token");
|
|
const [, algorithm, encoded] = match;
|
|
const bytes = Buffer.from(encoded, "base64");
|
|
if (bytes.length !== SRI_LENGTHS[algorithm]) {
|
|
throw cacheError("AAS_CACHE_INTEGRITY_INVALID", `npm ${algorithm} integrity has the wrong digest length`);
|
|
}
|
|
const canonical = bytes.toString("base64");
|
|
if (canonical !== encoded) {
|
|
throw cacheError("AAS_CACHE_INTEGRITY_INVALID", "npm integrity digest is not canonical base64");
|
|
}
|
|
return { algorithm, bytes, integrity };
|
|
}
|
|
|
|
function filesystemSafeIntegrityKey(integrity) {
|
|
const parsed = parseNpmIntegrity(integrity);
|
|
return `${parsed.algorithm}-${parsed.bytes.toString("base64url")}`;
|
|
}
|
|
|
|
function validatePackageVersion(version) {
|
|
if (typeof version !== "string" || version.length > 128 || !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.test(version)) {
|
|
throw cacheError("AAS_CACHE_VERSION_INVALID", "package version must be a canonical SemVer value");
|
|
}
|
|
return version;
|
|
}
|
|
|
|
function validateCatalogDigest(digest) {
|
|
if (typeof digest !== "string" || !/^sha256-[0-9a-f]{64}$/.test(digest)) {
|
|
throw cacheError("AAS_CACHE_CATALOG_DIGEST_INVALID", "catalog digest must be canonical sha256 hex");
|
|
}
|
|
return digest;
|
|
}
|
|
|
|
function validateCacheRoot(cacheRoot) {
|
|
if (typeof cacheRoot !== "string" || cacheRoot.length === 0 || cacheRoot.includes("\0")) {
|
|
throw cacheError("AAS_CACHE_ROOT_INVALID", "cache root must be a non-empty filesystem path");
|
|
}
|
|
return path.resolve(cacheRoot);
|
|
}
|
|
|
|
function runtimeCachePath({ cacheRoot, packageVersion, integrity }) {
|
|
return path.join(
|
|
validateCacheRoot(cacheRoot),
|
|
"runtimes",
|
|
validatePackageVersion(packageVersion),
|
|
filesystemSafeIntegrityKey(integrity),
|
|
);
|
|
}
|
|
|
|
function catalogCachePath({ cacheRoot, packageVersion, catalogDigest }) {
|
|
return path.join(
|
|
validateCacheRoot(cacheRoot),
|
|
"catalogs",
|
|
validatePackageVersion(packageVersion),
|
|
validateCatalogDigest(catalogDigest),
|
|
);
|
|
}
|
|
|
|
function validateCatalogIdentity(identity, expected = {}) {
|
|
if (!identity || typeof identity !== "object" || Array.isArray(identity)) {
|
|
throw cacheError("AAS_CACHE_IDENTITY_INVALID", "catalog identity must be an object");
|
|
}
|
|
const allowedKeys = new Set(["schemaVersion", "package", "version", "integrity", "catalogDigest"]);
|
|
for (const key of Object.keys(identity)) {
|
|
if (!allowedKeys.has(key)) throw cacheError("AAS_CACHE_IDENTITY_INVALID", `unknown catalog identity field: ${key}`);
|
|
}
|
|
if (identity.schemaVersion !== 1) throw cacheError("AAS_CACHE_IDENTITY_INVALID", "catalog identity schemaVersion must be 1");
|
|
if (identity.package !== CATALOG_PACKAGE) throw cacheError("AAS_CACHE_IDENTITY_INVALID", `catalog package must be ${CATALOG_PACKAGE}`);
|
|
const normalized = {
|
|
schemaVersion: 1,
|
|
package: CATALOG_PACKAGE,
|
|
version: validatePackageVersion(identity.version),
|
|
integrity: parseNpmIntegrity(identity.integrity).integrity,
|
|
catalogDigest: validateCatalogDigest(identity.catalogDigest),
|
|
};
|
|
for (const key of ["package", "version", "integrity", "catalogDigest"]) {
|
|
if (expected[key] !== undefined && normalized[key] !== expected[key]) {
|
|
throw cacheError("AAS_CACHE_IDENTITY_MISMATCH", `catalog identity ${key} does not match the expected value`);
|
|
}
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
module.exports = {
|
|
CATALOG_IDENTITY_FILE,
|
|
CATALOG_PACKAGE,
|
|
RUNTIME_IDENTITY_FILE,
|
|
DIGEST_VERSION,
|
|
cacheError,
|
|
catalogCachePath,
|
|
filesystemSafeIntegrityKey,
|
|
parseNpmIntegrity,
|
|
runtimeCachePath,
|
|
validateCacheRoot,
|
|
validateCatalogDigest,
|
|
validateCatalogIdentity,
|
|
validatePackageVersion,
|
|
};
|