webspresso 0.0.99 → 0.0.101

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.
Files changed (108) hide show
  1. package/.agents/ADMIN.md +47 -4
  2. package/.agents/AGENTS.md +4 -2
  3. package/.agents/AUTH.md +309 -0
  4. package/.agents/CLI.md +128 -0
  5. package/.agents/EMAIL.md +84 -2
  6. package/.agents/ERRORS.md +59 -0
  7. package/.agents/EVENTS.md +179 -0
  8. package/.agents/ORM.md +149 -0
  9. package/.agents/PLUGINS.md +44 -7
  10. package/.agents/QUEUE.md +18 -10
  11. package/.agents/ROUTING.md +97 -1
  12. package/.agents/STREAMING.md +35 -0
  13. package/AGENTS.md +2 -0
  14. package/README.md +145 -93
  15. package/bin/commands/add-docker.js +467 -0
  16. package/bin/commands/db-makemigrations.js +83 -0
  17. package/bin/commands/deploy-check.js +339 -0
  18. package/bin/commands/list.js +96 -0
  19. package/bin/commands/make-command.js +93 -0
  20. package/bin/commands/make-event.js +80 -0
  21. package/bin/commands/make-factory.js +72 -0
  22. package/bin/commands/make-mail.js +93 -0
  23. package/bin/commands/make-policy.js +106 -0
  24. package/bin/commands/run.js +195 -0
  25. package/bin/webspresso.js +21 -0
  26. package/core/auth/bouncer.js +196 -0
  27. package/core/auth/index.js +73 -1
  28. package/core/auth/magic-link.js +170 -0
  29. package/core/auth/manager.js +343 -3
  30. package/core/auth/middleware.js +45 -6
  31. package/core/auth/session-manager.js +255 -0
  32. package/core/auth/sessions.js +351 -0
  33. package/core/auth/tokens.js +1 -0
  34. package/core/auth/totp.js +4 -1
  35. package/core/auth/user-agent.js +161 -0
  36. package/core/commands/define-command.js +48 -0
  37. package/core/commands/index.js +18 -0
  38. package/core/commands/registry.js +156 -0
  39. package/core/commands/runner.js +76 -0
  40. package/core/compression/compressible.js +1 -1
  41. package/core/compression/middleware.js +35 -251
  42. package/core/content/service.js +8 -2
  43. package/core/events/discovery.js +142 -0
  44. package/core/events/dispatcher.js +552 -0
  45. package/core/events/index.js +23 -0
  46. package/core/events/signal.js +92 -0
  47. package/core/forms/flash.js +234 -0
  48. package/core/forms/form-builder.js +214 -0
  49. package/core/forms/index.js +16 -0
  50. package/core/mail/index.js +23 -0
  51. package/core/mail/mailable.js +250 -0
  52. package/core/mail/mailer.js +184 -0
  53. package/core/mail/transports.js +238 -0
  54. package/core/orm/factory.js +250 -0
  55. package/core/orm/index.js +8 -1
  56. package/core/orm/migrations/diff.js +185 -0
  57. package/core/orm/migrations/index.js +22 -4
  58. package/core/orm/model.js +55 -1
  59. package/core/orm/repository.js +283 -13
  60. package/core/orm/scopes.js +3 -2
  61. package/core/orm/types.js +6 -1
  62. package/core/queue/cron.js +252 -39
  63. package/core/queue/manager.js +20 -2
  64. package/core/queue/scheduler.js +115 -59
  65. package/core/ssr/stream.js +62 -2
  66. package/index.d.ts +540 -14
  67. package/index.js +44 -0
  68. package/package.json +11 -5
  69. package/plugins/admin-panel/api.js +509 -3
  70. package/plugins/admin-panel/index.js +17 -0
  71. package/plugins/admin-panel/modules/admin-users.js +1 -1
  72. package/plugins/audit-log/admin-component.js +54 -54
  73. package/plugins/audit-log/index.js +2 -2
  74. package/plugins/content/client/inline-edit.js +2 -46
  75. package/plugins/content/models/content-entry.js +4 -3
  76. package/plugins/content/models/content-type.js +4 -3
  77. package/plugins/cors.js +15 -4
  78. package/plugins/email/auth-bridge.js +66 -0
  79. package/plugins/email/bundled-templates.js +15 -0
  80. package/plugins/email/index.js +48 -0
  81. package/plugins/email/preview-handler.js +555 -0
  82. package/plugins/email/service.js +23 -12
  83. package/plugins/email/templates/auth-magic-link.mjml +13 -0
  84. package/plugins/email/transport.js +30 -11
  85. package/plugins/file-manager/admin-component.js +49 -49
  86. package/plugins/file-manager/index.js +4 -4
  87. package/plugins/index.js +2 -0
  88. package/plugins/redirect/index.js +1 -1
  89. package/plugins/webhooks/admin-component.js +608 -0
  90. package/plugins/webhooks/api-handlers.js +237 -0
  91. package/plugins/webhooks/crypto.js +144 -0
  92. package/plugins/webhooks/dispatcher.js +708 -0
  93. package/plugins/webhooks/index.js +263 -0
  94. package/plugins/webhooks/matcher.js +113 -0
  95. package/plugins/webhooks/migration-template.js +75 -0
  96. package/plugins/webhooks/models.js +98 -0
  97. package/plugins/webhooks/sender.js +176 -0
  98. package/src/api/api-loader.js +1 -1
  99. package/src/app-context.js +48 -6
  100. package/src/bundle/index.js +2 -1
  101. package/src/dev-reload/index.js +9 -1
  102. package/src/discovery/scan-routes.js +10 -1
  103. package/src/file-router.js +173 -4
  104. package/src/helpers.js +150 -1
  105. package/src/njk-frontmatter.js +3 -0
  106. package/src/pages/page-loader.js +1 -1
  107. package/src/routing/cascading.js +120 -4
  108. package/src/server.js +180 -26
@@ -4,6 +4,7 @@
4
4
  * @module core/auth/manager
5
5
  */
6
6
 
7
+ const EventEmitter = require('events');
7
8
  const { generateToken, hashToken } = require('./hash');
8
9
  const { PolicyManager } = require('./policy');
9
10
  const { signJwt, verifyJwt, decodeJwt } = require('./jwt');
@@ -67,7 +68,7 @@ const DEFAULT_CONFIG = {
67
68
  /**
68
69
  * AuthManager - Handles authentication operations
69
70
  */
70
- class AuthManager {
71
+ class AuthManager extends EventEmitter {
71
72
  /**
72
73
  * Create AuthManager instance
73
74
  * @param {Object} config - Configuration
@@ -83,6 +84,7 @@ class AuthManager {
83
84
  * @param {Object} [config.routes] - Route configuration
84
85
  */
85
86
  constructor(config) {
87
+ super();
86
88
  this.validateConfig(config);
87
89
 
88
90
  this.config = {
@@ -106,6 +108,7 @@ class AuthManager {
106
108
  passwordReset: config.notifications?.passwordReset || null,
107
109
  emailVerification: config.notifications?.emailVerification || null,
108
110
  welcome: config.notifications?.welcome || null,
111
+ magicLink: config.notifications?.magicLink || null,
109
112
  };
110
113
 
111
114
  // TOTP & 2FA initialization
@@ -138,6 +141,30 @@ class AuthManager {
138
141
 
139
142
  // Policy manager instance
140
143
  this.policies = new PolicyManager();
144
+
145
+ // Session tracking and revocation manager
146
+ const { SessionManager } = require('./session-manager');
147
+ const { createKnexSessionTracker, createMemorySessionTracker } = require('./sessions');
148
+
149
+ let tracker = config.sessionTracker || null;
150
+ if (!tracker && (config.sessionTracking || config.trackSessions)) {
151
+ if (config.db || this.config.db) {
152
+ tracker = createKnexSessionTracker(config.db || this.config.db);
153
+ } else {
154
+ tracker = createMemorySessionTracker();
155
+ }
156
+ }
157
+
158
+ if (tracker || config.sessionTracking || config.trackSessions) {
159
+ this.sessionManager = new SessionManager({
160
+ tracker: tracker || createMemorySessionTracker(),
161
+ sessionStore: config.sessionStore || this.config.session?.store,
162
+ touchThrottleMs: config.sessionTouchThrottleMs,
163
+ emitter: this,
164
+ });
165
+ } else {
166
+ this.sessionManager = null;
167
+ }
141
168
  }
142
169
 
143
170
  /**
@@ -285,7 +312,7 @@ class AuthManager {
285
312
 
286
313
  // Set req.user
287
314
  req.user = user;
288
-
315
+
289
316
  // Handle remember me
290
317
  if (options.remember && self.rememberTokens && self.config.rememberMe.enabled) {
291
318
  await self.createRememberToken(user.id, res);
@@ -294,18 +321,34 @@ class AuthManager {
294
321
  // Regenerate session ID for security if supported
295
322
  if (req.session && typeof req.session.regenerate === 'function') {
296
323
  return new Promise((resolve, reject) => {
297
- req.session.regenerate((err) => {
324
+ req.session.regenerate(async (err) => {
298
325
  if (err) {
299
326
  reject(new AuthenticationError('Session regeneration failed', 'SESSION_ERROR'));
300
327
  } else {
301
328
  // Restore userId after regenerate
302
329
  req.session.userId = user.id;
303
330
  req.session.loggedInAt = Date.now();
331
+ if (req.sessionID && self.sessionManager) {
332
+ try {
333
+ await self.sessionManager.track(req.sessionID, user.id, req);
334
+ } catch (e) {
335
+ // Non-blocking
336
+ }
337
+ }
304
338
  resolve();
305
339
  }
306
340
  });
307
341
  });
308
342
  }
343
+
344
+ // Track active session if sessionManager is active and not regenerated
345
+ if (req.sessionID && self.sessionManager) {
346
+ try {
347
+ await self.sessionManager.track(req.sessionID, user.id, req);
348
+ } catch (e) {
349
+ // Non-blocking
350
+ }
351
+ }
309
352
  },
310
353
 
311
354
  /**
@@ -337,6 +380,19 @@ class AuthManager {
337
380
  // Clear 2FA session state
338
381
  this.clear2Fa();
339
382
 
383
+ // Revoke session if sessionManager is active
384
+ if (req.sessionID && self.sessionManager) {
385
+ try {
386
+ if (options.everywhere && userId) {
387
+ await self.sessionManager.revokeAllSessions(userId);
388
+ } else {
389
+ await self.sessionManager.revokeSession(req.sessionID, { userId });
390
+ }
391
+ } catch (e) {
392
+ // Non-blocking
393
+ }
394
+ }
395
+
340
396
  // Clear user
341
397
  req.user = null;
342
398
 
@@ -530,6 +586,91 @@ class AuthManager {
530
586
  authorize(action, policy = null, resource = null) {
531
587
  return self.policies.authorize(req.user, action, policy, resource);
532
588
  },
589
+
590
+ /**
591
+ * Request a magic link for passwordless login
592
+ * @param {string} identifier - User email or username
593
+ * @param {Object} [options]
594
+ * @returns {Promise<{ sent: boolean, expiresAt?: Date, token?: string, magicLinkUrl?: string, userFound: boolean }>}
595
+ */
596
+ requestMagicLink(identifier, options = {}) {
597
+ return self.requestMagicLink(identifier, options);
598
+ },
599
+
600
+ /**
601
+ * Verify magic link token
602
+ * @param {string} rawToken
603
+ * @param {Object} [options]
604
+ * @returns {Promise<{ ok: true, user: Object, userId: * }>}
605
+ */
606
+ verifyMagicLink(rawToken, options = {}) {
607
+ return self.verifyMagicLink(rawToken, options);
608
+ },
609
+
610
+ /**
611
+ * Verify magic link token and log user into current request session
612
+ * @param {string} rawToken
613
+ * @param {Object} [options]
614
+ * @returns {Promise<{ ok: true, user: Object, userId: *, accessToken?: string, refreshToken?: string }>}
615
+ */
616
+ async loginWithMagicLink(rawToken, options = {}) {
617
+ const result = await self.verifyMagicLink(rawToken, options);
618
+ await this.login(result.user, options.remember);
619
+
620
+ const response = { ...result };
621
+ if (options.jwt) {
622
+ response.accessToken = self.generateUserToken(result.user, options.jwtAccessOptions);
623
+ response.refreshToken = self.generateRefreshToken(result.user, options.jwtRefreshOptions);
624
+ }
625
+
626
+ return response;
627
+ },
628
+
629
+ /**
630
+ * List active sessions/devices for current user
631
+ * @returns {Promise<Array<Object>>}
632
+ */
633
+ sessions() {
634
+ if (!req.user?.id) return Promise.resolve([]);
635
+ return self.listSessions(req.user.id, req.sessionID);
636
+ },
637
+
638
+ /**
639
+ * Revoke a specific session
640
+ * @param {string} sessionId
641
+ * @returns {Promise<boolean>}
642
+ */
643
+ async revokeSession(sessionId) {
644
+ if (sessionId === req.sessionID) {
645
+ await this.logout();
646
+ return true;
647
+ }
648
+ return await self.revokeSession(sessionId, { userId: req.user?.id });
649
+ },
650
+
651
+ /**
652
+ * Revoke all other active sessions except current
653
+ * @returns {Promise<number>}
654
+ */
655
+ revokeOtherSessions() {
656
+ if (!req.user?.id) return Promise.resolve(0);
657
+ return self.revokeOtherSessions(req.user.id, req.sessionID);
658
+ },
659
+
660
+ /**
661
+ * Revoke all active sessions for user (force logout from all devices)
662
+ * @param {string|number} [userId]
663
+ * @returns {Promise<number>}
664
+ */
665
+ async revokeAllSessions(userId = null) {
666
+ const targetUserId = userId || req.user?.id;
667
+ if (!targetUserId) return 0;
668
+ const count = await self.revokeAllSessions(targetUserId);
669
+ if (targetUserId === req.user?.id) {
670
+ await this.logout();
671
+ }
672
+ return count;
673
+ },
533
674
  };
534
675
  }
535
676
 
@@ -787,6 +928,122 @@ class AuthManager {
787
928
  return { ok: true, userId: verified.userId };
788
929
  }
789
930
 
931
+ /**
932
+ * Request magic link passwordless login — safe against user enumeration
933
+ * @param {string} identifier - Email or username
934
+ * @param {Object} [options]
935
+ * @param {number} [options.tokenTtlMs=900000] - Default 15 minutes (900,000ms)
936
+ * @param {Function} [options.buildMagicLinkUrl] - (rawToken) => string
937
+ * @param {string} [options.path='/auth/magic-link/verify']
938
+ * @param {boolean} [options.hideUserNotFound=true]
939
+ * @returns {Promise<{ sent: boolean, expiresAt?: Date, token?: string, magicLinkUrl?: string, userFound: boolean }>}
940
+ */
941
+ async requestMagicLink(identifier, options = {}) {
942
+ if (!this.authTokens) {
943
+ throw new Error('authTokens adapter is required for magic link authentication');
944
+ }
945
+
946
+ const user = await this.resolveUserByIdentifier(identifier);
947
+ if (!user) {
948
+ return {
949
+ sent: options.hideUserNotFound !== false,
950
+ userFound: false,
951
+ };
952
+ }
953
+
954
+ const tokenTtlMs = options.tokenTtlMs ?? 15 * 60 * 1000;
955
+ const { rawToken, expiresAt } = await createAuthToken(
956
+ this.authTokens,
957
+ TOKEN_TYPES.MAGIC_LINK,
958
+ user.id,
959
+ tokenTtlMs
960
+ );
961
+
962
+ let baseUrl = String(options.baseUrl || this.config.baseUrl || process.env.BASE_URL || 'http://localhost:3000');
963
+ while (baseUrl.endsWith('/')) {
964
+ baseUrl = baseUrl.slice(0, -1);
965
+ }
966
+ const pathSegment = options.path || '/auth/magic-link/verify';
967
+ const cleanPath = pathSegment.startsWith('/') ? pathSegment : `/${pathSegment}`;
968
+ const sep = cleanPath.includes('?') ? '&' : '?';
969
+
970
+ const hasCustomUrl = typeof options.buildMagicLinkUrl === 'function' || Boolean(options.path) || Boolean(options.baseUrl);
971
+ const customMagicLinkUrl = typeof options.buildMagicLinkUrl === 'function'
972
+ ? options.buildMagicLinkUrl(rawToken)
973
+ : hasCustomUrl
974
+ ? `${baseUrl}${cleanPath}${sep}token=${encodeURIComponent(rawToken)}`
975
+ : null;
976
+
977
+ const defaultMagicLinkUrl = `${baseUrl}${cleanPath}${sep}token=${encodeURIComponent(rawToken)}`;
978
+
979
+ if (typeof this.notifications.magicLink === 'function') {
980
+ await this.notifications.magicLink(user, {
981
+ token: rawToken,
982
+ expiresAt,
983
+ magicLinkUrl: customMagicLinkUrl,
984
+ expiresMinutes: Math.round(tokenTtlMs / 60000),
985
+ });
986
+ }
987
+
988
+ this.emit('magicLink:requested', { userId: user.id, email: user.email, expiresAt });
989
+
990
+ return {
991
+ sent: true,
992
+ userFound: true,
993
+ expiresAt,
994
+ token: rawToken,
995
+ magicLinkUrl: customMagicLinkUrl || defaultMagicLinkUrl,
996
+ };
997
+ }
998
+
999
+ /**
1000
+ * Verify magic link token and resolve user
1001
+ * @param {string} rawToken
1002
+ * @param {Object} [options]
1003
+ * @param {boolean} [options.markEmailVerified=true]
1004
+ * @param {boolean} [options.consumeAllForUser=true]
1005
+ * @returns {Promise<{ ok: true, user: Object, userId: * }>}
1006
+ */
1007
+ async verifyMagicLink(rawToken, options = {}) {
1008
+ if (!this.authTokens) {
1009
+ throw new Error('authTokens adapter is required for magic link authentication');
1010
+ }
1011
+
1012
+ const verified = await verifyAuthToken(
1013
+ this.authTokens,
1014
+ TOKEN_TYPES.MAGIC_LINK,
1015
+ rawToken
1016
+ );
1017
+ if (!verified) {
1018
+ throw new AuthenticationError('Invalid or expired magic link', 'INVALID_MAGIC_LINK');
1019
+ }
1020
+
1021
+ const user = await this.findUserById(verified.userId);
1022
+ if (!user) {
1023
+ throw new AuthenticationError('User not found for magic link', 'USER_NOT_FOUND');
1024
+ }
1025
+
1026
+ await consumeAuthToken(this.authTokens, verified.tokenHash);
1027
+
1028
+ if (options.consumeAllForUser !== false) {
1029
+ await this.authTokens.deleteAllForUser(verified.userId, TOKEN_TYPES.MAGIC_LINK);
1030
+ }
1031
+
1032
+ // Auto mark email verified if user has not verified it yet
1033
+ if (options.markEmailVerified !== false && this.updateUser && this.verifiedField && !user[this.verifiedField]) {
1034
+ try {
1035
+ await this.updateUser(user.id, { [this.verifiedField]: new Date() });
1036
+ user[this.verifiedField] = new Date();
1037
+ } catch (e) {
1038
+ // Continue even if update fails
1039
+ }
1040
+ }
1041
+
1042
+ this.emit('magicLink:verified', { userId: user.id });
1043
+
1044
+ return { ok: true, user, userId: verified.userId };
1045
+ }
1046
+
790
1047
  /**
791
1048
  * Send welcome notification if configured
792
1049
  * @param {Object} user
@@ -935,6 +1192,89 @@ class AuthManager {
935
1192
  }
936
1193
  return this._authMiddleware.require2Fa(options);
937
1194
  }
1195
+
1196
+ /**
1197
+ * Track session for a request
1198
+ * @param {string} sessionId
1199
+ * @param {string|number} userId
1200
+ * @param {Object} req
1201
+ */
1202
+ async trackSession(sessionId, userId, req) {
1203
+ if (this.sessionManager) {
1204
+ return await this.sessionManager.track(sessionId, userId, req);
1205
+ }
1206
+ return null;
1207
+ }
1208
+
1209
+ /**
1210
+ * Touch session to update activity timestamp
1211
+ * @param {string} sessionId
1212
+ */
1213
+ async touchSession(sessionId) {
1214
+ if (this.sessionManager) {
1215
+ return await this.sessionManager.touch(sessionId);
1216
+ }
1217
+ return false;
1218
+ }
1219
+
1220
+ /**
1221
+ * List active sessions for user
1222
+ * @param {string|number} userId
1223
+ * @param {string} [currentSessionId]
1224
+ */
1225
+ async listSessions(userId, currentSessionId = null) {
1226
+ if (this.sessionManager) {
1227
+ return await this.sessionManager.listSessions(userId, currentSessionId);
1228
+ }
1229
+ return [];
1230
+ }
1231
+
1232
+ /**
1233
+ * Check if session is valid in store
1234
+ * @param {string} sessionId
1235
+ * @param {string|number} [userId]
1236
+ */
1237
+ async isSessionValid(sessionId, userId = null) {
1238
+ if (this.sessionManager) {
1239
+ return await this.sessionManager.isSessionValid(sessionId, userId);
1240
+ }
1241
+ return true;
1242
+ }
1243
+
1244
+ /**
1245
+ * Revoke a specific session
1246
+ * @param {string} sessionId
1247
+ * @param {Object} [options]
1248
+ */
1249
+ async revokeSession(sessionId, options = {}) {
1250
+ if (this.sessionManager) {
1251
+ return await this.sessionManager.revokeSession(sessionId, options);
1252
+ }
1253
+ return false;
1254
+ }
1255
+
1256
+ /**
1257
+ * Revoke all other sessions for user
1258
+ * @param {string|number} userId
1259
+ * @param {string} currentSessionId
1260
+ */
1261
+ async revokeOtherSessions(userId, currentSessionId) {
1262
+ if (this.sessionManager) {
1263
+ return await this.sessionManager.revokeOtherSessions(userId, currentSessionId);
1264
+ }
1265
+ return 0;
1266
+ }
1267
+
1268
+ /**
1269
+ * Revoke all sessions for user
1270
+ * @param {string|number} userId
1271
+ */
1272
+ async revokeAllSessions(userId) {
1273
+ if (this.sessionManager) {
1274
+ return await this.sessionManager.revokeAllSessions(userId);
1275
+ }
1276
+ return 0;
1277
+ }
938
1278
  }
939
1279
 
940
1280
  module.exports = {
@@ -6,6 +6,7 @@
6
6
 
7
7
  const session = require('express-session');
8
8
  const cookieParser = require('cookie-parser');
9
+ const { createListSessionsHandler, createRevokeSessionHandler } = require('./session-manager');
9
10
 
10
11
  /**
11
12
  * Create authentication middleware stack
@@ -43,7 +44,21 @@ function createAuthMiddleware(authManager) {
43
44
  /**
44
45
  * Cookie parser middleware (needed for signed cookies / remember me)
45
46
  */
46
- const cookieMiddleware = cookieParser(config.session.secret);
47
+ const baseCookieMiddleware = cookieParser(config.session?.secret);
48
+ const cookieMiddleware = (req, res, next) => {
49
+ if (!req.cookies) {
50
+ return baseCookieMiddleware(req, res, next);
51
+ }
52
+ if (config.session?.secret) {
53
+ if (!req.secret) {
54
+ req.secret = config.session.secret;
55
+ }
56
+ if (!req.signedCookies || Object.keys(req.signedCookies).length === 0) {
57
+ req.signedCookies = cookieParser.signedCookies(req.cookies, config.session.secret);
58
+ }
59
+ }
60
+ next();
61
+ };
47
62
 
48
63
  /**
49
64
  * Authenticate middleware
@@ -79,12 +94,29 @@ function createAuthMiddleware(authManager) {
79
94
 
80
95
  // Try to load user from session if not authenticated by JWT
81
96
  if (!req.user && req.session?.userId) {
82
- const user = await authManager.findUserById(req.session.userId);
83
- if (user) {
84
- req.user = user;
85
- } else {
86
- // User no longer exists, clear session
97
+ // If session tracking is enabled, verify session has not been revoked
98
+ let isSessionValid = true;
99
+ if (authManager.sessionManager && req.sessionID) {
100
+ isSessionValid = await authManager.isSessionValid(req.sessionID, req.session.userId);
101
+ }
102
+
103
+ if (!isSessionValid) {
104
+ // Session was revoked remotely
87
105
  delete req.session.userId;
106
+ if (typeof req.session.destroy === 'function') {
107
+ req.session.destroy(() => {});
108
+ }
109
+ } else {
110
+ const user = await authManager.findUserById(req.session.userId);
111
+ if (user) {
112
+ req.user = user;
113
+ if (req.sessionID && authManager.sessionManager) {
114
+ authManager.touchSession(req.sessionID).catch(() => {});
115
+ }
116
+ } else {
117
+ // User no longer exists, clear session
118
+ delete req.session.userId;
119
+ }
88
120
  }
89
121
  }
90
122
 
@@ -95,6 +127,9 @@ function createAuthMiddleware(authManager) {
95
127
  req.user = user;
96
128
  req.session.userId = user.id;
97
129
  req.session.loggedInAt = Date.now();
130
+ if (req.sessionID && authManager.sessionManager) {
131
+ authManager.trackSession(req.sessionID, user.id, req).catch(() => {});
132
+ }
98
133
  }
99
134
  }
100
135
 
@@ -389,6 +424,10 @@ function createAuthMiddleware(authManager) {
389
424
  // Utility
390
425
  parseMiddlewareString,
391
426
 
427
+ // Session handlers
428
+ listSessions: createListSessionsHandler(authManager),
429
+ revokeSession: createRevokeSessionHandler(authManager),
430
+
392
431
  // Aliases for route config (factories — file-router calls requireAuth(opts) / requireGuest(opts))
393
432
  auth: requireAuth,
394
433
  jwt: requireJwt,