@clocklobster/cognito-client 1.0.0 → 1.1.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.
- package/README.md +692 -667
- package/dist/index.d.ts +22 -0
- package/dist/index.js +52 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -1,667 +1,692 @@
|
|
|
1
|
-
# cognito-client
|
|
2
|
-
|
|
3
|
-
[](https://opensource.org/licenses/MIT)
|
|
4
|
-
[](https://www.typescriptlang.org/)
|
|
5
|
-
[
|
|
8
|
-
user pools — sign-up, confirmation, sign-in, session restore/refresh, sign-out, forgot/reset
|
|
9
|
-
password, and the `NEW_PASSWORD_REQUIRED` challenge. **No product coupling, no hardcoded
|
|
10
|
-
routes, no default targets — every dependency is injected.**
|
|
11
|
-
|
|
12
|
-
> **Why this exists:** AWS Amplify Auth is heavyweight and opinionated. The raw
|
|
13
|
-
> `amazon-cognito-identity-js` SDK is callback-based and untyped. This client wraps the
|
|
14
|
-
> SDK into a clean, typed, Promise-based surface with every product concern (storage,
|
|
15
|
-
> error messages, navigation, pool config) injected — so you own the UX and policy, and
|
|
16
|
-
> the client owns the Cognito mechanics.
|
|
17
|
-
|
|
18
|
-
---
|
|
19
|
-
|
|
20
|
-
## Table of contents
|
|
21
|
-
|
|
22
|
-
- [Overview](#overview)
|
|
23
|
-
- [Features](#features)
|
|
24
|
-
- [Install](#install)
|
|
25
|
-
- [Quick start](#quick-start)
|
|
26
|
-
- [Configuration](#configuration)
|
|
27
|
-
- [API reference](#api-reference)
|
|
28
|
-
- [`CognitoClient`](#cognitoclient)
|
|
29
|
-
- [`signUp(email, password, attributeList?)`](#signupemail-password-attributelist)
|
|
30
|
-
- [`confirmSignUp(email, code)`](#confirmsignupemail-code)
|
|
31
|
-
- [`signIn(email, password)`](#signinemail-password)
|
|
32
|
-
- [`completeNewPassword(newPassword, userAttributes?)`](#completenewpasswordnewpassword-userattributes)
|
|
33
|
-
- [`getSession()`](#getsession)
|
|
34
|
-
- [`
|
|
35
|
-
- [`
|
|
36
|
-
- [`
|
|
37
|
-
- [`
|
|
38
|
-
- [`
|
|
39
|
-
- [
|
|
40
|
-
- [
|
|
41
|
-
- [
|
|
42
|
-
- [
|
|
43
|
-
- [
|
|
44
|
-
- [
|
|
45
|
-
- [
|
|
46
|
-
- [
|
|
47
|
-
- [
|
|
48
|
-
- [
|
|
49
|
-
- [
|
|
50
|
-
- [
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
- **Sign-
|
|
62
|
-
- **
|
|
63
|
-
- **Session
|
|
64
|
-
- **
|
|
65
|
-
- **
|
|
66
|
-
- **
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
- `
|
|
71
|
-
- `
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
- `
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
- **
|
|
84
|
-
- **
|
|
85
|
-
- **
|
|
86
|
-
- **
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
- **
|
|
117
|
-
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
console.log('
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
| `
|
|
199
|
-
| `
|
|
200
|
-
| `
|
|
201
|
-
| `
|
|
202
|
-
| `
|
|
203
|
-
| `
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
{ Name: '
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
console.log(result.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
- `{ challenge:
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
challenge
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
- On
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
console.log('
|
|
317
|
-
console.log('
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
- On
|
|
328
|
-
- On
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
fires
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
---
|
|
371
|
-
|
|
372
|
-
### `
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
---
|
|
481
|
-
|
|
482
|
-
##
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
```
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
-
|
|
603
|
-
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
---
|
|
607
|
-
|
|
608
|
-
##
|
|
609
|
-
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
```
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
1
|
+
# cognito-client
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://www.typescriptlang.org/)
|
|
5
|
+
[](#testing)
|
|
6
|
+
|
|
7
|
+
A generic, dependency-injected browser client for [AWS Cognito](https://aws.amazon.com/cognito/)
|
|
8
|
+
user pools — sign-up, confirmation, sign-in, session restore/refresh, sign-out, forgot/reset
|
|
9
|
+
password, and the `NEW_PASSWORD_REQUIRED` challenge. **No product coupling, no hardcoded
|
|
10
|
+
routes, no default targets — every dependency is injected.**
|
|
11
|
+
|
|
12
|
+
> **Why this exists:** AWS Amplify Auth is heavyweight and opinionated. The raw
|
|
13
|
+
> `amazon-cognito-identity-js` SDK is callback-based and untyped. This client wraps the
|
|
14
|
+
> SDK into a clean, typed, Promise-based surface with every product concern (storage,
|
|
15
|
+
> error messages, navigation, pool config) injected — so you own the UX and policy, and
|
|
16
|
+
> the client owns the Cognito mechanics.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Table of contents
|
|
21
|
+
|
|
22
|
+
- [Overview](#overview)
|
|
23
|
+
- [Features](#features)
|
|
24
|
+
- [Install](#install)
|
|
25
|
+
- [Quick start](#quick-start)
|
|
26
|
+
- [Configuration](#configuration)
|
|
27
|
+
- [API reference](#api-reference)
|
|
28
|
+
- [`CognitoClient`](#cognitoclient)
|
|
29
|
+
- [`signUp(email, password, attributeList?)`](#signupemail-password-attributelist)
|
|
30
|
+
- [`confirmSignUp(email, code)`](#confirmsignupemail-code)
|
|
31
|
+
- [`signIn(email, password)`](#signinemail-password)
|
|
32
|
+
- [`completeNewPassword(newPassword, userAttributes?)`](#completenewpasswordnewpassword-userattributes)
|
|
33
|
+
- [`getSession()`](#getsession)
|
|
34
|
+
- [`ensureSession(loginUrl)`](#ensuresessionloginurl)
|
|
35
|
+
- [`refreshSession()`](#refreshsession)
|
|
36
|
+
- [`forgotPassword(email)`](#forgotpasswordemail)
|
|
37
|
+
- [`confirmNewPassword(email, code, newPassword)`](#confirmnewpasswordemail-code-newpassword)
|
|
38
|
+
- [`signOut()`](#signout)
|
|
39
|
+
- [`redirectToLogin(loginUrl)`](#redirecttologinloginurl)
|
|
40
|
+
- [Token accessors](#token-accessors)
|
|
41
|
+
- [Types](#types)
|
|
42
|
+
- [Dependency injection](#dependency-injection)
|
|
43
|
+
- [The NEW_PASSWORD_REQUIRED challenge](#the-new_password_required-challenge)
|
|
44
|
+
- [Session persistence and the post-login redirect](#session-persistence-and-the-post-login-redirect)
|
|
45
|
+
- [Security model](#security-model)
|
|
46
|
+
- [Testing](#testing)
|
|
47
|
+
- [Development](#development)
|
|
48
|
+
- [Project layout](#project-layout)
|
|
49
|
+
- [Comparison with Amplify](#comparison-with-amplify)
|
|
50
|
+
- [Contributing](#contributing)
|
|
51
|
+
- [License](#license)
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Overview
|
|
56
|
+
|
|
57
|
+
`cognito-client` provides a product-neutral Cognito lifecycle for browser applications.
|
|
58
|
+
It wraps the `amazon-cognito-identity-js` SDK (loaded as a browser global or injected as
|
|
59
|
+
a mock) behind a typed, Promise-based interface:
|
|
60
|
+
|
|
61
|
+
- **Sign-up** + email confirmation
|
|
62
|
+
- **Sign-in** with `NEW_PASSWORD_REQUIRED` challenge support
|
|
63
|
+
- **Session restore** (survives page reload / post-login redirect)
|
|
64
|
+
- **Session refresh** (uses cached refresh token)
|
|
65
|
+
- **Forgot password** + confirm new password
|
|
66
|
+
- **Sign-out** (clears tokens + SDK session)
|
|
67
|
+
- **Redirect to login** with `?returnTo=` preservation
|
|
68
|
+
|
|
69
|
+
Every product concern is injected:
|
|
70
|
+
- `sdk` — the `amazon-cognito-identity-js` namespace
|
|
71
|
+
- `userPoolId` / `clientId` — pool configuration (plain strings or lazy suppliers)
|
|
72
|
+
- `storage` — the `Storage` used for SDK persistence (bind `sessionStorage` so tokens
|
|
73
|
+
never touch `localStorage`)
|
|
74
|
+
- `errorMapper` — maps SDK errors to your product's error copy
|
|
75
|
+
- `navigate` / `getCurrentPath` — navigation hooks for `redirectToLogin`
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Features
|
|
80
|
+
|
|
81
|
+
- **Fully dependency-injected** — no hardcoded pool IDs, no hardcoded routes, no hardcoded
|
|
82
|
+
error messages. You own the product policy; the client owns the Cognito mechanics.
|
|
83
|
+
- **Promise-based** — wraps the callback-based SDK into clean async/await
|
|
84
|
+
- **TypeScript-native** — full types for every method, option, and SDK interface
|
|
85
|
+
- **NEW_PASSWORD_REQUIRED challenge** — surfaced as a `SignInResult.challenge`, not an error
|
|
86
|
+
- **Session persistence** — tokens survive the post-login redirect via the injected `Storage`
|
|
87
|
+
- **Lazy pool config** — `userPoolId` / `clientId` can be functions resolved at first use
|
|
88
|
+
(for apps that load config at runtime)
|
|
89
|
+
- **Token safety** — runtime tokens live in memory only; the SDK session (refresh token)
|
|
90
|
+
lives in the injected `Storage` (use `sessionStorage` so it clears on tab close)
|
|
91
|
+
- **Product-neutrality tested** — a test asserts the core source contains no product
|
|
92
|
+
roles, routes, or copy
|
|
93
|
+
- **Zero runtime dependencies** — only dev dependencies (TypeScript, Vitest, jsdom)
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Install
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm install @clocklobster/cognito-client
|
|
101
|
+
# or
|
|
102
|
+
pnpm add @clocklobster/cognito-client
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Peer requirement
|
|
106
|
+
|
|
107
|
+
This client wraps [amazon-cognito-identity-js](https://www.npmjs.com/package/amazon-cognito-identity-js).
|
|
108
|
+
Install it in your app and pass the SDK namespace to the client constructor:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npm install amazon-cognito-identity-js
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Requirements
|
|
115
|
+
|
|
116
|
+
- **Browser environment** (uses `Storage`, `window` navigation)
|
|
117
|
+
- **TypeScript >= 5** (for type consumers; ships `.d.ts` files)
|
|
118
|
+
- `amazon-cognito-identity-js` loaded as a browser global or importable module
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Quick start
|
|
123
|
+
|
|
124
|
+
```typescript
|
|
125
|
+
import { CognitoClient } from '@clocklobster/cognito-client';
|
|
126
|
+
// Load the SDK — as a browser global, or via import:
|
|
127
|
+
// import * as AmazonCognitoIdentity from 'amazon-cognito-identity-js';
|
|
128
|
+
|
|
129
|
+
const cognito = new CognitoClient({
|
|
130
|
+
// Pool configuration (plain strings or lazy suppliers)
|
|
131
|
+
userPoolId: 'us-east-1_XXXXXXXXX',
|
|
132
|
+
clientId: 'your-app-client-id',
|
|
133
|
+
|
|
134
|
+
// The amazon-cognito-identity-js namespace
|
|
135
|
+
sdk: AmazonCognitoIdentity,
|
|
136
|
+
|
|
137
|
+
// Storage for the SDK's session (refresh token).
|
|
138
|
+
// Use sessionStorage so tokens never touch localStorage and clear on tab close.
|
|
139
|
+
storage: sessionStorage,
|
|
140
|
+
|
|
141
|
+
// Map SDK errors to your app's error messages
|
|
142
|
+
errorMapper: (err) => {
|
|
143
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
144
|
+
if (msg.includes('NotAuthorizedException')) {
|
|
145
|
+
return new Error('Incorrect email or password.');
|
|
146
|
+
}
|
|
147
|
+
if (msg.includes('UserNotFoundException')) {
|
|
148
|
+
return new Error('No account found with that email.');
|
|
149
|
+
}
|
|
150
|
+
return new Error(msg || 'Authentication failed.');
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
// Navigation (used by redirectToLogin)
|
|
154
|
+
navigate: (url) => (window.location.href = url),
|
|
155
|
+
getCurrentPath: () => window.location.pathname + window.location.search,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Sign up
|
|
159
|
+
await cognito.signUp('user@example.com', 'SecurePassword123!', [
|
|
160
|
+
{ Name: 'email', Value: 'user@example.com' },
|
|
161
|
+
]);
|
|
162
|
+
|
|
163
|
+
// Confirm sign-up
|
|
164
|
+
await cognito.confirmSignUp('user@example.com', '123456');
|
|
165
|
+
|
|
166
|
+
// Sign in
|
|
167
|
+
const result = await cognito.signIn('user@example.com', 'SecurePassword123!');
|
|
168
|
+
if (result.challenge === 'NEW_PASSWORD_REQUIRED') {
|
|
169
|
+
// Collect a new password from the user, then:
|
|
170
|
+
const tokens = await cognito.completeNewPassword('NewSecurePassword456!', {
|
|
171
|
+
// Optional: required attribute updates from the challenge
|
|
172
|
+
name: 'Jane',
|
|
173
|
+
});
|
|
174
|
+
console.log('Signed in:', tokens.idToken);
|
|
175
|
+
} else {
|
|
176
|
+
console.log('Signed in:', result.idToken);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// On page load (e.g. in your app bootstrap), restore the session:
|
|
180
|
+
const session = await cognito.getSession();
|
|
181
|
+
if (session) {
|
|
182
|
+
console.log('Restored session for', session.user);
|
|
183
|
+
console.log('ID token:', session.idToken);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Sign out
|
|
187
|
+
cognito.signOut();
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## Configuration
|
|
193
|
+
|
|
194
|
+
### `CognitoClientOptions`
|
|
195
|
+
|
|
196
|
+
| Option | Type | Required | Description |
|
|
197
|
+
|---|---|---|---|
|
|
198
|
+
| `userPoolId` | `string \| (() => string)` | yes | Cognito User Pool ID (e.g. `us-east-1_XXXXX`). Can be a lazy supplier resolved at first auth operation. |
|
|
199
|
+
| `clientId` | `string \| (() => string)` | yes | Cognito App Client ID. Can be a lazy supplier. |
|
|
200
|
+
| `sdk` | `CognitoSdk` | yes | The `amazon-cognito-identity-js` namespace (browser global or imported module). |
|
|
201
|
+
| `storage` | `Storage \| (() => Storage \| undefined)` | yes | The `Storage` used for ALL SDK persistence. Bind `sessionStorage` so tokens never touch `localStorage`. Can be a lazy supplier. |
|
|
202
|
+
| `errorMapper` | `(err: unknown) => Error` | yes | Maps SDK errors to your app's error messages. The SDK throws opaque errors; this is your chance to translate them. |
|
|
203
|
+
| `navigate` | `(url: string) => void` | yes | Navigation function used by `redirectToLogin`. Typically `(url) => window.location.href = url`. |
|
|
204
|
+
| `getCurrentPath` | `() => string` | no | Returns the current page path + query, used to build `?returnTo=`. Defaults to empty string (no returnTo). |
|
|
205
|
+
|
|
206
|
+
### Lazy suppliers
|
|
207
|
+
|
|
208
|
+
`userPoolId`, `clientId`, and `storage` accept either a plain value or a function. The
|
|
209
|
+
function is resolved at first auth operation (not at construction), so apps that load
|
|
210
|
+
config at runtime (e.g. from a fetched config endpoint) can supply a lazy supplier:
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
const cognito = new CognitoClient({
|
|
214
|
+
userPoolId: () => appConfig.cognito.userPoolId,
|
|
215
|
+
clientId: () => appConfig.cognito.clientId,
|
|
216
|
+
// ...
|
|
217
|
+
});
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## API reference
|
|
223
|
+
|
|
224
|
+
### `CognitoClient`
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { CognitoClient } from '@clocklobster/cognito-client';
|
|
228
|
+
|
|
229
|
+
const cognito = new CognitoClient(options);
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
### `signUp(email, password, attributeList?)`
|
|
235
|
+
|
|
236
|
+
Registers a new user in the Cognito user pool.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
const result = await cognito.signUp('user@example.com', 'Password123!', [
|
|
240
|
+
{ Name: 'email', Value: 'user@example.com' },
|
|
241
|
+
{ Name: 'phone_number', Value: '+14165551234' },
|
|
242
|
+
]);
|
|
243
|
+
// result: { userConfirmed: boolean, userSub: string }
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
- `attributeList` — array of `{ Name, Value }` Cognito attributes (defaults to `[]`)
|
|
247
|
+
- Returns `{ userConfirmed, userSub }` — `userConfirmed` is `false` if email/SMS
|
|
248
|
+
verification is required before sign-in
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
### `confirmSignUp(email, code)`
|
|
253
|
+
|
|
254
|
+
Confirms a sign-up using the verification code sent to the user's email/SMS.
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
await cognito.confirmSignUp('user@example.com', '123456');
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
### `signIn(email, password)`
|
|
263
|
+
|
|
264
|
+
Authenticates a user. Returns either tokens (success) or a `NEW_PASSWORD_REQUIRED`
|
|
265
|
+
challenge that must be completed via `completeNewPassword()`.
|
|
266
|
+
|
|
267
|
+
```typescript
|
|
268
|
+
const result = await cognito.signIn('user@example.com', 'Password123!');
|
|
269
|
+
|
|
270
|
+
if (result.challenge === null) {
|
|
271
|
+
// Success — tokens are available
|
|
272
|
+
console.log(result.idToken, result.accessToken);
|
|
273
|
+
} else if (result.challenge === 'NEW_PASSWORD_REQUIRED') {
|
|
274
|
+
// User must set a new permanent password
|
|
275
|
+
console.log(result.userAttributes);
|
|
276
|
+
console.log(result.requiredAttributes);
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Returns:** `SignInResult` — either:
|
|
281
|
+
- `{ challenge: null, idToken: string, accessToken: string }`, or
|
|
282
|
+
- `{ challenge: 'NEW_PASSWORD_REQUIRED', userAttributes, requiredAttributes }`
|
|
283
|
+
|
|
284
|
+
On success, the session is persisted to the injected `Storage` so it survives the
|
|
285
|
+
post-login redirect.
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### `completeNewPassword(newPassword, userAttributes?)`
|
|
290
|
+
|
|
291
|
+
Completes a `NEW_PASSWORD_REQUIRED` challenge issued by `signIn()`. Throws if no
|
|
292
|
+
challenge is in flight.
|
|
293
|
+
|
|
294
|
+
```typescript
|
|
295
|
+
const tokens = await cognito.completeNewPassword('NewPassword456!', {
|
|
296
|
+
name: 'Jane Doe', // optional required attribute updates
|
|
297
|
+
});
|
|
298
|
+
// tokens: { idToken, accessToken }
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
- `userAttributes` — any required attribute updates from the challenge. The `sub`
|
|
302
|
+
attribute is automatically scrubbed (Cognito rejects resending it).
|
|
303
|
+
- On success, tokens are stored and the challenge state is cleared.
|
|
304
|
+
- On failure, the challenge state is cleared and the user must sign in again.
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
### `getSession()`
|
|
309
|
+
|
|
310
|
+
Restores a cached session from the injected `Storage`. Used on page load to check
|
|
311
|
+
if the user is already authenticated.
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
const session = await cognito.getSession();
|
|
315
|
+
if (session) {
|
|
316
|
+
console.log('User:', session.user);
|
|
317
|
+
console.log('ID token:', session.idToken);
|
|
318
|
+
console.log('Access token:', session.accessToken);
|
|
319
|
+
} else {
|
|
320
|
+
// Not authenticated — redirect to login
|
|
321
|
+
cognito.redirectToLogin('/login.html');
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Returns:** `RestoredSession | null`
|
|
326
|
+
|
|
327
|
+
- On success: `{ idToken, accessToken, user }`
|
|
328
|
+
- On failure (stale/invalid session): signs out the SDK user, clears tokens, returns `null`
|
|
329
|
+
- On synchronous SDK throw (no cached refresh token): clears tokens, returns `null`
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
### `ensureSession(loginUrl)`
|
|
334
|
+
|
|
335
|
+
Canonical async page-load gate for protected pages. Unlike a sync token-presence
|
|
336
|
+
check (which passes with a stale-but-cached token after credentials timeout,
|
|
337
|
+
letting the page fetch and render private data before the 401 path discovers
|
|
338
|
+
the dead session), this validates the session through the SDK first.
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
const session = await cognito.ensureSession('/login.html');
|
|
342
|
+
if (!session) return; // dead session — already redirected to login
|
|
343
|
+
// live session (refreshed via the stored refresh token when needed)
|
|
344
|
+
console.log('ID token:', session.idToken);
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Returns:** `RestoredSession | null` — never throws.
|
|
348
|
+
|
|
349
|
+
- Live session: returned as `{ idToken, accessToken, user }`, refreshing via
|
|
350
|
+
the stored refresh token when the id token expired but the refresh token is
|
|
351
|
+
still alive (seamless, no redirect).
|
|
352
|
+
- Dead session: signs out the stale SDK state, redirects to `loginUrl` (with
|
|
353
|
+
`?returnTo=` preservation), returns `null` — before any API fetch fires.
|
|
354
|
+
|
|
355
|
+
### `refreshSession()`
|
|
356
|
+
|
|
357
|
+
Refreshes the session using the cached refresh token. Safe to call even when the SDK's
|
|
358
|
+
`signInUserSession` has not been loaded into memory yet (e.g. an API call races the
|
|
359
|
+
page's own `getSession()` call).
|
|
360
|
+
|
|
361
|
+
```typescript
|
|
362
|
+
const tokens = await cognito.refreshSession();
|
|
363
|
+
// tokens: { idToken, accessToken }
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
**Returns:** `SessionTokens` — `{ idToken, accessToken }`
|
|
367
|
+
|
|
368
|
+
**Throws:** if there is no cached session or the refresh token is invalid.
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
### `forgotPassword(email)`
|
|
373
|
+
|
|
374
|
+
Initiates the forgot-password flow. Cognito sends a verification code to the user's
|
|
375
|
+
email/SMS. Resolves when the code has been sent (the `inputVerificationCode` callback
|
|
376
|
+
fires).
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
await cognito.forgotPassword('user@example.com');
|
|
380
|
+
// Now prompt the user for the code + new password
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
### `confirmNewPassword(email, code, newPassword)`
|
|
386
|
+
|
|
387
|
+
Completes the forgot-password flow by submitting the verification code and a new password.
|
|
388
|
+
|
|
389
|
+
```typescript
|
|
390
|
+
await cognito.confirmNewPassword('user@example.com', '123456', 'NewPassword789!');
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
### `signOut()`
|
|
396
|
+
|
|
397
|
+
Signs out the current user from the SDK and clears all token/challenge state.
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
cognito.signOut();
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
Clears:
|
|
404
|
+
- `idToken`, `accessToken`, `currentUser`
|
|
405
|
+
- `pendingChallengeUser` (any in-flight `NEW_PASSWORD_REQUIRED` challenge)
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
### `redirectToLogin(loginUrl)`
|
|
410
|
+
|
|
411
|
+
Signs out any stale Cognito session, then navigates to `loginUrl` with the current page
|
|
412
|
+
preserved as `?returnTo=` so the login page can send the user back after sign-in.
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
cognito.redirectToLogin('/login.html');
|
|
416
|
+
// Navigates to: /login.html?returnTo=%2Fdashboard%3Ftab%3Dsettings
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
- `loginUrl` — the login page URL (product policy — the adapter owns this)
|
|
420
|
+
- Signs out first so the login form doesn't pick up a cached user whose tokens are dead
|
|
421
|
+
- Uses `getCurrentPath()` (if provided) to build the `?returnTo=` query parameter
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
### Token accessors
|
|
426
|
+
|
|
427
|
+
| Method | Returns | Description |
|
|
428
|
+
|---|---|---|
|
|
429
|
+
| `getUser()` | `string \| null` | The current user's username (or `null` if not signed in) |
|
|
430
|
+
| `getIdToken()` | `string \| null` | The current Cognito ID token JWT (or `null`) |
|
|
431
|
+
| `getAccessToken()` | `string \| null` | The current Cognito access token JWT (or `null`) |
|
|
432
|
+
|
|
433
|
+
These return `null` when no session is active. They read from in-memory state set by
|
|
434
|
+
`signIn()`, `completeNewPassword()`, `getSession()`, or `refreshSession()`.
|
|
435
|
+
|
|
436
|
+
---
|
|
437
|
+
|
|
438
|
+
### Types
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
// SignIn result — either tokens (success) or a challenge
|
|
442
|
+
type SignInResult =
|
|
443
|
+
| { challenge: null; idToken: string; accessToken: string }
|
|
444
|
+
| {
|
|
445
|
+
challenge: 'NEW_PASSWORD_REQUIRED';
|
|
446
|
+
userAttributes: Record<string, unknown>;
|
|
447
|
+
requiredAttributes: Record<string, unknown>;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
// Session tokens
|
|
451
|
+
interface SessionTokens {
|
|
452
|
+
idToken: string;
|
|
453
|
+
accessToken: string;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Restored session (from getSession)
|
|
457
|
+
interface RestoredSession extends SessionTokens {
|
|
458
|
+
user: string;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// The SDK surface the client uses (inject amazon-cognito-identity-js)
|
|
462
|
+
interface CognitoSdk {
|
|
463
|
+
CognitoUserPool: new (data: { UserPoolId: string; ClientId: string; Storage?: Storage }) => CognitoUserPoolLike;
|
|
464
|
+
CognitoUser: new (data: { Username: string; Pool: CognitoUserPoolLike; Storage?: Storage }) => CognitoUserLike;
|
|
465
|
+
AuthenticationDetails: new (data: { Username: string; Password: string }) => unknown;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Client options
|
|
469
|
+
interface CognitoClientOptions {
|
|
470
|
+
userPoolId: string | (() => string);
|
|
471
|
+
clientId: string | (() => string);
|
|
472
|
+
sdk: CognitoSdk;
|
|
473
|
+
storage: Storage | (() => Storage | undefined);
|
|
474
|
+
errorMapper: (err: unknown) => Error;
|
|
475
|
+
navigate: (url: string) => void;
|
|
476
|
+
getCurrentPath?: () => string;
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
---
|
|
481
|
+
|
|
482
|
+
## Dependency injection
|
|
483
|
+
|
|
484
|
+
Every dependency is injected — the client has zero hardcoded values:
|
|
485
|
+
|
|
486
|
+
| Dependency | Purpose | Typical binding |
|
|
487
|
+
|---|---|---|
|
|
488
|
+
| `sdk` | The `amazon-cognito-identity-js` namespace | Browser global or `import * as` |
|
|
489
|
+
| `userPoolId` | Cognito User Pool ID | String from config/env |
|
|
490
|
+
| `clientId` | Cognito App Client ID | String from config/env |
|
|
491
|
+
| `storage` | SDK session persistence | `sessionStorage` (never `localStorage`) |
|
|
492
|
+
| `errorMapper` | SDK error → user-facing error | Your app's error message map |
|
|
493
|
+
| `navigate` | Page navigation for `redirectToLogin` | `(url) => window.location.href = url` |
|
|
494
|
+
| `getCurrentPath` | Current path for `?returnTo=` | `() => window.location.pathname + window.location.search` |
|
|
495
|
+
|
|
496
|
+
This means:
|
|
497
|
+
- **No hardcoded pool IDs** — different environments (dev/staging/prod) inject different pools
|
|
498
|
+
- **No hardcoded error messages** — your app owns the UX copy
|
|
499
|
+
- **No hardcoded routes** — your app owns the login URL and redirect logic
|
|
500
|
+
- **No hardcoded storage** — bind `sessionStorage` for tab-scoped sessions, or a custom
|
|
501
|
+
`Storage` implementation for testing
|
|
502
|
+
|
|
503
|
+
---
|
|
504
|
+
|
|
505
|
+
## The NEW_PASSWORD_REQUIRED challenge
|
|
506
|
+
|
|
507
|
+
When a user signs in and Cognito requires a new permanent password (e.g. admin-created/
|
|
508
|
+
invited users in `FORCE_CHANGE_PASSWORD` state), `signIn()` does **not** throw an error.
|
|
509
|
+
Instead, it returns a challenge result:
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
const result = await cognito.signIn(email, tempPassword);
|
|
513
|
+
|
|
514
|
+
if (result.challenge === 'NEW_PASSWORD_REQUIRED') {
|
|
515
|
+
// The user is authenticated but must set a new password.
|
|
516
|
+
// Show a "set new password" form, then:
|
|
517
|
+
const tokens = await cognito.completeNewPassword(newPassword, {
|
|
518
|
+
name: 'Jane', // optional required attributes
|
|
519
|
+
});
|
|
520
|
+
// tokens.idToken / tokens.accessToken are now available
|
|
521
|
+
}
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
Key details:
|
|
525
|
+
- The `CognitoUser` reference is held internally during the challenge — you don't need
|
|
526
|
+
to re-authenticate to complete it
|
|
527
|
+
- The `sub` attribute is automatically scrubbed from `userAttributes` (Cognito rejects
|
|
528
|
+
resending it — it's read-only/server-managed)
|
|
529
|
+
- On success, tokens are stored and the challenge state is cleared
|
|
530
|
+
- On failure, the challenge state is cleared and the user must sign in again
|
|
531
|
+
|
|
532
|
+
---
|
|
533
|
+
|
|
534
|
+
## Session persistence and the post-login redirect
|
|
535
|
+
|
|
536
|
+
Cognito's SDK persists the session (refresh token) to the injected `Storage`. This
|
|
537
|
+
client binds `sessionStorage` by convention so:
|
|
538
|
+
|
|
539
|
+
- **Runtime tokens live in memory only** — `idToken` / `accessToken` are never written
|
|
540
|
+
to storage; they're held in the `CognitoClient` instance
|
|
541
|
+
- **The SDK session (refresh token) lives in `sessionStorage`** — so it survives the
|
|
542
|
+
post-login redirect but clears when the tab closes
|
|
543
|
+
- **On page load**, call `getSession()` to restore the session from `sessionStorage`
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
// App bootstrap (every page load):
|
|
547
|
+
const session = await cognito.getSession();
|
|
548
|
+
if (session) {
|
|
549
|
+
// User is authenticated — render the app
|
|
550
|
+
initApp(session);
|
|
551
|
+
} else {
|
|
552
|
+
// Not authenticated — redirect to login
|
|
553
|
+
cognito.redirectToLogin('/login.html');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Login page (after successful signIn):
|
|
557
|
+
const result = await cognito.signIn(email, password);
|
|
558
|
+
if (result.challenge === null) {
|
|
559
|
+
// Read ?returnTo= from the URL and navigate there
|
|
560
|
+
const returnTo = new URLSearchParams(window.location.search).get('returnTo') || '/';
|
|
561
|
+
window.location.href = returnTo;
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## Security model
|
|
568
|
+
|
|
569
|
+
- **Runtime tokens in memory only** — `idToken` and `accessToken` are never written to
|
|
570
|
+
`Storage`. They live in the `CognitoClient` instance and are cleared on sign-out or
|
|
571
|
+
terminal failure.
|
|
572
|
+
- **SDK session in `sessionStorage`** — the refresh token persists in the injected
|
|
573
|
+
`Storage` (bind `sessionStorage`, not `localStorage`, so it clears on tab close).
|
|
574
|
+
- **Terminal failure clears state** — a stale/invalid cached session triggers `signOut()`
|
|
575
|
+
+ `clearTokens()`, so no dead token state survives.
|
|
576
|
+
- **Sign-out is thorough** — calls `cognitoUser.signOut()` on the SDK AND clears all
|
|
577
|
+
in-memory token/challenge state.
|
|
578
|
+
- **`redirectToLogin` signs out first** — so the login form doesn't pick up a cached
|
|
579
|
+
user whose tokens are dead.
|
|
580
|
+
- **`sub` is scrubbed** — `completeNewPassword` strips the `sub` attribute from
|
|
581
|
+
`userAttributes` before sending (Cognito rejects resending it).
|
|
582
|
+
|
|
583
|
+
---
|
|
584
|
+
|
|
585
|
+
## Testing
|
|
586
|
+
|
|
587
|
+
The suite uses [Vitest](https://vitest.dev/) with a `jsdom` environment and a mock SDK.
|
|
588
|
+
41 tests across 5 describe blocks:
|
|
589
|
+
|
|
590
|
+
| Describe block | Tests | Coverage |
|
|
591
|
+
|---|---|---|
|
|
592
|
+
| `CognitoClient - dependency injection` | 5 | Lazy pool config, lazy storage, SDK injection, error mapper |
|
|
593
|
+
| `CognitoClient - signIn / session` | 9 | signIn success, NEW_PASSWORD_REQUIRED challenge, completeNewPassword, getSession, refreshSession, forgotPassword, confirmNewPassword |
|
|
594
|
+
| `CognitoClient - sign-out / navigation` | 3 | signOut, redirectToLogin with ?returnTo=, stale session cleanup |
|
|
595
|
+
| `CognitoClient - product neutrality` | 2 | No product roles/routes on the prototype, no product terms in source |
|
|
596
|
+
| `CognitoClient - property tests` | 22 | Invariants over generated inputs (tokens, scrubbing, lazy config, redirect, sign-up, errors) |
|
|
597
|
+
|
|
598
|
+
```bash
|
|
599
|
+
npm test # vitest run (jsdom, mock SDK — no real Cognito calls)
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
The **product-neutrality test** asserts that the `CognitoClient` source contains no
|
|
603
|
+
product-specific terms (roles, routes, copy) — this guarantees the core stays generic
|
|
604
|
+
as it evolves.
|
|
605
|
+
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## Development
|
|
609
|
+
|
|
610
|
+
```bash
|
|
611
|
+
# Install dependencies
|
|
612
|
+
pnpm install
|
|
613
|
+
|
|
614
|
+
# Typecheck
|
|
615
|
+
pnpm run typecheck # tsc --noEmit
|
|
616
|
+
|
|
617
|
+
# Run tests (jsdom + mock SDK — no real Cognito)
|
|
618
|
+
pnpm test # vitest run
|
|
619
|
+
|
|
620
|
+
# Build (emit to dist/)
|
|
621
|
+
pnpm run build # tsc -p tsconfig.build.json
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Requirements
|
|
625
|
+
|
|
626
|
+
- Node.js >= 18
|
|
627
|
+
- pnpm (or npm/yarn — the package has no runtime dependencies)
|
|
628
|
+
- TypeScript >= 5
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
632
|
+
## Project layout
|
|
633
|
+
|
|
634
|
+
```text
|
|
635
|
+
cognito-client/
|
|
636
|
+
├── src/
|
|
637
|
+
│ └── index.ts # CognitoClient + all types (single file, ~400 lines)
|
|
638
|
+
├── test/
|
|
639
|
+
│ ├── cognito-client.test.ts # 19 unit tests
|
|
640
|
+
│ └── cognito-client.property.test.ts # 22 property tests
|
|
641
|
+
├── package.json
|
|
642
|
+
├── tsconfig.json
|
|
643
|
+
├── tsconfig.build.json
|
|
644
|
+
├── vitest.config.ts
|
|
645
|
+
├── LICENSE
|
|
646
|
+
└── README.md
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
---
|
|
650
|
+
|
|
651
|
+
## Comparison with Amplify
|
|
652
|
+
|
|
653
|
+
| | `cognito-client` | AWS Amplify Auth |
|
|
654
|
+
|---|---|---|
|
|
655
|
+
| **Dependencies** | Zero runtime (you inject the SDK) | Heavyweight (~50 deps) |
|
|
656
|
+
| **Bundle size** | ~4KB (your code only) | ~100KB+ |
|
|
657
|
+
| **Error messages** | You own them (`errorMapper`) | Amplify's defaults |
|
|
658
|
+
| **Routes** | You own them (`navigate`) | Amplify's hosted UI / config |
|
|
659
|
+
| **Storage** | You choose (`sessionStorage` recommended) | Amplify's `localStorage` default |
|
|
660
|
+
| **Pool config** | String or lazy supplier | Static config at init |
|
|
661
|
+
| **NEW_PASSWORD_REQUIRED** | First-class challenge result | Handled internally |
|
|
662
|
+
| **Product coupling** | None (tested) | Amplify ecosystem assumptions |
|
|
663
|
+
| **TypeScript** | Full types, strict | Full types |
|
|
664
|
+
|
|
665
|
+
**When to use `cognito-client`:** you want a thin, typed, dependency-injected Cognito
|
|
666
|
+
wrapper that you fully control. You own the UX, the error messages, the storage strategy,
|
|
667
|
+
and the routing.
|
|
668
|
+
|
|
669
|
+
**When to use Amplify:** you want a batteries-included auth solution with hosted UI,
|
|
670
|
+
social providers, MFA, and the full Amplify ecosystem — and you're OK with the bundle
|
|
671
|
+
size and opinionated defaults.
|
|
672
|
+
|
|
673
|
+
---
|
|
674
|
+
|
|
675
|
+
## Contributing
|
|
676
|
+
|
|
677
|
+
Pull requests are welcome.
|
|
678
|
+
|
|
679
|
+
### Guidelines
|
|
680
|
+
|
|
681
|
+
1. Add or update tests for any change (Vitest, jsdom, mock SDK).
|
|
682
|
+
2. Ensure `pnpm run typecheck` and `pnpm test` pass.
|
|
683
|
+
3. Do not commit secrets, `.env` files, or `dist/` output.
|
|
684
|
+
4. Follow the existing code style (strict TypeScript, no `any`, dependency injection).
|
|
685
|
+
5. **Keep the core product-neutral** — the product-neutrality test must stay green. No
|
|
686
|
+
product roles, routes, or copy in `src/index.ts`.
|
|
687
|
+
|
|
688
|
+
---
|
|
689
|
+
|
|
690
|
+
## License
|
|
691
|
+
|
|
692
|
+
[MIT](LICENSE) © Victor Salmon
|