auth-vir 2.5.0 → 2.7.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 (40) hide show
  1. package/dist/auth-client/backend-auth.client.d.ts +10 -4
  2. package/dist/auth-client/backend-auth.client.js +50 -6
  3. package/dist/auth-client/frontend-auth.client.d.ts +2 -0
  4. package/dist/auth-client/frontend-auth.client.js +14 -1
  5. package/dist/auth.d.ts +12 -1
  6. package/dist/auth.js +32 -3
  7. package/dist/csrf-token.js +6 -0
  8. package/dist/generated/browser.js +1 -0
  9. package/dist/generated/client.js +1 -0
  10. package/dist/generated/enums.js +1 -0
  11. package/dist/generated/internal/class.js +5 -4
  12. package/dist/generated/internal/prismaNamespace.d.ts +2 -2
  13. package/dist/generated/internal/prismaNamespace.js +5 -4
  14. package/dist/generated/internal/prismaNamespaceBrowser.js +1 -0
  15. package/dist/index.d.ts +1 -0
  16. package/dist/index.js +1 -0
  17. package/dist/jwt/jwt.d.ts +12 -0
  18. package/dist/jwt/jwt.js +22 -12
  19. package/dist/jwt/user-jwt.d.ts +6 -0
  20. package/dist/jwt/user-jwt.js +7 -1
  21. package/dist/log.d.ts +12 -0
  22. package/dist/log.js +17 -0
  23. package/package.json +11 -11
  24. package/src/auth-client/backend-auth.client.ts +86 -11
  25. package/src/auth-client/frontend-auth.client.ts +14 -1
  26. package/src/auth.ts +49 -2
  27. package/src/csrf-token.ts +6 -0
  28. package/src/generated/browser.ts +1 -0
  29. package/src/generated/client.ts +1 -0
  30. package/src/generated/commonInputTypes.ts +1 -0
  31. package/src/generated/enums.ts +1 -0
  32. package/src/generated/internal/class.ts +5 -4
  33. package/src/generated/internal/prismaNamespace.ts +5 -4
  34. package/src/generated/internal/prismaNamespaceBrowser.ts +1 -0
  35. package/src/generated/models/User.ts +1 -0
  36. package/src/generated/models.ts +1 -0
  37. package/src/index.ts +1 -0
  38. package/src/jwt/jwt.ts +26 -14
  39. package/src/jwt/user-jwt.ts +7 -1
  40. package/src/log.ts +18 -0
@@ -103,12 +103,18 @@ export type BackendAuthClientConfig<DatabaseUser extends AnyObject, UserId exten
103
103
  */
104
104
  userSessionIdleTimeout: Readonly<AnyDuration>;
105
105
  /**
106
- * How long before a user's session times out when we should start trying to refresh their
107
- * session.
106
+ * How long into a user's session when we should start trying to refresh their session.
108
107
  *
109
- * @default {minutes: 10}
108
+ * @default {minutes: 2}
110
109
  */
111
- sessionRefreshThreshold: Readonly<AnyDuration>;
110
+ sessionRefreshTimeout: Readonly<AnyDuration>;
111
+ /**
112
+ * The maximum duration a session can last, regardless of activity. After this time, the
113
+ * user will be logged out even if they are actively using the application.
114
+ *
115
+ * @default {weeks: 2}
116
+ */
117
+ maxSessionDuration: Readonly<AnyDuration>;
112
118
  overrides: PartialWithUndefined<{
113
119
  csrfHeaderName: CsrfHeaderName;
114
120
  assumedUserHeaderName: string;
@@ -1,14 +1,18 @@
1
1
  import { ensureArray, } from '@augment-vir/common';
2
- import { calculateRelativeDate, getNowInUtcTimezone, isDateAfter } from 'date-vir';
2
+ import { calculateRelativeDate, createUtcFullDate, getNowInUtcTimezone, isDateAfter, negateDuration, } from 'date-vir';
3
3
  import { extractUserIdFromRequestHeaders, generateLogoutHeaders, generateSuccessfulLoginHeaders, insecureExtractUserIdFromCookieAlone, } from '../auth.js';
4
4
  import { AuthCookieName } from '../cookie.js';
5
5
  import { AuthHeaderName, mergeHeaderValues } from '../headers.js';
6
6
  import { parseJwtKeys } from '../jwt/jwt-keys.js';
7
+ import { authLog } from '../log.js';
7
8
  const defaultSessionIdleTimeout = {
8
9
  minutes: 20,
9
10
  };
10
- const defaultSessionRefreshThreshold = {
11
- minutes: 10,
11
+ const defaultSessionRefreshTimeout = {
12
+ minutes: 2,
13
+ };
14
+ const defaultMaxSessionDuration = {
15
+ weeks: 2,
12
16
  };
13
17
  /**
14
18
  * An auth client for creating and validating JWTs embedded in cookies. This should only be used in
@@ -60,8 +64,33 @@ export class BackendAuthClient {
60
64
  relativeTo: userIdResult.jwtExpiration,
61
65
  });
62
66
  if (isExpiredAlready) {
67
+ authLog('auth-vir: SESSION EXPIRED - JWT already expired, user will be logged out', {
68
+ userId: userIdResult.userId,
69
+ jwtExpiration: userIdResult.jwtExpiration,
70
+ });
63
71
  return undefined;
64
72
  }
73
+ /**
74
+ * Check if the session has exceeded the max session duration. If so, don't refresh the
75
+ * session and let it expire naturally.
76
+ */
77
+ const maxSessionDuration = this.config.maxSessionDuration || defaultMaxSessionDuration;
78
+ if (userIdResult.sessionStartedAt) {
79
+ const sessionStartDate = createUtcFullDate(userIdResult.sessionStartedAt);
80
+ const maxSessionEndDate = calculateRelativeDate(sessionStartDate, maxSessionDuration);
81
+ const isSessionExpired = isDateAfter({
82
+ fullDate: now,
83
+ relativeTo: maxSessionEndDate,
84
+ });
85
+ if (isSessionExpired) {
86
+ authLog('auth-vir: SESSION EXPIRED - max session duration exceeded, user will be logged out', {
87
+ userId: userIdResult.userId,
88
+ sessionStartedAt: userIdResult.sessionStartedAt,
89
+ maxSessionDuration,
90
+ });
91
+ return undefined;
92
+ }
93
+ }
65
94
  /**
66
95
  * This check performs the following: the current time + the refresh threshold > JWT
67
96
  * expiration.
@@ -77,9 +106,10 @@ export class BackendAuthClient {
77
106
  * - Y = JWT expiration within the refresh threshold: {@link isRefreshReady} = true.
78
107
  * - Z = JWT expiration outside the refresh threshold: {@link isRefreshReady} = false.
79
108
  */
109
+ const sessionRefreshTimeout = this.config.sessionRefreshTimeout || defaultSessionRefreshTimeout;
80
110
  const isRefreshReady = isDateAfter({
81
- fullDate: calculateRelativeDate(now, this.config.sessionRefreshThreshold || defaultSessionRefreshThreshold),
82
- relativeTo: userIdResult.jwtExpiration,
111
+ fullDate: now,
112
+ relativeTo: calculateRelativeDate(userIdResult.jwtExpiration, negateDuration(sessionRefreshTimeout)),
83
113
  });
84
114
  if (isRefreshReady) {
85
115
  return this.createLoginHeaders({
@@ -116,6 +146,7 @@ export class BackendAuthClient {
116
146
  async getSecureUser({ requestHeaders, isSignUpCookie, allowUserAuthRefresh, }) {
117
147
  const userIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth, this.config.overrides);
118
148
  if (!userIdResult) {
149
+ authLog('auth-vir: getSecureUser failed - could not extract user from request');
119
150
  return undefined;
120
151
  }
121
152
  const user = await this.getDatabaseUser({
@@ -124,6 +155,9 @@ export class BackendAuthClient {
124
155
  isSignUpCookie,
125
156
  });
126
157
  if (!user) {
158
+ authLog('auth-vir: getSecureUser failed - user not found in database', {
159
+ userId: userIdResult.userId,
160
+ });
127
161
  return undefined;
128
162
  }
129
163
  const assumedUser = await this.getAssumedUser({
@@ -161,6 +195,10 @@ export class BackendAuthClient {
161
195
  }
162
196
  /** Use these headers to log out the user. */
163
197
  async createLogoutHeaders(params) {
198
+ authLog('auth-vir: LOGOUT - BackendAuthClient.createLogoutHeaders called', {
199
+ allCookies: 'allCookies' in params ? params.allCookies : undefined,
200
+ isSignUpCookie: 'isSignUpCookie' in params ? params.isSignUpCookie : undefined,
201
+ }, new Error().stack);
164
202
  const signUpCookieHeaders = params.allCookies || params.isSignUpCookie
165
203
  ? generateLogoutHeaders(await this.getCookieParams({
166
204
  isSignUpCookie: true,
@@ -195,10 +233,12 @@ export class BackendAuthClient {
195
233
  requestHeaders,
196
234
  }), this.config.overrides)
197
235
  : undefined;
236
+ const existingUserIdResult = await extractUserIdFromRequestHeaders(requestHeaders, await this.getJwtParams(), isSignUpCookie ? AuthCookieName.SignUp : AuthCookieName.Auth, this.config.overrides);
237
+ const sessionStartedAt = existingUserIdResult?.sessionStartedAt;
198
238
  const newCookieHeaders = await generateSuccessfulLoginHeaders(userId, await this.getCookieParams({
199
239
  isSignUpCookie,
200
240
  requestHeaders,
201
- }), this.config.overrides);
241
+ }), this.config.overrides, sessionStartedAt);
202
242
  return {
203
243
  ...newCookieHeaders,
204
244
  'set-cookie': mergeHeaderValues(newCookieHeaders['set-cookie'], discardOppositeCookieHeaders?.['set-cookie']),
@@ -228,6 +268,7 @@ export class BackendAuthClient {
228
268
  // eslint-disable-next-line @typescript-eslint/no-deprecated
229
269
  const userIdResult = await insecureExtractUserIdFromCookieAlone(requestHeaders, await this.getJwtParams(), AuthCookieName.Auth);
230
270
  if (!userIdResult) {
271
+ authLog('auth-vir: getInsecureUser failed - could not extract user from request');
231
272
  return undefined;
232
273
  }
233
274
  const user = await this.getDatabaseUser({
@@ -236,6 +277,9 @@ export class BackendAuthClient {
236
277
  assumingUser: undefined,
237
278
  });
238
279
  if (!user) {
280
+ authLog('auth-vir: getInsecureUser failed - user not found in database', {
281
+ userId: userIdResult.userId,
282
+ });
239
283
  return undefined;
240
284
  }
241
285
  const refreshHeaders = allowUserAuthRefresh &&
@@ -56,6 +56,8 @@ export type FrontendAuthClientConfig = PartialWithUndefined<{
56
56
  export declare class FrontendAuthClient<AssumedUserParams extends JsonCompatibleObject = EmptyObject> {
57
57
  protected readonly config: FrontendAuthClientConfig;
58
58
  protected userCheckInterval: undefined | ReturnType<typeof createBlockingInterval>;
59
+ /** Used to clean up the activity listener on `.destroy()`. */
60
+ protected removeActivityListener: VoidFunction | undefined;
59
61
  constructor(config?: FrontendAuthClientConfig);
60
62
  /**
61
63
  * Destroys the client and performs all necessary cleanup (like clearing the user check
@@ -2,6 +2,7 @@ import { HttpStatus, } from '@augment-vir/common';
2
2
  import { listenToActivity } from 'detect-activity';
3
3
  import { CsrfTokenFailureReason, extractCsrfTokenHeader, getCurrentCsrfToken, storeCsrfToken, wipeCurrentCsrfToken, } from '../csrf-token.js';
4
4
  import { AuthHeaderName } from '../headers.js';
5
+ import { authLog } from '../log.js';
5
6
  /**
6
7
  * An auth client for sending and validating client requests to a backend. This should only be used
7
8
  * in a frontend environment as it accesses native browser APIs.
@@ -12,10 +13,12 @@ import { AuthHeaderName } from '../headers.js';
12
13
  export class FrontendAuthClient {
13
14
  config;
14
15
  userCheckInterval;
16
+ /** Used to clean up the activity listener on `.destroy()`. */
17
+ removeActivityListener;
15
18
  constructor(config = {}) {
16
19
  this.config = config;
17
20
  if (config.checkUser) {
18
- listenToActivity({
21
+ this.removeActivityListener = listenToActivity({
19
22
  listener: async () => {
20
23
  const response = await config.checkUser?.performCheck();
21
24
  if (response) {
@@ -35,12 +38,16 @@ export class FrontendAuthClient {
35
38
  */
36
39
  destroy() {
37
40
  this.userCheckInterval?.clearInterval();
41
+ this.removeActivityListener?.();
38
42
  }
39
43
  /** Wraps {@link getCurrentCsrfToken} to automatically handle wiping an invalid CSRF token. */
40
44
  async getCurrentCsrfToken() {
41
45
  const csrfTokenResult = getCurrentCsrfToken(this.config.overrides);
42
46
  if (csrfTokenResult.failure &&
43
47
  csrfTokenResult.failure !== CsrfTokenFailureReason.DoesNotExist) {
48
+ authLog('auth-vir: LOGOUT - getCurrentCsrfToken: invalid CSRF token', {
49
+ failure: csrfTokenResult.failure,
50
+ });
44
51
  await this.logout();
45
52
  return undefined;
46
53
  }
@@ -107,6 +114,7 @@ export class FrontendAuthClient {
107
114
  }
108
115
  /** Wipes the current user auth. */
109
116
  async logout() {
117
+ authLog('auth-vir: LOGOUT - FrontendAuthClient.logout called', new Error().stack);
110
118
  await this.config.authClearedCallback?.();
111
119
  wipeCurrentCsrfToken(this.config.overrides);
112
120
  }
@@ -118,11 +126,13 @@ export class FrontendAuthClient {
118
126
  */
119
127
  async handleLoginResponse(response) {
120
128
  if (!response.ok) {
129
+ authLog('auth-vir: LOGOUT - handleLoginResponse: response not ok');
121
130
  await this.logout();
122
131
  throw new Error('Login response failed.');
123
132
  }
124
133
  const { csrfToken } = extractCsrfTokenHeader(response, this.config.overrides);
125
134
  if (!csrfToken) {
135
+ authLog('auth-vir: LOGOUT - handleLoginResponse: no CSRF token in response');
126
136
  await this.logout();
127
137
  throw new Error('Did not receive any CSRF token.');
128
138
  }
@@ -137,6 +147,9 @@ export class FrontendAuthClient {
137
147
  async verifyResponseAuth(response) {
138
148
  if (response.status === HttpStatus.Unauthorized &&
139
149
  !response.headers?.get(AuthHeaderName.IsSignUpAuth)) {
150
+ authLog('auth-vir: LOGOUT - verifyResponseAuth: unauthorized response (401)', {
151
+ status: response.status,
152
+ });
140
153
  await this.logout();
141
154
  return false;
142
155
  }
package/dist/auth.d.ts CHANGED
@@ -3,6 +3,7 @@ import { type FullDate, type UtcTimezone } from 'date-vir';
3
3
  import { type CookieParams } from './cookie.js';
4
4
  import { AuthHeaderName } from './headers.js';
5
5
  import { type ParseJwtParams } from './jwt/jwt.js';
6
+ import { type JwtUserData } from './jwt/user-jwt.js';
6
7
  /**
7
8
  * All possible headers container types supported by {@link extractUserIdFromRequestHeaders}.
8
9
  *
@@ -18,6 +19,11 @@ export type UserIdResult<UserId extends string | number> = {
18
19
  userId: UserId;
19
20
  jwtExpiration: FullDate<UtcTimezone>;
20
21
  cookieName: string;
22
+ /**
23
+ * Unix timestamp (in milliseconds) when the session was originally started. Used to enforce max
24
+ * session duration.
25
+ */
26
+ sessionStartedAt: JwtUserData['sessionStartedAt'];
21
27
  };
22
28
  /**
23
29
  * Extract the user id from a request by checking both the request cookie and CSRF token. This is
@@ -48,7 +54,12 @@ export declare function generateSuccessfulLoginHeaders<CsrfHeaderName extends st
48
54
  /** The id from your database of the user you're authenticating. */
49
55
  userId: string | number, cookieConfig: Readonly<CookieParams>, overrides?: PartialWithUndefined<{
50
56
  csrfHeaderName: CsrfHeaderName;
51
- }>): Promise<{
57
+ }>,
58
+ /**
59
+ * The timestamp (in seconds) when the session originally started. If not provided, the current
60
+ * time will be used (for new sessions).
61
+ */
62
+ sessionStartedAt?: number | undefined): Promise<{
52
63
  'set-cookie': string;
53
64
  } & Record<CsrfHeaderName, string>>;
54
65
  /**
package/dist/auth.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { AuthCookieName, clearAuthCookie, extractCookieJwt, generateAuthCookie, } from './cookie.js';
2
2
  import { extractCsrfTokenHeader, generateCsrfToken, parseCsrfToken, storeCsrfToken, wipeCurrentCsrfToken, } from './csrf-token.js';
3
3
  import { AuthHeaderName } from './headers.js';
4
+ import { authLog } from './log.js';
4
5
  function readHeader(headers, headerName) {
5
6
  if (headers instanceof Headers) {
6
7
  return headers.get(headerName) || undefined;
@@ -38,19 +39,31 @@ export async function extractUserIdFromRequestHeaders(headers, jwtParams, cookie
38
39
  const csrfToken = readCsrfTokenHeader(headers, overrides);
39
40
  const cookie = readHeader(headers, 'cookie');
40
41
  if (!cookie || !csrfToken) {
42
+ authLog('auth-vir: extractUserIdFromRequestHeaders failed - missing cookie or CSRF token', {
43
+ hasCookie: !!cookie,
44
+ hasCsrfToken: !!csrfToken,
45
+ cookieName,
46
+ });
41
47
  return undefined;
42
48
  }
43
49
  const jwt = await extractCookieJwt(cookie, jwtParams, cookieName);
44
50
  if (!jwt || jwt.data.csrfToken !== csrfToken) {
51
+ authLog('auth-vir: extractUserIdFromRequestHeaders failed - JWT invalid or CSRF mismatch', {
52
+ hasJwt: !!jwt,
53
+ csrfMatch: jwt ? jwt.data.csrfToken === csrfToken : false,
54
+ cookieName,
55
+ });
45
56
  return undefined;
46
57
  }
47
58
  return {
48
59
  userId: jwt.data.userId,
49
60
  jwtExpiration: jwt.jwtExpiration,
50
61
  cookieName,
62
+ sessionStartedAt: jwt.data.sessionStartedAt,
51
63
  };
52
64
  }
53
- catch {
65
+ catch (error) {
66
+ authLog('auth-vir: extractUserIdFromRequestHeaders error', { error, cookieName });
54
67
  return undefined;
55
68
  }
56
69
  }
@@ -66,19 +79,23 @@ export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, c
66
79
  try {
67
80
  const cookie = readHeader(headers, 'cookie');
68
81
  if (!cookie) {
82
+ authLog('auth-vir: insecureExtractUserIdFromCookieAlone failed - no cookie');
69
83
  return undefined;
70
84
  }
71
85
  const jwt = await extractCookieJwt(cookie, jwtParams, cookieName);
72
86
  if (!jwt) {
87
+ authLog('auth-vir: insecureExtractUserIdFromCookieAlone failed - JWT extraction failed');
73
88
  return undefined;
74
89
  }
75
90
  return {
76
91
  userId: jwt.data.userId,
77
92
  jwtExpiration: jwt.jwtExpiration,
78
93
  cookieName,
94
+ sessionStartedAt: jwt.data.sessionStartedAt,
79
95
  };
80
96
  }
81
- catch {
97
+ catch (error) {
98
+ authLog('auth-vir: insecureExtractUserIdFromCookieAlone error', { error });
82
99
  return undefined;
83
100
  }
84
101
  }
@@ -89,13 +106,19 @@ export async function insecureExtractUserIdFromCookieAlone(headers, jwtParams, c
89
106
  */
90
107
  export async function generateSuccessfulLoginHeaders(
91
108
  /** The id from your database of the user you're authenticating. */
92
- userId, cookieConfig, overrides = {}) {
109
+ userId, cookieConfig, overrides = {},
110
+ /**
111
+ * The timestamp (in seconds) when the session originally started. If not provided, the current
112
+ * time will be used (for new sessions).
113
+ */
114
+ sessionStartedAt) {
93
115
  const csrfToken = generateCsrfToken(cookieConfig.cookieDuration);
94
116
  const csrfHeaderName = (overrides.csrfHeaderName || AuthHeaderName.CsrfToken);
95
117
  return {
96
118
  'set-cookie': await generateAuthCookie({
97
119
  csrfToken: csrfToken.token,
98
120
  userId,
121
+ sessionStartedAt: sessionStartedAt ?? Date.now(),
99
122
  }, cookieConfig),
100
123
  [csrfHeaderName]: JSON.stringify(csrfToken),
101
124
  };
@@ -107,6 +130,9 @@ userId, cookieConfig, overrides = {}) {
107
130
  * @category Auth : Host
108
131
  */
109
132
  export function generateLogoutHeaders(cookieConfig, overrides = {}) {
133
+ authLog('auth-vir: LOGOUT - generateLogoutHeaders called', {
134
+ cookieName: cookieConfig.cookieName,
135
+ }, new Error().stack);
110
136
  const csrfHeaderName = (overrides.csrfHeaderName || AuthHeaderName.CsrfToken);
111
137
  return {
112
138
  'set-cookie': clearAuthCookie(cookieConfig),
@@ -124,13 +150,16 @@ export function generateLogoutHeaders(cookieConfig, overrides = {}) {
124
150
  */
125
151
  export function handleAuthResponse(response, overrides = {}) {
126
152
  if (!response.ok) {
153
+ authLog('auth-vir: LOGOUT - handleAuthResponse: response not ok, wiping CSRF token');
127
154
  wipeCurrentCsrfToken(overrides);
128
155
  return;
129
156
  }
130
157
  const { csrfToken } = extractCsrfTokenHeader(response, overrides);
131
158
  if (!csrfToken) {
159
+ authLog('auth-vir: LOGOUT - handleAuthResponse: no CSRF token in response, wiping');
132
160
  wipeCurrentCsrfToken(overrides);
133
161
  throw new Error('Did not receive any CSRF token.');
134
162
  }
163
+ authLog('auth-vir: handleAuthResponse - successfully stored CSRF token');
135
164
  storeCsrfToken(csrfToken, overrides);
136
165
  }
@@ -2,6 +2,7 @@ import { randomString, wrapInTry, } from '@augment-vir/common';
2
2
  import { calculateRelativeDate, fullDateShape, getNowInUtcTimezone, isDateAfter, } from 'date-vir';
3
3
  import { defineShape, parseJsonWithShape } from 'object-shape-tester';
4
4
  import { AuthHeaderName } from './headers.js';
5
+ import { authLog } from './log.js';
5
6
  /**
6
7
  * Shape definition for {@link CsrfToken}.
7
8
  *
@@ -74,6 +75,7 @@ export function parseCsrfToken(value) {
74
75
  fallbackValue: undefined,
75
76
  });
76
77
  if (!csrfToken) {
78
+ authLog('auth-vir: CSRF token parse failed - will cause logout if used');
77
79
  return {
78
80
  failure: CsrfTokenFailureReason.ParseFailed,
79
81
  };
@@ -82,6 +84,9 @@ export function parseCsrfToken(value) {
82
84
  fullDate: getNowInUtcTimezone(),
83
85
  relativeTo: csrfToken.expiration,
84
86
  })) {
87
+ authLog('auth-vir: CSRF token expired - will cause logout', {
88
+ expiration: csrfToken.expiration,
89
+ });
85
90
  return {
86
91
  failure: CsrfTokenFailureReason.Expired,
87
92
  };
@@ -107,5 +112,6 @@ export function getCurrentCsrfToken(overrides = {}) {
107
112
  * @category Auth : Client
108
113
  */
109
114
  export function wipeCurrentCsrfToken(overrides = {}) {
115
+ authLog('auth-vir: wipeCurrentCsrfToken called', new Error().stack);
110
116
  return (overrides.localStorage || globalThis.localStorage).removeItem(overrides.csrfHeaderName || AuthHeaderName.CsrfToken);
111
117
  }
@@ -1,5 +1,6 @@
1
1
  /* !!! This is code generated by Prisma. Do not edit directly. !!! */
2
2
  /* eslint-disable */
3
+ // biome-ignore-all lint: generated file
3
4
  // @ts-nocheck
4
5
  /*
5
6
  * This file should be your main import to use Prisma-related types and utilities in a browser.
@@ -1,5 +1,6 @@
1
1
  /* !!! This is code generated by Prisma. Do not edit directly. !!! */
2
2
  /* eslint-disable */
3
+ // biome-ignore-all lint: generated file
3
4
  // @ts-nocheck
4
5
  /*
5
6
  * This file should be your main import to use Prisma. Through it you get access to all the models, enums, and input types.
@@ -1,5 +1,6 @@
1
1
  /* !!! This is code generated by Prisma. Do not edit directly. !!! */
2
2
  /* eslint-disable */
3
+ // biome-ignore-all lint: generated file
3
4
  // @ts-nocheck
4
5
  /*
5
6
  * This file exports all enum related types from the schema.
@@ -1,5 +1,6 @@
1
1
  /* !!! This is code generated by Prisma. Do not edit directly. !!! */
2
2
  /* eslint-disable */
3
+ // biome-ignore-all lint: generated file
3
4
  // @ts-nocheck
4
5
  /*
5
6
  * WARNING: This is an internal file that is subject to change!
@@ -21,8 +22,8 @@ const config = {
21
22
  "fromEnvVar": null
22
23
  },
23
24
  "config": {
24
- "moduleFormat": "esm",
25
- "engineType": "client"
25
+ "engineType": "client",
26
+ "moduleFormat": "esm"
26
27
  },
27
28
  "binaryTargets": [
28
29
  {
@@ -39,8 +40,8 @@ const config = {
39
40
  "isCustomOutput": true
40
41
  },
41
42
  "relativePath": "../../test-files",
42
- "clientVersion": "6.18.0",
43
- "engineVersion": "34b5a692b7bd79939a9a2c3ef97d816e749cda2f",
43
+ "clientVersion": "6.19.2",
44
+ "engineVersion": "c2990dca591cba766e3b7ef5d9e8a84796e47ab7",
44
45
  "datasourceNames": [
45
46
  "db"
46
47
  ],
@@ -58,8 +58,8 @@ export type PrismaVersion = {
58
58
  engine: string;
59
59
  };
60
60
  /**
61
- * Prisma Client JS version: 6.18.0
62
- * Query Engine version: 34b5a692b7bd79939a9a2c3ef97d816e749cda2f
61
+ * Prisma Client JS version: 6.19.2
62
+ * Query Engine version: c2990dca591cba766e3b7ef5d9e8a84796e47ab7
63
63
  */
64
64
  export declare const prismaVersion: PrismaVersion;
65
65
  /**
@@ -1,5 +1,6 @@
1
1
  /* !!! This is code generated by Prisma. Do not edit directly. !!! */
2
2
  /* eslint-disable */
3
+ // biome-ignore-all lint: generated file
3
4
  /*
4
5
  * WARNING: This is an internal file that is subject to change!
5
6
  *
@@ -38,12 +39,12 @@ export const skip = runtime.skip;
38
39
  export const Decimal = runtime.Decimal;
39
40
  export const getExtensionContext = runtime.Extensions.getExtensionContext;
40
41
  /**
41
- * Prisma Client JS version: 6.18.0
42
- * Query Engine version: 34b5a692b7bd79939a9a2c3ef97d816e749cda2f
42
+ * Prisma Client JS version: 6.19.2
43
+ * Query Engine version: c2990dca591cba766e3b7ef5d9e8a84796e47ab7
43
44
  */
44
45
  export const prismaVersion = {
45
- client: "6.18.0",
46
- engine: "34b5a692b7bd79939a9a2c3ef97d816e749cda2f"
46
+ client: "6.19.2",
47
+ engine: "c2990dca591cba766e3b7ef5d9e8a84796e47ab7"
47
48
  };
48
49
  export const NullTypes = {
49
50
  DbNull: runtime.objectEnumValues.classes.DbNull,
@@ -1,5 +1,6 @@
1
1
  /* !!! This is code generated by Prisma. Do not edit directly. !!! */
2
2
  /* eslint-disable */
3
+ // biome-ignore-all lint: generated file
3
4
  // @ts-nocheck
4
5
  /*
5
6
  * WARNING: This is an internal file that is subject to change!
package/dist/index.d.ts CHANGED
@@ -8,4 +8,5 @@ export * from './headers.js';
8
8
  export * from './jwt/jwt-keys.js';
9
9
  export * from './jwt/jwt.js';
10
10
  export * from './jwt/user-jwt.js';
11
+ export * from './log.js';
11
12
  export * from './mock-local-storage.js';
package/dist/index.js CHANGED
@@ -8,4 +8,5 @@ export * from './headers.js';
8
8
  export * from './jwt/jwt-keys.js';
9
9
  export * from './jwt/jwt.js';
10
10
  export * from './jwt/user-jwt.js';
11
+ export * from './log.js';
11
12
  export * from './mock-local-storage.js';
package/dist/jwt/jwt.d.ts CHANGED
@@ -60,6 +60,18 @@ export type CreateJwtParams = Readonly<{
60
60
  */
61
61
  notValidUntil: DateLike;
62
62
  }>>;
63
+ /**
64
+ * JWT uses seconds since the epoch per RFC 7519, whereas `toTimestamp` uses milliseconds.
65
+ *
66
+ * @category Internal
67
+ */
68
+ export declare function toJwtTimestamp(date: Readonly<FullDate>): number;
69
+ /**
70
+ * Converts a JWT timestamp (in seconds) into a FullDate instance.
71
+ *
72
+ * @category Internal
73
+ */
74
+ export declare function parseJwtTimestamp(seconds: number): FullDate<UtcTimezone>;
63
75
  /**
64
76
  * Creates a signed and encrypted JWT that contains the given data.
65
77
  *
package/dist/jwt/jwt.js CHANGED
@@ -1,8 +1,24 @@
1
1
  import { assertWrap, check } from '@augment-vir/assert';
2
- import { calculateRelativeDate, createFullDateInUserTimezone, createUtcFullDate, getNowInUtcTimezone, isDateAfter, toTimestamp, } from 'date-vir';
2
+ import { calculateRelativeDate, createFullDateInUserTimezone, createUtcFullDate, getNowInUtcTimezone, toTimestamp, } from 'date-vir';
3
3
  import { EncryptJWT, jwtDecrypt, jwtVerify, SignJWT } from 'jose';
4
4
  const encryptionProtectedHeader = { alg: 'dir', enc: 'A256GCM' };
5
5
  const signingProtectedHeader = { alg: 'HS512' };
6
+ /**
7
+ * JWT uses seconds since the epoch per RFC 7519, whereas `toTimestamp` uses milliseconds.
8
+ *
9
+ * @category Internal
10
+ */
11
+ export function toJwtTimestamp(date) {
12
+ return Math.floor(toTimestamp(date) / 1000);
13
+ }
14
+ /**
15
+ * Converts a JWT timestamp (in seconds) into a FullDate instance.
16
+ *
17
+ * @category Internal
18
+ */
19
+ export function parseJwtTimestamp(seconds) {
20
+ return createUtcFullDate(seconds * 1000);
21
+ }
6
22
  /**
7
23
  * Creates a signed and encrypted JWT that contains the given data.
8
24
  *
@@ -14,13 +30,13 @@ data, params) {
14
30
  const rawJwt = new SignJWT({ data })
15
31
  .setProtectedHeader(signingProtectedHeader)
16
32
  .setIssuedAt(params.issuedAt
17
- ? toTimestamp(createFullDateInUserTimezone(params.issuedAt))
33
+ ? toJwtTimestamp(createFullDateInUserTimezone(params.issuedAt))
18
34
  : undefined)
19
35
  .setIssuer(params.issuer)
20
36
  .setAudience(params.audience)
21
- .setExpirationTime(toTimestamp(calculateRelativeDate(getNowInUtcTimezone(), params.jwtDuration)));
37
+ .setExpirationTime(toJwtTimestamp(calculateRelativeDate(getNowInUtcTimezone(), params.jwtDuration)));
22
38
  if (params.notValidUntil) {
23
- rawJwt.setNotBefore(toTimestamp(createFullDateInUserTimezone(params.notValidUntil)));
39
+ rawJwt.setNotBefore(toJwtTimestamp(createFullDateInUserTimezone(params.notValidUntil)));
24
40
  }
25
41
  const signedJwt = await rawJwt.sign(params.jwtKeys.signingKey);
26
42
  return await new EncryptJWT({ jwt: signedJwt })
@@ -57,14 +73,8 @@ export async function parseJwt(encryptedJwt, params) {
57
73
  if (!check.deepEquals(verifiedJwt.protectedHeader, signingProtectedHeader)) {
58
74
  throw new Error('Invalid signing protected header.');
59
75
  }
60
- const expirationMs = assertWrap.isDefined(verifiedJwt.payload.exp, 'JWT has no expiration.');
61
- const jwtExpiration = createUtcFullDate(expirationMs);
62
- if (isDateAfter({
63
- fullDate: getNowInUtcTimezone(),
64
- relativeTo: jwtExpiration,
65
- })) {
66
- throw new Error('JWT expired.');
67
- }
76
+ const expirationSeconds = assertWrap.isDefined(verifiedJwt.payload.exp, 'JWT has no expiration.');
77
+ const jwtExpiration = parseJwtTimestamp(expirationSeconds);
68
78
  return {
69
79
  data: data,
70
80
  jwtExpiration,
@@ -13,6 +13,12 @@ export declare const userJwtDataShape: import("object-shape-tester").Shape<{
13
13
  * Consider using {@link generateCsrfToken} to generate this.
14
14
  */
15
15
  csrfToken: string;
16
+ /**
17
+ * Unix timestamp (in milliseconds) when the session was originally started. This is used to
18
+ * enforce the max session duration. If not present, the session is considered to have started
19
+ * when the JWT was issued.
20
+ */
21
+ sessionStartedAt: import("object-shape-tester").Shape<import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TUndefined, import("@sinclair/typebox").TNumber]>>>;
16
22
  }>;
17
23
  /**
18
24
  * Data required for user JWTs.
@@ -1,4 +1,4 @@
1
- import { checkValidShape, defineShape, unionShape } from 'object-shape-tester';
1
+ import { checkValidShape, defineShape, optionalShape, unionShape } from 'object-shape-tester';
2
2
  import { createJwt, parseJwt, } from './jwt.js';
3
3
  /**
4
4
  * Shape definition and source of truth for {@link JwtUserData}.
@@ -14,6 +14,12 @@ export const userJwtDataShape = defineShape({
14
14
  * Consider using {@link generateCsrfToken} to generate this.
15
15
  */
16
16
  csrfToken: '',
17
+ /**
18
+ * Unix timestamp (in milliseconds) when the session was originally started. This is used to
19
+ * enforce the max session duration. If not present, the session is considered to have started
20
+ * when the JWT was issued.
21
+ */
22
+ sessionStartedAt: optionalShape(0, { alsoUndefined: true }),
17
23
  });
18
24
  /**
19
25
  * Creates a new signed and encrypted {@link JwtUserData} when a client (frontend) successfully
package/dist/log.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Send logs to the console for debugging.
3
+ *
4
+ * @category Internal
5
+ */
6
+ export declare function authLog(...params: any[]): void;
7
+ /**
8
+ * Set to `false` to disable logging.
9
+ *
10
+ * @category Internal
11
+ */
12
+ export declare let shouldLogAuth: boolean;