better-auth
Version:
The most comprehensive authentication library for TypeScript.
298 lines (292 loc) • 9.87 kB
JavaScript
'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;