Files
jhonny-editor/code/scripts/uxp-hybrid-ccx-receipt-core.mjs
João Henrique b541f502ba feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto
- Configurado MCP server para Premiere Pro
- Adicionado documentação e skills
- Configurado Gitignore para o projeto
2026-09-08 09:59:31 -04:00

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;
}