Skip to content

Decisions

Philip Helger edited this page Oct 9, 2026 · 10 revisions

Decisions

A lightweight ADR (Architecture Decision Record) log. One entry per decision that is expensive to reverse or that people will otherwise ask about repeatedly. Each entry has Context, Decision, Consequences and Status.

Status values: proposed · accepted · deferred · rejected · superseded by ADR-nnn.

ADR Title Status
ADR-001 AS4 out of scope accepted
ADR-002 TSR/EUSR submission out of scope accepted
ADR-003 Document validation via a configured Phorm instance accepted
ADR-004 Project name peppol-cli, executable peppol accepted
ADR-005 Java 25 baseline accepted
ADR-006 picocli as the CLI framework accepted
ADR-007 Peppol only, no other networks accepted
ADR-008 Apache License 2.0 accepted
ADR-009 DNSSEC validation on by default accepted
ADR-010 The local NSS trust list may only be newer accepted

ADR-001: AS4 out of scope

Context. The obvious next question after "can this participant receive my invoice?" is "then send it". AS4 sending would require message-level configuration, signing and encryption material, MIME and compression handling, reliable storage of sent messages and receipts, and retry logic. AS4 receiving additionally requires a long-running, publicly reachable HTTPS server. Both already exist in the AP software, which is built, tested and certified for exactly this.

Decision. peppol-cli does not send or receive AS4 messages, does not inspect AS4 messages, and does not provide a mock AP. Diagnostics stop at the transport layer: TLS and HTTP reachability of the endpoint URL.

Consequences.

  • The tool stays a short-lived, stateless process with no persistent message store and no receiving port.
  • No AS4 signing or encryption keys are needed for the core feature set, which keeps the security surface small. Keys are still needed for SML/SMP write operations and TLS client authentication.
  • Users who need to send will be pointed at the AP software. The wiki must say this clearly so the omission does not read as an oversight.
  • A genuine gap remains: nobody can do a true end-to-end delivery test with this tool. Accepted.
  • The endpoint diagnostics must be honest about their limits — a successful TLS handshake is not proof that an AS4 exchange would succeed.

Status. accepted


ADR-002: TSR/EUSR submission out of scope

Context. Transaction Statistics Reports and End User Statistics Reports are produced by a service provider from its own message history and submitted under its own identity. The input data lives in the AP's database; the reporting libraries already exist (> TODO(verify): a peppol-reporting project exists alongside this one; confirm it is the intended library for TSR/EUSR and what its current scope is). A CLI has neither the message history nor a legitimate claim to the submitting identity.

Decision. peppol-cli does not submit TSR or EUSR reports.

Consequences.

  • No reporting period state, no submission credentials and no report storage in the CLI.
  • Operators keep using their AP's reporting facility, which is where the source data is.
  • Open, deliberately not decided here: whether local, read-only helpers (validate a report file, pretty-print one) would be acceptable, since they need no identity and no submission. If wanted, that is a new ADR.
  • > TODO(verify): the current specification names, versions and submission channel, so this entry names them correctly

Status. accepted


ADR-003: Document validation via a configured Phorm instance

Context. Validating a business document means running a large, frequently updated set of validation artefacts. Embedding them in the CLI would tie document-level correctness to the CLI's own release cycle, make the distribution considerably larger, and mean every user runs whatever artefact version they last updated to. A Phorm instance already performs validation as a service, and an organisation typically wants one authoritative instance so that its developers, its CI and its support staff all get the same verdict.

Decision. peppol validate sends the document to a Phorm instance configured by the user and renders the returned report. The CLI does not bundle validation artefacts and does not run validation locally. Document type / customization ID auto-detection is done locally, so the user does not have to name the artefact by hand.

Consequences.

  • Validation requires network access to the configured instance, and the instance's URL and credentials become part of the configuration. There is no useful offline validation.
  • Verdicts stay consistent across a team, and artefact updates are an operational matter for whoever runs the instance, not a CLI release.
  • The CLI sends business document content to that instance. This must be stated plainly in the documentation, since invoices are commercially sensitive. Pointing the tool at a third-party instance is a data-sharing decision the user must make knowingly.
  • A transport error must be clearly distinguishable from a validation failure — hence exit code 5 versus 4.
  • The Phorm API has since been verified against its source, and the details are recorded in the Feature Catalogue: POST /api/dd_and_validate and POST /api/validate/{vesid}, the raw document as the request body, a shared secret in the X-Token header, and JSON/XML/HTML reports selected by the Accept header.
  • A reusable client library exists: com.helger:phorm-client ("Java client library for the phorm REST API"), so this decision costs the CLI no protocol code at all. PhormClient covers explicit-VESID validation, detect-and-validate, detection alone, VESID listing and the hybrid PDF path, and returns a typed PhormValidationResult.
  • Local document type detection is available through the ddd library (DocumentDetailsDeterminator.findDocumentDetails), so auto-detection does not require a server round trip.

Status. accepted


ADR-004: Project name peppol-cli, executable peppol

Context. The project needs a repository name and an executable name. Candidates ranged from ph-prefixed puns consistent with the surrounding ecosystem to conventional -ctl tool names. The primary audience is AP and SMP operators and integrators, not existing users of the phax libraries.

Decision. The project is peppol-cli; the executable is peppol. Alternatives and the reason for rejecting each are recorded in Naming.

Consequences.

  • Commands read naturally and are guessable: peppol smp lookup, peppol cert verify.
  • The executable name is short and generic, so a $PATH collision check is needed before release.
  • "Peppol" is an OpenPeppol trademark. A prominent "not an official OpenPeppol tool" disclaimer is required on every wiki page (see _Footer.md) and in the README, plus a courtesy check with OpenPeppol.
  • Name-availability checks on GitHub, Homebrew, npm/PyPI and distro packages are follow-up tasks in Naming.

Status. accepted


ADR-005: Java 25 baseline

Context. The surrounding libraries are Java-based and the shared parent POM currently defaults to a Java 17 baseline (ph-parent-pom, property java.version). A CLI has no servlet container or customer-managed runtime to accommodate: the distribution decides which JRE it needs, and the only affected party is the person installing it.

Decision. peppol-cli targets Java 25 and overrides the inherited java.version property accordingly.

Consequences.

  • Current language and API features are available without conditional code.
  • Users need a Java 25 runtime. This must be stated in the README and checked at startup with a clear error rather than a UnsupportedClassVersionError, and it makes bundling a runtime in the distribution archive worth considering.
  • Depended-upon libraries may target a lower baseline, which is fine — they only have to be readable by Java 25, not compiled with it.
  • > TODO(verify): that every intended phax dependency is usable on Java 25 (module path / JPMS and any reflection-based code in particular)
  • Java 25's ahead-of-time class loading and linking improvements are relevant to CLI startup time and may reduce the pressure for a native image. > TODO(verify): the exact JDK 25 AOT cache mechanism, its flags and its measured effect before relying on this argument

Status. accepted


ADR-006: picocli as the CLI framework

Context. The tool needs argument parsing, a two-level command tree, help generation, shell completion and exit code handling. Writing this by hand is avoidable work. picocli is already the framework used by the sibling CLI tools in this ecosystem — verified in norma-cli, kaltblut-cli, ph-redact-cli, en16931-purifier-cli, en16931-ubl2cii-cli, en16931-cii2ubl-cli and phoss-peppol-mcp, at version 4.7.7.

Decision. Use picocli. Its CommandLine.ExitCode constants define the first three exit codes in CLI Conventions; its picocli-codegen companion generates the completion scripts.

Consequences.

  • Consistent help output, option syntax and behaviour with the other tools in the ecosystem, and no in-house parser to maintain.
  • The command tree structure follows picocli's annotation model, which suits the peppol <area> <action> shape.
  • picocli is annotation- and reflection-driven, so a future native image needs its annotation processor configured. picocli supports this; it is one more reason to keep the native image question separate rather than open-ended. See Open Questions #1.
  • > TODO(decide): pin the picocli version in this project or inherit it from a shared parent POM property

Status. accepted


ADR-007: Peppol only, no other networks

Context. The underlying libraries are deliberately broader than Peppol. smp-client-base carries OASIS BDXR SMP 1.0 and 2.0 clients (BDXRClientReadOnly, BDXR2ClientReadOnly, BDXRClient, BDXR2Client), peppol-smp-client ships a DBNAURLProviderSMP for DBNAlliance discovery, ESMPTransportProfile collects transport profiles across networks, and peppol-commons contains dbnalliance-commons, dbnalliance-xhe and hredelivery-commons modules. Exposing all of that through the CLI would be nearly free in implementation terms, which is exactly why it is tempting.

Decision. peppol-cli targets the Peppol network only. No --flavour for BDXR SMP, no --provider for DBNAlliance discovery, no DBNAlliance or HR eDelivery commands, and no multi-network transport profile handling.

Consequences.

  • Every command means one thing. peppol smp can-receive answers a Peppol question against Peppol rules, and its output needs no qualification about which network it refers to. That precision is the tool's main value.
  • The surface stays small: a --flavour on the SMP commands alone would multiply the option matrix and the test matrix, and every fixture would need a per-flavour variant.
  • Validity checks stay unambiguous. EPredefinedTransportProfileIdentifier (the Peppol code list) is the single source for "is this allowed?", with no need to explain why a profile valid on another network is invalid here.
  • The name reinforces it: the executable is peppol, so a user reasonably assumes Peppol semantics throughout.
  • Cost accepted: someone running a BDXR-based or DBNAlliance deployment gets no help from this tool, even though the libraries beneath it could serve them. They keep using the libraries directly.
  • This is a scope decision, not a technical limit. If a genuine demand appears it can be revisited — but it would need a new ADR and, realistically, a rethink of the command naming rather than a flag bolted on.

Status. accepted


ADR-008: Apache License 2.0

Context. The project needs a licence before its first public release. It builds almost entirely on libraries from the same ecosystem — ph-commons, peppol-commons, peppol-directory-client, phorm-client — all of which are Apache 2.0, as is picocli. The repository was created with an Apache 2.0 LICENSE file already in place.

Decision. peppol-cli is licensed under the Apache License, Version 2.0.

Consequences.

  • No licence friction with any planned dependency, and no copyleft obligation passed on to users who script against the tool or ship it inside a container image.
  • Permissive enough that a service provider can embed or repackage it, which suits a diagnostic tool that people will want to run inside their own tooling.
  • Every source file carries the standard Apache 2.0 header, matching the rest of the ecosystem.
  • Homebrew accepts it, so the distribution path in Architecture is unaffected.
  • The obligation this leaves: the transitive dependency set still needs a licence review before release. The SOAP/JAX-WS stack behind peppol-sml-client and dnsjava are not phax projects and have not been checked. See Open Questions #7.
  • "Peppol" remains an OpenPeppol trademark, which the licence does not affect — see Naming and the disclaimer in the page footer.

Status. accepted


ADR-009: DNSSEC validation on by default

Context. SML 1.3.0 §5 "DNS Spoof Mitigation" describes the threat directly: someone holding a rogue certificate could use DNS poisoning or a man-in-the-middle attack to make senders resolve a participant to a malicious SMP, routing messages to an attacker-controlled access point. It recommends DNSSEC as the mitigation.

Three facts make acting on that recommendation practical now rather than aspirational. The Peppol SML zones are verifiably signed — DS records at every level of peppol.org → tech.peppol.org → prod.tech.peppol.org → sml.prod.tech.peppol.org, algorithm 13, and the test zone likewise — and a validating resolver returns authenticated data for them. The libraries support validation: ph-dns 11.4.7's com.helger.dns.dnssec package, exposed by peppol-commons 13.1.2 as AbstractBDXLURLProvider.setDnsSecValidation (boolean). And the library's own default is false, so the choice is explicitly ours to make.

Decision. DNSSEC validation is enabled by default, overriding AbstractBDXLURLProvider.DEFAULT_DNSSEC_VALIDATION. --no-dnssec disables it. On validation failure the command fails; it never falls back to an unvalidated lookup.

Consequences.

  • A resolved SMP URL is authenticated by default, so the tool's answers are trustworthy without the user having to know to ask for it. For a diagnostic tool, the default answer being the stronger one is the whole point.
  • "Not found" becomes provably not found: the SML zones' authenticated denial of existence means exit code 3 carries real information rather than just an absence of data.
  • The significant cost: a user whose resolver does not forward DNSSEC records sees every lookup fail on their first command. That is a bad first impression caused by something outside their control, so the quality of that one error message is now a requirement rather than a nicety — it must name the resolver as the cause and offer --dns-server and --no-dnssec side by side. The Feature Catalogue records this, and peppol config validate checks the resolver up front.
  • No silent fallback, deliberately. Falling back on failure would provide the appearance of protection with none of the substance, and silent downgrade is the attack DNSSEC prevents. Proceeding unvalidated requires an explicit flag, which is visible in shell history and CI configuration.
  • This diverges from the library default, which is worth stating in the documentation so that a difference in behaviour between peppol-cli and another peppol-commons consumer is not mistaken for a bug in either.
  • Slightly more work per lookup (signature validation, key fetching), mitigated by the library's DNSSEC key cache — DnsSecHelper.DEFAULT_KEY_CACHE_MAX_TTL is one hour.
  • If OpenPeppol ever unsigned a zone, the default would need revisiting; an INSECURE result would surface that immediately rather than silently.

Status. accepted

ADR-010: The local NSS trust list may only be newer

Context. POLICY 1 of the Peppol Policy for Transport Security 1.1.0 requires a TLS certificate to chain to a root in "the latest version" of the Mozilla NSS list. ph-nss ships that list as a bundled PKCS#12, which begins ageing the day the tool is released — and the upstream list moves often, by observation within weeks. So there has to be a way to use a newer list without rebuilding, which means the tool loads trust data from a location a user can write to.

Loading is not the problem: KeyStoreHelper chains FileSystemResourceProvider ahead of ClassPathResourceProvider, so the existing TrustStoreDescriptor builder accepts a filesystem path exactly as it accepts MozillaNSSTrustStore.TRUSTSTORE_CLASSPATH. The problem is that "a file the user can drop in" and "the set of roots this tool trusts" are now the same thing.

This is deliberately different from the code list decision (Open Questions #5: bundled only, updated by upgrading the tool), and the difference is physical rather than a change of mind. Code list data lives in generated Java enums, so there is no runtime artefact to replace — an override would mean shipping a parser and a second code path for data that has none. The NSS list is a PKCS#12 loaded through a path, so an override costs one resolution step. Where the data format allows it and the data ages faster than the release cycle, overriding is worth the rules it needs; where it does not, it is not.

Decision. The effective list is chosen by version, not by precedence, and the rule admits no exemption. A list found in <cache-dir>/nss/ is used only if its version is strictly higher than the bundled list's; equal or lower, and the bundle is used. A file named explicitly with --nss-truststore or peppol.cli.tls.nss.file is held to the same test and refused when it does not pass, rather than being honoured as an instruction — naming a file does not make a downgrade safe, and an exemption would mean the one rule protecting the floor could be waived with a flag. The version is carried in the filename (mozilla-nss-root-certs-<version>-<yyyy-mm-dd>.p12), with integrity in a .sha256 beside it following ph-nss's own convention; a name that does not parse is not a candidate. Versions compare numerically via com.helger.base.version.Version, never as strings. The list is provisioned out of band only: the CLI never fetches or builds one.

Consequences.

  • Trust data can be upgraded locally but never downgraded. This is the whole point: an old list's real hazard is not the new root it lacks — that fails visibly — but the distrusted root it still carries, which fails silently, in the direction of accepting something it should not.
  • A dropped file cannot quietly weaken the tool. Without the floor, "newest wins" would make a trust downgrade a file copy away in a directory that every process running as the user can write.
  • The comparison needs a version on both sides, which ph-nss does not currently expose. Library gap 8 moves from cosmetic to blocking, and a local list whose filename does not parse is not a candidate at all.
  • Every report that depends on the list — the TLS policy check, the six-month staleness warning, the update check's NSS remark, peppol config show — must name the effective list and its source, since two machines can now disagree with no visible cause.
  • A corrupt or stale file in a cache directory degrades to the bundle with a warning rather than failing the command. The fallback is to the safer input, so there is no reason to make it fatal.

Status. accepted

Clone this wiki locally