strapi-plugin-magic-sessionmanager 4.5.0 → 4.5.2

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.
@@ -9584,6 +9584,9 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
9584
9584
  }
9585
9585
  }, 3e3);
9586
9586
  const sessionService = strapi2.plugin("magic-sessionmanager").service("session");
9587
+ if (!strapi2.sessionManagerIntervals) {
9588
+ strapi2.sessionManagerIntervals = {};
9589
+ }
9587
9590
  log.info("Running initial session cleanup...");
9588
9591
  try {
9589
9592
  const settings2 = await getPluginSettings$3(strapi2);
@@ -9593,23 +9596,50 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
9593
9596
  } catch (cleanupErr) {
9594
9597
  log.warn("Initial cleanup failed:", cleanupErr.message);
9595
9598
  }
9596
- const cleanupInterval = 30 * 60 * 1e3;
9597
- const cleanupIntervalHandle = setInterval(async () => {
9599
+ const scheduleIdleCleanup = async () => {
9600
+ let intervalMs = 30 * 60 * 1e3;
9601
+ let useDbDirect = false;
9598
9602
  try {
9599
- const service = strapi2.plugin("magic-sessionmanager").service("session");
9600
- const settings2 = await getPluginSettings$3(strapi2).catch(() => ({}));
9601
- await service.cleanupInactiveSessions({
9602
- useDbDirect: settings2.cleanupUseDbDirect === true
9603
- });
9604
- } catch (err) {
9605
- log.error("Periodic cleanup error:", err);
9603
+ const settings2 = await getPluginSettings$3(strapi2);
9604
+ intervalMs = Math.max(5 * 60 * 1e3, settings2.cleanupInterval || intervalMs);
9605
+ useDbDirect = settings2.cleanupUseDbDirect === true;
9606
+ } catch {
9606
9607
  }
9607
- }, cleanupInterval);
9608
- log.info("[TIME] Periodic cleanup scheduled (every 30 minutes)");
9609
- if (!strapi2.sessionManagerIntervals) {
9610
- strapi2.sessionManagerIntervals = {};
9611
- }
9612
- strapi2.sessionManagerIntervals.cleanup = cleanupIntervalHandle;
9608
+ const handle = setTimeout(async () => {
9609
+ try {
9610
+ const service = strapi2.plugin("magic-sessionmanager").service("session");
9611
+ await service.cleanupInactiveSessions({ useDbDirect });
9612
+ } catch (err) {
9613
+ log.error("Periodic cleanup error:", err);
9614
+ }
9615
+ scheduleIdleCleanup();
9616
+ }, intervalMs);
9617
+ strapi2.sessionManagerIntervals.cleanupTimeout = handle;
9618
+ };
9619
+ await scheduleIdleCleanup();
9620
+ const RETENTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
9621
+ const scheduleRetention = async () => {
9622
+ let useDbDirect = false;
9623
+ try {
9624
+ const settings2 = await getPluginSettings$3(strapi2);
9625
+ useDbDirect = settings2.cleanupUseDbDirect === true;
9626
+ } catch {
9627
+ }
9628
+ const handle = setTimeout(async () => {
9629
+ try {
9630
+ const service = strapi2.plugin("magic-sessionmanager").service("session");
9631
+ await service.deleteOldSessions({ useDbDirect });
9632
+ } catch (err) {
9633
+ log.error("Retention cleanup error:", err);
9634
+ }
9635
+ scheduleRetention();
9636
+ }, RETENTION_INTERVAL_MS);
9637
+ strapi2.sessionManagerIntervals.retentionTimeout = handle;
9638
+ };
9639
+ strapi2.sessionManagerIntervals.retentionStartup = setTimeout(() => {
9640
+ scheduleRetention();
9641
+ }, 5 * 60 * 1e3);
9642
+ log.info("[TIME] Dynamic cleanup + retention scheduled");
9613
9643
  mountPreLoginGeoGuard({ strapi: strapi2, log });
9614
9644
  mountFailedLoginLockout({ strapi: strapi2, log });
9615
9645
  mountLogoutRoute({ strapi: strapi2, log, sessionService });
@@ -10341,9 +10371,10 @@ var destroy$1 = async ({ strapi: strapi2 }) => {
10341
10371
  if (!handle) continue;
10342
10372
  try {
10343
10373
  clearInterval(handle);
10344
- log.info(`[STOP] ${name} interval stopped`);
10374
+ clearTimeout(handle);
10375
+ log.info(`[STOP] ${name} timer stopped`);
10345
10376
  } catch (err) {
10346
- log.warn(`Failed to stop ${name} interval:`, err.message);
10377
+ log.warn(`Failed to stop ${name} timer:`, err.message);
10347
10378
  }
10348
10379
  }
10349
10380
  strapi2.sessionManagerIntervals = {};
@@ -11732,15 +11763,27 @@ var settings$1 = {
11732
11763
  let settings2 = await pluginStore.get({ key: "settings" });
11733
11764
  if (!settings2) {
11734
11765
  settings2 = {
11766
+ // Timing & cleanup
11735
11767
  inactivityTimeout: 15,
11736
11768
  cleanupInterval: 30,
11737
11769
  lastSeenRateLimit: 30,
11738
11770
  retentionDays: 90,
11739
11771
  maxSessionAgeDays: 30,
11772
+ // Grace window (ms) during which a freshly-issued JWT is accepted
11773
+ // without a matching session row — see JWT-verify wrapper in bootstrap.
11774
+ sessionCreationGraceMs: 5e3,
11775
+ // Opt-in: use a single SQL UPDATE for idle-session cleanup
11776
+ // (bypasses Document Service lifecycle, required for very large installs).
11777
+ cleanupUseDbDirect: false,
11778
+ // Geo / security
11740
11779
  enableGeolocation: true,
11741
11780
  enableSecurityScoring: true,
11742
11781
  blockSuspiciousSessions: false,
11743
11782
  maxFailedLogins: 5,
11783
+ enableGeofencing: false,
11784
+ allowedCountries: [],
11785
+ blockedCountries: [],
11786
+ // Notifications
11744
11787
  enableEmailAlerts: false,
11745
11788
  alertOnSuspiciousLogin: true,
11746
11789
  alertOnNewLocation: true,
@@ -11748,11 +11791,10 @@ var settings$1 = {
11748
11791
  enableWebhooks: false,
11749
11792
  discordWebhookUrl: "",
11750
11793
  slackWebhookUrl: "",
11751
- enableGeofencing: false,
11752
- allowedCountries: [],
11753
- blockedCountries: [],
11794
+ // Session policy
11754
11795
  strictSessionEnforcement: false,
11755
11796
  trustedProxies: false,
11797
+ // Email templates
11756
11798
  emailTemplates: {
11757
11799
  suspiciousLogin: { subject: "", html: "", text: "" },
11758
11800
  newLocation: { subject: "", html: "", text: "" },
@@ -11785,16 +11827,26 @@ var settings$1 = {
11785
11827
  type: "plugin",
11786
11828
  name: "magic-sessionmanager"
11787
11829
  });
11830
+ const graceRaw = body.sessionCreationGraceMs;
11831
+ const grace = Number.isFinite(parseInt(graceRaw)) ? Math.max(0, Math.min(parseInt(graceRaw), 3e4)) : 5e3;
11788
11832
  const sanitizedSettings = {
11833
+ // Timing & cleanup
11789
11834
  inactivityTimeout: Math.max(1, Math.min(parseInt(body.inactivityTimeout) || 15, 1440)),
11790
11835
  cleanupInterval: Math.max(5, Math.min(parseInt(body.cleanupInterval) || 30, 1440)),
11791
11836
  lastSeenRateLimit: Math.max(5, Math.min(parseInt(body.lastSeenRateLimit) || 30, 300)),
11792
11837
  retentionDays: Math.max(1, Math.min(parseInt(body.retentionDays) || 90, 365)),
11793
11838
  maxSessionAgeDays: Math.max(1, Math.min(parseInt(body.maxSessionAgeDays) || 30, 365)),
11839
+ sessionCreationGraceMs: grace,
11840
+ cleanupUseDbDirect: !!body.cleanupUseDbDirect,
11841
+ // Geo / security
11794
11842
  enableGeolocation: !!body.enableGeolocation,
11795
11843
  enableSecurityScoring: !!body.enableSecurityScoring,
11796
11844
  blockSuspiciousSessions: !!body.blockSuspiciousSessions,
11797
11845
  maxFailedLogins: Math.max(1, Math.min(parseInt(body.maxFailedLogins) || 5, 100)),
11846
+ enableGeofencing: !!body.enableGeofencing,
11847
+ allowedCountries: sanitizeCountryList(body.allowedCountries),
11848
+ blockedCountries: sanitizeCountryList(body.blockedCountries),
11849
+ // Notifications
11798
11850
  enableEmailAlerts: !!body.enableEmailAlerts,
11799
11851
  alertOnSuspiciousLogin: !!body.alertOnSuspiciousLogin,
11800
11852
  alertOnNewLocation: !!body.alertOnNewLocation,
@@ -11802,11 +11854,10 @@ var settings$1 = {
11802
11854
  enableWebhooks: !!body.enableWebhooks,
11803
11855
  discordWebhookUrl: sanitizeWebhookUrl(body.discordWebhookUrl, "discord"),
11804
11856
  slackWebhookUrl: sanitizeWebhookUrl(body.slackWebhookUrl, "slack"),
11805
- enableGeofencing: !!body.enableGeofencing,
11806
- allowedCountries: sanitizeCountryList(body.allowedCountries),
11807
- blockedCountries: sanitizeCountryList(body.blockedCountries),
11857
+ // Session policy
11808
11858
  strictSessionEnforcement: !!body.strictSessionEnforcement,
11809
11859
  trustedProxies: !!body.trustedProxies,
11860
+ // Email templates
11810
11861
  emailTemplates: sanitizeEmailTemplates(body.emailTemplates)
11811
11862
  };
11812
11863
  await pluginStore.set({
@@ -12190,6 +12241,81 @@ var session$1 = ({ strapi: strapi2 }) => {
12190
12241
  throw err;
12191
12242
  }
12192
12243
  },
12244
+ /**
12245
+ * Permanently deletes sessions that have been inactive past the
12246
+ * configured retention window. Distinct from `deleteInactiveSessions`
12247
+ * (which deletes ALL inactive sessions) — this only drops rows older
12248
+ * than `retentionDays`, so recently-terminated sessions stay queryable
12249
+ * for audits.
12250
+ *
12251
+ * @param {Object} [options]
12252
+ * @param {number} [options.retentionDays] Overrides the stored setting.
12253
+ * @param {boolean} [options.useDbDirect] Fast-path via single SQL
12254
+ * DELETE. Bypasses lifecycle hooks; use only when necessary.
12255
+ * @returns {Promise<number>} Number of sessions deleted
12256
+ */
12257
+ async deleteOldSessions({ retentionDays, useDbDirect } = {}) {
12258
+ try {
12259
+ const settings2 = await getPluginSettings$1(strapi2);
12260
+ const effectiveDays = Number.isFinite(retentionDays) ? retentionDays : settings2.retentionDays || 90;
12261
+ if (effectiveDays === -1) {
12262
+ log.debug("[RETENTION] retentionDays=-1 (forever) — skipping");
12263
+ return 0;
12264
+ }
12265
+ const cutoffDate = new Date(Date.now() - effectiveDays * 24 * 60 * 60 * 1e3);
12266
+ const wantDbDirect = useDbDirect ?? settings2.cleanupUseDbDirect === true;
12267
+ log.info(`[RETENTION] Deleting inactive sessions older than ${effectiveDays} days (before ${cutoffDate.toISOString()})`);
12268
+ if (wantDbDirect) {
12269
+ try {
12270
+ const deleted = await strapi2.db.connection("magic_sessions").where("is_active", false).andWhere(function whereOldEnough() {
12271
+ this.where("logout_time", "<", cutoffDate).orWhere(function whereNullLogout() {
12272
+ this.whereNull("logout_time").andWhere(function whereOldByActivity() {
12273
+ this.where("last_active", "<", cutoffDate).orWhere(function whereNullActivity() {
12274
+ this.whereNull("last_active").andWhere("login_time", "<", cutoffDate);
12275
+ });
12276
+ });
12277
+ });
12278
+ }).del();
12279
+ log.info(`[SUCCESS] Retention (db-direct) deleted ${deleted} old session(s)`);
12280
+ return deleted;
12281
+ } catch (err) {
12282
+ log.warn("[RETENTION] DB-direct delete failed, falling back to Document Service:", err.message);
12283
+ }
12284
+ }
12285
+ let deletedCount = 0;
12286
+ const BATCH = 200;
12287
+ while (true) {
12288
+ const batch = await strapi2.documents(SESSION_UID$1).findMany({
12289
+ filters: {
12290
+ isActive: false,
12291
+ $or: [
12292
+ { logoutTime: { $lt: cutoffDate } },
12293
+ { logoutTime: { $null: true }, lastActive: { $lt: cutoffDate } },
12294
+ { logoutTime: { $null: true }, lastActive: { $null: true }, loginTime: { $lt: cutoffDate } }
12295
+ ]
12296
+ },
12297
+ fields: ["documentId"],
12298
+ sort: { loginTime: "asc" },
12299
+ limit: BATCH
12300
+ });
12301
+ if (!batch || batch.length === 0) break;
12302
+ for (const session2 of batch) {
12303
+ try {
12304
+ await strapi2.documents(SESSION_UID$1).delete({ documentId: session2.documentId });
12305
+ deletedCount++;
12306
+ } catch (err) {
12307
+ log.debug(`[RETENTION] Failed to delete session ${session2.documentId}:`, err.message);
12308
+ }
12309
+ }
12310
+ if (batch.length < BATCH) break;
12311
+ }
12312
+ log.info(`[SUCCESS] Retention deleted ${deletedCount} old session(s)`);
12313
+ return deletedCount;
12314
+ } catch (err) {
12315
+ log.error("Error in retention cleanup:", err);
12316
+ return 0;
12317
+ }
12318
+ },
12193
12319
  /**
12194
12320
  * Permanently deletes all inactive sessions.
12195
12321
  * Uses an inner scan loop that tolerates partial failures.
@@ -12235,7 +12361,7 @@ var session$1 = ({ strapi: strapi2 }) => {
12235
12361
  }
12236
12362
  };
12237
12363
  };
12238
- const version$1 = "4.5.0";
12364
+ const version$1 = "4.5.1";
12239
12365
  const require$$2 = {
12240
12366
  version: version$1
12241
12367
  };
@@ -9571,6 +9571,9 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
9571
9571
  }
9572
9572
  }, 3e3);
9573
9573
  const sessionService = strapi2.plugin("magic-sessionmanager").service("session");
9574
+ if (!strapi2.sessionManagerIntervals) {
9575
+ strapi2.sessionManagerIntervals = {};
9576
+ }
9574
9577
  log.info("Running initial session cleanup...");
9575
9578
  try {
9576
9579
  const settings2 = await getPluginSettings$3(strapi2);
@@ -9580,23 +9583,50 @@ var bootstrap$1 = async ({ strapi: strapi2 }) => {
9580
9583
  } catch (cleanupErr) {
9581
9584
  log.warn("Initial cleanup failed:", cleanupErr.message);
9582
9585
  }
9583
- const cleanupInterval = 30 * 60 * 1e3;
9584
- const cleanupIntervalHandle = setInterval(async () => {
9586
+ const scheduleIdleCleanup = async () => {
9587
+ let intervalMs = 30 * 60 * 1e3;
9588
+ let useDbDirect = false;
9585
9589
  try {
9586
- const service = strapi2.plugin("magic-sessionmanager").service("session");
9587
- const settings2 = await getPluginSettings$3(strapi2).catch(() => ({}));
9588
- await service.cleanupInactiveSessions({
9589
- useDbDirect: settings2.cleanupUseDbDirect === true
9590
- });
9591
- } catch (err) {
9592
- log.error("Periodic cleanup error:", err);
9590
+ const settings2 = await getPluginSettings$3(strapi2);
9591
+ intervalMs = Math.max(5 * 60 * 1e3, settings2.cleanupInterval || intervalMs);
9592
+ useDbDirect = settings2.cleanupUseDbDirect === true;
9593
+ } catch {
9593
9594
  }
9594
- }, cleanupInterval);
9595
- log.info("[TIME] Periodic cleanup scheduled (every 30 minutes)");
9596
- if (!strapi2.sessionManagerIntervals) {
9597
- strapi2.sessionManagerIntervals = {};
9598
- }
9599
- strapi2.sessionManagerIntervals.cleanup = cleanupIntervalHandle;
9595
+ const handle = setTimeout(async () => {
9596
+ try {
9597
+ const service = strapi2.plugin("magic-sessionmanager").service("session");
9598
+ await service.cleanupInactiveSessions({ useDbDirect });
9599
+ } catch (err) {
9600
+ log.error("Periodic cleanup error:", err);
9601
+ }
9602
+ scheduleIdleCleanup();
9603
+ }, intervalMs);
9604
+ strapi2.sessionManagerIntervals.cleanupTimeout = handle;
9605
+ };
9606
+ await scheduleIdleCleanup();
9607
+ const RETENTION_INTERVAL_MS = 24 * 60 * 60 * 1e3;
9608
+ const scheduleRetention = async () => {
9609
+ let useDbDirect = false;
9610
+ try {
9611
+ const settings2 = await getPluginSettings$3(strapi2);
9612
+ useDbDirect = settings2.cleanupUseDbDirect === true;
9613
+ } catch {
9614
+ }
9615
+ const handle = setTimeout(async () => {
9616
+ try {
9617
+ const service = strapi2.plugin("magic-sessionmanager").service("session");
9618
+ await service.deleteOldSessions({ useDbDirect });
9619
+ } catch (err) {
9620
+ log.error("Retention cleanup error:", err);
9621
+ }
9622
+ scheduleRetention();
9623
+ }, RETENTION_INTERVAL_MS);
9624
+ strapi2.sessionManagerIntervals.retentionTimeout = handle;
9625
+ };
9626
+ strapi2.sessionManagerIntervals.retentionStartup = setTimeout(() => {
9627
+ scheduleRetention();
9628
+ }, 5 * 60 * 1e3);
9629
+ log.info("[TIME] Dynamic cleanup + retention scheduled");
9600
9630
  mountPreLoginGeoGuard({ strapi: strapi2, log });
9601
9631
  mountFailedLoginLockout({ strapi: strapi2, log });
9602
9632
  mountLogoutRoute({ strapi: strapi2, log, sessionService });
@@ -10328,9 +10358,10 @@ var destroy$1 = async ({ strapi: strapi2 }) => {
10328
10358
  if (!handle) continue;
10329
10359
  try {
10330
10360
  clearInterval(handle);
10331
- log.info(`[STOP] ${name} interval stopped`);
10361
+ clearTimeout(handle);
10362
+ log.info(`[STOP] ${name} timer stopped`);
10332
10363
  } catch (err) {
10333
- log.warn(`Failed to stop ${name} interval:`, err.message);
10364
+ log.warn(`Failed to stop ${name} timer:`, err.message);
10334
10365
  }
10335
10366
  }
10336
10367
  strapi2.sessionManagerIntervals = {};
@@ -11719,15 +11750,27 @@ var settings$1 = {
11719
11750
  let settings2 = await pluginStore.get({ key: "settings" });
11720
11751
  if (!settings2) {
11721
11752
  settings2 = {
11753
+ // Timing & cleanup
11722
11754
  inactivityTimeout: 15,
11723
11755
  cleanupInterval: 30,
11724
11756
  lastSeenRateLimit: 30,
11725
11757
  retentionDays: 90,
11726
11758
  maxSessionAgeDays: 30,
11759
+ // Grace window (ms) during which a freshly-issued JWT is accepted
11760
+ // without a matching session row — see JWT-verify wrapper in bootstrap.
11761
+ sessionCreationGraceMs: 5e3,
11762
+ // Opt-in: use a single SQL UPDATE for idle-session cleanup
11763
+ // (bypasses Document Service lifecycle, required for very large installs).
11764
+ cleanupUseDbDirect: false,
11765
+ // Geo / security
11727
11766
  enableGeolocation: true,
11728
11767
  enableSecurityScoring: true,
11729
11768
  blockSuspiciousSessions: false,
11730
11769
  maxFailedLogins: 5,
11770
+ enableGeofencing: false,
11771
+ allowedCountries: [],
11772
+ blockedCountries: [],
11773
+ // Notifications
11731
11774
  enableEmailAlerts: false,
11732
11775
  alertOnSuspiciousLogin: true,
11733
11776
  alertOnNewLocation: true,
@@ -11735,11 +11778,10 @@ var settings$1 = {
11735
11778
  enableWebhooks: false,
11736
11779
  discordWebhookUrl: "",
11737
11780
  slackWebhookUrl: "",
11738
- enableGeofencing: false,
11739
- allowedCountries: [],
11740
- blockedCountries: [],
11781
+ // Session policy
11741
11782
  strictSessionEnforcement: false,
11742
11783
  trustedProxies: false,
11784
+ // Email templates
11743
11785
  emailTemplates: {
11744
11786
  suspiciousLogin: { subject: "", html: "", text: "" },
11745
11787
  newLocation: { subject: "", html: "", text: "" },
@@ -11772,16 +11814,26 @@ var settings$1 = {
11772
11814
  type: "plugin",
11773
11815
  name: "magic-sessionmanager"
11774
11816
  });
11817
+ const graceRaw = body.sessionCreationGraceMs;
11818
+ const grace = Number.isFinite(parseInt(graceRaw)) ? Math.max(0, Math.min(parseInt(graceRaw), 3e4)) : 5e3;
11775
11819
  const sanitizedSettings = {
11820
+ // Timing & cleanup
11776
11821
  inactivityTimeout: Math.max(1, Math.min(parseInt(body.inactivityTimeout) || 15, 1440)),
11777
11822
  cleanupInterval: Math.max(5, Math.min(parseInt(body.cleanupInterval) || 30, 1440)),
11778
11823
  lastSeenRateLimit: Math.max(5, Math.min(parseInt(body.lastSeenRateLimit) || 30, 300)),
11779
11824
  retentionDays: Math.max(1, Math.min(parseInt(body.retentionDays) || 90, 365)),
11780
11825
  maxSessionAgeDays: Math.max(1, Math.min(parseInt(body.maxSessionAgeDays) || 30, 365)),
11826
+ sessionCreationGraceMs: grace,
11827
+ cleanupUseDbDirect: !!body.cleanupUseDbDirect,
11828
+ // Geo / security
11781
11829
  enableGeolocation: !!body.enableGeolocation,
11782
11830
  enableSecurityScoring: !!body.enableSecurityScoring,
11783
11831
  blockSuspiciousSessions: !!body.blockSuspiciousSessions,
11784
11832
  maxFailedLogins: Math.max(1, Math.min(parseInt(body.maxFailedLogins) || 5, 100)),
11833
+ enableGeofencing: !!body.enableGeofencing,
11834
+ allowedCountries: sanitizeCountryList(body.allowedCountries),
11835
+ blockedCountries: sanitizeCountryList(body.blockedCountries),
11836
+ // Notifications
11785
11837
  enableEmailAlerts: !!body.enableEmailAlerts,
11786
11838
  alertOnSuspiciousLogin: !!body.alertOnSuspiciousLogin,
11787
11839
  alertOnNewLocation: !!body.alertOnNewLocation,
@@ -11789,11 +11841,10 @@ var settings$1 = {
11789
11841
  enableWebhooks: !!body.enableWebhooks,
11790
11842
  discordWebhookUrl: sanitizeWebhookUrl(body.discordWebhookUrl, "discord"),
11791
11843
  slackWebhookUrl: sanitizeWebhookUrl(body.slackWebhookUrl, "slack"),
11792
- enableGeofencing: !!body.enableGeofencing,
11793
- allowedCountries: sanitizeCountryList(body.allowedCountries),
11794
- blockedCountries: sanitizeCountryList(body.blockedCountries),
11844
+ // Session policy
11795
11845
  strictSessionEnforcement: !!body.strictSessionEnforcement,
11796
11846
  trustedProxies: !!body.trustedProxies,
11847
+ // Email templates
11797
11848
  emailTemplates: sanitizeEmailTemplates(body.emailTemplates)
11798
11849
  };
11799
11850
  await pluginStore.set({
@@ -12177,6 +12228,81 @@ var session$1 = ({ strapi: strapi2 }) => {
12177
12228
  throw err;
12178
12229
  }
12179
12230
  },
12231
+ /**
12232
+ * Permanently deletes sessions that have been inactive past the
12233
+ * configured retention window. Distinct from `deleteInactiveSessions`
12234
+ * (which deletes ALL inactive sessions) — this only drops rows older
12235
+ * than `retentionDays`, so recently-terminated sessions stay queryable
12236
+ * for audits.
12237
+ *
12238
+ * @param {Object} [options]
12239
+ * @param {number} [options.retentionDays] Overrides the stored setting.
12240
+ * @param {boolean} [options.useDbDirect] Fast-path via single SQL
12241
+ * DELETE. Bypasses lifecycle hooks; use only when necessary.
12242
+ * @returns {Promise<number>} Number of sessions deleted
12243
+ */
12244
+ async deleteOldSessions({ retentionDays, useDbDirect } = {}) {
12245
+ try {
12246
+ const settings2 = await getPluginSettings$1(strapi2);
12247
+ const effectiveDays = Number.isFinite(retentionDays) ? retentionDays : settings2.retentionDays || 90;
12248
+ if (effectiveDays === -1) {
12249
+ log.debug("[RETENTION] retentionDays=-1 (forever) — skipping");
12250
+ return 0;
12251
+ }
12252
+ const cutoffDate = new Date(Date.now() - effectiveDays * 24 * 60 * 60 * 1e3);
12253
+ const wantDbDirect = useDbDirect ?? settings2.cleanupUseDbDirect === true;
12254
+ log.info(`[RETENTION] Deleting inactive sessions older than ${effectiveDays} days (before ${cutoffDate.toISOString()})`);
12255
+ if (wantDbDirect) {
12256
+ try {
12257
+ const deleted = await strapi2.db.connection("magic_sessions").where("is_active", false).andWhere(function whereOldEnough() {
12258
+ this.where("logout_time", "<", cutoffDate).orWhere(function whereNullLogout() {
12259
+ this.whereNull("logout_time").andWhere(function whereOldByActivity() {
12260
+ this.where("last_active", "<", cutoffDate).orWhere(function whereNullActivity() {
12261
+ this.whereNull("last_active").andWhere("login_time", "<", cutoffDate);
12262
+ });
12263
+ });
12264
+ });
12265
+ }).del();
12266
+ log.info(`[SUCCESS] Retention (db-direct) deleted ${deleted} old session(s)`);
12267
+ return deleted;
12268
+ } catch (err) {
12269
+ log.warn("[RETENTION] DB-direct delete failed, falling back to Document Service:", err.message);
12270
+ }
12271
+ }
12272
+ let deletedCount = 0;
12273
+ const BATCH = 200;
12274
+ while (true) {
12275
+ const batch = await strapi2.documents(SESSION_UID$1).findMany({
12276
+ filters: {
12277
+ isActive: false,
12278
+ $or: [
12279
+ { logoutTime: { $lt: cutoffDate } },
12280
+ { logoutTime: { $null: true }, lastActive: { $lt: cutoffDate } },
12281
+ { logoutTime: { $null: true }, lastActive: { $null: true }, loginTime: { $lt: cutoffDate } }
12282
+ ]
12283
+ },
12284
+ fields: ["documentId"],
12285
+ sort: { loginTime: "asc" },
12286
+ limit: BATCH
12287
+ });
12288
+ if (!batch || batch.length === 0) break;
12289
+ for (const session2 of batch) {
12290
+ try {
12291
+ await strapi2.documents(SESSION_UID$1).delete({ documentId: session2.documentId });
12292
+ deletedCount++;
12293
+ } catch (err) {
12294
+ log.debug(`[RETENTION] Failed to delete session ${session2.documentId}:`, err.message);
12295
+ }
12296
+ }
12297
+ if (batch.length < BATCH) break;
12298
+ }
12299
+ log.info(`[SUCCESS] Retention deleted ${deletedCount} old session(s)`);
12300
+ return deletedCount;
12301
+ } catch (err) {
12302
+ log.error("Error in retention cleanup:", err);
12303
+ return 0;
12304
+ }
12305
+ },
12180
12306
  /**
12181
12307
  * Permanently deletes all inactive sessions.
12182
12308
  * Uses an inner scan loop that tolerates partial failures.
@@ -12222,7 +12348,7 @@ var session$1 = ({ strapi: strapi2 }) => {
12222
12348
  }
12223
12349
  };
12224
12350
  };
12225
- const version$1 = "4.5.0";
12351
+ const version$1 = "4.5.1";
12226
12352
  const require$$2 = {
12227
12353
  version: version$1
12228
12354
  };
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.5.0",
2
+ "version": "4.5.2",
3
3
  "keywords": [
4
4
  "strapi",
5
5
  "strapi-plugin",