Skip to content
Merged
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
2 changes: 2 additions & 0 deletions lib/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ export const SUPABASE_STORAGE_BUCKET = "user-files";
*/
export const RECOUP_ORG_ID = "04e3aba9-c130-4fb8-8b92-34e95d43e66b";

export const RECOUP_API_KEY = process.env.RECOUP_API_KEY || "";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

RECOUP_API_KEY should fail fast when unset, not silently fall back to "".

PRIVY_PROJECT_SECRET on lines 3–5 guards against a missing env var at startup with an explicit throw. RECOUP_API_KEY is equally load-bearing in production — it's the service-level auth token that gates MCP tool access in the email inbound flow. Falling back to "" means the app starts up successfully even if this env var is never configured in Vercel, and every inbound email request will carry an empty authToken with no error signal — silently regressing to pre-PR, tool-less behavior. The PR description itself calls out configuring this env var as a manual step, making the silent-failure risk concrete.

Apply the same startup-guard pattern used by PRIVY_PROJECT_SECRET:

🛡️ Proposed fix: fail fast on missing RECOUP_API_KEY
+if (!process.env.RECOUP_API_KEY) {
+  throw new Error("RECOUP_API_KEY environment variable is required");
+}
+
-export const RECOUP_API_KEY = process.env.RECOUP_API_KEY || "";
+export const RECOUP_API_KEY = process.env.RECOUP_API_KEY;
📝 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 const RECOUP_API_KEY = process.env.RECOUP_API_KEY || "";
if (!process.env.RECOUP_API_KEY) {
throw new Error("RECOUP_API_KEY environment variable is required");
}
export const RECOUP_API_KEY = process.env.RECOUP_API_KEY;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/const.ts` at line 32, RECOUP_API_KEY currently defaults to an empty
string which hides a missing configuration; change its initialization to mirror
the startup guard used by PRIVY_PROJECT_SECRET by throwing an explicit Error
when process.env.RECOUP_API_KEY is not set (refer to the existing
PRIVY_PROJECT_SECRET pattern) so the app fails fast at startup instead of
silently using "" for RECOUP_API_KEY.


// EVALS
export const EVAL_ACCOUNT_ID = "fb678396-a68f-4294-ae50-b8cacf9ce77b";
export const EVAL_ACCESS_TOKEN = process.env.EVAL_ACCESS_TOKEN || "";
Expand Down
116 changes: 116 additions & 0 deletions lib/emails/inbound/__tests__/validateNewEmailMemory.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { validateNewEmailMemory } from "../validateNewEmailMemory";
import type { ResendEmailReceivedEvent } from "@/lib/emails/validateInboundEmailEvent";
import { NextResponse } from "next/server";

vi.mock("@/lib/supabase/account_emails/selectAccountEmails", () => ({
default: vi.fn(),
}));

vi.mock("@/lib/emails/inbound/getEmailContent", () => ({
getEmailContent: vi.fn(),
}));

vi.mock("@/lib/emails/inbound/getEmailRoomId", () => ({
getEmailRoomId: vi.fn(),
}));

vi.mock("@/lib/emails/inbound/trimRepliedContext", () => ({
trimRepliedContext: vi.fn((html: string) => html),
}));

vi.mock("@/lib/chat/setupConversation", () => ({
setupConversation: vi.fn(),
}));

vi.mock("@/lib/supabase/memory_emails/insertMemoryEmail", () => ({
default: vi.fn(),
}));

vi.mock("@/lib/messages/getMessages", () => ({
getMessages: vi.fn((text: string) => [{ role: "user", content: text }]),
}));

vi.mock("@/lib/const", () => ({
RECOUP_API_KEY: "test-recoup-api-key",
}));

import selectAccountEmails from "@/lib/supabase/account_emails/selectAccountEmails";
import { getEmailContent } from "@/lib/emails/inbound/getEmailContent";
import { getEmailRoomId } from "@/lib/emails/inbound/getEmailRoomId";
import { setupConversation } from "@/lib/chat/setupConversation";

const MOCK_ACCOUNT_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee";
const MOCK_ROOM_ID = "11111111-2222-3333-4444-555555555555";
const MOCK_EMAIL_ID = "email-123";
const MOCK_MESSAGE_ID = "msg-456";

function createMockEvent(overrides?: Partial<ResendEmailReceivedEvent["data"]>): ResendEmailReceivedEvent {
return {
type: "email.received",
created_at: "2024-01-01T00:00:00.000Z",
data: {
email_id: MOCK_EMAIL_ID,
from: "artist@example.com",
to: ["agent@mail.recoupable.com"],
subject: "Test email",
message_id: MOCK_MESSAGE_ID,
created_at: "2024-01-01T00:00:00.000Z",
...overrides,
},
} as ResendEmailReceivedEvent;
}

describe("validateNewEmailMemory", () => {
beforeEach(() => {
vi.clearAllMocks();

vi.mocked(selectAccountEmails).mockResolvedValue([
{ account_id: MOCK_ACCOUNT_ID } as Awaited<ReturnType<typeof selectAccountEmails>>[0],
]);

vi.mocked(getEmailContent).mockResolvedValue({
html: "<p>Hello from email</p>",
headers: {},
} as Awaited<ReturnType<typeof getEmailContent>>);

vi.mocked(getEmailRoomId).mockResolvedValue(undefined);

vi.mocked(setupConversation).mockResolvedValue({ roomId: MOCK_ROOM_ID });
});

it("includes authToken from RECOUP_API_KEY in chatRequestBody", async () => {
const event = createMockEvent();

const result = await validateNewEmailMemory(event);

// Should not be a response (duplicate)
expect(result).not.toHaveProperty("response");

const { chatRequestBody } = result as { chatRequestBody: { authToken?: string }; emailText: string };
expect(chatRequestBody.authToken).toBe("test-recoup-api-key");
});

it("returns chatRequestBody with correct accountId, orgId, messages, and roomId", async () => {
const event = createMockEvent();

const result = await validateNewEmailMemory(event);
const { chatRequestBody } = result as { chatRequestBody: Record<string, unknown>; emailText: string };

expect(chatRequestBody.accountId).toBe(MOCK_ACCOUNT_ID);
expect(chatRequestBody.orgId).toBeNull();
expect(chatRequestBody.roomId).toBe(MOCK_ROOM_ID);
expect(chatRequestBody.messages).toBeDefined();
});

it("returns duplicate response when setupConversation throws unique constraint error", async () => {
vi.mocked(setupConversation).mockRejectedValue({ code: "23505" });

const event = createMockEvent();
const result = await validateNewEmailMemory(event);

expect(result).toHaveProperty("response");
const { response } = result as { response: NextResponse };
expect(response.status).toBe(200);
});
});
2 changes: 2 additions & 0 deletions lib/emails/inbound/validateNewEmailMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getMessages } from "@/lib/messages/getMessages";
import { getEmailContent } from "@/lib/emails/inbound/getEmailContent";
import { getEmailRoomId } from "@/lib/emails/inbound/getEmailRoomId";
import { ChatRequestBody } from "@/lib/chat/validateChatRequest";
import { RECOUP_API_KEY } from "@/lib/const";
import { setupConversation } from "@/lib/chat/setupConversation";
import insertMemoryEmail from "@/lib/supabase/memory_emails/insertMemoryEmail";
import { trimRepliedContext } from "@/lib/emails/inbound/trimRepliedContext";
Expand Down Expand Up @@ -70,6 +71,7 @@ export async function validateNewEmailMemory(
orgId: null,
messages: getMessages(emailText),
roomId: finalRoomId,
authToken: RECOUP_API_KEY,
};

return { chatRequestBody, emailText };
Expand Down