@logto/client 2.0.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;
@@ -57,6 +61,10 @@ class LogtoClient {
57
61
  }
58
62
  return js.decodeIdToken(idToken);
59
63
  }
64
+ async getAccessTokenClaims(resource) {
65
+ const accessToken = await this.getAccessToken(resource);
66
+ return js.decodeAccessToken(accessToken);
67
+ }
60
68
  async fetchUserInfo() {
61
69
  const { userinfoEndpoint } = await this.getOidcConfig();
62
70
  const accessToken = await this.getAccessToken();
@@ -152,34 +160,21 @@ class LogtoClient {
152
160
  }
153
161
  return item;
154
162
  }
155
- async setSignInSession(logtoSignInSessionItem) {
156
- if (!logtoSignInSessionItem) {
157
- await this.adapter.storage.removeItem('signInSession');
158
- return;
159
- }
160
- const jsonItem = JSON.stringify(logtoSignInSessionItem);
161
- await this.adapter.storage.setItem('signInSession', jsonItem);
163
+ async setSignInSession(value) {
164
+ return this.adapter.setStorageItem(types.PersistKey.SignInSession, value && JSON.stringify(value));
162
165
  }
163
- async setIdToken(idToken) {
164
- if (!idToken) {
165
- await this.adapter.storage.removeItem('idToken');
166
- return;
167
- }
168
- await this.adapter.storage.setItem('idToken', idToken);
166
+ async setIdToken(value) {
167
+ return this.adapter.setStorageItem(types.PersistKey.IdToken, value);
169
168
  }
170
- async setRefreshToken(refreshToken) {
171
- if (!refreshToken) {
172
- await this.adapter.storage.removeItem('refreshToken');
173
- return;
174
- }
175
- await this.adapter.storage.setItem('refreshToken', refreshToken);
169
+ async setRefreshToken(value) {
170
+ return this.adapter.setStorageItem(types.PersistKey.RefreshToken, value);
176
171
  }
177
172
  async getAccessTokenByRefreshToken(resource) {
178
173
  const currentRefreshToken = await this.getRefreshToken();
179
174
  if (!currentRefreshToken) {
180
175
  throw new errors.LogtoClientError('not_authenticated');
181
176
  }
182
- const accessTokenKey = index$1.buildAccessTokenKey(resource);
177
+ const accessTokenKey = index$2.buildAccessTokenKey(resource);
183
178
  const { appId: clientId } = this.logtoConfig;
184
179
  const { tokenEndpoint } = await this.getOidcConfig();
185
180
  const { accessToken, refreshToken, idToken, scope, expiresIn } = await js.fetchTokenByRefreshToken({
@@ -201,15 +196,6 @@ class LogtoClient {
201
196
  }
202
197
  return accessToken;
203
198
  }
204
- async _getOidcConfig() {
205
- const { endpoint } = this.logtoConfig;
206
- const discoveryEndpoint = index$1.getDiscoveryEndpoint(endpoint);
207
- return js.fetchOidcConfig(discoveryEndpoint, this.adapter.requester);
208
- }
209
- async _getJwtVerifyGetKey() {
210
- const { jwksUri } = await this.getOidcConfig();
211
- return jose.createRemoteJWKSet(new URL(jwksUri));
212
- }
213
199
  async verifyIdToken(idToken) {
214
200
  const { appId } = this.logtoConfig;
215
201
  const { issuer } = await this.getOidcConfig();
@@ -220,7 +206,7 @@ class LogtoClient {
220
206
  await this.setRefreshToken(refreshToken ?? null);
221
207
  await this.setIdToken(idToken);
222
208
  // NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
223
- const accessTokenKey = index$1.buildAccessTokenKey();
209
+ const accessTokenKey = index$2.buildAccessTokenKey();
224
210
  const expiresAt = Date.now() / 1000 + expiresIn;
225
211
  this.accessTokenMap.set(accessTokenKey, { token: accessToken, scope, expiresAt });
226
212
  await this.saveAccessTokenMap();
@@ -252,6 +238,19 @@ class LogtoClient {
252
238
  console.warn(error);
253
239
  }
254
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
+ }
255
254
  }
256
255
 
257
256
  Object.defineProperty(exports, 'LogtoError', {
@@ -281,5 +280,13 @@ Object.defineProperty(exports, 'UserScope', {
281
280
  exports.LogtoClientError = errors.LogtoClientError;
282
281
  exports.isLogtoAccessTokenMap = index.isLogtoAccessTokenMap;
283
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
+ });
284
291
  exports.createRequester = requester.createRequester;
285
292
  exports.default = LogtoClient;
@@ -0,0 +1 @@
1
+ export {};
package/lib/index.d.ts CHANGED
@@ -1,18 +1,29 @@
1
- import type { IdTokenClaims, UserInfoResponse, InteractionMode } from '@logto/js';
2
- import type { Nullable } from '@silverhand/essentials';
3
- import type { ClientAdapter } from './adapter.js';
1
+ import { type IdTokenClaims, type UserInfoResponse, type InteractionMode, type AccessTokenClaims } from '@logto/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>;
@@ -20,18 +31,17 @@ export default class LogtoClient {
20
31
  getIdToken(): Promise<Nullable<string>>;
21
32
  getAccessToken(resource?: string): Promise<string>;
22
33
  getIdTokenClaims(): Promise<IdTokenClaims>;
34
+ getAccessTokenClaims(resource?: string): Promise<AccessTokenClaims>;
23
35
  fetchUserInfo(): Promise<UserInfoResponse>;
24
36
  signIn(redirectUri: string, interactionMode?: InteractionMode): Promise<void>;
25
37
  isSignInRedirected(url: string): Promise<boolean>;
26
38
  handleSignInCallback(callbackUri: string): Promise<void>;
27
39
  signOut(postLogoutRedirectUri?: string): Promise<void>;
28
40
  protected getSignInSession(): Promise<Nullable<LogtoSignInSessionItem>>;
29
- protected setSignInSession(logtoSignInSessionItem: Nullable<LogtoSignInSessionItem>): Promise<void>;
41
+ protected setSignInSession(value: Nullable<LogtoSignInSessionItem>): Promise<void>;
30
42
  private setIdToken;
31
43
  private setRefreshToken;
32
44
  private getAccessTokenByRefreshToken;
33
- private _getOidcConfig;
34
- private _getJwtVerifyGetKey;
35
45
  private verifyIdToken;
36
46
  private saveCodeToken;
37
47
  private saveAccessTokenMap;
package/lib/index.js CHANGED
@@ -1,23 +1,27 @@
1
- import { Prompt, withDefaultScopes, decodeIdToken, 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() {
@@ -54,6 +58,10 @@ class LogtoClient {
54
58
  }
55
59
  return decodeIdToken(idToken);
56
60
  }
61
+ async getAccessTokenClaims(resource) {
62
+ const accessToken = await this.getAccessToken(resource);
63
+ return decodeAccessToken(accessToken);
64
+ }
57
65
  async fetchUserInfo() {
58
66
  const { userinfoEndpoint } = await this.getOidcConfig();
59
67
  const accessToken = await this.getAccessToken();
@@ -149,27 +157,14 @@ class LogtoClient {
149
157
  }
150
158
  return item;
151
159
  }
152
- async setSignInSession(logtoSignInSessionItem) {
153
- if (!logtoSignInSessionItem) {
154
- await this.adapter.storage.removeItem('signInSession');
155
- return;
156
- }
157
- const jsonItem = JSON.stringify(logtoSignInSessionItem);
158
- await this.adapter.storage.setItem('signInSession', jsonItem);
160
+ async setSignInSession(value) {
161
+ return this.adapter.setStorageItem(PersistKey.SignInSession, value && JSON.stringify(value));
159
162
  }
160
- async setIdToken(idToken) {
161
- if (!idToken) {
162
- await this.adapter.storage.removeItem('idToken');
163
- return;
164
- }
165
- await this.adapter.storage.setItem('idToken', idToken);
163
+ async setIdToken(value) {
164
+ return this.adapter.setStorageItem(PersistKey.IdToken, value);
166
165
  }
167
- async setRefreshToken(refreshToken) {
168
- if (!refreshToken) {
169
- await this.adapter.storage.removeItem('refreshToken');
170
- return;
171
- }
172
- await this.adapter.storage.setItem('refreshToken', refreshToken);
166
+ async setRefreshToken(value) {
167
+ return this.adapter.setStorageItem(PersistKey.RefreshToken, value);
173
168
  }
174
169
  async getAccessTokenByRefreshToken(resource) {
175
170
  const currentRefreshToken = await this.getRefreshToken();
@@ -198,15 +193,6 @@ class LogtoClient {
198
193
  }
199
194
  return accessToken;
200
195
  }
201
- async _getOidcConfig() {
202
- const { endpoint } = this.logtoConfig;
203
- const discoveryEndpoint = getDiscoveryEndpoint(endpoint);
204
- return fetchOidcConfig(discoveryEndpoint, this.adapter.requester);
205
- }
206
- async _getJwtVerifyGetKey() {
207
- const { jwksUri } = await this.getOidcConfig();
208
- return createRemoteJWKSet(new URL(jwksUri));
209
- }
210
196
  async verifyIdToken(idToken) {
211
197
  const { appId } = this.logtoConfig;
212
198
  const { issuer } = await this.getOidcConfig();
@@ -249,6 +235,19 @@ class LogtoClient {
249
235
  console.warn(error);
250
236
  }
251
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
+ }
252
251
  }
253
252
 
254
- 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.0.0",
3
+ "version": "2.2.0",
4
4
  "type": "module",
5
5
  "main": "./lib/index.cjs",
6
6
  "module": "./lib/index.js",
@@ -21,7 +21,7 @@
21
21
  "directory": "packages/client"
22
22
  },
23
23
  "dependencies": {
24
- "@logto/js": "^2.0.0",
24
+ "@logto/js": "^2.1.0",
25
25
  "@silverhand/essentials": "^2.6.2",
26
26
  "camelcase-keys": "^7.0.1",
27
27
  "jose": "^4.13.2"
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
- };