auth-vir 2.1.0 → 2.3.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, type RequireOneOrNone } 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,21 +154,38 @@ 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;
163
166
  requestHeaders: IncomingHttpHeaders;
164
167
  isSignUpCookie: boolean;
165
168
  }): Promise<Pick<RequiredAndNotNull<OutgoingHttpHeaders>, 'set-cookie'> & Record<CsrfHeaderName, string>>;
169
+ /** Combines `.getInsecureUser()` and `.getSecureUser()` into one method. */
170
+ getInsecureOrSecureUser(params: {
171
+ requestHeaders: IncomingHttpHeaders;
172
+ isSignUpCookie?: boolean | undefined;
173
+ }): Promise<RequireOneOrNone<{
174
+ secureUser: GetUserResult<DatabaseUser>;
175
+ /**
176
+ * @deprecated This only half authenticates the user. It should only be used in
177
+ * circumstances where JavaScript cannot be used to attach the CSRF token header to
178
+ * the request (like when opening a PDF file). Use `.getSecureUser()` instead,
179
+ * whenever possible.
180
+ */
181
+ insecureUser: GetUserResult<DatabaseUser>;
182
+ }>>;
166
183
  /**
167
184
  * @deprecated This only half authenticates the user. It should only be used in circumstances
168
185
  * where JavaScript cannot be used to attach the CSRF token header to the request (like when
169
186
  * opening a PDF file). Use `.getSecureUser()` instead, whenever possible.
170
187
  */
171
- getInsecureUser({ headers, }: {
172
- headers: IncomingHttpHeaders;
188
+ getInsecureUser({ requestHeaders, }: {
189
+ requestHeaders: IncomingHttpHeaders;
173
190
  }): Promise<GetUserResult<DatabaseUser> | undefined>;
174
191
  }
@@ -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. */
@@ -189,14 +200,24 @@ export class BackendAuthClient {
189
200
  : {}),
190
201
  };
191
202
  }
203
+ /** Combines `.getInsecureUser()` and `.getSecureUser()` into one method. */
204
+ async getInsecureOrSecureUser(params) {
205
+ const secureUser = await this.getSecureUser(params);
206
+ if (secureUser) {
207
+ return secureUser;
208
+ }
209
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
210
+ const insecureUser = await this.getInsecureUser(params);
211
+ return insecureUser ? { insecureUser } : {};
212
+ }
192
213
  /**
193
214
  * @deprecated This only half authenticates the user. It should only be used in circumstances
194
215
  * where JavaScript cannot be used to attach the CSRF token header to the request (like when
195
216
  * opening a PDF file). Use `.getSecureUser()` instead, whenever possible.
196
217
  */
197
- async getInsecureUser({ headers, }) {
218
+ async getInsecureUser({ requestHeaders, }) {
198
219
  // eslint-disable-next-line @typescript-eslint/no-deprecated
199
- const userIdResult = await insecureExtractUserIdFromCookieAlone(headers, await this.getJwtParams(), AuthCookieName.Auth);
220
+ const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookieName.Auth);
200
221
  if (!userIdResult) {
201
222
  return undefined;
202
223
  }
@@ -1,4 +1,4 @@
1
- import { 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, type SetOptionalWithUndefined } from '@augment-vir/common';
2
2
  import { type AnyDuration } from 'date-vir';
3
3
  import { type EmptyObject } from 'type-fest';
4
4
  /**
@@ -45,7 +45,7 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
45
45
  */
46
46
  export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
47
47
  protected readonly config: FrontendAuthClientConfig;
48
- protected userCheckInterval: undefined | ReturnType<typeof globalThis.setInterval>;
48
+ protected userCheckInterval: undefined | ReturnType<typeof createBlockingInterval>;
49
49
  constructor(config?: FrontendAuthClientConfig);
50
50
  /**
51
51
  * Destroys the client and performs all necessary cleanup (like clearing the user check
@@ -1,5 +1,4 @@
1
- import { HttpStatus, } from '@augment-vir/common';
2
- import { convertDuration } from 'date-vir';
1
+ import { createBlockingInterval, HttpStatus, } from '@augment-vir/common';
3
2
  import { CsrfTokenFailureReason, extractCsrfTokenHeader, getCurrentCsrfToken, storeCsrfToken, wipeCurrentCsrfToken, } from '../csrf-token.js';
4
3
  import { AuthHeaderName } from '../headers.js';
5
4
  /**
@@ -15,17 +14,14 @@ export class FrontendAuthClient {
15
14
  constructor(config = {}) {
16
15
  this.config = config;
17
16
  if (config.checkUser) {
18
- const intervalDuration = convertDuration(config.checkUser.interval || { minutes: 1 }, {
19
- milliseconds: true,
20
- }).milliseconds;
21
- this.userCheckInterval = globalThis.setInterval(async () => {
17
+ this.userCheckInterval = createBlockingInterval(async () => {
22
18
  const response = await config.checkUser?.performCheck();
23
19
  if (response) {
24
20
  await this.verifyResponseAuth({
25
21
  status: response.status,
26
22
  });
27
23
  }
28
- }, intervalDuration);
24
+ }, config.checkUser.interval || { minutes: 1 });
29
25
  }
30
26
  }
31
27
  /**
@@ -33,7 +29,7 @@ export class FrontendAuthClient {
33
29
  * interval).
34
30
  */
35
31
  destroy() {
36
- globalThis.clearInterval(this.userCheckInterval);
32
+ this.userCheckInterval?.clearInterval();
37
33
  }
38
34
  /** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
39
35
  async getCurrentCsrfToken() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "auth-vir",
3
- "version": "2.1.0",
3
+ "version": "2.3.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, type RequireOneOrNone} 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. */
@@ -442,19 +463,47 @@ export class BackendAuthClient<
442
463
  };
443
464
  }
444
465
 
466
+ /** Combines `.getInsecureUser()` and `.getSecureUser()` into one method. */
467
+ public async getInsecureOrSecureUser(params: {
468
+ requestHeaders: IncomingHttpHeaders;
469
+ isSignUpCookie?: boolean | undefined;
470
+ }): Promise<
471
+ RequireOneOrNone<{
472
+ secureUser: GetUserResult<DatabaseUser>;
473
+ /**
474
+ * @deprecated This only half authenticates the user. It should only be used in
475
+ * circumstances where JavaScript cannot be used to attach the CSRF token header to
476
+ * the request (like when opening a PDF file). Use `.getSecureUser()` instead,
477
+ * whenever possible.
478
+ */
479
+ insecureUser: GetUserResult<DatabaseUser>;
480
+ }>
481
+ > {
482
+ const secureUser = await this.getSecureUser(params);
483
+
484
+ if (secureUser) {
485
+ return secureUser;
486
+ }
487
+
488
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
489
+ const insecureUser = await this.getInsecureUser(params);
490
+
491
+ return insecureUser ? {insecureUser} : {};
492
+ }
493
+
445
494
  /**
446
495
  * @deprecated This only half authenticates the user. It should only be used in circumstances
447
496
  * where JavaScript cannot be used to attach the CSRF token header to the request (like when
448
497
  * opening a PDF file). Use `.getSecureUser()` instead, whenever possible.
449
498
  */
450
499
  public async getInsecureUser({
451
- headers,
500
+ requestHeaders,
452
501
  }: {
453
- headers: IncomingHttpHeaders;
502
+ requestHeaders: IncomingHttpHeaders;
454
503
  }): Promise<GetUserResult<DatabaseUser> | undefined> {
455
504
  // eslint-disable-next-line @typescript-eslint/no-deprecated
456
505
  const userIdResult = await insecureExtractUserIdFromCookieAlone<UserId>(
457
- headers,
506
+ requestHeaders,
458
507
  await this.getJwtParams(),
459
508
  AuthCookieName.Auth,
460
509
  );
@@ -1,4 +1,5 @@
1
1
  import {
2
+ createBlockingInterval,
2
3
  HttpStatus,
3
4
  type JsonCompatibleObject,
4
5
  type MaybePromise,
@@ -6,7 +7,7 @@ import {
6
7
  type SelectFrom,
7
8
  type SetOptionalWithUndefined,
8
9
  } from '@augment-vir/common';
9
- import {convertDuration, type AnyDuration} from 'date-vir';
10
+ import {type AnyDuration} from 'date-vir';
10
11
  import {type EmptyObject} from 'type-fest';
11
12
  import {
12
13
  CsrfTokenFailureReason,
@@ -68,22 +69,21 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
68
69
  * @category Client
69
70
  */
70
71
  export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
71
- protected userCheckInterval: undefined | ReturnType<typeof globalThis.setInterval>;
72
+ protected userCheckInterval: undefined | ReturnType<typeof createBlockingInterval>;
72
73
 
73
74
  constructor(protected readonly config: FrontendAuthClientConfig = {}) {
74
75
  if (config.checkUser) {
75
- const intervalDuration = convertDuration(config.checkUser.interval || {minutes: 1}, {
76
- milliseconds: true,
77
- }).milliseconds;
78
-
79
- this.userCheckInterval = globalThis.setInterval(async () => {
80
- const response = await config.checkUser?.performCheck();
81
- if (response) {
82
- await this.verifyResponseAuth({
83
- status: response.status,
84
- });
85
- }
86
- }, intervalDuration);
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
87
  }
88
88
  }
89
89
 
@@ -92,7 +92,7 @@ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject =
92
92
  * interval).
93
93
  */
94
94
  public destroy() {
95
- globalThis.clearInterval(this.userCheckInterval);
95
+ this.userCheckInterval?.clearInterval();
96
96
  }
97
97
 
98
98
  /** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */