Content State API — contract¶
The post-implementation contract for iiiris's IIIF Content State API 1.0
support (roadmap Phase 2). Operator/consumer how-to lives in
../content-state.md; this file is the durable "what
this subsystem guarantees" a re-implementation must satisfy.
Reference: IIIF Content State API 1.0 — https://iiif.io/api/content-state/1.0/
What it does¶
A Content State encodes a specific view — a Manifest, a Canvas, or a region of a Canvas — as a portable, shareable payload. iiiris:
- makes its bundled viewers read the
iiif-contentparameter, so a/view/?iiif-content=<state>link opens the encoded view; - offers mint / resolve helpers over its own Presentation resources;
- optionally hosts content states at dereferenceable URIs (the Form-B bookmark store), off by default.
The stateless core is on by default (part of the zero-config path); the store adds state and ships off.
Surface¶
HTTP routes¶
| Method | Path | Purpose |
|---|---|---|
| GET | /view/?iiif-content=<state> |
Bundled viewer initialised from a content state (bare form; /view/{id}?iiif-content= also accepted) |
| GET | /content-state/mint?target=<id>[&canvas=N][&xywh=x,y,w,h][&v=3] |
Build a state from one of our Presentation identifiers → JSON + encoded value + /view URL |
| GET, POST | /content-state/resolve |
Decode/inspect a state (GET ?iiif-content=; POST raw JSON body) |
| POST | /content-state |
Store a state → 201 + Location + minted URI (store only) |
| GET | /content-state/s/{id} |
Dereference a stored state as JSON-LD (store only) |
The /content-state/… helpers sit outside /iiif/ — they are iiiris
conveniences, not standard Content State server endpoints (the spec defines
none). Verbs (mint, resolve) are at fixed sub-paths; stored ids live under
/content-state/s/{id} so they never collide with the verbs.
Config¶
content_state:
enabled: true # stateless core; default true (zero-config path)
store:
enabled: false # bookmark store; default false
dir: "" # durable filesystem dir (required when store.enabled)
write_token: "" # POST Bearer token; empty ⇒ writes refused
content_state.enabled → Deps.ContentStateEnabled; the store →
Deps.ContentStateStore + Deps.ContentStateWriteToken.
Public Go API — internal/contentstate¶
Pure, I/O-free (no govips, no network):
- Codec:
Encode(json []byte) string,Decode(param string) ([]byte, error). - Classify / build / validate:
Parse(body) (State, error),Build(Target) ([]byte, error),Validate(body) ([]Problem, error). - Types:
State,Form,Target,Ref,Problem;ContextURI,Motivation.
Public Go API — internal/annostore¶
Durable, non-evicting store: Store interface (Put/Get), Filesystem
implementation, ErrNotFound.
Contracts¶
- Content-state-encoding.
Encode/Decodeimplement theiiif-contentGET codec:base64url(encodeURIComponent(json))with=padding stripped (RFC 4648 §5).encodeURIComponentis the ECMAScript function, notnet/urlescaping. Round-trip (Decode(Encode(x)) == x) is the core invariant and the CI conformance gate. URI-form states are never encoded. - Parse-only — no server-side fetch (SSRF guard).
Parse, the/viewdecode, andresolveextract ids from a state and never dereference the target. A content state routinely targets an external manifest; server-side fetching it would be a request-forgery vector. - Stateless core default-on, zero-config-safe. Nothing under
content_state:is required for the default path.content_state.enabled: falseunmounts the/content-state/helpers and stops/viewreadingiiif-content(behaviour reverts to pre-Content-State). - Both viewers honour
iiif-contentvia server-side parse. The/viewhandler decodes+parses the state (never fetches) and feeds each viewer: the Mirador window gets the resolvedcanvasId; Universal Viewer receives the state on its hash (#?iiif-content=). The bare/view/?iiif-content=is the canonical entry — a content state carries its ownpartOfmanifest, so no path id is needed; when both are present the state wins. This is additive:/view/with no id and noiiif-contentstill 404s. - Mint is iiiris-hosted targets only.
mintreflects one of our Presentation identifiers into a state; an absolute-URLtargetis rejected. Canvas ids use the single-sourcedpresentation.CanvasIDgrammar. v1 selector vocabulary: whole Manifest, Canvas, andxywhregion. - Untrusted input is byte-bounded. Decoded GET params and POST bodies (to
resolveand the store) are parsed under an explicit cap and rejected past it. - Store: durable, non-evicting, write-gated. When enabled the store hosts
states at
/content-state/s/{id}permanently (it never evicts to reclaim space;internal/annostorefilesystem backend, atomic writes, sanitised ids).POST /content-staterequires thewrite_tokenas anAuthorization: Bearercredential (constant-time compare); with no token configured, writes are refused — an enabled store is never an open relay. The POST body must parse as a content state.GET /content-state/s/{id}serves the stored bytes viaserveBytes(wildcard CORS +ETag+ conditional-GET) with content typeapplication/ld+json;profile="…/presentation/3/context.json"; reads are public. - Purity.
internal/contentstateperforms no I/O and imports neithergovipsnor the network; the server layer owns all HTTP, rendering, and storage.
Out of scope¶
- Time (
#t=) selectors and multi-target comparison inmint— parsed by the library but not produced by mint yet. - Multi-replica store backends (Redis/S3) — the store is single-node filesystem.
- Cross-version conversion / Presentation 2.1 states — mint and the store emit the Presentation 3.0 context.
- Auth-gated content states — resolution defers to the existing IIIF Auth flow; no special-casing.
- Client-side transports (paste, drag-drop, file upload,
data-iiif-content) — those live in consuming clients, not a server; iiiris honoursiiif-contentbecause its bundled viewer is a client. - Write tools beyond the store — annotation authoring is roadmap Phase 3.