better-near-auth 0.1.0 → 0.1.2
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/LICENSE +21 -0
- package/README.md +9 -7
- package/package.json +2 -2
- package/src/client.ts +14 -11
- package/src/index.ts +9 -8
- package/src/near.test.ts +1 -1
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Elliot Braem
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
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), and provides a complete drop-in solution with 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) and [fastintear](https://github.com/elliotBraem/fastintear), and provides a complete drop-in solution with session management, secure defaults, and automatic profile integration.
|
|
17
17
|
|
|
18
18
|
## Installation
|
|
19
19
|
|
|
@@ -30,6 +30,9 @@ npm install better-near-auth
|
|
|
30
30
|
import { siwn } from "better-near-auth";
|
|
31
31
|
|
|
32
32
|
export const auth = betterAuth({
|
|
33
|
+
database: drizzleAdapter(db, {
|
|
34
|
+
// db configuration
|
|
35
|
+
}),
|
|
33
36
|
plugins: [
|
|
34
37
|
siwn({
|
|
35
38
|
recipient: "myapp.com",
|
|
@@ -39,7 +42,7 @@ npm install better-near-auth
|
|
|
39
42
|
});
|
|
40
43
|
```
|
|
41
44
|
|
|
42
|
-
3.
|
|
45
|
+
3. Generate the schema to add the necessary fields and tables to the database.
|
|
43
46
|
|
|
44
47
|
```bash
|
|
45
48
|
npx @better-auth/cli generate
|
|
@@ -85,11 +88,11 @@ Access user profiles from NEAR Social automatically:
|
|
|
85
88
|
|
|
86
89
|
```ts title="profile-usage.ts"
|
|
87
90
|
// Get current user's profile (requires authentication)
|
|
88
|
-
const
|
|
91
|
+
const myProfile = await authClient.near.getProfile();
|
|
89
92
|
console.log("My profile:", myProfile);
|
|
90
93
|
|
|
91
94
|
// Get specific user's profile (no auth required)
|
|
92
|
-
const
|
|
95
|
+
const aliceProfile = await authClient.near.getProfile("alice.near");
|
|
93
96
|
console.log("Alice's profile:", aliceProfile);
|
|
94
97
|
```
|
|
95
98
|
|
|
@@ -108,7 +111,7 @@ The SIWN plugin accepts the following configuration options:
|
|
|
108
111
|
* **validateRecipient**: Function to validate recipients. Optional, uses exact match by default
|
|
109
112
|
* **validateMessage**: Function to validate messages. Optional, no validation by default
|
|
110
113
|
* **getProfile**: Function to fetch user profiles. Optional, uses NEAR Social by default
|
|
111
|
-
* **
|
|
114
|
+
* **validateLimitedAccessKey**: Function to validate function call access keys when `requireFullAccessKey` is false
|
|
112
115
|
|
|
113
116
|
### Client Options
|
|
114
117
|
|
|
@@ -163,7 +166,6 @@ export const auth = betterAuth({
|
|
|
163
166
|
// Optional: Custom profile lookup
|
|
164
167
|
getProfile: async (accountId) => {
|
|
165
168
|
// Custom profile logic, falls back to NEAR Social
|
|
166
|
-
return null; // Use default NEAR Social lookup
|
|
167
169
|
},
|
|
168
170
|
}),
|
|
169
171
|
],
|
|
@@ -293,7 +295,7 @@ export const auth = betterAuth({
|
|
|
293
295
|
},
|
|
294
296
|
|
|
295
297
|
// Validate function call keys against allowed contracts
|
|
296
|
-
|
|
298
|
+
validateLimitedAccessKey: async ({ accountId, publicKey, contractId }) => {
|
|
297
299
|
const allowedContracts = ["myapp.near", "social.near"];
|
|
298
300
|
return contractId ? allowedContracts.includes(contractId) : true;
|
|
299
301
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "better-near-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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,7 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"@better-auth/utils": "^0.2.6",
|
|
47
47
|
"@fastnear/utils": "^0.9.7",
|
|
48
|
-
"fastintear": "
|
|
48
|
+
"fastintear": "^0.2.3",
|
|
49
49
|
"near-sign-verify": "^0.4.3",
|
|
50
50
|
"zod": "^4.0.17"
|
|
51
51
|
},
|
package/src/client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { BetterAuthClientPlugin, BetterFetchOption, BetterFetchResponse } f
|
|
|
3
3
|
import { sign, type WalletInterface } from "near-sign-verify";
|
|
4
4
|
import type { siwn } from ".";
|
|
5
5
|
import { type AccountId, type NonceRequestT, type NonceResponseT, type ProfileResponseT, type VerifyRequestT, type VerifyResponseT } from "./types";
|
|
6
|
+
import type { User } from "better-auth";
|
|
6
7
|
|
|
7
8
|
export interface Signer {
|
|
8
9
|
accountId(): string | null;
|
|
@@ -25,7 +26,7 @@ export interface SIWNClientActions {
|
|
|
25
26
|
getProfile: (accountId?: AccountId) => Promise<BetterFetchResponse<ProfileResponseT>>;
|
|
26
27
|
};
|
|
27
28
|
signIn: {
|
|
28
|
-
near: (params: { recipient: string, signer: Signer }, callbacks?: AuthCallbacks) => Promise<
|
|
29
|
+
near: (params: { recipient: string, signer: Signer }, callbacks?: AuthCallbacks) => Promise<void>;
|
|
29
30
|
};
|
|
30
31
|
}
|
|
31
32
|
|
|
@@ -66,7 +67,10 @@ export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
|
|
|
66
67
|
},
|
|
67
68
|
},
|
|
68
69
|
signIn: {
|
|
69
|
-
near: async (
|
|
70
|
+
near: async (
|
|
71
|
+
params: { recipient: string, signer: Signer },
|
|
72
|
+
callbacks?: AuthCallbacks
|
|
73
|
+
): Promise<void> => {
|
|
70
74
|
try {
|
|
71
75
|
const { signer, recipient } = params;
|
|
72
76
|
|
|
@@ -74,32 +78,30 @@ export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
|
|
|
74
78
|
throw new Error("NEAR signer not available");
|
|
75
79
|
}
|
|
76
80
|
|
|
77
|
-
// Must be already connected
|
|
78
81
|
const accountId = signer.accountId();
|
|
79
82
|
if (!accountId) {
|
|
80
83
|
throw new Error("Wallet not connected. Please connect your wallet first.");
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
// Get nonce for signature
|
|
84
86
|
const nonceResponse: BetterFetchResponse<NonceResponseT> = await $fetch("/near/nonce", {
|
|
85
87
|
method: "POST",
|
|
86
88
|
body: { accountId }
|
|
87
89
|
});
|
|
88
90
|
|
|
91
|
+
if (nonceResponse.error) {
|
|
92
|
+
throw new Error(nonceResponse.error.message || "Failed to get nonce");
|
|
93
|
+
}
|
|
94
|
+
|
|
89
95
|
const nonce = nonceResponse?.data?.nonce;
|
|
90
96
|
const message = `Sign in to ${recipient}\n\nAccount ID: ${accountId}\nNonce: ${nonce}`;
|
|
91
|
-
|
|
92
|
-
// Convert base64 nonce to Uint8Array for signing
|
|
93
97
|
const nonceBytes = base64ToBytes(nonce!);
|
|
94
98
|
|
|
95
|
-
// Sign message
|
|
96
99
|
const authToken = await sign(message, {
|
|
97
100
|
signer,
|
|
98
101
|
recipient,
|
|
99
102
|
nonce: nonceBytes,
|
|
100
103
|
});
|
|
101
104
|
|
|
102
|
-
// Verify signature with backend
|
|
103
105
|
const verifyResponse: BetterFetchResponse<VerifyResponseT> = await $fetch("/near/verify", {
|
|
104
106
|
method: "POST",
|
|
105
107
|
body: {
|
|
@@ -108,17 +110,18 @@ export const siwnClient = (config: SIWNClientConfig): SIWNClientPlugin => {
|
|
|
108
110
|
}
|
|
109
111
|
});
|
|
110
112
|
|
|
113
|
+
if (verifyResponse.error) {
|
|
114
|
+
throw new Error(verifyResponse.error.message || "Failed to verify signature");
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
if (!verifyResponse?.data?.success) {
|
|
112
118
|
throw new Error("Authentication verification failed");
|
|
113
119
|
}
|
|
114
120
|
|
|
115
121
|
callbacks?.onSuccess?.();
|
|
116
|
-
return verifyResponse.data;
|
|
117
|
-
|
|
118
122
|
} catch (error) {
|
|
119
123
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
120
124
|
callbacks?.onError?.(err);
|
|
121
|
-
throw err;
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
127
|
}
|
package/src/index.ts
CHANGED
|
@@ -38,10 +38,10 @@ export type SIWNPluginOptions =
|
|
|
38
38
|
validateRecipient?: (recipient: string) => boolean;
|
|
39
39
|
validateMessage?: (message: string) => boolean;
|
|
40
40
|
getProfile?: (accountId: AccountId) => Promise<Profile | null>;
|
|
41
|
-
|
|
41
|
+
validateLimitedAccessKey?: (args: {
|
|
42
42
|
accountId: AccountId;
|
|
43
43
|
publicKey: string;
|
|
44
|
-
|
|
44
|
+
recipient?: string;
|
|
45
45
|
}) => Promise<boolean>;
|
|
46
46
|
}
|
|
47
47
|
| {
|
|
@@ -54,10 +54,10 @@ export type SIWNPluginOptions =
|
|
|
54
54
|
validateRecipient?: (recipient: string) => boolean;
|
|
55
55
|
validateMessage?: (message: string) => boolean;
|
|
56
56
|
getProfile?: (accountId: AccountId) => Promise<Profile | null>;
|
|
57
|
-
|
|
57
|
+
validateLimitedAccessKey?: (args: {
|
|
58
58
|
accountId: AccountId;
|
|
59
59
|
publicKey: string;
|
|
60
|
-
|
|
60
|
+
recipient: string;
|
|
61
61
|
}) => Promise<boolean>;
|
|
62
62
|
};
|
|
63
63
|
|
|
@@ -194,13 +194,14 @@ export const siwn = (options: SIWNPluginOptions) =>
|
|
|
194
194
|
});
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
if (!options.requireFullAccessKey && options.
|
|
198
|
-
const
|
|
197
|
+
if (!options.requireFullAccessKey && options.validateLimitedAccessKey) {
|
|
198
|
+
const isValidKey = await options.validateLimitedAccessKey({
|
|
199
199
|
accountId: result.accountId,
|
|
200
200
|
publicKey: result.publicKey,
|
|
201
|
-
|
|
201
|
+
recipient: options.recipient
|
|
202
|
+
}); // we could validate against some access control contract
|
|
202
203
|
|
|
203
|
-
if (!
|
|
204
|
+
if (!isValidKey) {
|
|
204
205
|
throw new APIError("UNAUTHORIZED", {
|
|
205
206
|
message: "Unauthorized: Invalid function call access key",
|
|
206
207
|
status: 401,
|
package/src/near.test.ts
CHANGED
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
// async verifyMessage({ authToken, expectedRecipient, accountId }) {
|
|
157
157
|
// return authToken === "valid_token" && expectedRecipient === domain;
|
|
158
158
|
// },
|
|
159
|
-
// async
|
|
159
|
+
// async validateLimitedAccessKey({ accountId, publicKey }) {
|
|
160
160
|
// return accountId === "test.near" && publicKey !== "";
|
|
161
161
|
// },
|
|
162
162
|
// }),
|