-
Notifications
You must be signed in to change notification settings - Fork 404
Brand transactional auth emails per app #2439
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
75e69fe
1c36599
4fa7577
7eb25a2
7039562
afcbf26
0f7d24a
e4dc584
241c8af
9b8e98b
7c4356e
785c01d
c2ec3a1
6f69e4d
76517e3
99d071b
4ca1534
7d80aae
e03d95b
703aebb
9771881
f82f39f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| --- | ||
| "@agent-native/core": patch | ||
| --- | ||
|
|
||
| Brand transactional auth emails per app. Signup verification and password | ||
| reset emails now send from `<app-slug>@agent-native.com` with reply-to | ||
| hello@agent-native.com and per-app subjects/headings ("Verify your email for | ||
| Agent-Native <App>" / "Reset your Agent-Native <App> password"). The | ||
| verification email body also includes the app's one-line description (competitor | ||
| names reframed as "replacement"); the reset email omits the pitch since it's a | ||
| security email. Unknown apps fall back to the generic "Agent Native" branding. | ||
|
|
||
| The branded sender and reply-to are applied only when the configured | ||
| EMAIL_FROM is already on agent-native.com, so self-hosted deployments keep | ||
| their own verified sender and support mailbox. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,14 @@ export interface SendEmailArgs { | |
| from?: string; | ||
| cc?: string | string[]; | ||
| replyTo?: string; | ||
| /** | ||
| * Per-app branding for first-party agent-native.com deployments. Applied | ||
| * only when the configured EMAIL_FROM is already on agent-native.com, so a | ||
| * self-hosted deployment keeps its own verified sender and support mailbox | ||
| * instead of sending as an unverified address a provider would reject. | ||
| * An explicit `from` / `replyTo` always wins. | ||
| */ | ||
| appSender?: { name: string; slug: string; replyTo?: string }; | ||
| inReplyTo?: string; | ||
| references?: string; | ||
| attachments?: EmailAttachment[]; | ||
|
|
@@ -123,14 +131,37 @@ function getFromAddress( | |
| return "Agent Native <onboarding@resend.dev>"; | ||
| } | ||
|
|
||
| const AGENT_NATIVE_SENDER_DOMAIN = "agent-native.com"; | ||
|
|
||
| /** | ||
| * Resolve per-app sender branding, but only for deployments whose configured | ||
| * sender is already on agent-native.com. Any other (or missing) EMAIL_FROM | ||
| * means we cannot prove the branded address is a verified sender, so the | ||
| * deployment's own configuration is left untouched. | ||
| */ | ||
| function resolveAppSender( | ||
| configuredFrom: string | undefined, | ||
| appSender: SendEmailArgs["appSender"], | ||
| ): { from: string; replyTo?: string } | undefined { | ||
| if (!configuredFrom || !appSender) return undefined; | ||
| const address = parseSendGridFrom(configuredFrom).email.toLowerCase(); | ||
| if (!address.endsWith(`@${AGENT_NATIVE_SENDER_DOMAIN}`)) return undefined; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Recognize the current first-party sender configuration
Additional Info
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not changing this — the premise does not hold up, though the failure mode you describe is a real one worth stating explicitly.
On the residual risk: you are right that if some deployment sets If someone confirms the production
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @steve8708 asked me to settle this rather than repeat myself, so here is a split verdict: I partly agree, and I fixed the part I agree with in What I still reject: widening the domain gate. The stated premise is that the first-party sender is There is also positive evidence the gate is already correct: this workspace has I also considered the obvious alternative of keeping the configured domain and swapping only the local-part ( What I agree with: the suppression should not be silent. Strip away the incorrect premise and there is a real defect underneath. When the gate does not match, branding vanishes with no signal at all — an operator sees generic senders and has nothing pointing at why. That is the failure mode Fixed by making it observable instead of invisible. Warn-once so a high-volume signup flow cannot spam logs, and it fires for a missing If someone confirms a real deployment sets |
||
| return { | ||
| from: `${appSender.name} <${appSender.slug}@${AGENT_NATIVE_SENDER_DOMAIN}>`, | ||
|
builder-io-integration[bot] marked this conversation as resolved.
Outdated
|
||
| replyTo: appSender.replyTo, | ||
| }; | ||
| } | ||
|
|
||
| async function sendEmailWithSignal( | ||
| args: SendEmailArgs, | ||
| signal?: AbortSignal, | ||
| ): Promise<void> { | ||
| const config = await resolveEmailTransport(); | ||
| signal?.throwIfAborted(); | ||
| const provider = config.provider; | ||
| const from = getFromAddress(config, args.from); | ||
| const branded = resolveAppSender(config.from, args.appSender); | ||
| const from = getFromAddress(config, args.from ?? branded?.from); | ||
| const replyTo = args.replyTo ?? branded?.replyTo; | ||
| const attachments = resolveAttachments(args); | ||
|
|
||
| if (provider === "resend") { | ||
|
|
@@ -142,7 +173,7 @@ async function sendEmailWithSignal( | |
| text: args.text, | ||
| }; | ||
| if (args.cc) payload.cc = Array.isArray(args.cc) ? args.cc : [args.cc]; | ||
| if (args.replyTo) payload.reply_to = args.replyTo; | ||
| if (replyTo) payload.reply_to = replyTo; | ||
| if (attachments?.length) { | ||
| payload.attachments = attachments.map((a) => ({ | ||
| filename: a.filename, | ||
|
|
@@ -193,7 +224,7 @@ async function sendEmailWithSignal( | |
| { type: "text/html", value: args.html }, | ||
| ], | ||
| }; | ||
| if (args.replyTo) sgPayload.reply_to = parseSendGridFrom(args.replyTo); | ||
| if (replyTo) sgPayload.reply_to = parseSendGridFrom(replyTo); | ||
| const sgHeaders: Record<string, string> = {}; | ||
| if (args.inReplyTo) sgHeaders["In-Reply-To"] = args.inReplyTo; | ||
| if (args.references) sgHeaders["References"] = args.references; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.