From 09a04517eb7d75075dc14fb8b67b997492ebc077 Mon Sep 17 00:00:00 2001 From: Alex Yang Date: Thu, 21 Aug 2025 16:00:56 -0700 Subject: [PATCH] feat: support custom schema merging in SIWE plugin (#4138) --- .../better-auth/src/plugins/siwe/index.ts | 6 +- .../better-auth/src/plugins/siwe/siwe.test.ts | 68 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/better-auth/src/plugins/siwe/index.ts b/packages/better-auth/src/plugins/siwe/index.ts index bc50daab43..9c4c25e3b9 100644 --- a/packages/better-auth/src/plugins/siwe/index.ts +++ b/packages/better-auth/src/plugins/siwe/index.ts @@ -1,7 +1,7 @@ import { APIError, createAuthEndpoint } from "../../api"; import { setSessionCookie } from "../../cookies"; import { z } from "zod"; -import type { BetterAuthPlugin } from "../../types"; +import type { BetterAuthPlugin, InferOptionSchema } from "../../types"; import type { ENSLookupArgs, ENSLookupResult, @@ -12,6 +12,7 @@ import type { User } from "../../types"; import { schema } from "./schema"; import { getOrigin } from "../../utils/url"; import { toChecksumAddress } from "../../utils/hashing"; +import { mergeSchema } from "../../db/schema"; export interface SIWEPluginOptions { domain: string; @@ -20,12 +21,13 @@ export interface SIWEPluginOptions { getNonce: () => Promise; verifyMessage: (args: SIWEVerifyMessageArgs) => Promise; ensLookup?: (args: ENSLookupArgs) => Promise; + schema?: InferOptionSchema; } export const siwe = (options: SIWEPluginOptions) => ({ id: "siwe", - schema, + schema: mergeSchema(schema, options?.schema), endpoints: { getSiweNonce: createAuthEndpoint( "/siwe/nonce", diff --git a/packages/better-auth/src/plugins/siwe/siwe.test.ts b/packages/better-auth/src/plugins/siwe/siwe.test.ts index 96fdf5b4a4..31c129f771 100644 --- a/packages/better-auth/src/plugins/siwe/siwe.test.ts +++ b/packages/better-auth/src/plugins/siwe/siwe.test.ts @@ -606,6 +606,74 @@ describe("siwe", async (it) => { expect(usersWithTestAddress.length).toBe(1); // Only one user should have this address }); + it("should support custom schema with mergeSchema", async () => { + const { client, auth } = await getTestInstance( + { + logger: { + level: "debug", + }, + plugins: [ + siwe({ + domain, + async getNonce() { + return "A1b2C3d4E5f6G7h8J"; + }, + async verifyMessage({ message, signature }) { + return ( + signature === "valid_signature" && message === "valid_message" + ); + }, + schema: { + walletAddress: { + modelName: "wallet_address", + fields: { + userId: "user_id", + address: "wallet_address", + chainId: "chain_id", + isPrimary: "is_primary", + createdAt: "created_at", + }, + }, + }, + }), + ], + }, + { clientOptions: { plugins: [siweClient()] } }, + ); + + const testAddress = "0x000000000000000000000000000000000000dEaD"; + const testChainId = 1; + + // Create account with custom schema + await client.siwe.nonce({ + walletAddress: testAddress, + chainId: testChainId, + }); + const result = await client.siwe.verify({ + message: "valid_message", + signature: "valid_signature", + walletAddress: testAddress, + chainId: testChainId, + }); + expect(result.error).toBeNull(); + expect(result.data?.success).toBe(true); + const context = await auth.$context; + + const walletAddresses: any[] = await context.adapter.findMany({ + model: "walletAddress", + where: [ + { field: "address", operator: "eq", value: testAddress }, + { field: "chainId", operator: "eq", value: testChainId }, + ], + }); + expect(walletAddresses.length).toBe(1); + expect(walletAddresses[0]?.address).toBe(testAddress); + expect(walletAddresses[0]?.chainId).toBe(testChainId); + expect(walletAddresses[0]?.isPrimary).toBe(true); + expect(walletAddresses[0]?.userId).toBeDefined(); + expect(walletAddresses[0]?.createdAt).toBeDefined(); + }); + it("should allow same address on different chains for same user", async () => { const { client, auth } = await getTestInstance( {