@meith/accounts 0.16.0

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.
@@ -0,0 +1,665 @@
1
+ import type {
2
+ AccountRecord,
3
+ AccountRepository,
4
+ AccountState,
5
+ AccountStore,
6
+ ActiveSessionRecord,
7
+ AuthEventRecord,
8
+ AuthEventRepository,
9
+ CredentialPurpose,
10
+ CredentialTokenRepository,
11
+ LinkIdentityInput,
12
+ LoginAttemptRepository,
13
+ NewAccount,
14
+ NewAuthEvent,
15
+ NewPasskey,
16
+ PasskeyRecord,
17
+ PasskeyRepository,
18
+ RecoveryCodeRepository,
19
+ RememberRotation,
20
+ RememberTokenRepository,
21
+ SessionLocation,
22
+ SessionRecord,
23
+ SessionRepository,
24
+ TwoFactorRecord,
25
+ TwoFactorRepository,
26
+ UserIdentityRecord,
27
+ UserIdentityRepository,
28
+ } from './ports'
29
+ import { withinRotationGrace } from './ports'
30
+
31
+ class MemoryAccounts implements AccountRepository {
32
+ private readonly byId = new Map<number, AccountRecord>()
33
+ private readonly ipPrefixes = new Map<
34
+ number,
35
+ { registration: string | null; lastVisit: string | null }
36
+ >()
37
+ private seq = 0
38
+
39
+ async findById(id: number): Promise<AccountRecord | null> {
40
+ return this.byId.get(id) ?? null
41
+ }
42
+
43
+ async findByUsernameLower(usernameLower: string): Promise<AccountRecord | null> {
44
+ for (const a of this.byId.values()) {
45
+ if (a.usernameLower === usernameLower) return a
46
+ }
47
+ return null
48
+ }
49
+
50
+ async findByEmailLower(emailLower: string): Promise<AccountRecord | null> {
51
+ for (const a of this.byId.values()) {
52
+ if (a.emailLower === emailLower) return a
53
+ }
54
+ return null
55
+ }
56
+
57
+ async create(input: NewAccount): Promise<AccountRecord> {
58
+ const record: AccountRecord = {
59
+ id: ++this.seq,
60
+ username: input.username,
61
+ usernameLower: input.usernameLower,
62
+ email: input.email,
63
+ emailLower: input.emailLower,
64
+ passwordHash: input.passwordHash,
65
+ passwordAlgo: input.passwordAlgo,
66
+ state: input.state,
67
+ emailVerifiedAt: null,
68
+ primaryGroupId: input.primaryGroupId,
69
+ }
70
+ this.byId.set(record.id, record)
71
+ this.ipPrefixes.set(record.id, {
72
+ registration: input.registrationIpPrefix ?? null,
73
+ lastVisit: null,
74
+ })
75
+ return record
76
+ }
77
+
78
+ async recordLastIpPrefix(userId: number, prefix: string): Promise<void> {
79
+ const current = this.ipPrefixes.get(userId)
80
+ if (current === undefined) return
81
+ this.ipPrefixes.set(userId, { ...current, lastVisit: prefix })
82
+ }
83
+
84
+ async updatePassword(userId: number, passwordHash: string, passwordAlgo: string): Promise<void> {
85
+ const cur = this.byId.get(userId)
86
+ if (cur) this.byId.set(userId, { ...cur, passwordHash, passwordAlgo })
87
+ }
88
+
89
+ async setState(userId: number, state: AccountState): Promise<void> {
90
+ const cur = this.byId.get(userId)
91
+ if (cur) this.byId.set(userId, { ...cur, state })
92
+ }
93
+
94
+ async markEmailVerified(
95
+ userId: number,
96
+ at: Date,
97
+ activate: boolean,
98
+ ): Promise<AccountState | null> {
99
+ const cur = this.byId.get(userId)
100
+ if (cur === undefined) return null
101
+
102
+ this.byId.set(userId, {
103
+ ...cur,
104
+ emailVerifiedAt: cur.emailVerifiedAt ?? at,
105
+ state: activate && cur.state === 'awaiting_activation' ? 'active' : cur.state,
106
+ })
107
+
108
+ return cur.state
109
+ }
110
+
111
+ private readonly lastActive = new Map<number, Date>()
112
+
113
+ async touchLastActive(userId: number, now: Date, windowSeconds: number): Promise<boolean> {
114
+ if (!this.byId.has(userId)) return false
115
+
116
+ const previous = this.lastActive.get(userId)
117
+ if (previous !== undefined && now.getTime() - previous.getTime() < windowSeconds * 1000) {
118
+ return false
119
+ }
120
+
121
+ this.lastActive.set(userId, now)
122
+ return true
123
+ }
124
+ }
125
+
126
+ class MemorySessions implements SessionRepository {
127
+ private readonly byId = new Map<number, SessionRecord>()
128
+ private readonly byHash = new Map<string, number>()
129
+ private readonly lastLocationWrite = new Map<number, number>()
130
+ private readonly devices = new Map<
131
+ number,
132
+ { ipPrefix: string | null; userAgent: string | null; createdAt: Date }
133
+ >()
134
+ private seq = 0
135
+
136
+ async create(input: {
137
+ tokenHash: string
138
+ userId: number
139
+ expiresAt: Date
140
+ ipPrefix?: string | null
141
+ userAgent?: string | null
142
+ }): Promise<SessionRecord> {
143
+ const record: SessionRecord = {
144
+ id: ++this.seq,
145
+ userId: input.userId,
146
+ expiresAt: input.expiresAt,
147
+ revokedAt: null,
148
+ supersededBySessionId: null,
149
+ credentialProvedAt: null,
150
+ lastSeenAt: new Date(),
151
+ }
152
+ this.byId.set(record.id, record)
153
+ this.byHash.set(input.tokenHash, record.id)
154
+ this.devices.set(record.id, {
155
+ ipPrefix: input.ipPrefix ?? null,
156
+ userAgent: input.userAgent ?? null,
157
+ createdAt: record.lastSeenAt,
158
+ })
159
+ return record
160
+ }
161
+
162
+ async findByTokenHash(tokenHash: string): Promise<SessionRecord | null> {
163
+ const id = this.byHash.get(tokenHash)
164
+ return id === undefined ? null : (this.byId.get(id) ?? null)
165
+ }
166
+
167
+ async markCredentialProved(sessionId: number, userId: number, at: Date): Promise<boolean> {
168
+ const current = this.byId.get(sessionId)
169
+ if (
170
+ current === undefined ||
171
+ current.userId !== userId ||
172
+ current.revokedAt !== null ||
173
+ current.supersededBySessionId !== null ||
174
+ current.expiresAt.getTime() <= at.getTime()
175
+ ) {
176
+ return false
177
+ }
178
+
179
+ this.byId.set(sessionId, { ...current, credentialProvedAt: at })
180
+ return true
181
+ }
182
+
183
+ async listActiveForUser(userId: number, now: Date): Promise<readonly ActiveSessionRecord[]> {
184
+ return [...this.byId.values()]
185
+ .filter(
186
+ (session) =>
187
+ session.userId === userId &&
188
+ session.revokedAt === null &&
189
+ session.expiresAt.getTime() > now.getTime(),
190
+ )
191
+ .map((session) => {
192
+ const device = this.devices.get(session.id)
193
+ return {
194
+ id: session.id,
195
+ createdAt: device?.createdAt ?? session.lastSeenAt,
196
+ lastSeenAt: session.lastSeenAt,
197
+ expiresAt: session.expiresAt,
198
+ ipPrefix: device?.ipPrefix ?? null,
199
+ userAgent: device?.userAgent ?? null,
200
+ }
201
+ })
202
+ .sort((a, b) => b.lastSeenAt.getTime() - a.lastSeenAt.getTime())
203
+ }
204
+
205
+ async revoke(sessionId: number): Promise<void> {
206
+ const cur = this.byId.get(sessionId)
207
+ if (cur && cur.revokedAt === null) {
208
+ this.byId.set(sessionId, { ...cur, revokedAt: new Date() })
209
+ }
210
+ }
211
+
212
+ async revokeOwned(userId: number, sessionId: number, now: Date): Promise<boolean> {
213
+ const cur = this.byId.get(sessionId)
214
+ if (!cur || cur.userId !== userId || cur.revokedAt !== null) return false
215
+
216
+ this.byId.set(sessionId, { ...cur, revokedAt: now })
217
+ return true
218
+ }
219
+
220
+ async revokeAllForUserExcept(userId: number, sessionId: number | null): Promise<number> {
221
+ let revoked = 0
222
+ const now = new Date()
223
+
224
+ for (const [id, session] of this.byId) {
225
+ if (session.userId !== userId || session.revokedAt !== null) continue
226
+ if (id === sessionId) continue
227
+
228
+ this.byId.set(id, { ...session, revokedAt: now })
229
+ revoked += 1
230
+ }
231
+ return revoked
232
+ }
233
+
234
+ async revokeAllForUser(userId: number): Promise<void> {
235
+ for (const [id, s] of this.byId) {
236
+ if (s.userId === userId && s.revokedAt === null) {
237
+ this.byId.set(id, { ...s, revokedAt: new Date() })
238
+ }
239
+ }
240
+ }
241
+
242
+ async supersede(oldSessionId: number, newSessionId: number, now: Date): Promise<void> {
243
+ const cur = this.byId.get(oldSessionId)
244
+ if (cur) {
245
+ this.byId.set(oldSessionId, {
246
+ ...cur,
247
+ supersededBySessionId: newSessionId,
248
+ revokedAt: cur.revokedAt ?? now,
249
+ })
250
+ }
251
+ }
252
+
253
+ async touchLocation(
254
+ sessionId: number,
255
+ _location: SessionLocation,
256
+ now: Date,
257
+ windowSeconds: number,
258
+ ): Promise<boolean> {
259
+ const cur = this.byId.get(sessionId)
260
+ if (!cur) return false
261
+ const last = this.lastLocationWrite.get(sessionId)
262
+ if (last !== undefined && (now.getTime() - last) / 1000 < windowSeconds) {
263
+ return false
264
+ }
265
+ this.lastLocationWrite.set(sessionId, now.getTime())
266
+ this.byId.set(sessionId, { ...cur, lastSeenAt: now })
267
+ return true
268
+ }
269
+ }
270
+
271
+ interface StoredRemember {
272
+ familyId: string
273
+ userId: number
274
+ expiresAt: Date
275
+ usedAt: Date | null
276
+ revokedAt: Date | null
277
+ }
278
+
279
+ class MemoryRememberTokens implements RememberTokenRepository {
280
+ private readonly byHash = new Map<string, StoredRemember>()
281
+
282
+ async issue(input: {
283
+ tokenHash: string
284
+ familyId: string
285
+ userId: number
286
+ expiresAt: Date
287
+ }): Promise<void> {
288
+ this.byHash.set(input.tokenHash, {
289
+ familyId: input.familyId,
290
+ userId: input.userId,
291
+ expiresAt: input.expiresAt,
292
+ usedAt: null,
293
+ revokedAt: null,
294
+ })
295
+ }
296
+
297
+ async rotate(input: {
298
+ presentedHash: string
299
+ nextHash: string
300
+ now: Date
301
+ nextExpiresAt: Date
302
+ }): Promise<RememberRotation> {
303
+ const t = this.byHash.get(input.presentedHash)
304
+ if (!t) return { status: 'invalid' }
305
+ if (t.expiresAt.getTime() <= input.now.getTime()) return { status: 'invalid' }
306
+ if (t.usedAt !== null || t.revokedAt !== null) {
307
+ if (!withinRotationGrace(t, input.now)) {
308
+ return { status: 'reuse', userId: t.userId, familyId: t.familyId }
309
+ }
310
+ } else {
311
+ this.byHash.set(input.presentedHash, { ...t, usedAt: input.now })
312
+ }
313
+
314
+ this.byHash.set(input.nextHash, {
315
+ familyId: t.familyId,
316
+ userId: t.userId,
317
+ expiresAt: input.nextExpiresAt,
318
+ usedAt: null,
319
+ revokedAt: null,
320
+ })
321
+ return { status: 'rotated', userId: t.userId, familyId: t.familyId }
322
+ }
323
+
324
+ async revokeFamily(familyId: string, _reason: string, now: Date): Promise<void> {
325
+ for (const [hash, t] of this.byHash) {
326
+ if (t.familyId === familyId && t.revokedAt === null) {
327
+ this.byHash.set(hash, { ...t, revokedAt: now })
328
+ }
329
+ }
330
+ }
331
+
332
+ async revokeAllForUser(userId: number, _reason: string, now: Date): Promise<void> {
333
+ for (const [hash, t] of this.byHash) {
334
+ if (t.userId === userId && t.revokedAt === null) {
335
+ this.byHash.set(hash, { ...t, revokedAt: now })
336
+ }
337
+ }
338
+ }
339
+
340
+ async findByTokenHash(tokenHash: string): Promise<{
341
+ familyId: string
342
+ userId: number
343
+ usedAt: Date | null
344
+ revokedAt: Date | null
345
+ } | null> {
346
+ const t = this.byHash.get(tokenHash)
347
+ if (!t) return null
348
+ return { familyId: t.familyId, userId: t.userId, usedAt: t.usedAt, revokedAt: t.revokedAt }
349
+ }
350
+ }
351
+
352
+ interface StoredToken {
353
+ userId: number
354
+ purpose: CredentialPurpose
355
+ payload: string | null
356
+ expiresAt: Date
357
+ consumedAt: Date | null
358
+ }
359
+
360
+ class MemoryCredentialTokens implements CredentialTokenRepository {
361
+ private readonly byHash = new Map<string, StoredToken>()
362
+
363
+ async issue(input: {
364
+ tokenHash: string
365
+ userId: number
366
+ purpose: CredentialPurpose
367
+ payload?: string | null
368
+ expiresAt: Date
369
+ }): Promise<void> {
370
+ this.byHash.set(input.tokenHash, {
371
+ userId: input.userId,
372
+ purpose: input.purpose,
373
+ payload: input.payload ?? null,
374
+ expiresAt: input.expiresAt,
375
+ consumedAt: null,
376
+ })
377
+ }
378
+
379
+ async consume(
380
+ tokenHash: string,
381
+ purpose: CredentialPurpose,
382
+ now: Date,
383
+ ): Promise<{ userId: number; payload: string | null } | null> {
384
+ const found = await this.peek(tokenHash, purpose, now)
385
+ if (found === null) return null
386
+
387
+ const t = this.byHash.get(tokenHash)!
388
+ this.byHash.set(tokenHash, { ...t, consumedAt: now })
389
+ return found
390
+ }
391
+
392
+ async peek(
393
+ tokenHash: string,
394
+ purpose: CredentialPurpose,
395
+ now: Date,
396
+ ): Promise<{ userId: number; payload: string | null } | null> {
397
+ const t = this.byHash.get(tokenHash)
398
+ if (!t) return null
399
+ if (t.purpose !== purpose) return null
400
+ if (t.consumedAt !== null) return null
401
+ if (t.expiresAt.getTime() <= now.getTime()) return null
402
+
403
+ return { userId: t.userId, payload: t.payload }
404
+ }
405
+
406
+ async revokeAllForUser(userId: number, purpose: CredentialPurpose): Promise<void> {
407
+ const now = new Date()
408
+ for (const [hash, t] of this.byHash) {
409
+ if (t.userId === userId && t.purpose === purpose && t.consumedAt === null) {
410
+ this.byHash.set(hash, { ...t, consumedAt: now })
411
+ }
412
+ }
413
+ }
414
+ }
415
+
416
+ interface Attempt {
417
+ succeeded: boolean
418
+ at: Date
419
+ }
420
+
421
+ class MemoryLoginAttempts implements LoginAttemptRepository {
422
+ private readonly buckets = new Map<string, Attempt[]>()
423
+
424
+ async record(bucket: string, succeeded: boolean, at: Date): Promise<void> {
425
+ const list = this.buckets.get(bucket) ?? []
426
+ list.push({ succeeded, at })
427
+ this.buckets.set(bucket, list)
428
+ }
429
+
430
+ async countFailuresSince(bucket: string, since: Date): Promise<number> {
431
+ const list = this.buckets.get(bucket) ?? []
432
+ return list.filter((a) => !a.succeeded && a.at.getTime() >= since.getTime()).length
433
+ }
434
+
435
+ async clear(bucket: string): Promise<void> {
436
+ this.buckets.delete(bucket)
437
+ }
438
+ }
439
+
440
+ class MemoryUserIdentities implements UserIdentityRepository {
441
+ private readonly byId = new Map<number, UserIdentityRecord>()
442
+ private seq = 0
443
+
444
+ async findBySubject(provider: string, subject: string): Promise<UserIdentityRecord | null> {
445
+ for (const record of this.byId.values()) {
446
+ if (record.provider === provider && record.subject === subject) return record
447
+ }
448
+ return null
449
+ }
450
+
451
+ async listForUser(userId: number): Promise<readonly UserIdentityRecord[]> {
452
+ return [...this.byId.values()]
453
+ .filter((record) => record.userId === userId)
454
+ .sort((a, b) => a.id - b.id)
455
+ }
456
+
457
+ async link(input: LinkIdentityInput): Promise<UserIdentityRecord> {
458
+ const existing = await this.findBySubject(input.provider, input.subject)
459
+ if (existing !== null) return existing
460
+
461
+ const record: UserIdentityRecord = {
462
+ id: ++this.seq,
463
+ userId: input.userId,
464
+ provider: input.provider,
465
+ subject: input.subject,
466
+ label: input.label,
467
+ linkedAt: input.now,
468
+ lastUsedAt: null,
469
+ }
470
+ this.byId.set(record.id, record)
471
+ return record
472
+ }
473
+
474
+ async unlink(userId: number, identityId: number): Promise<boolean> {
475
+ const record = this.byId.get(identityId)
476
+ if (record === undefined || record.userId !== userId) return false
477
+ this.byId.delete(identityId)
478
+ return true
479
+ }
480
+
481
+ async markUsed(identityId: number, now: Date): Promise<void> {
482
+ const record = this.byId.get(identityId)
483
+ if (record !== undefined) this.byId.set(identityId, { ...record, lastUsedAt: now })
484
+ }
485
+ }
486
+
487
+ class MemoryPasskeys implements PasskeyRepository {
488
+ private readonly byId = new Map<number, PasskeyRecord>()
489
+ private seq = 0
490
+
491
+ async findByCredentialId(credentialId: string): Promise<PasskeyRecord | null> {
492
+ for (const record of this.byId.values()) {
493
+ if (record.credentialId === credentialId) return record
494
+ }
495
+ return null
496
+ }
497
+
498
+ async listForUser(userId: number): Promise<readonly PasskeyRecord[]> {
499
+ return [...this.byId.values()]
500
+ .filter((record) => record.userId === userId)
501
+ .sort((a, b) => a.id - b.id)
502
+ }
503
+
504
+ async create(input: NewPasskey): Promise<PasskeyRecord> {
505
+ const record: PasskeyRecord = {
506
+ id: ++this.seq,
507
+ userId: input.userId,
508
+ credentialId: input.credentialId,
509
+ publicKey: input.publicKey,
510
+ signCount: input.signCount,
511
+ label: input.label,
512
+ transports: input.transports,
513
+ createdAt: input.now,
514
+ lastUsedAt: null,
515
+ }
516
+ this.byId.set(record.id, record)
517
+ return record
518
+ }
519
+
520
+ async remove(userId: number, passkeyId: number): Promise<boolean> {
521
+ const record = this.byId.get(passkeyId)
522
+ if (record === undefined || record.userId !== userId) return false
523
+ this.byId.delete(passkeyId)
524
+ return true
525
+ }
526
+
527
+ async markUsed(passkeyId: number, signCount: number, now: Date): Promise<void> {
528
+ const record = this.byId.get(passkeyId)
529
+ if (record !== undefined) {
530
+ this.byId.set(passkeyId, { ...record, signCount, lastUsedAt: now })
531
+ }
532
+ }
533
+ }
534
+
535
+ class MemoryTwoFactor implements TwoFactorRepository {
536
+ private readonly byUser = new Map<number, TwoFactorRecord>()
537
+
538
+ async find(userId: number): Promise<TwoFactorRecord | null> {
539
+ return this.byUser.get(userId) ?? null
540
+ }
541
+
542
+ async startEnrolment(input: {
543
+ userId: number
544
+ sealedSecret: string
545
+ now: Date
546
+ }): Promise<TwoFactorRecord> {
547
+ const record: TwoFactorRecord = {
548
+ userId: input.userId,
549
+ sealedSecret: input.sealedSecret,
550
+ confirmedAt: null,
551
+ lastStep: null,
552
+ createdAt: input.now,
553
+ }
554
+ this.byUser.set(input.userId, record)
555
+ return record
556
+ }
557
+
558
+ async confirm(userId: number, step: number, now: Date): Promise<boolean> {
559
+ const record = this.byUser.get(userId)
560
+ if (record === undefined || record.confirmedAt !== null) return false
561
+
562
+ this.byUser.set(userId, { ...record, confirmedAt: now, lastStep: step })
563
+ return true
564
+ }
565
+
566
+ async spendStep(userId: number, step: number): Promise<boolean> {
567
+ const record = this.byUser.get(userId)
568
+ if (record === undefined) return false
569
+ if (record.lastStep !== null && step <= record.lastStep) return false
570
+
571
+ this.byUser.set(userId, { ...record, lastStep: step })
572
+ return true
573
+ }
574
+
575
+ async remove(userId: number): Promise<boolean> {
576
+ return this.byUser.delete(userId)
577
+ }
578
+ }
579
+
580
+ class MemoryRecoveryCodes implements RecoveryCodeRepository {
581
+ private readonly byUser = new Map<number, Map<string, Date | null>>()
582
+
583
+ async replaceAll(userId: number, hashes: readonly string[], _now: Date): Promise<void> {
584
+ this.byUser.set(userId, new Map(hashes.map((hash) => [hash, null])))
585
+ }
586
+
587
+ async spend(userId: number, hash: string, now: Date): Promise<boolean> {
588
+ const codes = this.byUser.get(userId)
589
+ if (codes === undefined) return false
590
+ if (!codes.has(hash) || codes.get(hash) !== null) return false
591
+
592
+ codes.set(hash, now)
593
+ return true
594
+ }
595
+
596
+ async countUnused(userId: number): Promise<number> {
597
+ const codes = this.byUser.get(userId)
598
+ if (codes === undefined) return 0
599
+ return [...codes.values()].filter((usedAt) => usedAt === null).length
600
+ }
601
+
602
+ async removeAll(userId: number): Promise<void> {
603
+ this.byUser.delete(userId)
604
+ }
605
+ }
606
+
607
+ class MemoryAuthEvents implements AuthEventRepository {
608
+ private readonly events: AuthEventRecord[] = []
609
+ private seq = 0
610
+
611
+ async record(event: NewAuthEvent): Promise<void> {
612
+ this.events.push({
613
+ id: ++this.seq,
614
+ userId: event.userId,
615
+ kind: event.kind,
616
+ ipPrefix: event.ipPrefix ?? null,
617
+ userAgent: event.userAgent ?? null,
618
+ detail: event.detail ?? {},
619
+ at: event.at,
620
+ })
621
+ }
622
+
623
+ async listForUser(userId: number, limit: number): Promise<readonly AuthEventRecord[]> {
624
+ return this.events
625
+ .filter((event) => event.userId === userId)
626
+ .sort((a, b) => b.id - a.id)
627
+ .slice(0, limit)
628
+ }
629
+
630
+ async listRecent(input: {
631
+ limit: number
632
+ before?: number | undefined
633
+ kind?: AuthEventRecord['kind'] | undefined
634
+ }): Promise<readonly AuthEventRecord[]> {
635
+ return this.events
636
+ .filter((event) => input.kind === undefined || event.kind === input.kind)
637
+ .filter((event) => input.before === undefined || event.id < input.before)
638
+ .sort((a, b) => b.id - a.id)
639
+ .slice(0, input.limit)
640
+ }
641
+
642
+ async pruneBefore(cutoff: Date, limit = 5000): Promise<number> {
643
+ const doomed = this.events
644
+ .filter((event) => event.at.getTime() < cutoff.getTime())
645
+ .slice(0, limit)
646
+
647
+ for (const event of doomed) this.events.splice(this.events.indexOf(event), 1)
648
+ return doomed.length
649
+ }
650
+ }
651
+
652
+ export function createMemoryStore(): AccountStore {
653
+ return {
654
+ accounts: new MemoryAccounts(),
655
+ sessions: new MemorySessions(),
656
+ tokens: new MemoryCredentialTokens(),
657
+ loginAttempts: new MemoryLoginAttempts(),
658
+ remember: new MemoryRememberTokens(),
659
+ identities: new MemoryUserIdentities(),
660
+ passkeys: new MemoryPasskeys(),
661
+ twoFactor: new MemoryTwoFactor(),
662
+ recoveryCodes: new MemoryRecoveryCodes(),
663
+ authEvents: new MemoryAuthEvents(),
664
+ }
665
+ }