oficina / home Public
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 issuedat equal to the rotation time and expiresat 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.