auth-vir 2.3.4 → 2.3.6

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.
@@ -99,7 +99,7 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
99
99
  * How long before a user's session times out when we should start trying to refresh their
100
100
  * session.
101
101
  *
102
- * @default {minutes: 5}
102
+ * @default {minutes: 10}
103
103
  */
104
104
  sessionRefreshThreshold: Readonly<AnyDuration>;
105
105
  overrides: PartialWithUndefined<{
@@ -7,6 +7,9 @@ import { parseJwtKeys } from '../jwt/jwt-keys.js';
7
7
  const defaultSessionIdleTimeout = {
8
8
  minutes: 20,
9
9
  };
10
+ const defaultSessionRefreshThreshold = {
11
+ minutes: 10,
12
+ };
10
13
  /**
11
14
  * An auth client for creating and validating JWTs embedded in cookies. This should only be used in
12
15
  * a backend environment as it accesses native Node packages.
@@ -72,9 +75,7 @@ export class BackendAuthClient {
72
75
  * - Z = JWT expiration outside the refresh threshold: {@link isRefreshReady} = false.
73
76
  */
74
77
  const isRefreshReady = isDateAfter({
75
- fullDate: calculateRelativeDate(now, this.config.sessionRefreshThreshold || {
76
- minutes: 5,
77
- }),
78
+ fullDate: calculateRelativeDate(now, this.config.sessionRefreshThreshold || defaultSessionRefreshThreshold),
78
79
  relativeTo: userIdResult.jwtExpiration,
79
80
  });
80
81
  if (isRefreshReady) {
@@ -1,4 +1,4 @@
1
- import { createBlockingInterval, type JsonCompatibleObject, type MaybePromise, type PartialWithUndefined, type SelectFrom, type SetOptionalWithUndefined } from '@augment-vir/common';
1
+ import { createBlockingInterval, type JsonCompatibleObject, type MaybePromise, type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
2
2
  import { type AnyDuration } from 'date-vir';
3
3
  import { type EmptyObject } from 'type-fest';
4
4
  /**
@@ -23,10 +23,13 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
23
23
  * Get a response from the backend to see if the user is still authenticated. If the
24
24
  * response returns a non-authorized status, the user is wiped. Any other status is
25
25
  * ignored.
26
+ *
27
+ * If the user is not currently authorized, this should return `undefined` to prevent
28
+ * unnecessary network traffic.
26
29
  */
27
30
  performCheck: () => MaybePromise<SelectFrom<Response, {
28
31
  status: true;
29
- }>>;
32
+ }> | undefined>;
30
33
  /** @default {minutes: 1} */
31
34
  interval?: AnyDuration | undefined;
32
35
  };
@@ -46,11 +49,6 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
46
49
  export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
47
50
  protected readonly config: FrontendAuthClientConfig;
48
51
  protected userCheckInterval: undefined | ReturnType<typeof createBlockingInterval>;
49
- /**
50
- * Keeps track of whether the latest state of auth is logged in (`true`) or not (`false`). This
51
- * is used for determining if the check user interval should keep running.
52
- */
53
- protected isLatestAuthorized: boolean;
54
52
  constructor(config?: FrontendAuthClientConfig);
55
53
  /**
56
54
  * Destroys the client and performs all necessary cleanup (like clearing the user check
@@ -92,8 +90,8 @@ export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatible
92
90
  *
93
91
  * @returns `true` if the auth is okay, `false` otherwise.
94
92
  */
95
- verifyResponseAuth(response: Readonly<SetOptionalWithUndefined<SelectFrom<Response, {
93
+ verifyResponseAuth(response: Readonly<PartialWithUndefined<SelectFrom<Response, {
96
94
  status: true;
97
95
  headers: true;
98
- }>, 'headers'>>): Promise<boolean>;
96
+ }>>>): Promise<boolean>;
99
97
  }
@@ -1,4 +1,5 @@
1
1
  import { createBlockingInterval, HttpStatus, } from '@augment-vir/common';
2
+ import { isPageActive } from 'page-active';
2
3
  import { CsrfTokenFailureReason, extractCsrfTokenHeader, getCurrentCsrfToken, storeCsrfToken, wipeCurrentCsrfToken, } from '../csrf-token.js';
3
4
  import { AuthHeaderName } from '../headers.js';
4
5
  /**
@@ -11,22 +12,17 @@ import { AuthHeaderName } from '../headers.js';
11
12
  export class FrontendAuthClient {
12
13
  config;
13
14
  userCheckInterval;
14
- /**
15
- * Keeps track of whether the latest state of auth is logged in (`true`) or not (`false`). This
16
- * is used for determining if the check user interval should keep running.
17
- */
18
- isLatestAuthorized = false;
19
15
  constructor(config = {}) {
20
16
  this.config = config;
21
17
  if (config.checkUser) {
22
18
  this.userCheckInterval = createBlockingInterval(async () => {
23
- /** No need to check current user status when there is no user. */
24
- if (!this.isLatestAuthorized) {
19
+ if (!isPageActive()) {
20
+ /** Do not refresh the user when the page is inactive. */
25
21
  return;
26
22
  }
27
23
  const response = await config.checkUser?.performCheck();
28
24
  if (response) {
29
- this.isLatestAuthorized = await this.verifyResponseAuth({
25
+ await this.verifyResponseAuth({
30
26
  status: response.status,
31
27
  });
32
28
  }
@@ -111,7 +107,6 @@ export class FrontendAuthClient {
111
107
  }
112
108
  /** Wipes the current user auth. */
113
109
  async logout() {
114
- this.isLatestAuthorized = false;
115
110
  await this.config.authClearedCallback?.();
116
111
  wipeCurrentCsrfToken(this.config.overrides);
117
112
  }
@@ -132,7 +127,6 @@ export class FrontendAuthClient {
132
127
  throw new Error('Did not receive any CSRF token.');
133
128
  }
134
129
  storeCsrfToken(csrfToken, this.config.overrides);
135
- this.isLatestAuthorized = true;
136
130
  }
137
131
  /**
138
132
  * Use to verify _all_ responses received from the backend. Immediately logs the user out once
@@ -146,6 +140,11 @@ export class FrontendAuthClient {
146
140
  await this.logout();
147
141
  return false;
148
142
  }
143
+ /** If the response has a new CSRF token, store it. */
144
+ const { csrfToken } = extractCsrfTokenHeader(response, this.config.overrides);
145
+ if (csrfToken) {
146
+ storeCsrfToken(csrfToken, this.config.overrides);
147
+ }
149
148
  return true;
150
149
  }
151
150
  }
@@ -62,9 +62,9 @@ export type GetCsrfTokenResult = RequireExactlyOne<{
62
62
  *
63
63
  * @category Auth : Client
64
64
  */
65
- export declare function extractCsrfTokenHeader(response: Readonly<SelectFrom<Response, {
65
+ export declare function extractCsrfTokenHeader(response: Readonly<PartialWithUndefined<SelectFrom<Response, {
66
66
  headers: true;
67
- }>>, overrides?: PartialWithUndefined<{
67
+ }>>>, overrides?: PartialWithUndefined<{
68
68
  csrfHeaderName: string;
69
69
  }>): Readonly<GetCsrfTokenResult>;
70
70
  /**
@@ -45,7 +45,7 @@ export var CsrfTokenFailureReason;
45
45
  */
46
46
  export function extractCsrfTokenHeader(response, overrides = {}) {
47
47
  const csrfTokenHeaderName = overrides.csrfHeaderName || AuthHeaderName.CsrfToken;
48
- const rawCsrfToken = response.headers.get(csrfTokenHeaderName);
48
+ const rawCsrfToken = response.headers?.get(csrfTokenHeaderName);
49
49
  return parseCsrfToken(rawCsrfToken);
50
50
  }
51
51
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auth-vir",
3
- "version": "2.3.4",
3
+ "version": "2.3.6",
4
4
  "description": "Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.",
5
5
  "keywords": [
6
6
  "auth",
@@ -42,17 +42,18 @@
42
42
  "test:web": "virmator test web"
43
43
  },
44
44
  "dependencies": {
45
- "@augment-vir/assert": "^31.45.0",
46
- "@augment-vir/common": "^31.45.0",
45
+ "@augment-vir/assert": "^31.47.0",
46
+ "@augment-vir/common": "^31.47.0",
47
47
  "date-vir": "^8.0.0",
48
48
  "hash-wasm": "^4.12.0",
49
49
  "jose": "^6.1.0",
50
50
  "object-shape-tester": "^6.9.3",
51
+ "page-active": "^1.0.3",
51
52
  "type-fest": "^5.1.0",
52
53
  "url-vir": "^2.1.6"
53
54
  },
54
55
  "devDependencies": {
55
- "@augment-vir/test": "^31.45.0",
56
+ "@augment-vir/test": "^31.47.0",
56
57
  "@prisma/client": "^6.18.0",
57
58
  "@types/node": "^24.9.1",
58
59
  "@web/dev-server-esbuild": "^1.0.4",
@@ -62,7 +63,7 @@
62
63
  "@web/test-runner-visual-regression": "^0.10.0",
63
64
  "istanbul-smart-text-reporter": "^1.1.5",
64
65
  "markdown-code-example-inserter": "^3.0.3",
65
- "prisma-vir": "^2.0.0",
66
+ "prisma-vir": "^2.1.0",
66
67
  "typedoc": "^0.28.14"
67
68
  },
68
69
  "engines": {
@@ -123,7 +123,7 @@ export type BackendAuthClientConfig<
123
123
  * How long before a user's session times out when we should start trying to refresh their
124
124
  * session.
125
125
  *
126
- * @default {minutes: 5}
126
+ * @default {minutes: 10}
127
127
  */
128
128
  sessionRefreshThreshold: Readonly<AnyDuration>;
129
129
  overrides: PartialWithUndefined<{
@@ -137,6 +137,10 @@ const defaultSessionIdleTimeout: Readonly<AnyDuration> = {
137
137
  minutes: 20,
138
138
  };
139
139
 
140
+ const defaultSessionRefreshThreshold: Readonly<AnyDuration> = {
141
+ minutes: 10,
142
+ };
143
+
140
144
  /**
141
145
  * An auth client for creating and validating JWTs embedded in cookies. This should only be used in
142
146
  * a backend environment as it accesses native Node packages.
@@ -245,9 +249,7 @@ export class BackendAuthClient<
245
249
  const isRefreshReady = isDateAfter({
246
250
  fullDate: calculateRelativeDate(
247
251
  now,
248
- this.config.sessionRefreshThreshold || {
249
- minutes: 5,
250
- },
252
+ this.config.sessionRefreshThreshold || defaultSessionRefreshThreshold,
251
253
  ),
252
254
  relativeTo: userIdResult.jwtExpiration,
253
255
  });
@@ -5,9 +5,9 @@ import {
5
5
  type MaybePromise,
6
6
  type PartialWithUndefined,
7
7
  type SelectFrom,
8
- type SetOptionalWithUndefined,
9
8
  } from '@augment-vir/common';
10
9
  import {type AnyDuration} from 'date-vir';
10
+ import {isPageActive} from 'page-active';
11
11
  import {type EmptyObject} from 'type-fest';
12
12
  import {
13
13
  CsrfTokenFailureReason,
@@ -41,14 +41,18 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
41
41
  * Get a response from the backend to see if the user is still authenticated. If the
42
42
  * response returns a non-authorized status, the user is wiped. Any other status is
43
43
  * ignored.
44
+ *
45
+ * If the user is not currently authorized, this should return `undefined` to prevent
46
+ * unnecessary network traffic.
44
47
  */
45
48
  performCheck: () => MaybePromise<
46
- SelectFrom<
47
- Response,
48
- {
49
- status: true;
50
- }
51
- >
49
+ | SelectFrom<
50
+ Response,
51
+ {
52
+ status: true;
53
+ }
54
+ >
55
+ | undefined
52
56
  >;
53
57
  /** @default {minutes: 1} */
54
58
  interval?: AnyDuration | undefined;
@@ -70,24 +74,19 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
70
74
  */
71
75
  export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
72
76
  protected userCheckInterval: undefined | ReturnType<typeof createBlockingInterval>;
73
- /**
74
- * Keeps track of whether the latest state of auth is logged in (`true`) or not (`false`). This
75
- * is used for determining if the check user interval should keep running.
76
- */
77
- protected isLatestAuthorized = false;
78
77
 
79
78
  constructor(protected readonly config: FrontendAuthClientConfig = {}) {
80
79
  if (config.checkUser) {
81
80
  this.userCheckInterval = createBlockingInterval(
82
81
  async () => {
83
- /** No need to check current user status when there is no user. */
84
- if (!this.isLatestAuthorized) {
82
+ if (!isPageActive()) {
83
+ /** Do not refresh the user when the page is inactive. */
85
84
  return;
86
85
  }
87
-
88
86
  const response = await config.checkUser?.performCheck();
87
+
89
88
  if (response) {
90
- this.isLatestAuthorized = await this.verifyResponseAuth({
89
+ await this.verifyResponseAuth({
91
90
  status: response.status,
92
91
  });
93
92
  }
@@ -194,7 +193,6 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
194
193
 
195
194
  /** Wipes the current user auth. */
196
195
  public async logout() {
197
- this.isLatestAuthorized = false;
198
196
  await this.config.authClearedCallback?.();
199
197
  wipeCurrentCsrfToken(this.config.overrides);
200
198
  }
@@ -229,7 +227,6 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
229
227
  }
230
228
 
231
229
  storeCsrfToken(csrfToken, this.config.overrides);
232
- this.isLatestAuthorized = true;
233
230
  }
234
231
 
235
232
  /**
@@ -240,15 +237,14 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
240
237
  */
241
238
  public async verifyResponseAuth(
242
239
  response: Readonly<
243
- SetOptionalWithUndefined<
240
+ PartialWithUndefined<
244
241
  SelectFrom<
245
242
  Response,
246
243
  {
247
244
  status: true;
248
245
  headers: true;
249
246
  }
250
- >,
251
- 'headers'
247
+ >
252
248
  >
253
249
  >,
254
250
  ): Promise<boolean> {
@@ -260,6 +256,12 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
260
256
  return false;
261
257
  }
262
258
 
259
+ /** If the response has a new CSRF token, store it. */
260
+ const {csrfToken} = extractCsrfTokenHeader(response, this.config.overrides);
261
+ if (csrfToken) {
262
+ storeCsrfToken(csrfToken, this.config.overrides);
263
+ }
264
+
263
265
  return true;
264
266
  }
265
267
  }
package/src/csrf-token.ts CHANGED
@@ -77,14 +77,14 @@ export type GetCsrfTokenResult = RequireExactlyOne<{
77
77
  * @category Auth : Client
78
78
  */
79
79
  export function extractCsrfTokenHeader(
80
- response: Readonly<SelectFrom<Response, {headers: true}>>,
80
+ response: Readonly<PartialWithUndefined<SelectFrom<Response, {headers: true}>>>,
81
81
  overrides: PartialWithUndefined<{
82
82
  csrfHeaderName: string;
83
83
  }> = {},
84
84
  ): Readonly<GetCsrfTokenResult> {
85
85
  const csrfTokenHeaderName = overrides.csrfHeaderName || AuthHeaderName.CsrfToken;
86
86
 
87
- const rawCsrfToken = response.headers.get(csrfTokenHeaderName);
87
+ const rawCsrfToken = response.headers?.get(csrfTokenHeaderName);
88
88
 
89
89
  return parseCsrfToken(rawCsrfToken);
90
90
  }