@auth0/auth0-spa-js 2.18.3 → 2.19.1

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 (61) hide show
  1. package/README.md +1 -1
  2. package/dist/auth0-spa-js.development.js +427 -370
  3. package/dist/auth0-spa-js.development.js.map +1 -1
  4. package/dist/auth0-spa-js.production.esm.js +1 -1
  5. package/dist/auth0-spa-js.production.esm.js.map +1 -1
  6. package/dist/auth0-spa-js.production.js +1 -1
  7. package/dist/auth0-spa-js.production.js.map +1 -1
  8. package/dist/auth0-spa-js.worker.development.js +132 -81
  9. package/dist/auth0-spa-js.worker.development.js.map +1 -1
  10. package/dist/auth0-spa-js.worker.production.js +1 -1
  11. package/dist/auth0-spa-js.worker.production.js.map +1 -1
  12. package/dist/lib/auth0-spa-js.cjs.js +449 -393
  13. package/dist/lib/auth0-spa-js.cjs.js.map +1 -1
  14. package/dist/typings/Auth0Client.d.ts +476 -439
  15. package/dist/typings/Auth0Client.utils.d.ts +90 -90
  16. package/dist/typings/MyAccountApiClient.d.ts +92 -92
  17. package/dist/typings/TokenExchange.d.ts +77 -77
  18. package/dist/typings/api.d.ts +33 -2
  19. package/dist/typings/cache/cache-localstorage.d.ts +7 -7
  20. package/dist/typings/cache/cache-manager.d.ts +69 -56
  21. package/dist/typings/cache/cache-memory.d.ts +4 -4
  22. package/dist/typings/cache/index.d.ts +4 -4
  23. package/dist/typings/cache/key-manifest.d.ts +12 -12
  24. package/dist/typings/cache/shared.d.ts +68 -68
  25. package/dist/typings/constants.d.ts +58 -58
  26. package/dist/typings/dpop/dpop.d.ts +17 -17
  27. package/dist/typings/dpop/storage.d.ts +27 -27
  28. package/dist/typings/dpop/utils.d.ts +15 -15
  29. package/dist/typings/errors.d.ts +96 -96
  30. package/dist/typings/fetcher.d.ts +54 -54
  31. package/dist/typings/global.d.ts +826 -819
  32. package/dist/typings/http.d.ts +11 -5
  33. package/dist/typings/index.d.ts +24 -24
  34. package/dist/typings/jwt.d.ts +21 -21
  35. package/dist/typings/lock.d.ts +32 -32
  36. package/dist/typings/mfa/MfaApiClient.d.ts +225 -225
  37. package/dist/typings/mfa/MfaContextManager.d.ts +79 -79
  38. package/dist/typings/mfa/constants.d.ts +23 -23
  39. package/dist/typings/mfa/errors.d.ts +117 -117
  40. package/dist/typings/mfa/index.d.ts +4 -4
  41. package/dist/typings/mfa/types.d.ts +181 -181
  42. package/dist/typings/mfa/utils.d.ts +23 -23
  43. package/dist/typings/promise-utils.d.ts +2 -2
  44. package/dist/typings/scope.d.ts +35 -35
  45. package/dist/typings/storage.d.ts +26 -26
  46. package/dist/typings/transaction-manager.d.ts +33 -33
  47. package/dist/typings/utils.d.ts +36 -36
  48. package/dist/typings/version.d.ts +2 -2
  49. package/dist/typings/worker/token.worker.d.ts +1 -1
  50. package/dist/typings/worker/worker.types.d.ts +27 -20
  51. package/dist/typings/worker/worker.utils.d.ts +13 -7
  52. package/package.json +2 -2
  53. package/src/Auth0Client.ts +73 -2
  54. package/src/api.ts +116 -2
  55. package/src/cache/cache-manager.ts +85 -9
  56. package/src/global.ts +8 -0
  57. package/src/http.ts +28 -21
  58. package/src/version.ts +1 -1
  59. package/src/worker/token.worker.ts +120 -5
  60. package/src/worker/worker.types.ts +17 -6
  61. package/src/worker/worker.utils.ts +18 -7
@@ -1,36 +1,36 @@
1
- import { AuthenticationResult, PopupConfigOptions } from './global';
2
- export declare const parseAuthenticationResult: (queryString: string) => AuthenticationResult;
3
- export declare const runIframe: (authorizeUrl: string, eventOrigin: string, timeoutInSeconds?: number) => Promise<AuthenticationResult>;
4
- export declare const openPopup: (url: string) => Window | null;
5
- export declare const runPopup: (config: PopupConfigOptions, eventOrigin: string) => Promise<AuthenticationResult>;
6
- export declare const getCrypto: () => Crypto;
7
- export declare const createRandomString: () => string;
8
- export declare const encode: (value: string) => string;
9
- export declare const decode: (value: string) => string;
10
- /**
11
- * Strips any property that is not present in ALLOWED_AUTH0CLIENT_PROPERTIES
12
- * @param auth0Client - The full auth0Client object
13
- * @param excludeEnv - If true, excludes the 'env' property from the result
14
- * @returns The stripped auth0Client object
15
- */
16
- export declare const stripAuth0Client: (auth0Client: any, excludeEnv?: boolean) => any;
17
- export declare const createQueryParams: ({ clientId: client_id, ...params }: any) => string;
18
- export declare const sha256: (s: string) => Promise<any>;
19
- export declare const urlDecodeB64: (input: string) => string;
20
- export declare const bufferToBase64UrlEncoded: (input: number[] | Uint8Array) => string;
21
- export declare const validateCrypto: () => void;
22
- /**
23
- * @ignore
24
- */
25
- export declare const getDomain: (domainUrl: string) => string;
26
- /**
27
- * @ignore
28
- */
29
- export declare const getTokenIssuer: (issuer: string | undefined, domainUrl: string) => string;
30
- export declare const parseNumber: (value: any) => number | undefined;
31
- /**
32
- * Ponyfill for `Object.fromEntries()`, which is not available until ES2020.
33
- *
34
- * When the target of this project reaches ES2020, this can be removed.
35
- */
36
- export declare const fromEntries: <T = any>(iterable: Iterable<[PropertyKey, T]>) => Record<PropertyKey, T>;
1
+ import { AuthenticationResult, PopupConfigOptions } from './global';
2
+ export declare const parseAuthenticationResult: (queryString: string) => AuthenticationResult;
3
+ export declare const runIframe: (authorizeUrl: string, eventOrigin: string, timeoutInSeconds?: number) => Promise<AuthenticationResult>;
4
+ export declare const openPopup: (url: string) => Window | null;
5
+ export declare const runPopup: (config: PopupConfigOptions, eventOrigin: string) => Promise<AuthenticationResult>;
6
+ export declare const getCrypto: () => Crypto;
7
+ export declare const createRandomString: () => string;
8
+ export declare const encode: (value: string) => string;
9
+ export declare const decode: (value: string) => string;
10
+ /**
11
+ * Strips any property that is not present in ALLOWED_AUTH0CLIENT_PROPERTIES
12
+ * @param auth0Client - The full auth0Client object
13
+ * @param excludeEnv - If true, excludes the 'env' property from the result
14
+ * @returns The stripped auth0Client object
15
+ */
16
+ export declare const stripAuth0Client: (auth0Client: any, excludeEnv?: boolean) => any;
17
+ export declare const createQueryParams: ({ clientId: client_id, ...params }: any) => string;
18
+ export declare const sha256: (s: string) => Promise<any>;
19
+ export declare const urlDecodeB64: (input: string) => string;
20
+ export declare const bufferToBase64UrlEncoded: (input: number[] | Uint8Array) => string;
21
+ export declare const validateCrypto: () => void;
22
+ /**
23
+ * @ignore
24
+ */
25
+ export declare const getDomain: (domainUrl: string) => string;
26
+ /**
27
+ * @ignore
28
+ */
29
+ export declare const getTokenIssuer: (issuer: string | undefined, domainUrl: string) => string;
30
+ export declare const parseNumber: (value: any) => number | undefined;
31
+ /**
32
+ * Ponyfill for `Object.fromEntries()`, which is not available until ES2020.
33
+ *
34
+ * When the target of this project reaches ES2020, this can be removed.
35
+ */
36
+ export declare const fromEntries: <T = any>(iterable: Iterable<[PropertyKey, T]>) => Record<PropertyKey, T>;
@@ -1,2 +1,2 @@
1
- declare const _default: "2.18.3";
2
- export default _default;
1
+ declare const _default: "2.19.1";
2
+ export default _default;
@@ -1 +1 @@
1
- export {};
1
+ export {};
@@ -1,20 +1,27 @@
1
- import { FetchOptions } from '../global';
2
- /**
3
- * @ts-ignore
4
- */
5
- export type WorkerInitMessage = {
6
- type: 'init';
7
- allowedBaseUrl: string;
8
- };
9
- export type WorkerRefreshTokenMessage = {
10
- timeout: number;
11
- fetchUrl: string;
12
- fetchOptions: FetchOptions;
13
- useFormData?: boolean;
14
- useMrrt?: boolean;
15
- auth: {
16
- audience: string;
17
- scope: string;
18
- };
19
- };
20
- export type WorkerMessage = WorkerInitMessage | WorkerRefreshTokenMessage;
1
+ import { FetchOptions } from '../global';
2
+ export type WorkerInitMessage = {
3
+ type: 'init';
4
+ allowedBaseUrl: string;
5
+ };
6
+ type WorkerTokenMessage = {
7
+ timeout: number;
8
+ fetchUrl: string;
9
+ fetchOptions: FetchOptions;
10
+ useFormData?: boolean;
11
+ auth: {
12
+ audience: string;
13
+ scope: string;
14
+ };
15
+ };
16
+ export type WorkerRefreshTokenMessage = WorkerTokenMessage & {
17
+ type: 'refresh';
18
+ useMrrt?: boolean;
19
+ };
20
+ export type WorkerRevokeTokenMessage = Omit<WorkerTokenMessage, 'auth'> & {
21
+ type: 'revoke';
22
+ auth: {
23
+ audience: string;
24
+ };
25
+ };
26
+ export type WorkerMessage = WorkerInitMessage | WorkerRefreshTokenMessage | WorkerRevokeTokenMessage;
27
+ export {};
@@ -1,7 +1,13 @@
1
- import { WorkerRefreshTokenMessage } from './worker.types';
2
- /**
3
- * Sends the specified message to the web worker
4
- * @param message The message to send
5
- * @param to The worker to send the message to
6
- */
7
- export declare const sendMessage: (message: WorkerRefreshTokenMessage, to: Worker) => Promise<unknown>;
1
+ import { WorkerRefreshTokenMessage, WorkerRevokeTokenMessage } from './worker.types';
2
+ /**
3
+ * Sends a message to a Web Worker and returns a Promise that resolves with
4
+ * the worker's response, or rejects if the worker replies with an error.
5
+ *
6
+ * Uses a {@link MessageChannel} so each call gets its own private reply port,
7
+ * making concurrent calls safe without shared state.
8
+ *
9
+ * @param message - The typed message to send (`refresh` or `revoke`).
10
+ * @param to - The target {@link Worker} instance.
11
+ * @returns A Promise that resolves with the worker's response payload.
12
+ */
13
+ export declare const sendMessage: <T = any>(message: WorkerRefreshTokenMessage | WorkerRevokeTokenMessage, to: Worker) => Promise<T>;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "name": "@auth0/auth0-spa-js",
4
4
  "description": "Auth0 SDK for Single Page Applications using Authorization Code Grant Flow with PKCE",
5
5
  "license": "MIT",
6
- "version": "2.18.3",
6
+ "version": "2.19.1",
7
7
  "main": "dist/lib/auth0-spa-js.cjs.js",
8
8
  "types": "dist/typings/index.d.ts",
9
9
  "module": "dist/auth0-spa-js.production.esm.js",
@@ -93,7 +93,7 @@
93
93
  "rollup-plugin-livereload": "^2.0.5",
94
94
  "rollup-plugin-sourcemaps": "^0.6.3",
95
95
  "rollup-plugin-terser": "^7.0.2",
96
- "rollup-plugin-typescript2": "^0.36.0",
96
+ "rollup-plugin-typescript2": "^0.37.0",
97
97
  "rollup-plugin-visualizer": "^5.7.1",
98
98
  "rollup-plugin-web-worker-loader": "~1.6.1",
99
99
  "serve": "^14.0.1",
@@ -17,7 +17,7 @@ import {
17
17
 
18
18
  import { getLockManager, type ILockManager } from './lock';
19
19
 
20
- import { oauthToken } from './api';
20
+ import { oauthToken, revokeToken } from './api';
21
21
 
22
22
  import { injectDefaultScopes, scopesToRequest } from './scope';
23
23
 
@@ -90,7 +90,8 @@ import {
90
90
  RedirectConnectAccountOptions,
91
91
  ResponseType,
92
92
  ClientAuthorizationParams,
93
- ClientConfiguration
93
+ ClientConfiguration,
94
+ RevokeRefreshTokenOptions
94
95
  } from './global';
95
96
 
96
97
  // @ts-ignore
@@ -1144,6 +1145,76 @@ export class Auth0Client {
1144
1145
  return url + federatedQuery;
1145
1146
  }
1146
1147
 
1148
+ /**
1149
+ * ```js
1150
+ * await auth0.revokeRefreshToken();
1151
+ * ```
1152
+ *
1153
+ * Revokes the refresh token using the `/oauth/revoke` endpoint.
1154
+ * This invalidates the refresh token so it can no longer be used to obtain new access tokens.
1155
+ *
1156
+ * The method works with both memory and localStorage cache modes:
1157
+ * - For memory storage with worker: The refresh token never leaves the worker thread,
1158
+ * maintaining security isolation
1159
+ * - For localStorage: The token is retrieved from cache and revoked
1160
+ *
1161
+ * If `useRefreshTokens` is disabled, this method does nothing.
1162
+ *
1163
+ * **Important:** This method revokes the refresh token for a single audience. If your
1164
+ * application requests tokens for multiple audiences, each audience may have its own
1165
+ * refresh token. To fully revoke all refresh tokens, call this method once per audience.
1166
+ * If you want to terminate the user's session entirely, use `logout()` instead.
1167
+ *
1168
+ * When using Multi-Resource Refresh Tokens (MRRT), a single refresh token may cover
1169
+ * multiple audiences. In that case, revoking it will affect all cache entries that
1170
+ * share the same token.
1171
+ *
1172
+ * @param options - Optional parameters to identify which refresh token to revoke.
1173
+ * Defaults to the audience configured in `authorizationParams`.
1174
+ *
1175
+ * @example
1176
+ * // Revoke the default refresh token
1177
+ * await auth0.revokeRefreshToken();
1178
+ *
1179
+ * @example
1180
+ * // Revoke refresh tokens for each audience individually
1181
+ * await auth0.revokeRefreshToken({ audience: 'https://api.example.com' });
1182
+ * await auth0.revokeRefreshToken({ audience: 'https://api2.example.com' });
1183
+ */
1184
+ public async revokeRefreshToken(options: RevokeRefreshTokenOptions = {}): Promise<void> {
1185
+ if (!this.options.useRefreshTokens) {
1186
+ return;
1187
+ }
1188
+
1189
+ const audience =
1190
+ options.audience || this.options.authorizationParams.audience;
1191
+
1192
+ const resolvedAudience = audience || DEFAULT_AUDIENCE;
1193
+
1194
+ // For the non-worker path the main-thread cache holds the refresh tokens.
1195
+ // For the worker path the worker holds its own RT store — the cache returns
1196
+ // [] and revokeToken sends a single message; the worker loops internally.
1197
+ const refreshTokens = await this.cacheManager.getRefreshTokensByAudience(
1198
+ resolvedAudience,
1199
+ this.options.clientId
1200
+ );
1201
+
1202
+ await revokeToken(
1203
+ {
1204
+ baseUrl: this.domainUrl,
1205
+ timeout: this.httpTimeoutMs,
1206
+ auth0Client: this.options.auth0Client,
1207
+ useFormData: this.options.useFormData,
1208
+ client_id: this.options.clientId,
1209
+ refreshTokens,
1210
+ audience: resolvedAudience,
1211
+ onRefreshTokenRevoked: refreshToken =>
1212
+ this.cacheManager.stripRefreshToken(refreshToken)
1213
+ },
1214
+ this.worker
1215
+ );
1216
+ }
1217
+
1147
1218
  /**
1148
1219
  * ```js
1149
1220
  * await auth0.logout(options);
package/src/api.ts CHANGED
@@ -1,7 +1,31 @@
1
1
  import { TokenEndpointOptions, TokenEndpointResponse } from './global';
2
- import { DEFAULT_AUTH0_CLIENT, DEFAULT_AUDIENCE } from './constants';
2
+ import {
3
+ DEFAULT_AUTH0_CLIENT,
4
+ DEFAULT_AUDIENCE,
5
+ DEFAULT_FETCH_TIMEOUT_MS
6
+ } from './constants';
7
+
8
+ /**
9
+ * @ignore
10
+ * Internal options for the revokeToken API call.
11
+ * Kept in api.ts (not global.ts) so it is not part of the public type surface.
12
+ */
13
+ interface RevokeTokenOptions {
14
+ baseUrl: string;
15
+ /** Maps directly to the OAuth `client_id` parameter. */
16
+ client_id: string;
17
+ /** Tokens to revoke. Empty for the worker path — the worker holds its own store. */
18
+ refreshTokens: string[];
19
+ audience?: string;
20
+ timeout?: number;
21
+ auth0Client?: any;
22
+ useFormData?: boolean;
23
+ onRefreshTokenRevoked?: (refreshToken: string) => Promise<void> | void;
24
+ }
3
25
  import * as dpopUtils from './dpop/utils';
4
- import { getJSON } from './http';
26
+ import { GenericError } from './errors';
27
+ import { getJSON, fetchWithTimeout } from './http';
28
+ import { sendMessage } from './worker/worker.utils';
5
29
  import { createQueryParams, stripAuth0Client } from './utils';
6
30
 
7
31
  export async function oauthToken(
@@ -59,3 +83,93 @@ export async function oauthToken(
59
83
  isDpopSupported ? dpop : undefined
60
84
  );
61
85
  }
86
+
87
+ /**
88
+ * Revokes refresh tokens using the /oauth/revoke endpoint.
89
+ *
90
+ * Mirrors the oauthToken pattern: the worker/non-worker dispatch lives here,
91
+ * keeping Auth0Client free of transport concerns.
92
+ *
93
+ * - Worker path: sends a single message; the worker holds its own RT store and
94
+ * loops internally. refreshTokens is empty (worker ignores it).
95
+ * - Non-worker path: loops over refreshTokens and issues one request per token.
96
+ *
97
+ * @throws {GenericError} If any revoke request fails
98
+ */
99
+ export async function revokeToken(
100
+ {
101
+ baseUrl,
102
+ timeout,
103
+ auth0Client,
104
+ useFormData,
105
+ refreshTokens,
106
+ audience,
107
+ client_id,
108
+ onRefreshTokenRevoked
109
+ }: RevokeTokenOptions,
110
+ worker?: Worker
111
+ ): Promise<void> {
112
+ const resolvedTimeout = timeout || DEFAULT_FETCH_TIMEOUT_MS;
113
+ // token_type_hint is a SHOULD per RFC 7009 §2.1; used in both paths below.
114
+ const token_type_hint = 'refresh_token' as const;
115
+ const fetchUrl = `${baseUrl}/oauth/revoke`;
116
+ const headers = {
117
+ 'Content-Type': useFormData
118
+ ? 'application/x-www-form-urlencoded'
119
+ : 'application/json',
120
+ 'Auth0-Client': btoa(
121
+ JSON.stringify(stripAuth0Client(auth0Client || DEFAULT_AUTH0_CLIENT))
122
+ )
123
+ };
124
+
125
+ if (worker) {
126
+ // Worker holds its own RT store and injects each token into the request.
127
+ // Send the base body (without token) so the worker can loop over its tokens.
128
+ const baseParams = { client_id, token_type_hint };
129
+ const body = useFormData
130
+ ? createQueryParams(baseParams)
131
+ : JSON.stringify(baseParams);
132
+
133
+ try {
134
+ return await sendMessage(
135
+ {
136
+ type: 'revoke',
137
+ timeout: resolvedTimeout,
138
+ fetchUrl,
139
+ fetchOptions: { method: 'POST', body, headers },
140
+ useFormData,
141
+ auth: { audience: audience ?? DEFAULT_AUDIENCE }
142
+ },
143
+ worker
144
+ );
145
+ } catch (e) {
146
+ throw new GenericError('revoke_error', (e as Error).message);
147
+ }
148
+ }
149
+
150
+ for (const refreshToken of refreshTokens) {
151
+ const params = { client_id, token_type_hint, token: refreshToken };
152
+ const body = useFormData
153
+ ? createQueryParams(params)
154
+ : JSON.stringify(params);
155
+
156
+ const response = await fetchWithTimeout(
157
+ fetchUrl,
158
+ { method: 'POST', body, headers },
159
+ resolvedTimeout
160
+ );
161
+
162
+ if (!response.ok) {
163
+ let error: string | undefined;
164
+ let errorDescription: string | undefined;
165
+ try {
166
+ ({ error, error_description: errorDescription } = JSON.parse(await response.text()));
167
+ } catch {
168
+ // body absent or not valid JSON
169
+ }
170
+ throw new GenericError(error || 'revoke_error', errorDescription || `HTTP error ${response.status}`);
171
+ }
172
+
173
+ await onRefreshTokenRevoked?.(refreshToken);
174
+ }
175
+ }
@@ -77,6 +77,11 @@ export class CacheManager {
77
77
  cacheKey.toKey()
78
78
  );
79
79
 
80
+ // Track the key where the entry was actually found, so that
81
+ // expiry-related writes (strip / remove) target the correct entry
82
+ // instead of creating a ghost entry under the lookup key.
83
+ let resolvedCacheKey = cacheKey;
84
+
80
85
  if (!wrappedEntry) {
81
86
  const keys = await this.getCacheKeys();
82
87
 
@@ -86,6 +91,7 @@ export class CacheManager {
86
91
 
87
92
  if (matchedKey) {
88
93
  wrappedEntry = await this.cache.get<WrappedCacheEntry>(matchedKey);
94
+ resolvedCacheKey = CacheKey.fromKey(matchedKey);
89
95
  }
90
96
 
91
97
  // To refresh using MRRT we need to send a request to the server
@@ -106,11 +112,11 @@ export class CacheManager {
106
112
 
107
113
  if (wrappedEntry.expiresAt - expiryAdjustmentSeconds < nowSeconds) {
108
114
  if (wrappedEntry.body.refresh_token) {
109
- return this.modifiedCachedEntry(wrappedEntry, cacheKey);
115
+ return this.modifiedCachedEntry(wrappedEntry, resolvedCacheKey);
110
116
  }
111
117
 
112
- await this.cache.remove(cacheKey.toKey());
113
- await this.keyManifest?.remove(cacheKey.toKey());
118
+ await this.cache.remove(resolvedCacheKey.toKey());
119
+ await this.keyManifest?.remove(resolvedCacheKey.toKey());
114
120
 
115
121
  return;
116
122
  }
@@ -121,18 +127,27 @@ export class CacheManager {
121
127
  private async modifiedCachedEntry(wrappedEntry: WrappedCacheEntry, cacheKey: CacheKey): Promise<Partial<CacheEntry>> {
122
128
  // We need to keep audience and scope in order to check them later when doing refresh
123
129
  // using MRRT. See getScopeToRequest method.
124
- wrappedEntry.body = {
130
+ //
131
+ // Build a new object instead of mutating wrappedEntry.body in-place,
132
+ // because InMemoryCache returns direct references — mutating would
133
+ // corrupt the original entry stored under a different (superset) key.
134
+ const strippedBody: Partial<CacheEntry> = {
125
135
  refresh_token: wrappedEntry.body.refresh_token,
126
136
  audience: wrappedEntry.body.audience,
127
137
  scope: wrappedEntry.body.scope,
128
138
  };
129
139
 
130
- await this.cache.set(cacheKey.toKey(), wrappedEntry);
140
+ const strippedEntry: WrappedCacheEntry = {
141
+ body: strippedBody,
142
+ expiresAt: wrappedEntry.expiresAt,
143
+ };
144
+
145
+ await this.cache.set(cacheKey.toKey(), strippedEntry);
131
146
 
132
147
  return {
133
- refresh_token: wrappedEntry.body.refresh_token,
134
- audience: wrappedEntry.body.audience,
135
- scope: wrappedEntry.body.scope,
148
+ refresh_token: strippedBody.refresh_token,
149
+ audience: strippedBody.audience,
150
+ scope: strippedBody.scope,
136
151
  };
137
152
  }
138
153
 
@@ -163,6 +178,23 @@ export class CacheManager {
163
178
  await this.cache.remove(cacheKey.toKey());
164
179
  }
165
180
 
181
+ async stripRefreshToken(refreshToken: string): Promise<void> {
182
+ const keys = await this.getCacheKeys();
183
+
184
+ /* c8 ignore next */
185
+ if (!keys) return;
186
+
187
+ // Find all cache entries that have this refresh token and strip only the refresh token,
188
+ // leaving the access token intact (it remains valid until it expires)
189
+ for (const key of keys) {
190
+ const entry = await this.cache.get<WrappedCacheEntry>(key);
191
+ if (entry?.body?.refresh_token === refreshToken) {
192
+ delete entry.body.refresh_token;
193
+ await this.cache.set(key, entry);
194
+ }
195
+ }
196
+ }
197
+
166
198
  async clear(clientId?: string): Promise<void> {
167
199
  const keys = await this.getCacheKeys();
168
200
 
@@ -261,7 +293,11 @@ export class CacheManager {
261
293
  const cachedEntry = await this.cache.get<WrappedCacheEntry>(key);
262
294
 
263
295
  if (cachedEntry?.body?.refresh_token) {
264
- return this.modifiedCachedEntry(cachedEntry, keyToMatch);
296
+ return {
297
+ refresh_token: cachedEntry.body.refresh_token,
298
+ audience: cachedEntry.body.audience,
299
+ scope: cachedEntry.body.scope,
300
+ };
265
301
  }
266
302
  }
267
303
  }
@@ -269,6 +305,46 @@ export class CacheManager {
269
305
  return undefined;
270
306
  }
271
307
 
308
+ /**
309
+ * Returns all distinct refresh tokens stored for a given audience and client.
310
+ *
311
+ * Multiple cache entries may exist for the same audience when different scope
312
+ * combinations were obtained via separate authorization flows, each potentially
313
+ * carrying a different refresh token. A Set is used to deduplicate tokens that
314
+ * are shared across entries (e.g. MRRT).
315
+ *
316
+ * @param audience The audience to look up
317
+ * @param clientId The client id to scope the lookup
318
+ */
319
+ async getRefreshTokensByAudience(
320
+ audience: string,
321
+ clientId: string
322
+ ): Promise<string[]> {
323
+ const keys = await this.getCacheKeys();
324
+
325
+ if (!keys) return [];
326
+
327
+ const tokens = new Set<string>();
328
+
329
+ for (const key of keys) {
330
+ const cacheKey = CacheKey.fromKey(key);
331
+
332
+ if (
333
+ cacheKey.prefix === CACHE_KEY_PREFIX &&
334
+ cacheKey.clientId === clientId &&
335
+ cacheKey.audience === audience
336
+ ) {
337
+ const entry = await this.cache.get<WrappedCacheEntry>(key);
338
+
339
+ if (entry?.body?.refresh_token) {
340
+ tokens.add(entry.body.refresh_token);
341
+ }
342
+ }
343
+ }
344
+
345
+ return Array.from(tokens);
346
+ }
347
+
272
348
  /**
273
349
  * Updates the refresh token in all cache entries that contain the old refresh token.
274
350
  *
package/src/global.ts CHANGED
@@ -903,6 +903,14 @@ export type GetTokenSilentlyVerboseResponse = Omit<
903
903
  'refresh_token'
904
904
  >;
905
905
 
906
+ /**
907
+ * Options for revoking a refresh token
908
+ */
909
+ export interface RevokeRefreshTokenOptions {
910
+ /** Audience to identify which refresh token to revoke. Omit for default audience. */
911
+ audience?: string;
912
+ }
913
+
906
914
  // MFA API types
907
915
  export type {
908
916
  Authenticator,
package/src/http.ts CHANGED
@@ -17,27 +17,16 @@ import { DPOP_NONCE_HEADER } from './dpop/utils';
17
17
 
18
18
  export const createAbortController = () => new AbortController();
19
19
 
20
- const dofetch = async (fetchUrl: string, fetchOptions: FetchOptions) => {
21
- const response = await fetch(fetchUrl, fetchOptions);
22
-
23
- return {
24
- ok: response.ok,
25
- json: await response.json(),
26
-
27
- /**
28
- * This is not needed, but do it anyway so the object shape is the
29
- * same as when using a Web Worker (which *does* need this, see
30
- * src/worker/token.worker.ts).
31
- */
32
- headers: fromEntries(response.headers)
33
- };
34
- };
35
-
36
- const fetchWithoutWorker = async (
20
+ /**
21
+ * Wraps a single `fetch` call with an AbortController-based timeout and
22
+ * returns the raw `Response`. Shared by the JSON token path and the revoke
23
+ * path to avoid duplicating abort/timeout orchestration.
24
+ */
25
+ export const fetchWithTimeout = (
37
26
  fetchUrl: string,
38
27
  fetchOptions: FetchOptions,
39
28
  timeout: number
40
- ) => {
29
+ ): Promise<Response> => {
41
30
  const controller = createAbortController();
42
31
  fetchOptions.signal = controller.signal;
43
32
 
@@ -45,9 +34,8 @@ const fetchWithoutWorker = async (
45
34
 
46
35
  // The promise will resolve with one of these two promises (the fetch or the timeout), whichever completes first.
47
36
  return Promise.race([
48
- dofetch(fetchUrl, fetchOptions),
49
-
50
- new Promise((_, reject) => {
37
+ fetch(fetchUrl, fetchOptions),
38
+ new Promise<never>((_, reject) => {
51
39
  timeoutId = setTimeout(() => {
52
40
  controller.abort();
53
41
  reject(new Error("Timeout when executing 'fetch'"));
@@ -58,6 +46,24 @@ const fetchWithoutWorker = async (
58
46
  });
59
47
  };
60
48
 
49
+ const fetchWithoutWorker = async (
50
+ fetchUrl: string,
51
+ fetchOptions: FetchOptions,
52
+ timeout: number
53
+ ) => {
54
+ const response = await fetchWithTimeout(fetchUrl, fetchOptions, timeout);
55
+ return {
56
+ ok: response.ok,
57
+ json: await response.json(),
58
+ /**
59
+ * This is not needed, but do it anyway so the object shape is the
60
+ * same as when using a Web Worker (which *does* need this, see
61
+ * src/worker/token.worker.ts).
62
+ */
63
+ headers: fromEntries(response.headers)
64
+ };
65
+ };
66
+
61
67
  const fetchWithWorker = async (
62
68
  fetchUrl: string,
63
69
  audience: string,
@@ -70,6 +76,7 @@ const fetchWithWorker = async (
70
76
  ) => {
71
77
  return sendMessage(
72
78
  {
79
+ type: 'refresh',
73
80
  auth: {
74
81
  audience,
75
82
  scope
package/src/version.ts CHANGED
@@ -1 +1 @@
1
- export default '2.18.3';
1
+ export default '2.19.1';