Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@trpc-rate-limiter/cloudflare

Package Overview
Dependencies
Maintainers
1
Versions
5
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@trpc-rate-limiter/cloudflare - npm Package Compare versions

Comparing version
0.1.1
to
0.1.2
.swcrc

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

+29
{
"name": "cloudflare",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/cloudflare/src",
"projectType": "library",
"release": {
"version": {
"generatorOptions": {
"packageRoot": "dist/{projectRoot}",
"currentVersionResolver": "git-tag"
}
}
},
"tags": [],
"targets": {
"nx-release-publish": {
"options": {
"packageRoot": "dist/{projectRoot}"
}
},
"test": {
"executor": "@nx/vite:test",
"outputs": ["{options.reportsDirectory}"],
"options": {
"reportsDirectory": "../../coverage/packages/cloudflare"
}
}
}
}
const { withNx } = require("@nx/rollup/with-nx");
const terser = require("@rollup/plugin-terser");
const pkg = require("./package.json");
module.exports = withNx(
{
main: "./src/index.ts",
outputPath: "../../dist/packages/cloudflare",
tsConfig: "./tsconfig.lib.json",
compiler: "swc",
format: ["cjs", "esm"],
assets: [
{ input: "./packages/cloudflare", output: ".", glob: "README.md" },
],
},
{
plugins: [terser()],
},
);
export * from './stores'
export * from './types'
import { DurableObject } from 'cloudflare:workers'
import type { ClientRateLimitInfo } from '@trpc-rate-limiter/hono'
const initialState: ClientRateLimitInfo = {
totalHits: 0,
}
export class DurableObjectRateLimiter extends DurableObject {
value() {
return this.ctx.storage.get<ClientRateLimitInfo>('value')
}
async update(hits: number, windowMs: number) {
let payload = (await this.ctx.storage.get<ClientRateLimitInfo>('value')) || initialState
let alarmTimestamp = await this.ctx.storage.getAlarm()
const currentTime = Date.now()
if (!alarmTimestamp) {
alarmTimestamp = currentTime + windowMs
await this.ctx.storage.setAlarm(alarmTimestamp)
}
// if windowMs is changed while the alarm is active, the alarm will not be reset (bug)
if (alarmTimestamp <= currentTime) {
await this.reset()
payload = { totalHits: 0 }
alarmTimestamp = currentTime + windowMs
await this.ctx.storage.setAlarm(alarmTimestamp)
}
payload = {
totalHits: payload.totalHits + hits,
resetTime: new Date(alarmTimestamp),
}
await this.ctx.storage.put('value', payload)
return payload
}
async reset() {
await this.ctx.storage.put('value', initialState)
}
override async alarm() {
await this.reset()
}
}
import type { DurableObjectNamespace } from '@cloudflare/workers-types'
import type { ClientRateLimitInfo, InitStoreOptions, Store } from '@trpc-rate-limiter/hono'
import type { Options } from '../types'
import type { DurableObjectRateLimiter } from './DurableObjectClass'
export class DurableObjectStore implements Store {
/**
* The text to prepend to the key in Redis.
*/
prefix: string
/**
* The Durable Object namespace to use.
*/
namespace: DurableObjectNamespace<DurableObjectRateLimiter>
/**
* The number of milliseconds to remember that user's requests.
*/
windowMs!: number
/**
* @constructor for `DurableObjectStore`.
*
* @param options {Options} - The configuration options for the store.
*/
constructor(options: Options<DurableObjectNamespace<DurableObjectRateLimiter>>) {
this.namespace = options.namespace
this.prefix = options.prefix ?? 'hrl:'
}
/**
* Method to prefix the keys with the given text and return a `DurableObjectId`.
*
* @param key {string} - The key.
*
* @returns {DurableObjectId} - The text + the key.
*/
prefixKey(key: string): DurableObjectId {
return this.namespace.idFromName(`${this.prefix}${key}`)
}
/**
* Method that actually initializes the store.
*
* @param options {RateLimitConfiguration} - The options used to setup the middleware.
*/
init(options: InitStoreOptions) {
this.windowMs = options.windowMs
}
/**
* Method to fetch a client's hit count and reset time.
*
* @param key {string} - The identifier for a client.
*
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
*/
async get(key: string): Promise<ClientRateLimitInfo | undefined> {
return (await this.namespace.get(this.prefixKey(key)).value()) as
| ClientRateLimitInfo
| undefined
}
/**
* Method to increment a client's hit counter.
*
* @param key {string} - The identifier for a client
*
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client
*/
async increment(key: string): Promise<ClientRateLimitInfo> {
return (await this.namespace
.get(this.prefixKey(key))
.update(1, this.windowMs)) as ClientRateLimitInfo
}
/**
* Method to decrement a client's hit counter.
*
* @param key {string} - The identifier for a client
*/
async decrement(key: string): Promise<void> {
await this.namespace.get(this.prefixKey(key)).update(-1, this.windowMs)
}
/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client
*/
async resetKey(key: string): Promise<void> {
await this.namespace.get(this.prefixKey(key)).reset()
}
}
export * from "./KVStore";
export * from "./DurableObjectStore";
export * from "./DurableObjectClass";
import type { ClientRateLimitInfo, InitStoreOptions, Store } from '@trpc-rate-limiter/hono'
import type { Options } from '../types'
export class WorkersKVStore implements Store {
/**
* Expiration targets that are less than 60 seconds into the future are not supported. This is true for both expiration methods.
*
* see: https://developers.cloudflare.com/kv/api/write-key-value-pairs/#expiring-keys
*
*/
private static readonly KV_MIN_EXPIRATION_BUFFER = 60
/**
* The text to prepend to the key in Redis.
*/
prefix: string
/**
* The KV namespace to use.
*/
namespace: KVNamespace
/**
* The number of milliseconds to remember that user's requests.
*/
windowMs!: number
/**
* @constructor for `WorkersKVStore`.
*
* @param options {Options} - The configuration options for the store.
*/
constructor(options: Options<KVNamespace>) {
this.namespace = options.namespace
this.prefix = options.prefix ?? 'hrl:'
}
/**
* Method to prefix the keys with the given text.
*
* @param key {string} - The key.
*
* @returns {string} - The text + the key.
*/
prefixKey(key: string): string {
return `${this.prefix}${key}`
}
/**
* Method that actually initializes the store.
*
* @param options {RateLimitConfiguration} - The options used to setup the middleware.
*/
init(options: InitStoreOptions) {
this.windowMs = options.windowMs
}
/**
* Method to fetch a client's hit count and reset time.
*
* @param key {string} - The identifier for a client.
*
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
*/
async get(key: string): Promise<ClientRateLimitInfo | undefined> {
const result = await this.namespace.get<ClientRateLimitInfo>(this.prefixKey(key), 'json')
if (result) return result
return undefined
}
/**
* Method to increment a client's hit counter. If the current time is within an active window,
* it increments the existing hit count. Otherwise, it starts a new window with a hit count of 1.
*
* @param key {string} - The identifier for a client
*
* @returns {ClientRateLimitInfo} - An object containing:
* - totalHits: The updated number of hits for the client
* - resetTime: The time when the current rate limit window expires
*/
async increment(key: string): Promise<ClientRateLimitInfo> {
const nowMS = Date.now()
const record = await this.get(key)
const defaultResetTime = new Date(nowMS + this.windowMs)
const existingResetTimeMS = record?.resetTime && new Date(record.resetTime).getTime()
const isActiveWindow = existingResetTimeMS && existingResetTimeMS > nowMS
const payload: ClientRateLimitInfo = {
totalHits: isActiveWindow ? record.totalHits + 1 : 1,
resetTime:
isActiveWindow && existingResetTimeMS ? new Date(existingResetTimeMS) : defaultResetTime,
}
await this.updateRecord(key, payload)
return payload
}
/**
* Method to decrement a client's hit counter. Only decrements if there is an active time window.
* The hit counter will never go below 0.
*
* @param key {string} - The identifier for a client
* @returns {Promise<void>} - Returns void after attempting to decrement the counter
*/
async decrement(key: string): Promise<void> {
const nowMS = Date.now()
const record = await this.get(key)
const existingResetTimeMS = record?.resetTime && new Date(record.resetTime).getTime()
const isActiveWindow = existingResetTimeMS && existingResetTimeMS > nowMS
// Only decrement if in active window
if (isActiveWindow && record) {
const payload: ClientRateLimitInfo = {
totalHits: Math.max(0, record.totalHits - 1), // Never go below 0
resetTime: new Date(existingResetTimeMS),
}
await this.updateRecord(key, payload)
}
return
}
/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client
*/
async resetKey(key: string): Promise<void> {
await this.namespace.delete(this.prefixKey(key))
}
/**
* Method to calculate expiration.
*
* @param resetTime {Date} - The reset time.
*
* @returns {number} - The expiration in seconds.
*
* Note: KV expiration is always set to 60s after resetTime or nowSeconds to meet Cloudflare's minimum requirement.
* This doesn't affect rate limiting behavior which is controlled by resetTime.
*/
private calculateExpiration(resetTime: Date): number {
const resetTimeSeconds = Math.floor(resetTime.getTime() / 1000)
const nowSeconds = Math.floor(Date.now() / 1000)
return Math.max(resetTimeSeconds, nowSeconds) + WorkersKVStore.KV_MIN_EXPIRATION_BUFFER
}
/**
* Method to update a record.
*
* @param key {string} - The identifier for a client.
* @param payload {ClientRateLimitInfo} - The payload to update.
*/
private async updateRecord(key: string, payload: ClientRateLimitInfo): Promise<void> {
await this.namespace.put(this.prefixKey(key), JSON.stringify(payload), {
expiration: this.calculateExpiration(payload.resetTime as Date),
})
}
}
/**
* The configuration options for the store.
*/
export type Options<Binding> = {
/**
* The KV namespace to use.
*/
namespace: Binding
/**
* The text to prepend to the key in Redis.
*/
readonly prefix?: string
}
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "esnext",
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node", "@cloudflare/workers-types/2023-07-01"]
},
"include": ["src/**/*.ts"],
"exclude": [
"vite.config.ts",
"src/**/*.spec.ts",
"src/**/*.test.ts",
"./**/__tests__/*"
]
}
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest",
"@cloudflare/workers-types/2023-07-01"
]
},
"include": [
"vite.config.ts",
"vitest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
]
}
import { defineConfig } from "vite";
import { nxViteTsPaths } from "@nx/vite/plugins/nx-tsconfig-paths.plugin";
export default defineConfig({
root: __dirname,
cacheDir: "../../node_modules/.vite/packages/cloudflare",
plugins: [nxViteTsPaths()],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
test: {
globals: true,
cache: { dir: "../../node_modules/.vitest" },
environment: "node",
include: ["src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}"],
reporters: ["default"],
coverage: {
reportsDirectory: "../../coverage/packages/cloudflare",
provider: "v8",
},
},
});
+3
-5
{
"name": "@trpc-rate-limiter/cloudflare",
"description": "Cloudflare stores for trpc-rate-limiter.",
"version": "0.1.1",
"version": "0.1.2",
"license": "MIT",

@@ -35,5 +35,3 @@ "keywords": [

},
"types": "./index.cjs.d.ts",
"module": "./index.esm.js",
"main": "./index.cjs.js"
}
"types": "./index.cjs.d.ts"
}
export * from "./src/index";
"use strict";var e=require("cloudflare:workers");class t{static{this.KV_MIN_EXPIRATION_BUFFER=60}constructor(e){this.namespace=e.namespace,this.prefix=e.prefix??"hrl:"}prefixKey(e){return`${this.prefix}${e}`}init(e){this.windowMs=e.windowMs}async get(e){const t=await this.namespace.get(this.prefixKey(e),"json");if(t)return t}async increment(e){const t=Date.now(),a=await this.get(e),s=new Date(t+this.windowMs),i=a?.resetTime&&new Date(a.resetTime).getTime(),r=i&&i>t,n={totalHits:r?a.totalHits+1:1,resetTime:r&&i?new Date(i):s};return await this.updateRecord(e,n),n}async decrement(e){const t=Date.now(),a=await this.get(e),s=a?.resetTime&&new Date(a.resetTime).getTime();if(s&&s>t&&a){const t={totalHits:Math.max(0,a.totalHits-1),resetTime:new Date(s)};await this.updateRecord(e,t)}}async resetKey(e){await this.namespace.delete(this.prefixKey(e))}calculateExpiration(e){const a=Math.floor(e.getTime()/1e3),s=Math.floor(Date.now()/1e3);return Math.max(a,s)+t.KV_MIN_EXPIRATION_BUFFER}async updateRecord(e,t){await this.namespace.put(this.prefixKey(e),JSON.stringify(t),{expiration:this.calculateExpiration(t.resetTime)})}}const a={totalHits:0};class s extends e.DurableObject{value(){return this.ctx.storage.get("value")}async update(e,t){let s=await this.ctx.storage.get("value")||a;const i=new Date(s.resetTime??Date.now()+t);s={totalHits:s.totalHits+e,resetTime:i};return null==await this.ctx.storage.getAlarm()&&this.ctx.storage.setAlarm(i.getTime()),await this.ctx.storage.put("value",s),s}async reset(){await this.ctx.storage.put("value",a)}async alarm(){await this.reset()}}exports.DurableObjectRateLimiter=s,exports.DurableObjectStore=class{constructor(e){this.namespace=e.namespace,this.prefix=e.prefix??"hrl:"}prefixKey(e){return this.namespace.idFromName(`${this.prefix}${e}`)}init(e){this.windowMs=e.windowMs}async get(e){return await this.namespace.get(this.prefixKey(e)).value()}async increment(e){return await this.namespace.get(this.prefixKey(e)).update(1,this.windowMs)}async decrement(e){await this.namespace.get(this.prefixKey(e)).update(-1,this.windowMs)}async resetKey(e){await this.namespace.get(this.prefixKey(e)).reset()}},exports.WorkersKVStore=t;
export * from "./src/index";
import{DurableObject as t}from"cloudflare:workers";class e{static{this.KV_MIN_EXPIRATION_BUFFER=60}constructor(t){this.namespace=t.namespace,this.prefix=t.prefix??"hrl:"}prefixKey(t){return`${this.prefix}${t}`}init(t){this.windowMs=t.windowMs}async get(t){const e=await this.namespace.get(this.prefixKey(t),"json");if(e)return e}async increment(t){const e=Date.now(),a=await this.get(t),s=new Date(e+this.windowMs),i=a?.resetTime&&new Date(a.resetTime).getTime(),r=i&&i>e,n={totalHits:r?a.totalHits+1:1,resetTime:r&&i?new Date(i):s};return await this.updateRecord(t,n),n}async decrement(t){const e=Date.now(),a=await this.get(t),s=a?.resetTime&&new Date(a.resetTime).getTime();if(s&&s>e&&a){const e={totalHits:Math.max(0,a.totalHits-1),resetTime:new Date(s)};await this.updateRecord(t,e)}}async resetKey(t){await this.namespace.delete(this.prefixKey(t))}calculateExpiration(t){const a=Math.floor(t.getTime()/1e3),s=Math.floor(Date.now()/1e3);return Math.max(a,s)+e.KV_MIN_EXPIRATION_BUFFER}async updateRecord(t,e){await this.namespace.put(this.prefixKey(t),JSON.stringify(e),{expiration:this.calculateExpiration(e.resetTime)})}}class a{constructor(t){this.namespace=t.namespace,this.prefix=t.prefix??"hrl:"}prefixKey(t){return this.namespace.idFromName(`${this.prefix}${t}`)}init(t){this.windowMs=t.windowMs}async get(t){return await this.namespace.get(this.prefixKey(t)).value()}async increment(t){return await this.namespace.get(this.prefixKey(t)).update(1,this.windowMs)}async decrement(t){await this.namespace.get(this.prefixKey(t)).update(-1,this.windowMs)}async resetKey(t){await this.namespace.get(this.prefixKey(t)).reset()}}const s={totalHits:0};class i extends t{value(){return this.ctx.storage.get("value")}async update(t,e){let a=await this.ctx.storage.get("value")||s;const i=new Date(a.resetTime??Date.now()+e);a={totalHits:a.totalHits+t,resetTime:i};return null==await this.ctx.storage.getAlarm()&&this.ctx.storage.setAlarm(i.getTime()),await this.ctx.storage.put("value",a),a}async reset(){await this.ctx.storage.put("value",s)}async alarm(){await this.reset()}}export{i as DurableObjectRateLimiter,a as DurableObjectStore,e as WorkersKVStore};
export * from './stores';
export * from './types';
/// <reference types="@cloudflare/workers-types/2023-07-01" />
import { DurableObject } from 'cloudflare:workers';
import type { ClientRateLimitInfo } from '@trpc-rate-limiter/hono';
export declare class DurableObjectRateLimiter extends DurableObject {
value(): Promise<ClientRateLimitInfo | undefined>;
update(hits: number, windowMs: number): Promise<ClientRateLimitInfo>;
reset(): Promise<void>;
alarm(): Promise<void>;
}
/// <reference types="@cloudflare/workers-types/2023-07-01" />
import type { DurableObjectNamespace } from '@cloudflare/workers-types';
import type { ClientRateLimitInfo, InitStoreOptions, Store } from '@trpc-rate-limiter/hono';
import type { Options } from '../types';
import type { DurableObjectRateLimiter } from './DurableObjectClass';
export declare class DurableObjectStore implements Store {
/**
* The text to prepend to the key in Redis.
*/
prefix: string;
/**
* The Durable Object namespace to use.
*/
namespace: DurableObjectNamespace<DurableObjectRateLimiter>;
/**
* The number of milliseconds to remember that user's requests.
*/
windowMs: number;
/**
* @constructor for `DurableObjectStore`.
*
* @param options {Options} - The configuration options for the store.
*/
constructor(options: Options<DurableObjectNamespace<DurableObjectRateLimiter>>);
/**
* Method to prefix the keys with the given text and return a `DurableObjectId`.
*
* @param key {string} - The key.
*
* @returns {DurableObjectId} - The text + the key.
*/
prefixKey(key: string): DurableObjectId;
/**
* Method that actually initializes the store.
*
* @param options {RateLimitConfiguration} - The options used to setup the middleware.
*/
init(options: InitStoreOptions): void;
/**
* Method to fetch a client's hit count and reset time.
*
* @param key {string} - The identifier for a client.
*
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
*/
get(key: string): Promise<ClientRateLimitInfo | undefined>;
/**
* Method to increment a client's hit counter.
*
* @param key {string} - The identifier for a client
*
* @returns {ClientRateLimitInfo} - The number of hits and reset time for that client
*/
increment(key: string): Promise<ClientRateLimitInfo>;
/**
* Method to decrement a client's hit counter.
*
* @param key {string} - The identifier for a client
*/
decrement(key: string): Promise<void>;
/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client
*/
resetKey(key: string): Promise<void>;
}
export * from "./KVStore";
export * from "./DurableObjectStore";
export * from "./DurableObjectClass";
/// <reference types="@cloudflare/workers-types/2023-07-01" />
import type { ClientRateLimitInfo, InitStoreOptions, Store } from '@trpc-rate-limiter/hono';
import type { Options } from '../types';
export declare class WorkersKVStore implements Store {
/**
* Expiration targets that are less than 60 seconds into the future are not supported. This is true for both expiration methods.
*
* see: https://developers.cloudflare.com/kv/api/write-key-value-pairs/#expiring-keys
*
*/
private static readonly KV_MIN_EXPIRATION_BUFFER;
/**
* The text to prepend to the key in Redis.
*/
prefix: string;
/**
* The KV namespace to use.
*/
namespace: KVNamespace;
/**
* The number of milliseconds to remember that user's requests.
*/
windowMs: number;
/**
* @constructor for `WorkersKVStore`.
*
* @param options {Options} - The configuration options for the store.
*/
constructor(options: Options<KVNamespace>);
/**
* Method to prefix the keys with the given text.
*
* @param key {string} - The key.
*
* @returns {string} - The text + the key.
*/
prefixKey(key: string): string;
/**
* Method that actually initializes the store.
*
* @param options {RateLimitConfiguration} - The options used to setup the middleware.
*/
init(options: InitStoreOptions): void;
/**
* Method to fetch a client's hit count and reset time.
*
* @param key {string} - The identifier for a client.
*
* @returns {ClientRateLimitInfo | undefined} - The number of hits and reset time for that client.
*/
get(key: string): Promise<ClientRateLimitInfo | undefined>;
/**
* Method to increment a client's hit counter. If the current time is within an active window,
* it increments the existing hit count. Otherwise, it starts a new window with a hit count of 1.
*
* @param key {string} - The identifier for a client
*
* @returns {ClientRateLimitInfo} - An object containing:
* - totalHits: The updated number of hits for the client
* - resetTime: The time when the current rate limit window expires
*/
increment(key: string): Promise<ClientRateLimitInfo>;
/**
* Method to decrement a client's hit counter. Only decrements if there is an active time window.
* The hit counter will never go below 0.
*
* @param key {string} - The identifier for a client
* @returns {Promise<void>} - Returns void after attempting to decrement the counter
*/
decrement(key: string): Promise<void>;
/**
* Method to reset a client's hit counter.
*
* @param key {string} - The identifier for a client
*/
resetKey(key: string): Promise<void>;
/**
* Method to calculate expiration.
*
* @param resetTime {Date} - The reset time.
*
* @returns {number} - The expiration in seconds.
*
* Note: KV expiration is always set to 60s after resetTime or nowSeconds to meet Cloudflare's minimum requirement.
* This doesn't affect rate limiting behavior which is controlled by resetTime.
*/
private calculateExpiration;
/**
* Method to update a record.
*
* @param key {string} - The identifier for a client.
* @param payload {ClientRateLimitInfo} - The payload to update.
*/
private updateRecord;
}
/**
* The configuration options for the store.
*/
export type Options<Binding> = {
/**
* The KV namespace to use.
*/
namespace: Binding;
/**
* The text to prepend to the key in Redis.
*/
readonly prefix?: string;
};