Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,20 @@ VAULT_BASE_URL=http://localhost:8001/vault # points at the node's vault sub-pat
CACHE_TTL_SECS=30 # worst-case recall-propagation window; raise only with that tradeoff in mind
RATE_LIMIT_RPM=120 # per-IP request limit

# The public origin printed onto the product — the host a scanned QR code
# resolves against, baked into every passport's carrier URL at publish time.
# REQUIRED, by both the node and the resolver — neither has a default.
#
# The default below is the project's own demo resolver and is not wired up yet.
# Every operator serves their own: set this to your resolver deployment's public
# origin before publishing anything real. It cannot be corrected afterwards —
# publishing starts the ESPR retention window and `odal passport archive`
# refuses inside it, so the labels would have to be reissued.
RESOLVER_BASE_URL=https://id.odal-node.io
# The public origin printed onto the product: the host a scanned QR code
# resolves against, baked into every passport's carrier URL at publish time.
# The node signs it into the carrier; the resolver builds its GS1 Digital Link
# redirects on it. The compose file hands this one value to both.
#
# The value below is the resolver on this machine, which is right for a laptop
# and wrong everywhere else. Every operator serves their own: set this to your
# resolver deployment's public origin before publishing anything real. It
# cannot be corrected afterwards — publishing starts the ESPR retention window
# and `odal passport archive` refuses inside it, so the labels would have to be
# reissued.
RESOLVER_BASE_URL=http://localhost:8003
# OPERATOR_DID_URL= # default: derived from VAULT_BASE_URL host

# Private Prometheus metrics endpoint (GET /metrics). Resolver default
Expand Down
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,33 @@ under the pre-1.0 conventions in [VERSIONING.md](docs/governance/VERSIONING.md):

### Breaking

- **`RESOLVER_BASE_URL` is required, by the node and the resolver, and the
resolver now receives it.** *(Breaking: a deployment that never set it no
longer starts. Set it to the public origin your resolver serves at —
`.env.example` ships `http://localhost:8003` for a laptop — and both services
read that one line.)* Both binaries fell back to `https://id.odal-node.io`,
which does not resolve, and the compose file handed the resolver an explicit
environment with no `env_file` — so it **never saw the operator's value**.
Measured on a stack configured exactly as the demo runbook said: the node
signed `http://localhost:8003/01/…/21/…` into every carrier, and the resolver
answered that very URL with a `307` to the dead host, as did the AAS
response's canonical `Link`. Every scanned QR code went nowhere while the
node's own configuration looked right.

The value now has one reader, `dpp_common::config::resolver_base_url`, which
both binaries call: required, an absolute `http`/`https` URL with a host, no
credentials, query or fragment, returned without a trailing `/`. The compose
file passes it to both services with `${RESOLVER_BASE_URL:?}`, so `odal up`
refuses before anything starts; under a production profile, `odal up`'s
preflight also refuses any value naming this machine — `localhost`, a
loopback or unspecified address, IPv4-mapped included, however spelled. It
judges the value compose will actually interpolate: a variable exported in the
shell overrides `.env`, so a stale export was what got signed while the file
read fine — true of every key the preflight checks, not only this one. A
refusal never echoes a password, query or fragment from the value. A default here was a guess about where
another component lives — which is exactly what neither binary can know, and
a wrong guess is signed into labels that cannot be recalled.

- **The redaction moved to `dpp-domain`, and two things it does differently are
visible on the wire.** *(Breaking for **newly published** passports only.
Every public and audience route serves the payload decoded out of the stored
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ Anything not covered by a recipe is plain cargo — e.g. `cargo run -p dpp-node`
to run the node, and `cargo run -p dpp-cli -- bootstrap` to seed operator config
and the first API key.

**Environment**: Copy `.env.example` to `.env` before running. Required vars: `DATABASE_URL`, `KEY_STORE_PATH`, `KEY_STORE_PASSPHRASE`, `DID_WEB_BASE_URL`.
**Environment**: Copy `.env.example` to `.env` before running. Required vars: `DATABASE_URL`, `KEY_STORE_PATH`, `KEY_STORE_PASSPHRASE`, `DID_WEB_BASE_URL`, `RESOLVER_BASE_URL` (the resolver requires it too — it has no default in either binary).

## Architecture

Expand Down
2 changes: 1 addition & 1 deletion api/components/schemas/passport/PassportResponse.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ properties:
GS1 Digital Link the carrier (QR) encodes, set on publish:
{resolverBase}/01/{gtin}/21/{serial} for a trade item, else
{resolverBase}/dpp/{id}. resolverBase is per-deployment
(RESOLVER_BASE_URL, default https://id.odal-node.io).
(RESOLVER_BASE_URL, required, no default).
jwsSignature:
type:
- string
Expand Down
4 changes: 2 additions & 2 deletions api/openapi.bundled.json
Original file line number Diff line number Diff line change
Expand Up @@ -4190,7 +4190,7 @@
"application/aas+json": {
"schema": {
"type": "object",
"description": "An IDTA Asset Administration Shell `Environment` — shells\nand submodels in one self-contained document.\n\n`conceptDescriptions` is **absent**, not empty. This node\ncoins no concept descriptions, and the metamodel constrains\nthat member to `minItems: 1`, so an empty array would make\nthe whole document invalid.\n\n**Public tier only.** The passport is filtered through the\ndisclosure seam before any AAS mapper sees it, so this door\nnever carries a field the JSON-LD door would withhold.\nRestricted and conformity-tier data require a credentialed\nchannel and a different projection.\n\n**Schema-valid, not conformance-certified.** Every\nEnvironment is validated in `dpp-core`'s CI against IDTA's\npublished AAS JSON Schemas for metamodel **3.0, 3.1 and\n3.2**, and must satisfy all three — no single revision is\nthe strictest, so the intersection is the only target that\nmeans \"loadable whichever revision your toolchain\nimplements\".\n\nThat establishes metamodel validity only: it is not a claim\nof IDTA conformance, and it asserts nothing about whether a\nsubmodel matches a published submodel template. Note also\nthat no AAS JSON Schema sets `additionalProperties`, so\nschema validity alone cannot rule out a member the metamodel\ndoes not define; `dpp-core` gates that separately.\n\n**Unsigned, and it says so in a header.** This is a derived\nrepresentation of the signed canonical public view, which is\nwhat `application/ld+json` returns for this same URL. The\npublic proof covers that payload, not this serialisation of\nit, so attaching the signature here would hand a verifier a\nproof that fails against the bytes it arrived with.\n\nEvery `200` therefore carries:\n\n```\nLink: <{resolverBase}/dpp/{dppId}>; rel=\"alternate\"; type=\"application/ld+json\"\n```\n\n`alternate` rather than `canonical`: the two representations\nshare one URL and are separated only by `Accept`, so a\n`canonical` relation would point this resource at itself.\nFollow the link with that `Accept` to obtain the signed\npayload and its proof.\n\n`resolverBase` is per-deployment (`RESOLVER_BASE_URL`,\ndefault `https://id.odal-node.io`). Error responses carry no\n`Link` — an error is not a representation of the passport.\n"
"description": "An IDTA Asset Administration Shell `Environment` — shells\nand submodels in one self-contained document.\n\n`conceptDescriptions` is **absent**, not empty. This node\ncoins no concept descriptions, and the metamodel constrains\nthat member to `minItems: 1`, so an empty array would make\nthe whole document invalid.\n\n**Public tier only.** The passport is filtered through the\ndisclosure seam before any AAS mapper sees it, so this door\nnever carries a field the JSON-LD door would withhold.\nRestricted and conformity-tier data require a credentialed\nchannel and a different projection.\n\n**Schema-valid, not conformance-certified.** Every\nEnvironment is validated in `dpp-core`'s CI against IDTA's\npublished AAS JSON Schemas for metamodel **3.0, 3.1 and\n3.2**, and must satisfy all three — no single revision is\nthe strictest, so the intersection is the only target that\nmeans \"loadable whichever revision your toolchain\nimplements\".\n\nThat establishes metamodel validity only: it is not a claim\nof IDTA conformance, and it asserts nothing about whether a\nsubmodel matches a published submodel template. Note also\nthat no AAS JSON Schema sets `additionalProperties`, so\nschema validity alone cannot rule out a member the metamodel\ndoes not define; `dpp-core` gates that separately.\n\n**Unsigned, and it says so in a header.** This is a derived\nrepresentation of the signed canonical public view, which is\nwhat `application/ld+json` returns for this same URL. The\npublic proof covers that payload, not this serialisation of\nit, so attaching the signature here would hand a verifier a\nproof that fails against the bytes it arrived with.\n\nEvery `200` therefore carries:\n\n```\nLink: <{resolverBase}/dpp/{dppId}>; rel=\"alternate\"; type=\"application/ld+json\"\n```\n\n`alternate` rather than `canonical`: the two representations\nshare one URL and are separated only by `Accept`, so a\n`canonical` relation would point this resource at itself.\nFollow the link with that `Accept` to obtain the signed\npayload and its proof.\n\n`resolverBase` is per-deployment (`RESOLVER_BASE_URL`,\nrequired, no default). Error responses carry no\n`Link` — an error is not a representation of the passport.\n"
}
}
}
Expand Down Expand Up @@ -5024,7 +5024,7 @@
"null"
],
"format": "uri",
"description": "GS1 Digital Link the carrier (QR) encodes, set on publish: {resolverBase}/01/{gtin}/21/{serial} for a trade item, else {resolverBase}/dpp/{id}. resolverBase is per-deployment (RESOLVER_BASE_URL, default https://id.odal-node.io)."
"description": "GS1 Digital Link the carrier (QR) encodes, set on publish: {resolverBase}/01/{gtin}/21/{serial} for a trade item, else {resolverBase}/dpp/{id}. resolverBase is per-deployment (RESOLVER_BASE_URL, required, no default)."
},
"jwsSignature": {
"type": [
Expand Down
4 changes: 2 additions & 2 deletions api/openapi.bundled.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3410,7 +3410,7 @@ paths:
payload and its proof.

`resolverBase` is per-deployment (`RESOLVER_BASE_URL`,
default `https://id.odal-node.io`). Error responses carry no
required, no default). Error responses carry no
`Link` — an error is not a representation of the passport.
'404':
description: |
Expand Down Expand Up @@ -4164,7 +4164,7 @@ components:
- string
- 'null'
format: uri
description: 'GS1 Digital Link the carrier (QR) encodes, set on publish: {resolverBase}/01/{gtin}/21/{serial} for a trade item, else {resolverBase}/dpp/{id}. resolverBase is per-deployment (RESOLVER_BASE_URL, default https://id.odal-node.io).'
description: 'GS1 Digital Link the carrier (QR) encodes, set on publish: {resolverBase}/01/{gtin}/21/{serial} for a trade item, else {resolverBase}/dpp/{id}. resolverBase is per-deployment (RESOLVER_BASE_URL, required, no default).'
jwsSignature:
type:
- string
Expand Down
2 changes: 1 addition & 1 deletion api/paths/resolver/dpp_{dppId}.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ get:

`resolverBase` is per-deployment (`RESOLVER_BASE_URL`,

default `https://id.odal-node.io`). Error responses carry no
required, no default). Error responses carry no

`Link` — an error is not a representation of the passport.
'404':
Expand Down
4 changes: 4 additions & 0 deletions cli/src/console/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,10 @@ async fn step_infrastructure(cfg: &Config) -> Result<()> {
" {} (e.g. https://your-domain.example)",
style("DID_WEB_BASE_URL").dim()
);
println!(
" {} (your resolver's public origin — signed into every QR code)",
style("RESOLVER_BASE_URL").dim()
);
println!();
}

Expand Down
Loading
Loading