@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 +15 -0
- package/dist/interface/clientInterface.js +4 -4
- package/dist/schema-fields.js +1 -1
- package/dist/sessions.d.ts +15 -5
- package/dist/sessions.js +46 -9
- package/package.json +2 -2
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.
|
|
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/schema-fields.js
CHANGED
|
@@ -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 =
|
|
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 {
|
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,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.
|
|
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>;
|
|
@@ -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 `
|
|
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
|
|
100
|
+
private _getOrFetchPotentiallyInvalidAccessToken;
|
|
91
101
|
/**
|
|
92
|
-
* You should prefer `
|
|
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.
|
|
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
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 `
|
|
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
|
|
144
|
+
async _getOrFetchPotentiallyInvalidAccessToken() {
|
|
108
145
|
if (!this._refreshToken)
|
|
109
146
|
return null;
|
|
110
|
-
if (this.
|
|
147
|
+
if (this.isKnownToBeInvalid())
|
|
111
148
|
return null;
|
|
112
|
-
const oldAccessToken = this.
|
|
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 `
|
|
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.
|
|
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.
|
|
54
|
+
"@stackframe/stack-sc": "2.7.7"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@sentry/nextjs": "^8.40.0",
|