Skip to content

fix(passport): content-address copied documents so a re-issued file seals a new version - #678

Open
midego1 wants to merge 2 commits into
unopim:3.xfrom
midego1:fix/passport-content-addressed-documents
Open

midego1 wants to merge 2 commits into
unopim:3.xfrom
midego1:fix/passport-content-addressed-documents

Conversation

@midego1

@midego1 midego1 commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

Problem

PassportPayloadBuilder::copyToAssetDisk() copies a referenced document to publication/{uuid}/{locale}/{field}.{ext} and skips the copy when that path already exists.

Replacing a file in the catalog therefore changes nothing the publisher can see:

  1. the path is unchanged, so the payload is unchanged,
  2. so the checksum is unchanged, so Publisher::publish() returns null and no version is minted,
  3. and because the copy is skipped, the new bytes are never written to the asset disk either.

The passport keeps serving the previous document with no signal that it was replaced. For a re-issued declaration of conformity that is a silent compliance failure, and it also means a sealed version cannot actually guarantee which bytes it attested, since a later publish would have overwritten them at the same path if the guard were removed.

Fix

Content-address the copy. The first 16 hex characters of the file's SHA-256 go into the path:

publication/{uuid}/{locale}/{digest16}/{field}.{ext}

and the full digest is stamped into the document entry as sha256.

  • A re-issued file lands beside the old one, the payload changes, and Publisher seals a new version.
  • Every existing version keeps resolving to the exact bytes it attested.
  • The basename is still the field code, so download filenames are unchanged.
  • The exists() guard is now correct rather than a bug: an object at that path holds those bytes by construction.

The public template and asset controller only read path and label, so the extra sha256 key is inert for them. preview mode is untouched.

Upgrade note

The first republish of a product with documents after this change mints one new version per locale, because the path shape changed. That version is the first whose documents are genuinely attested, so this is intended and a one-time effect.

Tests

  • Builder: a re-issued file yields a new path and digest, keeps the old object intact, and keeps the basename.
  • Builder: an unchanged file yields the same path.
  • End to end (PassportDocumentReissueTest): re-issuing the source file mints version n+1 pointing at the new bytes while the old version's bytes remain; an unchanged file mints nothing. The second case also pins the meta-excluded checksum behaviour.

Related

Companion to #677, which stops pruning the document index of superseded versions. Together they make a sealed version's documents both retained and trustworthy.

…eals a new version

Documents were copied to `publication/{uuid}/{locale}/{field}.{ext}` and
the copy was skipped when that path already existed. Replacing a file
in the catalog therefore changed nothing the publisher could see: the
path was unchanged, so the checksum was unchanged, so no version was
minted, and the new bytes were never written either. The passport kept
serving the previous document with no signal that it had been replaced.
For a declaration of conformity that is a silent compliance failure.

Put the first 16 hex chars of the file's SHA-256 into the path
(`publication/{uuid}/{locale}/{digest}/{field}.{ext}`) and stamp the
full digest into the document entry. A re-issued file lands beside the
old one, the payload changes, and Publisher seals a new version; every
existing version keeps resolving to the exact bytes it attested. The
basename is unchanged, so download filenames are too.

Upgrade note: the first republish of a product with documents after
this change mints one new version per locale, because the path shape
changed. That version is the first whose documents are genuinely
attested, so this is intended.

Tests: builder yields a new path and digest for new bytes while keeping
the old object; the same path for unchanged bytes; and end to end, a
re-issued file mints version n+1 while an unchanged one mints nothing.
@midego1

midego1 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Context, motivation and suggested review order for this and the related PRs: #683

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant