pretext-pdf 1.1.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (342) hide show
  1. package/CHANGELOG.md +689 -0
  2. package/README.md +82 -7
  3. package/dist/allowed-props.d.ts +76 -0
  4. package/dist/allowed-props.d.ts.map +1 -1
  5. package/dist/allowed-props.js.map +1 -1
  6. package/dist/assets/generators/barcode.d.ts +9 -0
  7. package/dist/assets/generators/barcode.d.ts.map +1 -0
  8. package/dist/assets/generators/barcode.js +24 -0
  9. package/dist/assets/generators/barcode.js.map +1 -0
  10. package/dist/assets/generators/chart.d.ts +13 -0
  11. package/dist/assets/generators/chart.d.ts.map +1 -0
  12. package/dist/assets/generators/chart.js +32 -0
  13. package/dist/assets/generators/chart.js.map +1 -0
  14. package/dist/assets/generators/qr.d.ts +9 -0
  15. package/dist/assets/generators/qr.d.ts.map +1 -0
  16. package/dist/assets/generators/qr.js +25 -0
  17. package/dist/assets/generators/qr.js.map +1 -0
  18. package/dist/assets/index.d.ts +19 -0
  19. package/dist/assets/index.d.ts.map +1 -0
  20. package/dist/assets/index.js +19 -0
  21. package/dist/assets/index.js.map +1 -0
  22. package/dist/assets/loaders/images.d.ts +20 -0
  23. package/dist/assets/loaders/images.d.ts.map +1 -0
  24. package/dist/assets/loaders/images.js +69 -0
  25. package/dist/assets/loaders/images.js.map +1 -0
  26. package/dist/assets/loaders/orchestrator.d.ts +24 -0
  27. package/dist/assets/loaders/orchestrator.d.ts.map +1 -0
  28. package/dist/assets/loaders/orchestrator.js +109 -0
  29. package/dist/assets/loaders/orchestrator.js.map +1 -0
  30. package/dist/assets/loaders/vectors.d.ts +25 -0
  31. package/dist/assets/loaders/vectors.d.ts.map +1 -0
  32. package/dist/assets/loaders/vectors.js +118 -0
  33. package/dist/assets/loaders/vectors.js.map +1 -0
  34. package/dist/assets/loaders/watermark.d.ts +12 -0
  35. package/dist/assets/loaders/watermark.d.ts.map +1 -0
  36. package/dist/assets/loaders/watermark.js +40 -0
  37. package/dist/assets/loaders/watermark.js.map +1 -0
  38. package/dist/assets/security/fetch.d.ts +14 -0
  39. package/dist/assets/security/fetch.d.ts.map +1 -0
  40. package/dist/assets/security/fetch.js +112 -0
  41. package/dist/assets/security/fetch.js.map +1 -0
  42. package/dist/assets/security/ipv4-normalize.d.ts +28 -0
  43. package/dist/assets/security/ipv4-normalize.d.ts.map +1 -0
  44. package/dist/assets/security/ipv4-normalize.js +116 -0
  45. package/dist/assets/security/ipv4-normalize.js.map +1 -0
  46. package/dist/assets/security/path-allowlist.d.ts +12 -0
  47. package/dist/assets/security/path-allowlist.d.ts.map +1 -0
  48. package/dist/assets/security/path-allowlist.js +26 -0
  49. package/dist/assets/security/path-allowlist.js.map +1 -0
  50. package/dist/assets/security/url-validation.d.ts +22 -0
  51. package/dist/assets/security/url-validation.d.ts.map +1 -0
  52. package/dist/assets/security/url-validation.js +164 -0
  53. package/dist/assets/security/url-validation.js.map +1 -0
  54. package/dist/assets/svg/dimensions.d.ts +19 -0
  55. package/dist/assets/svg/dimensions.d.ts.map +1 -0
  56. package/dist/assets/svg/dimensions.js +43 -0
  57. package/dist/assets/svg/dimensions.js.map +1 -0
  58. package/dist/assets/svg/rasterize.d.ts +6 -0
  59. package/dist/assets/svg/rasterize.d.ts.map +1 -0
  60. package/dist/assets/svg/rasterize.js +38 -0
  61. package/dist/assets/svg/rasterize.js.map +1 -0
  62. package/dist/assets/svg/resolve-content.d.ts +16 -0
  63. package/dist/assets/svg/resolve-content.d.ts.map +1 -0
  64. package/dist/assets/svg/resolve-content.js +38 -0
  65. package/dist/assets/svg/resolve-content.js.map +1 -0
  66. package/dist/assets/svg/sanitize.d.ts +22 -0
  67. package/dist/assets/svg/sanitize.d.ts.map +1 -0
  68. package/dist/assets/svg/sanitize.js +46 -0
  69. package/dist/assets/svg/sanitize.js.map +1 -0
  70. package/dist/assets/util/redact-path.d.ts +14 -0
  71. package/dist/assets/util/redact-path.d.ts.map +1 -0
  72. package/dist/assets/util/redact-path.js +16 -0
  73. package/dist/assets/util/redact-path.js.map +1 -0
  74. package/dist/assets.d.ts +10 -27
  75. package/dist/assets.d.ts.map +1 -1
  76. package/dist/assets.js +10 -549
  77. package/dist/assets.js.map +1 -1
  78. package/dist/builder.d.ts.map +1 -1
  79. package/dist/builder.js +2 -1
  80. package/dist/builder.js.map +1 -1
  81. package/dist/cli.js +11 -1
  82. package/dist/cli.js.map +1 -1
  83. package/dist/compat.d.ts +63 -1
  84. package/dist/compat.d.ts.map +1 -1
  85. package/dist/compat.js +42 -5
  86. package/dist/compat.js.map +1 -1
  87. package/dist/errors.d.ts +2 -2
  88. package/dist/errors.d.ts.map +1 -1
  89. package/dist/errors.js +2 -2
  90. package/dist/errors.js.map +1 -1
  91. package/dist/fonts.d.ts.map +1 -1
  92. package/dist/fonts.js +8 -10
  93. package/dist/fonts.js.map +1 -1
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +7 -5
  97. package/dist/index.js.map +1 -1
  98. package/dist/layout-state.d.ts +1 -1
  99. package/dist/layout-state.d.ts.map +1 -1
  100. package/dist/layout-state.js +5 -0
  101. package/dist/layout-state.js.map +1 -1
  102. package/dist/measure-blocks/float-group.d.ts +9 -0
  103. package/dist/measure-blocks/float-group.d.ts.map +1 -0
  104. package/dist/measure-blocks/float-group.js +103 -0
  105. package/dist/measure-blocks/float-group.js.map +1 -0
  106. package/dist/measure-blocks/helpers.d.ts +44 -0
  107. package/dist/measure-blocks/helpers.d.ts.map +1 -0
  108. package/dist/measure-blocks/helpers.js +43 -0
  109. package/dist/measure-blocks/helpers.js.map +1 -0
  110. package/dist/measure-blocks/highlight.d.ts +26 -0
  111. package/dist/measure-blocks/highlight.d.ts.map +1 -0
  112. package/dist/measure-blocks/highlight.js +169 -0
  113. package/dist/measure-blocks/highlight.js.map +1 -0
  114. package/dist/measure-blocks/image.d.ts +9 -0
  115. package/dist/measure-blocks/image.d.ts.map +1 -0
  116. package/dist/measure-blocks/image.js +136 -0
  117. package/dist/measure-blocks/image.js.map +1 -0
  118. package/dist/measure-blocks/index.d.ts +24 -0
  119. package/dist/measure-blocks/index.d.ts.map +1 -0
  120. package/dist/measure-blocks/index.js +179 -0
  121. package/dist/measure-blocks/index.js.map +1 -0
  122. package/dist/measure-blocks/list.d.ts +8 -0
  123. package/dist/measure-blocks/list.d.ts.map +1 -0
  124. package/dist/measure-blocks/list.js +108 -0
  125. package/dist/measure-blocks/list.js.map +1 -0
  126. package/dist/measure-blocks/simple-blocks.d.ts +18 -0
  127. package/dist/measure-blocks/simple-blocks.d.ts.map +1 -0
  128. package/dist/measure-blocks/simple-blocks.js +121 -0
  129. package/dist/measure-blocks/simple-blocks.js.map +1 -0
  130. package/dist/measure-blocks/table/columns.d.ts +17 -0
  131. package/dist/measure-blocks/table/columns.d.ts.map +1 -0
  132. package/dist/measure-blocks/table/columns.js +83 -0
  133. package/dist/measure-blocks/table/columns.js.map +1 -0
  134. package/dist/measure-blocks/table/measure.d.ts +8 -0
  135. package/dist/measure-blocks/table/measure.d.ts.map +1 -0
  136. package/dist/measure-blocks/table/measure.js +231 -0
  137. package/dist/measure-blocks/table/measure.js.map +1 -0
  138. package/dist/measure-blocks/table/spans.d.ts +25 -0
  139. package/dist/measure-blocks/table/spans.d.ts.map +1 -0
  140. package/dist/measure-blocks/table/spans.js +55 -0
  141. package/dist/measure-blocks/table/spans.js.map +1 -0
  142. package/dist/measure-blocks/text-blocks.d.ts +17 -0
  143. package/dist/measure-blocks/text-blocks.d.ts.map +1 -0
  144. package/dist/measure-blocks/text-blocks.js +242 -0
  145. package/dist/measure-blocks/text-blocks.js.map +1 -0
  146. package/dist/measure-text.d.ts +21 -3
  147. package/dist/measure-text.d.ts.map +1 -1
  148. package/dist/measure-text.js +87 -36
  149. package/dist/measure-text.js.map +1 -1
  150. package/dist/measure.d.ts +1 -1
  151. package/dist/measure.d.ts.map +1 -1
  152. package/dist/measure.js +8 -6
  153. package/dist/measure.js.map +1 -1
  154. package/dist/node-polyfill.d.ts.map +1 -1
  155. package/dist/node-polyfill.js +9 -0
  156. package/dist/node-polyfill.js.map +1 -1
  157. package/dist/pipeline-footnotes.d.ts +1 -1
  158. package/dist/pipeline-footnotes.d.ts.map +1 -1
  159. package/dist/pipeline-toc.d.ts +1 -1
  160. package/dist/pipeline-toc.d.ts.map +1 -1
  161. package/dist/pipeline.d.ts +3 -3
  162. package/dist/pipeline.d.ts.map +1 -1
  163. package/dist/pipeline.js +4 -5
  164. package/dist/pipeline.js.map +1 -1
  165. package/dist/plugin-types.d.ts +1 -1
  166. package/dist/plugin-types.d.ts.map +1 -1
  167. package/dist/post-process.d.ts +2 -2
  168. package/dist/post-process.d.ts.map +1 -1
  169. package/dist/post-process.js +47 -10
  170. package/dist/post-process.js.map +1 -1
  171. package/dist/render-blocks/blockquote.d.ts +7 -0
  172. package/dist/render-blocks/blockquote.d.ts.map +1 -0
  173. package/dist/render-blocks/blockquote.js +87 -0
  174. package/dist/render-blocks/blockquote.js.map +1 -0
  175. package/dist/render-blocks/callout.d.ts +7 -0
  176. package/dist/render-blocks/callout.d.ts.map +1 -0
  177. package/dist/render-blocks/callout.js +84 -0
  178. package/dist/render-blocks/callout.js.map +1 -0
  179. package/dist/render-blocks/code.d.ts +7 -0
  180. package/dist/render-blocks/code.d.ts.map +1 -0
  181. package/dist/render-blocks/code.js +84 -0
  182. package/dist/render-blocks/code.js.map +1 -0
  183. package/dist/render-blocks/footnote.d.ts +11 -0
  184. package/dist/render-blocks/footnote.d.ts.map +1 -0
  185. package/dist/render-blocks/footnote.js +45 -0
  186. package/dist/render-blocks/footnote.js.map +1 -0
  187. package/dist/render-blocks/header-footer.d.ts +11 -0
  188. package/dist/render-blocks/header-footer.d.ts.map +1 -0
  189. package/dist/render-blocks/header-footer.js +56 -0
  190. package/dist/render-blocks/header-footer.js.map +1 -0
  191. package/dist/render-blocks/hr.d.ts +7 -0
  192. package/dist/render-blocks/hr.d.ts.map +1 -0
  193. package/dist/render-blocks/hr.js +24 -0
  194. package/dist/render-blocks/hr.js.map +1 -0
  195. package/dist/render-blocks/image.d.ts +9 -0
  196. package/dist/render-blocks/image.d.ts.map +1 -0
  197. package/dist/render-blocks/image.js +135 -0
  198. package/dist/render-blocks/image.js.map +1 -0
  199. package/dist/render-blocks/index.d.ts +17 -0
  200. package/dist/render-blocks/index.d.ts.map +1 -0
  201. package/dist/render-blocks/index.js +17 -0
  202. package/dist/render-blocks/index.js.map +1 -0
  203. package/dist/render-blocks/list-item.d.ts +7 -0
  204. package/dist/render-blocks/list-item.d.ts.map +1 -0
  205. package/dist/render-blocks/list-item.js +80 -0
  206. package/dist/render-blocks/list-item.js.map +1 -0
  207. package/dist/render-blocks/rich.d.ts +7 -0
  208. package/dist/render-blocks/rich.d.ts.map +1 -0
  209. package/dist/render-blocks/rich.js +160 -0
  210. package/dist/render-blocks/rich.js.map +1 -0
  211. package/dist/render-blocks/table.d.ts +7 -0
  212. package/dist/render-blocks/table.d.ts.map +1 -0
  213. package/dist/render-blocks/table.js +139 -0
  214. package/dist/render-blocks/table.js.map +1 -0
  215. package/dist/render-blocks/text.d.ts +7 -0
  216. package/dist/render-blocks/text.d.ts.map +1 -0
  217. package/dist/render-blocks/text.js +183 -0
  218. package/dist/render-blocks/text.js.map +1 -0
  219. package/dist/render-blocks/watermark.d.ts +8 -0
  220. package/dist/render-blocks/watermark.d.ts.map +1 -0
  221. package/dist/render-blocks/watermark.js +52 -0
  222. package/dist/render-blocks/watermark.js.map +1 -0
  223. package/dist/render-extras.d.ts.map +1 -1
  224. package/dist/render-extras.js +1 -2
  225. package/dist/render-extras.js.map +1 -1
  226. package/dist/render-utils.d.ts.map +1 -1
  227. package/dist/render-utils.js +10 -6
  228. package/dist/render-utils.js.map +1 -1
  229. package/dist/render.d.ts.map +1 -1
  230. package/dist/render.js +9 -3
  231. package/dist/render.js.map +1 -1
  232. package/dist/rich-text.d.ts +2 -1
  233. package/dist/rich-text.d.ts.map +1 -1
  234. package/dist/rich-text.js +0 -1
  235. package/dist/rich-text.js.map +1 -1
  236. package/dist/types-internal.d.ts +19 -3
  237. package/dist/types-internal.d.ts.map +1 -1
  238. package/dist/types-public/document.d.ts +261 -0
  239. package/dist/types-public/document.d.ts.map +1 -0
  240. package/dist/types-public/document.js +2 -0
  241. package/dist/types-public/document.js.map +1 -0
  242. package/dist/types-public/elements-block.d.ts +246 -0
  243. package/dist/types-public/elements-block.d.ts.map +1 -0
  244. package/dist/types-public/elements-block.js +8 -0
  245. package/dist/types-public/elements-block.js.map +1 -0
  246. package/dist/types-public/elements-media.d.ts +199 -0
  247. package/dist/types-public/elements-media.d.ts.map +1 -0
  248. package/dist/types-public/elements-media.js +2 -0
  249. package/dist/types-public/elements-media.js.map +1 -0
  250. package/dist/types-public/elements-text.d.ts +327 -0
  251. package/dist/types-public/elements-text.d.ts.map +1 -0
  252. package/dist/types-public/elements-text.js +2 -0
  253. package/dist/types-public/elements-text.js.map +1 -0
  254. package/dist/types-public/index.d.ts +14 -0
  255. package/dist/types-public/index.d.ts.map +1 -0
  256. package/dist/types-public/index.js +2 -0
  257. package/dist/types-public/index.js.map +1 -0
  258. package/dist/types-public/render-options.d.ts +38 -0
  259. package/dist/types-public/render-options.d.ts.map +1 -0
  260. package/dist/types-public/render-options.js +2 -0
  261. package/dist/types-public/render-options.js.map +1 -0
  262. package/dist/types-public/union.d.ts +13 -0
  263. package/dist/types-public/union.d.ts.map +1 -0
  264. package/dist/types-public/union.js +2 -0
  265. package/dist/types-public/union.js.map +1 -0
  266. package/dist/types-public/validation.d.ts +64 -0
  267. package/dist/types-public/validation.d.ts.map +1 -0
  268. package/dist/types-public/validation.js +2 -0
  269. package/dist/types-public/validation.js.map +1 -0
  270. package/dist/types-public.d.ts +5 -1081
  271. package/dist/types-public.d.ts.map +1 -1
  272. package/dist/types.d.ts +1 -1
  273. package/dist/types.d.ts.map +1 -1
  274. package/dist/validate/document.d.ts +28 -0
  275. package/dist/validate/document.d.ts.map +1 -0
  276. package/dist/validate/document.js +295 -0
  277. package/dist/validate/document.js.map +1 -0
  278. package/dist/validate/elements/forms-floats.d.ts +19 -0
  279. package/dist/validate/elements/forms-floats.d.ts.map +1 -0
  280. package/dist/validate/elements/forms-floats.js +96 -0
  281. package/dist/validate/elements/forms-floats.js.map +1 -0
  282. package/dist/validate/elements/list.d.ts +10 -0
  283. package/dist/validate/elements/list.d.ts.map +1 -0
  284. package/dist/validate/elements/list.js +66 -0
  285. package/dist/validate/elements/list.js.map +1 -0
  286. package/dist/validate/elements/media.d.ts +23 -0
  287. package/dist/validate/elements/media.d.ts.map +1 -0
  288. package/dist/validate/elements/media.js +179 -0
  289. package/dist/validate/elements/media.js.map +1 -0
  290. package/dist/validate/elements/structural-simple.d.ts +21 -0
  291. package/dist/validate/elements/structural-simple.d.ts.map +1 -0
  292. package/dist/validate/elements/structural-simple.js +63 -0
  293. package/dist/validate/elements/structural-simple.js.map +1 -0
  294. package/dist/validate/elements/structural.d.ts +12 -0
  295. package/dist/validate/elements/structural.d.ts.map +1 -0
  296. package/dist/validate/elements/structural.js +12 -0
  297. package/dist/validate/elements/structural.js.map +1 -0
  298. package/dist/validate/elements/table.d.ts +10 -0
  299. package/dist/validate/elements/table.d.ts.map +1 -0
  300. package/dist/validate/elements/table.js +165 -0
  301. package/dist/validate/elements/table.js.map +1 -0
  302. package/dist/validate/elements/text.d.ts +26 -0
  303. package/dist/validate/elements/text.d.ts.map +1 -0
  304. package/dist/validate/elements/text.js +331 -0
  305. package/dist/validate/elements/text.js.map +1 -0
  306. package/dist/validate/errors.d.ts +9 -0
  307. package/dist/validate/errors.d.ts.map +1 -0
  308. package/dist/validate/errors.js +43 -0
  309. package/dist/validate/errors.js.map +1 -0
  310. package/dist/validate/fonts.d.ts +11 -0
  311. package/dist/validate/fonts.d.ts.map +1 -0
  312. package/dist/validate/fonts.js +118 -0
  313. package/dist/validate/fonts.js.map +1 -0
  314. package/dist/validate/helpers.d.ts +76 -0
  315. package/dist/validate/helpers.d.ts.map +1 -0
  316. package/dist/validate/helpers.js +169 -0
  317. package/dist/validate/helpers.js.map +1 -0
  318. package/dist/validate/index.d.ts +37 -0
  319. package/dist/validate/index.d.ts.map +1 -0
  320. package/dist/validate/index.js +279 -0
  321. package/dist/validate/index.js.map +1 -0
  322. package/dist/validate.d.ts +6 -18
  323. package/dist/validate.d.ts.map +1 -1
  324. package/dist/validate.js +6 -1585
  325. package/dist/validate.js.map +1 -1
  326. package/dist/vendor/pretext/VERSION.d.ts +3 -0
  327. package/dist/vendor/pretext/VERSION.d.ts.map +1 -0
  328. package/dist/vendor/pretext/VERSION.js +12 -0
  329. package/dist/vendor/pretext/VERSION.js.map +1 -0
  330. package/dist/version-check.d.ts +47 -0
  331. package/dist/version-check.d.ts.map +1 -0
  332. package/dist/version-check.js +75 -0
  333. package/dist/version-check.js.map +1 -0
  334. package/package.json +30 -7
  335. package/dist/measure-blocks.d.ts +0 -26
  336. package/dist/measure-blocks.d.ts.map +0 -1
  337. package/dist/measure-blocks.js +0 -1317
  338. package/dist/measure-blocks.js.map +0 -1
  339. package/dist/render-blocks.d.ts +0 -28
  340. package/dist/render-blocks.d.ts.map +0 -1
  341. package/dist/render-blocks.js +0 -1059
  342. package/dist/render-blocks.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,695 @@ Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/)
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.7.0] — 2026-05-25
11
+
12
+ Signing path repaired. **No public API changes.** The cryptographic signing pipeline has been architecturally broken end-to-end since v1.3.6 — calling `render({ signature: { p12, passphrase } })` would fail with `SIGNATURE_FAILED: PDF signing failed: No ByteRangeStrings found within PDF buffer`. v1.7.0 fixes it with a surgical change inside `applySignature`.
13
+
14
+ ### Fixed (critical)
15
+
16
+ - **Signing path was architecturally broken since v1.3.6**. Root cause: `applySignature` loaded the placeholder doc via `@cantoo/pdf-lib`'s `PDFDocument.load`, then handed it to `@signpdf/placeholder-pdf-lib.pdflibAddPlaceholder`, which internally builds `/ByteRange` using **upstream** `pdf-lib`'s `PDFArray`/`PDFNumber`/`PDFName` classes (it imports them directly from `"pdf-lib"`). Cantoo's serializer doesn't recognize upstream's class instances and emitted a malformed `/ByteRange` dict; `@signpdf/utils.findByteRange` then aborted parsing with `No ByteRangeStrings found within PDF buffer`. Fix: load the doc via **upstream `pdf-lib`** for the placeholder hop only. Encryption stays on `@cantoo/pdf-lib` in `applyEncryption` — the two paths are mutually exclusive via the existing `SIGNATURE_CERT_AND_ENCRYPTION` guard, so we never need both pdf-libs active simultaneously. The previously KNOWN-BROKEN `test/signatures-crypto.test.ts → P12 signature verifies cryptographically (real CMS verify)` is now unskipped and green.
17
+
18
+ ### Added
19
+
20
+ - **AcroForm regression assertion** inside `test/signatures-crypto.test.ts`. Any valid signed PDF must carry `/AcroForm`, `/Fields [...]`, `/SigFlags 3`, and a `/Type /Sig` object — these four properties are asserted on the signed bytes so a future regression that loses AcroForm structure surfaces immediately.
21
+ - **Signature path snapshot tripwire** (`test/signatures-snapshot.test.ts` + `test/data/signatures-snapshot.json`). Captures the categorical structural shape of a signed PDF (presence of `/ByteRange`, `/AcroForm`, `/SigFlags` value, etc.) rather than byte offsets (which are document- and randomness-dependent). Wired into `test:phases`. Regenerate with `UPDATE_SNAPSHOT=1`.
22
+
23
+ ### Changed
24
+
25
+ - **`pdf-lib` declared as an explicit optional peer dependency** (`^1.17.1`). It was previously only present transitively via `@signpdf/placeholder-pdf-lib`'s `dependencies`. Now it's a documented peer with `peerDependenciesMeta.pdf-lib.optional: true`, mirroring the existing `@signpdf/*` pattern. Users with `@signpdf/*` already installed need no action — npm satisfies the new peer from the existing transitive install.
26
+ - **`SIGNATURE_DEP_MISSING` error message** now lists `pdf-lib` alongside the three `@signpdf/*` packages and drops the "currently non-functional due to fork incompatibility" disclaimer that was added in v1.3.6.
27
+
28
+ ### Migration
29
+
30
+ None. Same `signature: { p12, passphrase, reason, contactInfo, signerName, location, page, invisible }` config. Same error codes (`SIGNATURE_DEP_MISSING`, `SIGNATURE_P12_LOAD_FAILED`, `SIGNATURE_FAILED`, `SIGNATURE_CERT_AND_ENCRYPTION`). One error message string changed: `SIGNATURE_DEP_MISSING` now mentions `pdf-lib` and no longer carries the "non-functional" disclaimer.
31
+
32
+ ### Verification
33
+
34
+ - All 456 tests pass (was 454 pass + 1 skipped pre-fix; now 455 pass after unskip + 1 new snapshot test = 456 total).
35
+ - Encryption-after-signing path still rejected with `SIGNATURE_CERT_AND_ENCRYPTION` (regression-tested).
36
+ - All 7 v1.6.0 verification gates (G1–G7) still pass.
37
+
38
+ ---
39
+
40
+ ## [1.6.0] — 2026-05-25
41
+
42
+ Internal restructuring + SVG sanitizer hardening. **No public API changes.** The previously-monolithic `src/assets.ts` (961 lines pre-sprint) has been split into 10 focused files under `src/assets/`. A 14-line back-compat shim at `src/assets.ts` re-exports the barrel so every existing consumer (internal modules, public API, and direct test imports via `dist/assets.js`) keeps working unchanged.
43
+
44
+ ### Security
45
+
46
+ - **SVG sanitizer hardening** — `sanitizeSvg` now strips three additional payload classes that survived the previous regex chain:
47
+ - **`<foreignObject>` blocks** — the only XML-in-SVG construct that can host arbitrary HTML/XML namespaces. Both self-closing and paired forms are removed wholesale; sibling SVG primitives (`<rect/>`, `<path/>`, etc.) are preserved.
48
+ - **`javascript:` / `vbscript:` / `data:` hrefs on `<a>` elements** — previously only `<image>`/`<use>` hrefs were filtered. Only the dangerous href attribute is dropped, so the `<a>` element's text children still render.
49
+ - **CSS `expression(...)` calls inside `<style>` blocks** — legacy IE XSS vector. Only the `expression(...)` call site is excised; the surrounding stylesheet remains parseable.
50
+
51
+ Coverage: new `test/svg-sanitizer.test.ts` (10 cases) plus expanded MA-4 / MA-5 fixtures in `test/data/assets-split-tripwire.json` (30 fixtures total).
52
+
53
+ ### Changed
54
+
55
+ - **`src/assets.ts` split into 10 files** under `src/assets/` (see Internal). No public API change — the file at `src/assets.ts` is now a 14-line re-export shim that aggregates the new barrel `src/assets/index.ts`. Consumers importing from `dist/assets.js` continue to resolve every previously-public symbol (`loadImages`, `assertPathAllowed`, `sanitizeSvg`, `assertSafeUrl`, `resolveAndValidateUrl`, `normalizeIpv4Hostname`, `fetchWithTimeout`, `redactPath`, `VECTOR_RASTER_CONCURRENCY`, plus the `ResolvedSafeUrl` type) at the same module path.
56
+ - **`PretextPdfError` constructor signature snapshot refreshed** — `etc/pretext-pdf.api.md` now reflects the `options?: ErrorOptions` parameter that was added in v1.2.1 but never re-snapshotted. No code change — the parameter has been live for several releases. The snapshot was stale; this release reconciles it.
57
+
58
+ ### Internal
59
+
60
+ - **New `src/assets/` directory layout:**
61
+ ```
62
+ src/assets/
63
+ ├── index.ts # internal barrel
64
+ ├── util/
65
+ │ └── redact-path.ts # commit 4
66
+ ├── security/
67
+ │ ├── path-allowlist.ts # commit 5
68
+ │ ├── ipv4-normalize.ts # commit 6
69
+ │ ├── url-validation.ts # commit 7
70
+ │ └── fetch.ts # commit 8 (undici Agent stays lazy)
71
+ ├── svg/
72
+ │ ├── sanitize.ts # commit 9 (+ SVG_MAX_BYTES)
73
+ │ ├── dimensions.ts # commit 10
74
+ │ ├── resolve-content.ts # commit 10
75
+ │ └── rasterize.ts # commit 11 (@napi-rs/canvas dynamic)
76
+ ├── generators/
77
+ │ ├── qr.ts # commit 12 (qrcode dynamic)
78
+ │ ├── barcode.ts # commit 12 (bwip-js dynamic)
79
+ │ └── chart.ts # commit 12 (vega/vega-lite dynamic)
80
+ └── loaders/
81
+ ├── images.ts # commit 13
82
+ ├── vectors.ts # commit 14
83
+ ├── watermark.ts # commit 15
84
+ └── orchestrator.ts # commit 15 (top-level loadImages)
85
+ ```
86
+ All optional peer-dependency dynamic imports (`@napi-rs/canvas`, `qrcode`, `bwip-js`, `vega`, `vega-lite`, `undici`) are preserved as lazy loads — cold-start cost is unchanged.
87
+ - **7 verification gates (G1–G7) added** to catch regressions during the split:
88
+ - **G1** Snapshot tripwire (`test/assets-split-tripwire.test.ts`) — 30 fixtures covering sanitizer output, URL normalization, path allowlist, and error code surface
89
+ - **G2** DNS lookup dedup (`test/assets-dns-dedup.test.ts`)
90
+ - **G3** SSRF blocking (`test/security-ssrf.test.ts`, expanded)
91
+ - **G4** Parallel-render concurrency (`test/assets-concurrency.test.ts`) — 10× concurrent `render()` calls must produce bit-identical PDFs
92
+ - **G5** ErrorCode stability (`test/assets-errorcode-stability.test.ts`)
93
+ - **G6** api-extractor diff against `etc/pretext-pdf.api.md`
94
+ - **G7** Cold-start perf (`test/assets-perf-coldstart.test.ts`, baseline at `test/data/perf-coldstart-baseline.json`) — 100 sequential renders within 2.5× the v1.5.2 baseline
95
+ - **G7 measurement on the final shim**: 12,224ms / 100 renders (vs 21,205ms baseline = -42%, i.e. the split happens to be *faster*, well under the upper bound).
96
+
97
+ ---
98
+
99
+ ## [1.5.2] — 2026-05-25
100
+
101
+ Security hotfix. **No public API changes.** **Upgrade recommended for any deployment that accepts user-controlled image / SVG URLs.**
102
+
103
+ ### Security
104
+
105
+ - **CVE-class SSRF bypass via IPv4 alternative notations** — `isPrivateAddress` in `src/assets.ts` applied dotted-decimal regexes (`/^127\./`, `/^10\./`, …) against `URL#hostname`. The WHATWG URL parser does NOT normalize non-dotted IPv4 forms, so an attacker could reach private services by encoding the target in any inet_aton-compatible form:
106
+ - Pure decimal: `https://2130706433/x` → 127.0.0.1
107
+ - Pure hex: `https://0x7f000001/x` → 127.0.0.1
108
+ - Octal octet: `https://0177.0.0.1/x` → 127.0.0.1
109
+ - Hex octet: `https://0x7f.0.0.1/x` → 127.0.0.1
110
+ - Short form: `https://127.1/x` → 127.0.0.1
111
+
112
+ Without normalization, `parsed.hostname` is e.g. `"2130706433"`, the private-range regex chain misses it, the `isIpv4Literal` (4-dot) check misses it, and the URL falls through to DNS — which on Linux's `getaddrinfo` resolves to 127.0.0.1 and bypasses the SSRF guard. Same vector reaches RFC 1918 ranges (10/8, 192.168/16, 172.16/12), link-local (169.254.169.254 — AWS IMDS), and CGNAT.
113
+
114
+ **Fix:** new internal helper `normalizeIpv4Hostname()` implements inet_aton-style parsing (decimal/octal/hex per part, short-form packing for 1/2/3-part inputs, strict 32-bit range guard). `resolveAndValidateUrl` normalizes before the private-IP check AND before DNS, then treats the normalized form as an IP literal so undici never re-resolves a non-dotted private encoding. `isPrivateAddress` also normalizes its input as defense-in-depth on the post-DNS path. Public alternative encodings (e.g. `134744072` == 8.8.8.8) continue to resolve and fetch normally.
115
+
116
+ **Test coverage:** new `test/security-ipv4-bypass.test.ts` adds 24 cases — every blocked encoding above, public regression cases, plus direct unit tests for `normalizeIpv4Hostname` (round-trips, range guards, malformed-octal rejection, public-IP allowlist). Wired into the `test:phases` stage; phases stage grows from 417 to 441 tests.
117
+
118
+ ### Fixed
119
+
120
+ - **`measure-blocks/float-group.ts` — fontSize fallback intent clarified** — v1.5.1 M5a removed a dead `baseFontSize = doc.defaultFontSize ?? 12` plus per-block `fontSize = block.fontSize || baseFontSize` local that was never read (the item assignment used `block.fontSize` directly). Audit of upstream measure helpers (measure-paragraph, measure-heading, …) confirms `block.fontSize` is always populated with a positive value for real content, so the fallback would never have fired in practice. Added a leading comment so future contributors don't reintroduce the fallback in the mistaken belief that `block.fontSize === 0` is a real case. No behavior change.
121
+
122
+ ---
123
+
124
+ ## [1.5.1] — 2026-05-24
125
+
126
+ Hotfix batch closing 9 audit findings from the 6-agent v1.5.0 review. **No public API changes** — guarded by `test/public-api-surface.test.ts`. **No behavior changes other than the documented fixes**. Snapshot baseline expanded from 68 to 73 fixtures (5 new: 2 metadata.keywords + 3 watermark.image).
127
+
128
+ ### Security
129
+
130
+ - **Watermark image URL scheme validation (H1)** — `doc.watermark.image` now passes through the same `validateUrl` pre-flight check used by `validateImage`. Unsafe schemes (`javascript:`, `data:`, `vbscript:`, `blob:`, `about:`, `file:`) are rejected at validate-time so CLI lint and MCP validate tools catch them before render. Relative file paths fall through unchanged. The shared URL-shape helper `looksLikeUrl` was moved from `validate/elements/media.ts` to `validate/helpers.ts`.
131
+
132
+ ### Fixed
133
+
134
+ - **`metadata.keywords[]` element validation (H3)** — Previously, `for (const field of [...'keywords'...])` was paired with `typeof val === 'string'`, which silently no-op'd for the `string[]`-shaped `keywords` field. Each keyword entry is now validated for control-character injection and the 1000-character length cap via `validateMetadataString(kw, \`keywords[\${i}]\`)`.
135
+ - **highlight.js dynamic-import error logging (H4)** — Previously, `catch { /* not installed */ }` silently swallowed every failure including real module-load errors. The catch now logs a warning unless the error code is `ERR_MODULE_NOT_FOUND`/`MODULE_NOT_FOUND` (the only legitimate "optional dep absent" signal).
136
+ - **`hljs.highlight()` runtime exception logging (M2)** — Same silent-fallback issue as H4 but for tokenization failures. Now logs a warning naming the language before falling back to plain text.
137
+ - **Font-variant registration logging (M3)** — `installNodePolyfill` now tracks per-variant success and warns when an individual Inter weight (400 or 700) fails to register, instead of only warning when *both* failed. Text metrics for the missing variant may be inaccurate; operators see this in logs.
138
+
139
+ ### Changed
140
+
141
+ - **Removed redundant `as import('...').X` casts in `validate/elements/forms-floats.ts` (M1)** — `validateFormField`, `validateFootnoteDef`, and `validateFloatGroup` already accept `Extract<ContentElement, { type: 'X' }>`-typed `el`. The inner `const ff = el as FormFieldElement` casts were no-ops; removed.
142
+ - **Removed unreachable `toc-entry` validator (M4)** — The pre-switch throw at `validate/index.ts:260` already rejects `toc-entry` before dispatch. The `case 'toc-entry':` arm (guarded with `@ts-expect-error`) and the `validateTocEntry` function in `validate/elements/structural-simple.ts` were unreachable dead code. Drift-guard test updated with `VALIDATE_DISPATCHER_EXCLUDES` (mirrors the existing `MEASURE_DISPATCHER_EXCLUDES` pattern) to keep the orchestrator scan honest.
143
+ - **tsconfig: `noUnusedParameters` + `noUnusedLocals` enabled (M5a)** — Catches dead destructured locals and unused imports at build time. Cascade: 51 errors → 0. 30 of those were per-type `type _X = Exact<...>` drift guards in `allowed-props.ts` (load-bearing scaffolding) — consolidated into a single exported tuple `_AllowedPropsDriftGuard` that TypeScript counts as used. The remaining 20 were genuine unused-locals / unused-imports cleanup across measure, render, rich-text, and assets modules.
144
+ - **tsconfig: `verbatimModuleSyntax` enabled (M5b)** — Enforces `import type` discipline. Cascade was only 6 errors, all `HyphenatorOpts` runtime imports that needed splitting into `import type`. Well under the 30-line cascade-defer threshold.
145
+
146
+ ---
147
+
148
+ ## [1.5.0] — 2026-05-24
149
+
150
+ Architecture sprint completing the v1.4.0 god-file split debt. Six items shipped, single minor release. **No public API changes** — guarded by `test/public-api-surface.test.ts`. **No behavioral changes** — Item A's security-critical extraction guarded by new `test/validate-document-snapshot.test.ts` (68 fixtures, bit-exact preservation verified).
151
+
152
+ ### Changed (internal structure — non-breaking)
153
+
154
+ - **`src/validate/index.ts` (594L) → orchestrator (322L) + `src/validate/document.ts` (324L)** —
155
+ Extracted 11 doc-level check categories (pageSize, margins, fonts, header/footer, defaultParagraphStyle, sections, watermark, encryption, signature, bookmarks, hyphenation, metadata) into a new `validateDocumentLevel(doc, ctx)` function. Single function with labeled `// ── name ──` blocks. **Security-critical**: snapshot tripwire test (68 fixtures across all 11 categories) verified bit-exact error preservation through the move.
156
+ - **`src/measure-blocks/index.ts` (337L) → dispatcher (243L) + `src/measure-blocks/simple-blocks.ts` (151L)** —
157
+ Extracted 7 simple measurement arms (spacer, page-break, comment, form-field, hr, toc, footnote-def). Throw-guards for image/svg/qr-code/barcode/chart stay in dispatcher (security invariants tied to routing).
158
+ - **`src/validate/elements/structural.ts` (239L grab-bag) → `structural-simple.ts` (120L) + `forms-floats.ts` (128L)** —
159
+ Split light element validators (spacer, hr, toc, toc-entry, comment) from heavy ones (form-field, footnote-def, float-group). Cleaner separation of concerns.
160
+
161
+ ### Added
162
+
163
+ - **`src/validate/elements/README.md`** — Placement guide + validator signature contract + `_ctx` policy documentation + `withCycleGuard` usage guidance. Onboarding aid for adding new element types.
164
+ - **`test/validate-document-snapshot.test.ts`** + `test/data/validate-document-snapshot.json` — Bit-exact error preservation tripwire for `validateDocumentLevel`. 68 fixtures across pageSize, margins, fonts, header/footer, defaultParagraphStyle, sections, watermark, encryption, signature, bookmarks, hyphenation, metadata, content guards, and valid-doc sanity cases.
165
+
166
+ ### Fixed
167
+
168
+ - **`dist/` no longer tracked in git** (168 files removed) — was already in `.gitignore` but tracked from prior history.
169
+
170
+ ### Notes
171
+
172
+ - 12 commits across 6 items, each independently revertable via the 3-commit-per-split pattern (stage → route → delete) proven in v1.4.0.
173
+ - Test suite: 416 pass / 1 skip / 0 fail throughout the sprint, plus the new snapshot tripwire test (passes against generated baseline).
174
+ - Public API surface unchanged: 15 runtime exports × 6 entry points.
175
+ - `_ctx` policy decision: keep underscore-prefixed param for validators that don't currently consume context. Documented in `validate/elements/README.md` — stable signature lets future strict-mode or context-aware checks be added without changing call sites.
176
+ - `loadedFamilies` initialization order: populated after `validateDocumentLevel` returns. Audit confirmed no order dependency — `document.ts` never reads `loadedFamilies`, only `validateFontSpec` for shape validation.
177
+ - Path-traversal pre-flight in `validateImage` deferred to v2.0 — runtime SSRF + `allowedFileDirs` guards already provide defense-in-depth.
178
+
179
+ ---
180
+
181
+ ## [1.4.1] — 2026-05-23
182
+
183
+ Cleanup batch addressing audit findings surfaced by the v1.4.0 god-file split. Five MEDIUM-severity items, all internal — no public API changes, no behavioral changes for callers using documented schemas. Test suite unchanged: 416 pass / 1 skip / 0 fail.
184
+
185
+ ### Fixed
186
+
187
+ - **M1 — Dead outer `withCycleGuard` removed from `validateElement` dispatcher (`src/validate/index.ts`).** The dispatcher wrapped `list` and `float-group` cases in an outer `withCycleGuard` with an empty body, then the inner element validators (`validateList` in `elements/list.ts`, `validateFloatGroup` in `elements/structural.ts`) immediately opened their own guard on the same element. The outer guard ran a no-op body and `finally`-deleted the element from `seen` BEFORE the inner guard added it — dead code communicating false intent. The inner validators own the guard. Import of `withCycleGuard` also dropped from `index.ts` since it is no longer used there.
188
+ - **M2 — `measure-blocks/float-group.ts ↔ measure-blocks/index.ts` runtime cycle broken (Option C: dependency injection).** `float-group.ts` previously imported `measureBlock` from `./index.js`, while `index.ts` re-exported `measureFloatGroup` from `./float-group.js` — a real ESM cycle that hoisting tolerated but which violated module-boundary discipline. Resolved by promoting `measureBlock` to an explicit parameter of `measureFloatGroup` (new exported `MeasureBlockFn` type). The sole caller (the orchestrator in `measure.ts`) already has `measureBlock` in scope, so the change is non-invasive. Option C was chosen over Option A (inlining the dispatch — too much duplication) and Option B (extracting `dispatch.ts` — restructured more than needed for a one-edge cycle).
189
+
190
+ ### Changed
191
+
192
+ - **M3 — Concurrent validate test annotated with sync-vs-async semantics (`test/validate-concurrent.test.ts`).** Added a comment block above the parallel-validate `describe` clarifying that `validateDocument` is synchronous, so `Promise.all` over it cannot exercise real concurrent execution. The test now explicitly documents what it does prove (shape stability across N invocations) versus what it does not (concurrent isolation — guaranteed structurally by the per-call `WeakSet` opened at the top of `validate()` in `src/validate/index.ts`). The async render-path tests later in the same file DO exercise real concurrency because `render()` crosses `await` boundaries.
193
+ - **M4 — Benchmark harness upgraded (`scripts/run-bench-snapshot.mjs`).** Default measured runs raised from 3 to 10. New `--runs N` CLI flag for callers to override (CI: 5, dev: 3, full: 10). Output now reports `median`, `p90`, and `min` in addition to `avg`. Variance on cold-tsx invocations can hit ±70%, which made any <10% regression gate meaningless at N=3.
194
+
195
+ ### Added
196
+
197
+ - **M5 — URL scheme check in `validateImage` (`src/validate/elements/media.ts`).** When `el.src` is a string that looks like a URL (matches `data:`, `javascript:`, `vbscript:`, `blob:`, `about:`, `file:`, or any `scheme://` form), the validator now routes through `validateUrl` from `validate/helpers.ts`. Matches the runtime SSRF guard's posture in `src/compat.ts` so validate-only callers (CLI lint, MCP `validate_document` tool) catch unsafe schemes pre-flight instead of letting them through to the asset-loader guard. http/https/ftp/mailto/anchor links still pass.
198
+
199
+ ---
200
+
201
+ ## [1.4.0] — 2026-05-23
202
+
203
+ Architecture sprint: four god-files split into thin orchestrators + cohesive sub-modules. **No public API changes** — guarded by `test/public-api-surface.test.ts` (15 runtime exports across 6 entry points, snapshot tripwire). 12 granular commits, each independently revertable.
204
+
205
+ ### Changed (internal structure — non-breaking)
206
+
207
+ - **`src/validate.ts` (1834L) → `src/validate/` (9 files, ~250L each)** —
208
+ `validate/index.ts` orchestrator + `helpers.ts`, `fonts.ts`, `errors.ts`, and per-element validators under `elements/`. Introduced explicit `ValidationContext` ({ errors, strict, loadedFamilies, seen, options }) threaded into every element validator instead of relying on closure-captured locals. Per-call `WeakSet` for cycle detection — concurrent isolation verified by `test/validate-concurrent.test.ts`.
209
+ - **`src/measure-blocks.ts` (1600L) → `src/measure-blocks/` (10 files)** —
210
+ `measure-blocks/index.ts` dispatcher + `text-blocks.ts`, `list.ts`, `image.ts`, `float-group.ts`, `highlight.ts`, `helpers.ts`, plus `table/{measure,spans,columns}.ts`. Added explicit case arms for `qr-code`, `barcode`, `chart` (previously fell through to "Unknown element type"). `_hljsCache` intentionally remains module-scoped in `highlight.ts` — idempotent process-wide cache, not concurrent-isolated state.
211
+ - **`src/render-blocks.ts` (1277L) → `src/render-blocks/` (13 files)** —
212
+ Per-render-function modules. Each independent (no shared state). Dropped four unused imports from the original (`PDFFont`, `PDFName`, `renderTocEntry`, `renderFormField` — never referenced).
213
+ - **`src/types-public.ts` (1200L) → `src/types-public/` (8 files)** —
214
+ Split by domain: `document.ts`, `elements-{text,block,media}.ts`, `union.ts`, `validation.ts`, `render-options.ts`. Type-only intra-package cycles are erased at runtime (verified by clean tsc build).
215
+
216
+ ### Added
217
+
218
+ - **`test/drift-guards.test.ts` — measure-blocks dispatcher case-arm guard.** New invariant mirrors the existing validate + render guards: every `ELEMENT_TYPES` entry must have an explicit `case` arm in `src/measure-blocks/index.ts` (except internal `toc-entry`). Catches future additions to `ELEMENT_TYPES` that forget to handle measurement.
219
+
220
+ ### Fixed
221
+
222
+ - **Dead imports removed:** `validate` was imported but never used in `src/builder.ts` after the split. Removed.
223
+ - **Unused locals/params:** `lineHeight` in `float-group.ts` and `table/measure.ts` (computed but never read); `doc` param in `measureCallout` and `measureCode` (preserved as `_doc` for signature compatibility, signals intentional non-use).
224
+
225
+ ### Notes
226
+
227
+ - Public API surface tripwire: 15 runtime exports across 6 entry points, all unchanged.
228
+ - Benchmark gate: 5/7 corpora within 5% of v1.3.4; two corpora (`rich-text-mixed-spans` +12.9%, `table-stress` +7.2%) breach but match documented run-to-run variance (5-10% noise on dev machine). Cold-start module-load unchanged. CI re-snapshot recommended to disambiguate variance from regression.
229
+ - Known follow-up: ESM-fragile circular import in `measure-blocks/float-group.ts` ↔ `measure-blocks/index.ts` (load-safe today because `measureBlock` is async-runtime only; revisit if top-level `await` is ever added).
230
+ - Known follow-up: type-only intra-cycles in `types-public/` (no runtime risk — erased at compile time).
231
+
232
+ ---
233
+
234
+ ## [1.3.6] — 2026-05-23
235
+
236
+ Architecture-sprint scaffolding release: vendor integrity check, concurrent isolation tests, signing import correctness, public API tripwire.
237
+
238
+ ### Added
239
+
240
+ - **Boot-time vendor integrity check (#12)** — `src/version-check.ts` exports `assertVendorIntegrity()`, called once at the start of every `render()`. Verifies the vendored pretext version (`src/vendor/pretext/VERSION.ts`) is in the compatible range. Warns (does not throw) on drift. Inline semver matcher avoids adding a `semver` dependency.
241
+ - **Concurrent validate/render isolation tests (#25)** — `test/validate-concurrent.test.ts` exercises 8 parallel `validate()` calls plus 4 parallel `render()` calls across different fonts AND different scripts (en/he/ar/th). Byte-identical fingerprints between parallel and sequential runs prove `vendor/pretext/measurement.ts` and `vendor/pretext/analysis.ts` shared state holds up under concurrent use.
242
+ - **Public API surface tripwire** — `test/public-api-surface.test.ts` snapshots all 15 runtime exports across 6 entry points. Will guard against accidental API drift during the upcoming v1.4.0 god-file splits.
243
+ - **P12/CMS crypto verification test (#26)** — `test/signatures-crypto.test.ts` adds a real `crypto.createVerify('RSA-SHA256')` round-trip with positive + negative cases. **Currently `t.skip`'d** — see KNOWN ISSUES below.
244
+ - **`pretextPdf.mcpCompat`** field in `package.json` — declares `>=1.4.0 <2.0.0` compatibility range for the pretext-pdf-mcp consumer. MCP-side check ships separately.
245
+
246
+ ### Fixed
247
+
248
+ - **`signpdf` v3 API + import correctness** — `src/post-process.ts` was destructuring `pdflibAddPlaceholder` from `@signpdf/signpdf` where it does not exist; the symbol lives in `@signpdf/placeholder-pdf-lib`. Also updated to the v3 API (`new P12Signer(buffer, { passphrase })` instead of `signer.sign(buffer, { passphrase })`). Added `@signpdf/placeholder-pdf-lib` and `@signpdf/signer-p12` to `peerDependencies` + `peerDependenciesMeta.optional` (mirroring `@signpdf/signpdf`).
249
+ - **`SIGNATURE_DEP_MISSING` error message** — now identifies exactly which `@signpdf/*` packages are missing and tells the user which to install, instead of always listing all three.
250
+
251
+ ### KNOWN ISSUES (deferred to follow-up sprint)
252
+
253
+ - **Signing path is architecturally non-functional** — even with correct imports, `@cantoo/pdf-lib`'s serializer is fork-incompatible with `@signpdf/placeholder-pdf-lib`. The placeholder ByteRange dict is emitted in a shape that `@signpdf/utils.findByteRange` cannot parse. End-to-end signing has never worked. The crypto verify test (#26) is correctly written and ready to run once signing is repaired. Three fix paths exist: `@signpdf/placeholder-plain` swap (breaks AcroForm), porting placeholder-pdf-lib onto cantoo primitives, or a merge-bytes approach. None are in v1.3.6 scope. The `SIGNATURE_DEP_MISSING` message now pre-warns callers.
254
+
255
+ ## [1.3.5] — 2026-05-22
256
+
257
+ ### Fixed
258
+
259
+ - **`toc-entry` drift-guard regression** —
260
+ `test/drift-guards.test.ts` was failing because `src/validate.ts` had no
261
+ `case 'toc-entry':` arm. `toc-entry` elements are produced internally by
262
+ the TOC two-pass processor, but the drift guard correctly insists every
263
+ registered `ElementType` has a validator case. Added a defensive validator
264
+ for `text`, `pageNumber`, `level`, `levelIndent`, and `leader` so
265
+ user-authored `toc-entry` payloads (rare but possible) fail loud instead
266
+ of slipping through.
267
+
268
+ ### Removed
269
+
270
+ - **`test/pretext-api-contract.test.ts`** —
271
+ Reframed in v1.3.3 as a local export-shape guard for the vendored pretext
272
+ layout module, but the test was tautological: it could only fail if
273
+ someone hand-edited `src/vendor/pretext/*.ts` to remove an export, in
274
+ which case TypeScript would already fail the build. Deleted along with
275
+ its entry in the `test:contract` npm script. Drift guards in
276
+ `test/drift-guards.test.ts` cover the real risk (registry vs. switch
277
+ arms going out of sync).
278
+
279
+ ### Performance
280
+
281
+ - **v1.3.2+ benchmark numbers captured** —
282
+ Re-ran the seven core corpora against `benchmarks/benchmark-baseline.json`
283
+ (recorded 2026-04-10 at v1.3.0). Results documented in
284
+ `benchmarks/v1.3.2-results.md`: ~1.66x geometric-mean speedup across
285
+ corpora, with the largest wins on text-heavy workloads (table-stress
286
+ -52%, punctuation-heavy -51%, rtl-layout -43%). Confirms the DNS dedup,
287
+ parallel raster, and word-width cache work landed in v1.3.2 was real
288
+ rather than aspirational.
289
+
290
+ ### Docs
291
+
292
+ - **README version table extended** —
293
+ Added 1.1.x (vendor switch), 1.2.x (security + benchmarks), and
294
+ 1.3.0–1.3.4 (perf + drift guards) rows so the version table no longer
295
+ stops at 1.0.6.
296
+
297
+ ### Tooling
298
+
299
+ - **`scripts/run-bench-snapshot.mjs`** —
300
+ Small one-shot runner that prints avg/min render time per corpus across
301
+ three measured runs (after a warmup). Used to capture the v1.3.5
302
+ benchmark results above; useful for ad-hoc perf checks without touching
303
+ the regression-guarded `test/benchmark-baseline.test.ts`.
304
+
305
+ ---
306
+
307
+ ## [1.3.4] — 2026-05-17
308
+
309
+ ### Fixed
310
+
311
+ - **DNS dedup test now imports from source** —
312
+ `test/assets-dns-dedup.test.ts` previously imported `fetchWithTimeout` /
313
+ `assertSafeUrl` from `../dist/assets.js`, which would silently pass against
314
+ a stale build (false confidence). Switched to `../src/assets.js` so the
315
+ test always runs against the current source tree under tsx.
316
+
317
+ ### Added
318
+
319
+ - **FIFO eviction boundary test for word-width cache** —
320
+ `test/measure-text-cache.test.ts` now asserts that when the cache is
321
+ pre-filled to `WORD_WIDTH_CACHE_MAX` and a new `measureWord` call is made,
322
+ `cache.size` stays at the cap, the oldest insertion (`syn0`) is evicted,
323
+ and a re-accessed entry (`syn1`) survives — proving FIFO semantics, not LRU.
324
+
325
+ ### Changed
326
+
327
+ - **Constant tunability scope documented** — `VECTOR_RASTER_CONCURRENCY` and
328
+ `WORD_WIDTH_CACHE_MAX` are exported as read-only constants for observability
329
+ and test introspection. Consumers wanting different values must fork;
330
+ runtime tunability (env vars or options) is a future enhancement and not
331
+ planned for the v1.x line.
332
+
333
+ ---
334
+
335
+ ## [1.3.3] — 2026-05-17
336
+
337
+ ### Fixed
338
+
339
+ - **Parallel rasterization concurrency cap** — `loadVectorAssets` now runs at
340
+ most 4 SVG/QR/barcode/chart rasterization tasks concurrently (was unbounded).
341
+ Prevents file-descriptor / worker exhaustion on documents with many vector
342
+ assets. New exported constant: `VECTOR_RASTER_CONCURRENCY`.
343
+ - **Word-width cache memory bound** — `measureWord` now FIFO-evicts at 50,000
344
+ entries to bound memory for long-running processes that reuse a single
345
+ `wordWidthCache`. New exported constant: `WORD_WIDTH_CACHE_MAX`.
346
+
347
+ ### Changed
348
+
349
+ - **Pretext API contract test reframed** — `test/pretext-api-contract.test.ts`
350
+ header clarified: this is a local export-shape guard for the vendored
351
+ pretext layout module, not an upstream version canary. Pretext has been
352
+ vendored at `src/vendor/pretext/` since v1.1.0.
353
+ - **CHANGELOG clarification on v1.3.2 parallel rasterization** — see updated
354
+ v1.3.2 entry below; the speedup is real for I/O-bound fan-out, but CPU
355
+ rasterization still serializes on the V8 main thread.
356
+
357
+ ### Documented (not changed)
358
+
359
+ - **Word-width cache scope (H1)** — the cache is currently consulted on the
360
+ hyphenation path (`measureTextWithHyphenation`) only. The non-hyphenation
361
+ branch of `measureText` delegates directly to pretext's `layoutWithLines`
362
+ to preserve CJK character-level breaking, RTL/bidi, Thai segmentation,
363
+ kerning, and justify semantics that word-by-word summing would diverge
364
+ from. Documents that do not configure a hyphenator will not see
365
+ cross-paragraph cache reuse; this is intentional for correctness.
366
+
367
+ ---
368
+
369
+ ## [1.3.2] — 2026-05-17
370
+
371
+ ### Performance
372
+
373
+ - Removed double DNS resolution in image/SVG fetch (one lookup per remote asset, not two)
374
+ - Parallel SVG/QR/barcode generation+rasterization (sequential embed retained for pdf-lib safety)
375
+ - Sub-note (added in v1.3.3): the parallelism is real for the I/O-bound fan-out
376
+ (remote SVG fetches overlap). CPU rasterization (sharp/svg2pdfkit) still
377
+ contends on the V8 main thread, so wall-clock improvement is dominated by
378
+ remote-asset latency, not raster throughput.
379
+ - Document-level word-width measurement cache (cross-paragraph dedup of common-word measurements)
380
+ - Sub-note (added in v1.3.3): the cache is consulted on the hyphenation code
381
+ path only. See v1.3.3 "Documented (not changed)" for the rationale.
382
+
383
+ ---
384
+
385
+ ## [1.3.1] — 2026-05-17
386
+
387
+ ### Fixed
388
+
389
+ - Internal test fixtures updated to set `allowedFileDirs` (resolved with `path.resolve` for Windows drive-letter compatibility) after the v1.2.2 deny-by-default flip — no library behavior change. Affected: `signatures-crypto`, `signatures-validation`, `svg`, `image-floats` test files.
390
+ - `markdown-gfm` compat test updated to use an `https:` image src; `data:` URLs are blocked by the scheme guard added in v1.3.0, so the pre-existing fixture no longer round-tripped — test-only change.
391
+
392
+ ---
393
+
394
+ ## [1.3.0] — 2026-05-17
395
+
396
+ ### ⚠️ BREAKING (retroactive note covering v1.2.2)
397
+
398
+ - **`assertPathAllowed` is now deny-by-default.** Documents using `file://` image sources without an explicit `allowedFileDirs` configuration will throw `PATH_TRAVERSAL`. This was shipped in v1.2.2 as a security fix but is technically a breaking change — consumers on `^1.2.0` who upgrade past v1.2.1 must either set `allowedFileDirs` or migrate away from `file://` sources. v1.3.0 is the recommended upgrade target with full semver signal.
399
+
400
+ ### Fixed
401
+
402
+ - **Scheme guard whitespace bypass** in `compat.ts` — leading whitespace in image src (e.g. `" file:///etc/passwd"`) no longer bypasses scheme stripping.
403
+ - **Extended scheme blocklist** in `compat.ts` — added `vbscript:`, `blob:`, `about:` alongside existing `file://`, `data:`, `javascript:`.
404
+
405
+ ### Tests
406
+
407
+ - Added redirect-chain SSRF test using a local mock HTTP server.
408
+ - Pinned CLI exit-code assertion to detect regressions.
409
+
410
+ ---
411
+
412
+ ## [1.2.2] — 2026-05-17
413
+
414
+ ### Security
415
+
416
+ - **`assertPathAllowed` is now deny-by-default** — Previously, when `doc.allowedFileDirs` was
417
+ undefined or empty, local file:// paths were silently allowed. Now the function throws
418
+ `PATH_TRAVERSAL` unless `allowedFileDirs` is explicitly configured with at least one directory.
419
+ This closes an unintended open-access footgun for server-side deployments.
420
+
421
+ - **`compat.ts` — dangerous image schemes stripped in `fromPdfmake`** — `file://`, `data:`, and
422
+ `javascript:` image `src` values are now silently dropped during pdfmake→pretext-pdf translation
423
+ rather than forwarded verbatim. This prevents the compat shim from acting as an indirect
424
+ bypass for the scheme-level SSRF guards in `assets.ts`.
425
+
426
+ - **`compat.ts` — `allowedFileDirs` forwarded from `PdfmakeDocument`** — The `PdfmakeDocument`
427
+ interface now accepts `allowedFileDirs?: string[]`, which is forwarded into the resulting
428
+ `PdfDocument`. Callers who previously passed file paths via the compat shim can now
429
+ allowlist their directories explicitly.
430
+
431
+ - **CLI validates before rendering** — `pretext-pdf` now calls `validateDocument()` before
432
+ invoking `render()`. Invalid documents produce a `VALIDATION_ERROR` message on stderr and
433
+ exit with code 1, avoiding wasted work during the render phase.
434
+
435
+ ---
436
+
437
+ ## [1.2.1] — 2026-05-16
438
+
439
+ ### Fixed
440
+
441
+ - **`PretextPdfError` now preserves root cause via `err.cause`** — `ASSEMBLY_FAILED` errors
442
+ thrown by `merge()` and `assemble()` now carry the original pdf-lib error as `err.cause`,
443
+ making root-cause debugging possible without losing the upstream message.
444
+
445
+ - **`form.updateFieldAppearances()` failure is now logged** — Previously silently swallowed
446
+ (`catch { /* non-fatal */ }`). Now emits a structured warning via the document logger or
447
+ `console.warn`. Behaviour is unchanged (non-fatal); the warning aids debugging.
448
+
449
+ - **Owner-only encryption now warns explicitly** — When `doc.encryption` is set without
450
+ `userPassword`, a `console.warn` is emitted explaining that the PDF will open without a
451
+ password. Owner-only encryption remains valid (it restricts editing/printing, not opening).
452
+
453
+ - **`assemble([{}])` now throws `VALIDATION_ERROR`** — Regression from v1.2.0 discriminated
454
+ union changes: passing a part with neither `doc` nor `pdf` previously crashed with a
455
+ `TypeError` (no `.code` property). Now throws a proper `VALIDATION_ERROR` with a clear
456
+ message before attempting to render.
457
+
458
+ - **`watermark: {}` now throws `VALIDATION_ERROR`** — Regression from v1.2.0: the
459
+ `WatermarkSpec` discriminated union enforced text/image presence at compile-time but the
460
+ runtime validation was missing. A watermark object with neither `text` nor `image` now
461
+ correctly throws at validate time.
462
+
463
+ - **`svg: ''` (empty string) now throws `VALIDATION_ERROR`** — Empty SVG strings passed
464
+ the validation stage and surfaced as `SVG_LOAD_FAILED` during render. Now caught at
465
+ validate time with a clear `VALIDATION_ERROR`.
466
+
467
+ ### Changed
468
+
469
+ - **`PretextPdfError` constructor accepts optional `ErrorOptions`** — Third argument
470
+ `options?: ErrorOptions` (i.e. `{ cause?: unknown }`) is now accepted and passed to the
471
+ native `Error` constructor. Fully backwards-compatible — all existing call sites unchanged.
472
+
473
+ - **bidi-js missing-peer warning routes through document logger** — When a `logger` is
474
+ passed to `render()`, bidi-js peer-dependency warnings are now routed through
475
+ `logger.warn` instead of always using `console.warn`. New low-level export:
476
+ `setBidiWarnFn(fn)` (prefer the `logger` render option in application code).
477
+
478
+ ---
479
+
480
+ ## [1.2.0] — 2026-05-16
481
+
482
+ Post-audit hardening release. Type system tightened, concurrency-safe validation,
483
+ @internal type leaks closed, RTL/asset failures surface as structured errors,
484
+ SSRF defense upgraded to undici-pinned IP. No source-level API removals from the
485
+ package entry point — see migration notes below for `@internal` types that were
486
+ already not exported from `src/index.ts`.
487
+
488
+ ### Added
489
+
490
+ - **Discriminated unions on four public types** (`src/types-public.ts`, audit Phase B) —
491
+ `WatermarkSpec`, `AssemblyPart`, `SvgElement`, and `ImageElement` (float variants)
492
+ now use TypeScript discriminated unions instead of flat optional structs. The
493
+ compiler now prevents invalid combinations (e.g., a watermark with both `text`
494
+ and `image`, or an SVG element with neither `svg` nor `src`) that previously
495
+ could only be caught at runtime. Existing valid usages continue to compile.
496
+
497
+ - **`pdf-lib` type augmentation** (`src/vendor/pdf-lib-augment.d.ts`, audit Phase D) —
498
+ Documents the load-bearing `as any` casts against pdf-lib internals
499
+ (`PDFArray.push`, `PDFFont.embedder`) and removes the one genuinely-avoidable
500
+ cast in `measure.ts`.
501
+
502
+ - **DNS rebinding defense via undici Agent IP pinning** (`src/assets.ts`, audit B6) —
503
+ `assertSafeUrl` was upgraded to `resolveAndValidateUrl` which returns the
504
+ resolved IP, and `fetchWithTimeout` now uses an undici `Agent` whose
505
+ `connect.lookup` callback always returns the pre-validated IP. Closes the
506
+ TOCTOU window where DNS could rebind between validation and connect.
507
+ Extended private-range coverage: 0.0.0.0/8, 192.0.0/24, 198.18/15, IPv6
508
+ multicast, and IPv4-mapped IPv6 normalization. +14 SSRF tests (25 total).
509
+
510
+ - **Per-call cycle-detection state** (`src/validate.ts`, audit Perf-1) — Moved
511
+ `seenInRecursion` WeakSet from module scope into `validate()` and threaded
512
+ through `withCycleGuard`. Makes validation reentrant and concurrency-safe
513
+ (no shared mutable state across parallel `validate()` calls). +1 regression
514
+ test.
515
+
516
+ - **Structured error codes on RTL + asset failures** (`src/errors.ts`,
517
+ `src/measure-text.ts`, `src/assets.ts`, audit silent-failure pass) —
518
+ - `RTL_REORDER_FAILED` — surfaces when `bidi-js` is installed but throws.
519
+ Previously fell through with `isRTL:true` on logical-order text =
520
+ visually broken Arabic/Hebrew renders. Missing `bidi-js` still degrades
521
+ gracefully (warn + LTR render) since it is an optional peer dep.
522
+ - `CHART_LOAD_FAILED` — embedded in warn logs from QR/barcode/chart loaders
523
+ so failures are debuggable from log scraping alone.
524
+ - `FONT_ENCODE_FAIL` — replaces the prior bare `catch` in `src/fonts.ts`
525
+ that silently swallowed font subset failures (audit B2).
526
+
527
+ ### Changed
528
+
529
+ - **`@internal` types removed from the `types.ts` barrel** (audit H8 / type-design HIGH) —
530
+ `RichLine`, `RichFragment`, and `TocEntryElement` were tagged `@internal`
531
+ but re-exported through `src/types.ts`. They have been removed from that
532
+ barrel and canonicalized in `src/types-internal.ts`. `TocEntryElement` is
533
+ no longer a member of the public `ContentElement` union (it was always
534
+ pipeline-synthesized, never user-constructed). Internal imports updated
535
+ in `rich-text.ts`, `measure.ts`, `render-extras.ts`, `allowed-props.ts`,
536
+ `validate.ts`, `measure-blocks.ts`, `fonts.ts`. **Migration note:** these
537
+ types were never exported from `src/index.ts` (the package entry point),
538
+ so consumers using the supported import path (`import { ... } from
539
+ 'pretext-pdf'`) are unaffected. Deep imports (`'pretext-pdf/src/types'`)
540
+ are unsupported and never were stable.
541
+
542
+ - **`api-extractor` enforcement escalated to `error`** (`api-extractor.json`) —
543
+ `ae-forgotten-export` log level changed from `warning` to `error` so CI
544
+ fails on future `@internal` type leaks. `etc/pretext-pdf.api.md` baseline
545
+ regenerated.
546
+
547
+ - **`assertUnknownProps` parameter tightened** (`src/validate.ts`) —
548
+ `obj: any` → `obj: unknown` with an explicit type guard at the boundary.
549
+ Removes one of the few remaining `any` exposures at a security-sensitive
550
+ validation entrypoint.
551
+
552
+ ### Fixed
553
+
554
+ - **Concurrent validation false positives** (`src/validate.ts`) — Two
555
+ simultaneous `validateDocument(doc)` calls on the same object reference
556
+ could produce a false "cyclic reference detected" error because the
557
+ WeakSet was at module scope. Per-call WeakSet fix closes this and any
558
+ future re-entrant validator scenarios.
559
+
560
+ - **Stale `tests-743` badge** (`README.md`) — Replaced with the durable
561
+ `tests-passing` (current unit count is 319).
562
+
563
+ - **Dead `case 'toc-entry'` branch** (`src/fonts.ts:collectTextByFont`) —
564
+ After `TocEntryElement` left the public `ContentElement` union, the
565
+ branch became unreachable.
566
+
567
+ ### Migration notes (v1.1.x → v1.2.0)
568
+
569
+ If you only import from `'pretext-pdf'`: **no source changes needed.**
570
+
571
+ If you do unsupported deep imports of internal types
572
+ (`'pretext-pdf/src/types'`):
573
+ - `RichLine`, `RichFragment`, `TocEntryElement` — import from
574
+ `'pretext-pdf/src/types-internal'` instead, with the understanding that
575
+ these are not part of the stable public API and may change in any release.
576
+
577
+ If you construct `WatermarkSpec` / `AssemblyPart` / `SvgElement` / `ImageElement`
578
+ literals: you may need to delete fields you weren't using anyway. TypeScript
579
+ will flag any literals that previously satisfied the loose type but violated
580
+ the actual invariants.
581
+
582
+ ---
583
+
584
+ ## [1.1.3] — 2026-05-15
585
+
586
+ ### Added
587
+
588
+ - **Cycle detection + depth cap on TableElement walk** (`src/validate.ts`, Sprint 3 / M2) —
589
+ The rows/cells iteration is now wrapped in `withCycleGuard`, matching the
590
+ protection already in place for `ListItem.items`, `FloatGroup.content`, and
591
+ `RichParagraph.spans`. A self-referential row or cell shape now produces a
592
+ structured `VALIDATION_ERROR` instead of an unbounded walk.
593
+
594
+ - **Root-level depth guard for `document.content` entries** (`src/validate.ts`, Sprint 3 / M1) —
595
+ Each top-level element call into `validateElement` now runs an explicit
596
+ `assertDepthOk(depth, prefix)` so the `MAX_VALIDATION_DEPTH = 32` cap fires
597
+ even for plugin-typed elements that do not open their own `withCycleGuard`
598
+ scope. Internal recursive walks (`list`, `float-group`, `rich-paragraph`,
599
+ `table`) continue to enforce the cap via `withCycleGuard`.
600
+
601
+ - **Round-trip tests for the pdfmake compatibility shim** (`test/compat.test.ts`, Sprint 3 / M3) —
602
+ Four new tests covering pdfmake → pretext → render integration, style
603
+ propagation, sanity rendering of native pretext docs, and large-table
604
+ preservation (5 columns × 10 rows).
605
+
606
+ - **`## Validation` section in `README.md`** (Sprint 3 / M4) — Explicit guidance
607
+ to call `validateDocument()` before `render()` on untrusted input, with the
608
+ concrete failure modes (stack overflow on cyclic input, prototype pollution
609
+ via `__proto__`, runtime 500s on malformed shapes) the validator prevents.
610
+
611
+ ### Fixed
612
+
613
+ - **Type safety in validateDocument** (`src/validate.ts`) — Replaced unchecked `as PdfDocument` cast with `isValidPdfDocumentLike()` type guard. Returns proper error when input is not a plain object.
614
+
615
+ - **Prototype pollution in mergeStyles** (`src/compat.ts`) — `Object.assign(merged, s)` allowed user-supplied pdfmake JSON to pollute the prototype chain. Replaced with `copySafeStyleProperties()` that whitelists only known safe style keys (fontSize, bold, italics, color, alignment, font).
616
+
617
+ - **Path traversal in digital signatures** (`src/post-process.ts`) — P12 certificate path bypassed the `allowedFileDirs` security check. Now validates path via `assertPathAllowed()` before reading, preventing directory traversal attacks via signature feature.
618
+
619
+ - **Fragile errorCount regex in validateDocument** (`src/validate.ts`) — Original regex could match anywhere in error message. Refined to header-only pattern (`^Strict validation failed`) to extract true error count even when >20 errors are returned (capped array but accurate count in message).
620
+
621
+ - **Fake test coverage** (`test/validate-document.test.ts`) — Removed describe block with `assert.ok(true, 'TODO')` placeholder. Replaced with documentation explaining why the non-PretextPdfError code path is manually audited.
622
+
623
+ - **Missing LICENSE for vendored code** (`src/vendor/pretext/LICENSE`) — Added MIT license file with attribution to upstream pretext library and this fork, satisfying legal compliance for vendored dependencies.
624
+
625
+ ### Documentation
626
+
627
+ - **`Logger` interface guidance** (`src/types-public.ts`, audit L2) — Expanded
628
+ JSDoc on the `Logger` interface and the `logger?` field on `RenderOptions`
629
+ to call out that passing a no-op (`{ warn: () => {} }`) silences **every**
630
+ advisory warning — fine in tests, dangerous in production. Documents the
631
+ default (`console.warn`) and recommends pino/winston for production
632
+ routing. No code change.
633
+
634
+ - **pdfmake `defaultStyle` / `styles` mapping** (`src/compat.ts`, audit L3) —
635
+ Added JSDoc to `PdfmakeStyle` enumerating the supported subset
636
+ (`font`, `fontSize`, `bold`, `italics`, `color`, `alignment`) and the
637
+ silently-dropped pdfmake properties (`lineHeight`, `marginX/Y`,
638
+ `decoration`, `background`, `characterSpacing`, `noWrap`, etc.) that
639
+ consumers migrating from pdfmake commonly trip over. `fromPdfmake()`
640
+ now documents how `defaultStyle.font` / `.fontSize` route to
641
+ document-level `defaultFont` / `defaultFontSize` and how all other
642
+ `defaultStyle` properties cascade through `mergeStyles()`.
643
+
644
+ ### Changed
645
+
646
+ - **Benchmark floor override** (`test/benchmark-baseline.test.ts`, audit L4) —
647
+ The per-corpus regression guard now honors a `PRETEXT_BENCHMARK_FLOOR_MS`
648
+ environment variable. Set it to a positive integer to raise the 5000ms
649
+ default floor on slow CI runners; set it to `skip` / `0` / `false` / `off`
650
+ to bypass the timing assertion entirely. The structural assertions (corpus
651
+ IDs match baseline, stages present) still run.
652
+
653
+ ### Notes — Phase A / B / D history
654
+
655
+ The cycle-detection and depth-cap machinery (`withCycleGuard`,
656
+ `MAX_VALIDATION_DEPTH`, the per-container guards on `list`, `float-group`,
657
+ `rich-paragraph`) and the discriminated-union refactor of `ContentElement`
658
+ plus the typed `pdf-lib` augmentation were landed during in-flight audit
659
+ sprints between `[1.0.x]` and `[1.1.0]` that were not individually tagged.
660
+ Sprint 3 (this release) backfills those gaps with the M1/M2 root + table
661
+ guards, plus explicit tests and documentation.
662
+
663
+ ---
664
+
665
+ ## [1.1.2] — 2026-05-08
666
+
667
+ ### Fixed
668
+
669
+ - **Silent font-subset failure** (`src/fonts.ts`) — Bare `catch {}` on `pdfFont.encodeText()`
670
+ silently swallowed glyph-encoding errors, producing wrong characters with no signal.
671
+ Now logs a `console.warn` so callers know which font key failed.
672
+
673
+ - **Explicit RTL direction silently flipping to LTR** (`src/measure-text.ts`) — When
674
+ `dir:'rtl'` was set and `bidi-js` threw during reordering, the fallback incorrectly
675
+ returned `isRTL: false`, causing Arabic/Hebrew paragraphs to align and wrap as LTR.
676
+ The fallback now preserves `isRTL: true` so the layout engine honours the explicit
677
+ direction even without bidi reordering.
678
+
679
+ - **SSRF DNS rebinding window** (`src/assets.ts`) — `assertSafeUrl()` was synchronous.
680
+ An attacker with TTL=0 DNS could pass the hostname check then rebind to `169.254.x.x`
681
+ between the check and the actual `fetch()` call. The function is now async and
682
+ pre-resolves hostnames via `dns.lookup()` before the private-range check, closing
683
+ the TOCTOU window. Falls back gracefully when DNS is unavailable (fetch will also
684
+ fail in that case). All call sites updated to `await assertSafeUrl()`.
685
+
686
+ - **Concurrent PDFDocument mutation race** (`src/pipeline.ts`) — `loadFonts` and
687
+ `loadImages` were run with `Promise.all()` over the same `PDFDocument` instance.
688
+ Both mutate the cross-reference table, causing intermittent xref corruption under
689
+ load. Now sequenced: `loadFonts` completes before `loadImages` begins.
690
+
691
+ - **Test suite cascade: 692 tests silently dropped on benchmark failure** (`package.json`,
692
+ `scripts/test-all.mjs`) — The `&&`-chained `npm test` command aborted all downstream
693
+ stages when `test:contract` failed. Replaced with a Node.js runner that executes all
694
+ 4 stages and collects failures. Benchmark is now in a separate `test:benchmark` script
695
+ (not in `test:contract`) with `FLOOR_MS` raised to 5s to absorb dev-hardware variance.
696
+
697
+ ---
698
+
10
699
  ## [1.1.1] — 2026-05-08
11
700
 
12
701
  ### Fixed