@stackframe/stack-shared 2.7.6 → 2.7.8

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @stackframe/stack-shared
2
2
 
3
+ ## 2.7.8
4
+
5
+ ### Patch Changes
6
+
7
+ - Various changes
8
+ - Updated dependencies
9
+ - @stackframe/stack-sc@2.7.8
10
+
11
+ ## 2.7.7
12
+
13
+ ### Patch Changes
14
+
15
+ - Various changes
16
+ - @stackframe/stack-sc@2.7.7
17
+
3
18
  ## 2.7.6
4
19
 
5
20
  ### Patch Changes
@@ -163,9 +163,9 @@ export class StackClientInterface {
163
163
  /**
164
164
  * `tokenObj === null` means the session is invalid/not logged in
165
165
  */
166
- let tokenObj = await session.getPotentiallyExpiredTokens();
166
+ let tokenObj = await session.getOrFetchLikelyValidTokens(20000);
167
167
  let adminSession = "projectOwnerSession" in this.options ? this.options.projectOwnerSession : null;
168
- let adminTokenObj = adminSession ? await adminSession.getPotentiallyExpiredTokens() : null;
168
+ let adminTokenObj = adminSession ? await adminSession.getOrFetchLikelyValidTokens(20000) : null;
169
169
  // all requests should be dynamic to prevent Next.js caching
170
170
  await cookies?.();
171
171
  const url = this.getApiUrl() + path;
@@ -638,7 +638,7 @@ export class StackClientInterface {
638
638
  url.searchParams.set("after_callback_redirect_url", options.afterCallbackRedirectUrl);
639
639
  }
640
640
  if (options.type === "link") {
641
- const tokens = await options.session.getPotentiallyExpiredTokens();
641
+ const tokens = await options.session.getOrFetchLikelyValidTokens(20000);
642
642
  url.searchParams.set("token", tokens?.accessToken.token || "");
643
643
  if (options.providerScope) {
644
644
  url.searchParams.set("provider_scope", options.providerScope);
@@ -682,7 +682,7 @@ export class StackClientInterface {
682
682
  };
683
683
  }
684
684
  async signOut(session) {
685
- const tokenObj = await session.getPotentiallyExpiredTokens();
685
+ const tokenObj = await session.getOrFetchLikelyValidTokens(20000);
686
686
  if (tokenObj) {
687
687
  const resOrError = await this.sendClientRequestAndCatchKnownError("/auth/sessions/current", {
688
688
  method: "DELETE",
@@ -1,6 +1,12 @@
1
1
  export declare class AccessToken {
2
2
  readonly token: string;
3
3
  constructor(token: string);
4
+ get expiresAt(): Date;
5
+ /**
6
+ * @returns The number of milliseconds until the access token expires, or 0 if it has already expired.
7
+ */
8
+ get expiresInMillis(): number;
9
+ isExpired(): boolean;
4
10
  }
5
11
  export declare class RefreshToken {
6
12
  readonly token: string;
@@ -56,20 +62,20 @@ export declare class InternalSession {
56
62
  /**
57
63
  * Returns the access token if it is found in the cache, fetching it otherwise.
58
64
  *
59
- * This is usually the function you want to call to get an access token. When using the access token, you should catch errors that occur if it expires, and call `markAccessTokenExpired` to mark the token as expired if so (after which a call to this function will always refetch the token).
65
+ * This is usually the function you want to call to get an access token. Either set `minMillisUntilExpiration` to a reasonable value, or catch errors that occur if it expires, and call `markAccessTokenExpired` to mark the token as expired if so (after which a call to this function will always refetch the token).
60
66
  *
61
67
  * @returns null if the session is known to be invalid, cached tokens if they exist in the cache (which may or may not be valid still), or new tokens otherwise.
62
68
  */
63
- getPotentiallyExpiredTokens(): Promise<{
69
+ getOrFetchLikelyValidTokens(minMillisUntilExpiration: number): Promise<{
64
70
  accessToken: AccessToken;
65
71
  refreshToken: RefreshToken | null;
66
72
  } | null>;
67
73
  /**
68
74
  * Fetches new tokens that are, at the time of fetching, guaranteed to be valid.
69
75
  *
70
- * The newly generated tokens are shortlived, so it's good practice not to rely on their validity (if possible). However, this function is useful in some cases where you only want to pass access tokens to a service, and you want to make sure said access token has the longest possible lifetime.
76
+ * The newly generated tokens are short-lived, so it's good practice not to rely on their validity (if possible). However, this function is useful in some cases where you only want to pass access tokens to a service, and you want to make sure said access token has the longest possible lifetime.
71
77
  *
72
- * In most cases, you should prefer `getPotentiallyExpiredTokens` with a fallback to `markAccessTokenExpired` and a retry mechanism if the endpoint rejects the token.
78
+ * In most cases, you should prefer `getOrFetchLikelyValidTokens`.
73
79
  *
74
80
  * @returns null if the session is known to be invalid, or new tokens otherwise (which, at the time of fetching, are guaranteed to be valid).
75
81
  */
@@ -85,11 +91,11 @@ export declare class InternalSession {
85
91
  unsubscribe: () => void;
86
92
  };
87
93
  /**
88
- * @returns An access token (cached if possible), or null if the session either does not represent a user or the session is invalid.
94
+ * @returns An access token, which may be expired or expire soon, or null if it is known to be invalid.
89
95
  */
90
- private _getPotentiallyExpiredAccessToken;
96
+ private _getPotentiallyInvalidAccessTokenIfAvailable;
91
97
  /**
92
- * You should prefer `getPotentiallyExpiredAccessToken` in almost all cases.
98
+ * You should prefer `_getOrFetchPotentiallyInvalidAccessToken` in almost all cases.
93
99
  *
94
100
  * @returns A newly fetched access token (never read from cache), or null if the session either does not represent a user or the session is invalid.
95
101
  */
package/dist/sessions.js CHANGED
@@ -1,3 +1,4 @@
1
+ import * as jose from 'jose';
1
2
  import { StackAssertionError } from "./utils/errors";
2
3
  import { Store } from "./utils/stores";
3
4
  export class AccessToken {
@@ -7,6 +8,21 @@ export class AccessToken {
7
8
  throw new StackAssertionError("Access token is the string 'undefined'; it's unlikely this is the correct value. They're supposed to be unguessable!");
8
9
  }
9
10
  }
11
+ get expiresAt() {
12
+ const { exp } = jose.decodeJwt(this.token);
13
+ if (exp === undefined)
14
+ return new Date(8640000000000000); // max date value
15
+ return new Date(exp * 1000);
16
+ }
17
+ /**
18
+ * @returns The number of milliseconds until the access token expires, or 0 if it has already expired.
19
+ */
20
+ get expiresInMillis() {
21
+ return Math.max(0, this.expiresAt.getTime() - Date.now());
22
+ }
23
+ isExpired() {
24
+ return this.expiresInMillis <= 0;
25
+ }
10
26
  }
11
27
  export class RefreshToken {
12
28
  constructor(token) {
@@ -69,20 +85,31 @@ export class InternalSession {
69
85
  /**
70
86
  * Returns the access token if it is found in the cache, fetching it otherwise.
71
87
  *
72
- * This is usually the function you want to call to get an access token. When using the access token, you should catch errors that occur if it expires, and call `markAccessTokenExpired` to mark the token as expired if so (after which a call to this function will always refetch the token).
88
+ * This is usually the function you want to call to get an access token. Either set `minMillisUntilExpiration` to a reasonable value, or catch errors that occur if it expires, and call `markAccessTokenExpired` to mark the token as expired if so (after which a call to this function will always refetch the token).
73
89
  *
74
90
  * @returns null if the session is known to be invalid, cached tokens if they exist in the cache (which may or may not be valid still), or new tokens otherwise.
75
91
  */
76
- async getPotentiallyExpiredTokens() {
77
- const accessToken = await this._getPotentiallyExpiredAccessToken();
78
- return accessToken ? { accessToken, refreshToken: this._refreshToken } : null;
92
+ async getOrFetchLikelyValidTokens(minMillisUntilExpiration) {
93
+ if (minMillisUntilExpiration >= 60000) {
94
+ throw new Error(`Required access token expiry ${minMillisUntilExpiration}ms is too long; access tokens are too short to be used for more than 60s`);
95
+ }
96
+ const accessToken = this._getPotentiallyInvalidAccessTokenIfAvailable();
97
+ if (!accessToken || accessToken.expiresInMillis < minMillisUntilExpiration) {
98
+ const newTokens = await this.fetchNewTokens();
99
+ const expiresInMillis = newTokens?.accessToken.expiresInMillis;
100
+ if (expiresInMillis && expiresInMillis < minMillisUntilExpiration) {
101
+ throw new StackAssertionError(`Required access token expiry ${minMillisUntilExpiration}ms is too long; access tokens are too short when they're generated (${expiresInMillis}ms)`);
102
+ }
103
+ return newTokens;
104
+ }
105
+ return { accessToken, refreshToken: this._refreshToken };
79
106
  }
80
107
  /**
81
108
  * Fetches new tokens that are, at the time of fetching, guaranteed to be valid.
82
109
  *
83
- * The newly generated tokens are shortlived, so it's good practice not to rely on their validity (if possible). However, this function is useful in some cases where you only want to pass access tokens to a service, and you want to make sure said access token has the longest possible lifetime.
110
+ * The newly generated tokens are short-lived, so it's good practice not to rely on their validity (if possible). However, this function is useful in some cases where you only want to pass access tokens to a service, and you want to make sure said access token has the longest possible lifetime.
84
111
  *
85
- * In most cases, you should prefer `getPotentiallyExpiredTokens` with a fallback to `markAccessTokenExpired` and a retry mechanism if the endpoint rejects the token.
112
+ * In most cases, you should prefer `getOrFetchLikelyValidTokens`.
86
113
  *
87
114
  * @returns null if the session is known to be invalid, or new tokens otherwise (which, at the time of fetching, are guaranteed to be valid).
88
115
  */
@@ -91,6 +118,7 @@ export class InternalSession {
91
118
  return accessToken ? { accessToken, refreshToken: this._refreshToken } : null;
92
119
  }
93
120
  markAccessTokenExpired(accessToken) {
121
+ // TODO we don't need this anymore, since we now check the expiry by ourselves
94
122
  if (this._accessToken.get() === accessToken) {
95
123
  this._accessToken.set(null);
96
124
  }
@@ -102,24 +130,20 @@ export class InternalSession {
102
130
  return this._accessToken.onChange(callback);
103
131
  }
104
132
  /**
105
- * @returns An access token (cached if possible), or null if the session either does not represent a user or the session is invalid.
133
+ * @returns An access token, which may be expired or expire soon, or null if it is known to be invalid.
106
134
  */
107
- async _getPotentiallyExpiredAccessToken() {
135
+ _getPotentiallyInvalidAccessTokenIfAvailable() {
108
136
  if (!this._refreshToken)
109
137
  return null;
110
- if (this._knownToBeInvalid.get())
138
+ if (this.isKnownToBeInvalid())
111
139
  return null;
112
- const oldAccessToken = this._accessToken.get();
113
- if (oldAccessToken)
114
- return oldAccessToken;
115
- // refresh access token
116
- if (!this._refreshPromise) {
117
- this._refreshAndSetRefreshPromise(this._refreshToken);
118
- }
119
- return await this._refreshPromise;
140
+ const accessToken = this._accessToken.get();
141
+ if (accessToken && !accessToken.isExpired())
142
+ return accessToken;
143
+ return null;
120
144
  }
121
145
  /**
122
- * You should prefer `getPotentiallyExpiredAccessToken` in almost all cases.
146
+ * You should prefer `_getOrFetchPotentiallyInvalidAccessToken` in almost all cases.
123
147
  *
124
148
  * @returns A newly fetched access token (never read from cache), or null if the session either does not represent a user or the session is invalid.
125
149
  */
@@ -128,7 +152,9 @@ export class InternalSession {
128
152
  return null;
129
153
  if (this._knownToBeInvalid.get())
130
154
  return null;
131
- this._refreshAndSetRefreshPromise(this._refreshToken);
155
+ if (!this._refreshPromise) {
156
+ this._refreshAndSetRefreshPromise(this._refreshToken);
157
+ }
132
158
  return await this._refreshPromise;
133
159
  }
134
160
  _refreshAndSetRefreshPromise(refreshToken) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackframe/stack-shared",
3
- "version": "2.7.6",
3
+ "version": "2.7.8",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "files": [
@@ -51,7 +51,7 @@
51
51
  "oauth4webapi": "^2.10.3",
52
52
  "semver": "^7.6.3",
53
53
  "uuid": "^9.0.1",
54
- "@stackframe/stack-sc": "2.7.6"
54
+ "@stackframe/stack-sc": "2.7.8"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@sentry/nextjs": "^8.40.0",