Library Gaps - phax/peppol-cli GitHub Wiki

Library Gaps

Things peppol-cli needs that the existing libraries do not currently provide. Each was verified by reading the source; nothing here is inferred from documentation or from memory.

The Where it belongs column matters: most of these are better fixed upstream than worked around in the CLI, because other consumers need them too. A gap fixed in peppol-commons helps phoss-smp and phoss-ap as well; the same code hidden inside peppol-cli helps nobody else.

# Gap Verified finding Where it belongs
1 GLN check-digit validation is not wired up The algorithm does exist: com.helger.masterdata.ean.GTINValidator.isValidGLN(String) in ph-masterdata โ€” GLN is treated as GTIN-13, checked with the standard GS1 alternating 1/3 weighted mod-10 digit via AbstractUPCEAN.calcChecksum. The same class also covers GTIN-8/12/14 and SSCC. And peppol-id-checks already declares a direct dependency on com.helger.masterdata:ph-masterdata. What is missing is only the wiring: a grep across peppol-commons for GTINValidator / isValidGLN returns nothing, so no participant identifier validator calls it. Registered SPI validators remain just two โ€” Norwegian mod-11 and Belgian mod-97. peppol-id-checks, as a ParticipantIdentifierValidatorGLN SPI implementation that strips the 0088: prefix and delegates to GTINValidator.isValidGLN. No new dependency, no new algorithm โ€” genuinely a small piece of work. > TODO(verify): whether the same treatment is wanted for ICD 0209 (GS1) and any other GTIN-based scheme, since GTINValidator already covers those lengths.
2 Per-user config file discovery ph-config resolves the classpath, the working directory and whatever config.file points at. It has no notion of XDG base directories or %APPDATA%. ph-config. Every CLI in this ecosystem needs it โ€” norma-cli, kaltblut-cli, ph-redact-cli, the en16931-*-cli tools โ€” so solving it once upstream beats a fourth private copy. A concrete design is proposed at the end of this page.
3 Config value provenance peppol config show should print where each value came from. Sources carry a priority, but it is unclear how much origin information ConfiguredValue / IConfigurationSource expose. ph-config, if the information is not already reachable. > TODO(verify): read ConfiguredValue and IConfigurationSource to establish what is available before assuming this is a gap.
4 Config secret handling ph-config has no notion of a secret value, no redaction and no file-permission checking. Arguably the CLI's own concern, but a "this key is secret" marker in ph-config would let every consumer redact consistently instead of each maintaining its own key-name pattern. > TODO(decide): upstream or local?
5 No short, stable aliases for document type identifiers The code list provides display names (getCommonName(), the XML name attribute), but 16 of the 321 document type display names in v9.7 are duplicated, and display names are not guaranteed stable across code list versions. There is no curated short-name table anywhere in peppol-commons. Arguably peppol-commons, since the problem is not CLI-specific โ€” phoss-smp's UI and any other tool faces the same unusable 200-character identifiers. A curated alias table there would benefit everything. See Open Questions #9.
6 Migration key pattern is more permissive than the spec CSMLDefault.MIGRATION_CODE_PATTERN accepts _ (underscore) as one of the required special characters. SML 1.3.0 ยง2.2 enumerates the permitted set explicitly โ€” @ # $ % ( ) [ ] { } * ^ - ! ~ | + = with ASCII codes โ€” and underscore is not in it. So peppol-commons would accept a migration key that the spec does not permit, and an SML implementation validating strictly would reject it. peppol-commons, a one-character fix. Note the spec also contradicts itself: ยง2.2 requires at least two special characters, while ยง3.2.6 describes the MigrationKey as "a string of characters and numbers only, with a maximum length of 24 characters". ยง2.2 is the detailed normative statement and is what peppol-commons implements. > TODO(verify): worth raising with OpenPeppol so ยง3.2.6 is corrected.

| 7 | No list of Peppol Authority abbreviations | A Seat ID's 2-character prefix identifies the responsible Peppol Authority (SPIS 1.0.0), but no published list of those abbreviations exists. So nothing can turn PSE000055 into "Sweden" from an authoritative source, and every tool that shows a Seat ID shows an opaque prefix. | peppol-commons, as a curated list โ€” the problem is not CLI-specific, and one collected list serves every consumer. Design notes at the end of this page. |

| 8 | ph-nss does not expose the list version or date | MozillaNSSTrustStore publishes the trust store itself but no metadata about it. The version (v2.90) and last-update date live only in the free-text first line of mozilla-nss-root-certs.md โ€” "Content of the Mozilla NSS Root Certificate Truststore v2.90 (last update: 2026-10-08)". | ph-nss, as two constants: LIST_VERSION (a String) and LIST_LAST_UPDATE (a LocalDate), both generated alongside the trust store itself so they cannot drift from it. Any consumer that needs to say which trust data it used, or to warn that the data is stale, needs them; parsing a documentation header at runtime is the alternative and is clearly worse. peppol-cli needs this for the six-month staleness warning on its TLS policy check, and โ€” since the CLI now resolves its trust list from a local override when one is newer โ€” to decide which list to use at all, which makes the gap blocking rather than cosmetic. The version must be exposed in a form that compares numerically (2.100 is newer than 2.90); com.helger.base.version.Version is the natural type, though a String constant plus Version.parseDotOnly on the consumer side is equally workable. The .sha256 already shipped beside the trust store covers integrity, and peppol-cli follows that same convention for locally provisioned lists, so no new hash API is needed โ€” only the version and date, which are the two facts a consumer cannot recover from the trust store itself. Note that loading a list from a local file needs nothing: KeyStoreHelper already chains FileSystemResourceProvider ahead of ClassPathResourceProvider, so the existing TrustStoreDescriptor builder accepts a filesystem path unchanged. |

Non-blocking findings worth fixing anyway

Not peppol-cli requirements, but they will mislead whoever implements it.

  1. Phorm: GET /api/get/vesids is effectively unauthenticated. In ApiGetAllVESIDs.java the token check sits behind a disabled guard:
    if (false)
      if (!verifyAuthOrSetForbidden (aRequestScope, aUnifiedResponse, sLogPrefix))
        return;
    Every other API endpoint enforces X-Token. This may well be deliberate โ€” it makes the endpoint usable as a connectivity probe, which peppol-cli would benefit from โ€” but a dead if (false) does not say whether it is intended. Worth either removing or replacing with a comment stating the intent. Flagged because it is a security-relevant asymmetry, not because the CLI needs it changed.

Resolved

  • peppol-commons README out of date โ€” fixed. The usage examples no longer reference PeppolURLProvider or ESML.DIGIT_*, and ESML's class Javadoc no longer lists the removed constants. The remaining mentions in the README are changelog history, which is correct.
  • The MD5 misconception โ€” no longer present in the documentation. For the record, and because it is the single easiest thing to get wrong here: the participant hash is SHA-256, Base32-encoded, trailing = stripped (AbstractBDXLURLProvider; SML 1.3.0 ยง3.1.1; POLICY 7 of PFUOI 4.4.0). The authoritative test vector is 0010:5798000000001 โ†’ XUKHFQABQZIKI3YKVR2FHR4SNFA3PF5VPQ6K4TONV3LMVSY5ARVQ.
  • Generated sources are a trap for a new contributor โ€” documented. peppol-commons/CONTRIBUTING.md now explains that six modules carry no checked-in Java for their data types (peppol-id-datatypes, peppol-codelist-datatypes, peppol-smp-datatypes, peppol-sbdh, peppol-directory-businesscard and peppol-sml-client), why a fresh clone therefore shows hundreds of unresolved symbols in an IDE, and that mvn generate-sources fixes it.

How these gaps affect planning

Gap Affected feature Consequence if not closed
1 peppol pid check-value Without the wiring, GLN identifiers get syntax-only validation and the command silently under-delivers. With it, the check is ~10 lines.
2 Config discovery Every CLI in the ecosystem keeps reinventing it.

Proposal: per-user configuration discovery in ph-config

A concrete sketch for gap #2, written to fit ph-config as it is rather than to redesign it. Everything referenced below was read in ph-commons/ph-config and ph-commons/ph-base.

Shape: an opt-in augmenting helper, not a change to the default provider

ConfigFactory already has the right precedent โ€” addProfilePropertiesSources (MultiConfigurationValueProvider, String...), a static helper that adds sources to a caller-supplied provider and returns how many it added. A discovery helper should mirror that shape exactly.

createDefaultValueProvider () must not change. If per-user discovery became a default, server applications โ€” phoss-SMP, phoss-AP, Phorm โ€” would silently start reading ~/.config, which is wrong for a daemon and a security question besides. Opt-in keeps the change purely additive.

Proposed API

// New class, e.g. com.helger.config.source.userdir.UserConfigDirHelper

// Platform-aware directory resolution. Reads no configuration itself, so it is
// safe to call before any configuration exists.
@Nullable public static File getUserConfigDir (@NonNull @Nonempty String sAppName);
@Nullable public static File getUserCacheDir  (@NonNull @Nonempty String sAppName);

// The augmenting helper, mirroring ConfigFactory.addProfilePropertiesSources
@Nonnegative
public static int addUserConfigSources (@NonNull MultiConfigurationValueProvider aMCSVP,
                                       @NonNull @Nonempty String sAppName,
                                       @NonNull String... aBaseNames);

A CLI then does:

final MultiConfigurationValueProvider aVP = ConfigFactory.createValueProviderSystemOnly ();
UserConfigDirHelper.addUserConfigSources (aVP, "peppol-cli", "credentials", "config");
// ... plus the explicit --config file and the bundled reference.properties
final IConfig aConfig = Config.create (aVP);

Directory resolution

Built on what already exists in ph-base: SystemHelper.getOperatingSystem () returning an EOperatingSystem with isWindowsBased () / isMacOSBased () / isUnixBased (), plus SystemProperties.getUserHome (). No new dependency. A grep confirms ph-commons has no XDG or %APPDATA% handling today, so nothing is duplicated.

Platform Config directory Cache directory
Linux / BSD $XDG_CONFIG_HOME/<app> else ~/.config/<app> $XDG_CACHE_HOME/<app> else ~/.cache/<app>
macOS ~/.config/<app> by default, ~/Library/Application Support/<app> selectable ~/Library/Caches/<app>
Windows %APPDATA%\<app> %LOCALAPPDATA%\<app>\cache

The macOS choice is contentious: Apple's convention is ~/Library/Application Support, while developer tools overwhelmingly use ~/.config. Rather than ph-config legislating, make it a parameter with ~/.config as the default, so each application decides and the library stays out of the argument.

An explicit override should also be honoured โ€” a config.user.dir key read through ConfigFactory.getSystemConfig () so it works as both a system property and an environment variable, exactly as config.file already does.

Priorities

These are all RESOURCE-type sources, so EConfigSourceType needs no change โ€” only new constants slotted between the explicit config.file (200) and the classpath-bundled files (190 and below):

Source Proposed priority Rationale
config.file (existing) 200 An explicit --config must always win.
First user base name (e.g. credentials) RESOURCE.getDefaultPriority () - 2 = 198 Secrets override ordinary settings.
Second user base name (e.g. config) RESOURCE.getDefaultPriority () - 4 = 196 The user's own settings.
private-application.properties (existing) 190 Unchanged.
application.properties (existing) 180 Unchanged.
reference.properties (existing) 1 Shipped defaults.

Deriving each from EConfigSourceType.RESOURCE.getDefaultPriority (), as the existing constants already do, keeps the arithmetic in one place. Varargs order sets priority, decreasing by 2 per name, so a caller can pass any number of base names without the library inventing a constant for each.

Format handling: reuse, do not add

Resolve the extension through ConfigurationSourceResourceTypeRegistry.getInstance ().getFactoryOfFileExtensionOrFallback (...), exactly as ConfigFactory already does for config.file. Then:

  • .properties works immediately;
  • .json works as soon as ph-config-json is on the classpath, through its existing SPI registrar;
  • any future format registered via IConfigurationSourceResourceTypeRegistrarSPI works with no further change.

So the helper should probe each base name across the registered extensions rather than hard-coding .properties โ€” for base name config, try config.properties then config.json, first match wins, and log a warning when more than one exists, so a stale leftover cannot silently shadow the intended file.

Deliberately out of scope for ph-config

  • Creating directories or files, and setting permissions. The helper discovers and reads; chmod 600 and first-run scaffolding belong to the application. A configuration library should not be in the file-creation business.
  • Redaction and secret marking. Related but separate โ€” see gap #2.
  • A permission check could be offered as a standalone utility returning a status (e.g. "group- or world-readable") so a CLI can warn, but it must stay off the loading path: a server must never fail to start because a file mode looks untidy.

Why upstream

norma-cli, kaltblut-cli, ph-redact-cli and the en16931-*-cli tools all face this, and peppol-cli would be the fourth or fifth private implementation. The helper is on the order of 150 lines, adds no dependency, changes no existing behaviour, and removes a recurring papercut for every CLI in the ecosystem.

> TODO(decide): ph-config, or ph-base as a general platform-directories helper that ph-config merely consumes? The directory resolution itself has nothing to do with configuration, so ph-base is arguably the more honest home.

Proposal: a curated list of Peppol Authority abbreviations

For gap #7. Small, but one property shapes the design: the list has no authoritative upstream source, so it will always be incomplete and must never behave as though it were exhaustive.

Shape

A generated enum would be wrong, since there is nothing to generate from. A hand-maintained enum in peppol-id alongside SPIDHelper, which already owns Seat ID handling, fits better โ€” EPeppolAuthority implements IHasID <String>, one entry per known authority carrying the 2-character ID and the authority name, with the usual getFromIDOrNull. Plus a convenience on SPIDHelper, getAuthorityFromSeatID (String), so callers need not slice the prefix themselves.

The list, derived from the published authorities

The set of valid prefixes is the country code of each Peppol Authority listed at https://peppol.org/members/peppol-authorities/, plus OP for OpenPeppol. That page lists 26 jurisdictional authorities and OpenPeppol as the coordinating authority, giving:

Prefix Authority Prefix Authority
OP OpenPeppol (coordinating) LU Luxembourg
AE United Arab Emirates MY Malaysia
AU Australia NG Nigeria
BE Belgium NL The Netherlands
DE Germany NO Norway
DK Denmark NZ New Zealand
FI Finland OM Oman
FR France PL Poland
IE Ireland PT Portugal
IS Iceland SE Sweden
IT Italy SG Singapore
JP Japan SK Slovakia
TW Taiwan

Corroborated by real certificates. Every Seat ID observed while researching this wiki maps to an authority on that list: PNL000052 (Netherlands) and PNO000070 (Norway) from the signed SMP metadata in peppol-commons test resources, POP000000 and POP000270 (OpenPeppol), PDE000270 (Germany), and PSE000055 (Sweden) from the OpenPeppol PKI documentation. Five independent prefixes, five matches โ€” which is reasonable evidence that the derivation rule is right.

Two entries need a decision rather than a derivation:

  • England โ€” Supply Chain Coordination Ltd is listed as the authority for England, which has no ISO 3166-1 alpha-2 code of its own. > TODO(verify): is the prefix GB, or something else? This is the one entry the country-code rule cannot settle by itself.
  • Greece โ€” ISO 3166-1 gives GR, while EU VAT uses EL for the same country, and PFUOI 4.4.0 explicitly handles the EL prefix elsewhere. > TODO(verify): GR or EL?

Note also that OP is the only prefix that is not a country code at all, which is why the list cannot be replaced by a country-code lookup โ€” see below. And since authorities are added over time, any compiled list lags reality, which makes the next section a requirement rather than a nicety.

Unknown prefixes must stay valid

An unknown prefix means "an authority this list does not know yet", not an invalid Seat ID:

  • isValidSeatID must keep matching on the format alone and never against this list, so a Seat ID from a newly onboarded authority still validates.
  • A lookup that misses returns null, and the CLI shows the raw prefix rather than an error or a guess.
  • > TODO(decide): should an unknown prefix produce a note such as "authority NN is not in this build's list" โ€” honest about the gap and a prompt to report it โ€” or stay silent, to avoid noise on a perfectly valid identifier?

And the rule must not be reimplemented as a country-code lookup, however tempting the table above makes it look: OP is not a country, and a future non-geographic authority would be silently given a wrong name. The list is the authority, not ISO 3166.

โš ๏ธ **GitHub.com Fallback** โš ๏ธ