Skip to content

Commit 0ff02ac

Browse files
authored
Merge pull request #112 from upstash/DX-960
DX-960: Update Deny List from SDK
2 parents 180bd70 + db3a063 commit 0ff02ac

15 files changed

+840
-44
lines changed

Diff for: package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"fmt": "bunx @biomejs/biome check --apply ./src"
1313
},
1414
"devDependencies": {
15-
"@upstash/redis": "^1.28.3",
15+
"@upstash/redis": "^1.31.5",
1616
"bun-types": "latest",
1717
"rome": "^11.0.0",
1818
"tsup": "^7.2.0",

Diff for: src/deny-list.test.ts renamed to src/deny-list/deny-list.test.ts

+68-15
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { expect, test, describe, afterAll } from "bun:test";
1+
import { expect, test, describe, afterAll, beforeAll } from "bun:test";
22
import { Redis } from "@upstash/redis";
3-
import { Ratelimit } from "./index";
4-
import { checkDenyListCache, defaultDeniedResponse, resolveResponses } from "./deny-list";
5-
import { RatelimitResponseType } from "./types";
3+
import { Ratelimit } from "../index";
4+
import { checkDenyListCache, defaultDeniedResponse, resolveLimitPayload } from "./deny-list";
5+
import { DenyListResponse, RatelimitResponseType } from "../types";
66

77

88
test("should get expected response from defaultDeniedResponse", () => {
@@ -20,8 +20,18 @@ test("should get expected response from defaultDeniedResponse", () => {
2020
});
2121
});
2222

23+
describe("should resolve ratelimit and deny list response", async () => {
24+
const redis = Redis.fromEnv();
25+
const prefix = `test-resolve-prefix`;
26+
27+
let callCount = 0;
28+
const spyRedis = {
29+
multi: () => {
30+
callCount += 1;
31+
return redis.multi();
32+
}
33+
}
2334

24-
test.only("should override response in resolveResponses correctly", () => {
2535
const initialResponse = {
2636
success: true,
2737
limit: 100,
@@ -31,40 +41,83 @@ test.only("should override response in resolveResponses correctly", () => {
3141
reason: undefined,
3242
deniedValue: undefined
3343
};
34-
35-
const denyListResponse = "testValue";
44+
3645
const expectedResponse = {
3746
success: false,
3847
limit: 100,
3948
remaining: 0,
4049
reset: 60,
4150
pending: Promise.resolve(),
4251
reason: "denyList" as RatelimitResponseType,
43-
deniedValue: denyListResponse
52+
deniedValue: "testValue"
4453
};
4554

46-
const response = resolveResponses([initialResponse, denyListResponse]);
47-
expect(response).toEqual(expectedResponse);
48-
});
55+
test("should update ip deny list when invalidIpDenyList is true", async () => {
56+
let callCount = 0;
57+
const spyRedis = {
58+
multi: () => {
59+
callCount += 1;
60+
return redis.multi();
61+
}
62+
}
63+
64+
const denyListResponse: DenyListResponse = {
65+
deniedValue: "testValue",
66+
invalidIpDenyList: true
67+
};
68+
69+
const response = resolveLimitPayload(spyRedis as Redis, prefix, [initialResponse, denyListResponse], 8);
70+
await response.pending;
71+
72+
expect(response).toEqual(expectedResponse);
73+
expect(callCount).toBe(1) // calls multi once to store ips
74+
});
75+
76+
test("should update ip deny list when invalidIpDenyList is true", async () => {
77+
78+
let callCount = 0;
79+
const spyRedis = {
80+
multi: () => {
81+
callCount += 1;
82+
return redis.multi();
83+
}
84+
}
85+
86+
const denyListResponse: DenyListResponse = {
87+
deniedValue: "testValue",
88+
invalidIpDenyList: false
89+
};
90+
91+
const response = resolveLimitPayload(spyRedis as Redis, prefix, [initialResponse, denyListResponse], 8);
92+
await response.pending;
93+
94+
expect(response).toEqual(expectedResponse);
95+
expect(callCount).toBe(0) // doesn't call multi to update deny list
96+
});
97+
})
4998

5099

51100
describe("should reject in deny list", async () => {
52101
const redis = Redis.fromEnv();
53102
const prefix = `test-prefix`;
54103
const denyListKey = [prefix, "denyList", "all"].join(":");
55104

56-
// Insert a value into the deny list
57-
await redis.sadd(denyListKey, "denyIdentifier", "denyIp", "denyAgent", "denyCountry");
58105

59106
const ratelimit = new Ratelimit({
60107
redis,
61108
limiter: Ratelimit.tokenBucket(10, "5 s", 10),
62109
prefix,
63-
enableProtection: true
110+
enableProtection: true,
111+
denyListThreshold: 8
64112
});
65113

66114
afterAll(async () => {
67-
redis.del(denyListKey)
115+
await redis.del(denyListKey)
116+
})
117+
118+
// Insert a value into the deny list
119+
beforeAll(async () => {
120+
await redis.sadd(denyListKey, "denyIdentifier", "denyIp", "denyAgent", "denyCountry");
68121
})
69122

70123
test("should allow with values not in the deny list", async () => {

Diff for: src/deny-list.ts renamed to src/deny-list/deny-list.ts

+37-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { DeniedValue, LimitPayload, Redis } from "./types"
2-
import { RatelimitResponse } from "./types"
3-
import { Cache } from "./cache";
1+
import { DeniedValue, DenyListResponse, DenyListExtension, LimitPayload, IpDenyListStatusKey } from "../types"
2+
import { RatelimitResponse, Redis } from "../types"
3+
import { Cache } from "../cache";
4+
import { checkDenyListScript } from "./scripts";
5+
import { updateIpDenyList } from "./ip-deny-list";
46

57

68
const denyListCache = new Cache(new Map());
@@ -46,21 +48,28 @@ export const checkDenyList = async (
4648
redis: Redis,
4749
prefix: string,
4850
members: string[]
49-
): Promise<DeniedValue> => {
50-
const deniedMembers = await redis.smismember(
51-
[prefix, "denyList", "all"].join(":"),
51+
): Promise<DenyListResponse> => {
52+
const [ deniedValues, ipDenyListStatus ] = await redis.eval(
53+
checkDenyListScript,
54+
[
55+
[prefix, DenyListExtension, "all"].join(":"),
56+
[prefix, IpDenyListStatusKey].join(":"),
57+
],
5258
members
53-
);
59+
) as [boolean[], number];
5460

55-
let deniedMember: DeniedValue = undefined;
56-
deniedMembers.map((memberDenied, index) => {
61+
let deniedValue: DeniedValue = undefined;
62+
deniedValues.map((memberDenied, index) => {
5763
if (memberDenied) {
5864
blockMember(members[index])
59-
deniedMember = members[index]
65+
deniedValue = members[index]
6066
}
6167
})
6268

63-
return deniedMember;
69+
return {
70+
deniedValue,
71+
invalidIpDenyList: ipDenyListStatus === -2
72+
};
6473
};
6574

6675
/**
@@ -71,15 +80,28 @@ export const checkDenyList = async (
7180
* @param denyListResponse
7281
* @returns
7382
*/
74-
export const resolveResponses = (
75-
[ratelimitResponse, denyListResponse]: LimitPayload
83+
export const resolveLimitPayload = (
84+
redis: Redis,
85+
prefix: string,
86+
[ratelimitResponse, denyListResponse]: LimitPayload,
87+
threshold: number
7688
): RatelimitResponse => {
77-
if (denyListResponse) {
89+
90+
if (denyListResponse.deniedValue) {
7891
ratelimitResponse.success = false;
7992
ratelimitResponse.remaining = 0;
8093
ratelimitResponse.reason = "denyList";
81-
ratelimitResponse.deniedValue = denyListResponse
94+
ratelimitResponse.deniedValue = denyListResponse.deniedValue
95+
}
96+
97+
if (denyListResponse.invalidIpDenyList) {
98+
const updatePromise = updateIpDenyList(redis, prefix, threshold)
99+
ratelimitResponse.pending = Promise.all([
100+
ratelimitResponse.pending,
101+
updatePromise
102+
])
82103
}
104+
83105
return ratelimitResponse;
84106
};
85107

Diff for: src/deny-list/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./deny-list"

0 commit comments

Comments
 (0)