@logto/client 1.1.1 → 2.0.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,17 @@
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
+ };
package/lib/errors.cjs ADDED
@@ -0,0 +1,17 @@
1
+ 'use strict';
2
+
3
+ const logtoClientErrorCodes = Object.freeze({
4
+ 'sign_in_session.invalid': 'Invalid sign-in session.',
5
+ 'sign_in_session.not_found': 'Sign-in session not found.',
6
+ not_authenticated: 'Not authenticated.',
7
+ fetch_user_info_failed: 'Unable to fetch user info. The access token may be invalid.',
8
+ });
9
+ class LogtoClientError extends Error {
10
+ constructor(code, data) {
11
+ super(logtoClientErrorCodes[code]);
12
+ this.code = code;
13
+ this.data = data;
14
+ }
15
+ }
16
+
17
+ exports.LogtoClientError = LogtoClientError;
@@ -0,0 +1,13 @@
1
+ declare const logtoClientErrorCodes: Readonly<{
2
+ 'sign_in_session.invalid': "Invalid sign-in session.";
3
+ 'sign_in_session.not_found': "Sign-in session not found.";
4
+ not_authenticated: "Not authenticated.";
5
+ fetch_user_info_failed: "Unable to fetch user info. The access token may be invalid.";
6
+ }>;
7
+ export type LogtoClientErrorCode = keyof typeof logtoClientErrorCodes;
8
+ export declare class LogtoClientError extends Error {
9
+ code: LogtoClientErrorCode;
10
+ data: unknown;
11
+ constructor(code: LogtoClientErrorCode, data?: unknown);
12
+ }
13
+ export {};
package/lib/errors.js ADDED
@@ -0,0 +1,15 @@
1
+ const logtoClientErrorCodes = Object.freeze({
2
+ 'sign_in_session.invalid': 'Invalid sign-in session.',
3
+ 'sign_in_session.not_found': 'Sign-in session not found.',
4
+ not_authenticated: 'Not authenticated.',
5
+ fetch_user_info_failed: 'Unable to fetch user info. The access token may be invalid.',
6
+ });
7
+ class LogtoClientError extends Error {
8
+ constructor(code, data) {
9
+ super(logtoClientErrorCodes[code]);
10
+ this.code = code;
11
+ this.data = data;
12
+ }
13
+ }
14
+
15
+ export { LogtoClientError };
package/lib/index.cjs ADDED
@@ -0,0 +1,285 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var js = require('@logto/js');
6
+ var jose = require('jose');
7
+ var errors = require('./errors.cjs');
8
+ var index = require('./types/index.cjs');
9
+ var index$1 = require('./utils/index.cjs');
10
+ var once = require('./utils/once.cjs');
11
+ var requester = require('./utils/requester.cjs');
12
+
13
+ class LogtoClient {
14
+ constructor(logtoConfig, adapter) {
15
+ this.getOidcConfig = once.once(this._getOidcConfig);
16
+ this.getJwtVerifyGetKey = once.once(this._getJwtVerifyGetKey);
17
+ this.accessTokenMap = new Map();
18
+ this.logtoConfig = {
19
+ ...logtoConfig,
20
+ prompt: logtoConfig.prompt ?? js.Prompt.Consent,
21
+ scopes: js.withDefaultScopes(logtoConfig.scopes).split(' '),
22
+ };
23
+ this.adapter = adapter;
24
+ void this.loadAccessTokenMap();
25
+ }
26
+ async isAuthenticated() {
27
+ return Boolean(await this.getIdToken());
28
+ }
29
+ async getRefreshToken() {
30
+ return this.adapter.storage.getItem('refreshToken');
31
+ }
32
+ async getIdToken() {
33
+ return this.adapter.storage.getItem('idToken');
34
+ }
35
+ async getAccessToken(resource) {
36
+ if (!(await this.getIdToken())) {
37
+ throw new errors.LogtoClientError('not_authenticated');
38
+ }
39
+ const accessTokenKey = index$1.buildAccessTokenKey(resource);
40
+ const accessToken = this.accessTokenMap.get(accessTokenKey);
41
+ if (accessToken && accessToken.expiresAt > Date.now() / 1000) {
42
+ return accessToken.token;
43
+ }
44
+ // Since the access token has expired, delete it from the map.
45
+ if (accessToken) {
46
+ this.accessTokenMap.delete(accessTokenKey);
47
+ }
48
+ /**
49
+ * Need to fetch a new access token using refresh token.
50
+ */
51
+ return this.getAccessTokenByRefreshToken(resource);
52
+ }
53
+ async getIdTokenClaims() {
54
+ const idToken = await this.getIdToken();
55
+ if (!idToken) {
56
+ throw new errors.LogtoClientError('not_authenticated');
57
+ }
58
+ return js.decodeIdToken(idToken);
59
+ }
60
+ async fetchUserInfo() {
61
+ const { userinfoEndpoint } = await this.getOidcConfig();
62
+ const accessToken = await this.getAccessToken();
63
+ if (!accessToken) {
64
+ throw new errors.LogtoClientError('fetch_user_info_failed');
65
+ }
66
+ return js.fetchUserInfo(userinfoEndpoint, accessToken, this.adapter.requester);
67
+ }
68
+ async signIn(redirectUri, interactionMode) {
69
+ const { appId: clientId, prompt, resources, scopes } = this.logtoConfig;
70
+ const { authorizationEndpoint } = await this.getOidcConfig();
71
+ const codeVerifier = this.adapter.generateCodeVerifier();
72
+ const codeChallenge = await this.adapter.generateCodeChallenge(codeVerifier);
73
+ const state = this.adapter.generateState();
74
+ const signInUri = js.generateSignInUri({
75
+ authorizationEndpoint,
76
+ clientId,
77
+ redirectUri,
78
+ codeChallenge,
79
+ state,
80
+ scopes,
81
+ resources,
82
+ prompt,
83
+ interactionMode,
84
+ });
85
+ await this.setSignInSession({ redirectUri, codeVerifier, state });
86
+ await this.setRefreshToken(null);
87
+ await this.setIdToken(null);
88
+ this.adapter.navigate(signInUri);
89
+ }
90
+ async isSignInRedirected(url) {
91
+ const signInSession = await this.getSignInSession();
92
+ if (!signInSession) {
93
+ return false;
94
+ }
95
+ const { redirectUri } = signInSession;
96
+ const { origin, pathname } = new URL(url);
97
+ return `${origin}${pathname}` === redirectUri;
98
+ }
99
+ async handleSignInCallback(callbackUri) {
100
+ const { logtoConfig, adapter } = this;
101
+ const { requester } = adapter;
102
+ const signInSession = await this.getSignInSession();
103
+ if (!signInSession) {
104
+ throw new errors.LogtoClientError('sign_in_session.not_found');
105
+ }
106
+ const { redirectUri, state, codeVerifier } = signInSession;
107
+ const code = js.verifyAndParseCodeFromCallbackUri(callbackUri, redirectUri, state);
108
+ const { appId: clientId } = logtoConfig;
109
+ const { tokenEndpoint } = await this.getOidcConfig();
110
+ const codeTokenResponse = await js.fetchTokenByAuthorizationCode({
111
+ clientId,
112
+ tokenEndpoint,
113
+ redirectUri,
114
+ codeVerifier,
115
+ code,
116
+ }, requester);
117
+ await this.verifyIdToken(codeTokenResponse.idToken);
118
+ await this.saveCodeToken(codeTokenResponse);
119
+ await this.setSignInSession(null);
120
+ }
121
+ async signOut(postLogoutRedirectUri) {
122
+ const { appId: clientId } = this.logtoConfig;
123
+ const { endSessionEndpoint, revocationEndpoint } = await this.getOidcConfig();
124
+ const refreshToken = await this.getRefreshToken();
125
+ if (refreshToken) {
126
+ try {
127
+ await js.revoke(revocationEndpoint, clientId, refreshToken, this.adapter.requester);
128
+ }
129
+ catch {
130
+ // Do nothing at this point, as we don't want to break the sign-out flow even if the revocation is failed
131
+ }
132
+ }
133
+ const url = js.generateSignOutUri({
134
+ endSessionEndpoint,
135
+ postLogoutRedirectUri,
136
+ clientId,
137
+ });
138
+ this.accessTokenMap.clear();
139
+ await this.setRefreshToken(null);
140
+ await this.setIdToken(null);
141
+ await this.adapter.storage.removeItem('accessToken');
142
+ this.adapter.navigate(url);
143
+ }
144
+ async getSignInSession() {
145
+ const jsonItem = await this.adapter.storage.getItem('signInSession');
146
+ if (!jsonItem) {
147
+ return null;
148
+ }
149
+ const item = JSON.parse(jsonItem);
150
+ if (!index.isLogtoSignInSessionItem(item)) {
151
+ throw new errors.LogtoClientError('sign_in_session.invalid');
152
+ }
153
+ return item;
154
+ }
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);
162
+ }
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);
169
+ }
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);
176
+ }
177
+ async getAccessTokenByRefreshToken(resource) {
178
+ const currentRefreshToken = await this.getRefreshToken();
179
+ if (!currentRefreshToken) {
180
+ throw new errors.LogtoClientError('not_authenticated');
181
+ }
182
+ const accessTokenKey = index$1.buildAccessTokenKey(resource);
183
+ const { appId: clientId } = this.logtoConfig;
184
+ const { tokenEndpoint } = await this.getOidcConfig();
185
+ const { accessToken, refreshToken, idToken, scope, expiresIn } = await js.fetchTokenByRefreshToken({
186
+ clientId,
187
+ tokenEndpoint,
188
+ refreshToken: currentRefreshToken,
189
+ resource,
190
+ }, this.adapter.requester);
191
+ this.accessTokenMap.set(accessTokenKey, {
192
+ token: accessToken,
193
+ scope,
194
+ expiresAt: Math.round(Date.now() / 1000) + expiresIn,
195
+ });
196
+ await this.saveAccessTokenMap();
197
+ await this.setRefreshToken(refreshToken);
198
+ if (idToken) {
199
+ await this.verifyIdToken(idToken);
200
+ await this.setIdToken(idToken);
201
+ }
202
+ return accessToken;
203
+ }
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
+ async verifyIdToken(idToken) {
214
+ const { appId } = this.logtoConfig;
215
+ const { issuer } = await this.getOidcConfig();
216
+ const jwtVerifyGetKey = await this.getJwtVerifyGetKey();
217
+ await js.verifyIdToken(idToken, appId, issuer, jwtVerifyGetKey);
218
+ }
219
+ async saveCodeToken({ refreshToken, idToken, scope, accessToken, expiresIn, }) {
220
+ await this.setRefreshToken(refreshToken ?? null);
221
+ await this.setIdToken(idToken);
222
+ // NOTE: Will add scope to accessTokenKey when needed. (Linear issue LOG-1589)
223
+ const accessTokenKey = index$1.buildAccessTokenKey();
224
+ const expiresAt = Date.now() / 1000 + expiresIn;
225
+ this.accessTokenMap.set(accessTokenKey, { token: accessToken, scope, expiresAt });
226
+ await this.saveAccessTokenMap();
227
+ }
228
+ async saveAccessTokenMap() {
229
+ const data = {};
230
+ for (const [key, accessToken] of this.accessTokenMap.entries()) {
231
+ // eslint-disable-next-line @silverhand/fp/no-mutation
232
+ data[key] = accessToken;
233
+ }
234
+ await this.adapter.storage.setItem('accessToken', JSON.stringify(data));
235
+ }
236
+ async loadAccessTokenMap() {
237
+ const raw = await this.adapter.storage.getItem('accessToken');
238
+ if (!raw) {
239
+ return;
240
+ }
241
+ try {
242
+ const json = JSON.parse(raw);
243
+ if (!index.isLogtoAccessTokenMap(json)) {
244
+ return;
245
+ }
246
+ this.accessTokenMap.clear();
247
+ for (const [key, accessToken] of Object.entries(json)) {
248
+ this.accessTokenMap.set(key, accessToken);
249
+ }
250
+ }
251
+ catch (error) {
252
+ console.warn(error);
253
+ }
254
+ }
255
+ }
256
+
257
+ Object.defineProperty(exports, 'LogtoError', {
258
+ enumerable: true,
259
+ get: function () { return js.LogtoError; }
260
+ });
261
+ Object.defineProperty(exports, 'LogtoRequestError', {
262
+ enumerable: true,
263
+ get: function () { return js.LogtoRequestError; }
264
+ });
265
+ Object.defineProperty(exports, 'OidcError', {
266
+ enumerable: true,
267
+ get: function () { return js.OidcError; }
268
+ });
269
+ Object.defineProperty(exports, 'Prompt', {
270
+ enumerable: true,
271
+ get: function () { return js.Prompt; }
272
+ });
273
+ Object.defineProperty(exports, 'ReservedScope', {
274
+ enumerable: true,
275
+ get: function () { return js.ReservedScope; }
276
+ });
277
+ Object.defineProperty(exports, 'UserScope', {
278
+ enumerable: true,
279
+ get: function () { return js.UserScope; }
280
+ });
281
+ exports.LogtoClientError = errors.LogtoClientError;
282
+ exports.isLogtoAccessTokenMap = index.isLogtoAccessTokenMap;
283
+ exports.isLogtoSignInSessionItem = index.isLogtoSignInSessionItem;
284
+ exports.createRequester = requester.createRequester;
285
+ exports.default = LogtoClient;
package/lib/index.d.ts CHANGED
@@ -1,61 +1,17 @@
1
- import { Requester, Prompt, IdTokenClaims, UserInfoResponse, InteractionMode } from "@logto/js";
2
- import { Nullable, NormalizeKeyPaths } 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
- 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
- };
18
- declare const logtoClientErrorCodes: Readonly<{
19
- sign_in_session: {
20
- invalid: string;
21
- not_found: string;
22
- };
23
- not_authenticated: "Not authenticated.";
24
- fetch_user_info_failed: "Unable to fetch user info. The access token may be invalid.";
25
- }>;
26
- export type LogtoClientErrorCode = NormalizeKeyPaths<typeof logtoClientErrorCodes>;
27
- export class LogtoClientError extends Error {
28
- code: LogtoClientErrorCode;
29
- data: unknown;
30
- constructor(code: LogtoClientErrorCode, data?: unknown);
31
- }
32
- export type LogtoConfig = {
33
- endpoint: string;
34
- appId: string;
35
- appSecret?: string;
36
- scopes?: string[];
37
- resources?: string[];
38
- prompt?: Prompt;
39
- };
40
- export type AccessToken = {
41
- token: string;
42
- scope: string;
43
- expiresAt: number;
44
- };
45
- export const isLogtoSignInSessionItem: (data: unknown) => data is LogtoSignInSessionItem;
46
- export const isLogtoAccessTokenMap: (data: unknown) => data is Record<string, AccessToken>;
47
- export type LogtoSignInSessionItem = {
48
- redirectUri: string;
49
- codeVerifier: string;
50
- state: string;
51
- };
52
- export const createRequester: (fetchFunction: typeof fetch) => Requester;
1
+ import type { IdTokenClaims, UserInfoResponse, InteractionMode } from '@logto/js';
2
+ import type { Nullable } from '@silverhand/essentials';
3
+ import type { ClientAdapter } from './adapter.js';
4
+ import type { AccessToken, LogtoConfig, LogtoSignInSessionItem } from './types/index.js';
53
5
  export type { IdTokenClaims, LogtoErrorCode, UserInfoResponse, InteractionMode } from '@logto/js';
54
6
  export { LogtoError, OidcError, Prompt, LogtoRequestError, ReservedScope, UserScope, } from '@logto/js';
7
+ export * from './errors.js';
8
+ export type { Storage, StorageKey, ClientAdapter } from './adapter.js';
9
+ export { createRequester } from './utils/index.js';
10
+ export * from './types/index.js';
55
11
  export default class LogtoClient {
56
12
  protected readonly logtoConfig: LogtoConfig;
57
- protected readonly getOidcConfig: () => Promise<import("@silverhand/essentials").KeysToCamelCase<import("@logto/js").OidcConfigSnakeCaseResponse>>;
58
- protected readonly getJwtVerifyGetKey: () => Promise<import("jose/dist/types/types").GetKeyFunction<import("jose").JWSHeaderParameters, import("jose").FlattenedJWSInput>>;
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>>;
59
15
  protected readonly adapter: ClientAdapter;
60
16
  protected readonly accessTokenMap: Map<string, AccessToken>;
61
17
  constructor(logtoConfig: LogtoConfig, adapter: ClientAdapter);
@@ -71,6 +27,13 @@ export default class LogtoClient {
71
27
  signOut(postLogoutRedirectUri?: string): Promise<void>;
72
28
  protected getSignInSession(): Promise<Nullable<LogtoSignInSessionItem>>;
73
29
  protected setSignInSession(logtoSignInSessionItem: Nullable<LogtoSignInSessionItem>): Promise<void>;
30
+ private setIdToken;
31
+ private setRefreshToken;
32
+ private getAccessTokenByRefreshToken;
33
+ private _getOidcConfig;
34
+ private _getJwtVerifyGetKey;
35
+ private verifyIdToken;
36
+ private saveCodeToken;
37
+ private saveAccessTokenMap;
38
+ private loadAccessTokenMap;
74
39
  }
75
-
76
- //# sourceMappingURL=index.d.ts.map