Skip to content

Encypher text provenance as the provenance.encypher module - #1448

Draft
erik-sv wants to merge 12 commits into
IABTechLab:feature/55-scripts-in-cratesfrom
erik-sv:encypher/v2-provenance-module
Draft

erik-sv wants to merge 12 commits into
IABTechLab:feature/55-scripts-in-cratesfrom
erik-sv:encypher/v2-provenance-module

Conversation

@erik-sv

@erik-sv erik-sv commented Oct 24, 2026 •

Copy link
Copy Markdown

Ports Encypher's text provenance contribution onto the module architecture of the stack tracked in #777, based on row 55 (#1423, feature/55-scripts-in-crates at 5be1147). It changes no row of the stack.

What publishers get

A deployment can sign the article on its public pages with invisible provenance markers and a C2PA manifest link by selecting one module and placing its middleware, with core naming no vendor:

[provenance]
module = "encypher"

[provenance.encypher]
api_base_url = "https://api.example.com"
domain = "publisher.example.com"
worker_token = "encypher_worker_token"

[[serve]]
media_type = "text/html"
path = "/news/"
middleware = ["provenance.encypher"]

Every page the module cannot sign passes through unchanged.

Neutral core seams

  1. StreamProcessor::async_finish and StreamProcessor::finish: a middleware's stream processor may finish its last output asynchronously, with a deadline. A processor made of others ends each in order. Every publisher path that can await now ends its processor this way (no-hold pipeline, both auction-hold loops, the serve pass on a shared template, the lazy stream). A body read whole takes the same chunk pipeline, and the synchronous stream_publisher_body path is removed so no path skips a finish.
  2. PlatformTimer, optional on RuntimeServices, provided on Cloudflare, Spin and Axum.
  3. MiddlewareContext::status and MiddlewareContext::origin_private, read before core writes its own cache policy.
  4. Runtime KV on Fastly (the store linked as trusted_server_kv) and Axum (TRUSTED_SERVER_KV_PATH, on the blocking pool). Cloudflare and Spin already open their declared store on the stack.

The module

crates/provenance/encypher. Preparation applies the request gates with no I/O (GET, status 200, configured domain, origin response not marked private); the stream processor holds the page and signs in its asynchronous finish. Serve phase only, so a failed signing is never stored for every reader. It requires no permission and reads no gated value. It sends the article's visible text and the page address without its query. A page whose origin set a cookie or forbade shared caching is never sent, but the module cannot detect other personalized pages: a deployment places it only on public article routes and marks personalized responses private at the origin.

Removed in the port: the enabled flag, path_prefixes (the entry's path covers it), the [integrations.encypher] table, the HTML stream processor factory and the shared-template refusal.

Design: docs/superpowers/specs/2026-10-24-encypher-provenance-module-design.md.

Verification, run locally at e1690de or its parent

Command Result
cargo fmt --all -- --check 0
cargo clippy-fastly, -axum, -cloudflare, -cloudflare-wasm, -spin-native, -spin-wasm, -cli, -codegen 0 each
cargo test -p trusted-server-provenance-encypher 65 passed
cargo test -p trusted-server-core --lib 3020 passed, plus the new auction-hold test (1 passed)
cargo test-fastly, test-axum, test-cloudflare, test-spin no failures
cargo check-cloudflare, check-spin, build-fastly no errors
cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test parity 0
scripts/lint-wasm-clock.sh 0
python3 scripts/lint-core-platform-names.py 0
JS npx vitest run, npm run format 0, 0
cd docs && npm run format; markdown prettier check outside docs/ 0, 0
cargo test -p trusted-server-core --test template_build_digest, and its clippy 0, 0
docs-proposal: shellcheck (via uvx --from shellcheck-py), node --test, test.sh 0, 0, 0

Adapter suites ran before the last rustfmt-only change and the auction-hold test; core and module tests ran after the behavior edits.

Runtime smoke (Axum dev server, throwaway script, not committed): module selected, [[serve]] on /news/, runtime KV on, local origin with a public and a private article. Both pages returned 200 with the article intact. The public page was held and passed through (outcome=passed_through reason=http_error, no reachable API). The private page was not held (gate=origin_private). The config sets [geo] assume_single_jurisdiction = true, which the stack requires with [ec] module; scripts/smoke-axum.sh itself does not set it and stops at that check, from code and scripts this branch leaves unchanged.

Limit: the runtime smoke pointed the module at a signing endpoint that was not available, to exercise fail-open. Successful signing is tested in process only, by the publisher-path tests (buffered and streaming finalizers, stub origin and stub API). The live Encypher API was not exercised.

A stream processor may implement StreamProcessor::async_finish over the
bytes its last process_chunk call returned. StreamProcessor::finish ends
a processor once; a processor made of others ends each in order, so each
is handed what the one before it finished with. Every publisher path that
can await now ends its processor this way: the no-hold pipeline, both
auction-hold loops, the serve pass on a shared template and the lazy
stream. A body read whole takes the same chunk pipeline, and the
synchronous stream_publisher_body path is removed.

A middleware is also told the response status and whether the origin
marked the response as one reader's, read before core writes its own
cache policy.
The Encypher signing integration becomes a serve middleware in
crates/provenance/encypher, selected by [provenance] module = "encypher"
and placed by a [[serve]] entry. Its preparation applies the request gates
with no I/O, and its stream processor signs the article in the
asynchronous finish core now awaits. The worker token is a module secret
setting. The enabled flag and path_prefixes give way to the selection and
the entry's path, and the module is offered on every adapter and the CLI.
…arrow the module's data claims

Adds a regression test that the parser-confirmed auction hold injects bids
into a document a middleware processor held and finished asynchronously.
States that the module skips pages the origin marks private but cannot
detect other personalized pages, so a deployment places it on public
article routes. Applies rustfmt and the docs formatter.

This branch has not been deployed

No deployments
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