@stackforge-eu/factur-x 1.0.6 → 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 +37 -0
- package/dist/index.cjs +30 -3
- package/dist/index.js +31 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,41 @@ 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
|
+
|
|
26
|
+
## [1.0.7] — 2026-05-27
|
|
27
|
+
|
|
28
|
+
- **`fx:ConformanceLevel` in the XMP metadata used the raw `Profile` enum
|
|
29
|
+
identifier instead of the Factur-X / ZUGFeRD spec label.** The XMP packet
|
|
30
|
+
emitted `EN16931` / `BASIC_WL` where the spec requires `EN 16931` /
|
|
31
|
+
`BASIC WL`, so strict ZUGFeRD / Factur-X validators (Mustangproject, the
|
|
32
|
+
FNFE-MPE check tool, veraPDF's Factur-X profile) rejected every PDF this
|
|
33
|
+
library produced even though the embedded CII XML was correct. Added a
|
|
34
|
+
`Profile` → XMP-label mapping and routed `buildXmpMetadata` through it in
|
|
35
|
+
`src/core/embed.ts` (thanks @sco-indy, #2).
|
|
36
|
+
- **`flavor: Flavor.XRECHNUNG` still advertised `EN 16931` in the XMP packet.**
|
|
37
|
+
Hybrid XRechnung PDFs carry the EN16931 profile internally but per ZUGFeRD
|
|
38
|
+
2.x §2.4.4 must declare `XRECHNUNG` as the conformance level so the KoSIT /
|
|
39
|
+
ZUGFeRD validators route them through the XRechnung schematron set rather
|
|
40
|
+
than the vanilla Factur-X one. `resolveXmpConformanceLevel` in
|
|
41
|
+
`src/core/embed.ts` now special-cases the XRechnung flavor; the existing
|
|
42
|
+
`toXRechnung` → XML flow is unaffected. Regression test added in
|
|
43
|
+
`tests/embed.test.ts` covering all five profiles and the XRechnung flavor.
|
|
44
|
+
|
|
10
45
|
## [1.0.6] — 2026-05-23
|
|
11
46
|
|
|
12
47
|
- **`SpecifiedTradeSettlementPaymentMeans` emitted its child elements in the
|
|
@@ -94,6 +129,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
94
129
|
- Deno compatibility test suite
|
|
95
130
|
|
|
96
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
|
|
133
|
+
[1.0.7]: https://github.com/StackForge-EU/factur-x/compare/v1.0.6...v1.0.7
|
|
97
134
|
[1.0.6]: https://github.com/StackForge-EU/factur-x/compare/v1.0.5...v1.0.6
|
|
98
135
|
[1.0.5]: https://github.com/StackForge-EU/factur-x/compare/v1.0.4...v1.0.5
|
|
99
136
|
[1.0.4]: https://github.com/StackForge-EU/factur-x/compare/v1.0.3...v1.0.4
|
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 }; }
|
|
@@ -1111,6 +1112,28 @@ var AF_RELATIONSHIP_MAP = {
|
|
|
1111
1112
|
Data: pdfLib.AFRelationship.Data,
|
|
1112
1113
|
Source: pdfLib.AFRelationship.Source
|
|
1113
1114
|
};
|
|
1115
|
+
var XMP_CONFORMANCE_LEVEL = {
|
|
1116
|
+
["MINIMUM" /* MINIMUM */]: "MINIMUM",
|
|
1117
|
+
["BASIC_WL" /* BASIC_WL */]: "BASIC WL",
|
|
1118
|
+
["BASIC" /* BASIC */]: "BASIC",
|
|
1119
|
+
["EN16931" /* EN16931 */]: "EN 16931",
|
|
1120
|
+
["EXTENDED" /* EXTENDED */]: "EXTENDED"
|
|
1121
|
+
};
|
|
1122
|
+
function resolveXmpConformanceLevel(profile, flavor) {
|
|
1123
|
+
if (flavor === "xrechnung" /* XRECHNUNG */) return "XRECHNUNG";
|
|
1124
|
+
return XMP_CONFORMANCE_LEVEL[profile];
|
|
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
|
+
}
|
|
1114
1137
|
function ensureOutputIntent(pdfDoc, iccBytes, identifier) {
|
|
1115
1138
|
const existing = pdfDoc.catalog.lookup(pdfLib.PDFName.of("OutputIntents"));
|
|
1116
1139
|
if (existing instanceof pdfLib.PDFArray && existing.size() > 0) return;
|
|
@@ -1375,7 +1398,7 @@ async function embedFacturX(options) {
|
|
|
1375
1398
|
createDate: isoDate,
|
|
1376
1399
|
modifyDate: isoDate,
|
|
1377
1400
|
documentFileName: flavorConfig.attachmentFilename,
|
|
1378
|
-
conformanceLevel: profile
|
|
1401
|
+
conformanceLevel: resolveXmpConformanceLevel(profile, flavor)
|
|
1379
1402
|
});
|
|
1380
1403
|
const xmpBytes = new TextEncoder().encode(xmpXml);
|
|
1381
1404
|
const metadataStream = pdfDoc.context.stream(xmpBytes, {
|
|
@@ -1386,9 +1409,13 @@ async function embedFacturX(options) {
|
|
|
1386
1409
|
const metadataRef = pdfDoc.context.register(metadataStream);
|
|
1387
1410
|
pdfDoc.catalog.set(pdfLib.PDFName.of("Metadata"), metadataRef);
|
|
1388
1411
|
}
|
|
1389
|
-
|
|
1412
|
+
let pdfContentBytes = await pdfDoc.save();
|
|
1413
|
+
if (addPdfA3Metadata) {
|
|
1414
|
+
ensureIdTrailer(pdfDoc, pdfContentBytes);
|
|
1415
|
+
pdfContentBytes = await pdfDoc.save();
|
|
1416
|
+
}
|
|
1390
1417
|
return {
|
|
1391
|
-
pdf:
|
|
1418
|
+
pdf: pdfContentBytes,
|
|
1392
1419
|
xml,
|
|
1393
1420
|
...validation ? { validation } : {},
|
|
1394
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 {
|
|
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) => {
|
|
@@ -1081,6 +1082,28 @@ var AF_RELATIONSHIP_MAP = {
|
|
|
1081
1082
|
Data: AFRelationship.Data,
|
|
1082
1083
|
Source: AFRelationship.Source
|
|
1083
1084
|
};
|
|
1085
|
+
var XMP_CONFORMANCE_LEVEL = {
|
|
1086
|
+
["MINIMUM" /* MINIMUM */]: "MINIMUM",
|
|
1087
|
+
["BASIC_WL" /* BASIC_WL */]: "BASIC WL",
|
|
1088
|
+
["BASIC" /* BASIC */]: "BASIC",
|
|
1089
|
+
["EN16931" /* EN16931 */]: "EN 16931",
|
|
1090
|
+
["EXTENDED" /* EXTENDED */]: "EXTENDED"
|
|
1091
|
+
};
|
|
1092
|
+
function resolveXmpConformanceLevel(profile, flavor) {
|
|
1093
|
+
if (flavor === "xrechnung" /* XRECHNUNG */) return "XRECHNUNG";
|
|
1094
|
+
return XMP_CONFORMANCE_LEVEL[profile];
|
|
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
|
+
}
|
|
1084
1107
|
function ensureOutputIntent(pdfDoc, iccBytes, identifier) {
|
|
1085
1108
|
const existing = pdfDoc.catalog.lookup(PDFName.of("OutputIntents"));
|
|
1086
1109
|
if (existing instanceof PDFArray && existing.size() > 0) return;
|
|
@@ -1345,7 +1368,7 @@ async function embedFacturX(options) {
|
|
|
1345
1368
|
createDate: isoDate,
|
|
1346
1369
|
modifyDate: isoDate,
|
|
1347
1370
|
documentFileName: flavorConfig.attachmentFilename,
|
|
1348
|
-
conformanceLevel: profile
|
|
1371
|
+
conformanceLevel: resolveXmpConformanceLevel(profile, flavor)
|
|
1349
1372
|
});
|
|
1350
1373
|
const xmpBytes = new TextEncoder().encode(xmpXml);
|
|
1351
1374
|
const metadataStream = pdfDoc.context.stream(xmpBytes, {
|
|
@@ -1356,9 +1379,13 @@ async function embedFacturX(options) {
|
|
|
1356
1379
|
const metadataRef = pdfDoc.context.register(metadataStream);
|
|
1357
1380
|
pdfDoc.catalog.set(PDFName.of("Metadata"), metadataRef);
|
|
1358
1381
|
}
|
|
1359
|
-
|
|
1382
|
+
let pdfContentBytes = await pdfDoc.save();
|
|
1383
|
+
if (addPdfA3Metadata) {
|
|
1384
|
+
ensureIdTrailer(pdfDoc, pdfContentBytes);
|
|
1385
|
+
pdfContentBytes = await pdfDoc.save();
|
|
1386
|
+
}
|
|
1360
1387
|
return {
|
|
1361
|
-
pdf:
|
|
1388
|
+
pdf: pdfContentBytes,
|
|
1362
1389
|
xml,
|
|
1363
1390
|
...validation ? { validation } : {},
|
|
1364
1391
|
...xsdValidation ? { xsdValidation } : {}
|