@open-charging-cloud/wwcp-poi 0.1.1 → 0.3.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 (106) hide show
  1. package/CHANGELOG.md +382 -0
  2. package/README.md +534 -362
  3. package/dist/crypto/ber.d.ts +48 -0
  4. package/dist/crypto/ber.d.ts.map +1 -0
  5. package/dist/crypto/certificates.d.ts +21 -0
  6. package/dist/crypto/certificates.d.ts.map +1 -0
  7. package/dist/crypto/curves.d.ts +25 -0
  8. package/dist/crypto/curves.d.ts.map +1 -0
  9. package/dist/crypto/ecc.d.ts +34 -0
  10. package/dist/crypto/ecc.d.ts.map +1 -0
  11. package/dist/crypto/publicKeys.d.ts +51 -0
  12. package/dist/crypto/publicKeys.d.ts.map +1 -0
  13. package/dist/crypto/rsa.d.ts +30 -0
  14. package/dist/crypto/rsa.d.ts.map +1 -0
  15. package/dist/encoding/text.d.ts +9 -0
  16. package/dist/encoding/text.d.ts.map +1 -1
  17. package/dist/evTaler/amounts.d.ts +89 -0
  18. package/dist/evTaler/amounts.d.ts.map +1 -0
  19. package/dist/evTaler/contractTerms.d.ts +107 -0
  20. package/dist/evTaler/contractTerms.d.ts.map +1 -0
  21. package/dist/evTaler/meterValue.d.ts +65 -0
  22. package/dist/evTaler/meterValue.d.ts.map +1 -0
  23. package/dist/evTaler/payURI.d.ts +28 -0
  24. package/dist/evTaler/payURI.d.ts.map +1 -0
  25. package/dist/evTaler/settlement.d.ts +85 -0
  26. package/dist/evTaler/settlement.d.ts.map +1 -0
  27. package/dist/evTaler/step.d.ts +72 -0
  28. package/dist/evTaler/step.d.ts.map +1 -0
  29. package/dist/history/apply.d.ts +6 -1
  30. package/dist/history/apply.d.ts.map +1 -1
  31. package/dist/history/changeSet.d.ts +18 -2
  32. package/dist/history/changeSet.d.ts.map +1 -1
  33. package/dist/history/checkpoint.d.ts +7 -0
  34. package/dist/history/checkpoint.d.ts.map +1 -1
  35. package/dist/history/commit.d.ts +13 -1
  36. package/dist/history/commit.d.ts.map +1 -1
  37. package/dist/history/elements.d.ts +19 -0
  38. package/dist/history/elements.d.ts.map +1 -0
  39. package/dist/history/epoch.d.ts +2 -0
  40. package/dist/history/epoch.d.ts.map +1 -1
  41. package/dist/history/history.d.ts +46 -6
  42. package/dist/history/history.d.ts.map +1 -1
  43. package/dist/history/preconditions.d.ts +22 -0
  44. package/dist/history/preconditions.d.ts.map +1 -0
  45. package/dist/index.d.ts +18 -4
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +10296 -1558
  48. package/dist/index.js.map +1 -1
  49. package/dist/json/canonical.d.ts.map +1 -1
  50. package/dist/keys.d.ts +2 -0
  51. package/dist/keys.d.ts.map +1 -1
  52. package/dist/network/certificates.d.ts +16 -4
  53. package/dist/network/certificates.d.ts.map +1 -1
  54. package/dist/network/countries.d.ts +16 -0
  55. package/dist/network/countries.d.ts.map +1 -0
  56. package/dist/network/evTaler.d.ts +1 -14
  57. package/dist/network/evTaler.d.ts.map +1 -1
  58. package/dist/network/identifications.d.ts +6 -6
  59. package/dist/network/identifications.d.ts.map +1 -1
  60. package/dist/network/measurementPublicKey.d.ts +7 -0
  61. package/dist/network/measurementPublicKey.d.ts.map +1 -0
  62. package/dist/network/offers.d.ts +53 -0
  63. package/dist/network/offers.d.ts.map +1 -0
  64. package/dist/network/partyKeys.d.ts +8 -5
  65. package/dist/network/partyKeys.d.ts.map +1 -1
  66. package/dist/network/poiJSON.d.ts +172 -0
  67. package/dist/network/poiJSON.d.ts.map +1 -0
  68. package/dist/network/poiValues.d.ts +48 -0
  69. package/dist/network/poiValues.d.ts.map +1 -0
  70. package/dist/network/reading.d.ts +46 -0
  71. package/dist/network/reading.d.ts.map +1 -0
  72. package/dist/network/references.d.ts +36 -0
  73. package/dist/network/references.d.ts.map +1 -0
  74. package/dist/network/roamingNetwork.d.ts +10 -11
  75. package/dist/network/roamingNetwork.d.ts.map +1 -1
  76. package/dist/network/timeZones.d.ts +8 -0
  77. package/dist/network/timeZones.d.ts.map +1 -0
  78. package/dist/network/tree.d.ts +14 -0
  79. package/dist/network/tree.d.ts.map +1 -1
  80. package/dist/network/versionTexts.d.ts +1 -7
  81. package/dist/network/versionTexts.d.ts.map +1 -1
  82. package/dist/network/writtenForm.d.ts.map +1 -1
  83. package/dist/proofs/priceStatement.d.ts.map +1 -1
  84. package/dist/proofs/stationProof.d.ts +2 -0
  85. package/dist/proofs/stationProof.d.ts.map +1 -1
  86. package/dist/proofs/trustContext.d.ts +6 -0
  87. package/dist/proofs/trustContext.d.ts.map +1 -1
  88. package/dist/verifier/chargingStation.d.ts +5 -0
  89. package/dist/verifier/chargingStation.d.ts.map +1 -1
  90. package/dist/verifier/checkpoints.d.ts +6 -0
  91. package/dist/verifier/checkpoints.d.ts.map +1 -1
  92. package/dist/verifier/documentKeys.d.ts +27 -0
  93. package/dist/verifier/documentKeys.d.ts.map +1 -0
  94. package/dist/verifier/historicalMeasurements.d.ts +96 -0
  95. package/dist/verifier/historicalMeasurements.d.ts.map +1 -0
  96. package/dist/verifier/measurementKeys.d.ts +48 -0
  97. package/dist/verifier/measurementKeys.d.ts.map +1 -0
  98. package/dist/verifier/observedVersions.d.ts +70 -0
  99. package/dist/verifier/observedVersions.d.ts.map +1 -0
  100. package/dist/verifier/pricePublication.d.ts +74 -0
  101. package/dist/verifier/pricePublication.d.ts.map +1 -0
  102. package/dist/verifier/sessionPrices.d.ts +75 -0
  103. package/dist/verifier/sessionPrices.d.ts.map +1 -0
  104. package/dist/verifier/trustTransitions.d.ts +67 -0
  105. package/dist/verifier/trustTransitions.d.ts.map +1 -0
  106. package/package.json +15 -14
package/README.md CHANGED
@@ -1,362 +1,534 @@
1
- # WWCP POI for TypeScript
2
-
3
- [![CI](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/ci.yml/badge.svg)](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/ci.yml)
4
- [![Nightly](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/nightly.yml/badge.svg)](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/nightly.yml)
5
- [![npm](https://img.shields.io/npm/v/@open-charging-cloud/wwcp-poi)](https://www.npmjs.com/package/@open-charging-cloud/wwcp-poi)
6
-
7
- Verifies what a roaming network of [WWCP/POI](https://github.com/OpenChargingCloud/WWCP_POI) publishes about its
8
- charging stations and their tariffs, from the **trust anchor** of the roaming network alone: the QR code its operator
9
- publishes, naming the identity keys of the operator and its quorum.
10
-
11
- An EV driver's app selects a charging station; this library downloads the proof of the station and the price statements
12
- of its EVSEs, and checks that the station, its EVSEs as they are at runtime, the charging tariffs they take and the
13
- prices of the moment are the ones the roaming network operator attested and the charging station operator signed - before
14
- charging begins. Months later, e.g. disputing an invoice, the evidence the app kept proves which price applied, witnessed
15
- by checkpoints. The same proof gives the public keys of the energy meters, with which
16
- [ChargyCore.TS](https://github.com/OpenChargingCloud/ChargyCore.TS) checks the signed meter values of the session, and the
17
- transparency software and its certificate that say who checks them.
18
-
19
- Everything is calculated as the C# implementation calculates it, byte for byte: the canonical JSON and the deterministic
20
- CBOR of Styx, ETags, the effective tree with templates (section 8 of the specification), commit and checkpoint
21
- identifications of every epoch, COSE keys and their thumbprints, and the signatures ES256/384/512, Ed25519, Ed448 and
22
- ML-DSA-44/65/87.
23
-
24
-
25
- ## Status
26
-
27
- **Milestone 1: the examples of WWCP_POI, checked in TypeScript.** The examples A1 to A14 of
28
- [WWCP_POI/examples](https://github.com/OpenChargingCloud/WWCP_POI/tree/master/examples) at commit `33447ac` are checked
29
- from what the C# examples publish, as their `Validate()` methods check them, and the same for proofs of charging stations
30
- the C# implementation makes; also from a second run of the C# examples, with new keys. See [Tests](#tests).
31
-
32
- What a review of the C# implementation found while porting it is in
33
- [docs/csharp-review.md of WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests/blob/master/docs/csharp-review.md),
34
- cited here as *csharp-review N*.
35
-
36
- Whether both implementations really calculate, take and refuse the same is the business of
37
- [WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests): golden test vectors, run against
38
- this library and the C# implementation alike, and where they differ, written down and pinned
39
- ([docs/implementation-deviations.md](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests/blob/master/docs/implementation-deviations.md)).
40
- This repository vendors the vectors and runs them in its own suite (`tests/conformance.test.ts`).
41
-
42
-
43
- ## Installation
44
-
45
- From npm, as [`@open-charging-cloud/wwcp-poi`](https://www.npmjs.com/package/@open-charging-cloud/wwcp-poi):
46
-
47
- ```bash
48
- npm install @open-charging-cloud/wwcp-poi
49
- ```
50
-
51
- ESM, Node 22.18 or newer, browsers and React Native alike: no `node:*` module, no `Buffer`, no WebCrypto. The
52
- cryptography is [`@vanaheimr/cose`](https://github.com/Vanaheimr/COSE.TS) on the `@noble` family, the CBOR codec
53
- [`@vanaheimr/metrological-cbor`](https://github.com/Vanaheimr/MetrologicalCBOR.TS).
54
-
55
-
56
- ## An app: a charging station and its prices
57
-
58
- ```ts
59
- import { POIClient } from '@open-charging-cloud/wwcp-poi';
60
-
61
- // The text of the QR code of the roaming network, as scanned, e.g. from the website of its operator.
62
- const client = new POIClient('WWCP/TA-2:B7LJ683VCBCACVCC$C6REF6B…');
63
-
64
- // GET ~/RNs/QA/EVSEs/DE*GEF*E1*1/proof and …/priceStatements, from the first server of the trust anchor whose answer verifies.
65
- const checked = await client.chargingStation({ evseId: 'DE*GEF*E1*1' });
66
-
67
- checked.station.roamingNetwork.name; // 'Quality Assurance', as the operator signed it, not as the QR code says
68
- checked.station.chargingStation; // the station as it is at runtime, with what its templates give it
69
- checked.station.tariffs.get('DE*GEF*E1*1'); // the charging tariffs the EVSE takes
70
- checked.station.manufacturer?.model; // its model, as its manufacturer states it: revisions, firmware, ratings
71
- checked.station.energyMeters; // with their public keys, for transparency software
72
- checked.station.energyMeterModels; // per meter: its manufacturer and model
73
- checked.station.certificates; // of the station and its meters: each for a model, with its components
74
- checked.station.transparencySoftware; // per meter: the release, its manufacturer, the certificates naming it
75
- checked.prices.get('EVSE:DE*GEF*E1*1')?.statement; // the price statement valid now, signed by DE*GEF
76
- checked.documents; // as they came: keep them as evidence of the session
77
- ```
78
-
79
- What is checked (sections 8.4 to 8.6, 12.4 of the specification):
80
-
81
- - the proof leads to a root, the `effectiveTree` ETag of a version of the roaming network;
82
- - the key statement of the roaming network operator the proof gives is signed by identity keys the trust anchor pins, of
83
- as many holders as its `minConsensus` requires, counted as the trust anchor names them per holder, names their holders
84
- so as well, and is valid now;
85
- - attestations of that root, valid now - signed then or before, until their `validUntil`, at most a day later - by
86
- signing keys of the operator, valid when they attested and now, each certified by a trusted identity key of its
87
- holder, come from as many holders, each counted for the holder of that identity key as the trust anchor knows it;
88
- - each price statement is signed by a signing key of the charging station operator that the proof gives and that may sign
89
- `priceStatements`, names only charging tariffs of its EVSE, and chains with the statements before it; it is current no
90
- longer than its key.
91
-
92
- When a roaming network moves into a new epoch and replaces the keys of its operator (example A11), its operator publishes
93
- a new QR code: a key statement signed only by the new keys is refused under the old trust anchor. A verifier that follows
94
- the history commit by commit follows the keys as well (`OperatorTrust.followedBy`), and finds the new trust anchor naming
95
- the keys it followed.
96
-
97
- Without the client, the same with documents of any source:
98
-
99
- ```ts
100
- import { TrustAnchor, verifyChargingStation, verifyPriceStatements } from '@open-charging-cloud/wwcp-poi';
101
-
102
- const anchor = TrustAnchor.parse(qrCodeText);
103
- const station = verifyChargingStation(anchor, proofJSON, { evseId: 'DE*GEF*E1*1' });
104
- const prices = verifyPriceStatements(station, priceStatementsJSON);
105
- ```
106
-
107
- Who checks the meter (example A13): the certificates the station and its energy meters name, each of a conformity
108
- assessment body for a model of charging stations or of energy meters, in hardware revisions and firmware versions, with
109
- its components, the transparency software and, of a station, the meters it mounts; and the transparency software each
110
- meter, and a station signing measurements itself, names with its legal status, proven by the certificates naming it.
111
- POI proves who states them, not that they are true. `statedBy` tells whether the body states a certificate or the
112
- roaming network operator in its place, as the certificate says, and `forThisModel` whether it is for the model, the
113
- hardware revision and the firmware version of what names it (csharp-review 15, 16, 17).
114
-
115
- ```ts
116
- for (const { namedBy, certificate, body, statedBy, forThisModel } of station.certificates)
117
- console.log(namedBy.entityId, certificate['@id'], certificate['legislation'], body['@id'], statedBy, forThisModel);
118
-
119
- for (const { namedBy, legalStatus, release, manufacturer, certificates } of station.transparencySoftware)
120
- console.log(namedBy.entityId, legalStatus, release['version'], manufacturer['craRole'], certificates.map(({ certificate }) => certificate['@id']));
121
- ```
122
-
123
- Months later, the evidence of the session (example A10): the station proof attested and the price statement valid as
124
- charging began, the commit of the station proof and the statement each proven to be part of checkpoints the trusted
125
- witnesses signed:
126
-
127
- ```ts
128
- import { parseEvidence, parseTrustedWitnesses, verifyEvidence } from '@open-charging-cloud/wwcp-poi';
129
-
130
- // driver/evidence-DE_GEF_E1_1.json, as the app of example A10 keeps it
131
- const evidence = verifyEvidence(parseEvidence(keptJSON), { witnesses: parseTrustedWitnesses(witnessesJSON), requiredWitnesses: 2 });
132
- ```
133
-
134
-
135
- ## The traffic report: the status of an EVSE, live
136
-
137
- The status of an EVSE, its energy status and the price shown at this moment are the runtime status of the head of the
138
- history (example A12), pushed over the WebSocket `~/RNs/{RN_Id}/history/ws` the moment they change. They are unsigned,
139
- as trustworthy as the server telling them, and no evidence: a price that has to be provable is a price statement. A
140
- commit pushed with them is signed like every other, and checked like every other.
141
-
142
- ```ts
143
- import { RuntimeStatus, parseFrame, parseStatusUpdate, subscribeRequest } from '@open-charging-cloud/wwcp-poi';
144
-
145
- const status = RuntimeStatus.fromSnapshot(snapshotJSON); // the runtime status beside the head, not part of its version
146
- socket.send(subscribeRequest('1')); // [2,"1","subscribe",{"topics":["statusUpdates", …, "commits"]}]
147
-
148
- socket.onmessage = event => {
149
- const frame = parseFrame(event.data); // [6, id, action, payload]: a push
150
- if (frame.type === 'push' && frame.action !== 'commit')
151
- status.apply(parseStatusUpdate(frame.payload)); // the latest timestamp wins: a late report is 'Outdated'
152
- };
153
-
154
- status.get('EVSE', 'DE*GEF*E1*1', 'status')?.value; // 'charging'
155
- ```
156
-
157
-
158
- ## A charging station operator, a witness, a server: versions and histories
159
-
160
- ```ts
161
- import { History, TrustAnchor, verifyVersion, verifyCheckpointSignatures } from '@open-charging-cloud/wwcp-poi';
162
-
163
- // Example A1: a version of the roaming network and its commit, from the trust anchor alone.
164
- const version = verifyVersion(TrustAnchor.parse(qrCodeText), networkJSON, commitJSON);
165
- version.signedBy; // [ 'AlphaOrg', 'BetaOrg', 'GammaOrg' ]
166
-
167
- // Examples A4 to A6: join from the snapshot of a checkpoint, anchored in the trust anchor (10.5): its checkpoint signed as
168
- // the roaming network operator by the holders the trust anchor requires; a snapshot of the head instead by the
169
- // attestations it carries. Then take the commits since one by one, each checked as section 5.2 says: its change set
170
- // signed by its owners, fitting the head, leading to the ETags of the commit. Signed commits and checkpoints are taken
171
- // only with a verifier of their signatures.
172
- let trust = version.trust;
173
- const history = History.fromSnapshot(checkpointSnapshotJSON, {
174
- trustAnchor: TrustAnchor.parse(qrCodeText),
175
- verifyCommit: (commit, before, after) => {
176
- trust.verifyOperatorSignatures(commit.signatures, (s, key) => commit.verifySignature(s, key), 'the commit');
177
- trust = trust.followedBy(after); // the keys its operator states anew, signed by those trusted
178
- },
179
- verifyCheckpoint: checkpoint => verifyCheckpointSignatures(checkpoint, trust, { witnesses, requiredWitnesses: 2 }),
180
- });
181
-
182
- for (const commit of commitsSince)
183
- history.append(commit);
184
-
185
- // Example A11: the final checkpoint closes the epoch; the next one starts from its version, its genesis the one every
186
- // server makes, SHA-512 identifications and ML-DSA signatures from then on.
187
- history.takeCheckpoint(finalCheckpointJSON);
188
- const epoch2 = history.startNextEpoch();
189
- epoch2.root.commit.id; // the published genesis of epoch 2, 128 hexadecimal digits
190
- ```
191
-
192
-
193
- ## What is ported, and what is not yet
194
-
195
- | | |
196
- |---|---|
197
- | Canonical JSON, deterministic CBOR (Styx) | complete, including Styx's string escaping and decimals as `System.Decimal` holds them |
198
- | Keys, thumbprints, signatures | EC2, OKP and AKP COSE keys, read in the words of WWCP_POI; all signature algorithms of section 4; the key usages `infrastructure`, `tariffs`, `priceStatements`, `models`, `software`, `certificates`, `payments`, `party` |
199
- | Trust anchors | QR code (`WWCP/TA-2:` and Base45 of CBOR), URL and JSON forms, read and written; key statements followed by succession |
200
- | Roaming networks | ETags `json`, `cbor`, `effectiveTree`; the roaming network operator, its key statement and certificates; the keys of every party and of the roaming network operator as WWCP_POI reads them, none twice, none both a signing and an identity key, no identity key naming usages; the identifications unique within the network (grid connection points, energy meters, parking entities, transparency software releases and certificates, energy meter templates and groups, groups of operators and providers, offers), each once, compared as the C# types compare them; hardware revisions, firmware versions and serial numbers as WWCP_POI reads them, not empty, no whitespace at either end, those of a model each once; certificates as WWCP_POI reads them, for exactly one model, with its versions and releases; the conformity assessment bodies issuing them as WWCP_POI reads them, a notified body number of four digits for one body only, the legislations they are notified for neither empty nor twice, in their ordinal order, a certificate only under one of them; the content as WWCP_POI writes it (SPECIFICATION.md 2): what it takes and writes otherwise is refused, as written again it would be another text than the one hashed (ordered lists out of their order or with untrimmed texts, lists it leaves out given empty or writes also empty left out, properties given as null, embedded objects naming a JSON-LD context, texts in languages not as Styx writes them or empty, timestamps not UTC to the millisecond, entities without their lastChange, parties without their name, identifications given as numbers, properties of keys and of EV Taler merchants and exchanges it does not read, their base URLs and currencies not as it writes them); the roaming network operator, its key statement and certificates, the tree hash algorithms and the identifications of entities in the words of WWCP_POI.; of every entity what its type in WWCP_POI3 neither writes nor reads, and values it writes otherwise (trimmed, with a scheme, in its case, coordinates as doubles), in the words of its written form since 3de830a ("the content leaves it out", "the content holds it as …"). Not yet: properties of another JSON type or missing, the formats of identifications, and the references, which WWCP_POI refuses |
201
- | Effective members (9.2) | EVSEs, charging stations, charging pools, charging tariffs, energy meters. Parking spaces and energy meters of grid connection points taking templates are refused, not calculated |
202
- | Proofs (8.4), attestations (8.5) | complete, with the manufacturers and models of the station and its meters, the transparency software releases and certificates of the meters; subject attestations do not count for a whole proof |
203
- | Change sets (4) | `Add`, `Remove`, `UpdateProperty`, `RemoveProperty` of parties (charging station operators, e-mobility providers, grid and parking operators, charging station and energy meter manufacturers, transparency software manufacturers, conformity assessment bodies), the roaming network and its operator, of charging pools, stations, EVSEs, tariffs, templates and groups of charging station operators, energy meter templates and groups, models of manufacturers, transparency software releases and certificates, with their owners' signatures, quorum and proof of possession, and the properties a change set may change: not the version of a release, also not by removing it and adding it again; the `notifiedBodyNumber` and `notifiedFor` of a conformity assessment body by the roaming network operator only. Element changes, preconditions, offers and the entities of providers, grid and parking operators are refused, not applied. The references the C# implementation checks after every change set (a manufacturer, model, release or certificate a station, meter or certificate names) are not checked again: the `afterETags` of the change set pin the version it leads to. The identifications unique within the network are, as in every version |
204
- | Histories (5, 10, 11) | linear histories from snapshots, anchored in the trust anchor as a peer of WWCP_POI starts (10.5): a snapshot of a checkpoint by the checkpoint, signed as the roaming network operator, a snapshot of the head by its attestations, none without a trust anchor unless taken unanchored explicitly; commits, checkpoints, their lines and signatures, signed ones only with a verifier; proofs of commits, archive segments; epochs: their profiles, the final checkpoint, the genesis of the next epoch, its first checkpoint. Merges are not taken yet; no peer, no server |
205
- | Price statements (12) | statements, chains, checkpoint trees and proofs in every epoch |
206
- | Runtime status (12.5) | status updates, frames of the WebSocket, the runtime status of a snapshot. No WebSocket client: the app brings its own |
207
- | QI-Digital documents | a proof of concept of WWCP_POI, as example A14 uses it: digital calibration certificates, digital certificates of conformity of a model or of the quality system of a manufacturer, digital declarations of conformity, read in the words of WWCP_POI and checked against a roaming network (the keys of their issuers, the models, the charging stations and energy meters as they are at runtime) and against each other (a declaration and the certificates it names, a certificate and the meter it covers). Their signature input is what WWCP_POI writes again of what it read, numbers without trailing zeros. Read and checked, not signed |
208
- | EV Taler | phase 1 of WWCP_POI's concept of paying for charging with GNU Taler: the merchant of a charging station operator (`evTaler`), read and refused as WWCP_POI reads it, changed by a key that may sign `payments`; payment options an open set of names, none twice whatever its case; the payment option EVTaler of a charging pool or template only with a merchant; the EV Taler tariff of an EVSE and the exchanges in its currency, from the proof of its station (`evTalerTariffOf`, `evTalerExchangesAt`, `paymentOptionName`). Nothing here takes or makes a payment |
209
-
210
- Whatever is not ported is refused with a `POIError` saying so, but for the references named above: the library never
211
- takes as checked what it can not check.
212
- And whatever it calculated otherwise than the C# implementation would end in other ETags or identifications, and be
213
- refused as well.
214
-
215
-
216
- ## Local authorization and shared trust contexts
217
-
218
- An app that lets its users import a trust anchor before they trust its keys does not hand that anchor to a verifier: a
219
- verifier given a `TrustAnchor` trusts every identity key it pins. The app evaluates a `TrustContext` instead, from the
220
- identity keys it trusts locally:
221
-
222
- ```ts
223
- import { ChargingStationProof, RoamingNetworkOperator, TrustContext, verifyChargingStation } from '@open-charging-cloud/wwcp-poi';
224
-
225
- // The key statement and the identity keys of the operator as the proof gives them: trusted only once evaluated.
226
- const proof = ChargingStationProof.parse(proofJSON);
227
- const operator = RoamingNetworkOperator.parse(proof.roamingNetwork['operator'], anchor.id);
228
-
229
- if (operator.keyStatement !== undefined) {
230
-
231
- const result = TrustContext.evaluate(anchor, operator.keyStatement, {
232
- trustedIdentityKeyIds: locallyAuthorizedKeyIds,
233
- otherIdentityKeys: operator.identityKeys,
234
- at: Date.now()
235
- });
236
-
237
- // Show signatures, signers, quorum and validity apart. canTrustKeys allows importing
238
- // validly signed data even without a trusted quorum.
239
- if (result.context !== undefined) {
240
- const station = verifyChargingStation(result.context, proof, { evseId: 'DE*GEF*E1*1' });
241
- }
242
-
243
- }
244
- ```
245
-
246
- `TrustContext.evaluate` keeps of the holders of the trust anchor the identity keys the app trusts, and checks every
247
- signature of the key statement and that its key was valid when the statement was signed, that the statement is valid at
248
- `at`, that it names the holders of the trusted keys as the trust anchor does, and that trusted keys of as many holders
249
- as the larger of both quorums signed it. Two trusted keys of one holder still count once. A context exists only when all
250
- of that holds; `TrustContext.create` evaluates the same and throws otherwise.
251
-
252
- The context binds what is verified with it to the roaming network, its operator and the signed content of the key
253
- statement, which a statement embedded without its JSON-LD context gives as well. Another key statement, even one signed
254
- genuinely, needs another evaluation: a context does not follow the keys of the operator to their successors by itself.
255
- Other valid signatures, or the same in another order, do not change the content; every signature given and the trusted
256
- quorum are checked again.
257
-
258
- `OperatorTrust.of` and `fromOperatorJSON`, `verifyVersion`, `verifyChargingStation`, `ChargingStationProof.verify` and
259
- `verifyCheckpoint` take a context in place of a trust anchor. A context is evaluated for one time: `at` is that time or
260
- left out, another time needs another context. Given a trust anchor, they verify as before, trusting every key it pins.
261
- The other APIs taking a trust anchor, among them `verifyEvidence` and `POIClient`, take only one the app has authorized:
262
- they are no way around its local policy.
263
-
264
- The app keeps storing and showing its decisions, choosing keys and excluding them locally. It reads its current
265
- decisions consistently and creates a context from them for every verification: a context is no authority to store, nor
266
- to use again after a key was withdrawn or deleted or the policy changed, and does not see what changes after it was
267
- created. The library needs no IndexedDB, localStorage, network, DOM, Node or anything of an app.
268
-
269
- Removing a pinned key from the local roots removes the trust it started with. A key statement signed by the quorum may
270
- still name it among its identity keys and so delegate to it: excluding a key locally is no global revocation or deny
271
- list. Revocation and verified succession need policies of their own. Unknown imports, invalid evidence and expired data
272
- never give an authorized context.
273
-
274
-
275
- ## Tests
276
-
277
- ```bash
278
- npm run verify
279
- ```
280
-
281
- `verify` runs what CI runs: the type checks (as Node and as a browser sees `src/`), ESLint with the rules of
282
- ChargyCore.TS, and `npm test`, which is
283
-
284
- - `npm run test:node`: the tests below, in Node;
285
- - `npm run build` and `npm run test:bundle`: what `dist/` holds, as an app gets it: the bundle imports only the
286
- dependencies `package.json` names and neither `node:*` modules, `Buffer` nor `process`; an app imports it by the name
287
- of the package in plain Node and gets its declared types with TypeScript (`nodenext` and `bundler`); `npm pack`
288
- publishes nothing but `dist/` and the documents;
289
- - `npm run test:browser`: a trust anchor, a commit, a station proof of ML-DSA attestations, a price and the evidence of
290
- a session, checked in Chromium (Vitest browser mode with Playwright; once: `npx playwright install chromium`).
291
-
292
- GitHub Actions run it on every push and pull request (`.github/workflows/ci.yml`): Ubuntu on Node 22.18, the minimum
293
- `package.json` names, and on Node 26, Windows on Node 24. Every night (`.github/workflows/nightly.yml`) the same suite
294
- runs again, and, informational, with the dependencies the lockfile does not pin, with the newest releases of
295
- `@vanaheimr/cose` and `@vanaheimr/metrological-cbor`, and as `npm audit`. The C# implementation as it is today is checked
296
- every night by [WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests): its nightly run
297
- holds WWCP_POI master and this library to the same golden vectors, and runs the C# examples of WWCP_POI master anew,
298
- checked with `examples/validate-examples.ts` of this repository, which turns red the day WWCP_POI moves on and this port
299
- needs work.
300
-
301
- The test vectors are in `tests/vectors`, vendored from
302
- [WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests), where they are generated -
303
- never edit them here, and its `VendoredCopiesTests` fail when these copies drift:
304
-
305
- - `tests/vectors/poi-*.json`, the golden vectors: canonical JSON and CBOR, Base64 and Base45, the examples of section 6
306
- of the specification, station proofs of its example network signed with its keys against a trust anchor, what to
307
- calculate of the documents of the run below, documents to refuse, the QI-Digital documents of example A14 of that run
308
- taken and refused as WWCP_POI checks them, and small histories WWCPPOIConformanceTests signs itself, replayed step by
309
- step (the format: the
310
- [README of the vectors](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests/blob/master/test-vectors/README.md));
311
- - `tests/vectors/csharp-33447ac`, what one run of the C# examples of WWCP_POI at commit `33447ac` published (public data
312
- only; its output is `csharp-output.log` there), and in `proofs/`, what `proofs/MakeProofs.cs.txt` made of the same
313
- history with the C# implementation: proofs of charging stations of epoch 2, the commits of epoch 2 up to the head, and
314
- the ECDSA twin of a price statement.
315
-
316
- - `tests/conformance.test.ts`: the golden vectors, as the C# implementation runs them in WWCPPOIConformanceTests.
317
- - `tests/canonical.test.ts`, `tests/specification.test.ts`: the test vectors of Styx and of section 6 of the
318
- specification.
319
- - `tests/exampleA1.test.ts`, `tests/examples.test.ts`: the examples A1 to A9, epoch 1, the manufacturers and the models
320
- they state among them.
321
- - `tests/evidence.test.ts`: example A10, the evidence a driver's app keeps, and ECDSA twins of price statements.
322
- - `tests/exampleA11.test.ts`: example A11, epoch 2, followed with the old trust anchor and joined with the new one.
323
- - `tests/exampleA12.test.ts`: example A12, the runtime status and the commit pushed over the WebSocket.
324
- - `tests/exampleA13.test.ts`: example A13, transparency software manufacturers, conformity assessment bodies, and who
325
- checks the meter.
326
- - `tests/qidigital.test.ts`: example A14, the QI-Digital documents of a meter as WWCP_POI signed them, and others signed
327
- with the keys of section 6 of the specification, to change them at will.
328
- - `tests/chargingStation.test.ts`, `tests/client.test.ts`: proofs of charging stations of epoch 2, as an app checks them.
329
- - `tests/replay.test.ts`: the whole history of `QA`, both epochs, commit by commit, with every checkpoint.
330
- - `tests/build/bundle.test.js`, `tests/browser/browser.test.ts`: the bundle and the browser, as above.
331
-
332
- To check a run of your own of the C# examples, with its new keys:
333
-
334
- ```bash
335
- npx tsx examples/validate-examples.ts D:\Coding\OpenChargingCloud\WWCP_POI\examples\csharp
336
- ```
337
-
338
-
339
- ## Publishing
340
-
341
- ```
342
- npm version 0.1.1 --no-git-tag-version
343
- git commit -a
344
- git tag -s v0.1.1 -m "v0.1.1"
345
- git push origin master v0.1.1
346
- git push git1 master v0.1.1
347
- git push git2 master v0.1.1
348
-
349
- npm run verify
350
- npm pack --dry-run
351
- npm pack
352
- npm login
353
- npm whoami
354
- npm publish
355
- ```
356
-
357
-
358
- ## License
359
-
360
- [GNU Affero General Public License v3.0](LICENSE), as WWCP_POI.
361
-
362
- Copyright 2014-2026 GraphDefined GmbH
1
+ # WWCP POI for TypeScript
2
+
3
+ [![CI](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/ci.yml/badge.svg)](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/ci.yml)
4
+ [![Nightly](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/nightly.yml/badge.svg)](https://github.com/OpenChargingCloud/WWCP_POI.TS/actions/workflows/nightly.yml)
5
+ [![npm](https://img.shields.io/npm/v/@open-charging-cloud/wwcp-poi)](https://www.npmjs.com/package/@open-charging-cloud/wwcp-poi)
6
+
7
+ Verifies what a roaming network of [WWCP/POI](https://github.com/OpenChargingCloud/WWCP_POI) publishes about its
8
+ charging stations and their tariffs, from the **trust anchor** of the roaming network alone: the QR code its operator
9
+ publishes, naming the identity keys of the operator and its quorum.
10
+
11
+ An EV driver's app selects a charging station; this library downloads the proof of the station and the price statements
12
+ of its EVSEs, and checks that the station, its EVSEs as they are at runtime, the charging tariffs they take and the
13
+ prices of the moment are the ones the roaming network operator attested and the charging station operator signed - before
14
+ charging begins. Months later, e.g. disputing an invoice, the evidence the app kept proves which price applied, witnessed
15
+ by checkpoints. The same proof gives the public keys of the energy meters, with which
16
+ [ChargyCore.TS](https://github.com/OpenChargingCloud/ChargyCore.TS) checks the signed meter values of the session, and the
17
+ transparency software and its certificate that say who checks them.
18
+
19
+ Everything is calculated as the C# implementation calculates it, byte for byte: the canonical JSON and the deterministic
20
+ CBOR of Styx, ETags, the effective tree with templates (section 8 of the specification), commit and checkpoint
21
+ identifications of every epoch, COSE keys and their thumbprints, and the signatures ES256/384/512, Ed25519, Ed448 and
22
+ ML-DSA-44/65/87.
23
+
24
+
25
+ ## Status
26
+
27
+ **0.3.0 is prepared, not yet published to npm.** It reads, refuses and applies versions, change sets and histories as
28
+ WWCP_POI3 does, in its words, and adds EV Taler phase 2. See [the changelog](CHANGELOG.md#030-prepared-not-yet-published)
29
+ for the changes and migration requirements: versions, change sets and signatures 0.2.0 took are refused. Until then
30
+ 0.2.0, published to npm on 2 November 2026, is the latest release. The experimental profiles below remain experimental;
31
+ a package release does not establish production witness services or independently verified actual measurement time.
32
+
33
+ **Milestone 1: the examples of WWCP_POI, checked in TypeScript.** The examples A1 to A14 of
34
+ [WWCP_POI/examples](https://github.com/OpenChargingCloud/WWCP_POI/tree/master/examples) are checked from what the C#
35
+ examples publish, as their `Validate()` methods check them, and the same for proofs of charging stations the C#
36
+ implementation makes: in the tests from the run of WWCP_POI `045e180` vendored here, which also holds the EV Taler
37
+ merchant of example A15, and every night from a new run of WWCP_POI master, with new keys. EV Taler phase 2, which
38
+ example A16 shows, is checked on the proof of example A15 and probed against WWCP_POI3; what example A16 publishes is not
39
+ checked yet. See [Tests](#tests).
40
+
41
+ What a review of the C# implementation found while porting it is in
42
+ [docs/csharp-review.md of WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests/blob/master/docs/csharp-review.md),
43
+ cited here as *csharp-review N*.
44
+
45
+ Whether both implementations really calculate, take and refuse the same is the business of
46
+ [WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests): golden test vectors, run against
47
+ this library and the C# implementation alike, and where they differ, written down and pinned
48
+ ([docs/implementation-deviations.md](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests/blob/master/docs/implementation-deviations.md)).
49
+ This repository vendors the vectors and runs them in its own suite (`tests/conformance.test.ts`).
50
+
51
+
52
+ ## Installation
53
+
54
+ From npm, as [`@open-charging-cloud/wwcp-poi`](https://www.npmjs.com/package/@open-charging-cloud/wwcp-poi):
55
+
56
+ ```bash
57
+ npm install @open-charging-cloud/wwcp-poi
58
+ ```
59
+
60
+ ESM, Node 22.18 or newer, browsers and React Native alike: no `node:*` module, no `Buffer`, no WebCrypto. The
61
+ cryptography is [`@vanaheimr/cose`](https://github.com/Vanaheimr/COSE.TS) on the `@noble` family, the CBOR codec
62
+ [`@vanaheimr/metrological-cbor`](https://github.com/Vanaheimr/MetrologicalCBOR.TS).
63
+
64
+
65
+ ## An app: a charging station and its prices
66
+
67
+ ```ts
68
+ import { POIClient } from '@open-charging-cloud/wwcp-poi';
69
+
70
+ // The text of the QR code of the roaming network, as scanned, e.g. from the website of its operator.
71
+ const client = new POIClient('WWCP/TA-2:B7LJ683VCBCACVCC$C6REF6B…');
72
+
73
+ // GET ~/RNs/QA/EVSEs/DE*GEF*E1*1/proof and …/priceStatements, from the first server of the trust anchor whose answer verifies.
74
+ const checked = await client.chargingStation({ evseId: 'DE*GEF*E1*1' });
75
+
76
+ checked.station.roamingNetwork.name; // 'Quality Assurance', as the operator signed it, not as the QR code says
77
+ checked.station.chargingStation; // the station as it is at runtime, with what its templates give it
78
+ checked.station.tariffs.get('DE*GEF*E1*1'); // the charging tariffs the EVSE takes
79
+ checked.station.manufacturer?.model; // its model, as its manufacturer states it: revisions, firmware, ratings
80
+ checked.station.energyMeters; // with their public keys, for transparency software
81
+ checked.station.energyMeterModels; // per meter: its manufacturer and model
82
+ checked.station.certificates; // of the station and its meters: each for a model, with its components
83
+ checked.station.transparencySoftware; // per meter: the release, its manufacturer, the certificates naming it
84
+ checked.prices.get('EVSE:DE*GEF*E1*1')?.statement; // the price statement valid now, signed by DE*GEF
85
+ checked.documents; // as they came: keep them as evidence of the session
86
+ ```
87
+
88
+ What is checked (sections 8.4 to 8.6, 12.4 of the specification):
89
+
90
+ - the proof leads to a root, the `effectiveTree` ETag of a version of the roaming network;
91
+ - the key statement of the roaming network operator the proof gives is signed by identity keys the trust anchor pins, of
92
+ as many holders as its `minConsensus` requires, counted as the trust anchor names them per holder, names their holders
93
+ so as well, and is valid now;
94
+ - attestations of that root, valid now - signed then or before, until their `validUntil`, at most a day later - by
95
+ signing keys of the operator, valid when they attested and now, each certified by a trusted identity key of its
96
+ holder, come from as many holders, each counted for the holder of that identity key as the trust anchor knows it;
97
+ - each price statement is signed by a signing key of the charging station operator that the proof gives and that may sign
98
+ `priceStatements`, names only charging tariffs of its EVSE, and chains with the statements before it; it is current no
99
+ longer than its key.
100
+
101
+ When a roaming network moves into a new epoch and replaces the keys of its operator (example A11), its operator publishes
102
+ a new QR code: a key statement signed only by the new keys is refused under the old trust anchor. A verifier that follows
103
+ the history commit by commit follows the keys as well (`OperatorTrust.followedBy`), and finds the new trust anchor naming
104
+ the keys it followed.
105
+
106
+ Without the client, the same with documents of any source:
107
+
108
+ ```ts
109
+ import { TrustAnchor, verifyChargingStation, verifyPriceStatements } from '@open-charging-cloud/wwcp-poi';
110
+
111
+ const anchor = TrustAnchor.parse(qrCodeText);
112
+ const station = verifyChargingStation(anchor, proofJSON, { evseId: 'DE*GEF*E1*1' });
113
+ const prices = verifyPriceStatements(station, priceStatementsJSON);
114
+ ```
115
+
116
+ Who checks the meter (example A13): the certificates the station and its energy meters name, each of a conformity
117
+ assessment body for a model of charging stations or of energy meters, in hardware revisions and firmware versions, with
118
+ its components, the transparency software and, of a station, the meters it mounts; and the transparency software each
119
+ meter, and a station signing measurements itself, names with its legal status, proven by the certificates naming it.
120
+ POI proves who states them, not that they are true. `statedBy` tells whether the body states a certificate or the
121
+ roaming network operator in its place, as the certificate says, and `forThisModel` whether it is for the model, the
122
+ hardware revision and the firmware version of what names it (csharp-review 15, 16, 17).
123
+
124
+ ```ts
125
+ for (const { namedBy, certificate, body, statedBy, forThisModel } of station.certificates)
126
+ console.log(namedBy.entityId, certificate['@id'], certificate['legislation'], body['@id'], statedBy, forThisModel);
127
+
128
+ for (const { namedBy, legalStatus, release, manufacturer, certificates } of station.transparencySoftware)
129
+ console.log(namedBy.entityId, legalStatus, release['version'], manufacturer['craRole'], certificates.map(({ certificate }) => certificate['@id']));
130
+ ```
131
+
132
+ Months later, the evidence of the session (example A10): the station proof attested and the price statement valid as
133
+ charging began, the commit of the station proof and the statement each proven to be part of checkpoints the trusted
134
+ witnesses signed:
135
+
136
+ ```ts
137
+ import { parseEvidence, parseTrustedWitnesses, verifyEvidence } from '@open-charging-cloud/wwcp-poi';
138
+
139
+ // driver/evidence-DE_GEF_E1_1.json, as the app of example A10 keeps it
140
+ const evidence = verifyEvidence(parseEvidence(keptJSON), { witnesses: parseTrustedWitnesses(witnessesJSON), requiredWitnesses: 2 });
141
+ ```
142
+
143
+
144
+ ## The traffic report: the status of an EVSE, live
145
+
146
+ The status of an EVSE, its energy status and the price shown at this moment are the runtime status of the head of the
147
+ history (example A12), pushed over the WebSocket `~/RNs/{RN_Id}/history/ws` the moment they change. They are unsigned,
148
+ as trustworthy as the server telling them, and no evidence: a price that has to be provable is a price statement. A
149
+ commit pushed with them is signed like every other, and checked like every other.
150
+
151
+ ```ts
152
+ import { RuntimeStatus, parseFrame, parseStatusUpdate, subscribeRequest } from '@open-charging-cloud/wwcp-poi';
153
+
154
+ const status = RuntimeStatus.fromSnapshot(snapshotJSON); // the runtime status beside the head, not part of its version
155
+ socket.send(subscribeRequest('1')); // [2,"1","subscribe",{"topics":["statusUpdates", …, "commits"]}]
156
+
157
+ socket.onmessage = event => {
158
+ const frame = parseFrame(event.data); // [6, id, action, payload]: a push
159
+ if (frame.type === 'push' && frame.action !== 'commit')
160
+ status.apply(parseStatusUpdate(frame.payload)); // the latest timestamp wins: a late report is 'Outdated'
161
+ };
162
+
163
+ status.get('EVSE', 'DE*GEF*E1*1', 'status')?.value; // 'charging'
164
+ ```
165
+
166
+
167
+ ## A charging station operator, a witness, a server: versions and histories
168
+
169
+ ```ts
170
+ import { History, TrustAnchor, verifyVersion, verifyCheckpointSignatures } from '@open-charging-cloud/wwcp-poi';
171
+
172
+ // Example A1: a version of the roaming network and its commit, from the trust anchor alone.
173
+ const version = verifyVersion(TrustAnchor.parse(qrCodeText), networkJSON, commitJSON);
174
+ version.signedBy; // [ 'AlphaOrg', 'BetaOrg', 'GammaOrg' ]
175
+
176
+ // Examples A4 to A6: join from the snapshot of a checkpoint, anchored in the trust anchor (10.5): its checkpoint signed as
177
+ // the roaming network operator by the holders the trust anchor requires; a snapshot of the head instead by the
178
+ // attestations it carries. Then take the commits since one by one, each checked as section 5.2 says: its change set
179
+ // signed by its owners, fitting the head, leading to the ETags of the commit. Signed commits and checkpoints are taken
180
+ // only with a verifier of their signatures.
181
+ let trust = version.trust;
182
+ const history = History.fromSnapshot(checkpointSnapshotJSON, {
183
+ trustAnchor: TrustAnchor.parse(qrCodeText),
184
+ verifyCommit: (commit, before, after) => {
185
+ trust.verifyOperatorSignatures(commit.signatures, (s, key) => commit.verifySignature(s, key), 'the commit');
186
+ trust = trust.followedBy(after); // the keys its operator states anew, signed by those trusted
187
+ },
188
+ // A checkpoint is taken with the signatures that verified here, the others left out (S39).
189
+ verifyCheckpoint: checkpoint => verifyCheckpointSignatures(checkpoint, trust, { witnesses, requiredWitnesses: 2 }).verified,
190
+ });
191
+
192
+ for (const commit of commitsSince)
193
+ history.append(commit);
194
+
195
+ // Example A11: the final checkpoint closes the epoch; the next one starts from its version, its genesis the one every
196
+ // server makes, SHA-512 identifications and ML-DSA signatures from then on.
197
+ history.takeCheckpoint(finalCheckpointJSON);
198
+ const epoch2 = history.startNextEpoch();
199
+ epoch2.root.commit.id; // the published genesis of epoch 2, 128 hexadecimal digits
200
+ ```
201
+
202
+
203
+ ## What is ported, and what is not yet
204
+
205
+ | | |
206
+ |---|---|
207
+ | Canonical JSON, deterministic CBOR (Styx) | complete, including Styx's string escaping and decimals as `System.Decimal` holds them |
208
+ | Keys, thumbprints, signatures | EC2, OKP, AKP and RSA COSE keys, read in the words of WWCP_POI; all signature algorithms of section 4; the key usages `infrastructure`, `tariffs`, `priceStatements`, `models`, `software`, `certificates`, `payments`, `party` |
209
+ | Trust anchors | QR code (`WWCP/TA-2:` and Base45 of CBOR), URL and JSON forms, read and written; key statements followed by succession |
210
+ | Roaming networks | ETags `json`, `cbor`, `effectiveTree`; the roaming network operator, its key statement and certificates; the keys of every party and of the roaming network operator as WWCP_POI reads them, none twice, none both a signing and an identity key, no identity key naming usages; the identifications unique within the network (grid connection points, energy meters, parking entities, transparency software releases and certificates, energy meter templates and groups, groups of operators and providers, offers), each once, compared as the C# types compare them; transparency software releases and certificates identified under the manufacturer or body that has them (SPECIFICATION.md 4.1): its `@id`, a colon and a name without one, the prefix compared without case, also after a change set; hardware revisions, firmware versions and serial numbers as WWCP_POI reads them, not empty, no whitespace at either end, those of a model each once; certificates as WWCP_POI reads them, for exactly one model, with its versions and releases; the conformity assessment bodies issuing them as WWCP_POI reads them, a notified body number of four digits for one body only, the legislations they are notified for neither empty nor twice, in their ordinal order, a certificate only under one of them; the content as WWCP_POI writes it (SPECIFICATION.md 2): what it takes and writes otherwise is refused, as written again it would be another text than the one hashed (ordered lists out of their order or with untrimmed texts, lists it leaves out given empty or writes also empty left out, properties given as null, embedded objects naming a JSON-LD context, texts in languages not as Styx writes them or empty, timestamps not UTC to the millisecond, entities without their lastChange, parties without their name, identifications given as numbers, properties of keys and of EV Taler merchants and exchanges it does not read, their base URLs and currencies not as it writes them); the roaming network operator, its key statement and certificates, the tree hash algorithms and the identifications of entities in the words of WWCP_POI.; of every entity what its type in WWCP_POI3 neither writes nor reads, and values it writes otherwise (trimmed, with a scheme, in its case, coordinates as doubles, numbers, booleans and texts as their types, durations in whole ticks, opening times as Styx writes them, NUTS codes in upper case, countries of addresses by their alpha-3 codes), in the words of its written form since 3de830a ("the content leaves it out", "the content holds it as …"). Every entity WWCP_POI3 reads, as it reads it, refused in its words and order (`reading.ts`): JSON types, mandatory properties, values, identifications, names of enumerations, currencies, URLs, timestamps, texts in languages, data licenses, and what the constructor of each entity refuses of what it read; the references the constructor of the roaming network checks, with the identifications unique within it, in its order (`references.ts`): manufacturers, models, hardware revisions, firmware versions, releases, certificates, grid connection points, charging stations, parking spaces and maintainers the network has, stations and meters within what their models allow, no key of two parties; time zones of charging pools and their templates of a region of the IANA time zone database, as it writes them, which the platform knows; charging tariff offers of an operator not overlapping, in the time zones of their EVSEs, and of an e-mobility provider not made to overlap by a change set of it (`offers.ts`); measurement public keys and their certificates as WWCP_POI3 checks them with BouncyCastle (`measurementPublicKey.ts`): DER, RSA, EC of the 84 named curves BouncyCastle knows, Ed25519, Ed448, X25519, X448, DSA, ML-DSA, ML-KEM, SLH-DSA, each certificate issued by the next one. Taken as given, where WWCP_POI3 checks them: points on curves over binary fields, explicit curve parameters, keys of Diffie-Hellman, ElGamal, GOST and DSTU |
211
+ | Effective members (9.2) | EVSEs, charging stations, charging pools, charging tariffs, energy meters. Parking spaces and energy meters of grid connection points taking templates are refused, not calculated |
212
+ | Proofs (8.4), attestations (8.5) | complete, with the manufacturers and models of the station and its meters, the transparency software releases and certificates of the meters; subject attestations do not count for a whole proof; whether a proof gives a child array of an entity completely, and its size also where it gives some or none (`givesCompletely`, `sizeOf`) |
213
+ | Change sets (4) | `Add`, `Remove`, `UpdateProperty`, `RemoveProperty` of parties (charging station operators, e-mobility providers, grid and parking operators, charging station and energy meter manufacturers, transparency software manufacturers, conformity assessment bodies), the roaming network and its operator, of charging pools, stations, EVSEs, tariffs, templates and groups of charging station operators, energy meter templates and groups, models of manufacturers, transparency software releases and certificates, with their owners' signatures, quorum and proof of possession, and the properties a change set may change: not the version of a release, also not by removing it and adding it again; the `notifiedBodyNumber` and `notifiedFor` of a conformity assessment body by the roaming network operator only. The charging tariff offers of operators and providers, the groups of providers, the grid connection points of grid operators, the parking data of parking operators and the parking spaces of operators, owned by the party embedding them, a grid connection point by its maintainer. `AddElement`, `RemoveElement`, `ReplaceElement`, `UpdateElementProperty`, `RemoveElementProperty` of all of these, by their element path, as WWCP_POI3 applies them (`elements.ts`): as the change of the property of the entity their path starts with, owned and checked as that change, their old value compared with the element they address; keys and tree hash algorithms changed as a whole only. The properties of infrastructure a change set may change, as WWCP_POI3 names them. Preconditions as WWCP_POI3 checks them (`preconditions.ts`): the ETag an entity still has in the version a change set is applied to, over its representation as the roaming network embeds it, in JSON or CBOR, the entity found by its type and identification as its type compares it. Stamped as WWCP_POI 12e993a stamps it (S5): the lastChange of the network, of what it changes, adds and embeds, and the created of what it adds without one, are its createdAt, or the lastChange of the version it is applied to where that is later, so that no lastChange goes back; its keys are checked at its createdAt. The references and the identifications unique within the network are checked after the last change, as the C# implementation checks them there, in its words and order; what an entity refuses as it is read keeps the words of a version, without the `Change N:` of the C# implementation |
214
+ | Histories (5, 10, 11) | linear histories from snapshots, anchored in the trust anchor as a peer of WWCP_POI starts (10.5): a snapshot of a checkpoint by the checkpoint, signed as the roaming network operator, a snapshot of the head by its attestations, none without a trust anchor unless taken unanchored explicitly; its `effectiveLastChanges` read as WWCP_POI reads them, not taken, as the effective tree takes none (8.1); commits, checkpoints, their lines and signatures, signatures read as WWCP_POI reads them (texts trimmed, none empty), signed ones only with a verifier, checkpoints taken with the signatures that verify here, the others left out (S39), the roles they must be signed in (`requiredSignatureRoles`, `requiredCheckpointSignatureRoles`), a role of a commit only by a key its version lets sign in it (`keyMaySignIn`), the signatures of one key in one role counted once; proofs of commits, archive segments; epochs: their profiles, the final checkpoint, the genesis of the next epoch, its first checkpoint. Merges are not taken yet; no peer, no server |
215
+ | Price statements (12) | statements, chains, checkpoint trees and proofs in every epoch |
216
+ | Runtime status (12.5) | status updates, frames of the WebSocket, the runtime status of a snapshot. No WebSocket client: the app brings its own |
217
+ | QI-Digital documents | a proof of concept of WWCP_POI, as example A14 uses it: digital calibration certificates, digital certificates of conformity of a model or of the quality system of a manufacturer, digital declarations of conformity, read in the words of WWCP_POI and checked against a roaming network (the keys of their issuers, the models, the charging stations and energy meters as they are at runtime) and against each other (a declaration and the certificates it names, a certificate and the meter it covers). Their signature input is what WWCP_POI writes again of what it read, numbers without trailing zeros. Read and checked, not signed |
218
+ | EV Taler | phase 1 of WWCP_POI's concept of paying for charging with GNU Taler: the merchant of a charging station operator (`evTaler`), read and refused as WWCP_POI reads it, changed by a key that may sign `payments`; payment options an open set of names, none twice whatever its case; the payment option EVTaler of a charging pool or template only with a merchant; the EV Taler tariff of an EVSE and the exchanges in its currency, from the proof of its station (`evTalerTariffOf`, `evTalerExchangesAt`, `paymentOptionName`). Phase 2, as WWCP_POI c2d39e5 checks it (`src/evTaler`): the contract terms of the steps of a session (`EVTalerContractTerms`, version 0), their step in `extra.wwcp` (`EVTalerStep`), a `taler://pay` URI (`EVTalerPayURI`), checked against the proof of the station: the operator's merchant and exchanges, the EV Taler tariff of the EVSE (`evTalerTariff`), the price statement current when the session began, each step following the one before, from step 2 on the signed meter value at the start verified with a measurement key the proof gives by a transparency software passed in (`EVTalerMeterValueVerifier`; WWCP_POI reads no format of a meter), the amount of a step recalculated where the prices are of the simple profile (`EVTalerRates`), as System.Decimal calculates it (`NetDecimal`); the settlement statement (`EVTalerSettlement`), signed by the operator with a key for `payments`, refused without both signed meter values, its price recalculated. Nothing here takes or makes a payment, nor signs a settlement |
219
+
220
+ Whatever is not ported is refused with a `POIError` saying so, but for what the row of roaming networks names as not
221
+ yet ported: the library takes no other version the C# implementation refuses.
222
+ And whatever it calculated otherwise than the C# implementation would end in other ETags or identifications, and be
223
+ refused as well.
224
+
225
+
226
+ ## Local authorization and shared trust contexts
227
+
228
+ An app that lets its users import a trust anchor before they trust its keys does not hand that anchor to a verifier: a
229
+ verifier given a `TrustAnchor` trusts every identity key it pins. The app evaluates a `TrustContext` instead, from the
230
+ identity keys it trusts locally:
231
+
232
+ ```ts
233
+ import { ChargingStationProof, RoamingNetworkOperator, TrustContext, verifyChargingStation } from '@open-charging-cloud/wwcp-poi';
234
+
235
+ // The key statement and the identity keys of the operator as the proof gives them: trusted only once evaluated.
236
+ const proof = ChargingStationProof.parse(proofJSON);
237
+ const operator = RoamingNetworkOperator.parse(proof.roamingNetwork['operator'], anchor.id);
238
+
239
+ if (operator.keyStatement !== undefined) {
240
+
241
+ const result = TrustContext.evaluate(anchor, operator.keyStatement, {
242
+ trustedIdentityKeyIds: locallyAuthorizedKeyIds,
243
+ otherIdentityKeys: operator.identityKeys,
244
+ at: Date.now()
245
+ });
246
+
247
+ // Show signatures, signers, quorum and validity apart. canTrustKeys allows importing
248
+ // validly signed data even without a trusted quorum.
249
+ if (result.context !== undefined) {
250
+ const station = verifyChargingStation(result.context, proof, { evseId: 'DE*GEF*E1*1' });
251
+ }
252
+
253
+ }
254
+ ```
255
+
256
+ `TrustContext.evaluate` keeps of the holders of the trust anchor the identity keys the app trusts, and checks every
257
+ signature of the key statement and that its key was valid when the statement was signed, that the statement is valid at
258
+ `at`, that it names the holders of the trusted keys as the trust anchor does, and that trusted keys of as many holders
259
+ as the larger of both quorums signed it. Two trusted keys of one holder still count once. A context exists only when all
260
+ of that holds; `TrustContext.create` evaluates the same and throws otherwise.
261
+
262
+ The context binds what is verified with it to the roaming network, its operator and the signed content of the key
263
+ statement, which a statement embedded without its JSON-LD context gives as well. Another key statement, even one signed
264
+ genuinely, needs another evaluation: a context does not follow the keys of the operator to their successors by itself.
265
+ Other valid signatures, or the same in another order, do not change the content; every signature given and the trusted
266
+ quorum are checked again.
267
+
268
+ `OperatorTrust.of` and `fromOperatorJSON`, `verifyVersion`, `verifyChargingStation`, `ChargingStationProof.verify` and
269
+ `verifyCheckpoint` take a context in place of a trust anchor. A context is evaluated for one time: `at` is that time or
270
+ left out, another time needs another context. Given a trust anchor, they verify as before, trusting every key it pins.
271
+ The other APIs taking a trust anchor, among them `verifyEvidence` and `POIClient`, take only one the app has authorized:
272
+ they are no way around its local policy.
273
+
274
+ The app keeps storing and showing its decisions, choosing keys and excluding them locally. It reads its current
275
+ decisions consistently and creates a context from them for every verification: a context is no authority to store, nor
276
+ to use again after a key was withdrawn or deleted or the policy changed, and does not see what changes after it was
277
+ created. The library needs no IndexedDB, localStorage, network, DOM, Node or anything of an app.
278
+
279
+ Removing a pinned key from the local roots removes the trust it started with. A key statement signed by the quorum may
280
+ still name it among its identity keys and so delegate to it: excluding a key locally is no global revocation or deny
281
+ list. Revocation and verified succession need policies of their own. Unknown imports, invalid evidence and expired data
282
+ never give an authorized context.
283
+
284
+
285
+ ## Tests
286
+
287
+ ```bash
288
+ npm run verify
289
+ ```
290
+
291
+ `verify` runs what CI runs: the type checks (as Node and as a browser sees `src/`), ESLint with the rules of
292
+ ChargyCore.TS, and `npm test`, which is
293
+
294
+ - `npm run test:node`: the tests below, in Node;
295
+ - `npm run build` and `npm run test:bundle`: what `dist/` holds, as an app gets it: the bundle imports only the
296
+ dependencies `package.json` names and neither `node:*` modules, `Buffer` nor `process`; an app imports it by the name
297
+ of the package in plain Node and gets its declared types with TypeScript (`nodenext` and `bundler`); `npm pack`
298
+ publishes nothing but `dist/` and the documents;
299
+ - `npm run test:browser`: a trust anchor, a commit, a station proof of ML-DSA attestations, a price and the evidence of
300
+ a session, checked in Chromium (Vitest browser mode with Playwright; once: `npx playwright install chromium`).
301
+
302
+ GitHub Actions run it on every push and pull request (`.github/workflows/ci.yml`): Ubuntu on Node 22.18, the minimum
303
+ `package.json` names, and on Node 26, Windows on Node 24. Every night (`.github/workflows/nightly.yml`) the same suite
304
+ runs again, and, informational, with the dependencies the lockfile does not pin, with the newest releases of
305
+ `@vanaheimr/cose` and `@vanaheimr/metrological-cbor`, and as `npm audit`. The C# implementation as it is today is checked
306
+ every night by [WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests): its nightly run
307
+ holds WWCP_POI master and this library to the same golden vectors, and runs the C# examples of WWCP_POI master anew,
308
+ checked with `examples/validate-examples.ts` of this repository, which turns red the day WWCP_POI moves on and this port
309
+ needs work.
310
+
311
+ The test vectors are in `tests/vectors`, vendored from
312
+ [WWCPPOIConformanceTests](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests), where they are generated -
313
+ never edit them here, and its `VendoredCopiesTests` fail when these copies drift:
314
+
315
+ - `tests/vectors/poi-*.json`, the golden vectors: canonical JSON and CBOR, Base64 and Base45, the examples of section 6
316
+ of the specification, station proofs of its example network signed with its keys against a trust anchor, what to
317
+ calculate of the documents of the run below, documents to refuse, the QI-Digital documents of example A14 of that run
318
+ taken and refused as WWCP_POI checks them, small histories WWCPPOIConformanceTests signs itself, replayed step by
319
+ step, and the verifier extensions (`poi-verifier.json`) checked against the fixtures of ChargyWebApp below as
320
+ WWCP_POI3 checks them (the format: the
321
+ [README of the vectors](https://github.com/OpenChargingCloud/WWCPPOIConformanceTests/blob/master/test-vectors/README.md));
322
+ - `tests/vectors/csharp-045e180`, what one run of the C# examples A1 to A15 of WWCP_POI at commit `045e180` published
323
+ (public data only; its output is `csharp-output.log` there), and in `proofs/`, what `proofs/MakeProofs.cs.txt` made of
324
+ the same history with the C# implementation: proofs of charging stations of epoch 2, the commits of epoch 2 up to the
325
+ head, revision 11 after the EV Taler merchant of example A15, and the ECDSA twin of a price statement;
326
+ - `tests/vectors/chargy-fc513cd`, the POI fixtures of ChargyWebApp at commit `fc513cd`, which `poi-verifier.json` names
327
+ its documents in: histories, prices, publications, sessions and trust transitions;
328
+ - `tests/vectors/chargy`, what WWCPPOIConformanceTests makes of them itself, which `poi-verifier.json` names with
329
+ `"documents": "chargy"`: in `POIPrices/`, the price evidence of sessions as ChargyWebApp made it, every signature
330
+ deterministic.
331
+
332
+ - `tests/conformance.test.ts`: the golden vectors, as the C# implementation runs them in WWCPPOIConformanceTests.
333
+ - `tests/canonical.test.ts`, `tests/specification.test.ts`: the test vectors of Styx and of section 6 of the
334
+ specification.
335
+ - `tests/exampleA1.test.ts`, `tests/examples.test.ts`: the examples A1 to A9, epoch 1, the manufacturers and the models
336
+ they state among them.
337
+ - `tests/evidence.test.ts`: example A10, the evidence a driver's app keeps, and ECDSA twins of price statements.
338
+ - `tests/exampleA11.test.ts`: example A11, epoch 2, followed with the old trust anchor and joined with the new one.
339
+ - `tests/exampleA12.test.ts`: example A12, the runtime status and the commit pushed over the WebSocket.
340
+ - `tests/exampleA13.test.ts`: example A13, transparency software manufacturers, conformity assessment bodies, and who
341
+ checks the meter.
342
+ - `tests/qidigital.test.ts`: example A14, the QI-Digital documents of a meter as WWCP_POI signed them, and others signed
343
+ with the keys of section 6 of the specification, to change them at will.
344
+ - `tests/chargingStation.test.ts`, `tests/client.test.ts`: proofs of charging stations of epoch 2, as an app checks them.
345
+ - `tests/replay.test.ts`: the whole history of `QA`, both epochs, commit by commit, with every checkpoint.
346
+ - `tests/reading.test.ts`, `tests/references.test.ts`: `QA`, and `QA` with an entity of every kind
347
+ (`tests/richNetwork.ts`), changed as WWCP_POI3 at 89d2e02 refuses them, in its words.
348
+ - `tests/build/bundle.test.js`, `tests/browser/browser.test.ts`: the bundle and the browser, as above.
349
+
350
+ To check a run of your own of the C# examples, with its new keys:
351
+
352
+ ```bash
353
+ npx tsx examples/validate-examples.ts D:\Coding\OpenChargingCloud\WWCP_POI\examples\csharp
354
+ ```
355
+
356
+
357
+ ## Shared trust and session verification in 0.2.0
358
+
359
+ These APIs were previously supplied in the unpublished 0.1.2-dev.0 candidate. WWCP_POI3 has them as well
360
+ (`WWCP_POI3/Verifier`), except for the local journal of observed versions, which is the app's. The versioned historical
361
+ and session-price evidence, compact trust delegation and publication-receipt profiles remain experimental; they do not
362
+ change the strict standard Evidence schema. See [Migration](CHANGELOG.md#migration) before upgrading a consumer.
363
+
364
+ ### Meter-key association
365
+
366
+ `verifyMeterKey(context, proof, { evseId, meterSerialNumber, keyFormat: 'spki-der' })` re-verifies the original proof
367
+ with a locally authorized `TrustContext`, selects only that EVSE's directly associated meter and requires an exact
368
+ serial match. Multiple keys require an explicit `keyId`; it never searches unrelated meters or tries keys until one
369
+ verifies. No fallback to CPO/RNO signing keys is performed.
370
+
371
+ The first pilot supports public uncompressed ECDSA P-256 measurement keys. `parseMeasurementPublicKey(value, 'spki-der'
372
+ | 'cose-key')` requires an explicit format: it checks SHA-256(SPKI) or the deterministic COSE key thumbprint as
373
+ appropriate, validates the point and produces a canonical COSE key/material ID. It retains the original ID and its
374
+ derivation scheme. Unsupported algorithms and ambiguous associations fail explicitly rather than becoming trusted keys.
375
+
376
+ The result carries RN/RNO, EVSE, station, meter, original/canonical key identity, commit/revision/root, KeyStatement
377
+ digest, evaluation time and a conservative expiry bounded by the counted attestations and their certified keys. It is a
378
+ current-snapshot observation, not historical key-rotation/installation evidence or a permanent authorization token.
379
+ Reconstruct local authorization for each operation. Future station-owned measurement signers and additional key formats
380
+ need explicit association rules; they are not guessed by this pilot.
381
+
382
+ The portable public test vector `tests/vectors/meter-session.json` originates from ChargyWebApp commit 97da24d
383
+ (`tests/fixtures/POISession`); its source manifest is `meter-session-meta.json`. It contains public demonstration keys
384
+ only.
385
+
386
+
387
+ ### Historical meter evidence
388
+
389
+ `parseHistoricalMeterEvidence` and `verifyHistoricalMeterEvidence(currentContext, value, request, policy?)` keep current
390
+ local authorization separate from historical validity. The request supplies the actual original reading times, EVSE,
391
+ meter serial and explicit key format. Expired historical statements and operational certificates are checked at those
392
+ times; each later checkpoint uses its own supplied operator and signature times. The output is an immutable per-time
393
+ binding for Core, not a serialized trust token.
394
+
395
+ The versioned `historicalMeterEvidence/v1` envelope is a TS-library extension, not a change to the existing strict
396
+ Evidence schema. It contains `EVSEId` and `versions`, each with `chargingStationProof`, optional `commitProof`, and
397
+ optional `checkpoints` entries shaped as `{checkpoint, operator}`. Missing inclusion stays visible; malformed supplied
398
+ inclusion fails. Prices are not required just to verify a meter assignment. Only caller-authorized witnesses/revocations
399
+ count. `invalidFrom` compromise times apply to signature times or measurement-key use; ordinary expiry does not
400
+ retroactively invalidate a valid historical signature. An unauthorized current quorum still blocks all checks. Identity
401
+ succession not connected to today's authorized pins fails closed.
402
+
403
+ The claim is RNO attestation at the signed measurement times and optionally checkpoint inclusion. Independently
404
+ established actual time, sole/latest membership, complete owner-signed change replay and checkpoint continuity are not
405
+ inferred. A checkpoint obtained later does not prove it was public before charging. No full network archive is
406
+ downloaded. See the app's illustrated historical-session guide for integration.
407
+
408
+
409
+ `HistoricalTrustContext.create(anchor, savedReferenceStatement, {trustedIdentityKeyIds, otherIdentityKeys?, now?})`
410
+ authorizes historical checks from fresh caller-supplied local decisions. It verifies the saved reference at its own
411
+ signed time; expiry today does not require a server or a newly issued statement. Its one-minute validity bound is an
412
+ operation deadline, not historical document validity. Recreate it for each check and reread local authorization after
413
+ async work. Current `TrustContext` remains the separate path for fresh/current station checks. Unknown identity
414
+ succession still fails.
415
+
416
+
417
+ ### Session-bound prices
418
+
419
+ `verifySessionPriceEvidence` verifies the attested station at the original start, CPO PriceStatements with existing
420
+ generic algorithms (including Ed25519 and ML-DSA), usage/validity, entity/tariff references, supplied chains and
421
+ explicit selection. `SessionPriceAgreement` binds the exact original start document SHA-256, meter/EVSE, time, tariff,
422
+ statement digest, mode and binding/profile in a CPO signature. This is a CPO assertion, not customer consent. Unknown
423
+ agreements never authorize themselves. The experimental `chargy-gross-rates/v1` profile fixes unconditional gross rates
424
+ at start. EMP contracts, variable bindings and unsupported restrictions stay unproven.
425
+
426
+ Checkpoint authority is shared with historical meter verification. Price inclusion, `after` links/continuity, selected
427
+ statement identity and related checkpoint forks are checked separately. A later checkpoint never proves pre-start
428
+ publication, independent time or sole/latest offer. Original decimals are preserved. There is no P-256-only restriction
429
+ on CPO price/assignment signatures. See the app's price guide.
430
+
431
+
432
+ ### Locally observed versions
433
+
434
+ `compareObservedVersions` is a pure, shared policy for monotonic current selection and persistent authenticated
435
+ contradictions. Metadata projection helpers for station proofs, checkpoint positions/root revisions and PriceStatements
436
+ do not authenticate anything: call the usual role, quorum, subject and validity verifiers before recording them. The
437
+ caller owns durable storage and must compare and replace the state atomically. The app uses a separate IndexedDB journal
438
+ scoped to the exact local anchor policy and authenticated RN/RNO identity.
439
+
440
+ Current selection rejects known rollbacks, unresolved forks, missing checkpoint, price or live predecessors, and
441
+ unverified epoch changes. Historical inspection allows older coordinates and never advances current heads. Authenticated
442
+ gaps and rejected conflicting originals remain negative knowledge. A rejected gap cannot advance its head by
443
+ resubmitting the same tip. Station snapshots permit revision jumps without claiming commit ancestry. Limits are 4096
444
+ points and 128 conflicts; storage must fail closed rather than silently evict history. These local rules establish
445
+ neither global freshness nor independently authorized witnesses.
446
+
447
+
448
+ ### CPO envelope authority and compact trust succession
449
+
450
+ `verifyChargingDocumentKeys` re-verifies the original station/EVSE proof, exact CPO, document-time validity and the
451
+ explicit `chargeTransparencyRecords` usage (or an unrestricted party key). It returns normalized COSE material for the
452
+ shared Core envelope verifier; a price-only key does not authorize document signing. This is independent of the
453
+ P-256/OCMF meter pilot.
454
+
455
+ A transition names the policy it continues by `trustPolicyId(anchor)`: the SHA-256 of the canonical JSON of `{network,
456
+ holders, minConsensus, urls}`, the URLs as a WHATWG URL serializes them without trailing slashes, in their ordinal order,
457
+ as WWCP_POI3 calculates it. `TrustTransition` and `VerifiedTrustTransition` implement an experimental compact delegation
458
+ certificate, signed by both old authorized and target identity holder quorums, binding exact policies, statements,
459
+ sources and predecessor. It is not History owner/change-set replay and does not misuse `followedBy`.
460
+ `applyVerifiedTrustTransition` preserves negative journal knowledge while adopting explicit policy delegation and exact
461
+ final-checkpoint/genesis/first-checkpoint epoch evidence. Unverified epoch jumps and quorum reductions fail closed.
462
+ `TrustContext.withEpoch` carries verified profiles into subsequent proof checks; historical contexts can carry a
463
+ reverified authority timeline. These objects are recreated from local decisions and original evidence, never serialized
464
+ green flags. See the app's illustrated trust-succession guide and genuine public fixtures.
465
+
466
+ ### Independent price publication receipts
467
+
468
+ `parseWitnessPublicationPolicy` parses separately authorized organization, network, key and clock decisions. Parsing a
469
+ recommendation grants no trust. Pass the accepted policy as `publicationPolicy` to `verifySessionPriceEvidence`; its
470
+ optional `publicationReceipts` and `witnessCandidates` do not authorize themselves. `PricePublicationReceipt` signs a
471
+ versioned public-retrieval assertion for the exact selected price/EVSE/network/checkpoint and public source. The
472
+ experimental key usage is `witnessPublications`; existing usage restrictions still apply.
473
+
474
+ The result `publicationBeforeStart: 'witnessed-before-signed-start'` requires the separate distinct-organization quorum,
475
+ valid original signatures and inclusion, and every counted latest signing bound (including locally accepted clock error)
476
+ strictly before the original signed start. Several keys/aliases of one approved organization count once; reused key
477
+ material cannot create another organization. Known RNO/CPO key material is excluded from independent witnessing. Actual
478
+ clock accuracy and organizational independence remain explicit caller trust assumptions. The physical charging start,
479
+ global latest/sole offer, customer consent and final billing are not independently proven. This is not an RFC 3161
480
+ implementation.
481
+
482
+ `tests/vectors/price-publication.json` contains public Ed25519/ML-DSA-65 vectors and negative timing, scope, signature
483
+ and duplicate-organization cases. The caller must reread current policy across asynchronous work and persist original
484
+ receipts, never serialized green verdicts. No production witness service is implied.
485
+
486
+ Publication receipts additionally bind `priceStatementSHA256` (the complete original CPO-signed object including its
487
+ signature value) and `tariffSHA256` (the exact selected tariff including its currency). The ordinary statement digest
488
+ excludes its signature value and cannot alone prove that a signed offer, rather than its draft, was retrieved.
489
+ `pricePublicationOfferHashes` computes these bindings consistently for generators and callers. Neither alternate
490
+ signature encodings nor changed tariff definitions reuse another original's receipt.
491
+
492
+
493
+ ## Publishing
494
+
495
+ Version 0.3.0 is prepared locally. Preparation does not publish to npm, create a release tag or change a consumer's
496
+ dependency. Published versions are immutable. Validate the source and inspect the package before approving a release:
497
+
498
+ ```bash
499
+ npm ci
500
+ npx playwright install chromium
501
+ npm run verify
502
+ npm run examples
503
+ npm pack --dry-run
504
+ npm pack
505
+ ```
506
+
507
+ After release approval, replace the prepared-release markers in this README and the changelog with the actual release
508
+ status and date, rebuild and check the final archive. Commit the reviewed release files, then tag and push that exact
509
+ commit to each configured remote:
510
+
511
+ ```bash
512
+ git tag -s v0.3.0 -m "v0.3.0"
513
+ git push origin master v0.3.0
514
+ git push git1 master v0.3.0
515
+ git push git2 master v0.3.0
516
+ ```
517
+
518
+ Publish the inspected archive only after that separate approval:
519
+
520
+ ```bash
521
+ npm login
522
+ npm whoami
523
+ npm publish ./open-charging-cloud-wwcp-poi-0.3.0.tgz --access public
524
+ ```
525
+
526
+ After publication, consumers read [Migration](CHANGELOG.md#migration) and move to `@open-charging-cloud/wwcp-poi@^0.3.0`,
527
+ which a range of `^0.2.0` does not take, then refresh their lockfile and verify a clean install.
528
+
529
+
530
+ ## License
531
+
532
+ [GNU Affero General Public License v3.0](LICENSE), as WWCP_POI.
533
+
534
+ Copyright 2014-2026 GraphDefined GmbH