# Roster producer storage integration contract

Scope: discussion 33, implementing the approved roster contract in discussion 28.
This branch adds no mounted schema, route, operator command or production data.

`directorydata.RosterSchema` follows `directorydata.Schema` in a future directory
schema merge. It creates empty generation, consumer, token and receipt tables.
`NewRosterSource(db, sealer, directoryConfig, directoryOrigin)` copies the existing
application and route policy. `Read(ctx, RosterRequest, now)` returns the shared
`roster.Page`, HTTP status and a fixed error. A successful 409 has a nil error and
no member fields. The HTTP caller must sign both 200 and 409 with shared
`roster.SignPage`; no raw Page is a trusted wire response.

The HTTP integration still owns exact Host enforcement, method/query/header
shape and length checks, no-store on every response, fixed response bodies,
no request-URL logging, and roster-only signing keys. `Read` has no outbound
transport. All scope comes from the token's consumer and its matching live
registry row. A requested team only compares against that scope.

## Future audited writer transaction

There is deliberately no public mutation API. The following are preconditions
for the root-owned operator integration, not authority conferred by these tables.
Before opening its write transaction, authenticate and authorize the operation
using the separately reviewed operator authority. Verify the operation ID,
monotonic record sequence within its consumer scope, exact directory, consumer, team, app, audience and
expected current generation. Refuse replay, changed payload under an old ID,
and unknown authority. Never accept browser identity or bearer possession as
operator authority. Recheck expected stored state under the transaction lock.

Within the same transaction, write the intended registry/consumer/token or
generation change and its successful receipt. The receipt retains only opaque
operation ID, sequence, fixed operation kind, optional consumer ID, generation,
SHA256 record digest and completion time. It is not a signed provisioning record
and cannot approve a client generation. The operator path must deliver the
separately signed record through the approved transport. No token or team name
belongs in the receipt. Name sealing remains the existing ID-bound helper.

Token slots are normalized rows with a globally unique SHA256 hash of the exact
canonical 43-character bearer string. This prevents cross-consumer and cross-slot
collisions. To rotate, delete an older previous slot, change the current row to
previous with issued_at equal to the rotation time and expires_at at most 300
seconds later, and insert the new current hash. Here issued_at starts the slot's
validity window; it is not the original token creation timestamp. Current slots
have no expiry. Future-dated slots fail. Disable by setting enabled=0 in the
same audited transaction; no read changes slots or extends an overlap.

Ordinary boot never fills generation. Bootstrap needs the independently approved
fresh-install path. Restore needs the platform's fenced maintenance path and a
new random generation plus newly approved records before traffic resumes.
Directory storage alone does not supply either path. Generation and receipt
values must be canonical and exact when written; the reader also rejects a
malformed generation. Consumers bind to a matching registry consumer/team/app/
origin and the injected app/route policy; pending or disabled instances do not
serve rosters.

Membership INSERT, UPDATE and DELETE triggers advance affected teams' epochs in
the writing transaction and abort on signed 64-bit overflow. The future writer
must not add a second manual increment. Moving membership bumps both teams once;
changing a role bumps one. Even a no-op UPDATE bumps its team. Do not lower an
epoch or bypass triggers. Restore is the separate generation-fenced procedure.

## Snapshot and cursor

One read transaction resolves the token, enabled/overlap state, live binding,
generation, epoch, count and ordered page. It does not keep a server snapshot
between requests. A membership write after that transaction begins may appear
on the next call; the consumer's standing lease accounts for this bound.

The cursor is sealed with purpose `directory.roster-cursor/<directory origin>`.
Its closed JSON binds consumer, team, generation, epoch, fetch challenge,
last opaque subject, next index, issuance and expiry. It expires at ten seconds,
including at the exact boundary. Wrong scope/challenge, malformed, expired or
unauthentic cursors return fixed 400. A scoped valid cursor with changed
generation/epoch returns the signed 409 shape with current values. Rotation and
disable are checked again before cursor processing. No cursor grants authority.
