@upstash/ratelimit
Advanced tools
Comparing version 2.0.2 to 2.0.3
@@ -23,8 +23,2 @@ import { Aggregate } from '@upstash/core-analytics'; | ||
cache?: EphemeralCache; | ||
scriptHashes: { | ||
limitHash?: string; | ||
getRemainingHash?: string; | ||
resetHash?: string; | ||
}; | ||
cacheScripts: boolean; | ||
}; | ||
@@ -574,5 +568,9 @@ type MultiRegionContext = { | ||
/** | ||
* If enabled, lua scripts will be sent to Redis with SCRIPT LOAD durint the first request. | ||
* In the subsequent requests, hash of the script will be used to invoke it | ||
* @deprecated Has no affect since v2.0.3. Instead, hash values of scripts are | ||
* hardcoded in the sdk and it attempts to run the script using EVALSHA (with the hash). | ||
* If it fails, runs script load. | ||
* | ||
* Previously, if enabled, lua scripts were sent to Redis with SCRIPT LOAD durint the first request. | ||
* In the subsequent requests, hash of the script would be used to invoke the scripts | ||
* | ||
* @default true | ||
@@ -579,0 +577,0 @@ */ |
@@ -160,27 +160,14 @@ "use strict"; | ||
// src/hash.ts | ||
var setHash = async (ctx, script, kind) => { | ||
const regionContexts = "redis" in ctx ? [ctx] : ctx.regionContexts; | ||
const hashSample = regionContexts[0].scriptHashes[kind]; | ||
if (!hashSample) { | ||
await Promise.all(regionContexts.map(async (context) => { | ||
context.scriptHashes[kind] = await context.redis.scriptLoad(script); | ||
})); | ||
} | ||
; | ||
}; | ||
var safeEval = async (ctx, script, kind, keys, args) => { | ||
if (!ctx.cacheScripts) { | ||
return await ctx.redis.eval(script, keys, args); | ||
} | ||
; | ||
await setHash(ctx, script, kind); | ||
var safeEval = async (ctx, script, keys, args) => { | ||
try { | ||
return await ctx.redis.evalsha(ctx.scriptHashes[kind], keys, args); | ||
return await ctx.redis.evalsha(script.hash, keys, args); | ||
} catch (error) { | ||
if (`${error}`.includes("NOSCRIPT")) { | ||
console.log("Script with the expected hash was not found in redis db. It is probably flushed. Will load another scipt before continuing."); | ||
ctx.scriptHashes[kind] = void 0; | ||
await setHash(ctx, script, kind); | ||
console.log(" New script successfully loaded."); | ||
return await ctx.redis.evalsha(ctx.scriptHashes[kind], keys, args); | ||
const hash = await ctx.redis.scriptLoad(script.script); | ||
if (hash !== script.hash) { | ||
console.warn( | ||
"Upstash Ratelimit: Expected hash and the hash received from Redis are different. Ratelimit will work as usual but performance will be reduced." | ||
); | ||
} | ||
return await ctx.redis.evalsha(hash, keys, args); | ||
} | ||
@@ -191,4 +178,160 @@ throw error; | ||
// src/lua-scripts/single.ts | ||
var fixedWindowLimitScript = ` | ||
local key = KEYS[1] | ||
local window = ARGV[1] | ||
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1 | ||
local r = redis.call("INCRBY", key, incrementBy) | ||
if r == tonumber(incrementBy) then | ||
-- The first time this key is set, the value will be equal to incrementBy. | ||
-- So we only need the expire command once | ||
redis.call("PEXPIRE", key, window) | ||
end | ||
return r | ||
`; | ||
var fixedWindowRemainingTokensScript = ` | ||
local key = KEYS[1] | ||
local tokens = 0 | ||
local value = redis.call('GET', key) | ||
if value then | ||
tokens = value | ||
end | ||
return tokens | ||
`; | ||
var slidingWindowLimitScript = ` | ||
local currentKey = KEYS[1] -- identifier including prefixes | ||
local previousKey = KEYS[2] -- key of the previous bucket | ||
local tokens = tonumber(ARGV[1]) -- tokens per window | ||
local now = ARGV[2] -- current timestamp in milliseconds | ||
local window = ARGV[3] -- interval in milliseconds | ||
local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1 | ||
local requestsInCurrentWindow = redis.call("GET", currentKey) | ||
if requestsInCurrentWindow == false then | ||
requestsInCurrentWindow = 0 | ||
end | ||
local requestsInPreviousWindow = redis.call("GET", previousKey) | ||
if requestsInPreviousWindow == false then | ||
requestsInPreviousWindow = 0 | ||
end | ||
local percentageInCurrent = ( now % window ) / window | ||
-- weighted requests to consider from the previous window | ||
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow) | ||
if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then | ||
return -1 | ||
end | ||
local newValue = redis.call("INCRBY", currentKey, incrementBy) | ||
if newValue == tonumber(incrementBy) then | ||
-- The first time this key is set, the value will be equal to incrementBy. | ||
-- So we only need the expire command once | ||
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second | ||
end | ||
return tokens - ( newValue + requestsInPreviousWindow ) | ||
`; | ||
var slidingWindowRemainingTokensScript = ` | ||
local currentKey = KEYS[1] -- identifier including prefixes | ||
local previousKey = KEYS[2] -- key of the previous bucket | ||
local now = ARGV[1] -- current timestamp in milliseconds | ||
local window = ARGV[2] -- interval in milliseconds | ||
local requestsInCurrentWindow = redis.call("GET", currentKey) | ||
if requestsInCurrentWindow == false then | ||
requestsInCurrentWindow = 0 | ||
end | ||
local requestsInPreviousWindow = redis.call("GET", previousKey) | ||
if requestsInPreviousWindow == false then | ||
requestsInPreviousWindow = 0 | ||
end | ||
local percentageInCurrent = ( now % window ) / window | ||
-- weighted requests to consider from the previous window | ||
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow) | ||
return requestsInPreviousWindow + requestsInCurrentWindow | ||
`; | ||
var tokenBucketLimitScript = ` | ||
local key = KEYS[1] -- identifier including prefixes | ||
local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens | ||
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds | ||
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval | ||
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds | ||
local incrementBy = tonumber(ARGV[5]) -- how many tokens to consume, default is 1 | ||
local bucket = redis.call("HMGET", key, "refilledAt", "tokens") | ||
local refilledAt | ||
local tokens | ||
if bucket[1] == false then | ||
refilledAt = now | ||
tokens = maxTokens | ||
else | ||
refilledAt = tonumber(bucket[1]) | ||
tokens = tonumber(bucket[2]) | ||
end | ||
if now >= refilledAt + interval then | ||
local numRefills = math.floor((now - refilledAt) / interval) | ||
tokens = math.min(maxTokens, tokens + numRefills * refillRate) | ||
refilledAt = refilledAt + numRefills * interval | ||
end | ||
if tokens == 0 then | ||
return {-1, refilledAt + interval} | ||
end | ||
local remaining = tokens - incrementBy | ||
local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval | ||
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining) | ||
redis.call("PEXPIRE", key, expireAt) | ||
return {remaining, refilledAt + interval} | ||
`; | ||
var tokenBucketIdentifierNotFound = -1; | ||
var tokenBucketRemainingTokensScript = ` | ||
local key = KEYS[1] | ||
local maxTokens = tonumber(ARGV[1]) | ||
local bucket = redis.call("HMGET", key, "refilledAt", "tokens") | ||
if bucket[1] == false then | ||
return {maxTokens, ${tokenBucketIdentifierNotFound}} | ||
end | ||
return {tonumber(bucket[2]), tonumber(bucket[1])} | ||
`; | ||
var cachedFixedWindowLimitScript = ` | ||
local key = KEYS[1] | ||
local window = ARGV[1] | ||
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1 | ||
local r = redis.call("INCRBY", key, incrementBy) | ||
if r == incrementBy then | ||
-- The first time this key is set, the value will be equal to incrementBy. | ||
-- So we only need the expire command once | ||
redis.call("PEXPIRE", key, window) | ||
end | ||
return r | ||
`; | ||
var cachedFixedWindowRemainingTokenScript = ` | ||
local key = KEYS[1] | ||
local tokens = 0 | ||
local value = redis.call('GET', key) | ||
if value then | ||
tokens = value | ||
end | ||
return tokens | ||
`; | ||
// src/lua-scripts/multi.ts | ||
var fixedWindowLimitScript = ` | ||
var fixedWindowLimitScript2 = ` | ||
local key = KEYS[1] | ||
@@ -209,3 +352,3 @@ local id = ARGV[1] | ||
`; | ||
var fixedWindowRemainingTokensScript = ` | ||
var fixedWindowRemainingTokensScript2 = ` | ||
local key = KEYS[1] | ||
@@ -218,3 +361,3 @@ local tokens = 0 | ||
`; | ||
var slidingWindowLimitScript = ` | ||
var slidingWindowLimitScript2 = ` | ||
local currentKey = KEYS[1] -- identifier including prefixes | ||
@@ -254,3 +397,3 @@ local previousKey = KEYS[2] -- key of the previous bucket | ||
`; | ||
var slidingWindowRemainingTokensScript = ` | ||
var slidingWindowRemainingTokensScript2 = ` | ||
local currentKey = KEYS[1] -- identifier including prefixes | ||
@@ -304,2 +447,74 @@ local previousKey = KEYS[2] -- key of the previous bucket | ||
// src/lua-scripts/hash.ts | ||
var SCRIPTS = { | ||
singleRegion: { | ||
fixedWindow: { | ||
limit: { | ||
script: fixedWindowLimitScript, | ||
hash: "b13943e359636db027ad280f1def143f02158c13" | ||
}, | ||
getRemaining: { | ||
script: fixedWindowRemainingTokensScript, | ||
hash: "8c4c341934502aee132643ffbe58ead3450e5208" | ||
} | ||
}, | ||
slidingWindow: { | ||
limit: { | ||
script: slidingWindowLimitScript, | ||
hash: "e1391e429b699c780eb0480350cd5b7280fd9213" | ||
}, | ||
getRemaining: { | ||
script: slidingWindowRemainingTokensScript, | ||
hash: "65a73ac5a05bf9712903bc304b77268980c1c417" | ||
} | ||
}, | ||
tokenBucket: { | ||
limit: { | ||
script: tokenBucketLimitScript, | ||
hash: "5bece90aeef8189a8cfd28995b479529e270b3c6" | ||
}, | ||
getRemaining: { | ||
script: tokenBucketRemainingTokensScript, | ||
hash: "a15be2bb1db2a15f7c82db06146f9d08983900d0" | ||
} | ||
}, | ||
cachedFixedWindow: { | ||
limit: { | ||
script: cachedFixedWindowLimitScript, | ||
hash: "c26b12703dd137939b9a69a3a9b18e906a2d940f" | ||
}, | ||
getRemaining: { | ||
script: cachedFixedWindowRemainingTokenScript, | ||
hash: "8e8f222ccae68b595ee6e3f3bf2199629a62b91a" | ||
} | ||
} | ||
}, | ||
multiRegion: { | ||
fixedWindow: { | ||
limit: { | ||
script: fixedWindowLimitScript2, | ||
hash: "a8c14f3835aa87bd70e5e2116081b81664abcf5c" | ||
}, | ||
getRemaining: { | ||
script: fixedWindowRemainingTokensScript2, | ||
hash: "8ab8322d0ed5fe5ac8eb08f0c2e4557f1b4816fd" | ||
} | ||
}, | ||
slidingWindow: { | ||
limit: { | ||
script: slidingWindowLimitScript2, | ||
hash: "cb4fdc2575056df7c6d422764df0de3a08d6753b" | ||
}, | ||
getRemaining: { | ||
script: slidingWindowRemainingTokensScript2, | ||
hash: "558c9306b7ec54abb50747fe0b17e5d44bd24868" | ||
} | ||
} | ||
} | ||
}; | ||
var RESET_SCRIPT = { | ||
script: resetScript, | ||
hash: "54bd274ddc59fb3be0f42deee2f64322a10e2b50" | ||
}; | ||
// src/types.ts | ||
@@ -744,5 +959,3 @@ var DenyListExtension = "denyList"; | ||
regionContexts: config.redis.map((redis) => ({ | ||
redis, | ||
scriptHashes: {}, | ||
cacheScripts: config.cacheScripts ?? true | ||
redis | ||
})), | ||
@@ -796,4 +1009,3 @@ cache: config.ephemeralCache ? new Cache(config.ephemeralCache) : void 0 | ||
regionContext, | ||
fixedWindowLimitScript, | ||
"limitHash", | ||
SCRIPTS.multiRegion.fixedWindow.limit, | ||
[key], | ||
@@ -873,4 +1085,3 @@ [requestId, windowDuration, incrementBy] | ||
regionContext, | ||
fixedWindowRemainingTokensScript, | ||
"getRemainingHash", | ||
SCRIPTS.multiRegion.fixedWindow.getRemaining, | ||
[key], | ||
@@ -901,4 +1112,3 @@ [null] | ||
regionContext, | ||
resetScript, | ||
"resetHash", | ||
RESET_SCRIPT, | ||
[pattern], | ||
@@ -956,4 +1166,3 @@ [null] | ||
regionContext, | ||
slidingWindowLimitScript, | ||
"limitHash", | ||
SCRIPTS.multiRegion.slidingWindow.limit, | ||
[currentKey, previousKey], | ||
@@ -1047,4 +1256,3 @@ [tokens, now, windowDuration, requestId, incrementBy] | ||
regionContext, | ||
slidingWindowRemainingTokensScript, | ||
"getRemainingHash", | ||
SCRIPTS.multiRegion.slidingWindow.getRemaining, | ||
[currentKey, previousKey], | ||
@@ -1069,4 +1277,3 @@ [now, windowSize] | ||
regionContext, | ||
resetScript, | ||
"resetHash", | ||
RESET_SCRIPT, | ||
[pattern], | ||
@@ -1081,158 +1288,2 @@ [null] | ||
// src/lua-scripts/single.ts | ||
var fixedWindowLimitScript2 = ` | ||
local key = KEYS[1] | ||
local window = ARGV[1] | ||
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1 | ||
local r = redis.call("INCRBY", key, incrementBy) | ||
if r == tonumber(incrementBy) then | ||
-- The first time this key is set, the value will be equal to incrementBy. | ||
-- So we only need the expire command once | ||
redis.call("PEXPIRE", key, window) | ||
end | ||
return r | ||
`; | ||
var fixedWindowRemainingTokensScript2 = ` | ||
local key = KEYS[1] | ||
local tokens = 0 | ||
local value = redis.call('GET', key) | ||
if value then | ||
tokens = value | ||
end | ||
return tokens | ||
`; | ||
var slidingWindowLimitScript2 = ` | ||
local currentKey = KEYS[1] -- identifier including prefixes | ||
local previousKey = KEYS[2] -- key of the previous bucket | ||
local tokens = tonumber(ARGV[1]) -- tokens per window | ||
local now = ARGV[2] -- current timestamp in milliseconds | ||
local window = ARGV[3] -- interval in milliseconds | ||
local incrementBy = ARGV[4] -- increment rate per request at a given value, default is 1 | ||
local requestsInCurrentWindow = redis.call("GET", currentKey) | ||
if requestsInCurrentWindow == false then | ||
requestsInCurrentWindow = 0 | ||
end | ||
local requestsInPreviousWindow = redis.call("GET", previousKey) | ||
if requestsInPreviousWindow == false then | ||
requestsInPreviousWindow = 0 | ||
end | ||
local percentageInCurrent = ( now % window ) / window | ||
-- weighted requests to consider from the previous window | ||
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow) | ||
if requestsInPreviousWindow + requestsInCurrentWindow >= tokens then | ||
return -1 | ||
end | ||
local newValue = redis.call("INCRBY", currentKey, incrementBy) | ||
if newValue == tonumber(incrementBy) then | ||
-- The first time this key is set, the value will be equal to incrementBy. | ||
-- So we only need the expire command once | ||
redis.call("PEXPIRE", currentKey, window * 2 + 1000) -- Enough time to overlap with a new window + 1 second | ||
end | ||
return tokens - ( newValue + requestsInPreviousWindow ) | ||
`; | ||
var slidingWindowRemainingTokensScript2 = ` | ||
local currentKey = KEYS[1] -- identifier including prefixes | ||
local previousKey = KEYS[2] -- key of the previous bucket | ||
local now = ARGV[1] -- current timestamp in milliseconds | ||
local window = ARGV[2] -- interval in milliseconds | ||
local requestsInCurrentWindow = redis.call("GET", currentKey) | ||
if requestsInCurrentWindow == false then | ||
requestsInCurrentWindow = 0 | ||
end | ||
local requestsInPreviousWindow = redis.call("GET", previousKey) | ||
if requestsInPreviousWindow == false then | ||
requestsInPreviousWindow = 0 | ||
end | ||
local percentageInCurrent = ( now % window ) / window | ||
-- weighted requests to consider from the previous window | ||
requestsInPreviousWindow = math.floor(( 1 - percentageInCurrent ) * requestsInPreviousWindow) | ||
return requestsInPreviousWindow + requestsInCurrentWindow | ||
`; | ||
var tokenBucketLimitScript = ` | ||
local key = KEYS[1] -- identifier including prefixes | ||
local maxTokens = tonumber(ARGV[1]) -- maximum number of tokens | ||
local interval = tonumber(ARGV[2]) -- size of the window in milliseconds | ||
local refillRate = tonumber(ARGV[3]) -- how many tokens are refilled after each interval | ||
local now = tonumber(ARGV[4]) -- current timestamp in milliseconds | ||
local incrementBy = tonumber(ARGV[5]) -- how many tokens to consume, default is 1 | ||
local bucket = redis.call("HMGET", key, "refilledAt", "tokens") | ||
local refilledAt | ||
local tokens | ||
if bucket[1] == false then | ||
refilledAt = now | ||
tokens = maxTokens | ||
else | ||
refilledAt = tonumber(bucket[1]) | ||
tokens = tonumber(bucket[2]) | ||
end | ||
if now >= refilledAt + interval then | ||
local numRefills = math.floor((now - refilledAt) / interval) | ||
tokens = math.min(maxTokens, tokens + numRefills * refillRate) | ||
refilledAt = refilledAt + numRefills * interval | ||
end | ||
if tokens == 0 then | ||
return {-1, refilledAt + interval} | ||
end | ||
local remaining = tokens - incrementBy | ||
local expireAt = math.ceil(((maxTokens - remaining) / refillRate)) * interval | ||
redis.call("HSET", key, "refilledAt", refilledAt, "tokens", remaining) | ||
redis.call("PEXPIRE", key, expireAt) | ||
return {remaining, refilledAt + interval} | ||
`; | ||
var tokenBucketIdentifierNotFound = -1; | ||
var tokenBucketRemainingTokensScript = ` | ||
local key = KEYS[1] | ||
local maxTokens = tonumber(ARGV[1]) | ||
local bucket = redis.call("HMGET", key, "refilledAt", "tokens") | ||
if bucket[1] == false then | ||
return {maxTokens, ${tokenBucketIdentifierNotFound}} | ||
end | ||
return {tonumber(bucket[2]), tonumber(bucket[1])} | ||
`; | ||
var cachedFixedWindowLimitScript = ` | ||
local key = KEYS[1] | ||
local window = ARGV[1] | ||
local incrementBy = ARGV[2] -- increment rate per request at a given value, default is 1 | ||
local r = redis.call("INCRBY", key, incrementBy) | ||
if r == incrementBy then | ||
-- The first time this key is set, the value will be equal to incrementBy. | ||
-- So we only need the expire command once | ||
redis.call("PEXPIRE", key, window) | ||
end | ||
return r | ||
`; | ||
var cachedFixedWindowRemainingTokenScript = ` | ||
local key = KEYS[1] | ||
local tokens = 0 | ||
local value = redis.call('GET', key) | ||
if value then | ||
tokens = value | ||
end | ||
return tokens | ||
`; | ||
// src/single.ts | ||
@@ -1250,5 +1301,3 @@ var RegionRatelimit = class extends Ratelimit { | ||
ctx: { | ||
redis: config.redis, | ||
scriptHashes: {}, | ||
cacheScripts: config.cacheScripts ?? true | ||
redis: config.redis | ||
}, | ||
@@ -1300,4 +1349,3 @@ ephemeralCache: config.ephemeralCache, | ||
ctx, | ||
fixedWindowLimitScript2, | ||
"limitHash", | ||
SCRIPTS.singleRegion.fixedWindow.limit, | ||
[key], | ||
@@ -1325,4 +1373,3 @@ [windowDuration, incrementBy] | ||
ctx, | ||
fixedWindowRemainingTokensScript2, | ||
"getRemainingHash", | ||
SCRIPTS.singleRegion.fixedWindow.getRemaining, | ||
[key], | ||
@@ -1343,4 +1390,3 @@ [null] | ||
ctx, | ||
resetScript, | ||
"resetHash", | ||
RESET_SCRIPT, | ||
[pattern], | ||
@@ -1393,4 +1439,3 @@ [null] | ||
ctx, | ||
slidingWindowLimitScript2, | ||
"limitHash", | ||
SCRIPTS.singleRegion.slidingWindow.limit, | ||
[currentKey, previousKey], | ||
@@ -1420,4 +1465,3 @@ [tokens, now, windowSize, incrementBy] | ||
ctx, | ||
slidingWindowRemainingTokensScript2, | ||
"getRemainingHash", | ||
SCRIPTS.singleRegion.slidingWindow.getRemaining, | ||
[currentKey, previousKey], | ||
@@ -1438,4 +1482,3 @@ [now, windowSize] | ||
ctx, | ||
resetScript, | ||
"resetHash", | ||
RESET_SCRIPT, | ||
[pattern], | ||
@@ -1481,4 +1524,3 @@ [null] | ||
ctx, | ||
tokenBucketLimitScript, | ||
"limitHash", | ||
SCRIPTS.singleRegion.tokenBucket.limit, | ||
[identifier], | ||
@@ -1502,4 +1544,3 @@ [maxTokens, intervalDuration, refillRate, now, incrementBy] | ||
ctx, | ||
tokenBucketRemainingTokensScript, | ||
"getRemainingHash", | ||
SCRIPTS.singleRegion.tokenBucket.getRemaining, | ||
[identifier], | ||
@@ -1522,4 +1563,3 @@ [maxTokens] | ||
ctx, | ||
resetScript, | ||
"resetHash", | ||
RESET_SCRIPT, | ||
[pattern], | ||
@@ -1572,4 +1612,3 @@ [null] | ||
ctx, | ||
cachedFixedWindowLimitScript, | ||
"limitHash", | ||
SCRIPTS.singleRegion.cachedFixedWindow.limit, | ||
[key], | ||
@@ -1588,4 +1627,3 @@ [windowDuration, incrementBy] | ||
ctx, | ||
cachedFixedWindowLimitScript, | ||
"limitHash", | ||
SCRIPTS.singleRegion.cachedFixedWindow.limit, | ||
[key], | ||
@@ -1620,4 +1658,3 @@ [windowDuration, incrementBy] | ||
ctx, | ||
cachedFixedWindowRemainingTokenScript, | ||
"getRemainingHash", | ||
SCRIPTS.singleRegion.cachedFixedWindow.getRemaining, | ||
[key], | ||
@@ -1641,4 +1678,3 @@ [null] | ||
ctx, | ||
resetScript, | ||
"resetHash", | ||
RESET_SCRIPT, | ||
[pattern], | ||
@@ -1645,0 +1681,0 @@ [null] |
@@ -1,1 +0,1 @@ | ||
{ "name": "@upstash/ratelimit", "version": "v2.0.2", "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.10" } } | ||
{ "name": "@upstash/ratelimit", "version": "v2.0.3", "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.10" } } |
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
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
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
386142
3932
4