- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
296 lines
14 KiB
JavaScript
Executable File
296 lines
14 KiB
JavaScript
Executable File
import { createHash } from "node:crypto";
|
|
import {
|
|
hashZipEntry,
|
|
inspectZipArchive,
|
|
readZipEntry,
|
|
sha256File,
|
|
} from "./read-zip-archive.mjs";
|
|
import {
|
|
UXP_HYBRID_ADDON_ENTRYPOINT_PATH,
|
|
UXP_HYBRID_ADDON_RECEIPT_SCHEMA_VERSION,
|
|
UXP_HYBRID_ADDON_TARGETS,
|
|
compareUxpHybridPaths,
|
|
} from "./uxp-hybrid-addon-receipt-contract.mjs";
|
|
import {
|
|
UXP_HYBRID_CCX_AUTHORITY_URL,
|
|
UXP_HYBRID_CCX_RECEIPT_LEGACY_SCHEMA_VERSION,
|
|
UXP_HYBRID_CCX_RECEIPT_LEGACY_SEMANTICS,
|
|
UXP_HYBRID_CCX_RECEIPT_SCHEMA_VERSION,
|
|
UXP_HYBRID_CCX_RECEIPT_SEMANTICS,
|
|
compareUxpHybridCcxPaths,
|
|
} from "./uxp-hybrid-ccx-receipt-contract.mjs";
|
|
import {
|
|
canonicalNativeSdkHeaderInventorySha256,
|
|
verifyNativeSdkHeaderInventory,
|
|
} from "./verify-native-sdk-header-inventory.mjs";
|
|
import {
|
|
canonicalUxpHybridAddonReceiptSha256,
|
|
verifyUxpHybridAddonReceipt,
|
|
} from "./verify-uxp-hybrid-addon-receipt.mjs";
|
|
|
|
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
const MAX_ARCHIVE_BYTES = 0xffff_ffff;
|
|
const MAX_MANIFEST_BYTES = 1024 * 1024;
|
|
const MAX_ENTRYPOINT_BYTES = 16 * 1024 * 1024;
|
|
const MAX_ARTIFACT_BYTES = 2 ** 31;
|
|
const MAX_MANIFEST_ID_LENGTH = 512;
|
|
|
|
function receiptError(message) {
|
|
const error = new Error(message);
|
|
error.code = "UXP_HYBRID_CCX_RECEIPT_INVALID";
|
|
return error;
|
|
}
|
|
|
|
function record(value, label, expectedKeys) {
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw receiptError(`${label} must be an object`);
|
|
const keys = Object.keys(value).sort(compareUxpHybridCcxPaths);
|
|
const expected = [...expectedKeys].sort(compareUxpHybridCcxPaths);
|
|
if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index])) {
|
|
throw receiptError(`${label} must contain only the documented receipt fields`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function nonEmptyString(value, label, maximum = MAX_MANIFEST_ID_LENGTH) {
|
|
if (typeof value !== "string" || !value.trim() || value.length > maximum || value.includes("\0")) {
|
|
throw receiptError(`${label} must be a non-empty string of at most ${maximum} characters`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function sha256(value, label) {
|
|
if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
|
|
throw receiptError(`${label} must be a lowercase SHA-256 hex digest`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function positiveInteger(value, label, maximum) {
|
|
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) {
|
|
throw receiptError(`${label} must be a positive safe integer no larger than ${maximum}`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function nonNegativeInteger(value, label, maximum) {
|
|
if (!Number.isSafeInteger(value) || value < 0 || value > maximum) {
|
|
throw receiptError(`${label} must be a non-negative safe integer no larger than ${maximum}`);
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function addonName(value, label = "manifest.addonName") {
|
|
const name = nonEmptyString(value, label, 128);
|
|
if (!/^[A-Za-z0-9._-]+\.uxpaddon$/.test(name)) throw receiptError(`${label} must be a simple .uxpaddon filename`);
|
|
return name;
|
|
}
|
|
|
|
function validateManifestFacts(value, label, includeArchiveIdentity) {
|
|
const fields = includeArchiveIdentity
|
|
? ["bytes", "sha256", "idSha256", "idLength", "manifestVersion", "hostApp", "hostMinVersion", "addonName", "enableAddon"]
|
|
: ["manifestVersion", "hostApp", "hostMinVersion", "addonName", "enableAddon"];
|
|
const manifest = record(value, label, fields);
|
|
if (includeArchiveIdentity) {
|
|
positiveInteger(manifest.bytes, `${label}.bytes`, MAX_MANIFEST_BYTES);
|
|
sha256(manifest.sha256, `${label}.sha256`);
|
|
sha256(manifest.idSha256, `${label}.idSha256`);
|
|
positiveInteger(manifest.idLength, `${label}.idLength`, MAX_MANIFEST_ID_LENGTH);
|
|
}
|
|
if (!Number.isInteger(manifest.manifestVersion) || manifest.manifestVersion < 6) {
|
|
throw receiptError(`${label}.manifestVersion must be 6 or newer`);
|
|
}
|
|
if (manifest.hostApp !== "premierepro") throw receiptError(`${label}.hostApp must be premierepro`);
|
|
if (!/^\d+\.\d+\.\d+$/.test(String(manifest.hostMinVersion || ""))) {
|
|
throw receiptError(`${label}.hostMinVersion must be a semantic version`);
|
|
}
|
|
const name = addonName(manifest.addonName, `${label}.addonName`);
|
|
if (manifest.enableAddon !== true) throw receiptError(`${label}.enableAddon must be true`);
|
|
return Object.freeze({
|
|
manifestVersion: manifest.manifestVersion,
|
|
hostApp: manifest.hostApp,
|
|
hostMinVersion: manifest.hostMinVersion,
|
|
addonName: name,
|
|
enableAddon: true,
|
|
});
|
|
}
|
|
|
|
function sameManifestFacts(left, right, label) {
|
|
for (const key of ["manifestVersion", "hostApp", "hostMinVersion", "addonName", "enableAddon"]) {
|
|
if (left[key] !== right[key]) throw receiptError(`${label}.${key} must match the addon-layout receipt`);
|
|
}
|
|
}
|
|
|
|
function verifyCurrentAddonReceipt(addonReceipt, sdkHeaderReceipt) {
|
|
if (!addonReceipt || addonReceipt.schemaVersion !== UXP_HYBRID_ADDON_RECEIPT_SCHEMA_VERSION) {
|
|
throw receiptError(`addonReceipt must use schemaVersion ${UXP_HYBRID_ADDON_RECEIPT_SCHEMA_VERSION}`);
|
|
}
|
|
const addon = verifyUxpHybridAddonReceipt(addonReceipt, sdkHeaderReceipt === undefined ? {} : { sdkHeaderReceipt });
|
|
if (!("addonBytes" in addon) || addon.entrypoints !== 1) {
|
|
throw receiptError("addonReceipt must contain the current root main.js entrypoint accounting");
|
|
}
|
|
return addon;
|
|
}
|
|
|
|
function canonicalize(value) {
|
|
if (Array.isArray(value)) return value.map(canonicalize);
|
|
if (value && typeof value === "object") {
|
|
return Object.fromEntries(Object.keys(value).sort(compareUxpHybridCcxPaths).map((key) => [key, canonicalize(value[key])]));
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function expectedArchivePaths(addonReceipt) {
|
|
return ["manifest.json", UXP_HYBRID_ADDON_ENTRYPOINT_PATH, ...addonReceipt.artifacts.map((artifact) => artifact.path)];
|
|
}
|
|
|
|
function archiveManifest(buffer) {
|
|
let parsed;
|
|
try { parsed = JSON.parse(buffer.toString("utf8")); } catch { throw receiptError("CCX archive manifest.json must be readable JSON"); }
|
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw receiptError("CCX archive manifest.json must be an object");
|
|
const id = nonEmptyString(parsed.id, "CCX archive manifest.json id");
|
|
if (id.trim() !== id) throw receiptError("CCX archive manifest.json id must not have surrounding whitespace");
|
|
const facts = validateManifestFacts({
|
|
bytes: buffer.length,
|
|
sha256: createHash("sha256").update(buffer).digest("hex"),
|
|
idSha256: createHash("sha256").update(id).digest("hex"),
|
|
idLength: id.length,
|
|
manifestVersion: parsed.manifestVersion,
|
|
hostApp: parsed.host?.app,
|
|
hostMinVersion: parsed.host?.minVersion,
|
|
addonName: parsed.addon?.name,
|
|
enableAddon: parsed.requiredPermissions?.enableAddon,
|
|
}, "manifest", true);
|
|
return Object.freeze({
|
|
bytes: buffer.length,
|
|
sha256: createHash("sha256").update(buffer).digest("hex"),
|
|
idSha256: createHash("sha256").update(id).digest("hex"),
|
|
idLength: id.length,
|
|
...facts,
|
|
});
|
|
}
|
|
|
|
function verifyArchiveReceiptAgainstAddon(receipt, addonReceipt, sdkHeaderReceipt) {
|
|
const addon = verifyCurrentAddonReceipt(addonReceipt, sdkHeaderReceipt);
|
|
const manifest = validateManifestFacts(receipt.manifest, "manifest", true);
|
|
sameManifestFacts(manifest, addonReceipt.manifest, "manifest");
|
|
if (receipt.source.sdkVersion !== addonReceipt.source.sdkVersion) throw receiptError("source.sdkVersion must match the addon-layout receipt");
|
|
if (receipt.source.sdkHeaderReceiptSha256 !== addonReceipt.source.sdkHeaderReceiptSha256) {
|
|
throw receiptError("source.sdkHeaderReceiptSha256 must match the addon-layout receipt");
|
|
}
|
|
if (receipt.source.addonReceiptSha256 !== canonicalUxpHybridAddonReceiptSha256(addonReceipt)) {
|
|
throw receiptError("source.addonReceiptSha256 must match the addon-layout receipt");
|
|
}
|
|
if (receipt.stats.artifacts !== addon.artifacts || receipt.stats.addonBytes !== addon.addonBytes ||
|
|
receipt.stats.entrypoints !== addon.entrypoints || receipt.stats.entrypointBytes !== addon.entrypointBytes) {
|
|
throw receiptError("stats must match the addon-layout receipt");
|
|
}
|
|
return addon;
|
|
}
|
|
|
|
function validateArchiveContents(value) {
|
|
const contents = record(value, "contents", ["entries", "files", "directories", "pathSetSha256"]);
|
|
const entries = positiveInteger(contents.entries, "contents.entries", 100_000);
|
|
const files = nonNegativeInteger(contents.files, "contents.files", entries);
|
|
const directories = nonNegativeInteger(contents.directories, "contents.directories", entries);
|
|
if (files + directories !== entries) throw receiptError("contents file and directory totals must match entries");
|
|
sha256(contents.pathSetSha256, "contents.pathSetSha256");
|
|
return Object.freeze({ entries, files, directories, pathSetSha256: contents.pathSetSha256 });
|
|
}
|
|
|
|
export function verifyUxpHybridCcxReceipt(document, options = {}) {
|
|
const legacy = document?.schemaVersion === UXP_HYBRID_CCX_RECEIPT_LEGACY_SCHEMA_VERSION;
|
|
const receipt = record(document, "receipt", legacy
|
|
? ["schemaVersion", "source", "archive", "manifest", "semantics", "stats"]
|
|
: ["schemaVersion", "source", "archive", "contents", "manifest", "semantics", "stats"]);
|
|
if (!legacy && receipt.schemaVersion !== UXP_HYBRID_CCX_RECEIPT_SCHEMA_VERSION) {
|
|
throw receiptError(`schemaVersion must be ${UXP_HYBRID_CCX_RECEIPT_LEGACY_SCHEMA_VERSION} or ${UXP_HYBRID_CCX_RECEIPT_SCHEMA_VERSION}`);
|
|
}
|
|
const source = record(receipt.source, "source", ["sdk", "sdkVersion", "sdkHeaderReceiptSha256", "addonReceiptSha256", "authorityUrl"]);
|
|
if (source.sdk !== "uxp-hybrid") throw receiptError("source.sdk must be uxp-hybrid");
|
|
nonEmptyString(source.sdkVersion, "source.sdkVersion", 128);
|
|
sha256(source.sdkHeaderReceiptSha256, "source.sdkHeaderReceiptSha256");
|
|
sha256(source.addonReceiptSha256, "source.addonReceiptSha256");
|
|
if (source.authorityUrl !== UXP_HYBRID_CCX_AUTHORITY_URL) {
|
|
throw receiptError("source.authorityUrl must match the documented CCX packaging guide");
|
|
}
|
|
|
|
const archive = record(receipt.archive, "archive", ["format", "bytes", "sha256"]);
|
|
if (archive.format !== "zip") throw receiptError("archive.format must be zip");
|
|
positiveInteger(archive.bytes, "archive.bytes", MAX_ARCHIVE_BYTES);
|
|
sha256(archive.sha256, "archive.sha256");
|
|
|
|
validateManifestFacts(receipt.manifest, "manifest", true);
|
|
|
|
if (!legacy) validateArchiveContents(receipt.contents);
|
|
|
|
const semantics = record(receipt.semantics, "semantics", ["listed", "doesNotEstablish"]);
|
|
const requiredSemantics = legacy ? UXP_HYBRID_CCX_RECEIPT_LEGACY_SEMANTICS : UXP_HYBRID_CCX_RECEIPT_SEMANTICS;
|
|
if (semantics.listed !== requiredSemantics.listed || semantics.doesNotEstablish !== requiredSemantics.doesNotEstablish) {
|
|
throw receiptError("semantics must retain the documented evidence boundary");
|
|
}
|
|
|
|
const stats = record(receipt.stats, "stats", ["artifacts", "addonBytes", "entrypoints", "entrypointBytes"]);
|
|
if (stats.artifacts !== UXP_HYBRID_ADDON_TARGETS.length) throw receiptError(`stats.artifacts must be ${UXP_HYBRID_ADDON_TARGETS.length}`);
|
|
positiveInteger(stats.addonBytes, "stats.addonBytes", MAX_ARTIFACT_BYTES * UXP_HYBRID_ADDON_TARGETS.length);
|
|
if (stats.entrypoints !== 1) throw receiptError("stats.entrypoints must be 1");
|
|
positiveInteger(stats.entrypointBytes, "stats.entrypointBytes", MAX_ENTRYPOINT_BYTES);
|
|
|
|
if (options.addonReceipt !== undefined) verifyArchiveReceiptAgainstAddon(receipt, options.addonReceipt, options.sdkHeaderReceipt);
|
|
return Object.freeze({ artifacts: stats.artifacts, addonBytes: stats.addonBytes, entrypoints: stats.entrypoints, entrypointBytes: stats.entrypointBytes });
|
|
}
|
|
|
|
export function canonicalUxpHybridCcxReceiptSha256(document) {
|
|
verifyUxpHybridCcxReceipt(document);
|
|
return createHash("sha256").update(JSON.stringify(canonicalize(document))).digest("hex");
|
|
}
|
|
|
|
export async function buildUxpHybridCcxReceipt(options) {
|
|
if (!options?.addonReceipt || typeof options.addonReceipt !== "object") throw receiptError("addonReceipt is required");
|
|
if (!options?.sdkHeaderReceipt || typeof options.sdkHeaderReceipt !== "object") throw receiptError("sdkHeaderReceipt is required");
|
|
if (typeof options.ccxPath !== "string" || !options.ccxPath.trim() || options.ccxPath.includes("\0")) {
|
|
throw receiptError("ccxPath must be a non-empty path");
|
|
}
|
|
const addon = verifyCurrentAddonReceipt(options.addonReceipt, options.sdkHeaderReceipt);
|
|
const archive = await inspectZipArchive(options.ccxPath, expectedArchivePaths(options.addonReceipt));
|
|
const manifestEntry = archive.selected.get("manifest.json");
|
|
const entrypointEntry = archive.selected.get(UXP_HYBRID_ADDON_ENTRYPOINT_PATH);
|
|
const manifestContents = await readZipEntry(options.ccxPath, archive.bytes, manifestEntry, MAX_MANIFEST_BYTES);
|
|
const manifest = archiveManifest(manifestContents.buffer);
|
|
sameManifestFacts(manifest, options.addonReceipt.manifest, "manifest");
|
|
|
|
const entrypoint = await hashZipEntry(options.ccxPath, archive.bytes, entrypointEntry, MAX_ENTRYPOINT_BYTES);
|
|
if (entrypoint.bytes !== options.addonReceipt.entrypoint.bytes || entrypoint.sha256 !== options.addonReceipt.entrypoint.sha256) {
|
|
throw receiptError("CCX archive main.js must match the addon-layout receipt");
|
|
}
|
|
for (const artifact of options.addonReceipt.artifacts) {
|
|
const entry = archive.selected.get(artifact.path);
|
|
const observed = await hashZipEntry(options.ccxPath, archive.bytes, entry, MAX_ARTIFACT_BYTES);
|
|
if (observed.bytes !== artifact.bytes || observed.sha256 !== artifact.sha256) {
|
|
throw receiptError(`CCX archive addon artifact ${artifact.target} must match the addon-layout receipt`);
|
|
}
|
|
}
|
|
|
|
const receipt = {
|
|
schemaVersion: UXP_HYBRID_CCX_RECEIPT_SCHEMA_VERSION,
|
|
source: {
|
|
sdk: "uxp-hybrid",
|
|
sdkVersion: options.addonReceipt.source.sdkVersion,
|
|
sdkHeaderReceiptSha256: canonicalNativeSdkHeaderInventorySha256(options.sdkHeaderReceipt),
|
|
addonReceiptSha256: canonicalUxpHybridAddonReceiptSha256(options.addonReceipt),
|
|
authorityUrl: UXP_HYBRID_CCX_AUTHORITY_URL,
|
|
},
|
|
archive: { format: "zip", bytes: archive.bytes, sha256: await sha256File(options.ccxPath) },
|
|
contents: archive.summary,
|
|
manifest,
|
|
semantics: UXP_HYBRID_CCX_RECEIPT_SEMANTICS,
|
|
stats: {
|
|
artifacts: addon.artifacts,
|
|
addonBytes: addon.addonBytes,
|
|
entrypoints: addon.entrypoints,
|
|
entrypointBytes: addon.entrypointBytes,
|
|
},
|
|
};
|
|
verifyUxpHybridCcxReceipt(receipt, { addonReceipt: options.addonReceipt, sdkHeaderReceipt: options.sdkHeaderReceipt });
|
|
return receipt;
|
|
}
|