Fix rate-limit bypass vuln - #639
Conversation
…o add-upstash-rate-limits-and # Conflicts: # apps/web/src/hooks/use-persistent-chat.ts # apps/web/src/routes/api/typing.ts
Co-authored-by: cubic[bot] <cubic[bot]@users.noreply.github.com>
Co-authored-by: cubic[bot] <cubic[bot]@users.noreply.github.com>
- cleanup.ts: require token unconditionally, timing-safe compare, off-by-one fix - crons.ts: timing-safe token comparison - generate-title.ts: remove x-convex-token forwarding to QStash - delete-account.ts: remove x-convex-token forwarding and trust - export-chat.ts: remove x-convex-token forwarding and trust - backgroundStream.ts: restore increment-before-search TOCTOU fix, strict maxSteps validation - server-auth.ts: don't fall through to cookie on 5xx errors
- cleanup.ts: compare buffer byte lengths in safeCompare (non-ASCII safety) - server-auth.ts: only return null on 4xx, let 5xx fall through to cookie - crons.ts: avoid leaking token length in constant-time comparison
- cleanup.ts: constant-time safeCompare without length leak, stop forwarding WORKFLOW_CLEANUP_TOKEN to QStash - delete-account.ts: bounded batch loops (MAX_DELETE_BATCHES=500), rate limiting - export-chat.ts: apply exportRatelimit before processing - generate-title.ts: add rate limiting before workflow trigger - upstashUsage.ts: validate dateKey to prevent NaN/forever-TTL keys - check-redis.ts: add 5s fetch timeout - server/package.json: quote $VERCEL_GIT_COMMIT_REF - upstash.ts: remove unused chatIpRatelimit/chatRatelimit exports
Replace hand-rolled XOR loop with crypto.createHmac('sha256') +
timingSafeEqual for verified constant-time comparison in Node.js runtime.
…s and error handling - Move runCleanupBatchForWorkflow to cleanupAction.ts with 'use node' for crypto.timingSafeEqual (fixes hand-rolled XOR timing side-channel in crons.ts) - Keep as public action() so ConvexHttpClient can call it (token-gated via HMAC) - Fix server-auth.ts: return null when token endpoint returns 200 OK with no token (prevents auth bypass via cookie fallback) - Move getMidnightUtcEpochSeconds inside try/catch in upstashUsage.ts - Update cleanup.ts workflow to reference new api.cleanupAction path - Remove stale comment from crons.ts
…nd bounds validation - Remove session cookie forwarding from all 3 workflow trigger headers (generate-title, export-chat, delete-account) — cookies no longer sent to third-party QStash infrastructure - Pass pre-resolved auth token via payload instead of cookie headers - Remove getAuthTokenFromWorkflowHeaders from all workflow files - Add IP-based rate limiting to /api/models endpoint (30 req/60s) - Add 10s fetch timeout to OpenRouter models call - Fix OpenRouter proxy: use Accept header instead of Content-Type on GET, return 502 for upstream errors instead of forwarding status codes - Fix backgroundStream.ts double-counting: separate Convex mutation retry from Upstash increment to prevent billing double-charge on retry - Add bounds validation to cleanupAction.ts (retentionDays 1-3650, batchSize 1-1000) - Use env-based HMAC key in cleanupAction.ts safeCompare - Log warning when Upstash Redis is not configured (rate limiting disabled)
…ndle fetch errors - Fix P0: all 3 workflow payload parsers now pass through authToken field (generate-title, export-chat, delete-account) — prevents 'Unauthorized' on every workflow execution - Remove hardcoded HMAC key from safeCompare in cleanup.ts and cleanupAction.ts — key is now passed as parameter from the validated env var, eliminating static key in source code - Wrap fetchModelsFromOpenRouter in try/catch to handle timeout and network errors gracefully (returns 502 instead of unhandled 500) - Use Math.ceil for sub-cent usageCents in upstashUsage.ts to prevent silent usage loss from Math.floor(0.5) = 0
- Add Number.isFinite() checks to retentionDays and batchSize validation in cleanupAction.ts (NaN bypasses comparison operators) - Log OpenRouter fetch failures in models.ts catch block for observability
…odel endpoints - Replace QStash payload authToken with short-lived authTokenRef keys stored in Upstash Redis for generate-title/export-chat/delete-account workflows - Make cleanup batch executor internalAction and add Convex HTTP bridge endpoint to avoid public action exposure - Harden /api/models with trusted IP extraction, missing-IP rejection, and fully wrapped upstream fetch/body handling - Fix stream meta parsing for Upstash auto-deserialized objects and batch delete readStatus/promptTemplates in users cleanup - Add workflow payload validation, IPv6 localhost support, and clean indentation issues flagged by reviews
…handling - Encrypt workflow auth tokens at rest and consume via getdel with strict key-prefix validation - Harden Convex cleanup batch HTTP bridge with typed error status mapping - Require full QStash config (URL+token) and add production misconfig warning - Tighten models IP derivation with trust-proxy gating and Retry-After on 429 - Add provisional atomic usage reservation/adjustment path to reduce daily-limit race window - Normalize IPv6 localhost checks and finish remaining formatting/alignment fixes
* fix(web): validate chat ownership before Redis stream init Add Convex-based chat ownership validation in both GET and POST handlers of /api/chat to prevent unauthorized Redis stream access. Previously, an authenticated user could use another user's chatId to overwrite stream metadata and read streaming tokens. - GET handler: validate ownership via api.chats.get before reading stream - POST handler: validate ownership via api.chats.get before redis.stream.init Closes OSS-22 * fix(api): add chat ownership validation before redis stream operations * fix(chat): reuse convex client to eliminate redundant http request --------- Co-authored-by: Tembo <tembo@opencode.ai> Co-authored-by: tembo[bot] <208362400+tembo[bot]@users.noreply.github.com>
- Remove dead jonMode/dynamicPrompt fields from validator, schema, client store, hooks, and settings UI - Use exact-match error classification in http.ts instead of fragile substring matching - Fix inconsistent indentation in chats.ts JSON.stringify - Promote chatReadStatuses/promptTemplates deletion to workflow steps for proper transaction isolation - Deprecate deleteAccount mutation in favor of workflow-based deleteAccountWorkflowStep - Use static error message in app-sidebar toast instead of raw error.message - Fix DELETE rate-limit message in openrouter-key.ts
Add Upstash rate limits and secure auto chat titles
…isting documents Existing production streamJobs documents contain these fields. Convex schema validation rejects documents with extra fields, so they must remain in the schema until a migration removes them from all documents.
|
Requesting review from @leoisadev1 who has experience with the following files modified in this PR:
|
There was a problem hiding this comment.
12 issues found across 43 files
Confidence score: 2/5
- High risk:
apps/web/src/routes/api/models.tstrustsx-forwarded-forwithout validatingTRUSTED_PROXY_IPS, which is a concrete security vulnerability. apps/web/src/routes/api/workflow/generate-title.tshas a non-idempotent OpenRouterfetchoutsidecontext.run(), so Upstash Workflow replays can trigger duplicate external calls.- Several runtime correctness issues (race in
apps/server/convex/lib/upstashUsage.ts, optionaluserIdcast inapps/web/src/routes/api/workflow/export-chat.ts, and auth fallback ignoringALLOW_AUTH_COOKIE_FALLBACKinapps/web/src/lib/server-auth.ts) add user-facing risk, which drives the lower score. - Pay close attention to
apps/web/src/routes/api/models.ts,apps/web/src/routes/api/workflow/generate-title.ts,apps/server/convex/lib/upstashUsage.ts,apps/web/src/routes/api/workflow/export-chat.ts,apps/web/src/lib/server-auth.ts- security trust checks, workflow replay safety, atomicity, null guards, and auth gating.
Prompt for AI agents (all issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/web/src/routes/api/workflow/generate-title.ts">
<violation number="1" location="apps/web/src/routes/api/workflow/generate-title.ts:249">
P1: The OpenRouter API `fetch` call is not wrapped in a `context.run()` or `context.call()` step. Upstash Workflow replays the `serve` handler multiple times, skipping completed steps. Since this HTTP call isn't a step, it will be re-executed on every replay/retry — causing duplicate API calls, wasted credits, and potential inconsistent state.
Prefer `context.call()` for external HTTP requests (it offloads the call to Upstash and persists the result), or at minimum wrap the fetch in `context.run("call-llm", async () => { ... })`.</violation>
</file>
<file name="apps/web/src/components/app-sidebar.tsx">
<violation number="1" location="apps/web/src/components/app-sidebar.tsx:454">
P2: Fragile rate-limit detection: matching on error message content instead of the structured `error.name`. The server explicitly sets `error.name = "RateLimitError"` for this purpose, and line 520 in this same file still uses the `error.name` check. If the server-side action string changes from `"title generations"`, this detection silently breaks. Consider using `error.name === "RateLimitError"` for consistency and reliability.</violation>
</file>
<file name="apps/web/src/routes/api/workflow/export-chat.ts">
<violation number="1" location="apps/web/src/routes/api/workflow/export-chat.ts:124">
P2: `userId` is optional in `ExportChatPayload` but is cast to `Id<"users">` without a null check. If `userId` is `undefined`, this will cause a Convex validation error at runtime. Add a guard before the query call.</violation>
</file>
<file name="apps/web/src/routes/api/workflow/delete-account.ts">
<violation number="1" location="apps/web/src/routes/api/workflow/delete-account.ts:106">
P2: Four helper functions (`isLocalWorkflowExecutionEnabled`, `getWorkflowTriggerHeaders`, `getWorkflowCallbackUrl`, `hasWorkflowSigningKeysConfigured`) are duplicated verbatim across at least three workflow route files. Extract them into a shared utility module (e.g., `@/lib/workflow-utils.ts`) to avoid maintenance risk and inconsistency.</violation>
</file>
<file name="apps/web/src/lib/server-auth.ts">
<violation number="1" location="apps/web/src/lib/server-auth.ts:82">
P2: `ALLOW_AUTH_COOKIE_FALLBACK` is declared and validated but never actually checked in the fallback path. The cookie-based JWT fallback is unconditionally enabled when `IS_LOCAL_DEV` is true, ignoring the opt-in flag. Either gate the fallback on `ALLOW_AUTH_COOKIE_FALLBACK` or remove the dead constant.</violation>
</file>
<file name="apps/server/convex/users.ts">
<violation number="1" location="apps/server/convex/users.ts:870">
P2: `batchSize` argument is not forwarded for `delete-chat-read-statuses` and `delete-prompt-templates` steps, unlike all other steps in this action. This means callers cannot control batch size for these two steps, and they'll always use the default (100). This looks like an oversight since the underlying mutations accept `batchSize`.</violation>
</file>
<file name="apps/web/src/stores/ui.ts">
<violation number="1" location="apps/web/src/stores/ui.ts:34">
P3: Inconsistent indentation: the re-added state properties use 9-space indentation while the rest of the object (action methods) uses 8-space indentation. This appears to be an accidental extra space introduced during the removal of `jonMode`/`dynamicPrompt`.</violation>
</file>
<file name="apps/server/convex/lib/upstashUsage.ts">
<violation number="1" location="apps/server/convex/lib/upstashUsage.ts:145">
P1: Race condition: the negative-balance clamp (SET key 0) is not atomic with the preceding INCRBY. A concurrent `incrementDailyUsageInUpstash` or `reserveDailyUsageInUpstash` between the two pipeline calls will have its increment silently discarded when the SET overwrites the key. Consider using a Lua script (`EVAL`) to make the decrement-and-clamp atomic, e.g.:
```lua
local v = redis.call('INCRBY', KEYS[1], ARGV[1])
if v < 0 then redis.call('SET', KEYS[1], 0) end
redis.call('EXPIREAT', KEYS[1], ARGV[2])
return v
```</violation>
</file>
<file name="apps/server/convex/cleanupAction.ts">
<violation number="1" location="apps/server/convex/cleanupAction.ts:38">
P2: Validation mismatch: `Number.isFinite` allows non-integer values (e.g. `1.5`) that the downstream `cleanupSoftDeletedRecords` mutation rejects with `Number.isInteger`. This causes a confusing error from the wrong layer. Use `Number.isInteger` here to match the mutation's validation and fail early with a clear message.</violation>
</file>
<file name="apps/web/src/routes/api/models.ts">
<violation number="1" location="apps/web/src/routes/api/models.ts:124">
P0: **Security: `TRUSTED_PROXY_IPS` is never actually validated against the connecting IP.** The code only checks `TRUSTED_PROXY_IPS.size === 0` to decide whether to trust `x-forwarded-for`, but never verifies that the request actually originated from a listed proxy IP. Any client can bypass rate limiting by spoofing `x-forwarded-for` as long as `TRUSTED_PROXIES` is set to any non-empty value. The set must be checked against the actual connecting IP (e.g., from the underlying socket/connection) before trusting forwarded headers.</violation>
</file>
<file name="apps/server/convex/chats.test.ts">
<violation number="1" location="apps/server/convex/chats.test.ts:1287">
P2: Missing `fetch` mock in second `generateAndSetTitleInternal` test. If the action implementation calls `fetch` before checking the existing title, this test will attempt a real network request instead of using a mock, making it flaky and environment-dependent. Add a `fetch` mock (or move the shared mock into `beforeEach`) to ensure the test is fully isolated.</violation>
</file>
<file name="apps/web/src/routes/api/workflow/cleanup.ts">
<violation number="1" location="apps/web/src/routes/api/workflow/cleanup.ts:182">
P2: Payload validation throws outside a `context.run` step, which may cause unnecessary QStash retries on invalid payloads. Wrap the parsing in a `context.run` step (as done in `generate-title.ts`) so the framework can track the failure and avoid retry storms.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| return null; | ||
| } | ||
|
|
||
| if (TRUST_PROXY_MODE === "true") { |
There was a problem hiding this comment.
P0: Security: TRUSTED_PROXY_IPS is never actually validated against the connecting IP. The code only checks TRUSTED_PROXY_IPS.size === 0 to decide whether to trust x-forwarded-for, but never verifies that the request actually originated from a listed proxy IP. Any client can bypass rate limiting by spoofing x-forwarded-for as long as TRUSTED_PROXIES is set to any non-empty value. The set must be checked against the actual connecting IP (e.g., from the underlying socket/connection) before trusting forwarded headers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/routes/api/models.ts, line 124:
<comment>**Security: `TRUSTED_PROXY_IPS` is never actually validated against the connecting IP.** The code only checks `TRUSTED_PROXY_IPS.size === 0` to decide whether to trust `x-forwarded-for`, but never verifies that the request actually originated from a listed proxy IP. Any client can bypass rate limiting by spoofing `x-forwarded-for` as long as `TRUSTED_PROXIES` is set to any non-empty value. The set must be checked against the actual connecting IP (e.g., from the underlying socket/connection) before trusting forwarded headers.</comment>
<file context>
@@ -0,0 +1,201 @@
+ return null;
+ }
+
+ if (TRUST_PROXY_MODE === "true") {
+ // Without TRUSTED_PROXIES configured, x-forwarded-for is spoofable.
+ // Fail closed: return null so the request is rejected with a 400,
</file context>
| let llmResponseStatus = 0; | ||
| let llmResponseBody: { choices?: Array<{ message?: { content?: string } }> } | null = null; | ||
| try { | ||
| const response = await fetch("https://openrouter.ai/api/v1/chat/completions", { |
There was a problem hiding this comment.
P1: The OpenRouter API fetch call is not wrapped in a context.run() or context.call() step. Upstash Workflow replays the serve handler multiple times, skipping completed steps. Since this HTTP call isn't a step, it will be re-executed on every replay/retry — causing duplicate API calls, wasted credits, and potential inconsistent state.
Prefer context.call() for external HTTP requests (it offloads the call to Upstash and persists the result), or at minimum wrap the fetch in context.run("call-llm", async () => { ... }).
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/routes/api/workflow/generate-title.ts, line 249:
<comment>The OpenRouter API `fetch` call is not wrapped in a `context.run()` or `context.call()` step. Upstash Workflow replays the `serve` handler multiple times, skipping completed steps. Since this HTTP call isn't a step, it will be re-executed on every replay/retry — causing duplicate API calls, wasted credits, and potential inconsistent state.
Prefer `context.call()` for external HTTP requests (it offloads the call to Upstash and persists the result), or at minimum wrap the fetch in `context.run("call-llm", async () => { ... })`.</comment>
<file context>
@@ -0,0 +1,438 @@
+ let llmResponseStatus = 0;
+ let llmResponseBody: { choices?: Array<{ message?: { content?: string } }> } | null = null;
+ try {
+ const response = await fetch("https://openrouter.ai/api/v1/chat/completions", {
+ method: "POST",
+ headers: {
</file context>
|
|
||
| try { | ||
| const expiresAt = getMidnightUtcEpochSeconds(dateKey); | ||
| const result = await executePipeline([ |
There was a problem hiding this comment.
P1: Race condition: the negative-balance clamp (SET key 0) is not atomic with the preceding INCRBY. A concurrent incrementDailyUsageInUpstash or reserveDailyUsageInUpstash between the two pipeline calls will have its increment silently discarded when the SET overwrites the key. Consider using a Lua script (EVAL) to make the decrement-and-clamp atomic, e.g.:
local v = redis.call('INCRBY', KEYS[1], ARGV[1])
if v < 0 then redis.call('SET', KEYS[1], 0) end
redis.call('EXPIREAT', KEYS[1], ARGV[2])
return vPrompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/convex/lib/upstashUsage.ts, line 145:
<comment>Race condition: the negative-balance clamp (SET key 0) is not atomic with the preceding INCRBY. A concurrent `incrementDailyUsageInUpstash` or `reserveDailyUsageInUpstash` between the two pipeline calls will have its increment silently discarded when the SET overwrites the key. Consider using a Lua script (`EVAL`) to make the decrement-and-clamp atomic, e.g.:
```lua
local v = redis.call('INCRBY', KEYS[1], ARGV[1])
if v < 0 then redis.call('SET', KEYS[1], 0) end
redis.call('EXPIREAT', KEYS[1], ARGV[2])
return v
```</comment>
<file context>
@@ -0,0 +1,200 @@
+
+ try {
+ const expiresAt = getMidnightUtcEpochSeconds(dateKey);
+ const result = await executePipeline([
+ ["INCRBY", key, roundedCents],
+ ["EXPIREAT", key, expiresAt],
</file context>
| if (error instanceof Error && error.name === "RateLimitError") { | ||
| toast.error(error.message); | ||
| const message = error instanceof Error ? error.message : ""; | ||
| if (message.toLowerCase().includes("too many title generations")) { |
There was a problem hiding this comment.
P2: Fragile rate-limit detection: matching on error message content instead of the structured error.name. The server explicitly sets error.name = "RateLimitError" for this purpose, and line 520 in this same file still uses the error.name check. If the server-side action string changes from "title generations", this detection silently breaks. Consider using error.name === "RateLimitError" for consistency and reliability.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/components/app-sidebar.tsx, line 454:
<comment>Fragile rate-limit detection: matching on error message content instead of the structured `error.name`. The server explicitly sets `error.name = "RateLimitError"` for this purpose, and line 520 in this same file still uses the `error.name` check. If the server-side action string changes from `"title generations"`, this detection silently breaks. Consider using `error.name === "RateLimitError"` for consistency and reliability.</comment>
<file context>
@@ -422,28 +422,37 @@ export function AppSidebar({
- if (error instanceof Error && error.name === "RateLimitError") {
- toast.error(error.message);
+ const message = error instanceof Error ? error.message : "";
+ if (message.toLowerCase().includes("too many title generations")) {
+ toast.error("Too many title generations. Please try again later.");
} else {
</file context>
| const convexClient = createConvexServerClient(authToken); | ||
| const chatExportData = await convexClient.query(api.chats.getChatExportData, { | ||
| chatId: chatId as Id<"chats">, | ||
| userId: userId as Id<"users">, |
There was a problem hiding this comment.
P2: userId is optional in ExportChatPayload but is cast to Id<"users"> without a null check. If userId is undefined, this will cause a Convex validation error at runtime. Add a guard before the query call.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/routes/api/workflow/export-chat.ts, line 124:
<comment>`userId` is optional in `ExportChatPayload` but is cast to `Id<"users">` without a null check. If `userId` is `undefined`, this will cause a Convex validation error at runtime. Add a guard before the query call.</comment>
<file context>
@@ -0,0 +1,226 @@
+ const convexClient = createConvexServerClient(authToken);
+ const chatExportData = await convexClient.query(api.chats.getChatExportData, {
+ chatId: chatId as Id<"chats">,
+ userId: userId as Id<"users">,
+ });
+ if (!chatExportData) {
</file context>
| case "delete-chat-read-statuses": | ||
| return await ctx.runMutation(internal.users.deleteUserChatReadStatuses, { | ||
| userId, | ||
| }); | ||
| case "delete-prompt-templates": | ||
| return await ctx.runMutation(internal.users.deleteUserPromptTemplates, { | ||
| userId, | ||
| }); |
There was a problem hiding this comment.
P2: batchSize argument is not forwarded for delete-chat-read-statuses and delete-prompt-templates steps, unlike all other steps in this action. This means callers cannot control batch size for these two steps, and they'll always use the default (100). This looks like an oversight since the underlying mutations accept batchSize.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/convex/users.ts, line 870:
<comment>`batchSize` argument is not forwarded for `delete-chat-read-statuses` and `delete-prompt-templates` steps, unlike all other steps in this action. This means callers cannot control batch size for these two steps, and they'll always use the default (100). This looks like an oversight since the underlying mutations accept `batchSize`.</comment>
<file context>
@@ -598,18 +596,309 @@ export const updateName = mutation({
+ userId,
+ batchSize: args.batchSize,
+ });
+ case "delete-chat-read-statuses":
+ return await ctx.runMutation(internal.users.deleteUserChatReadStatuses, {
+ userId,
</file context>
| case "delete-chat-read-statuses": | |
| return await ctx.runMutation(internal.users.deleteUserChatReadStatuses, { | |
| userId, | |
| }); | |
| case "delete-prompt-templates": | |
| return await ctx.runMutation(internal.users.deleteUserPromptTemplates, { | |
| userId, | |
| }); | |
| case "delete-chat-read-statuses": | |
| return await ctx.runMutation(internal.users.deleteUserChatReadStatuses, { | |
| userId, | |
| batchSize: args.batchSize, | |
| }); | |
| case "delete-prompt-templates": | |
| return await ctx.runMutation(internal.users.deleteUserPromptTemplates, { | |
| userId, | |
| batchSize: args.batchSize, | |
| }); |
|
|
||
| // Bounds validation — prevent accidental full-purge or oversized batches | ||
| const retentionDays = args.retentionDays ?? 90; | ||
| if (!Number.isFinite(retentionDays) || retentionDays < 1 || retentionDays > 3650) { |
There was a problem hiding this comment.
P2: Validation mismatch: Number.isFinite allows non-integer values (e.g. 1.5) that the downstream cleanupSoftDeletedRecords mutation rejects with Number.isInteger. This causes a confusing error from the wrong layer. Use Number.isInteger here to match the mutation's validation and fail early with a clear message.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/convex/cleanupAction.ts, line 38:
<comment>Validation mismatch: `Number.isFinite` allows non-integer values (e.g. `1.5`) that the downstream `cleanupSoftDeletedRecords` mutation rejects with `Number.isInteger`. This causes a confusing error from the wrong layer. Use `Number.isInteger` here to match the mutation's validation and fail early with a clear message.</comment>
<file context>
@@ -0,0 +1,53 @@
+
+ // Bounds validation — prevent accidental full-purge or oversized batches
+ const retentionDays = args.retentionDays ?? 90;
+ if (!Number.isFinite(retentionDays) || retentionDays < 1 || retentionDays > 3650) {
+ throw new Error("retentionDays must be between 1 and 3650");
+ }
</file context>
| expect(chat?.title).toBe('Helpful Testing Title'); | ||
| }); | ||
|
|
||
| it('does not overwrite existing custom titles unless forced', async () => { |
There was a problem hiding this comment.
P2: Missing fetch mock in second generateAndSetTitleInternal test. If the action implementation calls fetch before checking the existing title, this test will attempt a real network request instead of using a mock, making it flaky and environment-dependent. Add a fetch mock (or move the shared mock into beforeEach) to ensure the test is fully isolated.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/convex/chats.test.ts, line 1287:
<comment>Missing `fetch` mock in second `generateAndSetTitleInternal` test. If the action implementation calls `fetch` before checking the existing title, this test will attempt a real network request instead of using a mock, making it flaky and environment-dependent. Add a `fetch` mock (or move the shared mock into `beforeEach`) to ensure the test is fully isolated.</comment>
<file context>
@@ -1111,3 +1111,194 @@ describe('chats.checkExportRateLimit', () => {
+ expect(chat?.title).toBe('Helpful Testing Title');
+ });
+
+ it('does not overwrite existing custom titles unless forced', async () => {
+ await t.run(async (ctx) => {
+ await ctx.db.patch(chatId, { title: 'Custom Existing Title' });
</file context>
| it('does not overwrite existing custom titles unless forced', async () => { | |
| it('does not overwrite existing custom titles unless forced', async () => { | |
| vi.spyOn(globalThis, 'fetch').mockResolvedValue( | |
| new Response( | |
| JSON.stringify({ | |
| choices: [{ message: { content: 'Should Not Be Used' } }], | |
| }), | |
| { | |
| status: 200, | |
| headers: { 'Content-Type': 'application/json' }, | |
| }, | |
| ), | |
| ); |
| } | ||
|
|
||
| const workflow = serve<CleanupPayload>(async (context) => { | ||
| const payload = parseCleanupPayload(context.requestPayload); |
There was a problem hiding this comment.
P2: Payload validation throws outside a context.run step, which may cause unnecessary QStash retries on invalid payloads. Wrap the parsing in a context.run step (as done in generate-title.ts) so the framework can track the failure and avoid retry storms.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/routes/api/workflow/cleanup.ts, line 182:
<comment>Payload validation throws outside a `context.run` step, which may cause unnecessary QStash retries on invalid payloads. Wrap the parsing in a `context.run` step (as done in `generate-title.ts`) so the framework can track the failure and avoid retry storms.</comment>
<file context>
@@ -0,0 +1,310 @@
+}
+
+const workflow = serve<CleanupPayload>(async (context) => {
+ const payload = parseCleanupPayload(context.requestPayload);
+ if (!payload) {
+ throw new Error("Invalid cleanup payload");
</file context>
| sidebarOpen: true, | ||
| sidebarCollapsed: false, | ||
| commandPaletteOpen: false, | ||
| filterStyle: "model" as FilterStyle, |
There was a problem hiding this comment.
P3: Inconsistent indentation: the re-added state properties use 9-space indentation while the rest of the object (action methods) uses 8-space indentation. This appears to be an accidental extra space introduced during the removal of jonMode/dynamicPrompt.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/stores/ui.ts, line 34:
<comment>Inconsistent indentation: the re-added state properties use 9-space indentation while the rest of the object (action methods) uses 8-space indentation. This appears to be an accidental extra space introduced during the removal of `jonMode`/`dynamicPrompt`.</comment>
<file context>
@@ -27,20 +25,16 @@ interface UIState {
- filterStyle: "model" as FilterStyle,
- jonMode: false,
- dynamicPrompt: true,
+ sidebarOpen: true,
+ sidebarCollapsed: false,
+ commandPaletteOpen: false,
</file context>
| sidebarOpen: true, | |
| sidebarCollapsed: false, | |
| commandPaletteOpen: false, | |
| filterStyle: "model" as FilterStyle, | |
| sidebarOpen: true, | |
| sidebarCollapsed: false, | |
| commandPaletteOpen: false, | |
| filterStyle: "model" as FilterStyle, |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Summary
Fixed rate-limit bypass vulnerability when
TRUST_PROXY=truewithout trusted proxy configuration. AddedTRUSTED_PROXIESenvironment variable to enforce proxy allowlisting and preventx-forwarded-forheader spoofing. WhenTRUST_PROXY=trueis set withoutTRUSTED_PROXIES, the system now:nullfor client IP (failing closed) to reject requests with a 400 errorChanges:
TRUSTED_PROXIESenv var parsing intoTRUSTED_PROXY_IPSsetTRUST_PROXY=truewithoutTRUSTED_PROXIES, log warning and fail closedgetClientIp()to returnnullwhenTRUST_PROXY=truebut no trusted proxies configuredFiles modified:
apps/web/src/routes/api/models.tsSummary by cubic
Fixes a rate-limit bypass by only trusting client IPs from known proxies or platform headers and rejecting unverifiable requests. Adds TRUSTED_PROXIES and IP-based rate limiting on /api/models.
Bug Fixes
Migration
Written for commit b4bb6ae. Summary will update on new commits.