Skip to content
Closed
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: 2 additions & 1 deletion apps/web/.env.example
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
DATABASE_URL="postgresql://postgres:password@localhost:5432/inboxzero?schema=public"
DIRECT_URL="postgresql://postgres:password@localhost:5432/inboxzero?schema=public"

NEXTAUTH_SECRET= # Generate a random secret here: https://generate-secret.vercel.app/32
BETTER_AUTH_SECRET= # Generate a random secret here: https://generate-secret.vercel.app/32
NEXTAUTH_SECRET= # Legacy support - Generate a random secret here: https://generate-secret.vercel.app/32
Comment on lines +4 to +5
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.

⚠️ Potential issue

Ensure encryption envs match env.ts expectations

env.ts requires EMAIL_ENCRYPT_SECRET and EMAIL_ENCRYPT_SALT, but this example file lists GOOGLE_ENCRYPT_* and MICROSOFT_ENCRYPT_*. Add the required EMAIL_ENCRYPT_* envs here to avoid runtime validation failures.

Proposed insertion near the “Gmail” or general auth section:

+# Email encryption (required by apps/web/env.ts)
+EMAIL_ENCRYPT_SECRET= # openssl rand -hex 32
+EMAIL_ENCRYPT_SALT=   # openssl rand -hex 16

Follow-up: If the system actually expects provider-specific secrets (GOOGLE_/MICROSOFT_), either keep both sets or align env.ts to match the provider-specific keys. I can generate a follow-up PR once you confirm the intended keys.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
BETTER_AUTH_SECRET= # Generate a random secret here: https://generate-secret.vercel.app/32
NEXTAUTH_SECRET= # Legacy support - Generate a random secret here: https://generate-secret.vercel.app/32
BETTER_AUTH_SECRET= # Generate a random secret here: https://generate-secret.vercel.app/32
NEXTAUTH_SECRET= # Legacy support - Generate a random secret here: https://generate-secret.vercel.app/32
# Email encryption (required by apps/web/env.ts)
EMAIL_ENCRYPT_SECRET= # openssl rand -hex 32
EMAIL_ENCRYPT_SALT= # openssl rand -hex 16
🧰 Tools
🪛 dotenv-linter (3.3.0)

[warning] 4-4: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 4-4: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)


[warning] 5-5: [SpaceCharacter] The line has spaces around equal sign

(SpaceCharacter)


[warning] 5-5: [ValueWithoutQuotes] This value needs to be surrounded in quotes

(ValueWithoutQuotes)

🤖 Prompt for AI Agents
In apps/web/.env.example around lines 4-5, the example file is missing the
EMAIL_ENCRYPT_SECRET and EMAIL_ENCRYPT_SALT environment variables that env.ts
validates; add EMAIL_ENCRYPT_SECRET and EMAIL_ENCRYPT_SALT (with placeholder
comments to generate random values) near the Gmail/auth section so runtime
validation passes, and either retain the existing GOOGLE_/MICROSOFT_
provider-specific keys or duplicate/alias them as needed to keep provider
expectations and env.ts aligned.

NEXTAUTH_URL=http://localhost:3000
AUTH_TRUST_HOST= # Set to `true` if running with Docker. See https://authjs.dev/getting-started/deployment#auth_trust_host

Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/(landing)/login/error/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { env } from "@/env";
import { useUser } from "@/hooks/useUser";
import { LoadingContent } from "@/components/LoadingContent";
import { Loading } from "@/components/Loading";
import AutoLogOut from "./AutoLogOut";

export default function LogInErrorPage() {
const { data, isLoading, error } = useUser();
Expand Down Expand Up @@ -39,7 +40,7 @@ export default function LogInErrorPage() {
</Button>
}
/>
{/* <AutoLogOut loggedIn={!!session?.user.email} /> */}
<AutoLogOut loggedIn={!!data?.id} />
</LoadingContent>
</BasicLayout>
);
Expand Down
5 changes: 3 additions & 2 deletions apps/web/app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { convertToModelMessages, type UIMessage } from "ai";
import { convertToCoreMessages } from "ai";
import type { UIMessage } from "@ai-sdk/ui-utils";
import { z } from "zod";
import { withEmailAccount } from "@/utils/middleware";
import { getEmailAccountWithAi } from "@/utils/user/get";
Expand Down Expand Up @@ -70,7 +71,7 @@ export const POST = withEmailAccount(async (request) => {

try {
const result = await aiProcessAssistantChat({
messages: convertToModelMessages(uiMessages),
messages: convertToCoreMessages(uiMessages),
emailAccountId,
user,
});
Expand Down
2 changes: 2 additions & 0 deletions apps/web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { env } from "@/env";
import { GlobalProviders } from "@/providers/GlobalProviders";
import { UTM } from "@/app/utm";
import { startupImage } from "@/app/startup-image";
import { StartupMigrations } from "@/components/StartupMigrations";
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.

💡 Verification agent

🧩 Analysis chain

Avoid running startup migrations in the layout render path.

Running migrations during RootLayout render can add latency per request, trigger multiple concurrent runs under load, and entangle UI with ops logic. Prefer Next.js instrumentation.ts to run once per server boot.

Apply this change:

  • Remove the import and component usage in layout.tsx:
-import { StartupMigrations } from "@/components/StartupMigrations";
@@
-        <StartupMigrations />
  • Create apps/web/instrumentation.ts:
// apps/web/instrumentation.ts
import { runStartupMigrations } from "@/utils/startup";

export async function register() {
  await runStartupMigrations();
}

Ensure runStartupMigrations is idempotent and protected by a distributed lock (e.g., Postgres advisory lock or Redis lock) to prevent concurrent execution across replicas.

Also applies to: 88-88


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for StartupMigrations usages..."
rg -n --hidden --no-ignore --smart-case "StartupMigrations" -S -C3 || true

echo
echo "Searching for runStartupMigrations usages/definitions..."
rg -n --hidden --no-ignore --smart-case "runStartupMigrations" -S -C3 || true

echo
echo "Looking for apps/web/app/layout.tsx file..."
rg -n --hidden --no-ignore --smart-case --glob "apps/web/**" "layout.tsx" -S -C3 || true

# If apps/web/app/layout.tsx exists, print it
if [ -f "apps/web/app/layout.tsx" ]; then
  echo
  echo "----- apps/web/app/layout.tsx -----"
  sed -n '1,240p' apps/web/app/layout.tsx || true
else
  echo
  echo "apps/web/app/layout.tsx not found at that exact path. Searching for any layout.tsx..."
  rg -n --hidden --no-ignore --smart-case "layout.tsx" -S -C3 || true
fi

echo
echo "Searching for instrumentation.ts files..."
rg -n --hidden --no-ignore --smart-case "instrumentation.ts" -S -C3 || true

echo
echo "Searching for utils/startup or similar startup utilities..."
rg -n --hidden --no-ignore --smart-case "startup" -S -C3 || true

Length of output: 20456


Move startup migrations out of RootLayout — run once on server boot via instrumentation.register

Verified: apps/web/app/layout.tsx imports and renders StartupMigrations (calls runStartupMigrations in apps/web/utils/startup.ts). No apps/web/instrumentation.ts exists. Running migrations in RootLayout causes per-request latency and concurrent-run risk.

Files to change:

  • apps/web/app/layout.tsx — remove import and component usage (currently at import line ~15 and render line ~88).
  • apps/web/components/StartupMigrations.tsx — server component that currently calls runStartupMigrations(); delete or leave unused.
  • apps/web/utils/startup.ts — keep runStartupMigrations here; make sure it’s idempotent and protected by a distributed lock.
  • Add apps/web/instrumentation.ts — call runStartupMigrations from register() so it runs once per server boot.

Apply these edits:

Remove from apps/web/app/layout.tsx:

-import { StartupMigrations } from "@/components/StartupMigrations";
@@
-        <StartupMigrations />

Add apps/web/instrumentation.ts:

// apps/web/instrumentation.ts
import { runStartupMigrations } from "@/utils/startup";

export async function register() {
  await runStartupMigrations();
}

Ensure runStartupMigrations is idempotent and guarded by a distributed lock (Postgres advisory lock or Redis lock) to prevent concurrent execution across replicas.

🤖 Prompt for AI Agents
In apps/web/app/layout.tsx around line 15 and line 88, remove the import of
StartupMigrations and the component usage so migrations are no longer run
per-request in RootLayout; create a new file apps/web/instrumentation.ts that
exports an async register() which calls runStartupMigrations() from
apps/web/utils/startup.ts so migrations run once at server boot; ensure
runStartupMigrations in apps/web/utils/startup.ts is idempotent and protected by
a distributed lock (e.g., Postgres advisory lock or a Redis lock) to prevent
concurrent execution across replicas; delete or leave
apps/web/components/StartupMigrations.tsx unused after removing its usage.


const inter = Inter({
subsets: ["latin"],
Expand Down Expand Up @@ -84,6 +85,7 @@ export default async function RootLayout({
<body
className={`h-full ${inter.variable} ${calFont.variable} font-sans antialiased`}
>
<StartupMigrations />
<PostHogProvider>
<Suspense>
<PostHogPageview />
Expand Down
13 changes: 13 additions & 0 deletions apps/web/components/StartupMigrations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { runStartupMigrations } from "@/utils/startup";

/**
* Server component that runs startup migrations
* This component will run on the server side when the app loads
*/
export async function StartupMigrations() {
// Run migrations on server side during page load
await runStartupMigrations();

// Return nothing - this is just for side effects
return null;
}
Comment on lines +7 to +13
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.

🛠️ Refactor suggestion

Running migrations on every layout render will add latency and DB load

Because this server component is rendered with the root layout, it’ll execute on most requests. Prefer a once-per-process trigger (or Next.js instrumentation.ts) to avoid per-request overhead.

Apply this diff to execute once per server instance:

 import { runStartupMigrations } from "@/utils/startup";
 
 /**
  * Server component that runs startup migrations
  * This component will run on the server side when the app loads
  */
 export async function StartupMigrations() {
-  // Run migrations on server side during page load
-  await runStartupMigrations();
+  // Ensure migrations run only once per server instance
+  // Module scope guarantees persistence across requests in the same process
+  let startupMigrationsPromise:
+    | Promise<void>
+    | null
+    | undefined = (globalThis as unknown as { __IZ_STARTUP_MIGRATIONS_PROMISE?: Promise<void> })
+      .__IZ_STARTUP_MIGRATIONS_PROMISE;
+  if (!startupMigrationsPromise) {
+    startupMigrationsPromise = runStartupMigrations();
+    (globalThis as unknown as { __IZ_STARTUP_MIGRATIONS_PROMISE?: Promise<void> }).__IZ_STARTUP_MIGRATIONS_PROMISE =
+      startupMigrationsPromise;
+  }
+  await startupMigrationsPromise;
 
   // Return nothing - this is just for side effects
   return null;
 }

Alternatively, move this into apps/web/instrumentation.ts to run once at server start. I can provide the instrumentation.ts variant if you prefer.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function StartupMigrations() {
// Run migrations on server side during page load
await runStartupMigrations();
// Return nothing - this is just for side effects
return null;
}
export async function StartupMigrations() {
// Ensure migrations run only once per server instance
// Module scope guarantees persistence across requests in the same process
let startupMigrationsPromise:
| Promise<void>
| null
| undefined = (globalThis as unknown as { __IZ_STARTUP_MIGRATIONS_PROMISE?: Promise<void> })
.__IZ_STARTUP_MIGRATIONS_PROMISE;
if (!startupMigrationsPromise) {
startupMigrationsPromise = runStartupMigrations();
(globalThis as unknown as { __IZ_STARTUP_MIGRATIONS_PROMISE?: Promise<void> }).__IZ_STARTUP_MIGRATIONS_PROMISE =
startupMigrationsPromise;
}
await startupMigrationsPromise;
// Return nothing - this is just for side effects
return null;
}

3 changes: 2 additions & 1 deletion apps/web/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export const env = createEnv({
NODE_ENV: z.enum(["development", "production", "test"]),
DATABASE_URL: z.string().url(),

NEXTAUTH_SECRET: z.string().min(1),
NEXTAUTH_SECRET: z.string().min(1).optional(), // Legacy support for migration
BETTER_AUTH_SECRET: z.string().min(1).optional(),
Comment on lines +21 to +22
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.

💡 Verification agent

🧩 Analysis chain

Scan for accidental hardcoded provider tokens

Given the sensitive nature of auth and queue/redis providers, ensure no Upstash/QStash secrets are hardcoded in source files.

Use this secret-scan to flag suspicious assignments (excludes .env files):


🏁 Script executed:

#!/bin/bash
# Grep for potentially hardcoded UPSTASH/QSTASH tokens in code (not env files)
rg -n -C1 -P '(UPSTASH|QSTASH)[A-Z0-9_]*\s*[:=]\s*["\'][^"\']{8,}["\']' -g '!**/.env*' -g '!**/node_modules/**'

Length of output: 116


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "Searching for provider names (upstash/qstash)..."
rg -n -C1 -P "(?i)\b(upstash|qstash)\b" -g '!**/.env*' -g '!**/node_modules/**' || echo "No matches for provider names."

echo "Searching for secret/token/key assignments (>=8 chars)..."
rg -n -C1 -P "(?i)\b(secret|token|key)\b\s*[:=]\s*['\"][A-Za-z0-9_\-+/=]{8,}['\"]" -g '!**/.env*' -g '!**/node_modules/**' || echo "No matches for secret assignments."

echo "Searching for long string literals (>=20 chars)..."
rg -n -C1 -P "['\"][A-Za-z0-9_\-+/=]{20,}['\"]" -g '!**/.env*' -g '!**/node_modules/**' || echo "No long string literals found."

Length of output: 240467


Hardcoded Upstash/QStash tokens found — please remove & rotate immediately

Scan results show committed token literals in the repo. Actionable locations:

  • docker-compose.yml
    • SRH_TOKEN (line ~35) — hardcoded long token literal present.
    • UPSTASH_REDIS_TOKEN (line ~61) — hardcoded long token literal present (same value).
  • docker/Dockerfile.prod
    • Several build-time envs set to dummy secrets (NEXTAUTH_SECRET, UPSTASH_REDIS_TOKEN, QSTASH_TOKEN, QSTASH_CURRENT_SIGNING_KEY, QSTASH_NEXT_SIGNING_KEY). Confirm these are intentionally dummy; if not, remove.
  • apps/web/env.ts (lines 21–22)
    • NEXTAUTH_SECRET / BETTER_AUTH_SECRET are Zod schema entries (no hardcoded secret here).

Recommended fixes (concise):

  • Remove the hardcoded tokens from docker-compose.yml and use environment variables / secrets manager instead.
  • Immediately rotate any tokens that were committed to source control.
  • If dummy values are needed for builds, use build ARGs or placeholder values that cannot be confused with real secrets.
  • Add a secret-scan check to CI / pre-commit to prevent future commits.
🤖 Prompt for AI Agents
In apps/web/env.ts around lines 21 to 22, the reviewer flagged hardcoded
Upstash/QStash tokens in the repo and listed these Zod schema entries
(NEXTAUTH_SECRET / BETTER_AUTH_SECRET); ensure you are not assigning any literal
default values in this file—keep these schema entries as optional validators
only and do not populate them with secrets. Remove any accidental hardcoded
strings, confirm the app reads secrets from process.env or a secret manager at
runtime, and if builds require placeholders use non-sensitive dummy values (or
build ARGs) that cannot be mistaken for real tokens; separately rotate any real
tokens found in docker-compose.yml or Dockerfile.prod and replace them with env
vars or secret references.

NEXTAUTH_URL: z.string().optional(),
AUTH_TRUST_HOST: z.coerce.boolean().optional(),

Expand Down
3 changes: 3 additions & 0 deletions apps/web/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ const withMDX = nextMdx();

const nextConfig: NextConfig = {
reactStrictMode: true,
typescript: {
ignoreBuildErrors: true,
},
Comment on lines +18 to +20
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.

⚠️ Potential issue

Don’t ship with TypeScript errors suppressed in production

typescript.ignoreBuildErrors: true masks real issues and undermines type safety (violates “Use TypeScript with strict null checks”). Gate it by environment so production builds fail on type errors.

Apply this diff to only ignore in non-production:

   typescript: {
-    ignoreBuildErrors: true,
+    ignoreBuildErrors: env.NODE_ENV !== "production",
   },

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/web/next.config.ts around lines 18 to 20, the config currently sets
typescript.ignoreBuildErrors: true which suppresses TypeScript errors in all
environments; change it so ignoreBuildErrors is true only for non-production
builds (e.g., NODE_ENV !== 'production') and false (or omitted) for production.
Update the config to read the environment and set ignoreBuildErrors accordingly,
ensuring production builds fail on type errors.

serverExternalPackages: ["@sentry/nextjs", "@sentry/node"],
turbopack: {
rules: {
Expand Down
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@ai-sdk/openai": "2.0.0",
"@ai-sdk/provider": "2.0.0",
"@ai-sdk/react": "2.0.0",
"@ai-sdk/ui-utils": "^1.2.11",
"@asteasolutions/zod-to-openapi": "7.3.2",
"@dub/analytics": "0.0.27",
"@formkit/auto-animate": "0.8.2",
Expand Down
14 changes: 14 additions & 0 deletions apps/web/prisma/migrations/20250623222304_/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
Warnings:

- You are about to drop the column `digestScheduleId` on the `EmailAccount` table. All the data in the column will be lost.

*/
-- DropForeignKey
ALTER TABLE "EmailAccount" DROP CONSTRAINT "EmailAccount_digestScheduleId_fkey";

-- DropIndex
DROP INDEX "EmailAccount_digestScheduleId_key";

-- AlterTable
ALTER TABLE "EmailAccount" DROP COLUMN "digestScheduleId";
4 changes: 3 additions & 1 deletion apps/web/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ model Account {
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at DateTime? @default(now())
expires_at DateTime?
accessTokenExpiresAt DateTime?
refreshTokenExpiresAt DateTime?
Comment on lines +22 to +24
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.

💡 Verification agent

🧩 Analysis chain

Verify data migration and usage consistency for new token expiry fields.

You added accessTokenExpiresAt and refreshTokenExpiresAt and left expires_at as optional. Ensure:

  • Existing data in expires_at is migrated to accessTokenExpiresAt.
  • All code paths that read/write expirations use the new fields.
  • Consider removing or formally deprecating expires_at to avoid ambiguity.

Run this script to check for remaining code references and migration presence:

If gaps are found, I can generate a safe, idempotent Prisma migration to backfill accessTokenExpiresAt from expires_at.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for legacy 'expires_at' usage in TS/TSX..."
rg -nP --type=ts --type=tsx -C2 '\bexpires_at\b'

echo
echo "Searching for new fields usage..."
rg -nP --type=ts --type=tsx -C2 '\b(accessTokenExpiresAt|refreshTokenExpiresAt)\b'

echo
echo "Checking Prisma migrations mentioning the new fields..."
fd -H migrations | xargs -I{} rg -nP -C2 '(accessTokenExpiresAt|refreshTokenExpiresAt|expires_at)' {}

Length of output: 219


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Showing apps/web/prisma/schema.prisma (if present)..."
if [ -f "apps/web/prisma/schema.prisma" ]; then
  sed -n '1,240p' apps/web/prisma/schema.prisma || true
else
  echo "apps/web/prisma/schema.prisma not found"
fi

echo
echo "2) Searching for 'expires_at' (snake_case) across repository..."
rg -n -C2 '\bexpires_at\b' --hidden --glob '!**/node_modules/**' || true

echo
echo "3) Searching for 'expiresAt' (camelCase) across repository..."
rg -n -C2 '\bexpiresAt\b' --hidden --glob '!**/node_modules/**' || true

echo
echo "4) Searching for new fields 'accessTokenExpiresAt' or 'refreshTokenExpiresAt'..."
rg -n -C2 '\b(accessTokenExpiresAt|refreshTokenExpiresAt)\b' --hidden --glob '!**/node_modules/**' || true

echo
echo "5) Searching .prisma files and migrations for any mentions..."
rg -n -C2 '(accessTokenExpiresAt|refreshTokenExpiresAt|expires_at)' --hidden --glob '**/*.prisma' --glob '!**/node_modules/**' || true
rg -n -C2 '(accessTokenExpiresAt|refreshTokenExpiresAt|expires_at)' --hidden --glob '**/migrations/**' --glob '!**/node_modules/**' || true

Length of output: 45170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

files=(
  "apps/web/utils/migration/fix-encrypted-tokens.ts"
  "apps/web/utils/auth.ts"
  "apps/web/utils/api-auth.ts"
  "apps/web/prisma/migrations/20250804163003_better_auth/migration.sql"
  "apps/web/prisma/migrations/20230730073019_init/migration.sql"
)

for f in "${files[@]}"; do
  echo "---- $f ----"
  if [ -f "$f" ]; then
    sed -n '1,240p' "$f" || true
  else
    echo "MISSING: $f"
  fi
  echo
done

Length of output: 21914


Action required: migrate expires_at -> accessTokenExpiresAt/refreshTokenExpiresAt and update codepaths

Short summary: I searched the repo. accessTokenExpiresAt/refreshTokenExpiresAt were added to schema.prisma, but many code paths still read/write the old expires_at column and the migrations do not add/backfill the new columns. You need a migration + code updates.

Files/locations that still reference expires_at (representative):

  • apps/web/prisma/schema.prisma (expires_at + new fields present)
  • apps/web/prisma/migrations/20250804163003_better_auth/migration.sql (changes expires_at type but does NOT add new columns)
  • apps/web/utils/api-auth.ts (selects/uses expires_at; validateApiKeyAndGetGmailClient)
  • apps/web/utils/account.ts (multiple selects/mappings of expires_at)
  • apps/web/utils/user/get.ts, apps/web/utils/user/delete.ts (selects/mappings)
  • apps/web/utils/gmail/client.ts (sets tokens.expires_at)
  • apps/web/utils/migration/fix-encrypted-tokens.ts (clears expires_at but only checks for new fields existence)
  • many API routes/controllers under apps/web/app/api/** (google/, outlook/, watch/, clean/, ai/, unsubscribe) selecting/mapping expires_at

Places already using the new fields:

  • apps/web/utils/auth.ts — maps tokens.expires_at into accessTokenExpiresAt when creating/updating accounts
  • apps/web/utils/migration/fix-encrypted-tokens.ts — checks for accessTokenExpiresAt/refreshTokenExpiresAt to decide whether to run cleanup

Required fixes (high level):

  • Add a Prisma migration that:
    • Adds accessTokenExpiresAt and refreshTokenExpiresAt columns to Account,
    • Backfills accessTokenExpiresAt from expires_at (convert as needed),
    • Is idempotent (safe to run on staging/production).
  • Update all code paths that read expires_at to:
    • Prefer account.accessTokenExpiresAt (and refreshTokenExpiresAt where applicable),
    • Fallback to expires_at only until backfill is applied (for compatibility).
  • Update all code paths that write token expiries to set the new fields (auth.ts already does this for accessTokenExpiresAt; ensure all other token write sites do likewise).
  • Update fix-encrypted-tokens.ts to clear the new fields when wiping corrupted tokens (currently it only clears expires_at).
  • After thorough rollout and verification, remove or formally deprecate expires_at from the schema and code.

If you want, I can generate the idempotent Prisma migration SQL (ADD COLUMN + UPDATE backfill) and a small codemod to replace common selects/usages or a checklist of exact files/lines to change.

🤖 Prompt for AI Agents
In apps/web/prisma/schema.prisma around lines 22–24, the schema added
accessTokenExpiresAt and refreshTokenExpiresAt but many codepaths and migrations
still use expires_at; create an idempotent Prisma migration that adds
accessTokenExpiresAt and refreshTokenExpiresAt to the Account table and
backfills accessTokenExpiresAt from expires_at (converting types as needed),
then update all code that reads expires_at to prefer
account.accessTokenExpiresAt (and refreshTokenExpiresAt where relevant) with a
fallback to expires_at until backfill completes, update all token-write sites to
set the new fields when saving tokens, modify
apps/web/utils/migration/fix-encrypted-tokens.ts to clear
accessTokenExpiresAt/refreshTokenExpiresAt as well as expires_at when wiping
corrupted tokens, and deploy/verify the backfill before removing/deprecating
expires_at from schema and code.

token_type String?
scope String?
id_token String? @db.Text
Expand Down
27 changes: 10 additions & 17 deletions apps/web/providers/ChatProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use client";

import { useChat as useAiChat } from "@ai-sdk/react";
import { DefaultChatTransport } from "ai";
import { parseAsString, useQueryState } from "nuqs";
import {
createContext,
Expand Down Expand Up @@ -47,20 +46,14 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {

const chat = useAiChat<ChatMessage>({
id: chatId ?? undefined,
transport: new DefaultChatTransport({
api: "/api/chat",
headers: {
[EMAIL_ACCOUNT_HEADER]: emailAccountId,
},
prepareSendMessagesRequest({ messages, id, body }) {
return {
body: {
id,
message: messages.at(-1),
...body,
},
};
},
api: "/api/chat",
headers: {
[EMAIL_ACCOUNT_HEADER]: emailAccountId,
},
experimental_prepareRequestBody: ({ id, messages, requestBody }) => ({
Comment on lines +49 to +53
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.

🛠️ Refactor suggestion

Don’t send undefined header values

If emailAccountId is null/undefined, you’ll set an invalid header. Omit the header when not available.

Apply this diff:

-    api: "/api/chat",
-    headers: {
-      [EMAIL_ACCOUNT_HEADER]: emailAccountId,
-    },
+    api: "/api/chat",
+    headers: emailAccountId
+      ? {
+          [EMAIL_ACCOUNT_HEADER]: emailAccountId,
+        }
+      : undefined,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
api: "/api/chat",
headers: {
[EMAIL_ACCOUNT_HEADER]: emailAccountId,
},
experimental_prepareRequestBody: ({ id, messages, requestBody }) => ({
api: "/api/chat",
headers: emailAccountId
? {
[EMAIL_ACCOUNT_HEADER]: emailAccountId,
}
: undefined,
experimental_prepareRequestBody: ({ id, messages, requestBody }) => ({
🤖 Prompt for AI Agents
In apps/web/providers/ChatProvider.tsx around lines 49 to 53, the
EMAIL_ACCOUNT_HEADER is being set to emailAccountId even when that value can be
null/undefined; change the headers object to only include the
EMAIL_ACCOUNT_HEADER when emailAccountId is present (e.g., build headers
conditionally or spread a conditional object) so that no header key is sent with
an undefined value. Ensure the rest of the request config remains unchanged.

id,
message: messages.at(-1),
...requestBody,
}),
// TODO: couldn't get this to work
// messages: initialMessages,
Expand All @@ -83,7 +76,7 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
}, [chat.setMessages, data]);

const handleSubmit = useCallback(() => {
chat.sendMessage({
chat.append({
role: "user",
parts: [
{
Expand All @@ -94,7 +87,7 @@ export function ChatProvider({ children }: { children: React.ReactNode }) {
});

setInput("");
}, [chat.sendMessage, input]);
}, [chat.append, input]);

return (
<ChatContext.Provider
Expand Down
39 changes: 39 additions & 0 deletions apps/web/scripts/fix-corrupted-tokens.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
#!/usr/bin/env tsx

/**
* Standalone script to fix corrupted tokens from NextAuth to Better-Auth migration
* Usage: pnpm exec tsx scripts/fix-corrupted-tokens.ts
*/

import { fixCorruptedTokens } from "@/utils/migration/fix-encrypted-tokens";

async function main() {
console.log("🔧 Starting corrupted token cleanup...");

try {
const result = await fixCorruptedTokens();

console.log("✅ Token cleanup completed successfully!");
console.log("📊 Results:");
console.log(` - Total accounts checked: ${result.total}`);
console.log(` - Corrupted tokens fixed: ${result.fixed}`);

if (result.fixed > 0) {
console.log(
"\n⚠️ Users with affected accounts will need to re-authenticate on their next login.",
);
console.log(
" This will happen automatically - no action required from users.",
);
} else {
console.log("🎉 No corrupted tokens found - all accounts are healthy!");
}

process.exit(0);
} catch (error) {
console.error("❌ Error during token cleanup:", error);
process.exit(1);
}
}

main();
2 changes: 1 addition & 1 deletion apps/web/utils/ai/assistant/process-user-request.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { stepCountIs, tool } from "ai";
import { tool, stepCountIs } from "ai";
import { z } from "zod";
import { after } from "next/server";
import { createGenerateText } from "@/utils/llms";
Expand Down
2 changes: 1 addition & 1 deletion apps/web/utils/ai/example-matches/find-example-matches.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { stepCountIs, tool } from "ai";
import { tool, stepCountIs } from "ai";
import { z } from "zod";
import type { gmail_v1 } from "@googleapis/gmail";
import { createGenerateText } from "@/utils/llms";
Expand Down
2 changes: 1 addition & 1 deletion apps/web/utils/ai/group/create-group.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { stepCountIs, tool } from "ai";
import { tool, stepCountIs } from "ai";
import { z } from "zod";
import type { gmail_v1 } from "@googleapis/gmail";
import { createGenerateText } from "@/utils/llms";
Expand Down
13 changes: 10 additions & 3 deletions apps/web/utils/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,11 @@ export const betterAuthConfig = betterAuth({
},
baseURL: env.NEXT_PUBLIC_BASE_URL,
trustedOrigins: [env.NEXT_PUBLIC_BASE_URL],
secret: process.env.NEXTAUTH_SECRET,
secret:
env.BETTER_AUTH_SECRET ||
env.NEXTAUTH_SECRET ||
process.env.BETTER_AUTH_SECRET ||
process.env.NEXTAUTH_SECRET,
Comment on lines +46 to +50
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.

🛠️ Refactor suggestion

Fail fast if auth secret is missing to avoid runtime misconfiguration.

The OR chain can evaluate to undefined when both secrets are absent. better-auth expects a secret; proceed with an explicit check and clear error.

Apply this change near the top of the file (before betterAuthConfig):

const AUTH_SECRET =
  env.BETTER_AUTH_SECRET ??
  env.NEXTAUTH_SECRET ??
  process.env.BETTER_AUTH_SECRET ??
  process.env.NEXTAUTH_SECRET;

if (!AUTH_SECRET) {
  throw new Error(
    "Auth secret missing. Set BETTER_AUTH_SECRET (preferred) or NEXTAUTH_SECRET."
  );
}

Then update the config:

-  secret:
-    env.BETTER_AUTH_SECRET ||
-    env.NEXTAUTH_SECRET ||
-    process.env.BETTER_AUTH_SECRET ||
-    process.env.NEXTAUTH_SECRET,
+  secret: AUTH_SECRET,

I can send a full diff if you prefer it inline.

🤖 Prompt for AI Agents
In apps/web/utils/auth.ts around lines 46 to 50, the current secret fallback can
be undefined and cause runtime misconfiguration; add an explicit AUTH_SECRET
resolution using nullish coalescing (check env.BETTER_AUTH_SECRET,
env.NEXTAUTH_SECRET, process.env.BETTER_AUTH_SECRET,
process.env.NEXTAUTH_SECRET), throw a clear Error if AUTH_SECRET is falsy before
betterAuthConfig is used, and then replace the inline secret expression in the
config with AUTH_SECRET so the app fails fast when no secret is provided.

emailAndPassword: {
enabled: false,
},
Expand Down Expand Up @@ -71,7 +75,8 @@ export const betterAuthConfig = betterAuth({
providerId: "provider",
refreshToken: "refresh_token",
accessToken: "access_token",
accessTokenExpiresAt: "expires_at",
accessTokenExpiresAt: "accessTokenExpiresAt",
refreshTokenExpiresAt: "refreshTokenExpiresAt",
idToken: "id_token",
},
},
Expand Down Expand Up @@ -430,7 +435,9 @@ export async function saveTokens({

const data = {
access_token: tokens.access_token,
expires_at: tokens.expires_at ? new Date(tokens.expires_at * 1000) : null,
accessTokenExpiresAt: tokens.expires_at
? new Date(tokens.expires_at * 1000)
: null,
refresh_token: refreshToken,
};
Comment on lines 436 to 442
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.

⚠️ Potential issue

Avoid clobbering refresh_token on encryption failure; skip the field update instead.

Currently, if encryptToken fails, refresh_token is set to an empty string, which can irreversibly break the account. Treat encryption failures as non-updates for that field, mirroring access_token handling.

Apply this diff:

-  const data = {
-    access_token: tokens.access_token,
-    accessTokenExpiresAt: tokens.expires_at
-      ? new Date(tokens.expires_at * 1000)
-      : null,
-    refresh_token: refreshToken,
-  };
+  const data: {
+    access_token?: string;
+    accessTokenExpiresAt: Date | null;
+    refresh_token?: string;
+  } = {
+    accessTokenExpiresAt: tokens.expires_at
+      ? new Date(tokens.expires_at * 1000)
+      : null,
+  };
+  if (tokens.access_token) data.access_token = tokens.access_token;
+  if (refreshToken) data.refresh_token = refreshToken;
@@
-    if (data.access_token)
-      data.access_token = encryptToken(data.access_token) || undefined;
-    if (data.refresh_token)
-      data.refresh_token = encryptToken(data.refresh_token) || "";
+    if (data.access_token) {
+      const enc = encryptToken(data.access_token);
+      data.access_token = enc ?? undefined; // skip update on failure
+    }
+    if (data.refresh_token) {
+      const enc = encryptToken(data.refresh_token);
+      if (enc) {
+        data.refresh_token = enc;
+      } else {
+        // Skip refresh token update to avoid data loss
+        delete data.refresh_token;
+        logger.error("Failed to encrypt refresh token; skipping update", {
+          emailAccountId,
+          providerAccountId,
+          provider,
+        });
+      }
+    }

This keeps existing DB values intact when encryption fails, preventing accidental logout/breakage.

Also applies to: 448-452

🤖 Prompt for AI Agents
In apps/web/utils/auth.ts around lines 436-442 and also 448-452, the current
code overwrites refresh_token with an empty string when encryptToken fails;
change the logic to mirror access_token handling by treating encryption failures
as non-updates for refresh_token — i.e., call encryptToken in a try/catch and
only set data.refresh_token to the encrypted value if encryption succeeds,
otherwise do not modify data.refresh_token (leave it undefined or omit the field
so the DB retains the existing value).


Expand Down
16 changes: 15 additions & 1 deletion apps/web/utils/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ export function decryptToken(encryptedText: string | null): string | null {
try {
const buffer = Buffer.from(encryptedText, "hex");

// Validate buffer length - must contain at least IV + Auth Tag
if (buffer.length < IV_LENGTH + AUTH_TAG_LENGTH) {
logger.warn("Encrypted token too short, likely corrupted");
return null;
}
Comment on lines +63 to +67
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.

🛠️ Refactor suggestion

Add hex-format validation before/around decryption to prevent truncated/invalid buffers.

Buffer.from with "hex" will silently drop an odd trailing nibble or non-hex chars, which can pass the length check and cause misleading errors. Validate the input is even-length and hex-only before proceeding.

Apply this diff to harden the validator:

-    // Validate buffer length - must contain at least IV + Auth Tag
-    if (buffer.length < IV_LENGTH + AUTH_TAG_LENGTH) {
-      logger.warn("Encrypted token too short, likely corrupted");
-      return null;
-    }
+    // Validate hex format (avoid truncated bytes) and minimum length
+    if (
+      typeof encryptedText !== "string" ||
+      encryptedText.length % 2 !== 0 ||
+      !/^[0-9a-fA-F]+$/.test(encryptedText)
+    ) {
+      logger.warn("Encrypted token has invalid hex format");
+      return null;
+    }
+
+    // Validate buffer length - must contain at least IV + Auth Tag
+    if (buffer.length < IV_LENGTH + AUTH_TAG_LENGTH) {
+      logger.warn("Encrypted token too short, likely corrupted", {
+        byteLength: buffer.length,
+      });
+      return null;
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Validate buffer length - must contain at least IV + Auth Tag
if (buffer.length < IV_LENGTH + AUTH_TAG_LENGTH) {
logger.warn("Encrypted token too short, likely corrupted");
return null;
}
// Validate hex format (avoid truncated bytes) and minimum length
if (
typeof encryptedText !== "string" ||
encryptedText.length % 2 !== 0 ||
!/^[0-9a-fA-F]+$/.test(encryptedText)
) {
logger.warn("Encrypted token has invalid hex format");
return null;
}
// Validate buffer length - must contain at least IV + Auth Tag
if (buffer.length < IV_LENGTH + AUTH_TAG_LENGTH) {
logger.warn("Encrypted token too short, likely corrupted", {
byteLength: buffer.length,
});
return null;
}
🤖 Prompt for AI Agents
In apps/web/utils/encryption.ts around lines 63 to 67, the code only checks
buffer length but does not validate the incoming hex string before Buffer.from,
allowing odd-length or non-hex input to be silently truncated; before calling
Buffer.from(hexString, "hex") strip an optional "0x" prefix, verify the
remaining string matches /^[0-9a-fA-F]+$/ and has even length (string.length % 2
=== 0), and if either check fails log a warning about invalid hex input and
return null so the subsequent IV/auth-tag length check is reliable.


// Extract IV (first 16 bytes)
const iv = buffer.subarray(0, IV_LENGTH);

Expand All @@ -79,7 +85,15 @@ export function decryptToken(encryptedText: string | null): string | null {

return decrypted.toString("utf8");
} catch (error) {
logger.error("Decryption failed", { error });
// Reduce log noise - only log detailed errors in development
if (process.env.NODE_ENV === "development") {
logger.error("Decryption failed", {
error,
encryptedLength: encryptedText?.length,
});
} else {
logger.warn("Token decryption failed - token may need refresh");
}
return null;
}
}
2 changes: 1 addition & 1 deletion apps/web/utils/llms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
RetryError,
streamText,
smoothStream,
stepCountIs,
type StreamTextOnFinishCallback,
type StreamTextOnStepFinishCallback,
stepCountIs,
} from "ai";
import type { LanguageModelV2 } from "@ai-sdk/provider";
import { saveAiUsage } from "@/utils/usage";
Expand Down
Loading