auth-vir 1.3.1 → 2.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.
Files changed (65) hide show
  1. package/README.md +27 -18
  2. package/dist/auth-client/backend-auth.client.d.ts +177 -0
  3. package/dist/auth-client/backend-auth.client.js +232 -0
  4. package/dist/auth-client/frontend-auth.client.d.ts +64 -0
  5. package/dist/auth-client/frontend-auth.client.js +107 -0
  6. package/dist/auth.d.ts +33 -47
  7. package/dist/auth.js +36 -36
  8. package/dist/cookie.d.ts +15 -4
  9. package/dist/cookie.js +17 -5
  10. package/dist/csrf-token.d.ts +113 -3
  11. package/dist/csrf-token.js +101 -5
  12. package/dist/generated/browser.d.ts +9 -0
  13. package/dist/generated/browser.js +16 -0
  14. package/dist/generated/client.d.ts +26 -0
  15. package/dist/generated/client.js +31 -0
  16. package/dist/generated/commonInputTypes.d.ts +122 -0
  17. package/dist/generated/enums.d.ts +1 -0
  18. package/dist/generated/enums.js +9 -0
  19. package/dist/generated/internal/class.d.ts +126 -0
  20. package/dist/generated/internal/class.js +84 -0
  21. package/dist/generated/internal/prismaNamespace.d.ts +544 -0
  22. package/dist/generated/internal/prismaNamespace.js +101 -0
  23. package/dist/generated/internal/prismaNamespaceBrowser.d.ts +75 -0
  24. package/dist/generated/internal/prismaNamespaceBrowser.js +69 -0
  25. package/dist/generated/models/User.d.ts +983 -0
  26. package/dist/generated/models/User.js +1 -0
  27. package/dist/generated/models.d.ts +2 -0
  28. package/dist/generated/models.js +1 -0
  29. package/dist/generated/shapes.gen.d.ts +8 -0
  30. package/dist/generated/shapes.gen.js +11 -0
  31. package/dist/headers.d.ts +20 -0
  32. package/dist/headers.js +33 -0
  33. package/dist/index.d.ts +5 -3
  34. package/dist/index.js +5 -3
  35. package/dist/jwt/jwt-keys.script.d.ts +1 -0
  36. package/dist/{jwt.d.ts → jwt/jwt.d.ts} +11 -2
  37. package/dist/{jwt.js → jwt/jwt.js} +14 -3
  38. package/dist/{user-jwt.d.ts → jwt/user-jwt.d.ts} +8 -8
  39. package/dist/{user-jwt.js → jwt/user-jwt.js} +12 -9
  40. package/package.json +17 -12
  41. package/src/auth-client/backend-auth.client.ts +500 -0
  42. package/src/auth-client/frontend-auth.client.ts +182 -0
  43. package/src/auth.ts +100 -78
  44. package/src/cookie.ts +20 -8
  45. package/src/csrf-token.ts +196 -5
  46. package/src/generated/browser.ts +23 -0
  47. package/src/generated/client.ts +47 -0
  48. package/src/generated/commonInputTypes.ts +147 -0
  49. package/src/generated/enums.ts +14 -0
  50. package/src/generated/internal/class.ts +236 -0
  51. package/src/generated/internal/prismaNamespace.ts +761 -0
  52. package/src/generated/internal/prismaNamespaceBrowser.ts +102 -0
  53. package/src/generated/models/User.ts +1135 -0
  54. package/src/generated/models.ts +11 -0
  55. package/src/generated/shapes.gen.ts +15 -0
  56. package/src/headers.ts +35 -0
  57. package/src/index.ts +5 -3
  58. package/src/{jwt.ts → jwt/jwt.ts} +34 -5
  59. package/src/{user-jwt.ts → jwt/user-jwt.ts} +22 -13
  60. /package/dist/{jwt-keys.script.d.ts → generated/commonInputTypes.js} +0 -0
  61. /package/dist/{jwt-keys.d.ts → jwt/jwt-keys.d.ts} +0 -0
  62. /package/dist/{jwt-keys.js → jwt/jwt-keys.js} +0 -0
  63. /package/dist/{jwt-keys.script.js → jwt/jwt-keys.script.js} +0 -0
  64. /package/src/{jwt-keys.script.ts → jwt/jwt-keys.script.ts} +0 -0
  65. /package/src/{jwt-keys.ts → jwt/jwt-keys.ts} +0 -0
@@ -0,0 +1,500 @@
1
+ import {
2
+ ensureArray,
3
+ type AnyObject,
4
+ type JsonCompatibleObject,
5
+ type MaybePromise,
6
+ type PartialWithUndefined,
7
+ type RequiredAndNotNull,
8
+ } from '@augment-vir/common';
9
+ import {calculateRelativeDate, getNowInUtcTimezone, isDateAfter, type AnyDuration} from 'date-vir';
10
+ import {type IncomingHttpHeaders, type OutgoingHttpHeaders} from 'node:http';
11
+ import {type EmptyObject} from 'type-fest';
12
+ import {
13
+ extractUserIdFromRequestHeaders,
14
+ generateLogoutHeaders,
15
+ generateSuccessfulLoginHeaders,
16
+ insecureExtractUserIdFromCookieAlone,
17
+ type UserIdResult,
18
+ } from '../auth.js';
19
+ import {AuthCookieName, type CookieParams} from '../cookie.js';
20
+ import {AuthHeaderName, mergeHeaderValues} from '../headers.js';
21
+ import {generateNewJwtKeys, parseJwtKeys, type JwtKeys, type RawJwtKeys} from '../jwt/jwt-keys.js';
22
+ import {type CreateJwtParams} from '../jwt/jwt.js';
23
+
24
+ /**
25
+ * Output from `BackendAuthClient.getSecureUser()`.
26
+ *
27
+ * @category Internal
28
+ */
29
+ export type GetUserResult<DatabaseUser extends AnyObject> = {
30
+ /** The retrieved user. */
31
+ user: DatabaseUser;
32
+ /**
33
+ * When `true`, indicates that the current `user` result is as assumed user. This can only be
34
+ * `true` if you've configured user assuming in `BackendAuthClient`.
35
+ */
36
+ isAssumed: boolean;
37
+ /**
38
+ * This should be merged into your own response headers. It usually contains auth cookie
39
+ * duration refresh headers.
40
+ */
41
+ responseHeaders: OutgoingHttpHeaders;
42
+ };
43
+
44
+ /**
45
+ * Config for {@link BackendAuthClient}.
46
+ *
47
+ * @category Internal
48
+ */
49
+ export type BackendAuthClientConfig<
50
+ DatabaseUser extends AnyObject,
51
+ UserId extends string | number,
52
+ CsrfHeaderName extends string = AuthHeaderName.CsrfToken,
53
+ AssumedUserParams extends JsonCompatibleObject = EmptyObject,
54
+ > = Readonly<
55
+ {
56
+ /** The origin of your backend that is offering auth cookies. */
57
+ serviceOrigin: string;
58
+ /** Finds the relevant user from your own database. */
59
+ getUserFromDatabase: (userParams: {
60
+ /** The user id extracted from the request cookie. */
61
+ userId: UserId;
62
+ /** Indicates that we're loading the user from a sign up cookie. */
63
+ isSignUpCookie: boolean;
64
+ /**
65
+ * If this is set, we're attempting to load a database user for the purpose of assuming
66
+ * their user identity. Otherwise, this is `undefined`.
67
+ */
68
+ assumedUserParams: AssumedUserParams | undefined;
69
+ }) => MaybePromise<DatabaseUser | undefined | null>;
70
+ /**
71
+ * Get JWT keys produced by {@link generateNewJwtKeys}. Make sure that each time this is
72
+ * called, the same JWT keys are returned (do not call {@link generateNewJwtKeys} each time
73
+ * this is called). Any time the JWT keys change, all current sessions will terminate.
74
+ */
75
+ getJetKeys: () => MaybePromise<Readonly<RawJwtKeys>>;
76
+ /**
77
+ * When `isDev` is set, cookies do not require HTTPS (so they can be used with
78
+ * http://localhost).
79
+ */
80
+ isDev: boolean;
81
+ } & PartialWithUndefined<{
82
+ /**
83
+ * Set this to allow specific users (determined by `canAssumeUser`) to assume the identity
84
+ * of other users. This should only be used for admins so that they can troubleshoot user
85
+ * issues.
86
+ *
87
+ * @see {@link AuthHeaderName}
88
+ */
89
+ assumeUser: {
90
+ /**
91
+ * Handles assumed user header value.
92
+ *
93
+ * @see {@link AuthHeaderName}
94
+ */
95
+ handleAssumedUserData: (
96
+ /**
97
+ * The assumed user header value.
98
+ *
99
+ * @see {@link AuthHeaderName}
100
+ */
101
+ data: string,
102
+ ) => MaybePromise<
103
+ | {
104
+ assumedUserParams: AssumedUserParams;
105
+ userId: UserId;
106
+ }
107
+ | undefined
108
+ >;
109
+ /**
110
+ * Return `true` to allow the current user (by the given id) to assume identities of
111
+ * other users. Return `false` to block it. It is recommended to only return `true` for
112
+ * admin users.
113
+ *
114
+ * @see {@link AuthHeaderName}
115
+ */
116
+ canAssumeUser: (params: {userId: UserId}) => MaybePromise<boolean>;
117
+ };
118
+ /**
119
+ * This determines how long a cookie will be valid until it needs to be refreshed.
120
+ *
121
+ * @default {minutes: 20}
122
+ */
123
+ userSessionIdleTimeout: Readonly<AnyDuration>;
124
+ /**
125
+ * How long before a user's session times out when we should start trying to refresh their
126
+ * session.
127
+ *
128
+ * @default {minutes: 5}
129
+ */
130
+ sessionRefreshThreshold: Readonly<AnyDuration>;
131
+ overrides: PartialWithUndefined<{
132
+ csrfHeaderName: CsrfHeaderName;
133
+ assumedUserHeaderName: string;
134
+ }>;
135
+ }>
136
+ >;
137
+
138
+ const defaultSessionIdleTimeout: Readonly<AnyDuration> = {
139
+ minutes: 20,
140
+ };
141
+
142
+ /**
143
+ * An auth client for creating and validating JWTs embedded in cookies. This should only be used in
144
+ * a backend environment as it accesses native Node packages.
145
+ *
146
+ * @category Auth : Host
147
+ * @category Client
148
+ */
149
+ export class BackendAuthClient<
150
+ DatabaseUser extends AnyObject,
151
+ UserId extends string | number,
152
+ CsrfHeaderName extends string = AuthHeaderName.CsrfToken,
153
+ AssumedUserParams extends AnyObject = EmptyObject,
154
+ > {
155
+ protected cachedParsedJwtKeys: Record<string, Readonly<JwtKeys>> = {};
156
+
157
+ constructor(
158
+ protected readonly config: BackendAuthClientConfig<
159
+ DatabaseUser,
160
+ UserId,
161
+ CsrfHeaderName,
162
+ AssumedUserParams
163
+ >,
164
+ ) {}
165
+
166
+ /** Get all the parameters used for cookie generation. */
167
+ protected async getCookieParams({
168
+ isSignUpCookie,
169
+ }: {
170
+ /**
171
+ * Set this to `true` when we are setting the initial cookie right after a user signs up.
172
+ * This allows them to auto-authorize when they verify their email address.
173
+ *
174
+ * This should only be set to `true` when a new user is signing up.
175
+ */
176
+ isSignUpCookie?: boolean | undefined;
177
+ }): Promise<Readonly<CookieParams>> {
178
+ return {
179
+ cookieDuration: this.config.userSessionIdleTimeout || defaultSessionIdleTimeout,
180
+ hostOrigin: this.config.serviceOrigin,
181
+ jwtParams: await this.getJwtParams(),
182
+ isDev: this.config.isDev,
183
+ cookieName: isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth,
184
+ };
185
+ }
186
+
187
+ /** Calls the provided `getUserFromDatabase` config. */
188
+ protected async getDatabaseUser({
189
+ isSignUpCookie,
190
+ userId,
191
+ assumedUserParams,
192
+ }: {
193
+ userId: UserId | undefined;
194
+ assumedUserParams: AssumedUserParams | undefined;
195
+ isSignUpCookie: boolean;
196
+ }): Promise<undefined | DatabaseUser> {
197
+ if (!userId) {
198
+ return undefined;
199
+ }
200
+
201
+ const authenticatedUser = await this.config.getUserFromDatabase({
202
+ assumedUserParams,
203
+ userId,
204
+ isSignUpCookie,
205
+ });
206
+
207
+ if (!authenticatedUser) {
208
+ return undefined;
209
+ }
210
+
211
+ return authenticatedUser;
212
+ }
213
+
214
+ /** Creates a `'cookie-set'` header to refresh the user's session cookie. */
215
+ protected async createCookieRefreshHeaders({
216
+ userIdResult,
217
+ }: {
218
+ userIdResult: Readonly<UserIdResult<UserId>>;
219
+ }): Promise<OutgoingHttpHeaders | undefined> {
220
+ const now = getNowInUtcTimezone();
221
+
222
+ /** Double check that the JWT hasn't already expired. */
223
+ const isExpiredAlready = isDateAfter({
224
+ fullDate: now,
225
+ relativeTo: userIdResult.jwtExpiration,
226
+ });
227
+
228
+ if (isExpiredAlready) {
229
+ return undefined;
230
+ }
231
+
232
+ /**
233
+ * This check performs the following: the current time + the refresh threshold > JWT
234
+ * expiration.
235
+ *
236
+ * Visually, this check looks like this:
237
+ *
238
+ * X C=======Y=======R Z
239
+ *
240
+ * - C = current time
241
+ * - R = C + refresh threshold
242
+ * - `=` = the time frame in which {@link isRefreshReady} = true.
243
+ * - X = JWT expiration that has already expired (rejected by {@link isExpiredAlready}.
244
+ * - Y = JWT expiration within the refresh threshold: {@link isRefreshReady} = true.
245
+ * - Z = JWT expiration outside the refresh threshold: {@link isRefreshReady} = false.
246
+ */
247
+ const isRefreshReady = isDateAfter({
248
+ fullDate: calculateRelativeDate(
249
+ now,
250
+ this.config.sessionRefreshThreshold || {
251
+ minutes: 5,
252
+ },
253
+ ),
254
+ relativeTo: userIdResult.jwtExpiration,
255
+ });
256
+
257
+ if (isRefreshReady) {
258
+ return this.createLoginHeaders({
259
+ requestHeaders: {},
260
+ userId: userIdResult.userId,
261
+ isSignUpCookie: userIdResult.cookieName === AuthCookieName.SignUp,
262
+ });
263
+ } else {
264
+ return undefined;
265
+ }
266
+ }
267
+
268
+ /** Reads the user's assumed user headers and, if configured, gets the assumed user. */
269
+ protected async getAssumedUser({
270
+ headers,
271
+ originalUserId,
272
+ }: {
273
+ originalUserId: UserId | undefined;
274
+ headers: IncomingHttpHeaders;
275
+ }): Promise<DatabaseUser | undefined> {
276
+ if (
277
+ !originalUserId ||
278
+ !this.config.assumeUser ||
279
+ !(await this.config.assumeUser.canAssumeUser({
280
+ userId: originalUserId,
281
+ }))
282
+ ) {
283
+ return undefined;
284
+ }
285
+
286
+ const assumedUserHeader: string | undefined = ensureArray(
287
+ headers[this.config.overrides?.assumedUserHeaderName || AuthHeaderName.AssumedUser],
288
+ )[0];
289
+
290
+ if (!assumedUserHeader) {
291
+ return undefined;
292
+ }
293
+
294
+ const parsedAssumedUserData =
295
+ await this.config.assumeUser.handleAssumedUserData(assumedUserHeader);
296
+
297
+ if (!parsedAssumedUserData || !parsedAssumedUserData.userId) {
298
+ return undefined;
299
+ }
300
+
301
+ const assumedUser = await this.getDatabaseUser({
302
+ isSignUpCookie: false,
303
+ userId: parsedAssumedUserData.userId,
304
+ assumedUserParams: parsedAssumedUserData.assumedUserParams,
305
+ });
306
+
307
+ return assumedUser;
308
+ }
309
+
310
+ /** Securely extract a user from their request headers. */
311
+ public async getSecureUser({
312
+ requestHeaders,
313
+ isSignUpCookie,
314
+ }: {
315
+ requestHeaders: IncomingHttpHeaders;
316
+ isSignUpCookie?: boolean | undefined;
317
+ }): Promise<GetUserResult<DatabaseUser> | undefined> {
318
+ const userIdResult = await extractUserIdFromRequestHeaders<UserId>(
319
+ requestHeaders,
320
+ await this.getJwtParams(),
321
+ isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth,
322
+ this.config.overrides,
323
+ );
324
+ if (!userIdResult) {
325
+ return undefined;
326
+ }
327
+
328
+ const user = await this.getDatabaseUser({
329
+ userId: userIdResult.userId,
330
+ assumedUserParams: undefined,
331
+ isSignUpCookie: !!isSignUpCookie,
332
+ });
333
+
334
+ if (!user) {
335
+ return undefined;
336
+ }
337
+
338
+ const assumedUser = await this.getAssumedUser({
339
+ headers: requestHeaders,
340
+ originalUserId: userIdResult.userId,
341
+ });
342
+
343
+ const cookieRefreshHeaders =
344
+ (await this.createCookieRefreshHeaders({
345
+ userIdResult,
346
+ })) || {};
347
+
348
+ if (assumedUser) {
349
+ return {
350
+ user: assumedUser,
351
+ isAssumed: true,
352
+ responseHeaders: cookieRefreshHeaders,
353
+ };
354
+ } else {
355
+ return {
356
+ user,
357
+ isAssumed: false,
358
+ responseHeaders: cookieRefreshHeaders,
359
+ };
360
+ }
361
+ }
362
+
363
+ /**
364
+ * Get all the JWT params used when creating the auth cookie, in case you need them for
365
+ * something else too.
366
+ */
367
+ public async getJwtParams(): Promise<Readonly<CreateJwtParams>> {
368
+ const rawJwtKeys = await this.config.getJetKeys();
369
+
370
+ const cacheKey = JSON.stringify(rawJwtKeys);
371
+
372
+ const cachedParsedKeys = this.cachedParsedJwtKeys[cacheKey];
373
+ const parsedKeys = cachedParsedKeys ?? (await parseJwtKeys(rawJwtKeys));
374
+
375
+ if (!cachedParsedKeys) {
376
+ this.cachedParsedJwtKeys = {[cacheKey]: parsedKeys};
377
+ }
378
+ return {
379
+ jwtKeys: parsedKeys,
380
+ audience: 'server-context',
381
+ issuer: 'server-auth',
382
+ jwtDuration: this.config.userSessionIdleTimeout || defaultSessionIdleTimeout,
383
+ };
384
+ }
385
+
386
+ /** Use these headers to log out the user. */
387
+ public async createLogoutHeaders(): Promise<
388
+ {
389
+ 'set-cookie': string;
390
+ } & Record<CsrfHeaderName, string>
391
+ > {
392
+ const signUpCookieHeaders = generateLogoutHeaders(
393
+ await this.getCookieParams({
394
+ isSignUpCookie: true,
395
+ }),
396
+ this.config.overrides,
397
+ );
398
+ const authCookieHeaders = generateLogoutHeaders(
399
+ await this.getCookieParams({
400
+ isSignUpCookie: false,
401
+ }),
402
+ this.config.overrides,
403
+ );
404
+
405
+ return {
406
+ ...authCookieHeaders,
407
+ 'set-cookie': mergeHeaderValues(
408
+ signUpCookieHeaders['set-cookie'],
409
+ authCookieHeaders['set-cookie'],
410
+ ),
411
+ };
412
+ }
413
+
414
+ /** Use these headers to log a user in. */
415
+ public async createLoginHeaders({
416
+ userId,
417
+ requestHeaders,
418
+ isSignUpCookie,
419
+ }: {
420
+ userId: UserId;
421
+ requestHeaders: IncomingHttpHeaders;
422
+ isSignUpCookie: boolean;
423
+ }): Promise<
424
+ Pick<RequiredAndNotNull<OutgoingHttpHeaders>, 'set-cookie'> & Record<CsrfHeaderName, string>
425
+ > {
426
+ const oppositeCookieName = isSignUpCookie ? AuthCookieName.Auth : AuthCookieName.SignUp;
427
+ const hasExistingOppositeCookie = requestHeaders.cookie?.includes(`${oppositeCookieName}=`);
428
+
429
+ const discardOppositeCookieHeaders = hasExistingOppositeCookie
430
+ ? generateLogoutHeaders(
431
+ await this.getCookieParams({
432
+ isSignUpCookie: !isSignUpCookie,
433
+ }),
434
+ this.config.overrides,
435
+ )
436
+ : undefined;
437
+
438
+ const newCookieHeaders = await generateSuccessfulLoginHeaders(
439
+ userId,
440
+ await this.getCookieParams({
441
+ isSignUpCookie,
442
+ }),
443
+ this.config.overrides,
444
+ );
445
+
446
+ return {
447
+ ...newCookieHeaders,
448
+ 'set-cookie': mergeHeaderValues(
449
+ newCookieHeaders['set-cookie'],
450
+ discardOppositeCookieHeaders?.['set-cookie'],
451
+ ),
452
+ ...(isSignUpCookie
453
+ ? {
454
+ [AuthHeaderName.IsSignUpAuth]: 'true',
455
+ }
456
+ : {}),
457
+ };
458
+ }
459
+
460
+ /**
461
+ * @deprecated This only half authenticates the user. It should only be used in circumstances
462
+ * where JavaScript cannot be used to attach the CSRF token header to the request (like when
463
+ * opening a PDF file). Use `.getSecureUser()` instead, whenever possible.
464
+ */
465
+ public async getInsecureUser({
466
+ headers,
467
+ }: {
468
+ headers: IncomingHttpHeaders;
469
+ }): Promise<GetUserResult<DatabaseUser> | undefined> {
470
+ // eslint-disable-next-line @typescript-eslint/no-deprecated
471
+ const userIdResult = await insecureExtractUserIdFromCookieAlone<UserId>(
472
+ headers,
473
+ await this.getJwtParams(),
474
+ AuthCookieName.Auth,
475
+ );
476
+
477
+ if (!userIdResult) {
478
+ return undefined;
479
+ }
480
+
481
+ const user = await this.getDatabaseUser({
482
+ isSignUpCookie: false,
483
+ userId: userIdResult.userId,
484
+ assumedUserParams: undefined,
485
+ });
486
+
487
+ if (!user) {
488
+ return undefined;
489
+ }
490
+
491
+ return {
492
+ user,
493
+ isAssumed: false,
494
+ responseHeaders:
495
+ (await this.createCookieRefreshHeaders({
496
+ userIdResult,
497
+ })) || {},
498
+ };
499
+ }
500
+ }
@@ -0,0 +1,182 @@
1
+ import {
2
+ HttpStatus,
3
+ type JsonCompatibleObject,
4
+ type MaybePromise,
5
+ type PartialWithUndefined,
6
+ type SelectFrom,
7
+ } from '@augment-vir/common';
8
+ import {type EmptyObject} from 'type-fest';
9
+ import {
10
+ CsrfTokenFailureReason,
11
+ extractCsrfTokenHeader,
12
+ getCurrentCsrfToken,
13
+ storeCsrfToken,
14
+ wipeCurrentCsrfToken,
15
+ } from '../csrf-token.js';
16
+ import {AuthHeaderName} from '../headers.js';
17
+
18
+ /**
19
+ * Config for {@link FrontendAuthClient}.
20
+ *
21
+ * @category Internal
22
+ */
23
+ export type FrontendAuthClientConfig = PartialWithUndefined<{
24
+ /**
25
+ * Determine if the current user can assume the identity of another user. If this is not
26
+ * defined, all users will be blocked from assuming other user identities.
27
+ */
28
+ canAssumeUser: () => MaybePromise<boolean>;
29
+ /** Called whenever the current user becomes unauthorized and their CSRF token is wiped. */
30
+ authClearedCallback: () => MaybePromise<void>;
31
+ overrides: PartialWithUndefined<{
32
+ localStorage: Pick<Storage, 'setItem' | 'removeItem' | 'getItem'>;
33
+ csrfHeaderName: string;
34
+ assumedUserHeaderName: string;
35
+ }>;
36
+ }>;
37
+
38
+ /**
39
+ * An auth client for sending and validating client requests to a backend. This should only be used
40
+ * in a frontend environment as it accesses native browser APIs.
41
+ *
42
+ * @category Auth : Client
43
+ * @category Client
44
+ */
45
+ export class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
46
+ constructor(protected readonly config: FrontendAuthClientConfig = {}) {}
47
+
48
+ /** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
49
+ public async getCurrentCsrfToken(): Promise<string | undefined> {
50
+ const csrfTokenResult = getCurrentCsrfToken(this.config.overrides);
51
+
52
+ if (
53
+ csrfTokenResult.failure &&
54
+ csrfTokenResult.failure !== CsrfTokenFailureReason.DoesNotExist
55
+ ) {
56
+ await this.logout();
57
+ return undefined;
58
+ } else {
59
+ return csrfTokenResult.csrfToken?.token;
60
+ }
61
+ }
62
+
63
+ /** @returns Whether the user assuming succeeded or not. */
64
+ public async assumeUser(assumedUserParams: Readonly<AssumedUserParams>): Promise<boolean> {
65
+ if (!(await this.config.canAssumeUser?.())) {
66
+ return false;
67
+ }
68
+
69
+ (this.config.overrides?.localStorage || globalThis.localStorage).setItem(
70
+ this.config.overrides?.assumedUserHeaderName || AuthHeaderName.AssumedUser,
71
+ JSON.stringify(assumedUserParams),
72
+ );
73
+
74
+ return true;
75
+ }
76
+
77
+ public getAssumedUser(): AssumedUserParams | undefined {
78
+ const rawValue = (this.config.overrides?.localStorage || globalThis.localStorage).getItem(
79
+ this.config.overrides?.assumedUserHeaderName || AuthHeaderName.AssumedUser,
80
+ );
81
+
82
+ if (!rawValue) {
83
+ return undefined;
84
+ }
85
+ try {
86
+ return JSON.parse(rawValue);
87
+ } catch {
88
+ return undefined;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Creates a `RequestInit` object for the `fetch` API. If you have other request init options,
94
+ * use [`mergeDeep` from
95
+ * `@augment-vir/common`](https://electrovir.github.io/augment-vir/functions/mergeDeep.html) to
96
+ * combine them with these.
97
+ */
98
+ public async createAuthenticatedRequestInit(): Promise<RequestInit> {
99
+ const csrfToken = await this.getCurrentCsrfToken();
100
+
101
+ const assumedUser = this.getAssumedUser();
102
+ const headers: HeadersInit = {
103
+ ...(csrfToken
104
+ ? {
105
+ [AuthHeaderName.CsrfToken]: csrfToken,
106
+ }
107
+ : {}),
108
+ ...(assumedUser
109
+ ? {
110
+ [this.config.overrides?.assumedUserHeaderName || AuthHeaderName.AssumedUser]:
111
+ JSON.stringify(assumedUser),
112
+ }
113
+ : {}),
114
+ };
115
+
116
+ return {
117
+ headers,
118
+ credentials: 'include',
119
+ };
120
+ }
121
+
122
+ /** Wipes the current user auth. */
123
+ public async logout() {
124
+ await this.config.authClearedCallback?.();
125
+ wipeCurrentCsrfToken(this.config.overrides);
126
+ }
127
+
128
+ /**
129
+ * Use to handle a login response. Automatically stores the CSRF token.
130
+ *
131
+ * @throws Error if the login response failed.
132
+ * @throws Error if the login response has an invalid CSRF token.
133
+ */
134
+ public async handleLoginResponse(
135
+ response: Readonly<
136
+ SelectFrom<
137
+ Response,
138
+ {
139
+ headers: true;
140
+ ok: true;
141
+ }
142
+ >
143
+ >,
144
+ ): Promise<void> {
145
+ if (!response.ok) {
146
+ await this.logout();
147
+ throw new Error('Login response failed.');
148
+ }
149
+
150
+ const {csrfToken} = extractCsrfTokenHeader(response, this.config.overrides);
151
+
152
+ if (!csrfToken) {
153
+ await this.logout();
154
+ throw new Error('Did not receive any CSRF token.');
155
+ }
156
+
157
+ storeCsrfToken(csrfToken, this.config.overrides);
158
+ }
159
+
160
+ /**
161
+ * Use to verify _all_ responses received from the backend. Immediately logs the user out once
162
+ * an unauthorized response is detected.
163
+ */
164
+ public async verifyResponseAuth(
165
+ response: Readonly<
166
+ SelectFrom<
167
+ Response,
168
+ {
169
+ status: true;
170
+ headers: true;
171
+ }
172
+ >
173
+ >,
174
+ ): Promise<void> {
175
+ if (
176
+ response.status === HttpStatus.Unauthorized &&
177
+ !response.headers.get(AuthHeaderName.IsSignUpAuth)
178
+ ) {
179
+ await this.logout();
180
+ }
181
+ }
182
+ }