mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 21:10:59 +00:00
fix(workouts): count a bridge's batches apart from the phone's
A workouts:write token and the iPhone app shared one per-account bucket of 60 batches a minute, so a bridge stuck in a retry loop could use up the allowance the phone needs to sync the same account. Scoped tokens now count in a bucket of their own, shared by all bridge tokens of the account so that minting more tokens does not multiply the allowance. Refs #1054
This commit is contained in:
1 parent
08d5acdcb5
commit
4120927a94
2 files changed
+43
-1
No files matched your search
@@ -207,6 +207,42 @@ describe("POST /api/workouts/batch — which credentials it admits", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/workouts/batch — rate limit buckets", () => {
|
||||
function lastBucket(): string {
|
||||
const calls = vi.mocked(checkRateLimit).mock.calls;
|
||||
return calls[calls.length - 1][0] as string;
|
||||
}
|
||||
|
||||
it("counts a workouts:write token apart from the phone", async () => {
|
||||
armToken([WORKOUTS_WRITE_SCOPE]);
|
||||
await POST(makeRequest({ workouts: [workout("s-1")] }));
|
||||
const bridge = lastBucket();
|
||||
|
||||
armToken(["*"]);
|
||||
await POST(
|
||||
makeRequest({ workouts: [workout("s-2", { source: "APPLE_HEALTH" })] }),
|
||||
);
|
||||
const phone = lastBucket();
|
||||
|
||||
expect(bridge).not.toBe(phone);
|
||||
expect(bridge).toContain(USER.id);
|
||||
expect(phone).toContain(USER.id);
|
||||
});
|
||||
|
||||
it("keeps a session and a wildcard token in the same bucket", async () => {
|
||||
armSession();
|
||||
await POST(
|
||||
makeRequest({ workouts: [workout("s-1", { source: "APPLE_HEALTH" })] }),
|
||||
);
|
||||
const session = lastBucket();
|
||||
armToken(["*"]);
|
||||
await POST(
|
||||
makeRequest({ workouts: [workout("s-2", { source: "APPLE_HEALTH" })] }),
|
||||
);
|
||||
expect(lastBucket()).toBe(session);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/workouts/batch — what a scoped caller writes", () => {
|
||||
it("attributes its rows to EXTERNAL", async () => {
|
||||
armToken([WORKOUTS_WRITE_SCOPE]);
|
||||
|
||||
@@ -232,8 +232,14 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
// database for every other user on the host. 60 batches/min/user is
|
||||
// generous for healthy iOS sync and tight enough that a misbehaving
|
||||
// client bottoms out within a minute.
|
||||
//
|
||||
// A scoped `workouts:write` token is a bridge, not the phone, so it counts
|
||||
// in a bucket of its own: a bridge stuck in a retry loop must not use up
|
||||
// the allowance the iPhone app needs to sync the same account's workouts.
|
||||
// All bridge tokens of one account share that bucket, so minting more
|
||||
// tokens does not multiply the allowance.
|
||||
const rl = await checkRateLimit(
|
||||
`workouts:batch:${user.id}`,
|
||||
scoped ? `workouts:batch:bridge:${user.id}` : `workouts:batch:${user.id}`,
|
||||
BATCH_RATE_LIMIT_MAX,
|
||||
BATCH_RATE_LIMIT_WINDOW_MS,
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user