| 1 | # Roster producer storage integration contract |
| 2 | |
| 3 | Scope: discussion 33, implementing the approved roster contract in discussion 28. |
| 4 | This branch adds no mounted schema, route, operator command or production data. |
| 5 | |
| 6 | `directorydata.RosterSchema` follows `directorydata.Schema` in a future directory |
| 7 | schema merge. It creates empty generation, consumer, token and receipt tables. |
| 8 | `NewRosterSource(db, sealer, directoryConfig, directoryOrigin)` copies the existing |
| 9 | application and route policy. `Read(ctx, RosterRequest, now)` returns the shared |
| 10 | `roster.Page`, HTTP status and a fixed error. A successful 409 has a nil error and |
| 11 | no member fields. The HTTP caller must sign both 200 and 409 with shared |
| 12 | `roster.SignPage`; no raw Page is a trusted wire response. |
| 13 | |
| 14 | The HTTP integration still owns exact Host enforcement, method/query/header |
| 15 | shape and length checks, no-store on every response, fixed response bodies, |
| 16 | no request-URL logging, and roster-only signing keys. `Read` has no outbound |
| 17 | transport. All scope comes from the token's consumer and its matching live |
| 18 | registry row. A requested team only compares against that scope. |
| 19 | |
| 20 | ## Future audited writer transaction |
| 21 | |
| 22 | There is deliberately no public mutation API. The following are preconditions |
| 23 | for the root-owned operator integration, not authority conferred by these tables. |
| 24 | Before opening its write transaction, authenticate and authorize the operation |
| 25 | using the separately reviewed operator authority. Verify the operation ID, |
| 26 | monotonic record sequence within its consumer scope, exact directory, consumer, team, app, audience and |
| 27 | expected current generation. Refuse replay, changed payload under an old ID, |
| 28 | and unknown authority. Never accept browser identity or bearer possession as |
| 29 | operator authority. Recheck expected stored state under the transaction lock. |
| 30 | |
| 31 | Within the same transaction, write the intended registry/consumer/token or |
| 32 | generation change and its successful receipt. The receipt retains only opaque |
| 33 | operation ID, sequence, fixed operation kind, optional consumer ID, generation, |
| 34 | SHA256 record digest and completion time. It is not a signed provisioning record |
| 35 | and cannot approve a client generation. The operator path must deliver the |
| 36 | separately signed record through the approved transport. No token or team name |
| 37 | belongs in the receipt. Name sealing remains the existing ID-bound helper. |
| 38 | |
| 39 | Token slots are normalized rows with a globally unique SHA256 hash of the exact |
| 40 | canonical 43-character bearer string. This prevents cross-consumer and cross-slot |
| 41 | collisions. To rotate, delete an older previous slot, change the current row to |
| 42 | previous with issued_at equal to the rotation time and expires_at at most 300 |
| 43 | seconds later, and insert the new current hash. Here issued_at starts the slot's |
| 44 | validity window; it is not the original token creation timestamp. Current slots |
| 45 | have no expiry. Future-dated slots fail. Disable by setting enabled=0 in the |
| 46 | same audited transaction; no read changes slots or extends an overlap. |
| 47 | |
| 48 | Ordinary boot never fills generation. Bootstrap needs the independently approved |
| 49 | fresh-install path. Restore needs the platform's fenced maintenance path and a |
| 50 | new random generation plus newly approved records before traffic resumes. |
| 51 | Directory storage alone does not supply either path. Generation and receipt |
| 52 | values must be canonical and exact when written; the reader also rejects a |
| 53 | malformed generation. Consumers bind to a matching registry consumer/team/app/ |
| 54 | origin and the injected app/route policy; pending or disabled instances do not |
| 55 | serve rosters. |
| 56 | |
| 57 | Membership INSERT, UPDATE and DELETE triggers advance affected teams' epochs in |
| 58 | the writing transaction and abort on signed 64-bit overflow. The future writer |
| 59 | must not add a second manual increment. Moving membership bumps both teams once; |
| 60 | changing a role bumps one. Even a no-op UPDATE bumps its team. Do not lower an |
| 61 | epoch or bypass triggers. Restore is the separate generation-fenced procedure. |
| 62 | |
| 63 | ## Snapshot and cursor |
| 64 | |
| 65 | One read transaction resolves the token, enabled/overlap state, live binding, |
| 66 | generation, epoch, count and ordered page. It does not keep a server snapshot |
| 67 | between requests. A membership write after that transaction begins may appear |
| 68 | on the next call; the consumer's standing lease accounts for this bound. |
| 69 | |
| 70 | The cursor is sealed with purpose `directory.roster-cursor/<directory origin>`. |
| 71 | Its closed JSON binds consumer, team, generation, epoch, fetch challenge, |
| 72 | last opaque subject, next index, issuance and expiry. It expires at ten seconds, |
| 73 | including at the exact boundary. Wrong scope/challenge, malformed, expired or |
| 74 | unauthentic cursors return fixed 400. A scoped valid cursor with changed |
| 75 | generation/epoch returns the signed 409 shape with current values. Rotation and |
| 76 | disable are checked again before cursor processing. No cursor grants authority. |
| 77 | |