diff --git a/.changeset/large-beds-move.md b/.changeset/large-beds-move.md new file mode 100644 index 00000000000..f67643cb31c --- /dev/null +++ b/.changeset/large-beds-move.md @@ -0,0 +1,5 @@ +--- +"@thirdweb-dev/service-utils": minor +--- + +pass `team` instead of `project` to `rateLimit` diff --git a/packages/service-utils/CHANGELOG.md b/packages/service-utils/CHANGELOG.md index 4fb828a3a14..ae330819bd6 100644 --- a/packages/service-utils/CHANGELOG.md +++ b/packages/service-utils/CHANGELOG.md @@ -1,5 +1,11 @@ # @thirdweb-dev/service-utils +## 0.7.3 + +### Patch Changes + +- [#6084](https://github.com/thirdweb-dev/js/pull/6084) [`b5e327e`](https://github.com/thirdweb-dev/js/commit/b5e327e5ec745ce1beb9a79404fa5b11d0c5588e) Thanks [@jnsdls](https://github.com/jnsdls)! - add `billingPlanVersion` to `TeamResponse` + ## 0.7.2 ### Patch Changes diff --git a/packages/service-utils/package.json b/packages/service-utils/package.json index 9123815b242..373716f225e 100644 --- a/packages/service-utils/package.json +++ b/packages/service-utils/package.json @@ -1,6 +1,6 @@ { "name": "@thirdweb-dev/service-utils", - "version": "0.7.2", + "version": "0.7.3", "type": "module", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/packages/service-utils/src/core/api.ts b/packages/service-utils/src/core/api.ts index 20dbb489ed1..cc16ea4854d 100644 --- a/packages/service-utils/src/core/api.ts +++ b/packages/service-utils/src/core/api.ts @@ -45,6 +45,7 @@ export type TeamResponse = { slug: string; image: string | null; billingPlan: "free" | "starter" | "growth" | "pro"; + billingPlanVersion: number; createdAt: Date; updatedAt: Date | null; billingEmail: string | null; @@ -163,24 +164,3 @@ export async function fetchTeamAndProject( ); } } - -export async function updateRateLimitedAt( - projectId: string, - config: CoreServiceConfig, -): Promise { - const { apiUrl, serviceScope: scope, serviceApiKey } = config; - - const url = `${apiUrl}/usage/rateLimit`; - - await fetch(url, { - method: "PUT", - headers: { - "x-service-api-key": serviceApiKey, - "content-type": "application/json", - }, - body: JSON.stringify({ - apiKeyId: projectId, // projectId is the apiKeyId - scope, - }), - }); -} diff --git a/packages/service-utils/src/core/rateLimit/index.ts b/packages/service-utils/src/core/rateLimit/index.ts index 10a90fabcbc..89584379f67 100644 --- a/packages/service-utils/src/core/rateLimit/index.ts +++ b/packages/service-utils/src/core/rateLimit/index.ts @@ -1,8 +1,4 @@ -import { - type CoreServiceConfig, - type ProjectResponse, - updateRateLimitedAt, -} from "../api.js"; +import type { CoreServiceConfig, TeamResponse } from "../api.js"; import type { RateLimitResult } from "./types.js"; const RATE_LIMIT_WINDOW_SECONDS = 10; @@ -14,7 +10,7 @@ type IRedis = { }; export async function rateLimit(args: { - project?: ProjectResponse; + team: TeamResponse; limitPerSecond: number; serviceConfig: CoreServiceConfig; redis: IRedis; @@ -25,13 +21,7 @@ export async function rateLimit(args: { */ sampleRate?: number; }): Promise { - const { - project, - limitPerSecond, - serviceConfig, - redis, - sampleRate = 1.0, - } = args; + const { team, limitPerSecond, serviceConfig, redis, sampleRate = 1.0 } = args; const shouldSampleRequest = Math.random() < sampleRate; if (!shouldSampleRequest) { @@ -57,7 +47,7 @@ export async function rateLimit(args: { const timestampWindow = Math.floor(Date.now() / (1000 * RATE_LIMIT_WINDOW_SECONDS)) * RATE_LIMIT_WINDOW_SECONDS; - const key = `rate-limit:${serviceScope}:${project?.id}:${timestampWindow}`; + const key = `rate-limit:${serviceScope}:${team.id}:${timestampWindow}`; // Increment and get the current request count in this window. const requestCount = await redis.incr(key); @@ -71,17 +61,6 @@ export async function rateLimit(args: { limitPerSecond * sampleRate * RATE_LIMIT_WINDOW_SECONDS; if (requestCount > limitPerWindow) { - /** - * Report rate limit hits. - * Only track rate limit when its hit for the first time. - * Not waiting for tracking to complete as user doesn't need to wait. - */ - if (requestCount === limitPerWindow + 1 && project?.id) { - updateRateLimitedAt(project.id, serviceConfig).catch(() => { - // no-op - }); - } - return { rateLimited: true, requestCount, diff --git a/packages/service-utils/src/core/rateLimit/rateLimit.test.ts b/packages/service-utils/src/core/rateLimit/rateLimit.test.ts index 203bd0e08de..04832eccb4c 100644 --- a/packages/service-utils/src/core/rateLimit/rateLimit.test.ts +++ b/packages/service-utils/src/core/rateLimit/rateLimit.test.ts @@ -1,6 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { validProjectResponse, validServiceConfig } from "../../mocks.js"; -import { updateRateLimitedAt } from "../api.js"; +import { validServiceConfig, validTeamResponse } from "../../mocks.js"; import { rateLimit } from "./index.js"; const mockRedis = { @@ -8,11 +7,6 @@ const mockRedis = { expire: vi.fn(), }; -// Mocking the updateRateLimitedAt function -vi.mock("../../../src/core/api", () => ({ - updateRateLimitedAt: vi.fn().mockResolvedValue({}), -})); - describe("rateLimit", () => { beforeEach(() => { // Clear mock function calls and reset any necessary state. @@ -27,7 +21,7 @@ describe("rateLimit", () => { it("should not rate limit if service scope is not in rate limits", async () => { const result = await rateLimit({ - project: validProjectResponse, + team: validTeamResponse, limitPerSecond: 0, serviceConfig: validServiceConfig, redis: mockRedis, @@ -44,7 +38,7 @@ describe("rateLimit", () => { mockRedis.incr.mockResolvedValue(50); // Current count is 50 requests in 10 seconds. const result = await rateLimit({ - project: validProjectResponse, + team: validTeamResponse, limitPerSecond: 5, serviceConfig: validServiceConfig, redis: mockRedis, @@ -55,7 +49,7 @@ describe("rateLimit", () => { requestCount: 50, rateLimit: 50, }); - expect(updateRateLimitedAt).not.toHaveBeenCalled(); + expect(mockRedis.expire).not.toHaveBeenCalled(); }); @@ -63,7 +57,7 @@ describe("rateLimit", () => { mockRedis.incr.mockResolvedValue(51); const result = await rateLimit({ - project: validProjectResponse, + team: validTeamResponse, limitPerSecond: 5, serviceConfig: validServiceConfig, redis: mockRedis, @@ -77,7 +71,7 @@ describe("rateLimit", () => { errorMessage: `You've exceeded your storage rate limit at 5 reqs/sec. To get higher rate limits, contact us at https://thirdweb.com/contact-us.`, errorCode: "RATE_LIMIT_EXCEEDED", }); - expect(updateRateLimitedAt).toHaveBeenCalled(); + expect(mockRedis.expire).not.toHaveBeenCalled(); }); @@ -85,7 +79,7 @@ describe("rateLimit", () => { mockRedis.incr.mockResolvedValue(1); const result = await rateLimit({ - project: validProjectResponse, + team: validTeamResponse, limitPerSecond: 5, serviceConfig: validServiceConfig, redis: mockRedis, @@ -104,7 +98,7 @@ describe("rateLimit", () => { vi.spyOn(global.Math, "random").mockReturnValue(0.08); const result = await rateLimit({ - project: validProjectResponse, + team: validTeamResponse, limitPerSecond: 5, serviceConfig: validServiceConfig, redis: mockRedis, @@ -127,7 +121,7 @@ describe("rateLimit", () => { vi.spyOn(global.Math, "random").mockReturnValue(0.15); const result = await rateLimit({ - project: validProjectResponse, + team: validTeamResponse, limitPerSecond: 5, serviceConfig: validServiceConfig, redis: mockRedis, diff --git a/packages/service-utils/src/index.ts b/packages/service-utils/src/index.ts index d1e79b62a5b..2bb94013d76 100644 --- a/packages/service-utils/src/index.ts +++ b/packages/service-utils/src/index.ts @@ -10,7 +10,7 @@ export type { TeamResponse, } from "./core/api.js"; -export { fetchTeamAndProject, updateRateLimitedAt } from "./core/api.js"; +export { fetchTeamAndProject } from "./core/api.js"; export { authorizeBundleId, diff --git a/packages/service-utils/src/mocks.ts b/packages/service-utils/src/mocks.ts index 5902dacd5aa..85627a7247c 100644 --- a/packages/service-utils/src/mocks.ts +++ b/packages/service-utils/src/mocks.ts @@ -42,6 +42,7 @@ export const validTeamResponse: TeamResponse = { createdAt: new Date("2024-06-01"), updatedAt: new Date("2024-06-01"), billingPlan: "free", + billingPlanVersion: 1, billingEmail: "test@example.com", billingStatus: "noPayment", growthTrialEligible: false,