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