@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.
@@ -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