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