@focura/auth-core 1.0.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
+ import crypto4 from 'crypto';
1
2
  import jwt from 'jsonwebtoken';
2
- import crypto3 from 'crypto';
3
3
  import { generateSecret, generateURI, verify } from 'otplib';
4
4
  import { z } from 'zod';
5
5
 
@@ -157,6 +157,9 @@ var init_sessionTimeout = __esm({
157
157
  }
158
158
  });
159
159
 
160
+ // src/auth/authService.ts
161
+ init_config();
162
+
160
163
  // src/tokens/backendToken.ts
161
164
  init_config();
162
165
  var TokenManager = class _TokenManager {
@@ -186,7 +189,7 @@ var TokenManager = class _TokenManager {
186
189
  role: p.role,
187
190
  type: "access",
188
191
  version: this.currentVersion,
189
- jti: crypto3.randomUUID(),
192
+ jti: crypto4.randomUUID(),
190
193
  sessionId: p.sessionId
191
194
  },
192
195
  this.privateKey,
@@ -206,7 +209,7 @@ var TokenManager = class _TokenManager {
206
209
  role: p.role,
207
210
  type: "refresh",
208
211
  version: this.currentVersion,
209
- jti: crypto3.randomUUID(),
212
+ jti: crypto4.randomUUID(),
210
213
  sessionId: p.sessionId
211
214
  },
212
215
  this.privateKey,
@@ -219,7 +222,7 @@ var TokenManager = class _TokenManager {
219
222
  );
220
223
  }
221
224
  createTokenPair(p) {
222
- const sessionId = p.sessionId || crypto3.randomUUID();
225
+ const sessionId = p.sessionId || crypto4.randomUUID();
223
226
  const payload = { ...p, sessionId };
224
227
  return {
225
228
  accessToken: this.createAccessToken(payload),
@@ -234,7 +237,7 @@ var TokenManager = class _TokenManager {
234
237
  sub: userId,
235
238
  type: "sse",
236
239
  version: this.currentVersion,
237
- jti: crypto3.randomUUID()
240
+ jti: crypto4.randomUUID()
238
241
  },
239
242
  this.privateKey,
240
243
  {
@@ -466,48 +469,6 @@ var TokenRevocation = class {
466
469
  }
467
470
  };
468
471
 
469
- // src/refresh/refreshLock.ts
470
- init_config();
471
- var RefreshLock = class {
472
- constructor(redis, prefix = DEFAULTS.keyPrefix) {
473
- this.redis = redis;
474
- this.prefix = prefix;
475
- }
476
- redis;
477
- prefix;
478
- lockKey(sessionId) {
479
- return `${this.prefix}refresh:lock:${sessionId}`;
480
- }
481
- async acquire(sessionId) {
482
- try {
483
- const result = await this.redis.setnx(
484
- this.lockKey(sessionId),
485
- "1"
486
- );
487
- return result === 1;
488
- } catch (err) {
489
- console.error("[RefreshLock] Failed to acquire lock:", err);
490
- return false;
491
- }
492
- }
493
- async release(sessionId) {
494
- try {
495
- await this.redis.del(this.lockKey(sessionId));
496
- } catch (err) {
497
- console.error("[RefreshLock] Failed to release lock:", err);
498
- }
499
- }
500
- async isLocked(sessionId) {
501
- try {
502
- const val = await this.redis.get(this.lockKey(sessionId));
503
- return !!val;
504
- } catch (err) {
505
- console.error("[RefreshLock] Failed to check lock status:", err);
506
- return false;
507
- }
508
- }
509
- };
510
-
511
472
  // src/session/sessionManager.ts
512
473
  init_config();
513
474
  var SessionManager = class {
@@ -604,77 +565,6 @@ var SessionManager = class {
604
565
  }
605
566
  }
606
567
  };
607
- var SERVER_TO_SERVER_UA = /^(node|undici|axios|curl|go-http-client|python-requests|java\/|okhttp|superagent)/i;
608
- function looksLikeServerToServerUA(userAgent) {
609
- return !userAgent || SERVER_TO_SERVER_UA.test(userAgent);
610
- }
611
- function looksLikeServerToServerRequest(req) {
612
- const ua = req.headers["user-agent"] ?? "";
613
- if (looksLikeServerToServerUA(ua)) return true;
614
- return !req.headers["accept-language"] && !req.headers["accept-encoding"];
615
- }
616
- function normalizeUserAgent(userAgent) {
617
- const ua = userAgent || "";
618
- const mobile = /Mobile|Android|iPhone|iPad|iPod/i.test(ua);
619
- const browser = /Edg\//i.test(ua) ? "Edge" : /OPR\/|Opera/i.test(ua) ? "Opera" : /Firefox\/|FxiOS/i.test(ua) ? "Firefox" : /CriOS\/|Chrome\//i.test(ua) ? "Chrome" : /Safari\//i.test(ua) ? "Safari" : "Other";
620
- const os = /Android/i.test(ua) ? "Android" : /iPhone|iPad|iPod/i.test(ua) ? "iOS" : /Windows/i.test(ua) ? "Windows" : /Macintosh|Mac OS X/i.test(ua) ? "macOS" : /Linux/i.test(ua) ? "Linux" : "Other";
621
- return `${browser}|${os}|${mobile ? "mobile" : "desktop"}`;
622
- }
623
- function primaryLanguage(acceptLanguage) {
624
- const first = (acceptLanguage || "").split(",")[0]?.trim() ?? "";
625
- return first ? first.split("-")[0].toLowerCase() : "";
626
- }
627
- function generateDeviceFingerprint(req) {
628
- const components = [
629
- normalizeUserAgent(req.headers["user-agent"] || ""),
630
- primaryLanguage(req.headers["accept-language"] || "")
631
- ].join("|");
632
- return crypto3.createHash("sha256").update(components).digest("hex").substring(0, 32);
633
- }
634
- function getClientIp(req) {
635
- if (typeof req.ip === "string" && req.ip.length > 0) {
636
- return req.ip.startsWith("::ffff:") ? req.ip.slice(7) : req.ip;
637
- }
638
- const forwarded = req.headers["x-forwarded-for"];
639
- if (typeof forwarded === "string") return forwarded.split(",")[0].trim();
640
- const ip = req.socket?.remoteAddress || "unknown";
641
- return ip.startsWith("::ffff:") ? ip.slice(7) : ip;
642
- }
643
- function createSessionMetadata(req) {
644
- return {
645
- deviceId: null,
646
- ipAddress: getClientIp(req),
647
- userAgent: req.headers["user-agent"] || "unknown",
648
- lastActivity: Date.now()
649
- };
650
- }
651
- function isPrivateIp(ip) {
652
- if (!ip || ip === "unknown") return true;
653
- const normalized = ip.startsWith("::ffff:") ? ip.slice(7) : ip;
654
- if (/^\d{1,3}(\.\d{1,3}){3}$/.test(normalized)) {
655
- const [a, b] = normalized.split(".").map(Number);
656
- return a === 0 || a === 10 || a === 127 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
657
- }
658
- return normalized === "::1" || /^fe80:/i.test(normalized) || /^fc/i.test(normalized) || /^fd/i.test(normalized);
659
- }
660
- function validateSessionBinding(req, storedMetadata) {
661
- const currentDeviceId = generateDeviceFingerprint(req);
662
- const currentIp = getClientIp(req);
663
- if (currentDeviceId !== storedMetadata.deviceId) {
664
- return { valid: false, reason: "DEVICE_MISMATCH" };
665
- }
666
- if (currentIp !== storedMetadata.ipAddress) {
667
- if (isPrivateIp(currentIp) || isPrivateIp(storedMetadata.ipAddress)) {
668
- return { valid: true };
669
- }
670
- const timeSinceLastActivity = Date.now() - storedMetadata.lastActivity;
671
- const maxIpChangeInterval = 5 * 60 * 1e3;
672
- if (timeSinceLastActivity < maxIpChangeInterval) {
673
- return { valid: false, reason: "SUSPICIOUS_IP_CHANGE" };
674
- }
675
- }
676
- return { valid: true };
677
- }
678
568
 
679
569
  // src/lockout/accountLockout.ts
680
570
  init_config();
@@ -784,12 +674,6 @@ var TotpManager = class {
784
674
  }
785
675
  };
786
676
 
787
- // src/index.ts
788
- init_sessionTimeout();
789
-
790
- // src/middleware/middlewareFactory.ts
791
- init_config();
792
-
793
677
  // src/errors/index.ts
794
678
  var UnauthorizedError = class extends Error {
795
679
  code;
@@ -891,8 +775,403 @@ var defaultErrors = {
891
775
  BadRequestError: (msg, code) => new BadRequestError(msg, code),
892
776
  ValidationError: (msg, details, code) => new ValidationError(msg, details, code)
893
777
  };
778
+ var SERVER_TO_SERVER_UA = /^(node|undici|axios|curl|go-http-client|python-requests|java\/|okhttp|superagent)/i;
779
+ function looksLikeServerToServerUA(userAgent) {
780
+ return !userAgent || SERVER_TO_SERVER_UA.test(userAgent);
781
+ }
782
+ function looksLikeServerToServerRequest(req) {
783
+ const ua = req.headers["user-agent"] ?? "";
784
+ if (looksLikeServerToServerUA(ua)) return true;
785
+ return !req.headers["accept-language"] && !req.headers["accept-encoding"];
786
+ }
787
+ function normalizeUserAgent(userAgent) {
788
+ const ua = userAgent || "";
789
+ const mobile = /Mobile|Android|iPhone|iPad|iPod/i.test(ua);
790
+ const browser = /Edg\//i.test(ua) ? "Edge" : /OPR\/|Opera/i.test(ua) ? "Opera" : /Firefox\/|FxiOS/i.test(ua) ? "Firefox" : /CriOS\/|Chrome\//i.test(ua) ? "Chrome" : /Safari\//i.test(ua) ? "Safari" : "Other";
791
+ const os = /Android/i.test(ua) ? "Android" : /iPhone|iPad|iPod/i.test(ua) ? "iOS" : /Windows/i.test(ua) ? "Windows" : /Macintosh|Mac OS X/i.test(ua) ? "macOS" : /Linux/i.test(ua) ? "Linux" : "Other";
792
+ return `${browser}|${os}|${mobile ? "mobile" : "desktop"}`;
793
+ }
794
+ function primaryLanguage(acceptLanguage) {
795
+ const first = (acceptLanguage || "").split(",")[0]?.trim() ?? "";
796
+ return first ? first.split("-")[0].toLowerCase() : "";
797
+ }
798
+ function generateDeviceFingerprint(req) {
799
+ const components = [
800
+ normalizeUserAgent(req.headers["user-agent"] || ""),
801
+ primaryLanguage(req.headers["accept-language"] || "")
802
+ ].join("|");
803
+ return crypto4.createHash("sha256").update(components).digest("hex").substring(0, 32);
804
+ }
805
+ function getClientIp(req) {
806
+ if (typeof req.ip === "string" && req.ip.length > 0) {
807
+ return req.ip.startsWith("::ffff:") ? req.ip.slice(7) : req.ip;
808
+ }
809
+ const forwarded = req.headers["x-forwarded-for"];
810
+ if (typeof forwarded === "string") return forwarded.split(",")[0].trim();
811
+ const ip = req.socket?.remoteAddress || "unknown";
812
+ return ip.startsWith("::ffff:") ? ip.slice(7) : ip;
813
+ }
814
+ function createSessionMetadata(req) {
815
+ return {
816
+ deviceId: null,
817
+ ipAddress: getClientIp(req),
818
+ userAgent: req.headers["user-agent"] || "unknown",
819
+ lastActivity: Date.now()
820
+ };
821
+ }
822
+ function isPrivateIp(ip) {
823
+ if (!ip || ip === "unknown") return true;
824
+ const normalized = ip.startsWith("::ffff:") ? ip.slice(7) : ip;
825
+ if (/^\d{1,3}(\.\d{1,3}){3}$/.test(normalized)) {
826
+ const [a, b] = normalized.split(".").map(Number);
827
+ return a === 0 || a === 10 || a === 127 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a === 169 && b === 254;
828
+ }
829
+ return normalized === "::1" || /^fe80:/i.test(normalized) || /^fc/i.test(normalized) || /^fd/i.test(normalized);
830
+ }
831
+ function validateSessionBinding(req, storedMetadata) {
832
+ const currentDeviceId = generateDeviceFingerprint(req);
833
+ const currentIp = getClientIp(req);
834
+ if (currentDeviceId !== storedMetadata.deviceId) {
835
+ return { valid: false, reason: "DEVICE_MISMATCH" };
836
+ }
837
+ if (currentIp !== storedMetadata.ipAddress) {
838
+ if (isPrivateIp(currentIp) || isPrivateIp(storedMetadata.ipAddress)) {
839
+ return { valid: true };
840
+ }
841
+ const timeSinceLastActivity = Date.now() - storedMetadata.lastActivity;
842
+ const maxIpChangeInterval = 5 * 60 * 1e3;
843
+ if (timeSinceLastActivity < maxIpChangeInterval) {
844
+ return { valid: false, reason: "SUSPICIOUS_IP_CHANGE" };
845
+ }
846
+ }
847
+ return { valid: true };
848
+ }
849
+
850
+ // src/auth/authService.ts
851
+ var AuthService = class {
852
+ tokenManager;
853
+ tokenRevocation;
854
+ sessionManager;
855
+ accountLockout;
856
+ totpManager;
857
+ audit;
858
+ errors;
859
+ config;
860
+ userStore;
861
+ constructor(rawConfig) {
862
+ this.config = resolveConfig(rawConfig);
863
+ this.userStore = rawConfig.userStore;
864
+ this.errors = rawConfig.errors ?? defaultErrors;
865
+ this.tokenManager = new TokenManager(rawConfig.jwt);
866
+ this.tokenRevocation = new TokenRevocation(rawConfig.redis, this.config.keyPrefix);
867
+ this.sessionManager = new SessionManager(
868
+ rawConfig.redis,
869
+ this.tokenRevocation,
870
+ rawConfig.auditLogger,
871
+ this.config.keyPrefix,
872
+ this.config.maxConcurrentSessions
873
+ );
874
+ this.accountLockout = new AccountLockout(rawConfig.redis, {
875
+ maxFailures: this.config.lockoutMaxFailures,
876
+ lockoutSeconds: this.config.lockoutSeconds,
877
+ windowSeconds: this.config.lockoutWindowSeconds,
878
+ prefix: `${this.config.keyPrefix}lockout`
879
+ });
880
+ this.totpManager = new TotpManager(rawConfig.jwt.issuer ?? "Auth");
881
+ this.audit = new AuditLog(rawConfig.auditLogger);
882
+ }
883
+ getConfig() {
884
+ return this.config;
885
+ }
886
+ getRedis() {
887
+ return this.config.redis;
888
+ }
889
+ async exchange(input) {
890
+ const age = Date.now() - input.timestamp;
891
+ if (age > 6e4 || age < 0) {
892
+ this.audit.log("EXCHANGE_FAILED", { userId: input.userId, reason: "Proof expired" });
893
+ throw this.errors.UnauthorizedError("Exchange proof expired", "PROOF_EXPIRED");
894
+ }
895
+ const proofPayload = `${input.userId}${input.email}${input.role}${input.sessionId}${input.timestamp}`;
896
+ const expected = crypto4.createHmac("sha256", this.config.hmacSecret).update(proofPayload).digest("hex");
897
+ let valid = false;
898
+ try {
899
+ if (input.signature) {
900
+ valid = crypto4.timingSafeEqual(Buffer.from(input.signature, "hex"), Buffer.from(expected, "hex"));
901
+ }
902
+ } catch {
903
+ valid = false;
904
+ }
905
+ if (!valid) {
906
+ this.audit.log("EXCHANGE_FAILED", { userId: input.userId, reason: "Invalid signature" });
907
+ throw this.errors.UnauthorizedError("Invalid exchange proof", "INVALID_SIGNATURE");
908
+ }
909
+ const idempotencyKey = `exchange:idempotent:${input.userId}:${input.sessionId}:${input.timestamp}`;
910
+ const existing = await this.config.redis.get(idempotencyKey);
911
+ if (existing) {
912
+ return JSON.parse(existing);
913
+ }
914
+ const user = await this.userStore.findById(input.userId);
915
+ if (!user) {
916
+ this.audit.log("EXCHANGE_FAILED", { userId: input.userId, reason: "User not found" });
917
+ throw this.errors.UnauthorizedError("User not found", "USER_NOT_FOUND");
918
+ }
919
+ if (user.email.toLowerCase() !== input.email.toLowerCase()) {
920
+ this.audit.log("EXCHANGE_FAILED", { userId: input.userId, reason: "Email mismatch" });
921
+ throw this.errors.UnauthorizedError("Email mismatch", "EMAIL_MISMATCH");
922
+ }
923
+ if (!user.emailVerified) {
924
+ this.audit.log("EXCHANGE_FAILED", { userId: input.userId, reason: "Email not verified" });
925
+ throw this.errors.UnauthorizedError("Email not verified", "EMAIL_NOT_VERIFIED");
926
+ }
927
+ const sessionId = input.sessionId || crypto4.randomUUID();
928
+ const tokens = this.tokenManager.createTokenPair({
929
+ id: user.id,
930
+ email: user.email,
931
+ role: user.role,
932
+ sessionId
933
+ });
934
+ const refreshTtlSeconds = TokenManager.parseExpiry(this.tokenManager.getRefreshTokenExpiry()) / 1e3;
935
+ await this.tokenRevocation.storeRefreshToken(user.id, TokenManager.extractJti(tokens.refreshToken), refreshTtlSeconds);
936
+ const sseToken = this.tokenManager.createSseToken(user.id);
937
+ const sseTtlSeconds = TokenManager.parseExpiry(this.tokenManager.getSseTokenExpiry()) / 1e3;
938
+ await this.tokenRevocation.storeSseToken(TokenManager.extractJti(sseToken), user.id, sseTtlSeconds);
939
+ await this.config.redis.setex(
940
+ `${this.config.keyPrefix}session:created:${sessionId}`,
941
+ this.config.absoluteTimeout,
942
+ Date.now().toString()
943
+ );
944
+ await this.config.redis.setex(
945
+ `${this.config.keyPrefix}session:activity:${sessionId}`,
946
+ this.config.inactivityTimeout,
947
+ Date.now().toString()
948
+ );
949
+ await this.sessionManager.trackUserSession(user.id, sessionId);
950
+ const result = { ...tokens, sseToken, sessionId };
951
+ await this.config.redis.setex(idempotencyKey, 90, JSON.stringify(result));
952
+ this.audit.log("EXCHANGE_SUCCESS", { userId: user.id, email: user.email });
953
+ return result;
954
+ }
955
+ async verifyToken(input) {
956
+ let decoded;
957
+ try {
958
+ decoded = this.tokenManager.verifyToken(input.token);
959
+ } catch (err) {
960
+ if (err.name === "TokenExpiredError") {
961
+ throw this.errors.TokenExpiredError();
962
+ }
963
+ throw this.errors.InvalidTokenError();
964
+ }
965
+ if (decoded.version !== this.config.currentVersion) {
966
+ throw this.errors.InvalidTokenError("TOKEN_VERSION_MISMATCH");
967
+ }
968
+ if (decoded.type !== "access") {
969
+ throw this.errors.InvalidTokenError("INVALID_TOKEN_TYPE");
970
+ }
971
+ const [isRevoked, sessionWasRevoked] = await Promise.all([
972
+ decoded.jti ? this.tokenRevocation.isAccessTokenRevoked(decoded.jti) : Promise.resolve(false),
973
+ decoded.sessionId ? this.tokenRevocation.isSessionRevoked(decoded.sessionId) : Promise.resolve(false)
974
+ ]);
975
+ if (isRevoked) throw this.errors.TokenRevokedError();
976
+ if (sessionWasRevoked) throw this.errors.TokenRevokedError();
977
+ if (decoded.sessionId && input.ipAddress && input.userAgent) {
978
+ const metadataKey = `${this.config.keyPrefix}session:metadata:${decoded.sessionId}`;
979
+ const storedMetadata = await this.config.redis.get(metadataKey);
980
+ if (storedMetadata) {
981
+ let metadata;
982
+ try {
983
+ metadata = JSON.parse(storedMetadata);
984
+ } catch {
985
+ metadata = { deviceId: null, ipAddress: input.ipAddress, userAgent: input.userAgent, lastActivity: Date.now() };
986
+ }
987
+ if (metadata.deviceId == null) {
988
+ metadata.deviceId = crypto4.createHash("sha256").update(`${input.userAgent}|${input.ipAddress}`).digest("hex").substring(0, 32);
989
+ metadata.ipAddress = input.ipAddress;
990
+ metadata.userAgent = input.userAgent;
991
+ metadata.lastActivity = Date.now();
992
+ await this.config.redis.setex(metadataKey, this.config.sessionMetadataTtl, JSON.stringify(metadata));
993
+ } else {
994
+ const req = { headers: { "user-agent": input.userAgent }, ip: input.ipAddress };
995
+ const binding = validateSessionBinding(req, metadata);
996
+ if (!binding.valid) {
997
+ await this.tokenRevocation.revokeAllRefreshTokens(decoded.id);
998
+ this.audit.log("SESSION_HIJACK_DETECTED", {
999
+ userId: decoded.id,
1000
+ sessionId: decoded.sessionId,
1001
+ reason: binding.reason
1002
+ });
1003
+ throw this.errors.SessionHijackError(binding.reason);
1004
+ }
1005
+ metadata.lastActivity = Date.now();
1006
+ await this.config.redis.setex(metadataKey, this.config.sessionMetadataTtl, JSON.stringify(metadata));
1007
+ }
1008
+ }
1009
+ }
1010
+ const user = await this.userStore.findById(decoded.id);
1011
+ if (!user) throw this.errors.UnauthorizedError("User not found", "USER_NOT_FOUND");
1012
+ if (!user.emailVerified) throw this.errors.EmailNotVerifiedError();
1013
+ if (user.bannedAt) throw this.errors.AccountBannedError(user.banReason, user.bannedAt);
1014
+ return {
1015
+ user,
1016
+ payload: {
1017
+ id: decoded.id,
1018
+ email: decoded.email,
1019
+ role: decoded.role,
1020
+ jti: decoded.jti,
1021
+ sessionId: decoded.sessionId
1022
+ }
1023
+ };
1024
+ }
1025
+ async refresh(input) {
1026
+ let decoded;
1027
+ try {
1028
+ decoded = this.tokenManager.verifyToken(input.refreshToken, "refresh");
1029
+ } catch {
1030
+ throw this.errors.UnauthorizedError("Invalid refresh token", "INVALID_TOKEN");
1031
+ }
1032
+ const lockKey = `${this.config.keyPrefix}refresh:lock:${decoded.sessionId}`;
1033
+ const lockResult = await this.config.redis.set(lockKey, "1", "EX", DEFAULTS.refreshLockTtlSeconds, "NX");
1034
+ if (lockResult !== "OK") {
1035
+ throw this.errors.BadRequestError("Refresh already in progress", "REFRESH_IN_PROGRESS");
1036
+ }
1037
+ try {
1038
+ const dedupeKey = `${this.config.keyPrefix}refresh:dedupe:${decoded.id}:${decoded.jti}`;
1039
+ const cached = await this.config.redis.get(dedupeKey);
1040
+ if (cached) return JSON.parse(cached);
1041
+ const sessionRevokedKey = `${this.config.keyPrefix}session:revoked:${decoded.sessionId}`;
1042
+ if (await this.config.redis.get(sessionRevokedKey) === "1") {
1043
+ throw this.errors.UnauthorizedError("Session revoked", "SESSION_REVOKED");
1044
+ }
1045
+ const createdKey = `${this.config.keyPrefix}session:created:${decoded.sessionId}`;
1046
+ if (await this.config.redis.exists(createdKey) !== 1) {
1047
+ throw this.errors.UnauthorizedError("Session expired", "SESSION_TIMEOUT");
1048
+ }
1049
+ const newTokens = this.tokenManager.createTokenPair({
1050
+ id: decoded.id,
1051
+ email: decoded.email,
1052
+ role: decoded.role,
1053
+ sessionId: decoded.sessionId
1054
+ });
1055
+ const refreshTtlSeconds = TokenManager.parseExpiry(this.tokenManager.getRefreshTokenExpiry()) / 1e3;
1056
+ await this.tokenRevocation.rotateRefreshToken(
1057
+ decoded.id,
1058
+ decoded.jti,
1059
+ TokenManager.extractJti(newTokens.refreshToken),
1060
+ refreshTtlSeconds
1061
+ );
1062
+ const newSseToken = this.tokenManager.createSseToken(decoded.id);
1063
+ const sseTtlSeconds = TokenManager.parseExpiry(this.tokenManager.getSseTokenExpiry()) / 1e3;
1064
+ await this.tokenRevocation.storeSseToken(TokenManager.extractJti(newSseToken), decoded.id, sseTtlSeconds);
1065
+ const response = { ...newTokens, sseToken: newSseToken };
1066
+ await this.config.redis.setex(dedupeKey, DEFAULTS.refreshDedupeTtlSeconds, JSON.stringify(response));
1067
+ this.audit.log("TOKEN_REFRESHED", { userId: decoded.id, sessionId: decoded.sessionId });
1068
+ return response;
1069
+ } finally {
1070
+ await this.config.redis.del(lockKey);
1071
+ }
1072
+ }
1073
+ async logout(input) {
1074
+ if (input.accessTokenJti && input.accessToken) {
1075
+ const expiry = this.tokenManager.getAccessTokenExpiry();
1076
+ const ttlSeconds = TokenManager.parseExpiry(expiry) / 1e3;
1077
+ await this.tokenRevocation.revokeAccessToken(input.accessTokenJti, ttlSeconds);
1078
+ }
1079
+ if (input.logoutAll && input.userId) {
1080
+ await this.tokenRevocation.revokeAllRefreshTokens(input.userId);
1081
+ if (input.sessionId) {
1082
+ await this.config.redis.setex(
1083
+ `${this.config.keyPrefix}session:revoked:${input.sessionId}`,
1084
+ DEFAULTS.revokedSessionTtl,
1085
+ "1"
1086
+ );
1087
+ }
1088
+ await this.sessionManager.revokeUserSession(input.userId, input.sessionId ?? "");
1089
+ this.audit.log("LOGOUT_ALL_DEVICES", { userId: input.userId });
1090
+ } else if (input.userId && input.sessionId) {
1091
+ await this.sessionManager.revokeUserSession(input.userId, input.sessionId);
1092
+ this.audit.log("LOGOUT", { userId: input.userId, sessionId: input.sessionId });
1093
+ }
1094
+ }
1095
+ async getActiveSessions(userId) {
1096
+ return this.sessionManager.getUserActiveSessions(userId);
1097
+ }
1098
+ async revokeSession(userId, sessionId) {
1099
+ await this.sessionManager.revokeUserSession(userId, sessionId);
1100
+ await this.tokenRevocation.markSessionRevoked(sessionId);
1101
+ this.audit.log("SESSION_REVOKED", { userId, sessionId });
1102
+ }
1103
+ generateTwoFactor() {
1104
+ const secret = this.totpManager.generateSecret();
1105
+ const uri = this.totpManager.createUri(secret, "");
1106
+ return { secret, uri };
1107
+ }
1108
+ createTwoFactorUri(secret, email) {
1109
+ return this.totpManager.createUri(secret, email);
1110
+ }
1111
+ async verifyTwoFactor(input) {
1112
+ return this.totpManager.verify(input.token, input.secret);
1113
+ }
1114
+ async recordLoginFailure(email) {
1115
+ return this.accountLockout.recordFailedAttempt(email);
1116
+ }
1117
+ async clearLoginFailures(email) {
1118
+ return this.accountLockout.clearFailedAttempts(email);
1119
+ }
1120
+ async isAccountLocked(email) {
1121
+ return this.accountLockout.isAccountLocked(email);
1122
+ }
1123
+ log(event, data) {
1124
+ this.audit.log(event, data);
1125
+ }
1126
+ };
1127
+
1128
+ // src/refresh/refreshLock.ts
1129
+ init_config();
1130
+ var RefreshLock = class {
1131
+ constructor(redis, prefix = DEFAULTS.keyPrefix) {
1132
+ this.redis = redis;
1133
+ this.prefix = prefix;
1134
+ }
1135
+ redis;
1136
+ prefix;
1137
+ lockKey(sessionId) {
1138
+ return `${this.prefix}refresh:lock:${sessionId}`;
1139
+ }
1140
+ async acquire(sessionId) {
1141
+ try {
1142
+ const result = await this.redis.setnx(
1143
+ this.lockKey(sessionId),
1144
+ "1"
1145
+ );
1146
+ return result === 1;
1147
+ } catch (err) {
1148
+ console.error("[RefreshLock] Failed to acquire lock:", err);
1149
+ return false;
1150
+ }
1151
+ }
1152
+ async release(sessionId) {
1153
+ try {
1154
+ await this.redis.del(this.lockKey(sessionId));
1155
+ } catch (err) {
1156
+ console.error("[RefreshLock] Failed to release lock:", err);
1157
+ }
1158
+ }
1159
+ async isLocked(sessionId) {
1160
+ try {
1161
+ const val = await this.redis.get(this.lockKey(sessionId));
1162
+ return !!val;
1163
+ } catch (err) {
1164
+ console.error("[RefreshLock] Failed to check lock status:", err);
1165
+ return false;
1166
+ }
1167
+ }
1168
+ };
1169
+
1170
+ // src/index.ts
1171
+ init_sessionTimeout();
894
1172
 
895
1173
  // src/middleware/middlewareFactory.ts
1174
+ init_config();
896
1175
  var MiddlewareFactory = class {
897
1176
  config;
898
1177
  tokenManager;
@@ -1082,7 +1361,7 @@ var MiddlewareFactory = class {
1082
1361
  const CSRF_TOKEN_TTL = 3600;
1083
1362
  return {
1084
1363
  generateToken: async (userId, sessionId) => {
1085
- const token = crypto3.randomBytes(CSRF_TOKEN_LENGTH).toString("base64url");
1364
+ const token = crypto4.randomBytes(CSRF_TOKEN_LENGTH).toString("base64url");
1086
1365
  const key = `${self.config.keyPrefix}csrf:${userId}:${sessionId}`;
1087
1366
  await self.config.redis.setex(key, CSRF_TOKEN_TTL, token);
1088
1367
  return token;
@@ -1093,7 +1372,7 @@ var MiddlewareFactory = class {
1093
1372
  const storedToken = await self.config.redis.get(key);
1094
1373
  if (!storedToken) return false;
1095
1374
  try {
1096
- return crypto3.timingSafeEqual(Buffer.from(token, "base64url"), Buffer.from(storedToken, "base64url"));
1375
+ return crypto4.timingSafeEqual(Buffer.from(token, "base64url"), Buffer.from(storedToken, "base64url"));
1097
1376
  } catch {
1098
1377
  return false;
1099
1378
  }
@@ -1111,7 +1390,7 @@ var MiddlewareFactory = class {
1111
1390
  if (!stored || !csrfToken) return res.status(403).json({ success: false, message: "Invalid CSRF token", code: "CSRF_VALIDATION_FAILED" });
1112
1391
  let isValid = false;
1113
1392
  try {
1114
- isValid = crypto3.timingSafeEqual(Buffer.from(csrfToken, "base64url"), Buffer.from(stored, "base64url"));
1393
+ isValid = crypto4.timingSafeEqual(Buffer.from(csrfToken, "base64url"), Buffer.from(stored, "base64url"));
1115
1394
  } catch {
1116
1395
  isValid = false;
1117
1396
  }
@@ -1180,11 +1459,11 @@ var MiddlewareFactory = class {
1180
1459
  throw self.errors.UnauthorizedError("Exchange proof expired", "PROOF_EXPIRED");
1181
1460
  }
1182
1461
  const proofPayload = `${userId}${email}${role}${sessionId}${timestamp}`;
1183
- const expected = crypto3.createHmac("sha256", self.config.hmacSecret).update(proofPayload).digest("hex");
1462
+ const expected = crypto4.createHmac("sha256", self.config.hmacSecret).update(proofPayload).digest("hex");
1184
1463
  let valid = false;
1185
1464
  try {
1186
1465
  if (signature) {
1187
- valid = crypto3.timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"));
1466
+ valid = crypto4.timingSafeEqual(Buffer.from(signature, "hex"), Buffer.from(expected, "hex"));
1188
1467
  }
1189
1468
  } catch {
1190
1469
  valid = false;
@@ -1211,7 +1490,7 @@ var MiddlewareFactory = class {
1211
1490
  self.audit.log("EXCHANGE_FAILED", { userId, email, ip, reason: "Email not verified" });
1212
1491
  throw self.errors.UnauthorizedError("Email not verified", "EMAIL_NOT_VERIFIED");
1213
1492
  }
1214
- const sid = sessionId || crypto3.randomUUID();
1493
+ const sid = sessionId || crypto4.randomUUID();
1215
1494
  const tokens = self.tokenManager.createTokenPair({
1216
1495
  id: user.id,
1217
1496
  email: user.email,
@@ -1357,6 +1636,6 @@ var logoutSchema = z.object({
1357
1636
  // src/index.ts
1358
1637
  init_config();
1359
1638
 
1360
- export { AUDIT_SEVERITY, AccountBannedError, AccountLockout, AuditLog, BadRequestError, DEFAULTS, EmailNotVerifiedError, ForbiddenError, InvalidTokenError, MiddlewareFactory, RefreshLock, SessionHijackError, SessionManager, SessionTimeoutManager, TokenExpiredError, TokenManager, TokenRevocation, TokenRevokedError, TotpManager, UnauthorizedError, ValidationError, createSessionMetadata, defaultErrors, exchangeSchema, generateDeviceFingerprint, getClientIp, isPrivateIp, logoutSchema, looksLikeServerToServerRequest, looksLikeServerToServerUA, normalizeUserAgent, refreshSchema, resolveConfig, validateSessionBinding };
1639
+ export { AUDIT_SEVERITY, AccountBannedError, AccountLockout, AuditLog, AuthService, BadRequestError, DEFAULTS, EmailNotVerifiedError, ForbiddenError, InvalidTokenError, MiddlewareFactory, RefreshLock, SessionHijackError, SessionManager, SessionTimeoutManager, TokenExpiredError, TokenManager, TokenRevocation, TokenRevokedError, TotpManager, UnauthorizedError, ValidationError, createSessionMetadata, defaultErrors, exchangeSchema, generateDeviceFingerprint, getClientIp, isPrivateIp, logoutSchema, looksLikeServerToServerRequest, looksLikeServerToServerUA, normalizeUserAgent, refreshSchema, resolveConfig, validateSessionBinding };
1361
1640
  //# sourceMappingURL=index.js.map
1362
1641
  //# sourceMappingURL=index.js.map