📦 deps(thirdparty): update snapshots

This commit is contained in:
ci[bot]
2026-07-18 00:02:59 +00:00
parent 82f7c6e56a
commit 47ce7f78dc
1446 changed files with 141041 additions and 6442 deletions
@@ -0,0 +1,181 @@
"use strict";
const zlib = require("node:zlib");
const { cacheError } = require("./identity");
const { collisionKey, validateRelativeAssetPath } = require("./scan");
const DEFAULT_ARCHIVE_LIMITS = Object.freeze({
maxEntries: 10000,
maxSingleFileBytes: 32 * 1024 * 1024,
maxExpandedTotalBytes: 160 * 1024 * 1024,
maxCompressionRatio: 128,
});
function parseOctal(buffer, field) {
const text = buffer.toString("ascii").replace(/\0.*$/, "").trim();
if (!/^[0-7]+$/.test(text)) throw cacheError("AAS_ARCHIVE_HEADER_INVALID", `invalid ${field}`);
const value = Number.parseInt(text, 8);
if (!Number.isSafeInteger(value) || value < 0) throw cacheError("AAS_ARCHIVE_HEADER_INVALID", `unsafe ${field}`);
return value;
}
function decodeField(buffer) {
const end = buffer.indexOf(0);
const bytes = end === -1 ? buffer : buffer.subarray(0, end);
try {
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
} catch {
throw cacheError("AAS_ARCHIVE_HEADER_INVALID", "archive path is not valid UTF-8");
}
}
function safeArchivePath(value) {
if (typeof value !== "string" || value.includes("\\") || value.startsWith("/")
|| /^[A-Za-z]:/.test(value) || value.startsWith("//")) {
throw cacheError("AAS_ARCHIVE_PATH_INVALID", "archive path is absolute or platform-ambiguous");
}
const withoutDirectorySlash = value.replace(/\/$/, "");
for (const segment of withoutDirectorySlash.split("/")) {
const deviceBase = segment.split(".")[0].toUpperCase();
if (!segment || /[\u0000-\u001f<>:"|?*]/u.test(segment) || /[ .]$/.test(segment)
|| /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9]|CONIN\$|CONOUT\$)$/.test(deviceBase)) {
throw cacheError("AAS_ARCHIVE_PATH_INVALID", "archive path is unsafe on a supported filesystem");
}
}
try {
return validateRelativeAssetPath(withoutDirectorySlash);
} catch {
throw cacheError("AAS_ARCHIVE_PATH_INVALID", "archive path contains traversal or invalid segments");
}
}
function parsePax(bytes) {
const values = {};
let offset = 0;
while (offset < bytes.length) {
const space = bytes.indexOf(0x20, offset);
if (space < 0) throw cacheError("AAS_ARCHIVE_PAX_INVALID", "PAX record length is missing");
const lengthText = bytes.subarray(offset, space).toString("ascii");
if (!/^[1-9][0-9]*$/.test(lengthText)) throw cacheError("AAS_ARCHIVE_PAX_INVALID", "PAX record length is invalid");
const length = Number(lengthText);
if (!Number.isSafeInteger(length) || length < 5 || offset + length > bytes.length || bytes[offset + length - 1] !== 0x0a) {
throw cacheError("AAS_ARCHIVE_PAX_INVALID", "PAX record is truncated");
}
const record = bytes.subarray(space + 1, offset + length - 1).toString("utf8");
const equals = record.indexOf("=");
if (equals < 1) throw cacheError("AAS_ARCHIVE_PAX_INVALID", "PAX record has no key");
const key = record.slice(0, equals);
const value = record.slice(equals + 1);
if (Object.hasOwn(values, key)) throw cacheError("AAS_ARCHIVE_PAX_INVALID", "duplicate PAX key");
values[key] = value;
offset += length;
}
if (values.linkpath !== undefined) throw cacheError("AAS_ARCHIVE_LINK_FORBIDDEN", "PAX linkpath is forbidden");
return values;
}
function assertArchiveMode(mode) {
if ((mode & 0o7000) !== 0 || (mode & 0o022) !== 0) {
throw cacheError("AAS_ARCHIVE_MODE_UNSAFE", "archive entry has anomalous permissions");
}
}
function parseTar(tarBytes, options = {}) {
const limits = { ...DEFAULT_ARCHIVE_LIMITS, ...(options.limits || {}) };
const selected = options.selectPaths ? new Set(options.selectPaths.map(safeArchivePath)) : null;
const entries = [];
const seen = new Map();
const collisionKeys = new Map();
let offset = 0;
let fileCount = 0;
let expandedBytes = 0;
let pendingPath = null;
let zeroBlocks = 0;
while (offset + 512 <= tarBytes.length) {
const header = tarBytes.subarray(offset, offset + 512);
offset += 512;
if (header.every((byte) => byte === 0)) {
zeroBlocks += 1;
if (zeroBlocks === 2) break;
continue;
}
zeroBlocks = 0;
const expectedChecksum = parseOctal(header.subarray(148, 156), "checksum");
let checksum = 0;
for (let index = 0; index < 512; index += 1) checksum += index >= 148 && index < 156 ? 0x20 : header[index];
if (checksum !== expectedChecksum) throw cacheError("AAS_ARCHIVE_CHECKSUM_INVALID", "archive header checksum mismatch");
const size = parseOctal(header.subarray(124, 136), "size");
const mode = parseOctal(header.subarray(100, 108), "mode");
assertArchiveMode(mode);
const type = String.fromCharCode(header[156] || 0);
const name = decodeField(header.subarray(0, 100));
const prefix = decodeField(header.subarray(345, 500));
let archivePath = pendingPath || (prefix ? `${prefix}/${name}` : name);
pendingPath = null;
const paddedSize = Math.ceil(size / 512) * 512;
if (offset + paddedSize > tarBytes.length) throw cacheError("AAS_ARCHIVE_TRUNCATED", "archive entry is truncated");
const body = tarBytes.subarray(offset, offset + size);
offset += paddedSize;
if (type === "x" || type === "g") {
if (size > 64 * 1024) throw cacheError("AAS_ARCHIVE_PAX_INVALID", "PAX metadata is too large");
const pax = parsePax(body);
if (pax.path !== undefined) pendingPath = pax.path;
continue;
}
if (type === "L") {
if (size > 4096) throw cacheError("AAS_ARCHIVE_PATH_INVALID", "GNU long path is too large");
pendingPath = decodeField(body).replace(/\0$/, "");
continue;
}
archivePath = safeArchivePath(archivePath);
const directory = type === "5";
const regular = type === "0" || type === "\0";
if (!directory && !regular) throw cacheError("AAS_ARCHIVE_SPECIAL_FILE_FORBIDDEN", "archive links and special entries are forbidden");
if (directory && size !== 0) throw cacheError("AAS_ARCHIVE_HEADER_INVALID", "directory entry has data");
fileCount += regular ? 1 : 0;
expandedBytes += regular ? size : 0;
if (fileCount > limits.maxEntries) throw cacheError("AAS_ARCHIVE_ENTRY_LIMIT", "archive exceeds the file-count limit");
if (regular && size > limits.maxSingleFileBytes) throw cacheError("AAS_ARCHIVE_FILE_LIMIT", "archive file exceeds the size limit");
if (expandedBytes > limits.maxExpandedTotalBytes) throw cacheError("AAS_ARCHIVE_TOTAL_LIMIT", "archive exceeds the expanded-byte limit");
const kind = directory ? "directory" : "file";
if (seen.has(archivePath)) throw cacheError("AAS_ARCHIVE_DUPLICATE_PATH", "archive contains a duplicate path");
const key = collisionKey(archivePath);
if (collisionKeys.has(key)) throw cacheError("AAS_ARCHIVE_PATH_COLLISION", "archive paths collide by case or Unicode normalization");
for (const [existingPath, existingKind] of seen) {
if ((archivePath.startsWith(`${existingPath}/`) && existingKind === "file")
|| (existingPath.startsWith(`${archivePath}/`) && kind === "file")) {
throw cacheError("AAS_ARCHIVE_FILE_DIRECTORY_COLLISION", "archive file and directory paths collide");
}
}
seen.set(archivePath, kind);
collisionKeys.set(key, archivePath);
if (regular && (!selected || selected.has(archivePath))) entries.push({ path: archivePath, mode, bytes: Buffer.from(body) });
}
if (zeroBlocks < 2) throw cacheError("AAS_ARCHIVE_TRUNCATED", "archive end markers are missing");
if (pendingPath !== null) throw cacheError("AAS_ARCHIVE_TRUNCATED", "archive ended after path metadata");
if (selected) {
const found = new Set(entries.map((entry) => entry.path));
const missing = [...selected].filter((entry) => !found.has(entry));
if (missing.length) throw cacheError("AAS_ARCHIVE_ASSET_MISSING", `archive asset is missing: ${missing[0]}`);
}
return { entries, fileCount, expandedBytes };
}
function parsePackageArchive(archiveBytes, options = {}) {
const limits = { ...DEFAULT_ARCHIVE_LIMITS, ...(options.limits || {}) };
const gzip = archiveBytes[0] === 0x1f && archiveBytes[1] === 0x8b;
let tarBytes = archiveBytes;
if (gzip) {
try {
tarBytes = zlib.gunzipSync(archiveBytes, { maxOutputLength: limits.maxExpandedTotalBytes + 1024 * 1024 });
} catch (cause) {
throw cacheError("AAS_ARCHIVE_DECOMPRESSION_FAILED", "archive decompression failed", cause);
}
if (tarBytes.length > archiveBytes.length * limits.maxCompressionRatio) {
throw cacheError("AAS_ARCHIVE_COMPRESSION_RATIO", "archive exceeds the compression-ratio limit");
}
}
return parseTar(tarBytes, { ...options, limits });
}
module.exports = { DEFAULT_ARCHIVE_LIMITS, parsePackageArchive, parsePax, parseTar, safeArchivePath };
@@ -0,0 +1,118 @@
"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,
};
@@ -0,0 +1,12 @@
"use strict";
module.exports = {
...require("./archive"),
...require("./identity"),
...require("./promote"),
...require("./resolver"),
...require("./runtime"),
...require("./scan"),
...require("./status"),
...require("./update"),
};
@@ -0,0 +1,139 @@
"use strict";
const crypto = require("node:crypto");
const fsp = require("node:fs/promises");
const path = require("node:path");
const { fsyncDirectoryAsync } = require("../durability");
const { canonicalJson } = require("../canonical-json");
const {
CATALOG_IDENTITY_FILE,
DIGEST_VERSION,
cacheError,
catalogCachePath,
validateCacheRoot,
validateCatalogIdentity,
} = require("./identity");
const { scanDataDirectory } = require("./scan");
const { catalogStatus } = require("./status");
async function fsyncDirectory(directoryPath) {
await fsyncDirectoryAsync(directoryPath);
}
async function writeFileCrashSafe(filePath, bytes) {
const handle = await fsp.open(filePath, "wx", 0o600);
try {
await handle.writeFile(bytes);
await handle.sync();
} finally {
await handle.close();
}
}
async function ensureRealDirectory(directoryPath, created) {
try {
const stat = await fsp.lstat(directoryPath);
if (!stat.isDirectory() || stat.isSymbolicLink()) throw cacheError("AAS_CACHE_DIRECTORY_UNSAFE", `cache path is not a real directory: ${directoryPath}`);
} catch (error) {
if (error.code !== "ENOENT") throw error;
const parent = path.dirname(directoryPath);
if (parent !== directoryPath) await ensureRealDirectory(parent, created);
try {
await fsp.mkdir(directoryPath, { mode: 0o700 });
created.push(directoryPath);
} catch (mkdirError) {
if (mkdirError.code !== "EEXIST") throw mkdirError;
const stat = await fsp.lstat(directoryPath);
if (!stat.isDirectory() || stat.isSymbolicLink()) throw cacheError("AAS_CACHE_DIRECTORY_UNSAFE", `cache path is not a real directory: ${directoryPath}`);
}
}
}
async function removeCreatedEmptyDirectories(created) {
for (const directoryPath of [...created].reverse()) {
try {
await fsp.rmdir(directoryPath);
} catch (error) {
if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY") throw error;
}
}
}
async function promoteCatalogDirectory({ cacheRoot, sourceDir, allowlist, controlPaths = [], identity, limits }) {
// Validate every untrusted input byte before the cache filesystem is mutated.
const scan = await scanDataDirectory({ sourceDir, allowlist, ignoredPaths: controlPaths, limits });
const normalizedIdentity = validateCatalogIdentity(identity);
if (normalizedIdentity.catalogDigest !== scan.catalogDigest) {
throw cacheError("AAS_CACHE_CATALOG_DIGEST_MISMATCH", "catalog content does not match the approved catalog digest");
}
const existing = await catalogStatus({
cacheRoot,
packageVersion: normalizedIdentity.version,
catalogDigest: normalizedIdentity.catalogDigest,
integrity: normalizedIdentity.integrity,
});
if (existing.status === "verified") return { status: "alreadyPresent", identity: existing.identity, targetPath: existing.targetPath };
if (existing.status === "invalid") throw cacheError("AAS_CACHE_EXISTING_INVALID", "an invalid object already occupies the immutable catalog cache key");
const root = validateCacheRoot(cacheRoot);
const targetPath = catalogCachePath({
cacheRoot: root,
packageVersion: normalizedIdentity.version,
catalogDigest: normalizedIdentity.catalogDigest,
});
const versionDirectory = path.dirname(targetPath);
const created = [];
let stagePath;
let promoted = false;
try {
await ensureRealDirectory(versionDirectory, created);
stagePath = path.join(versionDirectory, `.stage-${process.pid}-${crypto.randomBytes(12).toString("hex")}`);
await fsp.mkdir(stagePath, { mode: 0o700 });
const madeDirectories = new Set([stagePath]);
for (const record of [...scan.records, ...scan.ignoredRecords]) {
const segments = record.path.split("/");
const outputPath = path.join(stagePath, ...segments);
let cursor = stagePath;
for (const segment of segments.slice(0, -1)) {
cursor = path.join(cursor, segment);
if (!madeDirectories.has(cursor)) {
await fsp.mkdir(cursor, { mode: 0o700 });
madeDirectories.add(cursor);
}
}
await writeFileCrashSafe(outputPath, record.bytes);
}
const storedIdentity = {
...normalizedIdentity,
digestVersion: DIGEST_VERSION,
assets: scan.publicRecords,
controls: scan.publicIgnoredRecords,
};
await writeFileCrashSafe(path.join(stagePath, CATALOG_IDENTITY_FILE), Buffer.from(`${canonicalJson(storedIdentity)}\n`));
for (const directoryPath of [...madeDirectories].sort((left, right) => right.length - left.length)) await fsyncDirectory(directoryPath);
try {
await fsp.rename(stagePath, targetPath);
promoted = true;
} catch (error) {
if (error.code !== "EEXIST" && error.code !== "ENOTEMPTY") throw error;
const raced = await catalogStatus({
cacheRoot: root,
packageVersion: normalizedIdentity.version,
catalogDigest: normalizedIdentity.catalogDigest,
integrity: normalizedIdentity.integrity,
});
if (raced.status !== "verified") throw cacheError("AAS_CACHE_PROMOTION_CONFLICT", "catalog cache key was occupied during promotion");
return { status: "alreadyPresent", identity: raced.identity, targetPath: raced.targetPath };
}
await fsyncDirectory(versionDirectory);
return { status: "promoted", identity: storedIdentity, targetPath };
} finally {
if (stagePath && !promoted) await fsp.rm(stagePath, { recursive: true, force: true });
if (!promoted) await removeCreatedEmptyDirectories(created);
}
}
module.exports = { promoteCatalogDirectory };
@@ -0,0 +1,59 @@
"use strict";
const fsp = require("node:fs/promises");
const path = require("node:path");
const { loadBundledCatalog } = require("../catalog");
const { catalogStatus } = require("./status");
const { validateCacheRoot, validateCatalogDigest } = require("./identity");
function compareStrings(left, right) {
return left < right ? -1 : (left > right ? 1 : 0);
}
async function realDirectories(directory) {
let entries;
try {
entries = await fsp.readdir(directory, { withFileTypes: true });
} catch (error) {
if (error.code === "ENOENT") return [];
throw error;
}
return entries.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
.map((entry) => entry.name).sort(compareStrings);
}
function createVerifiedCatalogResolver({ cacheRoot, bundledRoot, maximumVersions = 64 } = {}) {
const bundled = loadBundledCatalog({ root: bundledRoot });
const root = cacheRoot ? validateCacheRoot(cacheRoot) : null;
return async function resolveCatalog(digest) {
validateCatalogDigest(digest);
if (digest === bundled.digest) return bundled;
if (!root) return null;
const versions = await realDirectories(path.join(root, "catalogs"));
if (versions.length > maximumVersions) {
const error = new Error("verified catalog cache exceeds the resolver limit");
error.code = "AAS_CACHE_RESOLVER_LIMIT";
throw error;
}
const matches = [];
for (const version of versions) {
const status = await catalogStatus({ cacheRoot: root, packageVersion: version, catalogDigest: digest });
if (status.status === "verified") matches.push(status);
}
if (matches.length === 0) return null;
if (matches.length > 1) {
const error = new Error("catalog digest resolves to multiple verified cache identities");
error.code = "AAS_CACHE_RESOLVER_AMBIGUOUS";
throw error;
}
const catalog = loadBundledCatalog({ root: matches[0].targetPath });
if (catalog.digest !== digest) {
const error = new Error("resolved catalog digest changed after verification");
error.code = "AAS_CACHE_RESOLVER_DRIFT";
throw error;
}
return catalog;
};
}
module.exports = { createVerifiedCatalogResolver, realDirectories };
@@ -0,0 +1,381 @@
"use strict";
const crypto = require("node:crypto");
const fs = require("node:fs");
const fsp = require("node:fs/promises");
const path = require("node:path");
const { fsyncDirectoryAsync } = require("../durability");
const { canonicalJson, sha256 } = require("../canonical-json");
const { parsePackageArchive } = require("./archive");
const {
CATALOG_PACKAGE,
DIGEST_VERSION,
RUNTIME_IDENTITY_FILE,
cacheError,
parseNpmIntegrity,
runtimeCachePath,
validateCacheRoot,
validatePackageVersion,
} = require("./identity");
const { REGISTRY_ORIGIN, fetchBytes, verifySri } = require("./update");
const MAX_RUNTIME_IDENTITY_BYTES = 4 * 1024 * 1024;
const RUNTIME_ARCHIVE_LIMITS = Object.freeze({
maxEntries: 10000,
maxSingleFileBytes: 32 * 1024 * 1024,
maxExpandedTotalBytes: 160 * 1024 * 1024,
maxCompressionRatio: 128,
});
const REQUIRED_RUNTIME_FILES = Object.freeze([
"package.json",
"tools/bin/aas-mcp.js",
"tools/lib/aas-v1/index.js",
"data/aas-v1/catalog-manifest.v1.json",
"data/catalog.json",
"data/plugin-compatibility.json",
"skills_index.json",
]);
const REQUIRED_BUNDLED_DEPENDENCIES = Object.freeze(["ajv", "sanitize-filename", "yaml"]);
function allowedRuntimeAsset(relativePath) {
return relativePath === "package.json"
|| relativePath === "tools/bin/aas-mcp.js"
|| relativePath.startsWith("tools/lib/aas-v1/")
|| relativePath.startsWith("data/aas-v1/")
|| relativePath === "data/catalog.json"
|| relativePath === "data/plugin-compatibility.json"
|| relativePath === "skills_index.json"
|| relativePath.startsWith("skills/")
|| relativePath.startsWith("schemas/aas-v1/")
|| relativePath.startsWith("node_modules/");
}
function privateModeUnsafe(stat) {
return process.platform !== "win32" && (stat.mode & 0o077) !== 0;
}
function publicRuntimeIdentity(identity) {
return Object.freeze({
package: identity.package,
version: identity.version,
integrity: identity.integrity,
closureDigest: identity.closureDigest,
});
}
function validateRuntimeIdentity(identity, expected = {}) {
if (!identity || typeof identity !== "object" || Array.isArray(identity)) {
throw cacheError("AAS_RUNTIME_IDENTITY_INVALID", "runtime identity must be an object");
}
const allowed = new Set(["schemaVersion", "package", "version", "integrity", "closureDigest", "digestVersion", "assets", "provenance"]);
for (const key of Object.keys(identity)) {
if (!allowed.has(key)) throw cacheError("AAS_RUNTIME_IDENTITY_INVALID", `unknown runtime identity field: ${key}`);
}
if (identity.schemaVersion !== 1 || identity.package !== CATALOG_PACKAGE || identity.digestVersion !== DIGEST_VERSION) {
throw cacheError("AAS_RUNTIME_IDENTITY_INVALID", "runtime identity is incompatible");
}
const version = validatePackageVersion(identity.version);
const integrity = parseNpmIntegrity(identity.integrity).integrity;
if (typeof identity.closureDigest !== "string" || !/^sha256-[0-9a-f]{64}$/.test(identity.closureDigest)) {
throw cacheError("AAS_RUNTIME_IDENTITY_INVALID", "runtime closure digest is invalid");
}
if (!Array.isArray(identity.assets) || identity.assets.length === 0 || identity.assets.length > RUNTIME_ARCHIVE_LIMITS.maxEntries) {
throw cacheError("AAS_RUNTIME_IDENTITY_INVALID", "runtime asset records are invalid");
}
const seen = new Set();
const assets = identity.assets.map((record) => {
if (!record || typeof record !== "object" || Array.isArray(record)
|| Object.keys(record).sort().join(",") !== "path,sha256,size"
|| typeof record.path !== "string" || !record.path.startsWith("package/") || record.path.includes("\\")
|| record.path.split("/").some((part) => !part || part === "." || part === "..")
|| !Number.isSafeInteger(record.size) || record.size < 0
|| !/^sha256-[0-9a-f]{64}$/.test(record.sha256) || seen.has(record.path)) {
throw cacheError("AAS_RUNTIME_IDENTITY_INVALID", "runtime asset record is invalid");
}
seen.add(record.path);
return { path: record.path, size: record.size, sha256: record.sha256 };
}).sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
const provenance = identity.provenance;
if (!provenance || typeof provenance !== "object" || Array.isArray(provenance)
|| Object.keys(provenance).sort().join(",") !== "attestationsPresent,registryOrigin,signaturesPresent"
|| provenance.registryOrigin !== REGISTRY_ORIGIN || typeof provenance.signaturesPresent !== "boolean"
|| typeof provenance.attestationsPresent !== "boolean") {
throw cacheError("AAS_RUNTIME_IDENTITY_INVALID", "runtime provenance is invalid");
}
const normalized = {
schemaVersion: 1,
package: CATALOG_PACKAGE,
version,
integrity,
closureDigest: identity.closureDigest,
digestVersion: DIGEST_VERSION,
assets,
provenance: { ...provenance },
};
if (sha256(canonicalJson({ digestVersion: DIGEST_VERSION, assets })) !== normalized.closureDigest) {
throw cacheError("AAS_RUNTIME_IDENTITY_INVALID", "runtime asset records do not match the closure digest");
}
for (const key of ["package", "version", "integrity", "closureDigest"]) {
if (expected[key] !== undefined && normalized[key] !== expected[key]) {
throw cacheError("AAS_RUNTIME_IDENTITY_MISMATCH", `runtime identity ${key} does not match the expected value`);
}
}
return normalized;
}
function releaseMetadataUrl(version) {
return `${REGISTRY_ORIGIN}/${CATALOG_PACKAGE}/${validatePackageVersion(version)}`;
}
async function inspectRuntimeRelease({ version, fetcher = fetchBytes }) {
const packageVersion = validatePackageVersion(version);
const bytes = await fetcher(releaseMetadataUrl(packageVersion), 2 * 1024 * 1024);
let metadata;
try { metadata = JSON.parse(bytes.toString("utf8")); } catch {
throw cacheError("AAS_RUNTIME_PACKUMENT_INVALID", "registry metadata is invalid JSON");
}
const dist = metadata?.dist;
if (metadata?.name !== CATALOG_PACKAGE || metadata?.version !== packageVersion
|| !dist || typeof dist.integrity !== "string" || typeof dist.tarball !== "string") {
throw cacheError("AAS_RUNTIME_PACKUMENT_INVALID", "registry metadata lacks the exact runtime identity");
}
const integrity = parseNpmIntegrity(dist.integrity).integrity;
const tarball = new URL(dist.tarball);
if (tarball.origin !== REGISTRY_ORIGIN || tarball.protocol !== "https:") {
throw cacheError("AAS_RUNTIME_TARBALL_URL_INVALID", "runtime tarball URL is outside the pinned npm origin");
}
return {
package: CATALOG_PACKAGE,
version: packageVersion,
integrity,
tarballUrl: tarball.href,
provenance: {
registryOrigin: REGISTRY_ORIGIN,
distIntegrity: integrity,
signaturesPresent: Array.isArray(dist.signatures) && dist.signatures.length > 0,
attestationsPresent: Boolean(dist.attestations),
},
};
}
function runtimeRecords(entries, version) {
if (entries.some((entry) => !entry.path.startsWith("package/"))) {
throw cacheError("AAS_RUNTIME_ARCHIVE_ROOT_INVALID", "runtime archive contains a file outside the npm package root");
}
const selectedEntries = entries.filter((entry) => allowedRuntimeAsset(entry.path.slice("package/".length)));
const byPath = new Map(selectedEntries.map((entry) => [entry.path.slice("package/".length), entry]));
for (const required of REQUIRED_RUNTIME_FILES) {
if (!byPath.has(required)) throw cacheError("AAS_RUNTIME_FILE_MISSING", `runtime archive is missing ${required}`);
}
let metadata;
try { metadata = JSON.parse(byPath.get("package.json").bytes.toString("utf8")); } catch {
throw cacheError("AAS_RUNTIME_PACKAGE_INVALID", "runtime package metadata is invalid");
}
const bin = typeof metadata.bin === "string" ? { [CATALOG_PACKAGE]: metadata.bin } : metadata.bin;
if (metadata.name !== CATALOG_PACKAGE || metadata.version !== version || bin?.["aas-mcp"] !== "tools/bin/aas-mcp.js") {
throw cacheError("AAS_RUNTIME_PACKAGE_INVALID", "runtime package metadata does not expose the expected MCP binary");
}
const bundled = metadata.bundledDependencies || metadata.bundleDependencies;
if (!Array.isArray(bundled)
|| REQUIRED_BUNDLED_DEPENDENCIES.some((dependency) => !bundled.includes(dependency))
|| REQUIRED_BUNDLED_DEPENDENCIES.some((dependency) => !byPath.has(`node_modules/${dependency}/package.json`))) {
throw cacheError("AAS_RUNTIME_DEPENDENCY_CLOSURE_MISSING", "runtime package lacks its declared verified dependency closure");
}
const records = selectedEntries.map((entry) => ({ path: entry.path, size: entry.bytes.length, sha256: sha256(entry.bytes), bytes: entry.bytes }))
.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
const assets = records.map(({ path: assetPath, size, sha256: digest }) => ({ path: assetPath, size, sha256: digest }));
return { records, assets, closureDigest: sha256(canonicalJson({ digestVersion: DIGEST_VERSION, assets })) };
}
async function ensureRealDirectory(directoryPath, created) {
try {
const stat = await fsp.lstat(directoryPath);
if (!stat.isDirectory() || stat.isSymbolicLink() || privateModeUnsafe(stat)) {
throw cacheError("AAS_RUNTIME_DIRECTORY_UNSAFE", "runtime cache path is not a private real directory");
}
} catch (error) {
if (error.code !== "ENOENT") throw error;
const parent = path.dirname(directoryPath);
if (parent !== directoryPath) await ensureRealDirectory(parent, created);
try {
await fsp.mkdir(directoryPath, { mode: 0o700 });
created.push(directoryPath);
} catch (mkdirError) {
if (mkdirError.code !== "EEXIST") throw mkdirError;
const stat = await fsp.lstat(directoryPath);
if (!stat.isDirectory() || stat.isSymbolicLink() || privateModeUnsafe(stat)) {
throw cacheError("AAS_RUNTIME_DIRECTORY_UNSAFE", "runtime cache path is not a private real directory");
}
}
}
}
async function fsyncDirectory(directoryPath) {
await fsyncDirectoryAsync(directoryPath);
}
async function writeExclusive(filePath, bytes, { sync = true } = {}) {
const handle = await fsp.open(filePath, "wx", 0o600);
try {
await handle.writeFile(bytes);
await handle.chmod(0o600);
if (sync) await handle.sync();
} finally { await handle.close(); }
}
async function readRuntimeAsset(targetPath, record) {
const absolute = path.join(targetPath, ...record.path.split("/"));
const stat = await fsp.lstat(absolute);
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || privateModeUnsafe(stat) || stat.size !== record.size) {
throw cacheError("AAS_RUNTIME_CONTENT_MISMATCH", "cached runtime contains an unsafe or changed asset");
}
const handle = await fsp.open(absolute, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
try {
const before = await handle.stat();
const bytes = await handle.readFile();
const after = await handle.stat();
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || sha256(bytes) !== record.sha256) {
throw cacheError("AAS_RUNTIME_CONTENT_MISMATCH", "cached runtime changed during verification");
}
} finally { await handle.close(); }
}
async function listRuntimeFiles(root, relative = "") {
const directory = relative ? path.join(root, ...relative.split("/")) : root;
const stat = await fsp.lstat(directory);
if (!stat.isDirectory() || stat.isSymbolicLink() || privateModeUnsafe(stat)) {
throw cacheError("AAS_RUNTIME_CONTENT_MISMATCH", "cached runtime contains an unsafe directory");
}
const found = [];
const entries = await fsp.readdir(directory, { withFileTypes: true });
entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
for (const entry of entries) {
const next = relative ? `${relative}/${entry.name}` : entry.name;
const absolute = path.join(root, ...next.split("/"));
const entryStat = await fsp.lstat(absolute);
if (entryStat.isSymbolicLink()) throw cacheError("AAS_RUNTIME_CONTENT_MISMATCH", "cached runtime contains a link");
if (entryStat.isDirectory()) found.push(...await listRuntimeFiles(root, next));
else if (entryStat.isFile()) found.push(next);
else throw cacheError("AAS_RUNTIME_CONTENT_MISMATCH", "cached runtime contains a special file");
}
return found;
}
async function runtimeStatus({ cacheRoot, packageVersion, integrity, closureDigest }) {
const targetPath = runtimeCachePath({ cacheRoot, packageVersion, integrity });
try {
const stat = await fsp.lstat(targetPath);
if (!stat.isDirectory() || stat.isSymbolicLink()) throw cacheError("AAS_RUNTIME_TARGET_INVALID", "runtime cache target is not a real directory");
const identityPath = path.join(targetPath, RUNTIME_IDENTITY_FILE);
const identityStat = await fsp.lstat(identityPath);
if (!identityStat.isFile() || identityStat.isSymbolicLink() || identityStat.nlink !== 1 || identityStat.size > MAX_RUNTIME_IDENTITY_BYTES) {
throw cacheError("AAS_RUNTIME_IDENTITY_INVALID", "stored runtime identity is not a bounded regular file");
}
const text = await fsp.readFile(identityPath, "utf8");
const parsed = JSON.parse(text);
if (`${canonicalJson(parsed)}\n` !== text) throw cacheError("AAS_RUNTIME_IDENTITY_INVALID", "stored runtime identity is not canonical JSON");
const identity = validateRuntimeIdentity(parsed, { version: packageVersion, integrity, ...(closureDigest ? { closureDigest } : {}) });
const files = (await listRuntimeFiles(targetPath)).sort();
const expected = [RUNTIME_IDENTITY_FILE, ...identity.assets.map((asset) => asset.path)].sort();
if (canonicalJson(files) !== canonicalJson(expected)) throw cacheError("AAS_RUNTIME_CONTENT_MISMATCH", "cached runtime has missing or unexpected files");
for (let index = 0; index < identity.assets.length; index += 32) {
await Promise.all(identity.assets.slice(index, index + 32).map((record) => readRuntimeAsset(targetPath, record)));
}
return { status: "verified", present: true, identity, runtimeIdentity: publicRuntimeIdentity(identity), targetPath };
} catch (error) {
if (error.code === "ENOENT") return { status: "missing", present: false, targetPath };
return { status: "invalid", present: true, targetPath, error: { code: error.code || "AAS_RUNTIME_STATUS_FAILED", message: error.message } };
}
}
async function promoteRuntime({ cacheRoot, release, parsed }) {
const scanned = runtimeRecords(parsed.entries, release.version);
const targetPath = runtimeCachePath({ cacheRoot, packageVersion: release.version, integrity: release.integrity });
const existing = await runtimeStatus({ cacheRoot, packageVersion: release.version, integrity: release.integrity, closureDigest: scanned.closureDigest });
if (existing.status === "verified") return { ...existing, status: "alreadyPresent" };
if (existing.status === "invalid") throw cacheError("AAS_RUNTIME_EXISTING_INVALID", "an invalid object occupies the immutable runtime cache key");
const versionDirectory = path.dirname(targetPath);
const created = [];
let stagePath;
let promoted = false;
try {
await ensureRealDirectory(versionDirectory, created);
stagePath = path.join(versionDirectory, `.stage-${process.pid}-${crypto.randomBytes(12).toString("hex")}`);
await fsp.mkdir(stagePath, { mode: 0o700 });
const directories = new Set([stagePath]);
for (const record of scanned.records) {
const segments = record.path.split("/");
let cursor = stagePath;
for (const segment of segments.slice(0, -1)) {
cursor = path.join(cursor, segment);
if (!directories.has(cursor)) { await fsp.mkdir(cursor, { mode: 0o700 }); directories.add(cursor); }
}
await writeExclusive(path.join(stagePath, ...segments), record.bytes);
}
const identity = validateRuntimeIdentity({
schemaVersion: 1,
package: CATALOG_PACKAGE,
version: release.version,
integrity: release.integrity,
closureDigest: scanned.closureDigest,
digestVersion: DIGEST_VERSION,
assets: scanned.assets,
provenance: {
registryOrigin: release.provenance.registryOrigin,
signaturesPresent: release.provenance.signaturesPresent,
attestationsPresent: release.provenance.attestationsPresent,
},
});
await writeExclusive(path.join(stagePath, RUNTIME_IDENTITY_FILE), Buffer.from(`${canonicalJson(identity)}\n`));
// Persist every file and nested directory before making the immutable
// content-addressed object visible through the final rename.
for (const directory of [...directories].sort((left, right) => right.split(path.sep).length - left.split(path.sep).length)) {
await fsyncDirectory(directory);
}
try { await fsp.rename(stagePath, targetPath); promoted = true; } catch (error) {
if (error.code !== "EEXIST" && error.code !== "ENOTEMPTY") throw error;
const raced = await runtimeStatus({ cacheRoot, packageVersion: release.version, integrity: release.integrity, closureDigest: scanned.closureDigest });
if (raced.status !== "verified") throw cacheError("AAS_RUNTIME_PROMOTION_CONFLICT", "runtime cache key was occupied during promotion");
return { ...raced, status: "alreadyPresent" };
}
await fsyncDirectory(versionDirectory);
return { status: "promoted", present: true, identity, runtimeIdentity: publicRuntimeIdentity(identity), targetPath };
} finally {
if (stagePath && !promoted) await fsp.rm(stagePath, { recursive: true, force: true });
if (!promoted) {
for (const directory of [...created].reverse()) await fsp.rmdir(directory).catch((error) => { if (error.code !== "ENOENT" && error.code !== "ENOTEMPTY") throw error; });
}
}
}
async function installRuntimeFromRegistry({ cacheRoot, version, expectedIntegrity, fetcher = fetchBytes }) {
validateCacheRoot(cacheRoot);
const release = await inspectRuntimeRelease({ version, fetcher });
if (expectedIntegrity !== undefined && release.integrity !== parseNpmIntegrity(expectedIntegrity).integrity) {
throw cacheError("AAS_RUNTIME_RELEASE_CHANGED", "runtime release integrity differs from the approved preview");
}
const archive = await fetcher(release.tarballUrl, 64 * 1024 * 1024);
verifySri(archive, release.integrity);
const parsed = parsePackageArchive(archive, { limits: RUNTIME_ARCHIVE_LIMITS });
const promoted = await promoteRuntime({ cacheRoot, release, parsed });
return { ok: true, status: promoted.status, runtimeIdentity: promoted.runtimeIdentity, targetPath: promoted.targetPath, provenance: release.provenance };
}
function runtimeMcpPath({ cacheRoot, packageVersion, integrity }) {
return path.join(runtimeCachePath({ cacheRoot, packageVersion, integrity }), "package", "tools", "bin", "aas-mcp.js");
}
module.exports = {
MAX_RUNTIME_IDENTITY_BYTES,
REQUIRED_RUNTIME_FILES,
REQUIRED_BUNDLED_DEPENDENCIES,
RUNTIME_ARCHIVE_LIMITS,
inspectRuntimeRelease,
installRuntimeFromRegistry,
promoteRuntime,
publicRuntimeIdentity,
runtimeMcpPath,
runtimeRecords,
runtimeStatus,
validateRuntimeIdentity,
};
@@ -0,0 +1,178 @@
"use strict";
const fs = require("node:fs");
const fsp = require("node:fs/promises");
const path = require("node:path");
const { canonicalJson, sha256 } = require("../canonical-json");
const { DIGEST_VERSION, cacheError } = require("./identity");
const DEFAULT_LIMITS = Object.freeze({
maxFiles: 128,
maxEntries: 256,
maxFileBytes: 32 * 1024 * 1024,
maxTotalBytes: 128 * 1024 * 1024,
maxDepth: 8,
});
function collisionKey(relativePath) {
return relativePath.normalize("NFKC").toLowerCase();
}
function validateRelativeAssetPath(value) {
if (typeof value !== "string" || value.length === 0 || value.length > 512 || value.includes("\0") || value.includes("\\")) {
throw cacheError("AAS_CACHE_ASSET_PATH_INVALID", "catalog asset path is invalid");
}
if (path.posix.isAbsolute(value) || path.win32.isAbsolute(value)) {
throw cacheError("AAS_CACHE_ASSET_PATH_INVALID", `absolute catalog asset path is forbidden: ${value}`);
}
const segments = value.split("/");
if (segments.some((segment) => segment === "" || segment === "." || segment === "..")) {
throw cacheError("AAS_CACHE_ASSET_PATH_INVALID", `catalog asset traversal is forbidden: ${value}`);
}
if (path.posix.normalize(value) !== value) {
throw cacheError("AAS_CACHE_ASSET_PATH_INVALID", `catalog asset path is not normalized: ${value}`);
}
return value;
}
function normalizeLimits(limits = {}) {
const merged = { ...DEFAULT_LIMITS, ...limits };
for (const [key, value] of Object.entries(merged)) {
if (!Number.isSafeInteger(value) || value < 1) throw cacheError("AAS_CACHE_LIMIT_INVALID", `${key} must be a positive safe integer`);
}
return merged;
}
function normalizeAllowlist(allowlist) {
if (!Array.isArray(allowlist) || allowlist.length === 0) {
throw cacheError("AAS_CACHE_ALLOWLIST_INVALID", "a non-empty explicit catalog asset allowlist is required");
}
const seen = new Map();
return allowlist.map(validateRelativeAssetPath).sort().map((assetPath) => {
const key = collisionKey(assetPath);
if (seen.has(key)) {
throw cacheError("AAS_CACHE_PATH_COLLISION", `allowlist paths collide: ${seen.get(key)} and ${assetPath}`);
}
seen.set(key, assetPath);
return assetPath;
});
}
function assertSafeMode(stat, relativePath, isDirectory) {
if ((stat.mode & 0o7000) !== 0 || (stat.mode & 0o022) !== 0 || (!isDirectory && (stat.mode & 0o111) !== 0)) {
throw cacheError("AAS_CACHE_MODE_UNSAFE", `anomalous permissions on catalog input: ${relativePath || "."}`);
}
}
async function readRegularFileNoFollow(absolutePath, relativePath, priorStat, limits) {
const flags = fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0);
let handle;
try {
handle = await fsp.open(absolutePath, flags);
const before = await handle.stat();
if (!before.isFile() || before.nlink !== 1 || before.dev !== priorStat.dev || before.ino !== priorStat.ino) {
throw cacheError("AAS_CACHE_INPUT_CHANGED", `catalog input changed during validation: ${relativePath}`);
}
if (before.size > limits.maxFileBytes) throw cacheError("AAS_CACHE_FILE_LIMIT", `catalog asset exceeds the file-size limit: ${relativePath}`);
const bytes = await handle.readFile();
const after = await handle.stat();
if (bytes.length !== before.size || after.size !== before.size || after.dev !== before.dev || after.ino !== before.ino) {
throw cacheError("AAS_CACHE_INPUT_CHANGED", `catalog input changed while being read: ${relativePath}`);
}
return bytes;
} finally {
if (handle) await handle.close();
}
}
async function scanDataDirectory({ sourceDir, allowlist, ignoredPaths = [], limits }) {
if (typeof sourceDir !== "string" || sourceDir.length === 0 || sourceDir.includes("\0")) {
throw cacheError("AAS_CACHE_SOURCE_INVALID", "catalog source directory is invalid");
}
const normalizedAllowlist = normalizeAllowlist(allowlist);
const ignored = new Set(ignoredPaths.map(validateRelativeAssetPath));
for (const ignoredPath of ignored) {
if (normalizedAllowlist.includes(ignoredPath)) throw cacheError("AAS_CACHE_ALLOWLIST_INVALID", `ignored path is also allowlisted: ${ignoredPath}`);
}
const bounded = normalizeLimits(limits);
const root = path.resolve(sourceDir);
const rootStat = await fsp.lstat(root).catch((error) => {
if (error.code === "ENOENT") throw cacheError("AAS_CACHE_SOURCE_MISSING", "catalog source directory does not exist");
throw error;
});
if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) throw cacheError("AAS_CACHE_SOURCE_INVALID", "catalog source must be a real directory");
assertSafeMode(rootStat, "", true);
const allowed = new Set(normalizedAllowlist);
const allowedDirectories = new Set();
for (const assetPath of [...normalizedAllowlist, ...ignored]) {
const segments = assetPath.split("/");
for (let index = 1; index < segments.length; index += 1) allowedDirectories.add(segments.slice(0, index).join("/"));
}
const collisionPaths = new Map();
const records = [];
const ignoredRecords = [];
let entryCount = 0;
let totalBytes = 0;
async function walk(relativeDirectory) {
const directory = relativeDirectory ? path.join(root, ...relativeDirectory.split("/")) : root;
const entries = await fsp.readdir(directory, { withFileTypes: true });
entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
for (const entry of entries) {
entryCount += 1;
if (entryCount > bounded.maxEntries) throw cacheError("AAS_CACHE_ENTRY_LIMIT", "catalog input exceeds the entry-count limit");
if (entry.name.includes("/") || entry.name.includes("\\") || entry.name === "." || entry.name === ".." || entry.name.includes("\0")) {
throw cacheError("AAS_CACHE_ASSET_PATH_INVALID", "catalog input contains an unsafe directory entry");
}
const relativePath = relativeDirectory ? `${relativeDirectory}/${entry.name}` : entry.name;
validateRelativeAssetPath(relativePath);
if (relativePath.split("/").length > bounded.maxDepth) throw cacheError("AAS_CACHE_DEPTH_LIMIT", `catalog input exceeds maximum depth: ${relativePath}`);
const key = collisionKey(relativePath);
if (collisionPaths.has(key)) throw cacheError("AAS_CACHE_PATH_COLLISION", `catalog paths collide: ${collisionPaths.get(key)} and ${relativePath}`);
collisionPaths.set(key, relativePath);
const absolutePath = path.join(root, ...relativePath.split("/"));
const stat = await fsp.lstat(absolutePath);
if (stat.isSymbolicLink()) throw cacheError("AAS_CACHE_LINK_FORBIDDEN", `symbolic links are forbidden: ${relativePath}`);
if (stat.isDirectory()) {
if (!allowedDirectories.has(relativePath)) throw cacheError("AAS_CACHE_ASSET_NOT_ALLOWED", `directory is not allowlisted: ${relativePath}`);
assertSafeMode(stat, relativePath, true);
await walk(relativePath);
continue;
}
if (!stat.isFile()) throw cacheError("AAS_CACHE_SPECIAL_FILE_FORBIDDEN", `special files are forbidden: ${relativePath}`);
if (stat.nlink !== 1) throw cacheError("AAS_CACHE_HARDLINK_FORBIDDEN", `hard-linked files are forbidden: ${relativePath}`);
assertSafeMode(stat, relativePath, false);
if (!allowed.has(relativePath) && !ignored.has(relativePath)) {
throw cacheError("AAS_CACHE_ASSET_NOT_ALLOWED", `file is not allowlisted: ${relativePath}`);
}
const bytes = await readRegularFileNoFollow(absolutePath, relativePath, stat, bounded);
totalBytes += bytes.length;
if (totalBytes > bounded.maxTotalBytes) throw cacheError("AAS_CACHE_TOTAL_LIMIT", "catalog input exceeds the expanded-byte limit");
const record = { path: relativePath, size: bytes.length, sha256: sha256(bytes), bytes };
if (ignored.has(relativePath)) ignoredRecords.push(record);
else records.push(record);
}
}
await walk("");
if (records.length > bounded.maxFiles) throw cacheError("AAS_CACHE_FILE_COUNT_LIMIT", "catalog input exceeds the file-count limit");
const found = new Set(records.map((record) => record.path));
const missing = normalizedAllowlist.filter((assetPath) => !found.has(assetPath));
if (missing.length > 0) throw cacheError("AAS_CACHE_ASSET_MISSING", `required catalog asset is missing: ${missing[0]}`);
records.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
const publicRecords = records.map(({ path: assetPath, size, sha256: digest }) => ({ path: assetPath, size, sha256: digest }));
const publicIgnoredRecords = ignoredRecords.map(({ path: assetPath, size, sha256: digest }) => ({ path: assetPath, size, sha256: digest }));
const catalogDigest = sha256(canonicalJson({ digestVersion: DIGEST_VERSION, assets: publicRecords }));
return { catalogDigest, records, ignoredRecords, publicRecords, publicIgnoredRecords, totalBytes };
}
module.exports = {
DEFAULT_LIMITS,
collisionKey,
normalizeAllowlist,
normalizeLimits,
scanDataDirectory,
validateRelativeAssetPath,
};
@@ -0,0 +1,93 @@
"use strict";
const fsp = require("node:fs/promises");
const path = require("node:path");
const { canonicalJson } = require("../canonical-json");
const {
CATALOG_IDENTITY_FILE,
DIGEST_VERSION,
cacheError,
catalogCachePath,
validateCatalogIdentity,
} = require("./identity");
const { scanDataDirectory } = require("./scan");
const MAX_IDENTITY_BYTES = 128 * 1024;
function validateStoredIdentity(value, expected) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw cacheError("AAS_CACHE_IDENTITY_INVALID", "stored catalog identity must be an object");
const allowed = new Set(["schemaVersion", "package", "version", "integrity", "catalogDigest", "digestVersion", "assets", "controls"]);
for (const key of Object.keys(value)) {
if (!allowed.has(key)) throw cacheError("AAS_CACHE_IDENTITY_INVALID", `unknown stored identity field: ${key}`);
}
const identity = validateCatalogIdentity({
schemaVersion: value.schemaVersion,
package: value.package,
version: value.version,
integrity: value.integrity,
catalogDigest: value.catalogDigest,
}, expected);
if (value.digestVersion !== DIGEST_VERSION) throw cacheError("AAS_CACHE_IDENTITY_INVALID", "unsupported catalog digest version");
if (!Array.isArray(value.assets) || value.assets.length === 0) throw cacheError("AAS_CACHE_IDENTITY_INVALID", "stored catalog identity needs asset records");
const assets = value.assets.map((record) => {
if (!record || typeof record !== "object" || Array.isArray(record) || Object.keys(record).sort().join(",") !== "path,sha256,size") {
throw cacheError("AAS_CACHE_IDENTITY_INVALID", "stored catalog asset record is invalid");
}
if (!Number.isSafeInteger(record.size) || record.size < 0 || !/^sha256-[0-9a-f]{64}$/.test(record.sha256)) {
throw cacheError("AAS_CACHE_IDENTITY_INVALID", "stored catalog asset size or digest is invalid");
}
return { path: record.path, size: record.size, sha256: record.sha256 };
});
const controls = (value.controls || []).map((record) => {
if (!record || typeof record !== "object" || Array.isArray(record) || Object.keys(record).sort().join(",") !== "path,sha256,size") {
throw cacheError("AAS_CACHE_IDENTITY_INVALID", "stored catalog control record is invalid");
}
if (!Number.isSafeInteger(record.size) || record.size < 0 || !/^sha256-[0-9a-f]{64}$/.test(record.sha256)) {
throw cacheError("AAS_CACHE_IDENTITY_INVALID", "stored catalog control size or digest is invalid");
}
return { path: record.path, size: record.size, sha256: record.sha256 };
});
return { ...identity, digestVersion: DIGEST_VERSION, assets, controls };
}
async function catalogStatus({ cacheRoot, packageVersion, catalogDigest, integrity }) {
const targetPath = catalogCachePath({ cacheRoot, packageVersion, catalogDigest });
try {
const targetStat = await fsp.lstat(targetPath);
if (!targetStat.isDirectory() || targetStat.isSymbolicLink()) throw cacheError("AAS_CACHE_TARGET_INVALID", "catalog cache target is not a real directory");
const identityPath = path.join(targetPath, CATALOG_IDENTITY_FILE);
const identityStat = await fsp.lstat(identityPath);
if (!identityStat.isFile() || identityStat.isSymbolicLink() || identityStat.nlink !== 1 || identityStat.size > MAX_IDENTITY_BYTES) {
throw cacheError("AAS_CACHE_IDENTITY_INVALID", "stored catalog identity is not a bounded regular file");
}
const text = await fsp.readFile(identityPath, "utf8");
const parsed = JSON.parse(text);
if (`${canonicalJson(parsed)}\n` !== text) throw cacheError("AAS_CACHE_IDENTITY_INVALID", "stored catalog identity is not canonical JSON");
const identity = validateStoredIdentity(parsed, {
version: packageVersion,
catalogDigest,
...(integrity === undefined ? {} : { integrity }),
});
const scan = await scanDataDirectory({
sourceDir: targetPath,
allowlist: identity.assets.map((asset) => asset.path),
ignoredPaths: [CATALOG_IDENTITY_FILE, ...identity.controls.map((asset) => asset.path)],
});
const observedControls = scan.publicIgnoredRecords.filter((record) => record.path !== CATALOG_IDENTITY_FILE);
if (scan.catalogDigest !== identity.catalogDigest || canonicalJson(scan.publicRecords) !== canonicalJson(identity.assets)
|| canonicalJson(observedControls) !== canonicalJson(identity.controls)) {
throw cacheError("AAS_CACHE_CONTENT_MISMATCH", "cached catalog bytes do not match their identity");
}
return { status: "verified", present: true, identity, targetPath };
} catch (error) {
if (error.code === "ENOENT") return { status: "missing", present: false, targetPath };
return {
status: "invalid",
present: true,
targetPath,
error: { code: error.code || "AAS_CACHE_STATUS_FAILED", message: error.message },
};
}
}
module.exports = { MAX_IDENTITY_BYTES, catalogStatus, validateStoredIdentity };
@@ -0,0 +1,161 @@
"use strict";
const crypto = require("node:crypto");
const fs = require("node:fs");
const https = require("node:https");
const os = require("node:os");
const path = require("node:path");
const { canonicalJson } = require("../canonical-json");
const { parsePackageArchive } = require("./archive");
const { cacheError, parseNpmIntegrity, validatePackageVersion } = require("./identity");
const { promoteCatalogDirectory } = require("./promote");
const PACKAGE_NAME = "agentic-awesome-skills";
const REGISTRY_ORIGIN = "https://registry.npmjs.org";
const CATALOG_MANIFEST_PATH = "data/aas-v1/catalog-manifest.v1.json";
const CATALOG_ASSET_PATHS = Object.freeze([
"data/aas-v1/skill-content-index.v1.json",
"data/aas-v1/skill-content.v1.ndjson",
"data/catalog.json",
"data/plugin-compatibility.json",
"tools/lib/aas-v1/metadata-reviews.v1.json",
"tools/lib/aas-v1/metadata-overrides.v1.json",
"tools/lib/aas-v1/ontology.v1.json",
]);
function fetchBytes(url, maximumBytes, request = https.get) {
const parsed = new URL(url);
if (parsed.origin !== REGISTRY_ORIGIN || parsed.protocol !== "https:") {
return Promise.reject(cacheError("AAS_UPDATE_REGISTRY_URL_INVALID", "registry URL is outside the pinned npm origin"));
}
return new Promise((resolve, reject) => {
const call = request(parsed, {
headers: { accept: "application/json", "user-agent": "agentic-awesome-skills/catalog-updater-v1" },
timeout: 15000,
}, (response) => {
if (response.statusCode !== 200) {
response.resume();
reject(cacheError("AAS_UPDATE_HTTP_STATUS", `registry returned ${response.statusCode}`));
return;
}
const chunks = [];
let total = 0;
response.on("data", (chunk) => {
total += chunk.length;
if (total > maximumBytes) {
response.destroy(cacheError("AAS_UPDATE_DOWNLOAD_LIMIT", "registry response exceeded its byte limit"));
return;
}
chunks.push(chunk);
});
response.on("end", () => resolve(Buffer.concat(chunks)));
response.on("error", reject);
});
call.on("timeout", () => call.destroy(cacheError("AAS_UPDATE_TIMEOUT", "registry request timed out")));
call.on("error", reject);
});
}
function verifySri(bytes, integrity) {
const parsed = parseNpmIntegrity(integrity);
const actual = crypto.createHash(parsed.algorithm).update(bytes).digest();
if (actual.length !== parsed.bytes.length || !crypto.timingSafeEqual(actual, parsed.bytes)) {
throw cacheError("AAS_UPDATE_DIST_INTEGRITY_MISMATCH", "downloaded tarball does not match npm dist.integrity");
}
}
function validateCatalogManifest(bytes, version) {
const text = bytes.toString("utf8");
const manifest = JSON.parse(text);
if (`${canonicalJson(manifest)}\n` !== text || manifest.schemaVersion !== 1 || manifest.digestVersion !== 1
|| manifest.package !== PACKAGE_NAME || manifest.packageVersion !== version || manifest.skillCount !== 1965
|| !/^sha256-[a-f0-9]{64}$/.test(manifest.catalogDigest) || !Array.isArray(manifest.assets)) {
throw cacheError("AAS_UPDATE_CATALOG_MANIFEST_INVALID", "catalog manifest is invalid or incompatible");
}
const paths = manifest.assets.map((asset) => asset.path).sort();
if (canonicalJson(paths) !== canonicalJson([...CATALOG_ASSET_PATHS].sort())) {
throw cacheError("AAS_UPDATE_CATALOG_ALLOWLIST_MISMATCH", "release catalog assets do not match the v1 allowlist");
}
return manifest;
}
function materializeSelected(entries) {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), "aas-catalog-update-"));
try {
for (const entry of entries) {
const relative = entry.path.replace(/^package\//, "");
const output = path.join(directory, ...relative.split("/"));
fs.mkdirSync(path.dirname(output), { recursive: true, mode: 0o700 });
fs.writeFileSync(output, entry.bytes, { mode: 0o600, flag: "wx" });
}
return directory;
} catch (error) {
fs.rmSync(directory, { recursive: true, force: true });
throw error;
}
}
async function updateCatalogFromRegistry({ cacheRoot, version, fetcher = fetchBytes }) {
const packageVersion = validatePackageVersion(version);
const metadataUrl = `${REGISTRY_ORIGIN}/${PACKAGE_NAME}/${packageVersion}`;
const metadataBytes = await fetcher(metadataUrl, 2 * 1024 * 1024);
let metadata;
try { metadata = JSON.parse(metadataBytes.toString("utf8")); } catch {
throw cacheError("AAS_UPDATE_PACKUMENT_INVALID", "registry metadata is invalid JSON");
}
const dist = metadata?.dist;
if (!dist || typeof dist.integrity !== "string" || typeof dist.tarball !== "string") {
throw cacheError("AAS_UPDATE_PACKUMENT_INVALID", "registry metadata lacks dist identity");
}
const tarballUrl = new URL(dist.tarball);
if (tarballUrl.origin !== REGISTRY_ORIGIN || tarballUrl.protocol !== "https:") {
throw cacheError("AAS_UPDATE_TARBALL_URL_INVALID", "registry tarball URL is outside the pinned origin");
}
const archiveBytes = await fetcher(tarballUrl.href, 64 * 1024 * 1024);
verifySri(archiveBytes, dist.integrity);
const selectedPaths = [CATALOG_MANIFEST_PATH, ...CATALOG_ASSET_PATHS].map((asset) => `package/${asset}`);
const parsed = parsePackageArchive(archiveBytes, { selectPaths: selectedPaths });
const byPath = new Map(parsed.entries.map((entry) => [entry.path.replace(/^package\//, ""), entry]));
const manifest = validateCatalogManifest(byPath.get(CATALOG_MANIFEST_PATH).bytes, packageVersion);
const sourceDir = materializeSelected(parsed.entries);
try {
const promoted = await promoteCatalogDirectory({
cacheRoot,
sourceDir,
allowlist: CATALOG_ASSET_PATHS,
controlPaths: [CATALOG_MANIFEST_PATH],
identity: {
schemaVersion: 1,
package: PACKAGE_NAME,
version: packageVersion,
integrity: dist.integrity,
catalogDigest: manifest.catalogDigest,
},
limits: { maxFiles: 16, maxEntries: 32, maxFileBytes: 32 * 1024 * 1024, maxTotalBytes: 64 * 1024 * 1024, maxDepth: 8 },
});
return {
ok: true,
status: promoted.status,
identity: promoted.identity,
provenance: {
registryOrigin: REGISTRY_ORIGIN,
distIntegrity: dist.integrity,
signaturesPresent: Array.isArray(dist.signatures) && dist.signatures.length > 0,
attestationsPresent: Boolean(dist.attestations),
},
};
} finally {
fs.rmSync(sourceDir, { recursive: true, force: true });
}
}
module.exports = {
CATALOG_ASSET_PATHS,
CATALOG_MANIFEST_PATH,
PACKAGE_NAME,
REGISTRY_ORIGIN,
fetchBytes,
updateCatalogFromRegistry,
validateCatalogManifest,
verifySri,
};