better-near-auth 0.1.2 → 0.1.4

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
@@ -55,20 +55,41 @@ npm install better-near-auth
55
55
  import { siwnClient } from "better-near-auth/client";
56
56
 
57
57
  export const authClient = createAuthClient({
58
- plugins: [siwnClient()],
58
+ plugins: [
59
+ siwnClient({
60
+ domain: "myapp.com", // this doesn't actually do anything yet... taking suggestions
61
+ networkId: "mainnet", // optional, default is "mainnet"
62
+ })
63
+ ],
59
64
  });
60
65
  ```
61
66
 
62
-
63
67
  ## Usage
64
68
 
65
- ### One-Line Authentication
69
+ ### Two-Step Authentication Flow
70
+
71
+ The plugin uses a secure two-step authentication process:
72
+
73
+ 1. **Step 1**: Connect wallet and cache nonce
74
+ 2. **Step 2**: Sign message and authenticate
66
75
 
67
- The simplest way to authenticate with NEAR:
76
+ ```ts title="two-step-auth.ts"
77
+ // Step 1: Connect wallet and get nonce
78
+ await authClient.requestSignIn.near(
79
+ { recipient: "myapp.com" },
80
+ {
81
+ onSuccess: () => {
82
+ console.log("Wallet connected, nonce cached!");
83
+ },
84
+ onError: (error) => {
85
+ console.error("Wallet connection failed:", error.message);
86
+ }
87
+ }
88
+ );
68
89
 
69
- ```ts title="sign-in-near.ts"
70
- const response = await authClient.signIn.near(
71
- { recipient: "myapp.com", signer: window.near },
90
+ // Step 2: Sign message and authenticate
91
+ await authClient.signIn.near(
92
+ { recipient: "myapp.com" },
72
93
  {
73
94
  onSuccess: () => {
74
95
  console.log("Successfully signed in!");
@@ -78,8 +99,91 @@ const response = await authClient.signIn.near(
78
99
  }
79
100
  }
80
101
  );
102
+ ```
103
+
104
+ ### Complete React Component Example
105
+
106
+ ```tsx title="LoginButton.tsx"
107
+ import { authClient } from "./auth-client";
108
+ import { useState } from "react";
109
+
110
+ export function LoginButton() {
111
+ const { data: session } = authClient.useSession();
112
+ const [isConnectingWallet, setIsConnectingWallet] = useState(false);
113
+ const [isSigningIn, setIsSigningIn] = useState(false);
114
+
115
+ // Get account ID from embedded fastintear client
116
+ const accountId = authClient.near.getAccountId();
117
+
118
+ if (session) {
119
+ return (
120
+ <div>
121
+ <p>Welcome, {session.user.name}!</p>
122
+ <button onClick={() => authClient.signOut()}>Sign out</button>
123
+ </div>
124
+ );
125
+ }
81
126
 
82
- console.log("Signed in as:", response.user.accountId);
127
+ const handleWalletConnect = async () => {
128
+ setIsConnectingWallet(true);
129
+
130
+ try {
131
+ await authClient.requestSignIn.near(
132
+ { recipient: "myapp.com" },
133
+ {
134
+ onSuccess: () => {
135
+ setIsConnectingWallet(false);
136
+ console.log("Wallet connected!");
137
+ },
138
+ onError: (error) => {
139
+ setIsConnectingWallet(false);
140
+ console.error("Wallet connection failed:", error.message);
141
+ },
142
+ }
143
+ );
144
+ } catch (error) {
145
+ setIsConnectingWallet(false);
146
+ console.error("Authentication error:", error);
147
+ }
148
+ };
149
+
150
+ const handleSignIn = async () => {
151
+ setIsSigningIn(true);
152
+
153
+ try {
154
+ await authClient.signIn.near(
155
+ { recipient: "myapp.com" },
156
+ {
157
+ onSuccess: () => {
158
+ setIsSigningIn(false);
159
+ console.log("Successfully signed in!");
160
+ },
161
+ onError: (error) => {
162
+ setIsSigningIn(false);
163
+ console.error("Sign in failed:", error.message);
164
+ },
165
+ }
166
+ );
167
+ } catch (error) {
168
+ setIsSigningIn(false);
169
+ console.error("Authentication error:", error);
170
+ }
171
+ };
172
+
173
+ return (
174
+ <div>
175
+ {!accountId ? (
176
+ <button onClick={handleWalletConnect} disabled={isConnectingWallet}>
177
+ {isConnectingWallet ? "Connecting..." : "Connect NEAR Wallet"}
178
+ </button>
179
+ ) : (
180
+ <button onClick={handleSignIn} disabled={isSigningIn}>
181
+ {isSigningIn ? "Signing in..." : `Sign in with NEAR (${accountId})`}
182
+ </button>
183
+ )}
184
+ </div>
185
+ );
186
+ }
83
187
  ```
84
188
 
85
189
  ### Profile Access
@@ -96,6 +200,20 @@ const aliceProfile = await authClient.near.getProfile("alice.near");
96
200
  console.log("Alice's profile:", aliceProfile);
97
201
  ```
98
202
 
203
+ ### Wallet Management
204
+
205
+ ```ts title="wallet-management.ts"
206
+ // Check if wallet is connected
207
+ const accountId = authClient.near.getAccountId();
208
+ console.log("Connected account:", accountId);
209
+
210
+ // Get the embedded NEAR client
211
+ const nearClient = authClient.near.getNearClient();
212
+
213
+ // Disconnect wallet and clear cached data
214
+ await authClient.near.disconnect();
215
+ ```
216
+
99
217
  ## Configuration Options
100
218
 
101
219
  ### Server Options
@@ -115,7 +233,10 @@ The SIWN plugin accepts the following configuration options:
115
233
 
116
234
  ### Client Options
117
235
 
118
- The SIWN client plugin doesn't require any configuration options:
236
+ The SIWN client plugin accepts the following configuration options:
237
+
238
+ * **domain**: Domain identifier... idk what it should do yet. Maybe shade agent.
239
+ * **networkId**: NEAR network to use ("mainnet" or "testnet"). Default is "mainnet"
119
240
 
120
241
  ```ts title="auth-client.ts"
121
242
  import { createAuthClient } from "better-auth/client";
@@ -124,7 +245,8 @@ import { siwnClient } from "better-near-auth/client";
124
245
  export const authClient = createAuthClient({
125
246
  plugins: [
126
247
  siwnClient({
127
- // Optional client configuration can go here
248
+ domain: "myapp.com",
249
+ networkId: "testnet", // Use testnet
128
250
  }),
129
251
  ],
130
252
  });
@@ -144,94 +266,51 @@ The SIWN plugin adds a `nearAccount` table to store user NEAR account associatio
144
266
  | isPrimary | boolean | Whether this is the user's primary account|
145
267
  | createdAt | date | Creation timestamp |
146
268
 
147
- ## Complete Implementation Example
269
+ ## API Reference
148
270
 
149
- Here's a complete example showing how to implement SIWN authentication:
271
+ ### Client Actions
150
272
 
151
- ```ts title="auth.ts"
152
- import { betterAuth } from "better-auth";
153
- import { siwn } from "better-near-auth";
273
+ The client plugin provides the following actions:
154
274
 
155
- export const auth = betterAuth({
156
- database: {
157
- provider: "sqlite",
158
- url: "./db.sqlite"
159
- },
160
- plugins: [
161
- siwn({
162
- recipient: "myapp.com",
163
- anonymous: false, // Require email for users
164
- emailDomainName: "myapp.com",
165
-
166
- // Optional: Custom profile lookup
167
- getProfile: async (accountId) => {
168
- // Custom profile logic, falls back to NEAR Social
169
- },
170
- }),
171
- ],
172
- });
173
- ```
275
+ #### `authClient.near`
174
276
 
175
- ```ts title="auth-client.ts"
176
- import { createAuthClient } from "better-auth/client";
177
- import { siwnClient } from "better-near-auth/client";
277
+ - `nonce(params)` - Request a nonce from the server
278
+ - `verify(params)` - Verify an auth token with the server
279
+ - `getProfile(accountId?)` - Get user profile from NEAR Social
280
+ - `getNearClient()` - Get the embedded fastintear client
281
+ - `getAccountId()` - Get the currently connected account ID
282
+ - `disconnect()` - Disconnect wallet and clear cached data
178
283
 
179
- export const authClient = createAuthClient({
180
- baseURL: "http://localhost:3000",
181
- plugins: [siwnClient()],
182
- });
183
- ```
284
+ #### `authClient.requestSignIn`
184
285
 
185
- ```tsx title="LoginButton.tsx"
186
- import { authClient } from "./auth-client";
187
- import { useState } from "react";
286
+ - `near(params, callbacks?)` - Connect wallet and cache nonce (Step 1)
188
287
 
189
- export function LoginButton() {
190
- const { data: session } = authClient.useSession();
191
- const [isSigningIn, setIsSigningIn] = useState(false);
288
+ #### `authClient.signIn`
192
289
 
193
- if (session) {
194
- return (
195
- <div>
196
- <p>Welcome, {session.user.name}!</p>
197
- <button onClick={() => authClient.signOut()}>Sign out</button>
198
- </div>
199
- );
200
- }
290
+ - `near(params, callbacks?)` - Sign message and authenticate (Step 2)
201
291
 
202
- const handleSignIn = async () => {
203
- setIsSigningIn(true);
204
-
205
- try {
206
- await authClient.signIn.near(
207
- { recipient: "myapp.com", signer: window.near },
208
- {
209
- onSuccess: () => {
210
- console.log("Successfully signed in!");
211
- },
212
- onError: (error) => {
213
- console.error("Sign in failed:", error.message);
214
- }
215
- }
216
- );
217
- } catch (error) {
218
- console.error("Authentication error:", error);
219
- } finally {
220
- setIsSigningIn(false);
221
- }
222
- };
292
+ ### Callback Interface
223
293
 
224
- return (
225
- <button onClick={handleSignIn} disabled={isSigningIn}>
226
- {isSigningIn ? "Signing in..." : "Sign in with NEAR"}
227
- </button>
228
- );
294
+ ```typescript
295
+ interface AuthCallbacks {
296
+ onSuccess?: () => void;
297
+ onError?: (error: Error & { status?: number; code?: string }) => void;
229
298
  }
230
299
  ```
231
300
 
301
+ ### Error Codes
302
+
303
+ Common error codes you may encounter:
304
+
305
+ - `SIGNER_NOT_AVAILABLE` - NEAR wallet not available
306
+ - `WALLET_NOT_CONNECTED` - Wallet not connected before signing
307
+ - `NONCE_NOT_FOUND` - No valid cached nonce found
308
+ - `ACCOUNT_MISMATCH` - Cached nonce doesn't match current account
309
+ - `UNAUTHORIZED_INVALID_OR_EXPIRED_NONCE` - Server nonce expired or invalid
310
+
232
311
  ## Advanced Configuration
233
312
 
234
- For advanced use cases, you can customize the validation functions passed to `verify` in `near-sign-verify`:
313
+ For advanced use cases, you can customize the validation functions:
235
314
 
236
315
  ```ts title="advanced-auth.ts"
237
316
  import { betterAuth } from "better-auth";
@@ -295,15 +374,78 @@ export const auth = betterAuth({
295
374
  },
296
375
 
297
376
  // Validate function call keys against allowed contracts
298
- validateLimitedAccessKey: async ({ accountId, publicKey, contractId }) => {
377
+ validateLimitedAccessKey: async ({ accountId, publicKey, recipient }) => {
299
378
  const allowedContracts = ["myapp.near", "social.near"];
300
- return contractId ? allowedContracts.includes(contractId) : true;
379
+ return recipient ? allowedContracts.includes(recipient) : true;
301
380
  },
302
381
  }),
303
382
  ],
304
383
  });
305
384
  ```
306
385
 
386
+ ## Network Support
387
+
388
+ The plugin automatically detects the network from the account ID:
389
+
390
+ - Accounts ending with `.testnet` use the testnet network
391
+ - All other accounts use the mainnet network
392
+
393
+ You can configure the client to use a specific network:
394
+
395
+ ```ts title="testnet-config.ts"
396
+ export const authClient = createAuthClient({
397
+ plugins: [
398
+ siwnClient({
399
+ domain: "myapp.com",
400
+ networkId: "testnet", // Use testnet
401
+ }),
402
+ ],
403
+ });
404
+ ```
405
+
406
+ ## Security Features
407
+
408
+ ### NEP-413 Compliance
409
+ - Follows NEAR Enhancement Proposal 413 for secure message signing
410
+ - Implements proper nonce handling to prevent replay attacks
411
+ - Validates message format and recipient information
412
+
413
+ ### Nonce Management
414
+ - Unique nonce storage per account/network/publicKey combination
415
+ - 15-minute server-side expiration for nonces
416
+ - 5-minute client-side cache expiration
417
+ - Automatic cleanup after successful authentication
418
+
419
+ ### Access Key Support
420
+ - Supports both full access keys and function call access keys
421
+ - Configurable validation for limited access keys
422
+ - Contract-specific access control when using function call keys
423
+
424
+ ## Troubleshooting
425
+
426
+ ### Common Issues
427
+
428
+ 1. **"Wallet not connected"**
429
+ - You must call `requestSignIn.near()` before `signIn.near()`
430
+ - Check that the embedded fastintear client is properly initialized
431
+
432
+ 2. **"No valid nonce found"**
433
+ - Ensure `requestSignIn.near()` completed successfully before calling `signIn.near()`
434
+ - Client nonces expire after 5 minutes
435
+
436
+ 3. **"Invalid or expired nonce"**
437
+ - Server nonces expire after 15 minutes
438
+ - Ensure client and server clocks are synchronized
439
+
440
+ 4. **"Account ID mismatch"**
441
+ - Verify the signed message contains the correct account ID
442
+ - Check for wallet switching between the two authentication steps
443
+
444
+ 5. **"Network ID mismatch"**
445
+ - Ensure the networkId sent to the server matches the account's network
446
+ - Testnet accounts must use "testnet", mainnet accounts use "mainnet"
447
+
448
+
307
449
  ## Links
308
450
 
309
451
  * [Better Auth Documentation](https://better-auth.com)
@@ -311,3 +453,4 @@ export const auth = betterAuth({
311
453
  * [NEP-413 Specification](https://github.com/near/NEPs/blob/master/neps/nep-0413.md)
312
454
  * [near-sign-verify](https://github.com/elliotBraem/near-sign-verify)
313
455
  * [fastintear](https://github.com/elliotBraem/fastintear)
456
+ * [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.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Sign in with NEAR (SIWN) plugin for Better Auth",
5
5
  "main": "index.ts",
6
6
  "module": "index.ts",
@@ -45,7 +45,8 @@
45
45
  "dependencies": {
46
46
  "@better-auth/utils": "^0.2.6",
47
47
  "@fastnear/utils": "^0.9.7",
48
- "fastintear": "^0.2.3",
48
+ "fastintear": "^0.2.4",
49
+ "nanostores": "^1.0.1",
49
50
  "near-sign-verify": "^0.4.3",
50
51
  "zod": "^4.0.17"
51
52
  },
package/src/client.ts CHANGED
@@ -1,14 +1,10 @@
1
1
  import { base64ToBytes } from "@fastnear/utils";
2
- import type { BetterAuthClientPlugin, BetterFetchOption, BetterFetchResponse } from "better-auth/client";
3
- import { sign, type WalletInterface } from "near-sign-verify";
2
+ import type { BetterAuthClientPlugin, BetterFetch, BetterFetchOption, BetterFetchResponse } from "better-auth/client";
3
+ import { createNearClient } from "fastintear";
4
+ import { atom } from "nanostores";
5
+ import { sign } from "near-sign-verify";
4
6
  import type { siwn } from ".";
5
7
  import { type AccountId, type NonceRequestT, type NonceResponseT, type ProfileResponseT, type VerifyRequestT, type VerifyResponseT } from "./types";
6
- import type { User } from "better-auth";
7
-
8
- export interface Signer {
9
- accountId(): string | null;
10
- signMessage: WalletInterface["signMessage"];
11
- }
12
8
 
13
9
  export interface AuthCallbacks {
14
10
  onSuccess?: () => void;
@@ -16,7 +12,17 @@ export interface AuthCallbacks {
16
12
  }
17
13
 
18
14
  export interface SIWNClientConfig {
19
- domain: string;
15
+ domain: string; // TODO: this could potentially be shade agent proxy or something, doesn't really have any purpose rn
16
+ networkId?: "mainnet" | "testnet";
17
+ // TODO: should include browser vs keypair
18
+ }
19
+
20
+ export interface CachedNonceData {
21
+ nonce: string;
22
+ accountId: string;
23
+ publicKey: string;
24
+ networkId: string;
25
+ timestamp: number;
20
26
  }
21
27
 
22
28
  export interface SIWNClientActions {
@@ -24,19 +30,44 @@ export interface SIWNClientActions {
24
30
  nonce: (params: NonceRequestT) => Promise<BetterFetchResponse<NonceResponseT>>;
25
31
  verify: (params: VerifyRequestT) => Promise<BetterFetchResponse<VerifyResponseT>>;
26
32
  getProfile: (accountId?: AccountId) => Promise<BetterFetchResponse<ProfileResponseT>>;
33
+ getNearClient: () => ReturnType<typeof createNearClient>;
34
+ getAccountId: () => string | null;
35
+ disconnect: () => Promise<void>;
36
+ };
37
+ requestSignIn: {
38
+ near: (params: { recipient: string }, callbacks?: AuthCallbacks) => Promise<void>;
27
39
  };
28
40
  signIn: {
29
- near: (params: { recipient: string, signer: Signer }, callbacks?: AuthCallbacks) => Promise<void>;
41
+ near: (params: { recipient: string }, callbacks?: AuthCallbacks) => Promise<void>;
30
42
  };
31
43
  }
32
44
 
33
45
  export interface SIWNClientPlugin extends BetterAuthClientPlugin {
34
46
  id: "siwn";
35
47
  $InferServerPlugin: ReturnType<typeof siwn>;
36
- getActions: ($fetch: any) => SIWNClientActions;
48
+ getActions: ($fetch: BetterFetch) => SIWNClientActions;
37
49
  }
38
50
 
39
51
  export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
52
+ // Create embedded NEAR client
53
+ const nearClient = createNearClient({
54
+ networkId: config.networkId || "mainnet"
55
+ });
56
+
57
+ // Create atoms for caching nonce only
58
+ const cachedNonce = atom<CachedNonceData | null>(null);
59
+
60
+ const clearNonce = () => {
61
+ cachedNonce.set(null);
62
+ };
63
+
64
+ const isNonceValid = (nonceData: CachedNonceData | null): boolean => {
65
+ if (!nonceData) return false;
66
+ const now = Date.now();
67
+ const fiveMinutes = 5 * 60 * 1000;
68
+ return (now - nonceData.timestamp) < fiveMinutes;
69
+ };
70
+
40
71
  return {
41
72
  id: "siwn",
42
73
  $InferServerPlugin: {} as ReturnType<typeof siwn>,
@@ -63,45 +94,134 @@ export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
63
94
  body: { accountId },
64
95
  ...fetchOptions
65
96
  });
66
-
97
+ },
98
+ getNearClient: () => nearClient,
99
+ getAccountId: () => nearClient.accountId(),
100
+ disconnect: async () => {
101
+ await nearClient.signOut();
102
+ clearNonce();
67
103
  },
68
104
  },
105
+ requestSignIn: {
106
+ near: async (
107
+ params: { recipient: string },
108
+ callbacks?: AuthCallbacks
109
+ ): Promise<void> => {
110
+ try {
111
+ const { recipient } = params;
112
+
113
+ if (!nearClient) {
114
+ const error = new Error("NEAR client not available") as Error & { code?: string };
115
+ error.code = "SIGNER_NOT_AVAILABLE";
116
+ throw error;
117
+ }
118
+
119
+ clearNonce();
120
+
121
+ await nearClient.requestSignIn({ contractId: recipient }, {
122
+ onSuccess: async ({ accountId, publicKey, networkId }: { accountId: string, publicKey: string, networkId: string }) => {
123
+ try {
124
+ const nonceRequest: NonceRequestT = {
125
+ accountId,
126
+ publicKey,
127
+ networkId: networkId as "mainnet" | "testnet"
128
+ };
129
+
130
+ const nonceResponse: BetterFetchResponse<NonceResponseT> = await $fetch("/near/nonce", {
131
+ method: "POST",
132
+ body: nonceRequest
133
+ });
134
+
135
+ if (nonceResponse.error) {
136
+ throw new Error(nonceResponse.error.message || "Failed to get nonce");
137
+ }
138
+
139
+ const nonce = nonceResponse?.data?.nonce;
140
+ if (!nonce) {
141
+ throw new Error("No nonce received from server");
142
+ }
143
+
144
+ // Cache nonce with all wallet data
145
+ const cachedData: CachedNonceData = {
146
+ nonce,
147
+ accountId,
148
+ publicKey,
149
+ networkId,
150
+ timestamp: Date.now()
151
+ };
152
+ cachedNonce.set(cachedData);
153
+
154
+ callbacks?.onSuccess?.();
155
+ } catch (error) {
156
+ const err = error instanceof Error ? error : new Error(String(error));
157
+ clearNonce();
158
+ callbacks?.onError?.(err);
159
+ }
160
+ },
161
+ onError: (error: any) => {
162
+ const err = error instanceof Error ? error : new Error(String(error));
163
+ clearNonce();
164
+ callbacks?.onError?.(err);
165
+ }
166
+ });
167
+ } catch (error) {
168
+ const err = error instanceof Error ? error : new Error(String(error));
169
+ clearNonce();
170
+ callbacks?.onError?.(err);
171
+ }
172
+ }
173
+ },
69
174
  signIn: {
70
175
  near: async (
71
- params: { recipient: string, signer: Signer },
176
+ params: { recipient: string },
72
177
  callbacks?: AuthCallbacks
73
178
  ): Promise<void> => {
74
179
  try {
75
- const { signer, recipient } = params;
180
+ const { recipient } = params;
76
181
 
77
- if (!signer) {
78
- throw new Error("NEAR signer not available");
182
+ if (!nearClient) {
183
+ const error = new Error("NEAR client not available") as Error & { code?: string };
184
+ error.code = "SIGNER_NOT_AVAILABLE";
185
+ throw error;
79
186
  }
80
187
 
81
- const accountId = signer.accountId();
188
+ const accountId = nearClient.accountId();
82
189
  if (!accountId) {
83
- throw new Error("Wallet not connected. Please connect your wallet first.");
190
+ const error = new Error("Wallet not connected. Please connect your wallet first.") as Error & { code?: string };
191
+ error.code = "WALLET_NOT_CONNECTED";
192
+ throw error;
84
193
  }
85
194
 
86
- const nonceResponse: BetterFetchResponse<NonceResponseT> = await $fetch("/near/nonce", {
87
- method: "POST",
88
- body: { accountId }
89
- });
195
+ // Retrieve nonce from cache
196
+ const nonceData = cachedNonce.get();
197
+
198
+ if (!isNonceValid(nonceData)) {
199
+ const error = new Error("No valid nonce found. Please call requestSignIn first.") as Error & { code?: string };
200
+ error.code = "NONCE_NOT_FOUND";
201
+ throw error;
202
+ }
90
203
 
91
- if (nonceResponse.error) {
92
- throw new Error(nonceResponse.error.message || "Failed to get nonce");
204
+ // Validate that the cached nonce matches the current account
205
+ if (nonceData!.accountId !== accountId) {
206
+ const error = new Error("Account ID mismatch. Please call requestSignIn again.") as Error & { code?: string };
207
+ error.code = "ACCOUNT_MISMATCH";
208
+ throw error;
93
209
  }
94
210
 
95
- const nonce = nonceResponse?.data?.nonce;
211
+ const { nonce } = nonceData!;
212
+
213
+ // Create the sign-in message
96
214
  const message = `Sign in to ${recipient}\n\nAccount ID: ${accountId}\nNonce: ${nonce}`;
97
- const nonceBytes = base64ToBytes(nonce!);
215
+ const nonceBytes = base64ToBytes(nonce);
98
216
 
217
+ // Sign the message
99
218
  const authToken = await sign(message, {
100
- signer,
219
+ signer: nearClient,
101
220
  recipient,
102
221
  nonce: nonceBytes,
103
222
  });
104
223
 
224
+ // Verify the signature with the server
105
225
  const verifyResponse: BetterFetchResponse<VerifyResponseT> = await $fetch("/near/verify", {
106
226
  method: "POST",
107
227
  body: {
@@ -118,9 +238,13 @@ export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
118
238
  throw new Error("Authentication verification failed");
119
239
  }
120
240
 
241
+ // Clear the nonce after successful authentication
242
+ clearNonce();
121
243
  callbacks?.onSuccess?.();
122
244
  } catch (error) {
123
245
  const err = error instanceof Error ? error : new Error(String(error));
246
+ // Clear nonce on error to prevent reuse
247
+ clearNonce();
124
248
  callbacks?.onError?.(err);
125
249
  }
126
250
  }
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@ import { bytesToBase64 } from "@fastnear/utils";
2
2
  import { APIError, createAuthEndpoint, sessionMiddleware } from "better-auth/api";
3
3
  import { setSessionCookie } from "better-auth/cookies";
4
4
  import type { BetterAuthPlugin, User } from "better-auth/types";
5
- import { generateNonce, verify, type VerificationResult, type VerifyOptions } from "near-sign-verify";
5
+ import { generateNonce, parseAuthToken, verify, type VerificationResult, type VerifyOptions } from "near-sign-verify";
6
6
  import { defaultGetProfile, getImageUrl, getNetworkFromAccountId } from "./profile";
7
7
  import { schema } from "./schema";
8
8
  import type {
@@ -73,15 +73,23 @@ export const siwn = (options: SIWNPluginOptions) =>
73
73
  body: NonceRequest,
74
74
  },
75
75
  async (ctx) => {
76
- const { accountId } = ctx.body;
76
+ const { accountId, publicKey, networkId } = ctx.body;
77
77
  const network = getNetworkFromAccountId(accountId);
78
+
79
+ if (networkId !== network) {
80
+ throw new APIError("BAD_REQUEST", {
81
+ message: "Network ID mismatch with account ID",
82
+ status: 400,
83
+ });
84
+ }
85
+
78
86
  const nonce = options.getNonce ? await options.getNonce() : generateNonce();
79
87
 
80
88
  // Store nonce as base64 string for database compatibility
81
89
  const nonceString = bytesToBase64(nonce);
82
90
 
83
91
  await ctx.context.internalAdapter.createVerificationValue({
84
- identifier: `siwn:${accountId}:${network}`,
92
+ identifier: `siwn:${accountId}:${network}:${publicKey}`,
85
93
  value: nonceString!,
86
94
  expiresAt: new Date(Date.now() + 15 * 60 * 1000),
87
95
  });
@@ -158,9 +166,11 @@ export const siwn = (options: SIWNPluginOptions) =>
158
166
  }
159
167
 
160
168
  try {
169
+ const { publicKey } = parseAuthToken(authToken);
170
+
161
171
  const verification =
162
172
  await ctx.context.internalAdapter.findVerificationValue(
163
- `siwn:${accountId}:${network}`,
173
+ `siwn:${accountId}:${network}:${publicKey}`,
164
174
  );
165
175
 
166
176
  if (!verification || new Date() > verification.expiresAt) {
package/src/types.ts CHANGED
@@ -34,7 +34,11 @@ export type SocialImage = z.infer<typeof socialImageSchema>;
34
34
  export type Profile = z.infer<typeof profileSchema>;
35
35
 
36
36
 
37
- export const NonceRequest = z.object({ accountId: accountIdSchema });
37
+ export const NonceRequest = z.object({
38
+ accountId: accountIdSchema,
39
+ publicKey: z.string(),
40
+ networkId: z.union([z.literal("mainnet"), z.literal("testnet")])
41
+ });
38
42
  export const VerifyRequest = z.object({
39
43
  authToken: z.string().min(1),
40
44
  accountId: accountIdSchema,