@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
|
@@ -0,0 +1,1687 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto$1 = require('crypto');
|
|
4
|
+
var server = require('@simplewebauthn/server');
|
|
5
|
+
var nacl = require('tweetnacl');
|
|
6
|
+
var bs58 = require('bs58');
|
|
7
|
+
var util = require('util');
|
|
8
|
+
var express = require('express');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
var nacl__default = /*#__PURE__*/_interopDefault(nacl);
|
|
13
|
+
var bs58__default = /*#__PURE__*/_interopDefault(bs58);
|
|
14
|
+
|
|
15
|
+
// src/server/db/adapters/postgres.ts
|
|
16
|
+
var POSTGRES_SCHEMA = `
|
|
17
|
+
-- Anonymous users
|
|
18
|
+
CREATE TABLE IF NOT EXISTS anon_users (
|
|
19
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
20
|
+
codename TEXT UNIQUE NOT NULL,
|
|
21
|
+
near_account_id TEXT UNIQUE NOT NULL,
|
|
22
|
+
mpc_public_key TEXT NOT NULL,
|
|
23
|
+
derivation_path TEXT NOT NULL,
|
|
24
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
25
|
+
last_active_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
-- Passkeys (WebAuthn credentials)
|
|
29
|
+
CREATE TABLE IF NOT EXISTS anon_passkeys (
|
|
30
|
+
credential_id TEXT PRIMARY KEY,
|
|
31
|
+
user_id UUID NOT NULL REFERENCES anon_users(id) ON DELETE CASCADE,
|
|
32
|
+
public_key BYTEA NOT NULL,
|
|
33
|
+
counter BIGINT NOT NULL DEFAULT 0,
|
|
34
|
+
device_type TEXT NOT NULL,
|
|
35
|
+
backed_up BOOLEAN NOT NULL DEFAULT false,
|
|
36
|
+
transports TEXT[],
|
|
37
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
-- Sessions
|
|
41
|
+
CREATE TABLE IF NOT EXISTS anon_sessions (
|
|
42
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
43
|
+
user_id UUID NOT NULL REFERENCES anon_users(id) ON DELETE CASCADE,
|
|
44
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
45
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
46
|
+
last_activity_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
47
|
+
ip_address TEXT,
|
|
48
|
+
user_agent TEXT
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
-- WebAuthn challenges (temporary)
|
|
52
|
+
CREATE TABLE IF NOT EXISTS anon_challenges (
|
|
53
|
+
id UUID PRIMARY KEY,
|
|
54
|
+
challenge TEXT NOT NULL,
|
|
55
|
+
type TEXT NOT NULL,
|
|
56
|
+
user_id UUID REFERENCES anon_users(id) ON DELETE CASCADE,
|
|
57
|
+
expires_at TIMESTAMPTZ NOT NULL,
|
|
58
|
+
metadata JSONB
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
-- Recovery data references
|
|
62
|
+
CREATE TABLE IF NOT EXISTS anon_recovery (
|
|
63
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
64
|
+
user_id UUID NOT NULL REFERENCES anon_users(id) ON DELETE CASCADE,
|
|
65
|
+
type TEXT NOT NULL,
|
|
66
|
+
reference TEXT NOT NULL,
|
|
67
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
68
|
+
UNIQUE(user_id, type)
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
-- Indexes
|
|
72
|
+
CREATE INDEX IF NOT EXISTS idx_anon_sessions_user ON anon_sessions(user_id);
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_anon_sessions_expires ON anon_sessions(expires_at);
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_anon_passkeys_user ON anon_passkeys(user_id);
|
|
75
|
+
CREATE INDEX IF NOT EXISTS idx_anon_challenges_expires ON anon_challenges(expires_at);
|
|
76
|
+
`;
|
|
77
|
+
function createPostgresAdapter(config) {
|
|
78
|
+
let pool = null;
|
|
79
|
+
async function getPool() {
|
|
80
|
+
if (!pool) {
|
|
81
|
+
const { Pool } = await import('pg');
|
|
82
|
+
pool = new Pool({ connectionString: config.connectionString });
|
|
83
|
+
}
|
|
84
|
+
return pool;
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
async initialize() {
|
|
88
|
+
const p = await getPool();
|
|
89
|
+
await p.query(POSTGRES_SCHEMA);
|
|
90
|
+
},
|
|
91
|
+
async createUser(input) {
|
|
92
|
+
const p = await getPool();
|
|
93
|
+
const result = await p.query(
|
|
94
|
+
`INSERT INTO anon_users (codename, near_account_id, mpc_public_key, derivation_path)
|
|
95
|
+
VALUES ($1, $2, $3, $4)
|
|
96
|
+
RETURNING id, codename, near_account_id, mpc_public_key, derivation_path, created_at, last_active_at`,
|
|
97
|
+
[input.codename, input.nearAccountId, input.mpcPublicKey, input.derivationPath]
|
|
98
|
+
);
|
|
99
|
+
const row = result.rows[0];
|
|
100
|
+
return {
|
|
101
|
+
id: row.id,
|
|
102
|
+
codename: row.codename,
|
|
103
|
+
nearAccountId: row.near_account_id,
|
|
104
|
+
mpcPublicKey: row.mpc_public_key,
|
|
105
|
+
derivationPath: row.derivation_path,
|
|
106
|
+
createdAt: row.created_at,
|
|
107
|
+
lastActiveAt: row.last_active_at
|
|
108
|
+
};
|
|
109
|
+
},
|
|
110
|
+
async getUserById(id) {
|
|
111
|
+
const p = await getPool();
|
|
112
|
+
const result = await p.query(
|
|
113
|
+
"SELECT * FROM anon_users WHERE id = $1",
|
|
114
|
+
[id]
|
|
115
|
+
);
|
|
116
|
+
if (result.rows.length === 0) return null;
|
|
117
|
+
const row = result.rows[0];
|
|
118
|
+
return {
|
|
119
|
+
id: row.id,
|
|
120
|
+
codename: row.codename,
|
|
121
|
+
nearAccountId: row.near_account_id,
|
|
122
|
+
mpcPublicKey: row.mpc_public_key,
|
|
123
|
+
derivationPath: row.derivation_path,
|
|
124
|
+
createdAt: row.created_at,
|
|
125
|
+
lastActiveAt: row.last_active_at
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
async getUserByCodename(codename) {
|
|
129
|
+
const p = await getPool();
|
|
130
|
+
const result = await p.query(
|
|
131
|
+
"SELECT * FROM anon_users WHERE codename = $1",
|
|
132
|
+
[codename]
|
|
133
|
+
);
|
|
134
|
+
if (result.rows.length === 0) return null;
|
|
135
|
+
const row = result.rows[0];
|
|
136
|
+
return {
|
|
137
|
+
id: row.id,
|
|
138
|
+
codename: row.codename,
|
|
139
|
+
nearAccountId: row.near_account_id,
|
|
140
|
+
mpcPublicKey: row.mpc_public_key,
|
|
141
|
+
derivationPath: row.derivation_path,
|
|
142
|
+
createdAt: row.created_at,
|
|
143
|
+
lastActiveAt: row.last_active_at
|
|
144
|
+
};
|
|
145
|
+
},
|
|
146
|
+
async getUserByNearAccount(nearAccountId) {
|
|
147
|
+
const p = await getPool();
|
|
148
|
+
const result = await p.query(
|
|
149
|
+
"SELECT * FROM anon_users WHERE near_account_id = $1",
|
|
150
|
+
[nearAccountId]
|
|
151
|
+
);
|
|
152
|
+
if (result.rows.length === 0) return null;
|
|
153
|
+
const row = result.rows[0];
|
|
154
|
+
return {
|
|
155
|
+
id: row.id,
|
|
156
|
+
codename: row.codename,
|
|
157
|
+
nearAccountId: row.near_account_id,
|
|
158
|
+
mpcPublicKey: row.mpc_public_key,
|
|
159
|
+
derivationPath: row.derivation_path,
|
|
160
|
+
createdAt: row.created_at,
|
|
161
|
+
lastActiveAt: row.last_active_at
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
async createPasskey(input) {
|
|
165
|
+
const p = await getPool();
|
|
166
|
+
await p.query(
|
|
167
|
+
`INSERT INTO anon_passkeys (credential_id, user_id, public_key, counter, device_type, backed_up, transports)
|
|
168
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
169
|
+
[
|
|
170
|
+
input.credentialId,
|
|
171
|
+
input.userId,
|
|
172
|
+
input.publicKey,
|
|
173
|
+
input.counter,
|
|
174
|
+
input.deviceType,
|
|
175
|
+
input.backedUp,
|
|
176
|
+
input.transports || null
|
|
177
|
+
]
|
|
178
|
+
);
|
|
179
|
+
return {
|
|
180
|
+
...input,
|
|
181
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
async getPasskeyById(credentialId) {
|
|
185
|
+
const p = await getPool();
|
|
186
|
+
const result = await p.query(
|
|
187
|
+
"SELECT * FROM anon_passkeys WHERE credential_id = $1",
|
|
188
|
+
[credentialId]
|
|
189
|
+
);
|
|
190
|
+
if (result.rows.length === 0) return null;
|
|
191
|
+
const row = result.rows[0];
|
|
192
|
+
return {
|
|
193
|
+
credentialId: row.credential_id,
|
|
194
|
+
userId: row.user_id,
|
|
195
|
+
publicKey: row.public_key,
|
|
196
|
+
counter: row.counter,
|
|
197
|
+
deviceType: row.device_type,
|
|
198
|
+
backedUp: row.backed_up,
|
|
199
|
+
transports: row.transports,
|
|
200
|
+
createdAt: row.created_at
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
async getPasskeysByUserId(userId) {
|
|
204
|
+
const p = await getPool();
|
|
205
|
+
const result = await p.query(
|
|
206
|
+
"SELECT * FROM anon_passkeys WHERE user_id = $1",
|
|
207
|
+
[userId]
|
|
208
|
+
);
|
|
209
|
+
return result.rows.map((row) => ({
|
|
210
|
+
credentialId: row.credential_id,
|
|
211
|
+
userId: row.user_id,
|
|
212
|
+
publicKey: row.public_key,
|
|
213
|
+
counter: row.counter,
|
|
214
|
+
deviceType: row.device_type,
|
|
215
|
+
backedUp: row.backed_up,
|
|
216
|
+
transports: row.transports,
|
|
217
|
+
createdAt: row.created_at
|
|
218
|
+
}));
|
|
219
|
+
},
|
|
220
|
+
async updatePasskeyCounter(credentialId, counter) {
|
|
221
|
+
const p = await getPool();
|
|
222
|
+
await p.query(
|
|
223
|
+
"UPDATE anon_passkeys SET counter = $1 WHERE credential_id = $2",
|
|
224
|
+
[counter, credentialId]
|
|
225
|
+
);
|
|
226
|
+
},
|
|
227
|
+
async deletePasskey(credentialId) {
|
|
228
|
+
const p = await getPool();
|
|
229
|
+
await p.query("DELETE FROM anon_passkeys WHERE credential_id = $1", [credentialId]);
|
|
230
|
+
},
|
|
231
|
+
async createSession(input) {
|
|
232
|
+
const p = await getPool();
|
|
233
|
+
const result = await p.query(
|
|
234
|
+
`INSERT INTO anon_sessions (id, user_id, expires_at, ip_address, user_agent)
|
|
235
|
+
VALUES (COALESCE($1, gen_random_uuid()), $2, $3, $4, $5)
|
|
236
|
+
RETURNING id, user_id, created_at, expires_at, last_activity_at, ip_address, user_agent`,
|
|
237
|
+
[input.id || null, input.userId, input.expiresAt, input.ipAddress || null, input.userAgent || null]
|
|
238
|
+
);
|
|
239
|
+
const row = result.rows[0];
|
|
240
|
+
return {
|
|
241
|
+
id: row.id,
|
|
242
|
+
userId: row.user_id,
|
|
243
|
+
createdAt: row.created_at,
|
|
244
|
+
expiresAt: row.expires_at,
|
|
245
|
+
lastActivityAt: row.last_activity_at,
|
|
246
|
+
ipAddress: row.ip_address,
|
|
247
|
+
userAgent: row.user_agent
|
|
248
|
+
};
|
|
249
|
+
},
|
|
250
|
+
async getSession(sessionId) {
|
|
251
|
+
const p = await getPool();
|
|
252
|
+
const result = await p.query(
|
|
253
|
+
"SELECT * FROM anon_sessions WHERE id = $1 AND expires_at > NOW()",
|
|
254
|
+
[sessionId]
|
|
255
|
+
);
|
|
256
|
+
if (result.rows.length === 0) return null;
|
|
257
|
+
const row = result.rows[0];
|
|
258
|
+
return {
|
|
259
|
+
id: row.id,
|
|
260
|
+
userId: row.user_id,
|
|
261
|
+
createdAt: row.created_at,
|
|
262
|
+
expiresAt: row.expires_at,
|
|
263
|
+
lastActivityAt: row.last_activity_at,
|
|
264
|
+
ipAddress: row.ip_address,
|
|
265
|
+
userAgent: row.user_agent
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
async deleteSession(sessionId) {
|
|
269
|
+
const p = await getPool();
|
|
270
|
+
await p.query("DELETE FROM anon_sessions WHERE id = $1", [sessionId]);
|
|
271
|
+
},
|
|
272
|
+
async deleteUserSessions(userId) {
|
|
273
|
+
const p = await getPool();
|
|
274
|
+
await p.query("DELETE FROM anon_sessions WHERE user_id = $1", [userId]);
|
|
275
|
+
},
|
|
276
|
+
async cleanExpiredSessions() {
|
|
277
|
+
const p = await getPool();
|
|
278
|
+
const result = await p.query("DELETE FROM anon_sessions WHERE expires_at < NOW()");
|
|
279
|
+
return result.rowCount || 0;
|
|
280
|
+
},
|
|
281
|
+
async storeChallenge(challenge) {
|
|
282
|
+
const p = await getPool();
|
|
283
|
+
await p.query(
|
|
284
|
+
`INSERT INTO anon_challenges (id, challenge, type, user_id, expires_at, metadata)
|
|
285
|
+
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
286
|
+
[
|
|
287
|
+
challenge.id,
|
|
288
|
+
challenge.challenge,
|
|
289
|
+
challenge.type,
|
|
290
|
+
challenge.userId || null,
|
|
291
|
+
challenge.expiresAt,
|
|
292
|
+
challenge.metadata ? JSON.stringify(challenge.metadata) : null
|
|
293
|
+
]
|
|
294
|
+
);
|
|
295
|
+
},
|
|
296
|
+
async getChallenge(challengeId) {
|
|
297
|
+
const p = await getPool();
|
|
298
|
+
const result = await p.query(
|
|
299
|
+
"SELECT * FROM anon_challenges WHERE id = $1",
|
|
300
|
+
[challengeId]
|
|
301
|
+
);
|
|
302
|
+
if (result.rows.length === 0) return null;
|
|
303
|
+
const row = result.rows[0];
|
|
304
|
+
return {
|
|
305
|
+
id: row.id,
|
|
306
|
+
challenge: row.challenge,
|
|
307
|
+
type: row.type,
|
|
308
|
+
userId: row.user_id,
|
|
309
|
+
expiresAt: row.expires_at,
|
|
310
|
+
metadata: row.metadata
|
|
311
|
+
};
|
|
312
|
+
},
|
|
313
|
+
async deleteChallenge(challengeId) {
|
|
314
|
+
const p = await getPool();
|
|
315
|
+
await p.query("DELETE FROM anon_challenges WHERE id = $1", [challengeId]);
|
|
316
|
+
},
|
|
317
|
+
async storeRecoveryData(data) {
|
|
318
|
+
const p = await getPool();
|
|
319
|
+
await p.query(
|
|
320
|
+
`INSERT INTO anon_recovery (user_id, type, reference)
|
|
321
|
+
VALUES ($1, $2, $3)
|
|
322
|
+
ON CONFLICT (user_id, type) DO UPDATE SET reference = $3, created_at = NOW()`,
|
|
323
|
+
[data.userId, data.type, data.reference]
|
|
324
|
+
);
|
|
325
|
+
},
|
|
326
|
+
async getRecoveryData(userId, type) {
|
|
327
|
+
const p = await getPool();
|
|
328
|
+
const result = await p.query(
|
|
329
|
+
"SELECT * FROM anon_recovery WHERE user_id = $1 AND type = $2",
|
|
330
|
+
[userId, type]
|
|
331
|
+
);
|
|
332
|
+
if (result.rows.length === 0) return null;
|
|
333
|
+
const row = result.rows[0];
|
|
334
|
+
return {
|
|
335
|
+
userId: row.user_id,
|
|
336
|
+
type: row.type,
|
|
337
|
+
reference: row.reference,
|
|
338
|
+
createdAt: row.created_at
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
var SESSION_COOKIE_NAME = "anon_session";
|
|
344
|
+
var DEFAULT_SESSION_DURATION_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
345
|
+
function signSessionId(sessionId, secret) {
|
|
346
|
+
const signature = crypto$1.createHmac("sha256", secret).update(sessionId).digest("base64url");
|
|
347
|
+
return `${sessionId}.${signature}`;
|
|
348
|
+
}
|
|
349
|
+
function verifySessionId(signedValue, secret) {
|
|
350
|
+
const parts = signedValue.split(".");
|
|
351
|
+
if (parts.length !== 2) return null;
|
|
352
|
+
const [sessionId, signature] = parts;
|
|
353
|
+
const expectedSignature = crypto$1.createHmac("sha256", secret).update(sessionId).digest("base64url");
|
|
354
|
+
if (signature !== expectedSignature) return null;
|
|
355
|
+
return sessionId;
|
|
356
|
+
}
|
|
357
|
+
function parseCookies(req) {
|
|
358
|
+
const cookies = {};
|
|
359
|
+
const cookieHeader = req.headers.cookie;
|
|
360
|
+
if (!cookieHeader) return cookies;
|
|
361
|
+
cookieHeader.split(";").forEach((cookie) => {
|
|
362
|
+
const [name, ...rest] = cookie.trim().split("=");
|
|
363
|
+
if (name && rest.length) {
|
|
364
|
+
cookies[name] = decodeURIComponent(rest.join("="));
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
return cookies;
|
|
368
|
+
}
|
|
369
|
+
function createSessionManager(db, config) {
|
|
370
|
+
const cookieName = config.cookieName || SESSION_COOKIE_NAME;
|
|
371
|
+
const durationMs = config.durationMs || DEFAULT_SESSION_DURATION_MS;
|
|
372
|
+
const isProduction = process.env.NODE_ENV === "production";
|
|
373
|
+
const cookieOptions = {
|
|
374
|
+
httpOnly: true,
|
|
375
|
+
secure: config.secure ?? isProduction,
|
|
376
|
+
sameSite: config.sameSite || "strict",
|
|
377
|
+
path: config.path || "/",
|
|
378
|
+
domain: config.domain
|
|
379
|
+
};
|
|
380
|
+
return {
|
|
381
|
+
async createSession(userId, res, options = {}) {
|
|
382
|
+
const sessionId = crypto$1.randomUUID();
|
|
383
|
+
const now = /* @__PURE__ */ new Date();
|
|
384
|
+
const expiresAt = new Date(now.getTime() + durationMs);
|
|
385
|
+
const sessionInput = {
|
|
386
|
+
userId,
|
|
387
|
+
expiresAt,
|
|
388
|
+
ipAddress: options.ipAddress,
|
|
389
|
+
userAgent: options.userAgent
|
|
390
|
+
};
|
|
391
|
+
const session = await db.createSession({
|
|
392
|
+
...sessionInput,
|
|
393
|
+
id: sessionId
|
|
394
|
+
});
|
|
395
|
+
const signedId = signSessionId(sessionId, config.secret);
|
|
396
|
+
res.cookie(cookieName, signedId, {
|
|
397
|
+
...cookieOptions,
|
|
398
|
+
maxAge: durationMs,
|
|
399
|
+
expires: expiresAt
|
|
400
|
+
});
|
|
401
|
+
return session;
|
|
402
|
+
},
|
|
403
|
+
async getSession(req) {
|
|
404
|
+
const cookies = parseCookies(req);
|
|
405
|
+
const signedId = cookies[cookieName];
|
|
406
|
+
if (!signedId) return null;
|
|
407
|
+
const sessionId = verifySessionId(signedId, config.secret);
|
|
408
|
+
if (!sessionId) return null;
|
|
409
|
+
const session = await db.getSession(sessionId);
|
|
410
|
+
if (!session) return null;
|
|
411
|
+
if (session.expiresAt < /* @__PURE__ */ new Date()) {
|
|
412
|
+
await db.deleteSession(sessionId);
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
return session;
|
|
416
|
+
},
|
|
417
|
+
async destroySession(req, res) {
|
|
418
|
+
const cookies = parseCookies(req);
|
|
419
|
+
const signedId = cookies[cookieName];
|
|
420
|
+
if (signedId) {
|
|
421
|
+
const sessionId = verifySessionId(signedId, config.secret);
|
|
422
|
+
if (sessionId) {
|
|
423
|
+
await db.deleteSession(sessionId);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
res.clearCookie(cookieName, {
|
|
427
|
+
...cookieOptions
|
|
428
|
+
});
|
|
429
|
+
},
|
|
430
|
+
async refreshSession(req, res) {
|
|
431
|
+
const session = await this.getSession(req);
|
|
432
|
+
if (!session) return null;
|
|
433
|
+
const now = Date.now();
|
|
434
|
+
const created = session.createdAt.getTime();
|
|
435
|
+
const expires = session.expiresAt.getTime();
|
|
436
|
+
const lifetime = expires - created;
|
|
437
|
+
const elapsed = now - created;
|
|
438
|
+
if (elapsed > lifetime * 0.5) {
|
|
439
|
+
const newExpiresAt = new Date(now + durationMs);
|
|
440
|
+
const signedId = signSessionId(session.id, config.secret);
|
|
441
|
+
res.cookie(cookieName, signedId, {
|
|
442
|
+
...cookieOptions,
|
|
443
|
+
maxAge: durationMs,
|
|
444
|
+
expires: newExpiresAt
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
return session;
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function createPasskeyManager(db, config) {
|
|
452
|
+
const challengeTimeoutMs = config.challengeTimeoutMs || 6e4;
|
|
453
|
+
return {
|
|
454
|
+
async startRegistration(userId, userDisplayName) {
|
|
455
|
+
const options = await server.generateRegistrationOptions({
|
|
456
|
+
rpName: config.rpName,
|
|
457
|
+
rpID: config.rpId,
|
|
458
|
+
userName: userDisplayName,
|
|
459
|
+
userDisplayName,
|
|
460
|
+
userID: new TextEncoder().encode(userId),
|
|
461
|
+
attestationType: "none",
|
|
462
|
+
excludeCredentials: [],
|
|
463
|
+
// No existing passkeys for new user
|
|
464
|
+
authenticatorSelection: {
|
|
465
|
+
residentKey: "preferred",
|
|
466
|
+
userVerification: "preferred",
|
|
467
|
+
authenticatorAttachment: "platform"
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
const challengeId = crypto$1.randomUUID();
|
|
471
|
+
const challenge = {
|
|
472
|
+
id: challengeId,
|
|
473
|
+
challenge: options.challenge,
|
|
474
|
+
type: "registration",
|
|
475
|
+
userId: void 0,
|
|
476
|
+
// Don't set foreign key - user doesn't exist
|
|
477
|
+
expiresAt: new Date(Date.now() + challengeTimeoutMs),
|
|
478
|
+
metadata: { tempUserId: userId, userDisplayName }
|
|
479
|
+
// Store temp ID here
|
|
480
|
+
};
|
|
481
|
+
await db.storeChallenge(challenge);
|
|
482
|
+
return {
|
|
483
|
+
challengeId,
|
|
484
|
+
options
|
|
485
|
+
};
|
|
486
|
+
},
|
|
487
|
+
async finishRegistration(challengeId, response) {
|
|
488
|
+
const challenge = await db.getChallenge(challengeId);
|
|
489
|
+
if (!challenge) {
|
|
490
|
+
throw new Error("Challenge not found or expired");
|
|
491
|
+
}
|
|
492
|
+
if (challenge.type !== "registration") {
|
|
493
|
+
throw new Error("Invalid challenge type");
|
|
494
|
+
}
|
|
495
|
+
if (challenge.expiresAt < /* @__PURE__ */ new Date()) {
|
|
496
|
+
await db.deleteChallenge(challengeId);
|
|
497
|
+
throw new Error("Challenge expired");
|
|
498
|
+
}
|
|
499
|
+
const tempUserId = challenge.metadata?.tempUserId;
|
|
500
|
+
if (!tempUserId) {
|
|
501
|
+
throw new Error("Challenge missing temp user ID");
|
|
502
|
+
}
|
|
503
|
+
let verification;
|
|
504
|
+
try {
|
|
505
|
+
verification = await server.verifyRegistrationResponse({
|
|
506
|
+
response,
|
|
507
|
+
expectedChallenge: challenge.challenge,
|
|
508
|
+
expectedOrigin: config.origin,
|
|
509
|
+
expectedRPID: config.rpId
|
|
510
|
+
});
|
|
511
|
+
} catch (error) {
|
|
512
|
+
console.error("[Passkey] Registration verification failed:", error);
|
|
513
|
+
await db.deleteChallenge(challengeId);
|
|
514
|
+
return { verified: false };
|
|
515
|
+
}
|
|
516
|
+
if (!verification.verified || !verification.registrationInfo) {
|
|
517
|
+
await db.deleteChallenge(challengeId);
|
|
518
|
+
return { verified: false };
|
|
519
|
+
}
|
|
520
|
+
const { registrationInfo } = verification;
|
|
521
|
+
await db.deleteChallenge(challengeId);
|
|
522
|
+
return {
|
|
523
|
+
verified: true,
|
|
524
|
+
passkeyData: {
|
|
525
|
+
credentialId: registrationInfo.credential.id,
|
|
526
|
+
publicKey: registrationInfo.credential.publicKey,
|
|
527
|
+
counter: registrationInfo.credential.counter,
|
|
528
|
+
deviceType: registrationInfo.credentialDeviceType,
|
|
529
|
+
backedUp: registrationInfo.credentialBackedUp,
|
|
530
|
+
transports: response.response.transports
|
|
531
|
+
},
|
|
532
|
+
tempUserId
|
|
533
|
+
};
|
|
534
|
+
},
|
|
535
|
+
async startAuthentication(userId) {
|
|
536
|
+
let allowCredentials;
|
|
537
|
+
if (userId) {
|
|
538
|
+
const passkeys = await db.getPasskeysByUserId(userId);
|
|
539
|
+
allowCredentials = passkeys.map((pk) => ({
|
|
540
|
+
id: pk.credentialId,
|
|
541
|
+
type: "public-key",
|
|
542
|
+
transports: pk.transports
|
|
543
|
+
}));
|
|
544
|
+
}
|
|
545
|
+
const options = await server.generateAuthenticationOptions({
|
|
546
|
+
rpID: config.rpId,
|
|
547
|
+
userVerification: "preferred",
|
|
548
|
+
allowCredentials
|
|
549
|
+
});
|
|
550
|
+
const challengeId = crypto$1.randomUUID();
|
|
551
|
+
const challenge = {
|
|
552
|
+
id: challengeId,
|
|
553
|
+
challenge: options.challenge,
|
|
554
|
+
type: "authentication",
|
|
555
|
+
userId,
|
|
556
|
+
expiresAt: new Date(Date.now() + challengeTimeoutMs)
|
|
557
|
+
};
|
|
558
|
+
await db.storeChallenge(challenge);
|
|
559
|
+
return {
|
|
560
|
+
challengeId,
|
|
561
|
+
options
|
|
562
|
+
};
|
|
563
|
+
},
|
|
564
|
+
async finishAuthentication(challengeId, response) {
|
|
565
|
+
const challenge = await db.getChallenge(challengeId);
|
|
566
|
+
if (!challenge) {
|
|
567
|
+
throw new Error("Challenge not found or expired");
|
|
568
|
+
}
|
|
569
|
+
if (challenge.type !== "authentication") {
|
|
570
|
+
throw new Error("Invalid challenge type");
|
|
571
|
+
}
|
|
572
|
+
if (challenge.expiresAt < /* @__PURE__ */ new Date()) {
|
|
573
|
+
await db.deleteChallenge(challengeId);
|
|
574
|
+
throw new Error("Challenge expired");
|
|
575
|
+
}
|
|
576
|
+
const passkey = await db.getPasskeyById(response.id);
|
|
577
|
+
if (!passkey) {
|
|
578
|
+
await db.deleteChallenge(challengeId);
|
|
579
|
+
throw new Error("Passkey not found");
|
|
580
|
+
}
|
|
581
|
+
let verification;
|
|
582
|
+
try {
|
|
583
|
+
verification = await server.verifyAuthenticationResponse({
|
|
584
|
+
response,
|
|
585
|
+
expectedChallenge: challenge.challenge,
|
|
586
|
+
expectedOrigin: config.origin,
|
|
587
|
+
expectedRPID: config.rpId,
|
|
588
|
+
credential: {
|
|
589
|
+
id: passkey.credentialId,
|
|
590
|
+
publicKey: passkey.publicKey,
|
|
591
|
+
counter: passkey.counter,
|
|
592
|
+
transports: passkey.transports
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
} catch (error) {
|
|
596
|
+
console.error("[Passkey] Authentication verification failed:", error);
|
|
597
|
+
await db.deleteChallenge(challengeId);
|
|
598
|
+
return { verified: false };
|
|
599
|
+
}
|
|
600
|
+
if (!verification.verified) {
|
|
601
|
+
await db.deleteChallenge(challengeId);
|
|
602
|
+
return { verified: false };
|
|
603
|
+
}
|
|
604
|
+
await db.updatePasskeyCounter(
|
|
605
|
+
passkey.credentialId,
|
|
606
|
+
verification.authenticationInfo.newCounter
|
|
607
|
+
);
|
|
608
|
+
await db.deleteChallenge(challengeId);
|
|
609
|
+
return {
|
|
610
|
+
verified: true,
|
|
611
|
+
userId: passkey.userId,
|
|
612
|
+
passkey
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function getMPCContractId(networkId) {
|
|
618
|
+
return networkId === "mainnet" ? "v1.signer-prod.near" : "v1.signer-prod.testnet";
|
|
619
|
+
}
|
|
620
|
+
function getRPCUrl(networkId) {
|
|
621
|
+
return networkId === "mainnet" ? "https://rpc.mainnet.near.org" : "https://rpc.testnet.near.org";
|
|
622
|
+
}
|
|
623
|
+
function base58Encode(bytes) {
|
|
624
|
+
const ALPHABET = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
|
|
625
|
+
let result = "";
|
|
626
|
+
let num = BigInt("0x" + bytes.toString("hex"));
|
|
627
|
+
while (num > 0n) {
|
|
628
|
+
const remainder = Number(num % 58n);
|
|
629
|
+
num = num / 58n;
|
|
630
|
+
result = ALPHABET[remainder] + result;
|
|
631
|
+
}
|
|
632
|
+
for (const byte of bytes) {
|
|
633
|
+
if (byte === 0) {
|
|
634
|
+
result = "1" + result;
|
|
635
|
+
} else {
|
|
636
|
+
break;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return result || "1";
|
|
640
|
+
}
|
|
641
|
+
function derivePublicKey(seed) {
|
|
642
|
+
const hash = crypto$1.createHash("sha512").update(seed).digest();
|
|
643
|
+
return hash.subarray(0, 32);
|
|
644
|
+
}
|
|
645
|
+
async function accountExists(accountId, networkId) {
|
|
646
|
+
try {
|
|
647
|
+
const rpcUrl = getRPCUrl(networkId);
|
|
648
|
+
const response = await fetch(rpcUrl, {
|
|
649
|
+
method: "POST",
|
|
650
|
+
headers: { "Content-Type": "application/json" },
|
|
651
|
+
body: JSON.stringify({
|
|
652
|
+
jsonrpc: "2.0",
|
|
653
|
+
id: "check-account",
|
|
654
|
+
method: "query",
|
|
655
|
+
params: {
|
|
656
|
+
request_type: "view_account",
|
|
657
|
+
finality: "final",
|
|
658
|
+
account_id: accountId
|
|
659
|
+
}
|
|
660
|
+
})
|
|
661
|
+
});
|
|
662
|
+
const result = await response.json();
|
|
663
|
+
return !result.error;
|
|
664
|
+
} catch {
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async function createTestnetAccount(accountId) {
|
|
669
|
+
const seed = crypto$1.randomBytes(32);
|
|
670
|
+
const publicKeyBytes = derivePublicKey(seed);
|
|
671
|
+
const publicKey = `ed25519:${base58Encode(publicKeyBytes)}`;
|
|
672
|
+
const helperUrl = "https://helper.testnet.near.org/account";
|
|
673
|
+
const response = await fetch(helperUrl, {
|
|
674
|
+
method: "POST",
|
|
675
|
+
headers: { "Content-Type": "application/json" },
|
|
676
|
+
body: JSON.stringify({
|
|
677
|
+
newAccountId: accountId,
|
|
678
|
+
newAccountPublicKey: publicKey
|
|
679
|
+
})
|
|
680
|
+
});
|
|
681
|
+
if (!response.ok) {
|
|
682
|
+
const errorText = await response.text();
|
|
683
|
+
throw new Error(`Testnet helper error: ${response.status} - ${errorText}`);
|
|
684
|
+
}
|
|
685
|
+
return publicKey;
|
|
686
|
+
}
|
|
687
|
+
function generateAccountName(userId, prefix) {
|
|
688
|
+
const hash = crypto$1.createHash("sha256").update(userId).digest("hex");
|
|
689
|
+
const shortHash = hash.substring(0, 12);
|
|
690
|
+
return `${prefix}-${shortHash}`;
|
|
691
|
+
}
|
|
692
|
+
var MPCAccountManager = class {
|
|
693
|
+
networkId;
|
|
694
|
+
mpcContractId;
|
|
695
|
+
accountPrefix;
|
|
696
|
+
constructor(config) {
|
|
697
|
+
this.networkId = config.networkId;
|
|
698
|
+
this.mpcContractId = getMPCContractId(config.networkId);
|
|
699
|
+
this.accountPrefix = config.accountPrefix || "anon";
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Create a new NEAR account for an anonymous user
|
|
703
|
+
*/
|
|
704
|
+
async createAccount(userId) {
|
|
705
|
+
const accountName = generateAccountName(userId, this.accountPrefix);
|
|
706
|
+
const suffix = this.networkId === "mainnet" ? ".near" : ".testnet";
|
|
707
|
+
const nearAccountId = `${accountName}${suffix}`;
|
|
708
|
+
const derivationPath = `near-anon-auth,${userId}`;
|
|
709
|
+
console.log("[MPC] Creating NEAR account:", {
|
|
710
|
+
nearAccountId,
|
|
711
|
+
derivationPath,
|
|
712
|
+
mpcContractId: this.mpcContractId
|
|
713
|
+
});
|
|
714
|
+
const exists = await accountExists(nearAccountId, this.networkId);
|
|
715
|
+
if (exists) {
|
|
716
|
+
console.log("[MPC] Account already exists:", nearAccountId);
|
|
717
|
+
return {
|
|
718
|
+
nearAccountId,
|
|
719
|
+
derivationPath,
|
|
720
|
+
mpcPublicKey: "existing-account",
|
|
721
|
+
onChain: true
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
if (this.networkId === "testnet") {
|
|
725
|
+
try {
|
|
726
|
+
const publicKey = await createTestnetAccount(nearAccountId);
|
|
727
|
+
console.log("[MPC] Account created:", nearAccountId);
|
|
728
|
+
return {
|
|
729
|
+
nearAccountId,
|
|
730
|
+
derivationPath,
|
|
731
|
+
mpcPublicKey: publicKey,
|
|
732
|
+
onChain: true
|
|
733
|
+
};
|
|
734
|
+
} catch (error) {
|
|
735
|
+
console.error("[MPC] Account creation failed:", error);
|
|
736
|
+
return {
|
|
737
|
+
nearAccountId,
|
|
738
|
+
derivationPath,
|
|
739
|
+
mpcPublicKey: "creation-failed",
|
|
740
|
+
onChain: false
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
console.warn("[MPC] Mainnet account creation requires funded creator");
|
|
745
|
+
return {
|
|
746
|
+
nearAccountId,
|
|
747
|
+
derivationPath,
|
|
748
|
+
mpcPublicKey: "mainnet-pending",
|
|
749
|
+
onChain: false
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Add a recovery wallet as an access key to the MPC account
|
|
754
|
+
*
|
|
755
|
+
* This creates an on-chain link without storing it in our database.
|
|
756
|
+
* The recovery wallet can be used to prove ownership and create new passkeys.
|
|
757
|
+
*/
|
|
758
|
+
async addRecoveryWallet(nearAccountId, recoveryWalletId) {
|
|
759
|
+
console.log("[MPC] Adding recovery wallet:", {
|
|
760
|
+
nearAccountId,
|
|
761
|
+
recoveryWalletId
|
|
762
|
+
});
|
|
763
|
+
return {
|
|
764
|
+
success: true,
|
|
765
|
+
txHash: `pending-${Date.now()}`
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Verify that a wallet has recovery access to an account
|
|
770
|
+
*/
|
|
771
|
+
async verifyRecoveryWallet(nearAccountId, recoveryWalletId) {
|
|
772
|
+
try {
|
|
773
|
+
const rpcUrl = getRPCUrl(this.networkId);
|
|
774
|
+
const response = await fetch(rpcUrl, {
|
|
775
|
+
method: "POST",
|
|
776
|
+
headers: { "Content-Type": "application/json" },
|
|
777
|
+
body: JSON.stringify({
|
|
778
|
+
jsonrpc: "2.0",
|
|
779
|
+
id: "check-keys",
|
|
780
|
+
method: "query",
|
|
781
|
+
params: {
|
|
782
|
+
request_type: "view_access_key_list",
|
|
783
|
+
finality: "final",
|
|
784
|
+
account_id: nearAccountId
|
|
785
|
+
}
|
|
786
|
+
})
|
|
787
|
+
});
|
|
788
|
+
const result = await response.json();
|
|
789
|
+
return !!result.result?.keys?.length;
|
|
790
|
+
} catch {
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* Get MPC contract ID
|
|
796
|
+
*/
|
|
797
|
+
getMPCContractId() {
|
|
798
|
+
return this.mpcContractId;
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Get network ID
|
|
802
|
+
*/
|
|
803
|
+
getNetworkId() {
|
|
804
|
+
return this.networkId;
|
|
805
|
+
}
|
|
806
|
+
};
|
|
807
|
+
function createMPCManager(config) {
|
|
808
|
+
return new MPCAccountManager(config);
|
|
809
|
+
}
|
|
810
|
+
function generateWalletChallenge(action, timestamp) {
|
|
811
|
+
return `near-anon-auth:${action}:${timestamp}`;
|
|
812
|
+
}
|
|
813
|
+
function verifyWalletSignature(signature, expectedMessage) {
|
|
814
|
+
try {
|
|
815
|
+
if (signature.message !== expectedMessage) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
const pubKeyStr = signature.publicKey.replace("ed25519:", "");
|
|
819
|
+
const publicKeyBytes = bs58__default.default.decode(pubKeyStr);
|
|
820
|
+
const signatureBytes = Buffer.from(signature.signature, "base64");
|
|
821
|
+
const messageHash = crypto$1.createHash("sha256").update(signature.message).digest();
|
|
822
|
+
return nacl__default.default.sign.detached.verify(
|
|
823
|
+
messageHash,
|
|
824
|
+
signatureBytes,
|
|
825
|
+
publicKeyBytes
|
|
826
|
+
);
|
|
827
|
+
} catch (error) {
|
|
828
|
+
console.error("[WalletRecovery] Signature verification failed:", error);
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
async function checkWalletAccess(nearAccountId, walletPublicKey, networkId) {
|
|
833
|
+
try {
|
|
834
|
+
const rpcUrl = networkId === "mainnet" ? "https://rpc.mainnet.near.org" : "https://rpc.testnet.near.org";
|
|
835
|
+
const response = await fetch(rpcUrl, {
|
|
836
|
+
method: "POST",
|
|
837
|
+
headers: { "Content-Type": "application/json" },
|
|
838
|
+
body: JSON.stringify({
|
|
839
|
+
jsonrpc: "2.0",
|
|
840
|
+
id: "check-access-key",
|
|
841
|
+
method: "query",
|
|
842
|
+
params: {
|
|
843
|
+
request_type: "view_access_key",
|
|
844
|
+
finality: "final",
|
|
845
|
+
account_id: nearAccountId,
|
|
846
|
+
public_key: walletPublicKey
|
|
847
|
+
}
|
|
848
|
+
})
|
|
849
|
+
});
|
|
850
|
+
const result = await response.json();
|
|
851
|
+
return !result.error;
|
|
852
|
+
} catch {
|
|
853
|
+
return false;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
function createWalletRecoveryManager(config) {
|
|
857
|
+
const CHALLENGE_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
858
|
+
return {
|
|
859
|
+
generateLinkChallenge() {
|
|
860
|
+
const timestamp = Date.now();
|
|
861
|
+
const challenge = generateWalletChallenge("link-recovery", timestamp);
|
|
862
|
+
const expiresAt = new Date(Date.now() + CHALLENGE_TIMEOUT_MS);
|
|
863
|
+
return { challenge, expiresAt };
|
|
864
|
+
},
|
|
865
|
+
verifyLinkSignature(signature, challenge) {
|
|
866
|
+
const verified = verifyWalletSignature(signature, challenge);
|
|
867
|
+
if (!verified) {
|
|
868
|
+
return { verified: false };
|
|
869
|
+
}
|
|
870
|
+
const walletId = signature.publicKey;
|
|
871
|
+
return { verified: true, walletId };
|
|
872
|
+
},
|
|
873
|
+
generateRecoveryChallenge() {
|
|
874
|
+
const timestamp = Date.now();
|
|
875
|
+
const challenge = generateWalletChallenge("recover-account", timestamp);
|
|
876
|
+
const expiresAt = new Date(Date.now() + CHALLENGE_TIMEOUT_MS);
|
|
877
|
+
return { challenge, expiresAt };
|
|
878
|
+
},
|
|
879
|
+
async verifyRecoverySignature(signature, challenge, nearAccountId) {
|
|
880
|
+
if (!verifyWalletSignature(signature, challenge)) {
|
|
881
|
+
return { verified: false };
|
|
882
|
+
}
|
|
883
|
+
const hasAccess = await checkWalletAccess(
|
|
884
|
+
nearAccountId,
|
|
885
|
+
signature.publicKey,
|
|
886
|
+
config.nearNetwork
|
|
887
|
+
);
|
|
888
|
+
return { verified: hasAccess };
|
|
889
|
+
}
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
var scryptAsync = util.promisify(crypto$1.scrypt);
|
|
893
|
+
async function deriveKey(password, salt) {
|
|
894
|
+
return scryptAsync(password, salt, 32);
|
|
895
|
+
}
|
|
896
|
+
async function encryptRecoveryData(payload, password) {
|
|
897
|
+
const salt = crypto$1.randomBytes(32);
|
|
898
|
+
const iv = crypto$1.randomBytes(16);
|
|
899
|
+
const key = await deriveKey(password, salt);
|
|
900
|
+
const cipher = crypto$1.createCipheriv("aes-256-gcm", key, iv);
|
|
901
|
+
const payloadJson = JSON.stringify(payload);
|
|
902
|
+
const encrypted = Buffer.concat([
|
|
903
|
+
cipher.update(payloadJson, "utf8"),
|
|
904
|
+
cipher.final()
|
|
905
|
+
]);
|
|
906
|
+
const authTag = cipher.getAuthTag();
|
|
907
|
+
return {
|
|
908
|
+
ciphertext: encrypted.toString("base64"),
|
|
909
|
+
iv: iv.toString("base64"),
|
|
910
|
+
salt: salt.toString("base64"),
|
|
911
|
+
authTag: authTag.toString("base64"),
|
|
912
|
+
version: 1
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
async function decryptRecoveryData(encryptedData, password) {
|
|
916
|
+
const salt = Buffer.from(encryptedData.salt, "base64");
|
|
917
|
+
const iv = Buffer.from(encryptedData.iv, "base64");
|
|
918
|
+
const ciphertext = Buffer.from(encryptedData.ciphertext, "base64");
|
|
919
|
+
const authTag = Buffer.from(encryptedData.authTag, "base64");
|
|
920
|
+
const key = await deriveKey(password, salt);
|
|
921
|
+
const decipher = crypto$1.createDecipheriv("aes-256-gcm", key, iv);
|
|
922
|
+
decipher.setAuthTag(authTag);
|
|
923
|
+
try {
|
|
924
|
+
const decrypted = Buffer.concat([
|
|
925
|
+
decipher.update(ciphertext),
|
|
926
|
+
decipher.final()
|
|
927
|
+
]);
|
|
928
|
+
return JSON.parse(decrypted.toString("utf8"));
|
|
929
|
+
} catch {
|
|
930
|
+
throw new Error("Invalid password or corrupted data");
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
async function pinToPinata(data, apiKey, apiSecret) {
|
|
934
|
+
const formData = new FormData();
|
|
935
|
+
const buffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
936
|
+
formData.append("file", new Blob([buffer]), "recovery.json");
|
|
937
|
+
const response = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", {
|
|
938
|
+
method: "POST",
|
|
939
|
+
headers: {
|
|
940
|
+
"pinata_api_key": apiKey,
|
|
941
|
+
"pinata_secret_api_key": apiSecret
|
|
942
|
+
},
|
|
943
|
+
body: formData
|
|
944
|
+
});
|
|
945
|
+
if (!response.ok) {
|
|
946
|
+
const error = await response.text();
|
|
947
|
+
throw new Error(`Pinata error: ${response.status} - ${error}`);
|
|
948
|
+
}
|
|
949
|
+
const result = await response.json();
|
|
950
|
+
return result.IpfsHash;
|
|
951
|
+
}
|
|
952
|
+
async function pinToWeb3Storage(data, apiToken) {
|
|
953
|
+
const buffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
954
|
+
const response = await fetch("https://api.web3.storage/upload", {
|
|
955
|
+
method: "POST",
|
|
956
|
+
headers: {
|
|
957
|
+
"Authorization": `Bearer ${apiToken}`,
|
|
958
|
+
"Content-Type": "application/octet-stream",
|
|
959
|
+
"X-Name": "phantom-recovery.json"
|
|
960
|
+
},
|
|
961
|
+
body: buffer
|
|
962
|
+
});
|
|
963
|
+
if (!response.ok) {
|
|
964
|
+
const error = await response.text();
|
|
965
|
+
throw new Error(`web3.storage error: ${response.status} - ${error}`);
|
|
966
|
+
}
|
|
967
|
+
const result = await response.json();
|
|
968
|
+
return result.cid;
|
|
969
|
+
}
|
|
970
|
+
async function pinToInfura(data, projectId, projectSecret) {
|
|
971
|
+
const formData = new FormData();
|
|
972
|
+
const buffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
973
|
+
formData.append("file", new Blob([buffer]), "recovery.json");
|
|
974
|
+
const auth = Buffer.from(`${projectId}:${projectSecret}`).toString("base64");
|
|
975
|
+
const response = await fetch("https://ipfs.infura.io:5001/api/v0/add", {
|
|
976
|
+
method: "POST",
|
|
977
|
+
headers: {
|
|
978
|
+
"Authorization": `Basic ${auth}`
|
|
979
|
+
},
|
|
980
|
+
body: formData
|
|
981
|
+
});
|
|
982
|
+
if (!response.ok) {
|
|
983
|
+
const error = await response.text();
|
|
984
|
+
throw new Error(`Infura error: ${response.status} - ${error}`);
|
|
985
|
+
}
|
|
986
|
+
const result = await response.json();
|
|
987
|
+
return result.Hash;
|
|
988
|
+
}
|
|
989
|
+
async function fetchFromIPFS(cid) {
|
|
990
|
+
const gateways = [
|
|
991
|
+
`https://gateway.pinata.cloud/ipfs/${cid}`,
|
|
992
|
+
`https://w3s.link/ipfs/${cid}`,
|
|
993
|
+
`https://ipfs.infura.io/ipfs/${cid}`,
|
|
994
|
+
`https://ipfs.io/ipfs/${cid}`,
|
|
995
|
+
`https://cloudflare-ipfs.com/ipfs/${cid}`,
|
|
996
|
+
`https://dweb.link/ipfs/${cid}`
|
|
997
|
+
];
|
|
998
|
+
for (const gateway of gateways) {
|
|
999
|
+
try {
|
|
1000
|
+
const response = await fetch(gateway, {
|
|
1001
|
+
headers: {
|
|
1002
|
+
"Accept": "application/octet-stream"
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
1005
|
+
if (response.ok) {
|
|
1006
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1007
|
+
}
|
|
1008
|
+
} catch {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
throw new Error("Failed to fetch from IPFS - tried all gateways");
|
|
1013
|
+
}
|
|
1014
|
+
function createIPFSRecoveryManager(config) {
|
|
1015
|
+
const MIN_PASSWORD_LENGTH = 12;
|
|
1016
|
+
async function pinData(data) {
|
|
1017
|
+
if (config.customPin) {
|
|
1018
|
+
return config.customPin(data);
|
|
1019
|
+
}
|
|
1020
|
+
switch (config.pinningService) {
|
|
1021
|
+
case "pinata":
|
|
1022
|
+
if (!config.apiKey || !config.apiSecret) {
|
|
1023
|
+
throw new Error("Pinata requires apiKey and apiSecret");
|
|
1024
|
+
}
|
|
1025
|
+
return pinToPinata(data, config.apiKey, config.apiSecret);
|
|
1026
|
+
case "web3storage":
|
|
1027
|
+
if (!config.apiKey) {
|
|
1028
|
+
throw new Error("web3.storage requires apiKey (API token)");
|
|
1029
|
+
}
|
|
1030
|
+
return pinToWeb3Storage(data, config.apiKey);
|
|
1031
|
+
case "infura":
|
|
1032
|
+
if (!config.projectId || !config.apiSecret) {
|
|
1033
|
+
throw new Error("Infura requires projectId and apiSecret");
|
|
1034
|
+
}
|
|
1035
|
+
return pinToInfura(data, config.projectId, config.apiSecret);
|
|
1036
|
+
case "custom":
|
|
1037
|
+
throw new Error("Custom pinning requires customPin function");
|
|
1038
|
+
default:
|
|
1039
|
+
throw new Error(`Unknown pinning service: ${config.pinningService}`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
async function fetchData(cid) {
|
|
1043
|
+
if (config.customFetch) {
|
|
1044
|
+
return config.customFetch(cid);
|
|
1045
|
+
}
|
|
1046
|
+
return fetchFromIPFS(cid);
|
|
1047
|
+
}
|
|
1048
|
+
function calculatePasswordStrength(password) {
|
|
1049
|
+
let score = 0;
|
|
1050
|
+
if (password.length >= 12) score++;
|
|
1051
|
+
if (password.length >= 16) score++;
|
|
1052
|
+
if (/[a-z]/.test(password)) score++;
|
|
1053
|
+
if (/[A-Z]/.test(password)) score++;
|
|
1054
|
+
if (/[0-9]/.test(password)) score++;
|
|
1055
|
+
if (/[^a-zA-Z0-9]/.test(password)) score++;
|
|
1056
|
+
if (score <= 2) return "weak";
|
|
1057
|
+
if (score <= 4) return "medium";
|
|
1058
|
+
return "strong";
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
async createRecoveryBackup(payload, password) {
|
|
1062
|
+
const validation = this.validatePassword(password);
|
|
1063
|
+
if (!validation.valid) {
|
|
1064
|
+
throw new Error(`Invalid password: ${validation.errors.join(", ")}`);
|
|
1065
|
+
}
|
|
1066
|
+
const encrypted = await encryptRecoveryData(payload, password);
|
|
1067
|
+
const data = new TextEncoder().encode(JSON.stringify(encrypted));
|
|
1068
|
+
const cid = await pinData(data);
|
|
1069
|
+
console.log(`[IPFS] Recovery backup created: ${cid} (${config.pinningService})`);
|
|
1070
|
+
return { cid };
|
|
1071
|
+
},
|
|
1072
|
+
async recoverFromBackup(cid, password) {
|
|
1073
|
+
const data = await fetchData(cid);
|
|
1074
|
+
const encrypted = JSON.parse(
|
|
1075
|
+
new TextDecoder().decode(data)
|
|
1076
|
+
);
|
|
1077
|
+
return decryptRecoveryData(encrypted, password);
|
|
1078
|
+
},
|
|
1079
|
+
validatePassword(password) {
|
|
1080
|
+
const errors = [];
|
|
1081
|
+
if (password.length < MIN_PASSWORD_LENGTH) {
|
|
1082
|
+
errors.push(`Password must be at least ${MIN_PASSWORD_LENGTH} characters`);
|
|
1083
|
+
}
|
|
1084
|
+
if (!/[a-z]/.test(password)) {
|
|
1085
|
+
errors.push("Password must contain lowercase letters");
|
|
1086
|
+
}
|
|
1087
|
+
if (!/[A-Z]/.test(password)) {
|
|
1088
|
+
errors.push("Password must contain uppercase letters");
|
|
1089
|
+
}
|
|
1090
|
+
if (!/[0-9]/.test(password)) {
|
|
1091
|
+
errors.push("Password must contain numbers");
|
|
1092
|
+
}
|
|
1093
|
+
const strength = calculatePasswordStrength(password);
|
|
1094
|
+
return {
|
|
1095
|
+
valid: errors.length === 0,
|
|
1096
|
+
errors,
|
|
1097
|
+
strength
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// src/server/middleware.ts
|
|
1104
|
+
function createAuthMiddleware(sessionManager, db) {
|
|
1105
|
+
return async (req, res, next) => {
|
|
1106
|
+
try {
|
|
1107
|
+
const session = await sessionManager.getSession(req);
|
|
1108
|
+
if (session) {
|
|
1109
|
+
const user = await db.getUserById(session.userId);
|
|
1110
|
+
if (user) {
|
|
1111
|
+
req.anonUser = user;
|
|
1112
|
+
req.anonSession = session;
|
|
1113
|
+
await sessionManager.refreshSession(req, res);
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
next();
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
console.error("[AnonAuth] Middleware error:", error);
|
|
1119
|
+
next();
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
function createRequireAuth(sessionManager, db) {
|
|
1124
|
+
return async (req, res, next) => {
|
|
1125
|
+
try {
|
|
1126
|
+
const session = await sessionManager.getSession(req);
|
|
1127
|
+
if (!session) {
|
|
1128
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
1129
|
+
}
|
|
1130
|
+
const user = await db.getUserById(session.userId);
|
|
1131
|
+
if (!user) {
|
|
1132
|
+
return res.status(401).json({ error: "User not found" });
|
|
1133
|
+
}
|
|
1134
|
+
req.anonUser = user;
|
|
1135
|
+
req.anonSession = session;
|
|
1136
|
+
await sessionManager.refreshSession(req, res);
|
|
1137
|
+
next();
|
|
1138
|
+
} catch (error) {
|
|
1139
|
+
console.error("[AnonAuth] Auth check error:", error);
|
|
1140
|
+
res.status(500).json({ error: "Authentication check failed" });
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
var NATO_PHONETIC = [
|
|
1145
|
+
"ALPHA",
|
|
1146
|
+
"BRAVO",
|
|
1147
|
+
"CHARLIE",
|
|
1148
|
+
"DELTA",
|
|
1149
|
+
"ECHO",
|
|
1150
|
+
"FOXTROT",
|
|
1151
|
+
"GOLF",
|
|
1152
|
+
"HOTEL",
|
|
1153
|
+
"INDIA",
|
|
1154
|
+
"JULIET",
|
|
1155
|
+
"KILO",
|
|
1156
|
+
"LIMA",
|
|
1157
|
+
"MIKE",
|
|
1158
|
+
"NOVEMBER",
|
|
1159
|
+
"OSCAR",
|
|
1160
|
+
"PAPA",
|
|
1161
|
+
"QUEBEC",
|
|
1162
|
+
"ROMEO",
|
|
1163
|
+
"SIERRA",
|
|
1164
|
+
"TANGO",
|
|
1165
|
+
"UNIFORM",
|
|
1166
|
+
"VICTOR",
|
|
1167
|
+
"WHISKEY",
|
|
1168
|
+
"XRAY",
|
|
1169
|
+
"YANKEE",
|
|
1170
|
+
"ZULU"
|
|
1171
|
+
];
|
|
1172
|
+
var ADJECTIVES = [
|
|
1173
|
+
"SWIFT",
|
|
1174
|
+
"SILENT",
|
|
1175
|
+
"SHADOW",
|
|
1176
|
+
"STEEL",
|
|
1177
|
+
"STORM",
|
|
1178
|
+
"FROST",
|
|
1179
|
+
"CRIMSON",
|
|
1180
|
+
"GOLDEN",
|
|
1181
|
+
"SILVER",
|
|
1182
|
+
"IRON",
|
|
1183
|
+
"DARK",
|
|
1184
|
+
"BRIGHT",
|
|
1185
|
+
"RAPID",
|
|
1186
|
+
"GHOST",
|
|
1187
|
+
"PHANTOM",
|
|
1188
|
+
"ARCTIC",
|
|
1189
|
+
"DESERT",
|
|
1190
|
+
"OCEAN",
|
|
1191
|
+
"MOUNTAIN",
|
|
1192
|
+
"FOREST",
|
|
1193
|
+
"THUNDER",
|
|
1194
|
+
"LIGHTNING",
|
|
1195
|
+
"COSMIC"
|
|
1196
|
+
];
|
|
1197
|
+
var ANIMALS = [
|
|
1198
|
+
"FALCON",
|
|
1199
|
+
"EAGLE",
|
|
1200
|
+
"HAWK",
|
|
1201
|
+
"WOLF",
|
|
1202
|
+
"BEAR",
|
|
1203
|
+
"LION",
|
|
1204
|
+
"TIGER",
|
|
1205
|
+
"PANTHER",
|
|
1206
|
+
"COBRA",
|
|
1207
|
+
"VIPER",
|
|
1208
|
+
"RAVEN",
|
|
1209
|
+
"OWL",
|
|
1210
|
+
"SHARK",
|
|
1211
|
+
"DRAGON",
|
|
1212
|
+
"PHOENIX",
|
|
1213
|
+
"GRIFFIN",
|
|
1214
|
+
"LEOPARD",
|
|
1215
|
+
"JAGUAR",
|
|
1216
|
+
"LYNX",
|
|
1217
|
+
"FOX",
|
|
1218
|
+
"ORCA",
|
|
1219
|
+
"RAPTOR",
|
|
1220
|
+
"CONDOR"
|
|
1221
|
+
];
|
|
1222
|
+
function randomSuffix() {
|
|
1223
|
+
const bytes = crypto$1.randomBytes(1);
|
|
1224
|
+
return bytes[0] % 99 + 1;
|
|
1225
|
+
}
|
|
1226
|
+
function randomPick(array) {
|
|
1227
|
+
const bytes = crypto$1.randomBytes(1);
|
|
1228
|
+
return array[bytes[0] % array.length];
|
|
1229
|
+
}
|
|
1230
|
+
function generateNatoCodename() {
|
|
1231
|
+
const word = randomPick(NATO_PHONETIC);
|
|
1232
|
+
const num = randomSuffix();
|
|
1233
|
+
return `${word}-${num}`;
|
|
1234
|
+
}
|
|
1235
|
+
function generateAnimalCodename() {
|
|
1236
|
+
const adj = randomPick(ADJECTIVES);
|
|
1237
|
+
const animal = randomPick(ANIMALS);
|
|
1238
|
+
const num = randomSuffix();
|
|
1239
|
+
return `${adj}-${animal}-${num}`;
|
|
1240
|
+
}
|
|
1241
|
+
function generateCodename(style = "nato-phonetic") {
|
|
1242
|
+
switch (style) {
|
|
1243
|
+
case "nato-phonetic":
|
|
1244
|
+
return generateNatoCodename();
|
|
1245
|
+
case "animals":
|
|
1246
|
+
return generateAnimalCodename();
|
|
1247
|
+
default:
|
|
1248
|
+
return generateNatoCodename();
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
function isValidCodename(codename) {
|
|
1252
|
+
const natoPattern = /^[A-Z]+-\d{1,2}$/;
|
|
1253
|
+
const animalPattern = /^[A-Z]+-[A-Z]+-\d{1,2}$/;
|
|
1254
|
+
return natoPattern.test(codename) || animalPattern.test(codename);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// src/server/router.ts
|
|
1258
|
+
function createRouter(config) {
|
|
1259
|
+
const router = express.Router();
|
|
1260
|
+
const {
|
|
1261
|
+
db,
|
|
1262
|
+
sessionManager,
|
|
1263
|
+
passkeyManager,
|
|
1264
|
+
mpcManager,
|
|
1265
|
+
walletRecovery,
|
|
1266
|
+
ipfsRecovery
|
|
1267
|
+
} = config;
|
|
1268
|
+
router.use(express.json());
|
|
1269
|
+
router.post("/register/start", async (req, res) => {
|
|
1270
|
+
try {
|
|
1271
|
+
const tempUserId = crypto.randomUUID();
|
|
1272
|
+
const style = config.codename?.style || "nato-phonetic";
|
|
1273
|
+
let codename;
|
|
1274
|
+
if (config.codename?.generator) {
|
|
1275
|
+
codename = config.codename.generator(tempUserId);
|
|
1276
|
+
} else {
|
|
1277
|
+
codename = generateCodename(style);
|
|
1278
|
+
}
|
|
1279
|
+
let attempts = 0;
|
|
1280
|
+
while (await db.getUserByCodename(codename) && attempts < 10) {
|
|
1281
|
+
codename = generateCodename(style);
|
|
1282
|
+
attempts++;
|
|
1283
|
+
}
|
|
1284
|
+
if (attempts >= 10) {
|
|
1285
|
+
return res.status(500).json({ error: "Failed to generate unique codename" });
|
|
1286
|
+
}
|
|
1287
|
+
const { challengeId, options } = await passkeyManager.startRegistration(
|
|
1288
|
+
tempUserId,
|
|
1289
|
+
codename
|
|
1290
|
+
);
|
|
1291
|
+
res.json({
|
|
1292
|
+
challengeId,
|
|
1293
|
+
options,
|
|
1294
|
+
codename,
|
|
1295
|
+
tempUserId
|
|
1296
|
+
});
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
console.error("[AnonAuth] Registration start error:", error);
|
|
1299
|
+
res.status(500).json({ error: "Registration failed" });
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
router.post("/register/finish", async (req, res) => {
|
|
1303
|
+
try {
|
|
1304
|
+
const { challengeId, response, tempUserId, codename } = req.body;
|
|
1305
|
+
if (!challengeId || !response || !tempUserId || !codename) {
|
|
1306
|
+
return res.status(400).json({ error: "Missing required fields" });
|
|
1307
|
+
}
|
|
1308
|
+
if (!isValidCodename(codename)) {
|
|
1309
|
+
return res.status(400).json({ error: "Invalid codename format" });
|
|
1310
|
+
}
|
|
1311
|
+
const { verified, passkey } = await passkeyManager.finishRegistration(
|
|
1312
|
+
challengeId,
|
|
1313
|
+
response
|
|
1314
|
+
);
|
|
1315
|
+
if (!verified || !passkey) {
|
|
1316
|
+
return res.status(400).json({ error: "Passkey verification failed" });
|
|
1317
|
+
}
|
|
1318
|
+
const mpcAccount = await mpcManager.createAccount(tempUserId);
|
|
1319
|
+
const user = await db.createUser({
|
|
1320
|
+
codename,
|
|
1321
|
+
nearAccountId: mpcAccount.nearAccountId,
|
|
1322
|
+
mpcPublicKey: mpcAccount.mpcPublicKey,
|
|
1323
|
+
derivationPath: mpcAccount.derivationPath
|
|
1324
|
+
});
|
|
1325
|
+
const session = await sessionManager.createSession(user.id, res, {
|
|
1326
|
+
ipAddress: req.ip,
|
|
1327
|
+
userAgent: req.headers["user-agent"]
|
|
1328
|
+
});
|
|
1329
|
+
res.json({
|
|
1330
|
+
success: true,
|
|
1331
|
+
codename: user.codename,
|
|
1332
|
+
nearAccountId: user.nearAccountId
|
|
1333
|
+
});
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
console.error("[AnonAuth] Registration finish error:", error);
|
|
1336
|
+
res.status(500).json({ error: "Registration failed" });
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
router.post("/login/start", async (req, res) => {
|
|
1340
|
+
try {
|
|
1341
|
+
const { codename } = req.body;
|
|
1342
|
+
let userId;
|
|
1343
|
+
if (codename) {
|
|
1344
|
+
const user = await db.getUserByCodename(codename);
|
|
1345
|
+
if (!user) {
|
|
1346
|
+
return res.status(404).json({ error: "User not found" });
|
|
1347
|
+
}
|
|
1348
|
+
userId = user.id;
|
|
1349
|
+
}
|
|
1350
|
+
const { challengeId, options } = await passkeyManager.startAuthentication(userId);
|
|
1351
|
+
res.json({ challengeId, options });
|
|
1352
|
+
} catch (error) {
|
|
1353
|
+
console.error("[AnonAuth] Login start error:", error);
|
|
1354
|
+
res.status(500).json({ error: "Login failed" });
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
router.post("/login/finish", async (req, res) => {
|
|
1358
|
+
try {
|
|
1359
|
+
const { challengeId, response } = req.body;
|
|
1360
|
+
if (!challengeId || !response) {
|
|
1361
|
+
return res.status(400).json({ error: "Missing required fields" });
|
|
1362
|
+
}
|
|
1363
|
+
const { verified, userId } = await passkeyManager.finishAuthentication(
|
|
1364
|
+
challengeId,
|
|
1365
|
+
response
|
|
1366
|
+
);
|
|
1367
|
+
if (!verified || !userId) {
|
|
1368
|
+
return res.status(401).json({ error: "Authentication failed" });
|
|
1369
|
+
}
|
|
1370
|
+
const user = await db.getUserById(userId);
|
|
1371
|
+
if (!user) {
|
|
1372
|
+
return res.status(404).json({ error: "User not found" });
|
|
1373
|
+
}
|
|
1374
|
+
await sessionManager.createSession(user.id, res, {
|
|
1375
|
+
ipAddress: req.ip,
|
|
1376
|
+
userAgent: req.headers["user-agent"]
|
|
1377
|
+
});
|
|
1378
|
+
res.json({
|
|
1379
|
+
success: true,
|
|
1380
|
+
codename: user.codename
|
|
1381
|
+
});
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
console.error("[AnonAuth] Login finish error:", error);
|
|
1384
|
+
res.status(500).json({ error: "Authentication failed" });
|
|
1385
|
+
}
|
|
1386
|
+
});
|
|
1387
|
+
router.post("/logout", async (req, res) => {
|
|
1388
|
+
try {
|
|
1389
|
+
await sessionManager.destroySession(req, res);
|
|
1390
|
+
res.json({ success: true });
|
|
1391
|
+
} catch (error) {
|
|
1392
|
+
console.error("[AnonAuth] Logout error:", error);
|
|
1393
|
+
res.status(500).json({ error: "Logout failed" });
|
|
1394
|
+
}
|
|
1395
|
+
});
|
|
1396
|
+
router.get("/session", async (req, res) => {
|
|
1397
|
+
try {
|
|
1398
|
+
const session = await sessionManager.getSession(req);
|
|
1399
|
+
if (!session) {
|
|
1400
|
+
return res.status(401).json({ authenticated: false });
|
|
1401
|
+
}
|
|
1402
|
+
const user = await db.getUserById(session.userId);
|
|
1403
|
+
if (!user) {
|
|
1404
|
+
return res.status(401).json({ authenticated: false });
|
|
1405
|
+
}
|
|
1406
|
+
res.json({
|
|
1407
|
+
authenticated: true,
|
|
1408
|
+
codename: user.codename,
|
|
1409
|
+
nearAccountId: user.nearAccountId,
|
|
1410
|
+
expiresAt: session.expiresAt
|
|
1411
|
+
});
|
|
1412
|
+
} catch (error) {
|
|
1413
|
+
console.error("[AnonAuth] Session check error:", error);
|
|
1414
|
+
res.status(500).json({ error: "Session check failed" });
|
|
1415
|
+
}
|
|
1416
|
+
});
|
|
1417
|
+
if (walletRecovery) {
|
|
1418
|
+
router.post("/recovery/wallet/link", async (req, res) => {
|
|
1419
|
+
try {
|
|
1420
|
+
const session = await sessionManager.getSession(req);
|
|
1421
|
+
if (!session) {
|
|
1422
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
1423
|
+
}
|
|
1424
|
+
const { challenge: walletChallenge, expiresAt } = walletRecovery.generateLinkChallenge();
|
|
1425
|
+
await db.storeChallenge({
|
|
1426
|
+
id: crypto.randomUUID(),
|
|
1427
|
+
challenge: walletChallenge,
|
|
1428
|
+
type: "recovery",
|
|
1429
|
+
userId: session.userId,
|
|
1430
|
+
expiresAt,
|
|
1431
|
+
metadata: { action: "wallet-link" }
|
|
1432
|
+
});
|
|
1433
|
+
res.json({
|
|
1434
|
+
challenge: walletChallenge,
|
|
1435
|
+
expiresAt: expiresAt.toISOString()
|
|
1436
|
+
});
|
|
1437
|
+
} catch (error) {
|
|
1438
|
+
console.error("[AnonAuth] Wallet link error:", error);
|
|
1439
|
+
res.status(500).json({ error: "Failed to initiate wallet link" });
|
|
1440
|
+
}
|
|
1441
|
+
});
|
|
1442
|
+
router.post("/recovery/wallet/verify", async (req, res) => {
|
|
1443
|
+
try {
|
|
1444
|
+
const session = await sessionManager.getSession(req);
|
|
1445
|
+
if (!session) {
|
|
1446
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
1447
|
+
}
|
|
1448
|
+
const { signature, challenge, walletAccountId } = req.body;
|
|
1449
|
+
if (!signature || !challenge || !walletAccountId) {
|
|
1450
|
+
return res.status(400).json({ error: "Missing required fields" });
|
|
1451
|
+
}
|
|
1452
|
+
const { verified, walletId } = walletRecovery.verifyLinkSignature(
|
|
1453
|
+
signature,
|
|
1454
|
+
challenge
|
|
1455
|
+
);
|
|
1456
|
+
if (!verified) {
|
|
1457
|
+
return res.status(401).json({ error: "Invalid signature" });
|
|
1458
|
+
}
|
|
1459
|
+
const user = await db.getUserById(session.userId);
|
|
1460
|
+
if (!user) {
|
|
1461
|
+
return res.status(404).json({ error: "User not found" });
|
|
1462
|
+
}
|
|
1463
|
+
await mpcManager.addRecoveryWallet(user.nearAccountId, walletAccountId);
|
|
1464
|
+
await db.storeRecoveryData({
|
|
1465
|
+
userId: user.id,
|
|
1466
|
+
type: "wallet",
|
|
1467
|
+
reference: "enabled",
|
|
1468
|
+
// We don't store the wallet ID!
|
|
1469
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1470
|
+
});
|
|
1471
|
+
res.json({
|
|
1472
|
+
success: true,
|
|
1473
|
+
message: "Wallet linked for recovery. The link is stored on-chain, not in our database."
|
|
1474
|
+
});
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
console.error("[AnonAuth] Wallet verify error:", error);
|
|
1477
|
+
res.status(500).json({ error: "Failed to verify wallet" });
|
|
1478
|
+
}
|
|
1479
|
+
});
|
|
1480
|
+
router.post("/recovery/wallet/start", async (req, res) => {
|
|
1481
|
+
try {
|
|
1482
|
+
const { challenge, expiresAt } = walletRecovery.generateRecoveryChallenge();
|
|
1483
|
+
res.json({
|
|
1484
|
+
challenge,
|
|
1485
|
+
expiresAt: expiresAt.toISOString()
|
|
1486
|
+
});
|
|
1487
|
+
} catch (error) {
|
|
1488
|
+
console.error("[AnonAuth] Wallet recovery start error:", error);
|
|
1489
|
+
res.status(500).json({ error: "Failed to start recovery" });
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
router.post("/recovery/wallet/finish", async (req, res) => {
|
|
1493
|
+
try {
|
|
1494
|
+
const { signature, challenge, nearAccountId } = req.body;
|
|
1495
|
+
if (!signature || !challenge || !nearAccountId) {
|
|
1496
|
+
return res.status(400).json({ error: "Missing required fields" });
|
|
1497
|
+
}
|
|
1498
|
+
const { verified } = await walletRecovery.verifyRecoverySignature(
|
|
1499
|
+
signature,
|
|
1500
|
+
challenge,
|
|
1501
|
+
nearAccountId
|
|
1502
|
+
);
|
|
1503
|
+
if (!verified) {
|
|
1504
|
+
return res.status(401).json({ error: "Recovery verification failed" });
|
|
1505
|
+
}
|
|
1506
|
+
const user = await db.getUserByNearAccount(nearAccountId);
|
|
1507
|
+
if (!user) {
|
|
1508
|
+
return res.status(404).json({ error: "Account not found" });
|
|
1509
|
+
}
|
|
1510
|
+
await sessionManager.createSession(user.id, res, {
|
|
1511
|
+
ipAddress: req.ip,
|
|
1512
|
+
userAgent: req.headers["user-agent"]
|
|
1513
|
+
});
|
|
1514
|
+
res.json({
|
|
1515
|
+
success: true,
|
|
1516
|
+
codename: user.codename,
|
|
1517
|
+
message: "Recovery successful. You can now register a new passkey."
|
|
1518
|
+
});
|
|
1519
|
+
} catch (error) {
|
|
1520
|
+
console.error("[AnonAuth] Wallet recovery finish error:", error);
|
|
1521
|
+
res.status(500).json({ error: "Recovery failed" });
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
if (ipfsRecovery) {
|
|
1526
|
+
router.post("/recovery/ipfs/setup", async (req, res) => {
|
|
1527
|
+
try {
|
|
1528
|
+
const session = await sessionManager.getSession(req);
|
|
1529
|
+
if (!session) {
|
|
1530
|
+
return res.status(401).json({ error: "Authentication required" });
|
|
1531
|
+
}
|
|
1532
|
+
const { password } = req.body;
|
|
1533
|
+
if (!password) {
|
|
1534
|
+
return res.status(400).json({ error: "Password required" });
|
|
1535
|
+
}
|
|
1536
|
+
const validation = ipfsRecovery.validatePassword(password);
|
|
1537
|
+
if (!validation.valid) {
|
|
1538
|
+
return res.status(400).json({
|
|
1539
|
+
error: "Password too weak",
|
|
1540
|
+
details: validation.errors
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
const user = await db.getUserById(session.userId);
|
|
1544
|
+
if (!user) {
|
|
1545
|
+
return res.status(404).json({ error: "User not found" });
|
|
1546
|
+
}
|
|
1547
|
+
const { cid } = await ipfsRecovery.createRecoveryBackup(
|
|
1548
|
+
{
|
|
1549
|
+
userId: user.id,
|
|
1550
|
+
nearAccountId: user.nearAccountId,
|
|
1551
|
+
derivationPath: user.derivationPath,
|
|
1552
|
+
createdAt: Date.now()
|
|
1553
|
+
},
|
|
1554
|
+
password
|
|
1555
|
+
);
|
|
1556
|
+
await db.storeRecoveryData({
|
|
1557
|
+
userId: user.id,
|
|
1558
|
+
type: "ipfs",
|
|
1559
|
+
reference: cid,
|
|
1560
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
1561
|
+
});
|
|
1562
|
+
res.json({
|
|
1563
|
+
success: true,
|
|
1564
|
+
cid,
|
|
1565
|
+
message: "Backup created. Save this CID with your password - you need both to recover."
|
|
1566
|
+
});
|
|
1567
|
+
} catch (error) {
|
|
1568
|
+
console.error("[AnonAuth] IPFS setup error:", error);
|
|
1569
|
+
res.status(500).json({ error: "Failed to create backup" });
|
|
1570
|
+
}
|
|
1571
|
+
});
|
|
1572
|
+
router.post("/recovery/ipfs/recover", async (req, res) => {
|
|
1573
|
+
try {
|
|
1574
|
+
const { cid, password } = req.body;
|
|
1575
|
+
if (!cid || !password) {
|
|
1576
|
+
return res.status(400).json({ error: "CID and password required" });
|
|
1577
|
+
}
|
|
1578
|
+
let payload;
|
|
1579
|
+
try {
|
|
1580
|
+
payload = await ipfsRecovery.recoverFromBackup(cid, password);
|
|
1581
|
+
} catch {
|
|
1582
|
+
return res.status(401).json({ error: "Invalid password or CID" });
|
|
1583
|
+
}
|
|
1584
|
+
const user = await db.getUserById(payload.userId);
|
|
1585
|
+
if (!user) {
|
|
1586
|
+
return res.status(404).json({ error: "Account not found" });
|
|
1587
|
+
}
|
|
1588
|
+
await sessionManager.createSession(user.id, res, {
|
|
1589
|
+
ipAddress: req.ip,
|
|
1590
|
+
userAgent: req.headers["user-agent"]
|
|
1591
|
+
});
|
|
1592
|
+
res.json({
|
|
1593
|
+
success: true,
|
|
1594
|
+
codename: user.codename,
|
|
1595
|
+
message: "Recovery successful. You can now register a new passkey."
|
|
1596
|
+
});
|
|
1597
|
+
} catch (error) {
|
|
1598
|
+
console.error("[AnonAuth] IPFS recovery error:", error);
|
|
1599
|
+
res.status(500).json({ error: "Recovery failed" });
|
|
1600
|
+
}
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
return router;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// src/server/index.ts
|
|
1607
|
+
function createAnonAuth(config) {
|
|
1608
|
+
let db;
|
|
1609
|
+
if (config.database.adapter) {
|
|
1610
|
+
db = config.database.adapter;
|
|
1611
|
+
} else if (config.database.type === "postgres") {
|
|
1612
|
+
if (!config.database.connectionString) {
|
|
1613
|
+
throw new Error("PostgreSQL requires connectionString");
|
|
1614
|
+
}
|
|
1615
|
+
db = createPostgresAdapter({
|
|
1616
|
+
connectionString: config.database.connectionString
|
|
1617
|
+
});
|
|
1618
|
+
} else if (config.database.type === "custom") {
|
|
1619
|
+
if (!config.database.adapter) {
|
|
1620
|
+
throw new Error("Custom database type requires adapter");
|
|
1621
|
+
}
|
|
1622
|
+
db = config.database.adapter;
|
|
1623
|
+
} else {
|
|
1624
|
+
throw new Error(`Unsupported database type: ${config.database.type}`);
|
|
1625
|
+
}
|
|
1626
|
+
const sessionManager = createSessionManager(db, {
|
|
1627
|
+
secret: config.sessionSecret,
|
|
1628
|
+
durationMs: config.sessionDurationMs
|
|
1629
|
+
});
|
|
1630
|
+
const rpConfig = config.rp || {
|
|
1631
|
+
name: "Anonymous Auth",
|
|
1632
|
+
id: "localhost",
|
|
1633
|
+
origin: "http://localhost:3000"
|
|
1634
|
+
};
|
|
1635
|
+
const passkeyManager = createPasskeyManager(db, {
|
|
1636
|
+
rpName: rpConfig.name,
|
|
1637
|
+
rpId: rpConfig.id,
|
|
1638
|
+
origin: rpConfig.origin
|
|
1639
|
+
});
|
|
1640
|
+
const mpcManager = createMPCManager({
|
|
1641
|
+
networkId: config.nearNetwork,
|
|
1642
|
+
accountPrefix: "anon"
|
|
1643
|
+
});
|
|
1644
|
+
let walletRecovery;
|
|
1645
|
+
let ipfsRecovery;
|
|
1646
|
+
if (config.recovery?.wallet) {
|
|
1647
|
+
walletRecovery = createWalletRecoveryManager({
|
|
1648
|
+
nearNetwork: config.nearNetwork
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
if (config.recovery?.ipfs) {
|
|
1652
|
+
ipfsRecovery = createIPFSRecoveryManager(config.recovery.ipfs);
|
|
1653
|
+
}
|
|
1654
|
+
const middleware = createAuthMiddleware(sessionManager, db);
|
|
1655
|
+
const requireAuth = createRequireAuth(sessionManager, db);
|
|
1656
|
+
const router = createRouter({
|
|
1657
|
+
db,
|
|
1658
|
+
sessionManager,
|
|
1659
|
+
passkeyManager,
|
|
1660
|
+
mpcManager,
|
|
1661
|
+
walletRecovery,
|
|
1662
|
+
ipfsRecovery,
|
|
1663
|
+
codename: config.codename
|
|
1664
|
+
});
|
|
1665
|
+
return {
|
|
1666
|
+
router,
|
|
1667
|
+
middleware,
|
|
1668
|
+
requireAuth,
|
|
1669
|
+
async initialize() {
|
|
1670
|
+
await db.initialize();
|
|
1671
|
+
},
|
|
1672
|
+
db,
|
|
1673
|
+
sessionManager,
|
|
1674
|
+
passkeyManager,
|
|
1675
|
+
mpcManager,
|
|
1676
|
+
walletRecovery,
|
|
1677
|
+
ipfsRecovery
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
exports.POSTGRES_SCHEMA = POSTGRES_SCHEMA;
|
|
1682
|
+
exports.createAnonAuth = createAnonAuth;
|
|
1683
|
+
exports.createPostgresAdapter = createPostgresAdapter;
|
|
1684
|
+
exports.generateCodename = generateCodename;
|
|
1685
|
+
exports.isValidCodename = isValidCodename;
|
|
1686
|
+
//# sourceMappingURL=index.cjs.map
|
|
1687
|
+
//# sourceMappingURL=index.cjs.map
|