UNPKG

better-auth

Version:

The most comprehensive authentication framework for TypeScript.

1 lines • 14.6 kB
{"version":3,"file":"index.mjs","names":["user: User | null","existingWalletAddress: WalletAddress | null","anyWalletAddress: WalletAddress | null","error: unknown"],"sources":["../../../src/plugins/siwe/index.ts"],"sourcesContent":["import type { BetterAuthPlugin } from \"@better-auth/core\";\nimport { createAuthEndpoint } from \"@better-auth/core/api\";\nimport * as z from \"zod\";\nimport { APIError } from \"../../api\";\nimport { setSessionCookie } from \"../../cookies\";\nimport { mergeSchema } from \"../../db/schema\";\nimport type { InferOptionSchema, User } from \"../../types\";\nimport { toChecksumAddress } from \"../../utils/hashing\";\nimport { getOrigin } from \"../../utils/url\";\nimport { schema } from \"./schema\";\nimport type {\n\tENSLookupArgs,\n\tENSLookupResult,\n\tSIWEVerifyMessageArgs,\n\tWalletAddress,\n} from \"./types\";\n\nexport interface SIWEPluginOptions {\n\tdomain: string;\n\temailDomainName?: string | undefined;\n\tanonymous?: boolean | undefined;\n\tgetNonce: () => Promise<string>;\n\tverifyMessage: (args: SIWEVerifyMessageArgs) => Promise<boolean>;\n\tensLookup?: ((args: ENSLookupArgs) => Promise<ENSLookupResult>) | undefined;\n\tschema?: InferOptionSchema<typeof schema> | undefined;\n}\n\nconst getSiweNonceBodySchema = z.object({\n\twalletAddress: z\n\t\t.string()\n\t\t.regex(/^0[xX][a-fA-F0-9]{40}$/i)\n\t\t.length(42),\n\tchainId: z.number().int().positive().max(2147483647).optional().default(1),\n});\n\nexport const siwe = (options: SIWEPluginOptions) =>\n\t({\n\t\tid: \"siwe\",\n\t\tschema: mergeSchema(schema, options?.schema),\n\t\tendpoints: {\n\t\t\tgetSiweNonce: createAuthEndpoint(\n\t\t\t\t\"/siwe/nonce\",\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: getSiweNonceBodySchema,\n\t\t\t\t},\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst { walletAddress: rawWalletAddress, chainId } = ctx.body;\n\t\t\t\t\tconst walletAddress = toChecksumAddress(rawWalletAddress);\n\t\t\t\t\tconst nonce = await options.getNonce();\n\n\t\t\t\t\t// Store nonce with wallet address and chain ID context\n\t\t\t\t\tawait ctx.context.internalAdapter.createVerificationValue({\n\t\t\t\t\t\tidentifier: `siwe:${walletAddress}:${chainId}`,\n\t\t\t\t\t\tvalue: nonce,\n\t\t\t\t\t\texpiresAt: new Date(Date.now() + 15 * 60 * 1000),\n\t\t\t\t\t});\n\n\t\t\t\t\treturn ctx.json({ nonce });\n\t\t\t\t},\n\t\t\t),\n\t\t\tverifySiweMessage: createAuthEndpoint(\n\t\t\t\t\"/siwe/verify\",\n\t\t\t\t{\n\t\t\t\t\tmethod: \"POST\",\n\t\t\t\t\tbody: z\n\t\t\t\t\t\t.object({\n\t\t\t\t\t\t\tmessage: z.string().min(1),\n\t\t\t\t\t\t\tsignature: z.string().min(1),\n\t\t\t\t\t\t\twalletAddress: z\n\t\t\t\t\t\t\t\t.string()\n\t\t\t\t\t\t\t\t.regex(/^0[xX][a-fA-F0-9]{40}$/i)\n\t\t\t\t\t\t\t\t.length(42),\n\t\t\t\t\t\t\tchainId: z\n\t\t\t\t\t\t\t\t.number()\n\t\t\t\t\t\t\t\t.int()\n\t\t\t\t\t\t\t\t.positive()\n\t\t\t\t\t\t\t\t.max(2147483647)\n\t\t\t\t\t\t\t\t.optional()\n\t\t\t\t\t\t\t\t.default(1),\n\t\t\t\t\t\t\temail: z.email().optional(),\n\t\t\t\t\t\t})\n\t\t\t\t\t\t.refine((data) => options.anonymous !== false || !!data.email, {\n\t\t\t\t\t\t\tmessage:\n\t\t\t\t\t\t\t\t\"Email is required when the anonymous plugin option is disabled.\",\n\t\t\t\t\t\t\tpath: [\"email\"],\n\t\t\t\t\t\t}),\n\t\t\t\t\trequireRequest: true,\n\t\t\t\t},\n\t\t\t\tasync (ctx) => {\n\t\t\t\t\tconst {\n\t\t\t\t\t\tmessage,\n\t\t\t\t\t\tsignature,\n\t\t\t\t\t\twalletAddress: rawWalletAddress,\n\t\t\t\t\t\tchainId,\n\t\t\t\t\t\temail,\n\t\t\t\t\t} = ctx.body;\n\t\t\t\t\tconst walletAddress = toChecksumAddress(rawWalletAddress);\n\t\t\t\t\tconst isAnon = options.anonymous ?? true;\n\n\t\t\t\t\tif (!isAnon && !email) {\n\t\t\t\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\t\t\t\tmessage: \"Email is required when anonymous is disabled.\",\n\t\t\t\t\t\t\tstatus: 400,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\t// Find stored nonce with wallet address and chain ID context\n\t\t\t\t\t\tconst verification =\n\t\t\t\t\t\t\tawait ctx.context.internalAdapter.findVerificationValue(\n\t\t\t\t\t\t\t\t`siwe:${walletAddress}:${chainId}`,\n\t\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// Ensure nonce is valid and not expired\n\t\t\t\t\t\tif (!verification || new Date() > verification.expiresAt) {\n\t\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\t\tmessage: \"Unauthorized: Invalid or expired nonce\",\n\t\t\t\t\t\t\t\tstatus: 401,\n\t\t\t\t\t\t\t\tcode: \"UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE\",\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Verify SIWE message with enhanced parameters\n\t\t\t\t\t\tconst { value: nonce } = verification;\n\t\t\t\t\t\tconst verified = await options.verifyMessage({\n\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\tsignature,\n\t\t\t\t\t\t\taddress: walletAddress,\n\t\t\t\t\t\t\tchainId,\n\t\t\t\t\t\t\tcacao: {\n\t\t\t\t\t\t\t\th: { t: \"caip122\" },\n\t\t\t\t\t\t\t\tp: {\n\t\t\t\t\t\t\t\t\tdomain: options.domain,\n\t\t\t\t\t\t\t\t\taud: options.domain,\n\t\t\t\t\t\t\t\t\tnonce,\n\t\t\t\t\t\t\t\t\tiss: options.domain,\n\t\t\t\t\t\t\t\t\tversion: \"1\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\ts: { t: \"eip191\", s: signature },\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\tif (!verified) {\n\t\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\t\tmessage: \"Unauthorized: Invalid SIWE signature\",\n\t\t\t\t\t\t\t\tstatus: 401,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Clean up used nonce\n\t\t\t\t\t\tawait ctx.context.internalAdapter.deleteVerificationValue(\n\t\t\t\t\t\t\tverification.id,\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\t// Look for existing user by their wallet addresses\n\t\t\t\t\t\tlet user: User | null = null;\n\n\t\t\t\t\t\t// Check if there's a wallet address record for this exact address+chainId combination\n\t\t\t\t\t\tconst existingWalletAddress: WalletAddress | null =\n\t\t\t\t\t\t\tawait ctx.context.adapter.findOne({\n\t\t\t\t\t\t\t\tmodel: \"walletAddress\",\n\t\t\t\t\t\t\t\twhere: [\n\t\t\t\t\t\t\t\t\t{ field: \"address\", operator: \"eq\", value: walletAddress },\n\t\t\t\t\t\t\t\t\t{ field: \"chainId\", operator: \"eq\", value: chainId },\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\tif (existingWalletAddress) {\n\t\t\t\t\t\t\t// Get the user associated with this wallet address\n\t\t\t\t\t\t\tuser = await ctx.context.adapter.findOne({\n\t\t\t\t\t\t\t\tmodel: \"user\",\n\t\t\t\t\t\t\t\twhere: [\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tfield: \"id\",\n\t\t\t\t\t\t\t\t\t\toperator: \"eq\",\n\t\t\t\t\t\t\t\t\t\tvalue: existingWalletAddress.userId,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// No exact match found, check if this address exists on any other chain\n\t\t\t\t\t\t\tconst anyWalletAddress: WalletAddress | null =\n\t\t\t\t\t\t\t\tawait ctx.context.adapter.findOne({\n\t\t\t\t\t\t\t\t\tmodel: \"walletAddress\",\n\t\t\t\t\t\t\t\t\twhere: [\n\t\t\t\t\t\t\t\t\t\t{ field: \"address\", operator: \"eq\", value: walletAddress },\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\tif (anyWalletAddress) {\n\t\t\t\t\t\t\t\t// Same address exists on different chain, get that user\n\t\t\t\t\t\t\t\tuser = await ctx.context.adapter.findOne({\n\t\t\t\t\t\t\t\t\tmodel: \"user\",\n\t\t\t\t\t\t\t\t\twhere: [\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tfield: \"id\",\n\t\t\t\t\t\t\t\t\t\t\toperator: \"eq\",\n\t\t\t\t\t\t\t\t\t\t\tvalue: anyWalletAddress.userId,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// Create new user if none exists\n\t\t\t\t\t\tif (!user) {\n\t\t\t\t\t\t\tconst domain =\n\t\t\t\t\t\t\t\toptions.emailDomainName ?? getOrigin(ctx.context.baseURL);\n\t\t\t\t\t\t\t// Use checksummed address for email generation\n\t\t\t\t\t\t\tconst userEmail =\n\t\t\t\t\t\t\t\t!isAnon && email ? email : `${walletAddress}@${domain}`;\n\t\t\t\t\t\t\tconst { name, avatar } =\n\t\t\t\t\t\t\t\t(await options.ensLookup?.({ walletAddress })) ?? {};\n\n\t\t\t\t\t\t\tuser = await ctx.context.internalAdapter.createUser({\n\t\t\t\t\t\t\t\tname: name ?? walletAddress,\n\t\t\t\t\t\t\t\temail: userEmail,\n\t\t\t\t\t\t\t\timage: avatar ?? \"\",\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t// Create wallet address record\n\t\t\t\t\t\t\tawait ctx.context.adapter.create({\n\t\t\t\t\t\t\t\tmodel: \"walletAddress\",\n\t\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\t\t\t\taddress: walletAddress,\n\t\t\t\t\t\t\t\t\tchainId,\n\t\t\t\t\t\t\t\t\tisPrimary: true, // First address is primary\n\t\t\t\t\t\t\t\t\tcreatedAt: new Date(),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t// Create account record for wallet authentication\n\t\t\t\t\t\t\tawait ctx.context.internalAdapter.createAccount({\n\t\t\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\t\t\tproviderId: \"siwe\",\n\t\t\t\t\t\t\t\taccountId: `${walletAddress}:${chainId}`,\n\t\t\t\t\t\t\t\tcreatedAt: new Date(),\n\t\t\t\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// User exists, but check if this specific address/chain combo exists\n\t\t\t\t\t\t\tif (!existingWalletAddress) {\n\t\t\t\t\t\t\t\t// Add this new chainId to existing user's addresses\n\t\t\t\t\t\t\t\tawait ctx.context.adapter.create({\n\t\t\t\t\t\t\t\t\tmodel: \"walletAddress\",\n\t\t\t\t\t\t\t\t\tdata: {\n\t\t\t\t\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\t\t\t\t\taddress: walletAddress,\n\t\t\t\t\t\t\t\t\t\tchainId,\n\t\t\t\t\t\t\t\t\t\tisPrimary: false, // Additional addresses are not primary by default\n\t\t\t\t\t\t\t\t\t\tcreatedAt: new Date(),\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t\t// Create account record for this new wallet+chain combination\n\t\t\t\t\t\t\t\tawait ctx.context.internalAdapter.createAccount({\n\t\t\t\t\t\t\t\t\tuserId: user.id,\n\t\t\t\t\t\t\t\t\tproviderId: \"siwe\",\n\t\t\t\t\t\t\t\t\taccountId: `${walletAddress}:${chainId}`,\n\t\t\t\t\t\t\t\t\tcreatedAt: new Date(),\n\t\t\t\t\t\t\t\t\tupdatedAt: new Date(),\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tconst session = await ctx.context.internalAdapter.createSession(\n\t\t\t\t\t\t\tuser.id,\n\t\t\t\t\t\t);\n\n\t\t\t\t\t\tif (!session) {\n\t\t\t\t\t\t\tthrow new APIError(\"INTERNAL_SERVER_ERROR\", {\n\t\t\t\t\t\t\t\tmessage: \"Internal Server Error\",\n\t\t\t\t\t\t\t\tstatus: 500,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tawait setSessionCookie(ctx, { session, user });\n\n\t\t\t\t\t\treturn ctx.json({\n\t\t\t\t\t\t\ttoken: session.token,\n\t\t\t\t\t\t\tsuccess: true,\n\t\t\t\t\t\t\tuser: {\n\t\t\t\t\t\t\t\tid: user.id,\n\t\t\t\t\t\t\t\twalletAddress,\n\t\t\t\t\t\t\t\tchainId,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\t\tif (error instanceof APIError) throw error;\n\t\t\t\t\t\tthrow new APIError(\"UNAUTHORIZED\", {\n\t\t\t\t\t\t\tmessage: \"Something went wrong. Please try again later.\",\n\t\t\t\t\t\t\terror: error instanceof Error ? error.message : \"Unknown error\",\n\t\t\t\t\t\t\tstatus: 401,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t),\n\t\t},\n\t\toptions,\n\t}) satisfies BetterAuthPlugin;\n"],"mappings":"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"}