mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-14 14:40:52 +00:00
Scope the idempotency replay cell to the record a request claims
A delegate retrying with one `Idempotency-Key` across two people's records met itself in one cell. The cache tuple was `(caller, key, method, path)`, so the second record's request replayed the first record's cached 201: the handler never ran, no row was written for the second person, and the caller was told it had been — with the other person's id in the body. Nothing errored and nothing logged a conflict, and five of the write verbs about to become delegable are wrapped here. Fold the claimed record into the `key` component, centrally, once. Not into `userId`: that column is a foreign key to `users` with a cascade behind it and cannot hold a composite. It keeps naming the caller, which is what makes two delegates on one record two cells for free. The separator is a byte the key validator refuses, so a caller cannot hand-craft a key into a cell belonging to a request they are not making. A request with no acting account keys byte-for-byte as it did before, so a client that never heard of sharing cannot be affected. Accepted and pinned by test: the fold uses the CLAIM, before the grant check, so within the 24h window a delegate whose access has ended can still replay a key they had already completed. They receive bytes they already had, nothing new is written, and a fresh key from them is refused because that request reaches the handler. Both frozen surfaces move in this diff. The claim reader gains a guard leg freezing its consumers, since an unchecked claim is exactly the value a future caller must not mistake for permission.
This commit is contained in:
1 parent
f24ae6deb8
commit
c7960d726d
7 files changed
+563
-8
No files matched your search
@@ -75,7 +75,16 @@ function request(): NextRequest {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
// The whole shape `getSession()` returns. The session half carries the
|
||||
// acting-account carrier, which the idempotency wrapper reads to decide
|
||||
// whose record a cell belongs to — a fake that omitted it made the wrapper
|
||||
// fail on a field production always has.
|
||||
mocks.getSession.mockResolvedValue({
|
||||
session: {
|
||||
id: "session-1",
|
||||
expiresAt: new Date(Date.now() + 60_000),
|
||||
actingAsUserId: null,
|
||||
},
|
||||
user: { id: "user-1", role: "USER" },
|
||||
});
|
||||
mocks.findUnique.mockResolvedValue(null);
|
||||
|
||||
Reference in new issue
Block a user