better-near-auth
Version:
Sign in with NEAR (SIWN) plugin for Better Auth
460 lines (459 loc) • 14.5 kB
JavaScript
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