@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 +15 -0
- package/dist/interface/clientInterface.js +4 -4
- package/dist/sessions.d.ts +13 -7
- package/dist/sessions.js +45 -19
- package/package.json +2 -2
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|
package/dist/sessions.d.ts
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
96
|
+
private _getPotentiallyInvalidAccessTokenIfAvailable;
|
|
91
97
|
/**
|
|
92
|
-
* You should prefer `
|
|
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.
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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 `
|
|
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
|
|
133
|
+
* @returns An access token, which may be expired or expire soon, or null if it is known to be invalid.
|
|
106
134
|
*/
|
|
107
|
-
|
|
135
|
+
_getPotentiallyInvalidAccessTokenIfAvailable() {
|
|
108
136
|
if (!this._refreshToken)
|
|
109
137
|
return null;
|
|
110
|
-
if (this.
|
|
138
|
+
if (this.isKnownToBeInvalid())
|
|
111
139
|
return null;
|
|
112
|
-
const
|
|
113
|
-
if (
|
|
114
|
-
return
|
|
115
|
-
|
|
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 `
|
|
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
|
-
|
|
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.
|
|
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.
|
|
54
|
+
"@stackframe/stack-sc": "2.7.8"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@sentry/nextjs": "^8.40.0",
|