@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/dist/index.d.ts CHANGED
@@ -112,6 +112,15 @@ export interface CognitoClientOptions {
112
112
  /** Current page path + query, used to build the `?returnTo=` destination. */
113
113
  getCurrentPath?: () => string;
114
114
  }
115
+ /**
116
+ * Fail-closed JWT expiry probe (no signature verification — cryptographic
117
+ * validation belongs to the backend and the Cognito SDK; this is purely a
118
+ * render-gating heuristic so a dead token never passes a page-load guard).
119
+ *
120
+ * Returns true when the token is expired OR cannot be parsed / carries no
121
+ * numeric `exp` claim. Real Cognito id tokens always carry `exp`.
122
+ */
123
+ export declare function isTokenExpired(token: string, nowMs?: number): boolean;
115
124
  export declare class CognitoClient {
116
125
  private readonly options;
117
126
  private pool;
@@ -146,6 +155,19 @@ export declare class CognitoClient {
146
155
  */
147
156
  completeNewPassword(newPassword: string, userAttributes?: Record<string, unknown>): Promise<SessionTokens>;
148
157
  getSession(): Promise<RestoredSession | null>;
158
+ /**
159
+ * Canonical async page-load gate for protected pages.
160
+ *
161
+ * Unlike a sync token-presence check (which passes with a stale-but-cached
162
+ * token after credentials timeout, letting the page fetch and render
163
+ * private data before the 401 path discovers the dead session), this
164
+ * validates the session through the SDK: a live session is returned
165
+ * (refreshing via the stored refresh token when the id token expired but
166
+ * the refresh token is still alive — seamless, no redirect), while a truly
167
+ * dead session resolves null AND redirects to `loginUrl` immediately —
168
+ * before any API fetch fires. Never throws.
169
+ */
170
+ ensureSession(loginUrl: string): Promise<RestoredSession | null>;
149
171
  refreshSession(): Promise<SessionTokens>;
150
172
  forgotPassword(email: string): Promise<void>;
151
173
  confirmNewPassword(email: string, code: string, newPassword: string): Promise<void>;
package/dist/index.js CHANGED
@@ -28,6 +28,30 @@
28
28
  function resolvePoolConfig(value) {
29
29
  return typeof value === 'function' ? value() : value;
30
30
  }
31
+ /** Clock skew (seconds) tolerated when judging a JWT expired. */
32
+ const TOKEN_EXPIRY_SKEW_SEC = 60;
33
+ /**
34
+ * Fail-closed JWT expiry probe (no signature verification — cryptographic
35
+ * validation belongs to the backend and the Cognito SDK; this is purely a
36
+ * render-gating heuristic so a dead token never passes a page-load guard).
37
+ *
38
+ * Returns true when the token is expired OR cannot be parsed / carries no
39
+ * numeric `exp` claim. Real Cognito id tokens always carry `exp`.
40
+ */
41
+ export function isTokenExpired(token, nowMs = Date.now()) {
42
+ try {
43
+ const parts = token.split('.');
44
+ if (parts.length < 2)
45
+ return true;
46
+ const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
47
+ if (typeof payload.exp !== 'number' || !Number.isFinite(payload.exp))
48
+ return true;
49
+ return payload.exp * 1000 <= nowMs + TOKEN_EXPIRY_SKEW_SEC * 1000;
50
+ }
51
+ catch {
52
+ return true;
53
+ }
54
+ }
31
55
  export class CognitoClient {
32
56
  constructor(options) {
33
57
  this.options = options;
@@ -190,6 +214,30 @@ export class CognitoClient {
190
214
  }
191
215
  });
192
216
  }
217
+ /**
218
+ * Canonical async page-load gate for protected pages.
219
+ *
220
+ * Unlike a sync token-presence check (which passes with a stale-but-cached
221
+ * token after credentials timeout, letting the page fetch and render
222
+ * private data before the 401 path discovers the dead session), this
223
+ * validates the session through the SDK: a live session is returned
224
+ * (refreshing via the stored refresh token when the id token expired but
225
+ * the refresh token is still alive — seamless, no redirect), while a truly
226
+ * dead session resolves null AND redirects to `loginUrl` immediately —
227
+ * before any API fetch fires. Never throws.
228
+ */
229
+ async ensureSession(loginUrl) {
230
+ try {
231
+ const session = await this.getSession();
232
+ if (!session)
233
+ this.redirectToLogin(loginUrl);
234
+ return session;
235
+ }
236
+ catch {
237
+ this.redirectToLogin(loginUrl);
238
+ return null;
239
+ }
240
+ }
193
241
  refreshSession() {
194
242
  this.initPool();
195
243
  const cognitoUser = this.pool.getCurrentUser();
@@ -203,6 +251,10 @@ export class CognitoClient {
203
251
  return new Promise((resolve, reject) => {
204
252
  cognitoUser.getSession((err, session) => {
205
253
  if (err || !session || !session.isValid()) {
254
+ // Terminal failure — same cleanup as getSession: a stale/invalid
255
+ // cached session must not leave token state behind.
256
+ cognitoUser.signOut();
257
+ this.clearTokens();
206
258
  return reject(err ? this.options.errorMapper(err) : new Error('No valid cached session'));
207
259
  }
208
260
  this.setTokensFromSession(session, cognitoUser.getUsername());
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.0.0",
6
+ "version": "1.1.0",
7
7
  "description": "Generic, dependency-injected browser AWS Cognito client — sign-up, sign-in, session restore/refresh, NEW_PASSWORD_REQUIRED challenge, forgot/reset password",
8
8
  "license": "MIT",
9
9
  "author": "Victor Salmon",
@@ -15,6 +15,7 @@
15
15
  "bugs": {
16
16
  "url": "https://github.com/victorsalmon/cognito-client/issues"
17
17
  },
18
+ "packageManager": "pnpm@11.22.0",
18
19
  "keywords": [
19
20
  "aws",
20
21
  "cognito",
@@ -37,9 +38,14 @@
37
38
  "build": "tsc -p tsconfig.build.json",
38
39
  "postinstall": "tsc -p tsconfig.build.json",
39
40
  "typecheck": "tsc --noEmit",
40
- "test": "vitest run"
41
+ "test": "vitest run",
42
+ "test:property": "vitest run property",
43
+ "test:mutation": "stryker run"
41
44
  },
42
45
  "devDependencies": {
46
+ "@stryker-mutator/core": "^10.0.0",
47
+ "@stryker-mutator/vitest-runner": "^10.0.0",
48
+ "fast-check": "^4.9.0",
43
49
  "jsdom": "^26.0.0",
44
50
  "typescript": "^5.7.2",
45
51
  "vitest": "^3.2.7"