@logto/client 2.3.0 → 2.3.2

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/index.cjs CHANGED
@@ -24,7 +24,11 @@ var requester = require('./utils/requester.cjs');
24
24
  */
25
25
  class LogtoClient {
26
26
  constructor(logtoConfig, adapter) {
27
- this.getOidcConfig = memoize.memoize(this.#getOidcConfig);
27
+ /**
28
+ * Get the OIDC configuration from the discovery endpoint. This method will
29
+ * only fetch the configuration once and cache the result.
30
+ */
31
+ this.getOidcConfig = once.once(this.#getOidcConfig);
28
32
  /**
29
33
  * Get the access token from the storage with refresh strategy.
30
34
  *
@@ -53,6 +57,16 @@ class LogtoClient {
53
57
  * It uses the same refresh strategy as {@link getAccessToken}.
54
58
  */
55
59
  this.getOrganizationToken = memoize.memoize(this.#getOrganizationToken);
60
+ /**
61
+ * Handle the sign-in callback by parsing the authorization code from the
62
+ * callback URI and exchanging it for the tokens.
63
+ *
64
+ * @param callbackUri The callback URI, including the search params, that the user is redirected to after the sign-in flow is completed.
65
+ * The origin and pathname of this URI must match the origin and pathname of the redirect URI specified in {@link signIn}.
66
+ * In many cases you'll probably end up passing `window.location.href` as the argument to this function.
67
+ * @throws LogtoClientError if the sign-in session is not found.
68
+ */
69
+ this.handleSignInCallback = memoize.memoize(this.#handleSignInCallback);
56
70
  this.getJwtVerifyGetKey = once.once(this.#getJwtVerifyGetKey);
57
71
  this.accessTokenMap = new Map();
58
72
  this.logtoConfig = index.normalizeLogtoConfig(logtoConfig);
@@ -183,50 +197,6 @@ class LogtoClient {
183
197
  const { origin, pathname } = new URL(url);
184
198
  return `${origin}${pathname}` === redirectUri;
185
199
  }
186
- /**
187
- * Handle the sign-in callback by parsing the authorization code from the
188
- * callback URI and exchanging it for the tokens.
189
- *
190
- * @param callbackUri The callback URI, including the search params, that the user is redirected to after the sign-in flow is completed.
191
- * The origin and pathname of this URI must match the origin and pathname of the redirect URI specified in {@link signIn}.
192
- * In many cases you'll probably end up passing `window.location.href` as the argument to this function.
193
- * @throws LogtoClientError if the sign-in session is not found.
194
- */
195
- async handleSignInCallback(callbackUri) {
196
- const { requester } = this.adapter;
197
- const signInSession = await this.getSignInSession();
198
- if (!signInSession) {
199
- throw new errors.LogtoClientError('sign_in_session.not_found');
200
- }
201
- const { redirectUri, state, codeVerifier } = signInSession;
202
- const code = js.verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);
203
- // NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
204
- const accessTokenKey = index$2.buildAccessTokenKey();
205
- const { appId: clientId } = this.logtoConfig;
206
- const { tokenEndpoint } = await this.getOidcConfig();
207
- const requestedAt = Math.round(Date.now() / 1000);
208
- const { idToken, refreshToken, accessToken, scope, expiresIn } = await js.fetchTokenByAuthorizationCode({
209
- clientId,
210
- tokenEndpoint,
211
- redirectUri,
212
- codeVerifier,
213
- code,
214
- }, requester);
215
- await this.verifyIdToken(idToken);
216
- await this.setRefreshToken(refreshToken ?? null);
217
- await this.setIdToken(idToken);
218
- this.accessTokenMap.set(accessTokenKey, {
219
- token: accessToken,
220
- scope,
221
- /** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
222
- * in the token claims. It is utilized by the client to determine if the cached access token
223
- * has expired and when a new access token should be requested.
224
- */
225
- expiresAt: requestedAt + expiresIn,
226
- });
227
- await this.saveAccessTokenMap();
228
- await this.setSignInSession(null);
229
- }
230
200
  /**
231
201
  * Start the sign-out flow with the specified redirect URI. The URI must be
232
202
  * registered in the Logto Console.
@@ -385,7 +355,42 @@ class LogtoClient {
385
355
  if (!this.logtoConfig.scopes?.includes(js.UserScope.Organizations)) {
386
356
  throw new errors.LogtoClientError('missing_scope_organizations');
387
357
  }
388
- return this.#getAccessToken(undefined, organizationId);
358
+ return this.getAccessToken(undefined, organizationId);
359
+ }
360
+ async #handleSignInCallback(callbackUri) {
361
+ const { requester } = this.adapter;
362
+ const signInSession = await this.getSignInSession();
363
+ if (!signInSession) {
364
+ throw new errors.LogtoClientError('sign_in_session.not_found');
365
+ }
366
+ const { redirectUri, state, codeVerifier } = signInSession;
367
+ const code = js.verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);
368
+ // NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
369
+ const accessTokenKey = index$2.buildAccessTokenKey();
370
+ const { appId: clientId } = this.logtoConfig;
371
+ const { tokenEndpoint } = await this.getOidcConfig();
372
+ const requestedAt = Math.round(Date.now() / 1000);
373
+ const { idToken, refreshToken, accessToken, scope, expiresIn } = await js.fetchTokenByAuthorizationCode({
374
+ clientId,
375
+ tokenEndpoint,
376
+ redirectUri,
377
+ codeVerifier,
378
+ code,
379
+ }, requester);
380
+ await this.verifyIdToken(idToken);
381
+ await this.setRefreshToken(refreshToken ?? null);
382
+ await this.setIdToken(idToken);
383
+ this.accessTokenMap.set(accessTokenKey, {
384
+ token: accessToken,
385
+ scope,
386
+ /** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
387
+ * in the token claims. It is utilized by the client to determine if the cached access token
388
+ * has expired and when a new access token should be requested.
389
+ */
390
+ expiresAt: requestedAt + expiresIn,
391
+ });
392
+ await this.saveAccessTokenMap();
393
+ await this.setSignInSession(null);
389
394
  }
390
395
  }
391
396
  /* eslint-enable max-lines */
package/lib/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type IdTokenClaims, type UserInfoResponse, type InteractionMode, type AccessTokenClaims } from '@logto/js';
1
+ import { type IdTokenClaims, type UserInfoResponse, type InteractionMode, type AccessTokenClaims, type OidcConfigResponse } from '@logto/js';
2
2
  import { type Nullable } from '@silverhand/essentials';
3
3
  import { type JWTVerifyGetKey } from 'jose';
4
4
  import { ClientAdapterInstance, type ClientAdapter } from './adapter/index.js';
@@ -20,15 +20,11 @@ export * from './types/index.js';
20
20
  export default class LogtoClient {
21
21
  #private;
22
22
  readonly logtoConfig: LogtoConfig;
23
- readonly getOidcConfig: (this: unknown) => Promise<import("@silverhand/essentials").KeysToCamelCase<{
24
- authorization_endpoint: string;
25
- token_endpoint: string;
26
- userinfo_endpoint: string;
27
- end_session_endpoint: string;
28
- revocation_endpoint: string;
29
- jwks_uri: string;
30
- issuer: string;
31
- }>>;
23
+ /**
24
+ * Get the OIDC configuration from the discovery endpoint. This method will
25
+ * only fetch the configuration once and cache the result.
26
+ */
27
+ readonly getOidcConfig: () => Promise<OidcConfigResponse>;
32
28
  /**
33
29
  * Get the access token from the storage with refresh strategy.
34
30
  *
@@ -57,6 +53,16 @@ export default class LogtoClient {
57
53
  * It uses the same refresh strategy as {@link getAccessToken}.
58
54
  */
59
55
  readonly getOrganizationToken: (this: unknown, organizationId: string) => Promise<string>;
56
+ /**
57
+ * Handle the sign-in callback by parsing the authorization code from the
58
+ * callback URI and exchanging it for the tokens.
59
+ *
60
+ * @param callbackUri The callback URI, including the search params, that the user is redirected to after the sign-in flow is completed.
61
+ * The origin and pathname of this URI must match the origin and pathname of the redirect URI specified in {@link signIn}.
62
+ * In many cases you'll probably end up passing `window.location.href` as the argument to this function.
63
+ * @throws LogtoClientError if the sign-in session is not found.
64
+ */
65
+ readonly handleSignInCallback: (this: unknown, callbackUri: string) => Promise<void>;
60
66
  protected readonly getJwtVerifyGetKey: (...args: unknown[]) => Promise<JWTVerifyGetKey>;
61
67
  protected readonly adapter: ClientAdapterInstance;
62
68
  protected readonly accessTokenMap: Map<string, AccessToken>;
@@ -129,16 +135,6 @@ export default class LogtoClient {
129
135
  * @param url The current URL.
130
136
  */
131
137
  isSignInRedirected(url: string): Promise<boolean>;
132
- /**
133
- * Handle the sign-in callback by parsing the authorization code from the
134
- * callback URI and exchanging it for the tokens.
135
- *
136
- * @param callbackUri The callback URI, including the search params, that the user is redirected to after the sign-in flow is completed.
137
- * The origin and pathname of this URI must match the origin and pathname of the redirect URI specified in {@link signIn}.
138
- * In many cases you'll probably end up passing `window.location.href` as the argument to this function.
139
- * @throws LogtoClientError if the sign-in session is not found.
140
- */
141
- handleSignInCallback(callbackUri: string): Promise<void>;
142
138
  /**
143
139
  * Start the sign-out flow with the specified redirect URI. The URI must be
144
140
  * registered in the Logto Console.
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { decodeIdToken, decodeAccessToken, fetchUserInfo, generateSignInUri, verifyAndParseCodeFromCallbackUri, fetchTokenByAuthorizationCode, revoke, generateSignOutUri, fetchTokenByRefreshToken, verifyIdToken, fetchOidcConfig, UserScope } from '@logto/js';
1
+ import { decodeIdToken, decodeAccessToken, fetchUserInfo, generateSignInUri, revoke, generateSignOutUri, fetchTokenByRefreshToken, verifyIdToken, fetchOidcConfig, UserScope, verifyAndParseCodeFromCallbackUri, fetchTokenByAuthorizationCode } from '@logto/js';
2
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';
@@ -21,7 +21,11 @@ export { createRequester } from './utils/requester.js';
21
21
  */
22
22
  class LogtoClient {
23
23
  constructor(logtoConfig, adapter) {
24
- this.getOidcConfig = memoize(this.#getOidcConfig);
24
+ /**
25
+ * Get the OIDC configuration from the discovery endpoint. This method will
26
+ * only fetch the configuration once and cache the result.
27
+ */
28
+ this.getOidcConfig = once(this.#getOidcConfig);
25
29
  /**
26
30
  * Get the access token from the storage with refresh strategy.
27
31
  *
@@ -50,6 +54,16 @@ class LogtoClient {
50
54
  * It uses the same refresh strategy as {@link getAccessToken}.
51
55
  */
52
56
  this.getOrganizationToken = memoize(this.#getOrganizationToken);
57
+ /**
58
+ * Handle the sign-in callback by parsing the authorization code from the
59
+ * callback URI and exchanging it for the tokens.
60
+ *
61
+ * @param callbackUri The callback URI, including the search params, that the user is redirected to after the sign-in flow is completed.
62
+ * The origin and pathname of this URI must match the origin and pathname of the redirect URI specified in {@link signIn}.
63
+ * In many cases you'll probably end up passing `window.location.href` as the argument to this function.
64
+ * @throws LogtoClientError if the sign-in session is not found.
65
+ */
66
+ this.handleSignInCallback = memoize(this.#handleSignInCallback);
53
67
  this.getJwtVerifyGetKey = once(this.#getJwtVerifyGetKey);
54
68
  this.accessTokenMap = new Map();
55
69
  this.logtoConfig = normalizeLogtoConfig(logtoConfig);
@@ -180,50 +194,6 @@ class LogtoClient {
180
194
  const { origin, pathname } = new URL(url);
181
195
  return `${origin}${pathname}` === redirectUri;
182
196
  }
183
- /**
184
- * Handle the sign-in callback by parsing the authorization code from the
185
- * callback URI and exchanging it for the tokens.
186
- *
187
- * @param callbackUri The callback URI, including the search params, that the user is redirected to after the sign-in flow is completed.
188
- * The origin and pathname of this URI must match the origin and pathname of the redirect URI specified in {@link signIn}.
189
- * In many cases you'll probably end up passing `window.location.href` as the argument to this function.
190
- * @throws LogtoClientError if the sign-in session is not found.
191
- */
192
- async handleSignInCallback(callbackUri) {
193
- const { requester } = this.adapter;
194
- const signInSession = await this.getSignInSession();
195
- if (!signInSession) {
196
- throw new LogtoClientError('sign_in_session.not_found');
197
- }
198
- const { redirectUri, state, codeVerifier } = signInSession;
199
- const code = verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);
200
- // NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
201
- const accessTokenKey = buildAccessTokenKey();
202
- const { appId: clientId } = this.logtoConfig;
203
- const { tokenEndpoint } = await this.getOidcConfig();
204
- const requestedAt = Math.round(Date.now() / 1000);
205
- const { idToken, refreshToken, accessToken, scope, expiresIn } = await fetchTokenByAuthorizationCode({
206
- clientId,
207
- tokenEndpoint,
208
- redirectUri,
209
- codeVerifier,
210
- code,
211
- }, requester);
212
- await this.verifyIdToken(idToken);
213
- await this.setRefreshToken(refreshToken ?? null);
214
- await this.setIdToken(idToken);
215
- this.accessTokenMap.set(accessTokenKey, {
216
- token: accessToken,
217
- scope,
218
- /** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
219
- * in the token claims. It is utilized by the client to determine if the cached access token
220
- * has expired and when a new access token should be requested.
221
- */
222
- expiresAt: requestedAt + expiresIn,
223
- });
224
- await this.saveAccessTokenMap();
225
- await this.setSignInSession(null);
226
- }
227
197
  /**
228
198
  * Start the sign-out flow with the specified redirect URI. The URI must be
229
199
  * registered in the Logto Console.
@@ -382,7 +352,42 @@ class LogtoClient {
382
352
  if (!this.logtoConfig.scopes?.includes(UserScope.Organizations)) {
383
353
  throw new LogtoClientError('missing_scope_organizations');
384
354
  }
385
- return this.#getAccessToken(undefined, organizationId);
355
+ return this.getAccessToken(undefined, organizationId);
356
+ }
357
+ async #handleSignInCallback(callbackUri) {
358
+ const { requester } = this.adapter;
359
+ const signInSession = await this.getSignInSession();
360
+ if (!signInSession) {
361
+ throw new LogtoClientError('sign_in_session.not_found');
362
+ }
363
+ const { redirectUri, state, codeVerifier } = signInSession;
364
+ const code = verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);
365
+ // NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
366
+ const accessTokenKey = buildAccessTokenKey();
367
+ const { appId: clientId } = this.logtoConfig;
368
+ const { tokenEndpoint } = await this.getOidcConfig();
369
+ const requestedAt = Math.round(Date.now() / 1000);
370
+ const { idToken, refreshToken, accessToken, scope, expiresIn } = await fetchTokenByAuthorizationCode({
371
+ clientId,
372
+ tokenEndpoint,
373
+ redirectUri,
374
+ codeVerifier,
375
+ code,
376
+ }, requester);
377
+ await this.verifyIdToken(idToken);
378
+ await this.setRefreshToken(refreshToken ?? null);
379
+ await this.setIdToken(idToken);
380
+ this.accessTokenMap.set(accessTokenKey, {
381
+ token: accessToken,
382
+ scope,
383
+ /** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
384
+ * in the token claims. It is utilized by the client to determine if the cached access token
385
+ * has expired and when a new access token should be requested.
386
+ */
387
+ expiresAt: requestedAt + expiresIn,
388
+ });
389
+ await this.saveAccessTokenMap();
390
+ await this.setSignInSession(null);
386
391
  }
387
392
  }
388
393
  /* eslint-enable max-lines */
package/lib/mock.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /// <reference types="jest" />
2
- import { Prompt } from '@logto/js';
2
+ import { type OidcConfigResponse, Prompt } from '@logto/js';
3
3
  import { type Nullable } from '@silverhand/essentials';
4
4
  import type { Storage } from './adapter/index.js';
5
5
  import type { AccessToken, LogtoConfig, LogtoSignInSessionItem } from './index.js';
@@ -71,18 +71,10 @@ export declare const createClient: (prompt?: Prompt, storage?: MockedStorage, wi
71
71
  * Make protected fields accessible for test
72
72
  */
73
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
- }>>;
74
+ runGetOidcConfig(): Promise<OidcConfigResponse>;
83
75
  runGetJwtVerifyGetKey(): Promise<import("jose").JWTVerifyGetKey>;
84
76
  getLogtoConfig(): Nullable<LogtoConfig>;
85
- getSignInSessionItem(): Promise<Nullable<LogtoSignInSessionItem>>;
77
+ getSignInSession(): Promise<Nullable<LogtoSignInSessionItem>>;
86
78
  setSignInSessionItem(item: Nullable<LogtoSignInSessionItem>): Promise<void>;
87
79
  getAccessTokenMap(): Map<string, AccessToken>;
88
80
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/client",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "type": "module",
5
5
  "main": "./lib/index.cjs",
6
6
  "module": "./lib/index.js",
@@ -27,8 +27,8 @@
27
27
  "jose": "^5.0.0"
28
28
  },
29
29
  "devDependencies": {
30
- "@silverhand/eslint-config": "^4.0.1",
31
- "@silverhand/ts-config": "^4.0.0",
30
+ "@silverhand/eslint-config": "^5.0.0",
31
+ "@silverhand/ts-config": "^5.0.0",
32
32
  "@swc/core": "^1.3.50",
33
33
  "@swc/jest": "^0.2.24",
34
34
  "@types/jest": "^29.5.0",