content-credentials-view
v0.2.0
Published
React components for displaying and validating C2PA Content Credentials
Maintainers
Readme
content-credentials-view
React components for C2PA Content Credentials.
Read the provenance record attached to a file and render it so a person can understand what it actually says — who signed it, what happened to it since, and whether that signature still holds.
Early development. The API is not stable before 1.0, and minor versions may break it.
Built against @contentauth/c2pa-web,
the current Content Authenticity Initiative SDK.
Install
pnpm add content-credentials-view @contentauth/c2pa-web reactreact and @contentauth/c2pa-web are peer dependencies, so you own both
versions. Copy the WASM binary somewhere you serve, once, at install time; this
is the path you pass as wasmSrc below:
cp node_modules/@contentauth/c2pa-web/dist/resources/c2pa_bg.wasm public/c2pa.wasmTen lines that work
import { C2paProvider, ContentCredentials } from 'content-credentials-view';
import 'content-credentials-view/styles.css';
// Hoisted, not inline: this object is compared by value, and a new one on every
// render would discard the worker and the WASM instance.
const ANCHORS = { trustAnchors: '/anchors.pem' };
export function Photo({ file }: { file: File }) {
return (
<C2paProvider wasmSrc="/c2pa.wasm" trustAnchors={ANCHORS}>
<ContentCredentials source={file} />
</C2paProvider>
);
}source takes a File, a Blob, a URL string, or null to read nothing yet.
Pass trustAnchors={null} to opt out of trust checking explicitly — everything
still renders, and the status says the signer was not checked.
The stylesheet is optional. Without it the markup is semantic and readable, and
every part carries a data-cc-part attribute to style against.
What this is, and what it is not
This renders C2PA manifest data. It does not verify that content is true. A valid manifest tells you an asset was signed by someone and has not changed since. It does not tell you the content is accurate. A camera can take a cryptographically perfect photograph of a screen showing a fake.
No manifest is the normal state of the internet. Fewer than 1% of published news images carry one, and that is among the most motivated publishers. Metadata is also stripped routinely by image pipelines, CDNs, and social uploads. This library never renders "no credentials" as a warning, and neither should you.
This cannot detect AI-generated images. Nothing reliably can. If that is what you need, this is the wrong tool.
A verdict here is this library's reading, not the verdict. What you see is
@contentauth/c2pa-web's validation of the file, interpreted against the trust
anchors you configured. Another C2PA tool — a different SDK version, a different
trust list, a different reading of the specification — can and does reach a
different conclusion about the same file. An independent security analysis in
April 2026 found conforming validators returning contradictory verdicts on
identical assets (arXiv:2604.24890v1,
23 April 2026, examining specification versions 2.2–2.4). Treat a status as one
tool's answer, not a fact about the file.
Trust anchors are your responsibility
trustAnchors is a required prop and this library ships no default. A bundled
trust list would mean the library decides who you trust, which is not a decision
a dependency should make for you.
Without anchors, everything still renders, but the status is
trust-not-configured and the interface says so. That state means a signature
is present and intact, not this signer is trustworthy.
This matters more than it sounds. C2PA certificates are paid, there is no free issuer equivalent to Let's Encrypt, and few certificate authorities are on the official trust list. An untrusted signer is the common case, not an alarm — and it is genuinely different from a file that was tampered with. Telling those two apart clearly is the main reason this library exists.
Provider props
Wrap the subtree that inspects assets in one C2paProvider. It creates a single
SDK instance — one web worker, one WASM instance — lazily, on the first read.
| Prop | Required | What it is |
| -------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| wasmSrc | yes | URL to fetch the WASM binary from, or an already-instantiated WebAssembly.Module. You host it. |
| trustAnchors | yes | What to check signers against, or null to opt out explicitly. No default, for the reason above. |
| workerSrc | no | URL of a self-hosted worker script, for a strict Content Security Policy that disallows blob: workers. Without it the SDK builds its worker from a blob: URL. |
| settings | no | Further SDK settings, passed through untouched. trustAnchors is merged into settings.trust and wins over any trust keys set there. |
Memoize trustAnchors and settings. Both are compared by value rather than
by identity, so an inline object literal does not rebuild the worker on every
render. That comparison serializes the object, which means key order counts:
{ userAnchors, trustAnchors } and { trustAnchors, userAnchors } hold the same
anchors but read as a change, and a change discards the worker and the WASM
instance and builds new ones. Hoist both objects to module scope, or wrap them in
useMemo, and the question never arises. workerSrc is compared by its href,
so an inline new URL(...) is safe.
Each provider owns its own instance. Two providers in one tree create two workers even with identical props, and a nested provider shadows the one above it for its own subtree — a page-level provider plus a widget-level one costs two of everything. Mount a second one only when you mean to.
Limitations
- Browser only. The underlying SDK assumes a browser and a
Blob(upstream #147). There is no server-side path: no SSR, no React Server Components, no Next.js middleware. Components are client-only. - The WASM binary must be served by you or loaded from a CDN. Self-hosting instructions and a build-time checksum are in the docs.
- Passing a URL instead of a
Blobmeans the fetch is subject to CORS. - Revocation is not checked, and
revokedalmost never appears. The C2PA specification makes revocation checking optional, permits it only over OCSP, and forbids certificate revocation lists;@contentauth/c2pa-web0.13.4 performs no revocation check by default, and the browser cannot reach an OCSP responder anyway. This library reportsrevokedonly when the file itself carries evidence of revocation. A file that does not reportrevokedhas not been checked — it has not been shown to be unrevoked. Verified against the SDK on 2026-08-14; see what the signature covers. - The signing time is not covered by the signature and can be replaced without breaking it. Every displayed time carries a note saying so. Same page.
- Parts of a file can be excluded from the hash by the signer. This library renders nothing from the asset body, so nothing it shows can come from an excluded region — but that is a property of what it renders, not a check it performs. Same page.
Versioning
Tracks @contentauth/c2pa-web ^0.13.
The upstream SDK is pre-1.0 and moves quickly — 0.5 to 0.13 in under six months
across 42 published versions. Upstream breaking changes will produce a minor
version bump here until the SDK reaches 1.0. Tested version pairs are listed in
COMPATIBILITY.md.
Privacy
Nothing leaves the browser. No telemetry, no analytics, no error reporting, no remote fonts. The only network requests are the WASM binary you configure and any URL you pass as a source.
Maintenance
Solo maintainer. Issues triaged weekly. Security reports answered within 48 hours. If I stop maintaining this, I will publish a deprecation notice pointing at a fork rather than let it rot silently.
License
MIT, matching c2pa-js and c2pa-rs.
API reference
Everything below is exported from content-credentials-view. Everything that
does not import React is also exported from content-credentials-view/core, for
using the read and interpretation logic without the components.
Components
| Export | Props | What it renders |
| -------------------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| C2paProvider | wasmSrc, trustAnchors, workerSrc?, settings? | Nothing of its own. Owns one SDK instance for its subtree. See Provider props. |
| ContentCredentials | source, className?, labels?, children? | The whole thing: summary, claim fields, and provenance chain. |
| ValidationSummary | interpretation, className?, labels?, children? | Headline, detail, learn-more link, and the raw codes behind a disclosure. |
| ClaimSummary | manifest, className?, labels?, children? | Signer, issuer, signing time, generator, and assertion labels as a definition list. |
| ProvenanceChain | tree, className?, labels?, children? | The ingredient tree, as an ARIA tree with arrow-key navigation. |
| Disclosure | label, part, children, defaultOpen? | A <button aria-expanded> and its panel. |
Every component takes children as a render prop that replaces the contents
while keeping the wrapper, its class, and its data-cc-* attributes.
Hooks
| Export | Signature | Returns |
| ----------------------- | ------------------------------------------ | ----------------------------------------------------------- |
| useContentCredentials | (source: CredentialSource) => CredentialsState | The read, discriminated on status. |
| useC2pa | () => C2paContextValue | { load, error }. Throws outside a C2paProvider. |
CredentialsState is { status: 'idle' } | { status: 'loading' } | SettledCredentials.
Narrowing away the first two gives you interpretation, activeManifest,
tree, raw, and error with nothing left to null-check.
Reading and interpreting
| Export | Signature | What it does |
| --------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------- |
| readCredentials | (sdk, blob, { trustAnchors, signal? }) => Promise<CredentialsResult> | One read. Frees its reader on every path, including abort. |
| interpret | (validation, options) => Interpretation | Codes to a status, a severity, and words. |
| buildProvenanceTree | (store) => ProvenanceNode | The ingredient tree, with cycles and depth-capping marked rather than cut. |
| isAbortError | (error) => boolean | Whether a caught value is an abort rather than a read failure. |
InterpretOptions takes trustConfigured (required), and optionally status,
signer, format, supportedFormats, error, and messages.
Status
| Export | What it is |
| ---------------------------------------------------------- | ------------------------------------------------------------------------ |
| CREDENTIAL_STATUSES, STATUS_PRECEDENCE | Every status, and the order they resolve in. |
| isError, isFailure, isPending, isPresent | Status predicates. none is not a failure and is not in isError. |
| MAX_PROVENANCE_DEPTH | Where the ingredient walk stops. |
| SignedAtAttestation, timestampAttestation | What a validator said about a signing time's timestamp, and how it is derived. |
Precedence, highest first:
tampered → revoked → expired → unsupported → untrusted-signer → trust-not-configured → trusted → none.
revoked is in that list but is, in practice, unreachable — the SDK performs no
revocation check. See Limitations.
Copy
| Export | What it is |
| --------------------- | ---------------------------------------------------------------------------------------- |
| CREDENTIAL_MESSAGES | Every status string. Clone and spread it to translate or re-voice. |
| COMPONENT_LABELS | Field names, disclosure labels, provenance notes, and the signing-time notes. Same idea. |
| SUPPORTED_FORMATS | The formats the reader handles, grouped for a person rather than as MIME types. |
Foreign values
Signer names, formats, assertion labels, codes, and error messages all come out
of the file. They are normalized, length-capped, and fenced in quotes and bidi
isolates before they reach any sentence this library wrote, so a signer called
Reuters — verified original cannot borrow our voice.
| Export | What it is |
| ------------------------------------- | ---------------------------------------------------------------------------- |
| foreign(value, cap, style?) | Normalize, cap, and fence one value. style is 'quoted' or 'plain'. |
| foreignRuns(text) | Split a message into our words and the file's. |
| withForeign(text) | The same split as React nodes, each foreign run in its own <span>. |
| withoutFencing(text) | Drop the isolates, keep the quotes. For plain-text output. |
| FOREIGN_CAPS | The length caps, with the standard each is derived from. |
| FOREIGN_OPEN, FOREIGN_CLOSE | The isolate characters, if you are splitting the string yourself. |
Styling hooks
Every element carries data-cc-part. The root and the summary also carry
data-cc-status and data-cc-severity; chain nodes carry data-cc-node and
data-cc-depth; foreign runs carry data-cc-part="foreign". Style against
those rather than against the class names, which are not part of the API.
Documentation
- Changelog — 0.2.0 is a breaking change, and corrects what earlier versions said about revocation.
- How do I display Content Credentials in React?
- What does a C2PA signature actually cover?
- Why does a valid certificate show as untrusted?
- What does it mean when a file has no Content Credentials?
- Can I use this to detect AI-generated images?
- Avoiding WASM memory leaks with c2pa-web in React
- Using this with Next.js
Not affiliated with or endorsed by the C2PA or the Content Authenticity Initiative. For the official tooling see c2pa-js, the specification, and Content Credentials Verify.
