Skip to content

Fix X-Forwarded-For bypass - #642

Merged
leoisadev1 merged 44 commits into
mainfrom
tembo/fix/rate-limit-x-forwarded-for-bypass
Feb 14, 2026
Merged

Fix X-Forwarded-For bypass#642
leoisadev1 merged 44 commits into
mainfrom
tembo/fix/rate-limit-x-forwarded-for-bypass

Conversation

@tembo

@tembo tembo Bot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixed rate limiting bypass vulnerability when TRUST_PROXY=true by:

  • Added IP validation (isValidIp()) using IPv4/IPv6 regex to reject malformed or spoofed values
  • Enhanced TRUST_PROXY=true mode to prefer platform-specific headers (cf-connecting-ip, x-vercel-forwarded-for) before falling back to generic X-Forwarded-For
  • Applied IP validation to all proxy modes (cloudflare, vercel, true)
  • Updated warning message to recommend secure platform-specific modes over generic TRUST_PROXY=true

Files modified:

  • apps/web/src/routes/api/models.ts

Want tembo to make any changes? Add a review or comment with @tembo and i'll get back to work!

View on Tembo View Agent Settings


Summary by cubic

Fix rate-limit bypass in /api/models by validating client IPs and gating extraction to trusted proxy modes. Blocks spoofed X-Forwarded-For and rejects requests when IP is missing or headers are untrusted.

  • Bug Fixes
    • Validate IPv4/IPv6 with isValidIp() across cloudflare, vercel, and true modes.
    • In TRUST_PROXY=true, prefer cf-connecting-ip and x-vercel-forwarded-for; only fall back to X-Forwarded-For when valid.
    • Fail closed when TRUST_PROXY is unset or invalid, with warnings to use platform-specific modes.

Written for commit 782c70c. Summary will update on new commits.

leoisadev1 and others added 30 commits February 11, 2026 11:30
…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>
leoisadev1 and others added 13 commits February 12, 2026 00:19
- 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.
@tembo tembo Bot added the tembo Pull request created by Tembo label Feb 13, 2026
@tembo
tembo Bot requested a review from leoisadev1 February 13, 2026 11:09
@railway-app

railway-app Bot commented Feb 13, 2026

Copy link
Copy Markdown

This PR was not deployed automatically as @tembo[bot] does not have access to the Railway project.

In order to get automatic PR deploys, please add @tembo[bot] to your workspace on Railway.

@tembo

tembo Bot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor Author

Requesting review from @leoisadev1 who has experience with the following files modified in this PR:

  • apps/web/src/routes/api/models.ts

@leoisadev1
leoisadev1 marked this pull request as ready for review February 14, 2026 02:29

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

17 issues found across 43 files

Confidence score: 2/5

  • High-risk behavioral bug: apps/web/src/routes/api/workflow/generate-title.ts runs the OpenRouter fetch outside context.run(), so it may execute multiple times per workflow step and cause repeated external calls.
  • Security/accuracy concerns in apps/web/src/routes/api/models.ts: client-controlled IP headers can be trusted behind generic proxies, and IPv4-mapped IPv6 addresses are rejected, which can break rate-limiting or access checks.
  • Multiple medium issues (race conditions and accounting leaks) in apps/server/convex/lib/upstashUsage.ts and apps/server/convex/backgroundStream.ts increase regression risk around usage tracking and refunds.
  • Pay close attention to apps/web/src/routes/api/workflow/generate-title.ts, apps/web/src/routes/api/models.ts, apps/server/convex/lib/upstashUsage.ts, apps/server/convex/backgroundStream.ts - repeated execution, IP trust/format handling, atomicity, and refund leakage.
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/models.ts">

<violation number="1" location="apps/web/src/routes/api/models.ts:14">
P1: IPv4-mapped IPv6 addresses (e.g., `::ffff:192.168.1.1`) are rejected because the regex doesn't include `.` in the character class. These are common in practice — Node.js and many proxies report IPv4 addresses in this format. Affected users would get 400 errors.

Include `.` in the character class to support IPv4-mapped/embedded addresses.</violation>

<violation number="2" location="apps/web/src/routes/api/models.ts:99">
P1: When behind a generic reverse proxy (not Cloudflare/Vercel), `cf-connecting-ip` and `x-vercel-forwarded-for` are **not** stripped by the proxy and are fully client-controlled, while `x-forwarded-for` is typically managed by the proxy. Preferring these headers over `x-forwarded-for` in `TRUST_PROXY=true` mode actually introduces a *new* spoofing vector — an attacker can set `cf-connecting-ip` to rotate through arbitrary IPs and bypass rate limiting.

Consider only using `x-forwarded-for` in `TRUST_PROXY=true` mode, or at minimum, falling back to `x-forwarded-for` *first* and only use platform-specific headers when the corresponding platform mode is selected.</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 defined and guarded in production but never actually checked in the fallback path. The cookie fallback is unconditionally enabled in local dev (`IS_LOCAL_DEV`), making this env var a no-op. The condition should also require `ALLOW_AUTH_COOKIE_FALLBACK` to be true.</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 string-based error detection: the server explicitly sets `error.name = "RateLimitError"` for this purpose. Matching on message content is brittle and will break if the server message format changes. Consider reverting to the name-based check, or at minimum matching on `"too many"` as a broader pattern.</violation>

<violation number="2" location="apps/web/src/components/app-sidebar.tsx:455">
P2: Hardcoded toast message loses the server-provided retry-after wait time. The server includes a specific duration (e.g., "in 30 seconds") in the error message, but this code discards it in favor of a generic "Please try again later." Consider using the actual error message: `toast.error(message)`.</violation>
</file>

<file name="apps/server/convex/lib/sanitize.ts">

<violation number="1" location="apps/server/convex/lib/sanitize.ts:44">
P2: HTML tag stripping is ordered after whitespace collapsing, so removing tags like `<b> </b>` can leave multiple consecutive spaces in the sanitized title. Move this line before the whitespace collapse step, or re-collapse spaces afterward.</violation>
</file>

<file name="apps/server/convex/cleanupAction.ts">

<violation number="1" location="apps/server/convex/cleanupAction.ts:38">
P2: Validation uses `Number.isFinite()` but the downstream mutation (`cleanupSoftDeletedRecords`) validates with `Number.isInteger()`. This allows fractional values (e.g. `1.5`) to pass action-level validation, only to fail inside the mutation. Use `Number.isInteger()` here to match the inner validation and fail early with a clear error.</violation>

<violation number="2" location="apps/server/convex/cleanupAction.ts:42">
P2: Same `Number.isFinite()` vs `Number.isInteger()` mismatch for `batchSize`. Use `Number.isInteger()` to match the downstream mutation's validation.</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 modified lines use 9-space indent while unchanged lines in the same object block use 8-space indent. This creates a visible misalignment between state properties and action methods within the same `(set) => ({...})` callback.</violation>
</file>

<file name="apps/web/src/lib/server-auth.test.ts">

<violation number="1" location="apps/web/src/lib/server-auth.test.ts:60">
P2: Weak test: the fallback cookie `fallback-token` is not a valid JWT, so this test would still pass even if the 4xx early-return were removed — the fallback path would also return `null` due to `isJwtNotExpired()` failing. Use `createJwt()` to create a valid fallback JWT (like the other fallback tests do), so the test truly verifies that a 401 prevents fallback.</violation>
</file>

<file name="apps/web/src/hooks/use-persistent-chat.ts">

<violation number="1" location="apps/web/src/hooks/use-persistent-chat.ts:1126">
P2: Inconsistent error handling: `forkMessage` still exposes raw `parsedError.message` to the user, while `editMessage` and `retryMessage` were updated in this PR to use `getUserFriendlyError()`. Apply the same sanitization here to avoid leaking internal error details.</violation>
</file>

<file name="apps/server/convex/lib/upstashUsage.ts">

<violation number="1" location="apps/server/convex/lib/upstashUsage.ts:171">
P2: `Math.ceil` on negative deltas silently drops small adjustments. For example, `Math.ceil(-0.5)` → `0`, which triggers the `roundedDelta === 0` early return, discarding the adjustment entirely. Consider using `Math.round` or rounding away from zero (e.g., `Math.sign(delta) * Math.ceil(Math.abs(delta))`) to ensure small negative adjustments aren't lost.</violation>

<violation number="2" location="apps/server/convex/lib/upstashUsage.ts:189">
P2: Race condition: the negative-total floor check and the subsequent `SET key 0` are not atomic. A concurrent `INCRBY` between the first pipeline response and the second pipeline execution could be overwritten by the `SET 0`, silently losing that increment. Consider using a Lua script (via Upstash `EVAL`) to make the check-and-set atomic.</violation>
</file>

<file name="apps/web/src/routes/api/workflow/generate-title.ts">

<violation number="1" location="apps/web/src/routes/api/workflow/generate-title.ts:246">
P1: The OpenRouter `fetch` call and its response handling are outside any `context.run()` step. In Upstash Workflow, the endpoint is re-invoked for every step in the run, and code outside `context.run()` executes every time. This means this fetch will fire redundantly on every step replay (e.g., when "save-title" runs), wasting OpenRouter API credits and compute.

The codebase's own `cleanup.ts` workflow correctly wraps its fetch calls inside `context.run()`. Consider either:
1. Wrapping this block in `context.run("call-llm", async () => { ... })` to make it a durable step, or
2. Using `context.call()` which offloads the HTTP call to Upstash infrastructure entirely.</violation>
</file>

<file name="apps/server/convex/users.ts">

<violation number="1" location="apps/server/convex/users.ts:871">
P2: `batchSize` is not forwarded to `deleteUserChatReadStatuses` and `deleteUserPromptTemplates`, unlike all other step cases. The caller passes `batchSize` for every step, but these two silently ignore it and always use the default (100). Add `batchSize: args.batchSize` for consistency.</violation>
</file>

<file name="apps/web/src/lib/redis.ts">

<violation number="1" location="apps/web/src/lib/redis.ts:62">
P2: `parseStreamMeta`: The string branch returns `JSON.parse(value) as StreamMeta` without validating the parsed result, but the object branch performs thorough field validation. This inconsistency means corrupted JSON in Redis could return an invalid `StreamMeta`. Consider reusing the object validation for the parsed JSON result.</violation>
</file>

<file name="apps/server/convex/backgroundStream.ts">

<violation number="1" location="apps/server/convex/backgroundStream.ts:815">
P2: Reserved Upstash usage (1 cent) is leaked when the "No API key available" early return is hit. The refund logic is only in the try/catch block which starts after this return. Add a refund before returning.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/web/src/routes/api/models.ts

// Basic IPv4 and IPv6 validation to reject obviously spoofed or malformed values.
const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
const IPV6_REGEX = /^[\da-fA-F:]+$/;

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: IPv4-mapped IPv6 addresses (e.g., ::ffff:192.168.1.1) are rejected because the regex doesn't include . in the character class. These are common in practice — Node.js and many proxies report IPv4 addresses in this format. Affected users would get 400 errors.

Include . in the character class to support IPv4-mapped/embedded addresses.

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 14:

<comment>IPv4-mapped IPv6 addresses (e.g., `::ffff:192.168.1.1`) are rejected because the regex doesn't include `.` in the character class. These are common in practice — Node.js and many proxies report IPv4 addresses in this format. Affected users would get 400 errors.

Include `.` in the character class to support IPv4-mapped/embedded addresses.</comment>

<file context>
@@ -0,0 +1,194 @@
+
+// Basic IPv4 and IPv6 validation to reject obviously spoofed or malformed values.
+const IPV4_REGEX = /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
+const IPV6_REGEX = /^[\da-fA-F:]+$/;
+
+function isValidIp(value: string): boolean {
</file context>
Fix with Cubic

TITLE_STYLE_PROMPTS[length],
].join(" ");

let llmResponseStatus = 0;

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The OpenRouter fetch call and its response handling are outside any context.run() step. In Upstash Workflow, the endpoint is re-invoked for every step in the run, and code outside context.run() executes every time. This means this fetch will fire redundantly on every step replay (e.g., when "save-title" runs), wasting OpenRouter API credits and compute.

The codebase's own cleanup.ts workflow correctly wraps its fetch calls inside context.run(). Consider either:

  1. Wrapping this block in context.run("call-llm", async () => { ... }) to make it a durable step, or
  2. Using context.call() which offloads the HTTP call to Upstash infrastructure entirely.
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 246:

<comment>The OpenRouter `fetch` call and its response handling are outside any `context.run()` step. In Upstash Workflow, the endpoint is re-invoked for every step in the run, and code outside `context.run()` executes every time. This means this fetch will fire redundantly on every step replay (e.g., when "save-title" runs), wasting OpenRouter API credits and compute.

The codebase's own `cleanup.ts` workflow correctly wraps its fetch calls inside `context.run()`. Consider either:
1. Wrapping this block in `context.run("call-llm", async () => { ... })` to make it a durable step, or
2. Using `context.call()` which offloads the HTTP call to Upstash infrastructure entirely.</comment>

<file context>
@@ -0,0 +1,438 @@
+		TITLE_STYLE_PROMPTS[length],
+	].join(" ");
+
+	let llmResponseStatus = 0;
+	let llmResponseBody: { choices?: Array<{ message?: { content?: string } }> } | null = null;
+	try {
</file context>
Fix with Cubic

if (response.status >= 400 && response.status < 500) {
return null;
}
if (!IS_LOCAL_DEV) return null;

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: ALLOW_AUTH_COOKIE_FALLBACK is defined and guarded in production but never actually checked in the fallback path. The cookie fallback is unconditionally enabled in local dev (IS_LOCAL_DEV), making this env var a no-op. The condition should also require ALLOW_AUTH_COOKIE_FALLBACK to be true.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/lib/server-auth.ts, line 82:

<comment>`ALLOW_AUTH_COOKIE_FALLBACK` is defined and guarded in production but never actually checked in the fallback path. The cookie fallback is unconditionally enabled in local dev (`IS_LOCAL_DEV`), making this env var a no-op. The condition should also require `ALLOW_AUTH_COOKIE_FALLBACK` to be true.</comment>

<file context>
@@ -16,18 +16,81 @@ type AuthSessionResponse = {
+			if (response.status >= 400 && response.status < 500) {
+				return null;
+			}
+			if (!IS_LOCAL_DEV) return null;
+		} catch {
+			if (!IS_LOCAL_DEV) return null;
</file context>
Fix with Cubic

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")) {

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Fragile string-based error detection: the server explicitly sets error.name = "RateLimitError" for this purpose. Matching on message content is brittle and will break if the server message format changes. Consider reverting to the name-based check, or at minimum matching on "too many" as a broader pattern.

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 string-based error detection: the server explicitly sets `error.name = "RateLimitError"` for this purpose. Matching on message content is brittle and will break if the server message format changes. Consider reverting to the name-based check, or at minimum matching on `"too many"` as a broader pattern.</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>
Suggested change
if (message.toLowerCase().includes("too many title generations")) {
if (error instanceof Error && error.name === "RateLimitError") {
Fix with Cubic

): Promise<void> {
if (!getConfig()) return;
if (!Number.isFinite(usageCentsDelta) || usageCentsDelta === 0) return;
const roundedDelta = Math.ceil(usageCentsDelta);

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Math.ceil on negative deltas silently drops small adjustments. For example, Math.ceil(-0.5)0, which triggers the roundedDelta === 0 early return, discarding the adjustment entirely. Consider using Math.round or rounding away from zero (e.g., Math.sign(delta) * Math.ceil(Math.abs(delta))) to ensure small negative adjustments aren't lost.

Prompt 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 171:

<comment>`Math.ceil` on negative deltas silently drops small adjustments. For example, `Math.ceil(-0.5)` → `0`, which triggers the `roundedDelta === 0` early return, discarding the adjustment entirely. Consider using `Math.round` or rounding away from zero (e.g., `Math.sign(delta) * Math.ceil(Math.abs(delta))`) to ensure small negative adjustments aren't lost.</comment>

<file context>
@@ -0,0 +1,200 @@
+): Promise<void> {
+	if (!getConfig()) return;
+	if (!Number.isFinite(usageCentsDelta) || usageCentsDelta === 0) return;
+	const roundedDelta = Math.ceil(usageCentsDelta);
+	if (roundedDelta === 0) return;
+
</file context>
Fix with Cubic

Comment on lines +871 to +873
return await ctx.runMutation(internal.users.deleteUserChatReadStatuses, {
userId,
});

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: batchSize is not forwarded to deleteUserChatReadStatuses and deleteUserPromptTemplates, unlike all other step cases. The caller passes batchSize for every step, but these two silently ignore it and always use the default (100). Add batchSize: args.batchSize for consistency.

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 871:

<comment>`batchSize` is not forwarded to `deleteUserChatReadStatuses` and `deleteUserPromptTemplates`, unlike all other step cases. The caller passes `batchSize` for every step, but these two silently ignore it and always use the default (100). Add `batchSize: args.batchSize` for consistency.</comment>

<file context>
@@ -598,18 +596,309 @@ export const updateName = mutation({
+					batchSize: args.batchSize,
+				});
+			case "delete-chat-read-statuses":
+				return await ctx.runMutation(internal.users.deleteUserChatReadStatuses, {
+					userId,
+				});
</file context>
Suggested change
return await ctx.runMutation(internal.users.deleteUserChatReadStatuses, {
userId,
});
return await ctx.runMutation(internal.users.deleteUserChatReadStatuses, {
userId,
batchSize: args.batchSize,
});
Fix with Cubic

Comment thread apps/web/src/lib/redis.ts
if (!value) return null;
if (typeof value === "string") {
try {
return JSON.parse(value) as StreamMeta;

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: parseStreamMeta: The string branch returns JSON.parse(value) as StreamMeta without validating the parsed result, but the object branch performs thorough field validation. This inconsistency means corrupted JSON in Redis could return an invalid StreamMeta. Consider reusing the object validation for the parsed JSON result.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/lib/redis.ts, line 62:

<comment>`parseStreamMeta`: The string branch returns `JSON.parse(value) as StreamMeta` without validating the parsed result, but the object branch performs thorough field validation. This inconsistency means corrupted JSON in Redis could return an invalid `StreamMeta`. Consider reusing the object validation for the parsed JSON result.</comment>

<file context>
@@ -80,14 +32,53 @@ export interface StreamMeta {
+	if (!value) return null;
+	if (typeof value === "string") {
+		try {
+			return JSON.parse(value) as StreamMeta;
+		} catch {
+			return null;
</file context>
Fix with Cubic

Comment thread apps/server/convex/backgroundStream.ts
Comment thread apps/web/src/stores/ui.ts
Comment on lines +34 to +37
sidebarOpen: true,
sidebarCollapsed: false,
commandPaletteOpen: false,
filterStyle: "model" as FilterStyle,

@cubic-dev-ai cubic-dev-ai Bot Feb 14, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Inconsistent indentation: the modified lines use 9-space indent while unchanged lines in the same object block use 8-space indent. This creates a visible misalignment between state properties and action methods within the same (set) => ({...}) callback.

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 modified lines use 9-space indent while unchanged lines in the same object block use 8-space indent. This creates a visible misalignment between state properties and action methods within the same `(set) => ({...})` callback.</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>
Suggested change
sidebarOpen: true,
sidebarCollapsed: false,
commandPaletteOpen: false,
filterStyle: "model" as FilterStyle,
sidebarOpen: true,
sidebarCollapsed: false,
commandPaletteOpen: false,
filterStyle: "model" as FilterStyle,
Fix with Cubic

@vercel

vercel Bot commented Feb 14, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
osschat-web Error Error Feb 14, 2026 2:55am

@leoisadev1
leoisadev1 merged commit cea322e into main Feb 14, 2026
1 check was pending
@leoisadev1
leoisadev1 deleted the tembo/fix/rate-limit-x-forwarded-for-bypass branch February 14, 2026 02:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tembo Pull request created by Tembo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant