Skip to content
Draft
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
16 changes: 16 additions & 0 deletions packages/shared-content/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# @odal/shared-content

Regulatory-fact content shared between the two Odal Node web properties (`odal-node.io` and `docs.odal-node.io`).

This package has no runtime — it exports typed TypeScript constants. It exists because the same facts (the data-boundary guarantees, the signing pipeline) were previously authored independently on the landing page's `/trust` and in the docs' "What Odal can and cannot see" page, and had already drifted from each other. Both sites now import from here.

Each fact carries two depths where the two sites genuinely need different detail:

- `summary` — one line, for the landing page's compressed presentation.
- `detail` — the full docs-depth wording.

Marketing prose (hero copy, feature-card blurbs) is **not** part of this package and should not be — voice legitimately differs between a landing page and docs. Only facts that must stay identical across both sites (step counts, table rows, guarantees) belong here.

## Sync rule

If a fact changes (a new deployment guarantee, a corrected step), change it once here. Neither site should hand-retype these facts locally again.
17 changes: 17 additions & 0 deletions packages/shared-content/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@odal/shared-content",
"description": "Single-sourced regulatory-fact content (the data boundary, the signing pipeline) shared between odal-node.io and docs.odal-node.io.",
"version": "0.1.0",
"license": "Apache-2.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./data-boundary": "./src/data-boundary.ts",
"./mechanism": "./src/mechanism.ts"
},
"files": [
"src"
]
}
64 changes: 64 additions & 0 deletions packages/shared-content/src/data-boundary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// The data-boundary facts (what Odal can see, cannot see, and could see but
// does not) — single-sourced. Before this package existed, the same facts
// were independently authored on odal-node.io/trust and on docs.odal-node.io's
// "What Odal can and cannot see" page, and had already drifted (a table
// header disagreed: "Managed" vs "Managed (Future)"). Both sites now render
// from these constants; only the depth of what they show differs.

export interface DataBoundaryRow {
property: string;
selfHosted: string;
managed: string;
}

export const dataBoundaryByDeployment: DataBoundaryRow[] = [
{
property: "Node discards raw import files; retains the signed passport (all tiers)",
selfHosted: "Yes — architectural invariant",
managed: "Yes — architectural invariant",
},
{
property: "Odal (the entity) can access stored data",
selfHosted: "No — not present in the deployment",
managed: "Constrained by access controls, audit logging, and contract",
},
{
property: "Odal can sign on the operator's behalf",
selfHosted: "No",
managed: "No — the operator holds the signing keys",
},
];

export interface DisclosureCategory {
/** One-line landing-depth statement. */
summary: string;
/** Docs-depth detail — one or more paragraphs/bullets, most-detailed first. */
detail: string[];
}

export const canSee: DisclosureCategory = {
summary:
"The signed passport you publish to a resolver we operate, and the metadata required to serve it.",
detail: [
"The GS1 Digital Link resolver cache",
"The DID document (public by definition)",
"The audit trail of signature and status transitions (managed deployments only)",
],
};

export const cannotSee: DisclosureCategory = {
summary:
"Your private keys (held in-process on your infrastructure), your raw production data, your supply-chain detail.",
detail: [
"Your private signing keys — held in-process on your infrastructure, encrypted at rest via Argon2id-derived AES-256-GCM, never transmitted",
"Your raw production data, supply-chain detail beyond passport content, or import files",
"In a self-hosted deployment: nothing at all — we have no access to the instance, the database, or the keys",
],
};

export const couldSeeButDoNot: DisclosureCategory = {
summary: "The contents of your import files, which the node discards after validation.",
detail: [
"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.",
],
};
8 changes: 8 additions & 0 deletions packages/shared-content/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* @odal/shared-content — regulatory-fact content shared between the two web
* properties, so a fact (a step count, a table row) is authored once instead
* of independently reworded on each site.
*/

export * from "./data-boundary";
export * from "./mechanism";
46 changes: 46 additions & 0 deletions packages/shared-content/src/mechanism.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// The signing pipeline (import → validate → sign → publish → verify) —
// single-sourced. Previously reworded independently on the landing page (4
// steps, no "Verify"), on odal-node.io/trust (5 steps), and in docs (5
// steps) — the step count itself disagreed between landing and docs.

export interface MechanismStep {
/** Short label — used by both the landing timeline and the docs list. */
label: string;
/** Landing-depth one-line description. */
summary: string;
/** Docs-depth description. */
detail: string;
}

export const mechanismSteps: MechanismStep[] = [
{
label: "Import",
summary: "Product data from CSV, Excel, or your ERP into your own node.",
detail:
"Product data arrives at your node — CSV, Excel, or ERP export. This happens on infrastructure you control.",
},
{
label: "Validate",
summary: "Against versioned sector schemas tracking the regulation, locally.",
detail:
"Locally against versioned sector schemas. Validation is a pure function — no network calls.",
},
{
label: "Sign",
summary: "With your own key, generated and held on your infrastructure.",
detail:
"Your Ed25519 private key, generated and held in-process, signs the validated passport into a JWS bound to your did:web identity.",
},
{
label: "Publish",
summary: "Only the signed passport becomes resolvable, via QR and GS1 Digital Link.",
detail:
"The signed passport becomes resolvable; the raw import files are discarded. Public fields are served to anyone, restricted tiers only against a verified credential.",
},
{
label: "Verify",
summary:
"Any consumer, authority, or recycler verifies the signature against your public DID document — without Odal in the loop.",
detail: "Anyone verifies against your public DID Document. Odal is not in the verify loop.",
},
];
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions site/dpp-docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ export default defineConfig({
starlight({
title: 'Odal Node',
description: 'EU Digital Product Passport infrastructure — open-source core, sovereign by design, built for ESPR compliance.',
// English stays unprefixed (root), matching odal-node.io's URL shape.
// No content has been translated — Starlight ships built-in UI-string
// translations for all four (sidebar chrome, search, "on this page",
// etc.), and its own fallback shows the English page with a visible
// "this page has not been translated yet" notice rather than a 404 —
// no per-locale content directories needed for that to work.
locales: {
root: { label: 'English', lang: 'en' },
de: { label: 'Deutsch', lang: 'de' },
it: { label: 'Italiano', lang: 'it' },
fr: { label: 'Français', lang: 'fr' },
es: { label: 'Español', lang: 'es' },
},
logo: {
light: './src/assets/logo-light.svg',
dark: './src/assets/logo-dark.svg',
Expand Down
1 change: 1 addition & 0 deletions site/dpp-docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"dependencies": {
"@astrojs/starlight": "^0.30.6",
"@odal/brand-tokens": "workspace:*",
"@odal/shared-content": "workspace:*",
"@scalar/api-reference": "^1.62.5",
"astro": "^5",
"vue": "^3.5.39"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,41 +3,54 @@ 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.
---

import {
canSee,
cannotSee,
couldSeeButDoNot,
dataBoundaryByDeployment,
mechanismSteps,
} from '@odal/shared-content';

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.

## By deployment

| Property | Self-hosted | Managed (Future) |
|---|---|---|
| Node discards raw import files; retains the signed passport (all tiers) | 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 |
{/* Row data is single-sourced from @odal/shared-content — this table
previously disagreed with the near-identical one on odal-node.io/trust
(a header read "Managed" on one, "Managed (Future)" here). */}

<table>
<thead>
<tr><th>Property</th><th>Self-hosted</th><th>Managed (Future)</th></tr>
</thead>
<tbody>
{dataBoundaryByDeployment.map((row) => (
<tr><td>{row.property}</td><td>{row.selfHosted}</td><td>{row.managed}</td></tr>
))}
</tbody>
</table>

## What we can see

The signed passport published to a resolver we operate, and the metadata required to serve it:

- The GS1 Digital Link resolver cache
- The DID document (public by definition)
- The audit trail of signature and status transitions (managed deployments only)
<ul>{canSee.detail.map((d) => <li>{d}</li>)}</ul>

## What we cannot see

- Your private signing keys — held in-process on your infrastructure, encrypted at rest via Argon2id-derived AES-256-GCM, never transmitted
- Your raw production data, supply-chain detail beyond passport content, or import files
- In a self-hosted deployment: nothing at all — we have no access to the instance, the database, or the keys
<ul>{cannotSee.detail.map((d) => <li>{d}</li>)}</ul>

## 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.
{couldSeeButDoNot.detail.map((d) => <p>{d}</p>)}

## 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.
5. **Verify** — anyone verifies against your public DID Document. Odal is not in the verify loop.
<ol>
{mechanismSteps.map((step) => (
<li><strong>{step.label}</strong> — {step.detail}</li>
))}
</ol>

## Read next

Expand Down
35 changes: 34 additions & 1 deletion site/dpp-landing/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,40 @@ import { viteStaticCopy } from 'vite-plugin-static-copy';

export default defineConfig({
site: "https://odal-node.io",
integrations: [sitemap()],
// i18n: chrome (nav/footer/badges — src/i18n/ui.ts) is translated for all
// five locales. Page prose (Hero copy, deadline citations, the
// data-boundary facts) is not yet — `fallback` + `fallbackType: "rewrite"`
// serves the English page content under a locale's URL when no
// locale-specific page exists, so /de/, /it/, /fr/, /es/ resolve instead
// of 404ing while prose translation is still pending native review.
i18n: {
defaultLocale: "en",
locales: ["en", "de", "it", "fr", "es"],
routing: {
prefixDefaultLocale: false,
fallbackType: "rewrite",
},
fallback: {
de: "en",
it: "en",
fr: "en",
es: "en",
},
},
integrations: [
sitemap({
i18n: {
defaultLocale: "en",
locales: {
en: "en-US",
de: "de-DE",
it: "it-IT",
fr: "fr-FR",
es: "es-ES",
},
},
}),
],
vite: {
plugins: [
tailwindcss(),
Expand Down
1 change: 1 addition & 0 deletions site/dpp-landing/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"dependencies": {
"@astrojs/sitemap": "^3",
"@odal/brand-tokens": "workspace:*",
"@odal/shared-content": "workspace:*",
"@tailwindcss/vite": "^4",
"astro": "^5",
"tailwindcss": "^4"
Expand Down
21 changes: 13 additions & 8 deletions site/dpp-landing/src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
// 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 { resolveLocale } from "../i18n/config";
import { useTranslations } from "../i18n/ui";

const lang = resolveLocale(Astro.currentLocale);
const t = useTranslations(lang);
const year = new Date().getFullYear();
---

Expand All @@ -14,30 +19,30 @@ const year = new Date().getFullYear();
Odal Node
</p>
<p class="leading-body text-primary-300">
Signed by you. Verified by anyone.
{t.footer.tagline}
</p>
<p class="mt-2 leading-body text-primary-300/80">
Sovereign Digital Product Passport infrastructure for EU ESPR.
{t.footer.subtagline}
</p>
<!-- <p class="mt-3 leading-body text-primary-300/80">
Apache-2.0 core — anyone may build on it.
</p> -->
</div>

<div>
<p class="mb-2 font-semibold text-white">Project</p>
<p class="mb-2 font-semibold text-white">{t.footer.projectHeading}</p>
<ul class="m-0 list-none space-y-1 p-0">
<li><a class="text-primary-200 hover:text-white" href="https://github.com/odal-node/dpp-core">dpp-core on GitHub</a></li>
<li><a class="text-primary-200 hover:text-white" href="https://github.com/odal-node/dpp-engine">dpp-engine on GitHub</a></li>
<li><a class="text-primary-200 hover:text-white" href="https://github.com/odal-node/dpp-core">{t.footer.dppCoreLink}</a></li>
<li><a class="text-primary-200 hover:text-white" href="https://github.com/odal-node/dpp-engine">{t.footer.dppEngineLink}</a></li>
</ul>
</div>

<div>
<p class="mb-2 font-semibold text-white">Privacy &amp; security</p>
<p class="mb-2 font-semibold text-white">{t.footer.privacyHeading}</p>
<ul class="m-0 list-none space-y-1 p-0">
<!-- <li><a class="text-primary-200 hover:text-white" href="/contact">Contact</a></li> -->
<li><a class="text-primary-200 hover:text-white" href="/privacy">Privacy policy</a></li>
<li><a class="text-primary-200 hover:text-white" href="https://github.com/odal-node/dpp-engine/blob/main/SECURITY.md">Security policy</a></li>
<li><a class="text-primary-200 hover:text-white" href="/privacy">{t.footer.privacyPolicyLink}</a></li>
<li><a class="text-primary-200 hover:text-white" href="https://github.com/odal-node/dpp-engine/blob/main/SECURITY.md">{t.footer.securityPolicyLink}</a></li>
<!-- <li><a class="text-primary-200 hover:text-white" href="mailto:contact@odal-node.io">contact@odal-node.io</a></li> -->
</ul>
</div>
Expand Down
Loading
Loading