@logto/client 2.1.0 → 2.2.0

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.
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ var essentials = require('@silverhand/essentials');
4
+ var types = require('./types.cjs');
5
+
6
+ class ClientAdapterInstance {
7
+ /* END OF IMPLEMENTATION */
8
+ constructor(adapter) {
9
+ // eslint-disable-next-line @silverhand/fp/no-mutating-assign
10
+ Object.assign(this, adapter);
11
+ }
12
+ async setStorageItem(key, value) {
13
+ if (!value) {
14
+ await this.storage.removeItem(key);
15
+ return;
16
+ }
17
+ await this.storage.setItem(key, value);
18
+ }
19
+ /**
20
+ * Try to get the string value from the cache and parse as JSON.
21
+ * Return the parsed value if it is an object, return `undefined` otherwise.
22
+ *
23
+ * @param key The cache key to get value from.
24
+ */
25
+ async getCachedObject(key) {
26
+ const cached = await essentials.trySafe(async () => {
27
+ const data = await this.unstable_cache?.getItem(key);
28
+ // It's actually `unknown`
29
+ // eslint-disable-next-line no-restricted-syntax
30
+ return essentials.conditional(data && JSON.parse(data));
31
+ });
32
+ if (cached && typeof cached === 'object') {
33
+ // Trust cache for now
34
+ // eslint-disable-next-line no-restricted-syntax
35
+ return cached;
36
+ }
37
+ }
38
+ /**
39
+ * Try to get the value from the cache first, if it doesn't exist in cache,
40
+ * run the getter function and store the result into cache.
41
+ *
42
+ * @param key The cache key to get value from.
43
+ */
44
+ async getWithCache(key, getter) {
45
+ const cached = await this.getCachedObject(key);
46
+ if (cached) {
47
+ return cached;
48
+ }
49
+ const result = await getter();
50
+ await this.unstable_cache?.setItem(key, JSON.stringify(result));
51
+ return result;
52
+ }
53
+ }
54
+
55
+ Object.defineProperty(exports, 'CacheKey', {
56
+ enumerable: true,
57
+ get: function () { return types.CacheKey; }
58
+ });
59
+ Object.defineProperty(exports, 'PersistKey', {
60
+ enumerable: true,
61
+ get: function () { return types.PersistKey; }
62
+ });
63
+ exports.ClientAdapterInstance = ClientAdapterInstance;
@@ -0,0 +1,29 @@
1
+ import { type Requester } from '@logto/js';
2
+ import { type Nullable } from '@silverhand/essentials';
3
+ import { type CacheKey, type Navigate, type PersistKey, type Storage, type StorageKey, type ClientAdapter, type InferStorageKey } from './types.js';
4
+ export declare class ClientAdapterInstance implements ClientAdapter {
5
+ requester: Requester;
6
+ storage: Storage<StorageKey | PersistKey>;
7
+ unstable_cache?: Storage<CacheKey> | undefined;
8
+ navigate: Navigate;
9
+ generateState: () => string;
10
+ generateCodeVerifier: () => string;
11
+ generateCodeChallenge: (codeVerifier: string) => Promise<string>;
12
+ constructor(adapter: ClientAdapter);
13
+ setStorageItem(key: InferStorageKey<typeof this.storage>, value: Nullable<string>): Promise<void>;
14
+ /**
15
+ * Try to get the string value from the cache and parse as JSON.
16
+ * Return the parsed value if it is an object, return `undefined` otherwise.
17
+ *
18
+ * @param key The cache key to get value from.
19
+ */
20
+ getCachedObject<T>(key: CacheKey): Promise<T | undefined>;
21
+ /**
22
+ * Try to get the value from the cache first, if it doesn't exist in cache,
23
+ * run the getter function and store the result into cache.
24
+ *
25
+ * @param key The cache key to get value from.
26
+ */
27
+ getWithCache<T>(key: CacheKey, getter: () => Promise<T>): Promise<T>;
28
+ }
29
+ export * from './types.js';
@@ -0,0 +1,53 @@
1
+ import { trySafe, conditional } from '@silverhand/essentials';
2
+ export { CacheKey, PersistKey } from './types.js';
3
+
4
+ class ClientAdapterInstance {
5
+ /* END OF IMPLEMENTATION */
6
+ constructor(adapter) {
7
+ // eslint-disable-next-line @silverhand/fp/no-mutating-assign
8
+ Object.assign(this, adapter);
9
+ }
10
+ async setStorageItem(key, value) {
11
+ if (!value) {
12
+ await this.storage.removeItem(key);
13
+ return;
14
+ }
15
+ await this.storage.setItem(key, value);
16
+ }
17
+ /**
18
+ * Try to get the string value from the cache and parse as JSON.
19
+ * Return the parsed value if it is an object, return `undefined` otherwise.
20
+ *
21
+ * @param key The cache key to get value from.
22
+ */
23
+ async getCachedObject(key) {
24
+ const cached = await trySafe(async () => {
25
+ const data = await this.unstable_cache?.getItem(key);
26
+ // It's actually `unknown`
27
+ // eslint-disable-next-line no-restricted-syntax
28
+ return conditional(data && JSON.parse(data));
29
+ });
30
+ if (cached && typeof cached === 'object') {
31
+ // Trust cache for now
32
+ // eslint-disable-next-line no-restricted-syntax
33
+ return cached;
34
+ }
35
+ }
36
+ /**
37
+ * Try to get the value from the cache first, if it doesn't exist in cache,
38
+ * run the getter function and store the result into cache.
39
+ *
40
+ * @param key The cache key to get value from.
41
+ */
42
+ async getWithCache(key, getter) {
43
+ const cached = await this.getCachedObject(key);
44
+ if (cached) {
45
+ return cached;
46
+ }
47
+ const result = await getter();
48
+ await this.unstable_cache?.setItem(key, JSON.stringify(result));
49
+ return result;
50
+ }
51
+ }
52
+
53
+ export { ClientAdapterInstance };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ 'use strict';
2
+
3
+ exports.PersistKey = void 0;
4
+ (function (PersistKey) {
5
+ PersistKey["IdToken"] = "idToken";
6
+ PersistKey["RefreshToken"] = "refreshToken";
7
+ PersistKey["AccessToken"] = "accessToken";
8
+ PersistKey["SignInSession"] = "signInSession";
9
+ })(exports.PersistKey || (exports.PersistKey = {}));
10
+ exports.CacheKey = void 0;
11
+ (function (CacheKey) {
12
+ /**
13
+ * OpenID Configuration endpoint response.
14
+ *
15
+ * @see {@link https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse | OpenID Connect Discovery 1.0}
16
+ */
17
+ CacheKey["OpenidConfig"] = "openidConfiguration";
18
+ /**
19
+ * The content of OpenID Provider's `jwks_uri` (JSON Web Key Set).
20
+ *
21
+ * @see {@link https://openid.net/specs/openid-connect-discovery-1_0-21.html#ProviderMetadata | OpenID Connect Discovery 1.0}
22
+ */
23
+ CacheKey["Jwks"] = "jwks";
24
+ })(exports.CacheKey || (exports.CacheKey = {}));
@@ -0,0 +1,45 @@
1
+ import type { Requester } from '@logto/js';
2
+ import type { Nullable } from '@silverhand/essentials';
3
+ /** @deprecated Use {@link PersistKey} instead. */
4
+ export type StorageKey = 'idToken' | 'refreshToken' | 'accessToken' | 'signInSession';
5
+ export declare enum PersistKey {
6
+ IdToken = "idToken",
7
+ RefreshToken = "refreshToken",
8
+ AccessToken = "accessToken",
9
+ SignInSession = "signInSession"
10
+ }
11
+ export declare enum CacheKey {
12
+ /**
13
+ * OpenID Configuration endpoint response.
14
+ *
15
+ * @see {@link https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse | OpenID Connect Discovery 1.0}
16
+ */
17
+ OpenidConfig = "openidConfiguration",
18
+ /**
19
+ * The content of OpenID Provider's `jwks_uri` (JSON Web Key Set).
20
+ *
21
+ * @see {@link https://openid.net/specs/openid-connect-discovery-1_0-21.html#ProviderMetadata | OpenID Connect Discovery 1.0}
22
+ */
23
+ Jwks = "jwks"
24
+ }
25
+ export type Storage<Keys extends string> = {
26
+ getItem(key: Keys): Promise<Nullable<string>>;
27
+ setItem(key: Keys, value: string): Promise<void>;
28
+ removeItem(key: Keys): Promise<void>;
29
+ };
30
+ export type InferStorageKey<S> = S extends Storage<infer Key> ? Key : never;
31
+ export type Navigate = (url: string) => void;
32
+ export type ClientAdapter = {
33
+ requester: Requester;
34
+ storage: Storage<StorageKey | PersistKey>;
35
+ /**
36
+ * An optional storage for caching well-known data.
37
+ *
38
+ * @see {@link CacheKey}
39
+ */
40
+ unstable_cache?: Storage<CacheKey>;
41
+ navigate: Navigate;
42
+ generateState: () => string;
43
+ generateCodeVerifier: () => string;
44
+ generateCodeChallenge: (codeVerifier: string) => Promise<string>;
45
+ };
@@ -0,0 +1,24 @@
1
+ var PersistKey;
2
+ (function (PersistKey) {
3
+ PersistKey["IdToken"] = "idToken";
4
+ PersistKey["RefreshToken"] = "refreshToken";
5
+ PersistKey["AccessToken"] = "accessToken";
6
+ PersistKey["SignInSession"] = "signInSession";
7
+ })(PersistKey || (PersistKey = {}));
8
+ var CacheKey;
9
+ (function (CacheKey) {
10
+ /**
11
+ * OpenID Configuration endpoint response.
12
+ *
13
+ * @see {@link https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse | OpenID Connect Discovery 1.0}
14
+ */
15
+ CacheKey["OpenidConfig"] = "openidConfiguration";
16
+ /**
17
+ * The content of OpenID Provider's `jwks_uri` (JSON Web Key Set).
18
+ *
19
+ * @see {@link https://openid.net/specs/openid-connect-discovery-1_0-21.html#ProviderMetadata | OpenID Connect Discovery 1.0}
20
+ */
21
+ CacheKey["Jwks"] = "jwks";
22
+ })(CacheKey || (CacheKey = {}));
23
+
24
+ export { CacheKey, PersistKey };
@@ -0,0 +1 @@
1
+ export {};
package/lib/index.cjs CHANGED
@@ -4,23 +4,27 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var js = require('@logto/js');
6
6
  var jose = require('jose');
7
+ var index$1 = require('./adapter/index.cjs');
7
8
  var errors = require('./errors.cjs');
9
+ var remoteJwkSet = require('./remote-jwk-set.cjs');
8
10
  var index = require('./types/index.cjs');
9
- var index$1 = require('./utils/index.cjs');
11
+ var index$2 = require('./utils/index.cjs');
12
+ var memoize = require('./utils/memoize.cjs');
10
13
  var once = require('./utils/once.cjs');
14
+ var types = require('./adapter/types.cjs');
11
15
  var requester = require('./utils/requester.cjs');
12
16
 
13
17
  class LogtoClient {
14
18
  constructor(logtoConfig, adapter) {
15
- this.getOidcConfig = once.once(this._getOidcConfig);
16
- this.getJwtVerifyGetKey = once.once(this._getJwtVerifyGetKey);
19
+ this.getOidcConfig = memoize.memoize(this.#getOidcConfig);
20
+ this.getJwtVerifyGetKey = once.once(this.#getJwtVerifyGetKey);
17
21
  this.accessTokenMap = new Map();
18
22
  this.logtoConfig = {
19
23
  ...logtoConfig,
20
24
  prompt: logtoConfig.prompt ?? js.Prompt.Consent,
21
25
  scopes: js.withDefaultScopes(logtoConfig.scopes).split(' '),
22
26
  };
23
- this.adapter = adapter;
27
+ this.adapter = new index$1.ClientAdapterInstance(adapter);
24
28
  void this.loadAccessTokenMap();
25
29
  }
26
30
  async isAuthenticated() {
@@ -36,7 +40,7 @@ class LogtoClient {
36
40
  if (!(await this.getIdToken())) {
37
41
  throw new errors.LogtoClientError('not_authenticated');
38
42
  }
39
- const accessTokenKey = index$1.buildAccessTokenKey(resource);
43
+ const accessTokenKey = index$2.buildAccessTokenKey(resource);
40
44
  const accessToken = this.accessTokenMap.get(accessTokenKey);
41
45
  if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
42
46
  return accessToken.token;
@@ -156,34 +160,21 @@ class LogtoClient {
156
160
  }
157
161
  return item;
158
162
  }
159
- async setSignInSession(logtoSignInSessionItem) {
160
- if (!logtoSignInSessionItem) {
161
- await this.adapter.storage.removeItem('signInSession');
162
- return;
163
- }
164
- const jsonItem = JSON.stringify(logtoSignInSessionItem);
165
- await this.adapter.storage.setItem('signInSession', jsonItem);
163
+ async setSignInSession(value) {
164
+ return this.adapter.setStorageItem(types.PersistKey.SignInSession, value && JSON.stringify(value));
166
165
  }
167
- async setIdToken(idToken) {
168
- if (!idToken) {
169
- await this.adapter.storage.removeItem('idToken');
170
- return;
171
- }
172
- await this.adapter.storage.setItem('idToken', idToken);
166
+ async setIdToken(value) {
167
+ return this.adapter.setStorageItem(types.PersistKey.IdToken, value);
173
168
  }
174
- async setRefreshToken(refreshToken) {
175
- if (!refreshToken) {
176
- await this.adapter.storage.removeItem('refreshToken');
177
- return;
178
- }
179
- await this.adapter.storage.setItem('refreshToken', refreshToken);
169
+ async setRefreshToken(value) {
170
+ return this.adapter.setStorageItem(types.PersistKey.RefreshToken, value);
180
171
  }
181
172
  async getAccessTokenByRefreshToken(resource) {
182
173
  const currentRefreshToken = await this.getRefreshToken();
183
174
  if (!currentRefreshToken) {
184
175
  throw new errors.LogtoClientError('not_authenticated');
185
176
  }
186
- const accessTokenKey = index$1.buildAccessTokenKey(resource);
177
+ const accessTokenKey = index$2.buildAccessTokenKey(resource);
187
178
  const { appId: clientId } = this.logtoConfig;
188
179
  const { tokenEndpoint } = await this.getOidcConfig();
189
180
  const { accessToken, refreshToken, idToken, scope, expiresIn } = await js.fetchTokenByRefreshToken({
@@ -205,15 +196,6 @@ class LogtoClient {
205
196
  }
206
197
  return accessToken;
207
198
  }
208
- async _getOidcConfig() {
209
- const { endpoint } = this.logtoConfig;
210
- const discoveryEndpoint = index$1.getDiscoveryEndpoint(endpoint);
211
- return js.fetchOidcConfig(discoveryEndpoint, this.adapter.requester);
212
- }
213
- async _getJwtVerifyGetKey() {
214
- const { jwksUri } = await this.getOidcConfig();
215
- return jose.createRemoteJWKSet(new URL(jwksUri));
216
- }
217
199
  async verifyIdToken(idToken) {
218
200
  const { appId } = this.logtoConfig;
219
201
  const { issuer } = await this.getOidcConfig();
@@ -224,7 +206,7 @@ class LogtoClient {
224
206
  await this.setRefreshToken(refreshToken ?? null);
225
207
  await this.setIdToken(idToken);
226
208
  // NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
227
- const accessTokenKey = index$1.buildAccessTokenKey();
209
+ const accessTokenKey = index$2.buildAccessTokenKey();
228
210
  const expiresAt = Date.now() / 1000 + expiresIn;
229
211
  this.accessTokenMap.set(accessTokenKey, { token: accessToken, scope, expiresAt });
230
212
  await this.saveAccessTokenMap();
@@ -256,6 +238,19 @@ class LogtoClient {
256
238
  console.warn(error);
257
239
  }
258
240
  }
241
+ async #getOidcConfig() {
242
+ return this.adapter.getWithCache(types.CacheKey.OpenidConfig, async () => {
243
+ return js.fetchOidcConfig(index$2.getDiscoveryEndpoint(this.logtoConfig.endpoint), this.adapter.requester);
244
+ });
245
+ }
246
+ async #getJwtVerifyGetKey() {
247
+ const { jwksUri } = await this.getOidcConfig();
248
+ if (!this.adapter.unstable_cache) {
249
+ return jose.createRemoteJWKSet(new URL(jwksUri));
250
+ }
251
+ const cachedJwkSet = new remoteJwkSet.CachedRemoteJwkSet(new URL(jwksUri), this.adapter);
252
+ return async (...args) => cachedJwkSet.getKey(...args);
253
+ }
259
254
  }
260
255
 
261
256
  Object.defineProperty(exports, 'LogtoError', {
@@ -285,5 +280,13 @@ Object.defineProperty(exports, 'UserScope', {
285
280
  exports.LogtoClientError = errors.LogtoClientError;
286
281
  exports.isLogtoAccessTokenMap = index.isLogtoAccessTokenMap;
287
282
  exports.isLogtoSignInSessionItem = index.isLogtoSignInSessionItem;
283
+ Object.defineProperty(exports, 'CacheKey', {
284
+ enumerable: true,
285
+ get: function () { return types.CacheKey; }
286
+ });
287
+ Object.defineProperty(exports, 'PersistKey', {
288
+ enumerable: true,
289
+ get: function () { return types.PersistKey; }
290
+ });
288
291
  exports.createRequester = requester.createRequester;
289
292
  exports.default = LogtoClient;
@@ -0,0 +1 @@
1
+ export {};
package/lib/index.d.ts CHANGED
@@ -1,18 +1,29 @@
1
1
  import { type IdTokenClaims, type UserInfoResponse, type InteractionMode, type AccessTokenClaims } from '@logto/js';
2
- import type { Nullable } from '@silverhand/essentials';
3
- import type { ClientAdapter } from './adapter.js';
2
+ import { type Nullable } from '@silverhand/essentials';
3
+ import { type JWTVerifyGetKey } from 'jose';
4
+ import { ClientAdapterInstance, type ClientAdapter } from './adapter/index.js';
4
5
  import type { AccessToken, LogtoConfig, LogtoSignInSessionItem } from './types/index.js';
5
6
  export type { IdTokenClaims, LogtoErrorCode, UserInfoResponse, InteractionMode } from '@logto/js';
6
7
  export { LogtoError, OidcError, Prompt, LogtoRequestError, ReservedScope, UserScope, } from '@logto/js';
7
8
  export * from './errors.js';
8
- export type { Storage, StorageKey, ClientAdapter } from './adapter.js';
9
+ export type { Storage, StorageKey, ClientAdapter } from './adapter/index.js';
10
+ export { PersistKey, CacheKey } from './adapter/index.js';
9
11
  export { createRequester } from './utils/index.js';
10
12
  export * from './types/index.js';
11
13
  export default class LogtoClient {
14
+ #private;
12
15
  protected readonly logtoConfig: LogtoConfig;
13
- protected readonly getOidcConfig: typeof this._getOidcConfig;
14
- protected readonly getJwtVerifyGetKey: (...args: unknown[]) => Promise<(protectedHeader?: import("jose").JWSHeaderParameters | undefined, token?: import("jose").FlattenedJWSInput | undefined) => Promise<import("jose").KeyLike>>;
15
- protected readonly adapter: ClientAdapter;
16
+ protected readonly getOidcConfig: (this: unknown) => Promise<import("@silverhand/essentials").KeysToCamelCase<{
17
+ authorization_endpoint: string;
18
+ token_endpoint: string;
19
+ userinfo_endpoint: string;
20
+ end_session_endpoint: string;
21
+ revocation_endpoint: string;
22
+ jwks_uri: string;
23
+ issuer: string;
24
+ }>>;
25
+ protected readonly getJwtVerifyGetKey: (...args: unknown[]) => Promise<JWTVerifyGetKey>;
26
+ protected readonly adapter: ClientAdapterInstance;
16
27
  protected readonly accessTokenMap: Map<string, AccessToken>;
17
28
  constructor(logtoConfig: LogtoConfig, adapter: ClientAdapter);
18
29
  isAuthenticated(): Promise<boolean>;
@@ -27,12 +38,10 @@ export default class LogtoClient {
27
38
  handleSignInCallback(callbackUri: string): Promise<void>;
28
39
  signOut(postLogoutRedirectUri?: string): Promise<void>;
29
40
  protected getSignInSession(): Promise<Nullable<LogtoSignInSessionItem>>;
30
- protected setSignInSession(logtoSignInSessionItem: Nullable<LogtoSignInSessionItem>): Promise<void>;
41
+ protected setSignInSession(value: Nullable<LogtoSignInSessionItem>): Promise<void>;
31
42
  private setIdToken;
32
43
  private setRefreshToken;
33
44
  private getAccessTokenByRefreshToken;
34
- private _getOidcConfig;
35
- private _getJwtVerifyGetKey;
36
45
  private verifyIdToken;
37
46
  private saveCodeToken;
38
47
  private saveAccessTokenMap;
package/lib/index.js CHANGED
@@ -1,23 +1,27 @@
1
- import { Prompt, withDefaultScopes, decodeIdToken, decodeAccessToken, fetchUserInfo, generateSignInUri, verifyAndParseCodeFromCallbackUri, fetchTokenByAuthorizationCode, revoke, generateSignOutUri, fetchTokenByRefreshToken, fetchOidcConfig, verifyIdToken } from '@logto/js';
1
+ import { Prompt, withDefaultScopes, decodeIdToken, decodeAccessToken, fetchUserInfo, generateSignInUri, verifyAndParseCodeFromCallbackUri, fetchTokenByAuthorizationCode, revoke, generateSignOutUri, fetchTokenByRefreshToken, verifyIdToken, fetchOidcConfig } from '@logto/js';
2
2
  export { LogtoError, LogtoRequestError, OidcError, Prompt, ReservedScope, UserScope } from '@logto/js';
3
3
  import { createRemoteJWKSet } from 'jose';
4
+ import { ClientAdapterInstance } from './adapter/index.js';
4
5
  import { LogtoClientError } from './errors.js';
6
+ import { CachedRemoteJwkSet } from './remote-jwk-set.js';
5
7
  import { isLogtoSignInSessionItem, isLogtoAccessTokenMap } from './types/index.js';
6
8
  import { buildAccessTokenKey, getDiscoveryEndpoint } from './utils/index.js';
9
+ import { memoize } from './utils/memoize.js';
7
10
  import { once } from './utils/once.js';
11
+ import { PersistKey, CacheKey } from './adapter/types.js';
8
12
  export { createRequester } from './utils/requester.js';
9
13
 
10
14
  class LogtoClient {
11
15
  constructor(logtoConfig, adapter) {
12
- this.getOidcConfig = once(this._getOidcConfig);
13
- this.getJwtVerifyGetKey = once(this._getJwtVerifyGetKey);
16
+ this.getOidcConfig = memoize(this.#getOidcConfig);
17
+ this.getJwtVerifyGetKey = once(this.#getJwtVerifyGetKey);
14
18
  this.accessTokenMap = new Map();
15
19
  this.logtoConfig = {
16
20
  ...logtoConfig,
17
21
  prompt: logtoConfig.prompt ?? Prompt.Consent,
18
22
  scopes: withDefaultScopes(logtoConfig.scopes).split(' '),
19
23
  };
20
- this.adapter = adapter;
24
+ this.adapter = new ClientAdapterInstance(adapter);
21
25
  void this.loadAccessTokenMap();
22
26
  }
23
27
  async isAuthenticated() {
@@ -153,27 +157,14 @@ class LogtoClient {
153
157
  }
154
158
  return item;
155
159
  }
156
- async setSignInSession(logtoSignInSessionItem) {
157
- if (!logtoSignInSessionItem) {
158
- await this.adapter.storage.removeItem('signInSession');
159
- return;
160
- }
161
- const jsonItem = JSON.stringify(logtoSignInSessionItem);
162
- await this.adapter.storage.setItem('signInSession', jsonItem);
160
+ async setSignInSession(value) {
161
+ return this.adapter.setStorageItem(PersistKey.SignInSession, value && JSON.stringify(value));
163
162
  }
164
- async setIdToken(idToken) {
165
- if (!idToken) {
166
- await this.adapter.storage.removeItem('idToken');
167
- return;
168
- }
169
- await this.adapter.storage.setItem('idToken', idToken);
163
+ async setIdToken(value) {
164
+ return this.adapter.setStorageItem(PersistKey.IdToken, value);
170
165
  }
171
- async setRefreshToken(refreshToken) {
172
- if (!refreshToken) {
173
- await this.adapter.storage.removeItem('refreshToken');
174
- return;
175
- }
176
- await this.adapter.storage.setItem('refreshToken', refreshToken);
166
+ async setRefreshToken(value) {
167
+ return this.adapter.setStorageItem(PersistKey.RefreshToken, value);
177
168
  }
178
169
  async getAccessTokenByRefreshToken(resource) {
179
170
  const currentRefreshToken = await this.getRefreshToken();
@@ -202,15 +193,6 @@ class LogtoClient {
202
193
  }
203
194
  return accessToken;
204
195
  }
205
- async _getOidcConfig() {
206
- const { endpoint } = this.logtoConfig;
207
- const discoveryEndpoint = getDiscoveryEndpoint(endpoint);
208
- return fetchOidcConfig(discoveryEndpoint, this.adapter.requester);
209
- }
210
- async _getJwtVerifyGetKey() {
211
- const { jwksUri } = await this.getOidcConfig();
212
- return createRemoteJWKSet(new URL(jwksUri));
213
- }
214
196
  async verifyIdToken(idToken) {
215
197
  const { appId } = this.logtoConfig;
216
198
  const { issuer } = await this.getOidcConfig();
@@ -253,6 +235,19 @@ class LogtoClient {
253
235
  console.warn(error);
254
236
  }
255
237
  }
238
+ async #getOidcConfig() {
239
+ return this.adapter.getWithCache(CacheKey.OpenidConfig, async () => {
240
+ return fetchOidcConfig(getDiscoveryEndpoint(this.logtoConfig.endpoint), this.adapter.requester);
241
+ });
242
+ }
243
+ async #getJwtVerifyGetKey() {
244
+ const { jwksUri } = await this.getOidcConfig();
245
+ if (!this.adapter.unstable_cache) {
246
+ return createRemoteJWKSet(new URL(jwksUri));
247
+ }
248
+ const cachedJwkSet = new CachedRemoteJwkSet(new URL(jwksUri), this.adapter);
249
+ return async (...args) => cachedJwkSet.getKey(...args);
250
+ }
256
251
  }
257
252
 
258
- export { LogtoClientError, LogtoClient as default, isLogtoAccessTokenMap, isLogtoSignInSessionItem };
253
+ export { CacheKey, LogtoClientError, PersistKey, LogtoClient as default, isLogtoAccessTokenMap, isLogtoSignInSessionItem };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
package/lib/mock.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  /// <reference types="jest" />
2
2
  import { Prompt } from '@logto/js';
3
- import type { Nullable } from '@silverhand/essentials';
4
- import type { Storage } from './adapter.js';
3
+ import { type Nullable } from '@silverhand/essentials';
4
+ import type { Storage } from './adapter/index.js';
5
5
  import type { AccessToken, LogtoConfig, LogtoSignInSessionItem } from './index.js';
6
6
  import LogtoClient from './index.js';
7
7
  export declare const appId = "app_id_value";
8
8
  export declare const endpoint = "https://logto.dev";
9
- export declare class MockedStorage implements Storage {
9
+ export declare class MockedStorage implements Storage<string> {
10
10
  private storage;
11
11
  constructor(values?: Record<string, string>);
12
12
  getItem(key: string): Promise<string | null>;
@@ -33,6 +33,15 @@ export declare const accessToken = "access_token_value";
33
33
  export declare const refreshToken = "new_refresh_token_value";
34
34
  export declare const idToken = "id_token_value";
35
35
  export declare const currentUnixTimeStamp: number;
36
+ export declare const mockFetchOidcConfig: (delay?: number) => jest.Mock<Promise<{
37
+ authorizationEndpoint: string;
38
+ tokenEndpoint: string;
39
+ userinfoEndpoint: string;
40
+ endSessionEndpoint: string;
41
+ revocationEndpoint: string;
42
+ jwksUri: string;
43
+ issuer: string;
44
+ }>, [], any>;
36
45
  export declare const fetchOidcConfig: jest.Mock<Promise<{
37
46
  authorizationEndpoint: string;
38
47
  tokenEndpoint: string;
@@ -48,19 +57,30 @@ export declare const navigate: jest.Mock<any, any, any>;
48
57
  export declare const generateCodeChallenge: jest.Mock<Promise<string>, [], any>;
49
58
  export declare const generateCodeVerifier: jest.Mock<string, [], any>;
50
59
  export declare const generateState: jest.Mock<string, [], any>;
51
- export declare const createAdapters: () => {
60
+ export declare const createAdapters: (withCache?: boolean) => {
52
61
  requester: jest.Mock<any, any, any>;
53
62
  storage: MockedStorage;
63
+ unstable_cache: import("@silverhand/essentials").Optional<MockedStorage>;
54
64
  navigate: jest.Mock<any, any, any>;
55
65
  generateCodeChallenge: jest.Mock<Promise<string>, [], any>;
56
66
  generateCodeVerifier: jest.Mock<string, [], any>;
57
67
  generateState: jest.Mock<string, [], any>;
58
68
  };
59
- export declare const createClient: (prompt?: Prompt, storage?: MockedStorage) => LogtoClient;
69
+ export declare const createClient: (prompt?: Prompt, storage?: MockedStorage, withCache?: boolean) => LogtoClientWithAccessors;
60
70
  /**
61
- * Make LogtoClient.signInSession accessible for test
71
+ * Make protected fields accessible for test
62
72
  */
63
- export declare class LogtoClientSignInSessionAccessor extends LogtoClient {
73
+ export declare class LogtoClientWithAccessors extends LogtoClient {
74
+ runGetOidcConfig(): Promise<import("@silverhand/essentials").KeysToCamelCase<{
75
+ authorization_endpoint: string;
76
+ token_endpoint: string;
77
+ userinfo_endpoint: string;
78
+ end_session_endpoint: string;
79
+ revocation_endpoint: string;
80
+ jwks_uri: string;
81
+ issuer: string;
82
+ }>>;
83
+ runGetJwtVerifyGetKey(): Promise<import("jose").JWTVerifyGetKey>;
64
84
  getLogtoConfig(): Nullable<LogtoConfig>;
65
85
  getSignInSessionItem(): Promise<Nullable<LogtoSignInSessionItem>>;
66
86
  setSignInSessionItem(item: Nullable<LogtoSignInSessionItem>): Promise<void>;
@@ -0,0 +1,62 @@
1
+ 'use strict';
2
+
3
+ var essentials = require('@silverhand/essentials');
4
+ var jose = require('jose');
5
+ var types = require('./adapter/types.cjs');
6
+
7
+ // Edited from jose's internal util `isJWKSLike`
8
+ function isJwkSetLike(jwkSet) {
9
+ return Boolean(jwkSet &&
10
+ typeof jwkSet === 'object' &&
11
+ 'keys' in jwkSet &&
12
+ Array.isArray(jwkSet.keys) &&
13
+ jwkSet.keys.every((element) => essentials.isObject(element)));
14
+ }
15
+ class CachedRemoteJwkSet {
16
+ constructor(url, adapter) {
17
+ this.url = url;
18
+ this.adapter = adapter;
19
+ if (!adapter.unstable_cache) {
20
+ throw new Error("No cache found in the client adapter. Use `createRemoteJWKSet()` from 'jose' instead.");
21
+ }
22
+ }
23
+ async getKey(...args) {
24
+ if (!this.jwkSet) {
25
+ this.jwkSet = await this.#load();
26
+ }
27
+ try {
28
+ return await this.#getLocalKey(...args);
29
+ }
30
+ catch (error) {
31
+ // Jose does not export the error definition
32
+ // Found in https://github.com/panva/jose/blob/d5b3cb672736112b1e1e31ac4d5e9cd641675206/src/util/errors.ts#L347
33
+ if (error instanceof Error && 'code' in error && error.code === 'ERR_JWKS_NO_MATCHING_KEY') {
34
+ this.jwkSet = await this.#load();
35
+ return this.#getLocalKey(...args);
36
+ }
37
+ throw error;
38
+ }
39
+ }
40
+ async #load() {
41
+ return this.adapter.getWithCache(types.CacheKey.Jwks, async () => {
42
+ const controller = new AbortController();
43
+ const response = await fetch(this.url, { signal: controller.signal, redirect: 'manual' });
44
+ if (!response.ok) {
45
+ throw new Error('Expected OK from the JSON Web Key Set HTTP response');
46
+ }
47
+ const json = await response.json();
48
+ if (!isJwkSetLike(json)) {
49
+ throw new Error('JSON Web Key Set malformed');
50
+ }
51
+ return json;
52
+ });
53
+ }
54
+ async #getLocalKey(...args) {
55
+ if (!this.jwkSet) {
56
+ throw new Error('No local JWK Set found.');
57
+ }
58
+ return jose.createLocalJWKSet(this.jwkSet)(...args);
59
+ }
60
+ }
61
+
62
+ exports.CachedRemoteJwkSet = CachedRemoteJwkSet;
@@ -0,0 +1,10 @@
1
+ import { type JSONWebKeySet, type JWTVerifyGetKey } from 'jose';
2
+ import { type ClientAdapterInstance } from './adapter/index.js';
3
+ export declare class CachedRemoteJwkSet {
4
+ #private;
5
+ readonly url: URL;
6
+ private readonly adapter;
7
+ protected jwkSet?: JSONWebKeySet;
8
+ constructor(url: URL, adapter: ClientAdapterInstance);
9
+ getKey(...args: Parameters<JWTVerifyGetKey>): Promise<import("jose").KeyLike>;
10
+ }
@@ -0,0 +1,60 @@
1
+ import { isObject } from '@silverhand/essentials';
2
+ import { createLocalJWKSet } from 'jose';
3
+ import { CacheKey } from './adapter/types.js';
4
+
5
+ // Edited from jose's internal util `isJWKSLike`
6
+ function isJwkSetLike(jwkSet) {
7
+ return Boolean(jwkSet &&
8
+ typeof jwkSet === 'object' &&
9
+ 'keys' in jwkSet &&
10
+ Array.isArray(jwkSet.keys) &&
11
+ jwkSet.keys.every((element) => isObject(element)));
12
+ }
13
+ class CachedRemoteJwkSet {
14
+ constructor(url, adapter) {
15
+ this.url = url;
16
+ this.adapter = adapter;
17
+ if (!adapter.unstable_cache) {
18
+ throw new Error("No cache found in the client adapter. Use `createRemoteJWKSet()` from 'jose' instead.");
19
+ }
20
+ }
21
+ async getKey(...args) {
22
+ if (!this.jwkSet) {
23
+ this.jwkSet = await this.#load();
24
+ }
25
+ try {
26
+ return await this.#getLocalKey(...args);
27
+ }
28
+ catch (error) {
29
+ // Jose does not export the error definition
30
+ // Found in https://github.com/panva/jose/blob/d5b3cb672736112b1e1e31ac4d5e9cd641675206/src/util/errors.ts#L347
31
+ if (error instanceof Error && 'code' in error && error.code === 'ERR_JWKS_NO_MATCHING_KEY') {
32
+ this.jwkSet = await this.#load();
33
+ return this.#getLocalKey(...args);
34
+ }
35
+ throw error;
36
+ }
37
+ }
38
+ async #load() {
39
+ return this.adapter.getWithCache(CacheKey.Jwks, async () => {
40
+ const controller = new AbortController();
41
+ const response = await fetch(this.url, { signal: controller.signal, redirect: 'manual' });
42
+ if (!response.ok) {
43
+ throw new Error('Expected OK from the JSON Web Key Set HTTP response');
44
+ }
45
+ const json = await response.json();
46
+ if (!isJwkSetLike(json)) {
47
+ throw new Error('JSON Web Key Set malformed');
48
+ }
49
+ return json;
50
+ });
51
+ }
52
+ async #getLocalKey(...args) {
53
+ if (!this.jwkSet) {
54
+ throw new Error('No local JWK Set found.');
55
+ }
56
+ return createLocalJWKSet(this.jwkSet)(...args);
57
+ }
58
+ }
59
+
60
+ export { CachedRemoteJwkSet };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,25 @@
1
+ 'use strict';
2
+
3
+ function memoize(run) {
4
+ const promiseCache = new Map();
5
+ const memoized = async function (...args) {
6
+ const promiseKey = args[0];
7
+ const cachedPromise = promiseCache.get(promiseKey);
8
+ if (cachedPromise) {
9
+ return cachedPromise;
10
+ }
11
+ const promise = (async () => {
12
+ try {
13
+ return await run.apply(this, args);
14
+ }
15
+ finally {
16
+ promiseCache.delete(promiseKey);
17
+ }
18
+ })();
19
+ promiseCache.set(promiseKey, promise);
20
+ return promise;
21
+ };
22
+ return memoized;
23
+ }
24
+
25
+ exports.memoize = memoize;
@@ -0,0 +1 @@
1
+ export declare function memoize<Args extends unknown[], Return>(run: (...args: Args) => Promise<Return>): (this: unknown, ...args: Args) => Promise<Return>;
@@ -0,0 +1,23 @@
1
+ function memoize(run) {
2
+ const promiseCache = new Map();
3
+ const memoized = async function (...args) {
4
+ const promiseKey = args[0];
5
+ const cachedPromise = promiseCache.get(promiseKey);
6
+ if (cachedPromise) {
7
+ return cachedPromise;
8
+ }
9
+ const promise = (async () => {
10
+ try {
11
+ return await run.apply(this, args);
12
+ }
13
+ finally {
14
+ promiseCache.delete(promiseKey);
15
+ }
16
+ })();
17
+ promiseCache.set(promiseKey, promise);
18
+ return promise;
19
+ };
20
+ return memoized;
21
+ }
22
+
23
+ export { memoize };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/client",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "main": "./lib/index.cjs",
6
6
  "module": "./lib/index.js",
package/lib/adapter.d.ts DELETED
@@ -1,17 +0,0 @@
1
- import type { Requester } from '@logto/js';
2
- import type { Nullable } from '@silverhand/essentials';
3
- export type StorageKey = 'idToken' | 'refreshToken' | 'accessToken' | 'signInSession';
4
- export type Storage = {
5
- getItem(key: StorageKey): Promise<Nullable<string>>;
6
- setItem(key: StorageKey, value: string): Promise<void>;
7
- removeItem(key: StorageKey): Promise<void>;
8
- };
9
- export type Navigate = (url: string) => void;
10
- export type ClientAdapter = {
11
- requester: Requester;
12
- storage: Storage;
13
- navigate: Navigate;
14
- generateState: () => string;
15
- generateCodeVerifier: () => string;
16
- generateCodeChallenge: (codeVerifier: string) => Promise<string>;
17
- };