@stackframe/stack-shared 2.7.5 → 2.7.7

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.7
4
+
5
+ ### Patch Changes
6
+
7
+ - Various changes
8
+ - @stackframe/stack-sc@2.7.7
9
+
10
+ ## 2.7.6
11
+
12
+ ### Patch Changes
13
+
14
+ - Fixed bugs, updated Neon requirements
15
+ - Updated dependencies
16
+ - @stackframe/stack-sc@2.7.6
17
+
3
18
  ## 2.7.5
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",
@@ -243,7 +243,7 @@ export const emailUsernameSchema = yupString().meta({ openapiField: { descriptio
243
243
  export const emailSenderEmailSchema = emailSchema.meta({ openapiField: { description: 'Email sender email. Needs to be specified when using type="standard"', exampleValue: 'example@your-domain.com' } });
244
244
  export const emailPasswordSchema = passwordSchema.meta({ openapiField: { description: 'Email password. Needs to be specified when using type="standard"', exampleValue: 'your-email-password' } });
245
245
  // Project domain config
246
- export const projectTrustedDomainSchema = yupString().test('is-https', 'Trusted domain must start with https://', (value) => value?.startsWith('https://')).meta({ openapiField: { description: 'Your domain URL. Make sure you own and trust this domain. Needs to start with https://', exampleValue: 'https://example.com' } });
246
+ export const projectTrustedDomainSchema = urlSchema.test('is-https', 'Trusted domain must start with https://', (value) => value?.startsWith('https://')).meta({ openapiField: { description: 'Your domain URL. Make sure you own and trust this domain. Needs to start with https://', exampleValue: 'https://example.com' } });
247
247
  export const handlerPathSchema = yupString().test('is-handler-path', 'Handler path must start with /', (value) => value?.startsWith('/')).meta({ openapiField: { description: 'Handler path. If you did not setup a custom handler path, it should be "/handler" by default. It needs to start with /', exampleValue: '/handler' } });
248
248
  // Users
249
249
  export class ReplaceFieldWithOwnUserId extends Error {
@@ -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,11 +62,11 @@ 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>;
@@ -69,7 +75,7 @@ export declare class InternalSession {
69
75
  *
70
76
  * 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.
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` with a fallback to `markAccessTokenExpired` and a retry mechanism if the endpoint rejects the token.
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
  */
@@ -84,12 +90,16 @@ export declare class InternalSession {
84
90
  onAccessTokenChange(callback: (newAccessToken: AccessToken | null) => void): {
85
91
  unsubscribe: () => void;
86
92
  };
93
+ /**
94
+ * @returns An access token, which may be expired or expire soon, or null if it is known to be invalid.
95
+ */
96
+ private _getPotentiallyInvalidAccessTokenIfAvailable;
87
97
  /**
88
98
  * @returns An access token (cached if possible), or null if the session either does not represent a user or the session is invalid.
89
99
  */
90
- private _getPotentiallyExpiredAccessToken;
100
+ private _getOrFetchPotentiallyInvalidAccessToken;
91
101
  /**
92
- * You should prefer `getPotentiallyExpiredAccessToken` in almost all cases.
102
+ * You should prefer `_getOrFetchAccessToken` in almost all cases.
93
103
  *
94
104
  * @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
105
  */
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)
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
110
  * 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.
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` with a fallback to `markAccessTokenExpired` and a retry mechanism if the endpoint rejects the token.
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
  }
@@ -101,15 +129,24 @@ export class InternalSession {
101
129
  onAccessTokenChange(callback) {
102
130
  return this._accessToken.onChange(callback);
103
131
  }
132
+ /**
133
+ * @returns An access token, which may be expired or expire soon, or null if it is known to be invalid.
134
+ */
135
+ _getPotentiallyInvalidAccessTokenIfAvailable() {
136
+ const accessToken = this._accessToken.get();
137
+ if (accessToken && !accessToken.isExpired())
138
+ return accessToken;
139
+ return null;
140
+ }
104
141
  /**
105
142
  * @returns An access token (cached if possible), or null if the session either does not represent a user or the session is invalid.
106
143
  */
107
- async _getPotentiallyExpiredAccessToken() {
144
+ async _getOrFetchPotentiallyInvalidAccessToken() {
108
145
  if (!this._refreshToken)
109
146
  return null;
110
- if (this._knownToBeInvalid.get())
147
+ if (this.isKnownToBeInvalid())
111
148
  return null;
112
- const oldAccessToken = this._accessToken.get();
149
+ const oldAccessToken = this._getPotentiallyInvalidAccessTokenIfAvailable();
113
150
  if (oldAccessToken)
114
151
  return oldAccessToken;
115
152
  // refresh access token
@@ -119,7 +156,7 @@ export class InternalSession {
119
156
  return await this._refreshPromise;
120
157
  }
121
158
  /**
122
- * You should prefer `getPotentiallyExpiredAccessToken` in almost all cases.
159
+ * You should prefer `_getOrFetchAccessToken` in almost all cases.
123
160
  *
124
161
  * @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
162
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackframe/stack-shared",
3
- "version": "2.7.5",
3
+ "version": "2.7.7",
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.5"
54
+ "@stackframe/stack-sc": "2.7.7"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@sentry/nextjs": "^8.40.0",