Skip to content

Fix account takeover risk - #644

Merged
leoisadev1 merged 44 commits into
mainfrom
tembo/fix/email-link-account-takeover
Feb 14, 2026
Merged

Fix account takeover risk#644
leoisadev1 merged 44 commits into
mainfrom
tembo/fix/email-link-account-takeover

Conversation

@tembo

@tembo tembo Bot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixed account takeover vulnerability (OSS-37) by requiring email verification before linking accounts during migration.

Changes:

  • Added requireEmailVerification: true to emailAndPassword config in apps/server/convex/auth.ts
  • Implemented emailVerification with sendVerificationEmail handler (placeholder for email provider integration)
  • Added sendResetPassword handler for password reset flow
  • Modified migration logic in apps/server/convex/users.ts to check identity.emailVerified before linking accounts by email
  • Added security logging to warn about blocked unverified email linking attempts

Files modified:

  • apps/server/convex/auth.ts - enabled email verification requirement
  • apps/server/convex/users.ts - added email verification check to migration linking logic

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

Prevents account takeover during migration by requiring verified emails before linking accounts and adding email verification + reset password handlers (OSS-37). Also hardens the /api/models endpoint with IP-based rate limiting and safer upstream error handling.

  • Bug Fixes
    • Require email verification in email/password auth.
    • Send verification emails on sign-up (log only).
    • Add password reset handler (log only).
    • Link by email during migration only if identity.emailVerified; otherwise skip and warn.
    • /api/models: trust-proxy IP extraction, IP rate limit (30/60s), 10s timeout, return 502 on upstream failures.

Written for commit 56b6812. 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
@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 requested a review from leoisadev1 February 13, 2026 17:09
@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/server/convex/auth.ts
  • apps/server/convex/users.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.

15 issues found across 44 files

Confidence score: 2/5

  • Security risk: apps/server/convex/auth.ts logs password reset URLs with secret tokens, which could allow anyone with log access to reset accounts.
  • Workflow replay risk: external fetch in apps/web/src/routes/api/workflow/generate-title.ts runs outside context.run()/call, so Upstash replays can trigger duplicate OpenRouter requests and side effects.
  • Score is low because these are concrete, user-impacting and security-sensitive behaviors rather than minor polish items.
  • Pay close attention to apps/server/convex/auth.ts, apps/web/src/routes/api/workflow/generate-title.ts - secret leakage and replayed side effects.
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/hooks/use-persistent-chat.ts">

<violation number="1" location="apps/web/src/hooks/use-persistent-chat.ts:1126">
P2: Inconsistent error handling: `forkMessage` exposes raw `parsedError.message` to users instead of using `getUserFriendlyError()` like the updated `editMessage` and `retryMessage` handlers. This could leak internal error details to the UI.</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 space collapsing, which can leave multiple consecutive spaces in the output. For example, `"hello <b> world </b> foo"` would become `"hello  world  foo"` (double spaces). Move this line before the space-collapse step, or re-collapse spaces afterward.</violation>
</file>

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

<violation number="1" location="apps/server/convex/auth.ts:97">
P1: Security: Password reset URL (containing a secret token) is logged to console/stdout. Anyone with access to server logs could use this URL to reset a user's password, which undermines the security fix this PR introduces. At minimum, avoid logging the full URL — or use a structured logger with appropriate log levels so this is suppressed in production.</violation>
</file>

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

<violation number="1" location="apps/web/src/lib/redis.ts:184">
P2: Using `Date.now()` as a fallback for unparseable timestamps silently produces plausible-but-incorrect values, masking data issues. The previous code returned `0` which was an obvious sentinel. Consider using `0` or `NaN` as fallback, and simplifying the IIFE into a helper.</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 indentation while the unchanged lines in the same object literal use 8 spaces. This creates mixed indentation within the same block (e.g., `sidebarOpen` at 9 spaces vs `toggleSidebar` at 8 spaces).</violation>
</file>

<file name="apps/web/src/lib/workflow-auth-token.ts">

<violation number="1" location="apps/web/src/lib/workflow-auth-token.ts:15">
P2: Inconsistent error handling: `encryptSecret` can throw (e.g., if `OPENROUTER_ENCRYPTION_KEY` is missing or invalid), but unlike `getWorkflowAuthToken` which wraps `decryptSecret` in try-catch and returns `null` on failure, `storeWorkflowAuthToken` will throw an unhandled exception. The `Promise<string | null>` return type suggests callers expect graceful `null` returns on failure, not exceptions.</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 against production use but never actually checked in the fallback path. The cookie fallback is unconditionally active whenever `IS_LOCAL_DEV` is true, making `ALLOW_AUTH_COOKIE_FALLBACK` dead code. The fallback should be gated on both `IS_LOCAL_DEV` and `ALLOW_AUTH_COOKIE_FALLBACK` to honor the opt-in intent.</violation>
</file>

<file name="scripts/dev.ts">

<violation number="1" location="scripts/dev.ts:42">
P2: `--disable-warning=localstorage-file` won't suppress any warnings. The `--disable-warning` flag expects a warning code (e.g., `DEP0025`) or type (e.g., `ExperimentalWarning`), not a CLI flag name. If the intent is to suppress the experimental localStorage warning, use `ExperimentalWarning` as the value instead.</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: matching on `message.toLowerCase().includes("too many title generations")` is brittle compared to the previous `error.name === "RateLimitError"` check. If the server-side `throwRateLimitError` message format ever changes, this will silently degrade to the generic error toast. Consider checking a structured property (e.g., error name or a custom error code) rather than substring-matching on the message body.</violation>
</file>

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

<violation number="1" location="apps/server/convex/chats.ts:571">
P3: Inconsistent indentation: `body` property in the `fetch()` options object is indented one level less than its sibling properties `method` and `headers`. This makes the code harder to read and could mislead developers into thinking `body` is outside the options object.</violation>
</file>

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

<violation number="1" location="apps/server/convex/cleanupAction.ts:38">
P2: Validation mismatch: `Number.isFinite` here allows floats, but the downstream `cleanupSoftDeletedRecords` mutation uses `Number.isInteger` and will reject them. Use `Number.isInteger` to match the inner mutation's validation and fail early with a clear error.</violation>

<violation number="2" location="apps/server/convex/cleanupAction.ts:42">
P2: Same validation mismatch as `retentionDays`: `Number.isFinite` allows floats but the downstream mutation requires integers via `Number.isInteger`.</violation>
</file>

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

<violation number="1" location="apps/server/convex/users.ts:876">
P2: `batchSize` is not forwarded to `deleteUserChatReadStatuses` and `deleteUserPromptTemplates`, unlike all other cases in this switch. The action accepts `batchSize` in its args but silently ignores it for these two steps, making the batch size uncontrollable by the caller.</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 a negative `usageCentsDelta` rounds toward zero, under-counting refunds/reductions. For example, `Math.ceil(-0.3)` becomes `0` and silently drops the adjustment. Consider using `Math.sign(x) * Math.ceil(Math.abs(x))` to consistently round away from zero (or `Math.round` if rounding to nearest is acceptable).</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:248">
P1: The `fetch` call to OpenRouter is outside any `context.run()` or `context.call()` step. Upstash Workflow replays the endpoint on each step execution, so code between steps re-executes on every replay. This means the OpenRouter API call will fire repeatedly on retries/replays, wasting API credits and potentially producing inconsistent results.

Use `context.call()` (preferred for third-party HTTP calls — it offloads the request to Upstash, supports longer timeouts, and doesn't consume compute while waiting) or at minimum wrap it in `context.run()`.</violation>
</file>

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

requireEmailVerification: true,
sendResetPassword: async ({ user, url }: { user: { email: string }; url: string }) => {
// TODO: integrate with email provider (e.g., Resend, SendGrid)
console.log(`[Auth] Password reset requested for ${user.email}: ${url}`);

@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: Security: Password reset URL (containing a secret token) is logged to console/stdout. Anyone with access to server logs could use this URL to reset a user's password, which undermines the security fix this PR introduces. At minimum, avoid logging the full URL — or use a structured logger with appropriate log levels so this is suppressed in production.

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

<comment>Security: Password reset URL (containing a secret token) is logged to console/stdout. Anyone with access to server logs could use this URL to reset a user's password, which undermines the security fix this PR introduces. At minimum, avoid logging the full URL — or use a structured logger with appropriate log levels so this is suppressed in production.</comment>

<file context>
@@ -85,11 +85,25 @@ export const createAuth = (
+			requireEmailVerification: true,
+			sendResetPassword: async ({ user, url }: { user: { email: string }; url: string }) => {
+				// TODO: integrate with email provider (e.g., Resend, SendGrid)
+				console.log(`[Auth] Password reset requested for ${user.email}: ${url}`);
+			},
+		},
</file context>
Fix with Cubic


let llmResponseStatus = 0;
let llmResponseBody: { choices?: Array<{ message?: { content?: string } }> } | null = null;
try {

@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 fetch call to OpenRouter is outside any context.run() or context.call() step. Upstash Workflow replays the endpoint on each step execution, so code between steps re-executes on every replay. This means the OpenRouter API call will fire repeatedly on retries/replays, wasting API credits and potentially producing inconsistent results.

Use context.call() (preferred for third-party HTTP calls — it offloads the request to Upstash, supports longer timeouts, and doesn't consume compute while waiting) or at minimum wrap it in context.run().

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

<comment>The `fetch` call to OpenRouter is outside any `context.run()` or `context.call()` step. Upstash Workflow replays the endpoint on each step execution, so code between steps re-executes on every replay. This means the OpenRouter API call will fire repeatedly on retries/replays, wasting API credits and potentially producing inconsistent results.

Use `context.call()` (preferred for third-party HTTP calls — it offloads the request to Upstash, supports longer timeouts, and doesn't consume compute while waiting) or at minimum wrap it in `context.run()`.</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",
</file context>
Fix with Cubic

} catch (err) {
const parsedError = err instanceof Error ? err : new Error("Unknown error");
toast.error("Failed to branch off", {
description: parsedError.message,

@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: Inconsistent error handling: forkMessage exposes raw parsedError.message to users instead of using getUserFriendlyError() like the updated editMessage and retryMessage handlers. This could leak internal error details to the UI.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/hooks/use-persistent-chat.ts, line 1126:

<comment>Inconsistent error handling: `forkMessage` exposes raw `parsedError.message` to users instead of using `getUserFriendlyError()` like the updated `editMessage` and `retryMessage` handlers. This could leak internal error details to the UI.</comment>

<file context>
@@ -1146,47 +1103,43 @@ export function usePersistentChat({
+		} catch (err) {
+			const parsedError = err instanceof Error ? err : new Error("Unknown error");
+			toast.error("Failed to branch off", {
+				description: parsedError.message,
+			});
+			return undefined;
</file context>
Fix with Cubic

Comment thread apps/server/convex/lib/sanitize.ts
Comment thread apps/web/src/lib/redis.ts
Comment on lines +184 to 193
timestamp:
typeof message.ts === "string"
? (() => {
const parsed = Number.parseInt(message.ts, 10);
return Number.isFinite(parsed) ? parsed : Date.now();
})()
: typeof message.ts === "number" && Number.isFinite(message.ts)
? message.ts
: Date.now(),
}));

@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: Using Date.now() as a fallback for unparseable timestamps silently produces plausible-but-incorrect values, masking data issues. The previous code returned 0 which was an obvious sentinel. Consider using 0 or NaN as fallback, and simplifying the IIFE into a helper.

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

<comment>Using `Date.now()` as a fallback for unparseable timestamps silently produces plausible-but-incorrect values, masking data issues. The previous code returned `0` which was an obvious sentinel. Consider using `0` or `NaN` as fallback, and simplifying the IIFE into a helper.</comment>

<file context>
@@ -172,25 +163,40 @@ export async function readStream(
+			message.type === "error"
+				? message.type
+				: "text",
+		timestamp:
+			typeof message.ts === "string"
+				? (() => {
</file context>
Suggested change
timestamp:
typeof message.ts === "string"
? (() => {
const parsed = Number.parseInt(message.ts, 10);
return Number.isFinite(parsed) ? parsed : Date.now();
})()
: typeof message.ts === "number" && Number.isFinite(message.ts)
? message.ts
: Date.now(),
}));
timestamp: (() => {
const raw = message.ts;
const parsed = typeof raw === "string" ? Number.parseInt(raw, 10) : typeof raw === "number" ? raw : NaN;
return Number.isFinite(parsed) ? parsed : 0;
})(),
Fix with Cubic


// Bounds validation — prevent accidental full-purge or oversized batches
const retentionDays = args.retentionDays ?? 90;
if (!Number.isFinite(retentionDays) || retentionDays < 1 || retentionDays > 3650) {

@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: Validation mismatch: Number.isFinite here allows floats, but the downstream cleanupSoftDeletedRecords mutation uses Number.isInteger and will reject them. Use Number.isInteger to match the inner mutation's validation and fail early with a clear error.

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` here allows floats, but the downstream `cleanupSoftDeletedRecords` mutation uses `Number.isInteger` and will reject them. Use `Number.isInteger` to match the inner mutation's validation and fail early with a clear error.</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>
Fix with Cubic

Comment on lines +876 to +883
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,
});

@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 cases in this switch. The action accepts batchSize in its args but silently ignores it for these two steps, making the batch size uncontrollable by the caller.

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

<comment>`batchSize` is not forwarded to `deleteUserChatReadStatuses` and `deleteUserPromptTemplates`, unlike all other cases in this switch. The action accepts `batchSize` in its args but silently ignores it for these two steps, making the batch size uncontrollable by the caller.</comment>

<file context>
@@ -598,18 +602,309 @@ export const updateName = mutation({
+					userId,
+					batchSize: args.batchSize,
+				});
+			case "delete-chat-read-statuses":
+				return await ctx.runMutation(internal.users.deleteUserChatReadStatuses, {
+					userId,
</file context>
Suggested change
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,
});
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 a negative usageCentsDelta rounds toward zero, under-counting refunds/reductions. For example, Math.ceil(-0.3) becomes 0 and silently drops the adjustment. Consider using Math.sign(x) * Math.ceil(Math.abs(x)) to consistently round away from zero (or Math.round if rounding to nearest is acceptable).

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 a negative `usageCentsDelta` rounds toward zero, under-counting refunds/reductions. For example, `Math.ceil(-0.3)` becomes `0` and silently drops the adjustment. Consider using `Math.sign(x) * Math.ceil(Math.abs(x))` to consistently round away from zero (or `Math.round` if rounding to nearest is acceptable).</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 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 indentation while the unchanged lines in the same object literal use 8 spaces. This creates mixed indentation within the same block (e.g., sidebarOpen at 9 spaces vs toggleSidebar at 8 spaces).

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 indentation while the unchanged lines in the same object literal use 8 spaces. This creates mixed indentation within the same block (e.g., `sidebarOpen` at 9 spaces vs `toggleSidebar` at 8 spaces).</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

Comment thread apps/server/convex/chats.ts
@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:56am

@leoisadev1
leoisadev1 merged commit c1e6f97 into main Feb 14, 2026
4 of 5 checks passed
@leoisadev1
leoisadev1 deleted the tembo/fix/email-link-account-takeover branch February 14, 2026 02:42
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