From 7eb2767db2f8abf4eab4df8a7c6f16659eae7e70 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:06:17 +0000 Subject: [PATCH 001/168] chore(deps-dev): bump vite-plugin-static-copy from 4.1.0 to 4.1.1 Bumps [vite-plugin-static-copy](https://github.com/sapphi-red/vite-plugin-static-copy) from 4.1.0 to 4.1.1. - [Release notes](https://github.com/sapphi-red/vite-plugin-static-copy/releases) - [Changelog](https://github.com/sapphi-red/vite-plugin-static-copy/blob/main/CHANGELOG.md) - [Commits](https://github.com/sapphi-red/vite-plugin-static-copy/compare/vite-plugin-static-copy@4.1.0...vite-plugin-static-copy@4.1.1) --- updated-dependencies: - dependency-name: vite-plugin-static-copy dependency-version: 4.1.1 dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pnpm-lock.yaml | 46 +++++++++++++++++++++++++---------- site/dpp-docs/package.json | 2 +- site/dpp-landing/package.json | 2 +- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b248f7..cd0fdfe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,8 +35,8 @@ importers: specifier: ^5 version: 5.9.3 vite-plugin-static-copy: - specifier: ^4.1.0 - version: 4.1.0(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) + specifier: ^4.1.1 + version: 4.1.1(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) site/dpp-landing: dependencies: @@ -66,8 +66,8 @@ importers: specifier: ^5 version: 5.9.3 vite-plugin-static-copy: - specifier: ^4.1.0 - version: 4.1.0(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) + specifier: ^4.1.1 + version: 4.1.1(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) packages: @@ -2430,8 +2430,8 @@ packages: resolution: {integrity: sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==} engines: {node: '>=18'} - p-map@7.0.4: - resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} + p-map@7.0.5: + resolution: {integrity: sha512-e8vJF4XdVkzqqSHguEMz41mQO1wKwxKm5ENrUJQUu9kLDCtn83cxbyHZcszr4QC5zEA7WffRRC4gsTecC7J9oA==} engines: {node: '>=18'} p-queue@8.1.1: @@ -2482,6 +2482,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + postcss-nested@6.2.0: resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} engines: {node: '>=12.0'} @@ -2783,6 +2787,10 @@ packages: resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2800,6 +2808,7 @@ packages: tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -2954,8 +2963,8 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite-plugin-static-copy@4.1.0: - resolution: {integrity: sha512-9XOarNV7LgP0KBB7AApxdgFikLXx3daZdqjC3AevYsL6MrUH62zphonLUs2a6LZc1HN1GY+vQdheZ8VVJb6dQQ==} + vite-plugin-static-copy@4.1.1: + resolution: {integrity: sha512-GrlA8YklrAfSyxJ4M3fdQLOo9oNkp56IM9FYgX/WtEgeIFkPwhu4wzpufBCIuNKCa6Fn77FkRdYxkHqV0FwjAw==} engines: {node: ^22.0.0 || >=24.0.0} peerDependencies: vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -5260,6 +5269,10 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fdir@6.5.0(picomatch@4.0.5): + optionalDependencies: + picomatch: 4.0.5 + fill-range@7.1.1: dependencies: to-regex-range: 5.0.1 @@ -6204,7 +6217,7 @@ snapshots: dependencies: yocto-queue: 1.2.2 - p-map@7.0.4: {} + p-map@7.0.5: {} p-queue@8.1.1: dependencies: @@ -6262,6 +6275,8 @@ snapshots: picomatch@4.0.4: {} + picomatch@4.0.5: {} + postcss-nested@6.2.0(postcss@8.5.15): dependencies: postcss: 8.5.15 @@ -6715,6 +6730,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -6851,12 +6871,12 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-static-copy@4.1.0(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)): + vite-plugin-static-copy@4.1.1(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)): dependencies: chokidar: 3.6.0 - p-map: 7.0.4 + p-map: 7.0.5 picocolors: 1.1.1 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 vite: 6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0): @@ -6866,7 +6886,7 @@ snapshots: picomatch: 4.0.4 postcss: 8.5.15 rollup: 4.60.4 - tinyglobby: 0.2.16 + tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.12.4 fsevents: 2.3.3 diff --git a/site/dpp-docs/package.json b/site/dpp-docs/package.json index 685d367..ebb78d2 100644 --- a/site/dpp-docs/package.json +++ b/site/dpp-docs/package.json @@ -22,6 +22,6 @@ "devDependencies": { "@astrojs/check": "^0.9", "typescript": "^5", - "vite-plugin-static-copy": "^4.1.0" + "vite-plugin-static-copy": "^4.1.1" } } diff --git a/site/dpp-landing/package.json b/site/dpp-landing/package.json index 9a78a5e..da49e33 100644 --- a/site/dpp-landing/package.json +++ b/site/dpp-landing/package.json @@ -22,6 +22,6 @@ "@astrojs/check": "^0.9", "@types/react": "^19.2.16", "typescript": "^5", - "vite-plugin-static-copy": "^4.1.0" + "vite-plugin-static-copy": "^4.1.1" } } From 543e1c36d84eddb2f42d2dc10f922a5cdc920bcc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:06:25 +0000 Subject: [PATCH 002/168] chore(deps): bump @tailwindcss/vite from 4.3.0 to 4.3.3 Bumps [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) from 4.3.0 to 4.3.3. - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/@tailwindcss-vite) --- updated-dependencies: - dependency-name: "@tailwindcss/vite" dependency-version: 4.3.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pnpm-lock.yaml | 188 +++++++++++++++++++++++++------------------------ 1 file changed, 97 insertions(+), 91 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b248f7..3176a54 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,7 +20,7 @@ importers: version: link:../../packages/brand-tokens '@scalar/api-reference': specifier: ^1.62.5 - version: 1.62.5(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.4.3) + version: 1.62.5(tailwindcss@4.3.3)(typescript@5.9.3)(zod@4.4.3) astro: specifier: ^5 version: 5.18.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0) @@ -48,7 +48,7 @@ importers: version: link:../../packages/brand-tokens '@tailwindcss/vite': specifier: ^4 - version: 4.3.0(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) + version: 4.3.3(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0)) astro: specifier: ^5 version: 5.18.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0) @@ -1138,69 +1138,69 @@ packages: '@swc/helpers@0.5.23': resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} - '@tailwindcss/node@4.3.0': - resolution: {integrity: sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==} + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} - '@tailwindcss/oxide-android-arm64@4.3.0': - resolution: {integrity: sha512-TJPiq67tKlLuObP6RkwvVGDoxCMBVtDgKkLfa/uyj7/FyxvQwHS+UOnVrXXgbEsfUaMgiVvC4KbJnRr26ho4Ng==} + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} engines: {node: '>= 20'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.3.0': - resolution: {integrity: sha512-oMN/WZRb+SO37BmUElEgeEWuU8E/HXRkiODxJxLe1UTHVXLrdVSgfaJV7pSlhRGMSOiXLuxTIjfsF3wYvz8cgQ==} + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} engines: {node: '>= 20'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.3.0': - resolution: {integrity: sha512-N6CUmu4a6bKVADfw77p+iw6Yd9Q3OBhe0veaDX+QazfuVYlQsHfDgxBrsjQ/IW+zywL8mTrNd0SdJT/zgtvMdA==} + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} engines: {node: '>= 20'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.3.0': - resolution: {integrity: sha512-zDL5hBkQdH5C6MpqbK3gQAgP80tsMwSI26vjOzjJtNCMUo0lFgOItzHKBIupOZNQxt3ouPH7RPhvNhiTfCe5CQ==} + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} engines: {node: '>= 20'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': - resolution: {integrity: sha512-R06HdNi7A7OEoMsf6d4tjZ71RCWnZQPHj2mnotSFURjNLdBC+cIgXQ7l81CqeoiQftjf6OOblxXMInMgN2VzMA==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} engines: {node: '>= 20'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': - resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-arm64-musl@4.3.0': - resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] libc: [musl] - '@tailwindcss/oxide-linux-x64-gnu@4.3.0': - resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [glibc] - '@tailwindcss/oxide-linux-x64-musl@4.3.0': - resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} engines: {node: '>= 20'} cpu: [x64] os: [linux] libc: [musl] - '@tailwindcss/oxide-wasm32-wasi@4.3.0': - resolution: {integrity: sha512-HNZGOUxEmElksYR7S6sC5jTeNGpobAsy9u7Gu0AskJ8/20FR9GqebUyB+HBcU/ax6BHuiuJi+Oda4B+YX6H1yA==} + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -1211,24 +1211,24 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': - resolution: {integrity: sha512-Pe+RPVTi1T+qymuuRpcdvwSVZjnll/f7n8gBxMMh3xLTctMDKqpdfGimbMyioqtLhUYZxdJ9wGNhV7MKHvgZsQ==} + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} engines: {node: '>= 20'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.3.0': - resolution: {integrity: sha512-Mvrf2kXW/yeW/OTezZlCGOirXRcUuLIBx/5Y12BaPM7wJoryG6dfS/NJL8aBPqtTEx/Vm4T4vKzFUcKDT+TKUA==} + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} engines: {node: '>= 20'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.3.0': - resolution: {integrity: sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==} + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} engines: {node: '>= 20'} - '@tailwindcss/vite@4.3.0': - resolution: {integrity: sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==} + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 || ^8 @@ -1739,8 +1739,8 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - enhanced-resolve@5.22.1: - resolution: {integrity: sha512-6QEuw3zoX1SJQc7b87aBXke/no+mG2bTBgw29gWMQonLmpEkWoCAVkl+M49e48AZlWzxiDzDZzYdp6kobcyLww==} + enhanced-resolve@5.24.2: + resolution: {integrity: sha512-rpsZEGT1jFuve6QlpyRp9ckQ+kN61hvF9BzCPyMdaKTm8UJce96KBn3sorXOFXlzjPrs3Vc4T1NsSroZ3PxlFw==} engines: {node: '>=10.13.0'} entities@4.5.0: @@ -2764,6 +2764,9 @@ packages: tailwindcss@4.3.0: resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} @@ -2800,6 +2803,7 @@ packages: tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -3736,9 +3740,9 @@ snapshots: - '@vue/composition-api' - vue - '@headlessui/tailwindcss@0.2.2(tailwindcss@4.3.0)': + '@headlessui/tailwindcss@0.2.2(tailwindcss@4.3.3)': dependencies: - tailwindcss: 4.3.0 + tailwindcss: 4.3.3 '@headlessui/vue@1.7.23(vue@3.5.39(typescript@5.9.3))': dependencies: @@ -4065,11 +4069,11 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true - '@scalar/agent-chat@0.12.18(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.4.3)': + '@scalar/agent-chat@0.12.18(tailwindcss@4.3.3)(typescript@5.9.3)(zod@4.4.3)': dependencies: '@ai-sdk/vue': 3.0.33(vue@3.5.39(typescript@5.9.3))(zod@4.4.3) - '@scalar/api-client': 3.13.3(tailwindcss@4.3.0)(typescript@5.9.3) - '@scalar/components': 0.27.6(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/api-client': 3.13.3(tailwindcss@4.3.3)(typescript@5.9.3) + '@scalar/components': 0.27.6(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@5.9.3) '@scalar/json-magic': 0.12.17 @@ -4103,17 +4107,17 @@ snapshots: - universal-cookie - zod - '@scalar/api-client@3.13.3(tailwindcss@4.3.0)(typescript@5.9.3)': + '@scalar/api-client@3.13.3(tailwindcss@4.3.3)(typescript@5.9.3)': dependencies: - '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.0) + '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.3) '@headlessui/vue': 1.7.23(vue@3.5.39(typescript@5.9.3)) - '@scalar/blocks': 0.1.4(tailwindcss@4.3.0)(typescript@5.9.3) - '@scalar/components': 0.27.6(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/blocks': 0.1.4(tailwindcss@4.3.3)(typescript@5.9.3) + '@scalar/components': 0.27.6(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@5.9.3) '@scalar/oas-utils': 0.19.4(typescript@5.9.3) '@scalar/openapi-types': 0.9.1 - '@scalar/sidebar': 0.9.29(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/sidebar': 0.9.29(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/snippetz': 0.9.21 '@scalar/themes': 0.16.2 '@scalar/typebox': 0.1.3 @@ -4151,19 +4155,19 @@ snapshots: - typescript - universal-cookie - '@scalar/api-reference@1.62.5(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.4.3)': + '@scalar/api-reference@1.62.5(tailwindcss@4.3.3)(typescript@5.9.3)(zod@4.4.3)': dependencies: '@headlessui/vue': 1.7.23(vue@3.5.39(typescript@5.9.3)) - '@scalar/agent-chat': 0.12.18(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.4.3) - '@scalar/api-client': 3.13.3(tailwindcss@4.3.0)(typescript@5.9.3) - '@scalar/blocks': 0.1.4(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/agent-chat': 0.12.18(tailwindcss@4.3.3)(typescript@5.9.3)(zod@4.4.3) + '@scalar/api-client': 3.13.3(tailwindcss@4.3.3)(typescript@5.9.3) + '@scalar/blocks': 0.1.4(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/code-highlight': 0.4.1 - '@scalar/components': 0.27.6(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/components': 0.27.6(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@5.9.3) '@scalar/oas-utils': 0.19.4(typescript@5.9.3) '@scalar/schemas': 0.7.2 - '@scalar/sidebar': 0.9.29(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/sidebar': 0.9.29(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/snippetz': 0.9.21 '@scalar/themes': 0.16.2 '@scalar/types': 0.16.2 @@ -4199,9 +4203,9 @@ snapshots: dependencies: '@scalar/helpers': 0.9.0 - '@scalar/blocks@0.1.4(tailwindcss@4.3.0)(typescript@5.9.3)': + '@scalar/blocks@0.1.4(tailwindcss@4.3.3)(typescript@5.9.3)': dependencies: - '@scalar/components': 0.27.6(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/components': 0.27.6(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@5.9.3) '@scalar/snippetz': 0.9.21 @@ -4237,11 +4241,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@scalar/components@0.27.6(tailwindcss@4.3.0)(typescript@5.9.3)': + '@scalar/components@0.27.6(tailwindcss@4.3.3)(typescript@5.9.3)': dependencies: '@floating-ui/utils': 0.2.10 '@floating-ui/vue': 1.1.9(vue@3.5.39(typescript@5.9.3)) - '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.0) + '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.3) '@headlessui/vue': 1.7.23(vue@3.5.39(typescript@5.9.3)) '@scalar/code-highlight': 0.4.1 '@scalar/helpers': 0.9.0 @@ -4299,9 +4303,9 @@ snapshots: '@scalar/helpers': 0.9.0 '@scalar/validation': 0.6.0 - '@scalar/sidebar@0.9.29(tailwindcss@4.3.0)(typescript@5.9.3)': + '@scalar/sidebar@0.9.29(tailwindcss@4.3.3)(typescript@5.9.3)': dependencies: - '@scalar/components': 0.27.6(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/components': 0.27.6(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@5.9.3) '@scalar/themes': 0.16.2 @@ -4464,72 +4468,72 @@ snapshots: dependencies: tslib: 2.8.1 - '@tailwindcss/node@4.3.0': + '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.22.1 + enhanced-resolve: 5.24.2 jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.3.0 + tailwindcss: 4.3.3 - '@tailwindcss/oxide-android-arm64@4.3.0': + '@tailwindcss/oxide-android-arm64@4.3.3': optional: true - '@tailwindcss/oxide-darwin-arm64@4.3.0': + '@tailwindcss/oxide-darwin-arm64@4.3.3': optional: true - '@tailwindcss/oxide-darwin-x64@4.3.0': + '@tailwindcss/oxide-darwin-x64@4.3.3': optional: true - '@tailwindcss/oxide-freebsd-x64@4.3.0': + '@tailwindcss/oxide-freebsd-x64@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.0': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.3.0': + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.3.0': + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.3.0': + '@tailwindcss/oxide-linux-x64-musl@4.3.3': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.3.0': + '@tailwindcss/oxide-wasm32-wasi@4.3.3': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.3.0': + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.3.0': + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': optional: true - '@tailwindcss/oxide@4.3.0': + '@tailwindcss/oxide@4.3.3': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.3.0 - '@tailwindcss/oxide-darwin-arm64': 4.3.0 - '@tailwindcss/oxide-darwin-x64': 4.3.0 - '@tailwindcss/oxide-freebsd-x64': 4.3.0 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.0 - '@tailwindcss/oxide-linux-arm64-gnu': 4.3.0 - '@tailwindcss/oxide-linux-arm64-musl': 4.3.0 - '@tailwindcss/oxide-linux-x64-gnu': 4.3.0 - '@tailwindcss/oxide-linux-x64-musl': 4.3.0 - '@tailwindcss/oxide-wasm32-wasi': 4.3.0 - '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 - '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 - - '@tailwindcss/vite@4.3.0(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))': - dependencies: - '@tailwindcss/node': 4.3.0 - '@tailwindcss/oxide': 4.3.0 - tailwindcss: 4.3.0 + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 vite: 6.4.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0) '@tanstack/virtual-core@3.17.3': {} @@ -5115,7 +5119,7 @@ snapshots: emoji-regex@8.0.0: {} - enhanced-resolve@5.22.1: + enhanced-resolve@5.24.2: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -6700,6 +6704,8 @@ snapshots: tailwindcss@4.3.0: {} + tailwindcss@4.3.3: {} + tapable@2.3.3: {} time-span@5.1.0: From abf3d3a485d46b4ae5845eb077c897c06ef6d73e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 17 Jul 2026 08:06:37 +0000 Subject: [PATCH 003/168] chore(deps): bump tailwindcss from 4.3.0 to 4.3.3 Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 4.3.0 to 4.3.3. - [Release notes](https://github.com/tailwindlabs/tailwindcss/releases) - [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md) - [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/tailwindcss) --- updated-dependencies: - dependency-name: tailwindcss dependency-version: 4.3.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- pnpm-lock.yaml | 54 ++++++++++++++++++++++++++++---------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b248f7..5171300 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,7 +20,7 @@ importers: version: link:../../packages/brand-tokens '@scalar/api-reference': specifier: ^1.62.5 - version: 1.62.5(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.4.3) + version: 1.62.5(tailwindcss@4.3.3)(typescript@5.9.3)(zod@4.4.3) astro: specifier: ^5 version: 5.18.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0) @@ -54,7 +54,7 @@ importers: version: 5.18.2(@types/node@24.12.4)(jiti@2.7.0)(lightningcss@1.32.0)(rollup@4.60.4)(typescript@5.9.3)(yaml@2.9.0) tailwindcss: specifier: ^4 - version: 4.3.0 + version: 4.3.3 devDependencies: '@astrojs/check': specifier: ^0.9 @@ -2764,6 +2764,9 @@ packages: tailwindcss@4.3.0: resolution: {integrity: sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==} + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} @@ -2800,6 +2803,7 @@ packages: tsconfck@3.1.6: resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} engines: {node: ^18 || >=20} + deprecated: unmaintained hasBin: true peerDependencies: typescript: ^5.0.0 @@ -3736,9 +3740,9 @@ snapshots: - '@vue/composition-api' - vue - '@headlessui/tailwindcss@0.2.2(tailwindcss@4.3.0)': + '@headlessui/tailwindcss@0.2.2(tailwindcss@4.3.3)': dependencies: - tailwindcss: 4.3.0 + tailwindcss: 4.3.3 '@headlessui/vue@1.7.23(vue@3.5.39(typescript@5.9.3))': dependencies: @@ -4065,11 +4069,11 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.4': optional: true - '@scalar/agent-chat@0.12.18(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.4.3)': + '@scalar/agent-chat@0.12.18(tailwindcss@4.3.3)(typescript@5.9.3)(zod@4.4.3)': dependencies: '@ai-sdk/vue': 3.0.33(vue@3.5.39(typescript@5.9.3))(zod@4.4.3) - '@scalar/api-client': 3.13.3(tailwindcss@4.3.0)(typescript@5.9.3) - '@scalar/components': 0.27.6(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/api-client': 3.13.3(tailwindcss@4.3.3)(typescript@5.9.3) + '@scalar/components': 0.27.6(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@5.9.3) '@scalar/json-magic': 0.12.17 @@ -4103,17 +4107,17 @@ snapshots: - universal-cookie - zod - '@scalar/api-client@3.13.3(tailwindcss@4.3.0)(typescript@5.9.3)': + '@scalar/api-client@3.13.3(tailwindcss@4.3.3)(typescript@5.9.3)': dependencies: - '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.0) + '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.3) '@headlessui/vue': 1.7.23(vue@3.5.39(typescript@5.9.3)) - '@scalar/blocks': 0.1.4(tailwindcss@4.3.0)(typescript@5.9.3) - '@scalar/components': 0.27.6(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/blocks': 0.1.4(tailwindcss@4.3.3)(typescript@5.9.3) + '@scalar/components': 0.27.6(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@5.9.3) '@scalar/oas-utils': 0.19.4(typescript@5.9.3) '@scalar/openapi-types': 0.9.1 - '@scalar/sidebar': 0.9.29(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/sidebar': 0.9.29(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/snippetz': 0.9.21 '@scalar/themes': 0.16.2 '@scalar/typebox': 0.1.3 @@ -4151,19 +4155,19 @@ snapshots: - typescript - universal-cookie - '@scalar/api-reference@1.62.5(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.4.3)': + '@scalar/api-reference@1.62.5(tailwindcss@4.3.3)(typescript@5.9.3)(zod@4.4.3)': dependencies: '@headlessui/vue': 1.7.23(vue@3.5.39(typescript@5.9.3)) - '@scalar/agent-chat': 0.12.18(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.4.3) - '@scalar/api-client': 3.13.3(tailwindcss@4.3.0)(typescript@5.9.3) - '@scalar/blocks': 0.1.4(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/agent-chat': 0.12.18(tailwindcss@4.3.3)(typescript@5.9.3)(zod@4.4.3) + '@scalar/api-client': 3.13.3(tailwindcss@4.3.3)(typescript@5.9.3) + '@scalar/blocks': 0.1.4(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/code-highlight': 0.4.1 - '@scalar/components': 0.27.6(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/components': 0.27.6(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@5.9.3) '@scalar/oas-utils': 0.19.4(typescript@5.9.3) '@scalar/schemas': 0.7.2 - '@scalar/sidebar': 0.9.29(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/sidebar': 0.9.29(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/snippetz': 0.9.21 '@scalar/themes': 0.16.2 '@scalar/types': 0.16.2 @@ -4199,9 +4203,9 @@ snapshots: dependencies: '@scalar/helpers': 0.9.0 - '@scalar/blocks@0.1.4(tailwindcss@4.3.0)(typescript@5.9.3)': + '@scalar/blocks@0.1.4(tailwindcss@4.3.3)(typescript@5.9.3)': dependencies: - '@scalar/components': 0.27.6(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/components': 0.27.6(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@5.9.3) '@scalar/snippetz': 0.9.21 @@ -4237,11 +4241,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@scalar/components@0.27.6(tailwindcss@4.3.0)(typescript@5.9.3)': + '@scalar/components@0.27.6(tailwindcss@4.3.3)(typescript@5.9.3)': dependencies: '@floating-ui/utils': 0.2.10 '@floating-ui/vue': 1.1.9(vue@3.5.39(typescript@5.9.3)) - '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.0) + '@headlessui/tailwindcss': 0.2.2(tailwindcss@4.3.3) '@headlessui/vue': 1.7.23(vue@3.5.39(typescript@5.9.3)) '@scalar/code-highlight': 0.4.1 '@scalar/helpers': 0.9.0 @@ -4299,9 +4303,9 @@ snapshots: '@scalar/helpers': 0.9.0 '@scalar/validation': 0.6.0 - '@scalar/sidebar@0.9.29(tailwindcss@4.3.0)(typescript@5.9.3)': + '@scalar/sidebar@0.9.29(tailwindcss@4.3.3)(typescript@5.9.3)': dependencies: - '@scalar/components': 0.27.6(tailwindcss@4.3.0)(typescript@5.9.3) + '@scalar/components': 0.27.6(tailwindcss@4.3.3)(typescript@5.9.3) '@scalar/helpers': 0.9.0 '@scalar/icons': 0.7.3(typescript@5.9.3) '@scalar/themes': 0.16.2 @@ -6700,6 +6704,8 @@ snapshots: tailwindcss@4.3.0: {} + tailwindcss@4.3.3: {} + tapable@2.3.3: {} time-span@5.1.0: From 9258cb273dfb7d710d0c7f25a6c41237d0bb9454 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Tue, 28 Jul 2026 17:40:30 +0200 Subject: [PATCH 004/168] docs: correct the access-control model against the primary text ESPR Article 10 is Requirements for the digital product passport and defines no tiers; access is Art. 11(b), free of charge, with the actor-to-data mapping delegated per product group under Art. 9(2)(f) and no such act adopted yet, so the public Public/Restricted/Private tier page was wrong at its premise and is replaced by the Battery Art. 77(2) lattice plus the constraints common to every regime. --- .../src/content/docs/core-concepts.mdx | 2 +- .../src/content/docs/engine/architecture.mdx | 2 +- .../what-odal-can-and-cannot-see.mdx | 2 +- .../docs/regulatory/access-control.mdx | 60 ++++++++++++++----- .../content/docs/regulatory/electronics.mdx | 2 +- .../src/content/docs/regulatory/espr.mdx | 4 +- .../src/pages/{_trust.astro => trust.astro} | 0 7 files changed, 52 insertions(+), 20 deletions(-) rename site/dpp-landing/src/pages/{_trust.astro => trust.astro} (100%) diff --git a/site/dpp-docs/src/content/docs/core-concepts.mdx b/site/dpp-docs/src/content/docs/core-concepts.mdx index 8cb765c..35409ea 100644 --- a/site/dpp-docs/src/content/docs/core-concepts.mdx +++ b/site/dpp-docs/src/content/docs/core-concepts.mdx @@ -9,7 +9,7 @@ Three ideas explain almost everything about how Odal Node is built and why. Unde ## Proof-bound: the data stays yours -The most important decision in the project is that the operator's raw source files never enter Odal's systems. The manufacturer validates a passport locally, signs it with their own key, and the node discards the import files it was built from. What the node keeps and serves is the signed passport itself — the product data the operator chose to publish, every field bound to a proof and gated by access tier — not the raw exports, spreadsheets, or supply-chain detail behind it. +The most important decision in the project is that the operator's raw source files never enter Odal's systems. The manufacturer validates a passport locally, signs it with their own key, and the node discards the import files it was built from. What the node keeps and serves is the signed passport itself — the product data the operator chose to publish, every field bound to a proof and filtered by the reader's access rights — not the raw exports, spreadsheets, or supply-chain detail behind it. This is a property of the software, not a promise about our conduct. Because the node discards the source files after signing, the raw inputs behind a passport are gone — there is nothing for an operator, or for us, to hand over later. Verification needs only the signed passport and the manufacturer's public identity; it does not need Odal to be online, or to exist. diff --git a/site/dpp-docs/src/content/docs/engine/architecture.mdx b/site/dpp-docs/src/content/docs/engine/architecture.mdx index 8cadd13..46346d6 100644 --- a/site/dpp-docs/src/content/docs/engine/architecture.mdx +++ b/site/dpp-docs/src/content/docs/engine/architecture.mdx @@ -20,7 +20,7 @@ Inside the node, three surfaces handle those steps: - The **write path** — where a passport is created, validated, signed, and versioned. - **Bulk import** — for bringing many products in at once, each becoming a draft that flows into the write path. -- The **public read path** — where a published passport is served and verified, and where the [access tiers](/regulatory/access-control) are enforced on every request. +- The **public read path** — where a published passport is served and verified, and where [access rights](/regulatory/access-control) are enforced on every request. Signing happens inside the node itself — the operator's key never leaves their infrastructure. What the node keeps and what it discards is covered in [Permanence & retention](/engine/retention) and [Operating a node securely](/engine/security). diff --git a/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx b/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx index 1eca517..798ec93 100644 --- a/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx +++ b/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx @@ -3,7 +3,7 @@ title: What Odal can and cannot see description: A precise statement of data access by deployment model — the proof-bound architecture, stated as verifiable facts. --- -The proof-bound architecture means the raw import files are read once on the operator's infrastructure — validated, used to sign the passport, then discarded. The signed passport itself, carrying the full product data across its access tiers, is what is stored and served. This page states precisely what Odal can see, cannot see, and could see but does not — by deployment model. +The proof-bound architecture means the raw import files are read once on the operator's infrastructure — validated, used to sign the passport, then discarded. The signed passport itself, carrying the full product data across its disclosure classes, is what is stored and served. This page states precisely what Odal can see, cannot see, and could see but does not — by deployment model. ## By deployment diff --git a/site/dpp-docs/src/content/docs/regulatory/access-control.mdx b/site/dpp-docs/src/content/docs/regulatory/access-control.mdx index 586ec7f..9c26dde 100644 --- a/site/dpp-docs/src/content/docs/regulatory/access-control.mdx +++ b/site/dpp-docs/src/content/docs/regulatory/access-control.mdx @@ -1,32 +1,64 @@ --- -title: Access Control (Art. 10) -description: ESPR Article 10's three-tier access model — public, restricted, and private information — and how Odal enforces the boundaries. +title: Access Control +description: Who may read which parts of a Digital Product Passport — what ESPR actually says, what the Battery Regulation adds, and how Odal enforces it. --- -ESPR Article 10 establishes that a Digital Product Passport carries three categories of information with different access rules. The categories are not advisory — they are part of the regulation's substantive requirements, and an implementation that does not enforce them is not compliant. +Access to a Digital Product Passport is differentiated: not every reader sees every field. But the rules are not where they are commonly assumed to be, and getting that wrong produces confident, incorrect compliance claims. -## The three tiers +## What ESPR actually says -**Public information** is available to anyone who scans the QR code. It includes the product identity, the manufacturer, the basic compliance summary, and the information needed for a consumer to make informed sustainability decisions. +ESPR (Regulation (EU) 2024/1781) **does not itself define access tiers.** Article 11(b) requires that -**Restricted information** is available to authorised actors — recyclers, repair operators, dismantlers, customs and market-surveillance authorities. It includes the detailed material composition needed for end-of-life processing, the technical specifications needed for repair, and the compliance evidence needed for authority verification. +> customers, manufacturers, importers, distributors, dealers, professional repairers, independent operators, refurbishers, remanufacturers, recyclers, market surveillance authorities and customs authorities, civil society organisations, trade unions and other relevant actors shall have **free of charge** and easy access to the digital product passport **based on their respective access rights set out in the applicable delegated act adopted pursuant to Article 4** -**Private information** is available only to the economic operator. It includes anything that the operator legitimately considers commercial confidential — supplier identities, formulation details, manufacturing recipes — that does not need to be disclosed for the public-good purposes Article 10 protects. +Two things follow. First, ESPR names a broad **list of actors** — some fourteen classes — and assigns them nothing. Second, the actor-to-data mapping is delegated: Article 9(2)(f) requires each product-group delegated act to specify "the actors that are to have access to data in the digital product passport and to what data they are to have access". + +**No such delegated act has been adopted for any ESPR product group yet.** For textiles, furniture, steel, aluminium and tyres, the access mapping is not merely unimplemented — it does not yet legally exist. + +Article 10, sometimes cited as the source of a three-tier model, is titled "Requirements for the digital product passport" and establishes no access categories. + +## Where a specified model does exist: batteries + +The Battery Regulation (EU) 2023/1542, Article 77(2), is currently the only fully specified access model. It assigns three audiences to four Annex XIII data sets: + +| Audience | Annex XIII points | +|---|---| +| General public | 1 | +| Notified bodies, market surveillance authorities, the Commission | 2 and 3 | +| Persons with a legitimate interest | 2 and 4 | + +**This is a lattice, not a ranking.** Point 3 (conformity test reports) is authority-only; point 4 (individual-battery data — cycle counts, state of health, use history) is legitimate-interest-only. Neither audience contains the other, so no ordered "public → restricted → private" scale can express it: any such ordering necessarily either hands authorities data the regulation withholds, or hides data from someone entitled to it. + +Odal models this directly — audiences and disclosure classes as separate vocabularies, with an explicit table of which audience may see which class — rather than as a tier number. + +One detail is still pending: the delegated act under Article 77(9), which fixes the access rights for Annex XIII points 2 and 4, has not been adopted. + +## Constraints that apply everywhere + +Read from the primary texts of ESPR, the Battery Regulation, the Toy Safety Regulation (EU) 2025/2509, the Detergents Regulation (EU) 2026/405 and the Construction Products Regulation (EU) 2024/3110: + +- **Access is free of charge.** Every one of these instruments requires it. Charging a reader for passport access is not a lawful model. +- **Consumers must not be required to register or supply a password.** The toy and detergent regulations state this outright. The public view stays frictionless — no account, no sign-up, no gate. +- **Passports must remain available for years, surviving the operator.** Ten years after placing on the market under the toy, detergent and construction rules, "including in cases of insolvency, liquidation or cessation of activity"; ESPR ties the period to at least the product's expected lifetime. ## How Odal enforces the boundaries -Every field in a passport carries a tier marker, and every access request arrives with a credential — a Verifiable Credential issued by a trusted authority (a national body, a sector association) that asserts which actor category the requester belongs to. Odal evaluates the request against the passport's access policy: does the credential authorise the requested tier, is it signed by a trusted issuer, has it expired. +Every field carries a disclosure classification drawn from the sector's own definition rather than hard-coded. A request arrives with a credential — a W3C Verifiable Credential asserting the holder's role — and the node resolves that role to an audience, then filters the passport to the disclosure classes that audience may see. + +The filtering step is a **pure function**: no network, no database. The credential check verifies the signature, the expiry, the issuer's trust status and the revocation list. + +Durable artefacts — stored signatures, audit records — are keyed by the **disclosure classes** they cover, never by an audience name. That is deliberate: ESPR's eventual actor vocabulary differs from the Battery Regulation's, and anything keyed to today's audience names would need migrating when the first ESPR delegated act lands. -That evaluation is a **pure function** — no network, no database — so it runs inside the resolver at the edge, deciding each request in microseconds without reaching back into the node. The answer is simply allow or deny. +## Why credentials and not API keys -## Why credential-based access and not API keys +The natural alternative — issuing API keys to recyclers and authorities — fails on three counts. API keys are bearer secrets that get reused, leaked or sold. They carry no verifiable identity assertion: possession is proof of access, not proof of who holds it. And they bind to a single issuer's authentication system, so every regulator would have to integrate with every platform separately. -The natural alternative — handing out API keys to recyclers and authorities — fails on three properties. API keys are bearer secrets that get reused, leaked, or sold. API keys do not carry a verifiable identity assertion — possession is proof of access, but not proof of who the holder is. API keys are bound to a single issuer's authentication system, which means every regulator has to integrate with every platform separately. +Verifiable Credentials address all three. They are non-bearer, they carry a signed issuer assertion, and any platform that can verify them can accept them. -Verifiable Credentials solve all three. They are non-bearer (the holder proves possession of the credential's bound private key), they carry a verifiable identity assertion (the issuer is signed into the credential), and they integrate uniformly with any platform that knows how to verify them. +This is also where the regulation is heading. ESPR Article 11 empowers the Commission to adopt implementing acts on procedures to issue and verify "the digital credentials of economic operators and other relevant actors that have access rights", and the toy and detergent regulations both defer their credential procedures to that same provision. Those implementing acts are not yet adopted, so no conformance claim is available — but the direction is legislated rather than speculative. ## Read next [What the core does](/core/overview) — how passports are signed and verified. -[ESPR Overview](/regulatory/espr) — the framework regulation Article 10 sits inside. -[How the node works](/engine/architecture) — the public read path that enforces these access boundaries on every request. +[ESPR Overview](/regulatory/espr) — the framework regulation these provisions sit inside. +[How the node works](/engine/architecture) — the public read path. diff --git a/site/dpp-docs/src/content/docs/regulatory/electronics.mdx b/site/dpp-docs/src/content/docs/regulatory/electronics.mdx index 397fd55..f9564b7 100644 --- a/site/dpp-docs/src/content/docs/regulatory/electronics.mdx +++ b/site/dpp-docs/src/content/docs/regulatory/electronics.mdx @@ -53,4 +53,4 @@ Until those fields are pinned to the adopted act, an electronics passport is val [Battery DPP](/regulatory/battery) — the battery-sector delegated act, the nearest hard mandate. [Textile DPP](/regulatory/textile) — the textile-sector delegated act and the unsold-goods provision. [ESPR Overview](/regulatory/espr) — the framework regulation. -[Access Control (Art. 10)](/regulatory/access-control) — the three-tier access model that applies across all sectors. +[Access Control](/regulatory/access-control) — who may read what. Note the only fully specified model is the Battery Regulation's; ESPR delegates its own per product group. diff --git a/site/dpp-docs/src/content/docs/regulatory/espr.mdx b/site/dpp-docs/src/content/docs/regulatory/espr.mdx index 6f24e2c..edd5728 100644 --- a/site/dpp-docs/src/content/docs/regulatory/espr.mdx +++ b/site/dpp-docs/src/content/docs/regulatory/espr.mdx @@ -23,7 +23,7 @@ Odal covers the articles of ESPR that are technically substantive for a passport **Articles 8 and 9** — the format of the passport and its data carrier: the passport itself is the format, and GS1 Digital Link is how a scan resolves to it. -**Article 10** — the three-tier access control. Odal carries the tier boundaries and the resolver enforces them on every request. +**Articles 10 and 11** — the passport's requirements and its technical design. Art. 11(b) is the access provision: readers get access "based on their respective access rights set out in the applicable delegated act", **free of charge**, with the actor-to-data mapping delegated to each product group under Art. 9(2)(f). ESPR fixes no access tiers of its own, and no product-group act has been adopted yet. **Transfer of responsibility** — when a product changes economic operator along the supply chain, Odal implements a dual-signature chain: both transferor and transferee sign, creating a verifiable chain of custody. One honest note: ESPR has **no single article** establishing transfer mechanics — the closest operative text is Art. 11(e) (passport continuity when an operator ceases activity). Our handshake is an engineering choice that satisfies and exceeds that continuity duty; we say so rather than inventing a citation. @@ -61,5 +61,5 @@ As each delegated act takes effect, the matching sector's determination switches [Battery DPP](/regulatory/battery) — the battery-sector delegated act in detail. [Textile DPP](/regulatory/textile) — the textile-sector delegated act in detail. -[Access Control (Art. 10)](/regulatory/access-control) — the three-tier model in detail. +[Access Control](/regulatory/access-control) — who may read what, and where the rules actually live. [EU Central Registry](/regulatory/central-registry) — what the central registry is and what it is not. diff --git a/site/dpp-landing/src/pages/_trust.astro b/site/dpp-landing/src/pages/trust.astro similarity index 100% rename from site/dpp-landing/src/pages/_trust.astro rename to site/dpp-landing/src/pages/trust.astro From 44bd73f5cc1a725f4d1b3c87c200b3a7bd5552b1 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Thu, 20 Aug 2026 21:05:37 +0200 Subject: [PATCH 005/168] docs(landing): disclose scan telemetry on the trust page as an aggregate per-passport-per-day count with no scanner-identifying field --- site/dpp-landing/src/pages/trust.astro | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/site/dpp-landing/src/pages/trust.astro b/site/dpp-landing/src/pages/trust.astro index eb738e0..e588266 100644 --- a/site/dpp-landing/src/pages/trust.astro +++ b/site/dpp-landing/src/pages/trust.astro @@ -18,6 +18,18 @@ const description =

+
+

+ When a passport is resolved, the node can record that it happened — an + aggregate count, per passport, per day. That is the whole of it. A scan + count records nothing about the person who scanned: no IP + address, no device, no location, no identity, no session. The software has + no field in which to store any of it, so there is nothing to leak and + nothing that could quietly become tracking. Producing a QR-code image is + counted separately — it measures label production, not people. +

+
+ + + + + + + + + + + + + + + + + + + + ODAL NODE + Signed by you. Verified by anyone. + diff --git a/deprecated/odal-mark-navy.svg b/deprecated/odal-mark-navy.svg new file mode 100644 index 0000000..f430ad8 --- /dev/null +++ b/deprecated/odal-mark-navy.svg @@ -0,0 +1,63 @@ + + Odal Node — open door beneath a constellation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/deprecated/odal-mark.svg b/deprecated/odal-mark.svg new file mode 100644 index 0000000..02b61a2 --- /dev/null +++ b/deprecated/odal-mark.svg @@ -0,0 +1,63 @@ + + Odal Node — open door beneath a constellation + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/deprecated/og-source.svg b/deprecated/og-source.svg new file mode 100644 index 0000000..667fbfb --- /dev/null +++ b/deprecated/og-source.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + +ODAL NODE +Signed by you. Verified by anyone. +EU Digital Product Passport infrastructure + \ No newline at end of file diff --git a/deprecated/og.png b/deprecated/og.png new file mode 100644 index 0000000000000000000000000000000000000000..818c9424e3260d2794985b3c0aef453573e62d65 GIT binary patch literal 103677 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 literal 0 HcmV?d00001 diff --git a/deprecated/source/odal-node-circle-background.png b/deprecated/source/odal-node-circle-background.png new file mode 100644 index 0000000000000000000000000000000000000000..4c3131887266102c4bfd9b3e5f02aca00b3a5b78 GIT binary patch literal 25376 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 P00000NkvXXu0mjfldMx2 literal 0 HcmV?d00001 diff --git a/deprecated/source/odal-node-fit-to-edges.png b/deprecated/source/odal-node-fit-to-edges.png new file mode 100644 index 0000000000000000000000000000000000000000..37c4fb7d99e588d4527aec5a65a3e829918e2950 GIT binary patch literal 23699 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 literal 0 HcmV?d00001 diff --git a/deprecated/source/odal-node-no-background.png b/deprecated/source/odal-node-no-background.png new file mode 100644 index 0000000000000000000000000000000000000000..f42d76f1150a1a168a60407da3399bcc57cdefaf GIT binary patch literal 10845 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 rSgUrCX!1W~yodgp8=TJdLRk}vfEaZOy{xuFe literal 0 HcmV?d00001 diff --git a/deprecated/source/odal-node-no-text.png b/deprecated/source/odal-node-no-text.png new file mode 100644 index 0000000000000000000000000000000000000000..3993eaa1630c641c8951051afc5d20cb52e99f05 GIT binary patch literal 11668 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∾7^ipgnEx+P0`sG%jeFYW5{RvtU!EW@7V~Y&s z);y92k^ZIgdOW_w(IfcMmPAlYA)~l!v?F!>2lBp{q7atafG=V1<3G4ieN(4co>*~x=K^G$dx_!g3)1F#SP|4lcEyT_-`=KWX zGVW=)r-Se}zQ0^@7%bnEo|}Mh{)-0@B)3mg{-|ioQE@My*l<|K73;!Fb^ySZjQtX zrJXZ`J}C}5gGA*3=Y^61)T`yzJJ?rmynYr5hTgyE@{HJo0+d|3a|q$rc@NaWuju9I zf!3!^{yx7%vBf4(p~99^<67f^3>%RstDHlXAl`1DQLHp~Pkl2sqS6zgx;zpFx4ycPWL5zqZx-D`VTCzgP{Omr5g0YOFo;+*%V>Ku+7n0R z<8w9xfx2B6>Mjy*Pw%v%r4eNs_Cl9}07QCjN8;*Dn4v2u71v8T61m-0gfXl8w9B_Dik#Id9fa?Mym$UHQ|;ymn-uFh=)JNX}NkqS;?BT!n<2PGV76v$p}6ZL`;N3HCbF>MF1OuH>zecmdeA z0M&9+Xv%@I0bXV!wndCW+#Sf7@$Z&y3u!B;O>96Kg`x7bvy@q}$NP`o=)kA2g4wB} zbFrddIGHo!%ob|EJ|wK6z&gcpjSG+++s7)kfYOEr(T?0Gb&B)>qz+{ogNssn;%{4a z$R2-u77S>-LC(BdzP#1f^nHQ;gcyHv&|a%OJ0lqw!`%|MHIv)txxo6m2%Zd2B|A4B zE(;W-p>J+zy&~<`on=_~?2i!n378d*Uagg*n+!aK5y?~ITwe#=Lg9HVJDti*2y5XO zOf7lUJr7FJ_nmVor}($hbwvMXW$OPs{_hAFV*M2}9eJ(0%WgdUZ$?2zMY>ATH0XZ- Di8cJJ literal 0 HcmV?d00001 diff --git a/deprecated/source/odal-node.png b/deprecated/source/odal-node.png new file mode 100644 index 0000000000000000000000000000000000000000..2c38fbbd5a64993fa39fa890607b1be5d44816d8 GIT binary patch literal 5750 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 { + for (const entry of readdirSync(dir)) { + if (SKIP_DIRS.has(entry)) continue; + const full = join(dir, entry); + if (statSync(full).isDirectory()) walk(full, out); + else if (!BINARY.has(extname(full)) && !SKIP_FILES.has(basename(full))) out.push(full); + } + return out; +}; + +const hits = []; +const files = walk(process.cwd()); + +for (const file of files) { + let text; + try { + text = readFileSync(file, 'utf8'); + } catch { + continue; // unreadable or genuinely binary + } + const lines = text.split('\n'); + for (const rule of RULES) { + lines.forEach((line, i) => { + const match = line.match(rule.pattern); + if (match) { + hits.push({ + file: relative(process.cwd(), file), + line: i + 1, + found: match[0], + why: rule.why, + }); + } + }); + } +} + +if (hits.length === 0) { + console.log(`leakage check: ${files.length} files scanned, clean.`); + process.exit(0); +} + +console.error(`leakage check: ${hits.length} occurrence(s) of internal vocabulary.\n`); +for (const h of hits) { + console.error(` ${h.file}:${h.line} "${h.found}" — ${h.why}`); +} +console.error('\n Restate the mechanism inline. If provenance matters, "an internal'); +console.error(' decision record, dated X" is the most that may be said.'); +process.exit(1); diff --git a/scripts/check-links.mjs b/scripts/check-links.mjs new file mode 100644 index 0000000..24d623a --- /dev/null +++ b/scripts/check-links.mjs @@ -0,0 +1,104 @@ +// Crawl both built sites for internal links that resolve to nothing. +// +// `astro check` type-checks templates and validates content-collection +// references, but a markdown link target is an opaque string to it. Four +// `[Licensing](/engine/licensing)` links passed `check` and 404'd in production, +// because the page's source file is underscore-prefixed and never routed. +// +// This runs over `dist`, so it sees what is actually published rather than what +// the source appears to promise. Two things follow from that: +// +// * Astro's redirect stubs carry a real to their target, so a stub +// pointing at a missing page is caught without parsing meta-refresh. +// * Cross-site links are resolved against the *other* site's build. A link +// from the docs to odal-node.io/roadmap is invisible to either site's own +// tooling, which is exactly how that one survived. +// +// Usage: node scripts/check-links.mjs +import { readdirSync, readFileSync, existsSync, statSync } from 'node:fs'; +import { join, extname, relative } from 'node:path'; + +const SITES = [ + { origin: 'https://odal-node.io', dist: 'site/dpp-landing/dist', name: 'odal-node.io' }, + { origin: 'https://docs.odal-node.io', dist: 'site/dpp-docs/dist', name: 'docs.odal-node.io' }, +]; + +const walk = (dir, out = []) => { + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + if (statSync(full).isDirectory()) walk(full, out); + else if (extname(full) === '.html') out.push(full); + } + return out; +}; + +// A published path resolves if it names a real file, or a directory holding an +// index.html — the two shapes Astro's static output emits. +const resolves = (dist, path) => { + const clean = decodeURI(path.split('#')[0].split('?')[0]); + if (clean === '' || clean === '/') return existsSync(join(dist, 'index.html')); + const base = join(dist, clean); + if (extname(clean)) return existsSync(base); + return existsSync(join(base, 'index.html')) || existsSync(`${base}.html`); +}; + +const broken = []; +let linkCount = 0; +let pageCount = 0; + +for (const site of SITES) { + if (!existsSync(site.dist)) { + console.error(`link check: ${site.dist} not found — run \`pnpm -r build\` first.`); + process.exit(1); + } + + for (const file of walk(site.dist)) { + pageCount += 1; + // Astro preserves HTML comments in its output, and this repo comments out + // links rather than deleting them. A commented-out link is not published. + const html = readFileSync(file, 'utf8').replace(//g, ''); + for (const [, attr] of html.matchAll(/(?:href|src)="([^"]+)"/g)) { + // A canonical is a declaration about this page, not a dependency of it. + // The 404 page necessarily self-canonicalises to a path that is not a + // route, because it is served at every unmatched path. + if (html.includes(`rel="canonical" href="${attr}"`)) continue; + let target = null; + let dist = site.dist; + + if (attr.startsWith('/')) { + target = attr; + } else { + const other = SITES.find((s) => attr.startsWith(`${s.origin}/`) || attr === s.origin); + if (other) { + target = attr.slice(other.origin.length) || '/'; + dist = other.dist; + } + } + + // Anchors, mailto:, and third-party origins are out of scope. + if (target === null) continue; + linkCount += 1; + if (!resolves(dist, target)) { + broken.push({ from: relative(process.cwd(), file), target: attr }); + } + } + } +} + +if (broken.length === 0) { + console.log(`link check: ${linkCount} internal links across ${pageCount} pages, all resolve.`); + process.exit(0); +} + +console.error(`link check: ${broken.length} broken internal link(s).\n`); +const byTarget = new Map(); +for (const b of broken) { + if (!byTarget.has(b.target)) byTarget.set(b.target, []); + byTarget.get(b.target).push(b.from); +} +for (const [target, sources] of [...byTarget].sort((a, b) => b[1].length - a[1].length)) { + console.error(` ${target} <- ${sources.length} page(s)`); + for (const s of sources.slice(0, 6)) console.error(` ${s}`); + if (sources.length > 6) console.error(` … and ${sources.length - 6} more`); +} +process.exit(1); diff --git a/site/dpp-docs/README.md b/site/dpp-docs/README.md index a89142e..3e42816 100644 --- a/site/dpp-docs/README.md +++ b/site/dpp-docs/README.md @@ -41,7 +41,7 @@ site/dpp-docs/ Brand assets shared with the landing site come from the workspace-root `../../public/brand/`, copied into the build by `viteStaticCopy` (see `astro.config.mjs`). -The sidebar structure is declared in `astro.config.mjs` and mirrored by the file-system layout under `src/content/docs/`. Renamed or removed slugs keep a redirect (e.g. `/design/proof-bound` → `/getting-started/what-odal-can-and-cannot-see`); the full redirect map is in `astro.config.mjs`. The current IA decisions live in [`../../docs/WEB_CONTENT_STRATEGY.md`](../../docs/WEB_CONTENT_STRATEGY.md) §6. +The sidebar structure is declared in `astro.config.mjs` and mirrored by the file-system layout under `src/content/docs/`. Renamed or removed slugs keep a redirect (e.g. `/design/proof-bound` → `/getting-started/what-odal-can-and-cannot-see`); the full redirect map is in `astro.config.mjs`, which is the source of truth for the information architecture. ## Honest stubs for `dpp-engine` @@ -49,7 +49,7 @@ Pages under `src/content/docs/engine/` that document unshipped surfaces use the ## Terminology rules -**Proof-bound architecture** (never "no-touch"); compliance calculators are **open** (never "pro-tier"); deployment claims only for shipped code — capability claims ("wasm32-safe, can run in edge runtimes") are fine. Full rules: [`../../docs/WEB_CONTENT_STRATEGY.md`](../../docs/WEB_CONTENT_STRATEGY.md) §7. +**Proof-bound architecture** (never "no-touch"); compliance calculators are **open** (never "pro-tier"); deployment claims only for shipped code — capability claims ("wasm32-safe, can run in edge runtimes") are fine. State what is built in the present tense and what is planned in the future tense, and never mix the two in one sentence. ## Deployment diff --git a/site/dpp-docs/astro.config.mjs b/site/dpp-docs/astro.config.mjs index d3b0b25..d100c3e 100644 --- a/site/dpp-docs/astro.config.mjs +++ b/site/dpp-docs/astro.config.mjs @@ -4,12 +4,15 @@ import { viteStaticCopy } from 'vite-plugin-static-copy'; export default defineConfig({ site: 'https://docs.odal-node.io', - // Renamed pages keep their old URLs working (WEB_CONTENT_STRATEGY.md §6). + // Renamed pages keep their old URLs working. redirects: { // Design pages removed; redirect to the closest living equivalent. '/design/no-touch-data': '/getting-started/what-odal-can-and-cannot-see', '/design/proof-bound': '/getting-started/what-odal-can-and-cannot-see', - '/design/open-core': '/engine/licensing', + // Points at the licence table rather than at /engine/licensing, whose + // source is underscore-prefixed and therefore never routed. Repoint it back + // when that page is published. + '/design/open-core': '/introduction', '/design/adr': '/core-concepts', '/design/why-no-capability-gating': '/core-concepts', // Core consolidated from per-crate/type pages into three concept pages. diff --git a/site/dpp-docs/public/_headers b/site/dpp-docs/public/_headers new file mode 100644 index 0000000..261e0d1 --- /dev/null +++ b/site/dpp-docs/public/_headers @@ -0,0 +1,27 @@ +# Cloudflare Pages custom headers — docs.odal-node.io +# Mirrors site/dpp-landing/public/_headers. The docs site is the larger surface +# and carries the only JavaScript-heavy page (/api), so nosniff and DENY matter +# more here than on the marketing site, not less. + +/* + X-Content-Type-Options: nosniff + X-Frame-Options: DENY + Referrer-Policy: strict-origin-when-cross-origin + Permissions-Policy: camera=(), microphone=(), geolocation=() + +# Astro content-hashes everything here, so a new build produces new filenames +# and an old one never needs invalidating. +/_astro/* + Cache-Control: public, max-age=31536000, immutable + +# Pagefind emits content-addressed index shards under a stable directory. +/pagefind/* + Cache-Control: public, max-age=604800 + +/favicon.svg + Cache-Control: public, max-age=604800 + +# Vendored from the engine and re-synced on change, so it must not be pinned +# for a year the way a hashed asset can be. +/openapi.yaml + Cache-Control: public, max-age=3600 diff --git a/site/dpp-docs/public/robots.txt b/site/dpp-docs/public/robots.txt new file mode 100644 index 0000000..9c804b6 --- /dev/null +++ b/site/dpp-docs/public/robots.txt @@ -0,0 +1,22 @@ +# robots.txt — docs.odal-node.io + +User-agent: * +Allow: / + +Sitemap: https://docs.odal-node.io/sitemap-index.xml + +# ---- Content signals (EU Directive 2019/790, Art. 4) ---- +# As a condition of accessing this website, you agree to abide by the following +# content signals: +# +# (a) If a content-signal = yes, you may collect content for the corresponding +# use. +# (b) If a content-signal = no, you may not collect content for the +# corresponding use. +# (c) If the website operator does not include a content signal for a +# corresponding use, the website operator neither grants nor restricts +# permission via content signal with respect to the corresponding use. +# +# search: yes +# ai-input: no +# ai-train: no diff --git a/site/dpp-docs/scripts/sync-openapi.mjs b/site/dpp-docs/scripts/sync-openapi.mjs index 2d69611..40a2c03 100644 --- a/site/dpp-docs/scripts/sync-openapi.mjs +++ b/site/dpp-docs/scripts/sync-openapi.mjs @@ -1,18 +1,58 @@ -// Copy the canonical OpenAPI spec from the sibling dpp-engine repo into public/. +// Vendor the canonical OpenAPI spec from the sibling dpp-engine repo into public/. // -// The copy at public/openapi.yaml is **vendored** (committed) so CI builds work -// without dpp-engine checked out alongside. Run this locally (or from a bot job -// that has both repos) whenever the spec changes: pnpm run sync:openapi -import { copyFileSync, existsSync } from 'node:fs'; +// The copy at public/openapi.yaml is committed so a build works without +// dpp-engine checked out alongside. That convenience is also the hazard: a +// vendored file drifts silently, and the drift is published. +// +// pnpm run sync:openapi copy engine -> public/, fail if the source is absent +// pnpm run check:openapi compare only, fail on drift or on an absent source +// +// Both modes exit non-zero when the source cannot be read. A sync that did not +// sync is a failure, not a notice — the previous version of this script warned +// and exited 0 in that case, so it could not report a problem through any path. +// +// The engine directory can be pointed elsewhere with DPP_ENGINE_DIR; the default +// is the sibling checkout used on a development machine. +import { copyFileSync, existsSync, readFileSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; const root = fileURLToPath(new URL('..', import.meta.url)); -const src = fileURLToPath(new URL('../../../../dpp-engine/api/openapi.yaml', import.meta.url)); +const engineDir = + process.env.DPP_ENGINE_DIR ?? fileURLToPath(new URL('../../../../dpp-engine', import.meta.url)); +const src = `${engineDir}/api/openapi.yaml`; const dest = `${root}public/openapi.yaml`; -if (existsSync(src)) { - copyFileSync(src, dest); - console.log(`synced ${src} -> ${dest}`); -} else { - console.warn(`skip: ${src} not found (dpp-engine not checked out) — using vendored ${dest}`); +const checkOnly = process.argv.includes('--check'); + +// Git normalises to LF on commit, but a Windows working copy is CRLF. Compare +// content, not line terminators, or the check fails on every developer machine. +const normalise = (p) => readFileSync(p, 'utf8').replace(/\r\n/g, '\n'); + +const fail = (message) => { + console.error(`openapi ${checkOnly ? 'check' : 'sync'}: ${message}`); + process.exit(1); +}; + +if (!existsSync(src)) { + fail( + `cannot read ${src}\n` + + ' The engine spec is the source of truth for the vendored copy. Check out\n' + + ' dpp-engine beside this repo, or set DPP_ENGINE_DIR to its location.', + ); +} + +if (checkOnly) { + if (!existsSync(dest)) fail(`vendored copy missing at ${dest}`); + if (normalise(src) === normalise(dest)) { + console.log('openapi check: vendored copy matches the engine spec.'); + process.exit(0); + } + fail( + 'the vendored copy has drifted from the engine spec.\n' + + ' Run `pnpm run sync:openapi` and commit the result.\n' + + ' Publishing a stale spec documents endpoints and schemas the product no longer has.', + ); } + +copyFileSync(src, dest); +console.log(`openapi sync: ${src} -> ${dest}`); diff --git a/site/dpp-docs/src/content/docs/core-concepts.mdx b/site/dpp-docs/src/content/docs/core-concepts.mdx index 8cb765c..63dbcdd 100644 --- a/site/dpp-docs/src/content/docs/core-concepts.mdx +++ b/site/dpp-docs/src/content/docs/core-concepts.mdx @@ -21,7 +21,7 @@ Odal Node is two parts with two licences. The **core** is the regulatory standar One rule decides which side any change belongs on: *if it changes because an EU regulation changed, it belongs in the core; if it changes because of how the system is deployed or operated, it belongs in the engine.* The dependency only ever points one way — the engine uses the core; the core knows nothing of the engine. -A fuller treatment is in [Licensing](/engine/licensing). +The licence table is on the [Introduction](/introduction). ## One seam for new regulation diff --git a/site/dpp-docs/src/content/docs/engine/architecture.mdx b/site/dpp-docs/src/content/docs/engine/architecture.mdx index 8cadd13..6276d1a 100644 --- a/site/dpp-docs/src/content/docs/engine/architecture.mdx +++ b/site/dpp-docs/src/content/docs/engine/architecture.mdx @@ -34,4 +34,3 @@ A node serves a single operator. There are no shared tenants and no cross-operat - [Operating a node securely](/engine/security) — how the node protects keys, data, and access. - [Self-Hosting](/engine/self-hosted) — run a node on your own infrastructure. - [What Odal can and cannot see](/getting-started/what-odal-can-and-cannot-see) — the data boundary, precisely. -- [Licensing](/engine/licensing) — the terms the engine ships under. diff --git a/site/dpp-docs/src/content/docs/engine/self-hosted.mdx b/site/dpp-docs/src/content/docs/engine/self-hosted.mdx index 654f167..3b40dde 100644 --- a/site/dpp-docs/src/content/docs/engine/self-hosted.mdx +++ b/site/dpp-docs/src/content/docs/engine/self-hosted.mdx @@ -29,4 +29,3 @@ The engine and the sector rules move as the regulation does. Updating a node bri - [How the node works](/engine/architecture) — what's running under the hood. - [The CLI](/engine/cli) — the commands you'll use day to day. -- [Licensing](/engine/licensing) — the self-host grant in full. diff --git a/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx b/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx index 1eca517..ff17b31 100644 --- a/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx +++ b/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx @@ -42,4 +42,3 @@ The contents of your import files. The software reads them once, validates the d ## Read next [Core Concepts](/core-concepts) — the three governing principles, including this one. -[Licensing](/engine/licensing) — the open-core model and the self-host grant. diff --git a/site/dpp-docs/src/content/docs/regulatory/central-registry.mdx b/site/dpp-docs/src/content/docs/regulatory/central-registry.mdx index 988f84b..c2b5e19 100644 --- a/site/dpp-docs/src/content/docs/regulatory/central-registry.mdx +++ b/site/dpp-docs/src/content/docs/regulatory/central-registry.mdx @@ -20,7 +20,7 @@ The registry's API is being defined at the time of writing. The expected require Odal has prepared for this beyond modelling the interface: **every published passport already commits its registration intent to a durable outbox, in the same database transaction as the publish itself.** A background worker drains that outbox with retry and backoff. The practical consequence: publishing never blocks on the registry, a crash never loses a registration, and the day the Commission's API goes live, the accumulated backlog registers without a single passport falling through. (For context: the Commission's own deadline to set up the registry was 19 July 2026; as of this writing, the API specification remains unpublished — so this durable-queue posture is not caution, it's the correct engineering for the actual situation.) ## Why pre-investing in the bridge diff --git a/site/dpp-landing/README.md b/site/dpp-landing/README.md index d1a99f3..b55336e 100644 --- a/site/dpp-landing/README.md +++ b/site/dpp-landing/README.md @@ -53,7 +53,7 @@ Pages prefixed with `_` are drafts: Astro does not build them into routes. They There is **no `tailwind.config.mjs`** — Tailwind 4 is configured CSS-first via the `@theme` block in `@odal/brand-tokens/tokens.css`. -The section structure and copy follow [`../../docs/WEB_CONTENT_STRATEGY.md`](../../docs/WEB_CONTENT_STRATEGY.md) (§4 landing structure, §7 voice); the visual treatment of those sections follows [`../../docs/redesign/DESIGN_SPEC.md`](../../docs/redesign/DESIGN_SPEC.md) §5; the voice baseline is [`../../docs/BRAND.md`](../../docs/BRAND.md). +Section order, copy and visual treatment are all deliberate rather than incidental: each section component carries its own rule in a header comment, and the tone across both sites is plain, unenthusiastic and claim-checkable. The originating decisions are held in an internal design record dated 2026-06-10. ## Deployment diff --git a/site/dpp-landing/src/components/GetStarted.astro b/site/dpp-landing/src/components/GetStarted.astro index 0d7a85a..7f4bfac 100644 --- a/site/dpp-landing/src/components/GetStarted.astro +++ b/site/dpp-landing/src/components/GetStarted.astro @@ -1,6 +1,6 @@ --- // GetStarted.astro — navy terminal block with the quick-start commands. -// RULE (WEB_CONTENT_STRATEGY.md §4.7): whatever is printed here must be the +// RULE: whatever is printed here must be the // tested self-host path. If the quick-start changes, this component changes // in the same PR. Entrypoint = dpp-engine/docker/docker-compose.yml // (node internal port 8000, mapped to host 8001 by default). diff --git a/site/dpp-landing/src/components/Hero.astro b/site/dpp-landing/src/components/Hero.astro index 6eb5290..db8c450 100644 --- a/site/dpp-landing/src/components/Hero.astro +++ b/site/dpp-landing/src/components/Hero.astro @@ -1,7 +1,7 @@ --- -// Hero.astro — the highest-stakes block on the entire site. See docs/BRAND.md §2. +// Hero.astro — the highest-stakes block on the entire site. // Navy band with the brand mark as a quiet watermark; ice text; two CTAs; -// one-line trust row. Copy source: docs/WEB_CONTENT_STRATEGY.md §4.1. +// one-line trust row. import Watermark from "../assets/favicon.svg"; import StatusBadge from "./StatusBadge.astro"; interface Props { diff --git a/site/dpp-landing/src/components/Roadmap.astro b/site/dpp-landing/src/components/Roadmap.astro index 52d51ac..2548299 100644 --- a/site/dpp-landing/src/components/Roadmap.astro +++ b/site/dpp-landing/src/components/Roadmap.astro @@ -1,6 +1,6 @@ --- // Roadmap.astro — three-column status display. Data lives in src/data/roadmap.json -// (WEB_CONTENT_STRATEGY.md §4.8) so editors change JSON, not components. +// so editors change JSON, not components. // Status enum: shipped | in-build | horizon — the "honest status" framing. import data from "../data/roadmap.json"; diff --git a/site/dpp-landing/src/components/Section.astro b/site/dpp-landing/src/components/Section.astro index edd8467..defdd91 100644 --- a/site/dpp-landing/src/components/Section.astro +++ b/site/dpp-landing/src/components/Section.astro @@ -1,6 +1,6 @@ --- // Section.astro — a standard content section with an eyebrow, title, and slot. -// Tones (DESIGN_SPEC §5): default = white, muted = warm grey, tinted = ice. +// Tones: default = white, muted = warm grey, tinted = ice. interface Props { eyebrow?: string; title: string; diff --git a/site/dpp-landing/src/components/StatusBadge.astro b/site/dpp-landing/src/components/StatusBadge.astro index 0c690af..6e5570a 100644 --- a/site/dpp-landing/src/components/StatusBadge.astro +++ b/site/dpp-landing/src/components/StatusBadge.astro @@ -1,7 +1,7 @@ --- // StatusBadge.astro — a single, honest project-status pill. // Signals that the project is pre-1.0 and the product is at alpha maturity. -// Deliberately minimal (WEB_CONTENT_STRATEGY.md §9, §7): no enthusiasm, no +// Deliberately minimal: no enthusiasm, no // claim beyond "early". Two tones so it reads on light (nav) and navy (hero). // // Uses the regulatory amber token family already in brand-tokens for the light diff --git a/site/dpp-landing/src/data/deadlines.json b/site/dpp-landing/src/data/deadlines.json index d62c9db..8129da4 100644 --- a/site/dpp-landing/src/data/deadlines.json +++ b/site/dpp-landing/src/data/deadlines.json @@ -1,5 +1,5 @@ { - "_comment": "Regulatory deadlines shown on the landing page. Every entry needs a citation. The _verify fields are internal notes — NOT rendered — and must be cleared by the compliance lead before launch (source: docs/WEB_CONTENT_STRATEGY.md §4.2).", + "_comment": "Regulatory deadlines shown on the landing page. Every entry needs a citation to a named instrument and article, verified against the Official Journal text — not against a secondary source. The _verify fields are working notes and are NOT rendered; a deadline whose _verify still records an open question has not been checked, and an unchecked deadline must not ship.", "deadlines": [ { "date": "2026-07-19", diff --git a/site/dpp-landing/src/data/roadmap.json b/site/dpp-landing/src/data/roadmap.json index 9fb7633..57857da 100644 --- a/site/dpp-landing/src/data/roadmap.json +++ b/site/dpp-landing/src/data/roadmap.json @@ -1,5 +1,5 @@ { - "_comment": "Roadmap data — the single editable source for the landing roadmap (WEB_CONTENT_STRATEGY.md §4.8). Status enum: shipped | in-build | horizon. Rule: every numeric claim cites where it is measured, or it is omitted. No project-milestone months — regulatory dates live in deadlines.json.", + "_comment": "Roadmap data — the single editable source for the landing roadmap, so editors change JSON rather than components. Status enum: shipped | in-build | horizon. Rule: every numeric claim cites where it is measured, or it is omitted. No project-milestone months — regulatory dates live in deadlines.json.", "columns": [ { "status": "shipped", diff --git a/site/dpp-landing/src/pages/index.astro b/site/dpp-landing/src/pages/index.astro index c86fa53..9b98635 100644 --- a/site/dpp-landing/src/pages/index.astro +++ b/site/dpp-landing/src/pages/index.astro @@ -1,6 +1,6 @@ --- // odal-node.io — home page. -// Section order and copy per docs/WEB_CONTENT_STRATEGY.md §4. +// Section order and copy are fixed deliberately; see each section component. // Terminology and claims rules: see §7 (no capability-tier language, no claims // ahead of shipped code, "proof-bound" never "no-touch"). import DeadlineCards from "../components/DeadlineCards.astro"; diff --git a/site/dpp-landing/src/styles/global.css b/site/dpp-landing/src/styles/global.css index e3245ca..44caccb 100644 --- a/site/dpp-landing/src/styles/global.css +++ b/site/dpp-landing/src/styles/global.css @@ -28,7 +28,7 @@ } /* Light-first design with dark *sections* (hero, footer) rather than a - global dark mode — decision in docs/redesign/DESIGN_SPEC.md §1. */ + global dark mode — decision recorded 2026-06-10. */ h1, h2, h3, h4 { font-weight: 600; From d406c6dbaaf770524fe28cacc0a9a5cb5deb06f9 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Thu, 20 Aug 2026 21:29:39 +0200 Subject: [PATCH 010/168] chore: untrack local-only asset and tooling directories that were staged in error --- .claude/launch.json | 17 ----- deprecated/apple-touch-icon.png | Bin 48732 -> 0 bytes deprecated/odal-logo-horizontal.svg | 30 --------- deprecated/odal-mark-navy.svg | 63 ------------------ deprecated/odal-mark.svg | 63 ------------------ deprecated/og-source.svg | 16 ----- deprecated/og.png | Bin 103677 -> 0 bytes .../source/odal-node-circle-background.png | Bin 25376 -> 0 bytes deprecated/source/odal-node-fit-to-edges.png | Bin 23699 -> 0 bytes deprecated/source/odal-node-linked-in.png | Bin 10141 -> 0 bytes deprecated/source/odal-node-no-background.png | Bin 10845 -> 0 bytes deprecated/source/odal-node-no-text.png | Bin 11668 -> 0 bytes deprecated/source/odal-node.png | Bin 5750 -> 0 bytes 13 files changed, 189 deletions(-) delete mode 100644 .claude/launch.json delete mode 100644 deprecated/apple-touch-icon.png delete mode 100644 deprecated/odal-logo-horizontal.svg delete mode 100644 deprecated/odal-mark-navy.svg delete mode 100644 deprecated/odal-mark.svg delete mode 100644 deprecated/og-source.svg delete mode 100644 deprecated/og.png delete mode 100644 deprecated/source/odal-node-circle-background.png delete mode 100644 deprecated/source/odal-node-fit-to-edges.png delete mode 100644 deprecated/source/odal-node-linked-in.png delete mode 100644 deprecated/source/odal-node-no-background.png delete mode 100644 deprecated/source/odal-node-no-text.png delete mode 100644 deprecated/source/odal-node.png diff --git a/.claude/launch.json b/.claude/launch.json deleted file mode 100644 index b396495..0000000 --- a/.claude/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "0.0.1", - "configurations": [ - { - "name": "dpp-docs", - "runtimeExecutable": "pnpm", - "runtimeArgs": ["--dir", "site/dpp-docs", "run", "dev"], - "port": 4321 - }, - { - "name": "dpp-landing", - "runtimeExecutable": "pnpm", - "runtimeArgs": ["--dir", "site/dpp-landing", "run", "dev", "--port", "4322"], - "port": 4322 - } - ] -} diff --git a/deprecated/apple-touch-icon.png b/deprecated/apple-touch-icon.png deleted file mode 100644 index e7c2e1580fc8821650c873a18c5e5aced904569d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48732 zcmV)|KzzT6P)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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

nktbS4&(h2Eh z#Fg7mpJRNXv+x~%X~pcb{ySUPWfc&ZOt)+=w8d>W>Ai&vkf50Sq>Wu&BR34J;iyJ` zMCLB5?q8_elpbAylSPLO*lcG-Z7lS!_LCL(-(KCG5JgA^_=d8p)C(x^WufnnLCB*- zm}V68CDQTsC!=zr$gt?>d)dHDUcjv=h$a@*;Nc)4(+tzO>y{H%D}gRvKW-GkplwXC z^7;AJV-zD(qph8~7Yf>3v{p1JvmK@yT12Fsx0lJ57M44{Eefhvi>&K4<5ut`4c6;V z!VnH6GD${%?x?G;A~?s$x8ApjcK*#ibsdoA-OQc7!Bqx@KW>C3?~bnj zIw4TlXT%DGwOtpwi>^d3V&Mk=WE<0(i<0h8j>-c=YADS_b~}FRp&*(j-t{sLEe(fQ zf+CGw@Dqx$a)=`~q#0^47Beqq?LMSXn%%|te`_1;#yb8``=O4)&e#lOaR)q(g|W{n zGf5+6&iWynpoC4MVl*C7M&%)emk=k@T4Br9i8*~lAyp>gj1stc}-2{}0bn9c} z@8S6eb%kcZ&5W6Ei2lIX}ntQIbp(2yvW=oIN) zEi}|gZzQr{S%?A0^)HiVoYxpW9Geq?~GBJfeAbFJy0lMf<6{K^Z$Z*RBzk1{Yhqt9usB)?eh>oT( z@KvS_;RB-i;2E#L{cZ1+AgsebzX(q^+PCdW6lPh-Je3KKtL`6OfhfDBgyX_~E`=h~ zX>v*%6z<)mg?#$RZb99}LMUxJ*X8@kFX&Jcl)UM`lK67(@gnqmIhXr~FpMkcP$xU> zd+?4!8%YTqVAvVfJF(*?!2g-L&IvWBxe)DE)Mx}qPiOi*iwA6WrY`T=U{ONUp;=M@ z-!oRpFeR7ibjJi4STE@NuJEosp7iH%DJ-tWJ!X>@zlWwPk(XwC7Uq0%v#5yv4 zcFYKG38#k}QkuSRm9G@d*5FvE{@7zkzAmzrjh_rJcxoSyo%{DLZ2-3hMs{-;>-XNb{d(p7yjL2UNJ>-fj z+~i2x+@Vj9>0lI6#g_>*Ipn{2XL(54vwuWzbI0x;FztB$T%f|LLZ=BKMskx*;WMOB z#a$_h;D|RUnUR#OJ)3;ti#3rqwT@y7_)Nw!99=_+FM~nn+h6x%CV20(m=aaM1gEr1 zh$=C^m#CbvoLxO!+f#o-@-u1Vk3f(O@I!Eo4Uljlz7%+~#TJY@G8exIF<`0v6C+=M zRgI}OcA#s-iL#N^(|20{xqt}K>Cx(s%)F+@^)uXnq0mju(%pPHDX7&o3X6KzX*qxw zTxg>a0giR6vxveOMdgU>!o<1N@;A@^2pB8dW>c$he+%mDmH!RZDOztH$}hDCe-K-{ zQ5aF!Q)CtCq!QpO@}mzjmel5G^aHI}sj0g^ARW%Tu8%{nGoS09o@p;0SsQq+r<|IIbmGt6`&!5>T1H)s)!@{VSLZmNn(CkzuPPEF6L!1=6OVS`;7Lq z5vG2oumB|>jdjAtdvDpDAPqVI4E1RE@7 z9?Ts>-UHP#A;y8!A}Kw+Gz){tpqSKv!i6$e~;2 zsgnQlSTN2z<<|w%1>I;w1dPA%yR89}^{hn~p>1DRnzVV?muyaaitx(oD$N&XY5oUx zJY#Ax`MhpYwfv=y8_@8u0(Byp7?-zL%$!m@Xd4K@d?{hut_QPMl(L(1T~^6l8=8)0 zx)-rwx6M@gQH@(4_GJP3rS#`}@AE7Yn`lb?|Z%1aQ1+ z8)w1$@jik;U6+U&5bLDyW!y0uKq3QYZi3(XeE*`sSCw`>6d%*(ABqsTY^~3&Tyynm z!X+l5n?H-_$gyfHmz_q@@=K3v`j7bjz7yoR}h0OYNzb}e^7Nj~N>^RFhW$T&dt03bi_6%x6(o)i3f zi;G2P^%xKFg_LdD_S~Quwf4ZVY?87LCqM1NJFEJEP3Tq|AikVgH8JwmM$mp75g1}% z_W7!ED+!h+DF4@s7OUftf*MEjW6dZdCnE&ne4%zK*Thn-b?xTipEvAB`b=EjkJQC>{@C{9-uQBU-_e9}---t=Sh_@0J23sj_{ zchzxo*XR6cRi5NOtm8t$*i*kvwNUtHXe^104gbwVm+(-VA)V9_$_k`+RX#d@`9v0G zz5PkYT7wbOvyjb_cdKwm1uPBU&KRe+GU~EZwRlzA7VR)(#IT$ASSba%>YF{N~R#* zXQ+?m=wdsCcNT{I)*aK6D+1wbJ_POGWMOfyDTCXa$Btk9M7HGjoC<2TiIsf6NZnkr z5iY%BV-GG|0gRNrZ#rfIN&F)HO>6M_Z~FLWDB#-|X-Cme<0PW@L;XKg@XKI^#3tP# zR}u4$Y76^(m#&X-&>X}B;NLEu04#FyB9=yIzZXE$W%@NOJ$DGNU0H0ZM8YB~r_ zlrl~FMi)`KpYx$Zf4E+M++HJsc8`1{<9-lo_#VV7bilr&s5kK;sQ2IYfyQlzKAhzO zsuT9NQcQA3-y=JbU1YfXUmf0M?!UTm-d76!c2VQR4XEeQFK4s?ACB3X(;{MiVMu4N z%_9Omv$@y*68@S^Z_9%P;xSrd7&mYFP0LWo9IrcPxxIQl@AJt_RXgE20*8u}8T+$k zfhB2w<69Ah-Y64j3GvtBA@q&VFWU065|uG$-d7Csm#U_SDZ!Wz1NNT?y^nEvu)@^u z3rVu8!Bl&3e>&IAi+*jeyDrP?gbOs)i3+WA8~!cgQrci%NfRn-|FnjP2gY``dwUyQ zb^Bo_d$1N;P{*sWz5U|dOQWhe3$Lp2*fpmRMg(MAXxIAe%WW3IJdGW}GdVR?%{en$_bwS8pR5oW125(TuU z_y^S)ne$~&{Rt9o^{-MF-)r!Z;wfrpA5RFR*)jWUxrX`A)S*FarYEp%#l+#BPJ+yS z{OgF2r}>z7^j&wYd_b3W1{wNS(~TV?5Up05T3BLYd9uxoO-O#&-@m9sD*G*!5U7=_ z=rW(iy}K)(B`3qR+vs6!#v)?k0h94*F0H`bjXdkZmSviPO15b8Uag-LOB0-bhqxAm z+X%%0t!eT-%;W($M`NMI&Ww*Ny^n88MdSF%FkNGNN1e4C%+pd@$7vDRXKj$2b>Qm! z?dN%l+pa|^fgmnYYuUi=Sz(m;C#TtvXf28^6=@Q!GK*59Rs|kMFw&ewK2HJ5Dl_uz zm$qP>@p&Id!<3A&(#t&qk#~=>_vaTf>D9PuO2OXLDa7RMd;L|~0YNp|qc(iZKgerkcjXajJj=t@>|p15lfUuaPsVA@`jJaQWhNci`Pu4G)Kgx}Cqa7bB~TB}D*E ztqCbDJiqa8R_{4i4rAC8XI1PS%t>~qvX}xYhL-14ba-b;F&GWjM$UeW3@8&KEb)ZO zzN1g(L6LL05PHbE%yz^Nn%XQU!MUc&-H1WxO7^`P#wg$R?87 zCX7$0*MJ(`nA=zrN+SDJYC#zR zuuVS>``ypb{8w)~Uh-M_m__`=HD6NZ#q5)R*ZdcG!04~gi^(Oc1D^`VHrkEjOJtUP zzecjnj*Q&a99(%m4bqqCgWU$8k^MmISDY?Ir4R2Axk5=LVNtU=WGx#&8N$#mNkJ~m zF2i$VU~u&%Abafs{|+&F3jE^y?-{*HwTq6+N6J9D0`r*-H$n0UNfyX;nC>UbS0|FIN}8e?NkdP5ca`) zlL(gz2U2NoS zLq+4s)|*_TSr1?9O(oW#!7LcYrZ`VW8p=A#hoSC#MPWG;_99`ZeR_}__*WK#m^BtC zc#X{{M*v!iPKlkosQoM?^CE9LrqQ8sGaOK*RGoE0-%xt=bya?+k32l%+;bR^k-8bn z@BcO7O?&xvxuI<43Cn4djuHbcRCR?*Bbz^Cf}70avu2gDm{Z^UNUUdo6c?XV5q}nB zzdgz1iEeMOPSV`m*)usD6PQKx?C}OoJ*s@S*FoHuHM`gZ?}05;``F-n>Ag z(XTAdnufo!+%g;4Lzo#%GF+J1w4~l-{8Gf2&U*Tc2}z@JK!Cm@Lb!T$r8F4c9p)x$ z`YWp#t?=xfnVKK_WVfoQ^h5sfe)^9xtDi=r86Jh^_>4sY6v17s78m>dpS09bp4+4C}4MhjJ{ zEOkEfh0$O19wlKeGOa;5is)(rU8?N$we@fnZ$~6%UfB00N0K*IGsplSiM2Yz=XP@+ z;vhp)E2gfvokzzofomqhIbosNHTJg#Obo%ZRQCnHKIA07rOTFJIY|vgG&pe%$K(D1 zo>8CUeO*G~6*{W?D8Dl|XvQLOO>t3)dO1czG0kP3p>SvjvNn}%QMD(^*J#ry|EN#0 zXDqF(#5zjiBlMH#+>;^a(+k$-jG=);OS$wnpU&}nd$Xl+_4b>~D>~Q0&F)#~2??A_ zZUUMh8(DhS={`ye;wQAT+>`cbcs8p}On(U8Y&ap^{SoU4{y_Rqp&{+}2WJ7tF6CG~ z+3LVa{ls1tS|4T5#=eirp5E@9pdBDU%Dhz-p0xbhIL(jRkdWB@UYYqhzVUOCcJOgRd>{!63XAg#it`Hz8VQL@3kyk$it-8y zN(%}WnMfD^|19wEbaZtN`v1S+3^$q;v0yIP$jr|-;H|WSr-z5rdp{&3tUR$a#rNNV zc(3l{e%eNpWnd_N>_qCRHVi=qc|)p2(wsSv1J+CAm^oc5Ah!48b0Lh3b2mJ7jmm1< zv~}q7lP3=?t6&AsZP#AA;2PeoiSLO3^f@6xRt}t|$e_5J8=n(l&DFOOFH$$~+ zH8m#w(2Mw%C5PsC%TCPVPhqQtHmzb$x{>S#JpCjJGSrwTEn3e}9aT^NkkC$PIHMkf z;~aX*&sKZn)Mh1N+!r)idDpS=5o=M-?yIc81*XtjBN7H=^azA@NE)wnRqB=OqW>Q} C48+s` diff --git a/deprecated/odal-logo-horizontal.svg b/deprecated/odal-logo-horizontal.svg deleted file mode 100644 index 35c61af..0000000 --- a/deprecated/odal-logo-horizontal.svg +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - ODAL NODE - Signed by you. Verified by anyone. - diff --git a/deprecated/odal-mark-navy.svg b/deprecated/odal-mark-navy.svg deleted file mode 100644 index f430ad8..0000000 --- a/deprecated/odal-mark-navy.svg +++ /dev/null @@ -1,63 +0,0 @@ - - Odal Node — open door beneath a constellation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/deprecated/odal-mark.svg b/deprecated/odal-mark.svg deleted file mode 100644 index 02b61a2..0000000 --- a/deprecated/odal-mark.svg +++ /dev/null @@ -1,63 +0,0 @@ - - Odal Node — open door beneath a constellation - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/deprecated/og-source.svg b/deprecated/og-source.svg deleted file mode 100644 index 667fbfb..0000000 --- a/deprecated/og-source.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - -ODAL NODE -Signed by you. Verified by anyone. -EU Digital Product Passport infrastructure - \ No newline at end of file diff --git a/deprecated/og.png b/deprecated/og.png deleted file mode 100644 index 818c9424e3260d2794985b3c0aef453573e62d65..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 103677 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 diff --git a/deprecated/source/odal-node-circle-background.png b/deprecated/source/odal-node-circle-background.png deleted file mode 100644 index 4c3131887266102c4bfd9b3e5f02aca00b3a5b78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25376 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 P00000NkvXXu0mjfldMx2 diff --git a/deprecated/source/odal-node-fit-to-edges.png b/deprecated/source/odal-node-fit-to-edges.png deleted file mode 100644 index 37c4fb7d99e588d4527aec5a65a3e829918e2950..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23699 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 diff --git a/deprecated/source/odal-node-no-background.png b/deprecated/source/odal-node-no-background.png deleted file mode 100644 index f42d76f1150a1a168a60407da3399bcc57cdefaf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10845 zcmeHtRZtvE@Fx)5T>~tx!7aGEy9Xz@%c5Cavx_?fcXv;4*Wm6Bfh=y9@9y=UuI};Q zHC`3_gn{`0_>Yl4{F9v5+!_9x$kdg!Wt8pQ zI(FXwuLJVB>GV$x*;7+h3+AKMak=PNu7#K9X{}>h~Gn*I&1^}ZV zEurn3eUY=&vd4&zpmO-$+qNWu0xa$9sirx;5^Z>b||IW`e5uu<134xp4_8Z?3u^sZ{znD6ZI zn)Ld3ZA5I#Dovc`tS#`D-{uxwD&vi5FOOT1{(N9_bQCyz5Z8v&lRnW+EgaGhgk~W; z;5nbA@8N~fx{`S7$we|aM*eM-S_a8(bGGzDKhe!;Q!}Q#(QG0AdXv*g@P${6LEVx& z2>csmKa++VPxpvc+P~rw>GCjr8t%TcmWaB8oQ-;`vt6$Mj!I8*VT&LEp^<>&V>iE~ z9EmL82*6&>tKT}=d}J;^;zMuz!3-+{;u+|J5~;Y{E-cemrSv_-z@*s zm*54S*CyAUjQVWa>+114Gw*8pHgU&9RsJwpX}{Do}LAtd`*F; zq3ztNhffSC!&$DQ*^}$P$n|p}lkG=)1ksS2@OX{4kQ@CpiZj zIL%^_eEtjEHg%>wF9JXN9o?a%;*$DP_)zdP@;f-c*qEceaIaVD@SpV@;%4r9L-#g0 zQ9-qG<9LVj27sGy_vmZKdr#HT?_QW=ppBIKx`_nG1D{=cpH9imU)w_l!S z)V$elXMNxqmWJ{A{-Wc<18jtEOY&kFSuI zS0jgWi`aqsf9U&_zP(X|w+DJgq^%!p!L`(l+rrz9{=xnB!i%mU6d>(NiWCceMibT= z(=xGV1_*QOPn&7LK4Mim)OGMjfS-Enlv&g~>qh?EtZzGW=eJxQ8`u6C?O@`6NDsx5 zr4(rn```?7H>GFF65b`zhS1JxLq)^V7oHwHr@Q}5f^DW z>LOfjE&0W#^>-*WjLro6@cM`Ig`z0%pTeYKvH(d}#x`W5LxBky6YU4)L4ViWIfFId zfX`=AYjR)tgMk9ccq+jikDtEcg18NJBrSaXhJ~VW@)GP(I)Nv@L z#K%9ESLuyq390bR;BRnsBqw3M6MOW&I(dzJdL34_G%|+y{-fL$B3v@LPU6J(8GVYE zO9KGEup1sx*eNuHZtk!P#NvgcnLStoG@!|Cp|5X;CjaR7{kt;hv8y(@8=q{zdkx9iP!qfUxnKfI z)Kzam1TBT=5|YVYohwr-IutAau`MSz?1Ktze@CWI@lYiwu|yy3NBU3@@1u$r)OOHhmnYSvvuN|7qg1p z{!V@SN~2U1(?i^RZo5L2J{Eg@;lu6p;JO3&?M>CO^eZ3@_jodbFj_m*U*yBWEo}+9 zi>)XkO@Sokq*d7LUrpTaZJZ=HZ1%Z30O&u1+%)>bhk&vXAh(Toz$*3YYMF4*iSeiQ zk}&FpVlTq1GPAe~ybn8d92-~u*30WC9RAG)DqqH`OqJgpsyhX9%K0+E##Bs*Zat|{ zEFmN!F9%a_^$+?>Hd0t;ERqLt&o>S3;ummJrLQD0PWdvkul$;_dP>(E?=(8OTIt^Y z*m=t2I##Iuro=5&=f>~}q64Q-hy85bbGZjLhctw8Y)VZvlFif6H^uvOLLWoG!Kb)y z1fgIrJMX)?eZL+|Py>5fqH>N8FE?erGLg~<6)rP*ONBh{9PKtO?aDYrKcI@Gu{Dwo z&)!Lb3v~P_!kC8$v=)V5c`fA+w5w$$dA5+g>0Q*(u@>-9@s=M&k8YV$UzyNsBW?n%MWRfoNf#eo)>B4ZfYNjh3qOGq40g=Ts0 zJd%pm;U1Jz5^$6j`qxTS_OmiuYJy};kxMt}2Ft~=$ z`Gq-M(!sb_gEQ}NRtC6i(0`21BBCi7Z9@rDg(2U5ni(#^Agwm+j=Z89T)busXP6M| zD`f3Gx1V>`U!{7L@fbBUKpcb7VtH=DRv1Ap|MV`X^`u1gtT`PTFF{o+o8HYpwHznR z-865hQrIy*RHB;XIFtN&b3MRw-Ua9Ckk0F|+WGCODw6BKxVltaIwd!GZqaDV`_F3k zhJKqXO!<@g8df#s%1y8Euu(c#6*sVo1Gy?`$@Qu~PcW~eF`yjVc4#N>(Wn8N&bmQz ze$?d3rNsQ1_Dc1#e!*ITM)@Kd7-454THv)Oe&%8FDl2w-zLi5^G2YR96PiT;W&Cc} z9_4cKQg(CE=QVQF5_oFEq{f>OZbjE|s{*8~&CCE#{2IGy@pIeuqKc{+3OEjydv37)fVTgSG8zGvhxExfLuH4LPa zUY%L#+J-pZZhSBKuw)HKh1`x^@rrb$cw%kYnBJ69U&9X<>YswxLxwO{Zgs*I`VygM zjh=$rQ=uy_C~U}naJD*i9s$_Dp&D_(0_|7pRuCs~jh57!EM+WjWG;>pJewWN5n|p> zYf5yh+LS|Am5gFqOrjl7B-utO^0KC~sZ+3|bnNHu;;E&MYG^@Dr&XjrU-kLjEu3v= zj0H7JtzGAE2*Vcd&~s;k=tIc#-z=XZ#8X{?<(m#o?yH<*p zUu@eYM=+40I@7ni%2rR8;!3I6;M3M$EBRG#dTMHfuxIXEC?HsX%lyqR9s&0*@VD!r zUA4O-A$;O#xZo2Hq&l&UmKS%5pXl{c`21rz=dPgj^B5;?wti; zZ28WPPNsaqu2#Fpkyfd(48@bFGApSL$E)9o0$V}{XAZ_CCm3w%^b_UxqQ(Mm^t=_| zOK_v$Cz!`jFRc=1_G{dsD9HhOmB07ryVX9lw?Yi)JGj<;FZQ=ZCY_|r2)lN);4NmN z{UC|dOC2WmDyLQbk?6~g!xU#aMP5a{mOaI=A70+cqtkQ@&wEP0RFhf0lWIBbws6pD zMfk*&hKrpQYRR+Lv(XV2=@WOKB~y{TdESnIfz=5$M+eO-j#fbdOv}+G49$I9tQe!o zd+vid@_{eYKJ+#PL>atnU8cXH*-831&_*7$xhWmodONNcn_mq8&UzjgWJ|MT!|{q) zD_sUx-%=J~GmUv0>Tll}mtwPAx|J^U?{4Spj1WbtqZ6g@E4-6@!aXS$uEu44_rOv* z{&c3yNFpb)r<5hUJ7EqzD9oHH!wr0R-|U)|+tXpMx019-g-_kX9RschG&Efb&f^$= z;=S@}-7b778Gp^(H&cM&@-~jHg!Z;CIg=%ud0rJ29~ohgrIngZZ zL54a=pTpz1rfmIVNK@D)(mvM3>ZTIj{p+pY-nDA&=I*ET4Vuoh202UKh8@vrh)WeG zFtZFIMYO10%hi-+gFj&tbkb@Cvp0N-=A3#q&o_UNq3o8d;y%HzFpq3EYse)a^5O`U z4XE4Ts`mamr@y3P!OLc1fagf=eAd;gs9>`=jtiMt;%wsVZGY{?NFeubywC9Q8s+WG zk$Xn&xX^Mfs%MMXVvkPop02K~8Ry%Y{c%$4QTnFZyX9;vfC!YAv3dv>h0|^dToG~8X9RLpS+GKKoXz{1zH>l1sn*<~`_uVkLC6=9lJ#*FEP?$FOSEeYJVFsO@l5{ zeVJ@(qMMF>bLPOUMX5{LUwbE7ZRdp{(5jSezo(RST7-rv(vr`?Fm#tgH4p1xq*`YU zHJ>M3u9r<{k6E!8zT6aau*;;56^ONx+WN>oh`{c!n@5akQ%2*-#%2-F#Ps3D3^nZSz$pV~) zjcW}Uhnkc?ckHa)^wf|pc)YP<&5DEdOEOhB*HDPYpai95oN10>ClF?CwznbS{)HKS zSGoPBbyeNp=p4#cj-Q)Llc_X}I!atPjWB0{c=ctqS2LtpKCu{2Z0Y`T6+c#-Z2;Eb z(GI-MDVv8<6n(`h=wd?;QNLAhVciKY38FRbt|6Ncc###e?~2{|)=aNQ@k+{m)hv8n zfh#|rK!!aMzH9<;*EMI4E9XhOr2a&#hHBT;ZC)$7L4IeP?W^$`z~fRzvrTsyW98tsXt`Vf!WeZmX` z#)$=+7cVNd0)0Fym;2xj>v@|R&h6D6TMMIK1i|8oN1_F(q9_MVg=b@Lg9_q+N_$=c ze#8IH^jlpBbzmAk@p~A{?oA1Irxbw5V!Eh{EEc02rl90Dq+9%)0_gryk-<+fNw9c!<@2+O#cvrphDe&g<$iTMH_+N z?Tr+j$kI!2Cmsm@M`ud|c|QRz;{rvGx-;6QcU@(P8H%JZ$e0UkwWwGbSVbAQEd`_d zkz*E5O~~r#Z4rly*DygSJ>E}WMUlcq8&WhkUYrVi4NmqM6v$q@87f;jk`F`5tguJY z9Vr5<+_?)T|91o+m{ojQJq6D)E^Nj^*wF}dtO0m^CPyFHw|spEqHX=kTw|%9-wkv0 z&%oG$yYXt?m? zdw43gLK#u4cymdo4BvzC|LsX9GA%iojwU(#*((5hK^IV~2moT~URicG0}_=UuKDme z|DA+FOk6@i{tCdr^P8$N=|6YiX@4uKA2n|)>io$hFxcW}ifs~r$2mLXg3+({6 zu&L&+n^kSz?(Xehr&nJQs7nn}MtQF51OCb=JNxZ7b@t}6VRsW=re1t36qhstElUC# zFNrAw9Gf+Jx=%N(^ zr4^i;6T;j*-5negxYKVB%Q>chumgJ%z923wRCEzfa&QRw4IA{EjL|+RFL(h}M8uuX zNkC*$w0i$YPvVas_O28=_JTSfpHNV!CmtR+INHCUkNmq0nN5NmCxE6no^i zQuf1wjHA5$|9V&EEe4xri}NB66kYH6;Egm5e=P-baoKw?H#u!}!DP+WHESDHH+Fm3 zh}Bp?ct`^+L2Zfv%VDKj@Z9i?3Sxa;uD~#5cAhsJ36|xvFNLSO*WAD=c}h>Acm%?U zWgy2EW#uH`4tZaZNAzl>TDJu41RYMn|E4L%Bn^J9Ix!QP@T?!IvPL|A7v0}1TGJdq zSJ^Ep0~k7IgyhpRsw8EIHguCENB`ojt3dph{~=|9P+x!N@Yg_~6X#3u^*5hsL;I}*^{I*>mTTl*VVj^SlDUrWg7P}FMFL;IO|PT&*y zJpWEwEJ*^&3Fr-G{L<+umAVzwDc^pY%eAD>C^NQ(R~iU77akv|z+F>gugp}pi$iI# z4_>Q6`7anWF7}$K%s(VyJlBUSRf(dPwkPQ%&lc~hBTF~qMRhe{g^Fh{M+?RRM0MMB z1GUZxB`D8_*c4JSl5{9(vfwS|fBd`7^3aNez~JRmUVTkjPiSxJEkv%9#&w)KISeJ$DS2*yyW@n3Y zB~u69+2{lfnrY`)I^f@gd+UKPM&aieftK?&=t156ON01;5!|~Lxae3oh0(1luL+sr zCi~ohIj0(!w5)l@o6M43ppLAXLQP$gcweG;2ulNsB4#MsWRiZ(D8aU$X}xNplk)AN zH55uiR+##Wsb=vK&MjZ~N5_WMj=Y7*ck(%XzJof%eomij6G5}FQTC|H1un!B-R$GE zQ3S3W-8mV>3wT`bQIfs|`sUwN#gj3SbYcmZNq2AxwmQ=YT!>VGZ!7H!Dy_i-i31%U z{o4jND$3ac1@G$yN}|~I(^gSw)2%^JIeJx-l8efu_>h&|^mqBdxL+ zOwcBSbw=QL^XO#+DsoPB_RumyqqXC1%!}7m23kN_aDB=z70IPj7gu#a$V?&5jF2`~ zi2kh>pI`-trd%(z(4@n!9!GVhJ+WoGq=XhY9J->7UN5~9?g=PWarQoY6vdiZBgGQ#o6NM)a6)54N) zi5~kV43I`UJ@qasxZqF67B5WJ=M1M%3YbFB4BC(fByjB*NAm`%Cd>3ZV@gH}#In+o|n49XCe$)iDHBO1t1!uQ+MS2k#i8)a=?%~e9aDiQ2A>_4VZ z4wo8=wJ4L=rV--z>@s367y?>|cf>a&hYIs_*^kCFQkvF9;?k zO+bQ7B9@&PZJY(xYkP*O?O7zjYs%$;u!+VGbuC-emKZ=~?(9QiWOjEBZ6ATC4J$p` zvV_XrhpLO5a2XADjdQ`iV#Afk@0%Bu3OAc{vuk6p$c6XXpADGDM&VC_O`s*s;@WXT zwi_3XIqF{)y8;n^)#hwG7e~reKHn~zhm22sXU-wjL$>N+nbC#D^H_WeYzeB{bTCa- z>b_`85X-S86@`d=5w1d;+Z;}e_Ft)VOFUHB>1Aqk;9CE69@n?TGB$V1UlgeE7BSf0Auw6m){r3Lqmj*8G{=6C zDE@Zd6`(?C8ZBN_Ahtd_VOGJ&kix?~6STJ!Ve<72XUJV>omF)yN4#oN4si2@n?USP z0oImJj7mDGze70dGwk?>b)AG-%x3X|w1YY)gv3q#*7IU8^;pf|rBM&z$op@RJ1?{R z&wiD;_Xo51lFd8Qo?Jh^H|T^W=HnH-137|}3%$zEH+PJdr8L$mJkz0_I$6>0Cn?+b zqKhtPs6a2>0t;>ys+I6tp;)5%wM})+8PD$PtM379%g1ikwMSTc(cC5`zFcNhPdi@4 z8Fvk`cxM*uB^2rS&RDkJC@i1{v9w7eV67|TnoOc|3B=4c>*o6>ics;VMxjhpX;mX` zjnrk#!Joj=Xg32y2}7&(2&~1|2SeA7WU8Kx!sQr)3MU))o`-AUGkKKD+JI3$;w`5- z{f!}c?RN1Fiq|i$Ss6BM3hAcKDQxMU(zXhMoF3RwDn=Lb0_&OpiDZW-a(Nvf*$3vs zXo34iQPZ$vAM~UjlG>locjs&{x-%5mI#i6Vnh2v(Mhfvpw5h5RJ65r52^GH;@NeMszrEPI(%MMj zX|g^+=qUfT^@73vpb?@Qu}=sVR>i{r$hG9b4ZhPbwT+;!Pw4n}?1C)@<@e9o6Cb>A zM*3U zF6NC+56tw)n5@YdbUQvK+-eCRM7N^>>({SN=OhmW(!N_K4z01 zjVmLv&mOpR)XaBsvfxyxD)bK@F!CQ3U7?HVmdlXMwA0p0IRBS zvz^ETY^YYugx=%DhdGuE>CW5ejxQN^xnr5j$yJzFfmNn}+7`Hz&AjYFOQP{iDqj%< zwch-HNl3XkNIn_QoGOvg&O)y>{hpHoJ@C^8*2 zEmFY?;mFRZ`8`qkU3KH!@TVkyL9v=J%y=+An!VyAhOA{lFb+LP`p#_m+NppGNJuOb zvJZ2$6J8Le0;MZexa$=063;%}jKgoRz;7sz-K2o{80XvYbD|Gh&r|AAb|-P;W~Pgh zCvT-w?b<|8iSDMB4ESo+u21hJXsPQqkM!ZN?JSSRJraTpz9{U32n9q8fo?5vWW`z^ zhw|5-sG?YuxiX8cj^IxG{5EV52Q0)X%q4@sdMViwZBW4}Vw09&I^b8Akd_`bdmvc@ z>{-{fZpslwhuK#0?VR+b&|mI$15xr5gV%x=vZt%q1i^W!Fz))n{iBfEA>YacmUQ!F zg5H_D6s-(wPNq$1%g`Wh*~so)uNl=*u`~XHrdw`qYr<6qt#s$XZ(k|9*I(Nj{)qh{ zhCFLdoWAcRIGjD7CpHi1qx`IN-CCh?(vDjL92Ei|^9DNI-iKRhVWT|?aojeVYPda; zD^4uV;+9N=E%t(yq%FlSl@q=_o)NqKBBEI<9`%&ZDN(Mz-5X(%O-#pR=LDjTn#A@D zzWKe9P>!kcf|DVsS?O$*eea3``wd%%Q#-==B ziEE)#I;+}H12gQnk6nO~_~+A$3bP3zsk_NRD4z?A>cBe?S#fP}Hd(WJ!^M*#{;WuI zHULm8Y58a2fOf=3ifLf-N^(uW{y3Q0vv=X5Kbp~E!UZuhPPnmQFQiNV4zJl5&-lrx zm@W=w?GoX)EyVUpF`(+&Ea#MsmhH)u?yXi0t7d5HQ2e5h=>r&dA!p9uiBc2hxAR!n z9R{_Dhx`*l*1uQg$dJe%X4c$r#t5t1Zts>-@Yegb*~%9e!l}H|R{>D%Vv~P`N$?PL z+izh<4K7O5L(P_D|I62G0?63QaGeQ3M1q&*z15nU!6zl-kiPLa9UXo(bT;q0K8VZ|-e^W#ed$NBjr;7FG3u?XE1^v&U zpL(c3#nHOOVLUDTKN!>6@;N<{lz(pZXQvypjz9G1ja*3G8u-3VP&2*Q3TejaiZBZY zN9duDVP*(*e&h~~7TumkZFkZzBMcH|Y{VezIN!()lK~~6on*ouJ?B)IUZL3*F=%uPD%_V{3MS?M(eqP#?TkVfS^=A*SU+BEXThoGD zWgg?4c$)+~^i>M9*^I=qEG3uwIC$#AfBnG?`M;;$`K)BI& zBj(4t@p3#+y1vh}?%JDr;kW*RAiL1WYgK9+pZG2Qt3p;m9QU0ahiR&p`C0VEq z3hP4Uvja=Ds*ti90sN%6fytIBY-)$7s;hGeWVjmuY44}*69F}IM2aatYc{efYb zEQ#TjEXEzTW2{`=zK0*d*cB2b5Qe>td`qB3murJnlQyhwYD&>RLrD4; rSgUrCX!1W~yodgp8=TJdLRk}vfEaZOy{xuFe diff --git a/deprecated/source/odal-node-no-text.png b/deprecated/source/odal-node-no-text.png deleted file mode 100644 index 3993eaa1630c641c8951051afc5d20cb52e99f05..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11668 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∾7^ipgnEx+P0`sG%jeFYW5{RvtU!EW@7V~Y&s z);y92k^ZIgdOW_w(IfcMmPAlYA)~l!v?F!>2lBp{q7atafG=V1<3G4ieN(4co>*~x=K^G$dx_!g3)1F#SP|4lcEyT_-`=KWX zGVW=)r-Se}zQ0^@7%bnEo|}Mh{)-0@B)3mg{-|ioQE@My*l<|K73;!Fb^ySZjQtX zrJXZ`J}C}5gGA*3=Y^61)T`yzJJ?rmynYr5hTgyE@{HJo0+d|3a|q$rc@NaWuju9I zf!3!^{yx7%vBf4(p~99^<67f^3>%RstDHlXAl`1DQLHp~Pkl2sqS6zgx;zpFx4ycPWL5zqZx-D`VTCzgP{Omr5g0YOFo;+*%V>Ku+7n0R z<8w9xfx2B6>Mjy*Pw%v%r4eNs_Cl9}07QCjN8;*Dn4v2u71v8T61m-0gfXl8w9B_Dik#Id9fa?Mym$UHQ|;ymn-uFh=)JNX}NkqS;?BT!n<2PGV76v$p}6ZL`;N3HCbF>MF1OuH>zecmdeA z0M&9+Xv%@I0bXV!wndCW+#Sf7@$Z&y3u!B;O>96Kg`x7bvy@q}$NP`o=)kA2g4wB} zbFrddIGHo!%ob|EJ|wK6z&gcpjSG+++s7)kfYOEr(T?0Gb&B)>qz+{ogNssn;%{4a z$R2-u77S>-LC(BdzP#1f^nHQ;gcyHv&|a%OJ0lqw!`%|MHIv)txxo6m2%Zd2B|A4B zE(;W-p>J+zy&~<`on=_~?2i!n378d*Uagg*n+!aK5y?~ITwe#=Lg9HVJDti*2y5XO zOf7lUJr7FJ_nmVor}($hbwvMXW$OPs{_hAFV*M2}9eJ(0%WgdUZ$?2zMY>ATH0XZ- Di8cJJ diff --git a/deprecated/source/odal-node.png b/deprecated/source/odal-node.png deleted file mode 100644 index 2c38fbbd5a64993fa39fa890607b1be5d44816d8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5750 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 Date: Thu, 20 Aug 2026 21:33:37 +0200 Subject: [PATCH 011/168] ci: run the link, leakage and API-spec gates on every change, and scope the workflow token to read-only --- .github/workflows/ci.yml | 33 +++++++++++++++++++++++++++++++++ package.json | 4 ++++ scripts/check-leakage.mjs | 21 ++++++++++++++++++++- site/dpp-docs/package.json | 3 ++- 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1b8efbb..84694c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,6 +5,12 @@ on: branches: [main] pull_request: +# The job builds and inspects; it never writes to the repository. Without this +# block it would inherit whatever the repository default happens to be, which is +# the only privileged path in the file. +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest @@ -12,6 +18,16 @@ jobs: - name: Checkout uses: actions/checkout@v4 + # The vendored OpenAPI spec is a copy of the engine's. Without the source + # beside it there is nothing to compare against, and a drift check that + # cannot see the source is a check that always passes. + - name: Checkout dpp-engine (source of the vendored API spec) + uses: actions/checkout@v4 + with: + repository: odal-node/dpp-engine + path: .dpp-engine + persist-credentials: false + - name: Enable Corepack run: corepack enable @@ -29,3 +45,20 @@ jobs: - name: Check run: pnpm -r check + + # Markdown link targets are opaque strings to `astro check`. This reads + # the built output, so it sees what is actually published — including + # cross-site links, which neither site's own tooling can resolve. + - name: Check links + run: pnpm run check:links + + # This repository is public. Internal decision-record numbers and paths + # into the private docs repo must not appear in it — including inside + # `public/`, which is served verbatim. + - name: Check for internal-vocabulary leakage + run: pnpm run check:leakage + + - name: Check the vendored API spec against the engine + run: pnpm run check:openapi + env: + DPP_ENGINE_DIR: ${{ github.workspace }}/.dpp-engine diff --git a/package.json b/package.json index 2a4d075..f108ca2 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,10 @@ "build:docs": "pnpm --filter dpp-docs build", "build": "pnpm -r build", "check": "pnpm -r check", + "check:links": "node scripts/check-links.mjs", + "check:leakage": "node scripts/check-leakage.mjs", + "check:openapi": "pnpm --filter dpp-docs run check:openapi", + "sync:openapi": "pnpm --filter dpp-docs run sync:openapi", "preview:landing": "pnpm --filter dpp-landing preview", "preview:docs": "pnpm --filter dpp-docs preview", "clean": "pnpm -r exec rm -rf dist .astro node_modules && rm -rf node_modules" diff --git a/scripts/check-leakage.mjs b/scripts/check-leakage.mjs index ffac416..1d3765d 100644 --- a/scripts/check-leakage.mjs +++ b/scripts/check-leakage.mjs @@ -16,6 +16,7 @@ // // Usage: node scripts/check-leakage.mjs import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { execSync } from 'node:child_process'; import { join, relative, extname, basename } from 'node:path'; const SKIP_DIRS = new Set([ @@ -25,6 +26,7 @@ const SKIP_DIRS = new Set([ 'dist', '.astro', '.pnpm-store', + '.dpp-engine', 'deprecated', ]); // This file necessarily contains the patterns it searches for. @@ -79,8 +81,25 @@ for (const file of files) { } } +// The directories above are skipped because they are meant to be untracked. +// That assumption is worth testing: a stray `git add -A` on a branch that +// predates the .gitignore entry commits them to a public repository, and the +// content scan would never look. Ask git what is actually tracked. +const tracked = execSync('git ls-files', { encoding: 'utf8' }) + .split('\n') + .filter((p) => p.startsWith('deprecated/') || p.startsWith('.claude/')); + +if (tracked.length > 0) { + console.error(`leakage check: ${tracked.length} local-only file(s) are tracked.\n`); + for (const p of tracked.slice(0, 10)) console.error(` ${p}`); + if (tracked.length > 10) console.error(` … and ${tracked.length - 10} more`); + console.error('\n These directories are gitignored because they must not be published.'); + console.error(' Untrack them with `git rm -r --cached

` before committing.'); + process.exit(1); +} + if (hits.length === 0) { - console.log(`leakage check: ${files.length} files scanned, clean.`); + console.log(`leakage check: ${files.length} files scanned, clean. No local-only files tracked.`); process.exit(0); } diff --git a/site/dpp-docs/package.json b/site/dpp-docs/package.json index 685d367..92b0904 100644 --- a/site/dpp-docs/package.json +++ b/site/dpp-docs/package.json @@ -10,7 +10,8 @@ "build": "astro build", "preview": "astro preview", "check": "astro check", - "sync:openapi": "node scripts/sync-openapi.mjs" + "sync:openapi": "node scripts/sync-openapi.mjs", + "check:openapi": "node scripts/sync-openapi.mjs --check" }, "dependencies": { "@astrojs/starlight": "^0.30.6", From ff5e7bbd23dcea6043a64c73d6d51906db2be8e4 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Thu, 20 Aug 2026 21:46:28 +0200 Subject: [PATCH 012/168] docs: correct the regulatory citations, security-property claims, registry status and access vocabulary against primary text and engine source --- .../src/content/docs/core-concepts.mdx | 2 +- .../src/content/docs/core/security.mdx | 2 +- .../src/content/docs/engine/retention.mdx | 4 ++-- .../src/content/docs/engine/security.mdx | 6 ++--- .../what-odal-can-and-cannot-see.mdx | 6 ++--- .../src/content/docs/regulatory/battery.mdx | 8 +++---- .../docs/regulatory/central-registry.mdx | 8 ++++--- .../src/content/docs/regulatory/espr.mdx | 22 ++++++++++--------- site/dpp-landing/src/pages/index.astro | 10 ++++----- 9 files changed, 36 insertions(+), 32 deletions(-) diff --git a/site/dpp-docs/src/content/docs/core-concepts.mdx b/site/dpp-docs/src/content/docs/core-concepts.mdx index 1940ffc..a685422 100644 --- a/site/dpp-docs/src/content/docs/core-concepts.mdx +++ b/site/dpp-docs/src/content/docs/core-concepts.mdx @@ -11,7 +11,7 @@ Three ideas explain almost everything about how Odal Node is built and why. Unde The most important decision in the project is that the operator's raw source files never enter Odal's systems. The manufacturer validates a passport locally, signs it with their own key, and the node discards the import files it was built from. What the node keeps and serves is the signed passport itself — the product data the operator chose to publish, every field bound to a proof and filtered by the reader's access rights — not the raw exports, spreadsheets, or supply-chain detail behind it. -This is a property of the software, not a promise about our conduct. Because the node discards the source files after signing, the raw inputs behind a passport are gone — there is nothing for an operator, or for us, to hand over later. Verification needs only the signed passport and the manufacturer's public identity; it does not need Odal to be online, or to exist. +This is a property of the software, not a promise about our conduct. Because the node discards the source files after signing, the uploaded file itself is gone. What survives is what the import produced: the passport, and a job record of what validated and what did not. Verification needs only the signed passport and the manufacturer's public identity; it does not need Odal to be online, or to exist. A fuller treatment of the data boundary is in [What Odal Node can and cannot see](/getting-started/what-odal-can-and-cannot-see). diff --git a/site/dpp-docs/src/content/docs/core/security.mdx b/site/dpp-docs/src/content/docs/core/security.mdx index f73c97e..e498d26 100644 --- a/site/dpp-docs/src/content/docs/core/security.mdx +++ b/site/dpp-docs/src/content/docs/core/security.mdx @@ -34,7 +34,7 @@ The practical guarantee is simple: the encrypted key file on its own is useless ## Sector logic is isolated by construction -The rules that decide whether a product's data is compliant change sector by sector and regulation by regulation — so they are deliberately *not* part of the trusted core. Each sector's compliance logic is compiled to WebAssembly and runs inside a sandbox with no ambient capabilities: no filesystem, no network, no way to reach the host. Product data goes in as plain memory, a validation verdict comes out, and nothing else crosses the boundary. Execution is bounded too — a fixed memory cap and a metered CPU budget — so a plugin that misbehaves is stopped, not obeyed. And a plugin is signature-verified before it is ever loaded. +The rules that decide whether a product's data is compliant change sector by sector and regulation by regulation — so they are deliberately *not* part of the trusted core. Each sector's compliance logic is compiled to WebAssembly and runs inside a sandbox with a deliberately short capability list. There is no filesystem and no network: no file can be read or written, and no socket opened. What the plugin *can* do beyond computing on the data it is given is narrow and enumerated — write a log line, read a clock pinned to one instant for the whole invocation, and draw OS randomness. Nothing in that set reaches the signing key, the database, or any other passport. Execution is bounded too — a fixed memory cap and a metered CPU budget — so a plugin that misbehaves is stopped, not obeyed. And a plugin is signature-verified before it is ever loaded. What this buys is a hard separation between *judging* data and *vouching for* it: a buggy or even hostile sector rule can fail a validation, but it can never touch the signing key, alter a passport, or forge a proof. New regulations arrive as new sandboxed plugins — the cryptographic guarantees on this page never depend on trusting them. The concrete runtime limits are described in [Operating a node securely](/engine/security). diff --git a/site/dpp-docs/src/content/docs/engine/retention.mdx b/site/dpp-docs/src/content/docs/engine/retention.mdx index 6a874a6..cd1be88 100644 --- a/site/dpp-docs/src/content/docs/engine/retention.mdx +++ b/site/dpp-docs/src/content/docs/engine/retention.mdx @@ -3,7 +3,7 @@ title: Permanence & retention description: A published passport must stay accessible and unaltered for years. How a node guarantees permanence by construction, in line with ESPR Article 9. --- -ESPR Article 9 makes a published passport a long-lived obligation, not a transient record. Once a product is on the market, its passport has to stay accessible for the period its delegated act sets — at least ten years after end-of-life for batteries — and a backup has to exist with an independent, certified third party so the passport survives even if the operator does not. +ESPR makes a published passport a long-lived obligation, not a transient record. Once a product is on the market, Art. 9(2)(i) requires its passport to stay available for a period the delegated act sets, "at least the expected lifetime of a specific product" — ESPR itself fixes no number. And under Art. 10(4) the operator must make a **back-up copy** available through a "digital product passport service provider", so the passport survives even if the operator does not. A node is built so those guarantees hold by construction, not by good behaviour. Permanence is not a policy an operator opts into; it is a property of how the software works. @@ -31,7 +31,7 @@ The QR code on a product encodes a resolver address that has to keep working for ## The independent backup -Article 9's second requirement — a backup held by a certified third party, so a passport survives an operator's insolvency or shutdown — is the operator's obligation. The [proof-bound model](/getting-started/what-odal-can-and-cannot-see) makes it tractable: what has to be preserved is the signed passport and its history, not a sprawling production dataset. The registry-backed mechanisms that support discovery and continuity are modelled and waiting on the upstream specification — see [EU Central Registry](/regulatory/central-registry). +Article 10(4)'s back-up copy — held by a digital product passport service provider, so a passport survives an operator's insolvency or shutdown — is the operator's obligation, and Art. 11(e) requires availability to continue through insolvency, liquidation or cessation of activity. The [proof-bound model](/getting-started/what-odal-can-and-cannot-see) makes it tractable: what has to be preserved is the signed passport and its history, not a sprawling production dataset. How this meets the registry is described under [EU Central Registry](/regulatory/central-registry). ## Read next diff --git a/site/dpp-docs/src/content/docs/engine/security.mdx b/site/dpp-docs/src/content/docs/engine/security.mdx index 416b4fb..2212a2b 100644 --- a/site/dpp-docs/src/content/docs/engine/security.mdx +++ b/site/dpp-docs/src/content/docs/engine/security.mdx @@ -21,7 +21,7 @@ The public passport endpoint requires no credential at all — it only ever serv ## Least privilege in the database -The node connects to PostgreSQL with an application role that cannot change the schema and cannot delete rows. Schema migrations run under a *separate*, privileged credential that is used only at startup and never kept in the live connection pool. On top of that, the database enforces the [permanence guarantees](/engine/retention) itself — a trigger rejects edits to a locked passport, and the audit trail is append-only at the database level — so those rules hold even if application code is wrong. +The node connects to PostgreSQL with an application role that cannot change the schema, and that holds no DELETE grant anywhere except on the import-job table, where a cleanup sweep needs one. Schema migrations run under a *separate*, privileged credential that is used only at startup and never kept in the live connection pool. On top of that, the database enforces the [permanence guarantees](/engine/retention) itself — a trigger rejects edits to a locked passport, and the audit trail is append-only at the database level — so those rules hold even if application code is wrong. ## The node fails closed @@ -29,7 +29,7 @@ A node refuses to start without its secrets — database credentials, the key-st ## Sector logic is sandboxed -Each sector's compliance logic runs in a Wasm sandbox with no filesystem, no network, and no system clock or randomness, capped at 64 MiB of memory and a fixed CPU budget (fuel metering). A buggy or hostile plugin exhausts its budget and is stopped — it can never reach the signing key, the database, or the rest of the node. This is the host enforcing, with concrete limits, the sandbox model described in [Security & cryptography](/core/security). +Each sector's compliance logic runs in a Wasm sandbox with no filesystem and no network, capped at 64 MiB of memory and a fixed CPU budget (fuel metering). It is not a vacuum: the plugin can read a clock and draw randomness. The clock is **pinned** to a single instant for the whole invocation, so a determination cannot vary with the time it ran; randomness is real OS entropy, deliberately not pinned, because a fixed seed would make a plugin's hash iteration order predictable without making any determination more reproducible. A buggy or hostile plugin exhausts its budget and is stopped — it can never reach the signing key, the database, or the rest of the node. This is the host enforcing, with concrete limits, the sandbox model described in [Security & cryptography](/core/security). ## The database is the source of truth @@ -41,7 +41,7 @@ A node serves exactly one operator. Isolation is a property of the deployment ## Reporting a vulnerability -The engine implements no cryptography of its own — it delegates all of it to the core. Dependencies are scanned for known advisories on every change. Suspected vulnerabilities go to **security@odal-node.io** under coordinated disclosure, never to a public issue first. +The signing path is the core's: the engine holds no signature or key-derivation code of its own. It does hash on its own account — SHA-256 for API-key and admin-password digests, compared in constant time — because those are node-operational concerns rather than passport ones. Dependencies are scanned for known advisories on every change. Suspected vulnerabilities go to **security@odal-node.io** under coordinated disclosure, never to a public issue first. ## Read next diff --git a/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx b/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx index 2aade76..8476520 100644 --- a/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx +++ b/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx @@ -9,7 +9,7 @@ The proof-bound architecture means the raw import files are read once on the ope | Property | Self-hosted | Managed (Future) | |---|---|---| -| Node discards raw import files; retains the signed passport (all tiers) | Yes — architectural invariant | Yes — architectural invariant | +| Node discards raw import files; retains the whole signed passport | Yes — architectural invariant | Yes — architectural invariant | | Odal (the entity) can access stored data | No — not present in the deployment | Constrained by access controls, audit logging, and contract | | Odal can sign on the operator's behalf | No | No — the operator holds the signing keys | @@ -29,14 +29,14 @@ The signed passport published to a resolver we operate, and the metadata require ## What we could see but do not -The contents of your import files. The software reads them once, validates the data, signs the passport, and discards the input. There is no setting, configuration, or internal code path that retains the raw import after signing — it is not a choice made per customer; it is how the software works. +The contents of your import files. The software reads them once, validates the data, signs the passport, and discards the input. There is no setting, configuration, or internal code path that retains the uploaded file after signing — it is not a choice made per customer; it is how the software works. Be precise about what that covers: the *file* is not kept, while records *derived* from it during the import — the passport, and a job record carrying per-row validation findings — are. ## The mechanism 1. **Import** — product data arrives at your node (CSV, Excel, ERP export) on infrastructure you control. 2. **Validate** — locally against versioned sector schemas. Validation is a pure function — no network calls. 3. **Sign** — your Ed25519 private key, generated and held in-process, signs the validated passport into a JWS bound to your `did:web` identity. -4. **Publish** — the signed passport becomes resolvable; the raw import files are discarded. Public fields are served to anyone, restricted tiers only against a verified credential. +4. **Publish** — the signed passport becomes resolvable; the raw import files are discarded. Public fields are served to anyone; restricted fields only against a verified credential. 5. **Verify** — anyone verifies against your public DID Document. Odal is not in the verify loop. ## Read next diff --git a/site/dpp-docs/src/content/docs/regulatory/battery.mdx b/site/dpp-docs/src/content/docs/regulatory/battery.mdx index a2eb2ac..d8deec5 100644 --- a/site/dpp-docs/src/content/docs/regulatory/battery.mdx +++ b/site/dpp-docs/src/content/docs/regulatory/battery.mdx @@ -3,7 +3,7 @@ title: Battery DPP description: The EU Battery Regulation (EU 2023/1542) — what a battery passport carries under Annex XIII, the supply-chain due-diligence record, and which checks are binding today. --- -The Battery Regulation (EU 2023/1542) is the first product-group regulation that mandates a Digital Product Passport in production. It applies to portable, industrial, light-means-of-transport (LMT), electric-vehicle (EV), and stationary energy storage batteries. The regulation is already in force, and the staged compliance dates have begun passing their first checkpoints. +The Battery Regulation (EU 2023/1542) is the first product-group regulation that mandates a Digital Product Passport in production. Article 1(3) applies it to five categories of battery, and only five: portable, starting-lighting-ignition (SLI), light means of transport (LMT), electric vehicle (EV) and industrial. The set is closed deliberately — where a battery could fall under more than one, the strictest requirements apply, and that tie-break only works over a closed set. Stationary energy-storage systems are not a sixth category; the Regulation places them inside industrial. The regulation is already in force, and the staged compliance dates have begun passing their first checkpoints. ## What the regulation requires @@ -23,9 +23,9 @@ A battery passport records the Annex XIII fields the regulation requires, with t The Battery Regulation is in force, but its requirements switch on in stages, and Odal validates only what is actually binding: -- **Enforced now** — the long-standing bans on mercury and cadmium (carried forward from the Batteries Directive, in force since 2008), and cross-field coherence checks such as a battery's operating-temperature range being internally consistent. -- **Declared now, binding later** — the Annex X minimum recycled-content shares for cobalt, lithium, nickel, and lead. These are finalised law but do not take effect until 2031, rising again in 2036, so today they are surfaced as advisory rather than as a pass/fail verdict. -- **Awaiting a delegated act** — minimum state-of-health thresholds (Article 10) and the A–E carbon-footprint class (Article 7), whose methodologies the Commission has not yet adopted. +- **Enforced now** — the restrictions on mercury and cadmium in Art. 6 and Annex I, and cross-field coherence checks such as a battery's operating-temperature range being internally consistent. +- **Declared now, binding later** — the minimum recycled-content shares for cobalt, lithium, nickel and lead, set by Art. 8(2) and 8(3) and cross-referenced to Annex VIII. (Annex X is the due-diligence raw-materials list, and has nothing to do with recycled content.) These are finalised law but do not take effect until 2031, rising again in 2036, so today they are surfaced as advisory rather than as a pass/fail verdict. +- **Awaiting a delegated act** — minimum electrochemical performance and durability values, empowered by Art. 10(5), and the carbon-footprint performance class under Art. 7(2). Art. 7(2) requires the label to declare a class but defers the classes themselves to an act that has not been adopted, and requires the scale to be revised every three years — so neither the labels nor how many there are is settled, and we do not guess at them. The passport itself becomes mandatory on **18 February 2027**. As each later requirement takes effect, the battery sector's checks extend to cover it, and passports already issued stay valid. The reasoning behind issuing a binding verdict only where the law binds is the [honesty model](/regulatory/espr) every sector follows. diff --git a/site/dpp-docs/src/content/docs/regulatory/central-registry.mdx b/site/dpp-docs/src/content/docs/regulatory/central-registry.mdx index c2b5e19..1c43ce7 100644 --- a/site/dpp-docs/src/content/docs/regulatory/central-registry.mdx +++ b/site/dpp-docs/src/content/docs/regulatory/central-registry.mdx @@ -15,12 +15,14 @@ This design is the right one because it lets the regulation centralise discovery ## What the registry requires of implementations -The registry's API is being defined at the time of writing. The expected requirements are: every published passport must be registered with the registry, the registration carries the product identifier and the resolver endpoint, the issuing platform must keep the registration in sync as passports are suspended or archived, and the platform must respond to registry health checks. +The registry went live on **20 July 2026**, meeting the Commission's own Art. 13 deadline, and the implementing act adopted under Art. 13(5) sets out how registration works. So the requirements are no longer expectations: a published passport is registered with its unique identifier, the registration carries the resolver endpoint, and the registering platform keeps it in sync as passports are suspended or archived. -Odal has prepared for this beyond modelling the interface: **every published passport already commits its registration intent to a durable outbox, in the same database transaction as the publish itself.** A background worker drains that outbox with retry and backoff. The practical consequence: publishing never blocks on the registry, a crash never loses a registration, and the day the Commission's API goes live, the accumulated backlog registers without a single passport falling through. (For context: the Commission's own deadline to set up the registry was 19 July 2026; as of this writing, the API specification remains unpublished — so this durable-queue posture is not caution, it's the correct engineering for the actual situation.) +Odal has prepared for this beyond modelling the interface: **every published passport already commits its registration intent to a durable outbox, in the same database transaction as the publish itself.** A background worker drains that outbox with retry and backoff. The practical consequence: publishing never blocks on the registry, a crash never loses a registration, and a backlog accumulated during an outage drains without a single passport falling through. + +Two things stand between that and a battery registration succeeding today, and neither is ours to fix. The semantic catalogue a battery registration must reference is not yet defined, which blocks the registration path for everyone rather than for us specifically. And the registry caps a unique product identifier at 50 characters where ours runs to 65, with the identifier required to be a URL conforming to standards that must be purchased before that constraint can even be read properly. We would rather name both than describe a clean drain we cannot yet demonstrate. ## Why pre-investing in the bridge diff --git a/site/dpp-docs/src/content/docs/regulatory/espr.mdx b/site/dpp-docs/src/content/docs/regulatory/espr.mdx index 37758df..2ec2858 100644 --- a/site/dpp-docs/src/content/docs/regulatory/espr.mdx +++ b/site/dpp-docs/src/content/docs/regulatory/espr.mdx @@ -5,11 +5,11 @@ description: The Ecodesign for Sustainable Products Regulation — what it is, w import { Aside } from "@astrojs/starlight/components"; -The Ecodesign for Sustainable Products Regulation (ESPR, EU 2024/1781) is the framework regulation that establishes the Digital Product Passport requirement across the European Union. It entered into force in 2024, and the first product-group delegated acts started landing immediately. By 2027, every product placed on the EU market that falls under a delegated act needs a machine-readable DPP. +The Ecodesign for Sustainable Products Regulation (ESPR, EU 2024/1781) is the framework regulation that establishes the Digital Product Passport requirement across the European Union. It entered into force in 2024. **No delegated act under Article 4 has yet been adopted for any product group**, so ESPR itself binds nobody to a passport today — the framework is in force and the obligations that hang off it are not. The dated passport obligations that do exist come from other instruments: the Battery Regulation from 18 February 2027, and the Construction Products Regulation from 8 January 2027. ## What ESPR requires -The regulation establishes three core obligations that shape every implementation. The first is the **passport itself** — every regulated product must have a machine-readable Digital Product Passport accessible via a data carrier (typically a QR code) on the product. The second is **regulatory access control** — the passport must distinguish between public information, information available to authorised actors, and private information, with cryptographic enforcement of the boundaries. The third is **retention** — the passport must remain accessible for the lifetime of the product plus a regulator-defined retention horizon (10 to 20 years depending on sector). +The regulation establishes three core obligations that shape every implementation. The first is the **passport itself** — every regulated product must have a machine-readable Digital Product Passport accessible via a data carrier (typically a QR code) on the product. The second is **access control** — Art. 11(b) requires that a long list of actors, from customers to recyclers to market-surveillance authorities, have access **free of charge** "based on their respective access rights set out in the applicable delegated act". ESPR states no tiers and assigns no actor to any data; that mapping is delegated per product group under Art. 9(2)(f), and it prescribes no enforcement mechanism. The third is **retention** — Art. 9(2)(i) requires the passport to stay available for "at least the expected lifetime of a specific product", and sets no figure. Where a figure exists it comes from the product regulation: ten years for toys, detergents and the construction operator's own duty, and twenty-five years for the construction passport *system*. ## What ESPR does not specify @@ -21,13 +21,13 @@ The implications for the implementation are that the platform has to track sever Odal covers the articles of ESPR that are technically substantive for a passport implementation: -**Articles 8 and 9** — the format of the passport and its data carrier: the passport itself is the format, and GS1 Digital Link is how a scan resolves to it. +**Articles 8, 9 and 12** — what a delegated act must specify about the passport, and the identifiers behind it. Art. 9(2) fixes the act's content, including who may read and write which data and how long the passport stays available; **Art. 12 and Annex III** define the unique product, operator and facility identifiers. GS1 Digital Link is how a scan resolves to the passport. **Articles 10 and 11** — the passport's requirements and its technical design. Art. 11(b) is the access provision: readers get access "based on their respective access rights set out in the applicable delegated act", **free of charge**, with the actor-to-data mapping delegated to each product group under Art. 9(2)(f). ESPR fixes no access tiers of its own, and no product-group act has been adopted yet. **Transfer of responsibility** — when a product changes economic operator along the supply chain, Odal implements a dual-signature chain: both transferor and transferee sign, creating a verifiable chain of custody. One honest note: ESPR has **no single article** establishing transfer mechanics — the closest operative text is Art. 11(e) (passport continuity when an operator ceases activity). Our handshake is an engineering choice that satisfies and exceeds that continuity duty; we say so rather than inventing a citation. -**Article 13** — the EU Central Registry: the Commission-run directory of registered passports (its API specification is still unpublished). Public access to the passport itself is served by the resolver — the public view, cached to stay fast under load. +**Article 13** — the EU Central Registry: the Commission-run directory of registered passports, which went live on 20 July 2026. Public access to the passport itself is served by the resolver — the public view, cached to stay fast under load. **Articles 24 & 25** — the disclosure duty for discarded unsold consumer products (Art. 24) and the destruction ban for unsold textiles and footwear (Art. 25, Annex VII — in force for large companies since 19 July 2026). Odal carries a dedicated unsold-goods passport variant that records the disposal pathway as a verifiable record. @@ -45,9 +45,11 @@ ESPR is a framework; the binding detail arrives through per-product-group delega | **Textile** | ESPR delegated act (in drafting) | Structural validation now; unsold-goods ban (Art. 25) applies from 2026 | | **Electronics** | EU 2023/1670 + 2023/1669 — ecodesign and energy labelling, not ESPR | In force since 20 Jun 2025 for smartphones, other mobile phones, cordless phones and tablets | | **Steel · Aluminium** | CBAM 2023/956 / ESPR | Reference benchmarks only; no DPP mandate yet (aluminium expected ~2030) | -| **Construction** | CPR 2024/3110 | Awaiting delegated acts (2028–2032) | -| **Toys** | EU 2025/2509 | Awaiting delegated act (~2030) | -| **Furniture · Detergent · Tyre** | ESPR | Seams in place; awaiting delegated acts | +| **Construction** | CPR 2024/3110 | In force; passport applies from 8 Jan 2027. Operator owes 10 years of availability, the passport *system* 25 | +| **Toys** | EU 2025/2509 | In force; passport applies from 1 Aug 2030 — a date set by the Toy Safety Regulation itself, not by an awaited act | +| **Detergent** | EU 2026/405 | In force; passport applies from 23 Sep 2029 | +| **Tyre** | Tyre labelling 2020/740 / ESPR | Labelling in force; no DPP mandate yet | +| **Furniture** | ESPR | Seam in place; awaiting a delegated act | Battery and textile have dedicated pages below; the rest run on the same mechanism, waiting on their regulation. @@ -55,11 +57,11 @@ Battery and textile have dedicated pages below; the rest run on the same mechani Within any sector, some obligations are in force while others wait on a delegated act, and Odal is deliberate about not overstating what it can certify. Every passport is checked for structural and cross-field validity — that runs for every sector, in force or not. But a *binding* determination — a verdict of compliant or non-compliant — is issued only where the underlying obligation actually binds. Where it does not, the result is recorded as **not assessed** rather than guessed, and the passport carries that honestly. -As each delegated act takes effect, the matching sector's determination switches on, with no change to passports already issued. This is why a battery's banned-substance limits are enforced today while its 2031 recycled-content minima are surfaced only as advisory until they bind. The system would rather say *not yet assessed* than assert a compliance claim the law does not yet support. +When a delegated act takes effect, the matching sector's determination is switched on by a maintainer — the gate reads a status recorded in the sector's manifest, so it changes when a new version of the software is released, not on the date the law changes. Passports already issued are unaffected either way. This is why a battery's banned-substance limits are enforced today while its 2031 recycled-content minima are surfaced only as advisory until they bind. The system would rather say *not yet assessed* than assert a compliance claim the law does not yet support. ## Read next -[Battery DPP](/regulatory/battery) — the battery-sector delegated act in detail. -[Textile DPP](/regulatory/textile) — the textile-sector delegated act in detail. +[Battery DPP](/regulatory/battery) — Regulation (EU) 2023/1542 in detail. +[Textile DPP](/regulatory/textile) — the textile act still in drafting, and the unsold-goods ban that is already in force. [Access Control](/regulatory/access-control) — who may read what, and where the rules actually live. [EU Central Registry](/regulatory/central-registry) — what the central registry is and what it is not. diff --git a/site/dpp-landing/src/pages/index.astro b/site/dpp-landing/src/pages/index.astro index 9b98635..36686f2 100644 --- a/site/dpp-landing/src/pages/index.astro +++ b/site/dpp-landing/src/pages/index.astro @@ -1,7 +1,7 @@ --- // odal-node.io — home page. // Section order and copy are fixed deliberately; see each section component. -// Terminology and claims rules: see §7 (no capability-tier language, no claims +// Terminology and claims rules: no capability-tier language, no claims // ahead of shipped code, "proof-bound" never "no-touch"). import DeadlineCards from "../components/DeadlineCards.astro"; import Hero from "../components/Hero.astro"; @@ -77,8 +77,8 @@ const steps = [ trust. A proof-bound architecture serves both without asking either to compromise: the raw import files are read once and discarded, while the signed passport they produce — carrying the full product data, every field bound to a - proof — is what the node stores and serves, released to each audience by access - tier. + proof — is what the node stores and serves, released to each audience by that + audience's access rights.

    @@ -100,9 +100,9 @@ const steps = [

Anyone verifies the signature against your public DID document — without Odal in - the loop. Public fields are readable by all; restricted tiers — for repairers, + the loop. Public fields are readable by all; restricted fields — for repairers, recyclers, and market-surveillance authorities — are released only against a - verified credential. What the node stores: the signed passport with every tier, + verified credential. What the node stores: the whole signed passport, the audit trail, the signature. What it never stores: raw import files, supply-chain detail beyond passport content, or your private keys.

From 3a8a71c9f0b8f197e4b3603d14a9d5e6ed88869c Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Thu, 20 Aug 2026 21:59:05 +0200 Subject: [PATCH 013/168] docs: publish licensing without a restated dependency-licence table, retract the unverified standards-conformance claim, and correct the remaining landing and roadmap claims --- pnpm-lock.yaml | 10 ------- public/brand/favicon.svg | 1 - site/dpp-docs/README.md | 2 +- site/dpp-docs/astro.config.mjs | 11 ++------ .../src/content/docs/core/standards.mdx | 8 +++++- .../engine/{_licensing.mdx => licensing.mdx} | 23 +++++++++------- .../src/content/docs/regulatory/espr.mdx | 2 +- site/dpp-docs/src/pages/api.astro | 12 ++++++--- .../src/scripts/mount-api-reference.ts | 26 ++++++------------- site/dpp-landing/README.md | 2 +- site/dpp-landing/astro.config.mjs | 2 -- site/dpp-landing/package.json | 1 - .../src/components/GetStarted.astro | 6 ++--- site/dpp-landing/src/data/deadlines.json | 8 +++--- site/dpp-landing/src/data/roadmap.json | 6 ++--- site/dpp-landing/src/pages/_about.astro | 4 +-- site/dpp-landing/src/pages/_contact.astro | 7 ++--- site/dpp-landing/src/pages/index.astro | 6 ++--- site/dpp-landing/src/pages/privacy.astro | 1 + 19 files changed, 61 insertions(+), 77 deletions(-) delete mode 100644 public/brand/favicon.svg rename site/dpp-docs/src/content/docs/engine/{_licensing.mdx => licensing.mdx} (72%) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b248f7..01ba75c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -59,9 +59,6 @@ importers: '@astrojs/check': specifier: ^0.9 version: 0.9.9(prettier@3.8.3)(typescript@5.9.3) - '@types/react': - specifier: ^19.2.16 - version: 19.2.16 typescript: specifier: ^5 version: 5.9.3 @@ -1276,9 +1273,6 @@ packages: '@types/node@24.12.4': resolution: {integrity: sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==} - '@types/react@19.2.16': - resolution: {integrity: sha512-esJiCAnl0kfpNdE69f3So4WJUXy95dLZydX0KwK46riIHDzHM7O9Vtf9xCHW0PXIqvgqNrswl522kA/5yx+F4w==} - '@types/sax@1.2.7': resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} @@ -4575,10 +4569,6 @@ snapshots: dependencies: undici-types: 7.16.0 - '@types/react@19.2.16': - dependencies: - csstype: 3.2.3 - '@types/sax@1.2.7': dependencies: '@types/node': 24.12.4 diff --git a/public/brand/favicon.svg b/public/brand/favicon.svg deleted file mode 100644 index f0ee6d1..0000000 --- a/public/brand/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/site/dpp-docs/README.md b/site/dpp-docs/README.md index 3e42816..b21d898 100644 --- a/site/dpp-docs/README.md +++ b/site/dpp-docs/README.md @@ -39,7 +39,7 @@ site/dpp-docs/ │ └── custom.css # Starlight overrides via @odal/brand-tokens ``` -Brand assets shared with the landing site come from the workspace-root `../../public/brand/`, copied into the build by `viteStaticCopy` (see `astro.config.mjs`). +Brand assets live in this site’s own `public/` and `src/assets/`. There is no shared asset directory — the two sites are deployed independently, and a cross-site copy step was removed because it published a duplicate favicon at a path nothing referenced. The sidebar structure is declared in `astro.config.mjs` and mirrored by the file-system layout under `src/content/docs/`. Renamed or removed slugs keep a redirect (e.g. `/design/proof-bound` → `/getting-started/what-odal-can-and-cannot-see`); the full redirect map is in `astro.config.mjs`, which is the source of truth for the information architecture. diff --git a/site/dpp-docs/astro.config.mjs b/site/dpp-docs/astro.config.mjs index c73a2c0..03cb345 100644 --- a/site/dpp-docs/astro.config.mjs +++ b/site/dpp-docs/astro.config.mjs @@ -1,6 +1,5 @@ import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; -import { viteStaticCopy } from 'vite-plugin-static-copy'; export default defineConfig({ site: 'https://docs.odal-node.io', @@ -9,10 +8,7 @@ export default defineConfig({ // Design pages removed; redirect to the closest living equivalent. '/design/no-touch-data': '/getting-started/what-odal-can-and-cannot-see', '/design/proof-bound': '/getting-started/what-odal-can-and-cannot-see', - // Points at the licence table rather than at /engine/licensing, whose - // source is underscore-prefixed and therefore never routed. Repoint it back - // when that page is published. - '/design/open-core': '/introduction', + '/design/open-core': '/engine/licensing', '/design/adr': '/core-concepts', '/design/why-no-capability-gating': '/core-concepts', // Core consolidated from per-crate/type pages into three concept pages. @@ -82,7 +78,7 @@ export default defineConfig({ { label: 'Operating securely', link: '/engine/security' }, { label: 'Self-Hosting', link: '/engine/self-hosted' }, { label: 'The CLI', link: '/engine/cli' }, - // { label: 'Licensing', link: '/engine/licensing' }, + { label: 'Licensing', link: '/engine/licensing' }, ], }, { @@ -108,8 +104,5 @@ export default defineConfig({ customCss: ['./src/styles/custom.css'], }), ], - vite: { - plugins: [viteStaticCopy({ targets: [{ src: '../../public/brand', dest: '' }] })], - }, output: 'static', }); diff --git a/site/dpp-docs/src/content/docs/core/standards.mdx b/site/dpp-docs/src/content/docs/core/standards.mdx index 615e877..d9ebee5 100644 --- a/site/dpp-docs/src/content/docs/core/standards.mdx +++ b/site/dpp-docs/src/content/docs/core/standards.mdx @@ -13,7 +13,13 @@ A passport is only useful if the rest of the world can read it. The core speaks ## The European system standards, tracked clause by clause -The European DPP system standards — written by CEN/CENELEC JTC 24 — are landing in stages. Six were published in May 2026 — **EN 18216, 18219, 18220, 18221, 18222, 18223** — covering exchange, identifiers, data carriers, storage, lifecycle APIs, and interoperability. The remaining two — **EN 18239** (access rights, security, business confidentiality) and **EN 18246** (data authentication) — are at FprEN stage, expected around September 2026. Odal tracks them clause by clause in a maintained conformance matrix, and the core's identifier, carrier, API, and authentication semantics align to the published texts. One honest nuance: even a published standard is *available* but not yet *harmonised* (cited in the Official Journal) — so we build to them today and claim presumption of conformity only when that citation lands. +The European DPP system standards — written by CEN/CENELEC JTC 24, in the EN 182xx series — are landing in stages, covering exchange, identifiers, data carriers, storage, lifecycle APIs, interoperability, access rights and data authentication. + +We are **tracking** that series, not claiming conformance to it, and the distinction is deliberate. These are paid standards and we have not purchased the texts. Nobody here has read them, so there is no clause-by-clause matrix and no basis on which we could assert that our identifier, carrier, API or authentication semantics align with what they actually say. We would rather state that plainly than describe an alignment nobody could check. + +What we have built to instead are the open, published specifications those standards draw on — GS1 Digital Link, W3C Verifiable Credentials, `did:web`, the IDTA Asset Administration Shell — all of which are readable by anyone evaluating this claim. When the EN texts are purchased and read, this page will say what was found, including where it disagrees with what we built. + +One further nuance that will still apply then: even a published standard is *available* but not yet *harmonised* (cited in the Official Journal), so presumption of conformity is a claim for the day that citation lands, not before. ## Read next diff --git a/site/dpp-docs/src/content/docs/engine/_licensing.mdx b/site/dpp-docs/src/content/docs/engine/licensing.mdx similarity index 72% rename from site/dpp-docs/src/content/docs/engine/_licensing.mdx rename to site/dpp-docs/src/content/docs/engine/licensing.mdx index 2a80d0a..e114234 100644 --- a/site/dpp-docs/src/content/docs/engine/_licensing.mdx +++ b/site/dpp-docs/src/content/docs/engine/licensing.mdx @@ -34,16 +34,19 @@ Each `dpp-engine` release ships with a specific change date in its licence heade ## Dependency licences -`dpp-engine` builds on `dpp-core` (Apache-2.0) and other open-source dependencies. Each crate's `Cargo.toml` declares its full dependency tree. The key third-party licences are: - -- **Axum / Tokio** — MIT (HTTP framework and async runtime). -- **wasmtime** — Apache-2.0 (the sector-plugin sandbox). -- **ed25519-dalek** — BSD 3-Clause (signing). -- **PostgreSQL** — PostgreSQL Licence (liberal, BSD-style). -- **Redis** — BSD 3-Clause (resolver cache). -- **NATS** — Apache-2.0 (event bus). - -No dependency introduces a copyleft obligation that would propagate to `dpp-engine` users. +`dpp-engine` builds on `dpp-core` (Apache-2.0) and a tree of open-source Rust crates. + +This page deliberately does **not** reproduce a table of those licences. A restated licence +list is a claim that goes stale silently: the licence lives in the dependency's own metadata, +changes on the dependency's schedule rather than ours, and a copy here would keep asserting the +old terms long after they changed. That is not hypothetical — a previous version of this page +carried a licence for one component that had been wrong since 2024. + +The authoritative answer for any given build is the build itself. `cargo license` or +`cargo deny` over the workspace enumerates every crate and its licence from the resolved +dependency graph, and `Cargo.lock` is committed, so the result is reproducible for any tagged +release. Anyone evaluating the engine for licence compatibility should run it against the +version they intend to deploy rather than trust a list written against some earlier one. ## Read next diff --git a/site/dpp-docs/src/content/docs/regulatory/espr.mdx b/site/dpp-docs/src/content/docs/regulatory/espr.mdx index 2ec2858..2159bcb 100644 --- a/site/dpp-docs/src/content/docs/regulatory/espr.mdx +++ b/site/dpp-docs/src/content/docs/regulatory/espr.mdx @@ -32,7 +32,7 @@ Odal covers the articles of ESPR that are technically substantive for a passport **Articles 24 & 25** — the disclosure duty for discarded unsold consumer products (Art. 24) and the destruction ban for unsold textiles and footwear (Art. 25, Annex VII — in force for large companies since 19 July 2026). Odal carries a dedicated unsold-goods passport variant that records the disposal pathway as a verifiable record. ## Which sectors are covered diff --git a/site/dpp-docs/src/pages/api.astro b/site/dpp-docs/src/pages/api.astro index b676eea..d9b2d64 100644 --- a/site/dpp-docs/src/pages/api.astro +++ b/site/dpp-docs/src/pages/api.astro @@ -19,8 +19,10 @@ import logoDark from '../assets/logo-dark.svg'; // Same alpha banner Starlight renders on every docs page (content.config.ts // schema default) and the same shared CSS (banner.css) — this page skips the -// Starlight layout entirely so neither comes for free here. +// Starlight layout entirely so neither comes for free here. The text is +// imported from site-meta so the two banners cannot drift apart. import '../styles/banner.css'; +import { ALPHA_BANNER_TEXT } from '../site-meta'; // Full-page OpenAPI reference, mounted client-side from the **bundled** // @scalar/api-reference — no CDN, no third-party runtime dependency, so the @@ -79,9 +81,11 @@ import '../styles/banner.css';
- + +
{ALPHA_BANNER_TEXT}
From 2ef70dfa2fb8715325cf81361864d73de2b600d0 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Sat, 19 Sep 2026 13:03:01 +0200 Subject: [PATCH 032/168] feat(landing): add the lifecycle, proposed home and product-group pages --- site/dpp-landing/astro.config.mjs | 12 +- site/dpp-landing/package.json | 4 +- site/dpp-landing/product-groups-source.json | 7 + .../scripts/sync-product-groups.mjs | 148 +++++++++++++++ site/dpp-landing/src/components/Nav.astro | 1 + .../src/data/product-groups/aluminium.json | 13 ++ .../src/data/product-groups/battery.json | 41 ++++ .../src/data/product-groups/construction.json | 21 +++ .../src/data/product-groups/detergent.json | 17 ++ .../src/data/product-groups/electronics.json | 26 +++ .../src/data/product-groups/furniture.json | 23 +++ .../src/data/product-groups/mattress.json | 15 ++ .../src/data/product-groups/steel.json | 17 ++ .../src/data/product-groups/textile.json | 19 ++ .../src/data/product-groups/toy.json | 15 ++ .../src/data/product-groups/tyre.json | 15 ++ .../src/data/product-groups/unsold-goods.json | 12 ++ site/dpp-landing/src/data/stations.json | 81 ++++++++ site/dpp-landing/src/pages/lifecycle.astro | 104 +++++++++++ site/dpp-landing/src/pages/main.astro | 115 ++++++++++++ .../src/pages/product-groups/[key].astro | 175 ++++++++++++++++++ 21 files changed, 879 insertions(+), 2 deletions(-) create mode 100644 site/dpp-landing/product-groups-source.json create mode 100644 site/dpp-landing/scripts/sync-product-groups.mjs create mode 100644 site/dpp-landing/src/data/product-groups/aluminium.json create mode 100644 site/dpp-landing/src/data/product-groups/battery.json create mode 100644 site/dpp-landing/src/data/product-groups/construction.json create mode 100644 site/dpp-landing/src/data/product-groups/detergent.json create mode 100644 site/dpp-landing/src/data/product-groups/electronics.json create mode 100644 site/dpp-landing/src/data/product-groups/furniture.json create mode 100644 site/dpp-landing/src/data/product-groups/mattress.json create mode 100644 site/dpp-landing/src/data/product-groups/steel.json create mode 100644 site/dpp-landing/src/data/product-groups/textile.json create mode 100644 site/dpp-landing/src/data/product-groups/toy.json create mode 100644 site/dpp-landing/src/data/product-groups/tyre.json create mode 100644 site/dpp-landing/src/data/product-groups/unsold-goods.json create mode 100644 site/dpp-landing/src/data/stations.json create mode 100644 site/dpp-landing/src/pages/lifecycle.astro create mode 100644 site/dpp-landing/src/pages/main.astro create mode 100644 site/dpp-landing/src/pages/product-groups/[key].astro diff --git a/site/dpp-landing/astro.config.mjs b/site/dpp-landing/astro.config.mjs index fa95912..deb0e48 100644 --- a/site/dpp-landing/astro.config.mjs +++ b/site/dpp-landing/astro.config.mjs @@ -11,7 +11,17 @@ import sitemap from "@astrojs/sitemap"; export default defineConfig({ site: "https://odal-node.io", - integrations: [sitemap()], + integrations: [ + sitemap({ + // A page carrying `noindex` must not also be advertised in the + // sitemap: the two instructions contradict each other, and a + // crawler that follows the sitemap first has been told to index + // something the page then refuses. /main is a draft published + // beside the live home page for comparison, not a second entry + // point competing for the same queries. + filter: (page) => !/\/main\/?$/.test(page), + }), + ], vite: { plugins: [ tailwindcss(), diff --git a/site/dpp-landing/package.json b/site/dpp-landing/package.json index eba155e..b5746dd 100644 --- a/site/dpp-landing/package.json +++ b/site/dpp-landing/package.json @@ -9,7 +9,9 @@ "dev": "astro dev", "build": "astro build", "preview": "astro preview", - "check": "astro check" + "check": "astro check", + "sync:product-groups": "node scripts/sync-product-groups.mjs", + "check:product-groups": "node scripts/sync-product-groups.mjs --check" }, "dependencies": { "@astrojs/sitemap": "^3", diff --git a/site/dpp-landing/product-groups-source.json b/site/dpp-landing/product-groups-source.json new file mode 100644 index 0000000..2925251 --- /dev/null +++ b/site/dpp-landing/product-groups-source.json @@ -0,0 +1,7 @@ +{ + "_comment": "Provenance for src/data/product-groups/*.json, vendored from dpp-core. The commit is what `pnpm run sync:product-groups` last copied from, and what `check:product-groups` verifies the vendored copies against. Same reasoning as the docs site's openapi-source.json: pinning to a commit rather than to core's main keeps this repo's CI deterministic — an unrelated merge in core cannot turn a pull request here red, and a coordinated change across both repos can land without deadlocking on which merges first. Bumping the pin is a deliberate, reviewable line in a diff.", + "_path_comment": "The product-group manifests carry NO law — act numbers, dates and passport obligations live in the instrument manifests, which are deliberately not vendored here. These files carry identity, schema versions, product categories, the disclosure map and the plugin binding. A page built from them can therefore state what the software models without making a regulatory claim it would have to keep current.", + "repository": "odal-node/dpp-core", + "path": "crates/dpp-domain/product-groups", + "commit": "81f70871481e97d50a584c4a8d6c32aefd616b20" +} diff --git a/site/dpp-landing/scripts/sync-product-groups.mjs b/site/dpp-landing/scripts/sync-product-groups.mjs new file mode 100644 index 0000000..382bc38 --- /dev/null +++ b/site/dpp-landing/scripts/sync-product-groups.mjs @@ -0,0 +1,148 @@ +// Vendor the product-group manifests from the sibling dpp-core repo. +// +// The copies under src/data/product-groups/ are committed so a build works +// without dpp-core checked out alongside. That convenience is also the hazard: +// a vendored file drifts silently, and the drift is published. +// +// pnpm run sync:product-groups copy core -> src/data/, record the commit +// pnpm run check:product-groups compare only, fail on drift or absent source +// +// Both modes exit non-zero when the source cannot be read. A sync that did not +// sync is a failure, not a notice. +// +// This mirrors site/dpp-docs/scripts/sync-openapi.mjs deliberately, including +// why it compares against a pinned commit rather than core's main branch: +// comparing against another repository's moving state makes this repository's +// CI depend on it, so an unrelated merge there turns pull requests red here, +// and a correction that must land in both deadlocks on which merges first. +// +// WHAT IS VENDORED, AND WHAT IS NOT +// +// Product-group manifests only. They carry no law — act numbers, dates and +// passport obligations live in the instrument manifests, which stay in core. A +// page built from these can say what the software models without making a +// regulatory claim this repository would then have to keep current. +import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const root = fileURLToPath(new URL('..', import.meta.url)); +const coreDir = + process.env.DPP_CORE_DIR ?? fileURLToPath(new URL('../../../../dpp-core', import.meta.url)); +const pinPath = `${root}product-groups-source.json`; +const destDir = `${root}src/data/product-groups`; + +const pin = JSON.parse(readFileSync(pinPath, 'utf8')); +const srcDir = `${coreDir}/${pin.path}`; + +const checkOnly = process.argv.includes('--check'); + +// Git normalises to LF on commit, but a Windows working copy is CRLF. Compare +// content, not line terminators, or the check fails on every developer machine. +const normalise = (text) => text.replace(/\r\n/g, '\n'); + +function fail(...lines) { + for (const l of lines) console.error(l); + process.exit(1); +} + +/** Read one manifest out of core's git history at the pinned commit. */ +function atPin(name) { + try { + return execFileSync('git', ['show', `${pin.commit}:${pin.path}/${name}`], { + cwd: coreDir, + encoding: 'utf8', + maxBuffer: 32 * 1024 * 1024, + }); + } catch { + return null; + } +} + +if (!existsSync(coreDir)) { + fail( + `Cannot read ${coreDir}.`, + `Check out dpp-core beside this repository, or set DPP_CORE_DIR.`, + ); +} + +// The set of manifests is taken from the PIN, not from whatever happens to be +// on disk now. Listing the working tree would let a manifest added upstream +// appear here without the pin moving, which is the drift this file exists to +// prevent. +let names; +try { + names = execFileSync('git', ['ls-tree', '--name-only', `${pin.commit}:${pin.path}`], { + cwd: coreDir, + encoding: 'utf8', + }) + .split('\n') + .map((n) => n.trim()) + .filter((n) => n.endsWith('.json')) + .sort(); +} catch { + fail( + `Cannot list ${pin.path} at commit ${pin.commit} in ${coreDir}.`, + `Fetch dpp-core so the pinned commit is present, or correct the "commit" field in product-groups-source.json.`, + ); +} + +// 🚨 A loop over an empty list succeeds. A sync that vendored nothing, or a +// check that compared nothing, must not report success — that is the same +// vacuous pass this project has found in four other gates. +if (!names.length) { + fail(`No manifests found at ${pin.path} in commit ${pin.commit}. Nothing was ${checkOnly ? 'checked' : 'synced'}.`); +} + +if (!checkOnly) mkdirSync(destDir, { recursive: true }); + +const drifted = []; +const missing = []; + +for (const name of names) { + const source = atPin(name); + if (source === null) { + missing.push(name); + continue; + } + const dest = `${destDir}/${name}`; + if (checkOnly) { + if (!existsSync(dest)) { + drifted.push(`${name} (not vendored)`); + continue; + } + if (normalise(readFileSync(dest, 'utf8')) !== normalise(source)) drifted.push(name); + } else { + writeFileSync(dest, source); + } +} + +if (missing.length) { + fail(`Missing at the pinned commit: ${missing.join(', ')}`); +} + +// A vendored file with no counterpart upstream is drift too, and in the +// direction a comparison loop over the source list cannot see. +if (checkOnly && existsSync(destDir)) { + const extra = readdirSync(destDir) + .filter((n) => n.endsWith('.json')) + .filter((n) => !names.includes(n)); + for (const n of extra) drifted.push(`${n} (vendored, absent upstream)`); +} + +if (checkOnly) { + if (drifted.length) { + fail( + `Vendored product-group manifests differ from ${pin.repository} at ${pin.commit}:`, + ...drifted.map((d) => ` ${d}`), + ``, + `Run: pnpm run sync:product-groups`, + `If core has moved on purpose, bump "commit" in product-groups-source.json first.`, + ); + } + console.log(`OK: ${names.length} product-group manifest(s) match ${pin.repository} at ${pin.commit.slice(0, 8)}.`); +} else { + const stamp = { ...pin }; + writeFileSync(pinPath, `${JSON.stringify(stamp, null, 2)}\n`); + console.log(`Vendored ${names.length} product-group manifest(s) from ${pin.commit.slice(0, 8)}.`); +} diff --git a/site/dpp-landing/src/components/Nav.astro b/site/dpp-landing/src/components/Nav.astro index e11e41a..14f2491 100644 --- a/site/dpp-landing/src/components/Nav.astro +++ b/site/dpp-landing/src/components/Nav.astro @@ -12,6 +12,7 @@ const links = [ { href: "https://docs.odal-node.io", label: "Docs", external: true }, // { href: "/trust", label: "Trust", external: false }, { href: "/roadmap", label: "Roadmap", external: false }, + { href: "/lifecycle", label: "Lifecycle", external: false }, { href: "/who-sees-what", label: "Who sees what", external: false }, // { href: "/about", label: "About", external: false }, { href: "https://github.com/odal-node", label: "GitHub", external: true }, diff --git a/site/dpp-landing/src/data/product-groups/aluminium.json b/site/dpp-landing/src/data/product-groups/aluminium.json new file mode 100644 index 0000000..dc5f2b1 --- /dev/null +++ b/site/dpp-landing/src/data/product-groups/aluminium.json @@ -0,0 +1,13 @@ +{ + "key": "aluminium", + "title": "Aluminium", + "schemaVersions": [ + "1.0.0", + "1.1.0", + "1.2.0" + ], + "currentSchemaVersion": "1.2.0", + "productCategories": [], + "plugin": "product-group-aluminium", + "notes": "CO2e thresholds in the plugin are indicative pending the delegated act. productCategories is empty on purpose: it previously listed primary/secondary-recycled/mixed, which are productionRoute values — how the metal was made, not what the product is. No act defines an aluminium category axis yet." +} diff --git a/site/dpp-landing/src/data/product-groups/battery.json b/site/dpp-landing/src/data/product-groups/battery.json new file mode 100644 index 0000000..1815ad8 --- /dev/null +++ b/site/dpp-landing/src/data/product-groups/battery.json @@ -0,0 +1,41 @@ +{ + "key": "battery", + "title": "Batteries", + "schemaVersions": [ + "1.0.0", + "2.0.0", + "2.1.0", + "2.2.0", + "2.3.0", + "2.4.0", + "2.5.0", + "2.6.0", + "2.7.0" + ], + "currentSchemaVersion": "2.7.0", + "productCategories": [ + "portable", + "industrial", + "ev", + "lmt", + "starting-lighting-ignition" + ], + "disclosure": { + "disassemblyInstructionsUrl": "restricted", + "cathodeMaterial": "restricted", + "anodeMaterial": "restricted", + "electrolyteMaterial": "restricted", + "sohMethodology": "restricted", + "stateOfHealth": "individual", + "stateOfHealthPct": "individual", + "expectedLifetime": "individual", + "dynamicPerformance": "individual", + "batteryStatus": "individual", + "usageHistory": "individual", + "componentPartNumbers": "restricted", + "sparePartsContacts": "restricted", + "safetyMeasures": "restricted", + "testReportResults": "conformity" + }, + "plugin": "product-group-battery" +} diff --git a/site/dpp-landing/src/data/product-groups/construction.json b/site/dpp-landing/src/data/product-groups/construction.json new file mode 100644 index 0000000..7a7a3f0 --- /dev/null +++ b/site/dpp-landing/src/data/product-groups/construction.json @@ -0,0 +1,21 @@ +{ + "key": "construction", + "title": "Construction Products", + "schemaVersions": [ + "1.0.0", + "1.1.0", + "1.2.0" + ], + "currentSchemaVersion": "1.2.0", + "productCategories": [ + "cement", + "concrete", + "structural-steel", + "glass" + ], + "disclosure": { + "epdUrl": "restricted" + }, + "plugin": "product-group-construction", + "notes": "productCategories are ours, not the Commission's stated first product families — rebar, doors and windows with their hardware, and thermal insulation are all absent while concrete and glass are not on the Commission's list. Worth realigning when the CPR family axis is settled." +} diff --git a/site/dpp-landing/src/data/product-groups/detergent.json b/site/dpp-landing/src/data/product-groups/detergent.json new file mode 100644 index 0000000..cec596d --- /dev/null +++ b/site/dpp-landing/src/data/product-groups/detergent.json @@ -0,0 +1,17 @@ +{ + "key": "detergent", + "title": "Detergents & Surfactants", + "schemaVersions": [ + "1.0.0", + "1.1.0", + "1.2.0" + ], + "currentSchemaVersion": "1.2.0", + "productCategories": [ + "laundry", + "dishwashing", + "surface-cleaner", + "personal-care" + ], + "plugin": "product-group-detergent" +} diff --git a/site/dpp-landing/src/data/product-groups/electronics.json b/site/dpp-landing/src/data/product-groups/electronics.json new file mode 100644 index 0000000..6026cbd --- /dev/null +++ b/site/dpp-landing/src/data/product-groups/electronics.json @@ -0,0 +1,26 @@ +{ + "key": "electronics", + "title": "Electronics (smartphones, mobile phones & tablets)", + "schemaVersions": [ + "1.0.0", + "1.1.0", + "1.2.0", + "1.3.0", + "1.4.0" + ], + "currentSchemaVersion": "1.4.0", + "productCategories": [ + "smartphone", + "other-mobile-phone", + "cordless-phone", + "tablet" + ], + "disclosure": { + "svhcSubstances": "restricted", + "disassemblyInstructionsUrl": "restricted", + "repairManualUrl": "restricted", + "criticalRawMaterials": "restricted" + }, + "plugin": "product-group-electronics", + "notes": "v1.4.0 (current) replaces the mandatory gtin with productIdentifier, a union of the EN 18219:2026 clause 5 ID schemes; a v1.3.0 record is read forward by a lens that wraps its gtin as scheme 1. v1.3.0 adds repairabilityIndexInputs, the six Reg. (EU) 2023/1669 Annex IV point 5 parameters the enacted repairability index is computed from, and indexScopeExclusion for the Art. 1(a) and 1(b) carve-outs; it was absent from this list until v1.4.0 landed, though the schema has always been embedded. v1.2.0 narrows productCategory to exactly the four Reg. (EU) 2023/1670 Art. 1(1) device types — laptop/monitor/tv/server/router/charger/earphone/pcb/other are removed: none of them has a DPP obligation, in force or dated, under any EU instrument. v1.1.0 corrects repairabilityScore to the structured {overall, criteria} shape; v1.0.0 is kept for historical immutability, having declared repairabilityScore as a bare number." +} diff --git a/site/dpp-landing/src/data/product-groups/furniture.json b/site/dpp-landing/src/data/product-groups/furniture.json new file mode 100644 index 0000000..c31c838 --- /dev/null +++ b/site/dpp-landing/src/data/product-groups/furniture.json @@ -0,0 +1,23 @@ +{ + "key": "furniture", + "title": "Furniture", + "schemaVersions": [ + "1.0.0", + "1.1.0", + "1.2.0", + "1.3.0" + ], + "currentSchemaVersion": "1.3.0", + "productCategories": [ + "chair", + "table", + "sofa", + "shelf" + ], + "disclosure": { + "svhcSubstances": "restricted", + "disassemblyInstructionsUrl": "restricted" + }, + "plugin": "product-group-furniture", + "notes": "Mattresses are a separate product group and not a category here — the working plan selects them separately from furniture. v1.3.0 (current) replaces the mandatory gtin with productIdentifier, a union of the EN 18219:2026 clause 5 ID schemes; a v1.2.0 record is read forward by a lens that wraps its gtin as scheme 1. v1.2.0 removes `mattress` from productType; v1.1.0 is kept for historical immutability and still accepts it." +} diff --git a/site/dpp-landing/src/data/product-groups/mattress.json b/site/dpp-landing/src/data/product-groups/mattress.json new file mode 100644 index 0000000..ef91608 --- /dev/null +++ b/site/dpp-landing/src/data/product-groups/mattress.json @@ -0,0 +1,15 @@ +{ + "key": "mattress", + "title": "Mattresses", + "schemaVersions": [ + "1.0.0", + "1.1.0" + ], + "currentSchemaVersion": "1.1.0", + "productCategories": [], + "disclosure": { + "svhcSubstances": "restricted", + "disassemblyInstructionsUrl": "restricted" + }, + "notes": "Split out of furniture: the working plan selects mattresses separately, with their own ranking and adoption year. Schema is furniture v1.1.0 with productType removed and nothing added — no delegated act exists for either group, so neither field set is more than a best reading. No plugin is bound yet; furniture's would have to branch on a category that no longer exists." +} diff --git a/site/dpp-landing/src/data/product-groups/steel.json b/site/dpp-landing/src/data/product-groups/steel.json new file mode 100644 index 0000000..455b8c8 --- /dev/null +++ b/site/dpp-landing/src/data/product-groups/steel.json @@ -0,0 +1,17 @@ +{ + "key": "steel", + "title": "Iron & Steel", + "schemaVersions": [ + "1.0.0", + "1.1.0", + "1.2.0" + ], + "currentSchemaVersion": "1.2.0", + "productCategories": [ + "flat", + "long", + "tube" + ], + "plugin": "product-group-steel", + "notes": "CO2e thresholds in the plugin are indicative pending the delegated act." +} diff --git a/site/dpp-landing/src/data/product-groups/textile.json b/site/dpp-landing/src/data/product-groups/textile.json new file mode 100644 index 0000000..8b79e6c --- /dev/null +++ b/site/dpp-landing/src/data/product-groups/textile.json @@ -0,0 +1,19 @@ +{ + "key": "textile", + "title": "Textiles (fibre DPP)", + "schemaVersions": [ + "1.0.0", + "1.1.0", + "1.2.0", + "1.3.0" + ], + "currentSchemaVersion": "1.3.0", + "productCategories": [], + "disclosure": { + "svhcSubstances": "restricted", + "disassemblyInstructions": "restricted", + "sparePartsAvailable": "restricted" + }, + "plugin": "product-group-textile", + "notes": "Fields are provisional and may change without a migration path. productCategories is empty on purpose: it previously listed apparel/footwear/home_textile, which no schema version defines — the textile schema has no enum-valued property at all — so nothing could set or validate them. The delegated act that would define a category axis is unadopted." +} diff --git a/site/dpp-landing/src/data/product-groups/toy.json b/site/dpp-landing/src/data/product-groups/toy.json new file mode 100644 index 0000000..ef6b8d6 --- /dev/null +++ b/site/dpp-landing/src/data/product-groups/toy.json @@ -0,0 +1,15 @@ +{ + "key": "toy", + "title": "Toys", + "schemaVersions": [ + "1.0.0", + "1.1.0", + "1.2.0" + ], + "currentSchemaVersion": "1.2.0", + "productCategories": [], + "disclosure": { + "svhcSubstances": "restricted" + }, + "plugin": "product-group-toy" +} diff --git a/site/dpp-landing/src/data/product-groups/tyre.json b/site/dpp-landing/src/data/product-groups/tyre.json new file mode 100644 index 0000000..1540d44 --- /dev/null +++ b/site/dpp-landing/src/data/product-groups/tyre.json @@ -0,0 +1,15 @@ +{ + "key": "tyre", + "title": "Tyres", + "schemaVersions": [ + "1.0.0", + "1.1.0" + ], + "currentSchemaVersion": "1.1.0", + "productCategories": [ + "C1", + "C2", + "C3" + ], + "plugin": "product-group-tyre" +} diff --git a/site/dpp-landing/src/data/product-groups/unsold-goods.json b/site/dpp-landing/src/data/product-groups/unsold-goods.json new file mode 100644 index 0000000..d45d89d --- /dev/null +++ b/site/dpp-landing/src/data/product-groups/unsold-goods.json @@ -0,0 +1,12 @@ +{ + "key": "unsold-goods", + "title": "Discarded unsold consumer products (ESPR Arts. 24-25)", + "schemaVersions": [ + "2.0.0" + ], + "currentSchemaVersion": "2.0.0", + "productCategories": [], + "disclosure": {}, + "plugin": "product-group-textile", + "notes": "Not a product group. It occupies a slot for implementation convenience and borrows the product-group-textile plugin; the obligation is horizontal, on an economic operator over a financial year, and imposes no passport. v2.0.0 is the format prescribed by Impl. Reg. (EU) 2026/2 Annex I. There is no v1.0.0: it predated that act and nothing could carry a document forward from it, so it was removed rather than migrated behind an invented lens. productCategories is empty because Art. 3 delimits by CN code, not by a name from a list of ours; disclosure is empty because the whole document is published on the operator's own website, so there is nothing in it to withhold from any audience." +} diff --git a/site/dpp-landing/src/data/stations.json b/site/dpp-landing/src/data/stations.json new file mode 100644 index 0000000..667e40b --- /dev/null +++ b/site/dpp-landing/src/data/stations.json @@ -0,0 +1,81 @@ +{ + "_comment": "The chain of custody as eight stations — the spine shared by /lifecycle and /main. Endpoints are the engine's real routes. Stations 6, 7 and 8 are the ones the live home page never reaches, and they are the three with no competitor equivalent. Every 'who' is an actor named in law, not an invented persona.", + "stations": [ + { + "n": 1, + "key": "declare", + "title": "Declare", + "who": "Manufacturer, importer", + "endpoint": "/integrator/api/v1/import/{productGroup}", + "lead": "The data arrives the way it actually exists: spreadsheets.", + "body": "CSV and Excel, with a dry run that reports errors per row rather than rejecting the file. Nobody's product data starts life in a schema, and a tool that insists otherwise is a tool nobody can start using." + }, + { + "n": 2, + "key": "evidence", + "title": "Evidence", + "who": "Supplier, test laboratory", + "endpoint": "/api/v1/dpp/{id}/evidence", + "lead": "A document a supplier has not sent is visible as a gap, not as a blank.", + "body": "Evidence is a first-class object with its own address and its own verification, rather than a field someone filled in. The difference matters when an auditor asks where a number came from." + }, + { + "n": 3, + "key": "check", + "title": "Check", + "who": "Compliance lead", + "endpoint": "/api/v1/dpp/{id}/lint", + "lead": "Checked against the product group's signed ruleset, before anything is signed.", + "body": "Rules arrive as signed bundles and are verified fail-closed. When a threshold changes, the ruleset changes — not the software, and not a redeploy." + }, + { + "n": 4, + "key": "seal", + "title": "Seal", + "who": "The operator, alone", + "endpoint": "/api/v1/dpp/{id}/publish", + "lead": "Signed with the operator's own key, on their own infrastructure. Claims freeze here.", + "body": "The identity is a did:web in a domain they control. Nothing is sent anywhere to be signed, which is the difference between holding a record and renting access to it." + }, + { + "n": 5, + "key": "resolve", + "title": "Resolve", + "who": "Consumer, retailer", + "endpoint": "/01/{gtin}/21/{serial}", + "lead": "Scan the code, get the public view. No login, no fee — because the regulation says so.", + "body": "A GS1 Digital Link when the product is identified by a GTIN, and a plain passport URL when it is not. Requiring the barcode would mean requiring GS1 membership." + }, + { + "n": 6, + "key": "disclose", + "title": "Disclose", + "who": "Repairer, recycler, authority", + "endpoint": "/credential/dpp/{id}", + "lead": "Depth proven by credential, not by a header the caller sets.", + "body": "The same record answers differently depending on who is asking, enforced per field. For batteries this is a lattice rather than a ladder: an authority sees less than a recycler in one specific respect.", + "unmatched": true, + "link": "/who-sees-what" + }, + { + "n": 7, + "key": "handover", + "title": "Hand over", + "who": "Two legal entities", + "endpoint": "/api/v1/dpp/{id}/transfer/initiate · /accept", + "lead": "Responsibility changes hands with both parties' signatures on the record.", + "body": "Remanufacture, repurposing, a sold product line. The chain of custody is part of the passport rather than an email thread, and either side can refuse or withdraw.", + "unmatched": true + }, + { + "n": 8, + "key": "end", + "title": "End", + "who": "Recycler, auditor", + "endpoint": "/api/v1/dpp/{id}/eol · /evidence", + "lead": "A typed end of life, and a dossier that verifies with no network and no trust in us.", + "body": "Passport, signatures, identity documents and the full hash-chained history in one signed file. Someone who has never heard of Odal Node can check it offline.", + "unmatched": true + } + ] +} diff --git a/site/dpp-landing/src/pages/lifecycle.astro b/site/dpp-landing/src/pages/lifecycle.astro new file mode 100644 index 0000000..9d64c24 --- /dev/null +++ b/site/dpp-landing/src/pages/lifecycle.astro @@ -0,0 +1,104 @@ +--- +// /lifecycle — the chain of custody, long-form. +// +// The longest and most credible route, chosen deliberately: this page's job is +// not to convert quickly but to be the thing a sceptical reader finishes and +// believes. Every station names a real endpoint and a real actor, so the page +// can be checked rather than taken on trust. +import Base from "../layouts/Base.astro"; +import Section from "../components/Section.astro"; +import stations from "../data/stations.json"; + +const title = "The chain of custody — eight stations | Odal Node"; +const description = + "One record, eight stations: declare, evidence, check, seal, resolve, disclose, hand over, end. Each with the actor who touches it and the endpoint that serves them."; + +const unmatched = stations.stations.filter((s) => s.unmatched); +--- + + +
+

+ Most descriptions of a product passport stop at publication — import, + validate, sign, publish. That is the first half. A product is repaired, + resold, remanufactured and eventually recycled, and the record has to + survive all of it while answering different people differently. +

+

+ Eight stations. Each names the actor who touches the record there, and the + endpoint that serves them. +

+
+ +
+
+ {stations.stations.map((s) => ( +
+
+ + {String(s.n).padStart(2, "0")} + +

{s.title}

+ {s.who} + {s.unmatched && ( + + No competitor equivalent + + )} +
+

{s.lead}

+

{s.body}

+

{s.endpoint}

+ {s.link && ( +

+ + Operate this one yourself → + +

+ )} +
+ ))} +
+
+ +
+

+ Import, validate, sign, publish, resolve — that is a passport generator, + and it is genuinely useful. It is also where the difficult half begins. +

+

+ Stations {unmatched.map((s) => s.n).join(", ")} are the ones that make a + record survive a decade of ownership changes rather than describing a + product on the day it was made: +

+
    + {unmatched.map((s) => ( +
  • + {s.title} — {s.lead} +
  • + ))} +
+

+ They are the reason this page is long. A capability that only shows up + three years after purchase cannot be demonstrated in a screenshot, so the + alternative is to describe it precisely enough to be checked. +

+
+ +
+

+ All eight stations exist in the software today. Nobody is running + it in production — no customers, no pilots, no live deployments. + The battery passport obligation begins on 18 February 2027, and we would + rather be correct when it arrives than early and wrong. +

+

+ The code is open:{" "} + dpp-core is Apache-2.0 + and can be read, audited or reused by anyone, including a competitor. +

+
+ diff --git a/site/dpp-landing/src/pages/main.astro b/site/dpp-landing/src/pages/main.astro new file mode 100644 index 0000000..0c210ac --- /dev/null +++ b/site/dpp-landing/src/pages/main.astro @@ -0,0 +1,115 @@ +--- +// /main — the PROPOSED home page, published beside the live one for comparison. +// +// Not a replacement. `/` still leads with four steps; this leads with the eight +// stations. Both are live so they can be read one after the other and the +// difference argued about with something concrete rather than in the abstract. +// +// 🚨 noindex: two pages competing for the same queries is the one certain way to +// rank for neither, and this one is a draft for an internal decision. +import Base from "../layouts/Base.astro"; +import Section from "../components/Section.astro"; +import Hero from "../components/Hero.astro"; +import DeadlineCards from "../components/DeadlineCards.astro"; +import StandardsRow from "../components/StandardsRow.astro"; +import stations from "../data/stations.json"; + +const title = "Odal Node — the record layer for EU product passports (proposed home)"; +const description = + "Proposed home page: leads with the eight-station chain of custody rather than the four publishing steps. Published beside the live home page for comparison."; +--- + + +
+
+

+ Draft. This is a proposed replacement for the home page, + published beside it so the two can be compared. The live page is{" "} + here. This page is + excluded from search engines. +

+
+
+ + + +
+

+ Import, validate, sign, publish, resolve. That is a passport generator, and + it is where the difficult half begins — because the product is then + repaired, resold, remanufactured and recycled, and the record has to + survive all of it. +

+ + + +

+ Read the eight in full → +

+
+ +
+

+ That is not a slogan — it is Regulation (EU) 2023/1542 Art. 77(2), and it + is the part most implementations get wrong by modelling access as a ladder + with the regulator on top. +

+

+ Rather than claim we handle it, the page below lets you pick an audience + and watch the fields appear and vanish. +

+

+ Try it → +

+
+ +
+ +
+ +
+ +
+ +
+

+ No customers, no pilots, no live deployments. The battery passport + obligation begins on 18 February 2027, and we would rather be correct when + it arrives than early and wrong. +

+

+ The roadmap says which is which → +

+
+ diff --git a/site/dpp-landing/src/pages/product-groups/[key].astro b/site/dpp-landing/src/pages/product-groups/[key].astro new file mode 100644 index 0000000..bbb2803 --- /dev/null +++ b/site/dpp-landing/src/pages/product-groups/[key].astro @@ -0,0 +1,175 @@ +--- +// /product-groups/[key] — one page per modelled product group, generated from +// the manifests vendored out of dpp-core at a pinned commit. +// +// WHY GENERATED RATHER THAN WRITTEN +// +// Twelve hand-written pages are twelve things to forget when a schema version +// moves. These render from the same manifests the software reads, so a page +// cannot claim a version the code does not serve. `check:product-groups` fails +// the build when the vendored copies drift from the pin. +// +// WHAT THESE PAGES DELIBERATELY DO NOT SAY +// +// No act numbers, no dates, no "mandatory from". The product-group manifests +// carry no law — that lives in core's instrument manifests, which are not +// vendored here. A page that stated an obligation would be a regulatory claim +// this repository would then have to keep current, and the one thing worse than +// no date is a stale one. +import Base from "../../layouts/Base.astro"; +import Section from "../../components/Section.astro"; +import pin from "../../../product-groups-source.json"; + +type Manifest = { + key: string; + title: string; + schemaVersions: string[]; + currentSchemaVersion: string; + productCategories?: string[]; + disclosure?: Record; + plugin?: string; +}; + +export function getStaticPaths() { + const all = import.meta.glob("../../data/product-groups/*.json", { eager: true }); + return Object.values(all).map((m: any) => { + const d = (m.default ?? m) as Manifest; + return { params: { key: d.key }, props: { manifest: d } }; + }); +} + +const { manifest } = Astro.props as { manifest: Manifest }; + +// The disclosure map is field -> class. Invert it: a reader wants to know which +// fields share a class, not to scan a field list for the one they care about. +const byClass = new Map(); +for (const [field, cls] of Object.entries(manifest.disclosure ?? {})) { + if (!byClass.has(cls)) byClass.set(cls, []); + byClass.get(cls)!.push(field); +} +const classes = [...byClass.entries()].sort((a, b) => a[0].localeCompare(b[0])); + +// What each class means. `conformity` and `individual` appear for batteries +// only, because Art. 77(2) is the only place that split is currently law. +const CLASS_NOTE: Record = { + restricted: "Not public. Shown to a reader holding a credential for this product group.", + conformity: "Conformity evidence. Battery Annex XIII point 3 — notified bodies, market surveillance and the Commission.", + individual: "The individual item's own history. Battery Annex XIII point 4 — legitimate interest only, and explicitly not authorities.", +}; + +const versionCount = manifest.schemaVersions?.length ?? 0; +const title = `${manifest.title} — product group | Odal Node`; +const description = `What Odal Node models for ${manifest.title.toLowerCase()}: ${versionCount} schema version(s), currently ${manifest.currentSchemaVersion}, with its disclosure classes and plugin binding.`; +--- + + +
+

+ Everything on this page is generated from the product-group manifest the + software itself reads, vendored from{" "} + {pin.repository} at a + pinned commit. It describes what is modelled, not what the + law requires — see the note at the foot. +

+ +
+
+

Current schema

+

+ v{manifest.currentSchemaVersion} +

+
+
+

Versions modelled

+

{versionCount}

+
+
+

Compliance plugin

+

+ {manifest.plugin ?? "none"} +

+
+
+
+ + {manifest.productCategories?.length ? ( +
+

+ A category changes which rules apply. These are the values the schema + accepts, not a marketing segmentation. +

+
+ {manifest.productCategories.map((c) => ( + + {c} + + ))} +
+
+ ) : null} + +
+ {classes.length === 0 ? ( + <> +

+ This product group declares no disclosure classes. That + is not an omission — it is the absence of a delegated act. +

+

+ ESPR Art. 11(b) gives each actor access "based on their respective + access rights set out in the applicable delegated act adopted pursuant + to Article 4". Until that act exists for + {" "}{manifest.title.toLowerCase()}, there is no split to model, and + inventing one would be asserting a rule nobody has written. +

+ + ) : ( + <> +

+ Every field below is withheld from the public view. The class decides + who may see it. +

+
+ {classes.map(([cls, fields]) => ( +
+
+

{cls}

+ {fields.length} field(s) +
+ {CLASS_NOTE[cls] && ( +

{CLASS_NOTE[cls]}

+ )} +

+ {fields.sort().join(" · ")} +

+
+ ))} +
+ {byClass.has("conformity") && byClass.has("individual") && ( +

+ Note the two classes above are not ordered. Conformity + evidence goes to authorities and not to recyclers; the individual + item's history goes to recyclers and not to authorities. See{" "} + who sees what. +

+ )} + + )} +
+ +
+

+ Generated from {pin.path}/{manifest.key}.json in + {" "}{pin.repository}, + vendored at commit {pin.commit.slice(0, 8)}. A build fails if + the vendored copy drifts from that commit. +

+

+ No dates or act numbers appear on this page deliberately.{" "} + Product-group manifests carry no law. Obligations, effective dates and + passport requirements live in separate instrument manifests that are not + vendored here — because a regulatory date published on a marketing site is + a claim that has to be kept current, and a stale one is worse than none. +

+
+ From ef85ab06e4d983cdd6d7391b5aafd520a9f04bb4 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Sat, 19 Sep 2026 13:03:08 +0200 Subject: [PATCH 033/168] fix(landing): restore spaces Astro trims where prose meets a link --- package.json | 1 + scripts/check-inline-spacing.mjs | 108 ++++++++++++++++++ .../src/components/GetStarted.astro | 2 +- site/dpp-landing/src/pages/_about.astro | 16 +-- site/dpp-landing/src/pages/_contact.astro | 10 +- site/dpp-landing/src/pages/privacy.astro | 6 +- site/dpp-landing/src/pages/roadmap.astro | 10 +- site/dpp-landing/src/pages/trust.astro | 2 +- site/dpp-landing/src/pages/waitlist.astro | 2 +- 9 files changed, 133 insertions(+), 24 deletions(-) create mode 100644 scripts/check-inline-spacing.mjs diff --git a/package.json b/package.json index f108ca2..500d1a9 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "build": "pnpm -r build", "check": "pnpm -r check", "check:links": "node scripts/check-links.mjs", + "check:spacing": "node scripts/check-inline-spacing.mjs", "check:leakage": "node scripts/check-leakage.mjs", "check:openapi": "pnpm --filter dpp-docs run check:openapi", "sync:openapi": "pnpm --filter dpp-docs run sync:openapi", diff --git a/scripts/check-inline-spacing.mjs b/scripts/check-inline-spacing.mjs new file mode 100644 index 0000000..9881046 --- /dev/null +++ b/scripts/check-inline-spacing.mjs @@ -0,0 +1,108 @@ +// Catch prose that will render with a word glued to a link. +// +// Astro trims the whitespace at a text node's boundary with an element. So +// this, which looks correct in the editor and formats correctly under +// Prettier: +// +//

+// The roadmap is reviewed fortnightly. See +// the project board +// for live status. +//

+// +// publishes as "fortnightly. Seethe project boardfor live status." Nothing +// catches it: it is valid HTML, `astro check` type-checks templates rather than +// reading them, and the link still resolves so `check-links` is happy. It is +// only visible by reading the rendered page, which is why twenty-eight of these +// reached `main` across the privacy, trust, waitlist, roadmap and about pages +// before anyone noticed. +// +// The fix is an explicit {" "} on the side that lost its space. This gate finds +// the sites that need one. +// +// WHY THIS READS SOURCE RATHER THAN `dist` +// +// In the built HTML the defect is indistinguishable from legitimate text: a +// schema page prints `anodeMaterial` and a footnote prints `xs`, +// both of which are a word touching a tag on purpose. In the source the +// difference is unambiguous — a line of prose ending in a letter, and an inline +// element beginning the next line. So the rule is stated where it is exact. +// +// Usage: node scripts/check-inline-spacing.mjs +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import { join, extname } from 'node:path'; + +const ROOTS = ['site/dpp-landing/src', 'site/dpp-docs/src']; + +// Elements that sit inside a sentence. A block element (

,

,
  • ) +// starting a new line is a paragraph break, not a lost space. +const INLINE = 'a|strong|em|code|b|i|abbr|small'; + +// A line ending in prose, then an inline element opening the next line. +// The trailing character must be prose — a letter, digit or sentence +// punctuation. Anything else is markup or an expression boundary: `=> (`, +// `&& (`, `>` and `{` all legitimately precede an element with no space, +// and matching them would bury the real findings in noise. +const OPENS = new RegExp(String.raw`[A-Za-z0-9,;:.’—-][ \t]*\n[ \t]*<(?:${INLINE})[\s>]`, 'g'); + +// An inline element closing at the end of a line, then prose on the next. +const CLOSES = new RegExp(String.raw`[ \t]*\n[ \t]*[A-Za-z0-9“]`, 'g'); + +const walk = (dir, out = []) => { + for (const entry of readdirSync(dir)) { + const full = join(dir, entry); + if (statSync(full).isDirectory()) walk(full, out); + else if (extname(full) === '.astro') out.push(full); + } + return out; +}; + +const findings = []; + +for (const root of ROOTS) { + let files; + try { + files = walk(root); + } catch { + continue; // a site that does not exist is not a failure + } + for (const file of files) { + const src = readFileSync(file, 'utf8'); + for (const [side, pattern] of [['before', OPENS], ['after', CLOSES]]) { + pattern.lastIndex = 0; + let m; + while ((m = pattern.exec(src)) !== null) { + const line = src.slice(0, m.index).split('\n').length; + // The match itself is a single character either side of the newline, + // which is not enough to find the sentence in a long page. Widen it to + // the words around the break, and mark the break itself. + const from = Math.max(0, m.index - 52); + findings.push({ + file, + line, + side, + excerpt: `${from > 0 ? '…' : ''}${src.slice(from, m.index + m[0].length)}` + .replace(/[ \t]*\n[ \t]*/g, ' ⏎ ') + .trim(), + }); + } + } + } +} + +if (findings.length === 0) { + console.log('inline spacing: no glued words.'); + process.exit(0); +} + +console.error( + `inline spacing: ${findings.length} place(s) where a rendered word will be glued to an inline element.\n`, +); +for (const f of findings) { + console.error(` ${f.file}:${f.line}`); + console.error(` ${f.excerpt}`); + console.error( + ` → add {" "} ${f.side === 'before' ? 'at the end of the prose line' : 'after the closing tag'}\n`, + ); +} +process.exit(1); diff --git a/site/dpp-landing/src/components/GetStarted.astro b/site/dpp-landing/src/components/GetStarted.astro index 864909c..cfefef0 100644 --- a/site/dpp-landing/src/components/GetStarted.astro +++ b/site/dpp-landing/src/components/GetStarted.astro @@ -25,7 +25,7 @@ const commands = [ ))}
  • - Full quick-start, including your first signed passport, in the + Full quick-start, including your first signed passport, in the{" "} documentation. Self-hosting is free — no licence keys, no passport caps.

    diff --git a/site/dpp-landing/src/pages/_about.astro b/site/dpp-landing/src/pages/_about.astro index 5d059d7..a0ff6d1 100644 --- a/site/dpp-landing/src/pages/_about.astro +++ b/site/dpp-landing/src/pages/_about.astro @@ -23,10 +23,10 @@ const description =

    The credibility of an open-source standard implementation comes from the - ecosystem that recognises it. The project implements + ecosystem that recognises it. The project implements{" "} GS1 Digital Link for resolution, has applied for observer participation in CIRPASS-2 — the EU project - building out the Digital Product Passport ecosystem — and tracks the + building out the Digital Product Passport ecosystem — and tracks the{" "} CEN/CENELEC JTC 24 schema work, updating the relevant sector schemas as the standards bodies publish.

    @@ -42,7 +42,7 @@ const description =

    Most compliance software is a system of record: the operator hands over their raw supply-chain data, and the vendor holds it, - stores it, and must be trusted with it. Odal Node is a + stores it, and must be trusted with it. Odal Node is a{" "} system of proof: the operator keeps the data and signs it locally; only the signed proof leaves their infrastructure. The difference is not a feature comparison — it is a different relationship to the @@ -60,11 +60,11 @@ const description =

    - Two principles govern every architectural decision. The first is the + Two principles govern every architectural decision. The first is the{" "} proof-bound architecture: raw production data never enters Odal's systems. The manufacturer signs locally with their own key, and only the proof leaves their infrastructure — proof-bound describes the - mechanism, not a promise. The second is the + mechanism, not a promise. The second is the{" "} open-core boundary: code that implements regulation is Apache-2.0 in dpp-core; code that implements the commercial operational layer is BSL-1.1 in dpp-engine. These principles @@ -73,8 +73,8 @@ const description =

    A longer treatment of both principles, with the architectural consequences - drawn out, lives in - the design principles section + drawn out, lives in{" "} + the design principles section{" "} of the documentation.

    @@ -83,7 +83,7 @@ const description =

    General enquiries: hello@odal-node.io. Security reports: security@odal-node.io - (do not open public issues for vulnerabilities). Technical questions: + (do not open public issues for vulnerabilities). Technical questions:{" "} GitHub Issues on dpp-core.

    diff --git a/site/dpp-landing/src/pages/_contact.astro b/site/dpp-landing/src/pages/_contact.astro index ed5a01b..5138e9f 100644 --- a/site/dpp-landing/src/pages/_contact.astro +++ b/site/dpp-landing/src/pages/_contact.astro @@ -12,26 +12,26 @@ const description = "Two ways to reach the team behind Odal Node."; Talking to us early. If your company faces one of the regulatory deadlines, we want to understand how you hold this data today. There is no managed offering to sell you — the software is self-hosted — - so this is a conversation rather than a pitch. Email + so this is a conversation rather than a pitch. Email{" "} hello@odal-node.io with the word "pilot" and a sentence about your products. You will get a direct reply from the team, not a sales sequence.

    For general enquiries, partnership conversations, regulatory questions, or - anything that does not fit a GitHub issue, the same mailbox applies: + anything that does not fit a GitHub issue, the same mailbox applies:{" "} hello@odal-node.io. A human reads it; you will get a reply.

    For technical questions, bug reports, feature requests, or contributions, - open an issue on + open an issue on{" "} the dpp-core GitHub repository. Issues there are public and tracked.

    -->

    - For security disclosures, - do not open a public issue. Report privately to + For security disclosures,{" "} + do not open a public issue. Report privately to{" "} security@odal-node.io.

    diff --git a/site/dpp-landing/src/pages/privacy.astro b/site/dpp-landing/src/pages/privacy.astro index 4dcdfcd..6b6928d 100644 --- a/site/dpp-landing/src/pages/privacy.astro +++ b/site/dpp-landing/src/pages/privacy.astro @@ -47,7 +47,7 @@ const MAILBOX = "security@odal-node.io"; The site is served as static files by Cloudflare Pages. As with any website, your browser must connect to a server to load a page, and that connection is visible to the host. To deliver the site and protect - it from abuse, Cloudflare processes standard request metadata: your + it from abuse, Cloudflare processes standard request metadata: your{" "} IP address, the time of the request, the page requested, your browser's user-agent string, and similar technical details.

    @@ -56,7 +56,7 @@ const MAILBOX = "security@odal-node.io"; receive it as an identifiable record of you. It is processed by Cloudflare as our hosting provider — acting as a data processor on our behalf — for content delivery, TLS, and security (such as DDoS protection), under - Cloudflare's own terms. See + Cloudflare's own terms. See{" "} Cloudflare's privacy policy. Our legal basis for this processing is our legitimate interest in serving and securing the site. @@ -81,7 +81,7 @@ const MAILBOX = "security@odal-node.io"; request usually amounts to "delete our correspondence", which we will do.

    - To exercise any of these rights, or to ask a privacy question, write to + To exercise any of these rights, or to ask a privacy question, write to{" "} {MAILBOX}. If you believe we have handled your data improperly, you also have the right to lodge a complaint with your local data protection authority. diff --git a/site/dpp-landing/src/pages/roadmap.astro b/site/dpp-landing/src/pages/roadmap.astro index 55f5677..1e404ee 100644 --- a/site/dpp-landing/src/pages/roadmap.astro +++ b/site/dpp-landing/src/pages/roadmap.astro @@ -19,8 +19,8 @@ const description = commitment.

    - The roadmap is reviewed and updated fortnightly. See - the public GitHub project board + The roadmap is reviewed and updated fortnightly. See{" "} + the public GitHub project board{" "} for live status on individual items.

    @@ -33,10 +33,10 @@ const description =

    - A feature moves from Horizon to In build - when it enters a sprint and has a named owner. It moves from + A feature moves from Horizon to In build{" "} + when it enters a sprint and has a named owner. It moves from{" "} In build to Shipped when it is in a - tagged release of the relevant repository, has documentation on + tagged release of the relevant repository, has documentation on{" "} docs.odal-node.io, and has an automated test exercising it through the running system — "shipped" means verified, not merged. diff --git a/site/dpp-landing/src/pages/trust.astro b/site/dpp-landing/src/pages/trust.astro index e588266..6e11b8d 100644 --- a/site/dpp-landing/src/pages/trust.astro +++ b/site/dpp-landing/src/pages/trust.astro @@ -107,7 +107,7 @@ const description =

  • Sign. Your private signing key — generated and held in-process on your infrastructure — signs the validated passport. - The result is a signature cryptographically bound to your published + The result is a signature cryptographically bound to your published{" "} did:web identity.
  • diff --git a/site/dpp-landing/src/pages/waitlist.astro b/site/dpp-landing/src/pages/waitlist.astro index be68453..f3f0eaa 100644 --- a/site/dpp-landing/src/pages/waitlist.astro +++ b/site/dpp-landing/src/pages/waitlist.astro @@ -16,7 +16,7 @@ const MAILBOX = "contact@odal-node.io";

    - Odal Node is in active development (alpha). To be kept posted, email + Odal Node is in active development (alpha). To be kept posted, email{" "} {MAILBOX} with a sentence about your products or what you're looking for.

    From 928981efc9bcb3f6eedcb28ca192ef748cc0f00b Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Sat, 19 Sep 2026 13:03:09 +0200 Subject: [PATCH 034/168] chore(ci): run the spacing and product-group drift gates --- .github/workflows/ci.yml | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e84d00e..689cdf1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,16 @@ jobs: fetch-depth: 0 persist-credentials: false + # Same reasoning as the engine checkout above, for the vendored + # product-group manifests the landing site's pages are generated from. + - name: Checkout dpp-core (source of the vendored product groups) + uses: actions/checkout@v4 + with: + repository: odal-node/dpp-core + path: .dpp-core + fetch-depth: 0 + persist-credentials: false + - name: Enable Corepack run: corepack enable @@ -63,6 +73,22 @@ jobs: - name: Check for internal-vocabulary leakage run: pnpm run check:leakage + # Astro trims whitespace where a text node meets an element, so prose + # that reads correctly in the source can publish as "Seethe project + # board". Valid HTML, resolving links, clean type-check — invisible to + # every other gate here. + - name: Check inline spacing + run: pnpm run check:spacing + + # The product-group pages are generated from manifests vendored out of + # dpp-core. This fails if a vendored copy no longer matches the commit + # recorded in product-groups-source.json, so a page cannot claim a schema + # version the software does not serve. + - name: Check the vendored product groups against core + run: pnpm --filter dpp-landing run check:product-groups + env: + DPP_CORE_DIR: ${{ github.workspace }}/.dpp-core + - name: Check the vendored API spec against the engine run: pnpm run check:openapi env: From dbc37cedfbe391730dafe930a56d74de9121a913 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Sat, 19 Sep 2026 13:13:23 +0200 Subject: [PATCH 035/168] feat(landing): rebuild the roadmap against what the repositories actually ship --- packages/brand-tokens/src/tokens.css | 7 +- site/dpp-landing/src/components/Footer.astro | 33 +- site/dpp-landing/src/components/Nav.astro | 7 +- site/dpp-landing/src/components/Roadmap.astro | 121 +++++-- site/dpp-landing/src/data/roadmap.json | 327 ++++++++++++++++-- .../src/pages/product-groups/[key].astro | 15 + .../src/pages/product-groups/index.astro | 151 ++++++++ site/dpp-landing/src/pages/roadmap.astro | 134 +++++-- 8 files changed, 699 insertions(+), 96 deletions(-) create mode 100644 site/dpp-landing/src/pages/product-groups/index.astro diff --git a/packages/brand-tokens/src/tokens.css b/packages/brand-tokens/src/tokens.css index 2cae494..e3c6853 100644 --- a/packages/brand-tokens/src/tokens.css +++ b/packages/brand-tokens/src/tokens.css @@ -61,9 +61,13 @@ --odal-regulatory-border: #fdba74; --odal-regulatory-text: #9a3412; - /* status badges — semantic, not brand: shipped = green stays green */ + /* status badges — semantic, not brand: shipped = green stays green. + `blocked` is its own status and not a shade of `active`: it marks work that + is finished and cannot be used, which is a different fact about the world + from work that is underway, and colouring the two alike hides it. */ --odal-status-released: #2e7d32; --odal-status-active: #d97706; + --odal-status-blocked: #9a3412; --odal-status-development: var(--odal-neutral-500); /* typography */ @@ -136,6 +140,7 @@ --color-status-released: #2e7d32; --color-status-active: #d97706; + --color-status-blocked: #9a3412; --color-status-development: #78716c; --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", diff --git a/site/dpp-landing/src/components/Footer.astro b/site/dpp-landing/src/components/Footer.astro index 65fe893..6400e87 100644 --- a/site/dpp-landing/src/components/Footer.astro +++ b/site/dpp-landing/src/components/Footer.astro @@ -7,8 +7,8 @@ const year = new Date().getFullYear(); ---
    -
    -
    +
    +

    -->
    -
    -

    Project

    + + -
    -

    Privacy & security

    + +
    +

    Product groups

    +
    + + +
    diff --git a/site/dpp-landing/src/components/Nav.astro b/site/dpp-landing/src/components/Nav.astro index 14f2491..389f498 100644 --- a/site/dpp-landing/src/components/Nav.astro +++ b/site/dpp-landing/src/components/Nav.astro @@ -8,12 +8,13 @@ import StatusBadge from "./StatusBadge.astro"; // mark inherits the wordmark's colour. Astro 5 imports .svg as a component. import OdalMark from "../assets/favicon.svg"; +// The nav stays at two destinations plus the CTA. Roadmap, Lifecycle, Who sees +// what and the product groups are reached from the footer instead: they are +// pages a reader goes looking for once, not places to move between, and a nav +// bar that lists everything stops telling anyone where to start. const links = [ { href: "https://docs.odal-node.io", label: "Docs", external: true }, // { href: "/trust", label: "Trust", external: false }, - { href: "/roadmap", label: "Roadmap", external: false }, - { href: "/lifecycle", label: "Lifecycle", external: false }, - { href: "/who-sees-what", label: "Who sees what", external: false }, // { href: "/about", label: "About", external: false }, { href: "https://github.com/odal-node", label: "GitHub", external: true }, ]; diff --git a/site/dpp-landing/src/components/Roadmap.astro b/site/dpp-landing/src/components/Roadmap.astro index 2548299..88ed2db 100644 --- a/site/dpp-landing/src/components/Roadmap.astro +++ b/site/dpp-landing/src/components/Roadmap.astro @@ -1,42 +1,109 @@ --- -// Roadmap.astro — three-column status display. Data lives in src/data/roadmap.json -// so editors change JSON, not components. -// Status enum: shipped | in-build | horizon — the "honest status" framing. +// Roadmap.astro — the four registers, each grouped by capability area. +// Data lives in src/data/roadmap.json so editors change JSON, not components. +// +// WHY FOUR AND NOT THREE +// +// The previous version had Shipped / In build / Horizon, and the seal and +// registry work sat under "in build" with the words "in progress" and "gated on +// upstream blockers". Both are finished. Neither can be used, because an +// electronic seal may only be created by a legal person and there is no company +// — which is not an engineering schedule and does not belong in a column that +// reads like one. `blocked` says the true thing: built, and legally inert. +// +// WHY EVERY ITEM CARRIES EVIDENCE +// +// A roadmap is the easiest page on a site to write optimistically, because +// nothing on it is checkable. Each item here names a crate, a command, a route +// or a page, so a reader who doubts a line can go and settle it. An item that +// cannot name one is an item we should not be making a claim about. import data from "../data/roadmap.json"; -type Status = "shipped" | "in-build" | "horizon"; -const { columns } = data as { - columns: { - status: Status; - title: string; - description: string; - items: { label: string; detail: string }[]; - }[]; +type Status = "shipped" | "blocked" | "in-build" | "horizon"; +type Item = { label: string; detail: string; evidence?: string }; +type Area = { area: string; items: Item[] }; + +const { registers } = data as unknown as { + registers: { status: Status; title: string; description: string; areas: Area[] }[]; }; -const dotClass: Record = { +const dot: Record = { shipped: "bg-status-released", + blocked: "bg-status-blocked", "in-build": "bg-status-active", horizon: "bg-status-development", }; + +const rule: Record = { + shipped: "border-status-released/30", + blocked: "border-status-blocked/40", + "in-build": "border-status-active/30", + horizon: "border-neutral-200", +}; + +// An evidence string is rendered as a link when we can resolve it to one, and +// as plain text otherwise. A command is not a URL, and a citation of the +// Official Journal is deliberately not linked to a page that may move. +// +// An HTTP route is not a link either, however much it looks like one. A path +// with a `{param}` in it names an endpoint on someone's node, not a page on +// this site — linking `/01/{gtin}/21/{serial}` produced the one broken link +// the link checker found on this page. +const href = (e: string): string | null => { + if (e.startsWith("/")) return e.includes("{") ? null : e; + if (e.startsWith("crates.io/")) return `https://${e}`; + if (e.startsWith("github.com/")) return `https://${e}`; + if (e.startsWith("docs.odal-node.io/")) return `https://${e}`; + return null; +}; --- -
    - {columns.map((col) => ( -
    -
    - -

    {col.title}

    +
    + {registers.map((reg) => ( +
    +
    +

    + + {reg.title} +

    +

    + {reg.description} +

    -

    {col.description}

    -
      - {col.items.map((it) => ( -
    • -

      {it.label}

      -

      {it.detail}

      -
    • + +
      + {reg.areas.map((area) => ( +
      +

      + {/* One area repeated per register would be noise, so a register + with a single catch-all area prints no heading at all. */} + {reg.areas.length > 1 ? area.area : ""} +

      +
        + {area.items.map((it) => ( +
      • +

        {it.label}

        +

        {it.detail}

        + {it.evidence && ( +

        + {href(it.evidence) ? ( + + {it.evidence} + + ) : ( + {it.evidence} + )} +

        + )} +
      • + ))} +
      +
      ))} -
    -
    +
    +
    ))} diff --git a/site/dpp-landing/src/data/roadmap.json b/site/dpp-landing/src/data/roadmap.json index be7943a..6895780 100644 --- a/site/dpp-landing/src/data/roadmap.json +++ b/site/dpp-landing/src/data/roadmap.json @@ -1,46 +1,315 @@ { - "_comment": "Roadmap data — the single editable source for the landing roadmap, so editors change JSON rather than components. Status enum: shipped | in-build | horizon. Rule: every numeric claim cites where it is measured, or it is omitted. No project-milestone months — regulatory dates live in deadlines.json.", - "columns": [ + "_comment": "Roadmap data — the single editable source for the roadmap page. Rewritten 2026-09-19 after auditing every item against the two repositories: released tags, crates.io versions, the schema and plugin inventory, the CLI's own command list, and the open issue trackers. Eleven of the previous twenty-one items were wrong in a way that flattered us or, twice, understated us.", + + "_registers": "Four registers, never mixed. 'shipped' is a verified fact. 'blocked' is also a fact — the machinery runs and the status it can claim is nil — and it is separated out because folding it into 'in build' is how a legal blocker gets read as an engineering schedule. 'in-build' is a commitment with a named condition. 'horizon' is an option under consideration, with no date attached to it. Never write an option in the grammar of a fact.", + + "_evidence_rule": "Every item names something a reader can check without asking us: a crate on crates.io, a CLI command, an HTTP route, or a page on this site. An item that cannot name one does not belong on this page.", + + "versions": { + "core": { "repo": "dpp-core", "version": "0.20.0", "note": "Apache-2.0, published to crates.io" }, + "engine": { "repo": "dpp-engine", "version": "0.13.0", "note": "BSL-1.1 with a free self-host grant" } + }, + + "registers": [ { "status": "shipped", "title": "Shipped", - "description": "Verified and available today.", - "items": [ - { "label": "Open regulatory core", "detail": "The EU DPP standard in Rust — pure, dependency-free, and fully open" }, - { "label": "Sector schemas", "detail": "Battery, textile and steel" }, - { "label": "Signing and identity", "detail": "Local signing, did:web identity, and key rotation" }, - { "label": "Resolution and interop", "detail": "GS1 Digital Link, QR, and AAS submodel export" }, - { "label": "Sandboxed sector plugins", "detail": "Each sector's rules run isolated, with signature verification" }, - { "label": "End-to-end node", "detail": "Create, sign, publish, and resolve a passport end to end" }, - { "label": "Full lifecycle provenance", "detail": "Suspend, archive, typed end-of-life, and dual-signed transfer of responsibility — on a tamper-evident, hash-chained audit trail" }, - { "label": "Evidence dossiers + offline verification", "detail": "Export a signed dossier and verify it with zero network — no trust in Odal required" }, - { "label": "Registry-ready outbox", "detail": "Registration intent survives crashes and drains with backoff; the live connector is not yet activated" }, - { "label": "Signed compliance-ruleset channel", "detail": "Rulesets arrive as Ed25519-signed bundles, verified fail-closed and hot-swapped; a production node refuses placeholder trust adapters" }, - { "label": "Bulk import", "detail": "CSV and Excel with dry-run validation and per-row errors" } + "description": "In a tagged release, with a test that exercises it through the running system. Verified fact.", + "areas": [ + { + "area": "The open library", + "items": [ + { + "label": "Eleven crates on crates.io", + "detail": "The domain model, cryptography, credentials, GS1 Digital Link, the Asset Administration Shell projection, the plugin contract and the regulatory rules — all Apache-2.0, all readable and reusable by anyone, including a competitor.", + "evidence": "crates.io/crates/dpp-domain" + }, + { + "label": "Regulatory rules as a dependency-free crate", + "detail": "The cross-field rules that change when a regulation changes are a pure no_std crate with zero dependencies, so they can be audited on their own terms rather than read through a framework.", + "evidence": "crates.io/crates/dpp-rules" + } + ] + }, + { + "area": "Product groups", + "items": [ + { + "label": "Every product group modelled, with its full version history", + "detail": "Each carries its schema versions, its categories, its disclosure split and its own note saying which of its fields are a best reading rather than a requirement. Older schema versions stay served — a passport published against one does not stop resolving when a newer one lands.", + "evidence": "/product-groups" + }, + { + "label": "Compliance rules run sandboxed, and are swapped without a redeploy", + "detail": "Each product group's rules compile to WebAssembly and run isolated from the node. The host verifies a plugin's signature and its declared schema range before dispatching to it.", + "evidence": "odal plugin install" + } + ] + }, + { + "area": "Identity and signing", + "items": [ + { + "label": "Signed on the operator's own key, on the operator's own infrastructure", + "detail": "Ed25519, an encrypted keystore, and a did:web identity in a domain the operator controls. The signing key can be rotated without invalidating what it already signed. Nothing is sent anywhere to be signed.", + "evidence": "crates.io/crates/dpp-crypto" + } + ] + }, + { + "area": "Publication and resolution", + "items": [ + { + "label": "Scan the code, get the public view", + "detail": "GS1 Digital Link resolution when the product carries a GTIN, and a plain passport URL when it does not — because requiring the barcode would mean requiring GS1 membership. No login and no fee, because the regulation says so.", + "evidence": "/01/{gtin}/21/{serial}" + }, + { + "label": "Asset Administration Shell export", + "detail": "A shell plus per-product-group submodels, for the industrial systems that speak AAS rather than passports.", + "evidence": "crates.io/crates/dpp-aas" + } + ] + }, + { + "area": "Disclosure", + "items": [ + { + "label": "Depth proven by credential, enforced per field", + "detail": "The same record answers a repairer, a recycler and an authority differently, and which fields each sees is decided by the credential they present rather than by a header they set. For batteries this is a lattice and not a ladder — an authority sees less than a recycler in one specific respect, and modelling it as a ranking would be wrong.", + "evidence": "/who-sees-what" + } + ] + }, + { + "area": "Lifecycle and custody", + "items": [ + { + "label": "A record that survives the sale", + "detail": "Suspension, retirement, a typed end of life, a recorded waste handover, and lineage that can name several predecessors rather than one.", + "evidence": "odal passport eol" + }, + { + "label": "Responsibility changes hands with both signatures on the record", + "detail": "Remanufacture, repurposing or a sold product line, between two legal entities — either side can refuse or withdraw, and the chain of custody is part of the passport rather than an email thread.", + "evidence": "odal passport transfer initiate" + }, + { + "label": "A tamper-evident history", + "detail": "Every change is appended to a hash-chained audit trail, so a record that was edited cannot present itself as one that was not.", + "evidence": "odal passport history" + } + ] + }, + { + "area": "Evidence and verification", + "items": [ + { + "label": "A dossier that verifies with no network and no trust in us", + "detail": "Passport, signatures, identity documents and the full history in one signed file. Someone who has never heard of Odal Node can check it offline.", + "evidence": "odal verify" + }, + { + "label": "The passport still resolves when the node does not", + "detail": "A pre-rendered static tier, signed and bounded, serving the same page the live resolver serves — through one renderer, so the two cannot drift.", + "evidence": "odal snapshot verify" + } + ] + }, + { + "area": "Running a node", + "items": [ + { + "label": "One command-line control plane", + "detail": "Twenty-six command groups: bring the node up, onboard the operator, mint scoped API keys, manage facilities and economic-operator identifiers, import, publish, issue credentials, inspect seals, read registry status and scan telemetry. There is no separate console to learn, and no operation that is only available in one.", + "evidence": "odal --help" + }, + { + "label": "Signed outbound webhooks", + "detail": "Passport events delivered to the operator's own systems, signed so a receiver can tell a real delivery from a replayed one.", + "evidence": "odal webhook list" + }, + { + "label": "Compliance rulesets arrive signed, and are verified fail-closed", + "detail": "Rulesets ship as signed bundles and are hot-swapped. A node configured for production refuses a placeholder trust adapter rather than accepting one quietly.", + "evidence": "odal ruleset reload" + }, + { + "label": "Scan telemetry the operator owns", + "detail": "How often a passport was resolved, operator-wide and per record, on the operator's own node rather than in someone else's analytics.", + "evidence": "odal stats" + }, + { + "label": "The ESPR Article 24 unsold-goods disclosure", + "detail": "Recorded as many lines rather than one aggregate, because the article asks for figures differentiated per product type, per reason and per destination. A destruction recorded under an exemption must say which exemption.", + "evidence": "odal unsold-goods record" + }, + { + "label": "Registration intent survives a crash", + "detail": "A durable outbox holds pending EU registry registrations and drains with backoff, so a registry that is slow, down or not yet reachable delays binding without ever losing custody.", + "evidence": "odal registry" + } + ] + }, + { + "area": "Getting data in", + "items": [ + { + "label": "Bulk import from spreadsheets, with per-row errors", + "detail": "CSV, TSV, Excel and JSON, with a dry-run mode that reports what is wrong row by row instead of rejecting the file. Coverage is partial and the gap is tracked below — not every product group has an import mapping, and fewer have a downloadable template.", + "evidence": "POST /api/v1/import/{productGroup}" + } + ] + } ] }, + + { + "status": "blocked", + "title": "Built, and legally inert", + "description": "The machinery is written, tested and running. What it cannot do is claim any legal status — and the reason is not an engineering one.", + "areas": [ + { + "area": "eIDAS qualified seal", + "items": [ + { + "label": "The sealing subsystem runs end to end today", + "detail": "Seals are created, stored, audited and re-read across restarts. EU Trusted List verification walks the list-of-lists down to the national list. Each seal carries its own findings — issuer, binding, evidenced level, attested time, archival state, certificate validity and revocation — and an operator can re-run a repair.", + "evidence": "odal seal status" + }, + { + "label": "Every seal it issues reports itself as self-issued and of no legal effect", + "detail": "Under eIDAS an electronic seal may only be created by a legal person. There is no company behind this project, so there is no qualified certificate, so there is no qualified seal. The software says exactly that rather than rounding it up, and it refuses to present a qualified claim it cannot support. When a credential exists, what changes is the backend and the verdict — not the design.", + "evidence": "Regulation (EU) No 910/2014, Art. 3(24)" + } + ] + }, + { + "area": "EU Central Registry", + "items": [ + { + "label": "The registry is live; enrolment is not open to us", + "detail": "The EU Digital Product Passport registry became operational on 20 July 2026. Registration identity is proved by qualified electronic seal or attestation — the same blocker as above, one step further along.", + "evidence": "Commission Implementing Regulation (EU) 2026/1778" + }, + { + "label": "Our own connector has a structural mismatch to correct first", + "detail": "It models a bearer-token exchange where the registry expects eIDAS-based identity. That is a wrong assumption rather than a wrong URL, and it is a breaking change we would rather make against the published specification than guess at. The connector is not pointed at real products.", + "evidence": "crates.io/crates/dpp-registry" + } + ] + } + ] + }, + { "status": "in-build", "title": "In build", - "description": "Under active work with a defined target.", - "items": [ - { "label": "Electronics sector", "detail": "Schema and plugin, prepared ahead of an electronics delegated act" }, - { "label": "EU Central Registry connector", "detail": "Interface modelled against the registry that went live in July 2026; activation still gated on upstream blockers" }, - { "label": "eIDAS qualified-seal path", "detail": "Qualified trust service provider selection in progress" }, - { "label": "Unsold-goods disclosure export", "detail": "Report artifact for the July 2026 textile obligation" }, - { "label": "Operator dashboard", "detail": "Passport management UI for managed and self-hosted nodes" } + "description": "Committed, each with a named condition. Nothing here is claimed as working.", + "areas": [ + { + "area": "In build", + "items": [ + { + "label": "Replacing the mandatory GTIN with the standard's identifier union", + "detail": "EN 18219 offers several identifier schemes as alternatives rather than a hierarchy, and requiring a GTIN meant requiring GS1 membership. The passport model and the plugins already take the union; the remaining work is the sweep through every rule, fixture and test that still assumes the old field. Condition: the next library release.", + "evidence": "github.com/odal-node/dpp-core/issues/328" + }, + { + "label": "Wiring the compliance calculators into the node", + "detail": "Cradle-to-gate CO2e and EN 45554 repairability are written and published as part of the open library, with the licensed emission-factor data injected rather than bundled. Nothing in the running node invokes them yet, so no passport carries a computed figure. Condition: the evidence-receipt path that records which ruleset version produced a number.", + "evidence": "crates.io/crates/dpp-calc" + }, + { + "label": "Import coverage across every product group", + "detail": "Bulk import maps five of the twelve product groups, and downloadable CSV templates exist for two. The mechanism is shipped; the per-group mappings are not. Condition: one mapping and one template per modelled group.", + "evidence": "github.com/odal-node/dpp-engine/issues/192" + }, + { + "label": "The unsold-goods disclosure as a publishable artifact", + "detail": "The record is written and read today. What does not exist is the document an operator publishes on their own website, which is where Article 24 actually puts the obligation. Condition: the output format, which no implementing act prescribes.", + "evidence": "odal unsold-goods list" + }, + { + "label": "Toy and detergent content completeness", + "detail": "Both schemas carry almost none of the content their annexes mandate — and that content is in the adopted acts, not in the implementing acts still pending. These two are modelled far more thinly than the rest and should be read as placeholders.", + "evidence": "github.com/odal-node/dpp-core/issues/262" + } + ] + } ] }, + { "status": "horizon", "title": "Horizon", - "description": "Planned; not in active work. No dates are promised here on purpose.", - "items": [ - { "label": "Multi-operator hosting", "detail": "Isolated per-operator deployments at fleet scale" }, - { "label": "Edge-cached public resolver", "detail": "CDN-cached passport pages for global scan latency" }, - { "label": "Further sectors", "detail": "Construction, tyres, furniture, detergents — as delegated acts finalise" }, - { "label": "Conformance services", "detail": "Verification badge against the CEN/CENELEC DPP standards, once those texts have been purchased and read" }, - { "label": "EBSI did:ebsi", "detail": "EU-operated DID method as an alternative issuer-identity path, if registry requirements call for it" } + "description": "Under consideration. Not started, not scheduled, and deliberately carrying no dates.", + "areas": [ + { + "area": "Known gaps in what is modelled", + "items": [ + { + "label": "The semantic repository", + "detail": "The framework requires passport data to be expressed against a published vocabulary held in a repository we do not yet read. It is mandatory and it is free, and not having read it is the largest single gap on this page.", + "evidence": "github.com/odal-node/dpp-core/issues/92" + }, + { + "label": "Language", + "detail": "Toys and detergents must present the passport in the language or languages of the Member State where the product is placed. There is no language dimension anywhere in the model.", + "evidence": "github.com/odal-node/dpp-core/issues/264" + }, + { + "label": "Images", + "detail": "The toy and detergent annexes both require a colour image of the product. There is no image concept in the passport at all.", + "evidence": "github.com/odal-node/dpp-core/issues/263" + }, + { + "label": "A position on personal data in a passport", + "detail": "One ESPR paragraph is unconditional about what a passport carries, and we have taken no position on what that means when the value identifies a customer. Deciding this badly is worse than deciding it late.", + "evidence": "github.com/odal-node/dpp-core/issues/261" + }, + { + "label": "Battery carbon footprint", + "detail": "Blocked upstream rather than by us: the calculation method has not been published, so there is nothing to implement. Guessing at it would produce a number that looks authoritative and is not.", + "evidence": "github.com/odal-node/dpp-core/issues/160" + } + ] + }, + { + "area": "Options not yet taken", + "items": [ + { + "label": "A public, genuinely offline conformance validator", + "detail": "Anyone could check a passport against the standards without sending it to us, or trusting us. Six European standards are cited by the framework; two have been read in full so far.", + "evidence": "github.com/odal-node/dpp-core/issues/81" + }, + { + "label": "A second credential format", + "detail": "The UN Transparency Protocol's passport credential as an alternative door into the same record, for supply chains that have standardised on it.", + "evidence": "github.com/odal-node/dpp-engine/issues/70" + }, + { + "label": "A second signing curve", + "detail": "P-256 alongside Ed25519, if a hardware or registry requirement ever calls for it. An open question rather than a plan.", + "evidence": "github.com/odal-node/dpp-core/issues/78" + }, + { + "label": "An EU-operated identity method", + "detail": "did:ebsi as an alternative issuer identity to did:web, if registry requirements call for it.", + "evidence": "crates.io/crates/dpp-vc" + }, + { + "label": "Hosting several operators", + "detail": "One operator, one node, one database, one key, and no shared data paths at any layer — that invariant is settled and is not what is open. What is open is only how the machines are provisioned.", + "evidence": "docs.odal-node.io/engine/self-hosted" + }, + { + "label": "An edge-cached public resolver", + "detail": "Cached passport pages for scan latency away from the node's own region.", + "evidence": "docs.odal-node.io/engine/architecture" + } + ] + } ] } ] diff --git a/site/dpp-landing/src/pages/product-groups/[key].astro b/site/dpp-landing/src/pages/product-groups/[key].astro index bbb2803..8512f73 100644 --- a/site/dpp-landing/src/pages/product-groups/[key].astro +++ b/site/dpp-landing/src/pages/product-groups/[key].astro @@ -28,6 +28,7 @@ type Manifest = { productCategories?: string[]; disclosure?: Record; plugin?: string; + notes?: string; }; export function getStaticPaths() { @@ -92,6 +93,20 @@ const description = `What Odal Node models for ${manifest.title.toLowerCase()}:
  • + {manifest.notes ? ( +
    +

    + This is the manifest's own notes field, reproduced verbatim. + It is where a group says which of its fields are a best reading rather + than a requirement, and it is written for the people maintaining the + schema rather than for this page. +

    +
    + {manifest.notes} +
    +
    + ) : null} + {manifest.productCategories?.length ? (

    diff --git a/site/dpp-landing/src/pages/product-groups/index.astro b/site/dpp-landing/src/pages/product-groups/index.astro new file mode 100644 index 0000000..6fe64ec --- /dev/null +++ b/site/dpp-landing/src/pages/product-groups/index.astro @@ -0,0 +1,151 @@ +--- +// /product-groups — the index for the twelve generated product-group pages. +// +// This exists so the footer carries one link rather than twelve, and so there +// is a single page that answers "which product groups does this cover?" — +// a question asked far more often than any individual group's schema version. +// +// Like the detail pages, everything here is read from the manifests vendored +// out of dpp-core at a pinned commit. Nothing is written by hand, so the count +// cannot drift from the software. +import Base from "../../layouts/Base.astro"; +import Section from "../../components/Section.astro"; +import pin from "../../../product-groups-source.json"; + +type Manifest = { + key: string; + title: string; + schemaVersions: string[]; + currentSchemaVersion: string; + productCategories?: string[]; + disclosure?: Record; + plugin?: string; + notes?: string; +}; + +const modules = import.meta.glob("../../data/product-groups/*.json", { eager: true }); +const groups = Object.values(modules) + .map((m: any) => (m.default ?? m) as Manifest) + .sort((a, b) => a.title.localeCompare(b.title)); + +// Distinct plugins, not manifests naming one: two groups share a plugin, so +// counting manifests reports one more than exists. The roadmap page counts the +// distinct set, and two pages on one site disagreeing about a number is worse +// than either number being slightly less flattering. +const boundToPlugin = groups.filter((g) => g.plugin).length; +const withPlugin = new Set(groups.filter((g) => g.plugin).map((g) => g.plugin)).size; +const withDisclosure = groups.filter((g) => Object.keys(g.disclosure ?? {}).length > 0).length; +const schemaVersions = groups.reduce((n, g) => n + (g.schemaVersions?.length ?? 0), 0); + +const title = "Product groups — what Odal Node models | Odal Node"; +const description = `The ${groups.length} product groups Odal Node models, with their current schema version, compliance plugin and disclosure classes. Generated from the manifests the software reads.`; + +// An ItemList is the one structured-data type that genuinely describes this +// page. It is a list of pages, and saying so is neither a rating nor a claim. +const itemListLd = { + "@context": "https://schema.org", + "@type": "ItemList", + name: "Product groups modelled by Odal Node", + numberOfItems: groups.length, + itemListElement: groups.map((g, i) => ({ + "@type": "ListItem", + position: i + 1, + name: g.title, + url: `https://odal-node.io/product-groups/${g.key}`, + })), +}; +--- + + +

    +

    + A product group decides which schema a passport is validated against and + which rules run over it. Each page below is generated from the manifest + the software itself reads, so a page cannot claim a schema version the + code does not serve. +

    +

    + Modelled is not the same as mandated. Most of these have + no delegated act yet. Modelling one early means the schema, the rules and + the disclosure split exist to be argued with now, rather than written in a + hurry once a date is fixed — and every page says plainly where its fields + are a best reading rather than a requirement. +

    + +
    +
    +

    Schema versions

    +

    {schemaVersions}

    +

    across {groups.length} groups, all still served

    +
    +
    +

    Compliance plugins

    +

    {withPlugin}

    +

    sandboxed WebAssembly, bound to {boundToPlugin} of the {groups.length} groups

    +
    +
    +

    With a disclosure split

    +

    {withDisclosure}

    +

    the rest await a delegated act

    +
    +
    +
    + +
    + +
    + +
    +

    + Every figure above is counted from {pin.path} in{" "} + {pin.repository}, + vendored at commit {pin.commit.slice(0, 8)}. A build here + fails if the vendored copies drift from that commit, so this page cannot + quietly fall behind the library. +

    +

    + No act numbers or dates appear on these pages deliberately.{" "} + The manifests carry no law — obligations and effective dates live in + separate instrument manifests that are not vendored here. A regulatory + date published on a marketing site is a claim that has to be kept current, + and a stale one is worse than none. +

    +
    + diff --git a/site/dpp-landing/src/pages/roadmap.astro b/site/dpp-landing/src/pages/roadmap.astro index 1e404ee..25c06e5 100644 --- a/site/dpp-landing/src/pages/roadmap.astro +++ b/site/dpp-landing/src/pages/roadmap.astro @@ -1,52 +1,134 @@ --- +// /roadmap — development status across the two public repositories. +// +// Rewritten 2026-09-19 after auditing every item against the repositories +// themselves rather than against the previous version of this page. What the +// audit found is why the page now looks the way it does: +// +// * Four items were understated. Twelve product groups were described as +// three; electronics and four "horizon" groups had already shipped. +// * Two were overstated. An operator dashboard was listed as in build and +// does not exist in any form, and the CLI — which is the actual control +// plane — was not mentioned at all. +// * Two were miscategorised in a way that mattered. The seal and registry +// work is finished and legally inert, which is not the same fact as +// "in progress", and a column reading "in build" said the wrong thing. +// * Two prose claims were false. This page said it was reviewed fortnightly, +// and linked to a public project board that does not exist. +// +// The rule the page now holds itself to: every item names something a reader +// can check without asking us, and no item is written in the grammar of a +// stronger claim than it can support. import Base from "../layouts/Base.astro"; import Section from "../components/Section.astro"; import OurRoadmap from "../components/Roadmap.astro"; +import data from "../data/roadmap.json"; -const title = "Roadmap — Odal Node"; +// Counted from the vendored manifests rather than written down, so the figure +// cannot drift from the library the way "battery, textile and steel" did. +const manifests = import.meta.glob("../data/product-groups/*.json", { eager: true }); +const groups = Object.values(manifests).map((m: any) => m.default ?? m); +const groupCount = groups.length; +const pluginCount = new Set(groups.filter((g) => g.plugin).map((g) => g.plugin)).size; + +const { versions } = data as unknown as { + versions: Record; +}; + +const title = "Roadmap — what is built, blocked and not started | Odal Node"; const description = - "What is shipped, what is under active development, and what is on the longer horizon for the dpp-core library and the dpp-engine infrastructure."; + "Development status for the Odal Node EU Digital Product Passport software: what ships today in dpp-core and dpp-engine, what is built but legally inert, what is in build, and the gaps we have not closed."; --- -
    +

    - This page mirrors the three-category roadmap presented on the home page and - adds detail. The categories are deliberately not tied to specific quarters — - timing on regulatory infrastructure is contingent on too many external - events (delegated act publication dates, schema-body votes, registry API - availability) to be promised precisely. The category itself is the - commitment. + Two public repositories: dpp-core, + the regulatory library, and dpp-engine, + the software that runs a node. This page is the status of both, audited + against the repositories rather than against the last version of this page.

    + +
    + {Object.values(versions).map((v) => ( +
    +

    {v.repo}

    +

    v{v.version}

    +

    {v.note}

    +
    + ))} +
    +

    product groups

    +

    {groupCount}

    +

    + {pluginCount} sandboxed rule plugins ·{" "} + see them all +

    +
    +
    +

    - The roadmap is reviewed and updated fortnightly. See{" "} - the public GitHub project board{" "} - for live status on individual items. + Nobody is running this in production. No customers, no + pilots, no live deployments, and nothing on this page should be read as + evidence of any. The battery passport obligation begins on 18 February + 2027, and we would rather be correct when it arrives than early and wrong. +

    +

    + There are no dates below. Timing on regulatory infrastructure depends on + delegated acts, standards-body votes and registry availability — none of + which we control — so the category is the commitment and a quarter would + be a guess dressed as one.

    -
    +
    -
    +
    +

    + An item is shipped when it is in a tagged release of the + relevant repository and a test exercises it through the running system. + Merged is not shipped, and neither is demonstrated — "shipped" here means + there is something that fails if it stops working. +

    +

    + An item is in build only with a named condition attached, + so that a reader can tell what would have to be true for it to move. An + item on the horizon is an option under consideration, and + is written in the grammar of an option: it carries no date, and no promise. +

    +

    + The fourth register is the one most roadmaps do not have.{" "} + Built and legally inert covers work that is finished, + tested and running, and that cannot be used for the thing it exists to do. + The sealing subsystem is the example: it works end to end and every seal it + produces reports itself as self-issued and of no legal effect, because + under eIDAS only a legal person may create an electronic seal. Filing that + under "in build" would turn a legal fact into an engineering timeline. +

    +

    + Items move backwards. Discovering while building something that it needs + more foundational work is an ordinary outcome of building it, and that work + returns to the horizon with a note about what was learned. A reversal + recorded is better than an item quietly drifting for six months. +

    +
    + +

    - A feature moves from Horizon to In build{" "} - when it enters a sprint and has a named owner. It moves from{" "} - In build to Shipped when it is in a - tagged release of the relevant repository, has documentation on{" "} - docs.odal-node.io, and has an - automated test exercising it through the running system — "shipped" means - verified, not merged. + It is not a schedule, and it is not exhaustive. The open issues in both + repositories are public and carry the detail this page compresses — + including the defects. If something you need is absent here, the useful + assumption is that it does not exist rather than that it was forgotten, + and the issue trackers are the place to say so.

    - Items can move backwards. Discovering that a feature in build needs more - foundational work is a reasonable outcome of building it; that work moves - back to the horizon with a note explaining what was learned. Honesty about - reversals is preferable to silently letting an item drift for six months - without status. + dpp-core issues{" "} + ·{" "} + dpp-engine issues

    From 8052c72397b4d42e20cc1c5036e998634f0d6d6b Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Sat, 19 Sep 2026 13:36:21 +0200 Subject: [PATCH 036/168] feat(landing): trim the nav to one destination and mark off-site links --- .../src/components/ExternalArrow.astro | 32 +++++++++++++++++++ site/dpp-landing/src/components/Footer.astro | 21 ++++++------ site/dpp-landing/src/components/Nav.astro | 30 +++++++---------- 3 files changed, 55 insertions(+), 28 deletions(-) create mode 100644 site/dpp-landing/src/components/ExternalArrow.astro diff --git a/site/dpp-landing/src/components/ExternalArrow.astro b/site/dpp-landing/src/components/ExternalArrow.astro new file mode 100644 index 0000000..d51d619 --- /dev/null +++ b/site/dpp-landing/src/components/ExternalArrow.astro @@ -0,0 +1,32 @@ +--- +// ExternalArrow.astro — the mark on a link that takes you off this site. +// +// Used by the nav (Docs) and the footer (GitHub, the security policy). The +// point is that a reader can tell, before clicking, which links leave — the +// nav and footer otherwise look identical whether a destination is a page here +// or someone else's domain. +// +// Decorative on purpose: `aria-hidden`, with no "opens in a new tab" label, +// because these links do not open a new tab. Announcing one would be a claim +// about behaviour that does not happen. +interface Props { + /** Extra classes, for tuning size or colour at the call site. */ + class?: string; +} +const { class: extra = "" } = Astro.props; +--- + + diff --git a/site/dpp-landing/src/components/Footer.astro b/site/dpp-landing/src/components/Footer.astro index 6400e87..7464116 100644 --- a/site/dpp-landing/src/components/Footer.astro +++ b/site/dpp-landing/src/components/Footer.astro @@ -3,6 +3,7 @@ // Uses the shared brand mark (currentColor variant) tinted ice for the dark band, // so it stays in sync with the asset instead of a hand-rolled inline copy. import OdalMark from "../assets/favicon.svg"; +import ExternalArrow from "./ExternalArrow.astro"; const year = new Date().getFullYear(); --- @@ -17,7 +18,7 @@ const year = new Date().getFullYear(); Signed by you. Verified by anyone.

    - Sovereign Digital Product Passport infrastructure for EU ESPR. + Sovereign Digital Product Passport infrastructure

    Explore

    -
    diff --git a/site/dpp-landing/src/components/Nav.astro b/site/dpp-landing/src/components/Nav.astro index 389f498..9d14816 100644 --- a/site/dpp-landing/src/components/Nav.astro +++ b/site/dpp-landing/src/components/Nav.astro @@ -7,16 +7,18 @@ import StatusBadge from "./StatusBadge.astro"; // hand-rolled inline SVG, so the nav stays in sync with the brand asset and the // mark inherits the wordmark's colour. Astro 5 imports .svg as a component. import OdalMark from "../assets/favicon.svg"; +import ExternalArrow from "./ExternalArrow.astro"; +import Button from "./Button.astro"; -// The nav stays at two destinations plus the CTA. Roadmap, Lifecycle, Who sees -// what and the product groups are reached from the footer instead: they are -// pages a reader goes looking for once, not places to move between, and a nav -// bar that lists everything stops telling anyone where to start. +// The nav is one destination plus the CTA. Roadmap, Lifecycle, Who sees what, +// the product groups and the repositories are all reached from the footer +// instead: they are pages a reader goes looking for once, not places to move +// between, and a nav bar that lists everything stops telling anyone where to +// start. `external` earns an arrow, so a reader can see which link leaves. const links = [ { href: "https://docs.odal-node.io", label: "Docs", external: true }, // { href: "/trust", label: "Trust", external: false }, // { href: "/about", label: "About", external: false }, - { href: "https://github.com/odal-node", label: "GitHub", external: true }, ]; const current = Astro.url.pathname; --- @@ -48,15 +50,10 @@ const current = Astro.url.pathname; }`} {...(l.external ? { rel: "noopener" } : {})} > - {l.label} + {l.label}{l.external && } ))} - - Join the waitlist - + @@ -85,15 +82,10 @@ const current = Astro.url.pathname; class="block rounded-md px-3 py-2.5 text-sm font-medium text-neutral-700 no-underline hover:bg-neutral-100" {...(l.external ? { rel: "noopener" } : {})} > - {l.label} + {l.label}{l.external && } ))} - - Join the waitlist - + From 8f6c18b2cce67009d45ab081d30089de5153f418 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Sat, 19 Sep 2026 13:36:25 +0200 Subject: [PATCH 037/168] feat(landing): one button, one card and one link style across the site --- site/dpp-landing/src/components/Button.astro | 101 +++++++++++ site/dpp-landing/src/components/Hero.astro | 12 +- site/dpp-landing/src/components/Section.astro | 21 ++- site/dpp-landing/src/pages/index.astro | 8 +- site/dpp-landing/src/pages/lifecycle.astro | 6 +- site/dpp-landing/src/pages/main.astro | 10 +- .../src/pages/product-groups/[key].astro | 8 +- .../src/pages/product-groups/index.astro | 8 +- site/dpp-landing/src/pages/roadmap.astro | 4 +- .../dpp-landing/src/pages/who-sees-what.astro | 22 +-- site/dpp-landing/src/styles/global.css | 160 +++++++++++++++++- 11 files changed, 317 insertions(+), 43 deletions(-) create mode 100644 site/dpp-landing/src/components/Button.astro diff --git a/site/dpp-landing/src/components/Button.astro b/site/dpp-landing/src/components/Button.astro new file mode 100644 index 0000000..3fde21b --- /dev/null +++ b/site/dpp-landing/src/components/Button.astro @@ -0,0 +1,101 @@ +--- +// Button.astro — the one button on this site. +// +// Before this there were five: the hero's two, the nav CTA, the mobile CTA and +// whatever a page reached for. They disagreed about radius, padding, weight +// and hover, which is the kind of thing nobody names but everybody sees. +// +// Two axes, because the site has two grounds: +// +// variant — `primary` carries the page's one main action; `secondary` sits +// beside it; `ghost` is a text action with no box. +// tone — `light` for the white and grey sections, `dark` for the navy +// hero and footer bands, where the same colours would vanish. +// +// Renders an when `href` is given and a +)} diff --git a/site/dpp-landing/src/components/Hero.astro b/site/dpp-landing/src/components/Hero.astro index db8c450..a3758e4 100644 --- a/site/dpp-landing/src/components/Hero.astro +++ b/site/dpp-landing/src/components/Hero.astro @@ -4,6 +4,7 @@ // one-line trust row. import Watermark from "../assets/favicon.svg"; import StatusBadge from "./StatusBadge.astro"; +import Button from "./Button.astro"; interface Props { eyebrow?: string; title: string; @@ -86,12 +87,13 @@ const { --> - + + {trustRow.length > 0 && (
      diff --git a/site/dpp-landing/src/components/Section.astro b/site/dpp-landing/src/components/Section.astro index defdd91..14b124b 100644 --- a/site/dpp-landing/src/components/Section.astro +++ b/site/dpp-landing/src/components/Section.astro @@ -1,6 +1,14 @@ --- // Section.astro — a standard content section with an eyebrow, title, and slot. // Tones: default = white, muted = warm grey, tinted = ice. +// +// The eyebrow now carries a short rule. An uppercase line of 10px letters with +// nothing beside it reads as a stray label; the rule turns it into a marker and +// gives the eye something to start the section on. +// +// Headings step up a size at `md` — at 3xl on a 1440px screen a section title +// sat at almost the same weight as the body around it, which is what made the +// long pages feel flat. interface Props { eyebrow?: string; title: string; @@ -13,16 +21,23 @@ const { eyebrow, title, id, tone = "default", wide = false } = Astro.props; const bg = tone === "muted" ? "bg-neutral-50" : tone === "tinted" ? "bg-primary-50" : "bg-transparent"; const width = wide ? "max-w-6xl" : "max-w-5xl"; + +// A section given an id is a section something links to, so its heading gets +// the id and becomes the anchor target rather than the band around it — +// otherwise the browser scrolls to the top of the padding and the title lands +// off screen. +const headingId = id ? `${id}-title` : undefined; --- -
      +
      {eyebrow && ( -

      +

      + {eyebrow}

      )} -

      +

      {title}

      diff --git a/site/dpp-landing/src/pages/index.astro b/site/dpp-landing/src/pages/index.astro index cce5194..713ea18 100644 --- a/site/dpp-landing/src/pages/index.astro +++ b/site/dpp-landing/src/pages/index.astro @@ -136,7 +136,7 @@ const steps = [ v: "One command to a running node — it builds from source, so allow a few minutes. No licence keys, no passport caps. Ever.", }, ].map((f) => ( -
      +

      {f.k}

      {f.v}

      @@ -153,21 +153,21 @@ const steps = [ Stated precisely:

      -
      +

      We can see

      The signed passport you publish to a resolver we operate, and the metadata required to serve it.

      -
      +

      We cannot see

      Your private keys (held in-process on your infrastructure), your raw production data, your supply-chain detail.

      -
      +

      We could see but do not

      The contents of your import files, which the node discards after diff --git a/site/dpp-landing/src/pages/lifecycle.astro b/site/dpp-landing/src/pages/lifecycle.astro index 9d64c24..a067f27 100644 --- a/site/dpp-landing/src/pages/lifecycle.astro +++ b/site/dpp-landing/src/pages/lifecycle.astro @@ -35,7 +35,7 @@ const unmatched = stations.stations.filter((s) => s.unmatched); {stations.stations.map((s) => (

      @@ -54,8 +54,8 @@ const unmatched = stations.stations.filter((s) => s.unmatched);

      {s.endpoint}

      {s.link && (

      - - Operate this one yourself → + + Operate this one yourself

      )} diff --git a/site/dpp-landing/src/pages/main.astro b/site/dpp-landing/src/pages/main.astro index 0c210ac..30a978b 100644 --- a/site/dpp-landing/src/pages/main.astro +++ b/site/dpp-landing/src/pages/main.astro @@ -54,8 +54,8 @@ const description = {stations.stations.map((s) => (
      @@ -75,7 +75,7 @@ const description =

      - Read the eight in full → + Read the eight in full

      @@ -90,7 +90,7 @@ const description = and watch the fields appear and vanish.

      - Try it → + Try it

      @@ -109,7 +109,7 @@ const description = it arrives than early and wrong.

      - The roadmap says which is which → + The roadmap says which is which

    diff --git a/site/dpp-landing/src/pages/product-groups/[key].astro b/site/dpp-landing/src/pages/product-groups/[key].astro index 8512f73..a4059df 100644 --- a/site/dpp-landing/src/pages/product-groups/[key].astro +++ b/site/dpp-landing/src/pages/product-groups/[key].astro @@ -74,17 +74,17 @@ const description = `What Odal Node models for ${manifest.title.toLowerCase()}:

    -
    +

    Current schema

    v{manifest.currentSchemaVersion}

    -
    +

    Versions modelled

    {versionCount}

    -
    +

    Compliance plugin

    {manifest.plugin ?? "none"} @@ -146,7 +146,7 @@ const description = `What Odal Node models for ${manifest.title.toLowerCase()}:

    {classes.map(([cls, fields]) => ( -
    +

    {cls}

    {fields.length} field(s) diff --git a/site/dpp-landing/src/pages/product-groups/index.astro b/site/dpp-landing/src/pages/product-groups/index.astro index 6fe64ec..55509ae 100644 --- a/site/dpp-landing/src/pages/product-groups/index.astro +++ b/site/dpp-landing/src/pages/product-groups/index.astro @@ -73,17 +73,17 @@ const itemListLd = {

    -
    +

    Schema versions

    {schemaVersions}

    across {groups.length} groups, all still served

    -
    +

    Compliance plugins

    {withPlugin}

    sandboxed WebAssembly, bound to {boundToPlugin} of the {groups.length} groups

    -
    +

    With a disclosure split

    {withDisclosure}

    the rest await a delegated act

    @@ -98,7 +98,7 @@ const itemListLd = { return (

    {g.title}

    diff --git a/site/dpp-landing/src/pages/roadmap.astro b/site/dpp-landing/src/pages/roadmap.astro index 25c06e5..de003d4 100644 --- a/site/dpp-landing/src/pages/roadmap.astro +++ b/site/dpp-landing/src/pages/roadmap.astro @@ -51,13 +51,13 @@ const description =
    {Object.values(versions).map((v) => ( -
    +

    {v.repo}

    v{v.version}

    {v.note}

    ))} -
    +

    product groups

    {groupCount}

    diff --git a/site/dpp-landing/src/pages/who-sees-what.astro b/site/dpp-landing/src/pages/who-sees-what.astro index af70c80..1cc401f 100644 --- a/site/dpp-landing/src/pages/who-sees-what.astro +++ b/site/dpp-landing/src/pages/who-sees-what.astro @@ -36,13 +36,16 @@ const description =

    -
    + +
    {access.audiences.map((a, i) => ( +
    + +
    +
    + + +
    + +
    +
    +
    +

    Product groups

    + +
    + + +
    +
    +
    + + + Docs +
    @@ -71,25 +184,182 @@ const current = Astro.url.pathname; - -
    diff --git a/site/dpp-landing/src/pages/product-groups/[key].astro b/site/dpp-landing/src/pages/product-groups/[key].astro index a4059df..02dfb3c 100644 --- a/site/dpp-landing/src/pages/product-groups/[key].astro +++ b/site/dpp-landing/src/pages/product-groups/[key].astro @@ -165,7 +165,7 @@ const description = `What Odal Node models for ${manifest.title.toLowerCase()}: Note the two classes above are not ordered. Conformity evidence goes to authorities and not to recyclers; the individual item's history goes to recyclers and not to authorities. See{" "} - who sees what. + visibility.

    )} diff --git a/site/dpp-landing/src/pages/who-sees-what.astro b/site/dpp-landing/src/pages/visibility.astro similarity index 98% rename from site/dpp-landing/src/pages/who-sees-what.astro rename to site/dpp-landing/src/pages/visibility.astro index 1cc401f..702726b 100644 --- a/site/dpp-landing/src/pages/who-sees-what.astro +++ b/site/dpp-landing/src/pages/visibility.astro @@ -1,5 +1,5 @@ --- -// /who-sees-what — the Art. 77(2) access lattice, operated rather than asserted. +// /visibility — the Art. 77(2) access lattice, operated rather than asserted. // // The point of this page is that a visitor changes the audience and watches // fields appear and vanish. A table of the same information is a claim; a From 311bbebfab0ca22eccda092d884f98b1d777aeae Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 25 Sep 2026 19:17:48 +0200 Subject: [PATCH 041/168] feat(landing): rewrite lifecycle and product-group pages in plain language, without routes, versions or commits --- site/dpp-landing/src/components/Nav.astro | 8 +- .../src/data/product-group-copy.json | 40 +++ site/dpp-landing/src/data/stations.json | 47 ++-- site/dpp-landing/src/lib/product-groups.ts | 68 +++++ site/dpp-landing/src/pages/_main.astro | 4 +- site/dpp-landing/src/pages/lifecycle.astro | 72 +++-- .../src/pages/product-groups/[key].astro | 261 ++++++++---------- .../src/pages/product-groups/index.astro | 170 ++++-------- 8 files changed, 331 insertions(+), 339 deletions(-) create mode 100644 site/dpp-landing/src/lib/product-groups.ts diff --git a/site/dpp-landing/src/components/Nav.astro b/site/dpp-landing/src/components/Nav.astro index 425f6dd..235a510 100644 --- a/site/dpp-landing/src/components/Nav.astro +++ b/site/dpp-landing/src/components/Nav.astro @@ -21,13 +21,7 @@ import OdalMark from "../assets/favicon.svg"; import ExternalArrow from "./ExternalArrow.astro"; import Button from "./Button.astro"; import ProductGroupIcon from "./ProductGroupIcon.astro"; -import copy from "../data/product-group-copy.json"; - -const groups = copy.order.map((key) => ({ - key, - ...(copy.groups as Record)[key], -})); -const unsold = copy.groups["unsold-goods"]; +import { groups, unsoldGoods as unsold } from "../lib/product-groups"; const explore = [ { diff --git a/site/dpp-landing/src/data/product-group-copy.json b/site/dpp-landing/src/data/product-group-copy.json index 1365518..70041e6 100644 --- a/site/dpp-landing/src/data/product-group-copy.json +++ b/site/dpp-landing/src/data/product-group-copy.json @@ -62,5 +62,45 @@ "name": "Unsold goods disclosure", "line": "What was discarded, and why. A report, not a passport" } + }, + "_labels_comment": "Human labels for the manifests' raw category values and non-public field names, so no page shows a key like 'lmt' or 'stateOfHealthPct'. A value missing here falls back to a generic humaniser, so a new field in dpp-core never breaks a page; it just reads less well until it is added here.", + "categoryLabels": { + "ev": "Electric vehicle", + "lmt": "Light means of transport", + "starting-lighting-ignition": "Starting, lighting and ignition", + "structural-steel": "Structural steel", + "surface-cleaner": "Surface cleaner", + "personal-care": "Personal care", + "other-mobile-phone": "Other mobile phone", + "cordless-phone": "Cordless phone", + "flat": "Flat products", + "long": "Long products", + "tube": "Tubes", + "C1": "C1: passenger car tyres", + "C2": "C2: light commercial vehicle tyres", + "C3": "C3: heavy vehicle tyres" + }, + "fieldLabels": { + "disassemblyInstructionsUrl": "Disassembly instructions", + "disassemblyInstructions": "Disassembly instructions", + "cathodeMaterial": "Cathode material", + "anodeMaterial": "Anode material", + "electrolyteMaterial": "Electrolyte material", + "sohMethodology": "How state of health is measured", + "stateOfHealth": "State of health", + "stateOfHealthPct": "State of health as a percentage", + "expectedLifetime": "Expected lifetime", + "dynamicPerformance": "Performance over time", + "batteryStatus": "Battery status", + "usageHistory": "Usage history", + "componentPartNumbers": "Component part numbers", + "sparePartsContacts": "Spare-part sources", + "safetyMeasures": "Safety measures", + "testReportResults": "Test report results", + "epdUrl": "Environmental product declaration", + "svhcSubstances": "Substances of very high concern", + "repairManualUrl": "Repair manual", + "criticalRawMaterials": "Critical raw materials", + "sparePartsAvailable": "Spare-part availability" } } diff --git a/site/dpp-landing/src/data/stations.json b/site/dpp-landing/src/data/stations.json index 19c3e1d..5bff7b7 100644 --- a/site/dpp-landing/src/data/stations.json +++ b/site/dpp-landing/src/data/stations.json @@ -1,81 +1,70 @@ { - "_comment": "The chain of custody as eight stations — the spine shared by /lifecycle and the unpublished draft home page. Endpoints are the engine's real routes. Stations 6, 7 and 8 are the ones the live home page never reaches, and they are the three with no competitor equivalent. Every 'who' is an actor named in law, not an invented persona.", + "_comment": "The chain of custody as eight stations, the spine of /lifecycle. Written for a general reader: no routes, commands or field names, which belong on the docs site. Every station describes something the software does today; checked against the engine's routes on 2026-09-25. No competitive claims: nobody here has seen what other tools ship past publishing, so the page does not say.", "stations": [ { "n": 1, "key": "declare", "title": "Declare", "who": "Manufacturer, importer", - "endpoint": "/integrator/api/v1/import/{productGroup}", - "lead": "The data arrives the way it actually exists: spreadsheets.", - "body": "CSV and Excel, with a dry run that reports errors per row rather than rejecting the file. Nobody's product data starts life in a schema, and a tool that insists otherwise is a tool nobody can start using." + "lead": "Product data arrives the way it already exists: in spreadsheets.", + "body": "Upload a CSV or Excel file and run a trial first. The node reports what is wrong row by row instead of rejecting the whole file, so nobody has to rebuild their data before they can start." }, { "n": 2, "key": "evidence", "title": "Evidence", "who": "Supplier, test laboratory", - "endpoint": "/api/v1/dpp/{id}/evidence", - "lead": "A document a supplier has not sent is visible as a gap, not as a blank.", - "body": "Evidence is a first-class object with its own address and its own verification, rather than a field someone filled in. The difference matters when an auditor asks where a number came from." + "lead": "Supporting documents sit on the record they support.", + "body": "Test reports and supplier declarations are attached to the passport and can be checked on their own. When an auditor asks where a number came from, the answer is on the record." }, { "n": 3, "key": "check", "title": "Check", "who": "Compliance lead", - "endpoint": "/api/v1/dpp/{id}/lint", - "lead": "Checked against the product group's signed ruleset, before anything is signed.", - "body": "Rules arrive as signed bundles and are verified fail-closed. When a threshold changes, the ruleset changes — not the software, and not a redeploy." + "lead": "Checked against the product group's rules before anything is signed.", + "body": "Most product groups carry their own set of rules. The rules arrive as signed updates, so when a threshold changes the rules change with it, without reinstalling anything." }, { "n": 4, "key": "seal", "title": "Seal", - "who": "The operator, alone", - "endpoint": "/api/v1/dpp/{id}/publish", - "lead": "Signed with the operator's own key, on their own infrastructure. Claims freeze here.", - "body": "The identity is a did:web in a domain they control. Nothing is sent anywhere to be signed, which is the difference between holding a record and renting access to it." + "who": "The operator", + "lead": "Signed with your own key, on your own servers.", + "body": "The signature is tied to an identity on a web domain you control, and nothing is sent anywhere to be signed. That is the difference between holding a record and renting access to it." }, { "n": 5, "key": "resolve", "title": "Resolve", "who": "Consumer, retailer", - "endpoint": "/01/{gtin}/21/{serial}", - "lead": "Scan the code, get the public view. No login, no fee — because the regulation says so.", - "body": "A GS1 Digital Link when the product is identified by a GTIN, and a plain passport URL when it is not. Requiring the barcode would mean requiring GS1 membership." + "lead": "Scan the code, see the public passport. No login, no fee.", + "body": "The code on the product leads straight to its passport. Products with a barcode number resolve through GS1 Digital Link; products without one get a plain web address, so nobody needs a GS1 membership to take part." }, { "n": 6, "key": "disclose", "title": "Disclose", "who": "Repairer, recycler, authority", - "endpoint": "/credential/dpp/{id}", - "lead": "Depth proven by credential, not by a header the caller sets.", - "body": "The same record answers differently depending on who is asking, enforced per field. For batteries this is a lattice rather than a ladder: an authority sees less than a recycler in one specific respect.", - "unmatched": true, + "lead": "Each reader sees the part of the record they are entitled to.", + "body": "The same passport answers a repairer, a recycler and an authority differently, field by field. Access is proven with a credential the reader holds, not by a setting they can change. For batteries the rules are not a simple ladder: an authority and a recycler each see something the other cannot.", "link": "/visibility" }, { "n": 7, "key": "handover", "title": "Hand over", - "who": "Two legal entities", - "endpoint": "/api/v1/dpp/{id}/transfer/initiate · /accept", + "who": "Two companies", "lead": "Responsibility changes hands with both parties' signatures on the record.", - "body": "Remanufacture, repurposing, a sold product line. The chain of custody is part of the passport rather than an email thread, and either side can refuse or withdraw.", - "unmatched": true + "body": "Remanufacturing, repurposing or selling a product line: the handover is part of the passport rather than an email thread, and either side can decline or withdraw before it completes." }, { "n": 8, "key": "end", "title": "End", "who": "Recycler, auditor", - "endpoint": "/api/v1/dpp/{id}/eol · /evidence", - "lead": "A typed end of life, and a dossier that verifies with no network and no trust in us.", - "body": "Passport, signatures, identity documents and the full hash-chained history in one signed file. Someone who has never heard of Odal Node can check it offline.", - "unmatched": true + "lead": "A recorded end of life, and a file anyone can check offline.", + "body": "The passport, its signatures and its full history export as one signed file. Someone who has never heard of Odal Node can verify it without a network connection and without trusting us." } ] } diff --git a/site/dpp-landing/src/lib/product-groups.ts b/site/dpp-landing/src/lib/product-groups.ts new file mode 100644 index 0000000..c7a988d --- /dev/null +++ b/site/dpp-landing/src/lib/product-groups.ts @@ -0,0 +1,68 @@ +// product-groups.ts — the one place pages read product groups from. +// +// Joins the manifests vendored from dpp-core (what the software models) with +// product-group-copy.json (how this site words it for a general reader), and +// turns raw category values and field names into labels. Pages never print a +// manifest key or field name directly. +import copy from "../data/product-group-copy.json"; + +export type Manifest = { + key: string; + title: string; + productCategories?: string[]; + disclosure?: Record; +}; + +export type Group = Manifest & { name: string; line: string }; + +const modules = import.meta.glob("../data/product-groups/*.json", { eager: true }); +const manifests = new Map( + Object.values(modules).map((m: any) => { + const d = (m.default ?? m) as Manifest; + return [d.key, d]; + }), +); + +const words = copy.groups as Record; + +function toGroup(key: string): Group { + const manifest = manifests.get(key); + if (!manifest) throw new Error(`product-group-copy.json names "${key}", which has no manifest`); + const w = words[key]; + if (!w) throw new Error(`manifest "${key}" has no wording in product-group-copy.json`); + return { ...manifest, ...w }; +} + +/** The product groups, in the site's display order. Excludes unsold goods. */ +export const groups: Group[] = copy.order.map(toGroup); + +/** Unsold goods: modelled beside the groups, but not a product group. */ +export const unsoldGoods: Group = toGroup("unsold-goods"); + +/** Every page that gets generated: the groups plus unsold goods. */ +export const allPages: Group[] = [...groups, unsoldGoods]; + +// A manifest the wording file forgot would silently get no page and no menu +// entry. Fail the build instead. +for (const key of manifests.keys()) { + if (!allPages.some((g) => g.key === key)) { + throw new Error(`manifest "${key}" is missing from product-group-copy.json "order"`); + } +} + +function humanise(raw: string): string { + const spaced = raw + .replace(/Url$/, "") + .replace(/Pct$/, " percentage") + .replace(/([a-z])([A-Z])/g, "$1 $2") + .replace(/[-_]/g, " ") + .toLowerCase() + .trim(); + return spaced.charAt(0).toUpperCase() + spaced.slice(1); +} + +export const categoryLabel = (raw: string): string => + (copy.categoryLabels as Record)[raw] ?? humanise(raw); + +export const fieldLabel = (raw: string): string => + (copy.fieldLabels as Record)[raw] ?? humanise(raw); diff --git a/site/dpp-landing/src/pages/_main.astro b/site/dpp-landing/src/pages/_main.astro index 873cfcb..3dac6ba 100644 --- a/site/dpp-landing/src/pages/_main.astro +++ b/site/dpp-landing/src/pages/_main.astro @@ -55,7 +55,7 @@ const description = = 6) ? "bg-primary-50" : "" }`} >
    @@ -63,7 +63,7 @@ const description = {String(s.n).padStart(2, "0")} {s.title} - {s.unmatched && ( + {(s.n >= 6) && ( Unmatched diff --git a/site/dpp-landing/src/pages/lifecycle.astro b/site/dpp-landing/src/pages/lifecycle.astro index a067f27..5d715b2 100644 --- a/site/dpp-landing/src/pages/lifecycle.astro +++ b/site/dpp-landing/src/pages/lifecycle.astro @@ -1,61 +1,54 @@ --- // /lifecycle — the chain of custody, long-form. // -// The longest and most credible route, chosen deliberately: this page's job is -// not to convert quickly but to be the thing a sceptical reader finishes and -// believes. Every station names a real endpoint and a real actor, so the page -// can be checked rather than taken on trust. +// The longest route on the site, chosen deliberately: this page's job is not +// to convert quickly but to be the thing a sceptical reader finishes and +// believes. It is written for anyone, so it names who touches the record at +// each station and what happens there, and leaves routes and commands to the +// docs site. import Base from "../layouts/Base.astro"; import Section from "../components/Section.astro"; import stations from "../data/stations.json"; -const title = "The chain of custody — eight stations | Odal Node"; +const title = "Lifecycle: one passport, eight stations | Odal Node"; const description = - "One record, eight stations: declare, evidence, check, seal, resolve, disclose, hand over, end. Each with the actor who touches it and the endpoint that serves them."; + "One record, eight stations: declare, evidence, check, seal, resolve, disclose, hand over, end. Who touches the passport at each one, and what happens there."; -const unmatched = stations.stations.filter((s) => s.unmatched); +// Stations six to eight are the ones that only matter after the sale. +const afterSale = stations.stations.filter((s) => s.n >= 6); --- -
    +

    - Most descriptions of a product passport stop at publication — import, - validate, sign, publish. That is the first half. A product is repaired, - resold, remanufactured and eventually recycled, and the record has to + Most descriptions of a product passport stop at publication: import, + check, sign, publish. That is the first half. A product is then repaired, + resold, remanufactured and eventually recycled, and its record has to survive all of it while answering different people differently.

    - Eight stations. Each names the actor who touches the record there, and the - endpoint that serves them. + Eight stations. Each names who touches the record there, and what + happens to it.

    -
    +

    - Import, validate, sign, publish, resolve — that is a passport generator, - and it is genuinely useful. It is also where the difficult half begins. + Import, check, sign, publish, resolve: that is a passport generator, and + it is useful. It is also where the harder half begins.

    - Stations {unmatched.map((s) => s.n).join(", ")} are the ones that make a - record survive a decade of ownership changes rather than describing a - product on the day it was made: + Stations {afterSale.map((s) => s.n).join(", ")} are what keep a record + intact through years of repairs and changes of owner, rather than + describing a product only on the day it was made:

      - {unmatched.map((s) => ( + {afterSale.map((s) => (
    • - {s.title} — {s.lead} + {s.title}: {s.lead}
    • ))}

    - They are the reason this page is long. A capability that only shows up - three years after purchase cannot be demonstrated in a screenshot, so the - alternative is to describe it precisely enough to be checked. + They are why this page is long. A capability that only matters years + after purchase cannot be shown in a screenshot, so the next best thing is + to describe it precisely.

    All eight stations exist in the software today. Nobody is running - it in production — no customers, no pilots, no live deployments. + it in production: no customers, no pilots, no live deployments. The battery passport obligation begins on 18 February 2027, and we would rather be correct when it arrives than early and wrong.

    - The code is open:{" "} - dpp-core is Apache-2.0 - and can be read, audited or reused by anyone, including a competitor. + The core library is{" "} + open source under + Apache-2.0, and anyone can read, audit or reuse it, including a + competitor.

    diff --git a/site/dpp-landing/src/pages/product-groups/[key].astro b/site/dpp-landing/src/pages/product-groups/[key].astro index 02dfb3c..e28800a 100644 --- a/site/dpp-landing/src/pages/product-groups/[key].astro +++ b/site/dpp-landing/src/pages/product-groups/[key].astro @@ -1,190 +1,155 @@ --- // /product-groups/[key] — one page per modelled product group, generated from -// the manifests vendored out of dpp-core at a pinned commit. +// the manifests vendored out of dpp-core and worded by product-group-copy.json. // -// WHY GENERATED RATHER THAN WRITTEN -// -// Twelve hand-written pages are twelve things to forget when a schema version -// moves. These render from the same manifests the software reads, so a page -// cannot claim a version the code does not serve. `check:product-groups` fails -// the build when the vendored copies drift from the pin. +// Written for a general reader: no schema versions, plugin names, field keys +// or commit hashes. Those belong on the docs site. A page cannot claim a group +// the software does not model, because it is generated from the same +// definitions the software reads. // // WHAT THESE PAGES DELIBERATELY DO NOT SAY // // No act numbers, no dates, no "mandatory from". The product-group manifests -// carry no law — that lives in core's instrument manifests, which are not -// vendored here. A page that stated an obligation would be a regulatory claim -// this repository would then have to keep current, and the one thing worse than -// no date is a stale one. +// carry no law, and a regulatory date published here would be a claim this +// repository then has to keep current. The one thing worse than no date is a +// stale one. import Base from "../../layouts/Base.astro"; import Section from "../../components/Section.astro"; -import pin from "../../../product-groups-source.json"; - -type Manifest = { - key: string; - title: string; - schemaVersions: string[]; - currentSchemaVersion: string; - productCategories?: string[]; - disclosure?: Record; - plugin?: string; - notes?: string; -}; +import ProductGroupIcon from "../../components/ProductGroupIcon.astro"; +import { allPages, categoryLabel, fieldLabel, type Group } from "../../lib/product-groups"; export function getStaticPaths() { - const all = import.meta.glob("../../data/product-groups/*.json", { eager: true }); - return Object.values(all).map((m: any) => { - const d = (m.default ?? m) as Manifest; - return { params: { key: d.key }, props: { manifest: d } }; - }); + return allPages.map((g) => ({ params: { key: g.key }, props: { group: g } })); } -const { manifest } = Astro.props as { manifest: Manifest }; +const { group } = Astro.props as { group: Group }; +const isUnsold = group.key === "unsold-goods"; -// The disclosure map is field -> class. Invert it: a reader wants to know which -// fields share a class, not to scan a field list for the one they care about. +// The disclosure map is field -> class. Invert it: a reader wants to know who +// sees what, not to scan a field list for the one they care about. const byClass = new Map(); -for (const [field, cls] of Object.entries(manifest.disclosure ?? {})) { +for (const [field, cls] of Object.entries(group.disclosure ?? {})) { if (!byClass.has(cls)) byClass.set(cls, []); - byClass.get(cls)!.push(field); + byClass.get(cls)!.push(fieldLabel(field)); } -const classes = [...byClass.entries()].sort((a, b) => a[0].localeCompare(b[0])); -// What each class means. `conformity` and `individual` appear for batteries -// only, because Art. 77(2) is the only place that split is currently law. -const CLASS_NOTE: Record = { - restricted: "Not public. Shown to a reader holding a credential for this product group.", - conformity: "Conformity evidence. Battery Annex XIII point 3 — notified bodies, market surveillance and the Commission.", - individual: "The individual item's own history. Battery Annex XIII point 4 — legitimate interest only, and explicitly not authorities.", +// Who each class is for, in plain words. `conformity` and `individual` appear +// for batteries only, because that is the one product group whose own law +// splits access this way. +const CLASS: Record = { + restricted: { + title: "For readers with a credential", + note: "Not public. Shown only to a reader who holds a credential for this product group.", + }, + conformity: { + title: "For authorities only", + note: "Evidence that the product meets the rules. Shown to notified bodies, market surveillance authorities and the Commission.", + }, + individual: { + title: "For readers with a legitimate interest only", + note: "The individual item's own history. Shown only to readers with a legitimate interest in it, and not to authorities.", + }, }; +const ORDER = ["restricted", "conformity", "individual"]; +const classes = [...byClass.entries()].sort( + (a, b) => ORDER.indexOf(a[0]) - ORDER.indexOf(b[0]), +); -const versionCount = manifest.schemaVersions?.length ?? 0; -const title = `${manifest.title} — product group | Odal Node`; -const description = `What Odal Node models for ${manifest.title.toLowerCase()}: ${versionCount} schema version(s), currently ${manifest.currentSchemaVersion}, with its disclosure classes and plugin binding.`; +const title = `${group.name}: product group | Odal Node`; +const description = isUnsold + ? "How Odal Node records the disclosure of discarded unsold products: a yearly report by the operator, not a product passport." + : `What an Odal Node passport for ${group.name.toLowerCase()} carries, and who may read each part of it.`; --- -
    -

    - Everything on this page is generated from the product-group manifest the - software itself reads, vendored from{" "} - {pin.repository} at a - pinned commit. It describes what is modelled, not what the - law requires — see the note at the foot. -

    - -
    -
    -

    Current schema

    -

    - v{manifest.currentSchemaVersion} -

    -
    -
    -

    Versions modelled

    -

    {versionCount}

    -
    -
    -

    Compliance plugin

    -

    - {manifest.plugin ?? "none"} -

    -
    +
    +
    +
    -
    - - {manifest.notes ? ( -
    +

    {group.line}.

    + {isUnsold ? (

    - This is the manifest's own notes field, reproduced verbatim. - It is where a group says which of its fields are a best reading rather - than a requirement, and it is written for the people maintaining the - schema rather than for this page. + This is not a product group, and it carries no passport. Companies + that discard unsold consumer products must publish, each year, how + many they discarded, why, and what happened to them; the smallest + companies are exempt. Odal Node records that disclosure line by line, + per type of product, reason and destination, for the company rather + than for any single product.

    -
    - {manifest.notes} -
    -
    - ) : null} + ) : ( +

    + This page is generated from the same product definition the software + uses to build and check a passport. It describes{" "} + what is modelled, not what the law requires. +

    + )} +
    - {manifest.productCategories?.length ? ( -
    + {group.productCategories?.length ? ( +

    - A category changes which rules apply. These are the values the schema - accepts, not a marketing segmentation. + A category can change which rules apply to a passport, so the software + asks for one.

    - {manifest.productCategories.map((c) => ( - - {c} + {group.productCategories.map((c) => ( + + {categoryLabel(c)} ))}
    ) : null} -
    - {classes.length === 0 ? ( - <> -

    - This product group declares no disclosure classes. That - is not an omission — it is the absence of a delegated act. -

    -

    - ESPR Art. 11(b) gives each actor access "based on their respective - access rights set out in the applicable delegated act adopted pursuant - to Article 4". Until that act exists for - {" "}{manifest.title.toLowerCase()}, there is no split to model, and - inventing one would be asserting a rule nobody has written. -

    - - ) : ( - <> -

    - Every field below is withheld from the public view. The class decides - who may see it. -

    -
    - {classes.map(([cls, fields]) => ( -
    -
    -

    {cls}

    - {fields.length} field(s) -
    - {CLASS_NOTE[cls] && ( -

    {CLASS_NOTE[cls]}

    - )} -

    - {fields.sort().join(" · ")} -

    -
    - ))} -
    - {byClass.has("conformity") && byClass.has("individual") && ( -

    - Note the two classes above are not ordered. Conformity - evidence goes to authorities and not to recyclers; the individual - item's history goes to recyclers and not to authorities. See{" "} - visibility. + {!isUnsold && ( +

    + {classes.length === 0 ? ( + <> +

    + Everything in this passport is public for now.{" "} + That is not an oversight. Under ESPR, who may see which part of a + passport is set by each product group's own rules, and none have + been adopted for {group.name.toLowerCase()} yet. Inventing a split + would mean asserting a rule nobody has written.

    - )} - - )} -
    + + ) : ( + <> +

    + The public sees the rest of the passport. These parts are shown only + to the readers named. +

    +
    + {classes.map(([cls, fields]) => ( +
    +

    + {CLASS[cls]?.title ?? "Not public"} +

    + {CLASS[cls] &&

    {CLASS[cls].note}

    } +
      + {fields.sort().map((f) => ( +
    • {f}
    • + ))} +
    +
    + ))} +
    + {byClass.has("conformity") && byClass.has("individual") && ( +

    + These are not ranked. Authorities see the + conformity evidence and not the item's own history; readers with a + legitimate interest see the history and not the evidence. See{" "} + how visibility works. +

    + )} + + )} +
    + )} -
    -

    - Generated from {pin.path}/{manifest.key}.json in - {" "}{pin.repository}, - vendored at commit {pin.commit.slice(0, 8)}. A build fails if - the vendored copy drifts from that commit. -

    +

    - No dates or act numbers appear on this page deliberately.{" "} - Product-group manifests carry no law. Obligations, effective dates and - passport requirements live in separate instrument manifests that are not - vendored here — because a regulatory date published on a marketing site is - a claim that has to be kept current, and a stale one is worse than none. + See all product groups

    diff --git a/site/dpp-landing/src/pages/product-groups/index.astro b/site/dpp-landing/src/pages/product-groups/index.astro index 55509ae..46980f8 100644 --- a/site/dpp-landing/src/pages/product-groups/index.astro +++ b/site/dpp-landing/src/pages/product-groups/index.astro @@ -1,44 +1,18 @@ --- -// /product-groups — the index for the twelve generated product-group pages. +// /product-groups — the index for the generated product-group pages. // -// This exists so the footer carries one link rather than twelve, and so there -// is a single page that answers "which product groups does this cover?" — -// a question asked far more often than any individual group's schema version. -// -// Like the detail pages, everything here is read from the manifests vendored -// out of dpp-core at a pinned commit. Nothing is written by hand, so the count -// cannot drift from the software. +// Answers "which products does this cover?" for a general reader. Every group +// is read from the manifests vendored out of dpp-core and worded by +// product-group-copy.json, so the list cannot drift from the software. No +// counts, versions or commits: those belong on the docs site. import Base from "../../layouts/Base.astro"; import Section from "../../components/Section.astro"; -import pin from "../../../product-groups-source.json"; - -type Manifest = { - key: string; - title: string; - schemaVersions: string[]; - currentSchemaVersion: string; - productCategories?: string[]; - disclosure?: Record; - plugin?: string; - notes?: string; -}; - -const modules = import.meta.glob("../../data/product-groups/*.json", { eager: true }); -const groups = Object.values(modules) - .map((m: any) => (m.default ?? m) as Manifest) - .sort((a, b) => a.title.localeCompare(b.title)); +import ProductGroupIcon from "../../components/ProductGroupIcon.astro"; +import { groups, unsoldGoods } from "../../lib/product-groups"; -// Distinct plugins, not manifests naming one: two groups share a plugin, so -// counting manifests reports one more than exists. The roadmap page counts the -// distinct set, and two pages on one site disagreeing about a number is worse -// than either number being slightly less flattering. -const boundToPlugin = groups.filter((g) => g.plugin).length; -const withPlugin = new Set(groups.filter((g) => g.plugin).map((g) => g.plugin)).size; -const withDisclosure = groups.filter((g) => Object.keys(g.disclosure ?? {}).length > 0).length; -const schemaVersions = groups.reduce((n, g) => n + (g.schemaVersions?.length ?? 0), 0); - -const title = "Product groups — what Odal Node models | Odal Node"; -const description = `The ${groups.length} product groups Odal Node models, with their current schema version, compliance plugin and disclosure classes. Generated from the manifests the software reads.`; +const title = "Product groups | Odal Node"; +const description = + "The product groups Odal Node can build passports for, what each passport carries, and who may read each part of it."; // An ItemList is the one structured-data type that genuinely describes this // page. It is a list of pages, and saying so is neither a rating nor a claim. @@ -46,106 +20,74 @@ const itemListLd = { "@context": "https://schema.org", "@type": "ItemList", name: "Product groups modelled by Odal Node", - numberOfItems: groups.length, - itemListElement: groups.map((g, i) => ({ + itemListElement: [...groups, unsoldGoods].map((g, i) => ({ "@type": "ListItem", position: i + 1, - name: g.title, + name: g.name, url: `https://odal-node.io/product-groups/${g.key}`, })), }; --- -
    +

    - A product group decides which schema a passport is validated against and - which rules run over it. Each page below is generated from the manifest - the software itself reads, so a page cannot claim a schema version the - code does not serve. + A product group decides what a passport has to carry and which rules it + is checked against. Pick one to see what its passport covers and who may + read each part.

    - Modelled is not the same as mandated. Most of these have - no delegated act yet. Modelling one early means the schema, the rules and - the disclosure split exist to be argued with now, rather than written in a - hurry once a date is fixed — and every page says plainly where its fields - are a best reading rather than a requirement. + Modelled is not the same as mandated. Most of these + groups have no adopted passport rules yet. Modelling one early means the + passport exists to be argued with now, rather than written in a hurry once + a date is fixed.

    - -
    -
    -

    Schema versions

    -

    {schemaVersions}

    -

    across {groups.length} groups, all still served

    -
    -
    -

    Compliance plugins

    -

    {withPlugin}

    -

    sandboxed WebAssembly, bound to {boundToPlugin} of the {groups.length} groups

    -
    -
    -

    With a disclosure split

    -

    {withDisclosure}

    -

    the rest await a delegated act

    -
    -
    -
    -
    +

    - Every figure above is counted from {pin.path} in{" "} - {pin.repository}, - vendored at commit {pin.commit.slice(0, 8)}. A build here - fails if the vendored copies drift from that commit, so this page cannot - quietly fall behind the library. + Every page here is generated from the same product definitions the + software itself uses. A page cannot describe a passport the software does + not build, and when a definition changes, the page changes with it.

    - No act numbers or dates appear on these pages deliberately.{" "} - The manifests carry no law — obligations and effective dates live in - separate instrument manifests that are not vendored here. A regulatory - date published on a marketing site is a claim that has to be kept current, - and a stale one is worse than none. + No deadlines appear on these pages, on purpose. A + regulatory date on a website is a claim that has to be kept current, and + a stale one is worse than none.

    From 1cb161f69c75b8e4d460771c3f844fa4347fe92e Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 25 Sep 2026 19:19:12 +0200 Subject: [PATCH 042/168] feat(landing): rewrite the visibility page so its headline matches Art. 77(2) and its copy reads plainly --- site/dpp-landing/src/data/access.json | 8 +-- site/dpp-landing/src/pages/visibility.astro | 78 +++++++++++---------- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/site/dpp-landing/src/data/access.json b/site/dpp-landing/src/data/access.json index 5680d2b..92f5d70 100644 --- a/site/dpp-landing/src/data/access.json +++ b/site/dpp-landing/src/data/access.json @@ -29,23 +29,23 @@ "points": [ { "id": "1", - "title": "Point 1 — public battery model information", + "title": "Point 1: public information about the battery model", "note": "Material composition, carbon footprint, recycled content, rated capacity, expected lifetime, declaration of conformity, waste management information." }, { "id": "2", - "title": "Point 2 — model information for legitimate interest and the Commission", + "title": "Point 2: model information for readers with a legitimate interest, and the Commission", "note": "Detailed composition, dismantling instructions and the safety measures dismantling requires." }, { "id": "3", - "title": "Point 3 — conformity evidence", + "title": "Point 3: evidence of conformity", "note": "\"Results of test reports proving compliance with the requirements laid down in this Regulation\".", "only": "authority" }, { "id": "4", - "title": "Point 4 — the individual battery's own history", + "title": "Point 4: the individual battery's own history", "note": "State of health, battery status, and \"data resulting from its use, including the number of charging and discharging cycles and negative events, such as accidents, as well as periodically recorded information on the operating environmental conditions, including temperature, and on the state of charge\".", "only": "legitimate" } diff --git a/site/dpp-landing/src/pages/visibility.astro b/site/dpp-landing/src/pages/visibility.astro index 702726b..6c46167 100644 --- a/site/dpp-landing/src/pages/visibility.astro +++ b/site/dpp-landing/src/pages/visibility.astro @@ -5,34 +5,39 @@ // fields appear and vanish. A table of the same information is a claim; a // mechanism someone drives is an argument. // +// The headline says the two audiences see *different* things, never that one +// sees less: authorities get point 3 and not point 4, legitimate interest gets +// point 4 and not point 3. "An authority sees less than a recycler" was the +// earlier headline, and it is false without the qualifier. +// // SCOPE: batteries only, and the page says so in three places. ESPR Art. 11(b) -// defers access rights to each product group's delegated act, and none is -// adopted for the other eleven — so a general "authorities see less" claim -// would assert a rule that exists for one product group. +// defers access rights to each product group's own delegated act, so a general +// claim would assert a rule that exists for one product group. import Base from "../layouts/Base.astro"; import Section from "../components/Section.astro"; import access from "../data/access.json"; -const title = "Who sees what — Art. 77(2) access, operable | Odal Node"; +const title = "Visibility: who sees which part of a battery passport | Odal Node"; const description = - "The battery passport's access rules are a lattice, not a ladder: an authority sees less than a recycler in one specific respect. Pick an audience and watch the fields change."; + "The battery passport's access rules are not a ladder: an authority and a recycler each see something the other cannot. Pick a reader and watch the parts of the passport change."; ---

    - That sentence surprises people, so here is the mechanism rather than the - claim. Pick an audience below and watch which parts of the record appear. + Each can see something the other cannot. That surprises people, so here + is the mechanism rather than the claim: pick a reader below and watch + which parts of the passport appear.

    - Everything on this page is Regulation (EU) 2023/1542 Art. 77(2) and{" "} - Annex XIII, quoted from the consolidated text. It is - the battery passport — see the note at the foot of the page for why - that matters. + Everything on this page comes from Regulation (EU) 2023/1542, + Art. 77(2) and Annex XIII, quoted from the + consolidated text. It covers the battery passport only; the note + at the foot of the page explains why that matters.

    @@ -54,7 +59,7 @@ const description =

    - Basis: {access.audiences[0].basis} + Legal basis: {access.audiences[0].basis}

      @@ -71,12 +76,12 @@ const description =

      {p.note}

      {p.only === "authority" && (

      - Authority only — not visible to a recycler. + Authorities only. Not shown to a recycler.

      )} {p.only === "legitimate" && (

      - Legitimate interest only — not visible to an authority. + Legitimate interest only. Not shown to an authority.

      )} @@ -85,43 +90,44 @@ const description =
    -
    +

    - Art. 77(2) assigns points 2 and 3 to notified bodies, + Art. 77(2) gives points 2 and 3 to notified bodies, market surveillance authorities and the Commission, and{" "} - points 2 and 4 to holders of a legitimate interest. + points 2 and 4 to readers with a legitimate interest.

    - Point 3 is absent from the second list. Point 4 is absent from the first. - No ordering can express that, so no integer "tier" can either — which is - why an implementation that models access as a ladder with the regulator on - top produces the wrong answer for point 4. + Point 3 is missing from the second list. Point 4 is missing from the + first. No ranking can express that, so no simple access level can either. + Software that puts the regulator at the top of a ladder gives the wrong + answer for point 4.

    Point 4 is the individual battery's own use history: charge cycles, accidents, temperature, state of charge. For an electric vehicle that is a - usage record of an identifiable vehicle. Handing it to an audience the law + usage record of an identifiable vehicle. Showing it to a reader the law does not grant it to is a personal-data disclosure, not a display bug.

    -
    +

    - ESPR does not impose this lattice. Art. 11(b) gives actors - access "based on their respective access rights set out in the - applicable delegated act adopted pursuant to Article 4" — so the split - belongs to each product group's own act. + ESPR does not impose this split. Art. 11(b) gives each + reader access "based on their respective access rights set out in the + applicable delegated act adopted pursuant to Article 4", so who sees + what belongs to each product group's own rules.

    - For batteries that act exists and is quoted above. For textiles, - electronics, furniture and the rest, no delegated act has been - adopted, so there is no authority/legitimate-interest split to - show. We model that as absence rather than guessing, and the product group - manifests carry only the classes their own law defines. + For batteries, the Battery Regulation sets the split itself, and it is + quoted above. For textiles, electronics, furniture and most other groups,{" "} + no such rules have been adopted, so there is no split + between authorities and legitimate interest to show. We model that as + absence rather than guessing: each product group carries only the access + rules its own law defines.

    - A fixed three-tier access model, applied everywhere, would be encoding one - product group's rule as if it were the regulation's. + One fixed set of access levels, applied everywhere, would treat one + product group's rule as if it were the whole regulation's.

    From f2ce09d616a826fb811abe7726d763ae9fab60e7 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 25 Sep 2026 19:20:55 +0200 Subject: [PATCH 043/168] fix(landing): take the em dashes out of every page's copy except the roadmap, which is being rewritten --- site/dpp-landing/src/data/standards.json | 2 +- site/dpp-landing/src/pages/404.astro | 2 +- site/dpp-landing/src/pages/index.astro | 26 ++++++++++---------- site/dpp-landing/src/pages/privacy.astro | 10 ++++---- site/dpp-landing/src/pages/trust.astro | 30 +++++++++++------------ site/dpp-landing/src/pages/waitlist.astro | 4 +-- 6 files changed, 36 insertions(+), 38 deletions(-) diff --git a/site/dpp-landing/src/data/standards.json b/site/dpp-landing/src/data/standards.json index 92e1ad9..924e6d8 100644 --- a/site/dpp-landing/src/data/standards.json +++ b/site/dpp-landing/src/data/standards.json @@ -5,6 +5,6 @@ { "name": "W3C Verifiable Credentials", "role": "Credential format for signed passports" }, { "name": "did:web", "role": "Issuer identity, anchored in DNS and HTTPS" }, { "name": "IDTA AAS", "role": "Industrial submodel export for Industry 4.0 consumers" }, - { "name": "CEN/CENELEC JTC 24", "role": "Harmonised DPP standards — tracked as they finalise" } + { "name": "CEN/CENELEC JTC 24", "role": "Harmonised DPP standards, tracked as they finalise" } ] } diff --git a/site/dpp-landing/src/pages/404.astro b/site/dpp-landing/src/pages/404.astro index 4121b5a..eb234e0 100644 --- a/site/dpp-landing/src/pages/404.astro +++ b/site/dpp-landing/src/pages/404.astro @@ -1,7 +1,7 @@ --- import Base from "../layouts/Base.astro"; -const title = "404 — Odal Node"; +const title = "Page not found | Odal Node"; const description = "The page you asked for does not exist on odal-node.io."; --- diff --git a/site/dpp-landing/src/pages/index.astro b/site/dpp-landing/src/pages/index.astro index 713ea18..f70c9df 100644 --- a/site/dpp-landing/src/pages/index.astro +++ b/site/dpp-landing/src/pages/index.astro @@ -9,9 +9,9 @@ import Section from "../components/Section.astro"; import StandardsRow from "../components/StandardsRow.astro"; import Base from "../layouts/Base.astro"; -const title = "Odal Node — EU Digital Product Passport Infrastructure | ESPR Compliance"; +const title = "Odal Node: EU Digital Product Passport Infrastructure | ESPR Compliance"; const description = - "Open-source infrastructure for EU Digital Product Passports (DPP) under ESPR. Self-hosted, sovereign compliance — your product data is validated and signed with your keys. Battery, textile, and electronics passports."; + "Open-source infrastructure for EU Digital Product Passports (DPP) under ESPR. Self-hosted, sovereign compliance: your product data is validated and signed with your keys. Battery, textile, and electronics passports."; // Page-specific structured data. const softwareAppLd = { @@ -37,7 +37,7 @@ const softwareAppLd = { // services (integrator, validation, vault + in-process signing, resolver) are // deliberately merged into a single flow; the docs keep them as separate pages. const steps = [ - { label: "Import", body: "Product data from CSV or Excel — including exports from your ERP — into your own node." }, + { label: "Import", body: "Product data from CSV or Excel, including exports from your ERP, into your own node." }, { label: "Validate", body: "Against versioned sector schemas tracking the regulation, locally." }, { label: "Sign", body: "With your own key, generated and held on your infrastructure." }, { label: "Publish", body: "Only the signed passport becomes resolvable, via QR and GS1 Digital Link." }, @@ -49,7 +49,7 @@ const steps = [ eyebrow="EU Digital Product Passports" status title="Signed by you. Verified by anyone." - subtitle="Odal Node is sovereign infrastructure for EU Digital Product Passports. Your product data is validated and signed on your infrastructure, with your keys — the raw import files are discarded, the signed passport is stored and served. Anyone can verify it." + subtitle="Odal Node is sovereign infrastructure for EU Digital Product Passports. Your product data is validated and signed on your infrastructure, with your keys. The raw import files are discarded; the signed passport is stored and served. Anyone can verify it." primaryHref="https://docs.odal-node.io/quick-start" primaryLabel="Self-host now" secondaryHref="https://docs.odal-node.io" @@ -58,7 +58,7 @@ const steps = [

    - Compliance arrives sector by sector, date by date — not "everything by 2027." + Compliance arrives sector by sector, date by date, not "everything by 2027." Three dated obligations are already in law:

    @@ -76,8 +76,8 @@ const steps = [ Manufacturers keep trade secrets. Regulators and customers need data they can trust. A proof-bound architecture serves both without asking either to compromise: the raw import files are read once and discarded, while the signed - passport they produce — carrying the full product data, every field bound to a - proof — is what the node stores and serves, released to each audience by that + passport they produce (carrying the full product data, every field bound to a + proof) is what the node stores and serves, released to each audience by that audience's access rights.

    @@ -99,9 +99,9 @@ const steps = [

    - Anyone verifies the signature against your public DID document — without Odal in - the loop. Public fields are readable by all; restricted fields — for repairers, - recyclers, and market-surveillance authorities — are released only against a + Anyone verifies the signature against your public DID document, without Odal in + the loop. Public fields are readable by all; restricted fields (for repairers, + recyclers, and market-surveillance authorities) are released only against a verified credential. What the node stores: the whole signed passport, the audit trail, the signature. What it never stores: raw import files, supply-chain detail beyond passport content, or your private keys. @@ -125,7 +125,7 @@ const steps = [ }, { k: "Honest open-core", - v: "dpp-core (the regulatory standard in Rust) is Apache-2.0, full capability, forever. dpp-engine is BSL-1.1 with a real self-host grant — production self-hosting is free.", + v: "dpp-core (the regulatory standard in Rust) is Apache-2.0, full capability, forever. dpp-engine is BSL-1.1 with a real self-host grant: production self-hosting is free.", }, { k: "No vendor lock-in", @@ -133,7 +133,7 @@ const steps = [ }, { k: "Self-host in minutes", - v: "One command to a running node — it builds from source, so allow a few minutes. No licence keys, no passport caps. Ever.", + v: "One command to a running node. It builds from source, so allow a few minutes. No licence keys, no passport caps. Ever.", }, ].map((f) => (

    @@ -148,7 +148,7 @@ const steps = [

    Proof-bound is not a promise about our conduct; it is a property of the - software. The node reads your import files once and discards them — so the raw + software. The node reads your import files once and discards them, so the raw production data behind a passport is never ours to access, by construction. Stated precisely:

    diff --git a/site/dpp-landing/src/pages/privacy.astro b/site/dpp-landing/src/pages/privacy.astro index 6b6928d..68c956e 100644 --- a/site/dpp-landing/src/pages/privacy.astro +++ b/site/dpp-landing/src/pages/privacy.astro @@ -2,7 +2,7 @@ import Base from "../layouts/Base.astro"; import Section from "../components/Section.astro"; -const title = "Privacy Policy — Odal Node"; +const title = "Privacy Policy | Odal Node"; const description = "How the Odal Node website handles your data: no cookies, no analytics, no third-party trackers. Hosted on Cloudflare Pages with zero data collection."; @@ -35,9 +35,9 @@ const MAILBOX = "security@odal-node.io";
    • The site sets no cookies and uses no local storage to track you.
    • -
    • There is no analytics — no Google Analytics, Plausible, Fathom, or any equivalent.
    • +
    • There is no analytics: no Google Analytics, Plausible, Fathom, or any equivalent.
    • There are no third-party trackers, advertising pixels, or social embeds.
    • -
    • We do not build a profile of you, and we do not sell or share data — there is nothing to sell.
    • +
    • We do not build a profile of you, and we do not sell or share data. There is nothing to sell.
    • The only data that exists is what your browser unavoidably sends to load a web page, and anything you choose to email us.
    @@ -54,7 +54,7 @@ const MAILBOX = "security@odal-node.io";

    We use this for nothing. We do not run analytics over it and we do not receive it as an identifiable record of you. It is processed by Cloudflare - as our hosting provider — acting as a data processor on our behalf — for + as our hosting provider (acting as a data processor on our behalf) for content delivery, TLS, and security (such as DDoS protection), under Cloudflare's own terms. See{" "} Cloudflare's privacy policy. @@ -65,7 +65,7 @@ const MAILBOX = "security@odal-node.io";

    - Some links lead off this site — the documentation, + Some links lead off this site: the documentation, our GitHub organisation, and a few cited standards. Once you follow them you are on someone else's property, governed by their privacy practices, not this one. We have no diff --git a/site/dpp-landing/src/pages/trust.astro b/site/dpp-landing/src/pages/trust.astro index 6e11b8d..9221a40 100644 --- a/site/dpp-landing/src/pages/trust.astro +++ b/site/dpp-landing/src/pages/trust.astro @@ -2,7 +2,7 @@ import Base from "../layouts/Base.astro"; import Section from "../components/Section.astro"; -const title = "Trust — Odal Node"; +const title = "Trust | Odal Node"; const description = "What Odal can see, cannot see, and could see but does not. The proof-bound architecture, stated precisely."; --- @@ -14,19 +14,19 @@ const description = raw production data is validated and signed locally, on infrastructure the manufacturer controls, then discarded. Only the signed proof is stored and served. This is a property of the software, not a policy - promise — it holds regardless of who operates the node. + promise: it holds regardless of who operates the node.

    - When a passport is resolved, the node can record that it happened — an + When a passport is resolved, the node can record that it happened: an aggregate count, per passport, per day. That is the whole of it. A scan count records nothing about the person who scanned: no IP address, no device, no location, no identity, no session. The software has no field in which to store any of it, so there is nothing to leak and nothing that could quietly become tracking. Producing a QR-code image is - counted separately — it measures label production, not people. + counted separately; it measures label production, not people.

    @@ -76,18 +76,18 @@ const description = Node discards raw inputs; stores only the signed proof - Yes — architectural invariant - Yes — architectural invariant + Yes, by design + Yes, by design Odal can access stored data - No — not present in the deployment + No. We are not part of the deployment Constrained by access controls, audit logging, and contract Odal can sign on the operator's behalf No - No — the operator holds the signing keys + No. The operator holds the signing keys @@ -96,19 +96,17 @@ const description =
    1. - Import. Product data arrives at your node — CSV, Excel, + Import. Product data arrives at your node as CSV, Excel or an ERP export. This happens on infrastructure you control.
    2. - Validate. The node validates locally against versioned - sector schemas. Validation is a pure function — no network calls, no - third-party API. + Validate. The node checks the data locally against the + product group's rules. Nothing is sent to a third party to be checked.
    3. - Sign. Your private signing key — generated and held - in-process on your infrastructure — signs the validated passport. - The result is a signature cryptographically bound to your published{" "} - did:web identity. + Sign. Your private signing key, generated and held on + your own infrastructure, signs the validated passport. The signature is + tied to an identity on a web domain you control.
    4. Publish. Only the signed passport becomes publicly diff --git a/site/dpp-landing/src/pages/waitlist.astro b/site/dpp-landing/src/pages/waitlist.astro index f3f0eaa..1ec3532 100644 --- a/site/dpp-landing/src/pages/waitlist.astro +++ b/site/dpp-landing/src/pages/waitlist.astro @@ -6,9 +6,9 @@ import Base from "../layouts/Base.astro"; import Section from "../components/Section.astro"; -const title = "Join the Waitlist — Odal Node | EU Digital Product Passport"; +const title = "Join the Waitlist | Odal Node"; const description = - "Sign up for early access to Odal Node — open-source EU Digital Product Passport infrastructure for ESPR compliance."; + "Sign up for early access to Odal Node, open-source EU Digital Product Passport infrastructure for ESPR compliance."; const MAILBOX = "contact@odal-node.io"; --- From eb63cacca19bb9dbdedd7acbdaa1346f89a10484 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 25 Sep 2026 19:38:58 +0200 Subject: [PATCH 044/168] feat(landing): rebuild the roadmap around capabilities and their variants, read from the code, with no issues, commands or versions --- .../src/components/GetStarted.astro | 33 -- site/dpp-landing/src/components/Roadmap.astro | 158 ++++----- site/dpp-landing/src/data/roadmap.json | 303 ++++++------------ site/dpp-landing/src/pages/roadmap.astro | 140 +++----- 4 files changed, 222 insertions(+), 412 deletions(-) delete mode 100644 site/dpp-landing/src/components/GetStarted.astro diff --git a/site/dpp-landing/src/components/GetStarted.astro b/site/dpp-landing/src/components/GetStarted.astro deleted file mode 100644 index cfefef0..0000000 --- a/site/dpp-landing/src/components/GetStarted.astro +++ /dev/null @@ -1,33 +0,0 @@ ---- -// GetStarted.astro — navy terminal block with the quick-start commands. -// RULE: whatever is printed here must be the -// tested self-host path. If the quick-start changes, this component changes -// in the same PR. Entrypoint = dpp-engine/docker/docker-compose.yml -// (node internal port 8000, mapped to host 8001 by default). -const commands = [ - "git clone https://github.com/odal-node/dpp-engine", - "cargo build -p dpp-cli --release", - "./target/release/odal up", -]; ---- - -
      -
      -

      - Get started -

      -

      Three commands to a running node — the first build takes a few minutes.

      -
      - {commands.map((c) => ( -

      - $ {c} -

      - ))} -
      -

      - Full quick-start, including your first signed passport, in the{" "} - documentation. - Self-hosting is free — no licence keys, no passport caps. -

      -
      -
      diff --git a/site/dpp-landing/src/components/Roadmap.astro b/site/dpp-landing/src/components/Roadmap.astro index 88ed2db..9ff1cf1 100644 --- a/site/dpp-landing/src/components/Roadmap.astro +++ b/site/dpp-landing/src/components/Roadmap.astro @@ -1,101 +1,101 @@ --- -// Roadmap.astro — the four registers, each grouped by capability area. -// Data lives in src/data/roadmap.json so editors change JSON, not components. +// Roadmap.astro — the four registers, each grouped into themes. +// Data lives in src/data/roadmap.json, so editors change JSON, not markup. // -// WHY FOUR AND NOT THREE +// WHY FOUR REGISTERS AND NOT THREE // -// The previous version had Shipped / In build / Horizon, and the seal and -// registry work sat under "in build" with the words "in progress" and "gated on -// upstream blockers". Both are finished. Neither can be used, because an -// electronic seal may only be created by a legal person and there is no company -// — which is not an engineering schedule and does not belong in a column that -// reads like one. `blocked` says the true thing: built, and legally inert. +// "Waiting on a legal step" is kept apart from "In progress" on purpose. The +// seal and registry work is finished; what holds it back is that only a legal +// person may create an electronic seal. Filing that under "in progress" would +// turn a legal fact into an engineering schedule. // -// WHY EVERY ITEM CARRIES EVIDENCE -// -// A roadmap is the easiest page on a site to write optimistically, because -// nothing on it is checkable. Each item here names a crate, a command, a route -// or a page, so a reader who doubts a line can go and settle it. An item that -// cannot name one is an item we should not be making a claim about. +// Items describe capabilities in plain words. Routes, commands and version +// numbers belong on the docs site, never here. import data from "../data/roadmap.json"; -type Status = "shipped" | "blocked" | "in-build" | "horizon"; -type Item = { label: string; detail: string; evidence?: string }; -type Area = { area: string; items: Item[] }; - -const { registers } = data as unknown as { - registers: { status: Status; title: string; description: string; areas: Area[] }[]; +type Status = "available" | "waiting" | "in-progress" | "considered"; +type Item = { + title: string; + line: string; + variants?: string[]; + link?: { href: string; label: string }; }; +type Theme = { theme: string; items: Item[] }; +type Register = { status: Status; title: string; description: string; themes: Theme[] }; -const dot: Record = { - shipped: "bg-status-released", - blocked: "bg-status-blocked", - "in-build": "bg-status-active", - horizon: "bg-status-development", -}; +const { registers } = data as unknown as { registers: Register[] }; -const rule: Record = { - shipped: "border-status-released/30", - blocked: "border-status-blocked/40", - "in-build": "border-status-active/30", - horizon: "border-neutral-200", +const tone: Record = { + available: { + dot: "bg-status-released", + rule: "border-status-released/30", + chip: "bg-primary-50 text-primary-800", + }, + waiting: { + dot: "bg-status-blocked", + rule: "border-status-blocked/40", + chip: "bg-neutral-100 text-neutral-700", + }, + "in-progress": { + dot: "bg-status-active", + rule: "border-status-active/30", + chip: "bg-neutral-100 text-neutral-700", + }, + considered: { + dot: "bg-status-development", + rule: "border-neutral-200", + chip: "bg-neutral-100 text-neutral-700", + }, }; -// An evidence string is rendered as a link when we can resolve it to one, and -// as plain text otherwise. A command is not a URL, and a citation of the -// Official Journal is deliberately not linked to a page that may move. -// -// An HTTP route is not a link either, however much it looks like one. A path -// with a `{param}` in it names an endpoint on someone's node, not a page on -// this site — linking `/01/{gtin}/21/{serial}` produced the one broken link -// the link checker found on this page. -const href = (e: string): string | null => { - if (e.startsWith("/")) return e.includes("{") ? null : e; - if (e.startsWith("crates.io/")) return `https://${e}`; - if (e.startsWith("github.com/")) return `https://${e}`; - if (e.startsWith("docs.odal-node.io/")) return `https://${e}`; - return null; -}; +const count = (r: Register) => r.themes.reduce((n, t) => n + t.items.length, 0); --- -
      + + +
      + {registers.map((reg) => ( +
      +

      - + {reg.title}

      -

      - {reg.description} -

      +

      {reg.description}

      -
      - {reg.areas.map((area) => ( -
      -

      - {/* One area repeated per register would be noise, so a register - with a single catch-all area prints no heading at all. */} - {reg.areas.length > 1 ? area.area : ""} -

      -
        - {area.items.map((it) => ( -
      • -

        {it.label}

        -

        {it.detail}

        - {it.evidence && ( -

        - {href(it.evidence) ? ( - - {it.evidence} - - ) : ( - {it.evidence} - )} +

        + {reg.themes.map((theme) => ( +
        1 }]}> + {reg.themes.length > 1 && ( +

        {theme.theme}

        + )} +
          + {theme.items.map((it) => ( +
        • +

          {it.title}

          +

          {it.line}

          + {it.variants && ( +
            + {it.variants.map((v) => ( +
          • {v}
          • + ))} +
          + )} + {it.link && ( +

          + {it.link.label}

          )}
        • diff --git a/site/dpp-landing/src/data/roadmap.json b/site/dpp-landing/src/data/roadmap.json index f528c54..c1474a6 100644 --- a/site/dpp-landing/src/data/roadmap.json +++ b/site/dpp-landing/src/data/roadmap.json @@ -1,312 +1,203 @@ { - "_comment": "Roadmap data — the single editable source for the roadmap page. Rewritten 2026-09-19 after auditing every item against the two repositories: released tags, crates.io versions, the schema and plugin inventory, the CLI's own command list, and the open issue trackers. Eleven of the previous twenty-one items were wrong in a way that flattered us or, twice, understated us.", - - "_registers": "Four registers, never mixed. 'shipped' is a verified fact. 'blocked' is also a fact — the machinery runs and the status it can claim is nil — and it is separated out because folding it into 'in build' is how a legal blocker gets read as an engineering schedule. 'in-build' is a commitment with a named condition. 'horizon' is an option under consideration, with no date attached to it. Never write an option in the grammar of a fact.", - - "_evidence_rule": "Every item names something a reader can check without asking us: a crate on crates.io, a CLI command, an HTTP route, or a page on this site. An item that cannot name one does not belong on this page.", - - "versions": { - "core": { "repo": "dpp-core", "version": "0.20.0", "note": "Apache-2.0, published to crates.io" }, - "engine": { "repo": "dpp-engine", "version": "0.13.0", "note": "BSL-1.1 with a free self-host grant" } - }, - + "_comment": "The public roadmap, the single editable source for /roadmap. Locked with the founder on 2026-09-25 and checked against dpp-engine main (v0.14.0) and dpp-core main that day. Rules: features and outcomes only, written for a general reader. No issue links, commands, routes, crate names, version numbers or decision-record numbers; technical detail belongs on the docs site. No dates on anything that is not built. 'variants' lists the main forms a capability takes, in plain words. Update 'updated' whenever an item moves.", + "updated": "2026-09-25", "registers": [ { - "status": "shipped", - "title": "Shipped", - "description": "In a tagged release, with a test that exercises it through the running system. Verified fact.", - "areas": [ + "status": "available", + "title": "Available now", + "description": "In the software today and exercised by its tests. Available is not the same as in use: nobody runs this in production yet.", + "themes": [ { - "area": "The open library", + "theme": "Your node, your keys", "items": [ { - "label": "Eleven crates on crates.io", - "detail": "The domain model, cryptography, credentials, GS1 Digital Link, the Asset Administration Shell projection, the plugin contract and the regulatory rules — all Apache-2.0, all readable and reusable by anyone, including a competitor.", - "evidence": "crates.io/crates/dpp-domain" + "title": "Runs on your own infrastructure", + "line": "One self-contained node on servers you choose, brought up with a single command. Self-hosting for your own compliance is free." }, { - "label": "Regulatory rules as a dependency-free crate", - "detail": "The cross-field rules that change when a regulation changes are a pure no_std crate with zero dependencies, so they can be audited on their own terms rather than read through a framework.", - "evidence": "crates.io/crates/dpp-rules" + "title": "Signed with your own key", + "line": "Every passport is signed on your servers, with a key that never leaves them, under an identity on a web domain you control. The key can be rotated without invalidating anything it already signed." } ] }, { - "area": "Product groups", + "theme": "Getting data in", "items": [ { - "label": "Every product group modelled, with its full version history", - "detail": "Each carries its schema versions, its categories, its disclosure split and its own note saying which of its fields are a best reading rather than a requirement. Older schema versions stay served — a passport published against one does not stop resolving when a newer one lands.", - "evidence": "/product-groups" + "title": "Spreadsheets in, passports out", + "line": "Import your product data with a trial run first. The node reports what is wrong row by row instead of rejecting the whole file.", + "variants": ["CSV", "Excel"] }, { - "label": "Compliance rules run sandboxed, and are swapped without a redeploy", - "detail": "Each product group's rules compile to WebAssembly and run isolated from the node. The host verifies a plugin's signature and its declared schema range before dispatching to it.", - "evidence": "odal plugin install" + "title": "Checked before anything is signed", + "line": "Each product group's rules run before signing, alongside warnings for values that look implausible. Rule updates arrive signed and apply without reinstalling anything.", + "variants": ["Blocking checks", "Advisory warnings", "Signed rule updates"] } ] }, { - "area": "Identity and signing", + "theme": "Reading a passport", "items": [ { - "label": "Signed on the operator's own key, on the operator's own infrastructure", - "detail": "Ed25519, an encrypted keystore, and a did:web identity in a domain the operator controls. The signing key can be rotated without invalidating what it already signed. Nothing is sent anywhere to be signed.", - "evidence": "crates.io/crates/dpp-crypto" - } - ] - }, - { - "area": "Publication and resolution", - "items": [ + "title": "Scan to read", + "line": "The code on the product opens its public passport. No login and no fee.", + "variants": ["With a GS1 barcode", "Without a barcode"] + }, { - "label": "Scan the code, get the public view", - "detail": "GS1 Digital Link resolution when the product carries a GTIN, and a plain passport URL when it does not — because requiring the barcode would mean requiring GS1 membership. No login and no fee, because the regulation says so.", - "evidence": "/01/{gtin}/21/{serial}" + "title": "Each reader sees what they are entitled to", + "line": "Repairers, recyclers and authorities see more than the public, field by field, proven by a credential they hold rather than a setting they choose.", + "variants": ["Public", "Legitimate interest", "Authorities"], + "link": { "href": "/visibility", "label": "How visibility works" } }, { - "label": "Asset Administration Shell export", - "detail": "A shell plus per-product-group submodels, for the industrial systems that speak AAS rather than passports.", - "evidence": "crates.io/crates/dpp-aas" + "title": "Export for industrial systems", + "line": "Passports also export in the Asset Administration Shell format that Industry 4.0 systems already read." } ] }, { - "area": "Disclosure", + "theme": "A record for the product's whole life", "items": [ { - "label": "Depth proven by credential, enforced per field", - "detail": "The same record answers a repairer, a recycler and an authority differently, and which fields each sees is decided by the credential they present rather than by a header they set. For batteries this is a lattice and not a ladder — an authority sees less than a recycler in one specific respect, and modelling it as a ranking would be wrong.", - "evidence": "/visibility" - } - ] - }, - { - "area": "Lifecycle and custody", - "items": [ + "title": "Changes without losing history", + "line": "Correct a passport, replace it with a newer one, suspend it or retire it. Every earlier version stays readable, and every change is written to a tamper-evident trail.", + "variants": ["Correct", "Replace", "Suspend", "Retire"] + }, { - "label": "A record that survives the sale", - "detail": "Suspension, retirement, a typed end of life, a recorded waste handover, and lineage that can name several predecessors rather than one.", - "evidence": "odal passport eol" + "title": "A handover between companies", + "line": "Responsibility moves from one company to another with both signatures on the record. Either side can decline or withdraw before it completes." }, { - "label": "Responsibility changes hands with both signatures on the record", - "detail": "Remanufacture, repurposing or a sold product line, between two legal entities — either side can refuse or withdraw, and the chain of custody is part of the passport rather than an email thread.", - "evidence": "odal passport transfer initiate" + "title": "Products made of products", + "line": "A passport can point to the passports of its components, and the whole chain is verified in one check." }, { - "label": "A tamper-evident history", - "detail": "Every change is appended to a hash-chained audit trail, so a record that was edited cannot present itself as one that was not.", - "evidence": "odal passport history" + "title": "A recorded end of life", + "line": "The end of a product's life is recorded on its passport, including the handover to a waste operator.", + "link": { "href": "/lifecycle", "label": "Follow the whole lifecycle" } } ] }, { - "area": "Evidence and verification", + "theme": "Proof that stands on its own", "items": [ { - "label": "A dossier that verifies with no network and no trust in us", - "detail": "Passport, signatures, identity documents and the full history in one signed file. Someone who has never heard of Odal Node can check it offline.", - "evidence": "odal verify" + "title": "A proof file anyone can check offline", + "line": "The passport, its signatures and its full history in one signed file. It verifies without a network connection and without trusting us." }, { - "label": "The passport still resolves when the node does not", - "detail": "A pre-rendered static tier, signed and bounded, serving the same page the live resolver serves — through one renderer, so the two cannot drift.", - "evidence": "odal snapshot verify" + "title": "Readable when your server is not", + "line": "A signed, pre-rendered copy of each public page can be kept outside the node, so a scan still works while the node is down." } ] }, { - "area": "Running a node", + "theme": "Running it day to day", "items": [ { - "label": "One command-line control plane", - "detail": "Twenty-six command groups: bring the node up, onboard the operator, mint scoped API keys, manage facilities and economic-operator identifiers, import, publish, issue credentials, inspect seals, read registry status and scan telemetry. There is no separate console to learn, and no operation that is only available in one.", - "evidence": "odal --help" - }, - { - "label": "Signed outbound webhooks", - "detail": "Passport events delivered to the operator's own systems, signed so a receiver can tell a real delivery from a replayed one.", - "evidence": "odal webhook list" + "title": "One tool to run it all", + "line": "Set up, onboard, import, publish, issue credentials and check status from a single tool.", + "variants": ["Command line", "Interactive console"] }, { - "label": "Compliance rulesets arrive signed, and are verified fail-closed", - "detail": "Rulesets ship as signed bundles and are hot-swapped. A node configured for production refuses a placeholder trust adapter rather than accepting one quietly.", - "evidence": "odal ruleset reload" + "title": "Your systems stay informed", + "line": "Passport events are sent to your own systems, signed so a receiver can tell a real delivery from a replayed one." }, { - "label": "Scan telemetry the operator owns", - "detail": "How often a passport was resolved, operator-wide and per record, on the operator's own node rather than in someone else's analytics.", - "evidence": "odal stats" + "title": "Scan counts you own", + "line": "How often each passport was opened, counted on your own node. Nothing is recorded about the person who scanned." }, { - "label": "The ESPR Article 24 unsold-goods disclosure", - "detail": "Recorded as many lines rather than one aggregate, because the article asks for figures differentiated per product type, per reason and per destination. A destruction recorded under an exemption must say which exemption.", - "evidence": "odal unsold-goods record" + "title": "Company details, recorded once", + "line": "Your sites and company identifiers live on the node and are stamped onto every new passport." }, { - "label": "Registration intent survives a crash", - "detail": "A durable outbox holds pending EU registry registrations and drains with backoff, so a registry that is slow, down or not yet reachable delays binding without ever losing custody.", - "evidence": "odal registry" - } - ] - }, - { - "area": "Getting data in", - "items": [ - { - "label": "Bulk import from spreadsheets, with per-row errors", - "detail": "CSV, TSV, Excel and JSON, with a dry-run mode that reports what is wrong row by row instead of rejecting the file. Coverage is partial and the gap is tracked below — not every product group has an import mapping, and fewer have a downloadable template.", - "evidence": "POST /api/v1/import/{productGroup}" + "title": "The unsold-goods disclosure", + "line": "Record what was discarded, why and where it went, line by line, ready for the yearly disclosure." } ] } ] }, - { - "status": "blocked", - "title": "Built, and legally inert", - "description": "The machinery is written, tested and running. What it cannot do is claim any legal status — and the reason is not an engineering one.", - "areas": [ - { - "area": "eIDAS qualified seal", - "items": [ - { - "label": "The sealing subsystem runs end to end today", - "detail": "Seals are created, stored, audited and re-read across restarts. EU Trusted List verification walks the list-of-lists down to the national list. Each seal carries its own findings — issuer, binding, evidenced level, attested time, archival state, certificate validity and revocation — and an operator can re-run a repair.", - "evidence": "odal seal status" - }, - { - "label": "Every seal it issues reports itself as self-issued and of no legal effect", - "detail": "Under eIDAS an electronic seal may only be created by a legal person. There is no company behind this project, so there is no qualified certificate, so there is no qualified seal. The software says exactly that rather than rounding it up, and it refuses to present a qualified claim it cannot support. When a credential exists, what changes is the backend and the verdict — not the design.", - "evidence": "Regulation (EU) No 910/2014, Art. 3(24)" - } - ] - }, + "status": "waiting", + "title": "Built, waiting on a legal step", + "description": "Finished and tested. What holds these back is not engineering: under eIDAS only a legal person can create an electronic seal, and there is no registered company behind the project yet.", + "themes": [ { - "area": "EU Central Registry", + "theme": "Legal identity", "items": [ { - "label": "The registry is live; enrolment is not open to us", - "detail": "The EU Digital Product Passport registry became operational on 20 July 2026. Registration identity is proved by qualified electronic seal or attestation — the same blocker as above, one step further along.", - "evidence": "Commission Implementing Regulation (EU) 2026/1778" + "title": "Electronic seals with legal effect", + "line": "Sealing works end to end today. Until a company holds a qualified certificate, every seal it makes says plainly that it carries no legal effect, rather than rounding that up." }, { - "label": "Our own connector has a structural mismatch to correct first", - "detail": "It models a bearer-token exchange where the registry expects eIDAS-based identity. That is a wrong assumption rather than a wrong URL, and it is a breaking change we would rather make against the published specification than guess at. The connector is not pointed at real products.", - "evidence": "crates.io/crates/dpp-registry" + "title": "Registration in the EU registry", + "line": "Registering passports needs the same legal identity. Our connection to the registry will be finished against its official interface, not guessed at." } ] } ] }, - { - "status": "in-build", - "title": "In build", - "description": "Committed, each with a named condition. Nothing here is claimed as working.", - "areas": [ + "status": "in-progress", + "title": "In progress", + "description": "Being built now. Nothing here is claimed as working.", + "themes": [ { - "area": "In build", + "theme": "Next", "items": [ { - "label": "Replacing the mandatory GTIN with the standard's identifier union", - "detail": "EN 18219 offers several identifier schemes as alternatives rather than a hierarchy, and requiring a GTIN meant requiring GS1 membership. The passport model and the plugins already take the union; the remaining work is the sweep through every rule, fixture and test that still assumes the old field. Condition: the next library release.", - "evidence": "github.com/odal-node/dpp-core/issues/328" + "title": "More ways to identify a product", + "line": "Every identifier scheme the European standard allows, not only a barcode number. It arrives with the next release of the open library." }, { - "label": "Wiring the compliance calculators into the node", - "detail": "Cradle-to-gate CO2e and EN 45554 repairability are written and published as part of the open library, with the licensed emission-factor data injected rather than bundled. Nothing in the running node invokes them yet, so no passport carries a computed figure. Condition: the evidence-receipt path that records which ruleset version produced a number.", - "evidence": "crates.io/crates/dpp-calc" + "title": "Carbon and repairability figures in the passport", + "line": "The calculators are written and published in the open library. The node does not run them yet, so no passport carries a computed figure today." }, { - "label": "Import coverage across every product group", - "detail": "Bulk import maps five of the twelve product groups, and downloadable CSV templates exist for two. The mechanism is shipped; the per-group mappings are not. Condition: one mapping and one template per modelled group.", - "evidence": "github.com/odal-node/dpp-engine/issues/192" + "title": "Spreadsheet import for every product group", + "line": "Import covers most product groups today. Phones and tablets, and detergents, are still to come." }, { - "label": "The unsold-goods disclosure as a publishable artifact", - "detail": "The record is written and read today. What does not exist is the document an operator publishes on their own website, which is where Article 24 actually puts the obligation. Condition: the output format, which no implementing act prescribes.", - "evidence": "odal unsold-goods list" - }, - { - "label": "Toy and detergent content completeness", - "detail": "Both schemas carry almost none of the content their annexes mandate — and that content is in the adopted acts, not in the implementing acts still pending. These two are modelled far more thinly than the rest and should be read as placeholders.", - "evidence": "github.com/odal-node/dpp-core/issues/262" + "title": "The unsold-goods report, ready to publish", + "line": "The records exist today. What is missing is the finished document a company publishes on its own website each year." } ] } ] }, - { - "status": "horizon", - "title": "Horizon", - "description": "Under consideration. Not started, not scheduled, and deliberately carrying no dates.", - "areas": [ - { - "area": "Known gaps in what is modelled", - "items": [ - { - "label": "The semantic repository", - "detail": "The framework requires passport data to be expressed against a published vocabulary held in a repository we do not yet read. It is mandatory and it is free, and not having read it is the largest single gap on this page.", - "evidence": "github.com/odal-node/dpp-core/issues/92" - }, - { - "label": "Language", - "detail": "Toys and detergents must present the passport in the language or languages of the Member State where the product is placed. There is no language dimension anywhere in the model.", - "evidence": "github.com/odal-node/dpp-core/issues/264" - }, - { - "label": "Images", - "detail": "The toy and detergent annexes both require a colour image of the product. There is no image concept in the passport at all.", - "evidence": "github.com/odal-node/dpp-core/issues/263" - }, - { - "label": "A position on personal data in a passport", - "detail": "One ESPR paragraph is unconditional about what a passport carries, and we have taken no position on what that means when the value identifies a customer. Deciding this badly is worse than deciding it late.", - "evidence": "github.com/odal-node/dpp-core/issues/261" - }, - { - "label": "Battery carbon footprint", - "detail": "Blocked upstream rather than by us: the calculation method has not been published, so there is nothing to implement. Guessing at it would produce a number that looks authoritative and is not.", - "evidence": "github.com/odal-node/dpp-core/issues/160" - } - ] - }, + "status": "considered", + "title": "Being considered", + "description": "Options, not promises. Not started, not scheduled, and deliberately without dates.", + "themes": [ { - "area": "Options not yet taken", + "theme": "Later", "items": [ { - "label": "A public, genuinely offline conformance validator", - "detail": "Anyone could check a passport against the standards without sending it to us, or trusting us. Six European standards are cited by the framework; two have been read in full so far.", - "evidence": "github.com/odal-node/dpp-core/issues/81" + "title": "Managed deployments", + "line": "A node run for you, for companies that would rather not operate one themselves. It would still be yours alone: one company, one node, nothing shared with anyone else." }, { - "label": "A second credential format", - "detail": "The UN Transparency Protocol's passport credential as an alternative door into the same record, for supply chains that have standardised on it.", - "evidence": "github.com/odal-node/dpp-engine/issues/70" + "title": "Passports in the reader's language", + "line": "Some product groups must be shown in the language of the country where the product is sold." }, { - "label": "A second signing curve", - "detail": "P-256 alongside Ed25519, if a hardware or registry requirement ever calls for it. An open question rather than a plan.", - "evidence": "github.com/odal-node/dpp-core/issues/78" + "title": "Product images", + "line": "A picture of the product inside its passport, where the rules ask for one." }, { - "label": "An EU-operated identity method", - "detail": "did:ebsi as an alternative issuer identity to did:web, if registry requirements call for it.", - "evidence": "crates.io/crates/dpp-vc" + "title": "The EU's shared vocabulary", + "line": "Passport data expressed in the common vocabulary the EU maintains, so every system reads a field the same way." }, { - "label": "Hosting several operators", - "detail": "One operator, one node, one database, one key, and no shared data paths at any layer — that invariant is settled and is not what is open. What is open is only how the machines are provisioned.", - "evidence": "docs.odal-node.io/engine/self-hosted" + "title": "A public checker anyone can run", + "line": "Check any passport against the European standards on your own machine, without sending it anywhere." }, { - "label": "An edge-cached public resolver", - "detail": "Cached passport pages for scan latency away from the node's own region.", - "evidence": "docs.odal-node.io/engine/architecture" + "title": "Battery carbon footprint", + "line": "Waiting on the EU to publish the calculation method. Guessing would produce a number that looks official and is not." } ] } diff --git a/site/dpp-landing/src/pages/roadmap.astro b/site/dpp-landing/src/pages/roadmap.astro index de003d4..4ecd694 100644 --- a/site/dpp-landing/src/pages/roadmap.astro +++ b/site/dpp-landing/src/pages/roadmap.astro @@ -1,134 +1,86 @@ --- -// /roadmap — development status across the two public repositories. +// /roadmap — what the software does today, what is waiting, and what is next. // -// Rewritten 2026-09-19 after auditing every item against the repositories -// themselves rather than against the previous version of this page. What the -// audit found is why the page now looks the way it does: -// -// * Four items were understated. Twelve product groups were described as -// three; electronics and four "horizon" groups had already shipped. -// * Two were overstated. An operator dashboard was listed as in build and -// does not exist in any form, and the CLI — which is the actual control -// plane — was not mentioned at all. -// * Two were miscategorised in a way that mattered. The seal and registry -// work is finished and legally inert, which is not the same fact as -// "in progress", and a column reading "in build" said the wrong thing. -// * Two prose claims were false. This page said it was reviewed fortnightly, -// and linked to a public project board that does not exist. -// -// The rule the page now holds itself to: every item names something a reader -// can check without asking us, and no item is written in the grammar of a -// stronger claim than it can support. +// Rewritten 2026-09-25 with the founder. The page lists capabilities, never +// tasks: no issue links, commands, routes or version numbers. Every "available" +// item was checked against the engine and core repositories that day. The rule +// the page holds itself to: no item is written in the grammar of a stronger +// claim than it can support, and nothing that is not built carries a date. import Base from "../layouts/Base.astro"; import Section from "../components/Section.astro"; import OurRoadmap from "../components/Roadmap.astro"; import data from "../data/roadmap.json"; -// Counted from the vendored manifests rather than written down, so the figure -// cannot drift from the library the way "battery, textile and steel" did. -const manifests = import.meta.glob("../data/product-groups/*.json", { eager: true }); -const groups = Object.values(manifests).map((m: any) => m.default ?? m); -const groupCount = groups.length; -const pluginCount = new Set(groups.filter((g) => g.plugin).map((g) => g.plugin)).size; - -const { versions } = data as unknown as { - versions: Record; -}; +const updated = new Date(`${data.updated}T00:00:00Z`).toLocaleDateString("en-GB", { + day: "numeric", + month: "long", + year: "numeric", + timeZone: "UTC", +}); -const title = "Roadmap — what is built, blocked and not started | Odal Node"; +const title = "Roadmap: what is built, waiting and next | Odal Node"; const description = - "Development status for the Odal Node EU Digital Product Passport software: what ships today in dpp-core and dpp-engine, what is built but legally inert, what is in build, and the gaps we have not closed."; + "What the Odal Node software does today, what is built and waiting on a legal step, what is in progress, and what is being considered. No dates on anything unbuilt."; --- -
          +

          - Two public repositories: dpp-core, - the regulatory library, and dpp-engine, - the software that runs a node. This page is the status of both, audited - against the repositories rather than against the last version of this page. + This page lists capabilities, not tasks, and it is checked against the + software itself rather than against its own last version.

          - -
          - {Object.values(versions).map((v) => ( -
          -

          {v.repo}

          -

          v{v.version}

          -

          {v.note}

          -
          - ))} -
          -

          product groups

          -

          {groupCount}

          -

          - {pluginCount} sandboxed rule plugins ·{" "} - see them all -

          -
          -
          -

          Nobody is running this in production. No customers, no - pilots, no live deployments, and nothing on this page should be read as - evidence of any. The battery passport obligation begins on 18 February - 2027, and we would rather be correct when it arrives than early and wrong. + pilots, no live deployments, and nothing below should be read as evidence + of any. The battery passport obligation begins on 18 February 2027, and we + would rather be correct when it arrives than early and wrong.

          - There are no dates below. Timing on regulatory infrastructure depends on - delegated acts, standards-body votes and registry availability — none of - which we control — so the category is the commitment and a quarter would - be a guess dressed as one. + There are no dates on anything unbuilt. Timing depends on EU acts, + standards votes and registry access, none of which we control, so the + status is the commitment and a quarter would be a guess. +

          +

          + Last updated .

          -
          +
          -
          +

          - An item is shipped when it is in a tagged release of the - relevant repository and a test exercises it through the running system. - Merged is not shipped, and neither is demonstrated — "shipped" here means - there is something that fails if it stops working. + An item is available when it is in a released version + and a test exercises it through the running system. Merged is not + available, and neither is demonstrated.

          - An item is in build only with a named condition attached, - so that a reader can tell what would have to be true for it to move. An - item on the horizon is an option under consideration, and - is written in the grammar of an option: it carries no date, and no promise. + An item is waiting on a legal step when it is built and + tested but cannot yet do the thing it exists for, for a legal reason + rather than an engineering one.

          - The fourth register is the one most roadmaps do not have.{" "} - Built and legally inert covers work that is finished, - tested and running, and that cannot be used for the thing it exists to do. - The sealing subsystem is the example: it works end to end and every seal it - produces reports itself as self-issued and of no legal effect, because - under eIDAS only a legal person may create an electronic seal. Filing that - under "in build" would turn a legal fact into an engineering timeline. + An item is in progress only while someone is building + it. An item being considered is an option: it carries no + date and no promise.

          - Items move backwards. Discovering while building something that it needs - more foundational work is an ordinary outcome of building it, and that work - returns to the horizon with a note about what was learned. A reversal - recorded is better than an item quietly drifting for six months. + Items can move backwards. Finding out while building something that it + needs more groundwork is an ordinary outcome, and a reversal recorded here + is better than an item quietly drifting.

          -
          -

          - It is not a schedule, and it is not exhaustive. The open issues in both - repositories are public and carry the detail this page compresses — - including the defects. If something you need is absent here, the useful - assumption is that it does not exist rather than that it was forgotten, - and the issue trackers are the place to say so. -

          +

          - dpp-core issues{" "} - ·{" "} - dpp-engine issues + This page is written for everyone. How each capability works, and how to + run it, is in the{" "} + documentation, and + the code itself is{" "} + public on GitHub.

          From 7e757a90275a399afdb5ea5271223c7db7c1432c Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 25 Sep 2026 19:38:58 +0200 Subject: [PATCH 045/168] fix(landing): correct the home page's deadline, standards, licence, resolver and sandbox claims, and drop managed deployments from the trust page --- .../src/components/DeadlineCards.astro | 12 +- site/dpp-landing/src/data/deadlines.json | 19 +-- site/dpp-landing/src/data/standards.json | 10 +- site/dpp-landing/src/pages/index.astro | 56 ++++---- site/dpp-landing/src/pages/trust.astro | 122 +++++++----------- site/dpp-landing/src/pages/waitlist.astro | 2 +- 6 files changed, 96 insertions(+), 125 deletions(-) diff --git a/site/dpp-landing/src/components/DeadlineCards.astro b/site/dpp-landing/src/components/DeadlineCards.astro index 4d20fd6..8feb96c 100644 --- a/site/dpp-landing/src/components/DeadlineCards.astro +++ b/site/dpp-landing/src/components/DeadlineCards.astro @@ -1,16 +1,22 @@ --- // DeadlineCards.astro — the regulatory clock. Data: src/data/deadlines.json. -// Uses the regulatory semantic tokens (amber family) — these are deadline +// Uses the regulatory semantic tokens (amber family): these are deadline // callouts, not brand surfaces. Every card carries its legal citation. +// +// A date that has passed reads "In force since", not as something upcoming. +// The comparison runs at build time, which is the only clock a static site +// has; every deploy rebuilds, so a card is at most one deploy out of date. import data from "../data/deadlines.json"; const { deadlines } = data; +const today = new Date().toISOString().slice(0, 10); +const cols = deadlines.length >= 3 ? "md:grid-cols-3" : "md:grid-cols-2"; --- -
          +
          {deadlines.map((d) => (

          - {d.dateLabel} + {d.date <= today ? "In force since " : "From "}{d.dateLabel}

          {d.title}

          diff --git a/site/dpp-landing/src/data/deadlines.json b/site/dpp-landing/src/data/deadlines.json index f644dc8..8697c5d 100644 --- a/site/dpp-landing/src/data/deadlines.json +++ b/site/dpp-landing/src/data/deadlines.json @@ -1,32 +1,23 @@ { - "_comment": "Regulatory deadlines shown on the landing page. Every entry needs a citation to a named instrument and article, verified against the Official Journal text — not against a secondary source. The _verify fields are working notes and are NOT rendered; a deadline whose _verify still records an open question has not been checked, and an unchecked deadline must not ship.", + "_comment": "Regulatory deadlines shown on the home page. Every entry needs a citation to a named instrument and article, verified against the Official Journal text, not a secondary source. The _verify fields are working notes and are NOT rendered; a deadline whose _verify records an open question has not been checked, and an unchecked deadline must not ship. Only obligations that are law belong here: a Commission working plan is not law and carries no date, so it is described in prose on the page instead (2026-09-25).", "deadlines": [ { "date": "2026-07-19", "dateLabel": "19 July 2026", "title": "Unsold goods", "who": "Large companies", - "summary": "Destruction of unsold textiles and footwear is banned for large companies, and destruction practices must be disclosed.", + "summary": "Destroying unsold textiles and footwear is banned for large companies, and what they discard must be disclosed.", "citation": "ESPR (EU) 2024/1781, Art. 25", - "_verify": "Confirmed: Art. 25; large companies from 19 Jul 2026; medium-sized from 19 Jul 2030; micro/small exempt. Destruction exceptions in Delegated Regulation (EU) 2026/296." + "_verify": "Confirmed: Art. 25; large companies from 19 Jul 2026; medium-sized from 19 Jul 2030; micro/small exempt. Destruction exceptions in Delegated Regulation (EU) 2026/296. Disclosure is Art. 24." }, { "date": "2027-02-18", "dateLabel": "18 February 2027", "title": "Battery passports", "who": "EV, industrial (>2 kWh), LMT", - "summary": "Every battery in scope placed on the EU market needs a digital battery passport, registered in the EU central registry.", + "summary": "Every battery in scope placed on the EU market or put into service needs a battery passport: an electronic record for that individual battery.", "citation": "Regulation (EU) 2023/1542, Art. 77", - "_verify": "Art. 77(1) verified verbatim against the OJ text: from 18 Feb 2027, each LMT battery, each industrial battery above 2 kWh and each EV battery. Registration modalities depend on the Art. 13(5) implementing act." - }, - { - "date": "2026-12-31", - "dateLabel": "Next, from 2026", - "title": "Priority sectors next", - "who": "Textiles, iron & steel, furniture, tyres", - "summary": "The ESPR 2025–2030 working plan sets the order: textiles and iron & steel lead the first delegated acts, each fixing its own DPP application date when adopted. Electronics is not a priority sector in this plan.", - "citation": "ESPR 2025–2030 Working Plan, COM(2025) 187", - "_verify": "Secondary-sourced only — the working plan PDF has not been read here. Adoption 16 Apr 2025 and six priority groups (textiles, furniture, mattresses, tyres, iron & steel, aluminium) come from secondary reporting. Read COM(2025) 187 before treating any of it as verified, and update per-sector dates as each delegated act is adopted." + "_verify": "Art. 77(1) verified verbatim against the consolidated OJ text 02023R1542-20250731 on 2026-09-25: from 18 Feb 2027, each LMT battery, each industrial battery above 2 kWh and each EV battery placed on the market or put into service shall have an electronic record ('battery passport'). The earlier 'registered in the EU central registry' clause was removed: Art. 77 does not say it." } ] } diff --git a/site/dpp-landing/src/data/standards.json b/site/dpp-landing/src/data/standards.json index 924e6d8..97063d4 100644 --- a/site/dpp-landing/src/data/standards.json +++ b/site/dpp-landing/src/data/standards.json @@ -1,10 +1,10 @@ { - "_comment": "Standards row — names and one-line roles only; no third-party logos we do not have rights to use.", + "_comment": "Standards row: names and one-line roles only; no third-party logos we do not have rights to use. The first four are implemented in the code (GS1 Digital Link resolution, W3C VC 2.0 credentials, did:web identity, AAS export), checked 2026-09-25. The fifth is the set of six harmonised DPP standards cited by Implementing Decision (EU) 2026/1736 (OJ 15.7.2026: EN 18216, 18219, 18220, 18221, 18222, 18223; not a range, there is no 18217 or 18218). Alignment with them is work in progress, and the row says so.", "standards": [ { "name": "GS1 Digital Link", "role": "Product identification and QR resolution" }, - { "name": "W3C Verifiable Credentials", "role": "Credential format for signed passports" }, - { "name": "did:web", "role": "Issuer identity, anchored in DNS and HTTPS" }, - { "name": "IDTA AAS", "role": "Industrial submodel export for Industry 4.0 consumers" }, - { "name": "CEN/CENELEC JTC 24", "role": "Harmonised DPP standards, tracked as they finalise" } + { "name": "W3C Verifiable Credentials", "role": "The format of every signed passport" }, + { "name": "did:web", "role": "Issuer identity, anchored in your own web domain" }, + { "name": "IDTA AAS", "role": "Export for Industry 4.0 systems" }, + { "name": "European DPP standards", "role": "The six harmonised standards cited in July 2026. Alignment in progress" } ] } diff --git a/site/dpp-landing/src/pages/index.astro b/site/dpp-landing/src/pages/index.astro index f70c9df..22ee443 100644 --- a/site/dpp-landing/src/pages/index.astro +++ b/site/dpp-landing/src/pages/index.astro @@ -11,7 +11,7 @@ import Base from "../layouts/Base.astro"; const title = "Odal Node: EU Digital Product Passport Infrastructure | ESPR Compliance"; const description = - "Open-source infrastructure for EU Digital Product Passports (DPP) under ESPR. Self-hosted, sovereign compliance: your product data is validated and signed with your keys. Battery, textile, and electronics passports."; + "Self-hosted infrastructure for EU Digital Product Passports (DPP) under ESPR, built on an open-source core. Sovereign compliance: your product data is validated and signed with your keys. Passports for batteries, textiles, steel and more."; // Page-specific structured data. const softwareAppLd = { @@ -27,10 +27,11 @@ const softwareAppLd = { "@type": "Offer", price: "0", priceCurrency: "EUR", - description: "Open-core: dpp-core is Apache-2.0; dpp-engine is BSL-1.1 with free self-host grant.", + description: "Free to self-host for your own compliance under the node's BSL-1.1 grant. The core library, dpp-core, is Apache-2.0.", }, - license: "https://opensource.org/licenses/Apache-2.0", - codeRepository: "https://github.com/odal-node/dpp-core", + // The node is dpp-engine, licensed BSL-1.1; only the core library is Apache-2.0. + license: "https://github.com/odal-node/dpp-engine/blob/main/LICENSE", + codeRepository: "https://github.com/odal-node/dpp-engine", }; // The end-to-end process as one timeline. On the landing the underlying @@ -50,21 +51,29 @@ const steps = [ status title="Signed by you. Verified by anyone." subtitle="Odal Node is sovereign infrastructure for EU Digital Product Passports. Your product data is validated and signed on your infrastructure, with your keys. The raw import files are discarded; the signed passport is stored and served. Anyone can verify it." - primaryHref="https://docs.odal-node.io/quick-start" - primaryLabel="Self-host now" - secondaryHref="https://docs.odal-node.io" - secondaryLabel="Read the docs" + ctaHref="https://docs.odal-node.io" + ctaLabel="Read the docs" />

          Compliance arrives sector by sector, date by date, not "everything by 2027." - Three dated obligations are already in law: + Two dated obligations are already in law:

          + {/* Source: COM(2025) 187 final, 16.4.2025, as verified in dpp-docs + reference/regulatory/DPP-SECTOR-INVENTORY-2026-07.md. These are + indicative adoption years for delegated acts, not passport dates. */}

          + After these, passports arrive product group by product group. The + Commission's 2025–2030 working plan puts iron and steel first, then + textiles, tyres and aluminium, then furniture and mattresses, with + indicative adoption from 2026 to 2029. Each act will set its own passport + dates when it is adopted. +

          +

          The question is whether the infrastructure you adopt now still fits the next sector's act. Odal Node is built so that it does: new sectors arrive as schema and plugin updates, not as new systems. @@ -120,12 +129,12 @@ const steps = [ v: "GS1 Digital Link resolution, W3C VC 2.0 credentials, did:web identity, AAS submodel export.", }, { - k: "Sandboxed sector plugins", - v: "Each sector's compliance logic runs in an isolated sandbox. New regulations land as plugins, not rewrites.", + k: "Sandboxed product-group rules", + v: "Product-group rules run in an isolated sandbox, and new rules arrive as signed updates rather than rewrites of the software.", }, { - k: "Honest open-core", - v: "dpp-core (the regulatory standard in Rust) is Apache-2.0, full capability, forever. dpp-engine is BSL-1.1 with a real self-host grant: production self-hosting is free.", + k: "Open core", + v: "The regulatory library, dpp-core, is Apache-2.0 and complete. The node, dpp-engine, is BSL-1.1 with a grant that makes self-hosting for your own compliance free.", }, { k: "No vendor lock-in", @@ -156,33 +165,30 @@ const steps = [

          We can see

          - The signed passport you publish to a resolver we operate, and the - metadata required to serve it. + What anyone can see: the public part of the passports you publish.

          We cannot see

          - Your private keys (held in-process on your infrastructure), your raw - production data, your supply-chain detail. + Your private keys, your raw production data and your supply-chain + detail. They stay on your infrastructure, and we are not part of it.

          -

          We could see but do not

          +

          Nobody keeps

          - The contents of your import files, which the node discards after - validation. + The contents of your import files. The node reads them once and + discards them after validation.

          -

          - +

          + How this holds by design

          - - -
          +
          diff --git a/site/dpp-landing/src/pages/trust.astro b/site/dpp-landing/src/pages/trust.astro index 9221a40..ac4b53b 100644 --- a/site/dpp-landing/src/pages/trust.astro +++ b/site/dpp-landing/src/pages/trust.astro @@ -1,24 +1,55 @@ --- +// /trust — what Odal can and cannot see, stated as properties of the software. +// +// Describes the self-hosted node only. Managed deployments are an option under +// consideration and are mentioned on the roadmap and nowhere else, so this page +// makes no claim about a deployment that does not exist. import Base from "../layouts/Base.astro"; import Section from "../components/Section.astro"; const title = "Trust | Odal Node"; const description = - "What Odal can see, cannot see, and could see but does not. The proof-bound architecture, stated precisely."; + "What Odal can and cannot see, and why that holds by design rather than by promise."; + +const guarantees = [ + { + title: "Raw inputs are discarded", + body: "The node keeps the signed passport and discards the import file it came from. No setting keeps the raw import after validation.", + }, + { + title: "We cannot reach your node", + body: "When you run the node, we are not part of the deployment and have no access to anything it stores.", + }, + { + title: "Nobody signs for you", + body: "Your signing key is generated and held on your own infrastructure. We cannot sign on your behalf.", + }, +]; --- -
          +

          - Odal Node is built on a proof-bound architecture: the manufacturer's - raw production data is validated and signed locally, on infrastructure - the manufacturer controls, then discarded. Only the signed proof is - stored and served. This is a property of the software, not a policy - promise: it holds regardless of who operates the node. + Odal Node is built on a proof-bound architecture: the manufacturer's raw + production data is validated and signed locally, on infrastructure the + manufacturer controls, then discarded. Only the signed proof is stored + and served. This is a property of the software, not a policy promise: it + holds regardless of who operates the node.

          -
          +
          +
            + {guarantees.map((g) => ( +
          • +

            {g.title}

            +

            {g.body}

            +
          • + ))} +
          +
          + +

          When a passport is resolved, the node can record that it happened: an aggregate count, per passport, per day. That is the whole of it. A scan @@ -30,70 +61,7 @@ const description =

          - - -
          - - - - - - - - - - - - - - - - - - - - - - - - - -
          PropertySelf-hostedManaged
          Node discards raw inputs; stores only the signed proofYes, by designYes, by design
          Odal can access stored dataNo. We are not part of the deploymentConstrained by access controls, audit logging, and contract
          Odal can sign on the operator's behalfNoNo. The operator holds the signing keys
          -
          - -
          +
          1. Import. Product data arrives at your node as CSV, Excel @@ -110,14 +78,14 @@ const description =
          2. Publish. Only the signed passport becomes publicly - resolvable via QR and GS1 Digital Link. The raw input is discarded. + readable, through its QR code and GS1 Digital Link. The raw input is + discarded.
          3. - Verify. Any consumer, authority, or recycler verifies - the signature against your public DID Document. Verification does not - require Odal to be online or to exist. + Verify. Any consumer, authority or recycler checks the + signature against the public identity published on your web domain. + Verification does not need Odal to be online, or even to exist.
          - diff --git a/site/dpp-landing/src/pages/waitlist.astro b/site/dpp-landing/src/pages/waitlist.astro index 1ec3532..20f3c60 100644 --- a/site/dpp-landing/src/pages/waitlist.astro +++ b/site/dpp-landing/src/pages/waitlist.astro @@ -8,7 +8,7 @@ import Section from "../components/Section.astro"; const title = "Join the Waitlist | Odal Node"; const description = - "Sign up for early access to Odal Node, open-source EU Digital Product Passport infrastructure for ESPR compliance."; + "Sign up for early access to Odal Node, self-hosted EU Digital Product Passport infrastructure built on an open-source core."; const MAILBOX = "contact@odal-node.io"; --- From f2ecfb0288fe8653da801281bc722e0b0b24f64d Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 25 Sep 2026 19:38:59 +0200 Subject: [PATCH 046/168] chore(landing): remove dead markup and unused props, clear every check warning, and stop the leakage gate scanning the dpp-core checkout --- .gitignore | 4 + scripts/check-leakage.mjs | 3 + .../scripts/sync-product-groups.mjs | 1 - site/dpp-landing/src/components/Footer.astro | 10 +-- site/dpp-landing/src/components/Hero.astro | 78 +++---------------- site/dpp-landing/src/layouts/Base.astro | 6 +- site/dpp-landing/src/pages/_main.astro | 6 +- site/dpp-landing/src/pages/privacy.astro | 16 ---- site/dpp-landing/src/pages/visibility.astro | 76 +++++++++--------- 9 files changed, 64 insertions(+), 136 deletions(-) diff --git a/.gitignore b/.gitignore index 019076b..4003a39 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,7 @@ Thumbs.db # local-only assets (never publish, never commit — this repo is public) deprecated/ .claude/ + +# Sibling repositories CI checks out for the drift gates. Never ours to commit. +.dpp-core/ +.dpp-engine/ diff --git a/scripts/check-leakage.mjs b/scripts/check-leakage.mjs index 1d3765d..59edb18 100644 --- a/scripts/check-leakage.mjs +++ b/scripts/check-leakage.mjs @@ -27,6 +27,9 @@ const SKIP_DIRS = new Set([ '.astro', '.pnpm-store', '.dpp-engine', + // CI checks dpp-core out here for the product-group drift gate. It is + // another repository's content, not this one's, so it is not ours to scan. + '.dpp-core', 'deprecated', ]); // This file necessarily contains the patterns it searches for. diff --git a/site/dpp-landing/scripts/sync-product-groups.mjs b/site/dpp-landing/scripts/sync-product-groups.mjs index 382bc38..5118d95 100644 --- a/site/dpp-landing/scripts/sync-product-groups.mjs +++ b/site/dpp-landing/scripts/sync-product-groups.mjs @@ -33,7 +33,6 @@ const pinPath = `${root}product-groups-source.json`; const destDir = `${root}src/data/product-groups`; const pin = JSON.parse(readFileSync(pinPath, 'utf8')); -const srcDir = `${coreDir}/${pin.path}`; const checkOnly = process.argv.includes('--check'); diff --git a/site/dpp-landing/src/components/Footer.astro b/site/dpp-landing/src/components/Footer.astro index 4fc3bf4..d18d576 100644 --- a/site/dpp-landing/src/components/Footer.astro +++ b/site/dpp-landing/src/components/Footer.astro @@ -20,13 +20,10 @@ const year = new Date().getFullYear();

          Sovereign Digital Product Passport infrastructure

          -
        - +

        Explore

          @@ -36,8 +33,7 @@ const year = new Date().getFullYear();
        - +

        Product groups

          diff --git a/site/dpp-landing/src/components/Hero.astro b/site/dpp-landing/src/components/Hero.astro index a3758e4..c9459bb 100644 --- a/site/dpp-landing/src/components/Hero.astro +++ b/site/dpp-landing/src/components/Hero.astro @@ -1,62 +1,33 @@ --- // Hero.astro — the highest-stakes block on the entire site. -// Navy band with the brand mark as a quiet watermark; ice text; two CTAs; -// one-line trust row. +// Navy band with the brand mark as a quiet watermark, ice text and a single +// call to action. One action on purpose: a lone outline button on a navy band +// reads as a secondary choice with nothing to be secondary to. import Watermark from "../assets/favicon.svg"; import StatusBadge from "./StatusBadge.astro"; import Button from "./Button.astro"; + interface Props { eyebrow?: string; title: string; subtitle: string; - primaryHref: string; - primaryLabel: string; - secondaryHref: string; - secondaryLabel: string; - trustRow?: string[]; + ctaHref: string; + ctaLabel: string; /** Show the alpha / in-development status badge in the eyebrow row. */ status?: boolean; } -const { - eyebrow, - title, - subtitle, - primaryHref, - primaryLabel, - secondaryHref, - secondaryLabel, - trustRow = [], - status = false, -} = Astro.props; +const { eyebrow, title, subtitle, ctaHref, ctaLabel, status = false } = Astro.props; ---
          - + - + - -
      diff --git a/site/dpp-landing/src/layouts/Base.astro b/site/dpp-landing/src/layouts/Base.astro index 86abad2..cc82b2f 100644 --- a/site/dpp-landing/src/layouts/Base.astro +++ b/site/dpp-landing/src/layouts/Base.astro @@ -71,10 +71,10 @@ const websiteLd = { - From b95867201bebd78f0aa850022ced5e3ad2534921 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 25 Sep 2026 19:47:13 +0200 Subject: [PATCH 047/168] fix(deps): move both sites to Astro 7.3.5 for the critical AVIF RCE advisory and patch the vulnerable transitive packages --- pnpm-lock.yaml | 738 ++++++++++++++++++---------------- site/dpp-docs/package.json | 2 +- site/dpp-landing/package.json | 2 +- 3 files changed, 399 insertions(+), 343 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f34bd16..b149581 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,16 +14,16 @@ importers: dependencies: '@astrojs/starlight': specifier: ^0.41.7 - version: 0.41.7(@astrojs/markdown-remark@7.2.4)(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0))(typescript@5.9.3) + version: 0.41.7(astro@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0))(typescript@5.9.3) '@odal/brand-tokens': specifier: workspace:* version: link:../../packages/brand-tokens '@scalar/api-reference': specifier: ^1.62.5 - version: 1.62.5(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.4.3) + version: 1.62.5(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.6.5) astro: - specifier: ^7.2.4 - version: 7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0) + specifier: ^7.3.5 + version: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0) vue: specifier: ^3.5.39 version: 3.5.39(typescript@5.9.3) @@ -50,8 +50,8 @@ importers: specifier: ^4 version: 4.3.0(vite@8.2.1(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) astro: - specifier: ^7.2.4 - version: 7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0) + specifier: ^7.3.5 + version: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0) tailwindcss: specifier: ^4 version: 4.3.0 @@ -96,69 +96,75 @@ packages: peerDependencies: typescript: ^5.0.0 || ^6.0.0 - '@astrojs/compiler-binding-darwin-arm64@0.3.2': - resolution: {integrity: sha512-MM8tn8CSimcfytaOla4b6acN8mKWiL/rlAA1fpT3/Wl7dNGSE4y8FjTN/zJVNnb63CsLWG5zZwCt01TXtDKh9g==} + '@astrojs/compiler-binding-android-arm64@0.5.0': + resolution: {integrity: sha512-wVviC/1KaBqGVF0DbTZ8UtAE3c8TN84CCe7w4ju3SGzo/ByWKn8HCqSU/+1wdltGCCSa30pjVk3H8DQUgnfhRQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@astrojs/compiler-binding-darwin-arm64@0.5.0': + resolution: {integrity: sha512-5NNIkaGeiztD9QQfhOy+nNrBCZVeHqVZPlfJVjTKq4yJiiSsxAhuBzmVlmK3BDhbSgUT8dai4JV5m6t4/BIQaw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@astrojs/compiler-binding-darwin-x64@0.3.2': - resolution: {integrity: sha512-2lXOlzf8xb7jLomRsf/aswh61/NnGusynB2OwFkK6k4pmOtpfXMYnG0PLfXrEvxXYj69NdCnmUYXtHDd+JOOag==} + '@astrojs/compiler-binding-darwin-x64@0.5.0': + resolution: {integrity: sha512-HU/waaccOQDXpvjoWA/9xzadNZIcWKblJoaw19lCxfVs6AHQYdZZOzZUQjY8HaLYGerTXjdADbra2XU+kXk9Og==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@astrojs/compiler-binding-linux-arm64-gnu@0.3.2': - resolution: {integrity: sha512-BmU3kWj7qnLrd4vzm49zFEPJ5oFnn1tCT4Vt9hZbqdU5Cmb8GZl7fn6VFsnNfe7B18a2gIFtVzbLINtYl5kBjQ==} + '@astrojs/compiler-binding-linux-arm64-gnu@0.5.0': + resolution: {integrity: sha512-a21petBJRi0mNBf+gkRZFoWdGH+VOo0zRQSzM4Nuez5RL5rFFkmbondMejkaRZzvE3LVYU2NVD5MGRgAs4V+nQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@astrojs/compiler-binding-linux-arm64-musl@0.3.2': - resolution: {integrity: sha512-f0heT9ZZEseSu5bHCeb80eL2DH07ArE6U9xi1WT/PEusNjzPmEr3GJsjG1tRLo5VYUUYX7h3ScaqGmGrMOVGmw==} + '@astrojs/compiler-binding-linux-arm64-musl@0.5.0': + resolution: {integrity: sha512-t5rmu23b/6e6v0bhPibCtwPdABXMRL+bUaJXdkJwTpkINp4zCgH6fNa5Z6+RgkgFSCpmg9uf1MtRIqx1gdmBaA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@astrojs/compiler-binding-linux-x64-gnu@0.3.2': - resolution: {integrity: sha512-M8fOUt0itRpqiGyoEA/ij184s8O+hqbCz3+YozRusOOM3osgGljpDThhbKAJjqh82wOo6FioQ4w8PBvU1XMD5Q==} + '@astrojs/compiler-binding-linux-x64-gnu@0.5.0': + resolution: {integrity: sha512-JWtouuxSiDb2OKKFsxOwfqlpJuZHo1ExirlBGfDgoJVG5zeJWwcd2tU2w4UaJA4suT/tAFSLk/vFhM5fLjWV4A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@astrojs/compiler-binding-linux-x64-musl@0.3.2': - resolution: {integrity: sha512-/Kebk8sO6HnLeSd691JkaAPfN7CqR9/KEXmWvyNPkaKNGmj8rTZ/lf2uXtnPu92Lan84UrKdIKVPy1fSo2encQ==} + '@astrojs/compiler-binding-linux-x64-musl@0.5.0': + resolution: {integrity: sha512-LlaSO5v3UC6IxuTKXDViWV/fFHYhLKSO9Of118lNBXv/sjIWwziU+jjENxPgxfq/4oUeOu+613y6H93LBPNTOA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@astrojs/compiler-binding-wasm32-wasi@0.3.2': - resolution: {integrity: sha512-pUA6xbcOSB7DhfzIArB8BCAkFfAIqriiR7zl5zOStd6oU2G0kIKj+GUdGnyXyhfiv881Hffyk5tC0mR18sDjDw==} - engines: {node: '>=14.0.0'} + '@astrojs/compiler-binding-wasm32-wasi@0.5.0': + resolution: {integrity: sha512-IP+Cp0m9yKg/AUT47077vHEaAOh/oT/iUZx1PDnV2GJzUxLSEotdQy5Hhv+ycvEadO/DbETSJfAWeV7kte4etQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@astrojs/compiler-binding-win32-arm64-msvc@0.3.2': - resolution: {integrity: sha512-ESruf+6Qkl1trHUFxI6GSf6t52j8yN2kCNSzMWdzt7V/T09tFHrYzrVaJQohb2C9bJUH76pNvX6Zb51+xCQc9Q==} + '@astrojs/compiler-binding-win32-arm64-msvc@0.5.0': + resolution: {integrity: sha512-v6plwy5GQBGvfv4QgiGhy/qkkQvYKW7Zh2vBArOnsGEDNHyUS43CWYwIQdw4ZdVVmLsFyhwTiueDZEP6zejddQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@astrojs/compiler-binding-win32-x64-msvc@0.3.2': - resolution: {integrity: sha512-wzzVrEbOwbsLWOdEbocskjMRx2aZPxJ7ZbmL+jnpBamFwmigm+2M/wzuM6JWncocgYwLic1csSpalBh96kQKXA==} + '@astrojs/compiler-binding-win32-x64-msvc@0.5.0': + resolution: {integrity: sha512-SgM158cSCJ248UIXaCCW2WqSTjOoFlDv93ed/ATguJqYpZNNVMlOdcPlgNH7WYj/OYeD0rIQr7XyRAYhB7+G1A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@astrojs/compiler-binding@0.3.2': - resolution: {integrity: sha512-8w/9CWmYrAJJ8N0SY3O43ws2BgxoW6u3QsD8u2mE140lMYAlwh+tlNoUeSBq22wVheFuiBbR212l6ixZ2IIgCQ==} + '@astrojs/compiler-binding@0.5.0': + resolution: {integrity: sha512-PxFTx3WoxQnbgUvvwmF6LRQwhHO77a4pewOtJvzxlrYtQ90tLgomLKvks/akrd2iix2ngGlOhxQonKNd8YoBHg==} engines: {node: ^20.19.0 || >=22.12.0} - '@astrojs/compiler-rs@0.3.2': - resolution: {integrity: sha512-xlx/T7JovIKduu4ucbTQUxQ5+Q8wxkHxhLjnZk3VlJbhbQ9RLvvuDk1p2YYFYFQ5y14dVm3FGGO4isQXa4F+Tg==} + '@astrojs/compiler-rs@0.5.0': + resolution: {integrity: sha512-2xIypn5R33FRWCXXXv0sfbDj8xcOvc2VmIbteytxMjOzP9LiGfvqaAFJjhdAjzhs6+ZKzYzbSE79rrPObYW5mg==} engines: {node: '>=22.12.0'} '@astrojs/compiler@2.13.1': @@ -167,6 +173,9 @@ packages: '@astrojs/internal-helpers@0.10.4': resolution: {integrity: sha512-nozZSy/mKYLqe4YrqbKtdOszedAfXYCtw3wZ0d+CAjz4GqQ4L9rl1ltIL5BlgwmYVinJg/RZ0MgGuWOdlyRZlA==} + '@astrojs/internal-helpers@0.11.0': + resolution: {integrity: sha512-3rzxJ+xbo0+8YyqOzLziIN32wmsHdCjEVz2sGOpRxJ+Ben/KiLph4ItxBy1abEL+E8fkRzqjg0rfXmaHJGw9JA==} + '@astrojs/language-server@2.16.10': resolution: {integrity: sha512-87VQ/5GSdHlRnUA+hGuerYyIGAj+9RbZmATyuKLEUePinUXhQ5YkRnRrHhOD9sSi5JOErLjrLkHnfZFEvGrV8w==} hasBin: true @@ -185,6 +194,9 @@ packages: '@astrojs/markdown-satteri@0.3.7': resolution: {integrity: sha512-NHcHbrKW/opbZnTZQ5nH293BdcK2VV0tjuzI88CLnvy2njiEJVwUQ5KFnYd4NoWgHJCY/I1CyjGWCR4Vv3khXQ==} + '@astrojs/markdown-satteri@0.4.2': + resolution: {integrity: sha512-g4QehXnB/MzxQWnSf17NciFInMqEJy9948p8cc7zGg/r7995m0Ln5PMr3Xlnj24iey1LchaoZ3SzTOa3D4P/3w==} + '@astrojs/mdx@7.0.7': resolution: {integrity: sha512-hv+NJh2s+/KDrjXaYNOaS344e3M2ekMXHBR7x9EwBwE3VvE1y/9Ifh1rhR1H2zK2sMgXoUnakI9e9ZeCKPX9/Q==} engines: {node: '>=22.12.0'} @@ -398,12 +410,21 @@ packages: '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/core@1.11.3': + resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==} + '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@emnapi/wasi-threads@1.2.2': resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@emnapi/wasi-threads@1.2.3': + resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} + '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} @@ -606,152 +627,161 @@ packages: resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} engines: {node: '>=18'} - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-arm64@0.35.4': + resolution: {integrity: sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-darwin-x64@0.35.4': + resolution: {integrity: sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + '@img/sharp-freebsd-wasm32@0.35.4': + resolution: {integrity: sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.3': + resolution: {integrity: sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==} cpu: [arm64] os: [darwin] - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + '@img/sharp-libvips-darwin-x64@1.3.3': + resolution: {integrity: sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==} cpu: [x64] os: [darwin] - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + '@img/sharp-libvips-linux-arm64@1.3.3': + resolution: {integrity: sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + '@img/sharp-libvips-linux-arm@1.3.3': + resolution: {integrity: sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + '@img/sharp-libvips-linux-ppc64@1.3.3': + resolution: {integrity: sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + '@img/sharp-libvips-linux-riscv64@1.3.3': + resolution: {integrity: sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + '@img/sharp-libvips-linux-s390x@1.3.3': + resolution: {integrity: sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + '@img/sharp-libvips-linux-x64@1.3.3': + resolution: {integrity: sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': + resolution: {integrity: sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + '@img/sharp-libvips-linuxmusl-x64@1.3.3': + resolution: {integrity: sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm64@0.35.4': + resolution: {integrity: sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [glibc] - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-arm@0.35.4': + resolution: {integrity: sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==} + engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] libc: [glibc] - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-ppc64@0.35.4': + resolution: {integrity: sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==} + engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] libc: [glibc] - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-riscv64@0.35.4': + resolution: {integrity: sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==} + engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] libc: [glibc] - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-s390x@0.35.4': + resolution: {integrity: sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==} + engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] libc: [glibc] - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linux-x64@0.35.4': + resolution: {integrity: sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [glibc] - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-arm64@0.35.4': + resolution: {integrity: sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] libc: [musl] - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-linuxmusl-x64@0.35.4': + resolution: {integrity: sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] libc: [musl] - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-wasm32@0.35.4': + resolution: {integrity: sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.4': + resolution: {integrity: sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==} + engines: {node: '>=20.9.0'} cpu: [wasm32] - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-arm64@0.35.4': + resolution: {integrity: sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-ia32@0.35.4': + resolution: {integrity: sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==} + engines: {node: ^20.9.0} cpu: [ia32] os: [win32] - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + '@img/sharp-win32-x64@0.35.4': + resolution: {integrity: sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] @@ -817,6 +847,13 @@ packages: '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + '@napi-rs/wasm-runtime@1.2.4': + resolution: {integrity: sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + '@opentelemetry/api@1.9.0': resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} @@ -1459,12 +1496,12 @@ packages: peerDependencies: astro: ^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0 - astro@7.2.4: - resolution: {integrity: sha512-+cuLsBns2wwUHI9a10xZMbjrF91m7+QNwqTVeljTx0B8Lf+8h0LgVGjdVIL2FALDKD2I565lczeeS3BFC+KdZg==} + astro@7.3.5: + resolution: {integrity: sha512-1p/ernaq/RuESbM5NMjWzbO6SiebMGrGcYlGjMfBhzzgM+n/X7AUmVyw8BqW2QYCdivpsHwJBcD1fqmlTZjZHg==} engines: {node: '>=22.12.0', npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true peerDependencies: - '@astrojs/markdown-remark': 7.2.4 + '@astrojs/markdown-remark': ^7.3.0 peerDependenciesMeta: '@astrojs/markdown-remark': optional: true @@ -1496,10 +1533,6 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -1557,10 +1590,6 @@ packages: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} - commander@14.0.3: - resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} - engines: {node: '>=20'} - common-ancestor-path@2.0.0: resolution: {integrity: sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==} engines: {node: '>= 18'} @@ -1582,8 +1611,8 @@ packages: crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} - css-select@5.2.2: - resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + css-select@6.0.0: + resolution: {integrity: sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==} css-selector-parser@3.3.0: resolution: {integrity: sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==} @@ -1596,8 +1625,8 @@ packages: resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} - css-what@6.2.2: - resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + css-what@7.0.0: + resolution: {integrity: sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==} engines: {node: '>= 6'} cssesc@3.0.0: @@ -1652,8 +1681,8 @@ packages: devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - diff@8.0.4: - resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} engines: {node: '>=0.3.1'} direction@2.0.1: @@ -1767,8 +1796,8 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.2: - resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -1786,9 +1815,9 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} - find-process@2.1.1: - resolution: {integrity: sha512-SrQDx3QhlmHM90iqn9rdjCQcw/T+WlpOkHFsjoRgB+zTpDfltNA1VSNYeYELwhUTJy12UFxqjWhmhOrJc+o4sA==} - hasBin: true + find-proc@0.2.0: + resolution: {integrity: sha512-a6h2nTrgB3g5Wrn4au9Ux4kMk7IJtyF6oA8uGn+mk3weLZssUtc1cp6meHIzzuW2o1srrBAxzkmXLbqjRkgB6w==} + engines: {node: ^20.19.0 || >=22.12.0} flatted@3.4.2: resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} @@ -1849,10 +1878,6 @@ packages: h3@1.15.11: resolution: {integrity: sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==} - has-flag@4.0.0: - resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} - engines: {node: '>=8'} - hast-util-embedded@3.0.0: resolution: {integrity: sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==} @@ -2017,8 +2042,8 @@ packages: js-base64@3.8.1: resolution: {integrity: sha512-5xVjhUZlHHeuO2W7w2rDFj/Kl1xLX+HjZxdOQwCsUOifl6UaoH1o1wsbsTMz+r0aeC7gCijvru02j6TfKZWzKg==} - js-yaml@4.3.1: - resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true json-schema-traverse@1.0.0: @@ -2189,10 +2214,6 @@ packages: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} - loglevel@1.9.2: - resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} - engines: {node: '>= 0.6.0'} - longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -2404,13 +2425,8 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} - nanoid@3.3.12: - resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - nanoid@3.3.18: - resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -2442,8 +2458,8 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} - obug@2.1.4: - resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + obug@3.0.0: + resolution: {integrity: sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==} engines: {node: '>=12.20.0'} ofetch@1.5.1: @@ -2536,12 +2552,8 @@ packages: resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} engines: {node: '>=4'} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} - engines: {node: ^10 || ^12 || >=14} - - postcss@8.5.26: - resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} prettier@3.8.3: @@ -2704,17 +2716,31 @@ packages: resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} engines: {node: '>=11.0.0'} + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + semver@7.8.1: resolution: {integrity: sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==} engines: {node: '>=10'} hasBin: true + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + set-cookie-parser@3.1.0: resolution: {integrity: sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==} - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + sharp@0.35.4: + resolution: {integrity: sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==} + engines: {node: '>=20.9.0'} + peerDependencies: + '@types/node': '*' + peerDependenciesMeta: + '@types/node': + optional: true shiki@4.4.3: resolution: {integrity: sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==} @@ -2728,8 +2754,8 @@ packages: engines: {node: '>=20.19.5', npm: '>=10.8.2'} hasBin: true - smol-toml@1.6.1: - resolution: {integrity: sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==} + smol-toml@1.9.0: + resolution: {integrity: sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==} engines: {node: '>= 18'} source-map-js@1.2.1: @@ -2782,12 +2808,8 @@ packages: resolution: {integrity: sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==} engines: {node: '>=18'} - supports-color@7.2.0: - resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} - engines: {node: '>=8'} - - svgo@4.0.1: - resolution: {integrity: sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==} + svgo@4.1.0: + resolution: {integrity: sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==} engines: {node: '>=16'} hasBin: true @@ -2820,8 +2842,8 @@ packages: tiny-inflate@1.0.3: resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} - tinyclip@0.1.15: - resolution: {integrity: sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==} + tinyclip@1.0.3: + resolution: {integrity: sha512-nTv2raKFMzGG6JJp0lpNDsRPdfFE6UHlu2rVEjXo8vkx1ZS3LW2CVXqIaLu8xZlu+6tbFs9V58Qc/Jmk/Ii7wA==} engines: {node: ^16.14.0 || >= 17.3.0} tinyexec@1.2.2: @@ -2996,6 +3018,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + verkit@0.4.1: + resolution: {integrity: sha512-mMVzj0TXExtVdlDEq+Mzp0eyOuyznNpFobNM3uAqe3HVm/Ps2c+u17BligMkZjZCA6EiXpoon8iRTer3iu40SQ==} + engines: {node: '>=18.12.0'} + vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} @@ -3232,33 +3258,36 @@ packages: zod@4.4.3: resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + zod@4.6.5: + resolution: {integrity: sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} snapshots: - '@ai-sdk/gateway@3.0.13(zod@4.4.3)': + '@ai-sdk/gateway@3.0.13(zod@4.6.5)': dependencies: '@ai-sdk/provider': 3.0.2 - '@ai-sdk/provider-utils': 4.0.5(zod@4.4.3) + '@ai-sdk/provider-utils': 4.0.5(zod@4.6.5) '@vercel/oidc': 3.1.0 - zod: 4.4.3 + zod: 4.6.5 - '@ai-sdk/provider-utils@4.0.5(zod@4.4.3)': + '@ai-sdk/provider-utils@4.0.5(zod@4.6.5)': dependencies: '@ai-sdk/provider': 3.0.2 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.1.0 - zod: 4.4.3 + zod: 4.6.5 '@ai-sdk/provider@3.0.2': dependencies: json-schema: 0.4.0 - '@ai-sdk/vue@3.0.33(vue@3.5.39(typescript@5.9.3))(zod@4.4.3)': + '@ai-sdk/vue@3.0.33(vue@3.5.39(typescript@5.9.3))(zod@4.6.5)': dependencies: - '@ai-sdk/provider-utils': 4.0.5(zod@4.4.3) - ai: 6.0.33(zod@4.4.3) + '@ai-sdk/provider-utils': 4.0.5(zod@4.6.5) + ai: 6.0.33(zod@4.6.5) swrv: 1.2.0(vue@3.5.39(typescript@5.9.3)) vue: 3.5.39(typescript@5.9.3) transitivePeerDependencies: @@ -3275,59 +3304,56 @@ snapshots: - prettier - prettier-plugin-astro - '@astrojs/compiler-binding-darwin-arm64@0.3.2': + '@astrojs/compiler-binding-android-arm64@0.5.0': optional: true - '@astrojs/compiler-binding-darwin-x64@0.3.2': + '@astrojs/compiler-binding-darwin-arm64@0.5.0': optional: true - '@astrojs/compiler-binding-linux-arm64-gnu@0.3.2': + '@astrojs/compiler-binding-darwin-x64@0.5.0': optional: true - '@astrojs/compiler-binding-linux-arm64-musl@0.3.2': + '@astrojs/compiler-binding-linux-arm64-gnu@0.5.0': optional: true - '@astrojs/compiler-binding-linux-x64-gnu@0.3.2': + '@astrojs/compiler-binding-linux-arm64-musl@0.5.0': optional: true - '@astrojs/compiler-binding-linux-x64-musl@0.3.2': + '@astrojs/compiler-binding-linux-x64-gnu@0.5.0': optional: true - '@astrojs/compiler-binding-wasm32-wasi@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + '@astrojs/compiler-binding-linux-x64-musl@0.5.0': + optional: true + + '@astrojs/compiler-binding-wasm32-wasi@0.5.0': dependencies: - '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@napi-rs/wasm-runtime': 1.2.4(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) optional: true - '@astrojs/compiler-binding-win32-arm64-msvc@0.3.2': + '@astrojs/compiler-binding-win32-arm64-msvc@0.5.0': optional: true - '@astrojs/compiler-binding-win32-x64-msvc@0.3.2': + '@astrojs/compiler-binding-win32-x64-msvc@0.5.0': optional: true - '@astrojs/compiler-binding@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + '@astrojs/compiler-binding@0.5.0': optionalDependencies: - '@astrojs/compiler-binding-darwin-arm64': 0.3.2 - '@astrojs/compiler-binding-darwin-x64': 0.3.2 - '@astrojs/compiler-binding-linux-arm64-gnu': 0.3.2 - '@astrojs/compiler-binding-linux-arm64-musl': 0.3.2 - '@astrojs/compiler-binding-linux-x64-gnu': 0.3.2 - '@astrojs/compiler-binding-linux-x64-musl': 0.3.2 - '@astrojs/compiler-binding-wasm32-wasi': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - '@astrojs/compiler-binding-win32-arm64-msvc': 0.3.2 - '@astrojs/compiler-binding-win32-x64-msvc': 0.3.2 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' + '@astrojs/compiler-binding-android-arm64': 0.5.0 + '@astrojs/compiler-binding-darwin-arm64': 0.5.0 + '@astrojs/compiler-binding-darwin-x64': 0.5.0 + '@astrojs/compiler-binding-linux-arm64-gnu': 0.5.0 + '@astrojs/compiler-binding-linux-arm64-musl': 0.5.0 + '@astrojs/compiler-binding-linux-x64-gnu': 0.5.0 + '@astrojs/compiler-binding-linux-x64-musl': 0.5.0 + '@astrojs/compiler-binding-wasm32-wasi': 0.5.0 + '@astrojs/compiler-binding-win32-arm64-msvc': 0.5.0 + '@astrojs/compiler-binding-win32-x64-msvc': 0.5.0 - '@astrojs/compiler-rs@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + '@astrojs/compiler-rs@0.5.0': dependencies: - '@astrojs/compiler-binding': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' + '@astrojs/compiler-binding': 0.5.0 '@astrojs/compiler@2.13.1': {} @@ -3335,11 +3361,22 @@ snapshots: dependencies: '@types/hast': 3.0.5 '@types/mdast': 4.0.4 - js-yaml: 4.3.1 + js-yaml: 4.3.2 picomatch: 4.0.5 retext-smartypants: 6.2.0 shiki: 4.4.3 - smol-toml: 1.6.1 + smol-toml: 1.9.0 + unified: 11.0.5 + + '@astrojs/internal-helpers@0.11.0': + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + js-yaml: 4.3.2 + picomatch: 4.0.5 + retext-smartypants: 6.2.0 + shiki: 4.4.3 + smol-toml: 1.9.0 unified: 11.0.5 '@astrojs/language-server@2.16.10(prettier@3.8.3)(typescript@5.9.3)': @@ -3396,13 +3433,20 @@ snapshots: github-slugger: 2.0.0 satteri: 0.10.4 - '@astrojs/mdx@7.0.7(@astrojs/markdown-satteri@0.3.7)(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0))': + '@astrojs/markdown-satteri@0.4.2': + dependencies: + '@astrojs/internal-helpers': 0.11.0 + '@astrojs/prism': 4.0.2 + github-slugger: 2.0.0 + satteri: 0.10.4 + + '@astrojs/mdx@7.0.7(@astrojs/markdown-satteri@0.3.7)(astro@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@astrojs/internal-helpers': 0.10.4 '@astrojs/markdown-remark': 7.2.4 '@mdx-js/mdx': 3.1.1 acorn: 8.16.0 - astro: 7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0) + astro: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0) es-module-lexer: 2.3.2 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -3428,24 +3472,24 @@ snapshots: stream-replace-string: 2.0.0 zod: 4.4.3 - '@astrojs/starlight@0.41.7(@astrojs/markdown-remark@7.2.4)(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0))(typescript@5.9.3)': + '@astrojs/starlight@0.41.7(astro@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0))(typescript@5.9.3)': dependencies: '@astrojs/markdown-satteri': 0.3.7 - '@astrojs/mdx': 7.0.7(@astrojs/markdown-satteri@0.3.7)(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0)) + '@astrojs/mdx': 7.0.7(@astrojs/markdown-satteri@0.3.7)(astro@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0)) '@astrojs/sitemap': 3.7.3 '@pagefind/default-ui': 1.5.2 '@types/hast': 3.0.5 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0) - astro-expressive-code: 0.44.1(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0)) + astro: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0) + astro-expressive-code: 0.44.1(astro@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0)) bcp-47: 2.1.0 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 hast-util-to-string: 3.0.1 hastscript: 9.0.1 i18next: 26.3.6(typescript@5.9.3) - js-yaml: 4.3.1 + js-yaml: 4.3.2 klona: 2.0.6 magic-string: 0.30.21 mdast-util-directive: 3.1.0 @@ -3460,8 +3504,6 @@ snapshots: unified: 11.0.5 unist-util-visit: 5.1.0 vfile: 6.0.3 - optionalDependencies: - '@astrojs/markdown-remark': 7.2.4 transitivePeerDependencies: - supports-color - typescript @@ -3687,16 +3729,32 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/core@1.11.3': + dependencies: + '@emnapi/wasi-threads': 1.2.3 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.11.1': dependencies: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.3': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.2': dependencies: tslib: 2.8.1 optional: true + '@emnapi/wasi-threads@1.2.3': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.28.2': optional: true @@ -3782,8 +3840,8 @@ snapshots: hast-util-to-html: 9.0.5 hast-util-to-text: 4.0.2 hastscript: 9.0.1 - postcss: 8.5.26 - postcss-nested: 6.2.0(postcss@8.5.26) + postcss: 8.5.28 + postcss-nested: 6.2.0(postcss@8.5.28) unist-util-visit: 5.1.0 unist-util-visit-parents: 6.0.2 @@ -3843,98 +3901,108 @@ snapshots: '@img/colour@1.1.0': optional: true - '@img/sharp-darwin-arm64@0.34.5': + '@img/sharp-darwin-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 optional: true - '@img/sharp-darwin-x64@0.34.5': + '@img/sharp-darwin-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.3.3 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.4': + dependencies: + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-libvips-darwin-arm64@1.2.4': + '@img/sharp-libvips-darwin-arm64@1.3.3': optional: true - '@img/sharp-libvips-darwin-x64@1.2.4': + '@img/sharp-libvips-darwin-x64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm64@1.2.4': + '@img/sharp-libvips-linux-arm64@1.3.3': optional: true - '@img/sharp-libvips-linux-arm@1.2.4': + '@img/sharp-libvips-linux-arm@1.3.3': optional: true - '@img/sharp-libvips-linux-ppc64@1.2.4': + '@img/sharp-libvips-linux-ppc64@1.3.3': optional: true - '@img/sharp-libvips-linux-riscv64@1.2.4': + '@img/sharp-libvips-linux-riscv64@1.3.3': optional: true - '@img/sharp-libvips-linux-s390x@1.2.4': + '@img/sharp-libvips-linux-s390x@1.3.3': optional: true - '@img/sharp-libvips-linux-x64@1.2.4': + '@img/sharp-libvips-linux-x64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + '@img/sharp-libvips-linuxmusl-arm64@1.3.3': optional: true - '@img/sharp-libvips-linuxmusl-x64@1.2.4': + '@img/sharp-libvips-linuxmusl-x64@1.3.3': optional: true - '@img/sharp-linux-arm64@0.34.5': + '@img/sharp-linux-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.3.3 optional: true - '@img/sharp-linux-arm@0.34.5': + '@img/sharp-linux-arm@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.3.3 optional: true - '@img/sharp-linux-ppc64@0.34.5': + '@img/sharp-linux-ppc64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.3.3 optional: true - '@img/sharp-linux-riscv64@0.34.5': + '@img/sharp-linux-riscv64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.3.3 optional: true - '@img/sharp-linux-s390x@0.34.5': + '@img/sharp-linux-s390x@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.3.3 optional: true - '@img/sharp-linux-x64@0.34.5': + '@img/sharp-linux-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.3.3 optional: true - '@img/sharp-linuxmusl-arm64@0.34.5': + '@img/sharp-linuxmusl-arm64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 optional: true - '@img/sharp-linuxmusl-x64@0.34.5': + '@img/sharp-linuxmusl-x64@0.35.4': optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 optional: true - '@img/sharp-wasm32@0.34.5': + '@img/sharp-wasm32@0.35.4': dependencies: - '@emnapi/runtime': 1.11.1 + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.4': + dependencies: + '@img/sharp-wasm32': 0.35.4 optional: true - '@img/sharp-win32-arm64@0.34.5': + '@img/sharp-win32-arm64@0.35.4': optional: true - '@img/sharp-win32-ia32@0.34.5': + '@img/sharp-win32-ia32@0.35.4': optional: true - '@img/sharp-win32-x64@0.34.5': + '@img/sharp-win32-x64@0.35.4': optional: true '@internationalized/date@3.12.2': @@ -4049,6 +4117,13 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@napi-rs/wasm-runtime@1.2.4(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': + dependencies: + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@tybys/wasm-util': 0.10.3 + optional: true + '@opentelemetry/api@1.9.0': {} '@oslojs/encoding@1.1.0': {} @@ -4133,9 +4208,9 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} - '@scalar/agent-chat@0.12.18(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.4.3)': + '@scalar/agent-chat@0.12.18(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.6.5)': dependencies: - '@ai-sdk/vue': 3.0.33(vue@3.5.39(typescript@5.9.3))(zod@4.4.3) + '@ai-sdk/vue': 3.0.33(vue@3.5.39(typescript@5.9.3))(zod@4.6.5) '@scalar/api-client': 3.13.3(tailwindcss@4.3.0)(typescript@5.9.3) '@scalar/components': 0.27.6(tailwindcss@4.3.0)(typescript@5.9.3) '@scalar/helpers': 0.9.0 @@ -4149,7 +4224,7 @@ snapshots: '@scalar/validation': 0.6.0 '@scalar/workspace-store': 0.55.3(typescript@5.9.3) '@vueuse/core': 13.9.0(vue@3.5.39(typescript@5.9.3)) - ai: 6.0.33(zod@4.4.3) + ai: 6.0.33(zod@4.6.5) js-base64: 3.8.1 neverpanic: 0.0.8 truncate-json: 3.0.1 @@ -4219,10 +4294,10 @@ snapshots: - typescript - universal-cookie - '@scalar/api-reference@1.62.5(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.4.3)': + '@scalar/api-reference@1.62.5(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.6.5)': dependencies: '@headlessui/vue': 1.7.23(vue@3.5.39(typescript@5.9.3)) - '@scalar/agent-chat': 0.12.18(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.4.3) + '@scalar/agent-chat': 0.12.18(tailwindcss@4.3.0)(typescript@5.9.3)(zod@4.6.5) '@scalar/api-client': 3.13.3(tailwindcss@4.3.0)(typescript@5.9.3) '@scalar/blocks': 0.1.4(tailwindcss@4.3.0)(typescript@5.9.3) '@scalar/code-highlight': 0.4.1 @@ -4714,7 +4789,7 @@ snapshots: '@vue/shared': 3.5.39 estree-walker: 2.0.2 magic-string: 0.30.21 - postcss: 8.5.15 + postcss: 8.5.28 source-map-js: 1.2.1 '@vue/compiler-ssr@3.5.39': @@ -4793,13 +4868,13 @@ snapshots: acorn@8.16.0: {} - ai@6.0.33(zod@4.4.3): + ai@6.0.33(zod@4.6.5): dependencies: - '@ai-sdk/gateway': 3.0.13(zod@4.4.3) + '@ai-sdk/gateway': 3.0.13(zod@4.6.5) '@ai-sdk/provider': 3.0.2 - '@ai-sdk/provider-utils': 4.0.5(zod@4.4.3) + '@ai-sdk/provider-utils': 4.0.5(zod@4.6.5) '@opentelemetry/api': 1.9.0 - zod: 4.4.3 + zod: 4.6.5 ajv-draft-04@1.0.0(ajv@8.20.0): optionalDependencies: @@ -4808,7 +4883,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.2 + fast-uri: 3.1.8 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -4841,17 +4916,17 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.44.1(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0)): + astro-expressive-code@0.44.1(astro@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0)): dependencies: - astro: 7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0) + astro: 7.3.5(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0) rehype-expressive-code: 0.44.1 url-extras: 0.1.0 - astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0): + astro@7.3.5(@types/node@24.12.4)(jiti@2.7.0)(yaml@2.9.0): dependencies: - '@astrojs/compiler-rs': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - '@astrojs/internal-helpers': 0.10.4 - '@astrojs/markdown-satteri': 0.3.7 + '@astrojs/compiler-rs': 0.5.0 + '@astrojs/internal-helpers': 0.11.0 + '@astrojs/markdown-satteri': 0.4.2 '@astrojs/telemetry': 3.3.3 '@capsizecss/unpack': 4.0.0 '@clack/prompts': 1.7.0 @@ -4864,47 +4939,46 @@ snapshots: common-ancestor-path: 2.0.0 cookie: 2.0.1 devalue: 5.8.1 - diff: 8.0.4 + diff: 9.0.0 dset: 3.1.4 es-module-lexer: 2.3.2 esbuild: 0.28.2 - find-process: 2.1.1 + find-proc: 0.2.0 flattie: 1.1.1 fontace: 0.4.1 get-tsconfig: 5.0.0-beta.4 github-slugger: 2.0.0 html-escaper: 3.0.3 http-cache-semantics: 4.2.0 - js-yaml: 4.3.1 + js-yaml: 4.3.2 jsonc-parser: 3.3.1 magic-string: 1.2.1 magicast: 0.5.3 mrmime: 2.0.1 neotraverse: 1.0.1 - obug: 2.1.4 + obug: 3.0.0 p-limit: 7.3.1 p-queue: 9.3.3 package-manager-detector: 1.6.0 piccolore: 0.1.3 picomatch: 4.0.5 - semver: 7.8.1 shiki: 4.4.3 - smol-toml: 1.6.1 - svgo: 4.0.1 - tinyclip: 0.1.15 + smol-toml: 1.9.0 + svgo: 4.1.0 + tinyclip: 1.0.3 tinyexec: 1.2.2 tinyglobby: 0.2.17 ultrahtml: 1.6.0 unifont: 0.7.5 unstorage: 1.17.5 + verkit: 0.4.1 vite: 8.2.1(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0) vitefu: 1.1.3(vite@8.2.1(@types/node@24.12.4)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 - zod: 4.4.3 + zod: 4.6.5 optionalDependencies: - '@astrojs/markdown-remark': 7.2.4 - sharp: 0.34.5 + sharp: 0.35.4(@types/node@24.12.4) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -4914,8 +4988,6 @@ snapshots: - '@azure/storage-blob' - '@capacitor/preferences' - '@deno/kv' - - '@emnapi/core' - - '@emnapi/runtime' - '@netlify/blobs' - '@planetscale/database' - '@types/node' @@ -4961,11 +5033,6 @@ snapshots: ccount@2.0.1: {} - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chalk@5.6.2: {} character-entities-html4@2.1.0: {} @@ -5018,8 +5085,6 @@ snapshots: commander@11.1.0: {} - commander@14.0.3: {} - common-ancestor-path@2.0.0: {} convert-hrtime@5.0.0: {} @@ -5034,10 +5099,10 @@ snapshots: dependencies: uncrypto: 0.1.3 - css-select@5.2.2: + css-select@6.0.0: dependencies: boolbase: 1.0.0 - css-what: 6.2.2 + css-what: 7.0.0 domhandler: 5.0.3 domutils: 3.2.2 nth-check: 2.1.1 @@ -5054,7 +5119,7 @@ snapshots: mdn-data: 2.27.1 source-map-js: 1.2.1 - css-what@6.2.2: {} + css-what@7.0.0: {} cssesc@3.0.0: {} @@ -5092,7 +5157,7 @@ snapshots: dependencies: dequal: 2.0.3 - diff@8.0.4: {} + diff@9.0.0: {} direction@2.0.1: {} @@ -5239,7 +5304,7 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.2: {} + fast-uri@3.1.8: {} fast-wrap-ansi@0.2.2: dependencies: @@ -5257,11 +5322,7 @@ snapshots: dependencies: to-regex-range: 5.0.1 - find-process@2.1.1: - dependencies: - chalk: 4.1.2 - commander: 14.0.3 - loglevel: 1.9.2 + find-proc@0.2.0: {} flatted@3.4.2: {} @@ -5316,8 +5377,6 @@ snapshots: ufo: 1.6.4 uncrypto: 0.1.3 - has-flag@4.0.0: {} - hast-util-embedded@3.0.0: dependencies: '@types/hast': 3.0.5 @@ -5581,7 +5640,7 @@ snapshots: js-base64@3.8.1: {} - js-yaml@4.3.1: + js-yaml@4.3.2: dependencies: argparse: 2.0.1 @@ -5695,8 +5754,6 @@ snapshots: lightningcss-win32-arm64-msvc: 1.33.0 lightningcss-win32-x64-msvc: 1.33.0 - loglevel@1.9.2: {} - longest-streak@3.1.0: {} lowlight@3.3.0: @@ -6200,9 +6257,7 @@ snapshots: muggle-string@0.4.1: {} - nanoid@3.3.12: {} - - nanoid@3.3.18: {} + nanoid@3.3.19: {} nanoid@5.1.16: {} @@ -6224,7 +6279,7 @@ snapshots: dependencies: boolbase: 1.0.0 - obug@2.1.4: {} + obug@3.0.0: {} ofetch@1.5.1: dependencies: @@ -6312,9 +6367,9 @@ snapshots: picomatch@4.0.5: {} - postcss-nested@6.2.0(postcss@8.5.26): + postcss-nested@6.2.0(postcss@8.5.28): dependencies: - postcss: 8.5.26 + postcss: 8.5.28 postcss-selector-parser: 6.1.2 postcss-selector-parser@6.1.2: @@ -6322,15 +6377,9 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss@8.5.15: - dependencies: - nanoid: 3.3.12 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - postcss@8.5.26: + postcss@8.5.28: dependencies: - nanoid: 3.3.18 + nanoid: 3.3.19 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -6617,40 +6666,47 @@ snapshots: sax@1.6.0: {} + sax@1.6.1: {} + semver@7.8.1: {} + semver@7.8.5: + optional: true + set-cookie-parser@3.1.0: {} - sharp@0.34.5: + sharp@0.35.4(@types/node@24.12.4): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 - semver: 7.8.1 + semver: 7.8.5 optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 + '@img/sharp-darwin-arm64': 0.35.4 + '@img/sharp-darwin-x64': 0.35.4 + '@img/sharp-freebsd-wasm32': 0.35.4 + '@img/sharp-libvips-darwin-arm64': 1.3.3 + '@img/sharp-libvips-darwin-x64': 1.3.3 + '@img/sharp-libvips-linux-arm': 1.3.3 + '@img/sharp-libvips-linux-arm64': 1.3.3 + '@img/sharp-libvips-linux-ppc64': 1.3.3 + '@img/sharp-libvips-linux-riscv64': 1.3.3 + '@img/sharp-libvips-linux-s390x': 1.3.3 + '@img/sharp-libvips-linux-x64': 1.3.3 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.3 + '@img/sharp-libvips-linuxmusl-x64': 1.3.3 + '@img/sharp-linux-arm': 0.35.4 + '@img/sharp-linux-arm64': 0.35.4 + '@img/sharp-linux-ppc64': 0.35.4 + '@img/sharp-linux-riscv64': 0.35.4 + '@img/sharp-linux-s390x': 0.35.4 + '@img/sharp-linux-x64': 0.35.4 + '@img/sharp-linuxmusl-arm64': 0.35.4 + '@img/sharp-linuxmusl-x64': 0.35.4 + '@img/sharp-webcontainers-wasm32': 0.35.4 + '@img/sharp-win32-arm64': 0.35.4 + '@img/sharp-win32-ia32': 0.35.4 + '@img/sharp-win32-x64': 0.35.4 + '@types/node': 24.12.4 optional: true shiki@4.4.3: @@ -6673,7 +6729,7 @@ snapshots: arg: 5.0.2 sax: 1.6.0 - smol-toml@1.6.1: {} + smol-toml@1.9.0: {} source-map-js@1.2.1: {} @@ -6725,19 +6781,15 @@ snapshots: make-asynchronous: 1.1.0 time-span: 5.1.0 - supports-color@7.2.0: - dependencies: - has-flag: 4.0.0 - - svgo@4.0.1: + svgo@4.1.0: dependencies: commander: 11.1.0 - css-select: 5.2.2 + css-select: 6.0.0 css-tree: 3.2.1 - css-what: 6.2.2 + css-what: 7.0.0 csso: 5.0.5 picocolors: 1.1.1 - sax: 1.6.0 + sax: 1.6.1 swrv@1.2.0(vue@3.5.39(typescript@5.9.3)): dependencies: @@ -6759,7 +6811,7 @@ snapshots: tiny-inflate@1.0.3: {} - tinyclip@0.1.15: {} + tinyclip@1.0.3: {} tinyexec@1.2.2: {} @@ -6894,6 +6946,8 @@ snapshots: util-deprecate@1.0.2: {} + verkit@0.4.1: {} + vfile-location@5.0.3: dependencies: '@types/unist': 3.0.3 @@ -6921,7 +6975,7 @@ snapshots: dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.26 + postcss: 8.5.28 rolldown: 1.2.5 tinyglobby: 0.2.17 optionalDependencies: @@ -7102,4 +7156,6 @@ snapshots: zod@4.4.3: {} + zod@4.6.5: {} + zwitch@2.0.4: {} diff --git a/site/dpp-docs/package.json b/site/dpp-docs/package.json index dfe82ab..379b178 100644 --- a/site/dpp-docs/package.json +++ b/site/dpp-docs/package.json @@ -17,7 +17,7 @@ "@astrojs/starlight": "^0.41.7", "@odal/brand-tokens": "workspace:*", "@scalar/api-reference": "^1.62.5", - "astro": "^7.2.4", + "astro": "^7.3.5", "vue": "^3.5.39" }, "devDependencies": { diff --git a/site/dpp-landing/package.json b/site/dpp-landing/package.json index b5746dd..c4911ee 100644 --- a/site/dpp-landing/package.json +++ b/site/dpp-landing/package.json @@ -17,7 +17,7 @@ "@astrojs/sitemap": "^3", "@odal/brand-tokens": "workspace:*", "@tailwindcss/vite": "^4", - "astro": "^7.2.4", + "astro": "^7.3.5", "tailwindcss": "^4" }, "devDependencies": { From cc32c57b015c773c58b3f4b803a09e3ea6ed9076 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 25 Sep 2026 19:59:28 +0200 Subject: [PATCH 048/168] feat(landing): vendor dpp-core's instrument registry and generate the regulations pages from it --- .github/workflows/ci.yml | 12 +- site/dpp-landing/core-source.json | 17 ++ site/dpp-landing/package.json | 4 +- site/dpp-landing/product-groups-source.json | 7 - site/dpp-landing/scripts/sync-from-core.mjs | 110 ++++++++++++ .../scripts/sync-product-groups.mjs | 147 --------------- site/dpp-landing/src/components/ActCard.astro | 63 +++++++ .../src/components/BasisMark.astro | 24 +++ .../instruments/battery-reg-2023-1542.json | 27 +++ .../src/data/instruments/cpr-2024-3110.json | 23 +++ .../data/instruments/detergents-2026-405.json | 23 +++ .../ecodesign-energy-labelling-mobile.json | 27 +++ .../src/data/instruments/elv-2026-1738.json | 14 ++ .../espr-horizontal-eee-recyclability.json | 17 ++ .../espr-horizontal-repairability.json | 10 ++ .../src/data/instruments/espr.json | 61 +++++++ .../src/data/instruments/ppwr-2025-40.json | 11 ++ .../instruments/toy-safety-2025-2509.json | 23 +++ .../unsold-goods-derogations-2026-296.json | 25 +++ .../unsold-goods-format-2026-2.json | 24 +++ site/dpp-landing/src/lib/regulations.ts | 168 ++++++++++++++++++ .../src/pages/product-groups/[key].astro | 39 +++- .../src/pages/product-groups/index.astro | 7 +- .../src/pages/regulations/[id].astro | 136 ++++++++++++++ .../src/pages/regulations/index.astro | 78 ++++++++ 25 files changed, 928 insertions(+), 169 deletions(-) create mode 100644 site/dpp-landing/core-source.json delete mode 100644 site/dpp-landing/product-groups-source.json create mode 100644 site/dpp-landing/scripts/sync-from-core.mjs delete mode 100644 site/dpp-landing/scripts/sync-product-groups.mjs create mode 100644 site/dpp-landing/src/components/ActCard.astro create mode 100644 site/dpp-landing/src/components/BasisMark.astro create mode 100644 site/dpp-landing/src/data/instruments/battery-reg-2023-1542.json create mode 100644 site/dpp-landing/src/data/instruments/cpr-2024-3110.json create mode 100644 site/dpp-landing/src/data/instruments/detergents-2026-405.json create mode 100644 site/dpp-landing/src/data/instruments/ecodesign-energy-labelling-mobile.json create mode 100644 site/dpp-landing/src/data/instruments/elv-2026-1738.json create mode 100644 site/dpp-landing/src/data/instruments/espr-horizontal-eee-recyclability.json create mode 100644 site/dpp-landing/src/data/instruments/espr-horizontal-repairability.json create mode 100644 site/dpp-landing/src/data/instruments/espr.json create mode 100644 site/dpp-landing/src/data/instruments/ppwr-2025-40.json create mode 100644 site/dpp-landing/src/data/instruments/toy-safety-2025-2509.json create mode 100644 site/dpp-landing/src/data/instruments/unsold-goods-derogations-2026-296.json create mode 100644 site/dpp-landing/src/data/instruments/unsold-goods-format-2026-2.json create mode 100644 site/dpp-landing/src/lib/regulations.ts create mode 100644 site/dpp-landing/src/pages/regulations/[id].astro create mode 100644 site/dpp-landing/src/pages/regulations/index.astro diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 689cdf1..4d06760 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,12 +80,12 @@ jobs: - name: Check inline spacing run: pnpm run check:spacing - # The product-group pages are generated from manifests vendored out of - # dpp-core. This fails if a vendored copy no longer matches the commit - # recorded in product-groups-source.json, so a page cannot claim a schema - # version the software does not serve. - - name: Check the vendored product groups against core - run: pnpm --filter dpp-landing run check:product-groups + # The product-group and regulation pages are generated from data vendored + # out of dpp-core. This fails if a vendored copy no longer matches the + # commit recorded in core-source.json, so a page cannot describe a product + # group or a legal act differently from the library. + - name: Check the vendored product groups and regulations against core + run: pnpm --filter dpp-landing run check:core env: DPP_CORE_DIR: ${{ github.workspace }}/.dpp-core diff --git a/site/dpp-landing/core-source.json b/site/dpp-landing/core-source.json new file mode 100644 index 0000000..3d5beff --- /dev/null +++ b/site/dpp-landing/core-source.json @@ -0,0 +1,17 @@ +{ + "_comment": "Provenance for everything this site vendors from dpp-core. One commit backs every source, so the product groups and the regulations on the site can never describe two different states of the library. `pnpm run sync:core` copies each source at this commit; `pnpm run check:core` fails the build when a vendored copy differs from it. Pinning to a commit rather than to core's main keeps this repository's CI deterministic: an unrelated merge in core cannot turn a pull request here red, and a change that must land in both repositories cannot deadlock on which merges first. Bumping the commit is a deliberate, reviewable line in a diff.", + "repository": "odal-node/dpp-core", + "commit": "81f70871481e97d50a584c4a8d6c32aefd616b20", + "sources": { + "product-groups": { + "path": "crates/dpp-domain/product-groups", + "dest": "src/data/product-groups", + "_note": "What the software models: identity, schema versions, categories, the disclosure map. Carries no law." + }, + "instruments": { + "path": "crates/dpp-domain/instruments", + "dest": "src/data/instruments", + "_note": "The legal acts, as typed records: CELEX id, status, passport obligation and date with its basis, and the product groups each act reaches. The only source of any regulatory date on the /regulations pages." + } + } +} diff --git a/site/dpp-landing/package.json b/site/dpp-landing/package.json index c4911ee..62cd6b0 100644 --- a/site/dpp-landing/package.json +++ b/site/dpp-landing/package.json @@ -10,8 +10,8 @@ "build": "astro build", "preview": "astro preview", "check": "astro check", - "sync:product-groups": "node scripts/sync-product-groups.mjs", - "check:product-groups": "node scripts/sync-product-groups.mjs --check" + "sync:core": "node scripts/sync-from-core.mjs", + "check:core": "node scripts/sync-from-core.mjs --check" }, "dependencies": { "@astrojs/sitemap": "^3", diff --git a/site/dpp-landing/product-groups-source.json b/site/dpp-landing/product-groups-source.json deleted file mode 100644 index 2925251..0000000 --- a/site/dpp-landing/product-groups-source.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "_comment": "Provenance for src/data/product-groups/*.json, vendored from dpp-core. The commit is what `pnpm run sync:product-groups` last copied from, and what `check:product-groups` verifies the vendored copies against. Same reasoning as the docs site's openapi-source.json: pinning to a commit rather than to core's main keeps this repo's CI deterministic — an unrelated merge in core cannot turn a pull request here red, and a coordinated change across both repos can land without deadlocking on which merges first. Bumping the pin is a deliberate, reviewable line in a diff.", - "_path_comment": "The product-group manifests carry NO law — act numbers, dates and passport obligations live in the instrument manifests, which are deliberately not vendored here. These files carry identity, schema versions, product categories, the disclosure map and the plugin binding. A page built from them can therefore state what the software models without making a regulatory claim it would have to keep current.", - "repository": "odal-node/dpp-core", - "path": "crates/dpp-domain/product-groups", - "commit": "81f70871481e97d50a584c4a8d6c32aefd616b20" -} diff --git a/site/dpp-landing/scripts/sync-from-core.mjs b/site/dpp-landing/scripts/sync-from-core.mjs new file mode 100644 index 0000000..65f1096 --- /dev/null +++ b/site/dpp-landing/scripts/sync-from-core.mjs @@ -0,0 +1,110 @@ +// Vendor data from the sibling dpp-core repo, at one pinned commit. +// +// The copies under src/data/ are committed so a build works without dpp-core +// checked out alongside. That convenience is also the hazard: a vendored file +// drifts silently, and the drift is published. +// +// pnpm run sync:core copy every source at the pinned commit +// pnpm run check:core compare only, fail on drift or an unreadable source +// +// The sources and the commit live in core-source.json. Both modes exit +// non-zero when a source cannot be read: a sync that did not sync is a +// failure, not a notice. +// +// This mirrors site/dpp-docs/scripts/sync-openapi.mjs deliberately, including +// why it compares against a pinned commit rather than core's main branch: +// comparing against another repository's moving state makes this repository's +// CI depend on it, so an unrelated merge there turns pull requests red here, +// and a correction that must land in both deadlocks on which merges first. +import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; +import { execFileSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; + +const root = fileURLToPath(new URL('..', import.meta.url)); +const coreDir = + process.env.DPP_CORE_DIR ?? fileURLToPath(new URL('../../../../dpp-core', import.meta.url)); +const pinPath = `${root}core-source.json`; +const pin = JSON.parse(readFileSync(pinPath, 'utf8')); +const checkOnly = process.argv.includes('--check'); + +// Git normalises to LF on commit, but a Windows working copy is CRLF. Compare +// content, not line terminators, or the check fails on every developer machine. +const normalise = (text) => text.replace(/\r\n/g, '\n'); + +function fail(...lines) { + for (const l of lines) console.error(l); + process.exit(1); +} + +const git = (args) => + execFileSync('git', args, { cwd: coreDir, encoding: 'utf8', maxBuffer: 32 * 1024 * 1024 }); + +if (!existsSync(coreDir)) { + fail(`Cannot read ${coreDir}.`, `Check out dpp-core beside this repository, or set DPP_CORE_DIR.`); +} + +const sources = Object.entries(pin.sources ?? {}); +// 🚨 A loop over an empty list succeeds. A config with no sources must not +// report success; that is the vacuous pass this project has found in other gates. +if (!sources.length) fail(`core-source.json names no sources. Nothing was ${checkOnly ? 'checked' : 'synced'}.`); + +const drifted = []; +const report = []; + +for (const [name, { path, dest }] of sources) { + // The file list comes from the PIN, not the working tree: listing the working + // tree would let a file added upstream appear here without the pin moving, + // which is the drift this script exists to prevent. + let files; + try { + files = git(['ls-tree', '--name-only', `${pin.commit}:${path}`]) + .split('\n') + .map((n) => n.trim()) + .filter((n) => n.endsWith('.json')) + .sort(); + } catch { + fail( + `Cannot list ${path} at commit ${pin.commit} in ${coreDir}.`, + `Fetch dpp-core so the pinned commit is present, or correct "commit" in core-source.json.`, + ); + } + if (!files.length) fail(`No JSON files at ${path} in commit ${pin.commit} (source "${name}").`); + + const destDir = `${root}${dest}`; + if (!checkOnly) mkdirSync(destDir, { recursive: true }); + + for (const file of files) { + const source = git(['show', `${pin.commit}:${path}/${file}`]); + const target = `${destDir}/${file}`; + if (!checkOnly) { + writeFileSync(target, source); + } else if (!existsSync(target)) { + drifted.push(`${name}/${file} (not vendored)`); + } else if (normalise(readFileSync(target, 'utf8')) !== normalise(source)) { + drifted.push(`${name}/${file}`); + } + } + + // A vendored file with no counterpart upstream is drift too, and in the + // direction a loop over the source list cannot see. + if (checkOnly && existsSync(destDir)) { + for (const extra of readdirSync(destDir).filter((n) => n.endsWith('.json') && !files.includes(n))) { + drifted.push(`${name}/${extra} (vendored, absent upstream)`); + } + } + report.push(`${files.length} ${name}`); +} + +if (checkOnly && drifted.length) { + fail( + `Vendored files differ from ${pin.repository} at ${pin.commit}:`, + ...drifted.map((d) => ` ${d}`), + ``, + `Run: pnpm run sync:core`, + `If core has moved on purpose, bump "commit" in core-source.json first.`, + ); +} + +console.log( + `${checkOnly ? 'OK' : 'Vendored'}: ${report.join(', ')} ${checkOnly ? 'match' : 'from'} ${pin.repository} at ${pin.commit.slice(0, 8)}.`, +); diff --git a/site/dpp-landing/scripts/sync-product-groups.mjs b/site/dpp-landing/scripts/sync-product-groups.mjs deleted file mode 100644 index 5118d95..0000000 --- a/site/dpp-landing/scripts/sync-product-groups.mjs +++ /dev/null @@ -1,147 +0,0 @@ -// Vendor the product-group manifests from the sibling dpp-core repo. -// -// The copies under src/data/product-groups/ are committed so a build works -// without dpp-core checked out alongside. That convenience is also the hazard: -// a vendored file drifts silently, and the drift is published. -// -// pnpm run sync:product-groups copy core -> src/data/, record the commit -// pnpm run check:product-groups compare only, fail on drift or absent source -// -// Both modes exit non-zero when the source cannot be read. A sync that did not -// sync is a failure, not a notice. -// -// This mirrors site/dpp-docs/scripts/sync-openapi.mjs deliberately, including -// why it compares against a pinned commit rather than core's main branch: -// comparing against another repository's moving state makes this repository's -// CI depend on it, so an unrelated merge there turns pull requests red here, -// and a correction that must land in both deadlocks on which merges first. -// -// WHAT IS VENDORED, AND WHAT IS NOT -// -// Product-group manifests only. They carry no law — act numbers, dates and -// passport obligations live in the instrument manifests, which stay in core. A -// page built from these can say what the software models without making a -// regulatory claim this repository would then have to keep current. -import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; -import { execFileSync } from 'node:child_process'; -import { fileURLToPath } from 'node:url'; - -const root = fileURLToPath(new URL('..', import.meta.url)); -const coreDir = - process.env.DPP_CORE_DIR ?? fileURLToPath(new URL('../../../../dpp-core', import.meta.url)); -const pinPath = `${root}product-groups-source.json`; -const destDir = `${root}src/data/product-groups`; - -const pin = JSON.parse(readFileSync(pinPath, 'utf8')); - -const checkOnly = process.argv.includes('--check'); - -// Git normalises to LF on commit, but a Windows working copy is CRLF. Compare -// content, not line terminators, or the check fails on every developer machine. -const normalise = (text) => text.replace(/\r\n/g, '\n'); - -function fail(...lines) { - for (const l of lines) console.error(l); - process.exit(1); -} - -/** Read one manifest out of core's git history at the pinned commit. */ -function atPin(name) { - try { - return execFileSync('git', ['show', `${pin.commit}:${pin.path}/${name}`], { - cwd: coreDir, - encoding: 'utf8', - maxBuffer: 32 * 1024 * 1024, - }); - } catch { - return null; - } -} - -if (!existsSync(coreDir)) { - fail( - `Cannot read ${coreDir}.`, - `Check out dpp-core beside this repository, or set DPP_CORE_DIR.`, - ); -} - -// The set of manifests is taken from the PIN, not from whatever happens to be -// on disk now. Listing the working tree would let a manifest added upstream -// appear here without the pin moving, which is the drift this file exists to -// prevent. -let names; -try { - names = execFileSync('git', ['ls-tree', '--name-only', `${pin.commit}:${pin.path}`], { - cwd: coreDir, - encoding: 'utf8', - }) - .split('\n') - .map((n) => n.trim()) - .filter((n) => n.endsWith('.json')) - .sort(); -} catch { - fail( - `Cannot list ${pin.path} at commit ${pin.commit} in ${coreDir}.`, - `Fetch dpp-core so the pinned commit is present, or correct the "commit" field in product-groups-source.json.`, - ); -} - -// 🚨 A loop over an empty list succeeds. A sync that vendored nothing, or a -// check that compared nothing, must not report success — that is the same -// vacuous pass this project has found in four other gates. -if (!names.length) { - fail(`No manifests found at ${pin.path} in commit ${pin.commit}. Nothing was ${checkOnly ? 'checked' : 'synced'}.`); -} - -if (!checkOnly) mkdirSync(destDir, { recursive: true }); - -const drifted = []; -const missing = []; - -for (const name of names) { - const source = atPin(name); - if (source === null) { - missing.push(name); - continue; - } - const dest = `${destDir}/${name}`; - if (checkOnly) { - if (!existsSync(dest)) { - drifted.push(`${name} (not vendored)`); - continue; - } - if (normalise(readFileSync(dest, 'utf8')) !== normalise(source)) drifted.push(name); - } else { - writeFileSync(dest, source); - } -} - -if (missing.length) { - fail(`Missing at the pinned commit: ${missing.join(', ')}`); -} - -// A vendored file with no counterpart upstream is drift too, and in the -// direction a comparison loop over the source list cannot see. -if (checkOnly && existsSync(destDir)) { - const extra = readdirSync(destDir) - .filter((n) => n.endsWith('.json')) - .filter((n) => !names.includes(n)); - for (const n of extra) drifted.push(`${n} (vendored, absent upstream)`); -} - -if (checkOnly) { - if (drifted.length) { - fail( - `Vendored product-group manifests differ from ${pin.repository} at ${pin.commit}:`, - ...drifted.map((d) => ` ${d}`), - ``, - `Run: pnpm run sync:product-groups`, - `If core has moved on purpose, bump "commit" in product-groups-source.json first.`, - ); - } - console.log(`OK: ${names.length} product-group manifest(s) match ${pin.repository} at ${pin.commit.slice(0, 8)}.`); -} else { - const stamp = { ...pin }; - writeFileSync(pinPath, `${JSON.stringify(stamp, null, 2)}\n`); - console.log(`Vendored ${names.length} product-group manifest(s) from ${pin.commit.slice(0, 8)}.`); -} diff --git a/site/dpp-landing/src/components/ActCard.astro b/site/dpp-landing/src/components/ActCard.astro new file mode 100644 index 0000000..c28b54d --- /dev/null +++ b/site/dpp-landing/src/components/ActCard.astro @@ -0,0 +1,63 @@ +--- +// ActCard.astro — one legal act as a card on /regulations. Everything shown is +// read from the vendored instrument record through lib/regulations.ts. +import { + type Instrument, + displayTitle, + kindLabel, + statusLabel, + passportView, + basisLabel, + groupRef, +} from "../lib/regulations"; +import BasisMark from "./BasisMark.astro"; + +interface Props { + act: Instrument; +} +const { act } = Astro.props; +const passport = passportView(act); +const reached = (act.productGroups ?? []).map((b) => groupRef(b.productGroup)); + +const tone: Record = { + required: "bg-regulatory-bg text-regulatory-text ring-regulatory-border", + "not-yet-operable": "bg-neutral-100 text-neutral-700 ring-neutral-200", + "per-act": "bg-primary-50 text-primary-800 ring-primary-100", + expected: "bg-neutral-100 text-neutral-600 ring-neutral-200", + none: "bg-neutral-100 text-neutral-600 ring-neutral-200", + elsewhere: "bg-neutral-100 text-neutral-600 ring-neutral-200", +}; +--- + +
      +

      + {kindLabel(act)} + + {statusLabel(act)} +

      +

      + + {displayTitle(act)} + +

      + +
      + + {passport.label} + + {passport.date && } +
      +

      {passport.detail}

      + + {reached.length > 0 && ( +
        + {reached.map((g) => ( +
      • {g.name}
      • + ))} +
      + )} + +

      + Details +

      +
      diff --git a/site/dpp-landing/src/components/BasisMark.astro b/site/dpp-landing/src/components/BasisMark.astro new file mode 100644 index 0000000..7c600da --- /dev/null +++ b/site/dpp-landing/src/components/BasisMark.astro @@ -0,0 +1,24 @@ +--- +// BasisMark.astro — says where a date comes from. "From the text" is quoted +// from the Official Journal; "Our assumption" is a reading with no text that +// states it. The two must never look alike. +interface Props { + basis: "sourced" | "assumed"; + label: string; +} +const { basis, label } = Astro.props; +--- + + + {basis === "sourced" ? ( + + ) : ( + + )} + {label} + diff --git a/site/dpp-landing/src/data/instruments/battery-reg-2023-1542.json b/site/dpp-landing/src/data/instruments/battery-reg-2023-1542.json new file mode 100644 index 0000000..ac94b95 --- /dev/null +++ b/site/dpp-landing/src/data/instruments/battery-reg-2023-1542.json @@ -0,0 +1,27 @@ +{ + "id": "battery-reg-2023-1542", + "title": "Batteries Regulation (EU) 2023/1542", + "celex": "32023R1542", + "kind": "direct", + "status": "adopted", + "currency": { "state": "consolidated", "asOf": "02023R1542-20260813", "checkedOn": "2026-09-11" }, + "passport": { + "obligation": "required", + "from": { "date": "2027-02-18", "basis": "sourced" } + }, + "retentionYears": 10, + "retentionYearsBasis": "assumed", + "productGroups": [ + { + "productGroup": "battery", + "status": "in_force", + "legalBasis": [ + "Regulation (EU) 2023/1542 Art. 77(1)", + "Regulation (EU) 2023/1542 Art. 9", + "Regulation (EU) 2025/1561" + ], + "notes": "In force and determinable today even though the passport is future-dated: the Art. 9 mercury and cadmium prohibitions have applied since 2008, carried forward from Directive 2006/66/EC. Scope of the passport obligation is EV, LMT and industrial batteries over 2 kWh. Reg. (EU) 2025/1561 moved the Art. 48(1) due-diligence trigger to 2027-08-18 and the Art. 48(5) Commission guidelines deadline to 2026-07-26. Major obligations remain future-dated: Art. 10(2) minimum performance values from 2027-08-18, Art. 8(1) recycled-content declaration from 2028-08-18, Art. 8(2) minimum shares from 2031-08-18. retentionYears is assumed, not sourced: the Regulation's 10-year figures are documentation retention — technical documentation, EU declaration of conformity, audit reports — not passport availability, which sits with Art. 77 and the unadopted Art. 77(9) implementing act." + } + ], + "notes": "The clearest case for separating instrument status from binding status, and passport date from determinability: the act is adopted, its obligations bind now, and its passport is owed from 2027-02-18. retentionYears is assumed, not sourced — the Regulation's 10-year figures are documentation retention (technical documentation, EU declaration of conformity, audit reports), not passport availability, which sits with Art. 77 and the unadopted Art. 77(9) implementing act. Granularity is deliberately unset: the EU registry treats batteries as item-level, but that is the registry's operational position from its user guide, not a level fixed by an adopted act." +} diff --git a/site/dpp-landing/src/data/instruments/cpr-2024-3110.json b/site/dpp-landing/src/data/instruments/cpr-2024-3110.json new file mode 100644 index 0000000..d5b2803 --- /dev/null +++ b/site/dpp-landing/src/data/instruments/cpr-2024-3110.json @@ -0,0 +1,23 @@ +{ + "id": "cpr-2024-3110", + "title": "Construction Products Regulation (EU) 2024/3110", + "celex": "32024R3110", + "kind": "direct", + "status": "adopted", + "currency": { "state": "inForce", "checkedOn": "2026-09-11" }, + "passport": { + "obligation": "required", + "from": { "date": "2026-01-08", "basis": "sourced" } + }, + "productGroups": [ + { + "productGroup": "construction", + "status": "provisional", + "legalBasis": ["Regulation (EU) 2024/3110 Art. 75"], + "retentionYears": 10, + "retentionYearsBasis": "sourced", + "notes": "retentionYears is sourced: the Regulation imposes 10 years on the economic operator. In force since 2025-01-07 and applicable in stages from 2026-01-08, but the per-product duty depends on harmonised technical specifications plus delegated and implementing acts, so nothing is determinable. The DPP system is expected to be implemented in principle by end 2027 with the obligation applying some 18 months later — both figures secondary-sourced, which is why the instrument carries no passport date at all rather than an invented one. WRONG AXIS, to be re-homed: the CPR uses 'product group' zero times and defines product family, then product category, then product type, with its own Annex VII family list. This binding keeps the existing catalog key so nothing breaks; re-homing it is a later step and needs CPR Annex VII read first." + } + ], + "notes": "A real passport instrument, unlike the electronics pair — Art. 75 creates a construction DPP outright. Its first expected product families are rebar, cement, doors and windows with their hardware, and thermal insulation, of which our existing product-category list carries only cement. The 2026-01-08 date is Art. 96: the Regulation applies from that day, and Arts. 75 and 76 — the construction DPP system and the passport itself — are not among the articles Art. 96 excepts. Two cautions. It is not 8 January 2027: that is Art. 96's date for Art. 92, which is Penalties. And the obligation is not yet operable, because Art. 75(1) requires the Commission to establish the DPP system by delegated act and none has been adopted, so the article applies while the system it depends on does not exist." +} diff --git a/site/dpp-landing/src/data/instruments/detergents-2026-405.json b/site/dpp-landing/src/data/instruments/detergents-2026-405.json new file mode 100644 index 0000000..d7c875c --- /dev/null +++ b/site/dpp-landing/src/data/instruments/detergents-2026-405.json @@ -0,0 +1,23 @@ +{ + "id": "detergents-2026-405", + "title": "Detergents Regulation (EU) 2026/405", + "celex": "32026R0405", + "kind": "direct", + "status": "adopted", + "currency": { "state": "inForce", "checkedOn": "2026-09-11" }, + "passport": { + "obligation": "required", + "from": { "date": "2029-09-23", "basis": "sourced" } + }, + "productGroups": [ + { + "productGroup": "detergent", + "status": "provisional", + "legalBasis": ["Regulation (EU) 2026/405"], + "retentionYears": 10, + "retentionYearsBasis": "sourced", + "notes": "Adopted 2026-02-11, published in the OJ 2026-03-02. Nothing determinable yet. retentionYears is sourced: the Regulation requires 10 years after placing on the market, including in cases of insolvency." + } + ], + "notes": "Two things worth keeping straight. First, the passport date is marked assumed rather than sourced: 2029-09-23 comes from a secondary source and has not been read out of the primary text (CELEX 32026R0405), and until it has it must not be presented as a legal deadline. Second, detergents is a direct instrument, not an ESPR delegated act — it appears in the ESPR Art. 18 statutory priority list but was NOT selected into the 2025-2030 working plan, so no ESPR act is coming for it and this Regulation is the whole of its passport obligation. Consequential difference from every other instrument here: under this Regulation the passport replaces the EU declaration of conformity as the primary means of demonstrating compliance, so a defective passport costs an operator its conformity route rather than one disclosure among several." +} diff --git a/site/dpp-landing/src/data/instruments/ecodesign-energy-labelling-mobile.json b/site/dpp-landing/src/data/instruments/ecodesign-energy-labelling-mobile.json new file mode 100644 index 0000000..2fe991c --- /dev/null +++ b/site/dpp-landing/src/data/instruments/ecodesign-energy-labelling-mobile.json @@ -0,0 +1,27 @@ +{ + "id": "ecodesign-energy-labelling-mobile", + "title": "Ecodesign and energy labelling for smartphones, mobile phones and tablets — Regulations (EU) 2023/1670 and 2023/1669", + "celex": "32023R1670", + "kind": "adjacent", + "status": "adopted", + "currency": { "state": "consolidated", "asOf": "02023R1670-20250620", "checkedOn": "2026-09-11" }, + "passport": { + "obligation": "displacedBy", + "system": "EPREL", + "basis": "ESPR Art. 9(4)(b)" + }, + "productGroups": [ + { + "productGroup": "electronics", + "status": "in_force", + "legalBasis": [ + "Regulation (EU) 2023/1670 Art. 1(1)", + "Regulation (EU) 2023/1669" + ], + "retentionYears": 10, + "retentionYearsBasis": "assumed", + "notes": "retentionYears is assumed: neither Regulation has been checked for a retention figure, and since neither imposes a passport there may be no such figure to find. Binding since 2025-06-20 for smartphones, other mobile phones, cordless phones and slate tablets — repairability class A-E, durability, seven-year spare parts, OS updates. Those obligations are real and determinable. What does not exist is a passport obligation: neither Regulation contains the phrase 'digital product passport', and their information mechanism is EPREL." + } + ], + "notes": "The record that the previous model could not hold. Its predecessor entry carried status in_force together with dppAppliesFrom 2025-06-20 — but 2025-06-20 is Reg. 2023/1670's *ecodesign* application date under Directive 2009/125/EC, relabelled as a passport date, and no passport obligation exists to date. Because in_force was the gate for binding determinations, a node could assert compliance against an obligation that is not there. Here the two facts are recorded separately and both stay true: the binding is in_force, and the passport is displaced. ESPR Art. 9(4)(b) lets the Commission exempt a product group where other Union law provides an equivalent digital system, and the 2025-2030 working plan states the position in terms: every product covered by ecodesign measures will have a passport 'except if there is an alternative digital system providing equivalent information, for example the EPREL database'. EPREL is the alternative to a passport, not a form of one, and we do not model it." +} diff --git a/site/dpp-landing/src/data/instruments/elv-2026-1738.json b/site/dpp-landing/src/data/instruments/elv-2026-1738.json new file mode 100644 index 0000000..5648fb7 --- /dev/null +++ b/site/dpp-landing/src/data/instruments/elv-2026-1738.json @@ -0,0 +1,14 @@ +{ + "id": "elv-2026-1738", + "title": "End-of-Life Vehicles Regulation (EU) 2026/1738", + "celex": "32026R1738", + "kind": "direct", + "status": "adopted", + "currency": { "state": "inForce", "checkedOn": "2026-09-11" }, + "passport": { + "obligation": "required", + "from": { "date": "2032-09-01", "basis": "sourced" } + }, + "productGroups": [], + "notes": "Recorded with no bindings, deliberately: the act exists and creates a Digital Circularity Vehicle Passport, and we hold no vehicle product group to bind it to. An instrument that reaches nothing we model is still worth holding — it is how the catalog stops the act being rediscovered as new, and it is the shape a horizontal act takes before its groups are known. Full requirements cover passenger cars and light commercial vans, with a reduced end-of-life-treatment set for heavy-duty vehicles, motorcycles and special-purpose vehicles. The passport date is now sourced, read from the OJ text: Art. 13(1) — 'From 1 September 2032, each vehicle placed on the market shall have a Digital Circularity Vehicle Passport'. An earlier secondary source gave 'two years after entry into force', i.e. around August 2028; that is the Regulation's general application date under Art. 59(2) and not the passport date, which is four years later. The manifest previously recorded no date at all rather than an unsourced one, which was the right call. Note also that 'digital product passport' appears zero times in this Regulation — the Digital Circularity Vehicle Passport is a differently-named instrument, not an ESPR DPP, though Art. 13(1) requires it to be aligned and interoperable with the Art. 77 battery passport and with passports established under Regulation (EU) 2024/1781. This Regulation amends four acts, not two: its title names Regulations (EU) No 168/2013, (EU) 2018/858, (EU) 2019/1020 and (EU) 2023/1542 — Art. 53 amends the Batteries Regulation (applies 13 Aug 2026), Art. 54 amends 2019/1020 (applies 1 Sep 2028), Art. 55 amends 2018/858 and 168/2013 (applies 1 Sep 2032). Scope is staged if a vehicle group is ever modelled: Art. 2(1) takes M1 and N1 first, special-purpose M1/N1 from 1 Sep 2029, and M2, M3, N2, N3, O and L-category from 1 Sep 2031. Note the relationship with ESPR: Art. 1(2)(h) excludes vehicles only in respect of aspects already regulated by sector-specific acts, so the exclusion is partial and aspect-scoped, and this Regulation is the automotive passport route." +} diff --git a/site/dpp-landing/src/data/instruments/espr-horizontal-eee-recyclability.json b/site/dpp-landing/src/data/instruments/espr-horizontal-eee-recyclability.json new file mode 100644 index 0000000..59c95ed --- /dev/null +++ b/site/dpp-landing/src/data/instruments/espr-horizontal-eee-recyclability.json @@ -0,0 +1,17 @@ +{ + "id": "espr-horizontal-eee-recyclability", + "title": "ESPR horizontal requirements on recycled content and recyclability of electrical and electronic equipment", + "kind": "delegated", + "status": "anticipated", + "parent": "espr", + "passport": { "obligation": "required" }, + "productGroups": [ + { + "productGroup": "lmt", + "status": "provisional", + "legalBasis": [], + "notes": "Light means of transport — monowheels, e-scooters, e-bikes and e-mopeds up to L2e under Regulation (EU) 168/2013. Source is the JRC priorities study's recyclability coverage table, which is a preparatory technical study, NOT law and not a Commission position by its own disclaimer. Recorded because it is the case this catalog exists to represent, not because it binds anything: the study states that the horizontal requirements' product sets include product groups that were never shortlisted as candidates for priority action, naming LMT. There is no product-group manifest for this key and there may never be one." + } + ], + "notes": "The second horizontal act from the 2025-2030 working plan, anticipated with no adoption year stated. It carries the binding that proves the model: a product group reached by an act while having no manifest, no schema and no place on any priority list. Nothing derives from a product group here — the reach is recorded on the act, which is the only place it can be written down. Every value is provisional or anticipated and none of it may be presented as sourced." +} diff --git a/site/dpp-landing/src/data/instruments/espr-horizontal-repairability.json b/site/dpp-landing/src/data/instruments/espr-horizontal-repairability.json new file mode 100644 index 0000000..5e6e279 --- /dev/null +++ b/site/dpp-landing/src/data/instruments/espr-horizontal-repairability.json @@ -0,0 +1,10 @@ +{ + "id": "espr-horizontal-repairability", + "title": "ESPR horizontal requirements on repairability, including a repairability score", + "kind": "delegated", + "status": "anticipated", + "parent": "espr", + "passport": { "obligation": "required" }, + "productGroups": [], + "notes": "One of the two horizontal acts the 2025-2030 working plan selects, with indicative adoption years of 2027 and 2029. Anticipated, so no CELEX and no text: nothing derived from it may be presented as sourced. Recorded with NO product groups on purpose. The working plan says its scope 'could include products such as consumer electronics and small household appliances' and is 'to be refined during the preparatory study' — that is a candidate scope, and binding a product group to it would be exactly the class of invention this catalog exists to prevent. The passport obligation follows from ESPR Art. 9(1) applying to products covered by ecodesign measures rather than from any text of this act; it is undated, and every future binding will be provisional until the act is adopted." +} diff --git a/site/dpp-landing/src/data/instruments/espr.json b/site/dpp-landing/src/data/instruments/espr.json new file mode 100644 index 0000000..59de9b6 --- /dev/null +++ b/site/dpp-landing/src/data/instruments/espr.json @@ -0,0 +1,61 @@ +{ + "id": "espr", + "title": "Ecodesign for Sustainable Products Regulation (EU) 2024/1781", + "celex": "32024R1781", + "kind": "framework", + "status": "adopted", + "currency": { "state": "consolidated", "asOf": "02024R1781-20240628", "checkedOn": "2026-09-11" }, + "passport": { "obligation": "required" }, + "retentionYears": 10, + "retentionYearsBasis": "assumed", + "productGroups": [ + { + "productGroup": "textile", + "status": "provisional", + "legalBasis": ["Regulation (EU) 2024/1781 Art. 18"], + "notes": "Art. 18 statutory priority list ('textiles, in particular garments and footwear'); selected as a final product in the 2025-2030 working plan with an indicative delegated-act adoption year of 2027. No delegated act exists, so nothing is determinable and no CN range is knowable." + }, + { + "productGroup": "furniture", + "status": "provisional", + "legalBasis": ["Regulation (EU) 2024/1781 Art. 18"], + "notes": "Art. 18 lists 'furniture, including mattresses'; the working plan then selects Furniture and Mattresses separately, with different rankings and indicative adoption years of 2028 and 2029 respectively. Mattresses is bound separately below and must not be carried as a category here." + }, + { + "productGroup": "mattress", + "status": "provisional", + "legalBasis": ["Regulation (EU) 2024/1781 Art. 18"], + "notes": "Selected in the 2025-2030 working plan as a final product distinct from Furniture, ranked fourth among final products, indicative adoption 2029. The Regulation groups it under furniture and the working plan splits it out — which is why product group cannot be a fixed taxonomy: the grouping changed between the framework and its own working plan." + }, + { + "productGroup": "tyre", + "status": "provisional", + "legalBasis": ["Regulation (EU) 2024/1781 Art. 18"], + "notes": "Art. 18 priority list; working plan final product, indicative adoption 2027. Already regulated in part by the Tyre Labelling Regulation (EU) 2020/740, which is a labelling instrument and not a passport one." + }, + { + "productGroup": "steel", + "status": "provisional", + "legalBasis": ["Regulation (EU) 2024/1781 Art. 18"], + "notes": "Art. 18 'iron and steel'; working plan intermediate product, ranked first, indicative adoption 2026. The JRC priorities study indicates a scope of CN chapters 72 and 73 — an indication from a preparatory study, not a legal scope: ESPR Art. 8(a) puts the commodity codes in the delegated act, and none exists." + }, + { + "productGroup": "aluminium", + "status": "provisional", + "legalBasis": ["Regulation (EU) 2024/1781 Art. 18"], + "notes": "Art. 18 priority list; working plan intermediate product, indicative adoption 2027. Intermediate products may have their delegated acts reach final products containing them, which is one of the ways one product accumulates several instruments." + }, + { + "productGroup": "unsold-goods", + "status": "in_force", + "legalBasis": [ + "Regulation (EU) 2024/1781 Art. 24", + "Regulation (EU) 2024/1781 Art. 25", + "Regulation (EU) 2024/1781 Annex VII" + ], + "passport": { "obligation": "notRequired" }, + "notes": "Binding today and determinable: the Art. 25(1) destruction prohibition for Annex VII goods has applied since 2026-07-19, and the Art. 24 disclosure duty is live for large operators, with medium-sized operators brought in from 2030-07-19 and micro and small enterprises exempt. There is no passport anywhere in Arts. 24-25 — the subject is an operator over a financial year, disclosed on its own website, not a product placed on the market. This is why the obligation binds while requiring no passport: the two questions are separate and this binding answers them differently. The 2026-07-19 date is the destruction ban's, and it must never be recorded as a passport date." + } + ], + "notes": "The framework itself. Art. 9(1) requires 'a digital product passport ... in accordance with the applicable delegated acts' — singular passport, plural acts — so the obligation is real but undated until a delegated act fixes it. Art. 5(7) lets one act cover many product groups and lets a group-specific act supplement a horizontal one, and no precedence rule exists anywhere in the Regulation: overlapping acts accumulate. retentionYears is assumed, not sourced, and applies to every group below: Art. 9(2)(i) sets no figure — it requires availability for at least the expected lifetime of the product, delegated per product group, and no delegated act has been adopted for any of them. As of 2026-08-24 that is true of every ESPR product group without exception." +} diff --git a/site/dpp-landing/src/data/instruments/ppwr-2025-40.json b/site/dpp-landing/src/data/instruments/ppwr-2025-40.json new file mode 100644 index 0000000..810b485 --- /dev/null +++ b/site/dpp-landing/src/data/instruments/ppwr-2025-40.json @@ -0,0 +1,11 @@ +{ + "id": "ppwr-2025-40", + "title": "Packaging and Packaging Waste Regulation (EU) 2025/40", + "celex": "32025R0040", + "kind": "adjacent", + "status": "adopted", + "currency": { "state": "inForce", "checkedOn": "2026-09-11" }, + "passport": { "obligation": "notRequired" }, + "productGroups": [], + "notes": "Held as a record specifically so it is not mistaken for a passport instrument again. The word 'passport' appears twice in the entire Regulation and both occurrences point outward: where the packaged product is covered by Regulation (EU) 2024/1781 or other Union law requiring a digital product passport, THAT passport should also carry the information required here. So there is no packaging passport, and a packaging product group would have nothing to dispatch on — no passport, no schema derived from law, no determination possible. What the Regulation does require is labelling, and a QR code or other standardised open data carrier on reusable packaging carrying re-use system and trip data, which is a data-carrier obligation. A trade-press claim that this created a packaging passport from 2028-08-12 was recorded once and refuted by reading the primary text. Deliberately unbound and parked pending a decision to revisit." +} diff --git a/site/dpp-landing/src/data/instruments/toy-safety-2025-2509.json b/site/dpp-landing/src/data/instruments/toy-safety-2025-2509.json new file mode 100644 index 0000000..e75b44e --- /dev/null +++ b/site/dpp-landing/src/data/instruments/toy-safety-2025-2509.json @@ -0,0 +1,23 @@ +{ + "id": "toy-safety-2025-2509", + "title": "Toy Safety Regulation (EU) 2025/2509", + "celex": "32025R2509", + "kind": "direct", + "status": "adopted", + "currency": { "state": "inForce", "checkedOn": "2026-09-11" }, + "passport": { + "obligation": "required", + "from": { "date": "2030-08-01", "basis": "sourced" } + }, + "retentionYears": 10, + "retentionYearsBasis": "sourced", + "productGroups": [ + { + "productGroup": "toy", + "status": "provisional", + "legalBasis": ["Regulation (EU) 2025/2509"], + "notes": "retentionYears is sourced: the Regulation requires 10 years after placing on the market, including in cases of insolvency. Adopted 2025-12-12 and in force since 2026-01-01, but fully applicable only from 2030-08-01 after a 54-month transition, so nothing is bindingly determinable yet. The implementing acts defining the technical and procedural passport requirements are not published, so a conformant toy data carrier cannot be generated today." + } + ], + "notes": "A direct instrument with a firm passport date, which is rarer than it looks — most of the catalog is undated. Its identifier-sharing provision is one of the two proofs that a product bears one passport however many instruments apply: the unique product identifier, operator identifier and registration identifier 'shall be the same' as those used under other Union law." +} diff --git a/site/dpp-landing/src/data/instruments/unsold-goods-derogations-2026-296.json b/site/dpp-landing/src/data/instruments/unsold-goods-derogations-2026-296.json new file mode 100644 index 0000000..c3ea921 --- /dev/null +++ b/site/dpp-landing/src/data/instruments/unsold-goods-derogations-2026-296.json @@ -0,0 +1,25 @@ +{ + "id": "unsold-goods-derogations-2026-296", + "title": "Derogations from the prohibition of destruction of unsold consumer products — Commission Delegated Regulation (EU) 2026/296", + "celex": "32026R0296", + "kind": "delegated", + "status": "adopted", + "currency": { "state": "inForce", "checkedOn": "2026-09-11" }, + "parent": "espr", + "passport": { "obligation": "notRequired" }, + "retentionYears": 5, + "retentionYearsBasis": "sourced", + "productGroups": [ + { + "productGroup": "unsold-goods", + "status": "in_force", + "legalBasis": [ + "Commission Delegated Regulation (EU) 2026/296 Art. 2", + "Commission Delegated Regulation (EU) 2026/296 Art. 3", + "Regulation (EU) 2024/1781 Art. 25(5)" + ], + "notes": "Adopted 2026-02-09, OJ 2026-04-22. Art. 2 is a CLOSED list of ten derogations, points (a) to (j), under which an Annex VII good may lawfully be destroyed. Point (h) — offered for donation and not accepted — is subordinate: it applies 'only where none of the circumstances referred to in points (a) to (g) are applicable', which is a condition over the whole set of reasons claimed for a category and cannot be checked on one line alone. Impl. Reg. (EU) 2026/2 Annex I note (h) makes this list the reason vocabulary of the disclosure, so the two acts interlock." + } + ], + "notes": "retentionYears is sourced and is NOT a passport retention figure: Art. 3 requires the economic operator to keep per-derogation documentation for five years after destruction, in electronic form, and produce it to a competent authority within 30 days of a request. It is evidence of a lawful destruction, not availability of a product record, and must not be folded into a passport availability period. Neither this act nor its implementing sibling creates a passport." +} diff --git a/site/dpp-landing/src/data/instruments/unsold-goods-format-2026-2.json b/site/dpp-landing/src/data/instruments/unsold-goods-format-2026-2.json new file mode 100644 index 0000000..0aeed34 --- /dev/null +++ b/site/dpp-landing/src/data/instruments/unsold-goods-format-2026-2.json @@ -0,0 +1,24 @@ +{ + "id": "unsold-goods-format-2026-2", + "title": "Format for the disclosure of information on discarded unsold consumer products — Commission Implementing Regulation (EU) 2026/2", + "celex": "32026R0002", + "kind": "implementing", + "status": "adopted", + "currency": { "state": "inForce", "checkedOn": "2026-09-11" }, + "parent": "espr", + "passport": { "obligation": "notRequired" }, + "productGroups": [ + { + "productGroup": "unsold-goods", + "status": "in_force", + "legalBasis": [ + "Commission Implementing Regulation (EU) 2026/2 Art. 2(1)", + "Commission Implementing Regulation (EU) 2026/2 Art. 3", + "Commission Implementing Regulation (EU) 2026/2 Annex I", + "Regulation (EU) 2024/1781 Art. 24(3)" + ], + "notes": "Adopted 2026-02-09, OJ 2026-02-10. Creates no duty of its own — it prescribes the format of the disclosure ESPR Art. 24 already requires, which is why it is an implementing act and carries no passport obligation. Art. 2(1) binds the visual presentation and content to Annex I. Art. 3 delimits categories on the first two digits of the CN code, except for the products of its Annex II, which take four. Art. 1 scopes the duty to each financial year from the first full financial year after the date of application, disclosed within 12 months of that year's end; Art. 2(2) lets an operator publishing sustainability reporting under Directive 2013/34/EU Arts. 19a or 29a link to it instead of disclosing on its website directly. Annex I note (h) points the reason vocabulary at the derogations of the delegated act under Art. 25(5), which is Del. Reg. (EU) 2026/296 — the two acts interlock and neither is complete alone." + } + ], + "notes": "Scope worth keeping straight: this implements Art. 24's DISCLOSURE duty, which reaches discarded unsold consumer products generally, and is therefore much wider than Art. 25's destruction prohibition over the Annex VII goods. Its own Annex II runs to 45 CN headings from soap and tyres to refrigerators and toys, none of which are in ESPR Annex VII. Treating Annex VII as the scope of the disclosure drops every category outside apparel and footwear from a report required to carry them." +} diff --git a/site/dpp-landing/src/lib/regulations.ts b/site/dpp-landing/src/lib/regulations.ts new file mode 100644 index 0000000..1acc32f --- /dev/null +++ b/site/dpp-landing/src/lib/regulations.ts @@ -0,0 +1,168 @@ +// regulations.ts — the one place pages read the legal acts from. +// +// The acts are the instrument records vendored from dpp-core (see +// core-source.json). This module only *words* them for a general reader; it +// never adds a fact. Every date on /regulations comes from these records, so +// the site and the software cannot disagree about one. +// +// THE ONE RULE THIS FILE ENFORCES +// +// A passport is shown as required only when the act requires one AND at least +// one product group it reaches is binding now. That is the core catalog's own +// "live obligation" fold (dpp-domain `InstrumentCatalog`): an act can require a +// passport on paper while nothing about it can be met yet, and a page that +// printed its date alone would tell a manufacturer they are already in breach. +import { groups, unsoldGoods, categoryLabel } from "./product-groups"; + +type Basis = "sourced" | "assumed"; + +type Passport = + | { obligation: "required"; from?: { date: string; basis: Basis } } + | { obligation: "notRequired" } + | { obligation: "displacedBy"; system: string; basis?: string }; + +type Binding = { + productGroup: string; + status: "in_force" | "provisional" | string; + legalBasis?: string[]; + passport?: Passport; +}; + +export type Instrument = { + id: string; + title: string; + celex?: string; + kind: "framework" | "direct" | "delegated" | "implementing" | "adjacent" | string; + status: "adopted" | "anticipated" | string; + currency?: { state: string; asOf?: string; checkedOn?: string }; + passport: Passport; + parent?: string; + productGroups?: Binding[]; +}; + +const modules = import.meta.glob("../data/instruments/*.json", { eager: true }); +const all: Instrument[] = Object.values(modules).map((m: any) => (m.default ?? m) as Instrument); +const byId = new Map(all.map((i) => [i.id, i])); + +// ── words ──────────────────────────────────────────────────────────────────── + +/** Vendored titles are verbatim from core; the site's copy uses no em dashes. */ +export const displayTitle = (i: Instrument) => i.title.replace(/\s+—\s+/g, ": "); + +const KIND: Record = { + framework: "Framework regulation", + direct: "Regulation", + delegated: "Delegated act", + implementing: "Implementing act", + adjacent: "Related act", +}; +export const kindLabel = (i: Instrument) => KIND[i.kind] ?? "Legal act"; + +export const statusLabel = (i: Instrument) => (i.status === "anticipated" ? "Expected" : "Adopted"); + +export const basisLabel = (b: Basis) => (b === "sourced" ? "From the text" : "Our assumption"); + +const SYSTEM: Record = { + EPREL: "EPREL, the EU's energy-label database", +}; + +export const bindingLabel = (status: string) => + status === "in_force" ? "Binding now" : status === "provisional" ? "Not yet determinable" : "Tracked only"; + +export const formatDate = (iso: string) => + new Date(`${iso}T00:00:00Z`).toLocaleDateString("en-GB", { + day: "numeric", + month: "long", + year: "numeric", + timeZone: "UTC", + }); + +// ── the passport line ──────────────────────────────────────────────────────── + +export type PassportView = { + /** Short state, for a chip. */ + state: "required" | "not-yet-operable" | "per-act" | "expected" | "none" | "elsewhere"; + label: string; + /** One plain sentence expanding the label. */ + detail: string; + date?: { iso: string; text: string; basis: Basis }; +}; + +const today = new Date().toISOString().slice(0, 10); + +export function passportView(i: Instrument): PassportView { + const p = i.passport; + if (p.obligation === "notRequired") { + return { state: "none", label: "No passport", detail: "This act shapes what is recorded, but creates no passport of its own." }; + } + if (p.obligation === "displacedBy") { + return { + state: "elsewhere", + label: "Handled elsewhere", + detail: `Its product information is held in ${SYSTEM[p.system] ?? p.system} rather than in a passport.`, + }; + } + if (i.status === "anticipated") { + return { state: "expected", label: "Expected", detail: "Not adopted yet. There is no text, so there is no date." }; + } + + const live = (i.productGroups ?? []).some((b) => b.status === "in_force"); + const from = p.from; + if (!from) { + return { + state: "per-act", + label: "Set act by act", + detail: "This framework requires passports, but each product group's own act sets the date.", + }; + } + const date = { iso: from.date, text: formatDate(from.date), basis: from.basis }; + if (!live && from.date <= today) { + return { + state: "not-yet-operable", + label: "Not yet operable", + detail: `The act has applied since ${date.text}, but the rules it depends on have not been adopted, so the passport cannot be issued under it yet.`, + date, + }; + } + return { + state: "required", + label: from.date <= today ? `Required since ${date.text}` : `Required from ${date.text}`, + detail: "Products in scope need a passport from this date.", + date, + }; +} + +// ── product groups ─────────────────────────────────────────────────────────── + +const known = new Map([...groups, unsoldGoods].map((g) => [g.key, g.name])); + +/** A binding may name a group the site has no page for (the law reaches it, we model no schema). */ +export const groupRef = (key: string) => ({ + key, + name: known.get(key) ?? categoryLabel(key), + href: known.has(key) ? `/product-groups/${key}` : undefined, +}); + +// ── lists ──────────────────────────────────────────────────────────────────── + +export const eurLex = (celex: string) => `https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:${celex}`; + +export const parentOf = (i: Instrument) => (i.parent ? byId.get(i.parent) : undefined); + +/** Every act, ordered: passport-creating acts first by date, then the rest by title. */ +export const instruments: Instrument[] = [...all].sort((a, b) => { + const rank = (i: Instrument) => (i.passport.obligation === "required" ? 0 : 1); + if (rank(a) !== rank(b)) return rank(a) - rank(b); + const da = a.passport.obligation === "required" ? a.passport.from?.date ?? "9999" : "9999"; + const db = b.passport.obligation === "required" ? b.passport.from?.date ?? "9999" : "9999"; + return da.localeCompare(db) || displayTitle(a).localeCompare(displayTitle(b)); +}); + +export const creatingPassports = instruments.filter((i) => i.passport.obligation === "required"); +export const shapingPassports = instruments.filter((i) => i.passport.obligation !== "required"); + +/** The acts that reach one product group, with the binding for it. */ +export const actsFor = (group: string) => + instruments.flatMap((i) => + (i.productGroups ?? []).filter((b) => b.productGroup === group).map((binding) => ({ instrument: i, binding })), + ); diff --git a/site/dpp-landing/src/pages/product-groups/[key].astro b/site/dpp-landing/src/pages/product-groups/[key].astro index e28800a..5f58372 100644 --- a/site/dpp-landing/src/pages/product-groups/[key].astro +++ b/site/dpp-landing/src/pages/product-groups/[key].astro @@ -17,6 +17,7 @@ import Base from "../../layouts/Base.astro"; import Section from "../../components/Section.astro"; import ProductGroupIcon from "../../components/ProductGroupIcon.astro"; import { allPages, categoryLabel, fieldLabel, type Group } from "../../lib/product-groups"; +import { actsFor, displayTitle, kindLabel, bindingLabel } from "../../lib/regulations"; export function getStaticPaths() { return allPages.map((g) => ({ params: { key: g.key }, props: { group: g } })); @@ -25,6 +26,10 @@ export function getStaticPaths() { const { group } = Astro.props as { group: Group }; const isUnsold = group.key === "unsold-goods"; +// The acts recorded as reaching this group. Dates stay on the regulation pages, +// so there is one place on the site a date can be wrong. +const acts = actsFor(group.key); + // The disclosure map is field -> class. Invert it: a reader wants to know who // sees what, not to scan a field list for the one they care about. const byClass = new Map(); @@ -107,10 +112,10 @@ const description = isUnsold <>

      Everything in this passport is public for now.{" "} - That is not an oversight. Under ESPR, who may see which part of a - passport is set by each product group's own rules, and none have - been adopted for {group.name.toLowerCase()} yet. Inventing a split - would mean asserting a rule nobody has written. + That is not an oversight. Who may see which part of a passport is + set by each product group's own rules, and the software models no + such split for {group.name.toLowerCase()} yet. Inventing one would + mean asserting a rule nobody has written.

      ) : ( @@ -147,6 +152,32 @@ const description = isUnsold
    )} +
    + {acts.length === 0 ? ( +

    No act recorded in the software's library reaches this group yet.

    + ) : ( + + )} +

    + Dates, and where each one comes from, are on each act's page. +

    +
    +

    See all product groups diff --git a/site/dpp-landing/src/pages/product-groups/index.astro b/site/dpp-landing/src/pages/product-groups/index.astro index 46980f8..28ea016 100644 --- a/site/dpp-landing/src/pages/product-groups/index.astro +++ b/site/dpp-landing/src/pages/product-groups/index.astro @@ -85,9 +85,10 @@ const itemListLd = { not build, and when a definition changes, the page changes with it.

    - No deadlines appear on these pages, on purpose. A - regulatory date on a website is a claim that has to be kept current, and - a stale one is worse than none. + No deadlines on these pages, on purpose. A regulatory + date on a website is a claim that has to be kept current, so the dates + for each act sit on the{" "}regulations pages, + generated from the same library, rather than here.

    diff --git a/site/dpp-landing/src/pages/regulations/[id].astro b/site/dpp-landing/src/pages/regulations/[id].astro new file mode 100644 index 0000000..76446f7 --- /dev/null +++ b/site/dpp-landing/src/pages/regulations/[id].astro @@ -0,0 +1,136 @@ +--- +// /regulations/[id] — one legal act, generated from its instrument record. +// +// Shows the facts the record holds, worded for a general reader: status, the +// passport line, the date and where it comes from, the framework it sits +// under, when it was last checked against the official text, and the product +// groups it reaches with the articles each reach rests on. The record's own +// maintainer notes are not reproduced; they are written for the people keeping +// the library, not for this page. +import Base from "../../layouts/Base.astro"; +import Section from "../../components/Section.astro"; +import BasisMark from "../../components/BasisMark.astro"; +import ExternalArrow from "../../components/ExternalArrow.astro"; +import { + type Instrument, + instruments, + displayTitle, + kindLabel, + statusLabel, + passportView, + basisLabel, + bindingLabel, + groupRef, + parentOf, + eurLex, + formatDate, +} from "../../lib/regulations"; + +export function getStaticPaths() { + return instruments.map((act) => ({ params: { id: act.id }, props: { act } })); +} + +const { act } = Astro.props as { act: Instrument }; +const name = displayTitle(act); +const passport = passportView(act); +const parent = parentOf(act); +const bindings = act.productGroups ?? []; + +const title = `${name} | Odal Node`; +const description = `${kindLabel(act)}: ${passport.label.toLowerCase()}. What it requires of a passport, and the product groups it reaches, as recorded in the Odal Node library.`; +--- + + +
    + +
    +
    +
    Passport
    +
    {passport.label}
    +
    {passport.detail}
    + {passport.date && ( +
    + )} +
    +
    +
    Status
    +
    {statusLabel(act)}
    +
    + {act.currency?.checkedOn + ? `Last checked against the official text on ${formatDate(act.currency.checkedOn)}.` + : "Not adopted, so there is no official text to check yet."} +
    +
    + {parent && ( +
    +
    Adopted under
    +
    + {displayTitle(parent)} +
    +
    + )} + {act.celex && ( +
    +
    Official text
    +
    + Read it on EUR-Lex +
    +
    + )} +
    +
    + +
    + {bindings.length === 0 ? ( +

    + None that the software models as a product group yet. The act still + applies to the products it covers; they have not been modelled here. +

    + ) : ( +
      + {bindings.map((b) => { + const g = groupRef(b.productGroup); + return ( +
    • +
      +

      + {g.href ? {g.name} : g.name} +

      + + {bindingLabel(b.status)} + +
      + {b.legalBasis && b.legalBasis.length > 0 && ( + <> +

      Rests on

      +
        + {b.legalBasis.map((l) =>
      • {l}
      • )} +
      + + )} +
    • + ); + })} +
    + )} +

    + Binding now means something about the group can be + determined under this act today. Not yet determinable{" "} + means the act exists or is expected, but the rules a determination needs + have not been adopted. +

    +
    + +
    +

    + This page describes how the act is recorded in the software's library. It + is not a complete statement of the act, and it does not tell you whether + the act applies to your product. The official text does. +

    +

    All regulations

    +
    + diff --git a/site/dpp-landing/src/pages/regulations/index.astro b/site/dpp-landing/src/pages/regulations/index.astro new file mode 100644 index 0000000..5bd52b6 --- /dev/null +++ b/site/dpp-landing/src/pages/regulations/index.astro @@ -0,0 +1,78 @@ +--- +// /regulations — the EU acts behind the passports, generated from the +// instrument records vendored out of dpp-core. Nothing here is hand-written +// law: every date, status and product group comes from those records, which +// the software itself runs on. See lib/regulations.ts for the one rule the +// wording enforces. +import Base from "../../layouts/Base.astro"; +import Section from "../../components/Section.astro"; +import ActCard from "../../components/ActCard.astro"; +import BasisMark from "../../components/BasisMark.astro"; +import { creatingPassports, shapingPassports, instruments, displayTitle } from "../../lib/regulations"; + +const title = "Regulations: the EU acts behind the passports | Odal Node"; +const description = + "The EU acts that create or shape Digital Product Passports, with the date each one applies and where that date comes from. Generated from the same records the software runs on."; + +const itemListLd = { + "@context": "https://schema.org", + "@type": "ItemList", + name: "EU acts modelled by Odal Node", + itemListElement: instruments.map((i, n) => ({ + "@type": "ListItem", + position: n + 1, + name: displayTitle(i), + url: `https://odal-node.io/regulations/${i.id}`, + })), +}; +--- + + +
    +

    + Each act below is a record in the software's own library, and every + adopted one has been checked against its official text. This page is + generated from those records, so the site cannot state a date the + software does not hold. +

    +

    Every date says where it comes from:

    +
      +
    • + quoted from the Official Journal +
    • +
    • + our reading, with no text that states it +
    • +
    +
    + +
    +
      + {creatingPassports.map((act) => ( +
    • + ))} +
    +
    + +
    +
      + {shapingPassports.map((act) => ( +
    • + ))} +
    +
    + +
    +

    + It is not legal advice, and it is not a complete list of + what applies to your product. It is the set of acts the software models. + An act can reach products that were never modelled as a product group, so + the absence of an act here is not evidence that none applies. +

    +

    + Where an act requires a passport but the rules it depends on have not been + adopted, the page says the obligation is not yet operable rather than + printing a date that has already passed. +

    +
    + From 65d2585893f375f0c95b23a523876d14626d565d Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 25 Sep 2026 19:59:28 +0200 Subject: [PATCH 049/168] feat(landing): add a build-on-odal page and reach both new pages from the Explore menu and footer --- site/dpp-landing/src/components/Footer.astro | 4 +- site/dpp-landing/src/components/Nav.astro | 14 ++- .../dpp-landing/src/pages/build-on-odal.astro | 110 ++++++++++++++++++ 3 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 site/dpp-landing/src/pages/build-on-odal.astro diff --git a/site/dpp-landing/src/components/Footer.astro b/site/dpp-landing/src/components/Footer.astro index d18d576..1c6d363 100644 --- a/site/dpp-landing/src/components/Footer.astro +++ b/site/dpp-landing/src/components/Footer.astro @@ -22,13 +22,15 @@ const year = new Date().getFullYear();

    - diff --git a/site/dpp-landing/src/components/Nav.astro b/site/dpp-landing/src/components/Nav.astro index 235a510..80ae50f 100644 --- a/site/dpp-landing/src/components/Nav.astro +++ b/site/dpp-landing/src/components/Nav.astro @@ -36,6 +36,18 @@ const explore = [ line: "Who can read which part of a passport, and why", icon: '', }, + { + href: "/regulations", + label: "Regulations", + line: "The EU acts behind the passports, and when each applies", + icon: '', + }, + { + href: "/build-on-odal", + label: "Build on Odal", + line: "Embed the library, write rules, connect your systems", + icon: '', + }, { href: "/roadmap", label: "Roadmap", @@ -45,7 +57,7 @@ const explore = [ ]; const path = Astro.url.pathname.replace(/\/$/, "") || "/"; -const inExplore = explore.some((e) => path === e.href); +const inExplore = explore.some((e) => path === e.href || path.startsWith(`${e.href}/`)); const inGroups = path.startsWith("/product-groups"); --- diff --git a/site/dpp-landing/src/pages/build-on-odal.astro b/site/dpp-landing/src/pages/build-on-odal.astro new file mode 100644 index 0000000..b571b2e --- /dev/null +++ b/site/dpp-landing/src/pages/build-on-odal.astro @@ -0,0 +1,110 @@ +--- +// /build-on-odal — how someone other than us builds on the software. +// +// Every route described here exists in the code today: the Apache-2.0 core +// library, signed product-group plugins, signed rule updates and the Asset +// Administration Shell export. The page says plainly that nobody has built on +// it yet, because an ecosystem page is the easiest place on a site to imply +// one that does not exist. Technical detail lives on the docs site; this page +// links to it rather than repeating it. +import Base from "../layouts/Base.astro"; +import Section from "../components/Section.astro"; +import ExternalArrow from "../components/ExternalArrow.astro"; + +const title = "Build on Odal | Odal Node"; +const description = + "Four ways to build on Odal Node: embed the open library, write product-group rules, publish rule updates, and connect industrial systems. Trusted by signatures, not by us."; + +const ways = [ + { + title: "Embed the open library", + body: "The regulatory core is a set of Apache-2.0 libraries anyone can use, including a competitor: the passport model, signing, credentials, GS1 Digital Link, the industrial export and the regulatory rules.", + href: "https://docs.odal-node.io/core/overview", + cta: "What the core does", + icon: '', + }, + { + title: "Write product-group rules", + body: "A product group's checks can ship as a plugin that runs in an isolated sandbox. In production, a node runs a plugin only when it is signed by a key its operator trusts.", + href: "https://docs.odal-node.io/core/sectors", + cta: "Extending it with plugins", + icon: '', + }, + { + title: "Publish rule updates", + body: "Rule updates travel as signed bundles. A node accepts one only when it is signed by a key its operator has chosen to trust, and swaps it in without reinstalling anything.", + href: "https://docs.odal-node.io/engine/architecture", + cta: "How the node works", + icon: '', + }, + { + title: "Connect industrial systems", + body: "Passports export in the Asset Administration Shell format, so Industry 4.0 systems can read them without a custom integration.", + href: "https://docs.odal-node.io/core/standards", + cta: "The standards it uses", + icon: '', + }, +]; +--- + + +
    +

    + Odal Node is built so that rules, plugins and integrations written by + someone else can run on it, on infrastructure we never touch. They are + trusted because of signatures a node's operator chooses to accept, not + because of us. +

    +

    + Nobody has built on it yet. This page describes what is + possible today, not an ecosystem that already exists. +

    +
    + +
    +
      + {ways.map((w) => ( +
    • + + + +

      {w.title}

      +

      {w.body}

      +

      + {w.cta} +

      +
    • + ))} +
    +
    + +
    +

    + The core library is Apache-2.0. Use it for anything, + including a product that competes with ours. +

    +

    + The node is source-available under BSL-1.1. Running it + for your own organisation's passports is free. Offering it to others as a + hosted service, or shipping it inside a competing product, needs a + commercial licence. Each release converts to Apache-2.0 four years after + it is published. +

    +

    + The licensing page{" "} + has the detail, and the licence texts in each repository are the ones + that bind. +

    +
    + +
    +

    + Every node belongs to its operator, and every passport verifies without + us. There is no central service here for a builder to depend on, or to be + captured by, which is the point. +

    +

    + The code is public on GitHub. +

    +
    + From d28460a3aec06d3de1fcf79ae760a28a59d4dd41 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 25 Sep 2026 19:59:29 +0200 Subject: [PATCH 050/168] feat(landing): name two shipped capabilities the site never mentioned on the roadmap --- site/dpp-landing/src/data/roadmap.json | 52 ++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/site/dpp-landing/src/data/roadmap.json b/site/dpp-landing/src/data/roadmap.json index c1474a6..bdb2f56 100644 --- a/site/dpp-landing/src/data/roadmap.json +++ b/site/dpp-landing/src/data/roadmap.json @@ -26,12 +26,19 @@ { "title": "Spreadsheets in, passports out", "line": "Import your product data with a trial run first. The node reports what is wrong row by row instead of rejecting the whole file.", - "variants": ["CSV", "Excel"] + "variants": [ + "CSV", + "Excel" + ] }, { "title": "Checked before anything is signed", "line": "Each product group's rules run before signing, alongside warnings for values that look implausible. Rule updates arrive signed and apply without reinstalling anything.", - "variants": ["Blocking checks", "Advisory warnings", "Signed rule updates"] + "variants": [ + "Blocking checks", + "Advisory warnings", + "Signed rule updates" + ] } ] }, @@ -41,13 +48,23 @@ { "title": "Scan to read", "line": "The code on the product opens its public passport. No login and no fee.", - "variants": ["With a GS1 barcode", "Without a barcode"] + "variants": [ + "With a GS1 barcode", + "Without a barcode" + ] }, { "title": "Each reader sees what they are entitled to", "line": "Repairers, recyclers and authorities see more than the public, field by field, proven by a credential they hold rather than a setting they choose.", - "variants": ["Public", "Legitimate interest", "Authorities"], - "link": { "href": "/visibility", "label": "How visibility works" } + "variants": [ + "Public", + "Legitimate interest", + "Authorities" + ], + "link": { + "href": "/visibility", + "label": "How visibility works" + } }, { "title": "Export for industrial systems", @@ -61,7 +78,16 @@ { "title": "Changes without losing history", "line": "Correct a passport, replace it with a newer one, suspend it or retire it. Every earlier version stays readable, and every change is written to a tamper-evident trail.", - "variants": ["Correct", "Replace", "Suspend", "Retire"] + "variants": [ + "Correct", + "Replace", + "Suspend", + "Retire" + ] + }, + { + "title": "Passports keep working when the rules change", + "line": "A new version of a product group's schema never breaks the passports issued under an older one. They keep resolving exactly as they were issued." }, { "title": "A handover between companies", @@ -74,7 +100,10 @@ { "title": "A recorded end of life", "line": "The end of a product's life is recorded on its passport, including the handover to a waste operator.", - "link": { "href": "/lifecycle", "label": "Follow the whole lifecycle" } + "link": { + "href": "/lifecycle", + "label": "Follow the whole lifecycle" + } } ] }, @@ -97,7 +126,10 @@ { "title": "One tool to run it all", "line": "Set up, onboard, import, publish, issue credentials and check status from a single tool.", - "variants": ["Command line", "Interactive console"] + "variants": [ + "Command line", + "Interactive console" + ] }, { "title": "Your systems stay informed", @@ -114,6 +146,10 @@ { "title": "The unsold-goods disclosure", "line": "Record what was discarded, why and where it went, line by line, ready for the yearly disclosure." + }, + { + "title": "Safe by default in production", + "line": "A node set up for production refuses to start with placeholder security settings, instead of accepting them quietly." } ] } From da2f79508b7cb5aed8321fa8018cb0c8f23861c6 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 25 Sep 2026 19:59:29 +0200 Subject: [PATCH 051/168] fix(landing): stop the prose measure from capping card grids marked not-prose --- site/dpp-landing/src/styles/global.css | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/site/dpp-landing/src/styles/global.css b/site/dpp-landing/src/styles/global.css index 037fab2..026ee46 100644 --- a/site/dpp-landing/src/styles/global.css +++ b/site/dpp-landing/src/styles/global.css @@ -105,13 +105,11 @@ * A `wide` section widens its container so grids and card rows can breathe. * Body copy must not widen with it: past roughly 75 characters the eye loses * the start of the next line, and /lifecycle and /roadmap are long enough - * pages for that to matter. Grids, cards and anything in `not-prose` are - * untouched, because only direct text children are constrained. + * pages for that to matter. Grids, cards and anything marked `not-prose` are + * untouched: only direct text children are constrained, and a list that is + * really a grid of cards opts out with `not-prose`. */ - .prose > p, - .prose > ul, - .prose > ol, - .prose > blockquote { + .prose > :is(p, ul, ol, blockquote):not(.not-prose) { max-width: 68ch; } From 0641ac42da65ec1b6116cad2e5c610abf9b7080b Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 25 Sep 2026 20:07:33 +0200 Subject: [PATCH 052/168] feat(landing): read the home page's deadline cards from the instrument registry instead of a hand-written file --- .../src/components/DeadlineCards.astro | 77 +++++++++++++------ site/dpp-landing/src/data/deadlines.json | 23 ------ site/dpp-landing/src/lib/regulations.ts | 14 ++++ site/dpp-landing/src/pages/index.astro | 8 +- 4 files changed, 76 insertions(+), 46 deletions(-) delete mode 100644 site/dpp-landing/src/data/deadlines.json diff --git a/site/dpp-landing/src/components/DeadlineCards.astro b/site/dpp-landing/src/components/DeadlineCards.astro index 8feb96c..0401a24 100644 --- a/site/dpp-landing/src/components/DeadlineCards.astro +++ b/site/dpp-landing/src/components/DeadlineCards.astro @@ -1,29 +1,62 @@ --- -// DeadlineCards.astro — the regulatory clock. Data: src/data/deadlines.json. -// Uses the regulatory semantic tokens (amber family): these are deadline -// callouts, not brand surfaces. Every card carries its legal citation. +// DeadlineCards.astro — the regulatory clock, read from the instrument records +// vendored out of dpp-core (see lib/regulations.ts). Nothing here is typed in by +// hand: a card exists for every passport obligation the registry dates, and its +// date, citation and basis are the registry's own. Uses the regulatory semantic +// tokens (amber family): these are deadline callouts, not brand surfaces. // // A date that has passed reads "In force since", not as something upcoming. -// The comparison runs at build time, which is the only clock a static site -// has; every deploy rebuilds, so a card is at most one deploy out of date. -import data from "../data/deadlines.json"; -const { deadlines } = data; +// The comparison runs at build time, the only clock a static site has; every +// deploy rebuilds, so a card is at most one deploy out of date. +import BasisMark from "./BasisMark.astro"; +import { datedPassports, shortTitle, groupRef, basisLabel } from "../lib/regulations"; + const today = new Date().toISOString().slice(0, 10); -const cols = deadlines.length >= 3 ? "md:grid-cols-3" : "md:grid-cols-2"; +const cols = + datedPassports.length >= 4 + ? "md:grid-cols-2 lg:grid-cols-4" + : datedPassports.length === 3 + ? "md:grid-cols-3" + : "md:grid-cols-2"; + +const cards = datedPassports.map(({ act, view }) => { + const bindings = act.productGroups ?? []; + return { + id: act.id, + title: shortTitle(act), + groups: bindings.map((b) => groupRef(b.productGroup).name).join(", "), + // The article the obligation rests on, or, where the act reaches no group + // the software models, the act's own number. + citation: + bindings.find((b) => b.legalBasis?.length)?.legalBasis?.[0] ?? + `Regulation ${act.title.match(/\(EU\)\s*\d{4}\/\d+/)?.[0] ?? ""}`.trim(), + date: view.date!, + }; +}); --- -
    - {deadlines.map((d) => ( -
    -

    - {d.date <= today ? "In force since " : "From "}{d.dateLabel} -

    -

    {d.title}

    -

    - {d.who} -

    -

    {d.summary}

    -

    {d.citation}

    -
    +
    + diff --git a/site/dpp-landing/src/data/deadlines.json b/site/dpp-landing/src/data/deadlines.json deleted file mode 100644 index 8697c5d..0000000 --- a/site/dpp-landing/src/data/deadlines.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "_comment": "Regulatory deadlines shown on the home page. Every entry needs a citation to a named instrument and article, verified against the Official Journal text, not a secondary source. The _verify fields are working notes and are NOT rendered; a deadline whose _verify records an open question has not been checked, and an unchecked deadline must not ship. Only obligations that are law belong here: a Commission working plan is not law and carries no date, so it is described in prose on the page instead (2026-09-25).", - "deadlines": [ - { - "date": "2026-07-19", - "dateLabel": "19 July 2026", - "title": "Unsold goods", - "who": "Large companies", - "summary": "Destroying unsold textiles and footwear is banned for large companies, and what they discard must be disclosed.", - "citation": "ESPR (EU) 2024/1781, Art. 25", - "_verify": "Confirmed: Art. 25; large companies from 19 Jul 2026; medium-sized from 19 Jul 2030; micro/small exempt. Destruction exceptions in Delegated Regulation (EU) 2026/296. Disclosure is Art. 24." - }, - { - "date": "2027-02-18", - "dateLabel": "18 February 2027", - "title": "Battery passports", - "who": "EV, industrial (>2 kWh), LMT", - "summary": "Every battery in scope placed on the EU market or put into service needs a battery passport: an electronic record for that individual battery.", - "citation": "Regulation (EU) 2023/1542, Art. 77", - "_verify": "Art. 77(1) verified verbatim against the consolidated OJ text 02023R1542-20250731 on 2026-09-25: from 18 Feb 2027, each LMT battery, each industrial battery above 2 kWh and each EV battery placed on the market or put into service shall have an electronic record ('battery passport'). The earlier 'registered in the EU central registry' clause was removed: Art. 77 does not say it." - } - ] -} diff --git a/site/dpp-landing/src/lib/regulations.ts b/site/dpp-landing/src/lib/regulations.ts index 1acc32f..d907170 100644 --- a/site/dpp-landing/src/lib/regulations.ts +++ b/site/dpp-landing/src/lib/regulations.ts @@ -49,6 +49,10 @@ const byId = new Map(all.map((i) => [i.id, i])); /** Vendored titles are verbatim from core; the site's copy uses no em dashes. */ export const displayTitle = (i: Instrument) => i.title.replace(/\s+—\s+/g, ": "); +/** "Batteries Regulation (EU) 2023/1542" → "Batteries Regulation", for cards. */ +export const shortTitle = (i: Instrument) => + displayTitle(i).replace(/\s*\(EU\).*$/, "").replace(/:.*$/, ""); + const KIND: Record = { framework: "Framework regulation", direct: "Regulation", @@ -159,6 +163,16 @@ export const instruments: Instrument[] = [...all].sort((a, b) => { }); export const creatingPassports = instruments.filter((i) => i.passport.obligation === "required"); + +/** + * Every passport obligation that carries a date and can be met under its act: + * the home page's deadline cards. An act whose date has passed while nothing it + * depends on exists ("not yet operable") is left out on purpose; it is not a + * deadline anyone can act on. + */ +export const datedPassports = creatingPassports + .map((act) => ({ act, view: passportView(act) })) + .filter(({ view }) => view.state === "required" && view.date); export const shapingPassports = instruments.filter((i) => i.passport.obligation !== "required"); /** The acts that reach one product group, with the binding for it. */ diff --git a/site/dpp-landing/src/pages/index.astro b/site/dpp-landing/src/pages/index.astro index 22ee443..cecca21 100644 --- a/site/dpp-landing/src/pages/index.astro +++ b/site/dpp-landing/src/pages/index.astro @@ -7,8 +7,14 @@ import DeadlineCards from "../components/DeadlineCards.astro"; import Hero from "../components/Hero.astro"; import Section from "../components/Section.astro"; import StandardsRow from "../components/StandardsRow.astro"; +import { datedPassports } from "../lib/regulations"; import Base from "../layouts/Base.astro"; +// Counted from the registry, so the sentence cannot disagree with the cards. +const WORDS = ["No", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"]; +const n = datedPassports.length; +const lead = `${WORDS[n] ?? n} passport obligation${n === 1 ? " already carries" : "s already carry"} a date in law:`; + const title = "Odal Node: EU Digital Product Passport Infrastructure | ESPR Compliance"; const description = "Self-hosted infrastructure for EU Digital Product Passports (DPP) under ESPR, built on an open-source core. Sovereign compliance: your product data is validated and signed with your keys. Passports for batteries, textiles, steel and more."; @@ -58,7 +64,7 @@ const steps = [

    Compliance arrives sector by sector, date by date, not "everything by 2027." - Two dated obligations are already in law: + {lead}

    From 09e45a8736a5f85b643ab160490d6a6208ea5c73 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 25 Sep 2026 20:23:30 +0200 Subject: [PATCH 053/168] feat(landing): add /verify, a browser twin of the node's dossier verifier held to the engine's own verdicts --- .github/workflows/ci.yml | 6 + site/dpp-landing/package.json | 3 +- site/dpp-landing/public/_headers | 10 + .../verify/examples/01-valid-simple.json | 131 +++++ .../examples/02-valid-with-transfer.json | 203 ++++++++ .../verify/examples/03-valid-with-eol.json | 151 ++++++ .../examples/04-valid-full-lifecycle.json | 223 ++++++++ .../examples/05-tampered-signature.json | 223 ++++++++ .../examples/06-tampered-audit-entry.json | 223 ++++++++ .../07-tampered-transfer-signature.json | 223 ++++++++ .../examples/08-hidden-field-injection.json | 224 ++++++++ .../examples/09-unknown-top-level-field.json | 224 ++++++++ .../public/verify/examples/10-not-json.txt | 1 + site/dpp-landing/src/components/Footer.astro | 1 + site/dpp-landing/src/components/Nav.astro | 6 + site/dpp-landing/src/lib/verify/canonical.ts | 75 +++ site/dpp-landing/src/lib/verify/dossier.ts | 493 ++++++++++++++++++ site/dpp-landing/src/lib/verify/verify.ts | 333 ++++++++++++ site/dpp-landing/src/pages/verify.astro | 325 ++++++++++++ site/dpp-landing/tests/verify-expected.json | 442 ++++++++++++++++ site/dpp-landing/tests/verify.test.ts | 72 +++ 21 files changed, 3591 insertions(+), 1 deletion(-) create mode 100644 site/dpp-landing/public/verify/examples/01-valid-simple.json create mode 100644 site/dpp-landing/public/verify/examples/02-valid-with-transfer.json create mode 100644 site/dpp-landing/public/verify/examples/03-valid-with-eol.json create mode 100644 site/dpp-landing/public/verify/examples/04-valid-full-lifecycle.json create mode 100644 site/dpp-landing/public/verify/examples/05-tampered-signature.json create mode 100644 site/dpp-landing/public/verify/examples/06-tampered-audit-entry.json create mode 100644 site/dpp-landing/public/verify/examples/07-tampered-transfer-signature.json create mode 100644 site/dpp-landing/public/verify/examples/08-hidden-field-injection.json create mode 100644 site/dpp-landing/public/verify/examples/09-unknown-top-level-field.json create mode 100644 site/dpp-landing/public/verify/examples/10-not-json.txt create mode 100644 site/dpp-landing/src/lib/verify/canonical.ts create mode 100644 site/dpp-landing/src/lib/verify/dossier.ts create mode 100644 site/dpp-landing/src/lib/verify/verify.ts create mode 100644 site/dpp-landing/src/pages/verify.astro create mode 100644 site/dpp-landing/tests/verify-expected.json create mode 100644 site/dpp-landing/tests/verify.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d06760..5f80f63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -89,6 +89,12 @@ jobs: env: DPP_CORE_DIR: ${{ github.workspace }}/.dpp-core + # /verify re-implements the node's dossier verifier in the browser. This + # holds it to the engine's own verdicts on engine-built dossiers, check + # by check, so the two cannot drift apart unnoticed. + - name: Test the browser verifier against the engine's verdicts + run: pnpm --filter dpp-landing run test:verify + - name: Check the vendored API spec against the engine run: pnpm run check:openapi env: diff --git a/site/dpp-landing/package.json b/site/dpp-landing/package.json index 62cd6b0..be72b5f 100644 --- a/site/dpp-landing/package.json +++ b/site/dpp-landing/package.json @@ -11,7 +11,8 @@ "preview": "astro preview", "check": "astro check", "sync:core": "node scripts/sync-from-core.mjs", - "check:core": "node scripts/sync-from-core.mjs --check" + "check:core": "node scripts/sync-from-core.mjs --check", + "test:verify": "node --test tests/verify.test.ts" }, "dependencies": { "@astrojs/sitemap": "^3", diff --git a/site/dpp-landing/public/_headers b/site/dpp-landing/public/_headers index 1396296..a270838 100644 --- a/site/dpp-landing/public/_headers +++ b/site/dpp-landing/public/_headers @@ -15,3 +15,13 @@ /og-image.png Cache-Control: public, max-age=604800 + +# /verify promises that a checked file never leaves the machine. This makes +# it enforceable: the page may only talk to its own origin (to fetch the +# example files), so no script on it can send a file anywhere else, by +# request, beacon or image. +/verify + Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'none'; frame-ancestors 'none' + +/verify/* + Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'none'; frame-ancestors 'none' diff --git a/site/dpp-landing/public/verify/examples/01-valid-simple.json b/site/dpp-landing/public/verify/examples/01-valid-simple.json new file mode 100644 index 0000000..123389a --- /dev/null +++ b/site/dpp-landing/public/verify/examples/01-valid-simple.json @@ -0,0 +1,131 @@ +{ + "auditEntries": [ + { + "action": "create", + "actor": "operator:ops@acme.example", + "entryHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000001", + "metadata": null, + "newStatus": "draft", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "", + "previousStatus": null, + "timestamp": "2026-09-01T09:00:00Z" + }, + { + "action": "publish", + "actor": "operator:ops@acme.example", + "entryHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", + "metadata": { + "schemaVersion": "2.7.0" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "previousStatus": "draft", + "timestamp": "2026-09-01T09:05:00.250Z" + } + ], + "didDocuments": { + "did:web:node.acme.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:node.acme.example#key-1" + ], + "id": "did:web:node.acme.example", + "verificationMethod": [ + { + "controller": "did:web:node.acme.example", + "id": "did:web:node.acme.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" + }, + "type": "JsonWebKey2020" + } + ] + } + }, + "fullView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.56KnYFaD2s9v01NhcheQZGIqBujqLCNrGcbnEe6ZbNV2UozvcQsgYQQKegxFqNMKQpoPeuJJW_tN5gPuhBolDA", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "cathodeMaterial": "NMC 811", + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "stateOfHealthPct": 100, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "manifest": { + "contentHashes": { + "auditEntries": "9a44c968cbff54df608c8d79217923814f2d5ce6bb3e34ef7856001a11093f59", + "fullView": "750252051b902ddd89a447e209f7794b999f3c3c90c4edf63b5dfd356f671080", + "publicView": "1689c57aa9a6db1a2f069d88d3859ff4f389e05f12070c080265c614df6e4bc2" + }, + "coreVersion": "0.20.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.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjlhNDRjOTY4Y2JmZjU0ZGY2MDhjOGQ3OTIxNzkyMzgxNGYyZDVjZTZiYjNlMzRlZjc4NTYwMDFhMTEwOTNmNTkiLCJmdWxsVmlldyI6Ijc1MDI1MjA1MWI5MDJkZGQ4OWE0NDdlMjA5Zjc3OTRiOTk5ZjNjM2M5MGM0ZWRmNjNiNWRmZDM1NmY2NzEwODAiLCJwdWJsaWNWaWV3IjoiMTY4OWM1N2FhOWE2ZGIxYTJmMDY5ZDg4ZDM4NTlmZjRmMzg5ZTA1ZjEyMDcwYzA4MDI2NWM2MTRkZjZlNGJjMiJ9LCJjb3JlVmVyc2lvbiI6IjAuMjAuMCIsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjVUMTI6MDA6MDBaIiwiZm9ybWF0VmVyc2lvbiI6IjEiLCJpc3N1ZXJEaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwibm9kZVZlcnNpb24iOiIwLjE0LjAiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIn0.fQiNTfW6SmicE1CAZZjFFaUetfVNzFKxZHhXxPtUypRGaDkgV-Nx8AKUjS5DkWq1-HtJ3XzuNpO6NRz-xnL8CA", + "publicView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.RPgAGQWX_gMRUmIX3tE25prFcCCUo5Bije_ZyuQ96SqDaLiWOZUC_Dm834Z2eUzKRdSPT1Jn3frRTUs19W5IAw", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + } +} \ No newline at end of file diff --git a/site/dpp-landing/public/verify/examples/02-valid-with-transfer.json b/site/dpp-landing/public/verify/examples/02-valid-with-transfer.json new file mode 100644 index 0000000..0739ea7 --- /dev/null +++ b/site/dpp-landing/public/verify/examples/02-valid-with-transfer.json @@ -0,0 +1,203 @@ +{ + "auditEntries": [ + { + "action": "create", + "actor": "operator:ops@acme.example", + "entryHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000001", + "metadata": null, + "newStatus": "draft", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "", + "previousStatus": null, + "timestamp": "2026-09-01T09:00:00Z" + }, + { + "action": "publish", + "actor": "operator:ops@acme.example", + "entryHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", + "metadata": { + "schemaVersion": "2.7.0" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "previousStatus": "draft", + "timestamp": "2026-09-01T09:05:00.250Z" + }, + { + "action": "transfer_completed", + "actor": "operator:ops@acme.example", + "entryHash": "25719a9ebd7d548cdbaddc026f5c5d3d5ee76dcba0ab1de1a073d511c91e8a25", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", + "metadata": { + "transferId": "0199a3c2-7b10-7e4a-9c3d-0000000000aa" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "previousStatus": "active", + "timestamp": "2026-09-10T14:30:00.123456Z" + } + ], + "didDocuments": { + "did:web:node.acme.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:node.acme.example#key-1" + ], + "id": "did:web:node.acme.example", + "verificationMethod": [ + { + "controller": "did:web:node.acme.example", + "id": "did:web:node.acme.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" + }, + "type": "JsonWebKey2020" + } + ] + }, + "did:web:secondlife.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:secondlife.example#key-1" + ], + "id": "did:web:secondlife.example", + "verificationMethod": [ + { + "controller": "did:web:secondlife.example", + "id": "did:web:secondlife.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "_RckOFqgx1tk-3jNYC-h2ZH96_drE8WO1wLqyDXp9hg" + }, + "type": "JsonWebKey2020" + } + ] + } + }, + "fullView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.56KnYFaD2s9v01NhcheQZGIqBujqLCNrGcbnEe6ZbNV2UozvcQsgYQQKegxFqNMKQpoPeuJJW_tN5gPuhBolDA", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "cathodeMaterial": "NMC 811", + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "stateOfHealthPct": 100, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "manifest": { + "contentHashes": { + "auditEntries": "6209104fcfe6a17445d634787da2008afd295276b1746a706111466686bf24c4", + "fullView": "750252051b902ddd89a447e209f7794b999f3c3c90c4edf63b5dfd356f671080", + "publicView": "1689c57aa9a6db1a2f069d88d3859ff4f389e05f12070c080265c614df6e4bc2", + "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" + }, + "coreVersion": "0.20.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.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjYyMDkxMDRmY2ZlNmExNzQ0NWQ2MzQ3ODdkYTIwMDhhZmQyOTUyNzZiMTc0NmE3MDYxMTE0NjY2ODZiZjI0YzQiLCJmdWxsVmlldyI6Ijc1MDI1MjA1MWI5MDJkZGQ4OWE0NDdlMjA5Zjc3OTRiOTk5ZjNjM2M5MGM0ZWRmNjNiNWRmZDM1NmY2NzEwODAiLCJwdWJsaWNWaWV3IjoiMTY4OWM1N2FhOWE2ZGIxYTJmMDY5ZDg4ZDM4NTlmZjRmMzg5ZTA1ZjEyMDcwYzA4MDI2NWM2MTRkZjZlNGJjMiIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.nwdFeYm-4dILZge9UPInjIegXKAWLeF98u2uH7WCYTbqKYybHgt9qncLY_Osg8cj4Bu0bwyRFCVqwc-nDHxtBA", + "publicView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.RPgAGQWX_gMRUmIX3tE25prFcCCUo5Bije_ZyuQ96SqDaLiWOZUC_Dm834Z2eUzKRdSPT1Jn3frRTUs19W5IAw", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "transferChain": { + "originalOperator": { + "country": "DE", + "did": "did:web:node.acme.example", + "euOperatorId": null, + "name": "Acme Zellen GmbH", + "role": "manufacturer" + }, + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "transfers": [ + { + "completedAt": "2026-09-10T14:30:00Z", + "fromOperator": { + "country": "DE", + "did": "did:web:node.acme.example", + "euOperatorId": null, + "name": "Acme Zellen GmbH", + "role": "manufacturer" + }, + "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.mtQCd6lEOMHAMcpsKZUFbx0vvRBS05El5_PWWXqItXxIp-lJO9VV1PILF9lTPceECXVTAgDfxCFsTdf29k3iCw", + "initiatedAt": "2026-09-10T14:00:00Z", + "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.hrv0yKqeb1DVf-X9ysNDsFOawnrocRUUaFo9ygKqitjNmxaFlj6o1Xyf6eXsyGDjOW7kBSjbrbSkbSbbsmHfCg", + "notes": "Repurposed into stationary storage.", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "reason": "repurposing", + "toOperator": { + "country": "NL", + "did": "did:web:secondlife.example", + "euOperatorId": null, + "name": "Second Life Storage BV", + "role": "repurposer" + }, + "transferId": "0199a3c2-7b10-7e4a-9c3d-0000000000aa" + } + ] + } +} \ No newline at end of file diff --git a/site/dpp-landing/public/verify/examples/03-valid-with-eol.json b/site/dpp-landing/public/verify/examples/03-valid-with-eol.json new file mode 100644 index 0000000..0d23f0b --- /dev/null +++ b/site/dpp-landing/public/verify/examples/03-valid-with-eol.json @@ -0,0 +1,151 @@ +{ + "auditEntries": [ + { + "action": "create", + "actor": "operator:ops@acme.example", + "entryHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000001", + "metadata": null, + "newStatus": "draft", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "", + "previousStatus": null, + "timestamp": "2026-09-01T09:00:00Z" + }, + { + "action": "publish", + "actor": "operator:ops@acme.example", + "entryHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", + "metadata": { + "schemaVersion": "2.7.0" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "previousStatus": "draft", + "timestamp": "2026-09-01T09:05:00.250Z" + }, + { + "action": "eol", + "actor": "operator:ops@acme.example", + "entryHash": "e42cbc27a807af62162f1bff91f808fc40a5c6d42d72a893173c1961981f3bef", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", + "metadata": { + "eolType": "recycled" + }, + "newStatus": "end_of_life", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "previousStatus": "active", + "timestamp": "2026-09-20T08:00:00Z" + } + ], + "didDocuments": { + "did:web:node.acme.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:node.acme.example#key-1" + ], + "id": "did:web:node.acme.example", + "verificationMethod": [ + { + "controller": "did:web:node.acme.example", + "id": "did:web:node.acme.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" + }, + "type": "JsonWebKey2020" + } + ] + } + }, + "eolEvent": { + "declaredAt": "2026-09-20T08:00:00Z", + "facility": "Recycling Nord GmbH", + "type": "recycled" + }, + "fullView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.56KnYFaD2s9v01NhcheQZGIqBujqLCNrGcbnEe6ZbNV2UozvcQsgYQQKegxFqNMKQpoPeuJJW_tN5gPuhBolDA", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "cathodeMaterial": "NMC 811", + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "stateOfHealthPct": 100, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "manifest": { + "contentHashes": { + "auditEntries": "78a2b7c11f531600519d8c4c9afc8df954d52912a36c3bc7fb7f1e5b503f5f33", + "eolEvent": "92ed1fde3e99fa59df0fb158a03bc10cad0174ed486271f3b6c715b9b633d964", + "fullView": "750252051b902ddd89a447e209f7794b999f3c3c90c4edf63b5dfd356f671080", + "publicView": "1689c57aa9a6db1a2f069d88d3859ff4f389e05f12070c080265c614df6e4bc2" + }, + "coreVersion": "0.20.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.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6Ijc4YTJiN2MxMWY1MzE2MDA1MTlkOGM0YzlhZmM4ZGY5NTRkNTI5MTJhMzZjM2JjN2ZiN2YxZTViNTAzZjVmMzMiLCJlb2xFdmVudCI6IjkyZWQxZmRlM2U5OWZhNTlkZjBmYjE1OGEwM2JjMTBjYWQwMTc0ZWQ0ODYyNzFmM2I2YzcxNWI5YjYzM2Q5NjQiLCJmdWxsVmlldyI6Ijc1MDI1MjA1MWI5MDJkZGQ4OWE0NDdlMjA5Zjc3OTRiOTk5ZjNjM2M5MGM0ZWRmNjNiNWRmZDM1NmY2NzEwODAiLCJwdWJsaWNWaWV3IjoiMTY4OWM1N2FhOWE2ZGIxYTJmMDY5ZDg4ZDM4NTlmZjRmMzg5ZTA1ZjEyMDcwYzA4MDI2NWM2MTRkZjZlNGJjMiJ9LCJjb3JlVmVyc2lvbiI6IjAuMjAuMCIsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjVUMTI6MDA6MDBaIiwiZm9ybWF0VmVyc2lvbiI6IjEiLCJpc3N1ZXJEaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwibm9kZVZlcnNpb24iOiIwLjE0LjAiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIn0.W6CMBfhjdRxlXLWA5RIcAGTCIg8a-jea75vo7lN1d5vNP7nUGYaMqBlq8XVN5ygCkR2XAnj5Q8TtB8mhc7iaBA", + "publicView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.RPgAGQWX_gMRUmIX3tE25prFcCCUo5Bije_ZyuQ96SqDaLiWOZUC_Dm834Z2eUzKRdSPT1Jn3frRTUs19W5IAw", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + } +} \ No newline at end of file diff --git a/site/dpp-landing/public/verify/examples/04-valid-full-lifecycle.json b/site/dpp-landing/public/verify/examples/04-valid-full-lifecycle.json new file mode 100644 index 0000000..6cafd24 --- /dev/null +++ b/site/dpp-landing/public/verify/examples/04-valid-full-lifecycle.json @@ -0,0 +1,223 @@ +{ + "auditEntries": [ + { + "action": "create", + "actor": "operator:ops@acme.example", + "entryHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000001", + "metadata": null, + "newStatus": "draft", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "", + "previousStatus": null, + "timestamp": "2026-09-01T09:00:00Z" + }, + { + "action": "publish", + "actor": "operator:ops@acme.example", + "entryHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", + "metadata": { + "schemaVersion": "2.7.0" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "previousStatus": "draft", + "timestamp": "2026-09-01T09:05:00.250Z" + }, + { + "action": "transfer_completed", + "actor": "operator:ops@acme.example", + "entryHash": "25719a9ebd7d548cdbaddc026f5c5d3d5ee76dcba0ab1de1a073d511c91e8a25", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", + "metadata": { + "transferId": "0199a3c2-7b10-7e4a-9c3d-0000000000aa" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "previousStatus": "active", + "timestamp": "2026-09-10T14:30:00.123456Z" + }, + { + "action": "eol", + "actor": "operator:ops@acme.example", + "entryHash": "84e93f7ef6393b43098018bd99f3e062698c3c2e89dda3609ba72a4b58652ab0", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", + "metadata": { + "eolType": "recycled" + }, + "newStatus": "end_of_life", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "25719a9ebd7d548cdbaddc026f5c5d3d5ee76dcba0ab1de1a073d511c91e8a25", + "previousStatus": "active", + "timestamp": "2026-09-20T08:00:00Z" + } + ], + "didDocuments": { + "did:web:node.acme.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:node.acme.example#key-1" + ], + "id": "did:web:node.acme.example", + "verificationMethod": [ + { + "controller": "did:web:node.acme.example", + "id": "did:web:node.acme.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" + }, + "type": "JsonWebKey2020" + } + ] + }, + "did:web:secondlife.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:secondlife.example#key-1" + ], + "id": "did:web:secondlife.example", + "verificationMethod": [ + { + "controller": "did:web:secondlife.example", + "id": "did:web:secondlife.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "_RckOFqgx1tk-3jNYC-h2ZH96_drE8WO1wLqyDXp9hg" + }, + "type": "JsonWebKey2020" + } + ] + } + }, + "eolEvent": { + "declaredAt": "2026-09-20T08:00:00Z", + "facility": "Recycling Nord GmbH", + "type": "recycled" + }, + "fullView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.56KnYFaD2s9v01NhcheQZGIqBujqLCNrGcbnEe6ZbNV2UozvcQsgYQQKegxFqNMKQpoPeuJJW_tN5gPuhBolDA", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "cathodeMaterial": "NMC 811", + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "stateOfHealthPct": 100, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "manifest": { + "contentHashes": { + "auditEntries": "e085a133b614c68a136354d07c0dcc960f33df17c4b405bf424a8b1336b2cdfe", + "eolEvent": "92ed1fde3e99fa59df0fb158a03bc10cad0174ed486271f3b6c715b9b633d964", + "fullView": "750252051b902ddd89a447e209f7794b999f3c3c90c4edf63b5dfd356f671080", + "publicView": "1689c57aa9a6db1a2f069d88d3859ff4f389e05f12070c080265c614df6e4bc2", + "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" + }, + "coreVersion": "0.20.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.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImUwODVhMTMzYjYxNGM2OGExMzYzNTRkMDdjMGRjYzk2MGYzM2RmMTdjNGI0MDViZjQyNGE4YjEzMzZiMmNkZmUiLCJlb2xFdmVudCI6IjkyZWQxZmRlM2U5OWZhNTlkZjBmYjE1OGEwM2JjMTBjYWQwMTc0ZWQ0ODYyNzFmM2I2YzcxNWI5YjYzM2Q5NjQiLCJmdWxsVmlldyI6Ijc1MDI1MjA1MWI5MDJkZGQ4OWE0NDdlMjA5Zjc3OTRiOTk5ZjNjM2M5MGM0ZWRmNjNiNWRmZDM1NmY2NzEwODAiLCJwdWJsaWNWaWV3IjoiMTY4OWM1N2FhOWE2ZGIxYTJmMDY5ZDg4ZDM4NTlmZjRmMzg5ZTA1ZjEyMDcwYzA4MDI2NWM2MTRkZjZlNGJjMiIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.Z0D_04GOq7-C-uIJlLqSshl99lzcjFFc_v7FRmtewsJYIo4950_Ui-gvLhQgZO5JSSiXc2UTn9aDlBXRpAL8BQ", + "publicView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.RPgAGQWX_gMRUmIX3tE25prFcCCUo5Bije_ZyuQ96SqDaLiWOZUC_Dm834Z2eUzKRdSPT1Jn3frRTUs19W5IAw", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "transferChain": { + "originalOperator": { + "country": "DE", + "did": "did:web:node.acme.example", + "euOperatorId": null, + "name": "Acme Zellen GmbH", + "role": "manufacturer" + }, + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "transfers": [ + { + "completedAt": "2026-09-10T14:30:00Z", + "fromOperator": { + "country": "DE", + "did": "did:web:node.acme.example", + "euOperatorId": null, + "name": "Acme Zellen GmbH", + "role": "manufacturer" + }, + "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.mtQCd6lEOMHAMcpsKZUFbx0vvRBS05El5_PWWXqItXxIp-lJO9VV1PILF9lTPceECXVTAgDfxCFsTdf29k3iCw", + "initiatedAt": "2026-09-10T14:00:00Z", + "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.hrv0yKqeb1DVf-X9ysNDsFOawnrocRUUaFo9ygKqitjNmxaFlj6o1Xyf6eXsyGDjOW7kBSjbrbSkbSbbsmHfCg", + "notes": "Repurposed into stationary storage.", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "reason": "repurposing", + "toOperator": { + "country": "NL", + "did": "did:web:secondlife.example", + "euOperatorId": null, + "name": "Second Life Storage BV", + "role": "repurposer" + }, + "transferId": "0199a3c2-7b10-7e4a-9c3d-0000000000aa" + } + ] + } +} \ No newline at end of file diff --git a/site/dpp-landing/public/verify/examples/05-tampered-signature.json b/site/dpp-landing/public/verify/examples/05-tampered-signature.json new file mode 100644 index 0000000..d697583 --- /dev/null +++ b/site/dpp-landing/public/verify/examples/05-tampered-signature.json @@ -0,0 +1,223 @@ +{ + "auditEntries": [ + { + "action": "create", + "actor": "operator:ops@acme.example", + "entryHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000001", + "metadata": null, + "newStatus": "draft", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "", + "previousStatus": null, + "timestamp": "2026-09-01T09:00:00Z" + }, + { + "action": "publish", + "actor": "operator:ops@acme.example", + "entryHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", + "metadata": { + "schemaVersion": "2.7.0" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "previousStatus": "draft", + "timestamp": "2026-09-01T09:05:00.250Z" + }, + { + "action": "transfer_completed", + "actor": "operator:ops@acme.example", + "entryHash": "25719a9ebd7d548cdbaddc026f5c5d3d5ee76dcba0ab1de1a073d511c91e8a25", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", + "metadata": { + "transferId": "0199a3c2-7b10-7e4a-9c3d-0000000000aa" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "previousStatus": "active", + "timestamp": "2026-09-10T14:30:00.123456Z" + }, + { + "action": "eol", + "actor": "operator:ops@acme.example", + "entryHash": "84e93f7ef6393b43098018bd99f3e062698c3c2e89dda3609ba72a4b58652ab0", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", + "metadata": { + "eolType": "recycled" + }, + "newStatus": "end_of_life", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "25719a9ebd7d548cdbaddc026f5c5d3d5ee76dcba0ab1de1a073d511c91e8a25", + "previousStatus": "active", + "timestamp": "2026-09-20T08:00:00Z" + } + ], + "didDocuments": { + "did:web:node.acme.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:node.acme.example#key-1" + ], + "id": "did:web:node.acme.example", + "verificationMethod": [ + { + "controller": "did:web:node.acme.example", + "id": "did:web:node.acme.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" + }, + "type": "JsonWebKey2020" + } + ] + }, + "did:web:secondlife.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:secondlife.example#key-1" + ], + "id": "did:web:secondlife.example", + "verificationMethod": [ + { + "controller": "did:web:secondlife.example", + "id": "did:web:secondlife.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "_RckOFqgx1tk-3jNYC-h2ZH96_drE8WO1wLqyDXp9hg" + }, + "type": "JsonWebKey2020" + } + ] + } + }, + "eolEvent": { + "declaredAt": "2026-09-20T08:00:00Z", + "facility": "Recycling Nord GmbH", + "type": "recycled" + }, + "fullView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.56KnYFaD2s9v01NhcheQZGIqBujqLCNrGcbnEe6ZbNV2UozvcQsgYQQKegxFqNMKQpoPeuJJW_tN5gPuhBolDA", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "cathodeMaterial": "NMC 811", + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "stateOfHealthPct": 100, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "manifest": { + "contentHashes": { + "auditEntries": "e085a133b614c68a136354d07c0dcc960f33df17c4b405bf424a8b1336b2cdfe", + "eolEvent": "92ed1fde3e99fa59df0fb158a03bc10cad0174ed486271f3b6c715b9b633d964", + "fullView": "750252051b902ddd89a447e209f7794b999f3c3c90c4edf63b5dfd356f671080", + "publicView": "1689c57aa9a6db1a2f069d88d3859ff4f389e05f12070c080265c614df6e4bc2", + "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" + }, + "coreVersion": "0.20.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.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImUwODVhMTMzYjYxNGM2OGExMzYzNTRkMDdjMGRjYzk2MGYzM2RmMTdjNGI0MDViZjQyNGE4YjEzMzZiMmNkZmUiLCJlb2xFdmVudCI6IjkyZWQxZmRlM2U5OWZhNTlkZjBmYjE1OGEwM2JjMTBjYWQwMTc0ZWQ0ODYyNzFmM2I2YzcxNWI5YjYzM2Q5NjQiLCJmdWxsVmlldyI6Ijc1MDI1MjA1MWI5MDJkZGQ4OWE0NDdlMjA5Zjc3OTRiOTk5ZjNjM2M5MGM0ZWRmNjNiNWRmZDM1NmY2NzEwODAiLCJwdWJsaWNWaWV3IjoiMTY4OWM1N2FhOWE2ZGIxYTJmMDY5ZDg4ZDM4NTlmZjRmMzg5ZTA1ZjEyMDcwYzA4MDI2NWM2MTRkZjZlNGJjMiIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.Z0D_04GOq7-C-uIJlLqSshl99lzcjFFc_v7FRmtewsJYIo4950_Ui-gvLhQgZO5JSSiXc2UTn9aDlBXRpAL8BQ", + "publicView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.RPgAGQWX_gMRUmIX3tE25prFcCCUo5Bije_ZyuQ96SqAaLiWOZUC_Dm834Z2eUzKRdSPT1Jn3frRTUs19W5IAw", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "transferChain": { + "originalOperator": { + "country": "DE", + "did": "did:web:node.acme.example", + "euOperatorId": null, + "name": "Acme Zellen GmbH", + "role": "manufacturer" + }, + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "transfers": [ + { + "completedAt": "2026-09-10T14:30:00Z", + "fromOperator": { + "country": "DE", + "did": "did:web:node.acme.example", + "euOperatorId": null, + "name": "Acme Zellen GmbH", + "role": "manufacturer" + }, + "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.mtQCd6lEOMHAMcpsKZUFbx0vvRBS05El5_PWWXqItXxIp-lJO9VV1PILF9lTPceECXVTAgDfxCFsTdf29k3iCw", + "initiatedAt": "2026-09-10T14:00:00Z", + "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.hrv0yKqeb1DVf-X9ysNDsFOawnrocRUUaFo9ygKqitjNmxaFlj6o1Xyf6eXsyGDjOW7kBSjbrbSkbSbbsmHfCg", + "notes": "Repurposed into stationary storage.", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "reason": "repurposing", + "toOperator": { + "country": "NL", + "did": "did:web:secondlife.example", + "euOperatorId": null, + "name": "Second Life Storage BV", + "role": "repurposer" + }, + "transferId": "0199a3c2-7b10-7e4a-9c3d-0000000000aa" + } + ] + } +} \ No newline at end of file diff --git a/site/dpp-landing/public/verify/examples/06-tampered-audit-entry.json b/site/dpp-landing/public/verify/examples/06-tampered-audit-entry.json new file mode 100644 index 0000000..196e5e9 --- /dev/null +++ b/site/dpp-landing/public/verify/examples/06-tampered-audit-entry.json @@ -0,0 +1,223 @@ +{ + "auditEntries": [ + { + "action": "delete", + "actor": "operator:ops@acme.example", + "entryHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000001", + "metadata": null, + "newStatus": "draft", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "", + "previousStatus": null, + "timestamp": "2026-09-01T09:00:00Z" + }, + { + "action": "publish", + "actor": "operator:ops@acme.example", + "entryHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", + "metadata": { + "schemaVersion": "2.7.0" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "previousStatus": "draft", + "timestamp": "2026-09-01T09:05:00.250Z" + }, + { + "action": "transfer_completed", + "actor": "operator:ops@acme.example", + "entryHash": "25719a9ebd7d548cdbaddc026f5c5d3d5ee76dcba0ab1de1a073d511c91e8a25", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", + "metadata": { + "transferId": "0199a3c2-7b10-7e4a-9c3d-0000000000aa" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "previousStatus": "active", + "timestamp": "2026-09-10T14:30:00.123456Z" + }, + { + "action": "eol", + "actor": "operator:ops@acme.example", + "entryHash": "84e93f7ef6393b43098018bd99f3e062698c3c2e89dda3609ba72a4b58652ab0", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", + "metadata": { + "eolType": "recycled" + }, + "newStatus": "end_of_life", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "25719a9ebd7d548cdbaddc026f5c5d3d5ee76dcba0ab1de1a073d511c91e8a25", + "previousStatus": "active", + "timestamp": "2026-09-20T08:00:00Z" + } + ], + "didDocuments": { + "did:web:node.acme.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:node.acme.example#key-1" + ], + "id": "did:web:node.acme.example", + "verificationMethod": [ + { + "controller": "did:web:node.acme.example", + "id": "did:web:node.acme.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" + }, + "type": "JsonWebKey2020" + } + ] + }, + "did:web:secondlife.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:secondlife.example#key-1" + ], + "id": "did:web:secondlife.example", + "verificationMethod": [ + { + "controller": "did:web:secondlife.example", + "id": "did:web:secondlife.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "_RckOFqgx1tk-3jNYC-h2ZH96_drE8WO1wLqyDXp9hg" + }, + "type": "JsonWebKey2020" + } + ] + } + }, + "eolEvent": { + "declaredAt": "2026-09-20T08:00:00Z", + "facility": "Recycling Nord GmbH", + "type": "recycled" + }, + "fullView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.56KnYFaD2s9v01NhcheQZGIqBujqLCNrGcbnEe6ZbNV2UozvcQsgYQQKegxFqNMKQpoPeuJJW_tN5gPuhBolDA", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "cathodeMaterial": "NMC 811", + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "stateOfHealthPct": 100, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "manifest": { + "contentHashes": { + "auditEntries": "e085a133b614c68a136354d07c0dcc960f33df17c4b405bf424a8b1336b2cdfe", + "eolEvent": "92ed1fde3e99fa59df0fb158a03bc10cad0174ed486271f3b6c715b9b633d964", + "fullView": "750252051b902ddd89a447e209f7794b999f3c3c90c4edf63b5dfd356f671080", + "publicView": "1689c57aa9a6db1a2f069d88d3859ff4f389e05f12070c080265c614df6e4bc2", + "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" + }, + "coreVersion": "0.20.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.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImUwODVhMTMzYjYxNGM2OGExMzYzNTRkMDdjMGRjYzk2MGYzM2RmMTdjNGI0MDViZjQyNGE4YjEzMzZiMmNkZmUiLCJlb2xFdmVudCI6IjkyZWQxZmRlM2U5OWZhNTlkZjBmYjE1OGEwM2JjMTBjYWQwMTc0ZWQ0ODYyNzFmM2I2YzcxNWI5YjYzM2Q5NjQiLCJmdWxsVmlldyI6Ijc1MDI1MjA1MWI5MDJkZGQ4OWE0NDdlMjA5Zjc3OTRiOTk5ZjNjM2M5MGM0ZWRmNjNiNWRmZDM1NmY2NzEwODAiLCJwdWJsaWNWaWV3IjoiMTY4OWM1N2FhOWE2ZGIxYTJmMDY5ZDg4ZDM4NTlmZjRmMzg5ZTA1ZjEyMDcwYzA4MDI2NWM2MTRkZjZlNGJjMiIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.Z0D_04GOq7-C-uIJlLqSshl99lzcjFFc_v7FRmtewsJYIo4950_Ui-gvLhQgZO5JSSiXc2UTn9aDlBXRpAL8BQ", + "publicView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.RPgAGQWX_gMRUmIX3tE25prFcCCUo5Bije_ZyuQ96SqDaLiWOZUC_Dm834Z2eUzKRdSPT1Jn3frRTUs19W5IAw", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "transferChain": { + "originalOperator": { + "country": "DE", + "did": "did:web:node.acme.example", + "euOperatorId": null, + "name": "Acme Zellen GmbH", + "role": "manufacturer" + }, + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "transfers": [ + { + "completedAt": "2026-09-10T14:30:00Z", + "fromOperator": { + "country": "DE", + "did": "did:web:node.acme.example", + "euOperatorId": null, + "name": "Acme Zellen GmbH", + "role": "manufacturer" + }, + "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.mtQCd6lEOMHAMcpsKZUFbx0vvRBS05El5_PWWXqItXxIp-lJO9VV1PILF9lTPceECXVTAgDfxCFsTdf29k3iCw", + "initiatedAt": "2026-09-10T14:00:00Z", + "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.hrv0yKqeb1DVf-X9ysNDsFOawnrocRUUaFo9ygKqitjNmxaFlj6o1Xyf6eXsyGDjOW7kBSjbrbSkbSbbsmHfCg", + "notes": "Repurposed into stationary storage.", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "reason": "repurposing", + "toOperator": { + "country": "NL", + "did": "did:web:secondlife.example", + "euOperatorId": null, + "name": "Second Life Storage BV", + "role": "repurposer" + }, + "transferId": "0199a3c2-7b10-7e4a-9c3d-0000000000aa" + } + ] + } +} \ No newline at end of file diff --git a/site/dpp-landing/public/verify/examples/07-tampered-transfer-signature.json b/site/dpp-landing/public/verify/examples/07-tampered-transfer-signature.json new file mode 100644 index 0000000..145f645 --- /dev/null +++ b/site/dpp-landing/public/verify/examples/07-tampered-transfer-signature.json @@ -0,0 +1,223 @@ +{ + "auditEntries": [ + { + "action": "create", + "actor": "operator:ops@acme.example", + "entryHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000001", + "metadata": null, + "newStatus": "draft", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "", + "previousStatus": null, + "timestamp": "2026-09-01T09:00:00Z" + }, + { + "action": "publish", + "actor": "operator:ops@acme.example", + "entryHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", + "metadata": { + "schemaVersion": "2.7.0" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "previousStatus": "draft", + "timestamp": "2026-09-01T09:05:00.250Z" + }, + { + "action": "transfer_completed", + "actor": "operator:ops@acme.example", + "entryHash": "25719a9ebd7d548cdbaddc026f5c5d3d5ee76dcba0ab1de1a073d511c91e8a25", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", + "metadata": { + "transferId": "0199a3c2-7b10-7e4a-9c3d-0000000000aa" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "previousStatus": "active", + "timestamp": "2026-09-10T14:30:00.123456Z" + }, + { + "action": "eol", + "actor": "operator:ops@acme.example", + "entryHash": "84e93f7ef6393b43098018bd99f3e062698c3c2e89dda3609ba72a4b58652ab0", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", + "metadata": { + "eolType": "recycled" + }, + "newStatus": "end_of_life", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "25719a9ebd7d548cdbaddc026f5c5d3d5ee76dcba0ab1de1a073d511c91e8a25", + "previousStatus": "active", + "timestamp": "2026-09-20T08:00:00Z" + } + ], + "didDocuments": { + "did:web:node.acme.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:node.acme.example#key-1" + ], + "id": "did:web:node.acme.example", + "verificationMethod": [ + { + "controller": "did:web:node.acme.example", + "id": "did:web:node.acme.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" + }, + "type": "JsonWebKey2020" + } + ] + }, + "did:web:secondlife.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:secondlife.example#key-1" + ], + "id": "did:web:secondlife.example", + "verificationMethod": [ + { + "controller": "did:web:secondlife.example", + "id": "did:web:secondlife.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "_RckOFqgx1tk-3jNYC-h2ZH96_drE8WO1wLqyDXp9hg" + }, + "type": "JsonWebKey2020" + } + ] + } + }, + "eolEvent": { + "declaredAt": "2026-09-20T08:00:00Z", + "facility": "Recycling Nord GmbH", + "type": "recycled" + }, + "fullView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.56KnYFaD2s9v01NhcheQZGIqBujqLCNrGcbnEe6ZbNV2UozvcQsgYQQKegxFqNMKQpoPeuJJW_tN5gPuhBolDA", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "cathodeMaterial": "NMC 811", + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "stateOfHealthPct": 100, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "manifest": { + "contentHashes": { + "auditEntries": "e085a133b614c68a136354d07c0dcc960f33df17c4b405bf424a8b1336b2cdfe", + "eolEvent": "92ed1fde3e99fa59df0fb158a03bc10cad0174ed486271f3b6c715b9b633d964", + "fullView": "750252051b902ddd89a447e209f7794b999f3c3c90c4edf63b5dfd356f671080", + "publicView": "1689c57aa9a6db1a2f069d88d3859ff4f389e05f12070c080265c614df6e4bc2", + "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" + }, + "coreVersion": "0.20.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.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImUwODVhMTMzYjYxNGM2OGExMzYzNTRkMDdjMGRjYzk2MGYzM2RmMTdjNGI0MDViZjQyNGE4YjEzMzZiMmNkZmUiLCJlb2xFdmVudCI6IjkyZWQxZmRlM2U5OWZhNTlkZjBmYjE1OGEwM2JjMTBjYWQwMTc0ZWQ0ODYyNzFmM2I2YzcxNWI5YjYzM2Q5NjQiLCJmdWxsVmlldyI6Ijc1MDI1MjA1MWI5MDJkZGQ4OWE0NDdlMjA5Zjc3OTRiOTk5ZjNjM2M5MGM0ZWRmNjNiNWRmZDM1NmY2NzEwODAiLCJwdWJsaWNWaWV3IjoiMTY4OWM1N2FhOWE2ZGIxYTJmMDY5ZDg4ZDM4NTlmZjRmMzg5ZTA1ZjEyMDcwYzA4MDI2NWM2MTRkZjZlNGJjMiIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.Z0D_04GOq7-C-uIJlLqSshl99lzcjFFc_v7FRmtewsJYIo4950_Ui-gvLhQgZO5JSSiXc2UTn9aDlBXRpAL8BQ", + "publicView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.RPgAGQWX_gMRUmIX3tE25prFcCCUo5Bije_ZyuQ96SqDaLiWOZUC_Dm834Z2eUzKRdSPT1Jn3frRTUs19W5IAw", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "transferChain": { + "originalOperator": { + "country": "DE", + "did": "did:web:node.acme.example", + "euOperatorId": null, + "name": "Acme Zellen GmbH", + "role": "manufacturer" + }, + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "transfers": [ + { + "completedAt": "2026-09-10T14:30:00Z", + "fromOperator": { + "country": "DE", + "did": "did:web:node.acme.example", + "euOperatorId": null, + "name": "Acme Zellen GmbH", + "role": "manufacturer" + }, + "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.mtQCd6lEOMHAMcpsKZUFbx0vvRBS05El5_PWWXqItXxIp-lJO9VV1PILF9lTPceECXVTAgDfxCFsTdf29k3iCw", + "initiatedAt": "2026-09-10T14:00:00Z", + "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.hrv0yKqeb1DVf-X9ysNDsFOawnrocRUUaFo9ygKqitjAmxaFlj6o1Xyf6eXsyGDjOW7kBSjbrbSkbSbbsmHfCg", + "notes": "Repurposed into stationary storage.", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "reason": "repurposing", + "toOperator": { + "country": "NL", + "did": "did:web:secondlife.example", + "euOperatorId": null, + "name": "Second Life Storage BV", + "role": "repurposer" + }, + "transferId": "0199a3c2-7b10-7e4a-9c3d-0000000000aa" + } + ] + } +} \ No newline at end of file diff --git a/site/dpp-landing/public/verify/examples/08-hidden-field-injection.json b/site/dpp-landing/public/verify/examples/08-hidden-field-injection.json new file mode 100644 index 0000000..d9a0420 --- /dev/null +++ b/site/dpp-landing/public/verify/examples/08-hidden-field-injection.json @@ -0,0 +1,224 @@ +{ + "auditEntries": [ + { + "action": "create", + "actor": "operator:ops@acme.example", + "entryHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000001", + "metadata": null, + "newStatus": "draft", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "", + "previousStatus": null, + "timestamp": "2026-09-01T09:00:00Z" + }, + { + "action": "publish", + "actor": "operator:ops@acme.example", + "entryHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", + "metadata": { + "schemaVersion": "2.7.0" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "previousStatus": "draft", + "timestamp": "2026-09-01T09:05:00.250Z" + }, + { + "action": "transfer_completed", + "actor": "operator:ops@acme.example", + "entryHash": "25719a9ebd7d548cdbaddc026f5c5d3d5ee76dcba0ab1de1a073d511c91e8a25", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", + "metadata": { + "transferId": "0199a3c2-7b10-7e4a-9c3d-0000000000aa" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "previousStatus": "active", + "timestamp": "2026-09-10T14:30:00.123456Z" + }, + { + "action": "eol", + "actor": "operator:ops@acme.example", + "entryHash": "84e93f7ef6393b43098018bd99f3e062698c3c2e89dda3609ba72a4b58652ab0", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", + "metadata": { + "eolType": "recycled" + }, + "newStatus": "end_of_life", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "25719a9ebd7d548cdbaddc026f5c5d3d5ee76dcba0ab1de1a073d511c91e8a25", + "previousStatus": "active", + "timestamp": "2026-09-20T08:00:00Z" + } + ], + "didDocuments": { + "did:web:node.acme.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:node.acme.example#key-1" + ], + "id": "did:web:node.acme.example", + "verificationMethod": [ + { + "controller": "did:web:node.acme.example", + "id": "did:web:node.acme.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" + }, + "type": "JsonWebKey2020" + } + ] + }, + "did:web:secondlife.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:secondlife.example#key-1" + ], + "id": "did:web:secondlife.example", + "verificationMethod": [ + { + "controller": "did:web:secondlife.example", + "id": "did:web:secondlife.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "_RckOFqgx1tk-3jNYC-h2ZH96_drE8WO1wLqyDXp9hg" + }, + "type": "JsonWebKey2020" + } + ] + } + }, + "eolEvent": { + "declaredAt": "2026-09-20T08:00:00Z", + "facility": "Recycling Nord GmbH", + "type": "recycled" + }, + "fullView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.56KnYFaD2s9v01NhcheQZGIqBujqLCNrGcbnEe6ZbNV2UozvcQsgYQQKegxFqNMKQpoPeuJJW_tN5gPuhBolDA", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "cathodeMaterial": "NMC 811", + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "stateOfHealthPct": 100, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "manifest": { + "contentHashes": { + "auditEntries": "e085a133b614c68a136354d07c0dcc960f33df17c4b405bf424a8b1336b2cdfe", + "eolEvent": "92ed1fde3e99fa59df0fb158a03bc10cad0174ed486271f3b6c715b9b633d964", + "fullView": "750252051b902ddd89a447e209f7794b999f3c3c90c4edf63b5dfd356f671080", + "publicView": "1689c57aa9a6db1a2f069d88d3859ff4f389e05f12070c080265c614df6e4bc2", + "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" + }, + "coreVersion": "0.20.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.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImUwODVhMTMzYjYxNGM2OGExMzYzNTRkMDdjMGRjYzk2MGYzM2RmMTdjNGI0MDViZjQyNGE4YjEzMzZiMmNkZmUiLCJlb2xFdmVudCI6IjkyZWQxZmRlM2U5OWZhNTlkZjBmYjE1OGEwM2JjMTBjYWQwMTc0ZWQ0ODYyNzFmM2I2YzcxNWI5YjYzM2Q5NjQiLCJmdWxsVmlldyI6Ijc1MDI1MjA1MWI5MDJkZGQ4OWE0NDdlMjA5Zjc3OTRiOTk5ZjNjM2M5MGM0ZWRmNjNiNWRmZDM1NmY2NzEwODAiLCJwdWJsaWNWaWV3IjoiMTY4OWM1N2FhOWE2ZGIxYTJmMDY5ZDg4ZDM4NTlmZjRmMzg5ZTA1ZjEyMDcwYzA4MDI2NWM2MTRkZjZlNGJjMiIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.Z0D_04GOq7-C-uIJlLqSshl99lzcjFFc_v7FRmtewsJYIo4950_Ui-gvLhQgZO5JSSiXc2UTn9aDlBXRpAL8BQ", + "publicView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.RPgAGQWX_gMRUmIX3tE25prFcCCUo5Bije_ZyuQ96SqDaLiWOZUC_Dm834Z2eUzKRdSPT1Jn3frRTUs19W5IAw", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "transferChain": { + "originalOperator": { + "country": "DE", + "did": "did:web:node.acme.example", + "euOperatorId": null, + "name": "Acme Zellen GmbH", + "role": "manufacturer" + }, + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "transfers": [ + { + "certificationStatus": "approved", + "completedAt": "2026-09-10T14:30:00Z", + "fromOperator": { + "country": "DE", + "did": "did:web:node.acme.example", + "euOperatorId": null, + "name": "Acme Zellen GmbH", + "role": "manufacturer" + }, + "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.mtQCd6lEOMHAMcpsKZUFbx0vvRBS05El5_PWWXqItXxIp-lJO9VV1PILF9lTPceECXVTAgDfxCFsTdf29k3iCw", + "initiatedAt": "2026-09-10T14:00:00Z", + "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.hrv0yKqeb1DVf-X9ysNDsFOawnrocRUUaFo9ygKqitjNmxaFlj6o1Xyf6eXsyGDjOW7kBSjbrbSkbSbbsmHfCg", + "notes": "Repurposed into stationary storage.", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "reason": "repurposing", + "toOperator": { + "country": "NL", + "did": "did:web:secondlife.example", + "euOperatorId": null, + "name": "Second Life Storage BV", + "role": "repurposer" + }, + "transferId": "0199a3c2-7b10-7e4a-9c3d-0000000000aa" + } + ] + } +} \ No newline at end of file diff --git a/site/dpp-landing/public/verify/examples/09-unknown-top-level-field.json b/site/dpp-landing/public/verify/examples/09-unknown-top-level-field.json new file mode 100644 index 0000000..e1c5745 --- /dev/null +++ b/site/dpp-landing/public/verify/examples/09-unknown-top-level-field.json @@ -0,0 +1,224 @@ +{ + "auditEntries": [ + { + "action": "create", + "actor": "operator:ops@acme.example", + "entryHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000001", + "metadata": null, + "newStatus": "draft", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "", + "previousStatus": null, + "timestamp": "2026-09-01T09:00:00Z" + }, + { + "action": "publish", + "actor": "operator:ops@acme.example", + "entryHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", + "metadata": { + "schemaVersion": "2.7.0" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "871ae92990314e9023afb2465f131aaf9deda025770ed57893feb45c0d49913d", + "previousStatus": "draft", + "timestamp": "2026-09-01T09:05:00.250Z" + }, + { + "action": "transfer_completed", + "actor": "operator:ops@acme.example", + "entryHash": "25719a9ebd7d548cdbaddc026f5c5d3d5ee76dcba0ab1de1a073d511c91e8a25", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", + "metadata": { + "transferId": "0199a3c2-7b10-7e4a-9c3d-0000000000aa" + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "ffdbcc02599bfe4cb32133206de2bca025c1c76cbab824dcb27ded330e18fb9f", + "previousStatus": "active", + "timestamp": "2026-09-10T14:30:00.123456Z" + }, + { + "action": "eol", + "actor": "operator:ops@acme.example", + "entryHash": "84e93f7ef6393b43098018bd99f3e062698c3c2e89dda3609ba72a4b58652ab0", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", + "metadata": { + "eolType": "recycled" + }, + "newStatus": "end_of_life", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "25719a9ebd7d548cdbaddc026f5c5d3d5ee76dcba0ab1de1a073d511c91e8a25", + "previousStatus": "active", + "timestamp": "2026-09-20T08:00:00Z" + } + ], + "didDocuments": { + "did:web:node.acme.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:node.acme.example#key-1" + ], + "id": "did:web:node.acme.example", + "verificationMethod": [ + { + "controller": "did:web:node.acme.example", + "id": "did:web:node.acme.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" + }, + "type": "JsonWebKey2020" + } + ] + }, + "did:web:secondlife.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:secondlife.example#key-1" + ], + "id": "did:web:secondlife.example", + "verificationMethod": [ + { + "controller": "did:web:secondlife.example", + "id": "did:web:secondlife.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "_RckOFqgx1tk-3jNYC-h2ZH96_drE8WO1wLqyDXp9hg" + }, + "type": "JsonWebKey2020" + } + ] + } + }, + "eolEvent": { + "declaredAt": "2026-09-20T08:00:00Z", + "facility": "Recycling Nord GmbH", + "type": "recycled" + }, + "fullView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.56KnYFaD2s9v01NhcheQZGIqBujqLCNrGcbnEe6ZbNV2UozvcQsgYQQKegxFqNMKQpoPeuJJW_tN5gPuhBolDA", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "cathodeMaterial": "NMC 811", + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "stateOfHealthPct": 100, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "manifest": { + "contentHashes": { + "auditEntries": "e085a133b614c68a136354d07c0dcc960f33df17c4b405bf424a8b1336b2cdfe", + "eolEvent": "92ed1fde3e99fa59df0fb158a03bc10cad0174ed486271f3b6c715b9b633d964", + "fullView": "750252051b902ddd89a447e209f7794b999f3c3c90c4edf63b5dfd356f671080", + "publicView": "1689c57aa9a6db1a2f069d88d3859ff4f389e05f12070c080265c614df6e4bc2", + "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" + }, + "coreVersion": "0.20.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.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImUwODVhMTMzYjYxNGM2OGExMzYzNTRkMDdjMGRjYzk2MGYzM2RmMTdjNGI0MDViZjQyNGE4YjEzMzZiMmNkZmUiLCJlb2xFdmVudCI6IjkyZWQxZmRlM2U5OWZhNTlkZjBmYjE1OGEwM2JjMTBjYWQwMTc0ZWQ0ODYyNzFmM2I2YzcxNWI5YjYzM2Q5NjQiLCJmdWxsVmlldyI6Ijc1MDI1MjA1MWI5MDJkZGQ4OWE0NDdlMjA5Zjc3OTRiOTk5ZjNjM2M5MGM0ZWRmNjNiNWRmZDM1NmY2NzEwODAiLCJwdWJsaWNWaWV3IjoiMTY4OWM1N2FhOWE2ZGIxYTJmMDY5ZDg4ZDM4NTlmZjRmMzg5ZTA1ZjEyMDcwYzA4MDI2NWM2MTRkZjZlNGJjMiIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.Z0D_04GOq7-C-uIJlLqSshl99lzcjFFc_v7FRmtewsJYIo4950_Ui-gvLhQgZO5JSSiXc2UTn9aDlBXRpAL8BQ", + "publicView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjV9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsImlzc3VlciI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJvcGVyYXRvciI6eyJjaXR5IjoiTcO8bmNoZW4iLCJjb3VudHJ5IjoiREUiLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCJ9LCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicHVibGlzaGVkQXQiOiIyMDI2LTA5LTAxVDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjcuMCIsInN0YXR1cyI6ImFjdGl2ZSJ9.RPgAGQWX_gMRUmIX3tE25prFcCCUo5Bije_ZyuQ96SqDaLiWOZUC_Dm834Z2eUzKRdSPT1Jn3frRTUs19W5IAw", + "payload": { + "content": { + "B": 2, + "b": 1, + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryWeightKg": 12.4, + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "ratedCapacityAh": 20, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "issuer": "did:web:node.acme.example", + "operator": { + "city": "München", + "country": "DE", + "name": "Acme Zellen GmbH" + }, + "productGroup": "battery", + "publishedAt": "2026-09-01T09:05:00Z", + "schemaVersion": "2.7.0", + "status": "active" + } + }, + "transferChain": { + "originalOperator": { + "country": "DE", + "did": "did:web:node.acme.example", + "euOperatorId": null, + "name": "Acme Zellen GmbH", + "role": "manufacturer" + }, + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "transfers": [ + { + "completedAt": "2026-09-10T14:30:00Z", + "fromOperator": { + "country": "DE", + "did": "did:web:node.acme.example", + "euOperatorId": null, + "name": "Acme Zellen GmbH", + "role": "manufacturer" + }, + "fromSignature": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJmcm9tT3BlcmF0b3IiOnsiY291bnRyeSI6IkRFIiwiZGlkIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsImV1T3BlcmF0b3JJZCI6bnVsbCwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgiLCJyb2xlIjoibWFudWZhY3R1cmVyIn0sImluaXRpYXRlZEF0IjoiMjAyNi0wOS0xMFQxNDowMDowMFoiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwicmVhc29uIjoicmVwdXJwb3NpbmciLCJ0b09wZXJhdG9yIjp7ImNvdW50cnkiOiJOTCIsImRpZCI6ImRpZDp3ZWI6c2Vjb25kbGlmZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiU2Vjb25kIExpZmUgU3RvcmFnZSBCViIsInJvbGUiOiJyZXB1cnBvc2VyIn0sInRyYW5zZmVySWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0wMDAwMDAwMDAwYWEifQ.mtQCd6lEOMHAMcpsKZUFbx0vvRBS05El5_PWWXqItXxIp-lJO9VV1PILF9lTPceECXVTAgDfxCFsTdf29k3iCw", + "initiatedAt": "2026-09-10T14:00:00Z", + "nodeAcceptanceAttestation": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJhdHRlc3RzIjoiYWNjZXB0YW5jZSIsImZyb21PcGVyYXRvciI6eyJjb3VudHJ5IjoiREUiLCJkaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwiZXVPcGVyYXRvcklkIjpudWxsLCJuYW1lIjoiQWNtZSBaZWxsZW4gR21iSCIsInJvbGUiOiJtYW51ZmFjdHVyZXIifSwiaW5pdGlhdGVkQXQiOiIyMDI2LTA5LTEwVDE0OjAwOjAwWiIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAiLCJyZWFzb24iOiJyZXB1cnBvc2luZyIsInRvT3BlcmF0b3IiOnsiY291bnRyeSI6Ik5MIiwiZGlkIjoiZGlkOndlYjpzZWNvbmRsaWZlLmV4YW1wbGUiLCJldU9wZXJhdG9ySWQiOm51bGwsIm5hbWUiOiJTZWNvbmQgTGlmZSBTdG9yYWdlIEJWIiwicm9sZSI6InJlcHVycG9zZXIifSwidHJhbnNmZXJJZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTAwMDAwMDAwMDBhYSJ9.hrv0yKqeb1DVf-X9ysNDsFOawnrocRUUaFo9ygKqitjNmxaFlj6o1Xyf6eXsyGDjOW7kBSjbrbSkbSbbsmHfCg", + "notes": "Repurposed into stationary storage.", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "reason": "repurposing", + "toOperator": { + "country": "NL", + "did": "did:web:secondlife.example", + "euOperatorId": null, + "name": "Second Life Storage BV", + "role": "repurposer" + }, + "transferId": "0199a3c2-7b10-7e4a-9c3d-0000000000aa" + } + ] + }, + "verifiedBy": "odal-node" +} \ No newline at end of file diff --git a/site/dpp-landing/public/verify/examples/10-not-json.txt b/site/dpp-landing/public/verify/examples/10-not-json.txt new file mode 100644 index 0000000..f035a97 --- /dev/null +++ b/site/dpp-landing/public/verify/examples/10-not-json.txt @@ -0,0 +1 @@ +This is not an evidence dossier. diff --git a/site/dpp-landing/src/components/Footer.astro b/site/dpp-landing/src/components/Footer.astro index 1c6d363..220a865 100644 --- a/site/dpp-landing/src/components/Footer.astro +++ b/site/dpp-landing/src/components/Footer.astro @@ -30,6 +30,7 @@ const year = new Date().getFullYear();
  • Lifecycle
  • Visibility
  • Regulations
  • +
  • Verify
  • Build on Odal
  • Roadmap
  • diff --git a/site/dpp-landing/src/components/Nav.astro b/site/dpp-landing/src/components/Nav.astro index 80ae50f..47ffa6d 100644 --- a/site/dpp-landing/src/components/Nav.astro +++ b/site/dpp-landing/src/components/Nav.astro @@ -42,6 +42,12 @@ const explore = [ line: "The EU acts behind the passports, and when each applies", icon: '', }, + { + href: "/verify", + label: "Verify", + line: "Check a passport's proof file in your browser", + icon: '', + }, { href: "/build-on-odal", label: "Build on Odal", diff --git a/site/dpp-landing/src/lib/verify/canonical.ts b/site/dpp-landing/src/lib/verify/canonical.ts new file mode 100644 index 0000000..347f553 --- /dev/null +++ b/site/dpp-landing/src/lib/verify/canonical.ts @@ -0,0 +1,75 @@ +// canonical.ts — the byte-level primitives the dossier verifier rests on. +// +// Each mirrors one function in the Rust code the node runs, and must produce +// the same bytes, or a genuine dossier would fail here while passing there: +// +// jcs() dpp_crypto::jws::canonicalize / dpp_rules content_hash: +// RFC 8785. Keys sorted by UTF-16 code unit, no whitespace, +// numbers in ECMAScript Number#toString form. JavaScript's +// own JSON.stringify already emits RFC 8785 strings and +// numbers, which is why RFC 8785 chose them. +// b64urlDecode() base64::URL_SAFE_NO_PAD: no padding, and trailing bits +// must be zero; anything else is an error, not a guess. +// sha256Hex() sha2::Sha256, hex-encoded lowercase. +// +// Golden-tested against dossiers the engine itself produced; see +// tests/verify.test.ts. + +export type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; + +/** RFC 8785 canonical JSON text. Throws on a non-finite number, as JCS does. */ +export function jcs(value: Json): string { + if (value === null || typeof value === "boolean" || typeof value === "string") { + return JSON.stringify(value); + } + if (typeof value === "number") { + if (!Number.isFinite(value)) throw new Error("JCS cannot represent a non-finite number"); + return JSON.stringify(value); + } + if (Array.isArray(value)) return `[${value.map(jcs).join(",")}]`; + // Default sort compares UTF-16 code units, which is exactly RFC 8785 §3.2.3. + const keys = Object.keys(value).sort(); + return `{${keys.map((k) => `${JSON.stringify(k)}:${jcs(value[k] as Json)}`).join(",")}}`; +} + +const encoder = new TextEncoder(); +export const utf8 = (text: string) => encoder.encode(text); + +const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; +const LOOKUP = new Map([...ALPHABET].map((c, i) => [c, i])); + +/** Strict unpadded base64url, as the `base64` crate's URL_SAFE_NO_PAD decodes it. */ +export function b64urlDecode(text: string): Uint8Array { + if (text.length % 4 === 1) throw new Error("invalid base64url length"); + const out: number[] = []; + let buffer = 0; + let bits = 0; + for (const ch of text) { + const v = LOOKUP.get(ch); + if (v === undefined) throw new Error(`invalid base64url character ${JSON.stringify(ch)}`); + buffer = (buffer << 6) | v; + bits += 6; + if (bits >= 8) { + bits -= 8; + out.push((buffer >> bits) & 0xff); + } + } + // Leftover bits must be zero padding; a non-zero remainder is a different + // encoding of the same bytes, and the Rust decoder refuses it. + if (bits > 0 && (buffer & ((1 << bits) - 1)) !== 0) throw new Error("invalid base64url trailing bits"); + return new Uint8Array(out); +} + +export async function sha256Hex(bytes: Uint8Array): Promise { + const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes as BufferSource)); + return [...digest].map((b) => b.toString(16).padStart(2, "0")).join(""); +} + +/** Hex SHA-256 of a value's canonical bytes: `dpp_rules::canonical::content_hash`. */ +export const contentHash = (value: Json) => sha256Hex(utf8(jcs(value))); + +export function bytesEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; + return true; +} diff --git a/site/dpp-landing/src/lib/verify/dossier.ts b/site/dpp-landing/src/lib/verify/dossier.ts new file mode 100644 index 0000000..ae07c19 --- /dev/null +++ b/site/dpp-landing/src/lib/verify/dossier.ts @@ -0,0 +1,493 @@ +// dossier.ts — read an evidence dossier exactly as the node's Rust types do. +// +// The node deserialises a dossier into typed structs (dpp-types `DossierV1`, +// dpp-domain `TransferRecord`, …) and re-serialises them for two of its +// checks. Verifying the same file here means reproducing that round trip: +// +// - Strict types (`deny_unknown_fields`) refuse an unknown key: the dossier, +// its manifest, both signed views and every audit entry. That is a +// malformed file, not a failed check. +// - Tolerant types silently drop an unknown key: the transfer chain, each +// transfer, each operator. The drop is what `input_fidelity` exists to +// catch, so this module drops it too. +// - Re-serialising normalises: UUIDs become lowercase hyphenated, timestamps +// become chrono's UTC form, an absent `Option` without `skip_serializing_if` +// becomes `null`, one with it disappears. +// +// Anything this module cannot reproduce exactly (an integer beyond 2^53, +// which JavaScript cannot hold) is reported as unsupported rather than +// guessed at. A verifier that is sometimes silently wrong is worse than one +// that says where its reach ends. +import type { Json } from "./canonical.ts"; + +type Obj = { [key: string]: Json }; + +export class Malformed extends Error {} +export class Unsupported extends Error {} + +// ── raw-text scan: what JSON.parse hides ───────────────────────────────────── + +type Path = (string | number)[]; + +/** + * Walk the raw text for the two things JSON.parse erases: integers too large + * to hold exactly, and duplicate keys (JSON.parse keeps the last; a Rust + * struct refuses a repeated field). + */ +function scan(text: string): { unsafeInteger: boolean; duplicates: { path: Path; key: string }[] } { + let i = 0; + let unsafeInteger = false; + const duplicates: { path: Path; key: string }[] = []; + const ws = () => { + while (i < text.length && " \t\n\r".includes(text[i])) i++; + }; + const str = (): string => { + const start = i; + i++; + while (text[i] !== '"') i += text[i] === "\\" ? 2 : 1; + i++; + return JSON.parse(text.slice(start, i)); + }; + const value = (path: Path): void => { + ws(); + const c = text[i]; + if (c === "{") { + i++; + const seen = new Set(); + ws(); + if (text[i] === "}") return void i++; + for (;;) { + ws(); + const key = str(); + if (seen.has(key)) duplicates.push({ path, key }); + seen.add(key); + ws(); + i++; // ':' + value([...path, key]); + ws(); + if (text[i++] === "}") return; + } + } + if (c === "[") { + i++; + ws(); + if (text[i] === "]") return void i++; + for (let n = 0; ; n++) { + value([...path, n]); + ws(); + if (text[i++] === "]") return; + } + } + if (c === '"') return void str(); + const m = /^-?\d+(\.\d+)?([eE][+-]?\d+)?/.exec(text.slice(i, i + 400)); + if (m) { + i += m[0].length; + if (!m[1] && !m[2]) { + const n = BigInt(m[0]); + if (n > BigInt(Number.MAX_SAFE_INTEGER) || n < -BigInt(Number.MAX_SAFE_INTEGER)) unsafeInteger = true; + } + return; + } + i += text.startsWith("true", i) ? 4 : text.startsWith("false", i) ? 5 : 4; // null + }; + value([]); + return { unsafeInteger, duplicates }; +} + +const LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? LONE_SURROGATE.test(k) || hasLoneSurrogate(x)); + return false; +} + +// ── field readers, each mirroring one serde type ───────────────────────────── + +const isObj = (v: unknown): v is Obj => typeof v === "object" && v !== null && !Array.isArray(v); + +function obj(v: Json | undefined, what: string): Obj { + if (!isObj(v)) throw new Malformed(`${what}: expected an object`); + return v; +} + +function fields(o: Obj, what: string, known: string[], strict: boolean): void { + for (const k of known.filter((k) => !k.startsWith("?"))) { + if (!(k in o)) throw new Malformed(`${what}: missing field \`${k}\``); + } + if (strict) { + const allowed = new Set(known.map((k) => k.replace(/^\?/, ""))); + for (const k of Object.keys(o)) if (!allowed.has(k)) throw new Malformed(`${what}: unknown field \`${k}\``); + } +} + +function str(v: Json | undefined, what: string): string { + if (typeof v !== "string") throw new Malformed(`${what}: expected a string`); + return v; +} + +/** `Option`: absent or null is None. */ +function optStr(v: Json | undefined, what: string): string | null { + return v === undefined || v === null ? null : str(v, what); +} + +/** `Option`: absent or null is None; anything else is kept verbatim. */ +const optValue = (v: Json | undefined): Json | null => (v === undefined || v === null ? null : v); + +const HEX = /^[0-9a-fA-F]{32}$/; + +/** `uuid::Uuid`: any form `Uuid::parse_str` accepts, re-serialised hyphenated lowercase. */ +function uuid(v: Json | undefined, what: string): string { + let s = str(v, what); + if (s.startsWith("urn:uuid:")) s = s.slice(9); + else if (s.startsWith("{") && s.endsWith("}")) s = s.slice(1, -1); + const hex = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/.test(s) + ? s.replace(/-/g, "") + : HEX.test(s) + ? s + : null; + if (!hex) throw new Malformed(`${what}: not a UUID`); + const h = hex.toLowerCase(); + return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`; +} + +const RFC3339 = /^(\d{4})-(\d{2})-(\d{2})[Tt ](\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(?:([Zz])|([+-])(\d{2}):(\d{2}))$/; + +/** + * `chrono::DateTime`: parsed with its offset, re-serialised in UTC with + * `Z` and the shortest of 0, 3, 6 or 9 fractional digits that is exact. + */ +function dateTime(v: Json | undefined, what: string): string { + const m = RFC3339.exec(str(v, what)); + if (!m) throw new Malformed(`${what}: not an RFC 3339 timestamp`); + const [, y, mo, d, h, mi, s, frac = "", z, sign, oh, om] = m; + // Built field by field so years below 100 are not read as 19xx, and checked + // back so an impossible date (30 February) is refused, as chrono refuses it, + // instead of rolling over. + const local = new Date(0); + local.setUTCFullYear(Number(y), Number(mo) - 1, Number(d)); + local.setUTCHours(Number(h), Number(mi), Number(s), 0); + const valid = + local.getUTCFullYear() === Number(y) && local.getUTCMonth() === Number(mo) - 1 && local.getUTCDate() === Number(d) && + local.getUTCHours() === Number(h) && local.getUTCMinutes() === Number(mi) && local.getUTCSeconds() === Number(s) && + (z || (Number(oh) < 24 && Number(om) < 60)); + if (!valid) throw new Malformed(`${what}: not a valid date`); + const offset = z ? 0 : (sign === "-" ? -1 : 1) * (Number(oh) * 60 + Number(om)); + const t = new Date(local.getTime() - offset * 60_000); + const nanos = Number(frac.slice(0, 9).padEnd(9, "0")); + const fraction = + nanos === 0 ? "" : + nanos % 1_000_000 === 0 ? `.${String(nanos / 1_000_000).padStart(3, "0")}` : + nanos % 1_000 === 0 ? `.${String(nanos / 1_000).padStart(6, "0")}` : + `.${String(nanos).padStart(9, "0")}`; + const p = (n: number, w = 2) => String(n).padStart(w, "0"); + return `${p(t.getUTCFullYear(), 4)}-${p(t.getUTCMonth() + 1)}-${p(t.getUTCDate())}T${p(t.getUTCHours())}:${p(t.getUTCMinutes())}:${p(t.getUTCSeconds())}${fraction}Z`; +} + +function optDateTime(v: Json | undefined, what: string): string | null { + return v === undefined || v === null ? null : dateTime(v, what); +} + +function oneOf(v: Json | undefined, what: string, allowed: readonly string[]): string { + const s = str(v, what); + if (!allowed.includes(s)) throw new Malformed(`${what}: unknown variant \`${s}\``); + return s; +} + +// ── the typed dossier ──────────────────────────────────────────────────────── + +const ROLES = [ + "manufacturer", "importer", "distributor", "authorisedRepresentative", "fulfilmentServiceProvider", + "remanufacturer", "repurposer", "preparerForReuse", "repairer", "recycler", +] as const; +const REASONS = [ + "sale", "return", "remanufacturing", "repurposing", "preparationForReuse", + "preparationForRepurposing", "wasteHandover", "import", "insolvencySuccession", +] as const; + +export type Manifest = { + formatVersion: string; + passportId: string; + issuerDid: string; + createdAt: string; + nodeVersion: string; + coreVersion: string; + rulesetVersion: string | null; + contentHashes: Record; +}; +export type SignedLayer = { payload: Json; jws: string }; +export type AuditEntry = { + id: string; + passportId: string; + actor: string; + action: string; + previousStatus: string | null; + newStatus: string | null; + metadata: Json | null; + timestamp: string; + requestId: string | null; + prevHash: string | null; + entryHash: string | null; +}; +export type Operator = { + did: string; + name: string; + role: string; + euOperatorId: string | null; + euOperatorIdScheme: string | null; + country: string; + registeredTradeName: string | null; + postalAddress: string | null; + electronicAddress: string | null; +}; +export type Transfer = { + transferId: string; + passportId: string; + fromOperator: Operator; + toOperator: Operator; + reason: string; + fromSignature: string | null; + nodeAcceptanceAttestation: string | null; + initiatedAt: string; + completedAt: string | null; + rejectedAt: string | null; + cancelledAt: string | null; + notes: string | null; +}; +export type TransferChain = { passportId: string; originalOperator: Operator; transfers: Transfer[] }; +export type Dossier = { + manifest: Manifest; + manifestJws: string; + fullView: SignedLayer; + publicView: SignedLayer; + didDocuments: Record; + auditEntries: AuditEntry[]; + transferChain: TransferChain | null; + eolEvent: Json | null; + checkpoint: Json | null; + calcReceipts: Json[]; + componentGraph: Json | null; + qualifiedSeal: Json | null; +}; + +function manifest(v: Json | undefined): Manifest { + const o = obj(v, "manifest"); + fields(o, "manifest", ["formatVersion", "passportId", "issuerDid", "createdAt", "nodeVersion", "coreVersion", "?rulesetVersion", "contentHashes"], true); + const hashes = obj(o.contentHashes, "manifest.contentHashes"); + return { + formatVersion: str(o.formatVersion, "manifest.formatVersion"), + passportId: str(o.passportId, "manifest.passportId"), + issuerDid: str(o.issuerDid, "manifest.issuerDid"), + createdAt: dateTime(o.createdAt, "manifest.createdAt"), + nodeVersion: str(o.nodeVersion, "manifest.nodeVersion"), + coreVersion: str(o.coreVersion, "manifest.coreVersion"), + rulesetVersion: optStr(o.rulesetVersion, "manifest.rulesetVersion"), + contentHashes: Object.fromEntries(Object.entries(hashes).map(([k, h]) => [k, str(h, `contentHashes.${k}`)])), + }; +} + +function signedLayer(v: Json | undefined, what: string): SignedLayer { + const o = obj(v, what); + fields(o, what, ["payload", "jws"], true); + return { payload: o.payload as Json, jws: str(o.jws, `${what}.jws`) }; +} + +function auditEntry(v: Json, n: number): AuditEntry { + const w = `auditEntries[${n}]`; + const o = obj(v, w); + // previousStatus, newStatus and metadata are `Option`s without a default: + // serde still reads a missing one as None, so they are optional to read. + fields(o, w, ["id", "passportId", "actor", "action", "?previousStatus", "?newStatus", "?metadata", "timestamp", "?requestId", "?prevHash", "?entryHash"], true); + return { + id: uuid(o.id, `${w}.id`), + passportId: str(o.passportId, `${w}.passportId`), + actor: str(o.actor, `${w}.actor`), + action: str(o.action, `${w}.action`), + previousStatus: optStr(o.previousStatus, `${w}.previousStatus`), + newStatus: optStr(o.newStatus, `${w}.newStatus`), + metadata: optValue(o.metadata), + timestamp: dateTime(o.timestamp, `${w}.timestamp`), + requestId: optStr(o.requestId, `${w}.requestId`), + prevHash: optStr(o.prevHash, `${w}.prevHash`), + entryHash: optStr(o.entryHash, `${w}.entryHash`), + }; +} + +function operator(v: Json | undefined, w: string): Operator { + const o = obj(v, w); + fields(o, w, ["did", "name", "role", "?euOperatorId", "country"], false); + return { + did: str(o.did, `${w}.did`), + name: str(o.name, `${w}.name`), + role: oneOf(o.role, `${w}.role`, ROLES), + euOperatorId: optStr(o.euOperatorId, `${w}.euOperatorId`), + euOperatorIdScheme: optStr(o.euOperatorIdScheme, `${w}.euOperatorIdScheme`), + country: str(o.country, `${w}.country`), + registeredTradeName: optStr(o.registeredTradeName, `${w}.registeredTradeName`), + postalAddress: optStr(o.postalAddress, `${w}.postalAddress`), + electronicAddress: optStr(o.electronicAddress, `${w}.electronicAddress`), + }; +} + +function transfer(v: Json, n: number): Transfer { + const w = `transferChain.transfers[${n}]`; + const o = obj(v, w); + fields(o, w, ["transferId", "passportId", "fromOperator", "toOperator", "reason", "initiatedAt"], false); + // `toSignature` is the serde alias the attestation was once stored under. + if ("toSignature" in o && "nodeAcceptanceAttestation" in o) { + throw new Malformed(`${w}: duplicate field \`nodeAcceptanceAttestation\``); + } + return { + transferId: uuid(o.transferId, `${w}.transferId`), + passportId: uuid(o.passportId, `${w}.passportId`), + fromOperator: operator(o.fromOperator, `${w}.fromOperator`), + toOperator: operator(o.toOperator, `${w}.toOperator`), + reason: oneOf(o.reason, `${w}.reason`, REASONS), + fromSignature: optStr(o.fromSignature, `${w}.fromSignature`), + nodeAcceptanceAttestation: optStr(o.nodeAcceptanceAttestation ?? o.toSignature, `${w}.nodeAcceptanceAttestation`), + initiatedAt: dateTime(o.initiatedAt, `${w}.initiatedAt`), + completedAt: optDateTime(o.completedAt, `${w}.completedAt`), + rejectedAt: optDateTime(o.rejectedAt, `${w}.rejectedAt`), + cancelledAt: optDateTime(o.cancelledAt, `${w}.cancelledAt`), + notes: optStr(o.notes, `${w}.notes`), + }; +} + +function transferChain(v: Json | undefined): TransferChain | null { + if (v === undefined || v === null) return null; + const o = obj(v, "transferChain"); + fields(o, "transferChain", ["passportId", "originalOperator", "transfers"], false); + if (!Array.isArray(o.transfers)) throw new Malformed("transferChain.transfers: expected an array"); + return { + passportId: uuid(o.passportId, "transferChain.passportId"), + originalOperator: operator(o.originalOperator, "transferChain.originalOperator"), + transfers: o.transfers.map(transfer), + }; +} + +/** Struct paths where a repeated key is a serde error rather than last-wins. */ +const TYPED_PATH = [ + /^$/, /^manifest$/, /^(fullView|publicView)$/, /^auditEntries\.\d+$/, /^transferChain$/, + /^transferChain\.originalOperator$/, /^transferChain\.transfers\.\d+(\.(fromOperator|toOperator))?$/, +]; + +/** Parse raw text into the typed dossier, or throw Malformed / Unsupported. */ +export function parseDossier(text: string): { raw: Json; dossier: Dossier } { + let raw: Json; + try { + raw = JSON.parse(text); + } catch { + throw new Malformed("not valid JSON"); + } + if (hasLoneSurrogate(raw)) throw new Malformed("a string contains an unpaired surrogate"); + const { unsafeInteger, duplicates } = scan(text); + const typedDup = duplicates.find((d) => TYPED_PATH.some((re) => re.test(d.path.join(".")))); + if (typedDup) throw new Malformed(`duplicate field \`${typedDup.key}\``); + if (unsafeInteger) { + throw new Unsupported("the file contains an integer larger than a browser can hold exactly, so its canonical form cannot be reproduced here"); + } + + const o = obj(raw, "dossier"); + fields(o, "dossier", ["manifest", "manifestJws", "fullView", "publicView", "didDocuments", "auditEntries", "?transferChain", "?eolEvent", "?checkpoint", "?calcReceipts", "?componentGraph", "?qualifiedSeal"], true); + if (!Array.isArray(o.auditEntries)) throw new Malformed("auditEntries: expected an array"); + if ("calcReceipts" in o && !Array.isArray(o.calcReceipts)) throw new Malformed("calcReceipts: expected an array"); + + return { + raw, + dossier: { + manifest: manifest(o.manifest), + manifestJws: str(o.manifestJws, "manifestJws"), + fullView: signedLayer(o.fullView, "fullView"), + publicView: signedLayer(o.publicView, "publicView"), + didDocuments: obj(o.didDocuments, "didDocuments"), + auditEntries: o.auditEntries.map(auditEntry), + transferChain: transferChain(o.transferChain), + eolEvent: optValue(o.eolEvent), + checkpoint: optValue(o.checkpoint), + calcReceipts: (o.calcReceipts as Json[] | undefined) ?? [], + componentGraph: optValue(o.componentGraph), + qualifiedSeal: optValue(o.qualifiedSeal), + }, + }; +} + +// ── re-serialisation: serde_json::to_value of each type ────────────────────── + +const some = (key: string, v: T | null): Obj => (v === null ? {} : { [key]: v }); + +export const manifestValue = (m: Manifest): Obj => ({ + formatVersion: m.formatVersion, + passportId: m.passportId, + issuerDid: m.issuerDid, + createdAt: m.createdAt, + nodeVersion: m.nodeVersion, + coreVersion: m.coreVersion, + ...some("rulesetVersion", m.rulesetVersion), + contentHashes: m.contentHashes, +}); + +export const auditValue = (e: AuditEntry): Obj => ({ + id: e.id, + passportId: e.passportId, + actor: e.actor, + action: e.action, + previousStatus: e.previousStatus, + newStatus: e.newStatus, + metadata: e.metadata, + timestamp: e.timestamp, + ...some("requestId", e.requestId), + ...some("prevHash", e.prevHash), + ...some("entryHash", e.entryHash), +}); + +const operatorValue = (p: Operator): Obj => ({ + did: p.did, + name: p.name, + role: p.role, + euOperatorId: p.euOperatorId, + ...some("euOperatorIdScheme", p.euOperatorIdScheme), + country: p.country, + ...some("registeredTradeName", p.registeredTradeName), + ...some("postalAddress", p.postalAddress), + ...some("electronicAddress", p.electronicAddress), +}); + +export const operatorJson = operatorValue; + +const transferValue = (t: Transfer): Obj => ({ + transferId: t.transferId, + passportId: t.passportId, + fromOperator: operatorValue(t.fromOperator), + toOperator: operatorValue(t.toOperator), + reason: t.reason, + fromSignature: t.fromSignature, + nodeAcceptanceAttestation: t.nodeAcceptanceAttestation, + initiatedAt: t.initiatedAt, + completedAt: t.completedAt, + ...some("rejectedAt", t.rejectedAt), + ...some("cancelledAt", t.cancelledAt), + notes: t.notes, +}); + +export const chainValue = (c: TransferChain): Obj => ({ + passportId: c.passportId, + originalOperator: operatorValue(c.originalOperator), + transfers: c.transfers.map(transferValue), +}); + +export const dossierValue = (d: Dossier): Obj => ({ + manifest: manifestValue(d.manifest), + manifestJws: d.manifestJws, + fullView: { payload: d.fullView.payload, jws: d.fullView.jws }, + publicView: { payload: d.publicView.payload, jws: d.publicView.jws }, + didDocuments: d.didDocuments, + auditEntries: d.auditEntries.map(auditValue), + ...some("transferChain", d.transferChain && chainValue(d.transferChain)), + ...some("eolEvent", d.eolEvent), + ...some("checkpoint", d.checkpoint), + ...(d.calcReceipts.length ? { calcReceipts: d.calcReceipts } : {}), + ...some("componentGraph", d.componentGraph), + ...some("qualifiedSeal", d.qualifiedSeal), +}); diff --git a/site/dpp-landing/src/lib/verify/verify.ts b/site/dpp-landing/src/lib/verify/verify.ts new file mode 100644 index 0000000..1a4e84e --- /dev/null +++ b/site/dpp-landing/src/lib/verify/verify.ts @@ -0,0 +1,333 @@ +// verify.ts — the browser twin of dpp-vault's `verify_dossier_json`. +// +// Runs the node's checks in the node's order, each independently, so one +// tamper flips the check it touches and not the rest. Nothing is fetched and +// nothing is sent: every key comes from the DID documents embedded in the +// file, exactly as the node's own offline verifier trusts them. +// +// Two deliberate differences from the node, both reported, never hidden: +// +// - `qualified_seal` is not opened. The node reads a CAdES seal with a seal +// inspector; the browser has none, which the node itself reports as +// "not opened" rather than failed. +// - A file the browser cannot reproduce byte for byte (an integer beyond +// 2^53) is refused as unsupported instead of checked approximately. +// +// Golden-tested against verdicts the engine's own verifier produced on +// dossiers the engine's own types built: tests/verify.test.ts. +import { b64urlDecode, bytesEqual, contentHash, jcs, sha256Hex, utf8, type Json } from "./canonical.ts"; +import { + Malformed, + Unsupported, + auditValue, + chainValue, + dossierValue, + manifestValue, + parseDossier, + operatorJson, + type Dossier, + type Transfer, +} from "./dossier.ts"; + +export type CheckName = + | "manifest_signature" + | "content_integrity" + | "full_view_signature" + | "public_view_signature" + | "audit_chain" + | "transfer_chain" + | "checkpoint" + | "calc_receipts" + | "component_graph" + | "qualified_seal" + | "input_fidelity"; + +export type CheckStatus = { status: "pass" } | { status: "fail" | "absent"; detail: string }; +export type Check = { name: CheckName } & CheckStatus; + +export type Outcome = + | { kind: "malformed"; message: string } + | { kind: "unsupported"; message: string } + | { kind: "report"; exit: 0 | 1; checks: Check[]; dossier: Dossier }; + +const pass = (): CheckStatus => ({ status: "pass" }); +const fail = (detail: string): CheckStatus => ({ status: "fail", detail }); +const absent = (detail: string): CheckStatus => ({ status: "absent", detail }); + +type Obj = { [key: string]: Json }; +const isObj = (v: unknown): v is Obj => typeof v === "object" && v !== null && !Array.isArray(v); + +// ── JWS: dpp-crypto's verifier and key extraction ──────────────────────────── + +/** `jws.splitn(3, '.')`: a fourth dot stays inside the signature segment. */ +function splitn3(jws: string): string[] { + const a = jws.indexOf("."); + if (a < 0) return [jws]; + const b = jws.indexOf(".", a + 1); + if (b < 0) return [jws.slice(0, a), jws.slice(a + 1)]; + return [jws.slice(0, a), jws.slice(a + 1, b), jws.slice(b + 1)]; +} + +function header(segment: string): Obj | null { + try { + const h = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(b64urlDecode(segment))); + return isObj(h) ? h : null; + } catch { + return null; + } +} + +/** `verify_jws`: false for a wrong shape or algorithm; throws on malformed bytes. */ +async function verifyJws(jws: string, publicKeyB64: string): Promise { + const parts = splitn3(jws); + if (parts.length !== 3) return false; + // Pinned, not negotiated: anything but EdDSA (including "none") is a failure. + if (header(parts[0])?.alg !== "EdDSA") return false; + const sig = b64urlDecode(parts[2]); + if (sig.length !== 64) throw new Error("Ed25519 signature must be 64 bytes"); + const raw = b64urlDecode(publicKeyB64); + if (raw.length !== 32) throw new Error("Ed25519 public key must be 32 bytes"); + let key: CryptoKey; + try { + key = await crypto.subtle.importKey("raw", raw as BufferSource, { name: "Ed25519" }, false, ["verify"]); + } catch { + throw new Error("invalid key"); + } + return crypto.subtle.verify({ name: "Ed25519" }, key, sig as BufferSource, utf8(`${parts[0]}.${parts[1]}`)); +} + +/** `verify_jws_content`: the signature holds AND covers exactly these canonical bytes. */ +async function verifyJwsContent(jws: string, key: string, expected: Json): Promise { + if (!(await verifyJws(jws, key))) return false; + const segment = jws.split(".")[1]; + if (segment === undefined) throw new Error("JWS has no payload segment"); + return bytesEqual(b64urlDecode(segment), utf8(jcs(expected))); +} + +function assertionIds(doc: Obj): string[] { + const am = doc.assertionMethod; + return Array.isArray(am) ? am.filter((x): x is string => typeof x === "string") : []; +} + +function ed25519X(jwk: Json | undefined): string | null { + if (!isObj(jwk) || jwk.kty !== "OKP" || jwk.crv !== "Ed25519" || typeof jwk.x !== "string") return null; + return jwk.x; +} + +/** Assertion-authorised verification methods, in document order. */ +function authorisedKeys(doc: Json): string[] { + if (!isObj(doc) || !Array.isArray(doc.verificationMethod)) return []; + const ids = assertionIds(doc); + return doc.verificationMethod.flatMap((vm) => { + if (!isObj(vm) || typeof vm.id !== "string" || !ids.includes(vm.id)) return []; + const x = ed25519X(vm.publicKeyJwk); + return x ? [x] : []; + }); +} + +/** + * `resolve_public_key`: a `kid` must resolve by SHA-256 fingerprint (which + * reaches rotated-out keys still listed); only a JWS with no `kid` at all falls + * back to the primary key. A `kid` that resolves to nothing is `null`, never a + * silent substitution. + */ +async function resolvePublicKey(jws: string, doc: Json): Promise { + const kid = header(jws.split(".")[0])?.kid; + const keys = authorisedKeys(doc); + if (typeof kid !== "string") return keys[0] ?? null; + for (const x of keys) { + try { + if ((await sha256Hex(b64urlDecode(x))) === kid) return x; + } catch { + /* an undecodable key is skipped, as the Rust `?` skips it */ + } + } + return null; +} + +async function signatureCheck(jws: string, key: string | null, issuer: string, payload: Json, wrong: string): Promise { + if (!key) return fail(`no DID document available for issuer ${issuer}`); + try { + return (await verifyJwsContent(jws, key, payload)) ? pass() : fail(wrong); + } catch (e) { + return fail(`malformed signature: ${(e as Error).message}`); + } +} + +// ── the chains ─────────────────────────────────────────────────────────────── + +async function auditChain(d: Dossier): Promise { + let expected = ""; + for (const [index, e] of d.auditEntries.entries()) { + const stored = e.prevHash ?? ""; + if (stored !== expected) { + return fail(`broken at entry ${index}: prev_hash link broken: stored ${JSON.stringify(stored)}, expected ${JSON.stringify(expected)}`); + } + const recomputed = await contentHash({ + id: e.id, + passportId: e.passportId, + actor: e.actor, + action: e.action, + previousStatus: e.previousStatus, + newStatus: e.newStatus, + metadata: e.metadata, + timestamp: e.timestamp, + prevHash: expected, + }); + if ((e.entryHash ?? "") !== recomputed) return fail(`broken at entry ${index}: entry_hash mismatch — content tampered`); + expected = recomputed; + } + return pass(); +} + +const signingPayload = (t: Transfer): Json => ({ + transferId: t.transferId, + passportId: t.passportId, + fromOperator: operatorJson(t.fromOperator), + toOperator: operatorJson(t.toOperator), + reason: t.reason, + initiatedAt: t.initiatedAt, +}); + +const acceptancePayload = (t: Transfer): Json => ({ attests: "acceptance", ...(signingPayload(t) as Obj) }); + +async function transferSignature(did: string, jws: string, payload: Json, docs: Obj): Promise { + const doc = docs[did]; + if (doc === undefined) return `no DID document available for ${did} — cannot verify`; + const key = await resolvePublicKey(jws, doc); + if (!key) return `no usable assertion key found in DID document for ${did}`; + try { + return (await verifyJwsContent(jws, key, payload)) + ? null + : `signature does not verify against ${did}'s key, or covers different content than the transfer terms`; + } catch (e) { + return `malformed signature: ${(e as Error).message}`; + } +} + +async function transferChain(d: Dossier): Promise { + if (!d.transferChain) return absent("no transfer chain on this passport"); + for (const [index, t] of d.transferChain.transfers.entries()) { + const completed = t.completedAt !== null && t.rejectedAt === null && t.cancelledAt === null; + if (t.fromSignature !== null) { + const issue = await transferSignature(t.fromOperator.did, t.fromSignature, signingPayload(t), d.didDocuments); + if (issue) return fail(`broken at transfer ${index}: From(${JSON.stringify(issue)})`); + } else if (completed) { + return fail(`broken at transfer ${index}: From("completed transfer is missing the from-operator signature")`); + } + // The acceptance is the hosting node's attestation, so it is checked + // against the dossier's issuer, never against the receiving operator. + if (t.nodeAcceptanceAttestation !== null) { + const issue = await transferSignature(d.manifest.issuerDid, t.nodeAcceptanceAttestation, acceptancePayload(t), d.didDocuments); + if (issue) return fail(`broken at transfer ${index}: Acceptance(${JSON.stringify(issue)})`); + } else if (completed) { + return fail(`broken at transfer ${index}: Acceptance("completed transfer is missing the node's acceptance attestation")`); + } + } + return pass(); +} + +// ── members that carry their own report ────────────────────────────────────── + +function componentGraph(report: Json | null): CheckStatus { + if (report === null) return absent("no component graph on this passport"); + const nodes = isObj(report) && Array.isArray(report.nodes) ? report.nodes : []; + const unverified = nodes.filter((n) => isObj(n) && n.verified === false) as Obj[]; + if (!unverified.length) return pass(); + const bad = unverified.find((n) => typeof n.reason === "string" && ["hashMismatch", "cycle", "malformedRef"].includes(n.reason)); + if (bad) return fail(`component tree integrity violation at ${bad.path === undefined ? "unknown" : JSON.stringify(bad.path)}`); + return absent(`${unverified.length} component(s) could not be verified at snapshot time (unreachable or beyond walk bounds)`); +} + +async function contentHashes(d: Dossier): Promise> { + const out: Record = { + fullView: await contentHash(d.fullView.payload), + publicView: await contentHash(d.publicView.payload), + auditEntries: await contentHash(d.auditEntries.map(auditValue)), + }; + if (d.transferChain) out.transferChain = await contentHash(chainValue(d.transferChain)); + if (d.eolEvent !== null) out.eolEvent = await contentHash(d.eolEvent); + if (d.componentGraph !== null) out.componentGraph = await contentHash(d.componentGraph); + if (d.qualifiedSeal !== null) out.qualifiedSeal = await contentHash(d.qualifiedSeal); + return out; +} + +async function contentIntegrity(d: Dossier): Promise { + let recomputed: Record; + try { + recomputed = await contentHashes(d); + } catch (e) { + return fail(`cannot canonicalise dossier members: ${(e as Error).message}`); + } + const committed = d.manifest.contentHashes; + const keys = new Set([...Object.keys(recomputed), ...Object.keys(committed)]); + if ([...keys].every((k) => recomputed[k] === committed[k])) return pass(); + const mismatched = Object.keys(recomputed).sort().filter((k) => recomputed[k] !== committed[k]); + return fail(`content hash mismatch for: ${mismatched.join(", ")}`); +} + +// ── the entry point ────────────────────────────────────────────────────────── + +/** Verify a dossier's raw text. Never throws; every outcome is a value. */ +export async function verifyDossierText(text: string): Promise { + let parsed: ReturnType; + try { + parsed = parseDossier(text); + } catch (e) { + if (e instanceof Unsupported) return { kind: "unsupported", message: e.message }; + if (e instanceof Malformed) return { kind: "malformed", message: e.message }; + return { kind: "malformed", message: (e as Error).message }; + } + const { raw, dossier: d } = parsed; + const issuer = d.manifest.issuerDid; + const issuerDoc = d.didDocuments[issuer]; + const issuerKey = issuerDoc === undefined ? null : await resolvePublicKey(d.manifestJws, issuerDoc); + + const checks: Check[] = [ + { + name: "manifest_signature", + ...(await signatureCheck(d.manifestJws, issuerKey, issuer, manifestValue(d.manifest), + "manifest signature invalid, or covers different content than this manifest")), + }, + { name: "content_integrity", ...(await contentIntegrity(d)) }, + { + name: "full_view_signature", + ...(await signatureCheck(d.fullView.jws, issuerKey, issuer, d.fullView.payload, + "signature invalid, or covers different content than this payload")), + }, + { + name: "public_view_signature", + ...(await signatureCheck(d.publicView.jws, issuerKey, issuer, d.publicView.payload, + "signature invalid, or covers different content than this payload")), + }, + { name: "audit_chain", ...(await auditChain(d)) }, + { name: "transfer_chain", ...(await transferChain(d)) }, + { + name: "checkpoint", + ...(d.checkpoint === null + ? absent("checkpoint layer not yet implemented — audit chain integrity is checked, but nothing pins the chain head against third-party re-hash") + : fail("checkpoint present but this verifier build does not yet check it")), + }, + { + name: "calc_receipts", + ...(d.calcReceipts.length === 0 + ? absent("no calculation receipts — dpp-calc invocation is not yet wired (licensed factor data pending)") + : fail("calc receipts present but this verifier build does not yet check them")), + }, + { name: "component_graph", ...componentGraph(d.componentGraph) }, + { + name: "qualified_seal", + ...(d.qualifiedSeal === null + ? absent("this passport carries no qualified seal") + : absent("no seal reader was supplied, so the seal was not opened")), + }, + { + name: "input_fidelity", + ...(jcs(raw) === jcs(dossierValue(d)) + ? pass() + : fail("dossier content changed after parsing — a field was likely dropped silently (e.g. an unknown field nested inside a tolerant type such as a transfer record)")), + }, + ]; + + return { kind: "report", exit: checks.some((c) => c.status === "fail") ? 1 : 0, checks, dossier: d }; +} diff --git a/site/dpp-landing/src/pages/verify.astro b/site/dpp-landing/src/pages/verify.astro new file mode 100644 index 0000000..7c5c430 --- /dev/null +++ b/site/dpp-landing/src/pages/verify.astro @@ -0,0 +1,325 @@ +--- +// /verify — check a passport's proof file (an evidence dossier) in the browser. +// +// Everything runs on the visitor's machine: the file is read with the File API +// and checked by src/lib/verify/, which mirrors the node's own verifier and is +// tested against its verdicts on every build (tests/verify.test.ts). The page +// makes no request carrying the file, and its Content-Security-Policy +// (public/_headers) forbids any request to another origin, so that promise is +// enforced rather than stated. +// +// Content from the file is only ever written with textContent, never as HTML: +// a dossier is untrusted input. +import Base from "../layouts/Base.astro"; +import Section from "../components/Section.astro"; + +const title = "Verify a proof file | Odal Node"; +const description = + "Check a Digital Product Passport's proof file in your browser: its signatures, its history and its handovers. The file never leaves your machine."; + +const examples = [ + { file: "04-valid-full-lifecycle.json", label: "A genuine passport" }, + { file: "05-tampered-signature.json", label: "A forged signature" }, + { file: "06-tampered-audit-entry.json", label: "An edited history" }, + { file: "08-hidden-field-injection.json", label: "A hidden field" }, + { file: "10-not-json.txt", label: "Not a proof file" }, +]; +--- + + +
    +

    + A proof file holds a passport, its signatures, its full history and every + handover of responsibility. Drop one here and your browser checks it + against the keys it names. +

    +

    + The file never leaves your machine. Nothing is uploaded: + the check runs in this page, and the page is not allowed to send anything + anywhere. +

    + +
    +
    + + +
    + Or paste its contents +
    + + +
    +
    +
    + + +
    + +
    +
    + +
    +
      +
    • +

      It proves the file is intact

      +

      + Every signature is checked against the passport it covers, the + history is checked link by link, and a single changed character is + caught where it happened. +

      +
    • +
    • +

      It does not prove who holds the keys

      +

      + The file carries a copy of the issuer's identity document. To tie the + keys to a company, compare it with the one that company publishes on + its own web domain. +

      +
    • +
    • +

      A qualified seal is not opened here

      +

      + If the file carries a qualified electronic seal, the browser reports + it as not checked rather than guessing. The node's own verifier reads + seals. +

      +
    • +
    • +

      It agrees with the node, by test

      +

      + This checker mirrors the verifier inside Odal Node, and every build of + this site tests it against that verifier's own verdicts. +

      +
    • +
    +
    + + + diff --git a/site/dpp-landing/tests/verify-expected.json b/site/dpp-landing/tests/verify-expected.json new file mode 100644 index 0000000..57a9bbb --- /dev/null +++ b/site/dpp-landing/tests/verify-expected.json @@ -0,0 +1,442 @@ +{ + "01-valid-simple.json": { + "checks": [ + { + "name": "manifest_signature", + "status": "pass" + }, + { + "name": "content_integrity", + "status": "pass" + }, + { + "name": "full_view_signature", + "status": "pass" + }, + { + "name": "public_view_signature", + "status": "pass" + }, + { + "name": "audit_chain", + "status": "pass" + }, + { + "detail": "no transfer chain on this passport", + "name": "transfer_chain", + "status": "absent" + }, + { + "detail": "checkpoint layer not yet implemented — audit chain integrity is checked, but nothing pins the chain head against third-party re-hash", + "name": "checkpoint", + "status": "absent" + }, + { + "detail": "no calculation receipts — dpp-calc invocation is not yet wired (licensed factor data pending)", + "name": "calc_receipts", + "status": "absent" + }, + { + "detail": "no component graph on this passport", + "name": "component_graph", + "status": "absent" + }, + { + "detail": "this passport carries no qualified seal", + "name": "qualified_seal", + "status": "absent" + }, + { + "name": "input_fidelity", + "status": "pass" + } + ], + "exit": 0 + }, + "02-valid-with-transfer.json": { + "checks": [ + { + "name": "manifest_signature", + "status": "pass" + }, + { + "name": "content_integrity", + "status": "pass" + }, + { + "name": "full_view_signature", + "status": "pass" + }, + { + "name": "public_view_signature", + "status": "pass" + }, + { + "name": "audit_chain", + "status": "pass" + }, + { + "name": "transfer_chain", + "status": "pass" + }, + { + "detail": "checkpoint layer not yet implemented — audit chain integrity is checked, but nothing pins the chain head against third-party re-hash", + "name": "checkpoint", + "status": "absent" + }, + { + "detail": "no calculation receipts — dpp-calc invocation is not yet wired (licensed factor data pending)", + "name": "calc_receipts", + "status": "absent" + }, + { + "detail": "no component graph on this passport", + "name": "component_graph", + "status": "absent" + }, + { + "detail": "this passport carries no qualified seal", + "name": "qualified_seal", + "status": "absent" + }, + { + "name": "input_fidelity", + "status": "pass" + } + ], + "exit": 0 + }, + "03-valid-with-eol.json": { + "checks": [ + { + "name": "manifest_signature", + "status": "pass" + }, + { + "name": "content_integrity", + "status": "pass" + }, + { + "name": "full_view_signature", + "status": "pass" + }, + { + "name": "public_view_signature", + "status": "pass" + }, + { + "name": "audit_chain", + "status": "pass" + }, + { + "detail": "no transfer chain on this passport", + "name": "transfer_chain", + "status": "absent" + }, + { + "detail": "checkpoint layer not yet implemented — audit chain integrity is checked, but nothing pins the chain head against third-party re-hash", + "name": "checkpoint", + "status": "absent" + }, + { + "detail": "no calculation receipts — dpp-calc invocation is not yet wired (licensed factor data pending)", + "name": "calc_receipts", + "status": "absent" + }, + { + "detail": "no component graph on this passport", + "name": "component_graph", + "status": "absent" + }, + { + "detail": "this passport carries no qualified seal", + "name": "qualified_seal", + "status": "absent" + }, + { + "name": "input_fidelity", + "status": "pass" + } + ], + "exit": 0 + }, + "04-valid-full-lifecycle.json": { + "checks": [ + { + "name": "manifest_signature", + "status": "pass" + }, + { + "name": "content_integrity", + "status": "pass" + }, + { + "name": "full_view_signature", + "status": "pass" + }, + { + "name": "public_view_signature", + "status": "pass" + }, + { + "name": "audit_chain", + "status": "pass" + }, + { + "name": "transfer_chain", + "status": "pass" + }, + { + "detail": "checkpoint layer not yet implemented — audit chain integrity is checked, but nothing pins the chain head against third-party re-hash", + "name": "checkpoint", + "status": "absent" + }, + { + "detail": "no calculation receipts — dpp-calc invocation is not yet wired (licensed factor data pending)", + "name": "calc_receipts", + "status": "absent" + }, + { + "detail": "no component graph on this passport", + "name": "component_graph", + "status": "absent" + }, + { + "detail": "this passport carries no qualified seal", + "name": "qualified_seal", + "status": "absent" + }, + { + "name": "input_fidelity", + "status": "pass" + } + ], + "exit": 0 + }, + "05-tampered-signature.json": { + "checks": [ + { + "name": "manifest_signature", + "status": "pass" + }, + { + "name": "content_integrity", + "status": "pass" + }, + { + "name": "full_view_signature", + "status": "pass" + }, + { + "detail": "signature invalid, or covers different content than this payload", + "name": "public_view_signature", + "status": "fail" + }, + { + "name": "audit_chain", + "status": "pass" + }, + { + "name": "transfer_chain", + "status": "pass" + }, + { + "detail": "checkpoint layer not yet implemented — audit chain integrity is checked, but nothing pins the chain head against third-party re-hash", + "name": "checkpoint", + "status": "absent" + }, + { + "detail": "no calculation receipts — dpp-calc invocation is not yet wired (licensed factor data pending)", + "name": "calc_receipts", + "status": "absent" + }, + { + "detail": "no component graph on this passport", + "name": "component_graph", + "status": "absent" + }, + { + "detail": "this passport carries no qualified seal", + "name": "qualified_seal", + "status": "absent" + }, + { + "name": "input_fidelity", + "status": "pass" + } + ], + "exit": 1 + }, + "06-tampered-audit-entry.json": { + "checks": [ + { + "name": "manifest_signature", + "status": "pass" + }, + { + "detail": "content hash mismatch for: auditEntries", + "name": "content_integrity", + "status": "fail" + }, + { + "name": "full_view_signature", + "status": "pass" + }, + { + "name": "public_view_signature", + "status": "pass" + }, + { + "detail": "broken at entry 0: entry_hash mismatch — content tampered", + "name": "audit_chain", + "status": "fail" + }, + { + "name": "transfer_chain", + "status": "pass" + }, + { + "detail": "checkpoint layer not yet implemented — audit chain integrity is checked, but nothing pins the chain head against third-party re-hash", + "name": "checkpoint", + "status": "absent" + }, + { + "detail": "no calculation receipts — dpp-calc invocation is not yet wired (licensed factor data pending)", + "name": "calc_receipts", + "status": "absent" + }, + { + "detail": "no component graph on this passport", + "name": "component_graph", + "status": "absent" + }, + { + "detail": "this passport carries no qualified seal", + "name": "qualified_seal", + "status": "absent" + }, + { + "name": "input_fidelity", + "status": "pass" + } + ], + "exit": 1 + }, + "07-tampered-transfer-signature.json": { + "checks": [ + { + "name": "manifest_signature", + "status": "pass" + }, + { + "detail": "content hash mismatch for: transferChain", + "name": "content_integrity", + "status": "fail" + }, + { + "name": "full_view_signature", + "status": "pass" + }, + { + "name": "public_view_signature", + "status": "pass" + }, + { + "name": "audit_chain", + "status": "pass" + }, + { + "detail": "broken at transfer 0: Acceptance(\"signature does not verify against did:web:node.acme.example's key, or covers different content than the transfer terms\")", + "name": "transfer_chain", + "status": "fail" + }, + { + "detail": "checkpoint layer not yet implemented — audit chain integrity is checked, but nothing pins the chain head against third-party re-hash", + "name": "checkpoint", + "status": "absent" + }, + { + "detail": "no calculation receipts — dpp-calc invocation is not yet wired (licensed factor data pending)", + "name": "calc_receipts", + "status": "absent" + }, + { + "detail": "no component graph on this passport", + "name": "component_graph", + "status": "absent" + }, + { + "detail": "this passport carries no qualified seal", + "name": "qualified_seal", + "status": "absent" + }, + { + "name": "input_fidelity", + "status": "pass" + } + ], + "exit": 1 + }, + "08-hidden-field-injection.json": { + "checks": [ + { + "name": "manifest_signature", + "status": "pass" + }, + { + "name": "content_integrity", + "status": "pass" + }, + { + "name": "full_view_signature", + "status": "pass" + }, + { + "name": "public_view_signature", + "status": "pass" + }, + { + "name": "audit_chain", + "status": "pass" + }, + { + "name": "transfer_chain", + "status": "pass" + }, + { + "detail": "checkpoint layer not yet implemented — audit chain integrity is checked, but nothing pins the chain head against third-party re-hash", + "name": "checkpoint", + "status": "absent" + }, + { + "detail": "no calculation receipts — dpp-calc invocation is not yet wired (licensed factor data pending)", + "name": "calc_receipts", + "status": "absent" + }, + { + "detail": "no component graph on this passport", + "name": "component_graph", + "status": "absent" + }, + { + "detail": "this passport carries no qualified seal", + "name": "qualified_seal", + "status": "absent" + }, + { + "detail": "dossier content changed after parsing — a field was likely dropped silently (e.g. an unknown field nested inside a tolerant type such as a transfer record)", + "name": "input_fidelity", + "status": "fail" + } + ], + "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 223 column 14", + "exit": 2 + }, + "10-not-json.txt": { + "error": "not a valid dossier — malformed JSON or an unrecognised field: expected value at line 1 column 1", + "exit": 2 + } +} \ No newline at end of file diff --git a/site/dpp-landing/tests/verify.test.ts b/site/dpp-landing/tests/verify.test.ts new file mode 100644 index 0000000..56a9817 --- /dev/null +++ b/site/dpp-landing/tests/verify.test.ts @@ -0,0 +1,72 @@ +// verify.test.ts — the browser verifier against the engine's own verdicts. +// +// public/verify/examples/ holds dossiers built with dpp-engine's own types and +// Ed25519 signing; verify-expected.json holds what dpp-vault's +// `verify_dossier_json` said about each one (engine e8ec2b4, 2026-09-25). This +// test fails if the TypeScript verifier disagrees with the Rust one on the +// outcome or on any single check. A second implementation that is not held to +// the first is two verifiers that will disagree one day without anyone +// noticing; this is what notices. +// +// Run: pnpm --filter dpp-landing run test:verify +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync, readdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { verifyDossierText } from "../src/lib/verify/verify.ts"; + +type Expected = + | { exit: 0 | 1; checks: { name: string; status: "pass" | "fail" | "absent" }[] } + | { exit: 2; error: string }; + +const dir = fileURLToPath(new URL("../public/verify/examples/", import.meta.url)); +const expected: Record = JSON.parse( + readFileSync(fileURLToPath(new URL("./verify-expected.json", import.meta.url)), "utf8"), +); + +// 🚨 A loop over an empty directory passes. The corpus must be there. +const files = readdirSync(dir).sort(); +test("the golden corpus is present and fully described", () => { + assert.ok(files.length >= 10, `expected at least 10 example dossiers, found ${files.length}`); + assert.deepEqual(files, Object.keys(expected).sort()); +}); + +for (const file of files) { + test(`agrees with the engine on ${file}`, async () => { + const want = expected[file]; + const got = await verifyDossierText(readFileSync(`${dir}${file}`, "utf8")); + if (want.exit === 2) { + assert.equal(got.kind, "malformed", `the engine refused ${file} as malformed: ${want.error}`); + return; + } + assert.equal(got.kind, "report", `the engine produced a report for ${file}`); + if (got.kind !== "report") return; + assert.equal(got.exit, want.exit, "overall verdict"); + assert.deepEqual( + got.checks.map((c) => `${c.name}:${c.status}`), + want.checks.map((c) => `${c.name}:${c.status}`), + "every named check, in order", + ); + }); +} + +// The page's own claim: a single changed character in a signed member is +// caught, and caught precisely. The first occurrence of the model id is in the +// full view, so exactly the full view's signature and the content hashes fail; +// the public view, untouched, still verifies. +test("a one-character edit to a signed payload is caught, and only where it happened", async () => { + const text = readFileSync(`${dir}04-valid-full-lifecycle.json`, "utf8").replace('"ACME-LMT-48V"', '"ACME-LMT-49V"'); + const got = await verifyDossierText(text); + assert.equal(got.kind, "report"); + if (got.kind === "report") { + assert.equal(got.exit, 1); + const failed = got.checks.filter((c) => c.status === "fail").map((c) => c.name); + assert.deepEqual(failed, ["content_integrity", "full_view_signature"]); + } +}); + +test("an integer beyond 2^53 is refused, not approximated", async () => { + const text = readFileSync(`${dir}01-valid-simple.json`, "utf8").replace('"ratedCapacityAh": 20', '"ratedCapacityAh": 9007199254740993'); + const got = await verifyDossierText(text); + assert.equal(got.kind, "unsupported"); +}); From db8b94ae06da868d30546877707c5107cab0f7cc Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Fri, 25 Sep 2026 21:00:10 +0200 Subject: [PATCH 054/168] fix(landing): give prose its spacing and list markers back, restore the home page's lost space, and teach the spacing gate about expressions --- scripts/check-inline-spacing.mjs | 28 +++++++++++++++++-- site/dpp-landing/src/pages/index.astro | 2 +- site/dpp-landing/src/styles/global.css | 37 ++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/scripts/check-inline-spacing.mjs b/scripts/check-inline-spacing.mjs index 9881046..50f65b1 100644 --- a/scripts/check-inline-spacing.mjs +++ b/scripts/check-inline-spacing.mjs @@ -48,6 +48,24 @@ const OPENS = new RegExp(String.raw`[A-Za-z0-9,;:.’—-][ \t]*\n[ \t]*<(?:${IN // An inline element closing at the end of a line, then prose on the next. const CLOSES = new RegExp(String.raw`[ \t]*\n[ \t]*[A-Za-z0-9“]`, 'g'); +// The same trim happens where prose meets a `{…}` expression, which is how +// "by 2027."Four passport obligations" reached staging on 2026-09-25: a +// sentence ending one line and a computed sentence `{lead}` opening the next. +// Checked in the template only (frontmatter and From 6fb0709f3ca19a3feb36b1f7061bc8417531142f Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Sat, 26 Sep 2026 16:10:10 +0200 Subject: [PATCH 088/168] Stop claiming access credentials are non-bearer, since the node checks no holder proof. --- site/dpp-docs/src/content/docs/regulatory/access-control.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/dpp-docs/src/content/docs/regulatory/access-control.mdx b/site/dpp-docs/src/content/docs/regulatory/access-control.mdx index a6f4dbc..0d89a1c 100644 --- a/site/dpp-docs/src/content/docs/regulatory/access-control.mdx +++ b/site/dpp-docs/src/content/docs/regulatory/access-control.mdx @@ -55,7 +55,7 @@ Durable artefacts — stored signatures, audit records — are keyed by the **di The natural alternative — issuing API keys to recyclers and authorities — fails on three counts. API keys are bearer secrets that get reused, leaked or sold. They carry no verifiable identity assertion: possession is proof of access, not proof of who holds it. And they bind to a single issuer's authentication system, so every regulator would have to integrate with every platform separately. -Verifiable Credentials address all three. They are non-bearer, they carry a signed issuer assertion, and any platform that can verify them can accept them. +Verifiable Credentials answer the second and third: they carry a signed issuer assertion, and any platform that can verify them can accept them. The first is not answered yet. The node accepts a credential on its own, without proof that the presenter is its holder, so a copied credential works until it expires or is revoked. That is one reason credentials a node issues itself are capped at 90 days. This is also where the regulation is heading. ESPR Article 11 empowers the Commission to adopt implementing acts on procedures to issue and verify "the digital credentials of economic operators and other relevant actors that have access rights", and the toy and detergent regulations both defer their credential procedures to that same provision. Those implementing acts are not yet adopted, so no conformance claim is available — but the direction is legislated rather than speculative. From 2f429d8586e69f1de99a59c6d74e407b7b1609b6 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Sat, 26 Sep 2026 19:49:41 +0200 Subject: [PATCH 089/168] Split the landing nav into How it works, Compliance, Developers and Resources beside the unchanged Product groups menu and Docs link, with the bar from 1280px. --- site/dpp-landing/src/components/Footer.astro | 36 ++++-- site/dpp-landing/src/components/Nav.astro | 113 +++++++++++++------ 2 files changed, 101 insertions(+), 48 deletions(-) diff --git a/site/dpp-landing/src/components/Footer.astro b/site/dpp-landing/src/components/Footer.astro index c039a0f..3dc0fe3 100644 --- a/site/dpp-landing/src/components/Footer.astro +++ b/site/dpp-landing/src/components/Footer.astro @@ -8,8 +8,8 @@ const year = new Date().getFullYear(); ---
    -
    -
    +
    +

    - -
    +

    How it works

    • Lifecycle
    • @@ -34,13 +34,27 @@ const year = new Date().getFullYear();
    -
    -

    Resources

    + + +
    +

    Developers

    + +
    + +
    +

    Resources

    +
    -
    +

    Product groups

    -
    +

    Privacy & security

    • What Odal can see
    • diff --git a/site/dpp-landing/src/components/Nav.astro b/site/dpp-landing/src/components/Nav.astro index 2f291f6..e79d38d 100644 --- a/site/dpp-landing/src/components/Nav.astro +++ b/site/dpp-landing/src/components/Nav.astro @@ -1,21 +1,24 @@ --- -// Nav.astro — top navigation. Mark + wordmark left; three menus, Docs and the +// Nav.astro — top navigation. Mark + wordmark left; five menus, Docs and the // CTA right. // -// Three menus, in this order: "How it works" (the software itself), -// "Resources" (the reference pages) and "Product groups". Split this way -// 2026-09-26, at the founder's choice, when the FAQ would have made one -// "Explore" menu seven items long. +// Five menus, in this order, each answering one question: "How it works" (what +// the software does), "Compliance" (what the law requires of my product, and +// when), "Developers" (how to build on it), "Resources" (is it ready, what does +// this word mean, how do I report a flaw) and "Product groups". Split this way +// on 2026-09-26 at the founder's choice, when "Resources" had grown to seven +// items; "Product groups" and the Docs link were kept exactly as they were. // // Each is a disclosure (a button that shows and hides a panel), not an ARIA // menu: the panels hold ordinary links, and a screen reader should read them as -// links. The first two are small panels under their buttons, rendered from one +// links. The first four are small panels under their buttons, rendered from one // template. "Product groups" is a full-width panel, because eleven groups with // a line each do not fit under a button. // -// The bar appears from `lg` (1024px), not `md`: three menus, Docs and the CTA -// beside the wordmark need about 760px, and at 768px every label wrapped onto -// two lines. Below `lg` the hamburger panel carries the same three sections. +// The bar appears from `xl` (1280px). Five menus, Docs and the CTA beside the +// wordmark measured about 1,100px on 2026-09-26, and at 1024px "How it works" +// and "Product groups" wrapped onto two lines. Below `xl` the hamburger panel +// carries the same five sections. // // All open on hover for a mouse and on click or Enter for everyone else, close // on Escape and on a click outside, and only one is ever open. @@ -32,7 +35,7 @@ import Button from "./Button.astro"; import ProductGroupIcon from "./ProductGroupIcon.astro"; import { groups, unsoldGoods as unsold } from "../lib/product-groups"; -type Entry = { href: string; label: string; line: string; icon: string }; +type Entry = { href: string; label: string; line: string; icon: string; external?: boolean }; const howItWorks: Entry[] = [ { @@ -59,14 +62,26 @@ const howItWorks: Entry[] = [ line: "Check a passport's proof file in your browser", icon: '', }, + { + href: "/trust", + label: "What Odal can see", + line: "What stays on your node, and why that holds by design", + icon: '', + }, ]; -const resources: Entry[] = [ +const compliance: Entry[] = [ { - href: "/build-on-odal", - label: "Build on Odal", - line: "Libraries, plugins and the licence line", - icon: '', + href: "/passport-check", + label: "Passport check", + line: "Does your product need a passport, and from when", + icon: '', + }, + { + href: "/timeline", + label: "Timeline", + line: "When each passport is required, year by year", + icon: '', }, { href: "/regulations", @@ -74,18 +89,32 @@ const resources: Entry[] = [ line: "The EU acts behind the passports, and when each applies", icon: '', }, +]; + +const developers: Entry[] = [ + { + href: "/build-on-odal", + label: "Build on Odal", + line: "Libraries, plugins and the licence line", + icon: '', + }, { - href: "/timeline", - label: "Timeline", - line: "When each passport is required, year by year", - icon: '', + href: "https://docs.odal-node.io", + label: "Documentation", + line: "Install, run and operate a node", + icon: '', + external: true, }, { - href: "/passport-check", - label: "Passport check", - line: "Does your product need a passport, and from when", - icon: '', + href: "https://docs.odal-node.io/api", + label: "API reference", + line: "Every route a node serves", + icon: '', + external: true, }, +]; + +const resources: Entry[] = [ { href: "/roadmap", label: "Roadmap", @@ -104,17 +133,25 @@ const resources: Entry[] = [ line: "The words around a passport, explained", icon: '', }, + { + href: "/security", + label: "Security", + line: "Report an issue, and how passports and keys are protected", + icon: '', + }, ]; const path = Astro.url.pathname.replace(/\/$/, "") || "/"; const within = (list: Entry[]) => list.some((e) => path === e.href || path.startsWith(`${e.href}/`)); const inGroups = path.startsWith("/product-groups"); -// The two small menus, in bar order; "Product groups" follows them. -const small = { - how: { id: "dd-how", label: "How it works", items: howItWorks, current: within(howItWorks) }, - resources: { id: "dd-resources", label: "Resources", items: resources, current: within(resources) }, -}; +// The four small menus, in bar order; "Product groups" follows them. +const small = [ + { id: "dd-how", label: "How it works", items: howItWorks }, + { id: "dd-compliance", label: "Compliance", items: compliance }, + { id: "dd-developers", label: "Developers", items: developers }, + { id: "dd-resources", label: "Resources", items: resources }, +].map((m) => ({ ...m, current: within(m.items) })); ---
      @@ -125,8 +162,8 @@ const small = { -
    + diff --git a/site/dpp-landing/src/pages/repairers-and-recyclers.astro b/site/dpp-landing/src/pages/repairers-and-recyclers.astro new file mode 100644 index 0000000..f334b75 --- /dev/null +++ b/site/dpp-landing/src/pages/repairers-and-recyclers.astro @@ -0,0 +1,162 @@ +--- +// /repairers-and-recyclers — the passport from the side of the people who +// repair, remanufacture, repurpose and recycle products. +// +// What each reader may see is read from data/access.json (the battery +// passport's Art. 77(2) access rules, the same file /visibility uses), so the +// two pages cannot disagree. Checked 2026-09-26 against Regulation (EU) +// 2023/1542 Art. 77 (consolidated text 02023R1542-20250731) and the released +// node's credential behaviour: node-issued credentials last at most 90 days +// and cannot be withdrawn, and a credential is accepted on its own, without +// proof that the person presenting it is its holder. +import Base from "../layouts/Base.astro"; +import Section from "../components/Section.astro"; +import Button from "../components/Button.astro"; +import access from "../data/access.json"; + +const title = "For repairers and recyclers: reading a Digital Product Passport | Odal Node"; +const description = + "What a repairer, remanufacturer, second-life operator or recycler can read in a battery passport, how to get the credential that opens the restricted parts, and what happens to the passport when the battery changes hands or is recycled."; + +const point = (id: string) => access.points.find((p) => p.id === id)!; +const open = point("1"); +const withCredential = [point("2"), point("4")]; + +const mailto = `mailto:contact@odal-node.io?subject=${encodeURIComponent("Reading passports as a repairer or recycler")}`; +--- + + +
    +

    + A battery passport has parts anyone can read and parts the law keeps for + readers with a legitimate interest: repairers, remanufacturers, + second-life operators and recyclers, for taking the battery apart safely + and knowing what is inside it, and the buyer of an individual battery. On + a passport served by Odal Node, those parts open with a credential. +

    +

    + This page covers the battery passport, the first one the law requires, + from 18 February 2027. For other product groups, no act sets these + access rights yet. +

    +
    + +
    +
    +
    +

    Without a credential

    +

    {open.title}

    +

    {open.note}

    +
    +
    +

    With a credential

    + {withCredential.map((p) => ( + <> +

    {p.title}

    +

    {p.note}

    + + ))} +
    +
    +

    + Test reports proving compliance are for authorities only, and a + battery's own history is for readers with a legitimate interest only: + neither group sees everything. Visibility shows + the split field by field, and the example + passport shows a complete one. +

    +
    + +
    +
      +
    1. +

      From the company behind the product

      +

      + The company that runs the passport can issue you a credential from its + own node, naming your role: repairer, recycler, remanufacturer, + preparer for re-use or distributor. It lasts up to 90 days, and you + ask for a new one when it ends. +

      +
    2. +
    3. +

      From an issuer the company trusts

      +

      + A company can also accept credentials from other issuers, such as a + trade body, and chooses which ones. Where that issuer publishes a + list of withdrawn credentials, the node checks it. +

      +
    4. +
    +

    + There is no EU-wide issuer of these credentials yet. EU law lets the + Commission set out how credentials are issued and checked, and the + Batteries Regulation required it to specify, by 18 August 2026, who + counts as having a legitimate interest and what they may do with the + information. Until the software follows those acts, it applies the + Regulation's own wording. +

    +
    + +
    +

    + A credential works on its own: anyone holding a copy can use it until it + expires or its issuer withdraws it. Keep it inside the business doing the + work. A credential issued by a company's own node cannot be withdrawn, + which is why it lasts 90 days at most. +

    +
    + +
    +
      +
    • +

      Repurposed or remanufactured

      +

      + Whoever puts a re-used, repurposed or remanufactured battery back on + the market takes over the passport duties, and the battery gets a new + passport linked to the original. +

      +
    • +
    • +

      Waste

      +

      + Once a battery becomes waste, the duties pass to its producer, the + producer's take-back scheme, or the waste operator chosen to handle + it. +

      +
    • +
    • +

      Recycled

      +

      + After the battery has been recycled, its passport ceases to exist. +

      +
    • +
    +

    + Odal Node records a handover inside the passport, signed by both sides, + and exports the whole record as one signed file anyone can check + without a network connection. Lifecycle follows + a passport from start to end. +

    +
    + +
    +

    + Every passport served by Odal Node is signed with an identity on the + issuing company's own web domain. Drop its proof file into{" "} + Verify to check the signatures and history in your + browser, without Odal. +

    +
    + +
    +

    + If you repair, remanufacture or recycle batteries and want to try + reading passports with a credential, write to us and say what you handle + and which systems you use. +

    +
    + + +
    +
    + diff --git a/site/dpp-landing/src/pages/visibility.astro b/site/dpp-landing/src/pages/visibility.astro index e99892c..4a8b6dd 100644 --- a/site/dpp-landing/src/pages/visibility.astro +++ b/site/dpp-landing/src/pages/visibility.astro @@ -154,6 +154,25 @@ const description = product group's rule as if it were the whole regulation's.

    + +
    + +
    From 0e888dc0562ef7b6140ccb91866a0d35b9197858 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Tue, 29 Sep 2026 21:55:31 +0200 Subject: [PATCH 141/168] fix(landing): stop calling the example battery passport complete, mark the carbon and recycled-content values as placeholders the guidance defers, and drop a repairability score no act defines for batteries --- site/dpp-landing/src/lib/site-map.ts | 2 +- .../src/pages/example-passport.astro | 21 ++++++++++++++----- .../src/pages/repairers-and-recyclers.astro | 2 +- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/site/dpp-landing/src/lib/site-map.ts b/site/dpp-landing/src/lib/site-map.ts index a6f1b2e..0272c38 100644 --- a/site/dpp-landing/src/lib/site-map.ts +++ b/site/dpp-landing/src/lib/site-map.ts @@ -47,7 +47,7 @@ export const menus: Menu[] = [ { href: "/example-passport", label: "Example passport", - line: "A complete battery passport, sorted by who may read it", + line: "A battery passport, sorted by who may read it", icon: '', }, ], diff --git a/site/dpp-landing/src/pages/example-passport.astro b/site/dpp-landing/src/pages/example-passport.astro index 36ca029..35d6d9a 100644 --- a/site/dpp-landing/src/pages/example-passport.astro +++ b/site/dpp-landing/src/pages/example-passport.astro @@ -12,13 +12,17 @@ import example from "../data/example-passport.json"; const title = "Example battery passport | Odal Node"; const description = - "A complete example of an electric-vehicle battery passport, sorted into what anyone can read, what authorities and readers with a legitimate interest can read, and what only one of them can."; + "An example of an electric-vehicle battery passport, sorted into what anyone can read, what authorities and readers with a legitimate interest can read, and what only one of them can."; type Json = null | boolean | number | string | Json[] | { [k: string]: Json }; const p = example.passport as unknown as Record & { productGroupData: Record }; const disclosure = (battery as { disclosure: Record }).disclosure; // The envelope fields every passport carries, then the battery's own data. +// The engine's demo data also carries `repairabilityScore`, which is left off +// here: no EU act defines a repairability score for batteries, so showing one on +// an example battery passport would suggest the law asks for it. The data file +// stays a verbatim copy of the engine's (see its _comment). const envelope: [string, Json][] = [ ["productName", p.productName], ["manufacturer", p.manufacturer], @@ -26,7 +30,6 @@ const envelope: [string, Json][] = [ ["placedOnMarketDate", p.placedOnMarketDate], ["commodityCode", p.commodityCode], ["materials", p.materials], - ["repairabilityScore", p.repairabilityScore], ]; const groupData = Object.entries(p.productGroupData).filter(([k]) => k !== "productGroup"); @@ -73,11 +76,19 @@ const flat = (o: Record) =>

    - This is a complete passport for an electric-vehicle battery, from the - software's own example data. The company, Musterzelle GmbH, - is invented, and the page shows the data a company submits before it is + This is a passport for an electric-vehicle battery, from the software's + own example data. The company, Musterzelle GmbH, is + invented, and the page shows the data a company submits before it is signed, sorted the way a node shows it to each reader.

    +

    + Some public values are placeholders. For passports from February 2027, + the Commission's guidance says to leave the carbon footprint and the + shares of recycled cobalt, lithium, nickel and lead empty until the rules + for calculating and showing them apply. The software still asks for + them, and that is being changed. Information on responsible sourcing is + due from August 2027, so it is not shown here. +

    To see a signed proof file checked in your browser, use{" "} Verify. To switch between readers and watch the parts diff --git a/site/dpp-landing/src/pages/repairers-and-recyclers.astro b/site/dpp-landing/src/pages/repairers-and-recyclers.astro index 88b8498..4a1ff90 100644 --- a/site/dpp-landing/src/pages/repairers-and-recyclers.astro +++ b/site/dpp-landing/src/pages/repairers-and-recyclers.astro @@ -63,7 +63,7 @@ const mailto = `mailto:contact@odal-node.io?subject=${encodeURIComponent("Readin battery's own history is for readers with a legitimate interest only: neither group sees everything. Visibility shows the split field by field, and the example - passport shows a complete one. + passport shows a whole passport sorted this way.

    From 46d782747d5e47b193bf4b0b1d447426d6568bdb Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Tue, 29 Sep 2026 22:02:16 +0200 Subject: [PATCH 142/168] test(landing): hold the browser verifier to UTF-16 key order on the bytes the engine signed, and say why the dossier file lists the keys the other way round --- site/dpp-landing/tests/verify.test.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/site/dpp-landing/tests/verify.test.ts b/site/dpp-landing/tests/verify.test.ts index d84a71f..043fdf2 100644 --- a/site/dpp-landing/tests/verify.test.ts +++ b/site/dpp-landing/tests/verify.test.ts @@ -14,6 +14,7 @@ import assert from "node:assert/strict"; import { readFileSync, readdirSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { verifyDossierText } from "../src/lib/verify/verify.ts"; +import { b64urlDecode, jcs } from "../src/lib/verify/canonical.ts"; type Expected = | { exit: 0 | 1; checks: { name: string; status: "pass" | "fail" | "absent" }[] } @@ -70,6 +71,25 @@ test("a one-character edit to a signed payload is caught, and only where it happ } }); +// RFC 8785 §3.2.3 sorts object keys by UTF-16 code unit, not by code point and +// not by UTF-8 byte. The three agree for almost every key, so a verifier that +// sorts the wrong way passes almost everything, and the golden verdicts above +// would only report it as a bad signature. The engine's example payload carries +// a pair on which they disagree (engine #429): "😀" is the surrogate pair +// 0xD83D 0xDE00 and "A" (fullwidth A) the single unit 0xFF21, so by code unit +// the emoji comes first and by code point it comes last. +// +// The dossier file lists the two the other way round. That is not a fault: the +// file is the node's readable copy, and its key order is not what was signed. +// This reads the signed bytes themselves, which is what the page checks. +test("keys are canonicalised by UTF-16 code unit, as the engine signed them", () => { + assert.equal(jcs({ "A": 6, "😀": 5 }), '{"😀":5,"A":6}'); + const dossier = JSON.parse(readFileSync(`${dir}04-valid-full-lifecycle.json`, "utf8")); + const signed = new TextDecoder().decode(b64urlDecode(dossier.fullView.jws.split(".")[1])); + assert.ok(signed.indexOf('"😀"') < signed.indexOf('"A"'), "the engine signed the emoji first"); + assert.equal(jcs(JSON.parse(signed)), signed, "canonicalising the signed payload again reproduces its bytes"); +}); + test("an integer beyond 2^53 is refused, not approximated", async () => { const text = readFileSync(`${dir}01-valid-simple.json`, "utf8").replace('"ratedCapacityAh": 20', '"ratedCapacityAh": 9007199254740993'); const got = await verifyDossierText(text); From 04633e782d4c6b2b10ccda76046cae3ef89a22ae Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Wed, 30 Sep 2026 00:22:10 +0200 Subject: [PATCH 143/168] fix(landing): list the /verify examples' readable payload keys in the order they were signed, and fail the tests if a re-copy from the engine brings code-point order back --- .../verify/examples/01-valid-simple.json | 16 ++++----- .../examples/02-valid-with-transfer.json | 16 ++++----- .../verify/examples/03-valid-with-eol.json | 16 ++++----- .../examples/04-valid-full-lifecycle.json | 16 ++++----- .../examples/05-tampered-signature.json | 16 ++++----- .../examples/06-tampered-audit-entry.json | 16 ++++----- .../07-tampered-transfer-signature.json | 16 ++++----- .../examples/08-hidden-field-injection.json | 16 ++++----- .../examples/09-unknown-top-level-field.json | 16 ++++----- site/dpp-landing/tests/verify.test.ts | 34 +++++++++++++++++-- 10 files changed, 103 insertions(+), 75 deletions(-) diff --git a/site/dpp-landing/public/verify/examples/01-valid-simple.json b/site/dpp-landing/public/verify/examples/01-valid-simple.json index 932a762..82cd623 100644 --- a/site/dpp-landing/public/verify/examples/01-valid-simple.json +++ b/site/dpp-landing/public/verify/examples/01-valid-simple.json @@ -35,8 +35,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -64,8 +64,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -129,8 +129,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -175,8 +175,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", diff --git a/site/dpp-landing/public/verify/examples/02-valid-with-transfer.json b/site/dpp-landing/public/verify/examples/02-valid-with-transfer.json index 8fba938..80b675c 100644 --- a/site/dpp-landing/public/verify/examples/02-valid-with-transfer.json +++ b/site/dpp-landing/public/verify/examples/02-valid-with-transfer.json @@ -35,8 +35,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -64,8 +64,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -184,8 +184,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -231,8 +231,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", diff --git a/site/dpp-landing/public/verify/examples/03-valid-with-eol.json b/site/dpp-landing/public/verify/examples/03-valid-with-eol.json index bfbe683..203e5ce 100644 --- a/site/dpp-landing/public/verify/examples/03-valid-with-eol.json +++ b/site/dpp-landing/public/verify/examples/03-valid-with-eol.json @@ -35,8 +35,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -64,8 +64,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -159,8 +159,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -206,8 +206,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", diff --git a/site/dpp-landing/public/verify/examples/04-valid-full-lifecycle.json b/site/dpp-landing/public/verify/examples/04-valid-full-lifecycle.json index 72424c2..cfba4c4 100644 --- a/site/dpp-landing/public/verify/examples/04-valid-full-lifecycle.json +++ b/site/dpp-landing/public/verify/examples/04-valid-full-lifecycle.json @@ -35,8 +35,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -64,8 +64,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -214,8 +214,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -262,8 +262,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", diff --git a/site/dpp-landing/public/verify/examples/05-tampered-signature.json b/site/dpp-landing/public/verify/examples/05-tampered-signature.json index 9484089..1fee5f1 100644 --- a/site/dpp-landing/public/verify/examples/05-tampered-signature.json +++ b/site/dpp-landing/public/verify/examples/05-tampered-signature.json @@ -35,8 +35,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -64,8 +64,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -214,8 +214,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -262,8 +262,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", diff --git a/site/dpp-landing/public/verify/examples/06-tampered-audit-entry.json b/site/dpp-landing/public/verify/examples/06-tampered-audit-entry.json index f5850ec..d50e29b 100644 --- a/site/dpp-landing/public/verify/examples/06-tampered-audit-entry.json +++ b/site/dpp-landing/public/verify/examples/06-tampered-audit-entry.json @@ -35,8 +35,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -64,8 +64,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -214,8 +214,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -262,8 +262,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", diff --git a/site/dpp-landing/public/verify/examples/07-tampered-transfer-signature.json b/site/dpp-landing/public/verify/examples/07-tampered-transfer-signature.json index 683bde8..7a8e4f5 100644 --- a/site/dpp-landing/public/verify/examples/07-tampered-transfer-signature.json +++ b/site/dpp-landing/public/verify/examples/07-tampered-transfer-signature.json @@ -35,8 +35,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -64,8 +64,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -214,8 +214,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -262,8 +262,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", diff --git a/site/dpp-landing/public/verify/examples/08-hidden-field-injection.json b/site/dpp-landing/public/verify/examples/08-hidden-field-injection.json index 10df725..6034702 100644 --- a/site/dpp-landing/public/verify/examples/08-hidden-field-injection.json +++ b/site/dpp-landing/public/verify/examples/08-hidden-field-injection.json @@ -35,8 +35,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -64,8 +64,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -214,8 +214,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -262,8 +262,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", diff --git a/site/dpp-landing/public/verify/examples/09-unknown-top-level-field.json b/site/dpp-landing/public/verify/examples/09-unknown-top-level-field.json index 245a16a..fee58b5 100644 --- a/site/dpp-landing/public/verify/examples/09-unknown-top-level-field.json +++ b/site/dpp-landing/public/verify/examples/09-unknown-top-level-field.json @@ -35,8 +35,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -64,8 +64,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -214,8 +214,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", @@ -262,8 +262,8 @@ "Émission": "0.5e-3 test", "é": 3, "€": 4, - "A": 6, - "😀": 5 + "😀": 5, + "A": 6 }, "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", "issuer": "did:web:node.acme.example", diff --git a/site/dpp-landing/tests/verify.test.ts b/site/dpp-landing/tests/verify.test.ts index 043fdf2..7e4e5b9 100644 --- a/site/dpp-landing/tests/verify.test.ts +++ b/site/dpp-landing/tests/verify.test.ts @@ -79,9 +79,8 @@ test("a one-character edit to a signed payload is caught, and only where it happ // 0xD83D 0xDE00 and "A" (fullwidth A) the single unit 0xFF21, so by code unit // the emoji comes first and by code point it comes last. // -// The dossier file lists the two the other way round. That is not a fault: the -// file is the node's readable copy, and its key order is not what was signed. -// This reads the signed bytes themselves, which is what the page checks. +// This reads the signed bytes themselves, which is what the page checks, not +// the readable copies beside them (see the next test for those). test("keys are canonicalised by UTF-16 code unit, as the engine signed them", () => { assert.equal(jcs({ "A": 6, "😀": 5 }), '{"😀":5,"A":6}'); const dossier = JSON.parse(readFileSync(`${dir}04-valid-full-lifecycle.json`, "utf8")); @@ -90,6 +89,35 @@ test("keys are canonicalised by UTF-16 code unit, as the engine signed them", () assert.equal(jcs(JSON.parse(signed)), signed, "canonicalising the signed payload again reproduces its bytes"); }); +// A dossier carries each signed view twice: the JWS, and a readable copy of its +// payload that the page shows when an example is loaded. The engine writes the +// readable copies in code-point order (a serde_json map sorts by UTF-8 bytes), +// so on the pair above they disagreed with the signed bytes and read as the +// very ordering bug the pair exists to catch (dpp-engine #430). The examples +// here have that pair swapped by hand in all four copies per file: a text edit +// that leaves every other byte, every JWS included, as the engine wrote it, and +// changes no verdict, since the page canonicalises before it checks anything. +// This fails if a re-copy from the engine brings the old order back before +// #430 is fixed there. +test("every object in the examples lists its keys in the order they are signed in", () => { + const unordered: string[] = []; + const walk = (value: unknown, at: string): void => { + if (Array.isArray(value)) return value.forEach((v, i) => walk(v, `${at}[${i}]`)); + if (!value || typeof value !== "object") return; + const keys = Object.keys(value); + // An integer-like key is listed first by any JavaScript object whatever the + // file says, so an object holding one cannot be read for order here. + if (!keys.some((k) => /^(0|[1-9]\d*)$/.test(k)) && keys.join("\u0000") !== [...keys].sort().join("\u0000")) { + unordered.push(at); + } + for (const k of keys) walk((value as Record)[k], `${at}.${k}`); + }; + for (const file of files.filter((f) => f.endsWith(".json"))) { + walk(JSON.parse(readFileSync(`${dir}${file}`, "utf8")), file); + } + assert.deepEqual(unordered, [], "objects whose keys are not in UTF-16 code-unit order"); +}); + test("an integer beyond 2^53 is refused, not approximated", async () => { const text = readFileSync(`${dir}01-valid-simple.json`, "utf8").replace('"ratedCapacityAh": 20', '"ratedCapacityAh": 9007199254740993'); const got = await verifyDossierText(text); From b4835ad7dc98fecfdc2cca069ef947ccef078908 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Wed, 30 Sep 2026 00:24:04 +0200 Subject: [PATCH 144/168] chore(deps): bump the build-time undici to 8.11.2 for three high advisories --- pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5747d4b..2115c91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2861,8 +2861,8 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@8.10.0: - resolution: {integrity: sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==} + undici@8.11.2: + resolution: {integrity: sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==} engines: {node: '>=22.19.0'} unhead@2.1.15: @@ -6761,7 +6761,7 @@ snapshots: undici-types@7.16.0: {} - undici@8.10.0: {} + undici@8.11.2: {} unhead@2.1.15: dependencies: @@ -6781,7 +6781,7 @@ snapshots: dependencies: css-tree: 3.2.1 ohash: 2.0.11 - undici: 8.10.0 + undici: 8.11.2 unist-util-find-after@5.0.0: dependencies: From d41fb8c0ef0c5e8d3d09f3c9af0645d792fb6cb7 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Wed, 30 Sep 2026 01:03:23 +0200 Subject: [PATCH 145/168] test(landing): say the /verify examples are the engine's regenerated corpus in signed key order, byte for byte, now that the engine writes it that way --- site/dpp-landing/tests/verify.test.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/site/dpp-landing/tests/verify.test.ts b/site/dpp-landing/tests/verify.test.ts index 7e4e5b9..b9b86d6 100644 --- a/site/dpp-landing/tests/verify.test.ts +++ b/site/dpp-landing/tests/verify.test.ts @@ -90,15 +90,14 @@ test("keys are canonicalised by UTF-16 code unit, as the engine signed them", () }); // A dossier carries each signed view twice: the JWS, and a readable copy of its -// payload that the page shows when an example is loaded. The engine writes the -// readable copies in code-point order (a serde_json map sorts by UTF-8 bytes), -// so on the pair above they disagreed with the signed bytes and read as the -// very ordering bug the pair exists to catch (dpp-engine #430). The examples -// here have that pair swapped by hand in all four copies per file: a text edit -// that leaves every other byte, every JWS included, as the engine wrote it, and -// changes no verdict, since the page canonicalises before it checks anything. -// This fails if a re-copy from the engine brings the old order back before -// #430 is fixed there. +// payload that the page shows when an example is loaded. The node used to write +// the readable copies in code-point order (a serde_json map sorts by UTF-8 +// bytes), so on the pair above they disagreed with the signed bytes and read as +// the very ordering bug the pair exists to catch (dpp-engine #430). The engine +// now writes every object in signed key order (dpp-engine #431), and these +// examples are its regenerated corpus, byte for byte. Order changes no verdict, +// since the page canonicalises before it checks anything; this fails if a +// re-copy from an engine without that fix brings code-point order back. test("every object in the examples lists its keys in the order they are signed in", () => { const unordered: string[] = []; const walk = (value: unknown, at: string): void => { From adddb0418f9eeadc31725cedd18b7133d2512841 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Wed, 30 Sep 2026 01:31:53 +0200 Subject: [PATCH 146/168] fix(landing): show real battery passports in the /verify examples, from the engine's regenerated corpus, and keep the canonicalisation keys in a dossier of their own --- .../verify/examples/01-valid-simple.json | 660 ++++++++++++++--- .../examples/02-valid-with-transfer.json | 668 ++++++++++++++--- .../verify/examples/03-valid-with-eol.json | 664 ++++++++++++++--- .../examples/04-valid-full-lifecycle.json | 672 +++++++++++++++--- .../examples/05-tampered-signature.json | 672 +++++++++++++++--- .../examples/06-tampered-audit-entry.json | 672 +++++++++++++++--- .../07-tampered-transfer-signature.json | 672 +++++++++++++++--- .../examples/08-hidden-field-injection.json | 672 +++++++++++++++--- .../examples/09-unknown-top-level-field.json | 672 +++++++++++++++--- .../examples/11-canonicalisation-vectors.json | 138 ++++ site/dpp-landing/tests/verify-expected.json | 56 +- site/dpp-landing/tests/verify.test.ts | 18 +- 12 files changed, 5305 insertions(+), 931 deletions(-) create mode 100644 site/dpp-landing/public/verify/examples/11-canonicalisation-vectors.json diff --git a/site/dpp-landing/public/verify/examples/01-valid-simple.json b/site/dpp-landing/public/verify/examples/01-valid-simple.json index 82cd623..8978f89 100644 --- a/site/dpp-landing/public/verify/examples/01-valid-simple.json +++ b/site/dpp-landing/public/verify/examples/01-valid-simple.json @@ -16,68 +16,300 @@ { "action": "published", "actor": "api-key", - "entryHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "entryHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", - "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" - }, - "publicViewPayload": { - "content": { - "B": 2, - "b": 1, + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], "batteryChemistry": "NMC", "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." }, + "productName": "Acme LMT 48V 20Ah battery pack", + "publishedAt": "2026-09-01T09:05:00Z", + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 + }, + "publicViewPayload": { + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "newStatus": "active", @@ -112,44 +344,197 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.bXoh9eU-rTvN-_heLcDWRfRe5hNQA_JmZnmSzNKr2t0f56qklAYFHb8xyevrfbb3phscXigOs8vbyAPMcOJ_Dg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImd0aW4iOiIwOTUwNjAwMDEzNDM1MiIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJyYXRlZENhcGFjaXR5S3doIjpudWxsLCJyZWN5Y2xlZENvbnRlbnRDb2JhbHRQY3QiOjEyLCJyZWN5Y2xlZENvbnRlbnRMZWFkUGN0IjowLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0Ijo2LCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOjgsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uTG9ja2VkIjp0cnVlLCJyZXRlbnRpb25VbnRpbCI6IjIwMzYtMDgtMjlUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNi4wIiwic3RhdHVzIjoiYWN0aXZlIiwidXBkYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJ2ZXJzaW9uIjoxfQ.WlMDfygpDjYk8xHRQjpQgppwhVkUgYhyj_9iiPH_IXvXb8IKcD0cA2lFJCznB1B6llfW5Q3qiZcfRAdB78nDBw", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "manifest": { "contentHashes": { - "auditEntries": "0ec08fb959a095356529e02031c72e3936c4767af9db6eb8b8b004390514d591", - "fullView": "b53ff594240922c190a6d957f4725b0e2353711426811b463f5e8916c3b39678", - "publicView": "53bdb718a8598f84b9a107774e05ad9495e1575237752a3d5cc2e12eeaa6d572" + "auditEntries": "f88a9f18e40918793c0888102ae898ac5ab35078a017fc1848ef4bde95ef67b7", + "fullView": "70f5a9779744ad58b77cebed129ecec4c24d7c5bbf3b65be4b3c61c4be5f0e80", + "publicView": "7ada532408d7c20c385804ca00d0a6a969f1448c796775c14657d7932ecf392a" }, "coreVersion": "0.20.0", "createdAt": "2026-09-25T12:00:00Z", @@ -158,37 +543,116 @@ "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjBlYzA4ZmI5NTlhMDk1MzU2NTI5ZTAyMDMxYzcyZTM5MzZjNDc2N2FmOWRiNmViOGI4YjAwNDM5MDUxNGQ1OTEiLCJmdWxsVmlldyI6ImI1M2ZmNTk0MjQwOTIyYzE5MGE2ZDk1N2Y0NzI1YjBlMjM1MzcxMTQyNjgxMWI0NjNmNWU4OTE2YzNiMzk2NzgiLCJwdWJsaWNWaWV3IjoiNTNiZGI3MThhODU5OGY4NGI5YTEwNzc3NGUwNWFkOTQ5NWUxNTc1MjM3NzUyYTNkNWNjMmUxMmVlYWE2ZDU3MiJ9LCJjb3JlVmVyc2lvbiI6IjAuMjAuMCIsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjVUMTI6MDA6MDBaIiwiZm9ybWF0VmVyc2lvbiI6IjEiLCJpc3N1ZXJEaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwibm9kZVZlcnNpb24iOiIwLjE0LjAiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIn0.iy3u4scbBPA_qAcFIgZvUtXxuPQdKKQ8nzpDpqBBHoWb2kzH7aGK99jDc-rKuab1JsULKKob_LCKgK7Ed0WkCQ", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImY4OGE5ZjE4ZTQwOTE4NzkzYzA4ODgxMDJhZTg5OGFjNWFiMzUwNzhhMDE3ZmMxODQ4ZWY0YmRlOTVlZjY3YjciLCJmdWxsVmlldyI6IjcwZjVhOTc3OTc0NGFkNThiNzdjZWJlZDEyOWVjZWM0YzI0ZDdjNWJiZjNiNjViZTRiM2M2MWM0YmU1ZjBlODAiLCJwdWJsaWNWaWV3IjoiN2FkYTUzMjQwOGQ3YzIwYzM4NTgwNGNhMDBkMGE2YTk2OWYxNDQ4Yzc5Njc3NWMxNDY1N2Q3OTMyZWNmMzkyYSJ9LCJjb3JlVmVyc2lvbiI6IjAuMjAuMCIsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjVUMTI6MDA6MDBaIiwiZm9ybWF0VmVyc2lvbiI6IjEiLCJpc3N1ZXJEaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwibm9kZVZlcnNpb24iOiIwLjE0LjAiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIn0.LowiXEfwTwp_l3t1Leui0lyq8gUKTPy41xRpSKy0wWfdiRso6Iri26iU_zaEQSHSQNLJZrrEXUrnvQbPbIr_CQ", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.TewhsuBa83tHnqMcTgSUZf7CtbzAVvHXVRVOJYFkjJEgzOvMa6O7NGiIWQdwDFtVh-wWvztRkUTprTGV6PqsAQ", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjbzJlUGVyVW5pdCI6bnVsbCwiY29tbW9kaXR5Q29kZSI6Ijg1MDc2MDAwIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowMDowMFoiLCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsIm1hbnVmYWN0dXJlciI6eyJhZGRyZXNzIjoiTXVzdGVyc3RyYXNzZSAxLCA4MDMzMSBNw7xuY2hlbiwgREUiLCJjb3VudHJ5IjpudWxsLCJkaWRXZWJVcmwiOiJodHRwczovL25vZGUuYWNtZS5leGFtcGxlLy53ZWxsLWtub3duL2RpZC5qc29uIiwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgifSwibWF0ZXJpYWxzIjpbeyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJOTUMgODExIGNhdGhvZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjgsIndlaWdodEtnIjozLjF9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiR3JhcGhpdGUgYW5vZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjQsIndlaWdodEtnIjoxLjl9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiQWx1bWluaXVtIHBhY2sgaG91c2luZyIsInJlY3ljbGVkUGN0Ijo2MCwid2VpZ2h0S2ciOjIuMn1dLCJwbGFjZWRPbk1hcmtldERhdGUiOiIyMDI2LTA5LTAxIiwicHJvZHVjdEdyb3VwIjoiYmF0dGVyeSIsInByb2R1Y3RHcm91cERhdGEiOnsiYmF0dGVyeUNoZW1pc3RyeSI6Ik5NQyIsImJhdHRlcnlNb2RlbElkIjoiQUNNRS1MTVQtNDhWIiwiYmF0dGVyeVBhc3Nwb3J0TnVtYmVyIjoiVVJOOk9ETDpCQVRUOjA5NTA2MDAwMTM0MzUyOkFDTUUtTE1ULTIwMjYtMDkiLCJiYXR0ZXJ5VHlwZSI6ImxtdCIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY28yZVBlclVuaXRLZyI6ODYuNCwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZXVEZWNsYXJhdGlvbk9mQ29uZm9ybWl0eSI6IkVVLURvQyBBQ01FLTIwMjYtTE1ULTA5MDEsIHBlciBSZWd1bGF0aW9uIChFVSkgMjAyMy8xNTQyIEFydC4gMTgiLCJleHBlY3RlZExpZmV0aW1lQ3ljbGVzIjoxMjAwLCJleHBlY3RlZExpZmV0aW1lUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiwgMjUgZGVnQywgMTAwJSBEb0QiLCJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJoYXphcmRvdXNTdWJzdGFuY2VzIjpbeyJjYXNOdW1iZXIiOiIyMTMyNC00MC0zIiwiY29uY2VudHJhdGlvblBjdCI6MC44LCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIn1dLCJpbml0aWFsUm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTUsImludGVybmFsQ2VsbFJlc2lzdGFuY2VNb2htIjoxNCwiaW50ZXJuYWxQYWNrUmVzaXN0YW5jZU1vaG0iOjExMCwibWFudWZhY3R1cmluZ0RhdGUiOiIyMDI2LTA4LTE4VDAwOjAwOjAwWiIsIm1hbnVmYWN0dXJpbmdQbGFjZSI6Ik3DvG5jaGVuLCBERSIsIm1hcmtpbmdJbmZvcm1hdGlvbiI6IlNlcGFyYXRlLWNvbGxlY3Rpb24gc3ltYm9sIHBlciBBcnQuIDEzKDQpOyBjYXBhY2l0eSAyMCBBaCAvIDk2MCBXaDsgNDggViBMTVQiLCJtYXhpbXVtVm9sdGFnZVYiOjU0LjYsIm1pbmltYWxWb2x0YWdlViI6MzksIm5vbWluYWxDYXBhY2l0eUFoIjoyMCwibm9taW5hbFZvbHRhZ2VWIjo0OCwibm90SW5Vc2VUZW1wZXJhdHVyZVJhbmdlIjp7Im1heEMiOjQ1LCJtaW5DIjotMTB9LCJub3RJblVzZVRlbXBlcmF0dXJlUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiBzdG9yYWdlIGNsYXVzZSIsIm9yaWdpbmFsUG93ZXJDYXBhYmlsaXR5VyI6OTYwLCJwb3dlckxpbWl0TWF4VyI6MTUwMCwicG93ZXJMaW1pdE1pblciOjIwLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjYuMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.OnjeF7qUrlZOKc-Xx8u-ThMayslJBs7HPAmpZBFKvyJpivIqOrN5lTCW15PsoyBeSOpjmRq3Mm_UC99B-Ya-Dg", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } } } \ No newline at end of file diff --git a/site/dpp-landing/public/verify/examples/02-valid-with-transfer.json b/site/dpp-landing/public/verify/examples/02-valid-with-transfer.json index 80b675c..3ff21b0 100644 --- a/site/dpp-landing/public/verify/examples/02-valid-with-transfer.json +++ b/site/dpp-landing/public/verify/examples/02-valid-with-transfer.json @@ -16,68 +16,300 @@ { "action": "published", "actor": "api-key", - "entryHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "entryHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", - "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" - }, - "publicViewPayload": { - "content": { - "B": 2, - "b": 1, + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], "batteryChemistry": "NMC", "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." }, + "productName": "Acme LMT 48V 20Ah battery pack", + "publishedAt": "2026-09-01T09:05:00Z", + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 + }, + "publicViewPayload": { + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "newStatus": "active", @@ -90,7 +322,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "fb94dc5d624df166d3c8818ca7314033d9686dc629ddb1fbfb2be44c04b758af", + "entryHash": "5c869a32da993e9a5ee13bc3b059fee9c3abc1fbbd840ab2449473f0e5453b08", "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", "metadata": { "event": "transfer.initiated", @@ -99,7 +331,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "prevHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000003", "timestamp": "2026-09-10T14:00:00.042Z" @@ -107,7 +339,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "c4862c22734f57d4834cdb0b7aead85efd2351164ee6119b2a56c95ffd76dd9c", + "entryHash": "3a5eb46b99bc15d235b342a0a6092d3e382d998b7de32588ed4966d74295fa51", "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", "metadata": { "event": "transfer.accepted", @@ -116,7 +348,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "fb94dc5d624df166d3c8818ca7314033d9686dc629ddb1fbfb2be44c04b758af", + "prevHash": "5c869a32da993e9a5ee13bc3b059fee9c3abc1fbbd840ab2449473f0e5453b08", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000004", "timestamp": "2026-09-10T14:30:00.123456Z" @@ -167,44 +399,197 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.bXoh9eU-rTvN-_heLcDWRfRe5hNQA_JmZnmSzNKr2t0f56qklAYFHb8xyevrfbb3phscXigOs8vbyAPMcOJ_Dg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImd0aW4iOiIwOTUwNjAwMDEzNDM1MiIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJyYXRlZENhcGFjaXR5S3doIjpudWxsLCJyZWN5Y2xlZENvbnRlbnRDb2JhbHRQY3QiOjEyLCJyZWN5Y2xlZENvbnRlbnRMZWFkUGN0IjowLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0Ijo2LCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOjgsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uTG9ja2VkIjp0cnVlLCJyZXRlbnRpb25VbnRpbCI6IjIwMzYtMDgtMjlUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNi4wIiwic3RhdHVzIjoiYWN0aXZlIiwidXBkYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJ2ZXJzaW9uIjoxfQ.WlMDfygpDjYk8xHRQjpQgppwhVkUgYhyj_9iiPH_IXvXb8IKcD0cA2lFJCznB1B6llfW5Q3qiZcfRAdB78nDBw", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "manifest": { "contentHashes": { - "auditEntries": "982d081a268dc2608903cad931fa04ad2c16551782a5317c266a8d72b87028bd", - "fullView": "b53ff594240922c190a6d957f4725b0e2353711426811b463f5e8916c3b39678", - "publicView": "53bdb718a8598f84b9a107774e05ad9495e1575237752a3d5cc2e12eeaa6d572", + "auditEntries": "1ce4965e8bbf4b7e9b7cbf26d108ac4aa10392a2750a296c9b8e972801cc6835", + "fullView": "70f5a9779744ad58b77cebed129ecec4c24d7c5bbf3b65be4b3c61c4be5f0e80", + "publicView": "7ada532408d7c20c385804ca00d0a6a969f1448c796775c14657d7932ecf392a", "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" }, "coreVersion": "0.20.0", @@ -214,37 +599,116 @@ "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6Ijk4MmQwODFhMjY4ZGMyNjA4OTAzY2FkOTMxZmEwNGFkMmMxNjU1MTc4MmE1MzE3YzI2NmE4ZDcyYjg3MDI4YmQiLCJmdWxsVmlldyI6ImI1M2ZmNTk0MjQwOTIyYzE5MGE2ZDk1N2Y0NzI1YjBlMjM1MzcxMTQyNjgxMWI0NjNmNWU4OTE2YzNiMzk2NzgiLCJwdWJsaWNWaWV3IjoiNTNiZGI3MThhODU5OGY4NGI5YTEwNzc3NGUwNWFkOTQ5NWUxNTc1MjM3NzUyYTNkNWNjMmUxMmVlYWE2ZDU3MiIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.p8S8P2S5g_WgGFI-2nv8A6ZXsUsVektHHx5520x4Y5zN0TBaGp2GloYRGHGXfBs1D3e1l4deT8JT9qWMmVvgBw", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjFjZTQ5NjVlOGJiZjRiN2U5YjdjYmYyNmQxMDhhYzRhYTEwMzkyYTI3NTBhMjk2YzliOGU5NzI4MDFjYzY4MzUiLCJmdWxsVmlldyI6IjcwZjVhOTc3OTc0NGFkNThiNzdjZWJlZDEyOWVjZWM0YzI0ZDdjNWJiZjNiNjViZTRiM2M2MWM0YmU1ZjBlODAiLCJwdWJsaWNWaWV3IjoiN2FkYTUzMjQwOGQ3YzIwYzM4NTgwNGNhMDBkMGE2YTk2OWYxNDQ4Yzc5Njc3NWMxNDY1N2Q3OTMyZWNmMzkyYSIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.5t4VLTBbsEQ37gcC8296m3-u8WHKTXwVCqVS_GZQk5KezjIuV0eIdXU9iwjAUHeNPWJqntrMrgM6BKQ5jlVuAg", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.TewhsuBa83tHnqMcTgSUZf7CtbzAVvHXVRVOJYFkjJEgzOvMa6O7NGiIWQdwDFtVh-wWvztRkUTprTGV6PqsAQ", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjbzJlUGVyVW5pdCI6bnVsbCwiY29tbW9kaXR5Q29kZSI6Ijg1MDc2MDAwIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowMDowMFoiLCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsIm1hbnVmYWN0dXJlciI6eyJhZGRyZXNzIjoiTXVzdGVyc3RyYXNzZSAxLCA4MDMzMSBNw7xuY2hlbiwgREUiLCJjb3VudHJ5IjpudWxsLCJkaWRXZWJVcmwiOiJodHRwczovL25vZGUuYWNtZS5leGFtcGxlLy53ZWxsLWtub3duL2RpZC5qc29uIiwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgifSwibWF0ZXJpYWxzIjpbeyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJOTUMgODExIGNhdGhvZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjgsIndlaWdodEtnIjozLjF9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiR3JhcGhpdGUgYW5vZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjQsIndlaWdodEtnIjoxLjl9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiQWx1bWluaXVtIHBhY2sgaG91c2luZyIsInJlY3ljbGVkUGN0Ijo2MCwid2VpZ2h0S2ciOjIuMn1dLCJwbGFjZWRPbk1hcmtldERhdGUiOiIyMDI2LTA5LTAxIiwicHJvZHVjdEdyb3VwIjoiYmF0dGVyeSIsInByb2R1Y3RHcm91cERhdGEiOnsiYmF0dGVyeUNoZW1pc3RyeSI6Ik5NQyIsImJhdHRlcnlNb2RlbElkIjoiQUNNRS1MTVQtNDhWIiwiYmF0dGVyeVBhc3Nwb3J0TnVtYmVyIjoiVVJOOk9ETDpCQVRUOjA5NTA2MDAwMTM0MzUyOkFDTUUtTE1ULTIwMjYtMDkiLCJiYXR0ZXJ5VHlwZSI6ImxtdCIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY28yZVBlclVuaXRLZyI6ODYuNCwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZXVEZWNsYXJhdGlvbk9mQ29uZm9ybWl0eSI6IkVVLURvQyBBQ01FLTIwMjYtTE1ULTA5MDEsIHBlciBSZWd1bGF0aW9uIChFVSkgMjAyMy8xNTQyIEFydC4gMTgiLCJleHBlY3RlZExpZmV0aW1lQ3ljbGVzIjoxMjAwLCJleHBlY3RlZExpZmV0aW1lUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiwgMjUgZGVnQywgMTAwJSBEb0QiLCJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJoYXphcmRvdXNTdWJzdGFuY2VzIjpbeyJjYXNOdW1iZXIiOiIyMTMyNC00MC0zIiwiY29uY2VudHJhdGlvblBjdCI6MC44LCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIn1dLCJpbml0aWFsUm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTUsImludGVybmFsQ2VsbFJlc2lzdGFuY2VNb2htIjoxNCwiaW50ZXJuYWxQYWNrUmVzaXN0YW5jZU1vaG0iOjExMCwibWFudWZhY3R1cmluZ0RhdGUiOiIyMDI2LTA4LTE4VDAwOjAwOjAwWiIsIm1hbnVmYWN0dXJpbmdQbGFjZSI6Ik3DvG5jaGVuLCBERSIsIm1hcmtpbmdJbmZvcm1hdGlvbiI6IlNlcGFyYXRlLWNvbGxlY3Rpb24gc3ltYm9sIHBlciBBcnQuIDEzKDQpOyBjYXBhY2l0eSAyMCBBaCAvIDk2MCBXaDsgNDggViBMTVQiLCJtYXhpbXVtVm9sdGFnZVYiOjU0LjYsIm1pbmltYWxWb2x0YWdlViI6MzksIm5vbWluYWxDYXBhY2l0eUFoIjoyMCwibm9taW5hbFZvbHRhZ2VWIjo0OCwibm90SW5Vc2VUZW1wZXJhdHVyZVJhbmdlIjp7Im1heEMiOjQ1LCJtaW5DIjotMTB9LCJub3RJblVzZVRlbXBlcmF0dXJlUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiBzdG9yYWdlIGNsYXVzZSIsIm9yaWdpbmFsUG93ZXJDYXBhYmlsaXR5VyI6OTYwLCJwb3dlckxpbWl0TWF4VyI6MTUwMCwicG93ZXJMaW1pdE1pblciOjIwLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjYuMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.OnjeF7qUrlZOKc-Xx8u-ThMayslJBs7HPAmpZBFKvyJpivIqOrN5lTCW15PsoyBeSOpjmRq3Mm_UC99B-Ya-Dg", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "transferChain": { diff --git a/site/dpp-landing/public/verify/examples/03-valid-with-eol.json b/site/dpp-landing/public/verify/examples/03-valid-with-eol.json index 203e5ce..c97ae2d 100644 --- a/site/dpp-landing/public/verify/examples/03-valid-with-eol.json +++ b/site/dpp-landing/public/verify/examples/03-valid-with-eol.json @@ -16,68 +16,300 @@ { "action": "published", "actor": "api-key", - "entryHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "entryHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", - "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" - }, - "publicViewPayload": { - "content": { - "B": 2, - "b": 1, + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], "batteryChemistry": "NMC", "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." }, + "productName": "Acme LMT 48V 20Ah battery pack", + "publishedAt": "2026-09-01T09:05:00Z", + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 + }, + "publicViewPayload": { + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "newStatus": "active", @@ -90,7 +322,7 @@ { "action": "deactivated", "actor": "api-key", - "entryHash": "4bd4656a21238e8fbd66bc35dee9f70c05d400462dc27f66d826dbb8837c2fee", + "entryHash": "f5fe9d960a95161eb72590f7a8c644c6ed022f956f1d8aaf84eb8a93b9154c58", "id": "0199a3c2-7b10-7e4a-9c3d-000000000005", "metadata": { "declaredAt": "2026-09-20T08:00:00Z", @@ -103,7 +335,7 @@ }, "newStatus": "deactivated", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "prevHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "previousStatus": "active", "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000005", "timestamp": "2026-09-20T08:00:00.018Z" @@ -142,45 +374,198 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.bXoh9eU-rTvN-_heLcDWRfRe5hNQA_JmZnmSzNKr2t0f56qklAYFHb8xyevrfbb3phscXigOs8vbyAPMcOJ_Dg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImd0aW4iOiIwOTUwNjAwMDEzNDM1MiIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJyYXRlZENhcGFjaXR5S3doIjpudWxsLCJyZWN5Y2xlZENvbnRlbnRDb2JhbHRQY3QiOjEyLCJyZWN5Y2xlZENvbnRlbnRMZWFkUGN0IjowLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0Ijo2LCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOjgsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uTG9ja2VkIjp0cnVlLCJyZXRlbnRpb25VbnRpbCI6IjIwMzYtMDgtMjlUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNi4wIiwic3RhdHVzIjoiYWN0aXZlIiwidXBkYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJ2ZXJzaW9uIjoxfQ.WlMDfygpDjYk8xHRQjpQgppwhVkUgYhyj_9iiPH_IXvXb8IKcD0cA2lFJCznB1B6llfW5Q3qiZcfRAdB78nDBw", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "manifest": { "contentHashes": { - "auditEntries": "18bf7c9f6a1be8bbb56fcf22afc506cfeefbf5f2ac5fc72668399216588cb625", + "auditEntries": "1799b3885d95741b2209401519a69f5db27cdee31b42b8003a1dd0e0c53a7363", "eolEvent": "7cb65bb485538e109105404e15c7bc411aa3050694fa4d63339c70dbe431d3a0", - "fullView": "b53ff594240922c190a6d957f4725b0e2353711426811b463f5e8916c3b39678", - "publicView": "53bdb718a8598f84b9a107774e05ad9495e1575237752a3d5cc2e12eeaa6d572" + "fullView": "70f5a9779744ad58b77cebed129ecec4c24d7c5bbf3b65be4b3c61c4be5f0e80", + "publicView": "7ada532408d7c20c385804ca00d0a6a969f1448c796775c14657d7932ecf392a" }, "coreVersion": "0.20.0", "createdAt": "2026-09-25T12:00:00Z", @@ -189,37 +574,116 @@ "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjE4YmY3YzlmNmExYmU4YmJiNTZmY2YyMmFmYzUwNmNmZWVmYmY1ZjJhYzVmYzcyNjY4Mzk5MjE2NTg4Y2I2MjUiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6ImI1M2ZmNTk0MjQwOTIyYzE5MGE2ZDk1N2Y0NzI1YjBlMjM1MzcxMTQyNjgxMWI0NjNmNWU4OTE2YzNiMzk2NzgiLCJwdWJsaWNWaWV3IjoiNTNiZGI3MThhODU5OGY4NGI5YTEwNzc3NGUwNWFkOTQ5NWUxNTc1MjM3NzUyYTNkNWNjMmUxMmVlYWE2ZDU3MiJ9LCJjb3JlVmVyc2lvbiI6IjAuMjAuMCIsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjVUMTI6MDA6MDBaIiwiZm9ybWF0VmVyc2lvbiI6IjEiLCJpc3N1ZXJEaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwibm9kZVZlcnNpb24iOiIwLjE0LjAiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIn0.I2sZUWf797ygySvXCzGDbZAaTWbVBtcQ4UXpXF80ru-gIz20O_RCYDmO7GjAx9Vy7sebYlr71bJjBUZWnzpiAg", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjE3OTliMzg4NWQ5NTc0MWIyMjA5NDAxNTE5YTY5ZjVkYjI3Y2RlZTMxYjQyYjgwMDNhMWRkMGUwYzUzYTczNjMiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6IjcwZjVhOTc3OTc0NGFkNThiNzdjZWJlZDEyOWVjZWM0YzI0ZDdjNWJiZjNiNjViZTRiM2M2MWM0YmU1ZjBlODAiLCJwdWJsaWNWaWV3IjoiN2FkYTUzMjQwOGQ3YzIwYzM4NTgwNGNhMDBkMGE2YTk2OWYxNDQ4Yzc5Njc3NWMxNDY1N2Q3OTMyZWNmMzkyYSJ9LCJjb3JlVmVyc2lvbiI6IjAuMjAuMCIsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjVUMTI6MDA6MDBaIiwiZm9ybWF0VmVyc2lvbiI6IjEiLCJpc3N1ZXJEaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwibm9kZVZlcnNpb24iOiIwLjE0LjAiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIn0.M7bMTQFVEzMax6kR3S4njIrYgEIC6qvquslOMdUxwJIWD4Iq_dg3qVyLwdBc6g6RpJcXBXOZQMMsoKbyDHZACQ", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.TewhsuBa83tHnqMcTgSUZf7CtbzAVvHXVRVOJYFkjJEgzOvMa6O7NGiIWQdwDFtVh-wWvztRkUTprTGV6PqsAQ", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjbzJlUGVyVW5pdCI6bnVsbCwiY29tbW9kaXR5Q29kZSI6Ijg1MDc2MDAwIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowMDowMFoiLCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsIm1hbnVmYWN0dXJlciI6eyJhZGRyZXNzIjoiTXVzdGVyc3RyYXNzZSAxLCA4MDMzMSBNw7xuY2hlbiwgREUiLCJjb3VudHJ5IjpudWxsLCJkaWRXZWJVcmwiOiJodHRwczovL25vZGUuYWNtZS5leGFtcGxlLy53ZWxsLWtub3duL2RpZC5qc29uIiwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgifSwibWF0ZXJpYWxzIjpbeyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJOTUMgODExIGNhdGhvZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjgsIndlaWdodEtnIjozLjF9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiR3JhcGhpdGUgYW5vZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjQsIndlaWdodEtnIjoxLjl9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiQWx1bWluaXVtIHBhY2sgaG91c2luZyIsInJlY3ljbGVkUGN0Ijo2MCwid2VpZ2h0S2ciOjIuMn1dLCJwbGFjZWRPbk1hcmtldERhdGUiOiIyMDI2LTA5LTAxIiwicHJvZHVjdEdyb3VwIjoiYmF0dGVyeSIsInByb2R1Y3RHcm91cERhdGEiOnsiYmF0dGVyeUNoZW1pc3RyeSI6Ik5NQyIsImJhdHRlcnlNb2RlbElkIjoiQUNNRS1MTVQtNDhWIiwiYmF0dGVyeVBhc3Nwb3J0TnVtYmVyIjoiVVJOOk9ETDpCQVRUOjA5NTA2MDAwMTM0MzUyOkFDTUUtTE1ULTIwMjYtMDkiLCJiYXR0ZXJ5VHlwZSI6ImxtdCIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY28yZVBlclVuaXRLZyI6ODYuNCwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZXVEZWNsYXJhdGlvbk9mQ29uZm9ybWl0eSI6IkVVLURvQyBBQ01FLTIwMjYtTE1ULTA5MDEsIHBlciBSZWd1bGF0aW9uIChFVSkgMjAyMy8xNTQyIEFydC4gMTgiLCJleHBlY3RlZExpZmV0aW1lQ3ljbGVzIjoxMjAwLCJleHBlY3RlZExpZmV0aW1lUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiwgMjUgZGVnQywgMTAwJSBEb0QiLCJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJoYXphcmRvdXNTdWJzdGFuY2VzIjpbeyJjYXNOdW1iZXIiOiIyMTMyNC00MC0zIiwiY29uY2VudHJhdGlvblBjdCI6MC44LCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIn1dLCJpbml0aWFsUm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTUsImludGVybmFsQ2VsbFJlc2lzdGFuY2VNb2htIjoxNCwiaW50ZXJuYWxQYWNrUmVzaXN0YW5jZU1vaG0iOjExMCwibWFudWZhY3R1cmluZ0RhdGUiOiIyMDI2LTA4LTE4VDAwOjAwOjAwWiIsIm1hbnVmYWN0dXJpbmdQbGFjZSI6Ik3DvG5jaGVuLCBERSIsIm1hcmtpbmdJbmZvcm1hdGlvbiI6IlNlcGFyYXRlLWNvbGxlY3Rpb24gc3ltYm9sIHBlciBBcnQuIDEzKDQpOyBjYXBhY2l0eSAyMCBBaCAvIDk2MCBXaDsgNDggViBMTVQiLCJtYXhpbXVtVm9sdGFnZVYiOjU0LjYsIm1pbmltYWxWb2x0YWdlViI6MzksIm5vbWluYWxDYXBhY2l0eUFoIjoyMCwibm9taW5hbFZvbHRhZ2VWIjo0OCwibm90SW5Vc2VUZW1wZXJhdHVyZVJhbmdlIjp7Im1heEMiOjQ1LCJtaW5DIjotMTB9LCJub3RJblVzZVRlbXBlcmF0dXJlUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiBzdG9yYWdlIGNsYXVzZSIsIm9yaWdpbmFsUG93ZXJDYXBhYmlsaXR5VyI6OTYwLCJwb3dlckxpbWl0TWF4VyI6MTUwMCwicG93ZXJMaW1pdE1pblciOjIwLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjYuMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.OnjeF7qUrlZOKc-Xx8u-ThMayslJBs7HPAmpZBFKvyJpivIqOrN5lTCW15PsoyBeSOpjmRq3Mm_UC99B-Ya-Dg", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } } } \ No newline at end of file diff --git a/site/dpp-landing/public/verify/examples/04-valid-full-lifecycle.json b/site/dpp-landing/public/verify/examples/04-valid-full-lifecycle.json index cfba4c4..f3f24b3 100644 --- a/site/dpp-landing/public/verify/examples/04-valid-full-lifecycle.json +++ b/site/dpp-landing/public/verify/examples/04-valid-full-lifecycle.json @@ -16,68 +16,300 @@ { "action": "published", "actor": "api-key", - "entryHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "entryHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", - "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" - }, - "publicViewPayload": { - "content": { - "B": 2, - "b": 1, + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], "batteryChemistry": "NMC", "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." }, + "productName": "Acme LMT 48V 20Ah battery pack", + "publishedAt": "2026-09-01T09:05:00Z", + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 + }, + "publicViewPayload": { + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "newStatus": "active", @@ -90,7 +322,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "fb94dc5d624df166d3c8818ca7314033d9686dc629ddb1fbfb2be44c04b758af", + "entryHash": "5c869a32da993e9a5ee13bc3b059fee9c3abc1fbbd840ab2449473f0e5453b08", "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", "metadata": { "event": "transfer.initiated", @@ -99,7 +331,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "prevHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000003", "timestamp": "2026-09-10T14:00:00.042Z" @@ -107,7 +339,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "c4862c22734f57d4834cdb0b7aead85efd2351164ee6119b2a56c95ffd76dd9c", + "entryHash": "3a5eb46b99bc15d235b342a0a6092d3e382d998b7de32588ed4966d74295fa51", "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", "metadata": { "event": "transfer.accepted", @@ -116,7 +348,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "fb94dc5d624df166d3c8818ca7314033d9686dc629ddb1fbfb2be44c04b758af", + "prevHash": "5c869a32da993e9a5ee13bc3b059fee9c3abc1fbbd840ab2449473f0e5453b08", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000004", "timestamp": "2026-09-10T14:30:00.123456Z" @@ -124,7 +356,7 @@ { "action": "deactivated", "actor": "api-key", - "entryHash": "05d0c1bb01b38181270d81c02a52fb46f8fca04b6f69cac79b74208224aa6a82", + "entryHash": "256b89a07f8ff158fe29f327e08996214563987ca111cddc563bb0e9a9f9a362", "id": "0199a3c2-7b10-7e4a-9c3d-000000000005", "metadata": { "declaredAt": "2026-09-20T08:00:00Z", @@ -137,7 +369,7 @@ }, "newStatus": "deactivated", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "c4862c22734f57d4834cdb0b7aead85efd2351164ee6119b2a56c95ffd76dd9c", + "prevHash": "3a5eb46b99bc15d235b342a0a6092d3e382d998b7de32588ed4966d74295fa51", "previousStatus": "active", "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000005", "timestamp": "2026-09-20T08:00:00.018Z" @@ -197,45 +429,198 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.bXoh9eU-rTvN-_heLcDWRfRe5hNQA_JmZnmSzNKr2t0f56qklAYFHb8xyevrfbb3phscXigOs8vbyAPMcOJ_Dg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImd0aW4iOiIwOTUwNjAwMDEzNDM1MiIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJyYXRlZENhcGFjaXR5S3doIjpudWxsLCJyZWN5Y2xlZENvbnRlbnRDb2JhbHRQY3QiOjEyLCJyZWN5Y2xlZENvbnRlbnRMZWFkUGN0IjowLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0Ijo2LCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOjgsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uTG9ja2VkIjp0cnVlLCJyZXRlbnRpb25VbnRpbCI6IjIwMzYtMDgtMjlUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNi4wIiwic3RhdHVzIjoiYWN0aXZlIiwidXBkYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJ2ZXJzaW9uIjoxfQ.WlMDfygpDjYk8xHRQjpQgppwhVkUgYhyj_9iiPH_IXvXb8IKcD0cA2lFJCznB1B6llfW5Q3qiZcfRAdB78nDBw", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "manifest": { "contentHashes": { - "auditEntries": "4e4ce37e3baccfbf97c635a4c14f1f59867fdb5d9bd4e1f2b5b7b042d87bdd92", + "auditEntries": "d3e3661902ae8ebc797d545c6124173a74c3b086b2bba8399628df6b53db79e0", "eolEvent": "7cb65bb485538e109105404e15c7bc411aa3050694fa4d63339c70dbe431d3a0", - "fullView": "b53ff594240922c190a6d957f4725b0e2353711426811b463f5e8916c3b39678", - "publicView": "53bdb718a8598f84b9a107774e05ad9495e1575237752a3d5cc2e12eeaa6d572", + "fullView": "70f5a9779744ad58b77cebed129ecec4c24d7c5bbf3b65be4b3c61c4be5f0e80", + "publicView": "7ada532408d7c20c385804ca00d0a6a969f1448c796775c14657d7932ecf392a", "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" }, "coreVersion": "0.20.0", @@ -245,37 +630,116 @@ "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjRlNGNlMzdlM2JhY2NmYmY5N2M2MzVhNGMxNGYxZjU5ODY3ZmRiNWQ5YmQ0ZTFmMmI1YjdiMDQyZDg3YmRkOTIiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6ImI1M2ZmNTk0MjQwOTIyYzE5MGE2ZDk1N2Y0NzI1YjBlMjM1MzcxMTQyNjgxMWI0NjNmNWU4OTE2YzNiMzk2NzgiLCJwdWJsaWNWaWV3IjoiNTNiZGI3MThhODU5OGY4NGI5YTEwNzc3NGUwNWFkOTQ5NWUxNTc1MjM3NzUyYTNkNWNjMmUxMmVlYWE2ZDU3MiIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.-RH5CefIIgnBJ33VqE9-flY978EP3fD3ooT5X49R1hn7Vj_2z80ipco9Lf-zaw7jTZ6WXF92VPpG6yUjGNkJBg", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImQzZTM2NjE5MDJhZThlYmM3OTdkNTQ1YzYxMjQxNzNhNzRjM2IwODZiMmJiYTgzOTk2MjhkZjZiNTNkYjc5ZTAiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6IjcwZjVhOTc3OTc0NGFkNThiNzdjZWJlZDEyOWVjZWM0YzI0ZDdjNWJiZjNiNjViZTRiM2M2MWM0YmU1ZjBlODAiLCJwdWJsaWNWaWV3IjoiN2FkYTUzMjQwOGQ3YzIwYzM4NTgwNGNhMDBkMGE2YTk2OWYxNDQ4Yzc5Njc3NWMxNDY1N2Q3OTMyZWNmMzkyYSIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.Y4XyqkZCD-XAtSYTICoyCngJx7sDCk7sYcdyezYe7ZDDY0k4w8WhK1oUaB5x_zg-malEGWJ_dXIVyI0P_M_qBg", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.TewhsuBa83tHnqMcTgSUZf7CtbzAVvHXVRVOJYFkjJEgzOvMa6O7NGiIWQdwDFtVh-wWvztRkUTprTGV6PqsAQ", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjbzJlUGVyVW5pdCI6bnVsbCwiY29tbW9kaXR5Q29kZSI6Ijg1MDc2MDAwIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowMDowMFoiLCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsIm1hbnVmYWN0dXJlciI6eyJhZGRyZXNzIjoiTXVzdGVyc3RyYXNzZSAxLCA4MDMzMSBNw7xuY2hlbiwgREUiLCJjb3VudHJ5IjpudWxsLCJkaWRXZWJVcmwiOiJodHRwczovL25vZGUuYWNtZS5leGFtcGxlLy53ZWxsLWtub3duL2RpZC5qc29uIiwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgifSwibWF0ZXJpYWxzIjpbeyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJOTUMgODExIGNhdGhvZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjgsIndlaWdodEtnIjozLjF9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiR3JhcGhpdGUgYW5vZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjQsIndlaWdodEtnIjoxLjl9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiQWx1bWluaXVtIHBhY2sgaG91c2luZyIsInJlY3ljbGVkUGN0Ijo2MCwid2VpZ2h0S2ciOjIuMn1dLCJwbGFjZWRPbk1hcmtldERhdGUiOiIyMDI2LTA5LTAxIiwicHJvZHVjdEdyb3VwIjoiYmF0dGVyeSIsInByb2R1Y3RHcm91cERhdGEiOnsiYmF0dGVyeUNoZW1pc3RyeSI6Ik5NQyIsImJhdHRlcnlNb2RlbElkIjoiQUNNRS1MTVQtNDhWIiwiYmF0dGVyeVBhc3Nwb3J0TnVtYmVyIjoiVVJOOk9ETDpCQVRUOjA5NTA2MDAwMTM0MzUyOkFDTUUtTE1ULTIwMjYtMDkiLCJiYXR0ZXJ5VHlwZSI6ImxtdCIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY28yZVBlclVuaXRLZyI6ODYuNCwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZXVEZWNsYXJhdGlvbk9mQ29uZm9ybWl0eSI6IkVVLURvQyBBQ01FLTIwMjYtTE1ULTA5MDEsIHBlciBSZWd1bGF0aW9uIChFVSkgMjAyMy8xNTQyIEFydC4gMTgiLCJleHBlY3RlZExpZmV0aW1lQ3ljbGVzIjoxMjAwLCJleHBlY3RlZExpZmV0aW1lUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiwgMjUgZGVnQywgMTAwJSBEb0QiLCJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJoYXphcmRvdXNTdWJzdGFuY2VzIjpbeyJjYXNOdW1iZXIiOiIyMTMyNC00MC0zIiwiY29uY2VudHJhdGlvblBjdCI6MC44LCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIn1dLCJpbml0aWFsUm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTUsImludGVybmFsQ2VsbFJlc2lzdGFuY2VNb2htIjoxNCwiaW50ZXJuYWxQYWNrUmVzaXN0YW5jZU1vaG0iOjExMCwibWFudWZhY3R1cmluZ0RhdGUiOiIyMDI2LTA4LTE4VDAwOjAwOjAwWiIsIm1hbnVmYWN0dXJpbmdQbGFjZSI6Ik3DvG5jaGVuLCBERSIsIm1hcmtpbmdJbmZvcm1hdGlvbiI6IlNlcGFyYXRlLWNvbGxlY3Rpb24gc3ltYm9sIHBlciBBcnQuIDEzKDQpOyBjYXBhY2l0eSAyMCBBaCAvIDk2MCBXaDsgNDggViBMTVQiLCJtYXhpbXVtVm9sdGFnZVYiOjU0LjYsIm1pbmltYWxWb2x0YWdlViI6MzksIm5vbWluYWxDYXBhY2l0eUFoIjoyMCwibm9taW5hbFZvbHRhZ2VWIjo0OCwibm90SW5Vc2VUZW1wZXJhdHVyZVJhbmdlIjp7Im1heEMiOjQ1LCJtaW5DIjotMTB9LCJub3RJblVzZVRlbXBlcmF0dXJlUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiBzdG9yYWdlIGNsYXVzZSIsIm9yaWdpbmFsUG93ZXJDYXBhYmlsaXR5VyI6OTYwLCJwb3dlckxpbWl0TWF4VyI6MTUwMCwicG93ZXJMaW1pdE1pblciOjIwLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjYuMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.OnjeF7qUrlZOKc-Xx8u-ThMayslJBs7HPAmpZBFKvyJpivIqOrN5lTCW15PsoyBeSOpjmRq3Mm_UC99B-Ya-Dg", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "transferChain": { diff --git a/site/dpp-landing/public/verify/examples/05-tampered-signature.json b/site/dpp-landing/public/verify/examples/05-tampered-signature.json index 1fee5f1..a438f97 100644 --- a/site/dpp-landing/public/verify/examples/05-tampered-signature.json +++ b/site/dpp-landing/public/verify/examples/05-tampered-signature.json @@ -16,68 +16,300 @@ { "action": "published", "actor": "api-key", - "entryHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "entryHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", - "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" - }, - "publicViewPayload": { - "content": { - "B": 2, - "b": 1, + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], "batteryChemistry": "NMC", "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." }, + "productName": "Acme LMT 48V 20Ah battery pack", + "publishedAt": "2026-09-01T09:05:00Z", + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 + }, + "publicViewPayload": { + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "newStatus": "active", @@ -90,7 +322,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "fb94dc5d624df166d3c8818ca7314033d9686dc629ddb1fbfb2be44c04b758af", + "entryHash": "5c869a32da993e9a5ee13bc3b059fee9c3abc1fbbd840ab2449473f0e5453b08", "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", "metadata": { "event": "transfer.initiated", @@ -99,7 +331,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "prevHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000003", "timestamp": "2026-09-10T14:00:00.042Z" @@ -107,7 +339,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "c4862c22734f57d4834cdb0b7aead85efd2351164ee6119b2a56c95ffd76dd9c", + "entryHash": "3a5eb46b99bc15d235b342a0a6092d3e382d998b7de32588ed4966d74295fa51", "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", "metadata": { "event": "transfer.accepted", @@ -116,7 +348,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "fb94dc5d624df166d3c8818ca7314033d9686dc629ddb1fbfb2be44c04b758af", + "prevHash": "5c869a32da993e9a5ee13bc3b059fee9c3abc1fbbd840ab2449473f0e5453b08", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000004", "timestamp": "2026-09-10T14:30:00.123456Z" @@ -124,7 +356,7 @@ { "action": "deactivated", "actor": "api-key", - "entryHash": "05d0c1bb01b38181270d81c02a52fb46f8fca04b6f69cac79b74208224aa6a82", + "entryHash": "256b89a07f8ff158fe29f327e08996214563987ca111cddc563bb0e9a9f9a362", "id": "0199a3c2-7b10-7e4a-9c3d-000000000005", "metadata": { "declaredAt": "2026-09-20T08:00:00Z", @@ -137,7 +369,7 @@ }, "newStatus": "deactivated", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "c4862c22734f57d4834cdb0b7aead85efd2351164ee6119b2a56c95ffd76dd9c", + "prevHash": "3a5eb46b99bc15d235b342a0a6092d3e382d998b7de32588ed4966d74295fa51", "previousStatus": "active", "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000005", "timestamp": "2026-09-20T08:00:00.018Z" @@ -197,45 +429,198 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.bXoh9eU-rTvN-_heLcDWRfRe5hNQA_JmZnmSzNKr2t0f56qklAYFHb8xyevrfbb3phscXigOs8vbyAPMcOJ_Dg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImd0aW4iOiIwOTUwNjAwMDEzNDM1MiIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJyYXRlZENhcGFjaXR5S3doIjpudWxsLCJyZWN5Y2xlZENvbnRlbnRDb2JhbHRQY3QiOjEyLCJyZWN5Y2xlZENvbnRlbnRMZWFkUGN0IjowLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0Ijo2LCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOjgsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uTG9ja2VkIjp0cnVlLCJyZXRlbnRpb25VbnRpbCI6IjIwMzYtMDgtMjlUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNi4wIiwic3RhdHVzIjoiYWN0aXZlIiwidXBkYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJ2ZXJzaW9uIjoxfQ.WlMDfygpDjYk8xHRQjpQgppwhVkUgYhyj_9iiPH_IXvXb8IKcD0cA2lFJCznB1B6llfW5Q3qiZcfRAdB78nDBw", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "manifest": { "contentHashes": { - "auditEntries": "4e4ce37e3baccfbf97c635a4c14f1f59867fdb5d9bd4e1f2b5b7b042d87bdd92", + "auditEntries": "d3e3661902ae8ebc797d545c6124173a74c3b086b2bba8399628df6b53db79e0", "eolEvent": "7cb65bb485538e109105404e15c7bc411aa3050694fa4d63339c70dbe431d3a0", - "fullView": "b53ff594240922c190a6d957f4725b0e2353711426811b463f5e8916c3b39678", - "publicView": "53bdb718a8598f84b9a107774e05ad9495e1575237752a3d5cc2e12eeaa6d572", + "fullView": "70f5a9779744ad58b77cebed129ecec4c24d7c5bbf3b65be4b3c61c4be5f0e80", + "publicView": "7ada532408d7c20c385804ca00d0a6a969f1448c796775c14657d7932ecf392a", "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" }, "coreVersion": "0.20.0", @@ -245,37 +630,116 @@ "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjRlNGNlMzdlM2JhY2NmYmY5N2M2MzVhNGMxNGYxZjU5ODY3ZmRiNWQ5YmQ0ZTFmMmI1YjdiMDQyZDg3YmRkOTIiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6ImI1M2ZmNTk0MjQwOTIyYzE5MGE2ZDk1N2Y0NzI1YjBlMjM1MzcxMTQyNjgxMWI0NjNmNWU4OTE2YzNiMzk2NzgiLCJwdWJsaWNWaWV3IjoiNTNiZGI3MThhODU5OGY4NGI5YTEwNzc3NGUwNWFkOTQ5NWUxNTc1MjM3NzUyYTNkNWNjMmUxMmVlYWE2ZDU3MiIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.-RH5CefIIgnBJ33VqE9-flY978EP3fD3ooT5X49R1hn7Vj_2z80ipco9Lf-zaw7jTZ6WXF92VPpG6yUjGNkJBg", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImQzZTM2NjE5MDJhZThlYmM3OTdkNTQ1YzYxMjQxNzNhNzRjM2IwODZiMmJiYTgzOTk2MjhkZjZiNTNkYjc5ZTAiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6IjcwZjVhOTc3OTc0NGFkNThiNzdjZWJlZDEyOWVjZWM0YzI0ZDdjNWJiZjNiNjViZTRiM2M2MWM0YmU1ZjBlODAiLCJwdWJsaWNWaWV3IjoiN2FkYTUzMjQwOGQ3YzIwYzM4NTgwNGNhMDBkMGE2YTk2OWYxNDQ4Yzc5Njc3NWMxNDY1N2Q3OTMyZWNmMzkyYSIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.Y4XyqkZCD-XAtSYTICoyCngJx7sDCk7sYcdyezYe7ZDDY0k4w8WhK1oUaB5x_zg-malEGWJ_dXIVyI0P_M_qBg", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.TewhsuBa83tHnqMcTgSUZf7CtbzAVvHXVRVOJYFkjJEAzOvMa6O7NGiIWQdwDFtVh-wWvztRkUTprTGV6PqsAQ", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjbzJlUGVyVW5pdCI6bnVsbCwiY29tbW9kaXR5Q29kZSI6Ijg1MDc2MDAwIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowMDowMFoiLCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsIm1hbnVmYWN0dXJlciI6eyJhZGRyZXNzIjoiTXVzdGVyc3RyYXNzZSAxLCA4MDMzMSBNw7xuY2hlbiwgREUiLCJjb3VudHJ5IjpudWxsLCJkaWRXZWJVcmwiOiJodHRwczovL25vZGUuYWNtZS5leGFtcGxlLy53ZWxsLWtub3duL2RpZC5qc29uIiwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgifSwibWF0ZXJpYWxzIjpbeyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJOTUMgODExIGNhdGhvZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjgsIndlaWdodEtnIjozLjF9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiR3JhcGhpdGUgYW5vZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjQsIndlaWdodEtnIjoxLjl9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiQWx1bWluaXVtIHBhY2sgaG91c2luZyIsInJlY3ljbGVkUGN0Ijo2MCwid2VpZ2h0S2ciOjIuMn1dLCJwbGFjZWRPbk1hcmtldERhdGUiOiIyMDI2LTA5LTAxIiwicHJvZHVjdEdyb3VwIjoiYmF0dGVyeSIsInByb2R1Y3RHcm91cERhdGEiOnsiYmF0dGVyeUNoZW1pc3RyeSI6Ik5NQyIsImJhdHRlcnlNb2RlbElkIjoiQUNNRS1MTVQtNDhWIiwiYmF0dGVyeVBhc3Nwb3J0TnVtYmVyIjoiVVJOOk9ETDpCQVRUOjA5NTA2MDAwMTM0MzUyOkFDTUUtTE1ULTIwMjYtMDkiLCJiYXR0ZXJ5VHlwZSI6ImxtdCIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY28yZVBlclVuaXRLZyI6ODYuNCwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZXVEZWNsYXJhdGlvbk9mQ29uZm9ybWl0eSI6IkVVLURvQyBBQ01FLTIwMjYtTE1ULTA5MDEsIHBlciBSZWd1bGF0aW9uIChFVSkgMjAyMy8xNTQyIEFydC4gMTgiLCJleHBlY3RlZExpZmV0aW1lQ3ljbGVzIjoxMjAwLCJleHBlY3RlZExpZmV0aW1lUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiwgMjUgZGVnQywgMTAwJSBEb0QiLCJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJoYXphcmRvdXNTdWJzdGFuY2VzIjpbeyJjYXNOdW1iZXIiOiIyMTMyNC00MC0zIiwiY29uY2VudHJhdGlvblBjdCI6MC44LCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIn1dLCJpbml0aWFsUm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTUsImludGVybmFsQ2VsbFJlc2lzdGFuY2VNb2htIjoxNCwiaW50ZXJuYWxQYWNrUmVzaXN0YW5jZU1vaG0iOjExMCwibWFudWZhY3R1cmluZ0RhdGUiOiIyMDI2LTA4LTE4VDAwOjAwOjAwWiIsIm1hbnVmYWN0dXJpbmdQbGFjZSI6Ik3DvG5jaGVuLCBERSIsIm1hcmtpbmdJbmZvcm1hdGlvbiI6IlNlcGFyYXRlLWNvbGxlY3Rpb24gc3ltYm9sIHBlciBBcnQuIDEzKDQpOyBjYXBhY2l0eSAyMCBBaCAvIDk2MCBXaDsgNDggViBMTVQiLCJtYXhpbXVtVm9sdGFnZVYiOjU0LjYsIm1pbmltYWxWb2x0YWdlViI6MzksIm5vbWluYWxDYXBhY2l0eUFoIjoyMCwibm9taW5hbFZvbHRhZ2VWIjo0OCwibm90SW5Vc2VUZW1wZXJhdHVyZVJhbmdlIjp7Im1heEMiOjQ1LCJtaW5DIjotMTB9LCJub3RJblVzZVRlbXBlcmF0dXJlUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiBzdG9yYWdlIGNsYXVzZSIsIm9yaWdpbmFsUG93ZXJDYXBhYmlsaXR5VyI6OTYwLCJwb3dlckxpbWl0TWF4VyI6MTUwMCwicG93ZXJMaW1pdE1pblciOjIwLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjYuMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.OnjeF7qUrlZOKc-Xx8u-ThMayslJBs7HPAmpZBFKvyJAivIqOrN5lTCW15PsoyBeSOpjmRq3Mm_UC99B-Ya-Dg", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "transferChain": { diff --git a/site/dpp-landing/public/verify/examples/06-tampered-audit-entry.json b/site/dpp-landing/public/verify/examples/06-tampered-audit-entry.json index d50e29b..0c6470c 100644 --- a/site/dpp-landing/public/verify/examples/06-tampered-audit-entry.json +++ b/site/dpp-landing/public/verify/examples/06-tampered-audit-entry.json @@ -16,68 +16,300 @@ { "action": "published", "actor": "api-key", - "entryHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "entryHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", - "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" - }, - "publicViewPayload": { - "content": { - "B": 2, - "b": 1, + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], "batteryChemistry": "NMC", "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." }, + "productName": "Acme LMT 48V 20Ah battery pack", + "publishedAt": "2026-09-01T09:05:00Z", + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 + }, + "publicViewPayload": { + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "newStatus": "active", @@ -90,7 +322,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "fb94dc5d624df166d3c8818ca7314033d9686dc629ddb1fbfb2be44c04b758af", + "entryHash": "5c869a32da993e9a5ee13bc3b059fee9c3abc1fbbd840ab2449473f0e5453b08", "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", "metadata": { "event": "transfer.initiated", @@ -99,7 +331,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "prevHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000003", "timestamp": "2026-09-10T14:00:00.042Z" @@ -107,7 +339,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "c4862c22734f57d4834cdb0b7aead85efd2351164ee6119b2a56c95ffd76dd9c", + "entryHash": "3a5eb46b99bc15d235b342a0a6092d3e382d998b7de32588ed4966d74295fa51", "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", "metadata": { "event": "transfer.accepted", @@ -116,7 +348,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "fb94dc5d624df166d3c8818ca7314033d9686dc629ddb1fbfb2be44c04b758af", + "prevHash": "5c869a32da993e9a5ee13bc3b059fee9c3abc1fbbd840ab2449473f0e5453b08", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000004", "timestamp": "2026-09-10T14:30:00.123456Z" @@ -124,7 +356,7 @@ { "action": "deactivated", "actor": "api-key", - "entryHash": "05d0c1bb01b38181270d81c02a52fb46f8fca04b6f69cac79b74208224aa6a82", + "entryHash": "256b89a07f8ff158fe29f327e08996214563987ca111cddc563bb0e9a9f9a362", "id": "0199a3c2-7b10-7e4a-9c3d-000000000005", "metadata": { "declaredAt": "2026-09-20T08:00:00Z", @@ -137,7 +369,7 @@ }, "newStatus": "deactivated", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "c4862c22734f57d4834cdb0b7aead85efd2351164ee6119b2a56c95ffd76dd9c", + "prevHash": "3a5eb46b99bc15d235b342a0a6092d3e382d998b7de32588ed4966d74295fa51", "previousStatus": "active", "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000005", "timestamp": "2026-09-20T08:00:00.018Z" @@ -197,45 +429,198 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.bXoh9eU-rTvN-_heLcDWRfRe5hNQA_JmZnmSzNKr2t0f56qklAYFHb8xyevrfbb3phscXigOs8vbyAPMcOJ_Dg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImd0aW4iOiIwOTUwNjAwMDEzNDM1MiIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJyYXRlZENhcGFjaXR5S3doIjpudWxsLCJyZWN5Y2xlZENvbnRlbnRDb2JhbHRQY3QiOjEyLCJyZWN5Y2xlZENvbnRlbnRMZWFkUGN0IjowLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0Ijo2LCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOjgsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uTG9ja2VkIjp0cnVlLCJyZXRlbnRpb25VbnRpbCI6IjIwMzYtMDgtMjlUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNi4wIiwic3RhdHVzIjoiYWN0aXZlIiwidXBkYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJ2ZXJzaW9uIjoxfQ.WlMDfygpDjYk8xHRQjpQgppwhVkUgYhyj_9iiPH_IXvXb8IKcD0cA2lFJCznB1B6llfW5Q3qiZcfRAdB78nDBw", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "manifest": { "contentHashes": { - "auditEntries": "4e4ce37e3baccfbf97c635a4c14f1f59867fdb5d9bd4e1f2b5b7b042d87bdd92", + "auditEntries": "d3e3661902ae8ebc797d545c6124173a74c3b086b2bba8399628df6b53db79e0", "eolEvent": "7cb65bb485538e109105404e15c7bc411aa3050694fa4d63339c70dbe431d3a0", - "fullView": "b53ff594240922c190a6d957f4725b0e2353711426811b463f5e8916c3b39678", - "publicView": "53bdb718a8598f84b9a107774e05ad9495e1575237752a3d5cc2e12eeaa6d572", + "fullView": "70f5a9779744ad58b77cebed129ecec4c24d7c5bbf3b65be4b3c61c4be5f0e80", + "publicView": "7ada532408d7c20c385804ca00d0a6a969f1448c796775c14657d7932ecf392a", "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" }, "coreVersion": "0.20.0", @@ -245,37 +630,116 @@ "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjRlNGNlMzdlM2JhY2NmYmY5N2M2MzVhNGMxNGYxZjU5ODY3ZmRiNWQ5YmQ0ZTFmMmI1YjdiMDQyZDg3YmRkOTIiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6ImI1M2ZmNTk0MjQwOTIyYzE5MGE2ZDk1N2Y0NzI1YjBlMjM1MzcxMTQyNjgxMWI0NjNmNWU4OTE2YzNiMzk2NzgiLCJwdWJsaWNWaWV3IjoiNTNiZGI3MThhODU5OGY4NGI5YTEwNzc3NGUwNWFkOTQ5NWUxNTc1MjM3NzUyYTNkNWNjMmUxMmVlYWE2ZDU3MiIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.-RH5CefIIgnBJ33VqE9-flY978EP3fD3ooT5X49R1hn7Vj_2z80ipco9Lf-zaw7jTZ6WXF92VPpG6yUjGNkJBg", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImQzZTM2NjE5MDJhZThlYmM3OTdkNTQ1YzYxMjQxNzNhNzRjM2IwODZiMmJiYTgzOTk2MjhkZjZiNTNkYjc5ZTAiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6IjcwZjVhOTc3OTc0NGFkNThiNzdjZWJlZDEyOWVjZWM0YzI0ZDdjNWJiZjNiNjViZTRiM2M2MWM0YmU1ZjBlODAiLCJwdWJsaWNWaWV3IjoiN2FkYTUzMjQwOGQ3YzIwYzM4NTgwNGNhMDBkMGE2YTk2OWYxNDQ4Yzc5Njc3NWMxNDY1N2Q3OTMyZWNmMzkyYSIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.Y4XyqkZCD-XAtSYTICoyCngJx7sDCk7sYcdyezYe7ZDDY0k4w8WhK1oUaB5x_zg-malEGWJ_dXIVyI0P_M_qBg", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.TewhsuBa83tHnqMcTgSUZf7CtbzAVvHXVRVOJYFkjJEgzOvMa6O7NGiIWQdwDFtVh-wWvztRkUTprTGV6PqsAQ", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjbzJlUGVyVW5pdCI6bnVsbCwiY29tbW9kaXR5Q29kZSI6Ijg1MDc2MDAwIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowMDowMFoiLCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsIm1hbnVmYWN0dXJlciI6eyJhZGRyZXNzIjoiTXVzdGVyc3RyYXNzZSAxLCA4MDMzMSBNw7xuY2hlbiwgREUiLCJjb3VudHJ5IjpudWxsLCJkaWRXZWJVcmwiOiJodHRwczovL25vZGUuYWNtZS5leGFtcGxlLy53ZWxsLWtub3duL2RpZC5qc29uIiwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgifSwibWF0ZXJpYWxzIjpbeyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJOTUMgODExIGNhdGhvZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjgsIndlaWdodEtnIjozLjF9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiR3JhcGhpdGUgYW5vZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjQsIndlaWdodEtnIjoxLjl9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiQWx1bWluaXVtIHBhY2sgaG91c2luZyIsInJlY3ljbGVkUGN0Ijo2MCwid2VpZ2h0S2ciOjIuMn1dLCJwbGFjZWRPbk1hcmtldERhdGUiOiIyMDI2LTA5LTAxIiwicHJvZHVjdEdyb3VwIjoiYmF0dGVyeSIsInByb2R1Y3RHcm91cERhdGEiOnsiYmF0dGVyeUNoZW1pc3RyeSI6Ik5NQyIsImJhdHRlcnlNb2RlbElkIjoiQUNNRS1MTVQtNDhWIiwiYmF0dGVyeVBhc3Nwb3J0TnVtYmVyIjoiVVJOOk9ETDpCQVRUOjA5NTA2MDAwMTM0MzUyOkFDTUUtTE1ULTIwMjYtMDkiLCJiYXR0ZXJ5VHlwZSI6ImxtdCIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY28yZVBlclVuaXRLZyI6ODYuNCwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZXVEZWNsYXJhdGlvbk9mQ29uZm9ybWl0eSI6IkVVLURvQyBBQ01FLTIwMjYtTE1ULTA5MDEsIHBlciBSZWd1bGF0aW9uIChFVSkgMjAyMy8xNTQyIEFydC4gMTgiLCJleHBlY3RlZExpZmV0aW1lQ3ljbGVzIjoxMjAwLCJleHBlY3RlZExpZmV0aW1lUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiwgMjUgZGVnQywgMTAwJSBEb0QiLCJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJoYXphcmRvdXNTdWJzdGFuY2VzIjpbeyJjYXNOdW1iZXIiOiIyMTMyNC00MC0zIiwiY29uY2VudHJhdGlvblBjdCI6MC44LCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIn1dLCJpbml0aWFsUm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTUsImludGVybmFsQ2VsbFJlc2lzdGFuY2VNb2htIjoxNCwiaW50ZXJuYWxQYWNrUmVzaXN0YW5jZU1vaG0iOjExMCwibWFudWZhY3R1cmluZ0RhdGUiOiIyMDI2LTA4LTE4VDAwOjAwOjAwWiIsIm1hbnVmYWN0dXJpbmdQbGFjZSI6Ik3DvG5jaGVuLCBERSIsIm1hcmtpbmdJbmZvcm1hdGlvbiI6IlNlcGFyYXRlLWNvbGxlY3Rpb24gc3ltYm9sIHBlciBBcnQuIDEzKDQpOyBjYXBhY2l0eSAyMCBBaCAvIDk2MCBXaDsgNDggViBMTVQiLCJtYXhpbXVtVm9sdGFnZVYiOjU0LjYsIm1pbmltYWxWb2x0YWdlViI6MzksIm5vbWluYWxDYXBhY2l0eUFoIjoyMCwibm9taW5hbFZvbHRhZ2VWIjo0OCwibm90SW5Vc2VUZW1wZXJhdHVyZVJhbmdlIjp7Im1heEMiOjQ1LCJtaW5DIjotMTB9LCJub3RJblVzZVRlbXBlcmF0dXJlUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiBzdG9yYWdlIGNsYXVzZSIsIm9yaWdpbmFsUG93ZXJDYXBhYmlsaXR5VyI6OTYwLCJwb3dlckxpbWl0TWF4VyI6MTUwMCwicG93ZXJMaW1pdE1pblciOjIwLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjYuMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.OnjeF7qUrlZOKc-Xx8u-ThMayslJBs7HPAmpZBFKvyJpivIqOrN5lTCW15PsoyBeSOpjmRq3Mm_UC99B-Ya-Dg", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "transferChain": { diff --git a/site/dpp-landing/public/verify/examples/07-tampered-transfer-signature.json b/site/dpp-landing/public/verify/examples/07-tampered-transfer-signature.json index 7a8e4f5..aa8a4de 100644 --- a/site/dpp-landing/public/verify/examples/07-tampered-transfer-signature.json +++ b/site/dpp-landing/public/verify/examples/07-tampered-transfer-signature.json @@ -16,68 +16,300 @@ { "action": "published", "actor": "api-key", - "entryHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "entryHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", - "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" - }, - "publicViewPayload": { - "content": { - "B": 2, - "b": 1, + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], "batteryChemistry": "NMC", "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." }, + "productName": "Acme LMT 48V 20Ah battery pack", + "publishedAt": "2026-09-01T09:05:00Z", + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 + }, + "publicViewPayload": { + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "newStatus": "active", @@ -90,7 +322,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "fb94dc5d624df166d3c8818ca7314033d9686dc629ddb1fbfb2be44c04b758af", + "entryHash": "5c869a32da993e9a5ee13bc3b059fee9c3abc1fbbd840ab2449473f0e5453b08", "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", "metadata": { "event": "transfer.initiated", @@ -99,7 +331,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "prevHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000003", "timestamp": "2026-09-10T14:00:00.042Z" @@ -107,7 +339,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "c4862c22734f57d4834cdb0b7aead85efd2351164ee6119b2a56c95ffd76dd9c", + "entryHash": "3a5eb46b99bc15d235b342a0a6092d3e382d998b7de32588ed4966d74295fa51", "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", "metadata": { "event": "transfer.accepted", @@ -116,7 +348,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "fb94dc5d624df166d3c8818ca7314033d9686dc629ddb1fbfb2be44c04b758af", + "prevHash": "5c869a32da993e9a5ee13bc3b059fee9c3abc1fbbd840ab2449473f0e5453b08", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000004", "timestamp": "2026-09-10T14:30:00.123456Z" @@ -124,7 +356,7 @@ { "action": "deactivated", "actor": "api-key", - "entryHash": "05d0c1bb01b38181270d81c02a52fb46f8fca04b6f69cac79b74208224aa6a82", + "entryHash": "256b89a07f8ff158fe29f327e08996214563987ca111cddc563bb0e9a9f9a362", "id": "0199a3c2-7b10-7e4a-9c3d-000000000005", "metadata": { "declaredAt": "2026-09-20T08:00:00Z", @@ -137,7 +369,7 @@ }, "newStatus": "deactivated", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "c4862c22734f57d4834cdb0b7aead85efd2351164ee6119b2a56c95ffd76dd9c", + "prevHash": "3a5eb46b99bc15d235b342a0a6092d3e382d998b7de32588ed4966d74295fa51", "previousStatus": "active", "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000005", "timestamp": "2026-09-20T08:00:00.018Z" @@ -197,45 +429,198 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.bXoh9eU-rTvN-_heLcDWRfRe5hNQA_JmZnmSzNKr2t0f56qklAYFHb8xyevrfbb3phscXigOs8vbyAPMcOJ_Dg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImd0aW4iOiIwOTUwNjAwMDEzNDM1MiIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJyYXRlZENhcGFjaXR5S3doIjpudWxsLCJyZWN5Y2xlZENvbnRlbnRDb2JhbHRQY3QiOjEyLCJyZWN5Y2xlZENvbnRlbnRMZWFkUGN0IjowLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0Ijo2LCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOjgsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uTG9ja2VkIjp0cnVlLCJyZXRlbnRpb25VbnRpbCI6IjIwMzYtMDgtMjlUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNi4wIiwic3RhdHVzIjoiYWN0aXZlIiwidXBkYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJ2ZXJzaW9uIjoxfQ.WlMDfygpDjYk8xHRQjpQgppwhVkUgYhyj_9iiPH_IXvXb8IKcD0cA2lFJCznB1B6llfW5Q3qiZcfRAdB78nDBw", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "manifest": { "contentHashes": { - "auditEntries": "4e4ce37e3baccfbf97c635a4c14f1f59867fdb5d9bd4e1f2b5b7b042d87bdd92", + "auditEntries": "d3e3661902ae8ebc797d545c6124173a74c3b086b2bba8399628df6b53db79e0", "eolEvent": "7cb65bb485538e109105404e15c7bc411aa3050694fa4d63339c70dbe431d3a0", - "fullView": "b53ff594240922c190a6d957f4725b0e2353711426811b463f5e8916c3b39678", - "publicView": "53bdb718a8598f84b9a107774e05ad9495e1575237752a3d5cc2e12eeaa6d572", + "fullView": "70f5a9779744ad58b77cebed129ecec4c24d7c5bbf3b65be4b3c61c4be5f0e80", + "publicView": "7ada532408d7c20c385804ca00d0a6a969f1448c796775c14657d7932ecf392a", "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" }, "coreVersion": "0.20.0", @@ -245,37 +630,116 @@ "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjRlNGNlMzdlM2JhY2NmYmY5N2M2MzVhNGMxNGYxZjU5ODY3ZmRiNWQ5YmQ0ZTFmMmI1YjdiMDQyZDg3YmRkOTIiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6ImI1M2ZmNTk0MjQwOTIyYzE5MGE2ZDk1N2Y0NzI1YjBlMjM1MzcxMTQyNjgxMWI0NjNmNWU4OTE2YzNiMzk2NzgiLCJwdWJsaWNWaWV3IjoiNTNiZGI3MThhODU5OGY4NGI5YTEwNzc3NGUwNWFkOTQ5NWUxNTc1MjM3NzUyYTNkNWNjMmUxMmVlYWE2ZDU3MiIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.-RH5CefIIgnBJ33VqE9-flY978EP3fD3ooT5X49R1hn7Vj_2z80ipco9Lf-zaw7jTZ6WXF92VPpG6yUjGNkJBg", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImQzZTM2NjE5MDJhZThlYmM3OTdkNTQ1YzYxMjQxNzNhNzRjM2IwODZiMmJiYTgzOTk2MjhkZjZiNTNkYjc5ZTAiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6IjcwZjVhOTc3OTc0NGFkNThiNzdjZWJlZDEyOWVjZWM0YzI0ZDdjNWJiZjNiNjViZTRiM2M2MWM0YmU1ZjBlODAiLCJwdWJsaWNWaWV3IjoiN2FkYTUzMjQwOGQ3YzIwYzM4NTgwNGNhMDBkMGE2YTk2OWYxNDQ4Yzc5Njc3NWMxNDY1N2Q3OTMyZWNmMzkyYSIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.Y4XyqkZCD-XAtSYTICoyCngJx7sDCk7sYcdyezYe7ZDDY0k4w8WhK1oUaB5x_zg-malEGWJ_dXIVyI0P_M_qBg", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.TewhsuBa83tHnqMcTgSUZf7CtbzAVvHXVRVOJYFkjJEgzOvMa6O7NGiIWQdwDFtVh-wWvztRkUTprTGV6PqsAQ", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjbzJlUGVyVW5pdCI6bnVsbCwiY29tbW9kaXR5Q29kZSI6Ijg1MDc2MDAwIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowMDowMFoiLCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsIm1hbnVmYWN0dXJlciI6eyJhZGRyZXNzIjoiTXVzdGVyc3RyYXNzZSAxLCA4MDMzMSBNw7xuY2hlbiwgREUiLCJjb3VudHJ5IjpudWxsLCJkaWRXZWJVcmwiOiJodHRwczovL25vZGUuYWNtZS5leGFtcGxlLy53ZWxsLWtub3duL2RpZC5qc29uIiwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgifSwibWF0ZXJpYWxzIjpbeyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJOTUMgODExIGNhdGhvZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjgsIndlaWdodEtnIjozLjF9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiR3JhcGhpdGUgYW5vZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjQsIndlaWdodEtnIjoxLjl9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiQWx1bWluaXVtIHBhY2sgaG91c2luZyIsInJlY3ljbGVkUGN0Ijo2MCwid2VpZ2h0S2ciOjIuMn1dLCJwbGFjZWRPbk1hcmtldERhdGUiOiIyMDI2LTA5LTAxIiwicHJvZHVjdEdyb3VwIjoiYmF0dGVyeSIsInByb2R1Y3RHcm91cERhdGEiOnsiYmF0dGVyeUNoZW1pc3RyeSI6Ik5NQyIsImJhdHRlcnlNb2RlbElkIjoiQUNNRS1MTVQtNDhWIiwiYmF0dGVyeVBhc3Nwb3J0TnVtYmVyIjoiVVJOOk9ETDpCQVRUOjA5NTA2MDAwMTM0MzUyOkFDTUUtTE1ULTIwMjYtMDkiLCJiYXR0ZXJ5VHlwZSI6ImxtdCIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY28yZVBlclVuaXRLZyI6ODYuNCwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZXVEZWNsYXJhdGlvbk9mQ29uZm9ybWl0eSI6IkVVLURvQyBBQ01FLTIwMjYtTE1ULTA5MDEsIHBlciBSZWd1bGF0aW9uIChFVSkgMjAyMy8xNTQyIEFydC4gMTgiLCJleHBlY3RlZExpZmV0aW1lQ3ljbGVzIjoxMjAwLCJleHBlY3RlZExpZmV0aW1lUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiwgMjUgZGVnQywgMTAwJSBEb0QiLCJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJoYXphcmRvdXNTdWJzdGFuY2VzIjpbeyJjYXNOdW1iZXIiOiIyMTMyNC00MC0zIiwiY29uY2VudHJhdGlvblBjdCI6MC44LCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIn1dLCJpbml0aWFsUm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTUsImludGVybmFsQ2VsbFJlc2lzdGFuY2VNb2htIjoxNCwiaW50ZXJuYWxQYWNrUmVzaXN0YW5jZU1vaG0iOjExMCwibWFudWZhY3R1cmluZ0RhdGUiOiIyMDI2LTA4LTE4VDAwOjAwOjAwWiIsIm1hbnVmYWN0dXJpbmdQbGFjZSI6Ik3DvG5jaGVuLCBERSIsIm1hcmtpbmdJbmZvcm1hdGlvbiI6IlNlcGFyYXRlLWNvbGxlY3Rpb24gc3ltYm9sIHBlciBBcnQuIDEzKDQpOyBjYXBhY2l0eSAyMCBBaCAvIDk2MCBXaDsgNDggViBMTVQiLCJtYXhpbXVtVm9sdGFnZVYiOjU0LjYsIm1pbmltYWxWb2x0YWdlViI6MzksIm5vbWluYWxDYXBhY2l0eUFoIjoyMCwibm9taW5hbFZvbHRhZ2VWIjo0OCwibm90SW5Vc2VUZW1wZXJhdHVyZVJhbmdlIjp7Im1heEMiOjQ1LCJtaW5DIjotMTB9LCJub3RJblVzZVRlbXBlcmF0dXJlUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiBzdG9yYWdlIGNsYXVzZSIsIm9yaWdpbmFsUG93ZXJDYXBhYmlsaXR5VyI6OTYwLCJwb3dlckxpbWl0TWF4VyI6MTUwMCwicG93ZXJMaW1pdE1pblciOjIwLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjYuMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.OnjeF7qUrlZOKc-Xx8u-ThMayslJBs7HPAmpZBFKvyJpivIqOrN5lTCW15PsoyBeSOpjmRq3Mm_UC99B-Ya-Dg", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "transferChain": { diff --git a/site/dpp-landing/public/verify/examples/08-hidden-field-injection.json b/site/dpp-landing/public/verify/examples/08-hidden-field-injection.json index 6034702..1085473 100644 --- a/site/dpp-landing/public/verify/examples/08-hidden-field-injection.json +++ b/site/dpp-landing/public/verify/examples/08-hidden-field-injection.json @@ -16,68 +16,300 @@ { "action": "published", "actor": "api-key", - "entryHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "entryHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", - "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" - }, - "publicViewPayload": { - "content": { - "B": 2, - "b": 1, + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], "batteryChemistry": "NMC", "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." }, + "productName": "Acme LMT 48V 20Ah battery pack", + "publishedAt": "2026-09-01T09:05:00Z", + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 + }, + "publicViewPayload": { + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "newStatus": "active", @@ -90,7 +322,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "fb94dc5d624df166d3c8818ca7314033d9686dc629ddb1fbfb2be44c04b758af", + "entryHash": "5c869a32da993e9a5ee13bc3b059fee9c3abc1fbbd840ab2449473f0e5453b08", "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", "metadata": { "event": "transfer.initiated", @@ -99,7 +331,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "prevHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000003", "timestamp": "2026-09-10T14:00:00.042Z" @@ -107,7 +339,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "c4862c22734f57d4834cdb0b7aead85efd2351164ee6119b2a56c95ffd76dd9c", + "entryHash": "3a5eb46b99bc15d235b342a0a6092d3e382d998b7de32588ed4966d74295fa51", "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", "metadata": { "event": "transfer.accepted", @@ -116,7 +348,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "fb94dc5d624df166d3c8818ca7314033d9686dc629ddb1fbfb2be44c04b758af", + "prevHash": "5c869a32da993e9a5ee13bc3b059fee9c3abc1fbbd840ab2449473f0e5453b08", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000004", "timestamp": "2026-09-10T14:30:00.123456Z" @@ -124,7 +356,7 @@ { "action": "deactivated", "actor": "api-key", - "entryHash": "05d0c1bb01b38181270d81c02a52fb46f8fca04b6f69cac79b74208224aa6a82", + "entryHash": "256b89a07f8ff158fe29f327e08996214563987ca111cddc563bb0e9a9f9a362", "id": "0199a3c2-7b10-7e4a-9c3d-000000000005", "metadata": { "declaredAt": "2026-09-20T08:00:00Z", @@ -137,7 +369,7 @@ }, "newStatus": "deactivated", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "c4862c22734f57d4834cdb0b7aead85efd2351164ee6119b2a56c95ffd76dd9c", + "prevHash": "3a5eb46b99bc15d235b342a0a6092d3e382d998b7de32588ed4966d74295fa51", "previousStatus": "active", "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000005", "timestamp": "2026-09-20T08:00:00.018Z" @@ -197,45 +429,198 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.bXoh9eU-rTvN-_heLcDWRfRe5hNQA_JmZnmSzNKr2t0f56qklAYFHb8xyevrfbb3phscXigOs8vbyAPMcOJ_Dg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImd0aW4iOiIwOTUwNjAwMDEzNDM1MiIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJyYXRlZENhcGFjaXR5S3doIjpudWxsLCJyZWN5Y2xlZENvbnRlbnRDb2JhbHRQY3QiOjEyLCJyZWN5Y2xlZENvbnRlbnRMZWFkUGN0IjowLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0Ijo2LCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOjgsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uTG9ja2VkIjp0cnVlLCJyZXRlbnRpb25VbnRpbCI6IjIwMzYtMDgtMjlUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNi4wIiwic3RhdHVzIjoiYWN0aXZlIiwidXBkYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJ2ZXJzaW9uIjoxfQ.WlMDfygpDjYk8xHRQjpQgppwhVkUgYhyj_9iiPH_IXvXb8IKcD0cA2lFJCznB1B6llfW5Q3qiZcfRAdB78nDBw", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "manifest": { "contentHashes": { - "auditEntries": "4e4ce37e3baccfbf97c635a4c14f1f59867fdb5d9bd4e1f2b5b7b042d87bdd92", + "auditEntries": "d3e3661902ae8ebc797d545c6124173a74c3b086b2bba8399628df6b53db79e0", "eolEvent": "7cb65bb485538e109105404e15c7bc411aa3050694fa4d63339c70dbe431d3a0", - "fullView": "b53ff594240922c190a6d957f4725b0e2353711426811b463f5e8916c3b39678", - "publicView": "53bdb718a8598f84b9a107774e05ad9495e1575237752a3d5cc2e12eeaa6d572", + "fullView": "70f5a9779744ad58b77cebed129ecec4c24d7c5bbf3b65be4b3c61c4be5f0e80", + "publicView": "7ada532408d7c20c385804ca00d0a6a969f1448c796775c14657d7932ecf392a", "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" }, "coreVersion": "0.20.0", @@ -245,37 +630,116 @@ "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjRlNGNlMzdlM2JhY2NmYmY5N2M2MzVhNGMxNGYxZjU5ODY3ZmRiNWQ5YmQ0ZTFmMmI1YjdiMDQyZDg3YmRkOTIiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6ImI1M2ZmNTk0MjQwOTIyYzE5MGE2ZDk1N2Y0NzI1YjBlMjM1MzcxMTQyNjgxMWI0NjNmNWU4OTE2YzNiMzk2NzgiLCJwdWJsaWNWaWV3IjoiNTNiZGI3MThhODU5OGY4NGI5YTEwNzc3NGUwNWFkOTQ5NWUxNTc1MjM3NzUyYTNkNWNjMmUxMmVlYWE2ZDU3MiIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.-RH5CefIIgnBJ33VqE9-flY978EP3fD3ooT5X49R1hn7Vj_2z80ipco9Lf-zaw7jTZ6WXF92VPpG6yUjGNkJBg", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImQzZTM2NjE5MDJhZThlYmM3OTdkNTQ1YzYxMjQxNzNhNzRjM2IwODZiMmJiYTgzOTk2MjhkZjZiNTNkYjc5ZTAiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6IjcwZjVhOTc3OTc0NGFkNThiNzdjZWJlZDEyOWVjZWM0YzI0ZDdjNWJiZjNiNjViZTRiM2M2MWM0YmU1ZjBlODAiLCJwdWJsaWNWaWV3IjoiN2FkYTUzMjQwOGQ3YzIwYzM4NTgwNGNhMDBkMGE2YTk2OWYxNDQ4Yzc5Njc3NWMxNDY1N2Q3OTMyZWNmMzkyYSIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.Y4XyqkZCD-XAtSYTICoyCngJx7sDCk7sYcdyezYe7ZDDY0k4w8WhK1oUaB5x_zg-malEGWJ_dXIVyI0P_M_qBg", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.TewhsuBa83tHnqMcTgSUZf7CtbzAVvHXVRVOJYFkjJEgzOvMa6O7NGiIWQdwDFtVh-wWvztRkUTprTGV6PqsAQ", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjbzJlUGVyVW5pdCI6bnVsbCwiY29tbW9kaXR5Q29kZSI6Ijg1MDc2MDAwIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowMDowMFoiLCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsIm1hbnVmYWN0dXJlciI6eyJhZGRyZXNzIjoiTXVzdGVyc3RyYXNzZSAxLCA4MDMzMSBNw7xuY2hlbiwgREUiLCJjb3VudHJ5IjpudWxsLCJkaWRXZWJVcmwiOiJodHRwczovL25vZGUuYWNtZS5leGFtcGxlLy53ZWxsLWtub3duL2RpZC5qc29uIiwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgifSwibWF0ZXJpYWxzIjpbeyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJOTUMgODExIGNhdGhvZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjgsIndlaWdodEtnIjozLjF9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiR3JhcGhpdGUgYW5vZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjQsIndlaWdodEtnIjoxLjl9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiQWx1bWluaXVtIHBhY2sgaG91c2luZyIsInJlY3ljbGVkUGN0Ijo2MCwid2VpZ2h0S2ciOjIuMn1dLCJwbGFjZWRPbk1hcmtldERhdGUiOiIyMDI2LTA5LTAxIiwicHJvZHVjdEdyb3VwIjoiYmF0dGVyeSIsInByb2R1Y3RHcm91cERhdGEiOnsiYmF0dGVyeUNoZW1pc3RyeSI6Ik5NQyIsImJhdHRlcnlNb2RlbElkIjoiQUNNRS1MTVQtNDhWIiwiYmF0dGVyeVBhc3Nwb3J0TnVtYmVyIjoiVVJOOk9ETDpCQVRUOjA5NTA2MDAwMTM0MzUyOkFDTUUtTE1ULTIwMjYtMDkiLCJiYXR0ZXJ5VHlwZSI6ImxtdCIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY28yZVBlclVuaXRLZyI6ODYuNCwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZXVEZWNsYXJhdGlvbk9mQ29uZm9ybWl0eSI6IkVVLURvQyBBQ01FLTIwMjYtTE1ULTA5MDEsIHBlciBSZWd1bGF0aW9uIChFVSkgMjAyMy8xNTQyIEFydC4gMTgiLCJleHBlY3RlZExpZmV0aW1lQ3ljbGVzIjoxMjAwLCJleHBlY3RlZExpZmV0aW1lUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiwgMjUgZGVnQywgMTAwJSBEb0QiLCJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJoYXphcmRvdXNTdWJzdGFuY2VzIjpbeyJjYXNOdW1iZXIiOiIyMTMyNC00MC0zIiwiY29uY2VudHJhdGlvblBjdCI6MC44LCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIn1dLCJpbml0aWFsUm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTUsImludGVybmFsQ2VsbFJlc2lzdGFuY2VNb2htIjoxNCwiaW50ZXJuYWxQYWNrUmVzaXN0YW5jZU1vaG0iOjExMCwibWFudWZhY3R1cmluZ0RhdGUiOiIyMDI2LTA4LTE4VDAwOjAwOjAwWiIsIm1hbnVmYWN0dXJpbmdQbGFjZSI6Ik3DvG5jaGVuLCBERSIsIm1hcmtpbmdJbmZvcm1hdGlvbiI6IlNlcGFyYXRlLWNvbGxlY3Rpb24gc3ltYm9sIHBlciBBcnQuIDEzKDQpOyBjYXBhY2l0eSAyMCBBaCAvIDk2MCBXaDsgNDggViBMTVQiLCJtYXhpbXVtVm9sdGFnZVYiOjU0LjYsIm1pbmltYWxWb2x0YWdlViI6MzksIm5vbWluYWxDYXBhY2l0eUFoIjoyMCwibm9taW5hbFZvbHRhZ2VWIjo0OCwibm90SW5Vc2VUZW1wZXJhdHVyZVJhbmdlIjp7Im1heEMiOjQ1LCJtaW5DIjotMTB9LCJub3RJblVzZVRlbXBlcmF0dXJlUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiBzdG9yYWdlIGNsYXVzZSIsIm9yaWdpbmFsUG93ZXJDYXBhYmlsaXR5VyI6OTYwLCJwb3dlckxpbWl0TWF4VyI6MTUwMCwicG93ZXJMaW1pdE1pblciOjIwLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjYuMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.OnjeF7qUrlZOKc-Xx8u-ThMayslJBs7HPAmpZBFKvyJpivIqOrN5lTCW15PsoyBeSOpjmRq3Mm_UC99B-Ya-Dg", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "transferChain": { diff --git a/site/dpp-landing/public/verify/examples/09-unknown-top-level-field.json b/site/dpp-landing/public/verify/examples/09-unknown-top-level-field.json index fee58b5..43abd4d 100644 --- a/site/dpp-landing/public/verify/examples/09-unknown-top-level-field.json +++ b/site/dpp-landing/public/verify/examples/09-unknown-top-level-field.json @@ -16,68 +16,300 @@ { "action": "published", "actor": "api-key", - "entryHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "entryHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", "metadata": { "fullViewPayload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", - "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" - }, - "publicViewPayload": { - "content": { - "B": 2, - "b": 1, + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], "batteryChemistry": "NMC", "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." }, + "productName": "Acme LMT 48V 20Ah battery pack", + "publishedAt": "2026-09-01T09:05:00Z", + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 + }, + "publicViewPayload": { + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "newStatus": "active", @@ -90,7 +322,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "fb94dc5d624df166d3c8818ca7314033d9686dc629ddb1fbfb2be44c04b758af", + "entryHash": "5c869a32da993e9a5ee13bc3b059fee9c3abc1fbbd840ab2449473f0e5453b08", "id": "0199a3c2-7b10-7e4a-9c3d-000000000003", "metadata": { "event": "transfer.initiated", @@ -99,7 +331,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "122cc906e4c0fd2172cfd97b1d6f5b0813c2db22f301b9e96f044c614fa84fdb", + "prevHash": "4e81bf531274f21aa02339144c34711eac79af7fb22ebf10a18982adc3f075a3", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000003", "timestamp": "2026-09-10T14:00:00.042Z" @@ -107,7 +339,7 @@ { "action": "transferred", "actor": "api-key", - "entryHash": "c4862c22734f57d4834cdb0b7aead85efd2351164ee6119b2a56c95ffd76dd9c", + "entryHash": "3a5eb46b99bc15d235b342a0a6092d3e382d998b7de32588ed4966d74295fa51", "id": "0199a3c2-7b10-7e4a-9c3d-000000000004", "metadata": { "event": "transfer.accepted", @@ -116,7 +348,7 @@ }, "newStatus": null, "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "fb94dc5d624df166d3c8818ca7314033d9686dc629ddb1fbfb2be44c04b758af", + "prevHash": "5c869a32da993e9a5ee13bc3b059fee9c3abc1fbbd840ab2449473f0e5453b08", "previousStatus": null, "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000004", "timestamp": "2026-09-10T14:30:00.123456Z" @@ -124,7 +356,7 @@ { "action": "deactivated", "actor": "api-key", - "entryHash": "05d0c1bb01b38181270d81c02a52fb46f8fca04b6f69cac79b74208224aa6a82", + "entryHash": "256b89a07f8ff158fe29f327e08996214563987ca111cddc563bb0e9a9f9a362", "id": "0199a3c2-7b10-7e4a-9c3d-000000000005", "metadata": { "declaredAt": "2026-09-20T08:00:00Z", @@ -137,7 +369,7 @@ }, "newStatus": "deactivated", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "prevHash": "c4862c22734f57d4834cdb0b7aead85efd2351164ee6119b2a56c95ffd76dd9c", + "prevHash": "3a5eb46b99bc15d235b342a0a6092d3e382d998b7de32588ed4966d74295fa51", "previousStatus": "active", "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000005", "timestamp": "2026-09-20T08:00:00.018Z" @@ -197,45 +429,198 @@ } }, "fullView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY2F0aG9kZU1hdGVyaWFsIjoiTk1DIDgxMSIsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwicmF0ZWRDYXBhY2l0eUFoIjoyMCwic3RhdGVPZkhlYWx0aFBjdCI6MTAwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.bXoh9eU-rTvN-_heLcDWRfRe5hNQA_JmZnmSzNKr2t0f56qklAYFHb8xyevrfbb3phscXigOs8vbyAPMcOJ_Dg", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJiYXRjaElkIjoiQUNNRS1MTVQtMjAyNi0wOSIsImNvMmVQZXJVbml0IjpudWxsLCJjb21tb2RpdHlDb2RlIjoiODUwNzYwMDAiLCJjcmVhdGVkQXQiOiIyMDI2LTA5LTAxVDA5OjAwOjAwWiIsImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiandzU2lnbmF0dXJlIjpudWxsLCJtYW51ZmFjdHVyZXIiOnsiYWRkcmVzcyI6Ik11c3RlcnN0cmFzc2UgMSwgODAzMzEgTcO8bmNoZW4sIERFIiwiY291bnRyeSI6bnVsbCwiZGlkV2ViVXJsIjoiaHR0cHM6Ly9ub2RlLmFjbWUuZXhhbXBsZS8ud2VsbC1rbm93bi9kaWQuanNvbiIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sIm1hdGVyaWFscyI6W3siY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiTk1DIDgxMSBjYXRob2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo4LCJ3ZWlnaHRLZyI6My4xfSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkdyYXBoaXRlIGFub2RlIGFjdGl2ZSBtYXRlcmlhbCIsInJlY3ljbGVkUGN0Ijo0LCJ3ZWlnaHRLZyI6MS45fSx7ImNvdW50cnlPZk9yaWdpbiI6IkRFIiwibmFtZSI6IkFsdW1pbml1bSBwYWNrIGhvdXNpbmciLCJyZWN5Y2xlZFBjdCI6NjAsIndlaWdodEtnIjoyLjJ9XSwicGxhY2VkT25NYXJrZXREYXRlIjoiMjAyNi0wOS0wMSIsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwcm9kdWN0R3JvdXBEYXRhIjp7ImFub2RlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijc3ODItNDItNSIsIm5hbWUiOiJHcmFwaGl0ZSIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiOTAwMy01NS04IiwibmFtZSI6IlNCUi9DTUMgYmluZGVyIiwid2VpZ2h0UGN0Ijo0fV0sImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlQYXNzcG9ydE51bWJlciI6IlVSTjpPREw6QkFUVDowOTUwNjAwMDEzNDM1MjpBQ01FLUxNVC0yMDI2LTA5IiwiYmF0dGVyeVN0YXR1cyI6Im9yaWdpbmFsIiwiYmF0dGVyeVR5cGUiOiJsbXQiLCJiYXR0ZXJ5V2VpZ2h0S2ciOjEyLjQsImNhdGhvZGVNYXRlcmlhbCI6W3siY2FzTnVtYmVyIjoiMzQ2NDE3LTk3LTgiLCJuYW1lIjoiTGlOaTAuOE1uMC4xQ28wLjFPMiIsIndlaWdodFBjdCI6OTZ9LHsiY2FzTnVtYmVyIjoiMjQ5MzctNzktOSIsIm5hbWUiOiJQVkRGIGJpbmRlciIsIndlaWdodFBjdCI6Mn0seyJjYXNOdW1iZXIiOiIxMzMzLTg2LTQiLCJuYW1lIjoiQ29uZHVjdGl2ZSBjYXJib24gYmxhY2siLCJ3ZWlnaHRQY3QiOjJ9XSwiY28yZVBlclVuaXRLZyI6ODYuNCwiY29tcG9uZW50UGFydE51bWJlcnMiOlsiQUNNRS1DRUxMLTIxNzAwLU5NQyIsIkFDTUUtQk1TLTEzUy1WMSIsIkFDTUUtTE1ULUVOQ0wtNDgiXSwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZGlzYXNzZW1ibHlJbnN0cnVjdGlvbnNVcmwiOiJodHRwczovL2FjbWUuZXhhbXBsZS9zZXJ2aWNlL2xtdC00OC9kaXNhc3NlbWJseSIsImR5bmFtaWNQZXJmb3JtYW5jZSI6eyJjYXBhY2l0eUZhZGVQY3QiOjAuNSwiZXhwZWN0ZWRMaWZldGltZUN5Y2xlcyI6MTE5NSwiaW50ZXJuYWxSZXNpc3RhbmNlTW9obSI6MTEwLjQsInJhdGVkQ2FwYWNpdHlBaCI6MTkuOSwicm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTQuOH0sImVsZWN0cm9seXRlTWF0ZXJpYWwiOlt7ImNhc051bWJlciI6Ijk2LTQ5LTEiLCJuYW1lIjoiRXRoeWxlbmUgY2FyYm9uYXRlIiwid2VpZ2h0UGN0IjozOH0seyJjYXNOdW1iZXIiOiI2MTYtMzgtNiIsIm5hbWUiOiJEaW1ldGh5bCBjYXJib25hdGUiLCJ3ZWlnaHRQY3QiOjQ5fSx7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIiwid2VpZ2h0UGN0IjoxM31dLCJldURlY2xhcmF0aW9uT2ZDb25mb3JtaXR5IjoiRVUtRG9DIEFDTUUtMjAyNi1MTVQtMDkwMSwgcGVyIFJlZ3VsYXRpb24gKEVVKSAyMDIzLzE1NDIgQXJ0LiAxOCIsImV4cGVjdGVkTGlmZXRpbWVDeWNsZXMiOjEyMDAsImV4cGVjdGVkTGlmZXRpbWVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2LCAyNSBkZWdDLCAxMDAlIERvRCIsImd0aW4iOiIwOTUwNjAwMDEzNDM1MiIsImhhemFyZG91c1N1YnN0YW5jZXMiOlt7ImNhc051bWJlciI6IjIxMzI0LTQwLTMiLCJjb25jZW50cmF0aW9uUGN0IjowLjgsIm5hbWUiOiJMaXRoaXVtIGhleGFmbHVvcm9waG9zcGhhdGUifV0sImluaXRpYWxSb3VuZFRyaXBFZmZpY2llbmN5UGN0Ijo5NSwiaW50ZXJuYWxDZWxsUmVzaXN0YW5jZU1vaG0iOjE0LCJpbnRlcm5hbFBhY2tSZXNpc3RhbmNlTW9obSI6MTEwLCJtYW51ZmFjdHVyaW5nRGF0ZSI6IjIwMjYtMDgtMThUMDA6MDA6MDBaIiwibWFudWZhY3R1cmluZ1BsYWNlIjoiTcO8bmNoZW4sIERFIiwibWFya2luZ0luZm9ybWF0aW9uIjoiU2VwYXJhdGUtY29sbGVjdGlvbiBzeW1ib2wgcGVyIEFydC4gMTMoNCk7IGNhcGFjaXR5IDIwIEFoIC8gOTYwIFdoOyA0OCBWIExNVCIsIm1heGltdW1Wb2x0YWdlViI6NTQuNiwibWluaW1hbFZvbHRhZ2VWIjozOSwibm9taW5hbENhcGFjaXR5QWgiOjIwLCJub21pbmFsVm9sdGFnZVYiOjQ4LCJub3RJblVzZVRlbXBlcmF0dXJlUmFuZ2UiOnsibWF4QyI6NDUsIm1pbkMiOi0xMH0sIm5vdEluVXNlVGVtcGVyYXR1cmVSZWZlcmVuY2VUZXN0IjoiRU4gNTA2MDQtMToyMDE2IHN0b3JhZ2UgY2xhdXNlIiwib3JpZ2luYWxQb3dlckNhcGFiaWxpdHlXIjo5NjAsInBvd2VyTGltaXRNYXhXIjoxNTAwLCJwb3dlckxpbWl0TWluVyI6MjAsInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJyYXRlZENhcGFjaXR5S3doIjpudWxsLCJyZWN5Y2xlZENvbnRlbnRDb2JhbHRQY3QiOjEyLCJyZWN5Y2xlZENvbnRlbnRMZWFkUGN0IjowLCJyZWN5Y2xlZENvbnRlbnRMaXRoaXVtUGN0Ijo2LCJyZWN5Y2xlZENvbnRlbnROaWNrZWxQY3QiOjgsInJlbmV3YWJsZUNvbnRlbnRQY3QiOjE0LCJyb3VuZFRyaXBFZmZpY2llbmN5QXRIYWxmQ3ljbGVMaWZlUGN0Ijo5MSwic2FmZXR5TWVhc3VyZXMiOiJEbyBub3Qgb3BlbiB0aGUgcGFjay4gUmVwbGFjZSBpdCB0aHJvdWdoIGFuIGF1dGhvcmlzZWQgZGVhbGVyLiBLZWVwIGl0IGF3YXkgZnJvbSBoZWF0IGFib3ZlIDYwIGRlZ0MuIiwic3BhcmVQYXJ0c0NvbnRhY3RzIjoic3BhcmVzQGFjbWUuZXhhbXBsZSIsInN0YXRlT2ZIZWFsdGgiOnsicGFyYW1ldGVyU2V0Ijoic3RhdGlvbmFyeU9yTG10IiwicmVtYWluaW5nQ2FwYWNpdHlQY3QiOjk5LjUsInJlbWFpbmluZ1Bvd2VyQ2FwYWJpbGl0eVBjdCI6OTkuMSwic2VsZkRpc2NoYXJnZVJhdGVQY3RQZXJNb250aCI6Mn0sInN0YXRlT2ZIZWFsdGhQY3QiOm51bGwsInRlc3RSZXBvcnRSZXN1bHRzIjoiRU4gNTA2MDQtMToyMDE2IHBhc3M7IFVOIDM4LjMgcGFzczsgcmVwb3J0IEFDTUUtVFItMjAyNi0wODE1IiwidXNhYmxlRXh0aW5ndWlzaGluZ0FnZW50IjoiV2F0ZXI7IGNvb2wgYSBidXJuaW5nIHBhY2sgd2l0aCBsYXJnZSB2b2x1bWVzIG9mIHdhdGVyIiwidXNhZ2VIaXN0b3J5Ijp7ImNoYXJnZURpc2NoYXJnZUN5Y2xlcyI6NH0sIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uTG9ja2VkIjp0cnVlLCJyZXRlbnRpb25VbnRpbCI6IjIwMzYtMDgtMjlUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNi4wIiwic3RhdHVzIjoiYWN0aXZlIiwidXBkYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJ2ZXJzaW9uIjoxfQ.WlMDfygpDjYk8xHRQjpQgppwhVkUgYhyj_9iiPH_IXvXb8IKcD0cA2lFJCznB1B6llfW5Q3qiZcfRAdB78nDBw", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "cathodeMaterial": "NMC 811", - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "stateOfHealthPct": 100, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "batchId": "ACME-LMT-2026-09", + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "jwsSignature": null, + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "anodeMaterial": [ + { + "casNumber": "7782-42-5", + "name": "Graphite", + "weightPct": 96.0 + }, + { + "casNumber": "9003-55-8", + "name": "SBR/CMC binder", + "weightPct": 4.0 + } + ], + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryStatus": "original", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "cathodeMaterial": [ + { + "casNumber": "346417-97-8", + "name": "LiNi0.8Mn0.1Co0.1O2", + "weightPct": 96.0 + }, + { + "casNumber": "24937-79-9", + "name": "PVDF binder", + "weightPct": 2.0 + }, + { + "casNumber": "1333-86-4", + "name": "Conductive carbon black", + "weightPct": 2.0 + } + ], + "co2ePerUnitKg": 86.4, + "componentPartNumbers": [ + "ACME-CELL-21700-NMC", + "ACME-BMS-13S-V1", + "ACME-LMT-ENCL-48" + ], + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "disassemblyInstructionsUrl": "https://acme.example/service/lmt-48/disassembly", + "dynamicPerformance": { + "capacityFadePct": 0.5, + "expectedLifetimeCycles": 1195, + "internalResistanceMohm": 110.4, + "ratedCapacityAh": 19.9, + "roundTripEfficiencyPct": 94.8 + }, + "electrolyteMaterial": [ + { + "casNumber": "96-49-1", + "name": "Ethylene carbonate", + "weightPct": 38.0 + }, + { + "casNumber": "616-38-6", + "name": "Dimethyl carbonate", + "weightPct": 49.0 + }, + { + "casNumber": "21324-40-3", + "name": "Lithium hexafluorophosphate", + "weightPct": 13.0 + } + ], + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "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.", + "sparePartsContacts": "spares@acme.example", + "stateOfHealth": { + "parameterSet": "stationaryOrLmt", + "remainingCapacityPct": 99.5, + "remainingPowerCapabilityPct": 99.1, + "selfDischargeRatePctPerMonth": 2.0 + }, + "stateOfHealthPct": null, + "testReportResults": "EN 50604-1:2016 pass; UN 38.3 pass; report ACME-TR-2026-0815", + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "usageHistory": { + "chargeDischargeCycles": 4 + }, + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionLocked": true, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "manifest": { "contentHashes": { - "auditEntries": "4e4ce37e3baccfbf97c635a4c14f1f59867fdb5d9bd4e1f2b5b7b042d87bdd92", + "auditEntries": "d3e3661902ae8ebc797d545c6124173a74c3b086b2bba8399628df6b53db79e0", "eolEvent": "7cb65bb485538e109105404e15c7bc411aa3050694fa4d63339c70dbe431d3a0", - "fullView": "b53ff594240922c190a6d957f4725b0e2353711426811b463f5e8916c3b39678", - "publicView": "53bdb718a8598f84b9a107774e05ad9495e1575237752a3d5cc2e12eeaa6d572", + "fullView": "70f5a9779744ad58b77cebed129ecec4c24d7c5bbf3b65be4b3c61c4be5f0e80", + "publicView": "7ada532408d7c20c385804ca00d0a6a969f1448c796775c14657d7932ecf392a", "transferChain": "52e0fd4f91be57a81e6219557f5f3c4f2331b0cbbc9461d376c967dae5445445" }, "coreVersion": "0.20.0", @@ -245,37 +630,116 @@ "nodeVersion": "0.14.0", "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40" }, - "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjRlNGNlMzdlM2JhY2NmYmY5N2M2MzVhNGMxNGYxZjU5ODY3ZmRiNWQ5YmQ0ZTFmMmI1YjdiMDQyZDg3YmRkOTIiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6ImI1M2ZmNTk0MjQwOTIyYzE5MGE2ZDk1N2Y0NzI1YjBlMjM1MzcxMTQyNjgxMWI0NjNmNWU4OTE2YzNiMzk2NzgiLCJwdWJsaWNWaWV3IjoiNTNiZGI3MThhODU5OGY4NGI5YTEwNzc3NGUwNWFkOTQ5NWUxNTc1MjM3NzUyYTNkNWNjMmUxMmVlYWE2ZDU3MiIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.-RH5CefIIgnBJ33VqE9-flY978EP3fD3ooT5X49R1hn7Vj_2z80ipco9Lf-zaw7jTZ6WXF92VPpG6yUjGNkJBg", + "manifestJws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6ImQzZTM2NjE5MDJhZThlYmM3OTdkNTQ1YzYxMjQxNzNhNzRjM2IwODZiMmJiYTgzOTk2MjhkZjZiNTNkYjc5ZTAiLCJlb2xFdmVudCI6IjdjYjY1YmI0ODU1MzhlMTA5MTA1NDA0ZTE1YzdiYzQxMWFhMzA1MDY5NGZhNGQ2MzMzOWM3MGRiZTQzMWQzYTAiLCJmdWxsVmlldyI6IjcwZjVhOTc3OTc0NGFkNThiNzdjZWJlZDEyOWVjZWM0YzI0ZDdjNWJiZjNiNjViZTRiM2M2MWM0YmU1ZjBlODAiLCJwdWJsaWNWaWV3IjoiN2FkYTUzMjQwOGQ3YzIwYzM4NTgwNGNhMDBkMGE2YTk2OWYxNDQ4Yzc5Njc3NWMxNDY1N2Q3OTMyZWNmMzkyYSIsInRyYW5zZmVyQ2hhaW4iOiI1MmUwZmQ0ZjkxYmU1N2E4MWU2MjE5NTU3ZjVmM2M0ZjIzMzFiMGNiYmM5NDYxZDM3NmM5NjdkYWU1NDQ1NDQ1In0sImNvcmVWZXJzaW9uIjoiMC4yMC4wIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0yNVQxMjowMDowMFoiLCJmb3JtYXRWZXJzaW9uIjoiMSIsImlzc3VlckRpZCI6ImRpZDp3ZWI6bm9kZS5hY21lLmV4YW1wbGUiLCJub2RlVmVyc2lvbiI6IjAuMTQuMCIsInBhc3Nwb3J0SWQiOiIwMTk5YTNjMi03YjEwLTdlNGEtOWMzZC0yZjFlOGI2YTVkNDAifQ.Y4XyqkZCD-XAtSYTICoyCngJx7sDCk7sYcdyezYe7ZDDY0k4w8WhK1oUaB5x_zg-malEGWJ_dXIVyI0P_M_qBg", "publicView": { - "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImJhdHRlcnlDaGVtaXN0cnkiOiJOTUMiLCJiYXR0ZXJ5TW9kZWxJZCI6IkFDTUUtTE1ULTQ4ViIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiaHVnZU51bWJlciI6MWUrMjEsIm5lZ2F0aXZlWmVyb1NhZmUiOi0wLjI1LCJyYXRlZENhcGFjaXR5QWgiOjIwLCJ0aW55TnVtYmVyIjo1ZS03LCLDiW1pc3Npb24iOiIwLjVlLTMgdGVzdCIsIsOpIjozLCLigqwiOjQsIvCfmIAiOjUsIu-8oSI6Nn0sImlkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIiwiaXNzdWVyIjoiZGlkOndlYjpub2RlLmFjbWUuZXhhbXBsZSIsIm9wZXJhdG9yIjp7ImNpdHkiOiJNw7xuY2hlbiIsImNvdW50cnkiOiJERSIsIm5hbWUiOiJBY21lIFplbGxlbiBHbWJIIn0sInByb2R1Y3RHcm91cCI6ImJhdHRlcnkiLCJwdWJsaXNoZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwic2NoZW1hVmVyc2lvbiI6IjIuNy4wIiwic3RhdHVzIjoiYWN0aXZlIn0.TewhsuBa83tHnqMcTgSUZf7CtbzAVvHXVRVOJYFkjJEgzOvMa6O7NGiIWQdwDFtVh-wWvztRkUTprTGV6PqsAQ", + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjbzJlUGVyVW5pdCI6bnVsbCwiY29tbW9kaXR5Q29kZSI6Ijg1MDc2MDAwIiwiY3JlYXRlZEF0IjoiMjAyNi0wOS0wMVQwOTowMDowMFoiLCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsIm1hbnVmYWN0dXJlciI6eyJhZGRyZXNzIjoiTXVzdGVyc3RyYXNzZSAxLCA4MDMzMSBNw7xuY2hlbiwgREUiLCJjb3VudHJ5IjpudWxsLCJkaWRXZWJVcmwiOiJodHRwczovL25vZGUuYWNtZS5leGFtcGxlLy53ZWxsLWtub3duL2RpZC5qc29uIiwibmFtZSI6IkFjbWUgWmVsbGVuIEdtYkgifSwibWF0ZXJpYWxzIjpbeyJjb3VudHJ5T2ZPcmlnaW4iOiJERSIsIm5hbWUiOiJOTUMgODExIGNhdGhvZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjgsIndlaWdodEtnIjozLjF9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiR3JhcGhpdGUgYW5vZGUgYWN0aXZlIG1hdGVyaWFsIiwicmVjeWNsZWRQY3QiOjQsIndlaWdodEtnIjoxLjl9LHsiY291bnRyeU9mT3JpZ2luIjoiREUiLCJuYW1lIjoiQWx1bWluaXVtIHBhY2sgaG91c2luZyIsInJlY3ljbGVkUGN0Ijo2MCwid2VpZ2h0S2ciOjIuMn1dLCJwbGFjZWRPbk1hcmtldERhdGUiOiIyMDI2LTA5LTAxIiwicHJvZHVjdEdyb3VwIjoiYmF0dGVyeSIsInByb2R1Y3RHcm91cERhdGEiOnsiYmF0dGVyeUNoZW1pc3RyeSI6Ik5NQyIsImJhdHRlcnlNb2RlbElkIjoiQUNNRS1MTVQtNDhWIiwiYmF0dGVyeVBhc3Nwb3J0TnVtYmVyIjoiVVJOOk9ETDpCQVRUOjA5NTA2MDAwMTM0MzUyOkFDTUUtTE1ULTIwMjYtMDkiLCJiYXR0ZXJ5VHlwZSI6ImxtdCIsImJhdHRlcnlXZWlnaHRLZyI6MTIuNCwiY28yZVBlclVuaXRLZyI6ODYuNCwiY3JpdGljYWxSYXdNYXRlcmlhbHMiOlt7ImNhc051bWJlciI6Ijc0NDAtNDgtNCIsIm5hbWUiOiJDb2JhbHQiLCJ3ZWlnaHRHcmFtcyI6MzEwfSx7ImNhc051bWJlciI6Ijc0MzktOTMtMiIsIm5hbWUiOiJMaXRoaXVtIiwid2VpZ2h0R3JhbXMiOjQyMH0seyJjYXNOdW1iZXIiOiI3NzgyLTQyLTUiLCJuYW1lIjoiTmF0dXJhbCBncmFwaGl0ZSIsIndlaWdodEdyYW1zIjoxODAwfV0sImN5Y2xlTGlmZVRlc3RDUmF0ZSI6MSwiZXVEZWNsYXJhdGlvbk9mQ29uZm9ybWl0eSI6IkVVLURvQyBBQ01FLTIwMjYtTE1ULTA5MDEsIHBlciBSZWd1bGF0aW9uIChFVSkgMjAyMy8xNTQyIEFydC4gMTgiLCJleHBlY3RlZExpZmV0aW1lQ3ljbGVzIjoxMjAwLCJleHBlY3RlZExpZmV0aW1lUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiwgMjUgZGVnQywgMTAwJSBEb0QiLCJndGluIjoiMDk1MDYwMDAxMzQzNTIiLCJoYXphcmRvdXNTdWJzdGFuY2VzIjpbeyJjYXNOdW1iZXIiOiIyMTMyNC00MC0zIiwiY29uY2VudHJhdGlvblBjdCI6MC44LCJuYW1lIjoiTGl0aGl1bSBoZXhhZmx1b3JvcGhvc3BoYXRlIn1dLCJpbml0aWFsUm91bmRUcmlwRWZmaWNpZW5jeVBjdCI6OTUsImludGVybmFsQ2VsbFJlc2lzdGFuY2VNb2htIjoxNCwiaW50ZXJuYWxQYWNrUmVzaXN0YW5jZU1vaG0iOjExMCwibWFudWZhY3R1cmluZ0RhdGUiOiIyMDI2LTA4LTE4VDAwOjAwOjAwWiIsIm1hbnVmYWN0dXJpbmdQbGFjZSI6Ik3DvG5jaGVuLCBERSIsIm1hcmtpbmdJbmZvcm1hdGlvbiI6IlNlcGFyYXRlLWNvbGxlY3Rpb24gc3ltYm9sIHBlciBBcnQuIDEzKDQpOyBjYXBhY2l0eSAyMCBBaCAvIDk2MCBXaDsgNDggViBMTVQiLCJtYXhpbXVtVm9sdGFnZVYiOjU0LjYsIm1pbmltYWxWb2x0YWdlViI6MzksIm5vbWluYWxDYXBhY2l0eUFoIjoyMCwibm9taW5hbFZvbHRhZ2VWIjo0OCwibm90SW5Vc2VUZW1wZXJhdHVyZVJhbmdlIjp7Im1heEMiOjQ1LCJtaW5DIjotMTB9LCJub3RJblVzZVRlbXBlcmF0dXJlUmVmZXJlbmNlVGVzdCI6IkVOIDUwNjA0LTE6MjAxNiBzdG9yYWdlIGNsYXVzZSIsIm9yaWdpbmFsUG93ZXJDYXBhYmlsaXR5VyI6OTYwLCJwb3dlckxpbWl0TWF4VyI6MTUwMCwicG93ZXJMaW1pdE1pblciOjIwLCJwcm9kdWN0R3JvdXAiOiJiYXR0ZXJ5IiwicmF0ZWRDYXBhY2l0eUt3aCI6bnVsbCwicmVjeWNsZWRDb250ZW50Q29iYWx0UGN0IjoxMiwicmVjeWNsZWRDb250ZW50TGVhZFBjdCI6MCwicmVjeWNsZWRDb250ZW50TGl0aGl1bVBjdCI6NiwicmVjeWNsZWRDb250ZW50Tmlja2VsUGN0Ijo4LCJyZW5ld2FibGVDb250ZW50UGN0IjoxNCwicm91bmRUcmlwRWZmaWNpZW5jeUF0SGFsZkN5Y2xlTGlmZVBjdCI6OTEsInVzYWJsZUV4dGluZ3Vpc2hpbmdBZ2VudCI6IldhdGVyOyBjb29sIGEgYnVybmluZyBwYWNrIHdpdGggbGFyZ2Ugdm9sdW1lcyBvZiB3YXRlciIsIndhc3RlQmF0dGVyeUluZm9ybWF0aW9uIjoiUmV0dXJuIHRoZSBiYXR0ZXJ5IHRvIGEgYmljeWNsZSByZXRhaWxlciBvciBhIG11bmljaXBhbCBjb2xsZWN0aW9uIHBvaW50LiBEbyBub3QgcGxhY2UgaXQgaW4gaG91c2Vob2xkIHdhc3RlLiJ9LCJwcm9kdWN0TmFtZSI6IkFjbWUgTE1UIDQ4ViAyMEFoIGJhdHRlcnkgcGFjayIsInB1Ymxpc2hlZEF0IjoiMjAyNi0wOS0wMVQwOTowNTowMFoiLCJxckNvZGVVcmwiOiJodHRwczovL2RwcC5hY21lLmV4YW1wbGUvMDEvMDk1MDYwMDAxMzQzNTIvMTAvQUNNRS1MTVQtMjAyNi0wOS8yMS83ZTRhOWMzZDJmMWU4YjZhNWQ0MCIsInJlcGFpcmFiaWxpdHlTY29yZSI6bnVsbCwicmV0ZW50aW9uVW50aWwiOiIyMDM2LTA4LTI5VDA5OjA1OjAwWiIsInNjaGVtYVZlcnNpb24iOiIyLjYuMCIsInN0YXR1cyI6ImFjdGl2ZSIsInVwZGF0ZWRBdCI6IjIwMjYtMDktMDFUMDk6MDU6MDBaIiwidmVyc2lvbiI6MX0.OnjeF7qUrlZOKc-Xx8u-ThMayslJBs7HPAmpZBFKvyJpivIqOrN5lTCW15PsoyBeSOpjmRq3Mm_UC99B-Ya-Dg", "payload": { - "content": { - "B": 2, - "b": 1, - "batteryChemistry": "NMC", - "batteryModelId": "ACME-LMT-48V", - "batteryWeightKg": 12.4, - "hugeNumber": 1e+21, - "negativeZeroSafe": -0.25, - "ratedCapacityAh": 20, - "tinyNumber": 5e-7, - "Émission": "0.5e-3 test", - "é": 3, - "€": 4, - "😀": 5, - "A": 6 - }, + "co2ePerUnit": null, + "commodityCode": "85076000", + "createdAt": "2026-09-01T09:00:00Z", "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", - "issuer": "did:web:node.acme.example", - "operator": { - "city": "München", - "country": "DE", + "manufacturer": { + "address": "Musterstrasse 1, 80331 München, DE", + "country": null, + "didWebUrl": "https://node.acme.example/.well-known/did.json", "name": "Acme Zellen GmbH" }, + "materials": [ + { + "countryOfOrigin": "DE", + "name": "NMC 811 cathode active material", + "recycledPct": 8.0, + "weightKg": 3.1 + }, + { + "countryOfOrigin": "DE", + "name": "Graphite anode active material", + "recycledPct": 4.0, + "weightKg": 1.9 + }, + { + "countryOfOrigin": "DE", + "name": "Aluminium pack housing", + "recycledPct": 60.0, + "weightKg": 2.2 + } + ], + "placedOnMarketDate": "2026-09-01", "productGroup": "battery", + "productGroupData": { + "batteryChemistry": "NMC", + "batteryModelId": "ACME-LMT-48V", + "batteryPassportNumber": "URN:ODL:BATT:09506000134352:ACME-LMT-2026-09", + "batteryType": "lmt", + "batteryWeightKg": 12.4, + "co2ePerUnitKg": 86.4, + "criticalRawMaterials": [ + { + "casNumber": "7440-48-4", + "name": "Cobalt", + "weightGrams": 310.0 + }, + { + "casNumber": "7439-93-2", + "name": "Lithium", + "weightGrams": 420.0 + }, + { + "casNumber": "7782-42-5", + "name": "Natural graphite", + "weightGrams": 1800.0 + } + ], + "cycleLifeTestCRate": 1.0, + "euDeclarationOfConformity": "EU-DoC ACME-2026-LMT-0901, per Regulation (EU) 2023/1542 Art. 18", + "expectedLifetimeCycles": 1200, + "expectedLifetimeReferenceTest": "EN 50604-1:2016, 25 degC, 100% DoD", + "gtin": "09506000134352", + "hazardousSubstances": [ + { + "casNumber": "21324-40-3", + "concentrationPct": 0.8, + "name": "Lithium hexafluorophosphate" + } + ], + "initialRoundTripEfficiencyPct": 95.0, + "internalCellResistanceMohm": 14.0, + "internalPackResistanceMohm": 110.0, + "manufacturingDate": "2026-08-18T00:00:00Z", + "manufacturingPlace": "München, DE", + "markingInformation": "Separate-collection symbol per Art. 13(4); capacity 20 Ah / 960 Wh; 48 V LMT", + "maximumVoltageV": 54.6, + "minimalVoltageV": 39.0, + "nominalCapacityAh": 20.0, + "nominalVoltageV": 48.0, + "notInUseTemperatureRange": { + "maxC": 45.0, + "minC": -10.0 + }, + "notInUseTemperatureReferenceTest": "EN 50604-1:2016 storage clause", + "originalPowerCapabilityW": 960.0, + "powerLimitMaxW": 1500.0, + "powerLimitMinW": 20.0, + "productGroup": "battery", + "ratedCapacityKwh": null, + "recycledContentCobaltPct": 12.0, + "recycledContentLeadPct": 0.0, + "recycledContentLithiumPct": 6.0, + "recycledContentNickelPct": 8.0, + "renewableContentPct": 14.0, + "roundTripEfficiencyAtHalfCycleLifePct": 91.0, + "usableExtinguishingAgent": "Water; cool a burning pack with large volumes of water", + "wasteBatteryInformation": "Return the battery to a bicycle retailer or a municipal collection point. Do not place it in household waste." + }, + "productName": "Acme LMT 48V 20Ah battery pack", "publishedAt": "2026-09-01T09:05:00Z", - "schemaVersion": "2.7.0", - "status": "active" + "qrCodeUrl": "https://dpp.acme.example/01/09506000134352/10/ACME-LMT-2026-09/21/7e4a9c3d2f1e8b6a5d40", + "repairabilityScore": null, + "retentionUntil": "2036-08-29T09:05:00Z", + "schemaVersion": "2.6.0", + "status": "active", + "updatedAt": "2026-09-01T09:05:00Z", + "version": 1 } }, "transferChain": { diff --git a/site/dpp-landing/public/verify/examples/11-canonicalisation-vectors.json b/site/dpp-landing/public/verify/examples/11-canonicalisation-vectors.json new file mode 100644 index 0000000..f4be94e --- /dev/null +++ b/site/dpp-landing/public/verify/examples/11-canonicalisation-vectors.json @@ -0,0 +1,138 @@ +{ + "auditEntries": [ + { + "action": "created", + "actor": "api-key", + "entryHash": "14ed4927896e84128e26e35d46e66cd3cf6ad1fc624766ae2f672c2447948f70", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000001", + "metadata": null, + "newStatus": "draft", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "", + "previousStatus": null, + "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000001", + "timestamp": "2026-09-01T09:00:00Z" + }, + { + "action": "published", + "actor": "api-key", + "entryHash": "fa56550871276ac2375d82b6d14b0f4add0b7ce3e7d9f4b01a2d45f744c63ffa", + "id": "0199a3c2-7b10-7e4a-9c3d-000000000002", + "metadata": { + "fullViewPayload": { + "content": { + "B": 2, + "b": 1, + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5, + "A": 6 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "status": "active" + }, + "publicViewPayload": { + "content": { + "B": 2, + "b": 1, + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5, + "A": 6 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "status": "active" + } + }, + "newStatus": "active", + "passportId": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "prevHash": "14ed4927896e84128e26e35d46e66cd3cf6ad1fc624766ae2f672c2447948f70", + "previousStatus": null, + "requestId": "0199a3c2-7b10-7e4a-8d2e-000000000002", + "timestamp": "2026-09-01T09:05:00.250Z" + } + ], + "didDocuments": { + "did:web:node.acme.example": { + "@context": [ + "https://www.w3.org/ns/did/v1" + ], + "assertionMethod": [ + "did:web:node.acme.example#key-1" + ], + "id": "did:web:node.acme.example", + "verificationMethod": [ + { + "controller": "did:web:node.acme.example", + "id": "did:web:node.acme.example#key-1", + "publicKeyJwk": { + "crv": "Ed25519", + "kty": "OKP", + "x": "6kpsY-KcUgq-9VB7Ey7F-ZVHdq6-vnuSQh7qaRRG0iw" + }, + "type": "JsonWebKey2020" + } + ] + } + }, + "fullView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwidGlueU51bWJlciI6NWUtNywiw4ltaXNzaW9uIjoiMC41ZS0zIHRlc3QiLCLDqSI6Mywi4oKsIjo0LCLwn5iAIjo1LCLvvKEiOjZ9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsInN0YXR1cyI6ImFjdGl2ZSJ9.-0gGJO8CC_JeYka-60cDzkSdj1vqst_47SyafFl9yb4jA2GCc_grSLhYrCrx3kC8rZpT6sTwRFT3gUo5UfUdBg", + "payload": { + "content": { + "B": 2, + "b": 1, + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5, + "A": 6 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "status": "active" + } + }, + "manifest": { + "contentHashes": { + "auditEntries": "1f63a80b7efd79c1c2fb2ec2d2f8b6d7ef49aeb925701076a98059b14b8ab0f7", + "fullView": "aa2588691c23ed546ddafa4d32ca3a96fb905ec06980b76bf2e1562e8d3984a1", + "publicView": "aa2588691c23ed546ddafa4d32ca3a96fb905ec06980b76bf2e1562e8d3984a1" + }, + "coreVersion": "0.20.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.eyJjb250ZW50SGFzaGVzIjp7ImF1ZGl0RW50cmllcyI6IjFmNjNhODBiN2VmZDc5YzFjMmZiMmVjMmQyZjhiNmQ3ZWY0OWFlYjkyNTcwMTA3NmE5ODA1OWIxNGI4YWIwZjciLCJmdWxsVmlldyI6ImFhMjU4ODY5MWMyM2VkNTQ2ZGRhZmE0ZDMyY2EzYTk2ZmI5MDVlYzA2OTgwYjc2YmYyZTE1NjJlOGQzOTg0YTEiLCJwdWJsaWNWaWV3IjoiYWEyNTg4NjkxYzIzZWQ1NDZkZGFmYTRkMzJjYTNhOTZmYjkwNWVjMDY5ODBiNzZiZjJlMTU2MmU4ZDM5ODRhMSJ9LCJjb3JlVmVyc2lvbiI6IjAuMjAuMCIsImNyZWF0ZWRBdCI6IjIwMjYtMDktMjVUMTI6MDA6MDBaIiwiZm9ybWF0VmVyc2lvbiI6IjEiLCJpc3N1ZXJEaWQiOiJkaWQ6d2ViOm5vZGUuYWNtZS5leGFtcGxlIiwibm9kZVZlcnNpb24iOiIwLjE0LjAiLCJwYXNzcG9ydElkIjoiMDE5OWEzYzItN2IxMC03ZTRhLTljM2QtMmYxZThiNmE1ZDQwIn0.IQ6HakefPjlcRC_KPc6GC6zQvb4DukdJf9v00kEgc1_wwo_vUhL9QloZhaSVQQlLcMz3X_oR_dO1ZdxKcEz4BQ", + "publicView": { + "jws": "eyJhbGciOiJFZERTQSIsImtpZCI6ImZlODEyYzEyZjNhYjRjZTZhYzVkYjY5YWMzNTJmOTA2Y2IxYjExZWY0M2ZiMzNlMjUyZWY3ZmY1NTIyNjM4ODkifQ.eyJjb250ZW50Ijp7IkIiOjIsImIiOjEsImh1Z2VOdW1iZXIiOjFlKzIxLCJuZWdhdGl2ZVplcm9TYWZlIjotMC4yNSwidGlueU51bWJlciI6NWUtNywiw4ltaXNzaW9uIjoiMC41ZS0zIHRlc3QiLCLDqSI6Mywi4oKsIjo0LCLwn5iAIjo1LCLvvKEiOjZ9LCJpZCI6IjAxOTlhM2MyLTdiMTAtN2U0YS05YzNkLTJmMWU4YjZhNWQ0MCIsInN0YXR1cyI6ImFjdGl2ZSJ9.-0gGJO8CC_JeYka-60cDzkSdj1vqst_47SyafFl9yb4jA2GCc_grSLhYrCrx3kC8rZpT6sTwRFT3gUo5UfUdBg", + "payload": { + "content": { + "B": 2, + "b": 1, + "hugeNumber": 1e+21, + "negativeZeroSafe": -0.25, + "tinyNumber": 5e-7, + "Émission": "0.5e-3 test", + "é": 3, + "€": 4, + "😀": 5, + "A": 6 + }, + "id": "0199a3c2-7b10-7e4a-9c3d-2f1e8b6a5d40", + "status": "active" + } + } +} \ No newline at end of file diff --git a/site/dpp-landing/tests/verify-expected.json b/site/dpp-landing/tests/verify-expected.json index 61c9069..aa52502 100644 --- a/site/dpp-landing/tests/verify-expected.json +++ b/site/dpp-landing/tests/verify-expected.json @@ -432,11 +432,65 @@ "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 317 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 781 column 14", "exit": 2 }, "10-not-json.txt": { "error": "not a valid dossier — malformed JSON or an unrecognised field: expected value at line 1 column 1", "exit": 2 + }, + "11-canonicalisation-vectors.json": { + "checks": [ + { + "name": "manifest_signature", + "status": "pass" + }, + { + "name": "content_integrity", + "status": "pass" + }, + { + "name": "full_view_signature", + "status": "pass" + }, + { + "name": "public_view_signature", + "status": "pass" + }, + { + "name": "audit_chain", + "status": "pass" + }, + { + "detail": "no transfer chain on this passport", + "name": "transfer_chain", + "status": "absent" + }, + { + "detail": "checkpoint layer not yet implemented — audit chain integrity is checked, but nothing pins the chain head against third-party re-hash", + "name": "checkpoint", + "status": "absent" + }, + { + "detail": "no calculation receipts — dpp-calc invocation is not yet wired (licensed factor data pending)", + "name": "calc_receipts", + "status": "absent" + }, + { + "detail": "no component graph on this passport", + "name": "component_graph", + "status": "absent" + }, + { + "detail": "this passport carries no qualified seal", + "name": "qualified_seal", + "status": "absent" + }, + { + "name": "input_fidelity", + "status": "pass" + } + ], + "exit": 0 } } \ No newline at end of file diff --git a/site/dpp-landing/tests/verify.test.ts b/site/dpp-landing/tests/verify.test.ts index b9b86d6..9044f30 100644 --- a/site/dpp-landing/tests/verify.test.ts +++ b/site/dpp-landing/tests/verify.test.ts @@ -74,16 +74,18 @@ test("a one-character edit to a signed payload is caught, and only where it happ // RFC 8785 §3.2.3 sorts object keys by UTF-16 code unit, not by code point and // not by UTF-8 byte. The three agree for almost every key, so a verifier that // sorts the wrong way passes almost everything, and the golden verdicts above -// would only report it as a bad signature. The engine's example payload carries -// a pair on which they disagree (engine #429): "😀" is the surrogate pair -// 0xD83D 0xDE00 and "A" (fullwidth A) the single unit 0xFF21, so by code unit -// the emoji comes first and by code point it comes last. +// would only report it as a bad signature. The engine's canonicalisation dossier +// carries a pair on which they disagree (engine #429): "😀" is the surrogate +// pair 0xD83D 0xDE00 and "A" (fullwidth A) the single unit 0xFF21, so by code +// unit the emoji comes first and by code point it comes last. That dossier, 11, +// is not a passport and the page never offers it as one; the example passports +// carry only what a real battery passport carries (engine #431). // // This reads the signed bytes themselves, which is what the page checks, not // the readable copies beside them (see the next test for those). test("keys are canonicalised by UTF-16 code unit, as the engine signed them", () => { assert.equal(jcs({ "A": 6, "😀": 5 }), '{"😀":5,"A":6}'); - const dossier = JSON.parse(readFileSync(`${dir}04-valid-full-lifecycle.json`, "utf8")); + const dossier = JSON.parse(readFileSync(`${dir}11-canonicalisation-vectors.json`, "utf8")); const signed = new TextDecoder().decode(b64urlDecode(dossier.fullView.jws.split(".")[1])); assert.ok(signed.indexOf('"😀"') < signed.indexOf('"A"'), "the engine signed the emoji first"); assert.equal(jcs(JSON.parse(signed)), signed, "canonicalising the signed payload again reproduces its bytes"); @@ -118,7 +120,11 @@ test("every object in the examples lists its keys in the order they are signed i }); test("an integer beyond 2^53 is refused, not approximated", async () => { - const text = readFileSync(`${dir}01-valid-simple.json`, "utf8").replace('"ratedCapacityAh": 20', '"ratedCapacityAh": 9007199254740993'); + const raw = readFileSync(`${dir}01-valid-simple.json`, "utf8"); + const text = raw.replace('"expectedLifetimeCycles": 1200', '"expectedLifetimeCycles": 9007199254740993'); + // An edit that finds nothing leaves a valid dossier behind, and the test + // would then fail for the wrong reason, or pass for one after a corpus change. + assert.notEqual(text, raw, "01 carries the integer this test enlarges"); const got = await verifyDossierText(text); assert.equal(got.kind, "unsupported"); }); From c23a45215f9eff2a0d4f480cbc64eaaeb01c8752 Mon Sep 17 00:00:00 2001 From: LKSNDRTMLKV Date: Wed, 30 Sep 2026 12:35:11 +0200 Subject: [PATCH 147/168] fix(web): stop saying the node discards import files, and say instead that everything it stores stays on the operator's infrastructure --- .../src/content/docs/core-concepts.mdx | 4 +-- .../src/content/docs/engine/architecture.mdx | 2 +- .../src/content/docs/engine/retention.mdx | 2 +- .../src/content/docs/engine/self-hosted.mdx | 2 +- .../docs/getting-started/licensing.mdx | 2 +- .../what-odal-can-and-cannot-see.mdx | 12 ++++----- .../src/content/docs/guides/import.mdx | 2 +- site/dpp-landing/src/data/faq.json | 2 +- site/dpp-landing/src/pages/_about.astro | 8 +++--- site/dpp-landing/src/pages/index.astro | 27 +++++++++---------- site/dpp-landing/src/pages/trust.astro | 16 +++++------ 11 files changed, 37 insertions(+), 42 deletions(-) diff --git a/site/dpp-docs/src/content/docs/core-concepts.mdx b/site/dpp-docs/src/content/docs/core-concepts.mdx index 30148f6..6e87672 100644 --- a/site/dpp-docs/src/content/docs/core-concepts.mdx +++ b/site/dpp-docs/src/content/docs/core-concepts.mdx @@ -9,9 +9,9 @@ Odal Node's design rests on three ideas. Each section below links to a fuller pa ## Proof-bound: the data stays yours -The operator's raw source files never enter Odal's systems. The manufacturer validates a passport locally and signs it with their own key, and the node then discards the import files it was built from. The node keeps and serves only the signed passport: the product data the operator chose to publish, with every field bound to a proof and filtered by the reader's access rights. It does not keep the raw exports, spreadsheets or supply-chain detail behind it. +The operator's product data never reaches Odal. The manufacturer validates a passport on their own node and signs it with their own key, and the node stores it there. What the node serves is the signed passport: the product data the operator chose to publish, with every field bound to a proof and filtered by the reader's access rights. -This is how the software works, so it does not depend on trusting us. After signing, the uploaded file is gone. What remains is the passport and a job record of what validated and what did not. Verifying a passport needs only the signed passport and the manufacturer's public identity. Odal does not need to be online, or to exist. +This is how the software works, so it does not depend on trusting us. Verifying a passport needs only the signed passport and the manufacturer's public identity. Odal does not need to be online, or to exist. [What Odal Node can and cannot see](/getting-started/what-odal-can-and-cannot-see) covers the data boundary in full. diff --git a/site/dpp-docs/src/content/docs/engine/architecture.mdx b/site/dpp-docs/src/content/docs/engine/architecture.mdx index e8fa09a..347031a 100644 --- a/site/dpp-docs/src/content/docs/engine/architecture.mdx +++ b/site/dpp-docs/src/content/docs/engine/architecture.mdx @@ -11,7 +11,7 @@ A node does five things, in order: 1. **Take data in**: product data arrives from a spreadsheet or an existing system, and each row becomes a draft. 2. **Check it**: the node validates the draft against its product group's schema and runs that group's sandboxed rules, with plausibility warnings alongside. -3. **Sign it**: the operator's own key signs the passport, once in full and once as its public view. The import file is discarded; what the import found in each row is kept. +3. **Sign it**: the operator's own key signs the passport, once in full and once as its public view. 4. **Keep it**: the signed passport and its history are stored in full, and a published passport can never be deleted. 5. **Serve it**: the passport becomes resolvable, so a scan returns the right view to the right reader, and anyone can verify it. diff --git a/site/dpp-docs/src/content/docs/engine/retention.mdx b/site/dpp-docs/src/content/docs/engine/retention.mdx index 5bbb95a..ec6725f 100644 --- a/site/dpp-docs/src/content/docs/engine/retention.mdx +++ b/site/dpp-docs/src/content/docs/engine/retention.mdx @@ -7,7 +7,7 @@ Under ESPR a published passport is a long-term obligation. Once a product is on A node enforces these guarantees in its code. The operator does not have to switch them on, and cannot switch them off. -Retention works in two opposite directions. The raw *import files* fed into a node are discarded after signing and never kept (see [What Odal can and cannot see](/getting-started/what-odal-can-and-cannot-see)). The signed *passport* they produce is kept permanently and can never be deleted. This page is about the second: how a published passport stays permanent. +A published passport is kept permanently and can never be deleted. This page is about how it stays permanent. ## Publishing locks a passport diff --git a/site/dpp-docs/src/content/docs/engine/self-hosted.mdx b/site/dpp-docs/src/content/docs/engine/self-hosted.mdx index dbd0e09..07493fd 100644 --- a/site/dpp-docs/src/content/docs/engine/self-hosted.mdx +++ b/site/dpp-docs/src/content/docs/engine/self-hosted.mdx @@ -20,7 +20,7 @@ The [Quick Start](/quick-start) is the short version of these steps. ## What stays with you -Your signing key is generated and held on your own infrastructure, encrypted at rest, and never leaves it. Your import files are discarded after signing. What remains is the signed passport (the full product data you published, its history and the metadata needed to serve it) and a record of what each import found, row by row. This holds whoever operates the node; see [What Odal can and cannot see](/getting-started/what-odal-can-and-cannot-see). +Your signing key is generated and held on your own infrastructure, encrypted at rest, and never leaves it. Everything the node stores stays there too, including the signed passport (the full product data you published, its history and the metadata needed to serve it), drafts, and a record of what each import found, row by row. This holds whoever operates the node; see [What Odal can and cannot see](/getting-started/what-odal-can-and-cannot-see). ## Keeping current diff --git a/site/dpp-docs/src/content/docs/getting-started/licensing.mdx b/site/dpp-docs/src/content/docs/getting-started/licensing.mdx index a1df6b3..6ef0afc 100644 --- a/site/dpp-docs/src/content/docs/getting-started/licensing.mdx +++ b/site/dpp-docs/src/content/docs/getting-started/licensing.mdx @@ -54,4 +54,4 @@ To get the licences for a given build, ask the build. `cargo license` or `cargo ## Read next [Self-Hosting](/engine/self-hosted): deploying a node on your own infrastructure. -[What Odal can and cannot see](/getting-started/what-odal-can-and-cannot-see): what the node keeps and what it discards. +[What Odal can and cannot see](/getting-started/what-odal-can-and-cannot-see): what Odal can reach and what stays on your node. diff --git a/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx b/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx index 7611129..e650742 100644 --- a/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx +++ b/site/dpp-docs/src/content/docs/getting-started/what-odal-can-and-cannot-see.mdx @@ -3,7 +3,7 @@ title: What Odal can and cannot see description: "What data Odal can and cannot reach on a self-hosted node, and how the proof-bound design makes that so." --- -On a proof-bound node, the import files are read once on the operator's infrastructure: validated, used to sign the passport, then discarded. The node stores and serves the signed passport, which carries the full product data across its disclosure classes. This page lists what Odal can see, what it cannot see, and what it could see but does not. +On a proof-bound node, product data is validated, signed and stored on the operator's infrastructure. The node serves the signed passport, which carries the full product data across its disclosure classes. This page lists what Odal can see, what it cannot see, and what the node records when a passport is resolved. It describes a self-hosted node, which is the only way Odal Node runs today. A node run for you is an option on the [roadmap](https://odal-node.io/roadmap), not something that exists. If it ever does, this page will describe it separately. @@ -18,15 +18,13 @@ The same as anyone: the public part of the passports you publish, and your DID d - **The restricted parts of your passports**: they are released only to readers presenting a credential your node accepts. - **Your import files, raw production data and supply-chain detail** beyond what you publish in a passport. -## What we could see but do not +## What stays on your node -Nobody keeps the contents of an import file, the operator included. The software reads it once, validates the data, signs the passport and discards the file. No setting, configuration or code path keeps the uploaded file after signing. This is how the software works for everyone, not a choice made per customer. - -The *file* is not kept. Records *derived* from it during the import are: the passport, and a job record with the validation findings for each row. +Everything the node stores stays on your infrastructure, including your passports and drafts, their history, the report of each import, and the scan counts described below. Whoever operates the node holds what it stores; on a self-hosted node, that is you. ## What the node records about resolution -One thing is recorded rather than discarded: when a published passport is resolved, the node can count it. +When a published passport is resolved, the node can count it. The count is an aggregate per passport, per day and per surface, and nothing more. Nothing about the person who scanned is recorded: no IP address, device, location, identity or session. The database has no column for any of it, so none of it can be stored. Generating a QR-code image is counted separately and never added to the scan total, because it measures label printing rather than readers. @@ -35,7 +33,7 @@ The count is an aggregate per passport, per day and per surface, and nothing mor 1. **Import**: product data arrives at your node (CSV, Excel or an ERP export) on infrastructure you control. 2. **Validate**: locally, against the product group's versioned schema and rules. Validation is a pure function: no network calls. 3. **Sign**: your Ed25519 key, held on your infrastructure, signs the validated passport as a JSON Web Signature bound to your `did:web` identity. -4. **Publish**: the signed passport becomes resolvable and the import file is discarded. Public fields are served to anyone; restricted fields only against a verified credential. +4. **Publish**: the signed passport becomes resolvable. Public fields are served to anyone; restricted fields only against a verified credential. 5. **Verify**: anyone verifies against your public DID document. Odal is not involved. ## Read next diff --git a/site/dpp-docs/src/content/docs/guides/import.mdx b/site/dpp-docs/src/content/docs/guides/import.mdx index ccbdfd1..f039afd 100644 --- a/site/dpp-docs/src/content/docs/guides/import.mdx +++ b/site/dpp-docs/src/content/docs/guides/import.mdx @@ -39,7 +39,7 @@ Validation runs the product group's schema and its sandboxed rules. Plausibility ## What the node keeps -The import **file** is discarded once the passport is signed; no setting keeps it. What the import produced is kept: the draft passports, and a job record with each row's validation findings. See [What Odal can and cannot see](/getting-started/what-odal-can-and-cannot-see). +An import creates the draft passports and a job record with each row's validation findings; the node deletes the job record after 30 days. See [What Odal can and cannot see](/getting-started/what-odal-can-and-cannot-see).