Module: Media¶
Optional. Everything in this chapter binds only an implementation that advertises the module IRI
https://schema.sempods.org/module/media at the conformance endpoint
(SPS-CORE-005). A pod holds RDF; only a deployment that means to
hold binaries provides this.
Status: descriptive. See ../../GOVERNANCE.md.
Profiles: RFC 9110, RFC 7232, RFC 4648 §5. Error codes are ../core/index.md §5;
authorization is ../core/grants.md.
1. The model¶
SPS-MEDIA-001 — This module holds bytes and a content type. An implementation MUST NOT
introduce image knowledge, variants, resizing or transformation. Rendering is the application's
business, done before upload.
SPS-MEDIA-002 — Where the module is not provided, the media routes MUST NOT exist. An
implementation MUST NOT answer them with an error that implies they are temporarily unavailable.
SPS-MEDIA-003 — A media identifier MUST be the SHA-256 of the bytes, encoded base64url without
padding.
SPS-MEDIA-004 — Content addressing MUST deduplicate within a pod and MUST NOT deduplicate
across pods.
Sharing one stored object between two pods makes one pod's deletion depend on the other's, which is a tenancy boundary crossed for a storage saving.
SPS-MEDIA-005 — An upload MUST NOT write RDF. It returns a media URL; whoever wants a
schema:ImageObject writes one themselves.
The registry and the graph do not know about each other, so there is nothing to keep in step and nothing that can drift. The cost is stated in §6.
2. Routes¶
POST {pod}/_system/media?context=<ctx> upload
GET {pod}/_system/media/{id} metadata
HEAD {pod}/_system/media/{id}
GET {pod}/_system/media/{id}/content the bytes
HEAD {pod}/_system/media/{id}/content
PUT {pod}/_system/media/{id}?context=<ctx> assign to a further context
DELETE {pod}/_system/media/{id}?context=<ctx> drop one assignment
SPS-MEDIA-026 — On a raw upload, the request's Content-Type header is the media's declared
type. An implementation MUST record it, MUST return it as the assignment's content type, and MUST
NOT infer it from the bytes.
SPS-MEDIA-028 — Where a raw upload declares no type, the implementation MUST record
application/octet-stream. It MUST NOT reject the upload, and MUST NOT choose a type of its own.
Delivery depends on the declared type — SPS-MEDIA-017 decides inline against attachment from it —
so an implementation with nowhere to carry one would have to sniff, and sniffing is exactly what
SPS-MEDIA-016 disables on the way out. application/octet-stream is the honest answer and the
allowlist serves it as an attachment.
SPS-MEDIA-029 — An implementation providing this module MUST serve exactly these addresses,
with these verbs:
| Route | Verbs |
|---|---|
{pod}/_system/media |
POST — upload |
{pod}/_system/media/{id} |
GET, HEAD — metadata · PUT — assign · DELETE — unassign |
{pod}/_system/media/{id}/content |
GET, HEAD — the bytes |
Without this the requirements below would constrain each verb once implemented while leaving an
implementation free to put it somewhere else — and SPS-MEDIA-013 fixes the content address, which
only works if the others are fixed too.
SPS-MEDIA-006 — POST, PUT and DELETE MUST each resolve exactly one write context, with
the rules of SPS-CRUD-007.
Media is control-plane state that is nonetheless context-bound. It is the one part of the control
plane that takes ?context=.
SPS-MEDIA-007 — PUT MUST be idempotent, and DELETE MUST be ensure-absent: dropping an
assignment that is not there MUST succeed.
SPS-MEDIA-008 — Assigning a media to a further context MUST require both write on the
target context and read on a context the media is already assigned to.
Write alone would let anyone attach an arbitrary media identifier to a context they control — and since the identifier is a content hash, a caller who has the file elsewhere could confirm the pod holds it, and then read it.
3. Authorization¶
SPS-MEDIA-009 — A media MUST be readable exactly when its assignment set intersects the
caller's readable contexts. An implementation MUST NOT add media-specific authorization.
Anonymous access, public-read, the manage cascade, revocation and the context-deletion cascade all
follow from that one sentence, with no second model to keep aligned.
SPS-MEDIA-010 — A media the caller may not read MUST answer 404, and an identifier that does
not exist MUST answer the same 404.
SPS-MEDIA-011 — POST MUST always answer 201, never 200, including where the bytes were
already stored.
SPS-MEDIA-012 — Metadata MUST list only the assignments the caller may read.
These three are one rule seen from three sides. The identifier is the digest of the content, so any observable difference between "stored" and "not stored" answers the question does this pod hold exactly this file — for a caller who already has the file and only wants to know who else does.
4. Delivery¶
SPS-MEDIA-027 — Where the same bytes carry different declared types across several
assignments, the type used for a content response MUST be chosen deterministically from the
assignments the caller may read — by the lowest context IRI among them.
Content addressing deduplicates by bytes, and the declared type hangs off the assignment, so the
same media identifier can legitimately carry image/png in one context and
application/octet-stream in another. The content route names no context, so without a rule two
implementations answer differently and SPS-MEDIA-017 picks a different disposition from each.
SPS-MEDIA-013 — The content URL MUST always be {pod}/_system/media/{id}/content.
This is the load-bearing property of the module. A deployment that later serves bytes from a CDN or
a signed storage URL changes what the server answers there, never what the stored data says — so
it is an optimisation rather than a migration of everyone's schema:contentUrl values.
SPS-MEDIA-014 — The entity tag on a content response MUST be derived from the content hash and
from the declared type of the assignment the response was built from
(SPS-MEDIA-027). If-None-Match MUST be honoured.
It is a strong validator by construction rather than by convention — the identifier already is the
digest of the bytes. The type has to be in it because two callers with different read access may
receive different declared types, and so different representations, at the same URL. A tag over the
bytes alone would let a conditional request or a cache carry one caller's representation to the
other, which is also why SPS-MEDIA-015 requires the response to be private and to vary on the
credential.
SPS-MEDIA-015 — Metadata and content responses MUST be marked private and MUST vary on the
credential. An implementation MUST NOT permit a shared cache to store them.
The answer changes per caller and, for the same caller, over time — revocation is promised to be
immediate (SPS-GRANT-003), and a shared cache would outlive it.
SPS-MEDIA-016 — Every content response MUST carry X-Content-Type-Options: nosniff and a
content security policy that sandboxes the response.
SPS-MEDIA-017 — Content-Disposition MUST be decided by an allowlist: inline for a small
set of known-safe image types, attachment for everything else, including unknown types.
SPS-MEDIA-018 — image/svg+xml and text/html MUST always be attachment.
SVG carries script. Served inline from the pod's own origin, an uploaded SVG is same-origin code with access to everything that origin can reach — and it arrives looking like an image, which is why a denylist of "dangerous types" is the wrong shape here and an allowlist is the right one.
5. Fetching a source¶
SPS-MEDIA-019 — An implementation MAY accept an upload that names a source URL for the server
to fetch. Where it does, all of the following MUST hold:
- the scheme is checked against an allowlist;
- every address the host resolves to is validated, not just the first;
- the validated address is pinned for the connection that follows;
- the whole chain is re-run for every redirect hop;
- a size cap is enforced while streaming rather than after.
SPS-MEDIA-030 — Where the server fetched the bytes, the declared type MUST be the
Content-Type the fetched response carried. Where it carried none, or none that parses, the
implementation MUST record application/octet-stream. It MUST NOT infer a type from the bytes or
from a filename.
SPS-MEDIA-026 answers this for a raw upload and said nothing about the fetched path, which leaves
the same media reachable under a different Content-Type, a different ETag
(SPS-MEDIA-014) and a different disposition depending on which route stored it.
Inferring from the filename is the tempting alternative and the worst one: the filename comes from
the same untrusted source as the bytes.
SPS-MEDIA-020 — The address policy MUST be derived from the IANA special-purpose address
registries' not globally reachable column, not from a hand-written list of private ranges.
Every element of SPS-MEDIA-019 closes a specific bypass, and leaving one out defeats the rest.
Validating one address of several lets a host with two records pass on the public one and connect on
the private one; not pinning it lets the name resolve differently between check and connect; not
re-running per hop lets a public URL redirect inward. A hand-written range list is how link-local
metadata endpoints keep being reachable.
6. Lifecycle¶
SPS-MEDIA-021 — Deleting a context MUST remove it from every media assignment.
SPS-MEDIA-022 — A media whose assignment set becomes empty MUST be marked unreferenced from
that moment, and the mark MUST be cleared if it is assigned again.
SPS-MEDIA-023 — Collection MUST NOT be immediate. An implementation MUST observe a grace
period after a media becomes unreferenced before removing its bytes.
SPS-MEDIA-024 — Collection MUST delete the stored object before the registry entry.
The reason is retryability, not the harmlessness of the interrupted state — and getting that backwards is easy, because the interrupted state of this order looks like the worse one.
Interrupt it and an entry survives whose object is gone. That entry is still marked unreferenced and still past the grace period, so the next collection run finds it, deletes an object that is already gone, and completes. The damage is bounded by one sweep interval and repairs itself.
Delete the entry first and the interruption leaves an object nothing points at — and nothing will
ever retry, because the entry that drove the sweep is what went first. That is a permanent leak, and
only reconciliation can even find it, which SPS-MEDIA-025 requires to report
rather than repair.
SPS-MEDIA-025 — A reconciliation facility, where offered, MUST report divergence and MUST NOT
repair it.
Automatic repair means a route that deletes bytes on the strength of a query. The remedy is an operator's: delete the stray object, or restore the missing one from a backup.
7. Named limitations¶
Stated so they are not discovered:
- No range requests. Nothing here requires them, and the reference implementation does not advertise them.
- Two check-then-act races survive. The media registry, the context registry and the byte store
are separate systems with no shared transaction. An upload can complete against bytes a
concurrent collection has just removed; a context can be deleted between authorizing an upload and
writing its assignment. The second is narrowed by re-checking at the point of decision and
answering
409(SPS-CORE-014), and the residual gap is one statement wide. - Nothing reconciles the graph against the registry. Deleting a
schema:ImageObjectthrough the CRUD surface does not drop the assignment, and the bytes stay. That is the price ofSPS-MEDIA-005, and an application owning both sides handles it. - Checksums are not re-verified. An upload is covered by construction — the identifier is the digest — but nothing re-reads the bytes afterwards. Corruption at rest is the store's and the backup's problem.
A deployment that provides this module takes on one obligation this specification cannot discharge: the store holds the only copy of every byte, and nothing in a pod can restore one.