From 1e49b2604028c40d07de6a1462581a00240d6261 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 16 Oct 2026 13:21:51 +0200 Subject: [PATCH] chore(deps)!: repin dpp-core to 0.22.0 --- CHANGELOG.md | 72 +++ Cargo.lock | 72 +-- Cargo.toml | 18 +- .../schemas/identity/DidDocument.yaml | 22 +- .../identity/InternalRotateKeyResponse.yaml | 2 +- .../passport-requests/AmendRequest.yaml | 3 +- .../CreatePassportRequest.yaml | 13 + .../schemas/passport/PassportResponse.yaml | 10 + .../passport/PersonalDataStatement.yaml | 55 +++ .../schemas/passport/PublishReadiness.yaml | 4 +- api/openapi.bundled.json | 92 +++- api/openapi.bundled.yaml | 95 +++- api/openapi.yaml | 2 + cli/src/core/snapshot.rs | 45 +- cli/tests/helpers/mod.rs | 18 +- crates/dpp-dal/src/pg/archiving_repo.rs | 1 + .../passport_docs/battery/v2.8.0.json | 179 ++++++++ .../passport_docs/textile/v1.4.0.json | 94 ++++ .../dpp-dal/tests/passport_column_coverage.rs | 1 + crates/dpp-dal/tests/pg_integration.rs | 3 +- crates/dpp-dal/tests/pg_passport_version.rs | 1 + .../tests/pg_retired_status_migration.rs | 1 + crates/dpp-dal/tests/pg_seal_outbox.rs | 1 + crates/dpp-dal/tests/pg_successor.rs | 1 + .../dpp-identity/src/handlers/rotate_key.rs | 54 ++- crates/dpp-identity/src/handlers/sign.rs | 12 +- crates/dpp-identity/src/handlers/verify.rs | 4 +- crates/dpp-identity/src/state.rs | 25 + .../src/domain/battery_template.rs | 26 +- .../src/domain/validate/aluminium.rs | 1 + .../src/domain/validate/battery.rs | 8 +- .../src/domain/validate/construction.rs | 1 + .../src/domain/validate/furniture.rs | 1 + .../src/domain/validate/mattress.rs | 1 + .../src/domain/validate/steel.rs | 1 + .../src/domain/validate/textile.rs | 1 + .../dpp-integrator/src/domain/validate/toy.rs | 1 + .../src/domain/validate/tyre.rs | 1 + .../dpp-integrator/src/infra/vault_client.rs | 1 + crates/dpp-node/src/infra/registry/client.rs | 56 +++ .../dpp-node/src/infra/registry/dashboard.rs | 318 +++++++++++++ crates/dpp-node/src/infra/registry/mapping.rs | 324 +++++++------ crates/dpp-node/src/infra/registry/mod.rs | 1 + crates/dpp-node/src/infra/registry/tests.rs | 432 +++++++++++++++--- crates/dpp-node/src/infra/registry_drain.rs | 145 ++++-- crates/dpp-node/src/infra/ruleset.rs | 9 +- crates/dpp-node/src/infra/transfer_drain.rs | 98 +++- .../create_passport_request_is_one_type.rs | 1 + crates/dpp-node/tests/openapi_contract.rs | 102 ++++- crates/dpp-node/tests/registry_outbox.rs | 254 ++++++---- crates/dpp-node/tests/s3_backup.rs | 1 + crates/dpp-node/tests/seal_outbox.rs | 1 + crates/dpp-node/tests/smoke.rs | 142 +++++- crates/dpp-node/tests/snapshot_outbox.rs | 1 + crates/dpp-node/tests/snapshot_static_tier.rs | 17 +- crates/dpp-node/tests/transfer_outbox.rs | 1 + .../tests/fixtures/abi-echo/Cargo.toml | 2 +- crates/dpp-plugin-host/tests/integration.rs | 2 +- .../dpp-resolver/src/handlers/resolve_json.rs | 1 + crates/dpp-resolver/src/infra/did.rs | 26 +- .../src/jws_verification_tests.rs | 2 +- crates/dpp-resolver/tests/resolver_e2e.rs | 2 +- crates/dpp-types/src/passport_request.rs | 22 +- .../examples/generate_demo_dossiers.rs | 29 +- crates/dpp-vault/src/api/passport_response.rs | 14 +- .../dpp-vault/src/domain/compliance/tests.rs | 2 +- crates/dpp-vault/src/domain/service/amend.rs | 3 +- crates/dpp-vault/src/domain/service/create.rs | 147 +++++- crates/dpp-vault/src/domain/service/mod.rs | 1 + .../dpp-vault/src/domain/service/publish.rs | 3 + crates/dpp-vault/src/domain/service/query.rs | 1 + crates/dpp-vault/src/domain/verify/engine.rs | 61 ++- crates/dpp-vault/src/domain/verify/jws.rs | 52 +-- .../src/domain/verify/transfer_chain.rs | 57 ++- crates/dpp-vault/src/handlers/create.rs | 11 +- crates/dpp-vault/src/infra/identity_client.rs | 2 +- crates/dpp-vault/src/infra/status_list.rs | 1 + crates/dpp-vault/src/middleware/credential.rs | 111 +++-- crates/dpp-vault/src/public_view.rs | 1 + crates/dpp-vault/tests/audience_read_route.rs | 4 +- crates/dpp-vault/tests/continuity_snapshot.rs | 15 +- crates/dpp-vault/tests/evidence_dossier.rs | 1 + crates/dpp-vault/tests/seal_repair_route.rs | 1 + crates/dpp-vault/tests/seal_route.rs | 1 + docs/ops/PRODUCTION-RUNBOOK.md | 2 +- ops/demo/dossiers/01-valid-simple.json | 70 ++- ops/demo/dossiers/02-valid-with-transfer.json | 98 ++-- ops/demo/dossiers/03-valid-with-eol.json | 74 ++- .../dossiers/04-valid-full-lifecycle.json | 102 +++-- ops/demo/dossiers/05-tampered-signature.json | 102 +++-- .../dossiers/06-tampered-audit-entry.json | 102 +++-- .../07-tampered-transfer-signature.json | 102 +++-- .../dossiers/08-hidden-field-injection.json | 102 +++-- .../dossiers/09-unknown-top-level-field.json | 102 +++-- .../dossiers/11-canonicalisation-vectors.json | 22 +- ops/demo/dossiers/expected.json | 2 +- .../passports/01-battery-industrial-nmc.json | 5 - .../passports/02-battery-industrial-lfp.json | 5 - .../03-battery-industrial-lead-acid.json | 5 - ops/demo/passports/04-battery-ev-nmc.json | 5 - ops/demo/passports/05-battery-lmt-lfp.json | 5 - ops/demo/passports/README.md | 2 +- 102 files changed, 3209 insertions(+), 1119 deletions(-) create mode 100644 api/components/schemas/passport/PersonalDataStatement.yaml create mode 100644 crates/dpp-dal/tests/fixtures/passport_docs/battery/v2.8.0.json create mode 100644 crates/dpp-dal/tests/fixtures/passport_docs/textile/v1.4.0.json create mode 100644 crates/dpp-node/src/infra/registry/dashboard.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 31ce1393..a0118484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,62 @@ under the pre-1.0 conventions in [VERSIONING.md](docs/governance/VERSIONING.md): ### Breaking +- **`dpp-core` 0.22.0: a key is named by its own thumbprint, and a token + signed before this release no longer verifies.** Verification methods were + named by position, so after a rotation `#key-1` named a different key. They are + now named by the key's RFC 7638 thumbprint in RFC 9278 URI form, and every + signature carries the absolute URL of the method that made it as its `kid`. + The DID document is `JsonWebKey` with the Controlled Identifiers context, and + a token that names no key is refused rather than checked against a default. + + **Migration:** sign again anything signed before the upgrade. That covers + passport and snapshot proofs, access credentials and signed ruleset bundles. + `POST /internal/keys/rotate` now answers `new_key_id` as + `did:web:…#urn:ietf:params:oauth:jwk-thumbprint:sha-256:…` instead of + `…#key-1`, and a reader of the DID document finds the current key at + `authentication[0]`. The demo dossiers are + regenerated. + +- **`dpp-core` 0.22.0: an access credential is typed as a VC-JWT.** The + `X-DPP-Credential` verifier now goes through core's + `authenticate_access_credential`, which requires `typ: vc+jwt` and `cty: vc` + and binds the credential to the issuer document it was checked against. A + signature of the issuer's key that is not typed as a credential, such as a + passport proof, no longer passes for one. Credentials issued before this + release must be issued again. + +- **`dpp-core` 0.22.0: a submission to the EU registry is acknowledged with a + correlation identifier, and the registration identifier comes later.** The + registry accepts a submission for checking and rules on it afterwards, and + only a successful ruling generates the registration identifier (IR (EU) + 2026/1778 Art. 8(7) and 8(8)). The adapter used to treat the answer to the + `POST` as a registration and polled by this node's passport id, a value the + registry never received. `register` now returns the correlation identifier, + and `check_submission` finds the submission's task on the registry's + dashboard and reads its result; `check_status` and `notify_transfer` take the + registration identifier. + + The outbox row's `registry_id` holds the correlation identifier while the row + is `submitted` and the registration identifier once it is `registered`, so a + transfer notification waits for `registered` and never sends a correlation + identifier as a record id. A submission the registry already holds, answered + to a replayed idempotency key, carries no handle: the row stays `submitted` + and is not polled until the identifier is recovered. A state the node has no + name for keeps the poll going and is never read as a refusal. The drain no + longer reports a `deactivated` outcome, because a submission's ruling is not + a record's state. + + An item-level registration is made under the serial the carrier prints, and + carries its batch, where it used to name the item by the passport's own id. + +- **`dpp-core` 0.22.0: the battery rules follow the Commission's guidance + v2.0.** `co2ePerUnitKg` is optional, and it and the four recycled-content + shares are not applicable to electric-vehicle, LMT and industrial batteries. + The battery import template stops asking for them, `co2ePerUnitKg` is an + optional import column, the demo passports and the demo dossier drop them, + and the mandatory-field counts quoted in the API description are now 41, 40 + and 33. + - **`POST /dpp/{dppId}/archive` is now `POST /dpp/{dppId}/retire`, the status it sets is `"retired"`, and the event it emits is `dpp.passport.retired`.** *(Breaking across six surfaces: the route, the `retireDpp` operationId, the @@ -159,6 +215,22 @@ under the pre-1.0 conventions in [VERSIONING.md](docs/governance/VERSIONING.md): ### Added +- **A field that can hold personal data is refused on the write that would store + it unless the operator says what is held.** ESPR Art. 10(1)(e), Reg. (EU) + 2025/2509 Art. 20(10) and Reg. (EU) 2026/405 Art. 22(h) forbid storing customer + personal data in a passport without consent, and a draft is stored. Create, + every draft update and the draft an amendment builds now run core's + `check_personal_data` before the write and answer `422` naming the field. + `personalData` is accepted on create, patchable on a draft through + `PUT /dpp/{dppId}` and `amend`, served back on the authenticated read, and + frozen at publish. The schema marks the fields: battery + `usageHistory.negativeEvents` and `usageHistory.operatingConditions[].note`, + and textile `repairHistoryUrl`. This node never reads what a field says. + + Not yet done: the erasable store for the records a `held: outside` statement + names, and the route that erases one. Until they exist an operator keeps + those records elsewhere and names them. + - **The GS1 resolver accepts a consumer product variant (AI 22) and a third-party extension (AI 235).** A label printed elsewhere may carry either after its GTIN, and the resolver answered it with the router's own `404` diff --git a/Cargo.lock b/Cargo.lock index 6bd81b6d..922e5a9b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2251,9 +2251,9 @@ checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" [[package]] name = "dpp-aas" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43325f6a27f419e609b7b685bbd28e2720df492f2a2ce06af0d49bc5335ed061" +checksum = "4c0957eeb9edaee056135bed203e98207d381630ce752135be38e1c4cd5bf0f5" dependencies = [ "dpp-domain", "dpp-vocab", @@ -2263,9 +2263,9 @@ dependencies = [ [[package]] name = "dpp-calc" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942b52b625c4c21d59b7345bb21d2cb29b8f94861af72bfeb8523802b5454c" +checksum = "92a76a50d0c01e3d9bf051c22cf21eb9e75aa62157c1957a78c3681898e001e8" dependencies = [ "chrono", "dpp-rules", @@ -2336,9 +2336,9 @@ dependencies = [ [[package]] name = "dpp-crypto" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03baf48e4f3cfe278fa66a4255453d9cb37ba712625dd36066a674023e8d31d" +checksum = "9ebeac7dee3982e5c2b12b36f96c10315c137857acb3ce5df0f8aad51fdaa3da" dependencies = [ "aes-gcm", "anyhow", @@ -2388,9 +2388,9 @@ dependencies = [ [[package]] name = "dpp-digital-link" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2711c198dc82c9e811faf62d6c10bbcb8a34ac7b28d1560f6de4ff8176413360" +checksum = "2362fbca86fc9895bba086a94cc4fb7e7db7972b673d59733e35c8fe1b636920" dependencies = [ "dpp-domain", "dpp-rules", @@ -2401,9 +2401,9 @@ dependencies = [ [[package]] name = "dpp-domain" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e734f800903d6c3c0c082417db1631804edbb98bef1e06aa82dba349af5afad" +checksum = "b770df4f4835914af417fca35a8d5c804c5ee144fd0d25e9c26768040e3afe17" dependencies = [ "async-trait", "chrono", @@ -2579,9 +2579,9 @@ dependencies = [ [[package]] name = "dpp-plugin-traits" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5daa6ef28838ecfe28947c421bf0179490a339bb04e6bee7f14968d2b41f43f5" +checksum = "6f767576abb9e08c1bede58d2a0a874e6ac79abb9714ca9f900e53bef33b54c9" dependencies = [ "semver", "serde", @@ -2591,9 +2591,9 @@ dependencies = [ [[package]] name = "dpp-registry" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a7932bd8822666c9ac41835b8c25468d839d362f8af5c4812397128a6cb6579" +checksum = "a024fb2a4c3a6f5718deb9f123315ffadfbba6e6cabe709f521554578e50ef11" dependencies = [ "chrono", "dpp-domain", @@ -2656,9 +2656,9 @@ dependencies = [ [[package]] name = "dpp-rules" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58b7b0e4ed097b1ada1bd2111edc1ef6efa5c955e1092a6982b4c2a7eaa458f7" +checksum = "7c3b8ed43e1ccf5406630ef0a21a382405123006a2c3463d10971c1244b42348" dependencies = [ "base64 0.23.1", "chrono", @@ -2769,9 +2769,9 @@ dependencies = [ [[package]] name = "dpp-vc" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a4633b0cdc7eaeb0bb0a18550b95f048c75e818a37e6cef5621c7d0e411560" +checksum = "aa7a2b467cbf6f3a657465a105dd5f84f20d8a294424c0ab879ed05741991669" dependencies = [ "anyhow", "async-trait", @@ -2792,9 +2792,9 @@ dependencies = [ [[package]] name = "dpp-vocab" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "615dc0cb2142d30c32fcc3a477c19960ce2be0d19b2f9afc169386b9039c2239" +checksum = "b6e9629ed973689c67a3153e65db810e34c3845655b6a3a41da10506d728cb50" dependencies = [ "serde", "serde_json", @@ -3061,7 +3061,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4043,7 +4043,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20fd6de4ccfcc187e38bc21cfa543cb5a302cb86a8b114eb7f0bf0dc9f8ac00f" dependencies = [ "io-lifetimes 3.0.1", - "windows-sys 0.52.0", + "windows-sys 0.60.2", ] [[package]] @@ -4166,9 +4166,9 @@ dependencies = [ [[package]] name = "jsonschema" -version = "0.56.0" +version = "0.58.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6a806f80c1f5560431009ce5ec29b59d38f950e0cd7db5f1b8925e6d8104e21" +checksum = "f622c574a29fb3034a0a90645afee194dfedc75fc124228801b6e77041075bd1" dependencies = [ "ahash", "bytecount", @@ -4195,18 +4195,18 @@ dependencies = [ [[package]] name = "jsonschema-regex" -version = "0.56.0" +version = "0.58.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb862addfa7782108933abcf842fe25334523a9df5b89ea4d9620e3dd7b42181" +checksum = "a968fffd387b2ab005bf00c1851f5d26e90df05b6f7ad609ed0d620033cca5e7" dependencies = [ "regex-syntax", ] [[package]] name = "jsonschema-value" -version = "0.56.0" +version = "0.58.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a05cd404c5ff6e2731dbbf7e290a27417750acfb59e329560a1d0af384c93fb0" +checksum = "39b7670345e5ce87371339d13705dbb9276b7e3e934bbe1efa2c6734a1f65a96" dependencies = [ "ahash", "bytecount", @@ -5347,7 +5347,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.119", @@ -5494,7 +5494,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5748,9 +5748,9 @@ dependencies = [ [[package]] name = "referencing" -version = "0.56.0" +version = "0.58.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3b4a92fac7e28c27de3ad26df2ecb9e652f227b258e845af034052e5b22c96c" +checksum = "558f3855529c0a7e78a48e409fa653456e55f1b167eab4109c16a255b7727e3f" dependencies = [ "ahash", "fluent-uri", @@ -5993,7 +5993,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6062,7 +6062,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -7013,7 +7013,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -8375,7 +8375,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c40d4259..f5f6fe57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,20 +52,20 @@ debug = "line-tables-only" # Core crates — consumed from crates.io (dpp-core is published independently; # see the pins below for the version actually in use, and README.md's sync # note for the other places that must agree on a bump). -dpp-domain = "0.21.0" +dpp-domain = "0.22.0" # Engine-wide types (tenant, auth, audit, API keys) — engine-internal crate. dpp-types = { path = "crates/dpp-types" } -dpp-crypto = "0.21.0" -dpp-vc = "0.21.0" +dpp-crypto = "0.22.0" +dpp-vc = "0.22.0" # KeyStore lives in dpp-core/dpp-crypto; the identity service crate is dpp-identity # (dir: crates/dpp-identity, pkg: dpp-identity). -dpp-digital-link = "0.21.0" -dpp-aas = "0.21.0" -dpp-plugin-traits = "0.21.0" -dpp-registry = "0.21.0" -dpp-calc = "0.21.0" -dpp-rules = { version = "0.21.0", features = ["bundle"] } +dpp-digital-link = "0.22.0" +dpp-aas = "0.22.0" +dpp-plugin-traits = "0.22.0" +dpp-registry = "0.22.0" +dpp-calc = "0.22.0" +dpp-rules = { version = "0.22.0", features = ["bundle"] } # Web framework axum = { version = "0.8", features = ["macros", "multipart"] } diff --git a/api/components/schemas/identity/DidDocument.yaml b/api/components/schemas/identity/DidDocument.yaml index 2d80b424..358cf80b 100644 --- a/api/components/schemas/identity/DidDocument.yaml +++ b/api/components/schemas/identity/DidDocument.yaml @@ -1,22 +1,30 @@ type: object description: | A W3C DID Core `did:web` document. Lists the operator's Ed25519 - verification methods; rotated keys are retained (so historical - signatures still verify) and revoked keys are omitted. + verification methods as `JsonWebKey`s (W3C Controlled Identifiers v1.0); + rotated keys are retained (so historical signatures still verify) and revoked + keys are omitted. + + Each method is named by its key's own RFC 7638 thumbprint, in the URI form of + RFC 9278, so a name never moves to another key when the keys are rotated. A + token's `kid` is the absolute URL of the method that signed it. The first + entry of `authentication` is the current key. example: '@context': - https://www.w3.org/ns/did/v1 - - https://w3id.org/security/suites/ed25519-2020/v1 + - https://www.w3.org/ns/cid/v1 id: did:web:id.odal-node.io verificationMethod: - - id: did:web:id.odal-node.io#key-1 - type: JsonWebKey2020 + - id: did:web:id.odal-node.io#urn:ietf:params:oauth:jwk-thumbprint:sha-256: + type: JsonWebKey controller: did:web:id.odal-node.io publicKeyJwk: kty: OKP crv: Ed25519 x: + kid: urn:ietf:params:oauth:jwk-thumbprint:sha-256: + alg: EdDSA authentication: - - did:web:id.odal-node.io#key-1 + - did:web:id.odal-node.io#urn:ietf:params:oauth:jwk-thumbprint:sha-256: assertionMethod: - - did:web:id.odal-node.io#key-1 + - did:web:id.odal-node.io#urn:ietf:params:oauth:jwk-thumbprint:sha-256: diff --git a/api/components/schemas/identity/InternalRotateKeyResponse.yaml b/api/components/schemas/identity/InternalRotateKeyResponse.yaml index 247e1e7c..1d9a4cdf 100644 --- a/api/components/schemas/identity/InternalRotateKeyResponse.yaml +++ b/api/components/schemas/identity/InternalRotateKeyResponse.yaml @@ -10,7 +10,7 @@ properties: type: string new_key_id: type: string - example: did:web:id.odal-node.io#key-1 + example: did:web:id.odal-node.io#urn:ietf:params:oauth:jwk-thumbprint:sha-256: fingerprint: type: string description: SHA-256 fingerprint (hex) of the new public key. diff --git a/api/components/schemas/passport-requests/AmendRequest.yaml b/api/components/schemas/passport-requests/AmendRequest.yaml index 7f16ad71..e9685a78 100644 --- a/api/components/schemas/passport-requests/AmendRequest.yaml +++ b/api/components/schemas/passport-requests/AmendRequest.yaml @@ -15,7 +15,8 @@ properties: description: | The correction, in the same shape the draft-update body takes: only the patchable content fields (`productName`, `co2ePerUnit`, - `repairabilityScore`, `productGroupData`, `componentRefs`) take effect. + `repairabilityScore`, `productGroupData`, `componentRefs`, `personalData`) take + effect. Other keys are ignored rather than refused, so a client that sends a full create-shaped body still works and still cannot make a create-time field diff --git a/api/components/schemas/passport-requests/CreatePassportRequest.yaml b/api/components/schemas/passport-requests/CreatePassportRequest.yaml index d3d4fa70..601af39a 100644 --- a/api/components/schemas/passport-requests/CreatePassportRequest.yaml +++ b/api/components/schemas/passport-requests/CreatePassportRequest.yaml @@ -130,6 +130,19 @@ properties: rather than *not applicable*, and publish does not refuse it — the lint result says so instead, because a default would put a claim about a unit onto a record that is about to be signed. + personalData: + type: object + additionalProperties: + $ref: ../passport/PersonalDataStatement.yaml + description: >- + What the operator states about personal data, keyed by the dotted path of + each field of `productGroupData` the product group schema marks as able to + hold it (for example `usageHistory.negativeEvents`). A marked field with a + value is refused with a `422` naming it, on create and on every later write + to the draft, unless it has an entry here. A statement about a field that is + not marked, or an `outside` statement with a blank record, is refused too. + Patchable on a draft, frozen at publish, and covered by the operator's + signature. The personal data itself never goes in the passport. componentRefs: type: array items: diff --git a/api/components/schemas/passport/PassportResponse.yaml b/api/components/schemas/passport/PassportResponse.yaml index a8a3e748..b4f3e59d 100644 --- a/api/components/schemas/passport/PassportResponse.yaml +++ b/api/components/schemas/passport/PassportResponse.yaml @@ -228,6 +228,16 @@ properties: so one second-life unit may derive from several predecessors. items: $ref: ./DerivationRef.yaml + personalData: + type: object + additionalProperties: + $ref: ./PersonalDataStatement.yaml + description: >- + What the operator states about personal data in the marked fields of + `productGroupData`, keyed by dotted field path. Absent when it states + nothing. On a **redacted read** the key is absent: a statement is shown + only to an audience that sees the field it speaks about, and never to the + public. componentRefs: type: array description: >- diff --git a/api/components/schemas/passport/PersonalDataStatement.yaml b/api/components/schemas/passport/PersonalDataStatement.yaml new file mode 100644 index 00000000..485b0fd0 --- /dev/null +++ b/api/components/schemas/passport/PersonalDataStatement.yaml @@ -0,0 +1,55 @@ +description: |- + What the operator states about the personal data related to one field of + `productGroupData` that the product group's schema marks as able to hold it + (`x-personal-data`). Either nothing related is held, or it is held outside the + passport in an erasable record. + + Every statement asserts that the field's value carries no personal data beyond + what the governing act requires the passport to carry. The personal data + itself is never in the passport: a published passport is signed, frozen, + archived and copied to a back-up provider, so it cannot be erased from, and + withdrawn consent (GDPR Art. 7(3)) obliges erasure (Art. 17(1)(b)). + + Where a governing act admits customer personal data only with explicit consent + (ESPR Art. 10(1)(e), Reg. (EU) 2025/2509 Art. 20(10), Reg. (EU) 2026/405 + Art. 22(h)), a basis other than `consent` is refused. The record named by + `record` is not served by any route of this node. +oneOf: + - type: object + title: Nothing held + required: + - held + properties: + held: + type: string + enum: + - nothing + additionalProperties: false + - type: object + title: Held outside the passport + required: + - held + - lawfulBasis + - record + properties: + held: + type: string + enum: + - outside + lawfulBasis: + type: string + description: The ground of GDPR Art. 6(1) the data is held on. + enum: + - consent + - contract + - legalObligation + - vitalInterests + - publicTask + - legitimateInterests + record: + type: string + minLength: 1 + description: >- + The identifier of the erasable record holding the data. Required, and + not blank. + additionalProperties: false diff --git a/api/components/schemas/passport/PublishReadiness.yaml b/api/components/schemas/passport/PublishReadiness.yaml index 243f96f1..ac52e0e4 100644 --- a/api/components/schemas/passport/PublishReadiness.yaml +++ b/api/components/schemas/passport/PublishReadiness.yaml @@ -10,8 +10,8 @@ description: | gates pass", not "publish will certainly succeed". The mandatory-content half is the reason this exists. It is the gate that most - often refuses a battery — 45 data points for an electric-vehicle one, 44 for - an LMT battery, 37 for an industrial one — and it could not previously be + often refuses a battery — 41 data points for an electric-vehicle one, 40 for + an LMT battery, 33 for an industrial one — and it could not previously be previewed anywhere: it is reachable only from a stored passport, and the only dry-run the API offered takes an unsaved request body. required: diff --git a/api/openapi.bundled.json b/api/openapi.bundled.json index a8cbb1ca..01995bf5 100644 --- a/api/openapi.bundled.json +++ b/api/openapi.bundled.json @@ -5561,6 +5561,13 @@ "$ref": "#/components/schemas/DerivationRef" } }, + "personalData": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/PersonalDataStatement" + }, + "description": "What the operator states about personal data in the marked fields of `productGroupData`, keyed by dotted field path. Absent when it states nothing. On a **redacted read** the key is absent: a statement is shown only to an audience that sees the field it speaks about, and never to the public." + }, "componentRefs": { "type": "array", "description": "Cross-operator references to the constituent passports this product is assembled from — its bill of materials, each qualified by how much and in what role. The inverse edge of `derivedFrom`: these point down to the constituents, those point up to the predecessors.\nDistinct from `materials`, which lists substances by weight. These point at other products that have passports of their own.", @@ -5899,6 +5906,62 @@ } } }, + "PersonalDataStatement": { + "description": "What the operator states about the personal data related to one field of\n`productGroupData` that the product group's schema marks as able to hold it\n(`x-personal-data`). Either nothing related is held, or it is held outside the\npassport in an erasable record.\n\nEvery statement asserts that the field's value carries no personal data beyond\nwhat the governing act requires the passport to carry. The personal data\nitself is never in the passport: a published passport is signed, frozen,\narchived and copied to a back-up provider, so it cannot be erased from, and\nwithdrawn consent (GDPR Art. 7(3)) obliges erasure (Art. 17(1)(b)).\n\nWhere a governing act admits customer personal data only with explicit consent\n(ESPR Art. 10(1)(e), Reg. (EU) 2025/2509 Art. 20(10), Reg. (EU) 2026/405\nArt. 22(h)), a basis other than `consent` is refused. The record named by\n`record` is not served by any route of this node.", + "oneOf": [ + { + "type": "object", + "title": "Nothing held", + "required": [ + "held" + ], + "properties": { + "held": { + "type": "string", + "enum": [ + "nothing" + ] + } + }, + "additionalProperties": false + }, + { + "type": "object", + "title": "Held outside the passport", + "required": [ + "held", + "lawfulBasis", + "record" + ], + "properties": { + "held": { + "type": "string", + "enum": [ + "outside" + ] + }, + "lawfulBasis": { + "type": "string", + "description": "The ground of GDPR Art. 6(1) the data is held on.", + "enum": [ + "consent", + "contract", + "legalObligation", + "vitalInterests", + "publicTask", + "legitimateInterests" + ] + }, + "record": { + "type": "string", + "minLength": 1, + "description": "The identifier of the erasable record holding the data. Required, and not blank." + } + }, + "additionalProperties": false + } + ] + }, "LifeStatus": { "type": "string", "description": "Where a unit sits in its product life — original, or the second-life operation that produced it, or waste. Regulation (EU) 2023/1542, Annex XIII point 4(c).\nOrthogonal to `status`, which is the passport record's own lifecycle (draft/published/…). A published passport can be `waste`; a draft cannot be `superseded`.\nNot derived from `derivedFrom`. Art. 77(7) permits several predecessors with nothing forcing them to share an operation, so a unit built from one repurposed and one remanufactured predecessor has no unambiguous derived status. The claim is stored and then checked: at least one derivation edge must support it.", @@ -6328,6 +6391,13 @@ ], "description": "Where this unit sits in its **product** life — Annex XIII point 4(c) of Regulation (EU) 2023/1542.\n\nOrthogonal to `status`, which is the *publication* lifecycle: a `repurposed` unit's passport is `active` like any other. Omit it for any product group but battery — only Reg. (EU) 2023/1542 defines this vocabulary, and a textile passport asserting `original` would be borrowing a battery term for a question its own instrument does not ask. Sending it for another product group is refused.\n\n**This is the only way to set it.** `lifeStatus` is protected from patching, because it is part of a body that gets signed and rewriting it after publication would change what a proof covers.\n\n🚨 **`waste` is refused here.** The other four describe how a unit came to be, and Art. 77(7) makes each operation produce a *new* passport — so a repurposed unit is created as `repurposed`. `waste` is the one value that happens to a record which continues, and under that article's second subparagraph it moves responsibility as well, so it belongs to a versioning event on the passport becoming waste rather than to the creation of a new one.\n\nOmitting it is lawful everywhere. For a battery it reads as *not stated* rather than *not applicable*, and publish does not refuse it — the lint result says so instead, because a default would put a claim about a unit onto a record that is about to be signed." }, + "personalData": { + "type": "object", + "additionalProperties": { + "$ref": "#/components/schemas/PersonalDataStatement" + }, + "description": "What the operator states about personal data, keyed by the dotted path of each field of `productGroupData` the product group schema marks as able to hold it (for example `usageHistory.negativeEvents`). A marked field with a value is refused with a `422` naming it, on create and on every later write to the draft, unless it has an entry here. A statement about a field that is not marked, or an `outside` statement with a blank record, is refused too. Patchable on a draft, frozen at publish, and covered by the operator's signature. The personal data itself never goes in the passport." + }, "componentRefs": { "type": "array", "items": { @@ -8968,30 +9038,32 @@ }, "DidDocument": { "type": "object", - "description": "A W3C DID Core `did:web` document. Lists the operator's Ed25519\nverification methods; rotated keys are retained (so historical\nsignatures still verify) and revoked keys are omitted.\n", + "description": "A W3C DID Core `did:web` document. Lists the operator's Ed25519\nverification methods as `JsonWebKey`s (W3C Controlled Identifiers v1.0);\nrotated keys are retained (so historical signatures still verify) and revoked\nkeys are omitted.\n\nEach method is named by its key's own RFC 7638 thumbprint, in the URI form of\nRFC 9278, so a name never moves to another key when the keys are rotated. A\ntoken's `kid` is the absolute URL of the method that signed it. The first\nentry of `authentication` is the current key.\n", "example": { "@context": [ "https://www.w3.org/ns/did/v1", - "https://w3id.org/security/suites/ed25519-2020/v1" + "https://www.w3.org/ns/cid/v1" ], "id": "did:web:id.odal-node.io", "verificationMethod": [ { - "id": "did:web:id.odal-node.io#key-1", - "type": "JsonWebKey2020", + "id": "did:web:id.odal-node.io#urn:ietf:params:oauth:jwk-thumbprint:sha-256:", + "type": "JsonWebKey", "controller": "did:web:id.odal-node.io", "publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", - "x": "" + "x": "", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:", + "alg": "EdDSA" } } ], "authentication": [ - "did:web:id.odal-node.io#key-1" + "did:web:id.odal-node.io#urn:ietf:params:oauth:jwk-thumbprint:sha-256:" ], "assertionMethod": [ - "did:web:id.odal-node.io#key-1" + "did:web:id.odal-node.io#urn:ietf:params:oauth:jwk-thumbprint:sha-256:" ] } }, @@ -9093,7 +9165,7 @@ }, "new_key_id": { "type": "string", - "example": "did:web:id.odal-node.io#key-1" + "example": "did:web:id.odal-node.io#urn:ietf:params:oauth:jwk-thumbprint:sha-256:" }, "fingerprint": { "type": "string", @@ -9603,7 +9675,7 @@ }, "PublishReadiness": { "type": "object", - "description": "Whether the passport would clear the publish gates that can be answered\nwithout attempting the transition: the category **mandatory-content** gate and\nthe product-group data/schema gates.\n\nDeliberately not the whole of publish. The registry-identity requirement is\noperator state rather than passport state, and the binding-compliance gate\nneeds a determination this route does not run — so `ready: true` means \"these\ngates pass\", not \"publish will certainly succeed\".\n\nThe mandatory-content half is the reason this exists. It is the gate that most\noften refuses a battery — 45 data points for an electric-vehicle one, 44 for\nan LMT battery, 37 for an industrial one — and it could not previously be\npreviewed anywhere: it is reachable only from a stored passport, and the only\ndry-run the API offered takes an unsaved request body.\n", + "description": "Whether the passport would clear the publish gates that can be answered\nwithout attempting the transition: the category **mandatory-content** gate and\nthe product-group data/schema gates.\n\nDeliberately not the whole of publish. The registry-identity requirement is\noperator state rather than passport state, and the binding-compliance gate\nneeds a determination this route does not run — so `ready: true` means \"these\ngates pass\", not \"publish will certainly succeed\".\n\nThe mandatory-content half is the reason this exists. It is the gate that most\noften refuses a battery — 41 data points for an electric-vehicle one, 40 for\nan LMT battery, 33 for an industrial one — and it could not previously be\npreviewed anywhere: it is reachable only from a stored passport, and the only\ndry-run the API offered takes an unsaved request body.\n", "required": [ "ready", "blockers", @@ -10150,7 +10222,7 @@ "properties": { "patch": { "type": "object", - "description": "The correction, in the same shape the draft-update body takes: only the\npatchable content fields (`productName`, `co2ePerUnit`,\n`repairabilityScore`, `productGroupData`, `componentRefs`) take effect.\n\nOther keys are ignored rather than refused, so a client that sends a full\ncreate-shaped body still works and still cannot make a create-time field\ntake effect. Fields fixed at issuance — the product group, the schema\nversion, the manufacturer, the lineage edges — are not amendable; a\npassport whose product group is wrong is not the same product.\n\n`carrierSerial` is fixed at issuance too, and is the one such key that is\nrefused rather than ignored when it differs from the serial the passport\nprints, with `422` naming `/carrierSerial`. It is what the label carries,\nand the successor keeps the predecessor's so that the label stays the\nsame. The same value is accepted.\n", + "description": "The correction, in the same shape the draft-update body takes: only the\npatchable content fields (`productName`, `co2ePerUnit`,\n`repairabilityScore`, `productGroupData`, `componentRefs`, `personalData`) take\neffect.\n\nOther keys are ignored rather than refused, so a client that sends a full\ncreate-shaped body still works and still cannot make a create-time field\ntake effect. Fields fixed at issuance — the product group, the schema\nversion, the manufacturer, the lineage edges — are not amendable; a\npassport whose product group is wrong is not the same product.\n\n`carrierSerial` is fixed at issuance too, and is the one such key that is\nrefused rather than ignored when it differs from the serial the passport\nprints, with `422` naming `/carrierSerial`. It is what the label carries,\nand the successor keeps the predecessor's so that the label stays the\nsame. The same value is accepted.\n", "additionalProperties": true, "example": { "productName": "Model X Battery Pack (rev B)", diff --git a/api/openapi.bundled.yaml b/api/openapi.bundled.yaml index 7d143bf0..70a6d9ff 100644 --- a/api/openapi.bundled.yaml +++ b/api/openapi.bundled.yaml @@ -4635,6 +4635,11 @@ components: A list rather than the single `parentPassportRef` it replaces: Regulation (EU) 2023/1542 Art. 77(7) is plural on both sides — "linked to the battery passport or passports of the original battery or batteries" — so one second-life unit may derive from several predecessors. items: $ref: '#/components/schemas/DerivationRef' + personalData: + type: object + additionalProperties: + $ref: '#/components/schemas/PersonalDataStatement' + description: 'What the operator states about personal data in the marked fields of `productGroupData`, keyed by dotted field path. Absent when it states nothing. On a **redacted read** the key is absent: a statement is shown only to an audience that sees the field it speaks about, and never to the public.' componentRefs: type: array description: |- @@ -4939,6 +4944,60 @@ components: description: Where to fetch the predecessor's passport, and the hash pinning its signed public view. operation: $ref: '#/components/schemas/SecondLifeOperation' + PersonalDataStatement: + description: |- + What the operator states about the personal data related to one field of + `productGroupData` that the product group's schema marks as able to hold it + (`x-personal-data`). Either nothing related is held, or it is held outside the + passport in an erasable record. + + Every statement asserts that the field's value carries no personal data beyond + what the governing act requires the passport to carry. The personal data + itself is never in the passport: a published passport is signed, frozen, + archived and copied to a back-up provider, so it cannot be erased from, and + withdrawn consent (GDPR Art. 7(3)) obliges erasure (Art. 17(1)(b)). + + Where a governing act admits customer personal data only with explicit consent + (ESPR Art. 10(1)(e), Reg. (EU) 2025/2509 Art. 20(10), Reg. (EU) 2026/405 + Art. 22(h)), a basis other than `consent` is refused. The record named by + `record` is not served by any route of this node. + oneOf: + - type: object + title: Nothing held + required: + - held + properties: + held: + type: string + enum: + - nothing + additionalProperties: false + - type: object + title: Held outside the passport + required: + - held + - lawfulBasis + - record + properties: + held: + type: string + enum: + - outside + lawfulBasis: + type: string + description: The ground of GDPR Art. 6(1) the data is held on. + enum: + - consent + - contract + - legalObligation + - vitalInterests + - publicTask + - legitimateInterests + record: + type: string + minLength: 1 + description: The identifier of the erasable record holding the data. Required, and not blank. + additionalProperties: false LifeStatus: type: string description: |- @@ -5320,6 +5379,11 @@ components: 🚨 **`waste` is refused here.** The other four describe how a unit came to be, and Art. 77(7) makes each operation produce a *new* passport — so a repurposed unit is created as `repurposed`. `waste` is the one value that happens to a record which continues, and under that article's second subparagraph it moves responsibility as well, so it belongs to a versioning event on the passport becoming waste rather than to the creation of a new one. Omitting it is lawful everywhere. For a battery it reads as *not stated* rather than *not applicable*, and publish does not refuse it — the lint result says so instead, because a default would put a claim about a unit onto a record that is about to be signed. + personalData: + type: object + additionalProperties: + $ref: '#/components/schemas/PersonalDataStatement' + description: What the operator states about personal data, keyed by the dotted path of each field of `productGroupData` the product group schema marks as able to hold it (for example `usageHistory.negativeEvents`). A marked field with a value is refused with a `422` naming it, on create and on every later write to the draft, unless it has an entry here. A statement about a field that is not marked, or an `outside` statement with a blank record, is refused too. Patchable on a draft, frozen at publish, and covered by the operator's signature. The personal data itself never goes in the passport. componentRefs: type: array items: @@ -7553,25 +7617,33 @@ components: type: object description: | A W3C DID Core `did:web` document. Lists the operator's Ed25519 - verification methods; rotated keys are retained (so historical - signatures still verify) and revoked keys are omitted. + verification methods as `JsonWebKey`s (W3C Controlled Identifiers v1.0); + rotated keys are retained (so historical signatures still verify) and revoked + keys are omitted. + + Each method is named by its key's own RFC 7638 thumbprint, in the URI form of + RFC 9278, so a name never moves to another key when the keys are rotated. A + token's `kid` is the absolute URL of the method that signed it. The first + entry of `authentication` is the current key. example: '@context': - https://www.w3.org/ns/did/v1 - - https://w3id.org/security/suites/ed25519-2020/v1 + - https://www.w3.org/ns/cid/v1 id: did:web:id.odal-node.io verificationMethod: - - id: did:web:id.odal-node.io#key-1 - type: JsonWebKey2020 + - id: did:web:id.odal-node.io#urn:ietf:params:oauth:jwk-thumbprint:sha-256: + type: JsonWebKey controller: did:web:id.odal-node.io publicKeyJwk: kty: OKP crv: Ed25519 x: + kid: urn:ietf:params:oauth:jwk-thumbprint:sha-256: + alg: EdDSA authentication: - - did:web:id.odal-node.io#key-1 + - did:web:id.odal-node.io#urn:ietf:params:oauth:jwk-thumbprint:sha-256: assertionMethod: - - did:web:id.odal-node.io#key-1 + - did:web:id.odal-node.io#urn:ietf:params:oauth:jwk-thumbprint:sha-256: InternalSignRequest: type: object required: @@ -7644,7 +7716,7 @@ components: type: string new_key_id: type: string - example: did:web:id.odal-node.io#key-1 + example: did:web:id.odal-node.io#urn:ietf:params:oauth:jwk-thumbprint:sha-256: fingerprint: type: string description: SHA-256 fingerprint (hex) of the new public key. @@ -8206,8 +8278,8 @@ components: gates pass", not "publish will certainly succeed". The mandatory-content half is the reason this exists. It is the gate that most - often refuses a battery — 45 data points for an electric-vehicle one, 44 for - an LMT battery, 37 for an industrial one — and it could not previously be + often refuses a battery — 41 data points for an electric-vehicle one, 40 for + an LMT battery, 33 for an industrial one — and it could not previously be previewed anywhere: it is reachable only from a stored passport, and the only dry-run the API offered takes an unsaved request body. required: @@ -8839,7 +8911,8 @@ components: description: | The correction, in the same shape the draft-update body takes: only the patchable content fields (`productName`, `co2ePerUnit`, - `repairabilityScore`, `productGroupData`, `componentRefs`) take effect. + `repairabilityScore`, `productGroupData`, `componentRefs`, `personalData`) take + effect. Other keys are ignored rather than refused, so a client that sends a full create-shaped body still works and still cannot make a create-time field diff --git a/api/openapi.yaml b/api/openapi.yaml index 737f2bee..fed76284 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -106,6 +106,8 @@ components: $ref: components/schemas/passport/SecondLifeOperation.yaml DerivationRef: $ref: components/schemas/passport/DerivationRef.yaml + PersonalDataStatement: + $ref: components/schemas/passport/PersonalDataStatement.yaml LifeStatus: $ref: components/schemas/passport/LifeStatus.yaml CarbonFootprint: diff --git a/cli/src/core/snapshot.rs b/cli/src/core/snapshot.rs index e168eda8..dd308616 100644 --- a/cli/src/core/snapshot.rs +++ b/cli/src/core/snapshot.rs @@ -206,11 +206,11 @@ async fn load_document(target: &str) -> Result { /// The base64 public key to check the snapshot's proof under. /// -/// With `--did-url`, the proof's own `kid` picks the verification method, and -/// only the primary key is used when it names none. That ordering matters -/// across a key rotation: a snapshot signed before the rotation verifies under -/// the archived key the DID document still carries, and resolving straight to -/// the primary would report a perfectly sound copy as unproven. +/// With `--did-url`, the proof's own `kid` picks the verification method, and a +/// proof that names none resolves to no key. That matters across a key +/// rotation: a snapshot signed before the rotation verifies under the archived +/// key the DID document still carries, and resolving straight to the primary +/// would report a perfectly sound copy as unproven. async fn resolve_key( document: &serde_json::Value, key: Option<&str>, @@ -243,15 +243,12 @@ async fn resolve_key( let did_document: serde_json::Value = serde_json::from_slice(&body) .with_context(|| format!("{did_url} did not return a JSON DID document"))?; - let kid = document + // The key the proof's `kid` names in the document. A proof that names none is + // not checked against a key chosen for it. + let resolved = document .get("snapshotJwsSignature") .and_then(serde_json::Value::as_str) - .and_then(dpp_crypto::jws::extract_kid_from_jws); - - let resolved = match kid { - Some(kid) => dpp_crypto::jws::extract_key_by_fingerprint(&did_document, &kid), - None => dpp_crypto::jws::extract_primary_public_key(&did_document), - }; + .and_then(|proof| dpp_crypto::jws::resolve_verification_key(&did_document, proof)); resolved.ok_or_else(|| anyhow!("no usable Ed25519 public key in the DID document at {did_url}")) } @@ -337,11 +334,28 @@ mod tests { store.generate_key("root").expect("generate key"); let did = dpp_vc::build_did_document(&store, "snapshot-test.example.com", "root") .expect("build did document"); - let key = dpp_crypto::jws::extract_primary_public_key(&did) - .expect("did document carries a primary key"); + // Resolved the way a verifier resolves it, from a token naming the key. + let probe = dpp_crypto::jws::sign(&store, "root", &serde_json::json!({}), &kid(&store)) + .expect("sign probe"); + let key = dpp_crypto::jws::resolve_verification_key(&did, &probe) + .expect("did document carries the signing key"); (dir, store, key) } + /// The `kid` the node signs under: the absolute DID URL of the key's + /// verification method in the document `keystore` builds. + fn kid(store: &dpp_crypto::keystore::KeyStore) -> String { + let thumbprint = store + .public_key("root") + .expect("key present") + .thumbprint_uri() + .expect("thumbprint"); + format!( + "{}#{thumbprint}", + dpp_vc::did_for("snapshot-test.example.com") + ) + } + /// A snapshot document in the shape `render_public_snapshot` writes: the /// dates go in, the document is signed as it then stands, and the proof is /// inserted afterwards so it never signs over itself. @@ -357,7 +371,8 @@ mod tests { "asOf": rfc3339(as_of), "validUntil": rfc3339(valid_until), }); - let proof = dpp_crypto::jws::sign(store, "root", &document).expect("sign snapshot"); + let proof = + dpp_crypto::jws::sign(store, "root", &document, &kid(store)).expect("sign snapshot"); document["snapshotJwsSignature"] = serde_json::Value::String(proof); document } diff --git a/cli/tests/helpers/mod.rs b/cli/tests/helpers/mod.rs index dfac5eca..756fbb6d 100644 --- a/cli/tests/helpers/mod.rs +++ b/cli/tests/helpers/mod.rs @@ -124,13 +124,23 @@ pub fn signed_snapshot( "asOf": rfc3339(as_of), "validUntil": rfc3339(valid_until), }); - let proof = dpp_crypto::jws::sign(&store, "root", &document).expect("sign snapshot"); - document["snapshotJwsSignature"] = serde_json::Value::String(proof); + // The node signs under the absolute DID URL of the key's verification method. + let thumbprint = store + .public_key("root") + .expect("key present") + .thumbprint_uri() + .expect("thumbprint"); + let kid = format!( + "{}#{thumbprint}", + dpp_vc::did_for("snapshot-test.example.com") + ); + let proof = dpp_crypto::jws::sign(&store, "root", &document, &kid).expect("sign snapshot"); + document["snapshotJwsSignature"] = serde_json::Value::String(proof.clone()); let did_document = dpp_vc::build_did_document(&store, "snapshot-test.example.com", "root") .expect("build did document"); - let key_b64 = dpp_crypto::jws::extract_primary_public_key(&did_document) - .expect("did document carries a primary key"); + let key_b64 = dpp_crypto::jws::resolve_verification_key(&did_document, &proof) + .expect("did document carries the signing key"); SnapshotFixture { _dir: dir, diff --git a/crates/dpp-dal/src/pg/archiving_repo.rs b/crates/dpp-dal/src/pg/archiving_repo.rs index 08db8712..79c301c0 100644 --- a/crates/dpp-dal/src/pg/archiving_repo.rs +++ b/crates/dpp-dal/src/pg/archiving_repo.rs @@ -313,6 +313,7 @@ mod tests { compliance_result: None, lint_result: None, product_group_data: None, + personal_data: Default::default(), status: PassportStatus::Draft, qr_code_url: None, jws_signature: None, diff --git a/crates/dpp-dal/tests/fixtures/passport_docs/battery/v2.8.0.json b/crates/dpp-dal/tests/fixtures/passport_docs/battery/v2.8.0.json new file mode 100644 index 00000000..bd4bf67a --- /dev/null +++ b/crates/dpp-dal/tests/fixtures/passport_docs/battery/v2.8.0.json @@ -0,0 +1,179 @@ +{ + "applicableInstruments": [ + { + "instrument": "battery-reg-2023-1542", + "recorded": "catalog" + } + ], + "batchId": "LOT-2026-08", + "co2ePerUnit": { + "valueKg": 39.1 + }, + "commodityCode": "85076000", + "complianceResult": { + "assessedAt": "2026-10-16T10:36:52.351050500Z", + "co2eScore": 39.1, + "complianceStatus": "PASSTHROUGH_NO_VALIDATION", + "recycledContentPct": null, + "repairabilityIndex": null + }, + "createdAt": "2026-10-16T10:36:52.342164800Z", + "disclosureSignatures": { + "public+restricted+conformity": "test-header.eyJhcHBsaWNhYmxlSW5zdHJ1bWVudHMiOlt7Imluc3RydW1lbnQiOiJiYXR0ZXJ5LXJlZy0yMDIzLTE1NDIiLCJyZWNvcmRlZCI6ImNhdGFsb2cifV0sImJhdGNoSWQiOiJMT1QtMjAyNi0wOCIsImNvMmVQZXJVbml0Ijp7InZhbHVlS2ciOjM5LjF9LCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjb21wbGlhbmNlUmVzdWx0Ijp7ImFzc2Vzc2VkQXQiOiIyMDI2LTEwLTE2VDEwOjM2OjUyLjM1MTA1MDUwMFoiLCJjbzJlU2NvcmUiOjM5LjEsImNvbXBsaWFuY2VTdGF0dXMiOiJQQVNTVEhST1VHSF9OT19WQUxJREFUSU9OIiwicmVjeWNsZWRDb250ZW50UGN0IjpudWxsLCJyZXBhaXJhYmlsaXR5SW5kZXgiOm51bGx9LCJjcmVhdGVkQXQiOiIyMDI2LTEwLTE2VDEwOjM2OjUyLjM0MjE2NDgwMFoiLCJmYWNpbGl0eSI6eyJjb3VudHJ5IjoiREUiLCJuYW1lIjoiU2VlZCBQbGFudCIsInNjaGVtZSI6ImdsbiIsInZhbHVlIjoiNDAxMjM0NTAwMDAwOSJ9LCJpZCI6IjAxYTE0NDQ5LTNhZjYtNzE3NS04OWY3LTc0YzI4N2QzMzExNSIsImxpbnRSZXN1bHQiOnsiYXNzZXNzZWRBdCI6IjIwMjYtMTAtMTZUMTA6MzY6NTIuMzUxMDU2NzAwWiIsImZpbmRpbmdzIjpbeyJjb2RlIjoiYmF0dGVyeS5tYXRlcmlhbF9jb21wb3NpdGlvbl9zdW0iLCJmaWVsZCI6ImNhdGhvZGVNYXRlcmlhbCIsIm1lc3NhZ2UiOiJjYXRob2RlTWF0ZXJpYWwgd2VpZ2h0UGN0IGVudHJpZXMgc3VtIHRvIDMyLjAlLCBleHBlY3RlZCB-MTAwJSDigJQgaW50ZW5kZWQ_Iiwic2V2ZXJpdHkiOiJXQVJOSU5HIn0seyJjb2RlIjoiYmF0dGVyeS5tYXRlcmlhbF9jb21wb3NpdGlvbl9zdW0iLCJmaWVsZCI6ImFub2RlTWF0ZXJpYWwiLCJtZXNzYWdlIjoiYW5vZGVNYXRlcmlhbCB3ZWlnaHRQY3QgZW50cmllcyBzdW0gdG8gMTguMCUsIGV4cGVjdGVkIH4xMDAlIOKAlCBpbnRlbmRlZD8iLCJzZXZlcml0eSI6IldBUk5JTkcifSx7ImNvZGUiOiJiYXR0ZXJ5Lm1hdGVyaWFsX2NvbXBvc2l0aW9uX3N1bSIsImZpZWxkIjoiZWxlY3Ryb2x5dGVNYXRlcmlhbCIsIm1lc3NhZ2UiOiJlbGVjdHJvbHl0ZU1hdGVyaWFsIHdlaWdodFBjdCBlbnRyaWVzIHN1bSB0byAxMS4wJSwgZXhwZWN0ZWQgfjEwMCUg4oCUIGludGVuZGVkPyIsInNldmVyaXR5IjoiV0FSTklORyJ9XSwicGFja1ZlcnNpb24iOiIxLjEuMCJ9LCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6IkluZHVzdHJpZXN0cmFzc2UgNCwgMTAxMTUgQmVybGluLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vZ3JlZW5jZWxsLmV4YW1wbGUuY29tLy53ZWxsLWtub3duL2RpZC5qc29uIiwibmFtZSI6IkdyZWVuQ2VsbCBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiQ0wiLCJuYW1lIjoiTGl0aGl1bSIsInJlY3ljbGVkUGN0IjoxMi41LCJ3ZWlnaHRLZyI6MC44fSx7ImNvdW50cnlPZk9yaWdpbiI6IkNEIiwibmFtZSI6IkNvYmFsdCIsInJlY3ljbGVkUGN0IjoxNi4wLCJ3ZWlnaHRLZyI6MC4zfV0sIm9wZXJhdG9ySWRlbnRpZmllciI6IkRFMTIzNDU2Nzg5IiwicHJvZHVjdEdyb3VwIjoiYmF0dGVyeSIsInByb2R1Y3RHcm91cERhdGEiOnsiYW5vZGVNYXRlcmlhbCI6W3sibmFtZSI6IkdyYXBoaXRlIiwid2VpZ2h0UGN0IjoxOC4wfV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IklORC00OC8xMDAtQSIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IkJQLTIwMjYtMDAwMTIzIiwiYmF0dGVyeVR5cGUiOiJpbmR1c3RyaWFsIiwiYmF0dGVyeVdlaWdodEtnIjoyNC41LCJjYXRob2RlTWF0ZXJpYWwiOlt7Im5hbWUiOiJMaU5pTW5Db08yIiwid2VpZ2h0UGN0IjozMi4wfV0sImNvMmVQZXJVbml0S2ciOjM5LjEsImNvbXBvbmVudFBhcnROdW1iZXJzIjpbIkNFTEwtMjE3MDAtQSIsIkJNUy00ODIwIl0sImNyaXRpY2FsUmF3TWF0ZXJpYWxzIjpbeyJjYXNOdW1iZXIiOiI3NDQwLTQ4LTQiLCJuYW1lIjoiQ29iYWx0Iiwid2VpZ2h0R3JhbXMiOjMwMC4wfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjgwMC4wfV0sImRpc2Fzc2VtYmx5SW5zdHJ1Y3Rpb25zVXJsIjoiaHR0cHM6Ly9ncmVlbmNlbGwuZXhhbXBsZS5jb20vZGlzYXNzZW1ibHkvaW5kLTQ4IiwiZWxlY3Ryb2x5dGVNYXRlcmlhbCI6W3sibmFtZSI6IkxpUEY2IGluIEVDL0RNQyIsIndlaWdodFBjdCI6MTEuMH1dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiaHR0cHM6Ly9ncmVlbmNlbGwuZXhhbXBsZS5jb20vZG9jL2luZC00OC5wZGYiLCJleHBlY3RlZExpZmV0aW1lQ3ljbGVzIjozNTAwLCJleHBlY3RlZExpZmV0aW1lUmVmZXJlbmNlVGVzdCI6IklFQyA2MTQyNy0yOjIwMTUgY3ljbGUgZW5kdXJhbmNlIiwiaGF6YXJkb3VzU3Vic3RhbmNlcyI6W3siY2FzTnVtYmVyIjoiMjEzMjQtNDAtMyIsImNvbmNlbnRyYXRpb25QY3QiOjEuMiwibmFtZSI6IkxpdGhpdW0gaGV4YWZsdW9yb3Bob3NwaGF0ZSJ9XSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjEuNCwiaW50ZXJuYWxQYWNrUmVzaXN0YW5jZU1vaG0iOjEyLjAsIm1hbnVmYWN0dXJpbmdEYXRlIjoiMjAyNi0wNi0xNVQwMDowMDowMFoiLCJtYW51ZmFjdHVyaW5nUGxhY2UiOiJCZXJsaW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiQ0UsIHNlcGFyYXRlLWNvbGxlY3Rpb24gc3ltYm9sLCBjYXBhY2l0eSBsYWJlbCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjo0MC4wLCJub21pbmFsQ2FwYWNpdHlBaCI6MTAwLjAsIm5vbWluYWxWb2x0YWdlViI6NDguMCwibm90SW5Vc2VUZW1wZXJhdHVyZVJhbmdlIjp7Im1heEMiOjYwLjAsIm1pbkMiOi0yMC4wfSwibm90SW5Vc2VUZW1wZXJhdHVyZVJlZmVyZW5jZVRlc3QiOiJJRUMgNjI2NjAtMToyMDE4IHN0b3JhZ2UiLCJvcmlnaW5hbFBvd2VyQ2FwYWJpbGl0eVciOjQ4MDAuMCwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wNy0xNSIsInBvd2VyTGltaXRNYXhXIjo1MjAwLjAsInBvd2VyTGltaXRNaW5XIjoyMDAuMCwicHJvZHVjdEdyb3VwIjoiYmF0dGVyeSIsInByb2R1Y3RJZGVudGlmaWVyIjp7Imd0aW4iOiIwOTUwNjAwMDEzNDM1MiIsInNjaGVtZSI6ImdzMSJ9LCJyYXRlZENhcGFjaXR5S3doIjpudWxsLCJyZWN5Y2xlZENvbnRlbnRDb2JhbHRQY3QiOjE2LjAsInJlY3ljbGVkQ29udGVudExlYWRQY3QiOjAuMCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6MTIuNSwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo2LjAsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjguMCwic2FmZXR5TWVhc3VyZXMiOiJJc29sYXRlIGJlZm9yZSBzZXJ2aWNpbmc7IGRvIG5vdCBwdW5jdHVyZSIsInNwYXJlUGFydHNDb250YWN0cyI6InBhcnRzQGdyZWVuY2VsbC5leGFtcGxlLmNvbSIsInRlc3RSZXBvcnRSZXN1bHRzIjoiSUVDIDYyNjE5OjIwMjIg4oCUIHBhc3MiLCJ1c2FibGVFeHRpbmd1aXNoaW5nQWdlbnQiOiJXYXRlciwgQ08yLCBBQkMgZHJ5IHBvd2RlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRvIGFuIGF1dGhvcmlzZWQgY29sbGVjdGlvbiBwb2ludCJ9LCJwcm9kdWN0TmFtZSI6IkdyaWRDZWxsIElORC00OC8xMDAiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMTAtMTZUMTA6MzY6NTIuNDA1ODQ1OTAwWiIsInFyQ29kZVVybCI6Imh0dHBzOi8vcmVzb2x2ZXIuZXhhbXBsZS5jb20vMDEvMDk1MDYwMDAxMzQzNTIvMjEvNzE3NTg5Zjc3NGMyODdkMzMxMTUiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvbkxvY2tlZCI6dHJ1ZSwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTEwLTEzVDEwOjM2OjUyLjQwNTg0NTkwMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi44LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTEwLTE2VDEwOjM2OjUyLjQwNTg0NTkwMFoiLCJ2ZXJzaW9uIjoxfQ.test-sig", + "public+restricted+individual": "test-header.eyJhcHBsaWNhYmxlSW5zdHJ1bWVudHMiOlt7Imluc3RydW1lbnQiOiJiYXR0ZXJ5LXJlZy0yMDIzLTE1NDIiLCJyZWNvcmRlZCI6ImNhdGFsb2cifV0sImJhdGNoSWQiOiJMT1QtMjAyNi0wOCIsImNvMmVQZXJVbml0Ijp7InZhbHVlS2ciOjM5LjF9LCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjb21wbGlhbmNlUmVzdWx0Ijp7ImFzc2Vzc2VkQXQiOiIyMDI2LTEwLTE2VDEwOjM2OjUyLjM1MTA1MDUwMFoiLCJjbzJlU2NvcmUiOjM5LjEsImNvbXBsaWFuY2VTdGF0dXMiOiJQQVNTVEhST1VHSF9OT19WQUxJREFUSU9OIiwicmVjeWNsZWRDb250ZW50UGN0IjpudWxsLCJyZXBhaXJhYmlsaXR5SW5kZXgiOm51bGx9LCJjcmVhdGVkQXQiOiIyMDI2LTEwLTE2VDEwOjM2OjUyLjM0MjE2NDgwMFoiLCJmYWNpbGl0eSI6eyJjb3VudHJ5IjoiREUiLCJuYW1lIjoiU2VlZCBQbGFudCIsInNjaGVtZSI6ImdsbiIsInZhbHVlIjoiNDAxMjM0NTAwMDAwOSJ9LCJpZCI6IjAxYTE0NDQ5LTNhZjYtNzE3NS04OWY3LTc0YzI4N2QzMzExNSIsImxpbnRSZXN1bHQiOnsiYXNzZXNzZWRBdCI6IjIwMjYtMTAtMTZUMTA6MzY6NTIuMzUxMDU2NzAwWiIsImZpbmRpbmdzIjpbeyJjb2RlIjoiYmF0dGVyeS5tYXRlcmlhbF9jb21wb3NpdGlvbl9zdW0iLCJmaWVsZCI6ImNhdGhvZGVNYXRlcmlhbCIsIm1lc3NhZ2UiOiJjYXRob2RlTWF0ZXJpYWwgd2VpZ2h0UGN0IGVudHJpZXMgc3VtIHRvIDMyLjAlLCBleHBlY3RlZCB-MTAwJSDigJQgaW50ZW5kZWQ_Iiwic2V2ZXJpdHkiOiJXQVJOSU5HIn0seyJjb2RlIjoiYmF0dGVyeS5tYXRlcmlhbF9jb21wb3NpdGlvbl9zdW0iLCJmaWVsZCI6ImFub2RlTWF0ZXJpYWwiLCJtZXNzYWdlIjoiYW5vZGVNYXRlcmlhbCB3ZWlnaHRQY3QgZW50cmllcyBzdW0gdG8gMTguMCUsIGV4cGVjdGVkIH4xMDAlIOKAlCBpbnRlbmRlZD8iLCJzZXZlcml0eSI6IldBUk5JTkcifSx7ImNvZGUiOiJiYXR0ZXJ5Lm1hdGVyaWFsX2NvbXBvc2l0aW9uX3N1bSIsImZpZWxkIjoiZWxlY3Ryb2x5dGVNYXRlcmlhbCIsIm1lc3NhZ2UiOiJlbGVjdHJvbHl0ZU1hdGVyaWFsIHdlaWdodFBjdCBlbnRyaWVzIHN1bSB0byAxMS4wJSwgZXhwZWN0ZWQgfjEwMCUg4oCUIGludGVuZGVkPyIsInNldmVyaXR5IjoiV0FSTklORyJ9XSwicGFja1ZlcnNpb24iOiIxLjEuMCJ9LCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6IkluZHVzdHJpZXN0cmFzc2UgNCwgMTAxMTUgQmVybGluLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vZ3JlZW5jZWxsLmV4YW1wbGUuY29tLy53ZWxsLWtub3duL2RpZC5qc29uIiwibmFtZSI6IkdyZWVuQ2VsbCBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiQ0wiLCJuYW1lIjoiTGl0aGl1bSIsInJlY3ljbGVkUGN0IjoxMi41LCJ3ZWlnaHRLZyI6MC44fSx7ImNvdW50cnlPZk9yaWdpbiI6IkNEIiwibmFtZSI6IkNvYmFsdCIsInJlY3ljbGVkUGN0IjoxNi4wLCJ3ZWlnaHRLZyI6MC4zfV0sIm9wZXJhdG9ySWRlbnRpZmllciI6IkRFMTIzNDU2Nzg5IiwicHJvZHVjdEdyb3VwIjoiYmF0dGVyeSIsInByb2R1Y3RHcm91cERhdGEiOnsiYW5vZGVNYXRlcmlhbCI6W3sibmFtZSI6IkdyYXBoaXRlIiwid2VpZ2h0UGN0IjoxOC4wfV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IklORC00OC8xMDAtQSIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IkJQLTIwMjYtMDAwMTIzIiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJpbmR1c3RyaWFsIiwiYmF0dGVyeVdlaWdodEtnIjoyNC41LCJjYXRob2RlTWF0ZXJpYWwiOlt7Im5hbWUiOiJMaU5pTW5Db08yIiwid2VpZ2h0UGN0IjozMi4wfV0sImNvMmVQZXJVbml0S2ciOjM5LjEsImNvbXBvbmVudFBhcnROdW1iZXJzIjpbIkNFTEwtMjE3MDAtQSIsIkJNUy00ODIwIl0sImNyaXRpY2FsUmF3TWF0ZXJpYWxzIjpbeyJjYXNOdW1iZXIiOiI3NDQwLTQ4LTQiLCJuYW1lIjoiQ29iYWx0Iiwid2VpZ2h0R3JhbXMiOjMwMC4wfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjgwMC4wfV0sImRpc2Fzc2VtYmx5SW5zdHJ1Y3Rpb25zVXJsIjoiaHR0cHM6Ly9ncmVlbmNlbGwuZXhhbXBsZS5jb20vZGlzYXNzZW1ibHkvaW5kLTQ4IiwiZWxlY3Ryb2x5dGVNYXRlcmlhbCI6W3sibmFtZSI6IkxpUEY2IGluIEVDL0RNQyIsIndlaWdodFBjdCI6MTEuMH1dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiaHR0cHM6Ly9ncmVlbmNlbGwuZXhhbXBsZS5jb20vZG9jL2luZC00OC5wZGYiLCJleHBlY3RlZExpZmV0aW1lQ3ljbGVzIjozNTAwLCJleHBlY3RlZExpZmV0aW1lUmVmZXJlbmNlVGVzdCI6IklFQyA2MTQyNy0yOjIwMTUgY3ljbGUgZW5kdXJhbmNlIiwiaGF6YXJkb3VzU3Vic3RhbmNlcyI6W3siY2FzTnVtYmVyIjoiMjEzMjQtNDAtMyIsImNvbmNlbnRyYXRpb25QY3QiOjEuMiwibmFtZSI6IkxpdGhpdW0gaGV4YWZsdW9yb3Bob3NwaGF0ZSJ9XSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjEuNCwiaW50ZXJuYWxQYWNrUmVzaXN0YW5jZU1vaG0iOjEyLjAsIm1hbnVmYWN0dXJpbmdEYXRlIjoiMjAyNi0wNi0xNVQwMDowMDowMFoiLCJtYW51ZmFjdHVyaW5nUGxhY2UiOiJCZXJsaW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiQ0UsIHNlcGFyYXRlLWNvbGxlY3Rpb24gc3ltYm9sLCBjYXBhY2l0eSBsYWJlbCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjo0MC4wLCJub21pbmFsQ2FwYWNpdHlBaCI6MTAwLjAsIm5vbWluYWxWb2x0YWdlViI6NDguMCwibm90SW5Vc2VUZW1wZXJhdHVyZVJhbmdlIjp7Im1heEMiOjYwLjAsIm1pbkMiOi0yMC4wfSwibm90SW5Vc2VUZW1wZXJhdHVyZVJlZmVyZW5jZVRlc3QiOiJJRUMgNjI2NjAtMToyMDE4IHN0b3JhZ2UiLCJvcmlnaW5hbFBvd2VyQ2FwYWJpbGl0eVciOjQ4MDAuMCwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wNy0xNSIsInBvd2VyTGltaXRNYXhXIjo1MjAwLjAsInBvd2VyTGltaXRNaW5XIjoyMDAuMCwicHJvZHVjdEdyb3VwIjoiYmF0dGVyeSIsInByb2R1Y3RJZGVudGlmaWVyIjp7Imd0aW4iOiIwOTUwNjAwMDEzNDM1MiIsInNjaGVtZSI6ImdzMSJ9LCJyYXRlZENhcGFjaXR5S3doIjpudWxsLCJyZWN5Y2xlZENvbnRlbnRDb2JhbHRQY3QiOjE2LjAsInJlY3ljbGVkQ29udGVudExlYWRQY3QiOjAuMCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6MTIuNSwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo2LjAsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjguMCwic2FmZXR5TWVhc3VyZXMiOiJJc29sYXRlIGJlZm9yZSBzZXJ2aWNpbmc7IGRvIG5vdCBwdW5jdHVyZSIsInNwYXJlUGFydHNDb250YWN0cyI6InBhcnRzQGdyZWVuY2VsbC5leGFtcGxlLmNvbSIsInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyLCBDTzIsIEFCQyBkcnkgcG93ZGVyIiwid2FzdGVCYXR0ZXJ5SW5mb3JtYXRpb24iOiJSZXR1cm4gdG8gYW4gYXV0aG9yaXNlZCBjb2xsZWN0aW9uIHBvaW50In0sInByb2R1Y3ROYW1lIjoiR3JpZENlbGwgSU5ELTQ4LzEwMCIsInB1Ymxpc2hlZEF0IjoiMjAyNi0xMC0xNlQxMDozNjo1Mi40MDU4NDU5MDBaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9yZXNvbHZlci5leGFtcGxlLmNvbS8wMS8wOTUwNjAwMDEzNDM1Mi8yMS83MTc1ODlmNzc0YzI4N2QzMzExNSIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTEwLTEzVDEwOjM2OjUyLjQwNTg0NTkwMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi44LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTEwLTE2VDEwOjM2OjUyLjQwNTg0NTkwMFoiLCJ2ZXJzaW9uIjoxfQ.test-sig" + }, + "facility": { + "country": "DE", + "name": "Seed Plant", + "scheme": "gln", + "value": "4012345000009" + }, + "id": "01a14449-3af6-7175-89f7-74c287d33115", + "jwsSignature": "test-header.eyJhcHBsaWNhYmxlSW5zdHJ1bWVudHMiOlt7Imluc3RydW1lbnQiOiJiYXR0ZXJ5LXJlZy0yMDIzLTE1NDIiLCJyZWNvcmRlZCI6ImNhdGFsb2cifV0sImJhdGNoSWQiOiJMT1QtMjAyNi0wOCIsImNvMmVQZXJVbml0Ijp7InZhbHVlS2ciOjM5LjF9LCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjb21wbGlhbmNlUmVzdWx0Ijp7ImFzc2Vzc2VkQXQiOiIyMDI2LTEwLTE2VDEwOjM2OjUyLjM1MTA1MDUwMFoiLCJjbzJlU2NvcmUiOjM5LjEsImNvbXBsaWFuY2VTdGF0dXMiOiJQQVNTVEhST1VHSF9OT19WQUxJREFUSU9OIiwicmVjeWNsZWRDb250ZW50UGN0IjpudWxsLCJyZXBhaXJhYmlsaXR5SW5kZXgiOm51bGx9LCJjcmVhdGVkQXQiOiIyMDI2LTEwLTE2VDEwOjM2OjUyLjM0MjE2NDgwMFoiLCJmYWNpbGl0eSI6eyJjb3VudHJ5IjoiREUiLCJuYW1lIjoiU2VlZCBQbGFudCIsInNjaGVtZSI6ImdsbiIsInZhbHVlIjoiNDAxMjM0NTAwMDAwOSJ9LCJpZCI6IjAxYTE0NDQ5LTNhZjYtNzE3NS04OWY3LTc0YzI4N2QzMzExNSIsImp3c1NpZ25hdHVyZSI6bnVsbCwibGludFJlc3VsdCI6eyJhc3Nlc3NlZEF0IjoiMjAyNi0xMC0xNlQxMDozNjo1Mi4zNTEwNTY3MDBaIiwiZmluZGluZ3MiOlt7ImNvZGUiOiJiYXR0ZXJ5Lm1hdGVyaWFsX2NvbXBvc2l0aW9uX3N1bSIsImZpZWxkIjoiY2F0aG9kZU1hdGVyaWFsIiwibWVzc2FnZSI6ImNhdGhvZGVNYXRlcmlhbCB3ZWlnaHRQY3QgZW50cmllcyBzdW0gdG8gMzIuMCUsIGV4cGVjdGVkIH4xMDAlIOKAlCBpbnRlbmRlZD8iLCJzZXZlcml0eSI6IldBUk5JTkcifSx7ImNvZGUiOiJiYXR0ZXJ5Lm1hdGVyaWFsX2NvbXBvc2l0aW9uX3N1bSIsImZpZWxkIjoiYW5vZGVNYXRlcmlhbCIsIm1lc3NhZ2UiOiJhbm9kZU1hdGVyaWFsIHdlaWdodFBjdCBlbnRyaWVzIHN1bSB0byAxOC4wJSwgZXhwZWN0ZWQgfjEwMCUg4oCUIGludGVuZGVkPyIsInNldmVyaXR5IjoiV0FSTklORyJ9LHsiY29kZSI6ImJhdHRlcnkubWF0ZXJpYWxfY29tcG9zaXRpb25fc3VtIiwiZmllbGQiOiJlbGVjdHJvbHl0ZU1hdGVyaWFsIiwibWVzc2FnZSI6ImVsZWN0cm9seXRlTWF0ZXJpYWwgd2VpZ2h0UGN0IGVudHJpZXMgc3VtIHRvIDExLjAlLCBleHBlY3RlZCB-MTAwJSDigJQgaW50ZW5kZWQ_Iiwic2V2ZXJpdHkiOiJXQVJOSU5HIn1dLCJwYWNrVmVyc2lvbiI6IjEuMS4wIn0sIm1hbnVmYWN0dXJlciI6eyJhZGRyZXNzIjoiSW5kdXN0cmllc3RyYXNzZSA0LCAxMDExNSBCZXJsaW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ncmVlbmNlbGwuZXhhbXBsZS5jb20vLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiR3JlZW5DZWxsIEdtYkgifSwibWF0ZXJpYWxzIjpbeyJjb3VudHJ5T2ZPcmlnaW4iOiJDTCIsIm5hbWUiOiJMaXRoaXVtIiwicmVjeWNsZWRQY3QiOjEyLjUsIndlaWdodEtnIjowLjh9LHsiY291bnRyeU9mT3JpZ2luIjoiQ0QiLCJuYW1lIjoiQ29iYWx0IiwicmVjeWNsZWRQY3QiOjE2LjAsIndlaWdodEtnIjowLjN9XSwib3BlcmF0b3JJZGVudGlmaWVyIjoiREUxMjM0NTY3ODkiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJhbm9kZU1hdGVyaWFsIjpbeyJuYW1lIjoiR3JhcGhpdGUiLCJ3ZWlnaHRQY3QiOjE4LjB9XSwiYmF0dGVyeUNoZW1pc3RyeSI6Ik5NQyIsImJhdHRlcnlNb2RlbElkIjoiSU5ELTQ4LzEwMC1BIiwiYmF0dGVyeVBhc3Nwb3J0TnVtYmVyIjoiQlAtMjAyNi0wMDAxMjMiLCJiYXR0ZXJ5U3RhdHVzIjoib3JpZ2luYWwiLCJiYXR0ZXJ5VHlwZSI6ImluZHVzdHJpYWwiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjI0LjUsImNhdGhvZGVNYXRlcmlhbCI6W3sibmFtZSI6IkxpTmlNbkNvTzIiLCJ3ZWlnaHRQY3QiOjMyLjB9XSwiY28yZVBlclVuaXRLZyI6MzkuMSwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQ0VMTC0yMTcwMC1BIiwiQk1TLTQ4MjAiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzAwLjB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6ODAwLjB9XSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2dyZWVuY2VsbC5leGFtcGxlLmNvbS9kaXNhc3NlbWJseS9pbmQtNDgiLCJlbGVjdHJvbHl0ZU1hdGVyaWFsIjpbeyJuYW1lIjoiTGlQRjYgaW4gRUMvRE1DIiwid2VpZ2h0UGN0IjoxMS4wfV0sImV1RGVjbGFyYXRpb25PZkNvbmZvcm1pdHkiOiJodHRwczovL2dyZWVuY2VsbC5leGFtcGxlLmNvbS9kb2MvaW5kLTQ4LnBkZiIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjM1MDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiSUVDIDYxNDI3LTI6MjAxNSBjeWNsZSBlbmR1cmFuY2UiLCJoYXphcmRvdXNTdWJzdGFuY2VzIjpbeyJjYXNOdW1iZXIiOiIyMTMyNC00MC0zIiwiY29uY2VudHJhdGlvblBjdCI6MS4yLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIn1dLCJpbnRlcm5hbENlbGxSZXNpc3RhbmNlTW9obSI6MS40LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTIuMCwibWFudWZhY3R1cmluZ0RhdGUiOiIyMDI2LTA2LTE1VDAwOjAwOjAwWiIsIm1hbnVmYWN0dXJpbmdQbGFjZSI6IkJlcmxpbiwgREUiLCJtYXJraW5nSW5mb3JtYXRpb24iOiJDRSwgc2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wsIGNhcGFjaXR5IGxhYmVsIiwibWF4aW11bVZvbHRhZ2VWIjo1NC42LCJtaW5pbWFsVm9sdGFnZVYiOjQwLjAsIm5vbWluYWxDYXBhY2l0eUFoIjoxMDAuMCwibm9taW5hbFZvbHRhZ2VWIjo0OC4wLCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NjAuMCwibWluQyI6LTIwLjB9LCJub3RJblVzZVRlbXBlcmF0dXJlUmVmZXJlbmNlVGVzdCI6IklFQyA2MjY2MC0xOjIwMTggc3RvcmFnZSIsIm9yaWdpbmFsUG93ZXJDYXBhYmlsaXR5VyI6NDgwMC4wLCJwbGFjZWRPbk1hcmtldERhdGUiOiIyMDI2LTA3LTE1IiwicG93ZXJMaW1pdE1heFciOjUyMDAuMCwicG93ZXJMaW1pdE1pblciOjIwMC4wLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdElkZW50aWZpZXIiOnsiZ3RpbiI6IjA5NTA2MDAwMTM0MzUyIiwic2NoZW1lIjoiZ3MxIn0sInJhdGVkQ2FwYWNpdHlLd2giOm51bGwsInJlY3ljbGVkQ29udGVudENvYmFsdFBjdCI6MTYuMCwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MC4wLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjoxMi41LCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOjYuMCwicmVuZXdhYmxlQ29udGVudFBjdCI6OC4wLCJzYWZldHlNZWFzdXJlcyI6Iklzb2xhdGUgYmVmb3JlIHNlcnZpY2luZzsgZG8gbm90IHB1bmN0dXJlIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoicGFydHNAZ3JlZW5jZWxsLmV4YW1wbGUuY29tIiwic3RhdGVPZkhlYWx0aFBjdCI6bnVsbCwidGVzdFJlcG9ydFJlc3VsdHMiOiJJRUMgNjI2MTk6MjAyMiDigJQgcGFzcyIsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyLCBDTzIsIEFCQyBkcnkgcG93ZGVyIiwid2FzdGVCYXR0ZXJ5SW5mb3JtYXRpb24iOiJSZXR1cm4gdG8gYW4gYXV0aG9yaXNlZCBjb2xsZWN0aW9uIHBvaW50In0sInByb2R1Y3ROYW1lIjoiR3JpZENlbGwgSU5ELTQ4LzEwMCIsInB1Ymxpc2hlZEF0IjoiMjAyNi0xMC0xNlQxMDozNjo1Mi40MDU4NDU5MDBaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9yZXNvbHZlci5leGFtcGxlLmNvbS8wMS8wOTUwNjAwMDEzNDM1Mi8yMS83MTc1ODlmNzc0YzI4N2QzMzExNSIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uTG9ja2VkIjp0cnVlLCJyZXRlbnRpb25VbnRpbCI6IjIwMzYtMTAtMTNUMTA6MzY6NTIuNDA1ODQ1OTAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMTAtMTZUMTA6MzY6NTIuNDA1ODQ1OTAwWiIsInZlcnNpb24iOjF9.test-sig", + "lintResult": { + "assessedAt": "2026-10-16T10:36:52.351056700Z", + "findings": [ + { + "code": "battery.material_composition_sum", + "field": "cathodeMaterial", + "message": "cathodeMaterial weightPct entries sum to 32.0%, expected ~100% — intended?", + "severity": "WARNING" + }, + { + "code": "battery.material_composition_sum", + "field": "anodeMaterial", + "message": "anodeMaterial weightPct entries sum to 18.0%, expected ~100% — intended?", + "severity": "WARNING" + }, + { + "code": "battery.material_composition_sum", + "field": "electrolyteMaterial", + "message": "electrolyteMaterial weightPct entries sum to 11.0%, expected ~100% — intended?", + "severity": "WARNING" + } + ], + "packVersion": "1.1.0" + }, + "manufacturer": { + "address": "Industriestrasse 4, 10115 Berlin, DE", + "country": null, + "didWebUrl": "https://greencell.example.com/.well-known/did.json", + "name": "GreenCell GmbH" + }, + "materials": [ + { + "countryOfOrigin": "CL", + "name": "Lithium", + "recycledPct": 12.5, + "weightKg": 0.8 + }, + { + "countryOfOrigin": "CD", + "name": "Cobalt", + "recycledPct": 16.0, + "weightKg": 0.3 + } + ], + "operatorIdentifier": "DE123456789", + "productGroup": "battery", + "productGroupData": { + "anodeMaterial": [ + { + "name": "Graphite", + "weightPct": 18.0 + } + ], + "batteryChemistry": "NMC", + "batteryModelId": "IND-48/100-A", + "batteryPassportNumber": "BP-2026-000123", + "batteryStatus": "original", + "batteryType": "industrial", + "batteryWeightKg": 24.5, + "cathodeMaterial": [ + { + "name": "LiNiMnCoO2", + "weightPct": 32.0 + } + ], + "co2ePerUnitKg": 39.1, + "componentPartNumbers": [ + "CELL-21700-A", + "BMS-4820" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 300.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 800.0 + } + ], + "disassemblyInstructionsUrl": "https://greencell.example.com/disassembly/ind-48", + "electrolyteMaterial": [ + { + "name": "LiPF6 in EC/DMC", + "weightPct": 11.0 + } + ], + "euDeclarationOfConformity": "https://greencell.example.com/doc/ind-48.pdf", + "expectedLifetimeCycles": 3500, + "expectedLifetimeReferenceTest": "IEC 61427-2:2015 cycle endurance", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 1.2, + "name": "Lithium hexafluorophosphate" + } + ], + "internalCellResistanceMohm": 1.4, + "internalPackResistanceMohm": 12.0, + "manufacturingDate": "2026-06-15T00:00:00Z", + "manufacturingPlace": "Berlin, DE", + "markingInformation": "CE, separate-collection symbol, capacity label", + "maximumVoltageV": 54.6, + "minimalVoltageV": 40.0, + "nominalCapacityAh": 100.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 60.0, + "minC": -20.0 + }, + "notInUseTemperatureReferenceTest": "IEC 62660-1:2018 storage", + "originalPowerCapabilityW": 4800.0, + "placedOnMarketDate": "2026-07-15", + "powerLimitMaxW": 5200.0, + "powerLimitMinW": 200.0, + "productGroup": "battery", + "productIdentifier": { + "gtin": "09506000134352", + "scheme": "gs1" + }, + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 16.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 12.5, + "recycledContentNickelPct": 6.0, + "renewableContentPct": 8.0, + "safetyMeasures": "Isolate before servicing; do not puncture", + "sparePartsContacts": "parts@greencell.example.com", + "stateOfHealthPct": null, + "testReportResults": "IEC 62619:2022 — pass", + "usableExtinguishingAgent": "Water, CO2, ABC dry powder", + "wasteBatteryInformation": "Return to an authorised collection point" + }, + "productName": "GridCell IND-48/100", + "publicJwsSignature": "test-header.eyJhcHBsaWNhYmxlSW5zdHJ1bWVudHMiOlt7Imluc3RydW1lbnQiOiJiYXR0ZXJ5LXJlZy0yMDIzLTE1NDIiLCJyZWNvcmRlZCI6ImNhdGFsb2cifV0sImJhdGNoSWQiOiJMT1QtMjAyNi0wOCIsImNvMmVQZXJVbml0Ijp7InZhbHVlS2ciOjM5LjF9LCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjb21wbGlhbmNlUmVzdWx0Ijp7ImFzc2Vzc2VkQXQiOiIyMDI2LTEwLTE2VDEwOjM2OjUyLjM1MTA1MDUwMFoiLCJjbzJlU2NvcmUiOjM5LjEsImNvbXBsaWFuY2VTdGF0dXMiOiJQQVNTVEhST1VHSF9OT19WQUxJREFUSU9OIiwicmVjeWNsZWRDb250ZW50UGN0IjpudWxsLCJyZXBhaXJhYmlsaXR5SW5kZXgiOm51bGx9LCJjcmVhdGVkQXQiOiIyMDI2LTEwLTE2VDEwOjM2OjUyLjM0MjE2NDgwMFoiLCJmYWNpbGl0eSI6eyJjb3VudHJ5IjoiREUiLCJuYW1lIjoiU2VlZCBQbGFudCIsInNjaGVtZSI6ImdsbiIsInZhbHVlIjoiNDAxMjM0NTAwMDAwOSJ9LCJpZCI6IjAxYTE0NDQ5LTNhZjYtNzE3NS04OWY3LTc0YzI4N2QzMzExNSIsIm1hbnVmYWN0dXJlciI6eyJhZGRyZXNzIjoiSW5kdXN0cmllc3RyYXNzZSA0LCAxMDExNSBCZXJsaW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ncmVlbmNlbGwuZXhhbXBsZS5jb20vLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiR3JlZW5DZWxsIEdtYkgifSwibWF0ZXJpYWxzIjpbeyJjb3VudHJ5T2ZPcmlnaW4iOiJDTCIsIm5hbWUiOiJMaXRoaXVtIiwicmVjeWNsZWRQY3QiOjEyLjUsIndlaWdodEtnIjowLjh9LHsiY291bnRyeU9mT3JpZ2luIjoiQ0QiLCJuYW1lIjoiQ29iYWx0IiwicmVjeWNsZWRQY3QiOjE2LjAsIndlaWdodEtnIjowLjN9XSwib3BlcmF0b3JJZGVudGlmaWVyIjoiREUxMjM0NTY3ODkiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJJTkQtNDgvMTAwLUEiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJCUC0yMDI2LTAwMDEyMyIsImJhdHRlcnlUeXBlIjoiaW5kdXN0cmlhbCIsImJhdHRlcnlXZWlnaHRLZyI6MjQuNSwiY28yZVBlclVuaXRLZyI6MzkuMSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzAwLjB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6ODAwLjB9XSwiZXVEZWNsYXJhdGlvbk9mQ29uZm9ybWl0eSI6Imh0dHBzOi8vZ3JlZW5jZWxsLmV4YW1wbGUuY29tL2RvYy9pbmQtNDgucGRmIiwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MzUwMCwiZXhwZWN0ZWRMaWZldGltZVJlZmVyZW5jZVRlc3QiOiJJRUMgNjE0MjctMjoyMDE1IGN5Y2xlIGVuZHVyYW5jZSIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjoxLjIsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImludGVybmFsQ2VsbFJlc2lzdGFuY2VNb2htIjoxLjQsImludGVybmFsUGFja1Jlc2lzdGFuY2VNb2htIjoxMi4wLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDYtMTVUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiQmVybGluLCBERSIsIm1hcmtpbmdJbmZvcm1hdGlvbiI6IkNFLCBzZXBhcmF0ZS1jb2xsZWN0aW9uIHN5bWJvbCwgY2FwYWNpdHkgbGFiZWwiLCJtYXhpbXVtVm9sdGFnZVYiOjU0LjYsIm1pbmltYWxWb2x0YWdlViI6NDAuMCwibm9taW5hbENhcGFjaXR5QWgiOjEwMC4wLCJub21pbmFsVm9sdGFnZVYiOjQ4LjAsIm5vdEluVXNlVGVtcGVyYXR1cmVSYW5nZSI6eyJtYXhDIjo2MC4wLCJtaW5DIjotMjAuMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiSUVDIDYyNjYwLTE6MjAxOCBzdG9yYWdlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo0ODAwLjAsInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDctMTUiLCJwb3dlckxpbWl0TWF4VyI6NTIwMC4wLCJwb3dlckxpbWl0TWluVyI6MjAwLjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxNi4wLCJyZWN5Y2xlZENvbnRlbnRMZWFkUGN0IjowLjAsInJlY3ljbGVkQ29udGVudExpdGhpdW1QY3QiOjEyLjUsInJlY3ljbGVkQ29udGVudE5pY2tlbFBjdCI6Ni4wLCJyZW5ld2FibGVDb250ZW50UGN0Ijo4LjAsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyLCBDTzIsIEFCQyBkcnkgcG93ZGVyIiwid2FzdGVCYXR0ZXJ5SW5mb3JtYXRpb24iOiJSZXR1cm4gdG8gYW4gYXV0aG9yaXNlZCBjb2xsZWN0aW9uIHBvaW50In0sInByb2R1Y3ROYW1lIjoiR3JpZENlbGwgSU5ELTQ4LzEwMCIsInB1Ymxpc2hlZEF0IjoiMjAyNi0xMC0xNlQxMDozNjo1Mi40MDU4NDU5MDBaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9yZXNvbHZlci5leGFtcGxlLmNvbS8wMS8wOTUwNjAwMDEzNDM1Mi8yMS83MTc1ODlmNzc0YzI4N2QzMzExNSIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTEwLTEzVDEwOjM2OjUyLjQwNTg0NTkwMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi44LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTEwLTE2VDEwOjM2OjUyLjQwNTg0NTkwMFoiLCJ2ZXJzaW9uIjoxfQ.test-sig", + "publishedAt": "2026-10-16T10:36:52.405845900Z", + "qrCodeUrl": "https://resolver.example.com/01/09506000134352/21/717589f774c287d33115", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-10-13T10:36:52.405845900Z", + "schemaVersion": "2.8.0", + "status": "active", + "updatedAt": "2026-10-16T10:36:52.405845900Z", + "version": 1 +} diff --git a/crates/dpp-dal/tests/fixtures/passport_docs/textile/v1.4.0.json b/crates/dpp-dal/tests/fixtures/passport_docs/textile/v1.4.0.json new file mode 100644 index 00000000..be40baf7 --- /dev/null +++ b/crates/dpp-dal/tests/fixtures/passport_docs/textile/v1.4.0.json @@ -0,0 +1,94 @@ +{ + "applicableInstruments": [ + { + "instrument": "espr", + "recorded": "catalog" + } + ], + "batchId": null, + "co2ePerUnit": { + "valueKg": 4.2 + }, + "complianceResult": { + "assessedAt": "2026-10-16T10:37:04.030466700Z", + "co2eScore": 4.2, + "complianceStatus": "PASSTHROUGH_NO_VALIDATION", + "recycledContentPct": 30.0, + "repairabilityIndex": null + }, + "createdAt": "2026-10-16T10:37:04.023567600Z", + "disclosureSignatures": { + "public+restricted+conformity": "test-header.eyJhcHBsaWNhYmxlSW5zdHJ1bWVudHMiOlt7Imluc3RydW1lbnQiOiJlc3ByIiwicmVjb3JkZWQiOiJjYXRhbG9nIn1dLCJiYXRjaElkIjpudWxsLCJjbzJlUGVyVW5pdCI6eyJ2YWx1ZUtnIjo0LjJ9LCJjb21wbGlhbmNlUmVzdWx0Ijp7ImFzc2Vzc2VkQXQiOiIyMDI2LTEwLTE2VDEwOjM3OjA0LjAzMDQ2NjcwMFoiLCJjbzJlU2NvcmUiOjQuMiwiY29tcGxpYW5jZVN0YXR1cyI6IlBBU1NUSFJPVUdIX05PX1ZBTElEQVRJT04iLCJyZWN5Y2xlZENvbnRlbnRQY3QiOjMwLjAsInJlcGFpcmFiaWxpdHlJbmRleCI6bnVsbH0sImNyZWF0ZWRBdCI6IjIwMjYtMTAtMTZUMTA6Mzc6MDQuMDIzNTY3NjAwWiIsImZhY2lsaXR5Ijp7ImNvdW50cnkiOiJERSIsIm5hbWUiOiJTZWVkIFBsYW50Iiwic2NoZW1lIjoiZ2xuIiwidmFsdWUiOiI0MDEyMzQ1MDAwMDA5In0sImlkIjoiMDFhMTQ0NDktNjg5Ny03MmUyLWJlYzUtYmM3YTY1NDVlOWY5IiwibGludFJlc3VsdCI6eyJhc3Nlc3NlZEF0IjoiMjAyNi0xMC0xNlQxMDozNzowNC4wMzA0NzMxMDBaIiwicGFja1ZlcnNpb24iOiIxLjEuMCJ9LCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6IlZpYSBSb21hIDEyLCAyMDEyMSBNaWxhbm8sIElUIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjpudWxsLCJuYW1lIjoiU2FtcGxlIFRleHRpbGVzIENvLiJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6bnVsbCwibmFtZSI6IkNvdHRvbiIsInJlY3ljbGVkUGN0IjpudWxsLCJ3ZWlnaHRLZyI6MC4yMn1dLCJvcGVyYXRvcklkZW50aWZpZXIiOiJERTEyMzQ1Njc4OSIsInByb2R1Y3RHcm91cCI6InRleHRpbGUiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImNhcmJvbkZvb3RwcmludEtnQ28yZSI6NC4yLCJjYXJlSW5zdHJ1Y3Rpb25zIjoiTWFjaGluZSB3YXNoIGNvbGQsIGxpbmUgZHJ5IiwiY2hlbWljYWxDb21wbGlhbmNlU3RhbmRhcmQiOiJPRUtPLVRFWCAxMDAiLCJjb3VudHJ5T2ZPcmlnaW4iOiJJVCIsImNvdW50cnlPZlJhd01hdGVyaWFsT3JpZ2luIjoiSU4iLCJkaXNhc3NlbWJseUluc3RydWN0aW9ucyI6IlJlbW92ZSBsYWJlbCBhbmQgbmVjayB0YXBlIGJlZm9yZSByZWN5Y2xpbmciLCJlbmRPZkxpZmVJbnN0cnVjdGlvbnMiOiJSZXR1cm4gdG8gYSB0ZXh0aWxlIGNvbGxlY3Rpb24gcG9pbnQiLCJleHBlY3RlZFdhc2hDeWNsZXMiOjUwLCJmaWJyZUNvbXBvc2l0aW9uIjpbeyJmaWJyZSI6ImNvdHRvbiIsInBjdCI6OTUuMH0seyJmaWJyZSI6ImVsYXN0YW5lIiwicGN0Ijo1LjB9XSwibWljcm9wbGFzdGljU2hlZGRpbmdNZ1Blcldhc2giOjEyLjAsInByb2R1Y3RHcm91cCI6InRleHRpbGUiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNjkiLCJzY2hlbWUiOiJnczEifSwicHJvZHVjdFdlaWdodEdyYW1zIjoyMjAuMCwicmVjeWNsYWJpbGl0eUNsYXNzIjoiQiIsInJlY3ljbGVkQ29udGVudFBjdCI6MzAuMCwic3BhcmVQYXJ0c0F2YWlsYWJsZSI6ZmFsc2UsIndhdGVyVXNlTGl0cmVzIjoxODAwLjB9LCJwcm9kdWN0TmFtZSI6Ik9yZ2FuaWMgQ290dG9uIFRlZSIsInB1Ymxpc2hlZEF0IjoiMjAyNi0xMC0xNlQxMDozNzowNC4wODE2MzZaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9yZXNvbHZlci5leGFtcGxlLmNvbS8wMS8wOTUwNjAwMDEzNDM2OS8yMS83MmUyYmVjNWJjN2E2NTQ1ZTlmOSIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uTG9ja2VkIjp0cnVlLCJyZXRlbnRpb25VbnRpbCI6IjIwMzYtMTAtMTNUMTA6Mzc6MDQuMDgxNjM2WiIsInNjaGVtYVZlcnNpb24iOiIxLjQuMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMTAtMTZUMTA6Mzc6MDQuMDgxNjM2WiIsInZlcnNpb24iOjF9.test-sig", + "public+restricted+individual": "test-header.eyJhcHBsaWNhYmxlSW5zdHJ1bWVudHMiOlt7Imluc3RydW1lbnQiOiJlc3ByIiwicmVjb3JkZWQiOiJjYXRhbG9nIn1dLCJiYXRjaElkIjpudWxsLCJjbzJlUGVyVW5pdCI6eyJ2YWx1ZUtnIjo0LjJ9LCJjb21wbGlhbmNlUmVzdWx0Ijp7ImFzc2Vzc2VkQXQiOiIyMDI2LTEwLTE2VDEwOjM3OjA0LjAzMDQ2NjcwMFoiLCJjbzJlU2NvcmUiOjQuMiwiY29tcGxpYW5jZVN0YXR1cyI6IlBBU1NUSFJPVUdIX05PX1ZBTElEQVRJT04iLCJyZWN5Y2xlZENvbnRlbnRQY3QiOjMwLjAsInJlcGFpcmFiaWxpdHlJbmRleCI6bnVsbH0sImNyZWF0ZWRBdCI6IjIwMjYtMTAtMTZUMTA6Mzc6MDQuMDIzNTY3NjAwWiIsImZhY2lsaXR5Ijp7ImNvdW50cnkiOiJERSIsIm5hbWUiOiJTZWVkIFBsYW50Iiwic2NoZW1lIjoiZ2xuIiwidmFsdWUiOiI0MDEyMzQ1MDAwMDA5In0sImlkIjoiMDFhMTQ0NDktNjg5Ny03MmUyLWJlYzUtYmM3YTY1NDVlOWY5IiwibGludFJlc3VsdCI6eyJhc3Nlc3NlZEF0IjoiMjAyNi0xMC0xNlQxMDozNzowNC4wMzA0NzMxMDBaIiwicGFja1ZlcnNpb24iOiIxLjEuMCJ9LCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6IlZpYSBSb21hIDEyLCAyMDEyMSBNaWxhbm8sIElUIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjpudWxsLCJuYW1lIjoiU2FtcGxlIFRleHRpbGVzIENvLiJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6bnVsbCwibmFtZSI6IkNvdHRvbiIsInJlY3ljbGVkUGN0IjpudWxsLCJ3ZWlnaHRLZyI6MC4yMn1dLCJvcGVyYXRvcklkZW50aWZpZXIiOiJERTEyMzQ1Njc4OSIsInByb2R1Y3RHcm91cCI6InRleHRpbGUiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImNhcmJvbkZvb3RwcmludEtnQ28yZSI6NC4yLCJjYXJlSW5zdHJ1Y3Rpb25zIjoiTWFjaGluZSB3YXNoIGNvbGQsIGxpbmUgZHJ5IiwiY2hlbWljYWxDb21wbGlhbmNlU3RhbmRhcmQiOiJPRUtPLVRFWCAxMDAiLCJjb3VudHJ5T2ZPcmlnaW4iOiJJVCIsImNvdW50cnlPZlJhd01hdGVyaWFsT3JpZ2luIjoiSU4iLCJkaXNhc3NlbWJseUluc3RydWN0aW9ucyI6IlJlbW92ZSBsYWJlbCBhbmQgbmVjayB0YXBlIGJlZm9yZSByZWN5Y2xpbmciLCJlbmRPZkxpZmVJbnN0cnVjdGlvbnMiOiJSZXR1cm4gdG8gYSB0ZXh0aWxlIGNvbGxlY3Rpb24gcG9pbnQiLCJleHBlY3RlZFdhc2hDeWNsZXMiOjUwLCJmaWJyZUNvbXBvc2l0aW9uIjpbeyJmaWJyZSI6ImNvdHRvbiIsInBjdCI6OTUuMH0seyJmaWJyZSI6ImVsYXN0YW5lIiwicGN0Ijo1LjB9XSwibWljcm9wbGFzdGljU2hlZGRpbmdNZ1Blcldhc2giOjEyLjAsInByb2R1Y3RHcm91cCI6InRleHRpbGUiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNjkiLCJzY2hlbWUiOiJnczEifSwicHJvZHVjdFdlaWdodEdyYW1zIjoyMjAuMCwicmVjeWNsYWJpbGl0eUNsYXNzIjoiQiIsInJlY3ljbGVkQ29udGVudFBjdCI6MzAuMCwic3BhcmVQYXJ0c0F2YWlsYWJsZSI6ZmFsc2UsIndhdGVyVXNlTGl0cmVzIjoxODAwLjB9LCJwcm9kdWN0TmFtZSI6Ik9yZ2FuaWMgQ290dG9uIFRlZSIsInB1Ymxpc2hlZEF0IjoiMjAyNi0xMC0xNlQxMDozNzowNC4wODE2MzZaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9yZXNvbHZlci5leGFtcGxlLmNvbS8wMS8wOTUwNjAwMDEzNDM2OS8yMS83MmUyYmVjNWJjN2E2NTQ1ZTlmOSIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTEwLTEzVDEwOjM3OjA0LjA4MTYzNloiLCJzY2hlbWFWZXJzaW9uIjoiMS40LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTEwLTE2VDEwOjM3OjA0LjA4MTYzNloiLCJ2ZXJzaW9uIjoxfQ.test-sig" + }, + "facility": { + "country": "DE", + "name": "Seed Plant", + "scheme": "gln", + "value": "4012345000009" + }, + "id": "01a14449-6897-72e2-bec5-bc7a6545e9f9", + "jwsSignature": "test-header.eyJhcHBsaWNhYmxlSW5zdHJ1bWVudHMiOlt7Imluc3RydW1lbnQiOiJlc3ByIiwicmVjb3JkZWQiOiJjYXRhbG9nIn1dLCJiYXRjaElkIjpudWxsLCJjbzJlUGVyVW5pdCI6eyJ2YWx1ZUtnIjo0LjJ9LCJjb21wbGlhbmNlUmVzdWx0Ijp7ImFzc2Vzc2VkQXQiOiIyMDI2LTEwLTE2VDEwOjM3OjA0LjAzMDQ2NjcwMFoiLCJjbzJlU2NvcmUiOjQuMiwiY29tcGxpYW5jZVN0YXR1cyI6IlBBU1NUSFJPVUdIX05PX1ZBTElEQVRJT04iLCJyZWN5Y2xlZENvbnRlbnRQY3QiOjMwLjAsInJlcGFpcmFiaWxpdHlJbmRleCI6bnVsbH0sImNyZWF0ZWRBdCI6IjIwMjYtMTAtMTZUMTA6Mzc6MDQuMDIzNTY3NjAwWiIsImZhY2lsaXR5Ijp7ImNvdW50cnkiOiJERSIsIm5hbWUiOiJTZWVkIFBsYW50Iiwic2NoZW1lIjoiZ2xuIiwidmFsdWUiOiI0MDEyMzQ1MDAwMDA5In0sImlkIjoiMDFhMTQ0NDktNjg5Ny03MmUyLWJlYzUtYmM3YTY1NDVlOWY5IiwiandzU2lnbmF0dXJlIjpudWxsLCJsaW50UmVzdWx0Ijp7ImFzc2Vzc2VkQXQiOiIyMDI2LTEwLTE2VDEwOjM3OjA0LjAzMDQ3MzEwMFoiLCJwYWNrVmVyc2lvbiI6IjEuMS4wIn0sIm1hbnVmYWN0dXJlciI6eyJhZGRyZXNzIjoiVmlhIFJvbWEgMTIsIDIwMTIxIE1pbGFubywgSVQiLCJjb3VudHJ5IjpudWxsLCJkaWRXZWJVcmwiOm51bGwsIm5hbWUiOiJTYW1wbGUgVGV4dGlsZXMgQ28uIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjpudWxsLCJuYW1lIjoiQ290dG9uIiwicmVjeWNsZWRQY3QiOm51bGwsIndlaWdodEtnIjowLjIyfV0sIm9wZXJhdG9ySWRlbnRpZmllciI6IkRFMTIzNDU2Nzg5IiwicHJvZHVjdEdyb3VwIjoidGV4dGlsZSIsInByb2R1Y3RHcm91cERhdGEiOnsiY2FyYm9uRm9vdHByaW50S2dDbzJlIjo0LjIsImNhcmVJbnN0cnVjdGlvbnMiOiJNYWNoaW5lIHdhc2ggY29sZCwgbGluZSBkcnkiLCJjaGVtaWNhbENvbXBsaWFuY2VTdGFuZGFyZCI6Ik9FS08tVEVYIDEwMCIsImNvdW50cnlPZk9yaWdpbiI6IklUIiwiY291bnRyeU9mUmF3TWF0ZXJpYWxPcmlnaW4iOiJJTiIsImRpc2Fzc2VtYmx5SW5zdHJ1Y3Rpb25zIjoiUmVtb3ZlIGxhYmVsIGFuZCBuZWNrIHRhcGUgYmVmb3JlIHJlY3ljbGluZyIsImVuZE9mTGlmZUluc3RydWN0aW9ucyI6IlJldHVybiB0byBhIHRleHRpbGUgY29sbGVjdGlvbiBwb2ludCIsImV4cGVjdGVkV2FzaEN5Y2xlcyI6NTAsImZpYnJlQ29tcG9zaXRpb24iOlt7ImZpYnJlIjoiY290dG9uIiwicGN0Ijo5NS4wfSx7ImZpYnJlIjoiZWxhc3RhbmUiLCJwY3QiOjUuMH1dLCJtaWNyb3BsYXN0aWNTaGVkZGluZ01nUGVyV2FzaCI6MTIuMCwicHJvZHVjdEdyb3VwIjoidGV4dGlsZSIsInByb2R1Y3RJZGVudGlmaWVyIjp7Imd0aW4iOiIwOTUwNjAwMDEzNDM2OSIsInNjaGVtZSI6ImdzMSJ9LCJwcm9kdWN0V2VpZ2h0R3JhbXMiOjIyMC4wLCJyZWN5Y2xhYmlsaXR5Q2xhc3MiOiJCIiwicmVjeWNsZWRDb250ZW50UGN0IjozMC4wLCJzcGFyZVBhcnRzQXZhaWxhYmxlIjpmYWxzZSwid2F0ZXJVc2VMaXRyZXMiOjE4MDAuMH0sInByb2R1Y3ROYW1lIjoiT3JnYW5pYyBDb3R0b24gVGVlIiwicHVibGlzaGVkQXQiOiIyMDI2LTEwLTE2VDEwOjM3OjA0LjA4MTYzNloiLCJxckNvZGVVcmwiOiJodHRwczovL3Jlc29sdmVyLmV4YW1wbGUuY29tLzAxLzA5NTA2MDAwMTM0MzY5LzIxLzcyZTJiZWM1YmM3YTY1NDVlOWY5IiwicmVwYWlyYWJpbGl0eVNjb3JlIjpudWxsLCJyZXRlbnRpb25Mb2NrZWQiOnRydWUsInJldGVudGlvblVudGlsIjoiMjAzNi0xMC0xM1QxMDozNzowNC4wODE2MzZaIiwic2NoZW1hVmVyc2lvbiI6IjEuNC4wIiwic3RhdHVzIjoiYWN0aXZlIiwidXBkYXRlZEF0IjoiMjAyNi0xMC0xNlQxMDozNzowNC4wODE2MzZaIiwidmVyc2lvbiI6MX0.test-sig", + "lintResult": { + "assessedAt": "2026-10-16T10:37:04.030473100Z", + "packVersion": "1.1.0" + }, + "manufacturer": { + "address": "Via Roma 12, 20121 Milano, IT", + "country": null, + "didWebUrl": null, + "name": "Sample Textiles Co." + }, + "materials": [ + { + "countryOfOrigin": null, + "name": "Cotton", + "recycledPct": null, + "weightKg": 0.22 + } + ], + "operatorIdentifier": "DE123456789", + "productGroup": "textile", + "productGroupData": { + "carbonFootprintKgCo2e": 4.2, + "careInstructions": "Machine wash cold, line dry", + "chemicalComplianceStandard": "OEKO-TEX 100", + "countryOfOrigin": "IT", + "countryOfRawMaterialOrigin": "IN", + "disassemblyInstructions": "Remove label and neck tape before recycling", + "endOfLifeInstructions": "Return to a textile collection point", + "expectedWashCycles": 50, + "fibreComposition": [ + { + "fibre": "cotton", + "pct": 95.0 + }, + { + "fibre": "elastane", + "pct": 5.0 + } + ], + "microplasticSheddingMgPerWash": 12.0, + "productGroup": "textile", + "productIdentifier": { + "gtin": "09506000134369", + "scheme": "gs1" + }, + "productWeightGrams": 220.0, + "recyclabilityClass": "B", + "recycledContentPct": 30.0, + "sparePartsAvailable": false, + "waterUseLitres": 1800.0 + }, + "productName": "Organic Cotton Tee", + "publicJwsSignature": "test-header.eyJhcHBsaWNhYmxlSW5zdHJ1bWVudHMiOlt7Imluc3RydW1lbnQiOiJlc3ByIiwicmVjb3JkZWQiOiJjYXRhbG9nIn1dLCJjbzJlUGVyVW5pdCI6eyJ2YWx1ZUtnIjo0LjJ9LCJjb21wbGlhbmNlUmVzdWx0Ijp7ImFzc2Vzc2VkQXQiOiIyMDI2LTEwLTE2VDEwOjM3OjA0LjAzMDQ2NjcwMFoiLCJjbzJlU2NvcmUiOjQuMiwiY29tcGxpYW5jZVN0YXR1cyI6IlBBU1NUSFJPVUdIX05PX1ZBTElEQVRJT04iLCJyZWN5Y2xlZENvbnRlbnRQY3QiOjMwLjAsInJlcGFpcmFiaWxpdHlJbmRleCI6bnVsbH0sImNyZWF0ZWRBdCI6IjIwMjYtMTAtMTZUMTA6Mzc6MDQuMDIzNTY3NjAwWiIsImZhY2lsaXR5Ijp7ImNvdW50cnkiOiJERSIsIm5hbWUiOiJTZWVkIFBsYW50Iiwic2NoZW1lIjoiZ2xuIiwidmFsdWUiOiI0MDEyMzQ1MDAwMDA5In0sImlkIjoiMDFhMTQ0NDktNjg5Ny03MmUyLWJlYzUtYmM3YTY1NDVlOWY5IiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJWaWEgUm9tYSAxMiwgMjAxMjEgTWlsYW5vLCBJVCIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6bnVsbCwibmFtZSI6IlNhbXBsZSBUZXh0aWxlcyBDby4ifSwibWF0ZXJpYWxzIjpbeyJjb3VudHJ5T2ZPcmlnaW4iOm51bGwsIm5hbWUiOiJDb3R0b24iLCJyZWN5Y2xlZFBjdCI6bnVsbCwid2VpZ2h0S2ciOjAuMjJ9XSwib3BlcmF0b3JJZGVudGlmaWVyIjoiREUxMjM0NTY3ODkiLCJwcm9kdWN0R3JvdXAiOiJ0ZXh0aWxlIiwicHJvZHVjdEdyb3VwRGF0YSI6eyJjYXJib25Gb290cHJpbnRLZ0NvMmUiOjQuMiwiY2FyZUluc3RydWN0aW9ucyI6Ik1hY2hpbmUgd2FzaCBjb2xkLCBsaW5lIGRyeSIsImNoZW1pY2FsQ29tcGxpYW5jZVN0YW5kYXJkIjoiT0VLTy1URVggMTAwIiwiY291bnRyeU9mT3JpZ2luIjoiSVQiLCJjb3VudHJ5T2ZSYXdNYXRlcmlhbE9yaWdpbiI6IklOIiwiZW5kT2ZMaWZlSW5zdHJ1Y3Rpb25zIjoiUmV0dXJuIHRvIGEgdGV4dGlsZSBjb2xsZWN0aW9uIHBvaW50IiwiZXhwZWN0ZWRXYXNoQ3ljbGVzIjo1MCwiZmlicmVDb21wb3NpdGlvbiI6W3siZmlicmUiOiJjb3R0b24iLCJwY3QiOjk1LjB9LHsiZmlicmUiOiJlbGFzdGFuZSIsInBjdCI6NS4wfV0sIm1pY3JvcGxhc3RpY1NoZWRkaW5nTWdQZXJXYXNoIjoxMi4wLCJwcm9kdWN0R3JvdXAiOiJ0ZXh0aWxlIiwicHJvZHVjdElkZW50aWZpZXIiOnsiZ3RpbiI6IjA5NTA2MDAwMTM0MzY5Iiwic2NoZW1lIjoiZ3MxIn0sInByb2R1Y3RXZWlnaHRHcmFtcyI6MjIwLjAsInJlY3ljbGFiaWxpdHlDbGFzcyI6IkIiLCJyZWN5Y2xlZENvbnRlbnRQY3QiOjMwLjAsIndhdGVyVXNlTGl0cmVzIjoxODAwLjB9LCJwcm9kdWN0TmFtZSI6Ik9yZ2FuaWMgQ290dG9uIFRlZSIsInB1Ymxpc2hlZEF0IjoiMjAyNi0xMC0xNlQxMDozNzowNC4wODE2MzZaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9yZXNvbHZlci5leGFtcGxlLmNvbS8wMS8wOTUwNjAwMDEzNDM2OS8yMS83MmUyYmVjNWJjN2E2NTQ1ZTlmOSIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTEwLTEzVDEwOjM3OjA0LjA4MTYzNloiLCJzY2hlbWFWZXJzaW9uIjoiMS40LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTEwLTE2VDEwOjM3OjA0LjA4MTYzNloiLCJ2ZXJzaW9uIjoxfQ.test-sig", + "publishedAt": "2026-10-16T10:37:04.081636Z", + "qrCodeUrl": "https://resolver.example.com/01/09506000134369/21/72e2bec5bc7a6545e9f9", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-10-13T10:37:04.081636Z", + "schemaVersion": "1.4.0", + "status": "active", + "updatedAt": "2026-10-16T10:37:04.081636Z", + "version": 1 +} diff --git a/crates/dpp-dal/tests/passport_column_coverage.rs b/crates/dpp-dal/tests/passport_column_coverage.rs index 0e24f269..8ba579c8 100644 --- a/crates/dpp-dal/tests/passport_column_coverage.rs +++ b/crates/dpp-dal/tests/passport_column_coverage.rs @@ -113,6 +113,7 @@ fn fully_populated(supersedes: Option) -> Passport { derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: Some(now + chrono::Duration::days(3650)), product_id: Some(uuid::Uuid::now_v7()), commodity_code: None, diff --git a/crates/dpp-dal/tests/pg_integration.rs b/crates/dpp-dal/tests/pg_integration.rs index 62854704..4eb8fb82 100644 --- a/crates/dpp-dal/tests/pg_integration.rs +++ b/crates/dpp-dal/tests/pg_integration.rs @@ -87,6 +87,7 @@ fn make_passport() -> Passport { derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: None, product_id: None, commodity_code: None, @@ -114,7 +115,7 @@ fn battery_passport_with(gtin: &str, batch: Option<&str>, status: PassportStatus nominal_voltage_v: 3.2, nominal_capacity_ah: 100.0, expected_lifetime_cycles: Some(3000), - co2e_per_unit_kg: 85.4, + co2e_per_unit_kg: Some(85.4), recycled_content_cobalt_pct: None, recycled_content_lithium_pct: None, recycled_content_nickel_pct: None, diff --git a/crates/dpp-dal/tests/pg_passport_version.rs b/crates/dpp-dal/tests/pg_passport_version.rs index 3b77c8d2..449632c5 100644 --- a/crates/dpp-dal/tests/pg_passport_version.rs +++ b/crates/dpp-dal/tests/pg_passport_version.rs @@ -75,6 +75,7 @@ fn a_passport() -> Passport { derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: None, product_id: None, commodity_code: None, diff --git a/crates/dpp-dal/tests/pg_retired_status_migration.rs b/crates/dpp-dal/tests/pg_retired_status_migration.rs index cfdd1765..8d3313fa 100644 --- a/crates/dpp-dal/tests/pg_retired_status_migration.rs +++ b/crates/dpp-dal/tests/pg_retired_status_migration.rs @@ -79,6 +79,7 @@ fn doc_with_the_old_spelling(id: PassportId) -> serde_json::Value { derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: None, product_id: None, commodity_code: None, diff --git a/crates/dpp-dal/tests/pg_seal_outbox.rs b/crates/dpp-dal/tests/pg_seal_outbox.rs index 5c22d42a..b296fdef 100644 --- a/crates/dpp-dal/tests/pg_seal_outbox.rs +++ b/crates/dpp-dal/tests/pg_seal_outbox.rs @@ -75,6 +75,7 @@ fn published_passport(jws: &str) -> Passport { derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: None, product_id: None, commodity_code: None, diff --git a/crates/dpp-dal/tests/pg_successor.rs b/crates/dpp-dal/tests/pg_successor.rs index 2258864e..b902fb05 100644 --- a/crates/dpp-dal/tests/pg_successor.rs +++ b/crates/dpp-dal/tests/pg_successor.rs @@ -58,6 +58,7 @@ fn passport(status: PassportStatus, supersedes: Option) -> Passport derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: None, product_id: None, commodity_code: None, diff --git a/crates/dpp-identity/src/handlers/rotate_key.rs b/crates/dpp-identity/src/handlers/rotate_key.rs index 4d96c150..372279ed 100644 --- a/crates/dpp-identity/src/handlers/rotate_key.rs +++ b/crates/dpp-identity/src/handlers/rotate_key.rs @@ -30,7 +30,8 @@ pub struct InternalRotateKeyRequest { #[derive(Debug, Serialize)] pub struct InternalRotateKeyResponse { pub operator_id: String, - /// The new primary verification method, e.g. `did:web:…#key-1`. + /// The new primary verification method: the DID, `#`, and the new key's + /// thumbprint URI, e.g. `did:web:…#urn:ietf:params:oauth:jwk-thumbprint:sha-256:…`. pub new_key_id: String, /// SHA-256 fingerprint (hex) of the new public key. pub fingerprint: String, @@ -46,10 +47,12 @@ pub struct InternalRotateKeyResponse { /// 1. Archive the existing key under a timestamped entry (so old signatures /// remain verifiable after the rotation). /// 2. Generate and persist a new key pair. -/// 3. Rebuild the DID document: new key is `#key-1` (primary / `authentication`); -/// archived keys are appended as `#key-2`, `#key-3`, … under `assertionMethod` -/// so historical passport signatures remain verifiable. -/// 4. Return the new fingerprint and the updated DID document. +/// 3. Rebuild the DID document: the new key is the first `authentication` +/// method; archived keys stay under `assertionMethod`, each named by its own +/// thumbprint so a name never moves to another key, and historical passport +/// signatures remain verifiable. +/// 4. Return the new key's method id, its fingerprint and the updated DID +/// document. pub async fn rotate_key_handler( State(state): State, Json(body): Json, @@ -93,10 +96,18 @@ pub async fn rotate_key_handler( } }; + let new_key_id = match state.verification_method_url(&body.operator_id) { + Ok(url) => url, + Err(e) => { + tracing::error!(operator_id = %body.operator_id, error = %e, "failed to name the new signing key"); + return http_problem::internal_error(e.to_string()).into_response(); + } + }; + ( StatusCode::OK, Json(InternalRotateKeyResponse { - new_key_id: format!("{}#key-1", did_document["id"].as_str().unwrap_or("")), + new_key_id, operator_id: body.operator_id, // `KeyEntry` implements `Drop` (zeroizing), so the field is cloned // rather than moved out. @@ -144,15 +155,18 @@ mod tests { let (store, _dir) = temp_store(); store.generate_key("op1").expect("provision initial key"); - let payload = json!({"id": "dpp:test:1", "status": "published"}); - let jws_before = signer::sign(&store, "op1", &payload).expect("sign before rotation"); - let kid = verifier::extract_kid_from_jws(&jws_before).expect("kid present in header"); - let state = AppState { store: Arc::new(store), did_web_base_url: "http://localhost".into(), }; + let payload = json!({"id": "dpp:test:1", "status": "published"}); + let kid_before = state + .verification_method_url("op1") + .expect("name the key before rotation"); + let jws_before = + signer::sign(&state.store, "op1", &payload, &kid_before).expect("sign before rotation"); + let resp = rotate_key_handler( State(state.clone()), Json(InternalRotateKeyRequest { @@ -167,8 +181,8 @@ mod tests { did_builder::build_did_document(&state.store, &state.did_web_base_url, "op1") .expect("build did document after rotation"); - // The pre-rotation key must still resolve by its kid fingerprint... - let archived_key = verifier::extract_key_by_fingerprint(&did_document, &kid) + // The pre-rotation key must still resolve by the kid it signed under... + let archived_key = verifier::resolve_verification_key(&did_document, &jws_before) .expect("archived key still resolvable by kid after rotation"); assert!( verifier::verify_jws(&jws_before, &archived_key).expect("verify_jws parses cleanly"), @@ -176,9 +190,19 @@ mod tests { ); // ...and it must genuinely be the archived key, not the new primary — - // otherwise this test would pass even if archival were broken. - let new_primary = - verifier::extract_primary_public_key(&did_document).expect("new primary key present"); + // otherwise this test would pass even if archival were broken. The new + // primary is the key a token signed now resolves to. + let kid_after = state + .verification_method_url("op1") + .expect("name the key after rotation"); + assert_ne!( + kid_before, kid_after, + "the new key must be named by its own thumbprint, not the old key's name" + ); + let jws_after = + signer::sign(&state.store, "op1", &payload, &kid_after).expect("sign after rotation"); + let new_primary = verifier::resolve_verification_key(&did_document, &jws_after) + .expect("new primary key present"); assert_ne!( archived_key, new_primary, "archived key must differ from the new primary" diff --git a/crates/dpp-identity/src/handlers/sign.rs b/crates/dpp-identity/src/handlers/sign.rs index 879f3cf6..9b2ee4e1 100644 --- a/crates/dpp-identity/src/handlers/sign.rs +++ b/crates/dpp-identity/src/handlers/sign.rs @@ -92,7 +92,17 @@ pub async fn sign_handler( return http_problem::internal_error("failed to provision signing key").into_response(); } - match signer::sign(&state.store, &body.operator_id, &payload_value) { + // The `kid` is the absolute DID URL of the key's verification method, which + // is the identifier the DID document this service serves gives that key. + let kid = match state.verification_method_url(&body.operator_id) { + Ok(kid) => kid, + Err(e) => { + tracing::error!(operator_id = %body.operator_id, error = %e, "failed to name the signing key"); + return http_problem::internal_error(e.to_string()).into_response(); + } + }; + + match signer::sign(&state.store, &body.operator_id, &payload_value, &kid) { Ok(jws) => ( StatusCode::OK, Json(InternalSignResponse { jws_signature: jws }), diff --git a/crates/dpp-identity/src/handlers/verify.rs b/crates/dpp-identity/src/handlers/verify.rs index a41d6f50..97c6dd72 100644 --- a/crates/dpp-identity/src/handlers/verify.rs +++ b/crates/dpp-identity/src/handlers/verify.rs @@ -151,7 +151,7 @@ mod tests { let (store, _dir) = temp_store(); store.generate_key("root").expect("provision key"); let payload = json!({"passportId": "abc", "productName": "Widget"}); - let jws = signer::sign(&store, "root", &payload).expect("sign"); + let jws = signer::sign(&store, "root", &payload, "test-kid").expect("sign"); let app = app(store); let resp = post_json( @@ -167,7 +167,7 @@ mod tests { let (store, _dir) = temp_store(); store.generate_key("root").expect("provision key"); let signed_payload = json!({"passportId": "abc"}); - let jws = signer::sign(&store, "root", &signed_payload).expect("sign"); + let jws = signer::sign(&store, "root", &signed_payload, "test-kid").expect("sign"); let app = app(store); // Caller expects a different payload than what was actually signed. diff --git a/crates/dpp-identity/src/state.rs b/crates/dpp-identity/src/state.rs index a8c1b4da..7090a41e 100644 --- a/crates/dpp-identity/src/state.rs +++ b/crates/dpp-identity/src/state.rs @@ -13,3 +13,28 @@ pub struct AppState { /// e.g. `https://identity.odal-node.io`. pub did_web_base_url: String, } + +impl AppState { + /// The absolute DID URL of the verification method that names `operator_id`'s + /// current key: this service's DID, `#`, and the key's thumbprint URI. + /// + /// It is the identifier [`dpp_vc::did_builder::build_did_document`] gives the + /// key, so it is both the `kid` a token signed with the key carries and the + /// `id` of its method. A thumbprint is derived from the public key alone, so + /// the name does not change when the key is rotated out. + /// + /// # Errors + /// + /// When the operator has no key, or its recorded public key is unreadable. + pub(crate) fn verification_method_url(&self, operator_id: &str) -> anyhow::Result { + let key = self + .store + .public_key(operator_id) + .ok_or_else(|| anyhow::anyhow!("no key found for {operator_id}"))?; + Ok(format!( + "{}#{}", + dpp_vc::did_builder::did_for(&self.did_web_base_url), + key.thumbprint_uri()? + )) + } +} diff --git a/crates/dpp-integrator/src/domain/battery_template.rs b/crates/dpp-integrator/src/domain/battery_template.rs index b62f4589..cd671948 100644 --- a/crates/dpp-integrator/src/domain/battery_template.rs +++ b/crates/dpp-integrator/src/domain/battery_template.rs @@ -279,11 +279,6 @@ const COMMON: &[Column] = &[ req("batteryWeightKg", "batteryWeightKg", "12.4"), req("nominalCapacityAh", "nominalCapacityAh", "100.0"), req("batteryChemistry", "batteryChemistry", "LFP"), - // Not in the rules table, but the row validator requires it for every - // battery — the template has to satisfy the importer as well as the - // publish gate, and a column the importer demands is as load-bearing as - // one the guidance does. - req("co2ePerUnitKg", "co2ePerUnitKg", "85.4"), req("batteryStatus", "batteryStatus", "original"), // Mandatory for EV and LMT, `Conditional` for industrial — but the row // validator requires it for every battery, so every template carries it. @@ -311,23 +306,10 @@ const COMMON: &[Column] = &[ "criticalRaw_1_countryOfOrigin", "CL", ), - // ── Recycled and renewable content ──────────────────────────────────── - req( - "recycledContentCobaltPct", - "recycledContentCobaltPct", - "0.0", - ), - req( - "recycledContentLithiumPct", - "recycledContentLithiumPct", - "6.5", - ), - req( - "recycledContentNickelPct", - "recycledContentNickelPct", - "0.0", - ), - req("recycledContentLeadPct", "recycledContentLeadPct", "0.0"), + // ── Renewable content ───────────────────────────────────────────────── + // The four recycled-content shares are not asked of any of the three + // categories this template serves: the Commission's guidance v2.0 has them + // not filled or displayed as of February 2027. req("renewableContentPct", "renewableContentPct", "18.0"), // ── Electrical characteristics ──────────────────────────────────────── req("minimalVoltageV", "minimalVoltageV", "40.0"), diff --git a/crates/dpp-integrator/src/domain/validate/aluminium.rs b/crates/dpp-integrator/src/domain/validate/aluminium.rs index 20af25d4..c5e13d92 100644 --- a/crates/dpp-integrator/src/domain/validate/aluminium.rs +++ b/crates/dpp-integrator/src/domain/validate/aluminium.rs @@ -127,6 +127,7 @@ pub fn validate_aluminium_row( // asks that a status be supported by a derivation edge — so an // imported `repurposed` would be exactly the defect that rule catches. life_status: None, + personal_data: Default::default(), component_refs: Vec::new(), }) } diff --git a/crates/dpp-integrator/src/domain/validate/battery.rs b/crates/dpp-integrator/src/domain/validate/battery.rs index 1baa153f..7c10c341 100644 --- a/crates/dpp-integrator/src/domain/validate/battery.rs +++ b/crates/dpp-integrator/src/domain/validate/battery.rs @@ -37,7 +37,7 @@ pub(super) const COLUMNS: &[Column] = &[ Column::required("nominalVoltageV"), Column::required("nominalCapacityAh"), Column::required("expectedLifetimeCycles"), - Column::required("co2ePerUnitKg"), + Column::optional("co2ePerUnitKg"), // Per-metal, not aggregate. `recycledContentPct` stood here and was read by // nothing: the template advertised a column the importer silently ignored, // so an operator who filled it in lost the value with no error. Annex XIII @@ -113,7 +113,7 @@ pub fn validate_battery_row( let nominal_voltage_v = require_f64(row, "nominalVoltageV", row_num, &mut errors); let nominal_capacity_ah = require_f64(row, "nominalCapacityAh", row_num, &mut errors); let expected_lifetime_cycles = require_u32(row, "expectedLifetimeCycles", row_num, &mut errors); - let co2e_per_unit_kg = require_f64(row, "co2ePerUnitKg", row_num, &mut errors); + let co2e_per_unit_kg = optional_f64(row, "co2ePerUnitKg", row_num, &mut errors); // Validate GTIN (14 digits, correct GS1 mod-10 check digit) if the value was present. let gtin = gtin_raw.and_then(|g| match Gtin::parse(&g) { @@ -248,8 +248,7 @@ pub fn validate_battery_row( nominal_capacity_ah: nominal_capacity_ah .expect("field verified present by errors.is_empty() guard above"), expected_lifetime_cycles, - co2e_per_unit_kg: co2e_per_unit_kg - .expect("field verified present by errors.is_empty() guard above"), + co2e_per_unit_kg, recycled_content_cobalt_pct: recycled_cobalt, recycled_content_lithium_pct: recycled_lithium, recycled_content_nickel_pct: recycled_nickel, @@ -389,6 +388,7 @@ pub fn validate_battery_row( // asks that a status be supported by a derivation edge — so an // imported `repurposed` would be exactly the defect that rule catches. life_status: None, + personal_data: Default::default(), component_refs: Vec::new(), }) } diff --git a/crates/dpp-integrator/src/domain/validate/construction.rs b/crates/dpp-integrator/src/domain/validate/construction.rs index 9ee3cd2c..7d420a91 100644 --- a/crates/dpp-integrator/src/domain/validate/construction.rs +++ b/crates/dpp-integrator/src/domain/validate/construction.rs @@ -126,6 +126,7 @@ pub fn validate_construction_row( // asks that a status be supported by a derivation edge — so an // imported `repurposed` would be exactly the defect that rule catches. life_status: None, + personal_data: Default::default(), component_refs: Vec::new(), }) } diff --git a/crates/dpp-integrator/src/domain/validate/furniture.rs b/crates/dpp-integrator/src/domain/validate/furniture.rs index e36f5d01..5d4ac09b 100644 --- a/crates/dpp-integrator/src/domain/validate/furniture.rs +++ b/crates/dpp-integrator/src/domain/validate/furniture.rs @@ -124,6 +124,7 @@ pub fn validate_furniture_row( // asks that a status be supported by a derivation edge — so an // imported `repurposed` would be exactly the defect that rule catches. life_status: None, + personal_data: Default::default(), component_refs: Vec::new(), }) } diff --git a/crates/dpp-integrator/src/domain/validate/mattress.rs b/crates/dpp-integrator/src/domain/validate/mattress.rs index 7836d81a..ed47e530 100644 --- a/crates/dpp-integrator/src/domain/validate/mattress.rs +++ b/crates/dpp-integrator/src/domain/validate/mattress.rs @@ -127,6 +127,7 @@ pub fn validate_mattress_row( // asks that a status be supported by a derivation edge — so an // imported `repurposed` would be exactly the defect that rule catches. life_status: None, + personal_data: Default::default(), component_refs: Vec::new(), }) } diff --git a/crates/dpp-integrator/src/domain/validate/steel.rs b/crates/dpp-integrator/src/domain/validate/steel.rs index 9da1d4f4..06eb9b95 100644 --- a/crates/dpp-integrator/src/domain/validate/steel.rs +++ b/crates/dpp-integrator/src/domain/validate/steel.rs @@ -127,6 +127,7 @@ pub fn validate_steel_row( // asks that a status be supported by a derivation edge — so an // imported `repurposed` would be exactly the defect that rule catches. life_status: None, + personal_data: Default::default(), component_refs: Vec::new(), }) } diff --git a/crates/dpp-integrator/src/domain/validate/textile.rs b/crates/dpp-integrator/src/domain/validate/textile.rs index f80ba132..57c7d6b7 100644 --- a/crates/dpp-integrator/src/domain/validate/textile.rs +++ b/crates/dpp-integrator/src/domain/validate/textile.rs @@ -165,6 +165,7 @@ pub fn validate_textile_row( // asks that a status be supported by a derivation edge — so an // imported `repurposed` would be exactly the defect that rule catches. life_status: None, + personal_data: Default::default(), component_refs: Vec::new(), }) } diff --git a/crates/dpp-integrator/src/domain/validate/toy.rs b/crates/dpp-integrator/src/domain/validate/toy.rs index ac6192dc..206768e8 100644 --- a/crates/dpp-integrator/src/domain/validate/toy.rs +++ b/crates/dpp-integrator/src/domain/validate/toy.rs @@ -122,6 +122,7 @@ pub fn validate_toy_row( // asks that a status be supported by a derivation edge — so an // imported `repurposed` would be exactly the defect that rule catches. life_status: None, + personal_data: Default::default(), component_refs: Vec::new(), }) } diff --git a/crates/dpp-integrator/src/domain/validate/tyre.rs b/crates/dpp-integrator/src/domain/validate/tyre.rs index 644da59a..16438760 100644 --- a/crates/dpp-integrator/src/domain/validate/tyre.rs +++ b/crates/dpp-integrator/src/domain/validate/tyre.rs @@ -129,6 +129,7 @@ pub fn validate_tyre_row( // asks that a status be supported by a derivation edge — so an // imported `repurposed` would be exactly the defect that rule catches. life_status: None, + personal_data: Default::default(), component_refs: Vec::new(), }) } diff --git a/crates/dpp-integrator/src/infra/vault_client.rs b/crates/dpp-integrator/src/infra/vault_client.rs index e57f7118..e1da4a75 100644 --- a/crates/dpp-integrator/src/infra/vault_client.rs +++ b/crates/dpp-integrator/src/infra/vault_client.rs @@ -296,6 +296,7 @@ mod tests { product_name: "Test Widget".into(), product_group: None, life_status: None, + personal_data: Default::default(), supersedes_id: None, manufacturer: ManufacturerInfo { name: "Acme".into(), diff --git a/crates/dpp-node/src/infra/registry/client.rs b/crates/dpp-node/src/infra/registry/client.rs index be52f3b6..9723c81a 100644 --- a/crates/dpp-node/src/infra/registry/client.rs +++ b/crates/dpp-node/src/infra/registry/client.rs @@ -137,6 +137,62 @@ impl EuRegistrySync { Ok(access_token) } + /// One authenticated `GET` of `url`, read as JSON. + /// + /// A single attempt, classified for [`Self::with_retry`]: connection + /// failures are `Unreachable`, `429` and `5xx` are retryable, and anything + /// else that is not a success, a body this node cannot read included, is + /// fatal. `what` names the call in the messages. + pub(super) async fn get_json( + &self, + url: &str, + query: &[(&str, String)], + what: &str, + ) -> Result { + let token = self + .get_token() + .await + .map_err(|e| RetryableError::Fatal(format!("token acquisition failed: {e}")))?; + + let resp = self + .client + .get(url) + .query(query) + .bearer_auth(&token) + .send() + .await + .map_err(|e| { + if e.is_connect() || e.is_timeout() { + RetryableError::Unreachable(e.to_string()) + } else { + RetryableError::Retryable(e.to_string()) + } + })?; + + let status = resp.status().as_u16(); + if status == 429 { + return Err(RetryableError::Retryable("rate limited (429)".into())); + } + if (500..600).contains(&status) { + let body = resp.text().await.unwrap_or_default(); + return Err(RetryableError::Retryable(format!( + "server error {status}: {}", + super::mapping::refusal_text(&body) + ))); + } + if !resp.status().is_success() { + let body = resp.text().await.unwrap_or_default(); + return Err(RetryableError::Fatal(format!( + "{what} failed {status}: {}", + super::mapping::refusal_text(&body) + ))); + } + + resp.json() + .await + .map_err(|e| RetryableError::Fatal(format!("invalid {what} response: {e}"))) + } + /// Execute an HTTP request with retry logic. /// /// Retries on 5xx, 429, timeouts, and connection errors. diff --git a/crates/dpp-node/src/infra/registry/dashboard.rs b/crates/dpp-node/src/infra/registry/dashboard.rs new file mode 100644 index 00000000..382807ad --- /dev/null +++ b/crates/dpp-node/src/infra/registry/dashboard.rs @@ -0,0 +1,318 @@ +//! Reading a submission back from the registry's dashboard. +//! +//! A registration is a task on the operator's dashboard. The registry's own web +//! interface finds it by listing the operator's tasks and reads what became of it +//! from the task's result, so those are the two routes used here +//! ([`TASKS_PATH`](dpp_registry::TASKS_PATH) and +//! [`TASK_RESULT_PATH_TEMPLATE`](dpp_registry::TASK_RESULT_PATH_TEMPLATE)). No +//! route that takes a correlation identifier has been seen, so the task is found +//! by paging the list. +//! +//! The field names are the ones the interface reads, as `dpp-registry` records +//! them. None of this is a published schema, and a state this node has no name +//! for is reported as unrecognised, never as a verdict. + +use dpp_domain::ports::registry_sync::SubmissionReport; +use dpp_registry::{PassportOutcome, PassportResult, SubmissionOutcome}; +use serde::Deserialize; + +/// The task type the registry gives a registration request. +pub(super) const REGISTRATION_TASK_TYPE: &str = "DPP_REGISTRATION"; + +/// One page of the operator's tasks. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct TaskPage { + /// The tasks on this page. + #[serde(default)] + pub content: Vec, +} + +/// One dashboard task, as far as finding a submission needs it. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct Task { + /// The task's own identifier, which its result is read by. Kept as it came: + /// nothing says whether it is a string or a number. + pub task_id: serde_json::Value, + /// The submission's correlation identifier. + #[serde(default)] + pub correlation_id: Option, + /// What kind of task this is. A registration is + /// [`REGISTRATION_TASK_TYPE`]. + #[serde(default, rename = "type")] + pub task_type: Option, + /// Where the submission has got to. + #[serde(default)] + pub status: Option, +} + +impl Task { + /// Whether this is the registration submitted under `correlation_id`. + pub(super) fn is_submission(&self, correlation_id: &str) -> bool { + self.correlation_id.as_deref() == Some(correlation_id) + && self + .task_type + .as_deref() + .is_none_or(|kind| kind == REGISTRATION_TASK_TYPE) + } + + /// The task identifier as a path segment. + pub(super) fn id_segment(&self) -> Option { + match &self.task_id { + serde_json::Value::String(id) if !id.trim().is_empty() => Some(id.clone()), + serde_json::Value::Number(id) => Some(id.to_string()), + _ => None, + } + } +} + +/// One task's result. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct TaskResult { + /// The submission's status, in the task spelling. + #[serde(default)] + pub status: Option, + /// What the registry reported about the submission. + #[serde(default)] + pub result_payload: Option, +} + +/// The body of a task result. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct ResultPayload { + /// The outcome in the result spelling (`SUCCEEDED`, `FAILED`), which the + /// interface shows when it is present. + #[serde(default)] + pub outcome_result: Option, + /// What the registry says about each passport. + #[serde(default)] + pub report: Option, +} + +/// The per-passport report. +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub(super) struct Report { + /// One entry per passport the report names. + #[serde(default)] + pub item_details: Vec, +} + +/// What a task and its result say became of the one passport this node +/// submitted under it. +/// +/// This node submits one passport per request, so the report is read as being +/// about that one. A result that does not fit is **not** a verdict, because +/// reading it as a refusal would discard a submission that may have succeeded +/// and reading it as a registration would record one that may not exist: it is +/// reported as unrecognised, which keeps the poll going. +pub(super) fn report_for(task: &Task, result: Option<&TaskResult>) -> SubmissionReport { + let results: &[PassportResult] = result + .and_then(|r| r.result_payload.as_ref()) + .and_then(|p| p.report.as_ref()) + .map_or(&[], |r| r.item_details.as_slice()); + + // The result spelling wins where there is one, as in the interface; the task + // spelling is the fallback. Both read as the same outcome. + let outcome = result + .and_then(|r| r.result_payload.as_ref()) + .and_then(|p| p.outcome_result.as_ref()) + .or_else(|| result.and_then(|r| r.status.as_ref())) + .or(task.status.as_ref()); + + match outcome { + None | Some(SubmissionOutcome::Processing) => SubmissionReport::Processing, + Some(SubmissionOutcome::Success) => registered(results), + Some(SubmissionOutcome::Failure) => SubmissionReport::Refused { + reason: refusal(results), + }, + Some(other) => SubmissionReport::Unrecognised { + state: other.wire_str().to_owned(), + }, + } +} + +/// The registration a successful submission produced. +fn registered(results: &[PassportResult]) -> SubmissionReport { + match results { + [only] if only.outcome != Some(PassportOutcome::Rejected) => { + match only.registration_identifier.as_deref().map(str::trim) { + Some(id) if !id.is_empty() => SubmissionReport::Registered { + registration_id: id.to_owned(), + }, + _ => SubmissionReport::Unrecognised { + state: "SUCCESS, with no registration identifier for the passport".to_owned(), + }, + } + } + _ => SubmissionReport::Unrecognised { + state: format!( + "SUCCESS, with {} passport results for a submission of one", + results.len() + ), + }, + } +} + +/// Why a failed submission failed, in the registry's own terms. +/// +/// A submission is refused as a whole, so the passport may carry no error of its +/// own, and that is said rather than left blank. +fn refusal(results: &[PassportResult]) -> String { + match results.iter().find_map(|r| r.error_code.as_deref()) { + Some(code) => format!("the registry refused the submission: {code}"), + None => { + "the registry refused the submission and named no error for this passport".to_owned() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + fn task(status: &str) -> Task { + serde_json::from_value(json!({ + "taskId": 7, + "correlationId": "corr-1", + "type": "DPP_REGISTRATION", + "status": status, + })) + .expect("a task") + } + + fn result(value: serde_json::Value) -> TaskResult { + serde_json::from_value(value).expect("a task result") + } + + fn accepted(registration: Option<&str>) -> serde_json::Value { + json!({ + "uniqueProductIdentifier": "https://id.example.com/01/09506000134352", + "outcome": "ACCEPTED", + "registrationIdentifier": registration, + }) + } + + #[test] + fn a_submission_is_found_by_its_correlation_id_and_its_type() { + assert!(task("PROCESSING").is_submission("corr-1")); + assert!(!task("PROCESSING").is_submission("corr-2")); + + let other_kind: Task = serde_json::from_value(json!({ + "taskId": "t", "correlationId": "corr-1", "type": "SOMETHING_ELSE", + })) + .expect("a task"); + assert!( + !other_kind.is_submission("corr-1"), + "a task of another kind is not this registration" + ); + } + + #[test] + fn a_task_id_is_a_path_segment_whether_it_arrives_as_a_string_or_a_number() { + assert_eq!(task("PROCESSING").id_segment().as_deref(), Some("7")); + let string_id: Task = serde_json::from_value(json!({ "taskId": "abc" })).expect("a task"); + assert_eq!(string_id.id_segment().as_deref(), Some("abc")); + let missing: Task = serde_json::from_value(json!({ "taskId": null })).expect("a task"); + assert_eq!(missing.id_segment(), None); + } + + #[test] + fn a_task_still_processing_is_not_ruled_on() { + assert_eq!( + report_for(&task("PROCESSING"), None), + SubmissionReport::Processing + ); + } + + #[test] + fn a_successful_submission_reports_the_registration_identifier() { + let result = result(json!({ + "status": "SUCCESS", + "resultPayload": { "report": { "itemDetails": [accepted(Some("REG-1"))] } }, + })); + assert_eq!( + report_for(&task("SUCCESS"), Some(&result)), + SubmissionReport::Registered { + registration_id: "REG-1".to_owned() + } + ); + } + + #[test] + fn the_result_spelling_of_an_outcome_is_read_as_the_same_outcome() { + let result = result(json!({ + "resultPayload": { + "outcomeResult": "SUCCEEDED", + "report": { "itemDetails": [accepted(Some("REG-2"))] }, + }, + })); + assert_eq!( + report_for(&task("PROCESSING"), Some(&result)), + SubmissionReport::Registered { + registration_id: "REG-2".to_owned() + }, + "the result's own word wins over the task's" + ); + } + + #[test] + fn a_failed_submission_is_refused_with_the_error_code_the_registry_gave() { + let result = result(json!({ + "status": "FAILURE", + "resultPayload": { "report": { "itemDetails": [{ + "uniqueProductIdentifier": "https://id.example.com/01/09506000134352", + "outcome": "REJECTED", + "errorCode": "NOT_RESOLVABLE_UPI", + }] } }, + })); + let SubmissionReport::Refused { reason } = report_for(&task("FAILURE"), Some(&result)) + else { + panic!("a failed submission is refused"); + }; + assert!(reason.contains("NOT_RESOLVABLE_UPI"), "{reason}"); + } + + #[test] + fn a_refusal_with_no_error_of_its_own_says_so() { + let result = result(json!({ "status": "FAILURE" })); + let SubmissionReport::Refused { reason } = report_for(&task("FAILURE"), Some(&result)) + else { + panic!("a failed submission is refused"); + }; + assert!(reason.contains("named no error"), "{reason}"); + } + + /// 🚨 A success that cannot be read as one registration is not a verdict. + /// Recording it as a refusal would discard a submission that may have + /// succeeded; recording a registration out of it would invent one. + #[test] + fn a_success_that_names_no_registration_keeps_the_poll_going() { + for body in [ + json!({ "status": "SUCCESS" }), + json!({ "status": "SUCCESS", "resultPayload": { "report": { "itemDetails": [accepted(None)] } } }), + json!({ "status": "SUCCESS", "resultPayload": { "report": { "itemDetails": [accepted(Some("A")), accepted(Some("B"))] } } }), + ] { + let report = report_for(&task("SUCCESS"), Some(&result(body.clone()))); + assert!( + matches!(report, SubmissionReport::Unrecognised { .. }) && !report.is_terminal(), + "{body} must stay unresolved, got {report:?}" + ); + } + } + + #[test] + fn a_state_the_node_has_no_name_for_is_reported_as_given() { + let report = report_for(&task("ON_HOLD"), None); + assert_eq!( + report, + SubmissionReport::Unrecognised { + state: "ON_HOLD".to_owned() + } + ); + } +} diff --git a/crates/dpp-node/src/infra/registry/mapping.rs b/crates/dpp-node/src/infra/registry/mapping.rs index 8a37ab8e..8db2b6a9 100644 --- a/crates/dpp-node/src/infra/registry/mapping.rs +++ b/crates/dpp-node/src/infra/registry/mapping.rs @@ -3,15 +3,15 @@ //! types, and makes the actual REST calls via `EuRegistrySync`. use super::client::{EuRegistrySync, RetryableError}; +use super::dashboard::{TaskPage, TaskResult, report_for}; use async_trait::async_trait; use chrono::Utc; use dpp_domain::{ error::DppError, operator::ResponsibleOperator, - passport::PassportId, ports::registry_sync::{ - RegistrationGranularity, RegistrationRequest, RegistryIdentifiers, RegistryRecord, - RegistryStatus, RegistrySyncPort, ServiceProviderRef, + RegistrationGranularity, RegistrationRequest, RegistryRecord, RegistryStatus, + RegistrySyncPort, ServiceProviderRef, SubmissionAcknowledgement, SubmissionReport, }, transfer::TransferRecord, }; @@ -19,8 +19,9 @@ use dpp_registry::{ EuRegistryEnvelope, EuRegistryResponse, FacilityIdentifier, Granularity, IDEMPOTENCY_KEY_HEADER, OperatorIdentifier, ProductIdentifier, ProductItemIdentifier, REGISTRATION_PATH, RegistrationLevel, RegistrationPayload, RegistrationSubmission, - RegistryErrorBody, STATUS_IDEMPOTENCY_KEY_REUSED, STATUS_PATH_TEMPLATE, - ServiceProviderReference, StatusResponse, TRANSFER_PATH_TEMPLATE, TransferNotification, + RegistryErrorBody, RegistryStatusCode, STATUS_IDEMPOTENCY_KEY_REUSED, STATUS_PATH_TEMPLATE, + ServiceProviderReference, StatusResponse, SubmissionOutcome, SubmissionReceipt, + TASK_RESULT_PATH_TEMPLATE, TASKS_PATH, TRANSFER_PATH_TEMPLATE, TransferNotification, }; /// Map the port's service provider onto the registry's reference to one. @@ -37,13 +38,35 @@ pub fn service_provider_reference(provider: &ServiceProviderRef) -> ServiceProvi } } -/// The path beneath the base URL for one passport's status or transfer route. +/// How many tasks one page of the dashboard list asks for. +const TASK_PAGE_SIZE: usize = 50; + +/// How many pages of the dashboard list are read looking for a submission. +const MAX_TASK_PAGES: u32 = 20; + +/// The URL of one of the registry's routes beneath `base_url`, with `{id}` +/// replaced by `id`. +/// +/// The templates are core's. Spelling them here as well is what let the +/// registration path sit wrong for as long as it did: the adapter and its mock +/// agreed with each other and with nothing else. /// -/// Both templates are core's, with `{id}` for the passport. Spelling them here -/// as well is what let the registration path sit wrong for as long as it did: -/// the adapter and its mock agreed with each other and with nothing else. -fn path_for(template: &str, passport_id: PassportId) -> String { - template.replace("{id}", &passport_id.to_string()) +/// `id` is one the registry gave: a registration identifier, which its guide +/// describes as a URI, or a task identifier. It is written as a single path +/// segment and escaped, so a `/` in it cannot add segments to the route. +fn url_for(base_url: &str, template: &str, id: &str) -> Result { + let mut url = reqwest::Url::parse(base_url) + .map_err(|e| DppError::Internal(format!("registry base URL {base_url:?}: {e}")))?; + url.path_segments_mut() + .map_err(|()| DppError::Internal(format!("registry base URL {base_url:?} has no path")))? + .pop_if_empty() + .extend( + template + .split('/') + .filter(|segment| !segment.is_empty()) + .map(|segment| if segment == "{id}" { id } else { segment }), + ); + Ok(url.into()) } /// What the registry said about a refusal, worded for the operator. @@ -55,7 +78,7 @@ fn path_for(template: &str, passport_id: PassportId) -> String { /// /// A body that is not that shape, or carries none of it, is returned as it /// came: reading it must never cost what the registry actually said. -fn refusal_text(body: &str) -> String { +pub(super) fn refusal_text(body: &str) -> String { let Ok(parsed) = serde_json::from_str::(body) else { return body.to_owned(); }; @@ -76,49 +99,26 @@ fn refusal_text(body: &str) -> String { } } -impl EuRegistrySync { - /// A submission the registry already holds, whose verdict is outstanding. - /// - /// What a replayed idempotency key means: an earlier attempt reached the - /// registry and its answer never reached us. The registry acknowledges the - /// key and nothing else, so there is no record id to report and none is - /// made up — `registry_id` is empty, as the other identifiers are on a - /// record built from a response that did not carry them. The drain moves - /// the row to `submitted` and polls. - pub(super) fn held_by_registry() -> RegistryRecord { - RegistryRecord { - identifiers: RegistryIdentifiers { - product_id: String::new(), - operator_id: String::new(), - facility_id: String::new(), - registry_id: String::new(), - }, - status: RegistryStatus::Pending, - registered_at: Utc::now(), - updated_at: Utc::now(), - } +/// How a registered record stands, in the port's words. +/// +/// `pending` and `rejected` were states of a submission, which the port reports +/// as a [`SubmissionReport`]; a record that answers with one has a state the +/// port has no name for, and carries it as the registry said it. +fn record_status(code: &RegistryStatusCode) -> RegistryStatus { + match code { + RegistryStatusCode::Registered => RegistryStatus::Registered, + RegistryStatusCode::SuspendedByAuthority => RegistryStatus::SuspendedByAuthority, + RegistryStatusCode::Deactivated => RegistryStatus::Deactivated, + other => RegistryStatus::Unrecognised(other.wire_str().to_owned()), } +} +impl EuRegistrySync { /// Map a bridge `EuRegistryResponse` to a domain `RegistryRecord`. pub(super) fn response_to_record(resp: &EuRegistryResponse) -> RegistryRecord { - use dpp_registry::RegistryStatusCode; - - let status = match resp.status { - RegistryStatusCode::Pending => RegistryStatus::Pending, - RegistryStatusCode::Registered => RegistryStatus::Registered, - RegistryStatusCode::Rejected => RegistryStatus::Rejected, - RegistryStatusCode::SuspendedByAuthority => RegistryStatus::SuspendedByAuthority, - RegistryStatusCode::Deactivated => RegistryStatus::Deactivated, - }; - RegistryRecord { - identifiers: RegistryIdentifiers { - product_id: resp.registry_id.clone(), - operator_id: String::new(), // populated from status endpoint - facility_id: String::new(), - registry_id: resp.registry_id.clone(), - }, - status, + registration_id: resp.registry_id.clone(), + status: record_status(&resp.status), registered_at: resp.updated_at, updated_at: resp.updated_at, } @@ -126,24 +126,9 @@ impl EuRegistrySync { /// Map a bridge `StatusResponse` to a domain `RegistryRecord`. pub(super) fn status_to_record(resp: &StatusResponse) -> RegistryRecord { - use dpp_registry::RegistryStatusCode; - - let status = match resp.status { - RegistryStatusCode::Pending => RegistryStatus::Pending, - RegistryStatusCode::Registered => RegistryStatus::Registered, - RegistryStatusCode::Rejected => RegistryStatus::Rejected, - RegistryStatusCode::SuspendedByAuthority => RegistryStatus::SuspendedByAuthority, - RegistryStatusCode::Deactivated => RegistryStatus::Deactivated, - }; - RegistryRecord { - identifiers: RegistryIdentifiers { - product_id: String::new(), - operator_id: String::new(), - facility_id: String::new(), - registry_id: resp.registry_id.clone(), - }, - status, + registration_id: resp.registry_id.clone(), + status: record_status(&resp.status), registered_at: resp.updated_at, updated_at: resp.updated_at, } @@ -209,8 +194,9 @@ pub(super) fn operator_identifier_for(operator: &ResponsibleOperator) -> Operato /// Map the port's granularity and linked model onto the registry's registration /// level. /// -/// The batch identifier is not linked here: the port carries no batch, and a -/// linked-but-blank identifier is refused. Absence is the lawful encoding of +/// The batch is linked when the request carries one, which core sets at batch +/// and item level where IR (EU) 2026/1778 Art. 8(4) and 8(5) link it. A +/// linked-but-blank identifier is refused; absence is the lawful encoding of /// "this product has no batch design". pub(super) fn level_for(request: &RegistrationRequest) -> RegistrationLevel { let granularity = match request.granularity { @@ -218,24 +204,39 @@ pub(super) fn level_for(request: &RegistrationRequest) -> RegistrationLevel { RegistrationGranularity::Batch => Granularity::Batch, RegistrationGranularity::Item => Granularity::Item, }; - let level = RegistrationLevel::new(granularity); - match &request.model_id { - Some(model_id) => level.with_model(model_id.clone()), - None => level, + let mut level = RegistrationLevel::new(granularity); + if let Some(model_id) = &request.model_id { + level = level.with_model(model_id.clone()); } + if let Some(batch_id) = &request.batch_id { + level = level.with_batch(batch_id.clone()); + } + level } /// The item identifier, which exists only at item level. /// /// A model- or batch-level registration covers every unit it groups, so naming /// one contradicts the level the registry validates on submission. +/// +/// The serial is the one the passport's carrier prints. This used to be the +/// passport's own id, which matches no label, so an item registered under it +/// could not be found from the product. A request that reaches item level with +/// no serial yields no item identifier and fails validation, rather than being +/// completed with a value the product does not carry. pub(super) fn item_id_for(request: &RegistrationRequest) -> Option { match request.granularity { - RegistrationGranularity::Item => Some(ProductItemIdentifier { - scheme: "serial".into(), - value: request.passport_id.to_string(), - batch_id: None, - }), + RegistrationGranularity::Item => { + request + .item_serial + .as_ref() + .map(|serial| ProductItemIdentifier { + scheme: "serial".into(), + value: serial.clone(), + // The batch is linked once, on the level. + batch_id: None, + }) + } RegistrationGranularity::Model | RegistrationGranularity::Batch => None, } } @@ -271,7 +272,10 @@ pub(super) fn product_id_for(request: &RegistrationRequest) -> Result Result { + async fn register( + &self, + request: RegistrationRequest, + ) -> Result { let base_url = &self.config.endpoint.base_url; let product_id = product_id_for(&request)?; @@ -413,9 +417,10 @@ impl RegistrySyncPort for EuRegistrySync { passport_id = %passport_id, detail = %refusal_text(&body), "EU registry already holds this submission (idempotency key \ - replayed) — polling for its verdict instead of resubmitting" + replayed) — not resubmitting; the correlation identifier to \ + poll it by is not known" ); - return Ok(Self::held_by_registry()); + return Ok(SubmissionAcknowledgement::AlreadyHeld); } return Err(RetryableError::Fatal(format!( "registration rejected {status}: {}", @@ -429,7 +434,7 @@ impl RegistrySyncPort for EuRegistrySync { // error is safe because the retry carries the same key, which // the registry answers with the replay above — whereas a // retry without it would register the product twice. - let eu_resp: EuRegistryResponse = resp.json().await.map_err(|e| { + let receipt: SubmissionReceipt = resp.json().await.map_err(|e| { RetryableError::Fatal(format!( "the registry answered {status} with a body this node cannot read \ ({e}); the submission may have been accepted, and the retry \ @@ -438,20 +443,35 @@ impl RegistrySyncPort for EuRegistrySync { )) })?; - Ok(Self::response_to_record(&eu_resp)) + // The acknowledgement is a handle, never a registration: the + // registry rules on the submission afterwards, and only then + // generates the registration identifier. One that answers + // without a handle leaves nothing to poll, so it is treated + // like a body that could not be read. + if receipt.correlation_id.trim().is_empty() { + return Err(RetryableError::Fatal( + "the registry accepted the submission without a correlation \ + identifier; the retry carries the same Idempotency-Key so it is \ + recognised, not registered twice" + .into(), + )); + } + + Ok(SubmissionAcknowledgement::Accepted { + correlation_id: receipt.correlation_id, + }) } }) .await; match result { - Ok(record) => { + Ok(acknowledgement) => { tracing::info!( passport_id = %passport_id, - registry_id = %record.identifiers.registry_id, - status = ?record.status, - "passport registered with EU registry" + acknowledgement = ?acknowledgement, + "passport submitted to the EU registry" ); - Ok(record) + Ok(acknowledgement) } // Unreachable/fatal/exhausted-retry all surface as real errors — the // outbox keeps the row `pending` and retries. Never fake success. @@ -459,65 +479,71 @@ impl RegistrySyncPort for EuRegistrySync { } } - async fn check_status(&self, passport_id: PassportId) -> Result { - let base_url = &self.config.endpoint.base_url; - - self.with_retry(|| { - let url = format!("{base_url}{}", path_for(STATUS_PATH_TEMPLATE, passport_id)); - async move { - let token = self - .get_token() - .await - .map_err(|e| RetryableError::Fatal(format!("token acquisition failed: {e}")))?; - - let resp = self - .client - .get(&url) - .bearer_auth(&token) - .send() - .await - .map_err(|e| { - if e.is_connect() || e.is_timeout() { - RetryableError::Unreachable(e.to_string()) - } else { - RetryableError::Retryable(e.to_string()) - } - })?; - - let status_code = resp.status().as_u16(); - if status_code == 404 { - return Err(RetryableError::Fatal(format!( - "passport {passport_id} not found in EU registry" - ))); - } - if status_code == 429 { - return Err(RetryableError::Retryable("rate limited (429)".into())); - } - if (500..600).contains(&status_code) { - let body = resp.text().await.unwrap_or_default(); - return Err(RetryableError::Retryable(format!( - "server error {status_code}: {}", - refusal_text(&body) - ))); - } - if !resp.status().is_success() { - let body = resp.text().await.unwrap_or_default(); - return Err(RetryableError::Fatal(format!( - "status check failed {status_code}: {}", - refusal_text(&body) - ))); - } + #[tracing::instrument(skip(self))] + async fn check_submission(&self, correlation_id: &str) -> Result { + let dashboard = &self.config.endpoint.dashboard_base_url; + let tasks_url = url_for(dashboard, TASKS_PATH, "")?; + + // The registry's interface finds a submission's task by paging the + // operator's tasks: no route that takes a correlation identifier has + // been seen. The page cap bounds the walk for an operator with a long + // history, and a task past it is reported as not yet ruled on. + let mut found = None; + for page in 0..MAX_TASK_PAGES { + let query = [ + ("page", page.to_string()), + ("size", TASK_PAGE_SIZE.to_string()), + ]; + let tasks: TaskPage = self + .with_retry(|| self.get_json(&tasks_url, &query, "task list")) + .await + .map_err(RetryableError::into_dpp_error)?; + let listed = tasks.content.len(); + found = tasks + .content + .into_iter() + .find(|task| task.is_submission(correlation_id)); + if found.is_some() || listed < TASK_PAGE_SIZE { + break; + } + } + let Some(task) = found else { + // Not listed (yet). That is not a verdict, so the poll goes on. + return Ok(SubmissionReport::Processing); + }; - let status_resp: StatusResponse = resp - .json() - .await - .map_err(|e| RetryableError::Fatal(format!("invalid status response: {e}")))?; + // A task the registry has not finished has no result worth reading, and + // one whose state this node cannot name is reported as it stands. + if !matches!( + task.status, + Some(SubmissionOutcome::Success | SubmissionOutcome::Failure) + ) { + return Ok(report_for(&task, None)); + } + let Some(task_id) = task.id_segment() else { + return Ok(SubmissionReport::Unrecognised { + state: "a finished task with no identifier to read its result by".to_owned(), + }); + }; + let result_url = url_for(dashboard, TASK_RESULT_PATH_TEMPLATE, &task_id)?; + let result: TaskResult = self + .with_retry(|| self.get_json(&result_url, &[], "task result")) + .await + .map_err(RetryableError::into_dpp_error)?; + Ok(report_for(&task, Some(&result))) + } - Ok(Self::status_to_record(&status_resp)) - } - }) - .await - .map_err(|e| e.into_dpp_error()) + async fn check_status(&self, registration_id: &str) -> Result { + let url = url_for( + &self.config.endpoint.base_url, + STATUS_PATH_TEMPLATE, + registration_id, + )?; + let status_resp: StatusResponse = self + .with_retry(|| self.get_json(&url, &[], "status check")) + .await + .map_err(RetryableError::into_dpp_error)?; + Ok(Self::status_to_record(&status_resp)) } async fn notify_transfer( @@ -582,11 +608,11 @@ impl RegistrySyncPort for EuRegistrySync { ); } + // The route names the registered record, so it is built from the + // registration identifier, never from this node's passport id. + let url = url_for(base_url, TRANSFER_PATH_TEMPLATE, registry_id)?; self.with_retry(|| { - let url = format!( - "{base_url}{}", - path_for(TRANSFER_PATH_TEMPLATE, passport_id) - ); + let url = url.clone(); let notification = notification.clone(); async move { let token = self diff --git a/crates/dpp-node/src/infra/registry/mod.rs b/crates/dpp-node/src/infra/registry/mod.rs index d83c9ab5..22de71e1 100644 --- a/crates/dpp-node/src/infra/registry/mod.rs +++ b/crates/dpp-node/src/infra/registry/mod.rs @@ -2,6 +2,7 @@ mod client; mod config; +mod dashboard; mod mapping; mod token; diff --git a/crates/dpp-node/src/infra/registry/tests.rs b/crates/dpp-node/src/infra/registry/tests.rs index b1e9f04a..04405453 100644 --- a/crates/dpp-node/src/infra/registry/tests.rs +++ b/crates/dpp-node/src/infra/registry/tests.rs @@ -11,6 +11,7 @@ use dpp_domain::DppError; use dpp_domain::operator::{OperatorRole, ResponsibleOperator}; use dpp_domain::ports::registry_sync::{ RegistrationGranularity, RegistrationRequest, RegistryStatus, RegistrySyncPort, + SubmissionAcknowledgement, SubmissionReport, }; use dpp_domain::transfer::{TransferReason, TransferRecord}; @@ -53,11 +54,15 @@ fn response_to_record_maps_status_correctly() { }; let record = EuRegistrySync::response_to_record(&resp); assert_eq!(record.status, RegistryStatus::Registered); - assert_eq!(record.identifiers.registry_id, "EU-REG-2026-00001"); + assert_eq!(record.registration_id, "EU-REG-2026-00001"); } +/// `rejected` and `pending` were states of a submission, and the port reports +/// those as a [`SubmissionReport`]. A *record* that answers with one has a state +/// the port has no name for, and carries it as the registry said it rather than +/// reading it as one of the three it does name. #[test] -fn response_to_record_maps_rejected() { +fn a_record_reply_of_rejected_is_carried_as_the_registry_said_it() { let resp = EuRegistryResponse { registry_id: "EU-REG-2026-00002".into(), passport_id: Uuid::nil(), @@ -67,11 +72,14 @@ fn response_to_record_maps_rejected() { updated_at: Utc::now(), }; let record = EuRegistrySync::response_to_record(&resp); - assert_eq!(record.status, RegistryStatus::Rejected); + assert_eq!( + record.status, + RegistryStatus::Unrecognised("rejected".into()) + ); } #[test] -fn status_to_record_maps_pending() { +fn a_status_reply_of_pending_is_carried_as_the_registry_said_it() { let resp = StatusResponse { registry_id: "EU-REG-2026-00003".into(), status: RegistryStatusCode::Pending, @@ -79,8 +87,11 @@ fn status_to_record_maps_pending() { message: None, }; let record = EuRegistrySync::status_to_record(&resp); - assert_eq!(record.status, RegistryStatus::Pending); - assert_eq!(record.identifiers.registry_id, "EU-REG-2026-00003"); + assert_eq!( + record.status, + RegistryStatus::Unrecognised("pending".into()) + ); + assert_eq!(record.registration_id, "EU-REG-2026-00003"); } /// A registration request with realistic non-facility fields. Pair it with a @@ -109,6 +120,8 @@ fn request_with_facility(facility: Option) -> Regi model_id: None, commodity_code: Some("85076000".into()), backup_url: None, + batch_id: None, + item_serial: Some("abc123".into()), product_identifier: Some(dpp_domain::ProductIdentifier::gs1( dpp_domain::Gtin::parse("09506000134352").unwrap(), )), @@ -243,7 +256,8 @@ mod mock_server { routing::{get, post}, }; use dpp_registry::{ - IDEMPOTENCY_KEY_HEADER, REGISTRATION_PATH, STATUS_PATH_TEMPLATE, TRANSFER_PATH_TEMPLATE, + IDEMPOTENCY_KEY_HEADER, REGISTRATION_PATH, STATUS_PATH_TEMPLATE, TASK_RESULT_PATH_TEMPLATE, + TASKS_PATH, TRANSFER_PATH_TEMPLATE, }; use serde_json::Value; use tokio::sync::Mutex; @@ -263,6 +277,14 @@ mod mock_server { /// cannot see it. pub(super) register_keys: Mutex>>, pub(super) status_queue: Mutex>, + /// Pages of the dashboard's task list, one popped per request. + pub(super) tasks_queue: Mutex>, + /// Dashboard task results, one popped per request. + pub(super) task_result_queue: Mutex>, + /// The task identifier each result was read by, and the `page` each + /// task-list request asked for. + pub(super) task_results_read: Mutex>, + pub(super) task_pages_asked: Mutex>, pub(super) transfer_queue: Mutex>, /// Bodies the registry actually received on `/transfer`. Asserting on /// the wire body is the only way to catch a field the adapter silently @@ -315,6 +337,28 @@ mod mock_server { pop_or_500(&state.status_queue).await } + async fn tasks_handler( + State(state): State>, + axum::extract::Query(query): axum::extract::Query< + std::collections::HashMap, + >, + ) -> Response { + state + .task_pages_asked + .lock() + .await + .push_back(query.get("page").cloned().unwrap_or_default()); + pop_or_500(&state.tasks_queue).await + } + + async fn task_result_handler( + State(state): State>, + Path(id): Path, + ) -> Response { + state.task_results_read.lock().await.push_back(id); + pop_or_500(&state.task_result_queue).await + } + async fn transfer_handler( State(state): State>, Path(_id): Path, @@ -335,6 +379,14 @@ mod mock_server { .route(REGISTRATION_PATH, post(register_handler)) .route(STATUS_PATH_TEMPLATE, get(status_handler)) .route(TRANSFER_PATH_TEMPLATE, post(transfer_handler)) + // The dashboard is a separate service of the registry, on its own + // base URL, and a submission's outcome is read from it. + .nest( + "/dashboard", + Router::new() + .route(TASKS_PATH, get(tasks_handler)) + .route(TASK_RESULT_PATH_TEMPLATE, get(task_result_handler)), + ) .with_state(state); let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); @@ -356,6 +408,7 @@ fn mock_config(base_url: &str) -> EuRegistrySyncConfig { api_version: "1.0".into(), mtls_required: false, token_endpoint: Some(format!("{base_url}/token")), + dashboard_base_url: format!("{base_url}/dashboard"), }, client_id: "test-client".into(), client_secret: "test-secret".into(), @@ -375,24 +428,41 @@ fn registered_response(registry_id: &str) -> serde_json::Value { }) } +/// What the registry answers a submission with: a handle on the check it will +/// make, never a registration. +fn accepted_response(correlation_id: &str) -> serde_json::Value { + serde_json::json!({ + "correlationId": correlation_id, + "outcome": "PROCESSING", + }) +} + +/// The acknowledgement a submission under `correlation_id` produces. +fn accepted(correlation_id: &str) -> SubmissionAcknowledgement { + SubmissionAcknowledgement::Accepted { + correlation_id: correlation_id.to_owned(), + } +} + #[tokio::test] -async fn register_succeeds_and_maps_response() { +async fn register_succeeds_and_returns_the_correlation_id() { let state = Arc::new(MockState::default()); state .register_queue .lock() .await - .push_back((axum::http::StatusCode::OK, registered_response("EU-REG-1"))); + .push_back((axum::http::StatusCode::OK, accepted_response("CORR-1"))); let base_url = mock_server::spawn(state.clone()).await; let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); - let record = sync + let acknowledgement = sync .register(valid_request()) .await .expect("register should succeed"); - assert_eq!(record.status, RegistryStatus::Registered); - assert_eq!(record.identifiers.registry_id, "EU-REG-1"); + // A submission is acknowledged with a handle, not registered: the registry + // rules on it afterwards, and only then generates a registration identifier. + assert_eq!(acknowledgement, accepted("CORR-1")); assert_eq!(state.register_hits.load(Ordering::SeqCst), 1); } @@ -462,17 +532,17 @@ async fn register_retries_on_429_then_succeeds() { axum::http::StatusCode::TOO_MANY_REQUESTS, serde_json::json!({}), )); - q.push_back((axum::http::StatusCode::OK, registered_response("EU-REG-2"))); + q.push_back((axum::http::StatusCode::OK, accepted_response("CORR-2"))); } let base_url = mock_server::spawn(state.clone()).await; let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); - let record = sync + let acknowledgement = sync .register(valid_request()) .await .expect("should succeed after one retry"); - assert_eq!(record.status, RegistryStatus::Registered); + assert_eq!(acknowledgement, accepted("CORR-2")); assert_eq!(state.register_hits.load(Ordering::SeqCst), 2); } @@ -507,20 +577,46 @@ async fn check_status_success() { axum::http::StatusCode::OK, serde_json::json!({ "registryId": "EU-REG-3", - "status": "pending", + "status": "registered", "updatedAt": Utc::now().to_rfc3339(), }), )); let base_url = mock_server::spawn(state.clone()).await; let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); + // Asked by the registration identifier, which the registry generated: it + // never received this node's passport id. let record = sync - .check_status(PassportId::new()) + .check_status("EU-REG-3") .await .expect("check_status should succeed"); - assert_eq!(record.status, RegistryStatus::Pending); - assert_eq!(record.identifiers.registry_id, "EU-REG-3"); + assert_eq!(record.status, RegistryStatus::Registered); + assert_eq!(record.registration_id, "EU-REG-3"); +} + +/// A registration identifier is a URI in the registry's own guide. It is one +/// path segment, so a `/` in it cannot add segments to the route. +#[tokio::test] +async fn a_registration_id_is_escaped_into_one_path_segment() { + let state = Arc::new(MockState::default()); + state.status_queue.lock().await.push_back(( + axum::http::StatusCode::OK, + serde_json::json!({ + "registryId": "https://reg.example/dpp/1", + "status": "registered", + "updatedAt": Utc::now().to_rfc3339(), + }), + )); + let base_url = mock_server::spawn(state.clone()).await; + let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); + + let record = sync + .check_status("https://reg.example/dpp/1") + .await + .expect("the route matches only if the id stayed one segment"); + + assert_eq!(record.registration_id, "https://reg.example/dpp/1"); } #[tokio::test] @@ -535,16 +631,205 @@ async fn check_status_404_is_fatal_not_found() { let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); let err = sync - .check_status(PassportId::new()) + .check_status("EU-REG-3") .await .expect_err("404 should surface as not-found"); assert!( - err.to_string().contains("not found in EU registry"), + err.to_string().contains("status check failed 404"), "got: {err}" ); } +// ── Reading a submission back ──────────────────────────────────────────────── + +fn task_page(tasks: serde_json::Value) -> (axum::http::StatusCode, serde_json::Value) { + ( + axum::http::StatusCode::OK, + serde_json::json!({ "content": tasks }), + ) +} + +fn registration_task(correlation_id: &str, task_id: u64, status: &str) -> serde_json::Value { + serde_json::json!({ + "taskId": task_id, + "type": "DPP_REGISTRATION", + "correlationId": correlation_id, + "status": status, + }) +} + +#[tokio::test] +async fn a_submission_still_being_checked_is_processing_and_reads_no_result() { + let state = Arc::new(MockState::default()); + state + .tasks_queue + .lock() + .await + .push_back(task_page(serde_json::json!([registration_task( + "CORR-1", + 7, + "PROCESSING" + )]))); + let base_url = mock_server::spawn(state.clone()).await; + let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); + + let report = sync.check_submission("CORR-1").await.expect("a report"); + + assert_eq!(report, SubmissionReport::Processing); + assert!( + state.task_results_read.lock().await.is_empty(), + "an unfinished task has no result worth reading" + ); +} + +#[tokio::test] +async fn a_successful_submission_reports_the_registration_identifier_from_its_result() { + let state = Arc::new(MockState::default()); + state + .tasks_queue + .lock() + .await + .push_back(task_page(serde_json::json!([ + registration_task("CORR-OTHER", 6, "SUCCESS"), + registration_task("CORR-1", 7, "SUCCESS"), + ]))); + state.task_result_queue.lock().await.push_back(( + axum::http::StatusCode::OK, + serde_json::json!({ + "status": "SUCCESS", + "resultPayload": { "report": { "itemDetails": [{ + "uniqueProductIdentifier": "https://id.example.com/01/09506000134352/21/abc123", + "outcome": "ACCEPTED", + "registrationIdentifier": "EU-REG-7", + }] } }, + }), + )); + let base_url = mock_server::spawn(state.clone()).await; + let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); + + let report = sync.check_submission("CORR-1").await.expect("a report"); + + assert_eq!( + report, + SubmissionReport::Registered { + registration_id: "EU-REG-7".into() + } + ); + assert_eq!( + state + .task_results_read + .lock() + .await + .iter() + .collect::>(), + ["7"], + "the result is read by the task's own identifier, not the correlation id" + ); +} + +#[tokio::test] +async fn a_failed_submission_is_refused_with_the_registrys_error_code() { + let state = Arc::new(MockState::default()); + state + .tasks_queue + .lock() + .await + .push_back(task_page(serde_json::json!([registration_task( + "CORR-1", 7, "FAILURE" + )]))); + state.task_result_queue.lock().await.push_back(( + axum::http::StatusCode::OK, + serde_json::json!({ + "status": "FAILURE", + "resultPayload": { "report": { "itemDetails": [{ + "uniqueProductIdentifier": "https://id.example.com/01/09506000134352/21/abc123", + "outcome": "REJECTED", + "errorCode": "NOT_RESOLVABLE_UPI", + }] } }, + }), + )); + let base_url = mock_server::spawn(state.clone()).await; + let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); + + let SubmissionReport::Refused { reason } = + sync.check_submission("CORR-1").await.expect("a report") + else { + panic!("a failed submission is refused"); + }; + assert!(reason.contains("NOT_RESOLVABLE_UPI"), "{reason}"); +} + +/// A submission the list does not show (yet) is not a verdict either way. +#[tokio::test] +async fn a_submission_the_dashboard_does_not_list_is_still_processing() { + let state = Arc::new(MockState::default()); + state + .tasks_queue + .lock() + .await + .push_back(task_page(serde_json::json!([registration_task( + "CORR-ELSE", + 1, + "SUCCESS" + )]))); + let base_url = mock_server::spawn(state.clone()).await; + let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); + + assert_eq!( + sync.check_submission("CORR-1").await.expect("a report"), + SubmissionReport::Processing + ); +} + +/// The task is found by paging the list, since no route takes a correlation +/// identifier: a full page that does not hold it means asking for the next. +#[tokio::test] +async fn the_task_list_is_paged_until_the_submission_is_found() { + let state = Arc::new(MockState::default()); + let full_page: Vec<_> = (0..50) + .map(|n| registration_task(&format!("CORR-FILL-{n}"), n, "SUCCESS")) + .collect(); + { + let mut q = state.tasks_queue.lock().await; + q.push_back(task_page(serde_json::Value::Array(full_page))); + q.push_back(task_page(serde_json::json!([registration_task( + "CORR-1", + 99, + "PROCESSING" + )]))); + } + let base_url = mock_server::spawn(state.clone()).await; + let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); + + let report = sync.check_submission("CORR-1").await.expect("a report"); + + assert_eq!(report, SubmissionReport::Processing); + assert_eq!( + state + .task_pages_asked + .lock() + .await + .iter() + .collect::>(), + ["0", "1"] + ); +} + +#[tokio::test] +async fn an_unreachable_dashboard_is_an_error_not_a_verdict() { + let state = Arc::new(MockState::default()); + // Nothing queued: the mock answers 500 on every attempt. + let base_url = mock_server::spawn(state.clone()).await; + let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); + + let err = sync + .check_submission("CORR-1") + .await + .expect_err("a registry that cannot be read says nothing about the submission"); + assert!(err.to_string().contains("failed after 3 attempts"), "{err}"); +} + #[tokio::test] async fn notify_transfer_success() { let state = Arc::new(MockState::default()); @@ -601,19 +886,19 @@ async fn invalid_payload_is_submitted_when_the_override_is_set() { .register_queue .lock() .await - .push_back((axum::http::StatusCode::OK, registered_response("EU-REG-9"))); + .push_back((axum::http::StatusCode::OK, accepted_response("CORR-9"))); let base_url = mock_server::spawn(state.clone()).await; let mut config = mock_config(&base_url); config.allow_invalid_payloads = true; let sync = EuRegistrySync::new(config).unwrap(); - let record = sync + let acknowledgement = sync .register(invalid_request()) .await .expect("the override must permit submission"); - assert_eq!(record.identifiers.registry_id, "EU-REG-9"); + assert_eq!(acknowledgement, accepted("CORR-9")); assert_eq!(state.register_hits.load(Ordering::SeqCst), 1); } @@ -639,17 +924,17 @@ async fn a_registration_this_node_builds_passes_validation() { .register_queue .lock() .await - .push_back((axum::http::StatusCode::OK, registered_response("EU-REG-OK"))); + .push_back((axum::http::StatusCode::OK, accepted_response("CORR-OK"))); let base_url = mock_server::spawn(state.clone()).await; // No override: this must succeed on the fail-closed path. let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); - let record = sync + let acknowledgement = sync .register(valid_request()) .await .expect("a fully-populated registration must pass local validation"); - assert_eq!(record.identifiers.registry_id, "EU-REG-OK"); + assert_eq!(acknowledgement, accepted("CORR-OK")); assert_eq!(state.register_hits.load(Ordering::SeqCst), 1); } @@ -697,6 +982,52 @@ fn only_item_level_registrations_carry_an_item_identifier() { } } +/// 🚨 The item is registered under the serial its carrier prints, never under +/// this node's passport id. The id matches no label, so an item registered under +/// it could not be found from the product. +#[test] +fn an_item_is_registered_under_its_carrier_serial_not_the_passport_id() { + let request = valid_request(); + let item = item_id_for(&request).expect("an item-level request names its unit"); + assert_eq!(item.scheme, "serial"); + assert_eq!(item.value, "abc123"); + assert_ne!(item.value, request.passport_id.to_string()); +} + +/// An item-level request with no serial is not completed with one made up: it +/// has no item identifier, and validation refuses the payload. +#[tokio::test] +async fn an_item_without_a_serial_is_refused_rather_than_given_one() { + let request = RegistrationRequest { + item_serial: None, + ..valid_request() + }; + assert!(item_id_for(&request).is_none()); + + let state = Arc::new(MockState::default()); + let base_url = mock_server::spawn(state.clone()).await; + let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); + let err = sync + .register(request) + .await + .expect_err("an item with no identifier must be refused"); + assert!(matches!(err, DppError::Validation(_)), "got: {err:?}"); + assert_eq!(state.register_hits.load(Ordering::SeqCst), 0); +} + +/// Art. 8(4) and 8(5) link a batch where one exists; the request carries it at +/// batch and item level. +#[test] +fn the_batch_the_request_carries_is_linked_on_the_level() { + let mut request = valid_request(); + assert!(level_for(&request).batch_id.is_none()); + + request.batch_id = Some("LOT-2026-01".into()); + let level = level_for(&request); + assert_eq!(level.batch_id.as_deref(), Some("LOT-2026-01")); + assert!(level.validate().is_ok()); +} + /// An unlinked model must stay absent rather than becoming a blank identifier, /// which validation refuses. #[test] @@ -859,7 +1190,7 @@ async fn a_vat_operator_is_not_submitted_as_a_did() { .register_queue .lock() .await - .push_back((axum::http::StatusCode::OK, registered_response("EU-REG-V"))); + .push_back((axum::http::StatusCode::OK, accepted_response("CORR-V"))); let base_url = mock_server::spawn(state.clone()).await; let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); @@ -895,7 +1226,7 @@ async fn a_did_operator_keeps_its_did_field() { .register_queue .lock() .await - .push_back((axum::http::StatusCode::OK, registered_response("EU-REG-D"))); + .push_back((axum::http::StatusCode::OK, accepted_response("CORR-D"))); let base_url = mock_server::spawn(state.clone()).await; let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); @@ -972,26 +1303,6 @@ fn a_counterparty_without_an_eu_identifier_falls_back_to_its_did() { // ── Status fidelity and idempotency ───────────────────────────────────────── -/// A registration the registry has accepted but not yet ruled on must map to -/// `Pending`, not success. Registration is validated asynchronously, so this is -/// the *expected* response to a submission — the drain turning it into -/// `registered` recorded every submission as complete. -#[test] -fn a_pending_response_stays_pending() { - let resp = EuRegistryResponse { - registry_id: "EU-REG-P".into(), - passport_id: Uuid::nil(), - status: RegistryStatusCode::Pending, - message: None, - rejection_reasons: None, - updated_at: Utc::now(), - }; - assert_eq!( - EuRegistrySync::response_to_record(&resp).status, - RegistryStatus::Pending - ); -} - /// A deactivated record is not a rejected one. Rejection means the submission /// was defective and can be corrected; deactivation means the record is out of /// service, which resubmitting does not fix. @@ -1007,7 +1318,7 @@ fn a_deactivated_record_is_not_reported_as_rejected() { }; let mapped = EuRegistrySync::response_to_record(&resp).status; assert_eq!(mapped, RegistryStatus::Deactivated); - assert_ne!(mapped, RegistryStatus::Rejected); + assert_ne!(mapped, RegistryStatus::Unrecognised("rejected".into())); let status = StatusResponse { registry_id: "EU-REG-D".into(), @@ -1037,7 +1348,7 @@ async fn retrying_a_registration_reuses_its_request_id() { serde_json::json!({"error": "upstream"}), )); } - q.push_back((axum::http::StatusCode::OK, registered_response("EU-REG-I"))); + q.push_back((axum::http::StatusCode::OK, accepted_response("CORR-I"))); } let base_url = mock_server::spawn(state.clone()).await; let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); @@ -1098,7 +1409,7 @@ async fn a_registration_carries_its_request_id_as_the_idempotency_key() { axum::http::StatusCode::INTERNAL_SERVER_ERROR, serde_json::json!({"error": "upstream"}), )); - q.push_back((axum::http::StatusCode::OK, registered_response("EU-REG-K"))); + q.push_back((axum::http::StatusCode::OK, accepted_response("CORR-K"))); } let base_url = mock_server::spawn(state.clone()).await; let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); @@ -1129,16 +1440,15 @@ async fn a_replayed_key_means_the_registry_already_has_the_submission() { let base_url = mock_server::spawn(state.clone()).await; let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); - let record = sync + let acknowledgement = sync .register(valid_request()) .await .expect("a replayed key is not a failure"); - assert_eq!(record.status, RegistryStatus::Pending); - assert!( - record.identifiers.registry_id.is_empty(), - "the registry returned no id, and none may be invented" - ); + // The registry's reply to a replayed key names the conflict, not the + // submission, so there is no correlation identifier to report and none may + // be invented. + assert_eq!(acknowledgement, SubmissionAcknowledgement::AlreadyHeld); assert_eq!( state.register_hits.load(Ordering::SeqCst), 1, @@ -1187,7 +1497,7 @@ async fn a_success_body_this_node_cannot_read_resolves_when_the_key_is_replayed( let mut q = state.register_queue.lock().await; q.push_back(( axum::http::StatusCode::OK, - serde_json::json!({"correlationId": "c-1", "outcome": "SOMETHING_NEW"}), + serde_json::json!({"somethingElse": "the registry changed its mind about the shape"}), )); q.push_back((axum::http::StatusCode::CONFLICT, key_reused_body())); } @@ -1213,7 +1523,7 @@ async fn a_success_body_this_node_cannot_read_resolves_when_the_key_is_replayed( .register(attempt()) .await .expect("the replay is recognised, not refused"); - assert_eq!(second.status, RegistryStatus::Pending); + assert_eq!(second, SubmissionAcknowledgement::AlreadyHeld); let keys = state.register_keys.lock().await; assert_eq!( @@ -1318,7 +1628,7 @@ async fn a_backup_link_with_its_provider_is_sent_with_both() { .register_queue .lock() .await - .push_back((axum::http::StatusCode::OK, registered_response("EU-REG-B"))); + .push_back((axum::http::StatusCode::OK, accepted_response("CORR-B"))); let base_url = mock_server::spawn(state.clone()).await; let sync = EuRegistrySync::new(mock_config(&base_url)).unwrap(); diff --git a/crates/dpp-node/src/infra/registry_drain.rs b/crates/dpp-node/src/infra/registry_drain.rs index dbf5feb0..8519882a 100644 --- a/crates/dpp-node/src/infra/registry_drain.rs +++ b/crates/dpp-node/src/infra/registry_drain.rs @@ -1,14 +1,30 @@ //! One drain pass over the registry-sync outbox. //! -//! Fetches due rows, registers each against the `RegistrySyncPort`, and records -//! the terminal (`registered`/`rejected`) or transient (backoff) outcome on the +//! Fetches due rows and moves each one along the registry's two steps through +//! the `RegistrySyncPort`: a pending row is submitted and, once the registry has +//! accepted it, polled by the correlation identifier it was given until the +//! registry rules on it. The ruling is recorded as the terminal +//! (`registered`/`rejected`) outcome, and a transient failure as a backoff, on the //! row. Extracted from the node's background loop so the drain semantics are //! unit-testable with a mock port — the loop in `main` just calls this on a //! timer and refreshes the gauges. +//! +//! # What the row's `registry_id` holds +//! +//! One column holds the registry's handle on the row, and which handle depends +//! on the step. While the row is `submitted` it is the **correlation +//! identifier** the registry gave the submission, which is what is polled. +//! Once the row is `registered` it is the **registration identifier** the +//! registry generated (IR (EU) 2026/1778 Art. 8(8)), which every later call +//! about the record names it by. The two are different identifiers of different +//! things, and the status says which one the column holds: a reader must check +//! the status before using it. use std::sync::Arc; -use dpp_domain::ports::registry_sync::{RegistrationRequest, RegistryStatus, RegistrySyncPort}; +use dpp_domain::ports::registry_sync::{ + RegistrationRequest, RegistrySyncPort, SubmissionAcknowledgement, SubmissionReport, +}; use dpp_types::{RegistrySyncOutbox, RegistrySyncStatus}; /// Outcome tallies for one drain pass — surfaced to metrics and asserted in tests. @@ -20,8 +36,6 @@ pub struct DrainStats { pub submitted: u32, /// Rows the registry terminally `rejected`. pub rejected: u32, - /// Rows the registry withdrew from service. - pub deactivated: u32, /// Rows that failed transiently and were backed off for retry. pub retried: u32, /// Rows dropped from draining because their payload was missing/corrupt. @@ -71,13 +85,35 @@ pub async fn drain_once( let pid = row.passport_id; // Already submitted: the registry has it and is validating. Poll for the - // verdict — resubmitting would register the same product twice. + // verdict by the correlation identifier it gave — resubmitting would + // register the same product twice. if row.status == RegistrySyncStatus::Submitted { + let Some(correlation_id) = row + .registry_id + .as_deref() + .map(str::trim) + .filter(|id| !id.is_empty()) + else { + // The registry held the submission already (a replayed + // idempotency key) and did not say under which handle. There is + // nothing to poll, and inventing a handle or falling back to this + // node's own passport id would ask the registry about something + // it never received. The row stays `submitted` until its + // correlation identifier is recovered from the registry's own + // record of the operator's requests. + tracing::warn!( + passport_id = %pid, + "registry holds this submission under a correlation identifier this node \ + does not have — it cannot be polled and needs reconciling" + ); + stats.submitted += 1; + continue; + }; let started = std::time::Instant::now(); - let outcome = registry_sync.check_status(pid).await; + let outcome = registry_sync.check_submission(correlation_id).await; metrics::histogram!("registry_outbox_drain_seconds") .record(started.elapsed().as_secs_f64()); - apply_outcome(outbox, pid, outcome, &mut stats).await; + apply_report(outbox, pid, outcome, &mut stats).await; continue; } @@ -104,54 +140,83 @@ pub async fn drain_once( let outcome = registry_sync.register(req).await; metrics::histogram!("registry_outbox_drain_seconds") .record(started.elapsed().as_secs_f64()); - apply_outcome(outbox, pid, outcome, &mut stats).await; + apply_acknowledgement(outbox, pid, outcome, &mut stats).await; } stats } -/// Record what the registry said about a row, whether that came from submitting -/// it or from polling an outstanding submission — the verdicts are the same set -/// either way. -async fn apply_outcome( +/// Record what the registry said when the row was submitted. +/// +/// An acknowledgement is never a registration: the registry accepted the +/// submission for checking and gave it a handle, and the ruling comes later. +async fn apply_acknowledgement( outbox: &Arc, pid: dpp_domain::passport::PassportId, - outcome: Result, + outcome: Result, stats: &mut DrainStats, ) { match outcome { - Ok(rec) if rec.status == RegistryStatus::Rejected => { - tracing::warn!(passport_id = %pid, "registry rejected registration"); - metrics::counter!("registry_outbox_rejected_total").increment(1); - let _ = outbox - .mark_rejected(pid, "registry rejected registration".into()) - .await; - stats.rejected += 1; - } - Ok(rec) if rec.status == RegistryStatus::Deactivated => { - // Not a defect in what we sent, so not a rejection: the record was - // withdrawn from service and resubmitting would not fix anything. - tracing::warn!(passport_id = %pid, "registry deactivated the registration"); - metrics::counter!("registry_outbox_deactivated_total").increment(1); - let _ = outbox - .mark_deactivated(pid, "registry deactivated the registration".into()) - .await; - stats.deactivated += 1; + Ok(SubmissionAcknowledgement::Accepted { correlation_id }) => { + let _ = outbox.mark_submitted(pid, correlation_id).await; + stats.submitted += 1; } - // The registry has the submission and is still validating it. This is - // the *expected* response to a fresh registration, not an edge case — - // treating it as success is what recorded every submission as complete. - Ok(rec) if rec.status == RegistryStatus::Pending => { - let _ = outbox - .mark_submitted(pid, rec.identifiers.registry_id) - .await; + // The registry already had the submission. An empty handle tells the + // outbox to keep whatever the row held and never to record `''`. + Ok(SubmissionAcknowledgement::AlreadyHeld) => { + let _ = outbox.mark_submitted(pid, String::new()).await; stats.submitted += 1; } - Ok(rec) => { + // An answer this node has no name for is not an acceptance: record it as + // a failed attempt, so the retry carries the same idempotency key and the + // registry says what it holds. + Ok(other) => { let _ = outbox - .mark_registered(pid, rec.identifiers.registry_id) + .mark_attempt_failed(pid, format!("unrecognised acknowledgement: {other:?}")) .await; + stats.retried += 1; + } + Err(e) => { + // Transient/unreachable — back off and retry. The row stays. + let _ = outbox.mark_attempt_failed(pid, e.to_string()).await; + stats.retried += 1; + } + } +} + +/// Record what the registry reported about a submission it accepted earlier. +async fn apply_report( + outbox: &Arc, + pid: dpp_domain::passport::PassportId, + outcome: Result, + stats: &mut DrainStats, +) { + match outcome { + Ok(SubmissionReport::Registered { registration_id }) => { + let _ = outbox.mark_registered(pid, registration_id).await; stats.registered += 1; } + Ok(SubmissionReport::Refused { reason }) => { + tracing::warn!(passport_id = %pid, reason = %reason, "registry rejected registration"); + metrics::counter!("registry_outbox_rejected_total").increment(1); + let _ = outbox.mark_rejected(pid, reason).await; + stats.rejected += 1; + } + // A state this node has no name for is not a verdict. Reading it as a + // refusal would discard a submission that may have succeeded, and as a + // registration would record one that may not exist, so the row stays + // `submitted` and the next pass asks again. + Ok(SubmissionReport::Unrecognised { state }) => { + tracing::warn!( + passport_id = %pid, + state = %state, + "registry reported a submission state this node has no name for — polling on" + ); + stats.submitted += 1; + } + // Still being checked: the expected answer to a fresh submission, not an + // edge case. Anything the report type grows later is read the same way, + // as not yet ruled on. + Ok(_) => stats.submitted += 1, Err(e) => { // Transient/unreachable — back off and retry. The row stays. let _ = outbox.mark_attempt_failed(pid, e.to_string()).await; diff --git a/crates/dpp-node/src/infra/ruleset.rs b/crates/dpp-node/src/infra/ruleset.rs index d5acc97d..ae18153e 100644 --- a/crates/dpp-node/src/infra/ruleset.rs +++ b/crates/dpp-node/src/infra/ruleset.rs @@ -115,7 +115,14 @@ pub fn sign_bundle( content_sha256: content_hash(&content)?, }; let manifest_value = serde_json::to_value(&manifest)?; - let manifest_jws = jws::sign(store, key_id, &manifest_value)?; + // A bundle is verified against the publisher's key as the node is configured + // with it, not through a DID document, so the `kid` names the key by its own + // thumbprint: the identifier that stays with the key whatever else changes. + let kid = store + .public_key(key_id) + .ok_or_else(|| anyhow::anyhow!("no key found for {key_id}"))? + .thumbprint_uri()?; + let manifest_jws = jws::sign(store, key_id, &manifest_value, &kid)?; Ok(SignedBundle { manifest_jws, content, diff --git a/crates/dpp-node/src/infra/transfer_drain.rs b/crates/dpp-node/src/infra/transfer_drain.rs index ec95691c..293371a2 100644 --- a/crates/dpp-node/src/infra/transfer_drain.rs +++ b/crates/dpp-node/src/infra/transfer_drain.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use dpp_domain::ports::registry_sync::{RegistryStatus, RegistrySyncPort}; use dpp_domain::transfer::TransferRecord; -use dpp_types::{RegistrySyncOutbox, RegistryTransferOutbox}; +use dpp_types::{RegistrySyncOutbox, RegistrySyncStatus, RegistryTransferOutbox}; /// Outcome tallies for one drain pass — surfaced to metrics and asserted in tests. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] @@ -31,6 +31,18 @@ pub struct TransferDrainStats { pub deferred: u32, } +/// Whether the registry answered a transfer notification with a refusal. +/// +/// A record has no `rejected` state: that was a state of a submission, and the +/// port reports it as a [`SubmissionReport`](dpp_domain::ports::registry_sync::SubmissionReport). +/// The registry's `rejected` reply to a *notification* is still a refusal of it, +/// and arrives as the state the port has no name for, carrying the registry's +/// word. Read as anything else it would be retried for as long as the registry +/// keeps saying no. +fn is_rejection(status: &RegistryStatus) -> bool { + matches!(status, RegistryStatus::Unrecognised(state) if state.eq_ignore_ascii_case("rejected")) +} + /// Drain up to `batch` due rows once. /// /// Never panics and never propagates: a per-row failure is recorded on the row @@ -71,15 +83,20 @@ pub async fn drain_once( continue; } }; - // Which registry record does this handover amend? Only the registration - // response can say. No id yet means the registration has not been - // accepted — hold the row rather than sending a notification the - // registry cannot attach to anything. + // Which registry record does this handover amend? Only the ruling on the + // registration can say. No registration identifier yet means the + // registration has not been accepted — hold the row rather than sending a + // notification the registry cannot attach to anything. + // + // 🚨 Only a `registered` row holds that identifier. While a row is + // `submitted` the same column holds the submission's correlation + // identifier, which names no record and must never be sent as one. let registry_id = registrations .pending_for(row.passport_id) .await .ok() .flatten() + .filter(|r| r.status == RegistrySyncStatus::Registered) .and_then(|r| r.registry_id) .filter(|id| !id.trim().is_empty()); let Some(registry_id) = registry_id else { @@ -102,7 +119,7 @@ pub async fn drain_once( metrics::histogram!("registry_transfer_drain_seconds") .record(started.elapsed().as_secs_f64()); match outcome { - Ok(rec) if rec.status == RegistryStatus::Rejected => { + Ok(rec) if is_rejection(&rec.status) => { tracing::warn!( passport_id = %row.passport_id, transfer_id = %tid, @@ -115,7 +132,7 @@ pub async fn drain_once( stats.rejected += 1; } Ok(rec) => { - let _ = outbox.mark_notified(tid, rec.identifiers.registry_id).await; + let _ = outbox.mark_notified(tid, rec.registration_id).await; stats.notified += 1; } Err(e) => { @@ -139,7 +156,7 @@ mod tests { use dpp_domain::operator::{OperatorRole, ResponsibleOperator}; use dpp_domain::passport::PassportId; use dpp_domain::ports::registry_sync::{ - RegistrationRequest, RegistryIdentifiers, RegistryRecord, + RegistrationRequest, RegistryRecord, SubmissionAcknowledgement, SubmissionReport, }; use dpp_domain::transfer::{TransferChain, TransferReason}; use dpp_types::{ @@ -247,9 +264,12 @@ mod tests { } /// Stands in for the registration outbox. `registry_id` is `None` when the - /// passport's own registration has not been accepted yet. + /// passport's own registration has not been accepted yet, and `status` is + /// where the row stands: a `submitted` row holds the submission's + /// correlation identifier in the same column. struct FakeRegistrations { registry_id: Option, + status: RegistrySyncStatus, } #[async_trait] @@ -312,7 +332,7 @@ mod tests { ) -> Result, DppError> { Ok(Some(RegistrySyncRow { passport_id, - status: RegistrySyncStatus::Registered, + status: self.status, status_intent: None, registry_id: self.registry_id.clone(), payload: None, @@ -350,10 +370,13 @@ mod tests { async fn register( &self, _request: RegistrationRequest, - ) -> Result { + ) -> Result { unreachable!("the transfer drain never registers") } - async fn check_status(&self, _pid: PassportId) -> Result { + async fn check_submission(&self, _id: &str) -> Result { + unreachable!("the transfer drain never polls a submission") + } + async fn check_status(&self, _registration_id: &str) -> Result { unreachable!("the transfer drain never checks status") } async fn notify_transfer( @@ -369,18 +392,15 @@ mod tests { // A notified transfer leaves the registration `Registered` — the // registry amends the record, it does not restatus it. Outcome::Notified => RegistryStatus::Registered, - Outcome::Rejected => RegistryStatus::Rejected, + // A record has no `rejected` state; the registry's refusal of a + // notification arrives as the state the port has no name for. + Outcome::Rejected => RegistryStatus::Unrecognised("rejected".into()), Outcome::Transient => { return Err(DppError::Internal("registry unreachable".into())); } }; Ok(RegistryRecord { - identifiers: RegistryIdentifiers { - product_id: String::new(), - operator_id: String::new(), - facility_id: String::new(), - registry_id: "EU-REG-T".into(), - }, + registration_id: "EU-REG-T".into(), status, registered_at: Utc::now(), updated_at: Utc::now(), @@ -411,8 +431,19 @@ mod tests { port: &Arc, registry_id: Option, ) -> TransferDrainStats { - let registrations: Arc = - Arc::new(FakeRegistrations { registry_id }); + run_with_row(outbox, port, registry_id, RegistrySyncStatus::Registered).await + } + + async fn run_with_row( + outbox: &Arc, + port: &Arc, + registry_id: Option, + status: RegistrySyncStatus, + ) -> TransferDrainStats { + let registrations: Arc = Arc::new(FakeRegistrations { + registry_id, + status, + }); drain_once( &(outbox.clone() as Arc), ®istrations, @@ -479,6 +510,31 @@ mod tests { ); } + /// 🚨 While a registration is `submitted` its row holds the submission's + /// correlation identifier in the column a registered row holds the + /// registration identifier in. A transfer notification naming the + /// correlation identifier would amend a record that does not exist, so the + /// row is held until the registry has ruled. + #[tokio::test] + async fn a_transfer_does_not_use_a_submissions_correlation_id_as_a_registration_id() { + let (outbox, port) = fakes(Outcome::Notified, vec![row_from(&record())]); + + let stats = run_with_row( + &outbox, + &port, + Some("CORRELATION-1".to_owned()), + RegistrySyncStatus::Submitted, + ) + .await; + + assert_eq!(stats.deferred, 1); + assert_eq!(stats.notified, 0); + assert!( + port.seen.lock().unwrap().is_empty(), + "the correlation identifier must never reach the registry as a record id" + ); + } + /// A blank registry id is the same as none — it identifies nothing. #[tokio::test] async fn a_blank_registry_id_defers_too() { diff --git a/crates/dpp-node/tests/create_passport_request_is_one_type.rs b/crates/dpp-node/tests/create_passport_request_is_one_type.rs index 98303f69..51314ad6 100644 --- a/crates/dpp-node/tests/create_passport_request_is_one_type.rs +++ b/crates/dpp-node/tests/create_passport_request_is_one_type.rs @@ -27,6 +27,7 @@ fn the_importer_builds_the_very_type_the_vault_accepts() { product_name: "Shared shape".to_owned(), product_group: None, life_status: None, + personal_data: Default::default(), supersedes_id: None, manufacturer: dpp_domain::passport::ManufacturerInfo { name: "Acme".to_owned(), diff --git a/crates/dpp-node/tests/openapi_contract.rs b/crates/dpp-node/tests/openapi_contract.rs index c3047b13..b389708c 100644 --- a/crates/dpp-node/tests/openapi_contract.rs +++ b/crates/dpp-node/tests/openapi_contract.rs @@ -273,6 +273,14 @@ const UNCHECKED: &[(&str, &str)] = &[ "DppId", "a bare `type: string, format: uuid`, not an object with properties", ), + ( + "PersonalDataStatement", + "an internally-tagged `oneOf` (`held`) of two objects, one of which \ + carries a closed set of lawful bases — neither checker expresses a \ + union, the same limit `DeactivationReason` runs into. The wire form of \ + each variant is pinned instead by \ + `every_personal_data_statement_serialises_as_documented`", + ), ( "DidDocument", "a W3C DID document passed through as opaque JSON; the node does not \ @@ -849,7 +857,7 @@ fn deactivation_reason_kinds() -> Vec { /// Bump only after re-checking the enums listed below against the released /// crate. Bumping it to make a red build green is the one thing that breaks /// this gate. -const CORE_VERSION_VERIFIED: &str = "0.21.0"; +const CORE_VERSION_VERIFIED: &str = "0.22.0"; /// Enums whose variants this test cannot enumerate, and so cannot gate. /// @@ -869,6 +877,7 @@ const UNENUMERABLE_CORE_ENUMS: &[&str] = &[ "RecordedBasis", "InstrumentStatus", "RegulatoryStatus", + "LawfulBasis", ]; /// A `dpp-core` repin must not pass silently. @@ -1149,6 +1158,82 @@ fn deactivation_reason_documents_every_kind() { ); } +/// `PersonalDataStatement` is `UNCHECKED` above: it is an internally-tagged union, +/// which neither checker expresses. This pins what they could not: the `held` +/// value of each variant, the fields each emits, and the closed set of lawful +/// bases the `outside` variant may name. +#[test] +fn every_personal_data_statement_serialises_as_documented() { + use dpp_domain::{HeldOutside, LawfulBasis, PersonalDataRecordId, PersonalDataStatement}; + + let spec = spec(); + let arms = schemas(&spec)["PersonalDataStatement"]["oneOf"] + .as_array() + .expect("PersonalDataStatement is not a oneOf"); + let arm_for = |held: &str| { + arms.iter() + .find(|arm| arm["properties"]["held"]["enum"][0].as_str() == Some(held)) + .unwrap_or_else(|| panic!("no oneOf arm documents `held: {held}`")) + }; + let keys = |arm: &Value| -> BTreeSet { + arm["properties"] + .as_object() + .expect("an arm documents properties") + .keys() + .cloned() + .collect() + }; + + let nothing = serde_json::to_value(PersonalDataStatement::NothingHeld).unwrap(); + assert_eq!( + nothing + .as_object() + .unwrap() + .keys() + .cloned() + .collect::>(), + keys(arm_for("nothing")), + "the `nothing` arm documents exactly what is emitted: {nothing}" + ); + + // The bases are listed by hand, as the other enums are: see the tripwire. + let bases = [ + LawfulBasis::Consent, + LawfulBasis::Contract, + LawfulBasis::LegalObligation, + LawfulBasis::VitalInterests, + LawfulBasis::PublicTask, + LawfulBasis::LegitimateInterests, + ]; + let mut emitted_bases = BTreeSet::new(); + let outside_arm = arm_for("outside"); + for basis in bases { + let outside = serde_json::to_value(PersonalDataStatement::HeldOutside(HeldOutside::new( + basis, + PersonalDataRecordId::new("record-1"), + ))) + .unwrap(); + assert_eq!( + outside + .as_object() + .unwrap() + .keys() + .cloned() + .collect::>(), + keys(outside_arm), + "the `outside` arm documents exactly what is emitted: {outside}" + ); + emitted_bases.insert(outside["lawfulBasis"].as_str().unwrap().to_owned()); + } + let documented_bases: BTreeSet = outside_arm["properties"]["lawfulBasis"]["enum"] + .as_array() + .expect("lawfulBasis documents its values") + .iter() + .map(|v| v.as_str().unwrap().to_owned()) + .collect(); + assert_eq!(documented_bases, emitted_bases, "lawful bases disagree"); +} + /// `RevocationStanding` is `UNCHECKED` above: its variants carry three different /// payload fields, so a single fixture cannot emit the union the key-set /// comparison expects. This pins what that comparison could not — the wire form @@ -3119,6 +3204,13 @@ mod fixtures { derived_from: vec![derivation_ref()], component_refs: vec![component_ref()], life_status: Some(LifeStatus::Repurposed), + // Populated, for the same rule as the fields around it: an empty map + // emits no key, and the schema check would pass without ever + // comparing the field. + personal_data: std::collections::BTreeMap::from([( + "usageHistory.negativeEvents".to_owned(), + dpp_domain::PersonalDataStatement::NothingHeld, + )]), retention_until: Some(ts()), product_id: Some(uuid()), commodity_code: Some(CommodityCode::parse("85076000").expect("valid CN-8")), @@ -3551,6 +3643,13 @@ mod fixtures { // value against `LifeStatus.yaml`. `Repurposed` also matches the // derivation edge beside it, so the fixture is internally coherent. life_status: Some(dpp_domain::passport::LifeStatus::Repurposed), + // Populated, for the same rule as the fields around it: an empty map + // emits no key, and the schema check would pass without ever + // comparing the field. + personal_data: std::collections::BTreeMap::from([( + "usageHistory.negativeEvents".to_owned(), + dpp_domain::PersonalDataStatement::NothingHeld, + )]), // Populated, for the same rule: `None` emits no key, and the schema // check would pass without ever comparing the field. carrier_serial: Some("SN-2026-0001".into()), @@ -3741,6 +3840,7 @@ mod fixtures { // Absent on purpose: this fixture exists to pin the *minimum* a create // body needs, and omitting the status is lawful for every product group. life_status: None, + personal_data: Default::default(), supersedes_id: None, manufacturer: manufacturer(), materials: None, diff --git a/crates/dpp-node/tests/registry_outbox.rs b/crates/dpp-node/tests/registry_outbox.rs index 2515561e..00ac541d 100644 --- a/crates/dpp-node/tests/registry_outbox.rs +++ b/crates/dpp-node/tests/registry_outbox.rs @@ -38,8 +38,8 @@ use dpp_domain::{ passport::{ManufacturerInfo, Passport, PassportId}, ports::passport_repo::PassportRepository, ports::registry_sync::{ - RegisteringOperator, RegistrationGranularity, RegistrationRequest, RegistryIdentifiers, - RegistryRecord, RegistryStatus, RegistrySyncPort, + RegisteringOperator, RegistrationGranularity, RegistrationRequest, RegistryRecord, + RegistrySyncPort, SubmissionAcknowledgement, SubmissionReport, }, product_group::{ProductGroup, ProductGroupData}, status::PassportStatus, @@ -111,6 +111,7 @@ fn draft_passport() -> Passport { derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: None, product_id: None, commodity_code: None, @@ -162,56 +163,80 @@ async fn outbox_row_count(dal: &PgDal, id: PassportId) -> i64 { // ─── Mock registry port ─────────────────────────────────────────────────────── +/// What the registry says. A submission is answered with `Accepted`, `Held` or +/// `Transient`; the poll that follows with `Registered`, `Rejected`, +/// `Processing` or `Transient`. The two steps are different questions with +/// different answers, which is the shape of a real asynchronous registration. #[derive(Clone, Copy)] enum Outcome { - Registered, + /// Submission accepted, under a correlation identifier. + Accepted, + /// A replayed submission: the registry holds it and gave no handle for it. + Held, + /// The registry could not be reached. Transient, + /// Poll: the registry ruled and generated a registration identifier. + Registered, + /// Poll: the registry refused the submission. Rejected, - /// Accepted for asynchronous validation — the registry's normal first answer. - Pending, - /// A replayed submission: the registry holds it and returned no id for it. - Held, - /// Withdrawn from service. - Deactivated, + /// Poll: the registry has not finished checking. + Processing, } +/// The handle the mock registry gives a submission. +const CORRELATION_ID: &str = "CORR-TEST-0001"; + +/// The registration identifier the mock registry generates. +const REGISTRATION_ID: &str = "EU-REG-TEST-0001"; + struct MockPort { + /// What `register` answers. outcome: Outcome, calls: Arc, - /// Calls to `check_status`, counted separately so a test can prove the + /// Calls to `check_submission`, counted separately so a test can prove the /// drain *polled* a submitted row instead of registering it again. polls: Arc, - /// What a poll reports, when it differs from the submission's answer. + /// What a poll reports. poll_outcome: Outcome, } -fn record(status: RegistryStatus, registry_id: &str) -> RegistryRecord { - RegistryRecord { - identifiers: RegistryIdentifiers { - product_id: "PROD".into(), - operator_id: "OP".into(), - facility_id: "FAC".into(), - registry_id: registry_id.into(), - }, - status, - registered_at: Utc::now(), - updated_at: Utc::now(), - } -} - #[async_trait] impl RegistrySyncPort for MockPort { - async fn register(&self, _req: RegistrationRequest) -> Result { + async fn register( + &self, + _req: RegistrationRequest, + ) -> Result { self.calls.fetch_add(1, Ordering::SeqCst); - Ok(match answer(self.outcome) { - Ok(rec) => rec, - Err(e) => return Err(e), - }) + match self.outcome { + Outcome::Accepted => Ok(SubmissionAcknowledgement::Accepted { + correlation_id: CORRELATION_ID.into(), + }), + Outcome::Held => Ok(SubmissionAcknowledgement::AlreadyHeld), + Outcome::Transient => Err(DppError::Internal("EU registry transient failure".into())), + other => panic!("not an answer to a submission: {}", other as u8), + } } - async fn check_status(&self, _pid: PassportId) -> Result { + async fn check_submission(&self, correlation_id: &str) -> Result { self.polls.fetch_add(1, Ordering::SeqCst); - answer(self.poll_outcome) + // A poll names the submission by the handle the registry gave it, never + // by this node's passport id. + assert_eq!(correlation_id, CORRELATION_ID); + match self.poll_outcome { + Outcome::Registered => Ok(SubmissionReport::Registered { + registration_id: REGISTRATION_ID.into(), + }), + Outcome::Rejected => Ok(SubmissionReport::Refused { + reason: "the registry refused the submission: NOT_RESOLVABLE_UPI".into(), + }), + Outcome::Processing => Ok(SubmissionReport::Processing), + Outcome::Transient => Err(DppError::Internal("EU registry transient failure".into())), + other => panic!("not an answer to a poll: {}", other as u8), + } + } + + async fn check_status(&self, _registration_id: &str) -> Result { + unimplemented!("not exercised") } async fn notify_transfer( @@ -223,21 +248,8 @@ impl RegistrySyncPort for MockPort { } } -/// The record (or error) the registry gives for an outcome, shared by -/// `register` and `check_status` — a verdict means the same thing either way. -fn answer(outcome: Outcome) -> Result { - match outcome { - Outcome::Registered => Ok(record(RegistryStatus::Registered, "EU-REG-TEST-0001")), - Outcome::Transient => Err(DppError::Internal("EU registry transient failure".into())), - Outcome::Rejected => Ok(record(RegistryStatus::Rejected, "")), - Outcome::Pending => Ok(record(RegistryStatus::Pending, "EU-REG-TEST-0001")), - Outcome::Held => Ok(record(RegistryStatus::Pending, "")), - Outcome::Deactivated => Ok(record(RegistryStatus::Deactivated, "EU-REG-TEST-0001")), - } -} - fn mock(outcome: Outcome) -> (Arc, Arc) { - let (port, calls, _) = mock_with_poll(outcome, outcome); + let (port, calls, _) = mock_with_poll(outcome, Outcome::Processing); (port, calls) } @@ -262,6 +274,15 @@ fn mock_with_poll( (port, calls, polls) } +/// Make every row due again, as the passage of the backoff would. +async fn make_due(dal: &PgDal, id: PassportId) { + sqlx::query("UPDATE odal.registry_sync SET next_attempt_at = now() WHERE passport_id = $1") + .bind(id.0) + .execute(dal.pool()) + .await + .expect("make the row due"); +} + // ─── (a) atomic publish + idempotency + (b) drains exactly once ─────────────── #[tokio::test(flavor = "multi_thread")] @@ -301,22 +322,37 @@ async fn publish_is_atomic_idempotent_and_drains_exactly_once() { outbox.commit_publish(&again, payload).await.unwrap(); assert_eq!(outbox_row_count(&dal, id).await, 1, "still exactly one row"); - // (b) drain once with a healthy port → registered; a second pass is a no-op, - // so the registry is called exactly once across the (simulated) restart. - let (port, calls) = mock(Outcome::Registered); + // (b) drain with a healthy port. The first pass submits and the registry + // answers with a handle; the second reads its ruling and registers. After + // that a pass is a no-op, so the registry is submitted to exactly once + // across the (simulated) restart. + let (port, calls, polls) = mock_with_poll(Outcome::Accepted, Outcome::Registered); let s1 = drain_once(&outbox, &port, None, 50).await; - assert_eq!(s1.registered, 1); + assert_eq!(s1.submitted, 1); + assert_eq!(s1.registered, 0, "an acknowledgement is not a registration"); + let submitted = outbox.pending_for(id).await.unwrap().unwrap(); + assert_eq!(submitted.status, RegistrySyncStatus::Submitted); + assert_eq!(submitted.registry_id.as_deref(), Some(CORRELATION_ID)); + + make_due(&dal, id).await; + let s2 = drain_once(&outbox, &port, None, 50).await; + assert_eq!(s2.registered, 1); let after = outbox.pending_for(id).await.unwrap().unwrap(); assert_eq!(after.status, RegistrySyncStatus::Registered); - assert_eq!(after.registry_id.as_deref(), Some("EU-REG-TEST-0001")); + assert_eq!( + after.registry_id.as_deref(), + Some(REGISTRATION_ID), + "the column now holds the registration identifier, not the handle" + ); assert!( outbox.due(50).await.unwrap().is_empty(), "registered row not due" ); - let s2 = drain_once(&outbox, &port, None, 50).await; - assert_eq!(s2.registered, 0); - assert_eq!(calls.load(Ordering::SeqCst), 1, "registered exactly once"); + let s3 = drain_once(&outbox, &port, None, 50).await; + assert_eq!(s3.registered, 0); + assert_eq!(calls.load(Ordering::SeqCst), 1, "submitted exactly once"); + assert_eq!(polls.load(Ordering::SeqCst), 1, "polled exactly once"); } // ─── (c) transient backoff and (d) terminal rejection ───────────────────────── @@ -346,11 +382,21 @@ async fn drain_backs_off_on_transient_and_marks_terminal_rejection() { // (d) terminal rejection → row rejected (kept for a human, never dropped). let rejected_id = create_and_publish(&dal, &outbox).await; - let (port_r, _) = mock(Outcome::Rejected); + // The registry accepts the submission and rules against it afterwards. + let (port_r, _, _) = mock_with_poll(Outcome::Accepted, Outcome::Rejected); + drain_once(&outbox, &port_r, None, 50).await; + make_due(&dal, rejected_id).await; let sr = drain_once(&outbox, &port_r, None, 50).await; assert_eq!(sr.rejected, 1); let row = outbox.pending_for(rejected_id).await.unwrap().unwrap(); assert_eq!(row.status, RegistrySyncStatus::Rejected); + assert!( + row.message + .as_deref() + .is_some_and(|m| m.contains("NOT_RESOLVABLE_UPI")), + "the registry's reason reaches the row for a human: {:?}", + row.message + ); } // ─── 0024 heals rows the old write path already clobbered ───────────────────── @@ -518,7 +564,7 @@ async fn suspend_before_drain_must_not_drop_the_pending_registration() { // And the loss is real, not just a state-machine detail: the registry is // never called for this passport. - let (port, calls) = mock(Outcome::Registered); + let (port, calls) = mock(Outcome::Accepted); drain_once(&outbox, &port, None, 50).await; assert_eq!( calls.load(Ordering::SeqCst), @@ -585,7 +631,7 @@ async fn retiring_an_unpublished_draft_creates_no_outbox_row() { ); // Nothing to drain, and no false rejection raised. - let (port, calls) = mock(Outcome::Registered); + let (port, calls) = mock(Outcome::Accepted); let stats = drain_once(&outbox, &port, None, 50).await; assert_eq!(calls.load(Ordering::SeqCst), 0, "registry not called"); assert_eq!( @@ -610,8 +656,10 @@ async fn republish_clears_a_stale_suspend_intent() { let id = create_and_publish(&dal, &outbox).await; - // Drain the registration, then suspend. - let (port, _) = mock(Outcome::Registered); + // Drain the registration — submitted, then ruled on — then suspend. + let (port, _, _) = mock_with_poll(Outcome::Accepted, Outcome::Registered); + drain_once(&outbox, &port, None, 50).await; + make_due(&dal, id).await; drain_once(&outbox, &port, None, 50).await; outbox .enqueue_status(id, RegistryStatusIntent::Suspended) @@ -656,7 +704,7 @@ async fn an_accepted_submission_is_not_yet_a_registration() { let outbox: Arc = Arc::new(PgRegistrySyncRepo::new(dal.clone())); let id = create_and_publish(&dal, &outbox).await; - let (port, calls) = mock(Outcome::Pending); + let (port, calls) = mock(Outcome::Accepted); let stats = drain_once(&outbox, &port, None, 10).await; assert_eq!(calls.load(Ordering::SeqCst), 1); @@ -671,11 +719,37 @@ async fn an_accepted_submission_is_not_yet_a_registration() { assert_eq!(row.status, RegistrySyncStatus::Submitted); assert_eq!( row.registry_id.as_deref(), - Some("EU-REG-TEST-0001"), - "the registry's record id is kept so the verdict can be polled for" + Some(CORRELATION_ID), + "the registry's handle is kept so the verdict can be polled for" ); } +/// A processing submission stays `submitted` and is asked again next pass: the +/// registry has not ruled, and neither recording a registration nor a refusal +/// would be true. +#[tokio::test(flavor = "multi_thread")] +async fn a_submission_still_being_checked_keeps_being_polled() { + let _c = start_pg().await; + let dal = _c.dal.clone(); + let outbox: Arc = Arc::new(PgRegistrySyncRepo::new(dal.clone())); + let id = create_and_publish(&dal, &outbox).await; + + let (port, calls, polls) = mock_with_poll(Outcome::Accepted, Outcome::Processing); + drain_once(&outbox, &port, None, 10).await; + for _ in 0..2 { + make_due(&dal, id).await; + let stats = drain_once(&outbox, &port, None, 10).await; + assert_eq!(stats.submitted, 1); + assert_eq!((stats.registered, stats.rejected), (0, 0)); + } + + assert_eq!(calls.load(Ordering::SeqCst), 1, "never resubmitted"); + assert_eq!(polls.load(Ordering::SeqCst), 2); + let row = outbox.pending_for(id).await.unwrap().unwrap(); + assert_eq!(row.status, RegistrySyncStatus::Submitted); + assert_eq!(row.registry_id.as_deref(), Some(CORRELATION_ID)); +} + /// A replayed idempotency key is answered without an id: the registry holds the /// submission and says nothing more about it. The row must still move to /// `submitted` — that is what stops the drain posting it again — but it must not @@ -699,6 +773,20 @@ async fn a_submission_the_registry_already_holds_is_submitted_without_an_id() { row.registry_id, None, "no id was returned, so none may be recorded" ); + + // With no handle there is nothing to poll, and the node's own passport id is + // not a substitute: the registry never received it. The row waits, neither + // polled nor submitted again. + make_due(&dal, id).await; + let later = drain_once(&outbox, &port, None, 10).await; + assert_eq!(later.submitted, 1); + assert_eq!( + calls.load(Ordering::SeqCst), + 1, + "the registry holds it already, so it is not submitted again" + ); + let row = outbox.pending_for(id).await.unwrap().unwrap(); + assert_eq!(row.status, RegistrySyncStatus::Submitted); } /// A submitted row is polled, never resubmitted — resubmitting is what would @@ -711,7 +799,7 @@ async fn a_submitted_row_is_polled_not_resubmitted() { let id = create_and_publish(&dal, &outbox).await; // First pass: accepted for validation. - let (port, calls, polls) = mock_with_poll(Outcome::Pending, Outcome::Registered); + let (port, calls, polls) = mock_with_poll(Outcome::Accepted, Outcome::Registered); drain_once(&outbox, &port, None, 10).await; assert_eq!(calls.load(Ordering::SeqCst), 1); assert_eq!(polls.load(Ordering::SeqCst), 0); @@ -738,34 +826,6 @@ async fn a_submitted_row_is_polled_not_resubmitted() { assert_eq!(row.status, RegistrySyncStatus::Registered); } -/// A verdict of "deactivated" is terminal but is not a rejection: nothing about -/// the submission was defective, so there is nothing to correct and resubmit. -#[tokio::test(flavor = "multi_thread")] -async fn a_deactivated_record_is_terminal_but_not_rejected() { - let _c = start_pg().await; - let dal = _c.dal.clone(); - let outbox: Arc = Arc::new(PgRegistrySyncRepo::new(dal.clone())); - let id = create_and_publish(&dal, &outbox).await; - - let (port, _calls) = mock(Outcome::Deactivated); - let stats = drain_once(&outbox, &port, None, 10).await; - - assert_eq!(stats.deactivated, 1); - assert_eq!(stats.rejected, 0); - - let row = outbox.pending_for(id).await.unwrap().unwrap(); - assert_eq!(row.status, RegistrySyncStatus::Deactivated); - assert_ne!(row.status, RegistrySyncStatus::Rejected); - - let counts = outbox.status_counts(5).await.expect("counts"); - assert_eq!(counts.deactivated, 1); - assert_eq!(counts.rejected, 0); - assert!( - outbox.due(10).await.unwrap().is_empty(), - "a deactivated row is terminal and stops draining" - ); -} - // ─── reconciliation: published passports nobody is tracking ────────────────── /// A passport published before this outbox existed (or lost to an older write @@ -861,7 +921,7 @@ async fn an_expired_operator_holds_the_drain_without_losing_rows() { let expired = operator_verified_at(&dal, Some(Utc::now() - chrono::Duration::days(365 * 3 + 1))).await; - let (port, calls) = mock(Outcome::Registered); + let (port, calls, _) = mock_with_poll(Outcome::Accepted, Outcome::Registered); let stats = drain_once(&outbox, &port, Some(&expired), 50).await; assert_eq!( @@ -882,6 +942,14 @@ async fn an_expired_operator_holds_the_drain_without_losing_rows() { let current = operator_verified_at(&dal, Some(Utc::now())).await; let stats = drain_once(&outbox, &port, Some(¤t), 50).await; assert_eq!(calls.load(Ordering::SeqCst), 1); + assert_eq!( + stats.submitted, 1, + "the held row is submitted once verified" + ); + + // And the registry's ruling follows on the next pass. + make_due(&dal, id).await; + let stats = drain_once(&outbox, &port, Some(¤t), 50).await; assert_eq!(stats.registered, 1); } @@ -894,7 +962,7 @@ async fn a_never_verified_operator_also_holds_the_drain() { create_and_publish(&dal, &outbox).await; let never = operator_verified_at(&dal, None).await; - let (port, calls) = mock(Outcome::Registered); + let (port, calls) = mock(Outcome::Accepted); drain_once(&outbox, &port, Some(&never), 50).await; assert_eq!(calls.load(Ordering::SeqCst), 0); diff --git a/crates/dpp-node/tests/s3_backup.rs b/crates/dpp-node/tests/s3_backup.rs index 18d4a490..5cd92d57 100644 --- a/crates/dpp-node/tests/s3_backup.rs +++ b/crates/dpp-node/tests/s3_backup.rs @@ -223,6 +223,7 @@ fn make_passport() -> Passport { derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: None, product_id: None, commodity_code: None, diff --git a/crates/dpp-node/tests/seal_outbox.rs b/crates/dpp-node/tests/seal_outbox.rs index 2fd45614..18f921de 100644 --- a/crates/dpp-node/tests/seal_outbox.rs +++ b/crates/dpp-node/tests/seal_outbox.rs @@ -219,6 +219,7 @@ fn draft_passport() -> Passport { derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: None, product_id: None, commodity_code: None, diff --git a/crates/dpp-node/tests/smoke.rs b/crates/dpp-node/tests/smoke.rs index 4ed2468b..cc639f9f 100644 --- a/crates/dpp-node/tests/smoke.rs +++ b/crates/dpp-node/tests/smoke.rs @@ -1848,30 +1848,30 @@ async fn an_issued_credential_names_the_did_document_this_node_serves() { // The `kid` in the JWS header has to resolve *in that document*, which is // the half a matching `issuer` string would not catch: the same DID can be // named while a different key signs. - let header_b64 = issued["credentialJws"] + let jws = issued["credentialJws"] .as_str() - .expect("credentialJws must be a string") - .split('.') - .next() - .expect("compact JWS has a header segment"); - let header: serde_json::Value = serde_json::from_slice( - &base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(header_b64) - .expect("JWS header is base64url"), - ) - .expect("JWS header is JSON"); - let kid = header["kid"].as_str().expect("JWS header must carry kid"); + .expect("credentialJws must be a string"); // Resolved with the verifier's own helper rather than by comparing strings. - // `kid` here is a SHA-256 fingerprint of the raw key, not a verification - // method id, and `extract_key_by_fingerprint` additionally requires the - // method to be authorised for `assertionMethod` — reimplementing either - // rule in the test would let the test agree with itself while a verifier - // disagreed. + // `kid` is the absolute URL of a verification method, and resolving it also + // requires the method to be controlled by the document and authorised for + // `assertionMethod` — reimplementing either rule in the test would let the + // test agree with itself while a verifier disagreed. + let kid = dpp_crypto::jws::extract_kid_from_jws(jws).expect("JWS header must carry kid"); assert!( - dpp_crypto::jws::verifier::extract_key_by_fingerprint(&did_doc, kid).is_some(), + dpp_crypto::jws::resolve_verification_key(&did_doc, jws).is_some(), "the signing kid {kid} must resolve to an assertion-authorised key in \ the document this node serves, got {did_doc}" ); + // The access credential is typed as a VC-JWT, so another signature of the + // same key cannot pass for one. + let header: serde_json::Value = serde_json::from_slice( + &base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(jws.split('.').next().expect("a header segment")) + .expect("JWS header is base64url"), + ) + .expect("JWS header is JSON"); + assert_eq!(header["typ"], "vc+jwt"); + assert_eq!(header["cty"], "vc"); // Country is normalised on the way in, so a credential carries the form a // verifier compares against rather than whatever the caller typed. @@ -2671,6 +2671,112 @@ async fn a_passport_cannot_be_created_already_waste() { ); } +/// The body of a textile draft whose `repairHistoryUrl` — a log of repairs to +/// one specific item, marked as able to hold personal data — has a value. +fn textile_with_a_repair_history(personal_data: Option) -> serde_json::Value { + let mut body = serde_json::json!({ + "productName": "Organic Cotton Tee", + "productGroup": "textile", + "manufacturer": {"name": "SmokeTestCorp", "address": "Berlin, DE"}, + "materials": [], + "productGroupData": { + "carbonFootprintKgCo2e": 4.2, + "careInstructions": "Machine wash cold, line dry", + "chemicalComplianceStandard": "OEKO-TEX 100", + "countryOfOrigin": "IT", + "countryOfRawMaterialOrigin": "IN", + "disassemblyInstructions": "Remove label and neck tape before recycling", + "endOfLifeInstructions": "Return to a textile collection point", + "expectedWashCycles": 50, + "fibreComposition": [{"fibre": "cotton", "pct": 95}, {"fibre": "elastane", "pct": 5}], + "microplasticSheddingMgPerWash": 12, + "productGroup": "textile", + "productIdentifier": {"gtin": "09506000134369", "scheme": "gs1"}, + "productWeightGrams": 220, + "recyclabilityClass": "B", + "recycledContentPct": 30, + "sparePartsAvailable": false, + "waterUseLitres": 1800, + "repairHistoryUrl": "https://repairs.example.com/item/42", + }, + }); + if let Some(statements) = personal_data { + body["personalData"] = statements; + } + body +} + +/// 🚨 ESPR Art. 10(1)(e) forbids *storing* customer personal data in a passport +/// without consent, and a draft is stored. So a field the schema marks as able to +/// hold it is refused on the write that would store it, not left until publish, +/// and the refusal names the field. Create and every later draft update are +/// held to it, and a statement lets the write through. +#[tokio::test] +async fn a_draft_holding_personal_data_without_a_statement_is_refused_on_every_write() { + let (base, _container) = start_db_and_node().await; + let token = make_jwt("00000000-0000-0000-0000-000000000084"); + let client = reqwest::Client::new(); + let nothing_held = serde_json::json!({ "repairHistoryUrl": { "held": "nothing" } }); + + // Create, with the field filled and no statement. + let resp = client + .post(format!("{base}/vault/api/v1/dpp")) + .bearer_auth(&token) + .json(&textile_with_a_repair_history(None)) + .send() + .await + .expect("create request failed"); + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + assert_eq!(status, 422, "must be refused: {body}"); + assert!( + body.contains("/productGroupData/repairHistoryUrl"), + "and the refusal names the field: {body}" + ); + + // Create, with the statement. + let resp = client + .post(format!("{base}/vault/api/v1/dpp")) + .bearer_auth(&token) + .json(&textile_with_a_repair_history(Some(nothing_held.clone()))) + .send() + .await + .expect("create request failed"); + assert_eq!(resp.status(), 201, "a stated field is accepted"); + let created: serde_json::Value = resp.json().await.unwrap(); + let id = created["id"].as_str().expect("an id").to_owned(); + assert_eq!( + created["personalData"], nothing_held, + "the statement is stored with the draft and served back: {created}" + ); + + // Withdrawing the statement from the draft leaves the content bare: refused. + let resp = client + .put(format!("{base}/vault/api/v1/dpp/{id}")) + .bearer_auth(&token) + .json(&serde_json::json!({ "personalData": {} })) + .send() + .await + .expect("update request failed"); + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + assert_eq!(status, 422, "a draft update is held to it too: {body}"); + assert!( + body.contains("/productGroupData/repairHistoryUrl"), + "{body}" + ); + + // Restating it is accepted. + let resp = client + .put(format!("{base}/vault/api/v1/dpp/{id}")) + .bearer_auth(&token) + .json(&serde_json::json!({ "personalData": nothing_held })) + .send() + .await + .expect("update request failed"); + assert_eq!(resp.status(), 200, "personalData is patchable on a draft"); +} + /// The vocabulary belongs to one regulation, so it belongs to one product group. #[tokio::test] async fn a_non_battery_passport_may_not_claim_a_battery_life_status() { diff --git a/crates/dpp-node/tests/snapshot_outbox.rs b/crates/dpp-node/tests/snapshot_outbox.rs index 384f61eb..12b542ec 100644 --- a/crates/dpp-node/tests/snapshot_outbox.rs +++ b/crates/dpp-node/tests/snapshot_outbox.rs @@ -323,6 +323,7 @@ fn passport(status: PassportStatus) -> Passport { derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: None, product_id: None, commodity_code: None, diff --git a/crates/dpp-node/tests/snapshot_static_tier.rs b/crates/dpp-node/tests/snapshot_static_tier.rs index 482baec8..b14eb801 100644 --- a/crates/dpp-node/tests/snapshot_static_tier.rs +++ b/crates/dpp-node/tests/snapshot_static_tier.rs @@ -223,12 +223,23 @@ fn signed_snapshot( "asOf": rfc3339(as_of), "validUntil": rfc3339(valid_until), }); - let proof = dpp_crypto::jws::sign(&store, "root", &document).expect("sign snapshot"); - document["snapshotJwsSignature"] = serde_json::Value::String(proof); + // The node signs under the absolute DID URL of the key's verification method. + let thumbprint = store + .public_key("root") + .expect("key present") + .thumbprint_uri() + .expect("thumbprint"); + let kid = format!( + "{}#{thumbprint}", + dpp_vc::did_for("snapshot-test.example.com") + ); + let proof = dpp_crypto::jws::sign(&store, "root", &document, &kid).expect("sign snapshot"); + document["snapshotJwsSignature"] = serde_json::Value::String(proof.clone()); let did = dpp_vc::build_did_document(&store, "snapshot-test.example.com", "root") .expect("build did document"); - let key = dpp_crypto::jws::extract_primary_public_key(&did).expect("primary key"); + let key = dpp_crypto::jws::resolve_verification_key(&did, &proof) + .expect("did document carries the signing key"); (dir, document, key) } diff --git a/crates/dpp-node/tests/transfer_outbox.rs b/crates/dpp-node/tests/transfer_outbox.rs index 13827424..6c61b361 100644 --- a/crates/dpp-node/tests/transfer_outbox.rs +++ b/crates/dpp-node/tests/transfer_outbox.rs @@ -75,6 +75,7 @@ fn published_passport() -> Passport { derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: None, product_id: None, commodity_code: None, diff --git a/crates/dpp-plugin-host/tests/fixtures/abi-echo/Cargo.toml b/crates/dpp-plugin-host/tests/fixtures/abi-echo/Cargo.toml index 0c3d5284..9cddb771 100644 --- a/crates/dpp-plugin-host/tests/fixtures/abi-echo/Cargo.toml +++ b/crates/dpp-plugin-host/tests/fixtures/abi-echo/Cargo.toml @@ -20,7 +20,7 @@ path = "src/lib.rs" # the guest is built against the same contract the host links. Bump the two # together. A path into a sibling dpp-core checkout built whatever that working # tree held, and resolved only from a checkout sitting next to it. -dpp-plugin-sdk = "0.21.0" +dpp-plugin-sdk = "0.22.0" serde_json = "1" [profile.release] diff --git a/crates/dpp-plugin-host/tests/integration.rs b/crates/dpp-plugin-host/tests/integration.rs index 09772270..cc7a9d14 100644 --- a/crates/dpp-plugin-host/tests/integration.rs +++ b/crates/dpp-plugin-host/tests/integration.rs @@ -149,7 +149,7 @@ fn battery_product_group_data() -> ProductGroupData { nominal_voltage_v: 400.0, nominal_capacity_ah: 100.0, expected_lifetime_cycles: Some(3000), - co2e_per_unit_kg: 150.0, + co2e_per_unit_kg: Some(150.0), recycled_content_cobalt_pct: None, recycled_content_lithium_pct: None, recycled_content_nickel_pct: None, diff --git a/crates/dpp-resolver/src/handlers/resolve_json.rs b/crates/dpp-resolver/src/handlers/resolve_json.rs index 62c1cd5f..573468b0 100644 --- a/crates/dpp-resolver/src/handlers/resolve_json.rs +++ b/crates/dpp-resolver/src/handlers/resolve_json.rs @@ -378,6 +378,7 @@ mod security_regression { compliance_result: None, lint_result: None, product_group_data: None, + personal_data: Default::default(), status: PassportStatus::Published, qr_code_url: None, jws_signature: Some("eyJ.full.proof".into()), diff --git a/crates/dpp-resolver/src/infra/did.rs b/crates/dpp-resolver/src/infra/did.rs index 620252c6..38f0d6ec 100644 --- a/crates/dpp-resolver/src/infra/did.rs +++ b/crates/dpp-resolver/src/infra/did.rs @@ -100,22 +100,16 @@ pub async fn verify_passport_jws( // Fetch the operator DID document; fail closed (503) on any transport/parse error. let did_doc = fetch_did(http, operator_did_url).await?; - // Select the key by kid (fingerprint) so rotation-archived keys remain usable. - // Fall back to the primary key for old kid-less JWS tokens. - let pub_key = { - let kid = jws::extract_kid_from_jws(jws); - kid.as_deref() - .and_then(|k| jws::extract_key_by_fingerprint(&did_doc, k)) - .or_else(|| jws::extract_primary_public_key(&did_doc)) - .ok_or_else(|| { - tracing::warn!( - code = event_codes::DID_UNREACHABLE, - operator_did_url, - "operator DID has no matching verification key" - ); - StatusCode::SERVICE_UNAVAILABLE - })? - }; + // The key the `kid` names in the operator's document, so rotation-archived + // keys stay usable. A token that names none is not checked against a default. + let pub_key = jws::resolve_verification_key(&did_doc, jws).ok_or_else(|| { + tracing::warn!( + code = event_codes::DID_UNREACHABLE, + operator_did_url, + "operator DID has no matching verification key" + ); + StatusCode::SERVICE_UNAVAILABLE + })?; // 1) Signature must verify against the operator key. match jws::verify_jws(jws, &pub_key) { diff --git a/crates/dpp-resolver/src/jws_verification_tests.rs b/crates/dpp-resolver/src/jws_verification_tests.rs index 3f523390..5064a06a 100644 --- a/crates/dpp-resolver/src/jws_verification_tests.rs +++ b/crates/dpp-resolver/src/jws_verification_tests.rs @@ -38,7 +38,7 @@ fn sign_jws(payload: &serde_json::Value) -> (String, serde_json::Value) { let signing_key = SigningKey::generate(&mut rand::rand_core::UnwrapErr(rand::rngs::SysRng)); let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD; let pub_key_b64 = b64.encode(signing_key.verifying_key().as_bytes()); - let header_b64 = b64.encode(r#"{"alg":"EdDSA","crv":"Ed25519"}"#); + let header_b64 = b64.encode(r#"{"alg":"EdDSA","kid":"did:web:test.resolver.example#key-1"}"#); let payload_b64 = b64.encode(serde_json::to_vec(payload).unwrap()); let signing_input = format!("{header_b64}.{payload_b64}"); let sig_b64 = b64.encode(signing_key.sign(signing_input.as_bytes()).to_bytes()); diff --git a/crates/dpp-resolver/tests/resolver_e2e.rs b/crates/dpp-resolver/tests/resolver_e2e.rs index 195f6dd3..1d9222a4 100644 --- a/crates/dpp-resolver/tests/resolver_e2e.rs +++ b/crates/dpp-resolver/tests/resolver_e2e.rs @@ -751,7 +751,7 @@ async fn gtin_resolution_verifies_a_valid_signature() { let gtin = "09506000134352"; let passport_id = "00000000-0000-4000-9000-0000000000aa"; let public_view = json!({ "id": passport_id, "productName": "Signed GTIN Widget" }); - let header = r#"{"alg":"EdDSA","crv":"Ed25519"}"#; + let header = r#"{"alg":"EdDSA","kid":"did:web:valid.example#key-1"}"#; let header_b64 = b64.encode(header); let payload_b64 = b64.encode(serde_json::to_vec(&public_view).unwrap()); let signing_input = format!("{header_b64}.{payload_b64}"); diff --git a/crates/dpp-types/src/passport_request.rs b/crates/dpp-types/src/passport_request.rs index ff81ab86..1fe82594 100644 --- a/crates/dpp-types/src/passport_request.rs +++ b/crates/dpp-types/src/passport_request.rs @@ -1,5 +1,8 @@ //! [`CreatePassportRequest`] — the body of `POST /vault/api/v1/dpp`. +use std::collections::BTreeMap; + +use dpp_domain::PersonalDataStatement; use dpp_domain::passport::{ComponentRef, DerivationRef, ManufacturerInfo, MaterialEntry}; use dpp_domain::product_group::{ProductGroup, ProductGroupData}; use serde::{Deserialize, Serialize}; @@ -32,7 +35,10 @@ use serde::{Deserialize, Serialize}; /// and `componentRefs` each carry a URI *and* a hash of the referenced passport's /// public signature, and inventing either would produce a link that fails /// verification. Those stay empty on the import path, and that is a property of -/// CSV, not an oversight. +/// CSV, not an oversight. So does `personalData`: a statement names an +/// erasable record held elsewhere, which a row cannot point to, and the battery +/// and textile importers leave every marked field empty, so there is nothing for +/// a statement to speak about. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CreatePassportRequest { @@ -122,6 +128,20 @@ pub struct CreatePassportRequest { /// never saw. #[serde(default, skip_serializing_if = "Option::is_none")] pub life_status: Option, + /// What the operator states about personal data, for each field of + /// `productGroupData` the product group's schema marks as able to hold it, + /// keyed by the field's dotted path (for example + /// `usageHistory.negativeEvents`). + /// + /// A marked field with a value is refused, on create and on every later + /// write to the draft, unless it has an entry here: either nothing related + /// is held (`{"held": "nothing"}`), or it is held outside the passport in an + /// erasable record, under a stated lawful basis (`{"held": "outside", + /// "lawfulBasis": "consent", "record": ""}`). The personal data itself + /// never goes in the passport. Frozen at publish, like the rest of the + /// content. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub personal_data: BTreeMap, /// Cross-operator references to this product's constituent passports (its /// bill of materials), each optionally qualified by how much of it the /// assembly contains and in what role. Shape-validated on receipt; local diff --git a/crates/dpp-vault/examples/generate_demo_dossiers.rs b/crates/dpp-vault/examples/generate_demo_dossiers.rs index 804cfd44..03ddb5bb 100644 --- a/crates/dpp-vault/examples/generate_demo_dossiers.rs +++ b/crates/dpp-vault/examples/generate_demo_dossiers.rs @@ -53,7 +53,6 @@ use dpp_vault::domain::service::{declared_eol, published_views, stamp_publish_ob use dpp_vault::domain::verify::{acceptance_payload, verify_dossier_json}; use ed25519_dalek::{Signer, SigningKey}; use serde_json::{Value, json}; -use sha2::{Digest, Sha256}; const PID: &str = "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40"; const TRANSFER_ID: &str = "0199a3c2-7b10-7e4a-9c3d-0000000000aa"; @@ -78,9 +77,13 @@ fn party(did: &str, seed: u8) -> Party { let public_key = key.verifying_key().to_bytes(); Party { did: did.into(), - // The fingerprint `dpp-crypto`'s key store records, and the verifier - // matches the JWS `kid` against. - kid: hex::encode(Sha256::digest(public_key)), + // The absolute URL of the verification method, which is both the `kid` + // a signature names its key by and the `id` the DID document gives it: + // the DID, and the key's own RFC 7638 thumbprint in RFC 9278 URI form. + kid: format!( + "{did}#{}", + KeyAlgorithm::Ed25519.thumbprint_uri(&public_key) + ), public_key, key, } @@ -88,15 +91,16 @@ fn party(did: &str, seed: u8) -> Party { fn did_doc(p: &Party) -> Value { json!({ - "@context": ["https://www.w3.org/ns/did/v1"], + "@context": ["https://www.w3.org/ns/did/v1", "https://www.w3.org/ns/cid/v1"], "id": p.did, "verificationMethod": [{ - "id": format!("{}#key-1", p.did), - "type": "JsonWebKey2020", + "id": p.kid, + "type": "JsonWebKey", "controller": p.did, - "publicKeyJwk": KeyAlgorithm::Ed25519.public_key_jwk(&p.public_key), + "publicKeyJwk": KeyAlgorithm::Ed25519.published_jwk(&p.public_key), }], - "assertionMethod": [format!("{}#key-1", p.did)], + "authentication": [p.kid], + "assertionMethod": [p.kid], }) } @@ -293,12 +297,7 @@ fn demo_passport() -> Passport { "originalPowerCapabilityW": 960.0, "powerLimitMinW": 20.0, "powerLimitMaxW": 1500.0, - "co2ePerUnitKg": 86.4, "renewableContentPct": 14.0, - "recycledContentCobaltPct": 12.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, - "recycledContentLeadPct": 0.0, "expectedLifetimeCycles": 1200, "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", "initialRoundTripEfficiencyPct": 95.0, @@ -365,7 +364,7 @@ fn demo_passport() -> Passport { "commodityCode": "85076000", // The battery schema this build publishes against. `demo_dossiers_verify` // validates the passport's product group data against it, strictly. - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", })) .expect("demo passport"); diff --git a/crates/dpp-vault/src/api/passport_response.rs b/crates/dpp-vault/src/api/passport_response.rs index 6637d3f1..bf3dc82e 100644 --- a/crates/dpp-vault/src/api/passport_response.rs +++ b/crates/dpp-vault/src/api/passport_response.rs @@ -15,7 +15,7 @@ use dpp_domain::product_group::{ }; use dpp_domain::seal::SealedEnvelope; use dpp_domain::status::PassportStatus; -use dpp_domain::{Granularity, InstrumentRef}; +use dpp_domain::{Granularity, InstrumentRef, PersonalDataStatement}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -73,6 +73,11 @@ pub struct PassportResponse { #[serde(default, skip_serializing_if = "Option::is_none")] pub lint_result: Option, pub product_group_data: Option, + /// What the operator states about personal data in the marked fields of + /// `productGroupData`. Absent when it states nothing. Core's redaction + /// removes it from every view the public can reach. + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub personal_data: BTreeMap, pub status: PassportStatus, pub qr_code_url: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -146,6 +151,7 @@ impl From<&Passport> for PassportResponse { compliance_result: p.compliance_result.clone(), lint_result: p.lint_result.clone(), product_group_data: p.product_group_data.clone(), + personal_data: p.personal_data.clone(), status: p.status.clone(), qr_code_url: p.qr_code_url.clone(), carrier_serial: p.carrier_serial.clone(), @@ -209,6 +215,7 @@ mod tests { compliance_result: None, lint_result: None, product_group_data: None, + personal_data: Default::default(), status: PassportStatus::Draft, qr_code_url: None, jws_signature: None, @@ -264,6 +271,10 @@ mod tests { p.retention_locked = true; p.version = 3; p.carrier_serial = Some("SN-0001".into()); + p.personal_data.insert( + "usageHistory.negativeEvents".into(), + dpp_domain::PersonalDataStatement::NothingHeld, + ); p } @@ -342,6 +353,7 @@ mod tests { "lifeStatus", "responsibleOperator", "componentRefs", + "personalData", "retentionUntil", "productId", "commodityCode", diff --git a/crates/dpp-vault/src/domain/compliance/tests.rs b/crates/dpp-vault/src/domain/compliance/tests.rs index 551ebc2a..b31f146b 100644 --- a/crates/dpp-vault/src/domain/compliance/tests.rs +++ b/crates/dpp-vault/src/domain/compliance/tests.rs @@ -32,7 +32,7 @@ fn ev_battery() -> ProductGroupData { battery_type: BatteryType::Ev, nominal_voltage_v: 400.0, nominal_capacity_ah: 200.0, - co2e_per_unit_kg: 5100.0, + co2e_per_unit_kg: Some(5100.0), rated_capacity_kwh: Some(80.0), recycled_content_cobalt_pct: Some(16.0), recycled_content_lithium_pct: Some(6.0), diff --git a/crates/dpp-vault/src/domain/service/amend.rs b/crates/dpp-vault/src/domain/service/amend.rs index 73ef96bb..0d82a6d9 100644 --- a/crates/dpp-vault/src/domain/service/amend.rs +++ b/crates/dpp-vault/src/domain/service/amend.rs @@ -46,7 +46,7 @@ use dpp_domain::{ use dpp_types::{audit::PassportAuditEntry, auth::AuthContext}; use super::PassportService; -use super::create::{apply_compliance, apply_lint, apply_patch}; +use super::create::{apply_compliance, apply_lint, apply_patch, guard_personal_data}; impl PassportService { /// Issue a corrected successor to a published passport. @@ -178,6 +178,7 @@ impl PassportService { // issuance is re-signed under the classes that governed it originally. apply_patch(&mut successor, patch)?; + guard_personal_data(&successor)?; apply_compliance(&mut successor, &*self.compliance); apply_lint(&mut successor); diff --git a/crates/dpp-vault/src/domain/service/create.rs b/crates/dpp-vault/src/domain/service/create.rs index 8cfc7525..da98d127 100644 --- a/crates/dpp-vault/src/domain/service/create.rs +++ b/crates/dpp-vault/src/domain/service/create.rs @@ -89,6 +89,7 @@ impl PassportService { // unparseable catalog version would otherwise pass into a record whose // schema validation is then silently skipped downstream. passport.validate()?; + guard_personal_data(&passport)?; self.guard_component_graph(passport.id, &passport.component_refs) .await?; @@ -145,6 +146,7 @@ impl PassportService { // it actually applied, then build the delta from *those* — never from // the request body. let applied = apply_patch(&mut passport, &patch)?; + guard_personal_data(&passport)?; self.guard_component_graph(id, &passport.component_refs) .await?; let pre_compliance_co2e = passport.co2e_per_unit.clone(); @@ -350,12 +352,13 @@ pub(super) fn apply_lint(passport: &mut Passport) { /// field is immutable here by default. The old shape had the opposite default, /// which is why four fields were writable without anyone deciding they should /// be. -const PATCHABLE_FIELDS: [&str; 5] = [ +const PATCHABLE_FIELDS: [&str; 6] = [ "productName", "co2ePerUnit", "repairabilityScore", "productGroupData", "componentRefs", + "personalData", ]; /// Serialise exactly the `applied` fields out of the validated passport. @@ -378,6 +381,7 @@ fn delta_for(passport: &Passport, applied: &[&'static str]) -> serde_json::Map serde_json::json!(passport.repairability_score), "productGroupData" => serde_json::json!(passport.product_group_data), "componentRefs" => serde_json::json!(passport.component_refs), + "personalData" => serde_json::json!(passport.personal_data), // Unreachable: `applied` only ever contains `PATCHABLE_FIELDS` // entries, and adding one there without a case here fails the // `every_patchable_field_serialises` test rather than silently @@ -507,10 +511,44 @@ pub(super) fn apply_patch( passport.component_refs = refs; applied.push("componentRefs"); } + if let Some(v) = obj.get("personalData") { + // The whole map is replaced, so `{}` withdraws every statement. Whether + // the statements fit the data is not decided here: that is + // [`guard_personal_data`]'s question, asked of the patched record as a + // whole so a change to `productGroupData` and one to `personalData` are + // judged together. + passport.personal_data = serde_json::from_value(v.clone()) + .map_err(|e| DppError::Validation(format!("invalid personalData: {e}").into()))?; + applied.push("personalData"); + } Ok(applied) } +/// Refuse a passport that stores a field able to hold personal data without +/// saying what is held, and any statement core finds wanting. +/// +/// ESPR Art. 10(1)(e), Reg. (EU) 2025/2509 Art. 20(10) and Reg. (EU) 2026/405 +/// Art. 22(h) forbid *storing* customer personal data in a passport without +/// consent, and a stored draft is storage. Asking only at publish would let a +/// draft carrying an accident report sit in the database until someone +/// published it, so this runs on every write that stores a passport: create, +/// each draft update, and the draft an amendment builds. +/// +/// Core's [`dpp_domain::check_personal_data`] makes the decision and never +/// reads what a field says; this maps its findings to the `422` the other +/// field-level refusals use, one entry per field. +pub(super) fn guard_personal_data(passport: &Passport) -> Result<(), DppError> { + let errors = dpp_domain::check_personal_data(passport); + if errors.is_empty() { + Ok(()) + } else { + Err(DppError::Validation(dpp_domain::ValidationErrors { + errors, + })) + } +} + /// `https` + the SSRF shape guard on the URI, and a lowercase-hex SHA-256 pin — /// the create path's `validate_passport_ref`, applied on update too. pub(crate) fn validate_component_ref(c: &ComponentRef, index: usize) -> Result<(), String> { @@ -578,6 +616,7 @@ mod tests { compliance_result: None, lint_result: None, product_group_data: None, + personal_data: Default::default(), status: PassportStatus::Draft, qr_code_url: None, jws_signature: None, @@ -988,4 +1027,110 @@ mod tests { assert!(local_component_id("https://id.other.example/dpp/opaque-slug").is_none()); assert!(local_component_id("").is_none()); } + + // ── personal data ──────────────────────────────────────────────────────── + + /// The field a battery schema marks as able to hold personal data. + const MARKED: &str = "usageHistory.negativeEvents"; + + /// An electric-vehicle battery whose accident log holds an entry: the shipped + /// demo passport, with the one marked field filled in. + fn battery_with_an_accident_logged() -> Passport { + let raw = include_str!("../../../../../ops/demo/passports/04-battery-ev-nmc.json"); + let mut body: serde_json::Value = serde_json::from_str(raw).expect("demo passport is JSON"); + body["productGroupData"]["usageHistory"]["negativeEvents"] = + serde_json::json!(["collision; pack housing deformed"]); + let mut p = stub(); + p.product_group_data = Some( + serde_json::from_value(body["productGroupData"].clone()) + .expect("demo battery data deserialises"), + ); + p + } + + fn refusal_fields(error: DppError) -> Vec { + match error { + DppError::Validation(v) => v.errors.into_iter().map(|e| e.field).collect(), + other => panic!("expected a field-level refusal, got {other:?}"), + } + } + + /// 🚨 The prohibition is on *storing* customer personal data, and a draft is + /// stored. A marked field with a value and no statement is refused on the + /// write itself, with a `422` that names the field — not left until publish. + #[test] + fn a_marked_field_with_a_value_and_no_statement_is_refused() { + let p = battery_with_an_accident_logged(); + assert!(p.personal_data.is_empty()); + + let fields = refusal_fields(super::guard_personal_data(&p).expect_err("no statement")); + assert_eq!(fields, ["/productGroupData/usageHistory/negativeEvents"]); + } + + #[test] + fn a_statement_that_nothing_is_held_lets_the_write_through() { + let mut p = battery_with_an_accident_logged(); + apply_patch( + &mut p, + &serde_json::json!({ "personalData": { MARKED: { "held": "nothing" } } }), + ) + .expect("a statement is patchable"); + + assert!(super::guard_personal_data(&p).is_ok()); + } + + #[test] + fn a_statement_about_a_field_that_is_not_marked_is_refused() { + let mut p = stub(); + apply_patch( + &mut p, + &serde_json::json!({ "personalData": { "productName": { "held": "nothing" } } }), + ) + .expect("a statement is patchable"); + + let fields = refusal_fields(super::guard_personal_data(&p).expect_err("not marked")); + assert_eq!(fields, ["/personalData/productName"]); + } + + /// The patch is judged as a whole: changing `productGroupData` to hold + /// something while `personalData` says nothing about it is one write, and + /// the record it would store is refused. + #[test] + fn a_patch_that_adds_marked_content_is_judged_with_the_statements_it_leaves() { + let filled = battery_with_an_accident_logged(); + let mut p = stub(); + p.product_group_data = filled.product_group_data.clone(); + apply_patch( + &mut p, + &serde_json::json!({ "personalData": { MARKED: { "held": "nothing" } } }), + ) + .expect("a statement is patchable"); + assert!(super::guard_personal_data(&p).is_ok()); + + // Withdrawing the statements leaves the content bare again. + let applied = apply_patch(&mut p, &serde_json::json!({ "personalData": {} })) + .expect("an empty map is patchable"); + assert_eq!(applied, ["personalData"]); + assert!(super::guard_personal_data(&p).is_err()); + } + + #[test] + fn a_malformed_statement_is_a_validation_error_not_a_panic() { + let mut p = stub(); + for bad in [ + serde_json::json!({ MARKED: { "held": "somewhere" } }), + serde_json::json!({ MARKED: "nothing" }), + serde_json::json!(["not", "a", "map"]), + ] { + let result = apply_patch(&mut p, &serde_json::json!({ "personalData": bad })); + assert!( + matches!(result, Err(DppError::Validation(_))), + "{bad} must be refused" + ); + } + assert!( + p.personal_data.is_empty(), + "a refused patch applies nothing" + ); + } } diff --git a/crates/dpp-vault/src/domain/service/mod.rs b/crates/dpp-vault/src/domain/service/mod.rs index af1bca02..aa07ad4a 100644 --- a/crates/dpp-vault/src/domain/service/mod.rs +++ b/crates/dpp-vault/src/domain/service/mod.rs @@ -541,6 +541,7 @@ mod snapshot_render_tests { compliance_result: None, lint_result: None, product_group_data: None, + personal_data: Default::default(), status: PassportStatus::Published, qr_code_url: Some("https://id.example/01/09506000134352".into()), jws_signature: Some("full.jws.signature".into()), diff --git a/crates/dpp-vault/src/domain/service/publish.rs b/crates/dpp-vault/src/domain/service/publish.rs index 30ab2e74..70e177f8 100644 --- a/crates/dpp-vault/src/domain/service/publish.rs +++ b/crates/dpp-vault/src/domain/service/publish.rs @@ -862,6 +862,7 @@ mod tests { compliance_result: None, lint_result: None, product_group_data: None, + personal_data: Default::default(), status: PassportStatus::Draft, qr_code_url: None, jws_signature: None, @@ -1030,6 +1031,8 @@ mod tests { commodity_code: None, backup_url: None, product_identifier: None, + batch_id: None, + item_serial: None, service_provider: None, } } diff --git a/crates/dpp-vault/src/domain/service/query.rs b/crates/dpp-vault/src/domain/service/query.rs index f17bb1f3..df138e46 100644 --- a/crates/dpp-vault/src/domain/service/query.rs +++ b/crates/dpp-vault/src/domain/service/query.rs @@ -606,6 +606,7 @@ mod label_resolution { compliance_result: None, lint_result: None, product_group_data: Some(ProductGroupData::Battery(Box::new(battery))), + personal_data: Default::default(), status, qr_code_url: None, jws_signature: None, diff --git a/crates/dpp-vault/src/domain/verify/engine.rs b/crates/dpp-vault/src/domain/verify/engine.rs index 6052c173..698ccafa 100644 --- a/crates/dpp-vault/src/domain/verify/engine.rs +++ b/crates/dpp-vault/src/domain/verify/engine.rs @@ -420,22 +420,29 @@ mod tests { use std::collections::BTreeMap; use uuid::Uuid; - fn did_doc_for(signing_key: &SigningKey) -> serde_json::Value { + /// The document `did` publishes: one method, `{did}#root`, controlled by it. + fn did_doc_for(did: &str, signing_key: &SigningKey) -> serde_json::Value { let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD; let x = b64.encode(signing_key.verifying_key().to_bytes()); serde_json::json!({ + "id": did, "verificationMethod": [{ - "id": "did:web:node.example#root", - "type": "JsonWebKey2020", + "id": format!("{did}#root"), + "type": "JsonWebKey", + "controller": did, "publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": x }, }], - "assertionMethod": ["did:web:node.example#root"], + "assertionMethod": [format!("{did}#root")], }) } - fn sign(signing_key: &SigningKey, payload: &serde_json::Value) -> String { + /// A signature by `did`'s key, naming it by the absolute URL of its method. + fn sign(did: &str, signing_key: &SigningKey, payload: &serde_json::Value) -> String { let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD; - let header = b64.encode(serde_json::to_vec(&serde_json::json!({"alg": "EdDSA"})).unwrap()); + let header = b64.encode( + serde_json::to_vec(&serde_json::json!({"alg": "EdDSA", "kid": format!("{did}#root")})) + .unwrap(), + ); let body = b64.encode(canonicalize(payload).unwrap()); let signing_input = format!("{header}.{body}"); let sig = signing_key.sign(signing_input.as_bytes()); @@ -489,15 +496,15 @@ mod tests { manifest_jws: String::new(), full_view: SignedLayer { payload: full_payload.clone(), - jws: sign(signing_key, &full_payload), + jws: sign("did:web:node.example", signing_key, &full_payload), }, public_view: SignedLayer { payload: public_payload.clone(), - jws: sign(signing_key, &public_payload), + jws: sign("did:web:node.example", signing_key, &public_payload), }, did_documents: BTreeMap::from([( "did:web:node.example".to_string(), - did_doc_for(signing_key), + did_doc_for("did:web:node.example", signing_key), )]), audit_entries, transfer_chain: None, @@ -511,7 +518,7 @@ mod tests { dossier.manifest.content_hashes = compute_content_hashes(&dossier).expect("test dossier canonicalises"); let manifest_value = serde_json::to_value(&dossier.manifest).unwrap(); - dossier.manifest_jws = sign(signing_key, &manifest_value); + dossier.manifest_jws = sign("did:web:node.example", signing_key, &manifest_value); dossier } @@ -536,7 +543,7 @@ mod tests { dossier.manifest.content_hashes = compute_content_hashes(&dossier).expect("test dossier canonicalises"); let manifest_value = serde_json::to_value(&dossier.manifest).unwrap(); - dossier.manifest_jws = sign(signing_key, &manifest_value); + dossier.manifest_jws = sign("did:web:node.example", signing_key, &manifest_value); dossier } @@ -623,7 +630,7 @@ mod tests { dossier.manifest.content_hashes = compute_content_hashes(&dossier).expect("test dossier canonicalises"); let manifest_value = serde_json::to_value(&dossier.manifest).unwrap(); - dossier.manifest_jws = sign(&signing_key, &manifest_value); + dossier.manifest_jws = sign("did:web:node.example", &signing_key, &manifest_value); assert_eq!( *by_name(&verify_dossier(&dossier, None), "content_integrity"), CheckStatus::Pass @@ -689,7 +696,7 @@ mod tests { dossier.manifest.content_hashes = compute_content_hashes(&dossier).expect("test dossier canonicalises"); let manifest_value = serde_json::to_value(&dossier.manifest).unwrap(); - dossier.manifest_jws = sign(&signing_key, &manifest_value); + dossier.manifest_jws = sign("did:web:node.example", &signing_key, &manifest_value); let report = verify_dossier(&dossier, None); assert!(matches!( @@ -753,29 +760,35 @@ mod tests { notes: None, }; let payload = record.signing_payload(); - record.from_signature = Some(sign(&from_key, &payload)); + record.from_signature = Some(sign("did:web:from.example", &from_key, &payload)); // Signed by the dossier's *issuer* — the hosting node — over the // acceptance payload, which is what the verifier now checks it against. // Signing it with `to_key` over the initiation payload, as this once // did, is the shape that made the attestation indistinguishable from a // copy of `from_signature`. - record.node_acceptance_attestation = Some(sign(&signing_key, &acceptance_payload(&record))); + record.node_acceptance_attestation = Some(sign( + "did:web:node.example", + &signing_key, + &acceptance_payload(&record), + )); dossier.transfer_chain = Some(TransferChain { passport_id: record.passport_id, original_operator: operator("did:web:from.example"), transfers: vec![record], }); - dossier - .did_documents - .insert("did:web:from.example".to_string(), did_doc_for(&from_key)); - dossier - .did_documents - .insert("did:web:to.example".to_string(), did_doc_for(&to_key)); + dossier.did_documents.insert( + "did:web:from.example".to_string(), + did_doc_for("did:web:from.example", &from_key), + ); + dossier.did_documents.insert( + "did:web:to.example".to_string(), + did_doc_for("did:web:to.example", &to_key), + ); dossier.manifest.content_hashes = compute_content_hashes(&dossier).expect("test dossier canonicalises"); let manifest_value = serde_json::to_value(&dossier.manifest).unwrap(); - dossier.manifest_jws = sign(&signing_key, &manifest_value); + dossier.manifest_jws = sign("did:web:node.example", &signing_key, &manifest_value); // Sanity: clean chain verifies before we tamper it. let clean_report = verify_dossier(&dossier, None); @@ -798,7 +811,7 @@ mod tests { dossier.manifest.content_hashes = compute_content_hashes(&dossier).expect("test dossier canonicalises"); let manifest_value = serde_json::to_value(&dossier.manifest).unwrap(); - dossier.manifest_jws = sign(&signing_key, &manifest_value); + dossier.manifest_jws = sign("did:web:node.example", &signing_key, &manifest_value); let report = verify_dossier(&dossier, None); assert!(matches!( @@ -882,7 +895,7 @@ mod tests { dossier.manifest.content_hashes = compute_content_hashes(&dossier).expect("test dossier canonicalises"); let manifest_value = serde_json::to_value(&dossier.manifest).unwrap(); - dossier.manifest_jws = sign(&signing_key, &manifest_value); + dossier.manifest_jws = sign("did:web:node.example", &signing_key, &manifest_value); let mut value = serde_json::to_value(&dossier).unwrap(); value["transferChain"]["transfers"][0]["certificationStatus"] = diff --git a/crates/dpp-vault/src/domain/verify/jws.rs b/crates/dpp-vault/src/domain/verify/jws.rs index 37851d49..8e339af7 100644 --- a/crates/dpp-vault/src/domain/verify/jws.rs +++ b/crates/dpp-vault/src/domain/verify/jws.rs @@ -1,29 +1,22 @@ //! Dossier-side JWS helpers on top of `dpp_crypto::jws`. //! -//! `dpp-crypto` exposes the raw verifier and the DID-document key-extraction -//! functions; what it does not expose is payload decoding and content -//! binding, which dossier verification needs — those live here. +//! `dpp-crypto` exposes the raw verifier and the DID-document key resolution; +//! what it does not expose is payload decoding and content binding, which +//! dossier verification needs — those live here. use anyhow::anyhow; use base64::Engine; -use dpp_crypto::jws::{ - canonicalize, extract_key_by_fingerprint, extract_kid_from_jws, extract_primary_public_key, - verify_jws, -}; +use dpp_crypto::jws::{canonicalize, resolve_verification_key, verify_jws}; /// Resolve the public key to verify `jws` against, from a DID document. /// -/// If the JWS carries a `kid`, it must resolve to a key by fingerprint (this -/// includes rotation-archived keys). A present-but-unresolvable `kid` (revoked, -/// rotated out, or simply wrong) returns `None` so the caller surfaces an -/// accurate "kid does not resolve" diagnosis — it does **not** silently -/// substitute the primary key. The primary-key fallback is reserved for legacy -/// tokens signed before `kid` was added, i.e. tokens carrying no `kid` at all. +/// The `kid` must name a verification method the document publishes and +/// references from `assertionMethod` (this includes rotation-archived keys). A +/// `kid` that names none (revoked, rotated out, or simply wrong) returns `None` +/// so the caller surfaces an accurate "kid does not resolve" diagnosis. So does a +/// token with no `kid` at all: it is not checked against a key chosen for it. pub(crate) fn resolve_public_key(jws: &str, did_document: &serde_json::Value) -> Option { - match extract_kid_from_jws(jws) { - Some(kid) => extract_key_by_fingerprint(did_document, &kid), - None => extract_primary_public_key(did_document), - } + resolve_verification_key(did_document, jws) } /// Decode the payload segment of a compact JWS to raw bytes (post-base64, @@ -85,16 +78,21 @@ mod tests { format!("{signing_input}.{}", b64.encode(sig.to_bytes())) } + /// The method the document publishes, which a `kid` names in full. + const METHOD: &str = "did:web:example.com#root"; + fn did_doc_for(signing_key: &SigningKey) -> serde_json::Value { let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD; let x = b64.encode(signing_key.verifying_key().to_bytes()); serde_json::json!({ + "id": "did:web:example.com", "verificationMethod": [{ - "id": "did:web:example.com#root", - "type": "JsonWebKey2020", + "id": METHOD, + "type": "JsonWebKey", + "controller": "did:web:example.com", "publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": x }, }], - "assertionMethod": ["did:web:example.com#root"], + "assertionMethod": [METHOD], }) } @@ -102,10 +100,10 @@ mod tests { fn content_binding_rejects_signature_over_different_content() { let signing_key = SigningKey::from_bytes(&[7u8; 32]); let did_doc = did_doc_for(&signing_key); - let key = resolve_public_key("x.x.x", &did_doc).unwrap(); let signed = serde_json::json!({"a": 1}); - let jws = sign(&signing_key, &signed); + let jws = sign_with_kid(&signing_key, &signed, METHOD); + let key = resolve_public_key(&jws, &did_doc).unwrap(); assert!(verify_jws_content(&jws, &key, &signed).unwrap()); let other = serde_json::json!({"a": 2}); @@ -116,12 +114,14 @@ mod tests { } #[test] - fn resolve_falls_back_to_primary_key_without_kid() { + fn a_token_without_a_kid_resolves_to_no_key() { let signing_key = SigningKey::from_bytes(&[7u8; 32]); let did_doc = did_doc_for(&signing_key); let jws = sign(&signing_key, &serde_json::json!({"a": 1})); // header has no kid - let key = resolve_public_key(&jws, &did_doc); - assert!(key.is_some(), "must fall back to the primary key"); + assert!( + resolve_public_key(&jws, &did_doc).is_none(), + "a token that names no key must not be checked against the document's only one" + ); } #[test] @@ -134,7 +134,7 @@ mod tests { let jws = sign_with_kid( &signing_key, &serde_json::json!({"a": 1}), - "not-a-real-fingerprint", + "did:web:example.com#not-a-real-method", ); assert!( resolve_public_key(&jws, &did_doc).is_none(), diff --git a/crates/dpp-vault/src/domain/verify/transfer_chain.rs b/crates/dpp-vault/src/domain/verify/transfer_chain.rs index 3e5de8df..529875e8 100644 --- a/crates/dpp-vault/src/domain/verify/transfer_chain.rs +++ b/crates/dpp-vault/src/domain/verify/transfer_chain.rs @@ -225,22 +225,29 @@ mod tests { } } - fn did_doc_for(signing_key: &SigningKey) -> serde_json::Value { + /// The document `did` publishes: one method, `{did}#root`, controlled by it. + fn did_doc_for(did: &str, signing_key: &SigningKey) -> serde_json::Value { let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD; let x = b64.encode(signing_key.verifying_key().to_bytes()); serde_json::json!({ + "id": did, "verificationMethod": [{ - "id": "did:web:example.com#root", - "type": "JsonWebKey2020", + "id": format!("{did}#root"), + "type": "JsonWebKey", + "controller": did, "publicKeyJwk": { "kty": "OKP", "crv": "Ed25519", "x": x }, }], - "assertionMethod": ["did:web:example.com#root"], + "assertionMethod": [format!("{did}#root")], }) } - fn sign(signing_key: &SigningKey, payload: &serde_json::Value) -> String { + /// A signature by `did`'s key, naming it by the absolute URL of its method. + fn sign(did: &str, signing_key: &SigningKey, payload: &serde_json::Value) -> String { let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD; - let header = b64.encode(serde_json::to_vec(&serde_json::json!({"alg": "EdDSA"})).unwrap()); + let header = b64.encode( + serde_json::to_vec(&serde_json::json!({"alg": "EdDSA", "kid": format!("{did}#root")})) + .unwrap(), + ); let body = b64.encode(canonicalize(payload).unwrap()); let signing_input = format!("{header}.{body}"); let sig = signing_key.sign(signing_input.as_bytes()); @@ -272,8 +279,9 @@ mod tests { cancelled_at: None, notes: None, }; - record.from_signature = Some(sign(from_key, &record.signing_payload())); - record.node_acceptance_attestation = Some(sign(node_key, &acceptance_payload(&record))); + record.from_signature = Some(sign(from_did, from_key, &record.signing_payload())); + record.node_acceptance_attestation = + Some(sign(NODE_DID, node_key, &acceptance_payload(&record))); record } @@ -293,8 +301,11 @@ mod tests { transfers: vec![record], }; let mut docs = BTreeMap::new(); - docs.insert("did:web:from.example".to_string(), did_doc_for(&from_key)); - docs.insert(NODE_DID.to_string(), did_doc_for(&node_key)); + docs.insert( + "did:web:from.example".to_string(), + did_doc_for("did:web:from.example", &from_key), + ); + docs.insert(NODE_DID.to_string(), did_doc_for(NODE_DID, &node_key)); assert!(verify_transfer_chain(&chain, &docs, NODE_DID).is_ok()); } @@ -318,8 +329,11 @@ mod tests { transfers: vec![record], }; let mut docs = BTreeMap::new(); - docs.insert("did:web:from.example".to_string(), did_doc_for(&from_key)); - docs.insert(NODE_DID.to_string(), did_doc_for(&node_key)); + docs.insert( + "did:web:from.example".to_string(), + did_doc_for("did:web:from.example", &from_key), + ); + docs.insert(NODE_DID.to_string(), did_doc_for(NODE_DID, &node_key)); // This assertion has now been both ways, and the previous comment asked // for exactly this moment: it said that if verification were @@ -391,7 +405,7 @@ mod tests { // acceptance attestation stopped being verified against the incoming // operator's DID — no `to` document is consulted at all now. let mut docs = BTreeMap::new(); - docs.insert(NODE_DID.to_string(), did_doc_for(&node_key)); + docs.insert(NODE_DID.to_string(), did_doc_for(NODE_DID, &node_key)); let brk = verify_transfer_chain(&chain, &docs, NODE_DID).expect_err("must fail closed"); assert!(matches!(brk.issue, TransferSignatureIssue::From(_))); @@ -422,7 +436,10 @@ mod tests { transfers: vec![record], }; let mut docs = BTreeMap::new(); - docs.insert("did:web:from.example".to_string(), did_doc_for(&from_key)); + docs.insert( + "did:web:from.example".to_string(), + did_doc_for("did:web:from.example", &from_key), + ); let brk = verify_transfer_chain(&chain, &docs, NODE_DID) .expect_err("a completed transfer must carry the node's acceptance attestation"); @@ -454,7 +471,10 @@ mod tests { transfers: vec![record], }; let mut docs = BTreeMap::new(); - docs.insert("did:web:from.example".to_string(), did_doc_for(&from_key)); + docs.insert( + "did:web:from.example".to_string(), + did_doc_for("did:web:from.example", &from_key), + ); assert!(verify_transfer_chain(&chain, &docs, NODE_DID).is_ok()); } @@ -509,14 +529,17 @@ mod tests { notes: None, }; let payload = record.signing_payload(); - record.from_signature = Some(sign(&from_key, &payload)); + record.from_signature = Some(sign("did:web:from.example", &from_key, &payload)); let chain = TransferChain { passport_id: record.passport_id, original_operator: operator("did:web:from.example"), transfers: vec![record], }; let mut docs = BTreeMap::new(); - docs.insert("did:web:from.example".to_string(), did_doc_for(&from_key)); + docs.insert( + "did:web:from.example".to_string(), + did_doc_for("did:web:from.example", &from_key), + ); assert!( verify_transfer_chain(&chain, &docs, NODE_DID).is_ok(), "an initiated (uncompleted) record must not fail on its pending countersignature" diff --git a/crates/dpp-vault/src/handlers/create.rs b/crates/dpp-vault/src/handlers/create.rs index c36ba602..0a0724a4 100644 --- a/crates/dpp-vault/src/handlers/create.rs +++ b/crates/dpp-vault/src/handlers/create.rs @@ -17,7 +17,7 @@ use uuid::Uuid; use crate::{middleware::scope::RequireWrite, state::AppState}; -use super::error::{api_error, field_conflict_error, internal_error}; +use super::error::{api_error, field_conflict_error, field_validation_error, internal_error}; use crate::extract::Json; /// Request body for passport creation. @@ -89,7 +89,7 @@ pub async fn create_handler( .co2e_per_unit .or_else(|| { body.product_group_data.as_ref().and_then(|sd| match sd { - ProductGroupData::Battery(b) => Some(b.co2e_per_unit_kg), + ProductGroupData::Battery(b) => b.co2e_per_unit_kg, ProductGroupData::Textile(t) => t.carbon_footprint_kg_co2e, _ => None, }) @@ -137,6 +137,7 @@ pub async fn create_handler( compliance_result: None, lint_result: None, product_group_data: body.product_group_data, + personal_data: body.personal_data, status: PassportStatus::Draft, qr_code_url: None, jws_signature: None, @@ -198,6 +199,10 @@ pub async fn create_handler( Json(crate::api::PassportResponse::from(&p)), ) .into_response(), + // What the service refuses on the assembled record rather than on the + // request: a field able to hold personal data that has a value and no + // statement, which names the field. + Err(dpp_domain::DppError::Validation(errs)) => field_validation_error(&errs), Err(e) => internal_error(e), } } @@ -377,7 +382,7 @@ mod schema_validation { nominal_voltage_v: 3.2, nominal_capacity_ah: 100.0, expected_lifetime_cycles: Some(3000), - co2e_per_unit_kg: 85.4, + co2e_per_unit_kg: Some(85.4), recycled_content_cobalt_pct: None, recycled_content_lithium_pct: Some(12.5), recycled_content_nickel_pct: None, diff --git a/crates/dpp-vault/src/infra/identity_client.rs b/crates/dpp-vault/src/infra/identity_client.rs index 7154411e..b9094a04 100644 --- a/crates/dpp-vault/src/infra/identity_client.rs +++ b/crates/dpp-vault/src/infra/identity_client.rs @@ -223,7 +223,7 @@ mod tests { .decode(payload_b64) .unwrap(); let payload: serde_json::Value = serde_json::from_slice(&payload_bytes).unwrap(); - let jws = dpp_crypto::jws::sign(&mock.store, "root", &payload).unwrap(); + let jws = dpp_crypto::jws::sign(&mock.store, "root", &payload, "test-kid").unwrap(); ( StatusCode::OK, Json(serde_json::json!({"jws_signature": jws})), diff --git a/crates/dpp-vault/src/infra/status_list.rs b/crates/dpp-vault/src/infra/status_list.rs index 7db71ab1..fde9c94c 100644 --- a/crates/dpp-vault/src/infra/status_list.rs +++ b/crates/dpp-vault/src/infra/status_list.rs @@ -95,6 +95,7 @@ mod tests { builder = builder.with_status(CredentialStatus { id: format!("{url}#0"), status_type: "BitstringStatusListEntry".into(), + status_purpose: "revocation".into(), status_list_index: Some("0".into()), status_list_credential: Some(url), }); diff --git a/crates/dpp-vault/src/middleware/credential.rs b/crates/dpp-vault/src/middleware/credential.rs index 9645b999..f7461b0e 100644 --- a/crates/dpp-vault/src/middleware/credential.rs +++ b/crates/dpp-vault/src/middleware/credential.rs @@ -9,9 +9,11 @@ //! Chosen over an embedded W3C Data Integrity `proof` member for four reasons, //! recorded here because it is a wire-format decision that binds every issuer: //! -//! 1. It reuses the verification path this workspace already has — -//! [`extract_kid_from_jws`] → issuer DID document → [`extract_key_by_fingerprint`] -//! → [`verify_jws`], with EdDSA pinned and `alg:none` rejected. +//! 1. It reuses the verification path core already has — +//! [`authenticate_access_credential`]: the `kid` names a verification method +//! in the issuer's DID document, the signature is checked against that key +//! with EdDSA pinned and `alg:none` rejected, and the token must be typed +//! `vc+jwt` so another signature of the same key cannot pass for a credential. //! 2. One canonicalisation scheme (JCS) rather than two. A second signing scheme //! would be a permanent review burden on the most security-sensitive code here. //! 3. Header-safe by construction: base64url, no escaping. @@ -41,11 +43,11 @@ use axum::http::{HeaderMap, StatusCode}; use axum::response::{IntoResponse, Response}; use base64::Engine; use dpp_common::http_problem; -use dpp_crypto::jws::verifier::{ - extract_key_by_fingerprint, extract_kid_from_jws, extract_primary_public_key, verify_jws, -}; use dpp_domain::Audience; -use dpp_vc::{DppAccessCredential, StatusList, TrustedIssuerRegistry}; +use dpp_vc::{ + DppAccessCredential, StatusList, TrustedIssuerRegistry, + credential::authenticate_access_credential, +}; /// The header a caller presents an access credential in. pub const CREDENTIAL_HEADER: &str = "X-DPP-Credential"; @@ -218,28 +220,26 @@ pub async fn read_and_verify( return CredentialOutcome::Rejected(reject("Credential header is not valid UTF-8.")); }; - let Some(credential) = decode_payload(jws) else { + let Some(unverified) = decode_payload(jws) else { return CredentialOutcome::Rejected(reject( "Credential is not a compact JWS carrying a DPP access credential.", )); }; // Authenticate before doing anything with the claims: until the signature - // verifies, `issuer` is a string the caller chose. - let Some(did_doc) = directory.did_document(&credential.issuer).await else { + // verifies, `issuer` is a string the caller chose, and it is used only to + // find the document the signature is checked against. + let Some(did_doc) = directory.did_document(&unverified.issuer).await else { return CredentialOutcome::Rejected(reject("Credential issuer DID could not be resolved.")); }; - let key = extract_kid_from_jws(jws) - .and_then(|kid| extract_key_by_fingerprint(&did_doc, &kid)) - .or_else(|| extract_primary_public_key(&did_doc)); - let Some(key) = key else { - return CredentialOutcome::Rejected(reject( - "Credential issuer DID document has no matching verification key.", - )); + // Core's reading of the token: the key its `kid` names in the issuer's + // document, the signature, the `vc+jwt` type that keeps another signature of + // the same key from passing as a credential, and the issuer the document + // belongs to. A token with no `kid` names no key and is refused. + let credential = match authenticate_access_credential(jws, &did_doc) { + Ok(credential) => credential, + Err(result) => return CredentialOutcome::Rejected(reject(rejection_detail(&result))), }; - if !verify_jws(jws, &key).unwrap_or(false) { - return CredentialOutcome::Rejected(reject("Credential signature did not verify.")); - } // Only now that the signature holds is it worth spending a network round // trip on revocation: an unsigned document's status URL is attacker chosen. @@ -297,10 +297,11 @@ fn rejection_detail(result: &dpp_vc::VerificationResult) -> &'static str { V::Revoked => "Credential has been revoked, or its revocation status could not be checked.", V::UntrustedIssuer { .. } => "Credential issuer is not trusted by this node.", V::OutOfScope { .. } => "Credential does not cover the requested product.", - // Not reachable from here — the signature is verified against the - // issuer's published key before core is called, and that failure has its - // own message at the call site — but mapped rather than lumped in so a - // future reordering cannot silently produce a misleading detail. + // The authentication step's refusal: no key matching the `kid`, a + // signature that does not verify, a token that is not typed as a + // credential, or an issuer other than the document's. Core's text names + // the `kid` and the document, which came from the caller, so one fixed + // sentence stands for all of them. V::InvalidSignature(_) => "Credential signature did not verify.", // `Valid` is unreachable — the caller checks `is_valid()` first — and // `MalformedCredential` carries text derived from the credential. @@ -341,9 +342,21 @@ mod tests { base64::engine::general_purpose::URL_SAFE_NO_PAD; /// Mint a compact JWS over the JCS-canonical credential, exactly as - /// `dpp_crypto::jws::sign` does — header `{alg,kid}`, JCS payload, EdDSA. + /// `dpp_vc::credential::sign_access_credential` does: the `kid` is the + /// absolute URL of the issuer's verification method, the token is typed as + /// a VC-JWT (`typ: vc+jwt`, `cty: vc`), the payload is JCS, the algorithm + /// EdDSA. fn sign_credential(key: &SigningKey, kid: &str, cred: &DppAccessCredential) -> String { - let header = B64.encode(format!(r#"{{"alg":"EdDSA","kid":"{kid}"}}"#).as_bytes()); + sign_with_header( + key, + &format!(r#"{{"alg":"EdDSA","cty":"vc","kid":"{ISSUER}#{kid}","typ":"vc+jwt"}}"#), + cred, + ) + } + + /// The same, under whatever protected header the caller states. + fn sign_with_header(key: &SigningKey, header_json: &str, cred: &DppAccessCredential) -> String { + let header = B64.encode(header_json.as_bytes()); let value = serde_json::to_value(cred).expect("to value"); let canonical = dpp_crypto::jws::canonicalize(&value).expect("canonicalize"); let payload = B64.encode(&canonical); @@ -487,6 +500,51 @@ mod tests { assert!(matches!(out, CredentialOutcome::Rejected(_))); } + /// 🚨 A signature by the issuer's key is authentic whatever it signs, so a + /// passport proof or an SD-JWT VC from the same key could otherwise be + /// offered as a credential. The type is what says this token is one. + #[tokio::test] + async fn the_issuers_signature_over_a_credential_shaped_payload_must_be_typed_as_one() { + let (key, kid) = key_and_kid(); + let cred = credential(CredentialRole::Recycler, 30); + let directory = Directory::with(Some(did_doc(&key, &kid))); + + for header in [ + // No type at all, as a passport proof is signed. + format!(r#"{{"alg":"EdDSA","kid":"{ISSUER}#{kid}"}}"#), + // The type of another credential format. + format!(r#"{{"alg":"EdDSA","kid":"{ISSUER}#{kid}","typ":"dc+sd-jwt"}}"#), + // The right type with the wrong content type. + format!(r#"{{"alg":"EdDSA","cty":"other","kid":"{ISSUER}#{kid}","typ":"vc+jwt"}}"#), + ] { + let jws = sign_with_header(&key, &header, &cred); + let out = read_and_verify(&headers_with(Some(&jws)), &directory, &trusting()).await; + assert!( + matches!(out, CredentialOutcome::Rejected(_)), + "{header} must not pass for a credential" + ); + } + } + + /// A token that names no key is not checked against the one the document + /// happens to publish. + #[tokio::test] + async fn a_credential_that_names_no_key_is_refused() { + let (key, kid) = key_and_kid(); + let jws = sign_with_header( + &key, + r#"{"alg":"EdDSA","cty":"vc","typ":"vc+jwt"}"#, + &credential(CredentialRole::Recycler, 30), + ); + let out = read_and_verify( + &headers_with(Some(&jws)), + &Directory::with(Some(did_doc(&key, &kid))), + &trusting(), + ) + .await; + assert!(matches!(out, CredentialOutcome::Rejected(_))); + } + #[tokio::test] async fn an_unresolvable_issuer_fails_closed() { let (key, kid) = key_and_kid(); @@ -592,6 +650,7 @@ mod tests { cred.credential_status = Some(dpp_vc::CredentialStatus { id: "https://issuer.example/status#4".to_owned(), status_type: "BitstringStatusListEntry".to_owned(), + status_purpose: "revocation".to_owned(), status_list_index: Some("4".to_owned()), status_list_credential: Some("https://issuer.example/status".to_owned()), }); diff --git a/crates/dpp-vault/src/public_view.rs b/crates/dpp-vault/src/public_view.rs index 706299e0..f9dc69b9 100644 --- a/crates/dpp-vault/src/public_view.rs +++ b/crates/dpp-vault/src/public_view.rs @@ -829,6 +829,7 @@ pub(crate) mod tests { compliance_result: None, lint_result: None, product_group_data: None, + personal_data: Default::default(), status: PassportStatus::Published, qr_code_url: None, jws_signature: None, diff --git a/crates/dpp-vault/tests/audience_read_route.rs b/crates/dpp-vault/tests/audience_read_route.rs index cca9441b..bb20cc99 100644 --- a/crates/dpp-vault/tests/audience_read_route.rs +++ b/crates/dpp-vault/tests/audience_read_route.rs @@ -54,7 +54,9 @@ fn issuer_key() -> SigningKey { /// Mint a compact JWS over the JCS-canonical credential, exactly as an issuer /// would. fn sign_credential(key: &SigningKey, cred: &DppAccessCredential) -> String { - let header = B64.encode(format!(r#"{{"alg":"EdDSA","kid":"{KID}"}}"#).as_bytes()); + let header = B64.encode( + format!(r#"{{"alg":"EdDSA","cty":"vc","kid":"{ISSUER}#{KID}","typ":"vc+jwt"}}"#).as_bytes(), + ); let value = serde_json::to_value(cred).expect("credential to value"); let canonical = dpp_crypto::jws::canonicalize(&value).expect("canonicalize"); let payload = B64.encode(&canonical); diff --git a/crates/dpp-vault/tests/continuity_snapshot.rs b/crates/dpp-vault/tests/continuity_snapshot.rs index 49eb14fc..78bf34ee 100644 --- a/crates/dpp-vault/tests/continuity_snapshot.rs +++ b/crates/dpp-vault/tests/continuity_snapshot.rs @@ -190,13 +190,23 @@ async fn build_service() -> ( store.generate_key("root").expect("generate key"); let did_document = dpp_vc::build_did_document(&store, "snapshot-test.example.com", "root") .expect("build did document"); - let public_key = dpp_crypto::jws::extract_primary_public_key(&did_document) - .expect("did document carries a primary key"); let identity = Arc::new(dpp_vc::LocalIdentityService::new( Arc::new(store), "root".to_owned(), "snapshot-test.example.com".to_owned(), )); + // The key, as a verifier gets it: resolved from the document by the `kid` a + // token this identity signed names. + let probe = dpp_domain::ports::identity::IdentityPort::sign_passport( + identity.as_ref(), + PassportId::new(), + &serde_json::json!({}), + ) + .await + .expect("sign probe") + .jws; + let public_key = dpp_crypto::jws::resolve_verification_key(&did_document, &probe) + .expect("did document carries the signing key"); let snapshots = InMemorySnapshotOutbox::default(); let service = PassportService::new( @@ -256,6 +266,7 @@ fn draft_passport() -> Passport { derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: None, product_id: None, commodity_code: None, diff --git a/crates/dpp-vault/tests/evidence_dossier.rs b/crates/dpp-vault/tests/evidence_dossier.rs index 8e3615a4..4ff31102 100644 --- a/crates/dpp-vault/tests/evidence_dossier.rs +++ b/crates/dpp-vault/tests/evidence_dossier.rs @@ -250,6 +250,7 @@ fn draft_passport() -> Passport { derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: None, product_id: None, commodity_code: None, diff --git a/crates/dpp-vault/tests/seal_repair_route.rs b/crates/dpp-vault/tests/seal_repair_route.rs index 67cc251c..4b7d0fbf 100644 --- a/crates/dpp-vault/tests/seal_repair_route.rs +++ b/crates/dpp-vault/tests/seal_repair_route.rs @@ -141,6 +141,7 @@ async fn seed_with(dal: &PgDal, jws: Option<&str>, seal: Option) derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: None, product_id: None, commodity_code: None, diff --git a/crates/dpp-vault/tests/seal_route.rs b/crates/dpp-vault/tests/seal_route.rs index aca7628c..830c99ca 100644 --- a/crates/dpp-vault/tests/seal_route.rs +++ b/crates/dpp-vault/tests/seal_route.rs @@ -73,6 +73,7 @@ async fn seed(dal: &PgDal, seal: Option, jws: Option<&str>) -> P derived_from: Vec::new(), component_refs: Vec::new(), life_status: None, + personal_data: Default::default(), retention_until: None, product_id: None, commodity_code: None, diff --git a/docs/ops/PRODUCTION-RUNBOOK.md b/docs/ops/PRODUCTION-RUNBOOK.md index 4858c798..ee11f64a 100644 --- a/docs/ops/PRODUCTION-RUNBOOK.md +++ b/docs/ops/PRODUCTION-RUNBOOK.md @@ -100,7 +100,7 @@ Auto-HTTPS, zero certificate ops. (Traefik equivalent if preferred; Caddy is les **Upgrades — the ritual.** (1) backup first; (2) bump the pinned `ODAL_VERSION` on the **staging** VM (your own demo VM is staging), run §2.6 smoke; (3) same on prod; (4) migrations apply at boot (forward-only — **rollback = restore from backup**, so step 1 is the rollback plan); (5) post-deploy: §2.6 items 1–3 minimum + diff `rulesetVersion` on `/vault/api/v1/node/state`. Log every upgrade in the operator record (BUILD-LOG habit, operator-facing). -**Key rotation.** The identity service supports rotation. The `kid`-based verification fix has landed (`dpp-crypto::jws::verifier::extract_key_by_fingerprint` resolves any archived key by its `kid` fingerprint, not just the primary) and has a green rotation regression test (`dpp-identity::handlers::rotate_key::tests::signature_signed_before_rotation_still_verifies_after`) confirming a JWS signed before rotation still verifies afterwards. Follow `rotate_key_handler`'s doc comment for the archive-then-generate ordering. +**Key rotation.** The identity service supports rotation. A key is named by its own thumbprint, so rotating never renames another key, and a token's `kid` is the absolute URL of the verification method that signed it (`dpp-crypto::jws::resolve_verification_key` resolves any archived key by it, not just the primary, and refuses a token that names none). That has a green rotation regression test (`dpp-identity::handlers::rotate_key::tests::signature_signed_before_rotation_still_verifies_after`) confirming a JWS signed before rotation still verifies afterwards. Follow `rotate_key_handler`'s doc comment for the archive-then-generate ordering. **Key custody (managed mode).** Two documented modes, chosen per contract: (a) Odal-held passphrase (full managed; passphrase in your vault + sealed offline copy), or (b) operator-held (they enter it at provisioning; you cannot sign without them — the deepest sovereignty tier). Either way the *custody statement* is part of the managed contract (pack 04 §3.7). diff --git a/ops/demo/dossiers/01-valid-simple.json b/ops/demo/dossiers/01-valid-simple.json index 4b2e95ee..6c1b1b8f 100644 --- a/ops/demo/dossiers/01-valid-simple.json +++ b/ops/demo/dossiers/01-valid-simple.json @@ -16,7 +16,7 @@ { "action": "published", "actor": "api-key", - "entryHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "entryHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { @@ -90,7 +90,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -173,10 +172,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -201,7 +199,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -246,7 +244,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -299,10 +296,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -313,7 +309,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -330,28 +326,34 @@ "didDocuments": { "did:web:node.acme.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:node.acme.example#key-1" + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" + ], + "authentication": [ + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" ], "id": "did:web:node.acme.example", "verificationMethod": [ { "controller": "did:web:node.acme.example", - "id": "did:web:node.acme.example#key-1", + "id": "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "kty": "OKP", "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInNhZmV0eU1lYXN1cmVzIjoiRG8gbm90IG9wZW4gdGhlIHBhY2suIFJlcGxhY2UgaXQgdGhyb3VnaCBhbiBhdXRob3Jpc2VkIGRlYWxlci4gS2VlcCBpdCBhd2F5IGZyb20gaGVhdCBhYm92ZSA2MCBkZWdDLiIsInNwYXJlUGFydHNDb250YWN0cyI6InNwYXJlc0BhY21lLmV4YW1wbGUiLCJzdGF0ZU9mSGVhbHRoIjp7InBhcmFtZXRlclNldCI6InN0YXRpb25hcnlPckxtdCIsInJlbWFpbmluZ0NhcGFjaXR5UGN0Ijo5OS41LCJyZW1haW5pbmdQb3dlckNhcGFiaWxpdHlQY3QiOjk5LjEsInNlbGZEaXNjaGFyZ2VSYXRlUGN0UGVyTW9udGgiOjJ9LCJzdGF0ZU9mSGVhbHRoUGN0IjpudWxsLCJ0ZXN0UmVwb3J0UmVzdWx0cyI6IkVOIDUwNjA0LTE6MjAxNiBwYXNzOyBVTiAzOC4zIHBhc3M7IHJlcG9ydCBBQ01FLVRSLTIwMjYtMDgxNSIsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsInVzYWdlSGlzdG9yeSI6eyJjaGFyZ2VEaXNjaGFyZ2VDeWNsZXMiOjR9LCJ3YXN0ZUJhdHRlcnlJbmZvcm1hdGlvbiI6IlJldHVybiB0aGUgYmF0dGVyeSB0byBhIGJpY3ljbGUgcmV0YWlsZXIgb3IgYSBtdW5pY2lwYWwgY29sbGVjdGlvbiBwb2ludC4gRG8gbm90IHBsYWNlIGl0IGluIGhvdXNlaG9sZCB3YXN0ZS4ifSwicHJvZHVjdE5hbWUiOiJBY21lIExNVCA0OFYgMjBBaCBiYXR0ZXJ5IHBhY2siLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9kcHAuYWNtZS5leGFtcGxlLzAxLzA5NTA2MDAwMTM0MzUyLzIxLzdlNGE5YzNkMmYxZThiNmE1ZDQwIiwicmVwYWlyYWJpbGl0eVNjb3JlIjpudWxsLCJyZXRlbnRpb25Mb2NrZWQiOnRydWUsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.55m5_OYvkiKUN9QV9BNB3JeqMOvkYjwYCiPxup_xciGjUrYtHOcaDdy8ULvSAy5NRGpT9p90RrajgBb0Iv5kCg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvbkxvY2tlZCI6dHJ1ZSwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.3BINMac1nxeKuqU19kKCV7k1c21eAr1DJOBT9MOT-5oh6zm3yYg6FNei69EjtkRH9EF1sRxxD_xs3auDY5HoDA", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -423,7 +425,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -506,10 +507,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -534,7 +534,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -542,20 +542,20 @@ }, "manifest": { "contentHashes": { - "auditEntries": "d4ba820a32410f354c8b3ee5cafff919fa183871c955503803f83a5aa4c3b81c", - "fullView": "e8c5a8fbbddd67948a3ee3eb7abfc07427c2ff2ab23eeba3dc4dc9bbf12cbdbd", - "publicView": "74c1a1eda478cc2643b8652bf188f47e449b851ae64ae05f922de314a9249b83" + "auditEntries": "8b653e038f5ab8330b1e094838e1775cf600c64bb9f9256c54412ab0ae4b7a15", + "fullView": "85fdd0d0865b22738ed8ece0cedf0e0d08107e0e13384d3ca2ff06fd3ad02d33", + "publicView": "d1c52ea096bffa8c85b10e3e509d58cb7960574b5f7798a12266a0dac3a6dd04" }, - "coreVersion": "0.21.0", + "coreVersion": "0.22.0", "createdAt": "2026-09-25T12:00:00Z", "formatVersion": "1", "issuerDid": "did:web:node.acme.example", "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImQ0YmE4MjBhMzI0MTBmMzU0YzhiM2VlNWNhZmZmOTE5ZmExODM4NzFjOTU1NTAzODAzZjgzYTVhYTRjM2I4MWMiLCJmdWxsVmlldyI6ImU4YzVhOGZiYmRkZDY3OTQ4YTNlZTNlYjdhYmZjMDc0MjdjMmZmMmFiMjNlZWJhM2RjNGRjOWJiZjEyY2JkYmQiLCJwdWJsaWNWaWV3IjoiNzRjMWExZWRhNDc4Y2MyNjQzYjg2NTJiZjE4OGY0N2U0NDliODUxYWU2NGFlMDVmOTIyZGUzMTRhOTI0OWI4MyJ9LCJjb3JlVmVyc2lvbiI6IjAuMjEuMCIsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjVUMTI6MDA6MDBaIiwiZm9ybWF0VmVyc2lvbiI6IjEiLCJpc3N1ZXJEaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwibm9kZVZlcnNpb24iOiIwLjE0LjAiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIn0.sYutIRK_RIxoxf2GXa4McJKdMayBpejZh-isdp11TgteX7lQyoSAYiwKRGH1FRFlH3k_sywAnm5hJw9tRfJ7AA", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjhiNjUzZTAzOGY1YWI4MzMwYjFlMDk0ODM4ZTE3NzVjZjYwMGM2NGJiOWY5MjU2YzU0NDEyYWIwYWU0YjdhMTUiLCJmdWxsVmlldyI6Ijg1ZmRkMGQwODY1YjIyNzM4ZWQ4ZWNlMGNlZGYwZTBkMDgxMDdlMGUxMzM4NGQzY2EyZmYwNmZkM2FkMDJkMzMiLCJwdWJsaWNWaWV3IjoiZDFjNTJlYTA5NmJmZmE4Yzg1YjEwZTNlNTA5ZDU4Y2I3OTYwNTc0YjVmNzc5OGExMjI2NmEwZGFjM2E2ZGQwNCJ9LCJjb3JlVmVyc2lvbiI6IjAuMjIuMCIsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjVUMTI6MDA6MDBaIiwiZm9ybWF0VmVyc2lvbiI6IjEiLCJpc3N1ZXJEaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwibm9kZVZlcnNpb24iOiIwLjE0LjAiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIn0.b8RdiMO0J2Mxp3KBnFJH61_p2WndkG10zis3FWi-Gc0dT49xSmf2ceGs7oXtCfl3xjHsy27QLDNaM2IaxscqAA", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjbzJlUGVyVW5pdEtnIjo4Ni40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.LHi5ToX900cUN4vak8l1zOZ5xc1H9jW25fL4XUWuxz2JU1_3PxxrjRUTyY9RctPxPSTWmS40fUWurEZnTdE3Dw", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwid2FzdGVCYXR0ZXJ5SW5mb3JtYXRpb24iOiJSZXR1cm4gdGhlIGJhdHRlcnkgdG8gYSBiaWN5Y2xlIHJldGFpbGVyIG9yIGEgbXVuaWNpcGFsIGNvbGxlY3Rpb24gcG9pbnQuIERvIG5vdCBwbGFjZSBpdCBpbiBob3VzZWhvbGQgd2FzdGUuIn0sInByb2R1Y3ROYW1lIjoiQWNtZSBMTVQgNDhWIDIwQWggYmF0dGVyeSBwYWNrIiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInFyQ29kZVVybCI6Imh0dHBzOi8vZHBwLmFjbWUuZXhhbXBsZS8wMS8wOTUwNjAwMDEzNDM1Mi8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.PgFgGaMF1f9RGQGqXEw31ZtocGBvVDfNcu2r2D88eG8wUQZMQLSVkb99NR59UErxDH0GYS_-olz_s5yy4_Z9BQ", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -596,7 +596,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -649,10 +648,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -663,7 +661,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 diff --git a/ops/demo/dossiers/02-valid-with-transfer.json b/ops/demo/dossiers/02-valid-with-transfer.json index 51ca3026..7331d20b 100644 --- a/ops/demo/dossiers/02-valid-with-transfer.json +++ b/ops/demo/dossiers/02-valid-with-transfer.json @@ -16,7 +16,7 @@ { "action": "published", "actor": "api-key", - "entryHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "entryHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { @@ -90,7 +90,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -173,10 +172,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -201,7 +199,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -246,7 +244,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -299,10 +296,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -313,7 +309,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -329,7 +325,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "1b61fd6f3f72f0730d61b59008f883d7630a03e2dea9f5bd67206482d0770530", + "entryHash": "fb79becbbefba459b654d0a85de2baf9143d3bbc9eaab3663172e0e14ba5975c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", "metadata": { "event": "transfer.initiated", @@ -338,7 +334,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "prevHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000003", "timestamp": "2026-09-10T14:00:00.042Z" @@ -346,7 +342,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "f462766c832366e92cef97196b764df1bd5ce5e1acc1fa5e2b395f99d4c3e6fa", + "entryHash": "f1fb57946b13af38d43e5866ff6c58285d31e5408e10eef40be3f5fd56578d97", "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", "metadata": { "event": "transfer.accepted", @@ -355,7 +351,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "1b61fd6f3f72f0730d61b59008f883d7630a03e2dea9f5bd67206482d0770530", + "prevHash": "fb79becbbefba459b654d0a85de2baf9143d3bbc9eaab3663172e0e14ba5975c", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000004", "timestamp": "2026-09-10T14:30:00.123456Z" @@ -364,49 +360,61 @@ "didDocuments": { "did:web:node.acme.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:node.acme.example#key-1" + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" + ], + "authentication": [ + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" ], "id": "did:web:node.acme.example", "verificationMethod": [ { "controller": "did:web:node.acme.example", - "id": "did:web:node.acme.example#key-1", + "id": "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "kty": "OKP", "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] }, "did:web:secondlife.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:secondlife.example#key-1" + "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4" + ], + "authentication": [ + "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4" ], "id": "did:web:secondlife.example", "verificationMethod": [ { "controller": "did:web:secondlife.example", - "id": "did:web:secondlife.example#key-1", + "id": "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4", "kty": "OKP", "x": "_RckOFqgx1tk-3jNYC-h2ZH96_drE8WO1wLqyDXp9hg" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInNhZmV0eU1lYXN1cmVzIjoiRG8gbm90IG9wZW4gdGhlIHBhY2suIFJlcGxhY2UgaXQgdGhyb3VnaCBhbiBhdXRob3Jpc2VkIGRlYWxlci4gS2VlcCBpdCBhd2F5IGZyb20gaGVhdCBhYm92ZSA2MCBkZWdDLiIsInNwYXJlUGFydHNDb250YWN0cyI6InNwYXJlc0BhY21lLmV4YW1wbGUiLCJzdGF0ZU9mSGVhbHRoIjp7InBhcmFtZXRlclNldCI6InN0YXRpb25hcnlPckxtdCIsInJlbWFpbmluZ0NhcGFjaXR5UGN0Ijo5OS41LCJyZW1haW5pbmdQb3dlckNhcGFiaWxpdHlQY3QiOjk5LjEsInNlbGZEaXNjaGFyZ2VSYXRlUGN0UGVyTW9udGgiOjJ9LCJzdGF0ZU9mSGVhbHRoUGN0IjpudWxsLCJ0ZXN0UmVwb3J0UmVzdWx0cyI6IkVOIDUwNjA0LTE6MjAxNiBwYXNzOyBVTiAzOC4zIHBhc3M7IHJlcG9ydCBBQ01FLVRSLTIwMjYtMDgxNSIsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsInVzYWdlSGlzdG9yeSI6eyJjaGFyZ2VEaXNjaGFyZ2VDeWNsZXMiOjR9LCJ3YXN0ZUJhdHRlcnlJbmZvcm1hdGlvbiI6IlJldHVybiB0aGUgYmF0dGVyeSB0byBhIGJpY3ljbGUgcmV0YWlsZXIgb3IgYSBtdW5pY2lwYWwgY29sbGVjdGlvbiBwb2ludC4gRG8gbm90IHBsYWNlIGl0IGluIGhvdXNlaG9sZCB3YXN0ZS4ifSwicHJvZHVjdE5hbWUiOiJBY21lIExNVCA0OFYgMjBBaCBiYXR0ZXJ5IHBhY2siLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9kcHAuYWNtZS5leGFtcGxlLzAxLzA5NTA2MDAwMTM0MzUyLzIxLzdlNGE5YzNkMmYxZThiNmE1ZDQwIiwicmVwYWlyYWJpbGl0eVNjb3JlIjpudWxsLCJyZXRlbnRpb25Mb2NrZWQiOnRydWUsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.55m5_OYvkiKUN9QV9BNB3JeqMOvkYjwYCiPxup_xciGjUrYtHOcaDdy8ULvSAy5NRGpT9p90RrajgBb0Iv5kCg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvbkxvY2tlZCI6dHJ1ZSwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.3BINMac1nxeKuqU19kKCV7k1c21eAr1DJOBT9MOT-5oh6zm3yYg6FNei69EjtkRH9EF1sRxxD_xs3auDY5HoDA", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -478,7 +486,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -561,10 +568,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -589,7 +595,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -597,21 +603,21 @@ }, "manifest": { "contentHashes": { - "auditEntries": "c21f3edd03d4f71d7428fe4560d2030ffd514db461234e89b489ae0934f4d7f3", - "fullView": "e8c5a8fbbddd67948a3ee3eb7abfc07427c2ff2ab23eeba3dc4dc9bbf12cbdbd", - "publicView": "74c1a1eda478cc2643b8652bf188f47e449b851ae64ae05f922de314a9249b83", - "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" + "auditEntries": "a5757e196ffa49c93b6f8f5bc860d59f8fe7706c311fa02ab40346afcc2c6d5c", + "fullView": "85fdd0d0865b22738ed8ece0cedf0e0d08107e0e13384d3ca2ff06fd3ad02d33", + "publicView": "d1c52ea096bffa8c85b10e3e509d58cb7960574b5f7798a12266a0dac3a6dd04", + "transferChain": "429b8b8e2b8f5823d3d2f55d66e78bc6c2a965a11118510b8644dcba6631794e" }, - "coreVersion": "0.21.0", + "coreVersion": "0.22.0", "createdAt": "2026-09-25T12:00:00Z", "formatVersion": "1", "issuerDid": "did:web:node.acme.example", "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImMyMWYzZWRkMDNkNGY3MWQ3NDI4ZmU0NTYwZDIwMzBmZmQ1MTRkYjQ2MTIzNGU4OWI0ODlhZTA5MzRmNGQ3ZjMiLCJmdWxsVmlldyI6ImU4YzVhOGZiYmRkZDY3OTQ4YTNlZTNlYjdhYmZjMDc0MjdjMmZmMmFiMjNlZWJhM2RjNGRjOWJiZjEyY2JkYmQiLCJwdWJsaWNWaWV3IjoiNzRjMWExZWRhNDc4Y2MyNjQzYjg2NTJiZjE4OGY0N2U0NDliODUxYWU2NGFlMDVmOTIyZGUzMTRhOTI0OWI4MyIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMS4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.TdSJCy5o_N9Rwte5i5kPZZJLkVVPP-N1PluWU0AN-b9QF9KpZWr5vXt-MWRAlSnrOiUbbT4HPKpE98NW6dk1AA", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImE1NzU3ZTE5NmZmYTQ5YzkzYjZmOGY1YmM4NjBkNTlmOGZlNzcwNmMzMTFmYTAyYWI0MDM0NmFmY2MyYzZkNWMiLCJmdWxsVmlldyI6Ijg1ZmRkMGQwODY1YjIyNzM4ZWQ4ZWNlMGNlZGYwZTBkMDgxMDdlMGUxMzM4NGQzY2EyZmYwNmZkM2FkMDJkMzMiLCJwdWJsaWNWaWV3IjoiZDFjNTJlYTA5NmJmZmE4Yzg1YjEwZTNlNTA5ZDU4Y2I3OTYwNTc0YjVmNzc5OGExMjI2NmEwZGFjM2E2ZGQwNCIsInRyYW5zZmVyQ2hhaW4iOiI0MjliOGI4ZTJiOGY1ODIzZDNkMmY1NWQ2NmU3OGJjNmMyYTk2NWExMTExODUxMGI4NjQ0ZGNiYTY2MzE3OTRlIn0sImNvcmVWZXJzaW9uIjoiMC4yMi4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.5wzKzG-RLeHCkCGRr4fbpiUK37gG3ObNMIXQ5PtU9JtrYosRsnaKceYlhq4hvmb6IVI731eqr13yqHzmuO2vBw", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjbzJlUGVyVW5pdEtnIjo4Ni40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.LHi5ToX900cUN4vak8l1zOZ5xc1H9jW25fL4XUWuxz2JU1_3PxxrjRUTyY9RctPxPSTWmS40fUWurEZnTdE3Dw", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwid2FzdGVCYXR0ZXJ5SW5mb3JtYXRpb24iOiJSZXR1cm4gdGhlIGJhdHRlcnkgdG8gYSBiaWN5Y2xlIHJldGFpbGVyIG9yIGEgbXVuaWNpcGFsIGNvbGxlY3Rpb24gcG9pbnQuIERvIG5vdCBwbGFjZSBpdCBpbiBob3VzZWhvbGQgd2FzdGUuIn0sInByb2R1Y3ROYW1lIjoiQWNtZSBMTVQgNDhWIDIwQWggYmF0dGVyeSBwYWNrIiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInFyQ29kZVVybCI6Imh0dHBzOi8vZHBwLmFjbWUuZXhhbXBsZS8wMS8wOTUwNjAwMDEzNDM1Mi8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.PgFgGaMF1f9RGQGqXEw31ZtocGBvVDfNcu2r2D88eG8wUQZMQLSVkb99NR59UErxDH0GYS_-olz_s5yy4_Z9BQ", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -652,7 +658,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -705,10 +710,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -719,7 +723,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -744,9 +748,9 @@ "name": "Acme Zellen GmbH", "role": "manufacturer" }, - "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.mtQCd6lEOMHAMcpsKZUFbx0vvRBS05El5_PWWXqItXxIp-lJO9VV1PILF9lTPceECXVTAgDfxCFsTdf29k3iCw", + "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.QnGV5iXvbEhKvEPWaK7RjBYVQ2JqnQ5zyoFqSYMH6VAF3cZiZpVpMtaGPXiZZbl9bgtA0rYNz7CT8Pcfq4_xCg", "initiatedAt": "2026-09-10T14:00:00Z", - "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.hrv0yKqeb1DVf-X9ysNDsFOawnrocRUUaFo9ygKqitjNmxaFlj6o1Xyf6eXsyGDjOW7kBSjbrbSkbSbbsmHfCg", + "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.1kAQj3w0HJAtFlBi_mKRZANGKI_KA9zLh-EksKdrDtDsfAO6YXVJuXijwvY1amqBtlfRuc6EqvORDOGJgg3cBA", "notes": "Repurposed into stationary storage.", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "reason": "repurposing", diff --git a/ops/demo/dossiers/03-valid-with-eol.json b/ops/demo/dossiers/03-valid-with-eol.json index 22717ffa..650fbb8b 100644 --- a/ops/demo/dossiers/03-valid-with-eol.json +++ b/ops/demo/dossiers/03-valid-with-eol.json @@ -16,7 +16,7 @@ { "action": "published", "actor": "api-key", - "entryHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "entryHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { @@ -90,7 +90,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -173,10 +172,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -201,7 +199,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -246,7 +244,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -299,10 +296,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -313,7 +309,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -329,7 +325,7 @@ { "action": "deactivated", "actor": "api-key", - "entryHash": "b5cec9e5dad32bb0c151c0a17a43fe650d808c234a3c1c5747738902af6bef3d", + "entryHash": "c89ab639506756bdb52a9e78490b0459aa070955150b004a20d82747ddd21a32", "id": "0199a3c2-7b10-7e4a-9c3d-000000000005", "metadata": { "declaredAt": "2026-09-20T08:00:00Z", @@ -342,7 +338,7 @@ }, "newStatus": "deactivated", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "prevHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "previousStatus": "active", "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000005", "timestamp": "2026-09-20T08:00:00.018Z" @@ -351,22 +347,28 @@ "didDocuments": { "did:web:node.acme.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:node.acme.example#key-1" + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" + ], + "authentication": [ + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" ], "id": "did:web:node.acme.example", "verificationMethod": [ { "controller": "did:web:node.acme.example", - "id": "did:web:node.acme.example#key-1", + "id": "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "kty": "OKP", "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] } @@ -381,7 +383,7 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInNhZmV0eU1lYXN1cmVzIjoiRG8gbm90IG9wZW4gdGhlIHBhY2suIFJlcGxhY2UgaXQgdGhyb3VnaCBhbiBhdXRob3Jpc2VkIGRlYWxlci4gS2VlcCBpdCBhd2F5IGZyb20gaGVhdCBhYm92ZSA2MCBkZWdDLiIsInNwYXJlUGFydHNDb250YWN0cyI6InNwYXJlc0BhY21lLmV4YW1wbGUiLCJzdGF0ZU9mSGVhbHRoIjp7InBhcmFtZXRlclNldCI6InN0YXRpb25hcnlPckxtdCIsInJlbWFpbmluZ0NhcGFjaXR5UGN0Ijo5OS41LCJyZW1haW5pbmdQb3dlckNhcGFiaWxpdHlQY3QiOjk5LjEsInNlbGZEaXNjaGFyZ2VSYXRlUGN0UGVyTW9udGgiOjJ9LCJzdGF0ZU9mSGVhbHRoUGN0IjpudWxsLCJ0ZXN0UmVwb3J0UmVzdWx0cyI6IkVOIDUwNjA0LTE6MjAxNiBwYXNzOyBVTiAzOC4zIHBhc3M7IHJlcG9ydCBBQ01FLVRSLTIwMjYtMDgxNSIsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsInVzYWdlSGlzdG9yeSI6eyJjaGFyZ2VEaXNjaGFyZ2VDeWNsZXMiOjR9LCJ3YXN0ZUJhdHRlcnlJbmZvcm1hdGlvbiI6IlJldHVybiB0aGUgYmF0dGVyeSB0byBhIGJpY3ljbGUgcmV0YWlsZXIgb3IgYSBtdW5pY2lwYWwgY29sbGVjdGlvbiBwb2ludC4gRG8gbm90IHBsYWNlIGl0IGluIGhvdXNlaG9sZCB3YXN0ZS4ifSwicHJvZHVjdE5hbWUiOiJBY21lIExNVCA0OFYgMjBBaCBiYXR0ZXJ5IHBhY2siLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9kcHAuYWNtZS5leGFtcGxlLzAxLzA5NTA2MDAwMTM0MzUyLzIxLzdlNGE5YzNkMmYxZThiNmE1ZDQwIiwicmVwYWlyYWJpbGl0eVNjb3JlIjpudWxsLCJyZXRlbnRpb25Mb2NrZWQiOnRydWUsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.55m5_OYvkiKUN9QV9BNB3JeqMOvkYjwYCiPxup_xciGjUrYtHOcaDdy8ULvSAy5NRGpT9p90RrajgBb0Iv5kCg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvbkxvY2tlZCI6dHJ1ZSwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.3BINMac1nxeKuqU19kKCV7k1c21eAr1DJOBT9MOT-5oh6zm3yYg6FNei69EjtkRH9EF1sRxxD_xs3auDY5HoDA", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -453,7 +455,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -536,10 +537,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -564,7 +564,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -572,21 +572,21 @@ }, "manifest": { "contentHashes": { - "auditEntries": "7cbad381a418f0d97abbb4f33e5f66ba1e715e1f2d0f93e069ec041aa55260a1", + "auditEntries": "85557a71c5d9b5538954e8f7df4dbd010af18638690975cecb191740f0995782", "eolEvent": "7cb65bb485538e109105404e15c7bc411aa3050694fa4d63339c70dbe431d3a0", - "fullView": "e8c5a8fbbddd67948a3ee3eb7abfc07427c2ff2ab23eeba3dc4dc9bbf12cbdbd", - "publicView": "74c1a1eda478cc2643b8652bf188f47e449b851ae64ae05f922de314a9249b83" + "fullView": "85fdd0d0865b22738ed8ece0cedf0e0d08107e0e13384d3ca2ff06fd3ad02d33", + "publicView": "d1c52ea096bffa8c85b10e3e509d58cb7960574b5f7798a12266a0dac3a6dd04" }, - "coreVersion": "0.21.0", + "coreVersion": "0.22.0", "createdAt": "2026-09-25T12:00:00Z", "formatVersion": "1", "issuerDid": "did:web:node.acme.example", "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjdjYmFkMzgxYTQxOGYwZDk3YWJiYjRmMzNlNWY2NmJhMWU3MTVlMWYyZDBmOTNlMDY5ZWMwNDFhYTU1MjYwYTEiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6ImU4YzVhOGZiYmRkZDY3OTQ4YTNlZTNlYjdhYmZjMDc0MjdjMmZmMmFiMjNlZWJhM2RjNGRjOWJiZjEyY2JkYmQiLCJwdWJsaWNWaWV3IjoiNzRjMWExZWRhNDc4Y2MyNjQzYjg2NTJiZjE4OGY0N2U0NDliODUxYWU2NGFlMDVmOTIyZGUzMTRhOTI0OWI4MyJ9LCJjb3JlVmVyc2lvbiI6IjAuMjEuMCIsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjVUMTI6MDA6MDBaIiwiZm9ybWF0VmVyc2lvbiI6IjEiLCJpc3N1ZXJEaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwibm9kZVZlcnNpb24iOiIwLjE0LjAiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIn0.z-LKtW-sMgdmxQhMgWKjU9hoZlOUraGCeL5A4UI-djD3GRoOcJQ0e9bb1DB9qYcYCakswJ55zzuJ6T5wkznxCA", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6Ijg1NTU3YTcxYzVkOWI1NTM4OTU0ZThmN2RmNGRiZDAxMGFmMTg2Mzg2OTA5NzVjZWNiMTkxNzQwZjA5OTU3ODIiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6Ijg1ZmRkMGQwODY1YjIyNzM4ZWQ4ZWNlMGNlZGYwZTBkMDgxMDdlMGUxMzM4NGQzY2EyZmYwNmZkM2FkMDJkMzMiLCJwdWJsaWNWaWV3IjoiZDFjNTJlYTA5NmJmZmE4Yzg1YjEwZTNlNTA5ZDU4Y2I3OTYwNTc0YjVmNzc5OGExMjI2NmEwZGFjM2E2ZGQwNCJ9LCJjb3JlVmVyc2lvbiI6IjAuMjIuMCIsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjVUMTI6MDA6MDBaIiwiZm9ybWF0VmVyc2lvbiI6IjEiLCJpc3N1ZXJEaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwibm9kZVZlcnNpb24iOiIwLjE0LjAiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIn0.xteMcnX26KX_au3LZpCBfCyeA8t_sqD4oO6GjZUrUrNWtjOKgpdTCQ8LJIEwakioNFdTWdfQIqIWakpBjZ_VCQ", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjbzJlUGVyVW5pdEtnIjo4Ni40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.LHi5ToX900cUN4vak8l1zOZ5xc1H9jW25fL4XUWuxz2JU1_3PxxrjRUTyY9RctPxPSTWmS40fUWurEZnTdE3Dw", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwid2FzdGVCYXR0ZXJ5SW5mb3JtYXRpb24iOiJSZXR1cm4gdGhlIGJhdHRlcnkgdG8gYSBiaWN5Y2xlIHJldGFpbGVyIG9yIGEgbXVuaWNpcGFsIGNvbGxlY3Rpb24gcG9pbnQuIERvIG5vdCBwbGFjZSBpdCBpbiBob3VzZWhvbGQgd2FzdGUuIn0sInByb2R1Y3ROYW1lIjoiQWNtZSBMTVQgNDhWIDIwQWggYmF0dGVyeSBwYWNrIiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInFyQ29kZVVybCI6Imh0dHBzOi8vZHBwLmFjbWUuZXhhbXBsZS8wMS8wOTUwNjAwMDEzNDM1Mi8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.PgFgGaMF1f9RGQGqXEw31ZtocGBvVDfNcu2r2D88eG8wUQZMQLSVkb99NR59UErxDH0GYS_-olz_s5yy4_Z9BQ", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -627,7 +627,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -680,10 +679,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -694,7 +692,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 diff --git a/ops/demo/dossiers/04-valid-full-lifecycle.json b/ops/demo/dossiers/04-valid-full-lifecycle.json index b120508f..a153e8d1 100644 --- a/ops/demo/dossiers/04-valid-full-lifecycle.json +++ b/ops/demo/dossiers/04-valid-full-lifecycle.json @@ -16,7 +16,7 @@ { "action": "published", "actor": "api-key", - "entryHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "entryHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { @@ -90,7 +90,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -173,10 +172,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -201,7 +199,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -246,7 +244,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -299,10 +296,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -313,7 +309,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -329,7 +325,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "1b61fd6f3f72f0730d61b59008f883d7630a03e2dea9f5bd67206482d0770530", + "entryHash": "fb79becbbefba459b654d0a85de2baf9143d3bbc9eaab3663172e0e14ba5975c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", "metadata": { "event": "transfer.initiated", @@ -338,7 +334,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "prevHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000003", "timestamp": "2026-09-10T14:00:00.042Z" @@ -346,7 +342,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "f462766c832366e92cef97196b764df1bd5ce5e1acc1fa5e2b395f99d4c3e6fa", + "entryHash": "f1fb57946b13af38d43e5866ff6c58285d31e5408e10eef40be3f5fd56578d97", "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", "metadata": { "event": "transfer.accepted", @@ -355,7 +351,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "1b61fd6f3f72f0730d61b59008f883d7630a03e2dea9f5bd67206482d0770530", + "prevHash": "fb79becbbefba459b654d0a85de2baf9143d3bbc9eaab3663172e0e14ba5975c", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000004", "timestamp": "2026-09-10T14:30:00.123456Z" @@ -363,7 +359,7 @@ { "action": "deactivated", "actor": "api-key", - "entryHash": "6fd9d12be0f3a26ae6fc2740a796a13363de1e3abd3082ce284c9eeebdc88dac", + "entryHash": "5cff9558d98c6d5294b3e6d7133d68627e164289528b9a8ff9a1d838ed6c28f9", "id": "0199a3c2-7b10-7e4a-9c3d-000000000005", "metadata": { "declaredAt": "2026-09-20T08:00:00Z", @@ -376,7 +372,7 @@ }, "newStatus": "deactivated", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "f462766c832366e92cef97196b764df1bd5ce5e1acc1fa5e2b395f99d4c3e6fa", + "prevHash": "f1fb57946b13af38d43e5866ff6c58285d31e5408e10eef40be3f5fd56578d97", "previousStatus": "active", "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000005", "timestamp": "2026-09-20T08:00:00.018Z" @@ -385,43 +381,55 @@ "didDocuments": { "did:web:node.acme.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:node.acme.example#key-1" + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" + ], + "authentication": [ + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" ], "id": "did:web:node.acme.example", "verificationMethod": [ { "controller": "did:web:node.acme.example", - "id": "did:web:node.acme.example#key-1", + "id": "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "kty": "OKP", "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] }, "did:web:secondlife.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:secondlife.example#key-1" + "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4" + ], + "authentication": [ + "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4" ], "id": "did:web:secondlife.example", "verificationMethod": [ { "controller": "did:web:secondlife.example", - "id": "did:web:secondlife.example#key-1", + "id": "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4", "kty": "OKP", "x": "_RckOFqgx1tk-3jNYC-h2ZH96_drE8WO1wLqyDXp9hg" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] } @@ -436,7 +444,7 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInNhZmV0eU1lYXN1cmVzIjoiRG8gbm90IG9wZW4gdGhlIHBhY2suIFJlcGxhY2UgaXQgdGhyb3VnaCBhbiBhdXRob3Jpc2VkIGRlYWxlci4gS2VlcCBpdCBhd2F5IGZyb20gaGVhdCBhYm92ZSA2MCBkZWdDLiIsInNwYXJlUGFydHNDb250YWN0cyI6InNwYXJlc0BhY21lLmV4YW1wbGUiLCJzdGF0ZU9mSGVhbHRoIjp7InBhcmFtZXRlclNldCI6InN0YXRpb25hcnlPckxtdCIsInJlbWFpbmluZ0NhcGFjaXR5UGN0Ijo5OS41LCJyZW1haW5pbmdQb3dlckNhcGFiaWxpdHlQY3QiOjk5LjEsInNlbGZEaXNjaGFyZ2VSYXRlUGN0UGVyTW9udGgiOjJ9LCJzdGF0ZU9mSGVhbHRoUGN0IjpudWxsLCJ0ZXN0UmVwb3J0UmVzdWx0cyI6IkVOIDUwNjA0LTE6MjAxNiBwYXNzOyBVTiAzOC4zIHBhc3M7IHJlcG9ydCBBQ01FLVRSLTIwMjYtMDgxNSIsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsInVzYWdlSGlzdG9yeSI6eyJjaGFyZ2VEaXNjaGFyZ2VDeWNsZXMiOjR9LCJ3YXN0ZUJhdHRlcnlJbmZvcm1hdGlvbiI6IlJldHVybiB0aGUgYmF0dGVyeSB0byBhIGJpY3ljbGUgcmV0YWlsZXIgb3IgYSBtdW5pY2lwYWwgY29sbGVjdGlvbiBwb2ludC4gRG8gbm90IHBsYWNlIGl0IGluIGhvdXNlaG9sZCB3YXN0ZS4ifSwicHJvZHVjdE5hbWUiOiJBY21lIExNVCA0OFYgMjBBaCBiYXR0ZXJ5IHBhY2siLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9kcHAuYWNtZS5leGFtcGxlLzAxLzA5NTA2MDAwMTM0MzUyLzIxLzdlNGE5YzNkMmYxZThiNmE1ZDQwIiwicmVwYWlyYWJpbGl0eVNjb3JlIjpudWxsLCJyZXRlbnRpb25Mb2NrZWQiOnRydWUsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.55m5_OYvkiKUN9QV9BNB3JeqMOvkYjwYCiPxup_xciGjUrYtHOcaDdy8ULvSAy5NRGpT9p90RrajgBb0Iv5kCg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvbkxvY2tlZCI6dHJ1ZSwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.3BINMac1nxeKuqU19kKCV7k1c21eAr1DJOBT9MOT-5oh6zm3yYg6FNei69EjtkRH9EF1sRxxD_xs3auDY5HoDA", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -508,7 +516,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -591,10 +598,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -619,7 +625,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -627,22 +633,22 @@ }, "manifest": { "contentHashes": { - "auditEntries": "46110fef3e331eab68e2616292edcc10d2bfcdfc16fa7bb0409cecdf7cb86a33", + "auditEntries": "90266d629c641b1fe4ac01969138546b180d174ad4d222c5deebb3146d65862d", "eolEvent": "7cb65bb485538e109105404e15c7bc411aa3050694fa4d63339c70dbe431d3a0", - "fullView": "e8c5a8fbbddd67948a3ee3eb7abfc07427c2ff2ab23eeba3dc4dc9bbf12cbdbd", - "publicView": "74c1a1eda478cc2643b8652bf188f47e449b851ae64ae05f922de314a9249b83", - "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" + "fullView": "85fdd0d0865b22738ed8ece0cedf0e0d08107e0e13384d3ca2ff06fd3ad02d33", + "publicView": "d1c52ea096bffa8c85b10e3e509d58cb7960574b5f7798a12266a0dac3a6dd04", + "transferChain": "429b8b8e2b8f5823d3d2f55d66e78bc6c2a965a11118510b8644dcba6631794e" }, - "coreVersion": "0.21.0", + "coreVersion": "0.22.0", "createdAt": "2026-09-25T12:00:00Z", "formatVersion": "1", "issuerDid": "did:web:node.acme.example", "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjQ2MTEwZmVmM2UzMzFlYWI2OGUyNjE2MjkyZWRjYzEwZDJiZmNkZmMxNmZhN2JiMDQwOWNlY2RmN2NiODZhMzMiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6ImU4YzVhOGZiYmRkZDY3OTQ4YTNlZTNlYjdhYmZjMDc0MjdjMmZmMmFiMjNlZWJhM2RjNGRjOWJiZjEyY2JkYmQiLCJwdWJsaWNWaWV3IjoiNzRjMWExZWRhNDc4Y2MyNjQzYjg2NTJiZjE4OGY0N2U0NDliODUxYWU2NGFlMDVmOTIyZGUzMTRhOTI0OWI4MyIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMS4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.nR0d3UZbNRttz9D0VL9qGHN9dQwzFQGa4eRni8N926diP9G-qB4nGbqgqyz9VqaL6DjKReENzDldyo87sev_BA", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjkwMjY2ZDYyOWM2NDFiMWZlNGFjMDE5NjkxMzg1NDZiMTgwZDE3NGFkNGQyMjJjNWRlZWJiMzE0NmQ2NTg2MmQiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6Ijg1ZmRkMGQwODY1YjIyNzM4ZWQ4ZWNlMGNlZGYwZTBkMDgxMDdlMGUxMzM4NGQzY2EyZmYwNmZkM2FkMDJkMzMiLCJwdWJsaWNWaWV3IjoiZDFjNTJlYTA5NmJmZmE4Yzg1YjEwZTNlNTA5ZDU4Y2I3OTYwNTc0YjVmNzc5OGExMjI2NmEwZGFjM2E2ZGQwNCIsInRyYW5zZmVyQ2hhaW4iOiI0MjliOGI4ZTJiOGY1ODIzZDNkMmY1NWQ2NmU3OGJjNmMyYTk2NWExMTExODUxMGI4NjQ0ZGNiYTY2MzE3OTRlIn0sImNvcmVWZXJzaW9uIjoiMC4yMi4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ._26ZWmNwdpJiyHqjwzgLwnFdAfAJNQWCyoO2F2hMGEOknkkXRRcW-heKHiZ8usv9IPs44mezYgJRB_Ks3twmCw", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjbzJlUGVyVW5pdEtnIjo4Ni40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.LHi5ToX900cUN4vak8l1zOZ5xc1H9jW25fL4XUWuxz2JU1_3PxxrjRUTyY9RctPxPSTWmS40fUWurEZnTdE3Dw", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwid2FzdGVCYXR0ZXJ5SW5mb3JtYXRpb24iOiJSZXR1cm4gdGhlIGJhdHRlcnkgdG8gYSBiaWN5Y2xlIHJldGFpbGVyIG9yIGEgbXVuaWNpcGFsIGNvbGxlY3Rpb24gcG9pbnQuIERvIG5vdCBwbGFjZSBpdCBpbiBob3VzZWhvbGQgd2FzdGUuIn0sInByb2R1Y3ROYW1lIjoiQWNtZSBMTVQgNDhWIDIwQWggYmF0dGVyeSBwYWNrIiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInFyQ29kZVVybCI6Imh0dHBzOi8vZHBwLmFjbWUuZXhhbXBsZS8wMS8wOTUwNjAwMDEzNDM1Mi8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.PgFgGaMF1f9RGQGqXEw31ZtocGBvVDfNcu2r2D88eG8wUQZMQLSVkb99NR59UErxDH0GYS_-olz_s5yy4_Z9BQ", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -683,7 +689,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -736,10 +741,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -750,7 +754,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -775,9 +779,9 @@ "name": "Acme Zellen GmbH", "role": "manufacturer" }, - "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.mtQCd6lEOMHAMcpsKZUFbx0vvRBS05El5_PWWXqItXxIp-lJO9VV1PILF9lTPceECXVTAgDfxCFsTdf29k3iCw", + "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.QnGV5iXvbEhKvEPWaK7RjBYVQ2JqnQ5zyoFqSYMH6VAF3cZiZpVpMtaGPXiZZbl9bgtA0rYNz7CT8Pcfq4_xCg", "initiatedAt": "2026-09-10T14:00:00Z", - "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.hrv0yKqeb1DVf-X9ysNDsFOawnrocRUUaFo9ygKqitjNmxaFlj6o1Xyf6eXsyGDjOW7kBSjbrbSkbSbbsmHfCg", + "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.1kAQj3w0HJAtFlBi_mKRZANGKI_KA9zLh-EksKdrDtDsfAO6YXVJuXijwvY1amqBtlfRuc6EqvORDOGJgg3cBA", "notes": "Repurposed into stationary storage.", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "reason": "repurposing", diff --git a/ops/demo/dossiers/05-tampered-signature.json b/ops/demo/dossiers/05-tampered-signature.json index d14139eb..785d54b1 100644 --- a/ops/demo/dossiers/05-tampered-signature.json +++ b/ops/demo/dossiers/05-tampered-signature.json @@ -16,7 +16,7 @@ { "action": "published", "actor": "api-key", - "entryHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "entryHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { @@ -90,7 +90,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -173,10 +172,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -201,7 +199,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -246,7 +244,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -299,10 +296,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -313,7 +309,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -329,7 +325,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "1b61fd6f3f72f0730d61b59008f883d7630a03e2dea9f5bd67206482d0770530", + "entryHash": "fb79becbbefba459b654d0a85de2baf9143d3bbc9eaab3663172e0e14ba5975c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", "metadata": { "event": "transfer.initiated", @@ -338,7 +334,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "prevHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000003", "timestamp": "2026-09-10T14:00:00.042Z" @@ -346,7 +342,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "f462766c832366e92cef97196b764df1bd5ce5e1acc1fa5e2b395f99d4c3e6fa", + "entryHash": "f1fb57946b13af38d43e5866ff6c58285d31e5408e10eef40be3f5fd56578d97", "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", "metadata": { "event": "transfer.accepted", @@ -355,7 +351,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "1b61fd6f3f72f0730d61b59008f883d7630a03e2dea9f5bd67206482d0770530", + "prevHash": "fb79becbbefba459b654d0a85de2baf9143d3bbc9eaab3663172e0e14ba5975c", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000004", "timestamp": "2026-09-10T14:30:00.123456Z" @@ -363,7 +359,7 @@ { "action": "deactivated", "actor": "api-key", - "entryHash": "6fd9d12be0f3a26ae6fc2740a796a13363de1e3abd3082ce284c9eeebdc88dac", + "entryHash": "5cff9558d98c6d5294b3e6d7133d68627e164289528b9a8ff9a1d838ed6c28f9", "id": "0199a3c2-7b10-7e4a-9c3d-000000000005", "metadata": { "declaredAt": "2026-09-20T08:00:00Z", @@ -376,7 +372,7 @@ }, "newStatus": "deactivated", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "f462766c832366e92cef97196b764df1bd5ce5e1acc1fa5e2b395f99d4c3e6fa", + "prevHash": "f1fb57946b13af38d43e5866ff6c58285d31e5408e10eef40be3f5fd56578d97", "previousStatus": "active", "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000005", "timestamp": "2026-09-20T08:00:00.018Z" @@ -385,43 +381,55 @@ "didDocuments": { "did:web:node.acme.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:node.acme.example#key-1" + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" + ], + "authentication": [ + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" ], "id": "did:web:node.acme.example", "verificationMethod": [ { "controller": "did:web:node.acme.example", - "id": "did:web:node.acme.example#key-1", + "id": "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "kty": "OKP", "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] }, "did:web:secondlife.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:secondlife.example#key-1" + "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4" + ], + "authentication": [ + "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4" ], "id": "did:web:secondlife.example", "verificationMethod": [ { "controller": "did:web:secondlife.example", - "id": "did:web:secondlife.example#key-1", + "id": "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4", "kty": "OKP", "x": "_RckOFqgx1tk-3jNYC-h2ZH96_drE8WO1wLqyDXp9hg" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] } @@ -436,7 +444,7 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInNhZmV0eU1lYXN1cmVzIjoiRG8gbm90IG9wZW4gdGhlIHBhY2suIFJlcGxhY2UgaXQgdGhyb3VnaCBhbiBhdXRob3Jpc2VkIGRlYWxlci4gS2VlcCBpdCBhd2F5IGZyb20gaGVhdCBhYm92ZSA2MCBkZWdDLiIsInNwYXJlUGFydHNDb250YWN0cyI6InNwYXJlc0BhY21lLmV4YW1wbGUiLCJzdGF0ZU9mSGVhbHRoIjp7InBhcmFtZXRlclNldCI6InN0YXRpb25hcnlPckxtdCIsInJlbWFpbmluZ0NhcGFjaXR5UGN0Ijo5OS41LCJyZW1haW5pbmdQb3dlckNhcGFiaWxpdHlQY3QiOjk5LjEsInNlbGZEaXNjaGFyZ2VSYXRlUGN0UGVyTW9udGgiOjJ9LCJzdGF0ZU9mSGVhbHRoUGN0IjpudWxsLCJ0ZXN0UmVwb3J0UmVzdWx0cyI6IkVOIDUwNjA0LTE6MjAxNiBwYXNzOyBVTiAzOC4zIHBhc3M7IHJlcG9ydCBBQ01FLVRSLTIwMjYtMDgxNSIsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsInVzYWdlSGlzdG9yeSI6eyJjaGFyZ2VEaXNjaGFyZ2VDeWNsZXMiOjR9LCJ3YXN0ZUJhdHRlcnlJbmZvcm1hdGlvbiI6IlJldHVybiB0aGUgYmF0dGVyeSB0byBhIGJpY3ljbGUgcmV0YWlsZXIgb3IgYSBtdW5pY2lwYWwgY29sbGVjdGlvbiBwb2ludC4gRG8gbm90IHBsYWNlIGl0IGluIGhvdXNlaG9sZCB3YXN0ZS4ifSwicHJvZHVjdE5hbWUiOiJBY21lIExNVCA0OFYgMjBBaCBiYXR0ZXJ5IHBhY2siLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9kcHAuYWNtZS5leGFtcGxlLzAxLzA5NTA2MDAwMTM0MzUyLzIxLzdlNGE5YzNkMmYxZThiNmE1ZDQwIiwicmVwYWlyYWJpbGl0eVNjb3JlIjpudWxsLCJyZXRlbnRpb25Mb2NrZWQiOnRydWUsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.55m5_OYvkiKUN9QV9BNB3JeqMOvkYjwYCiPxup_xciGjUrYtHOcaDdy8ULvSAy5NRGpT9p90RrajgBb0Iv5kCg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvbkxvY2tlZCI6dHJ1ZSwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.3BINMac1nxeKuqU19kKCV7k1c21eAr1DJOBT9MOT-5oh6zm3yYg6FNei69EjtkRH9EF1sRxxD_xs3auDY5HoDA", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -508,7 +516,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -591,10 +598,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -619,7 +625,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -627,22 +633,22 @@ }, "manifest": { "contentHashes": { - "auditEntries": "46110fef3e331eab68e2616292edcc10d2bfcdfc16fa7bb0409cecdf7cb86a33", + "auditEntries": "90266d629c641b1fe4ac01969138546b180d174ad4d222c5deebb3146d65862d", "eolEvent": "7cb65bb485538e109105404e15c7bc411aa3050694fa4d63339c70dbe431d3a0", - "fullView": "e8c5a8fbbddd67948a3ee3eb7abfc07427c2ff2ab23eeba3dc4dc9bbf12cbdbd", - "publicView": "74c1a1eda478cc2643b8652bf188f47e449b851ae64ae05f922de314a9249b83", - "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" + "fullView": "85fdd0d0865b22738ed8ece0cedf0e0d08107e0e13384d3ca2ff06fd3ad02d33", + "publicView": "d1c52ea096bffa8c85b10e3e509d58cb7960574b5f7798a12266a0dac3a6dd04", + "transferChain": "429b8b8e2b8f5823d3d2f55d66e78bc6c2a965a11118510b8644dcba6631794e" }, - "coreVersion": "0.21.0", + "coreVersion": "0.22.0", "createdAt": "2026-09-25T12:00:00Z", "formatVersion": "1", "issuerDid": "did:web:node.acme.example", "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjQ2MTEwZmVmM2UzMzFlYWI2OGUyNjE2MjkyZWRjYzEwZDJiZmNkZmMxNmZhN2JiMDQwOWNlY2RmN2NiODZhMzMiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6ImU4YzVhOGZiYmRkZDY3OTQ4YTNlZTNlYjdhYmZjMDc0MjdjMmZmMmFiMjNlZWJhM2RjNGRjOWJiZjEyY2JkYmQiLCJwdWJsaWNWaWV3IjoiNzRjMWExZWRhNDc4Y2MyNjQzYjg2NTJiZjE4OGY0N2U0NDliODUxYWU2NGFlMDVmOTIyZGUzMTRhOTI0OWI4MyIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMS4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.nR0d3UZbNRttz9D0VL9qGHN9dQwzFQGa4eRni8N926diP9G-qB4nGbqgqyz9VqaL6DjKReENzDldyo87sev_BA", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjkwMjY2ZDYyOWM2NDFiMWZlNGFjMDE5NjkxMzg1NDZiMTgwZDE3NGFkNGQyMjJjNWRlZWJiMzE0NmQ2NTg2MmQiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6Ijg1ZmRkMGQwODY1YjIyNzM4ZWQ4ZWNlMGNlZGYwZTBkMDgxMDdlMGUxMzM4NGQzY2EyZmYwNmZkM2FkMDJkMzMiLCJwdWJsaWNWaWV3IjoiZDFjNTJlYTA5NmJmZmE4Yzg1YjEwZTNlNTA5ZDU4Y2I3OTYwNTc0YjVmNzc5OGExMjI2NmEwZGFjM2E2ZGQwNCIsInRyYW5zZmVyQ2hhaW4iOiI0MjliOGI4ZTJiOGY1ODIzZDNkMmY1NWQ2NmU3OGJjNmMyYTk2NWExMTExODUxMGI4NjQ0ZGNiYTY2MzE3OTRlIn0sImNvcmVWZXJzaW9uIjoiMC4yMi4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ._26ZWmNwdpJiyHqjwzgLwnFdAfAJNQWCyoO2F2hMGEOknkkXRRcW-heKHiZ8usv9IPs44mezYgJRB_Ks3twmCw", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjbzJlUGVyVW5pdEtnIjo4Ni40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.LHi5ToX900cUN4vak8l1zOZ5xc1H9jW25fL4XUWuxz2AU1_3PxxrjRUTyY9RctPxPSTWmS40fUWurEZnTdE3Dw", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwid2FzdGVCYXR0ZXJ5SW5mb3JtYXRpb24iOiJSZXR1cm4gdGhlIGJhdHRlcnkgdG8gYSBiaWN5Y2xlIHJldGFpbGVyIG9yIGEgbXVuaWNpcGFsIGNvbGxlY3Rpb24gcG9pbnQuIERvIG5vdCBwbGFjZSBpdCBpbiBob3VzZWhvbGQgd2FzdGUuIn0sInByb2R1Y3ROYW1lIjoiQWNtZSBMTVQgNDhWIDIwQWggYmF0dGVyeSBwYWNrIiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInFyQ29kZVVybCI6Imh0dHBzOi8vZHBwLmFjbWUuZXhhbXBsZS8wMS8wOTUwNjAwMDEzNDM1Mi8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.PgFgGaMF1f9RGQGqXEw31ZtocGBvVDfNcu2r2D88eG8AUQZMQLSVkb99NR59UErxDH0GYS_-olz_s5yy4_Z9BQ", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -683,7 +689,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -736,10 +741,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -750,7 +754,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -775,9 +779,9 @@ "name": "Acme Zellen GmbH", "role": "manufacturer" }, - "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.mtQCd6lEOMHAMcpsKZUFbx0vvRBS05El5_PWWXqItXxIp-lJO9VV1PILF9lTPceECXVTAgDfxCFsTdf29k3iCw", + "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.QnGV5iXvbEhKvEPWaK7RjBYVQ2JqnQ5zyoFqSYMH6VAF3cZiZpVpMtaGPXiZZbl9bgtA0rYNz7CT8Pcfq4_xCg", "initiatedAt": "2026-09-10T14:00:00Z", - "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.hrv0yKqeb1DVf-X9ysNDsFOawnrocRUUaFo9ygKqitjNmxaFlj6o1Xyf6eXsyGDjOW7kBSjbrbSkbSbbsmHfCg", + "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.1kAQj3w0HJAtFlBi_mKRZANGKI_KA9zLh-EksKdrDtDsfAO6YXVJuXijwvY1amqBtlfRuc6EqvORDOGJgg3cBA", "notes": "Repurposed into stationary storage.", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "reason": "repurposing", diff --git a/ops/demo/dossiers/06-tampered-audit-entry.json b/ops/demo/dossiers/06-tampered-audit-entry.json index 03302170..38d3f2db 100644 --- a/ops/demo/dossiers/06-tampered-audit-entry.json +++ b/ops/demo/dossiers/06-tampered-audit-entry.json @@ -16,7 +16,7 @@ { "action": "published", "actor": "api-key", - "entryHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "entryHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { @@ -90,7 +90,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -173,10 +172,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -201,7 +199,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -246,7 +244,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -299,10 +296,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -313,7 +309,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -329,7 +325,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "1b61fd6f3f72f0730d61b59008f883d7630a03e2dea9f5bd67206482d0770530", + "entryHash": "fb79becbbefba459b654d0a85de2baf9143d3bbc9eaab3663172e0e14ba5975c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", "metadata": { "event": "transfer.initiated", @@ -338,7 +334,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "prevHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000003", "timestamp": "2026-09-10T14:00:00.042Z" @@ -346,7 +342,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "f462766c832366e92cef97196b764df1bd5ce5e1acc1fa5e2b395f99d4c3e6fa", + "entryHash": "f1fb57946b13af38d43e5866ff6c58285d31e5408e10eef40be3f5fd56578d97", "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", "metadata": { "event": "transfer.accepted", @@ -355,7 +351,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "1b61fd6f3f72f0730d61b59008f883d7630a03e2dea9f5bd67206482d0770530", + "prevHash": "fb79becbbefba459b654d0a85de2baf9143d3bbc9eaab3663172e0e14ba5975c", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000004", "timestamp": "2026-09-10T14:30:00.123456Z" @@ -363,7 +359,7 @@ { "action": "deactivated", "actor": "api-key", - "entryHash": "6fd9d12be0f3a26ae6fc2740a796a13363de1e3abd3082ce284c9eeebdc88dac", + "entryHash": "5cff9558d98c6d5294b3e6d7133d68627e164289528b9a8ff9a1d838ed6c28f9", "id": "0199a3c2-7b10-7e4a-9c3d-000000000005", "metadata": { "declaredAt": "2026-09-20T08:00:00Z", @@ -376,7 +372,7 @@ }, "newStatus": "deactivated", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "f462766c832366e92cef97196b764df1bd5ce5e1acc1fa5e2b395f99d4c3e6fa", + "prevHash": "f1fb57946b13af38d43e5866ff6c58285d31e5408e10eef40be3f5fd56578d97", "previousStatus": "active", "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000005", "timestamp": "2026-09-20T08:00:00.018Z" @@ -385,43 +381,55 @@ "didDocuments": { "did:web:node.acme.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:node.acme.example#key-1" + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" + ], + "authentication": [ + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" ], "id": "did:web:node.acme.example", "verificationMethod": [ { "controller": "did:web:node.acme.example", - "id": "did:web:node.acme.example#key-1", + "id": "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "kty": "OKP", "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] }, "did:web:secondlife.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:secondlife.example#key-1" + "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4" + ], + "authentication": [ + "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4" ], "id": "did:web:secondlife.example", "verificationMethod": [ { "controller": "did:web:secondlife.example", - "id": "did:web:secondlife.example#key-1", + "id": "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4", "kty": "OKP", "x": "_RckOFqgx1tk-3jNYC-h2ZH96_drE8WO1wLqyDXp9hg" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] } @@ -436,7 +444,7 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInNhZmV0eU1lYXN1cmVzIjoiRG8gbm90IG9wZW4gdGhlIHBhY2suIFJlcGxhY2UgaXQgdGhyb3VnaCBhbiBhdXRob3Jpc2VkIGRlYWxlci4gS2VlcCBpdCBhd2F5IGZyb20gaGVhdCBhYm92ZSA2MCBkZWdDLiIsInNwYXJlUGFydHNDb250YWN0cyI6InNwYXJlc0BhY21lLmV4YW1wbGUiLCJzdGF0ZU9mSGVhbHRoIjp7InBhcmFtZXRlclNldCI6InN0YXRpb25hcnlPckxtdCIsInJlbWFpbmluZ0NhcGFjaXR5UGN0Ijo5OS41LCJyZW1haW5pbmdQb3dlckNhcGFiaWxpdHlQY3QiOjk5LjEsInNlbGZEaXNjaGFyZ2VSYXRlUGN0UGVyTW9udGgiOjJ9LCJzdGF0ZU9mSGVhbHRoUGN0IjpudWxsLCJ0ZXN0UmVwb3J0UmVzdWx0cyI6IkVOIDUwNjA0LTE6MjAxNiBwYXNzOyBVTiAzOC4zIHBhc3M7IHJlcG9ydCBBQ01FLVRSLTIwMjYtMDgxNSIsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsInVzYWdlSGlzdG9yeSI6eyJjaGFyZ2VEaXNjaGFyZ2VDeWNsZXMiOjR9LCJ3YXN0ZUJhdHRlcnlJbmZvcm1hdGlvbiI6IlJldHVybiB0aGUgYmF0dGVyeSB0byBhIGJpY3ljbGUgcmV0YWlsZXIgb3IgYSBtdW5pY2lwYWwgY29sbGVjdGlvbiBwb2ludC4gRG8gbm90IHBsYWNlIGl0IGluIGhvdXNlaG9sZCB3YXN0ZS4ifSwicHJvZHVjdE5hbWUiOiJBY21lIExNVCA0OFYgMjBBaCBiYXR0ZXJ5IHBhY2siLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9kcHAuYWNtZS5leGFtcGxlLzAxLzA5NTA2MDAwMTM0MzUyLzIxLzdlNGE5YzNkMmYxZThiNmE1ZDQwIiwicmVwYWlyYWJpbGl0eVNjb3JlIjpudWxsLCJyZXRlbnRpb25Mb2NrZWQiOnRydWUsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.55m5_OYvkiKUN9QV9BNB3JeqMOvkYjwYCiPxup_xciGjUrYtHOcaDdy8ULvSAy5NRGpT9p90RrajgBb0Iv5kCg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvbkxvY2tlZCI6dHJ1ZSwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.3BINMac1nxeKuqU19kKCV7k1c21eAr1DJOBT9MOT-5oh6zm3yYg6FNei69EjtkRH9EF1sRxxD_xs3auDY5HoDA", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -508,7 +516,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -591,10 +598,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -619,7 +625,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -627,22 +633,22 @@ }, "manifest": { "contentHashes": { - "auditEntries": "46110fef3e331eab68e2616292edcc10d2bfcdfc16fa7bb0409cecdf7cb86a33", + "auditEntries": "90266d629c641b1fe4ac01969138546b180d174ad4d222c5deebb3146d65862d", "eolEvent": "7cb65bb485538e109105404e15c7bc411aa3050694fa4d63339c70dbe431d3a0", - "fullView": "e8c5a8fbbddd67948a3ee3eb7abfc07427c2ff2ab23eeba3dc4dc9bbf12cbdbd", - "publicView": "74c1a1eda478cc2643b8652bf188f47e449b851ae64ae05f922de314a9249b83", - "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" + "fullView": "85fdd0d0865b22738ed8ece0cedf0e0d08107e0e13384d3ca2ff06fd3ad02d33", + "publicView": "d1c52ea096bffa8c85b10e3e509d58cb7960574b5f7798a12266a0dac3a6dd04", + "transferChain": "429b8b8e2b8f5823d3d2f55d66e78bc6c2a965a11118510b8644dcba6631794e" }, - "coreVersion": "0.21.0", + "coreVersion": "0.22.0", "createdAt": "2026-09-25T12:00:00Z", "formatVersion": "1", "issuerDid": "did:web:node.acme.example", "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjQ2MTEwZmVmM2UzMzFlYWI2OGUyNjE2MjkyZWRjYzEwZDJiZmNkZmMxNmZhN2JiMDQwOWNlY2RmN2NiODZhMzMiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6ImU4YzVhOGZiYmRkZDY3OTQ4YTNlZTNlYjdhYmZjMDc0MjdjMmZmMmFiMjNlZWJhM2RjNGRjOWJiZjEyY2JkYmQiLCJwdWJsaWNWaWV3IjoiNzRjMWExZWRhNDc4Y2MyNjQzYjg2NTJiZjE4OGY0N2U0NDliODUxYWU2NGFlMDVmOTIyZGUzMTRhOTI0OWI4MyIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMS4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.nR0d3UZbNRttz9D0VL9qGHN9dQwzFQGa4eRni8N926diP9G-qB4nGbqgqyz9VqaL6DjKReENzDldyo87sev_BA", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjkwMjY2ZDYyOWM2NDFiMWZlNGFjMDE5NjkxMzg1NDZiMTgwZDE3NGFkNGQyMjJjNWRlZWJiMzE0NmQ2NTg2MmQiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6Ijg1ZmRkMGQwODY1YjIyNzM4ZWQ4ZWNlMGNlZGYwZTBkMDgxMDdlMGUxMzM4NGQzY2EyZmYwNmZkM2FkMDJkMzMiLCJwdWJsaWNWaWV3IjoiZDFjNTJlYTA5NmJmZmE4Yzg1YjEwZTNlNTA5ZDU4Y2I3OTYwNTc0YjVmNzc5OGExMjI2NmEwZGFjM2E2ZGQwNCIsInRyYW5zZmVyQ2hhaW4iOiI0MjliOGI4ZTJiOGY1ODIzZDNkMmY1NWQ2NmU3OGJjNmMyYTk2NWExMTExODUxMGI4NjQ0ZGNiYTY2MzE3OTRlIn0sImNvcmVWZXJzaW9uIjoiMC4yMi4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ._26ZWmNwdpJiyHqjwzgLwnFdAfAJNQWCyoO2F2hMGEOknkkXRRcW-heKHiZ8usv9IPs44mezYgJRB_Ks3twmCw", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjbzJlUGVyVW5pdEtnIjo4Ni40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.LHi5ToX900cUN4vak8l1zOZ5xc1H9jW25fL4XUWuxz2JU1_3PxxrjRUTyY9RctPxPSTWmS40fUWurEZnTdE3Dw", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwid2FzdGVCYXR0ZXJ5SW5mb3JtYXRpb24iOiJSZXR1cm4gdGhlIGJhdHRlcnkgdG8gYSBiaWN5Y2xlIHJldGFpbGVyIG9yIGEgbXVuaWNpcGFsIGNvbGxlY3Rpb24gcG9pbnQuIERvIG5vdCBwbGFjZSBpdCBpbiBob3VzZWhvbGQgd2FzdGUuIn0sInByb2R1Y3ROYW1lIjoiQWNtZSBMTVQgNDhWIDIwQWggYmF0dGVyeSBwYWNrIiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInFyQ29kZVVybCI6Imh0dHBzOi8vZHBwLmFjbWUuZXhhbXBsZS8wMS8wOTUwNjAwMDEzNDM1Mi8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.PgFgGaMF1f9RGQGqXEw31ZtocGBvVDfNcu2r2D88eG8wUQZMQLSVkb99NR59UErxDH0GYS_-olz_s5yy4_Z9BQ", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -683,7 +689,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -736,10 +741,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -750,7 +754,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -775,9 +779,9 @@ "name": "Acme Zellen GmbH", "role": "manufacturer" }, - "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.mtQCd6lEOMHAMcpsKZUFbx0vvRBS05El5_PWWXqItXxIp-lJO9VV1PILF9lTPceECXVTAgDfxCFsTdf29k3iCw", + "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.QnGV5iXvbEhKvEPWaK7RjBYVQ2JqnQ5zyoFqSYMH6VAF3cZiZpVpMtaGPXiZZbl9bgtA0rYNz7CT8Pcfq4_xCg", "initiatedAt": "2026-09-10T14:00:00Z", - "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.hrv0yKqeb1DVf-X9ysNDsFOawnrocRUUaFo9ygKqitjNmxaFlj6o1Xyf6eXsyGDjOW7kBSjbrbSkbSbbsmHfCg", + "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.1kAQj3w0HJAtFlBi_mKRZANGKI_KA9zLh-EksKdrDtDsfAO6YXVJuXijwvY1amqBtlfRuc6EqvORDOGJgg3cBA", "notes": "Repurposed into stationary storage.", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "reason": "repurposing", diff --git a/ops/demo/dossiers/07-tampered-transfer-signature.json b/ops/demo/dossiers/07-tampered-transfer-signature.json index 9b7a4d08..38229b16 100644 --- a/ops/demo/dossiers/07-tampered-transfer-signature.json +++ b/ops/demo/dossiers/07-tampered-transfer-signature.json @@ -16,7 +16,7 @@ { "action": "published", "actor": "api-key", - "entryHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "entryHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { @@ -90,7 +90,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -173,10 +172,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -201,7 +199,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -246,7 +244,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -299,10 +296,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -313,7 +309,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -329,7 +325,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "1b61fd6f3f72f0730d61b59008f883d7630a03e2dea9f5bd67206482d0770530", + "entryHash": "fb79becbbefba459b654d0a85de2baf9143d3bbc9eaab3663172e0e14ba5975c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", "metadata": { "event": "transfer.initiated", @@ -338,7 +334,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "prevHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000003", "timestamp": "2026-09-10T14:00:00.042Z" @@ -346,7 +342,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "f462766c832366e92cef97196b764df1bd5ce5e1acc1fa5e2b395f99d4c3e6fa", + "entryHash": "f1fb57946b13af38d43e5866ff6c58285d31e5408e10eef40be3f5fd56578d97", "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", "metadata": { "event": "transfer.accepted", @@ -355,7 +351,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "1b61fd6f3f72f0730d61b59008f883d7630a03e2dea9f5bd67206482d0770530", + "prevHash": "fb79becbbefba459b654d0a85de2baf9143d3bbc9eaab3663172e0e14ba5975c", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000004", "timestamp": "2026-09-10T14:30:00.123456Z" @@ -363,7 +359,7 @@ { "action": "deactivated", "actor": "api-key", - "entryHash": "6fd9d12be0f3a26ae6fc2740a796a13363de1e3abd3082ce284c9eeebdc88dac", + "entryHash": "5cff9558d98c6d5294b3e6d7133d68627e164289528b9a8ff9a1d838ed6c28f9", "id": "0199a3c2-7b10-7e4a-9c3d-000000000005", "metadata": { "declaredAt": "2026-09-20T08:00:00Z", @@ -376,7 +372,7 @@ }, "newStatus": "deactivated", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "f462766c832366e92cef97196b764df1bd5ce5e1acc1fa5e2b395f99d4c3e6fa", + "prevHash": "f1fb57946b13af38d43e5866ff6c58285d31e5408e10eef40be3f5fd56578d97", "previousStatus": "active", "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000005", "timestamp": "2026-09-20T08:00:00.018Z" @@ -385,43 +381,55 @@ "didDocuments": { "did:web:node.acme.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:node.acme.example#key-1" + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" + ], + "authentication": [ + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" ], "id": "did:web:node.acme.example", "verificationMethod": [ { "controller": "did:web:node.acme.example", - "id": "did:web:node.acme.example#key-1", + "id": "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "kty": "OKP", "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] }, "did:web:secondlife.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:secondlife.example#key-1" + "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4" + ], + "authentication": [ + "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4" ], "id": "did:web:secondlife.example", "verificationMethod": [ { "controller": "did:web:secondlife.example", - "id": "did:web:secondlife.example#key-1", + "id": "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4", "kty": "OKP", "x": "_RckOFqgx1tk-3jNYC-h2ZH96_drE8WO1wLqyDXp9hg" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] } @@ -436,7 +444,7 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInNhZmV0eU1lYXN1cmVzIjoiRG8gbm90IG9wZW4gdGhlIHBhY2suIFJlcGxhY2UgaXQgdGhyb3VnaCBhbiBhdXRob3Jpc2VkIGRlYWxlci4gS2VlcCBpdCBhd2F5IGZyb20gaGVhdCBhYm92ZSA2MCBkZWdDLiIsInNwYXJlUGFydHNDb250YWN0cyI6InNwYXJlc0BhY21lLmV4YW1wbGUiLCJzdGF0ZU9mSGVhbHRoIjp7InBhcmFtZXRlclNldCI6InN0YXRpb25hcnlPckxtdCIsInJlbWFpbmluZ0NhcGFjaXR5UGN0Ijo5OS41LCJyZW1haW5pbmdQb3dlckNhcGFiaWxpdHlQY3QiOjk5LjEsInNlbGZEaXNjaGFyZ2VSYXRlUGN0UGVyTW9udGgiOjJ9LCJzdGF0ZU9mSGVhbHRoUGN0IjpudWxsLCJ0ZXN0UmVwb3J0UmVzdWx0cyI6IkVOIDUwNjA0LTE6MjAxNiBwYXNzOyBVTiAzOC4zIHBhc3M7IHJlcG9ydCBBQ01FLVRSLTIwMjYtMDgxNSIsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsInVzYWdlSGlzdG9yeSI6eyJjaGFyZ2VEaXNjaGFyZ2VDeWNsZXMiOjR9LCJ3YXN0ZUJhdHRlcnlJbmZvcm1hdGlvbiI6IlJldHVybiB0aGUgYmF0dGVyeSB0byBhIGJpY3ljbGUgcmV0YWlsZXIgb3IgYSBtdW5pY2lwYWwgY29sbGVjdGlvbiBwb2ludC4gRG8gbm90IHBsYWNlIGl0IGluIGhvdXNlaG9sZCB3YXN0ZS4ifSwicHJvZHVjdE5hbWUiOiJBY21lIExNVCA0OFYgMjBBaCBiYXR0ZXJ5IHBhY2siLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9kcHAuYWNtZS5leGFtcGxlLzAxLzA5NTA2MDAwMTM0MzUyLzIxLzdlNGE5YzNkMmYxZThiNmE1ZDQwIiwicmVwYWlyYWJpbGl0eVNjb3JlIjpudWxsLCJyZXRlbnRpb25Mb2NrZWQiOnRydWUsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.55m5_OYvkiKUN9QV9BNB3JeqMOvkYjwYCiPxup_xciGjUrYtHOcaDdy8ULvSAy5NRGpT9p90RrajgBb0Iv5kCg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvbkxvY2tlZCI6dHJ1ZSwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.3BINMac1nxeKuqU19kKCV7k1c21eAr1DJOBT9MOT-5oh6zm3yYg6FNei69EjtkRH9EF1sRxxD_xs3auDY5HoDA", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -508,7 +516,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -591,10 +598,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -619,7 +625,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -627,22 +633,22 @@ }, "manifest": { "contentHashes": { - "auditEntries": "46110fef3e331eab68e2616292edcc10d2bfcdfc16fa7bb0409cecdf7cb86a33", + "auditEntries": "90266d629c641b1fe4ac01969138546b180d174ad4d222c5deebb3146d65862d", "eolEvent": "7cb65bb485538e109105404e15c7bc411aa3050694fa4d63339c70dbe431d3a0", - "fullView": "e8c5a8fbbddd67948a3ee3eb7abfc07427c2ff2ab23eeba3dc4dc9bbf12cbdbd", - "publicView": "74c1a1eda478cc2643b8652bf188f47e449b851ae64ae05f922de314a9249b83", - "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" + "fullView": "85fdd0d0865b22738ed8ece0cedf0e0d08107e0e13384d3ca2ff06fd3ad02d33", + "publicView": "d1c52ea096bffa8c85b10e3e509d58cb7960574b5f7798a12266a0dac3a6dd04", + "transferChain": "429b8b8e2b8f5823d3d2f55d66e78bc6c2a965a11118510b8644dcba6631794e" }, - "coreVersion": "0.21.0", + "coreVersion": "0.22.0", "createdAt": "2026-09-25T12:00:00Z", "formatVersion": "1", "issuerDid": "did:web:node.acme.example", "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjQ2MTEwZmVmM2UzMzFlYWI2OGUyNjE2MjkyZWRjYzEwZDJiZmNkZmMxNmZhN2JiMDQwOWNlY2RmN2NiODZhMzMiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6ImU4YzVhOGZiYmRkZDY3OTQ4YTNlZTNlYjdhYmZjMDc0MjdjMmZmMmFiMjNlZWJhM2RjNGRjOWJiZjEyY2JkYmQiLCJwdWJsaWNWaWV3IjoiNzRjMWExZWRhNDc4Y2MyNjQzYjg2NTJiZjE4OGY0N2U0NDliODUxYWU2NGFlMDVmOTIyZGUzMTRhOTI0OWI4MyIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMS4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.nR0d3UZbNRttz9D0VL9qGHN9dQwzFQGa4eRni8N926diP9G-qB4nGbqgqyz9VqaL6DjKReENzDldyo87sev_BA", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjkwMjY2ZDYyOWM2NDFiMWZlNGFjMDE5NjkxMzg1NDZiMTgwZDE3NGFkNGQyMjJjNWRlZWJiMzE0NmQ2NTg2MmQiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6Ijg1ZmRkMGQwODY1YjIyNzM4ZWQ4ZWNlMGNlZGYwZTBkMDgxMDdlMGUxMzM4NGQzY2EyZmYwNmZkM2FkMDJkMzMiLCJwdWJsaWNWaWV3IjoiZDFjNTJlYTA5NmJmZmE4Yzg1YjEwZTNlNTA5ZDU4Y2I3OTYwNTc0YjVmNzc5OGExMjI2NmEwZGFjM2E2ZGQwNCIsInRyYW5zZmVyQ2hhaW4iOiI0MjliOGI4ZTJiOGY1ODIzZDNkMmY1NWQ2NmU3OGJjNmMyYTk2NWExMTExODUxMGI4NjQ0ZGNiYTY2MzE3OTRlIn0sImNvcmVWZXJzaW9uIjoiMC4yMi4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ._26ZWmNwdpJiyHqjwzgLwnFdAfAJNQWCyoO2F2hMGEOknkkXRRcW-heKHiZ8usv9IPs44mezYgJRB_Ks3twmCw", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjbzJlUGVyVW5pdEtnIjo4Ni40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.LHi5ToX900cUN4vak8l1zOZ5xc1H9jW25fL4XUWuxz2JU1_3PxxrjRUTyY9RctPxPSTWmS40fUWurEZnTdE3Dw", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwid2FzdGVCYXR0ZXJ5SW5mb3JtYXRpb24iOiJSZXR1cm4gdGhlIGJhdHRlcnkgdG8gYSBiaWN5Y2xlIHJldGFpbGVyIG9yIGEgbXVuaWNpcGFsIGNvbGxlY3Rpb24gcG9pbnQuIERvIG5vdCBwbGFjZSBpdCBpbiBob3VzZWhvbGQgd2FzdGUuIn0sInByb2R1Y3ROYW1lIjoiQWNtZSBMTVQgNDhWIDIwQWggYmF0dGVyeSBwYWNrIiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInFyQ29kZVVybCI6Imh0dHBzOi8vZHBwLmFjbWUuZXhhbXBsZS8wMS8wOTUwNjAwMDEzNDM1Mi8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.PgFgGaMF1f9RGQGqXEw31ZtocGBvVDfNcu2r2D88eG8wUQZMQLSVkb99NR59UErxDH0GYS_-olz_s5yy4_Z9BQ", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -683,7 +689,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -736,10 +741,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -750,7 +754,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -775,9 +779,9 @@ "name": "Acme Zellen GmbH", "role": "manufacturer" }, - "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.mtQCd6lEOMHAMcpsKZUFbx0vvRBS05El5_PWWXqItXxIp-lJO9VV1PILF9lTPceECXVTAgDfxCFsTdf29k3iCw", + "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.QnGV5iXvbEhKvEPWaK7RjBYVQ2JqnQ5zyoFqSYMH6VAF3cZiZpVpMtaGPXiZZbl9bgtA0rYNz7CT8Pcfq4_xCg", "initiatedAt": "2026-09-10T14:00:00Z", - "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.hrv0yKqeb1DVf-X9ysNDsFOawnrocRUUaFo9ygKqitjAmxaFlj6o1Xyf6eXsyGDjOW7kBSjbrbSkbSbbsmHfCg", + "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.1kAQj3w0HJAtFlBi_mKRZANGKI_KA9zLh-EksKdrDtDAfAO6YXVJuXijwvY1amqBtlfRuc6EqvORDOGJgg3cBA", "notes": "Repurposed into stationary storage.", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "reason": "repurposing", diff --git a/ops/demo/dossiers/08-hidden-field-injection.json b/ops/demo/dossiers/08-hidden-field-injection.json index d9b133aa..7b5c1240 100644 --- a/ops/demo/dossiers/08-hidden-field-injection.json +++ b/ops/demo/dossiers/08-hidden-field-injection.json @@ -16,7 +16,7 @@ { "action": "published", "actor": "api-key", - "entryHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "entryHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { @@ -90,7 +90,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -173,10 +172,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -201,7 +199,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -246,7 +244,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -299,10 +296,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -313,7 +309,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -329,7 +325,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "1b61fd6f3f72f0730d61b59008f883d7630a03e2dea9f5bd67206482d0770530", + "entryHash": "fb79becbbefba459b654d0a85de2baf9143d3bbc9eaab3663172e0e14ba5975c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", "metadata": { "event": "transfer.initiated", @@ -338,7 +334,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "prevHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000003", "timestamp": "2026-09-10T14:00:00.042Z" @@ -346,7 +342,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "f462766c832366e92cef97196b764df1bd5ce5e1acc1fa5e2b395f99d4c3e6fa", + "entryHash": "f1fb57946b13af38d43e5866ff6c58285d31e5408e10eef40be3f5fd56578d97", "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", "metadata": { "event": "transfer.accepted", @@ -355,7 +351,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "1b61fd6f3f72f0730d61b59008f883d7630a03e2dea9f5bd67206482d0770530", + "prevHash": "fb79becbbefba459b654d0a85de2baf9143d3bbc9eaab3663172e0e14ba5975c", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000004", "timestamp": "2026-09-10T14:30:00.123456Z" @@ -363,7 +359,7 @@ { "action": "deactivated", "actor": "api-key", - "entryHash": "6fd9d12be0f3a26ae6fc2740a796a13363de1e3abd3082ce284c9eeebdc88dac", + "entryHash": "5cff9558d98c6d5294b3e6d7133d68627e164289528b9a8ff9a1d838ed6c28f9", "id": "0199a3c2-7b10-7e4a-9c3d-000000000005", "metadata": { "declaredAt": "2026-09-20T08:00:00Z", @@ -376,7 +372,7 @@ }, "newStatus": "deactivated", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "f462766c832366e92cef97196b764df1bd5ce5e1acc1fa5e2b395f99d4c3e6fa", + "prevHash": "f1fb57946b13af38d43e5866ff6c58285d31e5408e10eef40be3f5fd56578d97", "previousStatus": "active", "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000005", "timestamp": "2026-09-20T08:00:00.018Z" @@ -385,43 +381,55 @@ "didDocuments": { "did:web:node.acme.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:node.acme.example#key-1" + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" + ], + "authentication": [ + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" ], "id": "did:web:node.acme.example", "verificationMethod": [ { "controller": "did:web:node.acme.example", - "id": "did:web:node.acme.example#key-1", + "id": "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "kty": "OKP", "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] }, "did:web:secondlife.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:secondlife.example#key-1" + "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4" + ], + "authentication": [ + "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4" ], "id": "did:web:secondlife.example", "verificationMethod": [ { "controller": "did:web:secondlife.example", - "id": "did:web:secondlife.example#key-1", + "id": "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4", "kty": "OKP", "x": "_RckOFqgx1tk-3jNYC-h2ZH96_drE8WO1wLqyDXp9hg" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] } @@ -436,7 +444,7 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInNhZmV0eU1lYXN1cmVzIjoiRG8gbm90IG9wZW4gdGhlIHBhY2suIFJlcGxhY2UgaXQgdGhyb3VnaCBhbiBhdXRob3Jpc2VkIGRlYWxlci4gS2VlcCBpdCBhd2F5IGZyb20gaGVhdCBhYm92ZSA2MCBkZWdDLiIsInNwYXJlUGFydHNDb250YWN0cyI6InNwYXJlc0BhY21lLmV4YW1wbGUiLCJzdGF0ZU9mSGVhbHRoIjp7InBhcmFtZXRlclNldCI6InN0YXRpb25hcnlPckxtdCIsInJlbWFpbmluZ0NhcGFjaXR5UGN0Ijo5OS41LCJyZW1haW5pbmdQb3dlckNhcGFiaWxpdHlQY3QiOjk5LjEsInNlbGZEaXNjaGFyZ2VSYXRlUGN0UGVyTW9udGgiOjJ9LCJzdGF0ZU9mSGVhbHRoUGN0IjpudWxsLCJ0ZXN0UmVwb3J0UmVzdWx0cyI6IkVOIDUwNjA0LTE6MjAxNiBwYXNzOyBVTiAzOC4zIHBhc3M7IHJlcG9ydCBBQ01FLVRSLTIwMjYtMDgxNSIsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsInVzYWdlSGlzdG9yeSI6eyJjaGFyZ2VEaXNjaGFyZ2VDeWNsZXMiOjR9LCJ3YXN0ZUJhdHRlcnlJbmZvcm1hdGlvbiI6IlJldHVybiB0aGUgYmF0dGVyeSB0byBhIGJpY3ljbGUgcmV0YWlsZXIgb3IgYSBtdW5pY2lwYWwgY29sbGVjdGlvbiBwb2ludC4gRG8gbm90IHBsYWNlIGl0IGluIGhvdXNlaG9sZCB3YXN0ZS4ifSwicHJvZHVjdE5hbWUiOiJBY21lIExNVCA0OFYgMjBBaCBiYXR0ZXJ5IHBhY2siLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9kcHAuYWNtZS5leGFtcGxlLzAxLzA5NTA2MDAwMTM0MzUyLzIxLzdlNGE5YzNkMmYxZThiNmE1ZDQwIiwicmVwYWlyYWJpbGl0eVNjb3JlIjpudWxsLCJyZXRlbnRpb25Mb2NrZWQiOnRydWUsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.55m5_OYvkiKUN9QV9BNB3JeqMOvkYjwYCiPxup_xciGjUrYtHOcaDdy8ULvSAy5NRGpT9p90RrajgBb0Iv5kCg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvbkxvY2tlZCI6dHJ1ZSwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.3BINMac1nxeKuqU19kKCV7k1c21eAr1DJOBT9MOT-5oh6zm3yYg6FNei69EjtkRH9EF1sRxxD_xs3auDY5HoDA", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -508,7 +516,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -591,10 +598,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -619,7 +625,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -627,22 +633,22 @@ }, "manifest": { "contentHashes": { - "auditEntries": "46110fef3e331eab68e2616292edcc10d2bfcdfc16fa7bb0409cecdf7cb86a33", + "auditEntries": "90266d629c641b1fe4ac01969138546b180d174ad4d222c5deebb3146d65862d", "eolEvent": "7cb65bb485538e109105404e15c7bc411aa3050694fa4d63339c70dbe431d3a0", - "fullView": "e8c5a8fbbddd67948a3ee3eb7abfc07427c2ff2ab23eeba3dc4dc9bbf12cbdbd", - "publicView": "74c1a1eda478cc2643b8652bf188f47e449b851ae64ae05f922de314a9249b83", - "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" + "fullView": "85fdd0d0865b22738ed8ece0cedf0e0d08107e0e13384d3ca2ff06fd3ad02d33", + "publicView": "d1c52ea096bffa8c85b10e3e509d58cb7960574b5f7798a12266a0dac3a6dd04", + "transferChain": "429b8b8e2b8f5823d3d2f55d66e78bc6c2a965a11118510b8644dcba6631794e" }, - "coreVersion": "0.21.0", + "coreVersion": "0.22.0", "createdAt": "2026-09-25T12:00:00Z", "formatVersion": "1", "issuerDid": "did:web:node.acme.example", "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjQ2MTEwZmVmM2UzMzFlYWI2OGUyNjE2MjkyZWRjYzEwZDJiZmNkZmMxNmZhN2JiMDQwOWNlY2RmN2NiODZhMzMiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6ImU4YzVhOGZiYmRkZDY3OTQ4YTNlZTNlYjdhYmZjMDc0MjdjMmZmMmFiMjNlZWJhM2RjNGRjOWJiZjEyY2JkYmQiLCJwdWJsaWNWaWV3IjoiNzRjMWExZWRhNDc4Y2MyNjQzYjg2NTJiZjE4OGY0N2U0NDliODUxYWU2NGFlMDVmOTIyZGUzMTRhOTI0OWI4MyIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMS4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.nR0d3UZbNRttz9D0VL9qGHN9dQwzFQGa4eRni8N926diP9G-qB4nGbqgqyz9VqaL6DjKReENzDldyo87sev_BA", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjkwMjY2ZDYyOWM2NDFiMWZlNGFjMDE5NjkxMzg1NDZiMTgwZDE3NGFkNGQyMjJjNWRlZWJiMzE0NmQ2NTg2MmQiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6Ijg1ZmRkMGQwODY1YjIyNzM4ZWQ4ZWNlMGNlZGYwZTBkMDgxMDdlMGUxMzM4NGQzY2EyZmYwNmZkM2FkMDJkMzMiLCJwdWJsaWNWaWV3IjoiZDFjNTJlYTA5NmJmZmE4Yzg1YjEwZTNlNTA5ZDU4Y2I3OTYwNTc0YjVmNzc5OGExMjI2NmEwZGFjM2E2ZGQwNCIsInRyYW5zZmVyQ2hhaW4iOiI0MjliOGI4ZTJiOGY1ODIzZDNkMmY1NWQ2NmU3OGJjNmMyYTk2NWExMTExODUxMGI4NjQ0ZGNiYTY2MzE3OTRlIn0sImNvcmVWZXJzaW9uIjoiMC4yMi4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ._26ZWmNwdpJiyHqjwzgLwnFdAfAJNQWCyoO2F2hMGEOknkkXRRcW-heKHiZ8usv9IPs44mezYgJRB_Ks3twmCw", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjbzJlUGVyVW5pdEtnIjo4Ni40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.LHi5ToX900cUN4vak8l1zOZ5xc1H9jW25fL4XUWuxz2JU1_3PxxrjRUTyY9RctPxPSTWmS40fUWurEZnTdE3Dw", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwid2FzdGVCYXR0ZXJ5SW5mb3JtYXRpb24iOiJSZXR1cm4gdGhlIGJhdHRlcnkgdG8gYSBiaWN5Y2xlIHJldGFpbGVyIG9yIGEgbXVuaWNpcGFsIGNvbGxlY3Rpb24gcG9pbnQuIERvIG5vdCBwbGFjZSBpdCBpbiBob3VzZWhvbGQgd2FzdGUuIn0sInByb2R1Y3ROYW1lIjoiQWNtZSBMTVQgNDhWIDIwQWggYmF0dGVyeSBwYWNrIiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInFyQ29kZVVybCI6Imh0dHBzOi8vZHBwLmFjbWUuZXhhbXBsZS8wMS8wOTUwNjAwMDEzNDM1Mi8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.PgFgGaMF1f9RGQGqXEw31ZtocGBvVDfNcu2r2D88eG8wUQZMQLSVkb99NR59UErxDH0GYS_-olz_s5yy4_Z9BQ", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -683,7 +689,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -736,10 +741,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -750,7 +754,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -776,9 +780,9 @@ "name": "Acme Zellen GmbH", "role": "manufacturer" }, - "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.mtQCd6lEOMHAMcpsKZUFbx0vvRBS05El5_PWWXqItXxIp-lJO9VV1PILF9lTPceECXVTAgDfxCFsTdf29k3iCw", + "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.QnGV5iXvbEhKvEPWaK7RjBYVQ2JqnQ5zyoFqSYMH6VAF3cZiZpVpMtaGPXiZZbl9bgtA0rYNz7CT8Pcfq4_xCg", "initiatedAt": "2026-09-10T14:00:00Z", - "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.hrv0yKqeb1DVf-X9ysNDsFOawnrocRUUaFo9ygKqitjNmxaFlj6o1Xyf6eXsyGDjOW7kBSjbrbSkbSbbsmHfCg", + "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.1kAQj3w0HJAtFlBi_mKRZANGKI_KA9zLh-EksKdrDtDsfAO6YXVJuXijwvY1amqBtlfRuc6EqvORDOGJgg3cBA", "notes": "Repurposed into stationary storage.", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "reason": "repurposing", diff --git a/ops/demo/dossiers/09-unknown-top-level-field.json b/ops/demo/dossiers/09-unknown-top-level-field.json index a10eae97..327733be 100644 --- a/ops/demo/dossiers/09-unknown-top-level-field.json +++ b/ops/demo/dossiers/09-unknown-top-level-field.json @@ -16,7 +16,7 @@ { "action": "published", "actor": "api-key", - "entryHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "entryHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { @@ -90,7 +90,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -173,10 +172,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -201,7 +199,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -246,7 +244,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -299,10 +296,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -313,7 +309,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -329,7 +325,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "1b61fd6f3f72f0730d61b59008f883d7630a03e2dea9f5bd67206482d0770530", + "entryHash": "fb79becbbefba459b654d0a85de2baf9143d3bbc9eaab3663172e0e14ba5975c", "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", "metadata": { "event": "transfer.initiated", @@ -338,7 +334,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "2934a67be97238b4f38ff4ee4d53464a0c197cec343297b25124f894b942c2f2", + "prevHash": "aca1c64627ce429080b95ae4af1e6d16d8280eaa004004e205d3d7eb2934a92c", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000003", "timestamp": "2026-09-10T14:00:00.042Z" @@ -346,7 +342,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "f462766c832366e92cef97196b764df1bd5ce5e1acc1fa5e2b395f99d4c3e6fa", + "entryHash": "f1fb57946b13af38d43e5866ff6c58285d31e5408e10eef40be3f5fd56578d97", "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", "metadata": { "event": "transfer.accepted", @@ -355,7 +351,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "1b61fd6f3f72f0730d61b59008f883d7630a03e2dea9f5bd67206482d0770530", + "prevHash": "fb79becbbefba459b654d0a85de2baf9143d3bbc9eaab3663172e0e14ba5975c", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000004", "timestamp": "2026-09-10T14:30:00.123456Z" @@ -363,7 +359,7 @@ { "action": "deactivated", "actor": "api-key", - "entryHash": "6fd9d12be0f3a26ae6fc2740a796a13363de1e3abd3082ce284c9eeebdc88dac", + "entryHash": "5cff9558d98c6d5294b3e6d7133d68627e164289528b9a8ff9a1d838ed6c28f9", "id": "0199a3c2-7b10-7e4a-9c3d-000000000005", "metadata": { "declaredAt": "2026-09-20T08:00:00Z", @@ -376,7 +372,7 @@ }, "newStatus": "deactivated", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "f462766c832366e92cef97196b764df1bd5ce5e1acc1fa5e2b395f99d4c3e6fa", + "prevHash": "f1fb57946b13af38d43e5866ff6c58285d31e5408e10eef40be3f5fd56578d97", "previousStatus": "active", "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000005", "timestamp": "2026-09-20T08:00:00.018Z" @@ -385,43 +381,55 @@ "didDocuments": { "did:web:node.acme.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:node.acme.example#key-1" + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" + ], + "authentication": [ + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" ], "id": "did:web:node.acme.example", "verificationMethod": [ { "controller": "did:web:node.acme.example", - "id": "did:web:node.acme.example#key-1", + "id": "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "kty": "OKP", "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] }, "did:web:secondlife.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:secondlife.example#key-1" + "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4" + ], + "authentication": [ + "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4" ], "id": "did:web:secondlife.example", "verificationMethod": [ { "controller": "did:web:secondlife.example", - "id": "did:web:secondlife.example#key-1", + "id": "did:web:secondlife.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:atZR63tNG3W2GhPpVleKnfrw1N7N6LAqOe5grE2SBR4", "kty": "OKP", "x": "_RckOFqgx1tk-3jNYC-h2ZH96_drE8WO1wLqyDXp9hg" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] } @@ -436,7 +444,7 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInNhZmV0eU1lYXN1cmVzIjoiRG8gbm90IG9wZW4gdGhlIHBhY2suIFJlcGxhY2UgaXQgdGhyb3VnaCBhbiBhdXRob3Jpc2VkIGRlYWxlci4gS2VlcCBpdCBhd2F5IGZyb20gaGVhdCBhYm92ZSA2MCBkZWdDLiIsInNwYXJlUGFydHNDb250YWN0cyI6InNwYXJlc0BhY21lLmV4YW1wbGUiLCJzdGF0ZU9mSGVhbHRoIjp7InBhcmFtZXRlclNldCI6InN0YXRpb25hcnlPckxtdCIsInJlbWFpbmluZ0NhcGFjaXR5UGN0Ijo5OS41LCJyZW1haW5pbmdQb3dlckNhcGFiaWxpdHlQY3QiOjk5LjEsInNlbGZEaXNjaGFyZ2VSYXRlUGN0UGVyTW9udGgiOjJ9LCJzdGF0ZU9mSGVhbHRoUGN0IjpudWxsLCJ0ZXN0UmVwb3J0UmVzdWx0cyI6IkVOIDUwNjA0LTE6MjAxNiBwYXNzOyBVTiAzOC4zIHBhc3M7IHJlcG9ydCBBQ01FLVRSLTIwMjYtMDgxNSIsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsInVzYWdlSGlzdG9yeSI6eyJjaGFyZ2VEaXNjaGFyZ2VDeWNsZXMiOjR9LCJ3YXN0ZUJhdHRlcnlJbmZvcm1hdGlvbiI6IlJldHVybiB0aGUgYmF0dGVyeSB0byBhIGJpY3ljbGUgcmV0YWlsZXIgb3IgYSBtdW5pY2lwYWwgY29sbGVjdGlvbiBwb2ludC4gRG8gbm90IHBsYWNlIGl0IGluIGhvdXNlaG9sZCB3YXN0ZS4ifSwicHJvZHVjdE5hbWUiOiJBY21lIExNVCA0OFYgMjBBaCBiYXR0ZXJ5IHBhY2siLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwicXJDb2RlVXJsIjoiaHR0cHM6Ly9kcHAuYWNtZS5leGFtcGxlLzAxLzA5NTA2MDAwMTM0MzUyLzIxLzdlNGE5YzNkMmYxZThiNmE1ZDQwIiwicmVwYWlyYWJpbGl0eVNjb3JlIjpudWxsLCJyZXRlbnRpb25Mb2NrZWQiOnRydWUsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.55m5_OYvkiKUN9QV9BNB3JeqMOvkYjwYCiPxup_xciGjUrYtHOcaDdy8ULvSAy5NRGpT9p90RrajgBb0Iv5kCg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvbkxvY2tlZCI6dHJ1ZSwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.3BINMac1nxeKuqU19kKCV7k1c21eAr1DJOBT9MOT-5oh6zm3yYg6FNei69EjtkRH9EF1sRxxD_xs3auDY5HoDA", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -508,7 +516,6 @@ "weightPct": 2.0 } ], - "co2ePerUnitKg": 86.4, "componentPartNumbers": [ "ACME-CELL-21700-NMC", "ACME-BMS-13S-V1", @@ -591,10 +598,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "safetyMeasures": "Do not open the pack. Replace it through an authorised dealer. Keep it away from heat above 60 degC.", @@ -619,7 +625,7 @@ "repairabilityScore": null, "retentionLocked": true, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -627,22 +633,22 @@ }, "manifest": { "contentHashes": { - "auditEntries": "46110fef3e331eab68e2616292edcc10d2bfcdfc16fa7bb0409cecdf7cb86a33", + "auditEntries": "90266d629c641b1fe4ac01969138546b180d174ad4d222c5deebb3146d65862d", "eolEvent": "7cb65bb485538e109105404e15c7bc411aa3050694fa4d63339c70dbe431d3a0", - "fullView": "e8c5a8fbbddd67948a3ee3eb7abfc07427c2ff2ab23eeba3dc4dc9bbf12cbdbd", - "publicView": "74c1a1eda478cc2643b8652bf188f47e449b851ae64ae05f922de314a9249b83", - "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" + "fullView": "85fdd0d0865b22738ed8ece0cedf0e0d08107e0e13384d3ca2ff06fd3ad02d33", + "publicView": "d1c52ea096bffa8c85b10e3e509d58cb7960574b5f7798a12266a0dac3a6dd04", + "transferChain": "429b8b8e2b8f5823d3d2f55d66e78bc6c2a965a11118510b8644dcba6631794e" }, - "coreVersion": "0.21.0", + "coreVersion": "0.22.0", "createdAt": "2026-09-25T12:00:00Z", "formatVersion": "1", "issuerDid": "did:web:node.acme.example", "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjQ2MTEwZmVmM2UzMzFlYWI2OGUyNjE2MjkyZWRjYzEwZDJiZmNkZmMxNmZhN2JiMDQwOWNlY2RmN2NiODZhMzMiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6ImU4YzVhOGZiYmRkZDY3OTQ4YTNlZTNlYjdhYmZjMDc0MjdjMmZmMmFiMjNlZWJhM2RjNGRjOWJiZjEyY2JkYmQiLCJwdWJsaWNWaWV3IjoiNzRjMWExZWRhNDc4Y2MyNjQzYjg2NTJiZjE4OGY0N2U0NDliODUxYWU2NGFlMDVmOTIyZGUzMTRhOTI0OWI4MyIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMS4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.nR0d3UZbNRttz9D0VL9qGHN9dQwzFQGa4eRni8N926diP9G-qB4nGbqgqyz9VqaL6DjKReENzDldyo87sev_BA", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjkwMjY2ZDYyOWM2NDFiMWZlNGFjMDE5NjkxMzg1NDZiMTgwZDE3NGFkNGQyMjJjNWRlZWJiMzE0NmQ2NTg2MmQiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6Ijg1ZmRkMGQwODY1YjIyNzM4ZWQ4ZWNlMGNlZGYwZTBkMDgxMDdlMGUxMzM4NGQzY2EyZmYwNmZkM2FkMDJkMzMiLCJwdWJsaWNWaWV3IjoiZDFjNTJlYTA5NmJmZmE4Yzg1YjEwZTNlNTA5ZDU4Y2I3OTYwNTc0YjVmNzc5OGExMjI2NmEwZGFjM2E2ZGQwNCIsInRyYW5zZmVyQ2hhaW4iOiI0MjliOGI4ZTJiOGY1ODIzZDNkMmY1NWQ2NmU3OGJjNmMyYTk2NWExMTExODUxMGI4NjQ0ZGNiYTY2MzE3OTRlIn0sImNvcmVWZXJzaW9uIjoiMC4yMi4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ._26ZWmNwdpJiyHqjwzgLwnFdAfAJNQWCyoO2F2hMGEOknkkXRRcW-heKHiZ8usv9IPs44mezYgJRB_Ks3twmCw", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjbzJlUGVyVW5pdEtnIjo4Ni40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMjEvN2U0YTljM2QyZjFlOGI2YTVkNDAiLCJyZXBhaXJhYmlsaXR5U2NvcmUiOm51bGwsInJldGVudGlvblVudGlsIjoiMjAzNi0wOC0yOVQwOTowNTowMFoiLCJzY2hlbWFWZXJzaW9uIjoiMi43LjAiLCJzdGF0dXMiOiJhY3RpdmUiLCJ1cGRhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInZlcnNpb24iOjF9.LHi5ToX900cUN4vak8l1zOZ5xc1H9jW25fL4XUWuxz2JU1_3PxxrjRUTyY9RctPxPSTWmS40fUWurEZnTdE3Dw", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwibWFudWZhY3R1cmVyIjp7ImFkZHJlc3MiOiJNdXN0ZXJzdHJhc3NlIDEsIDgwMzMxIE3DvG5jaGVuLCBERSIsImNvdW50cnkiOm51bGwsImRpZFdlYlVybCI6Imh0dHBzOi8vbm9kZS5hY21lLmV4YW1wbGUvLndlbGwta25vd24vZGlkLmpzb24iLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJtYXRlcmlhbHMiOlt7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6Ik5NQyA4MTEgY2F0aG9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6OCwid2VpZ2h0S2ciOjMuMX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJHcmFwaGl0ZSBhbm9kZSBhY3RpdmUgbWF0ZXJpYWwiLCJyZWN5Y2xlZFBjdCI6NCwid2VpZ2h0S2ciOjEuOX0seyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJBbHVtaW5pdW0gcGFjayBob3VzaW5nIiwicmVjeWNsZWRQY3QiOjYwLCJ3ZWlnaHRLZyI6Mi4yfV0sInBsYWNlZE9uTWFya2V0RGF0ZSI6IjIwMjYtMDktMDEiLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHJvZHVjdEdyb3VwRGF0YSI6eyJiYXR0ZXJ5Q2hlbWlzdHJ5IjoiTk1DIiwiYmF0dGVyeU1vZGVsSWQiOiJBQ01FLUxNVC00OFYiLCJiYXR0ZXJ5UGFzc3BvcnROdW1iZXIiOiJVUk46T0RMOkJBVFQ6MDk1MDYwMDAxMzQzNTI6QUNNRS1MTVQtMjAyNi0wOSIsImJhdHRlcnlUeXBlIjoibG10IiwiYmF0dGVyeVdlaWdodEtnIjoxMi40LCJjcml0aWNhbFJhd01hdGVyaWFscyI6W3siY2FzTnVtYmVyIjoiNzQ0MC00OC00IiwibmFtZSI6IkNvYmFsdCIsIndlaWdodEdyYW1zIjozMTB9LHsiY2FzTnVtYmVyIjoiNzQzOS05My0yIiwibmFtZSI6IkxpdGhpdW0iLCJ3ZWlnaHRHcmFtcyI6NDIwfSx7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJOYXR1cmFsIGdyYXBoaXRlIiwid2VpZ2h0R3JhbXMiOjE4MDB9XSwiY3ljbGVMaWZlVGVzdENSYXRlIjoxLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0SWRlbnRpZmllciI6eyJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJzY2hlbWUiOiJnczEifSwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0IjpudWxsLCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOm51bGwsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwid2FzdGVCYXR0ZXJ5SW5mb3JtYXRpb24iOiJSZXR1cm4gdGhlIGJhdHRlcnkgdG8gYSBiaWN5Y2xlIHJldGFpbGVyIG9yIGEgbXVuaWNpcGFsIGNvbGxlY3Rpb24gcG9pbnQuIERvIG5vdCBwbGFjZSBpdCBpbiBob3VzZWhvbGQgd2FzdGUuIn0sInByb2R1Y3ROYW1lIjoiQWNtZSBMTVQgNDhWIDIwQWggYmF0dGVyeSBwYWNrIiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInFyQ29kZVVybCI6Imh0dHBzOi8vZHBwLmFjbWUuZXhhbXBsZS8wMS8wOTUwNjAwMDEzNDM1Mi8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjguMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.PgFgGaMF1f9RGQGqXEw31ZtocGBvVDfNcu2r2D88eG8wUQZMQLSVkb99NR59UErxDH0GYS_-olz_s5yy4_Z9BQ", "payload": { "batchId": "ACME-LMT-2026-09", "co2ePerUnit": null, @@ -683,7 +689,6 @@ "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", "batteryType": "lmt", "batteryWeightKg": 12.4, - "co2ePerUnitKg": 86.4, "criticalRawMaterials": [ { "casNumber": "7440-48-4", @@ -736,10 +741,9 @@ "scheme": "gs1" }, "ratedCapacityKwh": null, - "recycledContentCobaltPct": 12.0, - "recycledContentLeadPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 8.0, + "recycledContentCobaltPct": null, + "recycledContentLithiumPct": null, + "recycledContentNickelPct": null, "renewableContentPct": 14.0, "roundTripEfficiencyAtHalfCycleLifePct": 91.0, "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", @@ -750,7 +754,7 @@ "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/21/7e4a9c3d2f1e8b6a5d40", "repairabilityScore": null, "retentionUntil": "2036-08-29T09:05:00Z", - "schemaVersion": "2.7.0", + "schemaVersion": "2.8.0", "status": "active", "updatedAt": "2026-09-01T09:05:00Z", "version": 1 @@ -775,9 +779,9 @@ "name": "Acme Zellen GmbH", "role": "manufacturer" }, - "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.mtQCd6lEOMHAMcpsKZUFbx0vvRBS05El5_PWWXqItXxIp-lJO9VV1PILF9lTPceECXVTAgDfxCFsTdf29k3iCw", + "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.QnGV5iXvbEhKvEPWaK7RjBYVQ2JqnQ5zyoFqSYMH6VAF3cZiZpVpMtaGPXiZZbl9bgtA0rYNz7CT8Pcfq4_xCg", "initiatedAt": "2026-09-10T14:00:00Z", - "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.hrv0yKqeb1DVf-X9ysNDsFOawnrocRUUaFo9ygKqitjNmxaFlj6o1Xyf6eXsyGDjOW7kBSjbrbSkbSbbsmHfCg", + "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.1kAQj3w0HJAtFlBi_mKRZANGKI_KA9zLh-EksKdrDtDsfAO6YXVJuXijwvY1amqBtlfRuc6EqvORDOGJgg3cBA", "notes": "Repurposed into stationary storage.", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "reason": "repurposing", diff --git a/ops/demo/dossiers/11-canonicalisation-vectors.json b/ops/demo/dossiers/11-canonicalisation-vectors.json index 77bd6695..2862ed38 100644 --- a/ops/demo/dossiers/11-canonicalisation-vectors.json +++ b/ops/demo/dossiers/11-canonicalisation-vectors.json @@ -63,28 +63,34 @@ "didDocuments": { "did:web:node.acme.example": { "@context": [ - "https://www.w3.org/ns/did/v1" + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/ns/cid/v1" ], "assertionMethod": [ - "did:web:node.acme.example#key-1" + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" + ], + "authentication": [ + "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck" ], "id": "did:web:node.acme.example", "verificationMethod": [ { "controller": "did:web:node.acme.example", - "id": "did:web:node.acme.example#key-1", + "id": "did:web:node.acme.example#urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "publicKeyJwk": { + "alg": "EdDSA", "crv": "Ed25519", + "kid": "urn:ietf:params:oauth:jwk-thumbprint:sha-256:--6IM5l0OosLj9yWskISYhUA3n_3CURQkmrYMSha_ck", "kty": "OKP", "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" }, - "type": "JsonWebKey2020" + "type": "JsonWebKey" } ] } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwidGlueU51bWJlciI6NWUtNywiw4ltaXNzaW9uIjoiMC41ZS0zIHRlc3QiLCLDqSI6Mywi4oKsIjo0LCLwn5iAIjo1LCLvvKEiOjZ9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsInN0YXR1cyI6ImFjdGl2ZSJ9.-0gGJO8CC_JeYka-60cDzkSdj1vqst_47SyafFl9yb4jA2GCc_grSLhYrCrx3kC8rZpT6sTwRFT3gUo5UfUdBg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwidGlueU51bWJlciI6NWUtNywiw4ltaXNzaW9uIjoiMC41ZS0zIHRlc3QiLCLDqSI6Mywi4oKsIjo0LCLwn5iAIjo1LCLvvKEiOjZ9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsInN0YXR1cyI6ImFjdGl2ZSJ9.E4z7Y_9hxAqBfGqTL8Q8KbY7j30w1s9h4ntXyRZv2H5MMvkrtyQxgxYN2G-V3KXZ8vVpM_LvNsLlNBoyuY9RDw", "payload": { "content": { "B": 2, @@ -108,16 +114,16 @@ "fullView": "aa2588691c23ed546ddafa4d32ca3a96fb905ec06980b76bf2e1562e8d3984a1", "publicView": "aa2588691c23ed546ddafa4d32ca3a96fb905ec06980b76bf2e1562e8d3984a1" }, - "coreVersion": "0.21.0", + "coreVersion": "0.22.0", "createdAt": "2026-09-25T12:00:00Z", "formatVersion": "1", "issuerDid": "did:web:node.acme.example", "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjFmNjNhODBiN2VmZDc5YzFjMmZiMmVjMmQyZjhiNmQ3ZWY0OWFlYjkyNTcwMTA3NmE5ODA1OWIxNGI4YWIwZjciLCJmdWxsVmlldyI6ImFhMjU4ODY5MWMyM2VkNTQ2ZGRhZmE0ZDMyY2EzYTk2ZmI5MDVlYzA2OTgwYjc2YmYyZTE1NjJlOGQzOTg0YTEiLCJwdWJsaWNWaWV3IjoiYWEyNTg4NjkxYzIzZWQ1NDZkZGFmYTRkMzJjYTNhOTZmYjkwNWVjMDY5ODBiNzZiZjJlMTU2MmU4ZDM5ODRhMSJ9LCJjb3JlVmVyc2lvbiI6IjAuMjEuMCIsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjVUMTI6MDA6MDBaIiwiZm9ybWF0VmVyc2lvbiI6IjEiLCJpc3N1ZXJEaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwibm9kZVZlcnNpb24iOiIwLjE0LjAiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIn0.JJwZp2H_nLIl8bJyVvrT23fepD43SaAiqVBiDP48D1uGEMkQ_bGWb2n2qK6pq65zzYWSh6I1LHHHMyZyB-QsDw", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjFmNjNhODBiN2VmZDc5YzFjMmZiMmVjMmQyZjhiNmQ3ZWY0OWFlYjkyNTcwMTA3NmE5ODA1OWIxNGI4YWIwZjciLCJmdWxsVmlldyI6ImFhMjU4ODY5MWMyM2VkNTQ2ZGRhZmE0ZDMyY2EzYTk2ZmI5MDVlYzA2OTgwYjc2YmYyZTE1NjJlOGQzOTg0YTEiLCJwdWJsaWNWaWV3IjoiYWEyNTg4NjkxYzIzZWQ1NDZkZGFmYTRkMzJjYTNhOTZmYjkwNWVjMDY5ODBiNzZiZjJlMTU2MmU4ZDM5ODRhMSJ9LCJjb3JlVmVyc2lvbiI6IjAuMjIuMCIsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjVUMTI6MDA6MDBaIiwiZm9ybWF0VmVyc2lvbiI6IjEiLCJpc3N1ZXJEaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwibm9kZVZlcnNpb24iOiIwLjE0LjAiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIn0.otlYqcNOfPDgD2yOmdj4pB7e0V_MtS3-5vfCXOie3U0YEtq9GnIJYqRiJXNnPfq-cbLGzPp1mZO0raotXJbXDQ", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwidGlueU51bWJlciI6NWUtNywiw4ltaXNzaW9uIjoiMC41ZS0zIHRlc3QiLCLDqSI6Mywi4oKsIjo0LCLwn5iAIjo1LCLvvKEiOjZ9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsInN0YXR1cyI6ImFjdGl2ZSJ9.-0gGJO8CC_JeYka-60cDzkSdj1vqst_47SyafFl9yb4jA2GCc_grSLhYrCrx3kC8rZpT6sTwRFT3gUo5UfUdBg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUjdXJuOmlldGY6cGFyYW1zOm9hdXRoOmp3ay10aHVtYnByaW50OnNoYS0yNTY6LS02SU01bDBPb3NMajl5V3NrSVNZaFVBM25fM0NVUlFrbXJZTVNoYV9jayJ9.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwidGlueU51bWJlciI6NWUtNywiw4ltaXNzaW9uIjoiMC41ZS0zIHRlc3QiLCLDqSI6Mywi4oKsIjo0LCLwn5iAIjo1LCLvvKEiOjZ9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsInN0YXR1cyI6ImFjdGl2ZSJ9.E4z7Y_9hxAqBfGqTL8Q8KbY7j30w1s9h4ntXyRZv2H5MMvkrtyQxgxYN2G-V3KXZ8vVpM_LvNsLlNBoyuY9RDw", "payload": { "content": { "B": 2, diff --git a/ops/demo/dossiers/expected.json b/ops/demo/dossiers/expected.json index 018d455d..5a773da4 100644 --- a/ops/demo/dossiers/expected.json +++ b/ops/demo/dossiers/expected.json @@ -432,7 +432,7 @@ "exit": 1 }, "09-unknown-top-level-field.json": { - "error": "not a valid dossier — malformed JSON or an unrecognised field: unknown field `verifiedBy`, expected one of `manifest`, `manifestJws`, `fullView`, `publicView`, `didDocuments`, `auditEntries`, `transferChain`, `eolEvent`, `checkpoint`, `calcReceipts`, `componentGraph`, `qualifiedSeal` at line 795 column 14", + "error": "not a valid dossier — malformed JSON or an unrecognised field: unknown field `verifiedBy`, expected one of `manifest`, `manifestJws`, `fullView`, `publicView`, `didDocuments`, `auditEntries`, `transferChain`, `eolEvent`, `checkpoint`, `calcReceipts`, `componentGraph`, `qualifiedSeal` at line 799 column 14", "exit": 2 }, "10-not-json.txt": { diff --git a/ops/demo/passports/01-battery-industrial-nmc.json b/ops/demo/passports/01-battery-industrial-nmc.json index c7c76195..06c79df1 100644 --- a/ops/demo/passports/01-battery-industrial-nmc.json +++ b/ops/demo/passports/01-battery-industrial-nmc.json @@ -19,7 +19,6 @@ "productIdentifier": { "scheme": "gs1", "gtin": "09590000000014" }, "batteryType": "industrial", "batteryChemistry": "NMC", - "co2ePerUnitKg": 41.8, "batteryPassportNumber": "URN:ODL:BATT:09590000000014:ODL-IND-NMC-2026-Q2-001", "batteryModelId": "GC-IND-NMC48-320", "manufacturingPlace": "Berlin, DE (plant BLN-1)", @@ -35,10 +34,6 @@ "powerLimitMinW": 200.0, "powerLimitMaxW": 6000.0, "renewableContentPct": 18.0, - "recycledContentCobaltPct": 9.8, - "recycledContentLithiumPct": 10.5, - "recycledContentNickelPct": 12.3, - "recycledContentLeadPct": 0.0, "recycledContentReportingYear": 2026, "expectedLifetimeCycles": 2800, "expectedLifetimeReferenceTest": "IEC 61427-1:2013, 25 degC, 80% DoD", diff --git a/ops/demo/passports/02-battery-industrial-lfp.json b/ops/demo/passports/02-battery-industrial-lfp.json index 54786a1d..04a19663 100644 --- a/ops/demo/passports/02-battery-industrial-lfp.json +++ b/ops/demo/passports/02-battery-industrial-lfp.json @@ -19,7 +19,6 @@ "productIdentifier": { "scheme": "gs1", "gtin": "09590000000021" }, "batteryType": "industrial", "batteryChemistry": "LFP", - "co2ePerUnitKg": 33.4, "batteryPassportNumber": "URN:ODL:BATT:09590000000021:ODL-IND-LFP-2026-Q2-014", "batteryModelId": "GC-ESS-LFP512-100", "manufacturingPlace": "Berlin, DE (plant BLN-2)", @@ -35,10 +34,6 @@ "powerLimitMinW": 150.0, "powerLimitMaxW": 7500.0, "renewableContentPct": 22.0, - "recycledContentCobaltPct": 0.0, - "recycledContentLithiumPct": 7.5, - "recycledContentNickelPct": 0.0, - "recycledContentLeadPct": 0.0, "recycledContentReportingYear": 2026, "expectedLifetimeCycles": 6000, "expectedLifetimeReferenceTest": "IEC 61427-1:2013, 25 degC, 90% DoD", diff --git a/ops/demo/passports/03-battery-industrial-lead-acid.json b/ops/demo/passports/03-battery-industrial-lead-acid.json index 9a8249fe..f45f12d6 100644 --- a/ops/demo/passports/03-battery-industrial-lead-acid.json +++ b/ops/demo/passports/03-battery-industrial-lead-acid.json @@ -19,7 +19,6 @@ "productIdentifier": { "scheme": "gs1", "gtin": "09590000000038" }, "batteryType": "industrial", "batteryChemistry": "lead-acid", - "co2ePerUnitKg": 28.9, "batteryPassportNumber": "URN:ODL:BATT:09590000000038:ODL-IND-PB-2026-Q1-007", "batteryModelId": "RP-VRLA-12-200", "manufacturingPlace": "Duisburg, DE (plant DU-3)", @@ -35,10 +34,6 @@ "powerLimitMinW": 50.0, "powerLimitMaxW": 2400.0, "renewableContentPct": 2.0, - "recycledContentCobaltPct": 0.0, - "recycledContentLithiumPct": 0.0, - "recycledContentNickelPct": 0.0, - "recycledContentLeadPct": 82.0, "recycledContentReportingYear": 2026, "expectedLifetimeCycles": 1200, "expectedLifetimeReferenceTest": "IEC 60896-21/22, 25 degC, 50% DoD", diff --git a/ops/demo/passports/04-battery-ev-nmc.json b/ops/demo/passports/04-battery-ev-nmc.json index 92f7cf70..7b73a169 100644 --- a/ops/demo/passports/04-battery-ev-nmc.json +++ b/ops/demo/passports/04-battery-ev-nmc.json @@ -19,7 +19,6 @@ "productIdentifier": { "scheme": "gs1", "gtin": "09590000000045" }, "batteryType": "ev", "batteryChemistry": "NMC", - "co2ePerUnitKg": 61.2, "batteryPassportNumber": "URN:ODL:BATT:09590000000045:VBX-EV-2026-0421", "batteryModelId": "VBX-EV75-NMC811-P4S96", "manufacturingPlace": "Ingolstadt, DE (plant ING-1)", @@ -35,10 +34,6 @@ "powerLimitMinW": 500.0, "powerLimitMaxW": 220000.0, "renewableContentPct": 16.0, - "recycledContentCobaltPct": 12.0, - "recycledContentLithiumPct": 9.0, - "recycledContentNickelPct": 11.0, - "recycledContentLeadPct": 0.0, "recycledContentReportingYear": 2026, "capacityThresholdForExhaustionPct": 70.0, "expectedLifetimeCycles": 1800, diff --git a/ops/demo/passports/05-battery-lmt-lfp.json b/ops/demo/passports/05-battery-lmt-lfp.json index b2c3eee9..6147119e 100644 --- a/ops/demo/passports/05-battery-lmt-lfp.json +++ b/ops/demo/passports/05-battery-lmt-lfp.json @@ -19,7 +19,6 @@ "productIdentifier": { "scheme": "gs1", "gtin": "09590000000052" }, "batteryType": "lmt", "batteryChemistry": "LFP", - "co2ePerUnitKg": 9.4, "batteryPassportNumber": "URN:ODL:BATT:09590000000052:VBX-LMT-2026-0188", "batteryModelId": "VBX-LMT36-14-10S", "manufacturingPlace": "Ingolstadt, DE (plant ING-2)", @@ -35,10 +34,6 @@ "powerLimitMinW": 10.0, "powerLimitMaxW": 750.0, "renewableContentPct": 12.0, - "recycledContentCobaltPct": 0.0, - "recycledContentLithiumPct": 6.0, - "recycledContentNickelPct": 0.0, - "recycledContentLeadPct": 0.0, "recycledContentReportingYear": 2026, "expectedLifetimeCycles": 1000, "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", diff --git a/ops/demo/passports/README.md b/ops/demo/passports/README.md index 62139b02..e90552fa 100644 --- a/ops/demo/passports/README.md +++ b/ops/demo/passports/README.md @@ -25,7 +25,7 @@ These files go straight to `POST /vault/api/v1/dpp` with a fully-populated |------|---------------|-----------|-------| | `01-battery-industrial-nmc.json` | industrial | NMC | Reference industrial pack. Full point 1–3 set + the point 4 individual-battery blocks (`dynamicPerformance`, `stateOfHealth`, `usageHistory`). | | `02-battery-industrial-lfp.json` | industrial | LFP | Stationary-storage style, cobalt-free, 6000-cycle. | -| `03-battery-industrial-lead-acid.json` | industrial | lead-acid | VRLA standby. Carries `hazardSymbol: "lead"` and a non-zero `recycledContentLeadPct`. | +| `03-battery-industrial-lead-acid.json` | industrial | lead-acid | VRLA standby. Carries `hazardSymbol: "lead"`. | | `04-battery-ev-nmc.json` | ev | NMC | EV traction battery. Adds the EV-only `capacityThresholdForExhaustionPct` and the `electricVehicle` `stateOfHealth` parameter set (SOCE). | | `05-battery-lmt-lfp.json` | lmt | LFP | e-bike pack. `stationaryOrLmt` state-of-health set; **no** capacity-threshold field (barred for LMT). | | `06-battery-portable-nimh.json` | portable | NiMH | Outside the Commission's per-category guidance, so the content gate imposes nothing — the minimal end of the spectrum. |