@logto/client 2.2.0 → 2.2.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.
@@ -22,13 +22,23 @@ export declare enum CacheKey {
22
22
  */
23
23
  Jwks = "jwks"
24
24
  }
25
+ /**
26
+ * The storage object that allows the client to persist data.
27
+ *
28
+ * It's compatible with the `localStorage` API.
29
+ */
25
30
  export type Storage<Keys extends string> = {
26
31
  getItem(key: Keys): Promise<Nullable<string>>;
27
32
  setItem(key: Keys, value: string): Promise<void>;
28
33
  removeItem(key: Keys): Promise<void>;
29
34
  };
30
35
  export type InferStorageKey<S> = S extends Storage<infer Key> ? Key : never;
31
- export type Navigate = (url: string) => void;
36
+ /** The navigation function that redirects the user to the specified URL. */
37
+ export type Navigate = (url: string) => void | Promise<void>;
38
+ /**
39
+ * The adapter object that allows the customizations of the client behavior
40
+ * for different environments.
41
+ */
32
42
  export type ClientAdapter = {
33
43
  requester: Requester;
34
44
  storage: Storage<StorageKey | PersistKey>;
@@ -39,7 +49,26 @@ export type ClientAdapter = {
39
49
  */
40
50
  unstable_cache?: Storage<CacheKey>;
41
51
  navigate: Navigate;
52
+ /**
53
+ * The function that generates a random state string.
54
+ *
55
+ * @returns The state string.
56
+ */
42
57
  generateState: () => string;
58
+ /**
59
+ * The function that generates a random code verifier string for PKCE.
60
+ *
61
+ * @see {@link https://www.rfc-editor.org/rfc/rfc7636.html| RFC 7636}
62
+ * @returns The code verifier string.
63
+ */
43
64
  generateCodeVerifier: () => string;
65
+ /**
66
+ * The function that generates a code challenge string based on the code verifier
67
+ * for PKCE.
68
+ *
69
+ * @see {@link https://www.rfc-editor.org/rfc/rfc7636.html| RFC 7636}
70
+ * @param codeVerifier The code verifier string.
71
+ * @returns The code challenge string.
72
+ */
44
73
  generateCodeChallenge: (codeVerifier: string) => Promise<string>;
45
74
  };
package/lib/errors.cjs CHANGED
@@ -5,10 +5,12 @@ const logtoClientErrorCodes = Object.freeze({
5
5
  'sign_in_session.not_found': 'Sign-in session not found.',
6
6
  not_authenticated: 'Not authenticated.',
7
7
  fetch_user_info_failed: 'Unable to fetch user info. The access token may be invalid.',
8
+ user_cancelled: 'The user cancelled the action.',
8
9
  });
9
10
  class LogtoClientError extends Error {
10
11
  constructor(code, data) {
11
12
  super(logtoClientErrorCodes[code]);
13
+ this.name = 'LogtoClientError';
12
14
  this.code = code;
13
15
  this.data = data;
14
16
  }
package/lib/errors.d.ts CHANGED
@@ -3,9 +3,11 @@ declare const logtoClientErrorCodes: Readonly<{
3
3
  'sign_in_session.not_found': "Sign-in session not found.";
4
4
  not_authenticated: "Not authenticated.";
5
5
  fetch_user_info_failed: "Unable to fetch user info. The access token may be invalid.";
6
+ user_cancelled: "The user cancelled the action.";
6
7
  }>;
7
8
  export type LogtoClientErrorCode = keyof typeof logtoClientErrorCodes;
8
9
  export declare class LogtoClientError extends Error {
10
+ name: string;
9
11
  code: LogtoClientErrorCode;
10
12
  data: unknown;
11
13
  constructor(code: LogtoClientErrorCode, data?: unknown);
package/lib/errors.js CHANGED
@@ -3,10 +3,12 @@ const logtoClientErrorCodes = Object.freeze({
3
3
  'sign_in_session.not_found': 'Sign-in session not found.',
4
4
  not_authenticated: 'Not authenticated.',
5
5
  fetch_user_info_failed: 'Unable to fetch user info. The access token may be invalid.',
6
+ user_cancelled: 'The user cancelled the action.',
6
7
  });
7
8
  class LogtoClientError extends Error {
8
9
  constructor(code, data) {
9
10
  super(logtoClientErrorCodes[code]);
11
+ this.name = 'LogtoClientError';
10
12
  this.code = code;
11
13
  this.data = data;
12
14
  }
package/lib/index.cjs CHANGED
@@ -14,6 +14,13 @@ 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
+ /**
18
+ * The Logto base client class that provides the essential methods for
19
+ * interacting with the Logto server.
20
+ *
21
+ * It also provides an adapter object that allows the customizations of the
22
+ * client behavior for different environments.
23
+ */
17
24
  class LogtoClient {
18
25
  constructor(logtoConfig, adapter) {
19
26
  this.getOidcConfig = memoize.memoize(this.#getOidcConfig);
@@ -27,15 +34,37 @@ class LogtoClient {
27
34
  this.adapter = new index$1.ClientAdapterInstance(adapter);
28
35
  void this.loadAccessTokenMap();
29
36
  }
37
+ /**
38
+ * Check if the user is authenticated by checking if the ID token exists.
39
+ */
30
40
  async isAuthenticated() {
31
41
  return Boolean(await this.getIdToken());
32
42
  }
43
+ /**
44
+ * Get the Refresh Token from the storage.
45
+ */
33
46
  async getRefreshToken() {
34
47
  return this.adapter.storage.getItem('refreshToken');
35
48
  }
49
+ /**
50
+ * Get the ID Token from the storage. If you want to get the ID Token claims,
51
+ * use {@link getIdTokenClaims} instead.
52
+ */
36
53
  async getIdToken() {
37
54
  return this.adapter.storage.getItem('idToken');
38
55
  }
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
+ */
39
68
  async getAccessToken(resource) {
40
69
  if (!(await this.getIdToken())) {
41
70
  throw new errors.LogtoClientError('not_authenticated');
@@ -54,6 +83,9 @@ class LogtoClient {
54
83
  */
55
84
  return this.getAccessTokenByRefreshToken(resource);
56
85
  }
86
+ /**
87
+ * Get the ID Token claims.
88
+ */
57
89
  async getIdTokenClaims() {
58
90
  const idToken = await this.getIdToken();
59
91
  if (!idToken) {
@@ -61,10 +93,27 @@ class LogtoClient {
61
93
  }
62
94
  return js.decodeIdToken(idToken);
63
95
  }
96
+ /**
97
+ * Get the Access Token claims for the specified resource.
98
+ *
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
101
+ * resource, as specified in the Logto Console.
102
+ */
64
103
  async getAccessTokenClaims(resource) {
65
104
  const accessToken = await this.getAccessToken(resource);
66
105
  return js.decodeAccessToken(accessToken);
67
106
  }
107
+ /**
108
+ * Get the user information from the Userinfo Endpoint.
109
+ *
110
+ * Note the Userinfo Endpoint will return more claims than the ID Token. See
111
+ * {@link https://docs.logto.io/docs/recipes/integrate-logto/vanilla-js/#fetch-user-information | Fetch user information}
112
+ * for more information.
113
+ *
114
+ * @returns The user information.
115
+ * @throws LogtoClientError if the user is not authenticated.
116
+ */
68
117
  async fetchUserInfo() {
69
118
  const { userinfoEndpoint } = await this.getOidcConfig();
70
119
  const accessToken = await this.getAccessToken();
@@ -73,6 +122,22 @@ class LogtoClient {
73
122
  }
74
123
  return js.fetchUserInfo(userinfoEndpoint, accessToken, this.adapter.requester);
75
124
  }
125
+ /**
126
+ * Start the sign-in flow with the specified redirect URI. The URI must be
127
+ * registered in the Logto Console.
128
+ *
129
+ * The user will be redirected to that URI after the sign-in flow is completed,
130
+ * and the client will be able to get the authorization code from the URI.
131
+ * To fetch the tokens from the authorization code, use {@link handleSignInCallback}
132
+ * after the user is redirected in the callback URI.
133
+ *
134
+ * @param redirectUri The redirect URI that the user will be redirected to after the sign-in flow is completed.
135
+ * @param interactionMode The interaction mode to be used for the authorization request. Note it's not
136
+ * a part of the OIDC standard, but a Logto-specific extension. Defaults to `signIn`.
137
+ *
138
+ * @see {@link https://docs.logto.io/docs/recipes/integrate-logto/vanilla-js/#sign-in | Sign in} for more information.
139
+ * @see {@link InteractionMode}
140
+ */
76
141
  async signIn(redirectUri, interactionMode) {
77
142
  const { appId: clientId, prompt, resources, scopes } = this.logtoConfig;
78
143
  const { authorizationEndpoint } = await this.getOidcConfig();
@@ -90,11 +155,21 @@ class LogtoClient {
90
155
  prompt,
91
156
  interactionMode,
92
157
  });
93
- await this.setSignInSession({ redirectUri, codeVerifier, state });
94
- await this.setRefreshToken(null);
95
- await this.setIdToken(null);
96
- this.adapter.navigate(signInUri);
158
+ await Promise.all([
159
+ this.setSignInSession({ redirectUri, codeVerifier, state }),
160
+ this.setRefreshToken(null),
161
+ this.setIdToken(null),
162
+ ]);
163
+ await this.adapter.navigate(signInUri);
97
164
  }
165
+ /**
166
+ * Check if the user is redirected from the sign-in page by checking if the
167
+ * current URL matches the redirect URI in the sign-in session.
168
+ *
169
+ * If there's no sign-in session, it will return `false`.
170
+ *
171
+ * @param url The current URL.
172
+ */
98
173
  async isSignInRedirected(url) {
99
174
  const signInSession = await this.getSignInSession();
100
175
  if (!signInSession) {
@@ -104,28 +179,59 @@ class LogtoClient {
104
179
  const { origin, pathname } = new URL(url);
105
180
  return `${origin}${pathname}` === redirectUri;
106
181
  }
182
+ /**
183
+ * Handle the sign-in callback by parsing the authorization code from the
184
+ * callback URI and exchanging it for the tokens.
185
+ *
186
+ * @param callbackUri The callback URI that the user is redirected to after the sign-in flow is completed.
187
+ * This URI must match the redirect URI specified in {@link signIn}.
188
+ * @throws LogtoClientError if the sign-in session is not found.
189
+ */
107
190
  async handleSignInCallback(callbackUri) {
108
- const { logtoConfig, adapter } = this;
109
- const { requester } = adapter;
191
+ const { requester } = this.adapter;
110
192
  const signInSession = await this.getSignInSession();
111
193
  if (!signInSession) {
112
194
  throw new errors.LogtoClientError('sign_in_session.not_found');
113
195
  }
114
196
  const { redirectUri, state, codeVerifier } = signInSession;
115
197
  const code = js.verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);
116
- const { appId: clientId } = logtoConfig;
198
+ // NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
199
+ const accessTokenKey = index$2.buildAccessTokenKey();
200
+ const { appId: clientId } = this.logtoConfig;
117
201
  const { tokenEndpoint } = await this.getOidcConfig();
118
- const codeTokenResponse = await js.fetchTokenByAuthorizationCode({
202
+ const requestedAt = Math.round(Date.now() / 1000);
203
+ const { idToken, refreshToken, accessToken, scope, expiresIn } = await js.fetchTokenByAuthorizationCode({
119
204
  clientId,
120
205
  tokenEndpoint,
121
206
  redirectUri,
122
207
  codeVerifier,
123
208
  code,
124
209
  }, requester);
125
- await this.verifyIdToken(codeTokenResponse.idToken);
126
- await this.saveCodeToken(codeTokenResponse);
210
+ await this.verifyIdToken(idToken);
211
+ await this.setRefreshToken(refreshToken ?? null);
212
+ await this.setIdToken(idToken);
213
+ this.accessTokenMap.set(accessTokenKey, {
214
+ token: accessToken,
215
+ scope,
216
+ /** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
217
+ * in the token claims. It is utilized by the client to determine if the cached access token
218
+ * has expired and when a new access token should be requested.
219
+ */
220
+ expiresAt: requestedAt + expiresIn,
221
+ });
222
+ await this.saveAccessTokenMap();
127
223
  await this.setSignInSession(null);
128
224
  }
225
+ /**
226
+ * Start the sign-out flow with the specified redirect URI. The URI must be
227
+ * registered in the Logto Console.
228
+ *
229
+ * It will also revoke all the tokens and clean up the storage.
230
+ *
231
+ * The user will be redirected that URI after the sign-out flow is completed.
232
+ * If the `postLogoutRedirectUri` is not specified, the user will be redirected
233
+ * to a default page.
234
+ */
129
235
  async signOut(postLogoutRedirectUri) {
130
236
  const { appId: clientId } = this.logtoConfig;
131
237
  const { endSessionEndpoint, revocationEndpoint } = await this.getOidcConfig();
@@ -144,10 +250,12 @@ class LogtoClient {
144
250
  clientId,
145
251
  });
146
252
  this.accessTokenMap.clear();
147
- await this.setRefreshToken(null);
148
- await this.setIdToken(null);
149
- await this.adapter.storage.removeItem('accessToken');
150
- this.adapter.navigate(url);
253
+ await Promise.all([
254
+ this.setRefreshToken(null),
255
+ this.setIdToken(null),
256
+ this.adapter.storage.removeItem('accessToken'),
257
+ ]);
258
+ await this.adapter.navigate(url);
151
259
  }
152
260
  async getSignInSession() {
153
261
  const jsonItem = await this.adapter.storage.getItem('signInSession');
@@ -177,6 +285,7 @@ class LogtoClient {
177
285
  const accessTokenKey = index$2.buildAccessTokenKey(resource);
178
286
  const { appId: clientId } = this.logtoConfig;
179
287
  const { tokenEndpoint } = await this.getOidcConfig();
288
+ const requestedAt = Math.round(Date.now() / 1000);
180
289
  const { accessToken, refreshToken, idToken, scope, expiresIn } = await js.fetchTokenByRefreshToken({
181
290
  clientId,
182
291
  tokenEndpoint,
@@ -186,7 +295,11 @@ class LogtoClient {
186
295
  this.accessTokenMap.set(accessTokenKey, {
187
296
  token: accessToken,
188
297
  scope,
189
- expiresAt: Math.round(Date.now() / 1000) + expiresIn,
298
+ /** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
299
+ * in the token claims. It is utilized by the client to determine if the cached access token
300
+ * has expired and when a new access token should be requested.
301
+ */
302
+ expiresAt: requestedAt + expiresIn,
190
303
  });
191
304
  await this.saveAccessTokenMap();
192
305
  await this.setRefreshToken(refreshToken);
@@ -202,15 +315,6 @@ class LogtoClient {
202
315
  const jwtVerifyGetKey = await this.getJwtVerifyGetKey();
203
316
  await js.verifyIdToken(idToken, appId, issuer, jwtVerifyGetKey);
204
317
  }
205
- async saveCodeToken({ refreshToken, idToken, scope, accessToken, expiresIn, }) {
206
- await this.setRefreshToken(refreshToken ?? null);
207
- await this.setIdToken(idToken);
208
- // NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
209
- const accessTokenKey = index$2.buildAccessTokenKey();
210
- const expiresAt = Date.now() / 1000 + expiresIn;
211
- this.accessTokenMap.set(accessTokenKey, { token: accessToken, scope, expiresAt });
212
- await this.saveAccessTokenMap();
213
- }
214
318
  async saveAccessTokenMap() {
215
319
  const data = {};
216
320
  for (const [key, accessToken] of this.accessTokenMap.entries()) {
package/lib/index.d.ts CHANGED
@@ -10,6 +10,13 @@ export type { Storage, StorageKey, ClientAdapter } from './adapter/index.js';
10
10
  export { PersistKey, CacheKey } from './adapter/index.js';
11
11
  export { createRequester } from './utils/index.js';
12
12
  export * from './types/index.js';
13
+ /**
14
+ * The Logto base client class that provides the essential methods for
15
+ * interacting with the Logto server.
16
+ *
17
+ * It also provides an adapter object that allows the customizations of the
18
+ * client behavior for different environments.
19
+ */
13
20
  export default class LogtoClient {
14
21
  #private;
15
22
  protected readonly logtoConfig: LogtoConfig;
@@ -26,16 +33,100 @@ export default class LogtoClient {
26
33
  protected readonly adapter: ClientAdapterInstance;
27
34
  protected readonly accessTokenMap: Map<string, AccessToken>;
28
35
  constructor(logtoConfig: LogtoConfig, adapter: ClientAdapter);
36
+ /**
37
+ * Check if the user is authenticated by checking if the ID token exists.
38
+ */
29
39
  isAuthenticated(): Promise<boolean>;
40
+ /**
41
+ * Get the Refresh Token from the storage.
42
+ */
30
43
  getRefreshToken(): Promise<Nullable<string>>;
44
+ /**
45
+ * Get the ID Token from the storage. If you want to get the ID Token claims,
46
+ * use {@link getIdTokenClaims} instead.
47
+ */
31
48
  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
+ */
32
61
  getAccessToken(resource?: string): Promise<string>;
62
+ /**
63
+ * Get the ID Token claims.
64
+ */
33
65
  getIdTokenClaims(): Promise<IdTokenClaims>;
66
+ /**
67
+ * Get the Access Token claims for the specified resource.
68
+ *
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
71
+ * resource, as specified in the Logto Console.
72
+ */
34
73
  getAccessTokenClaims(resource?: string): Promise<AccessTokenClaims>;
74
+ /**
75
+ * Get the user information from the Userinfo Endpoint.
76
+ *
77
+ * Note the Userinfo Endpoint will return more claims than the ID Token. See
78
+ * {@link https://docs.logto.io/docs/recipes/integrate-logto/vanilla-js/#fetch-user-information | Fetch user information}
79
+ * for more information.
80
+ *
81
+ * @returns The user information.
82
+ * @throws LogtoClientError if the user is not authenticated.
83
+ */
35
84
  fetchUserInfo(): Promise<UserInfoResponse>;
85
+ /**
86
+ * Start the sign-in flow with the specified redirect URI. The URI must be
87
+ * registered in the Logto Console.
88
+ *
89
+ * The user will be redirected to that URI after the sign-in flow is completed,
90
+ * and the client will be able to get the authorization code from the URI.
91
+ * To fetch the tokens from the authorization code, use {@link handleSignInCallback}
92
+ * after the user is redirected in the callback URI.
93
+ *
94
+ * @param redirectUri The redirect URI that the user will be redirected to after the sign-in flow is completed.
95
+ * @param interactionMode The interaction mode to be used for the authorization request. Note it's not
96
+ * a part of the OIDC standard, but a Logto-specific extension. Defaults to `signIn`.
97
+ *
98
+ * @see {@link https://docs.logto.io/docs/recipes/integrate-logto/vanilla-js/#sign-in | Sign in} for more information.
99
+ * @see {@link InteractionMode}
100
+ */
36
101
  signIn(redirectUri: string, interactionMode?: InteractionMode): Promise<void>;
102
+ /**
103
+ * Check if the user is redirected from the sign-in page by checking if the
104
+ * current URL matches the redirect URI in the sign-in session.
105
+ *
106
+ * If there's no sign-in session, it will return `false`.
107
+ *
108
+ * @param url The current URL.
109
+ */
37
110
  isSignInRedirected(url: string): Promise<boolean>;
111
+ /**
112
+ * Handle the sign-in callback by parsing the authorization code from the
113
+ * callback URI and exchanging it for the tokens.
114
+ *
115
+ * @param callbackUri The callback URI that the user is redirected to after the sign-in flow is completed.
116
+ * This URI must match the redirect URI specified in {@link signIn}.
117
+ * @throws LogtoClientError if the sign-in session is not found.
118
+ */
38
119
  handleSignInCallback(callbackUri: string): Promise<void>;
120
+ /**
121
+ * Start the sign-out flow with the specified redirect URI. The URI must be
122
+ * registered in the Logto Console.
123
+ *
124
+ * It will also revoke all the tokens and clean up the storage.
125
+ *
126
+ * The user will be redirected that URI after the sign-out flow is completed.
127
+ * If the `postLogoutRedirectUri` is not specified, the user will be redirected
128
+ * to a default page.
129
+ */
39
130
  signOut(postLogoutRedirectUri?: string): Promise<void>;
40
131
  protected getSignInSession(): Promise<Nullable<LogtoSignInSessionItem>>;
41
132
  protected setSignInSession(value: Nullable<LogtoSignInSessionItem>): Promise<void>;
@@ -43,7 +134,6 @@ export default class LogtoClient {
43
134
  private setRefreshToken;
44
135
  private getAccessTokenByRefreshToken;
45
136
  private verifyIdToken;
46
- private saveCodeToken;
47
137
  private saveAccessTokenMap;
48
138
  private loadAccessTokenMap;
49
139
  }
package/lib/index.js CHANGED
@@ -11,6 +11,13 @@ 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
+ /**
15
+ * The Logto base client class that provides the essential methods for
16
+ * interacting with the Logto server.
17
+ *
18
+ * It also provides an adapter object that allows the customizations of the
19
+ * client behavior for different environments.
20
+ */
14
21
  class LogtoClient {
15
22
  constructor(logtoConfig, adapter) {
16
23
  this.getOidcConfig = memoize(this.#getOidcConfig);
@@ -24,15 +31,37 @@ class LogtoClient {
24
31
  this.adapter = new ClientAdapterInstance(adapter);
25
32
  void this.loadAccessTokenMap();
26
33
  }
34
+ /**
35
+ * Check if the user is authenticated by checking if the ID token exists.
36
+ */
27
37
  async isAuthenticated() {
28
38
  return Boolean(await this.getIdToken());
29
39
  }
40
+ /**
41
+ * Get the Refresh Token from the storage.
42
+ */
30
43
  async getRefreshToken() {
31
44
  return this.adapter.storage.getItem('refreshToken');
32
45
  }
46
+ /**
47
+ * Get the ID Token from the storage. If you want to get the ID Token claims,
48
+ * use {@link getIdTokenClaims} instead.
49
+ */
33
50
  async getIdToken() {
34
51
  return this.adapter.storage.getItem('idToken');
35
52
  }
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
+ */
36
65
  async getAccessToken(resource) {
37
66
  if (!(await this.getIdToken())) {
38
67
  throw new LogtoClientError('not_authenticated');
@@ -51,6 +80,9 @@ class LogtoClient {
51
80
  */
52
81
  return this.getAccessTokenByRefreshToken(resource);
53
82
  }
83
+ /**
84
+ * Get the ID Token claims.
85
+ */
54
86
  async getIdTokenClaims() {
55
87
  const idToken = await this.getIdToken();
56
88
  if (!idToken) {
@@ -58,10 +90,27 @@ class LogtoClient {
58
90
  }
59
91
  return decodeIdToken(idToken);
60
92
  }
93
+ /**
94
+ * Get the Access Token claims for the specified resource.
95
+ *
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
98
+ * resource, as specified in the Logto Console.
99
+ */
61
100
  async getAccessTokenClaims(resource) {
62
101
  const accessToken = await this.getAccessToken(resource);
63
102
  return decodeAccessToken(accessToken);
64
103
  }
104
+ /**
105
+ * Get the user information from the Userinfo Endpoint.
106
+ *
107
+ * Note the Userinfo Endpoint will return more claims than the ID Token. See
108
+ * {@link https://docs.logto.io/docs/recipes/integrate-logto/vanilla-js/#fetch-user-information | Fetch user information}
109
+ * for more information.
110
+ *
111
+ * @returns The user information.
112
+ * @throws LogtoClientError if the user is not authenticated.
113
+ */
65
114
  async fetchUserInfo() {
66
115
  const { userinfoEndpoint } = await this.getOidcConfig();
67
116
  const accessToken = await this.getAccessToken();
@@ -70,6 +119,22 @@ class LogtoClient {
70
119
  }
71
120
  return fetchUserInfo(userinfoEndpoint, accessToken, this.adapter.requester);
72
121
  }
122
+ /**
123
+ * Start the sign-in flow with the specified redirect URI. The URI must be
124
+ * registered in the Logto Console.
125
+ *
126
+ * The user will be redirected to that URI after the sign-in flow is completed,
127
+ * and the client will be able to get the authorization code from the URI.
128
+ * To fetch the tokens from the authorization code, use {@link handleSignInCallback}
129
+ * after the user is redirected in the callback URI.
130
+ *
131
+ * @param redirectUri The redirect URI that the user will be redirected to after the sign-in flow is completed.
132
+ * @param interactionMode The interaction mode to be used for the authorization request. Note it's not
133
+ * a part of the OIDC standard, but a Logto-specific extension. Defaults to `signIn`.
134
+ *
135
+ * @see {@link https://docs.logto.io/docs/recipes/integrate-logto/vanilla-js/#sign-in | Sign in} for more information.
136
+ * @see {@link InteractionMode}
137
+ */
73
138
  async signIn(redirectUri, interactionMode) {
74
139
  const { appId: clientId, prompt, resources, scopes } = this.logtoConfig;
75
140
  const { authorizationEndpoint } = await this.getOidcConfig();
@@ -87,11 +152,21 @@ class LogtoClient {
87
152
  prompt,
88
153
  interactionMode,
89
154
  });
90
- await this.setSignInSession({ redirectUri, codeVerifier, state });
91
- await this.setRefreshToken(null);
92
- await this.setIdToken(null);
93
- this.adapter.navigate(signInUri);
155
+ await Promise.all([
156
+ this.setSignInSession({ redirectUri, codeVerifier, state }),
157
+ this.setRefreshToken(null),
158
+ this.setIdToken(null),
159
+ ]);
160
+ await this.adapter.navigate(signInUri);
94
161
  }
162
+ /**
163
+ * Check if the user is redirected from the sign-in page by checking if the
164
+ * current URL matches the redirect URI in the sign-in session.
165
+ *
166
+ * If there's no sign-in session, it will return `false`.
167
+ *
168
+ * @param url The current URL.
169
+ */
95
170
  async isSignInRedirected(url) {
96
171
  const signInSession = await this.getSignInSession();
97
172
  if (!signInSession) {
@@ -101,28 +176,59 @@ class LogtoClient {
101
176
  const { origin, pathname } = new URL(url);
102
177
  return `${origin}${pathname}` === redirectUri;
103
178
  }
179
+ /**
180
+ * Handle the sign-in callback by parsing the authorization code from the
181
+ * callback URI and exchanging it for the tokens.
182
+ *
183
+ * @param callbackUri The callback URI that the user is redirected to after the sign-in flow is completed.
184
+ * This URI must match the redirect URI specified in {@link signIn}.
185
+ * @throws LogtoClientError if the sign-in session is not found.
186
+ */
104
187
  async handleSignInCallback(callbackUri) {
105
- const { logtoConfig, adapter } = this;
106
- const { requester } = adapter;
188
+ const { requester } = this.adapter;
107
189
  const signInSession = await this.getSignInSession();
108
190
  if (!signInSession) {
109
191
  throw new LogtoClientError('sign_in_session.not_found');
110
192
  }
111
193
  const { redirectUri, state, codeVerifier } = signInSession;
112
194
  const code = verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);
113
- const { appId: clientId } = logtoConfig;
195
+ // NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
196
+ const accessTokenKey = buildAccessTokenKey();
197
+ const { appId: clientId } = this.logtoConfig;
114
198
  const { tokenEndpoint } = await this.getOidcConfig();
115
- const codeTokenResponse = await fetchTokenByAuthorizationCode({
199
+ const requestedAt = Math.round(Date.now() / 1000);
200
+ const { idToken, refreshToken, accessToken, scope, expiresIn } = await fetchTokenByAuthorizationCode({
116
201
  clientId,
117
202
  tokenEndpoint,
118
203
  redirectUri,
119
204
  codeVerifier,
120
205
  code,
121
206
  }, requester);
122
- await this.verifyIdToken(codeTokenResponse.idToken);
123
- await this.saveCodeToken(codeTokenResponse);
207
+ await this.verifyIdToken(idToken);
208
+ await this.setRefreshToken(refreshToken ?? null);
209
+ await this.setIdToken(idToken);
210
+ this.accessTokenMap.set(accessTokenKey, {
211
+ token: accessToken,
212
+ scope,
213
+ /** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
214
+ * in the token claims. It is utilized by the client to determine if the cached access token
215
+ * has expired and when a new access token should be requested.
216
+ */
217
+ expiresAt: requestedAt + expiresIn,
218
+ });
219
+ await this.saveAccessTokenMap();
124
220
  await this.setSignInSession(null);
125
221
  }
222
+ /**
223
+ * Start the sign-out flow with the specified redirect URI. The URI must be
224
+ * registered in the Logto Console.
225
+ *
226
+ * It will also revoke all the tokens and clean up the storage.
227
+ *
228
+ * The user will be redirected that URI after the sign-out flow is completed.
229
+ * If the `postLogoutRedirectUri` is not specified, the user will be redirected
230
+ * to a default page.
231
+ */
126
232
  async signOut(postLogoutRedirectUri) {
127
233
  const { appId: clientId } = this.logtoConfig;
128
234
  const { endSessionEndpoint, revocationEndpoint } = await this.getOidcConfig();
@@ -141,10 +247,12 @@ class LogtoClient {
141
247
  clientId,
142
248
  });
143
249
  this.accessTokenMap.clear();
144
- await this.setRefreshToken(null);
145
- await this.setIdToken(null);
146
- await this.adapter.storage.removeItem('accessToken');
147
- this.adapter.navigate(url);
250
+ await Promise.all([
251
+ this.setRefreshToken(null),
252
+ this.setIdToken(null),
253
+ this.adapter.storage.removeItem('accessToken'),
254
+ ]);
255
+ await this.adapter.navigate(url);
148
256
  }
149
257
  async getSignInSession() {
150
258
  const jsonItem = await this.adapter.storage.getItem('signInSession');
@@ -174,6 +282,7 @@ class LogtoClient {
174
282
  const accessTokenKey = buildAccessTokenKey(resource);
175
283
  const { appId: clientId } = this.logtoConfig;
176
284
  const { tokenEndpoint } = await this.getOidcConfig();
285
+ const requestedAt = Math.round(Date.now() / 1000);
177
286
  const { accessToken, refreshToken, idToken, scope, expiresIn } = await fetchTokenByRefreshToken({
178
287
  clientId,
179
288
  tokenEndpoint,
@@ -183,7 +292,11 @@ class LogtoClient {
183
292
  this.accessTokenMap.set(accessTokenKey, {
184
293
  token: accessToken,
185
294
  scope,
186
- expiresAt: Math.round(Date.now() / 1000) + expiresIn,
295
+ /** The `expiresAt` variable provides an approximate estimation of the actual `exp` property
296
+ * in the token claims. It is utilized by the client to determine if the cached access token
297
+ * has expired and when a new access token should be requested.
298
+ */
299
+ expiresAt: requestedAt + expiresIn,
187
300
  });
188
301
  await this.saveAccessTokenMap();
189
302
  await this.setRefreshToken(refreshToken);
@@ -199,15 +312,6 @@ class LogtoClient {
199
312
  const jwtVerifyGetKey = await this.getJwtVerifyGetKey();
200
313
  await verifyIdToken(idToken, appId, issuer, jwtVerifyGetKey);
201
314
  }
202
- async saveCodeToken({ refreshToken, idToken, scope, accessToken, expiresIn, }) {
203
- await this.setRefreshToken(refreshToken ?? null);
204
- await this.setIdToken(idToken);
205
- // NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
206
- const accessTokenKey = buildAccessTokenKey();
207
- const expiresAt = Date.now() / 1000 + expiresIn;
208
- this.accessTokenMap.set(accessTokenKey, { token: accessToken, scope, expiresAt });
209
- await this.saveAccessTokenMap();
210
- }
211
315
  async saveAccessTokenMap() {
212
316
  const data = {};
213
317
  for (const [key, accessToken] of this.accessTokenMap.entries()) {
@@ -1,15 +1,51 @@
1
1
  import type { Prompt } from '@logto/js';
2
+ /** The configuration object for the Logto client. */
2
3
  export type LogtoConfig = {
4
+ /**
5
+ * The endpoint for the Logto server, you can get it from the integration guide
6
+ * or the team settings page of the Logto Console.
7
+ *
8
+ * @example https://foo.logto.app
9
+ */
3
10
  endpoint: string;
11
+ /**
12
+ * The client ID of your application, you can get it from the integration guide
13
+ * or the application details page of the Logto Console.
14
+ */
4
15
  appId: string;
16
+ /**
17
+ * The client secret of your application, you can get it from the application
18
+ * details page of the Logto Console.
19
+ */
5
20
  appSecret?: string;
21
+ /**
22
+ * The scopes (permissions) that your application needs to access.
23
+ * Scopes that will be added by default: `openid`, `offline_access` and `profile`.
24
+ *
25
+ * If resources are specified, scopes will be applied to every resource.
26
+ *
27
+ * @see {@link https://docs.logto.io/docs/recipes/integrate-logto/vanilla-js/#fetch-user-information | Fetch user information}
28
+ * for more information of available scopes for user information.
29
+ */
6
30
  scopes?: string[];
31
+ /**
32
+ * The API resources that your application needs to access. You can specify
33
+ * multiple resources by providing an array of strings.
34
+ *
35
+ * @see {@link https://docs.logto.io/docs/recipes/rbac/ | RBAC} to learn more about how to use role-based access control (RBAC) to protect API resources.
36
+ */
7
37
  resources?: string[];
38
+ /**
39
+ * The prompt parameter to be used for the authorization request.
40
+ */
8
41
  prompt?: Prompt;
9
42
  };
10
43
  export type AccessToken = {
44
+ /** The access token string. */
11
45
  token: string;
46
+ /** The scopes that the access token is granted for. */
12
47
  scope: string;
48
+ /** The timestamp of the access token expiration. */
13
49
  expiresAt: number;
14
50
  };
15
51
  export declare const isLogtoSignInSessionItem: (data: unknown) => data is LogtoSignInSessionItem;
@@ -2,6 +2,13 @@
2
2
 
3
3
  var js = require('@logto/js');
4
4
 
5
+ /**
6
+ * A factory function that creates a requester by accepting a `fetch`-like function.
7
+ *
8
+ * @param fetchFunction A `fetch`-like function.
9
+ * @returns A requester function.
10
+ * @see {@link Requester}
11
+ */
5
12
  const createRequester = (fetchFunction) => {
6
13
  return async (...args) => {
7
14
  const response = await fetchFunction(...args);
@@ -1,2 +1,9 @@
1
1
  import type { Requester } from '@logto/js';
2
+ /**
3
+ * A factory function that creates a requester by accepting a `fetch`-like function.
4
+ *
5
+ * @param fetchFunction A `fetch`-like function.
6
+ * @returns A requester function.
7
+ * @see {@link Requester}
8
+ */
2
9
  export declare const createRequester: (fetchFunction: typeof fetch) => Requester;
@@ -1,5 +1,12 @@
1
1
  import { isLogtoRequestError, LogtoError, LogtoRequestError } from '@logto/js';
2
2
 
3
+ /**
4
+ * A factory function that creates a requester by accepting a `fetch`-like function.
5
+ *
6
+ * @param fetchFunction A `fetch`-like function.
7
+ * @returns A requester function.
8
+ * @see {@link Requester}
9
+ */
3
10
  const createRequester = (fetchFunction) => {
4
11
  return async (...args) => {
5
12
  const response = await fetchFunction(...args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@logto/client",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "type": "module",
5
5
  "main": "./lib/index.cjs",
6
6
  "module": "./lib/index.js",
@@ -21,24 +21,24 @@
21
21
  "directory": "packages/client"
22
22
  },
23
23
  "dependencies": {
24
- "@logto/js": "^2.1.0",
24
+ "@logto/js": "^2.1.2",
25
25
  "@silverhand/essentials": "^2.6.2",
26
26
  "camelcase-keys": "^7.0.1",
27
27
  "jose": "^4.13.2"
28
28
  },
29
29
  "devDependencies": {
30
- "@silverhand/eslint-config": "^3.0.1",
31
- "@silverhand/ts-config": "^3.0.0",
30
+ "@silverhand/eslint-config": "^4.0.1",
31
+ "@silverhand/ts-config": "^4.0.0",
32
32
  "@swc/core": "^1.3.50",
33
33
  "@swc/jest": "^0.2.24",
34
34
  "@types/jest": "^29.5.0",
35
35
  "@types/node": "^18.0.0",
36
- "eslint": "^8.38.0",
36
+ "eslint": "^8.44.0",
37
37
  "jest": "^29.5.0",
38
38
  "jest-matcher-specific-error": "^1.0.0",
39
39
  "lint-staged": "^13.0.0",
40
40
  "nock": "^13.3.0",
41
- "prettier": "^2.8.7",
41
+ "prettier": "^3.0.0",
42
42
  "text-encoder": "^0.0.4",
43
43
  "type-fest": "^3.0.0",
44
44
  "typescript": "^5.0.0"