@stackforge-eu/factur-x 1.0.7 → 1.0.8

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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.8] — 2026-05-27
11
+
12
+ - **PDF trailer was missing the `/ID` entry required by PDF/A-3.** ISO 19005-3
13
+ (and PDF 1.7 §14.4) require the trailer dictionary to carry an `/ID` array of
14
+ two byte strings: the first immutable identifier of the original document,
15
+ the second updated on every modification. `pdf-lib` initializes
16
+ `trailerInfo = {}` and never writes one on save, so veraPDF and the FNFE-MPE
17
+ Factur-X profile rejected every PDF this library produced as non-conformant.
18
+ Added `ensureIdTrailer` in `src/core/embed.ts` which runs after the first
19
+ `pdfDoc.save()` when `addPdfA3Metadata` is on: it computes an MD5 over the
20
+ serialized bytes (per PDF 1.7 §14.4 — uniqueness, not cryptographic strength,
21
+ is what matters), preserves any pre-existing first identifier, and writes the
22
+ pair before re-saving. Regression tests added in `tests/embed.test.ts`
23
+ covering both the no-existing-trailer and existing-trailer paths (thanks
24
+ @sco-indy, #3).
25
+
10
26
  ## [1.0.7] — 2026-05-27
11
27
 
12
28
  - **`fx:ConformanceLevel` in the XMP metadata used the raw `Profile` enum
@@ -113,6 +129,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
113
129
  - Deno compatibility test suite
114
130
 
115
131
  [Unreleased]: https://github.com/StackForge-EU/factur-x/commits/main
132
+ [1.0.8]: https://github.com/StackForge-EU/factur-x/compare/v1.0.7...v1.0.8
116
133
  [1.0.7]: https://github.com/StackForge-EU/factur-x/compare/v1.0.6...v1.0.7
117
134
  [1.0.6]: https://github.com/StackForge-EU/factur-x/compare/v1.0.5...v1.0.6
118
135
  [1.0.5]: https://github.com/StackForge-EU/factur-x/compare/v1.0.4...v1.0.5
package/dist/index.cjs CHANGED
@@ -4,6 +4,7 @@ var fs = require('fs');
4
4
  var path = require('path');
5
5
  var url = require('url');
6
6
  var console = require('console');
7
+ var crypto = require('crypto');
7
8
  var pdfLib = require('pdf-lib');
8
9
 
9
10
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -1122,6 +1123,17 @@ function resolveXmpConformanceLevel(profile, flavor) {
1122
1123
  if (flavor === "xrechnung" /* XRECHNUNG */) return "XRECHNUNG";
1123
1124
  return XMP_CONFORMANCE_LEVEL[profile];
1124
1125
  }
1126
+ function ensureIdTrailer(pdfDoc, pdfContentBytes) {
1127
+ const existingIdTrailer = pdfDoc.context.trailerInfo.ID;
1128
+ const currentContentHash = pdfLib.PDFHexString.of(crypto.createHash("md5").update(pdfContentBytes).digest("hex"));
1129
+ const hasValidIdTrailer = existingIdTrailer instanceof pdfLib.PDFArray && existingIdTrailer.size() === 2;
1130
+ if (hasValidIdTrailer) {
1131
+ const existingFirstId = existingIdTrailer.lookup(0);
1132
+ pdfDoc.context.trailerInfo.ID = pdfDoc.context.obj([existingFirstId, currentContentHash]);
1133
+ } else {
1134
+ pdfDoc.context.trailerInfo.ID = pdfDoc.context.obj([currentContentHash, currentContentHash]);
1135
+ }
1136
+ }
1125
1137
  function ensureOutputIntent(pdfDoc, iccBytes, identifier) {
1126
1138
  const existing = pdfDoc.catalog.lookup(pdfLib.PDFName.of("OutputIntents"));
1127
1139
  if (existing instanceof pdfLib.PDFArray && existing.size() > 0) return;
@@ -1397,9 +1409,13 @@ async function embedFacturX(options) {
1397
1409
  const metadataRef = pdfDoc.context.register(metadataStream);
1398
1410
  pdfDoc.catalog.set(pdfLib.PDFName.of("Metadata"), metadataRef);
1399
1411
  }
1400
- const resultPdf = await pdfDoc.save();
1412
+ let pdfContentBytes = await pdfDoc.save();
1413
+ if (addPdfA3Metadata) {
1414
+ ensureIdTrailer(pdfDoc, pdfContentBytes);
1415
+ pdfContentBytes = await pdfDoc.save();
1416
+ }
1401
1417
  return {
1402
- pdf: resultPdf,
1418
+ pdf: pdfContentBytes,
1403
1419
  xml,
1404
1420
  ...validation ? { validation } : {},
1405
1421
  ...xsdValidation ? { xsdValidation } : {}
package/dist/index.js CHANGED
@@ -2,7 +2,8 @@ import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import console from 'console';
5
- import { AFRelationship, PDFDocument, PDFName, PDFArray, PDFDict, PDFStream, PDFRawStream, decodePDFRawStream, PDFString, PDFRef, PDFHexString } from 'pdf-lib';
5
+ import { createHash } from 'crypto';
6
+ import { AFRelationship, PDFDocument, PDFName, PDFArray, PDFDict, PDFStream, PDFRawStream, decodePDFRawStream, PDFString, PDFHexString, PDFRef } from 'pdf-lib';
6
7
 
7
8
  // src/types/input.ts
8
9
  var DocumentTypeCode = /* @__PURE__ */ ((DocumentTypeCode2) => {
@@ -1092,6 +1093,17 @@ function resolveXmpConformanceLevel(profile, flavor) {
1092
1093
  if (flavor === "xrechnung" /* XRECHNUNG */) return "XRECHNUNG";
1093
1094
  return XMP_CONFORMANCE_LEVEL[profile];
1094
1095
  }
1096
+ function ensureIdTrailer(pdfDoc, pdfContentBytes) {
1097
+ const existingIdTrailer = pdfDoc.context.trailerInfo.ID;
1098
+ const currentContentHash = PDFHexString.of(createHash("md5").update(pdfContentBytes).digest("hex"));
1099
+ const hasValidIdTrailer = existingIdTrailer instanceof PDFArray && existingIdTrailer.size() === 2;
1100
+ if (hasValidIdTrailer) {
1101
+ const existingFirstId = existingIdTrailer.lookup(0);
1102
+ pdfDoc.context.trailerInfo.ID = pdfDoc.context.obj([existingFirstId, currentContentHash]);
1103
+ } else {
1104
+ pdfDoc.context.trailerInfo.ID = pdfDoc.context.obj([currentContentHash, currentContentHash]);
1105
+ }
1106
+ }
1095
1107
  function ensureOutputIntent(pdfDoc, iccBytes, identifier) {
1096
1108
  const existing = pdfDoc.catalog.lookup(PDFName.of("OutputIntents"));
1097
1109
  if (existing instanceof PDFArray && existing.size() > 0) return;
@@ -1367,9 +1379,13 @@ async function embedFacturX(options) {
1367
1379
  const metadataRef = pdfDoc.context.register(metadataStream);
1368
1380
  pdfDoc.catalog.set(PDFName.of("Metadata"), metadataRef);
1369
1381
  }
1370
- const resultPdf = await pdfDoc.save();
1382
+ let pdfContentBytes = await pdfDoc.save();
1383
+ if (addPdfA3Metadata) {
1384
+ ensureIdTrailer(pdfDoc, pdfContentBytes);
1385
+ pdfContentBytes = await pdfDoc.save();
1386
+ }
1371
1387
  return {
1372
- pdf: resultPdf,
1388
+ pdf: pdfContentBytes,
1373
1389
  xml,
1374
1390
  ...validation ? { validation } : {},
1375
1391
  ...xsdValidation ? { xsdValidation } : {}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackforge-eu/factur-x",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "type": "module",
5
5
  "description": "Embed Factur-X, ZUGFeRD, and XRechnung structured invoice XML into PDF/A-3 documents",
6
6
  "keywords": [