Skip to content

Commit 47530c0

Browse files
duyetduyetbot
andauthored
fix(security): update vulnerable dependencies (#97)
Co-authored-by: duyetbot <bot@duyet.net>
1 parent 91899cc commit 47530c0

15 files changed

Lines changed: 410 additions & 557 deletions

bun.lock

Lines changed: 281 additions & 499 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,21 @@
1717
"db:generate": "cd packages/api && bunx drizzle-kit generate",
1818
"deploy": "cd packages/api && bunx wrangler deploy"
1919
},
20+
"overrides": {
21+
"@hono/node-server": "^2.0.3",
22+
"brace-expansion": "^5.0.6",
23+
"esbuild": "0.28.0",
24+
"fast-uri": "^3.1.2",
25+
"flatted": "^3.4.2",
26+
"hono": "^4.12.21",
27+
"ip-address": "^10.2.0",
28+
"path-to-regexp": "^8.4.2",
29+
"picomatch": "^2.3.2",
30+
"postcss": "^8.5.15",
31+
"vite": "^8.0.13",
32+
"ws": "^8.20.1"
33+
},
2034
"devDependencies": {
21-
"typescript": "^5.7.0"
35+
"typescript": "^5.9.3"
2236
}
2337
}

packages/api/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@
1212
},
1313
"dependencies": {
1414
"drizzle-orm": "^0.45.2",
15-
"hono": "^4.7.0",
15+
"hono": "^4.12.21",
1616
"nanoid": "^5.1.0",
1717
"zod": "^3.24.0"
1818
},
1919
"devDependencies": {
20-
"@cloudflare/vitest-pool-workers": "^0.9.0",
20+
"@cloudflare/vitest-pool-workers": "^0.16.7",
2121
"@cloudflare/workers-types": "^4.20250313.0",
2222
"@rollup/rollup-linux-x64-gnu": "^4.60.0",
23-
"drizzle-kit": "^0.30.0",
24-
"esbuild": "0.27.4",
23+
"drizzle-kit": "^0.31.10",
24+
"esbuild": "0.28.0",
2525
"typescript": "^5.7.0",
26-
"vitest": "^3.1.0",
27-
"wrangler": "^3.105.0"
26+
"vitest": "^4.1.6",
27+
"wrangler": "^4.93.0"
2828
}
2929
}

packages/api/src/middleware/project-creation-rate-limit.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { createMiddleware } from "hono/factory";
1717
import {
1818
checkProjectCreationRateLimit,
1919
hashIdentifier,
20+
PROJECT_CREATION_RATE_LIMIT,
2021
pruneOldRateLimits,
2122
} from "../services/projects";
2223
import type { Bindings, Variables } from "../types";
@@ -45,10 +46,16 @@ export const projectCreationRateLimit = createMiddleware<{
4546
identifier = `ip:${await hashIdentifier(ip)}`;
4647
}
4748

48-
const result = await checkProjectCreationRateLimit(db, identifier);
49+
const configuredRateLimit = Number(
50+
(c.env as { PROJECT_CREATION_RATE_LIMIT_MAX?: string }).PROJECT_CREATION_RATE_LIMIT_MAX,
51+
);
52+
const rateLimit = Number.isFinite(configuredRateLimit)
53+
? configuredRateLimit
54+
: PROJECT_CREATION_RATE_LIMIT;
55+
const result = await checkProjectCreationRateLimit(db, identifier, rateLimit);
4956

5057
// Attach project-creation-specific rate limit headers
51-
c.header("X-RateLimit-Limit-ProjectCreation", String(5));
58+
c.header("X-RateLimit-Limit-ProjectCreation", String(rateLimit));
5259
c.header("X-RateLimit-Remaining-ProjectCreation", String(result.remaining));
5360

5461
if (!result.allowed) {
@@ -59,7 +66,7 @@ export const projectCreationRateLimit = createMiddleware<{
5966
{
6067
error: {
6168
code: "RATE_LIMITED",
62-
message: `Project creation rate limit exceeded. Maximum 5 projects per minute. Retry after ${result.retryAfter} seconds.`,
69+
message: `Project creation rate limit exceeded. Maximum ${rateLimit} projects per minute. Retry after ${result.retryAfter} seconds.`,
6370
},
6471
},
6572
429,

packages/api/src/middleware/rate-limit.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ const slidingWindowRateLimit = async (
4545
c: Context<{ Bindings: Bindings; Variables: Variables }>,
4646
apiKeyHash: string,
4747
now: number,
48+
rateLimit: number,
4849
): Promise<SlidingWindowResult> => {
4950
const kv = c.env.RATE_LIMITS;
5051
if (!kv) {
@@ -66,7 +67,7 @@ const slidingWindowRateLimit = async (
6667
state.timestamps = state.timestamps.filter((t) => t > cutoff);
6768
const count = state.timestamps.length;
6869

69-
if (count >= RATE_LIMIT) {
70+
if (count >= rateLimit) {
7071
// Rate limit exceeded — calculate retry-after based on oldest request
7172
const oldestTimestamp = state.timestamps[0];
7273
const retryAfter = Math.ceil((oldestTimestamp + WINDOW_MS - now) / 1000);
@@ -110,6 +111,7 @@ const fixedWindowRateLimit = async (
110111
c: Context<{ Bindings: Bindings; Variables: Variables }>,
111112
apiKeyHash: string,
112113
now: number,
114+
rateLimit: number,
113115
): Promise<FixedWindowResult> => {
114116
const db = c.get("db");
115117

@@ -164,7 +166,7 @@ const fixedWindowRateLimit = async (
164166
}
165167

166168
return {
167-
allowed: currentCount <= RATE_LIMIT,
169+
allowed: currentCount <= rateLimit,
168170
count: currentCount,
169171
windowEnd,
170172
};
@@ -182,6 +184,8 @@ export const rateLimitMiddleware = createMiddleware<{
182184
const apiKeyHash = c.get("apiKeyHash");
183185

184186
const now = Date.now();
187+
const configuredRateLimit = Number((c.env as { RATE_LIMIT_MAX?: string }).RATE_LIMIT_MAX);
188+
const rateLimit = Number.isFinite(configuredRateLimit) ? configuredRateLimit : RATE_LIMIT;
185189
// Feature flag: set USE_SLIDING_WINDOW environment variable to "true" to enable
186190
const useSlidingWindow = (c.env as { USE_SLIDING_WINDOW?: string }).USE_SLIDING_WINDOW === "true";
187191

@@ -194,7 +198,7 @@ export const rateLimitMiddleware = createMiddleware<{
194198

195199
if (useSlidingWindow && c.env.RATE_LIMITS) {
196200
// Use sliding window with KV
197-
const kvResult = await slidingWindowRateLimit(c, apiKeyHash, now);
201+
const kvResult = await slidingWindowRateLimit(c, apiKeyHash, now, rateLimit);
198202
if (kvResult) {
199203
result = {
200204
allowed: kvResult.allowed,
@@ -203,7 +207,7 @@ export const rateLimitMiddleware = createMiddleware<{
203207
};
204208
} else {
205209
// KV fallback — use fixed window
206-
const fixedResult = await fixedWindowRateLimit(c, apiKeyHash, now);
210+
const fixedResult = await fixedWindowRateLimit(c, apiKeyHash, now, rateLimit);
207211
result = {
208212
allowed: fixedResult.allowed,
209213
count: fixedResult.count,
@@ -212,7 +216,7 @@ export const rateLimitMiddleware = createMiddleware<{
212216
}
213217
} else {
214218
// Use fixed window with D1
215-
const fixedResult = await fixedWindowRateLimit(c, apiKeyHash, now);
219+
const fixedResult = await fixedWindowRateLimit(c, apiKeyHash, now, rateLimit);
216220
result = {
217221
allowed: fixedResult.allowed,
218222
count: fixedResult.count,
@@ -221,7 +225,7 @@ export const rateLimitMiddleware = createMiddleware<{
221225
}
222226

223227
const { allowed, count, retryAfter, windowEnd } = result;
224-
const remaining = Math.max(0, RATE_LIMIT - count);
228+
const remaining = Math.max(0, rateLimit - count);
225229

226230
// Calculate retry-after and reset time
227231
let retryAfterSeconds: number;
@@ -242,7 +246,7 @@ export const rateLimitMiddleware = createMiddleware<{
242246
}
243247

244248
// Always attach rate limit headers on every response.
245-
c.header("X-RateLimit-Limit", String(RATE_LIMIT));
249+
c.header("X-RateLimit-Limit", String(rateLimit));
246250
c.header("X-RateLimit-Remaining", String(remaining));
247251
c.header("X-RateLimit-Reset", String(resetSeconds)); // Unix seconds
248252

@@ -252,7 +256,7 @@ export const rateLimitMiddleware = createMiddleware<{
252256
{
253257
error: {
254258
code: "RATE_LIMITED",
255-
message: `Rate limit exceeded. Maximum ${RATE_LIMIT} requests per minute. Retry after ${retryAfterSeconds} seconds.`,
259+
message: `Rate limit exceeded. Maximum ${rateLimit} requests per minute. Retry after ${retryAfterSeconds} seconds.`,
256260
},
257261
},
258262
429,

packages/api/src/services/projects.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,7 @@ export async function hashIdentifier(input: string): Promise<string> {
466466
export async function checkProjectCreationRateLimit(
467467
db: DrizzleD1Database,
468468
identifier: string,
469+
rateLimit = PROJECT_CREATION_RATE_LIMIT,
469470
): Promise<{
470471
allowed: boolean;
471472
remaining: number;
@@ -515,9 +516,9 @@ export async function checkProjectCreationRateLimit(
515516
currentCount = 1;
516517
}
517518

518-
const remaining = Math.max(0, PROJECT_CREATION_RATE_LIMIT - currentCount);
519+
const remaining = Math.max(0, rateLimit - currentCount);
519520

520-
if (currentCount > PROJECT_CREATION_RATE_LIMIT) {
521+
if (currentCount > rateLimit) {
521522
const windowEnd = windowStart + PROJECT_CREATION_WINDOW_MS;
522523
const retryAfter = Math.ceil((windowEnd - now) / 1000);
523524
const resetSeconds = Math.ceil(windowEnd / 1000);

packages/api/test/projects.test.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { env, SELF } from "cloudflare:test";
22
import { beforeAll, describe, expect, it } from "vitest";
33
import { applyMigrations, seedProject } from "./setup";
44

5+
const TEST_PROJECT_CREATION_RATE_LIMIT = 10000;
6+
57
// ---------------------------------------------------------------------------
68
// Additional typed response shapes (for new dashboard routes)
79
// ---------------------------------------------------------------------------
@@ -199,7 +201,9 @@ describe("Projects (/api/v1/projects)", () => {
199201
});
200202
expect(res.status).toBe(201);
201203

202-
expect(res.headers.get("X-RateLimit-Limit-ProjectCreation")).toBe("5");
204+
expect(res.headers.get("X-RateLimit-Limit-ProjectCreation")).toBe(
205+
String(TEST_PROJECT_CREATION_RATE_LIMIT),
206+
);
203207
expect(res.headers.get("X-RateLimit-Remaining-ProjectCreation")).toBeTruthy();
204208
});
205209

@@ -217,21 +221,23 @@ describe("Projects (/api/v1/projects)", () => {
217221
const windowStart = now - (now % 60_000);
218222
const rateLimitId = `pc:${ipIdentifier}:${windowStart}`;
219223

220-
// Insert a project-creation rate limit row at the limit (5 already consumed)
224+
// Insert a project-creation rate limit row at the configured test limit.
221225
await env.DB.prepare(
222226
`INSERT INTO rate_limits (id, api_key_hash, window_start, request_count, updated_at)
223227
VALUES (?, ?, ?, ?, ?)`,
224228
)
225-
.bind(rateLimitId, ipIdentifier, windowStart, 5, now)
229+
.bind(rateLimitId, ipIdentifier, windowStart, TEST_PROJECT_CREATION_RATE_LIMIT, now)
226230
.run();
227231

228-
// The next request should be over the limit (count becomes 6 > 5)
232+
// The next request should be over the configured limit.
229233
const res = await createProject({ name: "Over Limit", slug: `over-limit-${Date.now()}` });
230234
expect(res.status).toBe(429);
231235

232236
const body = await res.json<{ error: { code: string; message: string } }>();
233237
expect(body.error.code).toBe("RATE_LIMITED");
234-
expect(body.error.message).toContain("5 projects per minute");
238+
expect(body.error.message).toContain(
239+
`${TEST_PROJECT_CREATION_RATE_LIMIT} projects per minute`,
240+
);
235241
expect(res.headers.get("Retry-After")).toBeTruthy();
236242
expect(res.headers.get("X-RateLimit-Remaining-ProjectCreation")).toBe("0");
237243
});

packages/api/test/rate-limit.test.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { env, SELF } from "cloudflare:test";
2-
import { beforeAll, describe, expect, it } from "vitest";
2+
import { afterEach, beforeAll, describe, expect, it } from "vitest";
33
import { applyMigrations, authHeaders, seedProject, TEST_API_KEY } from "./setup";
44

5+
const TEST_RATE_LIMIT = 10000;
6+
57
// ---------------------------------------------------------------------------
68
// Tests
79
// ---------------------------------------------------------------------------
@@ -12,6 +14,10 @@ describe("Rate Limiting", () => {
1214
await seedProject();
1315
});
1416

17+
afterEach(async () => {
18+
await env.DB.prepare("DELETE FROM rate_limits").run();
19+
});
20+
1521
// -------------------------------------------------------------------------
1622
// Rate limit headers
1723
// -------------------------------------------------------------------------
@@ -22,7 +28,7 @@ describe("Rate Limiting", () => {
2228
});
2329
expect(res.status).toBe(200);
2430

25-
expect(res.headers.get("X-RateLimit-Limit")).toBe("100");
31+
expect(res.headers.get("X-RateLimit-Limit")).toBe(String(TEST_RATE_LIMIT));
2632
expect(res.headers.get("X-RateLimit-Remaining")).toBeTruthy();
2733
expect(res.headers.get("X-RateLimit-Reset")).toBeTruthy();
2834
});
@@ -56,12 +62,12 @@ describe("Rate Limiting", () => {
5662
const now = Date.now();
5763
const windowStart = now - (now % 60_000);
5864

59-
// Insert a rate limit row at the limit (100 requests already consumed)
65+
// Insert a rate limit row at the configured test limit.
6066
await env.DB.prepare(
6167
`INSERT INTO rate_limits (id, api_key_hash, window_start, request_count, updated_at)
6268
VALUES (?, ?, ?, ?, ?)`,
6369
)
64-
.bind("rl_test_429", keyHash, windowStart, 100, now)
70+
.bind("rl_test_429", keyHash, windowStart, TEST_RATE_LIMIT, now)
6571
.run();
6672

6773
// The next request should be over the limit (count becomes 101 > 100)
@@ -89,7 +95,7 @@ describe("Rate Limiting", () => {
8995
`INSERT INTO rate_limits (id, api_key_hash, window_start, request_count, updated_at)
9096
VALUES (?, ?, ?, ?, ?)`,
9197
)
92-
.bind("rl_test_reset", keyHash, pastWindowStart, 100, pastWindow)
98+
.bind("rl_test_reset", keyHash, pastWindowStart, TEST_RATE_LIMIT, pastWindow)
9399
.run();
94100

95101
// Request in the current window should succeed (new window, count = 1)

packages/api/test/setup.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,29 @@ export async function seedProject(): Promise<void> {
266266
const keyHash = await computeSHA256Hex(TEST_API_KEY);
267267
const keyPrefix = TEST_API_KEY.substring(0, 12);
268268

269+
for (const table of [
270+
"claim_verification_runs",
271+
"claim_evidence",
272+
"claims",
273+
"state_leases",
274+
"capability_tokens",
275+
"idempotency_keys",
276+
"state_tags",
277+
"state_snapshots",
278+
"state_events",
279+
"agent_states",
280+
"webhooks",
281+
"conversation_tags",
282+
"messages",
283+
"conversations",
284+
"api_keys",
285+
"projects",
286+
"organizations",
287+
"rate_limits",
288+
]) {
289+
await env.DB.prepare(`DELETE FROM ${table}`).run();
290+
}
291+
269292
await env.DB.prepare(
270293
`INSERT OR IGNORE INTO organizations (id, clerk_org_id, name, created_at) VALUES (?, ?, ?, ?)`,
271294
)

packages/api/test/tags.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -406,13 +406,14 @@ describe("Tags", () => {
406406

407407
describe("GET /v1/conversations?tag=", () => {
408408
it("filters conversations by valid tag", async () => {
409+
const filterTag = `filter-bug-${Date.now()}`;
409410
// Create conversations with different tags
410411
const res1 = await createConversation({ title: "Bug Report" });
411412
const conv1 = await res1.json<ConversationWithMessages>();
412413
await SELF.fetch(`http://localhost/v1/conversations/${conv1.id}/tags`, {
413414
method: "POST",
414415
headers: authHeaders(),
415-
body: JSON.stringify({ tags: ["bug"] }),
416+
body: JSON.stringify({ tags: [filterTag] }),
416417
});
417418

418419
const res2 = await createConversation({ title: "Feature Request" });
@@ -424,9 +425,12 @@ describe("Tags", () => {
424425
});
425426

426427
// Filter by tag
427-
const filterRes = await SELF.fetch("http://localhost/v1/conversations?tag=bug", {
428-
headers: authHeaders(),
429-
});
428+
const filterRes = await SELF.fetch(
429+
`http://localhost/v1/conversations?tag=${filterTag}`,
430+
{
431+
headers: authHeaders(),
432+
},
433+
);
430434
expect(filterRes.status).toBe(200);
431435

432436
const body = await filterRes.json<{ data: Array<{ id: string }> }>();

0 commit comments

Comments
 (0)