s3db.js 13.6.1 → 14.0.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.
Files changed (189) hide show
  1. package/README.md +56 -15
  2. package/dist/s3db.cjs +72446 -39022
  3. package/dist/s3db.cjs.map +1 -1
  4. package/dist/s3db.es.js +72172 -38790
  5. package/dist/s3db.es.js.map +1 -1
  6. package/mcp/lib/base-handler.js +157 -0
  7. package/mcp/lib/handlers/connection-handler.js +280 -0
  8. package/mcp/lib/handlers/query-handler.js +533 -0
  9. package/mcp/lib/handlers/resource-handler.js +428 -0
  10. package/mcp/lib/tool-registry.js +336 -0
  11. package/mcp/lib/tools/connection-tools.js +161 -0
  12. package/mcp/lib/tools/query-tools.js +267 -0
  13. package/mcp/lib/tools/resource-tools.js +404 -0
  14. package/package.json +85 -50
  15. package/src/clients/memory-client.class.js +346 -191
  16. package/src/clients/memory-storage.class.js +300 -84
  17. package/src/clients/s3-client.class.js +7 -6
  18. package/src/concerns/geo-encoding.js +19 -2
  19. package/src/concerns/ip.js +59 -9
  20. package/src/concerns/money.js +8 -1
  21. package/src/concerns/password-hashing.js +49 -8
  22. package/src/concerns/plugin-storage.js +186 -18
  23. package/src/concerns/storage-drivers/filesystem-driver.js +284 -0
  24. package/src/database.class.js +139 -29
  25. package/src/errors.js +332 -42
  26. package/src/plugins/api/auth/oidc-auth.js +66 -17
  27. package/src/plugins/api/auth/strategies/base-strategy.class.js +74 -0
  28. package/src/plugins/api/auth/strategies/factory.class.js +63 -0
  29. package/src/plugins/api/auth/strategies/global-strategy.class.js +44 -0
  30. package/src/plugins/api/auth/strategies/path-based-strategy.class.js +83 -0
  31. package/src/plugins/api/auth/strategies/path-rules-strategy.class.js +118 -0
  32. package/src/plugins/api/concerns/failban-manager.js +106 -57
  33. package/src/plugins/api/concerns/route-context.js +601 -0
  34. package/src/plugins/api/index.js +168 -40
  35. package/src/plugins/api/routes/auth-routes.js +198 -30
  36. package/src/plugins/api/routes/resource-routes.js +19 -4
  37. package/src/plugins/api/server/health-manager.class.js +163 -0
  38. package/src/plugins/api/server/middleware-chain.class.js +310 -0
  39. package/src/plugins/api/server/router.class.js +472 -0
  40. package/src/plugins/api/server.js +280 -1303
  41. package/src/plugins/api/utils/custom-routes.js +17 -5
  42. package/src/plugins/api/utils/guards.js +76 -17
  43. package/src/plugins/api/utils/openapi-generator-cached.class.js +133 -0
  44. package/src/plugins/api/utils/openapi-generator.js +7 -6
  45. package/src/plugins/audit.plugin.js +30 -8
  46. package/src/plugins/backup.plugin.js +110 -14
  47. package/src/plugins/cache/cache.class.js +22 -5
  48. package/src/plugins/cache/filesystem-cache.class.js +116 -19
  49. package/src/plugins/cache/memory-cache.class.js +211 -57
  50. package/src/plugins/cache/multi-tier-cache.class.js +371 -0
  51. package/src/plugins/cache/partition-aware-filesystem-cache.class.js +168 -47
  52. package/src/plugins/cache/redis-cache.class.js +552 -0
  53. package/src/plugins/cache/s3-cache.class.js +17 -8
  54. package/src/plugins/cache.plugin.js +176 -61
  55. package/src/plugins/cloud-inventory/drivers/alibaba-driver.js +8 -1
  56. package/src/plugins/cloud-inventory/drivers/aws-driver.js +60 -29
  57. package/src/plugins/cloud-inventory/drivers/azure-driver.js +8 -1
  58. package/src/plugins/cloud-inventory/drivers/base-driver.js +16 -2
  59. package/src/plugins/cloud-inventory/drivers/cloudflare-driver.js +8 -1
  60. package/src/plugins/cloud-inventory/drivers/digitalocean-driver.js +8 -1
  61. package/src/plugins/cloud-inventory/drivers/hetzner-driver.js +8 -1
  62. package/src/plugins/cloud-inventory/drivers/linode-driver.js +8 -1
  63. package/src/plugins/cloud-inventory/drivers/mongodb-atlas-driver.js +8 -1
  64. package/src/plugins/cloud-inventory/drivers/vultr-driver.js +8 -1
  65. package/src/plugins/cloud-inventory/index.js +29 -8
  66. package/src/plugins/cloud-inventory/registry.js +64 -42
  67. package/src/plugins/cloud-inventory.plugin.js +240 -138
  68. package/src/plugins/concerns/plugin-dependencies.js +54 -0
  69. package/src/plugins/concerns/resource-names.js +100 -0
  70. package/src/plugins/consumers/index.js +10 -2
  71. package/src/plugins/consumers/sqs-consumer.js +12 -2
  72. package/src/plugins/cookie-farm-suite.plugin.js +278 -0
  73. package/src/plugins/cookie-farm.errors.js +73 -0
  74. package/src/plugins/cookie-farm.plugin.js +869 -0
  75. package/src/plugins/costs.plugin.js +7 -1
  76. package/src/plugins/eventual-consistency/analytics.js +94 -19
  77. package/src/plugins/eventual-consistency/config.js +15 -7
  78. package/src/plugins/eventual-consistency/consolidation.js +29 -11
  79. package/src/plugins/eventual-consistency/garbage-collection.js +3 -1
  80. package/src/plugins/eventual-consistency/helpers.js +39 -14
  81. package/src/plugins/eventual-consistency/install.js +21 -2
  82. package/src/plugins/eventual-consistency/utils.js +32 -10
  83. package/src/plugins/fulltext.plugin.js +38 -11
  84. package/src/plugins/geo.plugin.js +61 -9
  85. package/src/plugins/identity/concerns/config.js +61 -0
  86. package/src/plugins/identity/concerns/mfa-manager.js +15 -2
  87. package/src/plugins/identity/concerns/rate-limit.js +124 -0
  88. package/src/plugins/identity/concerns/resource-schemas.js +9 -1
  89. package/src/plugins/identity/concerns/token-generator.js +29 -4
  90. package/src/plugins/identity/drivers/auth-driver.interface.js +76 -0
  91. package/src/plugins/identity/drivers/client-credentials-driver.js +127 -0
  92. package/src/plugins/identity/drivers/index.js +18 -0
  93. package/src/plugins/identity/drivers/password-driver.js +122 -0
  94. package/src/plugins/identity/email-service.js +17 -2
  95. package/src/plugins/identity/index.js +413 -69
  96. package/src/plugins/identity/oauth2-server.js +413 -30
  97. package/src/plugins/identity/oidc-discovery.js +16 -8
  98. package/src/plugins/identity/rsa-keys.js +115 -35
  99. package/src/plugins/identity/server.js +166 -45
  100. package/src/plugins/identity/session-manager.js +53 -7
  101. package/src/plugins/identity/ui/pages/mfa-verification.js +17 -15
  102. package/src/plugins/identity/ui/routes.js +363 -255
  103. package/src/plugins/importer/index.js +153 -20
  104. package/src/plugins/index.js +9 -2
  105. package/src/plugins/kubernetes-inventory/index.js +6 -0
  106. package/src/plugins/kubernetes-inventory/k8s-driver.js +867 -0
  107. package/src/plugins/kubernetes-inventory/resource-types.js +274 -0
  108. package/src/plugins/kubernetes-inventory.plugin.js +980 -0
  109. package/src/plugins/metrics.plugin.js +64 -16
  110. package/src/plugins/ml/base-model.class.js +25 -15
  111. package/src/plugins/ml/regression-model.class.js +1 -1
  112. package/src/plugins/ml.errors.js +57 -25
  113. package/src/plugins/ml.plugin.js +28 -4
  114. package/src/plugins/namespace.js +210 -0
  115. package/src/plugins/plugin.class.js +180 -8
  116. package/src/plugins/puppeteer/console-monitor.js +729 -0
  117. package/src/plugins/puppeteer/cookie-manager.js +492 -0
  118. package/src/plugins/puppeteer/network-monitor.js +816 -0
  119. package/src/plugins/puppeteer/performance-manager.js +746 -0
  120. package/src/plugins/puppeteer/proxy-manager.js +478 -0
  121. package/src/plugins/puppeteer/stealth-manager.js +556 -0
  122. package/src/plugins/puppeteer.errors.js +81 -0
  123. package/src/plugins/puppeteer.plugin.js +1327 -0
  124. package/src/plugins/queue-consumer.plugin.js +69 -14
  125. package/src/plugins/recon/behaviors/uptime-behavior.js +691 -0
  126. package/src/plugins/recon/concerns/command-runner.js +148 -0
  127. package/src/plugins/recon/concerns/diff-detector.js +372 -0
  128. package/src/plugins/recon/concerns/fingerprint-builder.js +307 -0
  129. package/src/plugins/recon/concerns/process-manager.js +338 -0
  130. package/src/plugins/recon/concerns/report-generator.js +478 -0
  131. package/src/plugins/recon/concerns/security-analyzer.js +571 -0
  132. package/src/plugins/recon/concerns/target-normalizer.js +68 -0
  133. package/src/plugins/recon/config/defaults.js +321 -0
  134. package/src/plugins/recon/config/resources.js +370 -0
  135. package/src/plugins/recon/index.js +778 -0
  136. package/src/plugins/recon/managers/dependency-manager.js +174 -0
  137. package/src/plugins/recon/managers/scheduler-manager.js +179 -0
  138. package/src/plugins/recon/managers/storage-manager.js +745 -0
  139. package/src/plugins/recon/managers/target-manager.js +274 -0
  140. package/src/plugins/recon/stages/asn-stage.js +314 -0
  141. package/src/plugins/recon/stages/certificate-stage.js +84 -0
  142. package/src/plugins/recon/stages/dns-stage.js +107 -0
  143. package/src/plugins/recon/stages/dnsdumpster-stage.js +362 -0
  144. package/src/plugins/recon/stages/fingerprint-stage.js +71 -0
  145. package/src/plugins/recon/stages/google-dorks-stage.js +440 -0
  146. package/src/plugins/recon/stages/http-stage.js +89 -0
  147. package/src/plugins/recon/stages/latency-stage.js +148 -0
  148. package/src/plugins/recon/stages/massdns-stage.js +302 -0
  149. package/src/plugins/recon/stages/osint-stage.js +1373 -0
  150. package/src/plugins/recon/stages/ports-stage.js +169 -0
  151. package/src/plugins/recon/stages/screenshot-stage.js +94 -0
  152. package/src/plugins/recon/stages/secrets-stage.js +514 -0
  153. package/src/plugins/recon/stages/subdomains-stage.js +295 -0
  154. package/src/plugins/recon/stages/tls-audit-stage.js +78 -0
  155. package/src/plugins/recon/stages/vulnerability-stage.js +78 -0
  156. package/src/plugins/recon/stages/web-discovery-stage.js +113 -0
  157. package/src/plugins/recon/stages/whois-stage.js +349 -0
  158. package/src/plugins/recon.plugin.js +75 -0
  159. package/src/plugins/recon.plugin.js.backup +2635 -0
  160. package/src/plugins/relation.errors.js +87 -14
  161. package/src/plugins/replicator.plugin.js +514 -137
  162. package/src/plugins/replicators/base-replicator.class.js +89 -1
  163. package/src/plugins/replicators/bigquery-replicator.class.js +66 -22
  164. package/src/plugins/replicators/dynamodb-replicator.class.js +22 -15
  165. package/src/plugins/replicators/mongodb-replicator.class.js +22 -15
  166. package/src/plugins/replicators/mysql-replicator.class.js +52 -17
  167. package/src/plugins/replicators/planetscale-replicator.class.js +30 -4
  168. package/src/plugins/replicators/postgres-replicator.class.js +62 -27
  169. package/src/plugins/replicators/s3db-replicator.class.js +25 -18
  170. package/src/plugins/replicators/schema-sync.helper.js +3 -3
  171. package/src/plugins/replicators/sqs-replicator.class.js +8 -2
  172. package/src/plugins/replicators/turso-replicator.class.js +23 -3
  173. package/src/plugins/replicators/webhook-replicator.class.js +42 -4
  174. package/src/plugins/s3-queue.plugin.js +464 -65
  175. package/src/plugins/scheduler.plugin.js +20 -6
  176. package/src/plugins/state-machine.plugin.js +40 -9
  177. package/src/plugins/tfstate/base-driver.js +28 -4
  178. package/src/plugins/tfstate/errors.js +65 -10
  179. package/src/plugins/tfstate/filesystem-driver.js +52 -8
  180. package/src/plugins/tfstate/index.js +163 -90
  181. package/src/plugins/tfstate/s3-driver.js +64 -6
  182. package/src/plugins/ttl.plugin.js +72 -17
  183. package/src/plugins/vector/distances.js +18 -12
  184. package/src/plugins/vector/kmeans.js +26 -4
  185. package/src/resource.class.js +115 -19
  186. package/src/testing/factory.class.js +20 -3
  187. package/src/testing/seeder.class.js +7 -1
  188. package/src/clients/memory-client.md +0 -917
  189. package/src/plugins/cloud-inventory/drivers/mock-drivers.js +0 -449
@@ -25,6 +25,7 @@ import { generateAuthCode } from '../oidc-discovery.js';
25
25
  import { tryFn } from '../../../concerns/try-fn.js';
26
26
  import { sessionAuth, adminOnly } from './middleware.js';
27
27
  import { idGenerator } from '../../../concerns/id.js';
28
+ import { createRedirectRateLimitMiddleware } from '../concerns/rate-limit.js';
28
29
 
29
30
  /**
30
31
  * Get page component (custom or default)
@@ -47,6 +48,38 @@ export function registerUIRoutes(app, plugin) {
47
48
  const customPages = config.ui.customPages || {};
48
49
  const failbanConfig = config.failban || {};
49
50
  const accountLockoutConfig = config.accountLockout || {};
51
+ const userAttributes = plugin.config?.resources?.users?.mergedConfig?.attributes || {};
52
+ const supportsStatusField = Object.prototype.hasOwnProperty.call(userAttributes, 'status');
53
+ const keyManager = plugin.oauth2Server?.keyManager || null;
54
+
55
+ const createMfaChallengeToken = (user, rememberFlag) => {
56
+ if (!keyManager) {
57
+ return null;
58
+ }
59
+
60
+ return keyManager.createToken({
61
+ type: 'mfa_challenge',
62
+ userId: user.id,
63
+ email: user.email,
64
+ remember: rememberFlag === '1'
65
+ }, '5m');
66
+ };
67
+
68
+ const verifyMfaChallengeToken = async (token) => {
69
+ if (!keyManager || !token) {
70
+ return null;
71
+ }
72
+
73
+ try {
74
+ const verified = await keyManager.verifyToken(token);
75
+ if (!verified || verified.payload?.type !== 'mfa_challenge') {
76
+ return null;
77
+ }
78
+ return verified.payload;
79
+ } catch {
80
+ return null;
81
+ }
82
+ };
50
83
 
51
84
  // Create UI config object with registration settings
52
85
  const uiConfig = {
@@ -101,63 +134,105 @@ export function registerUIRoutes(app, plugin) {
101
134
  }));
102
135
  });
103
136
 
137
+ const getClientIp = (c) =>
138
+ c.get('clientIp') ||
139
+ c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ||
140
+ c.req.header('x-real-ip') ||
141
+ 'unknown';
142
+
143
+ const buildRateLimitRedirect = (retryAfter) => {
144
+ const seconds = Math.max(retryAfter, 1);
145
+ const message = seconds > 60
146
+ ? `Too many login attempts. Please wait ${Math.ceil(seconds / 60)} minute(s) and try again.`
147
+ : `Too many login attempts. Please wait ${seconds} second(s) and try again.`;
148
+ return `/login?error=${encodeURIComponent(message)}`;
149
+ };
150
+
104
151
  // ============================================================================
105
152
  // POST /login - Handle login form submission
106
153
  // ============================================================================
107
- app.post('/login', async (c) => {
154
+ const loginHandler = async (c) => {
108
155
  try {
109
156
  const body = await c.req.parseBody();
110
- const { email, password, remember } = body;
157
+ const {
158
+ email,
159
+ password,
160
+ remember,
161
+ mfa_token,
162
+ backup_code,
163
+ mfa_challenge
164
+ } = body;
165
+
166
+ const clientIp = getClientIp(c);
167
+ const userAgent = c.req.header('user-agent') || 'unknown';
111
168
 
112
- // Get IP from context (set by failban middleware)
113
- const clientIp = c.get('clientIp') ||
114
- c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ||
115
- c.req.header('x-real-ip') ||
116
- 'unknown';
169
+ const usingChallenge = Boolean(mfa_challenge);
170
+ let normalizedEmail = email ? email.toLowerCase().trim() : '';
171
+ let rememberChoice = remember === '1' ? '1' : '0';
172
+ let challengePayload = null;
117
173
 
118
- // Validate input
119
- if (!email || !password) {
120
- // Record violation for missing credentials (possible attack)
174
+ if (usingChallenge) {
175
+ challengePayload = await verifyMfaChallengeToken(mfa_challenge);
176
+ if (!challengePayload) {
177
+ return c.redirect(`/login?error=${encodeURIComponent('Your login session expired. Please sign in again.')}`);
178
+ }
179
+ normalizedEmail = (challengePayload.email || '').toLowerCase();
180
+ rememberChoice = challengePayload.remember ? '1' : '0';
181
+ }
182
+
183
+ if (!normalizedEmail) {
121
184
  if (failbanManager && failbanConfig.endpoints.login) {
122
185
  await failbanManager.recordViolation(clientIp, 'invalid_login_request', {
123
186
  path: '/login',
124
- userAgent: c.req.header('user-agent')
187
+ userAgent
125
188
  });
126
189
  }
127
-
128
- return c.redirect(`/login?error=${encodeURIComponent('Email and password are required')}&email=${encodeURIComponent(email || '')}`);
190
+ return c.redirect(`/login?error=${encodeURIComponent('Email and password are required')}`);
129
191
  }
130
192
 
131
- // Find user by email
132
- const [okQuery, errQuery, users] = await tryFn(() =>
133
- usersResource.query({ email: email.toLowerCase().trim() })
134
- );
193
+ let user = null;
135
194
 
136
- if (!okQuery || users.length === 0) {
137
- // Don't reveal whether user exists (timing attack protection)
138
- await new Promise(resolve => setTimeout(resolve, 100));
195
+ if (usingChallenge) {
196
+ const [okUser, errUser, challengeUser] = await tryFn(() =>
197
+ usersResource.get(challengePayload.userId)
198
+ );
139
199
 
140
- // Record failed login attempt (user not found)
141
- if (failbanManager && failbanConfig.endpoints.login) {
142
- await failbanManager.recordViolation(clientIp, 'failed_login', {
143
- path: '/login',
144
- userAgent: c.req.header('user-agent'),
145
- email
146
- });
200
+ if (!okUser || !challengeUser || challengeUser.email.toLowerCase() !== normalizedEmail) {
201
+ if (config.verbose) {
202
+ console.error('[Identity Plugin] MFA challenge verification failed:', errUser?.message || 'challenge mismatch');
203
+ }
204
+ return c.redirect(`/login?error=${encodeURIComponent('Your login session expired. Please sign in again.')}`);
147
205
  }
148
206
 
149
- // Audit log
150
- await logAudit('login_failed', {
151
- email,
152
- reason: 'user_not_found',
153
- ipAddress: clientIp,
154
- userAgent: c.req.header('user-agent')
155
- });
207
+ user = challengeUser;
208
+ } else {
209
+ const [okQuery, errQuery, users] = await tryFn(() =>
210
+ usersResource.query({ email: normalizedEmail })
211
+ );
156
212
 
157
- return c.redirect(`/login?error=${encodeURIComponent('Invalid email or password')}&email=${encodeURIComponent(email)}`);
158
- }
213
+ if (!okQuery || users.length === 0) {
214
+ await new Promise(resolve => setTimeout(resolve, 100));
159
215
 
160
- const user = users[0];
216
+ if (failbanManager && failbanConfig.endpoints.login) {
217
+ await failbanManager.recordViolation(clientIp, 'failed_login', {
218
+ path: '/login',
219
+ userAgent,
220
+ email
221
+ });
222
+ }
223
+
224
+ await logAudit('login_failed', {
225
+ email,
226
+ reason: 'user_not_found',
227
+ ipAddress: clientIp,
228
+ userAgent
229
+ });
230
+
231
+ return c.redirect(`/login?error=${encodeURIComponent('Invalid email or password')}&email=${encodeURIComponent(email || '')}`);
232
+ }
233
+
234
+ user = users[0];
235
+ }
161
236
 
162
237
  // 🔒 ACCOUNT LOCKOUT CHECK
163
238
  if (accountLockoutConfig.enabled && user.lockedUntil) {
@@ -165,204 +240,213 @@ export function registerUIRoutes(app, plugin) {
165
240
  const lockedUntilTime = new Date(user.lockedUntil).getTime();
166
241
 
167
242
  if (lockedUntilTime > now) {
168
- // Account is still locked
169
243
  const remainingMinutes = Math.ceil((lockedUntilTime - now) / 60000);
170
244
  const message = `Your account has been locked due to too many failed login attempts. Please try again in ${remainingMinutes} minute${remainingMinutes > 1 ? 's' : ''} or contact support.`;
245
+ return c.redirect(`/login?error=${encodeURIComponent(message)}&email=${encodeURIComponent(normalizedEmail)}`);
246
+ }
171
247
 
172
- if (config.verbose) {
173
- console.log(`[Account Lockout] User ${user.email} attempted login while locked (expires in ${remainingMinutes}m)`);
174
- }
175
-
176
- return c.redirect(`/login?error=${encodeURIComponent(message)}&email=${encodeURIComponent(email)}`);
177
- } else {
178
- // Lock expired - auto-unlock the account
179
- await usersResource.update(user.id, {
180
- lockedUntil: null,
181
- failedLoginAttempts: 0,
182
- lastFailedLogin: null
183
- });
248
+ // Auto-unlock expired locks
249
+ await usersResource.update(user.id, {
250
+ lockedUntil: null,
251
+ failedLoginAttempts: 0,
252
+ lastFailedLogin: null
253
+ });
254
+ }
184
255
 
185
- if (config.verbose) {
186
- console.log(`[Account Lockout] Auto-unlocked user ${user.email} (lock expired)`);
256
+ // Verify password (initial step only)
257
+ if (!usingChallenge) {
258
+ if (!password) {
259
+ if (failbanManager && failbanConfig.endpoints.login) {
260
+ await failbanManager.recordViolation(clientIp, 'invalid_login_request', {
261
+ path: '/login',
262
+ userAgent,
263
+ email
264
+ });
187
265
  }
266
+ return c.redirect(`/login?error=${encodeURIComponent('Email and password are required')}&email=${encodeURIComponent(email || '')}`);
188
267
  }
189
- }
190
268
 
191
- // Verify password (auto-decrypted by S3DB)
192
- const [okVerify, errVerify, isValid] = await tryFn(() =>
193
- verifyPassword(password, user.password)
194
- );
269
+ const authResult = await plugin.authenticateWithPassword({
270
+ email: normalizedEmail,
271
+ password,
272
+ user
273
+ });
195
274
 
196
- if (!okVerify || !isValid) {
197
- // 🔒 FAILED LOGIN - Update account lockout counters
198
- if (accountLockoutConfig.enabled) {
199
- const failedAttempts = (user.failedLoginAttempts || 0) + 1;
200
- const now = new Date().toISOString();
275
+ if (!authResult.success) {
276
+ if (authResult.statusCode >= 500 && config.verbose) {
277
+ console.error('[Identity Plugin] Password driver error:', authResult.error);
278
+ }
201
279
 
202
- if (failedAttempts >= accountLockoutConfig.maxAttempts) {
203
- // Lock the account
204
- const lockoutUntil = new Date(Date.now() + accountLockoutConfig.lockoutDuration).toISOString();
280
+ if (accountLockoutConfig.enabled) {
281
+ const failedAttempts = (user.failedLoginAttempts || 0) + 1;
282
+ const nowIso = new Date().toISOString();
205
283
 
206
- await usersResource.update(user.id, {
207
- failedLoginAttempts: failedAttempts,
208
- lockedUntil: lockoutUntil,
209
- lastFailedLogin: now
210
- });
284
+ if (failedAttempts >= accountLockoutConfig.maxAttempts) {
285
+ const lockoutUntil = new Date(Date.now() + accountLockoutConfig.lockoutDuration).toISOString();
211
286
 
212
- const lockoutMinutes = Math.ceil(accountLockoutConfig.lockoutDuration / 60000);
213
- const message = `Too many failed login attempts. Your account has been locked for ${lockoutMinutes} minutes. Please contact support if you need assistance.`;
287
+ await usersResource.update(user.id, {
288
+ failedLoginAttempts: failedAttempts,
289
+ lockedUntil: lockoutUntil,
290
+ lastFailedLogin: nowIso
291
+ });
214
292
 
215
- if (config.verbose) {
216
- console.log(`[Account Lockout] Locked user ${user.email} after ${failedAttempts} failed attempts (until ${lockoutUntil})`);
293
+ const lockoutMinutes = Math.ceil(accountLockoutConfig.lockoutDuration / 60000);
294
+ const message = `Too many failed login attempts. Your account has been locked for ${lockoutMinutes} minutes. Please contact support if you need assistance.`;
295
+ return c.redirect(`/login?error=${encodeURIComponent(message)}&email=${encodeURIComponent(normalizedEmail)}`);
217
296
  }
218
297
 
219
- return c.redirect(`/login?error=${encodeURIComponent(message)}&email=${encodeURIComponent(email)}`);
220
- } else {
221
- // Increment failed attempts counter
222
298
  await usersResource.update(user.id, {
223
299
  failedLoginAttempts: failedAttempts,
224
- lastFailedLogin: now
300
+ lastFailedLogin: nowIso
225
301
  });
302
+ }
226
303
 
227
- if (config.verbose) {
228
- console.log(`[Account Lockout] User ${user.email} failed login attempt ${failedAttempts}/${accountLockoutConfig.maxAttempts}`);
229
- }
304
+ if (failbanManager && failbanConfig.endpoints.login) {
305
+ await failbanManager.recordViolation(clientIp, 'failed_login', {
306
+ path: '/login',
307
+ userAgent,
308
+ email: normalizedEmail,
309
+ userId: user.id
310
+ });
230
311
  }
231
- }
232
312
 
233
- // Record failed login attempt in failban (IP-based)
234
- if (failbanManager && failbanConfig.endpoints.login) {
235
- await failbanManager.recordViolation(clientIp, 'failed_login', {
236
- path: '/login',
237
- userAgent: c.req.header('user-agent'),
238
- email,
239
- userId: user.id
240
- });
313
+ return c.redirect(`/login?error=${encodeURIComponent('Invalid email or password')}&email=${encodeURIComponent(normalizedEmail)}`);
241
314
  }
242
315
 
243
- return c.redirect(`/login?error=${encodeURIComponent('Invalid email or password')}&email=${encodeURIComponent(email)}`);
244
- }
245
-
246
- // ✅ SUCCESS: Reset account lockout counters
247
- if (accountLockoutConfig.enabled && accountLockoutConfig.resetOnSuccess) {
248
- if (user.failedLoginAttempts > 0 || user.lockedUntil) {
249
- await usersResource.update(user.id, {
250
- failedLoginAttempts: 0,
251
- lockedUntil: null,
252
- lastFailedLogin: null
253
- });
254
-
255
- if (config.verbose) {
256
- console.log(`[Account Lockout] Reset counters for user ${user.email} after successful login`);
316
+ if (accountLockoutConfig.enabled && accountLockoutConfig.resetOnSuccess) {
317
+ if (user.failedLoginAttempts > 0 || user.lockedUntil) {
318
+ await usersResource.update(user.id, {
319
+ failedLoginAttempts: 0,
320
+ lockedUntil: null,
321
+ lastFailedLogin: null
322
+ });
257
323
  }
258
324
  }
325
+
326
+ user = authResult.user || user;
259
327
  }
260
328
 
261
- // Check if user is active
262
- if (user.status !== 'active') {
329
+ // Account active status
330
+ if (supportsStatusField && user.status && user.status !== 'active') {
263
331
  const message = user.status === 'suspended'
264
332
  ? 'Your account has been suspended. Please contact support.'
265
- : 'Your account is inactive. Please verify your email or contact support.';
266
- return c.redirect(`/login?error=${encodeURIComponent(message)}&email=${encodeURIComponent(email)}`);
333
+ : 'Your account is inactive. Please contact support.';
334
+ return c.redirect(`/login?error=${encodeURIComponent(message)}&email=${encodeURIComponent(normalizedEmail)}`);
335
+ }
336
+
337
+ if (!supportsStatusField && user.active === false) {
338
+ return c.redirect(`/login?error=${encodeURIComponent('Your account is inactive. Please contact support.')}&email=${encodeURIComponent(normalizedEmail)}`);
339
+ }
340
+
341
+ if (config.registration.requireEmailVerification && !user.emailVerified) {
342
+ const message = 'Please verify your email address before signing in.';
343
+ return c.redirect(`/login?error=${encodeURIComponent(message)}&email=${encodeURIComponent(normalizedEmail)}`);
267
344
  }
268
345
 
269
- // 🔐 MFA VERIFICATION (if enabled)
346
+ // 🔐 MFA logic
347
+ let hasMFA = false;
348
+ let mfaDevices = [];
270
349
  if (config.mfa.enabled && plugin.mfaDevicesResource) {
271
- // Check if user has MFA enabled
272
- const [okMFA, errMFA, mfaDevices] = await tryFn(() =>
350
+ const [okMFA, errMFA, devices] = await tryFn(() =>
273
351
  plugin.mfaDevicesResource.query({ userId: user.id, verified: true })
274
352
  );
353
+ if (!okMFA && config.verbose) {
354
+ console.error('[Identity Plugin] Failed to load MFA devices:', errMFA);
355
+ }
356
+ if (okMFA && devices && devices.length > 0) {
357
+ hasMFA = true;
358
+ mfaDevices = devices;
359
+ }
360
+ }
275
361
 
276
- const hasMFA = okMFA && mfaDevices.length > 0;
277
- const mfaRequired = config.mfa.required;
278
-
279
- if (hasMFA || mfaRequired) {
280
- // User has MFA enabled or MFA is mandatory
281
- const { mfa_token, backup_code } = body;
282
-
283
- if (!mfa_token && !backup_code) {
284
- // Redirect to MFA verification page
285
- // Create a temporary token to verify the user has passed password auth
286
- // Store the password for re-submission (it's already verified at this point)
287
- const tempToken = Buffer.from(JSON.stringify({
288
- userId: user.id,
289
- email: user.email,
290
- password: password, // Store to re-submit after MFA
291
- timestamp: Date.now()
292
- })).toString('base64');
293
-
294
- return c.redirect(`/login/mfa?token=${tempToken}&remember=${remember || ''}`);
295
- }
362
+ if (config.mfa.required && !hasMFA) {
363
+ return c.redirect(`/login?error=${encodeURIComponent('Multi-factor authentication is required for your account. Please contact support to complete enrollment.')}&email=${encodeURIComponent(normalizedEmail)}`);
364
+ }
296
365
 
297
- // Verify MFA token or backup code
298
- let mfaVerified = false;
366
+ const needsMfa = hasMFA || config.mfa.required;
299
367
 
300
- if (mfa_token && hasMFA) {
301
- // Verify TOTP token
302
- mfaVerified = plugin.mfaManager.verifyTOTP(mfaDevices[0].secret, mfa_token);
368
+ if (!usingChallenge && needsMfa) {
369
+ const challengeToken = createMfaChallengeToken(user, rememberChoice);
370
+ if (!challengeToken) {
371
+ return c.redirect(`/login?error=${encodeURIComponent('Unable to start MFA verification. Please contact support.')}`);
372
+ }
303
373
 
304
- if (mfaVerified) {
305
- // Update last used timestamp
306
- await plugin.mfaDevicesResource.patch(mfaDevices[0].id, {
307
- lastUsedAt: new Date().toISOString()
308
- });
374
+ const params = new URLSearchParams({ challenge: challengeToken });
375
+ if (rememberChoice === '1') {
376
+ params.set('remember', '1');
377
+ }
378
+ return c.redirect(`/login/mfa?${params.toString()}`);
379
+ }
309
380
 
310
- // Audit log
311
- await logAudit('mfa_verified', { userId: user.id, method: 'totp' });
381
+ if (usingChallenge && needsMfa) {
382
+ if (!mfa_token && !backup_code) {
383
+ const retryChallenge = createMfaChallengeToken(user, rememberChoice) || mfa_challenge;
384
+ const params = new URLSearchParams({ challenge: retryChallenge });
385
+ params.set('error', 'Multi-factor authentication is required.');
386
+ if (rememberChoice === '1') {
387
+ params.set('remember', '1');
388
+ }
389
+ return c.redirect(`/login/mfa?${params.toString()}`);
390
+ }
312
391
 
313
- if (config.verbose) {
314
- console.log(`[MFA] User ${user.email} verified with TOTP token`);
315
- }
316
- }
317
- } else if (backup_code && hasMFA) {
318
- // Verify backup code
319
- const matchIndex = await plugin.mfaManager.verifyBackupCode(
320
- backup_code,
321
- mfaDevices[0].backupCodes
322
- );
323
-
324
- if (matchIndex !== null && matchIndex >= 0) {
325
- // Remove used backup code
326
- const updatedCodes = [...mfaDevices[0].backupCodes];
327
- updatedCodes.splice(matchIndex, 1);
328
-
329
- await plugin.mfaDevicesResource.patch(mfaDevices[0].id, {
330
- backupCodes: updatedCodes,
331
- lastUsedAt: new Date().toISOString()
332
- });
392
+ let mfaVerified = false;
393
+
394
+ if (mfa_token && hasMFA) {
395
+ mfaVerified = plugin.mfaManager.verifyTOTP(mfaDevices[0].secret, mfa_token);
396
+ if (mfaVerified) {
397
+ await plugin.mfaDevicesResource.patch(mfaDevices[0].id, {
398
+ lastUsedAt: new Date().toISOString()
399
+ });
400
+ await logAudit('mfa_verified', { userId: user.id, method: 'totp' });
401
+ }
402
+ }
333
403
 
334
- mfaVerified = true;
404
+ if (!mfaVerified && backup_code && hasMFA) {
405
+ const matchIndex = await plugin.mfaManager.verifyBackupCode(
406
+ backup_code,
407
+ mfaDevices[0].backupCodes
408
+ );
335
409
 
336
- // Audit log
337
- await logAudit('mfa_verified', { userId: user.id, method: 'backup_code' });
410
+ if (matchIndex !== null && matchIndex >= 0) {
411
+ const updatedCodes = [...mfaDevices[0].backupCodes];
412
+ updatedCodes.splice(matchIndex, 1);
338
413
 
339
- if (config.verbose) {
340
- console.log(`[MFA] User ${user.email} verified with backup code (${updatedCodes.length} codes remaining)`);
341
- }
342
- }
414
+ await plugin.mfaDevicesResource.patch(mfaDevices[0].id, {
415
+ backupCodes: updatedCodes,
416
+ lastUsedAt: new Date().toISOString()
417
+ });
418
+
419
+ mfaVerified = true;
420
+ await logAudit('mfa_verified', { userId: user.id, method: 'backup_code' });
343
421
  }
422
+ }
344
423
 
345
- if (!mfaVerified) {
346
- // MFA verification failed
347
- await logAudit('mfa_failed', { userId: user.id, reason: 'invalid_token' });
424
+ if (!mfaVerified) {
425
+ await logAudit('mfa_failed', { userId: user.id, reason: 'invalid_token' });
348
426
 
349
- if (config.verbose) {
350
- console.log(`[MFA] User ${user.email} MFA verification failed`);
351
- }
427
+ if (failbanManager && failbanConfig.endpoints.login) {
428
+ await failbanManager.recordViolation(clientIp, 'failed_mfa', {
429
+ path: '/login',
430
+ userAgent,
431
+ email: normalizedEmail,
432
+ userId: user.id
433
+ });
434
+ }
352
435
 
353
- return c.redirect(`/login/mfa?error=${encodeURIComponent('Invalid MFA code. Please try again.')}&token=${Buffer.from(JSON.stringify({ userId: user.id, email: user.email, timestamp: Date.now() })).toString('base64')}`);
436
+ const retryChallenge = createMfaChallengeToken(user, rememberChoice) || mfa_challenge;
437
+ const params = new URLSearchParams({
438
+ challenge: retryChallenge,
439
+ error: 'Invalid MFA code. Please try again.'
440
+ });
441
+ if (rememberChoice === '1') {
442
+ params.set('remember', '1');
354
443
  }
444
+ return c.redirect(`/login/mfa?${params.toString()}`);
355
445
  }
356
446
  }
357
447
 
358
- // Get request metadata
359
- const ipAddress = c.req.header('x-forwarded-for')?.split(',')[0].trim() ||
360
- c.req.header('x-real-ip') ||
361
- 'unknown';
362
- const userAgent = c.req.header('user-agent') || 'unknown';
363
-
364
448
  // Create session
365
- const sessionExpiry = remember === '1' ? '30d' : config.session.sessionExpiry;
449
+ const sessionDuration = rememberChoice === '1' ? '30d' : config.session.sessionExpiry;
366
450
  const [okSession, errSession, session] = await tryFn(() =>
367
451
  sessionManager.createSession({
368
452
  userId: user.id,
@@ -371,9 +455,9 @@ export function registerUIRoutes(app, plugin) {
371
455
  name: user.name,
372
456
  isAdmin: user.isAdmin || false
373
457
  },
374
- ipAddress,
458
+ ipAddress: clientIp,
375
459
  userAgent,
376
- expiresIn: sessionExpiry
460
+ duration: sessionDuration
377
461
  })
378
462
  );
379
463
 
@@ -381,21 +465,18 @@ export function registerUIRoutes(app, plugin) {
381
465
  if (config.verbose) {
382
466
  console.error('[Identity Plugin] Failed to create session:', errSession);
383
467
  }
384
- return c.redirect(`/login?error=${encodeURIComponent('Failed to create session. Please try again.')}&email=${encodeURIComponent(email)}`);
468
+ return c.redirect(`/login?error=${encodeURIComponent('Failed to create session. Please try again.')}&email=${encodeURIComponent(normalizedEmail)}`);
385
469
  }
386
470
 
387
- // Set session cookie
388
- sessionManager.setSessionCookie(c, session.id, session.expiresAt);
471
+ sessionManager.setSessionCookie(c, session.sessionId, session.expiresAt);
389
472
 
390
- // Update last login timestamp
391
473
  await tryFn(() =>
392
474
  usersResource.patch(user.id, {
393
475
  lastLoginAt: new Date().toISOString(),
394
- lastLoginIp: ipAddress
476
+ lastLoginIp: clientIp
395
477
  })
396
478
  );
397
479
 
398
- // Redirect to original destination or profile
399
480
  const redirectTo = c.req.query('redirect') || '/profile';
400
481
  return c.redirect(redirectTo);
401
482
 
@@ -405,7 +486,19 @@ export function registerUIRoutes(app, plugin) {
405
486
  }
406
487
  return c.redirect(`/login?error=${encodeURIComponent('An error occurred. Please try again.')}`);
407
488
  }
408
- });
489
+ };
490
+
491
+ const loginLimiter = plugin.rateLimiters?.login;
492
+ if (loginLimiter) {
493
+ const loginRateMiddleware = createRedirectRateLimitMiddleware(
494
+ loginLimiter,
495
+ getClientIp,
496
+ buildRateLimitRedirect
497
+ );
498
+ app.post('/login', loginRateMiddleware, loginHandler);
499
+ } else {
500
+ app.post('/login', loginHandler);
501
+ }
409
502
 
410
503
  // ============================================================================
411
504
  // GET /login/mfa - Show MFA verification page (two-factor authentication)
@@ -416,35 +509,34 @@ export function registerUIRoutes(app, plugin) {
416
509
  }
417
510
 
418
511
  try {
419
- const token = c.req.query('token');
512
+ const challenge = c.req.query('challenge');
420
513
  const remember = c.req.query('remember');
421
514
  const error = c.req.query('error');
422
515
 
423
- if (!token) {
516
+ if (!challenge) {
424
517
  return c.redirect(`/login?error=${encodeURIComponent('Invalid MFA session')}`);
425
518
  }
426
519
 
427
- // Decode temporary token
428
- let userData;
429
- try {
430
- userData = JSON.parse(Buffer.from(token, 'base64').toString('utf-8'));
431
- } catch (err) {
432
- return c.redirect(`/login?error=${encodeURIComponent('Invalid MFA session')}`);
520
+ const payload = await verifyMfaChallengeToken(challenge);
521
+ if (!payload) {
522
+ return c.redirect(`/login?error=${encodeURIComponent('MFA session expired. Please login again.')}`);
433
523
  }
434
524
 
435
- // Check if token is still valid (5 minutes)
436
- const tokenAge = Date.now() - userData.timestamp;
437
- if (tokenAge > 300000) {
438
- return c.redirect(`/login?error=${encodeURIComponent('MFA session expired. Please login again.')}`);
525
+ const [okUser, , challengeUser] = await tryFn(() =>
526
+ usersResource.get(payload.userId)
527
+ );
528
+
529
+ if (!okUser || !challengeUser) {
530
+ return c.redirect(`/login?error=${encodeURIComponent('Your account could not be found. Please login again.')}`);
439
531
  }
440
532
 
441
- // Store email/password in hidden fields for form submission
442
- const mfaToken = JSON.stringify({ email: userData.email, password: userData.password });
533
+ const effectiveRemember = payload.remember || remember === '1';
443
534
 
444
535
  return c.html(MFAVerificationPage({
445
536
  error: error ? decodeURIComponent(error) : null,
446
- token: mfaToken,
447
- remember: remember || '',
537
+ email: challengeUser.email,
538
+ remember: effectiveRemember ? '1' : '0',
539
+ challenge,
448
540
  config: uiConfig
449
541
  }));
450
542
 
@@ -554,23 +646,28 @@ export function registerUIRoutes(app, plugin) {
554
646
  }
555
647
 
556
648
  // Get request metadata
557
- const ipAddress = c.req.header('x-forwarded-for')?.split(',')[0].trim() ||
558
- c.req.header('x-real-ip') ||
559
- 'unknown';
649
+ const ipAddress = getClientIp(c);
650
+
651
+ const initialActive = !config.registration.requireEmailVerification;
652
+ const userRecord = {
653
+ email: normalizedEmail,
654
+ name: name.trim(),
655
+ password, // Auto-encrypted with 'password' type
656
+ isAdmin: false,
657
+ emailVerified: config.registration.requireEmailVerification ? false : true,
658
+ active: initialActive,
659
+ registrationIp: ipAddress,
660
+ lastLoginAt: null,
661
+ lastLoginIp: null
662
+ };
663
+
664
+ if (supportsStatusField) {
665
+ userRecord.status = initialActive ? 'active' : 'pending_verification';
666
+ }
560
667
 
561
668
  // Create user (password will be auto-encrypted by S3DB)
562
669
  const [okUser, errUser, user] = await tryFn(() =>
563
- usersResource.insert({
564
- email: normalizedEmail,
565
- name: name.trim(),
566
- password: password, // Auto-encrypted with 'secret' type
567
- status: 'pending_verification', // Requires email verification
568
- isAdmin: false,
569
- emailVerified: false,
570
- registrationIp: ipAddress,
571
- lastLoginAt: null,
572
- lastLoginIp: null
573
- })
670
+ usersResource.insert(userRecord)
574
671
  );
575
672
 
576
673
  if (!okUser) {
@@ -580,34 +677,35 @@ export function registerUIRoutes(app, plugin) {
580
677
  return c.redirect(`/register?error=${encodeURIComponent('Failed to create account. Please try again.')}&email=${encodeURIComponent(email)}&name=${encodeURIComponent(name)}`);
581
678
  }
582
679
 
583
- // Generate verification token
584
- const verificationToken = generatePasswordResetToken();
585
- const verificationExpiry = calculateExpiration(24); // 24 hours
680
+ let successMessage = 'Account created successfully! You can sign in now.';
586
681
 
587
- // Update user with verification token
588
- await usersResource.update(user.id, {
589
- emailVerificationToken: verificationToken,
590
- emailVerificationExpiry: verificationExpiry
591
- });
682
+ if (config.registration.requireEmailVerification) {
683
+ const verificationToken = generatePasswordResetToken();
684
+ const verificationExpiry = calculateExpiration('24h'); // 24 hours
592
685
 
593
- // Send verification email
594
- if (plugin.emailService) {
595
- try {
596
- await plugin.emailService.sendEmailVerificationEmail({
597
- to: normalizedEmail,
598
- name: name.trim(),
599
- verificationToken
600
- });
601
- } catch (emailError) {
602
- if (config.verbose) {
603
- console.error('[Identity Plugin] Failed to send verification email:', emailError);
686
+ await usersResource.update(user.id, {
687
+ emailVerificationToken: verificationToken,
688
+ emailVerificationExpiry: verificationExpiry
689
+ });
690
+
691
+ if (plugin.emailService) {
692
+ try {
693
+ await plugin.emailService.sendEmailVerificationEmail({
694
+ to: normalizedEmail,
695
+ name: name.trim(),
696
+ verificationToken
697
+ });
698
+ } catch (emailError) {
699
+ if (config.verbose) {
700
+ console.error('[Identity Plugin] Failed to send verification email:', emailError);
701
+ }
604
702
  }
605
- // Don't fail registration if email fails - user can resend later
606
703
  }
704
+
705
+ successMessage = 'Account created successfully! Please check your email to verify your account.';
607
706
  }
608
707
 
609
- // Redirect to login with success message
610
- return c.redirect(`/login?success=${encodeURIComponent('Account created successfully! Please check your email to verify your account.')}&email=${encodeURIComponent(normalizedEmail)}`);
708
+ return c.redirect(`/login?success=${encodeURIComponent(successMessage)}&email=${encodeURIComponent(normalizedEmail)}`);
611
709
 
612
710
  } catch (error) {
613
711
  if (config.verbose) {
@@ -1425,8 +1523,8 @@ export function registerUIRoutes(app, plugin) {
1425
1523
  const now = new Date();
1426
1524
  const stats = {
1427
1525
  totalUsers: users.length,
1428
- activeUsers: users.filter(u => u.status === 'active').length,
1429
- pendingUsers: users.filter(u => u.status === 'pending_verification').length,
1526
+ activeUsers: users.filter(u => supportsStatusField ? u.status === 'active' : u.active !== false).length,
1527
+ pendingUsers: users.filter(u => supportsStatusField ? u.status === 'pending_verification' : (u.active === false && !u.emailVerified)).length,
1430
1528
  totalClients: clients.length,
1431
1529
  activeClients: clients.filter(c => c.active !== false).length,
1432
1530
  activeSessions: sessions.filter(s => new Date(s.expiresAt) > now).length,
@@ -1840,7 +1938,7 @@ export function registerUIRoutes(app, plugin) {
1840
1938
 
1841
1939
  // Only allow status/role changes if not self-editing
1842
1940
  if (!isSelfEdit) {
1843
- if (status) {
1941
+ if (supportsStatusField && status) {
1844
1942
  updates.status = status;
1845
1943
  }
1846
1944
  if (role) {
@@ -1921,6 +2019,10 @@ export function registerUIRoutes(app, plugin) {
1921
2019
  const { status } = body;
1922
2020
  const currentUser = c.get('user');
1923
2021
 
2022
+ if (!supportsStatusField) {
2023
+ return c.redirect(`/admin/users?error=${encodeURIComponent('Status management is not supported for this deployment')}`);
2024
+ }
2025
+
1924
2026
  // Prevent self-status change
1925
2027
  if (userId === currentUser.id) {
1926
2028
  return c.redirect(`/admin/users?error=${encodeURIComponent('You cannot change your own status')}`);
@@ -2404,13 +2506,19 @@ export function registerUIRoutes(app, plugin) {
2404
2506
  }
2405
2507
 
2406
2508
  // Verify the email
2509
+ const verificationUpdate = {
2510
+ emailVerified: true,
2511
+ emailVerificationToken: null,
2512
+ emailVerificationExpiry: null,
2513
+ active: true
2514
+ };
2515
+
2516
+ if (supportsStatusField) {
2517
+ verificationUpdate.status = 'active';
2518
+ }
2519
+
2407
2520
  const [okUpdate, errUpdate] = await tryFn(() =>
2408
- usersResource.update(user.id, {
2409
- emailVerified: true,
2410
- emailVerificationToken: null,
2411
- emailVerificationExpiry: null,
2412
- status: 'active' // Activate account on email verification
2413
- })
2521
+ usersResource.update(user.id, verificationUpdate)
2414
2522
  );
2415
2523
 
2416
2524
  if (!okUpdate) {
@@ -2479,7 +2587,7 @@ export function registerUIRoutes(app, plugin) {
2479
2587
 
2480
2588
  // Generate new verification token
2481
2589
  const verificationToken = generatePasswordResetToken(); // Reuse token generator
2482
- const verificationExpiry = calculateExpiration(24); // 24 hours
2590
+ const verificationExpiry = calculateExpiration('24h'); // 24 hours
2483
2591
 
2484
2592
  // Update user with new token
2485
2593
  const [okUpdate, errUpdate] = await tryFn(() =>