pretext-pdf 1.1.1 → 1.6.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 +659 -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 +32 -9
  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 +26 -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,665 @@ Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/)
7
7
 
8
8
  ---
9
9
 
10
+ ## [1.6.0] — 2026-05-25
11
+
12
+ 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.
13
+
14
+ ### Security
15
+
16
+ - **SVG sanitizer hardening** — `sanitizeSvg` now strips three additional payload classes that survived the previous regex chain:
17
+ - **`<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.
18
+ - **`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.
19
+ - **CSS `expression(...)` calls inside `<style>` blocks** — legacy IE XSS vector. Only the `expression(...)` call site is excised; the surrounding stylesheet remains parseable.
20
+
21
+ 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).
22
+
23
+ ### Changed
24
+
25
+ - **`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.
26
+ - **`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.
27
+
28
+ ### Internal
29
+
30
+ - **New `src/assets/` directory layout:**
31
+ ```
32
+ src/assets/
33
+ ├── index.ts # internal barrel
34
+ ├── util/
35
+ │ └── redact-path.ts # commit 4
36
+ ├── security/
37
+ │ ├── path-allowlist.ts # commit 5
38
+ │ ├── ipv4-normalize.ts # commit 6
39
+ │ ├── url-validation.ts # commit 7
40
+ │ └── fetch.ts # commit 8 (undici Agent stays lazy)
41
+ ├── svg/
42
+ │ ├── sanitize.ts # commit 9 (+ SVG_MAX_BYTES)
43
+ │ ├── dimensions.ts # commit 10
44
+ │ ├── resolve-content.ts # commit 10
45
+ │ └── rasterize.ts # commit 11 (@napi-rs/canvas dynamic)
46
+ ├── generators/
47
+ │ ├── qr.ts # commit 12 (qrcode dynamic)
48
+ │ ├── barcode.ts # commit 12 (bwip-js dynamic)
49
+ │ └── chart.ts # commit 12 (vega/vega-lite dynamic)
50
+ └── loaders/
51
+ ├── images.ts # commit 13
52
+ ├── vectors.ts # commit 14
53
+ ├── watermark.ts # commit 15
54
+ └── orchestrator.ts # commit 15 (top-level loadImages)
55
+ ```
56
+ 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.
57
+ - **7 verification gates (G1–G7) added** to catch regressions during the split:
58
+ - **G1** Snapshot tripwire (`test/assets-split-tripwire.test.ts`) — 30 fixtures covering sanitizer output, URL normalization, path allowlist, and error code surface
59
+ - **G2** DNS lookup dedup (`test/assets-dns-dedup.test.ts`)
60
+ - **G3** SSRF blocking (`test/security-ssrf.test.ts`, expanded)
61
+ - **G4** Parallel-render concurrency (`test/assets-concurrency.test.ts`) — 10× concurrent `render()` calls must produce bit-identical PDFs
62
+ - **G5** ErrorCode stability (`test/assets-errorcode-stability.test.ts`)
63
+ - **G6** api-extractor diff against `etc/pretext-pdf.api.md`
64
+ - **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
65
+ - **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).
66
+
67
+ ---
68
+
69
+ ## [1.5.2] — 2026-05-25
70
+
71
+ Security hotfix. **No public API changes.** **Upgrade recommended for any deployment that accepts user-controlled image / SVG URLs.**
72
+
73
+ ### Security
74
+
75
+ - **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:
76
+ - Pure decimal: `https://2130706433/x` → 127.0.0.1
77
+ - Pure hex: `https://0x7f000001/x` → 127.0.0.1
78
+ - Octal octet: `https://0177.0.0.1/x` → 127.0.0.1
79
+ - Hex octet: `https://0x7f.0.0.1/x` → 127.0.0.1
80
+ - Short form: `https://127.1/x` → 127.0.0.1
81
+
82
+ 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.
83
+
84
+ **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.
85
+
86
+ **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.
87
+
88
+ ### Fixed
89
+
90
+ - **`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.
91
+
92
+ ---
93
+
94
+ ## [1.5.1] — 2026-05-24
95
+
96
+ 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).
97
+
98
+ ### Security
99
+
100
+ - **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`.
101
+
102
+ ### Fixed
103
+
104
+ - **`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}]\`)`.
105
+ - **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).
106
+ - **`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.
107
+ - **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.
108
+
109
+ ### Changed
110
+
111
+ - **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.
112
+ - **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.
113
+ - **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.
114
+ - **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.
115
+
116
+ ---
117
+
118
+ ## [1.5.0] — 2026-05-24
119
+
120
+ 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).
121
+
122
+ ### Changed (internal structure — non-breaking)
123
+
124
+ - **`src/validate/index.ts` (594L) → orchestrator (322L) + `src/validate/document.ts` (324L)** —
125
+ 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.
126
+ - **`src/measure-blocks/index.ts` (337L) → dispatcher (243L) + `src/measure-blocks/simple-blocks.ts` (151L)** —
127
+ 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).
128
+ - **`src/validate/elements/structural.ts` (239L grab-bag) → `structural-simple.ts` (120L) + `forms-floats.ts` (128L)** —
129
+ Split light element validators (spacer, hr, toc, toc-entry, comment) from heavy ones (form-field, footnote-def, float-group). Cleaner separation of concerns.
130
+
131
+ ### Added
132
+
133
+ - **`src/validate/elements/README.md`** — Placement guide + validator signature contract + `_ctx` policy documentation + `withCycleGuard` usage guidance. Onboarding aid for adding new element types.
134
+ - **`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.
135
+
136
+ ### Fixed
137
+
138
+ - **`dist/` no longer tracked in git** (168 files removed) — was already in `.gitignore` but tracked from prior history.
139
+
140
+ ### Notes
141
+
142
+ - 12 commits across 6 items, each independently revertable via the 3-commit-per-split pattern (stage → route → delete) proven in v1.4.0.
143
+ - Test suite: 416 pass / 1 skip / 0 fail throughout the sprint, plus the new snapshot tripwire test (passes against generated baseline).
144
+ - Public API surface unchanged: 15 runtime exports × 6 entry points.
145
+ - `_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.
146
+ - `loadedFamilies` initialization order: populated after `validateDocumentLevel` returns. Audit confirmed no order dependency — `document.ts` never reads `loadedFamilies`, only `validateFontSpec` for shape validation.
147
+ - Path-traversal pre-flight in `validateImage` deferred to v2.0 — runtime SSRF + `allowedFileDirs` guards already provide defense-in-depth.
148
+
149
+ ---
150
+
151
+ ## [1.4.1] — 2026-05-23
152
+
153
+ 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.
154
+
155
+ ### Fixed
156
+
157
+ - **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.
158
+ - **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).
159
+
160
+ ### Changed
161
+
162
+ - **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.
163
+ - **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.
164
+
165
+ ### Added
166
+
167
+ - **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.
168
+
169
+ ---
170
+
171
+ ## [1.4.0] — 2026-05-23
172
+
173
+ 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.
174
+
175
+ ### Changed (internal structure — non-breaking)
176
+
177
+ - **`src/validate.ts` (1834L) → `src/validate/` (9 files, ~250L each)** —
178
+ `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`.
179
+ - **`src/measure-blocks.ts` (1600L) → `src/measure-blocks/` (10 files)** —
180
+ `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.
181
+ - **`src/render-blocks.ts` (1277L) → `src/render-blocks/` (13 files)** —
182
+ Per-render-function modules. Each independent (no shared state). Dropped four unused imports from the original (`PDFFont`, `PDFName`, `renderTocEntry`, `renderFormField` — never referenced).
183
+ - **`src/types-public.ts` (1200L) → `src/types-public/` (8 files)** —
184
+ 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).
185
+
186
+ ### Added
187
+
188
+ - **`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.
189
+
190
+ ### Fixed
191
+
192
+ - **Dead imports removed:** `validate` was imported but never used in `src/builder.ts` after the split. Removed.
193
+ - **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).
194
+
195
+ ### Notes
196
+
197
+ - Public API surface tripwire: 15 runtime exports across 6 entry points, all unchanged.
198
+ - 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.
199
+ - 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).
200
+ - Known follow-up: type-only intra-cycles in `types-public/` (no runtime risk — erased at compile time).
201
+
202
+ ---
203
+
204
+ ## [1.3.6] — 2026-05-23
205
+
206
+ Architecture-sprint scaffolding release: vendor integrity check, concurrent isolation tests, signing import correctness, public API tripwire.
207
+
208
+ ### Added
209
+
210
+ - **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.
211
+ - **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.
212
+ - **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.
213
+ - **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.
214
+ - **`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.
215
+
216
+ ### Fixed
217
+
218
+ - **`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`).
219
+ - **`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.
220
+
221
+ ### KNOWN ISSUES (deferred to follow-up sprint)
222
+
223
+ - **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.
224
+
225
+ ## [1.3.5] — 2026-05-22
226
+
227
+ ### Fixed
228
+
229
+ - **`toc-entry` drift-guard regression** —
230
+ `test/drift-guards.test.ts` was failing because `src/validate.ts` had no
231
+ `case 'toc-entry':` arm. `toc-entry` elements are produced internally by
232
+ the TOC two-pass processor, but the drift guard correctly insists every
233
+ registered `ElementType` has a validator case. Added a defensive validator
234
+ for `text`, `pageNumber`, `level`, `levelIndent`, and `leader` so
235
+ user-authored `toc-entry` payloads (rare but possible) fail loud instead
236
+ of slipping through.
237
+
238
+ ### Removed
239
+
240
+ - **`test/pretext-api-contract.test.ts`** —
241
+ Reframed in v1.3.3 as a local export-shape guard for the vendored pretext
242
+ layout module, but the test was tautological: it could only fail if
243
+ someone hand-edited `src/vendor/pretext/*.ts` to remove an export, in
244
+ which case TypeScript would already fail the build. Deleted along with
245
+ its entry in the `test:contract` npm script. Drift guards in
246
+ `test/drift-guards.test.ts` cover the real risk (registry vs. switch
247
+ arms going out of sync).
248
+
249
+ ### Performance
250
+
251
+ - **v1.3.2+ benchmark numbers captured** —
252
+ Re-ran the seven core corpora against `benchmarks/benchmark-baseline.json`
253
+ (recorded 2026-04-10 at v1.3.0). Results documented in
254
+ `benchmarks/v1.3.2-results.md`: ~1.66x geometric-mean speedup across
255
+ corpora, with the largest wins on text-heavy workloads (table-stress
256
+ -52%, punctuation-heavy -51%, rtl-layout -43%). Confirms the DNS dedup,
257
+ parallel raster, and word-width cache work landed in v1.3.2 was real
258
+ rather than aspirational.
259
+
260
+ ### Docs
261
+
262
+ - **README version table extended** —
263
+ Added 1.1.x (vendor switch), 1.2.x (security + benchmarks), and
264
+ 1.3.0–1.3.4 (perf + drift guards) rows so the version table no longer
265
+ stops at 1.0.6.
266
+
267
+ ### Tooling
268
+
269
+ - **`scripts/run-bench-snapshot.mjs`** —
270
+ Small one-shot runner that prints avg/min render time per corpus across
271
+ three measured runs (after a warmup). Used to capture the v1.3.5
272
+ benchmark results above; useful for ad-hoc perf checks without touching
273
+ the regression-guarded `test/benchmark-baseline.test.ts`.
274
+
275
+ ---
276
+
277
+ ## [1.3.4] — 2026-05-17
278
+
279
+ ### Fixed
280
+
281
+ - **DNS dedup test now imports from source** —
282
+ `test/assets-dns-dedup.test.ts` previously imported `fetchWithTimeout` /
283
+ `assertSafeUrl` from `../dist/assets.js`, which would silently pass against
284
+ a stale build (false confidence). Switched to `../src/assets.js` so the
285
+ test always runs against the current source tree under tsx.
286
+
287
+ ### Added
288
+
289
+ - **FIFO eviction boundary test for word-width cache** —
290
+ `test/measure-text-cache.test.ts` now asserts that when the cache is
291
+ pre-filled to `WORD_WIDTH_CACHE_MAX` and a new `measureWord` call is made,
292
+ `cache.size` stays at the cap, the oldest insertion (`syn0`) is evicted,
293
+ and a re-accessed entry (`syn1`) survives — proving FIFO semantics, not LRU.
294
+
295
+ ### Changed
296
+
297
+ - **Constant tunability scope documented** — `VECTOR_RASTER_CONCURRENCY` and
298
+ `WORD_WIDTH_CACHE_MAX` are exported as read-only constants for observability
299
+ and test introspection. Consumers wanting different values must fork;
300
+ runtime tunability (env vars or options) is a future enhancement and not
301
+ planned for the v1.x line.
302
+
303
+ ---
304
+
305
+ ## [1.3.3] — 2026-05-17
306
+
307
+ ### Fixed
308
+
309
+ - **Parallel rasterization concurrency cap** — `loadVectorAssets` now runs at
310
+ most 4 SVG/QR/barcode/chart rasterization tasks concurrently (was unbounded).
311
+ Prevents file-descriptor / worker exhaustion on documents with many vector
312
+ assets. New exported constant: `VECTOR_RASTER_CONCURRENCY`.
313
+ - **Word-width cache memory bound** — `measureWord` now FIFO-evicts at 50,000
314
+ entries to bound memory for long-running processes that reuse a single
315
+ `wordWidthCache`. New exported constant: `WORD_WIDTH_CACHE_MAX`.
316
+
317
+ ### Changed
318
+
319
+ - **Pretext API contract test reframed** — `test/pretext-api-contract.test.ts`
320
+ header clarified: this is a local export-shape guard for the vendored
321
+ pretext layout module, not an upstream version canary. Pretext has been
322
+ vendored at `src/vendor/pretext/` since v1.1.0.
323
+ - **CHANGELOG clarification on v1.3.2 parallel rasterization** — see updated
324
+ v1.3.2 entry below; the speedup is real for I/O-bound fan-out, but CPU
325
+ rasterization still serializes on the V8 main thread.
326
+
327
+ ### Documented (not changed)
328
+
329
+ - **Word-width cache scope (H1)** — the cache is currently consulted on the
330
+ hyphenation path (`measureTextWithHyphenation`) only. The non-hyphenation
331
+ branch of `measureText` delegates directly to pretext's `layoutWithLines`
332
+ to preserve CJK character-level breaking, RTL/bidi, Thai segmentation,
333
+ kerning, and justify semantics that word-by-word summing would diverge
334
+ from. Documents that do not configure a hyphenator will not see
335
+ cross-paragraph cache reuse; this is intentional for correctness.
336
+
337
+ ---
338
+
339
+ ## [1.3.2] — 2026-05-17
340
+
341
+ ### Performance
342
+
343
+ - Removed double DNS resolution in image/SVG fetch (one lookup per remote asset, not two)
344
+ - Parallel SVG/QR/barcode generation+rasterization (sequential embed retained for pdf-lib safety)
345
+ - Sub-note (added in v1.3.3): the parallelism is real for the I/O-bound fan-out
346
+ (remote SVG fetches overlap). CPU rasterization (sharp/svg2pdfkit) still
347
+ contends on the V8 main thread, so wall-clock improvement is dominated by
348
+ remote-asset latency, not raster throughput.
349
+ - Document-level word-width measurement cache (cross-paragraph dedup of common-word measurements)
350
+ - Sub-note (added in v1.3.3): the cache is consulted on the hyphenation code
351
+ path only. See v1.3.3 "Documented (not changed)" for the rationale.
352
+
353
+ ---
354
+
355
+ ## [1.3.1] — 2026-05-17
356
+
357
+ ### Fixed
358
+
359
+ - 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.
360
+ - `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.
361
+
362
+ ---
363
+
364
+ ## [1.3.0] — 2026-05-17
365
+
366
+ ### ⚠️ BREAKING (retroactive note covering v1.2.2)
367
+
368
+ - **`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.
369
+
370
+ ### Fixed
371
+
372
+ - **Scheme guard whitespace bypass** in `compat.ts` — leading whitespace in image src (e.g. `" file:///etc/passwd"`) no longer bypasses scheme stripping.
373
+ - **Extended scheme blocklist** in `compat.ts` — added `vbscript:`, `blob:`, `about:` alongside existing `file://`, `data:`, `javascript:`.
374
+
375
+ ### Tests
376
+
377
+ - Added redirect-chain SSRF test using a local mock HTTP server.
378
+ - Pinned CLI exit-code assertion to detect regressions.
379
+
380
+ ---
381
+
382
+ ## [1.2.2] — 2026-05-17
383
+
384
+ ### Security
385
+
386
+ - **`assertPathAllowed` is now deny-by-default** — Previously, when `doc.allowedFileDirs` was
387
+ undefined or empty, local file:// paths were silently allowed. Now the function throws
388
+ `PATH_TRAVERSAL` unless `allowedFileDirs` is explicitly configured with at least one directory.
389
+ This closes an unintended open-access footgun for server-side deployments.
390
+
391
+ - **`compat.ts` — dangerous image schemes stripped in `fromPdfmake`** — `file://`, `data:`, and
392
+ `javascript:` image `src` values are now silently dropped during pdfmake→pretext-pdf translation
393
+ rather than forwarded verbatim. This prevents the compat shim from acting as an indirect
394
+ bypass for the scheme-level SSRF guards in `assets.ts`.
395
+
396
+ - **`compat.ts` — `allowedFileDirs` forwarded from `PdfmakeDocument`** — The `PdfmakeDocument`
397
+ interface now accepts `allowedFileDirs?: string[]`, which is forwarded into the resulting
398
+ `PdfDocument`. Callers who previously passed file paths via the compat shim can now
399
+ allowlist their directories explicitly.
400
+
401
+ - **CLI validates before rendering** — `pretext-pdf` now calls `validateDocument()` before
402
+ invoking `render()`. Invalid documents produce a `VALIDATION_ERROR` message on stderr and
403
+ exit with code 1, avoiding wasted work during the render phase.
404
+
405
+ ---
406
+
407
+ ## [1.2.1] — 2026-05-16
408
+
409
+ ### Fixed
410
+
411
+ - **`PretextPdfError` now preserves root cause via `err.cause`** — `ASSEMBLY_FAILED` errors
412
+ thrown by `merge()` and `assemble()` now carry the original pdf-lib error as `err.cause`,
413
+ making root-cause debugging possible without losing the upstream message.
414
+
415
+ - **`form.updateFieldAppearances()` failure is now logged** — Previously silently swallowed
416
+ (`catch { /* non-fatal */ }`). Now emits a structured warning via the document logger or
417
+ `console.warn`. Behaviour is unchanged (non-fatal); the warning aids debugging.
418
+
419
+ - **Owner-only encryption now warns explicitly** — When `doc.encryption` is set without
420
+ `userPassword`, a `console.warn` is emitted explaining that the PDF will open without a
421
+ password. Owner-only encryption remains valid (it restricts editing/printing, not opening).
422
+
423
+ - **`assemble([{}])` now throws `VALIDATION_ERROR`** — Regression from v1.2.0 discriminated
424
+ union changes: passing a part with neither `doc` nor `pdf` previously crashed with a
425
+ `TypeError` (no `.code` property). Now throws a proper `VALIDATION_ERROR` with a clear
426
+ message before attempting to render.
427
+
428
+ - **`watermark: {}` now throws `VALIDATION_ERROR`** — Regression from v1.2.0: the
429
+ `WatermarkSpec` discriminated union enforced text/image presence at compile-time but the
430
+ runtime validation was missing. A watermark object with neither `text` nor `image` now
431
+ correctly throws at validate time.
432
+
433
+ - **`svg: ''` (empty string) now throws `VALIDATION_ERROR`** — Empty SVG strings passed
434
+ the validation stage and surfaced as `SVG_LOAD_FAILED` during render. Now caught at
435
+ validate time with a clear `VALIDATION_ERROR`.
436
+
437
+ ### Changed
438
+
439
+ - **`PretextPdfError` constructor accepts optional `ErrorOptions`** — Third argument
440
+ `options?: ErrorOptions` (i.e. `{ cause?: unknown }`) is now accepted and passed to the
441
+ native `Error` constructor. Fully backwards-compatible — all existing call sites unchanged.
442
+
443
+ - **bidi-js missing-peer warning routes through document logger** — When a `logger` is
444
+ passed to `render()`, bidi-js peer-dependency warnings are now routed through
445
+ `logger.warn` instead of always using `console.warn`. New low-level export:
446
+ `setBidiWarnFn(fn)` (prefer the `logger` render option in application code).
447
+
448
+ ---
449
+
450
+ ## [1.2.0] — 2026-05-16
451
+
452
+ Post-audit hardening release. Type system tightened, concurrency-safe validation,
453
+ @internal type leaks closed, RTL/asset failures surface as structured errors,
454
+ SSRF defense upgraded to undici-pinned IP. No source-level API removals from the
455
+ package entry point — see migration notes below for `@internal` types that were
456
+ already not exported from `src/index.ts`.
457
+
458
+ ### Added
459
+
460
+ - **Discriminated unions on four public types** (`src/types-public.ts`, audit Phase B) —
461
+ `WatermarkSpec`, `AssemblyPart`, `SvgElement`, and `ImageElement` (float variants)
462
+ now use TypeScript discriminated unions instead of flat optional structs. The
463
+ compiler now prevents invalid combinations (e.g., a watermark with both `text`
464
+ and `image`, or an SVG element with neither `svg` nor `src`) that previously
465
+ could only be caught at runtime. Existing valid usages continue to compile.
466
+
467
+ - **`pdf-lib` type augmentation** (`src/vendor/pdf-lib-augment.d.ts`, audit Phase D) —
468
+ Documents the load-bearing `as any` casts against pdf-lib internals
469
+ (`PDFArray.push`, `PDFFont.embedder`) and removes the one genuinely-avoidable
470
+ cast in `measure.ts`.
471
+
472
+ - **DNS rebinding defense via undici Agent IP pinning** (`src/assets.ts`, audit B6) —
473
+ `assertSafeUrl` was upgraded to `resolveAndValidateUrl` which returns the
474
+ resolved IP, and `fetchWithTimeout` now uses an undici `Agent` whose
475
+ `connect.lookup` callback always returns the pre-validated IP. Closes the
476
+ TOCTOU window where DNS could rebind between validation and connect.
477
+ Extended private-range coverage: 0.0.0.0/8, 192.0.0/24, 198.18/15, IPv6
478
+ multicast, and IPv4-mapped IPv6 normalization. +14 SSRF tests (25 total).
479
+
480
+ - **Per-call cycle-detection state** (`src/validate.ts`, audit Perf-1) — Moved
481
+ `seenInRecursion` WeakSet from module scope into `validate()` and threaded
482
+ through `withCycleGuard`. Makes validation reentrant and concurrency-safe
483
+ (no shared mutable state across parallel `validate()` calls). +1 regression
484
+ test.
485
+
486
+ - **Structured error codes on RTL + asset failures** (`src/errors.ts`,
487
+ `src/measure-text.ts`, `src/assets.ts`, audit silent-failure pass) —
488
+ - `RTL_REORDER_FAILED` — surfaces when `bidi-js` is installed but throws.
489
+ Previously fell through with `isRTL:true` on logical-order text =
490
+ visually broken Arabic/Hebrew renders. Missing `bidi-js` still degrades
491
+ gracefully (warn + LTR render) since it is an optional peer dep.
492
+ - `CHART_LOAD_FAILED` — embedded in warn logs from QR/barcode/chart loaders
493
+ so failures are debuggable from log scraping alone.
494
+ - `FONT_ENCODE_FAIL` — replaces the prior bare `catch` in `src/fonts.ts`
495
+ that silently swallowed font subset failures (audit B2).
496
+
497
+ ### Changed
498
+
499
+ - **`@internal` types removed from the `types.ts` barrel** (audit H8 / type-design HIGH) —
500
+ `RichLine`, `RichFragment`, and `TocEntryElement` were tagged `@internal`
501
+ but re-exported through `src/types.ts`. They have been removed from that
502
+ barrel and canonicalized in `src/types-internal.ts`. `TocEntryElement` is
503
+ no longer a member of the public `ContentElement` union (it was always
504
+ pipeline-synthesized, never user-constructed). Internal imports updated
505
+ in `rich-text.ts`, `measure.ts`, `render-extras.ts`, `allowed-props.ts`,
506
+ `validate.ts`, `measure-blocks.ts`, `fonts.ts`. **Migration note:** these
507
+ types were never exported from `src/index.ts` (the package entry point),
508
+ so consumers using the supported import path (`import { ... } from
509
+ 'pretext-pdf'`) are unaffected. Deep imports (`'pretext-pdf/src/types'`)
510
+ are unsupported and never were stable.
511
+
512
+ - **`api-extractor` enforcement escalated to `error`** (`api-extractor.json`) —
513
+ `ae-forgotten-export` log level changed from `warning` to `error` so CI
514
+ fails on future `@internal` type leaks. `etc/pretext-pdf.api.md` baseline
515
+ regenerated.
516
+
517
+ - **`assertUnknownProps` parameter tightened** (`src/validate.ts`) —
518
+ `obj: any` → `obj: unknown` with an explicit type guard at the boundary.
519
+ Removes one of the few remaining `any` exposures at a security-sensitive
520
+ validation entrypoint.
521
+
522
+ ### Fixed
523
+
524
+ - **Concurrent validation false positives** (`src/validate.ts`) — Two
525
+ simultaneous `validateDocument(doc)` calls on the same object reference
526
+ could produce a false "cyclic reference detected" error because the
527
+ WeakSet was at module scope. Per-call WeakSet fix closes this and any
528
+ future re-entrant validator scenarios.
529
+
530
+ - **Stale `tests-743` badge** (`README.md`) — Replaced with the durable
531
+ `tests-passing` (current unit count is 319).
532
+
533
+ - **Dead `case 'toc-entry'` branch** (`src/fonts.ts:collectTextByFont`) —
534
+ After `TocEntryElement` left the public `ContentElement` union, the
535
+ branch became unreachable.
536
+
537
+ ### Migration notes (v1.1.x → v1.2.0)
538
+
539
+ If you only import from `'pretext-pdf'`: **no source changes needed.**
540
+
541
+ If you do unsupported deep imports of internal types
542
+ (`'pretext-pdf/src/types'`):
543
+ - `RichLine`, `RichFragment`, `TocEntryElement` — import from
544
+ `'pretext-pdf/src/types-internal'` instead, with the understanding that
545
+ these are not part of the stable public API and may change in any release.
546
+
547
+ If you construct `WatermarkSpec` / `AssemblyPart` / `SvgElement` / `ImageElement`
548
+ literals: you may need to delete fields you weren't using anyway. TypeScript
549
+ will flag any literals that previously satisfied the loose type but violated
550
+ the actual invariants.
551
+
552
+ ---
553
+
554
+ ## [1.1.3] — 2026-05-15
555
+
556
+ ### Added
557
+
558
+ - **Cycle detection + depth cap on TableElement walk** (`src/validate.ts`, Sprint 3 / M2) —
559
+ The rows/cells iteration is now wrapped in `withCycleGuard`, matching the
560
+ protection already in place for `ListItem.items`, `FloatGroup.content`, and
561
+ `RichParagraph.spans`. A self-referential row or cell shape now produces a
562
+ structured `VALIDATION_ERROR` instead of an unbounded walk.
563
+
564
+ - **Root-level depth guard for `document.content` entries** (`src/validate.ts`, Sprint 3 / M1) —
565
+ Each top-level element call into `validateElement` now runs an explicit
566
+ `assertDepthOk(depth, prefix)` so the `MAX_VALIDATION_DEPTH = 32` cap fires
567
+ even for plugin-typed elements that do not open their own `withCycleGuard`
568
+ scope. Internal recursive walks (`list`, `float-group`, `rich-paragraph`,
569
+ `table`) continue to enforce the cap via `withCycleGuard`.
570
+
571
+ - **Round-trip tests for the pdfmake compatibility shim** (`test/compat.test.ts`, Sprint 3 / M3) —
572
+ Four new tests covering pdfmake → pretext → render integration, style
573
+ propagation, sanity rendering of native pretext docs, and large-table
574
+ preservation (5 columns × 10 rows).
575
+
576
+ - **`## Validation` section in `README.md`** (Sprint 3 / M4) — Explicit guidance
577
+ to call `validateDocument()` before `render()` on untrusted input, with the
578
+ concrete failure modes (stack overflow on cyclic input, prototype pollution
579
+ via `__proto__`, runtime 500s on malformed shapes) the validator prevents.
580
+
581
+ ### Fixed
582
+
583
+ - **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.
584
+
585
+ - **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).
586
+
587
+ - **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.
588
+
589
+ - **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).
590
+
591
+ - **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.
592
+
593
+ - **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.
594
+
595
+ ### Documentation
596
+
597
+ - **`Logger` interface guidance** (`src/types-public.ts`, audit L2) — Expanded
598
+ JSDoc on the `Logger` interface and the `logger?` field on `RenderOptions`
599
+ to call out that passing a no-op (`{ warn: () => {} }`) silences **every**
600
+ advisory warning — fine in tests, dangerous in production. Documents the
601
+ default (`console.warn`) and recommends pino/winston for production
602
+ routing. No code change.
603
+
604
+ - **pdfmake `defaultStyle` / `styles` mapping** (`src/compat.ts`, audit L3) —
605
+ Added JSDoc to `PdfmakeStyle` enumerating the supported subset
606
+ (`font`, `fontSize`, `bold`, `italics`, `color`, `alignment`) and the
607
+ silently-dropped pdfmake properties (`lineHeight`, `marginX/Y`,
608
+ `decoration`, `background`, `characterSpacing`, `noWrap`, etc.) that
609
+ consumers migrating from pdfmake commonly trip over. `fromPdfmake()`
610
+ now documents how `defaultStyle.font` / `.fontSize` route to
611
+ document-level `defaultFont` / `defaultFontSize` and how all other
612
+ `defaultStyle` properties cascade through `mergeStyles()`.
613
+
614
+ ### Changed
615
+
616
+ - **Benchmark floor override** (`test/benchmark-baseline.test.ts`, audit L4) —
617
+ The per-corpus regression guard now honors a `PRETEXT_BENCHMARK_FLOOR_MS`
618
+ environment variable. Set it to a positive integer to raise the 5000ms
619
+ default floor on slow CI runners; set it to `skip` / `0` / `false` / `off`
620
+ to bypass the timing assertion entirely. The structural assertions (corpus
621
+ IDs match baseline, stages present) still run.
622
+
623
+ ### Notes — Phase A / B / D history
624
+
625
+ The cycle-detection and depth-cap machinery (`withCycleGuard`,
626
+ `MAX_VALIDATION_DEPTH`, the per-container guards on `list`, `float-group`,
627
+ `rich-paragraph`) and the discriminated-union refactor of `ContentElement`
628
+ plus the typed `pdf-lib` augmentation were landed during in-flight audit
629
+ sprints between `[1.0.x]` and `[1.1.0]` that were not individually tagged.
630
+ Sprint 3 (this release) backfills those gaps with the M1/M2 root + table
631
+ guards, plus explicit tests and documentation.
632
+
633
+ ---
634
+
635
+ ## [1.1.2] — 2026-05-08
636
+
637
+ ### Fixed
638
+
639
+ - **Silent font-subset failure** (`src/fonts.ts`) — Bare `catch {}` on `pdfFont.encodeText()`
640
+ silently swallowed glyph-encoding errors, producing wrong characters with no signal.
641
+ Now logs a `console.warn` so callers know which font key failed.
642
+
643
+ - **Explicit RTL direction silently flipping to LTR** (`src/measure-text.ts`) — When
644
+ `dir:'rtl'` was set and `bidi-js` threw during reordering, the fallback incorrectly
645
+ returned `isRTL: false`, causing Arabic/Hebrew paragraphs to align and wrap as LTR.
646
+ The fallback now preserves `isRTL: true` so the layout engine honours the explicit
647
+ direction even without bidi reordering.
648
+
649
+ - **SSRF DNS rebinding window** (`src/assets.ts`) — `assertSafeUrl()` was synchronous.
650
+ An attacker with TTL=0 DNS could pass the hostname check then rebind to `169.254.x.x`
651
+ between the check and the actual `fetch()` call. The function is now async and
652
+ pre-resolves hostnames via `dns.lookup()` before the private-range check, closing
653
+ the TOCTOU window. Falls back gracefully when DNS is unavailable (fetch will also
654
+ fail in that case). All call sites updated to `await assertSafeUrl()`.
655
+
656
+ - **Concurrent PDFDocument mutation race** (`src/pipeline.ts`) — `loadFonts` and
657
+ `loadImages` were run with `Promise.all()` over the same `PDFDocument` instance.
658
+ Both mutate the cross-reference table, causing intermittent xref corruption under
659
+ load. Now sequenced: `loadFonts` completes before `loadImages` begins.
660
+
661
+ - **Test suite cascade: 692 tests silently dropped on benchmark failure** (`package.json`,
662
+ `scripts/test-all.mjs`) — The `&&`-chained `npm test` command aborted all downstream
663
+ stages when `test:contract` failed. Replaced with a Node.js runner that executes all
664
+ 4 stages and collects failures. Benchmark is now in a separate `test:benchmark` script
665
+ (not in `test:contract`) with `FLOOR_MS` raised to 5s to absorb dev-hardware variance.
666
+
667
+ ---
668
+
10
669
  ## [1.1.1] — 2026-05-08
11
670
 
12
671
  ### Fixed