mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 22:26:13 +00:00
feat(02-12.1): stamp a record-context epoch on every session
- add sessions.record_epoch, owned by a BEFORE UPDATE trigger so the ON DELETE SET NULL path and raw SQL writes cannot skip it - normalise pre-existing rows so epoch zero means never switched - project the epoch off the row getSession already loads
This commit is contained in:
1 parent
a08e82d0be
commit
5c9befc0ae
7 files changed
+376
-2
No files matched your search
@@ -100,6 +100,7 @@ const SESSION_OK = {
|
||||
id: "sess-1",
|
||||
expiresAt: new Date(Date.now() + 3_600_000),
|
||||
actingAsUserId: null,
|
||||
recordEpoch: 0,
|
||||
},
|
||||
user: SESSION_USER as never,
|
||||
};
|
||||
|
||||
@@ -81,6 +81,7 @@ function signedIn() {
|
||||
id: "s1",
|
||||
expiresAt: new Date(Date.now() + 1e6),
|
||||
actingAsUserId: null,
|
||||
recordEpoch: 0,
|
||||
},
|
||||
user: { id: "user-1" } as never,
|
||||
});
|
||||
|
||||
@@ -148,16 +148,25 @@ function user(
|
||||
};
|
||||
}
|
||||
|
||||
/** Sign the delegate in over the cookie transport. */
|
||||
/**
|
||||
* Sign the delegate in over the cookie transport.
|
||||
*
|
||||
* `recordEpoch` defaults to 0, which is the record-session fence's exemption:
|
||||
* a session that has never been pointed at another record is served without a
|
||||
* fence header, so every pre-fence case in this file keeps meaning what it
|
||||
* meant. Cases that want the fence live pass an epoch explicitly.
|
||||
*/
|
||||
function signedInCookie(
|
||||
actingAsUserId: string | null = null,
|
||||
role: "USER" | "ADMIN" = "USER",
|
||||
recordEpoch = 0,
|
||||
): void {
|
||||
vi.mocked(getSession).mockResolvedValue({
|
||||
session: {
|
||||
id: "session-1",
|
||||
expiresAt: new Date(Date.now() + 3_600_000),
|
||||
actingAsUserId,
|
||||
recordEpoch,
|
||||
},
|
||||
user: user(DELEGATE, role) as never,
|
||||
});
|
||||
@@ -649,6 +658,7 @@ describe("the cookie-only helpers stay out of reach of a switch", () => {
|
||||
id: "session-1",
|
||||
expiresAt: new Date(Date.now() + 3_600_000),
|
||||
actingAsUserId: OWNER,
|
||||
recordEpoch: 0,
|
||||
},
|
||||
user: {
|
||||
...user(DELEGATE),
|
||||
|
||||
+15
-1
@@ -246,7 +246,12 @@ export async function createSession(
|
||||
}
|
||||
|
||||
export async function getSession(): Promise<{
|
||||
session: { id: string; expiresAt: Date; actingAsUserId: string | null };
|
||||
session: {
|
||||
id: string;
|
||||
expiresAt: Date;
|
||||
actingAsUserId: string | null;
|
||||
recordEpoch: number;
|
||||
};
|
||||
user: User;
|
||||
} | null> {
|
||||
try {
|
||||
@@ -363,6 +368,15 @@ export async function getSession(): Promise<{
|
||||
// `src/__tests__/acting-account-boundary-guard.test.ts` freezes the set
|
||||
// of files allowed to touch this field.
|
||||
actingAsUserId: session.actingAsUserId,
|
||||
// v1.37.0 — the selector's move counter, projected off the same row for
|
||||
// the same reason. It is what makes the fence's central property true:
|
||||
// the epoch a request is validated against and the `actingAsUserId` the
|
||||
// handler is about to serve under are one row read, so they cannot
|
||||
// straddle a switch that landed between two queries.
|
||||
//
|
||||
// Written by a database trigger only (migration 0302); nothing in the
|
||||
// application assigns it, and the `BEFORE` timing means nothing can.
|
||||
recordEpoch: session.recordEpoch,
|
||||
},
|
||||
user: session.user,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user