mirror of
https://github.com/larksuite/cli.git
synced 2026-07-07 00:55:53 +08:00
509 lines
21 KiB
JavaScript
509 lines
21 KiB
JavaScript
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
const fs = require("fs");
|
|
const crypto = require("crypto");
|
|
const zlib = require("zlib");
|
|
|
|
const MAX_FACTS_BYTES = 4 * 1024 * 1024;
|
|
const MAX_COMPRESSION_RATIO = 100;
|
|
const MAX_ARRAY_ITEMS = 5000;
|
|
const MAX_STRING_BYTES = 8192;
|
|
const VALID_ACTIONS = new Set(["REJECT", "LABEL", "WARNING"]);
|
|
const MAX_OBJECT_KEYS = 1000;
|
|
const MAX_JSON_DEPTH = 12;
|
|
|
|
function isSymlink(entry) {
|
|
return ((entry.externalFileAttributes >>> 16) & 0o170000) === 0o120000;
|
|
}
|
|
|
|
function isRegularOrUnspecified(entry) {
|
|
const fileType = (entry.externalFileAttributes >>> 16) & 0o170000;
|
|
return fileType === 0 || fileType === 0o100000;
|
|
}
|
|
|
|
function verifyZipEntries(entries) {
|
|
if (entries.length !== 1) {
|
|
throw new Error(`expected exactly one artifact file, got ${entries.length}`);
|
|
}
|
|
const entry = entries[0];
|
|
if (entry.fileName !== "facts.json" || entry.fileName.startsWith("/") || entry.fileName.includes("..")) {
|
|
throw new Error(`invalid artifact path: ${entry.fileName}`);
|
|
}
|
|
if (isSymlink(entry)) {
|
|
throw new Error("facts artifact must not contain symlinks");
|
|
}
|
|
if (!isRegularOrUnspecified(entry)) {
|
|
throw new Error("facts artifact must contain a regular file");
|
|
}
|
|
if (entry.uncompressedSize <= 0 || entry.uncompressedSize > MAX_FACTS_BYTES) {
|
|
throw new Error(`invalid facts size: ${entry.uncompressedSize}`);
|
|
}
|
|
if (entry.compressedSize > 0 && entry.uncompressedSize / entry.compressedSize > MAX_COMPRESSION_RATIO) {
|
|
throw new Error("facts artifact compression ratio is too high");
|
|
}
|
|
return entry;
|
|
}
|
|
|
|
function readZipEntries(zipPath) {
|
|
return readZipEntriesFromBuffer(fs.readFileSync(zipPath));
|
|
}
|
|
|
|
function readZipEntriesFromBuffer(buf) {
|
|
const eocdOffset = findEndOfCentralDirectory(buf);
|
|
requireBufferRange(buf, eocdOffset, 22, "zip end of central directory");
|
|
const entriesTotal = buf.readUInt16LE(eocdOffset + 10);
|
|
const centralDirectorySize = buf.readUInt32LE(eocdOffset + 12);
|
|
const centralDirectoryOffset = buf.readUInt32LE(eocdOffset + 16);
|
|
requireBufferRange(buf, centralDirectoryOffset, centralDirectorySize, "zip central directory");
|
|
if (centralDirectoryOffset + centralDirectorySize > eocdOffset) {
|
|
throw new Error("zip central directory overlaps end of central directory");
|
|
}
|
|
const entries = [];
|
|
let offset = centralDirectoryOffset;
|
|
for (let i = 0; i < entriesTotal; i++) {
|
|
requireBufferRange(buf, offset, 46, "zip central directory entry");
|
|
if (buf.readUInt32LE(offset) !== 0x02014b50) {
|
|
throw new Error("invalid zip central directory");
|
|
}
|
|
const compressionMethod = buf.readUInt16LE(offset + 10);
|
|
const compressedSize = buf.readUInt32LE(offset + 20);
|
|
const uncompressedSize = buf.readUInt32LE(offset + 24);
|
|
const fileNameLength = buf.readUInt16LE(offset + 28);
|
|
const extraLength = buf.readUInt16LE(offset + 30);
|
|
const commentLength = buf.readUInt16LE(offset + 32);
|
|
const externalFileAttributes = buf.readUInt32LE(offset + 38);
|
|
const localHeaderOffset = buf.readUInt32LE(offset + 42);
|
|
const fileNameStart = offset + 46;
|
|
requireBufferRange(buf, fileNameStart, fileNameLength + extraLength + commentLength, "zip central directory name");
|
|
const fileName = buf.toString("utf8", fileNameStart, fileNameStart + fileNameLength);
|
|
entries.push({
|
|
fileName,
|
|
externalFileAttributes,
|
|
uncompressedSize,
|
|
compressedSize,
|
|
compressionMethod,
|
|
localHeaderOffset,
|
|
});
|
|
offset = fileNameStart + fileNameLength + extraLength + commentLength;
|
|
}
|
|
if (offset > centralDirectoryOffset + centralDirectorySize) {
|
|
throw new Error("zip central directory entry exceeds declared size");
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
function findEndOfCentralDirectory(buf) {
|
|
if (buf.length < 22) {
|
|
throw new Error("zip end of central directory not found");
|
|
}
|
|
const minOffset = Math.max(0, buf.length - 0xffff - 22);
|
|
for (let offset = buf.length - 22; offset >= minOffset; offset--) {
|
|
if (buf.readUInt32LE(offset) === 0x06054b50) {
|
|
return offset;
|
|
}
|
|
}
|
|
throw new Error("zip end of central directory not found");
|
|
}
|
|
|
|
function requireBufferRange(buf, offset, length, label) {
|
|
if (!Number.isInteger(offset) || !Number.isInteger(length) || offset < 0 || length < 0 || offset + length > buf.length) {
|
|
throw new Error(`${label} is outside artifact bounds`);
|
|
}
|
|
}
|
|
|
|
function extractEntryFromBuffer(buf, entry) {
|
|
const offset = entry.localHeaderOffset;
|
|
requireBufferRange(buf, offset, 30, "zip local file header");
|
|
if (buf.readUInt32LE(offset) !== 0x04034b50) {
|
|
throw new Error("invalid zip local file header");
|
|
}
|
|
const compressionMethod = buf.readUInt16LE(offset + 8);
|
|
const fileNameLength = buf.readUInt16LE(offset + 26);
|
|
const extraLength = buf.readUInt16LE(offset + 28);
|
|
const dataStart = offset + 30 + fileNameLength + extraLength;
|
|
requireBufferRange(buf, offset + 30, fileNameLength + extraLength, "zip local file name");
|
|
requireBufferRange(buf, dataStart, entry.compressedSize, "zip local file data");
|
|
const compressed = buf.subarray(dataStart, dataStart + entry.compressedSize);
|
|
let out;
|
|
if (compressionMethod === 0) {
|
|
out = Buffer.from(compressed);
|
|
} else if (compressionMethod === 8) {
|
|
out = zlib.inflateRawSync(compressed, { maxOutputLength: MAX_FACTS_BYTES });
|
|
} else {
|
|
throw new Error(`unsupported zip compression method: ${compressionMethod}`);
|
|
}
|
|
if (out.length !== entry.uncompressedSize) {
|
|
throw new Error(`facts size mismatch: ${out.length} != ${entry.uncompressedSize}`);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function verifyArtifactDigest(buf, expectedDigest) {
|
|
if (!expectedDigest) {
|
|
throw new Error("artifact digest is required");
|
|
}
|
|
const match = /^sha256:([a-f0-9]{64})$/i.exec(expectedDigest);
|
|
if (!match) {
|
|
throw new Error(`unsupported artifact digest: ${expectedDigest}`);
|
|
}
|
|
const got = crypto.createHash("sha256").update(buf).digest("hex");
|
|
if (got.toLowerCase() !== match[1].toLowerCase()) {
|
|
throw new Error("facts artifact digest mismatch");
|
|
}
|
|
}
|
|
|
|
function requireArray(facts, key) {
|
|
if (!(key in facts)) {
|
|
return [];
|
|
}
|
|
if (!Array.isArray(facts[key])) {
|
|
throw new Error(`facts JSON ${key} must be an array`);
|
|
}
|
|
if (facts[key].length > MAX_ARRAY_ITEMS) {
|
|
throw new Error(`facts JSON ${key} has too many items`);
|
|
}
|
|
return facts[key];
|
|
}
|
|
|
|
function requireObject(value, path) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
throw new Error(`facts JSON ${path} must be an object`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function requireString(value, path, { optional = false } = {}) {
|
|
if (value === undefined || value === null) {
|
|
if (optional) {
|
|
return "";
|
|
}
|
|
throw new Error(`facts JSON ${path} must be a string`);
|
|
}
|
|
if (typeof value !== "string") {
|
|
throw new Error(`facts JSON ${path} must be a string`);
|
|
}
|
|
if (Buffer.byteLength(value, "utf8") > MAX_STRING_BYTES) {
|
|
throw new Error(`facts JSON ${path} is too long`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function requireBoolean(value, path, { optional = false } = {}) {
|
|
if (value === undefined || value === null) {
|
|
if (optional) {
|
|
return false;
|
|
}
|
|
throw new Error(`facts JSON ${path} must be a boolean`);
|
|
}
|
|
if (typeof value !== "boolean") {
|
|
throw new Error(`facts JSON ${path} must be a boolean`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function requireInteger(value, path, { optional = false, min = 0, max = 1000000 } = {}) {
|
|
if (value === undefined || value === null) {
|
|
if (optional) {
|
|
return 0;
|
|
}
|
|
throw new Error(`facts JSON ${path} must be an integer`);
|
|
}
|
|
if (!Number.isInteger(value) || value < min || value > max) {
|
|
throw new Error(`facts JSON ${path} must be an integer between ${min} and ${max}`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function requireLine(value, path) {
|
|
if (!Number.isInteger(value) || value < 0 || value > 1000000) {
|
|
throw new Error(`facts JSON ${path} must be a valid line number`);
|
|
}
|
|
}
|
|
|
|
function requireSafePath(value, path) {
|
|
const file = requireString(value, path);
|
|
if (file.startsWith("/") || file.includes("..") || file.includes("\0")) {
|
|
throw new Error(`facts JSON ${path} must be a repository-relative path`);
|
|
}
|
|
return file;
|
|
}
|
|
|
|
function requireStringArray(value, path, { optional = false } = {}) {
|
|
if (value === undefined || value === null) {
|
|
if (optional) {
|
|
return [];
|
|
}
|
|
throw new Error(`facts JSON ${path} must be an array`);
|
|
}
|
|
if (!Array.isArray(value)) {
|
|
throw new Error(`facts JSON ${path} must be an array`);
|
|
}
|
|
if (value.length > MAX_ARRAY_ITEMS) {
|
|
throw new Error(`facts JSON ${path} has too many items`);
|
|
}
|
|
for (const [i, item] of value.entries()) {
|
|
requireString(item, `${path}[${i}]`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function requireJSONValue(value, path, depth = 0) {
|
|
if (depth > MAX_JSON_DEPTH) {
|
|
throw new Error(`facts JSON ${path} is too deeply nested`);
|
|
}
|
|
if (value === null || typeof value === "boolean" || typeof value === "number") {
|
|
return;
|
|
}
|
|
if (typeof value === "string") {
|
|
requireString(value, path);
|
|
return;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
if (value.length > MAX_ARRAY_ITEMS) {
|
|
throw new Error(`facts JSON ${path} has too many items`);
|
|
}
|
|
for (const [i, item] of value.entries()) {
|
|
requireJSONValue(item, `${path}[${i}]`, depth + 1);
|
|
}
|
|
return;
|
|
}
|
|
if (typeof value === "object") {
|
|
const entries = Object.entries(value);
|
|
if (entries.length > MAX_OBJECT_KEYS) {
|
|
throw new Error(`facts JSON ${path} has too many keys`);
|
|
}
|
|
for (const [key, item] of entries) {
|
|
requireString(key, `${path} key`);
|
|
requireJSONValue(item, `${path}.${key}`, depth + 1);
|
|
}
|
|
return;
|
|
}
|
|
throw new Error(`facts JSON ${path} must be a JSON value`);
|
|
}
|
|
|
|
function requireStringArrayMap(value, path, { optional = false } = {}) {
|
|
if (value === undefined || value === null) {
|
|
if (optional) {
|
|
return;
|
|
}
|
|
throw new Error(`facts JSON ${path} must be an object`);
|
|
}
|
|
const obj = requireObject(value, path);
|
|
const entries = Object.entries(obj);
|
|
if (entries.length > MAX_OBJECT_KEYS) {
|
|
throw new Error(`facts JSON ${path} has too many keys`);
|
|
}
|
|
for (const [key, values] of entries) {
|
|
requireString(key, `${path} key`);
|
|
requireStringArray(values, `${path}.${key}`);
|
|
}
|
|
}
|
|
|
|
function verifyDryRunRequest(value, path) {
|
|
if (value === undefined || value === null) {
|
|
return;
|
|
}
|
|
const item = requireObject(value, path);
|
|
requireString(item.method, `${path}.method`);
|
|
requireString(item.url, `${path}.url`);
|
|
requireStringArrayMap(item.query, `${path}.query`, { optional: true });
|
|
if (item.params !== undefined && item.params !== null) {
|
|
requireObject(item.params, `${path}.params`);
|
|
requireJSONValue(item.params, `${path}.params`);
|
|
}
|
|
if (item.body !== undefined && item.body !== null) {
|
|
requireJSONValue(item.body, `${path}.body`);
|
|
}
|
|
}
|
|
|
|
function verifyCommandExample(value, path) {
|
|
const item = requireObject(value, path);
|
|
requireString(item.raw, `${path}.raw`);
|
|
requireSafePath(item.source_file, `${path}.source_file`);
|
|
requireLine(item.line, `${path}.line`);
|
|
requireString(item.command_path, `${path}.command_path`, { optional: true });
|
|
requireString(item.domain, `${path}.domain`, { optional: true });
|
|
requireString(item.source, `${path}.source`, { optional: true });
|
|
requireBoolean(item.changed, `${path}.changed`, { optional: true });
|
|
requireBoolean(item.executable, `${path}.executable`, { optional: true });
|
|
requireString(item.skip_reason, `${path}.skip_reason`, { optional: true });
|
|
requireInteger(item.exit_code, `${path}.exit_code`, { optional: true, min: 0, max: 255 });
|
|
requireInteger(item.stdout_bytes, `${path}.stdout_bytes`, { optional: true });
|
|
requireInteger(item.api_call_count, `${path}.api_call_count`, { optional: true });
|
|
verifyDryRunRequest(item.expected_request, `${path}.expected_request`);
|
|
verifyDryRunRequest(item.dry_run, `${path}.dry_run`);
|
|
}
|
|
|
|
function verifyFactsJSON(data) {
|
|
let facts;
|
|
try {
|
|
facts = JSON.parse(data.toString("utf8"));
|
|
} catch (err) {
|
|
throw new Error(`facts JSON is invalid: ${err.message}`);
|
|
}
|
|
if (!facts || typeof facts !== "object" || Array.isArray(facts)) {
|
|
throw new Error("facts JSON must be an object");
|
|
}
|
|
if (facts.schema_version !== 1) {
|
|
throw new Error("facts JSON schema_version must be 1");
|
|
}
|
|
for (const [i, value] of requireArray(facts, "commands").entries()) {
|
|
const item = requireObject(value, `commands[${i}]`);
|
|
requireString(item.path, `commands[${i}].path`);
|
|
requireString(item.canonical_path, `commands[${i}].canonical_path`, { optional: true });
|
|
requireString(item.domain, `commands[${i}].domain`, { optional: true });
|
|
requireBoolean(item.changed, `commands[${i}].changed`, { optional: true });
|
|
requireString(item.source, `commands[${i}].source`);
|
|
requireBoolean(item.generated, `commands[${i}].generated`, { optional: true });
|
|
requireStringArray(item.flags, `commands[${i}].flags`, { optional: true });
|
|
for (const [j, example] of requireArray(item, "examples").entries()) {
|
|
verifyCommandExample(example, `commands[${i}].examples[${j}]`);
|
|
}
|
|
requireBoolean(item.legacy_naming, `commands[${i}].legacy_naming`, { optional: true });
|
|
requireBoolean(item.name_conflicts_existing, `commands[${i}].name_conflicts_existing`, { optional: true });
|
|
requireBoolean(item.flag_alias_conflict, `commands[${i}].flag_alias_conflict`, { optional: true });
|
|
}
|
|
for (const [i, value] of requireArray(facts, "skills").entries()) {
|
|
const item = requireObject(value, `skills[${i}]`);
|
|
requireSafePath(item.source_file, `skills[${i}].source_file`);
|
|
requireLine(item.line, `skills[${i}].line`);
|
|
requireString(item.raw, `skills[${i}].raw`);
|
|
requireString(item.command_path, `skills[${i}].command_path`, { optional: true });
|
|
requireString(item.domain, `skills[${i}].domain`, { optional: true });
|
|
requireBoolean(item.changed, `skills[${i}].changed`, { optional: true });
|
|
requireString(item.source, `skills[${i}].source`, { optional: true });
|
|
requireBoolean(item.references_invalid_command, `skills[${i}].references_invalid_command`, { optional: true });
|
|
requireBoolean(item.destructive_without_guard, `skills[${i}].destructive_without_guard`, { optional: true });
|
|
requireBoolean(item.scope_conflict, `skills[${i}].scope_conflict`, { optional: true });
|
|
}
|
|
for (const [i, value] of requireArray(facts, "skill_quality").entries()) {
|
|
const item = requireObject(value, `skill_quality[${i}]`);
|
|
requireSafePath(item.source_file, `skill_quality[${i}].source_file`);
|
|
requireString(item.domain, `skill_quality[${i}].domain`, { optional: true });
|
|
requireBoolean(item.changed, `skill_quality[${i}].changed`, { optional: true });
|
|
requireInteger(item.word_count, `skill_quality[${i}].word_count`, { optional: true });
|
|
requireInteger(item.critical_count, `skill_quality[${i}].critical_count`, { optional: true });
|
|
requireInteger(item.description_length, `skill_quality[${i}].description_length`, { optional: true });
|
|
requireBoolean(item.critical_over_budget, `skill_quality[${i}].critical_over_budget`, { optional: true });
|
|
}
|
|
for (const [i, value] of requireArray(facts, "errors").entries()) {
|
|
const item = requireObject(value, `errors[${i}]`);
|
|
requireSafePath(item.file, `errors[${i}].file`);
|
|
requireLine(item.line, `errors[${i}].line`);
|
|
requireString(item.command, `errors[${i}].command`, { optional: true });
|
|
requireString(item.command_path, `errors[${i}].command_path`, { optional: true });
|
|
requireString(item.domain, `errors[${i}].domain`, { optional: true });
|
|
requireBoolean(item.changed, `errors[${i}].changed`, { optional: true });
|
|
requireString(item.source, `errors[${i}].source`, { optional: true });
|
|
requireBoolean(item.boundary, `errors[${i}].boundary`, { optional: true });
|
|
requireBoolean(item.uses_structured_error, `errors[${i}].uses_structured_error`, { optional: true });
|
|
requireBoolean(item.has_hint, `errors[${i}].has_hint`, { optional: true });
|
|
requireInteger(item.hint_action_count, `errors[${i}].hint_action_count`, { optional: true });
|
|
requireBoolean(item.required_hint, `errors[${i}].required_hint`, { optional: true });
|
|
requireString(item.code, `errors[${i}].code`, { optional: true });
|
|
requireString(item.message, `errors[${i}].message`, { optional: true });
|
|
requireString(item.hint, `errors[${i}].hint`, { optional: true });
|
|
requireBoolean(item.retryable, `errors[${i}].retryable`, { optional: true });
|
|
}
|
|
for (const [i, value] of requireArray(facts, "outputs").entries()) {
|
|
const item = requireObject(value, `outputs[${i}]`);
|
|
requireString(item.command, `outputs[${i}].command`);
|
|
requireString(item.domain, `outputs[${i}].domain`, { optional: true });
|
|
requireBoolean(item.changed, `outputs[${i}].changed`, { optional: true });
|
|
requireString(item.source, `outputs[${i}].source`, { optional: true });
|
|
requireStringArray(item.fields, `outputs[${i}].fields`, { optional: true });
|
|
requireBoolean(item.is_list, `outputs[${i}].is_list`, { optional: true });
|
|
requireBoolean(item.has_default_limit, `outputs[${i}].has_default_limit`, { optional: true });
|
|
requireBoolean(item.has_field_selector, `outputs[${i}].has_field_selector`, { optional: true });
|
|
requireBoolean(item.has_decision_field, `outputs[${i}].has_decision_field`, { optional: true });
|
|
}
|
|
for (const [i, value] of requireArray(facts, "examples").entries()) {
|
|
verifyCommandExample(value, `examples[${i}]`);
|
|
}
|
|
for (const [i, value] of requireArray(facts, "diagnostics").entries()) {
|
|
const item = requireObject(value, `diagnostics[${i}]`);
|
|
requireString(item.rule, `diagnostics[${i}].rule`);
|
|
const action = requireString(item.action, `diagnostics[${i}].action`);
|
|
if (!VALID_ACTIONS.has(action)) {
|
|
throw new Error(`facts JSON diagnostics[${i}].action is invalid`);
|
|
}
|
|
requireSafePath(item.file, `diagnostics[${i}].file`);
|
|
requireLine(item.line, `diagnostics[${i}].line`);
|
|
requireString(item.message, `diagnostics[${i}].message`);
|
|
requireString(item.suggestion, `diagnostics[${i}].suggestion`, { optional: true });
|
|
requireString(item.subject_type, `diagnostics[${i}].subject_type`, { optional: true });
|
|
requireString(item.command_path, `diagnostics[${i}].command_path`, { optional: true });
|
|
requireString(item.flag_name, `diagnostics[${i}].flag_name`, { optional: true });
|
|
}
|
|
}
|
|
|
|
function writeVerifiedFacts(zipPath, outPath, expectedDigest = "") {
|
|
const buf = fs.readFileSync(zipPath);
|
|
verifyArtifactDigest(buf, expectedDigest);
|
|
const entry = verifyZipEntries(readZipEntriesFromBuffer(buf));
|
|
const data = extractEntryFromBuffer(buf, entry);
|
|
verifyFactsJSON(data);
|
|
fs.writeFileSync(outPath, data);
|
|
return entry;
|
|
}
|
|
|
|
function verificationFailureDecision(message, blockMode) {
|
|
return {
|
|
block_mode: blockMode,
|
|
degraded: true,
|
|
infrastructure_failure: true,
|
|
system_warnings: [{
|
|
severity: "critical",
|
|
message: `quality-gate facts artifact verification failed: ${message}`,
|
|
suggested_action: "inspect the semantic-review workflow artifact verification logs and rerun CI after the artifact issue is resolved",
|
|
}],
|
|
blockers: [],
|
|
warnings: [],
|
|
};
|
|
}
|
|
|
|
function writeFailureDecisionFromEnv(err) {
|
|
const decisionOut = process.env.SEMANTIC_REVIEW_DECISION_OUT || "";
|
|
if (!decisionOut) {
|
|
return;
|
|
}
|
|
const blockMode = process.env.SEMANTIC_REVIEW_BLOCK === "true";
|
|
const message = err && err.message ? err.message : String(err || "unknown error");
|
|
const decision = verificationFailureDecision(message, blockMode);
|
|
fs.writeFileSync(decisionOut, JSON.stringify(decision, null, 2) + "\n", "utf8");
|
|
const markdownOut = process.env.SEMANTIC_REVIEW_MARKDOWN_OUT || "";
|
|
if (markdownOut) {
|
|
fs.writeFileSync(markdownOut, [
|
|
"## Semantic Review",
|
|
"",
|
|
"The semantic review system could not produce a fully trusted result.",
|
|
"",
|
|
`- ${decision.system_warnings[0].message}`,
|
|
`- Action: ${decision.system_warnings[0].suggested_action}`,
|
|
"",
|
|
].join("\n"), "utf8");
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const [zipPath, outPath = "facts.json", expectedDigest = ""] = process.argv.slice(2);
|
|
if (!zipPath) {
|
|
console.error("usage: node scripts/semantic-review-verify-artifact.js <artifact.zip> [facts.json] [sha256:<digest>]");
|
|
process.exit(2);
|
|
}
|
|
try {
|
|
writeVerifiedFacts(zipPath, outPath, expectedDigest);
|
|
} catch (err) {
|
|
console.error(`semantic-review artifact verifier: ${err.message}`);
|
|
try {
|
|
writeFailureDecisionFromEnv(err);
|
|
} catch (writeErr) {
|
|
console.error(`semantic-review artifact verifier: failed to write infrastructure decision: ${writeErr.message}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
module.exports = { MAX_FACTS_BYTES, verifyArtifactDigest, verifyZipEntries, verifyFactsJSON, readZipEntries, extractEntryFromBuffer, writeVerifiedFacts, verificationFailureDecision };
|