oficina / slides Public

Clone
git clone https://amadan.net/oficina/slides

Plain git — no account needed to clone.

Download

Download this file

1package slides
2
3import "time"
4
5// Models is every model the schema generator manages. Keep it in step
6// with the structs below: `rastrillo migration generate` reads
7// it to work out what the database should look like, and
8// `rastrillo migration check` fails CI when it and the
9// migrations disagree.
10var Models = []any{
11 &User{},
12 &Deck{},
13 &DeckUpdate{},
14 &DeckSnapshot{},
15 &DeckAsset{},
16 &AssetUpload{},
17 &DeckGrant{},
18 &ShareLink{},
19 &Presentation{},
20 &Agent{},
21}
22
23// User is a person who has signed in here — spec §6.1's
24// `users(id, email, name, colour)`, created on first sign-in by
25// auth.go's ensureUser.
26//
27// It exists because under rastrillo/auth the session Subject is the
28// verified email address, not a numeric id (SKILL.md §5's warning:
29// sessions.UserID answers (0, false) and would scope every owned query
30// to user_id = 0). Every later model's owner column points at User.ID,
31// so the address has to become a row before anything can be scoped.
32//
33// Email carries the unique index because the address is the identity:
34// two rows for one address would mean one person owning two disjoint
35// sets of decks depending on which row a request happened to find.
36// Stored normalized (auth.go's normalizeAddress) so "Paul@Example.test"
37// and "paul@example.test" are the same person, which is what the
38// allowlist already assumes.
39type User struct {
40 ID int64
41 Email string `gorm:"uniqueIndex"`
42 // Name is what other people see beside this person's cursor and in
43 // a deck's activity list. Empty until there is a screen to set it;
44 // callers fall back to Email.
45 Name string
46 // Colour is this person's presence colour (spec §3.2's coloured
47 // outline and cursor label). Assigned at creation from a fixed
48 // palette so two people in one deck are told apart by more than
49 // their name — and never by colour alone, per the app's own WCAG
50 // commitment.
51 Colour string
52 CreatedAt time.Time
53}
54
55// Deck is one presentation: the row that owns a Y.Doc, not the document
56// itself. The content lives in DeckUpdate's log (spec §2.1); what is on
57// this row is everything a list, a link or a permission check needs
58// without opening the document at all.
59//
60// Sqid is the deck's public name — ten random characters, unique,
61// minted at creation and never derived from ID (decks.go's newSqid). It
62// is also the Y.Doc's guid, so a document and its URL cannot drift
63// apart.
64//
65// Title and Theme are duplicated: they are meta.title and meta.theme
66// inside the document too, and both move together on every write. The
67// copy exists so the deck list can render fifty rows without
68// instantiating fifty wasm modules.
69//
70// Seq is the log's high-water mark and the optimistic lock every write
71// takes: an insert at Seq+1 lands only if the row still reads Seq (see
72// decks.go's applyOps). It is not a version anybody sees.
73type Deck struct {
74 ID int64
75 Sqid string `gorm:"uniqueIndex"`
76 // Title is the deck's name, and the only free text on this row.
77 Title string
78 // OwnerID is the creator — spec §5's private rung, and the one
79 // person who may change settings or delete.
80 OwnerID int64 `gorm:"index"`
81 // Rung is who may open this deck: "private", "granted" or "team"
82 // (suite ruling 15, spec §5). Every deck starts private. This
83 // slice's UI offers private and team only; granted needs a screen
84 // for naming people, which has not been built — but DeckGrant and
85 // decks.go's visibleDecks already honour it.
86 Rung string
87 // Theme is the theme id (spec §2.1's meta.theme).
88 Theme string
89 Seq int64
90 CreatedAt time.Time
91 UpdatedAt time.Time
92}
93
94// DeckUpdate is one entry in a deck's update log: the Yjs update bytes
95// one write produced. Replaying every row of a deck in Seq order
96// reproduces the document exactly (decks.go's openDeck) — there are no
97// snapshots yet.
98//
99// Actor is spec §6.2's actor string, "person:<user id>" or
100// "agent:<agent id>". It lives on this row rather than in the CRDT
101// deliberately: the document is what every peer converges on, and who
102// wrote which byte of it is this instance's bookkeeping, not shared
103// content.
104type DeckUpdate struct {
105 ID int64
106 // DeckID and Seq carry one composite unique index rather than an
107 // index each. That index is what makes "the update after seq n" a
108 // single insert that either lands or violates, with no
109 // read-then-write window; and being leftmost in it, DeckID alone is
110 // still indexed, so a second index on DeckID would only cost
111 // writes.
112 DeckID int64 `gorm:"uniqueIndex:idx_deck_updates_deck_seq"`
113 Seq int64 `gorm:"uniqueIndex:idx_deck_updates_deck_seq"`
114 Actor string
115 Bytes []byte
116 CreatedAt time.Time
117}
118
119// DeckSnapshot is one deck's whole document at one seq, stored as a
120// single Yjs update (core.Handle.State) so a later open can start from
121// it instead of from the first row of the log.
122//
123// It is DERIVED DATA, and every decision here follows from that (Docs'
124// snapshots ruling, adopted). The log is what the deck is: delete every
125// row of this table and nothing is lost but time. So a snapshot is
126// written best-effort AFTER the update that crossed the boundary has
127// committed (snapshot.go's maybeSnapshot), a failure to write one is
128// logged and never fails the request, and a snapshot that cannot be
129// read is skipped in favour of replaying the plain log.
130//
131// Seq is the highest seq the bytes contain, and openDeck trusts it: it
132// replays the rows at seq > Seq and no others. Nothing here can check
133// that claim, so the ONE caller allowed to write a row is the one
134// holding a handle it knows the seq of.
135//
136// Bytes rather than a path: a deck's state is kilobytes, this is one
137// row per two hundred updates, and superseded rows are deleted as each
138// new one lands, so the table holds one row per deck in the steady
139// state.
140//
141// (deck_id, seq) is deliberately NOT unique. Two writers crossing the
142// boundary together produce equivalent rows at the same seq, and that
143// is an ordinary outcome rather than a collision to resolve.
144type DeckSnapshot struct {
145 ID int64
146 DeckID int64 `gorm:"index"`
147 Seq int64
148 Bytes []byte
149 // CreatedAt is when the snapshot was taken, not when the deck last
150 // moved: a snapshot is bookkeeping, and the deck's own UpdatedAt is
151 // the column anything user-facing reads.
152 CreatedAt time.Time
153}
154
155// DeckAsset records that one ref was VERIFIED for one deck: read back
156// out of the store, sniffed as an image, sanitised if an SVG
157// (assets.go's registerAsset). It is the half of the asset gate the
158// document cannot carry.
159//
160// The deck's own `assets` map says which refs a deck refers to, and
161// serveAsset checks it before serving a byte. But that map is written
162// by `register_asset`, an op any member can apply with any ref — and a
163// presigned PUT lands whatever bytes the browser sent under whatever
164// hash it named, with nothing in between looking at them. So a member
165// could PUT an unsanitised SVG and name its ref in an op, and the deck
166// would refer to it and this app would serve it under its own origin.
167// A row here is written only by registerAsset, after the bytes were
168// checked, so serveAsset requiring one (as well as the map) means the
169// map alone can never make an unverified object servable.
170//
171// Ref is the store's content address, which is what the served bytes
172// are named by; (deck_id, ref) is unique because verifying the same
173// bytes for the same deck twice is one fact, not two. Deleting the deck
174// deletes its rows (decks.go's deleteDeck): a verification is about a
175// deck, and nothing else ever reads one.
176type DeckAsset struct {
177 ID int64
178 DeckID int64 `gorm:"uniqueIndex:idx_deck_assets_deck_ref"`
179 Ref string `gorm:"uniqueIndex:idx_deck_assets_deck_ref"`
180 CreatedAt time.Time
181}
182
183// AssetUpload is the RECEIPT for one upload this app itself signed —
184// spec §2.7 as amended by codex 1 and codex 2. It is what makes an
185// upload a thing that happened rather than a ref somebody knows.
186//
187// Before it existed, `POST /d/{sqid}/uploads` signed a PUT for whatever
188// address the browser named and `POST /d/{sqid}/assets` verified
189// whatever address the browser named. Two consequences, each a hole:
190// a member could sign a PUT for an address another deck's picture was
191// already served from and replace the bytes under it (codex 1), and a
192// member who merely KNEW another deck's ref could register it into a
193// deck of their own and pass both of serveAsset's checks (codex 2) —
194// which defeated the whole "a content address is not an authorisation"
195// rule the asset routes are built on.
196//
197// So the browser never names an address again. signUpload mints a
198// STAGING key — a hash of this deck, this actor and 32 fresh random
199// bytes, so the address is a function of the principal and is not
200// guessable or reachable from outside — writes this row, and signs a PUT
201// for that key alone. Registration will only look at bytes a row here
202// says THIS principal staged for THIS deck; it then reads them, hashes
203// what it actually read, and promotes them to a final object keyed by
204// that hash. Nothing ever signs a PUT for a final address, so a
205// verified object is immutable by construction rather than by nobody
206// having tried.
207//
208// StagingKey carries the unique index because it is what a PUT and a
209// registration are both resolved by. ObjectKey is where the bytes
210// actually are: the staging key itself on a presigning store (the
211// browser wrote them there), or the content address blobs.Dir chose in
212// development, where the PUT comes through this app and the store is
213// content-addressed with no way to write a caller's key. ConsumedAt
214// makes a receipt single-use, so one signed upload is one registration.
215type AssetUpload struct {
216 ID int64
217 DeckID int64 `gorm:"index"`
218 // Actor is who asked for the URL, in DeckUpdate.Actor's form. Half
219 // of the pair registration matches on: an upload one member staged
220 // is not another member's to register, and an upload staged for one
221 // deck is not registrable into another.
222 Actor string
223 // StagingKey is the address this app told the browser to write to,
224 // and the only address it will ever sign a PUT for.
225 StagingKey string `gorm:"uniqueIndex"`
226 // ObjectKey is where the staged bytes really are once the PUT has
227 // landed. Empty until then on the development path.
228 ObjectKey string
229 // DeclaredHash is the SHA-256 the browser said the file has. Never
230 // trusted for anything — the final address comes from bytes this
231 // app read — but checked against what landed on the development
232 // path, where this app does the hashing anyway.
233 DeclaredHash string
234 // ContentType is what the browser said it was uploading, kept so
235 // the PUT can be signed with the header the bucket will store.
236 ContentType string
237 Size int64
238 CreatedAt time.Time
239 ConsumedAt *time.Time
240}
241
242// DeckGrant names one person who may open one deck — spec §5's
243// `granted` rung.
244//
245// No screen creates one yet. The model and visibleDecks' join exist
246// from the start anyway, because the alternative is a visibility seam
247// that quietly means "owner-or-team" long after grants land: the day
248// the screen arrives, nothing about who can read what has to be
249// rewritten.
250type DeckGrant struct {
251 ID int64
252 DeckID int64 `gorm:"index"`
253 UserID int64 `gorm:"index"`
254 // Actor is who granted it, in DeckUpdate.Actor's form.
255 Actor string
256 CreatedAt time.Time
257}
258
259// ShareLink is one read-only capability for one deck — spec §5's share
260// link, calendar's feed pattern (its calendar design §6).
261//
262// Only the hash is stored. A plaintext token in this table would be a
263// live, unauthenticated read capability for somebody's whole deck,
264// sitting in a file the platform replicates to object storage. The
265// consequence is that the URL cannot be shown again, so the share page
266// shows it at the moment of minting and says so.
267//
268// TokenHash carries the unique index because it is what a request is
269// resolved by: a single indexed read on /s/{token}, and no two rows
270// can ever claim one URL.
271//
272// Views is the honest part, calendar's reason: a link that has been
273// opened four hundred times is a different thing to hold than one
274// nobody has ever used, and the share page can only say so if the
275// number is kept.
276//
277// RevokedAt rather than a delete, also calendar's: the share page keeps
278// showing that the link existed and that it is off, and a URL still
279// pasted in somebody's chat window stops working rather than silently
280// matching a later link that happened to reuse the row.
281type ShareLink struct {
282 ID int64
283 DeckID int64 `gorm:"index"`
284 // TokenHash is the SHA-256 hex of the token (share.go's
285 // hashShareToken). The token itself is never written anywhere.
286 TokenHash string `gorm:"uniqueIndex"`
287 // Actor is who minted it, in DeckUpdate.Actor's form.
288 Actor string
289 Views int64
290 CreatedAt time.Time
291 RevokedAt *time.Time
292}
293
294// Presentation is one live talk: which deck, who is presenting, which
295// slide is on the screen, and the two capabilities that reach it —
296// spec §3.4's session row, and ruling 5's "presenting is a server-side
297// presentation session".
298//
299// It is the one record the presenter view, the phone remote and every
300// following audience member all read and write, which is what makes
301// them agree: there is no second place where "the deck is on slide
302// four" is written down, so there is nothing for them to disagree
303// about.
304//
305// Sqid is the session's public name and a VIEWING capability: anybody
306// who has it may watch the fullscreen surface, exactly as a share link
307// works. It is not a driving capability — that is RemoteTokenHash.
308//
309// Code is the six characters a person reads aloud or types into a
310// phone, from an alphabet with no O/0 and no I/1 for the same reason
311// the deck sqid's has none. It is unique among LIVE sessions only, and
312// enforced in Go rather than by an index, because a code that could
313// never be reused would make a long-lived instance run out of the
314// short, sayable ones. It is not a capability on its own: pairing
315// needs the code AND the token (presentation.go's remotePair).
316//
317// RemoteTokenHash is the SHA-256 hex of the remote's capability, the
318// share link's construction and the share link's reason (share.go):
319// only the hash is stored, so the URL can be shown exactly once, and
320// resolving one is a constant-time compare rather than a lookup.
321//
322// Slide is a 0-based index into the deck's slides. Every URL and every
323// stream event carries the 1-based number a person sees instead
324// (presentation.go's eventFor) — the two are one conversion in one
325// place, and this column is the index because that is what indexes
326// into the document.
327//
328// EndedAt rather than a delete, and StartedAt read as a 12-hour
329// deadline: a session is over when it is ended or when it is old, and
330// both answers are 410 rather than 404, because a talk that finished
331// is a different thing from a link that was never real.
332type Presentation struct {
333 ID int64
334 Sqid string `gorm:"uniqueIndex"`
335 DeckID int64 `gorm:"index"`
336 // Actor is who is presenting, in DeckUpdate.Actor's form.
337 Actor string
338 Slide int
339 Blackout bool
340 Code string `gorm:"index"`
341 RemoteTokenHash string
342 StartedAt time.Time
343 EndedAt *time.Time
344}
345
346// Agent is one non-human principal that may drive this instance's
347// decks — spec §7.1, and suite ruling 9 as this app reads it: an agent
348// is its OWN principal with an operator behind it, never a borrowed
349// session belonging to the person who minted it.
350//
351// The consequence lands on DeckUpdate.Actor. A write an agent makes is
352// recorded "agent:<id>", so a deck's history says which agent wrote a
353// slide and, through OperatorID, on whose authority — two facts a
354// shared session would have collapsed into one wrong one.
355//
356// Name is the short half of the principal (`claude`, `codex`), held to
357// lowercase letters, digits and hyphens so the principal string
358// `<name>~<operator email>` has exactly one separator in it and reads
359// the same everywhere it is shown. It is unique among one person's
360// LIVE agents, enforced in Go rather than by an index, because a name
361// that could never be reused would mean revoking `claude` costs you
362// the name forever.
363//
364// TokenHash is the SHA-256 hex of the token and carries the unique
365// index — share.go's construction and share.go's reason: only the
366// fingerprint is stored, so a bearer resolves in one indexed read and
367// the token itself can be shown exactly once. The screen that mints
368// one says so at the moment it shows it.
369//
370// RevokedAt rather than a delete, ShareLink's reason again: the page
371// keeps showing that the agent existed and that it is off, the actor
372// strings already written into deck histories still resolve to a name,
373// and a token still sitting in some machine's environment stops
374// working rather than silently matching a later agent that happened to
375// reuse the row.
376type Agent struct {
377 ID int64
378 Name string
379 // OperatorID is the person who minted this agent and whose
380 // visibility it borrows: an agent reads and writes exactly the
381 // decks its operator can, never more.
382 OperatorID int64 `gorm:"index"`
383 TokenHash string `gorm:"uniqueIndex"`
384 CreatedAt time.Time
385 RevokedAt *time.Time
386}
387