Skip to main content

Stream ledger

StreamSdk is stateless across calls. State for each stream lives in a VerifierLedger you provide, keyed by streamId. It caches the init manifest and session keys so subsequent media segments validate without re-reading the init.

The VerifierLedger interface

interface VerifierLedger {
put(id: string, bytes: Uint8Array): Promise<void>;
get(id: string): Promise<Uint8Array | null | undefined>;
forget(id: string): Promise<void>;
}

The interface has three methods and stores opaque bytes. Do not inspect or construct the bytes yourself; the SDK owns their format. Your only decision is where they live.

Why the ledger isn't built in

The verifier carries opaque per-stream state because C2PA 2.3 §19 requires the validator to remember information from the init manifest to validate subsequent media segments. The SDK delegates where that state lives for three reasons:

  1. The right backing store depends on the viewer's lifecycle, which only the integrator knows. See Choosing a backing store below.
  2. Atomicity belongs to the platform already in use. The contract is per-streamId linearizable read-modify-write so two segment validations cannot race on the same id. Browser platforms provide the primitives natively. A network-hosted ledger would defeat the offline-verification stance.
  3. A Limbo-hosted ledger would leak viewer playback. The ledger observes every streamId a viewer touches. Keeping it on the client preserves the privacy property that makes browser-side verification interesting in the first place.

Choosing a backing store

Match the ledger's lifetime to the viewer's. If the stream lives and dies with the player, an in-memory store is sufficient. If validation should survive a page refresh, the store must persist. If multiple tabs on the same origin can play the same streamId, the store must serialize concurrent writers. In every case it must satisfy the atomicity rule below; use a store that already provides it rather than inventing one.

in-memory-ledger.ts
import { type VerifierLedger } from '@limboai/verifox-sdk-verifier';

function createInMemoryLedger(): VerifierLedger {
const store = new Map<string, Uint8Array>();
return {
async put(id, bytes) { store.set(id, bytes); },
async get(id) { return store.get(id) ?? null; },
async forget(id) { store.delete(id); },
};
}

The atomicity rule

Atomicity contract

For any single streamId, a get followed by a put must complete without another get/put for the same id occurring in between. Different stream ids are independent: you can validate many streams concurrently in the same page, provided no two callers contend for the same id.

The SDK reads the cached state, folds in the new segment, and writes the result back on every validate call. Without per-streamId linearizable read-modify-write, two overlapping validations for the same id can read the same prior state and clobber each other's write. Issue same-id validate calls in arrival order, one at a time, backed by a store that serializes writers per id.

See also