@stackframe/stack-shared 2.8.39 → 2.8.40

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/esm/schema-fields.js +17 -1
  3. package/dist/esm/schema-fields.js.map +1 -1
  4. package/dist/esm/sessions.js +19 -9
  5. package/dist/esm/sessions.js.map +1 -1
  6. package/dist/esm/utils/paginated-lists.js +230 -0
  7. package/dist/esm/utils/paginated-lists.js.map +1 -0
  8. package/dist/helpers/password.d.mts +3 -3
  9. package/dist/helpers/password.d.ts +3 -3
  10. package/dist/index.d.mts +4 -5
  11. package/dist/index.d.ts +4 -5
  12. package/dist/interface/admin-interface.d.mts +4 -5
  13. package/dist/interface/admin-interface.d.ts +4 -5
  14. package/dist/interface/client-interface.d.mts +0 -1
  15. package/dist/interface/client-interface.d.ts +0 -1
  16. package/dist/interface/crud/current-user.d.mts +4 -4
  17. package/dist/interface/crud/current-user.d.ts +4 -4
  18. package/dist/interface/crud/oauth-providers.d.mts +4 -4
  19. package/dist/interface/crud/oauth-providers.d.ts +4 -4
  20. package/dist/interface/crud/project-api-keys.d.mts +2 -2
  21. package/dist/interface/crud/project-api-keys.d.ts +2 -2
  22. package/dist/interface/crud/projects.d.mts +12 -12
  23. package/dist/interface/crud/projects.d.ts +12 -12
  24. package/dist/interface/crud/team-member-profiles.d.mts +6 -6
  25. package/dist/interface/crud/team-member-profiles.d.ts +6 -6
  26. package/dist/interface/crud/transactions.d.mts +1 -1
  27. package/dist/interface/crud/transactions.d.ts +1 -1
  28. package/dist/interface/crud/users.d.mts +6 -6
  29. package/dist/interface/crud/users.d.ts +6 -6
  30. package/dist/interface/server-interface.d.mts +0 -1
  31. package/dist/interface/server-interface.d.ts +0 -1
  32. package/dist/known-errors.d.mts +3 -3
  33. package/dist/known-errors.d.ts +3 -3
  34. package/dist/schema-fields.d.mts +36 -7
  35. package/dist/schema-fields.d.ts +36 -7
  36. package/dist/schema-fields.js +19 -2
  37. package/dist/schema-fields.js.map +1 -1
  38. package/dist/sessions.d.mts +26 -4
  39. package/dist/sessions.d.ts +26 -4
  40. package/dist/sessions.js +19 -9
  41. package/dist/sessions.js.map +1 -1
  42. package/dist/utils/paginated-lists.d.mts +176 -0
  43. package/dist/utils/paginated-lists.d.ts +176 -0
  44. package/dist/utils/paginated-lists.js +256 -0
  45. package/dist/utils/paginated-lists.js.map +1 -0
  46. package/dist/utils/stores.d.mts +6 -6
  47. package/dist/utils/stores.d.ts +6 -6
  48. package/package.json +1 -1
@@ -1,9 +1,27 @@
1
- import * as jose from 'jose';
1
+ import { InferType } from 'yup';
2
+ import { accessTokenPayloadSchema } from './schema-fields.js';
3
+ import './utils/currency-constants.js';
4
+ import './utils/dates.js';
2
5
 
6
+ type AccessTokenPayload = InferType<typeof accessTokenPayloadSchema>;
3
7
  declare class AccessToken {
4
8
  readonly token: string;
5
9
  constructor(token: string);
6
- get decoded(): jose.JWTPayload;
10
+ get payload(): {
11
+ exp?: number | undefined;
12
+ sub: string;
13
+ name: string | null;
14
+ iss: string;
15
+ aud: string;
16
+ project_id: string;
17
+ branch_id: string;
18
+ refresh_token_id: string;
19
+ role: "authenticated";
20
+ email: string | null;
21
+ email_verified: boolean;
22
+ selected_team_id: string | null;
23
+ is_anonymous: boolean;
24
+ };
7
25
  get expiresAt(): Date;
8
26
  /**
9
27
  * @returns The number of milliseconds until the access token expires, or 0 if it has already expired.
@@ -62,12 +80,16 @@ declare class InternalSession {
62
80
  onInvalidate(callback: () => void): {
63
81
  unsubscribe: () => void;
64
82
  };
83
+ /**
84
+ * Returns the access token if it is found in the cache and not expired yet, or null otherwise. Never fetches new tokens.
85
+ */
86
+ getAccessTokenIfNotExpiredYet(minMillisUntilExpiration: number): AccessToken | null;
65
87
  /**
66
88
  * Returns the access token if it is found in the cache, fetching it otherwise.
67
89
  *
68
90
  * 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).
69
91
  *
70
- * @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.
92
+ * @returns null if the session is known to be invalid, cached tokens if they exist in the cache and the access token hasn't expired yet (the refresh token might still be invalid), or new tokens otherwise.
71
93
  */
72
94
  getOrFetchLikelyValidTokens(minMillisUntilExpiration: number): Promise<{
73
95
  accessToken: AccessToken;
@@ -106,4 +128,4 @@ declare class InternalSession {
106
128
  private _refreshAndSetRefreshPromise;
107
129
  }
108
130
 
109
- export { AccessToken, InternalSession, RefreshToken };
131
+ export { AccessToken, type AccessTokenPayload, InternalSession, RefreshToken };
package/dist/sessions.js CHANGED
@@ -36,6 +36,7 @@ __export(sessions_exports, {
36
36
  });
37
37
  module.exports = __toCommonJS(sessions_exports);
38
38
  var jose = __toESM(require("jose"));
39
+ var import_schema_fields = require("./schema-fields.js");
39
40
  var import_errors = require("./utils/errors.js");
40
41
  var import_stores = require("./utils/stores.js");
41
42
  var AccessToken = class {
@@ -45,11 +46,12 @@ var AccessToken = class {
45
46
  throw new import_errors.StackAssertionError("Access token is the string 'undefined'; it's unlikely this is the correct value. They're supposed to be unguessable!");
46
47
  }
47
48
  }
48
- get decoded() {
49
- return jose.decodeJwt(this.token);
49
+ get payload() {
50
+ const payload = jose.decodeJwt(this.token);
51
+ return import_schema_fields.accessTokenPayloadSchema.validateSync(payload);
50
52
  }
51
53
  get expiresAt() {
52
- const { exp } = this.decoded;
54
+ const { exp } = this.payload;
53
55
  if (exp === void 0) return /* @__PURE__ */ new Date(864e13);
54
56
  return new Date(exp * 1e3);
55
57
  }
@@ -113,19 +115,27 @@ var InternalSession = class _InternalSession {
113
115
  onInvalidate(callback) {
114
116
  return this._knownToBeInvalid.onChange(() => callback());
115
117
  }
118
+ /**
119
+ * Returns the access token if it is found in the cache and not expired yet, or null otherwise. Never fetches new tokens.
120
+ */
121
+ getAccessTokenIfNotExpiredYet(minMillisUntilExpiration) {
122
+ if (minMillisUntilExpiration > 6e4) {
123
+ throw new Error(`Required access token expiry ${minMillisUntilExpiration}ms is too long; access tokens are too short to be used for more than 60s`);
124
+ }
125
+ const accessToken = this._getPotentiallyInvalidAccessTokenIfAvailable();
126
+ if (!accessToken || accessToken.expiresInMillis < minMillisUntilExpiration) return null;
127
+ return accessToken;
128
+ }
116
129
  /**
117
130
  * Returns the access token if it is found in the cache, fetching it otherwise.
118
131
  *
119
132
  * 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).
120
133
  *
121
- * @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.
134
+ * @returns null if the session is known to be invalid, cached tokens if they exist in the cache and the access token hasn't expired yet (the refresh token might still be invalid), or new tokens otherwise.
122
135
  */
123
136
  async getOrFetchLikelyValidTokens(minMillisUntilExpiration) {
124
- if (minMillisUntilExpiration >= 6e4) {
125
- throw new Error(`Required access token expiry ${minMillisUntilExpiration}ms is too long; access tokens are too short to be used for more than 60s`);
126
- }
127
- const accessToken = this._getPotentiallyInvalidAccessTokenIfAvailable();
128
- if (!accessToken || accessToken.expiresInMillis < minMillisUntilExpiration) {
137
+ const accessToken = this.getAccessTokenIfNotExpiredYet(minMillisUntilExpiration);
138
+ if (!accessToken) {
129
139
  const newTokens = await this.fetchNewTokens();
130
140
  const expiresInMillis = newTokens?.accessToken.expiresInMillis;
131
141
  if (expiresInMillis && expiresInMillis < minMillisUntilExpiration) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/sessions.ts"],"sourcesContent":["import * as jose from 'jose';\nimport { StackAssertionError } from \"./utils/errors\";\nimport { Store } from \"./utils/stores\";\n\nexport class AccessToken {\n constructor(\n public readonly token: string,\n ) {\n if (token === \"undefined\") {\n throw new StackAssertionError(\"Access token is the string 'undefined'; it's unlikely this is the correct value. They're supposed to be unguessable!\");\n }\n }\n\n get decoded() {\n return jose.decodeJwt(this.token);\n }\n\n get expiresAt(): Date {\n const { exp } = this.decoded;\n if (exp === undefined) return new Date(8640000000000000); // max date value\n return new Date(exp * 1000);\n }\n\n /**\n * @returns The number of milliseconds until the access token expires, or 0 if it has already expired.\n */\n get expiresInMillis(): number {\n return Math.max(0, this.expiresAt.getTime() - Date.now());\n }\n\n isExpired(): boolean {\n return this.expiresInMillis <= 0;\n }\n}\n\nexport class RefreshToken {\n constructor(\n public readonly token: string,\n ) {\n if (token === \"undefined\") {\n throw new StackAssertionError(\"Refresh token is the string 'undefined'; it's unlikely this is the correct value. They're supposed to be unguessable!\");\n }\n }\n}\n\n/**\n * 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.\n *\n * A session never changes which user or session it belongs to, but the tokens in it may change over time.\n */\nexport class InternalSession {\n /**\n * 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.\n *\n * 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.\n *\n * This is useful for caching and indexing sessions.\n */\n public readonly sessionKey: string;\n\n /**\n * An access token that is not known to be invalid (ie. may be valid, but may have expired).\n */\n private _accessToken: Store<AccessToken | null>;\n private readonly _refreshToken: RefreshToken | null;\n\n /**\n * Whether the session as a whole is known to be invalid (ie. both access and refresh tokens are invalid). Used as a cache to avoid making multiple requests to the server (sessions never go back to being valid after being invalidated).\n *\n * It is possible for the access token to be invalid but the refresh token to be valid, in which case the session is\n * still valid (just needs a refresh). It is also possible for the access token to be valid but the refresh token to\n * be invalid, in which case the session is also valid (eg. if the refresh token is null because the user only passed\n * in an access token, eg. in a server-side request handler).\n */\n private _knownToBeInvalid = new Store<boolean>(false);\n\n private _refreshPromise: Promise<AccessToken | null> | null = null;\n\n constructor(private readonly _options: {\n refreshAccessTokenCallback(refreshToken: RefreshToken): Promise<AccessToken | null>,\n refreshToken: string | null,\n accessToken?: string | null,\n }) {\n this._accessToken = new Store(_options.accessToken ? new AccessToken(_options.accessToken) : null);\n this._refreshToken = _options.refreshToken ? new RefreshToken(_options.refreshToken) : null;\n if (_options.accessToken === null && _options.refreshToken === null) {\n // this session is already invalid\n this._knownToBeInvalid.set(true);\n }\n this.sessionKey = InternalSession.calculateSessionKey({ accessToken: _options.accessToken ?? null, refreshToken: _options.refreshToken });\n }\n\n static calculateSessionKey(ofTokens: { refreshToken: string | null, accessToken?: string | null }): string {\n if (ofTokens.refreshToken) {\n return `refresh-${ofTokens.refreshToken}`;\n } else if (ofTokens.accessToken) {\n return `access-${ofTokens.accessToken}`;\n } else {\n return \"not-logged-in\";\n }\n }\n\n isKnownToBeInvalid() {\n return this._knownToBeInvalid.get();\n }\n\n /**\n * Marks the session object as invalid, meaning that the refresh and access tokens can no longer be used.\n */\n markInvalid() {\n this._accessToken.set(null);\n this._knownToBeInvalid.set(true);\n }\n\n onInvalidate(callback: () => void): { unsubscribe: () => void } {\n return this._knownToBeInvalid.onChange(() => callback());\n }\n\n /**\n * Returns the access token if it is found in the cache, fetching it otherwise.\n *\n * 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).\n *\n * @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.\n */\n async getOrFetchLikelyValidTokens(minMillisUntilExpiration: number): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | null } | null> {\n if (minMillisUntilExpiration >= 60_000) {\n throw new Error(`Required access token expiry ${minMillisUntilExpiration}ms is too long; access tokens are too short to be used for more than 60s`);\n }\n\n const accessToken = this._getPotentiallyInvalidAccessTokenIfAvailable();\n if (!accessToken || accessToken.expiresInMillis < minMillisUntilExpiration) {\n const newTokens = await this.fetchNewTokens();\n const expiresInMillis = newTokens?.accessToken.expiresInMillis;\n if (expiresInMillis && expiresInMillis < minMillisUntilExpiration) {\n throw new StackAssertionError(`Required access token expiry ${minMillisUntilExpiration}ms is too long; access tokens are too short when they're generated (${expiresInMillis}ms)`);\n }\n return newTokens;\n }\n return { accessToken, refreshToken: this._refreshToken };\n }\n\n /**\n * Fetches new tokens that are, at the time of fetching, guaranteed to be valid.\n *\n * 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.\n *\n * In most cases, you should prefer `getOrFetchLikelyValidTokens`.\n *\n * @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).\n */\n async fetchNewTokens(): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | null } | null> {\n const accessToken = await this._getNewlyFetchedAccessToken();\n return accessToken ? { accessToken, refreshToken: this._refreshToken } : null;\n }\n\n markAccessTokenExpired(accessToken: AccessToken) {\n // TODO we don't need this anymore, since we now check the expiry by ourselves\n if (this._accessToken.get() === accessToken) {\n this._accessToken.set(null);\n }\n }\n\n /**\n * 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.\n */\n onAccessTokenChange(callback: (newAccessToken: AccessToken | null) => void): { unsubscribe: () => void } {\n return this._accessToken.onChange(callback);\n }\n\n /**\n * @returns An access token, which may be expired or expire soon, or null if it is known to be invalid.\n */\n private _getPotentiallyInvalidAccessTokenIfAvailable(): AccessToken | null {\n if (!this._refreshToken) return null;\n if (this.isKnownToBeInvalid()) return null;\n\n const accessToken = this._accessToken.get();\n if (accessToken && !accessToken.isExpired()) return accessToken;\n\n return null;\n }\n\n /**\n * You should prefer `_getOrFetchPotentiallyInvalidAccessToken` in almost all cases.\n *\n * @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.\n */\n private async _getNewlyFetchedAccessToken(): Promise<AccessToken | null> {\n if (!this._refreshToken) return null;\n if (this._knownToBeInvalid.get()) return null;\n\n if (!this._refreshPromise) {\n this._refreshAndSetRefreshPromise(this._refreshToken);\n }\n return await this._refreshPromise;\n }\n\n private _refreshAndSetRefreshPromise(refreshToken: RefreshToken) {\n let refreshPromise: Promise<AccessToken | null> = this._options.refreshAccessTokenCallback(refreshToken).then((accessToken) => {\n if (refreshPromise === this._refreshPromise) {\n this._refreshPromise = null;\n this._accessToken.set(accessToken);\n if (!accessToken) {\n this.markInvalid();\n }\n }\n return accessToken;\n });\n this._refreshPromise = refreshPromise;\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/sessions.ts"],"sourcesContent":["import * as jose from 'jose';\nimport { InferType } from 'yup';\nimport { accessTokenPayloadSchema } from './schema-fields';\nimport { StackAssertionError } from \"./utils/errors\";\nimport { Store } from \"./utils/stores\";\n\n\nexport type AccessTokenPayload = InferType<typeof accessTokenPayloadSchema>;\n\nexport class AccessToken {\n constructor(\n public readonly token: string,\n ) {\n if (token === \"undefined\") {\n throw new StackAssertionError(\"Access token is the string 'undefined'; it's unlikely this is the correct value. They're supposed to be unguessable!\");\n }\n }\n\n get payload() {\n const payload = jose.decodeJwt(this.token);\n return accessTokenPayloadSchema.validateSync(payload);\n }\n\n get expiresAt(): Date {\n const { exp } = this.payload;\n if (exp === undefined) return new Date(8640000000000000); // max date value\n return new Date(exp * 1000);\n }\n\n /**\n * @returns The number of milliseconds until the access token expires, or 0 if it has already expired.\n */\n get expiresInMillis(): number {\n return Math.max(0, this.expiresAt.getTime() - Date.now());\n }\n\n isExpired(): boolean {\n return this.expiresInMillis <= 0;\n }\n}\n\nexport class RefreshToken {\n constructor(\n public readonly token: string,\n ) {\n if (token === \"undefined\") {\n throw new StackAssertionError(\"Refresh token is the string 'undefined'; it's unlikely this is the correct value. They're supposed to be unguessable!\");\n }\n }\n}\n\n/**\n * 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.\n *\n * A session never changes which user or session it belongs to, but the tokens in it may change over time.\n */\nexport class InternalSession {\n /**\n * 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.\n *\n * 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.\n *\n * This is useful for caching and indexing sessions.\n */\n public readonly sessionKey: string;\n\n /**\n * An access token that is not known to be invalid (ie. may be valid, but may have expired).\n */\n private _accessToken: Store<AccessToken | null>;\n private readonly _refreshToken: RefreshToken | null;\n\n /**\n * Whether the session as a whole is known to be invalid (ie. both access and refresh tokens are invalid). Used as a cache to avoid making multiple requests to the server (sessions never go back to being valid after being invalidated).\n *\n * It is possible for the access token to be invalid but the refresh token to be valid, in which case the session is\n * still valid (just needs a refresh). It is also possible for the access token to be valid but the refresh token to\n * be invalid, in which case the session is also valid (eg. if the refresh token is null because the user only passed\n * in an access token, eg. in a server-side request handler).\n */\n private _knownToBeInvalid = new Store<boolean>(false);\n\n private _refreshPromise: Promise<AccessToken | null> | null = null;\n\n constructor(private readonly _options: {\n refreshAccessTokenCallback(refreshToken: RefreshToken): Promise<AccessToken | null>,\n refreshToken: string | null,\n accessToken?: string | null,\n }) {\n this._accessToken = new Store(_options.accessToken ? new AccessToken(_options.accessToken) : null);\n this._refreshToken = _options.refreshToken ? new RefreshToken(_options.refreshToken) : null;\n if (_options.accessToken === null && _options.refreshToken === null) {\n // this session is already invalid\n this._knownToBeInvalid.set(true);\n }\n this.sessionKey = InternalSession.calculateSessionKey({ accessToken: _options.accessToken ?? null, refreshToken: _options.refreshToken });\n }\n\n static calculateSessionKey(ofTokens: { refreshToken: string | null, accessToken?: string | null }): string {\n if (ofTokens.refreshToken) {\n return `refresh-${ofTokens.refreshToken}`;\n } else if (ofTokens.accessToken) {\n return `access-${ofTokens.accessToken}`;\n } else {\n return \"not-logged-in\";\n }\n }\n\n isKnownToBeInvalid() {\n return this._knownToBeInvalid.get();\n }\n\n /**\n * Marks the session object as invalid, meaning that the refresh and access tokens can no longer be used.\n */\n markInvalid() {\n this._accessToken.set(null);\n this._knownToBeInvalid.set(true);\n }\n\n onInvalidate(callback: () => void): { unsubscribe: () => void } {\n return this._knownToBeInvalid.onChange(() => callback());\n }\n\n /**\n * Returns the access token if it is found in the cache and not expired yet, or null otherwise. Never fetches new tokens.\n */\n getAccessTokenIfNotExpiredYet(minMillisUntilExpiration: number): AccessToken | null {\n if (minMillisUntilExpiration > 60_000) {\n throw new Error(`Required access token expiry ${minMillisUntilExpiration}ms is too long; access tokens are too short to be used for more than 60s`);\n }\n\n const accessToken = this._getPotentiallyInvalidAccessTokenIfAvailable();\n if (!accessToken || accessToken.expiresInMillis < minMillisUntilExpiration) return null;\n return accessToken;\n }\n\n /**\n * Returns the access token if it is found in the cache, fetching it otherwise.\n *\n * 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).\n *\n * @returns null if the session is known to be invalid, cached tokens if they exist in the cache and the access token hasn't expired yet (the refresh token might still be invalid), or new tokens otherwise.\n */\n async getOrFetchLikelyValidTokens(minMillisUntilExpiration: number): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | null } | null> {\n const accessToken = this.getAccessTokenIfNotExpiredYet(minMillisUntilExpiration);\n if (!accessToken) {\n const newTokens = await this.fetchNewTokens();\n const expiresInMillis = newTokens?.accessToken.expiresInMillis;\n if (expiresInMillis && expiresInMillis < minMillisUntilExpiration) {\n throw new StackAssertionError(`Required access token expiry ${minMillisUntilExpiration}ms is too long; access tokens are too short when they're generated (${expiresInMillis}ms)`);\n }\n return newTokens;\n }\n return { accessToken, refreshToken: this._refreshToken };\n }\n\n /**\n * Fetches new tokens that are, at the time of fetching, guaranteed to be valid.\n *\n * 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.\n *\n * In most cases, you should prefer `getOrFetchLikelyValidTokens`.\n *\n * @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).\n */\n async fetchNewTokens(): Promise<{ accessToken: AccessToken, refreshToken: RefreshToken | null } | null> {\n const accessToken = await this._getNewlyFetchedAccessToken();\n return accessToken ? { accessToken, refreshToken: this._refreshToken } : null;\n }\n\n markAccessTokenExpired(accessToken: AccessToken) {\n // TODO we don't need this anymore, since we now check the expiry by ourselves\n if (this._accessToken.get() === accessToken) {\n this._accessToken.set(null);\n }\n }\n\n /**\n * 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.\n */\n onAccessTokenChange(callback: (newAccessToken: AccessToken | null) => void): { unsubscribe: () => void } {\n return this._accessToken.onChange(callback);\n }\n\n /**\n * @returns An access token, which may be expired or expire soon, or null if it is known to be invalid.\n */\n private _getPotentiallyInvalidAccessTokenIfAvailable(): AccessToken | null {\n if (!this._refreshToken) return null;\n if (this.isKnownToBeInvalid()) return null;\n\n const accessToken = this._accessToken.get();\n if (accessToken && !accessToken.isExpired()) return accessToken;\n\n return null;\n }\n\n /**\n * You should prefer `_getOrFetchPotentiallyInvalidAccessToken` in almost all cases.\n *\n * @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.\n */\n private async _getNewlyFetchedAccessToken(): Promise<AccessToken | null> {\n if (!this._refreshToken) return null;\n if (this._knownToBeInvalid.get()) return null;\n\n if (!this._refreshPromise) {\n this._refreshAndSetRefreshPromise(this._refreshToken);\n }\n return await this._refreshPromise;\n }\n\n private _refreshAndSetRefreshPromise(refreshToken: RefreshToken) {\n let refreshPromise: Promise<AccessToken | null> = this._options.refreshAccessTokenCallback(refreshToken).then((accessToken) => {\n if (refreshPromise === this._refreshPromise) {\n this._refreshPromise = null;\n this._accessToken.set(accessToken);\n if (!accessToken) {\n this.markInvalid();\n }\n }\n return accessToken;\n });\n this._refreshPromise = refreshPromise;\n }\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,176 @@
1
+ type QueryOptions<Type extends 'next' | 'prev', Cursor, Filter, OrderBy> = {
2
+ filter: Filter;
3
+ orderBy: OrderBy;
4
+ limit: number;
5
+ /**
6
+ * Whether the limit should be treated as an exact value, or an approximate value.
7
+ *
8
+ * If set to 'exact', less items will only be returned if the list item is the first or last item.
9
+ *
10
+ * If set to 'at-least' or 'approximate', the implementation may decide to return more items than the limit requested if doing so comes at no (or negligible) extra cost.
11
+ *
12
+ * If set to 'at-most' or 'approximate', the implementation may decide to return less items than the limit requested if requesting more items would come at a non-negligible extra cost. In this case, if limit > 0, the implementation must still make progress towards the end of the list and the returned cursor must be different from the one passed in.
13
+ *
14
+ * Defaults to 'exact'.
15
+ */
16
+ limitPrecision: 'exact' | 'at-least' | 'at-most' | 'approximate';
17
+ } & ([Type] extends [never] ? unknown : [Type] extends ['next'] ? {
18
+ after: Cursor;
19
+ } : [Type] extends ['prev'] ? {
20
+ before: Cursor;
21
+ } : {
22
+ cursor: Cursor;
23
+ });
24
+ type ImplQueryOptions<Type extends 'next' | 'prev', Cursor, Filter, OrderBy> = QueryOptions<Type, Cursor, Filter, OrderBy> & {
25
+ limitPrecision: 'approximate';
26
+ };
27
+ type QueryResult<Item, Cursor> = {
28
+ items: {
29
+ item: Item;
30
+ itemCursor: Cursor;
31
+ }[];
32
+ isFirst: boolean;
33
+ isLast: boolean;
34
+ cursor: Cursor;
35
+ };
36
+ type ImplQueryResult<Item, Cursor> = {
37
+ items: {
38
+ item: Item;
39
+ itemCursor: Cursor;
40
+ }[];
41
+ isFirst: boolean;
42
+ isLast: boolean;
43
+ cursor: Cursor;
44
+ };
45
+ declare abstract class PaginatedList<Item, Cursor extends string, Filter extends unknown, OrderBy extends unknown> {
46
+ protected abstract _getFirstCursor(): Cursor;
47
+ protected abstract _getLastCursor(): Cursor;
48
+ protected abstract _compare(orderBy: OrderBy, a: Item, b: Item): number;
49
+ protected abstract _nextOrPrev(type: 'next' | 'prev', options: ImplQueryOptions<'next' | 'prev', Cursor, Filter, OrderBy>): Promise<ImplQueryResult<Item, Cursor>>;
50
+ getFirstCursor(): Cursor;
51
+ getLastCursor(): Cursor;
52
+ compare(orderBy: OrderBy, a: Item, b: Item): number;
53
+ nextOrPrev(type: 'next' | 'prev', options: QueryOptions<'next' | 'prev', Cursor, Filter, OrderBy>): Promise<QueryResult<Item, Cursor>>;
54
+ next({ after, ...rest }: QueryOptions<'next', Cursor, Filter, OrderBy>): Promise<QueryResult<Item, Cursor>>;
55
+ prev({ before, ...rest }: QueryOptions<'prev', Cursor, Filter, OrderBy>): Promise<QueryResult<Item, Cursor>>;
56
+ flatMap<Item2, Cursor2 extends string, Filter2 extends unknown, OrderBy2 extends unknown>(options: {
57
+ itemMapper: (itemEntry: {
58
+ item: Item;
59
+ itemCursor: Cursor;
60
+ }, filter: Filter2, orderBy: OrderBy2) => {
61
+ item: Item2;
62
+ itemCursor: Cursor2;
63
+ }[];
64
+ compare: (orderBy: OrderBy2, a: Item2, b: Item2) => number;
65
+ newCursorFromOldCursor: (cursor: Cursor) => Cursor2;
66
+ oldCursorFromNewCursor: (cursor: Cursor2) => Cursor;
67
+ oldFilterFromNewFilter: (filter: Filter2) => Filter;
68
+ oldOrderByFromNewOrderBy: (orderBy: OrderBy2) => OrderBy;
69
+ estimateItemsToFetch: (options: {
70
+ filter: Filter2;
71
+ orderBy: OrderBy2;
72
+ limit: number;
73
+ }) => number;
74
+ }): PaginatedList<Item2, Cursor2, Filter2, OrderBy2>;
75
+ map<Item2, Filter2 extends unknown, OrderBy2 extends unknown>(options: {
76
+ itemMapper: (item: Item) => Item2;
77
+ oldItemFromNewItem: (item: Item2) => Item;
78
+ oldFilterFromNewFilter: (filter: Filter2) => Filter;
79
+ oldOrderByFromNewOrderBy: (orderBy: OrderBy2) => OrderBy;
80
+ }): PaginatedList<Item2, Cursor, Filter2, OrderBy2>;
81
+ filter<Filter2 extends unknown>(options: {
82
+ filter: (item: Item, filter: Filter2) => boolean;
83
+ oldFilterFromNewFilter: (filter: Filter2) => Filter;
84
+ estimateItemsToFetch: (options: {
85
+ filter: Filter2;
86
+ orderBy: OrderBy;
87
+ limit: number;
88
+ }) => number;
89
+ }): PaginatedList<Item, Cursor, Filter2, OrderBy>;
90
+ addFilter<AddedFilter extends unknown>(options: {
91
+ filter: (item: Item, filter: Filter & AddedFilter) => boolean;
92
+ estimateItemsToFetch: (options: {
93
+ filter: Filter & AddedFilter;
94
+ orderBy: OrderBy;
95
+ limit: number;
96
+ }) => number;
97
+ }): PaginatedList<Item, Cursor, Filter & AddedFilter, OrderBy>;
98
+ static merge<Item, Filter extends unknown, OrderBy extends unknown>(...lists: PaginatedList<Item, any, Filter, OrderBy>[]): PaginatedList<Item, string, Filter, OrderBy>;
99
+ static empty(): {
100
+ _getFirstCursor(): "first";
101
+ _getLastCursor(): "last";
102
+ _compare(orderBy: any, a: any, b: any): number;
103
+ _nextOrPrev(type: 'next' | 'prev', options: ImplQueryOptions<'next' | 'prev', string, any, any>): Promise<{
104
+ items: never[];
105
+ isFirst: boolean;
106
+ isLast: boolean;
107
+ cursor: "first";
108
+ }>;
109
+ getFirstCursor(): "first" | "last";
110
+ getLastCursor(): "first" | "last";
111
+ compare(orderBy: any, a: never, b: never): number;
112
+ nextOrPrev(type: "next" | "prev", options: QueryOptions<"next" | "prev", "first" | "last", any, any>): Promise<QueryResult<never, "first" | "last">>;
113
+ next({ after, ...rest }: QueryOptions<"next", "first" | "last", any, any>): Promise<QueryResult<never, "first" | "last">>;
114
+ prev({ before, ...rest }: QueryOptions<"prev", "first" | "last", any, any>): Promise<QueryResult<never, "first" | "last">>;
115
+ flatMap<Item2, Cursor2 extends string, Filter2 extends unknown, OrderBy2 extends unknown>(options: {
116
+ itemMapper: (itemEntry: {
117
+ item: never;
118
+ itemCursor: "first" | "last";
119
+ }, filter: Filter2, orderBy: OrderBy2) => {
120
+ item: Item2;
121
+ itemCursor: Cursor2;
122
+ }[];
123
+ compare: (orderBy: OrderBy2, a: Item2, b: Item2) => number;
124
+ newCursorFromOldCursor: (cursor: "first" | "last") => Cursor2;
125
+ oldCursorFromNewCursor: (cursor: Cursor2) => "first" | "last";
126
+ oldFilterFromNewFilter: (filter: Filter2) => any;
127
+ oldOrderByFromNewOrderBy: (orderBy: OrderBy2) => any;
128
+ estimateItemsToFetch: (options: {
129
+ filter: Filter2;
130
+ orderBy: OrderBy2;
131
+ limit: number;
132
+ }) => number;
133
+ }): PaginatedList<Item2, Cursor2, Filter2, OrderBy2>;
134
+ map<Item2_1, Filter2_1 extends unknown, OrderBy2_1 extends unknown>(options: {
135
+ itemMapper: (item: never) => Item2_1;
136
+ oldItemFromNewItem: (item: Item2_1) => never;
137
+ oldFilterFromNewFilter: (filter: Filter2_1) => any;
138
+ oldOrderByFromNewOrderBy: (orderBy: OrderBy2_1) => any;
139
+ }): PaginatedList<Item2_1, "first" | "last", Filter2_1, OrderBy2_1>;
140
+ filter<Filter2_2 extends unknown>(options: {
141
+ filter: (item: never, filter: Filter2_2) => boolean;
142
+ oldFilterFromNewFilter: (filter: Filter2_2) => any;
143
+ estimateItemsToFetch: (options: {
144
+ filter: Filter2_2;
145
+ orderBy: any;
146
+ limit: number;
147
+ }) => number;
148
+ }): PaginatedList<never, "first" | "last", Filter2_2, any>;
149
+ addFilter<AddedFilter extends unknown>(options: {
150
+ filter: (item: never, filter: any) => boolean;
151
+ estimateItemsToFetch: (options: {
152
+ filter: any;
153
+ orderBy: any;
154
+ limit: number;
155
+ }) => number;
156
+ }): PaginatedList<never, "first" | "last", any, any>;
157
+ };
158
+ }
159
+ declare class ArrayPaginatedList<Item> extends PaginatedList<Item, `${number}`, (item: Item) => boolean, (a: Item, b: Item) => number> {
160
+ private readonly array;
161
+ constructor(array: Item[]);
162
+ _getFirstCursor(): "0";
163
+ _getLastCursor(): `${number}`;
164
+ _compare(orderBy: (a: Item, b: Item) => number, a: Item, b: Item): number;
165
+ _nextOrPrev(type: 'next' | 'prev', options: ImplQueryOptions<'next' | 'prev', `${number}`, (item: Item) => boolean, (a: Item, b: Item) => number>): Promise<{
166
+ items: {
167
+ item: Item;
168
+ itemCursor: `${number}`;
169
+ }[];
170
+ isFirst: boolean;
171
+ isLast: boolean;
172
+ cursor: `${number}`;
173
+ }>;
174
+ }
175
+
176
+ export { ArrayPaginatedList, PaginatedList };
@@ -0,0 +1,176 @@
1
+ type QueryOptions<Type extends 'next' | 'prev', Cursor, Filter, OrderBy> = {
2
+ filter: Filter;
3
+ orderBy: OrderBy;
4
+ limit: number;
5
+ /**
6
+ * Whether the limit should be treated as an exact value, or an approximate value.
7
+ *
8
+ * If set to 'exact', less items will only be returned if the list item is the first or last item.
9
+ *
10
+ * If set to 'at-least' or 'approximate', the implementation may decide to return more items than the limit requested if doing so comes at no (or negligible) extra cost.
11
+ *
12
+ * If set to 'at-most' or 'approximate', the implementation may decide to return less items than the limit requested if requesting more items would come at a non-negligible extra cost. In this case, if limit > 0, the implementation must still make progress towards the end of the list and the returned cursor must be different from the one passed in.
13
+ *
14
+ * Defaults to 'exact'.
15
+ */
16
+ limitPrecision: 'exact' | 'at-least' | 'at-most' | 'approximate';
17
+ } & ([Type] extends [never] ? unknown : [Type] extends ['next'] ? {
18
+ after: Cursor;
19
+ } : [Type] extends ['prev'] ? {
20
+ before: Cursor;
21
+ } : {
22
+ cursor: Cursor;
23
+ });
24
+ type ImplQueryOptions<Type extends 'next' | 'prev', Cursor, Filter, OrderBy> = QueryOptions<Type, Cursor, Filter, OrderBy> & {
25
+ limitPrecision: 'approximate';
26
+ };
27
+ type QueryResult<Item, Cursor> = {
28
+ items: {
29
+ item: Item;
30
+ itemCursor: Cursor;
31
+ }[];
32
+ isFirst: boolean;
33
+ isLast: boolean;
34
+ cursor: Cursor;
35
+ };
36
+ type ImplQueryResult<Item, Cursor> = {
37
+ items: {
38
+ item: Item;
39
+ itemCursor: Cursor;
40
+ }[];
41
+ isFirst: boolean;
42
+ isLast: boolean;
43
+ cursor: Cursor;
44
+ };
45
+ declare abstract class PaginatedList<Item, Cursor extends string, Filter extends unknown, OrderBy extends unknown> {
46
+ protected abstract _getFirstCursor(): Cursor;
47
+ protected abstract _getLastCursor(): Cursor;
48
+ protected abstract _compare(orderBy: OrderBy, a: Item, b: Item): number;
49
+ protected abstract _nextOrPrev(type: 'next' | 'prev', options: ImplQueryOptions<'next' | 'prev', Cursor, Filter, OrderBy>): Promise<ImplQueryResult<Item, Cursor>>;
50
+ getFirstCursor(): Cursor;
51
+ getLastCursor(): Cursor;
52
+ compare(orderBy: OrderBy, a: Item, b: Item): number;
53
+ nextOrPrev(type: 'next' | 'prev', options: QueryOptions<'next' | 'prev', Cursor, Filter, OrderBy>): Promise<QueryResult<Item, Cursor>>;
54
+ next({ after, ...rest }: QueryOptions<'next', Cursor, Filter, OrderBy>): Promise<QueryResult<Item, Cursor>>;
55
+ prev({ before, ...rest }: QueryOptions<'prev', Cursor, Filter, OrderBy>): Promise<QueryResult<Item, Cursor>>;
56
+ flatMap<Item2, Cursor2 extends string, Filter2 extends unknown, OrderBy2 extends unknown>(options: {
57
+ itemMapper: (itemEntry: {
58
+ item: Item;
59
+ itemCursor: Cursor;
60
+ }, filter: Filter2, orderBy: OrderBy2) => {
61
+ item: Item2;
62
+ itemCursor: Cursor2;
63
+ }[];
64
+ compare: (orderBy: OrderBy2, a: Item2, b: Item2) => number;
65
+ newCursorFromOldCursor: (cursor: Cursor) => Cursor2;
66
+ oldCursorFromNewCursor: (cursor: Cursor2) => Cursor;
67
+ oldFilterFromNewFilter: (filter: Filter2) => Filter;
68
+ oldOrderByFromNewOrderBy: (orderBy: OrderBy2) => OrderBy;
69
+ estimateItemsToFetch: (options: {
70
+ filter: Filter2;
71
+ orderBy: OrderBy2;
72
+ limit: number;
73
+ }) => number;
74
+ }): PaginatedList<Item2, Cursor2, Filter2, OrderBy2>;
75
+ map<Item2, Filter2 extends unknown, OrderBy2 extends unknown>(options: {
76
+ itemMapper: (item: Item) => Item2;
77
+ oldItemFromNewItem: (item: Item2) => Item;
78
+ oldFilterFromNewFilter: (filter: Filter2) => Filter;
79
+ oldOrderByFromNewOrderBy: (orderBy: OrderBy2) => OrderBy;
80
+ }): PaginatedList<Item2, Cursor, Filter2, OrderBy2>;
81
+ filter<Filter2 extends unknown>(options: {
82
+ filter: (item: Item, filter: Filter2) => boolean;
83
+ oldFilterFromNewFilter: (filter: Filter2) => Filter;
84
+ estimateItemsToFetch: (options: {
85
+ filter: Filter2;
86
+ orderBy: OrderBy;
87
+ limit: number;
88
+ }) => number;
89
+ }): PaginatedList<Item, Cursor, Filter2, OrderBy>;
90
+ addFilter<AddedFilter extends unknown>(options: {
91
+ filter: (item: Item, filter: Filter & AddedFilter) => boolean;
92
+ estimateItemsToFetch: (options: {
93
+ filter: Filter & AddedFilter;
94
+ orderBy: OrderBy;
95
+ limit: number;
96
+ }) => number;
97
+ }): PaginatedList<Item, Cursor, Filter & AddedFilter, OrderBy>;
98
+ static merge<Item, Filter extends unknown, OrderBy extends unknown>(...lists: PaginatedList<Item, any, Filter, OrderBy>[]): PaginatedList<Item, string, Filter, OrderBy>;
99
+ static empty(): {
100
+ _getFirstCursor(): "first";
101
+ _getLastCursor(): "last";
102
+ _compare(orderBy: any, a: any, b: any): number;
103
+ _nextOrPrev(type: 'next' | 'prev', options: ImplQueryOptions<'next' | 'prev', string, any, any>): Promise<{
104
+ items: never[];
105
+ isFirst: boolean;
106
+ isLast: boolean;
107
+ cursor: "first";
108
+ }>;
109
+ getFirstCursor(): "first" | "last";
110
+ getLastCursor(): "first" | "last";
111
+ compare(orderBy: any, a: never, b: never): number;
112
+ nextOrPrev(type: "next" | "prev", options: QueryOptions<"next" | "prev", "first" | "last", any, any>): Promise<QueryResult<never, "first" | "last">>;
113
+ next({ after, ...rest }: QueryOptions<"next", "first" | "last", any, any>): Promise<QueryResult<never, "first" | "last">>;
114
+ prev({ before, ...rest }: QueryOptions<"prev", "first" | "last", any, any>): Promise<QueryResult<never, "first" | "last">>;
115
+ flatMap<Item2, Cursor2 extends string, Filter2 extends unknown, OrderBy2 extends unknown>(options: {
116
+ itemMapper: (itemEntry: {
117
+ item: never;
118
+ itemCursor: "first" | "last";
119
+ }, filter: Filter2, orderBy: OrderBy2) => {
120
+ item: Item2;
121
+ itemCursor: Cursor2;
122
+ }[];
123
+ compare: (orderBy: OrderBy2, a: Item2, b: Item2) => number;
124
+ newCursorFromOldCursor: (cursor: "first" | "last") => Cursor2;
125
+ oldCursorFromNewCursor: (cursor: Cursor2) => "first" | "last";
126
+ oldFilterFromNewFilter: (filter: Filter2) => any;
127
+ oldOrderByFromNewOrderBy: (orderBy: OrderBy2) => any;
128
+ estimateItemsToFetch: (options: {
129
+ filter: Filter2;
130
+ orderBy: OrderBy2;
131
+ limit: number;
132
+ }) => number;
133
+ }): PaginatedList<Item2, Cursor2, Filter2, OrderBy2>;
134
+ map<Item2_1, Filter2_1 extends unknown, OrderBy2_1 extends unknown>(options: {
135
+ itemMapper: (item: never) => Item2_1;
136
+ oldItemFromNewItem: (item: Item2_1) => never;
137
+ oldFilterFromNewFilter: (filter: Filter2_1) => any;
138
+ oldOrderByFromNewOrderBy: (orderBy: OrderBy2_1) => any;
139
+ }): PaginatedList<Item2_1, "first" | "last", Filter2_1, OrderBy2_1>;
140
+ filter<Filter2_2 extends unknown>(options: {
141
+ filter: (item: never, filter: Filter2_2) => boolean;
142
+ oldFilterFromNewFilter: (filter: Filter2_2) => any;
143
+ estimateItemsToFetch: (options: {
144
+ filter: Filter2_2;
145
+ orderBy: any;
146
+ limit: number;
147
+ }) => number;
148
+ }): PaginatedList<never, "first" | "last", Filter2_2, any>;
149
+ addFilter<AddedFilter extends unknown>(options: {
150
+ filter: (item: never, filter: any) => boolean;
151
+ estimateItemsToFetch: (options: {
152
+ filter: any;
153
+ orderBy: any;
154
+ limit: number;
155
+ }) => number;
156
+ }): PaginatedList<never, "first" | "last", any, any>;
157
+ };
158
+ }
159
+ declare class ArrayPaginatedList<Item> extends PaginatedList<Item, `${number}`, (item: Item) => boolean, (a: Item, b: Item) => number> {
160
+ private readonly array;
161
+ constructor(array: Item[]);
162
+ _getFirstCursor(): "0";
163
+ _getLastCursor(): `${number}`;
164
+ _compare(orderBy: (a: Item, b: Item) => number, a: Item, b: Item): number;
165
+ _nextOrPrev(type: 'next' | 'prev', options: ImplQueryOptions<'next' | 'prev', `${number}`, (item: Item) => boolean, (a: Item, b: Item) => number>): Promise<{
166
+ items: {
167
+ item: Item;
168
+ itemCursor: `${number}`;
169
+ }[];
170
+ isFirst: boolean;
171
+ isLast: boolean;
172
+ cursor: `${number}`;
173
+ }>;
174
+ }
175
+
176
+ export { ArrayPaginatedList, PaginatedList };