@thirdweb-dev/service-utils 0.0.0-dev-0231c73-20230926212531 → 0.0.0-dev-d86829b-20230927000710

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-6a597a58.cjs.dev.js');
5
+ var index = require('../../dist/index-807f6a60.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,6 +91,8 @@ 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;
94
96
  async function authorizeWorker(authInput, serviceConfig) {
95
97
  let authData;
96
98
  try {
@@ -122,6 +124,14 @@ async function authorizeWorker(authInput, serviceConfig) {
122
124
  cacheTtlSeconds: serviceConfig.cacheTtlSeconds ?? DEFAULT_CACHE_TTL_SECONDS
123
125
  });
124
126
  }
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
+ }
125
135
  async function extractAuthorizationData(authInput) {
126
136
  const requestUrl = new URL(authInput.req.url);
127
137
  const headers = authInput.req.headers;
@@ -249,3 +259,4 @@ exports.extractAuthorizationData = extractAuthorizationData;
249
259
  exports.hashSecretKey = hashSecretKey;
250
260
  exports.logHttpRequest = logHttpRequest;
251
261
  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-30f0f58c.cjs.prod.js');
5
+ var index = require('../../dist/index-cfc8027b.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,6 +91,8 @@ 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;
94
96
  async function authorizeWorker(authInput, serviceConfig) {
95
97
  let authData;
96
98
  try {
@@ -122,6 +124,14 @@ async function authorizeWorker(authInput, serviceConfig) {
122
124
  cacheTtlSeconds: serviceConfig.cacheTtlSeconds ?? DEFAULT_CACHE_TTL_SECONDS
123
125
  });
124
126
  }
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
+ }
125
135
  async function extractAuthorizationData(authInput) {
126
136
  const requestUrl = new URL(authInput.req.url);
127
137
  const headers = authInput.req.headers;
@@ -249,3 +259,4 @@ exports.extractAuthorizationData = extractAuthorizationData;
249
259
  exports.hashSecretKey = hashSecretKey;
250
260
  exports.logHttpRequest = logHttpRequest;
251
261
  exports.publishUsageEvents = publishUsageEvents;
262
+ exports.rateLimitWorker = rateLimitWorker;
@@ -1,5 +1,5 @@
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';
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';
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,6 +88,8 @@ 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;
91
93
  async function authorizeWorker(authInput, serviceConfig) {
92
94
  let authData;
93
95
  try {
@@ -119,6 +121,14 @@ async function authorizeWorker(authInput, serviceConfig) {
119
121
  cacheTtlSeconds: serviceConfig.cacheTtlSeconds ?? DEFAULT_CACHE_TTL_SECONDS
120
122
  });
121
123
  }
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
+ }
122
132
  async function extractAuthorizationData(authInput) {
123
133
  const requestUrl = new URL(authInput.req.url);
124
134
  const headers = authInput.req.headers;
@@ -234,4 +244,4 @@ async function logHttpRequest(_ref) {
234
244
  }
235
245
  }
236
246
 
237
- export { authorizeWorker, deriveClientIdFromSecretKeyHash, extractAuthorizationData, hashSecretKey, logHttpRequest, publishUsageEvents };
247
+ export { authorizeWorker, deriveClientIdFromSecretKeyHash, extractAuthorizationData, hashSecretKey, logHttpRequest, publishUsageEvents, rateLimitWorker };
@@ -3,6 +3,7 @@ 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";
6
7
  import type { CoreAuthInput } from "../core/types";
7
8
  export * from "./usage";
8
9
  export * from "../core/services";
@@ -17,6 +18,7 @@ type AuthInput = CoreAuthInput & {
17
18
  req: Request;
18
19
  };
19
20
  export declare function authorizeWorker(authInput: AuthInput, serviceConfig: WorkerServiceConfig): Promise<AuthorizationResult>;
21
+ export declare function rateLimitWorker(authzResult: AuthorizationResult, serviceConfig: WorkerServiceConfig): Promise<RateLimitResult>;
20
22
  export declare function extractAuthorizationData(authInput: AuthInput): Promise<AuthorizationInput>;
21
23
  export declare function hashSecretKey(secretKey: string): Promise<string>;
22
24
  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,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
+ {"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,20 +1,10 @@
1
1
  import { CoreServiceConfig } from "../api";
2
2
  import { AuthorizationResult } from "../authorize/types";
3
3
  import { RateLimitResult } from "./types";
4
- type IRedis = {
5
- incr: (key: string) => Promise<number>;
6
- expire: (key: string, ttlSeconds: number) => Promise<0 | 1>;
4
+ type CacheOptions = {
5
+ get: (bucketId: string) => Promise<string | null>;
6
+ put: (bucketId: string, count: string) => Promise<void> | void;
7
7
  };
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>;
8
+ export declare function rateLimit(authzResult: AuthorizationResult, serviceConfig: CoreServiceConfig, cacheOptions: CacheOptions): Promise<RateLimitResult>;
19
9
  export {};
20
10
  //# 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;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
+ {"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,8 +1,6 @@
1
1
  export type RateLimitResult = {
2
- requestCount: number;
3
2
  rateLimited: false;
4
3
  } | {
5
- requestCount: number;
6
4
  rateLimited: true;
7
5
  status: number;
8
6
  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,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"}
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"}
@@ -449,21 +449,12 @@ async function authorize(authData, serviceConfig, cacheOptions) {
449
449
  };
450
450
  }
451
451
 
452
- const RATE_LIMIT_WINDOW_SECONDS = 10;
452
+ const DEFAULT_RATE_LIMIT_WINDOW_SECONDS = 10;
453
+ const HARD_LIMIT_MULTIPLE = 2; // 2x of allowed limit
453
454
 
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) {
455
+ async function rateLimit(authzResult, serviceConfig, cacheOptions) {
456
+ if (!authzResult.authorized) {
465
457
  return {
466
- requestCount: 0,
467
458
  rateLimited: false
468
459
  };
469
460
  }
@@ -471,54 +462,48 @@ async function rateLimit(args) {
471
462
  apiKeyMeta,
472
463
  accountMeta
473
464
  } = authzResult;
465
+ const {
466
+ rateLimits
467
+ } = apiKeyMeta || accountMeta || {};
474
468
  const accountId = apiKeyMeta?.accountId || accountMeta?.id;
475
469
  const {
476
470
  serviceScope
477
471
  } = serviceConfig;
478
- const {
479
- rateLimits
480
- } = apiKeyMeta || accountMeta || {};
481
- const limitPerSecond = rateLimits?.[serviceScope];
482
- if (!limitPerSecond) {
472
+ if (!rateLimits || !(serviceScope in rateLimits)) {
483
473
  // No rate limit is provided. Assume the request is not rate limited.
484
474
  return {
485
- requestCount: 0,
486
475
  rateLimited: false
487
476
  };
488
477
  }
478
+ const limit = rateLimits[serviceScope];
489
479
 
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
- }
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;
500
485
 
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.
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
505
490
  if (apiKeyMeta?.id) {
506
491
  await updateRateLimitedAt(apiKeyMeta.id, serviceConfig);
507
492
  }
508
493
 
509
- // Reject requests when they've exceeded 2x the rate limit.
510
- if (requestCount > 2 * limitPerWindow) {
494
+ // actually rate limit only when reached hard limit
495
+ if (current > limitWindow * HARD_LIMIT_MULTIPLE) {
511
496
  return {
512
- requestCount,
513
497
  rateLimited: true,
514
498
  status: 429,
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.`,
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.`,
516
500
  errorCode: "RATE_LIMIT_EXCEEDED"
517
501
  };
518
502
  }
503
+ } else {
504
+ await cacheOptions.put(key, current.toString());
519
505
  }
520
506
  return {
521
- requestCount,
522
507
  rateLimited: false
523
508
  };
524
509
  }
@@ -447,21 +447,12 @@ async function authorize(authData, serviceConfig, cacheOptions) {
447
447
  };
448
448
  }
449
449
 
450
- const RATE_LIMIT_WINDOW_SECONDS = 10;
450
+ const DEFAULT_RATE_LIMIT_WINDOW_SECONDS = 10;
451
+ const HARD_LIMIT_MULTIPLE = 2; // 2x of allowed limit
451
452
 
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) {
453
+ async function rateLimit(authzResult, serviceConfig, cacheOptions) {
454
+ if (!authzResult.authorized) {
463
455
  return {
464
- requestCount: 0,
465
456
  rateLimited: false
466
457
  };
467
458
  }
@@ -469,54 +460,48 @@ async function rateLimit(args) {
469
460
  apiKeyMeta,
470
461
  accountMeta
471
462
  } = authzResult;
463
+ const {
464
+ rateLimits
465
+ } = apiKeyMeta || accountMeta || {};
472
466
  const accountId = apiKeyMeta?.accountId || accountMeta?.id;
473
467
  const {
474
468
  serviceScope
475
469
  } = serviceConfig;
476
- const {
477
- rateLimits
478
- } = apiKeyMeta || accountMeta || {};
479
- const limitPerSecond = rateLimits?.[serviceScope];
480
- if (!limitPerSecond) {
470
+ if (!rateLimits || !(serviceScope in rateLimits)) {
481
471
  // No rate limit is provided. Assume the request is not rate limited.
482
472
  return {
483
- requestCount: 0,
484
473
  rateLimited: false
485
474
  };
486
475
  }
476
+ const limit = rateLimits[serviceScope];
487
477
 
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
- }
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;
498
483
 
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.
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
503
488
  if (apiKeyMeta?.id) {
504
489
  await updateRateLimitedAt(apiKeyMeta.id, serviceConfig);
505
490
  }
506
491
 
507
- // Reject requests when they've exceeded 2x the rate limit.
508
- if (requestCount > 2 * limitPerWindow) {
492
+ // actually rate limit only when reached hard limit
493
+ if (current > limitWindow * HARD_LIMIT_MULTIPLE) {
509
494
  return {
510
- requestCount,
511
495
  rateLimited: true,
512
496
  status: 429,
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.`,
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.`,
514
498
  errorCode: "RATE_LIMIT_EXCEEDED"
515
499
  };
516
500
  }
501
+ } else {
502
+ await cacheOptions.put(key, current.toString());
517
503
  }
518
504
  return {
519
- requestCount,
520
505
  rateLimited: false
521
506
  };
522
507
  }
@@ -449,21 +449,12 @@ async function authorize(authData, serviceConfig, cacheOptions) {
449
449
  };
450
450
  }
451
451
 
452
- const RATE_LIMIT_WINDOW_SECONDS = 10;
452
+ const DEFAULT_RATE_LIMIT_WINDOW_SECONDS = 10;
453
+ const HARD_LIMIT_MULTIPLE = 2; // 2x of allowed limit
453
454
 
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) {
455
+ async function rateLimit(authzResult, serviceConfig, cacheOptions) {
456
+ if (!authzResult.authorized) {
465
457
  return {
466
- requestCount: 0,
467
458
  rateLimited: false
468
459
  };
469
460
  }
@@ -471,54 +462,48 @@ async function rateLimit(args) {
471
462
  apiKeyMeta,
472
463
  accountMeta
473
464
  } = authzResult;
465
+ const {
466
+ rateLimits
467
+ } = apiKeyMeta || accountMeta || {};
474
468
  const accountId = apiKeyMeta?.accountId || accountMeta?.id;
475
469
  const {
476
470
  serviceScope
477
471
  } = serviceConfig;
478
- const {
479
- rateLimits
480
- } = apiKeyMeta || accountMeta || {};
481
- const limitPerSecond = rateLimits?.[serviceScope];
482
- if (!limitPerSecond) {
472
+ if (!rateLimits || !(serviceScope in rateLimits)) {
483
473
  // No rate limit is provided. Assume the request is not rate limited.
484
474
  return {
485
- requestCount: 0,
486
475
  rateLimited: false
487
476
  };
488
477
  }
478
+ const limit = rateLimits[serviceScope];
489
479
 
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
- }
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;
500
485
 
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.
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
505
490
  if (apiKeyMeta?.id) {
506
491
  await updateRateLimitedAt(apiKeyMeta.id, serviceConfig);
507
492
  }
508
493
 
509
- // Reject requests when they've exceeded 2x the rate limit.
510
- if (requestCount > 2 * limitPerWindow) {
494
+ // actually rate limit only when reached hard limit
495
+ if (current > limitWindow * HARD_LIMIT_MULTIPLE) {
511
496
  return {
512
- requestCount,
513
497
  rateLimited: true,
514
498
  status: 429,
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.`,
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.`,
516
500
  errorCode: "RATE_LIMIT_EXCEEDED"
517
501
  };
518
502
  }
503
+ } else {
504
+ await cacheOptions.put(key, current.toString());
519
505
  }
520
506
  return {
521
- requestCount,
522
507
  rateLimited: false
523
508
  };
524
509
  }
@@ -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-6a597a58.cjs.dev.js');
6
+ var index = require('../../dist/index-807f6a60.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-30f0f58c.cjs.prod.js');
6
+ var index = require('../../dist/index-cfc8027b.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-cd8f49a9.esm.js';
3
- export { r as rateLimit, u as usageLimit } from '../../dist/index-cd8f49a9.esm.js';
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';
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-0231c73-20230926212531",
3
+ "version": "0.0.0-dev-d86829b-20230927000710",
4
4
  "main": "dist/thirdweb-dev-service-utils.cjs.js",
5
5
  "module": "dist/thirdweb-dev-service-utils.esm.js",
6
6
  "exports": {