@factiii/auth 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2096 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ DEFAULT_STORAGE_KEYS: () => DEFAULT_STORAGE_KEYS,
34
+ OAuthVerificationError: () => OAuthVerificationError,
35
+ biometricVerifySchema: () => biometricVerifySchema,
36
+ changePasswordSchema: () => changePasswordSchema,
37
+ cleanBase32String: () => cleanBase32String,
38
+ clearAuthCookies: () => clearAuthCookies,
39
+ comparePassword: () => comparePassword,
40
+ createAccessToken: () => createAccessToken,
41
+ createAuthConfig: () => createAuthConfig,
42
+ createAuthGuard: () => createAuthGuard,
43
+ createAuthRouter: () => createAuthRouter,
44
+ createConsoleEmailAdapter: () => createConsoleEmailAdapter,
45
+ createNoopEmailAdapter: () => createNoopEmailAdapter,
46
+ createOAuthVerifier: () => createOAuthVerifier,
47
+ decodeToken: () => decodeToken,
48
+ defaultAuthConfig: () => defaultAuthConfig,
49
+ defaultCookieSettings: () => defaultCookieSettings,
50
+ defaultStorageKeys: () => defaultStorageKeys,
51
+ defaultTokenSettings: () => defaultTokenSettings,
52
+ detectBrowser: () => detectBrowser,
53
+ endAllSessionsSchema: () => endAllSessionsSchema,
54
+ generateOtp: () => generateOtp,
55
+ generateTotpCode: () => generateTotpCode,
56
+ generateTotpSecret: () => generateTotpSecret,
57
+ hashPassword: () => hashPassword,
58
+ isMobileDevice: () => isMobileDevice,
59
+ isNativeApp: () => isNativeApp,
60
+ isTokenExpiredError: () => isTokenExpiredError,
61
+ isTokenInvalidError: () => isTokenInvalidError,
62
+ loginSchema: () => loginSchema,
63
+ logoutSchema: () => logoutSchema,
64
+ oAuthLoginSchema: () => oAuthLoginSchema,
65
+ otpLoginRequestSchema: () => otpLoginRequestSchema,
66
+ otpLoginVerifySchema: () => otpLoginVerifySchema,
67
+ parseAuthCookies: () => parseAuthCookies,
68
+ requestPasswordResetSchema: () => requestPasswordResetSchema,
69
+ resetPasswordSchema: () => resetPasswordSchema,
70
+ setAuthCookies: () => setAuthCookies,
71
+ signupSchema: () => signupSchema,
72
+ twoFaResetSchema: () => twoFaResetSchema,
73
+ twoFaSetupSchema: () => twoFaSetupSchema,
74
+ twoFaVerifySchema: () => twoFaVerifySchema,
75
+ validatePasswordStrength: () => validatePasswordStrength,
76
+ verifyAccessToken: () => verifyAccessToken,
77
+ verifyEmailSchema: () => verifyEmailSchema,
78
+ verifyTotp: () => verifyTotp
79
+ });
80
+ module.exports = __toCommonJS(index_exports);
81
+
82
+ // src/middleware/authGuard.ts
83
+ var import_server = require("@trpc/server");
84
+
85
+ // src/adapters/email.ts
86
+ function createNoopEmailAdapter() {
87
+ return {
88
+ async sendVerificationEmail(email, code) {
89
+ console.log(
90
+ `[NoopEmailAdapter] Would send verification email to ${email} with code ${code}`
91
+ );
92
+ },
93
+ async sendPasswordResetEmail(email, token) {
94
+ console.log(
95
+ `[NoopEmailAdapter] Would send password reset email to ${email} with token ${token}`
96
+ );
97
+ },
98
+ async sendOTPEmail(email, otp) {
99
+ console.log(
100
+ `[NoopEmailAdapter] Would send OTP email to ${email} with code ${otp}`
101
+ );
102
+ },
103
+ async sendLoginNotification(email, browserName, ip) {
104
+ console.log(
105
+ `[NoopEmailAdapter] Would send login notification to ${email} from ${browserName} (${ip})`
106
+ );
107
+ }
108
+ };
109
+ }
110
+ function createConsoleEmailAdapter() {
111
+ return {
112
+ async sendVerificationEmail(email, code) {
113
+ console.log("\n=== EMAIL: Verification ===");
114
+ console.log(`To: ${email}`);
115
+ console.log(`Code: ${code}`);
116
+ console.log("===========================\n");
117
+ },
118
+ async sendPasswordResetEmail(email, token) {
119
+ console.log("\n=== EMAIL: Password Reset ===");
120
+ console.log(`To: ${email}`);
121
+ console.log(`Token: ${token}`);
122
+ console.log("=============================\n");
123
+ },
124
+ async sendOTPEmail(email, otp) {
125
+ console.log("\n=== EMAIL: OTP Login ===");
126
+ console.log(`To: ${email}`);
127
+ console.log(`OTP: ${otp}`);
128
+ console.log("========================\n");
129
+ },
130
+ async sendLoginNotification(email, browserName, ip) {
131
+ console.log("\n=== EMAIL: Login Notification ===");
132
+ console.log(`To: ${email}`);
133
+ console.log(`Browser: ${browserName}`);
134
+ console.log(`IP: ${ip || "Unknown"}`);
135
+ console.log("=================================\n");
136
+ }
137
+ };
138
+ }
139
+
140
+ // src/utilities/config.ts
141
+ var defaultTokenSettings = {
142
+ accessTokenExpiry: "5m",
143
+ passwordResetExpiryMs: 60 * 60 * 1e3,
144
+ // 1 hour
145
+ otpValidityMs: 15 * 60 * 1e3
146
+ // 15 minutes
147
+ };
148
+ var defaultCookieSettings = {
149
+ secure: true,
150
+ sameSite: "Strict",
151
+ httpOnly: true,
152
+ accessTokenPath: "/",
153
+ refreshTokenPath: "/api/trpc/auth.refresh",
154
+ maxAge: 365 * 24 * 60 * 60
155
+ // 1 year in seconds
156
+ };
157
+ var defaultStorageKeys = {
158
+ accessToken: "auth-at",
159
+ refreshToken: "auth-rt"
160
+ };
161
+ var defaultFeatures = {
162
+ twoFa: true,
163
+ oauth: { google: true, apple: true },
164
+ biometric: false,
165
+ emailVerification: true,
166
+ passwordReset: true,
167
+ otpLogin: true
168
+ };
169
+ function createAuthConfig(config) {
170
+ const emailService = config.emailService ?? createNoopEmailAdapter();
171
+ return {
172
+ ...config,
173
+ features: { ...defaultFeatures, ...config.features },
174
+ tokenSettings: { ...defaultTokenSettings, ...config.tokenSettings },
175
+ cookieSettings: { ...defaultCookieSettings, ...config.cookieSettings },
176
+ storageKeys: { ...defaultStorageKeys, ...config.storageKeys },
177
+ generateUsername: config.generateUsername ?? (() => `user_${Date.now()}`),
178
+ emailService
179
+ };
180
+ }
181
+ var defaultAuthConfig = {
182
+ features: defaultFeatures,
183
+ tokenSettings: defaultTokenSettings,
184
+ cookieSettings: defaultCookieSettings,
185
+ storageKeys: defaultStorageKeys
186
+ };
187
+
188
+ // src/utilities/cookies.ts
189
+ var DEFAULT_STORAGE_KEYS = {
190
+ ACCESS_TOKEN: "auth-at",
191
+ REFRESH_TOKEN: "auth-rt"
192
+ };
193
+ function parseAuthCookies(cookieHeader, storageKeys = {
194
+ accessToken: DEFAULT_STORAGE_KEYS.ACCESS_TOKEN,
195
+ refreshToken: DEFAULT_STORAGE_KEYS.REFRESH_TOKEN
196
+ }) {
197
+ if (!cookieHeader) {
198
+ return {};
199
+ }
200
+ const accessToken = cookieHeader.split(`${storageKeys.accessToken}=`)[1]?.split(";")[0];
201
+ const refreshToken = cookieHeader.split(`${storageKeys.refreshToken}=`)[1]?.split(";")[0];
202
+ return {
203
+ accessToken: accessToken || void 0,
204
+ refreshToken: refreshToken || void 0
205
+ };
206
+ }
207
+ function extractDomain(req) {
208
+ const origin = req.headers.origin;
209
+ if (origin) {
210
+ try {
211
+ return new URL(origin).hostname;
212
+ } catch {
213
+ }
214
+ }
215
+ const referer = req.headers.referer;
216
+ if (referer) {
217
+ try {
218
+ return new URL(referer).hostname;
219
+ } catch {
220
+ }
221
+ }
222
+ const host = req.headers.host;
223
+ if (host) {
224
+ return host.split(":")[0];
225
+ }
226
+ return void 0;
227
+ }
228
+ function setAuthCookies(res, credentials, settings, storageKeys = {
229
+ accessToken: DEFAULT_STORAGE_KEYS.ACCESS_TOKEN,
230
+ refreshToken: DEFAULT_STORAGE_KEYS.REFRESH_TOKEN
231
+ }) {
232
+ const cookies = [];
233
+ const domain = settings.domain ?? extractDomain(res.req);
234
+ const expiresDate = settings.maxAge ? new Date(Date.now() + settings.maxAge * 1e3).toUTCString() : void 0;
235
+ if (credentials.refreshToken) {
236
+ const refreshCookie = [
237
+ `${storageKeys.refreshToken}=${credentials.refreshToken}`,
238
+ "HttpOnly",
239
+ settings.secure ? "Secure=true" : "",
240
+ `SameSite=${settings.sameSite}`,
241
+ `Path=${settings.refreshTokenPath}`,
242
+ domain ? `Domain=${domain}` : "",
243
+ `Expires=${expiresDate}`
244
+ ].filter(Boolean).join("; ");
245
+ cookies.push(refreshCookie);
246
+ }
247
+ if (credentials.accessToken) {
248
+ const accessCookie = [
249
+ `${storageKeys.accessToken}=${credentials.accessToken}`,
250
+ settings.secure ? "Secure=true" : "",
251
+ `SameSite=${settings.sameSite}`,
252
+ `Path=${settings.accessTokenPath}`,
253
+ domain ? `Domain=${domain}` : "",
254
+ `Expires=${expiresDate}`
255
+ ].filter(Boolean).join("; ");
256
+ cookies.push(accessCookie);
257
+ }
258
+ if (cookies.length > 0) {
259
+ res.setHeader("Set-Cookie", cookies);
260
+ }
261
+ }
262
+ function clearAuthCookies(res, settings, storageKeys = {
263
+ accessToken: DEFAULT_STORAGE_KEYS.ACCESS_TOKEN,
264
+ refreshToken: DEFAULT_STORAGE_KEYS.REFRESH_TOKEN
265
+ }) {
266
+ const domain = extractDomain(res.req);
267
+ const expiredDate = (/* @__PURE__ */ new Date(0)).toUTCString();
268
+ const cookies = [
269
+ [
270
+ `${storageKeys.refreshToken}=destroy`,
271
+ "HttpOnly",
272
+ settings.secure ? "Secure=true" : "",
273
+ `SameSite=${settings.sameSite}`,
274
+ `Path=${settings.refreshTokenPath}`,
275
+ domain ? `Domain=${domain}` : "",
276
+ `Expires=${expiredDate}`
277
+ ].filter(Boolean).join("; "),
278
+ [
279
+ `${storageKeys.accessToken}=destroy`,
280
+ settings.secure ? "Secure=true" : "",
281
+ `SameSite=${settings.sameSite}`,
282
+ `Path=${settings.accessTokenPath}`,
283
+ domain ? `Domain=${domain}` : "",
284
+ `Expires=${expiredDate}`
285
+ ].filter(Boolean).join("; ")
286
+ ];
287
+ res.setHeader("Set-Cookie", cookies);
288
+ }
289
+
290
+ // src/utilities/jwt.ts
291
+ var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
292
+ function createAccessToken(payload, options) {
293
+ return import_jsonwebtoken.default.sign(payload, options.secret, {
294
+ expiresIn: options.expiresIn
295
+ });
296
+ }
297
+ function verifyAccessToken(token, options) {
298
+ return import_jsonwebtoken.default.verify(token, options.secret, {
299
+ ignoreExpiration: options.ignoreExpiration ?? false
300
+ });
301
+ }
302
+ function decodeToken(token) {
303
+ try {
304
+ return import_jsonwebtoken.default.decode(token);
305
+ } catch {
306
+ return null;
307
+ }
308
+ }
309
+ function isJwtError(error) {
310
+ return error instanceof Error && ["TokenExpiredError", "JsonWebTokenError", "NotBeforeError"].includes(
311
+ error.name
312
+ );
313
+ }
314
+ function isTokenExpiredError(error) {
315
+ return isJwtError(error) && error.name === "TokenExpiredError";
316
+ }
317
+ function isTokenInvalidError(error) {
318
+ return isJwtError(error) && error.name === "JsonWebTokenError";
319
+ }
320
+
321
+ // src/middleware/authGuard.ts
322
+ function createAuthGuard(config, t) {
323
+ const storageKeys = config.storageKeys ?? defaultStorageKeys;
324
+ const cookieSettings = { ...defaultCookieSettings, ...config.cookieSettings };
325
+ const revokeSession = async (ctx, sessionId, description, errorStack, path) => {
326
+ clearAuthCookies(ctx.res, cookieSettings, storageKeys);
327
+ if (config.hooks?.logError) {
328
+ try {
329
+ const contextInfo = {
330
+ reason: description,
331
+ sessionId,
332
+ userId: ctx.userId,
333
+ ip: ctx.ip,
334
+ userAgent: ctx.headers["user-agent"],
335
+ ...path ? { path } : {},
336
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
337
+ };
338
+ const combinedStack = [
339
+ errorStack ? `Error Stack:
340
+ ${errorStack}` : null,
341
+ "Context:",
342
+ JSON.stringify(contextInfo, null, 2)
343
+ ].filter(Boolean).join("\n\n");
344
+ await config.hooks.logError({
345
+ type: "SECURITY",
346
+ description: `Session revoked: ${description}`,
347
+ stack: combinedStack,
348
+ ip: ctx.ip,
349
+ userId: ctx.userId ?? null
350
+ });
351
+ } catch {
352
+ }
353
+ }
354
+ if (sessionId) {
355
+ try {
356
+ await config.prisma.session.update({
357
+ where: { id: sessionId },
358
+ data: { revokedAt: /* @__PURE__ */ new Date() }
359
+ });
360
+ if (config.hooks?.onSessionRevoked) {
361
+ const session = await config.prisma.session.findUnique({
362
+ where: { id: sessionId },
363
+ select: { id: true, userId: true, socketId: true }
364
+ });
365
+ if (session) {
366
+ await config.hooks.onSessionRevoked(
367
+ session.userId,
368
+ session.socketId,
369
+ description
370
+ );
371
+ }
372
+ }
373
+ } catch {
374
+ }
375
+ }
376
+ };
377
+ const authGuard = t.middleware(async ({ ctx, meta, next, path }) => {
378
+ const cookies = parseAuthCookies(ctx.headers.cookie, storageKeys);
379
+ const authToken = cookies.accessToken;
380
+ const refreshToken = cookies.refreshToken;
381
+ const userAgent = ctx.headers["user-agent"];
382
+ if (!userAgent) {
383
+ throw new import_server.TRPCError({
384
+ code: "BAD_REQUEST",
385
+ message: "User agent is required"
386
+ });
387
+ }
388
+ if (authToken) {
389
+ try {
390
+ const decodedToken = verifyAccessToken(authToken, {
391
+ secret: config.secrets.jwt,
392
+ ignoreExpiration: meta?.ignoreExpiration ?? false
393
+ });
394
+ if (path === "auth.refresh" && !refreshToken) {
395
+ await revokeSession(
396
+ ctx,
397
+ decodedToken.id,
398
+ "Session revoked: No refresh token",
399
+ void 0,
400
+ path
401
+ );
402
+ throw new import_server.TRPCError({
403
+ message: "Unauthorized",
404
+ code: "UNAUTHORIZED"
405
+ });
406
+ }
407
+ const session = await config.prisma.session.findUnique({
408
+ where: {
409
+ id: decodedToken.id,
410
+ ...path === "auth.refresh" ? { refreshToken } : {}
411
+ },
412
+ select: {
413
+ userId: true,
414
+ user: {
415
+ select: {
416
+ status: true,
417
+ verifiedHumanAt: true
418
+ }
419
+ },
420
+ revokedAt: true,
421
+ socketId: true,
422
+ id: true
423
+ }
424
+ });
425
+ if (!session) {
426
+ await revokeSession(
427
+ ctx,
428
+ decodedToken.id,
429
+ "Session revoked: Session not found",
430
+ void 0,
431
+ path
432
+ );
433
+ throw new import_server.TRPCError({
434
+ message: "Unauthorized",
435
+ code: "UNAUTHORIZED"
436
+ });
437
+ }
438
+ if (session.user.status === "BANNED") {
439
+ await revokeSession(
440
+ ctx,
441
+ session.id,
442
+ "Session revoked: User banned",
443
+ void 0,
444
+ path
445
+ );
446
+ throw new import_server.TRPCError({
447
+ message: "Unauthorized",
448
+ code: "UNAUTHORIZED"
449
+ });
450
+ }
451
+ if (config.features?.biometric && config.hooks?.getBiometricTimeout) {
452
+ const timeoutMs = await config.hooks.getBiometricTimeout();
453
+ if (timeoutMs !== null && !["auth.refresh", "auth.verifyBiometric", "auth.logout"].includes(
454
+ path
455
+ )) {
456
+ if (!session.user.verifiedHumanAt) {
457
+ throw new import_server.TRPCError({
458
+ message: "Biometric verification not completed. Please verify again.",
459
+ code: "FORBIDDEN"
460
+ });
461
+ }
462
+ const now = /* @__PURE__ */ new Date();
463
+ const verificationExpiry = new Date(
464
+ session.user.verifiedHumanAt.getTime() + timeoutMs
465
+ );
466
+ if (now > verificationExpiry) {
467
+ throw new import_server.TRPCError({
468
+ message: "Biometric verification expired. Please verify again.",
469
+ code: "FORBIDDEN"
470
+ });
471
+ }
472
+ }
473
+ }
474
+ if (session.revokedAt) {
475
+ await revokeSession(
476
+ ctx,
477
+ session.id,
478
+ "Session revoked: Session already revoked",
479
+ void 0,
480
+ path
481
+ );
482
+ throw new import_server.TRPCError({
483
+ message: "Unauthorized",
484
+ code: "UNAUTHORIZED"
485
+ });
486
+ }
487
+ if (meta?.adminRequired) {
488
+ const admin = await config.prisma.admin.findFirst({
489
+ where: { userId: session.userId },
490
+ select: { ip: true }
491
+ });
492
+ if (!admin || admin.ip !== ctx.ip) {
493
+ await revokeSession(
494
+ ctx,
495
+ session.id,
496
+ "Session revoked: Admin not found or IP mismatch",
497
+ void 0,
498
+ path
499
+ );
500
+ throw new import_server.TRPCError({
501
+ message: "Unauthorized",
502
+ code: "UNAUTHORIZED"
503
+ });
504
+ }
505
+ }
506
+ return next({
507
+ ctx: {
508
+ ...ctx,
509
+ userId: session.userId,
510
+ socketId: session.socketId,
511
+ sessionId: session.id,
512
+ refreshToken
513
+ }
514
+ });
515
+ } catch (err) {
516
+ if (err instanceof import_server.TRPCError && err.code === "FORBIDDEN") {
517
+ throw err;
518
+ }
519
+ if (!meta?.authRequired) {
520
+ return next({ ctx: { ...ctx, userId: 0 } });
521
+ }
522
+ const errorStack = err instanceof Error ? err.stack : void 0;
523
+ if (isTokenExpiredError(err) || isTokenInvalidError(err)) {
524
+ await revokeSession(
525
+ ctx,
526
+ null,
527
+ isTokenInvalidError(err) ? "Session revoked: Token invalid" : "Session revoked: Token expired",
528
+ errorStack,
529
+ path
530
+ );
531
+ throw new import_server.TRPCError({
532
+ message: isTokenInvalidError(err) ? "Token invalid" : "Token expired",
533
+ code: "UNAUTHORIZED"
534
+ });
535
+ }
536
+ if (err instanceof import_server.TRPCError && err.code === "UNAUTHORIZED") {
537
+ await revokeSession(
538
+ ctx,
539
+ null,
540
+ "Session revoked: Unauthorized",
541
+ errorStack,
542
+ path
543
+ );
544
+ throw new import_server.TRPCError({
545
+ message: "Unauthorized",
546
+ code: "UNAUTHORIZED"
547
+ });
548
+ }
549
+ throw err;
550
+ }
551
+ } else {
552
+ if (!meta?.authRequired) {
553
+ return next({ ctx: { ...ctx, userId: 0 } });
554
+ }
555
+ await revokeSession(
556
+ ctx,
557
+ null,
558
+ "Session revoked: No token sent",
559
+ void 0,
560
+ path
561
+ );
562
+ throw new import_server.TRPCError({ message: "Unauthorized", code: "UNAUTHORIZED" });
563
+ }
564
+ });
565
+ return authGuard;
566
+ }
567
+
568
+ // src/procedures/base.ts
569
+ var import_node_crypto = require("crypto");
570
+ var import_server2 = require("@trpc/server");
571
+
572
+ // src/utilities/browser.ts
573
+ function detectBrowser(userAgent) {
574
+ if (/cfnetwork|darwin/i.test(userAgent)) return "iOS App";
575
+ if (/iphone|ipad|ipod/i.test(userAgent) && /safari/i.test(userAgent) && !/crios|fxios|edg\//i.test(userAgent)) {
576
+ return "iOS Browser (Safari)";
577
+ }
578
+ if (/iphone|ipad|ipod/i.test(userAgent) && /crios/i.test(userAgent))
579
+ return "iOS Browser (Chrome)";
580
+ if (/iphone|ipad|ipod/i.test(userAgent) && /fxios/i.test(userAgent))
581
+ return "iOS Browser (Firefox)";
582
+ if (/iphone|ipad|ipod/i.test(userAgent) && /edg\//i.test(userAgent))
583
+ return "iOS Browser (Edge)";
584
+ if (/android/i.test(userAgent) && !/chrome|firefox|samsungbrowser|opr\/|edg\//i.test(userAgent)) {
585
+ return "Android App";
586
+ }
587
+ if (/android/i.test(userAgent) && /chrome/i.test(userAgent))
588
+ return "Android Browser (Chrome)";
589
+ if (/android/i.test(userAgent) && /firefox/i.test(userAgent))
590
+ return "Android Browser (Firefox)";
591
+ if (/android/i.test(userAgent) && /samsungbrowser/i.test(userAgent))
592
+ return "Android Browser (Samsung)";
593
+ if (/android/i.test(userAgent) && /opr\//i.test(userAgent))
594
+ return "Android Browser (Opera)";
595
+ if (/android/i.test(userAgent) && /edg\//i.test(userAgent))
596
+ return "Android Browser (Edge)";
597
+ if (/chrome|chromium/i.test(userAgent)) return "Chrome";
598
+ if (/firefox/i.test(userAgent)) return "Firefox";
599
+ if (/safari/i.test(userAgent) && !/chrome|chromium|crios/i.test(userAgent))
600
+ return "Safari";
601
+ if (/opr\//i.test(userAgent)) return "Opera";
602
+ if (/edg\//i.test(userAgent)) return "Edge";
603
+ return "Unknown";
604
+ }
605
+ function isMobileDevice(userAgent) {
606
+ return /android|iphone|ipad|ipod|mobile/i.test(userAgent);
607
+ }
608
+ function isNativeApp(userAgent) {
609
+ const browser = detectBrowser(userAgent);
610
+ return browser === "iOS App" || browser === "Android App";
611
+ }
612
+
613
+ // src/utilities/oauth.ts
614
+ var import_apple_signin_auth = __toESM(require("apple-signin-auth"));
615
+ var import_google_auth_library = require("google-auth-library");
616
+ var OAuthVerificationError = class extends Error {
617
+ constructor(message, statusCode = 401) {
618
+ super(message);
619
+ this.statusCode = statusCode;
620
+ this.name = "OAuthVerificationError";
621
+ }
622
+ };
623
+ function createOAuthVerifier(keys) {
624
+ let googleClient = null;
625
+ if (keys.google?.clientId) {
626
+ googleClient = new import_google_auth_library.OAuth2Client({
627
+ clientId: keys.google.clientId,
628
+ clientSecret: keys.google.clientSecret
629
+ });
630
+ }
631
+ return async function verifyOAuthToken(provider, token, extra) {
632
+ if (provider === "GOOGLE") {
633
+ if (!keys.google?.clientId) {
634
+ throw new OAuthVerificationError(
635
+ "Google OAuth configuration missing",
636
+ 500
637
+ );
638
+ }
639
+ if (!googleClient) {
640
+ throw new OAuthVerificationError(
641
+ "Google OAuth client not initialized",
642
+ 500
643
+ );
644
+ }
645
+ const audience = [keys.google.clientId];
646
+ if (keys.google.iosClientId) {
647
+ audience.push(keys.google.iosClientId);
648
+ }
649
+ const ticket = await googleClient.verifyIdToken({
650
+ idToken: token,
651
+ audience
652
+ });
653
+ const payload = ticket.getPayload();
654
+ if (!payload?.sub || !payload.email) {
655
+ throw new OAuthVerificationError("Invalid Google token", 401);
656
+ }
657
+ return {
658
+ oauthId: payload.sub,
659
+ email: payload.email
660
+ };
661
+ }
662
+ if (provider === "APPLE") {
663
+ if (!keys.apple?.clientId) {
664
+ throw new OAuthVerificationError(
665
+ "Apple OAuth configuration missing",
666
+ 500
667
+ );
668
+ }
669
+ const audience = [keys.apple.clientId];
670
+ if (keys.apple.iosClientId) {
671
+ audience.push(keys.apple.iosClientId);
672
+ }
673
+ const { sub, email } = await import_apple_signin_auth.default.verifyIdToken(token, {
674
+ audience,
675
+ ignoreExpiration: false
676
+ });
677
+ const finalEmail = email || extra?.email;
678
+ if (!finalEmail || !sub) {
679
+ throw new OAuthVerificationError("Invalid Apple token", 401);
680
+ }
681
+ return {
682
+ oauthId: sub,
683
+ email: finalEmail
684
+ };
685
+ }
686
+ throw new OAuthVerificationError("Unsupported OAuth provider", 400);
687
+ };
688
+ }
689
+
690
+ // src/utilities/password.ts
691
+ var import_bcryptjs = __toESM(require("bcryptjs"));
692
+ var DEFAULT_SALT_ROUNDS = 10;
693
+ async function hashPassword(password, saltRounds = DEFAULT_SALT_ROUNDS) {
694
+ const salt = await import_bcryptjs.default.genSalt(saltRounds);
695
+ return import_bcryptjs.default.hash(password, salt);
696
+ }
697
+ async function comparePassword(password, hashedPassword) {
698
+ return import_bcryptjs.default.compare(password, hashedPassword);
699
+ }
700
+ function validatePasswordStrength(password, minLength = 6) {
701
+ if (password.length < minLength) {
702
+ return {
703
+ valid: false,
704
+ error: `Password must be at least ${minLength} characters`
705
+ };
706
+ }
707
+ return { valid: true };
708
+ }
709
+
710
+ // src/utilities/totp.ts
711
+ var import_crypto = __toESM(require("crypto"));
712
+ var import_totp_generator = require("totp-generator");
713
+ var BASE32_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
714
+ function generateTotpSecret(length = 16) {
715
+ let secret = "";
716
+ for (let i = 0; i < length; i++) {
717
+ const randIndex = Math.floor(Math.random() * BASE32_CHARS.length);
718
+ secret += BASE32_CHARS[randIndex];
719
+ }
720
+ return secret;
721
+ }
722
+ function cleanBase32String(input) {
723
+ return input.replace(/[^A-Z2-7]/gi, "").toUpperCase();
724
+ }
725
+ async function generateTotpCode(secret) {
726
+ const cleanSecret = cleanBase32String(secret);
727
+ const { otp } = await import_totp_generator.TOTP.generate(cleanSecret);
728
+ return otp;
729
+ }
730
+ async function verifyTotp(code, secret) {
731
+ const cleanSecret = cleanBase32String(secret);
732
+ const normalizedCode = code.replace(/\s/g, "");
733
+ const { otp } = await import_totp_generator.TOTP.generate(cleanSecret);
734
+ return otp === normalizedCode;
735
+ }
736
+ function generateOtp(min = 1e5, max = 999999) {
737
+ return Math.floor(import_crypto.default.randomInt(min, max + 1));
738
+ }
739
+
740
+ // src/validators.ts
741
+ var import_zod = require("zod");
742
+ var usernameValidationRegex = /^[a-zA-Z0-9_]+$/;
743
+ var signupSchema = import_zod.z.object({
744
+ username: import_zod.z.string().min(1, { message: "Username is required" }).max(30, { message: "Username must be 30 characters or less" }).regex(usernameValidationRegex, {
745
+ message: "Username can only contain letters, numbers, and underscores"
746
+ }),
747
+ email: import_zod.z.string().email({ message: "Invalid email address" }),
748
+ password: import_zod.z.string().min(6, { message: "Password must contain at least 6 characters" })
749
+ });
750
+ var loginSchema = import_zod.z.object({
751
+ username: import_zod.z.string().min(1, { message: "Username or email is required" }),
752
+ password: import_zod.z.string().min(1, { message: "Password is required" }),
753
+ code: import_zod.z.string().optional()
754
+ // 2FA code
755
+ });
756
+ var oAuthLoginSchema = import_zod.z.object({
757
+ idToken: import_zod.z.string(),
758
+ user: import_zod.z.object({
759
+ email: import_zod.z.string().email().optional()
760
+ }).optional(),
761
+ provider: import_zod.z.enum(["GOOGLE", "APPLE"])
762
+ });
763
+ var requestPasswordResetSchema = import_zod.z.object({
764
+ email: import_zod.z.string().email({ message: "Invalid email address" })
765
+ });
766
+ var resetPasswordSchema = import_zod.z.object({
767
+ token: import_zod.z.string().min(1, { message: "Reset token is required" }),
768
+ password: import_zod.z.string().min(6, { message: "Password must contain at least 6 characters" })
769
+ });
770
+ var checkPasswordResetSchema = import_zod.z.object({
771
+ token: import_zod.z.string().min(1, { message: "Reset token is required" })
772
+ });
773
+ var changePasswordSchema = import_zod.z.object({
774
+ currentPassword: import_zod.z.string().min(1, { message: "Current password is required" }),
775
+ newPassword: import_zod.z.string().min(6, { message: "New password must contain at least 6 characters" })
776
+ });
777
+ var twoFaVerifySchema = import_zod.z.object({
778
+ code: import_zod.z.string().min(6, { message: "Verification code is required" }),
779
+ sessionId: import_zod.z.number().optional()
780
+ });
781
+ var twoFaSetupSchema = import_zod.z.object({
782
+ code: import_zod.z.string().min(6, { message: "Verification code is required" })
783
+ });
784
+ var twoFaResetSchema = import_zod.z.object({
785
+ username: import_zod.z.string().min(1),
786
+ password: import_zod.z.string().min(1)
787
+ });
788
+ var twoFaResetVerifySchema = import_zod.z.object({
789
+ code: import_zod.z.number().min(1e5).max(999999),
790
+ username: import_zod.z.string().min(1)
791
+ });
792
+ var verifyEmailSchema = import_zod.z.object({
793
+ code: import_zod.z.string().min(1, { message: "Verification code is required" })
794
+ });
795
+ var resendVerificationSchema = import_zod.z.object({
796
+ email: import_zod.z.string().email().optional()
797
+ });
798
+ var biometricVerifySchema = import_zod.z.object({});
799
+ var registerPushTokenSchema = import_zod.z.object({
800
+ pushToken: import_zod.z.string().min(1, { message: "Push token is required" })
801
+ });
802
+ var deregisterPushTokenSchema = import_zod.z.object({
803
+ pushToken: import_zod.z.string().min(1, { message: "Push token is required" })
804
+ });
805
+ var getTwofaSecretSchema = import_zod.z.object({
806
+ pushCode: import_zod.z.string().min(6, { message: "Push code is required" })
807
+ });
808
+ var disableTwofaSchema = import_zod.z.object({
809
+ password: import_zod.z.string().min(1, { message: "Password is required" })
810
+ });
811
+ var logoutSchema = import_zod.z.object({
812
+ allDevices: import_zod.z.boolean().optional().default(false)
813
+ });
814
+ var endAllSessionsSchema = import_zod.z.object({
815
+ skipCurrentSession: import_zod.z.boolean().optional().default(true)
816
+ });
817
+ var otpLoginRequestSchema = import_zod.z.object({
818
+ email: import_zod.z.string().email({ message: "Invalid email address" })
819
+ });
820
+ var otpLoginVerifySchema = import_zod.z.object({
821
+ email: import_zod.z.string().email(),
822
+ code: import_zod.z.number().min(1e5).max(999999)
823
+ });
824
+ function createSchemas(extensions) {
825
+ return {
826
+ signup: extensions?.signup ? signupSchema.merge(extensions.signup) : signupSchema,
827
+ login: extensions?.login ? loginSchema.merge(extensions.login) : loginSchema,
828
+ oauth: extensions?.oauth ? oAuthLoginSchema.merge(extensions.oauth) : oAuthLoginSchema
829
+ };
830
+ }
831
+
832
+ // src/procedures/base.ts
833
+ var BaseProcedureFactory = class {
834
+ constructor(config, procedure, authProcedure) {
835
+ this.config = config;
836
+ this.procedure = procedure;
837
+ this.authProcedure = authProcedure;
838
+ }
839
+ /** Returns all base auth procedures to be merged into the router. */
840
+ createBaseProcedures(schemas) {
841
+ return {
842
+ register: this.register(schemas.signup),
843
+ login: this.login(schemas.login),
844
+ logout: this.logout(),
845
+ refresh: this.refresh(),
846
+ endAllSessions: this.endAllSessions(),
847
+ changePassword: this.changePassword(),
848
+ sendPasswordResetEmail: this.sendPasswordResetEmail(),
849
+ checkPasswordReset: this.checkPasswordReset(),
850
+ resetPassword: this.resetPassword()
851
+ };
852
+ }
853
+ register(schema) {
854
+ return this.procedure.input(schema).mutation(async ({ ctx, input }) => {
855
+ const typedInput = input;
856
+ const { username, email, password } = typedInput;
857
+ const userAgent = ctx.headers["user-agent"];
858
+ if (!userAgent) {
859
+ throw new import_server2.TRPCError({
860
+ code: "BAD_REQUEST",
861
+ message: "User agent not found"
862
+ });
863
+ }
864
+ if (this.config.hooks?.beforeRegister) {
865
+ await this.config.hooks.beforeRegister(typedInput);
866
+ }
867
+ const usernameCheck = await this.config.prisma.user.findFirst({
868
+ where: { username: { equals: username, mode: "insensitive" } }
869
+ });
870
+ if (usernameCheck) {
871
+ throw new import_server2.TRPCError({
872
+ code: "CONFLICT",
873
+ message: "An account already exists with that username."
874
+ });
875
+ }
876
+ const emailCheck = await this.config.prisma.user.findFirst({
877
+ where: { email: { equals: email, mode: "insensitive" } },
878
+ select: { id: true }
879
+ });
880
+ if (emailCheck) {
881
+ throw new import_server2.TRPCError({
882
+ code: "CONFLICT",
883
+ message: "An account already exists with that email."
884
+ });
885
+ }
886
+ const hashedPassword = await hashPassword(password);
887
+ const user = await this.config.prisma.user.create({
888
+ data: {
889
+ username,
890
+ email,
891
+ password: hashedPassword,
892
+ status: "ACTIVE",
893
+ tag: this.config.features.biometric ? "BOT" : "HUMAN",
894
+ twoFaEnabled: false,
895
+ emailVerificationStatus: "UNVERIFIED",
896
+ verifiedHumanAt: null
897
+ }
898
+ });
899
+ if (this.config.hooks?.onUserCreated) {
900
+ await this.config.hooks.onUserCreated(user.id, typedInput);
901
+ }
902
+ const refreshToken = (0, import_node_crypto.randomUUID)();
903
+ const extraSessionData = this.config.hooks?.getSessionData ? await this.config.hooks.getSessionData(typedInput) : {};
904
+ const session = await this.config.prisma.session.create({
905
+ data: {
906
+ userId: user.id,
907
+ browserName: detectBrowser(userAgent),
908
+ socketId: null,
909
+ refreshToken,
910
+ ...extraSessionData
911
+ },
912
+ select: { id: true, refreshToken: true, userId: true }
913
+ });
914
+ if (this.config.hooks?.onSessionCreated) {
915
+ await this.config.hooks.onSessionCreated(session.id, typedInput);
916
+ }
917
+ const accessToken = createAccessToken(
918
+ { id: session.id, userId: session.userId, verifiedHumanAt: null },
919
+ {
920
+ secret: this.config.secrets.jwt,
921
+ expiresIn: this.config.tokenSettings.accessTokenExpiry
922
+ }
923
+ );
924
+ setAuthCookies(
925
+ ctx.res,
926
+ { accessToken, refreshToken: session.refreshToken },
927
+ this.config.cookieSettings,
928
+ this.config.storageKeys
929
+ );
930
+ return {
931
+ success: true,
932
+ user: { id: user.id, email: user.email, username: user.username }
933
+ };
934
+ });
935
+ }
936
+ login(schema) {
937
+ return this.procedure.input(schema).mutation(async ({ ctx, input }) => {
938
+ const typedInput = input;
939
+ const { username, password, code } = typedInput;
940
+ const userAgent = ctx.headers["user-agent"];
941
+ if (!userAgent) {
942
+ throw new import_server2.TRPCError({
943
+ code: "BAD_REQUEST",
944
+ message: "User agent not found"
945
+ });
946
+ }
947
+ if (this.config.hooks?.beforeLogin) {
948
+ await this.config.hooks.beforeLogin(typedInput);
949
+ }
950
+ const user = await this.config.prisma.user.findFirst({
951
+ where: {
952
+ OR: [
953
+ { email: { equals: username, mode: "insensitive" } },
954
+ { username: { equals: username, mode: "insensitive" } }
955
+ ]
956
+ },
957
+ select: {
958
+ id: true,
959
+ status: true,
960
+ password: true,
961
+ twoFaEnabled: true,
962
+ email: true,
963
+ username: true,
964
+ oauthProvider: true,
965
+ verifiedHumanAt: true
966
+ }
967
+ });
968
+ if (!user) {
969
+ throw new import_server2.TRPCError({
970
+ code: "FORBIDDEN",
971
+ message: "Invalid credentials."
972
+ });
973
+ }
974
+ if (user.status === "DEACTIVATED") {
975
+ throw new import_server2.TRPCError({
976
+ code: "FORBIDDEN",
977
+ message: "Your account has been deactivated."
978
+ });
979
+ }
980
+ if (user.status === "BANNED") {
981
+ throw new import_server2.TRPCError({
982
+ code: "FORBIDDEN",
983
+ message: "Your account has been banned."
984
+ });
985
+ }
986
+ if (!user.password) {
987
+ throw new import_server2.TRPCError({
988
+ code: "FORBIDDEN",
989
+ message: `This account uses ${user.oauthProvider?.toLowerCase() || "social login"}. Please use that method.`
990
+ });
991
+ }
992
+ const isMatch = await comparePassword(password, user.password);
993
+ if (!isMatch) {
994
+ throw new import_server2.TRPCError({
995
+ code: "FORBIDDEN",
996
+ message: "Invalid credentials."
997
+ });
998
+ }
999
+ if (user.twoFaEnabled && this.config.features?.twoFa) {
1000
+ if (!code) {
1001
+ throw new import_server2.TRPCError({
1002
+ code: "FORBIDDEN",
1003
+ message: "2FA code required."
1004
+ });
1005
+ }
1006
+ let validCode = false;
1007
+ const secrets = await this.config.prisma.session.findMany({
1008
+ where: { userId: user.id, twoFaSecret: { not: null } },
1009
+ select: { twoFaSecret: true }
1010
+ });
1011
+ for (const s of secrets) {
1012
+ if (s.twoFaSecret && await verifyTotp(code, cleanBase32String(s.twoFaSecret))) {
1013
+ validCode = true;
1014
+ break;
1015
+ }
1016
+ }
1017
+ if (!validCode) {
1018
+ const checkOTP = await this.config.prisma.oTPBasedLogin.findFirst({
1019
+ where: {
1020
+ code: Number(code),
1021
+ userId: user.id,
1022
+ disabled: false,
1023
+ createdAt: { gte: new Date(Date.now() - this.config.tokenSettings.otpValidityMs) }
1024
+ }
1025
+ });
1026
+ if (checkOTP) {
1027
+ validCode = true;
1028
+ await this.config.prisma.oTPBasedLogin.updateMany({
1029
+ where: { code: Number(code) },
1030
+ data: { disabled: true }
1031
+ });
1032
+ }
1033
+ }
1034
+ if (!validCode) {
1035
+ throw new import_server2.TRPCError({
1036
+ code: "FORBIDDEN",
1037
+ message: "Invalid 2FA code."
1038
+ });
1039
+ }
1040
+ }
1041
+ const refreshToken = (0, import_node_crypto.randomUUID)();
1042
+ const extraSessionData = this.config.hooks?.getSessionData ? await this.config.hooks.getSessionData(typedInput) : {};
1043
+ const session = await this.config.prisma.session.create({
1044
+ data: {
1045
+ userId: user.id,
1046
+ browserName: detectBrowser(userAgent),
1047
+ socketId: null,
1048
+ refreshToken,
1049
+ ...extraSessionData
1050
+ },
1051
+ select: {
1052
+ id: true,
1053
+ refreshToken: true,
1054
+ userId: true,
1055
+ socketId: true,
1056
+ browserName: true,
1057
+ issuedAt: true,
1058
+ lastUsed: true,
1059
+ revokedAt: true,
1060
+ deviceId: true,
1061
+ twoFaSecret: true
1062
+ }
1063
+ });
1064
+ if (this.config.hooks?.onUserLogin) {
1065
+ await this.config.hooks.onUserLogin(user.id, session.id);
1066
+ }
1067
+ if (this.config.hooks?.onSessionCreated) {
1068
+ await this.config.hooks.onSessionCreated(session.id, typedInput);
1069
+ }
1070
+ const accessToken = createAccessToken(
1071
+ { id: session.id, userId: session.userId, verifiedHumanAt: user.verifiedHumanAt },
1072
+ {
1073
+ secret: this.config.secrets.jwt,
1074
+ expiresIn: this.config.tokenSettings.accessTokenExpiry
1075
+ }
1076
+ );
1077
+ setAuthCookies(
1078
+ ctx.res,
1079
+ { accessToken, refreshToken: session.refreshToken },
1080
+ this.config.cookieSettings,
1081
+ this.config.storageKeys
1082
+ );
1083
+ return {
1084
+ success: true,
1085
+ user: { id: user.id, email: user.email, username: user.username }
1086
+ };
1087
+ });
1088
+ }
1089
+ logout() {
1090
+ return this.procedure.mutation(async ({ ctx }) => {
1091
+ const { userId, sessionId } = ctx;
1092
+ if (sessionId) {
1093
+ await this.config.prisma.session.update({
1094
+ where: { id: sessionId },
1095
+ data: { revokedAt: /* @__PURE__ */ new Date() }
1096
+ });
1097
+ if (userId) {
1098
+ await this.config.prisma.user.update({
1099
+ where: { id: userId },
1100
+ data: { isActive: false }
1101
+ });
1102
+ }
1103
+ if (this.config.hooks?.afterLogout) {
1104
+ await this.config.hooks.afterLogout(userId, sessionId, ctx.socketId);
1105
+ }
1106
+ }
1107
+ clearAuthCookies(ctx.res, this.config.cookieSettings, this.config.storageKeys);
1108
+ return { success: true };
1109
+ });
1110
+ }
1111
+ refresh() {
1112
+ return this.authProcedure.meta({ ignoreExpiration: true }).query(async ({ ctx }) => {
1113
+ const session = await this.config.prisma.session.update({
1114
+ where: { id: ctx.sessionId },
1115
+ data: { refreshToken: (0, import_node_crypto.randomUUID)(), lastUsed: /* @__PURE__ */ new Date() },
1116
+ select: {
1117
+ id: true,
1118
+ refreshToken: true,
1119
+ userId: true,
1120
+ user: { select: { verifiedHumanAt: true } }
1121
+ }
1122
+ });
1123
+ if (this.config.hooks?.onRefresh) {
1124
+ this.config.hooks.onRefresh(session.userId).catch(() => {
1125
+ });
1126
+ }
1127
+ const accessToken = createAccessToken(
1128
+ { id: session.id, userId: session.userId, verifiedHumanAt: session.user.verifiedHumanAt },
1129
+ {
1130
+ secret: this.config.secrets.jwt,
1131
+ expiresIn: this.config.tokenSettings.accessTokenExpiry
1132
+ }
1133
+ );
1134
+ setAuthCookies(
1135
+ ctx.res,
1136
+ { accessToken, refreshToken: session.refreshToken },
1137
+ this.config.cookieSettings,
1138
+ this.config.storageKeys
1139
+ );
1140
+ return { success: true };
1141
+ });
1142
+ }
1143
+ endAllSessions() {
1144
+ return this.authProcedure.input(endAllSessionsSchema).mutation(async ({ ctx, input }) => {
1145
+ const { skipCurrentSession } = input;
1146
+ const { userId, sessionId } = ctx;
1147
+ const sessionsToRevoke = await this.config.prisma.session.findMany({
1148
+ where: {
1149
+ userId,
1150
+ revokedAt: null,
1151
+ ...skipCurrentSession ? { NOT: { id: sessionId } } : {}
1152
+ },
1153
+ select: { socketId: true, id: true, userId: true }
1154
+ });
1155
+ await this.config.prisma.session.updateMany({
1156
+ where: {
1157
+ userId,
1158
+ revokedAt: null,
1159
+ ...skipCurrentSession ? { NOT: { id: sessionId } } : {}
1160
+ },
1161
+ data: { revokedAt: /* @__PURE__ */ new Date() }
1162
+ });
1163
+ for (const session of sessionsToRevoke) {
1164
+ if (this.config.hooks?.onSessionRevoked) {
1165
+ await this.config.hooks.onSessionRevoked(session.id, session.socketId, "End all sessions");
1166
+ }
1167
+ }
1168
+ if (!skipCurrentSession) {
1169
+ await this.config.prisma.user.update({
1170
+ where: { id: userId },
1171
+ data: { isActive: false }
1172
+ });
1173
+ }
1174
+ return { success: true, revokedCount: sessionsToRevoke.length };
1175
+ });
1176
+ }
1177
+ changePassword() {
1178
+ return this.authProcedure.input(changePasswordSchema).mutation(async ({ ctx, input }) => {
1179
+ const { userId, sessionId } = ctx;
1180
+ const { currentPassword, newPassword } = input;
1181
+ if (currentPassword === newPassword) {
1182
+ throw new import_server2.TRPCError({
1183
+ code: "BAD_REQUEST",
1184
+ message: "New password cannot be the same as current password"
1185
+ });
1186
+ }
1187
+ const user = await this.config.prisma.user.findUnique({
1188
+ where: { id: userId },
1189
+ select: { password: true }
1190
+ });
1191
+ if (!user) {
1192
+ throw new import_server2.TRPCError({ code: "NOT_FOUND", message: "User not found" });
1193
+ }
1194
+ if (!user.password) {
1195
+ throw new import_server2.TRPCError({
1196
+ code: "BAD_REQUEST",
1197
+ message: "This account uses social login and cannot change password."
1198
+ });
1199
+ }
1200
+ const isMatch = await comparePassword(currentPassword, user.password);
1201
+ if (!isMatch) {
1202
+ throw new import_server2.TRPCError({
1203
+ code: "FORBIDDEN",
1204
+ message: "Current password is incorrect"
1205
+ });
1206
+ }
1207
+ const hashedPassword = await hashPassword(newPassword);
1208
+ await this.config.prisma.user.update({
1209
+ where: { id: userId },
1210
+ data: { password: hashedPassword }
1211
+ });
1212
+ await this.config.prisma.session.updateMany({
1213
+ where: { userId, revokedAt: null, NOT: { id: sessionId } },
1214
+ data: { revokedAt: /* @__PURE__ */ new Date() }
1215
+ });
1216
+ if (this.config.hooks?.onPasswordChanged) {
1217
+ await this.config.hooks.onPasswordChanged(userId);
1218
+ }
1219
+ return {
1220
+ success: true,
1221
+ message: "Password changed. You will need to re-login on other devices."
1222
+ };
1223
+ });
1224
+ }
1225
+ sendPasswordResetEmail() {
1226
+ return this.procedure.input(requestPasswordResetSchema).mutation(async ({ input }) => {
1227
+ const { email } = input;
1228
+ const user = await this.config.prisma.user.findFirst({
1229
+ where: { email: { equals: email, mode: "insensitive" }, status: "ACTIVE" },
1230
+ select: { id: true, password: true, email: true }
1231
+ });
1232
+ if (!user) {
1233
+ return { message: "If an account exists with that email, a reset link has been sent." };
1234
+ }
1235
+ if (!user.password) {
1236
+ throw new import_server2.TRPCError({
1237
+ code: "BAD_REQUEST",
1238
+ message: "This account uses social login. Please use that method."
1239
+ });
1240
+ }
1241
+ await this.config.prisma.passwordReset.deleteMany({ where: { userId: user.id } });
1242
+ const passwordReset = await this.config.prisma.passwordReset.create({
1243
+ data: { userId: user.id }
1244
+ });
1245
+ if (this.config.emailService) {
1246
+ await this.config.emailService.sendPasswordResetEmail(user.email, String(passwordReset.id));
1247
+ }
1248
+ return { message: "Password reset email sent." };
1249
+ });
1250
+ }
1251
+ checkPasswordReset() {
1252
+ return this.procedure.input(checkPasswordResetSchema).query(async ({ input }) => {
1253
+ const { token } = input;
1254
+ const passwordReset = await this.config.prisma.passwordReset.findUnique({
1255
+ where: { id: token },
1256
+ select: { id: true, createdAt: true, userId: true }
1257
+ });
1258
+ if (!passwordReset) {
1259
+ throw new import_server2.TRPCError({ code: "NOT_FOUND", message: "Invalid reset token." });
1260
+ }
1261
+ if (passwordReset.createdAt.getTime() + this.config.tokenSettings.passwordResetExpiryMs < Date.now()) {
1262
+ await this.config.prisma.passwordReset.delete({ where: { id: token } });
1263
+ throw new import_server2.TRPCError({ code: "FORBIDDEN", message: "Reset token expired." });
1264
+ }
1265
+ return { valid: true };
1266
+ });
1267
+ }
1268
+ resetPassword() {
1269
+ return this.procedure.input(resetPasswordSchema).mutation(async ({ input }) => {
1270
+ const { token, password } = input;
1271
+ const passwordReset = await this.config.prisma.passwordReset.findFirst({
1272
+ where: { id: token },
1273
+ select: { id: true, createdAt: true, userId: true }
1274
+ });
1275
+ if (!passwordReset) {
1276
+ throw new import_server2.TRPCError({ code: "NOT_FOUND", message: "Invalid reset token." });
1277
+ }
1278
+ if (passwordReset.createdAt.getTime() + this.config.tokenSettings.passwordResetExpiryMs < Date.now()) {
1279
+ await this.config.prisma.passwordReset.delete({ where: { id: token } });
1280
+ throw new import_server2.TRPCError({ code: "FORBIDDEN", message: "Reset token expired." });
1281
+ }
1282
+ const hashedPassword = await hashPassword(password);
1283
+ await this.config.prisma.user.update({
1284
+ where: { id: passwordReset.userId },
1285
+ data: { password: hashedPassword }
1286
+ });
1287
+ await this.config.prisma.passwordReset.delete({ where: { id: token } });
1288
+ await this.config.prisma.session.updateMany({
1289
+ where: { userId: passwordReset.userId },
1290
+ data: { revokedAt: /* @__PURE__ */ new Date() }
1291
+ });
1292
+ return { message: "Password updated. Please log in with your new password." };
1293
+ });
1294
+ }
1295
+ };
1296
+
1297
+ // src/procedures/biometric.ts
1298
+ var import_server3 = require("@trpc/server");
1299
+ var BiometricProcedureFactory = class {
1300
+ constructor(config, authProcedure) {
1301
+ this.config = config;
1302
+ this.authProcedure = authProcedure;
1303
+ }
1304
+ createBiometricProcedures() {
1305
+ return {
1306
+ verifyBiometric: this.verifyBiometric(),
1307
+ getBiometricStatus: this.getBiometricStatus()
1308
+ };
1309
+ }
1310
+ checkConfig() {
1311
+ if (!this.config.features.biometric) {
1312
+ throw new import_server3.TRPCError({ code: "NOT_FOUND" });
1313
+ }
1314
+ }
1315
+ verifyBiometric() {
1316
+ return this.authProcedure.input(biometricVerifySchema).mutation(async ({ ctx }) => {
1317
+ this.checkConfig();
1318
+ const { userId } = ctx;
1319
+ await this.config.prisma.user.update({
1320
+ where: { id: userId },
1321
+ data: { verifiedHumanAt: /* @__PURE__ */ new Date(), tag: "HUMAN" }
1322
+ });
1323
+ if (this.config.hooks?.onBiometricVerified) {
1324
+ await this.config.hooks.onBiometricVerified(userId);
1325
+ }
1326
+ return { success: true, verifiedAt: /* @__PURE__ */ new Date() };
1327
+ });
1328
+ }
1329
+ getBiometricStatus() {
1330
+ return this.authProcedure.query(async ({ ctx }) => {
1331
+ this.checkConfig();
1332
+ const { userId } = ctx;
1333
+ const user = await this.config.prisma.user.findUnique({
1334
+ where: { id: userId },
1335
+ select: { verifiedHumanAt: true }
1336
+ });
1337
+ if (!user) {
1338
+ throw new import_server3.TRPCError({ code: "NOT_FOUND", message: "User not found" });
1339
+ }
1340
+ let timeoutMs = null;
1341
+ if (this.config.hooks?.getBiometricTimeout) {
1342
+ timeoutMs = await this.config.hooks.getBiometricTimeout();
1343
+ }
1344
+ let isExpired = false;
1345
+ if (user.verifiedHumanAt && timeoutMs !== null) {
1346
+ const expiresAt = new Date(user.verifiedHumanAt.getTime() + timeoutMs);
1347
+ isExpired = /* @__PURE__ */ new Date() > expiresAt;
1348
+ }
1349
+ return {
1350
+ verifiedHumanAt: user.verifiedHumanAt,
1351
+ isVerified: !!user.verifiedHumanAt && !isExpired,
1352
+ isExpired,
1353
+ requiresVerification: timeoutMs !== null
1354
+ };
1355
+ });
1356
+ }
1357
+ };
1358
+
1359
+ // src/procedures/emailVerification.ts
1360
+ var import_node_crypto2 = require("crypto");
1361
+ var import_server4 = require("@trpc/server");
1362
+ var EmailVerificationProcedureFactory = class {
1363
+ constructor(config, authProcedure) {
1364
+ this.config = config;
1365
+ this.authProcedure = authProcedure;
1366
+ }
1367
+ createEmailVerificationProcedures() {
1368
+ return {
1369
+ sendVerificationEmail: this.sendVerificationEmail(),
1370
+ verifyEmail: this.verifyEmail(),
1371
+ getVerificationStatus: this.getVerificationStatus()
1372
+ };
1373
+ }
1374
+ checkConfig() {
1375
+ if (!this.config.features.emailVerification) {
1376
+ throw new import_server4.TRPCError({ code: "NOT_FOUND" });
1377
+ }
1378
+ }
1379
+ sendVerificationEmail() {
1380
+ return this.authProcedure.mutation(async ({ ctx }) => {
1381
+ this.checkConfig();
1382
+ const { userId } = ctx;
1383
+ const user = await this.config.prisma.user.findUnique({
1384
+ where: { id: userId, status: "ACTIVE" },
1385
+ select: { id: true, email: true, emailVerificationStatus: true }
1386
+ });
1387
+ if (!user) {
1388
+ throw new import_server4.TRPCError({ code: "NOT_FOUND", message: "User not found" });
1389
+ }
1390
+ if (user.emailVerificationStatus === "VERIFIED") {
1391
+ return { message: "Email is already verified", emailSent: false };
1392
+ }
1393
+ const otp = (0, import_node_crypto2.randomUUID)();
1394
+ await this.config.prisma.user.update({
1395
+ where: { id: userId },
1396
+ data: { emailVerificationStatus: "PENDING", otpForEmailVerification: otp }
1397
+ });
1398
+ if (this.config.emailService) {
1399
+ try {
1400
+ await this.config.emailService.sendVerificationEmail(user.email, otp);
1401
+ return { message: "Verification email sent", emailSent: true };
1402
+ } catch {
1403
+ return { message: "Failed to send email", emailSent: false };
1404
+ }
1405
+ }
1406
+ return { message: "Email service not configured", emailSent: false };
1407
+ });
1408
+ }
1409
+ verifyEmail() {
1410
+ return this.authProcedure.input(verifyEmailSchema).mutation(async ({ ctx, input }) => {
1411
+ this.checkConfig();
1412
+ const { userId } = ctx;
1413
+ const { code } = input;
1414
+ const user = await this.config.prisma.user.findUnique({
1415
+ where: { id: userId, status: "ACTIVE" },
1416
+ select: { id: true, emailVerificationStatus: true, otpForEmailVerification: true }
1417
+ });
1418
+ if (!user) {
1419
+ throw new import_server4.TRPCError({ code: "NOT_FOUND", message: "User not found" });
1420
+ }
1421
+ if (user.emailVerificationStatus === "VERIFIED") {
1422
+ return { success: true, message: "Email is already verified" };
1423
+ }
1424
+ if (code !== user.otpForEmailVerification) {
1425
+ throw new import_server4.TRPCError({ code: "BAD_REQUEST", message: "Invalid verification code" });
1426
+ }
1427
+ await this.config.prisma.user.update({
1428
+ where: { id: userId },
1429
+ data: { emailVerificationStatus: "VERIFIED", otpForEmailVerification: null }
1430
+ });
1431
+ if (this.config.hooks?.onEmailVerified) {
1432
+ await this.config.hooks.onEmailVerified(userId);
1433
+ }
1434
+ return { success: true, message: "Email verified" };
1435
+ });
1436
+ }
1437
+ getVerificationStatus() {
1438
+ return this.authProcedure.query(async ({ ctx }) => {
1439
+ this.checkConfig();
1440
+ const { userId } = ctx;
1441
+ const user = await this.config.prisma.user.findUnique({
1442
+ where: { id: userId },
1443
+ select: { emailVerificationStatus: true, email: true }
1444
+ });
1445
+ if (!user) {
1446
+ throw new import_server4.TRPCError({ code: "NOT_FOUND", message: "User not found" });
1447
+ }
1448
+ return {
1449
+ email: user.email,
1450
+ status: user.emailVerificationStatus,
1451
+ isVerified: user.emailVerificationStatus === "VERIFIED"
1452
+ };
1453
+ });
1454
+ }
1455
+ };
1456
+
1457
+ // src/procedures/oauth.ts
1458
+ var import_node_crypto3 = require("crypto");
1459
+ var import_server5 = require("@trpc/server");
1460
+ var OAuthLoginProcedureFactory = class {
1461
+ constructor(config, procedure) {
1462
+ this.config = config;
1463
+ this.procedure = procedure;
1464
+ this.verifyOAuthToken = null;
1465
+ if (config.oauthKeys) {
1466
+ this.verifyOAuthToken = createOAuthVerifier(config.oauthKeys);
1467
+ }
1468
+ }
1469
+ createOAuthLoginProcedures(schemas) {
1470
+ return { oAuthLogin: this.oAuthLogin(schemas.oauth) };
1471
+ }
1472
+ checkConfig() {
1473
+ if (!this.config.features.oauth?.google && !this.config.features.oauth?.apple) {
1474
+ throw new import_server5.TRPCError({ code: "NOT_FOUND" });
1475
+ }
1476
+ }
1477
+ oAuthLogin(schema) {
1478
+ return this.procedure.input(schema).mutation(async ({ ctx, input }) => {
1479
+ this.checkConfig();
1480
+ const typedInput = input;
1481
+ const { idToken, user: appleUser, provider } = typedInput;
1482
+ const userAgent = ctx.headers["user-agent"];
1483
+ if (!userAgent) {
1484
+ throw new import_server5.TRPCError({ code: "BAD_REQUEST", message: "User agent not found" });
1485
+ }
1486
+ if (!this.verifyOAuthToken) {
1487
+ throw new import_server5.TRPCError({
1488
+ code: "INTERNAL_SERVER_ERROR",
1489
+ message: "OAuth not configured. Provide oauthKeys in config."
1490
+ });
1491
+ }
1492
+ const { email, oauthId } = await this.verifyOAuthToken(provider, idToken, appleUser);
1493
+ if (!email) {
1494
+ throw new import_server5.TRPCError({ code: "BAD_REQUEST", message: "Email not provided by OAuth provider" });
1495
+ }
1496
+ let user = await this.config.prisma.user.findFirst({
1497
+ where: {
1498
+ OR: [
1499
+ { email: { equals: email, mode: "insensitive" } },
1500
+ { oauthId: { equals: oauthId } }
1501
+ ]
1502
+ },
1503
+ select: {
1504
+ id: true,
1505
+ status: true,
1506
+ email: true,
1507
+ username: true,
1508
+ password: true,
1509
+ oauthProvider: true,
1510
+ oauthId: true,
1511
+ twoFaEnabled: true,
1512
+ verifiedHumanAt: true,
1513
+ emailVerificationStatus: true
1514
+ }
1515
+ });
1516
+ if (user?.oauthProvider && user.oauthProvider !== provider) {
1517
+ throw new import_server5.TRPCError({
1518
+ code: "BAD_REQUEST",
1519
+ message: `This email uses ${user.oauthProvider.toLowerCase()} sign-in.`
1520
+ });
1521
+ }
1522
+ if (user && !user.oauthProvider && user.password) {
1523
+ throw new import_server5.TRPCError({
1524
+ code: "BAD_REQUEST",
1525
+ message: "This email uses password login. Please use email/password."
1526
+ });
1527
+ }
1528
+ if (!user) {
1529
+ const generateUsername = this.config.generateUsername ?? (() => `user_${Date.now()}`);
1530
+ user = await this.config.prisma.user.create({
1531
+ data: {
1532
+ username: generateUsername(),
1533
+ email,
1534
+ password: null,
1535
+ emailVerificationStatus: "VERIFIED",
1536
+ oauthProvider: provider,
1537
+ oauthId,
1538
+ status: "ACTIVE",
1539
+ tag: this.config.features.biometric ? "BOT" : "HUMAN",
1540
+ twoFaEnabled: false,
1541
+ verifiedHumanAt: null
1542
+ }
1543
+ });
1544
+ if (this.config.hooks?.onUserCreated) {
1545
+ await this.config.hooks.onUserCreated(user.id, typedInput);
1546
+ }
1547
+ if (this.config.hooks?.onOAuthLinked) {
1548
+ await this.config.hooks.onOAuthLinked(user.id, provider);
1549
+ }
1550
+ }
1551
+ if (user.status === "DEACTIVATED") {
1552
+ throw new import_server5.TRPCError({ code: "FORBIDDEN", message: "Your account has been deactivated." });
1553
+ }
1554
+ if (user.status === "BANNED") {
1555
+ throw new import_server5.TRPCError({ code: "FORBIDDEN", message: "Your account has been banned." });
1556
+ }
1557
+ const refreshToken = (0, import_node_crypto3.randomUUID)();
1558
+ const extraSessionData = this.config.hooks?.getSessionData ? await this.config.hooks.getSessionData(typedInput) : {};
1559
+ const session = await this.config.prisma.session.create({
1560
+ data: {
1561
+ userId: user.id,
1562
+ browserName: detectBrowser(userAgent),
1563
+ socketId: null,
1564
+ refreshToken,
1565
+ ...extraSessionData
1566
+ },
1567
+ select: {
1568
+ id: true,
1569
+ refreshToken: true,
1570
+ userId: true,
1571
+ socketId: true,
1572
+ browserName: true,
1573
+ issuedAt: true,
1574
+ lastUsed: true,
1575
+ revokedAt: true,
1576
+ deviceId: true,
1577
+ twoFaSecret: true
1578
+ }
1579
+ });
1580
+ if (this.config.hooks?.onUserLogin) {
1581
+ await this.config.hooks.onUserLogin(user.id, session.id);
1582
+ }
1583
+ if (this.config.hooks?.onSessionCreated) {
1584
+ await this.config.hooks.onSessionCreated(session.id, typedInput);
1585
+ }
1586
+ const accessToken = createAccessToken(
1587
+ { id: session.id, userId: session.userId, verifiedHumanAt: user.verifiedHumanAt ?? null },
1588
+ {
1589
+ secret: this.config.secrets.jwt,
1590
+ expiresIn: this.config.tokenSettings.accessTokenExpiry
1591
+ }
1592
+ );
1593
+ setAuthCookies(
1594
+ ctx.res,
1595
+ { accessToken, refreshToken: session.refreshToken },
1596
+ this.config.cookieSettings,
1597
+ this.config.storageKeys
1598
+ );
1599
+ return {
1600
+ success: true,
1601
+ user: { id: user.id, email: user.email, username: user.username }
1602
+ };
1603
+ });
1604
+ }
1605
+ };
1606
+
1607
+ // src/procedures/twoFa.ts
1608
+ var import_server6 = require("@trpc/server");
1609
+ var TwoFaProcedureFactory = class {
1610
+ constructor(config, procedure, authProcedure) {
1611
+ this.config = config;
1612
+ this.procedure = procedure;
1613
+ this.authProcedure = authProcedure;
1614
+ }
1615
+ createTwoFaProcedures() {
1616
+ return {
1617
+ enableTwofa: this.enableTwofa(),
1618
+ disableTwofa: this.disableTwofa(),
1619
+ getTwofaSecret: this.getTwofaSecret(),
1620
+ twoFaReset: this.twoFaReset(),
1621
+ twoFaResetVerify: this.twoFaResetVerify(),
1622
+ registerPushToken: this.registerPushToken(),
1623
+ deregisterPushToken: this.deregisterPushToken()
1624
+ };
1625
+ }
1626
+ checkConfig() {
1627
+ if (!this.config.features.twoFa) {
1628
+ throw new import_server6.TRPCError({ code: "NOT_FOUND" });
1629
+ }
1630
+ }
1631
+ enableTwofa() {
1632
+ return this.authProcedure.mutation(async ({ ctx }) => {
1633
+ this.checkConfig();
1634
+ const { userId, sessionId } = ctx;
1635
+ const user = await this.config.prisma.user.findFirst({
1636
+ where: { id: userId },
1637
+ select: { twoFaEnabled: true, oauthProvider: true, password: true }
1638
+ });
1639
+ if (!user) {
1640
+ throw new import_server6.TRPCError({ code: "NOT_FOUND", message: "User not found." });
1641
+ }
1642
+ if (user.oauthProvider) {
1643
+ throw new import_server6.TRPCError({
1644
+ code: "FORBIDDEN",
1645
+ message: "2FA is not available for social login accounts."
1646
+ });
1647
+ }
1648
+ if (user.twoFaEnabled) {
1649
+ throw new import_server6.TRPCError({ code: "BAD_REQUEST", message: "2FA already enabled." });
1650
+ }
1651
+ const checkSession = await this.config.prisma.session.findFirst({
1652
+ where: { userId, id: sessionId },
1653
+ select: { deviceId: true }
1654
+ });
1655
+ if (!checkSession?.deviceId) {
1656
+ throw new import_server6.TRPCError({
1657
+ code: "BAD_REQUEST",
1658
+ message: "You must be logged in on mobile to enable 2FA."
1659
+ });
1660
+ }
1661
+ await this.config.prisma.session.updateMany({
1662
+ where: { userId, revokedAt: null, NOT: { id: sessionId } },
1663
+ data: { revokedAt: /* @__PURE__ */ new Date() }
1664
+ });
1665
+ await this.config.prisma.session.updateMany({
1666
+ where: { userId, NOT: { id: sessionId } },
1667
+ data: { twoFaSecret: null }
1668
+ });
1669
+ const secret = generateTotpSecret();
1670
+ await this.config.prisma.user.update({
1671
+ where: { id: userId },
1672
+ data: { twoFaEnabled: true }
1673
+ });
1674
+ await this.config.prisma.session.update({
1675
+ where: { id: sessionId },
1676
+ data: { twoFaSecret: secret }
1677
+ });
1678
+ if (this.config.hooks?.onTwoFaStatusChanged) {
1679
+ await this.config.hooks.onTwoFaStatusChanged(userId, true);
1680
+ }
1681
+ return { secret };
1682
+ });
1683
+ }
1684
+ disableTwofa() {
1685
+ return this.authProcedure.input(disableTwofaSchema).mutation(async ({ ctx, input }) => {
1686
+ this.checkConfig();
1687
+ const { userId, sessionId } = ctx;
1688
+ const { password } = input;
1689
+ const user = await this.config.prisma.user.findFirst({
1690
+ where: { id: userId },
1691
+ select: { password: true, status: true, oauthProvider: true }
1692
+ });
1693
+ if (!user) {
1694
+ throw new import_server6.TRPCError({ code: "NOT_FOUND", message: "User not found." });
1695
+ }
1696
+ if (user.status !== "ACTIVE") {
1697
+ throw new import_server6.TRPCError({ code: "FORBIDDEN", message: "Account deactivated." });
1698
+ }
1699
+ if (user.oauthProvider) {
1700
+ throw new import_server6.TRPCError({
1701
+ code: "FORBIDDEN",
1702
+ message: "2FA is not available for social login accounts."
1703
+ });
1704
+ }
1705
+ if (!user.password) {
1706
+ throw new import_server6.TRPCError({
1707
+ code: "BAD_REQUEST",
1708
+ message: "Cannot verify password for social login account."
1709
+ });
1710
+ }
1711
+ const isMatch = await comparePassword(password, user.password);
1712
+ if (!isMatch) {
1713
+ throw new import_server6.TRPCError({ code: "FORBIDDEN", message: "Incorrect password." });
1714
+ }
1715
+ await this.config.prisma.user.update({
1716
+ where: { id: userId },
1717
+ data: { twoFaEnabled: false }
1718
+ });
1719
+ await this.config.prisma.session.update({
1720
+ where: { id: sessionId },
1721
+ data: { twoFaSecret: null }
1722
+ });
1723
+ if (this.config.hooks?.onTwoFaStatusChanged) {
1724
+ await this.config.hooks.onTwoFaStatusChanged(userId, false);
1725
+ }
1726
+ return { disabled: true };
1727
+ });
1728
+ }
1729
+ getTwofaSecret() {
1730
+ return this.authProcedure.input(getTwofaSecretSchema).query(async ({ ctx, input }) => {
1731
+ this.checkConfig();
1732
+ const { userId, sessionId } = ctx;
1733
+ const { pushCode } = input;
1734
+ const user = await this.config.prisma.user.findFirst({
1735
+ where: { id: userId },
1736
+ select: { twoFaEnabled: true, oauthProvider: true }
1737
+ });
1738
+ if (user?.oauthProvider) {
1739
+ throw new import_server6.TRPCError({
1740
+ code: "FORBIDDEN",
1741
+ message: "2FA is not available for social login accounts."
1742
+ });
1743
+ }
1744
+ if (!user?.twoFaEnabled) {
1745
+ throw new import_server6.TRPCError({ code: "BAD_REQUEST", message: "2FA not enabled." });
1746
+ }
1747
+ const session = await this.config.prisma.session.findUnique({
1748
+ where: { id: sessionId, userId },
1749
+ select: { twoFaSecret: true, device: { select: { pushToken: true } } }
1750
+ });
1751
+ if (!session?.device) {
1752
+ throw new import_server6.TRPCError({ code: "BAD_REQUEST", message: "Invalid request" });
1753
+ }
1754
+ const expectedCode = await verifyTotp(pushCode, cleanBase32String(session.device.pushToken));
1755
+ if (!expectedCode) {
1756
+ throw new import_server6.TRPCError({ code: "BAD_REQUEST", message: "Invalid request" });
1757
+ }
1758
+ if (session.twoFaSecret) {
1759
+ return { secret: session.twoFaSecret };
1760
+ }
1761
+ const secret = generateTotpSecret();
1762
+ await this.config.prisma.session.update({
1763
+ where: { id: sessionId },
1764
+ data: { twoFaSecret: secret }
1765
+ });
1766
+ return { secret };
1767
+ });
1768
+ }
1769
+ twoFaReset() {
1770
+ return this.procedure.input(twoFaResetSchema).mutation(async ({ input }) => {
1771
+ this.checkConfig();
1772
+ const { username, password } = input;
1773
+ const user = await this.config.prisma.user.findFirst({
1774
+ where: { username: { equals: username, mode: "insensitive" }, twoFaEnabled: true },
1775
+ select: { id: true, password: true, email: true }
1776
+ });
1777
+ if (!user) {
1778
+ throw new import_server6.TRPCError({ code: "UNAUTHORIZED", message: "Invalid credentials." });
1779
+ }
1780
+ if (!user.password) {
1781
+ throw new import_server6.TRPCError({
1782
+ code: "BAD_REQUEST",
1783
+ message: "Social login accounts cannot use 2FA reset."
1784
+ });
1785
+ }
1786
+ const isMatch = await comparePassword(password, user.password);
1787
+ if (!isMatch) {
1788
+ throw new import_server6.TRPCError({ code: "FORBIDDEN", message: "Invalid credentials." });
1789
+ }
1790
+ const otp = generateOtp();
1791
+ await this.config.prisma.oTPBasedLogin.create({
1792
+ data: { userId: user.id, code: otp }
1793
+ });
1794
+ if (this.config.emailService) {
1795
+ await this.config.emailService.sendOTPEmail(user.email, otp);
1796
+ }
1797
+ return { success: true };
1798
+ });
1799
+ }
1800
+ twoFaResetVerify() {
1801
+ return this.procedure.input(twoFaResetVerifySchema).mutation(async ({ input }) => {
1802
+ this.checkConfig();
1803
+ const { code, username } = input;
1804
+ const user = await this.config.prisma.user.findFirst({
1805
+ where: { username: { equals: username, mode: "insensitive" } },
1806
+ select: { id: true }
1807
+ });
1808
+ if (!user) {
1809
+ throw new import_server6.TRPCError({ code: "NOT_FOUND", message: "User not found" });
1810
+ }
1811
+ const otp = await this.config.prisma.oTPBasedLogin.findFirst({
1812
+ where: {
1813
+ userId: user.id,
1814
+ code,
1815
+ disabled: false,
1816
+ createdAt: { gte: new Date(Date.now() - this.config.tokenSettings.otpValidityMs) }
1817
+ }
1818
+ });
1819
+ if (!otp) {
1820
+ throw new import_server6.TRPCError({ code: "FORBIDDEN", message: "Invalid or expired OTP" });
1821
+ }
1822
+ await this.config.prisma.oTPBasedLogin.update({
1823
+ where: { id: otp.id },
1824
+ data: { disabled: true }
1825
+ });
1826
+ await this.config.prisma.user.update({
1827
+ where: { id: user.id },
1828
+ data: { twoFaEnabled: false }
1829
+ });
1830
+ await this.config.prisma.session.updateMany({
1831
+ where: { userId: user.id },
1832
+ data: { twoFaSecret: null }
1833
+ });
1834
+ return { success: true, message: "2FA has been reset." };
1835
+ });
1836
+ }
1837
+ registerPushToken() {
1838
+ return this.authProcedure.input(registerPushTokenSchema).mutation(async ({ ctx, input }) => {
1839
+ this.checkConfig();
1840
+ const { userId, sessionId } = ctx;
1841
+ const { pushToken } = input;
1842
+ await this.config.prisma.session.updateMany({
1843
+ where: {
1844
+ userId,
1845
+ id: { not: sessionId },
1846
+ revokedAt: null,
1847
+ device: { pushToken }
1848
+ },
1849
+ data: { revokedAt: /* @__PURE__ */ new Date() }
1850
+ });
1851
+ const checkDevice = await this.config.prisma.device.findFirst({
1852
+ where: {
1853
+ pushToken,
1854
+ sessions: { some: { id: sessionId } },
1855
+ users: { some: { id: userId } }
1856
+ },
1857
+ select: { id: true }
1858
+ });
1859
+ if (!checkDevice) {
1860
+ await this.config.prisma.device.upsert({
1861
+ where: { pushToken },
1862
+ create: {
1863
+ pushToken,
1864
+ sessions: { connect: { id: sessionId } },
1865
+ users: { connect: { id: userId } }
1866
+ },
1867
+ update: {
1868
+ sessions: { connect: { id: sessionId } },
1869
+ users: { connect: { id: userId } }
1870
+ }
1871
+ });
1872
+ }
1873
+ return { registered: true };
1874
+ });
1875
+ }
1876
+ deregisterPushToken() {
1877
+ return this.authProcedure.meta({ ignoreExpiration: true }).input(deregisterPushTokenSchema).mutation(async ({ ctx, input }) => {
1878
+ this.checkConfig();
1879
+ const { userId } = ctx;
1880
+ const { pushToken } = input;
1881
+ const device = await this.config.prisma.device.findFirst({
1882
+ where: {
1883
+ ...userId !== 0 && { users: { some: { id: userId } } },
1884
+ pushToken
1885
+ },
1886
+ select: { id: true }
1887
+ });
1888
+ if (device) {
1889
+ await this.config.prisma.device.delete({
1890
+ where: { id: device.id }
1891
+ });
1892
+ }
1893
+ return { deregistered: true };
1894
+ });
1895
+ }
1896
+ };
1897
+
1898
+ // src/utilities/trpc.ts
1899
+ var import_server7 = require("@trpc/server");
1900
+ var import_superjson = __toESM(require("superjson"));
1901
+ var import_zod2 = require("zod");
1902
+ function isPrismaConnectionError(error) {
1903
+ if (!error || typeof error !== "object") {
1904
+ return false;
1905
+ }
1906
+ const errorCode = error.code;
1907
+ if (errorCode && typeof errorCode === "string") {
1908
+ const codeMatch = errorCode.match(/^P(\d+)$/);
1909
+ if (codeMatch) {
1910
+ const codeNum = parseInt(codeMatch[1], 10);
1911
+ if (codeNum >= 1e3 && codeNum <= 1003) {
1912
+ return true;
1913
+ }
1914
+ }
1915
+ }
1916
+ const constructorName = error.constructor?.name || "";
1917
+ if (constructorName.includes("Prisma")) {
1918
+ const errorMessage = error.message?.toLowerCase() || "";
1919
+ if (errorMessage.includes("can't reach database") || errorMessage.includes("authentication failed") || errorMessage.includes("database server") || errorMessage.includes("timeout") || errorMessage.includes("connection")) {
1920
+ return true;
1921
+ }
1922
+ }
1923
+ const cause = error.cause;
1924
+ if (cause) {
1925
+ return isPrismaConnectionError(cause);
1926
+ }
1927
+ return false;
1928
+ }
1929
+ function createTrpcBuilder(config) {
1930
+ return import_server7.initTRPC.context().meta().create({
1931
+ transformer: import_superjson.default,
1932
+ errorFormatter: (opts) => {
1933
+ const { shape, error } = opts;
1934
+ const { stack: _stack, ...safeData } = shape.data;
1935
+ if (error.code === "INTERNAL_SERVER_ERROR") {
1936
+ if (config.hooks?.logError) {
1937
+ const errorType = isPrismaConnectionError(error) || isPrismaConnectionError(error.cause) ? "DATABASE_ERROR" : "SERVER_ERROR";
1938
+ config.hooks.logError({
1939
+ type: errorType,
1940
+ description: error.message,
1941
+ stack: error.stack || "No stack trace",
1942
+ ip: opts.ctx?.ip,
1943
+ userId: opts.ctx?.userId ?? null
1944
+ }).catch(() => {
1945
+ });
1946
+ }
1947
+ return {
1948
+ ...shape,
1949
+ message: "An unexpected error occurred. Please try again later.",
1950
+ data: {
1951
+ ...safeData,
1952
+ zodError: error.cause instanceof import_zod2.ZodError ? error.cause.flatten() : null
1953
+ }
1954
+ };
1955
+ }
1956
+ return {
1957
+ ...shape,
1958
+ data: {
1959
+ ...safeData,
1960
+ zodError: error.cause instanceof import_zod2.ZodError ? error.cause.flatten() : null
1961
+ }
1962
+ };
1963
+ }
1964
+ });
1965
+ }
1966
+ function createBaseProcedure(t, authGuard) {
1967
+ return t.procedure.use(authGuard);
1968
+ }
1969
+ function getClientIp(req) {
1970
+ const forwarded = req.headers["x-forwarded-for"];
1971
+ if (forwarded) {
1972
+ return forwarded.split(",")[0]?.trim();
1973
+ }
1974
+ return req.socket.remoteAddress || void 0;
1975
+ }
1976
+
1977
+ // src/router.ts
1978
+ var createContext = ({
1979
+ req,
1980
+ res
1981
+ }) => ({
1982
+ headers: req.headers,
1983
+ userId: null,
1984
+ sessionId: null,
1985
+ refreshToken: null,
1986
+ socketId: null,
1987
+ ip: getClientIp(req),
1988
+ res
1989
+ });
1990
+ var AuthRouterFactory = class {
1991
+ constructor(userConfig) {
1992
+ this.userConfig = userConfig;
1993
+ this.config = createAuthConfig(this.userConfig);
1994
+ this.schemas = createSchemas(
1995
+ this.config.schemaExtensions
1996
+ );
1997
+ this.t = createTrpcBuilder(this.config);
1998
+ this.authGuard = createAuthGuard(this.config, this.t);
1999
+ this.procedure = createBaseProcedure(this.t, this.authGuard);
2000
+ this.authProcedure = this.procedure.meta({ authRequired: true });
2001
+ }
2002
+ createRouter() {
2003
+ const baseRoutes = new BaseProcedureFactory(
2004
+ this.config,
2005
+ this.procedure,
2006
+ this.authProcedure
2007
+ );
2008
+ const biometricRoutes = new BiometricProcedureFactory(
2009
+ this.config,
2010
+ this.authProcedure
2011
+ );
2012
+ const emailVerificationRoutes = new EmailVerificationProcedureFactory(
2013
+ this.config,
2014
+ this.authProcedure
2015
+ );
2016
+ const oAuthLoginRoutes = new OAuthLoginProcedureFactory(
2017
+ this.config,
2018
+ this.procedure
2019
+ );
2020
+ const twoFaRoutes = new TwoFaProcedureFactory(
2021
+ this.config,
2022
+ this.procedure,
2023
+ this.authProcedure
2024
+ );
2025
+ return this.t.router({
2026
+ ...baseRoutes.createBaseProcedures(this.schemas),
2027
+ ...oAuthLoginRoutes.createOAuthLoginProcedures(this.schemas),
2028
+ ...twoFaRoutes.createTwoFaProcedures(),
2029
+ ...biometricRoutes.createBiometricProcedures(),
2030
+ ...emailVerificationRoutes.createEmailVerificationProcedures()
2031
+ });
2032
+ }
2033
+ };
2034
+ function createAuthRouter(config) {
2035
+ const factory = new AuthRouterFactory(config);
2036
+ const router = factory.t.router({
2037
+ auth: factory.createRouter()
2038
+ });
2039
+ return {
2040
+ router,
2041
+ t: factory.t,
2042
+ procedure: factory.procedure,
2043
+ authProcedure: factory.authProcedure,
2044
+ createContext
2045
+ };
2046
+ }
2047
+ // Annotate the CommonJS export names for ESM import in node:
2048
+ 0 && (module.exports = {
2049
+ DEFAULT_STORAGE_KEYS,
2050
+ OAuthVerificationError,
2051
+ biometricVerifySchema,
2052
+ changePasswordSchema,
2053
+ cleanBase32String,
2054
+ clearAuthCookies,
2055
+ comparePassword,
2056
+ createAccessToken,
2057
+ createAuthConfig,
2058
+ createAuthGuard,
2059
+ createAuthRouter,
2060
+ createConsoleEmailAdapter,
2061
+ createNoopEmailAdapter,
2062
+ createOAuthVerifier,
2063
+ decodeToken,
2064
+ defaultAuthConfig,
2065
+ defaultCookieSettings,
2066
+ defaultStorageKeys,
2067
+ defaultTokenSettings,
2068
+ detectBrowser,
2069
+ endAllSessionsSchema,
2070
+ generateOtp,
2071
+ generateTotpCode,
2072
+ generateTotpSecret,
2073
+ hashPassword,
2074
+ isMobileDevice,
2075
+ isNativeApp,
2076
+ isTokenExpiredError,
2077
+ isTokenInvalidError,
2078
+ loginSchema,
2079
+ logoutSchema,
2080
+ oAuthLoginSchema,
2081
+ otpLoginRequestSchema,
2082
+ otpLoginVerifySchema,
2083
+ parseAuthCookies,
2084
+ requestPasswordResetSchema,
2085
+ resetPasswordSchema,
2086
+ setAuthCookies,
2087
+ signupSchema,
2088
+ twoFaResetSchema,
2089
+ twoFaSetupSchema,
2090
+ twoFaVerifySchema,
2091
+ validatePasswordStrength,
2092
+ verifyAccessToken,
2093
+ verifyEmailSchema,
2094
+ verifyTotp
2095
+ });
2096
+ //# sourceMappingURL=index.js.map