Skip to content

Authorization

Draft

Rule: every lifecycle transition is an asset manifest signed under the album’s per-epoch write-tier key — the same proof as a write. There is no weaker path to destroy data than to add it.

Clients reject anything verify_asset does not accept.

action is a closed enum; any other value is a structural error (Schema Rules).

ActionMeaning
createFirst write; prior_provenance_hash is null
replaceNew original bytes (e.g. re-encryption), same file_id/album_id
deleteSoft-delete into trash for a retention window
metadata-updateEdit the encrypted metadata blob or sidecar fields
derivative-addAdd a derivative
derivative-replaceReplace a derivative — the only path; silent overwrite is rejected
trash-restoreRecover from trash within the window

Adding a value requires a new protocol_version; albums stay pinned to their original set.

A client accepts an operation only if:

  • it is signed with the album’s write-tier key for its epoch, and carries a valid hybrid device_sig, both checked in verify_asset;
  • its prior_provenance_hash equals the current chain head (every action but create); stale or forked positions are rejected.

Every non-upload lifecycle write goes through one endpoint, POST /v1/albums/{album_id}/ops, carrying the signed manifest (plus metadata blob if any). Writes that move blob bytes (create, replace, derivatives with new bytes) use the upload protocol.

  • The structural envelope (server invariants 16–18, 25) runs before any row is written. New actions never add endpoints.
  • Idempotent by content hash: a replayed accepted manifest returns the identical response and writes nothing.
  • Provenance append and per-album sync_seq mint are one transaction — visible on the sync feed exactly when durable.
  • Rejections carry an error.* code, write nothing, and are logged.

The server cannot read the MLS roster. It holds the owner’s signed roster (PUT /v1/albums/{album_id}/roster, invariant 33): members with role reader/writer, a strictly increasing roster_version (at most +16 over the held one, so one publish cannot latch the counter), and the AMK epoch, signed by a non-revoked owner device. Only the owner publishes; removal is a later roster omitting the member, recorded with the version and epoch, not deleted.

CallerServer behaviour
Current writerMay upload and post ops; filed under the owner’s namespace, billed to the uploader
Current member (any role)May fetch blobs
Former member403 (Transfer — Download)
Never on roster404, as for an unknown address

The roster is a transport control, not confidentiality. Removing a writer reclaims nothing: their uploads stay, still billed to them, deletable only by the owner or a current writer (Quota — Scope Decisions).

The server carries out deletes and replaces but never permits them. Without keys it still enforces the structural envelope before persisting.

  • delete moves the asset to trash; the blob is hard-purged only after the retention window.
  • The window is signed into the delete manifest, so a server cannot shorten or extend it (Recycling).
  • trash-restore before expiry is itself an audited manifest.

Each operation appends a provenance record; the chain is append-only.

Federated operations pass the same verify_asset and envelope checks; peer ordering and timestamps are never trusted. Stale revival is caught by the chain check and quarantined.

  • Per action: correct keys verify; wrong write-tier key, device, or prior_provenance_hash is rejected; unknown action fails the envelope; a stale chain position is quarantined.
  • Invariants 16–18 against real Postgres; full lifecycle E2E in Module Map.