@upstash/ratelimit
Advanced tools
Comparing version 1.2.0 to 1.2.1-canary
import { Aggregate } from '@upstash/core-analytics'; | ||
import { Pipeline } from '@upstash/redis'; | ||
@@ -86,3 +87,3 @@ /** | ||
*/ | ||
deniedValue?: string; | ||
deniedValue?: DeniedValue; | ||
}; | ||
@@ -97,2 +98,3 @@ type Algorithm<TContext> = () => { | ||
type IsDenied = 0 | 1; | ||
type DeniedValue = string | undefined; | ||
type LimitOptions = { | ||
@@ -117,2 +119,3 @@ geo?: Geo; | ||
smismember: (key: string, members: string[]) => Promise<IsDenied[]>; | ||
multi: () => Pipeline; | ||
} | ||
@@ -243,2 +246,3 @@ | ||
enableProtection?: boolean; | ||
denyListThreshold?: number; | ||
}; | ||
@@ -268,2 +272,3 @@ /** | ||
protected readonly enableProtection: boolean; | ||
protected readonly denyListThreshold: number; | ||
constructor(config: RatelimitConfig<TContext>); | ||
@@ -569,2 +574,6 @@ /** | ||
enableProtection?: boolean; | ||
/** | ||
* @default 6 | ||
*/ | ||
denyListThreshold?: number; | ||
}; | ||
@@ -708,2 +717,49 @@ /** | ||
export { Algorithm, Analytics, AnalyticsConfig, MultiRegionRatelimit, MultiRegionRatelimitConfig, RegionRatelimit as Ratelimit, RegionRatelimitConfig as RatelimitConfig }; | ||
declare class ThresholdError extends Error { | ||
constructor(threshold: number); | ||
} | ||
/** | ||
* Gets the list of ips from the github source which are not in the | ||
* deny list already | ||
* | ||
* First, gets the ip list from github using the threshold. Then, calls redis with | ||
* a transaction which does the following: | ||
* - subtract the current ip deny list from all | ||
* - delete current ip deny list | ||
* - recreate ip deny list with the ips from github. Ips already in the users own lists | ||
* are excluded. | ||
* - status key is set to valid with ttl until next 2 AM UTC, which is a bit later than | ||
* when the list is updated on github. | ||
* | ||
* @param redis redis instance | ||
* @param prefix ratelimit prefix | ||
* @param threshold ips with less than or equal to the threshold are not included | ||
* @param ttl time to live in milliseconds for the status flag. Optional. If not | ||
* passed, ttl is infferred from current time. | ||
* @returns list of ips which are not in the deny list | ||
*/ | ||
declare const updateIpDenyList: (redis: Redis, prefix: string, threshold: number, ttl?: number) => Promise<unknown[]>; | ||
/** | ||
* Disables the ip deny list by removing the ip deny list from the all | ||
* set and removing the ip deny list. Also sets the status key to disabled | ||
* with no ttl. | ||
* | ||
* @param redis redis instance | ||
* @param prefix ratelimit prefix | ||
* @returns | ||
*/ | ||
declare const disableIpDenyList: (redis: Redis, prefix: string) => Promise<unknown[]>; | ||
type ipDenyList_ThresholdError = ThresholdError; | ||
declare const ipDenyList_ThresholdError: typeof ThresholdError; | ||
declare const ipDenyList_disableIpDenyList: typeof disableIpDenyList; | ||
declare const ipDenyList_updateIpDenyList: typeof updateIpDenyList; | ||
declare namespace ipDenyList { | ||
export { | ||
ipDenyList_ThresholdError as ThresholdError, | ||
ipDenyList_disableIpDenyList as disableIpDenyList, | ||
ipDenyList_updateIpDenyList as updateIpDenyList, | ||
}; | ||
} | ||
export { Algorithm, Analytics, AnalyticsConfig, ipDenyList as IpDenyList, MultiRegionRatelimit, MultiRegionRatelimitConfig, RegionRatelimit as Ratelimit, RegionRatelimitConfig as RatelimitConfig }; |
@@ -24,2 +24,3 @@ "use strict"; | ||
Analytics: () => Analytics, | ||
IpDenyList: () => ip_deny_list_exports, | ||
MultiRegionRatelimit: () => MultiRegionRatelimit, | ||
@@ -298,3 +299,103 @@ Ratelimit: () => RegionRatelimit | ||
// src/deny-list.ts | ||
// src/types.ts | ||
var DenyListExtension = "denyList"; | ||
var IpDenyListKey = "ipDenyList"; | ||
var IpDenyListStatusKey = "ipDenyListStatus"; | ||
// src/deny-list/scripts.ts | ||
var checkDenyListScript = ` | ||
-- Checks if values provideed in ARGV are present in the deny lists. | ||
-- This is done using the allDenyListsKey below. | ||
-- Additionally, checks the status of the ip deny list using the | ||
-- ipDenyListStatusKey below. Here are the possible states of the | ||
-- ipDenyListStatusKey key: | ||
-- * status == -1: set to "disabled" with no TTL | ||
-- * status == -2: not set, meaning that is was set before but expired | ||
-- * status > 0: set to "valid", with a TTL | ||
-- | ||
-- In the case of status == -2, we set the status to "pending" with | ||
-- 30 second ttl. During this time, the process which got status == -2 | ||
-- will update the ip deny list. | ||
local allDenyListsKey = KEYS[1] | ||
local ipDenyListStatusKey = KEYS[2] | ||
local results = redis.call('SMISMEMBER', allDenyListsKey, unpack(ARGV)) | ||
local status = redis.call('TTL', ipDenyListStatusKey) | ||
if status == -2 then | ||
redis.call('SETEX', ipDenyListStatusKey, 30, "pending") | ||
end | ||
return { results, status } | ||
`; | ||
// src/deny-list/ip-deny-list.ts | ||
var ip_deny_list_exports = {}; | ||
__export(ip_deny_list_exports, { | ||
ThresholdError: () => ThresholdError, | ||
disableIpDenyList: () => disableIpDenyList, | ||
updateIpDenyList: () => updateIpDenyList | ||
}); | ||
// src/deny-list/time.ts | ||
var MILLISECONDS_IN_HOUR = 60 * 60 * 1e3; | ||
var MILLISECONDS_IN_DAY = 24 * MILLISECONDS_IN_HOUR; | ||
var MILLISECONDS_TO_2AM = 2 * MILLISECONDS_IN_HOUR; | ||
var getIpListTTL = (time) => { | ||
const now = time || Date.now(); | ||
const timeSinceLast2AM = (now - MILLISECONDS_TO_2AM) % MILLISECONDS_IN_DAY; | ||
return MILLISECONDS_IN_DAY - timeSinceLast2AM; | ||
}; | ||
// src/deny-list/ip-deny-list.ts | ||
var baseUrl = "https://raw.githubusercontent.com/stamparm/ipsum/master/levels"; | ||
var ThresholdError = class extends Error { | ||
constructor(threshold) { | ||
super(`Allowed threshold values are from 1 to 8, 1 and 8 included. Received: ${threshold}`); | ||
this.name = "ThresholdError"; | ||
} | ||
}; | ||
var getIpDenyList = async (threshold) => { | ||
if (typeof threshold !== "number" || threshold < 1 || threshold > 8) { | ||
throw new ThresholdError(threshold); | ||
} | ||
try { | ||
const response = await fetch(`${baseUrl}/${threshold}.txt`); | ||
if (!response.ok) { | ||
throw new Error(`Error fetching data: ${response.statusText}`); | ||
} | ||
const data = await response.text(); | ||
const lines = data.split("\n"); | ||
return lines.filter((value) => value.length > 0); | ||
} catch (error) { | ||
throw new Error(`Failed to fetch ip deny list: ${error}`); | ||
} | ||
}; | ||
var updateIpDenyList = async (redis, prefix, threshold, ttl) => { | ||
const allIps = await getIpDenyList(threshold); | ||
const allDenyLists = [prefix, DenyListExtension, "all"].join(":"); | ||
const ipDenyList = [prefix, DenyListExtension, IpDenyListKey].join(":"); | ||
const statusKey = [prefix, IpDenyListStatusKey].join(":"); | ||
const transaction = redis.multi(); | ||
transaction.sdiffstore(allDenyLists, allDenyLists, ipDenyList); | ||
transaction.del(ipDenyList); | ||
transaction.sadd(ipDenyList, ...allIps); | ||
transaction.sdiffstore(ipDenyList, ipDenyList, allDenyLists); | ||
transaction.sunionstore(allDenyLists, allDenyLists, ipDenyList); | ||
transaction.set(statusKey, "valid", { px: ttl ?? getIpListTTL() }); | ||
return await transaction.exec(); | ||
}; | ||
var disableIpDenyList = async (redis, prefix) => { | ||
const allDenyListsKey = [prefix, DenyListExtension, "all"].join(":"); | ||
const ipDenyListKey = [prefix, DenyListExtension, IpDenyListKey].join(":"); | ||
const statusKey = [prefix, IpDenyListStatusKey].join(":"); | ||
const transaction = redis.multi(); | ||
transaction.sdiffstore(allDenyListsKey, allDenyListsKey, ipDenyListKey); | ||
transaction.del(ipDenyListKey); | ||
transaction.set(statusKey, "disabled"); | ||
return await transaction.exec(); | ||
}; | ||
// src/deny-list/deny-list.ts | ||
var denyListCache = new Cache(/* @__PURE__ */ new Map()); | ||
@@ -312,22 +413,36 @@ var checkDenyListCache = (members) => { | ||
var checkDenyList = async (redis, prefix, members) => { | ||
const deniedMembers = await redis.smismember( | ||
[prefix, "denyList", "all"].join(":"), | ||
const [deniedValues, ipDenyListStatus] = await redis.eval( | ||
checkDenyListScript, | ||
[ | ||
[prefix, DenyListExtension, "all"].join(":"), | ||
[prefix, IpDenyListStatusKey].join(":") | ||
], | ||
members | ||
); | ||
let deniedMember = void 0; | ||
deniedMembers.map((memberDenied, index) => { | ||
let deniedValue = void 0; | ||
deniedValues.map((memberDenied, index) => { | ||
if (memberDenied) { | ||
blockMember(members[index]); | ||
deniedMember = members[index]; | ||
deniedValue = members[index]; | ||
} | ||
}); | ||
return deniedMember; | ||
return { | ||
deniedValue, | ||
invalidIpDenyList: ipDenyListStatus === -2 | ||
}; | ||
}; | ||
var resolveResponses = ([ratelimitResponse, denyListResponse]) => { | ||
if (denyListResponse) { | ||
var resolveLimitPayload = (redis, prefix, [ratelimitResponse, denyListResponse], threshold) => { | ||
if (denyListResponse.deniedValue) { | ||
ratelimitResponse.success = false; | ||
ratelimitResponse.remaining = 0; | ||
ratelimitResponse.reason = "denyList"; | ||
ratelimitResponse.deniedValue = denyListResponse; | ||
ratelimitResponse.deniedValue = denyListResponse.deniedValue; | ||
} | ||
if (denyListResponse.invalidIpDenyList) { | ||
const updatePromise = updateIpDenyList(redis, prefix, threshold); | ||
ratelimitResponse.pending = Promise.all([ | ||
ratelimitResponse.pending, | ||
updatePromise | ||
]); | ||
} | ||
return ratelimitResponse; | ||
@@ -356,2 +471,3 @@ }; | ||
enableProtection; | ||
denyListThreshold; | ||
constructor(config) { | ||
@@ -363,2 +479,3 @@ this.ctx = config.ctx; | ||
this.enableProtection = config.enableProtection ?? false; | ||
this.denyListThreshold = config.denyListThreshold ?? 6; | ||
this.primaryRedis = "redis" in this.ctx ? this.ctx.redis : this.ctx.regionContexts[0].redis; | ||
@@ -493,17 +610,13 @@ this.analytics = config.analytics ? new Analytics({ | ||
const definedMembers = this.getDefinedMembers(identifier, req); | ||
const deniedMember = checkDenyListCache(definedMembers); | ||
const deniedValue = checkDenyListCache(definedMembers); | ||
let result; | ||
if (deniedMember) { | ||
result = [defaultDeniedResponse(deniedMember), deniedMember]; | ||
if (deniedValue) { | ||
result = [defaultDeniedResponse(deniedValue), { deniedValue, invalidIpDenyList: false }]; | ||
} else { | ||
result = await Promise.all([ | ||
this.limiter().limit(this.ctx, key, req?.rate), | ||
checkDenyList( | ||
this.primaryRedis, | ||
this.prefix, | ||
definedMembers | ||
) | ||
this.enableProtection ? checkDenyList(this.primaryRedis, this.prefix, definedMembers) : { deniedValue: void 0, invalidIpDenyList: false } | ||
]); | ||
} | ||
return resolveResponses(result); | ||
return resolveLimitPayload(this.primaryRedis, this.prefix, result, this.denyListThreshold); | ||
}; | ||
@@ -1118,3 +1231,4 @@ /** | ||
ephemeralCache: config.ephemeralCache, | ||
enableProtection: config.enableProtection | ||
enableProtection: config.enableProtection, | ||
denyListThreshold: config.denyListThreshold | ||
}); | ||
@@ -1486,2 +1600,3 @@ } | ||
Analytics, | ||
IpDenyList, | ||
MultiRegionRatelimit, | ||
@@ -1488,0 +1603,0 @@ Ratelimit |
@@ -1,1 +0,1 @@ | ||
{ "name": "@upstash/ratelimit", "version": "v1.2.0", "main": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ "dist" ], "scripts": { "build": "tsup", "test": "bun test src --coverage", "fmt": "bunx @biomejs/biome check --apply ./src" }, "devDependencies": { "@upstash/redis": "^1.28.3", "bun-types": "latest", "rome": "^11.0.0", "tsup": "^7.2.0", "turbo": "^1.10.15", "typescript": "^5.0.0" }, "license": "MIT", "dependencies": { "@upstash/core-analytics": "^0.0.9" } } | ||
{ "name": "@upstash/ratelimit", "version": "v1.2.1-canary", "main": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ "dist" ], "scripts": { "build": "tsup", "test": "bun test src --coverage", "fmt": "bunx @biomejs/biome check --apply ./src" }, "devDependencies": { "@upstash/redis": "^1.31.5", "bun-types": "latest", "rome": "^11.0.0", "tsup": "^7.2.0", "turbo": "^1.10.15", "typescript": "^5.0.0" }, "license": "MIT", "dependencies": { "@upstash/core-analytics": "^0.0.9" } } |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
Manifest confusion
Supply chain riskThis package has inconsistent metadata. This could be malicious or caused by an error when publishing the package.
Found 1 instance in 1 package
372291
3784
2
3