@logto/client 2.2.3 → 2.3.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.
package/lib/errors.cjs CHANGED
@@ -1,11 +1,14 @@
1
1
  'use strict';
2
2
 
3
+ var js = require('@logto/js');
4
+
3
5
  const logtoClientErrorCodes = Object.freeze({
4
6
  'sign_in_session.invalid': 'Invalid sign-in session.',
5
7
  'sign_in_session.not_found': 'Sign-in session not found.',
6
8
  not_authenticated: 'Not authenticated.',
7
9
  fetch_user_info_failed: 'Unable to fetch user info. The access token may be invalid.',
8
10
  user_cancelled: 'The user cancelled the action.',
11
+ missing_scope_organizations: `The \`${js.UserScope.Organizations}\` scope is required`,
9
12
  });
10
13
  class LogtoClientError extends Error {
11
14
  constructor(code, data) {
package/lib/errors.d.ts CHANGED
@@ -4,6 +4,7 @@ declare const logtoClientErrorCodes: Readonly<{
4
4
  not_authenticated: "Not authenticated.";
5
5
  fetch_user_info_failed: "Unable to fetch user info. The access token may be invalid.";
6
6
  user_cancelled: "The user cancelled the action.";
7
+ missing_scope_organizations: "The `urn:logto:scope:organizations` scope is required";
7
8
  }>;
8
9
  export type LogtoClientErrorCode = keyof typeof logtoClientErrorCodes;
9
10
  export declare class LogtoClientError extends Error {
package/lib/errors.js CHANGED
@@ -1,9 +1,12 @@
1
+ import { UserScope } from '@logto/js';
2
+
1
3
  const logtoClientErrorCodes = Object.freeze({
2
4
  'sign_in_session.invalid': 'Invalid sign-in session.',
3
5
  'sign_in_session.not_found': 'Sign-in session not found.',
4
6
  not_authenticated: 'Not authenticated.',
5
7
  fetch_user_info_failed: 'Unable to fetch user info. The access token may be invalid.',
6
8
  user_cancelled: 'The user cancelled the action.',
9
+ missing_scope_organizations: `The \`${UserScope.Organizations}\` scope is required`,
7
10
  });
8
11
  class LogtoClientError extends Error {
9
12
  constructor(code, data) {
package/lib/index.cjs CHANGED
@@ -14,6 +14,7 @@ var once = require('./utils/once.cjs');
14
14
  var types = require('./adapter/types.cjs');
15
15
  var requester = require('./utils/requester.cjs');
16
16
 
17
+ /* eslint-disable max-lines */
17
18
  /**
18
19
  * The Logto base client class that provides the essential methods for
19
20
  * interacting with the Logto server.
@@ -24,13 +25,37 @@ var requester = require('./utils/requester.cjs');
24
25
  class LogtoClient {
25
26
  constructor(logtoConfig, adapter) {
26
27
  this.getOidcConfig = memoize.memoize(this.#getOidcConfig);
28
+ /**
29
+ * Get the access token from the storage with refresh strategy.
30
+ *
31
+ * - If the access token has expired, it will try to fetch a new one using the Refresh Token.
32
+ * - If there's an ongoing Promise to fetch the access token, it will return the Promise.
33
+ *
34
+ * If you want to get the access token claims, use {@link getAccessTokenClaims} instead.
35
+ *
36
+ * @param resource The resource that the access token is granted for. If not
37
+ * specified, the access token will be used for OpenID Connect or the default
38
+ * resource, as specified in the Logto Console.
39
+ * @returns The access token string.
40
+ * @throws LogtoClientError if the user is not authenticated.
41
+ */
42
+ this.getAccessToken = memoize.memoize(this.#getAccessToken);
43
+ /**
44
+ * Get the access token for the specified organization from the storage with refresh strategy.
45
+ *
46
+ * Scope {@link UserScope.Organizations} is required in the config to use organization-related
47
+ * methods.
48
+ *
49
+ * @param organizationId The ID of the organization that the access token is granted for.
50
+ * @returns The access token string.
51
+ * @throws LogtoClientError if the user is not authenticated.
52
+ * @remarks
53
+ * It uses the same refresh strategy as {@link getAccessToken}.
54
+ */
55
+ this.getOrganizationToken = memoize.memoize(this.#getOrganizationToken);
27
56
  this.getJwtVerifyGetKey = once.once(this.#getJwtVerifyGetKey);
28
57
  this.accessTokenMap = new Map();
29
- this.logtoConfig = {
30
- ...logtoConfig,
31
- prompt: logtoConfig.prompt ?? js.Prompt.Consent,
32
- scopes: js.withDefaultScopes(logtoConfig.scopes).split(' '),
33
- };
58
+ this.logtoConfig = index.normalizeLogtoConfig(logtoConfig);
34
59
  this.adapter = new index$1.ClientAdapterInstance(adapter);
35
60
  void this.loadAccessTokenMap();
36
61
  }
@@ -53,36 +78,6 @@ class LogtoClient {
53
78
  async getIdToken() {
54
79
  return this.adapter.storage.getItem('idToken');
55
80
  }
56
- /**
57
- * Get the Access Token from the storage. If the Access Token has expired, it
58
- * will try to fetch a new one using the Refresh Token.
59
- *
60
- * If you want to get the Access Token claims, use {@link getAccessTokenClaims} instead.
61
- *
62
- * @param resource The resource that the Access Token is granted for. If not
63
- * specified, the Access Token will be used for OpenID Connect or the default
64
- * resource, as specified in the Logto Console.
65
- * @returns The Access Token string.
66
- * @throws LogtoClientError if the user is not authenticated.
67
- */
68
- async getAccessToken(resource) {
69
- if (!(await this.getIdToken())) {
70
- throw new errors.LogtoClientError('not_authenticated');
71
- }
72
- const accessTokenKey = index$2.buildAccessTokenKey(resource);
73
- const accessToken = this.accessTokenMap.get(accessTokenKey);
74
- if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
75
- return accessToken.token;
76
- }
77
- // Since the access token has expired, delete it from the map.
78
- if (accessToken) {
79
- this.accessTokenMap.delete(accessTokenKey);
80
- }
81
- /**
82
- * Need to fetch a new access token using refresh token.
83
- */
84
- return this.getAccessTokenByRefreshToken(resource);
85
- }
86
81
  /**
87
82
  * Get the ID Token claims.
88
83
  */
@@ -94,16 +89,25 @@ class LogtoClient {
94
89
  return js.decodeIdToken(idToken);
95
90
  }
96
91
  /**
97
- * Get the Access Token claims for the specified resource.
92
+ * Get the access token claims for the specified resource.
98
93
  *
99
- * @param resource The resource that the Access Token is granted for. If not
100
- * specified, the Access Token will be used for OpenID Connect or the default
94
+ * @param resource The resource that the access token is granted for. If not
95
+ * specified, the access token will be used for OpenID Connect or the default
101
96
  * resource, as specified in the Logto Console.
102
97
  */
103
98
  async getAccessTokenClaims(resource) {
104
99
  const accessToken = await this.getAccessToken(resource);
105
100
  return js.decodeAccessToken(accessToken);
106
101
  }
102
+ /**
103
+ * Get the organization token claims for the specified organization.
104
+ *
105
+ * @param organizationId The ID of the organization that the access token is granted for.
106
+ */
107
+ async getOrganizationTokenClaims(organizationId) {
108
+ const accessToken = await this.getOrganizationToken(organizationId);
109
+ return js.decodeAccessToken(accessToken);
110
+ }
107
111
  /**
108
112
  * Get the user information from the Userinfo Endpoint.
109
113
  *
@@ -278,12 +282,12 @@ class LogtoClient {
278
282
  async setRefreshToken(value) {
279
283
  return this.adapter.setStorageItem(types.PersistKey.RefreshToken, value);
280
284
  }
281
- async getAccessTokenByRefreshToken(resource) {
285
+ async getAccessTokenByRefreshToken(resource, organizationId) {
282
286
  const currentRefreshToken = await this.getRefreshToken();
283
287
  if (!currentRefreshToken) {
284
288
  throw new errors.LogtoClientError('not_authenticated');
285
289
  }
286
- const accessTokenKey = index$2.buildAccessTokenKey(resource);
290
+ const accessTokenKey = index$2.buildAccessTokenKey(resource, organizationId);
287
291
  const { appId: clientId } = this.logtoConfig;
288
292
  const { tokenEndpoint } = await this.getOidcConfig();
289
293
  const requestedAt = Math.round(Date.now() / 1000);
@@ -292,6 +296,7 @@ class LogtoClient {
292
296
  tokenEndpoint,
293
297
  refreshToken: currentRefreshToken,
294
298
  resource,
299
+ organizationId,
295
300
  }, this.adapter.requester);
296
301
  this.accessTokenMap.set(accessTokenKey, {
297
302
  token: accessToken,
@@ -358,7 +363,32 @@ class LogtoClient {
358
363
  const cachedJwkSet = new remoteJwkSet.CachedRemoteJwkSet(new URL(jwksUri), this.adapter);
359
364
  return async (...args) => cachedJwkSet.getKey(...args);
360
365
  }
366
+ async #getAccessToken(resource, organizationId) {
367
+ if (!(await this.isAuthenticated())) {
368
+ throw new errors.LogtoClientError('not_authenticated');
369
+ }
370
+ const accessTokenKey = index$2.buildAccessTokenKey(resource, organizationId);
371
+ const accessToken = this.accessTokenMap.get(accessTokenKey);
372
+ if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
373
+ return accessToken.token;
374
+ }
375
+ // Since the access token has expired, delete it from the map.
376
+ if (accessToken) {
377
+ this.accessTokenMap.delete(accessTokenKey);
378
+ }
379
+ /**
380
+ * Need to fetch a new access token using refresh token.
381
+ */
382
+ return this.getAccessTokenByRefreshToken(resource, organizationId);
383
+ }
384
+ async #getOrganizationToken(organizationId) {
385
+ if (!this.logtoConfig.scopes?.includes(js.UserScope.Organizations)) {
386
+ throw new errors.LogtoClientError('missing_scope_organizations');
387
+ }
388
+ return this.#getAccessToken(undefined, organizationId);
389
+ }
361
390
  }
391
+ /* eslint-enable max-lines */
362
392
 
363
393
  Object.defineProperty(exports, 'LogtoError', {
364
394
  enumerable: true,
@@ -376,6 +406,10 @@ Object.defineProperty(exports, 'Prompt', {
376
406
  enumerable: true,
377
407
  get: function () { return js.Prompt; }
378
408
  });
409
+ Object.defineProperty(exports, 'ReservedResource', {
410
+ enumerable: true,
411
+ get: function () { return js.ReservedResource; }
412
+ });
379
413
  Object.defineProperty(exports, 'ReservedScope', {
380
414
  enumerable: true,
381
415
  get: function () { return js.ReservedScope; }
@@ -384,9 +418,22 @@ Object.defineProperty(exports, 'UserScope', {
384
418
  enumerable: true,
385
419
  get: function () { return js.UserScope; }
386
420
  });
421
+ Object.defineProperty(exports, 'buildOrganizationUrn', {
422
+ enumerable: true,
423
+ get: function () { return js.buildOrganizationUrn; }
424
+ });
425
+ Object.defineProperty(exports, 'getOrganizationIdFromUrn', {
426
+ enumerable: true,
427
+ get: function () { return js.getOrganizationIdFromUrn; }
428
+ });
429
+ Object.defineProperty(exports, 'organizationUrnPrefix', {
430
+ enumerable: true,
431
+ get: function () { return js.organizationUrnPrefix; }
432
+ });
387
433
  exports.LogtoClientError = errors.LogtoClientError;
388
434
  exports.isLogtoAccessTokenMap = index.isLogtoAccessTokenMap;
389
435
  exports.isLogtoSignInSessionItem = index.isLogtoSignInSessionItem;
436
+ exports.normalizeLogtoConfig = index.normalizeLogtoConfig;
390
437
  Object.defineProperty(exports, 'CacheKey', {
391
438
  enumerable: true,
392
439
  get: function () { return types.CacheKey; }
package/lib/index.d.ts CHANGED
@@ -4,7 +4,7 @@ import { type JWTVerifyGetKey } from 'jose';
4
4
  import { ClientAdapterInstance, type ClientAdapter } from './adapter/index.js';
5
5
  import type { AccessToken, LogtoConfig, LogtoSignInSessionItem } from './types/index.js';
6
6
  export type { IdTokenClaims, LogtoErrorCode, UserInfoResponse, InteractionMode } from '@logto/js';
7
- export { LogtoError, LogtoRequestError, OidcError, Prompt, ReservedScope, UserScope, } from '@logto/js';
7
+ export { LogtoError, LogtoRequestError, OidcError, Prompt, ReservedScope, ReservedResource, UserScope, organizationUrnPrefix, buildOrganizationUrn, getOrganizationIdFromUrn, } from '@logto/js';
8
8
  export * from './errors.js';
9
9
  export type { Storage, StorageKey, ClientAdapter } from './adapter/index.js';
10
10
  export { PersistKey, CacheKey } from './adapter/index.js';
@@ -19,8 +19,8 @@ export * from './types/index.js';
19
19
  */
20
20
  export default class LogtoClient {
21
21
  #private;
22
- protected readonly logtoConfig: LogtoConfig;
23
- protected readonly getOidcConfig: (this: unknown) => Promise<import("@silverhand/essentials").KeysToCamelCase<{
22
+ readonly logtoConfig: LogtoConfig;
23
+ readonly getOidcConfig: (this: unknown) => Promise<import("@silverhand/essentials").KeysToCamelCase<{
24
24
  authorization_endpoint: string;
25
25
  token_endpoint: string;
26
26
  userinfo_endpoint: string;
@@ -29,6 +29,34 @@ export default class LogtoClient {
29
29
  jwks_uri: string;
30
30
  issuer: string;
31
31
  }>>;
32
+ /**
33
+ * Get the access token from the storage with refresh strategy.
34
+ *
35
+ * - If the access token has expired, it will try to fetch a new one using the Refresh Token.
36
+ * - If there's an ongoing Promise to fetch the access token, it will return the Promise.
37
+ *
38
+ * If you want to get the access token claims, use {@link getAccessTokenClaims} instead.
39
+ *
40
+ * @param resource The resource that the access token is granted for. If not
41
+ * specified, the access token will be used for OpenID Connect or the default
42
+ * resource, as specified in the Logto Console.
43
+ * @returns The access token string.
44
+ * @throws LogtoClientError if the user is not authenticated.
45
+ */
46
+ readonly getAccessToken: (this: unknown, resource?: string | undefined, organizationId?: string | undefined) => Promise<string>;
47
+ /**
48
+ * Get the access token for the specified organization from the storage with refresh strategy.
49
+ *
50
+ * Scope {@link UserScope.Organizations} is required in the config to use organization-related
51
+ * methods.
52
+ *
53
+ * @param organizationId The ID of the organization that the access token is granted for.
54
+ * @returns The access token string.
55
+ * @throws LogtoClientError if the user is not authenticated.
56
+ * @remarks
57
+ * It uses the same refresh strategy as {@link getAccessToken}.
58
+ */
59
+ readonly getOrganizationToken: (this: unknown, organizationId: string) => Promise<string>;
32
60
  protected readonly getJwtVerifyGetKey: (...args: unknown[]) => Promise<JWTVerifyGetKey>;
33
61
  protected readonly adapter: ClientAdapterInstance;
34
62
  protected readonly accessTokenMap: Map<string, AccessToken>;
@@ -46,31 +74,24 @@ export default class LogtoClient {
46
74
  * use {@link getIdTokenClaims} instead.
47
75
  */
48
76
  getIdToken(): Promise<Nullable<string>>;
49
- /**
50
- * Get the Access Token from the storage. If the Access Token has expired, it
51
- * will try to fetch a new one using the Refresh Token.
52
- *
53
- * If you want to get the Access Token claims, use {@link getAccessTokenClaims} instead.
54
- *
55
- * @param resource The resource that the Access Token is granted for. If not
56
- * specified, the Access Token will be used for OpenID Connect or the default
57
- * resource, as specified in the Logto Console.
58
- * @returns The Access Token string.
59
- * @throws LogtoClientError if the user is not authenticated.
60
- */
61
- getAccessToken(resource?: string): Promise<string>;
62
77
  /**
63
78
  * Get the ID Token claims.
64
79
  */
65
80
  getIdTokenClaims(): Promise<IdTokenClaims>;
66
81
  /**
67
- * Get the Access Token claims for the specified resource.
82
+ * Get the access token claims for the specified resource.
68
83
  *
69
- * @param resource The resource that the Access Token is granted for. If not
70
- * specified, the Access Token will be used for OpenID Connect or the default
84
+ * @param resource The resource that the access token is granted for. If not
85
+ * specified, the access token will be used for OpenID Connect or the default
71
86
  * resource, as specified in the Logto Console.
72
87
  */
73
88
  getAccessTokenClaims(resource?: string): Promise<AccessTokenClaims>;
89
+ /**
90
+ * Get the organization token claims for the specified organization.
91
+ *
92
+ * @param organizationId The ID of the organization that the access token is granted for.
93
+ */
94
+ getOrganizationTokenClaims(organizationId: string): Promise<AccessTokenClaims>;
74
95
  /**
75
96
  * Get the user information from the Userinfo Endpoint.
76
97
  *
package/lib/index.js CHANGED
@@ -1,16 +1,17 @@
1
- import { Prompt, withDefaultScopes, decodeIdToken, decodeAccessToken, fetchUserInfo, generateSignInUri, verifyAndParseCodeFromCallbackUri, fetchTokenByAuthorizationCode, revoke, generateSignOutUri, fetchTokenByRefreshToken, verifyIdToken, fetchOidcConfig } from '@logto/js';
2
- export { LogtoError, LogtoRequestError, OidcError, Prompt, ReservedScope, UserScope } from '@logto/js';
1
+ import { decodeIdToken, decodeAccessToken, fetchUserInfo, generateSignInUri, verifyAndParseCodeFromCallbackUri, fetchTokenByAuthorizationCode, revoke, generateSignOutUri, fetchTokenByRefreshToken, verifyIdToken, fetchOidcConfig, UserScope } from '@logto/js';
2
+ export { LogtoError, LogtoRequestError, OidcError, Prompt, ReservedResource, ReservedScope, UserScope, buildOrganizationUrn, getOrganizationIdFromUrn, organizationUrnPrefix } from '@logto/js';
3
3
  import { createRemoteJWKSet } from 'jose';
4
4
  import { ClientAdapterInstance } from './adapter/index.js';
5
5
  import { LogtoClientError } from './errors.js';
6
6
  import { CachedRemoteJwkSet } from './remote-jwk-set.js';
7
- import { isLogtoSignInSessionItem, isLogtoAccessTokenMap } from './types/index.js';
7
+ import { normalizeLogtoConfig, isLogtoSignInSessionItem, isLogtoAccessTokenMap } from './types/index.js';
8
8
  import { buildAccessTokenKey, getDiscoveryEndpoint } from './utils/index.js';
9
9
  import { memoize } from './utils/memoize.js';
10
10
  import { once } from './utils/once.js';
11
11
  import { PersistKey, CacheKey } from './adapter/types.js';
12
12
  export { createRequester } from './utils/requester.js';
13
13
 
14
+ /* eslint-disable max-lines */
14
15
  /**
15
16
  * The Logto base client class that provides the essential methods for
16
17
  * interacting with the Logto server.
@@ -21,13 +22,37 @@ export { createRequester } from './utils/requester.js';
21
22
  class LogtoClient {
22
23
  constructor(logtoConfig, adapter) {
23
24
  this.getOidcConfig = memoize(this.#getOidcConfig);
25
+ /**
26
+ * Get the access token from the storage with refresh strategy.
27
+ *
28
+ * - If the access token has expired, it will try to fetch a new one using the Refresh Token.
29
+ * - If there's an ongoing Promise to fetch the access token, it will return the Promise.
30
+ *
31
+ * If you want to get the access token claims, use {@link getAccessTokenClaims} instead.
32
+ *
33
+ * @param resource The resource that the access token is granted for. If not
34
+ * specified, the access token will be used for OpenID Connect or the default
35
+ * resource, as specified in the Logto Console.
36
+ * @returns The access token string.
37
+ * @throws LogtoClientError if the user is not authenticated.
38
+ */
39
+ this.getAccessToken = memoize(this.#getAccessToken);
40
+ /**
41
+ * Get the access token for the specified organization from the storage with refresh strategy.
42
+ *
43
+ * Scope {@link UserScope.Organizations} is required in the config to use organization-related
44
+ * methods.
45
+ *
46
+ * @param organizationId The ID of the organization that the access token is granted for.
47
+ * @returns The access token string.
48
+ * @throws LogtoClientError if the user is not authenticated.
49
+ * @remarks
50
+ * It uses the same refresh strategy as {@link getAccessToken}.
51
+ */
52
+ this.getOrganizationToken = memoize(this.#getOrganizationToken);
24
53
  this.getJwtVerifyGetKey = once(this.#getJwtVerifyGetKey);
25
54
  this.accessTokenMap = new Map();
26
- this.logtoConfig = {
27
- ...logtoConfig,
28
- prompt: logtoConfig.prompt ?? Prompt.Consent,
29
- scopes: withDefaultScopes(logtoConfig.scopes).split(' '),
30
- };
55
+ this.logtoConfig = normalizeLogtoConfig(logtoConfig);
31
56
  this.adapter = new ClientAdapterInstance(adapter);
32
57
  void this.loadAccessTokenMap();
33
58
  }
@@ -50,36 +75,6 @@ class LogtoClient {
50
75
  async getIdToken() {
51
76
  return this.adapter.storage.getItem('idToken');
52
77
  }
53
- /**
54
- * Get the Access Token from the storage. If the Access Token has expired, it
55
- * will try to fetch a new one using the Refresh Token.
56
- *
57
- * If you want to get the Access Token claims, use {@link getAccessTokenClaims} instead.
58
- *
59
- * @param resource The resource that the Access Token is granted for. If not
60
- * specified, the Access Token will be used for OpenID Connect or the default
61
- * resource, as specified in the Logto Console.
62
- * @returns The Access Token string.
63
- * @throws LogtoClientError if the user is not authenticated.
64
- */
65
- async getAccessToken(resource) {
66
- if (!(await this.getIdToken())) {
67
- throw new LogtoClientError('not_authenticated');
68
- }
69
- const accessTokenKey = buildAccessTokenKey(resource);
70
- const accessToken = this.accessTokenMap.get(accessTokenKey);
71
- if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
72
- return accessToken.token;
73
- }
74
- // Since the access token has expired, delete it from the map.
75
- if (accessToken) {
76
- this.accessTokenMap.delete(accessTokenKey);
77
- }
78
- /**
79
- * Need to fetch a new access token using refresh token.
80
- */
81
- return this.getAccessTokenByRefreshToken(resource);
82
- }
83
78
  /**
84
79
  * Get the ID Token claims.
85
80
  */
@@ -91,16 +86,25 @@ class LogtoClient {
91
86
  return decodeIdToken(idToken);
92
87
  }
93
88
  /**
94
- * Get the Access Token claims for the specified resource.
89
+ * Get the access token claims for the specified resource.
95
90
  *
96
- * @param resource The resource that the Access Token is granted for. If not
97
- * specified, the Access Token will be used for OpenID Connect or the default
91
+ * @param resource The resource that the access token is granted for. If not
92
+ * specified, the access token will be used for OpenID Connect or the default
98
93
  * resource, as specified in the Logto Console.
99
94
  */
100
95
  async getAccessTokenClaims(resource) {
101
96
  const accessToken = await this.getAccessToken(resource);
102
97
  return decodeAccessToken(accessToken);
103
98
  }
99
+ /**
100
+ * Get the organization token claims for the specified organization.
101
+ *
102
+ * @param organizationId The ID of the organization that the access token is granted for.
103
+ */
104
+ async getOrganizationTokenClaims(organizationId) {
105
+ const accessToken = await this.getOrganizationToken(organizationId);
106
+ return decodeAccessToken(accessToken);
107
+ }
104
108
  /**
105
109
  * Get the user information from the Userinfo Endpoint.
106
110
  *
@@ -275,12 +279,12 @@ class LogtoClient {
275
279
  async setRefreshToken(value) {
276
280
  return this.adapter.setStorageItem(PersistKey.RefreshToken, value);
277
281
  }
278
- async getAccessTokenByRefreshToken(resource) {
282
+ async getAccessTokenByRefreshToken(resource, organizationId) {
279
283
  const currentRefreshToken = await this.getRefreshToken();
280
284
  if (!currentRefreshToken) {
281
285
  throw new LogtoClientError('not_authenticated');
282
286
  }
283
- const accessTokenKey = buildAccessTokenKey(resource);
287
+ const accessTokenKey = buildAccessTokenKey(resource, organizationId);
284
288
  const { appId: clientId } = this.logtoConfig;
285
289
  const { tokenEndpoint } = await this.getOidcConfig();
286
290
  const requestedAt = Math.round(Date.now() / 1000);
@@ -289,6 +293,7 @@ class LogtoClient {
289
293
  tokenEndpoint,
290
294
  refreshToken: currentRefreshToken,
291
295
  resource,
296
+ organizationId,
292
297
  }, this.adapter.requester);
293
298
  this.accessTokenMap.set(accessTokenKey, {
294
299
  token: accessToken,
@@ -355,6 +360,31 @@ class LogtoClient {
355
360
  const cachedJwkSet = new CachedRemoteJwkSet(new URL(jwksUri), this.adapter);
356
361
  return async (...args) => cachedJwkSet.getKey(...args);
357
362
  }
363
+ async #getAccessToken(resource, organizationId) {
364
+ if (!(await this.isAuthenticated())) {
365
+ throw new LogtoClientError('not_authenticated');
366
+ }
367
+ const accessTokenKey = buildAccessTokenKey(resource, organizationId);
368
+ const accessToken = this.accessTokenMap.get(accessTokenKey);
369
+ if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
370
+ return accessToken.token;
371
+ }
372
+ // Since the access token has expired, delete it from the map.
373
+ if (accessToken) {
374
+ this.accessTokenMap.delete(accessTokenKey);
375
+ }
376
+ /**
377
+ * Need to fetch a new access token using refresh token.
378
+ */
379
+ return this.getAccessTokenByRefreshToken(resource, organizationId);
380
+ }
381
+ async #getOrganizationToken(organizationId) {
382
+ if (!this.logtoConfig.scopes?.includes(UserScope.Organizations)) {
383
+ throw new LogtoClientError('missing_scope_organizations');
384
+ }
385
+ return this.#getAccessToken(undefined, organizationId);
386
+ }
358
387
  }
388
+ /* eslint-enable max-lines */
359
389
 
360
- export { CacheKey, LogtoClientError, PersistKey, LogtoClient as default, isLogtoAccessTokenMap, isLogtoSignInSessionItem };
390
+ export { CacheKey, LogtoClientError, PersistKey, LogtoClient as default, isLogtoAccessTokenMap, isLogtoSignInSessionItem, normalizeLogtoConfig };
package/lib/mock.d.ts CHANGED
@@ -66,7 +66,7 @@ export declare const createAdapters: (withCache?: boolean) => {
66
66
  generateCodeVerifier: jest.Mock<string, [], any>;
67
67
  generateState: jest.Mock<string, [], any>;
68
68
  };
69
- export declare const createClient: (prompt?: Prompt, storage?: MockedStorage, withCache?: boolean) => LogtoClientWithAccessors;
69
+ export declare const createClient: (prompt?: Prompt, storage?: MockedStorage, withCache?: boolean, scopes?: string[]) => LogtoClientWithAccessors;
70
70
  /**
71
71
  * Make protected fields accessible for test
72
72
  */
@@ -1,7 +1,28 @@
1
1
  'use strict';
2
2
 
3
3
  var js = require('@logto/js');
4
+ var essentials = require('@silverhand/essentials');
4
5
 
6
+ /**
7
+ * Normalize the Logto client configuration per the following rules:
8
+ *
9
+ * - Add default scopes (`openid`, `offline_access` and `profile`) if not provided.
10
+ * - Add {@link ReservedResource.Organization} to resources if {@link UserScope.Organizations} is included in scopes.
11
+ *
12
+ * @param config The Logto client configuration to be normalized.
13
+ * @returns The normalized Logto client configuration.
14
+ */
15
+ const normalizeLogtoConfig = (config) => {
16
+ const { prompt = js.Prompt.Consent, scopes = [], resources, ...rest } = config;
17
+ return {
18
+ ...rest,
19
+ prompt,
20
+ scopes: js.withDefaultScopes(scopes).split(' '),
21
+ resources: scopes.includes(js.UserScope.Organizations)
22
+ ? essentials.deduplicate([...(resources ?? []), js.ReservedResource.Organization])
23
+ : resources,
24
+ };
25
+ };
5
26
  const isLogtoSignInSessionItem = (data) => {
6
27
  if (!js.isArbitraryObject(data)) {
7
28
  return false;
@@ -24,3 +45,4 @@ const isLogtoAccessTokenMap = (data) => {
24
45
 
25
46
  exports.isLogtoAccessTokenMap = isLogtoAccessTokenMap;
26
47
  exports.isLogtoSignInSessionItem = isLogtoSignInSessionItem;
48
+ exports.normalizeLogtoConfig = normalizeLogtoConfig;
@@ -1,4 +1,4 @@
1
- import type { Prompt } from '@logto/js';
1
+ import { Prompt } from '@logto/js';
2
2
  /** The configuration object for the Logto client. */
3
3
  export type LogtoConfig = {
4
4
  /**
@@ -40,6 +40,16 @@ export type LogtoConfig = {
40
40
  */
41
41
  prompt?: Prompt;
42
42
  };
43
+ /**
44
+ * Normalize the Logto client configuration per the following rules:
45
+ *
46
+ * - Add default scopes (`openid`, `offline_access` and `profile`) if not provided.
47
+ * - Add {@link ReservedResource.Organization} to resources if {@link UserScope.Organizations} is included in scopes.
48
+ *
49
+ * @param config The Logto client configuration to be normalized.
50
+ * @returns The normalized Logto client configuration.
51
+ */
52
+ export declare const normalizeLogtoConfig: (config: LogtoConfig) => LogtoConfig;
43
53
  export type AccessToken = {
44
54
  /** The access token string. */
45
55
  token: string;
@@ -1,5 +1,26 @@
1
- import { isArbitraryObject } from '@logto/js';
1
+ import { Prompt, withDefaultScopes, UserScope, ReservedResource, isArbitraryObject } from '@logto/js';
2
+ import { deduplicate } from '@silverhand/essentials';
2
3
 
4
+ /**
5
+ * Normalize the Logto client configuration per the following rules:
6
+ *
7
+ * - Add default scopes (`openid`, `offline_access` and `profile`) if not provided.
8
+ * - Add {@link ReservedResource.Organization} to resources if {@link UserScope.Organizations} is included in scopes.
9
+ *
10
+ * @param config The Logto client configuration to be normalized.
11
+ * @returns The normalized Logto client configuration.
12
+ */
13
+ const normalizeLogtoConfig = (config) => {
14
+ const { prompt = Prompt.Consent, scopes = [], resources, ...rest } = config;
15
+ return {
16
+ ...rest,
17
+ prompt,
18
+ scopes: withDefaultScopes(scopes).split(' '),
19
+ resources: scopes.includes(UserScope.Organizations)
20
+ ? deduplicate([...(resources ?? []), ReservedResource.Organization])
21
+ : resources,
22
+ };
23
+ };
3
24
  const isLogtoSignInSessionItem = (data) => {
4
25
  if (!isArbitraryObject(data)) {
5
26
  return false;
@@ -20,4 +41,4 @@ const isLogtoAccessTokenMap = (data) => {
20
41
  });
21
42
  };
22
43
 
23
- export { isLogtoAccessTokenMap, isLogtoSignInSessionItem };
44
+ export { isLogtoAccessTokenMap, isLogtoSignInSessionItem, normalizeLogtoConfig };
@@ -0,0 +1 @@
1
+ export {};
@@ -1,8 +1,9 @@
1
1
  'use strict';
2
2
 
3
3
  var js = require('@logto/js');
4
+ var essentials = require('@silverhand/essentials');
4
5
 
5
- const buildAccessTokenKey = (resource = '', scopes = []) => `${scopes.slice().sort().join(' ')}@${resource}`;
6
+ const buildAccessTokenKey = (resource = '', organizationId, scopes = []) => `${scopes.slice().sort().join(' ')}@${resource}${essentials.conditionalString(organizationId && `#${organizationId}`)}`;
6
7
  const getDiscoveryEndpoint = (endpoint) => new URL(js.discoveryPath, endpoint).toString();
7
8
 
8
9
  exports.buildAccessTokenKey = buildAccessTokenKey;
@@ -1,3 +1,3 @@
1
1
  export * from './requester.js';
2
- export declare const buildAccessTokenKey: (resource?: string, scopes?: string[]) => string;
2
+ export declare const buildAccessTokenKey: (resource?: string, organizationId?: string, scopes?: string[]) => string;
3
3
  export declare const getDiscoveryEndpoint: (endpoint: string) => string;
@@ -1,6 +1,7 @@
1
1
  import { discoveryPath } from '@logto/js';
2
+ import { conditionalString } from '@silverhand/essentials';
2
3
 
3
- const buildAccessTokenKey = (resource = '', scopes = []) => `${scopes.slice().sort().join(' ')}@${resource}`;
4
+ const buildAccessTokenKey = (resource = '', organizationId, scopes = []) => `${scopes.slice().sort().join(' ')}@${resource}${conditionalString(organizationId && `#${organizationId}`)}`;
4
5
  const getDiscoveryEndpoint = (endpoint) => new URL(discoveryPath, endpoint).toString();
5
6
 
6
7
  export { buildAccessTokenKey, getDiscoveryEndpoint };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/client",
3
- "version": "2.2.3",
3
+ "version": "2.3.0",
4
4
  "type": "module",
5
5
  "main": "./lib/index.cjs",
6
6
  "module": "./lib/index.js",
@@ -21,10 +21,10 @@
21
21
  "directory": "packages/client"
22
22
  },
23
23
  "dependencies": {
24
- "@logto/js": "^2.1.3",
24
+ "@logto/js": "^3.0.0",
25
25
  "@silverhand/essentials": "^2.6.2",
26
26
  "camelcase-keys": "^7.0.1",
27
- "jose": "^4.13.2"
27
+ "jose": "^5.0.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "@silverhand/eslint-config": "^4.0.1",
@@ -36,7 +36,7 @@
36
36
  "eslint": "^8.44.0",
37
37
  "jest": "^29.5.0",
38
38
  "jest-matcher-specific-error": "^1.0.0",
39
- "lint-staged": "^14.0.0",
39
+ "lint-staged": "^15.0.0",
40
40
  "nock": "^13.3.0",
41
41
  "prettier": "^3.0.0",
42
42
  "text-encoder": "^0.0.4",