@vitalpoint/near-phantom-auth 0.1.1
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 +250 -0
- package/dist/client/index.cjs +399 -0
- package/dist/client/index.cjs.map +1 -0
- package/dist/client/index.js +391 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index.cjs +4 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/server/index.cjs +1687 -0
- package/dist/server/index.cjs.map +1 -0
- package/dist/server/index.js +1676 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +91 -0
package/README.md
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# near-phantom-auth
|
|
2
|
+
|
|
3
|
+
Anonymous passkey authentication with NEAR MPC accounts and decentralized recovery.
|
|
4
|
+
|
|
5
|
+
> 🔒 **Privacy-first**: No email, no phone, no PII. Just biometrics and blockchain.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Passkey Authentication**: Face ID, Touch ID, Windows Hello - no passwords
|
|
10
|
+
- **NEAR MPC Accounts**: User-owned accounts via Chain Signatures (8-node threshold MPC)
|
|
11
|
+
- **Anonymous Identity**: Codename-based (ALPHA-7, BRAVO-12) - we never know who you are
|
|
12
|
+
- **Decentralized Recovery**:
|
|
13
|
+
- Link a NEAR wallet (on-chain access key, not stored in our DB)
|
|
14
|
+
- Password + IPFS backup (encrypted, you hold the keys)
|
|
15
|
+
- **HttpOnly Sessions**: XSS-proof cookie-based sessions
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @vitalpoint/near-phantom-auth
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### Server (Express)
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
import express from 'express';
|
|
29
|
+
import { createAnonAuth } from '@vitalpoint/near-phantom-auth/server';
|
|
30
|
+
|
|
31
|
+
const app = express();
|
|
32
|
+
|
|
33
|
+
const auth = createAnonAuth({
|
|
34
|
+
nearNetwork: 'testnet',
|
|
35
|
+
sessionSecret: process.env.SESSION_SECRET!,
|
|
36
|
+
database: {
|
|
37
|
+
type: 'postgres',
|
|
38
|
+
connectionString: process.env.DATABASE_URL!,
|
|
39
|
+
},
|
|
40
|
+
rp: {
|
|
41
|
+
name: 'My App',
|
|
42
|
+
id: 'myapp.com',
|
|
43
|
+
origin: 'https://myapp.com',
|
|
44
|
+
},
|
|
45
|
+
recovery: {
|
|
46
|
+
wallet: true,
|
|
47
|
+
ipfs: {
|
|
48
|
+
pinningService: 'pinata',
|
|
49
|
+
apiKey: process.env.PINATA_API_KEY,
|
|
50
|
+
apiSecret: process.env.PINATA_API_SECRET,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Initialize database schema
|
|
56
|
+
await auth.initialize();
|
|
57
|
+
|
|
58
|
+
// Mount auth routes
|
|
59
|
+
app.use('/auth', auth.router);
|
|
60
|
+
|
|
61
|
+
// Protect routes
|
|
62
|
+
app.get('/api/me', auth.requireAuth, (req, res) => {
|
|
63
|
+
res.json({
|
|
64
|
+
codename: req.anonUser!.codename,
|
|
65
|
+
nearAccountId: req.anonUser!.nearAccountId,
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
app.listen(3000);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Client (React)
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
import { AnonAuthProvider, useAnonAuth } from '@vitalpoint/near-phantom-auth/client';
|
|
76
|
+
|
|
77
|
+
function App() {
|
|
78
|
+
return (
|
|
79
|
+
<AnonAuthProvider apiUrl="/auth">
|
|
80
|
+
<AuthDemo />
|
|
81
|
+
</AnonAuthProvider>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function AuthDemo() {
|
|
86
|
+
const {
|
|
87
|
+
isLoading,
|
|
88
|
+
isAuthenticated,
|
|
89
|
+
codename,
|
|
90
|
+
register,
|
|
91
|
+
login,
|
|
92
|
+
logout,
|
|
93
|
+
error,
|
|
94
|
+
} = useAnonAuth();
|
|
95
|
+
|
|
96
|
+
if (isLoading) return <div>Loading...</div>;
|
|
97
|
+
|
|
98
|
+
if (!isAuthenticated) {
|
|
99
|
+
return (
|
|
100
|
+
<div>
|
|
101
|
+
<h1>Anonymous Auth Demo</h1>
|
|
102
|
+
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
103
|
+
<button onClick={register}>Register (Create Identity)</button>
|
|
104
|
+
<button onClick={() => login()}>Sign In (Existing Identity)</button>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<div>
|
|
111
|
+
<h1>Welcome, {codename}</h1>
|
|
112
|
+
<p>You are authenticated anonymously.</p>
|
|
113
|
+
<button onClick={logout}>Sign Out</button>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## How It Works
|
|
120
|
+
|
|
121
|
+
### Registration Flow
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
1. User clicks "Register"
|
|
125
|
+
2. Browser creates passkey (biometric prompt)
|
|
126
|
+
3. Server creates NEAR account via MPC
|
|
127
|
+
4. User gets codename (e.g., ALPHA-7)
|
|
128
|
+
5. Session cookie set (HttpOnly, Secure, SameSite=Strict)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Authentication Flow
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
1. User clicks "Sign In"
|
|
135
|
+
2. Browser prompts for passkey (biometric)
|
|
136
|
+
3. Server verifies signature
|
|
137
|
+
4. Session cookie set
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Recovery Options
|
|
141
|
+
|
|
142
|
+
#### Wallet Recovery
|
|
143
|
+
- User links existing NEAR wallet
|
|
144
|
+
- Wallet added as on-chain access key (NOT stored in our database)
|
|
145
|
+
- Recovery: Sign with wallet → Create new passkey
|
|
146
|
+
|
|
147
|
+
#### Password + IPFS Recovery
|
|
148
|
+
- User sets strong password
|
|
149
|
+
- Recovery data encrypted with password
|
|
150
|
+
- Encrypted blob stored on IPFS
|
|
151
|
+
- User saves: password + IPFS CID
|
|
152
|
+
- Recovery: Provide password + CID → Decrypt → Create new passkey
|
|
153
|
+
|
|
154
|
+
## API Routes
|
|
155
|
+
|
|
156
|
+
| Method | Route | Description |
|
|
157
|
+
|--------|-------|-------------|
|
|
158
|
+
| POST | `/register/start` | Start passkey registration |
|
|
159
|
+
| POST | `/register/finish` | Complete registration |
|
|
160
|
+
| POST | `/login/start` | Start authentication |
|
|
161
|
+
| POST | `/login/finish` | Complete authentication |
|
|
162
|
+
| POST | `/logout` | End session |
|
|
163
|
+
| GET | `/session` | Get current session |
|
|
164
|
+
| POST | `/recovery/wallet/link` | Start wallet linking |
|
|
165
|
+
| POST | `/recovery/wallet/verify` | Complete wallet linking |
|
|
166
|
+
| POST | `/recovery/wallet/start` | Start wallet recovery |
|
|
167
|
+
| POST | `/recovery/wallet/finish` | Complete wallet recovery |
|
|
168
|
+
| POST | `/recovery/ipfs/setup` | Create IPFS backup |
|
|
169
|
+
| POST | `/recovery/ipfs/recover` | Recover from IPFS |
|
|
170
|
+
|
|
171
|
+
## Configuration
|
|
172
|
+
|
|
173
|
+
### Database Adapters
|
|
174
|
+
|
|
175
|
+
Currently supports PostgreSQL. SQLite and custom adapters coming soon.
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
// PostgreSQL
|
|
179
|
+
database: {
|
|
180
|
+
type: 'postgres',
|
|
181
|
+
connectionString: 'postgresql://...',
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Custom adapter
|
|
185
|
+
database: {
|
|
186
|
+
type: 'custom',
|
|
187
|
+
adapter: myCustomAdapter,
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Codename Styles
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
codename: {
|
|
195
|
+
style: 'nato-phonetic', // ALPHA-7, BRAVO-12
|
|
196
|
+
// or
|
|
197
|
+
style: 'animals', // SWIFT-FALCON-42
|
|
198
|
+
// or
|
|
199
|
+
generator: (userId) => `SOURCE-${userId.slice(0, 8)}`,
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Recovery Options
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
recovery: {
|
|
207
|
+
// On-chain wallet recovery
|
|
208
|
+
wallet: true,
|
|
209
|
+
|
|
210
|
+
// IPFS + password recovery
|
|
211
|
+
ipfs: {
|
|
212
|
+
pinningService: 'pinata', // or 'web3storage', 'infura'
|
|
213
|
+
apiKey: '...',
|
|
214
|
+
apiSecret: '...',
|
|
215
|
+
// or custom functions
|
|
216
|
+
customPin: async (data) => cidString,
|
|
217
|
+
customFetch: async (cid) => Uint8Array,
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Security Model
|
|
223
|
+
|
|
224
|
+
### What We Store
|
|
225
|
+
|
|
226
|
+
| Data | Stored? | Location |
|
|
227
|
+
|------|---------|----------|
|
|
228
|
+
| Email | ❌ | - |
|
|
229
|
+
| Phone | ❌ | - |
|
|
230
|
+
| Real name | ❌ | - |
|
|
231
|
+
| IP address | ❌ | - |
|
|
232
|
+
| Codename | ✅ | Database |
|
|
233
|
+
| NEAR account | ✅ | Database + Blockchain |
|
|
234
|
+
| Passkey public key | ✅ | Database |
|
|
235
|
+
| Recovery wallet link | ❌ | On-chain only |
|
|
236
|
+
| IPFS CID | ✅ | Database (encrypted content on IPFS) |
|
|
237
|
+
|
|
238
|
+
### What We Cannot Know
|
|
239
|
+
|
|
240
|
+
- Real identity of users
|
|
241
|
+
- Link between codename and recovery wallet (it's on-chain, not in our DB)
|
|
242
|
+
- Contents of IPFS backup (encrypted with user's password)
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
MIT
|
|
247
|
+
|
|
248
|
+
## Contributing
|
|
249
|
+
|
|
250
|
+
Contributions welcome! Please read our contributing guidelines first.
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
5
|
+
|
|
6
|
+
// src/client/hooks/useAnonAuth.tsx
|
|
7
|
+
|
|
8
|
+
// src/client/api.ts
|
|
9
|
+
function createApiClient(config) {
|
|
10
|
+
const fetchFn = config.fetch || fetch;
|
|
11
|
+
const baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
12
|
+
async function request(method, path, body) {
|
|
13
|
+
const response = await fetchFn(`${baseUrl}${path}`, {
|
|
14
|
+
method,
|
|
15
|
+
headers: {
|
|
16
|
+
"Content-Type": "application/json"
|
|
17
|
+
},
|
|
18
|
+
credentials: "include",
|
|
19
|
+
// Include cookies
|
|
20
|
+
body: body ? JSON.stringify(body) : void 0
|
|
21
|
+
});
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
const error = await response.json().catch(() => ({ error: "Request failed" }));
|
|
24
|
+
throw new Error(error.error || `Request failed: ${response.status}`);
|
|
25
|
+
}
|
|
26
|
+
return response.json();
|
|
27
|
+
}
|
|
28
|
+
return {
|
|
29
|
+
// Registration
|
|
30
|
+
async startRegistration() {
|
|
31
|
+
return request("POST", "/register/start");
|
|
32
|
+
},
|
|
33
|
+
async finishRegistration(challengeId, response, tempUserId, codename) {
|
|
34
|
+
return request("POST", "/register/finish", {
|
|
35
|
+
challengeId,
|
|
36
|
+
response,
|
|
37
|
+
tempUserId,
|
|
38
|
+
codename
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
// Authentication
|
|
42
|
+
async startAuthentication(codename) {
|
|
43
|
+
return request("POST", "/login/start", { codename });
|
|
44
|
+
},
|
|
45
|
+
async finishAuthentication(challengeId, response) {
|
|
46
|
+
return request("POST", "/login/finish", {
|
|
47
|
+
challengeId,
|
|
48
|
+
response
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
// Session
|
|
52
|
+
async getSession() {
|
|
53
|
+
try {
|
|
54
|
+
return await request("GET", "/session");
|
|
55
|
+
} catch {
|
|
56
|
+
return { authenticated: false };
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
async logout() {
|
|
60
|
+
await request("POST", "/logout");
|
|
61
|
+
},
|
|
62
|
+
// Wallet Recovery
|
|
63
|
+
async startWalletLink() {
|
|
64
|
+
return request("POST", "/recovery/wallet/link");
|
|
65
|
+
},
|
|
66
|
+
async finishWalletLink(signature, challenge, walletAccountId) {
|
|
67
|
+
return request("POST", "/recovery/wallet/verify", {
|
|
68
|
+
signature,
|
|
69
|
+
challenge,
|
|
70
|
+
walletAccountId
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
async startWalletRecovery() {
|
|
74
|
+
return request("POST", "/recovery/wallet/start");
|
|
75
|
+
},
|
|
76
|
+
async finishWalletRecovery(signature, challenge, nearAccountId) {
|
|
77
|
+
return request("POST", "/recovery/wallet/finish", {
|
|
78
|
+
signature,
|
|
79
|
+
challenge,
|
|
80
|
+
nearAccountId
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
// IPFS Recovery
|
|
84
|
+
async setupIPFSRecovery(password) {
|
|
85
|
+
return request("POST", "/recovery/ipfs/setup", { password });
|
|
86
|
+
},
|
|
87
|
+
async recoverFromIPFS(cid, password) {
|
|
88
|
+
return request("POST", "/recovery/ipfs/recover", { cid, password });
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// src/client/passkey.ts
|
|
94
|
+
function isWebAuthnSupported() {
|
|
95
|
+
return typeof window !== "undefined" && typeof window.PublicKeyCredential !== "undefined" && typeof window.navigator.credentials !== "undefined";
|
|
96
|
+
}
|
|
97
|
+
async function isPlatformAuthenticatorAvailable() {
|
|
98
|
+
if (!isWebAuthnSupported()) return false;
|
|
99
|
+
try {
|
|
100
|
+
return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function base64urlToBuffer(base64url) {
|
|
106
|
+
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
107
|
+
const padLen = (4 - base64.length % 4) % 4;
|
|
108
|
+
const padded = base64 + "=".repeat(padLen);
|
|
109
|
+
const binary = atob(padded);
|
|
110
|
+
const bytes = new Uint8Array(binary.length);
|
|
111
|
+
for (let i = 0; i < binary.length; i++) {
|
|
112
|
+
bytes[i] = binary.charCodeAt(i);
|
|
113
|
+
}
|
|
114
|
+
return bytes.buffer;
|
|
115
|
+
}
|
|
116
|
+
function bufferToBase64url(buffer) {
|
|
117
|
+
const bytes = new Uint8Array(buffer);
|
|
118
|
+
let binary = "";
|
|
119
|
+
for (const byte of bytes) {
|
|
120
|
+
binary += String.fromCharCode(byte);
|
|
121
|
+
}
|
|
122
|
+
const base64 = btoa(binary);
|
|
123
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
124
|
+
}
|
|
125
|
+
async function createPasskey(options) {
|
|
126
|
+
if (!isWebAuthnSupported()) {
|
|
127
|
+
throw new Error("WebAuthn is not supported in this browser");
|
|
128
|
+
}
|
|
129
|
+
const publicKeyOptions = {
|
|
130
|
+
challenge: base64urlToBuffer(options.challenge),
|
|
131
|
+
rp: options.rp,
|
|
132
|
+
user: {
|
|
133
|
+
id: base64urlToBuffer(options.user.id),
|
|
134
|
+
name: options.user.name,
|
|
135
|
+
displayName: options.user.displayName
|
|
136
|
+
},
|
|
137
|
+
pubKeyCredParams: options.pubKeyCredParams,
|
|
138
|
+
timeout: options.timeout,
|
|
139
|
+
authenticatorSelection: options.authenticatorSelection,
|
|
140
|
+
attestation: options.attestation || "none",
|
|
141
|
+
excludeCredentials: options.excludeCredentials?.map((cred) => ({
|
|
142
|
+
id: base64urlToBuffer(cred.id),
|
|
143
|
+
type: cred.type,
|
|
144
|
+
transports: cred.transports
|
|
145
|
+
}))
|
|
146
|
+
};
|
|
147
|
+
const credential = await navigator.credentials.create({
|
|
148
|
+
publicKey: publicKeyOptions
|
|
149
|
+
});
|
|
150
|
+
if (!credential) {
|
|
151
|
+
throw new Error("Credential creation failed");
|
|
152
|
+
}
|
|
153
|
+
const response = credential.response;
|
|
154
|
+
return {
|
|
155
|
+
id: credential.id,
|
|
156
|
+
rawId: bufferToBase64url(credential.rawId),
|
|
157
|
+
type: "public-key",
|
|
158
|
+
response: {
|
|
159
|
+
clientDataJSON: bufferToBase64url(response.clientDataJSON),
|
|
160
|
+
attestationObject: bufferToBase64url(response.attestationObject),
|
|
161
|
+
transports: response.getTransports?.()
|
|
162
|
+
},
|
|
163
|
+
clientExtensionResults: credential.getClientExtensionResults()
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
async function authenticateWithPasskey(options) {
|
|
167
|
+
if (!isWebAuthnSupported()) {
|
|
168
|
+
throw new Error("WebAuthn is not supported in this browser");
|
|
169
|
+
}
|
|
170
|
+
const publicKeyOptions = {
|
|
171
|
+
challenge: base64urlToBuffer(options.challenge),
|
|
172
|
+
timeout: options.timeout,
|
|
173
|
+
rpId: options.rpId,
|
|
174
|
+
userVerification: options.userVerification,
|
|
175
|
+
allowCredentials: options.allowCredentials?.map((cred) => ({
|
|
176
|
+
id: base64urlToBuffer(cred.id),
|
|
177
|
+
type: cred.type,
|
|
178
|
+
transports: cred.transports
|
|
179
|
+
}))
|
|
180
|
+
};
|
|
181
|
+
const credential = await navigator.credentials.get({
|
|
182
|
+
publicKey: publicKeyOptions
|
|
183
|
+
});
|
|
184
|
+
if (!credential) {
|
|
185
|
+
throw new Error("Authentication failed");
|
|
186
|
+
}
|
|
187
|
+
const response = credential.response;
|
|
188
|
+
return {
|
|
189
|
+
id: credential.id,
|
|
190
|
+
rawId: bufferToBase64url(credential.rawId),
|
|
191
|
+
type: "public-key",
|
|
192
|
+
response: {
|
|
193
|
+
clientDataJSON: bufferToBase64url(response.clientDataJSON),
|
|
194
|
+
authenticatorData: bufferToBase64url(response.authenticatorData),
|
|
195
|
+
signature: bufferToBase64url(response.signature),
|
|
196
|
+
userHandle: response.userHandle ? bufferToBase64url(response.userHandle) : void 0
|
|
197
|
+
},
|
|
198
|
+
clientExtensionResults: credential.getClientExtensionResults()
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
var AnonAuthContext = react.createContext(null);
|
|
202
|
+
function AnonAuthProvider({ apiUrl, children }) {
|
|
203
|
+
const [api] = react.useState(() => createApiClient({ baseUrl: apiUrl }));
|
|
204
|
+
const [state, setState] = react.useState({
|
|
205
|
+
isLoading: true,
|
|
206
|
+
isAuthenticated: false,
|
|
207
|
+
codename: null,
|
|
208
|
+
nearAccountId: null,
|
|
209
|
+
expiresAt: null,
|
|
210
|
+
webAuthnSupported: false,
|
|
211
|
+
platformAuthAvailable: false,
|
|
212
|
+
error: null
|
|
213
|
+
});
|
|
214
|
+
react.useEffect(() => {
|
|
215
|
+
const checkSupport = async () => {
|
|
216
|
+
const webAuthnSupported = isWebAuthnSupported();
|
|
217
|
+
const platformAuthAvailable = await isPlatformAuthenticatorAvailable();
|
|
218
|
+
setState((prev) => ({
|
|
219
|
+
...prev,
|
|
220
|
+
webAuthnSupported,
|
|
221
|
+
platformAuthAvailable
|
|
222
|
+
}));
|
|
223
|
+
};
|
|
224
|
+
checkSupport();
|
|
225
|
+
}, []);
|
|
226
|
+
react.useEffect(() => {
|
|
227
|
+
const checkSession = async () => {
|
|
228
|
+
try {
|
|
229
|
+
const session = await api.getSession();
|
|
230
|
+
setState((prev) => ({
|
|
231
|
+
...prev,
|
|
232
|
+
isLoading: false,
|
|
233
|
+
isAuthenticated: session.authenticated,
|
|
234
|
+
codename: session.codename || null,
|
|
235
|
+
nearAccountId: session.nearAccountId || null,
|
|
236
|
+
expiresAt: session.expiresAt ? new Date(session.expiresAt) : null
|
|
237
|
+
}));
|
|
238
|
+
} catch (error) {
|
|
239
|
+
setState((prev) => ({
|
|
240
|
+
...prev,
|
|
241
|
+
isLoading: false,
|
|
242
|
+
error: error instanceof Error ? error.message : "Session check failed"
|
|
243
|
+
}));
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
checkSession();
|
|
247
|
+
}, [api]);
|
|
248
|
+
const register = react.useCallback(async () => {
|
|
249
|
+
try {
|
|
250
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
251
|
+
const { challengeId, options, tempUserId, codename } = await api.startRegistration();
|
|
252
|
+
const credential = await createPasskey(options);
|
|
253
|
+
const result = await api.finishRegistration(
|
|
254
|
+
challengeId,
|
|
255
|
+
credential,
|
|
256
|
+
tempUserId,
|
|
257
|
+
codename
|
|
258
|
+
);
|
|
259
|
+
if (result.success) {
|
|
260
|
+
setState((prev) => ({
|
|
261
|
+
...prev,
|
|
262
|
+
isLoading: false,
|
|
263
|
+
isAuthenticated: true,
|
|
264
|
+
codename: result.codename,
|
|
265
|
+
nearAccountId: result.nearAccountId
|
|
266
|
+
}));
|
|
267
|
+
} else {
|
|
268
|
+
throw new Error("Registration failed");
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
setState((prev) => ({
|
|
272
|
+
...prev,
|
|
273
|
+
isLoading: false,
|
|
274
|
+
error: error instanceof Error ? error.message : "Registration failed"
|
|
275
|
+
}));
|
|
276
|
+
}
|
|
277
|
+
}, [api]);
|
|
278
|
+
const login = react.useCallback(async (codename) => {
|
|
279
|
+
try {
|
|
280
|
+
setState((prev) => ({ ...prev, isLoading: true, error: null }));
|
|
281
|
+
const { challengeId, options } = await api.startAuthentication(codename);
|
|
282
|
+
const credential = await authenticateWithPasskey(options);
|
|
283
|
+
const result = await api.finishAuthentication(challengeId, credential);
|
|
284
|
+
if (result.success) {
|
|
285
|
+
const session = await api.getSession();
|
|
286
|
+
setState((prev) => ({
|
|
287
|
+
...prev,
|
|
288
|
+
isLoading: false,
|
|
289
|
+
isAuthenticated: true,
|
|
290
|
+
codename: session.codename || result.codename,
|
|
291
|
+
nearAccountId: session.nearAccountId || null,
|
|
292
|
+
expiresAt: session.expiresAt ? new Date(session.expiresAt) : null
|
|
293
|
+
}));
|
|
294
|
+
} else {
|
|
295
|
+
throw new Error("Authentication failed");
|
|
296
|
+
}
|
|
297
|
+
} catch (error) {
|
|
298
|
+
setState((prev) => ({
|
|
299
|
+
...prev,
|
|
300
|
+
isLoading: false,
|
|
301
|
+
error: error instanceof Error ? error.message : "Login failed"
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
}, [api]);
|
|
305
|
+
const logout = react.useCallback(async () => {
|
|
306
|
+
try {
|
|
307
|
+
await api.logout();
|
|
308
|
+
setState((prev) => ({
|
|
309
|
+
...prev,
|
|
310
|
+
isAuthenticated: false,
|
|
311
|
+
codename: null,
|
|
312
|
+
nearAccountId: null,
|
|
313
|
+
expiresAt: null
|
|
314
|
+
}));
|
|
315
|
+
} catch (error) {
|
|
316
|
+
setState((prev) => ({
|
|
317
|
+
...prev,
|
|
318
|
+
error: error instanceof Error ? error.message : "Logout failed"
|
|
319
|
+
}));
|
|
320
|
+
}
|
|
321
|
+
}, [api]);
|
|
322
|
+
const refreshSession = react.useCallback(async () => {
|
|
323
|
+
try {
|
|
324
|
+
const session = await api.getSession();
|
|
325
|
+
setState((prev) => ({
|
|
326
|
+
...prev,
|
|
327
|
+
isAuthenticated: session.authenticated,
|
|
328
|
+
codename: session.codename || null,
|
|
329
|
+
nearAccountId: session.nearAccountId || null,
|
|
330
|
+
expiresAt: session.expiresAt ? new Date(session.expiresAt) : null
|
|
331
|
+
}));
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.error("Session refresh failed:", error);
|
|
334
|
+
}
|
|
335
|
+
}, [api]);
|
|
336
|
+
const clearError = react.useCallback(() => {
|
|
337
|
+
setState((prev) => ({ ...prev, error: null }));
|
|
338
|
+
}, []);
|
|
339
|
+
const recovery = {
|
|
340
|
+
async linkWallet(signMessage, walletAccountId) {
|
|
341
|
+
const { challenge } = await api.startWalletLink();
|
|
342
|
+
const signature = await signMessage(challenge);
|
|
343
|
+
await api.finishWalletLink(signature, challenge, walletAccountId);
|
|
344
|
+
},
|
|
345
|
+
async recoverWithWallet(signMessage, nearAccountId) {
|
|
346
|
+
const { challenge } = await api.startWalletRecovery();
|
|
347
|
+
const signature = await signMessage(challenge);
|
|
348
|
+
const result = await api.finishWalletRecovery(signature, challenge, nearAccountId);
|
|
349
|
+
if (result.success) {
|
|
350
|
+
setState((prev) => ({
|
|
351
|
+
...prev,
|
|
352
|
+
isAuthenticated: true,
|
|
353
|
+
codename: result.codename
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
async setupPasswordRecovery(password) {
|
|
358
|
+
const result = await api.setupIPFSRecovery(password);
|
|
359
|
+
return { cid: result.cid };
|
|
360
|
+
},
|
|
361
|
+
async recoverWithPassword(cid, password) {
|
|
362
|
+
const result = await api.recoverFromIPFS(cid, password);
|
|
363
|
+
if (result.success) {
|
|
364
|
+
setState((prev) => ({
|
|
365
|
+
...prev,
|
|
366
|
+
isAuthenticated: true,
|
|
367
|
+
codename: result.codename
|
|
368
|
+
}));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
const value = {
|
|
373
|
+
...state,
|
|
374
|
+
register,
|
|
375
|
+
login,
|
|
376
|
+
logout,
|
|
377
|
+
refreshSession,
|
|
378
|
+
clearError,
|
|
379
|
+
recovery
|
|
380
|
+
};
|
|
381
|
+
return /* @__PURE__ */ jsxRuntime.jsx(AnonAuthContext.Provider, { value, children });
|
|
382
|
+
}
|
|
383
|
+
function useAnonAuth() {
|
|
384
|
+
const context = react.useContext(AnonAuthContext);
|
|
385
|
+
if (!context) {
|
|
386
|
+
throw new Error("useAnonAuth must be used within AnonAuthProvider");
|
|
387
|
+
}
|
|
388
|
+
return context;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
exports.AnonAuthProvider = AnonAuthProvider;
|
|
392
|
+
exports.authenticateWithPasskey = authenticateWithPasskey;
|
|
393
|
+
exports.createApiClient = createApiClient;
|
|
394
|
+
exports.createPasskey = createPasskey;
|
|
395
|
+
exports.isPlatformAuthenticatorAvailable = isPlatformAuthenticatorAvailable;
|
|
396
|
+
exports.isWebAuthnSupported = isWebAuthnSupported;
|
|
397
|
+
exports.useAnonAuth = useAnonAuth;
|
|
398
|
+
//# sourceMappingURL=index.cjs.map
|
|
399
|
+
//# sourceMappingURL=index.cjs.map
|