UNPKG

better-near-auth

Version:

Sign in with NEAR (SIWN) plugin for Better Auth

460 lines (459 loc) 14.5 kB
import { Near, fromNearConnect, generateNonce } from "near-kit"; import { hex } from "@scure/base"; import { atom } from "nanostores"; import { z } from "zod"; import { AccountIdSchema } from "near-kit/schemas"; //#region \0rolldown/runtime.js var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports); var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion //#region src/types.ts const socialImageSchema = z.object({ url: z.string().optional(), ipfs_cid: z.string().optional() }); const profileSchema = z.object({ name: z.string().optional(), description: z.string().optional(), image: socialImageSchema.optional(), backgroundImage: socialImageSchema.optional(), linktree: z.record(z.string(), z.string()).optional() }); const signedMessageSchema = z.object({ accountId: z.string(), publicKey: z.string(), signature: z.string(), state: z.string().optional() }); z.object({ signedMessage: signedMessageSchema, message: z.string(), recipient: z.string(), nonce: z.string(), accountId: AccountIdSchema }); z.object({ accountId: AccountIdSchema, network: z.enum(["mainnet", "testnet"]).optional() }); z.object({ accountId: AccountIdSchema, networkId: z.union([z.literal("mainnet"), z.literal("testnet")]) }); z.object({ signedMessage: signedMessageSchema, message: z.string(), recipient: z.string(), nonce: z.string(), accountId: AccountIdSchema }); z.object({ payload: z.string() }); z.object({ txHash: z.string(), status: z.enum([ "pending", "completed", "failed" ]) }); z.object({ status: z.enum([ "pending", "completed", "failed" ]), gasUsed: z.string().optional(), outcome: z.unknown().optional() }); z.object({ contractId: z.string(), methodName: z.string(), args: z.record(z.string(), z.any()).optional() }); z.object({ nonce: z.string() }); z.object({ token: z.string(), success: z.literal(true), user: z.object({ id: z.string(), accountId: AccountIdSchema, network: z.union([z.literal("mainnet"), z.literal("testnet")]) }) }); profileSchema.nullable(); z.object({ result: z.unknown() }); z.object({ accountId: AccountIdSchema.optional() }); const RelayedTransactionSchema = z.object({ id: z.string(), userId: z.string(), txHash: z.string(), senderId: z.string(), receiverId: z.string(), network: z.string(), status: z.string(), gasUsed: z.string().optional(), createdAt: z.string(), updatedAt: z.string().optional() }); z.object({ transactions: z.array(RelayedTransactionSchema) }); //#endregion //#region src/client.ts 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; let connectorModulePromise = null; let initClientPromise = null; const loadConnector = async () => { connectorModulePromise ??= import("./build.js").then((m) => /* @__PURE__ */ __toESM(m.default, 1)); const { NearConnector } = await connectorModulePromise; return NearConnector; }; const handleAccountConnection = async (accountId, publicKey) => { if (!accountId) return; nearState.set({ accountId, publicKey: publicKey || null, networkId: network }); walletConnected.set(true); }; const initClient = async ($fetch) => { if (clientInitialized) return true; if (initClientPromise) return initClientPromise; if (typeof globalThis.window === "undefined") return false; initClientPromise = (async () => { connector = new (await (loadConnector()))({ network, cspNonce: config.cspNonce }); 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; })(); try { return await initClientPromise; } finally { if (!clientInitialized) initClientPromise = null; } }; let sessionRestored = false; const restoreFromSession = async ($fetch) => { if (sessionRestored) return; if (nearState.get()?.accountId) { sessionRestored = true; return; } try { const res = await $fetch("/near/list-accounts", { method: "GET" }); const accounts = res.data?.accounts; if (accounts?.length) { const primary = res.data?.activeAccount || 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 = async () => { 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 = await 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 = await 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()) { if (!await ensureWalletConnected()) throw new Error("Wallet connection required — please approve the connection to sign"); } const { payload } = await buildActions(requireNear().transaction(state.accountId), receiverId).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: () => { return nearState.get()?.accountId || null; }, getState: () => nearState.get(), isWalletConnected: () => walletConnected.get(), ensureConnected: async () => { if (!clientInitialized) { if (!await initClient()) return false; } if (walletConnected.get()) try { const { accounts } = await (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" }); }, setPrimaryAccount: async (params) => { const response = await $fetch("/near/set-primary-account", { method: "POST", body: params }); const activeAccount = response.data?.activeAccount; if (activeAccount) nearState.set({ accountId: activeAccount.accountId, publicKey: activeAccount.publicKey ?? null, networkId: activeAccount.network }); return response; }, 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); } } } }; } }; }; //#endregion export { siwnClient, __commonJSMin as t }; //# sourceMappingURL=client.js.map