@stackframe/stack-shared 2.4.13 → 2.4.14
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 +6 -0
- package/dist/hooks/use-trigger.d.ts +1 -0
- package/dist/hooks/use-trigger.js +10 -0
- package/dist/interface/adminInterface.d.ts +9 -8
- package/dist/interface/adminInterface.js +4 -4
- package/dist/interface/clientInterface.d.ts +39 -31
- package/dist/interface/clientInterface.js +108 -120
- package/dist/interface/serverInterface.d.ts +11 -10
- package/dist/interface/serverInterface.js +8 -8
- package/dist/sessions.d.ts +75 -0
- package/dist/sessions.js +127 -0
- package/dist/utils/crypto.js +2 -1
- package/dist/utils/dom.js +1 -1
- package/dist/utils/env.d.ts +1 -0
- package/dist/utils/env.js +4 -1
- package/dist/utils/errors.js +5 -0
- package/dist/utils/globals.js +2 -2
- package/dist/utils/promises.d.ts +3 -1
- package/dist/utils/promises.js +11 -1
- package/dist/utils/proxies.d.ts +1 -0
- package/dist/utils/proxies.js +59 -0
- package/dist/utils/react.js +2 -1
- package/dist/utils/results.d.ts +3 -3
- package/dist/utils/results.js +10 -2
- package/dist/utils/stores.d.ts +23 -0
- package/dist/utils/stores.js +36 -0
- package/dist/utils/strings.d.ts +3 -0
- package/dist/utils/strings.js +46 -0
- package/dist/utils/uuids.d.ts +1 -1
- package/dist/utils/uuids.js +2 -1
- package/package.json +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import * as oauth from 'oauth4webapi';
|
|
2
2
|
import { Result } from "../utils/results";
|
|
3
|
-
import { AsyncStore } from '../utils/stores';
|
|
4
3
|
import { KnownError, KnownErrors } from '../known-errors';
|
|
5
|
-
import { StackAssertionError } from '../utils/errors';
|
|
4
|
+
import { StackAssertionError, captureError, throwErr } from '../utils/errors';
|
|
6
5
|
import { cookies } from '@stackframe/stack-sc';
|
|
7
6
|
import { generateSecureRandomString } from '../utils/crypto';
|
|
7
|
+
import { AccessToken, Session } from '../sessions';
|
|
8
|
+
import { globalVar } from '../utils/globals';
|
|
8
9
|
export const sharedProviders = [
|
|
9
10
|
"shared-github",
|
|
10
11
|
"shared-google",
|
|
@@ -37,18 +38,10 @@ export class StackClientInterface {
|
|
|
37
38
|
getApiUrl() {
|
|
38
39
|
return this.options.baseUrl + "/api/v1";
|
|
39
40
|
}
|
|
40
|
-
async
|
|
41
|
+
async fetchNewAccessToken(refreshToken) {
|
|
41
42
|
if (!('publishableClientKey' in this.options)) {
|
|
42
|
-
// TODO
|
|
43
|
-
throw new Error("Admin session token is currently not supported for fetching new access token");
|
|
44
|
-
}
|
|
45
|
-
const refreshToken = (await tokenStore.getOrWait()).refreshToken;
|
|
46
|
-
if (!refreshToken) {
|
|
47
|
-
tokenStore.set({
|
|
48
|
-
accessToken: null,
|
|
49
|
-
refreshToken: null,
|
|
50
|
-
});
|
|
51
|
-
return;
|
|
43
|
+
// TODO support it
|
|
44
|
+
throw new Error("Admin session token is currently not supported for fetching new access token. Did you try to log in on a StackApp initiated with the admin session?");
|
|
52
45
|
}
|
|
53
46
|
const as = {
|
|
54
47
|
issuer: this.options.baseUrl,
|
|
@@ -60,15 +53,12 @@ export class StackClientInterface {
|
|
|
60
53
|
client_secret: this.options.publishableClientKey,
|
|
61
54
|
token_endpoint_auth_method: 'client_secret_basic',
|
|
62
55
|
};
|
|
63
|
-
const rawResponse = await oauth.refreshTokenGrantRequest(as, client, refreshToken);
|
|
56
|
+
const rawResponse = await oauth.refreshTokenGrantRequest(as, client, refreshToken.token);
|
|
64
57
|
const response = await this._processResponse(rawResponse);
|
|
65
58
|
if (response.status === "error") {
|
|
66
59
|
const error = response.error;
|
|
67
60
|
if (error instanceof KnownErrors.RefreshTokenError) {
|
|
68
|
-
return
|
|
69
|
-
accessToken: null,
|
|
70
|
-
refreshToken: null,
|
|
71
|
-
});
|
|
61
|
+
return null;
|
|
72
62
|
}
|
|
73
63
|
throw error;
|
|
74
64
|
}
|
|
@@ -86,17 +76,23 @@ export class StackClientInterface {
|
|
|
86
76
|
// TODO Handle OAuth 2.0 response body error
|
|
87
77
|
throw new StackAssertionError("OAuth error", { result });
|
|
88
78
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
if (!result.access_token) {
|
|
80
|
+
throw new StackAssertionError("Access token not found in token endpoint response, this is weird!");
|
|
81
|
+
}
|
|
82
|
+
return new AccessToken(result.access_token);
|
|
93
83
|
}
|
|
94
|
-
async sendClientRequest(path, requestOptions,
|
|
95
|
-
|
|
96
|
-
accessToken: null,
|
|
84
|
+
async sendClientRequest(path, requestOptions, session, requestType = "client") {
|
|
85
|
+
session ??= this.createSession({
|
|
97
86
|
refreshToken: null,
|
|
98
87
|
});
|
|
99
|
-
return await Result.orThrowAsync(Result.retry(() => this.sendClientRequestInner(path, requestOptions,
|
|
88
|
+
return await Result.orThrowAsync(Result.retry(() => this.sendClientRequestInner(path, requestOptions, session, requestType), 5, { exponentialDelayBase: 1000 }));
|
|
89
|
+
}
|
|
90
|
+
createSession(options) {
|
|
91
|
+
const session = new Session({
|
|
92
|
+
refreshAccessTokenCallback: async (refreshToken) => await this.fetchNewAccessToken(refreshToken),
|
|
93
|
+
...options,
|
|
94
|
+
});
|
|
95
|
+
return session;
|
|
100
96
|
}
|
|
101
97
|
async sendClientRequestAndCatchKnownError(path, requestOptions, tokenStoreOrNull, errorsToCatch) {
|
|
102
98
|
try {
|
|
@@ -111,26 +107,13 @@ export class StackClientInterface {
|
|
|
111
107
|
throw e;
|
|
112
108
|
}
|
|
113
109
|
}
|
|
114
|
-
async sendClientRequestInner(path, options,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
let
|
|
120
|
-
|
|
121
|
-
await this.refreshAccessToken(tokenStore);
|
|
122
|
-
tokenObj = await tokenStore.getOrWait();
|
|
123
|
-
}
|
|
124
|
-
let adminTokenStore = null;
|
|
125
|
-
let adminTokenObj = null;
|
|
126
|
-
if ("projectOwnerTokens" in this.options) {
|
|
127
|
-
adminTokenStore = this.options.projectOwnerTokens;
|
|
128
|
-
adminTokenObj = await adminTokenStore.getOrWait();
|
|
129
|
-
if (!adminTokenObj.accessToken) {
|
|
130
|
-
await this.options.refreshProjectOwnerTokens();
|
|
131
|
-
adminTokenObj = await adminTokenStore.getOrWait();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
110
|
+
async sendClientRequestInner(path, options, session, requestType) {
|
|
111
|
+
/**
|
|
112
|
+
* `tokenObj === null` means the session is invalid/not logged in
|
|
113
|
+
*/
|
|
114
|
+
let tokenObj = await session.getPotentiallyExpiredTokens();
|
|
115
|
+
let adminSession = "projectOwnerSession" in this.options ? this.options.projectOwnerSession : null;
|
|
116
|
+
let adminTokenObj = adminSession ? await adminSession.getPotentiallyExpiredTokens() : null;
|
|
134
117
|
// all requests should be dynamic to prevent Next.js caching
|
|
135
118
|
cookies?.();
|
|
136
119
|
const url = this.getApiUrl() + path;
|
|
@@ -145,7 +128,7 @@ export class StackClientInterface {
|
|
|
145
128
|
* However, Cloudflare Workers don't actually support `credentials`, so we only set it
|
|
146
129
|
* if Cloudflare-exclusive globals are not detected. https://github.com/cloudflare/workers-sdk/issues/2514
|
|
147
130
|
*/
|
|
148
|
-
..."WebSocketPair" in
|
|
131
|
+
..."WebSocketPair" in globalVar ? {} : {
|
|
149
132
|
credentials: "omit",
|
|
150
133
|
},
|
|
151
134
|
...options,
|
|
@@ -154,18 +137,18 @@ export class StackClientInterface {
|
|
|
154
137
|
"X-Stack-Project-Id": this.projectId,
|
|
155
138
|
"X-Stack-Request-Type": requestType,
|
|
156
139
|
"X-Stack-Client-Version": this.options.clientVersion,
|
|
157
|
-
...tokenObj
|
|
158
|
-
"Authorization": "StackSession " + tokenObj.accessToken,
|
|
159
|
-
"X-Stack-Access-Token": tokenObj.accessToken,
|
|
140
|
+
...tokenObj ? {
|
|
141
|
+
"Authorization": "StackSession " + tokenObj.accessToken.token,
|
|
142
|
+
"X-Stack-Access-Token": tokenObj.accessToken.token,
|
|
160
143
|
} : {},
|
|
161
|
-
...tokenObj
|
|
162
|
-
"X-Stack-Refresh-Token": tokenObj.refreshToken,
|
|
144
|
+
...tokenObj?.refreshToken ? {
|
|
145
|
+
"X-Stack-Refresh-Token": tokenObj.refreshToken.token,
|
|
163
146
|
} : {},
|
|
164
147
|
...'publishableClientKey' in this.options ? {
|
|
165
148
|
"X-Stack-Publishable-Client-Key": this.options.publishableClientKey,
|
|
166
149
|
} : {},
|
|
167
150
|
...adminTokenObj ? {
|
|
168
|
-
"X-Stack-Admin-Access-Token": adminTokenObj.accessToken
|
|
151
|
+
"X-Stack-Admin-Access-Token": adminTokenObj.accessToken.token,
|
|
169
152
|
} : {},
|
|
170
153
|
/**
|
|
171
154
|
* Next.js until v15 would cache fetch requests by default, and forcefully disabling it was nearly impossible.
|
|
@@ -181,7 +164,7 @@ export class StackClientInterface {
|
|
|
181
164
|
/**
|
|
182
165
|
* Cloudflare Workers does not support cache, so don't pass it there
|
|
183
166
|
*/
|
|
184
|
-
..."WebSocketPair" in
|
|
167
|
+
..."WebSocketPair" in globalVar ? {} : {
|
|
185
168
|
cache: "no-store",
|
|
186
169
|
},
|
|
187
170
|
};
|
|
@@ -199,24 +182,24 @@ export class StackClientInterface {
|
|
|
199
182
|
}
|
|
200
183
|
const processedRes = await this._processResponse(rawRes);
|
|
201
184
|
if (processedRes.status === "error") {
|
|
202
|
-
// If the access token is
|
|
185
|
+
// If the access token is invalid, reset it and retry
|
|
203
186
|
if (processedRes.error instanceof KnownErrors.InvalidAccessToken) {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return Result.error(
|
|
187
|
+
if (!tokenObj) {
|
|
188
|
+
throw new StackAssertionError("Received invalid access token, but session is not logged in", { tokenObj, processedRes });
|
|
189
|
+
}
|
|
190
|
+
session.markAccessTokenExpired(tokenObj.accessToken);
|
|
191
|
+
return Result.error(processedRes.error);
|
|
209
192
|
}
|
|
210
193
|
// Same for the admin access token
|
|
211
|
-
// TODO HACK: Some of the backend hasn't been ported to use the new error codes, so if we have project owner tokens we need to check for ApiKeyNotFound too. Once the migration to smartRouteHandlers is complete, we can check for
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
return Result.error(
|
|
194
|
+
// TODO HACK: Some of the backend hasn't been ported to use the new error codes, so if we have project owner tokens we need to check for ApiKeyNotFound too. Once the migration to smartRouteHandlers is complete, we can check for InvalidAdminAccessToken only.
|
|
195
|
+
if (adminSession && (processedRes.error instanceof KnownErrors.InvalidAdminAccessToken || processedRes.error instanceof KnownErrors.ApiKeyNotFound)) {
|
|
196
|
+
if (!adminTokenObj) {
|
|
197
|
+
throw new StackAssertionError("Received invalid admin access token, but admin session is not logged in", { adminTokenObj, processedRes });
|
|
198
|
+
}
|
|
199
|
+
adminSession.markAccessTokenExpired(adminTokenObj.accessToken);
|
|
200
|
+
return Result.error(processedRes.error);
|
|
218
201
|
}
|
|
219
|
-
// Known errors are client side errors,
|
|
202
|
+
// Known errors are client side errors, so except for the ones above they should not be retried
|
|
220
203
|
// Hence, throw instead of returning an error
|
|
221
204
|
throw processedRes.error;
|
|
222
205
|
}
|
|
@@ -246,7 +229,7 @@ export class StackClientInterface {
|
|
|
246
229
|
if (res.headers.has("x-stack-known-error")) {
|
|
247
230
|
const errorJson = await res.json();
|
|
248
231
|
if (res.headers.get("x-stack-known-error") !== errorJson.code) {
|
|
249
|
-
throw new
|
|
232
|
+
throw new StackAssertionError("Mismatch between x-stack-known-error header and error code in body; the server's response is invalid");
|
|
250
233
|
}
|
|
251
234
|
const error = KnownError.fromJson(errorJson);
|
|
252
235
|
return Result.error(error);
|
|
@@ -268,7 +251,7 @@ export class StackClientInterface {
|
|
|
268
251
|
return res.error;
|
|
269
252
|
}
|
|
270
253
|
}
|
|
271
|
-
async sendVerificationEmail(emailVerificationRedirectUrl,
|
|
254
|
+
async sendVerificationEmail(emailVerificationRedirectUrl, session) {
|
|
272
255
|
const res = await this.sendClientRequestAndCatchKnownError("/auth/send-verification-email", {
|
|
273
256
|
method: "POST",
|
|
274
257
|
headers: {
|
|
@@ -277,7 +260,7 @@ export class StackClientInterface {
|
|
|
277
260
|
body: JSON.stringify({
|
|
278
261
|
emailVerificationRedirectUrl,
|
|
279
262
|
}),
|
|
280
|
-
},
|
|
263
|
+
}, session, [KnownErrors.EmailAlreadyVerified]);
|
|
281
264
|
if (res.status === "error") {
|
|
282
265
|
return res.error;
|
|
283
266
|
}
|
|
@@ -309,14 +292,14 @@ export class StackClientInterface {
|
|
|
309
292
|
return res.error;
|
|
310
293
|
}
|
|
311
294
|
}
|
|
312
|
-
async updatePassword(options,
|
|
295
|
+
async updatePassword(options, session) {
|
|
313
296
|
const res = await this.sendClientRequestAndCatchKnownError("/auth/update-password", {
|
|
314
297
|
method: "POST",
|
|
315
298
|
headers: {
|
|
316
299
|
"Content-Type": "application/json"
|
|
317
300
|
},
|
|
318
301
|
body: JSON.stringify(options),
|
|
319
|
-
},
|
|
302
|
+
}, session, [KnownErrors.PasswordMismatch, KnownErrors.PasswordRequirementsNotMet]);
|
|
320
303
|
if (res.status === "error") {
|
|
321
304
|
return res.error;
|
|
322
305
|
}
|
|
@@ -342,7 +325,7 @@ export class StackClientInterface {
|
|
|
342
325
|
return res.error;
|
|
343
326
|
}
|
|
344
327
|
}
|
|
345
|
-
async signInWithCredential(email, password,
|
|
328
|
+
async signInWithCredential(email, password, session) {
|
|
346
329
|
const res = await this.sendClientRequestAndCatchKnownError("/auth/signin", {
|
|
347
330
|
method: "POST",
|
|
348
331
|
headers: {
|
|
@@ -352,17 +335,17 @@ export class StackClientInterface {
|
|
|
352
335
|
email,
|
|
353
336
|
password,
|
|
354
337
|
}),
|
|
355
|
-
},
|
|
338
|
+
}, session, [KnownErrors.EmailPasswordMismatch]);
|
|
356
339
|
if (res.status === "error") {
|
|
357
340
|
return res.error;
|
|
358
341
|
}
|
|
359
342
|
const result = await res.data.json();
|
|
360
|
-
|
|
343
|
+
return {
|
|
361
344
|
accessToken: result.accessToken,
|
|
362
345
|
refreshToken: result.refreshToken,
|
|
363
|
-
}
|
|
346
|
+
};
|
|
364
347
|
}
|
|
365
|
-
async signUpWithCredential(email, password, emailVerificationRedirectUrl,
|
|
348
|
+
async signUpWithCredential(email, password, emailVerificationRedirectUrl, session) {
|
|
366
349
|
const res = await this.sendClientRequestAndCatchKnownError("/auth/signup", {
|
|
367
350
|
headers: {
|
|
368
351
|
"Content-Type": "application/json"
|
|
@@ -373,17 +356,17 @@ export class StackClientInterface {
|
|
|
373
356
|
password,
|
|
374
357
|
emailVerificationRedirectUrl,
|
|
375
358
|
}),
|
|
376
|
-
},
|
|
359
|
+
}, session, [KnownErrors.UserEmailAlreadyExists, KnownErrors.PasswordRequirementsNotMet]);
|
|
377
360
|
if (res.status === "error") {
|
|
378
361
|
return res.error;
|
|
379
362
|
}
|
|
380
363
|
const result = await res.data.json();
|
|
381
|
-
|
|
364
|
+
return {
|
|
382
365
|
accessToken: result.accessToken,
|
|
383
366
|
refreshToken: result.refreshToken,
|
|
384
|
-
}
|
|
367
|
+
};
|
|
385
368
|
}
|
|
386
|
-
async signInWithMagicLink(code,
|
|
369
|
+
async signInWithMagicLink(code, session) {
|
|
387
370
|
const res = await this.sendClientRequestAndCatchKnownError("/auth/magic-link-verification", {
|
|
388
371
|
method: "POST",
|
|
389
372
|
headers: {
|
|
@@ -397,11 +380,11 @@ export class StackClientInterface {
|
|
|
397
380
|
return res.error;
|
|
398
381
|
}
|
|
399
382
|
const result = await res.data.json();
|
|
400
|
-
|
|
383
|
+
return {
|
|
401
384
|
accessToken: result.accessToken,
|
|
402
385
|
refreshToken: result.refreshToken,
|
|
403
|
-
|
|
404
|
-
|
|
386
|
+
newUser: result.newUser,
|
|
387
|
+
};
|
|
405
388
|
}
|
|
406
389
|
async getOAuthUrl(provider, redirectUrl, codeChallenge, state) {
|
|
407
390
|
const updatedRedirectUrl = new URL(redirectUrl);
|
|
@@ -427,7 +410,7 @@ export class StackClientInterface {
|
|
|
427
410
|
url.searchParams.set("response_type", "code");
|
|
428
411
|
return url.toString();
|
|
429
412
|
}
|
|
430
|
-
async callOAuthCallback(oauthParams, redirectUri, codeVerifier, state
|
|
413
|
+
async callOAuthCallback(oauthParams, redirectUri, codeVerifier, state) {
|
|
431
414
|
if (!('publishableClientKey' in this.options)) {
|
|
432
415
|
// TODO fix
|
|
433
416
|
throw new Error("Admin session token is currently not supported for OAuth");
|
|
@@ -444,41 +427,46 @@ export class StackClientInterface {
|
|
|
444
427
|
};
|
|
445
428
|
const params = oauth.validateAuthResponse(as, client, oauthParams, state);
|
|
446
429
|
if (oauth.isOAuth2Error(params)) {
|
|
447
|
-
throw new StackAssertionError("Error validating OAuth response", { params }); // Handle OAuth 2.0 redirect error
|
|
430
|
+
throw new StackAssertionError("Error validating outer OAuth response", { params }); // Handle OAuth 2.0 redirect error
|
|
448
431
|
}
|
|
449
432
|
const response = await oauth.authorizationCodeGrantRequest(as, client, params, redirectUri, codeVerifier);
|
|
450
433
|
let challenges;
|
|
451
434
|
if ((challenges = oauth.parseWwwAuthenticateChallenges(response))) {
|
|
452
435
|
// TODO Handle WWW-Authenticate Challenges as needed
|
|
453
|
-
throw new StackAssertionError("OAuth WWW-Authenticate challenge not implemented", { challenges });
|
|
436
|
+
throw new StackAssertionError("Outer OAuth WWW-Authenticate challenge not implemented", { challenges });
|
|
454
437
|
}
|
|
455
438
|
const result = await oauth.processAuthorizationCodeOAuth2Response(as, client, response);
|
|
456
439
|
if (oauth.isOAuth2Error(result)) {
|
|
457
440
|
// TODO Handle OAuth 2.0 response body error
|
|
458
|
-
throw new StackAssertionError("OAuth error", { result });
|
|
441
|
+
throw new StackAssertionError("Outer OAuth error during authorization code response", { result });
|
|
459
442
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
443
|
+
return {
|
|
444
|
+
newUser: result.newUser,
|
|
445
|
+
accessToken: result.access_token,
|
|
446
|
+
refreshToken: result.refresh_token ?? throwErr("Refresh token not found in outer OAuth response"),
|
|
447
|
+
};
|
|
465
448
|
}
|
|
466
|
-
async signOut(
|
|
467
|
-
const tokenObj = await
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
"
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
449
|
+
async signOut(session) {
|
|
450
|
+
const tokenObj = await session.getPotentiallyExpiredTokens();
|
|
451
|
+
if (tokenObj) {
|
|
452
|
+
if (!tokenObj.refreshToken) {
|
|
453
|
+
// TODO implement this
|
|
454
|
+
captureError("clientInterface.signOut()", new StackAssertionError("Signing out a user without access to the refresh token does not invalidate the session on the server. Please open an issue in the Stack repository if you see this error"));
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
const res = await this.sendClientRequest("/auth/signout", {
|
|
458
|
+
method: "POST",
|
|
459
|
+
headers: {
|
|
460
|
+
"Content-Type": "application/json"
|
|
461
|
+
},
|
|
462
|
+
body: JSON.stringify({
|
|
463
|
+
refreshToken: tokenObj.refreshToken.token,
|
|
464
|
+
}),
|
|
465
|
+
}, session);
|
|
466
|
+
await res.json();
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
session.invalidate();
|
|
482
470
|
}
|
|
483
471
|
async getClientUserByToken(tokenStore) {
|
|
484
472
|
const response = await this.sendClientRequest("/current-user", {}, tokenStore);
|
|
@@ -487,13 +475,13 @@ export class StackClientInterface {
|
|
|
487
475
|
return Result.error(new Error("Failed to get user"));
|
|
488
476
|
return Result.ok(user);
|
|
489
477
|
}
|
|
490
|
-
async listClientUserTeamPermissions(options,
|
|
491
|
-
const response = await this.sendClientRequest(`/current-user/teams/${options.teamId}/permissions?type=${options.type}&direct=${options.direct}`, {},
|
|
478
|
+
async listClientUserTeamPermissions(options, session) {
|
|
479
|
+
const response = await this.sendClientRequest(`/current-user/teams/${options.teamId}/permissions?type=${options.type}&direct=${options.direct}`, {}, session);
|
|
492
480
|
const permissions = await response.json();
|
|
493
481
|
return permissions;
|
|
494
482
|
}
|
|
495
|
-
async listClientUserTeams(
|
|
496
|
-
const response = await this.sendClientRequest("/current-user/teams", {},
|
|
483
|
+
async listClientUserTeams(session) {
|
|
484
|
+
const response = await this.sendClientRequest("/current-user/teams", {}, session);
|
|
497
485
|
const teams = await response.json();
|
|
498
486
|
return teams;
|
|
499
487
|
}
|
|
@@ -504,31 +492,31 @@ export class StackClientInterface {
|
|
|
504
492
|
return Result.error(new Error("Failed to get project"));
|
|
505
493
|
return Result.ok(project);
|
|
506
494
|
}
|
|
507
|
-
async setClientUserCustomizableData(update,
|
|
495
|
+
async setClientUserCustomizableData(update, session) {
|
|
508
496
|
await this.sendClientRequest("/current-user", {
|
|
509
497
|
method: "PUT",
|
|
510
498
|
headers: {
|
|
511
499
|
"content-type": "application/json",
|
|
512
500
|
},
|
|
513
501
|
body: JSON.stringify(update),
|
|
514
|
-
},
|
|
502
|
+
}, session);
|
|
515
503
|
}
|
|
516
|
-
async listProjects(
|
|
517
|
-
const response = await this.sendClientRequest("/projects", {},
|
|
504
|
+
async listProjects(session) {
|
|
505
|
+
const response = await this.sendClientRequest("/projects", {}, session);
|
|
518
506
|
if (!response.ok) {
|
|
519
507
|
throw new Error("Failed to list projects: " + response.status + " " + (await response.text()));
|
|
520
508
|
}
|
|
521
509
|
const json = await response.json();
|
|
522
510
|
return json;
|
|
523
511
|
}
|
|
524
|
-
async createProject(project,
|
|
512
|
+
async createProject(project, session) {
|
|
525
513
|
const fetchResponse = await this.sendClientRequest("/projects", {
|
|
526
514
|
method: "POST",
|
|
527
515
|
headers: {
|
|
528
516
|
"content-type": "application/json",
|
|
529
517
|
},
|
|
530
518
|
body: JSON.stringify(project),
|
|
531
|
-
},
|
|
519
|
+
}, session);
|
|
532
520
|
if (!fetchResponse.ok) {
|
|
533
521
|
throw new Error("Failed to create project: " + fetchResponse.status + " " + (await fetchResponse.text()));
|
|
534
522
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { ClientInterfaceOptions, UserJson,
|
|
1
|
+
import { ClientInterfaceOptions, UserJson, StackClientInterface, OrglikeJson, UserUpdateJson, PermissionDefinitionJson, PermissionDefinitionScopeJson as PermissionDefinitionScopeJson, TeamMemberJson } from "./clientInterface";
|
|
2
2
|
import { Result } from "../utils/results";
|
|
3
3
|
import { ReadonlyJson } from "../utils/json";
|
|
4
4
|
import { EmailTemplateCrud, ListEmailTemplatesCrud } from "./crud/email-templates";
|
|
5
|
+
import { Session } from "../sessions";
|
|
5
6
|
export type ServerUserJson = UserJson & {
|
|
6
7
|
serverMetadata: ReadonlyJson;
|
|
7
8
|
};
|
|
@@ -28,27 +29,27 @@ export type ServerPermissionDefinitionJson = PermissionDefinitionJson & ServerPe
|
|
|
28
29
|
export type ServerAuthApplicationOptions = (ClientInterfaceOptions & ({
|
|
29
30
|
readonly secretServerKey: string;
|
|
30
31
|
} | {
|
|
31
|
-
readonly
|
|
32
|
+
readonly projectOwnerSession: Session;
|
|
32
33
|
}));
|
|
33
34
|
export declare const emailTemplateTypes: readonly ["EMAIL_VERIFICATION", "PASSWORD_RESET", "MAGIC_LINK"];
|
|
34
35
|
export type EmailTemplateType = typeof emailTemplateTypes[number];
|
|
35
36
|
export declare class StackServerInterface extends StackClientInterface {
|
|
36
37
|
options: ServerAuthApplicationOptions;
|
|
37
38
|
constructor(options: ServerAuthApplicationOptions);
|
|
38
|
-
protected sendServerRequest(path: string, options: RequestInit,
|
|
39
|
-
usedTokens:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
39
|
+
protected sendServerRequest(path: string, options: RequestInit, session: Session | null, requestType?: "server" | "admin"): Promise<Response & {
|
|
40
|
+
usedTokens: {
|
|
41
|
+
accessToken: import("../sessions").AccessToken;
|
|
42
|
+
refreshToken: import("../sessions").RefreshToken | null;
|
|
43
|
+
} | null;
|
|
43
44
|
}>;
|
|
44
|
-
getServerUserByToken(
|
|
45
|
+
getServerUserByToken(session: Session): Promise<Result<ServerUserJson>>;
|
|
45
46
|
getServerUserById(userId: string): Promise<Result<ServerUserJson>>;
|
|
46
47
|
listServerUserTeamPermissions(options: {
|
|
47
48
|
teamId: string;
|
|
48
49
|
type: 'global' | 'team';
|
|
49
50
|
direct: boolean;
|
|
50
|
-
},
|
|
51
|
-
listServerUserTeams(
|
|
51
|
+
}, session: Session): Promise<ServerPermissionDefinitionJson[]>;
|
|
52
|
+
listServerUserTeams(session: Session): Promise<ServerTeamJson[]>;
|
|
52
53
|
listPermissionDefinitions(): Promise<ServerPermissionDefinitionJson[]>;
|
|
53
54
|
createPermissionDefinition(data: ServerPermissionDefinitionCustomizableJson): Promise<ServerPermissionDefinitionJson>;
|
|
54
55
|
updatePermissionDefinition(permissionId: string, data: Partial<ServerPermissionDefinitionCustomizableJson>): Promise<void>;
|
|
@@ -7,17 +7,17 @@ export class StackServerInterface extends StackClientInterface {
|
|
|
7
7
|
super(options);
|
|
8
8
|
this.options = options;
|
|
9
9
|
}
|
|
10
|
-
async sendServerRequest(path, options,
|
|
10
|
+
async sendServerRequest(path, options, session, requestType = "server") {
|
|
11
11
|
return await this.sendClientRequest(path, {
|
|
12
12
|
...options,
|
|
13
13
|
headers: {
|
|
14
14
|
"x-stack-secret-server-key": "secretServerKey" in this.options ? this.options.secretServerKey : "",
|
|
15
15
|
...options.headers,
|
|
16
16
|
},
|
|
17
|
-
},
|
|
17
|
+
}, session, requestType);
|
|
18
18
|
}
|
|
19
|
-
async getServerUserByToken(
|
|
20
|
-
const response = await this.sendServerRequest("/current-user?server=true", {},
|
|
19
|
+
async getServerUserByToken(session) {
|
|
20
|
+
const response = await this.sendServerRequest("/current-user?server=true", {}, session);
|
|
21
21
|
const user = await response.json();
|
|
22
22
|
if (!user)
|
|
23
23
|
return Result.error(new Error("Failed to get user"));
|
|
@@ -30,13 +30,13 @@ export class StackServerInterface extends StackClientInterface {
|
|
|
30
30
|
return Result.error(new Error("Failed to get user"));
|
|
31
31
|
return Result.ok(user);
|
|
32
32
|
}
|
|
33
|
-
async listServerUserTeamPermissions(options,
|
|
34
|
-
const response = await this.sendServerRequest(`/current-user/teams/${options.teamId}/permissions?type=${options.type}&direct=${options.direct}&server=true`, {},
|
|
33
|
+
async listServerUserTeamPermissions(options, session) {
|
|
34
|
+
const response = await this.sendServerRequest(`/current-user/teams/${options.teamId}/permissions?type=${options.type}&direct=${options.direct}&server=true`, {}, session);
|
|
35
35
|
const permissions = await response.json();
|
|
36
36
|
return permissions;
|
|
37
37
|
}
|
|
38
|
-
async listServerUserTeams(
|
|
39
|
-
const response = await this.sendServerRequest("/current-user/teams?server=true", {},
|
|
38
|
+
async listServerUserTeams(session) {
|
|
39
|
+
const response = await this.sendServerRequest("/current-user/teams?server=true", {}, session);
|
|
40
40
|
const teams = await response.json();
|
|
41
41
|
return teams;
|
|
42
42
|
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export declare class AccessToken {
|
|
2
|
+
readonly token: string;
|
|
3
|
+
constructor(token: string);
|
|
4
|
+
}
|
|
5
|
+
export declare class RefreshToken {
|
|
6
|
+
readonly token: string;
|
|
7
|
+
constructor(token: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A session represents a user's session, which may or may not be valid. It may contain an access token, a refresh token, or both.
|
|
11
|
+
*
|
|
12
|
+
* A session never changes which user or session it belongs to, but the tokens may change over time.
|
|
13
|
+
*/
|
|
14
|
+
export declare class Session {
|
|
15
|
+
private readonly _options;
|
|
16
|
+
/**
|
|
17
|
+
* Each session has a session key that depends on the tokens inside. If the session has a refresh token, the session key depends only on the refresh token. If the session does not have a refresh token, the session key depends only on the access token.
|
|
18
|
+
*
|
|
19
|
+
* Multiple Session objects may have the same session key, which implies that they represent the same session by the same user. Furthermore, a session's key never changes over the lifetime of a session object.
|
|
20
|
+
*
|
|
21
|
+
* This makes session keys useful for caching and indexing sessions.
|
|
22
|
+
*/
|
|
23
|
+
readonly sessionKey: string;
|
|
24
|
+
/**
|
|
25
|
+
* An access token that is not known to be invalid (ie. may be valid, but may have expired).
|
|
26
|
+
*/
|
|
27
|
+
private _accessToken;
|
|
28
|
+
private readonly _refreshToken;
|
|
29
|
+
/**
|
|
30
|
+
* Whether the session as a whole is known to be invalid. Used as a cache to avoid making multiple requests to the server (sessions never go back to being valid after being invalidated).
|
|
31
|
+
*
|
|
32
|
+
* Applies to both the access token and the refresh token (it is possible for the access token to be invalid but the refresh token to be valid, in which case the session is still valid).
|
|
33
|
+
*/
|
|
34
|
+
private _knownToBeInvalid;
|
|
35
|
+
private _refreshPromise;
|
|
36
|
+
constructor(_options: {
|
|
37
|
+
refreshAccessTokenCallback(refreshToken: RefreshToken): Promise<AccessToken | null>;
|
|
38
|
+
refreshToken: string | null;
|
|
39
|
+
accessToken?: string | null;
|
|
40
|
+
});
|
|
41
|
+
static calculateSessionKey(ofTokens: {
|
|
42
|
+
refreshToken: string | null;
|
|
43
|
+
accessToken?: string | null;
|
|
44
|
+
}): string;
|
|
45
|
+
invalidate(): void;
|
|
46
|
+
onInvalidate(callback: () => void): {
|
|
47
|
+
unsubscribe: () => void;
|
|
48
|
+
};
|
|
49
|
+
getPotentiallyExpiredTokens(): Promise<{
|
|
50
|
+
accessToken: AccessToken;
|
|
51
|
+
refreshToken: RefreshToken | null;
|
|
52
|
+
} | null>;
|
|
53
|
+
getNewlyFetchedTokens(): Promise<{
|
|
54
|
+
accessToken: AccessToken;
|
|
55
|
+
refreshToken: RefreshToken | null;
|
|
56
|
+
} | null>;
|
|
57
|
+
markAccessTokenExpired(accessToken: AccessToken): void;
|
|
58
|
+
/**
|
|
59
|
+
* Note that a callback invocation with `null` does not mean the session has been invalidated; the access token may just have expired. Use `onInvalidate` to detect invalidation.
|
|
60
|
+
*/
|
|
61
|
+
onAccessTokenChange(callback: (newAccessToken: AccessToken | null) => void): {
|
|
62
|
+
unsubscribe: () => void;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* @returns An access token (cached if possible), or null if the session either does not represent a user or the session is invalid.
|
|
66
|
+
*/
|
|
67
|
+
private _getPotentiallyExpiredAccessToken;
|
|
68
|
+
/**
|
|
69
|
+
* You should prefer `getPotentiallyExpiredAccessToken` in almost all cases.
|
|
70
|
+
*
|
|
71
|
+
* @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.
|
|
72
|
+
*/
|
|
73
|
+
private _getNewlyFetchedAccessToken;
|
|
74
|
+
private _refreshAndSetRefreshPromise;
|
|
75
|
+
}
|