Skip to content

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-content parameter, 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.enabledDeps.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/Decode implement the iiif-content GET codec: base64url(encodeURIComponent(json)) with = padding stripped (RFC 4648 §5). encodeURIComponent is the ECMAScript function, not net/url escaping. 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 /view decode, and resolve extract 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: false unmounts the /content-state/ helpers and stops /view reading iiif-content (behaviour reverts to pre-Content-State).
  • Both viewers honour iiif-content via server-side parse. The /view handler decodes+parses the state (never fetches) and feeds each viewer: the Mirador window gets the resolved canvasId; Universal Viewer receives the state on its hash (#?iiif-content=). The bare /view/?iiif-content= is the canonical entry — a content state carries its own partOf manifest, so no path id is needed; when both are present the state wins. This is additive: /view/ with no id and no iiif-content still 404s.
  • Mint is iiiris-hosted targets only. mint reflects one of our Presentation identifiers into a state; an absolute-URL target is rejected. Canvas ids use the single-sourced presentation.CanvasID grammar. v1 selector vocabulary: whole Manifest, Canvas, and xywh region.
  • Untrusted input is byte-bounded. Decoded GET params and POST bodies (to resolve and 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/annostore filesystem backend, atomic writes, sanitised ids). POST /content-state requires the write_token as an Authorization: Bearer credential (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 via serveBytes (wildcard CORS + ETag + conditional-GET) with content type application/ld+json;profile="…/presentation/3/context.json"; reads are public.
  • Purity. internal/contentstate performs no I/O and imports neither govips nor the network; the server layer owns all HTTP, rendering, and storage.

Out of scope

  • Time (#t=) selectors and multi-target comparison in mint — 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 honours iiif-content because its bundled viewer is a client.
  • Write tools beyond the store — annotation authoring is roadmap Phase 3.