better-near-auth 0.2.4 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
 
14
14
  </div>
15
15
 
16
- This [Better Auth](https://better-auth.com) plugin enables secure authentication via NEAR wallets and keypairs by following the [NEP-413 standard](https://github.com/near/NEPs/blob/master/neps/nep-0413.md). It leverages [near-sign-verify](https://github.com/elliotBraem/near-sign-verify) and [fastintear](https://github.com/elliotBraem/fastintear) to provide a complete drop-in solution with session management, secure defaults, and automatic profile integration.
16
+ This [Better Auth](https://better-auth.com) plugin enables secure authentication via NEAR wallets and keypairs by following the [NEP-413 standard](https://github.com/near/NEPs/blob/master/neps/nep-0413.md). It leverages [near-sign-verify](https://github.com/elliotBraem/near-sign-verify), [near-kit](https://kit.near.tools/), and [Hot Connect](https://github.com/azbang/hot-connector) to provide a complete drop-in solution with session management, secure defaults, and automatic profile integration.
17
17
 
18
18
  ## Installation
19
19
 
@@ -112,7 +112,7 @@ export function LoginButton() {
112
112
  const [isConnectingWallet, setIsConnectingWallet] = useState(false);
113
113
  const [isSigningIn, setIsSigningIn] = useState(false);
114
114
 
115
- // Get account ID from embedded fastintear client
115
+ // Get account ID from near-kit client
116
116
  const accountId = authClient.near.getAccountId();
117
117
 
118
118
  if (session) {
@@ -277,7 +277,7 @@ The client plugin provides the following actions:
277
277
  - `nonce(params)` - Request a nonce from the server
278
278
  - `verify(params)` - Verify an auth token with the server
279
279
  - `getProfile(accountId?)` - Get user profile from NEAR Social
280
- - `getNearClient()` - Get the embedded fastintear client
280
+ - `getNearClient()` - Get the near-kit client instance
281
281
  - `getAccountId()` - Get the currently connected account ID
282
282
  - `disconnect()` - Disconnect wallet and clear cached data
283
283
 
@@ -427,7 +427,7 @@ export const authClient = createAuthClient({
427
427
 
428
428
  1. **"Wallet not connected"**
429
429
  - You must call `requestSignIn.near()` before `signIn.near()`
430
- - Check that the embedded fastintear client is properly initialized
430
+ - Check that the near-kit client is properly initialized
431
431
 
432
432
  2. **"No valid nonce found"**
433
433
  - Ensure `requestSignIn.near()` completed successfully before calling `signIn.near()`
@@ -452,5 +452,6 @@ export const authClient = createAuthClient({
452
452
  * [NEAR Protocol](https://near.org)
453
453
  * [NEP-413 Specification](https://github.com/near/NEPs/blob/master/neps/nep-0413.md)
454
454
  * [near-sign-verify](https://github.com/elliotBraem/near-sign-verify)
455
- * [fastintear](https://github.com/elliotBraem/fastintear)
455
+ * [near-kit](https://kit.near.tools/)
456
+ * [Hot Connect](https://github.com/azbang/hot-connector)
456
457
  * [Example Implementation](https://better-near-auth.near.page)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "better-near-auth",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
4
4
  "description": "Sign in with NEAR (SIWN) plugin for Better Auth",
5
5
  "main": "src/index.ts",
6
6
  "module": "src/index.ts",
@@ -47,15 +47,16 @@
47
47
  "typescript": "^5.7.0"
48
48
  },
49
49
  "dependencies": {
50
- "fastintear": "^0.3.8",
50
+ "@hot-labs/near-connect": "^latest",
51
51
  "nanostores": "^1.0.1",
52
+ "near-kit": "^latest",
52
53
  "near-sign-verify": "^0.4.5",
53
54
  "zod": "^4.1.12"
54
55
  },
55
56
  "devDependencies": {
56
57
  "@types/bun": "latest",
57
58
  "@types/node": "^24.9.2",
58
- "better-auth": "^1.3.34",
59
+ "better-auth": "^1.4.4",
59
60
  "typescript": "^5.9.3",
60
61
  "vitest": "^3.2.4"
61
62
  },
package/src/client.ts CHANGED
@@ -1,10 +1,12 @@
1
+ import { NearConnector } from "@hot-labs/near-connect";
2
+ import type { Account, NearWalletBase } from "@hot-labs/near-connect/build/types/wallet";
1
3
  import type { BetterAuthClientPlugin, BetterFetch, BetterFetchOption, BetterFetchResponse } from "better-auth/client";
2
- import { createNearClient } from "fastintear";
3
- import { base64ToBytes } from "fastintear/utils";
4
4
  import { atom } from "nanostores";
5
- import { parseAuthToken, sign } from "near-sign-verify";
5
+ import { Near, fromHotConnect } from "near-kit";
6
+ import { sign } from "near-sign-verify";
6
7
  import type { siwn } from ".";
7
8
  import { type AccountId, type NonceRequestT, type NonceResponseT, type ProfileResponseT, type VerifyRequestT, type VerifyResponseT } from "./types";
9
+ import { base64ToBytes } from "./utils";
8
10
 
9
11
  export interface AuthCallbacks {
10
12
  onSuccess?: () => void;
@@ -30,7 +32,7 @@ export interface SIWNClientActions {
30
32
  nonce: (params: NonceRequestT) => Promise<BetterFetchResponse<NonceResponseT>>;
31
33
  verify: (params: VerifyRequestT) => Promise<BetterFetchResponse<VerifyResponseT>>;
32
34
  getProfile: (accountId?: AccountId) => Promise<BetterFetchResponse<ProfileResponseT>>;
33
- getNearClient: () => ReturnType<typeof createNearClient>;
35
+ getNearClient: () => Near;
34
36
  getAccountId: () => string | null;
35
37
  getState: () => { accountId: string | null; publicKey: string | null; networkId: string } | null;
36
38
  disconnect: () => Promise<void>;
@@ -56,10 +58,24 @@ export interface SIWNClientPlugin extends BetterAuthClientPlugin {
56
58
  getActions: ($fetch: BetterFetch) => SIWNClientActions;
57
59
  }
58
60
 
61
+
59
62
  export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
60
63
  const cachedNonce = atom<CachedNonceData | null>(null);
61
64
  const nearState = atom<{ accountId: string | null; publicKey: string | null; networkId: string } | null>(null);
62
65
 
66
+ // Initialize Hot Connect connector
67
+ const network = config.networkId || "mainnet";
68
+ const connector = new NearConnector({ network });
69
+
70
+ // Public Near instance for read-only access
71
+ const publicNear = new Near({ network });
72
+
73
+ // Near instance will be created after wallet connection
74
+ let nearInstance: Near | null = null;
75
+ let connectionPromise: Promise<void> | null = null;
76
+ let connectionResolve: (() => void) | null = null;
77
+ let connectionReject: ((error: Error) => void) | null = null;
78
+
63
79
  const clearNonce = () => {
64
80
  cachedNonce.set(null);
65
81
  };
@@ -71,6 +87,66 @@ export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
71
87
  return (now - nonceData.timestamp) < fiveMinutes;
72
88
  };
73
89
 
90
+ const handleAccountConnection = async (accounts: Account[]) => {
91
+ try {
92
+ const accountId = accounts?.[0]?.accountId;
93
+ if (!accountId) return;
94
+
95
+ // Create Near instance with Hot Connect wallet adapter
96
+ nearInstance = new Near({
97
+ network,
98
+ wallet: fromHotConnect(connector),
99
+ });
100
+
101
+ // Update state with account info
102
+ nearState.set({
103
+ accountId,
104
+ publicKey: accounts?.[0]?.publicKey || null,
105
+ networkId: network
106
+ });
107
+
108
+ // Resolve connection promise if it exists
109
+ if (connectionResolve) {
110
+ connectionResolve();
111
+ connectionResolve = null;
112
+ connectionReject = null;
113
+ }
114
+ } catch (error) {
115
+ const err = error instanceof Error ? error : new Error(String(error));
116
+ if (connectionReject) {
117
+ connectionReject(err);
118
+ connectionResolve = null;
119
+ connectionReject = null;
120
+ }
121
+ }
122
+ };
123
+
124
+ // Check for existing connection immediately
125
+ connector.getConnectedWallet().then((result: {
126
+ wallet: NearWalletBase;
127
+ accounts: Account[];
128
+ }) => {
129
+ if (result && result.accounts && result.accounts.length > 0) {
130
+ handleAccountConnection(result.accounts);
131
+ }
132
+ }).catch(() => {
133
+ // Ignore errors on initial check
134
+ });
135
+
136
+ // Set up event listeners for Hot Connect
137
+ // Per documentation: connector.on("wallet:signIn", async (t) => { const address = t.accounts[0].accountId; })
138
+ connector.on("wallet:signIn", async (data) => {
139
+ if (data?.accounts) {
140
+ await handleAccountConnection(data.accounts);
141
+ }
142
+ });
143
+
144
+ connector.on("wallet:signOut", () => {
145
+ nearInstance = null;
146
+ nearState.set(null);
147
+ clearNonce();
148
+ });
149
+
74
150
  return {
75
151
  id: "siwn",
76
152
  $InferServerPlugin: {} as ReturnType<typeof siwn>,
@@ -81,25 +157,6 @@ export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
81
157
  }),
82
158
 
83
159
  getActions: ($fetch): SIWNClientActions => {
84
- const nearClient = createNearClient({
85
- networkId: config.networkId || "mainnet",
86
- callbacks: {
87
- onConnect: (accountData) => {
88
- // Update nearState atom when wallet connects
89
- nearState.set({
90
- accountId: accountData.accountId,
91
- publicKey: accountData.publicKey,
92
- networkId: config.networkId || "mainnet"
93
- });
94
- },
95
- onDisconnect: () => {
96
- // Clear nearState atom when wallet disconnects
97
- nearState.set(null);
98
- clearNonce();
99
- }
100
- }
101
- });
102
-
103
160
  return {
104
161
  near: {
105
162
  nonce: async (params: NonceRequestT, fetchOptions?: BetterFetchOption): Promise<BetterFetchResponse<NonceResponseT>> => {
@@ -123,11 +180,19 @@ export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
123
180
  ...fetchOptions
124
181
  });
125
182
  },
126
- getNearClient: () => nearClient,
127
- getAccountId: () => nearClient.accountId(),
183
+ getNearClient: () => {
184
+ return nearInstance || publicNear;
185
+ },
186
+ getAccountId: () => {
187
+ const state = nearState.get();
188
+ return state?.accountId || null;
189
+ },
128
190
  getState: () => nearState.get(),
129
191
  disconnect: async () => {
130
- await nearClient.signOut();
192
+ if (connector) {
193
+ await connector.disconnect();
194
+ }
195
+ nearInstance = null;
131
196
  clearNonce();
132
197
  nearState.set(null);
133
198
  },
@@ -138,13 +203,14 @@ export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
138
203
  try {
139
204
  const { recipient } = params;
140
205
 
141
- if (!nearClient) {
206
+ if (!nearInstance) {
142
207
  const error = new Error("NEAR client not available") as Error & { code?: string };
143
208
  error.code = "SIGNER_NOT_AVAILABLE";
144
209
  throw error;
145
210
  }
146
211
 
147
- const accountId = nearClient.accountId();
212
+ const state = nearState.get();
213
+ const accountId = state?.accountId;
148
214
  if (!accountId) {
149
215
  const error = new Error("Wallet not connected. Please connect your wallet first.") as Error & { code?: string };
150
216
  error.code = "WALLET_NOT_CONNECTED";
@@ -152,11 +218,10 @@ export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
152
218
  }
153
219
 
154
220
  // Get nonce first
155
- const state = nearState.get();
156
221
  const nonceRequest: NonceRequestT = {
157
222
  accountId,
158
- publicKey: state?.publicKey || "",
159
- networkId: (state?.networkId || "mainnet") as "mainnet" | "testnet"
223
+ publicKey: state.publicKey || "",
224
+ networkId: (state.networkId || network) as "mainnet" | "testnet"
160
225
  };
161
226
 
162
227
  const nonceResponse: BetterFetchResponse<NonceResponseT> = await $fetch("/near/nonce", {
@@ -177,13 +242,28 @@ export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
177
242
  const message = `Sign in to ${recipient}\n\nAccount ID: ${accountId}\nNonce: ${nonce}`;
178
243
  const nonceBytes = base64ToBytes(nonce);
179
244
 
180
- // Sign the message
245
+ // Sign the message using near-sign-verify
181
246
  const authToken = await sign(message, {
182
- signer: nearClient,
247
+ signer: nearInstance,
183
248
  recipient,
184
249
  nonce: nonceBytes,
185
250
  });
186
251
 
252
+ // Update state with publicKey from signed message if not already set
253
+ if (!state.publicKey) {
254
+ try {
255
+ const parsedToken = JSON.parse(authToken);
256
+ if (parsedToken.publicKey) {
257
+ nearState.set({
258
+ ...state,
259
+ publicKey: parsedToken.publicKey,
260
+ });
261
+ }
262
+ } catch (e) {
263
+ // Ignore
264
+ }
265
+ }
266
+
187
267
  // Link the account (instead of verify)
188
268
  const linkResponse: BetterFetchResponse<any> = await $fetch("/near/link-account", {
189
269
  method: "POST",
@@ -229,60 +309,68 @@ export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
229
309
  try {
230
310
  const { recipient } = params;
231
311
 
232
- if (!nearClient) {
233
- const error = new Error("NEAR client not available") as Error & { code?: string };
234
- error.code = "SIGNER_NOT_AVAILABLE";
235
- throw error;
312
+ clearNonce();
313
+
314
+ // Create a promise to wait for wallet connection
315
+ connectionPromise = new Promise<void>((resolve, reject) => {
316
+ connectionResolve = resolve;
317
+ connectionReject = reject;
318
+ });
319
+
320
+ // Trigger Hot Connect modal
321
+ await connector.connect();
322
+
323
+ // Wait for wallet:signIn event
324
+ await connectionPromise;
325
+
326
+ // After connection, get account info and request nonce
327
+ const state = nearState.get();
328
+ if (!state || !state.accountId) {
329
+ throw new Error("Failed to get account information after wallet connection");
236
330
  }
237
331
 
238
- clearNonce();
332
+ const { accountId, networkId, publicKey } = state;
239
333
 
240
- await nearClient.requestSignIn({ contractId: recipient }, {
241
- onSuccess: async ({ accountId, publicKey, networkId }: { accountId: string, publicKey: string, networkId: string }) => {
242
- try {
243
- const nonceRequest: NonceRequestT = {
244
- accountId,
245
- publicKey,
246
- networkId: networkId as "mainnet" | "testnet"
247
- };
248
-
249
- const nonceResponse: BetterFetchResponse<NonceResponseT> = await $fetch("/near/nonce", {
250
- method: "POST",
251
- body: nonceRequest
252
- });
334
+ if (!publicKey) {
335
+ throw new Error("Failed to get public key from wallet");
336
+ }
253
337
 
254
- if (nonceResponse.error) {
255
- throw new Error(nonceResponse.error.message || "Failed to get nonce");
256
- }
257
-
258
- const nonce = nonceResponse?.data?.nonce;
259
- if (!nonce) {
260
- throw new Error("No nonce received from server");
261
- }
262
-
263
- // Cache nonce with all wallet data
264
- const cachedData: CachedNonceData = {
265
- nonce,
266
- accountId,
267
- publicKey,
268
- networkId,
269
- timestamp: Date.now()
270
- };
271
- cachedNonce.set(cachedData);
272
-
273
- callbacks?.onSuccess?.();
274
- } catch (error) {
275
- const err = error instanceof Error ? error : new Error(String(error));
276
- clearNonce();
277
- callbacks?.onError?.(err);
278
- }
279
- },
280
- onError: (error: any) => {
281
- const err = error instanceof Error ? error : new Error(String(error));
282
- clearNonce();
283
- callbacks?.onError?.(err);
284
- }
338
+ nearState.set({
339
+ ...state,
340
+ publicKey,
285
341
  });
342
+
343
+ const nonceRequest: NonceRequestT = {
344
+ accountId,
345
+ publicKey: publicKey,
346
+ networkId: networkId as "mainnet" | "testnet"
347
+ };
348
+
349
+ const nonceResponse: BetterFetchResponse<NonceResponseT> = await $fetch("/near/nonce", {
350
+ method: "POST",
351
+ body: nonceRequest
352
+ });
353
+
354
+ if (nonceResponse.error) {
355
+ throw new Error(nonceResponse.error.message || "Failed to get nonce");
356
+ }
357
+
358
+ const nonce = nonceResponse?.data?.nonce;
359
+ if (!nonce) {
360
+ throw new Error("No nonce received from server");
361
+ }
362
+
363
+ // Cache nonce with all wallet data
364
+ const cachedData: CachedNonceData = {
365
+ nonce,
366
+ accountId,
367
+ publicKey,
368
+ networkId,
369
+ timestamp: Date.now()
370
+ };
371
+ cachedNonce.set(cachedData);
372
+
373
+ callbacks?.onSuccess?.();
286
374
  } catch (error) {
287
375
  const err = error instanceof Error ? error : new Error(String(error));
288
376
  clearNonce();
@@ -298,13 +386,14 @@ export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
298
386
  try {
299
387
  const { recipient } = params;
300
388
 
301
- if (!nearClient) {
389
+ if (!nearInstance) {
302
390
  const error = new Error("NEAR client not available") as Error & { code?: string };
303
391
  error.code = "SIGNER_NOT_AVAILABLE";
304
392
  throw error;
305
393
  }
306
394
 
307
- const accountId = nearClient.accountId();
395
+ const state = nearState.get();
396
+ const accountId = state?.accountId;
308
397
  if (!accountId) {
309
398
  const error = new Error("Wallet not connected. Please connect your wallet first.") as Error & { code?: string };
310
399
  error.code = "WALLET_NOT_CONNECTED";
@@ -335,12 +424,11 @@ export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
335
424
 
336
425
  // Sign the message
337
426
  const authToken = await sign(message, {
338
- signer: nearClient,
427
+ signer: nearInstance,
339
428
  recipient,
340
429
  nonce: nonceBytes,
341
430
  });
342
431
 
343
- // Verify the signature with the server
344
432
  const verifyResponse: BetterFetchResponse<VerifyResponseT> = await $fetch("/near/verify", {
345
433
  method: "POST",
346
434
  body: {
package/src/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { APIError, createAuthEndpoint, createAuthMiddleware, sessionMiddleware } from "better-auth/api";
2
2
  import { setSessionCookie } from "better-auth/cookies";
3
3
  import type { Account, BetterAuthPlugin, User } from "better-auth/types";
4
- import { bytesToBase64 } from "fastintear/utils";
5
- import { generateNonce, verify, type VerificationResult, type VerifyOptions } from "near-sign-verify";
4
+ import { generateNonce, parseAuthToken, verify, type VerificationResult, type VerifyOptions } from "near-sign-verify";
5
+ import { bytesToBase64 } from "./utils";
6
6
  import z from "zod";
7
7
  import { defaultGetProfile, getImageUrl, getNetworkFromAccountId } from "./profile";
8
8
  import { schema } from "./schema";
@@ -464,6 +464,7 @@ export const siwn = (options: SIWNPluginOptions) =>
464
464
  }
465
465
 
466
466
  try {
467
+ console.log("in server authToken", parseAuthToken(authToken));
467
468
  const verification =
468
469
  await ctx.context.internalAdapter.findVerificationValue(
469
470
  `siwn:${accountId}:${network}`,
@@ -622,8 +623,8 @@ export const siwn = (options: SIWNPluginOptions) =>
622
623
  }
623
624
 
624
625
  const session = await ctx.context.internalAdapter.createSession(
625
- user.id,
626
- ctx,
626
+ user.id
627
+ // ctx,
627
628
  );
628
629
 
629
630
  if (!session) {
@@ -645,6 +646,8 @@ export const siwn = (options: SIWNPluginOptions) =>
645
646
  },
646
647
  }));
647
648
  } catch (error: unknown) {
649
+ console.log("server authToken", authToken);
650
+ console.log("server parsed authToken", parseAuthToken(authToken));
648
651
  if (error instanceof APIError) throw error;
649
652
  throw new APIError("UNAUTHORIZED", {
650
653
  message: "Something went wrong. Please try again later.",
package/src/utils.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Utility functions for base64 encoding/decoding
3
+ * Replaces fastintear/utils functions (now using standard browser APIs)
4
+ */
5
+
6
+ export function bytesToBase64(bytes: Uint8Array): string {
7
+ return btoa(String.fromCharCode(...bytes));
8
+ }
9
+
10
+ export function base64ToBytes(base64: string): Uint8Array {
11
+ return Uint8Array.from(atob(base64), c => c.charCodeAt(0));
12
+ }
13
+