strapi-plugin-magic-sessionmanager 4.4.5 → 4.4.6

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.
@@ -172,11 +172,19 @@ const getClientIp$1 = (ctx) => {
172
172
  };
173
173
  const cleanIp = (ip) => {
174
174
  if (!ip) return "unknown";
175
- ip = ip.split(":")[0];
175
+ ip = ip.trim();
176
+ if (ip.startsWith("[")) {
177
+ const bracketEnd = ip.indexOf("]");
178
+ if (bracketEnd !== -1) {
179
+ ip = ip.substring(1, bracketEnd);
180
+ }
181
+ }
176
182
  if (ip.startsWith("::ffff:")) {
177
183
  ip = ip.substring(7);
178
184
  }
179
- ip = ip.trim();
185
+ if (ip.includes(".") && ip.includes(":") && ip.indexOf(":") === ip.lastIndexOf(":")) {
186
+ ip = ip.split(":")[0];
187
+ }
180
188
  return ip || "unknown";
181
189
  };
182
190
  const isPrivateIp = (ip) => {
@@ -220,11 +228,12 @@ function encryptToken$2(token) {
220
228
  const authTag = cipher.getAuthTag();
221
229
  return `${iv.toString("hex")}:${authTag.toString("hex")}:${encrypted}`;
222
230
  } catch (err) {
223
- console.error("[magic-sessionmanager/encryption] Encryption failed:", err);
231
+ const errMsg = err instanceof Error ? err.message : "Unknown encryption error";
232
+ console.error("[magic-sessionmanager/encryption] Encryption failed:", errMsg);
224
233
  throw new Error("Failed to encrypt token");
225
234
  }
226
235
  }
227
- function decryptToken$3(encryptedToken) {
236
+ function decryptToken$2(encryptedToken) {
228
237
  if (!encryptedToken) return null;
229
238
  try {
230
239
  const key = getEncryptionKey();
@@ -241,7 +250,8 @@ function decryptToken$3(encryptedToken) {
241
250
  decrypted += decipher.final("utf8");
242
251
  return decrypted;
243
252
  } catch (err) {
244
- console.error("[magic-sessionmanager/encryption] Decryption failed:", err);
253
+ const errMsg = err instanceof Error ? err.message : "Unknown decryption error";
254
+ console.error("[magic-sessionmanager/encryption] Decryption failed:", errMsg);
245
255
  return null;
246
256
  }
247
257
  }
@@ -257,7 +267,7 @@ function hashToken$3(token) {
257
267
  }
258
268
  var encryption = {
259
269
  encryptToken: encryptToken$2,
260
- decryptToken: decryptToken$3,
270
+ decryptToken: decryptToken$2,
261
271
  generateSessionId: generateSessionId$1,
262
272
  hashToken: hashToken$3
263
273
  };
@@ -288,12 +298,25 @@ function isAuthEndpoint(path2) {
288
298
  }
289
299
  const userIdCache = /* @__PURE__ */ new Map();
290
300
  const CACHE_TTL = 5 * 60 * 1e3;
301
+ const CACHE_MAX_SIZE = 1e3;
291
302
  async function getDocumentIdFromNumericId(strapi2, numericId) {
292
303
  const cacheKey = `user_${numericId}`;
293
304
  const cached2 = userIdCache.get(cacheKey);
294
305
  if (cached2 && Date.now() - cached2.timestamp < CACHE_TTL) {
295
306
  return cached2.documentId;
296
307
  }
308
+ if (userIdCache.size >= CACHE_MAX_SIZE) {
309
+ const now = Date.now();
310
+ for (const [key, value] of userIdCache) {
311
+ if (now - value.timestamp >= CACHE_TTL) {
312
+ userIdCache.delete(key);
313
+ }
314
+ }
315
+ if (userIdCache.size >= CACHE_MAX_SIZE) {
316
+ const keysToDelete = [...userIdCache.keys()].slice(0, Math.floor(CACHE_MAX_SIZE / 4));
317
+ keysToDelete.forEach((key) => userIdCache.delete(key));
318
+ }
319
+ }
297
320
  try {
298
321
  const user = await strapi2.entityService.findOne(USER_UID$3, numericId, {
299
322
  fields: ["documentId"]
@@ -336,7 +359,7 @@ var lastSeen = ({ strapi: strapi2, sessionService }) => {
336
359
  isActive: false
337
360
  },
338
361
  limit: 5,
339
- fields: ["documentId", "terminatedManually", "lastActive"],
362
+ fields: ["documentId", "terminatedManually", "lastActive", "loginTime"],
340
363
  sort: [{ lastActive: "desc" }]
341
364
  });
342
365
  if (inactiveSessions && inactiveSessions.length > 0) {
@@ -346,6 +369,14 @@ var lastSeen = ({ strapi: strapi2, sessionService }) => {
346
369
  return ctx.unauthorized("Session has been terminated. Please login again.");
347
370
  }
348
371
  const sessionToReactivate = inactiveSessions[0];
372
+ const maxAgeDays = config2.maxSessionAgeDays || 30;
373
+ const loginTime = sessionToReactivate.loginTime ? new Date(sessionToReactivate.loginTime).getTime() : sessionToReactivate.lastActive ? new Date(sessionToReactivate.lastActive).getTime() : 0;
374
+ const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
375
+ const isExpired = loginTime > 0 && Date.now() - loginTime > maxAgeMs;
376
+ if (isExpired) {
377
+ strapi2.log.info(`[magic-sessionmanager] [BLOCKED] Session exceeded max age of ${maxAgeDays} days (user: ${userDocId2.substring(0, 8)}...)`);
378
+ return ctx.unauthorized("Session expired. Please login again.");
379
+ }
349
380
  await strapi2.documents(SESSION_UID$4).update({
350
381
  documentId: sessionToReactivate.documentId,
351
382
  data: {
@@ -386,7 +417,7 @@ var lastSeen = ({ strapi: strapi2, sessionService }) => {
386
417
  };
387
418
  };
388
419
  const getClientIp = getClientIp_1;
389
- const { encryptToken: encryptToken$1, decryptToken: decryptToken$2, hashToken: hashToken$2 } = encryption;
420
+ const { encryptToken: encryptToken$1, decryptToken: decryptToken$1, hashToken: hashToken$2 } = encryption;
390
421
  const { createLogger: createLogger$3 } = logger;
391
422
  const SESSION_UID$3 = "plugin::magic-sessionmanager.session";
392
423
  const USER_UID$2 = "plugin::users-permissions.user";
@@ -420,11 +451,11 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
420
451
  log.info("║ [SUCCESS] SESSION MANAGER LICENSE ACTIVE ║");
421
452
  log.info("║ ║");
422
453
  if (licenseStatus.data) {
423
- log.info(`║ License: ${licenseStatus.data.licenseKey}`.padEnd(66) + "");
454
+ const maskedKey = licenseStatus.data.licenseKey ? `${licenseStatus.data.licenseKey.substring(0, 8)}...` : "N/A";
455
+ log.info(`║ License: ${maskedKey}`.padEnd(66) + "║");
424
456
  log.info(`║ User: ${licenseStatus.data.firstName} ${licenseStatus.data.lastName}`.padEnd(66) + "║");
425
- log.info(`║ Email: ${licenseStatus.data.email}`.padEnd(66) + "║");
426
457
  } else if (storedKey) {
427
- log.info(`║ License: ${storedKey} (Offline Mode)`.padEnd(66) + "║");
458
+ log.info(`║ License: ${storedKey.substring(0, 8)}... (Offline Mode)`.padEnd(66) + "║");
428
459
  log.info(`║ Status: Grace Period Active`.padEnd(66) + "║");
429
460
  }
430
461
  log.info("║ ║");
@@ -456,10 +487,21 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
456
487
  try {
457
488
  const token = ctx.request.headers?.authorization?.replace("Bearer ", "");
458
489
  if (!token) {
459
- ctx.status = 200;
460
- ctx.body = { message: "Logged out successfully" };
490
+ ctx.status = 401;
491
+ ctx.body = { error: { status: 401, message: "Authorization token required" } };
461
492
  return;
462
493
  }
494
+ try {
495
+ const jwtService = strapi2.plugin("users-permissions").service("jwt");
496
+ const decoded = await jwtService.verify(token);
497
+ if (!decoded || !decoded.id) {
498
+ ctx.status = 401;
499
+ ctx.body = { error: { status: 401, message: "Invalid token" } };
500
+ return;
501
+ }
502
+ } catch (jwtErr) {
503
+ log.debug("JWT verify failed during logout (cleaning up anyway):", jwtErr.message);
504
+ }
463
505
  const tokenHashValue = hashToken$2(token);
464
506
  const matchingSession = await strapi2.documents(SESSION_UID$3).findFirst({
465
507
  filters: {
@@ -481,6 +523,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
481
523
  },
482
524
  config: {
483
525
  auth: false
526
+ // We handle auth manually above to support expired-but-valid tokens
484
527
  }
485
528
  }]);
486
529
  log.info("[SUCCESS] /api/auth/logout route registered");
@@ -540,8 +583,7 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
540
583
  ctx.body = {
541
584
  error: {
542
585
  status: 403,
543
- message: "Login blocked for security reasons",
544
- details: { reason: blockReason }
586
+ message: "Login blocked for security reasons. Please contact support."
545
587
  }
546
588
  };
547
589
  return;
@@ -603,11 +645,22 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
603
645
  geoData
604
646
  });
605
647
  if (config2.discordWebhookUrl) {
606
- await notificationService.sendWebhook({
607
- event: "session.login",
608
- data: webhookData,
609
- webhookUrl: config2.discordWebhookUrl
610
- });
648
+ const webhookUrl = config2.discordWebhookUrl;
649
+ try {
650
+ const parsed = new URL(webhookUrl);
651
+ const isValidDomain = parsed.protocol === "https:" && (parsed.hostname === "discord.com" || parsed.hostname === "discordapp.com" || parsed.hostname.endsWith(".discord.com") || parsed.hostname === "hooks.slack.com");
652
+ if (isValidDomain) {
653
+ await notificationService.sendWebhook({
654
+ event: "session.login",
655
+ data: webhookData,
656
+ webhookUrl
657
+ });
658
+ } else {
659
+ log.warn(`[SECURITY] Blocked webhook to untrusted domain: ${parsed.hostname}`);
660
+ }
661
+ } catch {
662
+ log.warn("[SECURITY] Invalid webhook URL in plugin config");
663
+ }
611
664
  }
612
665
  }
613
666
  } catch (notifErr) {
@@ -788,6 +841,30 @@ async function ensureTokenHashIndex(strapi2, log) {
788
841
  log.debug("[INDEX] Could not create tokenHash index (will retry on next startup):", err.message);
789
842
  }
790
843
  }
844
+ const sessionCheckErrors = { count: 0, lastReset: Date.now() };
845
+ const MAX_CONSECUTIVE_ERRORS = 10;
846
+ const ERROR_RESET_INTERVAL = 60 * 1e3;
847
+ function shouldFailOpen() {
848
+ const now = Date.now();
849
+ if (now - sessionCheckErrors.lastReset > ERROR_RESET_INTERVAL) {
850
+ sessionCheckErrors.count = 0;
851
+ sessionCheckErrors.lastReset = now;
852
+ }
853
+ sessionCheckErrors.count++;
854
+ if (sessionCheckErrors.count > MAX_CONSECUTIVE_ERRORS) {
855
+ return false;
856
+ }
857
+ return true;
858
+ }
859
+ function resetErrorCounter() {
860
+ sessionCheckErrors.count = 0;
861
+ }
862
+ function isSessionExpired(session2, maxAgeDays = 30) {
863
+ if (!session2.loginTime) return false;
864
+ const loginTime = new Date(session2.loginTime).getTime();
865
+ const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1e3;
866
+ return Date.now() - loginTime > maxAgeMs;
867
+ }
791
868
  async function registerSessionAwareAuthStrategy(strapi2, log) {
792
869
  try {
793
870
  const usersPermissionsPlugin = strapi2.plugin("users-permissions");
@@ -809,15 +886,15 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
809
886
  }
810
887
  const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
811
888
  const strictMode = config2.strictSessionEnforcement === true;
889
+ const maxSessionAgeDays = config2.maxSessionAgeDays || 30;
812
890
  try {
813
891
  const tokenHashValue = hashToken$2(token);
814
- let userDocId = null;
815
892
  const user = await strapi2.entityService.findOne(
816
893
  "plugin::users-permissions.user",
817
894
  decoded.id,
818
895
  { fields: ["documentId"] }
819
896
  );
820
- userDocId = user?.documentId;
897
+ const userDocId = user?.documentId;
821
898
  if (!userDocId) {
822
899
  strapi2.log.debug("[magic-sessionmanager] [JWT] No documentId found, allowing through");
823
900
  return decoded;
@@ -827,9 +904,19 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
827
904
  user: { documentId: userDocId },
828
905
  tokenHash: tokenHashValue
829
906
  },
830
- fields: ["documentId", "isActive", "terminatedManually", "lastActive"]
907
+ fields: ["documentId", "isActive", "terminatedManually", "lastActive", "loginTime"]
831
908
  });
832
909
  if (thisSession) {
910
+ if (isSessionExpired(thisSession, maxSessionAgeDays)) {
911
+ strapi2.log.info(
912
+ `[magic-sessionmanager] [JWT-EXPIRED] Session exceeded max age of ${maxSessionAgeDays} days (user: ${userDocId.substring(0, 8)}...)`
913
+ );
914
+ await strapi2.documents(SESSION_UID$3).update({
915
+ documentId: thisSession.documentId,
916
+ data: { isActive: false, terminatedManually: true, logoutTime: /* @__PURE__ */ new Date() }
917
+ });
918
+ return null;
919
+ }
833
920
  if (thisSession.terminatedManually === true) {
834
921
  strapi2.log.info(
835
922
  `[magic-sessionmanager] [JWT-BLOCKED] Session was manually terminated (user: ${userDocId.substring(0, 8)}...)`
@@ -837,38 +924,32 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
837
924
  return null;
838
925
  }
839
926
  if (thisSession.isActive) {
927
+ resetErrorCounter();
840
928
  return decoded;
841
929
  }
842
930
  await strapi2.documents(SESSION_UID$3).update({
843
931
  documentId: thisSession.documentId,
844
- data: {
845
- isActive: true,
846
- lastActive: /* @__PURE__ */ new Date()
847
- }
932
+ data: { isActive: true, lastActive: /* @__PURE__ */ new Date() }
848
933
  });
849
934
  strapi2.log.info(
850
935
  `[magic-sessionmanager] [JWT-REACTIVATED] Session reactivated for user ${userDocId.substring(0, 8)}...`
851
936
  );
937
+ resetErrorCounter();
852
938
  return decoded;
853
939
  }
854
940
  const anyActiveSessions = await strapi2.documents(SESSION_UID$3).findMany({
855
- filters: {
856
- user: { documentId: userDocId },
857
- isActive: true
858
- },
941
+ filters: { user: { documentId: userDocId }, isActive: true },
859
942
  limit: 1
860
943
  });
861
944
  if (anyActiveSessions && anyActiveSessions.length > 0) {
862
945
  strapi2.log.debug(
863
946
  `[magic-sessionmanager] [JWT] No session for token but user has other active sessions (allowing)`
864
947
  );
948
+ resetErrorCounter();
865
949
  return decoded;
866
950
  }
867
951
  const terminatedSessions = await strapi2.documents(SESSION_UID$3).findMany({
868
- filters: {
869
- user: { documentId: userDocId },
870
- terminatedManually: true
871
- },
952
+ filters: { user: { documentId: userDocId }, terminatedManually: true },
872
953
  limit: 1
873
954
  });
874
955
  if (terminatedSessions && terminatedSessions.length > 0) {
@@ -886,10 +967,15 @@ async function registerSessionAwareAuthStrategy(strapi2, log) {
886
967
  strapi2.log.warn(
887
968
  `[magic-sessionmanager] [JWT-WARN] No session for user ${userDocId.substring(0, 8)}... (allowing)`
888
969
  );
970
+ resetErrorCounter();
889
971
  return decoded;
890
972
  } catch (err) {
891
- strapi2.log.warn("[magic-sessionmanager] [JWT] Session check error (allowing):", err.message);
892
- return decoded;
973
+ if (shouldFailOpen()) {
974
+ strapi2.log.warn("[magic-sessionmanager] [JWT] Session check error (allowing):", err.message);
975
+ return decoded;
976
+ }
977
+ strapi2.log.error("[magic-sessionmanager] [JWT] Too many consecutive errors, blocking request:", err.message);
978
+ return null;
893
979
  }
894
980
  };
895
981
  strapi2.log.info("[magic-sessionmanager] [AUTH] [SUCCESS] JWT verify wrapped with session validation");
@@ -1354,7 +1440,7 @@ function extractVersion(userAgent, regex) {
1354
1440
  var userAgentParser = {
1355
1441
  parseUserAgent: parseUserAgent$2
1356
1442
  };
1357
- const { decryptToken: decryptToken$1, hashToken: hashToken$1 } = encryption;
1443
+ const { hashToken: hashToken$1 } = encryption;
1358
1444
  const { parseUserAgent: parseUserAgent$1 } = userAgentParser;
1359
1445
  const SESSION_UID$2 = "plugin::magic-sessionmanager.session";
1360
1446
  const USER_UID$1 = "plugin::users-permissions.user";
@@ -1414,7 +1500,8 @@ var session$3 = {
1414
1500
  }
1415
1501
  const allSessions = await strapi.documents(SESSION_UID$2).findMany({
1416
1502
  filters: { user: { documentId: userId } },
1417
- sort: { loginTime: "desc" }
1503
+ sort: { loginTime: "desc" },
1504
+ limit: 200
1418
1505
  });
1419
1506
  const config2 = strapi.config.get("plugin::magic-sessionmanager") || {};
1420
1507
  const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
@@ -1452,7 +1539,7 @@ var session$3 = {
1452
1539
  strapi.documents(SESSION_UID$2).update({
1453
1540
  documentId: session2.documentId,
1454
1541
  data: {
1455
- geoLocation: JSON.stringify(geoLocation),
1542
+ geoLocation,
1456
1543
  securityScore: geoData.securityScore || null
1457
1544
  }
1458
1545
  }).catch(() => {
@@ -1514,9 +1601,15 @@ var session$3 = {
1514
1601
  const { userId } = ctx.params;
1515
1602
  const isAdminRequest = ctx.state.userAbility || ctx.state.admin;
1516
1603
  const requestingUserDocId = ctx.state.user?.documentId;
1517
- if (!isAdminRequest && requestingUserDocId && String(requestingUserDocId) !== String(userId)) {
1518
- strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserDocId} tried to access sessions of user ${userId}`);
1519
- return ctx.forbidden("You can only access your own sessions");
1604
+ if (!isAdminRequest) {
1605
+ if (!requestingUserDocId) {
1606
+ strapi.log.warn(`[magic-sessionmanager] Security: Request without documentId tried to access sessions of user ${userId}`);
1607
+ return ctx.forbidden("Cannot verify user identity");
1608
+ }
1609
+ if (String(requestingUserDocId) !== String(userId)) {
1610
+ strapi.log.warn(`[magic-sessionmanager] Security: User ${requestingUserDocId} tried to access sessions of user ${userId}`);
1611
+ return ctx.forbidden("You can only access your own sessions");
1612
+ }
1520
1613
  }
1521
1614
  const sessionService = strapi.plugin("magic-sessionmanager").service("session");
1522
1615
  const sessions = await sessionService.getUserSessions(userId);
@@ -1640,7 +1733,7 @@ var session$3 = {
1640
1733
  strapi.documents(SESSION_UID$2).update({
1641
1734
  documentId: currentSession.documentId,
1642
1735
  data: {
1643
- geoLocation: JSON.stringify(geoLocation),
1736
+ geoLocation,
1644
1737
  securityScore: geoData.securityScore || null
1645
1738
  }
1646
1739
  }).catch(() => {
@@ -1722,22 +1815,6 @@ var session$3 = {
1722
1815
  ctx.throw(500, "Error terminating session");
1723
1816
  }
1724
1817
  },
1725
- /**
1726
- * Terminate specific session
1727
- * DELETE /magic-sessionmanager/sessions/:sessionId
1728
- */
1729
- async terminateSession(ctx) {
1730
- try {
1731
- const { sessionId } = ctx.params;
1732
- const sessionService = strapi.plugin("magic-sessionmanager").service("session");
1733
- await sessionService.terminateSession({ sessionId });
1734
- ctx.body = {
1735
- message: `Session ${sessionId} terminated`
1736
- };
1737
- } catch (err) {
1738
- ctx.throw(500, "Error terminating session");
1739
- }
1740
- },
1741
1818
  /**
1742
1819
  * Simulate session timeout for testing (Admin action)
1743
1820
  * POST /magic-sessionmanager/sessions/:sessionId/simulate-timeout
@@ -1746,6 +1823,10 @@ var session$3 = {
1746
1823
  */
1747
1824
  async simulateTimeout(ctx) {
1748
1825
  try {
1826
+ const nodeEnv = process.env.NODE_ENV || "development";
1827
+ if (nodeEnv === "production") {
1828
+ return ctx.forbidden("simulate-timeout is disabled in production");
1829
+ }
1749
1830
  const { sessionId } = ctx.params;
1750
1831
  const session2 = await strapi.documents(SESSION_UID$2).findOne({
1751
1832
  documentId: sessionId
@@ -1818,9 +1899,15 @@ var session$3 = {
1818
1899
  if (!ipAddress) {
1819
1900
  return ctx.badRequest("IP address is required");
1820
1901
  }
1821
- const IPV4_REGEX = /^(\d{1,3}\.){3}\d{1,3}$/;
1822
- const IPV6_REGEX = /^[0-9a-fA-F:]+$/;
1823
- if (!IPV4_REGEX.test(ipAddress) && !IPV6_REGEX.test(ipAddress)) {
1902
+ const IPV4_REGEX = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
1903
+ const ipv4Match = ipAddress.match(IPV4_REGEX);
1904
+ const isValidIpv4 = ipv4Match && ipv4Match.slice(1).every((octet) => {
1905
+ const n = parseInt(octet, 10);
1906
+ return n >= 0 && n <= 255;
1907
+ });
1908
+ const IPV6_REGEX = /^[0-9a-fA-F:]{3,45}$/;
1909
+ const isValidIpv6 = !isValidIpv4 && IPV6_REGEX.test(ipAddress) && (ipAddress.match(/:/g) || []).length >= 2;
1910
+ if (!isValidIpv4 && !isValidIpv6) {
1824
1911
  return ctx.badRequest("Invalid IP address format");
1825
1912
  }
1826
1913
  const licenseGuard2 = strapi.plugin("magic-sessionmanager").service("license-guard");
@@ -1982,12 +2069,11 @@ var license$1 = ({ strapi: strapi2 }) => ({
1982
2069
  message: "No license found. Running in demo mode."
1983
2070
  });
1984
2071
  }
1985
- strapi2.log.info(`[magic-sessionmanager/license-controller] Checking stored license: ${licenseKey}`);
2072
+ strapi2.log.info(`[magic-sessionmanager/license-controller] Checking stored license: ${licenseKey.substring(0, 8)}...`);
1986
2073
  const verification = await licenseGuard2.verifyLicense(licenseKey);
1987
2074
  const license2 = await licenseGuard2.getLicenseByKey(licenseKey);
1988
2075
  strapi2.log.info("[magic-sessionmanager/license-controller] License data from MagicAPI:", {
1989
- licenseKey: license2?.licenseKey,
1990
- email: license2?.email,
2076
+ licenseKey: license2?.licenseKey ? `${license2.licenseKey.substring(0, 8)}...` : "N/A",
1991
2077
  featurePremium: license2?.featurePremium,
1992
2078
  isActive: license2?.isActive,
1993
2079
  pluginName: license2?.pluginName
@@ -2131,6 +2217,55 @@ var license$1 = ({ strapi: strapi2 }) => ({
2131
2217
  }
2132
2218
  }
2133
2219
  });
2220
+ const ALLOWED_WEBHOOK_DOMAINS = {
2221
+ discord: ["discord.com", "discordapp.com"],
2222
+ slack: ["hooks.slack.com"]
2223
+ };
2224
+ function sanitizeWebhookUrl(url, type2) {
2225
+ if (!url || typeof url !== "string") return "";
2226
+ const trimmed = url.trim();
2227
+ if (!trimmed) return "";
2228
+ try {
2229
+ const parsed = new URL(trimmed);
2230
+ if (parsed.protocol !== "https:") return "";
2231
+ const allowedDomains = ALLOWED_WEBHOOK_DOMAINS[type2] || [];
2232
+ const isAllowed = allowedDomains.some(
2233
+ (domain) => parsed.hostname === domain || parsed.hostname.endsWith(`.${domain}`)
2234
+ );
2235
+ if (!isAllowed) return "";
2236
+ return trimmed;
2237
+ } catch {
2238
+ return "";
2239
+ }
2240
+ }
2241
+ function sanitizeCountryList(list) {
2242
+ if (!Array.isArray(list)) return [];
2243
+ return list.filter((code) => typeof code === "string" && /^[A-Z]{2}$/.test(code.trim().toUpperCase())).map((code) => code.trim().toUpperCase());
2244
+ }
2245
+ function sanitizeEmailTemplates(templates) {
2246
+ const defaults = {
2247
+ suspiciousLogin: { subject: "", html: "", text: "" },
2248
+ newLocation: { subject: "", html: "", text: "" },
2249
+ vpnProxy: { subject: "", html: "", text: "" }
2250
+ };
2251
+ if (!templates || typeof templates !== "object") return defaults;
2252
+ const dangerousTags = /<\s*\/?\s*(script|iframe|object|embed|form|input|button|link|meta|base)\b[^>]*>/gi;
2253
+ const dangerousAttrs = /\s(on\w+|javascript\s*:)[^=]*=/gi;
2254
+ const result = {};
2255
+ for (const [key, defaultVal] of Object.entries(defaults)) {
2256
+ const tpl = templates[key];
2257
+ if (!tpl || typeof tpl !== "object") {
2258
+ result[key] = defaultVal;
2259
+ continue;
2260
+ }
2261
+ result[key] = {
2262
+ subject: typeof tpl.subject === "string" ? tpl.subject.substring(0, 200) : "",
2263
+ html: typeof tpl.html === "string" ? tpl.html.replace(dangerousTags, "").replace(dangerousAttrs, " ").substring(0, 1e4) : "",
2264
+ text: typeof tpl.text === "string" ? tpl.text.substring(0, 5e3) : ""
2265
+ };
2266
+ }
2267
+ return result;
2268
+ }
2134
2269
  var settings$1 = {
2135
2270
  /**
2136
2271
  * Get plugin settings
@@ -2192,29 +2327,26 @@ var settings$1 = {
2192
2327
  name: "magic-sessionmanager"
2193
2328
  });
2194
2329
  const sanitizedSettings = {
2195
- inactivityTimeout: parseInt(body.inactivityTimeout) || 15,
2196
- cleanupInterval: parseInt(body.cleanupInterval) || 30,
2197
- lastSeenRateLimit: parseInt(body.lastSeenRateLimit) || 30,
2198
- retentionDays: parseInt(body.retentionDays) || 90,
2330
+ inactivityTimeout: Math.max(1, Math.min(parseInt(body.inactivityTimeout) || 15, 1440)),
2331
+ cleanupInterval: Math.max(5, Math.min(parseInt(body.cleanupInterval) || 30, 1440)),
2332
+ lastSeenRateLimit: Math.max(5, Math.min(parseInt(body.lastSeenRateLimit) || 30, 300)),
2333
+ retentionDays: Math.max(1, Math.min(parseInt(body.retentionDays) || 90, 365)),
2334
+ maxSessionAgeDays: Math.max(1, Math.min(parseInt(body.maxSessionAgeDays) || 30, 365)),
2199
2335
  enableGeolocation: !!body.enableGeolocation,
2200
2336
  enableSecurityScoring: !!body.enableSecurityScoring,
2201
2337
  blockSuspiciousSessions: !!body.blockSuspiciousSessions,
2202
- maxFailedLogins: parseInt(body.maxFailedLogins) || 5,
2338
+ maxFailedLogins: Math.max(1, Math.min(parseInt(body.maxFailedLogins) || 5, 100)),
2203
2339
  enableEmailAlerts: !!body.enableEmailAlerts,
2204
2340
  alertOnSuspiciousLogin: !!body.alertOnSuspiciousLogin,
2205
2341
  alertOnNewLocation: !!body.alertOnNewLocation,
2206
2342
  alertOnVpnProxy: !!body.alertOnVpnProxy,
2207
2343
  enableWebhooks: !!body.enableWebhooks,
2208
- discordWebhookUrl: String(body.discordWebhookUrl || ""),
2209
- slackWebhookUrl: String(body.slackWebhookUrl || ""),
2344
+ discordWebhookUrl: sanitizeWebhookUrl(body.discordWebhookUrl, "discord"),
2345
+ slackWebhookUrl: sanitizeWebhookUrl(body.slackWebhookUrl, "slack"),
2210
2346
  enableGeofencing: !!body.enableGeofencing,
2211
- allowedCountries: Array.isArray(body.allowedCountries) ? body.allowedCountries : [],
2212
- blockedCountries: Array.isArray(body.blockedCountries) ? body.blockedCountries : [],
2213
- emailTemplates: body.emailTemplates || {
2214
- suspiciousLogin: { subject: "", html: "", text: "" },
2215
- newLocation: { subject: "", html: "", text: "" },
2216
- vpnProxy: { subject: "", html: "", text: "" }
2217
- }
2347
+ allowedCountries: sanitizeCountryList(body.allowedCountries),
2348
+ blockedCountries: sanitizeCountryList(body.blockedCountries),
2349
+ emailTemplates: sanitizeEmailTemplates(body.emailTemplates)
2218
2350
  };
2219
2351
  await pluginStore.set({
2220
2352
  key: "settings",
@@ -2285,15 +2417,15 @@ var session$1 = ({ strapi: strapi2 }) => {
2285
2417
  deviceType: parsedUA.deviceType,
2286
2418
  browserName: parsedUA.browserVersion ? `${parsedUA.browserName} ${parsedUA.browserVersion}` : parsedUA.browserName,
2287
2419
  osName: parsedUA.osVersion ? `${parsedUA.osName} ${parsedUA.osVersion}` : parsedUA.osName,
2288
- // Geolocation data (if available from Premium features)
2289
- geoLocation: geoData ? JSON.stringify({
2420
+ // Geolocation data (stored as JSON object, schema type: json)
2421
+ geoLocation: geoData ? {
2290
2422
  country: geoData.country,
2291
2423
  country_code: geoData.country_code,
2292
2424
  country_flag: geoData.country_flag,
2293
2425
  city: geoData.city,
2294
2426
  region: geoData.region,
2295
2427
  timezone: geoData.timezone
2296
- }) : null,
2428
+ } : null,
2297
2429
  securityScore: geoData?.securityScore || null
2298
2430
  }
2299
2431
  });
@@ -2314,6 +2446,14 @@ var session$1 = ({ strapi: strapi2 }) => {
2314
2446
  try {
2315
2447
  const now = /* @__PURE__ */ new Date();
2316
2448
  if (sessionId) {
2449
+ const existing = await strapi2.documents(SESSION_UID$1).findOne({
2450
+ documentId: sessionId,
2451
+ fields: ["documentId"]
2452
+ });
2453
+ if (!existing) {
2454
+ log.warn(`Session ${sessionId} not found for termination`);
2455
+ return;
2456
+ }
2317
2457
  await strapi2.documents(SESSION_UID$1).update({
2318
2458
  documentId: sessionId,
2319
2459
  data: {
@@ -2405,7 +2545,7 @@ var session$1 = ({ strapi: strapi2 }) => {
2405
2545
  strapi2.documents(SESSION_UID$1).update({
2406
2546
  documentId: session2.documentId,
2407
2547
  data: {
2408
- geoLocation: JSON.stringify(geoLocation),
2548
+ geoLocation,
2409
2549
  securityScore: geoData.securityScore || null
2410
2550
  }
2411
2551
  }).catch(() => {
@@ -2455,7 +2595,8 @@ var session$1 = ({ strapi: strapi2 }) => {
2455
2595
  const sessions = await strapi2.documents(SESSION_UID$1).findMany({
2456
2596
  filters: { isActive: true },
2457
2597
  populate: { user: { fields: ["documentId", "email", "username"] } },
2458
- sort: { loginTime: "desc" }
2598
+ sort: { loginTime: "desc" },
2599
+ limit: 1e3
2459
2600
  });
2460
2601
  const config2 = strapi2.config.get("plugin::magic-sessionmanager") || {};
2461
2602
  const inactivityTimeout = config2.inactivityTimeout || 15 * 60 * 1e3;
@@ -2492,7 +2633,7 @@ var session$1 = ({ strapi: strapi2 }) => {
2492
2633
  strapi2.documents(SESSION_UID$1).update({
2493
2634
  documentId: session2.documentId,
2494
2635
  data: {
2495
- geoLocation: JSON.stringify(geoLocation),
2636
+ geoLocation,
2496
2637
  securityScore: geoData.securityScore || null
2497
2638
  }
2498
2639
  }).catch(() => {
@@ -2585,7 +2726,7 @@ var session$1 = ({ strapi: strapi2 }) => {
2585
2726
  strapi2.documents(SESSION_UID$1).update({
2586
2727
  documentId: session2.documentId,
2587
2728
  data: {
2588
- geoLocation: JSON.stringify(geoLocation),
2729
+ geoLocation,
2589
2730
  securityScore: geoData.securityScore || null
2590
2731
  }
2591
2732
  }).catch(() => {
@@ -2722,20 +2863,36 @@ var session$1 = ({ strapi: strapi2 }) => {
2722
2863
  }
2723
2864
  },
2724
2865
  /**
2725
- * Delete all inactive sessions from database
2866
+ * Delete all inactive sessions from database in batches
2726
2867
  * WARNING: This permanently deletes records!
2727
2868
  * @returns {Promise<number>} Number of deleted sessions
2728
2869
  */
2729
2870
  async deleteInactiveSessions() {
2730
2871
  try {
2731
2872
  log.info("[DELETE] Deleting all inactive sessions...");
2732
- const inactiveSessions = await strapi2.documents(SESSION_UID$1).findMany({
2733
- filters: { isActive: false }
2734
- });
2735
2873
  let deletedCount = 0;
2736
- for (const session2 of inactiveSessions) {
2737
- await strapi2.documents(SESSION_UID$1).delete({ documentId: session2.documentId });
2738
- deletedCount++;
2874
+ const BATCH_SIZE = 100;
2875
+ let hasMore = true;
2876
+ while (hasMore) {
2877
+ const batch = await strapi2.documents(SESSION_UID$1).findMany({
2878
+ filters: { isActive: false },
2879
+ fields: ["documentId"],
2880
+ limit: BATCH_SIZE
2881
+ });
2882
+ if (!batch || batch.length === 0) {
2883
+ hasMore = false;
2884
+ break;
2885
+ }
2886
+ const deleteResults = await Promise.allSettled(
2887
+ batch.map(
2888
+ (session2) => strapi2.documents(SESSION_UID$1).delete({ documentId: session2.documentId })
2889
+ )
2890
+ );
2891
+ const batchDeleted = deleteResults.filter((r) => r.status === "fulfilled").length;
2892
+ deletedCount += batchDeleted;
2893
+ if (batch.length < BATCH_SIZE) {
2894
+ hasMore = false;
2895
+ }
2739
2896
  }
2740
2897
  log.info(`[SUCCESS] Deleted ${deletedCount} inactive sessions`);
2741
2898
  return deletedCount;
@@ -2746,7 +2903,7 @@ var session$1 = ({ strapi: strapi2 }) => {
2746
2903
  }
2747
2904
  };
2748
2905
  };
2749
- const version$1 = "4.4.4";
2906
+ const version$1 = "4.4.5";
2750
2907
  const require$$2 = {
2751
2908
  version: version$1
2752
2909
  };
@@ -3020,7 +3177,7 @@ var geolocation$1 = ({ strapi: strapi2 }) => ({
3020
3177
  */
3021
3178
  async getIpInfo(ipAddress) {
3022
3179
  try {
3023
- if (!ipAddress || ipAddress === "127.0.0.1" || ipAddress === "::1" || ipAddress.startsWith("192.168.") || ipAddress.startsWith("10.")) {
3180
+ if (!ipAddress || this.isPrivateIp(ipAddress)) {
3024
3181
  return {
3025
3182
  ip: ipAddress,
3026
3183
  country: "Local Network",
@@ -3123,6 +3280,26 @@ var geolocation$1 = ({ strapi: strapi2 }) => ({
3123
3280
  const codePoints = countryCode.toUpperCase().split("").map((char) => 127397 + char.charCodeAt());
3124
3281
  return String.fromCodePoint(...codePoints);
3125
3282
  },
3283
+ /**
3284
+ * Checks if an IP address is private/local (RFC 1918, RFC 4193, loopback, link-local)
3285
+ * @param {string} ip - IP address to check
3286
+ * @returns {boolean} True if IP is private/local
3287
+ */
3288
+ isPrivateIp(ip) {
3289
+ if (!ip || ip === "unknown") return true;
3290
+ if (ip === "127.0.0.1" || ip === "localhost" || ip === "::1") return true;
3291
+ if (ip.startsWith("192.168.")) return true;
3292
+ if (ip.startsWith("10.")) return true;
3293
+ if (ip.startsWith("172.")) {
3294
+ const second = parseInt(ip.split(".")[1], 10);
3295
+ if (second >= 16 && second <= 31) return true;
3296
+ }
3297
+ if (ip.startsWith("169.254.")) return true;
3298
+ if (ip.startsWith("fc00:") || ip.startsWith("fd00:")) return true;
3299
+ if (ip.startsWith("fe80:")) return true;
3300
+ if (ip === "::1") return true;
3301
+ return false;
3302
+ },
3126
3303
  /**
3127
3304
  * Fallback data when API fails
3128
3305
  */
@@ -3258,25 +3435,36 @@ VPN: {{reason.isVpn}}, Proxy: {{reason.isProxy}}`
3258
3435
  };
3259
3436
  },
3260
3437
  /**
3261
- * Replace template variables with actual values
3438
+ * Escapes HTML special characters to prevent XSS in email templates
3439
+ * @param {string} str - String to escape
3440
+ * @returns {string} HTML-safe string
3441
+ */
3442
+ escapeHtml(str2) {
3443
+ if (!str2 || typeof str2 !== "string") return str2 || "";
3444
+ return str2.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3445
+ },
3446
+ /**
3447
+ * Replace template variables with HTML-escaped actual values
3448
+ * SECURITY: All dynamic values are HTML-escaped to prevent XSS via email
3262
3449
  */
3263
3450
  replaceVariables(template2, data) {
3264
3451
  let result = template2;
3265
- result = result.replace(/\{\{user\.email\}\}/g, data.user?.email || "N/A");
3266
- result = result.replace(/\{\{user\.username\}\}/g, data.user?.username || "N/A");
3452
+ const esc2 = this.escapeHtml.bind(this);
3453
+ result = result.replace(/\{\{user\.email\}\}/g, esc2(data.user?.email || "N/A"));
3454
+ result = result.replace(/\{\{user\.username\}\}/g, esc2(data.user?.username || "N/A"));
3267
3455
  result = result.replace(
3268
3456
  /\{\{session\.loginTime\}\}/g,
3269
- data.session?.loginTime ? new Date(data.session.loginTime).toLocaleString() : "N/A"
3457
+ esc2(data.session?.loginTime ? new Date(data.session.loginTime).toLocaleString() : "N/A")
3270
3458
  );
3271
- result = result.replace(/\{\{session\.ipAddress\}\}/g, data.session?.ipAddress || "N/A");
3272
- result = result.replace(/\{\{session\.userAgent\}\}/g, data.session?.userAgent || "N/A");
3273
- result = result.replace(/\{\{geo\.city\}\}/g, data.geoData?.city || "Unknown");
3274
- result = result.replace(/\{\{geo\.country\}\}/g, data.geoData?.country || "Unknown");
3275
- result = result.replace(/\{\{geo\.timezone\}\}/g, data.geoData?.timezone || "Unknown");
3459
+ result = result.replace(/\{\{session\.ipAddress\}\}/g, esc2(data.session?.ipAddress || "N/A"));
3460
+ result = result.replace(/\{\{session\.userAgent\}\}/g, esc2(data.session?.userAgent || "N/A"));
3461
+ result = result.replace(/\{\{geo\.city\}\}/g, esc2(data.geoData?.city || "Unknown"));
3462
+ result = result.replace(/\{\{geo\.country\}\}/g, esc2(data.geoData?.country || "Unknown"));
3463
+ result = result.replace(/\{\{geo\.timezone\}\}/g, esc2(data.geoData?.timezone || "Unknown"));
3276
3464
  result = result.replace(/\{\{reason\.isVpn\}\}/g, data.reason?.isVpn ? "Yes" : "No");
3277
3465
  result = result.replace(/\{\{reason\.isProxy\}\}/g, data.reason?.isProxy ? "Yes" : "No");
3278
3466
  result = result.replace(/\{\{reason\.isThreat\}\}/g, data.reason?.isThreat ? "Yes" : "No");
3279
- result = result.replace(/\{\{reason\.securityScore\}\}/g, data.reason?.securityScore || "0");
3467
+ result = result.replace(/\{\{reason\.securityScore\}\}/g, esc2(String(data.reason?.securityScore || "0")));
3280
3468
  return result;
3281
3469
  },
3282
3470
  /**
@@ -39052,7 +39240,7 @@ const dist = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty
39052
39240
  const require$$0 = /* @__PURE__ */ getAugmentedNamespace(dist);
39053
39241
  const SESSION_UID = "plugin::magic-sessionmanager.session";
39054
39242
  const { errors } = require$$0;
39055
- var sessionRequired$1 = async (policyContext, config2, { strapi: strapi2 }) => {
39243
+ var sessionRequired$1 = async (policyContext, _policyConfig, { strapi: strapi2 }) => {
39056
39244
  if (!policyContext.state.user) {
39057
39245
  return true;
39058
39246
  }
@@ -39070,8 +39258,8 @@ var sessionRequired$1 = async (policyContext, config2, { strapi: strapi2 }) => {
39070
39258
  if (!userDocId) {
39071
39259
  return true;
39072
39260
  }
39073
- const config3 = strapi2.config.get("plugin::magic-sessionmanager") || {};
39074
- const strictMode = config3.strictSessionEnforcement === true;
39261
+ const pluginConfig = strapi2.config.get("plugin::magic-sessionmanager") || {};
39262
+ const strictMode = pluginConfig.strictSessionEnforcement === true;
39075
39263
  const activeSessions = await strapi2.documents(SESSION_UID).findMany({
39076
39264
  filters: {
39077
39265
  user: { documentId: userDocId },