diff --git a/CHANGELOG.md b/CHANGELOG.md index f614f2b..94e7e06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,122 +2,22 @@ This kit's version **tracks the OpenDPP API contract version it carries** (`openapi.json`'s `info.version`) — so "which kit is this?" answers "the contract it documents." Releases are git-tagged -`v`. The vendored standards keep their own versions (IDTA AAS v3.0 / +`v`. The vendored standards keep their own versions (IDTA AAS v3.1 / IDTA-01001-3-1; UNTP DPP v0.7.0). Format: [Keep a Changelog](https://keepachangelog.com). -## [Unreleased] +## [1.0.0] — initial release -_Nothing yet._ +The OpenDPP interop boundary kit, carrying OpenDPP public API contract **1.0.0** (`openapi.json`). +Contents: -## [1.6.0] — API contract 1.6.0 + documentation-completeness additions +- **Official + reference schemas** (`schemas/`) — IDTA Asset Administration Shell v3.1 + (IDTA-01001-3-1), UNTP DigitalProductPassport v0.7.0 / W3C Verifiable Credentials, and the vendored + CIRPASS-2 EU-registry pointer schema. +- **Live-reproducible samples** (`samples/`) — the AASX package and the UNTP / W3C-VC credential + representations (`vc+jwt`, embedded Data Integrity `vc+ld+json`, and SD-JWT-VC) that a verifier can + re-derive from the live OpenDPP API and check independently. +- **Offline conformance validator** (`validate/`) — `aas · untp · registry · semanticids · shacl · sdjwt`. +- **The CC-BY IDTA `semanticId` allowlist** (`idta-semantic-ids.json`), the field mappings, and + OpenDPP's **non-normative SHACL shapes** (`shapes/`). -Refreshed `openapi.json` to OpenDPP API contract **1.6.0**. The contract gained backward-compatible -documentation of public surface it already served (no endpoint behaviour changed): - -- **`GET /contexts/dpp/v1`** — the canonical, resolvable `@vocab`-based JSON-LD context that every - public passport/unit document references in its `@context` (the one to dereference when expanding - OpenDPP JSON-LD). `GET /context/v1` is now labelled the secondary fixed term list. -- **`GET /tenants/{tenantId}/did.json`** and **`GET /tenants/{tenantId}/status/revocation`** — the - issuer `did:web` document (public keys only) and the W3C Bitstring Status List used to verify and - revocation-check OpenDPP-issued Verifiable Credentials (new "Verifiable Credentials" tag). -- Verifiable-Credential content negotiation (`application/vc+jwt`, `application/vc+ld+json`, - `application/dc+sd-jwt`) and a structured **`406 Not Acceptable`** are now documented on the - passport resolvers and the owner-side `GET /api/v1/passports/{id}` alias. -- Description corrections (grants pagination; the `passport.updated` webhook on live passport edits; - the operator-not-found 404 message). - -Additive only — no paths, fields or status codes removed; existing integrations are unaffected. The -vendored standards and the IDTA `semanticId` allowlist are unchanged. - -## [1.5.0] — API contract 1.5.0 + SD-JWT-VC selective disclosure - -Carries OpenDPP API contract **1.5.0**. - -### Added - -- **`shapes/opendpp-dpp-shapes.ttl`** — OpenDPP-authored, **NON-NORMATIVE** SHACL starter shapes for the - DPP / battery (ESPR) vertical, targeting OpenDPP's real public JSON-LD DPP vocabulary - (`https://opendpp-node.eu/ns/dpp#` + `…/contexts/dpp/v1#`). They fill the gap left by the CIRPASS-2 - `dpp-validator`'s placeholder `example.org` shapes and are offered as a starter contribution to - CIRPASS-2 — NOT accepted, normative, "certified", or "EU-official". CIRPASS-2's own `dpp-validator` is - not used as an oracle (#175 R5). -- **`validate/validate.mjs shacl `** — a fifth offline validator door: expands an - OpenDPP `application/ld+json` passport to RDF (offline — the remote `@context` URL is stubbed so the - inline context is used, no network) and validates it against the SHACL shapes, printing a per-violation - report + `✓ CONFORMS` / `✗ NON-CONFORMING` (exit 0 / 1). -- **`samples/battery-passport.jsonld`** — the public `application/ld+json` passport projection of the demo - battery, which conforms to the new shapes. -- **`samples/textile-vc-credential.json` + `samples/textile-vc.jwt`** — a NON-BATTERY (textiles) UNTP DPP - credential, the first non-battery sample. It demonstrates OpenDPP's per-category **typed mapping**: fiber - composition → typed `materialProvenance`, and recycled content → a self-declared circularity - `performanceClaim` (against an OpenDPP self-declaration criterion, **not** an ESPR/third-party one) — - instead of the `characteristics` open bag. Live-fetched from the demo service (passport - `…/01/09501101531000`), schema-valid against UNTP DPP v0.7.0, and signed by the same demo tenant so it - verifies against the existing `battery-issuer-did.json`. CONFORMANCE.md's product-category row updated - accordingly (backend #119). -- **`samples/battery-vc.sdjwt` + `samples/battery-vc-presented.sdjwt`** — the demo battery as a conformant - IETF **SD-JWT-VC** (cryptographic selective disclosure): the issuer's full SD-JWT plus a 2-of-4 **holder - presentation** (a withheld claim survives only as an opaque `_sd` digest, yet it still verifies). Carries - the SD-JWT-VC required `iss` + `vct`; media type `application/dc+sd-jwt` (legacy `vc+sd-jwt` accepted). - Live-fetched from the demo service (`…/01/09501101532007`), verified against the bundled `did.json`. -- **`validate/validate.mjs sdjwt `** — a sixth offline validator door (zero extra deps): decodes - the SD-JWT-VC, checks the profile (`typ`/`iss`/`vct`), reconstructs disclosures (rejecting forged/duplicate - ones + an unsupported `_sd_alg`, per IETF SD-JWT §8.1), and verifies the ES256 signature against the - committed `did:web` document via Node WebCrypto — `✓ VALID` / `✗ NON-CONFORMING` (exit 0 / 1). The - CONFORMANCE selective-disclosure row flips to ✅ (backend #118). - -**API contract bumped 1.4.1 → 1.5.0** — the backend added `application/dc+sd-jwt` as a content type on the -public resolution endpoints (a backward-compatible addition), so `openapi.json` is refreshed to **1.5.0**. The -SHACL shapes, the textile sample, and the SD-JWT-VC door/samples are reference/tooling that ride along with -this release. - -## [1.4.1] — API contract 1.4.1 + CIRPASS-2 registry interop - -Carries OpenDPP API contract **1.4.1**. - -### Changed - -- **`openapi.json` refreshed to contract 1.4.1** — synced to the live API: the AAS-environment schema - descriptions now enumerate the full submodel set (the IDTA Digital Nameplate + the per-category submodel - views), and the JSON-LD context-endpoint description is corrected. Documentation-only contract change — no - breaking change. - -### Added - -- **`idta-semantic-ids.json`** — the authoritative IDTA submodel-template `semanticId` allowlist - (published/deprecated status + version), derived from - [admin-shell-io/submodel-templates](https://github.com/admin-shell-io/submodel-templates) (CC-BY-4.0, - pinned `784d22e`) and kept in lockstep with the OpenDPP backend's machine-checked snapshot. -- **`validate/validate.mjs semanticids `** — an offline `semanticId` classifier - (`real-idta-published` / `idta-deprecated` / `vendor-coined` / `eclass` / `unknown`), so IDTA - template **identity** is independently checkable on any AAS output (`--strict` to fail on a deprecated - or unverified IDTA-namespace id). -- **CONFORMANCE.md** — an "IDTA submodel-template identity" row tied to the new check, plus an explicit - "identity ≠ structural conformance" honesty bullet. -- **`schemas/cirpass2-eu-registry-pointer.schema.json`** + **`validate/validate.mjs registry `** - — the CIRPASS-2 `mock-eu-registry` pointer schema (ESPR Art. 13 index record, JSON Schema draft-2020-12; - vendored verbatim from `default-schema.json`, pinned `b383c4d`; Apache-2.0, **NON-NORMATIVE**) and a third - offline validator door, so an OpenDPP → EU-registry pointer is independently checkable (#175). -- **`samples/battery-registry-pointer-model.json`**, **`samples/battery-registry-pointer-item.json`** — the - MODEL and ITEM pointer projections of the demo battery, both schema-valid. -- **README.md + CONFORMANCE.md** — the OpenDPP → EU-registry-pointer field mapping, the MODEL/BATCH/ITEM - granularity model, the **6-of-14** `dpp-data-extractor` discovery-key coverage table, and a "CIRPASS-2 - reference ecosystem (non-normative)" conformance block. CIRPASS-2 is **NON-NORMATIVE** — "validated against - the reference", never "certified" / "EU-official". - -(The `semanticids` and CIRPASS-2 items above are reference/tooling — non-contract; this release's contract -change is the `openapi.json` refresh to 1.4.1.) - -## [1.4.0] — initial public release - -The OpenDPP interoperability boundary, lifted from the product into the open: - -- **Schemas** — the official IDTA AAS v3.0 (IDTA-01001-3-1) and UNTP DPP v0.7.0 JSON Schemas (vendored). -- **Samples** — live-reproducible artifacts for one battery: the AAS v3.0 Environment + AASX package, - the enveloping `vc+jwt`, the embedded `vc+ld+json` (W3C Data Integrity, `ecdsa-jcs-2019`), the - per-unit (item-granularity) credentials, and the issuer `did:web` document. -- **Validator** — a dependency-light offline conformance validator (`validate/validate.mjs`). -- **Contract** — the curated public `openapi.json`, the conformance matrix (`CONFORMANCE.md`), and the - AAS + UNTP field mappings (in `README.md`). -- Carries OpenDPP API contract **1.4.0**, which includes the embedded `vc+ld+json` Data Integrity - representation alongside the enveloping `vc+jwt`, on both `/passport/{id}` and `/unit/{id}`. +Apache-2.0. The conformance posture is documented in [CONFORMANCE.md](CONFORMANCE.md). diff --git a/openapi.json b/openapi.json index fd3aa32..cc9ac9c 100644 --- a/openapi.json +++ b/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "OpenDPP Integration API", - "version": "1.6.0", + "version": "1.0.0", "summary": "Create, validate, seal and publish EU Digital Product Passports; serialize battery units; manage facilities, access grants and webhooks; resolve and verify passports publicly.", "description": "OpenDPP is a B2B platform for EU Digital Product Passports (DPPs), aligned with the ESPR (Regulation (EU) 2024/1781) data requirements and the EU Battery Regulation (Regulation (EU) 2023/1542). This specification documents the **public integration surface**: everything an external system needs to create, validate, seal, publish, resolve and verify passports.\n\n## Authentication\nAuthenticate with a tenant **API key** sent as a Bearer token: `Authorization: Bearer op_dpp_token_…`. Keys are created in the Client Console (Developers → API keys), are shown **once** at creation, carry a role plus optional narrowed permissions and optional expiry, and can be revoked at any time. API-key clients are exempt from CSRF requirements. Public endpoints (tagged **Public Resolution**, plus the public validators and the audit verifier) need no credentials.\n\n## Tenancy\nTenant identity is **token-bound** — it is derived from your API key, never from the request host. The same paths work on the apex host and on tenant workspace hosts (`https://.opendpp-node.eu`); when a workspace host is used, it must match the key's tenant (requests across workspaces are rejected with `403`).\n\n## Errors\nAuthenticated endpoints return `{ success: false, error, message }` (some omit `success`). ESPR metadata validation failures return the richer shape documented as **ValidationFailed** with per-field `errors[]`/`warnings[]` (localizable via `?lang=` or `Accept-Language`; 28 languages). Bulk endpoints report row-level problems as `errors: string[]`. Malformed JSON and query-string violations return Fastify's default `{ statusCode, code, error, message }` body.\n\n## Rate limits\nGlobal limit: **100 requests/min per IP** (higher for verified crawlers), with `x-ratelimit-*` response headers. Public passport resolution is additionally limited to **30 requests/min per IP** (no headers). The public validator is limited to **10 requests/min per IP**. Stay under these limits with client-side queueing; on `429`, back off and retry after the indicated window.\n\n## Sealing & verification\nPassport seals are **eIDAS advanced electronic seals** (ECDSA P-256 over a Merkle root of the passport content, optional RFC 3161 timestamp). Anyone can verify a seal — no account required. `POST /api/v1/audit/verify` recomputes every Merkle leaf from the submitted values, so it requires the unredacted document (caller-supplied redacted-leaf hashes are deliberately not trusted). Redacted documents remain verifiable **offline**: masked fields keep their true leaf hashes in `proof.redactedLeaves`, letting any verifier rebuild the sealed root without the privileged values.\n\n## Public access tiers\nPublic resolution endpoints serve **tiered** views of the same URL: the public tier for anonymous callers; a restricted tier for holders of legitimate-interest (`dpp_li_…`) or authority (`dpp_auth_…`) capability tokens (presented as a Bearer token or `?grant=` query parameter); and the owner tier for the issuing tenant's own credentials.\n\n## Webhooks\nSubscribe to passport lifecycle events (`passport.ingested`, `passport.sealed`, `passport.recalled`, or `*`). Deliveries are HMAC-SHA256-signed; see the **webhooks** section of this document for the exact signature scheme, retry schedule, and payloads.\n\nThis document is also served machine-readably at [`/openapi.json`](https://opendpp-node.eu/openapi.json) and [`/openapi.yaml`](https://opendpp-node.eu/openapi.yaml).\n\n## Open interoperability kit\nThe interoperability boundary — the official AAS + UNTP/W3C-VC schemas, live-reproducible samples, an offline conformance validator, and the field mappings — is **open source** at [github.com/OpenDPP/opendpp-interop](https://github.com/OpenDPP/opendpp-interop) (Apache-2.0). It lets any integrator validate and verify OpenDPP's standards-conformant output without access to the product source.", "contact": { @@ -2866,7 +2866,7 @@ "value": { "success": false, "error": "Validation Failed", - "message": "Dynamic metadata payload failed ESPR category compliance validation", + "message": "Dynamic metadata payload failed ESPR category schema validation", "errors": [ { "path": "tensileStrengthClass", @@ -3116,7 +3116,7 @@ "Passports" ], "summary": "Dry-run ESPR validation of passport metadata (nothing is stored)", - "description": "Runs the full ESPR category compliance validation on a metadata payload **without persisting anything** — intended for pre-flight checks in integration pipelines.\n\n**Permission:** `passport:create` (Bearer API key or session JWT + CSRF for cookie sessions). Despite being read-only in effect, it is gated as a write permission, so subscription gating (**402**) applies.\n\n**Rate limit:** global 100 requests/min per IP. **Body limit: 262,144 bytes (256 KiB)** → **413** beyond that.\n\n**Behavioral caveats:**\n- The EPCIS **traceability lineage audit is NOT run** here (it only runs at real ingestion), so a payload can pass this dry-run and still fail `POST /api/v1/passports` on traceability errors.\n- `operatorId` is accepted by the body schema but **ignored** by the handler.\n- The 200 body always carries `errors: []`; `warnings` is **omitted entirely** when there are none (it is not an empty array). The same omission applies to `warnings` on the 400 Validation Failed body.\n- `friendlyMessage` localization via `?lang=` / `Accept-Language` (28 languages, default `en`); category-validity errors (`metadata.category` missing or unknown) carry no `friendlyMessage`.\n- Structural rejections of the request body (e.g. missing `productId`, non-object `metadata`) and malformed JSON return just `{\"error\": \"Bad Request\", \"message\": …}`; the only structurally bad input that reaches the handler is a whitespace-only `productId`, answered with the fuller `Bad Request` body shown below.", + "description": "Runs the full ESPR category schema validation on a metadata payload **without persisting anything** — intended for pre-flight checks in integration pipelines.\n\n**Permission:** `passport:create` (Bearer API key or session JWT + CSRF for cookie sessions). Despite being read-only in effect, it is gated as a write permission, so subscription gating (**402**) applies.\n\n**Rate limit:** global 100 requests/min per IP. **Body limit: 262,144 bytes (256 KiB)** → **413** beyond that.\n\n**Behavioral caveats:**\n- The EPCIS **traceability lineage audit is NOT run** here (it only runs at real ingestion), so a payload can pass this dry-run and still fail `POST /api/v1/passports` on traceability errors.\n- `operatorId` is accepted by the body schema but **ignored** by the handler.\n- The 200 body always carries `errors: []`; `warnings` is **omitted entirely** when there are none (it is not an empty array). The same omission applies to `warnings` on the 400 Validation Failed body.\n- `friendlyMessage` localization via `?lang=` / `Accept-Language` (28 languages, default `en`); category-validity errors (`metadata.category` missing or unknown) carry no `friendlyMessage`.\n- Structural rejections of the request body (e.g. missing `productId`, non-object `metadata`) and malformed JSON return just `{\"error\": \"Bad Request\", \"message\": …}`; the only structurally bad input that reaches the handler is a whitespace-only `productId`, answered with the fuller `Bad Request` body shown below.", "security": [ { "ApiKeyAuth": [] @@ -3190,7 +3190,7 @@ }, "example": { "success": true, - "message": "Passport metadata payload is 100% valid and ESPR category compliant", + "message": "Passport metadata payload is valid against the ESPR category data schema", "category": "iron-steel", "errors": [] } @@ -3210,7 +3210,7 @@ "value": { "success": false, "error": "Validation Failed", - "message": "Dynamic metadata payload failed ESPR category compliance validation", + "message": "Dynamic metadata payload failed ESPR category schema validation", "category": "iron-steel", "errors": [ { @@ -3375,7 +3375,7 @@ }, "example": { "success": true, - "message": "Passport metadata payload is 100% valid and ESPR category compliant", + "message": "Passport metadata payload is valid against the ESPR category data schema", "category": "iron-steel", "errors": [] } @@ -3395,7 +3395,7 @@ "value": { "success": false, "error": "Validation Failed", - "message": "Dynamic metadata payload failed ESPR category compliance validation", + "message": "Dynamic metadata payload failed ESPR category schema validation", "category": "textiles", "errors": [ { @@ -4747,7 +4747,7 @@ "value": { "success": false, "error": "Validation Failed", - "message": "Dynamic metadata payload failed ESPR category compliance validation", + "message": "Dynamic metadata payload failed ESPR category schema validation", "errors": [ { "path": "fiberComposition", @@ -7437,7 +7437,7 @@ "Traceability & Audit" ], "summary": "Run heuristic UFLPA/EUDR compliance screening over an event's lineage", - "description": "Walks the same upstream lineage DAG as `GET /api/v1/events/{id}/lineage` and screens every node's location data against two heuristic rules:\n\n- **UFLPA** — flags any node whose `bizLocation` starts with `CN-65` (ISO 3166-2 Xinjiang), contains the keyword `XINJIANG` (case-insensitive), or whose `readPoint` contains the coordinate pair `43.8256,87.6168`.\n- **EUDR** — flags any node whose `readPoint` parses as `geo:,` (or bare `,`) coordinates inside the sample deforestation polygon lat −5.0…−3.0, lng −65.0…−60.0.\n\nThese are geographic screening heuristics evaluated against the data registered on this node — not a legal compliance determination.\n\n**Permission:** `passport:read` (a read permission despite the POST verb — no subscription gating). Cookie-session clients must send the `X-CSRF-Token` header; Bearer clients are exempt. Tenant scoping and the `SUPER_ADMIN` bypass are identical to the lineage endpoint. **No request body is read** — send an empty body (an empty or absent JSON body is accepted).\n\n**Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers).\n\nWhen zero violations are found, the response embeds a `TraceabilityComplianceCertificate` object (status `VERIFIED_COMPLIANT`, standards `EUDR-2026` / `UFLPA-2026`); otherwise `certificate` is `null` and `errors` lists each violation as a human-readable string. ANY failure — unknown event id, other-tenant id, or even a circular lineage graph — is reported as the same generic 404 body.", + "description": "Walks the same upstream lineage DAG as `GET /api/v1/events/{id}/lineage` and screens every node's location data against two heuristic rules:\n\n- **UFLPA** — flags any node whose `bizLocation` starts with `CN-65` (ISO 3166-2 Xinjiang), contains the keyword `XINJIANG` (case-insensitive), or whose `readPoint` contains the coordinate pair `43.8256,87.6168`.\n- **EUDR** — flags any node whose `readPoint` parses as `geo:,` (or bare `,`) coordinates inside the sample deforestation polygon lat −5.0…−3.0, lng −65.0…−60.0.\n\nThese are geographic screening heuristics evaluated against the data registered on this node — not a legal compliance determination.\n\n**Permission:** `passport:read` (a read permission despite the POST verb — no subscription gating). Cookie-session clients must send the `X-CSRF-Token` header; Bearer clients are exempt. Tenant scoping and the `SUPER_ADMIN` bypass are identical to the lineage endpoint. **No request body is read** — send an empty body (an empty or absent JSON body is accepted).\n\n**Rate limit:** global limiter, 100 requests/min per IP (standard `x-ratelimit-*` headers).\n\nWhen zero violations are found, the response embeds a `TraceabilityComplianceCertificate` object (status `SCREENED_NO_MATCHES`, screens `OpenDPP-EUDR-heuristic` / `OpenDPP-UFLPA-screen`); otherwise `certificate` is `null` and `errors` lists each violation as a human-readable string. ANY failure — unknown event id, other-tenant id, or even a circular lineage graph — is reported as the same generic 404 body.", "security": [ { "ApiKeyAuth": [] @@ -7473,10 +7473,10 @@ "certificate": { "type": "TraceabilityComplianceCertificate", "rootEventId": "9b2fa884-1c3d-4e5f-8a6b-7c8d9e0f1a2b", - "status": "VERIFIED_COMPLIANT", + "status": "SCREENED_NO_MATCHES", "regulatoryStandards": [ - "EUDR-2026", - "UFLPA-2026" + "OpenDPP-EUDR-heuristic", + "OpenDPP-UFLPA-screen" ] } } @@ -7487,7 +7487,7 @@ "eventId": "9b2fa884-1c3d-4e5f-8a6b-7c8d9e0f1a2b", "compliant": false, "errors": [ - "UFLPA Compliance Failure: Event [4c81d2e6-9f0a-4b3c-8d5e-1a2b3c4d5e6f] contains raw materials originating from prohibited Xinjiang region (CN-65 Urumqi)." + "UFLPA screen: Event [4c81d2e6-9f0a-4b3c-8d5e-1a2b3c4d5e6f] location matches the Xinjiang sourcing heuristic (CN-65 Urumqi)." ], "auditedAt": "2026-06-12T09:41:00.000Z", "certificate": null @@ -11036,7 +11036,7 @@ }, "message": { "type": "string", - "const": "Passport metadata payload is 100% valid and ESPR category compliant" + "example": "Passport metadata payload is valid against the ESPR category data schema" }, "category": { "type": "string", @@ -11439,7 +11439,7 @@ }, "message": { "type": "string", - "const": "Dynamic metadata payload failed ESPR category compliance validation" + "example": "Dynamic metadata payload failed ESPR category schema validation" }, "errors": { "type": "array", @@ -13024,18 +13024,16 @@ }, "status": { "type": "string", - "const": "VERIFIED_COMPLIANT" + "example": "SCREENED_NO_MATCHES", + "description": "Screening outcome — informational (e.g. `SCREENED_NO_MATCHES` when no geographic screen matched); NOT a legal compliance verdict." }, "regulatoryStandards": { "type": "array", "items": { "type": "string", - "enum": [ - "EUDR-2026", - "UFLPA-2026" - ] + "example": "OpenDPP-EUDR-heuristic" }, - "description": "Always `[\"EUDR-2026\", \"UFLPA-2026\"]`." + "description": "Vendor screening-heuristic identifiers applied (e.g. `OpenDPP-EUDR-heuristic`, `OpenDPP-UFLPA-screen`) — informational, NOT EU regulatory standards." } } },