UNPKG

better-auth

Version:

The most comprehensive authentication library for TypeScript.

298 lines (292 loc) • 9.87 kB
'use strict'; const betterCall = require('better-call'); const socialProviders_index = require('../../shared/better-auth.Bafolo-S.cjs'); require('zod/v4'); const cookies_index = require('../../cookies/index.cjs'); require('../../shared/better-auth.BIMq4RPW.cjs'); require('../../shared/better-auth.DiSjtgs9.cjs'); require('../../shared/better-auth.CXhVNgXP.cjs'); require('defu'); const zod = require('zod'); const url = require('../../shared/better-auth.C-R0J0n1.cjs'); const sha3 = require('@noble/hashes/sha3'); require('../../shared/better-auth.C1hdVENX.cjs'); require('@better-auth/utils/hash'); require('@better-auth/utils/base64'); require('../../crypto/index.cjs'); require('@noble/ciphers/chacha'); require('@noble/ciphers/utils'); require('@noble/ciphers/webcrypto'); require('jose'); require('@noble/hashes/scrypt'); require('@better-auth/utils'); require('@better-auth/utils/hex'); require('@noble/hashes/utils'); require('../../shared/better-auth.CYeOI8C-.cjs'); require('@better-auth/utils/random'); require('@better-fetch/fetch'); require('../../shared/better-auth.D3mtHEZg.cjs'); require('@better-auth/utils/hmac'); require('@better-auth/utils/binary'); require('jose/errors'); require('../../shared/better-auth.ANpbi45u.cjs'); const schema = { walletAddress: { fields: { userId: { type: "string", references: { model: "user", field: "id" }, required: true }, address: { type: "string", required: true }, chainId: { type: "number", required: true }, isPrimary: { type: "boolean", defaultValue: false }, createdAt: { type: "date", required: true } } } }; function toChecksumAddress(address) { address = address.toLowerCase().replace("0x", ""); const hash = [...sha3.keccak_256(address)].map((v) => v.toString(16).padStart(2, "0")).join(""); let ret = "0x"; for (let i = 0; i < 40; i++) { if (parseInt(hash[i], 16) >= 8) { ret += address[i].toUpperCase(); } else { ret += address[i]; } } return ret; } const siwe = (options) => ({ id: "siwe", schema, endpoints: { getSiweNonce: socialProviders_index.createAuthEndpoint( "/siwe/nonce", { method: "POST", body: zod.z.object({ walletAddress: zod.z.string().regex(/^0[xX][a-fA-F0-9]{40}$/i).length(42), chainId: zod.z.number().int().positive().max(2147483647).optional().default(1) // Default to Ethereum mainnet }) }, async (ctx) => { const { walletAddress: rawWalletAddress, chainId } = ctx.body; const walletAddress = toChecksumAddress(rawWalletAddress); const nonce = await options.getNonce(); await ctx.context.internalAdapter.createVerificationValue({ identifier: `siwe:${walletAddress}:${chainId}`, value: nonce, expiresAt: new Date(Date.now() + 15 * 60 * 1e3) }); return ctx.json({ nonce }); } ), verifySiweMessage: socialProviders_index.createAuthEndpoint( "/siwe/verify", { method: "POST", body: zod.z.object({ message: zod.z.string().min(1), signature: zod.z.string().min(1), walletAddress: zod.z.string().regex(/^0[xX][a-fA-F0-9]{40}$/i).length(42), chainId: zod.z.number().int().positive().max(2147483647).optional().default(1), email: zod.z.string().email().optional() }).refine((data) => options.anonymous !== false || !!data.email, { message: "Email is required when the anonymous plugin option is disabled.", path: ["email"] }), requireRequest: true }, async (ctx) => { const { message, signature, walletAddress: rawWalletAddress, chainId, email } = ctx.body; const walletAddress = toChecksumAddress(rawWalletAddress); const isAnon = options.anonymous ?? true; if (!isAnon && !email) { throw new betterCall.APIError("BAD_REQUEST", { message: "Email is required when anonymous is disabled.", status: 400 }); } try { const verification = await ctx.context.internalAdapter.findVerificationValue( `siwe:${walletAddress}:${chainId}` ); if (!verification || /* @__PURE__ */ new Date() > verification.expiresAt) { throw new betterCall.APIError("UNAUTHORIZED", { message: "Unauthorized: Invalid or expired nonce", status: 401, code: "UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE" }); } const { value: nonce } = verification; const verified = await options.verifyMessage({ message, signature, address: walletAddress, chainId, cacao: { h: { t: "caip122" }, p: { domain: options.domain, aud: options.domain, nonce, iss: options.domain, version: "1" }, s: { t: "eip191", s: signature } } }); if (!verified) { throw new betterCall.APIError("UNAUTHORIZED", { message: "Unauthorized: Invalid SIWE signature", status: 401 }); } await ctx.context.internalAdapter.deleteVerificationValue( verification.id ); let user = null; const existingWalletAddress = await ctx.context.adapter.findOne({ model: "walletAddress", where: [ { field: "address", operator: "eq", value: walletAddress }, { field: "chainId", operator: "eq", value: chainId } ] }); if (existingWalletAddress) { user = await ctx.context.adapter.findOne({ model: "user", where: [ { field: "id", operator: "eq", value: existingWalletAddress.userId } ] }); } else { const anyWalletAddress = await ctx.context.adapter.findOne({ model: "walletAddress", where: [ { field: "address", operator: "eq", value: walletAddress } ] }); if (anyWalletAddress) { user = await ctx.context.adapter.findOne({ model: "user", where: [ { field: "id", operator: "eq", value: anyWalletAddress.userId } ] }); } } if (!user) { const domain = options.emailDomainName ?? url.getOrigin(ctx.context.baseURL); const userEmail = !isAnon && email ? email : `${walletAddress}@${domain}`; const { name, avatar } = await options.ensLookup?.({ walletAddress }) ?? {}; user = await ctx.context.internalAdapter.createUser({ name: name ?? walletAddress, email: userEmail, image: avatar ?? "" }); await ctx.context.adapter.create({ model: "walletAddress", data: { userId: user.id, address: walletAddress, chainId, isPrimary: true, // First address is primary createdAt: /* @__PURE__ */ new Date() } }); await ctx.context.internalAdapter.createAccount({ userId: user.id, providerId: "siwe", accountId: `${walletAddress}:${chainId}`, createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }); } else { if (!existingWalletAddress) { await ctx.context.adapter.create({ model: "walletAddress", data: { userId: user.id, address: walletAddress, chainId, isPrimary: false, // Additional addresses are not primary by default createdAt: /* @__PURE__ */ new Date() } }); await ctx.context.internalAdapter.createAccount({ userId: user.id, providerId: "siwe", accountId: `${walletAddress}:${chainId}`, createdAt: /* @__PURE__ */ new Date(), updatedAt: /* @__PURE__ */ new Date() }); } } const session = await ctx.context.internalAdapter.createSession( user.id, ctx ); if (!session) { throw new betterCall.APIError("INTERNAL_SERVER_ERROR", { message: "Internal Server Error", status: 500 }); } await cookies_index.setSessionCookie(ctx, { session, user }); return ctx.json({ token: session.token, success: true, user: { id: user.id, walletAddress, chainId } }); } catch (error) { if (error instanceof betterCall.APIError) throw error; throw new betterCall.APIError("UNAUTHORIZED", { message: "Something went wrong. Please try again later.", error: error instanceof Error ? error.message : "Unknown error", status: 401 }); } } ) } }); exports.siwe = siwe;