Format: Keep a Changelog.
Spec reference: ISO 32000-2:2020 (PDF 2.0). Legacy read tolerance: ISO 32000-1:2008 (PDF 1.7).
[Unreleased]
[1.0.0] - 2026-10-07
Added
-
Core surface (L0) — typed object graph + classical xref + page-tree walker.
pdfErrorsexposesPdfError,ParseError,RenderError,ContractError,EncryptionError; every throw in the package uses these classes with a kebab-case, namespacedcodeand a structuredcontext.pdfTokenizer(binary lexer, ISO 32000-2 §7.2),pdfParser/pdfParserObj/pdfParserStream(typed object parser, §7.3 —null/bool/int/real/name/string/array/dict/ref/stream),pdfXref(classical xref, §7.5.4),pdfTrailer,pdfCatalog(§7.7.2),pdfPages(balanced page-tree walker, cycle + depth detection),pdfPage(§7.7.3.3),pdfDocument(top-level reader,/Prevxref chaining). Top-levelpdffactory exposes.read(),.header(),.use(...)(idempotent by name),.usedExtension(),.write(). -
Writer + filters (L1) —
pdfSerializer(typed-object → bytes, canonical real-number formatting,#xxname escapes, automatic hex form for binary strings),pdfWriter(writeDocument(...)emits the header it is given,%PDF-2.0by default),pdfBuilder(constructive DSL —addPage/addContent/addFont/addMetadata/setVersion/setId/.build()— builds a document from-scratch with nopdf.read()upstream). Five filters:pdfFlate(delegates to@awacloud/fw/io/compress/zlib, full/PredictorPNG (10–15, incl. optimum) + TIFF (2) encode and decode),pdfAsciiHex,pdfAscii85(incl.zshorthand +~>EOD),pdfRunLength,pdfFilterDispatch(/Filter+/DecodeParmschain walker, abbreviation table, DCT/JPX passthrough). Compressed-object readerspdfObjStream(§7.5.7) andpdfCrossRefStream(§7.5.8). -
Content streams + fonts + AcroForm (L2) —
pdfContentStream(~70 operators per ISO 32000-2 Table 60, inline-imageBI…ID…EIcapture),pdfContentOps(operator catalogue),pdfGraphics(GStateStack,q/Q, CTM),pdfText(Td/TD/Tm/T* tracking,extractText),pdfColor,pdfImages(XObject typing),pdfResources(page-tree parent-chain resolution). Font glue to@awacloud/fonts:pdfFont(every ISO subtype),pdfFontEncoding,pdfType3,pdfFontEmbed(single adapter to@awacloud/fonts/embed-pdffor subset embedding). AcroForm baseline:pdfAcroForm,pdfFieldTree,pdfButtonField,pdfTextField,pdfChoiceField,pdfSignatureField,pdfAppearance. -
Annotations, tagged PDF, OCG, outlines/actions/destinations, embedded files, linearization, metadata, prepress, associated files (L3) —
pdfAnnotorchestrator + 12 subtype typers (Text, Link, FreeText, shape family viapdfShapeAnnot, markup family viapdfMarkupAnnot, Ink, Stamp, FileAttachment, Widget, Popup, Projection, Redact — ISO 32005).pdfStructTree/pdfStructElement/pdfRoleMap/pdfParentTree/pdfClassMap/pdfMarkedContent(StructTreeRoot walking, heterogeneous/Kchildren, RoleMap + Namespaces per Table 364–365, MCID resolution + extraction).pdfOCG/pdfOCConfig.pdfOutline,pdfDestination,pdfAction+ GoTo/GoToR/GoToE/URI/ Named/Launch typers.pdfFileSpec/pdfEmbeddedFile/pdfCollection(PDF Portfolio).pdfLinearization(types the/Linearizedparameter dictionary, read-only).pdfInfo/pdfXmp.pdfOutputIntent/pdfPageBoundary(PDF/A, PDF/X, PDF/E; effective Crop/Trim/Bleed/Art box resolution).pdfAssociatedFiles(/AF+/AFRelationship, PDF 2.0). -
Encryption, decrypt + encrypt (L3) —
pdfSecurity(Security Handler dispatcher),pdfStandardV4(PDF 1.6/ISO 32000-1 §7.6.3 — AESV2 AES-128-CBC and V2 RC4-128,/O//Uderivation, per-object key derivation, string/stream/embedded-file encrypt and decrypt round-trip),pdfStandardV5(PDF 1.7 — AES-256-CBC + SHA-256),pdfStandardV6(PDF 2.0 — Algorithm 2.B hardening loop + Algorithm 8 FEK unwrap, SHA-256/384/512 selection),pdfPermissions(/PermsAlgorithm 13),pdfAesGcm(TS 32003 AES-GCM crypt filter, decode and encode),pdfEncryptedWriter(writeDocument-style encrypted write path — V4/V5 R=5/V6 R=6,/CFM AESV4GCM opt-in). All crypto via@awacloud/fw/crypto/*(pure JS, worker-safe).readdoes not decrypt: it refuses an encrypted file unlessallowEncrypted: trueis passed, and the handlers are then called by the caller. -
Digital signatures, verify + generate —
pdfSignature/pdfByteRange/pdfTimestamp/pdfCertChain/pdfDssBuilder: Sig/DocTimeStamp typing, PKCS#7 detached blob locator, ByteRange compute/extract, RFC 3161 TSP parser, X.509 cert chain extraction (CMS SignedData + PEM), structural chain ordering check, DSS dict typing.verifySignature(...)performs full public-key verification —verifyPkwires RSA-PSS (PKCS#1 v1.5 refused per fw policy), ECDSA (P-256/P-384/P-521), Ed25519; the result carriesverified: true(aliasvalid),pkVerified: true,computedDigestwhen the digest chain and PK-verify both succeed — the digest-only structural pass this superseded is gone (see Security).pdfSign.sign(pdfBytes, opts)generates PAdES signatures at all four levels: B (single embedded PKCS#7, no TSA), T (adds an RFC 3161 timestamp token via a caller-suppliedopts.tsaSign({ digest, hashAlg })callback), LT (appends a/DSSdict with certs viapdfIncrementalWriter), LTA (appends a/DocTimeStampon top of the LT bytes).verifyAllSignatures/validateBLtaChainround-trip a full B→T→LT→LTA chain end-to-end (tsaVerified/imprintVerifiedon the reconstructedDocTimeStamp).validateDocMdp(ref)validates MDP/P1/2/3 +/DigestMethod+/V1.2/2.2tolerance. Algorithms: RSA-PSS, ECDSA, Ed25519 across both verify and sign. -
Constructive + incremental writers —
pdfIncrementalWriter.appendIncremental(pdfBytes, updates)emitsoriginalBytes ‖ updatedObjs ‖ xref ‖ trailer ‖ %%EOFwith a/Prevchain (the mechanismpdfSign's LT/LTA levels build on).pdfXrefStreamWriter(src/document/xrefStreamWriter.js) implementswriteXrefStreamDocument(model, opts)— native/Type /XRefemission (PDF 1.5+/2.0) with an opt-inuseObjStmgrouping non-stream indirects into compressed/Type /ObjStmwrappers (§7.5.7). It is registered insrc/main.jsmodules, re-exported by name from the root entry, and ships in every Read+Writedist/root. -
Parser hardening —
parserLimits(maxDepth: 200,maxArrayLen: 1_000_000,maxStreamBytes: 256 MiB), adjustable at runtime viasetParserLimits(partial);findEndstreamaccepts an explicitmaxScanBytes. -
Shared helper factory
pdfShared(src/_shared/index.js) — canonical magic bytes (HEADER_PREFIX,EOF_MARKER,BINARY_MARKER), frozenASCIIbyte-constant table, character-class predicates per §7.2 (isWs/isEol/isDigit/isHex/isDelim/isRegular/hexNibble), theHEX_LOlookup table, codec instances and wrappers (encodeAscii/decodeUtf8/decodeUtf8Lenient/decodeLatin1), byte helpers (pad10/hexLit/bytesEqual/concatBytes).pdfTokenizerand the top-levelpdfdeclare it as a dependency; the filters, the writer and the serializer still carry their own inline copies of some of these helpers.pdfSigOidsconsolidates the OID → dispatch-label tables (DIGEST_OIDS,SIG_DISPATCH_OIDS,KEY_ALG_OIDS,SIG_ALG_OIDS_VERBOSE,OID_TST_INFO,OID_AA_TIMESTAMP,shortOid) previously duplicated acrosssignature.js/timestamp.js/certChain.jsinto one factory, which all three now declare as a dependency. -
Coverage extras (opt-in, the
extrasarray ofsrc/main.js— 32 modules undersrc/extra/— tree-shaken when unused) reach the PDF 2.0 long-tail + PDF 1.7 read tolerance: P0 (common) —content-ops-extended,font-cid-typed,font-color-tagging,tagged-pdf-typed,annot-extended,pdf-a-output-intent,pdf-ua-tagged. P1 (extended) —form-actions-extended,color-spaces-extended,shading-typed,transparency-typed,sig-pades,sig-aes-gcm,document-parts(ISO TS 32004),redaction-iso32005,pdf-x-prepress,well-tagged-pdf(WTPDF 1.0),optional-content-extended,embedded-files-portfolio,associated-files,xmp-extended. P2 (tail) —linearization-write(builds the/Linearizeddictionary and a zero-length hint-stream placeholder; it does not produce a linearized file),3d-richmedia,jbig2-read(segment-header enumeration, decode intentionally not implemented),legacy-xfa-read,legacy-rc4-read(RC4 known-answer-test verified),legacy-deprecated-filters(LZWDecode via fw + DCT/JPX passthrough; CCITTFax delegates to the siblingccitt-fax-decodercodec — see below),legacy-deprecated-annots(Sound/Movie/Screen typing). P3 (misc) —misc(SpiderInfo/Threads/Legal/Requirements/NeedsRendering),info-dict-deprecated(Info dict lint). Plus two modules added after the initial P0–P3 pass:pdf-sandbox(pdfSandbox.lintActions(...)— classifies/Launch//JavaScript//SubmitForm//ImportData//URI, bundled opt-in viapdf-full) andccitt-fax-decoder(a full ITU-T T.4/T.6 codec, encode + decode, for K<0 Group 4, K=0 Group 3 1D, K>0 Group 3 mixed — split out once the original header-only stub was completed; consumed bylegacy-deprecated-filtersas a dependency, and covered by both suites' tests). The legacy decoders are called through their extras' own API; they are not registered intopdfFilterDispatch. -
Bundles — three ergonomic compositions of core + extras, each a pure fw descriptor consumed via
ModuleRuntime.resolve(...):pdfLargeBundle(@awacloud/pdf/pdf-large, P0+P1, the common PDF 2.0 features),pdfFullBundle(@awacloud/pdf/pdf-full, +P2+P3, every PDF 2.0 extra),pdfLegacyBundle(@awacloud/pdf/pdf-legacy, +legacy-*family, reads PDF 1.7;writestill emits a%PDF-2.0header and re-emits legacy content — XFA, RC4 encryption, LZW streams, Sound/Movie annotations — as it was read, without converting it). Each resolved bundle exposes.read,.write,.use,.usedExtension,.header, and every wired extra under its factory name; re-applying a bundle is a no-op. Seedocs/api/bundles/README.mdfor the per-bundle extras breakdown. -
Pre-built bundles — two-surface
dist/, Read and Read+Write families.tools/generate-bundles.mjs(bun run gen:bundles, a thin wrapper around@awacloud/tool-prebuild-generator) emits two path-discriminated surfaces side by side underdist/:dist/standalone/<root>. {js,min.js,meta.json}(dependencies: [], every fw + pdf-local factory inlined, zero runtime registration) anddist/build/<root>.{js,min.js,meta.json}(declares the fw modules as dependencies, inlines only the pdf-local factories, smallest payload). The roots form a two-family × size matrix: the four assembly roots (pdf,pdf-large,pdf-full,pdf-legacy) are the Read family, and four-rwroots (pdf-rw,pdf-large-rw,pdf-full-rw,pdf-legacy-rw) form the Read+Write family — each the Read root's segment plus the write inventory, withpdfXrefStreamWritershipping in every-rwroot — on both surfaces, 8 roots × 2 surfaces. Strictly additive: adding the-rwroots left the four Read roots unchanged.dist/build/index.jsis a barrel re-exporting the whole@awacloud/pdfnamespace.package.jsonexposes./build/*and./standalone/*. Output is byte-deterministic across runs (no build stamp); the root.gitignore's blanketdist/exclusion is re-included via the package's own.gitignore(!dist/+!dist/**). Seedocs/api/bundles/dist-matrix.md. -
Documentation —
docs/README.mdtop-level index;docs/api/one page per source module (core,_shared/,extra/,bundles/), following the@awacloud/fwmodule-page format;docs/guide/— getting started, read pipeline, extension hook, coverage (withparserLimits), crypto (AES-CBC malleability risk,verifiedvsvalidsemantics,auditByteRange,pdfSandbox), PDF 1.7 legacy reading, and PAdES signing and verification. -
Tests + integration — one sibling test file per source module, co-located in
src/; integration suite undertests/:roundtrip.integration.test.js(wires the full stack through@awacloud/fwModuleRuntime, read → write → read on fixtures sized 1–10 pages, everyfactory.toString()transportability assertion),legacy-conversion.test.js(%PDF-1.xheader tolerance),fuzz.test.js(empty/garbage/truncated/no-xref/bad-header inputs → typedPdfError, never a bareError),_helpers/build.js(shared fixture builder). The package's coverage floor isawa.coverageFloorinpackage.json. -
Architecture — clean binary format (no ZIP;
%PDF-2.0header + indirect objects + xref + trailer); worker-safe factories (factory.toString()serializable, no closure on mutable module-level state — see Changed); zero external dependency beyond@awacloud/fw+@awacloud/fonts(both workspace); content stream parsed to an operator list, not interpreted (rendering / coordinate math is left to the caller); fonts always delegated to@awacloud/fonts(even standard-14 metrics); AcroForm field inheritance resolved at lookup time, never collapsed onto children, to preserve roundtrip fidelity; bundle composition is layered (pdf-full ⊃ pdf-large,pdf-legacy ⊃ pdf-full). -
Package surface —
package.jsonexposes:. src/main.js 5 descriptor arrays + every core descriptor by name ./pdf src/pdf.js top-level orchestrator only ./errors src/errors.js pdfErrors ./serializer src/syntax/serializer.js ./filters/* src/syntax/filters/*.js ./annot/* src/annot/*.js ./tagged/* src/tagged/*.js ./crypto/* src/crypto/*.js ./sig/* src/sig/*.js ./ocg/* src/ocg/*.js ./action/* src/action/*.js ./embedded/* src/embedded/*.js ./metadata/* src/metadata/*.js ./prepress/* src/prepress/*.js ./extra/* src/extra/*.js the opt-in extras ./bundles/* src/bundles/*.js 3 compositions ./pdf-large src/bundles/pdf-large.js ./pdf-full src/bundles/pdf-full.js ./pdf-legacy src/bundles/pdf-legacy.js ./build/* dist/build/* two-surface prebuilt (fw-DI variant) ./standalone/* dist/standalone/* two-surface prebuilt (framework-free)Note:
src/document/*(writer, builder, incremental/xref-stream writers, catalog, pages, resources) has no dedicated subpath export — reachable only through the root entry's additive named re-exports (below).awa.maturity: "L4"(the initial core surface shipped at L0, then progressed L0 → L1 → L2 → L3 → L4 through the factory-only refactor and the read/write gap closure). -
addFontencodingoption. A non-embeddedaddFontspec accepts an optionalencoding—WinAnsiEncoding,MacRomanEncodingorStandardEncoding— emitted as/Encoding /<name>after the existing keys, so a Standard-14 simple font can declare how its codes map to glyphs. Absent (orundefined) emits nothing and the bytes are unchanged; an unknown value, orencodingcombined withembedded, throwspdf/builder/bad-fontwith{ name, encoding }in itscontext. -
./<family>/*.jsexport twins. All twelve wildcard families of theexportsmap (./filters/*,./annot/*,./tagged/*,./crypto/*,./sig/*,./ocg/*,./action/*,./embedded/*,./metadata/*,./prepress/*,./extra/*,./bundles/*) gain a./<family>/*.jstwin, so a specifier written with the.jssuffix (e.g.@awacloud/pdf/extra/sig-pades.js) resolves to the same file under Node and under a browser prefix import map. The existing forms are unchanged. -
pdf.write(model, opts)options.strictforwards toassembleIndirects; the lenient skip list is exposed asskippedObjectsand throughonSkipped. -
sign()signs encrypted bases. The signature field's/Tis encrypted with the document key derived fromopts.password(standard security handler, AESV2/AESV3); RC4 and AES-GCM bases are refused with typed errors. This supersedes the encrypted-base exception (signature-only update, no field) noted under Fixed. -
PAdES LT and LTA on encrypted bases. The DSS streams, the VRI strings and the document-timestamp field are encrypted with the document key; the signature
/Contentsstay clear, as the standard requires. -
External-oracle tests. OpenSSL-produced ECDSA and Ed25519 CMS signatures verify, and
sign()output matches their structure. -
Ed25519 signatures declare ISO/TS 32002. The Ed25519 signing update re-emits the Catalog with
/Extensionsdeclaring theISO_developer extension (/ExtensionLevel 32002, ISO/TS 32002 §4), merged with any existing extensions dictionary, and sets/Version /2.0on a document below PDF 2.0. A malformed/Extensionsis refused (pdf/sign/bad-extensions). ECDSA and RSA-PSS output is unchanged.
Changed
- API reference pages and source comments describe each opt-in module by
what it covers — internal milestone labels are removed from the
extra/pages and the module, crypto and signature comments. - Standard 14 font dictionaries are shared across pages.
pdfBuilder.addFont(non-embedded) writes one font dictionary per distinct(baseFont, subtype, encoding)and every page that uses it references that object. A document that repeats a font on several pages gets smaller: a 3-page document using 4 faces drops from 12 font dictionaries to 4. Documents without a repeated font are unchanged. - Truncated FlateDecode streams decode to their prefix.
pdfFlate.decodereturns the bytes decoded before a FlateDecode stream ends without its final block, flagged by a non-enumerabletruncated: true, instead of throwingpdf/flate/inflate-failed. Content streams of such files now extract. Other inflate errors still throwpdf/flate/inflate-failed. - Documentation pass. The README follows the published-package
layout (installation, quick start, sub-path table with targets, maturity,
licence, project links) with executed quick-start snippets and no
hand-typed counts; guides open with their purpose and prerequisites; the
coverage and bundle pages state what the filter dispatch, the legacy
extras and
writeactually do; reference pages were checked against the resolved module surfaces; apdfSharedreference page was added; links to the historical audit pages, which no longer ship, were removed. - The Read+Write prebuilt bundles include signature verification.
Every
-rwroot underdist/build/anddist/standalone/shipspdfSignaturebesidepdfSign, so an invoked bundle exposesverifySignatureandverifyAllSignatures. Each-rw.min.jsgrows by about 21.8 KB (6.4 KB gzipped); the Read bundles are unchanged. - Dist — dist regenerated with the licence banner: every committed
dist/**/*.js/.min.jsopens with the package's/*! … */legal block (content from the source repository's licence matrix), each*.meta.jsonbytesentry is measured on the final bytes, and there is nobuiltAttimestamp —bun run gen:bundlesis byte-deterministic; the fixedsign.js(no hard-coded/Root 1 0 Rtrailer; whole-token/ByteRangegap) ships in every-rwbundle. /ByteRangegap: the signer emits the whole<…>token; the verifier accepts exactly two forms.pdfSign.sign(the/Sig, and the LTA/DocTimeStampthrough the same emitter) now leaves the whole/Contents <…>token out of the signed ranges, delimiters included — ISO 32000-2 §12.8.3.3.1 requires the string to "fit precisely in the space between the ranges", and PDFBox and pyHanko emit and check that form (b). It previously left out the hex digits only (form a).computeByteRangetakes the token span as an optional fourth argument (opts.token, new codepdf/sig/byterange/bad-token); its three-argument call is unchanged.auditByteRangeaccepts a gap that is exactly the token or exactly the digits, and reports which in the additivegapFormfield ('token' | 'digits' | null); before, it accepted the token only and flaggedpdfSign's own output.verifySignature/verifyAllSignaturesnow apply that rule to every/Sig: any other gap givesverified: falsewithgap-start-mismatch/gap-end-mismatchinerrors. This is a tightening — measured on 2026-09-23, the verify path had no gap check at all: a gap of<+ digits, or digits +>, re-signed over its own ranges verifiedtrue; both are now refused. Signatures written in form (a) — every signed PDF committed in the repo — are not re-signed and keep verifying./DocTimeStamp/ByteRangegap check.verifyAllSignaturesapplies the same rule to every/DocTimeStamp: a gap that is not exactly the<…>token or exactly the hex digits givesverified: falsewithgap-start-mismatch/gap-end-mismatchinerrors(imprintVerifiedstill reports the imprint alone), and eachtimestamps[]entry gains the additivegapFormfield ('token' | 'digits' | null, present on every entry). A tightening: an off-by-one DocTimeStamp gap re-stamped over its own ranges verifiedtruebefore.- Package contents — the npm tarball ships
NOTICE(dual licence + trademark notice, commercial-licence contact) next toLICENSE; the pre-publication checklist no longer ships. Packagedescriptioncorrected: the shipped signature surface covers PKCS#7/PAdES sign and verify. main.jsrestructured as a declarative manifest — no runtime bootstrap, no re-export of resolved instances, no import of built bundles. Exportsfw_require(the@awacloud/fwfactories consumed by fw-bound modules, completed with every provider's transitive deps, e.g.zlib→deflate→bitstream/huffman,pem→b64,rsa/ecc→bn/random/hex/hmac),pkg_require(cross-package bridge re-exporting@awacloud/fonts' ownfw_require+modulesplus 4 internalembed-pdf/subsetForPdf/*helper descriptors it doesn't itself export, sopdfFontEmbed's full dependency graph resolves through a bare@awacloud/pdfregistration),modules(the core factories, topologically ordered),extras(opt-in),bundle(3 descriptors). Everymodulesdescriptor is additionally re-exported by binding name so a sibling composer (e.g.@awacloud/oconv,@awacloud/facturx) can resolve any pdf dependency through the bare@awacloud/pdfspecifier — purely additive, the five arrays stay byte-unchanged; theextrasandbundledescriptors are not re-exported by name.tools/generate-bundles.mjsis a thin wrapper over@awacloud/tool-prebuild-generator(see Added — two-surfacedist/).- Factory-only strict — the five error classes
(
PdfError/ParseError/RenderError/ContractError/EncryptionError) are declared insidepdfErrors's factory body; every consumer declares'pdfErrors'as a dependency and destructures the classes from the injectederrorsparameter instead of a top-levelimport { ParseError } from '../errors.js'. EachpdfErrors.factory()call therefore creates its own classes; aModuleRuntimecaches the resolved instance, so the modules of one runtime share them. The transitional ESM compatibility shim that temporarily re-exported the five classes as named bindings is fully retired (see Removed).pdfSigOidslikewise moved its 8 top-levelexport const/export functionOID tables into its factory body, withsignature.js/timestamp.js/certChain.jsreceiving them via DI. Across the refactor, every factory in the package is worker-safe: module-level constants/helpers a factory body referenced are relocated or inlined into that factory, sofactory.toString()rehydrates in a Web Worker without resolving an external module symbol (pinned bytests/roundtrip.integration.test.js's worker-safety assertion over every registered factory). - Bundles simplified to pure fw descriptors —
pdfLargeBundle/pdfFullBundle/pdfLegacyBundleare{ name, dependencies, factory }descriptors whose factory wires each resolved extra into the corepdfvia.use({ name, register })and returns the enriched instance; consumption is exclusivelyModuleRuntime.resolve(...)(see Removed for the retired imperative builders). verifySignature(...)result shape (breaking) — carriesverified,valid(alias, mirrorsverified),pkVerified,computedDigest, in place of the earlier structural-only{ valid: errors.length === 0, errors, signerCerts, hashAlg, signatureAlg }. See Security for the vulnerability this closes.pdfSignature.dependenciesgained'bitArray'.- Error codes namespaced by origin — granular kebab-case codes
(
pdf/flate/bad-predictor,pdf/sig/byterange/*,pdf/parser/*, …) replace earlier placeholder-style codes; every raised error carries a structuredcontext.pdf/ts/parseandpdf/ts/parse-failedrecords keep the underlying error ascause; apdf/sig/digest-failedrecord is{ code, message }, with the underlying error's message appended tomessage. - Hash streaming for
/ByteRange—verifySignatureuseshashMod.fn+update/finalizewhenbitArrayis available, avoiding anO(document size)intermediate buffer allocation; falls back tohashMod.hash(...)otherwise. standardV6scratch buffer — the hardening loop (Algorithm 2.B) reuses a scratch buffer for AES-128 key scheduling instead of allocating per round.- Packaging —
awa.maturityprogressedL0→L1→L2→L3→L4;package.jsoncarriesdescription,keywords,engines,sideEffects: false, and the legal and project metadata (licenseAGPL-3.0-only,author,repository,bugs,homepage);LICENSEandNOTICEship in the tarball. sign()documentation. Thesign()JSDoc lists every option the body reads.- Ed25519 viewer support documented. Adobe Acrobat Reader does not validate Ed25519 (EdDSA) signatures, including OpenSSL-produced ones; OpenSSL 3.5 and later verify them. The PAdES guide recommends ECDSA P-256 where Acrobat must validate the signature, and its algorithm claim row carries that caveat.
Deprecated
validon signature verification results. Thevalidfield ofverifySignature/verifyAllSignaturesresults (signatures and document timestamps) is deprecated: readverified. It carries the same value and is kept for compatibility.
Fixed
-
Identity crypt filters on V=5 mean no encryption. On V=5 (R=5 and R=6) files, a
/StmF,/StrFor/EFFnamedIdentity, or absent, now resolves toIdentity: those strings and streams are left as they are instead of being decrypted as AES-256, andsign()no longer encrypts its new strings on such files. -
The PAdES guide's external-TSA example runs. The
tsaSignexample is executed by the test suite, and the guide's verification-report sample shows SHA-512 for Ed25519. -
Source comments match the code. The comments of the legacy-filters extra, the writer, the filter dispatcher, the legacy bundle,
pdfSharedand the font-embed adapter describe the current behaviour: CCITT decoder delegation, header bytes, the dispatch map, no conversion on write, and the codec helpers. -
Documentation links resolve from the npm tarball. Links that pointed outside the package now point at the public repository at this release's tag, so they resolve from the tarball; references to sources that are not published are plain-text citations.
-
readDocumentopens PDF 1.5+ cross-reference streams and object streams. The top-level reader now picks a cross-reference form per section: anxrefkeyword takes the classical table path, anything else is parsed as a/Type /XRefstream (§7.5.8). Mixed/Prevchains (a classical incremental section over an xref-stream base, or the reverse) and hybrid-reference files (a classical trailer carrying/XRefStm) resolve end to end, and objects held in a/Type /ObjStmcontainer (§7.5.7) are materialised on demand bydoc._raw.resolve, each container decoded once per document.xref.sections[i]gainskind('table' | 'stream') and an indirect materialised from a container carriesobjStm; the publicreadDocumentsignature and return shape are otherwise unchanged. -
/DecodeParmsreach the decoders as plain values. Filter dispatch now marshals the typed dictionary (or array of dictionaries) into plain parameters, so a/Predictor(e.g. PNG predictor 12 on cross-reference streams) is actually applied instead of silently skipped. -
readDocumentsurvives two real-world shapes./Rootis resolved across a chain of cross-reference streams whose sections carry it in different trailers (merged newest first), and a non-catalog object marked free but still referenced degrades to a recorded loss instead of a throw; the returned document gains alossesarray for these. A document whose catalog itself is free is still refused. -
Indirect page resources are resolved.
typeResources/resolvePageResourcesaccept an optionalresolveRef, so a page whose/ExtGState(or another resource category) is an indirect reference no longer drops the whole resource map, fonts included. -
appendIncrementalextends cross-reference-stream bases with an uncompressed/Type /XRefsection carrying/Prevand/Root; the classical-table path is byte-unchanged. A hybrid-reference base (a classical trailer with/XRefStm) is refused withpdf/incremental/hybrid-base, and astartxrefthat designates neither form withpdf/incremental/unsupported-base.pdfXrefgainsreadXrefStreamDictandbuildXrefStream. -
Signing over cross-reference-stream and object-stream bases now yields a document that re-reads.
pdfSign.sign()appends the signature (and the LTA DocTimeStamp) throughpdfIncrementalWriter, so the signature section follows the base's cross-reference form (table or stream) and carries/Root,/Infoand/IDfrom the merged trailer instead of a hard-coded/Root 1 0 R. The signature takes the first object number at or past the merged/Size, so it no longer overwrites an object held in an object stream. At LT/LTA the Catalog is resolved throughreadDocumentfrom/Root, whatever its number or container, instead of a byte scan for1 0 obj. A hybrid-reference base is refused withpdf/incremental/hybrid-base, and nothing is written.pdfIncrementalWritergainsappendIncrementalWithOffsetsandreadBaseTrailer, andappendIncremental's output is unchanged.pdfSigngains apdfDocumentdependency, appended last. It is required only at LT/LTA and reported aspdf/sign/no-document-reader. Every level now requirespdfIncrementalWriter. -
pdfSign.sign()registers the signature in a signature field. The/Sigupdate now also writes an invisible/FT /Sigfield merged with its widget (/T Signature<n>,/V,/Rect [0 0 0 0],/F 132,/Ppage 1), the page 1/Annotsentry and the Catalog/AcroForm(/Fields,/SigFlags 3), per ISO 32000-2 §12.7.5.5, so viewers list the signature. The LTA/DocTimeStampgets its own field.pdf/sign/no-document-readernow fires at every level, because the field needs the Catalog and page 1 — this supersedes the LT/LTA-only rule above. An encrypted base keeps the signature-only update (no field). -
assembleIndirectsreports the objects it could not resolve. Read-then-write no longer drops an unresolvable object without a trace: every skipped{ num, gen, code }is listed on the returned array's non-enumerableskippedObjectsproperty, andassembleIndirects(model, { strict: true })throws aRenderErrorcodedpdf/writer/unresolvable-objectswhosecontext.objectsis that list. The default stays lenient and the output for a fully resolvable model is unchanged. -
ECDSA signature value is DER. The ECDSA CMS signature value is now the DER
ECDSA-Sig-Value; the verifier accepts the DER form and the earlier raw r||s. -
Ed25519 signs over SHA-512. Ed25519 signatures use SHA-512 as RFC 8419 requires;
hashAlgdefaults tosha512for Ed25519 and any other value is refused (pdf/sign/ed25519-requires-sha512). -
One-byte
/ToUnicodecodespace for simple fonts.embedSimplewrites a one-byte/ToUnicodecodespace matching its WinAnsi codes. -
Update trailers repeat
/Encrypt. Incremental updates written bypdfIncrementalWriter/pdfXref.buildXrefStreamcan repeat the base's/Encrypt(opts.encrypt);sign()does so over encrypted bases. -
CMS signed attributes in DER order. CMS signed attributes are written in DER SET OF order, so verifiers that re-encode them (OpenSSL with Ed25519) accept the signature.
Removed
-
pdfParserStream, the stream-body helpers module. No module depended on it andpdfParsercarries its own helpers. It is no longer inmodulesand no longer a named export of@awacloud/pdf. -
The
prebuilt/bundle directory that used to sit undersrc/bundles/, andtools/generate-prebuilds.mjs/bun run gen:prebuilds— retired in favour of the two-surfacedist/build/+dist/standalone/convention (see Added). Exported factory names and resolve keys are unchanged; only the on-disk location and generator moved (tools/generate-bundles.mjs/bun run gen:bundles). -
Imperative bundle builders
buildPdfLarge,buildPdfFull,buildPdfLegacyand the constantsPDF_LARGE_EXTRAS,PDF_FULL_EXTRAS,PDF_LEGACY_EXTRAS— consumption is now exclusivelyModuleRuntime.resolve('pdfLargeBundle' | 'pdfFullBundle' | 'pdfLegacyBundle'); the extras list lives inbundleDescriptor.dependencies. -
Named re-exports of extras from the bundle entry points — import an extra from its canonical module (
@awacloud/pdf/extra/...), or register theextrasarray exported by@awacloud/pdf(the root entry does not re-export extras by name). -
Top-level error-class named exports (
PdfError,ParseError,RenderError,ContractError,EncryptionError) from@awacloud/pdf/@awacloud/pdf/errors, and the transitional ESM compatibility shim that temporarily preserved them during the factory-only migration. Consumers resolve viapdfErrors:import { pdfErrors } from '@awacloud/pdf'; const { PdfError, ParseError, isPdfError } = pdfErrors.factory();or
runtime.resolve('pdfErrors').
Security
- The encrypted writer uses cryptographic randomness only.
pdfEncryptedWriterno longer falls back toMath.randomfor keys, salts and IVs. It usesencrypt.randomByteswhen given, elsecrypto.getRandomValues; with neither it throws anEncryptionErrorwith codepdf/crypto/enc-writer/no-random. - Signature verification — end of the silent false-positive.
Before public-key wiring,
verifySignature(...)returnedvalid: errors.length === 0without ever invokingrsa.pssVerify/ecc.verify/ed25519.verify— a PKCS#7 structurally valid but cryptographically forged signature reportedvalid: trueto a naïve consumer.verified/validare nowtrueonly once the digest chain and PK-verify both succeed (see Changed). auditByteRange(...)(src/sig/byteRange.js) — public helper detecting six/ByteRangeattack vectors:self-overlap,gap-start-mismatch,gap-end-mismatch,cross-overlap,incomplete-coverage(opt-inrequireFullCoverage),non-zero-start. Exposed viaruntime.resolve('pdfByteRange').- Sandboxed action flag — every typer for
/Launch,/JavaScript,/SubmitForm,/ImportData,/URIaddssandboxed: trueto its returned record, alongside the existingsecurityWarning, so a naïve consumer can't mistake a typed action record for an execution instruction. The opt-inpdfSandboxmodule (pdf-fullbundle) additionally exposeslintActions([...])→{ hasErrors, hasActiveContent, issues }. - Parser hardening — see Added (
parserLimits): depth cap, array literal cap, stream fallback-scan quota, each raising a typedpdf/parser/*error. - Encryption — Standard Security Handler V4/V5/V6 decrypt + encrypt
(AES-128/256-CBC, and RC4-128 through the V4 handler's
V2crypt filter), AES-GCM (TS 32003) decrypt + encrypt. The older RC4 revisions (40-bit R2, 128-bit R3) are read-only, through the opt-inlegacy-rc4-readextra of thepdf-legacybundle.