@stackframe/stack-shared 2.4.13 → 2.4.20

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.
@@ -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, InternalSession } 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 refreshAccessToken(tokenStore) {
41
+ async fetchNewAccessToken(refreshToken) {
41
42
  if (!('publishableClientKey' in this.options)) {
42
- // TODO fix
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 tokenStore.set({
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
- tokenStore.update(old => ({
90
- accessToken: result.access_token ?? null,
91
- refreshToken: result.refresh_token ?? old?.refreshToken ?? null,
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, tokenStoreOrNull, requestType = "client") {
95
- const tokenStore = tokenStoreOrNull ?? new AsyncStore({
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, tokenStore, requestType), 5, { exponentialDelayBase: 1000 }));
88
+ return await Result.orThrowAsync(Result.retry(() => this.sendClientRequestInner(path, requestOptions, session, requestType), 5, { exponentialDelayBase: 1000 }));
89
+ }
90
+ createSession(options) {
91
+ const session = new InternalSession({
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
- * This object will be modified for future retries, so it should be passed by reference.
117
- */
118
- tokenStore, requestType) {
119
- let tokenObj = await tokenStore.getOrWait();
120
- if (!tokenObj.accessToken && tokenObj.refreshToken) {
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 globalThis ? {} : {
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.accessToken ? {
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.refreshToken ? {
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 globalThis ? {} : {
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 expired, reset it and retry
185
+ // If the access token is invalid, reset it and retry
203
186
  if (processedRes.error instanceof KnownErrors.InvalidAccessToken) {
204
- tokenStore.set({
205
- accessToken: null,
206
- refreshToken: tokenObj.refreshToken,
207
- });
208
- return Result.error(new Error("Access token expired"));
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 AdminAccessTokenExpired only.
212
- if (adminTokenStore && (processedRes.error instanceof KnownErrors.AdminAccessTokenExpired || processedRes.error instanceof KnownErrors.ApiKeyNotFound)) {
213
- adminTokenStore.set({
214
- accessToken: null,
215
- refreshToken: adminTokenObj.refreshToken,
216
- });
217
- return Result.error(new Error("Admin access token expired"));
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, and should hence not be retried (except for access token expired above).
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 Error("Mismatch between x-stack-known-error header and error code in body; the server's response is invalid");
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, tokenStore) {
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
- }, tokenStore, [KnownErrors.EmailAlreadyVerified]);
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, tokenStore) {
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
- }, tokenStore, [KnownErrors.PasswordMismatch, KnownErrors.PasswordRequirementsNotMet]);
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, tokenStore) {
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
- }, tokenStore, [KnownErrors.EmailPasswordMismatch]);
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
- tokenStore.set({
343
+ return {
361
344
  accessToken: result.accessToken,
362
345
  refreshToken: result.refreshToken,
363
- });
346
+ };
364
347
  }
365
- async signUpWithCredential(email, password, emailVerificationRedirectUrl, tokenStore) {
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
- }, tokenStore, [KnownErrors.UserEmailAlreadyExists, KnownErrors.PasswordRequirementsNotMet]);
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
- tokenStore.set({
364
+ return {
382
365
  accessToken: result.accessToken,
383
366
  refreshToken: result.refreshToken,
384
- });
367
+ };
385
368
  }
386
- async signInWithMagicLink(code, tokenStore) {
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
- tokenStore.set({
383
+ return {
401
384
  accessToken: result.accessToken,
402
385
  refreshToken: result.refreshToken,
403
- });
404
- return { newUser: result.newUser };
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, tokenStore) {
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
- tokenStore.update(old => ({
461
- accessToken: result.access_token ?? null,
462
- refreshToken: result.refresh_token ?? old?.refreshToken ?? null,
463
- }));
464
- return result;
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(tokenStore) {
467
- const tokenObj = await tokenStore.getOrWait();
468
- const res = await this.sendClientRequest("/auth/signout", {
469
- method: "POST",
470
- headers: {
471
- "Content-Type": "application/json"
472
- },
473
- body: JSON.stringify({
474
- refreshToken: tokenObj.refreshToken ?? "",
475
- }),
476
- }, tokenStore);
477
- await res.json();
478
- tokenStore.set({
479
- accessToken: null,
480
- refreshToken: null,
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.markInvalid();
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, tokenStore) {
491
- const response = await this.sendClientRequest(`/current-user/teams/${options.teamId}/permissions?type=${options.type}&direct=${options.direct}`, {}, tokenStore);
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(tokenStore) {
496
- const response = await this.sendClientRequest("/current-user/teams", {}, tokenStore);
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, tokenStore) {
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
- }, tokenStore);
502
+ }, session);
515
503
  }
516
- async listProjects(tokenStore) {
517
- const response = await this.sendClientRequest("/projects", {}, tokenStore);
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, tokenStore) {
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
- }, tokenStore);
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, TokenStore, StackClientInterface, ReadonlyTokenStore, OrglikeJson, UserUpdateJson, PermissionDefinitionJson, PermissionDefinitionScopeJson as PermissionDefinitionScopeJson, TeamMemberJson } from "./clientInterface";
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 { InternalSession } 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 projectOwnerTokens: ReadonlyTokenStore;
32
+ readonly projectOwnerSession: InternalSession;
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, tokenStore: TokenStore | null, requestType?: "server" | "admin"): Promise<Response & {
39
- usedTokens: Readonly<{
40
- refreshToken: string | null;
41
- accessToken: string | null;
42
- }>;
39
+ protected sendServerRequest(path: string, options: RequestInit, session: InternalSession | 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(tokenStore: TokenStore): Promise<Result<ServerUserJson>>;
45
+ getServerUserByToken(session: InternalSession): 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
- }, tokenStore: TokenStore): Promise<ServerPermissionDefinitionJson[]>;
51
- listServerUserTeams(tokenStore: TokenStore): Promise<ServerTeamJson[]>;
51
+ }, session: InternalSession): Promise<ServerPermissionDefinitionJson[]>;
52
+ listServerUserTeams(session: InternalSession): 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, tokenStore, requestType = "server") {
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
- }, tokenStore, requestType);
17
+ }, session, requestType);
18
18
  }
19
- async getServerUserByToken(tokenStore) {
20
- const response = await this.sendServerRequest("/current-user?server=true", {}, tokenStore);
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, tokenStore) {
34
- const response = await this.sendServerRequest(`/current-user/teams/${options.teamId}/permissions?type=${options.type}&direct=${options.direct}&server=true`, {}, tokenStore);
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(tokenStore) {
39
- const response = await this.sendServerRequest("/current-user/teams?server=true", {}, tokenStore);
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,94 @@
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
+ * An InternalSession 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 in it may change over time.
13
+ */
14
+ export declare class InternalSession {
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 is 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
+ /**
46
+ * Marks the session object as invalid, meaning that the refresh and access tokens can no longer be used.
47
+ */
48
+ markInvalid(): void;
49
+ onInvalidate(callback: () => void): {
50
+ unsubscribe: () => void;
51
+ };
52
+ /**
53
+ * Returns the access token if it is found in the cache, fetching it otherwise.
54
+ *
55
+ * 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).
56
+ *
57
+ * @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.
58
+ */
59
+ getPotentiallyExpiredTokens(): Promise<{
60
+ accessToken: AccessToken;
61
+ refreshToken: RefreshToken | null;
62
+ } | null>;
63
+ /**
64
+ * Fetches new tokens that are, at the time of fetching, guaranteed to be valid.
65
+ *
66
+ * 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.
67
+ *
68
+ * In most cases, you should prefer `getPotentiallyExpiredTokens` with a fallback to `markAccessTokenExpired` and a retry mechanism if the endpoint rejects the token.
69
+ *
70
+ * @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).
71
+ */
72
+ fetchNewTokens(): Promise<{
73
+ accessToken: AccessToken;
74
+ refreshToken: RefreshToken | null;
75
+ } | null>;
76
+ markAccessTokenExpired(accessToken: AccessToken): void;
77
+ /**
78
+ * 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.
79
+ */
80
+ onAccessTokenChange(callback: (newAccessToken: AccessToken | null) => void): {
81
+ unsubscribe: () => void;
82
+ };
83
+ /**
84
+ * @returns An access token (cached if possible), or null if the session either does not represent a user or the session is invalid.
85
+ */
86
+ private _getPotentiallyExpiredAccessToken;
87
+ /**
88
+ * You should prefer `getPotentiallyExpiredAccessToken` in almost all cases.
89
+ *
90
+ * @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.
91
+ */
92
+ private _getNewlyFetchedAccessToken;
93
+ private _refreshAndSetRefreshPromise;
94
+ }