better-near-auth 0.5.1 → 0.6.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 +219 -257
- package/dist/client.d.ts +17 -6
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +133 -216
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +18 -366
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +668 -436
- package/dist/index.js.map +1 -1
- package/dist/profile.d.ts +1 -1
- package/dist/profile.d.ts.map +1 -1
- package/dist/profile.js +41 -11
- package/dist/profile.js.map +1 -1
- package/dist/rpc.d.ts +64 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +70 -0
- package/dist/rpc.js.map +1 -0
- package/dist/schema.d.ts +72 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +72 -0
- package/dist/schema.js.map +1 -1
- package/dist/types.d.ts +70 -20
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +31 -14
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +26 -4
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +78 -4
- package/dist/utils.js.map +1 -1
- package/package.json +11 -5
package/README.md
CHANGED
|
@@ -8,12 +8,19 @@
|
|
|
8
8
|
<h1 style="font-size: 2.5rem; font-weight: bold;">better-near-auth</h1>
|
|
9
9
|
|
|
10
10
|
<p>
|
|
11
|
-
<strong>Sign in with NEAR
|
|
11
|
+
<strong>Sign in with NEAR + gasless relay plugin for Better Auth</strong>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
</div>
|
|
15
15
|
|
|
16
|
-
This [Better Auth](https://better-auth.com) plugin enables secure authentication via NEAR wallets
|
|
16
|
+
This [Better Auth](https://better-auth.com) plugin enables secure authentication via NEAR wallets following [NEP-413](https://github.com/near/NEPs/blob/master/neps/nep-0413.md) and adds a built-in [NEP-366](https://github.com/near/NEPs/blob/master/neps/nep-0366.md) delegate action relayer so authenticated users can call on-chain contracts gaslessly. It uses [FastNear](https://fastnear.com) for wallet connection, RPC queries, and transaction broadcasting.
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **SIWN authentication** — wallet-based sign-in with automatic single-step/two-step flow detection
|
|
21
|
+
- **Gasless relay** — server relays signed delegate actions on-chain, paying gas from a relayer account
|
|
22
|
+
- **Ephemeral relayer keypair** — auto-generated ED25519 keypair on first startup, private key encrypted with AES-256-GCM in the database, persists across restarts
|
|
23
|
+
- **Profile integration** — FastNear KV primary, NEAR Social fallback
|
|
17
24
|
|
|
18
25
|
## Installation
|
|
19
26
|
|
|
@@ -36,7 +43,12 @@ npm install better-near-auth
|
|
|
36
43
|
plugins: [
|
|
37
44
|
siwn({
|
|
38
45
|
recipient: "myapp.com",
|
|
39
|
-
anonymous: true,
|
|
46
|
+
anonymous: true,
|
|
47
|
+
|
|
48
|
+
// Optional: enable gasless relay
|
|
49
|
+
relayer: {
|
|
50
|
+
whitelistedContracts: ["myapp.near"],
|
|
51
|
+
},
|
|
40
52
|
}),
|
|
41
53
|
],
|
|
42
54
|
});
|
|
@@ -58,7 +70,7 @@ npm install better-near-auth
|
|
|
58
70
|
plugins: [
|
|
59
71
|
siwnClient({
|
|
60
72
|
recipient: "myapp.com",
|
|
61
|
-
networkId: "mainnet",
|
|
73
|
+
networkId: "mainnet",
|
|
62
74
|
})
|
|
63
75
|
],
|
|
64
76
|
});
|
|
@@ -66,9 +78,9 @@ npm install better-near-auth
|
|
|
66
78
|
|
|
67
79
|
## Usage
|
|
68
80
|
|
|
69
|
-
###
|
|
81
|
+
### Sign In with NEAR
|
|
70
82
|
|
|
71
|
-
The
|
|
83
|
+
The `signIn.near()` method automatically detects wallet capabilities and uses the best available flow:
|
|
72
84
|
|
|
73
85
|
```tsx title="LoginButton.tsx"
|
|
74
86
|
import { authClient } from "./auth-client";
|
|
@@ -89,11 +101,9 @@ export function LoginButton() {
|
|
|
89
101
|
|
|
90
102
|
const handleSignIn = async () => {
|
|
91
103
|
setIsSigningIn(true);
|
|
92
|
-
|
|
93
104
|
await authClient.signIn.near({
|
|
94
105
|
onSuccess: () => {
|
|
95
106
|
setIsSigningIn(false);
|
|
96
|
-
console.log("Successfully signed in!");
|
|
97
107
|
},
|
|
98
108
|
onError: (error) => {
|
|
99
109
|
setIsSigningIn(false);
|
|
@@ -110,83 +120,59 @@ export function LoginButton() {
|
|
|
110
120
|
}
|
|
111
121
|
```
|
|
112
122
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
The `signIn.near()` method automatically:
|
|
116
|
-
|
|
117
|
-
1. **Checks wallet capabilities** - Detects if the wallet supports `signInAndSignMessage`
|
|
118
|
-
2. **Single-step flow** (supported wallets): One popup for connection + signing
|
|
119
|
-
3. **Two-step fallback** (unsupported wallets): Automatic fallback to connect then sign
|
|
120
|
-
|
|
121
|
-
**Supported wallets for single-step:**
|
|
122
|
-
- Meteor Wallet
|
|
123
|
-
- Intear Wallet
|
|
124
|
-
- NEAR CLI
|
|
125
|
-
- HOT Wallet
|
|
126
|
-
- MyNearWallet
|
|
127
|
-
- And more...
|
|
123
|
+
**Supported wallets for single-step flow:** Meteor Wallet, Intear Wallet, NEAR CLI, HOT Wallet, MyNearWallet, and more.
|
|
128
124
|
|
|
129
125
|
### Manual Two-Step Flow (Optional)
|
|
130
126
|
|
|
131
|
-
If you need explicit control over the connection step:
|
|
132
|
-
|
|
133
127
|
```ts
|
|
134
|
-
// Step 1: Connect wallet (optional, for explicit control)
|
|
135
128
|
await authClient.requestSignIn.near({
|
|
136
129
|
onSuccess: () => console.log("Wallet connected"),
|
|
137
130
|
});
|
|
138
131
|
|
|
139
|
-
// Step 2: Sign and authenticate
|
|
140
132
|
await authClient.signIn.near({
|
|
141
133
|
onSuccess: () => console.log("Signed in!"),
|
|
142
134
|
});
|
|
143
135
|
```
|
|
144
|
-
setIsSigningIn(false);
|
|
145
|
-
console.error("Sign in failed:", error.message);
|
|
146
|
-
},
|
|
147
|
-
}
|
|
148
|
-
);
|
|
149
|
-
} catch (error) {
|
|
150
|
-
setIsSigningIn(false);
|
|
151
|
-
console.error("Authentication error:", error);
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
136
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
137
|
+
### Gasless Relay
|
|
138
|
+
|
|
139
|
+
Once the relayer is configured on the server, authenticated users can call on-chain contracts without paying gas:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
// 1. Build a signed delegate action using the wallet's FAK
|
|
143
|
+
const signedAction = await authClient.near.buildSignedDelegateAction({
|
|
144
|
+
receiverId: "myapp.near",
|
|
145
|
+
actions: [{
|
|
146
|
+
type: "FunctionCall",
|
|
147
|
+
methodName: "some_method",
|
|
148
|
+
args: new TextEncoder().encode(JSON.stringify({ key: "value" })),
|
|
149
|
+
gas: "30000000000000",
|
|
150
|
+
deposit: "0",
|
|
151
|
+
}],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// 2. Relay it — the server pays gas
|
|
155
|
+
const result = await authClient.near.relayTransaction({
|
|
156
|
+
signedDelegateAction: signedAction,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
console.log("Tx hash:", result.txHash);
|
|
160
|
+
|
|
161
|
+
// 3. Check status
|
|
162
|
+
const status = await authClient.near.getRelayStatus(result.txHash);
|
|
163
163
|
```
|
|
164
164
|
|
|
165
165
|
### Profile Access
|
|
166
166
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
```ts title="profile-usage.ts"
|
|
170
|
-
// Get current user's profile (requires authentication)
|
|
167
|
+
```ts
|
|
171
168
|
const myProfile = await authClient.near.getProfile();
|
|
172
|
-
console.log("My profile:", myProfile);
|
|
173
|
-
|
|
174
|
-
// Get specific user's profile (no auth required)
|
|
175
169
|
const aliceProfile = await authClient.near.getProfile("alice.near");
|
|
176
|
-
console.log("Alice's profile:", aliceProfile);
|
|
177
170
|
```
|
|
178
171
|
|
|
179
172
|
### Wallet Management
|
|
180
173
|
|
|
181
|
-
```ts
|
|
182
|
-
// Check if wallet is connected
|
|
174
|
+
```ts
|
|
183
175
|
const accountId = authClient.near.getAccountId();
|
|
184
|
-
console.log("Connected account:", accountId);
|
|
185
|
-
|
|
186
|
-
// Get the embedded NEAR client
|
|
187
|
-
const nearClient = authClient.near.getNearClient();
|
|
188
|
-
|
|
189
|
-
// Disconnect wallet and clear cached data
|
|
190
176
|
await authClient.near.disconnect();
|
|
191
177
|
```
|
|
192
178
|
|
|
@@ -194,79 +180,105 @@ await authClient.near.disconnect();
|
|
|
194
180
|
|
|
195
181
|
### Server Options
|
|
196
182
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
183
|
+
| Option | Type | Default | Description |
|
|
184
|
+
|---|---|---|---|
|
|
185
|
+
| `recipient` | `string` | — | NEP-413 recipient identifier (required) |
|
|
186
|
+
| `anonymous` | `boolean` | `true` | Allow anonymous sign-ins |
|
|
187
|
+
| `emailDomainName` | `string` | recipient | Email domain for non-anonymous accounts |
|
|
188
|
+
| `requireFullAccessKey` | `boolean` | `true` | Require full access keys |
|
|
189
|
+
| `getNonce` | `() => Promise<Uint8Array>` | — | Custom nonce generation |
|
|
190
|
+
| `validateNonce` | `(nonce: Uint8Array) => boolean` | — | Custom nonce validation |
|
|
191
|
+
| `validateRecipient` | `(recipient: string) => boolean` | — | Custom recipient validation |
|
|
192
|
+
| `validateMessage` | `(message: string) => boolean` | — | Custom message validation |
|
|
193
|
+
| `getProfile` | `(accountId: string) => Promise<Profile \| null>` | — | Custom profile lookup |
|
|
194
|
+
| `validateLimitedAccessKey` | `(args) => Promise<boolean>` | — | Validate FAK when `requireFullAccessKey` is false |
|
|
195
|
+
| `fastnearApiKey` | `string` | `process.env.FASTNEAR_API_KEY` | FastNear API key |
|
|
196
|
+
| `relayer` | `RelayerConfig` | — | Relayer configuration (see below) |
|
|
197
|
+
|
|
198
|
+
#### Relayer Configuration
|
|
199
|
+
|
|
200
|
+
| Option | Type | Default | Description |
|
|
201
|
+
|---|---|---|---|
|
|
202
|
+
| `accountId` | `string` | — | Named relayer account (explicit mode) |
|
|
203
|
+
| `privateKey` | `string` | — | Base64 private key (explicit mode) |
|
|
204
|
+
| `relayTarget` | `string` | FastNear RPC | RPC URL for broadcasting |
|
|
205
|
+
| `whitelistedContracts` | `string[]` | — | Restrict relay to these contracts |
|
|
206
|
+
| `maxGasPerTransaction` | `string` | `"300 Tgas"` | Max gas per relayed tx |
|
|
207
|
+
| `maxDepositPerTransaction` | `string` | `"0"` | Max deposit per relayed tx |
|
|
208
|
+
|
|
209
|
+
When `accountId` and `privateKey` are omitted, the relayer starts in **ephemeral mode**: an ED25519 keypair is generated on first startup, the implicit account ID is derived from the public key, and the private key is encrypted with AES-256-GCM (using `BETTER_AUTH_SECRET` as KEK via HKDF-SHA256) and stored in the database. The same keypair is recovered on restart.
|
|
209
210
|
|
|
210
211
|
### Client Options
|
|
211
212
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
```ts title="auth-client.ts"
|
|
218
|
-
import { createAuthClient } from "better-auth/client";
|
|
219
|
-
import { siwnClient } from "better-near-auth/client";
|
|
220
|
-
|
|
221
|
-
export const authClient = createAuthClient({
|
|
222
|
-
plugins: [
|
|
223
|
-
siwnClient({
|
|
224
|
-
recipient: "myapp.com",
|
|
225
|
-
networkId: "testnet", // Use testnet
|
|
226
|
-
}),
|
|
227
|
-
],
|
|
228
|
-
});
|
|
229
|
-
```
|
|
213
|
+
| Option | Type | Default | Description |
|
|
214
|
+
|---|---|---|---|
|
|
215
|
+
| `recipient` | `string` | — | NEP-413 recipient (must match server) |
|
|
216
|
+
| `networkId` | `string` | `"mainnet"` | NEAR network |
|
|
230
217
|
|
|
231
218
|
## Schema
|
|
232
219
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
| Field
|
|
236
|
-
|
|
237
|
-
| id
|
|
238
|
-
| userId
|
|
239
|
-
| accountId | string
|
|
240
|
-
| network
|
|
241
|
-
| publicKey | string
|
|
242
|
-
| isPrimary | boolean |
|
|
243
|
-
| createdAt | date
|
|
220
|
+
### nearAccount
|
|
221
|
+
|
|
222
|
+
| Field | Type | Description |
|
|
223
|
+
|---|---|---|
|
|
224
|
+
| id | string | Primary key |
|
|
225
|
+
| userId | string | → user.id |
|
|
226
|
+
| accountId | string | NEAR account ID |
|
|
227
|
+
| network | string | mainnet/testnet |
|
|
228
|
+
| publicKey | string | Associated public key |
|
|
229
|
+
| isPrimary | boolean | User's primary account |
|
|
230
|
+
| createdAt | date | |
|
|
231
|
+
|
|
232
|
+
### relayedTransaction
|
|
233
|
+
|
|
234
|
+
| Field | Type | Description |
|
|
235
|
+
|---|---|---|
|
|
236
|
+
| userId | string | → user.id |
|
|
237
|
+
| txHash | string | On-chain tx hash |
|
|
238
|
+
| senderId | string | Delegate action sender |
|
|
239
|
+
| receiverId | string | Contract called |
|
|
240
|
+
| status | string | pending/completed/failed |
|
|
241
|
+
| gasUsed | string | Gas consumed |
|
|
242
|
+
| createdAt | date | |
|
|
243
|
+
|
|
244
|
+
### relayerKey
|
|
245
|
+
|
|
246
|
+
| Field | Type | Description |
|
|
247
|
+
|---|---|---|
|
|
248
|
+
| id | string | Singleton per network |
|
|
249
|
+
| accountId | string | Implicit NEAR account ID |
|
|
250
|
+
| encryptedPrivateKey | string | AES-256-GCM encrypted, base64 |
|
|
251
|
+
| iv | string | Initialization vector, base64 |
|
|
252
|
+
| publicKey | string | ed25519:base64 format |
|
|
253
|
+
| network | string | mainnet/testnet |
|
|
254
|
+
| createdAt | date | |
|
|
255
|
+
| lastUsedAt | date | Updated on each relay |
|
|
244
256
|
|
|
245
257
|
## API Reference
|
|
246
258
|
|
|
247
|
-
### Client Actions
|
|
259
|
+
### Client Actions — `authClient.near`
|
|
248
260
|
|
|
249
|
-
|
|
261
|
+
**SIWN**
|
|
262
|
+
- `nonce(params)` — Request a nonce from the server
|
|
263
|
+
- `verify(params)` — Verify an auth token with the server
|
|
264
|
+
- `getProfile(accountId?)` — Get user profile (FastNear KV → NEAR Social fallback)
|
|
265
|
+
- `getAccountId()` — Currently connected account ID
|
|
266
|
+
- `getState()` — Current wallet state
|
|
267
|
+
- `disconnect()` — Disconnect wallet and clear cached data
|
|
268
|
+
- `link(callbacks?)` — Link a NEAR account to the current session
|
|
269
|
+
- `unlink(params)` — Unlink a NEAR account
|
|
270
|
+
- `listAccounts()` — List all linked NEAR accounts
|
|
250
271
|
|
|
251
|
-
|
|
272
|
+
**Relay**
|
|
273
|
+
- `buildSignedDelegateAction({ receiverId, actions })` — Build + sign a delegate action via wallet FAK
|
|
274
|
+
- `relayTransaction({ signedDelegateAction })` — Submit a signed delegate action to the relayer
|
|
275
|
+
- `getRelayStatus(txHash)` — Check relayed transaction status
|
|
252
276
|
|
|
253
|
-
|
|
254
|
-
- `
|
|
255
|
-
- `getProfile(accountId?)` - Get user profile from NEAR Social
|
|
256
|
-
- `getNearClient()` - Get the near-kit client instance
|
|
257
|
-
- `getAccountId()` - Get the currently connected account ID
|
|
258
|
-
- `disconnect()` - Disconnect wallet and clear cached data
|
|
259
|
-
- `link(callbacks?)` - Link a NEAR account to the current session
|
|
260
|
-
- `unlink(params)` - Unlink a NEAR account from the current session
|
|
261
|
-
- `listAccounts()` - List all linked NEAR accounts
|
|
277
|
+
### `authClient.requestSignIn`
|
|
278
|
+
- `near(callbacks?)` — Connect wallet and cache nonce (two-step flow)
|
|
262
279
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
- `near(callbacks?)` - Connect wallet and cache nonce (for two-step flow)
|
|
266
|
-
|
|
267
|
-
#### `authClient.signIn`
|
|
268
|
-
|
|
269
|
-
- `near(callbacks?)` - Sign message and authenticate (single-step or two-step)
|
|
280
|
+
### `authClient.signIn`
|
|
281
|
+
- `near(callbacks?)` — Sign message and authenticate (single-step or two-step)
|
|
270
282
|
|
|
271
283
|
### Callback Interface
|
|
272
284
|
|
|
@@ -279,18 +291,30 @@ interface AuthCallbacks {
|
|
|
279
291
|
|
|
280
292
|
### Error Codes
|
|
281
293
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
294
|
+
| Code | Description |
|
|
295
|
+
|---|---|
|
|
296
|
+
| `SIGNER_NOT_AVAILABLE` | NEAR wallet not available |
|
|
297
|
+
| `WALLET_NOT_CONNECTED` | Wallet not connected before signing |
|
|
298
|
+
| `ACCOUNT_MISMATCH` | Cached nonce doesn't match current account |
|
|
299
|
+
| `UNAUTHORIZED_NONCE_REPLAY` | Nonce already used |
|
|
300
|
+
| `UNAUTHORIZED_INVALID_SIGNATURE` | Invalid signature verification |
|
|
301
|
+
|
|
302
|
+
### Server Endpoints
|
|
303
|
+
|
|
304
|
+
| Method | Path | Description |
|
|
305
|
+
|---|---|---|
|
|
306
|
+
| POST | `/near/nonce` | Generate nonce for signing |
|
|
307
|
+
| POST | `/near/verify` | Verify NEP-413 signature, create session |
|
|
308
|
+
| POST | `/near/profile` | Get NEAR profile |
|
|
309
|
+
| POST | `/near/link-account` | Link NEAR account to session |
|
|
310
|
+
| POST | `/near/unlink-account` | Unlink NEAR account |
|
|
311
|
+
| GET | `/near/list-accounts` | List linked NEAR accounts |
|
|
312
|
+
| POST | `/near/relay` | Relay a signed delegate action on-chain |
|
|
313
|
+
| GET | `/near/relay-status/:txHash` | Check relayed transaction status |
|
|
314
|
+
| GET | `/near/relayer-info` | Get relayer accountId, mode, balance |
|
|
289
315
|
|
|
290
316
|
## Advanced Configuration
|
|
291
317
|
|
|
292
|
-
For advanced use cases, you can customize the validation functions:
|
|
293
|
-
|
|
294
318
|
```ts title="advanced-auth.ts"
|
|
295
319
|
import { betterAuth } from "better-auth";
|
|
296
320
|
import { siwn } from "better-near-auth";
|
|
@@ -302,60 +326,51 @@ export const auth = betterAuth({
|
|
|
302
326
|
plugins: [
|
|
303
327
|
siwn({
|
|
304
328
|
recipient: "myapp.com",
|
|
305
|
-
anonymous: false,
|
|
329
|
+
anonymous: false,
|
|
306
330
|
emailDomainName: "myapp.com",
|
|
307
|
-
requireFullAccessKey: false,
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return generateNonce();
|
|
312
|
-
},
|
|
313
|
-
|
|
314
|
-
// Custom nonce validation (prevents replay attacks)
|
|
331
|
+
requireFullAccessKey: false,
|
|
332
|
+
|
|
333
|
+
getNonce: async () => generateNonce(),
|
|
334
|
+
|
|
315
335
|
validateNonce: (nonce: Uint8Array) => {
|
|
316
336
|
const nonceHex = Array.from(nonce).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
317
|
-
if (usedNonces.has(nonceHex))
|
|
318
|
-
return false; // Prevent replay attacks
|
|
319
|
-
}
|
|
337
|
+
if (usedNonces.has(nonceHex)) return false;
|
|
320
338
|
usedNonces.add(nonceHex);
|
|
321
339
|
return true;
|
|
322
340
|
},
|
|
323
|
-
|
|
324
|
-
// Custom recipient validation (allow multiple domains)
|
|
341
|
+
|
|
325
342
|
validateRecipient: (recipient: string) => {
|
|
326
|
-
|
|
327
|
-
return allowedRecipients.includes(recipient);
|
|
343
|
+
return ["myapp.com", "staging.myapp.com"].includes(recipient);
|
|
328
344
|
},
|
|
329
|
-
|
|
330
|
-
// Custom message validation
|
|
345
|
+
|
|
331
346
|
validateMessage: (message: string) => {
|
|
332
|
-
// Add custom message format validation
|
|
333
347
|
return message.includes("Sign in to") && message.length > 10;
|
|
334
348
|
},
|
|
335
|
-
|
|
336
|
-
// Custom profile lookup
|
|
349
|
+
|
|
337
350
|
getProfile: async (accountId) => {
|
|
338
|
-
// Custom profile logic, falls back to NEAR Social
|
|
339
351
|
try {
|
|
340
|
-
const
|
|
341
|
-
if (
|
|
342
|
-
const
|
|
343
|
-
return {
|
|
344
|
-
name: customProfile.displayName,
|
|
345
|
-
description: customProfile.bio,
|
|
346
|
-
image: { url: customProfile.avatar },
|
|
347
|
-
};
|
|
352
|
+
const res = await fetch(`https://api.myapp.com/profiles/${accountId}`);
|
|
353
|
+
if (res.ok) {
|
|
354
|
+
const p = await res.json();
|
|
355
|
+
return { name: p.displayName, description: p.bio, image: { url: p.avatar } };
|
|
348
356
|
}
|
|
349
|
-
} catch
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
return null; // Use default NEAR Social lookup
|
|
357
|
+
} catch {}
|
|
358
|
+
return null;
|
|
353
359
|
},
|
|
354
|
-
|
|
355
|
-
// Validate function call keys against allowed contracts
|
|
360
|
+
|
|
356
361
|
validateLimitedAccessKey: async ({ accountId, publicKey, recipient }) => {
|
|
357
|
-
const
|
|
358
|
-
return recipient ?
|
|
362
|
+
const allowed = ["myapp.near", "social.near"];
|
|
363
|
+
return recipient ? allowed.includes(recipient) : true;
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
fastnearApiKey: process.env.FASTNEAR_API_KEY,
|
|
367
|
+
|
|
368
|
+
relayer: {
|
|
369
|
+
accountId: "relayer.myapp.near",
|
|
370
|
+
privateKey: process.env.RELAYER_PRIVATE_KEY,
|
|
371
|
+
whitelistedContracts: ["myapp.near"],
|
|
372
|
+
maxGasPerTransaction: "300000000000000",
|
|
373
|
+
maxDepositPerTransaction: "0",
|
|
359
374
|
},
|
|
360
375
|
}),
|
|
361
376
|
],
|
|
@@ -364,80 +379,51 @@ export const auth = betterAuth({
|
|
|
364
379
|
|
|
365
380
|
## Network Support
|
|
366
381
|
|
|
367
|
-
The plugin
|
|
382
|
+
The plugin detects the network from the account ID:
|
|
368
383
|
|
|
369
|
-
- Accounts ending with `.testnet`
|
|
370
|
-
- All other accounts
|
|
384
|
+
- Accounts ending with `.testnet` → testnet
|
|
385
|
+
- All other accounts → mainnet
|
|
371
386
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
```ts title="testnet-config.ts"
|
|
375
|
-
export const authClient = createAuthClient({
|
|
376
|
-
plugins: [
|
|
377
|
-
siwnClient({
|
|
378
|
-
domain: "myapp.com",
|
|
379
|
-
networkId: "testnet", // Use testnet
|
|
380
|
-
}),
|
|
381
|
-
],
|
|
382
|
-
});
|
|
383
|
-
```
|
|
384
|
-
|
|
385
|
-
## Security Features
|
|
387
|
+
## Security
|
|
386
388
|
|
|
387
389
|
### NEP-413 Compliance
|
|
388
|
-
-
|
|
389
|
-
-
|
|
390
|
-
-
|
|
390
|
+
- Proper nonce handling prevents replay attacks
|
|
391
|
+
- Message format and recipient validation
|
|
392
|
+
- 15-minute server-side nonce expiration, 5-minute client-side cache
|
|
391
393
|
|
|
392
|
-
###
|
|
393
|
-
-
|
|
394
|
-
-
|
|
395
|
-
-
|
|
396
|
-
-
|
|
394
|
+
### Relayer Key Security
|
|
395
|
+
- Ephemeral private key encrypted at rest with AES-256-GCM
|
|
396
|
+
- KEK derived from `BETTER_AUTH_SECRET` via HKDF-SHA256
|
|
397
|
+
- Private key held only in process memory — never in env vars or config files
|
|
398
|
+
- Trust model matches Better Auth session tokens: DB access + secret = full access
|
|
397
399
|
|
|
398
400
|
### Access Key Support
|
|
399
|
-
-
|
|
401
|
+
- Full access keys and function-call access keys (FAK)
|
|
402
|
+
- FAK scoped to recipient contract for delegate actions
|
|
400
403
|
- Configurable validation for limited access keys
|
|
401
|
-
- Contract-specific access control when using function call keys
|
|
402
404
|
|
|
403
405
|
## Troubleshooting
|
|
404
406
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
3. **"Invalid or expired nonce"**
|
|
416
|
-
- Server nonces expire after 15 minutes
|
|
417
|
-
- Ensure client and server clocks are synchronized
|
|
418
|
-
|
|
419
|
-
4. **"Account ID mismatch"**
|
|
420
|
-
- Verify the signed message contains the correct account ID
|
|
421
|
-
- Check for wallet switching between the two authentication steps
|
|
422
|
-
|
|
423
|
-
5. **"Network ID mismatch"**
|
|
424
|
-
- Ensure the networkId sent to the server matches the account's network
|
|
425
|
-
- Testnet accounts must use "testnet", mainnet accounts use "mainnet"
|
|
426
|
-
|
|
407
|
+
| Issue | Solution |
|
|
408
|
+
|---|---|
|
|
409
|
+
| "Wallet not connected" | Call `requestSignIn.near()` before `signIn.near()` |
|
|
410
|
+
| "No valid nonce found" | Ensure `requestSignIn.near()` completed; client nonces expire after 5 min |
|
|
411
|
+
| "Invalid or expired nonce" | Server nonces expire after 15 min; check clock sync |
|
|
412
|
+
| "Account ID mismatch" | Verify signed message account ID matches wallet |
|
|
413
|
+
| "Network ID mismatch" | Ensure networkId matches the account's network |
|
|
414
|
+
| Relay fails with "insufficient balance" | Fund the relayer account with NEAR |
|
|
415
|
+
| Relay fails with "contract not whitelisted" | Add `receiverId` to `whitelistedContracts` |
|
|
427
416
|
|
|
428
417
|
## Examples
|
|
429
418
|
|
|
430
|
-
This repository includes example applications demonstrating how to use better-near-auth:
|
|
431
|
-
|
|
432
419
|
### Browser to Server Example
|
|
433
420
|
|
|
434
|
-
A full-stack example showing NEAR authentication
|
|
421
|
+
A full-stack example showing NEAR authentication + gasless relay.
|
|
435
422
|
|
|
436
423
|
- **Location**: `examples/browser-2-server/`
|
|
437
424
|
- **Live Demo**: [better-near-auth.near.page](https://better-near-auth.near.page)
|
|
438
425
|
- **Tech Stack**: Hono, Drizzle ORM, React, TanStack Router
|
|
439
426
|
|
|
440
|
-
**Running locally:**
|
|
441
427
|
```bash
|
|
442
428
|
# From repo root
|
|
443
429
|
pnpm install
|
|
@@ -445,54 +431,30 @@ cd examples/browser-2-server
|
|
|
445
431
|
pnpm dev
|
|
446
432
|
```
|
|
447
433
|
|
|
448
|
-
**Deployment:**
|
|
449
|
-
Each example can be deployed independently to Railway or other platforms. See the example's README for deployment instructions.
|
|
450
|
-
|
|
451
434
|
## Development
|
|
452
435
|
|
|
453
|
-
Interested in contributing? See [CONTRIBUTING.md](./CONTRIBUTING.md)
|
|
454
|
-
- Development setup
|
|
455
|
-
- How to add changesets
|
|
456
|
-
- Pull request guidelines
|
|
457
|
-
- Release process
|
|
436
|
+
Interested in contributing? See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
|
458
437
|
|
|
459
438
|
**Quick start:**
|
|
460
439
|
```bash
|
|
461
|
-
# Install dependencies
|
|
462
440
|
pnpm install
|
|
463
|
-
|
|
464
|
-
# Build the package
|
|
465
441
|
pnpm build
|
|
466
|
-
|
|
467
|
-
# Run type checking
|
|
468
442
|
pnpm typecheck
|
|
469
|
-
|
|
470
|
-
# Run tests
|
|
471
443
|
pnpm test
|
|
472
|
-
|
|
473
|
-
# Run example locally
|
|
474
|
-
cd examples/browser-2-server && pnpm dev
|
|
475
444
|
```
|
|
476
445
|
|
|
477
446
|
**Build output:**
|
|
478
|
-
- `dist/index.js`
|
|
479
|
-
- `dist/client.js`
|
|
480
|
-
- `dist/*.d.ts`
|
|
481
|
-
|
|
482
|
-
**Deployment workflow:**
|
|
483
|
-
1. Make changes and create changeset
|
|
484
|
-
2. Merge PR → Changesets publishes to npm
|
|
485
|
-
3. Example auto-updates to use published version
|
|
486
|
-
4. Railway auto-deploys
|
|
487
|
-
5. Example reverts to `workspace:*` for next development cycle
|
|
447
|
+
- `dist/index.js` — Server plugin (ESM)
|
|
448
|
+
- `dist/client.js` — Client plugin (ESM)
|
|
449
|
+
- `dist/*.d.ts` — TypeScript declarations
|
|
488
450
|
|
|
489
451
|
## Links
|
|
490
452
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
453
|
+
- [Better Auth Documentation](https://better-auth.com)
|
|
454
|
+
- [NEAR Protocol](https://near.org)
|
|
455
|
+
- [NEP-413 Specification](https://github.com/near/NEPs/blob/master/neps/nep-0413.md)
|
|
456
|
+
- [NEP-366 Delegate Actions](https://github.com/near/NEPs/blob/master/neps/nep-0366.md)
|
|
457
|
+
- [FastNear](https://fastnear.com)
|
|
458
|
+
- [near-sign-verify](https://github.com/elliotBraem/near-sign-verify)
|
|
459
|
+
- [Example Implementation](https://better-near-auth.near.page)
|
|
460
|
+
- [Contributing Guide](./CONTRIBUTING.md)
|