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
20 changes: 3 additions & 17 deletions app/api/e2/emails/reply.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
import { SendEmailCommand } from "@aws-sdk/client-sesv2";
import { waitUntil } from "@vercel/functions";
import { Autumn as autumn } from "autumn-js";
import { and, eq } from "drizzle-orm";
Expand All @@ -8,6 +8,7 @@ import {
getTenantSendingInfoForDomainOrParent,
type TenantSendingInfo,
} from "@/lib/aws-ses/identity-arn-helper";
import { getSesClient } from "@/lib/aws-ses/ses-client";
import { db } from "@/lib/db";
import {
SENT_EMAIL_STATUS,
Expand All @@ -32,22 +33,7 @@ import {
} from "../helper/attachment-processor";
import { validateAndRateLimit } from "../lib/auth";

// Initialize SES client
const awsRegion = process.env.AWS_REGION || "us-east-2";
const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID;
const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;

let sesClient: SESv2Client | null = null;

if (awsAccessKeyId && awsSecretAccessKey) {
sesClient = new SESv2Client({
region: awsRegion,
credentials: {
accessKeyId: awsAccessKeyId,
secretAccessKey: awsSecretAccessKey,
},
});
}
const sesClient = getSesClient();

// Request schema
const AttachmentSchema = t.Object({
Expand Down
20 changes: 3 additions & 17 deletions app/api/e2/emails/send.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
import { SendEmailCommand } from "@aws-sdk/client-sesv2";
import { Client as QStashClient } from "@upstash/qstash";
import { waitUntil } from "@vercel/functions";
import { Autumn as autumn } from "autumn-js";
Expand All @@ -10,6 +10,7 @@ import {
getTenantSendingInfoForDomainOrParent,
type TenantSendingInfo,
} from "@/lib/aws-ses/identity-arn-helper";
import { getSesClient } from "@/lib/aws-ses/ses-client";
import { db } from "@/lib/db";
import {
SCHEDULED_EMAIL_STATUS,
Expand Down Expand Up @@ -39,22 +40,7 @@ import {
import { buildRawEmailMessage } from "../helper/email-builder";
import { validateAndRateLimit } from "../lib/auth";

// Initialize SES client
const awsRegion = process.env.AWS_REGION || "us-east-2";
const awsAccessKeyId = process.env.AWS_ACCESS_KEY_ID;
const awsSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;

let sesClient: SESv2Client | null = null;

if (awsAccessKeyId && awsSecretAccessKey) {
sesClient = new SESv2Client({
region: awsRegion,
credentials: {
accessKeyId: awsAccessKeyId,
secretAccessKey: awsSecretAccessKey,
},
});
}
const sesClient = getSesClient();

// Request schema
const AttachmentSchema = t.Object({
Expand Down
4 changes: 2 additions & 2 deletions app/api/e2/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,9 @@ export async function validateAndRateLimit(
} else if (
apiSession?.valid &&
!apiSession?.error &&
apiSession?.key?.userId
apiSession?.key?.referenceId
) {
userId = apiSession.key.userId;
userId = apiSession.key.referenceId;
Copy link

Choose a reason for hiding this comment

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

Incomplete userId to referenceId migration breaks API auth

High Severity

The migration from better-auth/plugins to @better-auth/api-key@^1.5.0 renames key.userId to key.referenceId in the verifyApiKey response. Two auth files were updated, but app/api/e2/helper/main.ts (line 77–80) still reads apiKeyResult.key.userId, which will now be undefined. This causes the validateRequest function (used by app/api/internal/search/route.ts) to silently reject all valid API key authentications with a 401 "Unauthorized" error.

Additional Locations (2)

Fix in Cursor Fix in Web

console.log("🔑 [E2] Auth Type: API_KEY");
console.log("🔑 [E2] API Key:", apiKey);
console.log("✅ API key authentication successful for userId:", userId);
Expand Down
45 changes: 45 additions & 0 deletions app/api/webhooks/send-email/build-ses-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { SendEmailCommand } from "@aws-sdk/client-sesv2";
import type { TenantSendingInfo } from "@/lib/aws-ses/identity-arn-helper";
import { extractEmailAddress } from "@/lib/utils/email-utils";

/**
* Build a SendEmailCommand for SES with tenant-level tracking.
* Shared between handleScheduledEmail and handleBatchEmail.
*/
export function buildSesCommand(params: {
fromAddress: string;
toAddresses: string[];
ccAddresses: string[];
bccAddresses: string[];
rawMessage: string;
tenantSendingInfo: TenantSendingInfo;
}): SendEmailCommand {
return new SendEmailCommand({
FromEmailAddress: params.fromAddress,
...(params.tenantSendingInfo.identityArn && {
FromEmailAddressIdentityArn: params.tenantSendingInfo.identityArn,
}),
Destination: {
ToAddresses: params.toAddresses.map(extractEmailAddress),
CcAddresses:
params.ccAddresses.length > 0
? params.ccAddresses.map(extractEmailAddress)
: undefined,
BccAddresses:
params.bccAddresses.length > 0
? params.bccAddresses.map(extractEmailAddress)
: undefined,
},
Content: {
Raw: {
Data: Buffer.from(params.rawMessage),
},
},
...(params.tenantSendingInfo.configurationSetName && {
ConfigurationSetName: params.tenantSendingInfo.configurationSetName,
}),
...(params.tenantSendingInfo.tenantName && {
TenantName: params.tenantSendingInfo.tenantName,
}),
});
}
73 changes: 73 additions & 0 deletions app/api/webhooks/send-email/resolve-tenant-info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import {
getAgentIdentityArn,
getTenantSendingInfoForDomainOrParent,
type TenantSendingInfo,
} from "@/lib/aws-ses/identity-arn-helper";
import { getRootDomain, isSubdomain } from "@/lib/domains-and-dns/domain-utils";

/**
* Resolve tenant sending info (identity ARN, configuration set, tenant name)
* for a given user/domain/agent combination. Shared between handleScheduledEmail
* and handleBatchEmail.
*/
export async function resolveTenantInfo(
userId: string,
fromDomain: string,
isAgentEmail: boolean,
label: string,
): Promise<TenantSendingInfo> {
let tenantSendingInfo: TenantSendingInfo = {
identityArn: null,
configurationSetName: null,
tenantName: null,
};

if (isAgentEmail) {
tenantSendingInfo = {
identityArn: getAgentIdentityArn(),
configurationSetName: null,
tenantName: null,
};
} else {
const parentDomain = isSubdomain(fromDomain)
? getRootDomain(fromDomain)
: undefined;
tenantSendingInfo = await getTenantSendingInfoForDomainOrParent(
userId,
fromDomain,
parentDomain || undefined,
);
}

if (tenantSendingInfo.identityArn) {
console.log(
`🏢 Using SourceArn for ${label} tenant tracking: ${tenantSendingInfo.identityArn}`,
);
} else {
console.warn(
`⚠️ No SourceArn available - ${label} will not be tracked at tenant level`,
);
}

if (tenantSendingInfo.configurationSetName) {
console.log(
`📋 Using ConfigurationSet for ${label} tenant tracking: ${tenantSendingInfo.configurationSetName}`,
);
} else {
console.warn(
`⚠️ No ConfigurationSet available - ${label} metrics may not be tracked correctly`,
);
}

if (tenantSendingInfo.tenantName) {
console.log(
`🏠 Using TenantName for ${label} AWS SES tracking: ${tenantSendingInfo.tenantName}`,
);
} else {
console.warn(
`⚠️ No TenantName available - ${label} will NOT appear in tenant dashboard!`,
);
}

return tenantSendingInfo;
}
Loading