Compare commits

...
Author SHA1 Message Date
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
363bcad530 Version Packages (#6094)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-29 10:42:04 -08:00
Jonas DanielsandGitHub ee3822c785 pass team instead of project to rateLimit (#6092) 2025-01-29 10:36:11 -08:00
Joaquim VergesGitHubgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
1cff849b69 Version Packages (#6089)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2025-01-28 18:22:36 -08:00
Jonas DanielsandGitHub b5e327e5ec add billingPlanVersion to TeamResponse (#6084) 2025-01-28 18:20:57 -08:00
7 changed files with 29 additions and 63 deletions
+12
View File
@@ -1,5 +1,17 @@
# @thirdweb-dev/service-utils
## 0.8.0
### Minor Changes
- [#6092](https://github.com/thirdweb-dev/js/pull/6092) [`ee3822c`](https://github.com/thirdweb-dev/js/commit/ee3822c7855213fbfc53969b3017cce1010abbce) Thanks [@jnsdls](https://github.com/jnsdls)! - pass `team` instead of `project` to `rateLimit`
## 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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@thirdweb-dev/service-utils",
"version": "0.7.2",
"version": "0.8.0",
"type": "module",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
+1 -21
View File
@@ -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<void> {
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,
}),
});
}
@@ -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<RateLimitResult> {
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,
@@ -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,
+1 -1
View File
@@ -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,
+1
View File
@@ -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: "[email protected]",
billingStatus: "noPayment",
growthTrialEligible: false,