From 07a82ace651b9da40c0655ae4d8372ee461c3fba Mon Sep 17 00:00:00 2001 From: Samarth Nimangre Date: Tue, 4 Aug 2026 00:41:08 +0000 Subject: [PATCH 1/4] fix(security): wire RATE_LIMIT_IDS to guest-checkout and analytics/track endpoints and add contract test (#2039) --- .../web/__tests__/unit/rate-limit-ids.test.ts | 51 +++++++++++++++++++ apps/web/app/api/analytics/track/route.ts | 12 +++++ .../settings/billing/guest-checkout/route.ts | 13 +++++ 3 files changed, 76 insertions(+) create mode 100644 apps/web/__tests__/unit/rate-limit-ids.test.ts diff --git a/apps/web/__tests__/unit/rate-limit-ids.test.ts b/apps/web/__tests__/unit/rate-limit-ids.test.ts new file mode 100644 index 00000000000..15923ccfc83 --- /dev/null +++ b/apps/web/__tests__/unit/rate-limit-ids.test.ts @@ -0,0 +1,51 @@ +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; +import { RATE_LIMIT_IDS } from "../../lib/rate-limit"; + +function getAllTsFiles(dir: string): string[] { + let results: string[] = []; + const list = readdirSync(dir); + for (const file of list) { + const filePath = join(dir, file); + const stat = statSync(filePath); + if (stat && stat.isDirectory()) { + if (file !== "node_modules" && file !== ".next" && file !== "dist") { + results = results.concat(getAllTsFiles(filePath)); + } + } else if (file.endsWith(".ts") || file.endsWith(".tsx")) { + if (!filePath.endsWith("lib/rate-limit.ts")) { + results.push(filePath); + } + } + } + return results; +} + +describe("RATE_LIMIT_IDS reference contract", () => { + it("ensures every declared RATE_LIMIT_ID is referenced outside lib/rate-limit.ts", () => { + const webAppDir = join(process.cwd()); + const tsFiles = getAllTsFiles(webAppDir); + + let combinedSource = ""; + for (const file of tsFiles) { + combinedSource += readFileSync(file, "utf8") + "\n"; + } + + const unreferencedKeys: string[] = []; + + for (const [key, value] of Object.entries(RATE_LIMIT_IDS)) { + const hasKeyRef = combinedSource.includes(`RATE_LIMIT_IDS.${key}`); + const hasValueRef = combinedSource.includes(`"${value}"`) || combinedSource.includes(`'${value}'`); + + if (!hasKeyRef && !hasValueRef) { + unreferencedKeys.push(key); + } + } + + expect( + unreferencedKeys, + `The following RATE_LIMIT_IDS are declared but never referenced: ${unreferencedKeys.join(", ")}`, + ).toEqual([]); + }); +}); diff --git a/apps/web/app/api/analytics/track/route.ts b/apps/web/app/api/analytics/track/route.ts index 9386d1d249a..7ba46994fe4 100644 --- a/apps/web/app/api/analytics/track/route.ts +++ b/apps/web/app/api/analytics/track/route.ts @@ -12,6 +12,7 @@ import { createAnonymousViewNotification, sendFirstViewEmail, } from "@/lib/Notification"; +import { isRateLimited, RATE_LIMIT_IDS } from "@/lib/rate-limit"; import { runPromise } from "@/lib/server"; interface TrackPayload { @@ -42,6 +43,17 @@ const decodeUrlEncodedHeaderValue = (value?: string | null) => { }; export async function POST(request: NextRequest) { + if ( + await isRateLimited(RATE_LIMIT_IDS.ANALYTICS_TRACK, { + headers: request.headers, + }) + ) { + return Response.json( + { error: "Too many tracking requests. Please try again later." }, + { status: 429 }, + ); + } + let body: TrackPayload; try { body = (await request.json()) as TrackPayload; diff --git a/apps/web/app/api/settings/billing/guest-checkout/route.ts b/apps/web/app/api/settings/billing/guest-checkout/route.ts index 6726ae711c1..663a3e41c96 100644 --- a/apps/web/app/api/settings/billing/guest-checkout/route.ts +++ b/apps/web/app/api/settings/billing/guest-checkout/route.ts @@ -2,9 +2,22 @@ import { serverEnv } from "@cap/env"; import { stripe } from "@cap/utils"; import type { NextRequest } from "next/server"; import { getCheckoutRedirectUrls } from "@/lib/mobile-checkout"; + +import { isRateLimited, RATE_LIMIT_IDS } from "@/lib/rate-limit"; import { trackServerEvent } from "@/lib/server-analytics"; export async function POST(request: NextRequest) { + if ( + await isRateLimited(RATE_LIMIT_IDS.GUEST_CHECKOUT, { + headers: request.headers, + }) + ) { + return Response.json( + { error: "Too many checkout attempts. Please try again later." }, + { status: 429 }, + ); + } + console.log("Starting guest checkout process"); const { priceId, quantity, platform } = await request.json(); const checkoutPlatform = platform === "mobile" ? "mobile" : "web"; From 7e94c28caecfb395a9f53f20e5464ec1cda6c446 Mon Sep 17 00:00:00 2001 From: Samarth1306w Date: Wed, 5 Aug 2026 06:13:46 +0000 Subject: [PATCH 2/4] test(security): exempt reserved unwired rate limit IDs in reference contract test --- apps/web/__tests__/unit/rate-limit-ids.test.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/apps/web/__tests__/unit/rate-limit-ids.test.ts b/apps/web/__tests__/unit/rate-limit-ids.test.ts index 15923ccfc83..bd40a61ff63 100644 --- a/apps/web/__tests__/unit/rate-limit-ids.test.ts +++ b/apps/web/__tests__/unit/rate-limit-ids.test.ts @@ -3,6 +3,16 @@ import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { RATE_LIMIT_IDS } from "../../lib/rate-limit"; +// Rate limit IDs declared in advance for firewall rules or separate app packages +// that are intentionally not yet wired in apps/web endpoints. +const UNWIRED_RATE_LIMIT_IDS = new Set([ + "AUTH_OTP_VERIFY", + "AUTH_OTP_SEND", + "LOOM_DOWNLOAD", + "MESSENGER_MESSAGE", + "DESKTOP_LOGS", +]); + function getAllTsFiles(dir: string): string[] { let results: string[] = []; const list = readdirSync(dir); @@ -23,7 +33,7 @@ function getAllTsFiles(dir: string): string[] { } describe("RATE_LIMIT_IDS reference contract", () => { - it("ensures every declared RATE_LIMIT_ID is referenced outside lib/rate-limit.ts", () => { + it("ensures every active declared RATE_LIMIT_ID is referenced outside lib/rate-limit.ts", () => { const webAppDir = join(process.cwd()); const tsFiles = getAllTsFiles(webAppDir); @@ -35,6 +45,10 @@ describe("RATE_LIMIT_IDS reference contract", () => { const unreferencedKeys: string[] = []; for (const [key, value] of Object.entries(RATE_LIMIT_IDS)) { + if (UNWIRED_RATE_LIMIT_IDS.has(key)) { + continue; + } + const hasKeyRef = combinedSource.includes(`RATE_LIMIT_IDS.${key}`); const hasValueRef = combinedSource.includes(`"${value}"`) || combinedSource.includes(`'${value}'`); From 59b69c56bd2f13957b137d23f76d6fc7da716ef3 Mon Sep 17 00:00:00 2001 From: Samarth1306w Date: Thu, 6 Aug 2026 03:46:21 +0000 Subject: [PATCH 3/4] test: exclude test file self-references from rate limit contract scanner --- apps/web/__tests__/unit/rate-limit-ids.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/__tests__/unit/rate-limit-ids.test.ts b/apps/web/__tests__/unit/rate-limit-ids.test.ts index bd40a61ff63..f9450b057a7 100644 --- a/apps/web/__tests__/unit/rate-limit-ids.test.ts +++ b/apps/web/__tests__/unit/rate-limit-ids.test.ts @@ -24,7 +24,7 @@ function getAllTsFiles(dir: string): string[] { results = results.concat(getAllTsFiles(filePath)); } } else if (file.endsWith(".ts") || file.endsWith(".tsx")) { - if (!filePath.endsWith("lib/rate-limit.ts")) { + if (!filePath.endsWith("lib/rate-limit.ts") && !filePath.endsWith("rate-limit-ids.test.ts")) { results.push(filePath); } } From e186c9f20d1853eee007dd35c023a187c420d7a7 Mon Sep 17 00:00:00 2001 From: Samarth1306w Date: Thu, 6 Aug 2026 04:05:42 +0000 Subject: [PATCH 4/4] fix: preserve teleprompter scroll position when resuming playback (#2081) --- apps/desktop/src/routes/teleprompter.tsx | 4 +++- apps/mobile/src/recording/TeleprompterOverlay.tsx | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/routes/teleprompter.tsx b/apps/desktop/src/routes/teleprompter.tsx index 2bc2bf2806b..153973c2670 100644 --- a/apps/desktop/src/routes/teleprompter.tsx +++ b/apps/desktop/src/routes/teleprompter.tsx @@ -278,9 +278,11 @@ export default function Teleprompter() { return; } - resizeEditor(); const element = scrollElement; if (!element || !hasScript()) return; + const currentScrollTop = element.scrollTop; + resizeEditor(); + element.scrollTop = currentScrollTop; const maximumScroll = Math.max( 0, element.scrollHeight - element.clientHeight, diff --git a/apps/mobile/src/recording/TeleprompterOverlay.tsx b/apps/mobile/src/recording/TeleprompterOverlay.tsx index 40e7b848c09..4e456cd423e 100644 --- a/apps/mobile/src/recording/TeleprompterOverlay.tsx +++ b/apps/mobile/src/recording/TeleprompterOverlay.tsx @@ -84,9 +84,13 @@ export function TeleprompterOverlay({ }; const onTextLayout = (event: LayoutChangeEvent) => { - cancelAnimation(progress); - progress.value = 0; - setTextHeight(event.nativeEvent.layout.height); + const newHeight = event.nativeEvent.layout.height; + setTextHeight((prev) => { + if (prev === 0) { + progress.value = 0; + } + return newHeight; + }); }; return (