🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

pdfnative-cli

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

pdfnative-cli - npm Package Compare versions

Comparing version
0.1.0
to
0.2.0
+1185
-85
dist/cli.cjs

@@ -7,2 +7,3 @@ #!/usr/bin/env node

var promises = require('fs/promises');
var crypto = require('crypto');
var module$1 = require('module');

@@ -22,3 +23,9 @@

// src/utils/error.ts
var CliError;
function deprecate(name, replacement) {
if (_deprecateSeen.has(name)) return;
_deprecateSeen.add(name);
process.stderr.write(`warning: --${name} is deprecated; use ${replacement} instead.
`);
}
var CliError, _deprecateSeen;
var init_error = __esm({

@@ -34,2 +41,3 @@ "src/utils/error.ts"() {

};
_deprecateSeen = /* @__PURE__ */ new Set();
}

@@ -43,2 +51,17 @@ });

let i = 0;
const setFlag = (key, value) => {
const existing = flags[key];
if (existing === void 0 || typeof existing === "boolean") {
flags[key] = value;
return;
}
if (typeof value === "boolean") {
return;
}
if (typeof existing === "string") {
flags[key] = [existing, value];
} else {
existing.push(value);
}
};
while (i < argv.length) {

@@ -59,3 +82,3 @@ const token = argv[i];

const value = token.slice(eqIdx + 1);
flags[key] = value;
setFlag(key, value);
} else {

@@ -65,6 +88,6 @@ const key = token.slice(2);

if (next !== void 0 && !next.startsWith("-")) {
flags[key] = next;
setFlag(key, next);
i++;
} else {
flags[key] = true;
setFlag(key, true);
}

@@ -76,6 +99,6 @@ }

if (next !== void 0 && !next.startsWith("-")) {
flags[key] = next;
setFlag(key, next);
i++;
} else {
flags[key] = true;
setFlag(key, true);
}

@@ -92,11 +115,27 @@ } else {

const value = flags[name];
if (value !== void 0) {
if (typeof value !== "string") {
throw new CliError(`Flag --${name} requires a value.`, 2);
}
return value;
if (value === void 0) continue;
if (typeof value === "boolean") {
throw new CliError(`Flag --${name} requires a value.`, 2);
}
if (typeof value === "string") return value;
return value[0];
}
return void 0;
}
function getStringFlagAll(flags, ...names) {
const out = [];
for (const name of names) {
const value = flags[name];
if (value === void 0) continue;
if (typeof value === "boolean") {
throw new CliError(`Flag --${name} requires a value.`, 2);
}
if (typeof value === "string") {
out.push(value);
} else {
out.push(...value);
}
}
return out;
}
function hasFlag(flags, ...names) {

@@ -135,2 +174,7 @@ return names.some((n) => flags[n] !== void 0);

}
async function readBinaryFile(filePath) {
validatePath(filePath);
const buf = await promises.readFile(filePath);
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
}
function assertJsonSizeLimit(buf) {

@@ -192,2 +236,336 @@ if (buf.length > JSON_SIZE_LIMIT) {

});
async function loadLayoutFile(filePath) {
if (filePath === void 0) return {};
validatePath(filePath);
let raw;
try {
raw = await promises.readFile(filePath, "utf8");
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
throw new CliError(`Failed to read --layout file: ${msg}`, 1);
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
throw new CliError(`Failed to parse --layout JSON: ${msg}`, 1);
}
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
throw new CliError("--layout file must contain a JSON object.", 1);
}
const obj = parsed;
if (Array.isArray(obj.attachments)) {
obj.attachments = obj.attachments.map((a) => {
if (typeof a !== "object" || a === null) return a;
const rest = { ...a };
delete rest.data;
return rest;
});
}
return obj;
}
function parsePageSizePair(value) {
const m = /^(\d+(?:\.\d+)?)x(\d+(?:\.\d+)?)$/i.exec(value.trim());
if (m === null) return null;
return [Number.parseFloat(m[1]), Number.parseFloat(m[2])];
}
function parsePageSize(value) {
const lower = value.toLowerCase();
const named = NAMED_PAGE_SIZES[lower];
if (named !== void 0) {
return { pageWidth: named[0], pageHeight: named[1] };
}
const pair = parsePageSizePair(value);
if (pair !== null) {
return { pageWidth: pair[0], pageHeight: pair[1] };
}
const valid = Object.keys(NAMED_PAGE_SIZES).join(", ");
throw new CliError(
`Invalid --page-size value "${value}". Expected one of: ${valid}, or WxH (points).`,
2
);
}
function parseMargin(value) {
const parts = value.split(",").map((p) => p.trim());
const nums = parts.map((p) => {
const n = Number.parseFloat(p);
if (!Number.isFinite(n) || n < 0) {
throw new CliError(`Invalid --margin value "${value}".`, 2);
}
return n;
});
if (nums.length === 1) {
const v = nums[0];
return { t: v, r: v, b: v, l: v };
}
if (nums.length === 4) {
return {
t: nums[0],
r: nums[1],
b: nums[2],
l: nums[3]
};
}
throw new CliError(`Invalid --margin "${value}". Expected N or T,R,B,L.`, 2);
}
function parseTagged(value) {
const v = value.toLowerCase();
if (v === "none") return false;
if (VALID_TAGGED.includes(v)) {
return v;
}
throw new CliError(
`Invalid --tagged value "${value}". Valid: ${VALID_TAGGED.join(", ")}.`,
2
);
}
function conformanceToTagged(value) {
const validShort = /* @__PURE__ */ new Set(["1b", "2b", "3b"]);
if (!validShort.has(value)) {
throw new CliError(
`Invalid --conformance value "${value}". Valid: 1b, 2b, 3b.`,
2
);
}
return "pdfa" + value;
}
function buildPageTemplate(parts) {
if (parts.left === void 0 && parts.center === void 0 && parts.right === void 0) {
return void 0;
}
const tpl = {};
if (parts.left !== void 0) tpl.left = parts.left;
if (parts.center !== void 0) tpl.center = parts.center;
if (parts.right !== void 0) tpl.right = parts.right;
return tpl;
}
function buildEncryptionFromFlags(args) {
const ownerFlag = getStringFlag(args.flags, "encrypt-owner-pass");
const userFlag = getStringFlag(args.flags, "encrypt-user-pass");
const algoFlag = getStringFlag(args.flags, "encrypt-algorithm");
const permsFlag = getStringFlag(args.flags, "encrypt-permissions");
const owner = process.env.PDFNATIVE_ENCRYPT_OWNER_PASS ?? ownerFlag;
const user = process.env.PDFNATIVE_ENCRYPT_USER_PASS ?? userFlag;
if (owner === void 0 && user === void 0 && algoFlag === void 0 && permsFlag === void 0) {
return void 0;
}
if (owner === void 0 || owner.length === 0) {
throw new CliError(
"Encryption requires an owner password. Provide --encrypt-owner-pass <pass> or $PDFNATIVE_ENCRYPT_OWNER_PASS.",
2
);
}
const algo = algoFlag ?? "aes128";
if (!VALID_ENCRYPTION_ALGOS.has(algo)) {
throw new CliError(
`Invalid --encrypt-algorithm "${algo}". Valid: aes128, aes256.`,
2
);
}
const opts = {
ownerPassword: owner,
algorithm: algo
};
if (user !== void 0) opts.userPassword = user;
if (permsFlag !== void 0) {
const perms = {};
for (const raw of permsFlag.split(",")) {
const p = raw.trim();
if (p.length === 0) continue;
const lower = p.toLowerCase();
if (!VALID_PERMISSIONS.has(p) && !VALID_PERMISSIONS.has(lower)) {
throw new CliError(
`Invalid permission "${p}" in --encrypt-permissions. Valid: print, copy, modify, extractText.`,
2
);
}
const key = lower === "extracttext" || p === "extractText" ? "extractText" : lower;
perms[key] = true;
}
opts.permissions = perms;
}
return opts;
}
async function buildWatermarkFromFlags(args) {
const text = getStringFlag(args.flags, "watermark-text");
const opacity = getStringFlag(args.flags, "watermark-opacity");
const angle = getStringFlag(args.flags, "watermark-angle");
const color = getStringFlag(args.flags, "watermark-color");
const fontSize = getStringFlag(args.flags, "watermark-font-size");
const imagePath = getStringFlag(args.flags, "watermark-image");
const position = getStringFlag(args.flags, "watermark-position");
if (text === void 0 && imagePath === void 0 && opacity === void 0 && angle === void 0 && color === void 0 && fontSize === void 0 && position === void 0) {
return void 0;
}
const wm = {};
if (text !== void 0) {
const t = { text };
if (opacity !== void 0) t.opacity = parseUnit(opacity, "watermark-opacity", 0, 1);
if (angle !== void 0) t.angle = parseFloatFlag(angle, "watermark-angle");
if (color !== void 0) t.color = color;
if (fontSize !== void 0) t.fontSize = parseFloatFlag(fontSize, "watermark-font-size");
wm.text = t;
}
if (imagePath !== void 0) {
const data = await readBinaryFile(imagePath);
const img = { data };
if (opacity !== void 0 && text === void 0) {
img.opacity = parseUnit(opacity, "watermark-opacity", 0, 1);
}
wm.image = img;
}
if (position !== void 0) {
if (position !== "background" && position !== "foreground") {
throw new CliError(
`Invalid --watermark-position "${position}". Valid: background, foreground.`,
2
);
}
wm.position = position;
}
return wm;
}
function parseFloatFlag(value, flag) {
const n = Number.parseFloat(value);
if (!Number.isFinite(n)) {
throw new CliError(`Invalid --${flag} value "${value}".`, 2);
}
return n;
}
function parseUnit(value, flag, min, max) {
const n = parseFloatFlag(value, flag);
if (n < min || n > max) {
throw new CliError(`--${flag} must be between ${min} and ${max} (got ${value}).`, 2);
}
return n;
}
async function loadAttachmentsFromFlags(args) {
const paths = getStringFlagAll(args.flags, "attachment");
if (paths.length === 0) return void 0;
const out = [];
for (const raw of paths) {
const parts = raw.split(":");
let pathPart = parts[0] ?? "";
let offset = 1;
if (pathPart.length === 1 && /^[A-Za-z]$/.test(pathPart) && parts.length > 1) {
pathPart = `${pathPart}:${parts[1] ?? ""}`;
offset = 2;
}
if (pathPart.length === 0) {
throw new CliError(`Invalid --attachment value "${raw}".`, 2);
}
const mimePart = parts[offset];
const relPart = parts[offset + 1];
const descPart = parts[offset + 2];
const data = await readBinaryFile(pathPart);
const filename = pathPart.split(/[/\\]/).pop() ?? "attachment";
const mime = mimePart && mimePart.length > 0 ? mimePart : "application/octet-stream";
const att = {
filename,
data,
mimeType: mime
};
if (relPart !== void 0 && relPart.length > 0) {
att.relationship = relPart;
}
if (descPart !== void 0 && descPart.length > 0) {
att.description = descPart;
}
out.push(att);
}
return out;
}
async function buildLayoutOptions(args) {
const layoutPath = getStringFlag(args.flags, "layout");
const fromFile = await loadLayoutFile(layoutPath);
const out = { ...fromFile };
const pageSize = getStringFlag(args.flags, "page-size");
if (pageSize !== void 0) {
const { pageWidth, pageHeight } = parsePageSize(pageSize);
out.pageWidth = pageWidth;
out.pageHeight = pageHeight;
}
const margin = getStringFlag(args.flags, "margin");
if (margin !== void 0) {
out.margins = parseMargin(margin);
}
if (hasFlag(args.flags, "compress")) {
out.compress = true;
}
const tagged = getStringFlag(args.flags, "tagged");
const conformance = getStringFlag(args.flags, "conformance");
if (tagged !== void 0 && conformance !== void 0) {
throw new CliError(
"Use either --tagged or --conformance, not both. Prefer --tagged.",
2
);
}
if (tagged !== void 0) {
out.tagged = parseTagged(tagged);
} else if (conformance !== void 0) {
deprecate("conformance", "--tagged pdfa<level>");
out.tagged = conformanceToTagged(conformance);
}
const header = buildPageTemplate({
left: getStringFlag(args.flags, "header-left"),
center: getStringFlag(args.flags, "header-center"),
right: getStringFlag(args.flags, "header-right")
});
if (header !== void 0) out.headerTemplate = header;
const footer = buildPageTemplate({
left: getStringFlag(args.flags, "footer-left"),
center: getStringFlag(args.flags, "footer-center"),
right: getStringFlag(args.flags, "footer-right")
});
if (footer !== void 0) out.footerTemplate = footer;
const watermark = await buildWatermarkFromFlags(args);
if (watermark !== void 0) out.watermark = watermark;
const encryption = buildEncryptionFromFlags(args);
if (encryption !== void 0) out.encryption = encryption;
const attachments = await loadAttachmentsFromFlags(args);
if (attachments !== void 0) out.attachments = attachments;
const tg = out.tagged;
if (encryption !== void 0 && tg !== void 0 && tg !== false) {
throw new CliError(
"Encryption is mutually exclusive with --tagged (PDF/A forbids encryption per ISO 19005-1 \xA76.3.2).",
2
);
}
return out;
}
function assertStreamingCompatible(layout) {
const check = (tpl, label) => {
if (tpl === void 0) return;
for (const part of [tpl.left, tpl.center, tpl.right]) {
if (part !== void 0 && part.includes("{pages}")) {
throw new CliError(
`--stream is incompatible with the {pages} placeholder in --${label}-* (total page count not known until full render).`,
2
);
}
}
};
check(layout.headerTemplate, "header");
check(layout.footerTemplate, "footer");
}
var VALID_TAGGED, NAMED_PAGE_SIZES, VALID_ENCRYPTION_ALGOS, VALID_PERMISSIONS;
var init_layout = __esm({
"src/utils/layout.ts"() {
init_args();
init_io();
init_error();
VALID_TAGGED = ["none", "pdfa1b", "pdfa2b", "pdfa2u", "pdfa3b"];
NAMED_PAGE_SIZES = {
a4: [595.28, 841.89],
letter: [612, 792],
legal: [612, 1008],
a3: [841.89, 1190.55],
tabloid: [792, 1224],
a5: [419.53, 595.28]
};
VALID_ENCRYPTION_ALGOS = /* @__PURE__ */ new Set(["aes128", "aes256"]);
VALID_PERMISSIONS = /* @__PURE__ */ new Set(["print", "copy", "modify", "extractText", "extracttext"]);
}
});

@@ -199,2 +577,39 @@ // src/commands/render.ts

});
function isPdfParamsLike(value) {
if (typeof value !== "object" || value === null) return false;
const v = value;
return typeof v.title === "string" && Array.isArray(v.headers) && Array.isArray(v.rows);
}
function isDocumentParamsLike(value) {
if (typeof value !== "object" || value === null) return false;
const v = value;
return Array.isArray(v.blocks);
}
function hasTocBlock(params) {
for (const b of params.blocks) {
const block = b;
if (block.type === "toc") return true;
}
return false;
}
async function buildFontEntriesForLangs(langs) {
if (langs.length === 0) return [];
const entries = [];
let nextRef = 3;
for (const lang of langs) {
if (!pdfnative.hasFontLoader(lang)) {
throw new CliError(
`--lang "${lang}" is not a bundled pdfnative font. Register a loader programmatically before invoking the CLI to use a custom font.`,
2
);
}
const fontData = await pdfnative.loadFontData(lang);
if (fontData === null) {
throw new CliError(`Failed to load font data for --lang "${lang}".`, 1);
}
entries.push({ fontData, fontRef: `/F${nextRef}`, lang });
nextRef++;
}
return entries;
}
async function render(args) {

@@ -204,14 +619,20 @@ const inputPath = getStringFlag(args.flags, "input", "i");

const useStream = hasFlag(args.flags, "stream");
const conformance = getStringFlag(args.flags, "conformance");
if (conformance !== void 0 && !VALID_CONFORMANCE.has(conformance)) {
const variant = getStringFlag(args.flags, "variant") ?? "document";
const langsRaw = getStringFlag(args.flags, "lang");
if (!VALID_VARIANTS.has(variant)) {
throw new CliError(
`Invalid --conformance value "${conformance}". Valid values: 1b, 2b, 3b.`,
`Invalid --variant "${variant}". Valid: document, table.`,
2
);
}
const layout = await buildLayoutOptions(args);
if (useStream) assertStreamingCompatible(layout);
if (layout.compress === true) {
await pdfnative.initNodeCompression();
}
const inputBuf = await readFileOrStdin(inputPath);
assertJsonSizeLimit(inputBuf);
let params;
let parsedInput;
try {
params = JSON.parse(inputBuf.toString("utf8"));
parsedInput = JSON.parse(inputBuf.toString("utf8"));
} catch (e) {

@@ -221,17 +642,47 @@ const message = e instanceof Error ? e.message : String(e);

}
if (typeof params !== "object" || params === null) {
throw new CliError("JSON input must be a DocumentParams object.", 1);
if (variant === "table") {
if (!isPdfParamsLike(parsedInput)) {
throw new CliError(
"JSON input must be a PdfParams object (with title, headers, rows) when --variant table is used.",
1
);
}
if (useStream) {
const generator = pdfnative.buildPDFStream(parsedInput, layout);
await writeStreamingOutput(generator, outputPath);
} else {
const pdfBytes = pdfnative.buildPDFBytes(parsedInput, layout);
await writeOutput(pdfBytes, outputPath);
}
return;
}
if (conformance !== void 0) {
params["pdfaConformance"] = conformance;
if (!isDocumentParamsLike(parsedInput)) {
throw new CliError(
'JSON input must be a DocumentParams object (with a "blocks" array).',
1
);
}
let params = parsedInput;
if (langsRaw !== void 0) {
const langs = langsRaw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
const fontEntries = await buildFontEntriesForLangs(langs);
const existing = params.fontEntries ?? [];
params = { ...params, fontEntries: [...existing, ...fontEntries] };
}
const effectiveLayout = params.layout !== void 0 && params.layout !== null ? { ...params.layout, ...layout } : layout;
if (useStream && hasTocBlock(params)) {
throw new CliError(
"--stream is incompatible with TOC blocks (multi-pass pagination required).",
2
);
}
if (useStream) {
const generator = pdfnative.buildDocumentPDFStream(params);
const generator = pdfnative.buildDocumentPDFStream(params, effectiveLayout);
await writeStreamingOutput(generator, outputPath);
} else {
const pdfBytes = pdfnative.buildDocumentPDFBytes(params);
const pdfBytes = pdfnative.buildDocumentPDFBytes(params, effectiveLayout);
await writeOutput(pdfBytes, outputPath);
}
}
var VALID_CONFORMANCE;
var VALID_VARIANTS;
var init_render = __esm({

@@ -243,11 +694,6 @@ "src/commands/render.ts"() {

init_error();
VALID_CONFORMANCE = /* @__PURE__ */ new Set(["1b", "2b", "3b"]);
init_layout();
VALID_VARIANTS = /* @__PURE__ */ new Set(["document", "table"]);
}
});
// src/commands/sign.ts
var sign_exports = {};
__export(sign_exports, {
sign: () => sign
});
function pemToDer(pem) {

@@ -262,17 +708,90 @@ const body = pem.replace(/-----BEGIN [^-]+-----/g, "").replace(/-----END [^-]+-----/g, "").replace(/\s+/g, "");

}
async function loadPem(envVar, filePath, label) {
function splitPemBlocks(pem) {
const re = /-----BEGIN [^-]+-----[\s\S]*?-----END [^-]+-----/g;
const matches = pem.match(re);
return matches ?? [];
}
async function loadPem(envVar, filePath, label, flagName) {
const fromEnv = process.env[envVar];
if (fromEnv !== void 0 && fromEnv.trim().length > 0) {
return fromEnv;
}
if (fromEnv !== void 0 && fromEnv.trim().length > 0) return fromEnv;
if (filePath !== void 0) {
validatePath(filePath);
const buf = await promises.readFile(filePath, "utf8");
return buf;
return promises.readFile(filePath, "utf8");
}
throw new CliError(
`Missing ${label}. Provide $${envVar} (env) or --${label.replace(/ /g, "-")} <path>.`,
`Missing ${label}. Provide $${envVar} (env) or --${flagName} <path>.`,
2
);
}
async function loadPemChain(envVar, filePaths) {
const blocks = [];
const fromEnv = process.env[envVar];
if (fromEnv !== void 0 && fromEnv.trim().length > 0) {
blocks.push(...splitPemBlocks(fromEnv));
}
for (const filePath of filePaths) {
validatePath(filePath);
const content = await promises.readFile(filePath, "utf8");
const split = splitPemBlocks(content);
if (split.length === 0) {
blocks.push(content);
} else {
blocks.push(...split);
}
}
return blocks;
}
async function loadRsaPrivateKey(envVar, filePath, flagName) {
const pem = await loadPem(envVar, filePath, "private key", flagName);
try {
return pdfnative.parseRsaPrivateKey(pemToDer(pem));
} catch {
throw new CliError(
"Failed to parse RSA private key. Verify the file is a valid PEM-encoded PKCS#8 RSA key.",
1
);
}
}
async function loadCertificate(envVar, filePath, flagName) {
const pem = await loadPem(envVar, filePath, "certificate", flagName);
try {
return pdfnative.parseCertificate(pemToDer(pem));
} catch {
throw new CliError(
"Failed to parse X.509 certificate. Verify the file is valid PEM-encoded.",
1
);
}
}
function parseCertificateChain(pemBlocks) {
const out = [];
for (const pem of pemBlocks) {
try {
out.push(pdfnative.parseCertificate(pemToDer(pem)));
} catch {
throw new CliError("Failed to parse certificate in chain.", 1);
}
}
return out;
}
var init_keys = __esm({
"src/utils/keys.ts"() {
init_core_bridge();
init_io();
init_error();
}
});
// src/commands/sign.ts
var sign_exports = {};
__export(sign_exports, {
sign: () => sign
});
function parseSigningTime(raw) {
const t = new Date(raw);
if (Number.isNaN(t.getTime())) {
throw new CliError(`Invalid --signing-time "${raw}". Expected ISO 8601 (e.g. 2026-04-28T12:00:00Z).`, 2);
}
return t;
}
async function sign(args) {

@@ -283,19 +802,55 @@ const inputPath = getStringFlag(args.flags, "input", "i");

const certPath = getStringFlag(args.flags, "cert");
const algorithm = getStringFlag(args.flags, "algorithm") ?? "rsa-sha256";
const reason = getStringFlag(args.flags, "reason");
const name = getStringFlag(args.flags, "name");
const location = getStringFlag(args.flags, "location");
const contactInfo = getStringFlag(args.flags, "contact");
const signingTimeRaw = getStringFlag(args.flags, "signing-time");
const chainPaths = getStringFlagAll(args.flags, "cert-chain");
if (!VALID_ALGORITHMS.has(algorithm)) {
throw new CliError(
`Invalid --algorithm "${algorithm}". Valid: rsa-sha256, ecdsa-sha256.`,
2
);
}
if (algorithm === "ecdsa-sha256") {
throw new CliError(
"ECDSA signing is not yet available via the CLI. It requires a pdfnative release exposing parseEcPrivateKey. Use the pdfnative Node.js API directly to sign with ECDSA.",
2
);
}
const signingTime = signingTimeRaw !== void 0 ? parseSigningTime(signingTimeRaw) : void 0;
if (process.env["PDFNATIVE_SIGN_KEY"] === void 0 && keyPath === void 0) {
throw new CliError("Missing private key. Provide $PDFNATIVE_SIGN_KEY (env) or --key <path>.", 2);
}
if (process.env["PDFNATIVE_SIGN_CERT"] === void 0 && certPath === void 0) {
throw new CliError("Missing certificate. Provide $PDFNATIVE_SIGN_CERT (env) or --cert <path>.", 2);
}
const pdfBuf = await readFileOrStdin(inputPath);
const pdfBytes = new Uint8Array(pdfBuf);
const privateKeyPem = await loadPem("PDFNATIVE_SIGN_KEY", keyPath, "private key");
const certPem = await loadPem("PDFNATIVE_SIGN_CERT", certPath, "certificate");
let options;
const rsaKey = await loadRsaPrivateKey("PDFNATIVE_SIGN_KEY", keyPath, "key");
const signerCert = await loadCertificate("PDFNATIVE_SIGN_CERT", certPath, "cert");
const chainPemBlocks = await loadPemChain("PDFNATIVE_SIGN_CHAIN", chainPaths);
const certChain = chainPemBlocks.length > 0 ? parseCertificateChain(chainPemBlocks) : void 0;
const options = {
rsaKey,
signerCert,
algorithm
};
if (certChain !== void 0) options.certChain = certChain;
if (reason !== void 0) options.reason = reason;
if (name !== void 0) options.name = name;
if (location !== void 0) options.location = location;
if (contactInfo !== void 0) options.contactInfo = contactInfo;
if (signingTime !== void 0) options.signingTime = signingTime;
let signedBytes;
try {
const keyDer = pemToDer(privateKeyPem);
const certDer = pemToDer(certPem);
const rsaKey = pdfnative.parseRsaPrivateKey(keyDer);
const signerCert = pdfnative.parseCertificate(certDer);
options = { rsaKey, signerCert, algorithm: "rsa-sha256" };
} catch {
throw new CliError("Failed to parse signing credentials. Verify key and certificate are valid PEM-encoded files.", 1);
signedBytes = pdfnative.signPdfBytes(pdfBytes, options);
} catch (e) {
const safeMsg = e instanceof Error ? e.message.split("\n")[0] : "unknown error";
throw new CliError(`Failed to sign PDF: ${safeMsg ?? "unknown error"}`, 1);
}
const signedBytes = pdfnative.signPdfBytes(pdfBytes, options);
await writeOutput(signedBytes, outputPath);
}
var VALID_ALGORITHMS;
var init_sign = __esm({

@@ -307,5 +862,353 @@ "src/commands/sign.ts"() {

init_error();
init_keys();
VALID_ALGORITHMS = /* @__PURE__ */ new Set(["rsa-sha256", "ecdsa-sha256"]);
}
});
// src/commands/verify.ts
var verify_exports = {};
__export(verify_exports, {
verify: () => verify
});
function decodeHexString(hex) {
const clean = hex.replace(/\s+/g, "");
if (clean.length % 2 !== 0) {
throw new Error("hex string has odd length");
}
const out = new Uint8Array(clean.length / 2);
for (let i = 0; i < out.length; i++) {
out[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
}
return out;
}
function bytesToHex(bytes) {
let s = "";
for (let i = 0; i < bytes.length; i++) {
s += bytes[i].toString(16).padStart(2, "0");
}
return s;
}
function digestByteRange(pdfBytes, byteRange) {
const [a, b, c, d] = byteRange;
const hash = crypto.createHash("sha256");
hash.update(pdfBytes.subarray(a, a + b));
hash.update(pdfBytes.subarray(c, c + d));
return hash.digest("hex");
}
function decodeOid(node) {
if (node.tag !== 6) return null;
const bytes = node.value;
if (bytes.length === 0) return null;
const first = bytes[0];
const parts = [Math.floor(first / 40), first % 40];
let v = 0;
for (let i = 1; i < bytes.length; i++) {
const byte = bytes[i];
v = v << 7 | byte & 127;
if ((byte & 128) === 0) {
parts.push(v);
v = 0;
}
}
return parts.join(".");
}
function findMessageDigest(node) {
if (node.children.length >= 2) {
const oid = decodeOid(node.children[0]);
if (oid === MESSAGE_DIGEST_OID) {
const setNode = node.children[1];
if (setNode.children.length > 0) {
const oct = setNode.children[0];
if (oct.tag === 4) {
return oct.value;
}
}
}
}
for (const child of node.children) {
const found = findMessageDigest(child);
if (found !== null) return found;
}
return null;
}
function extractCertsFromCms(cmsBytes, root) {
const certs = [];
const visit = (node) => {
if (node.tag === 160 && node.children.length > 0) {
for (const child of node.children) {
if (child.tag === 48) {
certs.push(cmsBytes.subarray(child.offset, child.offset + child.totalLength));
}
}
}
for (const child of node.children) visit(child);
};
visit(root);
return certs;
}
function nameToString(name) {
if (name === void 0) return null;
const parts = [];
if (name.cn !== void 0) parts.push(`CN=${name.cn}`);
if (name.o !== void 0) parts.push(`O=${name.o}`);
if (name.ou !== void 0) parts.push(`OU=${name.ou}`);
if (name.c !== void 0) parts.push(`C=${name.c}`);
return parts.length > 0 ? parts.join(", ") : null;
}
function resolveValue(reader, val) {
if (val === void 0) return null;
if (pdfnative.isRef(val)) {
try {
return reader.resolveValue(val);
} catch {
return null;
}
}
return val;
}
function getDictString(dict, key) {
const v = dict.get(key);
return typeof v === "string" ? v : null;
}
function getDictName(dict, key) {
const v = dict.get(key);
return v !== void 0 && pdfnative.isName(v) ? pdfnative.nameValue(v) ?? null : null;
}
function findSignatureFields(reader) {
try {
const catalog = reader.getCatalog();
const acroVal = resolveValue(reader, catalog.get("AcroForm"));
if (acroVal === null || !pdfnative.isDict(acroVal)) return [];
const fieldsVal = resolveValue(reader, acroVal.get("Fields"));
if (fieldsVal === null || !pdfnative.isArray(fieldsVal)) return [];
const out = [];
for (const fieldRef of fieldsVal) {
const field = resolveValue(reader, fieldRef);
if (field === null || !pdfnative.isDict(field)) continue;
if (getDictName(field, "FT") !== "Sig") continue;
const sigVal = resolveValue(reader, field.get("V"));
if (sigVal === null || !pdfnative.isDict(sigVal)) continue;
out.push({
fieldName: getDictString(field, "T"),
sigDict: sigVal
});
}
return out;
} catch {
return [];
}
}
function parseSignatureDict(dict) {
const brVal = dict.get("ByteRange");
let byteRange = null;
if (Array.isArray(brVal) && brVal.length === 4 && brVal.every((n) => typeof n === "number")) {
byteRange = [
brVal[0],
brVal[1],
brVal[2],
brVal[3]
];
}
const contentsRaw = dict.get("Contents");
let contents = null;
if (typeof contentsRaw === "string") {
const stripped = contentsRaw.replace(/^</, "").replace(/>$/, "").trim();
if (/^[0-9a-fA-F\s]+$/.test(stripped) && stripped.length > 0) {
try {
contents = decodeHexString(stripped);
} catch {
contents = null;
}
}
if (contents === null) {
const raw = new Uint8Array(contentsRaw.length);
for (let i = 0; i < contentsRaw.length; i++) {
raw[i] = contentsRaw.charCodeAt(i) & 255;
}
contents = raw;
}
}
return {
byteRange,
contents,
subFilter: getDictName(dict, "SubFilter"),
signingTime: getDictString(dict, "M"),
reason: getDictString(dict, "Reason"),
location: getDictString(dict, "Location")
};
}
function findChainParent(cert, candidates) {
for (const c of candidates) {
if (c === cert) continue;
try {
if (pdfnative.verifyCertSignature(cert, c)) return c;
} catch {
}
}
return void 0;
}
function buildChain(leaf, pool) {
const chain = [leaf];
let current = leaf;
let chainValid = true;
const seen = /* @__PURE__ */ new Set([leaf]);
while (!pdfnative.isSelfSigned(current)) {
const parent = findChainParent(current, pool);
if (parent === void 0 || seen.has(parent)) {
chainValid = false;
break;
}
chain.push(parent);
seen.add(parent);
current = parent;
}
return { chain, chainValid, root: current };
}
function certEquals(a, b) {
if (a.raw.length !== b.raw.length) return false;
for (let i = 0; i < a.raw.length; i++) {
if (a.raw[i] !== b.raw[i]) return false;
}
return true;
}
async function verify(args) {
const inputPath = getStringFlag(args.flags, "input", "i");
const format = getStringFlag(args.flags, "format", "f") ?? "json";
const strict = hasFlag(args.flags, "strict");
const trustPaths = getStringFlagAll(args.flags, "trust");
if (format !== "json" && format !== "text") {
throw new CliError(`Invalid --format value "${format}". Valid: json, text.`, 2);
}
const trustPemBlocks = await loadPemChain("PDFNATIVE_VERIFY_TRUST", trustPaths);
const trustRoots = trustPemBlocks.length > 0 ? parseCertificateChain(trustPemBlocks) : [];
const inputBuf = await readFileOrStdin(inputPath);
const pdfBytes = new Uint8Array(inputBuf);
let reader;
try {
reader = pdfnative.openPdf(pdfBytes);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
throw new CliError(`Failed to read PDF: ${message}`, 1);
}
const fields = findSignatureFields(reader);
const reports = [];
fields.forEach((field, idx) => {
const notes = [];
const sig = parseSignatureDict(field.sigDict);
let digest = null;
let integrity = false;
let signerSubject = null;
let signerIssuer = null;
let chainValid = false;
let trustedRoot = false;
if (sig.byteRange !== null) {
digest = digestByteRange(pdfBytes, sig.byteRange);
} else {
notes.push("missing /ByteRange");
}
if (sig.contents !== null) {
try {
const root = pdfnative.derDecode(sig.contents);
const certDers = extractCertsFromCms(sig.contents, root);
if (certDers.length === 0) {
notes.push("no certificates embedded in CMS");
} else {
const certs = certDers.map((der) => pdfnative.parseCertificate(der));
const leaf = certs[0];
signerSubject = nameToString(leaf.subject);
signerIssuer = nameToString(leaf.issuer);
const md = findMessageDigest(root);
if (md !== null && digest !== null) {
integrity = bytesToHex(md) === digest;
if (!integrity) {
notes.push("messageDigest mismatch \u2014 content tampered after signing");
}
} else {
notes.push("messageDigest attribute not found in CMS");
}
const pool = [...certs, ...trustRoots];
const built = buildChain(leaf, pool);
chainValid = built.chainValid;
if (!chainValid) {
notes.push("chain incomplete (no parent for an intermediate cert)");
}
if (trustRoots.length === 0) {
trustedRoot = pdfnative.isSelfSigned(built.root);
if (trustedRoot) {
notes.push("no --trust provided; accepted self-signed root");
}
} else {
trustedRoot = trustRoots.some((t) => certEquals(t, built.root));
if (!trustedRoot) notes.push("chain root not in --trust list");
}
}
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
notes.push(`failed to parse CMS: ${msg}`);
}
} else {
notes.push("missing /Contents");
}
reports.push({
index: idx,
fieldName: field.fieldName,
subFilter: sig.subFilter,
signerSubject,
signerIssuer,
signingTime: sig.signingTime,
reason: sig.reason,
location: sig.location,
digest,
integrity,
chainValid,
trustedRoot,
notes
});
});
const allValid = reports.length > 0 && reports.every((r) => r.integrity && r.chainValid && r.trustedRoot);
const result = { signatures: reports, allValid };
if (format === "json") {
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
} else {
process.stdout.write(`Signatures: ${reports.length}
`);
for (const r of reports) {
process.stdout.write(
`
[${r.index}] field=${r.fieldName ?? "\u2014"} subFilter=${r.subFilter ?? "\u2014"}
signer: ${r.signerSubject ?? "\u2014"}
issuer: ${r.signerIssuer ?? "\u2014"}
signed at: ${r.signingTime ?? "\u2014"}
integrity: ${r.integrity ? "OK" : "FAIL"}
chain: ${r.chainValid ? "valid" : "invalid"}
trust: ${r.trustedRoot ? "trusted" : "untrusted"}
`
);
if (r.notes.length > 0) {
process.stdout.write(` notes: ${r.notes.join("; ")}
`);
}
}
process.stdout.write(
`
Result: ${allValid ? "all signatures valid" : "one or more checks failed"}
`
);
}
if (strict && !allValid) {
throw new CliError("", 1);
}
}
var MESSAGE_DIGEST_OID;
var init_verify = __esm({
"src/commands/verify.ts"() {
init_core_bridge();
init_args();
init_io();
init_error();
init_keys();
MESSAGE_DIGEST_OID = "1.2.840.113549.1.9.4";
}
});
// src/commands/inspect.ts

@@ -322,6 +1225,4 @@ var inspect_exports = {};

}
if (/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/.test(trimmed)) {
return null;
}
return trimmed || null;
if (/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/.test(trimmed)) return null;
return trimmed.length > 0 ? trimmed : null;
}

@@ -336,6 +1237,5 @@ function extractVersion(reader) {

function extractEncrypted(reader) {
const trailer = reader.trailer;
return trailer.get("Encrypt") !== void 0;
return reader.trailer.get("Encrypt") !== void 0;
}
function extractPdfaConformance(reader) {
function readXmp(reader) {
try {

@@ -352,10 +1252,15 @@ const catalog = reader.getCatalog();

);
const xmp = new TextDecoder("utf-8", { fatal: false }).decode(decoded);
const partMatch = /pdfaid:part[^>]*>(\d+)</.exec(xmp);
const confMatch = /pdfaid:conformance[^>]*>([A-Za-z]+)</.exec(xmp);
if (partMatch !== null && confMatch !== null) {
return `${partMatch[1]}${confMatch[1].toLowerCase()}`;
}
return new TextDecoder("utf-8", { fatal: false }).decode(decoded);
} catch {
return null;
}
}
function extractPdfaConformance(reader) {
const xmp = readXmp(reader);
if (xmp === null) return null;
const partMatch = /pdfaid:part[^>]*>(\d+)</.exec(xmp);
const confMatch = /pdfaid:conformance[^>]*>([A-Za-z]+)</.exec(xmp);
if (partMatch !== null && confMatch !== null) {
return `${partMatch[1]}${confMatch[1].toLowerCase()}`;
}
return null;

@@ -384,7 +1289,79 @@ }

}
function inspectPages(reader) {
const out = [];
for (let i = 0; i < reader.pageCount; i++) {
const page = reader.getPage(i);
const mediaBox = page.get("MediaBox");
let width = null;
let height = null;
const box = Array.isArray(mediaBox) ? mediaBox : null;
if (box !== null && box.length === 4) {
const w = box[2];
const h = box[3];
if (typeof w === "number") width = w;
if (typeof h === "number") height = h;
}
const rotation = typeof page.get("Rotate") === "number" ? page.get("Rotate") : 0;
const annots = page.get("Annots");
let annotations = 0;
let formFields = 0;
if (Array.isArray(annots)) {
for (const ref of annots) {
annotations++;
try {
const annot = reader.resolveValue(ref);
if (annot instanceof Map && annot.get("Subtype") === "/Widget") {
formFields++;
}
} catch {
}
}
}
out.push({ index: i, width, height, rotation, annotations, formFields });
}
return out;
}
function buildVerbose(reader) {
const trailerKeys = [];
for (const k of reader.trailer.keys()) trailerKeys.push(k);
const catalogKeys = [];
try {
for (const k of reader.getCatalog().keys()) catalogKeys.push(k);
} catch {
}
const objectCount = reader.xref?.entries?.size ?? 0;
const xmp = readXmp(reader);
return {
trailerKeys,
catalogKeys,
objectCount,
xmpMetadata: xmp
};
}
function evaluateChecks(checks, result) {
const out = [];
for (const c of checks) {
if (!VALID_CHECKS.has(c)) {
throw new CliError(
`Invalid --check value "${c}". Valid: ${[...VALID_CHECKS].join(", ")}.`,
2
);
}
if (c === "pdfa") out.push({ name: c, passed: result.pdfaConformance !== null });
if (c === "signed") out.push({ name: c, passed: result.signatures > 0 });
if (c === "encrypted") out.push({ name: c, passed: result.encrypted });
}
return {
checks: out.map((x) => `${x.name}=${x.passed ? "pass" : "fail"}`),
allPassed: out.every((x) => x.passed)
};
}
async function inspect(args) {
const inputPath = getStringFlag(args.flags, "input", "i");
const format = getStringFlag(args.flags, "format", "f") ?? "json";
const verbose = hasFlag(args.flags, "verbose");
const includePages = hasFlag(args.flags, "pages");
const checks = getStringFlagAll(args.flags, "check");
if (format !== "json" && format !== "text") {
throw new CliError(`Invalid --format value "${format}". Valid values: json, text.`, 2);
throw new CliError(`Invalid --format value "${format}". Valid: json, text.`, 2);
}

@@ -401,3 +1378,3 @@ const inputBuf = await readFileOrStdin(inputPath);

const info = reader.getInfo();
const result = {
const baseResult = {
version: extractVersion(reader),

@@ -416,2 +1393,7 @@ pageCount: reader.pageCount,

};
const result = {
...baseResult,
...includePages ? { pages: inspectPages(reader) } : {},
...verbose ? { verbose: buildVerbose(reader) } : {}
};
if (format === "json") {

@@ -432,5 +1414,30 @@ process.stdout.write(JSON.stringify(result, null, 2) + "\n");

];
if (result.pages !== void 0) {
lines.push("Pages detail:");
for (const p of result.pages) {
lines.push(
` #${p.index + 1}: ${p.width ?? "?"}x${p.height ?? "?"}pt rot=${p.rotation}\xB0 annots=${p.annotations} fields=${p.formFields}`
);
}
}
if (result.verbose !== void 0) {
lines.push(`Trailer keys: ${result.verbose.trailerKeys.join(", ")}`);
lines.push(`Catalog keys: ${result.verbose.catalogKeys.join(", ")}`);
lines.push(`Object count: ${result.verbose.objectCount}`);
if (result.verbose.xmpMetadata !== null) {
lines.push(`XMP metadata: (${result.verbose.xmpMetadata.length} chars)`);
}
}
process.stdout.write(lines.join("\n") + "\n");
}
if (checks.length > 0) {
const evaluation = evaluateChecks(checks, result);
if (!evaluation.allPassed) {
process.stderr.write(`check failed: ${evaluation.checks.join(", ")}
`);
throw new CliError("", 1);
}
}
}
var VALID_CHECKS;
var init_inspect = __esm({

@@ -442,2 +1449,3 @@ "src/commands/inspect.ts"() {

init_error();
VALID_CHECKS = /* @__PURE__ */ new Set(["pdfa", "signed", "encrypted"]);
}

@@ -456,7 +1464,8 @@ });

render Render a JSON document definition to PDF
sign Apply a digital signature to an existing PDF
sign Apply a digital signature to a PDF
verify Verify embedded PDF signatures
inspect Analyse a PDF and output metadata / conformance info
Options:
--help, -h Show this help message
--help, -h Show this help message
--version, -V Show version

@@ -466,15 +1475,51 @@

`;
var RENDER_USAGE = `pdfnative render \u2014 Render a JSON DocumentParams to PDF
var RENDER_USAGE = `pdfnative render \u2014 Render a JSON document definition to PDF
Usage:
pdfnative render [--input <file.json>] [--output <out.pdf>] [--stream] [--conformance <level>]
pdfnative render [--input <file>] [--output <out.pdf>] [options]
Options:
--input, -i Path to JSON input file (default: stdin)
I/O:
--input, -i Path to JSON input (default: stdin)
--output, -o Output PDF path (default: stdout)
--stream Use streaming output for large documents
--conformance PDF/A conformance level: 1b, 2b, or 3b
--stream Stream output (large documents). Incompatible with TOC blocks
and with header/footer templates that contain {pages}.
Variant:
--variant document (default) or table
Layout (flags override values from --layout file):
--layout Path to JSON layout file (PdfLayoutOptions)
--page-size Named (a4|letter|legal|a3|tabloid|a5) or WxH in points
--margin Uniform N or "top,right,bottom,left" in points
--tagged none|pdfa1b|pdfa2b|pdfa3b (PDF/A flag, sets PDF/A conformance)
--conformance DEPRECATED \u2014 alias for --tagged pdfa{1b|2b|3b}
--compress Enable Flate compression (initialises Node compression)
--lang Comma-separated language packs (e.g. th,ja,ar)
Header / Footer:
--header-left, --header-center, --header-right
--footer-left, --footer-center, --footer-right
Each accepts a template string. {page}, {pages}, {date} are
substituted by pdfnative.
Watermark:
--watermark-text Text watermark
--watermark-image Image path (PNG/JPEG)
--watermark-opacity 0.0\u20131.0 (default 0.2)
--watermark-rotation degrees (default 45)
Encryption (mutually exclusive with --tagged pdfa*):
--encrypt aes-128 | aes-256
--owner-password (or env $PDFNATIVE_ENCRYPT_OWNER_PASS \u2014 env wins)
--user-password (or env $PDFNATIVE_ENCRYPT_USER_PASS \u2014 env wins)
--permissions Comma-separated: print,copy,modify,annotate,form,
accessibility,assemble,print-hi-res
Attachments (PDF/A-3, repeatable):
--attachment <path>[:mime[:rel[:desc]]]
rel = Source|Data|Alternative|Supplement|Unspecified
--help, -h Show this help message
`;
var SIGN_USAGE = `pdfnative sign \u2014 Apply a digital signature to an existing PDF
var SIGN_USAGE = `pdfnative sign \u2014 Apply a digital signature to a PDF

@@ -484,12 +1529,48 @@ Usage:

Options:
I/O:
--input, -i Path to input PDF (default: stdin)
--output, -o Signed PDF output path (default: stdout)
--key Path to PEM private key file
(overridden by $PDFNATIVE_SIGN_KEY env var)
--cert Path to PEM certificate file
(overridden by $PDFNATIVE_SIGN_CERT env var)
Credentials (env wins over file flags):
--key Path to PEM private key (env: PDFNATIVE_SIGN_KEY)
--cert Path to PEM signer certificate (env: PDFNATIVE_SIGN_CERT)
--cert-chain Path to PEM intermediate (repeatable; env: PDFNATIVE_SIGN_CHAIN)
Algorithm:
--algorithm rsa-sha256 (default). ecdsa-sha256 not yet wired (pdfnative
does not yet expose parseEcPrivateKey).
Signature metadata (optional):
--reason Reason text shown in signature panel
--name Signer name override
--location Signing location
--contact Contact info
--signing-time ISO 8601 timestamp (default: now)
Security: key material is never written to logs or error messages.
--help, -h Show this help message
`;
var VERIFY_USAGE = `pdfnative verify \u2014 Verify CMS/PKCS#7 signatures in a PDF
Security: $PDFNATIVE_SIGN_KEY and $PDFNATIVE_SIGN_CERT env vars take precedence over file flags.
Usage:
pdfnative verify [--input <file.pdf>] [--trust <root.pem>]... [--strict] [--format json|text]
Options:
--input, -i Path to input PDF (default: stdin)
--trust PEM file with trusted root certs (repeatable;
env: PDFNATIVE_VERIFY_TRUST). When omitted, self-signed
roots are accepted.
--strict Exit code 1 if any signature fails any check.
--format, -f json (default) or text
--help, -h Show this help message
Reported per signature:
- byte-range integrity (SHA-256 against CMS messageDigest)
- signer subject / issuer
- certificate chain validity
- chain root trust evaluation
Out of scope (v0.2.0): full CMS-signature-value verification, OCSP/CRL,
RFC 3161 timestamps, LTV. These require future pdfnative API additions.
`;

@@ -499,7 +1580,12 @@ var INSPECT_USAGE = `pdfnative inspect \u2014 Analyse a PDF and output metadata

Usage:
pdfnative inspect [--input <file.pdf>] [--format <fmt>]
pdfnative inspect [--input <file.pdf>] [--format <fmt>] [options]
Options:
--input, -i Path to input PDF (default: stdin)
--format, -f Output format: json (default) or text
--format, -f json (default) or text
--verbose, -v Include trailerKeys, catalogKeys, objectCount,
XMP metadata length
--pages Per-page width/height/rotation/annotation/formField counts
--check Assert a property; repeatable; AND semantics; exits 1 on
failure. Values: pdfa | signed | encrypted
--help, -h Show this help message

@@ -522,2 +1608,6 @@ `;

}
case "verify": {
const m = await Promise.resolve().then(() => (init_verify(), verify_exports));
return m.verify;
}
case "inspect": {

@@ -528,3 +1618,5 @@ const m = await Promise.resolve().then(() => (init_inspect(), inspect_exports));

default:
return Promise.reject(new CliError(`Unknown command: ${name}. Run pdfnative --help for usage.`, 1));
return Promise.reject(
new CliError(`Unknown command: ${name}. Run pdfnative --help for usage.`, 1)
);
}

@@ -556,2 +1648,5 @@ }

break;
case "verify":
process.stdout.write(VERIFY_USAGE);
break;
case "inspect":

@@ -567,8 +1662,11 @@ process.stdout.write(INSPECT_USAGE);

}
const commandArgs = parseArgs(argv.filter((_t, _i) => {
if (_t === commandName && args.positionals[0] === commandName) {
let stripped = false;
const rest = argv.filter((tok) => {
if (!stripped && tok === commandName) {
stripped = true;
return false;
}
return true;
}));
});
const commandArgs = parseArgs(rest);
const command = await loadCommand(commandName);

@@ -579,3 +1677,5 @@ await command(commandArgs);

if (e instanceof CliError) {
process.stderr.write(e.message + "\n");
if (e.message.length > 0) {
process.stderr.write(e.message + "\n");
}
process.exit(e.exitCode);

@@ -582,0 +1682,0 @@ }

+1186
-86
#!/usr/bin/env node
import { buildDocumentPDFStream, buildDocumentPDFBytes, parseRsaPrivateKey, parseCertificate, signPdfBytes, openPdf } from 'pdfnative';
import { initNodeCompression, buildPDFStream, buildPDFBytes, buildDocumentPDFStream, buildDocumentPDFBytes, signPdfBytes, openPdf, derDecode, parseCertificate, isSelfSigned, hasFontLoader, loadFontData, parseRsaPrivateKey, isDict, isArray, isRef, isName, nameValue, verifyCertSignature } from 'pdfnative';
import { createWriteStream } from 'fs';
import { readFile, writeFile } from 'fs/promises';
import { createHash } from 'crypto';
import { createRequire } from 'module';

@@ -18,3 +19,9 @@

// src/utils/error.ts
var CliError;
function deprecate(name, replacement) {
if (_deprecateSeen.has(name)) return;
_deprecateSeen.add(name);
process.stderr.write(`warning: --${name} is deprecated; use ${replacement} instead.
`);
}
var CliError, _deprecateSeen;
var init_error = __esm({

@@ -30,2 +37,3 @@ "src/utils/error.ts"() {

};
_deprecateSeen = /* @__PURE__ */ new Set();
}

@@ -39,2 +47,17 @@ });

let i = 0;
const setFlag = (key, value) => {
const existing = flags[key];
if (existing === void 0 || typeof existing === "boolean") {
flags[key] = value;
return;
}
if (typeof value === "boolean") {
return;
}
if (typeof existing === "string") {
flags[key] = [existing, value];
} else {
existing.push(value);
}
};
while (i < argv.length) {

@@ -55,3 +78,3 @@ const token = argv[i];

const value = token.slice(eqIdx + 1);
flags[key] = value;
setFlag(key, value);
} else {

@@ -61,6 +84,6 @@ const key = token.slice(2);

if (next !== void 0 && !next.startsWith("-")) {
flags[key] = next;
setFlag(key, next);
i++;
} else {
flags[key] = true;
setFlag(key, true);
}

@@ -72,6 +95,6 @@ }

if (next !== void 0 && !next.startsWith("-")) {
flags[key] = next;
setFlag(key, next);
i++;
} else {
flags[key] = true;
setFlag(key, true);
}

@@ -88,11 +111,27 @@ } else {

const value = flags[name];
if (value !== void 0) {
if (typeof value !== "string") {
throw new CliError(`Flag --${name} requires a value.`, 2);
}
return value;
if (value === void 0) continue;
if (typeof value === "boolean") {
throw new CliError(`Flag --${name} requires a value.`, 2);
}
if (typeof value === "string") return value;
return value[0];
}
return void 0;
}
function getStringFlagAll(flags, ...names) {
const out = [];
for (const name of names) {
const value = flags[name];
if (value === void 0) continue;
if (typeof value === "boolean") {
throw new CliError(`Flag --${name} requires a value.`, 2);
}
if (typeof value === "string") {
out.push(value);
} else {
out.push(...value);
}
}
return out;
}
function hasFlag(flags, ...names) {

@@ -131,2 +170,7 @@ return names.some((n) => flags[n] !== void 0);

}
async function readBinaryFile(filePath) {
validatePath(filePath);
const buf = await readFile(filePath);
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
}
function assertJsonSizeLimit(buf) {

@@ -188,2 +232,336 @@ if (buf.length > JSON_SIZE_LIMIT) {

});
async function loadLayoutFile(filePath) {
if (filePath === void 0) return {};
validatePath(filePath);
let raw;
try {
raw = await readFile(filePath, "utf8");
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
throw new CliError(`Failed to read --layout file: ${msg}`, 1);
}
let parsed;
try {
parsed = JSON.parse(raw);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
throw new CliError(`Failed to parse --layout JSON: ${msg}`, 1);
}
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
throw new CliError("--layout file must contain a JSON object.", 1);
}
const obj = parsed;
if (Array.isArray(obj.attachments)) {
obj.attachments = obj.attachments.map((a) => {
if (typeof a !== "object" || a === null) return a;
const rest = { ...a };
delete rest.data;
return rest;
});
}
return obj;
}
function parsePageSizePair(value) {
const m = /^(\d+(?:\.\d+)?)x(\d+(?:\.\d+)?)$/i.exec(value.trim());
if (m === null) return null;
return [Number.parseFloat(m[1]), Number.parseFloat(m[2])];
}
function parsePageSize(value) {
const lower = value.toLowerCase();
const named = NAMED_PAGE_SIZES[lower];
if (named !== void 0) {
return { pageWidth: named[0], pageHeight: named[1] };
}
const pair = parsePageSizePair(value);
if (pair !== null) {
return { pageWidth: pair[0], pageHeight: pair[1] };
}
const valid = Object.keys(NAMED_PAGE_SIZES).join(", ");
throw new CliError(
`Invalid --page-size value "${value}". Expected one of: ${valid}, or WxH (points).`,
2
);
}
function parseMargin(value) {
const parts = value.split(",").map((p) => p.trim());
const nums = parts.map((p) => {
const n = Number.parseFloat(p);
if (!Number.isFinite(n) || n < 0) {
throw new CliError(`Invalid --margin value "${value}".`, 2);
}
return n;
});
if (nums.length === 1) {
const v = nums[0];
return { t: v, r: v, b: v, l: v };
}
if (nums.length === 4) {
return {
t: nums[0],
r: nums[1],
b: nums[2],
l: nums[3]
};
}
throw new CliError(`Invalid --margin "${value}". Expected N or T,R,B,L.`, 2);
}
function parseTagged(value) {
const v = value.toLowerCase();
if (v === "none") return false;
if (VALID_TAGGED.includes(v)) {
return v;
}
throw new CliError(
`Invalid --tagged value "${value}". Valid: ${VALID_TAGGED.join(", ")}.`,
2
);
}
function conformanceToTagged(value) {
const validShort = /* @__PURE__ */ new Set(["1b", "2b", "3b"]);
if (!validShort.has(value)) {
throw new CliError(
`Invalid --conformance value "${value}". Valid: 1b, 2b, 3b.`,
2
);
}
return "pdfa" + value;
}
function buildPageTemplate(parts) {
if (parts.left === void 0 && parts.center === void 0 && parts.right === void 0) {
return void 0;
}
const tpl = {};
if (parts.left !== void 0) tpl.left = parts.left;
if (parts.center !== void 0) tpl.center = parts.center;
if (parts.right !== void 0) tpl.right = parts.right;
return tpl;
}
function buildEncryptionFromFlags(args) {
const ownerFlag = getStringFlag(args.flags, "encrypt-owner-pass");
const userFlag = getStringFlag(args.flags, "encrypt-user-pass");
const algoFlag = getStringFlag(args.flags, "encrypt-algorithm");
const permsFlag = getStringFlag(args.flags, "encrypt-permissions");
const owner = process.env.PDFNATIVE_ENCRYPT_OWNER_PASS ?? ownerFlag;
const user = process.env.PDFNATIVE_ENCRYPT_USER_PASS ?? userFlag;
if (owner === void 0 && user === void 0 && algoFlag === void 0 && permsFlag === void 0) {
return void 0;
}
if (owner === void 0 || owner.length === 0) {
throw new CliError(
"Encryption requires an owner password. Provide --encrypt-owner-pass <pass> or $PDFNATIVE_ENCRYPT_OWNER_PASS.",
2
);
}
const algo = algoFlag ?? "aes128";
if (!VALID_ENCRYPTION_ALGOS.has(algo)) {
throw new CliError(
`Invalid --encrypt-algorithm "${algo}". Valid: aes128, aes256.`,
2
);
}
const opts = {
ownerPassword: owner,
algorithm: algo
};
if (user !== void 0) opts.userPassword = user;
if (permsFlag !== void 0) {
const perms = {};
for (const raw of permsFlag.split(",")) {
const p = raw.trim();
if (p.length === 0) continue;
const lower = p.toLowerCase();
if (!VALID_PERMISSIONS.has(p) && !VALID_PERMISSIONS.has(lower)) {
throw new CliError(
`Invalid permission "${p}" in --encrypt-permissions. Valid: print, copy, modify, extractText.`,
2
);
}
const key = lower === "extracttext" || p === "extractText" ? "extractText" : lower;
perms[key] = true;
}
opts.permissions = perms;
}
return opts;
}
async function buildWatermarkFromFlags(args) {
const text = getStringFlag(args.flags, "watermark-text");
const opacity = getStringFlag(args.flags, "watermark-opacity");
const angle = getStringFlag(args.flags, "watermark-angle");
const color = getStringFlag(args.flags, "watermark-color");
const fontSize = getStringFlag(args.flags, "watermark-font-size");
const imagePath = getStringFlag(args.flags, "watermark-image");
const position = getStringFlag(args.flags, "watermark-position");
if (text === void 0 && imagePath === void 0 && opacity === void 0 && angle === void 0 && color === void 0 && fontSize === void 0 && position === void 0) {
return void 0;
}
const wm = {};
if (text !== void 0) {
const t = { text };
if (opacity !== void 0) t.opacity = parseUnit(opacity, "watermark-opacity", 0, 1);
if (angle !== void 0) t.angle = parseFloatFlag(angle, "watermark-angle");
if (color !== void 0) t.color = color;
if (fontSize !== void 0) t.fontSize = parseFloatFlag(fontSize, "watermark-font-size");
wm.text = t;
}
if (imagePath !== void 0) {
const data = await readBinaryFile(imagePath);
const img = { data };
if (opacity !== void 0 && text === void 0) {
img.opacity = parseUnit(opacity, "watermark-opacity", 0, 1);
}
wm.image = img;
}
if (position !== void 0) {
if (position !== "background" && position !== "foreground") {
throw new CliError(
`Invalid --watermark-position "${position}". Valid: background, foreground.`,
2
);
}
wm.position = position;
}
return wm;
}
function parseFloatFlag(value, flag) {
const n = Number.parseFloat(value);
if (!Number.isFinite(n)) {
throw new CliError(`Invalid --${flag} value "${value}".`, 2);
}
return n;
}
function parseUnit(value, flag, min, max) {
const n = parseFloatFlag(value, flag);
if (n < min || n > max) {
throw new CliError(`--${flag} must be between ${min} and ${max} (got ${value}).`, 2);
}
return n;
}
async function loadAttachmentsFromFlags(args) {
const paths = getStringFlagAll(args.flags, "attachment");
if (paths.length === 0) return void 0;
const out = [];
for (const raw of paths) {
const parts = raw.split(":");
let pathPart = parts[0] ?? "";
let offset = 1;
if (pathPart.length === 1 && /^[A-Za-z]$/.test(pathPart) && parts.length > 1) {
pathPart = `${pathPart}:${parts[1] ?? ""}`;
offset = 2;
}
if (pathPart.length === 0) {
throw new CliError(`Invalid --attachment value "${raw}".`, 2);
}
const mimePart = parts[offset];
const relPart = parts[offset + 1];
const descPart = parts[offset + 2];
const data = await readBinaryFile(pathPart);
const filename = pathPart.split(/[/\\]/).pop() ?? "attachment";
const mime = mimePart && mimePart.length > 0 ? mimePart : "application/octet-stream";
const att = {
filename,
data,
mimeType: mime
};
if (relPart !== void 0 && relPart.length > 0) {
att.relationship = relPart;
}
if (descPart !== void 0 && descPart.length > 0) {
att.description = descPart;
}
out.push(att);
}
return out;
}
async function buildLayoutOptions(args) {
const layoutPath = getStringFlag(args.flags, "layout");
const fromFile = await loadLayoutFile(layoutPath);
const out = { ...fromFile };
const pageSize = getStringFlag(args.flags, "page-size");
if (pageSize !== void 0) {
const { pageWidth, pageHeight } = parsePageSize(pageSize);
out.pageWidth = pageWidth;
out.pageHeight = pageHeight;
}
const margin = getStringFlag(args.flags, "margin");
if (margin !== void 0) {
out.margins = parseMargin(margin);
}
if (hasFlag(args.flags, "compress")) {
out.compress = true;
}
const tagged = getStringFlag(args.flags, "tagged");
const conformance = getStringFlag(args.flags, "conformance");
if (tagged !== void 0 && conformance !== void 0) {
throw new CliError(
"Use either --tagged or --conformance, not both. Prefer --tagged.",
2
);
}
if (tagged !== void 0) {
out.tagged = parseTagged(tagged);
} else if (conformance !== void 0) {
deprecate("conformance", "--tagged pdfa<level>");
out.tagged = conformanceToTagged(conformance);
}
const header = buildPageTemplate({
left: getStringFlag(args.flags, "header-left"),
center: getStringFlag(args.flags, "header-center"),
right: getStringFlag(args.flags, "header-right")
});
if (header !== void 0) out.headerTemplate = header;
const footer = buildPageTemplate({
left: getStringFlag(args.flags, "footer-left"),
center: getStringFlag(args.flags, "footer-center"),
right: getStringFlag(args.flags, "footer-right")
});
if (footer !== void 0) out.footerTemplate = footer;
const watermark = await buildWatermarkFromFlags(args);
if (watermark !== void 0) out.watermark = watermark;
const encryption = buildEncryptionFromFlags(args);
if (encryption !== void 0) out.encryption = encryption;
const attachments = await loadAttachmentsFromFlags(args);
if (attachments !== void 0) out.attachments = attachments;
const tg = out.tagged;
if (encryption !== void 0 && tg !== void 0 && tg !== false) {
throw new CliError(
"Encryption is mutually exclusive with --tagged (PDF/A forbids encryption per ISO 19005-1 \xA76.3.2).",
2
);
}
return out;
}
function assertStreamingCompatible(layout) {
const check = (tpl, label) => {
if (tpl === void 0) return;
for (const part of [tpl.left, tpl.center, tpl.right]) {
if (part !== void 0 && part.includes("{pages}")) {
throw new CliError(
`--stream is incompatible with the {pages} placeholder in --${label}-* (total page count not known until full render).`,
2
);
}
}
};
check(layout.headerTemplate, "header");
check(layout.footerTemplate, "footer");
}
var VALID_TAGGED, NAMED_PAGE_SIZES, VALID_ENCRYPTION_ALGOS, VALID_PERMISSIONS;
var init_layout = __esm({
"src/utils/layout.ts"() {
init_args();
init_io();
init_error();
VALID_TAGGED = ["none", "pdfa1b", "pdfa2b", "pdfa2u", "pdfa3b"];
NAMED_PAGE_SIZES = {
a4: [595.28, 841.89],
letter: [612, 792],
legal: [612, 1008],
a3: [841.89, 1190.55],
tabloid: [792, 1224],
a5: [419.53, 595.28]
};
VALID_ENCRYPTION_ALGOS = /* @__PURE__ */ new Set(["aes128", "aes256"]);
VALID_PERMISSIONS = /* @__PURE__ */ new Set(["print", "copy", "modify", "extractText", "extracttext"]);
}
});

@@ -195,2 +573,39 @@ // src/commands/render.ts

});
function isPdfParamsLike(value) {
if (typeof value !== "object" || value === null) return false;
const v = value;
return typeof v.title === "string" && Array.isArray(v.headers) && Array.isArray(v.rows);
}
function isDocumentParamsLike(value) {
if (typeof value !== "object" || value === null) return false;
const v = value;
return Array.isArray(v.blocks);
}
function hasTocBlock(params) {
for (const b of params.blocks) {
const block = b;
if (block.type === "toc") return true;
}
return false;
}
async function buildFontEntriesForLangs(langs) {
if (langs.length === 0) return [];
const entries = [];
let nextRef = 3;
for (const lang of langs) {
if (!hasFontLoader(lang)) {
throw new CliError(
`--lang "${lang}" is not a bundled pdfnative font. Register a loader programmatically before invoking the CLI to use a custom font.`,
2
);
}
const fontData = await loadFontData(lang);
if (fontData === null) {
throw new CliError(`Failed to load font data for --lang "${lang}".`, 1);
}
entries.push({ fontData, fontRef: `/F${nextRef}`, lang });
nextRef++;
}
return entries;
}
async function render(args) {

@@ -200,14 +615,20 @@ const inputPath = getStringFlag(args.flags, "input", "i");

const useStream = hasFlag(args.flags, "stream");
const conformance = getStringFlag(args.flags, "conformance");
if (conformance !== void 0 && !VALID_CONFORMANCE.has(conformance)) {
const variant = getStringFlag(args.flags, "variant") ?? "document";
const langsRaw = getStringFlag(args.flags, "lang");
if (!VALID_VARIANTS.has(variant)) {
throw new CliError(
`Invalid --conformance value "${conformance}". Valid values: 1b, 2b, 3b.`,
`Invalid --variant "${variant}". Valid: document, table.`,
2
);
}
const layout = await buildLayoutOptions(args);
if (useStream) assertStreamingCompatible(layout);
if (layout.compress === true) {
await initNodeCompression();
}
const inputBuf = await readFileOrStdin(inputPath);
assertJsonSizeLimit(inputBuf);
let params;
let parsedInput;
try {
params = JSON.parse(inputBuf.toString("utf8"));
parsedInput = JSON.parse(inputBuf.toString("utf8"));
} catch (e) {

@@ -217,17 +638,47 @@ const message = e instanceof Error ? e.message : String(e);

}
if (typeof params !== "object" || params === null) {
throw new CliError("JSON input must be a DocumentParams object.", 1);
if (variant === "table") {
if (!isPdfParamsLike(parsedInput)) {
throw new CliError(
"JSON input must be a PdfParams object (with title, headers, rows) when --variant table is used.",
1
);
}
if (useStream) {
const generator = buildPDFStream(parsedInput, layout);
await writeStreamingOutput(generator, outputPath);
} else {
const pdfBytes = buildPDFBytes(parsedInput, layout);
await writeOutput(pdfBytes, outputPath);
}
return;
}
if (conformance !== void 0) {
params["pdfaConformance"] = conformance;
if (!isDocumentParamsLike(parsedInput)) {
throw new CliError(
'JSON input must be a DocumentParams object (with a "blocks" array).',
1
);
}
let params = parsedInput;
if (langsRaw !== void 0) {
const langs = langsRaw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
const fontEntries = await buildFontEntriesForLangs(langs);
const existing = params.fontEntries ?? [];
params = { ...params, fontEntries: [...existing, ...fontEntries] };
}
const effectiveLayout = params.layout !== void 0 && params.layout !== null ? { ...params.layout, ...layout } : layout;
if (useStream && hasTocBlock(params)) {
throw new CliError(
"--stream is incompatible with TOC blocks (multi-pass pagination required).",
2
);
}
if (useStream) {
const generator = buildDocumentPDFStream(params);
const generator = buildDocumentPDFStream(params, effectiveLayout);
await writeStreamingOutput(generator, outputPath);
} else {
const pdfBytes = buildDocumentPDFBytes(params);
const pdfBytes = buildDocumentPDFBytes(params, effectiveLayout);
await writeOutput(pdfBytes, outputPath);
}
}
var VALID_CONFORMANCE;
var VALID_VARIANTS;
var init_render = __esm({

@@ -239,11 +690,6 @@ "src/commands/render.ts"() {

init_error();
VALID_CONFORMANCE = /* @__PURE__ */ new Set(["1b", "2b", "3b"]);
init_layout();
VALID_VARIANTS = /* @__PURE__ */ new Set(["document", "table"]);
}
});
// src/commands/sign.ts
var sign_exports = {};
__export(sign_exports, {
sign: () => sign
});
function pemToDer(pem) {

@@ -258,17 +704,90 @@ const body = pem.replace(/-----BEGIN [^-]+-----/g, "").replace(/-----END [^-]+-----/g, "").replace(/\s+/g, "");

}
async function loadPem(envVar, filePath, label) {
function splitPemBlocks(pem) {
const re = /-----BEGIN [^-]+-----[\s\S]*?-----END [^-]+-----/g;
const matches = pem.match(re);
return matches ?? [];
}
async function loadPem(envVar, filePath, label, flagName) {
const fromEnv = process.env[envVar];
if (fromEnv !== void 0 && fromEnv.trim().length > 0) {
return fromEnv;
}
if (fromEnv !== void 0 && fromEnv.trim().length > 0) return fromEnv;
if (filePath !== void 0) {
validatePath(filePath);
const buf = await readFile(filePath, "utf8");
return buf;
return readFile(filePath, "utf8");
}
throw new CliError(
`Missing ${label}. Provide $${envVar} (env) or --${label.replace(/ /g, "-")} <path>.`,
`Missing ${label}. Provide $${envVar} (env) or --${flagName} <path>.`,
2
);
}
async function loadPemChain(envVar, filePaths) {
const blocks = [];
const fromEnv = process.env[envVar];
if (fromEnv !== void 0 && fromEnv.trim().length > 0) {
blocks.push(...splitPemBlocks(fromEnv));
}
for (const filePath of filePaths) {
validatePath(filePath);
const content = await readFile(filePath, "utf8");
const split = splitPemBlocks(content);
if (split.length === 0) {
blocks.push(content);
} else {
blocks.push(...split);
}
}
return blocks;
}
async function loadRsaPrivateKey(envVar, filePath, flagName) {
const pem = await loadPem(envVar, filePath, "private key", flagName);
try {
return parseRsaPrivateKey(pemToDer(pem));
} catch {
throw new CliError(
"Failed to parse RSA private key. Verify the file is a valid PEM-encoded PKCS#8 RSA key.",
1
);
}
}
async function loadCertificate(envVar, filePath, flagName) {
const pem = await loadPem(envVar, filePath, "certificate", flagName);
try {
return parseCertificate(pemToDer(pem));
} catch {
throw new CliError(
"Failed to parse X.509 certificate. Verify the file is valid PEM-encoded.",
1
);
}
}
function parseCertificateChain(pemBlocks) {
const out = [];
for (const pem of pemBlocks) {
try {
out.push(parseCertificate(pemToDer(pem)));
} catch {
throw new CliError("Failed to parse certificate in chain.", 1);
}
}
return out;
}
var init_keys = __esm({
"src/utils/keys.ts"() {
init_core_bridge();
init_io();
init_error();
}
});
// src/commands/sign.ts
var sign_exports = {};
__export(sign_exports, {
sign: () => sign
});
function parseSigningTime(raw) {
const t = new Date(raw);
if (Number.isNaN(t.getTime())) {
throw new CliError(`Invalid --signing-time "${raw}". Expected ISO 8601 (e.g. 2026-04-28T12:00:00Z).`, 2);
}
return t;
}
async function sign(args) {

@@ -279,19 +798,55 @@ const inputPath = getStringFlag(args.flags, "input", "i");

const certPath = getStringFlag(args.flags, "cert");
const algorithm = getStringFlag(args.flags, "algorithm") ?? "rsa-sha256";
const reason = getStringFlag(args.flags, "reason");
const name = getStringFlag(args.flags, "name");
const location = getStringFlag(args.flags, "location");
const contactInfo = getStringFlag(args.flags, "contact");
const signingTimeRaw = getStringFlag(args.flags, "signing-time");
const chainPaths = getStringFlagAll(args.flags, "cert-chain");
if (!VALID_ALGORITHMS.has(algorithm)) {
throw new CliError(
`Invalid --algorithm "${algorithm}". Valid: rsa-sha256, ecdsa-sha256.`,
2
);
}
if (algorithm === "ecdsa-sha256") {
throw new CliError(
"ECDSA signing is not yet available via the CLI. It requires a pdfnative release exposing parseEcPrivateKey. Use the pdfnative Node.js API directly to sign with ECDSA.",
2
);
}
const signingTime = signingTimeRaw !== void 0 ? parseSigningTime(signingTimeRaw) : void 0;
if (process.env["PDFNATIVE_SIGN_KEY"] === void 0 && keyPath === void 0) {
throw new CliError("Missing private key. Provide $PDFNATIVE_SIGN_KEY (env) or --key <path>.", 2);
}
if (process.env["PDFNATIVE_SIGN_CERT"] === void 0 && certPath === void 0) {
throw new CliError("Missing certificate. Provide $PDFNATIVE_SIGN_CERT (env) or --cert <path>.", 2);
}
const pdfBuf = await readFileOrStdin(inputPath);
const pdfBytes = new Uint8Array(pdfBuf);
const privateKeyPem = await loadPem("PDFNATIVE_SIGN_KEY", keyPath, "private key");
const certPem = await loadPem("PDFNATIVE_SIGN_CERT", certPath, "certificate");
let options;
const rsaKey = await loadRsaPrivateKey("PDFNATIVE_SIGN_KEY", keyPath, "key");
const signerCert = await loadCertificate("PDFNATIVE_SIGN_CERT", certPath, "cert");
const chainPemBlocks = await loadPemChain("PDFNATIVE_SIGN_CHAIN", chainPaths);
const certChain = chainPemBlocks.length > 0 ? parseCertificateChain(chainPemBlocks) : void 0;
const options = {
rsaKey,
signerCert,
algorithm
};
if (certChain !== void 0) options.certChain = certChain;
if (reason !== void 0) options.reason = reason;
if (name !== void 0) options.name = name;
if (location !== void 0) options.location = location;
if (contactInfo !== void 0) options.contactInfo = contactInfo;
if (signingTime !== void 0) options.signingTime = signingTime;
let signedBytes;
try {
const keyDer = pemToDer(privateKeyPem);
const certDer = pemToDer(certPem);
const rsaKey = parseRsaPrivateKey(keyDer);
const signerCert = parseCertificate(certDer);
options = { rsaKey, signerCert, algorithm: "rsa-sha256" };
} catch {
throw new CliError("Failed to parse signing credentials. Verify key and certificate are valid PEM-encoded files.", 1);
signedBytes = signPdfBytes(pdfBytes, options);
} catch (e) {
const safeMsg = e instanceof Error ? e.message.split("\n")[0] : "unknown error";
throw new CliError(`Failed to sign PDF: ${safeMsg ?? "unknown error"}`, 1);
}
const signedBytes = signPdfBytes(pdfBytes, options);
await writeOutput(signedBytes, outputPath);
}
var VALID_ALGORITHMS;
var init_sign = __esm({

@@ -303,5 +858,353 @@ "src/commands/sign.ts"() {

init_error();
init_keys();
VALID_ALGORITHMS = /* @__PURE__ */ new Set(["rsa-sha256", "ecdsa-sha256"]);
}
});
// src/commands/verify.ts
var verify_exports = {};
__export(verify_exports, {
verify: () => verify
});
function decodeHexString(hex) {
const clean = hex.replace(/\s+/g, "");
if (clean.length % 2 !== 0) {
throw new Error("hex string has odd length");
}
const out = new Uint8Array(clean.length / 2);
for (let i = 0; i < out.length; i++) {
out[i] = Number.parseInt(clean.slice(i * 2, i * 2 + 2), 16);
}
return out;
}
function bytesToHex(bytes) {
let s = "";
for (let i = 0; i < bytes.length; i++) {
s += bytes[i].toString(16).padStart(2, "0");
}
return s;
}
function digestByteRange(pdfBytes, byteRange) {
const [a, b, c, d] = byteRange;
const hash = createHash("sha256");
hash.update(pdfBytes.subarray(a, a + b));
hash.update(pdfBytes.subarray(c, c + d));
return hash.digest("hex");
}
function decodeOid(node) {
if (node.tag !== 6) return null;
const bytes = node.value;
if (bytes.length === 0) return null;
const first = bytes[0];
const parts = [Math.floor(first / 40), first % 40];
let v = 0;
for (let i = 1; i < bytes.length; i++) {
const byte = bytes[i];
v = v << 7 | byte & 127;
if ((byte & 128) === 0) {
parts.push(v);
v = 0;
}
}
return parts.join(".");
}
function findMessageDigest(node) {
if (node.children.length >= 2) {
const oid = decodeOid(node.children[0]);
if (oid === MESSAGE_DIGEST_OID) {
const setNode = node.children[1];
if (setNode.children.length > 0) {
const oct = setNode.children[0];
if (oct.tag === 4) {
return oct.value;
}
}
}
}
for (const child of node.children) {
const found = findMessageDigest(child);
if (found !== null) return found;
}
return null;
}
function extractCertsFromCms(cmsBytes, root) {
const certs = [];
const visit = (node) => {
if (node.tag === 160 && node.children.length > 0) {
for (const child of node.children) {
if (child.tag === 48) {
certs.push(cmsBytes.subarray(child.offset, child.offset + child.totalLength));
}
}
}
for (const child of node.children) visit(child);
};
visit(root);
return certs;
}
function nameToString(name) {
if (name === void 0) return null;
const parts = [];
if (name.cn !== void 0) parts.push(`CN=${name.cn}`);
if (name.o !== void 0) parts.push(`O=${name.o}`);
if (name.ou !== void 0) parts.push(`OU=${name.ou}`);
if (name.c !== void 0) parts.push(`C=${name.c}`);
return parts.length > 0 ? parts.join(", ") : null;
}
function resolveValue(reader, val) {
if (val === void 0) return null;
if (isRef(val)) {
try {
return reader.resolveValue(val);
} catch {
return null;
}
}
return val;
}
function getDictString(dict, key) {
const v = dict.get(key);
return typeof v === "string" ? v : null;
}
function getDictName(dict, key) {
const v = dict.get(key);
return v !== void 0 && isName(v) ? nameValue(v) ?? null : null;
}
function findSignatureFields(reader) {
try {
const catalog = reader.getCatalog();
const acroVal = resolveValue(reader, catalog.get("AcroForm"));
if (acroVal === null || !isDict(acroVal)) return [];
const fieldsVal = resolveValue(reader, acroVal.get("Fields"));
if (fieldsVal === null || !isArray(fieldsVal)) return [];
const out = [];
for (const fieldRef of fieldsVal) {
const field = resolveValue(reader, fieldRef);
if (field === null || !isDict(field)) continue;
if (getDictName(field, "FT") !== "Sig") continue;
const sigVal = resolveValue(reader, field.get("V"));
if (sigVal === null || !isDict(sigVal)) continue;
out.push({
fieldName: getDictString(field, "T"),
sigDict: sigVal
});
}
return out;
} catch {
return [];
}
}
function parseSignatureDict(dict) {
const brVal = dict.get("ByteRange");
let byteRange = null;
if (Array.isArray(brVal) && brVal.length === 4 && brVal.every((n) => typeof n === "number")) {
byteRange = [
brVal[0],
brVal[1],
brVal[2],
brVal[3]
];
}
const contentsRaw = dict.get("Contents");
let contents = null;
if (typeof contentsRaw === "string") {
const stripped = contentsRaw.replace(/^</, "").replace(/>$/, "").trim();
if (/^[0-9a-fA-F\s]+$/.test(stripped) && stripped.length > 0) {
try {
contents = decodeHexString(stripped);
} catch {
contents = null;
}
}
if (contents === null) {
const raw = new Uint8Array(contentsRaw.length);
for (let i = 0; i < contentsRaw.length; i++) {
raw[i] = contentsRaw.charCodeAt(i) & 255;
}
contents = raw;
}
}
return {
byteRange,
contents,
subFilter: getDictName(dict, "SubFilter"),
signingTime: getDictString(dict, "M"),
reason: getDictString(dict, "Reason"),
location: getDictString(dict, "Location")
};
}
function findChainParent(cert, candidates) {
for (const c of candidates) {
if (c === cert) continue;
try {
if (verifyCertSignature(cert, c)) return c;
} catch {
}
}
return void 0;
}
function buildChain(leaf, pool) {
const chain = [leaf];
let current = leaf;
let chainValid = true;
const seen = /* @__PURE__ */ new Set([leaf]);
while (!isSelfSigned(current)) {
const parent = findChainParent(current, pool);
if (parent === void 0 || seen.has(parent)) {
chainValid = false;
break;
}
chain.push(parent);
seen.add(parent);
current = parent;
}
return { chain, chainValid, root: current };
}
function certEquals(a, b) {
if (a.raw.length !== b.raw.length) return false;
for (let i = 0; i < a.raw.length; i++) {
if (a.raw[i] !== b.raw[i]) return false;
}
return true;
}
async function verify(args) {
const inputPath = getStringFlag(args.flags, "input", "i");
const format = getStringFlag(args.flags, "format", "f") ?? "json";
const strict = hasFlag(args.flags, "strict");
const trustPaths = getStringFlagAll(args.flags, "trust");
if (format !== "json" && format !== "text") {
throw new CliError(`Invalid --format value "${format}". Valid: json, text.`, 2);
}
const trustPemBlocks = await loadPemChain("PDFNATIVE_VERIFY_TRUST", trustPaths);
const trustRoots = trustPemBlocks.length > 0 ? parseCertificateChain(trustPemBlocks) : [];
const inputBuf = await readFileOrStdin(inputPath);
const pdfBytes = new Uint8Array(inputBuf);
let reader;
try {
reader = openPdf(pdfBytes);
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
throw new CliError(`Failed to read PDF: ${message}`, 1);
}
const fields = findSignatureFields(reader);
const reports = [];
fields.forEach((field, idx) => {
const notes = [];
const sig = parseSignatureDict(field.sigDict);
let digest = null;
let integrity = false;
let signerSubject = null;
let signerIssuer = null;
let chainValid = false;
let trustedRoot = false;
if (sig.byteRange !== null) {
digest = digestByteRange(pdfBytes, sig.byteRange);
} else {
notes.push("missing /ByteRange");
}
if (sig.contents !== null) {
try {
const root = derDecode(sig.contents);
const certDers = extractCertsFromCms(sig.contents, root);
if (certDers.length === 0) {
notes.push("no certificates embedded in CMS");
} else {
const certs = certDers.map((der) => parseCertificate(der));
const leaf = certs[0];
signerSubject = nameToString(leaf.subject);
signerIssuer = nameToString(leaf.issuer);
const md = findMessageDigest(root);
if (md !== null && digest !== null) {
integrity = bytesToHex(md) === digest;
if (!integrity) {
notes.push("messageDigest mismatch \u2014 content tampered after signing");
}
} else {
notes.push("messageDigest attribute not found in CMS");
}
const pool = [...certs, ...trustRoots];
const built = buildChain(leaf, pool);
chainValid = built.chainValid;
if (!chainValid) {
notes.push("chain incomplete (no parent for an intermediate cert)");
}
if (trustRoots.length === 0) {
trustedRoot = isSelfSigned(built.root);
if (trustedRoot) {
notes.push("no --trust provided; accepted self-signed root");
}
} else {
trustedRoot = trustRoots.some((t) => certEquals(t, built.root));
if (!trustedRoot) notes.push("chain root not in --trust list");
}
}
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
notes.push(`failed to parse CMS: ${msg}`);
}
} else {
notes.push("missing /Contents");
}
reports.push({
index: idx,
fieldName: field.fieldName,
subFilter: sig.subFilter,
signerSubject,
signerIssuer,
signingTime: sig.signingTime,
reason: sig.reason,
location: sig.location,
digest,
integrity,
chainValid,
trustedRoot,
notes
});
});
const allValid = reports.length > 0 && reports.every((r) => r.integrity && r.chainValid && r.trustedRoot);
const result = { signatures: reports, allValid };
if (format === "json") {
process.stdout.write(JSON.stringify(result, null, 2) + "\n");
} else {
process.stdout.write(`Signatures: ${reports.length}
`);
for (const r of reports) {
process.stdout.write(
`
[${r.index}] field=${r.fieldName ?? "\u2014"} subFilter=${r.subFilter ?? "\u2014"}
signer: ${r.signerSubject ?? "\u2014"}
issuer: ${r.signerIssuer ?? "\u2014"}
signed at: ${r.signingTime ?? "\u2014"}
integrity: ${r.integrity ? "OK" : "FAIL"}
chain: ${r.chainValid ? "valid" : "invalid"}
trust: ${r.trustedRoot ? "trusted" : "untrusted"}
`
);
if (r.notes.length > 0) {
process.stdout.write(` notes: ${r.notes.join("; ")}
`);
}
}
process.stdout.write(
`
Result: ${allValid ? "all signatures valid" : "one or more checks failed"}
`
);
}
if (strict && !allValid) {
throw new CliError("", 1);
}
}
var MESSAGE_DIGEST_OID;
var init_verify = __esm({
"src/commands/verify.ts"() {
init_core_bridge();
init_args();
init_io();
init_error();
init_keys();
MESSAGE_DIGEST_OID = "1.2.840.113549.1.9.4";
}
});
// src/commands/inspect.ts

@@ -318,6 +1221,4 @@ var inspect_exports = {};

}
if (/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/.test(trimmed)) {
return null;
}
return trimmed || null;
if (/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/.test(trimmed)) return null;
return trimmed.length > 0 ? trimmed : null;
}

@@ -332,6 +1233,5 @@ function extractVersion(reader) {

function extractEncrypted(reader) {
const trailer = reader.trailer;
return trailer.get("Encrypt") !== void 0;
return reader.trailer.get("Encrypt") !== void 0;
}
function extractPdfaConformance(reader) {
function readXmp(reader) {
try {

@@ -348,10 +1248,15 @@ const catalog = reader.getCatalog();

);
const xmp = new TextDecoder("utf-8", { fatal: false }).decode(decoded);
const partMatch = /pdfaid:part[^>]*>(\d+)</.exec(xmp);
const confMatch = /pdfaid:conformance[^>]*>([A-Za-z]+)</.exec(xmp);
if (partMatch !== null && confMatch !== null) {
return `${partMatch[1]}${confMatch[1].toLowerCase()}`;
}
return new TextDecoder("utf-8", { fatal: false }).decode(decoded);
} catch {
return null;
}
}
function extractPdfaConformance(reader) {
const xmp = readXmp(reader);
if (xmp === null) return null;
const partMatch = /pdfaid:part[^>]*>(\d+)</.exec(xmp);
const confMatch = /pdfaid:conformance[^>]*>([A-Za-z]+)</.exec(xmp);
if (partMatch !== null && confMatch !== null) {
return `${partMatch[1]}${confMatch[1].toLowerCase()}`;
}
return null;

@@ -380,7 +1285,79 @@ }

}
function inspectPages(reader) {
const out = [];
for (let i = 0; i < reader.pageCount; i++) {
const page = reader.getPage(i);
const mediaBox = page.get("MediaBox");
let width = null;
let height = null;
const box = Array.isArray(mediaBox) ? mediaBox : null;
if (box !== null && box.length === 4) {
const w = box[2];
const h = box[3];
if (typeof w === "number") width = w;
if (typeof h === "number") height = h;
}
const rotation = typeof page.get("Rotate") === "number" ? page.get("Rotate") : 0;
const annots = page.get("Annots");
let annotations = 0;
let formFields = 0;
if (Array.isArray(annots)) {
for (const ref of annots) {
annotations++;
try {
const annot = reader.resolveValue(ref);
if (annot instanceof Map && annot.get("Subtype") === "/Widget") {
formFields++;
}
} catch {
}
}
}
out.push({ index: i, width, height, rotation, annotations, formFields });
}
return out;
}
function buildVerbose(reader) {
const trailerKeys = [];
for (const k of reader.trailer.keys()) trailerKeys.push(k);
const catalogKeys = [];
try {
for (const k of reader.getCatalog().keys()) catalogKeys.push(k);
} catch {
}
const objectCount = reader.xref?.entries?.size ?? 0;
const xmp = readXmp(reader);
return {
trailerKeys,
catalogKeys,
objectCount,
xmpMetadata: xmp
};
}
function evaluateChecks(checks, result) {
const out = [];
for (const c of checks) {
if (!VALID_CHECKS.has(c)) {
throw new CliError(
`Invalid --check value "${c}". Valid: ${[...VALID_CHECKS].join(", ")}.`,
2
);
}
if (c === "pdfa") out.push({ name: c, passed: result.pdfaConformance !== null });
if (c === "signed") out.push({ name: c, passed: result.signatures > 0 });
if (c === "encrypted") out.push({ name: c, passed: result.encrypted });
}
return {
checks: out.map((x) => `${x.name}=${x.passed ? "pass" : "fail"}`),
allPassed: out.every((x) => x.passed)
};
}
async function inspect(args) {
const inputPath = getStringFlag(args.flags, "input", "i");
const format = getStringFlag(args.flags, "format", "f") ?? "json";
const verbose = hasFlag(args.flags, "verbose");
const includePages = hasFlag(args.flags, "pages");
const checks = getStringFlagAll(args.flags, "check");
if (format !== "json" && format !== "text") {
throw new CliError(`Invalid --format value "${format}". Valid values: json, text.`, 2);
throw new CliError(`Invalid --format value "${format}". Valid: json, text.`, 2);
}

@@ -397,3 +1374,3 @@ const inputBuf = await readFileOrStdin(inputPath);

const info = reader.getInfo();
const result = {
const baseResult = {
version: extractVersion(reader),

@@ -412,2 +1389,7 @@ pageCount: reader.pageCount,

};
const result = {
...baseResult,
...includePages ? { pages: inspectPages(reader) } : {},
...verbose ? { verbose: buildVerbose(reader) } : {}
};
if (format === "json") {

@@ -428,5 +1410,30 @@ process.stdout.write(JSON.stringify(result, null, 2) + "\n");

];
if (result.pages !== void 0) {
lines.push("Pages detail:");
for (const p of result.pages) {
lines.push(
` #${p.index + 1}: ${p.width ?? "?"}x${p.height ?? "?"}pt rot=${p.rotation}\xB0 annots=${p.annotations} fields=${p.formFields}`
);
}
}
if (result.verbose !== void 0) {
lines.push(`Trailer keys: ${result.verbose.trailerKeys.join(", ")}`);
lines.push(`Catalog keys: ${result.verbose.catalogKeys.join(", ")}`);
lines.push(`Object count: ${result.verbose.objectCount}`);
if (result.verbose.xmpMetadata !== null) {
lines.push(`XMP metadata: (${result.verbose.xmpMetadata.length} chars)`);
}
}
process.stdout.write(lines.join("\n") + "\n");
}
if (checks.length > 0) {
const evaluation = evaluateChecks(checks, result);
if (!evaluation.allPassed) {
process.stderr.write(`check failed: ${evaluation.checks.join(", ")}
`);
throw new CliError("", 1);
}
}
}
var VALID_CHECKS;
var init_inspect = __esm({

@@ -438,2 +1445,3 @@ "src/commands/inspect.ts"() {

init_error();
VALID_CHECKS = /* @__PURE__ */ new Set(["pdfa", "signed", "encrypted"]);
}

@@ -452,7 +1460,8 @@ });

render Render a JSON document definition to PDF
sign Apply a digital signature to an existing PDF
sign Apply a digital signature to a PDF
verify Verify embedded PDF signatures
inspect Analyse a PDF and output metadata / conformance info
Options:
--help, -h Show this help message
--help, -h Show this help message
--version, -V Show version

@@ -462,15 +1471,51 @@

`;
var RENDER_USAGE = `pdfnative render \u2014 Render a JSON DocumentParams to PDF
var RENDER_USAGE = `pdfnative render \u2014 Render a JSON document definition to PDF
Usage:
pdfnative render [--input <file.json>] [--output <out.pdf>] [--stream] [--conformance <level>]
pdfnative render [--input <file>] [--output <out.pdf>] [options]
Options:
--input, -i Path to JSON input file (default: stdin)
I/O:
--input, -i Path to JSON input (default: stdin)
--output, -o Output PDF path (default: stdout)
--stream Use streaming output for large documents
--conformance PDF/A conformance level: 1b, 2b, or 3b
--stream Stream output (large documents). Incompatible with TOC blocks
and with header/footer templates that contain {pages}.
Variant:
--variant document (default) or table
Layout (flags override values from --layout file):
--layout Path to JSON layout file (PdfLayoutOptions)
--page-size Named (a4|letter|legal|a3|tabloid|a5) or WxH in points
--margin Uniform N or "top,right,bottom,left" in points
--tagged none|pdfa1b|pdfa2b|pdfa3b (PDF/A flag, sets PDF/A conformance)
--conformance DEPRECATED \u2014 alias for --tagged pdfa{1b|2b|3b}
--compress Enable Flate compression (initialises Node compression)
--lang Comma-separated language packs (e.g. th,ja,ar)
Header / Footer:
--header-left, --header-center, --header-right
--footer-left, --footer-center, --footer-right
Each accepts a template string. {page}, {pages}, {date} are
substituted by pdfnative.
Watermark:
--watermark-text Text watermark
--watermark-image Image path (PNG/JPEG)
--watermark-opacity 0.0\u20131.0 (default 0.2)
--watermark-rotation degrees (default 45)
Encryption (mutually exclusive with --tagged pdfa*):
--encrypt aes-128 | aes-256
--owner-password (or env $PDFNATIVE_ENCRYPT_OWNER_PASS \u2014 env wins)
--user-password (or env $PDFNATIVE_ENCRYPT_USER_PASS \u2014 env wins)
--permissions Comma-separated: print,copy,modify,annotate,form,
accessibility,assemble,print-hi-res
Attachments (PDF/A-3, repeatable):
--attachment <path>[:mime[:rel[:desc]]]
rel = Source|Data|Alternative|Supplement|Unspecified
--help, -h Show this help message
`;
var SIGN_USAGE = `pdfnative sign \u2014 Apply a digital signature to an existing PDF
var SIGN_USAGE = `pdfnative sign \u2014 Apply a digital signature to a PDF

@@ -480,12 +1525,48 @@ Usage:

Options:
I/O:
--input, -i Path to input PDF (default: stdin)
--output, -o Signed PDF output path (default: stdout)
--key Path to PEM private key file
(overridden by $PDFNATIVE_SIGN_KEY env var)
--cert Path to PEM certificate file
(overridden by $PDFNATIVE_SIGN_CERT env var)
Credentials (env wins over file flags):
--key Path to PEM private key (env: PDFNATIVE_SIGN_KEY)
--cert Path to PEM signer certificate (env: PDFNATIVE_SIGN_CERT)
--cert-chain Path to PEM intermediate (repeatable; env: PDFNATIVE_SIGN_CHAIN)
Algorithm:
--algorithm rsa-sha256 (default). ecdsa-sha256 not yet wired (pdfnative
does not yet expose parseEcPrivateKey).
Signature metadata (optional):
--reason Reason text shown in signature panel
--name Signer name override
--location Signing location
--contact Contact info
--signing-time ISO 8601 timestamp (default: now)
Security: key material is never written to logs or error messages.
--help, -h Show this help message
`;
var VERIFY_USAGE = `pdfnative verify \u2014 Verify CMS/PKCS#7 signatures in a PDF
Security: $PDFNATIVE_SIGN_KEY and $PDFNATIVE_SIGN_CERT env vars take precedence over file flags.
Usage:
pdfnative verify [--input <file.pdf>] [--trust <root.pem>]... [--strict] [--format json|text]
Options:
--input, -i Path to input PDF (default: stdin)
--trust PEM file with trusted root certs (repeatable;
env: PDFNATIVE_VERIFY_TRUST). When omitted, self-signed
roots are accepted.
--strict Exit code 1 if any signature fails any check.
--format, -f json (default) or text
--help, -h Show this help message
Reported per signature:
- byte-range integrity (SHA-256 against CMS messageDigest)
- signer subject / issuer
- certificate chain validity
- chain root trust evaluation
Out of scope (v0.2.0): full CMS-signature-value verification, OCSP/CRL,
RFC 3161 timestamps, LTV. These require future pdfnative API additions.
`;

@@ -495,7 +1576,12 @@ var INSPECT_USAGE = `pdfnative inspect \u2014 Analyse a PDF and output metadata

Usage:
pdfnative inspect [--input <file.pdf>] [--format <fmt>]
pdfnative inspect [--input <file.pdf>] [--format <fmt>] [options]
Options:
--input, -i Path to input PDF (default: stdin)
--format, -f Output format: json (default) or text
--format, -f json (default) or text
--verbose, -v Include trailerKeys, catalogKeys, objectCount,
XMP metadata length
--pages Per-page width/height/rotation/annotation/formField counts
--check Assert a property; repeatable; AND semantics; exits 1 on
failure. Values: pdfa | signed | encrypted
--help, -h Show this help message

@@ -518,2 +1604,6 @@ `;

}
case "verify": {
const m = await Promise.resolve().then(() => (init_verify(), verify_exports));
return m.verify;
}
case "inspect": {

@@ -524,3 +1614,5 @@ const m = await Promise.resolve().then(() => (init_inspect(), inspect_exports));

default:
return Promise.reject(new CliError(`Unknown command: ${name}. Run pdfnative --help for usage.`, 1));
return Promise.reject(
new CliError(`Unknown command: ${name}. Run pdfnative --help for usage.`, 1)
);
}

@@ -552,2 +1644,5 @@ }

break;
case "verify":
process.stdout.write(VERIFY_USAGE);
break;
case "inspect":

@@ -563,8 +1658,11 @@ process.stdout.write(INSPECT_USAGE);

}
const commandArgs = parseArgs(argv.filter((_t, _i) => {
if (_t === commandName && args.positionals[0] === commandName) {
let stripped = false;
const rest = argv.filter((tok) => {
if (!stripped && tok === commandName) {
stripped = true;
return false;
}
return true;
}));
});
const commandArgs = parseArgs(rest);
const command = await loadCommand(commandName);

@@ -575,3 +1673,5 @@ await command(commandArgs);

if (e instanceof CliError) {
process.stderr.write(e.message + "\n");
if (e.message.length > 0) {
process.stderr.write(e.message + "\n");
}
process.exit(e.exitCode);

@@ -578,0 +1678,0 @@ }

+3
-3
{
"name": "pdfnative-cli",
"version": "0.1.0",
"description": "Official CLI for pdfnative — render JSON to PDF, sign, and inspect. Zero extra runtime dependencies.",
"version": "0.2.0",
"description": "Official CLI for pdfnative — render JSON to PDF, sign, inspect, and verify. Zero extra runtime dependencies.",
"type": "module",

@@ -66,3 +66,3 @@ "bin": {

"dependencies": {
"pdfnative": "^1.0.4"
"pdfnative": "^1.0.5"
},

@@ -69,0 +69,0 @@ "devDependencies": {

+99
-28

@@ -14,15 +14,24 @@ # pdfnative-cli

Official CLI for the [`pdfnative`](https://github.com/Nizoka/pdfnative) library — render JSON to PDF, apply digital signatures, and inspect PDF conformance, directly from the terminal. Zero extra runtime dependencies.
Official CLI for the [`pdfnative`](https://github.com/Nizoka/pdfnative) library — render JSON to PDF, apply digital signatures, verify them, and inspect PDF conformance, directly from the terminal. Zero extra runtime dependencies.
> **What's new in v0.2.0** — full coverage of the `pdfnative` v1.0.5 surface: encryption, watermarks, headers/footers with placeholders, PDF/A-3 attachments, multilingual fonts, table-variant rendering, signing metadata + cert chains, `inspect --verbose / --pages / --check`, and a brand-new `verify` command. **100 % backward-compatible** with v0.1.0 — see [release notes](release-notes/v0.2.0.md).
## Highlights
- **`render`** — pipe a JSON document definition into a production-ready PDF (streaming supported)
- **`sign`** — apply RSA or ECDSA digital signatures (CMS/PKCS#7) using key files or environment variables
- **`inspect`** — analyse any PDF: version, page count, encryption, PDF/A conformance, signature count, metadata
- **Zero extra dependencies** — `pdfnative` is the only runtime dependency; all PDF logic lives there
- **Streaming output** — `--stream` on render emits PDF chunks progressively for large documents
- **Stdin / stdout** — every command reads from stdin and writes to stdout by default, composable in shell pipelines
- **Secret-safe** — signing keys are loaded from env vars (`PDFNATIVE_SIGN_KEY`/`PDFNATIVE_SIGN_CERT`) and never logged
- **ESM-first, TypeScript strict** — built with tsup, typed declarations included
- **NPM provenance** — signed builds via GitHub Actions OIDC
- **`render`** — pipe a JSON document into a production-ready PDF. Encryption (AES-128/256),
watermarks (text + image), page templates, PDF/A archival, multilingual fonts, streaming,
and a hybrid `flags + --layout file.json` model for the full `PdfLayoutOptions` surface.
- **`sign`** — CMS/PKCS#7 digital signatures with full metadata (`--reason`, `--name`,
`--location`, `--contact`, `--signing-time`) and intermediate CA chains via
`--cert-chain` (repeatable). Keys loaded from env vars or files; never logged.
- **`inspect`** — PDF version, page count, encryption, PDF/A conformance, signature count,
metadata. `--verbose`, `--pages`, and `--check pdfa|signed|encrypted` for CI assertions.
- **`verify`** _(new in v0.2.0)_ — verify integrity, certificate chains, and trust roots
of every CMS/PKCS#7 signature embedded in a PDF. JSON & text output, `--strict` mode.
- **Zero extra dependencies** — `pdfnative` is the sole runtime dependency.
- **Stdin / stdout by default** — every command is shell-pipeline friendly.
- **Secret-safe** — signing keys, certs, encryption passwords never appear in error
output or stderr. PEM material redacted; layout-file `attachments[].data` injection blocked.
- **ESM-first, TypeScript strict** — built with tsup, typed declarations included.
- **NPM provenance** — signed builds via GitHub Actions OIDC.

@@ -34,5 +43,6 @@ ## Supported Features

| **Commands** | | |
| `render` JSON → PDF | ✅ | Streaming, PDF/A conformance, stdin/stdout |
| `sign` digital signatures | ✅ | RSA + ECDSA, CMS/PKCS#7, env var secrets |
| `inspect` PDF metadata | ✅ | Version, pages, encryption, signatures, PDFA |
| `render` JSON → PDF | ✅ | Streaming, hybrid layout model, multilingual fonts |
| `sign` digital signatures | ✅ | RSA (CMS/PKCS#7), metadata fields, cert chains |
| `inspect` PDF metadata | ✅ | `--verbose`, `--pages`, `--check pdfa\|signed\|encrypted` |
| `verify` signature verification (v0.2.0) | ✅ | Integrity + chain + trust; `--strict`, `--trust` |
| **Document Blocks** | | |

@@ -45,13 +55,31 @@ | Headings, paragraphs, lists | ✅ | Full text styling support |

| Page breaks, spacers | ✅ | Explicit pagination control |
| Table of contents | ✅ | Auto-generated with /GoTo links |
| **Advanced Layouts** | | |
| PDF/A archival (1b, 2b, 3b) | ✅ | `--conformance` flag |
| Table of contents | ✅ | Auto-generated with `/GoTo` links |
| **Advanced Layouts (v0.2.0)** | | |
| PDF/A archival (1b, 2b, 2u, 3b) | ✅ | `--tagged pdfa<level>` (preferred) or `--conformance` (deprecated) |
| Streaming output | ✅ | `--stream` for large documents |
| Compression | ✅ | Via pdfnative API (50–90% reduction) |
| Encryption (AES-128/256) | ⚠️ | Not exposed via CLI; use Node.js API |
| Watermarks | ⚠️ | Not exposed via CLI; use Node.js API |
| Custom headers/footers | ⚠️ | Not exposed via CLI; use `footerText` property |
| Custom page sizes | ⚠️ | Not exposed via CLI; use Node.js API |
| Compression | ✅ | `--compress` flag |
| Encryption (AES-128/256) | ✅ | `--encrypt-*` flags + env-var precedence |
| Watermarks (text + image) | ✅ | `--watermark-text`, `--watermark-image`, `--watermark-position` |
| Headers / footers with placeholders | ✅ | `--header-{l,c,r}`, `--footer-{l,c,r}`, `{page}/{pages}/{date}/{title}` |
| Custom page sizes | ✅ | `--page-size A4\|Letter\|…` or `WxH` in points |
| Custom margins | ✅ | `--margin <N>` or `--margin <t,r,b,l>` |
| PDF/A-3 attachments | ✅ | `--attachment <path>:<mime>:<rel>:<desc>` (repeatable) |
| Multilingual fonts | ✅ | `--lang th,ja,ar` (requires `registerFontLoader()` in wrapper; Latin built-in) |
| Table-centric variant (`PdfParams`) | ✅ | `--variant table` |
| Full `PdfLayoutOptions` | ✅ | `--layout <file.json>` |
| **Signing (v0.2.0)** | | |
| RSA signatures (rsa-sha256) | ✅ | Default algorithm |
| ECDSA signatures | ⚠️ | `--algorithm ecdsa-sha256` parsed; stub error pending pdfnative `parseEcPrivateKey` (v0.3.0) |
| Signature metadata | ✅ | `--reason`, `--name`, `--location`, `--contact`, `--signing-time` |
| Cert chains (intermediate CAs) | ✅ | `--cert-chain <pem>` (repeatable) or `PDFNATIVE_SIGN_CHAIN` env |
| **Verification (v0.2.0)** | | |
| Byte-range integrity (SHA-256) | ✅ | Recomputed and compared with CMS messageDigest attribute |
| Certificate chain verification | ✅ | Via pdfnative `verifyCertSignature` |
| Trust roots | ✅ | `--trust <root.pem>` (repeatable) + self-signed acceptance |
| Full CMS signature-value | ⚠️ | Deferred to v0.3.0 (pending pdfnative API) |
| OCSP / CRL revocation | ⚠️ | Deferred to v0.3.0+ |
| RFC 3161 timestamps | ⚠️ | Deferred to v0.3.0+ |
**Note:** Features marked **⚠️** are supported by `pdfnative` but not yet exposed through the CLI JSON interface. Use the `pdfnative` Node.js library directly for these features.
**Note:** features marked **⚠️** are tracked in [ROADMAP.md](ROADMAP.md). Everything else
works today.

@@ -191,7 +219,26 @@ ## Installation

|------|---------|-------------|
| `--input <file>` | stdin | Path to a JSON file containing `DocumentParams` |
| `--input <file>` | stdin | Path to a JSON file (`DocumentParams` or `PdfParams` if `--variant table`) |
| `--output <file>` | stdout | Output PDF path |
| `--stream` | false | Use streaming output (AsyncGenerator) |
| `--conformance <level>` | — | PDF/A conformance level: `1b`, `2b`, or `3b` |
| `--stream` | false | Use streaming output (`AsyncGenerator`) |
| `--variant <kind>` | `document` | `document` (default) or `table` (selects `buildPDFBytes`) |
| `--layout <file.json>` | — | Load a `Partial<PdfLayoutOptions>` (CLI flags override) |
| `--page-size <size>` | from layout file or pdfnative default | Named (`a4`, `letter`, `legal`, `a3`, `tabloid`, `a5`) or `WxH` in points |
| `--margin <N>` or `--margin <t,r,b,l>` | from layout / default | Page margins in points |
| `--compress` | false | Enable FlateDecode compression |
| `--tagged <level>` | none | PDF/A: `none`, `pdfa1b`, `pdfa2b`, `pdfa2u`, `pdfa3b` |
| `--conformance <1b\|2b\|3b>` | — | **Deprecated** — use `--tagged pdfa<level>` |
| `--watermark-text <s>` / `--watermark-image <path>` | — | Text or image watermark |
| `--watermark-opacity <0-1>` / `--angle <deg>` / `--color <#hex>` / `--font-size <pt>` | — | Watermark styling |
| `--watermark-position background\|foreground` | `background` | Render order |
| `--header-{left,center,right} <tpl>` | — | Header template; placeholders `{page}`, `{pages}`, `{date}`, `{title}` |
| `--footer-{left,center,right} <tpl>` | — | Footer template; same placeholders |
| `--encrypt-owner-pass <s>` | `$PDFNATIVE_ENCRYPT_OWNER_PASS` | Owner password (required for any `--encrypt-*`) |
| `--encrypt-user-pass <s>` | `$PDFNATIVE_ENCRYPT_USER_PASS` | Optional user password |
| `--encrypt-algorithm aes128\|aes256` | `aes128` | Encryption algorithm |
| `--encrypt-permissions <list>` | _all denied_ | Comma list: `print,copy,modify,extractText` |
| `--attachment <path>[:mime[:rel[:desc]]]` _(repeatable)_ | — | PDF/A-3 file attachment |
| `--lang <code,code>` | — | Activate registered font loaders for non-Latin scripts (`th`, `ja`, `ar`, …); Latin is built-in |
See `samples/render/` for a working example of every category.
### `pdfnative sign`

@@ -203,4 +250,11 @@

| `--output <file>` | stdout | Output signed PDF path |
| `--key <file>` | `PDFNATIVE_SIGN_KEY` env | Path to PEM private key (env var takes precedence) |
| `--cert <file>` | `PDFNATIVE_SIGN_CERT` env | Path to PEM certificate (env var takes precedence) |
| `--key <file>` | `$PDFNATIVE_SIGN_KEY` | Path to PEM private key (env var takes precedence) |
| `--cert <file>` | `$PDFNATIVE_SIGN_CERT` | Path to PEM certificate (env var takes precedence) |
| `--cert-chain <file>` _(repeatable)_ | `$PDFNATIVE_SIGN_CHAIN` | Intermediate CA PEMs |
| `--algorithm rsa-sha256\|ecdsa-sha256` | `rsa-sha256` | Signature algorithm. _ECDSA stubbed in v0.2.0; tracked for v0.3.0._ |
| `--reason <s>` | — | Reason for signing (PDF metadata) |
| `--name <s>` | — | Signer name (PDF metadata) |
| `--location <s>` | — | Signing location (PDF metadata) |
| `--contact <s>` | — | Signer contact (PDF metadata) |
| `--signing-time <ISO 8601>` | now | Explicit signing timestamp |

@@ -212,4 +266,21 @@ ### `pdfnative inspect`

| `--input <file>` | stdin | Path to the PDF to inspect |
| `--format <fmt>` | `json` | Output format: `json` or `text` |
| `--output <file>` | stdout | Output report path |
| `--format json\|text` | `json` | Output format |
| `--verbose` | false | Add trailer keys, catalog keys, object count, XMP |
| `--pages` | false | Add per-page metadata array |
| `--check pdfa\|signed\|encrypted` _(repeatable)_ | — | CI-friendly assertion; sets exit code (0 = pass, 1 = fail) |
### `pdfnative verify` _(new in v0.2.0)_
| Flag | Default | Description |
|------|---------|-------------|
| `--input <file>` | stdin | Path to the (possibly signed) PDF |
| `--format json\|text` | `json` | Output format |
| `--strict` | false | Exit 1 on any failure or zero signatures (CI-friendly) |
| `--trust <root.pem>` _(repeatable)_ | _self-signed only_ | Trusted root certificates (PEM) |
**Scope (v0.2.0):** integrity (byte-range SHA-256) + certificate chain signatures + trust
evaluation. Full CMS-signature-value verification, OCSP/CRL revocation, and RFC 3161
timestamp validation are deferred — see [ROADMAP.md](ROADMAP.md).
## Security

@@ -216,0 +287,0 @@

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display