@massimo.mazzoleni/cognito-max 1.0.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.
Files changed (64) hide show
  1. package/README.md +2410 -0
  2. package/dist/chunk-AD7T42HJ.js +3 -0
  3. package/dist/chunk-AD7T42HJ.js.map +1 -0
  4. package/dist/chunk-DKPFVGTY.js +683 -0
  5. package/dist/chunk-DKPFVGTY.js.map +1 -0
  6. package/dist/chunk-N4OQLBV6.js +135 -0
  7. package/dist/chunk-N4OQLBV6.js.map +1 -0
  8. package/dist/client-63FraVdm.d.ts +69 -0
  9. package/dist/client-BAoL8h4E.d.cts +69 -0
  10. package/dist/core/index.cjs +696 -0
  11. package/dist/core/index.cjs.map +1 -0
  12. package/dist/core/index.d.cts +3 -0
  13. package/dist/core/index.d.ts +3 -0
  14. package/dist/core/index.js +4 -0
  15. package/dist/core/index.js.map +1 -0
  16. package/dist/errors-BkUDHleb.d.cts +22 -0
  17. package/dist/errors-BkUDHleb.d.ts +22 -0
  18. package/dist/index.cjs +696 -0
  19. package/dist/index.cjs.map +1 -0
  20. package/dist/index.d.cts +3 -0
  21. package/dist/index.d.ts +3 -0
  22. package/dist/index.js +4 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/react/index.cjs +844 -0
  25. package/dist/react/index.cjs.map +1 -0
  26. package/dist/react/index.d.cts +104 -0
  27. package/dist/react/index.d.ts +104 -0
  28. package/dist/react/index.js +64 -0
  29. package/dist/react/index.js.map +1 -0
  30. package/dist/types-bxA1vonL.d.cts +113 -0
  31. package/dist/types-bxA1vonL.d.ts +113 -0
  32. package/dist/ui/index.cjs +1183 -0
  33. package/dist/ui/index.cjs.map +1 -0
  34. package/dist/ui/index.d.cts +241 -0
  35. package/dist/ui/index.d.ts +241 -0
  36. package/dist/ui/index.js +1109 -0
  37. package/dist/ui/index.js.map +1 -0
  38. package/package.json +81 -0
  39. package/src/core/client.ts +604 -0
  40. package/src/core/errors.ts +91 -0
  41. package/src/core/event-bus.ts +41 -0
  42. package/src/core/index.ts +5 -0
  43. package/src/core/internal/converters.ts +32 -0
  44. package/src/core/storage.ts +79 -0
  45. package/src/core/types.ts +87 -0
  46. package/src/index.ts +1 -0
  47. package/src/react/components/ProtectedRoute.tsx +56 -0
  48. package/src/react/context.tsx +126 -0
  49. package/src/react/hooks/useAuth.ts +75 -0
  50. package/src/react/hooks/useMfa.ts +19 -0
  51. package/src/react/hooks/useSession.ts +16 -0
  52. package/src/react/hooks/useUser.ts +24 -0
  53. package/src/react/index.ts +10 -0
  54. package/src/ui/components/ChangePasswordForm.tsx +105 -0
  55. package/src/ui/components/ForgotPasswordForm.tsx +159 -0
  56. package/src/ui/components/MfaSetupWizard.tsx +136 -0
  57. package/src/ui/components/RegisterForm.tsx +159 -0
  58. package/src/ui/components/SignInForm.tsx +296 -0
  59. package/src/ui/hooks/useChangePasswordForm.ts +81 -0
  60. package/src/ui/hooks/useForgotPasswordForm.ts +109 -0
  61. package/src/ui/hooks/useMfaSetup.ts +93 -0
  62. package/src/ui/hooks/useRegisterForm.ts +120 -0
  63. package/src/ui/hooks/useSignInForm.ts +245 -0
  64. package/src/ui/index.ts +31 -0
@@ -0,0 +1,604 @@
1
+ import {
2
+ AuthenticationDetails,
3
+ CognitoRefreshToken,
4
+ CognitoUser,
5
+ CognitoUserAttribute,
6
+ CognitoUserPool,
7
+ CognitoUserSession,
8
+ } from 'amazon-cognito-identity-js'
9
+ import {
10
+ CognitoIdentityProviderClient,
11
+ GetUserCommand,
12
+ } from '@aws-sdk/client-cognito-identity-provider'
13
+
14
+ import { TypedEventEmitter } from './event-bus'
15
+ import { mapCognitoError, CognitoAuthError, SessionExpiredError } from './errors'
16
+ import { AutoStorageAdapter } from './storage'
17
+ import { buildAuthSession, buildAuthUser } from './internal/converters'
18
+ import type {
19
+ AuthConfig,
20
+ AuthEvents,
21
+ AuthSession,
22
+ AuthState,
23
+ AuthUser,
24
+ MfaPreference,
25
+ MfaSetupResult,
26
+ MfaType,
27
+ ResolvedAuthConfig,
28
+ SignInResult,
29
+ } from './types'
30
+
31
+ function validateConfig(config: AuthConfig): void {
32
+ const missing = (['userPoolId', 'clientId', 'region'] as const).filter(k => !config[k])
33
+ if (missing.length) {
34
+ throw new CognitoAuthError(
35
+ `AuthConfig incompleto — campi obbligatori mancanti: ${missing.join(', ')}`,
36
+ 'INVALID_PARAMETER',
37
+ )
38
+ }
39
+ if (!/^[a-z]{2}-[a-z]+-\d$/.test(config.region)) {
40
+ throw new CognitoAuthError(
41
+ `AuthConfig.region non valido: "${config.region}" (es. eu-west-1)`,
42
+ 'INVALID_PARAMETER',
43
+ )
44
+ }
45
+ }
46
+
47
+
48
+ export class CognitoAuthClient extends TypedEventEmitter<AuthEvents> {
49
+ protected readonly config: ResolvedAuthConfig
50
+ private _state: AuthState = 'idle'
51
+ private readonly _pool: CognitoUserPool
52
+ private _idpClient: CognitoIdentityProviderClient | null = null
53
+ // Mappa challengeSession-id → CognitoUser in-flight (MFA / new-password challenge)
54
+ private readonly _pendingChallenges = new Map<string, CognitoUser>()
55
+ private _refreshTimer: ReturnType<typeof setTimeout> | null = null
56
+
57
+ constructor(config: AuthConfig) {
58
+ super()
59
+ validateConfig(config)
60
+ this.config = {
61
+ autoRefresh: true,
62
+ refreshMarginSeconds: 300,
63
+ totpIssuer: config.clientId,
64
+ storage: new AutoStorageAdapter(),
65
+ ...config,
66
+ }
67
+ this._pool = new CognitoUserPool({
68
+ UserPoolId: this.config.userPoolId,
69
+ ClientId: this.config.clientId,
70
+ // amazon-cognito-identity-js ICognitoStorage coincide con il nostro StorageAdapter
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
+ Storage: this.config.storage as any,
73
+ })
74
+ }
75
+
76
+ // ─── State ─────────────────────────────────────────────────────────────────
77
+
78
+ get state(): AuthState {
79
+ return this._state
80
+ }
81
+
82
+ protected setState(next: AuthState): void {
83
+ if (this._state === next) return
84
+ this._state = next
85
+ this.emit('stateChanged', next)
86
+ }
87
+
88
+ // ─── Sign-in ───────────────────────────────────────────────────────────────
89
+
90
+ async signIn(email: string, password: string): Promise<SignInResult> {
91
+ this.setState('loading')
92
+ const cognitoUser = this._makeCognitoUser(email)
93
+ const authDetails = new AuthenticationDetails({ Username: email, Password: password })
94
+
95
+ return new Promise<SignInResult>((resolve, reject) => {
96
+ cognitoUser.authenticateUser(authDetails, {
97
+ onSuccess: (session) => {
98
+ resolve(this._onAuthSuccess(cognitoUser, session))
99
+ },
100
+ onFailure: (err) => {
101
+ this.setState('unauthenticated')
102
+ reject(mapCognitoError(err))
103
+ },
104
+ mfaRequired: () => {
105
+ this.setState('mfa_required')
106
+ const challengeSession = this._storeChallengeUser(cognitoUser)
107
+ const result: SignInResult = { status: 'MFA_REQUIRED', mfaType: 'SMS', challengeSession }
108
+ this.emit('mfaRequired', { mfaType: 'SMS', challengeSession })
109
+ resolve(result)
110
+ },
111
+ totpRequired: () => {
112
+ this.setState('mfa_required')
113
+ const challengeSession = this._storeChallengeUser(cognitoUser)
114
+ const result: SignInResult = { status: 'MFA_REQUIRED', mfaType: 'TOTP', challengeSession }
115
+ this.emit('mfaRequired', { mfaType: 'TOTP', challengeSession })
116
+ resolve(result)
117
+ },
118
+ newPasswordRequired: (_userAttributes, requiredAttributes) => {
119
+ this.setState('new_password_required')
120
+ const challengeSession = this._storeChallengeUser(cognitoUser)
121
+ const required: string[] = Array.isArray(requiredAttributes) ? requiredAttributes : []
122
+ this.emit('newPasswordRequired', { requiredAttributes: required, challengeSession })
123
+ resolve({ status: 'NEW_PASSWORD_REQUIRED', requiredAttributes: required, challengeSession })
124
+ },
125
+ // Cognito richiede il setup TOTP prima di completare il login
126
+ mfaSetup: () => {
127
+ this.setState('mfa_required')
128
+ const challengeSession = this._storeChallengeUser(cognitoUser)
129
+ resolve({ status: 'MFA_SETUP_REQUIRED', challengeSession })
130
+ },
131
+ })
132
+ })
133
+ }
134
+
135
+ async respondToMfaChallenge(
136
+ challengeSession: string,
137
+ code: string,
138
+ mfaType: MfaType,
139
+ ): Promise<SignInResult> {
140
+ const cognitoUser = this._takeChallengeUser(challengeSession)
141
+ const sdkType = mfaType === 'TOTP' ? 'SOFTWARE_TOKEN_MFA' : 'SMS_MFA'
142
+
143
+ return new Promise<SignInResult>((resolve, reject) => {
144
+ cognitoUser.sendMFACode(
145
+ code,
146
+ {
147
+ onSuccess: (session) => resolve(this._onAuthSuccess(cognitoUser, session)),
148
+ onFailure: (err) => {
149
+ this.setState('unauthenticated')
150
+ reject(mapCognitoError(err))
151
+ },
152
+ },
153
+ sdkType,
154
+ )
155
+ })
156
+ }
157
+
158
+ async respondToNewPasswordChallenge(
159
+ challengeSession: string,
160
+ newPassword: string,
161
+ userAttributes?: Record<string, string>,
162
+ ): Promise<SignInResult> {
163
+ const cognitoUser = this._takeChallengeUser(challengeSession)
164
+
165
+ return new Promise<SignInResult>((resolve, reject) => {
166
+ cognitoUser.completeNewPasswordChallenge(
167
+ newPassword,
168
+ userAttributes ?? {}, // es. { name, family_name, given_name }
169
+ {
170
+ onSuccess: (session) => resolve(this._onAuthSuccess(cognitoUser, session)),
171
+ onFailure: (err) => {
172
+ this.setState('unauthenticated')
173
+ reject(mapCognitoError(err))
174
+ },
175
+ // Cognito può richiedere MFA anche dopo il cambio password forzato
176
+ mfaRequired: () => {
177
+ this.setState('mfa_required')
178
+ const newSession = this._storeChallengeUser(cognitoUser)
179
+ this.emit('mfaRequired', { mfaType: 'SMS', challengeSession: newSession })
180
+ resolve({ status: 'MFA_REQUIRED', mfaType: 'SMS', challengeSession: newSession })
181
+ },
182
+ totpRequired: () => {
183
+ this.setState('mfa_required')
184
+ const newSession = this._storeChallengeUser(cognitoUser)
185
+ this.emit('mfaRequired', { mfaType: 'TOTP', challengeSession: newSession })
186
+ resolve({ status: 'MFA_REQUIRED', mfaType: 'TOTP', challengeSession: newSession })
187
+ },
188
+ },
189
+ )
190
+ })
191
+ }
192
+
193
+ async signOut(global = false): Promise<void> {
194
+ this._clearRefreshTimer()
195
+ this._pendingChallenges.clear()
196
+
197
+ const cognitoUser = this._pool.getCurrentUser()
198
+ if (!cognitoUser) {
199
+ this.setState('unauthenticated')
200
+ this.emit('signedOut')
201
+ return
202
+ }
203
+
204
+ if (global) {
205
+ await new Promise<void>((resolve, reject) => {
206
+ cognitoUser.globalSignOut({
207
+ onSuccess: () => {
208
+ this.setState('unauthenticated')
209
+ this.emit('signedOut')
210
+ resolve()
211
+ },
212
+ onFailure: (err) => reject(mapCognitoError(err)),
213
+ })
214
+ })
215
+ } else {
216
+ cognitoUser.signOut()
217
+ this.setState('unauthenticated')
218
+ this.emit('signedOut')
219
+ }
220
+ }
221
+
222
+ // ─── Registration ──────────────────────────────────────────────────────────
223
+
224
+ async signUp(
225
+ email: string,
226
+ password: string,
227
+ attributes: Record<string, string> = {},
228
+ ): Promise<void> {
229
+ const userAttributes = Object.entries({ email, ...attributes }).map(
230
+ ([Name, Value]) => new CognitoUserAttribute({ Name, Value }),
231
+ )
232
+
233
+ await new Promise<void>((resolve, reject) => {
234
+ this._pool.signUp(email, password, userAttributes, [], (err) => {
235
+ if (err) return reject(mapCognitoError(err))
236
+ resolve()
237
+ })
238
+ })
239
+ }
240
+
241
+ async confirmSignUp(email: string, code: string): Promise<void> {
242
+ const cognitoUser = this._makeCognitoUser(email)
243
+ await new Promise<void>((resolve, reject) => {
244
+ cognitoUser.confirmRegistration(code, true, (err) => {
245
+ if (err) return reject(mapCognitoError(err))
246
+ resolve()
247
+ })
248
+ })
249
+ }
250
+
251
+ async resendConfirmationCode(email: string): Promise<void> {
252
+ const cognitoUser = this._makeCognitoUser(email)
253
+ await new Promise<void>((resolve, reject) => {
254
+ cognitoUser.resendConfirmationCode((err) => {
255
+ if (err) return reject(mapCognitoError(err))
256
+ resolve()
257
+ })
258
+ })
259
+ }
260
+
261
+ // ─── Password ──────────────────────────────────────────────────────────────
262
+
263
+ async forgotPassword(email: string): Promise<void> {
264
+ const cognitoUser = this._makeCognitoUser(email)
265
+ await new Promise<void>((resolve, reject) => {
266
+ cognitoUser.forgotPassword({
267
+ // inputVerificationCode → il codice è stato inviato all'email/telefono
268
+ inputVerificationCode: () => resolve(),
269
+ onSuccess: () => resolve(),
270
+ onFailure: (err) => reject(mapCognitoError(err)),
271
+ })
272
+ })
273
+ }
274
+
275
+ async confirmForgotPassword(
276
+ email: string,
277
+ code: string,
278
+ newPassword: string,
279
+ ): Promise<void> {
280
+ const cognitoUser = this._makeCognitoUser(email)
281
+ await new Promise<void>((resolve, reject) => {
282
+ cognitoUser.confirmPassword(code, newPassword, {
283
+ onSuccess: () => resolve(),
284
+ onFailure: (err) => reject(mapCognitoError(err)),
285
+ })
286
+ })
287
+ }
288
+
289
+ async changePassword(currentPassword: string, newPassword: string): Promise<void> {
290
+ const cognitoUser = await this._getAuthenticatedUser()
291
+ await new Promise<void>((resolve, reject) => {
292
+ cognitoUser.changePassword(currentPassword, newPassword, (err) => {
293
+ if (err) return reject(mapCognitoError(err))
294
+ resolve()
295
+ })
296
+ })
297
+ }
298
+
299
+ // ─── Session & User ────────────────────────────────────────────────────────
300
+
301
+ async getSession(): Promise<AuthSession> {
302
+ const cognitoUser = this._pool.getCurrentUser()
303
+ if (!cognitoUser) throw new SessionExpiredError()
304
+
305
+ const raw = await this._getRawSession(cognitoUser)
306
+ this._scheduleRefresh(cognitoUser, raw)
307
+ return buildAuthSession(raw)
308
+ }
309
+
310
+ async getCurrentUser(): Promise<AuthUser | null> {
311
+ const cognitoUser = this._pool.getCurrentUser()
312
+ if (!cognitoUser) return null
313
+
314
+ try {
315
+ const session = await this._getRawSession(cognitoUser)
316
+ this.setState('authenticated')
317
+ return buildAuthUser(session, cognitoUser.getUsername())
318
+ } catch {
319
+ return null
320
+ }
321
+ }
322
+
323
+ async getUserAttributes(): Promise<Record<string, string>> {
324
+ const cognitoUser = await this._getAuthenticatedUser()
325
+ return new Promise((resolve, reject) => {
326
+ cognitoUser.getUserAttributes((err, result) => {
327
+ if (err) return reject(mapCognitoError(err))
328
+ resolve(Object.fromEntries((result ?? []).map(a => [a.getName(), a.getValue()])))
329
+ })
330
+ })
331
+ }
332
+
333
+ async updateUserAttributes(attributes: Record<string, string>): Promise<void> {
334
+ const cognitoUser = await this._getAuthenticatedUser()
335
+ const attrs = Object.entries(attributes).map(
336
+ ([Name, Value]) => new CognitoUserAttribute({ Name, Value }),
337
+ )
338
+ await new Promise<void>((resolve, reject) => {
339
+ cognitoUser.updateAttributes(attrs, (err) => {
340
+ if (err) return reject(mapCognitoError(err))
341
+ resolve()
342
+ })
343
+ })
344
+ }
345
+
346
+ async verifyUserAttribute(
347
+ attribute: 'email' | 'phone_number',
348
+ code: string,
349
+ ): Promise<void> {
350
+ const cognitoUser = await this._getAuthenticatedUser()
351
+ await new Promise<void>((resolve, reject) => {
352
+ cognitoUser.verifyAttribute(attribute, code, {
353
+ onSuccess: () => resolve(),
354
+ onFailure: (err) => reject(mapCognitoError(err)),
355
+ })
356
+ })
357
+ }
358
+
359
+ async sendAttributeVerificationCode(attribute: 'email' | 'phone_number'): Promise<void> {
360
+ const cognitoUser = await this._getAuthenticatedUser()
361
+ await new Promise<void>((resolve, reject) => {
362
+ cognitoUser.getAttributeVerificationCode(attribute, {
363
+ onSuccess: () => resolve(),
364
+ onFailure: (err) => reject(mapCognitoError(err)),
365
+ inputVerificationCode: () => resolve(),
366
+ })
367
+ })
368
+ }
369
+
370
+ async deleteUser(): Promise<void> {
371
+ const cognitoUser = await this._getAuthenticatedUser()
372
+ this._clearRefreshTimer()
373
+ await new Promise<void>((resolve, reject) => {
374
+ cognitoUser.deleteUser((err) => {
375
+ if (err) return reject(mapCognitoError(err))
376
+ this.setState('unauthenticated')
377
+ this.emit('signedOut')
378
+ resolve()
379
+ })
380
+ })
381
+ }
382
+
383
+ // ─── MFA ───────────────────────────────────────────────────────────────────
384
+
385
+ async setupTotp(): Promise<MfaSetupResult> {
386
+ const cognitoUser = await this._getAuthenticatedUser()
387
+ return new Promise<MfaSetupResult>((resolve, reject) => {
388
+ cognitoUser.associateSoftwareToken({
389
+ associateSecretCode: (secretCode: string) => {
390
+ const issuer = encodeURIComponent(this.config.totpIssuer)
391
+ const account = encodeURIComponent(cognitoUser.getUsername())
392
+ const qrCodeUri =
393
+ `otpauth://totp/${issuer}:${account}` +
394
+ `?secret=${secretCode}&issuer=${issuer}`
395
+ resolve({ secretCode, qrCodeUri })
396
+ },
397
+ onFailure: (err: Error) => reject(mapCognitoError(err)),
398
+ })
399
+ })
400
+ }
401
+
402
+ async verifyTotpSetup(code: string): Promise<void> {
403
+ const cognitoUser = await this._getAuthenticatedUser()
404
+ return new Promise<void>((resolve, reject) => {
405
+ cognitoUser.verifySoftwareToken(code, this.config.totpIssuer, {
406
+ onSuccess: () => resolve(),
407
+ onFailure: (err: Error) => reject(mapCognitoError(err)),
408
+ })
409
+ })
410
+ }
411
+
412
+ async getMfaPreference(): Promise<MfaPreference> {
413
+ const session = await this.getSession()
414
+ try {
415
+ const response = await this._getIdpClient().send(
416
+ new GetUserCommand({ AccessToken: session.accessToken }),
417
+ )
418
+ const enabled: string[] = response.UserMFASettingList ?? []
419
+ const preferred: string | null = response.PreferredMfaSetting ?? null
420
+ const toSdkType = (s: string | null): MfaType | null =>
421
+ s === 'SOFTWARE_TOKEN_MFA' ? 'TOTP' : s === 'SMS_MFA' ? 'SMS' : null
422
+ return {
423
+ enabled: enabled.length > 0,
424
+ preferred: toSdkType(preferred),
425
+ totp: enabled.includes('SOFTWARE_TOKEN_MFA'),
426
+ sms: enabled.includes('SMS_MFA'),
427
+ }
428
+ } catch (err) {
429
+ throw mapCognitoError(err)
430
+ }
431
+ }
432
+
433
+ async setMfaPreference(type: MfaType): Promise<void> {
434
+ const cognitoUser = await this._getAuthenticatedUser()
435
+ const smsMfa = { Enabled: type === 'SMS', PreferredMfa: type === 'SMS' }
436
+ const totpMfa = { Enabled: type === 'TOTP', PreferredMfa: type === 'TOTP' }
437
+ return new Promise<void>((resolve, reject) => {
438
+ cognitoUser.setUserMfaPreference(smsMfa, totpMfa, (err: Error | undefined) => {
439
+ if (err) return reject(mapCognitoError(err))
440
+ resolve()
441
+ })
442
+ })
443
+ }
444
+
445
+ async disableMfa(): Promise<void> {
446
+ const cognitoUser = await this._getAuthenticatedUser()
447
+ const off = { Enabled: false, PreferredMfa: false }
448
+ return new Promise<void>((resolve, reject) => {
449
+ cognitoUser.setUserMfaPreference(off, off, (err: Error | undefined) => {
450
+ if (err) return reject(mapCognitoError(err))
451
+ resolve()
452
+ })
453
+ })
454
+ }
455
+
456
+ // ─── TOTP setup durante il challenge login ─────────────────────────────────
457
+
458
+ /**
459
+ * Ottiene il secretCode/QR URI per il setup TOTP durante il flusso di login
460
+ * (quando signIn restituisce MFA_SETUP_REQUIRED). Non richiede una sessione
461
+ * autenticata: usa il CognitoUser in attesa nel challenge.
462
+ */
463
+ async setupTotpChallenge(challengeSession: string): Promise<MfaSetupResult> {
464
+ const cognitoUser = this._peekChallengeUser(challengeSession)
465
+ return new Promise<MfaSetupResult>((resolve, reject) => {
466
+ cognitoUser.associateSoftwareToken({
467
+ associateSecretCode: (secretCode: string) => {
468
+ const issuer = encodeURIComponent(this.config.totpIssuer)
469
+ const account = encodeURIComponent(cognitoUser.getUsername())
470
+ const qrCodeUri =
471
+ `otpauth://totp/${issuer}:${account}` +
472
+ `?secret=${secretCode}&issuer=${issuer}`
473
+ resolve({ secretCode, qrCodeUri })
474
+ },
475
+ onFailure: (err: Error) => reject(mapCognitoError(err)),
476
+ })
477
+ })
478
+ }
479
+
480
+ /**
481
+ * Verifica il codice TOTP e completa il login. Dopo questa chiamata la
482
+ * sessione è autenticata e il challenge viene rimosso dalla mappa.
483
+ */
484
+ async verifyTotpChallenge(challengeSession: string, code: string): Promise<SignInResult> {
485
+ const cognitoUser = this._takeChallengeUser(challengeSession)
486
+ return new Promise<SignInResult>((resolve, reject) => {
487
+ cognitoUser.verifySoftwareToken(code, this.config.totpIssuer, {
488
+ onSuccess: (session: CognitoUserSession) => resolve(this._onAuthSuccess(cognitoUser, session)),
489
+ onFailure: (err: Error) => reject(mapCognitoError(err)),
490
+ })
491
+ })
492
+ }
493
+
494
+ // ─── Private helpers ───────────────────────────────────────────────────────
495
+
496
+ private _getIdpClient(): CognitoIdentityProviderClient {
497
+ if (!this._idpClient) {
498
+ this._idpClient = new CognitoIdentityProviderClient({ region: this.config.region })
499
+ }
500
+ return this._idpClient
501
+ }
502
+
503
+ private _makeCognitoUser(username: string): CognitoUser {
504
+ return new CognitoUser({
505
+ Username: username,
506
+ Pool: this._pool,
507
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
508
+ Storage: this.config.storage as any,
509
+ })
510
+ }
511
+
512
+ private _storeChallengeUser(cognitoUser: CognitoUser): string {
513
+ const id = crypto.randomUUID()
514
+ this._pendingChallenges.set(id, cognitoUser)
515
+ return id
516
+ }
517
+
518
+ private _takeChallengeUser(challengeSession: string): CognitoUser {
519
+ const user = this._pendingChallenges.get(challengeSession)
520
+ if (!user) {
521
+ throw new CognitoAuthError(
522
+ 'Challenge session non valida o scaduta',
523
+ 'UNKNOWN',
524
+ )
525
+ }
526
+ this._pendingChallenges.delete(challengeSession)
527
+ return user
528
+ }
529
+
530
+ /** Legge il CognitoUser senza rimuoverlo dalla mappa (per setupTotpChallenge). */
531
+ private _peekChallengeUser(challengeSession: string): CognitoUser {
532
+ const user = this._pendingChallenges.get(challengeSession)
533
+ if (!user) {
534
+ throw new CognitoAuthError(
535
+ 'Challenge session non valida o scaduta',
536
+ 'UNKNOWN',
537
+ )
538
+ }
539
+ return user
540
+ }
541
+
542
+ private _onAuthSuccess(cognitoUser: CognitoUser, session: CognitoUserSession): SignInResult {
543
+ const authSession = buildAuthSession(session)
544
+ const authUser = buildAuthUser(session, cognitoUser.getUsername())
545
+ this.setState('authenticated')
546
+ this.emit('signedIn', authUser)
547
+ this._scheduleRefresh(cognitoUser, session)
548
+ return { status: 'SUCCESS', user: authUser, session: authSession }
549
+ }
550
+
551
+ private _getRawSession(cognitoUser: CognitoUser): Promise<CognitoUserSession> {
552
+ return new Promise((resolve, reject) => {
553
+ cognitoUser.getSession((err: Error | null, session: CognitoUserSession | null) => {
554
+ if (err || !session) {
555
+ return reject(err ? mapCognitoError(err) : new SessionExpiredError())
556
+ }
557
+ if (!session.isValid()) {
558
+ return reject(new SessionExpiredError())
559
+ }
560
+ resolve(session)
561
+ })
562
+ })
563
+ }
564
+
565
+ private async _getAuthenticatedUser(): Promise<CognitoUser> {
566
+ const cognitoUser = this._pool.getCurrentUser()
567
+ if (!cognitoUser) throw new SessionExpiredError()
568
+ // getSession gestisce internamente il refresh se il token è scaduto
569
+ await this._getRawSession(cognitoUser)
570
+ return cognitoUser
571
+ }
572
+
573
+ private _scheduleRefresh(cognitoUser: CognitoUser, session: CognitoUserSession): void {
574
+ if (!this.config.autoRefresh) return
575
+ this._clearRefreshTimer()
576
+
577
+ const expiresAt = session.getAccessToken().getExpiration() * 1000
578
+ const refreshIn = expiresAt - Date.now() - this.config.refreshMarginSeconds * 1000
579
+
580
+ if (refreshIn <= 0) return
581
+
582
+ this._refreshTimer = setTimeout(() => {
583
+ const refreshToken = new CognitoRefreshToken({
584
+ RefreshToken: session.getRefreshToken().getToken(),
585
+ })
586
+ cognitoUser.refreshSession(refreshToken, (err, newSession: CognitoUserSession) => {
587
+ if (err) {
588
+ this.emit('sessionExpired')
589
+ this.setState('unauthenticated')
590
+ return
591
+ }
592
+ this.emit('tokenRefreshed', buildAuthSession(newSession))
593
+ this._scheduleRefresh(cognitoUser, newSession)
594
+ })
595
+ }, refreshIn)
596
+ }
597
+
598
+ private _clearRefreshTimer(): void {
599
+ if (this._refreshTimer !== null) {
600
+ clearTimeout(this._refreshTimer)
601
+ this._refreshTimer = null
602
+ }
603
+ }
604
+ }
@@ -0,0 +1,91 @@
1
+ export type AuthErrorCode =
2
+ | 'NOT_AUTHORIZED'
3
+ | 'USER_NOT_FOUND'
4
+ | 'USER_NOT_CONFIRMED'
5
+ | 'INVALID_PARAMETER'
6
+ | 'INVALID_PASSWORD'
7
+ | 'CODE_MISMATCH'
8
+ | 'EXPIRED_CODE'
9
+ | 'CODE_DELIVERY_FAILURE'
10
+ | 'LIMIT_EXCEEDED'
11
+ | 'TOO_MANY_REQUESTS'
12
+ | 'TOO_MANY_FAILED_ATTEMPTS'
13
+ | 'PASSWORD_RESET_REQUIRED'
14
+ | 'MFA_METHOD_NOT_FOUND'
15
+ | 'SOFTWARE_TOKEN_MFA_NOT_FOUND'
16
+ | 'SESSION_EXPIRED'
17
+ | 'NETWORK_ERROR'
18
+ | 'UNKNOWN'
19
+
20
+ export class CognitoAuthError extends Error {
21
+ override readonly name = 'CognitoAuthError'
22
+
23
+ constructor(
24
+ message: string,
25
+ public readonly code: AuthErrorCode,
26
+ public readonly originalError?: unknown,
27
+ ) {
28
+ super(message)
29
+ Object.setPrototypeOf(this, new.target.prototype)
30
+ }
31
+ }
32
+
33
+ export class SessionExpiredError extends CognitoAuthError {
34
+ constructor() {
35
+ super('La sessione è scaduta, effettua nuovamente il login', 'SESSION_EXPIRED')
36
+ Object.setPrototypeOf(this, new.target.prototype)
37
+ }
38
+ }
39
+
40
+ export class NotAuthorizedError extends CognitoAuthError {
41
+ constructor(message = 'Credenziali non valide') {
42
+ super(message, 'NOT_AUTHORIZED')
43
+ Object.setPrototypeOf(this, new.target.prototype)
44
+ }
45
+ }
46
+
47
+ export class UserNotConfirmedError extends CognitoAuthError {
48
+ constructor() {
49
+ super('Account non confermato. Controlla la tua email.', 'USER_NOT_CONFIRMED')
50
+ Object.setPrototypeOf(this, new.target.prototype)
51
+ }
52
+ }
53
+
54
+ export class InvalidCodeError extends CognitoAuthError {
55
+ constructor(expired = false) {
56
+ super(
57
+ expired ? 'Il codice è scaduto' : 'Codice non valido',
58
+ expired ? 'EXPIRED_CODE' : 'CODE_MISMATCH',
59
+ )
60
+ Object.setPrototypeOf(this, new.target.prototype)
61
+ }
62
+ }
63
+
64
+ // Maps raw Cognito SDK error names to our typed errors
65
+ export function mapCognitoError(error: unknown): CognitoAuthError {
66
+ if (error instanceof CognitoAuthError) return error
67
+
68
+ const name: string = (error as any)?.name ?? (error as any)?.code ?? ''
69
+ const message: string = (error as any)?.message ?? 'Si è verificato un errore di autenticazione'
70
+
71
+ const codeMap: Record<string, AuthErrorCode> = {
72
+ NotAuthorizedException: 'NOT_AUTHORIZED',
73
+ UserNotFoundException: 'USER_NOT_FOUND',
74
+ UserNotConfirmedException: 'USER_NOT_CONFIRMED',
75
+ InvalidParameterException: 'INVALID_PARAMETER',
76
+ InvalidPasswordException: 'INVALID_PASSWORD',
77
+ CodeMismatchException: 'CODE_MISMATCH',
78
+ ExpiredCodeException: 'EXPIRED_CODE',
79
+ CodeDeliveryFailureException: 'CODE_DELIVERY_FAILURE',
80
+ LimitExceededException: 'LIMIT_EXCEEDED',
81
+ TooManyRequestsException: 'TOO_MANY_REQUESTS',
82
+ TooManyFailedAttemptsException: 'TOO_MANY_FAILED_ATTEMPTS',
83
+ PasswordResetRequiredException: 'PASSWORD_RESET_REQUIRED',
84
+ MFAMethodNotFoundException: 'MFA_METHOD_NOT_FOUND',
85
+ SoftwareTokenMFANotFoundException: 'SOFTWARE_TOKEN_MFA_NOT_FOUND',
86
+ NetworkError: 'NETWORK_ERROR',
87
+ FetchError: 'NETWORK_ERROR',
88
+ }
89
+
90
+ return new CognitoAuthError(message, codeMap[name] ?? 'UNKNOWN', error)
91
+ }