@thirdweb-dev/service-utils 0.0.0-dev-8dbcee9-20230925135804 → 0.0.0-dev-0231c73-20230926212531

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var index = require('../../dist/index-807f6a60.cjs.dev.js');
5
+ var index = require('../../dist/index-6a597a58.cjs.dev.js');
6
6
  var aws4fetch = require('aws4fetch');
7
7
  var zod = require('zod');
8
8
  var services = require('../../dist/services-79b4664f.cjs.dev.js');
@@ -91,8 +91,6 @@ async function publishUsageEvents(usageEvents, config) {
91
91
  }
92
92
 
93
93
  const DEFAULT_CACHE_TTL_SECONDS = 60;
94
- // must be > DEFAULT_RATE_LIMIT_WINDOW_SECONDS
95
- const DEFAULT_RATE_LIMIT_CACHE_TTL_SECONDS = 60;
96
94
  async function authorizeWorker(authInput, serviceConfig) {
97
95
  let authData;
98
96
  try {
@@ -124,14 +122,6 @@ async function authorizeWorker(authInput, serviceConfig) {
124
122
  cacheTtlSeconds: serviceConfig.cacheTtlSeconds ?? DEFAULT_CACHE_TTL_SECONDS
125
123
  });
126
124
  }
127
- async function rateLimitWorker(authzResult, serviceConfig) {
128
- return await index.rateLimit(authzResult, serviceConfig, {
129
- get: async bucketId => serviceConfig.kvStore.get(bucketId),
130
- put: (bucketId, count) => serviceConfig.kvStore.put(bucketId, count, {
131
- expirationTtl: DEFAULT_RATE_LIMIT_CACHE_TTL_SECONDS
132
- })
133
- });
134
- }
135
125
  async function extractAuthorizationData(authInput) {
136
126
  const requestUrl = new URL(authInput.req.url);
137
127
  const headers = authInput.req.headers;
@@ -259,4 +249,3 @@ exports.extractAuthorizationData = extractAuthorizationData;
259
249
  exports.hashSecretKey = hashSecretKey;
260
250
  exports.logHttpRequest = logHttpRequest;
261
251
  exports.publishUsageEvents = publishUsageEvents;
262
- exports.rateLimitWorker = rateLimitWorker;
@@ -2,7 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var index = require('../../dist/index-cfc8027b.cjs.prod.js');
5
+ var index = require('../../dist/index-30f0f58c.cjs.prod.js');
6
6
  var aws4fetch = require('aws4fetch');
7
7
  var zod = require('zod');
8
8
  var services = require('../../dist/services-04997839.cjs.prod.js');
@@ -91,8 +91,6 @@ async function publishUsageEvents(usageEvents, config) {
91
91
  }
92
92
 
93
93
  const DEFAULT_CACHE_TTL_SECONDS = 60;
94
- // must be > DEFAULT_RATE_LIMIT_WINDOW_SECONDS
95
- const DEFAULT_RATE_LIMIT_CACHE_TTL_SECONDS = 60;
96
94
  async function authorizeWorker(authInput, serviceConfig) {
97
95
  let authData;
98
96
  try {
@@ -124,14 +122,6 @@ async function authorizeWorker(authInput, serviceConfig) {
124
122
  cacheTtlSeconds: serviceConfig.cacheTtlSeconds ?? DEFAULT_CACHE_TTL_SECONDS
125
123
  });
126
124
  }
127
- async function rateLimitWorker(authzResult, serviceConfig) {
128
- return await index.rateLimit(authzResult, serviceConfig, {
129
- get: async bucketId => serviceConfig.kvStore.get(bucketId),
130
- put: (bucketId, count) => serviceConfig.kvStore.put(bucketId, count, {
131
- expirationTtl: DEFAULT_RATE_LIMIT_CACHE_TTL_SECONDS
132
- })
133
- });
134
- }
135
125
  async function extractAuthorizationData(authInput) {
136
126
  const requestUrl = new URL(authInput.req.url);
137
127
  const headers = authInput.req.headers;
@@ -259,4 +249,3 @@ exports.extractAuthorizationData = extractAuthorizationData;
259
249
  exports.hashSecretKey = hashSecretKey;
260
250
  exports.logHttpRequest = logHttpRequest;
261
251
  exports.publishUsageEvents = publishUsageEvents;
262
- exports.rateLimitWorker = rateLimitWorker;
@@ -1,5 +1,5 @@
1
- import { a as authorize, r as rateLimit } from '../../dist/index-bcf68113.esm.js';
2
- export { r as rateLimit, u as usageLimit } from '../../dist/index-bcf68113.esm.js';
1
+ import { a as authorize } from '../../dist/index-cd8f49a9.esm.js';
2
+ export { r as rateLimit, u as usageLimit } from '../../dist/index-cd8f49a9.esm.js';
3
3
  import { AwsClient } from 'aws4fetch';
4
4
  import { z } from 'zod';
5
5
  export { b as SERVICES, S as SERVICE_DEFINITIONS, a as SERVICE_NAMES, g as getServiceByName } from '../../dist/services-bc12a5f6.esm.js';
@@ -88,8 +88,6 @@ async function publishUsageEvents(usageEvents, config) {
88
88
  }
89
89
 
90
90
  const DEFAULT_CACHE_TTL_SECONDS = 60;
91
- // must be > DEFAULT_RATE_LIMIT_WINDOW_SECONDS
92
- const DEFAULT_RATE_LIMIT_CACHE_TTL_SECONDS = 60;
93
91
  async function authorizeWorker(authInput, serviceConfig) {
94
92
  let authData;
95
93
  try {
@@ -121,14 +119,6 @@ async function authorizeWorker(authInput, serviceConfig) {
121
119
  cacheTtlSeconds: serviceConfig.cacheTtlSeconds ?? DEFAULT_CACHE_TTL_SECONDS
122
120
  });
123
121
  }
124
- async function rateLimitWorker(authzResult, serviceConfig) {
125
- return await rateLimit(authzResult, serviceConfig, {
126
- get: async bucketId => serviceConfig.kvStore.get(bucketId),
127
- put: (bucketId, count) => serviceConfig.kvStore.put(bucketId, count, {
128
- expirationTtl: DEFAULT_RATE_LIMIT_CACHE_TTL_SECONDS
129
- })
130
- });
131
- }
132
122
  async function extractAuthorizationData(authInput) {
133
123
  const requestUrl = new URL(authInput.req.url);
134
124
  const headers = authInput.req.headers;
@@ -244,4 +234,4 @@ async function logHttpRequest(_ref) {
244
234
  }
245
235
  }
246
236
 
247
- export { authorizeWorker, deriveClientIdFromSecretKeyHash, extractAuthorizationData, hashSecretKey, logHttpRequest, publishUsageEvents, rateLimitWorker };
237
+ export { authorizeWorker, deriveClientIdFromSecretKeyHash, extractAuthorizationData, hashSecretKey, logHttpRequest, publishUsageEvents };
@@ -3,7 +3,6 @@ import type { CoreServiceConfig } from "../core/api";
3
3
  import type { Request } from "@cloudflare/workers-types";
4
4
  import type { AuthorizationInput } from "../core/authorize";
5
5
  import type { AuthorizationResult } from "../core/authorize/types";
6
- import type { RateLimitResult } from "../core/rateLimit/types";
7
6
  import type { CoreAuthInput } from "../core/types";
8
7
  export * from "./usage";
9
8
  export * from "../core/services";
@@ -18,7 +17,6 @@ type AuthInput = CoreAuthInput & {
18
17
  req: Request;
19
18
  };
20
19
  export declare function authorizeWorker(authInput: AuthInput, serviceConfig: WorkerServiceConfig): Promise<AuthorizationResult>;
21
- export declare function rateLimitWorker(authzResult: AuthorizationResult, serviceConfig: WorkerServiceConfig): Promise<RateLimitResult>;
22
20
  export declare function extractAuthorizationData(authInput: AuthInput): Promise<AuthorizationInput>;
23
21
  export declare function hashSecretKey(secretKey: string): Promise<string>;
24
22
  export declare function deriveClientIdFromSecretKeyHash(secretKeyHash: string): string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"../../../../src/cf-worker","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,WAAW,EACX,QAAQ,EACT,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAGV,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAGrB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAGnD,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AAEnC,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,GAAG;IACpD,OAAO,EAAE,WAAW,CAAC;IACrB,GAAG,EAAE,gBAAgB,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAMF,KAAK,SAAS,GAAG,aAAa,GAAG;IAC/B,GAAG,EAAE,OAAO,CAAC;CACd,CAAC;AAEF,wBAAsB,eAAe,CACnC,SAAS,EAAE,SAAS,EACpB,aAAa,EAAE,mBAAmB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CA0C9B;AAED,wBAAsB,eAAe,CACnC,WAAW,EAAE,mBAAmB,EAChC,aAAa,EAAE,mBAAmB,GACjC,OAAO,CAAC,eAAe,CAAC,CAQ1B;AAED,wBAAsB,wBAAwB,CAC5C,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,kBAAkB,CAAC,CA2E7B;AAED,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,mBAIpD;AAED,wBAAgB,+BAA+B,CAAC,aAAa,EAAE,MAAM,UAEpE;AAQD,wBAAsB,cAAc,CAAC,EACnC,MAAM,EACN,QAAQ,EACR,GAAG,EACH,GAAG,EACH,QAAQ,EACR,aAAa,GACd,EAAE,SAAS,GAAG;IACb,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,QAAQ,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CAChC,iBAwBA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"../../../../src/cf-worker","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,WAAW,EACX,QAAQ,EACT,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAGV,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAGrB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,cAAc,SAAS,CAAC;AACxB,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AAEnC,MAAM,MAAM,mBAAmB,GAAG,iBAAiB,GAAG;IACpD,OAAO,EAAE,WAAW,CAAC;IACrB,GAAG,EAAE,gBAAgB,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAIF,KAAK,SAAS,GAAG,aAAa,GAAG;IAC/B,GAAG,EAAE,OAAO,CAAC;CACd,CAAC;AAEF,wBAAsB,eAAe,CACnC,SAAS,EAAE,SAAS,EACpB,aAAa,EAAE,mBAAmB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CA0C9B;AAED,wBAAsB,wBAAwB,CAC5C,SAAS,EAAE,SAAS,GACnB,OAAO,CAAC,kBAAkB,CAAC,CA2E7B;AAED,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,mBAIpD;AAED,wBAAgB,+BAA+B,CAAC,aAAa,EAAE,MAAM,UAEpE;AAQD,wBAAsB,cAAc,CAAC,EACnC,MAAM,EACN,QAAQ,EACR,GAAG,EACH,GAAG,EACH,QAAQ,EACR,aAAa,GACd,EAAE,SAAS,GAAG;IACb,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,QAAQ,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;CAChC,iBAwBA"}
@@ -1,10 +1,20 @@
1
1
  import { CoreServiceConfig } from "../api";
2
2
  import { AuthorizationResult } from "../authorize/types";
3
3
  import { RateLimitResult } from "./types";
4
- type CacheOptions = {
5
- get: (bucketId: string) => Promise<string | null>;
6
- put: (bucketId: string, count: string) => Promise<void> | void;
4
+ type IRedis = {
5
+ incr: (key: string) => Promise<number>;
6
+ expire: (key: string, ttlSeconds: number) => Promise<0 | 1>;
7
7
  };
8
- export declare function rateLimit(authzResult: AuthorizationResult, serviceConfig: CoreServiceConfig, cacheOptions: CacheOptions): Promise<RateLimitResult>;
8
+ export declare function rateLimit(args: {
9
+ authzResult: AuthorizationResult;
10
+ serviceConfig: CoreServiceConfig;
11
+ redis: IRedis;
12
+ /**
13
+ * Sample requests to reduce load on Redis.
14
+ * This scales down the request count and the rate limit threshold.
15
+ * @default 1.0
16
+ */
17
+ sampleRate?: number;
18
+ }): Promise<RateLimitResult>;
9
19
  export {};
10
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"../../../../../src/core/rateLimit","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAuB,MAAM,QAAQ,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAEzD,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAK1C,KAAK,YAAY,GAAG;IAClB,GAAG,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAClD,GAAG,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CAChE,CAAC;AAEF,wBAAsB,SAAS,CAC7B,WAAW,EAAE,mBAAmB,EAChC,aAAa,EAAE,iBAAiB,EAChC,YAAY,EAAE,YAAY,GACzB,OAAO,CAAC,eAAe,CAAC,CAuD1B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"../../../../../src/core/rateLimit","sources":["index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAuB,MAAM,QAAQ,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAK1C,KAAK,MAAM,GAAG;IACZ,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACvC,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;CAC7D,CAAC;AAEF,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,WAAW,EAAE,mBAAmB,CAAC;IACjC,aAAa,EAAE,iBAAiB,CAAC;IACjC,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,eAAe,CAAC,CAiE3B"}
@@ -1,6 +1,8 @@
1
1
  export type RateLimitResult = {
2
+ requestCount: number;
2
3
  rateLimited: false;
3
4
  } | {
5
+ requestCount: number;
4
6
  rateLimited: true;
5
7
  status: number;
6
8
  errorMessage: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"../../../../../src/core/rateLimit","sources":["types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB;IACE,WAAW,EAAE,KAAK,CAAC;CACpB,GACD;IACE,WAAW,EAAE,IAAI,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"../../../../../src/core/rateLimit","sources":["types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GACvB;IACE,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,KAAK,CAAC;CACpB,GACD;IACE,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,IAAI,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAC"}
@@ -449,12 +449,21 @@ async function authorize(authData, serviceConfig, cacheOptions) {
449
449
  };
450
450
  }
451
451
 
452
- const DEFAULT_RATE_LIMIT_WINDOW_SECONDS = 10;
453
- const HARD_LIMIT_MULTIPLE = 2; // 2x of allowed limit
452
+ const RATE_LIMIT_WINDOW_SECONDS = 10;
454
453
 
455
- async function rateLimit(authzResult, serviceConfig, cacheOptions) {
456
- if (!authzResult.authorized) {
454
+ // Redis interface compatible with ioredis (Node) and upstash (Cloudflare Workers).
455
+
456
+ async function rateLimit(args) {
457
+ const {
458
+ authzResult,
459
+ serviceConfig,
460
+ redis,
461
+ sampleRate = 1.0
462
+ } = args;
463
+ const shouldCountRequest = Math.random() < sampleRate;
464
+ if (!shouldCountRequest || !authzResult.authorized) {
457
465
  return {
466
+ requestCount: 0,
458
467
  rateLimited: false
459
468
  };
460
469
  }
@@ -462,48 +471,54 @@ async function rateLimit(authzResult, serviceConfig, cacheOptions) {
462
471
  apiKeyMeta,
463
472
  accountMeta
464
473
  } = authzResult;
465
- const {
466
- rateLimits
467
- } = apiKeyMeta || accountMeta || {};
468
474
  const accountId = apiKeyMeta?.accountId || accountMeta?.id;
469
475
  const {
470
476
  serviceScope
471
477
  } = serviceConfig;
472
- if (!rateLimits || !(serviceScope in rateLimits)) {
478
+ const {
479
+ rateLimits
480
+ } = apiKeyMeta || accountMeta || {};
481
+ const limitPerSecond = rateLimits?.[serviceScope];
482
+ if (!limitPerSecond) {
473
483
  // No rate limit is provided. Assume the request is not rate limited.
474
484
  return {
485
+ requestCount: 0,
475
486
  rateLimited: false
476
487
  };
477
488
  }
478
- const limit = rateLimits[serviceScope];
479
489
 
480
- // Floors the current time to the nearest DEFAULT_RATE_LIMIT_WINDOW_SECONDS.
481
- const bucketId = Math.floor(Date.now() / (1000 * DEFAULT_RATE_LIMIT_WINDOW_SECONDS)) * DEFAULT_RATE_LIMIT_WINDOW_SECONDS;
482
- const key = [serviceScope, accountId, bucketId].join(":");
483
- const value = parseInt((await cacheOptions.get(key)) || "0");
484
- const current = value + 1;
490
+ // Gets the 10-second window for the current timestamp.
491
+ const timestampWindow = Math.floor(Date.now() / (1000 * RATE_LIMIT_WINDOW_SECONDS)) * RATE_LIMIT_WINDOW_SECONDS;
492
+ const key = `rate-limit:${serviceScope}:${accountId}:${timestampWindow}`;
493
+
494
+ // Increment and get the current request count in this window.
495
+ const requestCount = await redis.incr(key);
496
+ if (requestCount === 1) {
497
+ // For the first increment, set an expiration to clean up this key.
498
+ await redis.expire(key, RATE_LIMIT_WINDOW_SECONDS);
499
+ }
485
500
 
486
- // limit is in seconds, but we need in DEFAULT_RATE_LIMIT_WINDOW_SECONDS
487
- const limitWindow = limit * DEFAULT_RATE_LIMIT_WINDOW_SECONDS;
488
- if (current > limitWindow) {
489
- // report rate limit hits
501
+ // Get the limit for this window accounting for the sample rate.
502
+ const limitPerWindow = limitPerSecond * sampleRate * RATE_LIMIT_WINDOW_SECONDS;
503
+ if (requestCount > limitPerWindow) {
504
+ // Report rate limit hits.
490
505
  if (apiKeyMeta?.id) {
491
506
  await updateRateLimitedAt(apiKeyMeta.id, serviceConfig);
492
507
  }
493
508
 
494
- // actually rate limit only when reached hard limit
495
- if (current > limitWindow * HARD_LIMIT_MULTIPLE) {
509
+ // Reject requests when they've exceeded 2x the rate limit.
510
+ if (requestCount > 2 * limitPerWindow) {
496
511
  return {
512
+ requestCount,
497
513
  rateLimited: true,
498
514
  status: 429,
499
- errorMessage: `You've exceeded your ${serviceScope} rate limit at ${limit} reqs/sec. To get higher rate limits, contact us at https://thirdweb.com/contact-us.`,
515
+ errorMessage: `You've exceeded your ${serviceScope} rate limit at ${limitPerSecond} reqs/sec. To get higher rate limits, contact us at https://thirdweb.com/contact-us.`,
500
516
  errorCode: "RATE_LIMIT_EXCEEDED"
501
517
  };
502
518
  }
503
- } else {
504
- await cacheOptions.put(key, current.toString());
505
519
  }
506
520
  return {
521
+ requestCount,
507
522
  rateLimited: false
508
523
  };
509
524
  }
@@ -449,12 +449,21 @@ async function authorize(authData, serviceConfig, cacheOptions) {
449
449
  };
450
450
  }
451
451
 
452
- const DEFAULT_RATE_LIMIT_WINDOW_SECONDS = 10;
453
- const HARD_LIMIT_MULTIPLE = 2; // 2x of allowed limit
452
+ const RATE_LIMIT_WINDOW_SECONDS = 10;
454
453
 
455
- async function rateLimit(authzResult, serviceConfig, cacheOptions) {
456
- if (!authzResult.authorized) {
454
+ // Redis interface compatible with ioredis (Node) and upstash (Cloudflare Workers).
455
+
456
+ async function rateLimit(args) {
457
+ const {
458
+ authzResult,
459
+ serviceConfig,
460
+ redis,
461
+ sampleRate = 1.0
462
+ } = args;
463
+ const shouldCountRequest = Math.random() < sampleRate;
464
+ if (!shouldCountRequest || !authzResult.authorized) {
457
465
  return {
466
+ requestCount: 0,
458
467
  rateLimited: false
459
468
  };
460
469
  }
@@ -462,48 +471,54 @@ async function rateLimit(authzResult, serviceConfig, cacheOptions) {
462
471
  apiKeyMeta,
463
472
  accountMeta
464
473
  } = authzResult;
465
- const {
466
- rateLimits
467
- } = apiKeyMeta || accountMeta || {};
468
474
  const accountId = apiKeyMeta?.accountId || accountMeta?.id;
469
475
  const {
470
476
  serviceScope
471
477
  } = serviceConfig;
472
- if (!rateLimits || !(serviceScope in rateLimits)) {
478
+ const {
479
+ rateLimits
480
+ } = apiKeyMeta || accountMeta || {};
481
+ const limitPerSecond = rateLimits?.[serviceScope];
482
+ if (!limitPerSecond) {
473
483
  // No rate limit is provided. Assume the request is not rate limited.
474
484
  return {
485
+ requestCount: 0,
475
486
  rateLimited: false
476
487
  };
477
488
  }
478
- const limit = rateLimits[serviceScope];
479
489
 
480
- // Floors the current time to the nearest DEFAULT_RATE_LIMIT_WINDOW_SECONDS.
481
- const bucketId = Math.floor(Date.now() / (1000 * DEFAULT_RATE_LIMIT_WINDOW_SECONDS)) * DEFAULT_RATE_LIMIT_WINDOW_SECONDS;
482
- const key = [serviceScope, accountId, bucketId].join(":");
483
- const value = parseInt((await cacheOptions.get(key)) || "0");
484
- const current = value + 1;
490
+ // Gets the 10-second window for the current timestamp.
491
+ const timestampWindow = Math.floor(Date.now() / (1000 * RATE_LIMIT_WINDOW_SECONDS)) * RATE_LIMIT_WINDOW_SECONDS;
492
+ const key = `rate-limit:${serviceScope}:${accountId}:${timestampWindow}`;
493
+
494
+ // Increment and get the current request count in this window.
495
+ const requestCount = await redis.incr(key);
496
+ if (requestCount === 1) {
497
+ // For the first increment, set an expiration to clean up this key.
498
+ await redis.expire(key, RATE_LIMIT_WINDOW_SECONDS);
499
+ }
485
500
 
486
- // limit is in seconds, but we need in DEFAULT_RATE_LIMIT_WINDOW_SECONDS
487
- const limitWindow = limit * DEFAULT_RATE_LIMIT_WINDOW_SECONDS;
488
- if (current > limitWindow) {
489
- // report rate limit hits
501
+ // Get the limit for this window accounting for the sample rate.
502
+ const limitPerWindow = limitPerSecond * sampleRate * RATE_LIMIT_WINDOW_SECONDS;
503
+ if (requestCount > limitPerWindow) {
504
+ // Report rate limit hits.
490
505
  if (apiKeyMeta?.id) {
491
506
  await updateRateLimitedAt(apiKeyMeta.id, serviceConfig);
492
507
  }
493
508
 
494
- // actually rate limit only when reached hard limit
495
- if (current > limitWindow * HARD_LIMIT_MULTIPLE) {
509
+ // Reject requests when they've exceeded 2x the rate limit.
510
+ if (requestCount > 2 * limitPerWindow) {
496
511
  return {
512
+ requestCount,
497
513
  rateLimited: true,
498
514
  status: 429,
499
- errorMessage: `You've exceeded your ${serviceScope} rate limit at ${limit} reqs/sec. To get higher rate limits, contact us at https://thirdweb.com/contact-us.`,
515
+ errorMessage: `You've exceeded your ${serviceScope} rate limit at ${limitPerSecond} reqs/sec. To get higher rate limits, contact us at https://thirdweb.com/contact-us.`,
500
516
  errorCode: "RATE_LIMIT_EXCEEDED"
501
517
  };
502
518
  }
503
- } else {
504
- await cacheOptions.put(key, current.toString());
505
519
  }
506
520
  return {
521
+ requestCount,
507
522
  rateLimited: false
508
523
  };
509
524
  }
@@ -447,12 +447,21 @@ async function authorize(authData, serviceConfig, cacheOptions) {
447
447
  };
448
448
  }
449
449
 
450
- const DEFAULT_RATE_LIMIT_WINDOW_SECONDS = 10;
451
- const HARD_LIMIT_MULTIPLE = 2; // 2x of allowed limit
450
+ const RATE_LIMIT_WINDOW_SECONDS = 10;
452
451
 
453
- async function rateLimit(authzResult, serviceConfig, cacheOptions) {
454
- if (!authzResult.authorized) {
452
+ // Redis interface compatible with ioredis (Node) and upstash (Cloudflare Workers).
453
+
454
+ async function rateLimit(args) {
455
+ const {
456
+ authzResult,
457
+ serviceConfig,
458
+ redis,
459
+ sampleRate = 1.0
460
+ } = args;
461
+ const shouldCountRequest = Math.random() < sampleRate;
462
+ if (!shouldCountRequest || !authzResult.authorized) {
455
463
  return {
464
+ requestCount: 0,
456
465
  rateLimited: false
457
466
  };
458
467
  }
@@ -460,48 +469,54 @@ async function rateLimit(authzResult, serviceConfig, cacheOptions) {
460
469
  apiKeyMeta,
461
470
  accountMeta
462
471
  } = authzResult;
463
- const {
464
- rateLimits
465
- } = apiKeyMeta || accountMeta || {};
466
472
  const accountId = apiKeyMeta?.accountId || accountMeta?.id;
467
473
  const {
468
474
  serviceScope
469
475
  } = serviceConfig;
470
- if (!rateLimits || !(serviceScope in rateLimits)) {
476
+ const {
477
+ rateLimits
478
+ } = apiKeyMeta || accountMeta || {};
479
+ const limitPerSecond = rateLimits?.[serviceScope];
480
+ if (!limitPerSecond) {
471
481
  // No rate limit is provided. Assume the request is not rate limited.
472
482
  return {
483
+ requestCount: 0,
473
484
  rateLimited: false
474
485
  };
475
486
  }
476
- const limit = rateLimits[serviceScope];
477
487
 
478
- // Floors the current time to the nearest DEFAULT_RATE_LIMIT_WINDOW_SECONDS.
479
- const bucketId = Math.floor(Date.now() / (1000 * DEFAULT_RATE_LIMIT_WINDOW_SECONDS)) * DEFAULT_RATE_LIMIT_WINDOW_SECONDS;
480
- const key = [serviceScope, accountId, bucketId].join(":");
481
- const value = parseInt((await cacheOptions.get(key)) || "0");
482
- const current = value + 1;
488
+ // Gets the 10-second window for the current timestamp.
489
+ const timestampWindow = Math.floor(Date.now() / (1000 * RATE_LIMIT_WINDOW_SECONDS)) * RATE_LIMIT_WINDOW_SECONDS;
490
+ const key = `rate-limit:${serviceScope}:${accountId}:${timestampWindow}`;
491
+
492
+ // Increment and get the current request count in this window.
493
+ const requestCount = await redis.incr(key);
494
+ if (requestCount === 1) {
495
+ // For the first increment, set an expiration to clean up this key.
496
+ await redis.expire(key, RATE_LIMIT_WINDOW_SECONDS);
497
+ }
483
498
 
484
- // limit is in seconds, but we need in DEFAULT_RATE_LIMIT_WINDOW_SECONDS
485
- const limitWindow = limit * DEFAULT_RATE_LIMIT_WINDOW_SECONDS;
486
- if (current > limitWindow) {
487
- // report rate limit hits
499
+ // Get the limit for this window accounting for the sample rate.
500
+ const limitPerWindow = limitPerSecond * sampleRate * RATE_LIMIT_WINDOW_SECONDS;
501
+ if (requestCount > limitPerWindow) {
502
+ // Report rate limit hits.
488
503
  if (apiKeyMeta?.id) {
489
504
  await updateRateLimitedAt(apiKeyMeta.id, serviceConfig);
490
505
  }
491
506
 
492
- // actually rate limit only when reached hard limit
493
- if (current > limitWindow * HARD_LIMIT_MULTIPLE) {
507
+ // Reject requests when they've exceeded 2x the rate limit.
508
+ if (requestCount > 2 * limitPerWindow) {
494
509
  return {
510
+ requestCount,
495
511
  rateLimited: true,
496
512
  status: 429,
497
- errorMessage: `You've exceeded your ${serviceScope} rate limit at ${limit} reqs/sec. To get higher rate limits, contact us at https://thirdweb.com/contact-us.`,
513
+ errorMessage: `You've exceeded your ${serviceScope} rate limit at ${limitPerSecond} reqs/sec. To get higher rate limits, contact us at https://thirdweb.com/contact-us.`,
498
514
  errorCode: "RATE_LIMIT_EXCEEDED"
499
515
  };
500
516
  }
501
- } else {
502
- await cacheOptions.put(key, current.toString());
503
517
  }
504
518
  return {
519
+ requestCount,
505
520
  rateLimited: false
506
521
  };
507
522
  }
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var node_crypto = require('node:crypto');
6
- var index = require('../../dist/index-807f6a60.cjs.dev.js');
6
+ var index = require('../../dist/index-6a597a58.cjs.dev.js');
7
7
  var services = require('../../dist/services-79b4664f.cjs.dev.js');
8
8
 
9
9
  /**
@@ -3,7 +3,7 @@
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var node_crypto = require('node:crypto');
6
- var index = require('../../dist/index-cfc8027b.cjs.prod.js');
6
+ var index = require('../../dist/index-30f0f58c.cjs.prod.js');
7
7
  var services = require('../../dist/services-04997839.cjs.prod.js');
8
8
 
9
9
  /**
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { a as authorize } from '../../dist/index-bcf68113.esm.js';
3
- export { r as rateLimit, u as usageLimit } from '../../dist/index-bcf68113.esm.js';
2
+ import { a as authorize } from '../../dist/index-cd8f49a9.esm.js';
3
+ export { r as rateLimit, u as usageLimit } from '../../dist/index-cd8f49a9.esm.js';
4
4
  export { b as SERVICES, S as SERVICE_DEFINITIONS, a as SERVICE_NAMES, g as getServiceByName } from '../../dist/services-bc12a5f6.esm.js';
5
5
 
6
6
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thirdweb-dev/service-utils",
3
- "version": "0.0.0-dev-8dbcee9-20230925135804",
3
+ "version": "0.0.0-dev-0231c73-20230926212531",
4
4
  "main": "dist/thirdweb-dev-service-utils.cjs.js",
5
5
  "module": "dist/thirdweb-dev-service-utils.esm.js",
6
6
  "exports": {