feat(magic-link): add request metadata to sendMagicLink

This commit is contained in:
Andreas Osberghaus
2026-03-12 15:39:01 +01:00
parent e897d5911a
commit 2062383f02
4 changed files with 64 additions and 2 deletions

View File

@@ -20,7 +20,7 @@ Magic link or email link is a way to authenticate users without a password. When
export const auth = betterAuth({
plugins: [
magicLink({ // [!code highlight]
sendMagicLink: async ({ email, token, url }, ctx) => { // [!code highlight]
sendMagicLink: async ({ email, token, url, metadata }, ctx) => { // [!code highlight]
// send email to user // [!code highlight]
} // [!code highlight]
}) // [!code highlight]
@@ -85,6 +85,10 @@ type signInMagicLink = {
* redirected to the callbackURL with an `error` query parameter.
*/
errorCallbackURL?: string = "/error"
/**
* Additional metadata forwarded to the sendMagicLink callback.
*/
metadata?: Record<string, any> = { inviteId: "123" }
}
```
</APIMethod>
@@ -130,6 +134,7 @@ type magicLinkVerify = {
- `email`: The email address of the user.
- `url`: The URL to be sent to the user. This URL contains the token.
- `token`: The token if you want to send the token with custom URL.
- `metadata`: Additional request metadata passed from `signIn.magicLink`.
and a `ctx` context object as the second parameter.

View File

@@ -12,6 +12,7 @@ import {
deviceAuthorizationClient,
emailOTPClient,
genericOAuthClient,
magicLinkClient,
multiSessionClient,
oidcClient,
organizationClient,
@@ -270,6 +271,36 @@ describe("type", () => {
expectTypeOf(client.test.signOut).toEqualTypeOf<() => Promise<void>>();
});
it("should infer magic link metadata in sign-in request", () => {
const client = createReactClient({
plugins: [magicLinkClient()],
});
type SignInMagicLinkInput = NonNullable<
Parameters<typeof client.signIn.magicLink>[0]
>;
expectTypeOf<SignInMagicLinkInput>().toMatchTypeOf<{
email: string;
name?: string | undefined;
callbackURL?: string | undefined;
newUserCallbackURL?: string | undefined;
errorCallbackURL?: string | undefined;
metadata?: Record<string, any> | undefined;
}>();
const request: SignInMagicLinkInput = {
email: "test@email.com",
metadata: {
inviteId: "123",
},
};
expectTypeOf(request.metadata).toEqualTypeOf<
Record<string, any> | undefined
>();
});
it("should infer session", () => {
const client = createSolidClient({
plugins: [testClientPlugin(), testClientPlugin2(), twoFactorClient()],

View File

@@ -39,6 +39,7 @@ export interface MagicLinkOptions {
email: string;
url: string;
token: string;
metadata?: Record<string, any>;
},
ctx?: GenericEndpointContext | undefined,
) => Awaitable<void>;
@@ -112,6 +113,12 @@ const signInMagicLinkBodySchema = z.object({
description: "URL to redirect after error.",
})
.optional(),
metadata: z
.record(z.string(), z.any())
.meta({
description: "Additional metadata to pass to sendMagicLink.",
})
.optional(),
});
const magicLinkVerifyQuerySchema = z.object({
token: z.string().meta({
@@ -208,7 +215,7 @@ export const magicLink = (options: MagicLinkOptions) => {
},
},
async (ctx) => {
const { email } = ctx.body;
const { email, metadata } = ctx.body;
const verificationToken = opts?.generateToken
? await opts.generateToken(email)
@@ -243,6 +250,7 @@ export const magicLink = (options: MagicLinkOptions) => {
email,
url: url.toString(),
token: verificationToken,
metadata,
},
ctx,
);

View File

@@ -9,6 +9,7 @@ type VerificationEmail = {
email: string;
token: string;
url: string;
metadata?: Record<string, any>;
};
describe("magic link", async () => {
@@ -50,6 +51,23 @@ describe("magic link", async () => {
"http://localhost:3000/api/auth/magic-link/verify",
),
});
expect(verificationEmail.metadata).toBeUndefined();
});
it("should forward metadata to sendMagicLink", async () => {
await client.signIn.magicLink({
email: testUser.email,
metadata: {
inviteId: "123",
},
});
expect(verificationEmail).toMatchObject({
email: testUser.email,
metadata: {
inviteId: "123",
},
});
});
it("should verify magic link", async () => {
const headers = new Headers();