UNPKG

better-near-auth

Version:

Sign in with NEAR (SIWN) plugin for Better Auth

377 lines 15.5 kB
import { Near, fromNearConnect, generateNonce, TransactionBuilder } from "near-kit"; import { NearConnector } from "@hot-labs/near-connect"; import { hex } from "@scure/base"; import { atom } from "nanostores"; import {} from "./types.js"; export const siwnClient = (config) => { const nearState = atom(null); const walletConnected = atom(false); const network = config.networkId || "mainnet"; let connector = null; let near = null; let clientInitialized = false; const handleAccountConnection = async (accountId, publicKey) => { if (!accountId) return; nearState.set({ accountId, publicKey: publicKey || null, networkId: network, }); walletConnected.set(true); }; const initClient = ($fetch) => { if (clientInitialized) return true; if (typeof globalThis.window === "undefined") return false; connector = new NearConnector({ network }); near = new Near({ network, wallet: fromNearConnect(connector), }); connector.on("wallet:signIn", async (data) => { const accountId = data.accounts?.[0]?.accountId; const publicKey = data.accounts?.[0]?.publicKey; if (accountId) { await handleAccountConnection(accountId, publicKey); } }); connector.on("wallet:signOut", () => { walletConnected.set(false); const state = nearState.get(); if (state) { nearState.set({ accountId: state.accountId, publicKey: null, networkId: state.networkId }); } }); connector.getConnectedWallet().then(({ accounts }) => { const account = accounts?.[0]; if (account?.accountId && !nearState.get()) { nearState.set({ accountId: account.accountId, publicKey: account.publicKey ?? null, networkId: network, }); } if (account?.accountId) { walletConnected.set(true); } }).catch(() => { }); if ($fetch) { restoreFromSession($fetch); } clientInitialized = true; return true; }; let sessionRestored = false; const restoreFromSession = async ($fetch) => { if (sessionRestored) return; const state = nearState.get(); if (state?.accountId) { sessionRestored = true; return; } try { const res = await $fetch("/near/list-accounts", { method: "GET" }); const accounts = res.data?.accounts; if (accounts?.length) { const primary = accounts.find((a) => a.isPrimary) || accounts[0]; if (primary) { nearState.set({ accountId: primary.accountId, publicKey: primary.publicKey ?? null, networkId: primary.network, }); } } } catch { } sessionRestored = true; }; const requireConnector = () => { if (!connector) throw new Error("Wallet not initialized — this operation requires a browser environment"); return connector; }; const requireNear = () => { if (!near) throw new Error("Wallet not initialized — this operation requires a browser environment"); return near; }; const ensureWalletConnected = async () => { const conn = requireConnector(); if (walletConnected.get()) { try { const { accounts } = await conn.getConnectedWallet(); if (accounts?.length) return true; } catch { } } return new Promise((resolve) => { const signInHandler = (data) => { const accountId = data.accounts?.[0]?.accountId; const publicKey = data.accounts?.[0]?.publicKey; if (accountId) { handleAccountConnection(accountId, publicKey); resolve(true); } }; conn.on("wallet:signIn", signInHandler); conn.connect().catch(() => { }).finally(() => { conn.off("wallet:signIn", signInHandler); if (!walletConnected.get()) { resolve(false); } }); }); }; const signWithWallet = async () => { const conn = requireConnector(); const nearClient = requireNear(); const nonceBytes = generateNonce(); const nonceHex = hex.encode(nonceBytes); const message = `Sign in to ${config.recipient}`; let connectedWallet = null; try { connectedWallet = await conn.getConnectedWallet(); } catch { } if (connectedWallet?.accounts?.length) { const signedMessage = await nearClient.signMessage({ message, recipient: config.recipient, nonce: nonceBytes, }); if (!signedMessage?.accountId) { throw new Error("Wallet sign-in was cancelled or failed"); } return { signedMessage, accountId: signedMessage.accountId, publicKey: signedMessage.publicKey, nonceHex, }; } const result = { value: null }; const handler = (data) => { const account = data.accounts?.[0]; if (account?.signedMessage) { result.value = { signedMessage: account.signedMessage, accountId: account.accountId, publicKey: account.signedMessage.publicKey, }; } }; conn.on("wallet:signInAndSignMessage", handler); try { await conn.connect({ signMessageParams: { message, recipient: config.recipient, nonce: nonceBytes, }, }); } finally { conn.off("wallet:signInAndSignMessage", handler); } if (!result.value) { throw new Error("Wallet sign-in was cancelled or failed"); } return { signedMessage: result.value.signedMessage, accountId: result.value.accountId, publicKey: result.value.publicKey, nonceHex, }; }; const buildSignedDelegateActionInternal = async (receiverId, buildActions) => { const state = nearState.get(); if (!state?.accountId) { throw new Error("No NEAR account found — please sign in with your NEAR wallet"); } if (!walletConnected.get()) { const reconnected = await ensureWalletConnected(); if (!reconnected) { throw new Error("Wallet connection required — please approve the connection to sign"); } } const nearClient = requireNear(); const builder = buildActions(nearClient.transaction(state.accountId), receiverId); const { payload } = await builder.delegate(); return payload; }; return { id: "siwn", $InferServerPlugin: {}, getAtoms: (_$fetch) => ({ nearState, walletConnected, }), getActions: ($fetch, _$store, _options) => { initClient($fetch); return { near: { nonce: async (params, fetchOptions) => { return await $fetch("/near/nonce", { method: "POST", body: params, ...fetchOptions }); }, verify: async (params, fetchOptions) => { return await $fetch("/near/verify", { method: "POST", body: params, ...fetchOptions }); }, getProfile: async (accountId, fetchOptions) => { return await $fetch("/near/profile", { method: "POST", body: { accountId }, ...fetchOptions }); }, view: async (params, fetchOptions) => { return await $fetch("/near/view", { method: "POST", body: params, ...fetchOptions }); }, getAccountId: () => { const state = nearState.get(); return state?.accountId || null; }, getState: () => nearState.get(), isWalletConnected: () => walletConnected.get(), ensureConnected: async () => { if (!clientInitialized) { if (!initClient()) return false; } if (walletConnected.get()) { try { const { accounts } = await requireConnector().getConnectedWallet(); if (accounts?.length) return true; } catch (err) { console.error("[siwn] restoreFromSession failed:", err instanceof Error ? err.message : err); } } return ensureWalletConnected(); }, disconnect: async () => { if (connector) await connector.disconnect(); walletConnected.set(false); nearState.set(null); }, link: async (callbacks) => { try { const { signedMessage, accountId, nonceHex } = await signWithWallet(); const message = `Sign in to ${config.recipient}`; await handleAccountConnection(accountId, signedMessage.publicKey); const linkResponse = await $fetch("/near/link-account", { method: "POST", body: { signedMessage, message, recipient: config.recipient, nonce: nonceHex, accountId, } }); if (linkResponse.error) { throw new Error(linkResponse.error.message || "Failed to link NEAR account"); } if (!linkResponse?.data?.success) { throw new Error("Account linking failed"); } callbacks?.onSuccess?.(); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); callbacks?.onError?.(err); } }, unlink: async (params, fetchOptions) => { return await $fetch("/near/unlink-account", { method: "POST", body: params, ...fetchOptions }); }, listAccounts: async () => { return await $fetch("/near/list-accounts", { method: "GET" }); }, buildSignedDelegateAction: async (receiverId, buildActions) => { return buildSignedDelegateActionInternal(receiverId, buildActions); }, relayTransaction: async (params) => { return await $fetch("/near/relay", { method: "POST", body: params, }); }, getRelayStatus: async (txHash) => { return await $fetch(`/near/relay-status/${txHash}`, { method: "GET", }); }, getRelayerInfo: async () => { return await $fetch("/near/relayer-info", { method: "GET", }); }, relayHistory: async () => { return await $fetch("/near/relay-history", { method: "GET", }); }, get client() { if (!near) throw new Error("Wallet not initialized — this operation requires a browser environment"); return near; }, }, signIn: { near: async (callbacks) => { try { const { signedMessage, accountId, nonceHex } = await signWithWallet(); const message = `Sign in to ${config.recipient}`; await handleAccountConnection(accountId, signedMessage.publicKey); const verifyResponse = await $fetch("/near/verify", { method: "POST", body: { signedMessage, message, recipient: config.recipient, nonce: nonceHex, accountId, } }); if (verifyResponse.error) { throw new Error(verifyResponse.error.message || "Failed to verify signature"); } if (!verifyResponse?.data?.success) { throw new Error("Authentication verification failed"); } callbacks?.onSuccess?.(); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); callbacks?.onError?.(err); } } } }; } }; }; //# sourceMappingURL=client.js.map