Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions apps/web/src/lib/auth-client.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {

createContext,
useCallback,
useContext,
Expand All @@ -16,7 +16,6 @@ import { env } from "./env";
import { analytics } from "./analytics";
import type {ReactNode} from "react";


/**
* Better Auth client with Convex integration.
*/
Expand Down
16 changes: 1 addition & 15 deletions apps/web/src/stores/prompt-draft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,15 @@ import { createJSONStorage, devtools, persist } from "zustand/middleware";
/**
* Store for persisting prompt drafts within the current browser session.
*
<<<<<<< HEAD
* Uses sessionStorage instead of localStorage to limit exposure of sensitive
* chat content — data is scoped to the tab/session and not accessible after
* the browser session ends.
||||||| 54e09ce
* Store for persisting prompt drafts across page reloads.
=======
* Security: Uses sessionStorage instead of localStorage to limit exposure
* of sensitive draft content. Drafts are automatically cleared when the
* browser tab is closed, reducing the risk of exfiltration via XSS or
* compromised browser profiles.
>>>>>>> main
*
* Non-annoying approach:
* - Drafts are saved per-chat (or "global" for new chat input)
* - Drafts are automatically cleared when a message is sent
<<<<<<< HEAD
* - Old drafts are cleaned up after 7 days to prevent storage bloat
||||||| 54e09ce
* - Old drafts are cleaned up after 7 days to prevent localStorage bloat
=======
* - Old drafts are cleaned up after 24 hours as a defensive measure
>>>>>>> main
*/

const DRAFT_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours (session-scoped, defensive expiry)
Expand Down Expand Up @@ -91,7 +77,7 @@ export const usePromptDraftStore = create<PromptDraftState>()(
return "";
}

// Don't return expired drafts
// Only return drafts that have not yet expired
if (Date.now() - draft.updatedAt < DRAFT_EXPIRY_MS) {
return draft.text;
}
Expand Down
11 changes: 8 additions & 3 deletions apps/web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@ import tailwindcss from '@tailwindcss/vite'

const config = defineConfig({
server: {
port: parseInt(process.env.PORT ?? '3000'),
host: '127.0.0.1',
port: parseInt(process.env.PORT ?? '3000', 10),
host: process.env.HOST ?? '127.0.0.1',
},
plugins: [
viteTsConfigPaths({
projects: ['./tsconfig.json'],
}),
tailwindcss(),
tanstackStart(),
nitro({ preset: process.env.VERCEL ? 'vercel' : 'bun' }),
nitro({
preset:
process.env.VERCEL === '1' || process.env.VERCEL === 'true'
? 'vercel'
: 'bun',
}),
viteReact(),
],
})
Expand Down
44 changes: 39 additions & 5 deletions scripts/check-redis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,17 @@ function parseEnvFile(content: string): Record<string, string> {
(value.startsWith("\"") && value.endsWith("\"")) ||
(value.startsWith("'") && value.endsWith("'"))
) {
const quoteChar = value[0];
value = value.slice(1, -1);

// Unescape the matching quote character and backslashes inside quoted values.
if (quoteChar === "\"") {
// For double-quoted values, interpret \" as " and \\ as \.
value = value.replace(/\\(["\\])/g, "$1");
} else if (quoteChar === "'") {
// For single-quoted values, interpret \' as ' and \\ as \.
value = value.replace(/\\(['\\])/g, "$1");
}
}

parsed[key] = value;
Expand All @@ -28,6 +38,17 @@ function parseEnvFile(content: string): Record<string, string> {
return parsed;
}

function isRedisPingResponse(
value: unknown,
): value is Array<{ result?: string }> {
if (!Array.isArray(value)) return false;
const first = value[0];
if (first === undefined) return true;
if (typeof first !== "object" || first === null) return false;
const result = (first as { result?: unknown }).result;
return result === undefined || typeof result === "string";
}

function loadLocalEnvDefaults(): void {
const envFiles = [
join(process.cwd(), "apps/web/.env.local"),
Expand Down Expand Up @@ -60,8 +81,11 @@ async function main() {
}

try {
const normalizedUrl = url.replace(/\/+$/, "");
const response = await fetch(`${normalizedUrl}/pipeline`, {
const baseUrl = new URL(url);
// Remove trailing slashes from the pathname to avoid double slashes when appending "/pipeline"
baseUrl.pathname = baseUrl.pathname.replace(/\/+$/, "") || "/";
const pipelineUrl = new URL("/pipeline", baseUrl);

@cubic-dev-ai cubic-dev-ai Bot Feb 18, 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: new URL("/pipeline", baseUrl) ignores baseUrl's pathname because /pipeline is an absolute path—it resolves against the origin only. This makes the pathname manipulation on the previous line dead code, and introduces a subtle behavioral regression if the URL ever contains a path prefix.

Use a relative path ("pipeline" without leading /) so the URL resolves relative to the base pathname:

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/check-redis.ts, line 87:

<comment>`new URL("/pipeline", baseUrl)` ignores `baseUrl`'s pathname because `/pipeline` is an absolute path—it resolves against the origin only. This makes the pathname manipulation on the previous line dead code, and introduces a subtle behavioral regression if the URL ever contains a path prefix.

Use a relative path (`"pipeline"` without leading `/`) so the URL resolves relative to the base pathname:</comment>

<file context>
@@ -60,8 +81,11 @@ async function main() {
+		const baseUrl = new URL(url);
+		// Remove trailing slashes from the pathname to avoid double slashes when appending "/pipeline"
+		baseUrl.pathname = baseUrl.pathname.replace(/\/+$/, "") || "/";
+		const pipelineUrl = new URL("/pipeline", baseUrl);
+		const response = await fetch(pipelineUrl, {
 			method: "POST",
</file context>
Fix with Cubic

const response = await fetch(pipelineUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
Expand All @@ -76,7 +100,11 @@ async function main() {
throw new Error(`HTTP ${response.status} ${body}`);
}

const payload = (await response.json()) as Array<{ result?: string }>;
const rawPayload = await response.json();
if (!isRedisPingResponse(rawPayload)) {
throw new Error("Unexpected Redis response format");
}
const payload = rawPayload;
const result = payload[0]?.result;
if (result !== "PONG") {
throw new Error("Redis ping failed");
Expand All @@ -87,10 +115,16 @@ async function main() {
process.stderr.write(
"Check UPSTASH_REDIS_REST_URL and UPSTASH_REDIS_REST_TOKEN.\n",
);
if (process.env.NODE_ENV === "production") {
const nodeEnv = process.env.NODE_ENV;
if (nodeEnv !== "development") {
process.stderr.write(
"[check-redis] Redis is required for rate limiting in this environment. Failing fast.\n",
);
process.exit(1);
}
process.stderr.write("[check-redis] Dev mode — continuing without Redis. Rate limiting is disabled.\n");
process.stderr.write(
"[check-redis] Dev mode — continuing without Redis. Rate limiting is disabled.\n",
);
}
}

Expand Down
Loading