feat(02-12.1): refuse a record request whose context has moved

- both record resolvers check the fence after authentication and before
  the carrier, off the one session row that also decides the scope
- POST /api/account/switch takes an optional expectedEpoch and writes
  conditionally, so concurrent switchers resolve to one outcome
- GET /api/auth/me publishes the context a browser bootstraps from
- every response that resolved a record scope echoes it back
- regenerate the API contract
This commit is contained in:
Marc-André Bombeck committed 2026-08-06 18:21:40 +02:00
1 parent 59359738c8
commit 45fec0fc1b
15 files changed
+1631 -98

No files matched your search

+22
View File
@@ -108,6 +108,28 @@ export type InviteGrantBody = z.infer<typeof inviteGrantSchema>;
*/
export const switchAccountSchema = z.object({
accountId: z.string().min(1).max(MAX_ACCOUNT_ID_LENGTH).nullable(),
/**
* v1.37.0 — the record epoch the caller believes this session is at.
*
* Present turns the selector write into a compare-and-set: exactly one of two
* tabs pressing the switcher at the same moment lands, and the loser gets a
* 409 rather than silently overwriting the winner. It is not a credential —
* the epoch is server-issued, monotonic, and scoped to the caller's own
* session, so naming it can only ever refuse the caller's own write.
*
* Absent keeps the pre-fence unconditional write, and that arm is reachable
* only by a bundle that predates the fence: the switcher is rendered from
* `/api/auth/me` data, so a fence-aware client has necessarily adopted a
* context before it can offer a switch at all.
*/
expectedEpoch: z
.number()
.int()
.min(0)
.optional()
.describe(
"The record epoch the caller believes this browser session is at, as published by GET /api/auth/me or a previous switch. Present makes the write conditional: if another tab moved the selector first the switch is refused with 409 sharing.session.changed and the caller reconciles through /api/auth/me. Omitted keeps the unconditional write, which is what a client predating the record-session fence sends. Cookie transport only.",
),
});
export type SwitchAccountBody = z.infer<typeof switchAccountSchema>;