Fix insecure draft storage - #640
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.
…Storage for improved security
|
Requesting review from @leoisadev1 who has experience with the following files modified in this PR:
|
There was a problem hiding this comment.
14 issues found across 45 files
Confidence score: 2/5
- High risk due to workflow misuse:
apps/web/src/routes/api/workflow/generate-title.tsmakes the OpenRouterfetch()outsidecontext.run(), which can re-run on each step and cause repeated external calls and inconsistent behavior. - Security/data handling concerns: draft migration leaves sensitive data in
localStorage(apps/web/src/stores/prompt-draft.ts) and decrypted auth tokens are persisted as workflow step results inapps/web/src/routes/api/workflow/delete-account.ts. - Concurrency/data integrity risk in
apps/server/convex/lib/upstashUsage.tswhere the negative-balance floor check is split across pipelines, allowing a race between read and write. - Pay close attention to
apps/web/src/routes/api/workflow/generate-title.ts,apps/web/src/stores/prompt-draft.ts,apps/web/src/routes/api/workflow/delete-account.ts,apps/server/convex/lib/upstashUsage.ts- replays/retries, sensitive data persistence, and race conditions.
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` still exposes raw `parsedError.message` to the user via toast, while `editMessage` and `retryMessage` in this same PR were updated to use `getUserFriendlyError()`. This can leak internal error details (API structures, DB field names, etc.) to the end user.</violation>
</file>
<file name="apps/web/src/stores/prompt-draft.ts">
<violation number="1" location="apps/web/src/stores/prompt-draft.ts:123">
P1: Switching to `sessionStorage` doesn't remove previously stored drafts from `localStorage`. Existing sensitive draft data under the `"openchat-prompt-drafts"` key will persist indefinitely in `localStorage`, undermining the security improvement this PR intends to make. Add a one-time migration cleanup to remove the old `localStorage` entry.</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:249">
P1: The `fetch()` call to the OpenRouter API is not wrapped in a workflow step (`context.run()` or `context.call()`). In Upstash Workflow, the endpoint is re-invoked for each step, and only operations inside `context.run()`/`context.call()` are memoized and skipped on replay. This bare fetch will be re-executed every time the workflow replays for subsequent steps (e.g., `save-title`), causing duplicate LLM calls and wasted API credits.
Use `context.call()` which is specifically designed for external HTTP requests in workflows — it offloads the call to Upstash and automatically handles retries and replay.</violation>
</file>
<file name="apps/server/convex/chats.ts">
<violation number="1" location="apps/server/convex/chats.ts:571">
P3: Indentation of `body` (2 tabs) doesn't match its sibling properties `method` and `headers` (3 tabs) in the fetch options object. This makes it look like `body` is outside the options object. Align it with the other properties.</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 and inconsistent rate-limit detection: this uses string matching on the error message, while the same file (line 520, bulk-delete handler) and the server (`throwRateLimitError`) use the structured `error.name === "RateLimitError"` check. The message-based check will silently break if the server wording changes, and the hardcoded toast loses the dynamic retry-after time the server provides.</violation>
</file>
<file name="apps/server/convex/lib/upstashUsage.ts">
<violation number="1" location="apps/server/convex/lib/upstashUsage.ts:189">
P1: Race condition (TOCTOU): The negative-balance floor check reads the total from the first pipeline, then issues a separate `SET key 0` in a second pipeline. A concurrent `INCRBY` between the two pipelines would be silently wiped out, causing usage data loss. Consider using a Lua script via `EVAL` to atomically check-and-floor:
["EVAL", "local v=redis.call('INCRBY',KEYS[1],ARGV[1]) if v<0 then redis.call('SET',KEYS[1],0) end return v", 1, key, roundedDelta]
</file>
<file name="apps/web/src/lib/server-auth.ts">
<violation number="1" location="apps/web/src/lib/server-auth.ts:82">
P1: `ALLOW_AUTH_COOKIE_FALLBACK` is defined and guarded against production use, but never actually checked in the fallback code path. The cookie-based JWT fallback is unconditionally available in dev/test, defeating the purpose of the opt-in env var. The fallback guard should also require `ALLOW_AUTH_COOKIE_FALLBACK` to be `true`.</violation>
</file>
<file name="apps/server/convex/lib/sanitize.ts">
<violation number="1" location="apps/server/convex/lib/sanitize.ts:44">
P2: Regex-based HTML stripping `/<[^>]*>/g` will corrupt legitimate titles containing angle brackets (e.g., `"Understanding template<T>"` → `"Understanding template"`). For an AI chat app with code-related conversations, this is a realistic data loss scenario. Consider encoding angle brackets instead of removing them (e.g., replacing `<` with `<`) or using a proper HTML sanitization library that distinguishes tags from text.</violation>
</file>
<file name="scripts/dev.ts">
<violation number="1" location="scripts/dev.ts:25">
P2: The local variable `process` shadows the global `process` object. Rename it to avoid confusion and potential bugs if this function is later extended (e.g., someone adding `process.env` access inside `run` would silently get the subprocess object, not the global).</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 mutation rejects with `Number.isInteger`. Use `Number.isInteger` here to match the mutation's validation and catch invalid input at the correct boundary.</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:204">
P2: The decrypted auth token is returned from `context.run("resolve-auth", ...)`, which causes Upstash Workflow to persist it in plain text as a step result for replay. This partially undermines the encrypt/`getdel`/TTL protections in `workflow-auth-token.ts`. Consider restructuring so the token isn't stored as a step result — e.g., re-resolve the token inside each `context.run` step that needs it, or pass it via workflow request headers instead of step return values.</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:120">
P2: `runExportChatInline` should validate that `userId` is defined before proceeding. The function currently trusts its caller to always provide a `userId`, but its type signature allows `undefined`. Add a guard at the top of the function.</violation>
</file>
<file name="apps/server/convex/users.ts">
<violation number="1" location="apps/server/convex/users.ts:870">
P2: `batchSize` is not forwarded for `delete-chat-read-statuses` and `delete-prompt-templates` cases, unlike all other deletion steps. This prevents callers from controlling the batch size for these two steps. Pass `batchSize: args.batchSize` for consistency.</violation>
</file>
<file name="apps/server/convex/chats.test.ts">
<violation number="1" location="apps/server/convex/chats.test.ts:1287">
P2: This test doesn't mock `fetch`, relying on the implementation short-circuiting before the API call. If the implementation order changes, this test will make a real HTTP request or fail unexpectedly. Mock `fetch` and assert it was **not** called — this both prevents accidental network calls and documents the expected behavior.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| { | ||
| name: "openchat-prompt-drafts", | ||
| storage: createJSONStorage(() => localStorage), | ||
| storage: createJSONStorage(() => sessionStorage), |
There was a problem hiding this comment.
P1: Switching to sessionStorage doesn't remove previously stored drafts from localStorage. Existing sensitive draft data under the "openchat-prompt-drafts" key will persist indefinitely in localStorage, undermining the security improvement this PR intends to make. Add a one-time migration cleanup to remove the old localStorage entry.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/src/stores/prompt-draft.ts, line 123:
<comment>Switching to `sessionStorage` doesn't remove previously stored drafts from `localStorage`. Existing sensitive draft data under the `"openchat-prompt-drafts"` key will persist indefinitely in `localStorage`, undermining the security improvement this PR intends to make. Add a one-time migration cleanup to remove the old `localStorage` entry.</comment>
<file context>
@@ -115,7 +120,7 @@ export const usePromptDraftStore = create<PromptDraftState>()(
{
name: "openchat-prompt-drafts",
- storage: createJSONStorage(() => localStorage),
+ storage: createJSONStorage(() => sessionStorage),
},
),
</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 fetch() call to the OpenRouter API is not wrapped in a workflow step (context.run() or context.call()). In Upstash Workflow, the endpoint is re-invoked for each step, and only operations inside context.run()/context.call() are memoized and skipped on replay. This bare fetch will be re-executed every time the workflow replays for subsequent steps (e.g., save-title), causing duplicate LLM calls and wasted API credits.
Use context.call() which is specifically designed for external HTTP requests in workflows — it offloads the call to Upstash and automatically handles retries and replay.
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 `fetch()` call to the OpenRouter API is not wrapped in a workflow step (`context.run()` or `context.call()`). In Upstash Workflow, the endpoint is re-invoked for each step, and only operations inside `context.run()`/`context.call()` are memoized and skipped on replay. This bare fetch will be re-executed every time the workflow replays for subsequent steps (e.g., `save-title`), causing duplicate LLM calls and wasted API credits.
Use `context.call()` which is specifically designed for external HTTP requests in workflows — it offloads the call to Upstash and automatically handles retries and replay.</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>
| : typeof total === "string" | ||
| ? Number.parseInt(total, 10) | ||
| : null; | ||
| if (typeof parsedTotal === "number" && Number.isFinite(parsedTotal) && parsedTotal < 0) { |
There was a problem hiding this comment.
P1: Race condition (TOCTOU): The negative-balance floor check reads the total from the first pipeline, then issues a separate SET key 0 in a second pipeline. A concurrent INCRBY between the two pipelines would be silently wiped out, causing usage data loss. Consider using a Lua script via EVAL to atomically check-and-floor:
["EVAL", "local v=redis.call('INCRBY',KEYS[1],ARGV[1]) if v<0 then redis.call('SET',KEYS[1],0) end return v", 1, key, roundedDelta]
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 189:
<comment>Race condition (TOCTOU): The negative-balance floor check reads the total from the first pipeline, then issues a separate `SET key 0` in a second pipeline. A concurrent `INCRBY` between the two pipelines would be silently wiped out, causing usage data loss. Consider using a Lua script via `EVAL` to atomically check-and-floor:
["EVAL", "local v=redis.call('INCRBY',KEYS[1],ARGV[1]) if v<0 then redis.call('SET',KEYS[1],0) end return v", 1, key, roundedDelta]
<file context>
@@ -0,0 +1,200 @@
+ : typeof total === "string"
+ ? Number.parseInt(total, 10)
+ : null;
+ if (typeof parsedTotal === "number" && Number.isFinite(parsedTotal) && parsedTotal < 0) {
+ await executePipeline([
+ ["SET", key, 0],
</file context>
| if (response.status >= 400 && response.status < 500) { | ||
| return null; | ||
| } | ||
| if (!IS_LOCAL_DEV) return null; |
There was a problem hiding this comment.
P1: ALLOW_AUTH_COOKIE_FALLBACK is defined and guarded against production use, but never actually checked in the fallback code path. The cookie-based JWT fallback is unconditionally available in dev/test, defeating the purpose of the opt-in env var. The fallback guard 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 against production use, but never actually checked in the fallback code path. The cookie-based JWT fallback is unconditionally available in dev/test, defeating the purpose of the opt-in env var. The fallback guard 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>
| } catch (err) { | ||
| const parsedError = err instanceof Error ? err : new Error("Unknown error"); | ||
| toast.error("Failed to branch off", { | ||
| description: parsedError.message, |
There was a problem hiding this comment.
P2: Inconsistent error handling: forkMessage still exposes raw parsedError.message to the user via toast, while editMessage and retryMessage in this same PR were updated to use getUserFriendlyError(). This can leak internal error details (API structures, DB field names, etc.) to the end user.
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` still exposes raw `parsedError.message` to the user via toast, while `editMessage` and `retryMessage` in this same PR were updated to use `getUserFriendlyError()`. This can leak internal error details (API structures, DB field names, etc.) to the end user.</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>
| return EMPTY_DELETE_RESULT; | ||
| } | ||
|
|
||
| const authToken = await context.run("resolve-auth", async () => { |
There was a problem hiding this comment.
P2: The decrypted auth token is returned from context.run("resolve-auth", ...), which causes Upstash Workflow to persist it in plain text as a step result for replay. This partially undermines the encrypt/getdel/TTL protections in workflow-auth-token.ts. Consider restructuring so the token isn't stored as a step result — e.g., re-resolve the token inside each context.run step that needs it, or pass it via workflow request headers instead of step return values.
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/delete-account.ts, line 204:
<comment>The decrypted auth token is returned from `context.run("resolve-auth", ...)`, which causes Upstash Workflow to persist it in plain text as a step result for replay. This partially undermines the encrypt/`getdel`/TTL protections in `workflow-auth-token.ts`. Consider restructuring so the token isn't stored as a step result — e.g., re-resolve the token inside each `context.run` step that needs it, or pass it via workflow request headers instead of step return values.</comment>
<file context>
@@ -0,0 +1,387 @@
+ return EMPTY_DELETE_RESULT;
+ }
+
+ const authToken = await context.run("resolve-auth", async () => {
+ return getWorkflowAuthToken(authTokenRef);
+ });
</file context>
| payload: ExportChatPayload, | ||
| authToken: string, | ||
| ): Promise<{ downloadUrl: string; byteLength: number; fileName: string }> { | ||
| const { chatId, userId, format = "markdown" } = payload; |
There was a problem hiding this comment.
P2: runExportChatInline should validate that userId is defined before proceeding. The function currently trusts its caller to always provide a userId, but its type signature allows undefined. Add a guard at the top of the function.
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 120:
<comment>`runExportChatInline` should validate that `userId` is defined before proceeding. The function currently trusts its caller to always provide a `userId`, but its type signature allows `undefined`. Add a guard at the top of the function.</comment>
<file context>
@@ -0,0 +1,226 @@
+ payload: ExportChatPayload,
+ authToken: string,
+): Promise<{ downloadUrl: string; byteLength: number; fileName: string }> {
+ const { chatId, userId, format = "markdown" } = payload;
+ const convexClient = createConvexServerClient(authToken);
+ const chatExportData = await convexClient.query(api.chats.getChatExportData, {
</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 is not forwarded for delete-chat-read-statuses and delete-prompt-templates cases, unlike all other deletion steps. This prevents callers from controlling the batch size for these two steps. Pass 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 870:
<comment>`batchSize` is not forwarded for `delete-chat-read-statuses` and `delete-prompt-templates` cases, unlike all other deletion steps. This prevents callers from controlling the batch size for these two steps. Pass `batchSize: args.batchSize` for consistency.</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, | |
| }); |
| 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: This test doesn't mock fetch, relying on the implementation short-circuiting before the API call. If the implementation order changes, this test will make a real HTTP request or fail unexpectedly. Mock fetch and assert it was not called — this both prevents accidental network calls and documents the expected behavior.
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>This test doesn't mock `fetch`, relying on the implementation short-circuiting before the API call. If the implementation order changes, this test will make a real HTTP request or fail unexpectedly. Mock `fetch` and assert it was **not** called — this both prevents accidental network calls and documents the expected behavior.</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>
| "HTTP-Referer": process.env.CONVEX_SITE_URL || "https://osschat.io", | ||
| "X-Title": "OSSChat", | ||
| }, | ||
| body: JSON.stringify({ |
There was a problem hiding this comment.
P3: Indentation of body (2 tabs) doesn't match its sibling properties method and headers (3 tabs) in the fetch options object. This makes it look like body is outside the options object. Align it with the other properties.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/convex/chats.ts, line 571:
<comment>Indentation of `body` (2 tabs) doesn't match its sibling properties `method` and `headers` (3 tabs) in the fetch options object. This makes it look like `body` is outside the options object. Align it with the other properties.</comment>
<file context>
@@ -526,6 +526,87 @@ const TITLE_STYLE_PROMPTS: Record<"short" | "standard" | "long", string> = {
+ "HTTP-Referer": process.env.CONVEX_SITE_URL || "https://osschat.io",
+ "X-Title": "OSSChat",
+ },
+ body: JSON.stringify({
+ model: TITLE_MODEL_ID,
+ messages: [
</file context>
| body: JSON.stringify({ | |
| body: JSON.stringify({ |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Summary
Migrated prompt draft storage from
localStoragetosessionStorageto mitigate security risks. Drafts containing sensitive user input are now automatically cleared when the browser tab closes, reducing exposure via XSS or compromised browser profiles.Changes:
localStoragewithsessionStorageinapps/web/src/stores/prompt-draft.tsapps/web/src/hooks/use-prompt-draft.tsandapps/web/src/stores/prompt-draft.tsto reflect security rationaleFiles modified:
apps/web/src/hooks/use-prompt-draft.tsapps/web/src/stores/prompt-draft.tsFixes OSS-34
Summary by cubic
Switched prompt draft storage to sessionStorage to limit sensitive data exposure. Drafts clear on tab close, expire after 24h, and the models API enforces IP rate limits with a 10s timeout (OSS-34).
Written for commit 394083f. Summary will update on new commits.