auth-vir 2.0.4 → 2.2.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.
@@ -1,7 +1,7 @@
1
1
  import { type AnyObject, type JsonCompatibleObject, type MaybePromise, type PartialWithUndefined, type RequiredAndNotNull } from '@augment-vir/common';
2
2
  import { type AnyDuration } from 'date-vir';
3
3
  import { type IncomingHttpHeaders, type OutgoingHttpHeaders } from 'node:http';
4
- import { type EmptyObject } from 'type-fest';
4
+ import { type EmptyObject, type RequireExactlyOne } from 'type-fest';
5
5
  import { type UserIdResult } from '../auth.js';
6
6
  import { type CookieParams } from '../cookie.js';
7
7
  import { AuthHeaderName } from '../headers.js';
@@ -154,9 +154,12 @@ export declare class BackendAuthClient<DatabaseUser extends AnyObject, UserId ex
154
154
  */
155
155
  getJwtParams(): Promise<Readonly<CreateJwtParams>>;
156
156
  /** Use these headers to log out the user. */
157
- createLogoutHeaders(): Promise<{
158
- 'set-cookie': string;
159
- } & Record<CsrfHeaderName, string>>;
157
+ createLogoutHeaders(params: RequireExactlyOne<{
158
+ allCookies: true;
159
+ isSignUpCookie: boolean;
160
+ }>): Promise<Partial<Record<CsrfHeaderName, string>> & {
161
+ 'set-cookie': string[];
162
+ }>;
160
163
  /** Use these headers to log a user in. */
161
164
  createLoginHeaders({ userId, requestHeaders, isSignUpCookie, }: {
162
165
  userId: UserId;
@@ -155,16 +155,27 @@ export class BackendAuthClient {
155
155
  };
156
156
  }
157
157
  /** Use these headers to log out the user. */
158
- async createLogoutHeaders() {
159
- const signUpCookieHeaders = generateLogoutHeaders(await this.getCookieParams({
160
- isSignUpCookie: true,
161
- }), this.config.overrides);
162
- const authCookieHeaders = generateLogoutHeaders(await this.getCookieParams({
163
- isSignUpCookie: false,
164
- }), this.config.overrides);
165
- return {
158
+ async createLogoutHeaders(params) {
159
+ const signUpCookieHeaders = params.allCookies || params.isSignUpCookie
160
+ ? generateLogoutHeaders(await this.getCookieParams({
161
+ isSignUpCookie: true,
162
+ }), this.config.overrides)
163
+ : undefined;
164
+ const authCookieHeaders = params.allCookies || !params.isSignUpCookie
165
+ ? generateLogoutHeaders(await this.getCookieParams({
166
+ isSignUpCookie: false,
167
+ }), this.config.overrides)
168
+ : undefined;
169
+ const setCookieHeader = {
170
+ 'set-cookie': mergeHeaderValues(signUpCookieHeaders?.['set-cookie'], authCookieHeaders?.['set-cookie']),
171
+ };
172
+ const csrfTokenHeader = {
166
173
  ...authCookieHeaders,
167
- 'set-cookie': mergeHeaderValues(signUpCookieHeaders['set-cookie'], authCookieHeaders['set-cookie']),
174
+ ...signUpCookieHeaders,
175
+ };
176
+ return {
177
+ ...csrfTokenHeader,
178
+ ...setCookieHeader,
168
179
  };
169
180
  }
170
181
  /** Use these headers to log a user in. */
@@ -1,4 +1,5 @@
1
- import { type JsonCompatibleObject, type MaybePromise, type PartialWithUndefined, type SelectFrom } from '@augment-vir/common';
1
+ import { createBlockingInterval, type JsonCompatibleObject, type MaybePromise, type PartialWithUndefined, type SelectFrom, type SetOptionalWithUndefined } from '@augment-vir/common';
2
+ import { type AnyDuration } from 'date-vir';
2
3
  import { type EmptyObject } from 'type-fest';
3
4
  /**
4
5
  * Config for {@link FrontendAuthClient}.
@@ -13,6 +14,22 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
13
14
  canAssumeUser: () => MaybePromise<boolean>;
14
15
  /** Called whenever the current user becomes unauthorized and their CSRF token is wiped. */
15
16
  authClearedCallback: () => MaybePromise<void>;
17
+ /**
18
+ * Performs automatic checks on an interval to see if the user is still authenticated. Omit this
19
+ * to turn off automatic checks.
20
+ */
21
+ checkUser: {
22
+ /**
23
+ * Get a response from the backend to see if the user is still authenticated. If the
24
+ * response returns a non-authorized status, the user is wiped. Any other status is
25
+ * ignored.
26
+ */
27
+ performCheck: () => MaybePromise<SelectFrom<Response, {
28
+ status: true;
29
+ }>>;
30
+ /** @default {minutes: 1} */
31
+ interval?: AnyDuration | undefined;
32
+ };
16
33
  overrides: PartialWithUndefined<{
17
34
  localStorage: Pick<Storage, 'setItem' | 'removeItem' | 'getItem'>;
18
35
  csrfHeaderName: string;
@@ -28,7 +45,13 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
28
45
  */
29
46
  export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
30
47
  protected readonly config: FrontendAuthClientConfig;
48
+ protected userCheckInterval: undefined | ReturnType<typeof createBlockingInterval>;
31
49
  constructor(config?: FrontendAuthClientConfig);
50
+ /**
51
+ * Destroys the client and performs all necessary cleanup (like clearing the user check
52
+ * interval).
53
+ */
54
+ destroy(): void;
32
55
  /** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
33
56
  getCurrentCsrfToken(): Promise<string | undefined>;
34
57
  /**
@@ -62,8 +85,8 @@ export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatible
62
85
  * Use to verify _all_ responses received from the backend. Immediately logs the user out once
63
86
  * an unauthorized response is detected.
64
87
  */
65
- verifyResponseAuth(response: Readonly<SelectFrom<Response, {
88
+ verifyResponseAuth(response: Readonly<SetOptionalWithUndefined<SelectFrom<Response, {
66
89
  status: true;
67
90
  headers: true;
68
- }>>): Promise<void>;
91
+ }>, 'headers'>>): Promise<void>;
69
92
  }
@@ -1,4 +1,4 @@
1
- import { HttpStatus, } from '@augment-vir/common';
1
+ import { createBlockingInterval, HttpStatus, } from '@augment-vir/common';
2
2
  import { CsrfTokenFailureReason, extractCsrfTokenHeader, getCurrentCsrfToken, storeCsrfToken, wipeCurrentCsrfToken, } from '../csrf-token.js';
3
3
  import { AuthHeaderName } from '../headers.js';
4
4
  /**
@@ -10,8 +10,26 @@ import { AuthHeaderName } from '../headers.js';
10
10
  */
11
11
  export class FrontendAuthClient {
12
12
  config;
13
+ userCheckInterval;
13
14
  constructor(config = {}) {
14
15
  this.config = config;
16
+ if (config.checkUser) {
17
+ this.userCheckInterval = createBlockingInterval(async () => {
18
+ const response = await config.checkUser?.performCheck();
19
+ if (response) {
20
+ await this.verifyResponseAuth({
21
+ status: response.status,
22
+ });
23
+ }
24
+ }, config.checkUser.interval || { minutes: 1 });
25
+ }
26
+ }
27
+ /**
28
+ * Destroys the client and performs all necessary cleanup (like clearing the user check
29
+ * interval).
30
+ */
31
+ destroy() {
32
+ this.userCheckInterval?.clearInterval();
15
33
  }
16
34
  /** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
17
35
  async getCurrentCsrfToken() {
@@ -111,7 +129,7 @@ export class FrontendAuthClient {
111
129
  */
112
130
  async verifyResponseAuth(response) {
113
131
  if (response.status === HttpStatus.Unauthorized &&
114
- !response.headers.get(AuthHeaderName.IsSignUpAuth)) {
132
+ !response.headers?.get(AuthHeaderName.IsSignUpAuth)) {
115
133
  await this.logout();
116
134
  }
117
135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auth-vir",
3
- "version": "2.0.4",
3
+ "version": "2.2.0",
4
4
  "description": "Auth made easy and secure via JWT cookies, CSRF tokens, and password hashing helpers.",
5
5
  "keywords": [
6
6
  "auth",
@@ -42,8 +42,8 @@
42
42
  "test:web": "virmator test web"
43
43
  },
44
44
  "dependencies": {
45
- "@augment-vir/assert": "^31.41.0",
46
- "@augment-vir/common": "^31.41.0",
45
+ "@augment-vir/assert": "^31.42.0",
46
+ "@augment-vir/common": "^31.42.0",
47
47
  "date-vir": "^8.0.0",
48
48
  "hash-wasm": "^4.12.0",
49
49
  "jose": "^6.1.0",
@@ -52,9 +52,9 @@
52
52
  "url-vir": "^2.1.6"
53
53
  },
54
54
  "devDependencies": {
55
- "@augment-vir/test": "^31.41.0",
55
+ "@augment-vir/test": "^31.42.0",
56
56
  "@prisma/client": "^6.17.1",
57
- "@types/node": "^24.8.1",
57
+ "@types/node": "^24.9.1",
58
58
  "@web/dev-server-esbuild": "^1.0.4",
59
59
  "@web/test-runner": "^0.20.2",
60
60
  "@web/test-runner-commands": "^0.9.0",
@@ -8,7 +8,7 @@ import {
8
8
  } from '@augment-vir/common';
9
9
  import {calculateRelativeDate, getNowInUtcTimezone, isDateAfter, type AnyDuration} from 'date-vir';
10
10
  import {type IncomingHttpHeaders, type OutgoingHttpHeaders} from 'node:http';
11
- import {type EmptyObject} from 'type-fest';
11
+ import {type EmptyObject, type RequireExactlyOne} from 'type-fest';
12
12
  import {
13
13
  extractUserIdFromRequestHeaders,
14
14
  generateLogoutHeaders,
@@ -369,31 +369,52 @@ export class BackendAuthClient<
369
369
  }
370
370
 
371
371
  /** Use these headers to log out the user. */
372
- public async createLogoutHeaders(): Promise<
373
- {
374
- 'set-cookie': string;
375
- } & Record<CsrfHeaderName, string>
372
+ public async createLogoutHeaders(
373
+ params: RequireExactlyOne<{
374
+ allCookies: true;
375
+ isSignUpCookie: boolean;
376
+ }>,
377
+ ): Promise<
378
+ Partial<Record<CsrfHeaderName, string>> & {
379
+ 'set-cookie': string[];
380
+ }
376
381
  > {
377
- const signUpCookieHeaders = generateLogoutHeaders(
378
- await this.getCookieParams({
379
- isSignUpCookie: true,
380
- }),
381
- this.config.overrides,
382
- );
383
- const authCookieHeaders = generateLogoutHeaders(
384
- await this.getCookieParams({
385
- isSignUpCookie: false,
386
- }),
387
- this.config.overrides,
388
- );
389
-
390
- return {
391
- ...authCookieHeaders,
382
+ const signUpCookieHeaders =
383
+ params.allCookies || params.isSignUpCookie
384
+ ? (generateLogoutHeaders(
385
+ await this.getCookieParams({
386
+ isSignUpCookie: true,
387
+ }),
388
+ this.config.overrides,
389
+ ) satisfies Record<CsrfHeaderName, string>)
390
+ : undefined;
391
+ const authCookieHeaders =
392
+ params.allCookies || !params.isSignUpCookie
393
+ ? (generateLogoutHeaders(
394
+ await this.getCookieParams({
395
+ isSignUpCookie: false,
396
+ }),
397
+ this.config.overrides,
398
+ ) satisfies Record<CsrfHeaderName, string>)
399
+ : undefined;
400
+
401
+ const setCookieHeader: {
402
+ 'set-cookie': string[];
403
+ } = {
392
404
  'set-cookie': mergeHeaderValues(
393
- signUpCookieHeaders['set-cookie'],
394
- authCookieHeaders['set-cookie'],
405
+ signUpCookieHeaders?.['set-cookie'],
406
+ authCookieHeaders?.['set-cookie'],
395
407
  ),
396
408
  };
409
+ const csrfTokenHeader = {
410
+ ...authCookieHeaders,
411
+ ...signUpCookieHeaders,
412
+ } as Record<CsrfHeaderName, string>;
413
+
414
+ return {
415
+ ...csrfTokenHeader,
416
+ ...setCookieHeader,
417
+ };
397
418
  }
398
419
 
399
420
  /** Use these headers to log a user in. */
@@ -1,10 +1,13 @@
1
1
  import {
2
+ createBlockingInterval,
2
3
  HttpStatus,
3
4
  type JsonCompatibleObject,
4
5
  type MaybePromise,
5
6
  type PartialWithUndefined,
6
7
  type SelectFrom,
8
+ type SetOptionalWithUndefined,
7
9
  } from '@augment-vir/common';
10
+ import {type AnyDuration} from 'date-vir';
8
11
  import {type EmptyObject} from 'type-fest';
9
12
  import {
10
13
  CsrfTokenFailureReason,
@@ -28,6 +31,29 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
28
31
  canAssumeUser: () => MaybePromise<boolean>;
29
32
  /** Called whenever the current user becomes unauthorized and their CSRF token is wiped. */
30
33
  authClearedCallback: () => MaybePromise<void>;
34
+
35
+ /**
36
+ * Performs automatic checks on an interval to see if the user is still authenticated. Omit this
37
+ * to turn off automatic checks.
38
+ */
39
+ checkUser: {
40
+ /**
41
+ * Get a response from the backend to see if the user is still authenticated. If the
42
+ * response returns a non-authorized status, the user is wiped. Any other status is
43
+ * ignored.
44
+ */
45
+ performCheck: () => MaybePromise<
46
+ SelectFrom<
47
+ Response,
48
+ {
49
+ status: true;
50
+ }
51
+ >
52
+ >;
53
+ /** @default {minutes: 1} */
54
+ interval?: AnyDuration | undefined;
55
+ };
56
+
31
57
  overrides: PartialWithUndefined<{
32
58
  localStorage: Pick<Storage, 'setItem' | 'removeItem' | 'getItem'>;
33
59
  csrfHeaderName: string;
@@ -43,7 +69,31 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
43
69
  * @category Client
44
70
  */
45
71
  export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
46
- constructor(protected readonly config: FrontendAuthClientConfig = {}) {}
72
+ protected userCheckInterval: undefined | ReturnType<typeof createBlockingInterval>;
73
+
74
+ constructor(protected readonly config: FrontendAuthClientConfig = {}) {
75
+ if (config.checkUser) {
76
+ this.userCheckInterval = createBlockingInterval(
77
+ async () => {
78
+ const response = await config.checkUser?.performCheck();
79
+ if (response) {
80
+ await this.verifyResponseAuth({
81
+ status: response.status,
82
+ });
83
+ }
84
+ },
85
+ config.checkUser.interval || {minutes: 1},
86
+ );
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Destroys the client and performs all necessary cleanup (like clearing the user check
92
+ * interval).
93
+ */
94
+ public destroy() {
95
+ this.userCheckInterval?.clearInterval();
96
+ }
47
97
 
48
98
  /** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
49
99
  public async getCurrentCsrfToken(): Promise<string | undefined> {
@@ -176,18 +226,21 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
176
226
  */
177
227
  public async verifyResponseAuth(
178
228
  response: Readonly<
179
- SelectFrom<
180
- Response,
181
- {
182
- status: true;
183
- headers: true;
184
- }
229
+ SetOptionalWithUndefined<
230
+ SelectFrom<
231
+ Response,
232
+ {
233
+ status: true;
234
+ headers: true;
235
+ }
236
+ >,
237
+ 'headers'
185
238
  >
186
239
  >,
187
240
  ): Promise<void> {
188
241
  if (
189
242
  response.status === HttpStatus.Unauthorized &&
190
- !response.headers.get(AuthHeaderName.IsSignUpAuth)
243
+ !response.headers?.get(AuthHeaderName.IsSignUpAuth)
191
244
  ) {
192
245
  await this.logout();
193
246
  }