From 947945bb765c7e5732d051df8aedac7dcd9837e8 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Sat, 31 Oct 2026 12:06:32 -0400 Subject: [PATCH 1/2] feat(core): carry email headers from @seamless-auth/types EmailMessage gains the optional headers record from types 0.34.0, and core hands an override's headers to the email transport untouched. The enrollment invite input now comes from types too, and express exports it. --- .changeset/email-message-headers.md | 9 +++++++ packages/core/src/authMessaging.ts | 9 ++----- .../core/tests/deliverAuthMessage.test.js | 24 +++++++++++++++++++ packages/express/src/index.ts | 1 + packages/express/tests/types/consumer.ts | 11 +++++++++ 5 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 .changeset/email-message-headers.md diff --git a/.changeset/email-message-headers.md b/.changeset/email-message-headers.md new file mode 100644 index 0000000..7396922 --- /dev/null +++ b/.changeset/email-message-headers.md @@ -0,0 +1,9 @@ +--- +"@seamless-auth/core": minor +"@seamless-auth/express": minor +"@seamless-auth/nextjs": patch +--- + +`EmailMessage` carries an optional `headers` record, taken from `@seamless-auth/types` 0.34.0, so an override or a custom transport can send headers such as `List-Unsubscribe` and `List-Unsubscribe-Post`. Core passes an override's headers to the email transport untouched; `@seamless-auth/messaging-aws` sends them through SES v2 and validates the names and values. + +`SendEnrollmentInviteEmailInput` now comes from `@seamless-auth/types` rather than being declared in core, and `@seamless-auth/express` exports it alongside the other messaging types. diff --git a/packages/core/src/authMessaging.ts b/packages/core/src/authMessaging.ts index a3a101f..4b36606 100644 --- a/packages/core/src/authMessaging.ts +++ b/packages/core/src/authMessaging.ts @@ -18,6 +18,7 @@ export type { DeliveryResult, EmailMessage, MessagingChannel, + SendEnrollmentInviteEmailInput, SendMagicLinkEmailInput, SendOtpEmailInput, SendOtpSmsInput, @@ -30,19 +31,13 @@ import type { DeliveryPurpose, DeliveryResult, EmailMessage, + SendEnrollmentInviteEmailInput, SendMagicLinkEmailInput, SendOtpEmailInput, SendOtpSmsInput, SmsMessage, } from "@seamless-auth/types"; -export interface SendEnrollmentInviteEmailInput { - to: string; - signInUrl: string; - from?: string; - subject?: string; -} - /** * An owner notice the auth API asked this server to send, such as "account recovery * was requested". `text` is ready to send. `cancelUrl` carries a credential: send it diff --git a/packages/core/tests/deliverAuthMessage.test.js b/packages/core/tests/deliverAuthMessage.test.js index 810a87d..7290dac 100644 --- a/packages/core/tests/deliverAuthMessage.test.js +++ b/packages/core/tests/deliverAuthMessage.test.js @@ -84,6 +84,30 @@ describe("deliverAuthMessage", () => { expect(email.send.mock.calls[0][0].subject).toBe("Code for user@acme.test"); }); + it("hands an override's headers to the transport untouched", async () => { + const email = emailTransport(); + const headers = { + "List-Unsubscribe": "", + "List-Unsubscribe-Post": "List-Unsubscribe=One-Click", + }; + + await deliverAuthMessage( + { + email, + overrides: { + magicLinkEmail: (_input, defaults) => ({ ...defaults, headers }), + }, + }, + { + kind: "magic_link_email", + to: "user@acme.test", + magicLinkUrl: "https://acme.test/link", + }, + ); + + expect(email.send.mock.calls[0][0].headers).toEqual(headers); + }); + it("routes an SMS instruction to the SMS transport", async () => { const sms = smsTransport(); diff --git a/packages/express/src/index.ts b/packages/express/src/index.ts index 078cd5a..a125025 100644 --- a/packages/express/src/index.ts +++ b/packages/express/src/index.ts @@ -26,6 +26,7 @@ export type { ResourceServerOptions, ResourceServerUser, SeamlessAuthMessagingOptions, + SendEnrollmentInviteEmailInput, StepUpRequirement, SeamlessUser, SmsMessage, diff --git a/packages/express/tests/types/consumer.ts b/packages/express/tests/types/consumer.ts index c3dd967..3458337 100644 --- a/packages/express/tests/types/consumer.ts +++ b/packages/express/tests/types/consumer.ts @@ -6,6 +6,7 @@ import { getSeamlessUser, requireAuth, requireRole, + type EmailMessage, } from "@seamless-auth/express"; const app = express(); @@ -33,3 +34,13 @@ requireRole(42); createSeamlessAuthServer(42); // @ts-expect-error getSeamlessUser takes a request and options getSeamlessUser(42); + +const unsubscribable: EmailMessage = { + to: "user@example.com", + subject: "News", + text: "Hello", + headers: { "List-Unsubscribe": "" }, +}; +// @ts-expect-error header values are strings +const badHeader: EmailMessage = { ...unsubscribable, headers: { "X-Count": 1 } }; +void badHeader; From 5ca256b86b2d96122b74904b104a095df81aa853 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Sat, 31 Oct 2026 12:07:20 -0400 Subject: [PATCH 2/2] feat(fastify,nextjs): export the enrollment invite input --- .changeset/email-message-headers.md | 5 +++-- packages/fastify/src/index.ts | 1 + packages/nextjs/src/index.ts | 1 + 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.changeset/email-message-headers.md b/.changeset/email-message-headers.md index 7396922..b99ccce 100644 --- a/.changeset/email-message-headers.md +++ b/.changeset/email-message-headers.md @@ -1,9 +1,10 @@ --- "@seamless-auth/core": minor "@seamless-auth/express": minor -"@seamless-auth/nextjs": patch +"@seamless-auth/fastify": minor +"@seamless-auth/nextjs": minor --- `EmailMessage` carries an optional `headers` record, taken from `@seamless-auth/types` 0.34.0, so an override or a custom transport can send headers such as `List-Unsubscribe` and `List-Unsubscribe-Post`. Core passes an override's headers to the email transport untouched; `@seamless-auth/messaging-aws` sends them through SES v2 and validates the names and values. -`SendEnrollmentInviteEmailInput` now comes from `@seamless-auth/types` rather than being declared in core, and `@seamless-auth/express` exports it alongside the other messaging types. +`SendEnrollmentInviteEmailInput` now comes from `@seamless-auth/types` rather than being declared in core, and the Express, Fastify and Next.js adapters export it alongside the other messaging types. diff --git a/packages/fastify/src/index.ts b/packages/fastify/src/index.ts index 4c897ec..d98f0d6 100644 --- a/packages/fastify/src/index.ts +++ b/packages/fastify/src/index.ts @@ -24,6 +24,7 @@ export type { ResourceServerOptions, ResourceServerUser, SeamlessAuthMessagingOptions, + SendEnrollmentInviteEmailInput, SeamlessAuthUser, SeamlessUser, SmsMessage, diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 585abb1..24aef51 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -37,6 +37,7 @@ export type { ResourceServerOptions, ResourceServerUser, SeamlessAuthMessagingOptions, + SendEnrollmentInviteEmailInput, SeamlessAuthUser, SeamlessUser, SmsMessage,