Files
2026-07-20 00:03:02 +00:00

160 lines
6.5 KiB
JavaScript

"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",
"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
|| !Number.isSafeInteger(manifest.skillCount) || manifest.skillCount <= 0
|| !/^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,
};